@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,66 @@
1
+ import type {
2
+ ActionHandler,
3
+ ActionHandlerContext,
4
+ ActionHandlerResult,
5
+ ConnectionId,
6
+ LocalRequestContext,
7
+ PostToConnectionFn,
8
+ } from './types'
9
+ import type { WebSocketUserContext } from './types'
10
+
11
+ /**
12
+ * Routes incoming WebSocket messages by `action` field — mirrors
13
+ * API Gateway's `$request.body.action` route selection.
14
+ */
15
+ export class ActionRouter {
16
+ private routes = new Map<string, ActionHandler>()
17
+ private defaultHandler: ActionHandler | null = null
18
+
19
+ /** Register a handler for a specific action (equivalent to webSocketApi.addRoute()) */
20
+ addRoute(action: string, handler: ActionHandler): void {
21
+ this.routes.set(action, handler)
22
+ }
23
+
24
+ /** Set the $default handler for unknown actions */
25
+ setDefaultHandler(handler: ActionHandler): void {
26
+ this.defaultHandler = handler
27
+ }
28
+
29
+ /** Parse incoming message JSON, extract `action`, and dispatch to the matching handler */
30
+ async route(
31
+ connectionId: ConnectionId,
32
+ rawBody: string,
33
+ postToConnection: PostToConnectionFn,
34
+ requestContext: LocalRequestContext,
35
+ userContext: WebSocketUserContext
36
+ ): Promise<ActionHandlerResult> {
37
+ let parsed: { action?: string; [k: string]: unknown }
38
+ try {
39
+ parsed = JSON.parse(rawBody)
40
+ } catch {
41
+ return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON' }) }
42
+ }
43
+
44
+ const action = parsed.action
45
+ if (!action || typeof action !== 'string') {
46
+ return { statusCode: 400, body: JSON.stringify({ error: 'Missing "action" field' }) }
47
+ }
48
+
49
+ const handler = this.routes.get(action) ?? this.defaultHandler
50
+ if (!handler) {
51
+ return {
52
+ statusCode: 400,
53
+ body: JSON.stringify({ error: `Unknown action: ${action}` }),
54
+ }
55
+ }
56
+
57
+ const ctx: ActionHandlerContext = {
58
+ connectionId,
59
+ requestContext: { ...requestContext, routeKey: action },
60
+ postToConnection,
61
+ userContext,
62
+ }
63
+
64
+ return handler(rawBody, ctx)
65
+ }
66
+ }
@@ -0,0 +1,67 @@
1
+ import type { WebSocket } from 'ws'
2
+ import type { ConnectionId } from './types'
3
+ import type { WebSocketUserContext } from './types'
4
+
5
+ /** Generates a unique connection ID (same pattern as socket-real.ts) */
6
+ const makeConnectionId = (): ConnectionId =>
7
+ `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
8
+
9
+ /**
10
+ * In-memory connection registry — replaces API Gateway's built-in connection management.
11
+ * Maps connectionId → WebSocket instance and connectionId → user context.
12
+ */
13
+ export class ConnectionRegistry {
14
+ private connections = new Map<ConnectionId, WebSocket>()
15
+ private userContexts = new Map<ConnectionId, WebSocketUserContext>()
16
+
17
+ /** Register a WebSocket and return its assigned connectionId */
18
+ register(ws: WebSocket): ConnectionId {
19
+ const connectionId = makeConnectionId()
20
+ this.connections.set(connectionId, ws)
21
+ return connectionId
22
+ }
23
+
24
+ /** Remove a connection by ID */
25
+ unregister(connectionId: ConnectionId): void {
26
+ this.connections.delete(connectionId)
27
+ this.userContexts.delete(connectionId)
28
+ }
29
+
30
+ /** Look up a WebSocket by connectionId */
31
+ get(connectionId: ConnectionId): WebSocket | undefined {
32
+ return this.connections.get(connectionId)
33
+ }
34
+
35
+ /** Return all active connectionIds */
36
+ getAll(): ConnectionId[] {
37
+ return Array.from(this.connections.keys())
38
+ }
39
+
40
+ /** Store user context for a connection */
41
+ setUserContext(connectionId: ConnectionId, ctx: WebSocketUserContext): void {
42
+ this.userContexts.set(connectionId, ctx)
43
+ }
44
+
45
+ /** Retrieve user context for a connection */
46
+ getUserContext(connectionId: ConnectionId): WebSocketUserContext | undefined {
47
+ return this.userContexts.get(connectionId)
48
+ }
49
+
50
+ /** Find all connectionIds belonging to a specific user */
51
+ getConnectionsByUserId(userId: string): ConnectionId[] {
52
+ const result: ConnectionId[] = []
53
+ for (const [connId, ctx] of this.userContexts) {
54
+ if (ctx.userId === userId) result.push(connId)
55
+ }
56
+ return result
57
+ }
58
+
59
+ /** Find all connectionIds belonging to a specific organization */
60
+ getConnectionsByOrgId(orgId: string): ConnectionId[] {
61
+ const result: ConnectionId[] = []
62
+ for (const [connId, ctx] of this.userContexts) {
63
+ if (ctx.organizationId === orgId) result.push(connId)
64
+ }
65
+ return result
66
+ }
67
+ }
@@ -0,0 +1,131 @@
1
+ import type { APIGatewayProxyEvent } from 'aws-lambda'
2
+ import type http from 'node:http'
3
+ import type { ConnectionId, LocalRequestContext } from './types'
4
+
5
+ const EMPTY_MULTI_VALUE: Record<string, string[]> = {}
6
+ const EMPTY_HEADERS: Record<string, string> = {}
7
+
8
+ /** Creates a minimal APIGatewayProxyEvent shell with the given requestContext fields */
9
+ function baseEvent(
10
+ requestContext: Partial<APIGatewayProxyEvent['requestContext']>,
11
+ overrides?: Partial<APIGatewayProxyEvent>
12
+ ): APIGatewayProxyEvent {
13
+ return {
14
+ body: null,
15
+ headers: EMPTY_HEADERS,
16
+ multiValueHeaders: EMPTY_MULTI_VALUE,
17
+ httpMethod: 'GET',
18
+ isBase64Encoded: false,
19
+ path: '',
20
+ pathParameters: null,
21
+ queryStringParameters: null,
22
+ multiValueQueryStringParameters: null,
23
+ stageVariables: null,
24
+ resource: '',
25
+ requestContext: {
26
+ accountId: 'local',
27
+ apiId: 'local',
28
+ authorizer: null,
29
+ protocol: 'websocket',
30
+ httpMethod: 'GET',
31
+ identity: {
32
+ accessKey: null,
33
+ accountId: null,
34
+ apiKey: null,
35
+ apiKeyId: null,
36
+ caller: null,
37
+ clientCert: null,
38
+ cognitoAuthenticationProvider: null,
39
+ cognitoAuthenticationType: null,
40
+ cognitoIdentityId: null,
41
+ cognitoIdentityPoolId: null,
42
+ principalOrgId: null,
43
+ sourceIp: '127.0.0.1',
44
+ user: null,
45
+ userAgent: null,
46
+ userArn: null,
47
+ },
48
+ path: '',
49
+ stage: 'local',
50
+ requestId: `local-${Date.now()}`,
51
+ requestTimeEpoch: Date.now(),
52
+ resourceId: '',
53
+ resourcePath: '',
54
+ ...requestContext,
55
+ },
56
+ ...overrides,
57
+ }
58
+ }
59
+
60
+ /** Synthesize a $connect event */
61
+ export function synthesizeConnectEvent(
62
+ connectionId: ConnectionId,
63
+ req: http.IncomingMessage,
64
+ config?: { domainName?: string; stage?: string }
65
+ ): APIGatewayProxyEvent {
66
+ const headers: Record<string, string> = {}
67
+ for (const [key, value] of Object.entries(req.headers)) {
68
+ if (typeof value === 'string') headers[key] = value
69
+ else if (Array.isArray(value)) headers[key] = value.join(', ')
70
+ }
71
+
72
+ return baseEvent(
73
+ {
74
+ connectionId,
75
+ domainName: config?.domainName ?? 'localhost',
76
+ stage: config?.stage ?? 'local',
77
+ routeKey: '$connect',
78
+ eventType: 'CONNECT',
79
+ } as any,
80
+ { headers }
81
+ )
82
+ }
83
+
84
+ /** Synthesize a $disconnect event */
85
+ export function synthesizeDisconnectEvent(
86
+ connectionId: ConnectionId,
87
+ config?: { domainName?: string; stage?: string }
88
+ ): APIGatewayProxyEvent {
89
+ return baseEvent({
90
+ connectionId,
91
+ domainName: config?.domainName ?? 'localhost',
92
+ stage: config?.stage ?? 'local',
93
+ routeKey: '$disconnect',
94
+ eventType: 'DISCONNECT',
95
+ } as any)
96
+ }
97
+
98
+ /** Synthesize a message event with body and routeKey */
99
+ export function synthesizeMessageEvent(
100
+ connectionId: ConnectionId,
101
+ body: string,
102
+ routeKey: string,
103
+ config?: { domainName?: string; stage?: string }
104
+ ): APIGatewayProxyEvent {
105
+ return baseEvent(
106
+ {
107
+ connectionId,
108
+ domainName: config?.domainName ?? 'localhost',
109
+ stage: config?.stage ?? 'local',
110
+ routeKey,
111
+ eventType: 'MESSAGE',
112
+ } as any,
113
+ { body }
114
+ )
115
+ }
116
+
117
+ /** Build a LocalRequestContext from parameters */
118
+ export function buildRequestContext(
119
+ connectionId: ConnectionId,
120
+ routeKey: string,
121
+ eventType: LocalRequestContext['eventType'],
122
+ config?: { domainName?: string; stage?: string }
123
+ ): LocalRequestContext {
124
+ return {
125
+ connectionId,
126
+ domainName: config?.domainName ?? 'localhost',
127
+ stage: config?.stage ?? 'local',
128
+ routeKey,
129
+ eventType,
130
+ }
131
+ }
@@ -0,0 +1,146 @@
1
+ import type { WebSocket, WebSocketServer } from 'ws'
2
+
3
+ import { ActionRouter } from './action-router'
4
+ import { ConnectionRegistry } from './connection-registry'
5
+ import { buildRequestContext } from './event-synthesizer'
6
+ import { LocalGatewayClient } from './local-gateway-client'
7
+ import type { ConnectionId, SocketAdapterConfig } from './types'
8
+
9
+ export type { ConnectionId, PostToConnectionFn, SocketAdapterConfig } from './types'
10
+ export { ConnectionRegistry } from './connection-registry'
11
+ export { LocalGatewayClient } from './local-gateway-client'
12
+ export { ActionRouter } from './action-router'
13
+
14
+ /**
15
+ * Attaches a full socket adapter to a WebSocketServer instance.
16
+ * Maps 1:1 to each API Gateway WebSocket event so local dev
17
+ * behaves identically to the deployed system.
18
+ */
19
+ export function attachSocketAdapter(wss: WebSocketServer, config?: SocketAdapterConfig) {
20
+ const debug = config?.debug ?? false
21
+ const domainName = config?.domainName ?? 'localhost'
22
+ const stage = config?.stage ?? 'local'
23
+
24
+ const connectionRegistry = new ConnectionRegistry()
25
+ const gatewayClient = new LocalGatewayClient(connectionRegistry)
26
+ const postToConnection = gatewayClient.asFunction()
27
+ const actionRouter = new ActionRouter()
28
+
29
+ // Register consumer-provided routes
30
+ if (config?.routes) {
31
+ for (const [action, handler] of Object.entries(config.routes)) {
32
+ actionRouter.addRoute(action, handler)
33
+ }
34
+ }
35
+
36
+ // $default handler for unknown actions
37
+ if (config?.defaultHandler) {
38
+ actionRouter.setDefaultHandler(config.defaultHandler)
39
+ } else {
40
+ actionRouter.setDefaultHandler(async (body, ctx) => {
41
+ let action = 'unknown'
42
+ try {
43
+ action = JSON.parse(body).action ?? 'unknown'
44
+ } catch {}
45
+ return {
46
+ statusCode: 400,
47
+ body: JSON.stringify({ error: `Unknown action: ${action}` }),
48
+ }
49
+ })
50
+ }
51
+
52
+ // Use consumer-provided handlers or sensible defaults
53
+ const connectHandler = config?.connectHandler ?? (async () => ({
54
+ response: { statusCode: 200 },
55
+ userContext: undefined,
56
+ }))
57
+
58
+ const disconnectHandler = config?.disconnectHandler ?? (async () => {})
59
+
60
+ // Reverse lookup: WebSocket → connectionId
61
+ const wsToConnectionId = new WeakMap<WebSocket, ConnectionId>()
62
+
63
+ wss.on('connection', async (ws: WebSocket, req) => {
64
+ // 1. Extract token from query string
65
+ const url = new URL(req.url ?? '', `http://${req.headers.host ?? 'localhost'}`)
66
+ const token = url.searchParams.get('token') ?? undefined
67
+
68
+ // 2. Register connection and assign connectionId
69
+ const connectionId = connectionRegistry.register(ws)
70
+ wsToConnectionId.set(ws, connectionId)
71
+
72
+ if (debug) console.log(`[socket-adapter] $connect connectionId=${connectionId}`)
73
+ if (debug) console.log(`[socket-adapter] token ${token ? 'present' : 'MISSING'}`)
74
+
75
+ // 3. Authenticate via connectHandler
76
+ const { response, userContext } = await connectHandler(connectionId, { token })
77
+ if (debug) console.log(`[socket-adapter] connectHandler result:`, response)
78
+ if (debug && userContext) {
79
+ console.log(`[socket-adapter] authenticated userId=${userContext.userId} orgId=${userContext.organizationId}`)
80
+ }
81
+
82
+ // 4. Reject if connectHandler returns non-200
83
+ if (response.statusCode !== 200) {
84
+ if (debug) console.log(`[socket-adapter] rejected connectionId=${connectionId} status=${response.statusCode}`)
85
+ ws.close(1008, 'Authentication failed')
86
+ connectionRegistry.unregister(connectionId)
87
+ wsToConnectionId.delete(ws)
88
+ return
89
+ }
90
+
91
+ // 5. Store user context in the registry (if provided)
92
+ if (userContext) {
93
+ connectionRegistry.setUserContext(connectionId, userContext)
94
+ }
95
+
96
+ // 6. Send welcome message to client
97
+ const welcomeMessage: Record<string, unknown> = { type: 'connected', connectionId }
98
+ if (userContext) welcomeMessage.userId = userContext.userId
99
+ ws.send(JSON.stringify(welcomeMessage))
100
+ if (debug) console.log(`[socket-adapter] welcome sent to ${connectionId}${userContext ? ` userId=${userContext.userId}` : ''}`)
101
+
102
+ // 7. Route incoming messages through ActionRouter
103
+ ws.on('message', async (raw) => {
104
+ const rawBody = raw.toString()
105
+
106
+ if (debug) console.log(`[socket-adapter] message from ${connectionId}:`, rawBody)
107
+
108
+ const requestContext = buildRequestContext(connectionId, '$default', 'MESSAGE', {
109
+ domainName,
110
+ stage,
111
+ })
112
+
113
+ const resolvedUserContext = userContext ?? connectionRegistry.getUserContext(connectionId) ?? {
114
+ userId: 'anonymous',
115
+ organizationId: 'anonymous',
116
+ }
117
+
118
+ const result = await actionRouter.route(connectionId, rawBody, postToConnection, requestContext, resolvedUserContext)
119
+
120
+ // Send the handler result back to the sender
121
+ if (result.body) {
122
+ ws.send(result.body)
123
+ }
124
+ })
125
+
126
+ // 8. Handle disconnect
127
+ ws.on('close', async (code, reason) => {
128
+ if (debug) {
129
+ console.log(
130
+ `[socket-adapter] $disconnect connectionId=${connectionId} code=${code} reason=${reason.toString()}`
131
+ )
132
+ }
133
+
134
+ await disconnectHandler(connectionId)
135
+ connectionRegistry.unregister(connectionId)
136
+ wsToConnectionId.delete(ws)
137
+ })
138
+ })
139
+
140
+ return {
141
+ postToConnection,
142
+ connectionRegistry,
143
+ actionRouter,
144
+ getConnectionId: (ws: WebSocket) => wsToConnectionId.get(ws),
145
+ }
146
+ }
@@ -0,0 +1,31 @@
1
+ import { WebSocket } from 'ws'
2
+ import type { ConnectionRegistry } from './connection-registry'
3
+ import type { ConnectionId, PostToConnectionFn } from './types'
4
+
5
+ /**
6
+ * Local replacement for @aws-sdk/client-apigatewaymanagementapi.
7
+ * Looks up the WebSocket in ConnectionRegistry and sends data directly.
8
+ */
9
+ export class LocalGatewayClient {
10
+ constructor(private registry: ConnectionRegistry) {}
11
+
12
+ /** Send data to a specific connection. Throws GoneException (410) if not found. */
13
+ async postToConnection(connectionId: ConnectionId, data: unknown): Promise<void> {
14
+ const ws = this.registry.get(connectionId)
15
+
16
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
17
+ const error = new Error(`GoneException: Connection ${connectionId} is no longer available`)
18
+ ;(error as any).statusCode = 410
19
+ ;(error as any).name = 'GoneException'
20
+ throw error
21
+ }
22
+
23
+ const payload = typeof data === 'string' ? data : JSON.stringify(data)
24
+ ws.send(payload)
25
+ }
26
+
27
+ /** Returns a bound PostToConnectionFn for dependency injection */
28
+ asFunction(): PostToConnectionFn {
29
+ return this.postToConnection.bind(this)
30
+ }
31
+ }
@@ -0,0 +1,66 @@
1
+ import type { WebSocket } from 'ws'
2
+
3
+ /** User context attached to a WebSocket connection */
4
+ export interface WebSocketUserContext {
5
+ userId: string
6
+ organizationId: string
7
+ [key: string]: unknown
8
+ }
9
+
10
+ /** Unique identifier for a WebSocket connection (mirrors API Gateway connectionId) */
11
+ export type ConnectionId = string
12
+
13
+ /** Function signature for sending data to a connection (replaces PostToConnectionCommand) */
14
+ export type PostToConnectionFn = (connectionId: ConnectionId, data: unknown) => Promise<void>
15
+
16
+ /** Synthesized equivalent of event.requestContext for local dev */
17
+ export interface LocalRequestContext {
18
+ connectionId: ConnectionId
19
+ domainName: string
20
+ stage: string
21
+ routeKey: string
22
+ eventType: 'CONNECT' | 'DISCONNECT' | 'MESSAGE'
23
+ }
24
+
25
+ /** Context passed to action handlers */
26
+ export interface ActionHandlerContext {
27
+ connectionId: ConnectionId
28
+ requestContext: LocalRequestContext
29
+ postToConnection: PostToConnectionFn
30
+ userContext: WebSocketUserContext
31
+ }
32
+
33
+ /** Result returned from an action handler */
34
+ export interface ActionHandlerResult {
35
+ statusCode: number
36
+ body?: string
37
+ }
38
+
39
+ /** Handler function for a routed action */
40
+ export type ActionHandler = (
41
+ body: string,
42
+ ctx: ActionHandlerContext
43
+ ) => Promise<ActionHandlerResult>
44
+
45
+ /** Handler called on $connect — returns response + optional user context */
46
+ export type ConnectHandlerFn = (
47
+ connectionId: ConnectionId,
48
+ params: { token?: string },
49
+ ) => Promise<{
50
+ response: { statusCode: number; body?: string }
51
+ userContext?: WebSocketUserContext
52
+ }>
53
+
54
+ /** Handler called on $disconnect */
55
+ export type DisconnectHandlerFn = (connectionId: ConnectionId) => Promise<void>
56
+
57
+ /** Configuration for the socket adapter */
58
+ export interface SocketAdapterConfig {
59
+ domainName?: string
60
+ stage?: string
61
+ debug?: boolean
62
+ connectHandler?: ConnectHandlerFn
63
+ disconnectHandler?: DisconnectHandlerFn
64
+ routes?: Record<string, ActionHandler>
65
+ defaultHandler?: ActionHandler
66
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./dist",
5
+ "target": "ES2022",
6
+ "module": "CommonJS",
7
+ "moduleResolution": "Node",
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "composite": true,
13
+ "strict": true,
14
+ "skipLibCheck": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }