serverless-ircd 0.9.0 → 0.10.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 (134) hide show
  1. package/.github/workflows/deploy-aws.yml +156 -32
  2. package/.github/workflows/deploy-cf-tcp.yml +11 -9
  3. package/.github/workflows/deploy-cf.yml +14 -14
  4. package/CHANGELOG.md +305 -0
  5. package/README.md +134 -41
  6. package/apps/aws-stack/README.md +3 -5
  7. package/apps/aws-stack/bin/aws.ts +82 -9
  8. package/apps/aws-stack/cdk.json +0 -3
  9. package/apps/aws-stack/package.json +3 -4
  10. package/apps/aws-stack/src/aws-stack.ts +177 -52
  11. package/apps/aws-stack/src/static-site.ts +323 -0
  12. package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
  13. package/apps/aws-stack/tests/stack.test.ts +267 -92
  14. package/apps/aws-stack/tests/static-site.test.ts +491 -0
  15. package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
  16. package/apps/cf-tcp-container/package.json +2 -3
  17. package/apps/cf-tcp-container/src/container-server.ts +12 -9
  18. package/apps/cf-tcp-container/wrangler.toml +1 -10
  19. package/apps/cf-worker/package.json +3 -4
  20. package/apps/cf-worker/wrangler.toml +12 -71
  21. package/apps/local-cli/package.json +1 -1
  22. package/apps/local-cli/src/server.ts +21 -17
  23. package/apps/web/landing/favicon.ico +0 -0
  24. package/apps/web/landing/index.html +1 -0
  25. package/apps/web/package.json +2 -2
  26. package/apps/web/scripts/build.mjs +66 -4
  27. package/apps/web/src/build-env.ts +125 -4
  28. package/apps/web/src/config-schema.ts +20 -6
  29. package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
  30. package/apps/web/tests/build-env.test.ts +210 -9
  31. package/apps/web/tests/build-smoke.test.ts +2 -2
  32. package/apps/web/tests/config-schema.test.ts +149 -25
  33. package/docs/AWS-Deployment.md +670 -96
  34. package/docs/AWS-TCP-Deployment.md +20 -45
  35. package/docs/Cloudflare-Deployment-Guide.md +87 -113
  36. package/docs/Cloudflare-TCP-Deployment.md +25 -49
  37. package/docs/Release-Process.md +27 -23
  38. package/docs/Services.md +69 -22
  39. package/docs/WebClientGuide.md +35 -26
  40. package/package.json +7 -10
  41. package/packages/aws-adapter/package.json +1 -1
  42. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
  43. package/packages/aws-adapter/src/config-loader.ts +19 -2
  44. package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
  45. package/packages/aws-adapter/src/handlers/connect.ts +26 -0
  46. package/packages/aws-adapter/src/handlers/default.ts +190 -123
  47. package/packages/aws-adapter/src/handlers/index.ts +67 -23
  48. package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
  49. package/packages/aws-adapter/src/index.ts +5 -7
  50. package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
  51. package/packages/aws-adapter/src/serialize.ts +15 -0
  52. package/packages/aws-adapter/src/tables.ts +2 -12
  53. package/packages/aws-adapter/tests/aws-harness.ts +0 -1
  54. package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
  55. package/packages/aws-adapter/tests/connect.test.ts +124 -1
  56. package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
  57. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
  58. package/packages/aws-adapter/tests/handlers.test.ts +117 -11
  59. package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
  60. package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
  61. package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
  62. package/packages/aws-adapter/tests/stats.test.ts +0 -3
  63. package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
  64. package/packages/aws-adapter/tests/tables.test.ts +1 -8
  65. package/packages/aws-adapter/tests/transactions.test.ts +0 -1
  66. package/packages/cf-adapter/package.json +1 -5
  67. package/packages/cf-adapter/src/cf-runtime.ts +59 -8
  68. package/packages/cf-adapter/src/channel-do.ts +13 -3
  69. package/packages/cf-adapter/src/connection-do.ts +266 -109
  70. package/packages/cf-adapter/src/d1-services-store.ts +63 -26
  71. package/packages/cf-adapter/src/env.ts +11 -10
  72. package/packages/cf-adapter/src/index.ts +0 -6
  73. package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
  74. package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
  75. package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
  76. package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
  77. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
  78. package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
  79. package/packages/cf-adapter/tests/serialize.test.ts +25 -0
  80. package/packages/in-memory-runtime/package.json +1 -1
  81. package/packages/irc-core/package.json +1 -1
  82. package/packages/irc-core/src/account-migration.ts +140 -0
  83. package/packages/irc-core/src/commands/account-auth.ts +16 -19
  84. package/packages/irc-core/src/commands/memoserv.ts +1 -1
  85. package/packages/irc-core/src/commands/nickserv.ts +122 -11
  86. package/packages/irc-core/src/commands/registration.ts +2 -2
  87. package/packages/irc-core/src/commands/sasl.ts +22 -31
  88. package/packages/irc-core/src/config.ts +35 -9
  89. package/packages/irc-core/src/credential-hashing.ts +11 -54
  90. package/packages/irc-core/src/index.ts +1 -0
  91. package/packages/irc-core/src/ports.ts +159 -179
  92. package/packages/irc-core/src/types.ts +38 -10
  93. package/packages/irc-core/tests/account-migration.test.ts +133 -0
  94. package/packages/irc-core/tests/commands/markread.test.ts +54 -0
  95. package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
  96. package/packages/irc-core/tests/commands/nickserv.test.ts +237 -1
  97. package/packages/irc-core/tests/commands/oper.test.ts +15 -0
  98. package/packages/irc-core/tests/commands/registration.test.ts +140 -133
  99. package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
  100. package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
  101. package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
  102. package/packages/irc-core/tests/message-store.test.ts +5 -0
  103. package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
  104. package/packages/irc-core/tests/ports.test.ts +71 -0
  105. package/packages/irc-core/tests/services-store.test.ts +204 -0
  106. package/packages/irc-core/vitest.config.ts +6 -1
  107. package/packages/irc-server/package.json +1 -1
  108. package/packages/irc-server/src/actor.ts +0 -14
  109. package/packages/irc-server/tests/actor.test.ts +19 -47
  110. package/packages/irc-test-support/package.json +1 -1
  111. package/pnpm-workspace.yaml +1 -0
  112. package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
  113. package/scripts/deploy-web-aws.mjs +290 -0
  114. package/scripts/package.json +23 -0
  115. package/scripts/tsconfig.test.json +12 -0
  116. package/scripts/vitest.config.ts +19 -0
  117. package/tools/ci-hardening/package.json +1 -1
  118. package/tools/ci-hardening/src/index.ts +2 -0
  119. package/tools/ci-hardening/src/validate.ts +57 -0
  120. package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
  121. package/tools/ci-hardening/tests/validate.test.ts +42 -0
  122. package/tools/load-test/package.json +1 -1
  123. package/tools/migrate-accounts-to-services.ts +270 -0
  124. package/tools/package.json +2 -1
  125. package/tools/seed-aws-accounts.ts +35 -10
  126. package/tools/seed-cf-accounts.ts +42 -9
  127. package/tools/tcp-ws-forwarder/package.json +1 -1
  128. package/packages/aws-adapter/src/account-store.ts +0 -121
  129. package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
  130. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
  131. package/packages/aws-adapter/tests/account-store.test.ts +0 -276
  132. package/packages/cf-adapter/src/d1-account-store.ts +0 -198
  133. package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
  134. package/packages/irc-core/tests/account-store.test.ts +0 -131
@@ -12,7 +12,7 @@ the two are independent and a deployment can enable either or both.
12
12
  > This doc covers the *how*.
13
13
 
14
14
  **Acceptance criterion (TICKET-058):** a new contributor can deploy
15
- their own TCP+TLS staging instance following only this doc.
15
+ their own TCP+TLS instance following only this doc.
16
16
 
17
17
  Cross-reference: `apps/cf-tcp-container/` (origin),
18
18
  `apps/cf-tcp-container/wrangler.toml` (container config),
@@ -77,7 +77,7 @@ Two artifacts are deployed and configured:
77
77
  | **Cloudflare Enterprise**| **Spectrum is an Enterprise-tier feature.** Required for the TCP path. |
78
78
  | Containers beta access | Cloudflare Containers is in beta; request access in the dashboard. |
79
79
  | A Cloudflare zone | The public hostname (e.g. `irc.example.com`) must live in a zone you control. |
80
- | Git checkout | Clean working tree on `main` for production deploys. |
80
+ | Git checkout | Clean working tree on `main` for deploys. |
81
81
 
82
82
  > The wss path runs on the Workers Free/Paid plan. The TCP path does
83
83
  > **not** — Spectrum mandates Enterprise. If you do not have Enterprise,
@@ -169,25 +169,25 @@ pnpm --filter @serverless-ircd/cf-tcp-container test
169
169
 
170
170
  ---
171
171
 
172
- ## 5. Deploy staging
172
+ ## 5. Deploy
173
173
 
174
- Staging is what CI deploys on every push that touches
175
- `apps/cf-tcp-container/**`. You can also deploy it manually.
174
+ The deploy is `workflow_dispatch`-only (a maintainer triggers it by
175
+ hand); you can also deploy manually.
176
176
 
177
177
  ### 5.1 Deploy the container origin
178
178
 
179
179
  ```bash
180
180
  # From the repo root:
181
- pnpm deploy:cf-tcp:staging
181
+ pnpm deploy:cf-tcp
182
182
  # Equivalent to:
183
- # pnpm build && pnpm --filter @serverless-ircd/cf-tcp-container deploy:staging
184
- # → wrangler deploy --env staging (inside apps/cf-tcp-container)
183
+ # pnpm build && pnpm --filter @serverless-ircd/cf-tcp-container run deploy
184
+ # → wrangler deploy (inside apps/cf-tcp-container)
185
185
  ```
186
186
 
187
187
  This builds the container image from `apps/cf-tcp-container/Dockerfile`
188
- and deploys the `sirc-tcp-origin-staging` Container. The first deploy
189
- prints the **origin address** — copy it into `CF_TCP_ORIGIN_ADDRESS`
190
- (used by the Spectrum step next).
188
+ and deploys the `sirc-tcp-origin` Container. The first deploy prints
189
+ the **origin address** — copy it into `CF_TCP_ORIGIN_ADDRESS` (used by
190
+ the Spectrum step next).
191
191
 
192
192
  ### 5.2 Apply the Spectrum TLS config
193
193
 
@@ -197,7 +197,7 @@ terraform init
197
197
  terraform apply \
198
198
  -var zone_id=$CF_ZONE_ID \
199
199
  -var origin_address=$CF_TCP_ORIGIN_ADDRESS \
200
- -var hostname=irc-staging.example.com
200
+ -var hostname=irc.example.com
201
201
  ```
202
202
 
203
203
  This creates the `cloudflare_spectrum_application.irc_tls` resource:
@@ -208,7 +208,7 @@ TLS on :6697 → plaintext TCP to the container origin on :6667.
208
208
  ```bash
209
209
  # Minimal smoke: register + quit over the real TLS endpoint.
210
210
  printf 'NICK smoke\r\nUSER smoke 0 * :Smoke\r\nQUIT :smoke\r\n' | \
211
- timeout 10 openssl s_client -connect irc-staging.example.com:6697 -quiet | \
211
+ timeout 10 openssl s_client -connect irc.example.com:6697 -quiet | \
212
212
  grep -q '001' && echo "SMOKE PASS" || echo "SMOKE FAIL"
213
213
  ```
214
214
 
@@ -218,7 +218,7 @@ CI does the same — see `.github/workflows/deploy-cf-tcp.yml` step
218
218
  ### 5.4 Connect a real IRC client
219
219
 
220
220
  ```
221
- /server add ircd irc-staging.example.com/6697
221
+ /server add ircd irc.example.com/6697
222
222
  /set irc.server.ircd.ssl on
223
223
  /connect ircd
224
224
  /join #test
@@ -226,27 +226,6 @@ CI does the same — see `.github/workflows/deploy-cf-tcp.yml` step
226
226
 
227
227
  ---
228
228
 
229
- ## 6. Deploy production
230
-
231
- Production is the default `wrangler.toml` environment (no `--env` flag).
232
-
233
- ```bash
234
- # 1. Edit wrangler.toml [vars] for production identity.
235
- # 2. Deploy the container:
236
- pnpm deploy:cf-tcp:prod
237
- # 3. Apply Spectrum config for the production hostname:
238
- cd apps/cf-tcp-container/terraform
239
- terraform apply -var zone_id=$CF_ZONE_ID \
240
- -var origin_address=$CF_TCP_ORIGIN_ADDRESS \
241
- -var hostname=irc.example.com
242
- ```
243
-
244
- There is **no CI auto-deploy to production** — production deploys are
245
- manual. Staging deploys on every push to `main` that touches the
246
- container path.
247
-
248
- ---
249
-
250
229
  ## 7. Configuration reference
251
230
 
252
231
  ### 7.1 Container — `apps/cf-tcp-container/wrangler.toml`
@@ -360,7 +339,7 @@ and SASL credentials ship as `[vars]` (`OPER_*`, `SASL_ACCOUNTS`). When
360
339
  strong-secret handling lands, set them via:
361
340
 
362
341
  ```bash
363
- wrangler secret put OPER_PASSWORD --env staging
342
+ wrangler secret put OPER_PASSWORD
364
343
  ```
365
344
 
366
345
  CI uses these **GitHub Actions secrets/variables** (configure under
@@ -384,19 +363,20 @@ Settings → Secrets and variables → Actions):
384
363
  (the wss path). The two are independent — a deployment can enable
385
364
  either or both.
386
365
 
387
- Triggers: push to `main` touching `apps/cf-tcp-container/**` or the
388
- workflow itself, plus `workflow_dispatch`. Steps (in order):
366
+ Triggers: `workflow_dispatch` only (a maintainer triggers it by hand;
367
+ there is no automatic push-to-main deploy). Steps (in order):
389
368
 
390
369
  1. Checkout → pnpm 11 + Node 24 → `pnpm install --frozen-lockfile`.
391
370
  2. `pnpm build`, `pnpm typecheck`, `pnpm --filter cf-tcp-container test`.
392
- 3. `wrangler deploy --env staging` (container).
371
+ 3. `wrangler deploy` (container).
393
372
  4. `terraform init && terraform apply -auto-approve` (Spectrum).
394
373
  5. TLS smoke (`openssl s_client`), gated on `CF_TCP_SMOKE_HOST`.
395
374
 
396
- Concurrency is serialized via `concurrency.group: cf-tcp-staging`.
375
+ Concurrency is serialized via `concurrency.group: cf-tcp-deploy`.
397
376
 
398
- There is **no auto-deploy to production**; production is manual
399
- (`pnpm deploy:cf-tcp:prod` + `terraform apply` for the prod hostname).
377
+ There is a single deploy target the staging/prod split was collapsed,
378
+ so the same workflow + account ships to whichever Container namespace
379
+ the credentials target.
400
380
 
401
381
  ---
402
382
 
@@ -481,16 +461,12 @@ terraform version # >= 1.5
481
461
  pnpm build
482
462
  pnpm --filter @serverless-ircd/cf-tcp-container start
483
463
 
484
- # Staging
485
- pnpm deploy:cf-tcp:staging # container
464
+ # Deploy (single target — staging vs prod is which account you point at)
465
+ pnpm deploy:cf-tcp # container
486
466
  cd apps/cf-tcp-container/terraform && \
487
467
  terraform apply -var zone_id=$CF_ZONE_ID \
488
468
  -var origin_address=$CF_TCP_ORIGIN_ADDRESS \
489
- -var hostname=irc-staging.example.com # Spectrum
490
-
491
- # Production (manual)
492
- pnpm deploy:cf-tcp:prod
493
- cd apps/cf-tcp-container/terraform && terraform apply ... # prod hostname
469
+ -var hostname=irc.example.com # Spectrum
494
470
 
495
471
  # Tests
496
472
  pnpm --filter @serverless-ircd/cf-tcp-container test
@@ -37,7 +37,7 @@ this repo itself) should pin exact versions.
37
37
 
38
38
  ### 1.1 Workspace version lock
39
39
 
40
- All fifteen `package.json` files (root + 14 workspace packages) carry the
40
+ All sixteen `package.json` files (root + 15 workspace packages) carry the
41
41
  **same** version string. They are bumped together in the release
42
42
  commit. The pre-release checklist in §4 enforces this.
43
43
 
@@ -63,8 +63,8 @@ not always possible.
63
63
  | Push to `main` | Maintainers | Merging the release PR. |
64
64
  | Push tags (`v*`) | Maintainers | `git push origin v0.x.0`. |
65
65
  | Create GitHub Release | Maintainers | Publishing the release notes. |
66
- | `CLOUDFLARE_API_TOKEN` secret | Repo Settings → Secrets | Local prod deploys. CI staging deploys. |
67
- | `CF_SMOKE_URL` variable | Repo Settings → Variables | Staging smoke e2e in CI. |
66
+ | `CLOUDFLARE_API_TOKEN` secret | Repo Settings → Secrets | Manual + CI deploys (single target). |
67
+ | `CF_SMOKE_URL` variable | Repo Settings → Variables | Smoke e2e in CI (manual dispatch). |
68
68
 
69
69
  There is no separate "release engineer" role. The maintainer who cuts
70
70
  the tag owns the release end-to-end, including the production deploy
@@ -101,7 +101,7 @@ git pull --ff-only origin main
101
101
  # 2. CI is green on the tip of main.
102
102
  # Open: https://github.com/<owner>/<repo>/actions
103
103
  # - CI workflow: lint / typecheck / coverage all green.
104
- # - deploy-cf workflow: staging deploy + smoke e2e green.
104
+ # - deploy-cf workflow: deploy + smoke e2e green (manual dispatch).
105
105
 
106
106
  # 3. Local full verify.
107
107
  pnpm install --frozen-lockfile
@@ -120,9 +120,9 @@ pnpm build # every package produces dist/
120
120
  # be green (step 2), so locally re-run `pnpm coverage` as a
121
121
  # belt-and-braces check before tagging.
122
122
 
123
- # 5. Staging smoke e2e passes against the deployed tip-of-main.
123
+ # 5. Smoke e2e passes against the deployed tip-of-main.
124
124
  node apps/cf-worker/scripts/smoke.mjs \
125
- --url wss://serverless-ircd-staging.<subdomain>.workers.dev
125
+ --url wss://serverless-ircd.<subdomain>.workers.dev
126
126
 
127
127
  # 6. CHANGELOG.md is complete and accurate (§5).
128
128
  # 7. All package.json versions are bumped and lock-step (§6).
@@ -187,8 +187,9 @@ understand what changed and what they need to do.
187
187
 
188
188
  ## 6. Bumping versions
189
189
 
190
- There are fifteen `package.json` files. They must end up at the same
191
- version:
190
+ There are sixteen `package.json` files (root + 15 workspace packages).
191
+ They must end up at the same version (`tools/package.json` is a parent
192
+ container with no `version` field and is not bumped):
192
193
 
193
194
  ```
194
195
  ./package.json (workspace root)
@@ -206,6 +207,7 @@ apps/web/package.json
206
207
  tools/tcp-ws-forwarder/package.json
207
208
  tools/load-test/package.json
208
209
  tools/ci-hardening/package.json
210
+ scripts/package.json (deploy helpers)
209
211
  ```
210
212
 
211
213
  To cut `v0.X.Y`:
@@ -217,8 +219,8 @@ git checkout -b release/v0.X.Y
217
219
  # bump each file. The version field is the only change.
218
220
  # (Optional helper: `pnpm -r exec -- node -e '...'` if you script it.)
219
221
 
220
- # verify all eight files agree:
221
- rg '"version":' package.json packages/*/package.json apps/*/package.json tools/*/package.json
222
+ # verify all files agree:
223
+ rg '"version":' package.json packages/*/package.json apps/*/package.json tools/*/package.json scripts/package.json
222
224
  ```
223
225
 
224
226
  The lockfile (`pnpm-lock.yaml`) is unaffected — none of the workspace
@@ -270,7 +272,7 @@ git pull --ff-only origin main
270
272
  git log -1 --oneline # expect: chore(release): v0.X.Y
271
273
 
272
274
  # Confirm versions are at the new release.
273
- rg '"version":' package.json packages/*/package.json apps/*/package.json tools/*/package.json
275
+ rg '"version":' package.json packages/*/package.json apps/*/package.json tools/*/package.json scripts/package.json
274
276
  ```
275
277
 
276
278
  ### 8.1 Tag and push
@@ -299,8 +301,10 @@ configured (`git config tag.gpgSign true`).
299
301
 
300
302
  ### 8.3 Production deploy (manual)
301
303
 
302
- There is **no CI auto-deploy to production**. The maintainer runs the
303
- prod deploy by hand, from a clean checkout on the tag:
304
+ There is a single deploy target per platform (`pnpm deploy:cf`); the
305
+ staging/prod split was collapsed, so the same command ships to whichever
306
+ Cloudflare account the credentials target. The maintainer runs the
307
+ deploy by hand, from a clean checkout on the tag:
304
308
 
305
309
  ```bash
306
310
  git checkout v0.X.Y
@@ -313,17 +317,17 @@ pnpm build
313
317
  pnpm typecheck
314
318
  pnpm test
315
319
 
316
- # Deploy the Cloudflare Worker to production.
317
- pnpm deploy:cf:prod
318
- # → wrangler deploy (default env = production)
320
+ # Deploy the Cloudflare Worker.
321
+ pnpm deploy:cf
322
+ # → wrangler deploy
319
323
 
320
- # Tail prod logs while the first connections arrive.
324
+ # Tail logs while the first connections arrive.
321
325
  npx wrangler tail
322
326
 
323
- # Smoke-check prod with the same script CI uses on staging.
324
- # You must point it at the prod wss:// URL:
327
+ # Smoke-check the deploy with the same script CI uses.
328
+ # You must point it at the wss:// URL:
325
329
  node apps/cf-worker/scripts/smoke.mjs \
326
- --url wss://<prod-workers-subdomain>.workers.dev
330
+ --url wss://<workers-subdomain>.workers.dev
327
331
  ```
328
332
 
329
333
  If smoke fails on prod:
@@ -355,7 +359,7 @@ To redeploy a previous release:
355
359
  git checkout v0.W.Z # the previous tag
356
360
  pnpm install --frozen-lockfile
357
361
  pnpm build
358
- pnpm deploy:cf:prod
362
+ pnpm deploy:cf
359
363
  ```
360
364
 
361
365
  This works as long as the previous release's code is compatible with
@@ -430,7 +434,7 @@ pnpm lint && pnpm typecheck && pnpm test && pnpm build
430
434
 
431
435
  # 2. Branch, bump versions + CHANGELOG, open PR (§6, §7).
432
436
  git checkout -b release/v0.X.Y
433
- # ... edit 8 × package.json + CHANGELOG.md ...
437
+ # ... edit 16 × package.json + CHANGELOG.md ...
434
438
  git commit -am "chore(release): v0.X.Y"
435
439
  git push -u origin release/v0.X.Y
436
440
  # ... open PR, get it merged ...
@@ -445,7 +449,7 @@ git push origin v0.X.Y
445
449
  # 5. Prod deploy (§8.3).
446
450
  git checkout v0.X.Y
447
451
  pnpm install --frozen-lockfile && pnpm build
448
- pnpm deploy:cf:prod
452
+ pnpm deploy:cf
449
453
  node apps/cf-worker/scripts/smoke.mjs --url wss://<prod>.workers.dev
450
454
  ```
451
455
 
package/docs/Services.md CHANGED
@@ -46,8 +46,9 @@ ServerlessIRCd chose **integrated services**. The rationale (full detail in
46
46
  - The pure-reducer + location-of-authority architecture already has the
47
47
  right seam: a `ServicesStore` port absorbs the side effects, and reducers
48
48
  stay pure and unit-testable.
49
- - One storage layer is shared conceptually with the SASL `AccountStore`
50
- authentication and nick ownership are meant to compose, not duplicate.
49
+ - One storage layer (`ServicesStore`) is the **single credential home**
50
+ for SASL PLAIN, SASL EXTERNAL, PASS-auth, and NickServ IDENTIFY
51
+ authentication and nick ownership compose, not duplicate.
51
52
  - The serverless substrates (Cloudflare Durable Objects, AWS DynamoDB)
52
53
  handle persistence natively; a separate long-running services process
53
54
  would defeat the deployment model.
@@ -90,6 +91,12 @@ If no store is bound, every services command replies:
90
91
  milliseconds, NickServ waits before force-disconnecting a `ghost`-enforced
91
92
  client. `0` makes `ghost` behave like `kill` (immediate). Omit for the
92
93
  **30 000 ms** default (`DEFAULT_NICK_ENFORCE_GRACE_MS`).
94
+ - **`nickServ.minPasswordLength`** (`integer > 0`, optional) — minimum
95
+ length enforced by NickServ `SET PASSWORD` for the new password. Omit for
96
+ the **8** default (`DEFAULT_MIN_PASSWORD_LENGTH`); a shorter new password
97
+ is rejected before hashing with
98
+ `Password is too short (minimum <n> characters).`. The maximum (256) is
99
+ fixed, not configurable, to bound the scrypt input budget.
93
100
 
94
101
  ---
95
102
 
@@ -176,6 +183,7 @@ policy, and nick-info queries. A registered nick is an account.
176
183
  | `DROP` | `PRIVMSG NickServ :DROP [nick]` | Requires prior identify. Drops the registration, clears `state.account` and `+r`, fans `ACCOUNT *`. Reply: `Nickname <nick> has been dropped.` |
177
184
  | `INFO` | `PRIVMSG NickServ :INFO [nick]` | Defaults to the current nick. Replies (NOTICE): `Nick:`, `Account:`, and (only if the caller is identified as the owning account **or** is an oper) `Email:`. A non-owner sees `Nick:` / `Account:` only. Unregistered → `Nick <target> is not registered.` |
178
185
  | `SET ENFORCE` | `PRIVMSG NickServ :SET ENFORCE none|ghost|kill` | Requires identify as the owning account. See enforcement below. |
186
+ | `SET PASSWORD` | `PRIVMSG NickServ :SET PASSWORD <old-password> <new-password>` | Requires identify as the owning account **and** re-supplies the current password (defence against a hijacked `+r` session). Re-hashes the new password (scrypt) and overwrites the credential. New-password length floor: `nickServ.minPasswordLength` (default **8**); fixed ceiling **256**. Reply: `Password changed.` Wrong old password → `Invalid password.` (same text as a failed `IDENTIFY`; no leak). The current session stays `+r`; **other sessions of the same account are NOT kicked.** |
179
187
 
180
188
  Help / unknown command: `Available commands: REGISTER, IDENTIFY, DROP, INFO, SET`.
181
189
 
@@ -431,27 +439,24 @@ Both accept `--accounts alice:s3cret bob:pw …` or `--file accounts.txt`
431
439
  ```
432
440
  # Cloudflare
433
441
  node --import tsx tools/seed-cf-accounts.ts \
434
- --database serverless-ircd-accounts-staging --env staging --remote \
442
+ --database serverless-ircd-accounts --remote \
435
443
  --accounts alice:s3cret
436
444
 
437
- # AWS
445
+ # AWS (the --table is the Services table, not the legacy Accounts table)
438
446
  node --import tsx tools/seed-aws-accounts.ts \
439
- --table StagingAccounts --endpoint http://localhost:8000 \
447
+ --table StagingServices --endpoint http://localhost:8000 \
440
448
  --accounts alice:s3cret
441
449
  ```
442
450
 
443
- > **NickServ accounts vs SASL accounts.** NickServ registrations live in
444
- > the **`ServicesStore`** (`nickserv_accounts`), which is unified with the
445
- > SASL `AccountStore` the seed scripts above populate: a NickServ
446
- > registration is a SASL account and vice versa, both scrypt-hashed (see
447
- > the [walkthrough](#13-end-to-end-walkthrough)). The recommended way to
448
- > create a NickServ account is the live
449
- > `PRIVMSG NickServ :REGISTER …` flow over an established connection. A
450
- > dedicated `tools/seed-nickserv-accounts.ts` mirroring the SASL seeders
451
- > is tracked as a follow-up; because credentials are scrypt-hashed at rest
452
- > (see [§11](#11-security-characteristics--known-gaps)), bulk-seeded rows
453
- > must supply a scrypt `algorithm`/`salt`/`hash` triple rather than a
454
- > plaintext password.
451
+ > **NickServ accounts vs SASL accounts — unified.** Both SASL PLAIN and
452
+ > NickServ IDENTIFY consult the **`ServicesStore`** (`nickserv_accounts`).
453
+ > There is no separate SASL credential table: a NickServ registration is a
454
+ > SASL account and vice versa, both scrypt-hashed (see the
455
+ > [walkthrough](#13-end-to-end-walkthrough)). The recommended way to create
456
+ > a NickServ account is the live `PRIVMSG NickServ :REGISTER …` flow over
457
+ > an established connection. The seed scripts above populate
458
+ > `nickserv_accounts` directly via the same scrypt-hashed `registerNick`
459
+ > path; the legacy standalone `accounts` / `Accounts` table was dropped.
455
460
 
456
461
  Operators can manage AKILL/JUPE entries via `PRIVMSG OperServ` (oper-only,
457
462
  see [§7](#7-operserv--network-bans-jupes-raw-oper-only)), or pre-populate
@@ -464,13 +469,13 @@ them by writing directly into the `ServicesStore` backing tables.
464
469
  Be aware of the following when operating or contributing to services:
465
470
 
466
471
  - **NickServ passwords are stored scrypt-hashed** in the `ServicesStore`
467
- (the same scrypt path the SASL `AccountStore` uses:
468
- `InMemoryServicesStore.registerNick` hashes via `hashAccountCredential`
472
+ (`InMemoryServicesStore.registerNick` hashes via `hashAccountCredential`
469
473
  before persisting; `verifyNick` re-derives via `verifyHashedPassword`;
470
474
  the D1 `nickserv_accounts` table stores `algorithm`/`salt`/`hash`
471
- columns, never the plaintext password). NickServ and SASL accounts are
472
- unified, so a NickServ registration is a SASL account and vice versa.
473
- Treat the services backing store (D1 database / DynamoDB table) as
475
+ columns, never the plaintext password). The `ServicesStore` is the
476
+ **single credential home**: SASL PLAIN, SASL EXTERNAL, PASS-auth
477
+ (`PASS <nick>:<password>`), and NickServ `IDENTIFY` all verify through
478
+ the same `verifyNick` / `verifyCertFP` surface. Treat the services backing store (D1 database / DynamoDB table) as
474
479
  sensitive regardless: restrict access and enable at-rest encryption
475
480
  (DynamoDB encryption-at-rest is on by default).
476
481
  - **HostServ `REQUEST` approval mode is configurable** — the default
@@ -576,3 +581,45 @@ When `bob` later identifies and joins, ChanServ auto-ops them:
576
581
 
577
582
  That completes the core nick + channel registration flow. From here, the
578
583
  per-service references in §4–§8 cover every shipped command.
584
+
585
+ ---
586
+
587
+ ## 14. Unified credential verification — four entry points
588
+
589
+ The `ServicesStore` is the **single credential home**. Every
590
+ account-setting path reduces to "this connection is now account X" via
591
+ one of four entry points, all of which consult the same scrypt-hashed
592
+ credential on the `nickserv_accounts` row:
593
+
594
+ | Entry point | Mechanism | Credential verify call | Reducer |
595
+ |-------------|-----------|------------------------|---------|
596
+ | **SASL PLAIN** | `AUTHENTICATE PLAIN` (IRCv3 `sasl`) | `services.verifyNick(username, password)` | `commands/sasl.ts` |
597
+ | **SASL EXTERNAL** (CertFP) | `AUTHENTICATE EXTERNAL` (IRCv3 `sasl` + mTLS) | `services.verifyCertFP(certSubject)` (subject resolved by the unchanged `MtlsIdentityProvider`) | `commands/sasl.ts` |
598
+ | **NickServ `IDENTIFY`** | `PRIVMSG NickServ :IDENTIFY <password>` | `services.verifyNick(state.nick, password)` | `commands/nickserv.ts` |
599
+ | **PASS-auth** | `PASS <nick>:<password>` at or after registration | `services.verifyNick(nick, password)` | `commands/account-auth.ts` (`attemptPassAccountAuth`) |
600
+
601
+ All four share the same `applyAccountSuccess` downstream chain: stamp
602
+ `state.account`, set read-only `+r`, seed read-markers, replay persisted
603
+ away, deliver queued memos, re-apply assigned vhost, and emit `900
604
+ RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS` + the `account-notify` `ACCOUNT`
605
+ broadcast.
606
+
607
+ A NickServ-registered nick authenticates through any of the four entry
608
+ points without a second registration — the credential set by
609
+ `PRIVMSG NickServ :REGISTER` IS the SASL/PASS credential.
610
+
611
+ ### SASL EXTERNAL (CertFP)
612
+
613
+ SASL EXTERNAL maps a verified client-certificate subject to an account.
614
+ The edge platform (CF API Shield mTLS, AWS API Gateway custom-domain
615
+ mTLS) terminates TLS and validates the client cert; the verified subject
616
+ is surfaced to the Worker / Lambda via the unchanged
617
+ `MtlsIdentityProvider` port. The subject→account lookup lives in
618
+ `ServicesStore.verifyCertFP`, which scans `certSubjects: string[]` on
619
+ each registered nick (byte-exact, case-sensitive comparison — cert
620
+ subjects are NOT case-folded). `addCertFP(account, subject)` /
621
+ `removeCertFP(account, subject)` mutators are shipped for an upcoming
622
+ NickServ `CERT ADD`/`LIST`/`DEL` command (Atheme parity); the
623
+ `cert_subjects` column on `nickserv_accounts` / the `certSubjects`
624
+ attribute on the DynamoDB `Services` item carries the array (JSON-encoded
625
+ `'[]'` by default).
@@ -11,9 +11,19 @@ Cross-reference: `PLAN.md` §6.1 (CF mapping), `docs/PlanWebClient.md`
11
11
  (design), `docs/Cloudflare-Deployment-Guide.md` (Worker deploy),
12
12
  `apps/cf-worker/wrangler.toml`, `apps/web/`.
13
13
 
14
+ > **AWS counterpart.** This doc covers the **Cloudflare** web-client
15
+ > path, where the Worker serves both the SPA and the WebSocket from
16
+ > one origin. The AWS deployment **cannot** unify them — the SPA
17
+ > lives on CloudFront (S3 + OAC) and the WebSocket endpoint on API
18
+ > Gateway, so the baked config carries the API Gateway wss URL as a
19
+ > literal and the deploy is a separate two-phase pipeline (stack →
20
+ > bake → s3 sync → invalidate). For the AWS web client — construct,
21
+ > build, deploy pipeline, the `WEB_ORIGINS` CSWSH defence, and custom
22
+ > domain — see **`docs/AWS-Deployment.md` §16 (Web client)**.
23
+
14
24
  **Acceptance criterion (TICKET-139):** a new contributor can
15
25
  `git submodule update --init`, `pnpm install`,
16
- `pnpm --filter web build:staging`, and `wrangler deploy --env staging`
26
+ `pnpm --filter web build`, and `wrangler deploy`
17
27
  following only this doc.
18
28
 
19
29
  ---
@@ -82,7 +92,7 @@ pnpm --version # 11.x
82
92
 
83
93
  ---
84
94
 
85
- ## 3. Quickstart (clean checkout → deployed staging SPA)
95
+ ## 3. Quickstart (clean checkout → deployed SPA)
86
96
 
87
97
  ```bash
88
98
  # 1. Pull submodules (docs wiki + Kiwi IRC upstream sources).
@@ -91,9 +101,9 @@ git submodule update --init
91
101
  # 2. Install workspace deps.
92
102
  pnpm install
93
103
 
94
- # 3. Build the SPA + landing page (default config = staging target).
95
- pnpm --filter web build:staging
96
- # Equivalent: pnpm --filter @serverless-ircd/web build:staging
104
+ # 3. Build the SPA + landing page (default config = dev target).
105
+ pnpm --filter web build
106
+ # Equivalent: pnpm --filter @serverless-ircd/web build
97
107
  # → apps/web/dist/index.html (landing page)
98
108
  # → apps/web/dist/webclient/index.html (Kiwi SPA)
99
109
  # → apps/web/dist/webclient/static/config.json (baked, env-specific)
@@ -107,9 +117,9 @@ pnpm --filter @serverless-ircd/cf-worker dev
107
117
  # → http://localhost:8787/webclient/ Kiwi SPA
108
118
  # → ws://localhost:8787/ IRC-over-WebSocket (same endpoint)
109
119
 
110
- # 6. Deploy staging (Worker + assets in one command).
111
- pnpm deploy:cf:staging
112
- # → wrangler deploy --env staging
120
+ # 6. Deploy (Worker + assets in one command).
121
+ pnpm deploy:cf
122
+ # → wrangler deploy
113
123
  ```
114
124
 
115
125
  Open `http://localhost:8787/webclient/` in a browser — Kiwi boots, reads
@@ -131,14 +141,14 @@ The build is an orchestrated layering over the upstream Kiwi build —
131
141
 
132
142
  | `pnpm ...` command | Reads | Bakes into `dist/webclient/static/config.json` |
133
143
  |---|---|---|
134
- | `--filter web build` | `static/config.json` | default (staging-shaped) config |
135
- | `--filter web build:staging` | `static/config.staging.json` | staging config |
144
+ | `--filter web build` | `static/config.json` | dev default config |
136
145
  | `--filter web build:prod` | `static/config.prod.json` | prod config |
137
146
 
138
- `--env` is parsed by `src/build-env.ts`; an unknown value aborts the
139
- build with a readable error. The selected file is validated against the
140
- Zod schema (`src/config-schema.ts`) **before** it is baked, so a
141
- malformed config fails the build rather than shipping a broken SPA.
147
+ `--env` is parsed by `src/build-env.ts`; an unknown value (including the
148
+ retired `staging`) aborts the build with a readable error. The selected
149
+ file is validated against the Zod schema (`src/config-schema.ts`)
150
+ **before** it is baked, so a malformed config fails the build rather
151
+ than shipping a broken SPA.
142
152
 
143
153
  ### 4.2 What the build does, in order
144
154
 
@@ -172,9 +182,9 @@ The schema models only the fields ServerlessIRCd cares about; the rest
172
182
  of Kiwi's surface is passed through untouched (`.passthrough()`).
173
183
 
174
184
  ```jsonc
175
- // static/config.staging.json
185
+ // static/config.prod.json
176
186
  {
177
- "windowTitle": "ServerlessIRCd (staging)",
187
+ "windowTitle": "ServerlessIRCd",
178
188
  "startupOptions": {
179
189
  "server": "{{hostname}}", // resolved to window.location.hostname at boot
180
190
  "direct": true, // MUST be true — direct WS, no kiwiServer gateway
@@ -191,7 +201,7 @@ Field notes (see the schema docstrings for the full rationale):
191
201
  - **`startupOptions.server`** — `{{hostname}}` (or `{{host}}` to keep
192
202
  a non-default port) is a Kiwi template resolved by the browser to
193
203
  `window.location.hostname` at load time. This is what lets one build
194
- artifact target staging, prod, preview, and PR deploys without a
204
+ artifact target prod, preview, and PR deploys without a
195
205
  per-env hostname. A literal hostname is also accepted.
196
206
  - **`startupOptions.direct`** — MUST be `true`. ServerlessIRCd is a
197
207
  direct IRC-over-WebSocket server; the Kiwi default (`false`) routes
@@ -209,7 +219,7 @@ Field notes (see the schema docstrings for the full rationale):
209
219
  A malformed config aborts the build with an aggregated error, e.g.:
210
220
 
211
221
  ```
212
- config.staging.json: Invalid Kiwi web config:
222
+ config.prod.json: Invalid Kiwi web config:
213
223
  - startupOptions.port: port must be a valid TCP port (1-65535)
214
224
  - startupOptions.channel: must begin with '#'
215
225
  ```
@@ -275,7 +285,7 @@ WEB_ORIGINS = "https://irc.example.com,https://web.example.com"
275
285
 
276
286
  ```bash
277
287
  # or per-deploy via the secret store / dashboard env vars:
278
- wrangler secret put WEB_ORIGINS --env staging
288
+ wrangler secret put WEB_ORIGINS
279
289
  ```
280
290
 
281
291
  ### 5.3 Responses
@@ -323,8 +333,8 @@ node apps/cf-worker/scripts/smoke.mjs
323
333
  ### 6.2 Iterate on the SPA config without rebuilding Kiwi
324
334
 
325
335
  The Kiwi upstream build is the slow step. For a config-only change,
326
- edit `static/config.staging.json`, then re-run
327
- `pnpm --filter web build:staging` — step 2 of the build reuses
336
+ edit `static/config.prod.json`, then re-run
337
+ `pnpm --filter web build:prod` — step 2 of the build reuses
328
338
  `upstream/node_modules` and skips straight to re-baking the config
329
339
  (steps 4–6 are fast).
330
340
 
@@ -502,8 +512,7 @@ git submodule update --init # docs + apps/web/upstream (Kiwi)
502
512
  pnpm install
503
513
 
504
514
  # Build the SPA + landing page (pick the env)
505
- pnpm --filter web build # default config (staging-shaped)
506
- pnpm --filter web build:staging # static/config.staging.json
515
+ pnpm --filter web build # default config (dev target)
507
516
  pnpm --filter web build:prod # static/config.prod.json
508
517
 
509
518
  # Local dev (Worker serves SPA + landing page + WS on one origin)
@@ -511,8 +520,8 @@ pnpm build # build the rest of the workspace
511
520
  pnpm --filter @serverless-ircd/cf-worker dev # http://localhost:8787
512
521
  node apps/cf-worker/scripts/smoke.mjs # WS smoke
513
522
 
514
- # Deploy staging (Worker + assets in one command)
515
- pnpm deploy:cf:staging # wrangler deploy --env staging
523
+ # Deploy (Worker + assets in one command)
524
+ pnpm deploy:cf # wrangler deploy
516
525
 
517
526
  # Tests / lint
518
527
  pnpm --filter web test
@@ -527,7 +536,7 @@ Key files:
527
536
  | `apps/web/scripts/build.mjs` | Orchestrates the upstream Kiwi build + layers our config. |
528
537
  | `apps/web/src/config-schema.ts` | Zod schema for `static/config.*.json` (the only TS we own). |
529
538
  | `apps/web/src/build-env.ts` | `--env` → config-file resolution (unit-tested). |
530
- | `apps/web/static/config.{,staging,prod}.json` | Per-env Kiwi configs (validated at build time). |
539
+ | `apps/web/static/config.{,prod}.json` | Kiwi configs (validated at build time). |
531
540
  | `apps/web/landing/index.html` | Static landing page (project front door), copied to `dist/index.html`. |
532
541
  | `apps/web/tests/build-smoke.test.ts` | Landing-page + build-output assertions. |
533
542
  | `apps/cf-worker/src/worker.ts` | Edge handler: WS → `ConnectionDO`, `/health`, else `env.ASSETS`. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverless-ircd",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Serverless IRC daemon with a platform-agnostic core and Cloudflare Workers + AWS adapters",
6
6
  "license": "BSD-3-Clause",
@@ -17,7 +17,7 @@
17
17
  "typescript": "^5.9.3",
18
18
  "vite": "^7.3.6",
19
19
  "vitest": "^4.1.10",
20
- "@serverless-ircd/aws-adapter": "0.9.0"
20
+ "@serverless-ircd/aws-adapter": "0.10.0"
21
21
  },
22
22
  "scripts": {
23
23
  "build": "turbo run build",
@@ -32,13 +32,10 @@
32
32
  "mutation:commands": "pnpm --filter @serverless-ircd/irc-core mutation:commands",
33
33
  "mutation": "pnpm run mutation:protocol && pnpm run mutation:commands",
34
34
  "clean": "turbo run clean && rimraf node_modules",
35
- "deploy:cf:staging": "pnpm --filter @serverless-ircd/cf-worker deploy:staging",
36
- "deploy:cf:prod": "pnpm --filter @serverless-ircd/cf-worker deploy:prod",
37
- "smoke:cf:staging": "pnpm --filter @serverless-ircd/cf-worker smoke:staging",
38
- "deploy:cf-tcp:staging": "pnpm build && pnpm --filter @serverless-ircd/cf-tcp-container deploy:staging",
39
- "deploy:cf-tcp:prod": "pnpm build && pnpm --filter @serverless-ircd/cf-tcp-container deploy:prod",
40
- "deploy:aws:staging": "pnpm build && pnpm --filter @serverless-ircd/aws-stack deploy:staging",
41
- "deploy:aws:prod": "pnpm build && pnpm --filter @serverless-ircd/aws-stack deploy:prod",
42
- "smoke:aws:staging": "pnpm --filter @serverless-ircd/aws-stack smoke:staging"
35
+ "deploy:cf": "pnpm --filter @serverless-ircd/cf-worker run deploy",
36
+ "smoke:cf": "pnpm --filter @serverless-ircd/cf-worker run smoke",
37
+ "deploy:cf-tcp": "pnpm build && pnpm --filter @serverless-ircd/cf-tcp-container run deploy",
38
+ "deploy:aws": "pnpm build && pnpm --filter @serverless-ircd/aws-stack run deploy",
39
+ "smoke:aws": "pnpm --filter @serverless-ircd/aws-stack run smoke"
43
40
  }
44
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/aws-adapter",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "description": "AWS Lambda + DynamoDB adapter: AwsRuntime implementing IrcRuntime + $connect/$disconnect/$default handlers",
6
6
  "license": "BSD-3-Clause",