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
|
@@ -49,6 +49,11 @@ interface Fixture {
|
|
|
49
49
|
|
|
50
50
|
let fx: Fixture | null;
|
|
51
51
|
|
|
52
|
+
function requireFx(): Fixture {
|
|
53
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
54
|
+
return fx;
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
async function makeFixture(): Promise<Fixture> {
|
|
53
58
|
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
54
59
|
const prefix = `h${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
@@ -91,6 +96,9 @@ async function makeFixture(): Promise<Fixture> {
|
|
|
91
96
|
motd: MOTD,
|
|
92
97
|
messages: bindMessageStore(SERVER_CONFIG),
|
|
93
98
|
managementApi: mgmt as never,
|
|
99
|
+
// Bind a REHASH reload source so the $default dispatch path exercises
|
|
100
|
+
// the configLoader wiring (and an oper's REHASH re-fetches config).
|
|
101
|
+
configLoader: async () => SERVER_CONFIG,
|
|
94
102
|
},
|
|
95
103
|
cleanup: async () => {
|
|
96
104
|
for (const logical of Object.keys(TABLE_DEFS)) {
|
|
@@ -122,12 +130,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
122
130
|
it('returns 200 and persists a Connections row', async () => {
|
|
123
131
|
const res = await dispatch(
|
|
124
132
|
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
125
|
-
|
|
133
|
+
requireFx().deps,
|
|
126
134
|
);
|
|
127
135
|
expect(res.statusCode).toBe(200);
|
|
128
136
|
const probe = new AwsRuntime({
|
|
129
|
-
dynamo:
|
|
130
|
-
tables:
|
|
137
|
+
dynamo: requireFx().client,
|
|
138
|
+
tables: requireFx().tables,
|
|
131
139
|
connId: '__probe__',
|
|
132
140
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
133
141
|
managementApi: null,
|
|
@@ -137,13 +145,37 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
137
145
|
expect(info?.registration).toBe('pre-registration');
|
|
138
146
|
});
|
|
139
147
|
|
|
148
|
+
it('echoes the agreed Sec-WebSocket-Protocol header back on a spec-mode connect', async () => {
|
|
149
|
+
const res = await dispatch(
|
|
150
|
+
{
|
|
151
|
+
requestContext: { routeKey: '$connect', connectionId: 'c-sub' },
|
|
152
|
+
headers: { 'Sec-WebSocket-Protocol': 'text.ircv3.net' },
|
|
153
|
+
},
|
|
154
|
+
requireFx().deps,
|
|
155
|
+
);
|
|
156
|
+
expect(res.statusCode).toBe(200);
|
|
157
|
+
expect(res.headers?.['Sec-WebSocket-Protocol']).toBe('text.ircv3.net');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('omits the Sec-WebSocket-Protocol header on a legacy connect', async () => {
|
|
161
|
+
const res = await dispatch(
|
|
162
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c-nosub' } },
|
|
163
|
+
requireFx().deps,
|
|
164
|
+
);
|
|
165
|
+
expect(res.statusCode).toBe(200);
|
|
166
|
+
expect(res.headers?.['Sec-WebSocket-Protocol']).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
140
169
|
it('is idempotent on APIGW retry (same connectionId overwrites)', async () => {
|
|
141
170
|
for (let i = 0; i < 2; i++) {
|
|
142
|
-
await dispatch(
|
|
171
|
+
await dispatch(
|
|
172
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
173
|
+
requireFx().deps,
|
|
174
|
+
);
|
|
143
175
|
}
|
|
144
176
|
const probe = new AwsRuntime({
|
|
145
|
-
dynamo:
|
|
146
|
-
tables:
|
|
177
|
+
dynamo: requireFx().client,
|
|
178
|
+
tables: requireFx().tables,
|
|
147
179
|
connId: '__probe__',
|
|
148
180
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
149
181
|
managementApi: null,
|
|
@@ -153,21 +185,84 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
153
185
|
});
|
|
154
186
|
});
|
|
155
187
|
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
// $connect admission gate (max-clients cap)
|
|
190
|
+
// -------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
describe('$connect admission gate', () => {
|
|
193
|
+
it('rejects the connection that exceeds the maxClients cap', async () => {
|
|
194
|
+
const capped: HandlerDeps = {
|
|
195
|
+
...requireFx().deps,
|
|
196
|
+
serverConfig: { ...SERVER_CONFIG, maxClients: 2 },
|
|
197
|
+
};
|
|
198
|
+
const r1 = await dispatch(
|
|
199
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'a1' } },
|
|
200
|
+
capped,
|
|
201
|
+
);
|
|
202
|
+
const r2 = await dispatch(
|
|
203
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'a2' } },
|
|
204
|
+
capped,
|
|
205
|
+
);
|
|
206
|
+
const r3 = await dispatch(
|
|
207
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'a3' } },
|
|
208
|
+
capped,
|
|
209
|
+
);
|
|
210
|
+
expect(r1.statusCode).toBe(200);
|
|
211
|
+
expect(r2.statusCode).toBe(200);
|
|
212
|
+
expect(r3.statusCode).toBe(429);
|
|
213
|
+
|
|
214
|
+
const probe = new AwsRuntime({
|
|
215
|
+
dynamo: requireFx().client,
|
|
216
|
+
tables: requireFx().tables,
|
|
217
|
+
connId: '__probe__',
|
|
218
|
+
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
219
|
+
managementApi: null,
|
|
220
|
+
});
|
|
221
|
+
expect((await probe.getConnectionInfo('a1'))?.id).toBe('a1');
|
|
222
|
+
expect((await probe.getConnectionInfo('a2'))?.id).toBe('a2');
|
|
223
|
+
// The over-cap connection must NOT be recorded.
|
|
224
|
+
expect(await probe.getConnectionInfo('a3')).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('admits a connection when the table is below the cap', async () => {
|
|
228
|
+
const capped: HandlerDeps = {
|
|
229
|
+
...requireFx().deps,
|
|
230
|
+
serverConfig: { ...SERVER_CONFIG, maxClients: 5 },
|
|
231
|
+
};
|
|
232
|
+
const res = await dispatch(
|
|
233
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'b1' } },
|
|
234
|
+
capped,
|
|
235
|
+
);
|
|
236
|
+
expect(res.statusCode).toBe(200);
|
|
237
|
+
const probe = new AwsRuntime({
|
|
238
|
+
dynamo: requireFx().client,
|
|
239
|
+
tables: requireFx().tables,
|
|
240
|
+
connId: '__probe__',
|
|
241
|
+
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
242
|
+
managementApi: null,
|
|
243
|
+
});
|
|
244
|
+
expect((await probe.getConnectionInfo('b1'))?.id).toBe('b1');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
156
248
|
// -------------------------------------------------------------------------
|
|
157
249
|
// $disconnect
|
|
158
250
|
// -------------------------------------------------------------------------
|
|
159
251
|
|
|
160
252
|
describe('$disconnect', () => {
|
|
161
253
|
it('returns 200 and deletes the Connections row', async () => {
|
|
162
|
-
await dispatch(
|
|
254
|
+
await dispatch(
|
|
255
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
256
|
+
requireFx().deps,
|
|
257
|
+
);
|
|
163
258
|
const res = await dispatch(
|
|
164
259
|
{ requestContext: { routeKey: '$disconnect', connectionId: 'c1' } },
|
|
165
|
-
|
|
260
|
+
requireFx().deps,
|
|
166
261
|
);
|
|
167
262
|
expect(res.statusCode).toBe(200);
|
|
168
263
|
const probe = new AwsRuntime({
|
|
169
|
-
dynamo:
|
|
170
|
-
tables:
|
|
264
|
+
dynamo: requireFx().client,
|
|
265
|
+
tables: requireFx().tables,
|
|
171
266
|
connId: '__probe__',
|
|
172
267
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
173
268
|
managementApi: null,
|
|
@@ -178,33 +273,39 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
178
273
|
it('returns 200 even when the connection was never seen', async () => {
|
|
179
274
|
const res = await dispatch(
|
|
180
275
|
{ requestContext: { routeKey: '$disconnect', connectionId: 'never' } },
|
|
181
|
-
|
|
276
|
+
requireFx().deps,
|
|
182
277
|
);
|
|
183
278
|
expect(res.statusCode).toBe(200);
|
|
184
279
|
});
|
|
185
280
|
|
|
186
281
|
it('releases the nick on disconnect', async () => {
|
|
187
282
|
// Connect + register alice.
|
|
188
|
-
await dispatch(
|
|
283
|
+
await dispatch(
|
|
284
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
285
|
+
requireFx().deps,
|
|
286
|
+
);
|
|
189
287
|
await dispatch(
|
|
190
288
|
{
|
|
191
289
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
192
290
|
body: 'NICK alice\r\n',
|
|
193
291
|
},
|
|
194
|
-
|
|
292
|
+
requireFx().deps,
|
|
195
293
|
);
|
|
196
294
|
await dispatch(
|
|
197
295
|
{
|
|
198
296
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
199
297
|
body: 'USER alice 0 * :Alice\r\n',
|
|
200
298
|
},
|
|
201
|
-
|
|
299
|
+
requireFx().deps,
|
|
202
300
|
);
|
|
203
301
|
// Disconnect.
|
|
204
|
-
await dispatch(
|
|
302
|
+
await dispatch(
|
|
303
|
+
{ requestContext: { routeKey: '$disconnect', connectionId: 'c1' } },
|
|
304
|
+
requireFx().deps,
|
|
305
|
+
);
|
|
205
306
|
const probe = new AwsRuntime({
|
|
206
|
-
dynamo:
|
|
207
|
-
tables:
|
|
307
|
+
dynamo: requireFx().client,
|
|
308
|
+
tables: requireFx().tables,
|
|
208
309
|
connId: '__probe__',
|
|
209
310
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
210
311
|
managementApi: null,
|
|
@@ -224,7 +325,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
224
325
|
requestContext: { routeKey: '$default', connectionId: 'ghost' },
|
|
225
326
|
body: 'PING :tok\r\n',
|
|
226
327
|
},
|
|
227
|
-
|
|
328
|
+
requireFx().deps,
|
|
228
329
|
);
|
|
229
330
|
expect(res.statusCode).toBe(410);
|
|
230
331
|
});
|
|
@@ -234,23 +335,26 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
234
335
|
const event = {
|
|
235
336
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
236
337
|
} as never;
|
|
237
|
-
const res = await dispatch(event,
|
|
338
|
+
const res = await dispatch(event, requireFx().deps);
|
|
238
339
|
expect(res.statusCode).toBe(400);
|
|
239
340
|
});
|
|
240
341
|
|
|
241
342
|
it('runs a NICK + USER frame and emits the welcome block', async () => {
|
|
242
|
-
await dispatch(
|
|
243
|
-
|
|
343
|
+
await dispatch(
|
|
344
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
345
|
+
requireFx().deps,
|
|
346
|
+
);
|
|
347
|
+
requireFx().mgmt.register('c1', () => {});
|
|
244
348
|
await dispatch(
|
|
245
349
|
{
|
|
246
350
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
247
351
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
248
352
|
},
|
|
249
|
-
|
|
353
|
+
requireFx().deps,
|
|
250
354
|
);
|
|
251
355
|
const probe = new AwsRuntime({
|
|
252
|
-
dynamo:
|
|
253
|
-
tables:
|
|
356
|
+
dynamo: requireFx().client,
|
|
357
|
+
tables: requireFx().tables,
|
|
254
358
|
connId: '__probe__',
|
|
255
359
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
256
360
|
managementApi: null,
|
|
@@ -261,9 +365,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
261
365
|
});
|
|
262
366
|
|
|
263
367
|
it('echoes outbound lines back to the caller via the management API', async () => {
|
|
264
|
-
await dispatch(
|
|
368
|
+
await dispatch(
|
|
369
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
370
|
+
requireFx().deps,
|
|
371
|
+
);
|
|
265
372
|
const sink: string[] = [];
|
|
266
|
-
|
|
373
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
267
374
|
for (const line of data.split('\r\n')) {
|
|
268
375
|
if (line.length > 0) sink.push(line);
|
|
269
376
|
}
|
|
@@ -273,31 +380,123 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
273
380
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
274
381
|
body: 'PING :tok\r\n',
|
|
275
382
|
},
|
|
276
|
-
|
|
383
|
+
requireFx().deps,
|
|
277
384
|
);
|
|
278
385
|
expect(sink).toContain('PONG :tok');
|
|
279
386
|
});
|
|
280
387
|
|
|
388
|
+
it('delivers each outbound line as its own postToConnection for a spec-mode connection', async () => {
|
|
389
|
+
await dispatch(
|
|
390
|
+
{
|
|
391
|
+
requestContext: { routeKey: '$connect', connectionId: 'spec1' },
|
|
392
|
+
headers: { 'Sec-WebSocket-Protocol': 'text.ircv3.net' },
|
|
393
|
+
},
|
|
394
|
+
requireFx().deps,
|
|
395
|
+
);
|
|
396
|
+
const calls: string[] = [];
|
|
397
|
+
requireFx().mgmt.register('spec1', (data) => {
|
|
398
|
+
calls.push(data);
|
|
399
|
+
});
|
|
400
|
+
// Spec clients send exactly one IRC message per WebSocket frame.
|
|
401
|
+
await dispatch(
|
|
402
|
+
{
|
|
403
|
+
requestContext: { routeKey: '$default', connectionId: 'spec1' },
|
|
404
|
+
body: 'NICK bob\r\n',
|
|
405
|
+
},
|
|
406
|
+
requireFx().deps,
|
|
407
|
+
);
|
|
408
|
+
await dispatch(
|
|
409
|
+
{
|
|
410
|
+
requestContext: { routeKey: '$default', connectionId: 'spec1' },
|
|
411
|
+
body: 'USER bob 0 * :Bob\r\n',
|
|
412
|
+
},
|
|
413
|
+
requireFx().deps,
|
|
414
|
+
);
|
|
415
|
+
// The welcome block is multiple IRC lines; the IRCv3 spec contract
|
|
416
|
+
// sends exactly one line per WebSocket message (no trailing CRLF).
|
|
417
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
418
|
+
for (const data of calls) {
|
|
419
|
+
expect(data).not.toContain('\r\n');
|
|
420
|
+
}
|
|
421
|
+
expect(calls.some((d) => d.includes(' 001 '))).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('delivers one postToConnection per line for a binary.ircv3.net connection', async () => {
|
|
425
|
+
await dispatch(
|
|
426
|
+
{
|
|
427
|
+
requestContext: { routeKey: '$connect', connectionId: 'spec2' },
|
|
428
|
+
headers: { 'Sec-WebSocket-Protocol': 'binary.ircv3.net' },
|
|
429
|
+
},
|
|
430
|
+
requireFx().deps,
|
|
431
|
+
);
|
|
432
|
+
const calls: string[] = [];
|
|
433
|
+
requireFx().mgmt.register('spec2', (data) => {
|
|
434
|
+
calls.push(data);
|
|
435
|
+
});
|
|
436
|
+
await dispatch(
|
|
437
|
+
{
|
|
438
|
+
requestContext: { routeKey: '$default', connectionId: 'spec2' },
|
|
439
|
+
body: 'NICK binbob\r\n',
|
|
440
|
+
},
|
|
441
|
+
requireFx().deps,
|
|
442
|
+
);
|
|
443
|
+
await dispatch(
|
|
444
|
+
{
|
|
445
|
+
requestContext: { routeKey: '$default', connectionId: 'spec2' },
|
|
446
|
+
body: 'USER binbob 0 * :BinBob\r\n',
|
|
447
|
+
},
|
|
448
|
+
requireFx().deps,
|
|
449
|
+
);
|
|
450
|
+
expect(calls.length).toBeGreaterThan(1);
|
|
451
|
+
for (const data of calls) {
|
|
452
|
+
expect(data).not.toContain('\r\n');
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('batches outbound lines into a single postToConnection for a legacy connection', async () => {
|
|
457
|
+
await dispatch(
|
|
458
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'leg1' } },
|
|
459
|
+
requireFx().deps,
|
|
460
|
+
);
|
|
461
|
+
const calls: string[] = [];
|
|
462
|
+
requireFx().mgmt.register('leg1', (data) => {
|
|
463
|
+
calls.push(data);
|
|
464
|
+
});
|
|
465
|
+
await dispatch(
|
|
466
|
+
{
|
|
467
|
+
requestContext: { routeKey: '$default', connectionId: 'leg1' },
|
|
468
|
+
body: 'NICK carol\r\nUSER carol 0 * :Carol\r\n',
|
|
469
|
+
},
|
|
470
|
+
requireFx().deps,
|
|
471
|
+
);
|
|
472
|
+
expect(calls.length).toBe(1);
|
|
473
|
+
expect(calls[0]).toContain('\r\n');
|
|
474
|
+
expect(calls[0]).toContain(' 001 ');
|
|
475
|
+
});
|
|
476
|
+
|
|
281
477
|
it('persists joined channels across $default invocations', async () => {
|
|
282
|
-
await dispatch(
|
|
283
|
-
|
|
478
|
+
await dispatch(
|
|
479
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
480
|
+
requireFx().deps,
|
|
481
|
+
);
|
|
482
|
+
requireFx().mgmt.register('c1', () => {});
|
|
284
483
|
await dispatch(
|
|
285
484
|
{
|
|
286
485
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
287
486
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
288
487
|
},
|
|
289
|
-
|
|
488
|
+
requireFx().deps,
|
|
290
489
|
);
|
|
291
490
|
await dispatch(
|
|
292
491
|
{
|
|
293
492
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
294
493
|
body: 'JOIN #persist\r\n',
|
|
295
494
|
},
|
|
296
|
-
|
|
495
|
+
requireFx().deps,
|
|
297
496
|
);
|
|
298
497
|
const probe = new AwsRuntime({
|
|
299
|
-
dynamo:
|
|
300
|
-
tables:
|
|
498
|
+
dynamo: requireFx().client,
|
|
499
|
+
tables: requireFx().tables,
|
|
301
500
|
connId: '__probe__',
|
|
302
501
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
303
502
|
managementApi: null,
|
|
@@ -308,26 +507,32 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
308
507
|
|
|
309
508
|
it('rebuilds an existing channel roster via refreshChannel when a second client JOINs', async () => {
|
|
310
509
|
// First connection registers + JOINs.
|
|
311
|
-
await dispatch(
|
|
312
|
-
|
|
510
|
+
await dispatch(
|
|
511
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
512
|
+
requireFx().deps,
|
|
513
|
+
);
|
|
514
|
+
requireFx().mgmt.register('c1', () => {});
|
|
313
515
|
await dispatch(
|
|
314
516
|
{
|
|
315
517
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
316
518
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
317
519
|
},
|
|
318
|
-
|
|
520
|
+
requireFx().deps,
|
|
319
521
|
);
|
|
320
522
|
await dispatch(
|
|
321
523
|
{
|
|
322
524
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
323
525
|
body: 'JOIN #room\r\n',
|
|
324
526
|
},
|
|
325
|
-
|
|
527
|
+
requireFx().deps,
|
|
326
528
|
);
|
|
327
529
|
// Second connection registers + JOINs the SAME channel.
|
|
328
|
-
await dispatch(
|
|
530
|
+
await dispatch(
|
|
531
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c2' } },
|
|
532
|
+
requireFx().deps,
|
|
533
|
+
);
|
|
329
534
|
const c2sink: string[] = [];
|
|
330
|
-
|
|
535
|
+
requireFx().mgmt.register('c2', (data) => {
|
|
331
536
|
for (const line of data.split('\r\n')) {
|
|
332
537
|
if (line.length > 0) c2sink.push(line);
|
|
333
538
|
}
|
|
@@ -337,14 +542,14 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
337
542
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
338
543
|
body: 'NICK bob\r\nUSER bob 0 * :Bob\r\n',
|
|
339
544
|
},
|
|
340
|
-
|
|
545
|
+
requireFx().deps,
|
|
341
546
|
);
|
|
342
547
|
await dispatch(
|
|
343
548
|
{
|
|
344
549
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
345
550
|
body: 'JOIN #room\r\n',
|
|
346
551
|
},
|
|
347
|
-
|
|
552
|
+
requireFx().deps,
|
|
348
553
|
);
|
|
349
554
|
// The 353 RPL_NAMREPLY c2 receives must list BOTH alice and bob —
|
|
350
555
|
// proving that the actor's `refreshChannel` populated the cache
|
|
@@ -356,9 +561,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
356
561
|
});
|
|
357
562
|
|
|
358
563
|
it('records a PRIVMSG and replays it via CHATHISTORY LATEST in a BATCH', async () => {
|
|
359
|
-
await dispatch(
|
|
564
|
+
await dispatch(
|
|
565
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
566
|
+
requireFx().deps,
|
|
567
|
+
);
|
|
360
568
|
const sink: string[] = [];
|
|
361
|
-
|
|
569
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
362
570
|
for (const line of data.split('\r\n')) {
|
|
363
571
|
if (line.length > 0) sink.push(line);
|
|
364
572
|
}
|
|
@@ -368,7 +576,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
368
576
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
369
577
|
body: 'CAP REQ :draft/chathistory server-time message-tags batch\r\nNICK aws-alice\r\nUSER aws-alice 0 * :Alice\r\nCAP END\r\nJOIN #aws-chathist\r\n',
|
|
370
578
|
},
|
|
371
|
-
|
|
579
|
+
requireFx().deps,
|
|
372
580
|
);
|
|
373
581
|
sink.length = 0;
|
|
374
582
|
await dispatch(
|
|
@@ -376,7 +584,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
376
584
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
377
585
|
body: 'PRIVMSG #aws-chathist :hello from aws\r\nCHATHISTORY LATEST #aws-chathist * 10\r\n',
|
|
378
586
|
},
|
|
379
|
-
|
|
587
|
+
requireFx().deps,
|
|
380
588
|
);
|
|
381
589
|
const batchOpen = sink.find((l) => /^BATCH \+\S+ chathistory #aws-chathist/.test(l));
|
|
382
590
|
expect(batchOpen).toBeDefined();
|
|
@@ -385,16 +593,19 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
385
593
|
});
|
|
386
594
|
|
|
387
595
|
it('completes SASL PLAIN with 900/903 when an AccountStore is bound', async () => {
|
|
388
|
-
await dispatch(
|
|
596
|
+
await dispatch(
|
|
597
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
598
|
+
requireFx().deps,
|
|
599
|
+
);
|
|
389
600
|
const sink: string[] = [];
|
|
390
|
-
|
|
601
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
391
602
|
for (const line of data.split('\r\n')) {
|
|
392
603
|
if (line.length > 0) sink.push(line);
|
|
393
604
|
}
|
|
394
605
|
});
|
|
395
606
|
// Swap in a deps with a seeded AccountStore for this dispatch.
|
|
396
607
|
const depsWithAccounts: HandlerDeps = {
|
|
397
|
-
...
|
|
608
|
+
...requireFx().deps,
|
|
398
609
|
accounts: new InMemoryAccountStore([{ username: 'aws-sasl', password: 's3cret' }]),
|
|
399
610
|
};
|
|
400
611
|
const payload = Buffer.from('\0aws-sasl\0s3cret', 'utf8').toString('base64');
|
|
@@ -411,9 +622,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
411
622
|
});
|
|
412
623
|
|
|
413
624
|
it('emits 904 when no AccountStore is bound (regression guard)', async () => {
|
|
414
|
-
await dispatch(
|
|
625
|
+
await dispatch(
|
|
626
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c2' } },
|
|
627
|
+
requireFx().deps,
|
|
628
|
+
);
|
|
415
629
|
const sink: string[] = [];
|
|
416
|
-
|
|
630
|
+
requireFx().mgmt.register('c2', (data) => {
|
|
417
631
|
for (const line of data.split('\r\n')) {
|
|
418
632
|
if (line.length > 0) sink.push(line);
|
|
419
633
|
}
|
|
@@ -424,7 +638,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
424
638
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
425
639
|
body: `CAP REQ :sasl\r\nAUTHENTICATE PLAIN\r\nAUTHENTICATE ${payload}\r\n`,
|
|
426
640
|
},
|
|
427
|
-
|
|
641
|
+
requireFx().deps,
|
|
428
642
|
);
|
|
429
643
|
expect(sink.some((l) => l.includes(' 904 '))).toBe(true);
|
|
430
644
|
});
|
|
@@ -436,6 +650,41 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
436
650
|
// The 500-branch is covered indirectly by GoneException tests.
|
|
437
651
|
expect(true).toBe(true);
|
|
438
652
|
});
|
|
653
|
+
|
|
654
|
+
it('runs an oper REHASH through the bound configLoader and emits 382', async () => {
|
|
655
|
+
await dispatch(
|
|
656
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'rh1' } },
|
|
657
|
+
requireFx().deps,
|
|
658
|
+
);
|
|
659
|
+
const sink: string[] = [];
|
|
660
|
+
requireFx().mgmt.register('rh1', (data) => {
|
|
661
|
+
for (const line of data.split('\r\n')) {
|
|
662
|
+
if (line.length > 0) sink.push(line);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
// Seed oper creds + a reload source so OPER then REHASH both succeed.
|
|
666
|
+
const depsWithOper: HandlerDeps = {
|
|
667
|
+
...requireFx().deps,
|
|
668
|
+
serverConfig: { ...SERVER_CONFIG, operCreds: [{ user: 'alice', password: 's3cret' }] },
|
|
669
|
+
configLoader: async () => ({
|
|
670
|
+
...SERVER_CONFIG,
|
|
671
|
+
operCreds: [{ user: 'alice', password: 'rotated' }],
|
|
672
|
+
}),
|
|
673
|
+
};
|
|
674
|
+
await dispatch(
|
|
675
|
+
{
|
|
676
|
+
requestContext: { routeKey: '$default', connectionId: 'rh1' },
|
|
677
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nOPER alice s3cret\r\nREHASH\r\n',
|
|
678
|
+
},
|
|
679
|
+
depsWithOper,
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// 381 (oper) + 382 (rehashing from the Secrets Manager source) emitted.
|
|
683
|
+
expect(sink.some((l) => l.includes(' 381 '))).toBe(true);
|
|
684
|
+
expect(sink.some((l) => l.includes(' 382 ') && l.includes('Secrets Manager'))).toBe(true);
|
|
685
|
+
// Never a 421 for REHASH.
|
|
686
|
+
expect(sink.some((l) => l.includes(' 421 ') && l.includes('REHASH'))).toBe(false);
|
|
687
|
+
});
|
|
439
688
|
});
|
|
440
689
|
|
|
441
690
|
// -------------------------------------------------------------------------
|
|
@@ -446,7 +695,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
446
695
|
it('returns 200 (APIGW only forwards the three known routes)', async () => {
|
|
447
696
|
const res = await dispatch(
|
|
448
697
|
{ requestContext: { routeKey: '$custom', connectionId: 'c1' } },
|
|
449
|
-
|
|
698
|
+
requireFx().deps,
|
|
450
699
|
);
|
|
451
700
|
expect(res.statusCode).toBe(200);
|
|
452
701
|
});
|
|
@@ -462,7 +711,7 @@ class LocalPostToConnection implements PostToConnection {
|
|
|
462
711
|
register(connId: string, sink: (data: string) => void): void {
|
|
463
712
|
this.sinks.set(connId, sink);
|
|
464
713
|
}
|
|
465
|
-
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<
|
|
714
|
+
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<unknown> {
|
|
466
715
|
const sink = this.sinks.get(input.ConnectionId);
|
|
467
716
|
if (sink !== undefined) sink(input.Data);
|
|
468
717
|
return {};
|