serverless-ircd 0.2.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 +258 -18
- package/README.md +72 -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/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- 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 +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- 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 +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- 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 +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- 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/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +40 -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 +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- 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 +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- 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/case-fold.ts +64 -0
- 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 +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- 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/tagmsg.ts +205 -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 +84 -3
- 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 +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- 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/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- 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 +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -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 +182 -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 +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
- 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 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
10
10
|
import {
|
|
11
11
|
DEFAULT_SERVER_CONFIG,
|
|
12
|
+
InMemoryAccountStore,
|
|
12
13
|
type ParsedServerConfig,
|
|
13
14
|
StaticMotdProvider,
|
|
14
15
|
} from '@serverless-ircd/irc-core';
|
|
@@ -48,6 +49,11 @@ interface Fixture {
|
|
|
48
49
|
|
|
49
50
|
let fx: Fixture | null;
|
|
50
51
|
|
|
52
|
+
function requireFx(): Fixture {
|
|
53
|
+
if (!fx) throw new Error('test fixture not initialized');
|
|
54
|
+
return fx;
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
async function makeFixture(): Promise<Fixture> {
|
|
52
58
|
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
53
59
|
const prefix = `h${Date.now()}-${Math.floor(Math.random() * 1e6)}-`;
|
|
@@ -121,12 +127,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
121
127
|
it('returns 200 and persists a Connections row', async () => {
|
|
122
128
|
const res = await dispatch(
|
|
123
129
|
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
124
|
-
|
|
130
|
+
requireFx().deps,
|
|
125
131
|
);
|
|
126
132
|
expect(res.statusCode).toBe(200);
|
|
127
133
|
const probe = new AwsRuntime({
|
|
128
|
-
dynamo:
|
|
129
|
-
tables:
|
|
134
|
+
dynamo: requireFx().client,
|
|
135
|
+
tables: requireFx().tables,
|
|
130
136
|
connId: '__probe__',
|
|
131
137
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
132
138
|
managementApi: null,
|
|
@@ -138,11 +144,14 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
138
144
|
|
|
139
145
|
it('is idempotent on APIGW retry (same connectionId overwrites)', async () => {
|
|
140
146
|
for (let i = 0; i < 2; i++) {
|
|
141
|
-
await dispatch(
|
|
147
|
+
await dispatch(
|
|
148
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
149
|
+
requireFx().deps,
|
|
150
|
+
);
|
|
142
151
|
}
|
|
143
152
|
const probe = new AwsRuntime({
|
|
144
|
-
dynamo:
|
|
145
|
-
tables:
|
|
153
|
+
dynamo: requireFx().client,
|
|
154
|
+
tables: requireFx().tables,
|
|
146
155
|
connId: '__probe__',
|
|
147
156
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
148
157
|
managementApi: null,
|
|
@@ -152,21 +161,84 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
152
161
|
});
|
|
153
162
|
});
|
|
154
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
|
+
|
|
155
224
|
// -------------------------------------------------------------------------
|
|
156
225
|
// $disconnect
|
|
157
226
|
// -------------------------------------------------------------------------
|
|
158
227
|
|
|
159
228
|
describe('$disconnect', () => {
|
|
160
229
|
it('returns 200 and deletes the Connections row', async () => {
|
|
161
|
-
await dispatch(
|
|
230
|
+
await dispatch(
|
|
231
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
232
|
+
requireFx().deps,
|
|
233
|
+
);
|
|
162
234
|
const res = await dispatch(
|
|
163
235
|
{ requestContext: { routeKey: '$disconnect', connectionId: 'c1' } },
|
|
164
|
-
|
|
236
|
+
requireFx().deps,
|
|
165
237
|
);
|
|
166
238
|
expect(res.statusCode).toBe(200);
|
|
167
239
|
const probe = new AwsRuntime({
|
|
168
|
-
dynamo:
|
|
169
|
-
tables:
|
|
240
|
+
dynamo: requireFx().client,
|
|
241
|
+
tables: requireFx().tables,
|
|
170
242
|
connId: '__probe__',
|
|
171
243
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
172
244
|
managementApi: null,
|
|
@@ -177,33 +249,39 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
177
249
|
it('returns 200 even when the connection was never seen', async () => {
|
|
178
250
|
const res = await dispatch(
|
|
179
251
|
{ requestContext: { routeKey: '$disconnect', connectionId: 'never' } },
|
|
180
|
-
|
|
252
|
+
requireFx().deps,
|
|
181
253
|
);
|
|
182
254
|
expect(res.statusCode).toBe(200);
|
|
183
255
|
});
|
|
184
256
|
|
|
185
257
|
it('releases the nick on disconnect', async () => {
|
|
186
258
|
// Connect + register alice.
|
|
187
|
-
await dispatch(
|
|
259
|
+
await dispatch(
|
|
260
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
261
|
+
requireFx().deps,
|
|
262
|
+
);
|
|
188
263
|
await dispatch(
|
|
189
264
|
{
|
|
190
265
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
191
266
|
body: 'NICK alice\r\n',
|
|
192
267
|
},
|
|
193
|
-
|
|
268
|
+
requireFx().deps,
|
|
194
269
|
);
|
|
195
270
|
await dispatch(
|
|
196
271
|
{
|
|
197
272
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
198
273
|
body: 'USER alice 0 * :Alice\r\n',
|
|
199
274
|
},
|
|
200
|
-
|
|
275
|
+
requireFx().deps,
|
|
201
276
|
);
|
|
202
277
|
// Disconnect.
|
|
203
|
-
await dispatch(
|
|
278
|
+
await dispatch(
|
|
279
|
+
{ requestContext: { routeKey: '$disconnect', connectionId: 'c1' } },
|
|
280
|
+
requireFx().deps,
|
|
281
|
+
);
|
|
204
282
|
const probe = new AwsRuntime({
|
|
205
|
-
dynamo:
|
|
206
|
-
tables:
|
|
283
|
+
dynamo: requireFx().client,
|
|
284
|
+
tables: requireFx().tables,
|
|
207
285
|
connId: '__probe__',
|
|
208
286
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
209
287
|
managementApi: null,
|
|
@@ -223,7 +301,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
223
301
|
requestContext: { routeKey: '$default', connectionId: 'ghost' },
|
|
224
302
|
body: 'PING :tok\r\n',
|
|
225
303
|
},
|
|
226
|
-
|
|
304
|
+
requireFx().deps,
|
|
227
305
|
);
|
|
228
306
|
expect(res.statusCode).toBe(410);
|
|
229
307
|
});
|
|
@@ -233,23 +311,26 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
233
311
|
const event = {
|
|
234
312
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
235
313
|
} as never;
|
|
236
|
-
const res = await dispatch(event,
|
|
314
|
+
const res = await dispatch(event, requireFx().deps);
|
|
237
315
|
expect(res.statusCode).toBe(400);
|
|
238
316
|
});
|
|
239
317
|
|
|
240
318
|
it('runs a NICK + USER frame and emits the welcome block', async () => {
|
|
241
|
-
await dispatch(
|
|
242
|
-
|
|
319
|
+
await dispatch(
|
|
320
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
321
|
+
requireFx().deps,
|
|
322
|
+
);
|
|
323
|
+
requireFx().mgmt.register('c1', () => {});
|
|
243
324
|
await dispatch(
|
|
244
325
|
{
|
|
245
326
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
246
327
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
247
328
|
},
|
|
248
|
-
|
|
329
|
+
requireFx().deps,
|
|
249
330
|
);
|
|
250
331
|
const probe = new AwsRuntime({
|
|
251
|
-
dynamo:
|
|
252
|
-
tables:
|
|
332
|
+
dynamo: requireFx().client,
|
|
333
|
+
tables: requireFx().tables,
|
|
253
334
|
connId: '__probe__',
|
|
254
335
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
255
336
|
managementApi: null,
|
|
@@ -260,9 +341,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
260
341
|
});
|
|
261
342
|
|
|
262
343
|
it('echoes outbound lines back to the caller via the management API', async () => {
|
|
263
|
-
await dispatch(
|
|
344
|
+
await dispatch(
|
|
345
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
346
|
+
requireFx().deps,
|
|
347
|
+
);
|
|
264
348
|
const sink: string[] = [];
|
|
265
|
-
|
|
349
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
266
350
|
for (const line of data.split('\r\n')) {
|
|
267
351
|
if (line.length > 0) sink.push(line);
|
|
268
352
|
}
|
|
@@ -272,31 +356,34 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
272
356
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
273
357
|
body: 'PING :tok\r\n',
|
|
274
358
|
},
|
|
275
|
-
|
|
359
|
+
requireFx().deps,
|
|
276
360
|
);
|
|
277
361
|
expect(sink).toContain('PONG :tok');
|
|
278
362
|
});
|
|
279
363
|
|
|
280
364
|
it('persists joined channels across $default invocations', async () => {
|
|
281
|
-
await dispatch(
|
|
282
|
-
|
|
365
|
+
await dispatch(
|
|
366
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
367
|
+
requireFx().deps,
|
|
368
|
+
);
|
|
369
|
+
requireFx().mgmt.register('c1', () => {});
|
|
283
370
|
await dispatch(
|
|
284
371
|
{
|
|
285
372
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
286
373
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
287
374
|
},
|
|
288
|
-
|
|
375
|
+
requireFx().deps,
|
|
289
376
|
);
|
|
290
377
|
await dispatch(
|
|
291
378
|
{
|
|
292
379
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
293
380
|
body: 'JOIN #persist\r\n',
|
|
294
381
|
},
|
|
295
|
-
|
|
382
|
+
requireFx().deps,
|
|
296
383
|
);
|
|
297
384
|
const probe = new AwsRuntime({
|
|
298
|
-
dynamo:
|
|
299
|
-
tables:
|
|
385
|
+
dynamo: requireFx().client,
|
|
386
|
+
tables: requireFx().tables,
|
|
300
387
|
connId: '__probe__',
|
|
301
388
|
handlers: { send: () => {}, disconnect: () => {}, snapshot: () => undefined },
|
|
302
389
|
managementApi: null,
|
|
@@ -307,26 +394,32 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
307
394
|
|
|
308
395
|
it('rebuilds an existing channel roster via refreshChannel when a second client JOINs', async () => {
|
|
309
396
|
// First connection registers + JOINs.
|
|
310
|
-
await dispatch(
|
|
311
|
-
|
|
397
|
+
await dispatch(
|
|
398
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
399
|
+
requireFx().deps,
|
|
400
|
+
);
|
|
401
|
+
requireFx().mgmt.register('c1', () => {});
|
|
312
402
|
await dispatch(
|
|
313
403
|
{
|
|
314
404
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
315
405
|
body: 'NICK alice\r\nUSER alice 0 * :Alice\r\n',
|
|
316
406
|
},
|
|
317
|
-
|
|
407
|
+
requireFx().deps,
|
|
318
408
|
);
|
|
319
409
|
await dispatch(
|
|
320
410
|
{
|
|
321
411
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
322
412
|
body: 'JOIN #room\r\n',
|
|
323
413
|
},
|
|
324
|
-
|
|
414
|
+
requireFx().deps,
|
|
325
415
|
);
|
|
326
416
|
// Second connection registers + JOINs the SAME channel.
|
|
327
|
-
await dispatch(
|
|
417
|
+
await dispatch(
|
|
418
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c2' } },
|
|
419
|
+
requireFx().deps,
|
|
420
|
+
);
|
|
328
421
|
const c2sink: string[] = [];
|
|
329
|
-
|
|
422
|
+
requireFx().mgmt.register('c2', (data) => {
|
|
330
423
|
for (const line of data.split('\r\n')) {
|
|
331
424
|
if (line.length > 0) c2sink.push(line);
|
|
332
425
|
}
|
|
@@ -336,14 +429,14 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
336
429
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
337
430
|
body: 'NICK bob\r\nUSER bob 0 * :Bob\r\n',
|
|
338
431
|
},
|
|
339
|
-
|
|
432
|
+
requireFx().deps,
|
|
340
433
|
);
|
|
341
434
|
await dispatch(
|
|
342
435
|
{
|
|
343
436
|
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
344
437
|
body: 'JOIN #room\r\n',
|
|
345
438
|
},
|
|
346
|
-
|
|
439
|
+
requireFx().deps,
|
|
347
440
|
);
|
|
348
441
|
// The 353 RPL_NAMREPLY c2 receives must list BOTH alice and bob —
|
|
349
442
|
// proving that the actor's `refreshChannel` populated the cache
|
|
@@ -355,9 +448,12 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
355
448
|
});
|
|
356
449
|
|
|
357
450
|
it('records a PRIVMSG and replays it via CHATHISTORY LATEST in a BATCH', async () => {
|
|
358
|
-
await dispatch(
|
|
451
|
+
await dispatch(
|
|
452
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
453
|
+
requireFx().deps,
|
|
454
|
+
);
|
|
359
455
|
const sink: string[] = [];
|
|
360
|
-
|
|
456
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
361
457
|
for (const line of data.split('\r\n')) {
|
|
362
458
|
if (line.length > 0) sink.push(line);
|
|
363
459
|
}
|
|
@@ -367,7 +463,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
367
463
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
368
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',
|
|
369
465
|
},
|
|
370
|
-
|
|
466
|
+
requireFx().deps,
|
|
371
467
|
);
|
|
372
468
|
sink.length = 0;
|
|
373
469
|
await dispatch(
|
|
@@ -375,7 +471,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
375
471
|
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
376
472
|
body: 'PRIVMSG #aws-chathist :hello from aws\r\nCHATHISTORY LATEST #aws-chathist * 10\r\n',
|
|
377
473
|
},
|
|
378
|
-
|
|
474
|
+
requireFx().deps,
|
|
379
475
|
);
|
|
380
476
|
const batchOpen = sink.find((l) => /^BATCH \+\S+ chathistory #aws-chathist/.test(l));
|
|
381
477
|
expect(batchOpen).toBeDefined();
|
|
@@ -383,6 +479,57 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
383
479
|
expect(replay).toBeDefined();
|
|
384
480
|
});
|
|
385
481
|
|
|
482
|
+
it('completes SASL PLAIN with 900/903 when an AccountStore is bound', async () => {
|
|
483
|
+
await dispatch(
|
|
484
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c1' } },
|
|
485
|
+
requireFx().deps,
|
|
486
|
+
);
|
|
487
|
+
const sink: string[] = [];
|
|
488
|
+
requireFx().mgmt.register('c1', (data) => {
|
|
489
|
+
for (const line of data.split('\r\n')) {
|
|
490
|
+
if (line.length > 0) sink.push(line);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
// Swap in a deps with a seeded AccountStore for this dispatch.
|
|
494
|
+
const depsWithAccounts: HandlerDeps = {
|
|
495
|
+
...requireFx().deps,
|
|
496
|
+
accounts: new InMemoryAccountStore([{ username: 'aws-sasl', password: 's3cret' }]),
|
|
497
|
+
};
|
|
498
|
+
const payload = Buffer.from('\0aws-sasl\0s3cret', 'utf8').toString('base64');
|
|
499
|
+
await dispatch(
|
|
500
|
+
{
|
|
501
|
+
requestContext: { routeKey: '$default', connectionId: 'c1' },
|
|
502
|
+
body: `CAP REQ :sasl\r\nAUTHENTICATE PLAIN\r\nAUTHENTICATE ${payload}\r\n`,
|
|
503
|
+
},
|
|
504
|
+
depsWithAccounts,
|
|
505
|
+
);
|
|
506
|
+
expect(sink.some((l) => l.includes(' 900 '))).toBe(true);
|
|
507
|
+
expect(sink.some((l) => l.includes(' 903 '))).toBe(true);
|
|
508
|
+
expect(sink.some((l) => l.includes(' 904 '))).toBe(false);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('emits 904 when no AccountStore is bound (regression guard)', async () => {
|
|
512
|
+
await dispatch(
|
|
513
|
+
{ requestContext: { routeKey: '$connect', connectionId: 'c2' } },
|
|
514
|
+
requireFx().deps,
|
|
515
|
+
);
|
|
516
|
+
const sink: string[] = [];
|
|
517
|
+
requireFx().mgmt.register('c2', (data) => {
|
|
518
|
+
for (const line of data.split('\r\n')) {
|
|
519
|
+
if (line.length > 0) sink.push(line);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
const payload = Buffer.from('\0aws-sasl\0s3cret', 'utf8').toString('base64');
|
|
523
|
+
await dispatch(
|
|
524
|
+
{
|
|
525
|
+
requestContext: { routeKey: '$default', connectionId: 'c2' },
|
|
526
|
+
body: `CAP REQ :sasl\r\nAUTHENTICATE PLAIN\r\nAUTHENTICATE ${payload}\r\n`,
|
|
527
|
+
},
|
|
528
|
+
requireFx().deps,
|
|
529
|
+
);
|
|
530
|
+
expect(sink.some((l) => l.includes(' 904 '))).toBe(true);
|
|
531
|
+
});
|
|
532
|
+
|
|
386
533
|
it('returns 500 when unmarshalConnection throws on a corrupted row (caught by Lambda)', async () => {
|
|
387
534
|
// Skip: the catch in handleDefault wraps `actor.receiveTextFrame`,
|
|
388
535
|
// not `unmarshalConnection`. A corrupted-row test would exercise
|
|
@@ -400,7 +547,7 @@ describe.skipIf(!available)('handlers', () => {
|
|
|
400
547
|
it('returns 200 (APIGW only forwards the three known routes)', async () => {
|
|
401
548
|
const res = await dispatch(
|
|
402
549
|
{ requestContext: { routeKey: '$custom', connectionId: 'c1' } },
|
|
403
|
-
|
|
550
|
+
requireFx().deps,
|
|
404
551
|
);
|
|
405
552
|
expect(res.statusCode).toBe(200);
|
|
406
553
|
});
|
|
@@ -416,7 +563,7 @@ class LocalPostToConnection implements PostToConnection {
|
|
|
416
563
|
register(connId: string, sink: (data: string) => void): void {
|
|
417
564
|
this.sinks.set(connId, sink);
|
|
418
565
|
}
|
|
419
|
-
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<
|
|
566
|
+
async postToConnection(input: { ConnectionId: string; Data: string }): Promise<unknown> {
|
|
420
567
|
const sink = this.sinks.get(input.ConnectionId);
|
|
421
568
|
if (sink !== undefined) sink(input.Data);
|
|
422
569
|
return {};
|