From 9a4a73152313f92dde9efb114f4421b5c9b8c35d Mon Sep 17 00:00:00 2001 From: The Devyash Date: Tue, 16 Dec 2025 16:07:12 +0000 Subject: [PATCH 1/2] docs(env): example Signed-off-by: The Devyash --- .env.example | 10 +++--- src/server.ts | 96 +++++++++++++++++++++++++-------------------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/.env.example b/.env.example index bcbcd2b..037f2c5 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -HMAC_SECRET="" -DATABASE_URL="" -LEMON_SQUEEZY_API_KEY="" -LEMON_SQUEEZY_STORE_ID="" -LEMON_SQUEEZY_VARIANT_ID="" +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 diff --git a/src/server.ts b/src/server.ts index 4fe57f1..d1a4be7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,48 +1,48 @@ -import * as http from "node:http"; -import type { ConnectRouter } from "@connectrpc/connect"; -import { connectNodeAdapter } from "@connectrpc/connect-node"; -import { createValidateInterceptor } from "@connectrpc/validate"; -import { EventService } from "./gen/event/v1/event_pb.ts"; -import { AuthService } from "./gen/auth/v1/auth_pb.ts"; -import { PaymentService } from "./gen/payment/v1/payment_pb.ts"; -import { authInterceptor } from "./interceptors/auth.ts"; -import { registerEvent } from "./routes/events/registerEvent.ts"; -import { createAPIKey } from "./routes/auth/createAPIKey.ts"; -import { createCheckoutLink } from "./routes/payment/createCheckoutLink.ts"; -import { getPostgresDB } from "./storage/db/postgres/db.ts"; - -const DATABASE_URL = process.env.DATABASE_URL; -const HMAC_SECRET = process.env.HMAC_SECRET; - -if (!DATABASE_URL) { - throw new Error("DATABASE_URL is not defined in environment variables"); -} - -if (!HMAC_SECRET) { - throw new Error("HMAC_SECRET environment variable is not set"); -} - -getPostgresDB(DATABASE_URL); - -const handler = connectNodeAdapter({ - interceptors: [createValidateInterceptor(), authInterceptor()], - routes: (router: ConnectRouter) => { - // EventService implementation - router.service(EventService, { - registerEvent, - }); - - // AuthService implementation - router.service(AuthService, { - createAPIKey, - }); - - // PaymentService implementation - router.service(PaymentService, { - createCheckoutLink, - }); - }, -}); - -http.createServer(handler).listen(8000); -console.log("Server listening on http://localhost:8000"); +import * as http from "node:http"; +import type { ConnectRouter } from "@connectrpc/connect"; +import { connectNodeAdapter } from "@connectrpc/connect-node"; +import { createValidateInterceptor } from "@connectrpc/validate"; +import { EventService } from "./gen/event/v1/event_pb.ts"; +import { AuthService } from "./gen/auth/v1/auth_pb.ts"; +import { PaymentService } from "./gen/payment/v1/payment_pb.ts"; +import { authInterceptor } from "./interceptors/auth.ts"; +import { registerEvent } from "./routes/events/registerEvent.ts"; +import { createAPIKey } from "./routes/auth/createAPIKey.ts"; +import { createCheckoutLink } from "./routes/payment/createCheckoutLink.ts"; +import { getPostgresDB } from "./storage/db/postgres/db.ts"; + +const DATABASE_URL = process.env.DATABASE_URL; +const HMAC_SECRET = process.env.HMAC_SECRET; + +if (!DATABASE_URL) { + throw new Error("DATABASE_URL is not defined in environment variables"); +} + +if (!HMAC_SECRET) { + throw new Error("HMAC_SECRET environment variable is not set"); +} + +getPostgresDB(DATABASE_URL); + +const handler = connectNodeAdapter({ + interceptors: [createValidateInterceptor(), authInterceptor()], + routes: (router: ConnectRouter) => { + // EventService implementation + router.service(EventService, { + registerEvent, + }); + + // AuthService implementation + router.service(AuthService, { + createAPIKey, + }); + + // PaymentService implementation + router.service(PaymentService, { + createCheckoutLink, + }); + }, +}); + +http.createServer(handler).listen(8000); +console.log("Server listening on http://localhost:8000"); From c308d6706f9f35a3111141a1e7bbc198ce4950e7 Mon Sep 17 00:00:00 2001 From: The Devyash Date: Tue, 16 Dec 2025 17:32:25 +0000 Subject: [PATCH 2/2] feat(grpc): upgrade to http2 Signed-off-by: The Devyash --- src/routes/gRPC/events/registerEvent.ts | 226 ++++++++++++------------ src/routes/http/createdCheckout.ts | 7 +- src/server.ts | 10 +- 3 files changed, 122 insertions(+), 121 deletions(-) diff --git a/src/routes/gRPC/events/registerEvent.ts b/src/routes/gRPC/events/registerEvent.ts index 04e76e9..62b9a3e 100644 --- a/src/routes/gRPC/events/registerEvent.ts +++ b/src/routes/gRPC/events/registerEvent.ts @@ -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 { - 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 { + 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); + } +} diff --git a/src/routes/http/createdCheckout.ts b/src/routes/http/createdCheckout.ts index d3d083f..385e411 100644 --- a/src/routes/http/createdCheckout.ts +++ b/src/routes/http/createdCheckout.ts @@ -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"; @@ -87,7 +88,7 @@ function verifyWebhookSignature( /** * Reads the request body as a string */ -function readBody(req: IncomingMessage): Promise { +function readBody(req: IncomingMessage | Http2ServerRequest): Promise { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => { @@ -106,8 +107,8 @@ function readBody(req: IncomingMessage): Promise { * Handles the Lemon Squeezy order-created webhook */ export async function handleLemonSqueezyWebhook( - req: IncomingMessage, - res: ServerResponse, + req: IncomingMessage | Http2ServerRequest, + res: ServerResponse | Http2ServerResponse, ): Promise { try { logger.logOperationInfo(OPERATION, "start", "Processing webhook request", {}); diff --git a/src/server.ts b/src/server.ts index ce25764..89f29f1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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"; @@ -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 ( @@ -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");