Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
HMAC_SECRET=""
DATABASE_URL=""
LEMON_SQUEEZY_API_KEY=""
LEMON_SQUEEZY_STORE_ID=""
LEMON_SQUEEZY_VARIANT_ID=""
LEMON_SQUEEZY_WEBHOOK_SECRET=""
HMAC_SECRET="YOUR_VERY_SECRET_CODE_GOES_HERE" # For max security, use "openssl rand -base64 64" to generate one
DATABASE_URL="YOUR_NOT_SO_SECRET_DATABASE_URL_GOES_HERE"
LEMON_SQUEEZY_API_KEY="" # Just get it from somewhere
LEMON_SQUEEZY_STORE_ID="" # Just get it from somewhere
LEMON_SQUEEZY_VARIANT_ID="" # Just get it from somewhere
226 changes: 113 additions & 113 deletions src/routes/gRPC/events/registerEvent.ts
Original file line number Diff line number Diff line change
@@ -1,113 +1,113 @@
import type {
RegisterEventRequest,
RegisterEventResponse,
} from "../../../gen/event/v1/event_pb";
import { RegisterEventResponseSchema } from "../../../gen/event/v1/event_pb";
import { create } from "@bufbuild/protobuf";
import { eventSchema } from "../../../zod/event";
import { type EventType } from "../../../interface/event/Event";
import { SDKCall } from "../../../events/RawEvents/SDKCall";
import { EventError } from "../../../errors/event";
import { AuthError } from "../../../errors/auth";
import { ZodError } from "zod";
import { StorageAdapterFactory } from "../../../factory";
import type { HandlerContext } from "@connectrpc/connect";
import { apiKeyContextKey } from "../../../context/auth";
import { logger } from "../../../errors/logger";

const OPERATION = "RegisterEvent";

export async function registerEvent(
req: RegisterEventRequest,
context: HandlerContext,
): Promise<RegisterEventResponse> {
try {
// Get API key ID from context (set by auth interceptor)
const apiKeyId = context.values.get(apiKeyContextKey);
if (!apiKeyId) {
throw AuthError.invalidAPIKey("API key ID not found in context");
}

logger.logOperationInfo(OPERATION, "authenticated", "Request authenticated", {
apiKeyId,
});

// Validate the incoming request against the schema
let eventSkeleton;
try {
eventSkeleton = await eventSchema.parseAsync(req);
} catch (error) {
if (error instanceof EventError) {
throw error;
}
if (error instanceof ZodError) {
const issues = error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join("; ");
throw EventError.validationFailed(issues, error);
}
throw EventError.validationFailed(
"Unknown validation error",
error as Error,
);
}

// Create the appropriate event based on type
let event: EventType;

try {
switch (eventSkeleton.type) {
case "SDK_CALL":
event = new SDKCall(eventSkeleton.userId, eventSkeleton.data);
break;
default:
throw EventError.unsupportedEventType(eventSkeleton.type);
}
} catch (error) {
if (error instanceof EventError) {
throw error;
}
throw EventError.unknown(error as Error);
}

// Get the storage adapter and persist the event
try {
const adapter = await StorageAdapterFactory.getStorageAdapter(
event,
apiKeyId,
);
await adapter.add();
} catch (error) {
throw EventError.serializationError(
"Failed to store event",
error as Error,
);
}

logger.logOperationInfo(OPERATION, "completed", "Event stored successfully", {
apiKeyId,
userId: eventSkeleton.userId,
});

return create(RegisterEventResponseSchema, {
random: "Event stored successfully",
});
} catch (error) {
logger.logOperationError(
OPERATION,
"failed",
error instanceof EventError ? error.type : "UNKNOWN",
"RegisterEvent handler failed",
error instanceof Error ? error : undefined,
{ apiKeyId: context.values.get(apiKeyContextKey) },
);

// Re-throw EventError as-is
if (error instanceof EventError) {
throw error;
}

// Wrap unexpected errors
throw EventError.unknown(error as Error);
}
}
import type {
RegisterEventRequest,
RegisterEventResponse,
} from "../../../gen/event/v1/event_pb";
import { RegisterEventResponseSchema } from "../../../gen/event/v1/event_pb";
import { create } from "@bufbuild/protobuf";
import { eventSchema } from "../../../zod/event";
import { type EventType } from "../../../interface/event/Event";
import { SDKCall } from "../../../events/RawEvents/SDKCall";
import { EventError } from "../../../errors/event";
import { AuthError } from "../../../errors/auth";
import { ZodError } from "zod";
import { StorageAdapterFactory } from "../../../factory";
import type { HandlerContext } from "@connectrpc/connect";
import { apiKeyContextKey } from "../../../context/auth";
import { logger } from "../../../errors/logger";
const OPERATION = "RegisterEvent";
export async function registerEvent(
req: RegisterEventRequest,
context: HandlerContext,
): Promise<RegisterEventResponse> {
try {
// Get API key ID from context (set by auth interceptor)
const apiKeyId = context.values.get(apiKeyContextKey);
if (!apiKeyId) {
throw AuthError.invalidAPIKey("API key ID not found in context");
}
logger.logOperationInfo(OPERATION, "authenticated", "Request authenticated", {
apiKeyId,
});
// Validate the incoming request against the schema
let eventSkeleton;
try {
eventSkeleton = await eventSchema.parseAsync(req);
} catch (error) {
if (error instanceof EventError) {
throw error;
}
if (error instanceof ZodError) {
const issues = error.issues
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
.join("; ");
throw EventError.validationFailed(issues, error);
}
throw EventError.validationFailed(
"Unknown validation error",
error as Error,
);
}
// Create the appropriate event based on type
let event: EventType;
try {
switch (eventSkeleton.type) {
case "SDK_CALL":
event = new SDKCall(eventSkeleton.userId, eventSkeleton.data);
break;
default:
throw EventError.unsupportedEventType(eventSkeleton.type);
}
} catch (error) {
if (error instanceof EventError) {
throw error;
}
throw EventError.unknown(error as Error);
}
// Get the storage adapter and persist the event
try {
const adapter = await StorageAdapterFactory.getStorageAdapter(
event,
apiKeyId,
);
await adapter.add();
} catch (error) {
throw EventError.serializationError(
"Failed to store event",
error as Error,
);
}
logger.logOperationInfo(OPERATION, "completed", "Event stored successfully", {
apiKeyId,
userId: eventSkeleton.userId,
});
return create(RegisterEventResponseSchema, {
random: "Event stored successfully",
});
} catch (error) {
logger.logOperationError(
OPERATION,
"failed",
error instanceof EventError ? error.type : "UNKNOWN",
"RegisterEvent handler failed",
error instanceof Error ? error : undefined,
{ apiKeyId: context.values.get(apiKeyContextKey) },
);
// Re-throw EventError as-is
if (error instanceof EventError) {
throw error;
}
// Wrap unexpected errors
throw EventError.unknown(error as Error);
}
}
7 changes: 4 additions & 3 deletions src/routes/http/createdCheckout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import crypto from "node:crypto";
import { lemonSqueezySetup } from "@lemonsqueezy/lemonsqueezy.js";
import { Payment } from "../../events/RawEvents/Payment.ts";
Expand Down Expand Up @@ -87,7 +88,7 @@ function verifyWebhookSignature(
/**
* Reads the request body as a string
*/
function readBody(req: IncomingMessage): Promise<string> {
function readBody(req: IncomingMessage | Http2ServerRequest): Promise<string> {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => {
Expand All @@ -106,8 +107,8 @@ function readBody(req: IncomingMessage): Promise<string> {
* Handles the Lemon Squeezy order-created webhook
*/
export async function handleLemonSqueezyWebhook(
req: IncomingMessage,
res: ServerResponse,
req: IncomingMessage | Http2ServerRequest,
res: ServerResponse | Http2ServerResponse,
): Promise<void> {
try {
logger.logOperationInfo(OPERATION, "start", "Processing webhook request", {});
Expand Down
10 changes: 5 additions & 5 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as http from "node:http";
import * as http2 from "node:http2";
import type { ConnectRouter } from "@connectrpc/connect";
import { connectNodeAdapter } from "@connectrpc/connect-node";
import { createValidateInterceptor } from "@connectrpc/validate";
Expand Down Expand Up @@ -47,8 +47,8 @@ const grpcHandler = connectNodeAdapter({

// Create a combined handler for both gRPC and HTTP webhooks
const requestHandler = (
req: http.IncomingMessage,
res: http.ServerResponse,
req: http2.Http2ServerRequest,
res: http2.Http2ServerResponse,
) => {
// Handle webhook endpoint
if (
Expand All @@ -63,6 +63,6 @@ const requestHandler = (
grpcHandler(req, res);
};

http.createServer(requestHandler).listen(8069);
console.log("Server listening on http://localhost:8069");
http2.createServer(requestHandler).listen(8069);
console.log("Server listening on http://localhost:8069 (HTTP/2)");
console.log("Webhook endpoint: http://localhost:8069/webhooks/lemonsqueezy");