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.
- 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 +174 -18
- package/README.md +63 -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/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +20 -3
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +6 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +35 -2
- package/packages/aws-adapter/src/handlers/index.ts +69 -3
- 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 +6 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- 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 +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +154 -53
- 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 +2 -4
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +111 -46
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +33 -9
- package/packages/cf-adapter/src/index.ts +9 -8
- 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/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/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 +8 -1
- package/packages/cf-adapter/wrangler.test.toml +8 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +6 -1
- 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/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +9 -0
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- 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/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +58 -0
- 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 +1 -0
- package/packages/irc-core/src/ports.ts +181 -12
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +38 -1
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- 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/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- 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 +9 -35
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -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 +158 -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 +12 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +400 -3
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +4 -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 +73 -9
- 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 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -443
package/docs/PlanWebClient.md
DELETED
|
@@ -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.
|