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
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { mockClient } from 'aws-sdk-client-mock';
|
|
2
1
|
import { DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SERVER_CONFIG,
|
|
4
|
+
type ParsedServerConfig,
|
|
5
|
+
type SaslPayload,
|
|
6
|
+
} from '@serverless-ircd/irc-core';
|
|
7
|
+
import { mockClient } from 'aws-sdk-client-mock';
|
|
4
8
|
import { describe, expect, it } from 'vitest';
|
|
5
9
|
import {
|
|
6
10
|
DynamoAccountStore,
|
|
11
|
+
type HashedAccountCredential,
|
|
7
12
|
bindAccountStore,
|
|
8
13
|
hashAccountCredential,
|
|
9
|
-
type HashedAccountCredential,
|
|
10
14
|
putAccountCredential,
|
|
11
15
|
resolveAccountStore,
|
|
12
16
|
} from '../src/account-store';
|
|
@@ -28,7 +32,7 @@ describe('bindAccountStore', () => {
|
|
|
28
32
|
it('returns an AccountStore that accepts valid PLAIN creds', () => {
|
|
29
33
|
const store = bindAccountStore(configWith([{ username: 'alice', password: 'secret' }]));
|
|
30
34
|
expect(store).toBeDefined();
|
|
31
|
-
const out = store
|
|
35
|
+
const out = store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'secret' });
|
|
32
36
|
expect(out).toEqual({ ok: true, account: 'alice' });
|
|
33
37
|
});
|
|
34
38
|
|
|
@@ -41,8 +45,9 @@ describe('bindAccountStore', () => {
|
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
it('rejects wrong credentials', () => {
|
|
44
|
-
const store = bindAccountStore(configWith([{ username: 'alice', password: 'secret' }]))
|
|
45
|
-
expect(store.
|
|
48
|
+
const store = bindAccountStore(configWith([{ username: 'alice', password: 'secret' }]));
|
|
49
|
+
expect(store).toBeDefined();
|
|
50
|
+
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'wrong' }).ok).toBe(
|
|
46
51
|
false,
|
|
47
52
|
);
|
|
48
53
|
});
|
|
@@ -177,16 +182,14 @@ describe('resolveAccountStore', () => {
|
|
|
177
182
|
const store = await resolveAccountStore(
|
|
178
183
|
ddb as unknown as DynamoDBDocumentClient,
|
|
179
184
|
'Accounts',
|
|
180
|
-
fallback === undefined
|
|
185
|
+
fallback === undefined
|
|
186
|
+
? undefined
|
|
187
|
+
: { ...DEFAULT_SERVER_CONFIG, saslAccounts: [{ username: 'envuser', password: 'env-pw' }] },
|
|
181
188
|
);
|
|
182
189
|
expect(store).toBeDefined();
|
|
183
|
-
expect(
|
|
184
|
-
store!.verify('PLAIN', plainPayload('alice', 's3cret')).ok,
|
|
185
|
-
).toBe(true);
|
|
190
|
+
expect(store?.verify('PLAIN', plainPayload('alice', 's3cret')).ok).toBe(true);
|
|
186
191
|
// Config account does NOT verify (table is authoritative).
|
|
187
|
-
expect(
|
|
188
|
-
store!.verify('PLAIN', plainPayload('envuser', 'env-pw')).ok,
|
|
189
|
-
).toBe(false);
|
|
192
|
+
expect(store?.verify('PLAIN', plainPayload('envuser', 'env-pw')).ok).toBe(false);
|
|
190
193
|
});
|
|
191
194
|
|
|
192
195
|
it('falls back to config-seeded store when the table is empty', async () => {
|
|
@@ -199,9 +202,7 @@ describe('resolveAccountStore', () => {
|
|
|
199
202
|
configWith([{ username: 'bob', password: 'pw' }]),
|
|
200
203
|
);
|
|
201
204
|
expect(store).toBeDefined();
|
|
202
|
-
expect(
|
|
203
|
-
store!.verify('PLAIN', plainPayload('bob', 'pw')).ok,
|
|
204
|
-
).toBe(true);
|
|
205
|
+
expect(store?.verify('PLAIN', plainPayload('bob', 'pw')).ok).toBe(true);
|
|
205
206
|
});
|
|
206
207
|
|
|
207
208
|
it('falls back to config-seeded store when the scan throws', async () => {
|
|
@@ -214,9 +215,7 @@ describe('resolveAccountStore', () => {
|
|
|
214
215
|
configWith([{ username: 'bob', password: 'pw' }]),
|
|
215
216
|
);
|
|
216
217
|
expect(store).toBeDefined();
|
|
217
|
-
expect(
|
|
218
|
-
store!.verify('PLAIN', plainPayload('bob', 'pw')).ok,
|
|
219
|
-
).toBe(true);
|
|
218
|
+
expect(store?.verify('PLAIN', plainPayload('bob', 'pw')).ok).toBe(true);
|
|
220
219
|
});
|
|
221
220
|
|
|
222
221
|
it('returns undefined when table is empty and no config accounts', async () => {
|
|
@@ -252,7 +251,7 @@ describe('putAccountCredential', () => {
|
|
|
252
251
|
|
|
253
252
|
const calls = ddb.commandCalls(PutCommand);
|
|
254
253
|
expect(calls).toHaveLength(1);
|
|
255
|
-
const item = calls[0]
|
|
254
|
+
const item = calls[0]?.args[0].input.Item as HashedAccountCredential;
|
|
256
255
|
expect(item.account).toBe('alice');
|
|
257
256
|
expect(item.algorithm).toBe('scrypt');
|
|
258
257
|
expect(item.salt).toBe(entry.salt);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure unit tests for the AWS `$connect` admission policy
|
|
3
|
+
* (`src/admission.ts`). No DynamoDB — these exercise every branch of
|
|
4
|
+
* {@link decideConnectAdmission} directly so the per-IP branch is covered
|
|
5
|
+
* even though the AWS handler does not yet feed it a `perIp` count.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { decideConnectAdmission } from '../src/admission.js';
|
|
10
|
+
|
|
11
|
+
describe('decideConnectAdmission', () => {
|
|
12
|
+
it('admits when the total is below the cap and no per-IP limit is set', () => {
|
|
13
|
+
const outcome = decideConnectAdmission({ total: 0 }, { maxClients: 2 });
|
|
14
|
+
expect(outcome).toEqual({ admitted: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('admits when the total is exactly one below the cap', () => {
|
|
18
|
+
expect(decideConnectAdmission({ total: 1 }, { maxClients: 2 })).toEqual({
|
|
19
|
+
admitted: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects with 429 once the total reaches the cap', () => {
|
|
24
|
+
expect(decideConnectAdmission({ total: 2 }, { maxClients: 2 })).toEqual({
|
|
25
|
+
admitted: false,
|
|
26
|
+
statusCode: 429,
|
|
27
|
+
reason: 'server full',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('rejects with 429 when the total exceeds the cap', () => {
|
|
32
|
+
expect(decideConnectAdmission({ total: 9999 }, { maxClients: 10 })).toEqual({
|
|
33
|
+
admitted: false,
|
|
34
|
+
statusCode: 429,
|
|
35
|
+
reason: 'server full',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('admits when a per-IP limit is set but no per-IP count is supplied', () => {
|
|
40
|
+
expect(
|
|
41
|
+
decideConnectAdmission({ total: 0 }, { maxClients: 10, maxConnectionsPerIp: 3 }),
|
|
42
|
+
).toEqual({ admitted: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('admits when the per-IP count is below the per-IP limit', () => {
|
|
46
|
+
expect(
|
|
47
|
+
decideConnectAdmission({ total: 0, perIp: 2 }, { maxClients: 10, maxConnectionsPerIp: 3 }),
|
|
48
|
+
).toEqual({ admitted: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('rejects with 429 once the per-IP count reaches the per-IP limit', () => {
|
|
52
|
+
expect(
|
|
53
|
+
decideConnectAdmission({ total: 0, perIp: 3 }, { maxClients: 10, maxConnectionsPerIp: 3 }),
|
|
54
|
+
).toEqual({
|
|
55
|
+
admitted: false,
|
|
56
|
+
statusCode: 429,
|
|
57
|
+
reason: 'too many connections from this IP',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('checks the global cap before the per-IP cap', () => {
|
|
62
|
+
expect(
|
|
63
|
+
decideConnectAdmission({ total: 10, perIp: 3 }, { maxClients: 10, maxConnectionsPerIp: 3 }),
|
|
64
|
+
).toEqual({
|
|
65
|
+
admitted: false,
|
|
66
|
+
statusCode: 429,
|
|
67
|
+
reason: 'server full',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -57,7 +57,8 @@ const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
|
57
57
|
* unavailable so `describe.skipIf` short-circuits before any test runs.
|
|
58
58
|
*/
|
|
59
59
|
export const awsHarnessFactory: IrcHarnessFactory = Object.freeze({
|
|
60
|
-
name: 'aws',
|
|
60
|
+
name: 'aws+ws',
|
|
61
|
+
transport: 'ws',
|
|
61
62
|
async create() {
|
|
62
63
|
return new AwsHarness();
|
|
63
64
|
},
|
|
@@ -344,6 +345,17 @@ class AwsClientHandle implements ClientHarness {
|
|
|
344
345
|
await this.actor.receiveTextFrame(`${line}\r\n`);
|
|
345
346
|
}
|
|
346
347
|
|
|
348
|
+
async feed(chunk: string): Promise<void> {
|
|
349
|
+
if (this.actor === undefined) {
|
|
350
|
+
throw new Error(`client ${this.id} has no bound actor`);
|
|
351
|
+
}
|
|
352
|
+
if (this.closed) return;
|
|
353
|
+
// Routes the raw chunk through the bound transport (the WS text-frame
|
|
354
|
+
// transport by default; a future aws+tcp factory via the NLB+Lambda
|
|
355
|
+
// byte-stream path would inject a TcpByteStreamTransport).
|
|
356
|
+
await this.actor.receive(chunk);
|
|
357
|
+
}
|
|
358
|
+
|
|
347
359
|
async waitForLine(
|
|
348
360
|
predicate: (line: string) => boolean,
|
|
349
361
|
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
@@ -76,6 +76,26 @@ describe('buildLambdaConfigInput', () => {
|
|
|
76
76
|
expect(buildLambdaConfigInput({ QUIT_MESSAGE: 'bye' }).quitMessage).toBe('bye');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
it('threads SERVER_VERSION into serverVersion', () => {
|
|
80
|
+
expect(buildLambdaConfigInput({ SERVER_VERSION: '1.2.3' }).serverVersion).toBe('1.2.3');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('passes a non-numeric CREATED_AT through verbatim', () => {
|
|
84
|
+
expect(buildLambdaConfigInput({ CREATED_AT: '2024-06-01' }).createdAt).toBe('2024-06-01');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('parses a numeric CREATED_AT string into an epoch-ms number', () => {
|
|
88
|
+
expect(buildLambdaConfigInput({ CREATED_AT: '1700000000000' }).createdAt).toBe(
|
|
89
|
+
1_700_000_000_000,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('omits serverVersion/createdAt when their env vars are unset', () => {
|
|
94
|
+
const out = buildLambdaConfigInput({});
|
|
95
|
+
expect('serverVersion' in out).toBe(false);
|
|
96
|
+
expect('createdAt' in out).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
79
99
|
it('parses SASL_ACCOUNTS newline-delimited username:password pairs', () => {
|
|
80
100
|
const out = buildLambdaConfigInput({ SASL_ACCOUNTS: 'alice:secret\nbob:b-pass' });
|
|
81
101
|
expect(out.saslAccounts).toEqual([
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `handleConnect` that exercise the DynamoDB plumbing
|
|
3
|
+
* branches the integration suite cannot reach cheaply — namely the
|
|
4
|
+
* paginated `Scan COUNT` loop (a real DynamoDB Local table fits in a
|
|
5
|
+
* single page) and the `Count ?? 0` defensive fallback. A hand-rolled
|
|
6
|
+
* stub client stands in for `DynamoDBDocumentClient`; the admission
|
|
7
|
+
* policy itself is covered by `admission.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
11
|
+
import { DEFAULT_SERVER_CONFIG } from '@serverless-ircd/irc-core';
|
|
12
|
+
import { describe, expect, it } from 'vitest';
|
|
13
|
+
import { handleConnect } from '../src/handlers/connect.js';
|
|
14
|
+
import type { TablesConfig } from '../src/tables.js';
|
|
15
|
+
|
|
16
|
+
const TABLES: TablesConfig = {
|
|
17
|
+
Connections: 'Connections',
|
|
18
|
+
ChannelMeta: 'ChannelMeta',
|
|
19
|
+
ChannelMembers: 'ChannelMembers',
|
|
20
|
+
Nicks: 'Nicks',
|
|
21
|
+
Accounts: 'Accounts',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface Stub {
|
|
25
|
+
dynamo: DynamoDBDocumentClient;
|
|
26
|
+
puts: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Builds a stub client that serves the supplied `Scan` pages in order. */
|
|
30
|
+
function makeStub(pages: Array<Record<string, unknown>>): Stub {
|
|
31
|
+
let scanIdx = 0;
|
|
32
|
+
const calls = { puts: 0 };
|
|
33
|
+
const dynamo = {
|
|
34
|
+
send: async (cmd: unknown) => {
|
|
35
|
+
if (cmd instanceof ScanCommand) {
|
|
36
|
+
return pages[scanIdx++] ?? { Count: 0 };
|
|
37
|
+
}
|
|
38
|
+
if (cmd instanceof PutCommand) {
|
|
39
|
+
calls.puts++;
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
throw new Error('unexpected command');
|
|
43
|
+
},
|
|
44
|
+
} as unknown as DynamoDBDocumentClient;
|
|
45
|
+
return {
|
|
46
|
+
dynamo,
|
|
47
|
+
get puts() {
|
|
48
|
+
return calls.puts;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('handleConnect admission plumbing', () => {
|
|
54
|
+
it('sums Scan COUNT across pages and admits (writes the row) when below the cap', async () => {
|
|
55
|
+
const stub = makeStub([{ Count: 1, LastEvaluatedKey: { connectionId: 'page-1' } }, {}]);
|
|
56
|
+
const outcome = await handleConnect({
|
|
57
|
+
dynamo: stub.dynamo,
|
|
58
|
+
tables: TABLES,
|
|
59
|
+
connectionId: 'c-paged',
|
|
60
|
+
now: 42,
|
|
61
|
+
serverConfig: { ...DEFAULT_SERVER_CONFIG, maxClients: 5 },
|
|
62
|
+
});
|
|
63
|
+
expect(outcome).toEqual({ admitted: true });
|
|
64
|
+
expect(stub.puts).toBe(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not write a row when the count reaches the cap', async () => {
|
|
68
|
+
const stub = makeStub([{ Count: 5 }]);
|
|
69
|
+
const outcome = await handleConnect({
|
|
70
|
+
dynamo: stub.dynamo,
|
|
71
|
+
tables: TABLES,
|
|
72
|
+
connectionId: 'c-full',
|
|
73
|
+
serverConfig: { ...DEFAULT_SERVER_CONFIG, maxClients: 5 },
|
|
74
|
+
});
|
|
75
|
+
expect(outcome).toEqual({ admitted: false, statusCode: 429, reason: 'server full' });
|
|
76
|
+
expect(stub.puts).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -19,15 +19,11 @@ import {
|
|
|
19
19
|
createConnection,
|
|
20
20
|
} from '@serverless-ircd/irc-core';
|
|
21
21
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
22
|
-
import { AwsRuntime,
|
|
23
|
-
import { handleDisconnect } from '../src/handlers/disconnect.js';
|
|
24
|
-
import {
|
|
25
|
-
type HandlerDeps,
|
|
26
|
-
__resetHandlerCacheForTests,
|
|
27
|
-
dispatch,
|
|
28
|
-
} from '../src/handlers/index.js';
|
|
22
|
+
import { AwsRuntime, type PostToConnection, cleanupConnection } from '../src/aws-runtime.js';
|
|
29
23
|
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
30
24
|
import { createDynamoDocumentClient } from '../src/dynamo.js';
|
|
25
|
+
import { handleDisconnect } from '../src/handlers/disconnect.js';
|
|
26
|
+
import { type HandlerDeps, __resetHandlerCacheForTests, dispatch } from '../src/handlers/index.js';
|
|
31
27
|
import { marshalChannelMember, marshalConnection, marshalNick } from '../src/serialize.js';
|
|
32
28
|
import type { TablesConfig } from '../src/tables.js';
|
|
33
29
|
|
|
@@ -49,6 +45,11 @@ interface Fixture {
|
|
|
49
45
|
|
|
50
46
|
let fx: Fixture | null;
|
|
51
47
|
|
|
48
|
+
function requireFx(): Fixture {
|
|
49
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
50
|
+
return fx;
|
|
51
|
+
}
|
|
52
|
+
|
|
52
53
|
async function makeFixture(): Promise<Fixture> {
|
|
53
54
|
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
54
55
|
const prefix = `q${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
@@ -182,13 +183,13 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
182
183
|
});
|
|
183
184
|
|
|
184
185
|
it('handleDisconnect fans the QUIT line out to every other channel member', async () => {
|
|
185
|
-
await seedConnection(
|
|
186
|
-
await seedConnection(
|
|
186
|
+
await seedConnection(requireFx().client, requireFx().tables, 'a', 'alice', '#room');
|
|
187
|
+
await seedConnection(requireFx().client, requireFx().tables, 'b', 'bob', '#room');
|
|
187
188
|
|
|
188
189
|
const mgmt = new RecordingPostToConnection();
|
|
189
190
|
await handleDisconnect({
|
|
190
|
-
dynamo:
|
|
191
|
-
tables:
|
|
191
|
+
dynamo: requireFx().client,
|
|
192
|
+
tables: requireFx().tables,
|
|
192
193
|
connectionId: 'a',
|
|
193
194
|
managementApi: mgmt,
|
|
194
195
|
});
|
|
@@ -201,14 +202,14 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
201
202
|
});
|
|
202
203
|
|
|
203
204
|
it('handleDisconnect without a managementApi performs no fanout but still cleans up', async () => {
|
|
204
|
-
await seedConnection(
|
|
205
|
+
await seedConnection(requireFx().client, requireFx().tables, 'a', 'alice', '#room');
|
|
205
206
|
await handleDisconnect({
|
|
206
|
-
dynamo:
|
|
207
|
-
tables:
|
|
207
|
+
dynamo: requireFx().client,
|
|
208
|
+
tables: requireFx().tables,
|
|
208
209
|
connectionId: 'a',
|
|
209
210
|
managementApi: null,
|
|
210
211
|
});
|
|
211
|
-
const p = probe(
|
|
212
|
+
const p = probe(requireFx().client, requireFx().tables);
|
|
212
213
|
expect(await p.getConnectionInfo('a')).toBeNull();
|
|
213
214
|
expect(await p.lookupNick('alice' as Nick)).toBeNull();
|
|
214
215
|
});
|
|
@@ -216,8 +217,8 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
216
217
|
it('a never-registered connection disconnects without fanout (no nick)', async () => {
|
|
217
218
|
const mgmt = new RecordingPostToConnection();
|
|
218
219
|
await handleDisconnect({
|
|
219
|
-
dynamo:
|
|
220
|
-
tables:
|
|
220
|
+
dynamo: requireFx().client,
|
|
221
|
+
tables: requireFx().tables,
|
|
221
222
|
connectionId: 'nobody',
|
|
222
223
|
managementApi: mgmt,
|
|
223
224
|
});
|
|
@@ -225,43 +226,43 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
225
226
|
});
|
|
226
227
|
|
|
227
228
|
it('fanout to a gone peer triggers lazy cleanup instead of crashing', async () => {
|
|
228
|
-
await seedConnection(
|
|
229
|
-
await seedConnection(
|
|
229
|
+
await seedConnection(requireFx().client, requireFx().tables, 'a', 'alice', '#room');
|
|
230
|
+
await seedConnection(requireFx().client, requireFx().tables, 'b', 'bob', '#room');
|
|
230
231
|
|
|
231
232
|
const mgmt = new RecordingPostToConnection();
|
|
232
233
|
mgmt.markGone('b');
|
|
233
234
|
|
|
234
235
|
await expect(
|
|
235
236
|
handleDisconnect({
|
|
236
|
-
dynamo:
|
|
237
|
-
tables:
|
|
237
|
+
dynamo: requireFx().client,
|
|
238
|
+
tables: requireFx().tables,
|
|
238
239
|
connectionId: 'a',
|
|
239
240
|
managementApi: mgmt,
|
|
240
241
|
}),
|
|
241
242
|
).resolves.toBeUndefined();
|
|
242
243
|
|
|
243
244
|
// The gone peer b was lazily cleaned up (Connection row removed).
|
|
244
|
-
const p = probe(
|
|
245
|
+
const p = probe(requireFx().client, requireFx().tables);
|
|
245
246
|
expect(await p.getConnectionInfo('b')).toBeNull();
|
|
246
247
|
});
|
|
247
248
|
|
|
248
249
|
it('cleanupConnection directly broadcasts QUIT when given a managementApi', async () => {
|
|
249
|
-
await seedConnection(
|
|
250
|
-
await seedConnection(
|
|
250
|
+
await seedConnection(requireFx().client, requireFx().tables, 'a', 'alice', '#room');
|
|
251
|
+
await seedConnection(requireFx().client, requireFx().tables, 'b', 'bob', '#room');
|
|
251
252
|
|
|
252
253
|
const mgmt = new RecordingPostToConnection();
|
|
253
|
-
await cleanupConnection(
|
|
254
|
+
await cleanupConnection(requireFx().client, requireFx().tables, 'a', mgmt);
|
|
254
255
|
|
|
255
256
|
const bobReceived = mgmt.sent.get('b') ?? [];
|
|
256
257
|
expect(bobReceived).toContain(':alice!alice@x QUIT :Client Quit');
|
|
257
|
-
const p = probe(
|
|
258
|
+
const p = probe(requireFx().client, requireFx().tables);
|
|
258
259
|
expect(await p.getConnectionInfo('a')).toBeNull();
|
|
259
260
|
});
|
|
260
261
|
|
|
261
262
|
it('the $disconnect dispatch route fans QUIT out to peers via the management API', async () => {
|
|
262
263
|
const deps: HandlerDeps = {
|
|
263
|
-
dynamo:
|
|
264
|
-
tables:
|
|
264
|
+
dynamo: requireFx().client,
|
|
265
|
+
tables: requireFx().tables,
|
|
265
266
|
serverConfig: SERVER_CONFIG,
|
|
266
267
|
motd: new StaticMotdProvider([]),
|
|
267
268
|
messages: { record() {}, query: () => Promise.resolve([]) } as never,
|
|
@@ -273,11 +274,17 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
273
274
|
await dispatch({ requestContext: { routeKey: '$connect', connectionId: 'a' } }, deps);
|
|
274
275
|
await dispatch({ requestContext: { routeKey: '$connect', connectionId: 'b' } }, deps);
|
|
275
276
|
await dispatch(
|
|
276
|
-
{
|
|
277
|
+
{
|
|
278
|
+
requestContext: { routeKey: '$default', connectionId: 'a' },
|
|
279
|
+
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\nJOIN #room\r\n',
|
|
280
|
+
},
|
|
277
281
|
deps,
|
|
278
282
|
);
|
|
279
283
|
await dispatch(
|
|
280
|
-
{
|
|
284
|
+
{
|
|
285
|
+
requestContext: { routeKey: '$default', connectionId: 'b' },
|
|
286
|
+
body: 'NICK bob\r\nUSER bob 0 * :Bob\r\nJOIN #room\r\n',
|
|
287
|
+
},
|
|
281
288
|
deps,
|
|
282
289
|
);
|
|
283
290
|
mgmt.sent.clear();
|
|
@@ -289,18 +296,18 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
289
296
|
expect(bobReceived.some((l) => /^:alice!alice\S* QUIT :/.test(l))).toBe(true);
|
|
290
297
|
});
|
|
291
298
|
|
|
292
|
-
it(
|
|
299
|
+
it("the send() GoneException cleanup path also broadcasts QUIT to the gone peer's channels", async () => {
|
|
293
300
|
// alice and carol share #room; bob shares a different channel with carol.
|
|
294
|
-
await seedConnection(
|
|
295
|
-
await seedConnection(
|
|
301
|
+
await seedConnection(requireFx().client, requireFx().tables, 'alice', 'alice', '#room');
|
|
302
|
+
await seedConnection(requireFx().client, requireFx().tables, 'carol', 'carol', '#room');
|
|
296
303
|
|
|
297
304
|
// A management double where alice is "gone" (raises GoneException).
|
|
298
305
|
const mgmt = new RecordingPostToConnection();
|
|
299
306
|
mgmt.markGone('alice');
|
|
300
307
|
|
|
301
308
|
const runtime = new AwsRuntime({
|
|
302
|
-
dynamo:
|
|
303
|
-
tables:
|
|
309
|
+
dynamo: requireFx().client,
|
|
310
|
+
tables: requireFx().tables,
|
|
304
311
|
connId: 'carol',
|
|
305
312
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
306
313
|
managementApi: mgmt,
|
|
@@ -313,21 +320,21 @@ describe.skipIf(!available)('$disconnect QUIT fanout', () => {
|
|
|
313
320
|
const carolReceived = mgmt.sent.get('carol') ?? [];
|
|
314
321
|
expect(carolReceived.some((l) => l.startsWith(':alice!alice@x QUIT :'))).toBe(true);
|
|
315
322
|
// alice was cleaned up.
|
|
316
|
-
const p = probe(
|
|
323
|
+
const p = probe(requireFx().client, requireFx().tables);
|
|
317
324
|
expect(await p.getConnectionInfo('alice')).toBeNull();
|
|
318
325
|
});
|
|
319
326
|
|
|
320
327
|
it('fanout to a peer that throws a non-Gone error rethrows (no silent swallow)', async () => {
|
|
321
|
-
await seedConnection(
|
|
322
|
-
await seedConnection(
|
|
328
|
+
await seedConnection(requireFx().client, requireFx().tables, 'a', 'alice', '#room');
|
|
329
|
+
await seedConnection(requireFx().client, requireFx().tables, 'b', 'bob', '#room');
|
|
323
330
|
|
|
324
331
|
const mgmt = new RecordingPostToConnection();
|
|
325
332
|
mgmt.markError('b', new Error('transient network failure'));
|
|
326
333
|
|
|
327
334
|
await expect(
|
|
328
335
|
handleDisconnect({
|
|
329
|
-
dynamo:
|
|
330
|
-
tables:
|
|
336
|
+
dynamo: requireFx().client,
|
|
337
|
+
tables: requireFx().tables,
|
|
331
338
|
connectionId: 'a',
|
|
332
339
|
managementApi: mgmt,
|
|
333
340
|
}),
|
|
@@ -42,6 +42,11 @@ interface Fixture {
|
|
|
42
42
|
|
|
43
43
|
let fx: Fixture | null;
|
|
44
44
|
|
|
45
|
+
function requireFx(): Fixture {
|
|
46
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
47
|
+
return fx;
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
async function makeFixture(): Promise<Fixture> {
|
|
46
51
|
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
47
52
|
const prefix = `g${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
@@ -136,7 +141,7 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
136
141
|
});
|
|
137
142
|
|
|
138
143
|
it('send() to a gone connection triggers cleanupConnection and deletes Connections/Nicks/Members rows', async () => {
|
|
139
|
-
await seedConnection(
|
|
144
|
+
await seedConnection(requireFx().client, requireFx().tables, 'gone', 'bob', '#room');
|
|
140
145
|
|
|
141
146
|
// A self-connection runtime that tries to `send` to 'gone'.
|
|
142
147
|
const mgmt: PostToConnection = {
|
|
@@ -149,8 +154,8 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
149
154
|
};
|
|
150
155
|
|
|
151
156
|
const runtime = new AwsRuntime({
|
|
152
|
-
dynamo:
|
|
153
|
-
tables:
|
|
157
|
+
dynamo: requireFx().client,
|
|
158
|
+
tables: requireFx().tables,
|
|
154
159
|
connId: 'self',
|
|
155
160
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
156
161
|
managementApi: mgmt,
|
|
@@ -159,8 +164,8 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
159
164
|
await runtime.send('gone', [{ text: ':self PRIVMSG bob :hi' } as never]);
|
|
160
165
|
|
|
161
166
|
const probe = new AwsRuntime({
|
|
162
|
-
dynamo:
|
|
163
|
-
tables:
|
|
167
|
+
dynamo: requireFx().client,
|
|
168
|
+
tables: requireFx().tables,
|
|
164
169
|
connId: '__probe__',
|
|
165
170
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
166
171
|
managementApi: null,
|
|
@@ -172,15 +177,15 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
172
177
|
});
|
|
173
178
|
|
|
174
179
|
it('send() to a non-gone error rethrows', async () => {
|
|
175
|
-
await seedConnection(
|
|
180
|
+
await seedConnection(requireFx().client, requireFx().tables, 'present', 'carol', '#room');
|
|
176
181
|
const mgmt: PostToConnection = {
|
|
177
182
|
async postToConnection() {
|
|
178
183
|
throw new Error('network down');
|
|
179
184
|
},
|
|
180
185
|
};
|
|
181
186
|
const runtime = new AwsRuntime({
|
|
182
|
-
dynamo:
|
|
183
|
-
tables:
|
|
187
|
+
dynamo: requireFx().client,
|
|
188
|
+
tables: requireFx().tables,
|
|
184
189
|
connId: 'self',
|
|
185
190
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
186
191
|
managementApi: mgmt,
|
|
@@ -190,8 +195,8 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
190
195
|
).rejects.toThrow('network down');
|
|
191
196
|
// The target row should NOT have been cleaned up.
|
|
192
197
|
const probe = new AwsRuntime({
|
|
193
|
-
dynamo:
|
|
194
|
-
tables:
|
|
198
|
+
dynamo: requireFx().client,
|
|
199
|
+
tables: requireFx().tables,
|
|
195
200
|
connId: '__probe__',
|
|
196
201
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
197
202
|
managementApi: null,
|
|
@@ -200,12 +205,12 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
200
205
|
});
|
|
201
206
|
|
|
202
207
|
it('cleanupConnection is idempotent when called twice', async () => {
|
|
203
|
-
await seedConnection(
|
|
204
|
-
await cleanupConnection(
|
|
205
|
-
await cleanupConnection(
|
|
208
|
+
await seedConnection(requireFx().client, requireFx().tables, 'gone2', 'dave', '#room');
|
|
209
|
+
await cleanupConnection(requireFx().client, requireFx().tables, 'gone2', null);
|
|
210
|
+
await cleanupConnection(requireFx().client, requireFx().tables, 'gone2', null);
|
|
206
211
|
const probe = new AwsRuntime({
|
|
207
|
-
dynamo:
|
|
208
|
-
tables:
|
|
212
|
+
dynamo: requireFx().client,
|
|
213
|
+
tables: requireFx().tables,
|
|
209
214
|
connId: '__probe__',
|
|
210
215
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
211
216
|
managementApi: null,
|
|
@@ -215,20 +220,20 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
215
220
|
|
|
216
221
|
it('cleanupConnection is safe when the connection row is missing', async () => {
|
|
217
222
|
await expect(
|
|
218
|
-
cleanupConnection(
|
|
223
|
+
cleanupConnection(requireFx().client, requireFx().tables, 'never-existed', null),
|
|
219
224
|
).resolves.toBeUndefined();
|
|
220
225
|
});
|
|
221
226
|
|
|
222
227
|
it('broadcast() skips and cleans up gone members', async () => {
|
|
223
|
-
await seedConnection(
|
|
228
|
+
await seedConnection(requireFx().client, requireFx().tables, 'gone3', 'eve', '#room');
|
|
224
229
|
const mgmt: PostToConnection = {
|
|
225
230
|
async postToConnection() {
|
|
226
231
|
throw new GoneException({ $metadata: {}, message: 'Gone' });
|
|
227
232
|
},
|
|
228
233
|
};
|
|
229
234
|
const runtime = new AwsRuntime({
|
|
230
|
-
dynamo:
|
|
231
|
-
tables:
|
|
235
|
+
dynamo: requireFx().client,
|
|
236
|
+
tables: requireFx().tables,
|
|
232
237
|
connId: 'self',
|
|
233
238
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
234
239
|
managementApi: mgmt,
|
|
@@ -236,8 +241,8 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
236
241
|
// `broadcast` iterates ChannelMembers and skips `except`.
|
|
237
242
|
await runtime.broadcast('#room' as never, [{ text: ':self PRIVMSG #room :hi' } as never]);
|
|
238
243
|
const probe = new AwsRuntime({
|
|
239
|
-
dynamo:
|
|
240
|
-
tables:
|
|
244
|
+
dynamo: requireFx().client,
|
|
245
|
+
tables: requireFx().tables,
|
|
241
246
|
connId: '__probe__',
|
|
242
247
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
243
248
|
managementApi: null,
|
|
@@ -246,18 +251,18 @@ describe.skipIf(!available)('GoneException cleanup', () => {
|
|
|
246
251
|
});
|
|
247
252
|
|
|
248
253
|
it('send() with managementApi=null is a silent no-op (no throw, no cleanup)', async () => {
|
|
249
|
-
await seedConnection(
|
|
254
|
+
await seedConnection(requireFx().client, requireFx().tables, 'present2', 'frank', '#room');
|
|
250
255
|
const runtime = new AwsRuntime({
|
|
251
|
-
dynamo:
|
|
252
|
-
tables:
|
|
256
|
+
dynamo: requireFx().client,
|
|
257
|
+
tables: requireFx().tables,
|
|
253
258
|
connId: 'self',
|
|
254
259
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
255
260
|
managementApi: null,
|
|
256
261
|
});
|
|
257
262
|
await runtime.send('present2', [{ text: ':self PRIVMSG frank :hi' } as never]);
|
|
258
263
|
const probe = new AwsRuntime({
|
|
259
|
-
dynamo:
|
|
260
|
-
tables:
|
|
264
|
+
dynamo: requireFx().client,
|
|
265
|
+
tables: requireFx().tables,
|
|
261
266
|
connId: '__probe__',
|
|
262
267
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
263
268
|
managementApi: null,
|