serverless-ircd 0.9.0 → 0.11.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 +28 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +398 -67
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +714 -105
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +18 -14
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +95 -77
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +161 -11
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +9 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +529 -190
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -22
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +563 -54
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the pure helpers exported from `tools/migrate-accounts-to-services.ts`.
|
|
3
|
+
*
|
|
4
|
+
* The helpers are side-effect-free (the CLI entry point is guarded behind
|
|
5
|
+
* `void main()` which only runs when the module is executed directly), so
|
|
6
|
+
* they can be imported and tested without triggering wrangler / DynamoDB
|
|
7
|
+
* IO.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type LegacyAccountRow,
|
|
12
|
+
buildMigrationSql,
|
|
13
|
+
buildServicesPutItem,
|
|
14
|
+
parseSaslAccountsForMigration,
|
|
15
|
+
} from '@serverless-ircd/irc-core';
|
|
16
|
+
import { describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
describe('parseSaslAccountsForMigration', () => {
|
|
19
|
+
it('returns an empty array for undefined', () => {
|
|
20
|
+
expect(parseSaslAccountsForMigration(undefined)).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns an empty array for an empty string', () => {
|
|
24
|
+
expect(parseSaslAccountsForMigration('')).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('parses a single username:password entry', () => {
|
|
28
|
+
expect(parseSaslAccountsForMigration('alice:s3cret')).toEqual([
|
|
29
|
+
{ username: 'alice', password: 's3cret' },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('parses multiple newline-delimited entries', () => {
|
|
34
|
+
expect(parseSaslAccountsForMigration('alice:s3cret\nbob:pw')).toEqual([
|
|
35
|
+
{ username: 'alice', password: 's3cret' },
|
|
36
|
+
{ username: 'bob', password: 'pw' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('skips blank lines and malformed entries', () => {
|
|
41
|
+
expect(
|
|
42
|
+
parseSaslAccountsForMigration('alice:s3cret\n\nnocolon\n:emptyuser\nemptypass:\n \n'),
|
|
43
|
+
).toEqual([{ username: 'alice', password: 's3cret' }]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('buildMigrationSql — D1 migration SQL', () => {
|
|
48
|
+
const legacyRow = (account: string, salt = 'salt', hash = 'hash'): LegacyAccountRow => ({
|
|
49
|
+
account,
|
|
50
|
+
algorithm: 'scrypt',
|
|
51
|
+
salt,
|
|
52
|
+
hash,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('(a) creates a nickserv_accounts row with cert_subjects "[]" for a legacy account', () => {
|
|
56
|
+
const stmts = buildMigrationSql([legacyRow('alice')], undefined);
|
|
57
|
+
// First statement is CREATE TABLE.
|
|
58
|
+
expect(stmts[0]).toContain('CREATE TABLE IF NOT EXISTS nickserv_accounts');
|
|
59
|
+
// Second statement is the INSERT OR IGNORE for alice.
|
|
60
|
+
expect(stmts[1]).toContain('INSERT OR IGNORE INTO nickserv_accounts');
|
|
61
|
+
expect(stmts[1]).toContain("'alice'");
|
|
62
|
+
expect(stmts[1]).toContain("'[]'");
|
|
63
|
+
expect(stmts[1]).toContain("'scrypt'");
|
|
64
|
+
expect(stmts[1]).toContain("'salt'");
|
|
65
|
+
expect(stmts[1]).toContain("'hash'");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('uses INSERT OR IGNORE (never INSERT OR REPLACE) so existing rows are never clobbered', () => {
|
|
69
|
+
const stmts = buildMigrationSql([legacyRow('alice')], undefined);
|
|
70
|
+
expect(stmts[1]).toContain('INSERT OR IGNORE');
|
|
71
|
+
expect(stmts[1]).not.toContain('INSERT OR REPLACE');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('(c) a legacy row never overwrites an existing NickServ registration (INSERT OR IGNORE)', () => {
|
|
75
|
+
// The SQL is INSERT OR IGNORE — if the nick_key already exists in
|
|
76
|
+
// nickserv_accounts, the INSERT is a no-op. The generated SQL encodes
|
|
77
|
+
// this; running it twice produces the same statements.
|
|
78
|
+
const stmts1 = buildMigrationSql([legacyRow('alice')], undefined);
|
|
79
|
+
const stmts2 = buildMigrationSql([legacyRow('alice')], undefined);
|
|
80
|
+
expect(stmts1).toEqual(stmts2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('carries SASL_ACCOUNTS env-seed entries not already in the legacy table', () => {
|
|
84
|
+
const stmts = buildMigrationSql([legacyRow('alice')], 'bob:bobspw\nalice:alicespw');
|
|
85
|
+
// CREATE TABLE + alice (legacy) + bob (env-seed).
|
|
86
|
+
// alice is in the legacy table so the env-seed alice is NOT duplicated.
|
|
87
|
+
expect(stmts).toHaveLength(3);
|
|
88
|
+
expect(stmts[1]).toContain("'alice'");
|
|
89
|
+
expect(stmts[1]).toContain("'salt'");
|
|
90
|
+
expect(stmts[2]).toContain("'bob'");
|
|
91
|
+
// bob's credential is freshly hashed from the plaintext password.
|
|
92
|
+
expect(stmts[2]).not.toContain("'bobspw'");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not re-hash legacy rows (preserves the pre-hashed scrypt credential verbatim)', () => {
|
|
96
|
+
const stmts = buildMigrationSql([legacyRow('alice', 'mysalt', 'myhash')], undefined);
|
|
97
|
+
expect(stmts[1]).toContain("'mysalt'");
|
|
98
|
+
expect(stmts[1]).toContain("'myhash'");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('produces only CREATE TABLE when both legacy rows and env-seed are empty', () => {
|
|
102
|
+
const stmts = buildMigrationSql([], undefined);
|
|
103
|
+
expect(stmts).toHaveLength(1);
|
|
104
|
+
expect(stmts[0]).toContain('CREATE TABLE');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('escapes single quotes in usernames to prevent SQL injection', () => {
|
|
108
|
+
const stmts = buildMigrationSql([legacyRow("O'Brien")], undefined);
|
|
109
|
+
expect(stmts[1]).toContain("'O''Brien'");
|
|
110
|
+
expect(stmts[1]).not.toContain("'O'Brien'");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('buildServicesPutItem — DynamoDB migration', () => {
|
|
115
|
+
it('targets the NICK:<fold> partition with the nick-type discriminator', () => {
|
|
116
|
+
const input = buildServicesPutItem(
|
|
117
|
+
{ account: 'alice', algorithm: 'scrypt', salt: 's', hash: 'h' },
|
|
118
|
+
'StagingServices',
|
|
119
|
+
);
|
|
120
|
+
expect(input.TableName).toBe('StagingServices');
|
|
121
|
+
expect(input.Item.pk).toBe('NICK:alice');
|
|
122
|
+
expect(input.Item.sk).toBe('#');
|
|
123
|
+
expect(input.Item.type).toBe('nick');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('preserves the legacy scrypt credential fields on the item', () => {
|
|
127
|
+
const input = buildServicesPutItem(
|
|
128
|
+
{ account: 'alice', algorithm: 'scrypt', salt: 'mysalt', hash: 'myhash' },
|
|
129
|
+
'Services',
|
|
130
|
+
);
|
|
131
|
+
expect(input.Item.algorithm).toBe('scrypt');
|
|
132
|
+
expect(input.Item.salt).toBe('mysalt');
|
|
133
|
+
expect(input.Item.hash).toBe('myhash');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('defaults certSubjects to an empty array', () => {
|
|
137
|
+
const input = buildServicesPutItem(
|
|
138
|
+
{ account: 'alice', algorithm: 'scrypt', salt: 's', hash: 'h' },
|
|
139
|
+
'Services',
|
|
140
|
+
);
|
|
141
|
+
expect(input.Item.certSubjects).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('folds the nick case-insensitively into the partition key', () => {
|
|
145
|
+
const input = buildServicesPutItem(
|
|
146
|
+
{ account: 'Alice', algorithm: 'scrypt', salt: 's', hash: 'h' },
|
|
147
|
+
'Services',
|
|
148
|
+
);
|
|
149
|
+
// rfc1459 folding: Alice → alice
|
|
150
|
+
expect(input.Item.pk).toBe('NICK:alice');
|
|
151
|
+
expect(input.Item.nick).toBe('Alice');
|
|
152
|
+
expect(input.Item.account).toBe('Alice');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('stamps the default email / enforce / createdAt fields', () => {
|
|
156
|
+
const input = buildServicesPutItem(
|
|
157
|
+
{ account: 'alice', algorithm: 'scrypt', salt: 's', hash: 'h' },
|
|
158
|
+
'Services',
|
|
159
|
+
);
|
|
160
|
+
expect(input.Item.email).toBe('');
|
|
161
|
+
expect(input.Item.enforce).toBe('none');
|
|
162
|
+
expect(input.Item.createdAt).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the NLB TLS secure-flag propagation (user mode `S`).
|
|
3
|
+
*
|
|
4
|
+
* The stack wires the NLB streaming Lambda exclusively behind a
|
|
5
|
+
* `Protocol.TLS` listener (ACM-cert terminated at the edge), so every
|
|
6
|
+
* connection on that entry is TLS-protected. These tests pin the
|
|
7
|
+
* propagation of that fact end-to-end:
|
|
8
|
+
* - the `nlbStreamHandler` dispatcher (`dispatchNlbStream`) asserts
|
|
9
|
+
* `secure: true`, so registration stamps read-only user mode `S`
|
|
10
|
+
* (visible via `MODE` → `221 … +S`) and the Connections row
|
|
11
|
+
* persists both `secure` and `userModes.tls`;
|
|
12
|
+
* - STS reflects the secure transport (`CAP LS` advertises the
|
|
13
|
+
* duration-only TLS form, never the plaintext `port=` upgrade form);
|
|
14
|
+
* - SASL EXTERNAL negotiation is permitted on the secure path (the
|
|
15
|
+
* mech is accepted when an mTLS provider is bound; the verify-level
|
|
16
|
+
* `904` for a missing account is owned by the EXTERNAL-verify
|
|
17
|
+
* ticket, not a secure-connection refusal);
|
|
18
|
+
* - a plain transport (secure not asserted) stays unflagged.
|
|
19
|
+
*
|
|
20
|
+
* Uses real DynamoDB Local, mirroring `nlb-stream.test.ts`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
24
|
+
import { GetCommand } from '@aws-sdk/lib-dynamodb';
|
|
25
|
+
import {
|
|
26
|
+
InMemoryMtlsIdentityProvider,
|
|
27
|
+
type ParsedServerConfig,
|
|
28
|
+
StaticMotdProvider,
|
|
29
|
+
certIdentityFromSubject,
|
|
30
|
+
} from '@serverless-ircd/irc-core';
|
|
31
|
+
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
32
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
33
|
+
import type { PostToConnection } from '../src/aws-runtime.js';
|
|
34
|
+
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
35
|
+
import { createDynamoDocumentClient } from '../src/dynamo.js';
|
|
36
|
+
import { type HandlerDeps, dispatchNlbStream } from '../src/handlers/index.js';
|
|
37
|
+
import {
|
|
38
|
+
type NlbStreamEvent,
|
|
39
|
+
deriveNlbConnectionId,
|
|
40
|
+
handleNlbStream,
|
|
41
|
+
} from '../src/handlers/nlb-stream.js';
|
|
42
|
+
import { bindMessageStore } from '../src/message-store.js';
|
|
43
|
+
import type { TablesConfig } from '../src/tables.js';
|
|
44
|
+
|
|
45
|
+
const available = process.env.DDB_AVAILABLE === '1';
|
|
46
|
+
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
47
|
+
|
|
48
|
+
const SERVER_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
49
|
+
serverName: 'irc.example.com',
|
|
50
|
+
networkName: 'ExampleNet',
|
|
51
|
+
motdLines: ['Welcome to the test IRC server.'],
|
|
52
|
+
// SASL EXTERNAL is operator opt-in; these tests exercise the enabled
|
|
53
|
+
// deployment (mTLS + TLS listener configured at the edge).
|
|
54
|
+
sasl: { externalEnabled: true },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const STS_CONFIG: ParsedServerConfig = makeTestServerConfig({
|
|
58
|
+
serverName: 'irc.example.com',
|
|
59
|
+
networkName: 'ExampleNet',
|
|
60
|
+
motdLines: ['Welcome to the test IRC server.'],
|
|
61
|
+
sts: { duration: 3600, port: 6697 },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const MOTD = new StaticMotdProvider(SERVER_CONFIG.motdLines);
|
|
65
|
+
|
|
66
|
+
interface Fixture {
|
|
67
|
+
tables: TablesConfig;
|
|
68
|
+
client: ReturnType<typeof createDynamoDocumentClient>;
|
|
69
|
+
mgmt: LocalPostToConnection;
|
|
70
|
+
deps: HandlerDeps;
|
|
71
|
+
stsDeps: HandlerDeps;
|
|
72
|
+
cleanup: () => Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let fx: Fixture | null;
|
|
76
|
+
|
|
77
|
+
function requireFx(): Fixture {
|
|
78
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
79
|
+
return fx;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function makeFixture(): Promise<Fixture> {
|
|
83
|
+
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
84
|
+
const prefix = `s${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
85
|
+
const tables = Object.fromEntries(
|
|
86
|
+
Object.keys(TABLE_DEFS).map((name) => [name, `${prefix}${name}`]),
|
|
87
|
+
) as TablesConfig;
|
|
88
|
+
const client = createDynamoDocumentClient({ endpoint });
|
|
89
|
+
for (const [logical, props] of Object.entries(TABLE_DEFS)) {
|
|
90
|
+
const pkName = props.partitionKey?.name;
|
|
91
|
+
const skName = props.sortKey?.name;
|
|
92
|
+
if (pkName === undefined) throw new Error(`table ${logical} missing partitionKey`);
|
|
93
|
+
const attributeDefinitions: Array<{ AttributeName: string; AttributeType: 'S' | 'N' }> = [
|
|
94
|
+
{ AttributeName: pkName, AttributeType: 'S' },
|
|
95
|
+
];
|
|
96
|
+
const keySchema: Array<{ AttributeName: string; KeyType: 'HASH' | 'RANGE' }> = [
|
|
97
|
+
{ AttributeName: pkName, KeyType: 'HASH' },
|
|
98
|
+
];
|
|
99
|
+
if (skName !== undefined) {
|
|
100
|
+
attributeDefinitions.push({ AttributeName: skName, AttributeType: 'S' });
|
|
101
|
+
keySchema.push({ AttributeName: skName, KeyType: 'RANGE' });
|
|
102
|
+
}
|
|
103
|
+
// Secondary indexes declared by the CDK definition (the Connections
|
|
104
|
+
// per-IP admission GSI) are created here too — the NLB new-flow rate
|
|
105
|
+
// gate Queries the index, so a fixture without it diverges from the
|
|
106
|
+
// deployed schema.
|
|
107
|
+
const globalSecondaryIndexes = (props.globalSecondaryIndexes ?? []).map((g) => {
|
|
108
|
+
const gpk = g.partitionKey;
|
|
109
|
+
if (gpk === undefined) throw new Error(`index ${g.indexName} missing partitionKey`);
|
|
110
|
+
const gsiKeySchema: Array<{ AttributeName: string; KeyType: 'HASH' | 'RANGE' }> = [
|
|
111
|
+
{ AttributeName: gpk.name, KeyType: 'HASH' },
|
|
112
|
+
];
|
|
113
|
+
attributeDefinitions.push({ AttributeName: gpk.name, AttributeType: 'S' });
|
|
114
|
+
if (g.sortKey !== undefined) {
|
|
115
|
+
gsiKeySchema.push({ AttributeName: g.sortKey.name, KeyType: 'RANGE' });
|
|
116
|
+
attributeDefinitions.push({ AttributeName: g.sortKey.name, AttributeType: 'N' });
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
IndexName: g.indexName,
|
|
120
|
+
KeySchema: gsiKeySchema,
|
|
121
|
+
Projection: { ProjectionType: 'ALL' as const },
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
await client.send(
|
|
125
|
+
new CreateTableCommand({
|
|
126
|
+
TableName: `${prefix}${logical}`,
|
|
127
|
+
AttributeDefinitions: attributeDefinitions,
|
|
128
|
+
KeySchema: keySchema,
|
|
129
|
+
...(globalSecondaryIndexes.length > 0
|
|
130
|
+
? { GlobalSecondaryIndexes: globalSecondaryIndexes }
|
|
131
|
+
: {}),
|
|
132
|
+
BillingMode: 'PAY_PER_REQUEST',
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const mgmt = new LocalPostToConnection();
|
|
137
|
+
const deps: HandlerDeps = {
|
|
138
|
+
dynamo: client,
|
|
139
|
+
tables,
|
|
140
|
+
serverConfig: SERVER_CONFIG,
|
|
141
|
+
motd: MOTD,
|
|
142
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
143
|
+
managementApi: mgmt as never,
|
|
144
|
+
};
|
|
145
|
+
const stsDeps: HandlerDeps = {
|
|
146
|
+
...deps,
|
|
147
|
+
serverConfig: STS_CONFIG,
|
|
148
|
+
messages: bindMessageStore(STS_CONFIG),
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
tables,
|
|
152
|
+
client,
|
|
153
|
+
mgmt,
|
|
154
|
+
deps,
|
|
155
|
+
stsDeps,
|
|
156
|
+
cleanup: async () => {
|
|
157
|
+
for (const logical of Object.keys(TABLE_DEFS)) {
|
|
158
|
+
try {
|
|
159
|
+
await client.send(new DeleteTableCommand({ TableName: `${prefix}${logical}` }));
|
|
160
|
+
} catch {
|
|
161
|
+
// Already gone.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Builds an NLB event from raw client bytes (base64-encodes the body). */
|
|
169
|
+
function nlbEvent(sourceIp: string, sourcePort: string, data: string): NlbStreamEvent {
|
|
170
|
+
return {
|
|
171
|
+
requestContext: { elb: { targetGroupArn: 'arn:aws:elasticloadbalancing:::targetgroup/test' } },
|
|
172
|
+
version: '2.0',
|
|
173
|
+
body: Buffer.from(data, 'utf8').toString('base64'),
|
|
174
|
+
isBase64Encoded: true,
|
|
175
|
+
headers: {
|
|
176
|
+
'x-forwarded-for': sourceIp,
|
|
177
|
+
'x-forwarded-port': sourcePort,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Decodes an NLB response body back to raw IRC bytes. */
|
|
183
|
+
function decodeBody(res: { body: string }): string {
|
|
184
|
+
return Buffer.from(res.body, 'base64').toString('utf8');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Reads a raw Connections row. */
|
|
188
|
+
async function readRow(
|
|
189
|
+
tableName: string,
|
|
190
|
+
client: Fixture['client'],
|
|
191
|
+
connId: string,
|
|
192
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
193
|
+
const row = await client.send(
|
|
194
|
+
new GetCommand({ TableName: tableName, Key: { connectionId: connId } }),
|
|
195
|
+
);
|
|
196
|
+
return row.Item;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
describe.skipIf(!available)('NLB TLS secure-flag propagation', () => {
|
|
200
|
+
beforeEach(async () => {
|
|
201
|
+
fx = await makeFixture();
|
|
202
|
+
});
|
|
203
|
+
afterEach(async () => {
|
|
204
|
+
if (fx) await fx.cleanup();
|
|
205
|
+
fx = null;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('flags the NLB+TLS connection secure: MODE read-back shows +S and the row persists secure + userModes.tls', async () => {
|
|
209
|
+
const f = requireFx();
|
|
210
|
+
const res = await dispatchNlbStream(
|
|
211
|
+
nlbEvent(
|
|
212
|
+
'10.0.0.70',
|
|
213
|
+
'50070',
|
|
214
|
+
'NICK tlsclient\r\nUSER tls 0 * :TLS User\r\nMODE tlsclient\r\n',
|
|
215
|
+
),
|
|
216
|
+
f.deps,
|
|
217
|
+
);
|
|
218
|
+
expect(res.statusCode).toBe(200);
|
|
219
|
+
const body = decodeBody(res);
|
|
220
|
+
// Read-only user mode `S` is visible in the MODE read form (221).
|
|
221
|
+
expect(body).toContain('221 tlsclient +S');
|
|
222
|
+
expect(body).not.toContain('221 tlsclient +\r\n');
|
|
223
|
+
|
|
224
|
+
// The persisted row carries the TLS fact for cross-invocation readers.
|
|
225
|
+
const row = await readRow(
|
|
226
|
+
f.tables.Connections,
|
|
227
|
+
f.client,
|
|
228
|
+
deriveNlbConnectionId('10.0.0.70', '50070'),
|
|
229
|
+
);
|
|
230
|
+
expect(row?.secure).toBe(true);
|
|
231
|
+
expect(row?.userModes).toMatchObject({ tls: true });
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('keeps the secure flag across invocations of the same flow (persist + re-stamp)', async () => {
|
|
235
|
+
const f = requireFx();
|
|
236
|
+
await dispatchNlbStream(
|
|
237
|
+
nlbEvent('10.0.0.71', '50071', 'NICK reuser\r\nUSER u 0 * :Re User\r\n'),
|
|
238
|
+
f.deps,
|
|
239
|
+
);
|
|
240
|
+
const res = await dispatchNlbStream(nlbEvent('10.0.0.71', '50071', 'MODE reuser\r\n'), f.deps);
|
|
241
|
+
expect(res.statusCode).toBe(200);
|
|
242
|
+
expect(decodeBody(res)).toContain('221 reuser +S');
|
|
243
|
+
|
|
244
|
+
const row = await readRow(
|
|
245
|
+
f.tables.Connections,
|
|
246
|
+
f.client,
|
|
247
|
+
deriveNlbConnectionId('10.0.0.71', '50071'),
|
|
248
|
+
);
|
|
249
|
+
expect(row?.secure).toBe(true);
|
|
250
|
+
expect(row?.userModes).toMatchObject({ tls: true });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('does not flag a plain transport when secure is not asserted (contrast)', async () => {
|
|
254
|
+
const f = requireFx();
|
|
255
|
+
// handleNlbStream called WITHOUT `secure` models a plain-TCP listener
|
|
256
|
+
// target: no S mode, and the row records the plain transport.
|
|
257
|
+
const res = await handleNlbStream(
|
|
258
|
+
nlbEvent(
|
|
259
|
+
'10.0.0.72',
|
|
260
|
+
'50072',
|
|
261
|
+
'NICK plainclient\r\nUSER p 0 * :Plain User\r\nMODE plainclient\r\n',
|
|
262
|
+
),
|
|
263
|
+
{
|
|
264
|
+
dynamo: f.client,
|
|
265
|
+
tables: f.tables,
|
|
266
|
+
serverConfig: SERVER_CONFIG,
|
|
267
|
+
motd: MOTD,
|
|
268
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
269
|
+
managementApi: f.mgmt,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
expect(res.statusCode).toBe(200);
|
|
273
|
+
const body = decodeBody(res);
|
|
274
|
+
expect(body).toContain('221 plainclient +\r\n');
|
|
275
|
+
expect(body).not.toContain('+S');
|
|
276
|
+
|
|
277
|
+
const row = await readRow(
|
|
278
|
+
f.tables.Connections,
|
|
279
|
+
f.client,
|
|
280
|
+
deriveNlbConnectionId('10.0.0.72', '50072'),
|
|
281
|
+
);
|
|
282
|
+
expect(row?.secure).toBe(false);
|
|
283
|
+
expect(row?.userModes).toMatchObject({ tls: false });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('advertises the TLS form of sts on the secure NLB path (duration only, no port)', async () => {
|
|
287
|
+
const f = requireFx();
|
|
288
|
+
const res = await dispatchNlbStream(
|
|
289
|
+
nlbEvent('10.0.0.73', '50073', 'CAP LS 302\r\n'),
|
|
290
|
+
f.stsDeps,
|
|
291
|
+
);
|
|
292
|
+
expect(res.statusCode).toBe(200);
|
|
293
|
+
const body = decodeBody(res);
|
|
294
|
+
// Already on the secure listener: duration-only advertisement.
|
|
295
|
+
expect(body).toContain('sts=duration=3600');
|
|
296
|
+
expect(body).not.toContain('port=');
|
|
297
|
+
|
|
298
|
+
// Contrast — the same config over a transport that did not assert
|
|
299
|
+
// secure advertises the plaintext upgrade form with the port.
|
|
300
|
+
const plain = await handleNlbStream(nlbEvent('10.0.0.74', '50074', 'CAP LS 302\r\n'), {
|
|
301
|
+
dynamo: f.client,
|
|
302
|
+
tables: f.tables,
|
|
303
|
+
serverConfig: STS_CONFIG,
|
|
304
|
+
motd: MOTD,
|
|
305
|
+
messages: bindMessageStore(STS_CONFIG),
|
|
306
|
+
managementApi: f.mgmt,
|
|
307
|
+
});
|
|
308
|
+
expect(decodeBody(plain)).toContain('sts=duration=3600,port=6697');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('permits SASL EXTERNAL negotiation on the secure NLB path', async () => {
|
|
312
|
+
const f = requireFx();
|
|
313
|
+
const connId = deriveNlbConnectionId('10.0.0.75', '50075');
|
|
314
|
+
const opts = {
|
|
315
|
+
dynamo: f.client,
|
|
316
|
+
tables: f.tables,
|
|
317
|
+
serverConfig: SERVER_CONFIG,
|
|
318
|
+
motd: MOTD,
|
|
319
|
+
messages: bindMessageStore(SERVER_CONFIG),
|
|
320
|
+
managementApi: f.mgmt,
|
|
321
|
+
secure: true,
|
|
322
|
+
mtlsIdentity: new InMemoryMtlsIdentityProvider([
|
|
323
|
+
{ connId, identity: certIdentityFromSubject('CN=extuser') },
|
|
324
|
+
]),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Mechanism selection: EXTERNAL is advertised and accepted (the
|
|
328
|
+
// server asks for the empty payload with `AUTHENTICATE +`).
|
|
329
|
+
const ls = await handleNlbStream(
|
|
330
|
+
nlbEvent('10.0.0.75', '50075', 'CAP LS 302\r\nAUTHENTICATE EXTERNAL\r\n'),
|
|
331
|
+
opts,
|
|
332
|
+
);
|
|
333
|
+
const lsBody = decodeBody(ls);
|
|
334
|
+
expect(lsBody).toContain('sasl=PLAIN,EXTERNAL');
|
|
335
|
+
expect(lsBody).toContain('AUTHENTICATE +');
|
|
336
|
+
|
|
337
|
+
// Payload delivery: the exchange proceeds to the verify step. With no
|
|
338
|
+
// services store bound the verify fails with 904 (account resolution
|
|
339
|
+
// is the EXTERNAL-verify ticket's concern) — never a 908 mech-level
|
|
340
|
+
// refusal on secure-connection grounds.
|
|
341
|
+
const fin = await handleNlbStream(nlbEvent('10.0.0.75', '50075', 'AUTHENTICATE +\r\n'), opts);
|
|
342
|
+
const finBody = decodeBody(fin);
|
|
343
|
+
expect(finBody).toContain('904');
|
|
344
|
+
expect(finBody).not.toContain('908');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Test double for `ApiGatewayManagementApi`. Routes `postToConnection`
|
|
350
|
+
* calls to a registered sink for `ConnectionId`.
|
|
351
|
+
*/
|
|
352
|
+
class LocalPostToConnection implements PostToConnection {
|
|
353
|
+
private readonly sinks = new Map<string, (data: string) => void>();
|
|
354
|
+
register(connId: string, sink: (data: string) => void): void {
|
|
355
|
+
this.sinks.set(connId, sink);
|
|
356
|
+
}
|
|
357
|
+
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<unknown> {
|
|
358
|
+
const sink = this.sinks.get(input.ConnectionId);
|
|
359
|
+
if (sink !== undefined) sink(input.Data);
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
}
|