tempest-react-sdk 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/dist/audio/sfx-pool.cjs +1 -1
  2. package/dist/audio/sfx-pool.cjs.map +1 -1
  3. package/dist/audio/sfx-pool.js +16 -7
  4. package/dist/audio/sfx-pool.js.map +1 -1
  5. package/dist/audio/use-sfx-pool.cjs +1 -1
  6. package/dist/audio/use-sfx-pool.cjs.map +1 -1
  7. package/dist/audio/use-sfx-pool.js +3 -8
  8. package/dist/audio/use-sfx-pool.js.map +1 -1
  9. package/dist/auth/passkey.cjs +1 -1
  10. package/dist/auth/passkey.cjs.map +1 -1
  11. package/dist/auth/passkey.js +79 -81
  12. package/dist/auth/passkey.js.map +1 -1
  13. package/dist/charts.cjs +1 -1
  14. package/dist/charts.js +4 -4
  15. package/dist/components/BarList/BarList.cjs +1 -1
  16. package/dist/components/BarList/BarList.cjs.map +1 -1
  17. package/dist/components/BarList/BarList.js +31 -24
  18. package/dist/components/BarList/BarList.js.map +1 -1
  19. package/dist/components/BarList/bar-list-model.cjs +1 -1
  20. package/dist/components/BarList/bar-list-model.cjs.map +1 -1
  21. package/dist/components/BarList/bar-list-model.js +5 -5
  22. package/dist/components/BarList/bar-list-model.js.map +1 -1
  23. package/dist/components/ClickOutside/ClickOutside.cjs +1 -1
  24. package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
  25. package/dist/components/ClickOutside/ClickOutside.js +13 -14
  26. package/dist/components/ClickOutside/ClickOutside.js.map +1 -1
  27. package/dist/components/DataTable/DataTable.cjs +1 -1
  28. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  29. package/dist/components/DataTable/DataTable.js +99 -95
  30. package/dist/components/DataTable/DataTable.js.map +1 -1
  31. package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
  32. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
  33. package/dist/components/DataTable/use-dev-warnings.js +5 -3
  34. package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
  35. package/dist/components/FilterBar/filter-apply.cjs +1 -1
  36. package/dist/components/FilterBar/filter-apply.cjs.map +1 -1
  37. package/dist/components/FilterBar/filter-apply.js +35 -38
  38. package/dist/components/FilterBar/filter-apply.js.map +1 -1
  39. package/dist/components/FilterBar/filter-query.cjs +1 -1
  40. package/dist/components/FilterBar/filter-query.cjs.map +1 -1
  41. package/dist/components/FilterBar/filter-query.js +16 -13
  42. package/dist/components/FilterBar/filter-query.js.map +1 -1
  43. package/dist/geo/use-position-tracker.cjs +1 -1
  44. package/dist/geo/use-position-tracker.cjs.map +1 -1
  45. package/dist/geo/use-position-tracker.js +25 -26
  46. package/dist/geo/use-position-tracker.js.map +1 -1
  47. package/dist/hooks/use-async.cjs +1 -1
  48. package/dist/hooks/use-async.cjs.map +1 -1
  49. package/dist/hooks/use-async.js +27 -30
  50. package/dist/hooks/use-async.js.map +1 -1
  51. package/dist/hooks/use-click-outside.cjs +1 -1
  52. package/dist/hooks/use-click-outside.cjs.map +1 -1
  53. package/dist/hooks/use-click-outside.js +8 -9
  54. package/dist/hooks/use-click-outside.js.map +1 -1
  55. package/dist/hooks/use-countdown.cjs +1 -1
  56. package/dist/hooks/use-countdown.cjs.map +1 -1
  57. package/dist/hooks/use-countdown.js +8 -13
  58. package/dist/hooks/use-countdown.js.map +1 -1
  59. package/dist/hooks/use-event-listener.cjs +1 -1
  60. package/dist/hooks/use-event-listener.cjs.map +1 -1
  61. package/dist/hooks/use-event-listener.js +6 -6
  62. package/dist/hooks/use-event-listener.js.map +1 -1
  63. package/dist/hooks/use-interval.cjs +1 -1
  64. package/dist/hooks/use-interval.cjs.map +1 -1
  65. package/dist/hooks/use-interval.js +5 -6
  66. package/dist/hooks/use-interval.js.map +1 -1
  67. package/dist/hooks/use-latest-ref.cjs.map +1 -1
  68. package/dist/hooks/use-latest-ref.js.map +1 -1
  69. package/dist/hooks/use-long-press-handlers.cjs +1 -1
  70. package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
  71. package/dist/hooks/use-long-press-handlers.js +8 -10
  72. package/dist/hooks/use-long-press-handlers.js.map +1 -1
  73. package/dist/hooks/use-long-press.cjs +1 -1
  74. package/dist/hooks/use-long-press.cjs.map +1 -1
  75. package/dist/hooks/use-long-press.js +6 -6
  76. package/dist/hooks/use-long-press.js.map +1 -1
  77. package/dist/hooks/use-online.cjs +1 -1
  78. package/dist/hooks/use-online.cjs.map +1 -1
  79. package/dist/hooks/use-online.js +10 -10
  80. package/dist/hooks/use-online.js.map +1 -1
  81. package/dist/hooks/use-service-worker-update.cjs +1 -1
  82. package/dist/hooks/use-service-worker-update.cjs.map +1 -1
  83. package/dist/hooks/use-service-worker-update.js +24 -25
  84. package/dist/hooks/use-service-worker-update.js.map +1 -1
  85. package/dist/hooks/use-stable-callback.cjs +1 -1
  86. package/dist/hooks/use-stable-callback.cjs.map +1 -1
  87. package/dist/hooks/use-stable-callback.js +4 -3
  88. package/dist/hooks/use-stable-callback.js.map +1 -1
  89. package/dist/hooks/use-timeout.cjs +1 -1
  90. package/dist/hooks/use-timeout.cjs.map +1 -1
  91. package/dist/hooks/use-timeout.js +5 -6
  92. package/dist/hooks/use-timeout.js.map +1 -1
  93. package/dist/hooks/use-typewriter.cjs +1 -1
  94. package/dist/hooks/use-typewriter.cjs.map +1 -1
  95. package/dist/hooks/use-typewriter.js +9 -18
  96. package/dist/hooks/use-typewriter.js.map +1 -1
  97. package/dist/http/api-client.cjs +1 -1
  98. package/dist/http/api-client.cjs.map +1 -1
  99. package/dist/http/api-client.js +11 -16
  100. package/dist/http/api-client.js.map +1 -1
  101. package/dist/http/describe-api-error.cjs +1 -1
  102. package/dist/http/describe-api-error.cjs.map +1 -1
  103. package/dist/http/describe-api-error.js +12 -15
  104. package/dist/http/describe-api-error.js.map +1 -1
  105. package/dist/http/errors.cjs +1 -1
  106. package/dist/http/errors.cjs.map +1 -1
  107. package/dist/http/errors.js +18 -6
  108. package/dist/http/errors.js.map +1 -1
  109. package/dist/http/resumable-upload.cjs +1 -1
  110. package/dist/http/resumable-upload.cjs.map +1 -1
  111. package/dist/http/resumable-upload.js +125 -118
  112. package/dist/http/resumable-upload.js.map +1 -1
  113. package/dist/http/retry.cjs +1 -1
  114. package/dist/http/retry.cjs.map +1 -1
  115. package/dist/http/retry.js +20 -16
  116. package/dist/http/retry.js.map +1 -1
  117. package/dist/http/use-describe-api-error.cjs +1 -1
  118. package/dist/http/use-describe-api-error.cjs.map +1 -1
  119. package/dist/http/use-describe-api-error.js +2 -2
  120. package/dist/http/use-describe-api-error.js.map +1 -1
  121. package/dist/i18n/create-i18n.cjs +1 -1
  122. package/dist/i18n/create-i18n.cjs.map +1 -1
  123. package/dist/i18n/create-i18n.js +6 -6
  124. package/dist/i18n/create-i18n.js.map +1 -1
  125. package/dist/icons/material-symbols.cjs +1 -1
  126. package/dist/icons/material-symbols.cjs.map +1 -1
  127. package/dist/icons/material-symbols.js +136 -5
  128. package/dist/icons/material-symbols.js.map +1 -1
  129. package/dist/icons.d.ts +17 -5
  130. package/dist/offline/create-offline-sync.cjs +1 -1
  131. package/dist/offline/create-offline-sync.cjs.map +1 -1
  132. package/dist/offline/create-offline-sync.js +17 -9
  133. package/dist/offline/create-offline-sync.js.map +1 -1
  134. package/dist/offline/use-offline-sync.cjs +1 -1
  135. package/dist/offline/use-offline-sync.cjs.map +1 -1
  136. package/dist/offline/use-offline-sync.js +7 -8
  137. package/dist/offline/use-offline-sync.js.map +1 -1
  138. package/dist/push/use-push-subscription.cjs +1 -1
  139. package/dist/push/use-push-subscription.cjs.map +1 -1
  140. package/dist/push/use-push-subscription.js +13 -12
  141. package/dist/push/use-push-subscription.js.map +1 -1
  142. package/dist/query/retry-policy.cjs +1 -1
  143. package/dist/query/retry-policy.cjs.map +1 -1
  144. package/dist/query/retry-policy.js +3 -3
  145. package/dist/query/retry-policy.js.map +1 -1
  146. package/dist/sse/use-event-stream.cjs +1 -1
  147. package/dist/sse/use-event-stream.cjs.map +1 -1
  148. package/dist/sse/use-event-stream.js +19 -20
  149. package/dist/sse/use-event-stream.js.map +1 -1
  150. package/dist/tabular/use-tabular-predictor.cjs +1 -1
  151. package/dist/tabular/use-tabular-predictor.cjs.map +1 -1
  152. package/dist/tabular/use-tabular-predictor.js +20 -17
  153. package/dist/tabular/use-tabular-predictor.js.map +1 -1
  154. package/dist/tempest-react-sdk.cjs +1 -1
  155. package/dist/tempest-react-sdk.d.ts +381 -143
  156. package/dist/tempest-react-sdk.js +143 -143
  157. package/dist/theme/ThemeProvider.cjs +1 -1
  158. package/dist/theme/ThemeProvider.cjs.map +1 -1
  159. package/dist/theme/ThemeProvider.js +12 -17
  160. package/dist/theme/ThemeProvider.js.map +1 -1
  161. package/dist/utils/base64.cjs +2 -0
  162. package/dist/utils/base64.cjs.map +1 -0
  163. package/dist/utils/base64.js +16 -0
  164. package/dist/utils/base64.js.map +1 -0
  165. package/dist/utils/compare-values.cjs +1 -1
  166. package/dist/utils/compare-values.cjs.map +1 -1
  167. package/dist/utils/compare-values.js +4 -3
  168. package/dist/utils/compare-values.js.map +1 -1
  169. package/dist/utils/compressed-storage.cjs +1 -1
  170. package/dist/utils/compressed-storage.cjs.map +1 -1
  171. package/dist/utils/compressed-storage.js +16 -46
  172. package/dist/utils/compressed-storage.js.map +1 -1
  173. package/dist/utils/storage.cjs +1 -1
  174. package/dist/utils/storage.cjs.map +1 -1
  175. package/dist/utils/storage.js +37 -17
  176. package/dist/utils/storage.js.map +1 -1
  177. package/dist/vision/use-camera-stream.cjs +1 -1
  178. package/dist/vision/use-camera-stream.cjs.map +1 -1
  179. package/dist/vision/use-camera-stream.js +34 -33
  180. package/dist/vision/use-camera-stream.js.map +1 -1
  181. package/dist/vite/base-url.cjs +2 -0
  182. package/dist/vite/base-url.cjs.map +1 -0
  183. package/dist/vite/base-url.js +8 -0
  184. package/dist/vite/base-url.js.map +1 -0
  185. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  186. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  187. package/dist/vite/tempest-pwa-dev-sw.js +54 -29
  188. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  189. package/dist/vite/tempest-pwa-manifest.cjs +2 -2
  190. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  191. package/dist/vite/tempest-pwa-manifest.js +15 -14
  192. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  193. package/dist/vite.d.ts +2 -2
  194. package/dist/ws/use-web-socket.cjs +1 -1
  195. package/dist/ws/use-web-socket.cjs.map +1 -1
  196. package/dist/ws/use-web-socket.js +40 -42
  197. package/dist/ws/use-web-socket.js.map +1 -1
  198. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- function e(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):1}function t(t={}){let{baseUrl:n=``,voices:r=1,maxSources:i=48}=t,a=e(t.volume??1),o=Math.max(1,Math.floor(r)),s=new Map,c=new WeakMap;function l(e){return!n||/^(https?:)?\/\//.test(e)||e.startsWith(`data:`)?e:`${n.replace(/\/$/,``)}/${e.replace(/^\//,``)}`}function u(e){for(let t of e.elements)t.pause(),t.removeAttribute(`src`),t.load()}function d(e){if(typeof Audio>`u`)return null;let t=l(e),n=s.get(t);if(n)return s.delete(t),s.set(t,n),n;let r={elements:Array.from({length:o},()=>{let e=new Audio(t);return e.preload=`auto`,e}),next:0};if(s.set(t,r),s.size>i){let e=s.keys().next();if(!e.done&&e.value!==t){let t=s.get(e.value);t&&u(t),s.delete(e.value)}}return r}return{play(t,n={}){let r=d(t);if(!r)return;let i=r.elements[r.next];r.next=(r.next+1)%r.elements.length;let o=e(n.volume??1);c.set(i,o),i.pause(),i.currentTime=0,i.volume=e(o*a),i.play().catch(()=>{})},preload(e){for(let t of Array.isArray(e)?e:[e])d(t)?.elements.forEach(e=>e.load())},setVolume(t){a=e(t);for(let t of s.values())for(let n of t.elements)n.paused||(n.volume=e((c.get(n)??1)*a))},stop(e){let t=e?[s.get(l(e))]:[...s.values()];for(let e of t)if(e)for(let t of e.elements)t.pause(),t.currentTime=0},dispose(){for(let e of s.values())u(e);s.clear()}}}exports.createSfxPool=t;
1
+ function e(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):1}function t(t={}){let{baseUrl:n=``,voices:r=1,maxSources:i=48}=t,a=e(t.volume??1),o=Math.max(1,Math.floor(r)),s=new Map,c=new WeakMap;function l(e){return!n||/^(https?:)?\/\//.test(e)||e.startsWith(`data:`)?e:`${n.replace(/\/$/,``)}/${e.replace(/^\//,``)}`}function u(e){for(let t of e.elements)t.pause(),t.removeAttribute(`src`),t.load()}function d(e){if(typeof Audio>`u`)return null;let t=l(e),n=s.get(t);if(n)return s.delete(t),s.set(t,n),{entry:n,created:!1};let r={elements:Array.from({length:o},()=>{let e=new Audio(t);return e.preload=`auto`,e}),next:0};if(s.set(t,r),s.size>i){let e=s.keys().next();if(!e.done&&e.value!==t){let t=s.get(e.value);t&&u(t),s.delete(e.value)}}return{entry:r,created:!0}}return{play(t,n={}){let r=d(t);if(!r)return;let i=r.entry,o=i.elements[i.next];i.next=(i.next+1)%i.elements.length;let s=e(n.volume??1);c.set(o,s),o.pause(),o.currentTime=0,o.volume=e(s*a),o.play().catch(()=>{})},preload(e){for(let t of Array.isArray(e)?e:[e]){let e=d(t);if(e?.created)for(let t of e.entry.elements)t.load()}},setVolume(t){a=e(t);for(let t of s.values())for(let n of t.elements)n.paused||(n.volume=e((c.get(n)??1)*a))},stop(e){let t=e?[s.get(l(e))]:[...s.values()];for(let e of t)if(e)for(let t of e.elements)t.pause(),t.currentTime=0},dispose(){for(let e of s.values())u(e);s.clear()}}}exports.createSfxPool=t;
2
2
  //# sourceMappingURL=sfx-pool.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sfx-pool.cjs","names":[],"sources":["../../src/audio/sfx-pool.ts"],"sourcesContent":["/** Options for {@link createSfxPool}. */\nexport interface SfxPoolOptions {\n /**\n * Master volume, `0`–`1`, multiplied into every per-play volume. Default\n * `1`. Wire it to the app's sound setting so one value governs the lot.\n */\n volume?: number;\n /**\n * Prefix applied to sources that are not absolute URLs — typically Vite's\n * `import.meta.env.BASE_URL`, so a build served from a subpath resolves.\n * Default `\"\"`.\n */\n baseUrl?: string;\n /**\n * Elements kept per source. `1` (the default) restarts the clip on every\n * play, which is what a menu blip wants. Raise it to let a sound overlap\n * itself — a hit landing while the previous one is still ringing.\n */\n voices?: number;\n /**\n * Maximum number of distinct sources held. When exceeded, the least\n * recently played source is released. Default `48`.\n */\n maxSources?: number;\n}\n\n/** Per-play overrides. */\nexport interface PlaySfxOptions {\n /** Volume for this play, `0`–`1`, multiplied by the pool's master. Default `1`. */\n volume?: number;\n}\n\n/** Imperative handle over a pool of short sound effects. */\nexport interface SfxPool {\n /** Play a clip, allocating and caching its element on first use. */\n play: (src: string, options?: PlaySfxOptions) => void;\n /** Fetch clips ahead of the first play, so it is not silent while the file downloads. */\n preload: (src: string | string[]) => void;\n /** Set the master volume, applying it to anything already sounding. */\n setVolume: (volume: number) => void;\n /** Stop one source, or every source when called with no argument. */\n stop: (src?: string) => void;\n /** Release every element. Call on unmount. */\n dispose: () => void;\n}\n\n/** Clamp to the range `HTMLMediaElement.volume` accepts, rejecting `NaN`. */\nfunction clampVolume(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * A pool of preallocated `<audio>` elements for short sound effects.\n *\n * `new Audio(src)` on every play allocates an element and re-enters the network\n * stack for a file the browser already has, which is the wrong shape for a\n * sound that fires dozens of times a minute — a UI blip, a hit, a pickup. The\n * pool allocates once per source and replays.\n *\n * This is deliberately not {@link createAudioPlayer}: that handle tracks a\n * single \"current\" clip with loop, sink routing and lifecycle callbacks, which\n * is what background music needs. Effects are the opposite case — many\n * sources, all short, fire-and-forget, and the only thing that matters is that\n * firing one is cheap.\n *\n * A blocked `play()` is swallowed. Browsers reject playback until the user has\n * interacted with the page, and a sound effect is by definition not worth\n * interrupting anything over; call {@link SfxPool.preload} after the first\n * interaction if you want the pool warm.\n *\n * @param options - Master volume, base URL, voices per source and pool size.\n * @returns The pool handle.\n *\n * @example\n * const sfx = createSfxPool({ volume: 0.6, baseUrl: import.meta.env.BASE_URL });\n * sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function createSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { baseUrl = \"\", voices = 1, maxSources = 48 } = options;\n\n let master = clampVolume(options.volume ?? 1);\n const voiceCount = Math.max(1, Math.floor(voices));\n\n /**\n * Insertion-ordered by last play, so evicting the first key drops the\n * least recently used source.\n */\n const sources = new Map<string, { elements: HTMLAudioElement[]; next: number }>();\n\n /**\n * Per-play gain of whatever each element last played at.\n *\n * `setVolume` has to rescale live playback by the same factor it was\n * started with; assigning the master directly would yank a clip that\n * started at half volume up to full.\n */\n const gains = new WeakMap<HTMLAudioElement, number>();\n\n function resolve(src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n }\n\n function release(entry: { elements: HTMLAudioElement[] }): void {\n for (const element of entry.elements) {\n element.pause();\n element.removeAttribute(\"src\");\n element.load();\n }\n }\n\n function acquire(src: string): { elements: HTMLAudioElement[]; next: number } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n // Re-insert so the map order stays \"least recently played first\".\n sources.delete(url);\n sources.set(url, existing);\n return existing;\n }\n\n const entry = {\n elements: Array.from({ length: voiceCount }, () => {\n const element = new Audio(url);\n element.preload = \"auto\";\n return element;\n }),\n next: 0,\n };\n sources.set(url, entry);\n\n if (sources.size > maxSources) {\n const oldest = sources.keys().next();\n if (!oldest.done && oldest.value !== url) {\n const evicted = sources.get(oldest.value);\n if (evicted) release(evicted);\n sources.delete(oldest.value);\n }\n }\n\n return entry;\n }\n\n return {\n play(src, playOptions = {}) {\n const entry = acquire(src);\n if (!entry) return;\n\n const element = entry.elements[entry.next];\n entry.next = (entry.next + 1) % entry.elements.length;\n\n const gain = clampVolume(playOptions.volume ?? 1);\n gains.set(element, gain);\n\n element.pause();\n element.currentTime = 0;\n element.volume = clampVolume(gain * master);\n void element.play().catch(() => {});\n },\n\n preload(src) {\n for (const one of Array.isArray(src) ? src : [src]) {\n acquire(one)?.elements.forEach((element) => element.load());\n }\n },\n\n setVolume(volume) {\n master = clampVolume(volume);\n for (const entry of sources.values()) {\n for (const element of entry.elements) {\n if (element.paused) continue;\n element.volume = clampVolume((gains.get(element) ?? 1) * master);\n }\n }\n },\n\n stop(src) {\n const targets = src ? [sources.get(resolve(src))] : [...sources.values()];\n for (const entry of targets) {\n if (!entry) continue;\n for (const element of entry.elements) {\n element.pause();\n element.currentTime = 0;\n }\n }\n },\n\n dispose() {\n for (const entry of sources.values()) release(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"AA+CA,SAAS,EAAY,EAAuB,CAExC,OADK,OAAO,SAAS,CAAK,EACnB,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAK,CAAC,EADD,CAExC,CA8BA,SAAgB,EAAc,EAA0B,CAAC,EAAY,CACjE,GAAM,CAAE,UAAU,GAAI,SAAS,EAAG,aAAa,IAAO,EAElD,EAAS,EAAY,EAAQ,QAAU,CAAC,EACtC,EAAa,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,EAM3C,EAAU,IAAI,IASd,EAAQ,IAAI,QAElB,SAAS,EAAQ,EAAqB,CAElC,MADI,CAAC,GAAW,kBAAkB,KAAK,CAAG,GAAK,EAAI,WAAW,OAAO,EAAU,EACxE,GAAG,EAAQ,QAAQ,MAAO,EAAE,EAAE,GAAG,EAAI,QAAQ,MAAO,EAAE,GACjE,CAEA,SAAS,EAAQ,EAA+C,CAC5D,IAAK,IAAM,KAAW,EAAM,SACxB,EAAQ,MAAM,EACd,EAAQ,gBAAgB,KAAK,EAC7B,EAAQ,KAAK,CAErB,CAEA,SAAS,EAAQ,EAAoE,CACjF,GAAI,OAAO,MAAU,IAAa,OAAO,KAEzC,IAAM,EAAM,EAAQ,CAAG,EACjB,EAAW,EAAQ,IAAI,CAAG,EAChC,GAAI,EAIA,OAFA,EAAQ,OAAO,CAAG,EAClB,EAAQ,IAAI,EAAK,CAAQ,EAClB,EAGX,IAAM,EAAQ,CACV,SAAU,MAAM,KAAK,CAAE,OAAQ,CAAW,MAAS,CAC/C,IAAM,EAAU,IAAI,MAAM,CAAG,EAE7B,MADA,GAAQ,QAAU,OACX,CACX,CAAC,EACD,KAAM,CACV,EAGA,GAFA,EAAQ,IAAI,EAAK,CAAK,EAElB,EAAQ,KAAO,EAAY,CAC3B,IAAM,EAAS,EAAQ,KAAK,CAAC,CAAC,KAAK,EACnC,GAAI,CAAC,EAAO,MAAQ,EAAO,QAAU,EAAK,CACtC,IAAM,EAAU,EAAQ,IAAI,EAAO,KAAK,EACpC,GAAS,EAAQ,CAAO,EAC5B,EAAQ,OAAO,EAAO,KAAK,CAC/B,CACJ,CAEA,OAAO,CACX,CAEA,MAAO,CACH,KAAK,EAAK,EAAc,CAAC,EAAG,CACxB,IAAM,EAAQ,EAAQ,CAAG,EACzB,GAAI,CAAC,EAAO,OAEZ,IAAM,EAAU,EAAM,SAAS,EAAM,MACrC,EAAM,MAAQ,EAAM,KAAO,GAAK,EAAM,SAAS,OAE/C,IAAM,EAAO,EAAY,EAAY,QAAU,CAAC,EAChD,EAAM,IAAI,EAAS,CAAI,EAEvB,EAAQ,MAAM,EACd,EAAQ,YAAc,EACtB,EAAQ,OAAS,EAAY,EAAO,CAAM,EAC1C,EAAa,KAAK,CAAC,CAAC,UAAY,CAAC,CAAC,CACtC,EAEA,QAAQ,EAAK,CACT,IAAK,IAAM,KAAO,MAAM,QAAQ,CAAG,EAAI,EAAM,CAAC,CAAG,EAC7C,EAAQ,CAAG,CAAC,EAAE,SAAS,QAAS,GAAY,EAAQ,KAAK,CAAC,CAElE,EAEA,UAAU,EAAQ,CACd,EAAS,EAAY,CAAM,EAC3B,IAAK,IAAM,KAAS,EAAQ,OAAO,EAC/B,IAAK,IAAM,KAAW,EAAM,SACpB,EAAQ,SACZ,EAAQ,OAAS,GAAa,EAAM,IAAI,CAAO,GAAK,GAAK,CAAM,EAG3E,EAEA,KAAK,EAAK,CACN,IAAM,EAAU,EAAM,CAAC,EAAQ,IAAI,EAAQ,CAAG,CAAC,CAAC,EAAI,CAAC,GAAG,EAAQ,OAAO,CAAC,EACxE,IAAK,IAAM,KAAS,EACX,KACL,IAAK,IAAM,KAAW,EAAM,SACxB,EAAQ,MAAM,EACd,EAAQ,YAAc,CAGlC,EAEA,SAAU,CACN,IAAK,IAAM,KAAS,EAAQ,OAAO,EAAG,EAAQ,CAAK,EACnD,EAAQ,MAAM,CAClB,CACJ,CACJ"}
1
+ {"version":3,"file":"sfx-pool.cjs","names":[],"sources":["../../src/audio/sfx-pool.ts"],"sourcesContent":["/** Options for {@link createSfxPool}. */\nexport interface SfxPoolOptions {\n /**\n * Master volume, `0`–`1`, multiplied into every per-play volume. Default\n * `1`. Wire it to the app's sound setting so one value governs the lot.\n */\n volume?: number;\n /**\n * Prefix applied to sources that are not absolute URLs — typically Vite's\n * `import.meta.env.BASE_URL`, so a build served from a subpath resolves.\n * Default `\"\"`.\n */\n baseUrl?: string;\n /**\n * Elements kept per source. `1` (the default) restarts the clip on every\n * play, which is what a menu blip wants. Raise it to let a sound overlap\n * itself — a hit landing while the previous one is still ringing.\n */\n voices?: number;\n /**\n * Maximum number of distinct sources held. When exceeded, the least\n * recently played source is released. Default `48`.\n */\n maxSources?: number;\n}\n\n/** Per-play overrides. */\nexport interface PlaySfxOptions {\n /** Volume for this play, `0`–`1`, multiplied by the pool's master. Default `1`. */\n volume?: number;\n}\n\n/** Imperative handle over a pool of short sound effects. */\nexport interface SfxPool {\n /** Play a clip, allocating and caching its element on first use. */\n play: (src: string, options?: PlaySfxOptions) => void;\n /** Fetch clips ahead of the first play, so it is not silent while the file downloads. */\n preload: (src: string | string[]) => void;\n /** Set the master volume, applying it to anything already sounding. */\n setVolume: (volume: number) => void;\n /** Stop one source, or every source when called with no argument. */\n stop: (src?: string) => void;\n /** Release every element. Call on unmount. */\n dispose: () => void;\n}\n\n/** Clamp to the range `HTMLMediaElement.volume` accepts, rejecting `NaN`. */\nfunction clampVolume(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * A pool of preallocated `<audio>` elements for short sound effects.\n *\n * `new Audio(src)` on every play allocates an element and re-enters the network\n * stack for a file the browser already has, which is the wrong shape for a\n * sound that fires dozens of times a minute — a UI blip, a hit, a pickup. The\n * pool allocates once per source and replays.\n *\n * This is deliberately not {@link createAudioPlayer}: that handle tracks a\n * single \"current\" clip with loop, sink routing and lifecycle callbacks, which\n * is what background music needs. Effects are the opposite case — many\n * sources, all short, fire-and-forget, and the only thing that matters is that\n * firing one is cheap.\n *\n * A blocked `play()` is swallowed. Browsers reject playback until the user has\n * interacted with the page, and a sound effect is by definition not worth\n * interrupting anything over; call {@link SfxPool.preload} after the first\n * interaction if you want the pool warm.\n *\n * @param options - Master volume, base URL, voices per source and pool size.\n * @returns The pool handle.\n *\n * @example\n * const sfx = createSfxPool({ volume: 0.6, baseUrl: import.meta.env.BASE_URL });\n * sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\n/** One source's voices plus the round-robin cursor over them. */\ninterface SourceEntry {\n /** The audio elements sharing this source. */\n elements: HTMLAudioElement[];\n /** Index of the element the next play claims. */\n next: number;\n}\n\nexport function createSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { baseUrl = \"\", voices = 1, maxSources = 48 } = options;\n\n let master = clampVolume(options.volume ?? 1);\n const voiceCount = Math.max(1, Math.floor(voices));\n\n /**\n * Insertion-ordered by last play, so evicting the first key drops the\n * least recently used source.\n */\n const sources = new Map<string, SourceEntry>();\n\n /**\n * Per-play gain of whatever each element last played at.\n *\n * `setVolume` has to rescale live playback by the same factor it was\n * started with; assigning the master directly would yank a clip that\n * started at half volume up to full.\n */\n const gains = new WeakMap<HTMLAudioElement, number>();\n\n function resolve(src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n }\n\n function release(entry: { elements: HTMLAudioElement[] }): void {\n for (const element of entry.elements) {\n element.pause();\n element.removeAttribute(\"src\");\n element.load();\n }\n }\n\n /**\n * The pool entry for a source, creating and admitting it when it is new.\n *\n * Re-inserts an existing entry so the map order stays \"least recently used\n * first\", which is what makes evicting the first key correct.\n *\n * `created` is reported because assigning `src` on an element whose `preload`\n * is `\"auto\"` already starts the fetch: calling `load()` on an entry that was\n * merely looked up would abort that fetch and discard whatever is already\n * buffered, per the media element load algorithm.\n *\n * @param src - Raw source, resolved against `baseUrl`.\n * @returns The entry and whether this call created it, or `null` where\n * `Audio` does not exist.\n */\n function acquire(src: string): { entry: SourceEntry; created: boolean } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n sources.delete(url);\n sources.set(url, existing);\n return { entry: existing, created: false };\n }\n\n const entry: SourceEntry = {\n elements: Array.from({ length: voiceCount }, () => {\n const element = new Audio(url);\n element.preload = \"auto\";\n return element;\n }),\n next: 0,\n };\n sources.set(url, entry);\n\n if (sources.size > maxSources) {\n const oldest = sources.keys().next();\n if (!oldest.done && oldest.value !== url) {\n const evicted = sources.get(oldest.value);\n if (evicted) release(evicted);\n sources.delete(oldest.value);\n }\n }\n\n return { entry, created: true };\n }\n\n return {\n play(src, playOptions = {}) {\n const acquired = acquire(src);\n if (!acquired) return;\n\n const entry = acquired.entry;\n const element = entry.elements[entry.next];\n entry.next = (entry.next + 1) % entry.elements.length;\n\n const gain = clampVolume(playOptions.volume ?? 1);\n gains.set(element, gain);\n\n element.pause();\n element.currentTime = 0;\n element.volume = clampVolume(gain * master);\n void element.play().catch(() => {});\n },\n\n preload(src) {\n for (const one of Array.isArray(src) ? src : [src]) {\n const acquired = acquire(one);\n if (!acquired?.created) continue;\n for (const element of acquired.entry.elements) element.load();\n }\n },\n\n setVolume(volume) {\n master = clampVolume(volume);\n for (const entry of sources.values()) {\n for (const element of entry.elements) {\n if (element.paused) continue;\n element.volume = clampVolume((gains.get(element) ?? 1) * master);\n }\n }\n },\n\n stop(src) {\n const targets = src ? [sources.get(resolve(src))] : [...sources.values()];\n for (const entry of targets) {\n if (!entry) continue;\n for (const element of entry.elements) {\n element.pause();\n element.currentTime = 0;\n }\n }\n },\n\n dispose() {\n for (const entry of sources.values()) release(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -13,7 +13,10 @@ function t(t = {}) {
13
13
  function d(e) {
14
14
  if (typeof Audio > "u") return null;
15
15
  let t = l(e), n = s.get(t);
16
- if (n) return s.delete(t), s.set(t, n), n;
16
+ if (n) return s.delete(t), s.set(t, n), {
17
+ entry: n,
18
+ created: !1
19
+ };
17
20
  let r = {
18
21
  elements: Array.from({ length: o }, () => {
19
22
  let e = new Audio(t);
@@ -28,19 +31,25 @@ function t(t = {}) {
28
31
  t && u(t), s.delete(e.value);
29
32
  }
30
33
  }
31
- return r;
34
+ return {
35
+ entry: r,
36
+ created: !0
37
+ };
32
38
  }
33
39
  return {
34
40
  play(t, n = {}) {
35
41
  let r = d(t);
36
42
  if (!r) return;
37
- let i = r.elements[r.next];
38
- r.next = (r.next + 1) % r.elements.length;
39
- let o = e(n.volume ?? 1);
40
- c.set(i, o), i.pause(), i.currentTime = 0, i.volume = e(o * a), i.play().catch(() => {});
43
+ let i = r.entry, o = i.elements[i.next];
44
+ i.next = (i.next + 1) % i.elements.length;
45
+ let s = e(n.volume ?? 1);
46
+ c.set(o, s), o.pause(), o.currentTime = 0, o.volume = e(s * a), o.play().catch(() => {});
41
47
  },
42
48
  preload(e) {
43
- for (let t of Array.isArray(e) ? e : [e]) d(t)?.elements.forEach((e) => e.load());
49
+ for (let t of Array.isArray(e) ? e : [e]) {
50
+ let e = d(t);
51
+ if (e?.created) for (let t of e.entry.elements) t.load();
52
+ }
44
53
  },
45
54
  setVolume(t) {
46
55
  a = e(t);
@@ -1 +1 @@
1
- {"version":3,"file":"sfx-pool.js","names":[],"sources":["../../src/audio/sfx-pool.ts"],"sourcesContent":["/** Options for {@link createSfxPool}. */\nexport interface SfxPoolOptions {\n /**\n * Master volume, `0`–`1`, multiplied into every per-play volume. Default\n * `1`. Wire it to the app's sound setting so one value governs the lot.\n */\n volume?: number;\n /**\n * Prefix applied to sources that are not absolute URLs — typically Vite's\n * `import.meta.env.BASE_URL`, so a build served from a subpath resolves.\n * Default `\"\"`.\n */\n baseUrl?: string;\n /**\n * Elements kept per source. `1` (the default) restarts the clip on every\n * play, which is what a menu blip wants. Raise it to let a sound overlap\n * itself — a hit landing while the previous one is still ringing.\n */\n voices?: number;\n /**\n * Maximum number of distinct sources held. When exceeded, the least\n * recently played source is released. Default `48`.\n */\n maxSources?: number;\n}\n\n/** Per-play overrides. */\nexport interface PlaySfxOptions {\n /** Volume for this play, `0`–`1`, multiplied by the pool's master. Default `1`. */\n volume?: number;\n}\n\n/** Imperative handle over a pool of short sound effects. */\nexport interface SfxPool {\n /** Play a clip, allocating and caching its element on first use. */\n play: (src: string, options?: PlaySfxOptions) => void;\n /** Fetch clips ahead of the first play, so it is not silent while the file downloads. */\n preload: (src: string | string[]) => void;\n /** Set the master volume, applying it to anything already sounding. */\n setVolume: (volume: number) => void;\n /** Stop one source, or every source when called with no argument. */\n stop: (src?: string) => void;\n /** Release every element. Call on unmount. */\n dispose: () => void;\n}\n\n/** Clamp to the range `HTMLMediaElement.volume` accepts, rejecting `NaN`. */\nfunction clampVolume(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * A pool of preallocated `<audio>` elements for short sound effects.\n *\n * `new Audio(src)` on every play allocates an element and re-enters the network\n * stack for a file the browser already has, which is the wrong shape for a\n * sound that fires dozens of times a minute — a UI blip, a hit, a pickup. The\n * pool allocates once per source and replays.\n *\n * This is deliberately not {@link createAudioPlayer}: that handle tracks a\n * single \"current\" clip with loop, sink routing and lifecycle callbacks, which\n * is what background music needs. Effects are the opposite case — many\n * sources, all short, fire-and-forget, and the only thing that matters is that\n * firing one is cheap.\n *\n * A blocked `play()` is swallowed. Browsers reject playback until the user has\n * interacted with the page, and a sound effect is by definition not worth\n * interrupting anything over; call {@link SfxPool.preload} after the first\n * interaction if you want the pool warm.\n *\n * @param options - Master volume, base URL, voices per source and pool size.\n * @returns The pool handle.\n *\n * @example\n * const sfx = createSfxPool({ volume: 0.6, baseUrl: import.meta.env.BASE_URL });\n * sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function createSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { baseUrl = \"\", voices = 1, maxSources = 48 } = options;\n\n let master = clampVolume(options.volume ?? 1);\n const voiceCount = Math.max(1, Math.floor(voices));\n\n /**\n * Insertion-ordered by last play, so evicting the first key drops the\n * least recently used source.\n */\n const sources = new Map<string, { elements: HTMLAudioElement[]; next: number }>();\n\n /**\n * Per-play gain of whatever each element last played at.\n *\n * `setVolume` has to rescale live playback by the same factor it was\n * started with; assigning the master directly would yank a clip that\n * started at half volume up to full.\n */\n const gains = new WeakMap<HTMLAudioElement, number>();\n\n function resolve(src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n }\n\n function release(entry: { elements: HTMLAudioElement[] }): void {\n for (const element of entry.elements) {\n element.pause();\n element.removeAttribute(\"src\");\n element.load();\n }\n }\n\n function acquire(src: string): { elements: HTMLAudioElement[]; next: number } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n // Re-insert so the map order stays \"least recently played first\".\n sources.delete(url);\n sources.set(url, existing);\n return existing;\n }\n\n const entry = {\n elements: Array.from({ length: voiceCount }, () => {\n const element = new Audio(url);\n element.preload = \"auto\";\n return element;\n }),\n next: 0,\n };\n sources.set(url, entry);\n\n if (sources.size > maxSources) {\n const oldest = sources.keys().next();\n if (!oldest.done && oldest.value !== url) {\n const evicted = sources.get(oldest.value);\n if (evicted) release(evicted);\n sources.delete(oldest.value);\n }\n }\n\n return entry;\n }\n\n return {\n play(src, playOptions = {}) {\n const entry = acquire(src);\n if (!entry) return;\n\n const element = entry.elements[entry.next];\n entry.next = (entry.next + 1) % entry.elements.length;\n\n const gain = clampVolume(playOptions.volume ?? 1);\n gains.set(element, gain);\n\n element.pause();\n element.currentTime = 0;\n element.volume = clampVolume(gain * master);\n void element.play().catch(() => {});\n },\n\n preload(src) {\n for (const one of Array.isArray(src) ? src : [src]) {\n acquire(one)?.elements.forEach((element) => element.load());\n }\n },\n\n setVolume(volume) {\n master = clampVolume(volume);\n for (const entry of sources.values()) {\n for (const element of entry.elements) {\n if (element.paused) continue;\n element.volume = clampVolume((gains.get(element) ?? 1) * master);\n }\n }\n },\n\n stop(src) {\n const targets = src ? [sources.get(resolve(src))] : [...sources.values()];\n for (const entry of targets) {\n if (!entry) continue;\n for (const element of entry.elements) {\n element.pause();\n element.currentTime = 0;\n }\n }\n },\n\n dispose() {\n for (const entry of sources.values()) release(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sfx-pool.js","names":[],"sources":["../../src/audio/sfx-pool.ts"],"sourcesContent":["/** Options for {@link createSfxPool}. */\nexport interface SfxPoolOptions {\n /**\n * Master volume, `0`–`1`, multiplied into every per-play volume. Default\n * `1`. Wire it to the app's sound setting so one value governs the lot.\n */\n volume?: number;\n /**\n * Prefix applied to sources that are not absolute URLs — typically Vite's\n * `import.meta.env.BASE_URL`, so a build served from a subpath resolves.\n * Default `\"\"`.\n */\n baseUrl?: string;\n /**\n * Elements kept per source. `1` (the default) restarts the clip on every\n * play, which is what a menu blip wants. Raise it to let a sound overlap\n * itself — a hit landing while the previous one is still ringing.\n */\n voices?: number;\n /**\n * Maximum number of distinct sources held. When exceeded, the least\n * recently played source is released. Default `48`.\n */\n maxSources?: number;\n}\n\n/** Per-play overrides. */\nexport interface PlaySfxOptions {\n /** Volume for this play, `0`–`1`, multiplied by the pool's master. Default `1`. */\n volume?: number;\n}\n\n/** Imperative handle over a pool of short sound effects. */\nexport interface SfxPool {\n /** Play a clip, allocating and caching its element on first use. */\n play: (src: string, options?: PlaySfxOptions) => void;\n /** Fetch clips ahead of the first play, so it is not silent while the file downloads. */\n preload: (src: string | string[]) => void;\n /** Set the master volume, applying it to anything already sounding. */\n setVolume: (volume: number) => void;\n /** Stop one source, or every source when called with no argument. */\n stop: (src?: string) => void;\n /** Release every element. Call on unmount. */\n dispose: () => void;\n}\n\n/** Clamp to the range `HTMLMediaElement.volume` accepts, rejecting `NaN`. */\nfunction clampVolume(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * A pool of preallocated `<audio>` elements for short sound effects.\n *\n * `new Audio(src)` on every play allocates an element and re-enters the network\n * stack for a file the browser already has, which is the wrong shape for a\n * sound that fires dozens of times a minute — a UI blip, a hit, a pickup. The\n * pool allocates once per source and replays.\n *\n * This is deliberately not {@link createAudioPlayer}: that handle tracks a\n * single \"current\" clip with loop, sink routing and lifecycle callbacks, which\n * is what background music needs. Effects are the opposite case — many\n * sources, all short, fire-and-forget, and the only thing that matters is that\n * firing one is cheap.\n *\n * A blocked `play()` is swallowed. Browsers reject playback until the user has\n * interacted with the page, and a sound effect is by definition not worth\n * interrupting anything over; call {@link SfxPool.preload} after the first\n * interaction if you want the pool warm.\n *\n * @param options - Master volume, base URL, voices per source and pool size.\n * @returns The pool handle.\n *\n * @example\n * const sfx = createSfxPool({ volume: 0.6, baseUrl: import.meta.env.BASE_URL });\n * sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\n/** One source's voices plus the round-robin cursor over them. */\ninterface SourceEntry {\n /** The audio elements sharing this source. */\n elements: HTMLAudioElement[];\n /** Index of the element the next play claims. */\n next: number;\n}\n\nexport function createSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { baseUrl = \"\", voices = 1, maxSources = 48 } = options;\n\n let master = clampVolume(options.volume ?? 1);\n const voiceCount = Math.max(1, Math.floor(voices));\n\n /**\n * Insertion-ordered by last play, so evicting the first key drops the\n * least recently used source.\n */\n const sources = new Map<string, SourceEntry>();\n\n /**\n * Per-play gain of whatever each element last played at.\n *\n * `setVolume` has to rescale live playback by the same factor it was\n * started with; assigning the master directly would yank a clip that\n * started at half volume up to full.\n */\n const gains = new WeakMap<HTMLAudioElement, number>();\n\n function resolve(src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n }\n\n function release(entry: { elements: HTMLAudioElement[] }): void {\n for (const element of entry.elements) {\n element.pause();\n element.removeAttribute(\"src\");\n element.load();\n }\n }\n\n /**\n * The pool entry for a source, creating and admitting it when it is new.\n *\n * Re-inserts an existing entry so the map order stays \"least recently used\n * first\", which is what makes evicting the first key correct.\n *\n * `created` is reported because assigning `src` on an element whose `preload`\n * is `\"auto\"` already starts the fetch: calling `load()` on an entry that was\n * merely looked up would abort that fetch and discard whatever is already\n * buffered, per the media element load algorithm.\n *\n * @param src - Raw source, resolved against `baseUrl`.\n * @returns The entry and whether this call created it, or `null` where\n * `Audio` does not exist.\n */\n function acquire(src: string): { entry: SourceEntry; created: boolean } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n sources.delete(url);\n sources.set(url, existing);\n return { entry: existing, created: false };\n }\n\n const entry: SourceEntry = {\n elements: Array.from({ length: voiceCount }, () => {\n const element = new Audio(url);\n element.preload = \"auto\";\n return element;\n }),\n next: 0,\n };\n sources.set(url, entry);\n\n if (sources.size > maxSources) {\n const oldest = sources.keys().next();\n if (!oldest.done && oldest.value !== url) {\n const evicted = sources.get(oldest.value);\n if (evicted) release(evicted);\n sources.delete(oldest.value);\n }\n }\n\n return { entry, created: true };\n }\n\n return {\n play(src, playOptions = {}) {\n const acquired = acquire(src);\n if (!acquired) return;\n\n const entry = acquired.entry;\n const element = entry.elements[entry.next];\n entry.next = (entry.next + 1) % entry.elements.length;\n\n const gain = clampVolume(playOptions.volume ?? 1);\n gains.set(element, gain);\n\n element.pause();\n element.currentTime = 0;\n element.volume = clampVolume(gain * master);\n void element.play().catch(() => {});\n },\n\n preload(src) {\n for (const one of Array.isArray(src) ? src : [src]) {\n const acquired = acquire(one);\n if (!acquired?.created) continue;\n for (const element of acquired.entry.elements) element.load();\n }\n },\n\n setVolume(volume) {\n master = clampVolume(volume);\n for (const entry of sources.values()) {\n for (const element of entry.elements) {\n if (element.paused) continue;\n element.volume = clampVolume((gains.get(element) ?? 1) * master);\n }\n }\n },\n\n stop(src) {\n const targets = src ? [sources.get(resolve(src))] : [...sources.values()];\n for (const entry of targets) {\n if (!entry) continue;\n for (const element of entry.elements) {\n element.pause();\n element.currentTime = 0;\n }\n }\n },\n\n dispose() {\n for (const entry of sources.values()) release(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("./sfx-pool.cjs");let t=require("react");function n(n={}){let{volume:r=1,baseUrl:i,voices:a,maxSources:o}=n,[s]=(0,t.useState)(()=>e.createSfxPool({baseUrl:i,voices:a,maxSources:o,volume:r}));return(0,t.useEffect)(()=>{s.setVolume(r)},[s,r]),(0,t.useEffect)(()=>()=>s.dispose(),[s]),s}exports.useSfxPool=n;
1
+ const e=require("./sfx-pool.cjs");let t=require("react");function n(n={}){let{volume:r=1}=n,[i]=(0,t.useState)(()=>e.createSfxPool(n));return(0,t.useEffect)(()=>{i.setVolume(r)},[i,r]),(0,t.useEffect)(()=>()=>i.dispose(),[i]),i}exports.useSfxPool=n;
2
2
  //# sourceMappingURL=use-sfx-pool.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-sfx-pool.cjs","names":[],"sources":["../../src/audio/use-sfx-pool.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createSfxPool, type SfxPool, type SfxPoolOptions } from \"./sfx-pool\";\n\n/**\n * Component-scoped {@link createSfxPool}, with the lifecycle wired up.\n *\n * The pool is created once — through a lazy `useState` initializer, so no\n * element is allocated on a render that gets discarded — and disposed on\n * unmount, so its elements do not outlive the screen that owns them.\n *\n * `volume` is tracked separately: changing it calls `setVolume` on the existing\n * pool rather than rebuilding it, which would throw away every clip the user\n * has already downloaded — exactly the cost the pool exists to avoid.\n * `baseUrl`, `voices` and `maxSources` are read once, at creation.\n *\n * @param options - Passed through to {@link createSfxPool}.\n * @returns A stable pool handle.\n *\n * @example\n * const sfx = useSfxPool({ volume: settings.sfxVolume / 100, baseUrl: import.meta.env.BASE_URL });\n *\n * useEffect(() => { sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]); }, [sfx]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function useSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { volume = 1, baseUrl, voices, maxSources } = options;\n\n const [pool] = useState(() => createSfxPool({ baseUrl, voices, maxSources, volume }));\n\n useEffect(() => {\n pool.setVolume(volume);\n }, [pool, volume]);\n\n useEffect(() => () => pool.dispose(), [pool]);\n\n return pool;\n}\n"],"mappings":"yDAyBA,SAAgB,EAAW,EAA0B,CAAC,EAAY,CAC9D,GAAM,CAAE,SAAS,EAAG,UAAS,SAAQ,cAAe,EAE9C,CAAC,IAAA,EAAQ,EAAA,SAAA,KAAe,EAAA,cAAc,CAAE,UAAS,SAAQ,aAAY,QAAO,CAAC,CAAC,EAQpF,OANA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAK,UAAU,CAAM,CACzB,EAAG,CAAC,EAAM,CAAM,CAAC,GAEjB,EAAA,EAAA,UAAA,SAAsB,EAAK,QAAQ,EAAG,CAAC,CAAI,CAAC,EAErC,CACX"}
1
+ {"version":3,"file":"use-sfx-pool.cjs","names":[],"sources":["../../src/audio/use-sfx-pool.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createSfxPool, type SfxPool, type SfxPoolOptions } from \"./sfx-pool\";\n\n/**\n * Component-scoped {@link createSfxPool}, with the lifecycle wired up.\n *\n * The pool is created once — through a lazy `useState` initializer, so no\n * element is allocated on a render that gets discarded — and disposed on\n * unmount, so its elements do not outlive the screen that owns them.\n *\n * `volume` is tracked separately: changing it calls `setVolume` on the existing\n * pool rather than rebuilding it, which would throw away every clip the user\n * has already downloaded — exactly the cost the pool exists to avoid.\n * `baseUrl`, `voices` and `maxSources` are read once, at creation. The options\n * object is forwarded whole rather than rebuilt field by field, so a field added\n * to {@link SfxPoolOptions} does not silently stop reaching the pool.\n *\n * @param options - Passed through to {@link createSfxPool}.\n * @returns A stable pool handle.\n *\n * @example\n * const sfx = useSfxPool({ volume: settings.sfxVolume / 100, baseUrl: import.meta.env.BASE_URL });\n *\n * useEffect(() => { sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]); }, [sfx]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function useSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { volume = 1 } = options;\n\n const [pool] = useState(() => createSfxPool(options));\n\n useEffect(() => {\n pool.setVolume(volume);\n }, [pool, volume]);\n\n useEffect(() => () => pool.dispose(), [pool]);\n\n return pool;\n}\n"],"mappings":"yDA2BA,SAAgB,EAAW,EAA0B,CAAC,EAAY,CAC9D,GAAM,CAAE,SAAS,GAAM,EAEjB,CAAC,IAAA,EAAQ,EAAA,SAAA,KAAe,EAAA,cAAc,CAAO,CAAC,EAQpD,OANA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAK,UAAU,CAAM,CACzB,EAAG,CAAC,EAAM,CAAM,CAAC,GAEjB,EAAA,EAAA,UAAA,SAAsB,EAAK,QAAQ,EAAG,CAAC,CAAI,CAAC,EAErC,CACX"}
@@ -2,15 +2,10 @@ import { createSfxPool as e } from "./sfx-pool.js";
2
2
  import { useEffect as t, useState as n } from "react";
3
3
  //#region src/audio/use-sfx-pool.ts
4
4
  function r(r = {}) {
5
- let { volume: i = 1, baseUrl: a, voices: o, maxSources: s } = r, [c] = n(() => e({
6
- baseUrl: a,
7
- voices: o,
8
- maxSources: s,
9
- volume: i
10
- }));
5
+ let { volume: i = 1 } = r, [a] = n(() => e(r));
11
6
  return t(() => {
12
- c.setVolume(i);
13
- }, [c, i]), t(() => () => c.dispose(), [c]), c;
7
+ a.setVolume(i);
8
+ }, [a, i]), t(() => () => a.dispose(), [a]), a;
14
9
  }
15
10
  //#endregion
16
11
  export { r as useSfxPool };
@@ -1 +1 @@
1
- {"version":3,"file":"use-sfx-pool.js","names":[],"sources":["../../src/audio/use-sfx-pool.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createSfxPool, type SfxPool, type SfxPoolOptions } from \"./sfx-pool\";\n\n/**\n * Component-scoped {@link createSfxPool}, with the lifecycle wired up.\n *\n * The pool is created once — through a lazy `useState` initializer, so no\n * element is allocated on a render that gets discarded — and disposed on\n * unmount, so its elements do not outlive the screen that owns them.\n *\n * `volume` is tracked separately: changing it calls `setVolume` on the existing\n * pool rather than rebuilding it, which would throw away every clip the user\n * has already downloaded — exactly the cost the pool exists to avoid.\n * `baseUrl`, `voices` and `maxSources` are read once, at creation.\n *\n * @param options - Passed through to {@link createSfxPool}.\n * @returns A stable pool handle.\n *\n * @example\n * const sfx = useSfxPool({ volume: settings.sfxVolume / 100, baseUrl: import.meta.env.BASE_URL });\n *\n * useEffect(() => { sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]); }, [sfx]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function useSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { volume = 1, baseUrl, voices, maxSources } = options;\n\n const [pool] = useState(() => createSfxPool({ baseUrl, voices, maxSources, volume }));\n\n useEffect(() => {\n pool.setVolume(volume);\n }, [pool, volume]);\n\n useEffect(() => () => pool.dispose(), [pool]);\n\n return pool;\n}\n"],"mappings":";;;AAyBA,SAAgB,EAAW,IAA0B,CAAC,GAAY;CAC9D,IAAM,EAAE,YAAS,GAAG,YAAS,WAAQ,kBAAe,GAE9C,CAAC,KAAQ,QAAe,EAAc;EAAE;EAAS;EAAQ;EAAY;CAAO,CAAC,CAAC;CAQpF,OANA,QAAgB;EACZ,EAAK,UAAU,CAAM;CACzB,GAAG,CAAC,GAAM,CAAM,CAAC,GAEjB,cAAsB,EAAK,QAAQ,GAAG,CAAC,CAAI,CAAC,GAErC;AACX"}
1
+ {"version":3,"file":"use-sfx-pool.js","names":[],"sources":["../../src/audio/use-sfx-pool.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createSfxPool, type SfxPool, type SfxPoolOptions } from \"./sfx-pool\";\n\n/**\n * Component-scoped {@link createSfxPool}, with the lifecycle wired up.\n *\n * The pool is created once — through a lazy `useState` initializer, so no\n * element is allocated on a render that gets discarded — and disposed on\n * unmount, so its elements do not outlive the screen that owns them.\n *\n * `volume` is tracked separately: changing it calls `setVolume` on the existing\n * pool rather than rebuilding it, which would throw away every clip the user\n * has already downloaded — exactly the cost the pool exists to avoid.\n * `baseUrl`, `voices` and `maxSources` are read once, at creation. The options\n * object is forwarded whole rather than rebuilt field by field, so a field added\n * to {@link SfxPoolOptions} does not silently stop reaching the pool.\n *\n * @param options - Passed through to {@link createSfxPool}.\n * @returns A stable pool handle.\n *\n * @example\n * const sfx = useSfxPool({ volume: settings.sfxVolume / 100, baseUrl: import.meta.env.BASE_URL });\n *\n * useEffect(() => { sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]); }, [sfx]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function useSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { volume = 1 } = options;\n\n const [pool] = useState(() => createSfxPool(options));\n\n useEffect(() => {\n pool.setVolume(volume);\n }, [pool, volume]);\n\n useEffect(() => () => pool.dispose(), [pool]);\n\n return pool;\n}\n"],"mappings":";;;AA2BA,SAAgB,EAAW,IAA0B,CAAC,GAAY;CAC9D,IAAM,EAAE,YAAS,MAAM,GAEjB,CAAC,KAAQ,QAAe,EAAc,CAAO,CAAC;CAQpD,OANA,QAAgB;EACZ,EAAK,UAAU,CAAM;CACzB,GAAG,CAAC,GAAM,CAAM,CAAC,GAEjB,cAAsB,EAAK,QAAQ,GAAG,CAAC,CAAI,CAAC,GAErC;AACX"}
@@ -1,2 +1,2 @@
1
- var e=class extends Error{kind;constructor(e,t,n){super(t),this.name=`PasskeyError`,this.kind=e,this.cause=n}},t=[{type:`public-key`,alg:-8},{type:`public-key`,alg:-7},{type:`public-key`,alg:-257}];function n(){return globalThis.PublicKeyCredential}function r(e){let t=e.replace(/-/g,`+`).replace(/_/g,`/`),n=t.padEnd(t.length+(4-t.length%4)%4,`=`),r=atob(n),i=new Uint8Array(r.length);for(let e=0;e<r.length;e+=1)i[e]=r.charCodeAt(e);return i}function i(e){let t=e instanceof Uint8Array?e:new Uint8Array(e),n=``;for(let e of t)n+=String.fromCharCode(e);return btoa(n).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function a(){return typeof window<`u`&&typeof navigator<`u`&&navigator.credentials!==void 0&&typeof navigator.credentials.create==`function`&&n()!==void 0}async function o(){let e=n();if(!e?.isUserVerifyingPlatformAuthenticatorAvailable)return!1;try{return await e.isUserVerifyingPlatformAuthenticatorAvailable()}catch{return!1}}async function s(){let e=n();if(!e?.isConditionalMediationAvailable)return!1;try{return await e.isConditionalMediationAvailable()}catch{return!1}}function c(t,n){if(t instanceof e)return t;if(typeof window<`u`&&!window.isSecureContext)return new e(`insecure`,`Passkeys require a secure (HTTPS) connection.`,t);if(t instanceof DOMException)switch(t.name){case`NotAllowedError`:return new e(`cancelled`,`The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.`,t);case`InvalidStateError`:return new e(`already-registered`,`This device already has a passkey for this account. Sign in with it instead of creating another.`,t);case`NotSupportedError`:return new e(`not-supported`,`No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.`,t);case`SecurityError`:return new e(`rp-mismatch`,`The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.`,t);case`AbortError`:return new e(`aborted`,`The passkey request was aborted.`,t)}return t instanceof TypeError?new e(`invalid-options`,`The ${n} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,t):new e(`unknown`,t instanceof Error?t.message:`Unexpected error during the passkey ${n} ceremony.`,t)}function l(e){if(e)return e.map(e=>({id:r(e.id),type:e.type,transports:e.transports}))}function u(n={}){let{rpId:u,timeoutMs:d=6e4,credentials:f}=n;function p(){if(f)return f;if(!a())throw new e(`unsupported`,typeof window<`u`&&!window.isSecureContext?`Passkeys require a secure (HTTPS) connection.`:`This browser does not support passkeys (WebAuthn).`);return navigator.credentials}async function m(n,a={}){let o=p(),s={challenge:r(n.challenge),rp:{name:n.rp.name,id:n.rp.id??u},user:{id:r(n.user.id),name:n.user.name,displayName:n.user.displayName},pubKeyCredParams:n.pubKeyCredParams??t,timeout:n.timeout??d,excludeCredentials:l(n.excludeCredentials),authenticatorSelection:n.authenticatorSelection,attestation:n.attestation,extensions:n.extensions},f;try{f=await o.create({publicKey:s,signal:a.signal})}catch(e){throw c(e,`register`)}if(!f)throw new e(`unknown`,`The authenticator returned no credential.`);let m=f,h=m.response,g=h.getPublicKey?.();return{id:m.id,rawId:i(m.rawId),type:`public-key`,authenticatorAttachment:m.authenticatorAttachment??null,response:{clientDataJSON:i(h.clientDataJSON),attestationObject:i(h.attestationObject),transports:h.getTransports?.(),publicKeyAlgorithm:h.getPublicKeyAlgorithm?.(),publicKey:g?i(g):void 0,authenticatorData:h.getAuthenticatorData?i(h.getAuthenticatorData()):void 0},clientExtensionResults:m.getClientExtensionResults()}}async function h(t,n={}){let a=p(),o={challenge:r(t.challenge),rpId:t.rpId??u,timeout:t.timeout??d,allowCredentials:l(t.allowCredentials),userVerification:t.userVerification,extensions:t.extensions},s;try{s=await a.get({publicKey:o,signal:n.signal,mediation:n.mediation})}catch(e){throw c(e,`authenticate`)}if(!s)throw new e(`unknown`,`The authenticator returned no credential.`);let f=s,m=f.response;return{id:f.id,rawId:i(f.rawId),type:`public-key`,authenticatorAttachment:f.authenticatorAttachment??null,response:{clientDataJSON:i(m.clientDataJSON),authenticatorData:i(m.authenticatorData),signature:i(m.signature),userHandle:m.userHandle?i(m.userHandle):null},clientExtensionResults:f.getClientExtensionResults()}}return{register:m,authenticate:h,isSupported:()=>f!==void 0||a(),isPlatformAuthenticatorAvailable:o,isConditionalMediationAvailable:s}}exports.DEFAULT_PUB_KEY_CRED_PARAMS=t,exports.PasskeyError=e,exports.base64UrlToBytes=r,exports.bytesToBase64Url=i,exports.classifyPasskeyError=c,exports.createPasskeyClient=u,exports.isConditionalMediationAvailable=s,exports.isPasskeySupported=a,exports.isPlatformAuthenticatorAvailable=o;
1
+ const e=require("../utils/base64.cjs");var t=class extends Error{kind;constructor(e,t,n){super(t),this.name=`PasskeyError`,this.kind=e,this.cause=n}},n=[{type:`public-key`,alg:-8},{type:`public-key`,alg:-7},{type:`public-key`,alg:-257}];function r(){return globalThis.PublicKeyCredential}function i(t){return e.base64ToBytes(t)}function a(t){let n=t instanceof Uint8Array?t:new Uint8Array(t);return e.bytesToBase64(n,{urlSafe:!0})}function o(){return typeof window<`u`&&typeof navigator<`u`&&navigator.credentials!==void 0&&typeof navigator.credentials.create==`function`&&r()!==void 0}async function s(){let e=r();if(!e?.isUserVerifyingPlatformAuthenticatorAvailable)return!1;try{return await e.isUserVerifyingPlatformAuthenticatorAvailable()}catch{return!1}}async function c(){let e=r();if(!e?.isConditionalMediationAvailable)return!1;try{return await e.isConditionalMediationAvailable()}catch{return!1}}function l(e,n){if(e instanceof t)return e;if(typeof window<`u`&&!window.isSecureContext)return new t(`insecure`,`Passkeys require a secure (HTTPS) connection.`,e);if(e instanceof DOMException)switch(e.name){case`NotAllowedError`:return new t(`cancelled`,`The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.`,e);case`InvalidStateError`:return new t(`already-registered`,`This device already has a passkey for this account. Sign in with it instead of creating another.`,e);case`NotSupportedError`:return new t(`not-supported`,`No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.`,e);case`SecurityError`:return new t(`rp-mismatch`,`The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.`,e);case`AbortError`:return new t(`aborted`,`The passkey request was aborted.`,e)}return e instanceof TypeError?new t(`invalid-options`,`The ${n} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,e):new t(`unknown`,e instanceof Error?e.message:`Unexpected error during the passkey ${n} ceremony.`,e)}function u(e){if(e)return e.map(e=>({id:i(e.id),type:e.type,transports:e.transports}))}function d(e={}){let{rpId:r,timeoutMs:d=6e4,credentials:f}=e;function p(){if(f)return f;if(!o())throw new t(`unsupported`,typeof window<`u`&&!window.isSecureContext?`Passkeys require a secure (HTTPS) connection.`:`This browser does not support passkeys (WebAuthn).`);return navigator.credentials}async function m(e,o={}){let s=p(),c={challenge:i(e.challenge),rp:{name:e.rp.name,id:e.rp.id??r},user:{id:i(e.user.id),name:e.user.name,displayName:e.user.displayName},pubKeyCredParams:e.pubKeyCredParams??n,timeout:e.timeout??d,excludeCredentials:u(e.excludeCredentials),authenticatorSelection:e.authenticatorSelection,attestation:e.attestation,extensions:e.extensions},f;try{f=await s.create({publicKey:c,signal:o.signal})}catch(e){throw l(e,`register`)}if(!f)throw new t(`unknown`,`The authenticator returned no credential.`);let m=f,h=m.response,g=h.getPublicKey?.();return{id:m.id,rawId:a(m.rawId),type:`public-key`,authenticatorAttachment:m.authenticatorAttachment??null,response:{clientDataJSON:a(h.clientDataJSON),attestationObject:a(h.attestationObject),transports:h.getTransports?.(),publicKeyAlgorithm:h.getPublicKeyAlgorithm?.(),publicKey:g?a(g):void 0,authenticatorData:h.getAuthenticatorData?a(h.getAuthenticatorData()):void 0},clientExtensionResults:m.getClientExtensionResults()}}async function h(e,n={}){let o=p(),s={challenge:i(e.challenge),rpId:e.rpId??r,timeout:e.timeout??d,allowCredentials:u(e.allowCredentials),userVerification:e.userVerification,extensions:e.extensions},c;try{c=await o.get({publicKey:s,signal:n.signal,mediation:n.mediation})}catch(e){throw l(e,`authenticate`)}if(!c)throw new t(`unknown`,`The authenticator returned no credential.`);let f=c,m=f.response;return{id:f.id,rawId:a(f.rawId),type:`public-key`,authenticatorAttachment:f.authenticatorAttachment??null,response:{clientDataJSON:a(m.clientDataJSON),authenticatorData:a(m.authenticatorData),signature:a(m.signature),userHandle:m.userHandle?a(m.userHandle):null},clientExtensionResults:f.getClientExtensionResults()}}return{register:m,authenticate:h,isSupported:()=>f!==void 0||o(),isPlatformAuthenticatorAvailable:s,isConditionalMediationAvailable:c}}exports.DEFAULT_PUB_KEY_CRED_PARAMS=n,exports.PasskeyError=t,exports.base64UrlToBytes=i,exports.bytesToBase64Url=a,exports.classifyPasskeyError=l,exports.createPasskeyClient=d,exports.isConditionalMediationAvailable=c,exports.isPasskeySupported=o,exports.isPlatformAuthenticatorAvailable=s;
2
2
  //# sourceMappingURL=passkey.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.cjs","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"AA+CA,IAAa,EAAb,cAAkC,KAAM,CAEpC,KASA,YAAY,EAAwB,EAAiB,EAAiB,CAClE,MAAM,CAAO,EACb,KAAK,KAAO,eACZ,KAAK,KAAO,EACZ,KAAK,MAAQ,CACjB,CACJ,EAyLa,EAAqE,CAC9E,CAAE,KAAM,aAAc,IAAK,EAAG,EAC9B,CAAE,KAAM,aAAc,IAAK,EAAG,EAC9B,CAAE,KAAM,aAAc,IAAK,IAAK,CACpC,EAgBA,SAAS,GAAqE,CAC1E,OAAQ,WAAoE,mBAChF,CAcA,SAAgB,EAAiB,EAAwC,CACrE,IAAM,EAAS,EAAM,QAAQ,KAAM,GAAG,CAAC,CAAC,QAAQ,KAAM,GAAG,EACnD,EAAS,EAAO,OAAO,EAAO,QAAW,EAAK,EAAO,OAAS,GAAM,EAAI,GAAG,EAC3E,EAAS,KAAK,CAAM,EACpB,EAAQ,IAAI,WAAW,EAAO,MAAM,EAC1C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAChD,EAAM,GAAS,EAAO,WAAW,CAAK,EAE1C,OAAO,CACX,CAQA,SAAgB,EAAiB,EAAyC,CACtE,IAAM,EAAQ,aAAiB,WAAa,EAAQ,IAAI,WAAW,CAAK,EACpE,EAAS,GACb,IAAK,IAAM,KAAQ,EAAO,GAAU,OAAO,aAAa,CAAI,EAC5D,OAAO,KAAK,CAAM,CAAC,CAAC,QAAQ,MAAO,GAAG,CAAC,CAAC,QAAQ,MAAO,GAAG,CAAC,CAAC,QAAQ,MAAO,EAAE,CACjF,CASA,SAAgB,GAA8B,CAC1C,OACI,OAAO,OAAW,KAClB,OAAO,UAAc,KACrB,UAAU,cAAgB,IAAA,IAC1B,OAAO,UAAU,YAAY,QAAW,YACxC,EAA2B,IAAM,IAAA,EAEzC,CAeA,eAAsB,GAAqD,CACvE,IAAM,EAAU,EAA2B,EAC3C,GAAI,CAAC,GAAS,8CAA+C,MAAO,GACpE,GAAI,CACA,OAAO,MAAM,EAAQ,8CAA8C,CACvE,MAAQ,CACJ,MAAO,EACX,CACJ,CAOA,eAAsB,GAAoD,CACtE,IAAM,EAAU,EAA2B,EAC3C,GAAI,CAAC,GAAS,gCAAiC,MAAO,GACtD,GAAI,CACA,OAAO,MAAM,EAAQ,gCAAgC,CACzD,MAAQ,CACJ,MAAO,EACX,CACJ,CAcA,SAAgB,EAAqB,EAAgB,EAAyC,CAC1F,GAAI,aAAiB,EAAc,OAAO,EAE1C,GAAI,OAAO,OAAW,KAAe,CAAC,OAAO,gBACzC,OAAO,IAAI,EAAa,WAAY,gDAAiD,CAAK,EAG9F,GAAI,aAAiB,aACjB,OAAQ,EAAM,KAAd,CACI,IAAK,kBACD,OAAO,IAAI,EACP,YACA,kIACA,CACJ,EACJ,IAAK,oBACD,OAAO,IAAI,EACP,qBACA,mGACA,CACJ,EACJ,IAAK,oBACD,OAAO,IAAI,EACP,gBACA,gGACA,CACJ,EACJ,IAAK,gBACD,OAAO,IAAI,EACP,cACA,kHACA,CACJ,EACJ,IAAK,aACD,OAAO,IAAI,EAAa,UAAW,mCAAoC,CAAK,CACpF,CAWJ,OARI,aAAiB,UACV,IAAI,EACP,kBACA,OAAO,EAAS,yGAChB,CACJ,EAGG,IAAI,EACP,UACA,aAAiB,MACX,EAAM,QACN,uCAAuC,EAAS,YACtD,CACJ,CACJ,CAEA,SAAS,EACL,EAC2C,CACtC,KACL,OAAO,EAAK,IAAK,IAAU,CACvB,GAAI,EAAiB,EAAK,EAAE,EAC5B,KAAM,EAAK,KACX,WAAY,EAAK,UACrB,EAAE,CACN,CAmCA,SAAgB,EAAoB,EAAsC,CAAC,EAAkB,CACzF,GAAM,CAAE,OAAM,YAAY,IAAQ,eAAgB,EAElD,SAAS,GAAsC,CAC3C,GAAI,EAAa,OAAO,EACxB,GAAI,CAAC,EAAmB,EACpB,MAAM,IAAI,EACN,cACA,OAAO,OAAW,KAAe,CAAC,OAAO,gBACnC,gDACA,oDACV,EAEJ,OAAO,UAAU,WACrB,CAEA,eAAe,EACX,EACA,EAA4B,CAAC,EACG,CAChC,IAAM,EAAM,EAAU,EAChB,EAAgD,CAClD,UAAW,EAAiB,EAAK,SAAS,EAC1C,GAAI,CAAE,KAAM,EAAK,GAAG,KAAM,GAAI,EAAK,GAAG,IAAM,CAAK,EACjD,KAAM,CACF,GAAI,EAAiB,EAAK,KAAK,EAAE,EACjC,KAAM,EAAK,KAAK,KAChB,YAAa,EAAK,KAAK,WAC3B,EACA,iBAAkB,EAAK,kBAAoB,EAC3C,QAAS,EAAK,SAAW,EACzB,mBAAoB,EAAc,EAAK,kBAAkB,EACzD,uBAAwB,EAAK,uBAC7B,YAAa,EAAK,YAClB,WAAY,EAAK,UACrB,EAEI,EACJ,GAAI,CACA,EAAa,MAAM,EAAI,OAAO,CAAE,YAAW,OAAQ,EAAK,MAAO,CAAC,CACpE,OAAS,EAAO,CACZ,MAAM,EAAqB,EAAO,UAAU,CAChD,CACA,GAAI,CAAC,EACD,MAAM,IAAI,EAAa,UAAW,2CAA2C,EAGjF,IAAM,EAAQ,EACR,EAAW,EAAM,SACjB,EAAiB,EAAS,eAAe,EAE/C,MAAO,CACH,GAAI,EAAM,GACV,MAAO,EAAiB,EAAM,KAAK,EACnC,KAAM,aACN,wBAAyB,EAAM,yBAA2B,KAC1D,SAAU,CACN,eAAgB,EAAiB,EAAS,cAAc,EACxD,kBAAmB,EAAiB,EAAS,iBAAiB,EAC9D,WAAY,EAAS,gBAAgB,EACrC,mBAAoB,EAAS,wBAAwB,EACrD,UAAW,EAAiB,EAAiB,CAAc,EAAI,IAAA,GAC/D,kBAAmB,EAAS,qBACtB,EAAiB,EAAS,qBAAqB,CAAC,EAChD,IAAA,EACV,EACA,uBAAwB,EAAM,0BAA0B,CAC5D,CACJ,CAEA,eAAe,EACX,EACA,EAAgC,CAAC,EACC,CAClC,IAAM,EAAM,EAAU,EAChB,EAA+C,CACjD,UAAW,EAAiB,EAAK,SAAS,EAC1C,KAAM,EAAK,MAAQ,EACnB,QAAS,EAAK,SAAW,EACzB,iBAAkB,EAAc,EAAK,gBAAgB,EACrD,iBAAkB,EAAK,iBACvB,WAAY,EAAK,UACrB,EAEI,EACJ,GAAI,CACA,EAAa,MAAM,EAAI,IAAI,CACvB,YACA,OAAQ,EAAK,OACb,UAAW,EAAK,SACpB,CAAC,CACL,OAAS,EAAO,CACZ,MAAM,EAAqB,EAAO,cAAc,CACpD,CACA,GAAI,CAAC,EACD,MAAM,IAAI,EAAa,UAAW,2CAA2C,EAGjF,IAAM,EAAQ,EACR,EAAW,EAAM,SAEvB,MAAO,CACH,GAAI,EAAM,GACV,MAAO,EAAiB,EAAM,KAAK,EACnC,KAAM,aACN,wBAAyB,EAAM,yBAA2B,KAC1D,SAAU,CACN,eAAgB,EAAiB,EAAS,cAAc,EACxD,kBAAmB,EAAiB,EAAS,iBAAiB,EAC9D,UAAW,EAAiB,EAAS,SAAS,EAC9C,WAAY,EAAS,WAAa,EAAiB,EAAS,UAAU,EAAI,IAC9E,EACA,uBAAwB,EAAM,0BAA0B,CAC5D,CACJ,CAEA,MAAO,CACH,WACA,eACA,gBAAmB,IAAgB,IAAA,IAAa,EAAmB,EACnE,mCACA,iCACJ,CACJ"}
1
+ {"version":3,"file":"passkey.cjs","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n\nimport { base64ToBytes, bytesToBase64 } from \"@/utils/base64\";\n\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * This is the WebAuthn-facing name for {@link base64ToBytes}; the codec itself is\n * shared, so the padding rule has one implementation rather than three.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n return base64ToBytes(value);\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * Normalises `ArrayBuffer` to a view — which is what the WebAuthn response\n * fields hand over — and delegates the encoding to {@link bytesToBase64}.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n return bytesToBase64(bytes, { urlSafe: true });\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}