@nawa-captcha/server 0.0.0-stage → 0.1.1

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 (258) hide show
  1. package/README.md +65 -2
  2. package/dist/binding.d.ts +22 -0
  3. package/dist/binding.d.ts.map +1 -0
  4. package/dist/binding.js +46 -0
  5. package/dist/binding.js.map +1 -0
  6. package/dist/catalog.d.ts +12 -0
  7. package/dist/catalog.d.ts.map +1 -0
  8. package/dist/catalog.js +42 -0
  9. package/dist/catalog.js.map +1 -0
  10. package/dist/crypto.d.ts +44 -0
  11. package/dist/crypto.d.ts.map +1 -0
  12. package/dist/crypto.js +108 -0
  13. package/dist/crypto.js.map +1 -0
  14. package/dist/engine.d.ts +245 -0
  15. package/dist/engine.d.ts.map +1 -0
  16. package/dist/engine.js +407 -0
  17. package/dist/engine.js.map +1 -0
  18. package/dist/express.d.ts +47 -0
  19. package/dist/express.d.ts.map +1 -0
  20. package/dist/express.js +107 -0
  21. package/dist/express.js.map +1 -0
  22. package/dist/heuristics.d.ts +33 -0
  23. package/dist/heuristics.d.ts.map +1 -0
  24. package/dist/heuristics.js +152 -0
  25. package/dist/heuristics.js.map +1 -0
  26. package/dist/http.d.ts +51 -0
  27. package/dist/http.d.ts.map +1 -0
  28. package/dist/http.js +70 -0
  29. package/dist/http.js.map +1 -0
  30. package/dist/i18n.d.ts +237 -0
  31. package/dist/i18n.d.ts.map +1 -0
  32. package/dist/i18n.js +92 -0
  33. package/dist/i18n.js.map +1 -0
  34. package/dist/index.d.ts +24 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +22 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/primitives.browser.d.ts +15 -0
  39. package/dist/primitives.browser.d.ts.map +1 -0
  40. package/dist/primitives.browser.js +328 -0
  41. package/dist/primitives.browser.js.map +1 -0
  42. package/dist/primitives.d.ts +17 -0
  43. package/dist/primitives.d.ts.map +1 -0
  44. package/dist/primitives.js +58 -0
  45. package/dist/primitives.js.map +1 -0
  46. package/dist/puzzles/accessible.d.ts +43 -0
  47. package/dist/puzzles/accessible.d.ts.map +1 -0
  48. package/dist/puzzles/accessible.js +317 -0
  49. package/dist/puzzles/accessible.js.map +1 -0
  50. package/dist/puzzles/audio.d.ts +42 -0
  51. package/dist/puzzles/audio.d.ts.map +1 -0
  52. package/dist/puzzles/audio.js +109 -0
  53. package/dist/puzzles/audio.js.map +1 -0
  54. package/dist/puzzles/biggest.d.ts +5 -0
  55. package/dist/puzzles/biggest.d.ts.map +1 -0
  56. package/dist/puzzles/biggest.js +40 -0
  57. package/dist/puzzles/biggest.js.map +1 -0
  58. package/dist/puzzles/clock.d.ts +8 -0
  59. package/dist/puzzles/clock.d.ts.map +1 -0
  60. package/dist/puzzles/clock.js +68 -0
  61. package/dist/puzzles/clock.js.map +1 -0
  62. package/dist/puzzles/color-name.d.ts +5 -0
  63. package/dist/puzzles/color-name.d.ts.map +1 -0
  64. package/dist/puzzles/color-name.js +73 -0
  65. package/dist/puzzles/color-name.js.map +1 -0
  66. package/dist/puzzles/common.d.ts +75 -0
  67. package/dist/puzzles/common.d.ts.map +1 -0
  68. package/dist/puzzles/common.js +268 -0
  69. package/dist/puzzles/common.js.map +1 -0
  70. package/dist/puzzles/count.d.ts +8 -0
  71. package/dist/puzzles/count.d.ts.map +1 -0
  72. package/dist/puzzles/count.js +38 -0
  73. package/dist/puzzles/count.js.map +1 -0
  74. package/dist/puzzles/dice.d.ts +8 -0
  75. package/dist/puzzles/dice.d.ts.map +1 -0
  76. package/dist/puzzles/dice.js +61 -0
  77. package/dist/puzzles/dice.js.map +1 -0
  78. package/dist/puzzles/find-shape.d.ts +8 -0
  79. package/dist/puzzles/find-shape.d.ts.map +1 -0
  80. package/dist/puzzles/find-shape.js +45 -0
  81. package/dist/puzzles/find-shape.js.map +1 -0
  82. package/dist/puzzles/grid.d.ts +44 -0
  83. package/dist/puzzles/grid.d.ts.map +1 -0
  84. package/dist/puzzles/grid.js +33 -0
  85. package/dist/puzzles/grid.js.map +1 -0
  86. package/dist/puzzles/hold.d.ts +24 -0
  87. package/dist/puzzles/hold.d.ts.map +1 -0
  88. package/dist/puzzles/hold.js +68 -0
  89. package/dist/puzzles/hold.js.map +1 -0
  90. package/dist/puzzles/icons-objects.d.ts +3 -0
  91. package/dist/puzzles/icons-objects.d.ts.map +1 -0
  92. package/dist/puzzles/icons-objects.js +5 -0
  93. package/dist/puzzles/icons-objects.js.map +1 -0
  94. package/dist/puzzles/icons-weather.d.ts +3 -0
  95. package/dist/puzzles/icons-weather.d.ts.map +1 -0
  96. package/dist/puzzles/icons-weather.js +5 -0
  97. package/dist/puzzles/icons-weather.js.map +1 -0
  98. package/dist/puzzles/icons.d.ts +14 -0
  99. package/dist/puzzles/icons.d.ts.map +1 -0
  100. package/dist/puzzles/icons.js +49 -0
  101. package/dist/puzzles/icons.js.map +1 -0
  102. package/dist/puzzles/index.d.ts +63 -0
  103. package/dist/puzzles/index.d.ts.map +1 -0
  104. package/dist/puzzles/index.js +93 -0
  105. package/dist/puzzles/index.js.map +1 -0
  106. package/dist/puzzles/jigsaw.d.ts +22 -0
  107. package/dist/puzzles/jigsaw.d.ts.map +1 -0
  108. package/dist/puzzles/jigsaw.js +38 -0
  109. package/dist/puzzles/jigsaw.js.map +1 -0
  110. package/dist/puzzles/math-compare.d.ts +8 -0
  111. package/dist/puzzles/math-compare.d.ts.map +1 -0
  112. package/dist/puzzles/math-compare.js +47 -0
  113. package/dist/puzzles/math-compare.js.map +1 -0
  114. package/dist/puzzles/math.d.ts +16 -0
  115. package/dist/puzzles/math.d.ts.map +1 -0
  116. package/dist/puzzles/math.js +106 -0
  117. package/dist/puzzles/math.js.map +1 -0
  118. package/dist/puzzles/maze.d.ts +18 -0
  119. package/dist/puzzles/maze.d.ts.map +1 -0
  120. package/dist/puzzles/maze.js +138 -0
  121. package/dist/puzzles/maze.js.map +1 -0
  122. package/dist/puzzles/odd-one-out.d.ts +5 -0
  123. package/dist/puzzles/odd-one-out.d.ts.map +1 -0
  124. package/dist/puzzles/odd-one-out.js +36 -0
  125. package/dist/puzzles/odd-one-out.js.map +1 -0
  126. package/dist/puzzles/odd-rotation.d.ts +8 -0
  127. package/dist/puzzles/odd-rotation.d.ts.map +1 -0
  128. package/dist/puzzles/odd-rotation.js +48 -0
  129. package/dist/puzzles/odd-rotation.js.map +1 -0
  130. package/dist/puzzles/order-numbers.d.ts +5 -0
  131. package/dist/puzzles/order-numbers.d.ts.map +1 -0
  132. package/dist/puzzles/order-numbers.js +32 -0
  133. package/dist/puzzles/order-numbers.js.map +1 -0
  134. package/dist/puzzles/order-size.d.ts +5 -0
  135. package/dist/puzzles/order-size.d.ts.map +1 -0
  136. package/dist/puzzles/order-size.js +30 -0
  137. package/dist/puzzles/order-size.js.map +1 -0
  138. package/dist/puzzles/order-time.d.ts +5 -0
  139. package/dist/puzzles/order-time.d.ts.map +1 -0
  140. package/dist/puzzles/order-time.js +27 -0
  141. package/dist/puzzles/order-time.js.map +1 -0
  142. package/dist/puzzles/order.d.ts +30 -0
  143. package/dist/puzzles/order.d.ts.map +1 -0
  144. package/dist/puzzles/order.js +61 -0
  145. package/dist/puzzles/order.js.map +1 -0
  146. package/dist/puzzles/pairs.d.ts +15 -0
  147. package/dist/puzzles/pairs.d.ts.map +1 -0
  148. package/dist/puzzles/pairs.js +63 -0
  149. package/dist/puzzles/pairs.js.map +1 -0
  150. package/dist/puzzles/pattern.d.ts +8 -0
  151. package/dist/puzzles/pattern.d.ts.map +1 -0
  152. package/dist/puzzles/pattern.js +68 -0
  153. package/dist/puzzles/pattern.js.map +1 -0
  154. package/dist/puzzles/pow.d.ts +23 -0
  155. package/dist/puzzles/pow.d.ts.map +1 -0
  156. package/dist/puzzles/pow.js +52 -0
  157. package/dist/puzzles/pow.js.map +1 -0
  158. package/dist/puzzles/rotate-match.d.ts +8 -0
  159. package/dist/puzzles/rotate-match.d.ts.map +1 -0
  160. package/dist/puzzles/rotate-match.js +34 -0
  161. package/dist/puzzles/rotate-match.js.map +1 -0
  162. package/dist/puzzles/rotate-object.d.ts +5 -0
  163. package/dist/puzzles/rotate-object.d.ts.map +1 -0
  164. package/dist/puzzles/rotate-object.js +33 -0
  165. package/dist/puzzles/rotate-object.js.map +1 -0
  166. package/dist/puzzles/rotate.d.ts +24 -0
  167. package/dist/puzzles/rotate.d.ts.map +1 -0
  168. package/dist/puzzles/rotate.js +40 -0
  169. package/dist/puzzles/rotate.js.map +1 -0
  170. package/dist/puzzles/shadow-drag.d.ts +20 -0
  171. package/dist/puzzles/shadow-drag.d.ts.map +1 -0
  172. package/dist/puzzles/shadow-drag.js +64 -0
  173. package/dist/puzzles/shadow-drag.js.map +1 -0
  174. package/dist/puzzles/shadow-pick.d.ts +5 -0
  175. package/dist/puzzles/shadow-pick.d.ts.map +1 -0
  176. package/dist/puzzles/shadow-pick.js +38 -0
  177. package/dist/puzzles/shadow-pick.js.map +1 -0
  178. package/dist/puzzles/slide-unlock.d.ts +11 -0
  179. package/dist/puzzles/slide-unlock.d.ts.map +1 -0
  180. package/dist/puzzles/slide-unlock.js +79 -0
  181. package/dist/puzzles/slide-unlock.js.map +1 -0
  182. package/dist/puzzles/slider.d.ts +23 -0
  183. package/dist/puzzles/slider.d.ts.map +1 -0
  184. package/dist/puzzles/slider.js +74 -0
  185. package/dist/puzzles/slider.js.map +1 -0
  186. package/dist/puzzles/spell.d.ts +8 -0
  187. package/dist/puzzles/spell.d.ts.map +1 -0
  188. package/dist/puzzles/spell.js +41 -0
  189. package/dist/puzzles/spell.js.map +1 -0
  190. package/dist/puzzles/story.d.ts +5 -0
  191. package/dist/puzzles/story.d.ts.map +1 -0
  192. package/dist/puzzles/story.js +101 -0
  193. package/dist/puzzles/story.js.map +1 -0
  194. package/dist/puzzles/symmetry.d.ts +8 -0
  195. package/dist/puzzles/symmetry.d.ts.map +1 -0
  196. package/dist/puzzles/symmetry.js +49 -0
  197. package/dist/puzzles/symmetry.js.map +1 -0
  198. package/dist/puzzles/tap-order.d.ts +20 -0
  199. package/dist/puzzles/tap-order.d.ts.map +1 -0
  200. package/dist/puzzles/tap-order.js +37 -0
  201. package/dist/puzzles/tap-order.js.map +1 -0
  202. package/dist/puzzles/trace-letters.d.ts +5 -0
  203. package/dist/puzzles/trace-letters.d.ts.map +1 -0
  204. package/dist/puzzles/trace-letters.js +19 -0
  205. package/dist/puzzles/trace-letters.js.map +1 -0
  206. package/dist/puzzles/trace.d.ts +32 -0
  207. package/dist/puzzles/trace.d.ts.map +1 -0
  208. package/dist/puzzles/trace.js +81 -0
  209. package/dist/puzzles/trace.js.map +1 -0
  210. package/dist/render/audio.d.ts +53 -0
  211. package/dist/render/audio.d.ts.map +1 -0
  212. package/dist/render/audio.js +236 -0
  213. package/dist/render/audio.js.map +1 -0
  214. package/dist/render/clock.d.ts +6 -0
  215. package/dist/render/clock.d.ts.map +1 -0
  216. package/dist/render/clock.js +23 -0
  217. package/dist/render/clock.js.map +1 -0
  218. package/dist/render/glyphs.d.ts +18 -0
  219. package/dist/render/glyphs.d.ts.map +1 -0
  220. package/dist/render/glyphs.js +70 -0
  221. package/dist/render/glyphs.js.map +1 -0
  222. package/dist/render/pictograms.d.ts +60 -0
  223. package/dist/render/pictograms.d.ts.map +1 -0
  224. package/dist/render/pictograms.js +167 -0
  225. package/dist/render/pictograms.js.map +1 -0
  226. package/dist/render/png.d.ts +11 -0
  227. package/dist/render/png.d.ts.map +1 -0
  228. package/dist/render/png.js +107 -0
  229. package/dist/render/png.js.map +1 -0
  230. package/dist/render/raster.d.ts +61 -0
  231. package/dist/render/raster.d.ts.map +1 -0
  232. package/dist/render/raster.js +389 -0
  233. package/dist/render/raster.js.map +1 -0
  234. package/dist/render/shapes.d.ts +25 -0
  235. package/dist/render/shapes.d.ts.map +1 -0
  236. package/dist/render/shapes.js +157 -0
  237. package/dist/render/shapes.js.map +1 -0
  238. package/dist/rng.d.ts +25 -0
  239. package/dist/rng.d.ts.map +1 -0
  240. package/dist/rng.js +93 -0
  241. package/dist/rng.js.map +1 -0
  242. package/dist/selection.d.ts +54 -0
  243. package/dist/selection.d.ts.map +1 -0
  244. package/dist/selection.js +85 -0
  245. package/dist/selection.js.map +1 -0
  246. package/dist/store.d.ts +66 -0
  247. package/dist/store.d.ts.map +1 -0
  248. package/dist/store.js +100 -0
  249. package/dist/store.js.map +1 -0
  250. package/dist/tokens.d.ts +70 -0
  251. package/dist/tokens.d.ts.map +1 -0
  252. package/dist/tokens.js +103 -0
  253. package/dist/tokens.js.map +1 -0
  254. package/dist/types.d.ts +62 -0
  255. package/dist/types.d.ts.map +1 -0
  256. package/dist/types.js +61 -0
  257. package/dist/types.js.map +1 -0
  258. package/package.json +68 -4
package/README.md CHANGED
@@ -1,3 +1,66 @@
1
- # Temporary Holding Version
1
+ # @nawa-captcha/server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Nawa Captcha** — a self-hosted, privacy-friendly and accessible puzzle CAPTCHA. A drop-in alternative to reCAPTCHA, hCaptcha and Turnstile with no third-party tracking: your server issues the challenge and verifies the answer.
4
+
5
+ This package: Node.js server: issue and verify challenges, Express middleware.
6
+
7
+ - 🧩 38 challenge types you choose from (one, two or many, per action): slider jigsaws, rotate, drag to the shadow, jigsaw, hold, icon/colour/clock/symmetry grids, match pairs, tap in order, spell, order and story tiles, connect the dots, maze, math, dice, invisible proof-of-work, an accessible text question and an audio challenge — [see them live](https://oriebalzyuot96.github.io/nawa-captcha/#playground)
8
+ - 🔒 Answers are sealed (AES-256-GCM) and never reach the browser; pass tokens are signed and single-use
9
+ - ♿ Keyboard, touch and screen-reader friendly (text question and audio challenge); Arabic and English; RTL
10
+ - 🎨 Premium 3D design, six themes in light and dark plus high contrast (WCAG 2.2 AA), Pelagic UI tokens, optional sound effects
11
+ - 🖥️ Backends: Node.js / Express and ASP.NET Core (`NawaCaptcha.AspNetCore`, coming to NuGet)
12
+
13
+ ## Install
14
+
15
+ > **Coming to npm.** `@nawa-captcha/server` is published once the `@nawa-captcha` npm organisation exists. Until then, build it from the [repository](https://github.com/oriebalzyuot96/nawa-captcha) (`npm install && npm run build`, then `npm pack -w @nawa-captcha/server`).
16
+
17
+ ```bash
18
+ npm i @nawa-captcha/server
19
+ # or
20
+ yarn add @nawa-captcha/server
21
+ # or
22
+ pnpm add @nawa-captcha/server
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```js
28
+ import express from 'express';
29
+ import { createNawaCaptcha } from '@nawa-captcha/server';
30
+ import { nawaCaptchaExpress, requireCaptcha } from '@nawa-captcha/server/express';
31
+
32
+ const guard = createNawaCaptcha({
33
+ secret: process.env.NAWA_CAPTCHA_SECRET, // ≥ 32 chars, never sent to browsers
34
+ siteKeys: ['main'],
35
+ actions: ['login'],
36
+ });
37
+
38
+ const app = express();
39
+ // POST /captcha/challenge and POST /captcha/verify
40
+ app.use('/captcha', express.json({ limit: '32kb' }), nawaCaptchaExpress(guard));
41
+ // Protect a route: the token comes from the X-Nawa-Captcha-Token header or the nawa-captcha-token field
42
+ app.post('/login', express.urlencoded({ extended: false }),
43
+ requireCaptcha(guard, { expectedAction: 'login', maxAgeSec: 300 }),
44
+ (req, res) => res.send('ok'));
45
+ ```
46
+
47
+ ## Choosing puzzles
48
+
49
+ ```js
50
+ const guard = createNawaCaptcha({
51
+ secret: process.env.NAWA_CAPTCHA_SECRET,
52
+ types: ['slider', 'rotate-object', 'pairs', 'maze'], // allow-list: one, two or many
53
+ mode: 'random', // 'random' | 'sequence' | 'adaptive' (default)
54
+ weights: { maze: 2 },
55
+ perAction: { login: ['pow', 'slider'], signup: { types: ['maze', 'spell'], steps: 2 } },
56
+ });
57
+ // Routes can demand more: requireCaptcha(guard, { expectedAction: 'signup', allowedTypes: ['maze', 'spell'], minSteps: 2 })
58
+ ```
59
+
60
+ The browser can narrow the list (`<nawa-captcha types="slider,maze">`) but never widen it. With `steps` > 1, `POST /verify` returns `{ next }` until the last puzzle; the pass token then lists every solved type in `stp`. Every type, its keyboard operation and its blind-guess rate are in the [main README](https://github.com/oriebalzyuot96/nawa-captcha#puzzle-types).
61
+
62
+ Then verify on your backend before trusting the request — see the [full guide](https://github.com/oriebalzyuot96/nawa-captcha#readme).
63
+
64
+ 📖 Docs: https://oriebalzyuot96.github.io/nawa-captcha/ · 💻 Source: https://github.com/oriebalzyuot96/nawa-captcha
65
+
66
+ MIT © oriebalzyuot96
@@ -0,0 +1,22 @@
1
+ /** Which client attributes a token is bound to. Encoded in the token as `cbm`. */
2
+ export type BindMode = 'none' | 'ip' | 'ua' | 'ip+ua';
3
+ export declare const BIND_MODES: readonly BindMode[];
4
+ export interface ClientInfo {
5
+ readonly ip?: string | undefined;
6
+ readonly userAgent?: string | undefined;
7
+ }
8
+ /** `::ffff:1.2.3.4` and `1.2.3.4` are the same client; IPv6 is compared lower-case. */
9
+ export declare function normalizeIp(ip: string | undefined): string;
10
+ export declare function normalizeUa(ua: string | undefined): string;
11
+ /**
12
+ * The exact string hashed into `cb`. Mirrored byte-for-byte by the .NET verifier:
13
+ * ip mode: "ip=<ip>"
14
+ * ua mode: "ua=<ua>"
15
+ * ip+ua mode: "ip=<ip>\nua=<ua>"
16
+ */
17
+ export declare function bindingMaterial(mode: BindMode, client: ClientInfo): string | undefined;
18
+ /** Returns base64url(HMAC-SHA256(K_bind, material)), or undefined when the client info is missing. */
19
+ export declare function bindingHash(key: Uint8Array, mode: BindMode, client: ClientInfo): string | undefined;
20
+ /** Pseudonymous per-client key for counters (rate limits, adaptive difficulty). Never stores raw IPs. */
21
+ export declare function clientKey(key: Uint8Array, client: ClientInfo): string;
22
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC;AACtD,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAAkC,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAID,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAK1D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE1D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAatF;AAED,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAGnG;AAED,yGAAyG;AACzG,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAGrE"}
@@ -0,0 +1,46 @@
1
+ import { b64url, hmac } from './crypto.js';
2
+ export const BIND_MODES = ['none', 'ip', 'ua', 'ip+ua'];
3
+ const MAX_UA = 512;
4
+ /** `::ffff:1.2.3.4` and `1.2.3.4` are the same client; IPv6 is compared lower-case. */
5
+ export function normalizeIp(ip) {
6
+ if (!ip)
7
+ return '';
8
+ let value = ip.trim().toLowerCase();
9
+ if (value.startsWith('::ffff:') && value.includes('.'))
10
+ value = value.slice(7);
11
+ return value;
12
+ }
13
+ export function normalizeUa(ua) {
14
+ return (ua ?? '').trim().slice(0, MAX_UA);
15
+ }
16
+ /**
17
+ * The exact string hashed into `cb`. Mirrored byte-for-byte by the .NET verifier:
18
+ * ip mode: "ip=<ip>"
19
+ * ua mode: "ua=<ua>"
20
+ * ip+ua mode: "ip=<ip>\nua=<ua>"
21
+ */
22
+ export function bindingMaterial(mode, client) {
23
+ const ip = normalizeIp(client.ip);
24
+ const ua = normalizeUa(client.userAgent);
25
+ switch (mode) {
26
+ case 'none':
27
+ return undefined;
28
+ case 'ip':
29
+ return ip ? `ip=${ip}` : undefined;
30
+ case 'ua':
31
+ return ua ? `ua=${ua}` : undefined;
32
+ case 'ip+ua':
33
+ return ip && ua ? `ip=${ip}\nua=${ua}` : undefined;
34
+ }
35
+ }
36
+ /** Returns base64url(HMAC-SHA256(K_bind, material)), or undefined when the client info is missing. */
37
+ export function bindingHash(key, mode, client) {
38
+ const material = bindingMaterial(mode, client);
39
+ return material === undefined ? undefined : b64url(hmac(key, material));
40
+ }
41
+ /** Pseudonymous per-client key for counters (rate limits, adaptive difficulty). Never stores raw IPs. */
42
+ export function clientKey(key, client) {
43
+ const ip = normalizeIp(client.ip);
44
+ return b64url(hmac(key, `ck=${ip || 'unknown'}`)).slice(0, 22);
45
+ }
46
+ //# sourceMappingURL=binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAI3C,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAO7E,MAAM,MAAM,GAAG,GAAG,CAAC;AAEnB,uFAAuF;AACvF,MAAM,UAAU,WAAW,CAAC,EAAsB;IAChD,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,CAAC;IACnB,IAAI,KAAK,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,IAAI,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC/E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAsB;IAChD,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,IAAc,EAAE,MAAkB;IAChE,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACzC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,MAAM;YACT,OAAO,SAAS,CAAC;QACnB,KAAK,IAAI;YACP,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACrC,KAAK,IAAI;YACP,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACrC,KAAK,OAAO;YACV,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACvD,CAAC;AACH,CAAC;AAED,sGAAsG;AACtG,MAAM,UAAU,WAAW,CAAC,GAAe,EAAE,IAAc,EAAE,MAAkB;IAC7E,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,SAAS,CAAC,GAAe,EAAE,MAAkB;IAC3D,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAClC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { ChallengeType } from './types.js';
2
+ export type TypeFamily = 'drag' | 'select' | 'tap' | 'order' | 'draw' | 'type' | 'invisible';
3
+ export interface TypeInfo {
4
+ readonly family: TypeFamily;
5
+ readonly en: string;
6
+ readonly ar: string;
7
+ /** Rough chance that a blind guess passes one attempt (documentation; see README). */
8
+ readonly guess: string;
9
+ }
10
+ /** Human-readable names for docs, galleries and playgrounds. */
11
+ export declare const TYPE_INFO: Readonly<Record<ChallengeType, TypeInfo>>;
12
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7F,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,gEAAgE;AAChE,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,CAuC/D,CAAC"}
@@ -0,0 +1,42 @@
1
+ /** Human-readable names for docs, galleries and playgrounds. */
2
+ export const TYPE_INFO = {
3
+ slider: { family: 'drag', en: 'Slider jigsaw', ar: 'لغز السحب', guess: '≈ 8%' },
4
+ 'slide-unlock': { family: 'drag', en: 'Slide to the target', ar: 'اسحب إلى الهدف', guess: '≈ 7%' },
5
+ 'shadow-drag': { family: 'drag', en: 'Drag to the shadow', ar: 'اسحب إلى الظل', guess: '< 2%' },
6
+ jigsaw: { family: 'drag', en: 'Jigsaw (4 pieces)', ar: 'أحجية من 4 قطع', guess: '1 in 23' },
7
+ rotate: { family: 'drag', en: 'Rotate the scene', ar: 'تدوير المشهد', guess: '≈ 7%' },
8
+ 'rotate-object': { family: 'drag', en: 'Rotate the object', ar: 'تدوير الشيء', guess: '≈ 7%' },
9
+ 'rotate-match': { family: 'drag', en: 'Align the inner circle', ar: 'محاذاة الدائرة الداخلية', guess: '≈ 7%' },
10
+ hold: { family: 'drag', en: 'Hold and release', ar: 'اضغط ثم أفلت', guess: '≈ 1 in 4' },
11
+ icons: { family: 'select', en: 'Select matching shapes', ar: 'اختيار الأشكال المطابقة', guess: '< 1%' },
12
+ 'icons-weather': { family: 'select', en: 'Select weather icons', ar: 'اختيار رموز الطقس', guess: '< 1%' },
13
+ 'icons-objects': { family: 'select', en: 'Select everyday objects', ar: 'اختيار الأشياء', guess: '< 1%' },
14
+ 'odd-one-out': { family: 'select', en: 'Odd one out', ar: 'المختلف', guess: '1 in 9' },
15
+ 'odd-rotation': { family: 'select', en: 'Find the mirrored letter', ar: 'الحرف المعكوس', guess: '1 in 6' },
16
+ 'color-name': { family: 'select', en: 'Colour by name + pattern', ar: 'اللون بالاسم والنقش', guess: '< 1%' },
17
+ biggest: { family: 'select', en: 'Largest / smallest', ar: 'الأكبر / الأصغر', guess: '1 in 9' },
18
+ 'find-shape': { family: 'select', en: 'Find the shape', ar: 'ابحث عن الشكل', guess: '< 1%' },
19
+ 'shadow-pick': { family: 'select', en: 'Pick the shadow', ar: 'اختر الظل', guess: '1 in 6' },
20
+ pattern: { family: 'select', en: 'What comes next', ar: 'ما التالي', guess: '1 in 6' },
21
+ clock: { family: 'select', en: 'Read the clock', ar: 'قراءة الساعة', guess: '1 in 6' },
22
+ 'math-compare': { family: 'select', en: 'Compare sums', ar: 'مقارنة النواتج', guess: '1 in 62' },
23
+ symmetry: { family: 'select', en: 'Complete the mirror', ar: 'أكمل التماثل', guess: '< 0.1%' },
24
+ pairs: { family: 'select', en: 'Match pairs', ar: 'مطابقة الأزواج', guess: '1 in 105' },
25
+ 'tap-order': { family: 'tap', en: 'Tap numbers in order', ar: 'انقر الأرقام بالترتيب', guess: '< 0.1%' },
26
+ spell: { family: 'tap', en: 'Spell the word', ar: 'تهجئة الكلمة', guess: '< 0.1%' },
27
+ order: { family: 'order', en: 'Order dots / bars', ar: 'ترتيب النقاط والأعمدة', guess: '1 in 24' },
28
+ 'order-size': { family: 'order', en: 'Order by size', ar: 'الترتيب حسب الحجم', guess: '1 in 24' },
29
+ 'order-numbers': { family: 'order', en: 'Order numbers', ar: 'ترتيب الأعداد', guess: '1 in 24' },
30
+ 'order-time': { family: 'order', en: 'Order clock times', ar: 'ترتيب الأوقات', guess: '1 in 24' },
31
+ story: { family: 'order', en: 'Picture story', ar: 'قصة مصورة', guess: '1 in 6–24' },
32
+ trace: { family: 'draw', en: 'Connect the dots', ar: 'توصيل النقاط', guess: '< 0.1%' },
33
+ 'trace-letters': { family: 'draw', en: 'Connect A–E', ar: 'توصيل الحروف', guess: '< 0.1%' },
34
+ maze: { family: 'draw', en: 'Maze', ar: 'متاهة', guess: '< 0.1%' },
35
+ math: { family: 'type', en: 'Math image', ar: 'عملية حسابية', guess: '≈ 10%' },
36
+ count: { family: 'type', en: 'Count the objects', ar: 'عدّ الأشياء', guess: '≈ 1 in 5' },
37
+ dice: { family: 'type', en: 'Dice total', ar: 'مجموع النرد', guess: '≈ 1 in 6' },
38
+ pow: { family: 'invisible', en: 'Invisible proof-of-work', ar: 'إثبات عمل غير مرئي', guess: 'n/a (CPU cost)' },
39
+ accessible: { family: 'invisible', en: 'Accessible text question', ar: 'سؤال نصي ميسّر', guess: 'machine-readable' },
40
+ audio: { family: 'invisible', en: 'Audio challenge (digits / beeps)', ar: 'تحدٍّ صوتي (أرقام / نغمات)', guess: '1 in 10,000 (digits) · ≈ 1 in 6 (beeps)' },
41
+ };
42
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /** HKDF salt shared by every Nawa Captcha implementation (Node, .NET). Changing it breaks interop. */
2
+ export declare const HKDF_SALT = "nawa-guard/v1";
3
+ export type KeyPurpose = 'challenge-token' | 'pass-token' | 'client-binding';
4
+ export interface SecretEntry {
5
+ /** Key id embedded in every token so secrets can be rotated. 1-16 chars of [A-Za-z0-9_-]. */
6
+ readonly kid: string;
7
+ /** High-entropy secret, at least 32 characters. Never ship it to a browser. */
8
+ readonly secret: string;
9
+ }
10
+ export interface Keyring {
11
+ /** The kid used for new tokens (first entry). */
12
+ readonly activeKid: string;
13
+ key(kid: string, purpose: KeyPurpose): Uint8Array | undefined;
14
+ }
15
+ export declare const MIN_SECRET_LENGTH = 32;
16
+ export declare const utf8: (text: string) => Uint8Array;
17
+ export declare function fromUtf8(bytes: Uint8Array): string | undefined;
18
+ export declare function deriveKey(secret: string, purpose: KeyPurpose): Uint8Array;
19
+ /**
20
+ * Builds a keyring from one or more secrets. The first entry signs/seals new tokens; all entries
21
+ * are accepted when opening/verifying, which is what makes zero-downtime rotation possible.
22
+ */
23
+ export declare function createKeyring(secrets: string | readonly SecretEntry[]): Keyring;
24
+ export declare function isValidKid(kid: string): boolean;
25
+ export declare function b64url(buf: Uint8Array): string;
26
+ export declare function fromB64url(text: string): Uint8Array | undefined;
27
+ export declare function randomId(bytes?: number): string;
28
+ export declare function hmac(key: Uint8Array, data: string): Uint8Array;
29
+ export declare function sha256(data: string | Uint8Array): Uint8Array;
30
+ /** Constant-time equality that is also safe for inputs of different lengths. */
31
+ export declare function safeEqual(a: Uint8Array | string, b: Uint8Array | string): boolean;
32
+ /** AES-256-GCM seal: returns iv(12) and ciphertext||tag(16). */
33
+ export declare function seal(key: Uint8Array, plaintext: string, aad: string): {
34
+ iv: Uint8Array;
35
+ data: Uint8Array;
36
+ };
37
+ export declare function open(key: Uint8Array, iv: Uint8Array, data: Uint8Array, aad: string): string | undefined;
38
+ /** Uniform integer in [min, maxExclusive); fractional bounds are rounded inward. */
39
+ export declare const randInt: (min: number, maxExclusive: number) => number;
40
+ export declare const randFloat: (min?: number, max?: number) => number;
41
+ export declare const pick: <T>(items: readonly T[]) => T;
42
+ export declare const shuffle: <T>(items: T[]) => T[];
43
+ export declare const sample: <T>(items: readonly T[], count: number) => T[];
44
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAGA,sGAAsG;AACtG,eAAO,MAAM,SAAS,kBAAkB,CAAC;AAEzC,MAAM,MAAM,UAAU,GAAG,iBAAiB,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAE7E,MAAM,WAAW,WAAW;IAC1B,6FAA6F;IAC7F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,OAAO;IACtB,iDAAiD;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;CAC/D;AAGD,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAKpC,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,KAAG,UAAkC,CAAC;AACvE,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,UAAU,CAEzE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,WAAW,EAAE,GAAG,OAAO,CAyB/E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAE9C;AAID,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAQ/D;AAED,wBAAgB,QAAQ,CAAC,KAAK,SAAK,GAAG,MAAM,CAE3C;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,CAE9D;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,CAE5D;AAED,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,EAAE,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CASjF;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAG1G;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIvG;AAID,oFAAoF;AACpF,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,EAAE,cAAc,MAAM,KAAG,MAA0C,CAAC;AACvG,eAAO,MAAM,SAAS,GAAI,YAAO,EAAE,YAAO,KAAG,MAAmC,CAAC;AACjF,eAAO,MAAM,IAAI,GAAI,CAAC,EAAE,OAAO,SAAS,CAAC,EAAE,KAAG,CAA0B,CAAC;AACzE,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,OAAO,CAAC,EAAE,KAAG,CAAC,EAA8B,CAAC;AACxE,eAAO,MAAM,MAAM,GAAI,CAAC,EAAE,OAAO,SAAS,CAAC,EAAE,EAAE,OAAO,MAAM,KAAG,CAAC,EAAoC,CAAC"}
package/dist/crypto.js ADDED
@@ -0,0 +1,108 @@
1
+ import { aesGcmDecrypt, aesGcmEncrypt, fromBase64, hkdfSha256, hmacSha256, randomBytes, sha256 as sha256Bytes, timingSafeEqual } from './primitives.js';
2
+ import { bytesToB64url, cryptoRng } from './rng.js';
3
+ /** HKDF salt shared by every Nawa Captcha implementation (Node, .NET). Changing it breaks interop. */
4
+ export const HKDF_SALT = 'nawa-guard/v1';
5
+ const KID_RE = /^[A-Za-z0-9_-]{1,16}$/;
6
+ export const MIN_SECRET_LENGTH = 32;
7
+ const encoder = new TextEncoder();
8
+ const decoder = new TextDecoder('utf-8', { fatal: true });
9
+ export const utf8 = (text) => encoder.encode(text);
10
+ export function fromUtf8(bytes) {
11
+ try {
12
+ return decoder.decode(bytes);
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ export function deriveKey(secret, purpose) {
19
+ return hkdfSha256(utf8(secret), utf8(HKDF_SALT), utf8(purpose), 32);
20
+ }
21
+ /**
22
+ * Builds a keyring from one or more secrets. The first entry signs/seals new tokens; all entries
23
+ * are accepted when opening/verifying, which is what makes zero-downtime rotation possible.
24
+ */
25
+ export function createKeyring(secrets) {
26
+ const list = typeof secrets === 'string' ? [{ kid: 'k1', secret: secrets }] : secrets;
27
+ if (list.length === 0)
28
+ throw new Error('nawa-captcha: at least one secret is required');
29
+ const keys = new Map();
30
+ for (const { kid, secret } of list) {
31
+ if (!KID_RE.test(kid))
32
+ throw new Error(`nawa-captcha: invalid kid "${kid}" (1-16 chars of A-Z a-z 0-9 _ -)`);
33
+ if (typeof secret !== 'string' || secret.length < MIN_SECRET_LENGTH) {
34
+ throw new Error(`nawa-captcha: secret for kid "${kid}" must be at least ${MIN_SECRET_LENGTH} characters`);
35
+ }
36
+ if (keys.has(kid))
37
+ throw new Error(`nawa-captcha: duplicate kid "${kid}"`);
38
+ keys.set(kid, new Map([
39
+ ['challenge-token', deriveKey(secret, 'challenge-token')],
40
+ ['pass-token', deriveKey(secret, 'pass-token')],
41
+ ['client-binding', deriveKey(secret, 'client-binding')],
42
+ ]));
43
+ }
44
+ const first = list[0];
45
+ if (!first)
46
+ throw new Error('nawa-captcha: at least one secret is required');
47
+ return {
48
+ activeKid: first.kid,
49
+ key: (kid, purpose) => keys.get(kid)?.get(purpose),
50
+ };
51
+ }
52
+ export function isValidKid(kid) {
53
+ return KID_RE.test(kid);
54
+ }
55
+ export function b64url(buf) {
56
+ return bytesToB64url(buf);
57
+ }
58
+ const B64URL_RE = /^[A-Za-z0-9_-]*$/;
59
+ export function fromB64url(text) {
60
+ if (!B64URL_RE.test(text) || text.length % 4 === 1)
61
+ return undefined;
62
+ const b64 = text.replace(/-/g, '+').replace(/_/g, '/');
63
+ try {
64
+ return fromBase64(b64 + '='.repeat((4 - (b64.length % 4)) % 4));
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ export function randomId(bytes = 16) {
71
+ return b64url(randomBytes(bytes));
72
+ }
73
+ export function hmac(key, data) {
74
+ return hmacSha256(key, utf8(data));
75
+ }
76
+ export function sha256(data) {
77
+ return sha256Bytes(typeof data === 'string' ? utf8(data) : data);
78
+ }
79
+ /** Constant-time equality that is also safe for inputs of different lengths. */
80
+ export function safeEqual(a, b) {
81
+ const left = typeof a === 'string' ? utf8(a) : a;
82
+ const right = typeof b === 'string' ? utf8(b) : b;
83
+ if (left.length !== right.length) {
84
+ // Still burn a comparison so the timing does not reveal the length mismatch early.
85
+ timingSafeEqual(left, left);
86
+ return false;
87
+ }
88
+ return timingSafeEqual(left, right);
89
+ }
90
+ /** AES-256-GCM seal: returns iv(12) and ciphertext||tag(16). */
91
+ export function seal(key, plaintext, aad) {
92
+ const iv = randomBytes(12);
93
+ return { iv, data: aesGcmEncrypt(key, iv, utf8(plaintext), utf8(aad)) };
94
+ }
95
+ export function open(key, iv, data, aad) {
96
+ if (iv.length !== 12 || data.length < 17)
97
+ return undefined;
98
+ const plain = aesGcmDecrypt(key, iv, data, utf8(aad));
99
+ return plain ? fromUtf8(plain) : undefined;
100
+ }
101
+ // ---------------------------------------------------------------- randomness helpers (CSPRNG only)
102
+ /** Uniform integer in [min, maxExclusive); fractional bounds are rounded inward. */
103
+ export const randInt = (min, maxExclusive) => cryptoRng.int(min, maxExclusive);
104
+ export const randFloat = (min = 0, max = 1) => cryptoRng.float(min, max);
105
+ export const pick = (items) => cryptoRng.pick(items);
106
+ export const shuffle = (items) => cryptoRng.shuffle(items);
107
+ export const sample = (items, count) => cryptoRng.sample(items, count);
108
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,WAAW,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACxJ,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEpD,sGAAsG;AACtG,MAAM,CAAC,MAAM,SAAS,GAAG,eAAe,CAAC;AAiBzC,MAAM,MAAM,GAAG,uBAAuB,CAAC;AACvC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAEpC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAClC,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAE1D,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAc,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvE,MAAM,UAAU,QAAQ,CAAC,KAAiB;IACxC,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,OAAmB;IAC3D,OAAO,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAwC;IACpE,MAAM,IAAI,GAA2B,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAC9G,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACxF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAuC,CAAC;IAC5D,KAAK,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,mCAAmC,CAAC,CAAC;QAC7G,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,GAAG,iBAAiB,EAAE,CAAC;YACpE,MAAM,IAAI,KAAK,CAAC,iCAAiC,GAAG,sBAAsB,iBAAiB,aAAa,CAAC,CAAC;QAC5G,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,GAAG,CAAC,CAAC;QAC3E,IAAI,CAAC,GAAG,CACN,GAAG,EACH,IAAI,GAAG,CAAyB;YAC9B,CAAC,iBAAiB,EAAE,SAAS,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;YACzD,CAAC,YAAY,EAAE,SAAS,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;YAC/C,CAAC,gBAAgB,EAAE,SAAS,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;SACxD,CAAC,CACH,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAC7E,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,GAAG;QACpB,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;KACnD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,GAAe;IACpC,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACrE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAK,GAAG,EAAE;IACjC,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAe,EAAE,IAAY;IAChD,OAAO,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAyB;IAC9C,OAAO,WAAW,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,SAAS,CAAC,CAAsB,EAAE,CAAsB;IACtE,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,IAAI,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,mFAAmF;QACnF,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,IAAI,CAAC,GAAe,EAAE,SAAiB,EAAE,GAAW;IAClE,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC3B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;AAC1E,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAe,EAAE,EAAc,EAAE,IAAgB,EAAE,GAAW;IACjF,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,SAAS,CAAC;IAC3D,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7C,CAAC;AAED,oGAAoG;AAEpG,oFAAoF;AACpF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,GAAW,EAAE,YAAoB,EAAU,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AACvG,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,EAAU,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AACjF,MAAM,CAAC,MAAM,IAAI,GAAG,CAAI,KAAmB,EAAK,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACzE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAI,KAAU,EAAO,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,MAAM,GAAG,CAAI,KAAmB,EAAE,KAAa,EAAO,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC"}
@@ -0,0 +1,245 @@
1
+ import { type BindMode, type ClientInfo } from './binding.js';
2
+ import { type Keyring, type SecretEntry } from './crypto.js';
3
+ import { type ActionPolicy, type ResolvedPolicy, type SelectionMode } from './selection.js';
4
+ import { type NawaCaptchaStore } from './store.js';
5
+ import { type PassTokenClaims } from './tokens.js';
6
+ import { type ChallengeType, type CheckFailure, type Lang, type ViewKind } from './types.js';
7
+ export type { ActionPolicy, SelectionMode } from './selection.js';
8
+ export type RateLimitHook = (ctx: {
9
+ kind: 'challenge' | 'verify';
10
+ clientKey: string;
11
+ client: ClientInfo;
12
+ }) => boolean | Promise<boolean>;
13
+ export interface RateLimitOptions {
14
+ readonly windowSec: number;
15
+ /** Challenges per client per window. */
16
+ readonly challenge: number;
17
+ /** Verify calls per client per window. */
18
+ readonly verify: number;
19
+ }
20
+ export interface AdaptiveOptions {
21
+ /** How long failures are remembered. */
22
+ readonly windowSec: number;
23
+ /** Failures that raise the difficulty one level. */
24
+ readonly failuresPerLevel: number;
25
+ readonly maxLevel: number;
26
+ }
27
+ export type GuardEvent = {
28
+ type: 'challenge-issued';
29
+ challengeType: ChallengeType;
30
+ level: number;
31
+ clientKey: string;
32
+ action: string;
33
+ step?: number;
34
+ steps?: number;
35
+ } | {
36
+ type: 'step-solved';
37
+ challengeType: ChallengeType;
38
+ level: number;
39
+ clientKey: string;
40
+ action: string;
41
+ step: number;
42
+ steps: number;
43
+ solveMs: number;
44
+ } | {
45
+ type: 'verify-success';
46
+ challengeType: ChallengeType;
47
+ level: number;
48
+ clientKey: string;
49
+ action: string;
50
+ solveMs: number;
51
+ solvedTypes?: readonly ChallengeType[];
52
+ } | {
53
+ type: 'verify-failed';
54
+ challengeType?: ChallengeType;
55
+ reason: VerifyFailure;
56
+ detail?: string;
57
+ clientKey: string;
58
+ } | {
59
+ type: 'rate-limited';
60
+ kind: 'challenge' | 'verify';
61
+ clientKey: string;
62
+ };
63
+ export interface NawaCaptchaOptions {
64
+ /** Server secret (≥32 chars), or a list of `{ kid, secret }` for rotation (first one signs). */
65
+ readonly secret: string | readonly SecretEntry[];
66
+ /** Allowed public site keys. When omitted any well-formed site key is accepted. */
67
+ readonly siteKeys?: readonly string[];
68
+ /** Allowed action names. When omitted any well-formed action is accepted. */
69
+ readonly actions?: readonly string[];
70
+ readonly store?: NawaCaptchaStore;
71
+ readonly challengeTtlSec?: number;
72
+ readonly passTtlSec?: number;
73
+ /** Bind tokens to the client's IP and/or User-Agent. Default `ip+ua`. */
74
+ readonly bind?: BindMode;
75
+ /**
76
+ * Allow-list of challenge types the server may issue (one, two or many). Default: all types for
77
+ * `adaptive` (filtered by the ladder), every visual type for `random`/`sequence`. The browser
78
+ * can only narrow this list, never widen it.
79
+ */
80
+ readonly types?: readonly ChallengeType[];
81
+ /** Relative weights for random picks (default 1 each; 0 disables a type). */
82
+ readonly weights?: Partial<Record<ChallengeType, number>>;
83
+ /** How a type is picked from the allowed ones. Default `adaptive`. */
84
+ readonly mode?: SelectionMode;
85
+ /** Puzzles to solve in a row (each a different type when possible) before a pass token. Default 1, max 3. */
86
+ readonly steps?: number;
87
+ /** Per-action overrides, e.g. `{ login: ['pow', 'slider'], signup: { types: [...], steps: 2 } }`. */
88
+ readonly perAction?: Readonly<Record<string, readonly ChallengeType[] | ActionPolicy>>;
89
+ /** Honour the browser's `types` narrowing hint (`<nawa-captcha types="…">`). Default true. */
90
+ readonly clientNarrowing?: boolean;
91
+ /** Type pools by difficulty level (index = level), for `adaptive`. */
92
+ readonly ladder?: readonly (readonly ChallengeType[])[];
93
+ /** Start every client at an invisible proof-of-work; visible puzzles only after failures/risk. */
94
+ readonly invisibleFirst?: boolean;
95
+ /** Base proof-of-work difficulty in leading zero bits (each level adds 2). Default 16. */
96
+ readonly powBits?: number;
97
+ /** Let the browser pick one exact puzzle type from the allow-list (demos, galleries). */
98
+ readonly allowClientType?: boolean;
99
+ /** The accessible alternatives (text question, audio) can always be requested, even when not in `types`. Default true. */
100
+ readonly allowAccessible?: boolean;
101
+ readonly maxAttempts?: Partial<Record<ChallengeType, number>>;
102
+ readonly minSolveMs?: Partial<Record<ChallengeType, number>>;
103
+ readonly rateLimit?: false | RateLimitOptions | RateLimitHook;
104
+ readonly adaptive?: false | AdaptiveOptions;
105
+ /** Extra levels from your own risk signals (e.g. failed logins for this account). */
106
+ readonly assessRisk?: (ctx: {
107
+ client: ClientInfo;
108
+ action: string;
109
+ clientKey: string;
110
+ }) => number | Promise<number>;
111
+ readonly onEvent?: (event: GuardEvent) => void;
112
+ readonly defaultLang?: Lang;
113
+ readonly now?: () => number;
114
+ }
115
+ export interface ChallengeRequest {
116
+ readonly siteKey?: unknown;
117
+ readonly action?: unknown;
118
+ readonly lang?: unknown;
119
+ /** One exact type (only honoured with `allowClientType`, except `accessible`). */
120
+ readonly type?: unknown;
121
+ /** Narrowing hint: an array (or comma-separated string) of types. Never widens the allow-list. */
122
+ readonly types?: unknown;
123
+ readonly client: ClientInfo;
124
+ }
125
+ export interface ChallengeResponse {
126
+ readonly token: string;
127
+ readonly type: ChallengeType;
128
+ /** The widget view that renders this type. */
129
+ readonly view: ViewKind;
130
+ readonly lang: Lang;
131
+ readonly prompt: string;
132
+ readonly data: unknown;
133
+ readonly level: number;
134
+ /** Multi-step: this step (1-based) and the total. Both 1 for single puzzles. */
135
+ readonly step: number;
136
+ readonly steps: number;
137
+ /** Unix ms. */
138
+ readonly expiresAt: number;
139
+ /** Lifetime in ms (relative, so browsers with a skewed clock still refresh on time). */
140
+ readonly expiresInMs: number;
141
+ }
142
+ export type ChallengeError = 'invalid-input' | 'rate-limited';
143
+ export type VerifyFailure = 'invalid-input' | 'invalid-token' | 'expired' | 'client-mismatch' | 'replayed' | 'attempts-exceeded' | 'too-fast' | 'rate-limited' | CheckFailure;
144
+ /** What the browser is told. Deliberately coarser than {@link VerifyFailure} (no tuning oracle for bots). */
145
+ export type PublicVerifyError = 'invalid-input' | 'invalid-token' | 'expired' | 'failed' | 'rate-limited';
146
+ export type VerifyResult = {
147
+ ok: true;
148
+ passToken: string;
149
+ expiresAt: number;
150
+ expiresInMs: number;
151
+ type: ChallengeType;
152
+ solvedTypes: readonly ChallengeType[];
153
+ next?: undefined;
154
+ }
155
+ /** Multi-step: this step was solved; the next challenge must be solved before a pass token is issued. */
156
+ | {
157
+ ok: true;
158
+ next: ChallengeResponse;
159
+ passToken?: undefined;
160
+ } | {
161
+ ok: false;
162
+ error: PublicVerifyError;
163
+ reason: VerifyFailure;
164
+ detail?: string;
165
+ retry: boolean;
166
+ };
167
+ export interface VerifyRequest {
168
+ readonly token: unknown;
169
+ readonly answer: unknown;
170
+ readonly client: ClientInfo;
171
+ }
172
+ export type PassTokenError = 'missing-token' | 'malformed' | 'unknown-key' | 'bad-signature' | 'expired' | 'not-yet-valid' | 'too-old' | 'action-mismatch' | 'site-mismatch' | 'type-not-allowed' | 'too-few-steps' | 'client-mismatch' | 'replayed';
173
+ export type PassTokenResult = {
174
+ ok: true;
175
+ claims: PassTokenClaims;
176
+ } | {
177
+ ok: false;
178
+ error: PassTokenError;
179
+ };
180
+ export interface VerifyPassTokenOptions {
181
+ /** Same secret(s) as the issuing server; pass a list to accept rotated keys. */
182
+ readonly secret: string | readonly SecretEntry[] | Keyring;
183
+ /** The action the protected route expects (e.g. "login"). Required: tokens are not interchangeable across actions. */
184
+ readonly expectedAction: string | readonly string[];
185
+ readonly expectedSiteKey?: string;
186
+ /** Reject tokens issued longer ago than this, even if not yet expired. */
187
+ readonly maxAgeSec?: number;
188
+ /** Shared store for single-use enforcement. Defaults to a process-wide in-memory store. */
189
+ readonly store?: NawaCaptchaStore;
190
+ /** The request's client info; required when the token is client-bound. */
191
+ readonly client?: ClientInfo;
192
+ /**
193
+ * Restrict which challenge types are acceptable for this route (e.g. exclude 'pow'). For a
194
+ * multi-step token, at least one solved step must be of an allowed type.
195
+ */
196
+ readonly allowedTypes?: readonly ChallengeType[];
197
+ /** Require at least this many solved puzzles (multi-step tokens). Default 1. */
198
+ readonly minSteps?: number;
199
+ readonly clockSkewSec?: number;
200
+ readonly now?: () => number;
201
+ }
202
+ export declare class NawaCaptcha {
203
+ readonly options: NawaCaptchaOptions;
204
+ readonly keys: Keyring;
205
+ readonly store: NawaCaptchaStore;
206
+ private readonly registry;
207
+ private readonly powByBits;
208
+ private readonly base;
209
+ private readonly explicitTypes;
210
+ private readonly policies;
211
+ private readonly bind;
212
+ private readonly now;
213
+ constructor(options: NawaCaptchaOptions);
214
+ /** The effective selection policy for an action (global options merged with `perAction`). */
215
+ policy(action: string): ResolvedPolicy;
216
+ private emit;
217
+ private clientKeyOf;
218
+ private allowed;
219
+ private levelFor;
220
+ private definition;
221
+ private maxAttempts;
222
+ private minSolveMs;
223
+ /** Picks a type from `pool` according to the policy, avoiding `exclude` when possible. */
224
+ private choose;
225
+ private issue;
226
+ createChallenge(req: ChallengeRequest): Promise<{
227
+ ok: true;
228
+ challenge: ChallengeResponse;
229
+ } | {
230
+ ok: false;
231
+ error: ChallengeError;
232
+ }>;
233
+ verify(req: VerifyRequest): Promise<VerifyResult>;
234
+ /** Server-side pass-token check using this instance's secrets and store. */
235
+ verifyPassToken(token: unknown, opts: Omit<VerifyPassTokenOptions, 'secret' | 'store'> & {
236
+ store?: NawaCaptchaStore;
237
+ }): Promise<PassTokenResult>;
238
+ }
239
+ export declare function createNawaCaptcha(options: NawaCaptchaOptions): NawaCaptcha;
240
+ /**
241
+ * Verifies a pass token on your backend (the equivalent of reCAPTCHA siteverify, but local):
242
+ * signature, expiry, max age, action, site key, allowed types, steps, client binding, and single use.
243
+ */
244
+ export declare function verifyPassToken(token: unknown, opts: VerifyPassTokenOptions): Promise<PassTokenResult>;
245
+ //# sourceMappingURL=engine.d.ts.map