@mono-labs/dev 0.1.251

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/aws-event-synthesis/index.d.ts +25 -0
  2. package/dist/aws-event-synthesis/index.d.ts.map +1 -0
  3. package/dist/aws-event-synthesis/index.js +90 -0
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +15 -0
  7. package/dist/local-server/event-synthesizer.d.ts +7 -0
  8. package/dist/local-server/event-synthesizer.d.ts.map +1 -0
  9. package/dist/local-server/event-synthesizer.js +62 -0
  10. package/dist/local-server/index.d.ts +17 -0
  11. package/dist/local-server/index.d.ts.map +1 -0
  12. package/dist/local-server/index.js +66 -0
  13. package/dist/local-server/types.d.ts +14 -0
  14. package/dist/local-server/types.d.ts.map +1 -0
  15. package/dist/local-server/types.js +2 -0
  16. package/dist/websocket/action-router.d.ts +17 -0
  17. package/dist/websocket/action-router.d.ts.map +1 -0
  18. package/dist/websocket/action-router.js +48 -0
  19. package/dist/websocket/connection-registry.d.ts +28 -0
  20. package/dist/websocket/connection-registry.d.ts.map +1 -0
  21. package/dist/websocket/connection-registry.js +59 -0
  22. package/dist/websocket/event-synthesizer.d.ts +24 -0
  23. package/dist/websocket/event-synthesizer.d.ts.map +1 -0
  24. package/dist/websocket/event-synthesizer.js +103 -0
  25. package/dist/websocket/index.d.ts +20 -0
  26. package/dist/websocket/index.d.ts.map +1 -0
  27. package/dist/websocket/index.js +132 -0
  28. package/dist/websocket/local-gateway-client.d.ts +15 -0
  29. package/dist/websocket/local-gateway-client.d.ts.map +1 -0
  30. package/dist/websocket/local-gateway-client.js +31 -0
  31. package/dist/websocket/types.d.ts +55 -0
  32. package/dist/websocket/types.d.ts.map +1 -0
  33. package/dist/websocket/types.js +2 -0
  34. package/package.json +38 -0
  35. package/src/aws-event-synthesis/index.ts +99 -0
  36. package/src/index.ts +21 -0
  37. package/src/local-server/event-synthesizer.ts +71 -0
  38. package/src/local-server/index.ts +94 -0
  39. package/src/local-server/types.ts +35 -0
  40. package/src/websocket/action-router.ts +66 -0
  41. package/src/websocket/connection-registry.ts +67 -0
  42. package/src/websocket/event-synthesizer.ts +131 -0
  43. package/src/websocket/index.ts +146 -0
  44. package/src/websocket/local-gateway-client.ts +31 -0
  45. package/src/websocket/types.ts +66 -0
  46. package/tsconfig.json +19 -0
@@ -0,0 +1,20 @@
1
+ import type { WebSocket, WebSocketServer } from 'ws';
2
+ import { ActionRouter } from './action-router';
3
+ import { ConnectionRegistry } from './connection-registry';
4
+ import type { SocketAdapterConfig } from './types';
5
+ export type { ConnectionId, PostToConnectionFn, SocketAdapterConfig } from './types';
6
+ export { ConnectionRegistry } from './connection-registry';
7
+ export { LocalGatewayClient } from './local-gateway-client';
8
+ export { ActionRouter } from './action-router';
9
+ /**
10
+ * Attaches a full socket adapter to a WebSocketServer instance.
11
+ * Maps 1:1 to each API Gateway WebSocket event so local dev
12
+ * behaves identically to the deployed system.
13
+ */
14
+ export declare function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapterConfig): {
15
+ postToConnection: import("./types").PostToConnectionFn;
16
+ connectionRegistry: ConnectionRegistry;
17
+ actionRouter: ActionRouter;
18
+ getConnectionId: (ws: WebSocket) => string | undefined;
19
+ };
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/websocket/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAA;AAEpD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAG1D,OAAO,KAAK,EAAgB,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAEhE,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE9C;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,mBAAmB;;;;0BA6H9D,SAAS;EAEhC"}
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActionRouter = exports.LocalGatewayClient = exports.ConnectionRegistry = void 0;
4
+ exports.attachSocketAdapter = attachSocketAdapter;
5
+ const action_router_1 = require("./action-router");
6
+ const connection_registry_1 = require("./connection-registry");
7
+ const event_synthesizer_1 = require("./event-synthesizer");
8
+ const local_gateway_client_1 = require("./local-gateway-client");
9
+ var connection_registry_2 = require("./connection-registry");
10
+ Object.defineProperty(exports, "ConnectionRegistry", { enumerable: true, get: function () { return connection_registry_2.ConnectionRegistry; } });
11
+ var local_gateway_client_2 = require("./local-gateway-client");
12
+ Object.defineProperty(exports, "LocalGatewayClient", { enumerable: true, get: function () { return local_gateway_client_2.LocalGatewayClient; } });
13
+ var action_router_2 = require("./action-router");
14
+ Object.defineProperty(exports, "ActionRouter", { enumerable: true, get: function () { return action_router_2.ActionRouter; } });
15
+ /**
16
+ * Attaches a full socket adapter to a WebSocketServer instance.
17
+ * Maps 1:1 to each API Gateway WebSocket event so local dev
18
+ * behaves identically to the deployed system.
19
+ */
20
+ function attachSocketAdapter(wss, config) {
21
+ const debug = config?.debug ?? false;
22
+ const domainName = config?.domainName ?? 'localhost';
23
+ const stage = config?.stage ?? 'local';
24
+ const connectionRegistry = new connection_registry_1.ConnectionRegistry();
25
+ const gatewayClient = new local_gateway_client_1.LocalGatewayClient(connectionRegistry);
26
+ const postToConnection = gatewayClient.asFunction();
27
+ const actionRouter = new action_router_1.ActionRouter();
28
+ // Register consumer-provided routes
29
+ if (config?.routes) {
30
+ for (const [action, handler] of Object.entries(config.routes)) {
31
+ actionRouter.addRoute(action, handler);
32
+ }
33
+ }
34
+ // $default handler for unknown actions
35
+ if (config?.defaultHandler) {
36
+ actionRouter.setDefaultHandler(config.defaultHandler);
37
+ }
38
+ else {
39
+ actionRouter.setDefaultHandler(async (body, ctx) => {
40
+ let action = 'unknown';
41
+ try {
42
+ action = JSON.parse(body).action ?? 'unknown';
43
+ }
44
+ catch { }
45
+ return {
46
+ statusCode: 400,
47
+ body: JSON.stringify({ error: `Unknown action: ${action}` }),
48
+ };
49
+ });
50
+ }
51
+ // Use consumer-provided handlers or sensible defaults
52
+ const connectHandler = config?.connectHandler ?? (async () => ({
53
+ response: { statusCode: 200 },
54
+ userContext: undefined,
55
+ }));
56
+ const disconnectHandler = config?.disconnectHandler ?? (async () => { });
57
+ // Reverse lookup: WebSocket → connectionId
58
+ const wsToConnectionId = new WeakMap();
59
+ wss.on('connection', async (ws, req) => {
60
+ // 1. Extract token from query string
61
+ const url = new URL(req.url ?? '', `http://${req.headers.host ?? 'localhost'}`);
62
+ const token = url.searchParams.get('token') ?? undefined;
63
+ // 2. Register connection and assign connectionId
64
+ const connectionId = connectionRegistry.register(ws);
65
+ wsToConnectionId.set(ws, connectionId);
66
+ if (debug)
67
+ console.log(`[socket-adapter] $connect connectionId=${connectionId}`);
68
+ if (debug)
69
+ console.log(`[socket-adapter] token ${token ? 'present' : 'MISSING'}`);
70
+ // 3. Authenticate via connectHandler
71
+ const { response, userContext } = await connectHandler(connectionId, { token });
72
+ if (debug)
73
+ console.log(`[socket-adapter] connectHandler result:`, response);
74
+ if (debug && userContext) {
75
+ console.log(`[socket-adapter] authenticated userId=${userContext.userId} orgId=${userContext.organizationId}`);
76
+ }
77
+ // 4. Reject if connectHandler returns non-200
78
+ if (response.statusCode !== 200) {
79
+ if (debug)
80
+ console.log(`[socket-adapter] rejected connectionId=${connectionId} status=${response.statusCode}`);
81
+ ws.close(1008, 'Authentication failed');
82
+ connectionRegistry.unregister(connectionId);
83
+ wsToConnectionId.delete(ws);
84
+ return;
85
+ }
86
+ // 5. Store user context in the registry (if provided)
87
+ if (userContext) {
88
+ connectionRegistry.setUserContext(connectionId, userContext);
89
+ }
90
+ // 6. Send welcome message to client
91
+ const welcomeMessage = { type: 'connected', connectionId };
92
+ if (userContext)
93
+ welcomeMessage.userId = userContext.userId;
94
+ ws.send(JSON.stringify(welcomeMessage));
95
+ if (debug)
96
+ console.log(`[socket-adapter] welcome sent to ${connectionId}${userContext ? ` userId=${userContext.userId}` : ''}`);
97
+ // 7. Route incoming messages through ActionRouter
98
+ ws.on('message', async (raw) => {
99
+ const rawBody = raw.toString();
100
+ if (debug)
101
+ console.log(`[socket-adapter] message from ${connectionId}:`, rawBody);
102
+ const requestContext = (0, event_synthesizer_1.buildRequestContext)(connectionId, '$default', 'MESSAGE', {
103
+ domainName,
104
+ stage,
105
+ });
106
+ const resolvedUserContext = userContext ?? connectionRegistry.getUserContext(connectionId) ?? {
107
+ userId: 'anonymous',
108
+ organizationId: 'anonymous',
109
+ };
110
+ const result = await actionRouter.route(connectionId, rawBody, postToConnection, requestContext, resolvedUserContext);
111
+ // Send the handler result back to the sender
112
+ if (result.body) {
113
+ ws.send(result.body);
114
+ }
115
+ });
116
+ // 8. Handle disconnect
117
+ ws.on('close', async (code, reason) => {
118
+ if (debug) {
119
+ console.log(`[socket-adapter] $disconnect connectionId=${connectionId} code=${code} reason=${reason.toString()}`);
120
+ }
121
+ await disconnectHandler(connectionId);
122
+ connectionRegistry.unregister(connectionId);
123
+ wsToConnectionId.delete(ws);
124
+ });
125
+ });
126
+ return {
127
+ postToConnection,
128
+ connectionRegistry,
129
+ actionRouter,
130
+ getConnectionId: (ws) => wsToConnectionId.get(ws),
131
+ };
132
+ }
@@ -0,0 +1,15 @@
1
+ import type { ConnectionRegistry } from './connection-registry';
2
+ import type { ConnectionId, PostToConnectionFn } from './types';
3
+ /**
4
+ * Local replacement for @aws-sdk/client-apigatewaymanagementapi.
5
+ * Looks up the WebSocket in ConnectionRegistry and sends data directly.
6
+ */
7
+ export declare class LocalGatewayClient {
8
+ private registry;
9
+ constructor(registry: ConnectionRegistry);
10
+ /** Send data to a specific connection. Throws GoneException (410) if not found. */
11
+ postToConnection(connectionId: ConnectionId, data: unknown): Promise<void>;
12
+ /** Returns a bound PostToConnectionFn for dependency injection */
13
+ asFunction(): PostToConnectionFn;
14
+ }
15
+ //# sourceMappingURL=local-gateway-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-gateway-client.d.ts","sourceRoot":"","sources":["../../src/websocket/local-gateway-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAE/D;;;GAGG;AACH,qBAAa,kBAAkB;IAClB,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,kBAAkB;IAEhD,mFAAmF;IAC7E,gBAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAchF,kEAAkE;IAClE,UAAU,IAAI,kBAAkB;CAGhC"}
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalGatewayClient = void 0;
4
+ const ws_1 = require("ws");
5
+ /**
6
+ * Local replacement for @aws-sdk/client-apigatewaymanagementapi.
7
+ * Looks up the WebSocket in ConnectionRegistry and sends data directly.
8
+ */
9
+ class LocalGatewayClient {
10
+ registry;
11
+ constructor(registry) {
12
+ this.registry = registry;
13
+ }
14
+ /** Send data to a specific connection. Throws GoneException (410) if not found. */
15
+ async postToConnection(connectionId, data) {
16
+ const ws = this.registry.get(connectionId);
17
+ if (!ws || ws.readyState !== ws_1.WebSocket.OPEN) {
18
+ const error = new Error(`GoneException: Connection ${connectionId} is no longer available`);
19
+ error.statusCode = 410;
20
+ error.name = 'GoneException';
21
+ throw error;
22
+ }
23
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
24
+ ws.send(payload);
25
+ }
26
+ /** Returns a bound PostToConnectionFn for dependency injection */
27
+ asFunction() {
28
+ return this.postToConnection.bind(this);
29
+ }
30
+ }
31
+ exports.LocalGatewayClient = LocalGatewayClient;
@@ -0,0 +1,55 @@
1
+ /** User context attached to a WebSocket connection */
2
+ export interface WebSocketUserContext {
3
+ userId: string;
4
+ organizationId: string;
5
+ [key: string]: unknown;
6
+ }
7
+ /** Unique identifier for a WebSocket connection (mirrors API Gateway connectionId) */
8
+ export type ConnectionId = string;
9
+ /** Function signature for sending data to a connection (replaces PostToConnectionCommand) */
10
+ export type PostToConnectionFn = (connectionId: ConnectionId, data: unknown) => Promise<void>;
11
+ /** Synthesized equivalent of event.requestContext for local dev */
12
+ export interface LocalRequestContext {
13
+ connectionId: ConnectionId;
14
+ domainName: string;
15
+ stage: string;
16
+ routeKey: string;
17
+ eventType: 'CONNECT' | 'DISCONNECT' | 'MESSAGE';
18
+ }
19
+ /** Context passed to action handlers */
20
+ export interface ActionHandlerContext {
21
+ connectionId: ConnectionId;
22
+ requestContext: LocalRequestContext;
23
+ postToConnection: PostToConnectionFn;
24
+ userContext: WebSocketUserContext;
25
+ }
26
+ /** Result returned from an action handler */
27
+ export interface ActionHandlerResult {
28
+ statusCode: number;
29
+ body?: string;
30
+ }
31
+ /** Handler function for a routed action */
32
+ export type ActionHandler = (body: string, ctx: ActionHandlerContext) => Promise<ActionHandlerResult>;
33
+ /** Handler called on $connect — returns response + optional user context */
34
+ export type ConnectHandlerFn = (connectionId: ConnectionId, params: {
35
+ token?: string;
36
+ }) => Promise<{
37
+ response: {
38
+ statusCode: number;
39
+ body?: string;
40
+ };
41
+ userContext?: WebSocketUserContext;
42
+ }>;
43
+ /** Handler called on $disconnect */
44
+ export type DisconnectHandlerFn = (connectionId: ConnectionId) => Promise<void>;
45
+ /** Configuration for the socket adapter */
46
+ export interface SocketAdapterConfig {
47
+ domainName?: string;
48
+ stage?: string;
49
+ debug?: boolean;
50
+ connectHandler?: ConnectHandlerFn;
51
+ disconnectHandler?: DisconnectHandlerFn;
52
+ routes?: Record<string, ActionHandler>;
53
+ defaultHandler?: ActionHandler;
54
+ }
55
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/websocket/types.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,sFAAsF;AACtF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAA;AAEjC,6FAA6F;AAC7F,MAAM,MAAM,kBAAkB,GAAG,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE7F,mEAAmE;AACnE,MAAM,WAAW,mBAAmB;IACnC,YAAY,EAAE,YAAY,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAA;CAC/C;AAED,wCAAwC;AACxC,MAAM,WAAW,oBAAoB;IACpC,YAAY,EAAE,YAAY,CAAA;IAC1B,cAAc,EAAE,mBAAmB,CAAA;IACnC,gBAAgB,EAAE,kBAAkB,CAAA;IACpC,WAAW,EAAE,oBAAoB,CAAA;CACjC;AAED,6CAA6C;AAC7C,MAAM,WAAW,mBAAmB;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACb;AAED,2CAA2C;AAC3C,MAAM,MAAM,aAAa,GAAG,CAC3B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,oBAAoB,KACrB,OAAO,CAAC,mBAAmB,CAAC,CAAA;AAEjC,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAC9B,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KACtB,OAAO,CAAC;IACZ,QAAQ,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC/C,WAAW,CAAC,EAAE,oBAAoB,CAAA;CAClC,CAAC,CAAA;AAEF,oCAAoC;AACpC,MAAM,MAAM,mBAAmB,GAAG,CAAC,YAAY,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE/E,2CAA2C;AAC3C,MAAM,WAAW,mBAAmB;IACnC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,cAAc,CAAC,EAAE,gBAAgB,CAAA;IACjC,iBAAiB,CAAC,EAAE,mBAAmB,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACtC,cAAc,CAAC,EAAE,aAAa,CAAA;CAC9B"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mono-labs/dev",
3
+ "version": "0.1.251",
4
+ "type": "commonjs",
5
+ "description": "Local development server and WebSocket adapter for mono-labs",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "echo \"Error: no test specified\" && exit 1",
18
+ "deploy": "tsc && npm publish --access public --registry https://registry.npmjs.org/",
19
+ "release:patch": "npm version patch -m \"chore: release %s\" && npm publish --access public",
20
+ "release:minor": "npm version minor -m \"chore: release %s\" && npm publish --access public",
21
+ "release:major": "npm version major -m \"chore: release %s\" && npm publish --access public"
22
+ },
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "cors": "^2.8.5",
26
+ "express": "^4.21.0",
27
+ "ws": "^8.18.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/cors": "^2.8.17",
31
+ "@types/express": "^5.0.0",
32
+ "@types/node": "^25.1.0",
33
+ "@types/ws": "^8.5.13",
34
+ "aws-lambda": "^1.0.7",
35
+ "@types/aws-lambda": "^8.10.145",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
@@ -0,0 +1,99 @@
1
+ import type { Context as LambdaContext } from 'aws-lambda'
2
+ import type { Response } from 'express'
3
+
4
+ // --- Header utilities ---
5
+
6
+ /** Flatten Node.js IncomingHttpHeaders to Record<string, string> */
7
+ export function flattenHeaders(
8
+ headers: Record<string, string | string[] | undefined>
9
+ ): Record<string, string> {
10
+ const flat: Record<string, string> = {}
11
+ for (const [key, value] of Object.entries(headers)) {
12
+ if (typeof value === 'string') flat[key] = value
13
+ else if (Array.isArray(value)) flat[key] = value.join(', ')
14
+ }
15
+ return flat
16
+ }
17
+
18
+ /** Serialize a request body to string (passthrough if already string, JSON.stringify if object) */
19
+ export function serializeBody(body: unknown): string | null {
20
+ if (body === undefined || body === null) return null
21
+ if (typeof body === 'string') return body
22
+ return JSON.stringify(body)
23
+ }
24
+
25
+ /** Convert Express parsed query to Record<string, string | undefined> */
26
+ export function extractQueryParams(
27
+ query: Record<string, unknown>
28
+ ): Record<string, string | undefined> {
29
+ const params: Record<string, string | undefined> = {}
30
+ for (const [key, value] of Object.entries(query)) {
31
+ if (typeof value === 'string') params[key] = value
32
+ else if (value !== undefined) params[key] = String(value)
33
+ }
34
+ return params
35
+ }
36
+
37
+ /** Split a URL into path and raw query string */
38
+ export function splitUrl(originalUrl: string): { path: string; queryString: string } {
39
+ const qIndex = originalUrl.indexOf('?')
40
+ if (qIndex === -1) return { path: originalUrl, queryString: '' }
41
+ return {
42
+ path: originalUrl.slice(0, qIndex),
43
+ queryString: originalUrl.slice(qIndex + 1),
44
+ }
45
+ }
46
+
47
+ // --- Mock Lambda context ---
48
+
49
+ /** Generate a local request ID */
50
+ export function generateRequestId(): string {
51
+ return `local-${Date.now()}`
52
+ }
53
+
54
+ /** Create a minimal mock Lambda Context */
55
+ export function createMockLambdaContext(functionName?: string): LambdaContext {
56
+ const name = functionName ?? 'local-handler'
57
+ return {
58
+ callbackWaitsForEmptyEventLoop: true,
59
+ functionName: name,
60
+ functionVersion: '$LATEST',
61
+ invokedFunctionArn: `arn:aws:lambda:us-east-1:000000000000:function:${name}`,
62
+ memoryLimitInMB: '128',
63
+ awsRequestId: generateRequestId(),
64
+ logGroupName: `/aws/lambda/${name}`,
65
+ logStreamName: 'local',
66
+ getRemainingTimeInMillis: () => 30_000,
67
+ done: () => {},
68
+ fail: () => {},
69
+ succeed: () => {},
70
+ }
71
+ }
72
+
73
+ // --- Result -> Express response ---
74
+
75
+ /** Send a Lambda-style result (API Gateway V2 or ALB) back through Express */
76
+ export function sendLambdaResult(
77
+ res: Response,
78
+ result: {
79
+ statusCode?: number
80
+ headers?: Record<string, any>
81
+ body?: string
82
+ isBase64Encoded?: boolean
83
+ }
84
+ ): void {
85
+ const statusCode = result.statusCode ?? 200
86
+
87
+ if (result.headers) {
88
+ for (const [key, value] of Object.entries(result.headers)) {
89
+ res.setHeader(key, value)
90
+ }
91
+ }
92
+
93
+ if (result.isBase64Encoded && result.body) {
94
+ const buffer = Buffer.from(result.body, 'base64')
95
+ res.status(statusCode).send(buffer)
96
+ } else {
97
+ res.status(statusCode).send(result.body ?? '')
98
+ }
99
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Core
2
+ export { LocalServer } from './local-server'
3
+ export type { ApiGatewayHandler, ALBHandler, LocalServerConfig } from './local-server/types'
4
+
5
+ // WebSocket
6
+ export { attachSocketAdapter } from './websocket'
7
+ export { ConnectionRegistry } from './websocket/connection-registry'
8
+ export { ActionRouter } from './websocket/action-router'
9
+ export { LocalGatewayClient } from './websocket/local-gateway-client'
10
+ export type {
11
+ ConnectionId,
12
+ PostToConnectionFn,
13
+ SocketAdapterConfig,
14
+ ConnectHandlerFn,
15
+ DisconnectHandlerFn,
16
+ ActionHandler,
17
+ ActionHandlerContext,
18
+ ActionHandlerResult,
19
+ LocalRequestContext,
20
+ WebSocketUserContext,
21
+ } from './websocket/types'
@@ -0,0 +1,71 @@
1
+ import type { Request } from 'express'
2
+ import type { APIGatewayProxyEventV2, ALBEvent } from 'aws-lambda'
3
+
4
+ import {
5
+ flattenHeaders,
6
+ serializeBody,
7
+ extractQueryParams,
8
+ splitUrl,
9
+ generateRequestId,
10
+ } from '../aws-event-synthesis'
11
+
12
+ /** Synthesize an API Gateway V2 event from an Express request */
13
+ export function synthesizeApiGatewayEvent(req: Request): APIGatewayProxyEventV2 {
14
+ const headers = flattenHeaders(req.headers)
15
+ const body = serializeBody(req.body)
16
+ const queryParams = extractQueryParams(req.query as Record<string, unknown>)
17
+ const { path, queryString } = splitUrl(req.originalUrl)
18
+ const requestId = generateRequestId()
19
+ const now = Date.now()
20
+
21
+ return {
22
+ version: '2.0',
23
+ routeKey: `$default`,
24
+ rawPath: path,
25
+ rawQueryString: queryString,
26
+ headers,
27
+ queryStringParameters: Object.keys(queryParams).length > 0 ? queryParams as Record<string, string> : undefined,
28
+ requestContext: {
29
+ accountId: 'local',
30
+ apiId: 'local',
31
+ domainName: req.hostname ?? 'localhost',
32
+ domainPrefix: 'local',
33
+ http: {
34
+ method: req.method,
35
+ path,
36
+ protocol: req.protocol ?? 'HTTP/1.1',
37
+ sourceIp: req.ip ?? '127.0.0.1',
38
+ userAgent: req.headers['user-agent'] ?? '',
39
+ },
40
+ requestId,
41
+ routeKey: '$default',
42
+ stage: 'local',
43
+ time: new Date(now).toISOString(),
44
+ timeEpoch: now,
45
+ },
46
+ body: body ?? undefined,
47
+ isBase64Encoded: false,
48
+ }
49
+ }
50
+
51
+ /** Synthesize an ALB event from an Express request */
52
+ export function synthesizeALBEvent(req: Request): ALBEvent {
53
+ const headers = flattenHeaders(req.headers)
54
+ const body = serializeBody(req.body)
55
+ const queryParams = extractQueryParams(req.query as Record<string, unknown>)
56
+ const { path } = splitUrl(req.originalUrl)
57
+
58
+ return {
59
+ requestContext: {
60
+ elb: {
61
+ targetGroupArn: 'arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/local/local',
62
+ },
63
+ },
64
+ httpMethod: req.method,
65
+ path,
66
+ headers,
67
+ queryStringParameters: queryParams as Record<string, string | undefined>,
68
+ body: body ?? '',
69
+ isBase64Encoded: false,
70
+ }
71
+ }
@@ -0,0 +1,94 @@
1
+ import http from 'node:http'
2
+
3
+ import express from 'express'
4
+ import cors from 'cors'
5
+ import { WebSocketServer } from 'ws'
6
+
7
+ import { createMockLambdaContext, sendLambdaResult } from '../aws-event-synthesis'
8
+ import { synthesizeApiGatewayEvent, synthesizeALBEvent } from './event-synthesizer'
9
+
10
+ import type { attachSocketAdapter } from '../websocket'
11
+ import type { SocketAdapterConfig } from '../websocket/types'
12
+ import type {
13
+ ApiGatewayHandler,
14
+ ALBHandler,
15
+ LambdaOptionsApiGateway,
16
+ LambdaOptionsALB,
17
+ LocalServerConfig,
18
+ } from './types'
19
+
20
+ export type { ApiGatewayHandler, ALBHandler, LocalServerConfig } from './types'
21
+
22
+ export class LocalServer {
23
+ readonly app: express.Express
24
+ private httpServer: http.Server
25
+ private config: LocalServerConfig
26
+
27
+ constructor(config?: LocalServerConfig) {
28
+ this.config = config ?? {}
29
+ this.app = express()
30
+
31
+ this.app.use(express.json())
32
+ this.app.use(cors())
33
+
34
+ this.app.get('/', (_req, res) => {
35
+ res.send('Hello from Express HTTP Server')
36
+ })
37
+
38
+ this.httpServer = http.createServer(this.app)
39
+ }
40
+
41
+ // --- Type-safe overloads ---
42
+
43
+ lambda(path: string, handler: ApiGatewayHandler): this
44
+ lambda(path: string, handler: ApiGatewayHandler, options: LambdaOptionsApiGateway): this
45
+ lambda(path: string, handler: ALBHandler, options: LambdaOptionsALB): this
46
+ lambda(
47
+ path: string,
48
+ handler: ApiGatewayHandler | ALBHandler,
49
+ options?: LambdaOptionsApiGateway | LambdaOptionsALB,
50
+ ): this {
51
+ const eventType = options?.eventType ?? 'api-gateway'
52
+
53
+ this.app.use(path, async (req: express.Request, res: express.Response) => {
54
+ try {
55
+ const context = createMockLambdaContext()
56
+
57
+ if (eventType === 'alb') {
58
+ const event = synthesizeALBEvent(req)
59
+ const result = await (handler as ALBHandler)(event, context)
60
+ sendLambdaResult(res, result)
61
+ } else {
62
+ const event = synthesizeApiGatewayEvent(req)
63
+ const result = await (handler as ApiGatewayHandler)(event, context)
64
+ sendLambdaResult(res, result ?? {})
65
+ }
66
+ } catch (err) {
67
+ console.error(`[LocalServer] Error handling ${req.method} ${req.originalUrl}:`, err)
68
+ if (!res.headersSent) {
69
+ res.status(500).json({ error: 'Internal Server Error' })
70
+ }
71
+ }
72
+ })
73
+
74
+ return this
75
+ }
76
+
77
+ attachSocket(
78
+ adapterFn: typeof attachSocketAdapter,
79
+ config?: SocketAdapterConfig,
80
+ ): ReturnType<typeof attachSocketAdapter> {
81
+ const wss = new WebSocketServer({ server: this.httpServer })
82
+ return adapterFn(wss, config)
83
+ }
84
+
85
+ listen(port: number, hostname?: string): void {
86
+ const host = hostname ?? '0.0.0.0'
87
+ this.httpServer.listen(port, host, () => {
88
+ if (this.config.debug) {
89
+ console.info(`HTTP Server running at http://localhost:${port}`)
90
+ console.info(`WebSocket server running on ws://localhost:${port}`)
91
+ }
92
+ })
93
+ }
94
+ }
@@ -0,0 +1,35 @@
1
+ import type { Context } from 'aws-lambda'
2
+ import type {
3
+ APIGatewayProxyEventV2,
4
+ APIGatewayProxyStructuredResultV2,
5
+ ALBEvent,
6
+ ALBResult,
7
+ } from 'aws-lambda'
8
+
9
+ // --- Handler types ---
10
+
11
+ export type ApiGatewayHandler = (
12
+ event: APIGatewayProxyEventV2,
13
+ context: Context,
14
+ ) => Promise<APIGatewayProxyStructuredResultV2>
15
+
16
+ export type ALBHandler = (
17
+ event: ALBEvent,
18
+ context: Context,
19
+ ) => Promise<ALBResult>
20
+
21
+ // --- Lambda options (discriminated) ---
22
+
23
+ export interface LambdaOptionsApiGateway {
24
+ eventType?: 'api-gateway'
25
+ }
26
+
27
+ export interface LambdaOptionsALB {
28
+ eventType: 'alb'
29
+ }
30
+
31
+ // --- Server config ---
32
+
33
+ export interface LocalServerConfig {
34
+ debug?: boolean
35
+ }