serverless-ircd 0.3.0 → 0.5.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 (223) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/deploy-aws.yml +1 -3
  3. package/.github/workflows/deploy-cf-tcp.yml +87 -0
  4. package/.github/workflows/deploy-cf.yml +1 -1
  5. package/.node-version +1 -0
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +349 -18
  8. package/README.md +132 -30
  9. package/apps/aws-stack/README.md +6 -5
  10. package/apps/aws-stack/bin/aws.ts +7 -0
  11. package/apps/aws-stack/package.json +4 -4
  12. package/apps/aws-stack/src/aws-stack.ts +150 -10
  13. package/apps/aws-stack/tests/stack.test.ts +145 -4
  14. package/apps/cf-tcp-container/Dockerfile +69 -0
  15. package/apps/cf-tcp-container/package.json +34 -0
  16. package/apps/cf-tcp-container/src/config-loader.ts +145 -0
  17. package/apps/cf-tcp-container/src/container-do.ts +38 -0
  18. package/apps/cf-tcp-container/src/container-server.ts +363 -0
  19. package/apps/cf-tcp-container/src/main.ts +77 -0
  20. package/apps/cf-tcp-container/src/persistence.ts +144 -0
  21. package/apps/cf-tcp-container/src/worker.ts +41 -0
  22. package/apps/cf-tcp-container/terraform/provider.tf +24 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
  25. package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
  26. package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
  27. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
  28. package/apps/cf-tcp-container/tsconfig.build.json +17 -0
  29. package/apps/cf-tcp-container/tsconfig.test.json +15 -0
  30. package/apps/cf-tcp-container/vitest.config.ts +26 -0
  31. package/apps/cf-tcp-container/wrangler.toml +63 -0
  32. package/apps/cf-worker/package.json +1 -1
  33. package/apps/cf-worker/wrangler.test.toml +6 -0
  34. package/apps/cf-worker/wrangler.toml +28 -2
  35. package/apps/local-cli/package.json +1 -1
  36. package/apps/local-cli/src/config-loader.ts +10 -0
  37. package/apps/local-cli/src/server.ts +149 -24
  38. package/apps/local-cli/tests/e2e.test.ts +1 -1
  39. package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
  40. package/package.json +14 -10
  41. package/packages/aws-adapter/package.json +3 -3
  42. package/packages/aws-adapter/src/account-store.ts +1 -1
  43. package/packages/aws-adapter/src/admission.ts +74 -0
  44. package/packages/aws-adapter/src/aws-runtime.ts +75 -4
  45. package/packages/aws-adapter/src/config-loader.ts +32 -0
  46. package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
  47. package/packages/aws-adapter/src/handlers/connect.ts +96 -9
  48. package/packages/aws-adapter/src/handlers/default.ts +98 -7
  49. package/packages/aws-adapter/src/handlers/index.ts +109 -4
  50. package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
  51. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  52. package/packages/aws-adapter/src/index.ts +8 -0
  53. package/packages/aws-adapter/src/serialize.ts +11 -1
  54. package/packages/aws-adapter/src/stats.ts +80 -0
  55. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
  56. package/packages/aws-adapter/tests/account-store.test.ts +19 -20
  57. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  58. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  59. package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
  60. package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
  61. package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
  62. package/packages/aws-adapter/tests/connect.test.ts +174 -0
  63. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
  64. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  65. package/packages/aws-adapter/tests/handlers.test.ts +302 -53
  66. package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
  67. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  68. package/packages/aws-adapter/tests/stats.test.ts +317 -0
  69. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  70. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  71. package/packages/cf-adapter/package.json +5 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +68 -5
  73. package/packages/cf-adapter/src/channel-do.ts +2 -2
  74. package/packages/cf-adapter/src/config-loader.ts +33 -0
  75. package/packages/cf-adapter/src/connection-do.ts +278 -85
  76. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  77. package/packages/cf-adapter/src/env.ts +54 -11
  78. package/packages/cf-adapter/src/index.ts +11 -8
  79. package/packages/cf-adapter/src/registry-do.ts +22 -3
  80. package/packages/cf-adapter/src/sharding.ts +1 -2
  81. package/packages/cf-adapter/src/stats.ts +65 -0
  82. package/packages/cf-adapter/tests/cf-harness.ts +12 -2
  83. package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
  84. package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
  85. package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
  86. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  87. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  88. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  89. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  90. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
  91. package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
  92. package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
  93. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  94. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  95. package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
  96. package/packages/cf-adapter/tests/sharding.test.ts +1 -1
  97. package/packages/cf-adapter/tests/stats.test.ts +120 -0
  98. package/packages/cf-adapter/tests/worker/main.ts +15 -8
  99. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
  100. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
  101. package/packages/cf-adapter/wrangler.test.toml +15 -0
  102. package/packages/in-memory-runtime/package.json +1 -1
  103. package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
  104. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
  105. package/packages/irc-core/package.json +6 -1
  106. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  107. package/packages/irc-core/src/admission.ts +16 -15
  108. package/packages/irc-core/src/caps/capabilities.ts +24 -3
  109. package/packages/irc-core/src/cloak.ts +1 -1
  110. package/packages/irc-core/src/commands/cap.ts +8 -1
  111. package/packages/irc-core/src/commands/index.ts +17 -0
  112. package/packages/irc-core/src/commands/invite.ts +2 -4
  113. package/packages/irc-core/src/commands/ison.ts +61 -0
  114. package/packages/irc-core/src/commands/isupport.ts +6 -2
  115. package/packages/irc-core/src/commands/kick.ts +2 -4
  116. package/packages/irc-core/src/commands/kill.ts +127 -0
  117. package/packages/irc-core/src/commands/list.ts +1 -1
  118. package/packages/irc-core/src/commands/lusers.ts +204 -0
  119. package/packages/irc-core/src/commands/mode.ts +4 -8
  120. package/packages/irc-core/src/commands/names.ts +3 -5
  121. package/packages/irc-core/src/commands/part.ts +2 -4
  122. package/packages/irc-core/src/commands/quit.ts +12 -0
  123. package/packages/irc-core/src/commands/registration.ts +18 -12
  124. package/packages/irc-core/src/commands/rehash.ts +119 -0
  125. package/packages/irc-core/src/commands/sasl.ts +72 -9
  126. package/packages/irc-core/src/commands/server-info.ts +129 -0
  127. package/packages/irc-core/src/commands/setname.ts +109 -0
  128. package/packages/irc-core/src/commands/stats.ts +152 -0
  129. package/packages/irc-core/src/commands/topic.ts +2 -4
  130. package/packages/irc-core/src/commands/trace.ts +137 -0
  131. package/packages/irc-core/src/commands/userhost.ts +84 -0
  132. package/packages/irc-core/src/commands/wallops.ts +118 -0
  133. package/packages/irc-core/src/commands/whowas.ts +113 -0
  134. package/packages/irc-core/src/config.ts +65 -0
  135. package/packages/irc-core/src/credential-hashing.ts +124 -0
  136. package/packages/irc-core/src/effects.ts +33 -30
  137. package/packages/irc-core/src/index.ts +3 -0
  138. package/packages/irc-core/src/ports.ts +360 -12
  139. package/packages/irc-core/src/protocol/numerics.ts +48 -11
  140. package/packages/irc-core/src/protocol/outbound.ts +20 -3
  141. package/packages/irc-core/src/types.ts +46 -2
  142. package/packages/irc-core/src/ws-framing.ts +132 -0
  143. package/packages/irc-core/src/ws-subprotocol.ts +66 -0
  144. package/packages/irc-core/tests/account-store.test.ts +45 -2
  145. package/packages/irc-core/tests/admission.test.ts +18 -0
  146. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  147. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  148. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  149. package/packages/irc-core/tests/commands/kill.test.ts +243 -0
  150. package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
  151. package/packages/irc-core/tests/commands/mode.test.ts +57 -0
  152. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  153. package/packages/irc-core/tests/commands/registration.test.ts +151 -6
  154. package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
  155. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  156. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  157. package/packages/irc-core/tests/commands/setname.test.ts +225 -0
  158. package/packages/irc-core/tests/commands/stats.test.ts +294 -0
  159. package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
  160. package/packages/irc-core/tests/commands/trace.test.ts +282 -0
  161. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  162. package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
  163. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  164. package/packages/irc-core/tests/config.test.ts +95 -1
  165. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  166. package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
  167. package/packages/irc-core/tests/effects.test.ts +14 -27
  168. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  169. package/packages/irc-core/tests/numerics.test.ts +102 -0
  170. package/packages/irc-core/tests/outbound.test.ts +51 -0
  171. package/packages/irc-core/tests/ports.test.ts +22 -0
  172. package/packages/irc-core/tests/raw-modules.d.ts +11 -0
  173. package/packages/irc-core/tests/stats-store.test.ts +222 -0
  174. package/packages/irc-core/tests/types.test.ts +35 -1
  175. package/packages/irc-core/tests/ws-framing.test.ts +213 -0
  176. package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
  177. package/packages/irc-core/tsconfig.build.json +1 -1
  178. package/packages/irc-core/tsconfig.test.json +1 -1
  179. package/packages/irc-server/package.json +1 -1
  180. package/packages/irc-server/src/actor.ts +393 -16
  181. package/packages/irc-server/src/dispatch.ts +1 -3
  182. package/packages/irc-server/src/index.ts +10 -2
  183. package/packages/irc-server/src/routing.ts +15 -0
  184. package/packages/irc-server/src/runtime.ts +31 -0
  185. package/packages/irc-server/src/transport.ts +104 -0
  186. package/packages/irc-server/tests/actor.test.ts +1489 -4
  187. package/packages/irc-server/tests/dispatch.test.ts +37 -17
  188. package/packages/irc-server/tests/raw-modules.d.ts +11 -0
  189. package/packages/irc-server/tests/routing.test.ts +5 -0
  190. package/packages/irc-server/tests/runtime.test.ts +7 -0
  191. package/packages/irc-server/tests/transport.test.ts +230 -0
  192. package/packages/irc-test-support/package.json +1 -1
  193. package/packages/irc-test-support/src/harness.ts +44 -9
  194. package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
  195. package/packages/irc-test-support/src/index.ts +3 -0
  196. package/packages/irc-test-support/src/scenarios.ts +141 -3
  197. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  198. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  199. package/pnpm-workspace.yaml +10 -1
  200. package/tools/ci-hardening/package.json +1 -1
  201. package/tools/package.json +9 -0
  202. package/tools/seed-aws-accounts.ts +5 -6
  203. package/tools/seed-cf-accounts.ts +107 -0
  204. package/tools/tcp-ws-forwarder/package.json +1 -1
  205. package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
  206. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
  207. package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
  208. package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
  209. package/docs/ADR-002-location-of-authority.md +0 -82
  210. package/docs/ADR-003-durable-object-sharding.md +0 -93
  211. package/docs/ADR-004-dynamodb-schema.md +0 -96
  212. package/docs/ADR-005-wss-only-transport-v1.md +0 -83
  213. package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
  214. package/docs/ADR-007-deterministic-ports.md +0 -82
  215. package/docs/ADR-008-monorepo-tooling.md +0 -60
  216. package/docs/AWS-Adapter-Architecture.md +0 -496
  217. package/docs/AWS-Deployment.md +0 -1186
  218. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  219. package/docs/Home.md +0 -11
  220. package/docs/Observability.md +0 -87
  221. package/docs/PlanIRCv3Websocket.md +0 -489
  222. package/docs/PlanWebClient.md +0 -451
  223. package/docs/Release-Process.md +0 -443
@@ -35,13 +35,16 @@ import {
35
35
  type Nick,
36
36
  type RawLine,
37
37
  type RosterEntry,
38
+ type ServerConfig,
38
39
  toSnapshot,
39
40
  } from '@serverless-ircd/irc-core';
40
41
  import type { IrcRuntime } from '@serverless-ircd/irc-server';
42
+ import type { CfConfigEnv } from './config-loader.js';
43
+ import { loadServerConfigFromCfEnv } from './config-loader.js';
41
44
  import type { ChannelRegistryRpc, Env, RegistryRpc } from './env.js';
42
- import { DEFAULT_REGISTRY_SHARDS, registryKeyForNick, shardNick } from './sharding.js';
43
45
  import type { PersistedConnectionState } from './serialize.js';
44
46
  import { deserialize } from './serialize.js';
47
+ import { DEFAULT_REGISTRY_SHARDS, registryKeyForNick, shardNick } from './sharding.js';
45
48
 
46
49
  /** Per-connection transport callbacks the ConnectionDO wires up. */
47
50
  export interface CfConnectionHandlers {
@@ -144,6 +147,48 @@ export class CfRuntime implements IrcRuntime {
144
147
  }
145
148
  }
146
149
 
150
+ /**
151
+ * Global cross-connection fan-out for `WALLOPS`. Enumerates every registry
152
+ * shard to collect all live connection ids, fetches each connection's
153
+ * state to honour the `+w` recipient gate, and delivers via the
154
+ * ConnectionDO `deliver` RPC. `except` (the originator) is skipped, as is
155
+ * the bound connection's own live state shortcut.
156
+ *
157
+ * Cost: this is N shard RPCs + one `getConnState` per online connection +
158
+ * one `deliver` per `+w` recipient. At very high connection counts the
159
+ * per-WALLOPS cost grows linearly with the fleet; deployments should bound
160
+ * the oper's WALLOPS rate (an oper flooding WALLOPS is a self-DoS). The
161
+ * shard walk is parallelised via `Promise.all` so wall-clock stays near one
162
+ * round-trip for the enumeration phase.
163
+ */
164
+ async broadcastWallops(lines: RawLine[], except?: ConnId): Promise<void> {
165
+ // Phase 1 — enumerate every connId across all registry shards.
166
+ const shardEntries = await Promise.all(
167
+ Array.from({ length: this.registryShards }, (_, i) => this.registryShardRpc(i).listEntries()),
168
+ );
169
+ const seen = new Set<ConnId>();
170
+ for (const entries of shardEntries) {
171
+ for (const [, connId] of entries) {
172
+ seen.add(connId);
173
+ }
174
+ }
175
+
176
+ // Phase 2 — fetch each candidate's state, filter +w, deliver.
177
+ // Bound the per-call fan-out by skipping the excepted/bound connection
178
+ // up front (the bound connection's own +w never needs a self-RPC).
179
+ await Promise.all(
180
+ Array.from(seen, async (connId) => {
181
+ if (connId === except || connId === this.connId) return;
182
+ const stub = this.env.CONNECTION_DO.get(this.env.CONNECTION_DO.idFromString(connId));
183
+ const dto = await (stub as unknown as ConnectionRpc).getConnState();
184
+ if (dto === null) return;
185
+ const state = deserialize(dto);
186
+ if (!state.userModes.wallops) return;
187
+ await (stub as unknown as ConnectionRpc).deliver(lines);
188
+ }),
189
+ );
190
+ }
191
+
147
192
  // -------------------------------------------------------------------------
148
193
  // Nick registry
149
194
  // -------------------------------------------------------------------------
@@ -247,9 +292,7 @@ export class CfRuntime implements IrcRuntime {
247
292
  const out = new Map<ConnId, ConnectionState>();
248
293
  await Promise.all(
249
294
  memberIds.map(async (id) => {
250
- const connStub = this.env.CONNECTION_DO.get(
251
- this.env.CONNECTION_DO.idFromString(id),
252
- );
295
+ const connStub = this.env.CONNECTION_DO.get(this.env.CONNECTION_DO.idFromString(id));
253
296
  const dto = await (connStub as unknown as ConnectionRpc).getConnState();
254
297
  if (dto !== null) {
255
298
  out.set(id, deserialize(dto));
@@ -267,6 +310,16 @@ export class CfRuntime implements IrcRuntime {
267
310
  return snapshots.filter((s): s is ChanSnapshot => s !== null);
268
311
  }
269
312
 
313
+ /**
314
+ * Re-reads the bound Workers env (vars + secrets, the config source for
315
+ * Cloudflare deployments) and reparses it through the shared schema. The
316
+ * actor swaps its live config with the result; a rejection leaves the
317
+ * previous config in effect.
318
+ */
319
+ async reloadConfig(): Promise<ServerConfig> {
320
+ return loadServerConfigFromCfEnv(this.env as unknown as CfConfigEnv);
321
+ }
322
+
270
323
  // -------------------------------------------------------------------------
271
324
  // Internal
272
325
  // -------------------------------------------------------------------------
@@ -283,6 +336,16 @@ export class CfRuntime implements IrcRuntime {
283
336
  return stub as unknown as RegistryRpc;
284
337
  }
285
338
 
339
+ /**
340
+ * Returns the registry DO RPC stub for shard index `i` (`0 <= i < shards`).
341
+ * Used by global fan-out paths (WALLOPS) that must enumerate every shard
342
+ * rather than address the one owning a specific nick.
343
+ */
344
+ private registryShardRpc(i: number): RegistryRpc {
345
+ const stub = this.env.REGISTRY_DO.get(this.env.REGISTRY_DO.idFromName(`s${i}`));
346
+ return stub as unknown as RegistryRpc;
347
+ }
348
+
286
349
  /** Returns the channel DO RPC stub for `name` (keyed by lowercased name). */
287
350
  private channelRpc(name: ChanName): {
288
351
  broadcast(lines: unknown[], except?: string): Promise<void>;
@@ -321,7 +384,7 @@ export function makeCfRuntime(
321
384
  handlers: CfConnectionHandlers,
322
385
  ): IrcRuntime {
323
386
  // `_registryKey` is ignored — the production CfRuntime always shards the
324
- // registry by nick (the prior per-conn key was a 033 scaffolding
387
+ // registry by nick (the prior per-conn key was a scaffolding
325
388
  // convention that the real RegistryDO does not use).
326
389
  void _registryKey;
327
390
  return new CfRuntime(env, connId, handlers);
@@ -30,7 +30,7 @@
30
30
  * RPC return-type note: snapshot reads return the JSON-friendly
31
31
  * {@link ChannelSnapshotDto} (members/banMasks/pendingInvites as
32
32
  * arrays) so they survive the DO RPC boundary without relying on
33
- * structured-clone of `Map`/`Set`. The CfRuntime (036)
33
+ * structured-clone of `Map`/`Set`. The CfRuntime
34
34
  * rehydrates these into a full {@link ChanSnapshot} at the call site.
35
35
  */
36
36
 
@@ -89,7 +89,7 @@ export interface ConnectionDeliveryRpc {
89
89
  * JSON-friendly snapshot DTO returned by `getChannelSnapshot`. Maps and
90
90
  * Sets are flattened to arrays so the value survives the DO RPC
91
91
  * boundary in both production and the test pool. The CfRuntime
92
- * (036) rehydrates this into a {@link ChanSnapshot} at the
92
+ * rehydrates this into a {@link ChanSnapshot} at the
93
93
  * call site.
94
94
  */
95
95
  export interface ChannelSnapshotDto {
@@ -22,6 +22,17 @@ import { type ParsedServerConfig, parseServerConfig } from '@serverless-ircd/irc
22
22
  export interface CfConfigEnv {
23
23
  SERVER_NAME?: string;
24
24
  NETWORK_NAME?: string;
25
+ /**
26
+ * Server version surfaced in `002`/`004`/`351`/`371`. When unset the
27
+ * schema default ({@link DEFAULT_SERVER_VERSION}) applies.
28
+ */
29
+ SERVER_VERSION?: string;
30
+ /**
31
+ * "Created" text for `003 RPL_CREATED`. A numeric string is parsed into
32
+ * an epoch-ms number so the reducer formats it as a UTC timestamp; any
33
+ * other value is passed through verbatim.
34
+ */
35
+ CREATED_AT?: string;
25
36
  MOTD_LINES?: string;
26
37
  MAX_CLIENTS?: string;
27
38
  CHANNEL_PREFIXES?: string;
@@ -56,6 +67,12 @@ function buildConfigInput(env: CfConfigEnv): Record<string, unknown> {
56
67
  serverName: env.SERVER_NAME,
57
68
  networkName: env.NETWORK_NAME,
58
69
  };
70
+ if (env.SERVER_VERSION !== undefined) {
71
+ input.serverVersion = env.SERVER_VERSION;
72
+ }
73
+ if (env.CREATED_AT !== undefined) {
74
+ input.createdAt = parseCreatedAt(env.CREATED_AT);
75
+ }
59
76
  if (env.MOTD_LINES !== undefined && env.MOTD_LINES.length > 0) {
60
77
  input.motdLines = env.MOTD_LINES.split('\n');
61
78
  }
@@ -102,6 +119,22 @@ function buildConfigInput(env: CfConfigEnv): Record<string, unknown> {
102
119
  return input;
103
120
  }
104
121
 
122
+ /**
123
+ * Parses the `CREATED_AT` env var into the schema's `createdAt` field.
124
+ *
125
+ * A purely numeric string is treated as epoch-ms (returned as a number so
126
+ * the reducer formats it as a UTC timestamp); any other value is returned
127
+ * verbatim as a human-readable string. Empty strings fall through to the
128
+ * schema default (the caller only invokes this when the env var is set).
129
+ */
130
+ function parseCreatedAt(raw: string): string | number {
131
+ if (/^\d+$/.test(raw.trim())) {
132
+ const n = Number(raw.trim());
133
+ if (Number.isFinite(n)) return n;
134
+ }
135
+ return raw;
136
+ }
137
+
105
138
  /**
106
139
  * Parses a newline-delimited `username:password` string into the config's
107
140
  * `saslAccounts` shape. Malformed entries are skipped so a trailing