serverless-ircd 0.7.0 → 0.9.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 (168) hide show
  1. package/.github/workflows/ci.yml +7 -3
  2. package/.gitmodules +1 -1
  3. package/CHANGELOG.md +518 -29
  4. package/README.md +221 -161
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/aws-stack/src/aws-stack.ts +186 -18
  7. package/apps/aws-stack/tests/stack.test.ts +400 -56
  8. package/apps/cf-tcp-container/package.json +1 -1
  9. package/apps/cf-tcp-container/src/container-server.ts +21 -1
  10. package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
  11. package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
  12. package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
  13. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
  14. package/apps/cf-worker/package.json +1 -1
  15. package/apps/cf-worker/src/worker.ts +4 -4
  16. package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
  17. package/apps/cf-worker/tests/smoke.test.ts +5 -5
  18. package/apps/cf-worker/wrangler.test.toml +6 -6
  19. package/apps/cf-worker/wrangler.toml +7 -5
  20. package/apps/local-cli/package.json +1 -1
  21. package/apps/local-cli/src/config-loader.ts +8 -0
  22. package/apps/local-cli/src/main.ts +16 -0
  23. package/apps/local-cli/src/server.ts +95 -31
  24. package/apps/local-cli/tests/config-loader.test.ts +14 -0
  25. package/apps/local-cli/tests/config-resolution.test.ts +65 -0
  26. package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
  27. package/apps/local-cli/tests/rehash.test.ts +147 -0
  28. package/apps/local-cli/tests/server-helpers.test.ts +63 -0
  29. package/apps/local-cli/tests/tcp.test.ts +89 -0
  30. package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
  31. package/apps/web/landing/index.html +237 -16
  32. package/apps/web/package.json +3 -2
  33. package/apps/web/scripts/build.mjs +46 -16
  34. package/apps/web/src/build-env.ts +1 -1
  35. package/apps/web/src/config-schema.ts +6 -6
  36. package/apps/web/src/render-docs.ts +292 -0
  37. package/apps/web/tests/build-smoke.test.ts +43 -14
  38. package/apps/web/tests/config-schema.test.ts +1 -1
  39. package/apps/web/tests/landing-content.test.ts +103 -0
  40. package/apps/web/tests/render-docs.test.ts +198 -0
  41. package/docs/AWS-Adapter-Architecture.md +3 -2
  42. package/docs/AWS-Deployment.md +21 -8
  43. package/docs/Cloudflare-Deployment-Guide.md +22 -6
  44. package/docs/PlanExtensions.md +113 -3
  45. package/docs/Release-Process.md +23 -13
  46. package/docs/Services.md +578 -0
  47. package/docs/WebClientGuide.md +32 -31
  48. package/package.json +2 -2
  49. package/packages/aws-adapter/package.json +1 -1
  50. package/packages/aws-adapter/src/aws-runtime.ts +20 -1
  51. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
  52. package/packages/aws-adapter/src/config-loader.ts +11 -0
  53. package/packages/aws-adapter/src/connection-counter.ts +89 -0
  54. package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
  55. package/packages/aws-adapter/src/handlers/connect.ts +55 -51
  56. package/packages/aws-adapter/src/handlers/default.ts +36 -4
  57. package/packages/aws-adapter/src/handlers/index.ts +15 -0
  58. package/packages/aws-adapter/src/handlers/nlb-stream.ts +25 -2
  59. package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
  60. package/packages/aws-adapter/src/index.ts +4 -0
  61. package/packages/aws-adapter/src/stats.ts +6 -1
  62. package/packages/aws-adapter/src/tables.ts +34 -4
  63. package/packages/aws-adapter/tests/aws-harness.ts +3 -0
  64. package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
  65. package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
  66. package/packages/aws-adapter/tests/connect.test.ts +158 -32
  67. package/packages/aws-adapter/tests/connection-counter.test.ts +144 -0
  68. package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
  69. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
  70. package/packages/aws-adapter/tests/global-setup.ts +28 -1
  71. package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
  72. package/packages/aws-adapter/tests/handlers.test.ts +105 -3
  73. package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
  74. package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
  75. package/packages/aws-adapter/tests/tables.test.ts +6 -1
  76. package/packages/cf-adapter/package.json +1 -1
  77. package/packages/cf-adapter/src/config-loader.ts +11 -0
  78. package/packages/cf-adapter/src/connection-do.ts +130 -8
  79. package/packages/cf-adapter/src/d1-services-store.ts +703 -0
  80. package/packages/cf-adapter/src/env.ts +8 -0
  81. package/packages/cf-adapter/src/index.ts +5 -0
  82. package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
  83. package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
  84. package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
  85. package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
  86. package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
  87. package/packages/cf-adapter/tests/serialize.test.ts +1 -0
  88. package/packages/in-memory-runtime/package.json +1 -1
  89. package/packages/irc-core/package.json +1 -1
  90. package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
  91. package/packages/irc-core/src/commands/account-auth.ts +200 -0
  92. package/packages/irc-core/src/commands/chanserv.ts +1166 -0
  93. package/packages/irc-core/src/commands/hostserv.ts +522 -0
  94. package/packages/irc-core/src/commands/index.ts +13 -0
  95. package/packages/irc-core/src/commands/join.ts +171 -9
  96. package/packages/irc-core/src/commands/markread.ts +202 -0
  97. package/packages/irc-core/src/commands/memoserv.ts +319 -0
  98. package/packages/irc-core/src/commands/mode.ts +96 -4
  99. package/packages/irc-core/src/commands/nickserv.ts +402 -0
  100. package/packages/irc-core/src/commands/oper.ts +18 -1
  101. package/packages/irc-core/src/commands/operserv.ts +346 -0
  102. package/packages/irc-core/src/commands/pre-away.ts +3 -1
  103. package/packages/irc-core/src/commands/privmsg.ts +42 -0
  104. package/packages/irc-core/src/commands/read-marker.ts +8 -8
  105. package/packages/irc-core/src/commands/registration.ts +78 -12
  106. package/packages/irc-core/src/commands/sasl.ts +18 -49
  107. package/packages/irc-core/src/commands/service-aliases.ts +52 -0
  108. package/packages/irc-core/src/commands/tagmsg.ts +41 -6
  109. package/packages/irc-core/src/commands/topic.ts +60 -10
  110. package/packages/irc-core/src/config.ts +36 -5
  111. package/packages/irc-core/src/effects.ts +56 -1
  112. package/packages/irc-core/src/ports.ts +1653 -84
  113. package/packages/irc-core/src/protocol/numerics.ts +8 -0
  114. package/packages/irc-core/src/state/channel.ts +38 -1
  115. package/packages/irc-core/src/state/connection.ts +25 -1
  116. package/packages/irc-core/src/types.ts +48 -12
  117. package/packages/irc-core/tests/commands/chanserv.test.ts +2335 -0
  118. package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
  119. package/packages/irc-core/tests/commands/hostserv.test.ts +1006 -0
  120. package/packages/irc-core/tests/commands/join.test.ts +572 -1
  121. package/packages/irc-core/tests/commands/markread.test.ts +361 -0
  122. package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
  123. package/packages/irc-core/tests/commands/mode.test.ts +381 -2
  124. package/packages/irc-core/tests/commands/nickserv.test.ts +990 -0
  125. package/packages/irc-core/tests/commands/oper.test.ts +13 -0
  126. package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
  127. package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
  128. package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
  129. package/packages/irc-core/tests/commands/registration.test.ts +1015 -20
  130. package/packages/irc-core/tests/commands/sasl.test.ts +229 -12
  131. package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
  132. package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
  133. package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
  134. package/packages/irc-core/tests/commands/topic.test.ts +94 -2
  135. package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
  136. package/packages/irc-core/tests/config.test.ts +49 -5
  137. package/packages/irc-core/tests/effects.test.ts +19 -0
  138. package/packages/irc-core/tests/message-store.test.ts +63 -0
  139. package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
  140. package/packages/irc-core/tests/services-store.test.ts +1289 -0
  141. package/packages/irc-core/tests/state/channel.test.ts +3 -0
  142. package/packages/irc-server/package.json +1 -1
  143. package/packages/irc-server/src/actor.ts +121 -16
  144. package/packages/irc-server/src/dispatch.ts +94 -7
  145. package/packages/irc-server/src/routing.ts +19 -0
  146. package/packages/irc-server/tests/actor.test.ts +988 -15
  147. package/packages/irc-server/tests/dispatch.test.ts +270 -2
  148. package/packages/irc-server/tests/routing.test.ts +6 -0
  149. package/packages/irc-test-support/package.json +1 -1
  150. package/packages/irc-test-support/src/in-memory-harness.ts +37 -8
  151. package/packages/irc-test-support/src/index.ts +1 -0
  152. package/packages/irc-test-support/src/scenarios.ts +21 -6
  153. package/packages/irc-test-support/tests/in-memory-harness.test.ts +51 -0
  154. package/packages/irc-test-support/vitest.config.ts +6 -1
  155. package/tools/ci-hardening/package.json +1 -1
  156. package/tools/load-test/package.json +1 -1
  157. package/tools/load-test/src/client.ts +13 -13
  158. package/tools/load-test/tests/client.test.ts +258 -2
  159. package/tools/load-test/tests/config.test.ts +39 -0
  160. package/tools/load-test/tests/harness.test.ts +21 -0
  161. package/tools/load-test/tests/metrics.test.ts +7 -0
  162. package/tools/tcp-ws-forwarder/package.json +1 -1
  163. package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
  164. package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
  165. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +53 -2
  166. package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
  167. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
  168. package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
@@ -0,0 +1,578 @@
1
+ # IRC Services Reference
2
+
3
+ Operator and contributor reference for ServerlessIRCd's integrated IRC
4
+ services — **NickServ**, **ChanServ**, **HostServ**, **OperServ**, and
5
+ **MemoServ**. These ship as part of the daemon itself: there is no separate
6
+ services process, no pseudo-server, and no server-to-server (S2S) link.
7
+
8
+ > **Migration caveat (read first).** The integrated-services design is
9
+ > **not binary-compatible with Atheme or Anope.** There is no S2S protocol,
10
+ > so an existing Atheme/Anope services process cannot connect to
11
+ > ServerlessIRCd, and existing services databases cannot be imported.
12
+ > Operators migrating from a services-aware network must **re-register
13
+ > nicks and channels** against the ServerlessIRCd `ServicesStore`. See
14
+ > [§12 Migration notes](#12-migration-notes-from-athemeanope).
15
+
16
+ Cross-reference: `docs/PlanExtensions.md` §6 (architecture decision and
17
+ services scope), `PLAN.md` §1 (S2S is an explicit non-goal), the
18
+ `ServicesStore` port in `packages/irc-core/src/ports.ts`, and the
19
+ per-service reducers in `packages/irc-core/src/commands/`.
20
+
21
+ **Acceptance criterion (TICKET-131):** a new contributor can register a
22
+ NickServ account and a ChanServ channel end-to-end following only this doc.
23
+ See the [walkthrough in §13](#13-end-to-end-walkthrough).
24
+
25
+ ---
26
+
27
+ ## 1. Architecture: integrated services (not pseudo-servers)
28
+
29
+ Two designs exist in the wild:
30
+
31
+ - **Pseudo-client services (the Atheme / Anope model).** A separate process
32
+ connects to the ircd as one or more virtual IRC clients named `NickServ`
33
+ and so on; users interact via `PRIVMSG NickServ :REGISTER …`. The link
34
+ between ircd and services speaks a server-to-server protocol (TS6 / P10 /
35
+ IRCv3 S2S).
36
+ - **Integrated services (this project).** Service names are reserved nicks
37
+ recognized by the daemon at `PRIVMSG` time and routed to dedicated pure
38
+ reducers backed by a `ServicesStore` port. **No S2S, no separate process,
39
+ no pseudo-client connection.**
40
+
41
+ ServerlessIRCd chose **integrated services**. The rationale (full detail in
42
+ `docs/PlanExtensions.md` §6.1):
43
+
44
+ - S2S is an explicit `PLAN.md` §1 non-goal, and the pseudo-client model
45
+ requires it.
46
+ - The pure-reducer + location-of-authority architecture already has the
47
+ right seam: a `ServicesStore` port absorbs the side effects, and reducers
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.
51
+ - The serverless substrates (Cloudflare Durable Objects, AWS DynamoDB)
52
+ handle persistence natively; a separate long-running services process
53
+ would defeat the deployment model.
54
+
55
+ **Trade-off:** integrated services are not binary-compatible with existing
56
+ Atheme/Anope modules. Operators migrating from a services-aware network
57
+ must re-register nicks and channels.
58
+
59
+ ---
60
+
61
+ ## 2. How services are enabled
62
+
63
+ Services are present on a deployment **iff** a `ServicesStore` is injected
64
+ into the actor's `Ctx.services`. Each adapter constructs and binds one when
65
+ its services backing store is configured:
66
+
67
+ | Adapter | Services backend | Wired when |
68
+ |---------|------------------|------------|
69
+ | **Cloudflare** | D1 (`nickserv_accounts`, `chanserv_channels`, …) | the `ACCOUNTS_DB` D1 binding is set — `loadD1ServicesStore()` in `packages/cf-adapter/src/d1-services-store.ts`, invoked from the connection Durable Object |
70
+ | **AWS** | DynamoDB (single services table, partitioned) | a services table name is supplied — `loadDynamoServicesStore()` in `packages/aws-adapter/src/dynamo-services-store.ts` |
71
+ | **in-memory / local CLI / tests** | `InMemoryServicesStore` (`Map`s) | always, for the local CLI and unit tests |
72
+
73
+ Both D1 and DynamoDB backends are **write-behind**: the store loads a
74
+ snapshot once per cold start into a synchronously-readable cache (reducers
75
+ must stay pure and free of IO), and flushes writes back to the durable
76
+ table. A redeploy/reconnect rehydrates from the persisted snapshot, so
77
+ registrations and channel access lists survive.
78
+
79
+ If no store is bound, every services command replies:
80
+ ```
81
+ :NickServ!NickServ@services NOTICE <nick> :Services are not available on this server.
82
+ ```
83
+
84
+ ### Config knobs (`packages/irc-core/src/config.ts`)
85
+
86
+ - **`servicesEnabled`** (`boolean`, optional) — currently gates only the
87
+ `ACCOUNTEXTBAN=a` ISUPPORT token advertised by `005`. It does **not**
88
+ gate the reducers; those are live whenever a store is injected.
89
+ - **`nickEnforceGraceMs`** (`integer ≥ 0`, optional) — grace period, in
90
+ milliseconds, NickServ waits before force-disconnecting a `ghost`-enforced
91
+ client. `0` makes `ghost` behave like `kill` (immediate). Omit for the
92
+ **30 000 ms** default (`DEFAULT_NICK_ENFORCE_GRACE_MS`).
93
+
94
+ ---
95
+
96
+ ## 3. Conventions
97
+
98
+ Every service command is sent as a `PRIVMSG` whose target is the service
99
+ nick and whose trailing parameter is the subcommand and arguments:
100
+
101
+ ```
102
+ PRIVMSG <Service> :<subcommand> [args…]
103
+ ```
104
+
105
+ All service replies are `NOTICE`s from a fixed hostmask independent of the
106
+ deployment's `serverName`:
107
+
108
+ ```
109
+ :<Service>!<Service>@services NOTICE <nick> :<text>
110
+ ```
111
+
112
+ Notable conventions:
113
+
114
+ - **`PRIVMSG` only.** Services are routed only for `PRIVMSG`. A `NOTICE`
115
+ or `TAGMSG` to a service nick is not intercepted and falls through to the
116
+ normal nick-target path.
117
+ - **Identification is the `+r` user mode.** A connection is "identified"
118
+ when NickServ `IDENTIFY` (or a SASL login) has set `state.account`. Many
119
+ commands require this.
120
+ - **Case-folding.** All nicks, accounts, and channel keys in the store are
121
+ **rfc1459 case-folded** (advertised via `005 CASEMAPPING=rfc1459`);
122
+ display spellings are preserved on returned records.
123
+ - **Numerics vs NOTICEs.** Services emit **no** success/error numerics of
124
+ their own — feedback is always a `NOTICE` from the pseudo-client. The
125
+ only numeric emissions tied to services are enforcement/gate codes:
126
+ `519` (ChanServ `+R` JOIN gate), `432` (OperServ JUPE), `404` (ChanServ
127
+ `+R`/`+M` send gates), `472` (services-only channel-mode letters).
128
+
129
+ ### Shortcut aliases (`/NICKSERV`, `/NS`, `/CS`, …)
130
+
131
+ For compatibility with the Atheme/Anope services convention (and IRCd
132
+ alias modules such as InspIRCd `m_alias` / UnrealIRCd `alias{}`), every
133
+ service also accepts a **shortcut verb** that the daemon rewrites to the
134
+ canonical `PRIVMSG <ServiceNick> :<joined args>` before routing. These are
135
+ not RFC-standardized but are widely expected by clients and users.
136
+
137
+ | Shortcut verb(s) | Equivalent to |
138
+ |---|---|
139
+ | `NICKSERV`, `NS` | `PRIVMSG NickServ :<args>` |
140
+ | `CHANSERV`, `CS` | `PRIVMSG ChanServ :<args>` |
141
+ | `HOSTSERV`, `HS` | `PRIVMSG HostServ :<args>` |
142
+ | `MEMOSERV`, `MS` | `PRIVMSG MemoServ :<args>` |
143
+ | `OPERSERV`, `OS` | `PRIVMSG OperServ :<args>` |
144
+
145
+ The alias verb is the first token; everything after it is joined with
146
+ single spaces and becomes the `PRIVMSG` trailing body, so e.g.
147
+ `NICKSERV IDENTIFY hunter2` behaves identically to
148
+ `PRIVMSG NickServ :IDENTIFY hunter2`, and `NS REGISTER pw email` is the
149
+ same as `PRIVMSG NickServ :REGISTER pw email`. A colon-trailing form
150
+ (`NS :IDENTIFY hunter2`) is accepted too. The verbs are matched
151
+ case-insensitively. The alias → service nick table is the single source
152
+ of truth in `packages/irc-core/src/commands/service-aliases.ts`
153
+ (`SHORTCUT_TO_SERVICE`); both the dispatcher rewrite and the service-nick
154
+ routing consult it, so no service-reducer changes were needed.
155
+
156
+ When no `ServicesStore` is bound, a shortcut behaves exactly like a direct
157
+ `PRIVMSG` to the service nick — it falls through to the normal nick-target
158
+ path (a `401 ERR_NOSUCHNICK` for the unrecognised pseudo-client), never a
159
+ bare `421 ERR_UNKNOWNCOMMAND`.
160
+
161
+ ---
162
+
163
+ ## 4. NickServ — nick registration & identification
164
+
165
+ Source: `packages/irc-core/src/commands/nickserv.ts`. Wire nick `NickServ`.
166
+
167
+ NickServ owns nick registration, identification (`+r`), nick-enforcement
168
+ policy, and nick-info queries. A registered nick is an account.
169
+
170
+ ### Commands
171
+
172
+ | Command | Syntax | Notes |
173
+ |---|---|---|
174
+ | `REGISTER` | `PRIVMSG NickServ :REGISTER <password> <email>` | Registers the *current* nick. Does **not** auto-identify (no `+r` set). Reply: `Nickname <nick> is now registered.` Already-registered → `Nickname <nick> is already registered.` |
175
+ | `IDENTIFY` | `PRIVMSG NickServ :IDENTIFY [nick] <password>` | One-arg form identifies the current nick; two-arg form identifies a named nick. On success: sets `state.account`, sets user mode **`+r`**, fans `ACCOUNT` to `account-notify` peers, then delivers unread MemoServ memos. Reply: `You are now identified for nick <account>.` |
176
+ | `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
+ | `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
+ | `SET ENFORCE` | `PRIVMSG NickServ :SET ENFORCE none|ghost|kill` | Requires identify as the owning account. See enforcement below. |
179
+
180
+ Help / unknown command: `Available commands: REGISTER, IDENTIFY, DROP, INFO, SET`.
181
+
182
+ ### Nick enforcement
183
+
184
+ `SET ENFORCE <policy>` decides what happens when an **unidentified** client
185
+ uses a registered nick (checked at welcome completion and on every `NICK`
186
+ change):
187
+
188
+ - `none` (default) — warning NOTICE only:
189
+ `This nickname is registered and protected. If it is yours, IDENTIFY; otherwise choose another.`
190
+ - `ghost` — force-disconnect the unidentified client after the grace period
191
+ (`nickEnforceGraceMs`, default 30 s); `0` makes it immediate.
192
+ - `kill` — force-disconnect **immediately** with reason
193
+ `Nick enforced by NickServ`.
194
+
195
+ > **Note on `IDENTIFY` vs SASL.** Both set `state.account` and the `+r`
196
+ > user mode and broadcast `ACCOUNT`. They differ in what else runs: SASL
197
+ > success additionally seeds persisted read-markers and replays persisted
198
+ > away state; NickServ `IDENTIFY` additionally delivers unread memos. The
199
+ > two paths are otherwise equivalent account-setting entry points.
200
+
201
+ ---
202
+
203
+ ## 5. ChanServ — channel registration & access
204
+
205
+ Source: `packages/irc-core/src/commands/chanserv.ts`. Wire nick `ChanServ`.
206
+ ChanServ-emitted `MODE` lines use hostmask `ChanServ!ChanServ@services`.
207
+
208
+ ChanServ owns channel registration, founder/set management, the access list
209
+ (SOP/AOP/HOP/VOP via `ACCESS` and shorthand), configurable level
210
+ thresholds, and the `+r`/`+R`/`+M` channel modes.
211
+
212
+ ### Registration & info
213
+
214
+ | Command | Syntax | Notes |
215
+ |---|---|---|
216
+ | `REGISTER` | `PRIVMSG ChanServ :REGISTER <#channel>` | Requires identify (founder = `state.account`). Sets channel mode **`+r`**. Reply: `Channel <channel> is now registered.` |
217
+ | `DROP` | `PRIVMSG ChanServ :DROP <#channel>` | Founder-only. Clears `+r`/`+R`/`+M`. Reply: `Channel <channel> has been dropped.` |
218
+ | `INFO` | `PRIVMSG ChanServ :INFO <#channel>` | Replies: `Channel:`, `Founder:`. |
219
+
220
+ ### `SET` (founder-only)
221
+
222
+ All `SET` subcommands require the caller be identified as the channel
223
+ founder.
224
+
225
+ | Subcommand | Syntax | Effect |
226
+ |---|---|---|
227
+ | `FOUNDER` | `SET FOUNDER <#channel> <account>` | Transfers founder to a registered nick. |
228
+ | `MLOCK` | `SET MLOCK <#channel> <modestring>` | Mode lock. Allowed letters: `i t n m s p r R M k l o v b` (optional leading `+`/`-`). An op later unsetting a positively-locked boolean letter triggers ChanServ to reassert it (`:ChanServ!ChanServ@services MODE <chan> +<letters>`). `*` or absent clears the lock. |
229
+ | `RESTRICTED` | `SET RESTRICTED <#channel> ON|OFF` | Toggles channel mode **`+R`** (block unidentified). |
230
+ | `KEEPTOPIC` | `SET KEEPTOPIC <#channel> ON|OFF` | Persists the topic; restores it when the channel is recreated after going empty. |
231
+
232
+ ### Access list (SOP / AOP / HOP / VOP)
233
+
234
+ `ACCESS <#channel> ADD|DEL <account>` and the shorthand verbs
235
+ `SOP`/`AOP`/`HOP`/`VOP` (each `ADD|DEL|LIST`) map an account to a privilege
236
+ level. `ADD`/`DEL` are founder-only; `LIST` is read-only.
237
+
238
+ Default shorthand levels and the auto-prefix they grant on `JOIN`:
239
+
240
+ | Verb | Level | Auto-prefix on JOIN |
241
+ |---|---|---|
242
+ | `SOP` | 10 | `@` (op) |
243
+ | `AOP` | 5 (`AUTOOP`) | `@` (op) |
244
+ | `HOP` | 4 (`AUTOHALFOP`) | (halfop, where represented) |
245
+ | `VOP` | 3 (`AUTOVOICE`) | `+` (voice) |
246
+
247
+ A level **≥ `AUTOOP`** grants op; **≥ `AUTOVOICE`** grants voice; below
248
+ that, no prefix. The channel **founder** always receives `@` on join. The
249
+ auto-prefix is broadcast as `:ChanServ!ChanServ@services MODE <chan> +o|+v <nick>`
250
+ so all clients update their display. (The first joiner to an empty channel
251
+ gets `@` by RFC 2812, bypassing ChanServ.)
252
+
253
+ ### `LEVELS` (founder-only for `SET`/`RESET`)
254
+
255
+ `LEVELS <#channel> SET|LIST|RESET` redefines the numeric threshold each
256
+ auto-prefix op requires (`AUTOOP`, `AUTOHALFOP`, `AUTOVOICE`). `LIST` is
257
+ read-only and falls back to the defaults when no override is set.
258
+
259
+ ### Channel modes introduced by ChanServ
260
+
261
+ | Mode | Meaning | Set by |
262
+ |---|---|---|
263
+ | `+r` | Channel is registered | ChanServ `REGISTER` |
264
+ | `+R` | Block join/message from unidentified nicks | `SET RESTRICTED ON` |
265
+ | `+M` | Moderated-identified: only identified (or op/voice) may speak | (cleared on `DROP`; not directly set by a ChanServ command) |
266
+
267
+ These three letters are **services-only**: a user `MODE` attempt to set
268
+ them returns `472 ERR_UNKNOWNMODE`. Their enforcement:
269
+
270
+ - `+R` **JOIN gate:** unidentified joiner →
271
+ `519 ERR_ALLMUSTREGISTER <chan> :You must register with NickServ to join this channel`.
272
+ - `+R` / `+M` **PRIVMSG gate:** unidentified sender (and, for `+M`, any
273
+ non-op/non-voice) → `404 ERR_CANNOTSENDTOCHAN`.
274
+
275
+ ---
276
+
277
+ ## 6. HostServ — virtual hosts
278
+
279
+ Source: `packages/irc-core/src/commands/hostserv.ts`. Wire nick `HostServ`.
280
+
281
+ HostServ manages per-account virtual hosts (vhosts), layered on the
282
+ existing cloak machinery. Toggling a vhost mutates the connection's host
283
+ and broadcasts an IRCv3 `CHGHOST` (`chghost` cap) to every shared-channel
284
+ peer.
285
+
286
+ | Command | Syntax | Gating | Notes |
287
+ |---|---|---|---|
288
+ | `ON` | `PRIVMSG HostServ :ON` | identified | Activates the account's assigned vhost; `CHGHOST` broadcast. Reply: `Your vhost is now active.` |
289
+ | `OFF` | `PRIVMSG HostServ :OFF` | identified | Restores the real (cloaked) host; `CHGHOST` broadcast. Reply: `Your vhost has been disabled.` |
290
+ | `REQUEST` | `PRIVMSG HostServ :REQUEST <vhost>` | identified | Behaviour depends on `hostservAutoApproveVhosts` (see below). Auto-approve mode: records the vhost immediately; reply `Vhost <vhost> has been requested and approved.` Queue mode: records a pending request; reply `Vhost <vhost> is pending oper approval.` The vhost must match a DNS-shape grammar (labels ≤63 chars, ≤253 total). |
291
+ | `SET` | `PRIVMSG HostServ :SET <account> <vhost>` | **oper-only** | Records a vhost directly against a registered nick. `*` clears. Not an oper → `You must be an oper to use SET.` |
292
+ | `APPROVE` (alias `ACTIVATE`) | `PRIVMSG HostServ :APPROVE <account>` | **oper-only** | Activates the pending vhost request recorded via queue-mode `REQUEST`. Calls `setVhost` and replies `Vhost <vhost> for <account> has been approved and activated.` A best-effort NOTICE is sent to the requester via `SendToNick` when they are online, telling them to run `/msg HostServ ON` to apply the vhost to their active connection. |
293
+ | `REJECT` | `PRIVMSG HostServ :REJECT <account>` | **oper-only** | Drops the pending vhost request without activating it. Replies `Vhost request for <account> has been rejected.` and best-effort NOTICEs the requester via `SendToNick` when they are online. |
294
+ | `LIST` | `PRIVMSG HostServ :LIST` | **oper-only** | Enumerates every pending request as `<account>: <vhost>` NOTICE lines. Empty queue → `No pending vhost requests.` |
295
+
296
+ ### Approval mode (`hostservAutoApproveVhosts`)
297
+
298
+ The `ServerConfig.hostservAutoApproveVhosts` knob (boolean, default
299
+ `false`) selects how `REQUEST <vhost>` is handled:
300
+
301
+ - **`true` (auto-approve):** the legacy behaviour. `REQUEST`
302
+ records the vhost against the account immediately via `setVhost`. The
303
+ user can then `/msg HostServ ON` to apply it. `APPROVE`/`REJECT`/`LIST`
304
+ are still wired but only relevant for any pre-existing pending requests.
305
+ - **`false` (default — oper queue):** `REQUEST` records a **pending**
306
+ request via `ServicesStore.requestVhost` and replies that the request
307
+ is pending oper approval. The vhost is NOT activated and no `CHGHOST`
308
+ is emitted until an oper runs `APPROVE <account>`. `REJECT <account>`
309
+ drops the request; `LIST` enumerates the queue.
310
+
311
+ The queue + the oper commands are available in either mode; only
312
+ `REQUEST`'s routing through them is gated on the knob. The default
313
+ (`false`) matches the Atheme/Anope convention of requiring oper review
314
+ for vhosts; set `true` to restore the legacy self-service auto-approve.
315
+
316
+ > **Pending-request delivery note.** When an oper `APPROVE`s or `REJECT`s
317
+ > a pending request and the requester is online, HostServ delivers a
318
+ > NOTICE via the actor layer's `SendToNick` (silently dropped when the
319
+ > requester is offline). The actual `CHGHOST` application on the
320
+ > requester's live connection is **not** done from the oper's reducer —
321
+ > the pure reducer model forbids cross-connection state mutation — so
322
+ > the requester must run `/msg HostServ ON` themselves to apply an
323
+ > approved vhost to their active connection. The NOTICE tells them so.
324
+
325
+ ---
326
+
327
+ ## 7. OperServ — network bans, jupes, raw (oper-only)
328
+
329
+ Source: `packages/irc-core/src/commands/operserv.ts`. Wire nick `OperServ`.
330
+
331
+ > **Routing.** `PRIVMSG OperServ` is routed to the OperServ reducer in the
332
+ > actor's target routing (alongside NickServ, ChanServ, HostServ, and
333
+ > MemoServ). A client `PRIVMSG OperServ :<command>` reaches the reducer
334
+ > instead of falling through to the nick-target path. Interception is
335
+ > **PRIVMSG-only**: `NOTICE`/`TAGMSG OperServ` are not intercepted and fall
336
+ > through to the normal user-target path, matching the other four services.
337
+ >
338
+ > **Enforcement hooks** (unchanged) are wired through the registration
339
+ > reducer:
340
+ > - **AKILL** — checked at welcome completion against the connecting
341
+ > hostmask; a match disconnects with reason `AKILL: network-wide ban`
342
+ > after a `NOTICE` from `OperServ!OperServ@services`.
343
+ > - **JUPE** — checked on every `NICK` attempt; a juped nick is rejected
344
+ > with `432 ERR_ERRONEUSNICKNAME`.
345
+ >
346
+ > The command surface below (AKILL/JUPE/RAW management) is oper-gated and
347
+ > now reachable by clients via `PRIVMSG OperServ`. A non-oper receives
348
+ > `Permission denied — oper privileges required.` with no state change.
349
+
350
+ Command surface (oper-only, reachable via `PRIVMSG OperServ`):
351
+
352
+ | Command | Syntax | Notes |
353
+ |---|---|---|
354
+ | `AKILL ADD` | `PRIVMSG OperServ :AKILL ADD <mask> <reason>` | Records a network-wide ban; matched at connect time. Re-adding a mask replaces the reason. |
355
+ | `AKILL DEL` | `PRIVMSG OperServ :AKILL DEL <mask>` | Removes a mask. |
356
+ | `AKILL LIST` | `PRIVMSG OperServ :AKILL LIST` | Lists `<mask> — <reason>`. |
357
+ | `JUPE` | `PRIVMSG OperServ :JUPE <nick> [reason]` | Blocks a nick/server name locally. |
358
+ | `UNJUPE` | `PRIVMSG OperServ :UNJUPE <nick>` | Removes a jupe. |
359
+ | `RAW` | `PRIVMSG OperServ :RAW <line>` | Emits a raw IRC line (recovery use only). Oper-only and logged. |
360
+
361
+ Every subcommand requires oper privileges; non-opers get
362
+ `Permission denied — oper privileges required.` with no state change.
363
+
364
+ ---
365
+
366
+ ## 8. MemoServ — offline messages
367
+
368
+ Source: `packages/irc-core/src/commands/memoserv.ts`. Wire nick `MemoServ`.
369
+
370
+ MemoServ queues messages for a registered nick and delivers them when the
371
+ recipient next identifies (NickServ `IDENTIFY`) or logs in via SASL. All
372
+ commands require identification. Memos are delivered as
373
+ `:MemoServ!MemoServ@services NOTICE <nick> :[Memo #<id> from <from>] <body>`.
374
+
375
+ | Command | Syntax | Notes |
376
+ |---|---|---|
377
+ | `SEND` | `PRIVMSG MemoServ :SEND <nick> <message>` | Records a memo against the recipient's account (recipient must be a registered nick). The body may contain spaces. Queued; delivered on the recipient's next identify/SASL login. |
378
+ | `LIST` | `PRIVMSG MemoServ :LIST` | Lists **unread** memos: `#<id> <from>: <body>`. |
379
+ | `READ` | `PRIVMSG MemoServ :READ <n>` | Displays memo `#n` and marks it read. |
380
+ | `DEL` | `PRIVMSG MemoServ :DEL <n>` | Deletes memo `#n`. |
381
+
382
+ ---
383
+
384
+ ## 9. The `ServicesStore` port
385
+
386
+ The services side effects are absorbed by a single synchronous port in
387
+ `packages/irc-core/src/ports.ts` (synchronous because reducers are pure and
388
+ free of IO; adapters pre-load a snapshot and flush writes asynchronously).
389
+ The reference implementation `InMemoryServicesStore` ships in the same
390
+ file; D1 and DynamoDB backends live in their adapter packages.
391
+
392
+ Method groups (signatures abbreviated; see `ports.ts` for canonical forms):
393
+
394
+ - **NickServ** — `registerNick`, `verifyNick`, `dropNick`, `isRegisteredNick`,
395
+ `setNickEnforce`/`getNickEnforce`, `getNick`.
396
+ - **ChanServ (registration)** — `registerChannel`, `dropChannel`,
397
+ `getChannelFounder`, `setChannelAccess`/`getChannelAccess`/`listChannelAccess`.
398
+ - **ChanServ (levels)** — `getChannelLevel`/`setChannelLevel`/
399
+ `listChannelLevels`/`resetChannelLevels`.
400
+ - **ChanServ (record)** — `getChannel`, `setChannelFounder`,
401
+ `setChannelMlock`/`getChannelMlock`, `setChannelRestricted`,
402
+ `setChannelKeepTopic`, `setChannelTopic`/`getChannelTopic`.
403
+ - **HostServ** — `setVhost`/`getVhost`, plus the pending-vhost queue
404
+ (`requestVhost`/`getPendingVhost`/`listPendingVhosts`/`approveVhost`/
405
+ `rejectVhost`) consumed by queue-mode `REQUEST` and the oper
406
+ `APPROVE`/`REJECT`/`LIST` commands.
407
+ - **MemoServ** — `recordMemo`, `listMemos`, `markMemoRead`, `deleteMemo`.
408
+ - **OperServ** — `addAkill`/`removeAkill`/`listAkills`/`matchAkill`,
409
+ `addJupe`/`removeJupe`/`isJuped`/`getJupe`/`listJupes`.
410
+ - **Read-marker (IRCv3 `draft/read-marker`)** — `getLastReadMarker`/
411
+ `setLastReadMarker`/`listReadMarkers`.
412
+
413
+ Password comparison is constant-time. All nick/account/channel keys are
414
+ rfc1459 case-folded.
415
+
416
+ ---
417
+
418
+ ## 10. Admin tooling & seeding accounts
419
+
420
+ Two seed scripts ship under `tools/` for **SASL accounts** (the
421
+ scrypt-hashed `accounts`/`Accounts` store used by `AUTHENTICATE PLAIN`):
422
+
423
+ - `tools/seed-cf-accounts.ts` — writes to a Cloudflare D1 `accounts` table
424
+ via `wrangler d1 execute`, scrypt-hashing each password.
425
+ - `tools/seed-aws-accounts.ts` — writes to a DynamoDB `Accounts` table via
426
+ the AWS SDK, scrypt-hashing each password.
427
+
428
+ Both accept `--accounts alice:s3cret bob:pw …` or `--file accounts.txt`
429
+ (newline-delimited `username:password`).
430
+
431
+ ```
432
+ # Cloudflare
433
+ node --import tsx tools/seed-cf-accounts.ts \
434
+ --database serverless-ircd-accounts-staging --env staging --remote \
435
+ --accounts alice:s3cret
436
+
437
+ # AWS
438
+ node --import tsx tools/seed-aws-accounts.ts \
439
+ --table StagingAccounts --endpoint http://localhost:8000 \
440
+ --accounts alice:s3cret
441
+ ```
442
+
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.
455
+
456
+ Operators can manage AKILL/JUPE entries via `PRIVMSG OperServ` (oper-only,
457
+ see [§7](#7-operserv--network-bans-jupes-raw-oper-only)), or pre-populate
458
+ them by writing directly into the `ServicesStore` backing tables.
459
+
460
+ ---
461
+
462
+ ## 11. Security characteristics & known gaps
463
+
464
+ Be aware of the following when operating or contributing to services:
465
+
466
+ - **NickServ passwords are stored scrypt-hashed** in the `ServicesStore`
467
+ (the same scrypt path the SASL `AccountStore` uses:
468
+ `InMemoryServicesStore.registerNick` hashes via `hashAccountCredential`
469
+ before persisting; `verifyNick` re-derives via `verifyHashedPassword`;
470
+ 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
474
+ sensitive regardless: restrict access and enable at-rest encryption
475
+ (DynamoDB encryption-at-rest is on by default).
476
+ - **HostServ `REQUEST` approval mode is configurable** — the default
477
+ (`hostservAutoApproveVhosts: false`) queues every request for oper
478
+ review via the `APPROVE`/`REJECT`/`LIST` commands (see [§6](#6-hostserv--virtual-hosts)).
479
+ Set `hostservAutoApproveVhosts: true` to restore the legacy
480
+ self-service auto-approve, where any identified user can self-assign a
481
+ DNS-shape vhost.
482
+ - **OperServ management is oper-gated** — every `PRIVMSG OperServ`
483
+ subcommand (AKILL/JUPE/RAW) requires oper privileges; non-opers receive
484
+ `Permission denied — oper privileges required.` with no state change
485
+ (see [§7](#7-operserv--network-bans-jupes-raw-oper-only)).
486
+ - **Memo bodies** are stored and replayed verbatim; no size cap beyond the
487
+ general IRC line-length budget applied at the wire.
488
+ - **`+r` user mode** is set by NickServ `IDENTIFY` and SASL; it is not set
489
+ by the legacy `PASS <nick>:<password>` form (that path is tracked
490
+ separately).
491
+
492
+ ---
493
+
494
+ ## 12. Migration notes (from Atheme/Anope)
495
+
496
+ - **No S2S, no services process.** An existing Atheme/Anope instance cannot
497
+ attach to ServerlessIRCd. Plan to **re-register** nicks and channels.
498
+ - **Re-register nicks** with `PRIVMSG NickServ :REGISTER <password> <email>`
499
+ on the new server. There is no import path from Atheme/Anope databases.
500
+ - **Re-register channels** with `ChanServ REGISTER` from the identified
501
+ founder, then re-create the access list via `ACCESS`/`SOP`/`AOP`/`HOP`/
502
+ `VOP`. MLOCK, RESTRICTED, and KEEPTOPIC must be re-applied with `SET`.
503
+ - **OperServ parity is complete.** AKILL (connect-time enforcement) and
504
+ JUPE (nick-time enforcement) are live, and the AKILL/JUPE/RAW management
505
+ commands are client-reachable via `PRIVMSG OperServ` (oper-only, see
506
+ [§7](#7-operserv--network-bans-jupes-raw-oper-only)).
507
+ - **HostServ vhost approval.** The default
508
+ (`hostservAutoApproveVhosts: false`) matches the Atheme/Anope
509
+ convention: `REQUEST` queues for oper `APPROVE`/`REJECT`/`LIST`
510
+ (see [§6](#6-hostserv--virtual-hosts)). Set `hostservAutoApproveVhosts: true`
511
+ to restore self-service auto-approve.
512
+ - **Out of scope** (will not migrate): `BotServ` (channel bots are a
513
+ runtime cost serverless deployments should not absorb — run your own bot
514
+ as an ordinary client) and `StatServ` (covered by `STATS` and the
515
+ observability stack). Grouped/linked nicks and fantasy commands are also
516
+ out of scope for this pass.
517
+
518
+ ---
519
+
520
+ ## 13. End-to-end walkthrough
521
+
522
+ This satisfies the TICKET-131 acceptance criterion: register a NickServ
523
+ account and a ChanServ channel using only this doc. Connect to a
524
+ services-enabled deployment with any IRC client and register first:
525
+
526
+ ```
527
+ NICK alice
528
+ USER alice 0 * :Alice
529
+ PRIVMSG NickServ :REGISTER hunter2 alice@example.com
530
+ ```
531
+ ```
532
+ :NickServ!NickServ@services NOTICE alice :Nickname alice is now registered.
533
+ ```
534
+
535
+ The nick is registered but not yet identified. Identify to receive the `+r`
536
+ user mode (required before registering a channel or sending memos):
537
+
538
+ ```
539
+ PRIVMSG NickServ :IDENTIFY alice hunter2
540
+ ```
541
+ ```
542
+ :NickServ!NickServ@services NOTICE alice :You are now identified for nick alice.
543
+ ```
544
+
545
+ Now register a channel (you are the founder):
546
+
547
+ ```
548
+ JOIN #mychannel
549
+ PRIVMSG ChanServ :REGISTER #mychannel
550
+ ```
551
+ ```
552
+ :ChanServ!ChanServ@services NOTICE alice :Channel #mychannel is now registered.
553
+ :ChanServ!ChanServ@services MODE #mychannel +r
554
+ ```
555
+
556
+ Add a friend (who has already registered their nick `bob`) to the access
557
+ list as an auto-op, lock the topic, and restrict the channel to identified
558
+ users:
559
+
560
+ ```
561
+ PRIVMSG ChanServ :AOP #mychannel ADD bob
562
+ PRIVMSG ChanServ :SET KEEPTOPIC #mychannel ON
563
+ PRIVMSG ChanServ :SET RESTRICTED #mychannel ON
564
+ ```
565
+ ```
566
+ :ChanServ!ChanServ@services NOTICE alice :Bob added to the access list for #mychannel at level 5.
567
+ :ChanServ!ChanServ@services NOTICE alice :Keep-topic for #mychannel is now set to ON.
568
+ :ChanServ!ChanServ@services NOTICE alice :Restricted (block unidentified) for #mychannel is now set to ON.
569
+ ```
570
+
571
+ When `bob` later identifies and joins, ChanServ auto-ops them:
572
+
573
+ ```
574
+ :ChanServ!ChanServ@services MODE #mychannel +o bob
575
+ ```
576
+
577
+ That completes the core nick + channel registration flow. From here, the
578
+ per-service references in §4–§8 cover every shipped command.