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
@@ -1,4 +1,5 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { certIdentityFromSubject } from '../src/certfp';
2
3
  import {
3
4
  CapturingLogger,
4
5
  type Clock,
@@ -14,6 +15,7 @@ import {
14
15
  SequentialIdFactory,
15
16
  SystemClock,
16
17
  UuidIdFactory,
18
+ constantTimeEquals,
17
19
  } from '../src/ports';
18
20
 
19
21
  describe('SystemClock', () => {
@@ -54,6 +56,11 @@ describe('FakeClock', () => {
54
56
  });
55
57
  });
56
58
 
59
+ // RFC 4122 v4 layout: 8-4-4-4-12 hex chars with the version nibble (M) fixed
60
+ // to 4 and the variant nibble (N) constrained to 8-b. Used by the shape and
61
+ // CSPRNG-sourcing tests below.
62
+ const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
63
+
57
64
  describe('UuidIdFactory', () => {
58
65
  it('implements the IdFactory port', () => {
59
66
  const f: IdFactory = new UuidIdFactory();
@@ -74,6 +81,72 @@ describe('UuidIdFactory', () => {
74
81
  const f = new UuidIdFactory();
75
82
  expect(f.sessionId()).not.toBe(f.sessionId());
76
83
  });
84
+
85
+ it('emits RFC 4122 v4-shaped strings from every method', () => {
86
+ const f = new UuidIdFactory();
87
+ expect(f.batchId()).toMatch(UUID_V4_RE);
88
+ expect(f.nonce()).toMatch(UUID_V4_RE);
89
+ expect(f.sessionId()).toMatch(UUID_V4_RE);
90
+ expect(f.traceId()).toMatch(UUID_V4_RE);
91
+ });
92
+
93
+ it('does not consult Math.random (randomness comes from the CSPRNG)', () => {
94
+ // Regression guard: if a future change reverts to Math.random, mocking it
95
+ // to a constant would make the output deterministic. A CSPRNG-backed
96
+ // factory must not touch Math.random at all.
97
+ const original = Math.random;
98
+ let calls = 0;
99
+ Math.random = () => {
100
+ calls++;
101
+ return 0.5;
102
+ };
103
+ try {
104
+ const f = new UuidIdFactory();
105
+ const a = f.nonce();
106
+ const b = f.sessionId();
107
+ expect(calls).toBe(0);
108
+ expect(a).toMatch(UUID_V4_RE);
109
+ expect(b).toMatch(UUID_V4_RE);
110
+ // With Math.random pinned, a Math.random-backed impl would emit the
111
+ // same value every call; the CSPRNG emits two distinct values.
112
+ expect(a).not.toBe(b);
113
+ } finally {
114
+ Math.random = original;
115
+ }
116
+ });
117
+
118
+ it('produces hex digits indistinguishable from uniform (chi-square, CSPRNG regression guard)', () => {
119
+ // Regression guard against a biased or constant RNG. Counts the trailing
120
+ // 12 hex chars of each id — those bytes are 100% random in a UUIDv4 (the
121
+ // version nibble at index 14 and variant nibble at index 19 are
122
+ // structural and excluded from the bucket count). Buckets the chars by
123
+ // nibble value (0-15) and chi-squares against the uniform expectation.
124
+ //
125
+ // Non-determinism guard: the chi-square statistic for genuinely uniform
126
+ // input follows chi2 with 15 degrees of freedom (mean 15, stddev ~5.5).
127
+ // The chosen threshold (70) sits ~10σ above the mean — P(chi2_15 > 70) ≈
128
+ // 2e-9, i.e. effectively zero false-positives across billions of CI
129
+ // runs. A catastrophic revert (Math.random pinned to a constant) drives
130
+ // chi into the hundreds of millions, so headroom is enormous.
131
+ const f = new UuidIdFactory();
132
+ const N = 10_000;
133
+ const buckets = new Array<number>(16).fill(0);
134
+ for (let i = 0; i < N; i++) {
135
+ // Slice(24) is the trailing 'xxxxxxxxxxxx' group of the UUID string —
136
+ // 12 hex chars, all sourced from getRandomValues.
137
+ const tail = f.nonce().slice(24);
138
+ for (const c of tail) {
139
+ const idx = Number.parseInt(c, 16);
140
+ buckets[idx] = (buckets[idx] ?? 0) + 1;
141
+ }
142
+ }
143
+ const expected = (N * 12) / 16;
144
+ let chi = 0;
145
+ for (const observed of buckets) {
146
+ chi += (observed - expected) ** 2 / expected;
147
+ }
148
+ expect(chi).toBeLessThan(70);
149
+ });
77
150
  });
78
151
 
79
152
  describe('SequentialIdFactory', () => {
@@ -365,17 +438,22 @@ const _assertLogRecord: LogRecord = {
365
438
  void _assertLogRecord;
366
439
 
367
440
  describe('InMemoryMtlsIdentityProvider', () => {
368
- it('returns the certificate subject for a known connection id', () => {
441
+ it('returns the certificate identity for a known connection id', () => {
369
442
  const provider = new InMemoryMtlsIdentityProvider([
370
- { connId: 'c1', identity: 'CN=alice' },
371
- { connId: 'c2', identity: 'CN=bob' },
443
+ { connId: 'c1', identity: certIdentityFromSubject('CN=alice') },
444
+ { connId: 'c2', identity: certIdentityFromSubject('CN=bob', 'ff11') },
372
445
  ]);
373
- expect(provider.getIdentity('c1')).toBe('CN=alice');
374
- expect(provider.getIdentity('c2')).toBe('CN=bob');
446
+ expect(provider.getIdentity('c1')).toEqual({
447
+ subject: 'CN=alice',
448
+ canonicalDn: 'cn=alice',
449
+ });
450
+ expect(provider.getIdentity('c2')?.fingerprint).toBe('ff11');
375
451
  });
376
452
 
377
453
  it('returns undefined for an unknown connection id', () => {
378
- const provider = new InMemoryMtlsIdentityProvider([{ connId: 'c1', identity: 'CN=alice' }]);
454
+ const provider = new InMemoryMtlsIdentityProvider([
455
+ { connId: 'c1', identity: certIdentityFromSubject('CN=alice') },
456
+ ]);
379
457
  expect(provider.getIdentity('nope')).toBeUndefined();
380
458
  });
381
459
 
@@ -384,3 +462,88 @@ describe('InMemoryMtlsIdentityProvider', () => {
384
462
  expect(provider.getIdentity('c1')).toBeUndefined();
385
463
  });
386
464
  });
465
+
466
+ // ============================================================================
467
+ // constantTimeEquals — constant-time string equality
468
+ // ============================================================================
469
+
470
+ describe('constantTimeEquals', () => {
471
+ it('returns true for equal strings', () => {
472
+ expect(constantTimeEquals('s3cret', 's3cret')).toBe(true);
473
+ });
474
+
475
+ it('returns true for two empty strings', () => {
476
+ expect(constantTimeEquals('', '')).toBe(true);
477
+ });
478
+
479
+ it('returns false when the first character differs', () => {
480
+ expect(constantTimeEquals('X3cret', 's3cret')).toBe(false);
481
+ });
482
+
483
+ it('returns false when the last character differs', () => {
484
+ expect(constantTimeEquals('s3creX', 's3cret')).toBe(false);
485
+ });
486
+
487
+ it('returns false when the attempt is a proper prefix of the secret', () => {
488
+ expect(constantTimeEquals('s3cr', 's3cret')).toBe(false);
489
+ });
490
+
491
+ it('returns false when the attempt is longer than the secret', () => {
492
+ expect(constantTimeEquals('s3cret-extra', 's3cret')).toBe(false);
493
+ });
494
+
495
+ it('returns false for an empty attempt against a non-empty secret', () => {
496
+ expect(constantTimeEquals('', 's3cret')).toBe(false);
497
+ });
498
+
499
+ it('returns false for a non-empty attempt against an empty secret', () => {
500
+ expect(constantTimeEquals('s3cret', '')).toBe(false);
501
+ });
502
+
503
+ it('returns true for equal multi-byte strings', () => {
504
+ expect(constantTimeEquals('pässwörd', 'pässwörd')).toBe(true);
505
+ });
506
+
507
+ it('returns false for unequal multi-byte strings', () => {
508
+ expect(constantTimeEquals('pässwörd', 'password')).toBe(false);
509
+ });
510
+ });
511
+
512
+ // Deterministic timing-bucket proof: instead of measuring wall-clock time the
513
+ // per-position read primitive (`charCodeAt`) is stubbed with a counter. A
514
+ // constant-time compare reads every position of both inputs up to the longer
515
+ // length, so the read count is identical across equal inputs, first-char
516
+ // mismatches, last-char mismatches, and length mismatches of the same
517
+ // magnitude — no distinguishable work bucket exists.
518
+ describe('constantTimeEquals — timing buckets (deterministic read counts)', () => {
519
+ function countReads(a: string, b: string): number {
520
+ const spy = vi.spyOn(String.prototype, 'charCodeAt');
521
+ try {
522
+ constantTimeEquals(a, b);
523
+ return spy.mock.calls.length;
524
+ } finally {
525
+ spy.mockRestore();
526
+ }
527
+ }
528
+
529
+ it('reads every position of both inputs for same-length secrets (2 × length)', () => {
530
+ expect(countReads('s3cret', 's3cret')).toBe(12);
531
+ expect(countReads('X3cret', 's3cret')).toBe(12);
532
+ expect(countReads('s3creX', 's3cret')).toBe(12);
533
+ expect(countReads('s3c!et', 's3cret')).toBe(12);
534
+ });
535
+
536
+ it('reads the longer length even when the attempt is a length-mismatched prefix', () => {
537
+ expect(countReads('s3cr', 's3cret')).toBe(12);
538
+ expect(countReads('', 's3cret')).toBe(12);
539
+ });
540
+
541
+ it('reads the attempt length when the attempt is longer than the secret', () => {
542
+ expect(countReads('s3cret-longer', 's3cret')).toBe(26);
543
+ expect(countReads('X3cret-longer', 's3cret')).toBe(26);
544
+ });
545
+
546
+ it('spends zero reads on two empty strings', () => {
547
+ expect(countReads('', '')).toBe(0);
548
+ });
549
+ });