diff --git a/.env.example b/.env.example index 20710eb..037f2c5 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ -HMAC_SECRET="" -DATABASE_URL="" -LEMON_SQUEEZY_API_KEY="" -LEMON_SQUEEZY_STORE_ID="" -LEMON_SQUEEZY_VARIANT_ID="" -LEMON_SQUEEZY_WEBHOOK_SECRET="" \ No newline at end of file +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/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");