ribaunt 0.2.3 → 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 +53 -8
  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 +24 -1
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +160 -13
  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 +24 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +158 -12
  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 +135 -5
  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 +3 -1
  48. package/dist/widget-react.d.ts.map +1 -1
  49. package/dist/widget-react.js +99 -15
  50. package/dist/widget-react.js.map +1 -1
  51. package/dist/widget.d.ts +35 -15
  52. package/dist/widget.d.ts.map +1 -1
  53. package/dist/widget.js +61 -10
  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 +32 -5
  58. package/dist/worker-client.js.map +1 -1
  59. package/package.json +19 -7
  60. package/context7.json +0 -4
package/README.md CHANGED
@@ -16,6 +16,7 @@ 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
@@ -151,6 +152,8 @@ const challenges = await createChallenge(5, 4, 120);
151
152
 
152
153
  Validate user- or config-controlled values before passing them to `createChallenge()`. Invalid values throw.
153
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
+
154
157
  ### `createChallenge({ difficulty: "auto", ... })`
155
158
 
156
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.
@@ -204,9 +207,41 @@ const workload = selectWorkload({
204
207
  minDifficulty: 3,
205
208
  maxDifficulty: 6,
206
209
  });
207
- // { difficulty: 5, amount: 4, estimatedAttempts: 1256 }
210
+ // { difficulty: 3, amount: 5, estimatedAttempts: 20480 }
208
211
  ```
209
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 */ }
237
+ ```
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
+
210
245
  ### `verifySolution(tokens, solutions, options?)`
211
246
 
212
247
  Verifies submitted solutions and returns a structured result:
@@ -245,13 +280,15 @@ const rateLimiter = {
245
280
  check: async (key?: string) => (await redisBucket.hit(key ?? 'unknown')) < 10,
246
281
  };
247
282
 
248
- // Challenge endpoint: key on the client IP so each IP is limited independently.
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).
249
287
  app.get('/api/captcha/challenge', async (req, res) => {
250
288
  try {
251
289
  const challenges = await createChallenge({
252
290
  difficulty: 5,
253
291
  amount: 4,
254
- context: req.ip,
255
292
  rateLimiter,
256
293
  });
257
294
  res.json({ challenges });
@@ -264,6 +301,8 @@ app.get('/api/captcha/challenge', async (req, res) => {
264
301
  });
265
302
  ```
266
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
+
267
306
  ### `onEvent` telemetry hook
268
307
 
269
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.
@@ -281,7 +320,7 @@ await createChallenge({ difficulty: 5, amount: 4, onEvent });
281
320
  const result = await verifySolution(tokens, solutions, { onEvent });
282
321
  ```
283
322
 
284
- `verify-failure` events carry the same `reason` values as `VerifySolutionResult` (`invalid-token`, `expired-token`, `invalid-solution`, `context-mismatch`, `replay-detected`, `configuration-error`). Add aggregation over Windows, StatsD, or Prometheus yourself — Ribaunt only emits raw events.
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.
285
324
 
286
325
  ### `solveChallenge(token, options?)`
287
326
 
@@ -306,6 +345,8 @@ const solutions = solveChallenge(challenges, {
306
345
  show-warning="false"
307
346
  warning-message="Verification may take longer on this device."
308
347
  solve-timeout="15000"
348
+ worker-mode="preferred"
349
+ wasm-mode="preferred"
309
350
  disabled="false"
310
351
  ></ribaunt-widget>
311
352
  ```
@@ -320,7 +361,9 @@ const solutions = solveChallenge(challenges, {
320
361
  | `show-warning` | `showWarning` | Shows a warning banner. |
321
362
  | `warning-message` | `warningMessage` | Custom warning text. |
322
363
  | `show-progress` | `showProgress` | Set to `"false"` to swap the percentage ring for a plain bars spinner with a static `Loading...` label. |
323
- | `solve-timeout` | `solveTimeout` | Optional solve timeout in milliseconds. |
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`. |
324
367
  | `disabled` | `disabled` | Blocks user interaction and automatic verification. |
325
368
  | | `fallback` | React-only. Custom loading element while widget dynamic import loads. Defaults to a built-in shimmer skeleton. |
326
369
 
@@ -353,6 +396,7 @@ See [theming docs](https://github.com/ribaunt/ribaunt/blob/main/docs/theming.md)
353
396
 
354
397
  - [Quick start](https://github.com/ribaunt/ribaunt/blob/main/docs/quick-start.md)
355
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)
356
400
  - [Events](https://github.com/ribaunt/ribaunt/blob/main/docs/events.md)
357
401
  - [HTML integration](https://github.com/ribaunt/ribaunt/blob/main/docs/integrations/html.md)
358
402
  - [React integration](https://github.com/ribaunt/ribaunt/blob/main/docs/integrations/react.md)
@@ -371,9 +415,10 @@ You can also ask documentation-aware tools to use the Context7 library ID:
371
415
  ## Development
372
416
 
373
417
  ```bash
374
- npm install
375
- npm test
376
- npm run build
418
+ corepack enable
419
+ pnpm install
420
+ pnpm test
421
+ pnpm run build
377
422
  ```
378
423
 
379
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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"}