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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* `HibernatingWebSocketHandler` so idle WebSocket connections cost
|
|
7
7
|
* nothing between messages. When a message arrives the DO wakes, replays
|
|
8
8
|
* the persisted {@link ConnectionState} from `state.storage`, runs the
|
|
9
|
-
* frame through {@link ConnectionActor} with
|
|
10
|
-
*
|
|
9
|
+
* frame through {@link ConnectionActor} with the {@link CfRuntime}, and
|
|
10
|
+
* persists the updated state back.
|
|
11
11
|
*
|
|
12
12
|
* Lifecycle:
|
|
13
13
|
* - `fetch()` WebSocket upgrade; `acceptWebSocket`.
|
|
@@ -16,19 +16,21 @@
|
|
|
16
16
|
* - `alarm()` PING sweep; missing PONG disconnects.
|
|
17
17
|
*
|
|
18
18
|
* Cross-DO coordination:
|
|
19
|
-
* - `REGISTRY_DO`
|
|
20
|
-
* - `CHANNEL_DO` per lowercased channel
|
|
19
|
+
* - `REGISTRY_DO` — sharded nick registry (034).
|
|
20
|
+
* - `CHANNEL_DO` per lowercased channel — authoritative roster/modes
|
|
21
|
+
* (035).
|
|
21
22
|
*
|
|
22
23
|
* The actor's `ActorChannelAccess` is a passthrough that returns
|
|
23
24
|
* throwaway {@link ChannelState} objects — reducers mutate the local
|
|
24
25
|
* view and emit `ApplyChannelDelta` effects that the CfRuntime forwards
|
|
25
|
-
* to the authoritative ChannelDO.
|
|
26
|
-
*
|
|
27
|
-
* state)
|
|
26
|
+
* to the authoritative ChannelDO. Connection-authority commands and
|
|
27
|
+
* JOIN/QUIT fan-out are fully wired; richer channel reads (NAMES of
|
|
28
|
+
* remote state) are served via the ChannelDO snapshot RPC.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
import { DurableObject } from 'cloudflare:workers';
|
|
31
32
|
import {
|
|
33
|
+
type AccountStore,
|
|
32
34
|
type ChanName,
|
|
33
35
|
type ChannelState,
|
|
34
36
|
type Clock,
|
|
@@ -37,15 +39,19 @@ import {
|
|
|
37
39
|
ConsoleLogger,
|
|
38
40
|
type IdFactory,
|
|
39
41
|
InMemoryMessageStore,
|
|
42
|
+
InMemoryMtlsIdentityProvider,
|
|
43
|
+
InMemoryNickHistoryStore,
|
|
40
44
|
LogLevel,
|
|
41
45
|
type Logger,
|
|
42
46
|
type MessageStore,
|
|
43
47
|
type MotdProvider,
|
|
48
|
+
type MtlsIdentityProvider,
|
|
49
|
+
type NickHistoryStore,
|
|
44
50
|
type RawLine,
|
|
51
|
+
type SaslAccountCredential,
|
|
45
52
|
type ServerConfig,
|
|
46
53
|
SystemClock,
|
|
47
54
|
UuidIdFactory,
|
|
48
|
-
applyChannelDelta,
|
|
49
55
|
createChannel,
|
|
50
56
|
createConnection,
|
|
51
57
|
toSnapshot,
|
|
@@ -53,8 +59,10 @@ import {
|
|
|
53
59
|
import { ConnectionActor } from '@serverless-ircd/irc-server';
|
|
54
60
|
import { makeCfRuntime } from './cf-runtime.js';
|
|
55
61
|
import type { CfConnectionHandlers } from './cf-runtime.js';
|
|
62
|
+
import { resolveAccountStore } from './d1-account-store.js';
|
|
56
63
|
import type { Env } from './env.js';
|
|
57
|
-
import {
|
|
64
|
+
import { STATE_STORAGE_KEY, deserialize, serialize } from './serialize.js';
|
|
65
|
+
import type { PersistedConnectionState } from './serialize.js';
|
|
58
66
|
|
|
59
67
|
/** Default PING cadence (ms) — tuned for typical IRC client behavior. */
|
|
60
68
|
export const DEFAULT_PING_INTERVAL_MS = 60_000;
|
|
@@ -104,12 +112,53 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
104
112
|
* follow-up. Lazily constructed so the first frame seeds it.
|
|
105
113
|
*/
|
|
106
114
|
private messages: MessageStore | undefined;
|
|
115
|
+
/**
|
|
116
|
+
* Per-connection nick-history store backing `WHOWAS`. The minimum-viable
|
|
117
|
+
* in-worker ring buffer is scoped to this connection: sign-offs this
|
|
118
|
+
* connection records are queryable by the same connection. A
|
|
119
|
+
* Durable-Object-backed persistent variant (shared across connections,
|
|
120
|
+
* keyed by nick) is a documented follow-up. Lazily constructed.
|
|
121
|
+
*/
|
|
122
|
+
private history: NickHistoryStore | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* SASL account store, resolved once via {@link loadAccountStore} before the
|
|
125
|
+
* first frame dispatches. The two-phase load mirrors AWS's
|
|
126
|
+
* `loadDynamoAccountStore`: D1 `accounts` table (authoritative when it has
|
|
127
|
+
* rows) is queried at boot; when empty/unreachable the `SASL_ACCOUNTS`
|
|
128
|
+
* env-var seed provides the fallback `InMemoryAccountStore`. The
|
|
129
|
+
* `AccountStore.verify` port is synchronous, so pre-loading MUST complete
|
|
130
|
+
* before the first reducer runs.
|
|
131
|
+
*/
|
|
132
|
+
private accounts: AccountStore | undefined;
|
|
133
|
+
/** Guards {@link loadAccountStore} so the D1 query runs at most once. */
|
|
134
|
+
private accountsResolved = false;
|
|
135
|
+
/**
|
|
136
|
+
* Verified client-cert subject captured at WebSocket upgrade time when
|
|
137
|
+
* CF API Shield mTLS is configured. Surfaced to the SASL EXTERNAL reducer
|
|
138
|
+
* via an {@link MtlsIdentityProvider} so `AUTHENTICATE EXTERNAL` can map
|
|
139
|
+
* the cert subject to an account.
|
|
140
|
+
*
|
|
141
|
+
* CF API Shield mTLS setup:
|
|
142
|
+
* 1. Upload the client CA to API Shield (dashboard → Security → API Shield).
|
|
143
|
+
* 2. Create an mTLS policy on the custom hostname that requires client certs.
|
|
144
|
+
* 3. The verified subject is injected into `request.cf.tlsClientAuthCertSubject`
|
|
145
|
+
* on every request, which the Worker reads at upgrade time.
|
|
146
|
+
* When unset (no mTLS configured), EXTERNAL auth fails with `904`.
|
|
147
|
+
*/
|
|
148
|
+
private mtlsCertSubject: string | undefined;
|
|
107
149
|
private readonly clock: Clock = SystemClock;
|
|
108
150
|
private readonly ids: IdFactory = new UuidIdFactory();
|
|
109
151
|
private readonly pingIntervalMs: number = DEFAULT_PING_INTERVAL_MS;
|
|
110
152
|
private readonly pongTimeoutMs: number = DEFAULT_PONG_TIMEOUT_MS;
|
|
111
|
-
|
|
112
|
-
// (
|
|
153
|
+
// Outbound lines are batched per call, not via a reserved instance
|
|
154
|
+
// field: `deliver()` joins every line of a ChannelDO broadcast into a
|
|
155
|
+
// single `WebSocket.send()`, and the actor `send` handler in
|
|
156
|
+
// `buildActor` does the same for one frame's response. Cross-call
|
|
157
|
+
// per-microtask coalescing (the optimization previously reserved here)
|
|
158
|
+
// was dropped — it would add a drain queue and ordering hazards between
|
|
159
|
+
// independent fan-out calls for no measured benefit (see ADR-003 and
|
|
160
|
+
// the CF adapter load-test report). Revisit only if WS round-trips
|
|
161
|
+
// become a demonstrated bottleneck.
|
|
113
162
|
|
|
114
163
|
// -------------------------------------------------------------------------
|
|
115
164
|
// WebSocket upgrade
|
|
@@ -123,6 +172,14 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
123
172
|
const pair = new WebSocketPair();
|
|
124
173
|
const server = pair[0];
|
|
125
174
|
const client = pair[1];
|
|
175
|
+
// Capture the verified client-cert subject when CF API Shield mTLS is
|
|
176
|
+
// active. `request.cf` is a Cloudflare-specific extension; the property
|
|
177
|
+
// is present only when a client certificate was verified against the
|
|
178
|
+
// uploaded CA pool.
|
|
179
|
+
const cf = (request as Request & { cf?: Record<string, unknown> }).cf;
|
|
180
|
+
if (cf !== undefined && typeof cf.tlsClientAuthCertSubject === 'string') {
|
|
181
|
+
this.mtlsCertSubject = cf.tlsClientAuthCertSubject;
|
|
182
|
+
}
|
|
126
183
|
// Hibernation: `acceptWebSocket` accepts the server side implicitly.
|
|
127
184
|
// Calling `server.accept()` first throws "already accepted".
|
|
128
185
|
this.ctx.acceptWebSocket(server);
|
|
@@ -139,9 +196,9 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
139
196
|
override async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
|
140
197
|
const text = typeof message === 'string' ? message : new TextDecoder().decode(message);
|
|
141
198
|
const state = await this.loadState();
|
|
142
|
-
const before = new Set(state.joinedChannels);
|
|
143
199
|
const beforeNick = state.nick;
|
|
144
200
|
|
|
201
|
+
await this.loadAccountStore();
|
|
145
202
|
const actor = this.buildActor(ws, state);
|
|
146
203
|
try {
|
|
147
204
|
await actor.receiveTextFrame(text);
|
|
@@ -154,17 +211,11 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
154
211
|
if (beforeNick === undefined && state.nick !== undefined) {
|
|
155
212
|
await this.ctx.storage.setAlarm(Date.now() + this.pingIntervalMs);
|
|
156
213
|
}
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
if (!before.has(chan)) {
|
|
163
|
-
// joinReducer already emitted an ApplyChannelDelta; the stub
|
|
164
|
-
// ChannelDO records it. No extra work needed here for 033.
|
|
165
|
-
void chan;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
214
|
+
// No per-ConnectionDO channel-registration step is needed here: channel
|
|
215
|
+
// membership is driven solely by the `ApplyChannelDelta` effect the
|
|
216
|
+
// joinReducer emits, which the CfRuntime forwards to the authoritative
|
|
217
|
+
// ChannelDO. The ChannelDO roster IS the fan-out registration target, so
|
|
218
|
+
// a second loop over newly joined channels here would be redundant.
|
|
168
219
|
}
|
|
169
220
|
|
|
170
221
|
/** QUIT fanout + nick release. RFC 1459: a missing close is a QUIT. */
|
|
@@ -299,9 +350,9 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
299
350
|
},
|
|
300
351
|
snapshot: (): ConnectionState | undefined => this.cached,
|
|
301
352
|
};
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
353
|
+
// The CfRuntime shards the registry by nick (034); the per-connection
|
|
354
|
+
// key passed here is ignored by `makeCfRuntime` (kept in the signature
|
|
355
|
+
// for call-site stability).
|
|
305
356
|
const registryKey = this.ctx.id.toString();
|
|
306
357
|
const runtime = makeCfRuntime(this.env, state.id, registryKey, handlers);
|
|
307
358
|
if (this.channelAccess === undefined) {
|
|
@@ -311,6 +362,11 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
311
362
|
// Bound `connectionId` so every record the actor emits is filterable
|
|
312
363
|
// per-connection in the Workers dashboard.
|
|
313
364
|
const logger: Logger = new ConsoleLogger({ connectionId: state.id }, undefined, LogLevel.Info);
|
|
365
|
+
const accounts = this.accountStore();
|
|
366
|
+
const mtlsIdentity: MtlsIdentityProvider | undefined =
|
|
367
|
+
this.mtlsCertSubject !== undefined
|
|
368
|
+
? new InMemoryMtlsIdentityProvider([{ connId: state.id, identity: this.mtlsCertSubject }])
|
|
369
|
+
: undefined;
|
|
314
370
|
return new ConnectionActor({
|
|
315
371
|
state,
|
|
316
372
|
runtime,
|
|
@@ -320,6 +376,9 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
320
376
|
ids: this.ids,
|
|
321
377
|
motd,
|
|
322
378
|
messages: this.messageStore(),
|
|
379
|
+
history: this.historyStore(),
|
|
380
|
+
...(accounts !== undefined ? { accounts } : {}),
|
|
381
|
+
...(mtlsIdentity !== undefined ? { mtlsIdentity } : {}),
|
|
323
382
|
logger,
|
|
324
383
|
});
|
|
325
384
|
}
|
|
@@ -327,6 +386,7 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
327
386
|
/** Tears down a connection: emit QUIT effects, release nick, close WS. */
|
|
328
387
|
private async tearDown(ws: WebSocket): Promise<void> {
|
|
329
388
|
const state = await this.loadState();
|
|
389
|
+
await this.loadAccountStore();
|
|
330
390
|
const actor = this.buildActor(ws, state);
|
|
331
391
|
// Drive QUIT through the actor so the same effect pipeline handles
|
|
332
392
|
// fanout as during normal operation.
|
|
@@ -359,6 +419,8 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
359
419
|
...DEFAULT_SERVER_CONFIG,
|
|
360
420
|
...(this.env.SERVER_NAME !== undefined ? { serverName: this.env.SERVER_NAME } : {}),
|
|
361
421
|
...(this.env.NETWORK_NAME !== undefined ? { networkName: this.env.NETWORK_NAME } : {}),
|
|
422
|
+
...(this.env.SERVER_VERSION !== undefined ? { serverVersion: this.env.SERVER_VERSION } : {}),
|
|
423
|
+
...(this.env.CREATED_AT !== undefined ? { createdAt: this.env.CREATED_AT } : {}),
|
|
362
424
|
};
|
|
363
425
|
}
|
|
364
426
|
|
|
@@ -384,6 +446,41 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
384
446
|
return this.messages;
|
|
385
447
|
}
|
|
386
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Lazily constructs the per-connection nick-history store. A future
|
|
451
|
+
* persistent variant (DO/KV-backed, keyed by nick) will replace this
|
|
452
|
+
* without touching the actor wiring.
|
|
453
|
+
*/
|
|
454
|
+
private historyStore(): NickHistoryStore {
|
|
455
|
+
if (this.history === undefined) {
|
|
456
|
+
this.history = new InMemoryNickHistoryStore(this.clock);
|
|
457
|
+
}
|
|
458
|
+
return this.history;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Resolves the SASL account store once (D1 table → `SASL_ACCOUNTS` seed),
|
|
463
|
+
* caching the result. Safe to call on every frame; the D1 query runs at
|
|
464
|
+
* most once per ConnectionDO instance. Must complete before the first
|
|
465
|
+
* reducer dispatches so the synchronous `AccountStore.verify` port has its
|
|
466
|
+
* data ready.
|
|
467
|
+
*/
|
|
468
|
+
private async loadAccountStore(): Promise<void> {
|
|
469
|
+
if (this.accountsResolved) return;
|
|
470
|
+
this.accountsResolved = true;
|
|
471
|
+
this.accounts = await resolveAccountStore(this.env.ACCOUNTS_DB, this.env.SASL_ACCOUNTS);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Returns the resolved account store, or `undefined` when no accounts are
|
|
476
|
+
* configured (so the actor's `ctx.accounts` stays unset: `AUTHENTICATE
|
|
477
|
+
* PLAIN` → `904`). Populated by {@link loadAccountStore}; callers MUST
|
|
478
|
+
* `await loadAccountStore()` before reading this.
|
|
479
|
+
*/
|
|
480
|
+
private accountStore(): AccountStore | undefined {
|
|
481
|
+
return this.accounts;
|
|
482
|
+
}
|
|
483
|
+
|
|
387
484
|
// -------------------------------------------------------------------------
|
|
388
485
|
// Test hooks (only invoked from `runInDurableObject` in tests)
|
|
389
486
|
// -------------------------------------------------------------------------
|
|
@@ -409,6 +506,7 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
409
506
|
private async tearDownNoSocket(state: ConnectionState): Promise<void> {
|
|
410
507
|
// Emit QUIT effects without dispatching transport calls (the socket
|
|
411
508
|
// is already gone). The cross-DO fanout still runs.
|
|
509
|
+
await this.loadAccountStore();
|
|
412
510
|
const noOpHandlers: CfConnectionHandlers = {
|
|
413
511
|
send: (): void => {},
|
|
414
512
|
disconnect: (): void => {},
|
|
@@ -428,6 +526,7 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
428
526
|
ids: this.ids,
|
|
429
527
|
motd: this.motdProvider(),
|
|
430
528
|
messages: this.messageStore(),
|
|
529
|
+
history: this.historyStore(),
|
|
431
530
|
});
|
|
432
531
|
await actor.receiveTextFrame('QUIT\r\n');
|
|
433
532
|
await this.releaseAndPersist(state);
|
|
@@ -470,6 +569,38 @@ export class ConnectionDO extends DurableObject<Env> {
|
|
|
470
569
|
const state = await this.loadState();
|
|
471
570
|
return toSnapshot(state);
|
|
472
571
|
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Cross-connection RPC: returns the full connection state as a
|
|
575
|
+
* JSON-friendly DTO ({@link PersistedConnectionState}). Called by
|
|
576
|
+
* another connection's {@link CfRuntime} when a `GetConnection`
|
|
577
|
+
* lookup targets this connection (e.g. WHOIS). The DTO survives DO
|
|
578
|
+
* RPC because `Set`/`Map` fields are pre-flattened to arrays by
|
|
579
|
+
* {@link serialize}; the caller rehydrates via {@link deserialize}.
|
|
580
|
+
*/
|
|
581
|
+
public async getConnState(): Promise<PersistedConnectionState | null> {
|
|
582
|
+
const state = await this.loadState();
|
|
583
|
+
return serialize(state);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Cross-connection RPC: tears down this connection. Reuses the same
|
|
588
|
+
* QUIT fan-out / nick-release / WebSocket-close path as the normal
|
|
589
|
+
* `webSocketClose` handler so KICK and operator-driven disconnect
|
|
590
|
+
* produce identical side effects. If no live socket is attached the
|
|
591
|
+
* no-socket teardown path still releases the nick.
|
|
592
|
+
*/
|
|
593
|
+
public async disconnect(reason?: string): Promise<void> {
|
|
594
|
+
void reason;
|
|
595
|
+
const sockets = this.ctx.getWebSockets();
|
|
596
|
+
const ws = sockets[0];
|
|
597
|
+
if (ws === undefined) {
|
|
598
|
+
const state = await this.loadState();
|
|
599
|
+
await this.tearDownNoSocket(state);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await this.tearDown(ws as WebSocket);
|
|
603
|
+
}
|
|
473
604
|
}
|
|
474
605
|
|
|
475
606
|
/** Default MOTD when no `MOTD_LINES` env is supplied. */
|
|
@@ -569,8 +700,27 @@ class PassthroughChannelAccess {
|
|
|
569
700
|
}
|
|
570
701
|
}
|
|
571
702
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
703
|
+
/**
|
|
704
|
+
* Parses the `SASL_ACCOUNTS` env var into credential entries.
|
|
705
|
+
*
|
|
706
|
+
* Format: newline-delimited `username:password` pairs (mirroring the
|
|
707
|
+
* `MOTD_LINES` convention). Blank lines and malformed entries (missing
|
|
708
|
+
* colon or empty username/password) are skipped so a trailing newline or a
|
|
709
|
+
* partial edit does not break boot. Exported so the parsing boundary is
|
|
710
|
+
* unit-testable in isolation.
|
|
711
|
+
*/
|
|
712
|
+
export function parseSaslAccountsEnv(raw: string | undefined): SaslAccountCredential[] {
|
|
713
|
+
if (raw === undefined || raw.length === 0) return [];
|
|
714
|
+
const out: SaslAccountCredential[] = [];
|
|
715
|
+
for (const line of raw.split('\n')) {
|
|
716
|
+
const trimmed = line.trim();
|
|
717
|
+
if (trimmed.length === 0) continue;
|
|
718
|
+
const sep = trimmed.indexOf(':');
|
|
719
|
+
if (sep <= 0) continue; // missing colon or empty username
|
|
720
|
+
const username = trimmed.slice(0, sep);
|
|
721
|
+
const password = trimmed.slice(sep + 1);
|
|
722
|
+
if (username.length === 0 || password.length === 0) continue;
|
|
723
|
+
out.push({ username, password });
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1-backed SASL account store primitives for the Cloudflare adapter.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the AWS `dynamo-account-store.ts` / `account-store.ts` pair:
|
|
5
|
+
* credentials are stored as scrypt hashes (never plaintext) in a D1
|
|
6
|
+
* `accounts` table and pre-loaded at {@link ConnectionDO} construction
|
|
7
|
+
* into the shared {@link HashedAccountStore}.
|
|
8
|
+
*
|
|
9
|
+
* Table-then-config precedence is implemented by {@link resolveAccountStore}:
|
|
10
|
+
* when the D1 table has rows it is authoritative; when it is empty (or
|
|
11
|
+
* unreachable, or the binding is absent) the code falls back to the
|
|
12
|
+
* `SASL_ACCOUNTS` env-var seed — preserving the config-seed behaviour for
|
|
13
|
+
* deployments that have not migrated to D1.
|
|
14
|
+
*
|
|
15
|
+
* The adapters diverge only at the scan/put boundary; the hashing helpers
|
|
16
|
+
* and the synchronous {@link HashedAccountStore} are shared via `irc-core`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type AccountStore,
|
|
21
|
+
type HashedAccountCredential,
|
|
22
|
+
HashedAccountStore,
|
|
23
|
+
InMemoryAccountStore,
|
|
24
|
+
hashAccountCredential,
|
|
25
|
+
} from '@serverless-ircd/irc-core';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SQL to create the `accounts` table. Exported so the seed tool and
|
|
29
|
+
* provisioning docs reference one schema definition.
|
|
30
|
+
*/
|
|
31
|
+
export const CREATE_ACCOUNTS_TABLE_SQL =
|
|
32
|
+
'CREATE TABLE IF NOT EXISTS accounts (account TEXT PRIMARY KEY, algorithm TEXT NOT NULL, salt TEXT NOT NULL, hash TEXT NOT NULL)';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A raw D1 row — the untyped shape returned by `prepare().all()`. Each
|
|
36
|
+
* column is `unknown` until validated into a {@link HashedAccountCredential}.
|
|
37
|
+
*/
|
|
38
|
+
type AccountsRow = Record<string, unknown>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Coerces a raw D1 row into a {@link HashedAccountCredential}, or returns
|
|
42
|
+
* `undefined` when the row is missing required columns or has the wrong
|
|
43
|
+
* types. Mirrors the silent-skip behaviour of the DynamoDB loader.
|
|
44
|
+
*/
|
|
45
|
+
function coerceRow(row: AccountsRow): HashedAccountCredential | undefined {
|
|
46
|
+
const { account, algorithm, salt, hash } = row;
|
|
47
|
+
if (
|
|
48
|
+
typeof account !== 'string' ||
|
|
49
|
+
typeof algorithm !== 'string' ||
|
|
50
|
+
typeof salt !== 'string' ||
|
|
51
|
+
typeof hash !== 'string'
|
|
52
|
+
) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
account,
|
|
57
|
+
algorithm: algorithm as HashedAccountCredential['algorithm'],
|
|
58
|
+
salt,
|
|
59
|
+
hash,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Queries the D1 `accounts` table and returns a fully-populated
|
|
65
|
+
* {@link HashedAccountStore}, or `undefined` when the table has no rows.
|
|
66
|
+
*
|
|
67
|
+
* Rows missing required attributes are silently skipped. Callers that need
|
|
68
|
+
* the table-then-config fallback should use {@link resolveAccountStore}
|
|
69
|
+
* rather than catching `undefined` themselves.
|
|
70
|
+
*
|
|
71
|
+
* @param db The D1 binding (may be `undefined` when D1 is not configured).
|
|
72
|
+
*/
|
|
73
|
+
export async function loadD1AccountStore(
|
|
74
|
+
db: D1Database | undefined,
|
|
75
|
+
): Promise<HashedAccountStore | undefined> {
|
|
76
|
+
if (db === undefined) return undefined;
|
|
77
|
+
const result = await db
|
|
78
|
+
.prepare('SELECT account, algorithm, salt, hash FROM accounts')
|
|
79
|
+
.all<AccountsRow>();
|
|
80
|
+
const entries: HashedAccountCredential[] = [];
|
|
81
|
+
for (const row of result.results ?? []) {
|
|
82
|
+
const entry = coerceRow(row);
|
|
83
|
+
if (entry !== undefined) entries.push(entry);
|
|
84
|
+
}
|
|
85
|
+
if (entries.length === 0) return undefined;
|
|
86
|
+
return new HashedAccountStore(entries);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Writes (or re-seeds) a SASL PLAIN account into the D1 `accounts` table.
|
|
91
|
+
*
|
|
92
|
+
* The password is hashed via {@link hashAccountCredential} (scrypt, random
|
|
93
|
+
* salt) and stored as a {@link HashedAccountCredential} row — plaintext is
|
|
94
|
+
* never written. A repeat call for the same `username` overwrites the prior
|
|
95
|
+
* row (the PK is `account`), so this is the canonical CRUD/seed entry point
|
|
96
|
+
* for oper and SASL account provisioning.
|
|
97
|
+
*
|
|
98
|
+
* @returns The stored {@link HashedAccountCredential} (for tooling/tests).
|
|
99
|
+
*/
|
|
100
|
+
export async function putAccountCredential(
|
|
101
|
+
db: D1Database,
|
|
102
|
+
username: string,
|
|
103
|
+
password: string,
|
|
104
|
+
): Promise<HashedAccountCredential> {
|
|
105
|
+
const entry = hashAccountCredential(username, password);
|
|
106
|
+
await db
|
|
107
|
+
.prepare('INSERT OR REPLACE INTO accounts (account, algorithm, salt, hash) VALUES (?, ?, ?, ?)')
|
|
108
|
+
.bind(entry.account, entry.algorithm, entry.salt, entry.hash)
|
|
109
|
+
.run();
|
|
110
|
+
return entry;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Builds a self-contained `INSERT OR REPLACE` SQL statement that writes a
|
|
115
|
+
* scrypt-hashed account row, suitable for `wrangler d1 execute --command`.
|
|
116
|
+
*
|
|
117
|
+
* The password is hashed via {@link hashAccountCredential}; the plaintext
|
|
118
|
+
* never appears in the SQL. The values are single-quoted with embedded
|
|
119
|
+
* single quotes doubled (`'` → `''`) to avoid injection through the
|
|
120
|
+
* username or the base64 output.
|
|
121
|
+
*
|
|
122
|
+
* This is the pure (non-IO) half of {@link putAccountCredential}; the seed
|
|
123
|
+
* CLI (`tools/seed-cf-accounts.ts`) calls this and pipes the result to
|
|
124
|
+
* `wrangler d1 execute`, while the runtime `ConnectionDO` path uses the
|
|
125
|
+
* bound-parameter {@link putAccountCredential}.
|
|
126
|
+
*
|
|
127
|
+
* @returns The hashed credential and the executable SQL string.
|
|
128
|
+
*/
|
|
129
|
+
export function buildAccountPutStatement(
|
|
130
|
+
username: string,
|
|
131
|
+
password: string,
|
|
132
|
+
): { entry: HashedAccountCredential; sql: string } {
|
|
133
|
+
const entry = hashAccountCredential(username, password);
|
|
134
|
+
const sql = `INSERT OR REPLACE INTO accounts (account, algorithm, salt, hash) VALUES ('${sqlEscape(entry.account)}', '${sqlEscape(entry.algorithm)}', '${sqlEscape(entry.salt)}', '${sqlEscape(entry.hash)}')`;
|
|
135
|
+
return { entry, sql };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Doubles single quotes for safe embedding in a D1 SQL string literal. */
|
|
139
|
+
function sqlEscape(value: string): string {
|
|
140
|
+
return value.replace(/'/g, "''");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves the SASL account store for a CF deployment, applying the
|
|
145
|
+
* table-then-config precedence.
|
|
146
|
+
*
|
|
147
|
+
* 1. Queries the D1 `accounts` table (pre-loading every hashed credential
|
|
148
|
+
* into a {@link HashedAccountStore}). When the table has one or more
|
|
149
|
+
* rows it is authoritative — the table wins and the config seed is
|
|
150
|
+
* ignored.
|
|
151
|
+
* 2. When the table is empty (or the binding is absent), falls back to
|
|
152
|
+
* the `SASL_ACCOUNTS` env-var seed (an {@link InMemoryAccountStore})
|
|
153
|
+
* so existing deployments that have not migrated to D1 keep working.
|
|
154
|
+
* 3. Returns `undefined` when neither source has accounts so the actor's
|
|
155
|
+
* `ctx.accounts` stays unset (`AUTHENTICATE PLAIN` → `904`).
|
|
156
|
+
*
|
|
157
|
+
* Query failures are swallowed and treated as an empty table, falling
|
|
158
|
+
* through to the config seed — mirroring `resolveAccountStore` on AWS.
|
|
159
|
+
*
|
|
160
|
+
* @param db The D1 binding (`undefined` when D1 is not configured).
|
|
161
|
+
* @param saslAccountsRaw The raw `SASL_ACCOUNTS` env var (newline-delimited
|
|
162
|
+
* `username:password` pairs), or `undefined` when unset.
|
|
163
|
+
*/
|
|
164
|
+
export async function resolveAccountStore(
|
|
165
|
+
db: D1Database | undefined,
|
|
166
|
+
saslAccountsRaw: string | undefined,
|
|
167
|
+
): Promise<AccountStore | undefined> {
|
|
168
|
+
try {
|
|
169
|
+
const store = await loadD1AccountStore(db);
|
|
170
|
+
if (store !== undefined) return store;
|
|
171
|
+
} catch {
|
|
172
|
+
// Table might not exist yet or be unreachable; fall through to seed.
|
|
173
|
+
}
|
|
174
|
+
const seed = parseSeed(saslAccountsRaw);
|
|
175
|
+
if (seed.length === 0) return undefined;
|
|
176
|
+
return new InMemoryAccountStore(seed);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parses the `SASL_ACCOUNTS` env var into credential entries for the
|
|
181
|
+
* config-seed fallback. Re-exported from the in-memory account-store module
|
|
182
|
+
* so the precedence resolver is self-contained.
|
|
183
|
+
*/
|
|
184
|
+
function parseSeed(raw: string | undefined): Array<{ username: string; password: string }> {
|
|
185
|
+
if (raw === undefined || raw.length === 0) return [];
|
|
186
|
+
const out: Array<{ username: string; password: string }> = [];
|
|
187
|
+
for (const line of raw.split('\n')) {
|
|
188
|
+
const trimmed = line.trim();
|
|
189
|
+
if (trimmed.length === 0) continue;
|
|
190
|
+
const sep = trimmed.indexOf(':');
|
|
191
|
+
if (sep <= 0) continue;
|
|
192
|
+
const username = trimmed.slice(0, sep);
|
|
193
|
+
const password = trimmed.slice(sep + 1);
|
|
194
|
+
if (username.length === 0 || password.length === 0) continue;
|
|
195
|
+
out.push({ username, password });
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The production {@link Env} for `apps/cf-worker`. ConnectionDO uses the
|
|
11
|
-
*
|
|
12
|
-
* authorities.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* four DO namespaces below to coordinate with the registry, channel, and
|
|
12
|
+
* channel-registry authorities. The CfRuntime (036) wires them together
|
|
13
|
+
* behind the platform-agnostic {@link IrcRuntime} port; ConnectionDO
|
|
14
|
+
* (033) only needs `CONNECTION_DO` itself plus the three collaborator
|
|
15
|
+
* namespaces.
|
|
15
16
|
*
|
|
16
17
|
* RPC method shapes on {@link RegistryRpc} and {@link ChannelRpc} are
|
|
17
18
|
* implemented by both the production DOs and the test stubs in
|
|
@@ -22,22 +23,60 @@ export interface Env {
|
|
|
22
23
|
CONNECTION_DO: DurableObjectNamespace;
|
|
23
24
|
/**
|
|
24
25
|
* Nick registry authority. RPC: reserveNick / changeNick / releaseNick /
|
|
25
|
-
* lookupNick.
|
|
26
|
+
* lookupNick. Backed by the sharded {@link RegistryDO} (034), which keys
|
|
27
|
+
* instances by `hash(nick) % N` so the single-threaded DO gives the nick
|
|
28
|
+
* uniqueness invariant for free.
|
|
26
29
|
*
|
|
27
30
|
* Note: the production DO class is what's brand-typed; here we keep the
|
|
28
|
-
* namespace unparameterized and cast to the RPC surface at the call
|
|
29
|
-
* site. The full type plumbing lands with 034.
|
|
31
|
+
* namespace unparameterized and cast to the RPC surface at the call site.
|
|
30
32
|
*/
|
|
31
33
|
REGISTRY_DO: DurableObjectNamespace;
|
|
32
34
|
/**
|
|
33
35
|
* Channel authority, keyed by lowercased channel name. RPC: broadcast /
|
|
34
|
-
* applyChannelDelta / getChannelSnapshot
|
|
36
|
+
* applyChannelDelta / getChannelSnapshot / listMembers. Backed by the
|
|
37
|
+
* {@link ChannelDO} (035), which owns the authoritative roster, modes,
|
|
38
|
+
* topic, and ban list and fans out broadcasts to each member's
|
|
39
|
+
* ConnectionDO.
|
|
35
40
|
*/
|
|
36
41
|
CHANNEL_DO: DurableObjectNamespace;
|
|
42
|
+
/**
|
|
43
|
+
* Channel registry authority. A single DO instance tracks the set of
|
|
44
|
+
* all channel names so {@link CfRuntime.listChannels} can enumerate
|
|
45
|
+
* them without scanning every per-channel DO. RPC: register /
|
|
46
|
+
* unregister / list.
|
|
47
|
+
*/
|
|
48
|
+
CHANNEL_REGISTRY_DO: DurableObjectNamespace;
|
|
49
|
+
/**
|
|
50
|
+
* D1 database backing persistent SASL account credentials. The `accounts`
|
|
51
|
+
* table holds scrypt-hashed rows (`{ account PK, algorithm, salt, hash }`),
|
|
52
|
+
* loaded at {@link ConnectionDO} construction by `loadD1AccountStore`.
|
|
53
|
+
*
|
|
54
|
+
* Optional so deployments that have not migrated to D1 keep working: when
|
|
55
|
+
* unset, the precedence resolver falls straight through to the
|
|
56
|
+
* `SASL_ACCOUNTS` env-var seed (the legacy `InMemoryAccountStore`).
|
|
57
|
+
*/
|
|
58
|
+
ACCOUNTS_DB?: D1Database;
|
|
37
59
|
/** Server-level config knobs (name, MOTD lines, limits). */
|
|
38
60
|
SERVER_NAME: string;
|
|
39
61
|
NETWORK_NAME: string;
|
|
62
|
+
/**
|
|
63
|
+
* Server version surfaced in `002`/`004`/`351`/`371`. Optional; when unset
|
|
64
|
+
* the reducer falls back to the irc-core default version.
|
|
65
|
+
*/
|
|
66
|
+
SERVER_VERSION?: string;
|
|
67
|
+
/**
|
|
68
|
+
* "Created" text for `003 RPL_CREATED`. Optional; when unset the reducer
|
|
69
|
+
* falls back to the default created text.
|
|
70
|
+
*/
|
|
71
|
+
CREATED_AT?: string;
|
|
40
72
|
MOTD_LINES: string;
|
|
73
|
+
/**
|
|
74
|
+
* SASL PLAIN accounts seeded into the in-worker `AccountStore`.
|
|
75
|
+
* Newline-delimited `username:password` pairs; the connection-do parses
|
|
76
|
+
* these into an `InMemoryAccountStore` so `AUTHENTICATE PLAIN` works
|
|
77
|
+
* end-to-end. Empty/undefined disables SASL account verification.
|
|
78
|
+
*/
|
|
79
|
+
SASL_ACCOUNTS?: string;
|
|
41
80
|
}
|
|
42
81
|
|
|
43
82
|
/**
|
|
@@ -64,4 +103,15 @@ export interface ChannelRpc {
|
|
|
64
103
|
broadcast(lines: unknown[], except?: string): Promise<void>;
|
|
65
104
|
applyChannelDelta(delta: unknown): Promise<void>;
|
|
66
105
|
getChannelSnapshot(): Promise<unknown>;
|
|
106
|
+
listMembers(): Promise<string[]>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* RPC contract for the channel registry DO. Tracks the set of all
|
|
111
|
+
* channel names so {@link CfRuntime.listChannels} can enumerate them.
|
|
112
|
+
*/
|
|
113
|
+
export interface ChannelRegistryRpc {
|
|
114
|
+
register(name: string): Promise<void>;
|
|
115
|
+
unregister(name: string): Promise<void>;
|
|
116
|
+
list(): Promise<string[]>;
|
|
67
117
|
}
|