serverless-ircd 0.3.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.
Files changed (159) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/deploy-aws.yml +1 -3
  3. package/.github/workflows/deploy-cf-tcp.yml +87 -0
  4. package/.github/workflows/deploy-cf.yml +1 -1
  5. package/.node-version +1 -0
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +174 -18
  8. package/README.md +63 -30
  9. package/apps/aws-stack/README.md +2 -2
  10. package/apps/aws-stack/bin/aws.ts +7 -0
  11. package/apps/aws-stack/package.json +4 -4
  12. package/apps/aws-stack/src/aws-stack.ts +118 -6
  13. package/apps/aws-stack/tests/stack.test.ts +98 -3
  14. package/apps/cf-tcp-container/Dockerfile +69 -0
  15. package/apps/cf-tcp-container/package.json +34 -0
  16. package/apps/cf-tcp-container/src/config-loader.ts +145 -0
  17. package/apps/cf-tcp-container/src/container-do.ts +38 -0
  18. package/apps/cf-tcp-container/src/container-server.ts +363 -0
  19. package/apps/cf-tcp-container/src/main.ts +77 -0
  20. package/apps/cf-tcp-container/src/persistence.ts +144 -0
  21. package/apps/cf-tcp-container/src/worker.ts +41 -0
  22. package/apps/cf-tcp-container/terraform/provider.tf +24 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
  25. package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
  26. package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
  27. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
  28. package/apps/cf-tcp-container/tsconfig.build.json +17 -0
  29. package/apps/cf-tcp-container/tsconfig.test.json +15 -0
  30. package/apps/cf-tcp-container/vitest.config.ts +26 -0
  31. package/apps/cf-tcp-container/wrangler.toml +63 -0
  32. package/apps/cf-worker/package.json +1 -1
  33. package/apps/cf-worker/wrangler.test.toml +6 -0
  34. package/apps/cf-worker/wrangler.toml +28 -2
  35. package/apps/local-cli/package.json +1 -1
  36. package/apps/local-cli/src/config-loader.ts +10 -0
  37. package/apps/local-cli/src/server.ts +20 -3
  38. package/package.json +14 -10
  39. package/packages/aws-adapter/package.json +3 -3
  40. package/packages/aws-adapter/src/account-store.ts +1 -1
  41. package/packages/aws-adapter/src/admission.ts +74 -0
  42. package/packages/aws-adapter/src/aws-runtime.ts +6 -4
  43. package/packages/aws-adapter/src/config-loader.ts +32 -0
  44. package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
  45. package/packages/aws-adapter/src/handlers/connect.ts +64 -8
  46. package/packages/aws-adapter/src/handlers/default.ts +35 -2
  47. package/packages/aws-adapter/src/handlers/index.ts +69 -3
  48. package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
  49. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  50. package/packages/aws-adapter/src/index.ts +6 -0
  51. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
  52. package/packages/aws-adapter/tests/account-store.test.ts +19 -20
  53. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  54. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  55. package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
  56. package/packages/aws-adapter/tests/connect.test.ts +78 -0
  57. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
  58. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  59. package/packages/aws-adapter/tests/handlers.test.ts +154 -53
  60. package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
  61. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  62. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  63. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  64. package/packages/cf-adapter/package.json +1 -1
  65. package/packages/cf-adapter/src/cf-runtime.ts +2 -4
  66. package/packages/cf-adapter/src/config-loader.ts +33 -0
  67. package/packages/cf-adapter/src/connection-do.ts +111 -46
  68. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  69. package/packages/cf-adapter/src/env.ts +33 -9
  70. package/packages/cf-adapter/src/index.ts +9 -8
  71. package/packages/cf-adapter/tests/cf-harness.ts +11 -1
  72. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
  73. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  74. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  75. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  76. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  77. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  78. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  79. package/packages/cf-adapter/tests/worker/main.ts +8 -1
  80. package/packages/cf-adapter/wrangler.test.toml +8 -0
  81. package/packages/in-memory-runtime/package.json +1 -1
  82. package/packages/irc-core/package.json +6 -1
  83. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  84. package/packages/irc-core/src/caps/capabilities.ts +23 -2
  85. package/packages/irc-core/src/cloak.ts +1 -1
  86. package/packages/irc-core/src/commands/cap.ts +8 -1
  87. package/packages/irc-core/src/commands/index.ts +9 -0
  88. package/packages/irc-core/src/commands/ison.ts +61 -0
  89. package/packages/irc-core/src/commands/quit.ts +12 -0
  90. package/packages/irc-core/src/commands/registration.ts +18 -12
  91. package/packages/irc-core/src/commands/sasl.ts +72 -9
  92. package/packages/irc-core/src/commands/server-info.ts +129 -0
  93. package/packages/irc-core/src/commands/userhost.ts +84 -0
  94. package/packages/irc-core/src/commands/whowas.ts +113 -0
  95. package/packages/irc-core/src/config.ts +58 -0
  96. package/packages/irc-core/src/credential-hashing.ts +124 -0
  97. package/packages/irc-core/src/effects.ts +6 -29
  98. package/packages/irc-core/src/index.ts +1 -0
  99. package/packages/irc-core/src/ports.ts +181 -12
  100. package/packages/irc-core/src/protocol/numerics.ts +14 -8
  101. package/packages/irc-core/src/types.ts +38 -1
  102. package/packages/irc-core/tests/account-store.test.ts +45 -2
  103. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  104. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  105. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  106. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  107. package/packages/irc-core/tests/commands/registration.test.ts +151 -6
  108. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  109. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  110. package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
  111. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  112. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  113. package/packages/irc-core/tests/config.test.ts +95 -1
  114. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  115. package/packages/irc-core/tests/effects.test.ts +0 -27
  116. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  117. package/packages/irc-core/tests/numerics.test.ts +12 -0
  118. package/packages/irc-core/tests/types.test.ts +35 -1
  119. package/packages/irc-core/tsconfig.build.json +1 -1
  120. package/packages/irc-core/tsconfig.test.json +1 -1
  121. package/packages/irc-server/package.json +1 -1
  122. package/packages/irc-server/src/actor.ts +158 -14
  123. package/packages/irc-server/src/dispatch.ts +0 -3
  124. package/packages/irc-server/src/index.ts +10 -2
  125. package/packages/irc-server/src/routing.ts +12 -0
  126. package/packages/irc-server/src/transport.ts +101 -0
  127. package/packages/irc-server/tests/actor.test.ts +400 -3
  128. package/packages/irc-server/tests/dispatch.test.ts +0 -17
  129. package/packages/irc-server/tests/routing.test.ts +4 -0
  130. package/packages/irc-server/tests/transport.test.ts +230 -0
  131. package/packages/irc-test-support/package.json +1 -1
  132. package/packages/irc-test-support/src/harness.ts +44 -9
  133. package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
  134. package/packages/irc-test-support/src/index.ts +3 -0
  135. package/packages/irc-test-support/src/scenarios.ts +132 -2
  136. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  137. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  138. package/pnpm-workspace.yaml +9 -1
  139. package/tools/ci-hardening/package.json +1 -1
  140. package/tools/package.json +4 -0
  141. package/tools/seed-aws-accounts.ts +5 -6
  142. package/tools/seed-cf-accounts.ts +104 -0
  143. package/tools/tcp-ws-forwarder/package.json +1 -1
  144. package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
  145. package/docs/ADR-002-location-of-authority.md +0 -82
  146. package/docs/ADR-003-durable-object-sharding.md +0 -93
  147. package/docs/ADR-004-dynamodb-schema.md +0 -96
  148. package/docs/ADR-005-wss-only-transport-v1.md +0 -83
  149. package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
  150. package/docs/ADR-007-deterministic-ports.md +0 -82
  151. package/docs/ADR-008-monorepo-tooling.md +0 -60
  152. package/docs/AWS-Adapter-Architecture.md +0 -496
  153. package/docs/AWS-Deployment.md +0 -1186
  154. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  155. package/docs/Home.md +0 -11
  156. package/docs/Observability.md +0 -87
  157. package/docs/PlanIRCv3Websocket.md +0 -489
  158. package/docs/PlanWebClient.md +0 -451
  159. package/docs/Release-Process.md +0 -443
@@ -1,451 +0,0 @@
1
- # ServerlessIRCd — Web Client Plan
2
-
3
- A static, CDN-served **Kiwi IRC** web frontend that lets a browser connect
4
- directly to a deployed ServerlessIRCd instance. The SPA opens a native
5
- `wss://` WebSocket to the Cloudflare Worker entry and speaks the IRC protocol
6
- one-message-per-text-frame — exactly what the server already emits today. No
7
- proxy in the hot path.
8
-
9
- > **Status:** planning. No code has landed. Tickets proposed in §7; once
10
- > ratified they will be appended to `tickets.md` / `progress.md` under a new
11
- > **Phase 8 — Web client** heading and supersede the corresponding sections
12
- > here.
13
-
14
- ---
15
-
16
- ## 1. Goals & non-goals
17
-
18
- **Goals**
19
- - A drop-in static frontend that boots Kiwi IRC, pre-configured for a given
20
- ServerlessIRCd deployment (staging, prod, self-host).
21
- - Browser talks **directly** to the existing WebSocket entry — no extra
22
- proxy, gateway, or transport adaptation in the v1 hot path.
23
- - Per-environment builds with reproducible config (no runtime secrets in the
24
- bundle).
25
- - Same TDD discipline as the rest of the monorepo: schema tests, build
26
- smoke, headless e2e against a deployed stack.
27
- - One-command `pnpm --filter web build:staging` produces a deployable
28
- `dist/` (or per-env variant).
29
-
30
- **Non-goals (v1 of the frontend)**
31
- - Server-side rendering / dynamic routing. Kiwi is a Vue SPA; SSR buys us
32
- nothing here.
33
- - Custom Kiwi plugins, themes, or heavy branding. Stock chrome is fine for
34
- v1; the upstream build is layered with our `static/config.json` only.
35
- - Server-side account / oper UI beyond what stock Kiwi ships. SASL PLAIN is
36
- already part of Kiwi's default flow.
37
- - A second transport (SockJS, Kiwi multi-server transport via
38
- `webircgateway`). Documented as an **optional** fallback only.
39
- - WEBIRC hostname injection at the gateway layer. ServerlessIRCd will do its
40
- own hostmask cloaking in TICKET-046; we don't need a WEBIRC proxy for it.
41
-
42
- **Design-for-later**
43
- - Plugin seam: a clean place under `apps/web/plugins/` (mirrors Kiwi's
44
- plugin convention) so a future ticket can add SASL UI helpers, chathistory
45
- controls (once TICKET-063 lands), etc.
46
- - Cloudflare Pages variant of the build output kept valid so prod can move
47
- off the Worker `[assets]` binding without rebuilding.
48
-
49
- ---
50
-
51
- ## 2. Architecture
52
-
53
- ```
54
- ┌────────────────────────────────────────────────────────────────────┐
55
- │ Browser │
56
- │ loads index.html + JS bundle (Kiwi IRC SPA) │
57
- │ fetches /static/config.json at boot │
58
- └────────────────────────────────────────────────────────────────────┘
59
- │ │
60
- │ HTTPS (static assets) │ wss (one IRC msg / frame)
61
- ▼ ▼
62
- ┌────────────────────────────────────────────────────────────────────┐
63
- │ apps/cf-worker (existing Cloudflare Worker) │
64
- │ fetch(): │
65
- │ • Upgrade: websocket → ConnectionDO (existing path) │
66
- │ + Origin allowlist check (NEW, TICKET-070) │
67
- │ • otherwise → [assets] binding serves dist/ │
68
- └────────────────────────────────────────────────────────────────────┘
69
-
70
-
71
- (ConnectionDO / RegistryDO / ChannelDO
72
- unchanged — see PLAN §6.1)
73
- ```
74
-
75
- The Worker is the single origin for both the SPA and the WebSocket. That
76
- makes CORS trivial (same-origin) and lets one domain serve the whole
77
- product. A Cloudflare Pages deployment of the same `dist/` is a documented
78
- alternative for prod if cost / separation of concerns ever favors it.
79
-
80
- **Why no `webircgateway` in the hot path?** ServerlessIRCd already speaks
81
- the wire format browser Kiwi expects: native WebSocket text frames, one IRC
82
- message per frame, with `\r\n`-joined-frame tolerance (PLAN §9). The
83
- gateway exists in-tree (`webircgateway/`) and ships as an **optional**
84
- sidecar for environments where raw wss is blocked by corporate proxies or
85
- mobile carriers; it provides SockJS + Kiwi multi-server transport fallback
86
- in those cases. v1 of the frontend does not require it.
87
-
88
- ---
89
-
90
- ## 3. Component placement
91
-
92
- ### 3.1 New workspace package: `apps/web`
93
-
94
- ```
95
- apps/web/
96
- ├── upstream/ Kiwi IRC source — git submodule, tag-pinned
97
- │ (mirrors existing `docs/` + `webircgateway/`
98
- │ submodule pattern)
99
- ├── static/
100
- │ ├── config.json Our environment-specific Kiwi config
101
- │ ├── config.staging.json Per-env override (build matrix)
102
- │ ├── config.prod.json
103
- │ └── assets/ Brand overrides (logo, favicon) — optional
104
- ├── src/
105
- │ ├── config-schema.ts Zod schema for config.json (the only TS we own)
106
- │ └── plugins/ Reserved for future Kiwi plugins (empty in v1)
107
- ├── scripts/
108
- │ └── build.mjs Orchestrates upstream build + layers our config
109
- ├── tests/
110
- │ ├── config-schema.test.ts Zod validation, red/green per field
111
- │ └── build-smoke.test.ts Asserts dist/ shape post-build
112
- ├── package.json pnpm workspace package (@serverless-ircd/web)
113
- ├── tsconfig.json extends tsconfig.base.json
114
- └── README.md per-package developer notes
115
- ```
116
-
117
- The package owns **no IRC logic** — it's purely a build/config layer over
118
- the upstream Kiwi SPA. The only TypeScript we ship is the Zod config schema
119
- and any future plugin shims.
120
-
121
- ### 3.2 Touchpoints with existing code
122
-
123
- | File | Change |
124
- |---|---|
125
- | `apps/cf-worker/src/worker.ts:44-58` | Non-WS branch falls through to assets binding; WS branch gains `Origin` check. |
126
- | `apps/cf-worker/wrangler.toml` | Add `[assets]` block pointing at `../../apps/web/dist`; add `WEB_ORIGINS` var per env. |
127
- | `pnpm-workspace.yaml` | Already globs `apps/*` — `apps/web` is picked up automatically. No change. |
128
- | `turbo.json` | Add `apps/web#build` to the pipeline with a `dependsOn: []` (no internal workspace deps). |
129
- | `.gitmodules` | Append `apps/web/upstream` entry pinning the Kiwi release tag. |
130
- | `packages/irc-core/...` | None. The frontend consumes zero `@serverless-ircd/*` packages at runtime. |
131
-
132
- ---
133
-
134
- ## 4. Decisions to confirm
135
-
136
- Each row is a proposed default. Override before TICKET-066 lands.
137
-
138
- | # | Decision | Proposed default | Alternative |
139
- |---|---|---|---|
140
- | 4.1 | Static hosting | Worker `[assets]` binding (single-origin staging) | Cloudflare Pages; separate static host |
141
- | 4.2 | Kiwi distribution | Git submodule at `apps/web/upstream/`, tag-pinned | Fork in-tree; npm package (Kiwi isn't published for this) |
142
- | 4.3 | Config injection | `static/config.json` checked into repo; build matrix emits `dist-staging/` / `dist-prod/` | Build-time env vars only; runtime fetch from `/config` |
143
- | 4.4 | Origin allowlist | New `WEB_ORIGINS` Worker var; WS `Upgrade` rejected with 403 if `Origin` not listed | Trust same-origin only (breaks Pages variant); no check (CSWSH risk) |
144
- | 4.5 | webircgateway | Out of hot path; documented as optional SockJS fallback | Run as sidecar in v1 (only needed for proxy resilience) |
145
- | 4.6 | Branding | Stock Kiwi chrome with a `static/assets/logo.svg` swap | Full theme fork |
146
- | 4.7 | Default channels | One `#welcome` channel auto-joined on first connect | Empty; let the user type channels |
147
-
148
- ### 4.1 Hosting — Worker assets vs Pages
149
-
150
- - **Worker `[assets]` binding (recommended for staging).** Single domain
151
- serves SPA + WS. Simplest CORS story, simplest deploy story, one
152
- wrangler command. Adds zero runtime cost (assets are served by the
153
- Workers runtime, not by JS).
154
- - **Cloudflare Pages (alternative for prod).** Decouples static deploys
155
- from Worker deploys — useful if frontend changes more often than the
156
- IRCd. Requires the Worker to allow the Pages origin in `WEB_ORIGINS`.
157
-
158
- The build output is identical for both; the choice is a deploy-time knob,
159
- not a build-time one.
160
-
161
- ### 4.4 Origin allowlist — why it's mandatory
162
-
163
- WebSocket upgrades do **not** follow the same-origin policy. An attacker
164
- page can `new WebSocket('wss://irc.example.com/')` and, if the user has
165
- previously registered a nick in another tab, race the connection into
166
- issuing commands on their behalf (CSWSH — cross-site WebSocket
167
- hijacking). The Worker MUST reject upgrades whose `Origin` header is not
168
- in a configured allowlist. This is a defense-in-depth item on top of any
169
- session/nick ownership semantics the IRCd grows later.
170
-
171
- ---
172
-
173
- ## 5. Kiwi IRC configuration surface
174
-
175
- Kiwi reads a JSON config either from `window.kiwiConfig` or from
176
- `/static/config.json` fetched at boot (we use the latter — it lets us
177
- swap config per env without rebuilding the JS bundle). The schema we own
178
- mirrors the subset of Kiwi's documented config keys that we care about;
179
- unknown keys are passed through unchanged so the upstream's evolution
180
- doesn't break our build.
181
-
182
- ```jsonc
183
- // static/config.staging.json (example)
184
- {
185
- "server": "irc-staging.example.com",
186
- "port": 443,
187
- "tls": true,
188
- "path": "/", // WS path on the Worker
189
- "nick": "kiwi-?", // ? = random suffix
190
- "channels": ["#welcome"],
191
- "brand": "ServerlessIRCd (staging)",
192
- "windowTitle": "ServerlessIRCd staging",
193
- "showSendButton": true,
194
- "theme": "Default",
195
- "plugins": []
196
- }
197
- ```
198
-
199
- Zod schema (`apps/web/src/config-schema.ts`):
200
-
201
- ```ts
202
- export const KiwiConfig = z.object({
203
- server: z.string().min(1),
204
- port: z.number().int().min(1).max(65535),
205
- tls: z.boolean(),
206
- path: z.string().startsWith('/').default('/'),
207
- nick: z.string().min(1),
208
- channels: z.array(z.string().startsWith('#')).default([]),
209
- brand: z.string().default('ServerlessIRCd'),
210
- windowTitle: z.string().default('ServerlessIRCd'),
211
- }).passthrough(); // tolerate upstream keys we don't model
212
- ```
213
-
214
- Build matrix:
215
-
216
- | `pnpm ...` command | Reads | Writes |
217
- |---|---|---|
218
- | `--filter web build` | `static/config.json` | `dist/` |
219
- | `--filter web build:staging` | `static/config.staging.json` | `dist/` (CI deploys this) |
220
- | `--filter web build:prod` | `static/config.prod.json` | `dist/` |
221
-
222
- A malformed config fails the build with a readable Zod error. No silent
223
- fall-through.
224
-
225
- ---
226
-
227
- ## 6. Testing strategy
228
-
229
- Strict TDD per the project rules. The frontend is mostly assembled rather
230
- than authored, so the testable surface is narrow but real:
231
-
232
- | Layer | Tooling | What it asserts |
233
- |---|---|---|
234
- | Config schema | vitest + Zod | Malformed `config.json` fails fast; well-formed config round-trips. |
235
- | Build smoke | vitest on `dist/` | `dist/index.html` + `dist/static/config.json` exist post-build; configured `server` matches the requested env. |
236
- | Origin check | vitest (unit on `apps/cf-worker`) | Disallowed `Origin` → 403; allowed → upgrade proceeds. |
237
- | E2E | Playwright headless vs staging | SPA boots, Kiwi reaches `001`/`376`, joins `#welcome`, round-trips a `PRIVMSG`. |
238
-
239
- E2E reuses the scenario vocabulary already in
240
- `apps/cf-worker/scripts/smoke.mjs` so the WS smoke and the browser smoke
241
- share fixture data.
242
-
243
- **Determinism:** the Zod schema, build smoke, and Origin check tests are
244
- all pure. The Playwright e2e is gated behind a `--e2e` flag (off by
245
- default in `pnpm test`, on in CI's nightly job) so it can't make the unit
246
- suite flaky.
247
-
248
- ---
249
-
250
- ## 7. Proposed tickets
251
-
252
- Seven new tickets under a new **Phase 8 — Web client** heading in
253
- `tickets.md` / `progress.md`. Each follows the existing TDD outline shape
254
- (Red → Green → Refactor, acceptance criteria, dependencies).
255
-
256
- ### TICKET-066 — `apps/web` workspace skeleton
257
- - **Phase:** 8
258
- - **Points:** 2
259
- - **Dependencies:** —
260
- - **Description:** Create the `apps/web` pnpm workspace package with an
261
- empty `index.html`, `package.json`, `tsconfig.json` extending
262
- `tsconfig.base.json`, and a vitest config. Add the `build` task to
263
- `turbo.json`. No Kiwi source yet — just the scaffolding so subsequent
264
- tickets have a home.
265
- - **Acceptance Criteria:**
266
- - `pnpm install` recognizes `@serverless-ircd/web`.
267
- - `pnpm --filter web test` runs (no tests yet, exits 0).
268
- - `pnpm --filter web typecheck` passes against `tsconfig.base.json`.
269
- - Biome lints the new files without errors.
270
- - **TDD Outline:** Red on a vitest asserting `apps/web/package.json`
271
- `name === '@serverless-ircd/web'`; green by adding the package.
272
-
273
- ### TICKET-067 — Vendor Kiwi IRC as git submodule; build pipeline
274
- - **Phase:** 8
275
- - **Points:** 3
276
- - **Dependencies:** TICKET-066
277
- - **Description:** Add Kiwi IRC source as a git submodule at
278
- `apps/web/upstream/` pinned to a release tag. `scripts/build.mjs` runs
279
- Kiwi's upstream build (`yarn && yarn build` inside `upstream/`), then
280
- copies `upstream/dist/` into `apps/web/dist/` and layers our
281
- `static/config.json` over the upstream default. `pnpm --filter web
282
- build` produces a working SPA bundle.
283
- - **Acceptance Criteria:**
284
- - `pnpm --filter web build` from a clean checkout produces
285
- `apps/web/dist/index.html`, `apps/web/dist/static/config.json`, and
286
- the JS bundle.
287
- - The submodule is pinned to a specific upstream tag (not `main`).
288
- - `.gitmodules` entry added; `git submodule update --init` is in the
289
- contributor quickstart.
290
- - **TDD Outline:** Red on a vitest build-smoke test asserting
291
- `dist/index.html` exists post-build; green by wiring `build.mjs`.
292
-
293
- ### TICKET-068 — `static/config.json` Zod schema + per-env build matrix
294
- - **Phase:** 8
295
- - **Points:** 2
296
- - **Dependencies:** TICKET-067
297
- - **Description:** Ship `src/config-schema.ts` (Zod, see §5) with
298
- `static/config.json`, `static/config.staging.json`, and
299
- `static/config.prod.json`. `build.mjs` accepts a `--env` flag selecting
300
- which config to bake into `dist/`. Default config targets the staging
301
- Worker hostname.
302
- - **Acceptance Criteria:**
303
- - Malformed config (missing `server`, out-of-range `port`, non-`#`
304
- channel) fails the build with a readable Zod error.
305
- - `pnpm --filter web build:staging` bakes `config.staging.json`;
306
- `:prod` bakes `config.prod.json`.
307
- - A vitest assertion confirms the configured `server` matches the
308
- requested env (catches cross-env bake mistakes in CI).
309
- - **TDD Outline:** Red on Zod rejecting `{}`; green. Red on a config
310
- with `port: 99999`; green. Red on `channels: ["foo"]`; green.
311
-
312
- ### TICKET-069 — Serve static assets from `apps/cf-worker` via `[assets]`
313
- - **Phase:** 8
314
- - **Points:** 2
315
- - **Dependencies:** TICKET-067
316
- - **Description:** Add a Wrangler `[assets]` binding to
317
- `apps/cf-worker/wrangler.toml` pointing at `../../apps/web/dist`. The
318
- non-WS branch of `worker.ts` `fetch()` falls through to assets (the
319
- binding handles this automatically; the existing plaintext health
320
- response moves to a `/health` path). Per-env (`[env.staging.assets]`)
321
- blocks for staging.
322
- - **Acceptance Criteria:**
323
- - `wrangler dev` serves `dist/index.html` at `/` and the JS bundle at
324
- its hashed asset path.
325
- - `/health` returns the previous plaintext response.
326
- - WS upgrade still routes to `ConnectionDO`.
327
- - Staging deploys both Worker and assets in one `wrangler deploy
328
- --env staging`.
329
- - **TDD Outline:** Red on a `vitest-pool-workers` test asserting
330
- `GET /` returns the SPA HTML; green by adding the binding. Red on
331
- `GET /health` returning the plaintext string; green.
332
-
333
- ### TICKET-070 — WS Origin allowlist in Worker (CSWSH defense)
334
- - **Phase:** 8
335
- - **Points:** 3
336
- - **Dependencies:** TICKET-069
337
- - **Description:** Worker validates the `Origin` header on every WS
338
- upgrade against a config-driven `WEB_ORIGINS` var (comma-separated,
339
- per-env). Disallowed origins get `403 Forbidden`. Documentation in
340
- `docs/web-client.md` makes clear this is mandatory, not optional.
341
- - **Acceptance Criteria:**
342
- - Upgrade with `Origin` in `WEB_ORIGINS` → proceeds (existing path).
343
- - Upgrade with `Origin` not in list → `403`.
344
- - Upgrade with missing `Origin` (non-browser client — curl, the
345
- `tcp-ws-forwarder`, WeeChat) → proceeds (preserves existing
346
- non-browser tooling).
347
- - Empty/missing `WEB_ORIGINS` var → fail-closed (build error on
348
- `wrangler deploy`) so a deploy can never silently disable the check.
349
- - **TDD Outline:** Red on disallowed Origin receiving 403 (currently
350
- proceeds); green by adding the check. Red on missing Origin with
351
- empty `WEB_ORIGINS` proceeding; green by failing closed.
352
-
353
- ### TICKET-071 — E2E: headless browser vs deployed staging frontend
354
- - **Phase:** 8
355
- - **Points:** 5
356
- - **Dependencies:** TICKET-068, TICKET-069, TICKET-070
357
- - **Description:** Playwright e2e in `apps/web/tests/e2e/` that opens
358
- the deployed staging SPA, asserts Kiwi reaches registered state
359
- (`001`/`376`), joins `#welcome`, sends a `PRIVMSG`, and sees the echo
360
- on its own buffer. Reuses the scenario fixture data already living in
361
- `apps/cf-worker/scripts/smoke.mjs`. Gated behind `--e2e`; wired into
362
- the nightly CI job, not the per-push suite.
363
- - **Acceptance Criteria:**
364
- - Single test passes against the staging Worker + frontend.
365
- - The test fails clearly when the SPA fails to connect (timeout on
366
- registration).
367
- - Adding a new scenario is a one-file append (data-driven).
368
- - **TDD Outline:** Red on a stub test asserting `document.title`
369
- matches the configured `windowTitle`; green once the SPA loads from
370
- staging. Then incrementally add the register/join/privmsg assertions.
371
-
372
- ### TICKET-072 — Docs: `docs/web-client.md`
373
- - **Phase:** 8
374
- - **Points:** 1
375
- - **Dependencies:** TICKET-071
376
- - **Description:** End-to-end contributor/operator doc covering: how to
377
- bootstrap the submodule, build per-env, serve locally, deploy via
378
- wrangler; the CSWSH rationale and the mandatory `WEB_ORIGINS` var;
379
- when and how to deploy the optional `webircgateway` sidecar for SockJS
380
- fallback; how to swap to Cloudflare Pages if prod favors it.
381
- - **Acceptance Criteria:**
382
- - A new contributor can `git submodule update --init`, `pnpm install`,
383
- `pnpm --filter web build:staging`, and `wrangler deploy --env
384
- staging` following only this doc.
385
- - `WEB_ORIGINS` is called out as mandatory, with examples.
386
- - `webircgateway` is clearly labeled optional with the triggering
387
- use case (proxy-blocked wss).
388
- - **TDD Outline:** N/A (documentation). Reviewed by following the steps
389
- from scratch in a clean checkout.
390
-
391
- ---
392
-
393
- ## 8. Risks
394
-
395
- | Risk | Mitigation |
396
- |---|---|
397
- | **CSWSH** — attacker page opens WS to IRCd riding a user's session | Origin allowlist (TICKET-070); WS does not follow SOP. |
398
- | Bundle staleness in submodule (Kiwi release drift) | Pin a tag, not `main`; weekly CI check-for-updates job (low priority, follow-up). |
399
- | Corporate-proxy networks blocking raw wss | Document `webircgateway` SockJS fallback (already vendored); not v1-blocking. |
400
- | Dev/prod config leakage (internal hostnames baked into a prod bundle) | Per-env build matrix; prod build only reads `config.prod.json`; CI asserts `server` matches env. |
401
- | Kiwi assumes IRC server reachable at `/` on plain ws/wss | Worker already accepts WS upgrade at `/`; no Kiwi custom-transport plugin required. |
402
- | Worker `[assets]` size cap / deploy time | Kiwi bundle is a few hundred KB gzipped; well under Wrangler's assets-directory cap. Monitor as plugins land. |
403
- | Submodule init forgotten on fresh clone | `docs/web-client.md` quickstart + a `pnpm postinstall` hook that warns if `apps/web/upstream/` is empty. |
404
-
405
- ---
406
-
407
- ## 9. Out of scope for v1 of the frontend
408
-
409
- - Custom Kiwi plugins (SASL UI helpers, chathistory controls once
410
- TICKET-063 lands, etc.). The `src/plugins/` directory is reserved.
411
- - Custom themes / heavy branding. Stock Kiwi chrome with a logo swap.
412
- - Server-side rendering or any server-side runtime.
413
- - A second transport (SockJS / Kiwi multi-server transport via
414
- `webircgateway`). Documented as optional only.
415
- - Account / oper login UI beyond stock Kiwi.
416
- - Internationalization overrides beyond what Kiwi ships by default.
417
- - Mobile-specific build variants (PWA manifest, etc.). The SPA is
418
- responsive out of the box; deferred to a follow-up if measured
419
- mobile usage justifies it.
420
-
421
- ---
422
-
423
- ## 10. Deliverables checklist (definition of done for v1 web client)
424
-
425
- - [ ] `apps/web` workspace builds under turbo with no internal workspace
426
- runtime deps.
427
- - [ ] `pnpm --filter web build:staging` produces a `dist/` that, served
428
- by the staging Worker, reaches registered state in a real browser.
429
- - [ ] Worker `[assets]` binding serves the SPA in staging; `/health`
430
- preserves the plaintext response.
431
- - [ ] WS `Origin` allowlist enforced; missing `WEB_ORIGINS` fails the
432
- deploy.
433
- - [ ] Playwright e2e green against staging on the nightly CI job.
434
- - [ ] `docs/web-client.md` walkable from a clean checkout.
435
- - [ ] All new tickets green in `progress.md`; PLAN §3 / README repo
436
- layout updated to mention `apps/web`.
437
-
438
- ---
439
-
440
- ## 11. Open questions
441
-
442
- - Should the frontend live on the same domain as the Worker (current
443
- proposal) or a subdomain (`web.example.com` vs `irc.example.com`)?
444
- Same-domain is simpler; subdomain is more conventional for IRC
445
- networks. Decide before TICKET-069.
446
- - Do we want a `Makefile`-style convenience target (`pnpm web:dev`) that
447
- boots both `wrangler dev` and a vite preview of the SPA in parallel,
448
- or keep them as separate commands? Lean toward convenience target.
449
- - Should the Origin allowlist support wildcards (`*.example.com`) or
450
- exact-match only? Lean toward exact-match for v1; revisit if multi-
451
- tenant deployment ever demands it.