serverless-ircd 0.3.0 → 0.5.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 +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +15 -8
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/wrangler.test.toml +15 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +4 -8
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
|
@@ -19,10 +19,15 @@ import {
|
|
|
19
19
|
type IdFactory,
|
|
20
20
|
type MessageStore,
|
|
21
21
|
type MotdProvider,
|
|
22
|
+
type MtlsIdentityProvider,
|
|
23
|
+
type NickHistoryStore,
|
|
22
24
|
type ParsedServerConfig,
|
|
25
|
+
type ServerConfig,
|
|
23
26
|
SystemClock,
|
|
24
27
|
UuidIdFactory,
|
|
28
|
+
type WsFrameMode,
|
|
25
29
|
createConnection,
|
|
30
|
+
frameToLines,
|
|
26
31
|
} from '@serverless-ircd/irc-core';
|
|
27
32
|
import { type ActorChannelAccess, ConnectionActor } from '@serverless-ircd/irc-server';
|
|
28
33
|
import { AwsRuntime, type AwsRuntimeHandlers, type PostToConnection } from '../aws-runtime.js';
|
|
@@ -31,8 +36,18 @@ import {
|
|
|
31
36
|
type MarshalledConnection,
|
|
32
37
|
unmarshalConnection,
|
|
33
38
|
} from '../serialize.js';
|
|
39
|
+
import { AwsStats } from '../stats.js';
|
|
34
40
|
import type { TablesConfig } from '../tables.js';
|
|
35
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Wall-clock timestamp captured at this Lambda isolate's first module load
|
|
44
|
+
* (cold start). Used as the `uptimeStartedAt` anchor for {@link AwsStats}
|
|
45
|
+
* so `STATS u` reports uptime for the current isolate. A deployment-wide
|
|
46
|
+
* uptime would require a persistent record (future work); for now each
|
|
47
|
+
* isolate reports its own.
|
|
48
|
+
*/
|
|
49
|
+
const LAMBDA_STARTUP_AT = Date.now();
|
|
50
|
+
|
|
36
51
|
/** Parameters accepted by {@link handleDefault}. */
|
|
37
52
|
export interface DefaultParams {
|
|
38
53
|
dynamo: DynamoDBDocumentClient;
|
|
@@ -53,6 +68,30 @@ export interface DefaultParams {
|
|
|
53
68
|
* — preserves the behaviour of callers that have not opted in.
|
|
54
69
|
*/
|
|
55
70
|
accounts?: AccountStore;
|
|
71
|
+
/**
|
|
72
|
+
* mTLS client-cert identity source for SASL EXTERNAL. When omitted the
|
|
73
|
+
* actor runs with `ctx.mtlsIdentity` undefined so `AUTHENTICATE EXTERNAL`
|
|
74
|
+
* fails with `904` — preserves the behaviour of callers that have not
|
|
75
|
+
* opted in.
|
|
76
|
+
*
|
|
77
|
+
* AWS APIGW custom-domain mTLS setup:
|
|
78
|
+
* 1. Upload a PEM trust store to S3 (the CA bundle clients must present).
|
|
79
|
+
* 2. Create a custom domain with `mutualTlsAuthentication` pointing at the
|
|
80
|
+
* S3 trust store URI.
|
|
81
|
+
* 3. Map the custom domain to the WebSocket API stage.
|
|
82
|
+
* 4. The `$connect` event's `requestContext.identity.clientCertSubjectDN`
|
|
83
|
+
* carries the verified cert subject — capture it at connect time and
|
|
84
|
+
* persist it alongside the connection row, then construct an
|
|
85
|
+
* {@link MtlsIdentityProvider} that resolves it by connection ID.
|
|
86
|
+
*/
|
|
87
|
+
mtlsIdentity?: MtlsIdentityProvider;
|
|
88
|
+
/**
|
|
89
|
+
* Nick-history source for the `WHOWAS` command. When omitted the actor
|
|
90
|
+
* runs with `ctx.history` undefined so `WHOWAS` always returns
|
|
91
|
+
* `406`+`369` — preserves the behaviour of callers that have not opted
|
|
92
|
+
* in. A DynamoDB-backed persistent variant is a documented follow-up.
|
|
93
|
+
*/
|
|
94
|
+
history?: NickHistoryStore;
|
|
56
95
|
managementApi: ApiGatewayManagementApi | PostToConnection | null;
|
|
57
96
|
/** Injected for tests; defaults to {@link SystemClock}. */
|
|
58
97
|
clock?: Clock;
|
|
@@ -60,6 +99,12 @@ export interface DefaultParams {
|
|
|
60
99
|
ids?: IdFactory;
|
|
61
100
|
/** Optional management endpoint URL used when constructing a default client. */
|
|
62
101
|
managementEndpoint?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Config-reload source for `REHASH`. When omitted the runtime's
|
|
104
|
+
* `reloadConfig` rejects so the actor emits a graceful error-suffixed
|
|
105
|
+
* `382` and retains the prior config.
|
|
106
|
+
*/
|
|
107
|
+
configLoader?: () => Promise<ServerConfig>;
|
|
63
108
|
}
|
|
64
109
|
|
|
65
110
|
/**
|
|
@@ -86,6 +131,12 @@ export async function handleDefault(params: DefaultParams): Promise<{ statusCode
|
|
|
86
131
|
const persisted = result.Item as unknown as MarshalledConnection;
|
|
87
132
|
const state = unmarshalConnection(persisted);
|
|
88
133
|
|
|
134
|
+
// Recover the IRCv3 WebSocket frame mode persisted at `$connect`.
|
|
135
|
+
// Absent = legacy (no subprotocol negotiated). Drives both the inbound
|
|
136
|
+
// line-framing transport (spec: one IRC message per frame, never split)
|
|
137
|
+
// and the outbound delivery shape (spec: one postToConnection per line).
|
|
138
|
+
const wsMode: WsFrameMode = persisted.wsMode ?? 'legacy';
|
|
139
|
+
|
|
89
140
|
// Mark this frame's activity.
|
|
90
141
|
state.lastSeen = clock.now();
|
|
91
142
|
|
|
@@ -98,8 +149,13 @@ export async function handleDefault(params: DefaultParams): Promise<{ statusCode
|
|
|
98
149
|
for (const l of lines) outbound.push(l.text);
|
|
99
150
|
},
|
|
100
151
|
disconnect: () => {
|
|
101
|
-
// Lambda cannot close its own
|
|
102
|
-
//
|
|
152
|
+
// No-op: a Lambda `$default` invocation cannot close its own APIGW
|
|
153
|
+
// socket. The canonical teardown is the `$disconnect` route (which
|
|
154
|
+
// fires when APIGW observes the close) plus the sweeper/ping-checker
|
|
155
|
+
// for connections that vanish without one. The actor's state
|
|
156
|
+
// mutations (nick release, roster deltas) are persisted below before
|
|
157
|
+
// this invocation returns, so the eventual `$disconnect` completes
|
|
158
|
+
// the fanout. See docs/AWS-Deployment.md §14.1.
|
|
103
159
|
},
|
|
104
160
|
snapshot: () => state,
|
|
105
161
|
};
|
|
@@ -110,6 +166,7 @@ export async function handleDefault(params: DefaultParams): Promise<{ statusCode
|
|
|
110
166
|
handlers,
|
|
111
167
|
managementApi: params.managementApi,
|
|
112
168
|
clock,
|
|
169
|
+
...(params.configLoader !== undefined ? { configLoader: params.configLoader } : {}),
|
|
113
170
|
});
|
|
114
171
|
|
|
115
172
|
const channelAccess = new LambdaChannelAccess(runtime, clock.now());
|
|
@@ -119,11 +176,24 @@ export async function handleDefault(params: DefaultParams): Promise<{ statusCode
|
|
|
119
176
|
runtime,
|
|
120
177
|
channels: channelAccess,
|
|
121
178
|
serverConfig: params.serverConfig,
|
|
179
|
+
configSource: 'Secrets Manager',
|
|
122
180
|
clock,
|
|
123
181
|
ids,
|
|
124
182
|
motd: params.motd,
|
|
183
|
+
transport: { feed: (chunk) => frameToLines(chunk, wsMode) },
|
|
184
|
+
// AwsStats Scans Connections + ChannelMeta for LUSERS/STATS.
|
|
185
|
+
// `uptimeStartedAt` is captured at the Lambda's cold start so `STATS u`
|
|
186
|
+
// reports per-isolate uptime; a deployment-wide uptime would require a
|
|
187
|
+
// persistent record (future work).
|
|
188
|
+
stats: new AwsStats({
|
|
189
|
+
dynamo: params.dynamo,
|
|
190
|
+
tables: params.tables,
|
|
191
|
+
uptimeStartedAt: LAMBDA_STARTUP_AT,
|
|
192
|
+
}),
|
|
125
193
|
...(params.messages !== undefined ? { messages: params.messages } : {}),
|
|
126
194
|
...(params.accounts !== undefined ? { accounts: params.accounts } : {}),
|
|
195
|
+
...(params.mtlsIdentity !== undefined ? { mtlsIdentity: params.mtlsIdentity } : {}),
|
|
196
|
+
...(params.history !== undefined ? { history: params.history } : {}),
|
|
127
197
|
});
|
|
128
198
|
|
|
129
199
|
try {
|
|
@@ -139,13 +209,12 @@ export async function handleDefault(params: DefaultParams): Promise<{ statusCode
|
|
|
139
209
|
await persistState(params.dynamo, params.tables, params.connectionId, state, clock.now());
|
|
140
210
|
|
|
141
211
|
// In production, deliver outbound bytes back to the caller via APIGW.
|
|
212
|
+
// Spec-mode connections (text/binary.ircv3.net) receive exactly one IRC
|
|
213
|
+
// line per `postToConnection` (no trailing CRLF); legacy connections
|
|
214
|
+
// receive every line CRLF-joined in a single message.
|
|
142
215
|
if (params.managementApi !== null && outbound.length > 0) {
|
|
143
|
-
const data = outbound.join('\r\n');
|
|
144
216
|
try {
|
|
145
|
-
await params.managementApi.
|
|
146
|
-
ConnectionId: params.connectionId,
|
|
147
|
-
Data: data,
|
|
148
|
-
});
|
|
217
|
+
await postOutbound(params.managementApi, params.connectionId, outbound, wsMode);
|
|
149
218
|
} catch {
|
|
150
219
|
// The handler still returns 200 — the actor's mutations are
|
|
151
220
|
// persisted; the caller just didn't see the reply this frame.
|
|
@@ -154,6 +223,28 @@ export async function handleDefault(params: DefaultParams): Promise<{ statusCode
|
|
|
154
223
|
return { statusCode: 200 };
|
|
155
224
|
}
|
|
156
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Delivers `lines` to `connectionId` via APIGW according to `mode`:
|
|
228
|
+
* - `legacy` — all lines joined with `\r\n` in a single `postToConnection`.
|
|
229
|
+
* - `spec-text` / `spec-binary` — one `postToConnection` per line, no
|
|
230
|
+
* trailing CRLF (the IRCv3 WebSocket contract: one IRC message per
|
|
231
|
+
* WebSocket message).
|
|
232
|
+
*/
|
|
233
|
+
async function postOutbound(
|
|
234
|
+
api: ApiGatewayManagementApi | PostToConnection,
|
|
235
|
+
connectionId: string,
|
|
236
|
+
lines: string[],
|
|
237
|
+
mode: WsFrameMode,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
if (mode === 'legacy') {
|
|
240
|
+
await api.postToConnection({ ConnectionId: connectionId, Data: lines.join('\r\n') });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
await api.postToConnection({ ConnectionId: connectionId, Data: line });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
157
248
|
/**
|
|
158
249
|
* Writes the actor's mutated `state` back to the Connections row.
|
|
159
250
|
*
|
|
@@ -16,10 +16,14 @@ import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
|
16
16
|
import {
|
|
17
17
|
type AccountStore,
|
|
18
18
|
EmptyMotdProvider,
|
|
19
|
+
InMemoryNickHistoryStore,
|
|
19
20
|
type MessageStore,
|
|
20
21
|
type MotdProvider,
|
|
22
|
+
type NickHistoryStore,
|
|
21
23
|
type ParsedServerConfig,
|
|
24
|
+
type ServerConfig,
|
|
22
25
|
StaticMotdProvider,
|
|
26
|
+
SystemClock,
|
|
23
27
|
} from '@serverless-ircd/irc-core';
|
|
24
28
|
import { resolveAccountStore } from '../account-store.js';
|
|
25
29
|
import { type LambdaConfigEnv, loadServerConfigFromLambdaEnv } from '../config-loader.js';
|
|
@@ -29,6 +33,13 @@ import { TABLE_KEYS, type TablesConfig } from '../tables.js';
|
|
|
29
33
|
import { handleConnect } from './connect.js';
|
|
30
34
|
import { handleDefault } from './default.js';
|
|
31
35
|
import { handleDisconnect } from './disconnect.js';
|
|
36
|
+
import {
|
|
37
|
+
type NlbStreamEvent,
|
|
38
|
+
type NlbStreamParams,
|
|
39
|
+
type NlbStreamResponse,
|
|
40
|
+
deriveNlbConnectionId,
|
|
41
|
+
handleNlbStream,
|
|
42
|
+
} from './nlb-stream.js';
|
|
32
43
|
import {
|
|
33
44
|
DEFAULT_PING_INTERVAL_MS,
|
|
34
45
|
DEFAULT_PONG_TIMEOUT_MS,
|
|
@@ -50,7 +61,20 @@ export interface WebSocketEvent {
|
|
|
50
61
|
connectionId: string;
|
|
51
62
|
domainName?: string;
|
|
52
63
|
stage?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Source IP of the connecting client. APIGW populates
|
|
66
|
+
* `requestContext.identity.sourceIp` on every `$connect`. Carried on
|
|
67
|
+
* the event type so the (currently deferred) per-IP admission gate
|
|
68
|
+
* can consume it without reshaping the handler signature.
|
|
69
|
+
*/
|
|
70
|
+
identity?: { sourceIp?: string };
|
|
53
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Request headers. APIGW populates `event.headers` on every route; the
|
|
74
|
+
* `$connect` upgrade carries the client's `Sec-WebSocket-Protocol` offer
|
|
75
|
+
* here so the handler can negotiate the IRCv3 subprotocol.
|
|
76
|
+
*/
|
|
77
|
+
headers?: Record<string, string>;
|
|
54
78
|
body?: string;
|
|
55
79
|
}
|
|
56
80
|
|
|
@@ -58,6 +82,12 @@ export interface WebSocketEvent {
|
|
|
58
82
|
export interface LambdaResponse {
|
|
59
83
|
statusCode: number;
|
|
60
84
|
body?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Response headers. The `$connect` route sets `Sec-WebSocket-Protocol`
|
|
87
|
+
* to echo the agreed IRCv3 subprotocol back to the client; the CDK
|
|
88
|
+
* integration-response maps it onto the WebSocket handshake.
|
|
89
|
+
*/
|
|
90
|
+
headers?: Record<string, string>;
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
/** Injectable dependencies. Tests construct one of these to bypass env loading. */
|
|
@@ -81,13 +111,31 @@ export interface HandlerDeps {
|
|
|
81
111
|
* configured (the default) so `ctx.accounts` stays unset.
|
|
82
112
|
*/
|
|
83
113
|
accounts?: AccountStore;
|
|
114
|
+
/**
|
|
115
|
+
* Nick-history source bound to every actor. Constructed once per cold
|
|
116
|
+
* start (memoised via {@link cachedDeps}) as an in-memory store so it
|
|
117
|
+
* survives across warm Lambda invocations. A DynamoDB-backed persistent
|
|
118
|
+
* variant is a documented follow-up.
|
|
119
|
+
*/
|
|
120
|
+
history?: NickHistoryStore;
|
|
121
|
+
/**
|
|
122
|
+
* Config-reload source for `REHASH` — re-invokes the Lambda env config
|
|
123
|
+
* loader (Secrets Manager / SSM / env vars). Bound once per cold start;
|
|
124
|
+
* each `REHASH` re-reads the live env so rotated oper creds / MOTD take
|
|
125
|
+
* effect without a redeploy. When omitted the actor's graceful failure
|
|
126
|
+
* path applies (error-suffixed `382`).
|
|
127
|
+
*/
|
|
128
|
+
configLoader?: () => Promise<ServerConfig>;
|
|
84
129
|
}
|
|
85
130
|
|
|
86
131
|
/**
|
|
87
132
|
* Strongly-typed event discriminated by routeKey. Useful for tests that
|
|
88
133
|
* construct events directly.
|
|
89
134
|
*/
|
|
90
|
-
export type ConnectEvent = {
|
|
135
|
+
export type ConnectEvent = {
|
|
136
|
+
requestContext: { routeKey: '$connect'; connectionId: string };
|
|
137
|
+
headers?: Record<string, string>;
|
|
138
|
+
};
|
|
91
139
|
export type DisconnectEvent = {
|
|
92
140
|
requestContext: { routeKey: '$disconnect'; connectionId: string };
|
|
93
141
|
};
|
|
@@ -132,14 +180,25 @@ export const handler = async (event: WebSocketEvent): Promise<LambdaResponse> =>
|
|
|
132
180
|
export async function dispatch(event: WebSocketEvent, deps: HandlerDeps): Promise<LambdaResponse> {
|
|
133
181
|
const connId = event.requestContext.connectionId;
|
|
134
182
|
switch (event.requestContext.routeKey) {
|
|
135
|
-
case '$connect':
|
|
136
|
-
await handleConnect({
|
|
183
|
+
case '$connect': {
|
|
184
|
+
const outcome = await handleConnect({
|
|
137
185
|
dynamo: deps.dynamo,
|
|
138
186
|
tables: deps.tables,
|
|
139
187
|
connectionId: connId,
|
|
140
188
|
serverConfig: deps.serverConfig,
|
|
189
|
+
secWebSocketProtocol: event.headers?.['Sec-WebSocket-Protocol'] ?? null,
|
|
141
190
|
});
|
|
142
|
-
|
|
191
|
+
if (outcome.admitted) {
|
|
192
|
+
if (outcome.subprotocol !== null) {
|
|
193
|
+
return {
|
|
194
|
+
statusCode: 200,
|
|
195
|
+
headers: { 'Sec-WebSocket-Protocol': outcome.subprotocol },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { statusCode: 200 };
|
|
199
|
+
}
|
|
200
|
+
return { statusCode: outcome.statusCode, body: outcome.reason };
|
|
201
|
+
}
|
|
143
202
|
|
|
144
203
|
case '$disconnect':
|
|
145
204
|
await handleDisconnect({
|
|
@@ -165,6 +224,8 @@ export async function dispatch(event: WebSocketEvent, deps: HandlerDeps): Promis
|
|
|
165
224
|
motd: deps.motd,
|
|
166
225
|
messages: deps.messages,
|
|
167
226
|
...(deps.accounts !== undefined ? { accounts: deps.accounts } : {}),
|
|
227
|
+
...(deps.history !== undefined ? { history: deps.history } : {}),
|
|
228
|
+
...(deps.configLoader !== undefined ? { configLoader: deps.configLoader } : {}),
|
|
168
229
|
managementApi: deps.managementApi,
|
|
169
230
|
});
|
|
170
231
|
return result;
|
|
@@ -245,6 +306,43 @@ export {
|
|
|
245
306
|
type SweepResult,
|
|
246
307
|
};
|
|
247
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Lambda entry for the NLB TCP+TLS streaming target. Wired
|
|
311
|
+
* to the NLB target group in the CDK stack. Shares the cached
|
|
312
|
+
* {@link HandlerDeps} with the wss {@link handler} — same tables, same
|
|
313
|
+
* config, same management API (when available). Each NLB client chunk
|
|
314
|
+
* arrives as one invocation; the handler returns the outbound bytes for
|
|
315
|
+
* the NLB to stream back over the held-open TCP connection.
|
|
316
|
+
*/
|
|
317
|
+
export async function nlbStreamHandler(event: NlbStreamEvent): Promise<NlbStreamResponse> {
|
|
318
|
+
if (cachedDeps === undefined) {
|
|
319
|
+
if (cachedDepsPromise === undefined) {
|
|
320
|
+
cachedDepsPromise = buildDepsFromEnv();
|
|
321
|
+
}
|
|
322
|
+
cachedDeps = await cachedDepsPromise;
|
|
323
|
+
cachedDepsPromise = undefined;
|
|
324
|
+
}
|
|
325
|
+
const deps = cachedDeps;
|
|
326
|
+
return handleNlbStream(event, {
|
|
327
|
+
dynamo: deps.dynamo,
|
|
328
|
+
tables: deps.tables,
|
|
329
|
+
serverConfig: deps.serverConfig,
|
|
330
|
+
motd: deps.motd,
|
|
331
|
+
messages: deps.messages,
|
|
332
|
+
managementApi: deps.managementApi,
|
|
333
|
+
...(deps.accounts !== undefined ? { accounts: deps.accounts } : {}),
|
|
334
|
+
...(deps.history !== undefined ? { history: deps.history } : {}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export {
|
|
339
|
+
handleNlbStream,
|
|
340
|
+
deriveNlbConnectionId,
|
|
341
|
+
type NlbStreamEvent,
|
|
342
|
+
type NlbStreamParams,
|
|
343
|
+
type NlbStreamResponse,
|
|
344
|
+
};
|
|
345
|
+
|
|
248
346
|
/**
|
|
249
347
|
* Builds a {@link HandlerDeps} from the current `process.env`.
|
|
250
348
|
*
|
|
@@ -267,7 +365,12 @@ export async function buildDepsFromEnv(env: NodeJS.ProcessEnv = process.env): Pr
|
|
|
267
365
|
}
|
|
268
366
|
const motd = cfg.motdLines.length > 0 ? new StaticMotdProvider(cfg.motdLines) : EmptyMotdProvider;
|
|
269
367
|
const messages = bindMessageStore(cfg);
|
|
368
|
+
const history = new InMemoryNickHistoryStore(SystemClock);
|
|
270
369
|
const accounts = await resolveAccountStore(dynamo, tables.Accounts, cfg);
|
|
370
|
+
// REHASH reload source: re-read the live env so a rotated oper password
|
|
371
|
+
// or MOTD takes effect on the next REHASH without a redeploy.
|
|
372
|
+
const configLoader = async (): Promise<ServerConfig> =>
|
|
373
|
+
loadServerConfigFromLambdaEnv(env as LambdaConfigEnv);
|
|
271
374
|
return {
|
|
272
375
|
dynamo,
|
|
273
376
|
tables,
|
|
@@ -275,6 +378,8 @@ export async function buildDepsFromEnv(env: NodeJS.ProcessEnv = process.env): Pr
|
|
|
275
378
|
motd,
|
|
276
379
|
managementApi,
|
|
277
380
|
messages,
|
|
381
|
+
history,
|
|
382
|
+
configLoader,
|
|
278
383
|
...(accounts !== undefined ? { accounts } : {}),
|
|
279
384
|
};
|
|
280
385
|
}
|