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,172 @@
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 { deliverUnreadMemos } from './memoserv.js';
29
+ import { nowAwayLine, replayPersistedAway } from './pre-away.js';
30
+ import { seedReadMarkers } from './read-marker.js';
31
+
32
+ /**
33
+ * Applies the account-login success effects shared by SASL, PASS-auth, and
34
+ * NickServ IDENTIFY.
35
+ *
36
+ * Mutates `state`: sets `state.account`, stamps read-only user mode `r`
37
+ * (`userModes.registered = true`), seeds `lastReadMarkers` from the bound
38
+ * services store, replays the persisted away reason, and returns the
39
+ * effects the caller must emit:
40
+ * - `Send` carrying `900 RPL_LOGGEDIN` + `903 RPL_SASLSUCCESS`;
41
+ * - an optional `306 RPL_NOWAWAY` when an away reason was replayed;
42
+ * - any queued MemoServ `NOTICE`s delivered from the services store;
43
+ * - the `account-notify` `ACCOUNT` broadcasts to cap-enabled peers in
44
+ * shared channels (empty at identify time — no channels joined yet).
45
+ *
46
+ * Does NOT clear SASL scratch state; the SASL reducer owns that.
47
+ */
48
+ export function applyAccountSuccess(
49
+ state: ConnectionState,
50
+ ctx: Ctx,
51
+ account: string,
52
+ ): EffectType[] {
53
+ state.account = account;
54
+ // A successful account login is equivalent to NickServ IDENTIFY: stamp
55
+ // read-only user mode `r` so `221`/WHOIS reflect the identified state.
56
+ state.userModes.registered = true;
57
+ // IRCv3 draft/read-marker: restore the user's per-channel last-read
58
+ // markers from the persisted store so a reconnect resumes at their last
59
+ // read position. No-op when no services store is bound.
60
+ if (ctx.services !== undefined) {
61
+ seedReadMarkers(state, ctx.services, account);
62
+ }
63
+ // IRCv3 draft/pre-away: replay the user's persisted away reason onto the
64
+ // fresh connection and emit 306 so the user sees they are still away.
65
+ const replayedAway =
66
+ ctx.away !== undefined ? replayPersistedAway(state, ctx.away, account) : undefined;
67
+ const awayEffects: EffectType[] =
68
+ replayedAway !== undefined
69
+ ? [Effect.send(ctx.connId, [nowAwayLine(state, ctx.serverName)])]
70
+ : [];
71
+ // MemoServ: deliver queued unread memos recorded while the account was
72
+ // offline. No-op when no services store is bound or there are none.
73
+ const memoEffects: EffectType[] = [];
74
+ if (ctx.services !== undefined) {
75
+ deliverUnreadMemos(state, ctx.services, account, memoEffects);
76
+ }
77
+ return [
78
+ Effect.send(ctx.connId, [loggedInLine(ctx, account), saslSuccessLine(ctx)]),
79
+ ...awayEffects,
80
+ ...memoEffects,
81
+ ...emitAccountNotify({ conn: state, account }),
82
+ ];
83
+ }
84
+
85
+ /**
86
+ * Parses a `PASS` payload of the form `<nick>:<password>` (the literal
87
+ * shape used by the `SASL_ACCOUNTS` env var and the seed tooling). Returns
88
+ * `null` when `attempt` is absent, has no colon, or the nick (left of the
89
+ * first colon) is empty. Only the first colon splits; the password may
90
+ * itself contain colons.
91
+ */
92
+ export function parsePassAccountAttempt(
93
+ attempt: string | undefined,
94
+ ): { nick: string; password: string } | null {
95
+ if (attempt === undefined) return null;
96
+ const colon = attempt.indexOf(':');
97
+ if (colon <= 0) return null;
98
+ return { nick: attempt.slice(0, colon), password: attempt.slice(colon + 1) };
99
+ }
100
+
101
+ /**
102
+ * Attempts PASS-based account authentication at registration completion.
103
+ *
104
+ * Recognises the `<nick>:<password>` form in `state.passAttempt`, verifies
105
+ * it against the bound {@link AccountStore} (PLAIN), and on success runs
106
+ * the shared {@link applyAccountSuccess} chain. Returns the success
107
+ * effects, or `[]` for every other outcome:
108
+ * - already identified (`state.account` set, e.g. via SASL) → no-op;
109
+ * - no `<nick>:<password>` form (bare value) → left to the server-password
110
+ * gate;
111
+ * - payload nick does not match `state.nick` (when set) → not a coherent
112
+ * account attempt, treated as failed auth;
113
+ * - no {@link AccountStore} bound → the `<nick>:<password>` form is
114
+ * ignored (collapses to the server-password gate);
115
+ * - verify failure (unknown nick or wrong password) → no account effects.
116
+ *
117
+ * A failed verify is followed by a verify against a fixed dummy entry so
118
+ * the failure path performs the same AccountStore work whether the payload
119
+ * nick was unknown or the password was wrong — the observable outcome
120
+ * (no numerics, no disconnect, no state change) is identical for both
121
+ * cases, giving no information to a remote attacker.
122
+ */
123
+ export function passBasedAccountAuth(state: ConnectionState, ctx: Ctx): EffectType[] {
124
+ if (state.account !== undefined) return [];
125
+ const parsed = parsePassAccountAttempt(state.passAttempt);
126
+ if (parsed === null) return [];
127
+ if (state.nick !== undefined && state.nick !== parsed.nick) return [];
128
+ const store = ctx.accounts;
129
+ if (store === undefined) return [];
130
+ const result = store.verify('PLAIN', {
131
+ kind: 'PLAIN',
132
+ username: parsed.nick,
133
+ password: parsed.password,
134
+ });
135
+ if (result.ok) {
136
+ return applyAccountSuccess(state, ctx, result.account);
137
+ }
138
+ // Equalise timing: perform a verify against a fixed dummy entry so a
139
+ // failed PASS-auth always costs one extra AccountStore call regardless
140
+ // of whether the payload nick was unknown or the password was wrong.
141
+ store.verify('PLAIN', {
142
+ kind: 'PLAIN',
143
+ username: PASS_AUTH_DUMMY_NICK,
144
+ password: PASS_AUTH_DUMMY_PASSWORD,
145
+ });
146
+ return [];
147
+ }
148
+
149
+ /** Fixed dummy credential used to equalise failed-PASS-auth timing. */
150
+ const PASS_AUTH_DUMMY_NICK = '\u0000pass-auth-timing-dummy';
151
+ const PASS_AUTH_DUMMY_PASSWORD = '\u0000pass-auth-timing-dummy';
152
+
153
+ /** Builds the `900 RPL_LOGGEDIN` line for an account login. */
154
+ function loggedInLine(ctx: Ctx, account: string): RawLine {
155
+ const nick = ctx.connection.nick ?? '*';
156
+ const hostmask = hostmaskOf(ctx.connection) ?? nick;
157
+ return {
158
+ text: `:${ctx.serverName} ${numeric(Numerics.RPL_LOGGEDIN)} ${nick} ${hostmask} ${account} :You are now logged in as ${account}`,
159
+ };
160
+ }
161
+
162
+ /** Builds the `903 RPL_SASLSUCCESS` line for an account login. */
163
+ function saslSuccessLine(ctx: Ctx): RawLine {
164
+ return {
165
+ text: `:${ctx.serverName} ${numeric(Numerics.RPL_SASLSUCCESS)} ${ctx.connection.nick ?? '*'} :SASL authentication successful`,
166
+ };
167
+ }
168
+
169
+ /** Zero-pads a numeric code to its three-digit wire form. */
170
+ function numeric(n: number): string {
171
+ return n.toString().padStart(3, '0');
172
+ }