serverless-ircd 0.7.0 → 0.8.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 (135) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/.gitmodules +1 -1
  3. package/CHANGELOG.md +273 -29
  4. package/README.md +155 -55
  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-worker/package.json +1 -1
  10. package/apps/cf-worker/src/worker.ts +4 -4
  11. package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
  12. package/apps/cf-worker/tests/smoke.test.ts +5 -5
  13. package/apps/cf-worker/wrangler.test.toml +6 -6
  14. package/apps/cf-worker/wrangler.toml +7 -5
  15. package/apps/local-cli/package.json +1 -1
  16. package/apps/local-cli/src/config-loader.ts +8 -0
  17. package/apps/local-cli/src/main.ts +16 -0
  18. package/apps/local-cli/src/server.ts +1 -0
  19. package/apps/local-cli/tests/config-loader.test.ts +14 -0
  20. package/apps/web/landing/index.html +14 -16
  21. package/apps/web/package.json +2 -2
  22. package/apps/web/scripts/build.mjs +23 -16
  23. package/apps/web/src/build-env.ts +1 -1
  24. package/apps/web/src/config-schema.ts +6 -6
  25. package/apps/web/tests/build-smoke.test.ts +14 -14
  26. package/apps/web/tests/config-schema.test.ts +1 -1
  27. package/docs/AWS-Deployment.md +21 -8
  28. package/docs/Cloudflare-Deployment-Guide.md +22 -6
  29. package/docs/PlanExtensions.md +113 -3
  30. package/docs/Release-Process.md +23 -13
  31. package/docs/Services.md +546 -0
  32. package/docs/WebClientGuide.md +32 -31
  33. package/package.json +2 -2
  34. package/packages/aws-adapter/package.json +1 -1
  35. package/packages/aws-adapter/src/aws-runtime.ts +5 -0
  36. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
  37. package/packages/aws-adapter/src/config-loader.ts +11 -0
  38. package/packages/aws-adapter/src/connection-counter.ts +89 -0
  39. package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
  40. package/packages/aws-adapter/src/handlers/connect.ts +55 -51
  41. package/packages/aws-adapter/src/handlers/default.ts +36 -4
  42. package/packages/aws-adapter/src/handlers/index.ts +15 -0
  43. package/packages/aws-adapter/src/handlers/nlb-stream.ts +15 -0
  44. package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
  45. package/packages/aws-adapter/src/index.ts +4 -0
  46. package/packages/aws-adapter/src/stats.ts +6 -1
  47. package/packages/aws-adapter/src/tables.ts +34 -4
  48. package/packages/aws-adapter/tests/aws-harness.ts +3 -0
  49. package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
  50. package/packages/aws-adapter/tests/connect.test.ts +158 -32
  51. package/packages/aws-adapter/tests/connection-counter.test.ts +127 -0
  52. package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
  53. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
  54. package/packages/aws-adapter/tests/handlers.test.ts +105 -3
  55. package/packages/aws-adapter/tests/tables.test.ts +6 -1
  56. package/packages/cf-adapter/package.json +1 -1
  57. package/packages/cf-adapter/src/config-loader.ts +11 -0
  58. package/packages/cf-adapter/src/connection-do.ts +112 -2
  59. package/packages/cf-adapter/src/d1-services-store.ts +703 -0
  60. package/packages/cf-adapter/src/env.ts +8 -0
  61. package/packages/cf-adapter/src/index.ts +5 -0
  62. package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
  63. package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
  64. package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
  65. package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
  66. package/packages/cf-adapter/tests/serialize.test.ts +1 -0
  67. package/packages/in-memory-runtime/package.json +1 -1
  68. package/packages/irc-core/package.json +1 -1
  69. package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
  70. package/packages/irc-core/src/commands/account-auth.ts +172 -0
  71. package/packages/irc-core/src/commands/chanserv.ts +882 -0
  72. package/packages/irc-core/src/commands/hostserv.ts +487 -0
  73. package/packages/irc-core/src/commands/index.ts +12 -0
  74. package/packages/irc-core/src/commands/join.ts +164 -8
  75. package/packages/irc-core/src/commands/markread.ts +202 -0
  76. package/packages/irc-core/src/commands/memoserv.ts +319 -0
  77. package/packages/irc-core/src/commands/mode.ts +96 -4
  78. package/packages/irc-core/src/commands/nickserv.ts +390 -0
  79. package/packages/irc-core/src/commands/oper.ts +18 -1
  80. package/packages/irc-core/src/commands/operserv.ts +346 -0
  81. package/packages/irc-core/src/commands/pre-away.ts +3 -1
  82. package/packages/irc-core/src/commands/privmsg.ts +42 -0
  83. package/packages/irc-core/src/commands/read-marker.ts +8 -8
  84. package/packages/irc-core/src/commands/registration.ts +61 -6
  85. package/packages/irc-core/src/commands/sasl.ts +18 -49
  86. package/packages/irc-core/src/commands/tagmsg.ts +41 -6
  87. package/packages/irc-core/src/commands/topic.ts +37 -0
  88. package/packages/irc-core/src/config.ts +36 -5
  89. package/packages/irc-core/src/effects.ts +56 -1
  90. package/packages/irc-core/src/ports.ts +1653 -84
  91. package/packages/irc-core/src/protocol/numerics.ts +8 -0
  92. package/packages/irc-core/src/state/channel.ts +21 -1
  93. package/packages/irc-core/src/state/connection.ts +25 -1
  94. package/packages/irc-core/src/types.ts +48 -12
  95. package/packages/irc-core/tests/commands/chanserv.test.ts +1668 -0
  96. package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
  97. package/packages/irc-core/tests/commands/hostserv.test.ts +935 -0
  98. package/packages/irc-core/tests/commands/join.test.ts +393 -1
  99. package/packages/irc-core/tests/commands/markread.test.ts +361 -0
  100. package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
  101. package/packages/irc-core/tests/commands/mode.test.ts +381 -2
  102. package/packages/irc-core/tests/commands/nickserv.test.ts +807 -0
  103. package/packages/irc-core/tests/commands/oper.test.ts +13 -0
  104. package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
  105. package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
  106. package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
  107. package/packages/irc-core/tests/commands/registration.test.ts +788 -14
  108. package/packages/irc-core/tests/commands/sasl.test.ts +185 -12
  109. package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
  110. package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
  111. package/packages/irc-core/tests/commands/topic.test.ts +94 -2
  112. package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
  113. package/packages/irc-core/tests/config.test.ts +49 -5
  114. package/packages/irc-core/tests/effects.test.ts +19 -0
  115. package/packages/irc-core/tests/message-store.test.ts +63 -0
  116. package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
  117. package/packages/irc-core/tests/services-store.test.ts +1289 -0
  118. package/packages/irc-core/tests/state/channel.test.ts +3 -0
  119. package/packages/irc-server/package.json +1 -1
  120. package/packages/irc-server/src/actor.ts +71 -16
  121. package/packages/irc-server/src/dispatch.ts +94 -7
  122. package/packages/irc-server/src/routing.ts +19 -0
  123. package/packages/irc-server/tests/actor.test.ts +623 -12
  124. package/packages/irc-server/tests/dispatch.test.ts +270 -2
  125. package/packages/irc-server/tests/routing.test.ts +6 -0
  126. package/packages/irc-test-support/package.json +1 -1
  127. package/packages/irc-test-support/src/in-memory-harness.ts +29 -3
  128. package/packages/irc-test-support/src/index.ts +1 -0
  129. package/packages/irc-test-support/tests/in-memory-harness.test.ts +32 -0
  130. package/tools/ci-hardening/package.json +1 -1
  131. package/tools/load-test/package.json +1 -1
  132. package/tools/tcp-ws-forwarder/package.json +1 -1
  133. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +2 -2
  134. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
  135. package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
@@ -0,0 +1,546 @@
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
+ ---
130
+
131
+ ## 4. NickServ — nick registration & identification
132
+
133
+ Source: `packages/irc-core/src/commands/nickserv.ts`. Wire nick `NickServ`.
134
+
135
+ NickServ owns nick registration, identification (`+r`), nick-enforcement
136
+ policy, and nick-info queries. A registered nick is an account.
137
+
138
+ ### Commands
139
+
140
+ | Command | Syntax | Notes |
141
+ |---|---|---|
142
+ | `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.` |
143
+ | `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>.` |
144
+ | `DROP` | `PRIVMSG NickServ :DROP [nick]` | Requires prior identify. Drops the registration, clears `state.account` and `+r`, fans `ACCOUNT *`. Reply: `Nickname <nick> has been dropped.` |
145
+ | `INFO` | `PRIVMSG NickServ :INFO [nick]` | Defaults to the current nick. Replies (NOTICE): `Nick:`, `Account:`, `Email:`. Unregistered → `Nick <target> is not registered.` |
146
+ | `SET ENFORCE` | `PRIVMSG NickServ :SET ENFORCE none|ghost|kill` | Requires identify as the owning account. See enforcement below. |
147
+
148
+ Help / unknown command: `Available commands: REGISTER, IDENTIFY, DROP, INFO, SET`.
149
+
150
+ ### Nick enforcement
151
+
152
+ `SET ENFORCE <policy>` decides what happens when an **unidentified** client
153
+ uses a registered nick (checked at welcome completion and on every `NICK`
154
+ change):
155
+
156
+ - `none` (default) — warning NOTICE only:
157
+ `This nickname is registered and protected. If it is yours, IDENTIFY; otherwise choose another.`
158
+ - `ghost` — force-disconnect the unidentified client after the grace period
159
+ (`nickEnforceGraceMs`, default 30 s); `0` makes it immediate.
160
+ - `kill` — force-disconnect **immediately** with reason
161
+ `Nick enforced by NickServ`.
162
+
163
+ > **Note on `IDENTIFY` vs SASL.** Both set `state.account` and the `+r`
164
+ > user mode and broadcast `ACCOUNT`. They differ in what else runs: SASL
165
+ > success additionally seeds persisted read-markers and replays persisted
166
+ > away state; NickServ `IDENTIFY` additionally delivers unread memos. The
167
+ > two paths are otherwise equivalent account-setting entry points.
168
+
169
+ ---
170
+
171
+ ## 5. ChanServ — channel registration & access
172
+
173
+ Source: `packages/irc-core/src/commands/chanserv.ts`. Wire nick `ChanServ`.
174
+ ChanServ-emitted `MODE` lines use hostmask `ChanServ!ChanServ@services`.
175
+
176
+ ChanServ owns channel registration, founder/set management, the access list
177
+ (SOP/AOP/HOP/VOP via `ACCESS` and shorthand), configurable level
178
+ thresholds, and the `+r`/`+R`/`+M` channel modes.
179
+
180
+ ### Registration & info
181
+
182
+ | Command | Syntax | Notes |
183
+ |---|---|---|
184
+ | `REGISTER` | `PRIVMSG ChanServ :REGISTER <#channel>` | Requires identify (founder = `state.account`). Sets channel mode **`+r`**. Reply: `Channel <channel> is now registered.` |
185
+ | `DROP` | `PRIVMSG ChanServ :DROP <#channel>` | Founder-only. Clears `+r`/`+R`/`+M`. Reply: `Channel <channel> has been dropped.` |
186
+ | `INFO` | `PRIVMSG ChanServ :INFO <#channel>` | Replies: `Channel:`, `Founder:`. |
187
+
188
+ ### `SET` (founder-only)
189
+
190
+ All `SET` subcommands require the caller be identified as the channel
191
+ founder.
192
+
193
+ | Subcommand | Syntax | Effect |
194
+ |---|---|---|
195
+ | `FOUNDER` | `SET FOUNDER <#channel> <account>` | Transfers founder to a registered nick. |
196
+ | `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. |
197
+ | `RESTRICTED` | `SET RESTRICTED <#channel> ON|OFF` | Toggles channel mode **`+R`** (block unidentified). |
198
+ | `KEEPTOPIC` | `SET KEEPTOPIC <#channel> ON|OFF` | Persists the topic; restores it when the channel is recreated after going empty. |
199
+
200
+ ### Access list (SOP / AOP / HOP / VOP)
201
+
202
+ `ACCESS <#channel> ADD|DEL <account>` and the shorthand verbs
203
+ `SOP`/`AOP`/`HOP`/`VOP` (each `ADD|DEL|LIST`) map an account to a privilege
204
+ level. `ADD`/`DEL` are founder-only; `LIST` is read-only.
205
+
206
+ Default shorthand levels and the auto-prefix they grant on `JOIN`:
207
+
208
+ | Verb | Level | Auto-prefix on JOIN |
209
+ |---|---|---|
210
+ | `SOP` | 10 | `@` (op) |
211
+ | `AOP` | 5 (`AUTOOP`) | `@` (op) |
212
+ | `HOP` | 4 (`AUTOHALFOP`) | (halfop, where represented) |
213
+ | `VOP` | 3 (`AUTOVOICE`) | `+` (voice) |
214
+
215
+ A level **≥ `AUTOOP`** grants op; **≥ `AUTOVOICE`** grants voice; below
216
+ that, no prefix. The channel **founder** always receives `@` on join. The
217
+ auto-prefix is broadcast as `:ChanServ!ChanServ@services MODE <chan> +o|+v <nick>`
218
+ so all clients update their display. (The first joiner to an empty channel
219
+ gets `@` by RFC 2812, bypassing ChanServ.)
220
+
221
+ ### `LEVELS` (founder-only for `SET`/`RESET`)
222
+
223
+ `LEVELS <#channel> SET|LIST|RESET` redefines the numeric threshold each
224
+ auto-prefix op requires (`AUTOOP`, `AUTOHALFOP`, `AUTOVOICE`). `LIST` is
225
+ read-only and falls back to the defaults when no override is set.
226
+
227
+ ### Channel modes introduced by ChanServ
228
+
229
+ | Mode | Meaning | Set by |
230
+ |---|---|---|
231
+ | `+r` | Channel is registered | ChanServ `REGISTER` |
232
+ | `+R` | Block join/message from unidentified nicks | `SET RESTRICTED ON` |
233
+ | `+M` | Moderated-identified: only identified (or op/voice) may speak | (cleared on `DROP`; not directly set by a ChanServ command) |
234
+
235
+ These three letters are **services-only**: a user `MODE` attempt to set
236
+ them returns `472 ERR_UNKNOWNMODE`. Their enforcement:
237
+
238
+ - `+R` **JOIN gate:** unidentified joiner →
239
+ `519 ERR_ALLMUSTREGISTER <chan> :You must register with NickServ to join this channel`.
240
+ - `+R` / `+M` **PRIVMSG gate:** unidentified sender (and, for `+M`, any
241
+ non-op/non-voice) → `404 ERR_CANNOTSENDTOCHAN`.
242
+
243
+ ---
244
+
245
+ ## 6. HostServ — virtual hosts
246
+
247
+ Source: `packages/irc-core/src/commands/hostserv.ts`. Wire nick `HostServ`.
248
+
249
+ HostServ manages per-account virtual hosts (vhosts), layered on the
250
+ existing cloak machinery. Toggling a vhost mutates the connection's host
251
+ and broadcasts an IRCv3 `CHGHOST` (`chghost` cap) to every shared-channel
252
+ peer.
253
+
254
+ | Command | Syntax | Gating | Notes |
255
+ |---|---|---|---|
256
+ | `ON` | `PRIVMSG HostServ :ON` | identified | Activates the account's assigned vhost; `CHGHOST` broadcast. Reply: `Your vhost is now active.` |
257
+ | `OFF` | `PRIVMSG HostServ :OFF` | identified | Restores the real (cloaked) host; `CHGHOST` broadcast. Reply: `Your vhost has been disabled.` |
258
+ | `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). |
259
+ | `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.` |
260
+ | `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. |
261
+ | `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. |
262
+ | `LIST` | `PRIVMSG HostServ :LIST` | **oper-only** | Enumerates every pending request as `<account>: <vhost>` NOTICE lines. Empty queue → `No pending vhost requests.` |
263
+
264
+ ### Approval mode (`hostservAutoApproveVhosts`)
265
+
266
+ The `ServerConfig.hostservAutoApproveVhosts` knob (boolean, default
267
+ `false`) selects how `REQUEST <vhost>` is handled:
268
+
269
+ - **`true` (auto-approve):** the legacy behaviour. `REQUEST`
270
+ records the vhost against the account immediately via `setVhost`. The
271
+ user can then `/msg HostServ ON` to apply it. `APPROVE`/`REJECT`/`LIST`
272
+ are still wired but only relevant for any pre-existing pending requests.
273
+ - **`false` (default — oper queue):** `REQUEST` records a **pending**
274
+ request via `ServicesStore.requestVhost` and replies that the request
275
+ is pending oper approval. The vhost is NOT activated and no `CHGHOST`
276
+ is emitted until an oper runs `APPROVE <account>`. `REJECT <account>`
277
+ drops the request; `LIST` enumerates the queue.
278
+
279
+ The queue + the oper commands are available in either mode; only
280
+ `REQUEST`'s routing through them is gated on the knob. The default
281
+ (`false`) matches the Atheme/Anope convention of requiring oper review
282
+ for vhosts; set `true` to restore the legacy self-service auto-approve.
283
+
284
+ > **Pending-request delivery note.** When an oper `APPROVE`s or `REJECT`s
285
+ > a pending request and the requester is online, HostServ delivers a
286
+ > NOTICE via the actor layer's `SendToNick` (silently dropped when the
287
+ > requester is offline). The actual `CHGHOST` application on the
288
+ > requester's live connection is **not** done from the oper's reducer —
289
+ > the pure reducer model forbids cross-connection state mutation — so
290
+ > the requester must run `/msg HostServ ON` themselves to apply an
291
+ > approved vhost to their active connection. The NOTICE tells them so.
292
+
293
+ ---
294
+
295
+ ## 7. OperServ — network bans, jupes, raw (oper-only)
296
+
297
+ Source: `packages/irc-core/src/commands/operserv.ts`. Wire nick `OperServ`.
298
+
299
+ > **Routing.** `PRIVMSG OperServ` is routed to the OperServ reducer in the
300
+ > actor's target routing (alongside NickServ, ChanServ, HostServ, and
301
+ > MemoServ). A client `PRIVMSG OperServ :<command>` reaches the reducer
302
+ > instead of falling through to the nick-target path. Interception is
303
+ > **PRIVMSG-only**: `NOTICE`/`TAGMSG OperServ` are not intercepted and fall
304
+ > through to the normal user-target path, matching the other four services.
305
+ >
306
+ > **Enforcement hooks** (unchanged) are wired through the registration
307
+ > reducer:
308
+ > - **AKILL** — checked at welcome completion against the connecting
309
+ > hostmask; a match disconnects with reason `AKILL: network-wide ban`
310
+ > after a `NOTICE` from `OperServ!OperServ@services`.
311
+ > - **JUPE** — checked on every `NICK` attempt; a juped nick is rejected
312
+ > with `432 ERR_ERRONEUSNICKNAME`.
313
+ >
314
+ > The command surface below (AKILL/JUPE/RAW management) is oper-gated and
315
+ > now reachable by clients via `PRIVMSG OperServ`. A non-oper receives
316
+ > `Permission denied — oper privileges required.` with no state change.
317
+
318
+ Command surface (oper-only, reachable via `PRIVMSG OperServ`):
319
+
320
+ | Command | Syntax | Notes |
321
+ |---|---|---|
322
+ | `AKILL ADD` | `PRIVMSG OperServ :AKILL ADD <mask> <reason>` | Records a network-wide ban; matched at connect time. Re-adding a mask replaces the reason. |
323
+ | `AKILL DEL` | `PRIVMSG OperServ :AKILL DEL <mask>` | Removes a mask. |
324
+ | `AKILL LIST` | `PRIVMSG OperServ :AKILL LIST` | Lists `<mask> — <reason>`. |
325
+ | `JUPE` | `PRIVMSG OperServ :JUPE <nick> [reason]` | Blocks a nick/server name locally. |
326
+ | `UNJUPE` | `PRIVMSG OperServ :UNJUPE <nick>` | Removes a jupe. |
327
+ | `RAW` | `PRIVMSG OperServ :RAW <line>` | Emits a raw IRC line (recovery use only). Oper-only and logged. |
328
+
329
+ Every subcommand requires oper privileges; non-opers get
330
+ `Permission denied — oper privileges required.` with no state change.
331
+
332
+ ---
333
+
334
+ ## 8. MemoServ — offline messages
335
+
336
+ Source: `packages/irc-core/src/commands/memoserv.ts`. Wire nick `MemoServ`.
337
+
338
+ MemoServ queues messages for a registered nick and delivers them when the
339
+ recipient next identifies (NickServ `IDENTIFY`) or logs in via SASL. All
340
+ commands require identification. Memos are delivered as
341
+ `:MemoServ!MemoServ@services NOTICE <nick> :[Memo #<id> from <from>] <body>`.
342
+
343
+ | Command | Syntax | Notes |
344
+ |---|---|---|
345
+ | `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. |
346
+ | `LIST` | `PRIVMSG MemoServ :LIST` | Lists **unread** memos: `#<id> <from>: <body>`. |
347
+ | `READ` | `PRIVMSG MemoServ :READ <n>` | Displays memo `#n` and marks it read. |
348
+ | `DEL` | `PRIVMSG MemoServ :DEL <n>` | Deletes memo `#n`. |
349
+
350
+ ---
351
+
352
+ ## 9. The `ServicesStore` port
353
+
354
+ The services side effects are absorbed by a single synchronous port in
355
+ `packages/irc-core/src/ports.ts` (synchronous because reducers are pure and
356
+ free of IO; adapters pre-load a snapshot and flush writes asynchronously).
357
+ The reference implementation `InMemoryServicesStore` ships in the same
358
+ file; D1 and DynamoDB backends live in their adapter packages.
359
+
360
+ Method groups (signatures abbreviated; see `ports.ts` for canonical forms):
361
+
362
+ - **NickServ** — `registerNick`, `verifyNick`, `dropNick`, `isRegisteredNick`,
363
+ `setNickEnforce`/`getNickEnforce`, `getNick`.
364
+ - **ChanServ (registration)** — `registerChannel`, `dropChannel`,
365
+ `getChannelFounder`, `setChannelAccess`/`getChannelAccess`/`listChannelAccess`.
366
+ - **ChanServ (levels)** — `getChannelLevel`/`setChannelLevel`/
367
+ `listChannelLevels`/`resetChannelLevels`.
368
+ - **ChanServ (record)** — `getChannel`, `setChannelFounder`,
369
+ `setChannelMlock`/`getChannelMlock`, `setChannelRestricted`,
370
+ `setChannelKeepTopic`, `setChannelTopic`/`getChannelTopic`.
371
+ - **HostServ** — `setVhost`/`getVhost`, plus the pending-vhost queue
372
+ (`requestVhost`/`getPendingVhost`/`listPendingVhosts`/`approveVhost`/
373
+ `rejectVhost`) consumed by queue-mode `REQUEST` and the oper
374
+ `APPROVE`/`REJECT`/`LIST` commands.
375
+ - **MemoServ** — `recordMemo`, `listMemos`, `markMemoRead`, `deleteMemo`.
376
+ - **OperServ** — `addAkill`/`removeAkill`/`listAkills`/`matchAkill`,
377
+ `addJupe`/`removeJupe`/`isJuped`/`getJupe`/`listJupes`.
378
+ - **Read-marker (IRCv3 `draft/read-marker`)** — `getLastReadMarker`/
379
+ `setLastReadMarker`/`listReadMarkers`.
380
+
381
+ Password comparison is constant-time. All nick/account/channel keys are
382
+ rfc1459 case-folded.
383
+
384
+ ---
385
+
386
+ ## 10. Admin tooling & seeding accounts
387
+
388
+ Two seed scripts ship under `tools/` for **SASL accounts** (the
389
+ scrypt-hashed `accounts`/`Accounts` store used by `AUTHENTICATE PLAIN`):
390
+
391
+ - `tools/seed-cf-accounts.ts` — writes to a Cloudflare D1 `accounts` table
392
+ via `wrangler d1 execute`, scrypt-hashing each password.
393
+ - `tools/seed-aws-accounts.ts` — writes to a DynamoDB `Accounts` table via
394
+ the AWS SDK, scrypt-hashing each password.
395
+
396
+ Both accept `--accounts alice:s3cret bob:pw …` or `--file accounts.txt`
397
+ (newline-delimited `username:password`).
398
+
399
+ ```
400
+ # Cloudflare
401
+ node --import tsx tools/seed-cf-accounts.ts \
402
+ --database serverless-ircd-accounts-staging --env staging --remote \
403
+ --accounts alice:s3cret
404
+
405
+ # AWS
406
+ node --import tsx tools/seed-aws-accounts.ts \
407
+ --table StagingAccounts --endpoint http://localhost:8000 \
408
+ --accounts alice:s3cret
409
+ ```
410
+
411
+ > **NickServ accounts vs SASL accounts.** NickServ registrations live in
412
+ > the **`ServicesStore`** (`nickserv_accounts`), which is unified with the
413
+ > SASL `AccountStore` the seed scripts above populate: a NickServ
414
+ > registration is a SASL account and vice versa, both scrypt-hashed (see
415
+ > the [walkthrough](#13-end-to-end-walkthrough)). The recommended way to
416
+ > create a NickServ account is the live
417
+ > `PRIVMSG NickServ :REGISTER …` flow over an established connection. A
418
+ > dedicated `tools/seed-nickserv-accounts.ts` mirroring the SASL seeders
419
+ > is tracked as a follow-up; because credentials are scrypt-hashed at rest
420
+ > (see [§11](#11-security-characteristics--known-gaps)), bulk-seeded rows
421
+ > must supply a scrypt `algorithm`/`salt`/`hash` triple rather than a
422
+ > plaintext password.
423
+
424
+ Operators can manage AKILL/JUPE entries via `PRIVMSG OperServ` (oper-only,
425
+ see [§7](#7-operserv--network-bans-jupes-raw-oper-only)), or pre-populate
426
+ them by writing directly into the `ServicesStore` backing tables.
427
+
428
+ ---
429
+
430
+ ## 11. Security characteristics & known gaps
431
+
432
+ Be aware of the following when operating or contributing to services:
433
+
434
+ - **NickServ passwords are stored scrypt-hashed** in the `ServicesStore`
435
+ (the same scrypt path the SASL `AccountStore` uses:
436
+ `InMemoryServicesStore.registerNick` hashes via `hashAccountCredential`
437
+ before persisting; `verifyNick` re-derives via `verifyHashedPassword`;
438
+ the D1 `nickserv_accounts` table stores `algorithm`/`salt`/`hash`
439
+ columns, never the plaintext password). NickServ and SASL accounts are
440
+ unified, so a NickServ registration is a SASL account and vice versa.
441
+ Treat the services backing store (D1 database / DynamoDB table) as
442
+ sensitive regardless: restrict access and enable at-rest encryption
443
+ (DynamoDB encryption-at-rest is on by default).
444
+ - **HostServ `REQUEST` approval mode is configurable** — the default
445
+ (`hostservAutoApproveVhosts: false`) queues every request for oper
446
+ review via the `APPROVE`/`REJECT`/`LIST` commands (see [§6](#6-hostserv--virtual-hosts)).
447
+ Set `hostservAutoApproveVhosts: true` to restore the legacy
448
+ self-service auto-approve, where any identified user can self-assign a
449
+ DNS-shape vhost.
450
+ - **OperServ management is oper-gated** — every `PRIVMSG OperServ`
451
+ subcommand (AKILL/JUPE/RAW) requires oper privileges; non-opers receive
452
+ `Permission denied — oper privileges required.` with no state change
453
+ (see [§7](#7-operserv--network-bans-jupes-raw-oper-only)).
454
+ - **Memo bodies** are stored and replayed verbatim; no size cap beyond the
455
+ general IRC line-length budget applied at the wire.
456
+ - **`+r` user mode** is set by NickServ `IDENTIFY` and SASL; it is not set
457
+ by the legacy `PASS <nick>:<password>` form (that path is tracked
458
+ separately).
459
+
460
+ ---
461
+
462
+ ## 12. Migration notes (from Atheme/Anope)
463
+
464
+ - **No S2S, no services process.** An existing Atheme/Anope instance cannot
465
+ attach to ServerlessIRCd. Plan to **re-register** nicks and channels.
466
+ - **Re-register nicks** with `PRIVMSG NickServ :REGISTER <password> <email>`
467
+ on the new server. There is no import path from Atheme/Anope databases.
468
+ - **Re-register channels** with `ChanServ REGISTER` from the identified
469
+ founder, then re-create the access list via `ACCESS`/`SOP`/`AOP`/`HOP`/
470
+ `VOP`. MLOCK, RESTRICTED, and KEEPTOPIC must be re-applied with `SET`.
471
+ - **OperServ parity is complete.** AKILL (connect-time enforcement) and
472
+ JUPE (nick-time enforcement) are live, and the AKILL/JUPE/RAW management
473
+ commands are client-reachable via `PRIVMSG OperServ` (oper-only, see
474
+ [§7](#7-operserv--network-bans-jupes-raw-oper-only)).
475
+ - **HostServ vhost approval.** The default
476
+ (`hostservAutoApproveVhosts: false`) matches the Atheme/Anope
477
+ convention: `REQUEST` queues for oper `APPROVE`/`REJECT`/`LIST`
478
+ (see [§6](#6-hostserv--virtual-hosts)). Set `hostservAutoApproveVhosts: true`
479
+ to restore self-service auto-approve.
480
+ - **Out of scope** (will not migrate): `BotServ` (channel bots are a
481
+ runtime cost serverless deployments should not absorb — run your own bot
482
+ as an ordinary client) and `StatServ` (covered by `STATS` and the
483
+ observability stack). Grouped/linked nicks and fantasy commands are also
484
+ out of scope for this pass.
485
+
486
+ ---
487
+
488
+ ## 13. End-to-end walkthrough
489
+
490
+ This satisfies the TICKET-131 acceptance criterion: register a NickServ
491
+ account and a ChanServ channel using only this doc. Connect to a
492
+ services-enabled deployment with any IRC client and register first:
493
+
494
+ ```
495
+ NICK alice
496
+ USER alice 0 * :Alice
497
+ PRIVMSG NickServ :REGISTER hunter2 alice@example.com
498
+ ```
499
+ ```
500
+ :NickServ!NickServ@services NOTICE alice :Nickname alice is now registered.
501
+ ```
502
+
503
+ The nick is registered but not yet identified. Identify to receive the `+r`
504
+ user mode (required before registering a channel or sending memos):
505
+
506
+ ```
507
+ PRIVMSG NickServ :IDENTIFY alice hunter2
508
+ ```
509
+ ```
510
+ :NickServ!NickServ@services NOTICE alice :You are now identified for nick alice.
511
+ ```
512
+
513
+ Now register a channel (you are the founder):
514
+
515
+ ```
516
+ JOIN #mychannel
517
+ PRIVMSG ChanServ :REGISTER #mychannel
518
+ ```
519
+ ```
520
+ :ChanServ!ChanServ@services NOTICE alice :Channel #mychannel is now registered.
521
+ :ChanServ!ChanServ@services MODE #mychannel +r
522
+ ```
523
+
524
+ Add a friend (who has already registered their nick `bob`) to the access
525
+ list as an auto-op, lock the topic, and restrict the channel to identified
526
+ users:
527
+
528
+ ```
529
+ PRIVMSG ChanServ :AOP #mychannel ADD bob
530
+ PRIVMSG ChanServ :SET KEEPTOPIC #mychannel ON
531
+ PRIVMSG ChanServ :SET RESTRICTED #mychannel ON
532
+ ```
533
+ ```
534
+ :ChanServ!ChanServ@services NOTICE alice :Bob added to the access list for #mychannel at level 5.
535
+ :ChanServ!ChanServ@services NOTICE alice :Keep-topic for #mychannel is now set to ON.
536
+ :ChanServ!ChanServ@services NOTICE alice :Restricted (block unidentified) for #mychannel is now set to ON.
537
+ ```
538
+
539
+ When `bob` later identifies and joins, ChanServ auto-ops them:
540
+
541
+ ```
542
+ :ChanServ!ChanServ@services MODE #mychannel +o bob
543
+ ```
544
+
545
+ That completes the core nick + channel registration flow. From here, the
546
+ per-service references in §4–§8 cover every shipped command.