serverless-ircd 0.3.0 → 0.4.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 +174 -18
- package/README.md +63 -30
- package/apps/aws-stack/README.md +2 -2
- 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 +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- 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 +20 -3
- 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 +6 -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 +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +35 -2
- package/packages/aws-adapter/src/handlers/index.ts +69 -3
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +6 -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/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -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 +154 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- 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 +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +2 -4
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +111 -46
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +33 -9
- package/packages/cf-adapter/src/index.ts +9 -8
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- 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/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/worker/main.ts +8 -1
- package/packages/cf-adapter/wrangler.test.toml +8 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- 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 +9 -0
- package/packages/irc-core/src/commands/ison.ts +61 -0
- 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/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +58 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +181 -12
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +38 -1
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- 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/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- 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/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -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/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- 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 +158 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +12 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +400 -3
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +4 -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 +132 -2
- 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 +9 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -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)}-`;
|
|
@@ -122,12 +127,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
122
127
|
it('returns 200 and persists a Connections row', async () => {
|
|
123
128
|
const res = await dispatch(
|
|
124
129
|
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
125
|
-
|
|
130
|
+
requireFx().deps,
|
|
126
131
|
);
|
|
127
132
|
expect(res.statusCode).toBe(200);
|
|
128
133
|
const probe = new AwsRuntime({
|
|
129
|
-
dynamo:
|
|
130
|
-
tables:
|
|
134
|
+
dynamo: requireFx().client,
|
|
135
|
+
tables: requireFx().tables,
|
|
131
136
|
connId: '__probe__',
|
|
132
137
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
133
138
|
managementApi: null,
|
|
@@ -139,11 +144,14 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
139
144
|
|
|
140
145
|
it('is idempotent on APIGW retry (same connectionId overwrites)', async () => {
|
|
141
146
|
for (let i = 0; i < 2; i++) {
|
|
142
|
-
await dispatch(
|
|
147
|
+
await dispatch(
|
|
148
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
149
|
+
requireFx().deps,
|
|
150
|
+
);
|
|
143
151
|
}
|
|
144
152
|
const probe = new AwsRuntime({
|
|
145
|
-
dynamo:
|
|
146
|
-
tables:
|
|
153
|
+
dynamo: requireFx().client,
|
|
154
|
+
tables: requireFx().tables,
|
|
147
155
|
connId: '__probe__',
|
|
148
156
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
149
157
|
managementApi: null,
|
|
@@ -153,21 +161,84 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
153
161
|
});
|
|
154
162
|
});
|
|
155
163
|
|
|
164
|
+
// -------------------------------------------------------------------------
|
|
165
|
+
// $connect admission gate (max-clients cap)
|
|
166
|
+
// -------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
describe('$connect admission gate', () => {
|
|
169
|
+
it('rejects the connection that exceeds the maxClients cap', async () => {
|
|
170
|
+
const capped: HandlerDeps = {
|
|
171
|
+
...requireFx().deps,
|
|
172
|
+
serverConfig: { ...SERVER_CONFIG, maxClients: 2 },
|
|
173
|
+
};
|
|
174
|
+
const r1 = await dispatch(
|
|
175
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'a1' } },
|
|
176
|
+
capped,
|
|
177
|
+
);
|
|
178
|
+
const r2 = await dispatch(
|
|
179
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'a2' } },
|
|
180
|
+
capped,
|
|
181
|
+
);
|
|
182
|
+
const r3 = await dispatch(
|
|
183
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'a3' } },
|
|
184
|
+
capped,
|
|
185
|
+
);
|
|
186
|
+
expect(r1.statusCode).toBe(200);
|
|
187
|
+
expect(r2.statusCode).toBe(200);
|
|
188
|
+
expect(r3.statusCode).toBe(429);
|
|
189
|
+
|
|
190
|
+
const probe = new AwsRuntime({
|
|
191
|
+
dynamo: requireFx().client,
|
|
192
|
+
tables: requireFx().tables,
|
|
193
|
+
connId: '__probe__',
|
|
194
|
+
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
195
|
+
managementApi: null,
|
|
196
|
+
});
|
|
197
|
+
expect((await probe.getConnectionInfo('a1'))?.id).toBe('a1');
|
|
198
|
+
expect((await probe.getConnectionInfo('a2'))?.id).toBe('a2');
|
|
199
|
+
// The over-cap connection must NOT be recorded.
|
|
200
|
+
expect(await probe.getConnectionInfo('a3')).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('admits a connection when the table is below the cap', async () => {
|
|
204
|
+
const capped: HandlerDeps = {
|
|
205
|
+
...requireFx().deps,
|
|
206
|
+
serverConfig: { ...SERVER_CONFIG, maxClients: 5 },
|
|
207
|
+
};
|
|
208
|
+
const res = await dispatch(
|
|
209
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'b1' } },
|
|
210
|
+
capped,
|
|
211
|
+
);
|
|
212
|
+
expect(res.statusCode).toBe(200);
|
|
213
|
+
const probe = new AwsRuntime({
|
|
214
|
+
dynamo: requireFx().client,
|
|
215
|
+
tables: requireFx().tables,
|
|
216
|
+
connId: '__probe__',
|
|
217
|
+
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
218
|
+
managementApi: null,
|
|
219
|
+
});
|
|
220
|
+
expect((await probe.getConnectionInfo('b1'))?.id).toBe('b1');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
156
224
|
// -------------------------------------------------------------------------
|
|
157
225
|
// $disconnect
|
|
158
226
|
// -------------------------------------------------------------------------
|
|
159
227
|
|
|
160
228
|
describe('$disconnect', () => {
|
|
161
229
|
it('returns 200 and deletes the Connections row', async () => {
|
|
162
|
-
await dispatch(
|
|
230
|
+
await dispatch(
|
|
231
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
232
|
+
requireFx().deps,
|
|
233
|
+
);
|
|
163
234
|
const res = await dispatch(
|
|
164
235
|
{ requestContext: { routeKey: '$disconnect', connectionId: 'c1' } },
|
|
165
|
-
|
|
236
|
+
requireFx().deps,
|
|
166
237
|
);
|
|
167
238
|
expect(res.statusCode).toBe(200);
|
|
168
239
|
const probe = new AwsRuntime({
|
|
169
|
-
dynamo:
|
|
170
|
-
tables:
|
|
240
|
+
dynamo: requireFx().client,
|
|
241
|
+
tables: requireFx().tables,
|
|
171
242
|
connId: '__probe__',
|
|
172
243
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
173
244
|
managementApi: null,
|
|
@@ -178,33 +249,39 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
178
249
|
it('returns 200 even when the connection was never seen', async () => {
|
|
179
250
|
const res = await dispatch(
|
|
180
251
|
{ requestContext: { routeKey: '$disconnect', connectionId: 'never' } },
|
|
181
|
-
|
|
252
|
+
requireFx().deps,
|
|
182
253
|
);
|
|
183
254
|
expect(res.statusCode).toBe(200);
|
|
184
255
|
});
|
|
185
256
|
|
|
186
257
|
it('releases the nick on disconnect', async () => {
|
|
187
258
|
// Connect + register alice.
|
|
188
|
-
await dispatch(
|
|
259
|
+
await dispatch(
|
|
260
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
261
|
+
requireFx().deps,
|
|
262
|
+
);
|
|
189
263
|
await dispatch(
|
|
190
264
|
{
|
|
191
265
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
192
266
|
body: 'NICK alice\r\n',
|
|
193
267
|
},
|
|
194
|
-
|
|
268
|
+
requireFx().deps,
|
|
195
269
|
);
|
|
196
270
|
await dispatch(
|
|
197
271
|
{
|
|
198
272
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
199
273
|
body: 'USER alice 0 * :Alice\r\n',
|
|
200
274
|
},
|
|
201
|
-
|
|
275
|
+
requireFx().deps,
|
|
202
276
|
);
|
|
203
277
|
// Disconnect.
|
|
204
|
-
await dispatch(
|
|
278
|
+
await dispatch(
|
|
279
|
+
{ requestContext: { routeKey: '$disconnect', connectionId: 'c1' } },
|
|
280
|
+
requireFx().deps,
|
|
281
|
+
);
|
|
205
282
|
const probe = new AwsRuntime({
|
|
206
|
-
dynamo:
|
|
207
|
-
tables:
|
|
283
|
+
dynamo: requireFx().client,
|
|
284
|
+
tables: requireFx().tables,
|
|
208
285
|
connId: '__probe__',
|
|
209
286
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
210
287
|
managementApi: null,
|
|
@@ -224,7 +301,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
224
301
|
requestContext: { routeKey: '$default', connectionId: 'ghost' },
|
|
225
302
|
body: 'PING :tok\r\n',
|
|
226
303
|
},
|
|
227
|
-
|
|
304
|
+
requireFx().deps,
|
|
228
305
|
);
|
|
229
306
|
expect(res.statusCode).toBe(410);
|
|
230
307
|
});
|
|
@@ -234,23 +311,26 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
234
311
|
const event = {
|
|
235
312
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
236
313
|
} as never;
|
|
237
|
-
const res = await dispatch(event,
|
|
314
|
+
const res = await dispatch(event, requireFx().deps);
|
|
238
315
|
expect(res.statusCode).toBe(400);
|
|
239
316
|
});
|
|
240
317
|
|
|
241
318
|
it('runs a NICK + USER frame and emits the welcome block', async () => {
|
|
242
|
-
await dispatch(
|
|
243
|
-
|
|
319
|
+
await dispatch(
|
|
320
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
321
|
+
requireFx().deps,
|
|
322
|
+
);
|
|
323
|
+
requireFx().mgmt.register('c1', () => {});
|
|
244
324
|
await dispatch(
|
|
245
325
|
{
|
|
246
326
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
247
327
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
248
328
|
},
|
|
249
|
-
|
|
329
|
+
requireFx().deps,
|
|
250
330
|
);
|
|
251
331
|
const probe = new AwsRuntime({
|
|
252
|
-
dynamo:
|
|
253
|
-
tables:
|
|
332
|
+
dynamo: requireFx().client,
|
|
333
|
+
tables: requireFx().tables,
|
|
254
334
|
connId: '__probe__',
|
|
255
335
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
256
336
|
managementApi: null,
|
|
@@ -261,9 +341,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
261
341
|
});
|
|
262
342
|
|
|
263
343
|
it('echoes outbound lines back to the caller via the management API', async () => {
|
|
264
|
-
await dispatch(
|
|
344
|
+
await dispatch(
|
|
345
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
346
|
+
requireFx().deps,
|
|
347
|
+
);
|
|
265
348
|
const sink: string[] = [];
|
|
266
|
-
|
|
349
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
267
350
|
for (const line of data.split('\r\n')) {
|
|
268
351
|
if (line.length > 0) sink.push(line);
|
|
269
352
|
}
|
|
@@ -273,31 +356,34 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
273
356
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
274
357
|
body: 'PING :tok\r\n',
|
|
275
358
|
},
|
|
276
|
-
|
|
359
|
+
requireFx().deps,
|
|
277
360
|
);
|
|
278
361
|
expect(sink).toContain('PONG :tok');
|
|
279
362
|
});
|
|
280
363
|
|
|
281
364
|
it('persists joined channels across $default invocations', async () => {
|
|
282
|
-
await dispatch(
|
|
283
|
-
|
|
365
|
+
await dispatch(
|
|
366
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
367
|
+
requireFx().deps,
|
|
368
|
+
);
|
|
369
|
+
requireFx().mgmt.register('c1', () => {});
|
|
284
370
|
await dispatch(
|
|
285
371
|
{
|
|
286
372
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
287
373
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
288
374
|
},
|
|
289
|
-
|
|
375
|
+
requireFx().deps,
|
|
290
376
|
);
|
|
291
377
|
await dispatch(
|
|
292
378
|
{
|
|
293
379
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
294
380
|
body: 'JOIN #persist\r\n',
|
|
295
381
|
},
|
|
296
|
-
|
|
382
|
+
requireFx().deps,
|
|
297
383
|
);
|
|
298
384
|
const probe = new AwsRuntime({
|
|
299
|
-
dynamo:
|
|
300
|
-
tables:
|
|
385
|
+
dynamo: requireFx().client,
|
|
386
|
+
tables: requireFx().tables,
|
|
301
387
|
connId: '__probe__',
|
|
302
388
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
303
389
|
managementApi: null,
|
|
@@ -308,26 +394,32 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
308
394
|
|
|
309
395
|
it('rebuilds an existing channel roster via refreshChannel when a second client JOINs', async () => {
|
|
310
396
|
// First connection registers + JOINs.
|
|
311
|
-
await dispatch(
|
|
312
|
-
|
|
397
|
+
await dispatch(
|
|
398
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
399
|
+
requireFx().deps,
|
|
400
|
+
);
|
|
401
|
+
requireFx().mgmt.register('c1', () => {});
|
|
313
402
|
await dispatch(
|
|
314
403
|
{
|
|
315
404
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
316
405
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
317
406
|
},
|
|
318
|
-
|
|
407
|
+
requireFx().deps,
|
|
319
408
|
);
|
|
320
409
|
await dispatch(
|
|
321
410
|
{
|
|
322
411
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
323
412
|
body: 'JOIN #room\r\n',
|
|
324
413
|
},
|
|
325
|
-
|
|
414
|
+
requireFx().deps,
|
|
326
415
|
);
|
|
327
416
|
// Second connection registers + JOINs the SAME channel.
|
|
328
|
-
await dispatch(
|
|
417
|
+
await dispatch(
|
|
418
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c2' } },
|
|
419
|
+
requireFx().deps,
|
|
420
|
+
);
|
|
329
421
|
const c2sink: string[] = [];
|
|
330
|
-
|
|
422
|
+
requireFx().mgmt.register('c2', (data) => {
|
|
331
423
|
for (const line of data.split('\r\n')) {
|
|
332
424
|
if (line.length > 0) c2sink.push(line);
|
|
333
425
|
}
|
|
@@ -337,14 +429,14 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
337
429
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
338
430
|
body: 'NICK bob\r\nUSER bob 0 * :Bob\r\n',
|
|
339
431
|
},
|
|
340
|
-
|
|
432
|
+
requireFx().deps,
|
|
341
433
|
);
|
|
342
434
|
await dispatch(
|
|
343
435
|
{
|
|
344
436
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
345
437
|
body: 'JOIN #room\r\n',
|
|
346
438
|
},
|
|
347
|
-
|
|
439
|
+
requireFx().deps,
|
|
348
440
|
);
|
|
349
441
|
// The 353 RPL_NAMREPLY c2 receives must list BOTH alice and bob —
|
|
350
442
|
// proving that the actor's `refreshChannel` populated the cache
|
|
@@ -356,9 +448,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
356
448
|
});
|
|
357
449
|
|
|
358
450
|
it('records a PRIVMSG and replays it via CHATHISTORY LATEST in a BATCH', async () => {
|
|
359
|
-
await dispatch(
|
|
451
|
+
await dispatch(
|
|
452
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
453
|
+
requireFx().deps,
|
|
454
|
+
);
|
|
360
455
|
const sink: string[] = [];
|
|
361
|
-
|
|
456
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
362
457
|
for (const line of data.split('\r\n')) {
|
|
363
458
|
if (line.length > 0) sink.push(line);
|
|
364
459
|
}
|
|
@@ -368,7 +463,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
368
463
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
369
464
|
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
465
|
},
|
|
371
|
-
|
|
466
|
+
requireFx().deps,
|
|
372
467
|
);
|
|
373
468
|
sink.length = 0;
|
|
374
469
|
await dispatch(
|
|
@@ -376,7 +471,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
376
471
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
377
472
|
body: 'PRIVMSG #aws-chathist :hello from aws\r\nCHATHISTORY LATEST #aws-chathist * 10\r\n',
|
|
378
473
|
},
|
|
379
|
-
|
|
474
|
+
requireFx().deps,
|
|
380
475
|
);
|
|
381
476
|
const batchOpen = sink.find((l) => /^BATCH \+\S+ chathistory #aws-chathist/.test(l));
|
|
382
477
|
expect(batchOpen).toBeDefined();
|
|
@@ -385,16 +480,19 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
385
480
|
});
|
|
386
481
|
|
|
387
482
|
it('completes SASL PLAIN with 900/903 when an AccountStore is bound', async () => {
|
|
388
|
-
await dispatch(
|
|
483
|
+
await dispatch(
|
|
484
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
485
|
+
requireFx().deps,
|
|
486
|
+
);
|
|
389
487
|
const sink: string[] = [];
|
|
390
|
-
|
|
488
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
391
489
|
for (const line of data.split('\r\n')) {
|
|
392
490
|
if (line.length > 0) sink.push(line);
|
|
393
491
|
}
|
|
394
492
|
});
|
|
395
493
|
// Swap in a deps with a seeded AccountStore for this dispatch.
|
|
396
494
|
const depsWithAccounts: HandlerDeps = {
|
|
397
|
-
...
|
|
495
|
+
...requireFx().deps,
|
|
398
496
|
accounts: new InMemoryAccountStore([{ username: 'aws-sasl', password: 's3cret' }]),
|
|
399
497
|
};
|
|
400
498
|
const payload = Buffer.from('\0aws-sasl\0s3cret', 'utf8').toString('base64');
|
|
@@ -411,9 +509,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
411
509
|
});
|
|
412
510
|
|
|
413
511
|
it('emits 904 when no AccountStore is bound (regression guard)', async () => {
|
|
414
|
-
await dispatch(
|
|
512
|
+
await dispatch(
|
|
513
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c2' } },
|
|
514
|
+
requireFx().deps,
|
|
515
|
+
);
|
|
415
516
|
const sink: string[] = [];
|
|
416
|
-
|
|
517
|
+
requireFx().mgmt.register('c2', (data) => {
|
|
417
518
|
for (const line of data.split('\r\n')) {
|
|
418
519
|
if (line.length > 0) sink.push(line);
|
|
419
520
|
}
|
|
@@ -424,7 +525,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
424
525
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
425
526
|
body: `CAP REQ :sasl\r\nAUTHENTICATE PLAIN\r\nAUTHENTICATE ${payload}\r\n`,
|
|
426
527
|
},
|
|
427
|
-
|
|
528
|
+
requireFx().deps,
|
|
428
529
|
);
|
|
429
530
|
expect(sink.some((l) => l.includes(' 904 '))).toBe(true);
|
|
430
531
|
});
|
|
@@ -446,7 +547,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
446
547
|
it('returns 200 (APIGW only forwards the three known routes)', async () => {
|
|
447
548
|
const res = await dispatch(
|
|
448
549
|
{ requestContext: { routeKey: '$custom', connectionId: 'c1' } },
|
|
449
|
-
|
|
550
|
+
requireFx().deps,
|
|
450
551
|
);
|
|
451
552
|
expect(res.statusCode).toBe(200);
|
|
452
553
|
});
|
|
@@ -462,7 +563,7 @@ class LocalPostToConnection implements PostToConnection {
|
|
|
462
563
|
register(connId: string, sink: (data: string) => void): void {
|
|
463
564
|
this.sinks.set(connId, sink);
|
|
464
565
|
}
|
|
465
|
-
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<
|
|
566
|
+
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<unknown> {
|
|
466
567
|
const sink = this.sinks.get(input.ConnectionId);
|
|
467
568
|
if (sink !== undefined) sink(input.Data);
|
|
468
569
|
return {};
|