serverless-ircd 0.1.0 → 0.2.0
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/.github/workflows/ci.yml +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +352 -0
- package/MOTD.txt +3 -0
- package/PLAN-FIXES.md +358 -0
- package/PLAN.md +420 -0
- package/README.md +115 -81
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.toml +25 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +56 -0
- package/apps/local-cli/src/server.ts +241 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +74 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/dashboards/cloudwatch-irc.json +139 -0
- package/docs/AWS-Adapter-Architecture.md +494 -0
- package/docs/AWS-Deployment.md +1107 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +1 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +440 -0
- package/package.json +8 -2
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/aws-runtime.ts +783 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +69 -0
- package/packages/aws-adapter/src/config-loader.ts +96 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +322 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +29 -0
- package/packages/aws-adapter/src/handlers/index.ts +248 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +67 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +158 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +427 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/config-loader.ts +106 -0
- package/packages/cf-adapter/src/connection-do.ts +34 -0
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +42 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +108 -3
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +115 -0
- package/packages/irc-core/package.json +12 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +7 -0
- package/packages/irc-core/src/commands/join.ts +59 -7
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +71 -14
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/config.ts +247 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +5 -0
- package/packages/irc-core/src/ports.ts +655 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +66 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +277 -9
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/config.test.ts +646 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +162 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +151 -0
- package/packages/irc-server/tests/actor.test.ts +470 -14
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +201 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +1 -1
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/progress.md +107 -0
- package/tickets.md +2485 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/webircgateway/LICENSE +201 -0
- package/webircgateway/Makefile +44 -0
- package/webircgateway/README.md +134 -0
- package/webircgateway/config.conf.example +135 -0
- package/webircgateway/go.mod +16 -0
- package/webircgateway/go.sum +89 -0
- package/webircgateway/main.go +118 -0
- package/webircgateway/pkg/dnsbl/dnsbl.go +121 -0
- package/webircgateway/pkg/identd/identd.go +86 -0
- package/webircgateway/pkg/identd/rpcclient.go +59 -0
- package/webircgateway/pkg/irc/isupport.go +56 -0
- package/webircgateway/pkg/irc/message.go +217 -0
- package/webircgateway/pkg/irc/state.go +79 -0
- package/webircgateway/pkg/proxy/proxy.go +129 -0
- package/webircgateway/pkg/proxy/server.go +237 -0
- package/webircgateway/pkg/recaptcha/recaptcha.go +59 -0
- package/webircgateway/pkg/webircgateway/client.go +741 -0
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +495 -0
- package/webircgateway/pkg/webircgateway/config.go +385 -0
- package/webircgateway/pkg/webircgateway/gateway.go +278 -0
- package/webircgateway/pkg/webircgateway/gateway_utils.go +133 -0
- package/webircgateway/pkg/webircgateway/hooks.go +152 -0
- package/webircgateway/pkg/webircgateway/letsencrypt.go +41 -0
- package/webircgateway/pkg/webircgateway/messagetags.go +103 -0
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +206 -0
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +107 -0
- package/webircgateway/pkg/webircgateway/transport_tcp.go +113 -0
- package/webircgateway/pkg/webircgateway/transport_websocket.go +126 -0
- package/webircgateway/pkg/webircgateway/utils.go +147 -0
- package/webircgateway/plugins/example/plugin.go +11 -0
- package/webircgateway/plugins/stats/plugin.go +52 -0
- package/webircgateway/staticcheck.conf +1 -0
- package/webircgateway/webircgateway.svg +3 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda handler entry point.
|
|
3
|
+
*
|
|
4
|
+
* Dispatches on `event.requestContext.routeKey` to the per-route handlers:
|
|
5
|
+
* - `$connect` → persist a fresh Connections row.
|
|
6
|
+
* - `$disconnect` → tear down the connection + clean up memberships.
|
|
7
|
+
* - `$default` → run the actor over `event.body` and persist state.
|
|
8
|
+
*
|
|
9
|
+
* The handler reads its config from `process.env` (SERVER_NAME, NETWORK_NAME,
|
|
10
|
+
* MOTD, table-name vars) via {@link loadServerConfigFromLambdaEnv}. Tests
|
|
11
|
+
* can inject a fully-formed {@link HandlerDeps} to bypass env loading.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { ApiGatewayManagementApi } from '@aws-sdk/client-apigatewaymanagementapi';
|
|
15
|
+
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
16
|
+
import {
|
|
17
|
+
EmptyMotdProvider,
|
|
18
|
+
type MessageStore,
|
|
19
|
+
type MotdProvider,
|
|
20
|
+
type ParsedServerConfig,
|
|
21
|
+
StaticMotdProvider,
|
|
22
|
+
} from '@serverless-ircd/irc-core';
|
|
23
|
+
import { type LambdaConfigEnv, loadServerConfigFromLambdaEnv } from '../config-loader.js';
|
|
24
|
+
import { createDynamoDocumentClient } from '../dynamo.js';
|
|
25
|
+
import { bindMessageStore } from '../message-store.js';
|
|
26
|
+
import { TABLE_KEYS, type TablesConfig } from '../tables.js';
|
|
27
|
+
import { handleConnect } from './connect.js';
|
|
28
|
+
import { handleDefault } from './default.js';
|
|
29
|
+
import { handleDisconnect } from './disconnect.js';
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_PING_INTERVAL_MS,
|
|
32
|
+
DEFAULT_PONG_TIMEOUT_MS,
|
|
33
|
+
type PingCheckParams,
|
|
34
|
+
type PingCheckResult,
|
|
35
|
+
handlePingCheck,
|
|
36
|
+
} from './ping-checker.js';
|
|
37
|
+
import {
|
|
38
|
+
DEFAULT_STALE_AFTER_MS,
|
|
39
|
+
type SweepParams,
|
|
40
|
+
type SweepResult,
|
|
41
|
+
handleSweep,
|
|
42
|
+
} from './sweeper.js';
|
|
43
|
+
|
|
44
|
+
/** API Gateway WebSocket event shape (the bits the handler reads). */
|
|
45
|
+
export interface WebSocketEvent {
|
|
46
|
+
requestContext: {
|
|
47
|
+
routeKey: string;
|
|
48
|
+
connectionId: string;
|
|
49
|
+
domainName?: string;
|
|
50
|
+
stage?: string;
|
|
51
|
+
};
|
|
52
|
+
body?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Lambda response shape. */
|
|
56
|
+
export interface LambdaResponse {
|
|
57
|
+
statusCode: number;
|
|
58
|
+
body?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Injectable dependencies. Tests construct one of these to bypass env loading. */
|
|
62
|
+
export interface HandlerDeps {
|
|
63
|
+
dynamo: DynamoDBDocumentClient;
|
|
64
|
+
tables: TablesConfig;
|
|
65
|
+
serverConfig: ParsedServerConfig;
|
|
66
|
+
motd: MotdProvider;
|
|
67
|
+
managementApi: ApiGatewayManagementApi | null;
|
|
68
|
+
/**
|
|
69
|
+
* Chat-history persistence source bound to every actor. Constructed
|
|
70
|
+
* once per cold start (memoised via {@link cachedDeps}) so it survives
|
|
71
|
+
* across warm Lambda invocations. The v1 impl is in-memory; a
|
|
72
|
+
* DynamoDB-backed variant is a documented follow-up.
|
|
73
|
+
*/
|
|
74
|
+
messages: MessageStore;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Strongly-typed event discriminated by routeKey. Useful for tests that
|
|
79
|
+
* construct events directly.
|
|
80
|
+
*/
|
|
81
|
+
export type ConnectEvent = { requestContext: { routeKey: '$connect'; connectionId: string } };
|
|
82
|
+
export type DisconnectEvent = {
|
|
83
|
+
requestContext: { routeKey: '$disconnect'; connectionId: string };
|
|
84
|
+
};
|
|
85
|
+
export type DefaultEvent = {
|
|
86
|
+
requestContext: { routeKey: '$default'; connectionId: string };
|
|
87
|
+
body: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Memoised `HandlerDeps`; built on the first cold-start invocation. */
|
|
91
|
+
let cachedDeps: HandlerDeps | undefined;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The Lambda entry point. Routes on `event.requestContext.routeKey`.
|
|
95
|
+
*
|
|
96
|
+
* Cold-start path reads `process.env` for config + table names. Warm
|
|
97
|
+
* invocations reuse the cached {@link HandlerDeps}.
|
|
98
|
+
*/
|
|
99
|
+
export const handler = async (event: WebSocketEvent): Promise<LambdaResponse> => {
|
|
100
|
+
if (cachedDeps === undefined) {
|
|
101
|
+
cachedDeps = buildDepsFromEnv();
|
|
102
|
+
}
|
|
103
|
+
const deps = cachedDeps;
|
|
104
|
+
return dispatch(event, deps);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Pure dispatcher — exported for unit tests that inject deps directly.
|
|
109
|
+
*/
|
|
110
|
+
export async function dispatch(event: WebSocketEvent, deps: HandlerDeps): Promise<LambdaResponse> {
|
|
111
|
+
const connId = event.requestContext.connectionId;
|
|
112
|
+
switch (event.requestContext.routeKey) {
|
|
113
|
+
case '$connect':
|
|
114
|
+
await handleConnect({
|
|
115
|
+
dynamo: deps.dynamo,
|
|
116
|
+
tables: deps.tables,
|
|
117
|
+
connectionId: connId,
|
|
118
|
+
serverConfig: deps.serverConfig,
|
|
119
|
+
});
|
|
120
|
+
return { statusCode: 200 };
|
|
121
|
+
|
|
122
|
+
case '$disconnect':
|
|
123
|
+
await handleDisconnect({
|
|
124
|
+
dynamo: deps.dynamo,
|
|
125
|
+
tables: deps.tables,
|
|
126
|
+
connectionId: connId,
|
|
127
|
+
managementApi: deps.managementApi,
|
|
128
|
+
});
|
|
129
|
+
return { statusCode: 200 };
|
|
130
|
+
|
|
131
|
+
case '$default':
|
|
132
|
+
if (event.body === undefined) {
|
|
133
|
+
return { statusCode: 400, body: 'empty body' };
|
|
134
|
+
}
|
|
135
|
+
{
|
|
136
|
+
const result = await handleDefault({
|
|
137
|
+
dynamo: deps.dynamo,
|
|
138
|
+
tables: deps.tables,
|
|
139
|
+
connectionId: connId,
|
|
140
|
+
body: event.body,
|
|
141
|
+
serverConfig: deps.serverConfig,
|
|
142
|
+
motd: deps.motd,
|
|
143
|
+
messages: deps.messages,
|
|
144
|
+
managementApi: deps.managementApi,
|
|
145
|
+
});
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
default:
|
|
150
|
+
// Unknown routeKey — should never happen (APIGW only forwards the
|
|
151
|
+
// three routes above). Return 200 so APIGW doesn't retry.
|
|
152
|
+
return { statusCode: 200 };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resets the cached {@link HandlerDeps}. Used by tests that mutate env
|
|
158
|
+
* between cases.
|
|
159
|
+
*/
|
|
160
|
+
export function __resetHandlerCacheForTests(): void {
|
|
161
|
+
cachedDeps = undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Lambda entry for the gone-connection sweeper. Wired to an EventBridge
|
|
166
|
+
* schedule in the CDK stack (`rate(5 minutes)`). Shares the cached
|
|
167
|
+
* {@link HandlerDeps} with {@link handler} — same tables, same config,
|
|
168
|
+
* no `managementApi` needed (the socket is, by definition, gone).
|
|
169
|
+
*
|
|
170
|
+
* `_event` is the EventBridge scheduled-event payload; we ignore it
|
|
171
|
+
* (the sweeper reads authoritative state from DynamoDB, not the event).
|
|
172
|
+
*/
|
|
173
|
+
export async function sweeperHandler(_event: unknown): Promise<SweepResult> {
|
|
174
|
+
if (cachedDeps === undefined) {
|
|
175
|
+
cachedDeps = buildDepsFromEnv();
|
|
176
|
+
}
|
|
177
|
+
const deps = cachedDeps;
|
|
178
|
+
return handleSweep({ dynamo: deps.dynamo, tables: deps.tables });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Lambda entry for the idle / PING checker. Wired to an EventBridge
|
|
183
|
+
* schedule in the CDK stack (`rate(1 minute)`). Shares the cached
|
|
184
|
+
* {@link HandlerDeps} with {@link handler} — same tables, same config,
|
|
185
|
+
* same `managementApi` (needed to push `PING` bytes back to the idle
|
|
186
|
+
* client via `postToConnection`).
|
|
187
|
+
*
|
|
188
|
+
* `_event` is the EventBridge scheduled-event payload; we ignore it
|
|
189
|
+
* (the checker reads authoritative state from DynamoDB, not the event).
|
|
190
|
+
*/
|
|
191
|
+
export async function pingCheckerHandler(_event: unknown): Promise<PingCheckResult> {
|
|
192
|
+
if (cachedDeps === undefined) {
|
|
193
|
+
cachedDeps = buildDepsFromEnv();
|
|
194
|
+
}
|
|
195
|
+
const deps = cachedDeps;
|
|
196
|
+
return handlePingCheck({
|
|
197
|
+
dynamo: deps.dynamo,
|
|
198
|
+
tables: deps.tables,
|
|
199
|
+
managementApi: deps.managementApi,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
DEFAULT_PING_INTERVAL_MS,
|
|
205
|
+
DEFAULT_PONG_TIMEOUT_MS,
|
|
206
|
+
DEFAULT_STALE_AFTER_MS,
|
|
207
|
+
handlePingCheck,
|
|
208
|
+
handleSweep,
|
|
209
|
+
type PingCheckParams,
|
|
210
|
+
type PingCheckResult,
|
|
211
|
+
type SweepParams,
|
|
212
|
+
type SweepResult,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Builds a {@link HandlerDeps} from the current `process.env`.
|
|
217
|
+
*
|
|
218
|
+
* Exposed so tests can construct production-shaped deps without re-running
|
|
219
|
+
* the env parsing.
|
|
220
|
+
*/
|
|
221
|
+
export function buildDepsFromEnv(env: NodeJS.ProcessEnv = process.env): HandlerDeps {
|
|
222
|
+
const cfg = loadServerConfigFromLambdaEnv(env as LambdaConfigEnv);
|
|
223
|
+
const tables = tablesConfigFromEnv(env);
|
|
224
|
+
const region = typeof env.AWS_REGION === 'string' ? env.AWS_REGION : undefined;
|
|
225
|
+
const dynamo =
|
|
226
|
+
region !== undefined ? createDynamoDocumentClient({ region }) : createDynamoDocumentClient({});
|
|
227
|
+
let managementApi: ApiGatewayManagementApi | null = null;
|
|
228
|
+
const managementUrl = typeof env.MANAGEMENT_URL === 'string' ? env.MANAGEMENT_URL : undefined;
|
|
229
|
+
if (managementUrl !== undefined) {
|
|
230
|
+
managementApi = new ApiGatewayManagementApi({ endpoint: managementUrl });
|
|
231
|
+
}
|
|
232
|
+
const motd = cfg.motdLines.length > 0 ? new StaticMotdProvider(cfg.motdLines) : EmptyMotdProvider;
|
|
233
|
+
const messages = bindMessageStore(cfg);
|
|
234
|
+
return { dynamo, tables, serverConfig: cfg, motd, managementApi, messages };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function tablesConfigFromEnv(env: NodeJS.ProcessEnv): TablesConfig {
|
|
238
|
+
const out = {} as TablesConfig;
|
|
239
|
+
for (const logical of Object.keys(TABLE_KEYS) as Array<keyof typeof TABLE_KEYS>) {
|
|
240
|
+
const envName = `${logical.toUpperCase()}_TABLE`;
|
|
241
|
+
const physical = env[envName];
|
|
242
|
+
if (typeof physical !== 'string') {
|
|
243
|
+
throw new Error(`Missing env var ${envName} (table name for ${logical})`);
|
|
244
|
+
}
|
|
245
|
+
out[logical] = physical;
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$ping` handler — Lambda entry invoked on a fixed EventBridge schedule
|
|
3
|
+
* (rate(1 minute) in the CDK stack). Scans `Connections` for rows past
|
|
4
|
+
* the configured idle thresholds and either:
|
|
5
|
+
*
|
|
6
|
+
* • posts a server-initiated `PING :<token>` to the client via
|
|
7
|
+
* `ApiGatewayManagementApi.postToConnection`, or
|
|
8
|
+
* • tears the connection down via {@link cleanupConnection} when the
|
|
9
|
+
* client has been silent past the PONG window.
|
|
10
|
+
*
|
|
11
|
+
* Why a batch-scan Lambda (not per-connection EventBridge one-shots):
|
|
12
|
+
* the architecture sketch in `docs/AWS-Adapter-Architecture.md` §6
|
|
13
|
+
* mentions per-connection Scheduler entries, but that model scales to
|
|
14
|
+
* N scheduler entries per active user and complicates cancellation
|
|
15
|
+
* on every `$default`. A single scheduled Lambda that scans and acts
|
|
16
|
+
* is simpler, cheaper, and matches how the gone-connection sweeper
|
|
17
|
+
* already works.
|
|
18
|
+
*
|
|
19
|
+
* Decision parity with the Cloudflare adapter: this is the AWS
|
|
20
|
+
* equivalent of `ConnectionDO.alarm()` in `cf-adapter/src/connection-do.ts`,
|
|
21
|
+
* which sends a PING every `pingIntervalMs` and disconnects when
|
|
22
|
+
* `lastSeen + pongTimeoutMs < now`. The AWS batch Lambda makes the
|
|
23
|
+
* same per-connection decision for every connection in a single pass.
|
|
24
|
+
*
|
|
25
|
+
* Idle timer: every `$default` invocation refreshes `idleSince` to
|
|
26
|
+
* `Date.now()` (see `handleDefault`'s `persistState`), so any frame
|
|
27
|
+
* from the client — including the `PONG` reply this handler's PING
|
|
28
|
+
* elicits — resets the idle window. `idleSince` is also the DynamoDB
|
|
29
|
+
* TTL attribute, so connections that slip past both this Lambda and
|
|
30
|
+
* the sweeper are eventually reaped by DynamoDB itself.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { randomUUID } from 'node:crypto';
|
|
34
|
+
import {
|
|
35
|
+
type ApiGatewayManagementApi,
|
|
36
|
+
GoneException,
|
|
37
|
+
} from '@aws-sdk/client-apigatewaymanagementapi';
|
|
38
|
+
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
39
|
+
import { ScanCommand, UpdateCommand } from '@aws-sdk/lib-dynamodb';
|
|
40
|
+
import type { ConnId, RawLine } from '@serverless-ircd/irc-core';
|
|
41
|
+
import { cleanupConnection } from '../aws-runtime.js';
|
|
42
|
+
import type { PostToConnection } from '../aws-runtime.js';
|
|
43
|
+
import type { TablesConfig } from '../tables.js';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default PING cadence — matches `cf-adapter`'s
|
|
47
|
+
* `DEFAULT_PING_INTERVAL_MS` so a connection sees the same liveness
|
|
48
|
+
* pressure on both substrates.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_PING_INTERVAL_MS = 60_000;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default no-PONG disconnect window — total idle to disconnect equals
|
|
54
|
+
* `DEFAULT_PING_INTERVAL_MS + DEFAULT_PONG_TIMEOUT_MS` (150 s), giving
|
|
55
|
+
* the client ~90 s after the first PING to reply. Mirrors the CF
|
|
56
|
+
* adapter's `DEFAULT_PONG_TIMEOUT_MS`.
|
|
57
|
+
*/
|
|
58
|
+
export const DEFAULT_PONG_TIMEOUT_MS = 90_000;
|
|
59
|
+
|
|
60
|
+
/** Parameters accepted by {@link handlePingCheck}. */
|
|
61
|
+
export interface PingCheckParams {
|
|
62
|
+
dynamo: DynamoDBDocumentClient;
|
|
63
|
+
tables: TablesConfig;
|
|
64
|
+
/**
|
|
65
|
+
* Backend used to push `PING` bytes to the remote socket. May be:
|
|
66
|
+
* - a real `ApiGatewayManagementApi` client (production),
|
|
67
|
+
* - a test double that satisfies {@link PostToConnection},
|
|
68
|
+
* - `null` when running in a context that has no APIGW endpoint
|
|
69
|
+
* (the PING path becomes a silent no-op and only the disconnect
|
|
70
|
+
* branch can fire). Used by the sweeper-style unit tests.
|
|
71
|
+
*/
|
|
72
|
+
managementApi: ApiGatewayManagementApi | PostToConnection | null;
|
|
73
|
+
/** Wall-clock "now" (default: `Date.now()`). */
|
|
74
|
+
now?: number;
|
|
75
|
+
/** A connection is PINGed when `idleFor >= pingIntervalMs` (default 60 s). */
|
|
76
|
+
pingIntervalMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* A connection is disconnected when `idleFor >= pingIntervalMs + pongTimeoutMs`
|
|
79
|
+
* (default 90 s, so the total idle-to-disconnect is 150 s). The
|
|
80
|
+
* client gets roughly this long after the first PING to send PONG.
|
|
81
|
+
*/
|
|
82
|
+
pongTimeoutMs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Overrides the PING token source. Defaults to `crypto.randomUUID()`.
|
|
85
|
+
* Exposed for determinism in tests; production callers should not
|
|
86
|
+
* set this.
|
|
87
|
+
*/
|
|
88
|
+
makeToken?: () => string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Result returned by {@link handlePingCheck}. */
|
|
92
|
+
export interface PingCheckResult {
|
|
93
|
+
/** Total rows examined (post-pagination sum). */
|
|
94
|
+
scanned: number;
|
|
95
|
+
/** Rows that received a `PING` this pass. */
|
|
96
|
+
pinged: number;
|
|
97
|
+
/** Rows torn down this pass. */
|
|
98
|
+
disconnected: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Scans `Connections` and applies the PING / disconnect policy per row.
|
|
103
|
+
*
|
|
104
|
+
* Idempotent: running twice pings 0 the second time once the disconnect
|
|
105
|
+
* threshold has elapsed (the row is gone), and PINGing the same live
|
|
106
|
+
* client twice is harmless (clients reply to every PING with a PONG).
|
|
107
|
+
*/
|
|
108
|
+
export async function handlePingCheck(params: PingCheckParams): Promise<PingCheckResult> {
|
|
109
|
+
const now = params.now ?? Date.now();
|
|
110
|
+
const pingIntervalMs = params.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
|
|
111
|
+
const pongTimeoutMs = params.pongTimeoutMs ?? DEFAULT_PONG_TIMEOUT_MS;
|
|
112
|
+
const disconnectAfter = pingIntervalMs + pongTimeoutMs;
|
|
113
|
+
const makeToken = params.makeToken ?? defaultToken;
|
|
114
|
+
|
|
115
|
+
let scanned = 0;
|
|
116
|
+
let pinged = 0;
|
|
117
|
+
let disconnected = 0;
|
|
118
|
+
let lastKey: unknown = undefined;
|
|
119
|
+
|
|
120
|
+
do {
|
|
121
|
+
const res = await params.dynamo.send(
|
|
122
|
+
new ScanCommand({
|
|
123
|
+
TableName: params.tables.Connections,
|
|
124
|
+
...(lastKey !== undefined ? { ExclusiveStartKey: lastKey as never } : {}),
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
lastKey = res.LastEvaluatedKey;
|
|
128
|
+
for (const item of res.Items ?? []) {
|
|
129
|
+
scanned++;
|
|
130
|
+
const row = item as { connectionId?: unknown; idleSince?: unknown };
|
|
131
|
+
const connId = row.connectionId;
|
|
132
|
+
const idleSince = row.idleSince;
|
|
133
|
+
if (typeof connId !== 'string' || typeof idleSince !== 'number') continue;
|
|
134
|
+
const idleFor = now - idleSince;
|
|
135
|
+
|
|
136
|
+
if (idleFor >= disconnectAfter) {
|
|
137
|
+
// Silent past the PONG window: tear down. cleanupConnection is
|
|
138
|
+
// idempotent and tolerates the row being concurrently removed
|
|
139
|
+
// by `$disconnect` or the sweeper.
|
|
140
|
+
await cleanupConnection(params.dynamo, params.tables, connId, params.managementApi);
|
|
141
|
+
disconnected++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (idleFor >= pingIntervalMs) {
|
|
146
|
+
// managementApi === null means the PING path is a silent no-op
|
|
147
|
+
// (e.g. the sweeper-style unit tests). We do not count it as a
|
|
148
|
+
// pinged connection because no bytes went out.
|
|
149
|
+
if (params.managementApi === null) continue;
|
|
150
|
+
const token = makeToken();
|
|
151
|
+
const line: RawLine = { text: `PING :${token}` };
|
|
152
|
+
const data = `${line.text}\r\n`;
|
|
153
|
+
try {
|
|
154
|
+
await params.managementApi.postToConnection({ ConnectionId: connId, Data: data });
|
|
155
|
+
} catch (err: unknown) {
|
|
156
|
+
if (err instanceof GoneException || isGone(err)) {
|
|
157
|
+
// The socket is gone but the row is still there. Reap it
|
|
158
|
+
// directly so the client's silent death doesn't have to
|
|
159
|
+
// wait for the next sweeper cycle.
|
|
160
|
+
await cleanupConnection(params.dynamo, params.tables, connId, params.managementApi);
|
|
161
|
+
// The PING attempt counts: the Lambda did the work of
|
|
162
|
+
// deciding to ping, even though the send threw. The row
|
|
163
|
+
// is also counted as disconnected by the cleanup path, but
|
|
164
|
+
// we only bump `disconnected` from the idle-threshold
|
|
165
|
+
// branch above; the gone-cleanup is reported via `pinged`
|
|
166
|
+
// so callers can tell the two cases apart.
|
|
167
|
+
pinged++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
// Stamp the row so subsequent passes can distinguish "PING sent,
|
|
173
|
+
// awaiting PONG" from "freshly idle, never pinged" if a future
|
|
174
|
+
// revision wants to avoid duplicate PINGs. Today the Lambda
|
|
175
|
+
// simply re-pings on every fire; the stamp is bookkeeping for
|
|
176
|
+
// observers (and tests).
|
|
177
|
+
await markPingSent(params.dynamo, params.tables.Connections, connId, now);
|
|
178
|
+
pinged++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} while (lastKey !== undefined);
|
|
182
|
+
|
|
183
|
+
return { scanned, pinged, disconnected };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Internals
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
/** Default token source — Node 20 Lambda runtime exposes `crypto.randomUUID` globally. */
|
|
191
|
+
function defaultToken(): string {
|
|
192
|
+
return randomUUID();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Stamps `pingSentAt = now` on the connection row (side-channel attribute). */
|
|
196
|
+
async function markPingSent(
|
|
197
|
+
dynamo: DynamoDBDocumentClient,
|
|
198
|
+
connectionsTable: string,
|
|
199
|
+
connId: ConnId,
|
|
200
|
+
now: number,
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
await dynamo.send(
|
|
203
|
+
new UpdateCommand({
|
|
204
|
+
TableName: connectionsTable,
|
|
205
|
+
Key: { connectionId: connId },
|
|
206
|
+
UpdateExpression: 'SET pingSentAt = :t',
|
|
207
|
+
ExpressionAttributeValues: { ':t': now },
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** True iff the SDK error indicates the APIGW connection is gone. */
|
|
213
|
+
function isGone(err: unknown): boolean {
|
|
214
|
+
if (err === null || typeof err !== 'object') return false;
|
|
215
|
+
const name = (err as { name?: string }).name;
|
|
216
|
+
return name === 'GoneException' || name === 'ForbiddenException';
|
|
217
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initial ConnectionState for a fresh `$connect` invocation.
|
|
3
|
+
*
|
|
4
|
+
* Centralised so the `$connect` handler and the test harness construct
|
|
5
|
+
* the same canonical pre-registration state.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ConnectionState, createConnection } from '@serverless-ircd/irc-core';
|
|
9
|
+
|
|
10
|
+
/** Builds the canonical pre-registration state for `connectionId`. */
|
|
11
|
+
export function createInitialConnectionState(connectionId: string, now: number): ConnectionState {
|
|
12
|
+
return createConnection({ id: connectionId, connectedSince: now });
|
|
13
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `$sweeper` handler — Lambda entry invoked on a fixed EventBridge
|
|
3
|
+
* schedule. Scans `Connections` for rows whose `idleSince` is older
|
|
4
|
+
* than a TTL threshold and runs {@link cleanupConnection} on each.
|
|
5
|
+
*
|
|
6
|
+
* Catches connections that vanished without API Gateway ever emitting
|
|
7
|
+
* `$disconnect` (mobile backgrounding, NAT timeouts, client crashes).
|
|
8
|
+
* The DynamoDB TTL on `idleSince` also expires those rows eventually,
|
|
9
|
+
* but TTL reaping is eventual (up to ~48h) and does NOT clean up the
|
|
10
|
+
* matching `ChannelMembers` / `Nicks` rows — the sweeper does, via
|
|
11
|
+
* the same idempotent teardown path used on `$disconnect`.
|
|
12
|
+
*
|
|
13
|
+
* Threshold default matches the APIGW WebSocket 2-hour hard cap: any
|
|
14
|
+
* connection idle for ≥2h is definitionally gone (APIGW itself will
|
|
15
|
+
* have closed the socket). PING timers handle the shorter-window
|
|
16
|
+
* liveness checks; this is the long-tail backstop.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
20
|
+
import { ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
21
|
+
import { cleanupConnection } from '../aws-runtime.js';
|
|
22
|
+
import type { TablesConfig } from '../tables.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default stale threshold — twice the APIGW WebSocket 2-hour hard cap
|
|
26
|
+
* would be excessive; we use the cap itself. A connection idle for two
|
|
27
|
+
* hours cannot still have an open socket (APIGW enforces the cap).
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_STALE_AFTER_MS = 2 * 60 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
/** Parameters accepted by {@link handleSweep}. */
|
|
32
|
+
export interface SweepParams {
|
|
33
|
+
dynamo: DynamoDBDocumentClient;
|
|
34
|
+
tables: TablesConfig;
|
|
35
|
+
/** Wall-clock "now" (default: `Date.now()`). */
|
|
36
|
+
now?: number;
|
|
37
|
+
/** A connection is stale when `idleSince < now - staleAfterMs`. */
|
|
38
|
+
staleAfterMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Result returned by {@link handleSweep}. */
|
|
42
|
+
export interface SweepResult {
|
|
43
|
+
/** Total rows examined (post-pagination sum). */
|
|
44
|
+
scanned: number;
|
|
45
|
+
/** Rows torn down this pass. */
|
|
46
|
+
swept: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scans `Connections` and tears down every row past the staleness
|
|
51
|
+
* threshold. Idempotent: running twice sweeps 0 the second time
|
|
52
|
+
* (cleanupConnection tolerates missing rows).
|
|
53
|
+
*/
|
|
54
|
+
export async function handleSweep(params: SweepParams): Promise<SweepResult> {
|
|
55
|
+
const now = params.now ?? Date.now();
|
|
56
|
+
const staleAfterMs = params.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
57
|
+
const cutoff = now - staleAfterMs;
|
|
58
|
+
|
|
59
|
+
let scanned = 0;
|
|
60
|
+
let swept = 0;
|
|
61
|
+
let lastKey: unknown = undefined;
|
|
62
|
+
|
|
63
|
+
do {
|
|
64
|
+
const res = await params.dynamo.send(
|
|
65
|
+
new ScanCommand({
|
|
66
|
+
TableName: params.tables.Connections,
|
|
67
|
+
...(lastKey !== undefined ? { ExclusiveStartKey: lastKey as never } : {}),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
lastKey = res.LastEvaluatedKey;
|
|
71
|
+
for (const item of res.Items ?? []) {
|
|
72
|
+
scanned++;
|
|
73
|
+
const row = item as { connectionId?: string; idleSince?: number };
|
|
74
|
+
const idleSince = row.idleSince;
|
|
75
|
+
if (typeof idleSince !== 'number' || idleSince >= cutoff) continue;
|
|
76
|
+
const connId = row.connectionId;
|
|
77
|
+
if (typeof connId !== 'string') continue;
|
|
78
|
+
await cleanupConnection(params.dynamo, params.tables, connId, null);
|
|
79
|
+
swept++;
|
|
80
|
+
}
|
|
81
|
+
} while (lastKey !== undefined);
|
|
82
|
+
|
|
83
|
+
return { scanned, swept };
|
|
84
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @serverless-ircd/aws-adapter — entry point.
|
|
3
|
+
*
|
|
4
|
+
* Exports accumulate as the TDD cycles land. The CDK-coupled `TABLE_DEFS`
|
|
5
|
+
* lives at `@serverless-ircd/aws-adapter/cdk-table-defs` so the Lambda
|
|
6
|
+
* bundle does not pull in `aws-cdk-lib`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { AwsRuntime } from './aws-runtime.js';
|
|
10
|
+
export type {
|
|
11
|
+
AwsRuntimeHandlers,
|
|
12
|
+
AwsRuntimeOptions,
|
|
13
|
+
PostToConnection,
|
|
14
|
+
} from './aws-runtime.js';
|
|
15
|
+
export { cleanupConnection } from './aws-runtime.js';
|
|
16
|
+
export { TABLE_KEYS, TABLE_NAMES, tablesConfigFromNames } from './tables.js';
|
|
17
|
+
export type { TableName, TablesConfig } from './tables.js';
|
|
18
|
+
export { createDynamoDocumentClient } from './dynamo.js';
|
|
19
|
+
export {
|
|
20
|
+
loadServerConfigFromLambdaEnv,
|
|
21
|
+
type LambdaConfigEnv,
|
|
22
|
+
} from './config-loader.js';
|
|
23
|
+
export {
|
|
24
|
+
CONNECTION_VERSION,
|
|
25
|
+
marshalChannelMember,
|
|
26
|
+
marshalChannelMeta,
|
|
27
|
+
marshalConnection,
|
|
28
|
+
marshalNick,
|
|
29
|
+
unmarshalChannelMember,
|
|
30
|
+
unmarshalChannelMeta,
|
|
31
|
+
unmarshalConnection,
|
|
32
|
+
unmarshalNick,
|
|
33
|
+
} from './serialize.js';
|
|
34
|
+
export type {
|
|
35
|
+
MarshalledChannelMember,
|
|
36
|
+
MarshalledChannelMeta,
|
|
37
|
+
MarshalledConnection,
|
|
38
|
+
MarshalledNick,
|
|
39
|
+
} from './serialize.js';
|
|
40
|
+
|
|
41
|
+
// Lambda handler entry — exported so `apps/aws-stack`'s NodejsFunction and
|
|
42
|
+
// downstream tooling can import from a single package surface.
|
|
43
|
+
export {
|
|
44
|
+
handler,
|
|
45
|
+
dispatch,
|
|
46
|
+
buildDepsFromEnv,
|
|
47
|
+
pingCheckerHandler,
|
|
48
|
+
sweeperHandler,
|
|
49
|
+
handlePingCheck,
|
|
50
|
+
handleSweep,
|
|
51
|
+
DEFAULT_PING_INTERVAL_MS,
|
|
52
|
+
DEFAULT_PONG_TIMEOUT_MS,
|
|
53
|
+
DEFAULT_STALE_AFTER_MS,
|
|
54
|
+
__resetHandlerCacheForTests,
|
|
55
|
+
} from './handlers/index.js';
|
|
56
|
+
export type {
|
|
57
|
+
ConnectEvent,
|
|
58
|
+
DefaultEvent,
|
|
59
|
+
DisconnectEvent,
|
|
60
|
+
HandlerDeps,
|
|
61
|
+
LambdaResponse,
|
|
62
|
+
PingCheckParams,
|
|
63
|
+
PingCheckResult,
|
|
64
|
+
SweepParams,
|
|
65
|
+
SweepResult,
|
|
66
|
+
WebSocketEvent,
|
|
67
|
+
} from './handlers/index.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat-history store construction for the AWS adapter.
|
|
3
|
+
*
|
|
4
|
+
* Centralises `MessageStore` creation so the actor-construction sites
|
|
5
|
+
* (`handleDefault`) bind a single helper rather than constructing the
|
|
6
|
+
* store inline. The v1 implementation is an in-memory ring buffer
|
|
7
|
+
* (`InMemoryMessageStore`); a DynamoDB-backed persistent variant (new
|
|
8
|
+
* `Messages` table, partition key `chan`, sort key `time`, GSI on
|
|
9
|
+
* `msgid`) is a documented follow-up — swapping it in means replacing
|
|
10
|
+
* this one function.
|
|
11
|
+
*
|
|
12
|
+
* The in-memory v1 store is scoped to the Lambda execution context:
|
|
13
|
+
* it persists across warm invocations (via the memoised `HandlerDeps`)
|
|
14
|
+
* but is lost on cold start. Within a single multi-line `$default`
|
|
15
|
+
* frame (PRIVMSG + CHATHISTORY joined by `\r\n`) recording and
|
|
16
|
+
* playback work fully end-to-end.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
InMemoryMessageStore,
|
|
21
|
+
type MessageStore,
|
|
22
|
+
type ParsedServerConfig,
|
|
23
|
+
} from '@serverless-ircd/irc-core';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Constructs the chat-history store for a deployment.
|
|
27
|
+
*
|
|
28
|
+
* @param _serverConfig Reserved for the future persistent variant
|
|
29
|
+
* (retention cap, table-name resolution); the in-memory v1 impl
|
|
30
|
+
* uses the `InMemoryMessageStore` default retention.
|
|
31
|
+
*/
|
|
32
|
+
export function bindMessageStore(_serverConfig?: ParsedServerConfig): MessageStore {
|
|
33
|
+
return new InMemoryMessageStore();
|
|
34
|
+
}
|