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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CF adapter — per-connection inbound frame-rate window at the adapter
|
|
3
|
+
* boundary (`ConnectionDO.webSocketMessage`).
|
|
4
|
+
*
|
|
5
|
+
* The gate fires BEFORE the actor / storage write: a connection that
|
|
6
|
+
* delivers more than `adapter.maxFramesPerWindow` frames inside the
|
|
7
|
+
* `adapter.frameWindowSeconds` sliding window is closed with RFC 6455
|
|
8
|
+
* close code 1008 (Policy Violation) after receiving the canonical
|
|
9
|
+
* `ERROR :Closing link (flooding)` line.
|
|
10
|
+
*
|
|
11
|
+
* The default window (50 frames / 5 s) exercises the under/at/over
|
|
12
|
+
* thresholds; the sliding-window decay uses the `__setFrameWindowForTest`
|
|
13
|
+
* seam to install a tight window so the test does not sleep for real
|
|
14
|
+
* seconds.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { env, runInDurableObject } from 'cloudflare:test';
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
|
|
20
|
+
declare global {
|
|
21
|
+
namespace Cloudflare {
|
|
22
|
+
interface Env {
|
|
23
|
+
CONNECTION_DO: DurableObjectNamespace;
|
|
24
|
+
REGISTRY_DO: DurableObjectNamespace;
|
|
25
|
+
CHANNEL_DO: DurableObjectNamespace;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Poll interval (ms) for the waitFor* helpers. */
|
|
31
|
+
const POLL_MS = 10;
|
|
32
|
+
/** Default timeout (ms) for the waitFor* helpers. */
|
|
33
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
34
|
+
|
|
35
|
+
class FrameWsClient {
|
|
36
|
+
readonly lines: string[] = [];
|
|
37
|
+
readonly ws: WebSocket;
|
|
38
|
+
closedCode: number | undefined;
|
|
39
|
+
|
|
40
|
+
constructor(ws: WebSocket) {
|
|
41
|
+
this.ws = ws;
|
|
42
|
+
ws.addEventListener('message', (ev: MessageEvent) => {
|
|
43
|
+
const data = typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data);
|
|
44
|
+
for (const line of data.split('\r\n')) {
|
|
45
|
+
if (line.length > 0) this.lines.push(line);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
ws.addEventListener('close', (ev: CloseEvent) => {
|
|
49
|
+
this.closedCode = ev.code;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
send(line: string): void {
|
|
54
|
+
this.ws.send(`${line}\r\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
countPongs(): number {
|
|
58
|
+
return this.lines.filter((l) => l.startsWith('PONG')).length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async waitForPongs(n: number, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<void> {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
if (this.countPongs() >= n) return;
|
|
65
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`waitForPongs(${n}) timed out; lines: ${JSON.stringify(this.lines)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async waitForClose(timeoutMs = DEFAULT_TIMEOUT_MS): Promise<number> {
|
|
71
|
+
const deadline = Date.now() + timeoutMs;
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
if (this.closedCode !== undefined) return this.closedCode;
|
|
74
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
75
|
+
}
|
|
76
|
+
throw new Error(`waitForClose timed out; lines: ${JSON.stringify(this.lines)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function openConnection(
|
|
81
|
+
connId: string,
|
|
82
|
+
): Promise<{ client: FrameWsClient; stub: DurableObjectStub }> {
|
|
83
|
+
const stub = env.CONNECTION_DO.get(env.CONNECTION_DO.idFromName(connId));
|
|
84
|
+
const response = await stub.fetch('https://do/upgrade', { headers: { Upgrade: 'websocket' } });
|
|
85
|
+
const ws = response.webSocket;
|
|
86
|
+
if (ws === undefined || ws === null) {
|
|
87
|
+
throw new Error('ConnectionDO.fetch did not return a WebSocket');
|
|
88
|
+
}
|
|
89
|
+
ws.accept();
|
|
90
|
+
const client = await new Promise<FrameWsClient>((resolve) => {
|
|
91
|
+
setTimeout(() => resolve(new FrameWsClient(ws as WebSocket)), 0);
|
|
92
|
+
});
|
|
93
|
+
return { client, stub };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** RPC shape of the `ConnectionDO` test seam used for the decay test. */
|
|
97
|
+
interface FrameWindowSeam {
|
|
98
|
+
__setFrameWindowForTest(cfg: { maxFrames: number; windowMs: number } | undefined): void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('ConnectionDO — inbound frame-rate window (default 50 / 5 s)', () => {
|
|
102
|
+
it('admits frames while the window is under the ceiling and at the ceiling (no close)', async () => {
|
|
103
|
+
const { client } = await openConnection('conn-frame-under');
|
|
104
|
+
// 50 frames = exactly the default ceiling; every one is admitted and
|
|
105
|
+
// answered (each PING is one WebSocket frame through the actor).
|
|
106
|
+
for (let i = 1; i <= 50; i++) {
|
|
107
|
+
client.send(`PING :u${i}`);
|
|
108
|
+
}
|
|
109
|
+
await client.waitForPongs(50);
|
|
110
|
+
// Grace window: a close would have landed by now if the 50th frame
|
|
111
|
+
// had tripped the gate.
|
|
112
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
113
|
+
expect(client.closedCode).toBeUndefined();
|
|
114
|
+
client.ws.close();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('closes with RFC 6455 code 1008 after ERROR :Closing link (flooding) on frame 51', async () => {
|
|
118
|
+
const { client } = await openConnection('conn-frame-over');
|
|
119
|
+
for (let i = 1; i <= 50; i++) {
|
|
120
|
+
client.send(`PING :o${i}`);
|
|
121
|
+
}
|
|
122
|
+
await client.waitForPongs(50);
|
|
123
|
+
|
|
124
|
+
// Frame 51 exceeds the window: no actor run (no PONG), the ERROR
|
|
125
|
+
// line is delivered, then the socket is closed with 1008.
|
|
126
|
+
client.send('PING :o51');
|
|
127
|
+
const code = await client.waitForClose();
|
|
128
|
+
expect(code).toBe(1008);
|
|
129
|
+
expect(client.lines).toContain('ERROR :Closing link (flooding)');
|
|
130
|
+
expect(client.lines.some((l) => l.startsWith('PONG :o51'))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('admits a fresh burst after the window decays (sliding window)', async () => {
|
|
134
|
+
const { client, stub } = await openConnection('conn-frame-decay');
|
|
135
|
+
// Tight window (5 frames / 400 ms) via the test seam so the decay
|
|
136
|
+
// wait is fast instead of a real 5-second sleep.
|
|
137
|
+
await runInDurableObject(stub, (instance: unknown) => {
|
|
138
|
+
(instance as FrameWindowSeam).__setFrameWindowForTest({
|
|
139
|
+
maxFrames: 5,
|
|
140
|
+
windowMs: 400,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
for (let i = 1; i <= 5; i++) {
|
|
145
|
+
client.send(`PING :d1-${i}`);
|
|
146
|
+
}
|
|
147
|
+
await client.waitForPongs(5);
|
|
148
|
+
|
|
149
|
+
// Wait past the window: the first burst decays out entirely.
|
|
150
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
151
|
+
for (let i = 1; i <= 5; i++) {
|
|
152
|
+
client.send(`PING :d2-${i}`);
|
|
153
|
+
}
|
|
154
|
+
await client.waitForPongs(10);
|
|
155
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
156
|
+
expect(client.closedCode).toBeUndefined();
|
|
157
|
+
expect(client.countPongs()).toBe(10);
|
|
158
|
+
client.ws.close();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('still floods out mid-burst once the tight window is full (seam sanity)', async () => {
|
|
162
|
+
const { client, stub } = await openConnection('conn-frame-tight');
|
|
163
|
+
await runInDurableObject(stub, (instance: unknown) => {
|
|
164
|
+
(instance as FrameWindowSeam).__setFrameWindowForTest({
|
|
165
|
+
maxFrames: 2,
|
|
166
|
+
windowMs: 60_000,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
client.send('PING :t1');
|
|
170
|
+
client.send('PING :t2');
|
|
171
|
+
client.send('PING :t3');
|
|
172
|
+
const code = await client.waitForClose();
|
|
173
|
+
expect(code).toBe(1008);
|
|
174
|
+
expect(client.lines).toContain('ERROR :Closing link (flooding)');
|
|
175
|
+
expect(client.countPongs()).toBe(2);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -6,60 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, expect, it } from 'vitest';
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
expect(parseSaslAccountsEnv('')).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('parses a single well-formed entry', () => {
|
|
21
|
-
expect(parseSaslAccountsEnv('alice:secret')).toEqual([
|
|
22
|
-
{ username: 'alice', password: 'secret' },
|
|
23
|
-
]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('parses multiple newline-delimited entries', () => {
|
|
27
|
-
expect(parseSaslAccountsEnv('alice:secret\nbob:pw')).toEqual([
|
|
28
|
-
{ username: 'alice', password: 'secret' },
|
|
29
|
-
{ username: 'bob', password: 'pw' },
|
|
30
|
-
]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('skips blank lines', () => {
|
|
34
|
-
expect(parseSaslAccountsEnv('alice:secret\n\nbob:pw\n \n')).toEqual([
|
|
35
|
-
{ username: 'alice', password: 'secret' },
|
|
36
|
-
{ username: 'bob', password: 'pw' },
|
|
37
|
-
]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('skips lines with no colon separator', () => {
|
|
41
|
-
expect(parseSaslAccountsEnv('alice:secret\nnocolonhere\nbob:pw')).toEqual([
|
|
42
|
-
{ username: 'alice', password: 'secret' },
|
|
43
|
-
{ username: 'bob', password: 'pw' },
|
|
44
|
-
]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('skips lines where the colon is at position 0 (empty username)', () => {
|
|
48
|
-
expect(parseSaslAccountsEnv(':secret\nalice:valid')).toEqual([
|
|
49
|
-
{ username: 'alice', password: 'valid' },
|
|
50
|
-
]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('skips lines with an empty password after the colon', () => {
|
|
54
|
-
expect(parseSaslAccountsEnv('alice:\nbob:valid')).toEqual([
|
|
55
|
-
{ username: 'bob', password: 'valid' },
|
|
56
|
-
]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('returns an empty array when every line is malformed', () => {
|
|
60
|
-
expect(parseSaslAccountsEnv('\n \n:no-user\nuser:')).toEqual([]);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
9
|
+
import {
|
|
10
|
+
NO_OP_DISCONNECT,
|
|
11
|
+
NO_OP_SEND,
|
|
12
|
+
buildOptionalActorFields,
|
|
13
|
+
chooseMotdProvider,
|
|
14
|
+
extractMtlsSubject,
|
|
15
|
+
wsFrameModeFromTags,
|
|
16
|
+
} from '../src/connection-do';
|
|
63
17
|
|
|
64
18
|
describe('wsFrameModeFromTags', () => {
|
|
65
19
|
it("returns 'legacy' when no tags are present", () => {
|
|
@@ -219,3 +173,159 @@ describe('PassthroughChannelAccess.refreshChannel', () => {
|
|
|
219
173
|
expect(chan.topic?.text).toBe('keep');
|
|
220
174
|
});
|
|
221
175
|
});
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// chooseMotdProvider — default vs. populated MOTD_LINES branch
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
describe('chooseMotdProvider', () => {
|
|
182
|
+
it('returns the default MOTD when the env value is undefined', () => {
|
|
183
|
+
const provider = chooseMotdProvider(undefined);
|
|
184
|
+
expect(provider.lines()).toEqual(['Welcome to the ServerlessIRCd Cloudflare adapter.']);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns the default MOTD when the env value is an empty string', () => {
|
|
188
|
+
const provider = chooseMotdProvider('');
|
|
189
|
+
expect(provider.lines()).toEqual(['Welcome to the ServerlessIRCd Cloudflare adapter.']);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('splits a populated MOTD_LINES on newlines', () => {
|
|
193
|
+
const provider = chooseMotdProvider('Line one\nLine two\nLine three');
|
|
194
|
+
expect(provider.lines()).toEqual(['Line one', 'Line two', 'Line three']);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// buildOptionalActorFields — conditional spread branches
|
|
200
|
+
// ============================================================================
|
|
201
|
+
|
|
202
|
+
describe('buildOptionalActorFields', () => {
|
|
203
|
+
it('returns an empty object when every optional is undefined', () => {
|
|
204
|
+
expect(buildOptionalActorFields(undefined, undefined)).toEqual({});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('includes `services` when defined', () => {
|
|
208
|
+
const fake = {} as unknown as Parameters<typeof buildOptionalActorFields>[0];
|
|
209
|
+
expect(buildOptionalActorFields(fake, undefined).services).toBe(fake);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('includes `mtlsIdentity` when defined', () => {
|
|
213
|
+
const fake = {} as unknown as Parameters<typeof buildOptionalActorFields>[1];
|
|
214
|
+
expect(buildOptionalActorFields(undefined, fake).mtlsIdentity).toBe(fake);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// NO_OP_SEND / NO_OP_DISCONNECT — invoked once to cover the function bodies
|
|
220
|
+
// (the no-socket teardown path constructs these handlers but the actor never
|
|
221
|
+
// invokes send/disconnect during QUIT processing — there is no peer to
|
|
222
|
+
// deliver to and the socket is already gone).
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
describe('NO_OP_SEND / NO_OP_DISCONNECT', () => {
|
|
226
|
+
it('NO_OP_SEND runs without throwing and returns undefined', () => {
|
|
227
|
+
expect(NO_OP_SEND([{ text: 'ignored' }])).toBeUndefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('NO_OP_DISCONNECT runs without throwing and returns undefined', () => {
|
|
231
|
+
expect(NO_OP_DISCONNECT('any reason')).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// extractMtlsSubject — request.cf mTLS identity capture
|
|
237
|
+
// (cf.tlsClientAuthCertSubject + cf.tlsClientAuth.certFingerprintSHA256)
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
describe('extractMtlsSubject', () => {
|
|
241
|
+
it('returns the subject identity when cf.tlsClientAuthCertSubject is a string', () => {
|
|
242
|
+
// The CF edge injects request.cf on every proxied request. We can't
|
|
243
|
+
// mutate Request.cf in workerd tests (it's read-only), so the
|
|
244
|
+
// upgrade-time capture is exercised via this pure seam.
|
|
245
|
+
const fake = { cf: { tlsClientAuthCertSubject: 'CN=alice' } } as unknown as Request;
|
|
246
|
+
expect(extractMtlsSubject(fake)).toEqual({
|
|
247
|
+
subject: 'CN=alice',
|
|
248
|
+
canonicalDn: 'cn=alice',
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('includes the fingerprint from cf.tlsClientAuth.certFingerprintSHA256 when present', () => {
|
|
253
|
+
const fake = {
|
|
254
|
+
cf: {
|
|
255
|
+
tlsClientAuthCertSubject: 'CN=alice',
|
|
256
|
+
tlsClientAuth: { certFingerprintSHA256: 'af363dc85bc942a8' },
|
|
257
|
+
},
|
|
258
|
+
} as unknown as Request;
|
|
259
|
+
expect(extractMtlsSubject(fake)).toEqual({
|
|
260
|
+
subject: 'CN=alice',
|
|
261
|
+
canonicalDn: 'cn=alice',
|
|
262
|
+
fingerprint: 'af363dc85bc942a8',
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('omits the fingerprint when certFingerprintSHA256 is the documented empty default', () => {
|
|
267
|
+
// Cloudflare documents certFingerprintSHA256 as `""` when the
|
|
268
|
+
// connection is not mTLS — treat that as "no fingerprint".
|
|
269
|
+
const fake = {
|
|
270
|
+
cf: {
|
|
271
|
+
tlsClientAuthCertSubject: 'CN=alice',
|
|
272
|
+
tlsClientAuth: { certFingerprintSHA256: '' },
|
|
273
|
+
},
|
|
274
|
+
} as unknown as Request;
|
|
275
|
+
expect(extractMtlsSubject(fake)?.fingerprint).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('ignores a non-string certFingerprintSHA256', () => {
|
|
279
|
+
const fake = {
|
|
280
|
+
cf: {
|
|
281
|
+
tlsClientAuthCertSubject: 'CN=alice',
|
|
282
|
+
tlsClientAuth: { certFingerprintSHA256: 42 },
|
|
283
|
+
},
|
|
284
|
+
} as unknown as Request;
|
|
285
|
+
expect(extractMtlsSubject(fake)?.fingerprint).toBeUndefined();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('falls back to cf.tlsClientAuth.certSubjectDN when the legacy subject field is absent', () => {
|
|
289
|
+
const fake = {
|
|
290
|
+
cf: {
|
|
291
|
+
tlsClientAuth: { certSubjectDN: 'CN=alice,O=Example', certFingerprintSHA256: 'aa11' },
|
|
292
|
+
},
|
|
293
|
+
} as unknown as Request;
|
|
294
|
+
const identity = extractMtlsSubject(fake);
|
|
295
|
+
expect(identity?.subject).toBe('CN=alice,O=Example');
|
|
296
|
+
expect(identity?.canonicalDn).toBe('cn=alice,o=Example');
|
|
297
|
+
expect(identity?.fingerprint).toBe('aa11');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('prefers the legacy subject field over tlsClientAuth.certSubjectDN', () => {
|
|
301
|
+
const fake = {
|
|
302
|
+
cf: {
|
|
303
|
+
tlsClientAuthCertSubject: 'CN=legacy',
|
|
304
|
+
tlsClientAuth: { certSubjectDN: 'CN=newer' },
|
|
305
|
+
},
|
|
306
|
+
} as unknown as Request;
|
|
307
|
+
expect(extractMtlsSubject(fake)?.subject).toBe('CN=legacy');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('returns undefined when cf is absent', () => {
|
|
311
|
+
const fake = { cf: undefined } as unknown as Request;
|
|
312
|
+
expect(extractMtlsSubject(fake)).toBeUndefined();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('returns undefined when cf is present but carries no cert subject', () => {
|
|
316
|
+
const fake = {
|
|
317
|
+
cf: { country: 'US', tlsClientAuth: { certVerified: 'NONE' } },
|
|
318
|
+
} as unknown as Request;
|
|
319
|
+
expect(extractMtlsSubject(fake)).toBeUndefined();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('returns undefined when tlsClientAuthCertSubject is not a string', () => {
|
|
323
|
+
const fake = { cf: { tlsClientAuthCertSubject: 42 } } as unknown as Request;
|
|
324
|
+
expect(extractMtlsSubject(fake)).toBeUndefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('returns undefined when tlsClientAuth is not an object', () => {
|
|
328
|
+
const fake = { cf: { tlsClientAuth: 'nope' } } as unknown as Request;
|
|
329
|
+
expect(extractMtlsSubject(fake)).toBeUndefined();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* End-to-end SASL PLAIN through ConnectionDO with D1-backed
|
|
2
|
+
* End-to-end SASL PLAIN through ConnectionDO with D1-backed services.
|
|
3
3
|
*
|
|
4
|
-
* Verifies the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - D1 empty → the `SASL_ACCOUNTS` env-var seed still works (fallback).
|
|
4
|
+
* Verifies the services-store credential path: an account seeded into the
|
|
5
|
+
* `nickserv_accounts` table (via `ServicesStore.registerNick`) verifies
|
|
6
|
+
* end-to-end through `AUTHENTICATE PLAIN`.
|
|
8
7
|
*
|
|
9
8
|
* Each test file gets its own isolated D1 database from miniflare, so the
|
|
10
9
|
* table state set up here does not leak into `connection-do.test.ts`.
|
|
@@ -12,7 +11,7 @@
|
|
|
12
11
|
|
|
13
12
|
import { env } from 'cloudflare:test';
|
|
14
13
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
15
|
-
import {
|
|
14
|
+
import { CREATE_SERVICES_TABLES_SQL } from '../src/d1-services-store';
|
|
16
15
|
|
|
17
16
|
declare global {
|
|
18
17
|
namespace Cloudflare {
|
|
@@ -107,16 +106,56 @@ async function authenticatePlain(
|
|
|
107
106
|
client.send(`AUTHENTICATE ${plainB64(authcid, password)}\r\n`);
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Seeds a NickServ registration directly into the D1 services table by
|
|
111
|
+
* driving a first connection through `PRIVMSG NickServ :REGISTER`. Returns
|
|
112
|
+
* once the registration NOTICE is observed.
|
|
113
|
+
*/
|
|
114
|
+
async function seedViaNickservRegister(
|
|
115
|
+
connId: string,
|
|
116
|
+
nick: string,
|
|
117
|
+
password: string,
|
|
118
|
+
email: string,
|
|
119
|
+
): Promise<IrcWsClient> {
|
|
120
|
+
const client = await connect(connId);
|
|
121
|
+
client.send(`NICK ${nick}\r\n`);
|
|
122
|
+
client.send(`USER ${nick} 0 * :${nick}\r\n`);
|
|
123
|
+
await client.waitForMessage((l) => l.includes(' 001 '));
|
|
124
|
+
client.send(`PRIVMSG NickServ :REGISTER ${password} ${email}\r\n`);
|
|
125
|
+
await client.waitForMessage((l) => l.includes('NickServ') && l.includes('registered'));
|
|
126
|
+
return client;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('ConnectionDO — SASL PLAIN with D1-backed services', () => {
|
|
111
130
|
beforeEach(async () => {
|
|
112
|
-
|
|
131
|
+
for (const sql of CREATE_SERVICES_TABLES_SQL) {
|
|
132
|
+
await env.ACCOUNTS_DB.exec(sql);
|
|
133
|
+
}
|
|
113
134
|
});
|
|
114
135
|
afterEach(async () => {
|
|
115
|
-
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS
|
|
136
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS nickserv_accounts');
|
|
137
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS chanserv_channels');
|
|
138
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS chanserv_access');
|
|
139
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS chanserv_levels');
|
|
140
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS hostserv_vhosts');
|
|
141
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS hostserv_pending_vhosts');
|
|
142
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS memoserv_memos');
|
|
143
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS read_markers');
|
|
144
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS operserv_akills');
|
|
145
|
+
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS operserv_jupes');
|
|
116
146
|
});
|
|
117
147
|
|
|
118
|
-
it('completes SASL PLAIN with 900/903 for a
|
|
119
|
-
|
|
148
|
+
it('completes SASL PLAIN with 900/903 for a NickServ-registered account', async () => {
|
|
149
|
+
// Seed alice via NickServ REGISTER on a first connection.
|
|
150
|
+
const seeder = await seedViaNickservRegister(
|
|
151
|
+
'conn-d1-sasl-seed',
|
|
152
|
+
'd1-alice',
|
|
153
|
+
'd1-s3cret',
|
|
154
|
+
'd1@example.com',
|
|
155
|
+
);
|
|
156
|
+
await seeder.close();
|
|
157
|
+
|
|
158
|
+
// A second connection speaking SASL PLAIN against the registered nick.
|
|
120
159
|
const client = await connect('conn-d1-sasl-1');
|
|
121
160
|
await authenticatePlain(client, 'd1-alice', 'd1-s3cret');
|
|
122
161
|
await client.waitForMessage((l) => l.includes(' 900 '));
|
|
@@ -125,42 +164,27 @@ describe('ConnectionDO — SASL PLAIN with D1-backed accounts', () => {
|
|
|
125
164
|
await client.close();
|
|
126
165
|
});
|
|
127
166
|
|
|
128
|
-
it('rejects SASL PLAIN with 904 for a wrong password
|
|
129
|
-
await
|
|
167
|
+
it('rejects SASL PLAIN with 904 for a wrong password', async () => {
|
|
168
|
+
const seeder = await seedViaNickservRegister(
|
|
169
|
+
'conn-d1-sasl-seed2',
|
|
170
|
+
'd1-bob',
|
|
171
|
+
'd1-s3cret',
|
|
172
|
+
'd1@example.com',
|
|
173
|
+
);
|
|
174
|
+
await seeder.close();
|
|
175
|
+
|
|
130
176
|
const client = await connect('conn-d1-sasl-2');
|
|
131
|
-
await authenticatePlain(client, 'd1-
|
|
177
|
+
await authenticatePlain(client, 'd1-bob', 'wrong');
|
|
132
178
|
const fail = await client.waitForMessage((l) => l.includes(' 904 '));
|
|
133
179
|
expect(fail).toContain(' 904 ');
|
|
134
180
|
await client.close();
|
|
135
181
|
});
|
|
136
182
|
|
|
137
|
-
it('
|
|
138
|
-
// SASL_ACCOUNTS env seeds "cf-sasl:s3cret" but D1 has no such row.
|
|
139
|
-
await putAccountCredential(env.ACCOUNTS_DB, 'd1-alice', 'd1-s3cret');
|
|
183
|
+
it('rejects SASL PLAIN with 904 for an unknown nick', async () => {
|
|
140
184
|
const client = await connect('conn-d1-sasl-3');
|
|
141
|
-
|
|
142
|
-
await authenticatePlain(client, 'cf-sasl', 's3cret');
|
|
185
|
+
await authenticatePlain(client, 'nobody', 'whatever');
|
|
143
186
|
const fail = await client.waitForMessage((l) => l.includes(' 904 '));
|
|
144
187
|
expect(fail).toContain(' 904 ');
|
|
145
188
|
await client.close();
|
|
146
189
|
});
|
|
147
190
|
});
|
|
148
|
-
|
|
149
|
-
describe('ConnectionDO — SASL PLAIN in-memory fallback (D1 empty)', () => {
|
|
150
|
-
// The table exists but has zero rows → resolver falls back to SASL_ACCOUNTS.
|
|
151
|
-
beforeEach(async () => {
|
|
152
|
-
await env.ACCOUNTS_DB.exec(CREATE_ACCOUNTS_TABLE_SQL);
|
|
153
|
-
});
|
|
154
|
-
afterEach(async () => {
|
|
155
|
-
await env.ACCOUNTS_DB.exec('DROP TABLE IF EXISTS accounts');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('falls back to SASL_ACCOUNTS seed when D1 is empty', async () => {
|
|
159
|
-
const client = await connect('conn-d1-sasl-fallback');
|
|
160
|
-
await authenticatePlain(client, 'cf-sasl', 's3cret');
|
|
161
|
-
await client.waitForMessage((l) => l.includes(' 900 '));
|
|
162
|
-
const success = await client.waitForMessage((l) => l.includes(' 903 '));
|
|
163
|
-
expect(success).toContain(' 903 ');
|
|
164
|
-
await client.close();
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -223,12 +223,15 @@ describe('ConnectionDO — IRCv3 WebSocket spec contract (end-to-end)', () => {
|
|
|
223
223
|
client.messages.length = 0;
|
|
224
224
|
|
|
225
225
|
// One WS message that LOOKS like two PING lines. In spec mode the frame
|
|
226
|
-
// is a single IRC message, so
|
|
226
|
+
// is a single IRC message, so it is never split into two commands: the
|
|
227
|
+
// embedded CR makes the line unparseable (bare-CR rejection) and the
|
|
228
|
+
// actor answers with exactly ONE 421 — never a PONG, let alone two.
|
|
227
229
|
client.sendRaw('PING one\r\nPING two');
|
|
228
|
-
await client.
|
|
229
|
-
// Give a grace window for a (incorrect) second
|
|
230
|
+
await client.waitForLine((l) => l.includes(' 421 '));
|
|
231
|
+
// Give a grace window for a (incorrect) PONG or second reply to arrive.
|
|
230
232
|
await new Promise((r) => setTimeout(r, 100));
|
|
231
|
-
expect(countCommand(client.messages, 'PONG')).toBe(
|
|
233
|
+
expect(countCommand(client.messages, 'PONG')).toBe(0);
|
|
234
|
+
expect(client.messages.filter((m) => m.includes(' 421 '))).toHaveLength(1);
|
|
232
235
|
client.ws.close();
|
|
233
236
|
});
|
|
234
237
|
|