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
@@ -202,13 +202,26 @@ export const tagmsgChannelReducer: Reducer<ChannelState> = (state, msg, ctx) =>
202
202
  // this channel. Persist the marker (advances the connection's in-memory
203
203
  // marker too); a valueless tag carries no msgid and is dropped here. The
204
204
  // TAGMSG still fans out below so the user's other connections learn it.
205
- if (isReadMarkerTagmsg(msg.tags)) {
205
+ const isReadMarker = isReadMarkerTagmsg(msg.tags);
206
+ if (isReadMarker) {
206
207
  const msgid = readMarkerMsgid(msg.tags);
207
208
  if (msgid !== undefined) {
208
209
  persistReadMarker(ctx, state.nameLower, msgid);
209
210
  }
210
211
  }
211
212
 
213
+ // IRCv3 draft/read-marker spec compliance: a read marker is private to
214
+ // the originator's account and MUST NOT be disclosed to other users. The
215
+ // mark TAGMSG fans out ONLY to the originator's own account (account-
216
+ // scoped broadcast); dispatch restricts the recipient set to same-account
217
+ // channel members. An unidentified originator has no account to share the
218
+ // mark with, so the mark stays session-local — no fanout at all (the
219
+ // originator's own client still observes its mark via echo-message below).
220
+ // Every other TAGMSG (typing, generic) keeps the channel-wide fanout.
221
+ const originAccount = ctx.connection.account;
222
+ const suppressFanout = isReadMarker && originAccount === undefined;
223
+ const accountFilter = isReadMarker ? originAccount : undefined;
224
+
212
225
  // Broadcast the tag-bearing line to message-tags-enabled members only.
213
226
  // When the TAGMSG carries `+draft/typing` or `+draft/read-marker`, the
214
227
  // audience widens to also include members that announced the matching
@@ -217,11 +230,33 @@ export const tagmsgChannelReducer: Reducer<ChannelState> = (state, msg, ctx) =>
217
230
  // `caps` so dispatch applies OR semantics per recipient.
218
231
  const tagPrefix = serializeTags(msg.tags);
219
232
  const line: RawLine = { text: `${tagPrefix}:${hostmask} TAGMSG ${state.name}` };
220
- const widenCaps = widenAudienceCaps(msg.tags);
221
- if (widenCaps !== undefined) {
222
- effects.push(Effect.broadcast(state.name, [line], ctx.connId, undefined, undefined, widenCaps));
223
- } else {
224
- effects.push(Effect.broadcast(state.name, [line], ctx.connId, MESSAGE_TAGS_CAP));
233
+ if (!suppressFanout) {
234
+ const widenCaps = widenAudienceCaps(msg.tags);
235
+ if (widenCaps !== undefined) {
236
+ effects.push(
237
+ Effect.broadcast(
238
+ state.name,
239
+ [line],
240
+ ctx.connId,
241
+ undefined,
242
+ undefined,
243
+ widenCaps,
244
+ accountFilter,
245
+ ),
246
+ );
247
+ } else {
248
+ effects.push(
249
+ Effect.broadcast(
250
+ state.name,
251
+ [line],
252
+ ctx.connId,
253
+ MESSAGE_TAGS_CAP,
254
+ undefined,
255
+ undefined,
256
+ accountFilter,
257
+ ),
258
+ );
259
+ }
225
260
  }
226
261
 
227
262
  // IRCv3 echo-message: echo the same line back to the sender, after
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { Effect } from '../effects.js';
22
22
  import type { Effect as EffectType, RawLine } from '../effects.js';
23
+ import type { PersistedTopic } from '../ports.js';
23
24
  import { Numerics } from '../protocol/numerics.js';
24
25
  import type { ChannelState, ChannelTopic } from '../state/channel.js';
25
26
  import { hostmaskOf } from '../state/connection.js';
@@ -121,6 +122,10 @@ export const topicReducer: Reducer<ChannelState> = (state, msg, ctx) => {
121
122
  // path that persists the change, so subsequent reads by other
122
123
  // connections observe the cleared topic.
123
124
  effects.push(Effect.applyChannelDelta(state.name, { topic: null }));
125
+ // ChanServ KEEPTOPIC: clear the persisted snapshot when the channel has
126
+ // KEEPTOPIC enabled, so a subsequent empty-recreate does not restore a
127
+ // stale topic.
128
+ persistKeepTopic(ctx, state.nameLower, undefined);
124
129
  } else {
125
130
  const topic: ChannelTopic = {
126
131
  text: truncated,
@@ -130,6 +135,9 @@ export const topicReducer: Reducer<ChannelState> = (state, msg, ctx) => {
130
135
  state.topic = topic;
131
136
  // Mirror the topic set to the channel authority (see comment above).
132
137
  effects.push(Effect.applyChannelDelta(state.name, { topic }));
138
+ // ChanServ KEEPTOPIC: persist the snapshot when the channel has
139
+ // KEEPTOPIC enabled, so a subsequent empty-recreate restores it.
140
+ persistKeepTopic(ctx, state.nameLower, topic);
133
141
  }
134
142
 
135
143
  const hostmask = callerHostmask(ctx.connection);
@@ -167,3 +175,32 @@ function readTopic(
167
175
  );
168
176
  return { state, effects };
169
177
  }
178
+
179
+ // ============================================================================
180
+ // ChanServ KEEPTOPIC persistence
181
+ // ============================================================================
182
+
183
+ /**
184
+ * Persists the topic snapshot for a ChanServ-registered channel with
185
+ * KEEPTOPIC enabled. Called by the topic reducer on every topic change
186
+ * (set or clear) so a subsequent empty-recreate can restore the snapshot.
187
+ *
188
+ * No-op when:
189
+ * - no services store is bound,
190
+ * - the channel is not registered,
191
+ * - KEEPTOPIC is off.
192
+ *
193
+ * `topic === undefined` clears the persisted snapshot; a {@link ChannelTopic}
194
+ * replaces it.
195
+ */
196
+ function persistKeepTopic(ctx: Ctx, nameLower: string, topic: ChannelTopic | undefined): void {
197
+ const services = ctx.services;
198
+ if (services === undefined) return;
199
+ const rec = services.getChannel(nameLower);
200
+ if (rec === undefined || !rec.keepTopic) return;
201
+ const persisted: PersistedTopic | undefined =
202
+ topic === undefined
203
+ ? undefined
204
+ : { text: topic.text, setter: topic.setter, setAt: topic.setAt };
205
+ services.setChannelTopic(nameLower, persisted);
206
+ }
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { z } from 'zod';
20
- import { BUILD_DATE } from './build-info.js';
20
+ import { BUILD_DATE, PACKAGE_VERSION } from './build-info.js';
21
21
  import { DEFAULT_MULTILINE_MAX_BYTES } from './caps/capabilities.js';
22
22
 
23
23
  /**
@@ -144,11 +144,14 @@ export const DEFAULT_FLOOD_CONTROL_CONFIG: {
144
144
 
145
145
  /**
146
146
  * Default server version surfaced in `002`/`004`/`351`/`371` when a deployment
147
- * does not supply one. Kept in sync with `irc-core/package.json` `version`;
148
- * adapters inject the deploy-time version via the `serverVersion` config
149
- * knob (see the per-adapter config loaders).
147
+ * does not supply one. Sourced from `irc-core/package.json` at build time via
148
+ * `scripts/generate-build-info.mjs`, so a deployment that omits the
149
+ * `serverVersion` knob advertises the version of the package it was shipped
150
+ * with — no manual sync required. Adapters may still inject a deploy-time
151
+ * version via the `serverVersion` config knob (see the per-adapter config
152
+ * loaders).
150
153
  */
151
- export const DEFAULT_SERVER_VERSION = '0.3.0';
154
+ export const DEFAULT_SERVER_VERSION: string = PACKAGE_VERSION;
152
155
 
153
156
  /**
154
157
  * Default "created" text for `003 RPL_CREATED` when a deployment does not
@@ -162,6 +165,14 @@ export const DEFAULT_SERVER_VERSION = '0.3.0';
162
165
  */
163
166
  export const DEFAULT_CREATED_TEXT: string = BUILD_DATE;
164
167
 
168
+ /**
169
+ * Default grace period (ms) NickServ applies before force-disconnecting a
170
+ * client using a `ghost`-enforced registered nick without identifying.
171
+ * Matches the Atheme/Anope convention (~30s); overridable per deployment
172
+ * via `ServerConfig.nickEnforceGraceMs`.
173
+ */
174
+ export const DEFAULT_NICK_ENFORCE_GRACE_MS = 30_000;
175
+
165
176
  /**
166
177
  * The full server-config schema. Optional fields default to deployment-
167
178
  * sensible values so a minimal `{ serverName, networkName }` config is
@@ -337,6 +348,26 @@ export const ServerConfigSchema = z.object({
337
348
  * nicks make them meaningless.
338
349
  */
339
350
  servicesEnabled: z.boolean().optional(),
351
+
352
+ /**
353
+ * Grace period (ms) NickServ applies before force-disconnecting a client
354
+ * using a `ghost`-enforced registered nick without identifying. See
355
+ * `ServerConfig.nickEnforceGraceMs`. `0` disables the grace (ghost
356
+ * behaves like kill). Omit for the 30s default.
357
+ */
358
+ nickEnforceGraceMs: z.number().int().nonnegative().optional(),
359
+
360
+ /**
361
+ * HostServ approval mode for `REQUEST <vhost>`. `true` (the default)
362
+ * preserves the legacy auto-approve behaviour — the requested vhost is
363
+ * recorded against the account immediately (no oper review). `false`
364
+ * switches HostServ into oper-queue mode: `REQUEST` records a pending
365
+ * request that an oper must `APPROVE` (or `REJECT`) via HostServ. The
366
+ * queue + the `APPROVE` / `REJECT` / `LIST` (oper-only) commands are
367
+ * available in either mode, but only `false` routes `REQUEST` through
368
+ * them.
369
+ */
370
+ hostservAutoApproveVhosts: z.boolean().default(false),
340
371
  });
341
372
 
342
373
  /** Input type: what adapters supply (fields with defaults may be omitted). */
@@ -55,6 +55,17 @@ export interface BroadcastEffect {
55
55
  * path.
56
56
  */
57
57
  caps?: readonly string[];
58
+ /**
59
+ * Account-scoped recipient filter (IRCv3 `draft/read-marker` privacy).
60
+ * When set, dispatch restricts the recipient set to channel members
61
+ * authenticated as this account — a read marker is private to the
62
+ * originator's own account and MUST NOT be disclosed to other users.
63
+ * Comparison is case-insensitive (RFC 1459 fold). Recipients still must
64
+ * satisfy the {@link cap} / {@link caps} filter; the {@link except}
65
+ * originator skip still applies. Used only by the read-marker mark
66
+ * TAGMSG path; every other broadcast leaves this unset (channel-wide).
67
+ */
68
+ account?: string;
58
69
  }
59
70
 
60
71
  export interface DisconnectEffect {
@@ -121,6 +132,39 @@ export interface BroadcastWallopsEffect {
121
132
  except?: ConnId;
122
133
  }
123
134
 
135
+ /**
136
+ * NickServ nick-enforcement directive.
137
+ *
138
+ * Emitted by the NICK path when a client takes a registered nick they are
139
+ * not identified as, and the registered account's
140
+ * {@link NickEnforcePolicy} is `ghost` or `kill`. The actor layer (via
141
+ * `dispatch`) interprets the policy:
142
+ *
143
+ * - `none` — never emitted (the reducer sends the warning NOTICE only).
144
+ * - `kill` — `disconnect("Nick enforced by NickServ")` immediately.
145
+ * - `ghost` — schedule the same disconnect after {@link EnforceNickEffect.graceMs};
146
+ * when `graceMs === 0` the disconnect is immediate (matches `kill`).
147
+ *
148
+ * The grace duration is stamped by the reducer from the deployment's
149
+ * `nickEnforceGraceMs` config so `dispatch` stays free of clock access and
150
+ * the in-memory tests stay deterministic (the scheduler is injectable via
151
+ * {@link DispatchOptions.scheduler}).
152
+ */
153
+ export interface EnforceNickEffect {
154
+ tag: 'EnforceNick';
155
+ /** Connection using the enforced nick. */
156
+ conn: ConnId;
157
+ /** Display spelling of the enforced nick. */
158
+ nick: Nick;
159
+ /** Per-account enforcement policy. */
160
+ policy: import('./ports.js').NickEnforcePolicy;
161
+ /**
162
+ * Grace period in ms before the disconnect fires (`ghost` only).
163
+ * `0` for `kill` and for `ghost` when the deployment disables the grace.
164
+ */
165
+ graceMs: number;
166
+ }
167
+
124
168
  /** Discriminated union of every effect the dispatch interpreter knows. */
125
169
  export type Effect =
126
170
  | SendEffect
@@ -131,7 +175,8 @@ export type Effect =
131
175
  | ReleaseNickEffect
132
176
  | ApplyChannelDeltaEffect
133
177
  | SendToNickEffect
134
- | BroadcastWallopsEffect;
178
+ | BroadcastWallopsEffect
179
+ | EnforceNickEffect;
135
180
 
136
181
  /** Literal string tag of every {@link Effect} variant. */
137
182
  export type EffectTag = Effect['tag'];
@@ -163,12 +208,14 @@ export const Effect = {
163
208
  cap?: string,
164
209
  capLines?: RawLine[],
165
210
  caps?: readonly string[],
211
+ account?: string,
166
212
  ): Effect {
167
213
  const e: BroadcastEffect = { tag: 'Broadcast', chan, lines };
168
214
  if (except !== undefined) e.except = except;
169
215
  if (cap !== undefined) e.cap = cap;
170
216
  if (capLines !== undefined) e.capLines = capLines;
171
217
  if (caps !== undefined) e.caps = caps;
218
+ if (account !== undefined) e.account = account;
172
219
  return e;
173
220
  },
174
221
  disconnect(conn: ConnId, reason?: string): Effect {
@@ -198,4 +245,12 @@ export const Effect = {
198
245
  if (except !== undefined) e.except = except;
199
246
  return e;
200
247
  },
248
+ enforceNick(
249
+ conn: ConnId,
250
+ nick: Nick,
251
+ policy: import('./ports.js').NickEnforcePolicy,
252
+ graceMs: number,
253
+ ): Effect {
254
+ return { tag: 'EnforceNick', conn, nick, policy, graceMs };
255
+ },
201
256
  } as const;