@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.
- package/dist/aws-event-synthesis/index.d.ts +25 -0
- package/dist/aws-event-synthesis/index.d.ts.map +1 -0
- package/dist/aws-event-synthesis/index.js +90 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/local-server/event-synthesizer.d.ts +7 -0
- package/dist/local-server/event-synthesizer.d.ts.map +1 -0
- package/dist/local-server/event-synthesizer.js +62 -0
- package/dist/local-server/index.d.ts +17 -0
- package/dist/local-server/index.d.ts.map +1 -0
- package/dist/local-server/index.js +66 -0
- package/dist/local-server/types.d.ts +14 -0
- package/dist/local-server/types.d.ts.map +1 -0
- package/dist/local-server/types.js +2 -0
- package/dist/websocket/action-router.d.ts +17 -0
- package/dist/websocket/action-router.d.ts.map +1 -0
- package/dist/websocket/action-router.js +48 -0
- package/dist/websocket/connection-registry.d.ts +28 -0
- package/dist/websocket/connection-registry.d.ts.map +1 -0
- package/dist/websocket/connection-registry.js +59 -0
- package/dist/websocket/event-synthesizer.d.ts +24 -0
- package/dist/websocket/event-synthesizer.d.ts.map +1 -0
- package/dist/websocket/event-synthesizer.js +103 -0
- package/dist/websocket/index.d.ts +20 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +132 -0
- package/dist/websocket/local-gateway-client.d.ts +15 -0
- package/dist/websocket/local-gateway-client.d.ts.map +1 -0
- package/dist/websocket/local-gateway-client.js +31 -0
- package/dist/websocket/types.d.ts +55 -0
- package/dist/websocket/types.d.ts.map +1 -0
- package/dist/websocket/types.js +2 -0
- package/package.json +38 -0
- package/src/aws-event-synthesis/index.ts +99 -0
- package/src/index.ts +21 -0
- package/src/local-server/event-synthesizer.ts +71 -0
- package/src/local-server/index.ts +94 -0
- package/src/local-server/types.ts +35 -0
- package/src/websocket/action-router.ts +66 -0
- package/src/websocket/connection-registry.ts +67 -0
- package/src/websocket/event-synthesizer.ts +131 -0
- package/src/websocket/index.ts +146 -0
- package/src/websocket/local-gateway-client.ts +31 -0
- package/src/websocket/types.ts +66 -0
- 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"}
|
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
|
+
}
|