serverless-ircd 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +34 -2
- package/.github/workflows/deploy-aws.yml +1 -2
- package/.github/workflows/deploy-cf-tcp.yml +1 -5
- package/.github/workflows/deploy-cf.yml +1 -2
- package/CHANGELOG.md +260 -0
- package/README.md +132 -22
- package/apps/aws-stack/README.md +33 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +74 -9
- package/apps/aws-stack/tests/coverage-command.test.ts +155 -0
- package/apps/aws-stack/tests/stack.test.ts +2 -2
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +129 -0
- package/apps/aws-stack/tests/vitest.setup.ts +18 -0
- package/apps/aws-stack/vitest.config.ts +3 -0
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/config-loader.ts +6 -7
- package/apps/cf-tcp-container/src/container-server.ts +3 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +10 -3
- package/apps/cf-tcp-container/wrangler.toml +1 -1
- package/apps/cf-worker/package.json +2 -1
- package/apps/cf-worker/vitest.config.ts +1 -1
- package/apps/cf-worker/wrangler.toml +5 -3
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +6 -6
- package/apps/local-cli/src/main.ts +20 -0
- package/apps/local-cli/src/server.ts +9 -7
- package/apps/local-cli/tests/e2e.test.ts +112 -28
- package/apps/local-cli/tests/tcp.test.ts +71 -19
- package/apps/local-cli/tests/ws-subprotocol.test.ts +12 -7
- package/biome.json +1 -0
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +110 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +96 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/ADR-009-dual-transport-wss-and-irc-tls.md +138 -0
- package/docs/ADR-Index.md +15 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1275 -0
- package/docs/AWS-TCP-Deployment.md +410 -0
- package/docs/Cloudflare-Deployment-Guide.md +719 -0
- package/docs/Cloudflare-TCP-Deployment.md +510 -0
- package/docs/Home.md +15 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanExtensions.md +812 -0
- package/docs/PlanIRCv3Websocket.md +503 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/handlers/default.ts +3 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +9 -0
- package/packages/aws-adapter/src/serialize.ts +29 -1
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +49 -8
- package/packages/aws-adapter/tests/account-store.test.ts +4 -7
- package/packages/aws-adapter/tests/aws-harness.ts +5 -2
- package/packages/aws-adapter/tests/aws-integration.test.ts +8 -4
- package/packages/aws-adapter/tests/aws-runtime.test.ts +79 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +11 -2
- package/packages/aws-adapter/tests/connect.test.ts +9 -9
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +3 -4
- package/packages/aws-adapter/tests/global-setup.ts +55 -30
- package/packages/aws-adapter/tests/gone-exception.test.ts +4 -9
- package/packages/aws-adapter/tests/handlers.test.ts +90 -4
- package/packages/aws-adapter/tests/message-store.test.ts +6 -5
- package/packages/aws-adapter/tests/nlb-stream.test.ts +60 -8
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/connection-do.ts +3 -0
- package/packages/cf-adapter/src/serialize.ts +25 -4
- package/packages/cf-adapter/tests/cf-runtime.test.ts +269 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +117 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +49 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +91 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +48 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +104 -0
- package/packages/cf-adapter/tests/serialize.test.ts +308 -0
- package/packages/cf-adapter/vitest.config.ts +1 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/caps/capabilities.ts +37 -3
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/caps/sts.ts +84 -0
- package/packages/irc-core/src/commands/account-notify.ts +53 -0
- package/packages/irc-core/src/commands/away.ts +9 -3
- package/packages/irc-core/src/commands/cap.ts +23 -5
- package/packages/irc-core/src/commands/chathistory.ts +40 -11
- package/packages/irc-core/src/commands/index.ts +34 -0
- package/packages/irc-core/src/commands/isupport.ts +53 -0
- package/packages/irc-core/src/commands/mode.ts +8 -1
- package/packages/irc-core/src/commands/monitor.ts +327 -0
- package/packages/irc-core/src/commands/multiline.ts +256 -0
- package/packages/irc-core/src/commands/pre-away.ts +112 -0
- package/packages/irc-core/src/commands/privmsg.ts +19 -12
- package/packages/irc-core/src/commands/read-marker.ts +108 -0
- package/packages/irc-core/src/commands/registration.ts +8 -0
- package/packages/irc-core/src/commands/sasl.ts +24 -1
- package/packages/irc-core/src/commands/tagmsg.ts +71 -1
- package/packages/irc-core/src/commands/whois.ts +5 -0
- package/packages/irc-core/src/config.ts +65 -10
- package/packages/irc-core/src/effects.ts +14 -0
- package/packages/irc-core/src/ports.ts +389 -0
- package/packages/irc-core/src/protocol/index.ts +14 -0
- package/packages/irc-core/src/protocol/labeled-response.ts +133 -0
- package/packages/irc-core/src/protocol/numerics.ts +15 -0
- package/packages/irc-core/src/protocol/outbound.ts +16 -1
- package/packages/irc-core/src/protocol/standard-replies.ts +277 -0
- package/packages/irc-core/src/state/connection.ts +32 -1
- package/packages/irc-core/src/types.ts +112 -0
- package/packages/irc-core/stryker.commands.conf.json +1 -2
- package/packages/irc-core/tests/away-store.test.ts +73 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +119 -2
- package/packages/irc-core/tests/caps/sts.test.ts +123 -0
- package/packages/irc-core/tests/commands/account-notify.test.ts +171 -0
- package/packages/irc-core/tests/commands/away.test.ts +85 -2
- package/packages/irc-core/tests/commands/cap.test.ts +165 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +148 -1
- package/packages/irc-core/tests/commands/echo-message.test.ts +9 -3
- package/packages/irc-core/tests/commands/isupport.test.ts +80 -0
- package/packages/irc-core/tests/commands/mode.test.ts +72 -0
- package/packages/irc-core/tests/commands/monitor.test.ts +411 -0
- package/packages/irc-core/tests/commands/multiline.test.ts +428 -0
- package/packages/irc-core/tests/commands/pre-away.test.ts +241 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +154 -3
- package/packages/irc-core/tests/commands/read-marker.test.ts +211 -0
- package/packages/irc-core/tests/commands/registration.test.ts +64 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +116 -2
- package/packages/irc-core/tests/commands/tagmsg.test.ts +357 -0
- package/packages/irc-core/tests/commands/whois.test.ts +34 -0
- package/packages/irc-core/tests/config.test.ts +103 -13
- package/packages/irc-core/tests/labeled-response.test.ts +181 -0
- package/packages/irc-core/tests/message-tags.test.ts +114 -0
- package/packages/irc-core/tests/monitor-store.test.ts +200 -0
- package/packages/irc-core/tests/read-marker-store.test.ts +108 -0
- package/packages/irc-core/tests/standard-replies.test.ts +412 -0
- package/packages/irc-core/tests/types.test.ts +27 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +450 -3
- package/packages/irc-server/src/dispatch.ts +108 -16
- package/packages/irc-server/tests/actor.test.ts +1432 -39
- package/packages/irc-server/tests/dispatch.test.ts +263 -2
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/index.ts +6 -0
- package/packages/irc-test-support/src/test-config.ts +54 -0
- package/packages/irc-test-support/tests/test-config.test.ts +51 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/tests/ddb-local.test.ts +82 -0
- package/tools/load-test/package.json +33 -0
- package/tools/load-test/src/client.ts +351 -0
- package/tools/load-test/src/config.ts +313 -0
- package/tools/load-test/src/harness.ts +116 -0
- package/tools/load-test/src/main.ts +120 -0
- package/tools/load-test/src/metrics.ts +168 -0
- package/tools/load-test/src/report.ts +106 -0
- package/tools/load-test/tests/client.test.ts +212 -0
- package/tools/load-test/tests/config.test.ts +152 -0
- package/tools/load-test/tests/framing.test.ts +37 -0
- package/tools/load-test/tests/harness.test.ts +165 -0
- package/tools/load-test/tests/metrics.test.ts +174 -0
- package/tools/load-test/tests/report.test.ts +161 -0
- package/tools/load-test/tests/smoke.test.ts +67 -0
- package/tools/load-test/tsconfig.build.json +12 -0
- package/tools/load-test/tsconfig.test.json +10 -0
- package/tools/load-test/vitest.config.ts +29 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
# ServerlessIRCd — Extensions Plan (post-v1)
|
|
2
|
+
|
|
3
|
+
Plan for the IRC features that v1 deliberately left out: the remaining
|
|
4
|
+
**IRCv3 client-server extensions** and **IRC services** (NickServ /
|
|
5
|
+
ChanServ / OperServ / HostServ / MemoServ). Each section is scoped,
|
|
6
|
+
mapped onto the existing hexagonal architecture, and proposed as
|
|
7
|
+
tickets for `tickets.md` / `progress.md` once ratified.
|
|
8
|
+
|
|
9
|
+
> **Status:** planning. Nothing here has landed. Ticket numbers below
|
|
10
|
+
> start at **TICKET-110** (next free id per `todo.md`). When this plan
|
|
11
|
+
> is ratified the tickets get appended to `tickets.md` / `progress.md`
|
|
12
|
+
> under two new phase headings (**Phase 12 — IRCv3 extensions**,
|
|
13
|
+
> **Phase 13 — IRC services**) and the corresponding sections here are
|
|
14
|
+
> superseded.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Goals & non-goals
|
|
19
|
+
|
|
20
|
+
**Goals**
|
|
21
|
+
- Close the gap between the v1 cap set and what mainstream clients
|
|
22
|
+
(IRCCloud, TheLounge, Textual, gamja) actually negotiate today.
|
|
23
|
+
- Add the IRCv3 features that compose with work already shipped:
|
|
24
|
+
`account-tag` is meaningless without `account-notify`; `message-tags`
|
|
25
|
+
without `msgid` / `labeled-response` leaves clients correlating by
|
|
26
|
+
content.
|
|
27
|
+
- Provide a services story (NickServ/ChanServ) that fits the
|
|
28
|
+
pure-reducer + location-of-authority architecture instead of bolting
|
|
29
|
+
on a pseudo-server.
|
|
30
|
+
- Stay serverless-native: every new persistence need is behind a port
|
|
31
|
+
(`MessageStore`, `AccountStore`, `ServicesStore`) so the same core
|
|
32
|
+
runs on Cloudflare and AWS unchanged.
|
|
33
|
+
|
|
34
|
+
**Non-goals (this plan)**
|
|
35
|
+
- **Server-to-server linking** (TS6, IRCv3 S2S, SALR). Remains a
|
|
36
|
+
non-goal, same as v1 (`PLAN.md` §1). A `ServerLink` seam is the
|
|
37
|
+
documented design-for-later; no S2S work is proposed here.
|
|
38
|
+
- **Atheme / Anope compatibility.** Services are integrated into the
|
|
39
|
+
daemon, not implemented as a separate pseudo-server speaking a link
|
|
40
|
+
protocol. See §6 architecture decision.
|
|
41
|
+
- **Full services suite parity.** BotServ and StatServ are explicitly
|
|
42
|
+
out of scope; OperServ ships a minimal AKILL/JUPE set only.
|
|
43
|
+
- **Plaintext :6667 support.** Still TLS-only (ADR-009).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 2. Current cap set (baseline)
|
|
48
|
+
|
|
49
|
+
`packages/irc-core/src/caps/capabilities.ts:28` advertises:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
account-tag, away-notify, batch, chghost, draft/chathistory,
|
|
53
|
+
echo-message, extended-join, invite-notify, message-tags,
|
|
54
|
+
multi-prefix, safelist, sasl=PLAIN[,EXTERNAL], server-time
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
What follows fills in the missing pieces, grouped by spec maturity.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Missing IRCv3 extensions
|
|
62
|
+
|
|
63
|
+
### 3.1 Stable spec extensions (high-value, low-risk)
|
|
64
|
+
|
|
65
|
+
| Cap | Spec | What it adds | Effort | Depends on |
|
|
66
|
+
|-----|------|--------------|--------|------------|
|
|
67
|
+
| `account-notify` | [ircv3.net/specs/extensions/account-notify-3.2](https://ircv3.net/specs/extensions/account-notify-3.2.html) | Push `ACCOUNT` messages to peers in shared channels on login/logout (complements `account-tag`, `extended-join`) | S (2 pts) | SASL AccountStore wiring (TICKET-071 ✅) |
|
|
68
|
+
| `msgid` | [ircv3.net/specs/extensions/message-ids](https://ircv3.net/specs/extensions/message-ids.html) | Stable per-message id as a `msgid` tag on every PRIVMSG/NOTICE to cap-enabled peers (live fanout), sharing the single `IdFactory.nonce()` value also recorded for chathistory replay; needed for `draft/read-marker`, replies, dedup | S (2 pts) | `message-tags` ✅, `IdFactory` ✅ |
|
|
69
|
+
| `standard-replies` | [ircv3.net/specs/extensions/standard-replies-3.0](https://ircv3.net/specs/extensions/standard-replies-3.0.html) | Replace a class of bare numerics with `FAIL`/`WARN`/`NOTE` for cap-enabled clients; better client UX | M (3 pts) | none |
|
|
70
|
+
| `monitor` | [ircv3.net/specs/extensions/monitor-3.2](https://ircv3.net/specs/extensions/monitor-3.2.html) | `MONITOR` command, modern replacement for `ISON`; per-connection watchlist + `730`/`731`/`732`/`733`/`734` numerics | M (5 pts) | Registry lookups ✅ |
|
|
71
|
+
| `labeled-response` | [ircv3.net/specs/extensions/labeled-response-3.2](https://ircv3.net/specs/extensions/labeled-response-3.2.html) | Client tags a request with `label`; server wraps the reply in a `BATCH` carrying that label. Composes with `batch` ✅ | L (8 pts) | `batch` ✅, `message-tags` ✅ |
|
|
72
|
+
| `sts` | [ircv3.net/specs/extensions/sts](https://ircv3.net/specs/extensions/sts.html) | Strict Transport Security: advertise a TLS+port policy to upgrade plaintext/WebSocket clients. Relevant once raw TCP `:6697` is exposed (it is, per Phase 7 ✅) | S (2 pts) | dual transport ✅ |
|
|
73
|
+
|
|
74
|
+
**Notes:**
|
|
75
|
+
- `account-notify` is the cheapest win and unblocks the
|
|
76
|
+
`account-tag`/`extended-join`/`account-notify` triple that modern
|
|
77
|
+
clients render fully.
|
|
78
|
+
- `msgid` is mostly there: the chathistory work (TICKET-063) already
|
|
79
|
+
produces stable msgids via `IdFactory.nonce()`. This ticket is the
|
|
80
|
+
cap advertisement + emitting the tag on every PRIVMSG/NOTICE/TAGMSG,
|
|
81
|
+
not just recorded ones.
|
|
82
|
+
- `labeled-response` is the largest item because the request→reply
|
|
83
|
+
correlation touches every reducer that emits a client-facing reply;
|
|
84
|
+
the `BATCH` wrapper already exists (TICKET-025), so the work is in
|
|
85
|
+
plumbing a per-request label through `ctx`.
|
|
86
|
+
- `sts` is config-driven only (no protocol state); the value is
|
|
87
|
+
advertising the right TLS port for the deployed adapter.
|
|
88
|
+
|
|
89
|
+
### 3.2 Draft / spec-pending extensions
|
|
90
|
+
|
|
91
|
+
| Cap | Spec | What it adds | Effort | Depends on |
|
|
92
|
+
|-----|------|--------------|--------|------------|
|
|
93
|
+
| `draft/pre-away` | [ircv3 draft](https://github.com/ircv3/ircv3-specifications/pull/415) | Persist away status + message across reconnects; cross-device away sync. Noted out-of-scope in TICKET-063. | M (3 pts) | `AccountStore` ✅, services `UserStore` (§6) |
|
|
94
|
+
| `draft/read-marker` | [ircv3 draft](https://github.com/ircv3/ircv3-specifications/pull/416) | Per-`(user, channel)` last-read marker, propagated via `+draft/read-marker` tag. The chathistory plumbing (TICKET-063) already keeps the marker locally; this ticket persists it across connections and shares it. | M (5 pts) | chathistory ✅, services `UserStore` |
|
|
95
|
+
| `draft/multiline` | [ircv3 draft](https://ircv3.net/specs/extensions/multiline.html) | `BATCH +id draft/multiline :target` for multi-line messages up to a byte budget. Clients (TheLounge, gamja) render as joined blocks. | M (5 pts) | `batch` ✅, `message-tags` ✅ |
|
|
96
|
+
| `draft/typing` | [ircv3 client-behavior/typing](https://ircv3.net/specs/client-behavior/typing-notification.html) | `TAGMSG +draft/typing=active|paused|done` to channel members (excluding sender unless `echo-message`). Client UX only; no server state. | S (2 pts) | `message-tags` ✅, `TAGMSG` ✅ |
|
|
97
|
+
|
|
98
|
+
**Notes:**
|
|
99
|
+
- The `draft/` prefix caps stay namespaced until the spec stabilizes,
|
|
100
|
+
matching the existing `draft/chathistory` precedent.
|
|
101
|
+
- `draft/typing` is a near-trivial follow-on from TAGMSG (TICKET-076 ✅):
|
|
102
|
+
just whitelist the `+draft/typing=…` client tag and broadcast.
|
|
103
|
+
|
|
104
|
+
### 3.3 Already-shipped caps blocked on adapter wiring
|
|
105
|
+
|
|
106
|
+
Worth calling out explicitly so they are not re-implemented by mistake:
|
|
107
|
+
|
|
108
|
+
| Cap | Status |
|
|
109
|
+
|-----|--------|
|
|
110
|
+
| `account-tag` | Reducer emits; only meaningful once `AccountStore` is bound per adapter (TICKET-071 ✅). |
|
|
111
|
+
| `extended-join` | Reducer emits the `account realname` form; same dependency as above. |
|
|
112
|
+
| `draft/chathistory` | Reducer + in-memory store shipped (TICKET-063 ✅); adapter `MessageStore` wiring landed in TICKET-070 ✅. Persistent CF (D1/DO) and AWS (DynamoDB) stores are the next step. |
|
|
113
|
+
| `msgid` | Standalone cap advertised and emitted on the live PRIVMSG/NOTICE fanout (TICKET-111 ✅). A single `IdFactory.nonce()` is shared between the live `@msgid=<id>` tag and the chathistory record so the two match for the same message. |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. Missing ISUPPORT tokens
|
|
118
|
+
|
|
119
|
+
`packages/irc-core/src/commands/isupport.ts` should grow:
|
|
120
|
+
|
|
121
|
+
- `MONITOR=<n>` — maximum entries per connection (TICKET for `monitor`).
|
|
122
|
+
- `ACCOUNTEXTBAN` / `EXTBAN=~,acc` — extended ban by account (services
|
|
123
|
+
`+r` work).
|
|
124
|
+
- `STATUSMSG=@+` — prefixes that map to status-msg delivery (ships with
|
|
125
|
+
any `STATUSMSG`-aware PRIVMSG change).
|
|
126
|
+
- `TYPING` — draft advertisement when `draft/typing` lands.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 5. Channel + user modes that arrive with services
|
|
131
|
+
|
|
132
|
+
The v1 mode set (`o v b i k l t n m s p` channel, `i o w s` user) gains:
|
|
133
|
+
|
|
134
|
+
| Mode | Type | Meaning | Source |
|
|
135
|
+
|------|------|---------|--------|
|
|
136
|
+
| `r` (channel) | A | Channel is registered with ChanServ | services |
|
|
137
|
+
| `R` (channel) | B | Block join/message from unregistered nicks | services |
|
|
138
|
+
| `M` (channel) | B | Mod speak requires registered nick (vs. `+m` = op) | services |
|
|
139
|
+
| `c` (channel) | B | Block ANSI color (requires message inspection) | optional |
|
|
140
|
+
| `r` (user) | — | User is identified to NickServ | services |
|
|
141
|
+
| `q` (user, recv-only) | — | Protected from oper kick (U-line / services) | services |
|
|
142
|
+
| `S` (user) | — | Connected via TLS (set by transport, read-only) | transport |
|
|
143
|
+
|
|
144
|
+
`S` is a near-freebie — the transport already knows whether TLS is in
|
|
145
|
+
use (`ConnectionActor` accepts a `transportMeta`); surfacing it as a
|
|
146
|
+
user mode costs one reducer branch.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 6. IRC services
|
|
151
|
+
|
|
152
|
+
### 6.1 Architecture decision: integrated services (not pseudo-servers)
|
|
153
|
+
|
|
154
|
+
Two designs exist in the wild:
|
|
155
|
+
|
|
156
|
+
**A. Pseudo-client services (Atheme / Anope model).** A separate
|
|
157
|
+
process connects as one or more virtual IRC clients named `NickServ`
|
|
158
|
+
etc.; users interact via `PRIVMSG NickServ :REGISTER hunter2 email`.
|
|
159
|
+
The link between ircd and services speaks a server-to-server protocol
|
|
160
|
+
(TS6 / P10 / IRCv3 S2S).
|
|
161
|
+
|
|
162
|
+
**B. Integrated services.** Service names are reserved nicks
|
|
163
|
+
recognized by the daemon at PRIVMSG time and routed to dedicated
|
|
164
|
+
reducers backed by a `ServicesStore` port. No S2S, no separate process,
|
|
165
|
+
no pseudo-client connection.
|
|
166
|
+
|
|
167
|
+
**Decision: B — integrated services.** Rationale:
|
|
168
|
+
|
|
169
|
+
- S2S is an explicit non-goal (`PLAN.md` §1). Model A requires it.
|
|
170
|
+
- The pure-reducer + location-of-authority architecture already has the
|
|
171
|
+
right seam: a new `ServicesStore` port (`packages/irc-core/src/ports.ts`,
|
|
172
|
+
mirroring `AccountStore` / `MessageStore`) absorbs the side effects,
|
|
173
|
+
and reducers stay pure.
|
|
174
|
+
- One storage layer is shared with the existing SASL `AccountStore`
|
|
175
|
+
(TICKET-071 ✅) — a NickServ registration *is* a SASL account, so the
|
|
176
|
+
services work composes with auth rather than duplicating it.
|
|
177
|
+
- The serverless substrates (Durable Objects, DynamoDB) handle
|
|
178
|
+
persistence natively; spinning up a separate long-running services
|
|
179
|
+
process would defeat the deployment model.
|
|
180
|
+
|
|
181
|
+
**Trade-off documented:** integrated services are not binary-compatible
|
|
182
|
+
with existing Atheme/Anope modules. Operators migrating from a
|
|
183
|
+
services-aware network should expect to re-register nicks and channels.
|
|
184
|
+
This is called out in `docs/` when the work ships.
|
|
185
|
+
|
|
186
|
+
### 6.2 The `ServicesStore` port
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
/** Persists services-owned state. Mirrors the AccountStore/MessageStore pattern. */
|
|
190
|
+
export interface ServicesStore {
|
|
191
|
+
// NickServ
|
|
192
|
+
registerNick(account: string, passwordHash: string, email?: string): Promise<void>;
|
|
193
|
+
verifyNick(account: string, password: string): Promise<boolean>;
|
|
194
|
+
dropNick(account: string): Promise<void>;
|
|
195
|
+
isRegisteredNick(account: string): Promise<boolean>;
|
|
196
|
+
setNickEnforce(account: string, mode: 'none' | 'kill' | 'ghost'): Promise<void>;
|
|
197
|
+
|
|
198
|
+
// ChanServ
|
|
199
|
+
registerChannel(chan: string, founder: string): Promise<void>;
|
|
200
|
+
dropChannel(chan: string): Promise<void>;
|
|
201
|
+
getChannelFounder(chan: string): Promise<string | null>;
|
|
202
|
+
setChannelAccess(chan: string, account: string, level: AccessLevel): Promise<void>;
|
|
203
|
+
|
|
204
|
+
// HostServ
|
|
205
|
+
setVhost(account: string, vhost: string): Promise<void>;
|
|
206
|
+
getVhost(account: string): Promise<string | null>;
|
|
207
|
+
|
|
208
|
+
// MemoServ
|
|
209
|
+
recordMemo(to: string, from: string, body: string): Promise<void>;
|
|
210
|
+
listMemos(account: string): Promise<Memo[]>;
|
|
211
|
+
markMemoRead(account: string, id: string): Promise<void>;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Adapter implementations:
|
|
216
|
+
|
|
217
|
+
| Adapter | Backend |
|
|
218
|
+
|---------|---------|
|
|
219
|
+
| in-memory-runtime | `Map`s, used by local-cli + tests |
|
|
220
|
+
| cf-adapter | D1 (shared with the SASL store from TICKET-093 ✅) or a dedicated `ServicesDO` |
|
|
221
|
+
| aws-adapter | DynamoDB `Accounts` table extended (TICKET-072 ✅ already provisions it) + new `Channels`/`Memos` tables |
|
|
222
|
+
|
|
223
|
+
### 6.3 Services scope
|
|
224
|
+
|
|
225
|
+
| Service | In scope | Out of scope (this pass) |
|
|
226
|
+
|---------|----------|--------------------------|
|
|
227
|
+
| **NickServ** | `REGISTER`, `IDENTIFY`, `LOGOUT`, `GHOST`, `DROP`, `INFO`, `SET ENFORCE`/`PASSWORD`/`EMAIL`, link to SASL account, `+r` user mode, nick enforcement | CertFP (covered by mTLS SASL EXTERNAL ✅), grouped nicks (alias list) |
|
|
228
|
+
| **ChanServ** | `REGISTER`, `IDENTIFY`, `DROP`, `SET FOUNDER`/`MLOCK`/`RESTRICTED`/`KEEPTOPIC`, `ACCESS`/`LEVELS` (founder/sop/aop/vop/hop), `+r`/`+R`/`+M` channel modes, KEEPTOPIC persistence | ACL inheritance, fantasy commands, `OP`/`DEOP` shorthand (defer) |
|
|
229
|
+
| **HostServ** | `ON`/`OFF`/`REQUEST`/`SET` (oper), integrate with the existing cloak machinery (`packages/irc-core` cloaking shipped in Phase 5), `+t` user mode (vhost in use) | per-channel vhosts |
|
|
230
|
+
| **OperServ** | `AKILL` (network-wide ban, persisted + applied on connect), `JUPE` (block a nick/server name locally), `RAW` (oper-only, useful for recovery) | STATS aggregation (TICKET-104 🔄 covers server stats), `IGNORE` list |
|
|
231
|
+
| **MemoServ** | `SEND`, `LIST`, `READ`, `DEL`, `FORWARD`, send-on-identify (queued memos delivered at login) | channel memos, group memos |
|
|
232
|
+
| **BotServ** | — | **Out of scope entirely.** Channel-joined bots are a runtime cost serverless deployments should not absorb by default. Operators can run their own bots as ordinary clients. |
|
|
233
|
+
| **StatServ** | — | **Out of scope.** `STATS` (TICKET-104) + observability dashboards (TICKET-045 ✅) cover it. |
|
|
234
|
+
|
|
235
|
+
### 6.4 Nick enforcement policy
|
|
236
|
+
|
|
237
|
+
`NickServ SET ENFORCE <mode>` decides what happens when an
|
|
238
|
+
unidentified client uses a registered nick:
|
|
239
|
+
|
|
240
|
+
- `none` (default): warning notice only.
|
|
241
|
+
- `ghost`: kick the existing unidentified client off after a grace
|
|
242
|
+
period (default 30s); connection is force-disconnected via the
|
|
243
|
+
existing `disconnect(conn, reason)` runtime method.
|
|
244
|
+
- `kill`: same as ghost but immediately.
|
|
245
|
+
|
|
246
|
+
Implemented as a new `EnforceNick` effect emitted from the registration
|
|
247
|
+
reducer when the chosen nick matches a registered account the
|
|
248
|
+
connection is not identified as. The dispatch layer resolves it against
|
|
249
|
+
`runtime.disconnect(...)`.
|
|
250
|
+
|
|
251
|
+
### 6.5 Services ↔ daemon hooks
|
|
252
|
+
|
|
253
|
+
Already-present seams the services reuse:
|
|
254
|
+
|
|
255
|
+
- `AccountStore.verify('PLAIN', ...)` (TICKET-026 ✅) → a successful
|
|
256
|
+
NickServ `IDENTIFY` and a SASL PLAIN login both reduce to "this
|
|
257
|
+
connection is now account X", setting the `+r` user mode and
|
|
258
|
+
emitting `ACCOUNT` to `account-notify`-capable peers.
|
|
259
|
+
- `MessageStore` (TICKET-063 ✅) → MemoServ's `SEND` is a record into
|
|
260
|
+
a per-account memo queue; delivery is a fanout at identify time.
|
|
261
|
+
- `clock` / `IdFactory` ports → memo ids, enforcement grace timers.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 7. Proposed tickets
|
|
266
|
+
|
|
267
|
+
Ticket numbers are placeholders starting at **TICKET-110** (next free
|
|
268
|
+
id per `todo.md`). Final ids assigned at ratification. Each ticket
|
|
269
|
+
follows strict TDD (Red → Green → Refactor) per `CLAUDE.md` /
|
|
270
|
+
`AGENTS.md`.
|
|
271
|
+
|
|
272
|
+
### Phase 12 — IRCv3 extensions
|
|
273
|
+
|
|
274
|
+
#### TICKET-110 — IRCv3 `account-notify` (push `ACCOUNT` on login/logout)
|
|
275
|
+
- **Phase:** 12
|
|
276
|
+
- **Points:** 2
|
|
277
|
+
- **Dependencies:** TICKET-071 (`AccountStore` wiring, ✅)
|
|
278
|
+
- **Description:** Advertise `account-notify`; emit `ACCOUNT <name|*>`
|
|
279
|
+
to peers in shared channels (excluding sender unless `echo-message`)
|
|
280
|
+
on SASL success, NickServ IDENTIFY (services), and `LOGOUT`. Reuses
|
|
281
|
+
the `extended-join`/`chghost` broadcast helper.
|
|
282
|
+
- **Acceptance Criteria:**
|
|
283
|
+
- `CAP LS` includes `account-notify`.
|
|
284
|
+
- A login by peer A visible to peer B (sharing a channel, cap
|
|
285
|
+
negotiated) produces `:A ACCOUNT alice`.
|
|
286
|
+
- A logout produces `:A ACCOUNT *`.
|
|
287
|
+
- No `ACCOUNT` messages to clients without the cap.
|
|
288
|
+
- 100% coverage on the new broadcast branch.
|
|
289
|
+
- **TDD Outline:** Red on "B sees `ACCOUNT` after A's SASL success";
|
|
290
|
+
green via the broadcast helper; then cap-gating + echo-message path.
|
|
291
|
+
|
|
292
|
+
#### TICKET-111 — IRCv3 `msgid` (emit `msgid` tag on every message)
|
|
293
|
+
- **Phase:** 12
|
|
294
|
+
- **Points:** 2
|
|
295
|
+
- **Dependencies:** TICKET-063 (chathistory msgid plumbing, ✅)
|
|
296
|
+
- **Description:** Advertise `msgid` as a standalone cap. Every
|
|
297
|
+
PRIVMSG/NOTICE/TAGMSG emitted to cap-enabled clients carries a
|
|
298
|
+
`+msgid=<id>` tag from `IdFactory.nonce()`. The chathistory reducer
|
|
299
|
+
already records the msgid; this ticket surfaces it on the live
|
|
300
|
+
fanout line, not just on replay.
|
|
301
|
+
- **Acceptance Criteria:**
|
|
302
|
+
- `CAP LS` includes `msgid`.
|
|
303
|
+
- Live PRIVMSG to a cap-enabled peer carries `@+msgid=<id>`.
|
|
304
|
+
- The msgid on the live line and on the chathistory replay line
|
|
305
|
+
match for the same recorded message.
|
|
306
|
+
- Clients without the cap see no `msgid` tag (existing behavior).
|
|
307
|
+
- 100% coverage on the tag-emission branch.
|
|
308
|
+
- **TDD Outline:** Red on "peer with `msgid` cap sees `@+msgid=…` on a
|
|
309
|
+
PRIVMSG"; green; then assert parity with the recorded msgid.
|
|
310
|
+
|
|
311
|
+
#### TICKET-112 — IRCv3 `standard-replies` (`FAIL`/`WARN`/`NOTE`)
|
|
312
|
+
- **Phase:** 12
|
|
313
|
+
- **Points:** 3
|
|
314
|
+
- **Dependencies:** —
|
|
315
|
+
- **Description:** Advertise `standard-replies`. For cap-enabled
|
|
316
|
+
clients, replace a curated subset of bare numeric error replies
|
|
317
|
+
(`461 ERR_NEEDMOREPARAMS`, `403 ERR_NOSUCHCHANNEL`, `432/433`
|
|
318
|
+
registration errors, SASL failures) with the structured
|
|
319
|
+
`FAIL COMMAND code context :description` form. The code-path
|
|
320
|
+
branches that *should not* change (numeric-only replies like MOTD
|
|
321
|
+
lines, NAMES) are left on the numeric form.
|
|
322
|
+
- **Acceptance Criteria:**
|
|
323
|
+
- `CAP LS` includes `standard-replies`.
|
|
324
|
+
- For each curated numeric, a cap-enabled client receives a
|
|
325
|
+
`FAIL`/`WARN`/`NOTE` line; a legacy client receives the unchanged
|
|
326
|
+
numeric.
|
|
327
|
+
- The mapping is data-driven (a `numericToStandardReply` table), not
|
|
328
|
+
scattered `if (hasCap)` checks.
|
|
329
|
+
- 100% coverage on the table + dispatch helper.
|
|
330
|
+
- **TDD Outline:** Red on `461`→`FAIL` for cap-enabled client; green
|
|
331
|
+
via the table; then each curated code; then the legacy fallback.
|
|
332
|
+
|
|
333
|
+
#### TICKET-113 — IRCv3 `MONITOR` (modern `ISON` replacement)
|
|
334
|
+
- **Phase:** 12
|
|
335
|
+
- **Points:** 5
|
|
336
|
+
- **Dependencies:** —
|
|
337
|
+
- **Description:** Advertise `MONITOR=<n>` (configurable max, default
|
|
338
|
+
30). Implement the `MONITOR + nick{,nick}` / `-` / `C` / `L` / `S`
|
|
339
|
+
subcommands and the `730 RPL_MONONLINE` / `731 RPL_MONOFFLINE` /
|
|
340
|
+
`732 RPL_MONLIST` / `734 ERR_MONLISTFULL` numerics. Online/offline
|
|
341
|
+
transitions push `730`/`731` asynchronously via the Registry's nick
|
|
342
|
+
reserve/release hooks.
|
|
343
|
+
- **Acceptance Criteria:**
|
|
344
|
+
- `005` advertises `MONITOR=30`.
|
|
345
|
+
- `MONITOR +alice` while alice is offline → `731`.
|
|
346
|
+
- Alice connects → `730` pushed to the watcher.
|
|
347
|
+
- `MONITOR L` lists the watchlist; `MONITOR C` clears it.
|
|
348
|
+
- Over-limit → `734` with the rejected nick.
|
|
349
|
+
- 100% coverage on the reducer and the async push hook.
|
|
350
|
+
- **TDD Outline:** Red on the offline→`731` path; green; then the
|
|
351
|
+
online push; then list/clear/over-limit.
|
|
352
|
+
|
|
353
|
+
#### TICKET-114 — IRCv3 `labeled-response` (correlate request→reply via `BATCH`)
|
|
354
|
+
- **Phase:** 12
|
|
355
|
+
- **Points:** 8
|
|
356
|
+
- **Dependencies:** TICKET-025 (`batch`, ✅)
|
|
357
|
+
- **Description:** Advertise `labeled-response`. A client sends a
|
|
358
|
+
message with `+label=<id>`; the server wraps every reply caused by
|
|
359
|
+
that message in a `BATCH +id labeled-response … BATCH -id`. If the
|
|
360
|
+
request produces no reply, the server emits an empty batch. The
|
|
361
|
+
label is plumbed through `ctx` so every reducer's emitted effects
|
|
362
|
+
inherit it.
|
|
363
|
+
- **Acceptance Criteria:**
|
|
364
|
+
- `CAP LS` includes `labeled-response`.
|
|
365
|
+
- A `PRIVMSG` carrying `+label=foo` produces a `BATCH +foo
|
|
366
|
+
labeled-response` (echo + any numerics) `BATCH -foo` for
|
|
367
|
+
cap-enabled clients.
|
|
368
|
+
- A command that produces no reply (e.g. a no-op `PING` already
|
|
369
|
+
covered by `PONG` is *not* empty; a `JOIN` to a nonexistent channel
|
|
370
|
+
that 403s still wraps the 403) — spec edge cases enumerated.
|
|
371
|
+
- 100% coverage on the label plumbing + wrapping helper.
|
|
372
|
+
- **TDD Outline:** Red on "PRIVMSG with `+label=foo` wraps echo in
|
|
373
|
+
BATCH"; green; then the no-reply case; then the multi-reply case.
|
|
374
|
+
|
|
375
|
+
#### TICKET-115 — IRCv3 `sts` (Strict Transport Security policy)
|
|
376
|
+
- **Phase:** 12
|
|
377
|
+
- **Points:** 2
|
|
378
|
+
- **Dependencies:** dual transport (Phase 7 ✅)
|
|
379
|
+
- **Description:** Advertise `sts` with a deployment-configurable
|
|
380
|
+
policy (`duration`, `port` for the TLS listener, optional `preload`).
|
|
381
|
+
On a plaintext/WebSocket connection (when one exists), the server
|
|
382
|
+
emits the `STS` policy so compliant clients upgrade; on the TLS
|
|
383
|
+
connection the policy is still sent to lock in the duration.
|
|
384
|
+
- **Acceptance Criteria:**
|
|
385
|
+
- `CAP LS` includes `sts=duration=…,port=6697`.
|
|
386
|
+
- Values come from `ServerConfig`, not literals.
|
|
387
|
+
- A wss / irc+tls connection advertises the duration.
|
|
388
|
+
- 100% coverage on the policy builder.
|
|
389
|
+
- **TDD Outline:** Red on cap advertisement; green; then override via
|
|
390
|
+
config.
|
|
391
|
+
|
|
392
|
+
#### TICKET-116 — IRCv3 `draft/typing` (typing indicator broadcast)
|
|
393
|
+
- **Phase:** 12
|
|
394
|
+
- **Points:** 2
|
|
395
|
+
- **Dependencies:** TICKET-076 (`TAGMSG`, ✅)
|
|
396
|
+
- **Description:** Advertise `draft/typing`. A client sends
|
|
397
|
+
`TAGMSG <target> +draft/typing=active|paused|done`; the server
|
|
398
|
+
broadcasts to channel members (or routes to a user target),
|
|
399
|
+
excluding the sender unless `echo-message` is negotiated. No state
|
|
400
|
+
is kept server-side.
|
|
401
|
+
- **Acceptance Criteria:**
|
|
402
|
+
- `CAP LS` includes `draft/typing`.
|
|
403
|
+
- Channel typing TAGMSG fans out to cap-enabled members.
|
|
404
|
+
- The `+draft/typing=*` client tag is whitelisted for clients
|
|
405
|
+
without `message-tags` (per the spec's client-tag exception).
|
|
406
|
+
- 100% coverage on the whitelist + broadcast.
|
|
407
|
+
- **TDD Outline:** Red on channel typing fanout; green; then
|
|
408
|
+
echo-message path; then user-target path.
|
|
409
|
+
|
|
410
|
+
#### TICKET-117 — IRCv3 `draft/multiline` (multi-line `BATCH`)
|
|
411
|
+
- **Phase:** 12
|
|
412
|
+
- **Points:** 5
|
|
413
|
+
- **Dependencies:** TICKET-025 (`batch`, ✅)
|
|
414
|
+
- **Description:** Advertise `draft/multiline` with a configurable
|
|
415
|
+
byte budget (default 4096). A cap-enabled client sends
|
|
416
|
+
`BATCH +id draft/multiline :target` followed by N PRIVMSG/NOTICE
|
|
417
|
+
lines, then `BATCH -id`. The server concatenates per spec (with
|
|
418
|
+
`\n`), enforces the byte cap, and broadcasts as a single PRIVMSG
|
|
419
|
+
carrying a `+draft/multiline` tag plus the joined body (or a
|
|
420
|
+
batched replay to other multiline-capable peers — spec calls for
|
|
421
|
+
the joined form to legacy peers and the batch form to cap peers).
|
|
422
|
+
- **Acceptance Criteria:**
|
|
423
|
+
- `CAP LS` includes `draft/multiline=35615` (or configured max).
|
|
424
|
+
- Multi-line batch from a cap-enabled client is delivered to
|
|
425
|
+
cap-enabled peers as a `draft/multiline` BATCH.
|
|
426
|
+
- Legacy peers receive the joined single PRIVMSG.
|
|
427
|
+
- Over-budget batch is rejected with `FAIL`.
|
|
428
|
+
- 100% coverage on the reducer + byte-budget enforcement.
|
|
429
|
+
- **TDD Outline:** Red on a 2-line batch delivered as joined PRIVMSG
|
|
430
|
+
to a legacy peer; green; then the cap-peer batched delivery; then
|
|
431
|
+
budget enforcement.
|
|
432
|
+
|
|
433
|
+
#### TICKET-118 — IRCv3 `draft/read-marker` (persistent last-read)
|
|
434
|
+
- **Phase:** 12
|
|
435
|
+
- **Points:** 5
|
|
436
|
+
- **Dependencies:** TICKET-063 (chathistory, ✅), services `UserStore`
|
|
437
|
+
(TICKET-122)
|
|
438
|
+
- **Description:** Advertise `draft/read-marker`. The chathistory
|
|
439
|
+
work already keeps a per-`(connection, channel)` last-read marker
|
|
440
|
+
in `ConnectionState`. This ticket persists the marker per
|
|
441
|
+
`(account, channel)` in `ServicesStore`, propagates updates via
|
|
442
|
+
`+draft/read-marker=<msgid>` tags on relevant messages, and seeds a
|
|
443
|
+
fresh connection's marker from the persisted value at identify time.
|
|
444
|
+
- **Acceptance Criteria:**
|
|
445
|
+
- `CAP LS` includes `draft/read-marker`.
|
|
446
|
+
- Marking a message read updates the persisted marker.
|
|
447
|
+
- Reconnect restores the marker.
|
|
448
|
+
- `CHATHISTORY BEFORE` honours the marker in the absence of an
|
|
449
|
+
explicit pivot.
|
|
450
|
+
- 100% coverage on the persistence + tag emission.
|
|
451
|
+
- **TDD Outline:** Red on reconnect-restore; green via the store;
|
|
452
|
+
then tag emission; then CHATHISTORY integration.
|
|
453
|
+
- **Implementation notes (shipped):** Landed as a focused
|
|
454
|
+
`ReadMarkerStore` port (`get` / `set` / `forAccount`) plus an
|
|
455
|
+
`InMemoryReadMarkerStore` reference impl in `irc-core/ports.ts`, rather
|
|
456
|
+
than waiting on the full services `UserStore` (TICKET-122). The mark is
|
|
457
|
+
delivered as a `TAGMSG` carrying `+draft/read-marker=<msgid>` (msgid-based,
|
|
458
|
+
composing with the chathistory marker plumbing, diverging from the
|
|
459
|
+
work-in-progress ircv3 draft's timestamped `MARKREAD` verb). A mark TAGMSG
|
|
460
|
+
is persisted via `persistReadMarker`, fanned out to `message-tags` OR
|
|
461
|
+
`draft/read-marker` peers (mirroring the `draft/typing` precedent), and
|
|
462
|
+
`+draft/read-marker` is whitelisted in `filterClientTags` so draft-only
|
|
463
|
+
peers keep the tag. SASL identify seeds `lastReadMarkers` via
|
|
464
|
+
`seedReadMarkers`; `CHATHISTORY BEFORE` with no pivot falls back to the
|
|
465
|
+
connection's marker. TICKET-122 should fold this store into the unified
|
|
466
|
+
services `UserStore` when it lands.
|
|
467
|
+
|
|
468
|
+
#### TICKET-119 — IRCv3 `draft/pre-away` (persist away status)
|
|
469
|
+
- **Phase:** 12
|
|
470
|
+
- **Points:** 3
|
|
471
|
+
- **Dependencies:** TICKET-061 (`AWAY`, ✅), services `UserStore`
|
|
472
|
+
(TICKET-122)
|
|
473
|
+
- **Description:** Advertise `draft/pre-away`. Persist away reason in
|
|
474
|
+
`ServicesStore` keyed by account. On identify (or SASL login), the
|
|
475
|
+
server re-applies the away state and emits the `AWAY` line +
|
|
476
|
+
`306 RPL_NOWAWAY` as if the user had sent `AWAY :reason` themselves.
|
|
477
|
+
Noted out-of-scope in TICKET-063.
|
|
478
|
+
- **Acceptance Criteria:**
|
|
479
|
+
- `CAP LS` includes `draft/pre-away`.
|
|
480
|
+
- An identified user who previously went `AWAY :brb` and reconnects
|
|
481
|
+
is auto-marked away.
|
|
482
|
+
- `AWAY` (unset) clears the persisted reason.
|
|
483
|
+
- 100% coverage on the persistence + replay.
|
|
484
|
+
- **TDD Outline:** Red on reconnect auto-away; green; then the unset
|
|
485
|
+
path.
|
|
486
|
+
- **Implementation notes (shipped):** Landed as a focused `AwayStore`
|
|
487
|
+
port (`get` / `set` / `clear`) plus an `InMemoryAwayStore` reference
|
|
488
|
+
impl in `irc-core/ports.ts`, rather than waiting on the full services
|
|
489
|
+
`UserStore` (TICKET-122) — same shape as the TICKET-118 `ReadMarkerStore`
|
|
490
|
+
precedent. The `awayReducer` calls `persistAway` on set and
|
|
491
|
+
`clearPersistedAway` on unset (gated on a bound store AND an
|
|
492
|
+
identified connection, so unidentified sessions still track their own
|
|
493
|
+
in-memory `away` for the duration of the connection). SASL identify
|
|
494
|
+
calls `replayPersistedAway` to copy a stored reason onto the fresh
|
|
495
|
+
connection's `ConnectionState.away` and emits a single
|
|
496
|
+
`306 RPL_NOWAWAY` so the user sees they are still away. The
|
|
497
|
+
companion `away-notify` broadcast is intentionally NOT emitted at
|
|
498
|
+
identify time — the connection has not joined any channels yet, so it
|
|
499
|
+
would have no audience; peers learn the away state through the normal
|
|
500
|
+
`WHO` / `WHOIS` / `AWAY` queries once the user joins. TICKET-122
|
|
501
|
+
should fold this store into the unified services `UserStore` when it
|
|
502
|
+
lands.
|
|
503
|
+
|
|
504
|
+
#### TICKET-120 — ISUPPORT tokens for new caps + modes
|
|
505
|
+
- **Phase:** 12
|
|
506
|
+
- **Points:** 2
|
|
507
|
+
- **Dependencies:** the cap tickets above + services modes
|
|
508
|
+
(TICKET-122)
|
|
509
|
+
- **Description:** Add `MONITOR=<n>`, `STATUSMSG=@+`, `EXTBAN=~,q`,
|
|
510
|
+
`ACCOUNTEXTBAN=a`, draft tokens for `draft/typing`/`draft/multiline`
|
|
511
|
+
to `generateIsupport`. Tokens are config-derived where applicable
|
|
512
|
+
(max monitor count, extban prefix).
|
|
513
|
+
- **Acceptance Criteria:**
|
|
514
|
+
- Each token advertises the configured value.
|
|
515
|
+
- Tokens only appear when the relevant cap/mode is supported (e.g.
|
|
516
|
+
`ACCOUNTEXTBAN` only once services ship).
|
|
517
|
+
- 100% coverage on the new branches.
|
|
518
|
+
- **TDD Outline:** Red on each missing token; green by adding it.
|
|
519
|
+
|
|
520
|
+
#### TICKET-121 — User mode `S` (TLS connected)
|
|
521
|
+
- **Phase:** 12
|
|
522
|
+
- **Points:** 1
|
|
523
|
+
- **Dependencies:** dual transport (Phase 7 ✅)
|
|
524
|
+
- **Description:** When the connection is over TLS (wss or irc+tls),
|
|
525
|
+
set user mode `S` read-only on the connection at registration. The
|
|
526
|
+
mode is not settable via `MODE` (parse rejects `+S` with `501`).
|
|
527
|
+
Surface in WHOIS (`:server 276 nick target :is using a secure
|
|
528
|
+
connection`) for clients that advertise the `RPL_WHOISSECURE` cap.
|
|
529
|
+
- **Acceptance Criteria:**
|
|
530
|
+
- TLS connection → `+S` user mode visible in `WHO`/`WHOIS`.
|
|
531
|
+
- `MODE nick +S` rejected with `501 ERR_UMODEUNKNOWNFLAG`.
|
|
532
|
+
- 100% coverage on the mode-set + parse rejection.
|
|
533
|
+
- **TDD Outline:** Red on TLS registration setting `+S`; green; then
|
|
534
|
+
the parse rejection.
|
|
535
|
+
|
|
536
|
+
### Phase 13 — IRC services
|
|
537
|
+
|
|
538
|
+
#### TICKET-122 — `ServicesStore` port + in-memory reference impl
|
|
539
|
+
- **Phase:** 13
|
|
540
|
+
- **Points:** 5
|
|
541
|
+
- **Dependencies:** TICKET-029 (`IrcRuntime` port, ✅),
|
|
542
|
+
TICKET-071 (`AccountStore`, ✅)
|
|
543
|
+
- **Description:** Define `ServicesStore` (§6.2) in
|
|
544
|
+
`packages/irc-core/src/ports.ts`. Ship `InMemoryServicesStore`
|
|
545
|
+
(in-memory-runtime) covering NickServ/ChanServ/HostServ/MemoServ
|
|
546
|
+
primitives. Adapter backends (D1, DynamoDB) ship in dedicated
|
|
547
|
+
follow-on tickets.
|
|
548
|
+
- **Acceptance Criteria:**
|
|
549
|
+
- Port interface compiles under strict mode.
|
|
550
|
+
- `InMemoryServicesStore` implements every method; 100% coverage.
|
|
551
|
+
- Reused by local-cli and unit tests.
|
|
552
|
+
- **TDD Outline:** Red on `registerNick`/`verifyNick` round-trip;
|
|
553
|
+
green; then each method.
|
|
554
|
+
|
|
555
|
+
#### TICKET-123 — NickServ: REGISTER / IDENTIFY / DROP / INFO + `+r`
|
|
556
|
+
- **Phase:** 13
|
|
557
|
+
- **Points:** 8
|
|
558
|
+
- **Dependencies:** TICKET-122
|
|
559
|
+
- **Description:** Wire `PRIVMSG NickServ :<command>` to a NickServ
|
|
560
|
+
reducer. `REGISTER password email` creates a SASL account (reuses
|
|
561
|
+
the existing `AccountStore` so SASL PLAIN works immediately after
|
|
562
|
+
registration). `IDENTIFY password` logs the current connection in
|
|
563
|
+
(sets `+r` user mode, emits `ACCOUNT` to `account-notify` peers).
|
|
564
|
+
`DROP` removes the registration. `INFO [nick]` queries it.
|
|
565
|
+
Successful IDENTIFY triggers MemoServ delivery of queued memos.
|
|
566
|
+
- **Acceptance Criteria:**
|
|
567
|
+
- `REGISTER hunter2 alice@example.com` followed by SASL PLAIN with
|
|
568
|
+
`alice:hunter2` succeeds.
|
|
569
|
+
- `IDENTIFY hunter2` from an unidentified connection sets `+r` and
|
|
570
|
+
emits `ACCOUNT`.
|
|
571
|
+
- `DROP alice` requires re-identify.
|
|
572
|
+
- 100% coverage on the reducer + state transitions.
|
|
573
|
+
- **TDD Outline:** Red on REGISTER→SASL round-trip; green; then
|
|
574
|
+
IDENTIFY; then DROP; then INFO.
|
|
575
|
+
|
|
576
|
+
#### TICKET-124 — NickServ: nick enforcement (`SET ENFORCE`)
|
|
577
|
+
- **Phase:** 13
|
|
578
|
+
- **Points:** 5
|
|
579
|
+
- **Dependencies:** TICKET-123
|
|
580
|
+
- **Description:** Implement `NickServ SET ENFORCE none|ghost|kill`
|
|
581
|
+
plus the `EnforceNick` effect. When a client registers (NICK) with
|
|
582
|
+
a nick that is registered to an account they are not identified as,
|
|
583
|
+
the policy fires after the configured grace period. `ghost` force-
|
|
584
|
+
disconnects; `kill` force-disconnects immediately. Default policy
|
|
585
|
+
per registered account is `none`.
|
|
586
|
+
- **Acceptance Criteria:**
|
|
587
|
+
- `SET ENFORCE ghost` + unidentified use of the nick → grace
|
|
588
|
+
period → `disconnect("Nick enforced by NickServ")`.
|
|
589
|
+
- `SET ENFORCE kill` → immediate disconnect.
|
|
590
|
+
- `SET ENFORCE none` → warning notice only.
|
|
591
|
+
- 100% coverage on the effect + dispatch integration.
|
|
592
|
+
- **TDD Outline:** Red on grace-period disconnect; green; then each
|
|
593
|
+
policy.
|
|
594
|
+
|
|
595
|
+
#### TICKET-125 — ChanServ: REGISTER / DROP / SET + `+r`/`+R`/`+M`
|
|
596
|
+
- **Phase:** 13
|
|
597
|
+
- **Points:** 8
|
|
598
|
+
- **Dependencies:** TICKET-123, TICKET-011 (`MODE`, ✅)
|
|
599
|
+
- **Description:** Wire `PRIVMSG ChanServ :<command>`. `REGISTER`
|
|
600
|
+
records the channel founder; sets channel mode `+r`. `SET
|
|
601
|
+
FOUNDER/MLOCK/RESTRICTED/KEEPTOPIC` updates the persisted channel
|
|
602
|
+
record. New channel modes: `+r` (registered, settable only by
|
|
603
|
+
ChanServ), `+R` (block join/message from unidentified), `+M` (mod
|
|
604
|
+
speak requires identification). `KEEPTOPIC` persists the topic and
|
|
605
|
+
restores it on the first join after the channel becomes empty.
|
|
606
|
+
- **Acceptance Criteria:**
|
|
607
|
+
- `REGISTER #chan` by an identified founder sets `+r`.
|
|
608
|
+
- `+R` blocks join from unidentified (`519 ERR_ALLMUSTREGISTER`,
|
|
609
|
+
server-specific numeric) and PRIVMSG (`404`).
|
|
610
|
+
- `KEEPTOPIC` round-trips a topic across empty-recreate.
|
|
611
|
+
- `MLOCK` reapplies the locked modes if an op tries to unset them.
|
|
612
|
+
- 100% coverage on the reducer + mode interactions.
|
|
613
|
+
- **TDD Outline:** Red on REGISTER→`+r`; green; then `+R`/`+M`;
|
|
614
|
+
then KEEPTOPIC; then MLOCK.
|
|
615
|
+
|
|
616
|
+
#### TICKET-126 — ChanServ: ACCESS / LEVELS (SOP/AOP/VOP/HOP)
|
|
617
|
+
- **Phase:** 13
|
|
618
|
+
- **Points:** 5
|
|
619
|
+
- **Dependencies:** TICKET-125
|
|
620
|
+
- **Description:** `ChanServ ACCESS #chan ADD nick level` and the
|
|
621
|
+
shorthand `SOP`/`AOP`/`VOP`/`HOP` variants map an account to a
|
|
622
|
+
privilege level. On JOIN, ChanServ applies the matching channel
|
|
623
|
+
prefix (`@`, `+`) automatically. `LEVELS` lets the founder redefine
|
|
624
|
+
what numeric level each command requires.
|
|
625
|
+
- **Acceptance Criteria:**
|
|
626
|
+
- `AOP alice ADD` → alice auto-opped on JOIN.
|
|
627
|
+
- `VOP bob ADD` → bob auto-voiced.
|
|
628
|
+
- `ACCESS #chan DEL alice` removes.
|
|
629
|
+
- `LEVELS SET AUTOOP 5` redefines the threshold.
|
|
630
|
+
- 100% coverage on the access reducer + JOIN hook.
|
|
631
|
+
- **TDD Outline:** Red on AOP→auto-op; green; then DEL; then LEVELS.
|
|
632
|
+
|
|
633
|
+
#### TICKET-127 — HostServ: ON / OFF / REQUEST / SET (oper)
|
|
634
|
+
- **Phase:** 13
|
|
635
|
+
- **Points:** 3
|
|
636
|
+
- **Dependencies:** TICKET-122, cloaking (Phase 5 ✅)
|
|
637
|
+
- **Description:** Wire `PRIVMSG HostServ :<command>`. `REQUEST
|
|
638
|
+
vhost` queues a request for oper approval (or auto-approves if
|
|
639
|
+
configured). `SET account vhost` (oper-only) sets it directly.
|
|
640
|
+
`ON`/`OFF` toggles the cloak at runtime; the cloak machinery
|
|
641
|
+
already exists, this ticket wires it to per-account state and emits
|
|
642
|
+
`CHGHOST` (TICKET-022 ✅) on toggle.
|
|
643
|
+
- **Acceptance Criteria:**
|
|
644
|
+
- `SET alice example.net` + `ON` → `CHGHOST` emitted to peers;
|
|
645
|
+
WHOIS shows the vhost.
|
|
646
|
+
- `OFF` restores the real (cloaked) host.
|
|
647
|
+
- 100% coverage on the reducer + CHGHOST emission.
|
|
648
|
+
- **TDD Outline:** Red on `ON`→`CHGHOST`; green; then `OFF`; then
|
|
649
|
+
`REQUEST`/`SET` approval path.
|
|
650
|
+
|
|
651
|
+
#### TICKET-128 — OperServ: AKILL / JUPE / RAW
|
|
652
|
+
- **Phase:** 13
|
|
653
|
+
- **Points:** 5
|
|
654
|
+
- **Dependencies:** TICKET-075 (`OPER`, ✅), TICKET-122
|
|
655
|
+
- **Description:** Wire `PRIVMSG OperServ :<command>` (oper-only;
|
|
656
|
+
rejected with a services notice otherwise). `AKILL ADD mask reason`
|
|
657
|
+
records a network-wide ban persisted in `ServicesStore`; the
|
|
658
|
+
`$connect`/`$default` admission path checks it on every new
|
|
659
|
+
connection and on every PRIVMSG, force-disconnecting on match.
|
|
660
|
+
`JUPE nick` blocks a nick/server name locally (NICK collision
|
|
661
|
+
reducer returns `432` for the jupe'd nick). `RAW <line>` is oper-
|
|
662
|
+
only and emits a raw IRC line (useful for recovery; logged).
|
|
663
|
+
- **Acceptance Criteria:**
|
|
664
|
+
- `AKILL ADD *!*@bad.example.net flooding` → next matching
|
|
665
|
+
connection is disconnected at `$connect`.
|
|
666
|
+
- `JUPE moo` → `NICK moo` returns `432 ERR_ERRONEUSNICKNAME`.
|
|
667
|
+
- Non-oper → service notice rejection, no state change.
|
|
668
|
+
- 100% coverage on each reducer + the admission hook.
|
|
669
|
+
- **TDD Outline:** Red on AKILL disconnect; green; then JUPE; then
|
|
670
|
+
RAW; then non-oper rejection.
|
|
671
|
+
|
|
672
|
+
#### TICKET-129 — MemoServ: SEND / LIST / READ / DEL + queue delivery
|
|
673
|
+
- **Phase:** 13
|
|
674
|
+
- **Points:** 5
|
|
675
|
+
- **Dependencies:** TICKET-123, TICKET-122 (`MessageStore` pattern,
|
|
676
|
+
✅)
|
|
677
|
+
- **Description:** Wire `PRIVMSG MemoServ :<command>`. `SEND nick
|
|
678
|
+
body` records a memo against the recipient's account. On successful
|
|
679
|
+
NickServ IDENTIFY or SASL login, queued memos are delivered (via
|
|
680
|
+
the existing MemoServ pseudo-client notice format). `LIST`/`READ`/
|
|
681
|
+
`DEL`/`FORWARD` round out the surface.
|
|
682
|
+
- **Acceptance Criteria:**
|
|
683
|
+
- `SEND alice Hi` while alice is offline → alice receives the memo
|
|
684
|
+
on next IDENTIFY.
|
|
685
|
+
- `LIST` enumerates unread.
|
|
686
|
+
- `READ n` delivers memo #n; `DEL n` removes.
|
|
687
|
+
- 100% coverage on the reducer + delivery hook.
|
|
688
|
+
- **TDD Outline:** Red on offline-send→identify-delivery; green; then
|
|
689
|
+
list/read/del.
|
|
690
|
+
|
|
691
|
+
#### TICKET-130 — Adapter backends for `ServicesStore`
|
|
692
|
+
- **Phase:** 13
|
|
693
|
+
- **Points:** 8
|
|
694
|
+
- **Dependencies:** TICKET-122, all of TICKET-123..129 (reducers
|
|
695
|
+
landed against the in-memory store first)
|
|
696
|
+
- **Description:** Ship persistent `ServicesStore` implementations:
|
|
697
|
+
- CF: D1 (share the database from TICKET-093 ✅), new tables
|
|
698
|
+
`nickserv_accounts`, `chanserv_channels`, `chanserv_access`,
|
|
699
|
+
`hostserv_vhosts`, `memoserv_memos`.
|
|
700
|
+
- AWS: DynamoDB, extending the existing `Accounts` table
|
|
701
|
+
(TICKET-072 ✅) and adding `Channels`, `ChannelAccess`, `Vhosts`,
|
|
702
|
+
`Memos`.
|
|
703
|
+
- Both adapters wire the store into `ConnectionActorOptions.services`.
|
|
704
|
+
- **Acceptance Criteria:**
|
|
705
|
+
- Each adapter: end-to-end NickServ register→identify→SASL PLAIN.
|
|
706
|
+
- Channel registration survives a deploy (state persisted).
|
|
707
|
+
- Memos delivered on reconnect after identify.
|
|
708
|
+
- 100% coverage on the new store implementations.
|
|
709
|
+
- **TDD Outline:** Red per adapter on REGISTER→survive-restart; green
|
|
710
|
+
via the persistent store.
|
|
711
|
+
|
|
712
|
+
#### TICKET-131 — Docs: services reference + admin tooling
|
|
713
|
+
- **Phase:** 13
|
|
714
|
+
- **Points:** 2
|
|
715
|
+
- **Dependencies:** TICKET-130
|
|
716
|
+
- **Description:** `docs/Services.md` covering: integrated-services
|
|
717
|
+
architecture decision (§6.1), per-service command reference, admin
|
|
718
|
+
tooling (`tools/seed-nickserv-accounts.ts` mirroring the existing
|
|
719
|
+
`seed-{aws,cf}-accounts.ts` scripts), migration notes for operators
|
|
720
|
+
coming from Atheme/Anope.
|
|
721
|
+
- **Acceptance Criteria:**
|
|
722
|
+
- A new contributor can register a NickServ account and a ChanServ
|
|
723
|
+
channel end-to-end following only the doc.
|
|
724
|
+
- The migration caveat (no Atheme/Anope compatibility) is called
|
|
725
|
+
out up front.
|
|
726
|
+
- **TDD Outline:** N/A (documentation). Reviewed by walking the
|
|
727
|
+
steps from a clean checkout.
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## 8. Sequencing
|
|
732
|
+
|
|
733
|
+
Recommended landing order, respecting dependencies:
|
|
734
|
+
|
|
735
|
+
1. **Cheap IRCv3 wins first.** TICKET-110 (`account-notify`),
|
|
736
|
+
TICKET-111 (`msgid`), TICKET-115 (`sts`), TICKET-121 (`+S` mode),
|
|
737
|
+
TICKET-116 (`draft/typing`). All small, all compose with shipped
|
|
738
|
+
work, all unblock better client UX.
|
|
739
|
+
2. **`MONITOR` + `standard-replies`.** TICKET-113, TICKET-112.
|
|
740
|
+
3. **`labeled-response`.** TICKET-114 — largest IRCv3 item, do last
|
|
741
|
+
among the stable caps; touches every reducer that emits a reply.
|
|
742
|
+
4. **Services foundation.** TICKET-122 (port + in-memory store).
|
|
743
|
+
5. **NickServ core.** TICKET-123, TICKET-124 — needed before ChanServ
|
|
744
|
+
because every other service keys off the identified account.
|
|
745
|
+
6. **ChanServ.** TICKET-125, TICKET-126.
|
|
746
|
+
7. **HostServ / OperServ / MemoServ.** TICKET-127, TICKET-128,
|
|
747
|
+
TICKET-129 (parallelizable).
|
|
748
|
+
8. **Persistent adapter backends.** TICKET-130 (unblocks any
|
|
749
|
+
production use of services).
|
|
750
|
+
9. **Draft caps that depend on services.** TICKET-118 (`draft/read-
|
|
751
|
+
marker`), TICKET-119 (`draft/pre-away`).
|
|
752
|
+
10. **`draft/multiline`.** TICKET-117 — independent, can land
|
|
753
|
+
anywhere after the IRCv3 cheap wins.
|
|
754
|
+
11. **Docs + ISUPPORT catch-up.** TICKET-131, TICKET-120.
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## 9. Risks
|
|
759
|
+
|
|
760
|
+
| Risk | Mitigation |
|
|
761
|
+
|------|------------|
|
|
762
|
+
| `labeled-response` plumbing touches every reducer | Land `BATCH` wrapping first (already shipped); add label propagation as a `ctx` field and a single dispatch hook. Refactor incrementally per reducer. |
|
|
763
|
+
| Integrated services drift from user expectations (Atheme parity) | Document the decision up front (TICKET-131); keep command names/behaviour aligned where it's free. |
|
|
764
|
+
| Nick enforcement races (NICK + IDENTIFY interleaved) | All mutations through the single `ServicesStore` authority per account; the `EnforceNick` effect is emitted after `reserveNick` succeeds, so the nick is owned before enforcement fires. |
|
|
765
|
+
| Services-backed caps (`draft/read-marker`, `draft/pre-away`) coupled to services launch | Track as Phase 12 tickets with explicit Phase 13 dependencies; ship IRCv3 cheap wins independently. |
|
|
766
|
+
| D1/DynamoDB cost from per-message `msgid` recording | `msgid` is emitted on the live line without persisting; only `MessageStore` records (chathistory) hit storage. Same cost profile as today. |
|
|
767
|
+
| `draft/multiline` byte budget abuse | Hard cap enforced in the reducer (`FAIL` over-budget); per-connection flood control (TICKET-065 ✅) bounds the burst. |
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
## 10. Definition of done (per phase)
|
|
772
|
+
|
|
773
|
+
**Phase 12 — IRCv3 extensions** is done when:
|
|
774
|
+
|
|
775
|
+
- [ ] Every stable IRCv3 cap in §3.1 is advertised and exercised by
|
|
776
|
+
the parametrized contract suite on the in-memory runtime.
|
|
777
|
+
- [ ] The draft caps in §3.2 are advertised under `draft/` namespace
|
|
778
|
+
and exercised; they can be flipped off via config without code
|
|
779
|
+
changes.
|
|
780
|
+
- [ ] `005 RPL_ISUPPORT` advertises every new token.
|
|
781
|
+
- [ ] Client compat sweep (TICKET-050 ⬜) passes against ≥3 clients
|
|
782
|
+
with the new caps negotiated.
|
|
783
|
+
|
|
784
|
+
**Phase 13 — IRC services** is done when:
|
|
785
|
+
|
|
786
|
+
- [ ] NickServ/ChanServ/HostServ/OperServ/MemoServ reducers ship at
|
|
787
|
+
100% coverage against `InMemoryServicesStore`.
|
|
788
|
+
- [ ] Both adapters ship persistent `ServicesStore` backends; the
|
|
789
|
+
parametrized contract suite is green on both.
|
|
790
|
+
- [ ] End-to-end: register → identify → SASL PLAIN → ChanServ
|
|
791
|
+
REGISTER → KEEPTOPIC round-trip survives a deploy restart.
|
|
792
|
+
- [ ] `docs/Services.md` is complete and reviewer-validated.
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## 11. Open questions
|
|
797
|
+
|
|
798
|
+
- **Account name vs. nick.** Should NickServ register an *account*
|
|
799
|
+
(multiple nicks group to it, Atheme-style) or register a single
|
|
800
|
+
nick? Default proposal: account-per-registration with nick grouping
|
|
801
|
+
as a follow-up, matching SASL's existing account model.
|
|
802
|
+
- **Memo size + retention.** Default cap (256 bytes? 2 KiB?) and TTL
|
|
803
|
+
(30 days?). Default proposal: 2 KiB per memo, 30-day TTL, both
|
|
804
|
+
config-tunable.
|
|
805
|
+
- **`+R`/`+M` numerics.** Server-specific `519 ERR_ALLMUSTREGISTER`
|
|
806
|
+
vs. reusing `473 ERR_INVITEONLYCHAN` semantics. Default proposal:
|
|
807
|
+
introduce the spec-recognized numerics where they exist, document
|
|
808
|
+
server-specific ones in `isupport`.
|
|
809
|
+
- **AKILL scope.** Network-wide (persisted) vs. per-listener (edge
|
|
810
|
+
only). Default proposal: persisted at the `ServicesStore`, enforced
|
|
811
|
+
in the admission path of every adapter — true network-wide on a
|
|
812
|
+
single-server deployment.
|