@svrnsec/pulse 0.6.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 (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +883 -622
  3. package/SECURITY.md +86 -86
  4. package/bin/svrnsec-pulse.js +7 -7
  5. package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
  6. package/dist/pulse.cjs.map +1 -0
  7. package/dist/pulse.esm.js +6380 -6421
  8. package/dist/pulse.esm.js.map +1 -1
  9. package/index.d.ts +895 -846
  10. package/package.json +185 -165
  11. package/pkg/pulse_core.js +174 -173
  12. package/src/analysis/audio.js +213 -213
  13. package/src/analysis/authenticityAudit.js +408 -390
  14. package/src/analysis/coherence.js +502 -502
  15. package/src/analysis/coordinatedBehavior.js +825 -0
  16. package/src/analysis/heuristic.js +428 -428
  17. package/src/analysis/jitter.js +446 -446
  18. package/src/analysis/llm.js +473 -472
  19. package/src/analysis/populationEntropy.js +404 -403
  20. package/src/analysis/provider.js +248 -248
  21. package/src/analysis/refraction.js +392 -0
  22. package/src/analysis/trustScore.js +356 -356
  23. package/src/cli/args.js +36 -36
  24. package/src/cli/commands/scan.js +192 -192
  25. package/src/cli/runner.js +157 -157
  26. package/src/collector/adaptive.js +200 -200
  27. package/src/collector/bio.js +297 -287
  28. package/src/collector/canvas.js +247 -239
  29. package/src/collector/dram.js +203 -203
  30. package/src/collector/enf.js +311 -311
  31. package/src/collector/entropy.js +195 -195
  32. package/src/collector/gpu.js +248 -245
  33. package/src/collector/idleAttestation.js +480 -480
  34. package/src/collector/sabTimer.js +189 -191
  35. package/src/fingerprint.js +475 -475
  36. package/src/index.js +342 -342
  37. package/src/integrations/react-native.js +462 -459
  38. package/src/integrations/react.js +184 -185
  39. package/src/middleware/express.js +155 -155
  40. package/src/middleware/next.js +174 -175
  41. package/src/proof/challenge.js +249 -249
  42. package/src/proof/engagementToken.js +426 -394
  43. package/src/proof/fingerprint.js +268 -268
  44. package/src/proof/validator.js +83 -143
  45. package/src/registry/serializer.js +349 -349
  46. package/src/terminal.js +263 -263
  47. package/src/update-notifier.js +259 -264
  48. package/dist/pulse.cjs.js.map +0 -1
@@ -1,80 +1,75 @@
1
1
  /**
2
- * @sovereign/pulse — Server-Side Validator
2
+ * @svrnsec/pulse — Server-Side Validator
3
3
  *
4
4
  * Verifies a ProofPayload + BLAKE3 commitment received from the client.
5
- * This module is for NODE.JS / SERVER use only. It should NOT be bundled
6
- * into the browser build (see package.json "exports" field).
7
- *
8
- * Trust model:
9
- * • The server issues a challenge `nonce` before the client runs pulse().
10
- * • The client returns { payload, hash }.
11
- * • The server calls validateProof(payload, hash, options) to:
12
- * 1. Verify hash integrity (no tampering).
13
- * 2. Verify nonce freshness (no replay).
14
- * 3. Verify timestamp recency.
15
- * 4. Check jitter score against thresholds.
16
- * 5. Check canvas fingerprint against software-renderer blocklist.
17
- * 6. Cross-validate signal consistency.
18
- *
19
- * NOTE: The server NEVER sees raw timing arrays or mouse coordinates.
20
- * Only statistical summaries are transmitted. This is the ZK property.
5
+ * This module is for NODE.JS / SERVER use only.
21
6
  */
22
7
 
23
8
  import { blake3 } from '@noble/hashes/blake3';
24
9
  import { bytesToHex } from '@noble/hashes/utils';
25
- import { randomFillSync } from 'node:crypto';
10
+ import { randomFillSync, timingSafeEqual } from 'node:crypto';
26
11
  import { canonicalJson } from './fingerprint.js';
27
12
  import { computeServerDynamicThreshold } from '../analysis/coherence.js';
28
13
 
29
14
  // ---------------------------------------------------------------------------
30
- // Known software / virtual renderer substring patterns (lowercase)
15
+ // Known software / virtual renderer patterns
31
16
  // ---------------------------------------------------------------------------
32
17
  const VM_RENDERER_BLOCKLIST = [
33
- // Software / virtual renderers
34
18
  'llvmpipe', 'swiftshader', 'softpipe', 'mesa offscreen',
35
19
  'microsoft basic render', 'vmware svga', 'vmware', 'virtualbox',
36
20
  'parallels', 'chromium swiftshader', 'google swiftshader',
37
- 'angle (', 'cirrussm', 'qxl', 'virtio', 'bochs',
38
- // NVIDIA datacenter / inference no consumer unit has these
39
- 'nvidia t4', // AWS/GCP inference VM
40
- 'nvidia a10g', // AWS g5 inference
41
- 'nvidia a100', // Datacenter A100
42
- 'nvidia h100', // Hopper — datacenter only
43
- 'nvidia h200', // Hopper successor — datacenter only
44
- 'nvidia b100', // Blackwell — datacenter only
45
- 'nvidia b200', // Blackwell Ultra — datacenter only
46
- 'nvidia gh200', // Grace-Hopper superchip
47
- // AMD datacenter / HPC — no consumer has these
48
- 'amd instinct', // covers mi100, mi200, mi250, mi300 family
49
- 'amd mi300',
50
- 'amd mi250',
51
- 'amd mi200',
52
- // Cloud-specific AI accelerators
53
- 'aws inferentia',
54
- 'aws trainium',
55
- 'google tpu',
21
+ 'cirrussm', 'qxl', 'virtio', 'bochs',
22
+ 'nvidia t4', 'nvidia a10g', 'nvidia a100', 'nvidia h100',
23
+ 'nvidia h200', 'nvidia b100', 'nvidia b200', 'nvidia gh200',
24
+ 'amd instinct', 'amd mi300', 'amd mi250', 'amd mi200',
25
+ 'aws inferentia', 'aws trainium', 'google tpu',
56
26
  ];
57
27
 
28
+ // ANGLE with software backend — match only software variants, not real hardware through ANGLE
29
+ const VM_RENDERER_REGEX = [
30
+ /angle\s*\(.*software/i,
31
+ ];
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Recursive prototype pollution guard
35
+ // ---------------------------------------------------------------------------
36
+ function _checkProtoPollution(obj, depth = 0) {
37
+ if (depth > 10 || obj === null || typeof obj !== 'object') return false;
38
+ if (Array.isArray(obj)) {
39
+ for (const item of obj) {
40
+ if (_checkProtoPollution(item, depth + 1)) return true;
41
+ }
42
+ return false;
43
+ }
44
+ if (
45
+ Object.prototype.hasOwnProperty.call(obj, '__proto__') ||
46
+ Object.prototype.hasOwnProperty.call(obj, 'constructor') ||
47
+ Object.prototype.hasOwnProperty.call(obj, 'prototype')
48
+ ) {
49
+ return true;
50
+ }
51
+ for (const key of Object.keys(obj)) {
52
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
53
+ if (_checkProtoPollution(obj[key], depth + 1)) return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
58
59
  // ---------------------------------------------------------------------------
59
60
  // validateProof
60
61
  // ---------------------------------------------------------------------------
61
62
 
62
63
  /**
63
- * Validates a client-submitted proof.
64
- *
65
64
  * @param {import('./fingerprint.js').ProofPayload} payload
66
- * @param {string} receivedHash - hex BLAKE3 from the client
65
+ * @param {string} receivedHash
67
66
  * @param {object} [opts]
68
- * @param {number} [opts.minJitterScore=0.55] - minimum acceptable jitter score
69
- * @param {number} [opts.maxAgeMs=300_000] - max payload age (5 min)
70
- * @param {number} [opts.clockSkewMs=30_000] - tolerated future timestamp drift
71
- * @param {boolean} [opts.requireBio=false] - reject if no bio activity
72
- * @param {boolean} [opts.blockSoftwareRenderer=true] - reject software WebGL
73
- * @param {Function} [opts.checkNonce] - async fn(nonce) → boolean
74
- * Called to verify the nonce was issued by this server and not yet consumed.
75
- * Should mark the nonce as consumed atomically (e.g. Redis SET NX with TTL).
76
- * If omitted, nonce freshness is NOT checked (not recommended for production).
77
- *
67
+ * @param {number} [opts.minJitterScore=0.55]
68
+ * @param {number} [opts.maxAgeMs=300_000]
69
+ * @param {number} [opts.clockSkewMs=30_000]
70
+ * @param {boolean} [opts.requireBio=false]
71
+ * @param {boolean} [opts.blockSoftwareRenderer=true]
72
+ * @param {Function} [opts.checkNonce]
78
73
  * @returns {Promise<ValidationResult>}
79
74
  */
80
75
  export async function validateProof(payload, receivedHash, opts = {}) {
@@ -96,12 +91,8 @@ export async function validateProof(payload, receivedHash, opts = {}) {
96
91
  return _reject(['INVALID_PAYLOAD_STRUCTURE']);
97
92
  }
98
93
 
99
- // Prototype pollution guard — reject any payload with __proto__ / constructor tricks
100
- if (
101
- Object.prototype.hasOwnProperty.call(payload, '__proto__') ||
102
- Object.prototype.hasOwnProperty.call(payload, 'constructor') ||
103
- Object.prototype.hasOwnProperty.call(payload, 'prototype')
104
- ) {
94
+ // Recursive prototype pollution guard — checks all nested objects
95
+ if (_checkProtoPollution(payload)) {
105
96
  return _reject(['PROTOTYPE_POLLUTION_ATTEMPT']);
106
97
  }
107
98
 
@@ -113,7 +104,7 @@ export async function validateProof(payload, receivedHash, opts = {}) {
113
104
  }
114
105
  }
115
106
 
116
- // Type assertions on top-level scalars
107
+ // Type assertions
117
108
  if (typeof payload.version !== 'number') return _reject(['INVALID_TYPE:version']);
118
109
  if (typeof payload.timestamp !== 'number') return _reject(['INVALID_TYPE:timestamp']);
119
110
  if (typeof payload.nonce !== 'string') return _reject(['INVALID_TYPE:nonce']);
@@ -124,14 +115,8 @@ export async function validateProof(payload, receivedHash, opts = {}) {
124
115
  return _reject(['INVALID_TYPE:classification']);
125
116
  }
126
117
 
127
- // Note: we deliberately do not enforce a strict nonce format here so that
128
- // test fixtures can provide short placeholder nonces. The `checkNonce`
129
- // function (if supplied) should perform any format validation it requires
130
- // and return false for invalid or replayed nonces.
131
-
132
- // Timestamp must be a plausible Unix ms value (> year 2020, < year 2100)
133
- const TS_MIN = 1_577_836_800_000; // 2020-01-01
134
- const TS_MAX = 4_102_444_800_000; // 2100-01-01
118
+ const TS_MIN = 1_577_836_800_000;
119
+ const TS_MAX = 4_102_444_800_000;
135
120
  if (payload.timestamp < TS_MIN || payload.timestamp > TS_MAX) {
136
121
  return _reject(['TIMESTAMP_OUT_OF_RANGE']);
137
122
  }
@@ -140,8 +125,7 @@ export async function validateProof(payload, receivedHash, opts = {}) {
140
125
  return _reject(['UNSUPPORTED_PROOF_VERSION']);
141
126
  }
142
127
 
143
- // ── 1. Hash integrity ─────────────────────────────────────────────────────
144
- // receivedHash must be exactly 64 lowercase hex characters
128
+ // ── 1. Hash integrity (timing-safe comparison) ────────────────────────────
145
129
  if (typeof receivedHash !== 'string' || !/^[0-9a-f]{64}$/.test(receivedHash)) {
146
130
  return _reject(['INVALID_HASH_FORMAT']);
147
131
  }
@@ -149,7 +133,13 @@ export async function validateProof(payload, receivedHash, opts = {}) {
149
133
  const enc = new TextEncoder().encode(canonical);
150
134
  const computed = bytesToHex(blake3(enc));
151
135
 
152
- if (computed !== receivedHash) {
136
+ try {
137
+ const computedBuf = Buffer.from(computed, 'hex');
138
+ const receivedBuf = Buffer.from(receivedHash, 'hex');
139
+ if (computedBuf.length !== receivedBuf.length || !timingSafeEqual(computedBuf, receivedBuf)) {
140
+ return _reject(['HASH_MISMATCH_PAYLOAD_TAMPERED']);
141
+ }
142
+ } catch {
153
143
  return _reject(['HASH_MISMATCH_PAYLOAD_TAMPERED']);
154
144
  }
155
145
 
@@ -166,7 +156,9 @@ export async function validateProof(payload, receivedHash, opts = {}) {
166
156
  }
167
157
 
168
158
  // ── 3. Nonce freshness ────────────────────────────────────────────────────
159
+ let nonceChecked = false;
169
160
  if (checkNonce) {
161
+ nonceChecked = true;
170
162
  const nonceOk = await checkNonce(payload.nonce);
171
163
  if (!nonceOk) {
172
164
  valid = false;
@@ -183,23 +175,12 @@ export async function validateProof(payload, receivedHash, opts = {}) {
183
175
  reasons.push(`JITTER_SCORE_TOO_LOW: ${jitterScore} < ${minJitterScore}`);
184
176
  }
185
177
 
186
- // ── 4b. Dynamic threshold (evidence-proportional gate) ──────────────────
187
- // The server independently computes the minimum passing score based on how
188
- // much evidence the proof contains. The client's dynamicThreshold field is
189
- // NEVER trusted — it is only used for logging/auditing.
190
- //
191
- // Logic: a proof with only 50 iterations and no bio/audio faces a higher bar
192
- // (0.62) than a full 200-iteration proof with phased data (0.50).
193
- // This makes replay attacks with minimal proofs automatically fail the gate.
178
+ // ── 4b. Dynamic threshold server recomputes from raw signals, not client finalScore
194
179
  const serverDynamicMin = computeServerDynamicThreshold(payload);
195
-
196
- // We check the FINAL client score (which includes stage-3 coherence adjustment)
197
- // if it was included, otherwise fall back to the base jitterScore.
198
- const finalClientScore = payload.classification?.finalScore ?? jitterScore;
199
- if (finalClientScore < serverDynamicMin) {
180
+ if (jitterScore < serverDynamicMin) {
200
181
  valid = false;
201
182
  reasons.push(
202
- `DYNAMIC_THRESHOLD_NOT_MET: score=${finalClientScore} < ` +
183
+ `DYNAMIC_THRESHOLD_NOT_MET: jitterScore=${jitterScore} < ` +
203
184
  `serverMin=${serverDynamicMin} (evidenceWeight=${
204
185
  _computeEvidenceWeight(payload).toFixed(3)
205
186
  })`
@@ -213,10 +194,7 @@ export async function validateProof(payload, receivedHash, opts = {}) {
213
194
  }
214
195
  }
215
196
 
216
- // Hard override from the client heuristic engine (stage 2).
217
- // EJR_PHASE_HARD_KILL fires when the stored entropyJitterRatio is mathematically
218
- // inconsistent with the stored cold_QE / hot_QE values — proof of tampering.
219
- // A legitimate SDK running on real hardware never triggers this.
197
+ // Hard override from heuristic engine (stage 2)
220
198
  if (payload.heuristic?.hardOverride === 'vm') {
221
199
  valid = false;
222
200
  reasons.push(
@@ -225,9 +203,7 @@ export async function validateProof(payload, receivedHash, opts = {}) {
225
203
  );
226
204
  }
227
205
 
228
- // Hard override from the client coherence stage (stage 3).
229
- // Second line of defence — catches the same contradiction via a different
230
- // code path and also catches the phase-trajectory forgery variant.
206
+ // Hard override from coherence stage (stage 3)
231
207
  if (payload.coherence?.hardOverride === 'vm') {
232
208
  valid = false;
233
209
  reasons.push(
@@ -236,7 +212,7 @@ export async function validateProof(payload, receivedHash, opts = {}) {
236
212
  );
237
213
  }
238
214
 
239
- // Surface all coherence flags for risk tracking / audit logs
215
+ // Surface all coherence flags for risk tracking
240
216
  for (const flag of (payload.heuristic?.coherenceFlags ?? [])) {
241
217
  riskFlags.push(`HEURISTIC:${flag}`);
242
218
  }
@@ -252,6 +228,7 @@ export async function validateProof(payload, receivedHash, opts = {}) {
252
228
  reasons.push(`SOFTWARE_RENDERER_DETECTED: ${canvas.webglRenderer}`);
253
229
  }
254
230
  const rendererLc = (canvas.webglRenderer ?? '').toLowerCase();
231
+ // Check substring patterns
255
232
  for (const pattern of VM_RENDERER_BLOCKLIST) {
256
233
  if (rendererLc.includes(pattern)) {
257
234
  valid = false;
@@ -260,6 +237,15 @@ export async function validateProof(payload, receivedHash, opts = {}) {
260
237
  break;
261
238
  }
262
239
  }
240
+ // Check regex patterns (e.g. ANGLE software)
241
+ for (const re of VM_RENDERER_REGEX) {
242
+ if (re.test(rendererLc)) {
243
+ valid = false;
244
+ reasons.push(`BLOCKLISTED_RENDERER: ${canvas.webglRenderer}`);
245
+ riskFlags.push('RENDERER_MATCH:angle_software');
246
+ break;
247
+ }
248
+ }
263
249
  if (!canvas.available) {
264
250
  riskFlags.push('CANVAS_UNAVAILABLE');
265
251
  }
@@ -275,7 +261,6 @@ export async function validateProof(payload, receivedHash, opts = {}) {
275
261
  if (bio.mouseSampleCount === 0 && bio.keyboardSampleCount === 0) {
276
262
  riskFlags.push('ZERO_BIO_SAMPLES');
277
263
  }
278
- // Interference coefficient check: real human+hardware shows measurable correlation
279
264
  if (bio.interferenceCoefficient < -0.3) {
280
265
  riskFlags.push('NEGATIVE_INTERFERENCE_COEFFICIENT');
281
266
  }
@@ -284,19 +269,15 @@ export async function validateProof(payload, receivedHash, opts = {}) {
284
269
  // ── 7. Internal consistency checks ────────────────────────────────────────
285
270
  const entropy = payload.signals?.entropy;
286
271
  if (entropy) {
287
- // CV and jitter score should be directionally consistent
288
272
  if (entropy.timingsCV < 0.01 && jitterScore > 0.7) {
289
273
  riskFlags.push('INCONSISTENCY:LOW_CV_BUT_HIGH_SCORE');
290
274
  }
291
- // Timer granularity should not be exactly 0 (no real device has infinite resolution)
292
275
  if (entropy.timerGranularityMs === 0) {
293
276
  riskFlags.push('SUSPICIOUS_ZERO_TIMER_GRANULARITY');
294
277
  }
295
- // Extreme thermal patterns inconsistent with score
296
278
  if (entropy.thermalPattern === 'flat' && jitterScore > 0.8) {
297
279
  riskFlags.push('INCONSISTENCY:FLAT_THERMAL_BUT_HIGH_SCORE');
298
280
  }
299
- // Hurst exponent way out of range
300
281
  if (entropy.hurstExponent != null) {
301
282
  if (entropy.hurstExponent < 0.2 || entropy.hurstExponent > 0.85) {
302
283
  riskFlags.push(`EXTREME_HURST:${entropy.hurstExponent}`);
@@ -305,29 +286,11 @@ export async function validateProof(payload, receivedHash, opts = {}) {
305
286
  }
306
287
 
307
288
  // ── 7b. Cross-signal physics forgery detection ────────────────────────────
308
- // BLAKE3 prevents tampering with a payload that was legitimately generated by
309
- // the SDK. However, a determined attacker can:
310
- // 1. Obtain a valid server nonce
311
- // 2. Craft a fake payload with forged statistics
312
- // 3. Compute BLAKE3(forgedPayload) themselves (BLAKE3 is public)
313
- // 4. Submit { payload: forgedPayload, hash: selfComputedHash }
314
- //
315
- // These checks detect statistically impossible metric combinations that no
316
- // real device would ever produce, catching crafted payloads even though the
317
- // hash integrity check passes.
318
- //
319
- // All three thresholds are set conservatively: they only fire when the
320
- // combination is physically IMPOSSIBLE, not just unlikely, to avoid false
321
- // positives on unusual-but-legitimate hardware.
322
289
  if (entropy) {
323
290
  const cv = entropy.timingsCV ?? null;
324
291
  const qe = entropy.quantizationEntropy ?? null;
325
292
  const lag1 = entropy.autocorr_lag1 ?? null;
326
293
 
327
- // Impossibly flat CV + high physical score
328
- // Real explanation: CV < 0.015 means timing jitter < 1.5% — hypervisor-flat.
329
- // No real-silicon CPU running a WASM matrix multiply achieves this.
330
- // A high jitterScore (> 0.65) is physically incompatible with CV < 0.015.
331
294
  if (cv !== null && cv < 0.015 && jitterScore > 0.65) {
332
295
  valid = false;
333
296
  reasons.push(
@@ -336,10 +299,6 @@ export async function validateProof(payload, receivedHash, opts = {}) {
336
299
  );
337
300
  }
338
301
 
339
- // VM-grade autocorrelation + high physical score
340
- // lag1 > 0.70 is a hypervisor scheduler rhythm — unambiguous VM signature.
341
- // A device with that level of autocorrelation cannot score > 0.70 on the
342
- // physical scale; the jitter classifier would have penalised it heavily.
343
302
  if (lag1 !== null && lag1 > 0.70 && jitterScore > 0.70) {
344
303
  valid = false;
345
304
  reasons.push(
@@ -348,10 +307,6 @@ export async function validateProof(payload, receivedHash, opts = {}) {
348
307
  );
349
308
  }
350
309
 
351
- // VM-grade quantization entropy + high physical score
352
- // QE < 2.0 means timings cluster on a small number of distinct values —
353
- // the classic integer-millisecond quantisation of an emulated/virtual timer.
354
- // A device producing QE < 2.0 cannot legitimately score > 0.65 as physical.
355
310
  if (qe !== null && qe < 2.0 && jitterScore > 0.65) {
356
311
  valid = false;
357
312
  reasons.push(
@@ -364,7 +319,6 @@ export async function validateProof(payload, receivedHash, opts = {}) {
364
319
  // ── 8. Audio signal check ─────────────────────────────────────────────────
365
320
  const audio = payload.signals?.audio;
366
321
  if (audio?.available) {
367
- // Impossibly low jitter CV may indicate a synthetic audio driver
368
322
  if (audio.callbackJitterCV < 0.001) {
369
323
  riskFlags.push('AUDIO_JITTER_TOO_FLAT');
370
324
  }
@@ -374,6 +328,9 @@ export async function validateProof(payload, receivedHash, opts = {}) {
374
328
  let confidence;
375
329
  if (!valid) {
376
330
  confidence = 'rejected';
331
+ } else if (!nonceChecked) {
332
+ // Without nonce verification, confidence cannot be higher than 'low'
333
+ confidence = 'low';
377
334
  } else if (riskFlags.length === 0 && jitterScore >= 0.75) {
378
335
  confidence = 'high';
379
336
  } else if (riskFlags.length <= 2 && jitterScore >= 0.60) {
@@ -398,30 +355,13 @@ export async function validateProof(payload, receivedHash, opts = {}) {
398
355
  };
399
356
  }
400
357
 
401
- /**
402
- * @typedef {object} ValidationResult
403
- * @property {boolean} valid
404
- * @property {number} score
405
- * @property {'high'|'medium'|'low'|'rejected'} confidence
406
- * @property {string[]} reasons - human-readable rejection reasons
407
- * @property {string[]} riskFlags - non-blocking risk indicators
408
- * @property {object} meta
409
- */
358
+ /** @typedef {object} ValidationResult */
410
359
 
411
360
  // ---------------------------------------------------------------------------
412
- // generateNonce (convenience helper for the server challenge flow)
361
+ // generateNonce
413
362
  // ---------------------------------------------------------------------------
414
363
 
415
- /**
416
- * Generate a cryptographically random 32-byte nonce for the server challenge.
417
- * The server should store this nonce with a TTL before issuing it to the client.
418
- *
419
- * @returns {string} hex nonce
420
- */
421
364
  export function generateNonce() {
422
- // Synchronous nonce generator for server-side use and tests.
423
- // Prefer global crypto.getRandomValues when available; otherwise use
424
- // Node's `randomFillSync` which is synchronous and available in Node.
425
365
  let buf;
426
366
  if (typeof globalThis.crypto?.getRandomValues === 'function') {
427
367
  buf = new Uint8Array(32);