serverless-ircd 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.github/workflows/ci.yml +7 -3
  2. package/.gitmodules +1 -1
  3. package/CHANGELOG.md +518 -29
  4. package/README.md +221 -161
  5. package/apps/aws-stack/package.json +1 -1
  6. package/apps/aws-stack/src/aws-stack.ts +186 -18
  7. package/apps/aws-stack/tests/stack.test.ts +400 -56
  8. package/apps/cf-tcp-container/package.json +1 -1
  9. package/apps/cf-tcp-container/src/container-server.ts +21 -1
  10. package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
  11. package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
  12. package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
  13. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
  14. package/apps/cf-worker/package.json +1 -1
  15. package/apps/cf-worker/src/worker.ts +4 -4
  16. package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
  17. package/apps/cf-worker/tests/smoke.test.ts +5 -5
  18. package/apps/cf-worker/wrangler.test.toml +6 -6
  19. package/apps/cf-worker/wrangler.toml +7 -5
  20. package/apps/local-cli/package.json +1 -1
  21. package/apps/local-cli/src/config-loader.ts +8 -0
  22. package/apps/local-cli/src/main.ts +16 -0
  23. package/apps/local-cli/src/server.ts +95 -31
  24. package/apps/local-cli/tests/config-loader.test.ts +14 -0
  25. package/apps/local-cli/tests/config-resolution.test.ts +65 -0
  26. package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
  27. package/apps/local-cli/tests/rehash.test.ts +147 -0
  28. package/apps/local-cli/tests/server-helpers.test.ts +63 -0
  29. package/apps/local-cli/tests/tcp.test.ts +89 -0
  30. package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
  31. package/apps/web/landing/index.html +237 -16
  32. package/apps/web/package.json +3 -2
  33. package/apps/web/scripts/build.mjs +46 -16
  34. package/apps/web/src/build-env.ts +1 -1
  35. package/apps/web/src/config-schema.ts +6 -6
  36. package/apps/web/src/render-docs.ts +292 -0
  37. package/apps/web/tests/build-smoke.test.ts +43 -14
  38. package/apps/web/tests/config-schema.test.ts +1 -1
  39. package/apps/web/tests/landing-content.test.ts +103 -0
  40. package/apps/web/tests/render-docs.test.ts +198 -0
  41. package/docs/AWS-Adapter-Architecture.md +3 -2
  42. package/docs/AWS-Deployment.md +21 -8
  43. package/docs/Cloudflare-Deployment-Guide.md +22 -6
  44. package/docs/PlanExtensions.md +113 -3
  45. package/docs/Release-Process.md +23 -13
  46. package/docs/Services.md +578 -0
  47. package/docs/WebClientGuide.md +32 -31
  48. package/package.json +2 -2
  49. package/packages/aws-adapter/package.json +1 -1
  50. package/packages/aws-adapter/src/aws-runtime.ts +20 -1
  51. package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
  52. package/packages/aws-adapter/src/config-loader.ts +11 -0
  53. package/packages/aws-adapter/src/connection-counter.ts +89 -0
  54. package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
  55. package/packages/aws-adapter/src/handlers/connect.ts +55 -51
  56. package/packages/aws-adapter/src/handlers/default.ts +36 -4
  57. package/packages/aws-adapter/src/handlers/index.ts +15 -0
  58. package/packages/aws-adapter/src/handlers/nlb-stream.ts +25 -2
  59. package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
  60. package/packages/aws-adapter/src/index.ts +4 -0
  61. package/packages/aws-adapter/src/stats.ts +6 -1
  62. package/packages/aws-adapter/src/tables.ts +34 -4
  63. package/packages/aws-adapter/tests/aws-harness.ts +3 -0
  64. package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
  65. package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
  66. package/packages/aws-adapter/tests/connect.test.ts +158 -32
  67. package/packages/aws-adapter/tests/connection-counter.test.ts +144 -0
  68. package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
  69. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
  70. package/packages/aws-adapter/tests/global-setup.ts +28 -1
  71. package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
  72. package/packages/aws-adapter/tests/handlers.test.ts +105 -3
  73. package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
  74. package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
  75. package/packages/aws-adapter/tests/tables.test.ts +6 -1
  76. package/packages/cf-adapter/package.json +1 -1
  77. package/packages/cf-adapter/src/config-loader.ts +11 -0
  78. package/packages/cf-adapter/src/connection-do.ts +130 -8
  79. package/packages/cf-adapter/src/d1-services-store.ts +703 -0
  80. package/packages/cf-adapter/src/env.ts +8 -0
  81. package/packages/cf-adapter/src/index.ts +5 -0
  82. package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
  83. package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
  84. package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
  85. package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
  86. package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
  87. package/packages/cf-adapter/tests/serialize.test.ts +1 -0
  88. package/packages/in-memory-runtime/package.json +1 -1
  89. package/packages/irc-core/package.json +1 -1
  90. package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
  91. package/packages/irc-core/src/commands/account-auth.ts +200 -0
  92. package/packages/irc-core/src/commands/chanserv.ts +1166 -0
  93. package/packages/irc-core/src/commands/hostserv.ts +522 -0
  94. package/packages/irc-core/src/commands/index.ts +13 -0
  95. package/packages/irc-core/src/commands/join.ts +171 -9
  96. package/packages/irc-core/src/commands/markread.ts +202 -0
  97. package/packages/irc-core/src/commands/memoserv.ts +319 -0
  98. package/packages/irc-core/src/commands/mode.ts +96 -4
  99. package/packages/irc-core/src/commands/nickserv.ts +402 -0
  100. package/packages/irc-core/src/commands/oper.ts +18 -1
  101. package/packages/irc-core/src/commands/operserv.ts +346 -0
  102. package/packages/irc-core/src/commands/pre-away.ts +3 -1
  103. package/packages/irc-core/src/commands/privmsg.ts +42 -0
  104. package/packages/irc-core/src/commands/read-marker.ts +8 -8
  105. package/packages/irc-core/src/commands/registration.ts +78 -12
  106. package/packages/irc-core/src/commands/sasl.ts +18 -49
  107. package/packages/irc-core/src/commands/service-aliases.ts +52 -0
  108. package/packages/irc-core/src/commands/tagmsg.ts +41 -6
  109. package/packages/irc-core/src/commands/topic.ts +60 -10
  110. package/packages/irc-core/src/config.ts +36 -5
  111. package/packages/irc-core/src/effects.ts +56 -1
  112. package/packages/irc-core/src/ports.ts +1653 -84
  113. package/packages/irc-core/src/protocol/numerics.ts +8 -0
  114. package/packages/irc-core/src/state/channel.ts +38 -1
  115. package/packages/irc-core/src/state/connection.ts +25 -1
  116. package/packages/irc-core/src/types.ts +48 -12
  117. package/packages/irc-core/tests/commands/chanserv.test.ts +2335 -0
  118. package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
  119. package/packages/irc-core/tests/commands/hostserv.test.ts +1006 -0
  120. package/packages/irc-core/tests/commands/join.test.ts +572 -1
  121. package/packages/irc-core/tests/commands/markread.test.ts +361 -0
  122. package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
  123. package/packages/irc-core/tests/commands/mode.test.ts +381 -2
  124. package/packages/irc-core/tests/commands/nickserv.test.ts +990 -0
  125. package/packages/irc-core/tests/commands/oper.test.ts +13 -0
  126. package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
  127. package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
  128. package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
  129. package/packages/irc-core/tests/commands/registration.test.ts +1015 -20
  130. package/packages/irc-core/tests/commands/sasl.test.ts +229 -12
  131. package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
  132. package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
  133. package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
  134. package/packages/irc-core/tests/commands/topic.test.ts +94 -2
  135. package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
  136. package/packages/irc-core/tests/config.test.ts +49 -5
  137. package/packages/irc-core/tests/effects.test.ts +19 -0
  138. package/packages/irc-core/tests/message-store.test.ts +63 -0
  139. package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
  140. package/packages/irc-core/tests/services-store.test.ts +1289 -0
  141. package/packages/irc-core/tests/state/channel.test.ts +3 -0
  142. package/packages/irc-server/package.json +1 -1
  143. package/packages/irc-server/src/actor.ts +121 -16
  144. package/packages/irc-server/src/dispatch.ts +94 -7
  145. package/packages/irc-server/src/routing.ts +19 -0
  146. package/packages/irc-server/tests/actor.test.ts +988 -15
  147. package/packages/irc-server/tests/dispatch.test.ts +270 -2
  148. package/packages/irc-server/tests/routing.test.ts +6 -0
  149. package/packages/irc-test-support/package.json +1 -1
  150. package/packages/irc-test-support/src/in-memory-harness.ts +37 -8
  151. package/packages/irc-test-support/src/index.ts +1 -0
  152. package/packages/irc-test-support/src/scenarios.ts +21 -6
  153. package/packages/irc-test-support/tests/in-memory-harness.test.ts +51 -0
  154. package/packages/irc-test-support/vitest.config.ts +6 -1
  155. package/tools/ci-hardening/package.json +1 -1
  156. package/tools/load-test/package.json +1 -1
  157. package/tools/load-test/src/client.ts +13 -13
  158. package/tools/load-test/tests/client.test.ts +258 -2
  159. package/tools/load-test/tests/config.test.ts +39 -0
  160. package/tools/load-test/tests/harness.test.ts +21 -0
  161. package/tools/load-test/tests/metrics.test.ts +7 -0
  162. package/tools/tcp-ws-forwarder/package.json +1 -1
  163. package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
  164. package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
  165. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +53 -2
  166. package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
  167. package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
  168. package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Shared account-login helpers used by every account-setting entry point.
3
+ *
4
+ * PLAN §6.5 (services ↔ daemon hooks): a successful SASL PLAIN/EXTERNAL
5
+ * login, a PASS-based account login at registration, and (later) a
6
+ * NickServ `IDENTIFY` all reduce to the same thing — "this connection is
7
+ * now account X". {@link applyAccountSuccess} is that shared reduction:
8
+ * it stamps the account, sets the read-only `+r` user mode, restores
9
+ * per-account state (read markers, persisted away, queued memos), and
10
+ * emits the `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS` numerics plus the
11
+ * `account-notify` broadcast. Keeping the chain in one place guarantees
12
+ * the entry points never diverge.
13
+ *
14
+ * {@link passBasedAccountAuth} is the legacy IRC convention of carrying
15
+ * NickServ credentials in `PASS <nick>:<password>` at registration; it
16
+ * resolves at registration completion via the injected
17
+ * {@link AccountStore} and, on success, delegates to
18
+ * {@link applyAccountSuccess}.
19
+ */
20
+
21
+ import { Effect } from '../effects.js';
22
+ import type { Effect as EffectType, RawLine } from '../effects.js';
23
+ import { Numerics } from '../protocol/numerics.js';
24
+ import { hostmaskOf } from '../state/connection.js';
25
+ import type { ConnectionState } from '../state/connection.js';
26
+ import type { Ctx } from '../types.js';
27
+ import { emitAccountNotify } from './account-notify.js';
28
+ import { applyAssignedVhost } from './hostserv.js';
29
+ import { deliverUnreadMemos } from './memoserv.js';
30
+ import { nowAwayLine, replayPersistedAway } from './pre-away.js';
31
+ import { seedReadMarkers } from './read-marker.js';
32
+
33
+ /**
34
+ * Applies the account-login success effects shared by SASL, PASS-auth, and
35
+ * NickServ IDENTIFY.
36
+ *
37
+ * Mutates `state`: sets `state.account`, stamps read-only user mode `r`
38
+ * (`userModes.registered = true`), seeds `lastReadMarkers` from the bound
39
+ * services store, replays the persisted away reason, re-applies any assigned
40
+ * HostServ vhost (so a reconnecting user does not have to re-run
41
+ * `HostServ ON`), and returns the effects the caller must emit:
42
+ * - `Send` carrying `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS`;
43
+ * - an optional `306 RPL_NOWAWAY` when an away reason was replayed;
44
+ * - any queued MemoServ `NOTICE`s delivered from the services store;
45
+ * - an optional `CHGHOST` fanout when a vhost was applied post-JOIN (empty
46
+ * at SASL / PASS-auth time — no channels joined yet);
47
+ * - the `account-notify` `ACCOUNT` broadcasts to cap-enabled peers in
48
+ * shared channels (empty at identify time — no channels joined yet).
49
+ *
50
+ * Does NOT clear SASL scratch state; the SASL reducer owns that.
51
+ */
52
+ export function applyAccountSuccess(
53
+ state: ConnectionState,
54
+ ctx: Ctx,
55
+ account: string,
56
+ ): EffectType[] {
57
+ state.account = account;
58
+ // A successful account login is equivalent to NickServ IDENTIFY: stamp
59
+ // read-only user mode `r` so `221`/WHOIS reflect the identified state.
60
+ state.userModes.registered = true;
61
+ // HostServ vhost: re-apply any assigned vhost so a reconnecting user with
62
+ // a vhost does not have to re-run `HostServ ON` every session. Collected
63
+ // into a local array because the CHGHOST fanout is only non-empty when the
64
+ // connection has joined channels (never at SASL/PASS-auth time).
65
+ const vhostEffects: EffectType[] = [];
66
+ if (ctx.services !== undefined) {
67
+ // IRCv3 draft/read-marker: restore the user's per-channel last-read
68
+ // markers from the persisted store so a reconnect resumes at their last
69
+ // read position. No-op when no services store is bound.
70
+ seedReadMarkers(state, ctx.services, account);
71
+ applyAssignedVhost(state, ctx.services, account, vhostEffects);
72
+ }
73
+ // IRCv3 draft/pre-away: replay the user's persisted away reason onto the
74
+ // fresh connection and emit 306 so the user sees they are still away.
75
+ const replayedAway =
76
+ ctx.away !== undefined ? replayPersistedAway(state, ctx.away, account) : undefined;
77
+ const awayEffects: EffectType[] =
78
+ replayedAway !== undefined
79
+ ? [Effect.send(ctx.connId, [nowAwayLine(state, ctx.serverName)])]
80
+ : [];
81
+ // MemoServ: deliver queued unread memos recorded while the account was
82
+ // offline. No-op when no services store is bound or there are none.
83
+ const memoEffects: EffectType[] = [];
84
+ if (ctx.services !== undefined) {
85
+ deliverUnreadMemos(state, ctx.services, account, memoEffects);
86
+ }
87
+ return [
88
+ Effect.send(ctx.connId, [loggedInLine(ctx, account), saslSuccessLine(ctx)]),
89
+ ...awayEffects,
90
+ ...memoEffects,
91
+ ...vhostEffects,
92
+ ...emitAccountNotify({ conn: state, account }),
93
+ ];
94
+ }
95
+
96
+ /**
97
+ * Parses a `PASS` payload of the form `<nick>:<password>` (the literal
98
+ * shape used by the `SASL_ACCOUNTS` env var and the seed tooling). Returns
99
+ * `null` when `attempt` is absent, has no colon, or the nick (left of the
100
+ * first colon) is empty. Only the first colon splits; the password may
101
+ * itself contain colons.
102
+ */
103
+ export function parsePassAccountAttempt(
104
+ attempt: string | undefined,
105
+ ): { nick: string; password: string } | null {
106
+ if (attempt === undefined) return null;
107
+ const colon = attempt.indexOf(':');
108
+ if (colon <= 0) return null;
109
+ return { nick: attempt.slice(0, colon), password: attempt.slice(colon + 1) };
110
+ }
111
+
112
+ /**
113
+ * Attempts PASS-based account authentication at registration completion.
114
+ *
115
+ * Thin wrapper around {@link attemptPassAccountAuth} that reads the stashed
116
+ * `state.passAttempt`. See {@link attemptPassAccountAuth} for the full
117
+ * outcome matrix.
118
+ */
119
+ export function passBasedAccountAuth(state: ConnectionState, ctx: Ctx): EffectType[] {
120
+ return attemptPassAccountAuth(state, ctx, state.passAttempt);
121
+ }
122
+
123
+ /**
124
+ * Shared verify + {@link applyAccountSuccess} pipeline for a raw
125
+ * `<nick>:<password>` attempt string.
126
+ *
127
+ * Used by both {@link passBasedAccountAuth} (at registration completion,
128
+ * reading `state.passAttempt`) and the post-registration late-PASS login
129
+ * in `passReducer` (reading the `PASS` param directly), so the two entry
130
+ * points share one verify+success path and never diverge.
131
+ *
132
+ * Returns the account-login success effects (`900`/`903`/`+r`/read-marker
133
+ * seeding/away replay/memo delivery/`account-notify`), or `[]` for every
134
+ * other outcome:
135
+ * - already identified (`state.account` set, e.g. via SASL) → no-op;
136
+ * - no `<nick>:<password>` form (bare value or undefined) → no-op;
137
+ * - payload nick does not match `state.nick` (when set) → no-op;
138
+ * - no {@link AccountStore} bound → no-op;
139
+ * - verify failure (unknown nick or wrong password) → no-op.
140
+ *
141
+ * A failed verify is followed by a verify against a fixed dummy entry so
142
+ * the failure path performs the same AccountStore work whether the payload
143
+ * nick was unknown or the password was wrong — the observable outcome
144
+ * (no numerics, no disconnect, no state change) is identical for both
145
+ * cases, giving no information to a remote attacker.
146
+ */
147
+ export function attemptPassAccountAuth(
148
+ state: ConnectionState,
149
+ ctx: Ctx,
150
+ attempt: string | undefined,
151
+ ): EffectType[] {
152
+ if (state.account !== undefined) return [];
153
+ const parsed = parsePassAccountAttempt(attempt);
154
+ if (parsed === null) return [];
155
+ if (state.nick !== undefined && state.nick !== parsed.nick) return [];
156
+ const store = ctx.accounts;
157
+ if (store === undefined) return [];
158
+ const result = store.verify('PLAIN', {
159
+ kind: 'PLAIN',
160
+ username: parsed.nick,
161
+ password: parsed.password,
162
+ });
163
+ if (result.ok) {
164
+ return applyAccountSuccess(state, ctx, result.account);
165
+ }
166
+ // Equalise timing: perform a verify against a fixed dummy entry so a
167
+ // failed PASS-auth always costs one extra AccountStore call regardless
168
+ // of whether the payload nick was unknown or the password was wrong.
169
+ store.verify('PLAIN', {
170
+ kind: 'PLAIN',
171
+ username: PASS_AUTH_DUMMY_NICK,
172
+ password: PASS_AUTH_DUMMY_PASSWORD,
173
+ });
174
+ return [];
175
+ }
176
+
177
+ /** Fixed dummy credential used to equalise failed-PASS-auth timing. */
178
+ const PASS_AUTH_DUMMY_NICK = '\u0000pass-auth-timing-dummy';
179
+ const PASS_AUTH_DUMMY_PASSWORD = '\u0000pass-auth-timing-dummy';
180
+
181
+ /** Builds the `900 RPL_LOGGEDIN` line for an account login. */
182
+ function loggedInLine(ctx: Ctx, account: string): RawLine {
183
+ const nick = ctx.connection.nick ?? '*';
184
+ const hostmask = hostmaskOf(ctx.connection) ?? nick;
185
+ return {
186
+ text: `:${ctx.serverName} ${numeric(Numerics.RPL_LOGGEDIN)} ${nick} ${hostmask} ${account} :You are now logged in as ${account}`,
187
+ };
188
+ }
189
+
190
+ /** Builds the `903 RPL_SASLSUCCESS` line for an account login. */
191
+ function saslSuccessLine(ctx: Ctx): RawLine {
192
+ return {
193
+ text: `:${ctx.serverName} ${numeric(Numerics.RPL_SASLSUCCESS)} ${ctx.connection.nick ?? '*'} :SASL authentication successful`,
194
+ };
195
+ }
196
+
197
+ /** Zero-pads a numeric code to its three-digit wire form. */
198
+ function numeric(n: number): string {
199
+ return n.toString().padStart(3, '0');
200
+ }