ribaunt 0.2.2 → 0.2.4

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 (60) hide show
  1. package/README.md +113 -10
  2. package/demo/lib/ribaunt-solver.wasm +0 -0
  3. package/demo/lib/solver-worker.js +174 -0
  4. package/demo/lib/solver-worker.js.map +1 -0
  5. package/demo/lib/solver.js +135 -0
  6. package/demo/lib/solver.js.map +1 -0
  7. package/demo/lib/wasm-solver.js +296 -0
  8. package/demo/lib/wasm-solver.js.map +1 -0
  9. package/demo/lib/widget-browser.js +9 -0
  10. package/demo/lib/widget-browser.js.map +1 -0
  11. package/demo/lib/widget.js +942 -0
  12. package/demo/lib/widget.js.map +1 -0
  13. package/demo/lib/worker-client.js +128 -0
  14. package/demo/lib/worker-client.js.map +1 -0
  15. package/dist/cjs/index.d.ts +48 -3
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +196 -17
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/risk.d.ts +124 -0
  20. package/dist/cjs/risk.d.ts.map +1 -0
  21. package/dist/cjs/risk.js +213 -0
  22. package/dist/cjs/risk.js.map +1 -0
  23. package/dist/index.d.ts +48 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +193 -16
  26. package/dist/index.js.map +1 -1
  27. package/dist/ribaunt-solver.wasm +0 -0
  28. package/dist/risk.d.ts +124 -0
  29. package/dist/risk.d.ts.map +1 -0
  30. package/dist/risk.js +202 -0
  31. package/dist/risk.js.map +1 -0
  32. package/dist/solver-worker.d.ts.map +1 -1
  33. package/dist/solver-worker.js +166 -7
  34. package/dist/solver-worker.js.map +1 -1
  35. package/dist/solver.d.ts +1 -0
  36. package/dist/solver.d.ts.map +1 -1
  37. package/dist/solver.js +38 -3
  38. package/dist/solver.js.map +1 -1
  39. package/dist/wasm-solver.d.ts +29 -0
  40. package/dist/wasm-solver.d.ts.map +1 -0
  41. package/dist/wasm-solver.js +296 -0
  42. package/dist/wasm-solver.js.map +1 -0
  43. package/dist/widget-browser.d.ts +3 -2
  44. package/dist/widget-browser.d.ts.map +1 -1
  45. package/dist/widget-browser.js +1 -1
  46. package/dist/widget-browser.js.map +1 -1
  47. package/dist/widget-react.d.ts +4 -1
  48. package/dist/widget-react.d.ts.map +1 -1
  49. package/dist/widget-react.js +102 -14
  50. package/dist/widget-react.js.map +1 -1
  51. package/dist/widget.d.ts +42 -14
  52. package/dist/widget.d.ts.map +1 -1
  53. package/dist/widget.js +254 -12
  54. package/dist/widget.js.map +1 -1
  55. package/dist/worker-client.d.ts +7 -1
  56. package/dist/worker-client.d.ts.map +1 -1
  57. package/dist/worker-client.js +69 -19
  58. package/dist/worker-client.js.map +1 -1
  59. package/package.json +22 -9
  60. package/context7.json +0 -4
package/README.md CHANGED
@@ -16,10 +16,19 @@ Ribaunt is a stateless proof-of-work CAPTCHA library for Node.js and modern brow
16
16
  - Browser widget for plain HTML apps
17
17
  - React and Next.js-friendly wrapper via `ribaunt/widget-react`
18
18
  - Server helpers for creating, solving, and verifying PoW challenges
19
+ - Programmable risk engine (`assess()`) with caller-supplied signals, pluggable scorers, and `allow`/`challenge`/`block` policy
19
20
  - Default process-local replay protection with support for remote replay stores
20
21
  - CSS custom properties for theming
21
22
  - TypeScript types included
22
23
 
24
+ ## What Ribaunt does not do
25
+
26
+ Read this before relying on Ribaunt alone.
27
+
28
+ - **Ribaunt does not rate-limit your endpoints.** Nothing stops an attacker from hammering your challenge or verify endpoints themselves, no solving required. Put a rate limiter (per-IP, per-account, or WAF-level) in front of both. Ribaunt exposes optional rate-limit and telemetry hooks so you can wire your own limiter and metrics in at the point challenges are issued and verified (see `rateLimiter` and `onEvent` below).
29
+ - **Ribaunt does not prove a human is present.** It raises the cost of automated abuse. A sufficiently resourced attacker can solve any proof-of-work challenge.
30
+ - **Ribaunt is one layer, not the whole stack.** Use it behind rate limiting and risk signals, and never as the sole gate for sensitive actions such as password reset, account recovery, or payments.
31
+
23
32
  ## Install
24
33
 
25
34
  ```bash
@@ -56,8 +65,8 @@ const app = express();
56
65
 
57
66
  app.use(express.json());
58
67
 
59
- app.get('/api/captcha/challenge', (_req, res) => {
60
- const challenges = createChallenge(5, 4, 120);
68
+ app.get('/api/captcha/challenge', async (_req, res) => {
69
+ const challenges = await createChallenge(5, 4, 120);
61
70
  res.json({ challenges });
62
71
  });
63
72
 
@@ -129,10 +138,10 @@ For Next.js App Router, keep the widget in a client component. Do not expose `RI
129
138
 
130
139
  ### `createChallenge(difficulty, amount, ttlSeconds)`
131
140
 
132
- Creates signed challenge tokens.
141
+ Creates signed challenge tokens. Since the optional `rateLimiter` hook can be async, `createChallenge()` returns a `Promise`:
133
142
 
134
143
  ```ts
135
- const challenges = createChallenge(5, 4, 120);
144
+ const challenges = await createChallenge(5, 4, 120);
136
145
  ```
137
146
 
138
147
  | Parameter | Default | Description |
@@ -143,12 +152,14 @@ const challenges = createChallenge(5, 4, 120);
143
152
 
144
153
  Validate user- or config-controlled values before passing them to `createChallenge()`. Invalid values throw.
145
154
 
155
+ > **API note:** the options object form defaults to a single challenge (`amount: 1`) unless you pass `amount`; only the positional form above defaults to `4`.
156
+
146
157
  ### `createChallenge({ difficulty: "auto", ... })`
147
158
 
148
159
  Creates adaptive challenge tokens from a server-bounded calibration hint. Calibration is untrusted and raise-only: it can increase work for fast clients, but it never lowers the server baseline.
149
160
 
150
161
  ```ts
151
- const challenges = createChallenge({
162
+ const challenges = await createChallenge({
152
163
  difficulty: 'auto',
153
164
  calibration: requestBody.calibration,
154
165
  targetDurationMs: 750,
@@ -196,9 +207,41 @@ const workload = selectWorkload({
196
207
  minDifficulty: 3,
197
208
  maxDifficulty: 6,
198
209
  });
199
- // { difficulty: 5, amount: 4, estimatedAttempts: 1256 }
210
+ // { difficulty: 3, amount: 5, estimatedAttempts: 20480 }
211
+ ```
212
+
213
+ ### `assess(options)` — Risk Engine
214
+
215
+ Stateless, caller-driven risk assessment. All signals are caller-supplied (untrusted). Returns a bounded heuristic risk score (0–100, not a probability) and a policy action.
216
+
217
+ ```ts
218
+ import { assess } from 'ribaunt';
219
+
220
+ const assessment = await assess({
221
+ signals: {
222
+ accountAgeSeconds: account.ageSeconds,
223
+ requestVelocity: requestsPerMinute,
224
+ userAgent: req.headers['user-agent'],
225
+ ip: req.ip, // accepted for custom scorers; default scorer ignores it
226
+ },
227
+ // thresholds: { challenge: 40, block: 80 }, // defaults shown
228
+ // scorer: myScorer, // optional pluggable RiskScorer
229
+ // workload: { minDifficulty: 3, maxDifficulty: 6, targetDurationMs: 750, calibration },
230
+ });
231
+
232
+ if (assessment.action === 'allow') { /* continue */ }
233
+ else if (assessment.action === 'challenge') {
234
+ const workload = assessment.workload!; // { difficulty, amount, estimatedAttempts } from selectWorkload()
235
+ }
236
+ else { /* block — application decides how to reject */ }
200
237
  ```
201
238
 
239
+ - `risk < challenge` → `allow` (no workload)
240
+ - `challenge ≤ risk < block` → `challenge` (with workload via existing `selectWorkload()`)
241
+ - `risk ≥ block` → `block` (no workload)
242
+
243
+ Defaults are `DEFAULT_RISK_THRESHOLDS = { challenge: 40, block: 80 }` — tune to your app; they are policy defaults, not fraud probabilities. Custom scorers (`RiskScorer { score(signals): Promise<number> }`) are `async` so you can call a remote model; invalid outputs (`NaN`/`Infinity`/`<0`/`>100`/non-number) are rejected, not clamped, and thrown errors propagate. See `docs/risk-engine.md` for the full default-scorer buckets, trust model, and limitations.
244
+
202
245
  ### `verifySolution(tokens, solutions, options?)`
203
246
 
204
247
  Verifies submitted solutions and returns a structured result:
@@ -226,6 +269,59 @@ const result = await verifySolution(tokens, solutions, {
226
269
  });
227
270
  ```
228
271
 
272
+ ### `rateLimiter` hook
273
+
274
+ Ribaunt does not rate-limit anything itself — bring your own limiter. Both `createChallenge()` and `verifySolution()` accept an optional `rateLimiter: { check(key?) => Promise<boolean> }` that runs before the operation. If it returns `false`, issuance or verification is blocked with a `RateLimitedError` (`code = 'rate-limited'`) so you can catch it specifically. Errors thrown by your limiter propagate unchanged.
275
+
276
+ ```ts
277
+ import { RateLimitedError } from 'ribaunt';
278
+
279
+ const rateLimiter = {
280
+ check: async (key?: string) => (await redisBucket.hit(key ?? 'unknown')) < 10,
281
+ };
282
+
283
+ // Challenge endpoint: key the limiter on the client IP by closing over the
284
+ // request instead of passing `context`. Passing `context: req.ip` here would
285
+ // also cryptographically bind each challenge to that IP, forcing the verify
286
+ // call to pass the exact same `context` (see `verifySolution` below).
287
+ app.get('/api/captcha/challenge', async (req, res) => {
288
+ try {
289
+ const challenges = await createChallenge({
290
+ difficulty: 5,
291
+ amount: 4,
292
+ rateLimiter,
293
+ });
294
+ res.json({ challenges });
295
+ } catch (error) {
296
+ if (error instanceof RateLimitedError) {
297
+ return res.status(429).json({ error: 'Too many challenge requests' });
298
+ }
299
+ throw error;
300
+ }
301
+ });
302
+ ```
303
+
304
+ > **Context binding:** if you *do* pass `context` to `createChallenge()`, every challenge token is bound to that context via an HMAC digest. You must then pass the identical `context` to `verifySolution()` or verification fails with `context-mismatch`. Use `context` for binding a challenge to a specific action (e.g. one signup attempt), not for rate limiting.
305
+
306
+ ### `onEvent` telemetry hook
307
+
308
+ Both entry points accept an optional synchronous `onEvent` callback for structured lifecycle events. It is fire-and-forget: a throwing consumer can never break challenge issuance or verification.
309
+
310
+ ```ts
311
+ const counts = { issued: 0, success: 0, failure: 0 };
312
+
313
+ const onEvent = (event: RibauntEvent) => {
314
+ if (event.type === 'challenge-issued') counts.issued += 1;
315
+ else if (event.type === 'verify-success') counts.success += 1;
316
+ else if (event.type === 'verify-failure') counts.failure += 1;
317
+ };
318
+
319
+ await createChallenge({ difficulty: 5, amount: 4, onEvent });
320
+ const result = await verifySolution(tokens, solutions, { onEvent });
321
+ ```
322
+
323
+ `verify-failure` events carry the same `reason` values as `VerifySolutionResult` (`invalid-token`, `expired-token`, `invalid-solution`, `context-mismatch`, `replay-detected`, `replay-store-unavailable`, `configuration-error`). Add aggregation over Windows, StatsD, or Prometheus yourself — Ribaunt only emits raw events.
324
+
229
325
  ### `solveChallenge(token, options?)`
230
326
 
231
327
  Solves challenges synchronously. This is mainly useful for tests and debugging.
@@ -249,6 +345,8 @@ const solutions = solveChallenge(challenges, {
249
345
  show-warning="false"
250
346
  warning-message="Verification may take longer on this device."
251
347
  solve-timeout="15000"
348
+ worker-mode="preferred"
349
+ wasm-mode="preferred"
252
350
  disabled="false"
253
351
  ></ribaunt-widget>
254
352
  ```
@@ -262,7 +360,10 @@ const solutions = solveChallenge(challenges, {
262
360
  | `calibrate` | `calibrate` | Benchmarks the browser and sends `{ calibration }` with POST challenge requests. |
263
361
  | `show-warning` | `showWarning` | Shows a warning banner. |
264
362
  | `warning-message` | `warningMessage` | Custom warning text. |
265
- | `solve-timeout` | `solveTimeout` | Optional solve timeout in milliseconds. |
363
+ | `show-progress` | `showProgress` | Set to `"false"` to swap the percentage ring for a plain bars spinner with a static `Loading...` label. |
364
+ | `solve-timeout` | `solveTimeout` | Optional timeout in milliseconds for the whole verification attempt (fetching, solving, and verifying). |
365
+ | `worker-mode` | `workerMode` | Web Worker solving: `preferred` (default; falls back to the main thread), `required` (fail if unavailable), or `disabled`. |
366
+ | `wasm-mode` | `wasmMode` | WASM solver: `preferred` (default; uses WASM batch solver inside worker if available), `disabled` (always use JS solver inside worker). Independent from `worker-mode`. |
266
367
  | `disabled` | `disabled` | Blocks user interaction and automatic verification. |
267
368
  | | `fallback` | React-only. Custom loading element while widget dynamic import loads. Defaults to a built-in shimmer skeleton. |
268
369
 
@@ -295,6 +396,7 @@ See [theming docs](https://github.com/ribaunt/ribaunt/blob/main/docs/theming.md)
295
396
 
296
397
  - [Quick start](https://github.com/ribaunt/ribaunt/blob/main/docs/quick-start.md)
297
398
  - [Configuration](https://github.com/ribaunt/ribaunt/blob/main/docs/configuration.md)
399
+ - [Risk Engine](https://github.com/ribaunt/ribaunt/blob/main/docs/risk-engine.md)
298
400
  - [Events](https://github.com/ribaunt/ribaunt/blob/main/docs/events.md)
299
401
  - [HTML integration](https://github.com/ribaunt/ribaunt/blob/main/docs/integrations/html.md)
300
402
  - [React integration](https://github.com/ribaunt/ribaunt/blob/main/docs/integrations/react.md)
@@ -313,9 +415,10 @@ You can also ask documentation-aware tools to use the Context7 library ID:
313
415
  ## Development
314
416
 
315
417
  ```bash
316
- npm install
317
- npm test
318
- npm run build
418
+ corepack enable
419
+ pnpm install
420
+ pnpm test
421
+ pnpm run build
319
422
  ```
320
423
 
321
424
  ## License
Binary file
@@ -0,0 +1,174 @@
1
+ /// <reference lib="webworker" />
2
+ import { solveChallenge, decodeChallengeToken } from './solver.js';
3
+ import { ensureWasm, solveBatch, resetWasmHeap } from './wasm-solver.js';
4
+ const workerScope = self;
5
+ const activeControllers = new Map();
6
+ // Cache WASM initialization per worker lifetime (wasm-solver already caches, but keep explicit)
7
+ let wasmBackendState = 'uninitialized';
8
+ const WASM_BATCH_SIZE = 1024;
9
+ const yieldToEventLoop = (() => {
10
+ if (typeof MessageChannel !== 'function') {
11
+ return () => new Promise((resolve) => setTimeout(resolve, 0));
12
+ }
13
+ let channel = null;
14
+ const waiting = new Set();
15
+ return () => {
16
+ if (!channel) {
17
+ channel = new MessageChannel();
18
+ channel.port1.onmessage = () => {
19
+ for (const resolve of waiting)
20
+ resolve();
21
+ waiting.clear();
22
+ };
23
+ channel.port1.unref?.();
24
+ }
25
+ return new Promise((resolve) => {
26
+ waiting.add(resolve);
27
+ channel.port2.postMessage(null);
28
+ });
29
+ };
30
+ })();
31
+ async function solveSingleChallengeWasm(token, signal) {
32
+ const payload = decodeChallengeToken(token);
33
+ if (!payload)
34
+ return undefined;
35
+ const { challenge, difficulty } = payload;
36
+ let startNonce = 0;
37
+ try {
38
+ while (true) {
39
+ if (signal?.aborted) {
40
+ throw new DOMException('Challenge solving aborted', 'AbortError');
41
+ }
42
+ const result = solveBatch(challenge, startNonce, WASM_BATCH_SIZE, difficulty);
43
+ if (result.found && result.nonce && result.hash) {
44
+ // Validate before returning (wasm-solver already validated, but double-check)
45
+ if (typeof result.nonce !== 'string' || typeof result.hash !== 'string') {
46
+ throw new Error('Invalid WASM result shape');
47
+ }
48
+ return { nonce: result.nonce, hash: result.hash };
49
+ }
50
+ startNonce += WASM_BATCH_SIZE;
51
+ // Overflow guard - do not wrap; allow final batch where last nonce is 0x7fffffff
52
+ if (startNonce > 0x7fffffff - WASM_BATCH_SIZE + 1) {
53
+ throw new Error('WASM solver nonce overflow');
54
+ }
55
+ // Yield every 2048 nonces to keep worker responsive (matches JS solver's 2048)
56
+ if (startNonce % 2048 === 0) {
57
+ await yieldToEventLoop();
58
+ }
59
+ }
60
+ }
61
+ finally {
62
+ // Reset heap to avoid leak between tokens/requests
63
+ try {
64
+ resetWasmHeap();
65
+ }
66
+ catch {
67
+ // ignore
68
+ }
69
+ }
70
+ }
71
+ async function solveChallengeWasm(tokens, onProgress, signal) {
72
+ const solutions = [];
73
+ for (let i = 0; i < tokens.length; i++) {
74
+ const token = tokens[i];
75
+ if (!token)
76
+ throw new Error(`Invalid token at index ${i}`);
77
+ if (signal?.aborted)
78
+ throw new DOMException('Challenge solving aborted', 'AbortError');
79
+ const solution = await solveSingleChallengeWasm(token, signal);
80
+ if (!solution)
81
+ throw new Error(`Failed to solve challenge ${i + 1}`);
82
+ solutions.push(solution);
83
+ if (onProgress) {
84
+ const progress = Math.round(((i + 1) / tokens.length) * 100);
85
+ onProgress(progress);
86
+ }
87
+ }
88
+ return solutions;
89
+ }
90
+ async function selectBackend(wasmMode) {
91
+ if (wasmMode === 'disabled')
92
+ return 'js';
93
+ // preferred (default)
94
+ if (wasmBackendState === 'wasm-unavailable')
95
+ return 'js';
96
+ if (wasmBackendState === 'wasm-ready')
97
+ return 'wasm';
98
+ // uninitialized -> try to init
99
+ try {
100
+ const ok = await ensureWasm();
101
+ wasmBackendState = ok ? 'wasm-ready' : 'wasm-unavailable';
102
+ return ok ? 'wasm' : 'js';
103
+ }
104
+ catch {
105
+ wasmBackendState = 'wasm-unavailable';
106
+ return 'js';
107
+ }
108
+ }
109
+ workerScope.addEventListener('message', (event) => {
110
+ const request = event.data;
111
+ if (!request)
112
+ return;
113
+ if (request.type === 'cancel') {
114
+ const controller = activeControllers.get(request.id);
115
+ if (controller) {
116
+ controller.abort();
117
+ activeControllers.delete(request.id);
118
+ workerScope.postMessage({ type: 'cancelled', id: request.id });
119
+ workerScope.close();
120
+ }
121
+ return;
122
+ }
123
+ if (request.type !== 'solve')
124
+ return;
125
+ const controller = new AbortController();
126
+ activeControllers.set(request.id, controller);
127
+ const wasmMode = request.wasmMode === 'disabled' ? 'disabled' : 'preferred';
128
+ (async () => {
129
+ const backend = await selectBackend(wasmMode);
130
+ // Telemetry: report backend selection once per request
131
+ if (!controller.signal.aborted) {
132
+ try {
133
+ workerScope.postMessage({ type: 'backend', id: request.id, backend });
134
+ }
135
+ catch {
136
+ // telemetry must never break solving
137
+ }
138
+ }
139
+ const solver = backend === 'wasm' ? solveChallengeWasm : solveChallenge;
140
+ // For wasm-unavailable fallback, re-select if wasm fails during solve? We already selected js
141
+ // But if backend is wasm and solve fails with internal error, we surface not fallback
142
+ return solver(request.tokens, (progress) => {
143
+ if (!controller.signal.aborted) {
144
+ workerScope.postMessage({
145
+ type: 'progress',
146
+ id: request.id,
147
+ progress,
148
+ });
149
+ }
150
+ }, controller.signal);
151
+ })().then((solutions) => {
152
+ activeControllers.delete(request.id);
153
+ if (controller.signal.aborted)
154
+ return;
155
+ workerScope.postMessage({
156
+ type: 'result',
157
+ id: request.id,
158
+ solutions,
159
+ });
160
+ }, (error) => {
161
+ activeControllers.delete(request.id);
162
+ if (controller.signal.aborted)
163
+ return;
164
+ // If WASM was selected and failed due to unexpected runtime error, surface as error
165
+ // For expected initialization failures we already fell back to JS, so this error is genuine
166
+ workerScope.postMessage({
167
+ type: 'error',
168
+ id: request.id,
169
+ error: error instanceof Error ? error.message : String(error),
170
+ });
171
+ });
172
+ });
173
+ export {};
174
+ //# sourceMappingURL=solver-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solver-worker.js","sourceRoot":"","sources":["../src/solver-worker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Browser-compatible challenge solver using Web Crypto API
3
+ */
4
+ /**
5
+ * Decode JWT token (browser-compatible, without verification)
6
+ */
7
+ function decodeJWT(token) {
8
+ try {
9
+ const parts = token.split('.');
10
+ if (parts.length !== 3 || !parts[1])
11
+ return null;
12
+ const normalizedPayload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
13
+ const paddedPayload = normalizedPayload.padEnd(normalizedPayload.length + ((4 - (normalizedPayload.length % 4)) % 4), '=');
14
+ const payload = JSON.parse(atob(paddedPayload));
15
+ return payload;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * SHA-256 hash using Web Crypto API
23
+ */
24
+ async function sha256(message) {
25
+ if (typeof TextEncoder === 'undefined') {
26
+ throw new Error('TextEncoder is unavailable in this browser environment');
27
+ }
28
+ if (!globalThis.crypto?.subtle) {
29
+ throw new Error('Web Crypto API is unavailable. Use HTTPS or localhost.');
30
+ }
31
+ const msgBuffer = new TextEncoder().encode(message);
32
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
33
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
34
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
35
+ return hashHex;
36
+ }
37
+ /**
38
+ * Yield to the event loop without the ~4ms clamping browsers apply to
39
+ * nested setTimeout calls. MessageChannel tasks run as macrotasks with
40
+ * microsecond-level latency, keeping the UI responsive at a fraction of
41
+ * the timer overhead.
42
+ *
43
+ * The channel is created lazily on first yield, and the receiving port is
44
+ * unref'd where supported (Node.js): a port with an active message listener
45
+ * otherwise keeps the host event loop alive and prevents clean exit.
46
+ */
47
+ const yieldToEventLoop = (() => {
48
+ if (typeof MessageChannel !== 'function') {
49
+ return () => new Promise((resolve) => setTimeout(resolve, 0));
50
+ }
51
+ let channel = null;
52
+ const waiting = new Set();
53
+ return () => {
54
+ if (!channel) {
55
+ channel = new MessageChannel();
56
+ channel.port1.onmessage = () => {
57
+ for (const resolve of waiting)
58
+ resolve();
59
+ waiting.clear();
60
+ };
61
+ channel.port1.unref?.();
62
+ }
63
+ return new Promise((resolve) => {
64
+ waiting.add(resolve);
65
+ channel.port2.postMessage(null);
66
+ });
67
+ };
68
+ })();
69
+ export async function calibrateBrowser(iterations = 128) {
70
+ if (!Number.isFinite(iterations) || iterations < 1) {
71
+ throw new Error('Calibration iterations must be at least 1');
72
+ }
73
+ const normalizedIterations = Math.floor(iterations);
74
+ const startedAt = performance.now();
75
+ for (let index = 0; index < normalizedIterations; index++) {
76
+ await sha256(`ribaunt-calibration:${index}`);
77
+ }
78
+ return {
79
+ iterations: normalizedIterations,
80
+ durationMs: Math.max(1, Math.round(performance.now() - startedAt)),
81
+ };
82
+ }
83
+ export const calibrateClient = calibrateBrowser;
84
+ export function decodeChallengeToken(token) {
85
+ return decodeJWT(token);
86
+ }
87
+ /**
88
+ * Solve a single challenge token (browser-compatible)
89
+ */
90
+ export async function solveSingleChallenge(token, signal) {
91
+ const payload = decodeJWT(token);
92
+ if (!payload)
93
+ return undefined;
94
+ const { challenge, difficulty } = payload;
95
+ const prefix = '0'.repeat(difficulty);
96
+ let nonce = 0;
97
+ while (true) {
98
+ if (signal?.aborted) {
99
+ throw new DOMException('Challenge solving aborted', 'AbortError');
100
+ }
101
+ const hash = await sha256(`${challenge}${nonce}`);
102
+ if (hash.startsWith(prefix)) {
103
+ return { nonce: String(nonce), hash };
104
+ }
105
+ nonce++;
106
+ // Yield to keep the UI responsive; each batch amortizes the yield cost.
107
+ if (nonce % 2048 === 0) {
108
+ await yieldToEventLoop();
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Solve multiple challenge tokens (browser-compatible)
114
+ */
115
+ export async function solveChallenge(tokens, onProgress, signal) {
116
+ const solutions = [];
117
+ for (let i = 0; i < tokens.length; i++) {
118
+ const token = tokens[i];
119
+ if (!token) {
120
+ throw new Error(`Invalid token at index ${i}`);
121
+ }
122
+ const solution = await solveSingleChallenge(token, signal);
123
+ if (!solution) {
124
+ throw new Error(`Failed to solve challenge ${i + 1}`);
125
+ }
126
+ solutions.push(solution);
127
+ // Report progress
128
+ if (onProgress) {
129
+ const progress = Math.round(((i + 1) / tokens.length) * 100);
130
+ onProgress(progress);
131
+ }
132
+ }
133
+ return solutions;
134
+ }
135
+ //# sourceMappingURL=solver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"solver.js","sourceRoot":"","sources":["../src/solver.ts"],"names":[],"mappings":"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"}