serverless-ircd 0.9.0 → 0.11.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 (254) hide show
  1. package/.github/workflows/ci.yml +28 -0
  2. package/.github/workflows/deploy-aws.yml +156 -32
  3. package/.github/workflows/deploy-cf-tcp.yml +35 -9
  4. package/.github/workflows/deploy-cf.yml +40 -14
  5. package/CHANGELOG.md +594 -0
  6. package/README.md +286 -60
  7. package/apps/aws-stack/README.md +3 -5
  8. package/apps/aws-stack/bin/aws.ts +118 -9
  9. package/apps/aws-stack/cdk.json +0 -3
  10. package/apps/aws-stack/package.json +3 -4
  11. package/apps/aws-stack/src/aws-stack.ts +398 -67
  12. package/apps/aws-stack/src/static-site.ts +323 -0
  13. package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
  14. package/apps/aws-stack/tests/stack.test.ts +714 -105
  15. package/apps/aws-stack/tests/static-site.test.ts +491 -0
  16. package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
  17. package/apps/cf-tcp-container/Dockerfile +37 -5
  18. package/apps/cf-tcp-container/package.json +7 -3
  19. package/apps/cf-tcp-container/src/config-loader.ts +113 -2
  20. package/apps/cf-tcp-container/src/container-server.ts +267 -87
  21. package/apps/cf-tcp-container/src/main.ts +22 -7
  22. package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
  25. package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
  26. package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
  27. package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
  28. package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
  29. package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
  30. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
  31. package/apps/cf-tcp-container/wrangler.toml +18 -14
  32. package/apps/cf-worker/package.json +3 -4
  33. package/apps/cf-worker/src/worker.ts +77 -5
  34. package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
  35. package/apps/cf-worker/tests/smoke.test.ts +4 -0
  36. package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
  37. package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
  38. package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
  39. package/apps/cf-worker/wrangler.test.toml +15 -1
  40. package/apps/cf-worker/wrangler.toml +95 -77
  41. package/apps/local-cli/package.json +1 -1
  42. package/apps/local-cli/src/config-loader.ts +14 -2
  43. package/apps/local-cli/src/line-scanner.ts +26 -0
  44. package/apps/local-cli/src/server.ts +44 -19
  45. package/apps/local-cli/tests/line-scanner.test.ts +64 -0
  46. package/apps/local-cli/tests/tcp.test.ts +29 -0
  47. package/apps/web/landing/favicon.ico +0 -0
  48. package/apps/web/landing/index.html +1 -0
  49. package/apps/web/package.json +2 -2
  50. package/apps/web/scripts/build.mjs +66 -4
  51. package/apps/web/src/build-env.ts +125 -4
  52. package/apps/web/src/config-schema.ts +20 -6
  53. package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
  54. package/apps/web/tests/build-env.test.ts +210 -9
  55. package/apps/web/tests/build-smoke.test.ts +2 -2
  56. package/apps/web/tests/config-schema.test.ts +149 -25
  57. package/docs/AWS-Deployment.md +793 -118
  58. package/docs/AWS-TCP-Deployment.md +57 -47
  59. package/docs/Chat-History.md +55 -0
  60. package/docs/Cloudflare-Deployment-Guide.md +95 -114
  61. package/docs/Cloudflare-TCP-Deployment.md +160 -101
  62. package/docs/Release-Process.md +27 -23
  63. package/docs/SASL-EXTERNAL.md +175 -0
  64. package/docs/Services.md +69 -22
  65. package/docs/WebClientGuide.md +35 -26
  66. package/package.json +7 -10
  67. package/packages/aws-adapter/package.json +1 -1
  68. package/packages/aws-adapter/src/admission.ts +28 -13
  69. package/packages/aws-adapter/src/aws-runtime.ts +30 -3
  70. package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
  71. package/packages/aws-adapter/src/config-loader.ts +153 -8
  72. package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
  73. package/packages/aws-adapter/src/handlers/connect.ts +73 -1
  74. package/packages/aws-adapter/src/handlers/default.ts +279 -123
  75. package/packages/aws-adapter/src/handlers/index.ts +98 -25
  76. package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
  77. package/packages/aws-adapter/src/index.ts +5 -7
  78. package/packages/aws-adapter/src/ip-admission.ts +79 -0
  79. package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
  80. package/packages/aws-adapter/src/serialize.ts +23 -0
  81. package/packages/aws-adapter/src/tables.ts +11 -12
  82. package/packages/aws-adapter/tests/admission.test.ts +60 -2
  83. package/packages/aws-adapter/tests/aws-harness.ts +23 -2
  84. package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
  85. package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
  86. package/packages/aws-adapter/tests/connect.test.ts +323 -3
  87. package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
  88. package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
  89. package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
  90. package/packages/aws-adapter/tests/handlers.test.ts +174 -12
  91. package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
  92. package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
  93. package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
  94. package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
  95. package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
  96. package/packages/aws-adapter/tests/stats.test.ts +0 -3
  97. package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
  98. package/packages/aws-adapter/tests/tables.test.ts +1 -8
  99. package/packages/aws-adapter/tests/transactions.test.ts +0 -1
  100. package/packages/cf-adapter/package.json +1 -5
  101. package/packages/cf-adapter/src/cf-runtime.ts +100 -10
  102. package/packages/cf-adapter/src/channel-do.ts +13 -3
  103. package/packages/cf-adapter/src/config-loader.ts +133 -8
  104. package/packages/cf-adapter/src/connection-do.ts +406 -116
  105. package/packages/cf-adapter/src/counter-do.ts +142 -0
  106. package/packages/cf-adapter/src/d1-services-store.ts +105 -26
  107. package/packages/cf-adapter/src/env.ts +99 -10
  108. package/packages/cf-adapter/src/index.ts +17 -7
  109. package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
  110. package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
  111. package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
  112. package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
  113. package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
  114. package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
  115. package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
  116. package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
  117. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
  118. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
  119. package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
  120. package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
  121. package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
  122. package/packages/cf-adapter/tests/serialize.test.ts +25 -0
  123. package/packages/cf-adapter/tests/worker/main.ts +4 -0
  124. package/packages/cf-adapter/wrangler.test.toml +18 -1
  125. package/packages/in-memory-runtime/package.json +1 -1
  126. package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
  127. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
  128. package/packages/irc-core/package.json +1 -1
  129. package/packages/irc-core/src/account-migration.ts +140 -0
  130. package/packages/irc-core/src/caps/capabilities.ts +20 -10
  131. package/packages/irc-core/src/certfp.ts +178 -0
  132. package/packages/irc-core/src/commands/account-auth.ts +16 -19
  133. package/packages/irc-core/src/commands/cap.ts +10 -2
  134. package/packages/irc-core/src/commands/chanserv.ts +117 -14
  135. package/packages/irc-core/src/commands/chathistory.ts +13 -5
  136. package/packages/irc-core/src/commands/hostserv.ts +84 -8
  137. package/packages/irc-core/src/commands/index.ts +2 -1
  138. package/packages/irc-core/src/commands/invite.ts +1 -7
  139. package/packages/irc-core/src/commands/join.ts +1 -16
  140. package/packages/irc-core/src/commands/kick.ts +1 -8
  141. package/packages/irc-core/src/commands/list.ts +1 -8
  142. package/packages/irc-core/src/commands/memoserv.ts +1 -1
  143. package/packages/irc-core/src/commands/mode.ts +1 -8
  144. package/packages/irc-core/src/commands/multiline.ts +4 -10
  145. package/packages/irc-core/src/commands/names.ts +53 -13
  146. package/packages/irc-core/src/commands/nickserv.ts +161 -11
  147. package/packages/irc-core/src/commands/oper.ts +361 -8
  148. package/packages/irc-core/src/commands/part.ts +4 -10
  149. package/packages/irc-core/src/commands/privmsg.ts +8 -4
  150. package/packages/irc-core/src/commands/registration.ts +148 -4
  151. package/packages/irc-core/src/commands/sasl.ts +154 -46
  152. package/packages/irc-core/src/commands/topic.ts +10 -12
  153. package/packages/irc-core/src/commands/who.ts +1 -8
  154. package/packages/irc-core/src/config.ts +424 -25
  155. package/packages/irc-core/src/credential-hashing.ts +11 -54
  156. package/packages/irc-core/src/effects.ts +24 -0
  157. package/packages/irc-core/src/flood-control.ts +10 -10
  158. package/packages/irc-core/src/frame-rate-limit.ts +82 -0
  159. package/packages/irc-core/src/index.ts +9 -0
  160. package/packages/irc-core/src/oper-hashing.ts +43 -0
  161. package/packages/irc-core/src/oper-lockout.ts +87 -0
  162. package/packages/irc-core/src/ports.ts +529 -190
  163. package/packages/irc-core/src/protocol/bytes.ts +65 -0
  164. package/packages/irc-core/src/protocol/channel-name.ts +37 -0
  165. package/packages/irc-core/src/protocol/index.ts +12 -1
  166. package/packages/irc-core/src/protocol/outbound.ts +43 -10
  167. package/packages/irc-core/src/protocol/parser.ts +79 -10
  168. package/packages/irc-core/src/state/connection.ts +13 -0
  169. package/packages/irc-core/src/types.ts +266 -23
  170. package/packages/irc-core/src/ws-framing.ts +5 -4
  171. package/packages/irc-core/tests/account-migration.test.ts +133 -0
  172. package/packages/irc-core/tests/bytes.test.ts +89 -0
  173. package/packages/irc-core/tests/certfp.test.ts +117 -0
  174. package/packages/irc-core/tests/commands/cap.test.ts +76 -2
  175. package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
  176. package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
  177. package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
  178. package/packages/irc-core/tests/commands/join.test.ts +78 -1
  179. package/packages/irc-core/tests/commands/markread.test.ts +54 -0
  180. package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
  181. package/packages/irc-core/tests/commands/names.test.ts +193 -0
  182. package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
  183. package/packages/irc-core/tests/commands/oper.test.ts +574 -1
  184. package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
  185. package/packages/irc-core/tests/commands/registration.test.ts +602 -133
  186. package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
  187. package/packages/irc-core/tests/commands/topic.test.ts +137 -2
  188. package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
  189. package/packages/irc-core/tests/config.test.ts +534 -2
  190. package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
  191. package/packages/irc-core/tests/effects.test.ts +14 -0
  192. package/packages/irc-core/tests/flood-control.test.ts +29 -1
  193. package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
  194. package/packages/irc-core/tests/message-store.test.ts +5 -0
  195. package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
  196. package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
  197. package/packages/irc-core/tests/outbound.test.ts +148 -0
  198. package/packages/irc-core/tests/parser.test.ts +287 -5
  199. package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
  200. package/packages/irc-core/tests/ports.test.ts +170 -7
  201. package/packages/irc-core/tests/services-store.test.ts +567 -1
  202. package/packages/irc-core/tests/ws-framing.test.ts +45 -0
  203. package/packages/irc-core/vitest.config.ts +6 -1
  204. package/packages/irc-server/package.json +1 -1
  205. package/packages/irc-server/src/actor.ts +123 -22
  206. package/packages/irc-server/src/dispatch.ts +1 -0
  207. package/packages/irc-server/src/index.ts +7 -0
  208. package/packages/irc-server/src/redact.ts +159 -0
  209. package/packages/irc-server/src/runtime.ts +14 -0
  210. package/packages/irc-server/src/transport.ts +28 -1
  211. package/packages/irc-server/tests/actor.test.ts +563 -54
  212. package/packages/irc-server/tests/dispatch.test.ts +31 -0
  213. package/packages/irc-server/tests/redact.test.ts +198 -0
  214. package/packages/irc-server/tests/runtime.test.ts +2 -0
  215. package/packages/irc-server/tests/transport.test.ts +66 -0
  216. package/packages/irc-test-support/package.json +1 -1
  217. package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
  218. package/pnpm-workspace.yaml +1 -0
  219. package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
  220. package/scripts/deploy-web-aws.mjs +290 -0
  221. package/scripts/package.json +23 -0
  222. package/scripts/tsconfig.test.json +12 -0
  223. package/scripts/vitest.config.ts +19 -0
  224. package/tools/ci-hardening/package.json +2 -2
  225. package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
  226. package/tools/ci-hardening/src/cf-deploy.ts +118 -0
  227. package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
  228. package/tools/ci-hardening/src/env-var-drift.ts +192 -0
  229. package/tools/ci-hardening/src/hostname-guard.ts +11 -0
  230. package/tools/ci-hardening/src/index.ts +19 -0
  231. package/tools/ci-hardening/src/validate.ts +57 -0
  232. package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
  233. package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
  234. package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
  235. package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
  236. package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
  237. package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
  238. package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
  239. package/tools/ci-hardening/tests/validate.test.ts +42 -0
  240. package/tools/ci-hardening/vitest.config.ts +5 -1
  241. package/tools/hash-oper-cred.ts +85 -0
  242. package/tools/load-test/package.json +1 -1
  243. package/tools/migrate-accounts-to-services.ts +270 -0
  244. package/tools/package.json +2 -1
  245. package/tools/seed-aws-accounts.ts +35 -10
  246. package/tools/seed-cf-accounts.ts +42 -9
  247. package/tools/tcp-ws-forwarder/package.json +1 -1
  248. package/packages/aws-adapter/src/account-store.ts +0 -121
  249. package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
  250. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
  251. package/packages/aws-adapter/tests/account-store.test.ts +0 -276
  252. package/packages/cf-adapter/src/d1-account-store.ts +0 -198
  253. package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
  254. package/packages/irc-core/tests/account-store.test.ts +0 -131
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Per-connection inbound frame-rate window for the adapter boundary.
3
+ *
4
+ * Client-side flood control (the token bucket in `flood-control.ts`)
5
+ * lives in the actor and only fires AFTER the frame has been parsed and
6
+ * dispatched — which on the serverless adapters means the frame has
7
+ * already cost a Durable Object storage write (Cloudflare) or a DynamoDB
8
+ * write (AWS). This module is the cheaper, earlier gate: a pure
9
+ * sliding-window admission decision the adapters consult BEFORE invoking
10
+ * the actor, so a hostile client spamming low-cost commands is cut off at
11
+ * the connection boundary instead of driving outsized storage cost.
12
+ *
13
+ * Semantics: a connection may deliver at most `maxFrames` frames inside
14
+ * any sliding `windowMs` window. Timestamps older than the window decay
15
+ * out lazily (they stop counting as soon as `now - t >= windowMs`), so a
16
+ * connection that pauses longer than the window starts fresh. The frame
17
+ * that would exceed the budget is rejected — the caller closes the
18
+ * connection with RFC 6455 close code 1008 (Policy Violation) after
19
+ * delivering the canonical `ERROR :Closing link (flooding)` line.
20
+ *
21
+ * Determinism invariant: the decision is a pure function of
22
+ * `(times, now, maxFrames, windowMs)`; the caller owns the timestamp
23
+ * list (in-memory on the CF Durable Object, persisted on the AWS
24
+ * Connections row) and the clock.
25
+ */
26
+
27
+ /**
28
+ * RFC 6455 close code used when the inbound frame window is exceeded
29
+ * (Policy Violation).
30
+ */
31
+ export const FRAME_FLOOD_CLOSE_CODE = 1008;
32
+
33
+ /**
34
+ * The `ERROR` line delivered to the client just before the flood close,
35
+ * matching the canonical ircd convention for connection-level flood
36
+ * kills.
37
+ */
38
+ export const FRAME_FLOOD_ERROR_LINE = 'ERROR :Closing link (flooding)';
39
+
40
+ /** Result of {@link admitFrameWindow}. */
41
+ export interface FrameWindowDecision {
42
+ /**
43
+ * `true` when the frame fits the window (the caller processes it);
44
+ * `false` when the window is full (the caller closes the connection).
45
+ */
46
+ readonly admitted: boolean;
47
+ /**
48
+ * The post-decision window state the caller should retain: the
49
+ * previous entries pruned of decayed timestamps, plus — when
50
+ * admitted — the current frame's `now` appended. Never mutated in
51
+ * place; a fresh array is returned on every call.
52
+ */
53
+ readonly times: number[];
54
+ }
55
+
56
+ /**
57
+ * Decides whether the frame arriving at `now` fits inside the
58
+ * `maxFrames`-per-`windowMs` sliding window.
59
+ *
60
+ * Entries with `t < now - windowMs` decay out first (an entry at exactly
61
+ * `now - windowMs` still counts — inclusive boundary, mirroring
62
+ * `AdmissionStats.recentAdmissions`). The decision then reduces to:
63
+ * if the pruned window already holds `maxFrames` entries, reject;
64
+ * otherwise admit and record `now`.
65
+ */
66
+ export function admitFrameWindow(
67
+ times: readonly number[],
68
+ now: number,
69
+ maxFrames: number,
70
+ windowMs: number,
71
+ ): FrameWindowDecision {
72
+ const cutoff = now - windowMs;
73
+ const kept: number[] = [];
74
+ for (const t of times) {
75
+ if (t >= cutoff) kept.push(t);
76
+ }
77
+ if (kept.length >= maxFrames) {
78
+ return { admitted: false, times: kept };
79
+ }
80
+ kept.push(now);
81
+ return { admitted: true, times: kept };
82
+ }
@@ -10,19 +10,28 @@ export * from './caps/index.js';
10
10
  export * from './commands/index.js';
11
11
  export * from './effects.js';
12
12
  export * from './ports.js';
13
+ export * from './certfp.js';
13
14
  export * from './credential-hashing.js';
15
+ export * from './account-migration.js';
16
+ export * from './oper-hashing.js';
14
17
  export * from './flood-control.js';
18
+ export * from './frame-rate-limit.js';
15
19
  export * from './cloak.js';
16
20
  export * from './case-fold.js';
17
21
  export * from './ws-subprotocol.js';
18
22
  export * from './ws-framing.js';
19
23
  export * from './admission.js';
24
+ export * from './oper-lockout.js';
20
25
  export * from './config.js';
21
26
  export type {
22
27
  BuildCtxOptions,
28
+ ChathistoryConfig,
23
29
  CloakingConfig,
24
30
  Ctx,
25
31
  OperCred,
32
+ OperCredHashed,
33
+ OperCredPlaintext,
34
+ OperThrottleConfig,
26
35
  Reducer,
27
36
  ReducerResult,
28
37
  ServerConfig,
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pure helper for `tools/hash-oper-cred.ts`.
3
+ *
4
+ * Wraps {@link hashAccountCredential}'s scrypt derivation in the
5
+ * `{user, salt, hash}` shape the oper config consumes (see
6
+ * {@link OperCredSchema}). The CLI entry point (arg parsing, console
7
+ * output) lives in the tools script; this helper is side-effect-free
8
+ * and fully unit-testable, mirroring the
9
+ * `account-migration.ts` / `tools/migrate-accounts-to-services.ts`
10
+ * split.
11
+ */
12
+
13
+ import { hashAccountCredential } from './credential-hashing.js';
14
+ import type { OperCredHashed } from './types.js';
15
+
16
+ export type { OperCredHashed };
17
+
18
+ /**
19
+ * Hashes a plaintext oper `{user, password}` pair into the at-rest
20
+ * `{user, salt, hash}` shape operators paste into secrets.
21
+ *
22
+ * The `salt` is cryptographically random by default; pass `{ salt }`
23
+ * for deterministic test scenarios. The `hash` is scrypt-derived via
24
+ * {@link hashAccountCredential} (memory-hard, GPU-resistant) and both
25
+ * `salt` and `hash` are base64 strings safe to embed in JSON / env
26
+ * vars / wrangler secrets.
27
+ *
28
+ * Round-trips via {@link matchOperCred} (the OPER reducer's verify
29
+ * path) — the resulting {@link OperCredHashed} is directly assignable
30
+ * to the {@link OperCredSchema} array on `ServerConfig`.
31
+ */
32
+ export function hashOperCredential(
33
+ user: string,
34
+ password: string,
35
+ opts?: { salt?: Uint8Array },
36
+ ): OperCredHashed {
37
+ const entry = hashAccountCredential(user, password, opts);
38
+ return {
39
+ user: entry.account,
40
+ salt: entry.salt,
41
+ hash: entry.hash,
42
+ };
43
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Per-IP failed-`OPER` counter — the failed-authentication counterpart of
3
+ * {@link AdmissionStats}.
4
+ *
5
+ * Online brute force against `OPER` is bounded by this sliding-window
6
+ * counter: once a single source host accumulates `oper.maxFailures`
7
+ * credential failures within `oper.failureWindowSeconds`, every further
8
+ * `OPER` from that host is rejected with `491 ERR_NOOPERHOST` before the
9
+ * credential store is consulted (see `commands/oper.ts`).
10
+ *
11
+ * Ownership + injection follows the {@link AdmissionStats} precedent: the
12
+ * runtime (in-memory reference runtime, local CLI) constructs exactly one
13
+ * instance per process and shares it across every connection via the
14
+ * `ctx.operFailures` port, so the counter is per-IP across the whole
15
+ * deployment rather than per-connection. The instance MUST be constructed
16
+ * with the same {@link Clock} the actors use so window arithmetic is
17
+ * coherent.
18
+ *
19
+ * Determinism: same Clock + same call stream → same answers. There are no
20
+ * ambient inputs (never `Date.now`).
21
+ */
22
+
23
+ import type { Clock } from './ports.js';
24
+
25
+ /** Per-IP failed-`OPER` counter owned by the runtime (sliding window). */
26
+ export class OperFailureStats {
27
+ private readonly clock: Clock;
28
+ /**
29
+ * Per-IP timestamps of recent OPER credential failures, used by the
30
+ * lockout gate. Stored ascending; entries older than the queried window
31
+ * are pruned lazily (identical bookkeeping to `AdmissionStats`'
32
+ * `perIpAdmitTimes`).
33
+ */
34
+ private readonly failureTimes = new Map<string, number[]>();
35
+
36
+ constructor(clock: Clock) {
37
+ this.clock = clock;
38
+ }
39
+
40
+ /**
41
+ * Returns the number of `OPER` credential failures recorded for `ip`
42
+ * within the last `windowMs` milliseconds (inclusive of the moment
43
+ * returned by `clock.now()`). Stale entries are pruned as a side effect,
44
+ * so an IP whose every failure has expired drops out of the map
45
+ * entirely.
46
+ */
47
+ recentFailures(ip: string, windowMs: number): number {
48
+ const times = this.failureTimes.get(ip);
49
+ if (times === undefined) return 0;
50
+ const cutoff = this.clock.now() - windowMs;
51
+ let writeIdx = 0;
52
+ // `times` is dense (only ever appended to via push), so iterating
53
+ // values via for-of avoids the noUncheckedIndexedAccess trap while
54
+ // still allowing in-place compaction of stale entries.
55
+ for (const t of times) {
56
+ if (t >= cutoff) {
57
+ times[writeIdx++] = t;
58
+ }
59
+ }
60
+ if (writeIdx === 0) {
61
+ this.failureTimes.delete(ip);
62
+ return 0;
63
+ }
64
+ times.length = writeIdx;
65
+ return writeIdx;
66
+ }
67
+
68
+ /**
69
+ * Records one `OPER` credential failure for `ip`, stamped with the
70
+ * injected clock's current time. Called by the OPER reducer on the
71
+ * `464 ERR_PASSWDMISMATCH` path.
72
+ */
73
+ recordFailure(ip: string): void {
74
+ const times = this.failureTimes.get(ip) ?? [];
75
+ times.push(this.clock.now());
76
+ this.failureTimes.set(ip, times);
77
+ }
78
+
79
+ /**
80
+ * Clears every recorded failure for `ip`. Called by the OPER reducer on
81
+ * successful authentication so a legitimate oper's success never
82
+ * contributes to a later lockout.
83
+ */
84
+ reset(ip: string): void {
85
+ this.failureTimes.delete(ip);
86
+ }
87
+ }