serverless-ircd 0.1.0 → 0.3.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 (204) hide show
  1. package/.github/workflows/ci.yml +96 -2
  2. package/.github/workflows/deploy-aws.yml +129 -0
  3. package/.github/workflows/deploy-cf.yml +0 -2
  4. package/.gitmodules +3 -0
  5. package/CHANGELOG.md +436 -0
  6. package/README.md +127 -84
  7. package/apps/aws-stack/README.md +73 -0
  8. package/apps/aws-stack/bin/aws.ts +49 -0
  9. package/apps/aws-stack/cdk.json +10 -0
  10. package/apps/aws-stack/package.json +41 -0
  11. package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
  12. package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
  13. package/apps/aws-stack/scripts/smoke.mjs +142 -0
  14. package/apps/aws-stack/src/aws-stack.ts +263 -0
  15. package/apps/aws-stack/src/tables.ts +10 -0
  16. package/apps/aws-stack/tests/localstack.test.ts +46 -0
  17. package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
  18. package/apps/aws-stack/tests/stack.test.ts +464 -0
  19. package/apps/aws-stack/tsconfig.build.json +11 -0
  20. package/apps/aws-stack/tsconfig.test.json +8 -0
  21. package/apps/aws-stack/vitest.config.ts +20 -0
  22. package/apps/cf-worker/package.json +1 -1
  23. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  24. package/apps/cf-worker/src/worker.ts +8 -2
  25. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  26. package/apps/cf-worker/wrangler.test.toml +5 -1
  27. package/apps/cf-worker/wrangler.toml +66 -17
  28. package/apps/local-cli/package.json +1 -1
  29. package/apps/local-cli/src/config-loader.ts +57 -0
  30. package/apps/local-cli/src/main.ts +1 -1
  31. package/apps/local-cli/src/server.ts +267 -32
  32. package/apps/local-cli/tests/config-loader.test.ts +107 -0
  33. package/apps/local-cli/tests/e2e.test.ts +126 -0
  34. package/apps/local-cli/tests/security-e2e.test.ts +239 -0
  35. package/biome.json +3 -1
  36. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  37. package/docs/ADR-002-location-of-authority.md +82 -0
  38. package/docs/ADR-003-durable-object-sharding.md +93 -0
  39. package/docs/ADR-004-dynamodb-schema.md +96 -0
  40. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  41. package/docs/ADR-006-sasl-mechanism-scope.md +86 -0
  42. package/docs/ADR-007-deterministic-ports.md +82 -0
  43. package/docs/ADR-008-monorepo-tooling.md +60 -0
  44. package/docs/AWS-Adapter-Architecture.md +496 -0
  45. package/docs/AWS-Deployment.md +1186 -0
  46. package/docs/Cloudflare-Deployment-Guide.md +660 -0
  47. package/docs/Home.md +11 -0
  48. package/docs/Observability.md +87 -0
  49. package/docs/PlanIRCv3Websocket.md +489 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +19 -14
  53. package/packages/aws-adapter/README.md +35 -0
  54. package/packages/aws-adapter/package.json +52 -0
  55. package/packages/aws-adapter/src/account-store.ts +121 -0
  56. package/packages/aws-adapter/src/aws-runtime.ts +871 -0
  57. package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
  58. package/packages/aws-adapter/src/config-loader.ts +126 -0
  59. package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
  60. package/packages/aws-adapter/src/dynamo.ts +61 -0
  61. package/packages/aws-adapter/src/handlers/connect.ts +44 -0
  62. package/packages/aws-adapter/src/handlers/default.ts +330 -0
  63. package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
  64. package/packages/aws-adapter/src/handlers/index.ts +293 -0
  65. package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
  66. package/packages/aws-adapter/src/handlers/state.ts +13 -0
  67. package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
  68. package/packages/aws-adapter/src/index.ts +74 -0
  69. package/packages/aws-adapter/src/message-store.ts +34 -0
  70. package/packages/aws-adapter/src/serialize.ts +283 -0
  71. package/packages/aws-adapter/src/tables.ts +53 -0
  72. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
  73. package/packages/aws-adapter/tests/account-store.test.ts +280 -0
  74. package/packages/aws-adapter/tests/aws-harness.ts +408 -0
  75. package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
  76. package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
  77. package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
  78. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
  79. package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
  80. package/packages/aws-adapter/tests/global-setup.ts +301 -0
  81. package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
  82. package/packages/aws-adapter/tests/handlers.test.ts +473 -0
  83. package/packages/aws-adapter/tests/message-store.test.ts +55 -0
  84. package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
  85. package/packages/aws-adapter/tests/serialize.test.ts +249 -0
  86. package/packages/aws-adapter/tests/smoke.test.ts +48 -0
  87. package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
  88. package/packages/aws-adapter/tests/tables.test.ts +74 -0
  89. package/packages/aws-adapter/tests/transactions.test.ts +446 -0
  90. package/packages/aws-adapter/tsconfig.build.json +16 -0
  91. package/packages/aws-adapter/tsconfig.test.json +14 -0
  92. package/packages/aws-adapter/vitest.config.ts +23 -0
  93. package/packages/cf-adapter/package.json +2 -1
  94. package/packages/cf-adapter/src/cf-runtime.ts +42 -22
  95. package/packages/cf-adapter/src/channel-do.ts +40 -1
  96. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  97. package/packages/cf-adapter/src/config-loader.ts +135 -0
  98. package/packages/cf-adapter/src/connection-do.ts +119 -0
  99. package/packages/cf-adapter/src/env.ts +27 -1
  100. package/packages/cf-adapter/src/index.ts +2 -1
  101. package/packages/cf-adapter/tests/cf-harness.ts +2 -2
  102. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
  103. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  104. package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
  105. package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
  106. package/packages/cf-adapter/tests/worker/main.ts +2 -1
  107. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  108. package/packages/cf-adapter/wrangler.test.toml +10 -1
  109. package/packages/in-memory-runtime/package.json +1 -1
  110. package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
  111. package/packages/in-memory-runtime/src/index.ts +1 -1
  112. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
  113. package/packages/irc-core/package.json +13 -2
  114. package/packages/irc-core/src/admission.ts +216 -0
  115. package/packages/irc-core/src/caps/capabilities.ts +1 -0
  116. package/packages/irc-core/src/case-fold.ts +64 -0
  117. package/packages/irc-core/src/cloak.ts +81 -0
  118. package/packages/irc-core/src/commands/cap.ts +1 -2
  119. package/packages/irc-core/src/commands/chathistory.ts +305 -0
  120. package/packages/irc-core/src/commands/index.ts +9 -0
  121. package/packages/irc-core/src/commands/invite.ts +6 -9
  122. package/packages/irc-core/src/commands/isupport.ts +1 -1
  123. package/packages/irc-core/src/commands/join.ts +63 -10
  124. package/packages/irc-core/src/commands/kick.ts +4 -11
  125. package/packages/irc-core/src/commands/list.ts +5 -4
  126. package/packages/irc-core/src/commands/mode.ts +5 -9
  127. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  128. package/packages/irc-core/src/commands/motd.ts +6 -110
  129. package/packages/irc-core/src/commands/oper.ts +151 -0
  130. package/packages/irc-core/src/commands/privmsg.ts +24 -0
  131. package/packages/irc-core/src/commands/registration.ts +79 -21
  132. package/packages/irc-core/src/commands/sasl.ts +251 -0
  133. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  134. package/packages/irc-core/src/config.ts +270 -0
  135. package/packages/irc-core/src/flood-control.ts +175 -0
  136. package/packages/irc-core/src/index.ts +7 -0
  137. package/packages/irc-core/src/ports.ts +719 -0
  138. package/packages/irc-core/src/protocol/base64.ts +16 -0
  139. package/packages/irc-core/src/protocol/index.ts +2 -1
  140. package/packages/irc-core/src/protocol/numerics.ts +11 -0
  141. package/packages/irc-core/src/protocol/parser.ts +27 -2
  142. package/packages/irc-core/src/state/connection.ts +41 -0
  143. package/packages/irc-core/src/types.ts +88 -2
  144. package/packages/irc-core/stryker.commands.conf.json +41 -0
  145. package/packages/irc-core/stryker.protocol.conf.json +26 -0
  146. package/packages/irc-core/tests/account-store.test.ts +88 -0
  147. package/packages/irc-core/tests/admission.test.ts +229 -0
  148. package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
  149. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  150. package/packages/irc-core/tests/cloak.test.ts +78 -0
  151. package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
  152. package/packages/irc-core/tests/commands/join.test.ts +246 -2
  153. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  154. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  155. package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
  156. package/packages/irc-core/tests/commands/registration.test.ts +380 -20
  157. package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
  158. package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -0
  159. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  160. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  161. package/packages/irc-core/tests/config.test.ts +721 -0
  162. package/packages/irc-core/tests/flood-control.test.ts +394 -0
  163. package/packages/irc-core/tests/message-store.test.ts +530 -0
  164. package/packages/irc-core/tests/parser.test.ts +44 -1
  165. package/packages/irc-core/tests/ports.test.ts +263 -0
  166. package/packages/irc-server/package.json +1 -1
  167. package/packages/irc-server/src/actor.ts +186 -44
  168. package/packages/irc-server/src/dispatch.ts +89 -5
  169. package/packages/irc-server/src/index.ts +2 -0
  170. package/packages/irc-server/src/routing.ts +160 -0
  171. package/packages/irc-server/tests/actor.test.ts +690 -15
  172. package/packages/irc-server/tests/dispatch.test.ts +84 -0
  173. package/packages/irc-server/tests/routing.test.ts +204 -0
  174. package/packages/irc-test-support/README.md +32 -0
  175. package/packages/irc-test-support/package.json +37 -0
  176. package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
  177. package/packages/irc-test-support/src/index.ts +24 -0
  178. package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
  179. package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
  180. package/packages/irc-test-support/tests/smoke.test.ts +11 -0
  181. package/packages/irc-test-support/tsconfig.build.json +16 -0
  182. package/packages/irc-test-support/tsconfig.test.json +14 -0
  183. package/packages/irc-test-support/vitest.config.ts +20 -0
  184. package/pnpm-workspace.yaml +1 -0
  185. package/tools/ci-hardening/package.json +30 -0
  186. package/tools/ci-hardening/src/index.ts +6 -0
  187. package/tools/ci-hardening/src/validate.ts +103 -0
  188. package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
  189. package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
  190. package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
  191. package/tools/ci-hardening/tests/validate.test.ts +177 -0
  192. package/tools/ci-hardening/tsconfig.build.json +12 -0
  193. package/tools/ci-hardening/tsconfig.test.json +10 -0
  194. package/tools/ci-hardening/vitest.config.ts +21 -0
  195. package/tools/seed-aws-accounts.ts +80 -0
  196. package/tools/tcp-ws-forwarder/package.json +1 -1
  197. package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
  198. package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
  199. package/tools/tcp-ws-forwarder/src/main.ts +33 -14
  200. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
  201. package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
  202. package/packages/cf-adapter/tests/integration/index.ts +0 -17
  203. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
  204. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
@@ -0,0 +1,96 @@
1
+ # ADR-004: AWS DynamoDB schema
2
+
3
+ **Date:** 2025-02-15 (Phase 4)
4
+ **Status:** Accepted
5
+ **Supersedes:** —
6
+ **Superseded by:** —
7
+
8
+ ## Context
9
+
10
+ The AWS adapter runs IRC state on DynamoDB (single table family, not
11
+ single-table design). Unlike Cloudflare Durable Objects, DynamoDB does not
12
+ serialize access to a record by default — concurrent writes can interleave.
13
+ Nick uniqueness, channel membership atomicity, and gone-connection cleanup
14
+ all require explicit concurrency control.
15
+
16
+ The schema must support: connection records, channel metadata, channel
17
+ membership (roster), nick registry, and SASL account credentials. It must
18
+ also handle the Lambda invocation model: each `$default` invocation is a
19
+ fresh process that reads state, runs a reducer, and writes back — there is no
20
+ in-memory state between invocations.
21
+
22
+ ## Decision
23
+
24
+ Five tables, each with a clear partition-key boundary:
25
+
26
+ | Table | Partition Key | Sort Key | TTL | Purpose |
27
+ |---|---|---|---|---|
28
+ | `Connections` | `connectionId` | — | `idleSince` | Per-connection state: nick, user, host, caps, joined channels, away |
29
+ | `ChannelMeta` | `channelName` | — | — | Topic, modes, key, limit |
30
+ | `ChannelMembers` | `channelName` | `connectionId` | — | Roster: membership + prefix flags (op/voice) |
31
+ | `Nicks` | `nickLower` | — | — | Nick → connectionId registry |
32
+ | `Accounts` | `account` | — | — | SASL credentials (scrypt-hashed, never plaintext) |
33
+
34
+ **Concurrency invariants:**
35
+ - **Nick uniqueness:** enforced via conditional `PutItem`
36
+ (`ConditionExpression: attribute_not_exists(nickLower)`). Two racing
37
+ Puts for the same nick — exactly one succeeds; the other gets a
38
+ `ConditionalCheckFailedException` → `433 ERR_NICKNAMEINUSE`.
39
+ - **Membership atomicity:** `TransactWriteItems` spans `Connections`
40
+ (update `joinedChannels`) and `ChannelMembers` (add/remove member) in a
41
+ single atomic transaction. Concurrent JOIN/PART by the same connection
42
+ cannot leave inconsistent state.
43
+ - **Gone connections:** `ApiGatewayManagementApi.postToConnection` throws
44
+ `GoneException` for a stale connection. The handler catches this, triggers
45
+ lazy cleanup (removes the `Connections` row + `ChannelMembers` rows), and
46
+ broadcasts QUIT to peers (TICKET-074). A sweeper Lambda reaps rows whose
47
+ `idleSince` TTL has expired.
48
+ - **Environment isolation:** physical table names are environment-prefixed
49
+ (`<Env>Connections`, e.g. `StagingConnections`) so staging and production
50
+ can coexist in the same account + region (TICKET-066).
51
+
52
+ **SASL credentials** (`Accounts`): stored as scrypt `HashedAccountCredential`
53
+ rows (`{ account, algorithm, salt, hash }`, base64-encoded). The Lambda
54
+ pre-loads all rows at cold start into an in-memory `DynamoAccountStore`; the
55
+ reducer's `AccountStore.verify()` stays synchronous (reducers are pure — no
56
+ async). Seed tooling (`tools/seed-aws-accounts.ts`) writes hashed rows; never
57
+ plaintext.
58
+
59
+ ## Consequences
60
+
61
+ **Positive:**
62
+ - Clear separation of concerns: each table is one authority (matches
63
+ ADR-002). A channel's metadata, membership, and each member's connection
64
+ record are independently addressable.
65
+ - Conditional writes + `TransactWriteItems` give race-free semantics without
66
+ locks — the natural fit for DynamoDB.
67
+ - `idleSince` TTL auto-reaps idle connections even if the sweeper misses a
68
+ tick; the sweeper is a belt-and-suspenders cleanup, not the only path.
69
+ - Pay-per-request billing means idle tables cost nothing; the schema does not
70
+ over-provision.
71
+
72
+ **Negative:**
73
+ - Five tables (vs. a single-table design) means more `TransactWriteItems`
74
+ spanning multiple partitions. `TransactWriteItems` consumes more RCUs than
75
+ individual writes and has a 100-item-per-transaction cap. Membership
76
+ transactions are 2 items (well within limits), but a future feature that
77
+ touches many channels in one command could approach the cap.
78
+ - No cross-table GSI for "all channels a connection is in" — the
79
+ `Connections` row stores `joinedChannels` as a set, which must be updated
80
+ on every JOIN/PART. This duplicates membership data between `Connections`
81
+ and `ChannelMembers`, kept consistent by the transaction.
82
+ - `Accounts` pre-loading at cold start means every Lambda cold start scans
83
+ the table. For deployments with thousands of accounts, this adds latency.
84
+ Mitigated by Lambda reuse (warm starts skip the scan); a cache layer or
85
+ on-demand lookup is a documented follow-up.
86
+
87
+ ## References
88
+
89
+ - PLAN §4 — state model & authoritative ownership
90
+ - PLAN §6.2 — DynamoDB tables
91
+ - `packages/aws-adapter/src/cdk-table-defs.ts` — `TABLE_DEFS` (source of truth)
92
+ - `packages/aws-adapter/src/aws-runtime.ts` — `AwsRuntime` implementation
93
+ - TICKET-039 — CDK stack
94
+ - TICKET-041 — DynamoDB transactions for membership
95
+ - TICKET-066 — environment-name prefixing
96
+ - TICKET-072 — `Accounts` table scrypt-hashed `AccountStore`
@@ -0,0 +1,83 @@
1
+ # ADR-005: wss-only transport in v1 (no raw TCP)
2
+
3
+ **Date:** 2025-01-15 (Phase 1)
4
+ **Status:** Superseded by ADR-009 (Phase 7 — TICKET-052)
5
+ **Supersedes:** —
6
+
7
+ ## Context
8
+
9
+ IRC is traditionally spoken over raw TCP (port 6667 plaintext, 6697 TLS).
10
+ WebSocket Secure (wss) is a framing layer over TLS that adds an HTTP upgrade
11
+ handshake. Serverless compute platforms have fundamentally different support
12
+ for these two transports:
13
+
14
+ - **Cloudflare Workers** can accept WebSocket upgrades but **cannot accept raw
15
+ TCP**. Spectrum (Enterprise tier) can terminate TLS and proxy to an origin,
16
+ but Workers itself is request-response.
17
+ - **AWS API Gateway WebSocket API** provides persistent WebSocket connections
18
+ with per-message Lambda invocation. Raw TCP requires a Network Load Balancer
19
+ + Lambda streaming (invoke mode) — a fundamentally different integration.
20
+
21
+ The v1 goal is a clean serverless story: one codebase, two adapters, zero
22
+ stateful origin servers. Raw TCP on either platform requires additional,
23
+ non-serverless infrastructure (a long-running container on CF; NLB + Lambda
24
+ streaming on AWS).
25
+
26
+ Additionally, wss carries one IRC message per WebSocket text frame, which
27
+ simplifies framing: the server does not need to implement a stateful
28
+ byte-stream line buffer. The parser still tolerates `\r\n`-joined frames
29
+ (multiple messages in one text frame) for robustness, but the common case is
30
+ one-message-per-frame.
31
+
32
+ ## Decision
33
+
34
+ v1 ships **wss-only**. No plaintext TCP listener, no `irc://` scheme, no
35
+ port 6667/6697. Clients connect via `wss://server.tld/` and speak IRC inside
36
+ WebSocket text frames.
37
+
38
+ Rationale:
39
+ 1. Both target platforms support wss natively without additional
40
+ infrastructure.
41
+ 2. wss is TLS-terminated at the edge (CF) or at API Gateway (AWS) — the
42
+ Lambda/Worker never sees plaintext credentials.
43
+ 3. One-message-per-frame eliminates the need for a TCP-style line-framing
44
+ buffer in the core (though the parser tolerates joined frames).
45
+ 4. The serverless cost/ops model is clean: no long-running origin, no TCP
46
+ keepalive management.
47
+
48
+ The `ConnectionActor` transport seam accepts WebSocket text frames directly.
49
+ A `Transport` interface is not abstracted in v1 — the actor's
50
+ `receiveTextFrame(text)` is the only entry point.
51
+
52
+ ## Consequences
53
+
54
+ **Positive:**
55
+ - Zero infrastructure beyond the serverless platform. No TCP listener, no
56
+ origin server, no port management.
57
+ - TLS everywhere — no accidental plaintext exposure.
58
+ - Simple framing: WebSocket text frames are message-delimited by definition.
59
+ - Lower operational burden for v1.
60
+
61
+ **Negative:**
62
+ - RFC 1459/2812 clients that only speak raw TCP cannot connect. WeeChat and
63
+ HexChat support WebSocket; some older clients and bots do not.
64
+ - The WebSocket overhead (HTTP upgrade, per-frame masking) adds ~2–10 bytes
65
+ per message vs. raw TCP. Negligible for IRC's small message sizes.
66
+ - Some IRCv3 extensions assume a TCP transport (e.g. SASL EXTERNAL via client
67
+ certs over TLS passthrough). Without raw TCP, mTLS must be done at the edge
68
+ (CF API Shield, API Gateway custom domain), which changes the cert
69
+ verification surface.
70
+
71
+ **Superseded:** Phase 7 (TICKET-052) reverses this decision for v1.x: a
72
+ dual-transport model (wss stays the default; `irc+tls` on :6697 is added via
73
+ Cloudflare Spectrum + Container origin, and AWS NLB + Lambda streaming). This
74
+ ADR documents the original v1 rationale; the superseding ADR covers the
75
+ reversal and per-platform TCP+TLS mapping.
76
+
77
+ ## References
78
+
79
+ - PLAN §3 — "No plaintext TCP in v1 (wss only)"
80
+ - PLAN §9 — "Framing: wss-only"
81
+ - TICKET-052 — superseding ADR: dual wss + irc+tls
82
+ - `packages/irc-server/src/actor.ts` — `receiveTextFrame(text)` (single
83
+ transport entry point in v1)
@@ -0,0 +1,86 @@
1
+ # ADR-006: SASL mechanism scope (PLAIN only, EXTERNAL deferred)
2
+
3
+ **Date:** 2025-01-20 (Phase 1)
4
+ **Status:** Accepted (EXTERNAL re-evaluation tracked in TICKET-079)
5
+ **Supersedes:** —
6
+ **Superseded by:** —
7
+
8
+ ## Context
9
+
10
+ IRCv3 SASL (`CAP sasl`) lets a client authenticate during registration,
11
+ before the welcome sequence. The server advertises supported mechanisms in
12
+ the `sasl` cap value (e.g. `sasl=PLAIN,EXTERNAL`). Each mechanism has
13
+ different infrastructure requirements:
14
+
15
+ - **PLAIN:** the client sends `AUTHENTICATE PLAIN`, then a base64 payload of
16
+ `\0username\0password`. The server verifies the credentials against an
17
+ `AccountStore`. No client certificates, no TLS-passthrough — works over
18
+ any transport, including wss with edge-terminated TLS.
19
+ - **EXTERNAL:** the server authenticates the client based on an external
20
+ identity, typically a client certificate verified via mutual TLS (mTLS).
21
+ This requires the TLS handshake to present the client cert to the
22
+ application layer. On serverless platforms with edge-terminated TLS
23
+ (Cloudflare, API Gateway), the application does not see the raw client
24
+ cert — it must be configured at the edge (API Shield mTLS, custom-domain
25
+ mTLS trust store). That infrastructure does not exist in v1 (ADR-005:
26
+ wss-only, no raw TCP; mTLS passthrough is Phase 7, TICKET-054).
27
+
28
+ ## Decision
29
+
30
+ v1 ships **SASL PLAIN only** as a functional mechanism. EXTERNAL is
31
+ **recognized but stubbed**: the reducer accepts `AUTHENTICATE EXTERNAL`,
32
+ acknowledges the mechanism, and immediately returns `904 ERR_SASLFAIL` with a
33
+ documented code comment cross-referencing TICKET-054 (mTLS) and TICKET-079
34
+ (decision: implement or stop advertising).
35
+
36
+ The `sasl` cap advertises `PLAIN,EXTERNAL` (both names listed) so clients
37
+ that probe mechanism availability see EXTERNAL as nominally supported. The
38
+ actual `904` on EXTERNAL attempt is the functional gate. TICKET-079 is the
39
+ follow-up that decides whether to implement EXTERNAL against a pluggable
40
+ `MtlsIdentityProvider` or stop advertising it until mTLS lands.
41
+
42
+ `AccountStore` is a port in `irc-core` consumed by the SASL reducer. The
43
+ `verify(mechanism, payload)` method is **synchronous** — adapters pre-load
44
+ credentials at boot so the reducer stays pure (ADR-001). PLAIN verification
45
+ decodes the base64 payload, extracts username + password, and matches against
46
+ the store.
47
+
48
+ ## Consequences
49
+
50
+ **Positive:**
51
+ - PLAIN works end-to-end in v1 with no special infrastructure. The
52
+ `AccountStore` port abstracts the credential store; adapters seed it from
53
+ config (in-memory), DynamoDB (AWS), or a persistent backend (D1/DO for CF,
54
+ TICKET-093).
55
+ - EXTERNAL is forward-compatible: advertising it now means clients that
56
+ prefer EXTERNAL will attempt it; once mTLS lands (Phase 7), the same
57
+ `AUTHENTICATE EXTERNAL` flow succeeds without a client-side change.
58
+ - The synchronous `AccountStore.verify()` keeps the reducer pure and
59
+ deterministic. Async credential lookups (DynamoDB) are resolved at cold
60
+ start or via pre-loading, not inside the reducer.
61
+
62
+ **Negative:**
63
+ - EXTERNAL is advertised but non-functional in v1 — a client that negotiates
64
+ `sasl` and sends `AUTHENTICATE EXTERNAL` gets `904` with no actionable
65
+ error message. This is a poor UX until TICKET-079 resolves (stop-advertising
66
+ is the recommended interim per the ticket).
67
+ - PLAIN sends the password base64-encoded (not encrypted) inside the wss
68
+ frame. wss provides the TLS layer, so the password is encrypted in transit
69
+ — but PLAIN is still weaker than EXTERNAL (no mutual auth).
70
+ - The synchronous `AccountStore` contract means the adapter must pre-load all
71
+ credentials. For deployments with many accounts, cold-start latency grows
72
+ linearly with account count. A lazy/async lookup variant is a documented
73
+ follow-up but would require an async-reducer or effect-based credential
74
+ query — a significant architecture change.
75
+
76
+ ## References
77
+
78
+ - PLAN §3 — "sasl (mechanisms: PLAIN, EXTERNAL reserved for later with mTLS)"
79
+ - PLAN §9 — SASL mechanism scope
80
+ - TICKET-026 — SASL reducer implementation
81
+ - TICKET-071 — AccountStore wiring into ConnectionActor
82
+ - TICKET-072 — AWS DynamoDB AccountStore (scrypt hashing)
83
+ - TICKET-079 — SASL EXTERNAL: implement or stop advertising (interim decision)
84
+ - TICKET-054 — mTLS support (Phase 7, enables functional EXTERNAL)
85
+ - `packages/irc-core/src/commands/sasl.ts` — `authenticateReducer`
86
+ - `packages/irc-core/src/ports.ts` — `AccountStore` port
@@ -0,0 +1,82 @@
1
+ # ADR-007: Deterministic ports for time and ID generation
2
+
3
+ **Date:** 2025-01-15 (Phase 1)
4
+ **Status:** Accepted
5
+ **Supersedes:** —
6
+ **Superseded by:** —
7
+
8
+ ## Context
9
+
10
+ IRC protocol behaviour is time-sensitive: idle/PING timers, WHOIS signon
11
+ time, `server-time` tags (ISO-8601 timestamps on every message), SASL nonce
12
+ generation, chathistory message IDs. A traditional ircd calls `Date.now()`
13
+ and `crypto.randomUUID()` directly in handlers. This makes tests
14
+ non-deterministic: the same input produces different output on every run,
15
+ depending on when the test executed.
16
+
17
+ Non-determinism is fatal for the parametrized contract suite (TICKET-032),
18
+ which runs identical scenarios across in-memory, CF, and AWS runtimes. If a
19
+ reducer embeds `Date.now()`, the expected `@time=` tag changes every
20
+ millisecond and the assertion cannot be written.
21
+
22
+ ## Decision
23
+
24
+ Time and ID generation are **injected ports**, not global calls:
25
+
26
+ ```ts
27
+ interface Clock {
28
+ now(): number; // epoch milliseconds
29
+ }
30
+
31
+ interface IdFactory {
32
+ nonce(): string; // unique message ID for message-tags/chathistory
33
+ traceId(): string; // per-request trace ID for observability
34
+ }
35
+ ```
36
+
37
+ Both are passed through `Ctx` to every reducer. Reducers read `ctx.clock.now()`
38
+ and `ctx.ids.nonce()` — never `Date.now()` or `Math.random()`.
39
+
40
+ Concrete implementations:
41
+ - **`FakeClock`** (tests): returns a fixed value set at construction.
42
+ Deterministic: `new FakeClock(10_000)` always reports `now() === 10_000`.
43
+ - **`SystemClock`** (production): wraps `Date.now()`.
44
+ - **`SequentialIdFactory`** (tests): returns `id-0`, `id-1`, `id-2`, …
45
+ Deterministic: the same sequence of calls produces the same IDs.
46
+ - **`RandomIdFactory`** (production): wraps `crypto.randomUUID()`.
47
+
48
+ ## Consequences
49
+
50
+ **Positive:**
51
+ - Reducer tests are fully deterministic. The same `(state, message, ctx)`
52
+ triple always produces the same `{ state, effects }`. No flaky tests.
53
+ - The contract suite can assert exact wire output including `@time=` tags
54
+ and `msgid=` tags, because the fake clock and ID factory are pinned.
55
+ - Time travel is trivial in tests: advance the `FakeClock` between messages
56
+ to test idle/PING logic, flood-control refill, and chathistory bounds.
57
+ - The reducer purity contract (ADR-001) is enforceable: a reducer that calls
58
+ `Date.now()` is simply not using the injected port, and the test will fail
59
+ because the output is wrong.
60
+
61
+ **Negative:**
62
+ - Every call site that needs time or an ID must thread `ctx` — there is no
63
+ shortcut. This is a small constant cost per reducer.
64
+ - The `Clock` and `IdFactory` must be consistent across the actor's lifetime.
65
+ The actor pins them at construction; switching mid-connection would
66
+ produce inconsistent timestamps. This is enforced by the actor's readonly
67
+ fields.
68
+ - `Date.now()` resolution is milliseconds; sub-millisecond ordering is not
69
+ possible with this port. This has not been a problem for IRC (message
70
+ ordering is preserved by connection/channel authority serialization, not
71
+ by timestamp).
72
+
73
+ ## References
74
+
75
+ - PLAN §9 — "Time/IDs: injected ports (Clock, IdFactory) for deterministic
76
+ tests"
77
+ - PLAN §8 — "Determinism rules: inject a Clock and deterministic ID/nonce
78
+ ports. No real timers/random in reducers."
79
+ - ADR-001 — pure reducers (why determinism matters)
80
+ - `packages/irc-core/src/ports.ts` — `Clock`, `IdFactory`, `FakeClock`,
81
+ `SequentialIdFactory`, `SystemClock`, `RandomIdFactory`
82
+ - `packages/irc-core/src/types.ts` — `Ctx.clock`, `Ctx.ids`
@@ -0,0 +1,60 @@
1
+ # ADR-008: Monorepo and tooling stack
2
+
3
+ **Date:** 2025-01-10 (Phase 0)
4
+ **Status:** Accepted
5
+ **Supersedes:** —
6
+ **Superseded by:** —
7
+
8
+ ## Context
9
+
10
+ ServerlessIRCd is a multi-package TypeScript project: a platform-agnostic
11
+ core (`irc-core`), an orchestration layer (`irc-server`), a reference runtime
12
+ (`in-memory-runtime`), two cloud adapters (`cf-adapter`, `aws-adapter`), and
13
+ several apps (`local-cli`, `cf-worker`, `aws-stack`). The toolchain must
14
+ support cross-package type checking, incremental builds, a unified test
15
+ runner, and CI that enforces coverage gates per package.
16
+
17
+ ## Decision
18
+
19
+ | Concern | Choice | Rationale |
20
+ |---|---|---|
21
+ | Package manager | **pnpm** (workspaces) | Fast, disk-efficient, first-class monorepo support |
22
+ | Build orchestration | **turbo** (caching) | Incremental builds, remote cache, task dependency graph |
23
+ | TypeScript | Project references + `tsconfig.base.json` | Strict mode, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess` |
24
+ | Test runner | **vitest** | ESM-native, fast, integrates with Workers (`vitest-pool-workers`), built-in coverage |
25
+ | Coverage | v8 provider, gate ≥90% (100% on `irc-core`) | Enforced in CI (`.github/workflows/ci.yml`) |
26
+ | Lint/format | **Biome** | Single tool (replaces ESLint + Prettier), fast, zero config drift |
27
+ | CF bundler | **wrangler** (esbuild internally) | Native Workers toolchain |
28
+ | AWS bundler | **esbuild** (Lambda bundles) + **CDK v2** (infra) | Tree-shaking, fast cold starts |
29
+ | Runtime target | Node 20 (Lambda) + latest Workers runtime; ES2022+ | Aligns with platform-supported runtimes |
30
+
31
+ ## Consequences
32
+
33
+ **Positive:**
34
+ - pnpm workspaces + turbo give fast local dev: only changed packages rebuild;
35
+ tests run incrementally.
36
+ - vitest's ESM-native design avoids the CJS/ESM interop pain that plagues
37
+ Jest-based monorepos. The same test files run under `vitest-pool-workers`
38
+ (real `workerd`) for CF adapter tests.
39
+ - Biome eliminates the ESLint/Prettier config drift problem — one tool, one
40
+ config, one command (`biome check`).
41
+ - TypeScript project references enforce package boundaries at compile time:
42
+ `irc-core` cannot accidentally import from `cf-adapter`.
43
+
44
+ **Negative:**
45
+ - pnpm's strict node-linker can surprise contributors used to npm/yarn's
46
+ hoisting. `node_modules` is per-package; global scripts need
47
+ `pnpm --filter`.
48
+ - turbo's caching requires consistent environment hashing; a CI cache miss
49
+ (e.g. different Node minor version) degrades to a full rebuild silently.
50
+ - Biome is newer than ESLint and lacks some ecosystem plugins. Custom rules
51
+ (e.g. "no ticket IDs in source files") are enforced via convention
52
+ (`AGENTS.md`) rather than a Biome plugin.
53
+
54
+ ## References
55
+
56
+ - PLAN §9 — "Monorepo: pnpm + TypeScript project references; turbo for
57
+ caching"
58
+ - `pnpm-workspace.yaml`, `turbo.json`, `tsconfig.base.json`, `biome.json`
59
+ - `.github/workflows/ci.yml` — coverage gates and CI pipeline
60
+ - `AGENTS.md` — code style conventions