serverless-ircd 0.7.0 → 0.8.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.
Files changed (135) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/.gitmodules +1 -1
  3. package/CHANGELOG.md +273 -29
  4. package/README.md +155 -55
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/aws-stack/src/aws-stack.ts +186 -18
  7. package/apps/aws-stack/tests/stack.test.ts +400 -56
  8. package/apps/cf-tcp-container/package.json +1 -1
  9. package/apps/cf-worker/package.json +1 -1
  10. package/apps/cf-worker/src/worker.ts +4 -4
  11. package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
  12. package/apps/cf-worker/tests/smoke.test.ts +5 -5
  13. package/apps/cf-worker/wrangler.test.toml +6 -6
  14. package/apps/cf-worker/wrangler.toml +7 -5
  15. package/apps/local-cli/package.json +1 -1
  16. package/apps/local-cli/src/config-loader.ts +8 -0
  17. package/apps/local-cli/src/main.ts +16 -0
  18. package/apps/local-cli/src/server.ts +1 -0
  19. package/apps/local-cli/tests/config-loader.test.ts +14 -0
  20. package/apps/web/landing/index.html +14 -16
  21. package/apps/web/package.json +2 -2
  22. package/apps/web/scripts/build.mjs +23 -16
  23. package/apps/web/src/build-env.ts +1 -1
  24. package/apps/web/src/config-schema.ts +6 -6
  25. package/apps/web/tests/build-smoke.test.ts +14 -14
  26. package/apps/web/tests/config-schema.test.ts +1 -1
  27. package/docs/AWS-Deployment.md +21 -8
  28. package/docs/Cloudflare-Deployment-Guide.md +22 -6
  29. package/docs/PlanExtensions.md +113 -3
  30. package/docs/Release-Process.md +23 -13
  31. package/docs/Services.md +546 -0
  32. package/docs/WebClientGuide.md +32 -31
  33. package/package.json +2 -2
  34. package/packages/aws-adapter/package.json +1 -1
  35. package/packages/aws-adapter/src/aws-runtime.ts +5 -0
  36. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
  37. package/packages/aws-adapter/src/config-loader.ts +11 -0
  38. package/packages/aws-adapter/src/connection-counter.ts +89 -0
  39. package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
  40. package/packages/aws-adapter/src/handlers/connect.ts +55 -51
  41. package/packages/aws-adapter/src/handlers/default.ts +36 -4
  42. package/packages/aws-adapter/src/handlers/index.ts +15 -0
  43. package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
  44. package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
  45. package/packages/aws-adapter/src/index.ts +4 -0
  46. package/packages/aws-adapter/src/stats.ts +6 -1
  47. package/packages/aws-adapter/src/tables.ts +34 -4
  48. package/packages/aws-adapter/tests/aws-harness.ts +3 -0
  49. package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
  50. package/packages/aws-adapter/tests/connect.test.ts +158 -32
  51. package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
  52. package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
  53. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
  54. package/packages/aws-adapter/tests/handlers.test.ts +105 -3
  55. package/packages/aws-adapter/tests/tables.test.ts +6 -1
  56. package/packages/cf-adapter/package.json +1 -1
  57. package/packages/cf-adapter/src/config-loader.ts +11 -0
  58. package/packages/cf-adapter/src/connection-do.ts +112 -2
  59. package/packages/cf-adapter/src/d1-services-store.ts +703 -0
  60. package/packages/cf-adapter/src/env.ts +8 -0
  61. package/packages/cf-adapter/src/index.ts +5 -0
  62. package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
  63. package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
  64. package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
  65. package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
  66. package/packages/cf-adapter/tests/serialize.test.ts +1 -0
  67. package/packages/in-memory-runtime/package.json +1 -1
  68. package/packages/irc-core/package.json +1 -1
  69. package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
  70. package/packages/irc-core/src/commands/account-auth.ts +172 -0
  71. package/packages/irc-core/src/commands/chanserv.ts +882 -0
  72. package/packages/irc-core/src/commands/hostserv.ts +487 -0
  73. package/packages/irc-core/src/commands/index.ts +12 -0
  74. package/packages/irc-core/src/commands/join.ts +164 -8
  75. package/packages/irc-core/src/commands/markread.ts +202 -0
  76. package/packages/irc-core/src/commands/memoserv.ts +319 -0
  77. package/packages/irc-core/src/commands/mode.ts +96 -4
  78. package/packages/irc-core/src/commands/nickserv.ts +390 -0
  79. package/packages/irc-core/src/commands/oper.ts +18 -1
  80. package/packages/irc-core/src/commands/operserv.ts +346 -0
  81. package/packages/irc-core/src/commands/pre-away.ts +3 -1
  82. package/packages/irc-core/src/commands/privmsg.ts +42 -0
  83. package/packages/irc-core/src/commands/read-marker.ts +8 -8
  84. package/packages/irc-core/src/commands/registration.ts +61 -6
  85. package/packages/irc-core/src/commands/sasl.ts +18 -49
  86. package/packages/irc-core/src/commands/tagmsg.ts +41 -6
  87. package/packages/irc-core/src/commands/topic.ts +37 -0
  88. package/packages/irc-core/src/config.ts +36 -5
  89. package/packages/irc-core/src/effects.ts +56 -1
  90. package/packages/irc-core/src/ports.ts +1653 -84
  91. package/packages/irc-core/src/protocol/numerics.ts +8 -0
  92. package/packages/irc-core/src/state/channel.ts +21 -1
  93. package/packages/irc-core/src/state/connection.ts +25 -1
  94. package/packages/irc-core/src/types.ts +48 -12
  95. package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
  96. package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
  97. package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
  98. package/packages/irc-core/tests/commands/join.test.ts +393 -1
  99. package/packages/irc-core/tests/commands/markread.test.ts +361 -0
  100. package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
  101. package/packages/irc-core/tests/commands/mode.test.ts +381 -2
  102. package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
  103. package/packages/irc-core/tests/commands/oper.test.ts +13 -0
  104. package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
  105. package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
  106. package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
  107. package/packages/irc-core/tests/commands/registration.test.ts +788 -14
  108. package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
  109. package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
  110. package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
  111. package/packages/irc-core/tests/commands/topic.test.ts +94 -2
  112. package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
  113. package/packages/irc-core/tests/config.test.ts +49 -5
  114. package/packages/irc-core/tests/effects.test.ts +19 -0
  115. package/packages/irc-core/tests/message-store.test.ts +63 -0
  116. package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
  117. package/packages/irc-core/tests/services-store.test.ts +1289 -0
  118. package/packages/irc-core/tests/state/channel.test.ts +3 -0
  119. package/packages/irc-server/package.json +1 -1
  120. package/packages/irc-server/src/actor.ts +71 -16
  121. package/packages/irc-server/src/dispatch.ts +94 -7
  122. package/packages/irc-server/src/routing.ts +19 -0
  123. package/packages/irc-server/tests/actor.test.ts +623 -12
  124. package/packages/irc-server/tests/dispatch.test.ts +270 -2
  125. package/packages/irc-server/tests/routing.test.ts +6 -0
  126. package/packages/irc-test-support/package.json +1 -1
  127. package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
  128. package/packages/irc-test-support/src/index.ts +1 -0
  129. package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
  130. package/tools/ci-hardening/package.json +1 -1
  131. package/tools/load-test/package.json +1 -1
  132. package/tools/tcp-ws-forwarder/package.json +1 -1
  133. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
  134. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
  135. package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
package/README.md CHANGED
@@ -6,7 +6,7 @@ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
6
6
 
7
7
  One TypeScript codebase. Two serverless substrates.
8
8
 
9
- > **Status:** **v0.7.0 (preview).** The pure protocol core, the
9
+ > **Status:** **v0.8.0 (preview).** The pure protocol core, the
10
10
  > `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
11
11
  > the **Cloudflare Workers** adapter, and the **AWS** (API Gateway
12
12
  > WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
@@ -19,22 +19,33 @@ One TypeScript codebase. Two serverless substrates.
19
19
  > deferred IRC verbs (`KILL`, `REHASH`, `LUSERS` + `STATS`, `TRACE`,
20
20
  > `WALLOPS`, `SETNAME`) all land, and the S2S (`CONNECT` / `SQUIT` /
21
21
  > `LINKS`) and obsolete RFC 2812 (`SERVICE` / `SUMMON` / `USERS`) verbs
22
- > are formally dropped. v0.6.0 landed the **IRCv3 extension sweep
23
- > (Phase 12)** — ten caps (`account-notify`, `msgid`,
22
+ > are formally dropped. v0.6.0 landed the **IRCv3 extension sweep**
23
+ > — ten caps (`account-notify`, `msgid`,
24
24
  > `standard-replies`, `MONITOR`, `labeled-response`, `sts`,
25
25
  > `draft/typing`, `draft/multiline`, `draft/read-marker`,
26
26
  > `draft/pre-away`), the ISUPPORT tokens for the new caps, and the
27
27
  > read-only user mode `S` (TLS connected). **v0.7.0 lands the web
28
- > client (Phase 14)**: a vendored **Kiwi IRC** SPA served at `/app/`
28
+ > client**: a vendored **Kiwi IRC** SPA served at `/webclient/`
29
29
  > and a static landing page at `/` directly by the Cloudflare Worker,
30
30
  > with **Cross-Site WebSocket Hijacking (CSWSH) defense** wired into
31
31
  > the WS upgrade path (same-origin auto-derive by default, optional
32
32
  > `WEB_ORIGINS` allowlist for cross-origin deploys) — the browser
33
33
  > opens a native `wss://` straight to the existing IRC-over-WebSocket
34
- > endpoint. Remaining 0.x work: the client compatibility sweep
34
+ > endpoint. **v0.8.0 lands integrated IRC services** — NickServ
35
+ > (`REGISTER` / `IDENTIFY` / `DROP` / `INFO` / `SET ENFORCE` +
36
+ > nick enforcement), ChanServ (`REGISTER` / `DROP` / `SET` /
37
+ > `ACCESS` / `LEVELS` + the `+r` / `+R` / `+M` channel modes),
38
+ > HostServ (`ON` / `OFF` / `REQUEST` + oper `SET` / `APPROVE`),
39
+ > OperServ (`AKILL` / `JUPE` / `RAW`), and MemoServ
40
+ > (`SEND` / `LIST` / `READ` / `DEL`) — backed by a `ServicesStore`
41
+ > port with **persistent D1 (Cloudflare) and DynamoDB (AWS)
42
+ > backends** (write-behind, surviving redeploys). The SASL
43
+ > `AccountStore` and NickServ accounts are **unified** under one
44
+ > scrypt-hashed credential store, `PASS <nick>:<password>` identifies
45
+ > at registration, and `draft/read-marker` gains the timestamp-based
46
+ > `MARKREAD` verb. Remaining 0.x work: the client compatibility sweep
35
47
  > (WeeChat / HexChat / IRCCloud / TheLounge), the Playwright
36
- > browser-driven e2e for the SPA, IRC services (NickServ / ChanServ /
37
- > HostServ / OperServ / MemoServ), and driving the remaining
48
+ > browser-driven e2e for the SPA, and driving the remaining
38
49
  > sub-100% packages to full coverage. See `CHANGELOG.md` for the
39
50
  > per-release manifests.
40
51
 
@@ -43,9 +54,9 @@ One TypeScript codebase. Two serverless substrates.
43
54
  ## Why
44
55
 
45
56
  IRC servers have historically been long-running stateful processes. This
46
- project explores what happens when the protocol brain is a set of **pure
47
- reducers** and the only platform-specific code is the side-effect plumbing
48
- (transports, registries, fanout). The payoff:
57
+ project moves the protocol logic into a set of **pure reducers** so that
58
+ the only platform-specific code is the side-effect layer (transports,
59
+ registries, fanout). The consequences:
49
60
 
50
61
  - Every command handler is a trivial unit test: arrange state, apply a message,
51
62
  assert the new state and emitted effects.
@@ -57,7 +68,7 @@ reducers** and the only platform-specific code is the side-effect plumbing
57
68
 
58
69
  ## Architecture
59
70
 
60
- Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
71
+ Hexagonal / ports-and-adapters. The core implements the IRC protocol; adapters handle I/O.
61
72
 
62
73
  ```
63
74
  ┌─────────────────────────────────────────────────────────────────┐
@@ -97,7 +108,7 @@ function. Both feed the same `ConnectionActor` through a `Transport`
97
108
  seam (`WsTextFrameTransport` vs. `TcpByteStreamTransport`); the
98
109
  parser/reducer/dispatch pipeline is identical.
99
110
 
100
- ### The key idea: pure reducers + location-of-authority
111
+ ### Design: pure reducers + location-of-authority
101
112
 
102
113
  Each command handler is a pure function:
103
114
 
@@ -122,6 +133,7 @@ Each reducer runs in whichever authority **owns** the state it mutates:
122
133
  | PRIVMSG/NOTICE to channel | Channel entity | Fanout (reads roster) |
123
134
  | Nick collision check | Registry entity | Nick → Connection map |
124
135
  | NAMES / WHO / WHOIS | Reads snapshots | (no mutation) |
136
+ | Services (`PRIVMSG NickServ`/`ChanServ`/…) | Connection entity | `ServicesStore` (account/channel/vhost/memo state) |
125
137
 
126
138
  ---
127
139
 
@@ -130,7 +142,7 @@ Each reducer runs in whichever authority **owns** the state it mutates:
130
142
  ```
131
143
  ServerlessIRCd/
132
144
  ├── packages/
133
- │ ├── irc-core/ pure protocol + reducers (the brain)
145
+ │ ├── irc-core/ pure protocol + reducers
134
146
  │ ├── irc-server/ orchestration, IrcRuntime port, dispatch
135
147
  │ ├── in-memory-runtime/ reference runtime (used by tests + local CLI)
136
148
  │ ├── irc-test-support/ parametrized IRC scenario suite + harness seam
@@ -140,11 +152,11 @@ ServerlessIRCd/
140
152
  │ ├── cf-worker/ worker entry, DO migrations, bindings, [assets] + CSWSH gate
141
153
  │ ├── cf-tcp-container/ Spectrum + Container origin for irc+tls :6697
142
154
  │ ├── aws-stack/ CDK stack (APIGW WS + NLB + Lambda streaming + DynamoDB)
143
- │ ├── web/ vendored Kiwi IRC SPA (→ /app/) + static landing page (→ /)
155
+ │ ├── web/ vendored Kiwi IRC SPA (→ /webclient/) + static landing page (→ /)
144
156
  │ └── local-cli/ runnable WS + TCP server using in-memory-runtime
145
157
  ├── tools/
146
158
  │ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
147
- │ ├── load-test/ synthetic WebSocket IRC client farm (10k conns, p50/p95/p99, drop rate)
159
+ │ ├── load-test/ synthetic WebSocket IRC client pool (10k conns, p50/p95/p99, drop rate)
148
160
  │ ├── ci-hardening/ coverage-gate + mutation-config validators
149
161
  │ ├── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
150
162
  │ └── seed-cf-accounts.ts scrypt-hash SASL PLAIN accounts into Cloudflare D1
@@ -201,7 +213,7 @@ needs the upstream sources (`git submodule update --init apps/web/upstream`)
201
213
  and `yarn` (Kiwi ships a `yarn.lock`; `corepack enable` provides it):
202
214
 
203
215
  ```bash
204
- pnpm --filter web build # builds the Kiwi SPA into apps/web/dist/app/ (/app/)
216
+ pnpm --filter web build # builds the Kiwi SPA into apps/web/dist/webclient/ (/webclient/)
205
217
  ```
206
218
 
207
219
  Coverage reports are written to `packages/*/coverage/`. CI (`.github/workflows/ci.yml`)
@@ -223,7 +235,7 @@ that share one runtime, so a TCP client and a WS client can see each other
223
235
 
224
236
  - a **WebSocket** listener (the serverless transport) on `--port`, and
225
237
  - an **RFC-style TCP** listener on `--tcp-port` (default `<port> + 1`) so
226
- real IRC clients (WeeChat, HexChat, irssi, …) can dial in directly.
238
+ real IRC clients (WeeChat, HexChat, irssi, …) can connect directly.
227
239
 
228
240
  First build the workspace (the CLI runs from compiled `dist/`):
229
241
 
@@ -256,11 +268,55 @@ On startup it logs a JSON line like:
256
268
  | `--motd-file <p>` | built-in | Read MOTD lines from this file (one per line). |
257
269
  | `--server-name <h>`| `irc.localhost` | Server hostname advertised in `001`/`005`. Production MUST override. |
258
270
  | `--network-name <n>`| `LocalNet` | Network name advertised in `005 NETWORK=…`. |
271
+ | `--server-password <p>`| | Server-password gate. When set, every connection must supply `PASS <p>` (or SASL-identify) before `001`. Treat as a secret. |
259
272
  | `-h, --help` | | Show help and exit. |
260
273
 
261
274
  Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
262
275
  `SIGTERM` perform a graceful shutdown (closes active sockets, then exits).
263
276
 
277
+ ### Server password (`SERVER_PASSWORD` / `--server-password`)
278
+
279
+ All three adapters (Cloudflare Worker, AWS Lambda, local CLI) support an
280
+ optional **server-password gate**. When the knob is set, every connection
281
+ must supply the matching `PASS <value>` before `001 RPL_WELCOME` is
282
+ emitted; when unset or empty the gate is disabled (the default).
283
+
284
+ | Adapter | Knob |
285
+ |----------------|-------------------------------------------------------------------------------|
286
+ | Cloudflare | `wrangler secret put SERVER_PASSWORD` (Workers secret; never a `[vars]` entry). |
287
+ | AWS | `SERVER_PASSWORD` Lambda env var (sourced from Secrets Manager / SSM). |
288
+ | local CLI | `--server-password <p>` flag, or `serverPassword` on `StartServerOptions`. |
289
+
290
+ On a mismatched/missing `PASS` the server emits `464 ERR_PASSWDMISMATCH`
291
+ and disconnects with reason `Bad Password`. **SASL short-circuit:** a
292
+ connection that has authenticated an account via SASL
293
+ (`AUTHENTICATE PLAIN` / `EXTERNAL`) is exempt — the reducer's gate
294
+ treats `state.account !== undefined` as already authorised, so a
295
+ deployment with both `SERVER_PASSWORD` and configured SASL accounts
296
+ does not need to hand the shared password to identified users. The
297
+ server-wide gate is a shared deployment secret (never log it); per-user
298
+ credentials still go through the hashed SASL account store
299
+ (`HashedAccountStore` / D1 / DynamoDB), which is unaffected.
300
+
301
+ **PASS-based account login:** in addition to SASL, a client may identify
302
+ to its NickServ account at registration by sending
303
+ `PASS <nick>:<password>` (the same `<nick>:<password>` literal the
304
+ `SASL_ACCOUNTS` seed tooling uses). When an `AccountStore` is configured,
305
+ `NICK alice` + `PASS alice:hunter2` + `USER …` verifies the credentials
306
+ against the account store and, on success, logs the connection in as
307
+ `alice` — emitting `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS` (the same
308
+ "account is set" numerics SASL uses) before `001 RPL_WELCOME`, stamping
309
+ user mode `+r`, and running the same read-marker / away / memo replay as
310
+ a SASL login. The nick left of the `:` must match the `NICK` sent; a bare
311
+ `PASS <value>` with no colon is always treated as a server-password
312
+ candidate, never as account credentials. A wrong password or unknown nick
313
+ is indistinguishable (no `904`/`464` from the auth path) and the
314
+ connection simply proceeds un-identified when no server password is set.
315
+ This composes with the gate above by precedence: an already-identified
316
+ connection (SASL or PASS-auth) satisfies the gate; a `<nick>:<password>`
317
+ that fails verify never matches a bare shared secret, so a server with
318
+ both configured still rejects it with `464`.
319
+
264
320
  ### Connecting
265
321
 
266
322
  The WebSocket listener speaks WebSocket text frames (one IRC message per
@@ -283,9 +339,9 @@ given.
283
339
  The local CLI ships a built-in TCP listener, so no bridge is needed for local
284
340
  development. Deployed stacks (Cloudflare Workers and AWS API Gateway) are
285
341
  WebSocket-only, so a stock TCP IRC client (WeeChat, HexChat, irssi, …) cannot
286
- dial them directly. `tools/tcp-ws-forwarder` is a local bridge for that case:
342
+ connect to them directly. `tools/tcp-ws-forwarder` is a local bridge for that case:
287
343
  it listens on a TCP port and, for each connection, opens one WebSocket to a
288
- `ws://` / `wss://` target and pumps IRC lines both ways — reassembling the TCP
344
+ `ws://` / `wss://` target and forwards IRC lines both directions — reassembling the TCP
289
345
  byte stream into one message per WS frame outbound, and normalizing inbound
290
346
  frames back to canonical CRLF.
291
347
 
@@ -330,9 +386,9 @@ against any line-oriented WebSocket endpoint.
330
386
 
331
387
  ## Load testing (`tools/load-test`)
332
388
 
333
- A synthetic WebSocket IRC client farm (PLAN §7, Phase 7). It opens N
389
+ A synthetic WebSocket IRC client pool (PLAN §7). It opens N
334
390
  connections to a `ws://` / `wss://` target, registers each (NICK/USER),
335
- joins a channel, and optionally chats. Per-phase **p50 / p95 / p99
391
+ joins a channel, and optionally chats. Per-stage **p50 / p95 / p99
336
392
  latency** (connect / register / join / message) and the **drop rate**
337
393
  are captured, then printed as a markdown report to stdout (and written
338
394
  to `--report <path>` when given).
@@ -379,12 +435,12 @@ report).
379
435
 
380
436
  ## Web client (`apps/web`)
381
437
 
382
- A vendored **Kiwi IRC** SPA served at `/app/` and a static project
438
+ A vendored **Kiwi IRC** SPA served at `/webclient/` and a static project
383
439
  landing page served at `/`, both baked out of `apps/web/dist/` and
384
440
  served directly by the Cloudflare Worker via its `[assets]` binding.
385
- The browser opens a native `wss://` straight to the Worker's existing
441
+ The browser opens a native `wss://` directly to the Worker's
386
442
  IRC-over-WebSocket endpoint — no proxy, gateway, or transport
387
- adaptation in the hot path. See `docs/WebClientGuide.md` for the
443
+ adaptation in the request path. See `docs/WebClientGuide.md` for the
388
444
  end-to-end contributor/operator guide.
389
445
 
390
446
  Build the SPA + landing page (needs the submodule + `yarn`, provided by
@@ -394,8 +450,8 @@ Build the SPA + landing page (needs the submodule + `yarn`, provided by
394
450
  git submodule update --init apps/web/upstream # one-time per clone
395
451
  pnpm --filter web build:staging # or :prod / default
396
452
  # → apps/web/dist/index.html (landing page, served at /)
397
- # → apps/web/dist/app/index.html (Kiwi SPA, served at /app/)
398
- # → apps/web/dist/app/static/config.json (baked, env-specific)
453
+ # → apps/web/dist/webclient/index.html (Kiwi SPA, served at /webclient/)
454
+ # → apps/web/dist/webclient/static/config.json (baked, env-specific)
399
455
  ```
400
456
 
401
457
  Then run everything locally (Worker serves SPA + landing page + WS on
@@ -405,7 +461,7 @@ one origin):
405
461
  pnpm build # workspace packages
406
462
  pnpm --filter @serverless-ircd/cf-worker dev # http://localhost:8787
407
463
  # / → landing page
408
- # /app/ → Kiwi SPA (opens ws://localhost:8787/)
464
+ # /webclient/ → Kiwi SPA (opens ws://localhost:8787/)
409
465
  # /health → plaintext liveness
410
466
  ```
411
467
 
@@ -501,49 +557,49 @@ suite passes against every runtime, both adapters are deployed to staging,
501
557
  and ≥3 reference clients (WeeChat, HexChat, IRCCloud, TheLounge) connect
502
558
  cleanly. Per-release manifests live in `CHANGELOG.md`.
503
559
 
504
- - **Phase 0** — Foundation (monorepo, turbo, vitest, biome, CI). ✅
505
- - **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport,
560
+ - **Foundation** (monorepo, turbo, vitest, biome, CI). ✅
561
+ - **Pure protocol engine** reducers, IRCv3 caps, isupport,
506
562
  SASL (`PLAIN`), token-bucket flood control, chat history. ✅ (most in
507
563
  **v0.1.0**; SASL/flood/chathistory landed in **v0.2.0**).
508
- - **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
564
+ - **`IrcRuntime` port** — in-memory runtime, `ConnectionActor`,
509
565
  local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
510
- - **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
566
+ - **Cloudflare adapter** (ConnectionDO / ChannelDO / RegistryDO).
511
567
  ✅ landed in **v0.1.0** (staging deploy via CI; prod deploy is manual).
512
- - **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
568
+ - **AWS adapter** (APIGW WS + Lambda + DynamoDB + CDK).
513
569
  ✅ landed in **v0.2.0** (staging deploy via CI; prod deploy is manual).
514
- - **Phase 5** — Observability, security hardening, config, CI gates.
570
+ - **Observability, security hardening, config, CI gates.**
515
571
  ✅ landed across v0.2.0–v0.4.0 (logger port, server-password, cloaking,
516
572
  admission limits, coverage + mutation gates, OPER credential auth, the
517
573
  Cloudflare config loader, and AWS max-clients admission); the
518
574
  required-`serverName` gate closed in **v0.6.0**.
519
- - **Phase 6** — TLS hardening & raw TCP transport (`irc+tls :6697`).
575
+ - **TLS hardening & raw TCP transport** (`irc+tls :6697`).
520
576
  ✅ landed in **v0.4.0**: generalized `ConnectionActor` transport seam,
521
577
  Cloudflare Spectrum + Container origin, AWS NLB + Lambda streaming,
522
578
  mTLS → SASL `EXTERNAL`, transport-parametrized contract suite, ADR-009.
523
- - **Phase 7 — Load testing & compatibility sweep.** 🔄 partial as of
579
+ - **Load testing & compatibility sweep.** 🔄 partial as of
524
580
  **v0.6.0**: the `tools/load-test` 10k-connection harness is in-tree;
525
581
  the formal 10k-connection report per platform and the client
526
582
  compatibility matrix (WeeChat / HexChat / IRCCloud / TheLounge) are
527
583
  still pending.
528
- - **Phase 8 — PLAN-FIXES remediation.** ✅ landed across v0.3.0–v0.4.0
584
+ - **PLAN-FIXES remediation.** ✅ landed across v0.3.0–v0.4.0
529
585
  (MessageStore wired into all three adapters, AccountStore end-to-end,
530
586
  the DynamoDB `Accounts` table decided, CF enumeration RPCs, AWS QUIT
531
587
  fanout, `OPER` / `TAGMSG` / `WHOWAS`, RFC 1459 case-mapping, real
532
588
  MOTD, config-driven server identity, AWS max-clients admission,
533
589
  CF D1 SASL store, and a sweep of dead-code / stale-comment cleanup).
534
- - **Phase 9 — Runtime & tooling baseline.** ✅ landed in **v0.4.0**
590
+ - **Runtime & tooling baseline.** ✅ landed in **v0.4.0**
535
591
  (Node ≥ 24 / pnpm 11).
536
- - **Phase 10 — IRCv3 WebSocket extension.** ✅ landed in **v0.4.0**:
592
+ - **IRCv3 WebSocket extension.** ✅ landed in **v0.4.0**:
537
593
  `text.ircv3.net` / `binary.ircv3.net` subprotocol negotiation +
538
594
  per-message framing across the CF Worker, AWS APIGW, local CLI, and
539
595
  `tcp-ws-forwarder` (510-byte budget, `1009`/`1003` close codes,
540
596
  legacy fallback).
541
- - **Phase 11 — Deferred IRC verbs.** ✅ landed in **v0.5.0**: `KILL`,
597
+ - **Deferred IRC verbs.** ✅ landed in **v0.5.0**: `KILL`,
542
598
  `REHASH`, `LUSERS` + `STATS`, `TRACE`, `WALLOPS`, `SETNAME`, plus
543
599
  the formal drop of the S2S (`CONNECT`/`SQUIT`/`LINKS`) and obsolete
544
600
  RFC 2812 (`SERVICE`/`SUMMON`/`USERS`) verbs. The protocol surface is
545
601
  feature-complete for modern clients.
546
- - **Phase 12 — IRCv3 extension sweep.** ✅ landed in **v0.6.0**: ten
602
+ - **IRCv3 extension sweep.** ✅ landed in **v0.6.0**: ten
547
603
  negotiated caps (`account-notify`, `msgid`, `standard-replies`,
548
604
  `MONITOR`, `labeled-response`, `sts`, `draft/typing`,
549
605
  `draft/multiline`, `draft/read-marker`, `draft/pre-away`), the
@@ -552,13 +608,21 @@ cleanly. Per-release manifests live in `CHANGELOG.md`.
552
608
  (TLS connected), and the async push fanout seam (Registry hooks →
553
609
  `MONITOR` online/offline, `BroadcastWallops`-style caps-gated
554
610
  delivery).
555
- - **Phase 13 — IRC services.** pending: `ServicesStore` port +
556
- NickServ / ChanServ / HostServ / OperServ / MemoServ. Adapter
557
- backends (D1 / DynamoDB) ship alongside; until then
558
- `draft/read-marker` and `draft/pre-away` use in-memory reference
559
- stores and do not survive an adapter restart.
560
- - **Phase 14 — Web client.** 🔄 partial as of **v0.7.0**: a vendored
561
- Kiwi IRC SPA is served at `/app/` and a static landing page at `/`
611
+ - **IRC services.** landed in **v0.8.0**: `ServicesStore` port
612
+ + integrated NickServ (`REGISTER`/`IDENTIFY`/`DROP`/`INFO`/`SET
613
+ ENFORCE` + nick enforcement), ChanServ (`REGISTER`/`DROP`/`SET`/
614
+ `ACCESS`/`LEVELS` + `+r`/`+R`/`+M`), HostServ
615
+ (`ON`/`OFF`/`REQUEST` + oper `SET`/`APPROVE`), OperServ
616
+ (`AKILL`/`JUPE`/`RAW`), MemoServ (`SEND`/`LIST`/`READ`/`DEL`).
617
+ Persistent backends ship alongside **D1** on Cloudflare,
618
+ **DynamoDB** on AWS — both write-behind so registrations survive
619
+ redeploys. The SASL `AccountStore` and NickServ accounts are
620
+ **unified** under one scrypt-hashed credential store, `PASS
621
+ <nick>:<password>` identifies at registration, and `draft/read-marker`
622
+ / `draft/pre-away` now persist through the `ServicesStore` (no longer
623
+ in-memory-only). See `docs/Services.md`.
624
+ - **Web client.** 🔄 partial as of **v0.7.0**: a vendored
625
+ Kiwi IRC SPA is served at `/webclient/` and a static landing page at `/`
562
626
  by the Cloudflare Worker via an `[assets]` binding, with a Zod-
563
627
  validated per-env config matrix and **CSWSH defense** (same-origin
564
628
  auto-derive + optional `WEB_ORIGINS` allowlist). The Playwright
@@ -594,10 +658,34 @@ RFC 2812 verbs `SERVICE`/`SUMMON`/`USERS` are **formally dropped** as of
594
658
  v0.5.0 — they return `421 ERR_UNKNOWNCOMMAND`, and their reserved
595
659
  numerics have been removed. S2S linking is a PLAN non-goal.)
596
660
 
597
- **Channel modes:** `o v b i k l t n m s p`.
661
+ **Channel modes:** `o v b i k l t n m s p`, plus the
662
+ services-derived modes `r` (registered), `R` (block unidentified
663
+ join/message), `M` (moderated-identified) — settable only via ChanServ,
664
+ not via `MODE`.
598
665
  **User modes:** `i`, `o` (local only), `w`, `s`, and the read-only `S`
599
666
  (TLS connected — set by the transport at registration, surfaced in WHOIS
600
- via `276 RPL_WHOISSECURE`, not settable via `MODE`).
667
+ via `276 RPL_WHOISSECURE`, not settable via `MODE`) and the read-only
668
+ `r` (registered — set by NickServ `IDENTIFY` / SASL login, surfaced as
669
+ `+r`, not settable via `MODE`).
670
+
671
+ **Integrated IRC services** (v0.8.0): the service nicks `NickServ`,
672
+ `ChanServ`, `HostServ`, `OperServ`, and `MemoServ` are reserved and
673
+ routed to dedicated pure reducers when a `ServicesStore` is bound —
674
+ there is **no separate services process and no S2S link**. Reached via
675
+ `PRIVMSG <Service> :<subcommand>`:
676
+ NickServ (`REGISTER` / `IDENTIFY` / `DROP` / `INFO` / `SET ENFORCE` +
677
+ nick enforcement on the `NICK` path), ChanServ (`REGISTER` / `DROP` /
678
+ `SET` / `INFO` / `ACCESS` / `LEVELS` + auto-op on `JOIN`), HostServ
679
+ (`ON` / `OFF` / `REQUEST` + oper `SET` / `APPROVE` / `LIST`, with
680
+ `CHGHOST` fanout), OperServ (`AKILL` / `JUPE` / `UNJUPE` / `RAW`), and
681
+ MemoServ (`SEND` / `LIST` / `READ` / `DEL` with queue delivery at
682
+ identify). The SASL `AccountStore` and NickServ accounts share one
683
+ scrypt-hashed credential store, so a registered nick is also a SASL
684
+ login and vice versa. Backends: D1 on Cloudflare, DynamoDB on AWS,
685
+ in-memory for the local CLI / tests (all write-behind; registrations
686
+ survive redeploys). When no store is bound, services commands reply
687
+ `501` and the rest of the daemon is unaffected. See
688
+ `docs/Services.md` for the full reference.
601
689
 
602
690
  **IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
603
691
  `TAGMSG` command), `server-time`, `account-tag`, `account-notify`
@@ -614,7 +702,9 @@ shared between live and `draft/chathistory` replay), `standard-replies`
614
702
  so compliant clients upgrade from plaintext to TLS and pin the secure
615
703
  listener), `draft/chathistory`, `safelist`, `draft/typing` (typing-indicator
616
704
  broadcast), `draft/multiline=<n>` (multi-line `BATCH`, default 4096-byte
617
- budget), `draft/read-marker` (per-account persisted last-read), and
705
+ budget), `draft/read-marker` (per-account persisted last-read, with the
706
+ timestamp-based `MARKREAD <target> [timestamp]` verb plus the
707
+ `+draft/read-marker` tag fanout), and
618
708
  `draft/pre-away` (per-account persisted away reason, replayed at
619
709
  identify). Case-insensitive nick/channel comparison uses **RFC 1459**
620
710
  case-mapping (advertised via `005 CASEMAPPING=rfc1459`). The new
@@ -648,11 +738,21 @@ bridges a stock TCP client to a deployed WebSocket endpoint.
648
738
  required-`serverName` gate; `tools/load-test`; aws-adapter / aws-stack
649
739
  coverage unblocks; CF packages moved to istanbul; deploys now
650
740
  manual-only), and **v0.7.0** (the web client — vendored Kiwi IRC SPA
651
- at `/app/`, static landing page at `/`, Cloudflare Worker `[assets]`
652
- binding, per-env Zod-validated config matrix, and CSWSH defense via
653
- same-origin auto-derive + optional `WEB_ORIGINS`; `/health` becomes
654
- an explicit route; mutation-killing tests for `irc-core`'s parser /
655
- batch / numerics).
741
+ at `/webclient/`, static landing page at `/`, Cloudflare Worker
742
+ `[assets]` binding, per-env Zod-validated config matrix, and CSWSH
743
+ defense via same-origin auto-derive + optional `WEB_ORIGINS`;
744
+ `/health` becomes an explicit route; mutation-killing tests for
745
+ `irc-core`'s parser / batch / numerics), and **v0.8.0** (integrated
746
+ IRC services — NickServ / ChanServ / HostServ / OperServ / MemoServ
747
+ backed by a `ServicesStore` port with persistent D1 + DynamoDB
748
+ backends; unified SASL + NickServ scrypt-hashed account store;
749
+ `PASS <nick>:<password>` registration login; the `draft/read-marker`
750
+ `MARKREAD` verb).
751
+ - `docs/Services.md` — operator and contributor reference for the
752
+ integrated IRC services: architecture (integrated vs. pseudo-client),
753
+ the per-service command tables, the `+r`/`+R`/`+M` modes, nick
754
+ enforcement, adapter backends, and an end-to-end registration
755
+ walkthrough.
656
756
  - `docs/WebClientGuide.md` — end-to-end contributor/operator doc for the
657
757
  web client: build pipeline, per-env config matrix, CSWSH rationale and
658
758
  the optional `WEB_ORIGINS` var, local dev, optional Cloudflare Pages
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/aws-stack",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": true,
5
5
  "description": "AWS CDK v2 stack: API Gateway v2 WebSocket API, Lambda, DynamoDB tables, least-privilege IAM",
6
6
  "license": "BSD-3-Clause",