tempest-react-sdk 0.81.0 → 0.82.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 (179) hide show
  1. package/README.md +5 -6
  2. package/bin/create-tempest-app.e2e.test.mjs +31 -0
  3. package/bin/create-tempest-app.mjs +11 -1
  4. package/bin/lib/doctor/doctor.e2e.test.mjs +29 -0
  5. package/bin/lib/doctor/vitest.mjs +120 -0
  6. package/bin/lib/doctor/vitest.test.mjs +139 -0
  7. package/bin/lib/fs/contain.mjs +7 -0
  8. package/bin/lib/openapi/escape.mjs +68 -4
  9. package/bin/lib/openapi/generate.hostile.test.mjs +137 -0
  10. package/bin/lib/openapi/generate.mjs +78 -13
  11. package/bin/lib/openapi/pattern-risk.mjs +134 -0
  12. package/bin/lib/openapi/pattern-risk.test.mjs +45 -0
  13. package/bin/tempest-gen.test.mjs +100 -0
  14. package/bin/tempest.mjs +24 -8
  15. package/dist/access/use-can.cjs +1 -1
  16. package/dist/access/use-can.cjs.map +1 -1
  17. package/dist/access/use-can.js +25 -28
  18. package/dist/access/use-can.js.map +1 -1
  19. package/dist/auth/create-tempest-auth.cjs +1 -1
  20. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  21. package/dist/auth/create-tempest-auth.js +192 -149
  22. package/dist/auth/create-tempest-auth.js.map +1 -1
  23. package/dist/auth/session-identity.cjs +2 -0
  24. package/dist/auth/session-identity.cjs.map +1 -0
  25. package/dist/auth/session-identity.js +12 -0
  26. package/dist/auth/session-identity.js.map +1 -0
  27. package/dist/br/boleto.cjs.map +1 -1
  28. package/dist/br/boleto.js.map +1 -1
  29. package/dist/components/AuthForm/AuthLinks.cjs +1 -1
  30. package/dist/components/AuthForm/AuthLinks.cjs.map +1 -1
  31. package/dist/components/AuthForm/AuthLinks.js +18 -15
  32. package/dist/components/AuthForm/AuthLinks.js.map +1 -1
  33. package/dist/components/AuthPage/AuthPage.cjs +1 -1
  34. package/dist/components/AuthPage/AuthPage.cjs.map +1 -1
  35. package/dist/components/AuthPage/AuthPage.js +58 -52
  36. package/dist/components/AuthPage/AuthPage.js.map +1 -1
  37. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +1 -1
  38. package/dist/components/Breadcrumbs/Breadcrumbs.cjs.map +1 -1
  39. package/dist/components/Breadcrumbs/Breadcrumbs.js +22 -21
  40. package/dist/components/Breadcrumbs/Breadcrumbs.js.map +1 -1
  41. package/dist/components/Button/Button.cjs +1 -1
  42. package/dist/components/Button/Button.cjs.map +1 -1
  43. package/dist/components/Button/Button.js +41 -40
  44. package/dist/components/Button/Button.js.map +1 -1
  45. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  46. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  47. package/dist/components/Chat/ChatAttachments.js +38 -38
  48. package/dist/components/Form/Form.cjs +1 -1
  49. package/dist/components/Form/Form.cjs.map +1 -1
  50. package/dist/components/Form/Form.js +30 -32
  51. package/dist/components/Form/Form.js.map +1 -1
  52. package/dist/components/Layout/Layout.cjs +1 -1
  53. package/dist/components/Layout/Layout.cjs.map +1 -1
  54. package/dist/components/Layout/Layout.js +25 -24
  55. package/dist/components/Layout/Layout.js.map +1 -1
  56. package/dist/components/Layout/gap.cjs +2 -0
  57. package/dist/components/Layout/gap.cjs.map +1 -0
  58. package/dist/components/Layout/gap.js +15 -0
  59. package/dist/components/Layout/gap.js.map +1 -0
  60. package/dist/components/NavigationMenu/NavigationMenu.cjs +1 -1
  61. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  62. package/dist/components/NavigationMenu/NavigationMenu.js +71 -70
  63. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  64. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  65. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  66. package/dist/components/Sidebar/SidebarEntry.cjs +1 -1
  67. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -1
  68. package/dist/components/Sidebar/SidebarEntry.js +30 -29
  69. package/dist/components/Sidebar/SidebarEntry.js.map +1 -1
  70. package/dist/data/create-data-provider.cjs +1 -1
  71. package/dist/data/create-data-provider.cjs.map +1 -1
  72. package/dist/data/create-data-provider.js +1 -2
  73. package/dist/data/create-data-provider.js.map +1 -1
  74. package/dist/forms/validate-form.cjs +1 -1
  75. package/dist/forms/validate-form.cjs.map +1 -1
  76. package/dist/forms/validate-form.js +6 -1
  77. package/dist/forms/validate-form.js.map +1 -1
  78. package/dist/forms/zod-resolver.cjs +1 -1
  79. package/dist/forms/zod-resolver.cjs.map +1 -1
  80. package/dist/forms/zod-resolver.js +8 -3
  81. package/dist/forms/zod-resolver.js.map +1 -1
  82. package/dist/http/api-client.cjs +1 -1
  83. package/dist/http/api-client.cjs.map +1 -1
  84. package/dist/http/api-client.js +90 -72
  85. package/dist/http/api-client.js.map +1 -1
  86. package/dist/http/credential-scope.cjs +1 -1
  87. package/dist/http/credential-scope.cjs.map +1 -1
  88. package/dist/http/credential-scope.js +33 -9
  89. package/dist/http/credential-scope.js.map +1 -1
  90. package/dist/http/presigned-post.cjs.map +1 -1
  91. package/dist/http/presigned-post.js.map +1 -1
  92. package/dist/http/resumable-upload.cjs +1 -1
  93. package/dist/http/resumable-upload.cjs.map +1 -1
  94. package/dist/http/resumable-upload.js +90 -84
  95. package/dist/http/resumable-upload.js.map +1 -1
  96. package/dist/http/upload-with-progress.cjs +1 -1
  97. package/dist/http/upload-with-progress.cjs.map +1 -1
  98. package/dist/http/upload-with-progress.js +8 -8
  99. package/dist/http/upload-with-progress.js.map +1 -1
  100. package/dist/oauth/GoogleSignIn.cjs +1 -1
  101. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  102. package/dist/oauth/GoogleSignIn.js +5 -4
  103. package/dist/oauth/GoogleSignIn.js.map +1 -1
  104. package/dist/offline/create-offline-sync.cjs +1 -1
  105. package/dist/offline/create-offline-sync.cjs.map +1 -1
  106. package/dist/offline/create-offline-sync.js +87 -69
  107. package/dist/offline/create-offline-sync.js.map +1 -1
  108. package/dist/query/offline-persistence.cjs +1 -1
  109. package/dist/query/offline-persistence.cjs.map +1 -1
  110. package/dist/query/offline-persistence.js +54 -44
  111. package/dist/query/offline-persistence.js.map +1 -1
  112. package/dist/sse/create-event-stream.cjs +1 -1
  113. package/dist/sse/create-event-stream.cjs.map +1 -1
  114. package/dist/sse/create-event-stream.js +48 -41
  115. package/dist/sse/create-event-stream.js.map +1 -1
  116. package/dist/sse/ticket-event-stream.cjs +1 -1
  117. package/dist/sse/ticket-event-stream.cjs.map +1 -1
  118. package/dist/sse/ticket-event-stream.js +48 -19
  119. package/dist/sse/ticket-event-stream.js.map +1 -1
  120. package/dist/sse/use-event-stream.cjs +1 -1
  121. package/dist/sse/use-event-stream.cjs.map +1 -1
  122. package/dist/sse/use-event-stream.js +19 -14
  123. package/dist/sse/use-event-stream.js.map +1 -1
  124. package/dist/sw/background-sync.cjs +1 -1
  125. package/dist/sw/background-sync.cjs.map +1 -1
  126. package/dist/sw/background-sync.js +10 -8
  127. package/dist/sw/background-sync.js.map +1 -1
  128. package/dist/sw/cache.cjs +1 -1
  129. package/dist/sw/cache.cjs.map +1 -1
  130. package/dist/sw/cache.js +36 -30
  131. package/dist/sw/cache.js.map +1 -1
  132. package/dist/sw/create-push-handler.cjs +1 -1
  133. package/dist/sw/create-push-handler.cjs.map +1 -1
  134. package/dist/sw/create-push-handler.js +20 -10
  135. package/dist/sw/create-push-handler.js.map +1 -1
  136. package/dist/sw.d.ts +44 -13
  137. package/dist/tabular/compact.cjs +1 -1
  138. package/dist/tabular/compact.cjs.map +1 -1
  139. package/dist/tabular/compact.js +35 -16
  140. package/dist/tabular/compact.js.map +1 -1
  141. package/dist/tabular/manifest.cjs +1 -1
  142. package/dist/tabular/manifest.cjs.map +1 -1
  143. package/dist/tabular/manifest.js +33 -15
  144. package/dist/tabular/manifest.js.map +1 -1
  145. package/dist/tabular.d.ts +11 -1
  146. package/dist/tempest-react-sdk.cjs +1 -1
  147. package/dist/tempest-react-sdk.d.ts +457 -68
  148. package/dist/tempest-react-sdk.js +224 -224
  149. package/dist/theme/initial-theme.cjs +4 -4
  150. package/dist/theme/initial-theme.cjs.map +1 -1
  151. package/dist/theme/initial-theme.js +8 -5
  152. package/dist/theme/initial-theme.js.map +1 -1
  153. package/dist/utils/compressed-storage.cjs +1 -1
  154. package/dist/utils/compressed-storage.cjs.map +1 -1
  155. package/dist/utils/compressed-storage.js +13 -7
  156. package/dist/utils/compressed-storage.js.map +1 -1
  157. package/dist/utils/objects.cjs +1 -1
  158. package/dist/utils/objects.cjs.map +1 -1
  159. package/dist/utils/objects.js +13 -7
  160. package/dist/utils/objects.js.map +1 -1
  161. package/dist/utils/safe-url.cjs +1 -1
  162. package/dist/utils/safe-url.cjs.map +1 -1
  163. package/dist/utils/safe-url.js +4 -1
  164. package/dist/utils/safe-url.js.map +1 -1
  165. package/dist/webrtc/peer-mesh.cjs +1 -1
  166. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  167. package/dist/webrtc/peer-mesh.js +7 -1
  168. package/dist/webrtc/peer-mesh.js.map +1 -1
  169. package/dist/ws/create-web-socket.cjs +1 -1
  170. package/dist/ws/create-web-socket.cjs.map +1 -1
  171. package/dist/ws/create-web-socket.js +77 -61
  172. package/dist/ws/create-web-socket.js.map +1 -1
  173. package/dist/ws/use-web-socket.cjs +1 -1
  174. package/dist/ws/use-web-socket.cjs.map +1 -1
  175. package/dist/ws/use-web-socket.js +4 -3
  176. package/dist/ws/use-web-socket.js.map +1 -1
  177. package/package.json +1 -1
  178. package/template-pwa/src/pwa.tsx +42 -0
  179. package/template-pwa/src/sw.ts +5 -4
@@ -1,11 +1,11 @@
1
- const e=require("../utils/storage.cjs");function t(t={}){let{storageKey:n=`tempest-theme`,defaultTheme:r=`system`}=t;if(typeof window>`u`)return r===`dark`?`dark`:`light`;let i=e.storage.getRaw(n)??r;return i===`dark`||i===`light`?i:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function n(e={}){let t=e.storageKey??`tempest-theme`,n=e.defaultTheme??`system`;return`
1
+ const e=require("../utils/storage.cjs");function t(t={}){let{storageKey:n=`tempest-theme`,defaultTheme:r=`system`}=t;if(typeof window>`u`)return r===`dark`?`dark`:`light`;let i=e.storage.getRaw(n)??r;return i===`dark`||i===`light`?i:window.matchMedia(`(prefers-color-scheme: dark)`).matches?`dark`:`light`}function n(e){return JSON.stringify(e).replace(/[<>&\u2028\u2029]/g,e=>`\\u${e.charCodeAt(0).toString(16).padStart(4,`0`)}`)}function r(e={}){let t=e.storageKey??`tempest-theme`,r=e.defaultTheme??`system`;return`
2
2
  (function(){try{
3
- var key=${JSON.stringify(t)};
4
- var def=${JSON.stringify(n)};
3
+ var key=${n(t)};
4
+ var def=${n(r)};
5
5
  var stored=localStorage.getItem(key);
6
6
  var mode=stored||def;
7
7
  var resolved=mode==="dark"||mode==="light"?mode:(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");
8
8
  document.documentElement.setAttribute("data-tempest-theme",resolved);
9
9
  }catch(e){}})();
10
- `.trim()}exports.getInitialTheme=t,exports.themeInitScript=n;
10
+ `.trim()}exports.getInitialTheme=t,exports.themeInitScript=r;
11
11
  //# sourceMappingURL=initial-theme.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"initial-theme.cjs","names":["storage","require_storage","getInitialTheme","options","defaultTheme","mode","storageKey","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${JSON.stringify(storageKey)};\n var def=${JSON.stringify(defaultTheme)};\n var stored=localStorage.getItem(key);\n var mode=stored||def;\n var resolved=mode===\"dark\"||mode===\"light\"?mode:(matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\");\n document.documentElement.setAttribute(\"data-tempest-theme\",resolved);\n}catch(e){}})();\n`.trim();\n}\n"],"mappings":"A,wCAeA,SAAgBE,EAAgB,EAAkC,CAAC,EAAkB,CACjF,GAAM,CAAE,aAAa,gBAAiB,eAAe,UAAaC,EAElE,GAAI,OAAO,OAAW,IAClB,OAAOC,IAAiB,OAAS,OAAS,QAI9C,IAAMC,EADSL,EAAAA,QAAQ,OAAOM,CACN,GAAUF,EAGlC,OAFIC,IAAS,QAAUA,IAAS,QAAgBA,EAEzC,OAAO,WAAW,8BAA8B,CAAC,CAAC,QAAU,OAAS,OAChF,CAUA,SAAgBE,EAAgB,EAAkC,CAAC,EAAW,CAC1E,IAAMD,EAAaH,EAAQ,YAAc,gBACnCC,EAAeD,EAAQ,cAAgB,SAC7C,MAAO;;YAEC,KAAK,UAAUG,CAAU,EAAE;YAC3B,KAAK,UAAUF,CAAY,EAAE;;;;;;EAMvC,KAAK,CACP,C"}
1
+ {"version":3,"file":"initial-theme.cjs","names":["storage","require_storage","getInitialTheme","options","defaultTheme","mode","storageKey","scriptLiteral","value","char","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * A string as a JavaScript literal that is also safe inside an HTML `<script>`.\n *\n * `JSON.stringify` produces a valid literal but leaves `<`, `>`, `&` and the\n * line separators U+2028/U+2029 as they are, and the HTML parser ends a script\n * element at the first `</script` whatever JavaScript thinks of it: a\n * `storageKey` of `\"</script><script>alert(1)</script>\"` closed the snippet and\n * ran the second script. Each of those characters is written as its `\\uXXXX`\n * escape, which JavaScript reads back to the same string.\n *\n * @param value - The option value to embed.\n * @returns The quoted, escaped literal.\n */\nfunction scriptLiteral(value: string): string {\n return JSON.stringify(value).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * Both options are embedded through an escaper that keeps them inside the\n * `<script>` element whatever they contain, so the snippet is safe to inline\n * even when the storage key comes from configuration.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${scriptLiteral(storageKey)};\n var def=${scriptLiteral(defaultTheme)};\n var stored=localStorage.getItem(key);\n var mode=stored||def;\n var resolved=mode===\"dark\"||mode===\"light\"?mode:(matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\");\n document.documentElement.setAttribute(\"data-tempest-theme\",resolved);\n}catch(e){}})();\n`.trim();\n}\n"],"mappings":"A,wCAeA,SAAgBE,EAAgB,EAAkC,CAAC,EAAkB,CACjF,GAAM,CAAE,aAAa,gBAAiB,eAAe,UAAaC,EAElE,GAAI,OAAO,OAAW,IAClB,OAAOC,IAAiB,OAAS,OAAS,QAI9C,IAAMC,EADSL,EAAAA,QAAQ,OAAOM,CACN,GAAUF,EAGlC,OAFIC,IAAS,QAAUA,IAAS,QAAgBA,EAEzC,OAAO,WAAW,8BAA8B,CAAC,CAAC,QAAU,OAAS,OAChF,CAeA,SAASE,EAAc,EAAuB,CAC1C,OAAO,KAAK,UAAUC,CAAK,CAAC,CAAC,QACzB,qBACC,GAAS,MAAMC,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,GACnE,CACJ,CAcA,SAAgBC,EAAgB,EAAkC,CAAC,EAAW,CAC1E,IAAMJ,EAAaH,EAAQ,YAAc,gBACnCC,EAAeD,EAAQ,cAAgB,SAC7C,MAAO;;YAECI,EAAcD,CAAU,EAAE;YAC1BC,EAAcH,CAAY,EAAE;;;;;;EAMtC,KAAK,CACP,C"}
@@ -6,12 +6,15 @@ function t(t = {}) {
6
6
  let i = e.getRaw(n) ?? r;
7
7
  return i === "dark" || i === "light" ? i : window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
8
8
  }
9
- function n(e = {}) {
10
- let t = e.storageKey ?? "tempest-theme", n = e.defaultTheme ?? "system";
9
+ function n(e) {
10
+ return JSON.stringify(e).replace(/[<>&\u2028\u2029]/g, (e) => `\\u${e.charCodeAt(0).toString(16).padStart(4, "0")}`);
11
+ }
12
+ function r(e = {}) {
13
+ let t = e.storageKey ?? "tempest-theme", r = e.defaultTheme ?? "system";
11
14
  return `
12
15
  (function(){try{
13
- var key=${JSON.stringify(t)};
14
- var def=${JSON.stringify(n)};
16
+ var key=${n(t)};
17
+ var def=${n(r)};
15
18
  var stored=localStorage.getItem(key);
16
19
  var mode=stored||def;
17
20
  var resolved=mode==="dark"||mode==="light"?mode:(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");
@@ -20,6 +23,6 @@ function n(e = {}) {
20
23
  `.trim();
21
24
  }
22
25
  //#endregion
23
- export { t as getInitialTheme, n as themeInitScript };
26
+ export { t as getInitialTheme, r as themeInitScript };
24
27
 
25
28
  //# sourceMappingURL=initial-theme.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"initial-theme.js","names":["getInitialTheme","options","defaultTheme","mode","storage","storageKey","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${JSON.stringify(storageKey)};\n var def=${JSON.stringify(defaultTheme)};\n var stored=localStorage.getItem(key);\n var mode=stored||def;\n var resolved=mode===\"dark\"||mode===\"light\"?mode:(matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\");\n document.documentElement.setAttribute(\"data-tempest-theme\",resolved);\n}catch(e){}})();\n`.trim();\n}\n"],"mappings":";;AAeA,SAAgBA,EAAgB,IAAkC,CAAC,GAAkB;CACjF,IAAM,EAAE,gBAAa,iBAAiB,kBAAe,aAAaC;CAElE,IAAI,OAAO,SAAW,KAClB,OAAOC,MAAiB,SAAS,SAAS;CAI9C,IAAMC,IADSC,EAAQ,OAAOC,CACN,KAAUH;CAGlC,OAFIC,MAAS,UAAUA,MAAS,UAAgBA,IAEzC,OAAO,WAAW,8BAA8B,CAAC,CAAC,UAAU,SAAS;AAChF;AAUA,SAAgBG,EAAgB,IAAkC,CAAC,GAAW;CAC1E,IAAMD,IAAaJ,EAAQ,cAAc,iBACnCC,IAAeD,EAAQ,gBAAgB;CAC7C,OAAO;;YAEC,KAAK,UAAUI,CAAU,EAAE;YAC3B,KAAK,UAAUH,CAAY,EAAE;;;;;;EAMvC,KAAK;AACP"}
1
+ {"version":3,"file":"initial-theme.js","names":["getInitialTheme","options","defaultTheme","mode","storage","storageKey","scriptLiteral","value","char","themeInitScript"],"sources":["../../src/theme/initial-theme.ts"],"sourcesContent":["import { storage } from \"@/utils/storage\";\nimport type { ResolvedTheme, ThemeMode } from \"./types\";\n\nexport interface GetInitialThemeOptions {\n /** localStorage key. Default: `\"tempest-theme\"`. */\n storageKey?: string;\n /** Theme used when nothing is stored and the user has no system preference. */\n defaultTheme?: ThemeMode;\n}\n\n/**\n * Inline-safe theme resolution intended for early bootstrap (e.g. inside the\n * `<head>` script that prevents the flash of incorrect theme). Reads from\n * localStorage and falls back to `prefers-color-scheme`.\n */\nexport function getInitialTheme(options: GetInitialThemeOptions = {}): ResolvedTheme {\n const { storageKey = \"tempest-theme\", defaultTheme = \"system\" } = options;\n\n if (typeof window === \"undefined\") {\n return defaultTheme === \"dark\" ? \"dark\" : \"light\";\n }\n\n const stored = storage.getRaw(storageKey) as ThemeMode | null;\n const mode: ThemeMode = stored ?? defaultTheme;\n if (mode === \"dark\" || mode === \"light\") return mode;\n\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\n/**\n * A string as a JavaScript literal that is also safe inside an HTML `<script>`.\n *\n * `JSON.stringify` produces a valid literal but leaves `<`, `>`, `&` and the\n * line separators U+2028/U+2029 as they are, and the HTML parser ends a script\n * element at the first `</script` whatever JavaScript thinks of it: a\n * `storageKey` of `\"</script><script>alert(1)</script>\"` closed the snippet and\n * ran the second script. Each of those characters is written as its `\\uXXXX`\n * escape, which JavaScript reads back to the same string.\n *\n * @param value - The option value to embed.\n * @returns The quoted, escaped literal.\n */\nfunction scriptLiteral(value: string): string {\n return JSON.stringify(value).replace(\n /[<>&\\u2028\\u2029]/g,\n (char) => `\\\\u${char.charCodeAt(0).toString(16).padStart(4, \"0\")}`,\n );\n}\n\n/**\n * Plain HTML snippet that sets `data-tempest-theme` on `<html>` before React\n * hydrates. Inline this in `<head>` to avoid a flash of the wrong theme on\n * first paint.\n *\n * Both options are embedded through an escaper that keeps them inside the\n * `<script>` element whatever they contain, so the snippet is safe to inline\n * even when the storage key comes from configuration.\n *\n * @example\n * <script dangerouslySetInnerHTML={{ __html: themeInitScript() }} />\n */\nexport function themeInitScript(options: GetInitialThemeOptions = {}): string {\n const storageKey = options.storageKey ?? \"tempest-theme\";\n const defaultTheme = options.defaultTheme ?? \"system\";\n return `\n(function(){try{\n var key=${scriptLiteral(storageKey)};\n var def=${scriptLiteral(defaultTheme)};\n var stored=localStorage.getItem(key);\n var mode=stored||def;\n var resolved=mode===\"dark\"||mode===\"light\"?mode:(matchMedia(\"(prefers-color-scheme: dark)\").matches?\"dark\":\"light\");\n document.documentElement.setAttribute(\"data-tempest-theme\",resolved);\n}catch(e){}})();\n`.trim();\n}\n"],"mappings":";;AAeA,SAAgBA,EAAgB,IAAkC,CAAC,GAAkB;CACjF,IAAM,EAAE,gBAAa,iBAAiB,kBAAe,aAAaC;CAElE,IAAI,OAAO,SAAW,KAClB,OAAOC,MAAiB,SAAS,SAAS;CAI9C,IAAMC,IADSC,EAAQ,OAAOC,CACN,KAAUH;CAGlC,OAFIC,MAAS,UAAUA,MAAS,UAAgBA,IAEzC,OAAO,WAAW,8BAA8B,CAAC,CAAC,UAAU,SAAS;AAChF;AAeA,SAASG,EAAc,GAAuB;CAC1C,OAAO,KAAK,UAAUC,CAAK,CAAC,CAAC,QACzB,uBACC,MAAS,MAAMC,EAAK,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,GACnE;AACJ;AAcA,SAAgBC,EAAgB,IAAkC,CAAC,GAAW;CAC1E,IAAMJ,IAAaJ,EAAQ,cAAc,iBACnCC,IAAeD,EAAQ,gBAAgB;CAC7C,OAAO;;YAECK,EAAcD,CAAU,EAAE;YAC1BC,EAAcJ,CAAY,EAAE;;;;;;EAMtC,KAAK;AACP"}
@@ -1,2 +1,2 @@
1
- const e=require("./storage.cjs"),t=require("./base64.cjs");let n=require("fflate");var r=`~tgz1:`;function i(e){let i=JSON.stringify(e),a=(0,n.gzipSync)(new TextEncoder().encode(i));return`${r}${t.bytesToBase64(a)}`}function a(e){if(!e.startsWith(r))return JSON.parse(e);let i=(0,n.gunzipSync)(t.base64ToBytes(e.slice(6)));return JSON.parse(new TextDecoder().decode(i))}var o={serialize:i,deserialize:a},s=e.createJsonStorage(o);exports.compressToString=i,exports.compressedStorage=s,exports.compressedStorageCodec=o,exports.decompressFromString=a;
1
+ const e=require("./storage.cjs"),t=require("./base64.cjs");let n=require("fflate");var r=`~tgz1:`;function i(e){let i=JSON.stringify(e),a=(0,n.gzipSync)(new TextEncoder().encode(i));return`${r}${t.bytesToBase64(a)}`}var a=67108864;function o(e){if(!e.startsWith(r))return JSON.parse(e);let i=t.base64ToBytes(e.slice(6));if(i.byteLength>=4){let e=new DataView(i.buffer,i.byteOffset,i.byteLength).getUint32(i.byteLength-4,!0);if(e>a)throw RangeError(`Compressed value declares ${e} bytes once inflated, above the ${a}-byte limit.`)}let o=(0,n.gunzipSync)(i);return JSON.parse(new TextDecoder().decode(o))}var s={serialize:i,deserialize:o},c=e.createJsonStorage(s);exports.compressToString=i,exports.compressedStorage=c,exports.compressedStorageCodec=s,exports.decompressFromString=o;
2
2
  //# sourceMappingURL=compressed-storage.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"compressed-storage.cjs","names":["gzipSync","bytesToBase64","gunzipSync","base64ToBytes","createJsonStorage","require_storage","require_base64","fflate","MARKER","compressToString","json","value","bytes","decompressFromString","raw","compressedStorageCodec","compressedStorage"],"sources":["../../src/utils/compressed-storage.ts"],"sourcesContent":["import { gunzipSync, gzipSync } from \"fflate\";\n\nimport { base64ToBytes, bytesToBase64 } from \"./base64\";\n\nimport { createJsonStorage, type JsonStorage, type StorageCodec } from \"./storage\";\n\n/**\n * Gzip-backed `localStorage`, for payloads that would otherwise eat the quota.\n *\n * Kept in its own module rather than folded into {@link storage} so that\n * importing the plain typed wrapper never drags `fflate` into the bundle.\n *\n * @tempest-limits empty-catch — every operation here is best-effort by\n * contract, exactly like {@link storage}. `localStorage` throws on quota\n * exhaustion, in Safari private mode, and when a cross-origin frame has storage\n * blocked; a caller persisting state has no recovery to run and no message to\n * show, so the write is dropped and the value stays in memory for the session.\n */\n\n/**\n * Prefix stamped on every value this module writes.\n *\n * It makes the format self-describing, which is what lets a read fall back to\n * plain JSON: a key that predates compression — or that a degraded write stored\n * uncompressed — is still readable, so turning compression on for an existing\n * key does not orphan the data already there. The character also cannot open a\n * JSON document, so the two cases can never be confused.\n */\nconst MARKER = \"~tgz1:\";\n\n/**\n * Serialize a value to a gzipped, base64 string carrying the format marker.\n *\n * Base64 costs a third more characters than the raw compressed bytes, and\n * `localStorage` bills two bytes per character on top of that. Packing the\n * bytes into UTF-16 code units directly would be denser, but lone surrogates\n * survive neither every storage implementation nor a JSON round-trip, and a\n * save that decodes to garbage is far worse than one that is bigger. Even with\n * that overhead a typical JSON document lands well under a third of its\n * uncompressed size.\n *\n * @typeParam T - The value being stored.\n * @param value - Any JSON-serializable value.\n * @returns The encoded string, ready for `localStorage`.\n */\nexport function compressToString<T>(value: T): string {\n const json = JSON.stringify(value);\n const bytes = gzipSync(new TextEncoder().encode(json));\n return `${MARKER}${bytesToBase64(bytes)}`;\n}\n\n/**\n * Decode a string produced by {@link compressToString}.\n *\n * A string without the marker is parsed as plain JSON, so values written before\n * compression was enabled — or by a write that fell back after `gzipSync`\n * failed — still read back.\n *\n * @typeParam T - The expected value shape.\n * @param raw - The stored string.\n * @returns The decoded value.\n * @throws If the payload is neither valid compressed data nor valid JSON.\n */\nexport function decompressFromString<T>(raw: string): T {\n if (!raw.startsWith(MARKER)) return JSON.parse(raw) as T;\n const bytes = gunzipSync(base64ToBytes(raw.slice(MARKER.length)));\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Codec pair for {@link useLocalStorage}, so a compressed key gets the hook's\n * cross-tab sync and SSR guard for free.\n *\n * @example\n * const [save, setSave] = useLocalStorage(\"save\", EMPTY_SAVE, compressedStorageCodec);\n */\nexport const compressedStorageCodec: StorageCodec = {\n serialize: compressToString,\n deserialize: decompressFromString,\n};\n\n/**\n * Typed `localStorage` wrapper that gzips what it writes.\n *\n * {@link createJsonStorage} with {@link compressedStorageCodec}, so it is the\n * same implementation as {@link storage} and genuinely interchangeable with it —\n * `get`, `set` and `remove`, differing only in how the value is encoded. It used\n * to be a hand-written copy that had no `remove`, which made that promise false\n * for anybody who took the docstring at its word.\n *\n * When compression itself fails the value is written as plain JSON rather than\n * dropped: a slightly larger record still loads, an absent one does not. Only a\n * storage-level failure — quota, blocked storage — loses the write.\n */\nexport const compressedStorage: JsonStorage = createJsonStorage(compressedStorageCodec);\n"],"mappings":"A,mFA4BA,IAAMQ,EAAS,SAiBf,SAAgBC,EAAoB,EAAkB,CAClD,IAAMC,EAAO,KAAK,UAAUC,CAAK,EAC3BC,GAAA,EAAQZ,EAAAA,SAAAA,CAAS,IAAI,YAAY,CAAC,CAAC,OAAOU,CAAI,CAAC,EACrD,MAAO,GAAGF,IAASP,EAAAA,cAAcW,CAAK,GAC1C,CAcA,SAAgBC,EAAwB,EAAgB,CACpD,GAAI,CAACC,EAAI,WAAWN,CAAM,EAAG,OAAO,KAAK,MAAMM,CAAG,EAClD,IAAMF,GAAA,EAAQV,EAAAA,WAAAA,CAAWC,EAAAA,cAAcW,EAAI,MAAM,CAAa,CAAC,CAAC,EAChE,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAOF,CAAK,CAAC,CACrD,CASA,IAAaG,EAAuC,CAChD,UAAWN,EACX,YAAaI,CACjB,EAeaG,EAAiCZ,EAAAA,kBAAkBW,CAAsB,E"}
1
+ {"version":3,"file":"compressed-storage.cjs","names":["gzipSync","bytesToBase64","base64ToBytes","gunzipSync","createJsonStorage","require_storage","require_base64","fflate","MARKER","compressToString","json","value","bytes","MAX_DECOMPRESSED_BYTES","decompressFromString","raw","compressed","size","compressedStorageCodec","compressedStorage"],"sources":["../../src/utils/compressed-storage.ts"],"sourcesContent":["import { gunzipSync, gzipSync } from \"fflate\";\n\nimport { base64ToBytes, bytesToBase64 } from \"./base64\";\n\nimport { createJsonStorage, type JsonStorage, type StorageCodec } from \"./storage\";\n\n/**\n * Gzip-backed `localStorage`, for payloads that would otherwise eat the quota.\n *\n * Kept in its own module rather than folded into {@link storage} so that\n * importing the plain typed wrapper never drags `fflate` into the bundle.\n *\n * @tempest-limits empty-catch — every operation here is best-effort by\n * contract, exactly like {@link storage}. `localStorage` throws on quota\n * exhaustion, in Safari private mode, and when a cross-origin frame has storage\n * blocked; a caller persisting state has no recovery to run and no message to\n * show, so the write is dropped and the value stays in memory for the session.\n */\n\n/**\n * Prefix stamped on every value this module writes.\n *\n * It makes the format self-describing, which is what lets a read fall back to\n * plain JSON: a key that predates compression — or that a degraded write stored\n * uncompressed — is still readable, so turning compression on for an existing\n * key does not orphan the data already there. The character also cannot open a\n * JSON document, so the two cases can never be confused.\n */\nconst MARKER = \"~tgz1:\";\n\n/**\n * Serialize a value to a gzipped, base64 string carrying the format marker.\n *\n * Base64 costs a third more characters than the raw compressed bytes, and\n * `localStorage` bills two bytes per character on top of that. Packing the\n * bytes into UTF-16 code units directly would be denser, but lone surrogates\n * survive neither every storage implementation nor a JSON round-trip, and a\n * save that decodes to garbage is far worse than one that is bigger. Even with\n * that overhead a typical JSON document lands well under a third of its\n * uncompressed size.\n *\n * @typeParam T - The value being stored.\n * @param value - Any JSON-serializable value.\n * @returns The encoded string, ready for `localStorage`.\n */\nexport function compressToString<T>(value: T): string {\n const json = JSON.stringify(value);\n const bytes = gzipSync(new TextEncoder().encode(json));\n return `${MARKER}${bytesToBase64(bytes)}`;\n}\n\n/**\n * Largest decompressed size {@link decompressFromString} will allocate: 64 MiB.\n *\n * Deflate reaches ~1000:1 on repetitive input (measured with fflate: 20 MB of\n * `\"A\"` gzips to 19 551 bytes, ratio 1023), so the ~5 MB a `localStorage` origin\n * holds could ask for gigabytes on read. A legitimate value is far below this:\n * it had to pass through `JSON.stringify` and `gzipSync` in the same tab first.\n */\nconst MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;\n\n/**\n * Decode a string produced by {@link compressToString}.\n *\n * A string without the marker is parsed as plain JSON, so values written before\n * compression was enabled — or by a write that fell back after `gzipSync`\n * failed — still read back.\n *\n * The output size is bounded before anything is inflated. fflate sizes its\n * output buffer from the gzip trailer (`ISIZE`) and never grows it — measured: a\n * 1 MB payload whose trailer claims 10 bytes inflates to exactly 10 — so\n * refusing a trailer above {@link MAX_DECOMPRESSED_BYTES} bounds the allocation,\n * whatever the trailer says about the real data. A corrupted or hostile value\n * fails fast instead of taking the tab's memory.\n *\n * @typeParam T - The expected value shape.\n * @param raw - The stored string.\n * @returns The decoded value.\n * @throws If the payload is neither valid compressed data nor valid JSON, or\n * declares a decompressed size above 64 MiB.\n */\nexport function decompressFromString<T>(raw: string): T {\n if (!raw.startsWith(MARKER)) return JSON.parse(raw) as T;\n const compressed = base64ToBytes(raw.slice(MARKER.length));\n if (compressed.byteLength >= 4) {\n const trailer = new DataView(\n compressed.buffer,\n compressed.byteOffset,\n compressed.byteLength,\n );\n const size = trailer.getUint32(compressed.byteLength - 4, true);\n if (size > MAX_DECOMPRESSED_BYTES) {\n throw new RangeError(\n `Compressed value declares ${size} bytes once inflated, above the ` +\n `${MAX_DECOMPRESSED_BYTES}-byte limit.`,\n );\n }\n }\n const bytes = gunzipSync(compressed);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Codec pair for {@link useLocalStorage}, so a compressed key gets the hook's\n * cross-tab sync and SSR guard for free.\n *\n * @example\n * const [save, setSave] = useLocalStorage(\"save\", EMPTY_SAVE, compressedStorageCodec);\n */\nexport const compressedStorageCodec: StorageCodec = {\n serialize: compressToString,\n deserialize: decompressFromString,\n};\n\n/**\n * Typed `localStorage` wrapper that gzips what it writes.\n *\n * {@link createJsonStorage} with {@link compressedStorageCodec}, so it is the\n * same implementation as {@link storage} and genuinely interchangeable with it —\n * `get`, `set` and `remove`, differing only in how the value is encoded. It used\n * to be a hand-written copy that had no `remove`, which made that promise false\n * for anybody who took the docstring at its word.\n *\n * When compression itself fails the value is written as plain JSON rather than\n * dropped: a slightly larger record still loads, an absent one does not. Only a\n * storage-level failure — quota, blocked storage — loses the write.\n */\nexport const compressedStorage: JsonStorage = createJsonStorage(compressedStorageCodec);\n"],"mappings":"A,mFA4BA,IAAMQ,EAAS,SAiBf,SAAgBC,EAAoB,EAAkB,CAClD,IAAMC,EAAO,KAAK,UAAUC,CAAK,EAC3BC,GAAA,EAAQZ,EAAAA,SAAAA,CAAS,IAAI,YAAY,CAAC,CAAC,OAAOU,CAAI,CAAC,EACrD,MAAO,GAAGF,IAASP,EAAAA,cAAcW,CAAK,GAC1C,CAUA,IAAMC,EAAyB,SAsB/B,SAAgBC,EAAwB,EAAgB,CACpD,GAAI,CAACC,EAAI,WAAWP,CAAM,EAAG,OAAO,KAAK,MAAMO,CAAG,EAClD,IAAMC,EAAad,EAAAA,cAAca,EAAI,MAAM,CAAa,CAAC,EACzD,GAAIC,EAAW,YAAc,EAAG,CAM5B,IAAMC,EAAO,IALO,SAChBD,EAAW,OACXA,EAAW,WACXA,EAAW,UAEF,CAAA,CAAQ,UAAUA,EAAW,WAAa,EAAG,EAAI,EAC9D,GAAIC,EAAOJ,EACP,MAAU,WACN,6BAA6BI,EAAK,kCAC3BJ,EAAuB,aAClC,CAER,CACA,IAAMD,GAAA,EAAQT,EAAAA,WAAAA,CAAWa,CAAU,EACnC,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAOJ,CAAK,CAAC,CACrD,CASA,IAAaM,EAAuC,CAChD,UAAWT,EACX,YAAaK,CACjB,EAeaK,EAAiCf,EAAAA,kBAAkBc,CAAsB,E"}
@@ -7,16 +7,22 @@ function o(e) {
7
7
  let t = JSON.stringify(e), r = i(new TextEncoder().encode(t));
8
8
  return `${a}${n(r)}`;
9
9
  }
10
- function s(e) {
10
+ var s = 67108864;
11
+ function c(e) {
11
12
  if (!e.startsWith(a)) return JSON.parse(e);
12
- let n = r(t(e.slice(6)));
13
- return JSON.parse(new TextDecoder().decode(n));
13
+ let n = t(e.slice(6));
14
+ if (n.byteLength >= 4) {
15
+ let e = new DataView(n.buffer, n.byteOffset, n.byteLength).getUint32(n.byteLength - 4, !0);
16
+ if (e > s) throw RangeError(`Compressed value declares ${e} bytes once inflated, above the ${s}-byte limit.`);
17
+ }
18
+ let i = r(n);
19
+ return JSON.parse(new TextDecoder().decode(i));
14
20
  }
15
- var c = {
21
+ var l = {
16
22
  serialize: o,
17
- deserialize: s
18
- }, l = e(c);
23
+ deserialize: c
24
+ }, u = e(l);
19
25
  //#endregion
20
- export { o as compressToString, l as compressedStorage, c as compressedStorageCodec, s as decompressFromString };
26
+ export { o as compressToString, u as compressedStorage, l as compressedStorageCodec, c as decompressFromString };
21
27
 
22
28
  //# sourceMappingURL=compressed-storage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"compressed-storage.js","names":["MARKER","compressToString","json","value","bytes","gzipSync","bytesToBase64","decompressFromString","raw","gunzipSync","base64ToBytes","compressedStorageCodec","compressedStorage","createJsonStorage"],"sources":["../../src/utils/compressed-storage.ts"],"sourcesContent":["import { gunzipSync, gzipSync } from \"fflate\";\n\nimport { base64ToBytes, bytesToBase64 } from \"./base64\";\n\nimport { createJsonStorage, type JsonStorage, type StorageCodec } from \"./storage\";\n\n/**\n * Gzip-backed `localStorage`, for payloads that would otherwise eat the quota.\n *\n * Kept in its own module rather than folded into {@link storage} so that\n * importing the plain typed wrapper never drags `fflate` into the bundle.\n *\n * @tempest-limits empty-catch — every operation here is best-effort by\n * contract, exactly like {@link storage}. `localStorage` throws on quota\n * exhaustion, in Safari private mode, and when a cross-origin frame has storage\n * blocked; a caller persisting state has no recovery to run and no message to\n * show, so the write is dropped and the value stays in memory for the session.\n */\n\n/**\n * Prefix stamped on every value this module writes.\n *\n * It makes the format self-describing, which is what lets a read fall back to\n * plain JSON: a key that predates compression — or that a degraded write stored\n * uncompressed — is still readable, so turning compression on for an existing\n * key does not orphan the data already there. The character also cannot open a\n * JSON document, so the two cases can never be confused.\n */\nconst MARKER = \"~tgz1:\";\n\n/**\n * Serialize a value to a gzipped, base64 string carrying the format marker.\n *\n * Base64 costs a third more characters than the raw compressed bytes, and\n * `localStorage` bills two bytes per character on top of that. Packing the\n * bytes into UTF-16 code units directly would be denser, but lone surrogates\n * survive neither every storage implementation nor a JSON round-trip, and a\n * save that decodes to garbage is far worse than one that is bigger. Even with\n * that overhead a typical JSON document lands well under a third of its\n * uncompressed size.\n *\n * @typeParam T - The value being stored.\n * @param value - Any JSON-serializable value.\n * @returns The encoded string, ready for `localStorage`.\n */\nexport function compressToString<T>(value: T): string {\n const json = JSON.stringify(value);\n const bytes = gzipSync(new TextEncoder().encode(json));\n return `${MARKER}${bytesToBase64(bytes)}`;\n}\n\n/**\n * Decode a string produced by {@link compressToString}.\n *\n * A string without the marker is parsed as plain JSON, so values written before\n * compression was enabled — or by a write that fell back after `gzipSync`\n * failed — still read back.\n *\n * @typeParam T - The expected value shape.\n * @param raw - The stored string.\n * @returns The decoded value.\n * @throws If the payload is neither valid compressed data nor valid JSON.\n */\nexport function decompressFromString<T>(raw: string): T {\n if (!raw.startsWith(MARKER)) return JSON.parse(raw) as T;\n const bytes = gunzipSync(base64ToBytes(raw.slice(MARKER.length)));\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Codec pair for {@link useLocalStorage}, so a compressed key gets the hook's\n * cross-tab sync and SSR guard for free.\n *\n * @example\n * const [save, setSave] = useLocalStorage(\"save\", EMPTY_SAVE, compressedStorageCodec);\n */\nexport const compressedStorageCodec: StorageCodec = {\n serialize: compressToString,\n deserialize: decompressFromString,\n};\n\n/**\n * Typed `localStorage` wrapper that gzips what it writes.\n *\n * {@link createJsonStorage} with {@link compressedStorageCodec}, so it is the\n * same implementation as {@link storage} and genuinely interchangeable with it —\n * `get`, `set` and `remove`, differing only in how the value is encoded. It used\n * to be a hand-written copy that had no `remove`, which made that promise false\n * for anybody who took the docstring at its word.\n *\n * When compression itself fails the value is written as plain JSON rather than\n * dropped: a slightly larger record still loads, an absent one does not. Only a\n * storage-level failure — quota, blocked storage — loses the write.\n */\nexport const compressedStorage: JsonStorage = createJsonStorage(compressedStorageCodec);\n"],"mappings":";;;;AA4BA,IAAMA,IAAS;AAiBf,SAAgBC,EAAoB,GAAkB;CAClD,IAAMC,IAAO,KAAK,UAAUC,CAAK,GAC3BC,IAAQC,EAAS,IAAI,YAAY,CAAC,CAAC,OAAOH,CAAI,CAAC;CACrD,OAAO,GAAGF,IAASM,EAAcF,CAAK;AAC1C;AAcA,SAAgBG,EAAwB,GAAgB;CACpD,IAAI,CAACC,EAAI,WAAWR,CAAM,GAAG,OAAO,KAAK,MAAMQ,CAAG;CAClD,IAAMJ,IAAQK,EAAWC,EAAcF,EAAI,MAAM,CAAa,CAAC,CAAC;CAChE,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAOJ,CAAK,CAAC;AACrD;AASA,IAAaO,IAAuC;CAChD,WAAWV;CACX,aAAaM;AACjB,GAeaK,IAAiCC,EAAkBF,CAAsB"}
1
+ {"version":3,"file":"compressed-storage.js","names":["MARKER","compressToString","json","value","bytes","gzipSync","bytesToBase64","MAX_DECOMPRESSED_BYTES","decompressFromString","raw","compressed","base64ToBytes","size","gunzipSync","compressedStorageCodec","compressedStorage","createJsonStorage"],"sources":["../../src/utils/compressed-storage.ts"],"sourcesContent":["import { gunzipSync, gzipSync } from \"fflate\";\n\nimport { base64ToBytes, bytesToBase64 } from \"./base64\";\n\nimport { createJsonStorage, type JsonStorage, type StorageCodec } from \"./storage\";\n\n/**\n * Gzip-backed `localStorage`, for payloads that would otherwise eat the quota.\n *\n * Kept in its own module rather than folded into {@link storage} so that\n * importing the plain typed wrapper never drags `fflate` into the bundle.\n *\n * @tempest-limits empty-catch — every operation here is best-effort by\n * contract, exactly like {@link storage}. `localStorage` throws on quota\n * exhaustion, in Safari private mode, and when a cross-origin frame has storage\n * blocked; a caller persisting state has no recovery to run and no message to\n * show, so the write is dropped and the value stays in memory for the session.\n */\n\n/**\n * Prefix stamped on every value this module writes.\n *\n * It makes the format self-describing, which is what lets a read fall back to\n * plain JSON: a key that predates compression — or that a degraded write stored\n * uncompressed — is still readable, so turning compression on for an existing\n * key does not orphan the data already there. The character also cannot open a\n * JSON document, so the two cases can never be confused.\n */\nconst MARKER = \"~tgz1:\";\n\n/**\n * Serialize a value to a gzipped, base64 string carrying the format marker.\n *\n * Base64 costs a third more characters than the raw compressed bytes, and\n * `localStorage` bills two bytes per character on top of that. Packing the\n * bytes into UTF-16 code units directly would be denser, but lone surrogates\n * survive neither every storage implementation nor a JSON round-trip, and a\n * save that decodes to garbage is far worse than one that is bigger. Even with\n * that overhead a typical JSON document lands well under a third of its\n * uncompressed size.\n *\n * @typeParam T - The value being stored.\n * @param value - Any JSON-serializable value.\n * @returns The encoded string, ready for `localStorage`.\n */\nexport function compressToString<T>(value: T): string {\n const json = JSON.stringify(value);\n const bytes = gzipSync(new TextEncoder().encode(json));\n return `${MARKER}${bytesToBase64(bytes)}`;\n}\n\n/**\n * Largest decompressed size {@link decompressFromString} will allocate: 64 MiB.\n *\n * Deflate reaches ~1000:1 on repetitive input (measured with fflate: 20 MB of\n * `\"A\"` gzips to 19 551 bytes, ratio 1023), so the ~5 MB a `localStorage` origin\n * holds could ask for gigabytes on read. A legitimate value is far below this:\n * it had to pass through `JSON.stringify` and `gzipSync` in the same tab first.\n */\nconst MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;\n\n/**\n * Decode a string produced by {@link compressToString}.\n *\n * A string without the marker is parsed as plain JSON, so values written before\n * compression was enabled — or by a write that fell back after `gzipSync`\n * failed — still read back.\n *\n * The output size is bounded before anything is inflated. fflate sizes its\n * output buffer from the gzip trailer (`ISIZE`) and never grows it — measured: a\n * 1 MB payload whose trailer claims 10 bytes inflates to exactly 10 — so\n * refusing a trailer above {@link MAX_DECOMPRESSED_BYTES} bounds the allocation,\n * whatever the trailer says about the real data. A corrupted or hostile value\n * fails fast instead of taking the tab's memory.\n *\n * @typeParam T - The expected value shape.\n * @param raw - The stored string.\n * @returns The decoded value.\n * @throws If the payload is neither valid compressed data nor valid JSON, or\n * declares a decompressed size above 64 MiB.\n */\nexport function decompressFromString<T>(raw: string): T {\n if (!raw.startsWith(MARKER)) return JSON.parse(raw) as T;\n const compressed = base64ToBytes(raw.slice(MARKER.length));\n if (compressed.byteLength >= 4) {\n const trailer = new DataView(\n compressed.buffer,\n compressed.byteOffset,\n compressed.byteLength,\n );\n const size = trailer.getUint32(compressed.byteLength - 4, true);\n if (size > MAX_DECOMPRESSED_BYTES) {\n throw new RangeError(\n `Compressed value declares ${size} bytes once inflated, above the ` +\n `${MAX_DECOMPRESSED_BYTES}-byte limit.`,\n );\n }\n }\n const bytes = gunzipSync(compressed);\n return JSON.parse(new TextDecoder().decode(bytes)) as T;\n}\n\n/**\n * Codec pair for {@link useLocalStorage}, so a compressed key gets the hook's\n * cross-tab sync and SSR guard for free.\n *\n * @example\n * const [save, setSave] = useLocalStorage(\"save\", EMPTY_SAVE, compressedStorageCodec);\n */\nexport const compressedStorageCodec: StorageCodec = {\n serialize: compressToString,\n deserialize: decompressFromString,\n};\n\n/**\n * Typed `localStorage` wrapper that gzips what it writes.\n *\n * {@link createJsonStorage} with {@link compressedStorageCodec}, so it is the\n * same implementation as {@link storage} and genuinely interchangeable with it —\n * `get`, `set` and `remove`, differing only in how the value is encoded. It used\n * to be a hand-written copy that had no `remove`, which made that promise false\n * for anybody who took the docstring at its word.\n *\n * When compression itself fails the value is written as plain JSON rather than\n * dropped: a slightly larger record still loads, an absent one does not. Only a\n * storage-level failure — quota, blocked storage — loses the write.\n */\nexport const compressedStorage: JsonStorage = createJsonStorage(compressedStorageCodec);\n"],"mappings":";;;;AA4BA,IAAMA,IAAS;AAiBf,SAAgBC,EAAoB,GAAkB;CAClD,IAAMC,IAAO,KAAK,UAAUC,CAAK,GAC3BC,IAAQC,EAAS,IAAI,YAAY,CAAC,CAAC,OAAOH,CAAI,CAAC;CACrD,OAAO,GAAGF,IAASM,EAAcF,CAAK;AAC1C;AAUA,IAAMG,IAAyB;AAsB/B,SAAgBC,EAAwB,GAAgB;CACpD,IAAI,CAACC,EAAI,WAAWT,CAAM,GAAG,OAAO,KAAK,MAAMS,CAAG;CAClD,IAAMC,IAAaC,EAAcF,EAAI,MAAM,CAAa,CAAC;CACzD,IAAIC,EAAW,cAAc,GAAG;EAM5B,IAAME,IAAO,IALO,SAChBF,EAAW,QACXA,EAAW,YACXA,EAAW,UAEF,CAAA,CAAQ,UAAUA,EAAW,aAAa,GAAG,EAAI;EAC9D,IAAIE,IAAOL,GACP,MAAU,WACN,6BAA6BK,EAAK,kCAC3BL,EAAuB,aAClC;CAER;CACA,IAAMH,IAAQS,EAAWH,CAAU;CACnC,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC,CAAC,OAAON,CAAK,CAAC;AACrD;AASA,IAAaU,IAAuC;CAChD,WAAWb;CACX,aAAaO;AACjB,GAeaO,IAAiCC,EAAkBF,CAAsB"}
@@ -1,2 +1,2 @@
1
- function e(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function t(e,t){let n={};for(let r of t)Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function n(e,t){let n=new Set(t),r={};for(let t of Object.keys(e))n.has(t)||(r[t]=e[t]);return r}function r(t,n){if(!e(t)||!e(n))return n===void 0?t:n;let i={...t};for(let t of Object.keys(n)){let a=n[t],o=i[t];i[t]=e(o)&&e(a)?r(o,a):a}return i}function i(t){return t==null?!0:typeof t==`string`||Array.isArray(t)?t.length===0:t instanceof Map||t instanceof Set?t.size===0:e(t)?Object.keys(t).length===0:!1}exports.deepMerge=r,exports.isEmpty=i,exports.omit=n,exports.pick=t;
1
+ function e(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function t(e,t){let n={};for(let r of t)Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}function n(e,t){let n=new Set(t),r={};for(let t of Object.keys(e))n.has(t)||(r[t]=e[t]);return r}var r=new Set([`__proto__`,`constructor`,`prototype`]);function i(t,n){if(!e(t)||!e(n))return n===void 0?t:n;let a={...t};for(let t of Object.keys(n)){if(r.has(t))continue;let o=n[t],s=a[t];a[t]=e(s)&&e(o)?i(s,o):o}return a}function a(t){return t==null?!0:typeof t==`string`||Array.isArray(t)?t.length===0:t instanceof Map||t instanceof Set?t.size===0:e(t)?Object.keys(t).length===0:!1}exports.deepMerge=i,exports.isEmpty=a,exports.omit=n,exports.pick=t;
2
2
  //# sourceMappingURL=objects.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"objects.cjs","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":"AAOA,SAASA,EAAc,EAAkD,CACrE,GAAI,OAAOC,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMC,EAAQ,OAAO,eAAeD,CAAK,EACzC,OAAOC,IAAU,OAAO,WAAaA,IAAU,IACnD,CAWA,SAAgBC,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAOC,EACV,OAAO,UAAU,eAAe,KAAKC,EAAKF,CAAG,IAC7C,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAWA,SAAgBI,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAU,IAAI,IAAiBH,CAAI,EACnCF,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,EACxBE,EAAQ,IAAIJ,CAAG,IAChB,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAqBA,SAAgBM,EAAa,EAAW,EAAuB,CAC3D,GAAI,CAACV,EAAcW,CAAM,GAAK,CAACX,EAAcY,CAAM,EAC/C,OAAQA,IAAW,IAAA,GAAYD,EAAUC,EAE7C,IAAMR,EAAkC,CAAE,GAAGO,CAAO,EACpD,IAAK,IAAMN,KAAO,OAAO,KAAKO,CAAM,EAAG,CACnC,IAAMC,EAAeD,EAAmCP,GAClDS,EAAcV,EAAOC,GAC3B,AAGID,EAAOC,GAHPL,EAAcc,CAAW,GAAKd,EAAca,CAAW,EACzCH,EAAUI,EAAaD,CAAW,EAElCA,CAEtB,CACA,OAAOT,CACX,CAmBA,SAAgBW,EAAQ,EAAyB,CAM7C,OALId,GAAU,KAAoC,GAC9C,OAAOA,GAAU,UACjB,MAAM,QAAQA,CAAK,EAAUA,EAAM,SAAW,EAC9CA,aAAiB,KAAOA,aAAiB,IAAYA,EAAM,OAAS,EACpED,EAAcC,CAAK,EAAU,OAAO,KAAKA,CAAK,CAAC,CAAC,SAAW,EACxD,EACX,C"}
1
+ {"version":3,"file":"objects.cjs","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","UNSAFE_MERGE_KEYS","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/** Keys {@link deepMerge} never copies from `source`. */\nconst UNSAFE_MERGE_KEYS: ReadonlySet<string> = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n * - `__proto__`, `constructor` and `prototype` keys in `source` are skipped.\n * Assigning `result[\"__proto__\"]` does not create a key: it **replaces the\n * result's prototype**, so a `JSON.parse`d settings blob carrying\n * `{\"__proto__\": {\"isAdmin\": true}}` produced an object whose `isAdmin` read\n * `true` through inheritance while `Object.keys` showed nothing. No global\n * prototype is touched either way; the skip removes the surprise, and the\n * other two names go with it because nothing legitimate merges into them.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n if (UNSAFE_MERGE_KEYS.has(key)) continue;\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":"AAOA,SAASA,EAAc,EAAkD,CACrE,GAAI,OAAOC,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMC,EAAQ,OAAO,eAAeD,CAAK,EACzC,OAAOC,IAAU,OAAO,WAAaA,IAAU,IACnD,CAWA,SAAgBC,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAOC,EACV,OAAO,UAAU,eAAe,KAAKC,EAAKF,CAAG,IAC7C,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAWA,SAAgBI,EAA0C,EAAQ,EAAuB,CACrF,IAAMC,EAAU,IAAI,IAAiBH,CAAI,EACnCF,EAAS,CAAC,EAChB,IAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,EACxBE,EAAQ,IAAIJ,CAAG,IAChB,EAAOA,GAAOE,EAAIF,IAG1B,OAAOD,CACX,CAGA,IAAMM,EAAyC,IAAI,IAAI,CAAC,YAAa,cAAe,WAAW,CAAC,EA4BhG,SAAgBC,EAAa,EAAW,EAAuB,CAC3D,GAAI,CAACX,EAAcY,CAAM,GAAK,CAACZ,EAAca,CAAM,EAC/C,OAAQA,IAAW,IAAA,GAAYD,EAAUC,EAE7C,IAAMT,EAAkC,CAAE,GAAGQ,CAAO,EACpD,IAAK,IAAMP,KAAO,OAAO,KAAKQ,CAAM,EAAG,CACnC,GAAIH,EAAkB,IAAIL,CAAG,EAAG,SAChC,IAAMS,EAAeD,EAAmCR,GAClDU,EAAcX,EAAOC,GAC3B,AAGID,EAAOC,GAHPL,EAAce,CAAW,GAAKf,EAAcc,CAAW,EACzCH,EAAUI,EAAaD,CAAW,EAElCA,CAEtB,CACA,OAAOV,CACX,CAmBA,SAAgBY,EAAQ,EAAyB,CAM7C,OALIf,GAAU,KAAoC,GAC9C,OAAOA,GAAU,UACjB,MAAM,QAAQA,CAAK,EAAUA,EAAM,SAAW,EAC9CA,aAAiB,KAAOA,aAAiB,IAAYA,EAAM,OAAS,EACpED,EAAcC,CAAK,EAAU,OAAO,KAAKA,CAAK,CAAC,CAAC,SAAW,EACxD,EACX,C"}
@@ -14,19 +14,25 @@ function n(e, t) {
14
14
  for (let t of Object.keys(e)) n.has(t) || (r[t] = e[t]);
15
15
  return r;
16
16
  }
17
- function r(t, n) {
17
+ var r = /* @__PURE__ */ new Set([
18
+ "__proto__",
19
+ "constructor",
20
+ "prototype"
21
+ ]);
22
+ function i(t, n) {
18
23
  if (!e(t) || !e(n)) return n === void 0 ? t : n;
19
- let i = { ...t };
24
+ let a = { ...t };
20
25
  for (let t of Object.keys(n)) {
21
- let a = n[t], o = i[t];
22
- i[t] = e(o) && e(a) ? r(o, a) : a;
26
+ if (r.has(t)) continue;
27
+ let o = n[t], s = a[t];
28
+ a[t] = e(s) && e(o) ? i(s, o) : o;
23
29
  }
24
- return i;
30
+ return a;
25
31
  }
26
- function i(t) {
32
+ function a(t) {
27
33
  return t == null ? !0 : typeof t == "string" || Array.isArray(t) ? t.length === 0 : t instanceof Map || t instanceof Set ? t.size === 0 : e(t) ? Object.keys(t).length === 0 : !1;
28
34
  }
29
35
  //#endregion
30
- export { r as deepMerge, i as isEmpty, n as omit, t as pick };
36
+ export { i as deepMerge, a as isEmpty, n as omit, t as pick };
31
37
 
32
38
  //# sourceMappingURL=objects.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"objects.js","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":";AAOA,SAASA,EAAc,GAAkD;CACrE,IAAI,OAAOC,KAAU,aAAYA,GAAgB,OAAO;CACxD,IAAMC,IAAQ,OAAO,eAAeD,CAAK;CACzC,OAAOC,MAAU,OAAO,aAAaA,MAAU;AACnD;AAWA,SAAgBC,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAOC,GACd,AAAI,OAAO,UAAU,eAAe,KAAKC,GAAKF,CAAG,MAC7C,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAWA,SAAgBI,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAU,IAAI,IAAiBH,CAAI,GACnCF,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,GAC7B,AAAKE,EAAQ,IAAIJ,CAAG,MAChB,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAqBA,SAAgBM,EAAa,GAAW,GAAuB;CAC3D,IAAI,CAACV,EAAcW,CAAM,KAAK,CAACX,EAAcY,CAAM,GAC/C,OAAQA,MAAW,KAAA,IAAYD,IAAUC;CAE7C,IAAMR,IAAkC,EAAE,GAAGO,EAAO;CACpD,KAAK,IAAMN,KAAO,OAAO,KAAKO,CAAM,GAAG;EACnC,IAAMC,IAAeD,EAAmCP,IAClDS,IAAcV,EAAOC;EAC3B,AAGID,EAAOC,KAHPL,EAAcc,CAAW,KAAKd,EAAca,CAAW,IACzCH,EAAUI,GAAaD,CAAW,IAElCA;CAEtB;CACA,OAAOT;AACX;AAmBA,SAAgBW,EAAQ,GAAyB;CAM7C,OALId,KAAU,OAAoC,KAC9C,OAAOA,KAAU,YACjB,MAAM,QAAQA,CAAK,IAAUA,EAAM,WAAW,IAC9CA,aAAiB,OAAOA,aAAiB,MAAYA,EAAM,SAAS,IACpED,EAAcC,CAAK,IAAU,OAAO,KAAKA,CAAK,CAAC,CAAC,WAAW,IACxD;AACX"}
1
+ {"version":3,"file":"objects.js","names":["isPlainRecord","value","proto","pick","result","key","keys","obj","omit","exclude","UNSAFE_MERGE_KEYS","deepMerge","target","source","sourceValue","targetValue","isEmpty"],"sources":["../../src/utils/objects.ts"],"sourcesContent":["/**\n * Determine whether a value is a plain object literal.\n *\n * Returns `false` for arrays, `null`, dates, maps, sets, and class instances.\n *\n * @internal\n */\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Create a new object containing only the given `keys` from `obj`.\n *\n * Missing keys are skipped. The input is never mutated.\n *\n * @example\n * pick({ id: 1, name: \"Ana\", age: 30 }, [\"id\", \"name\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (Object.prototype.hasOwnProperty.call(obj, key)) {\n result[key] = obj[key];\n }\n }\n return result;\n}\n\n/**\n * Create a new object with the given `keys` removed from `obj`.\n *\n * The input is never mutated.\n *\n * @example\n * omit({ id: 1, name: \"Ana\", age: 30 }, [\"age\"]);\n * // { id: 1, name: \"Ana\" }\n */\nexport function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {\n const exclude = new Set<PropertyKey>(keys);\n const result = {} as Record<PropertyKey, unknown>;\n for (const key of Object.keys(obj) as (keyof T)[]) {\n if (!exclude.has(key)) {\n result[key] = obj[key];\n }\n }\n return result as Omit<T, K>;\n}\n\n/** Keys {@link deepMerge} never copies from `source`. */\nconst UNSAFE_MERGE_KEYS: ReadonlySet<string> = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/**\n * Recursively merge `source` into `target`, returning a new object.\n *\n * - Plain objects are merged key by key, recursing into nested plain objects.\n * - Arrays and non-plain values (dates, class instances, primitives) replace\n * the target value entirely — they are not merged element by element.\n * - Neither `target` nor `source` is mutated.\n * - `__proto__`, `constructor` and `prototype` keys in `source` are skipped.\n * Assigning `result[\"__proto__\"]` does not create a key: it **replaces the\n * result's prototype**, so a `JSON.parse`d settings blob carrying\n * `{\"__proto__\": {\"isAdmin\": true}}` produced an object whose `isAdmin` read\n * `true` through inheritance while `Object.keys` showed nothing. No global\n * prototype is touched either way; the skip removes the surprise, and the\n * other two names go with it because nothing legitimate merges into them.\n *\n * @example\n * deepMerge(\n * { a: 1, nested: { x: 1, y: 2 } },\n * { nested: { y: 20, z: 30 } },\n * );\n * // { a: 1, nested: { x: 1, y: 20, z: 30 } }\n *\n * @example\n * deepMerge({ tags: [\"a\", \"b\"] }, { tags: [\"c\"] });\n * // { tags: [\"c\"] } (arrays replaced, not merged)\n */\nexport function deepMerge<T>(target: T, source: Partial<T>): T {\n if (!isPlainRecord(target) || !isPlainRecord(source)) {\n return (source === undefined ? target : (source as unknown as T)) as T;\n }\n const result: Record<string, unknown> = { ...target };\n for (const key of Object.keys(source)) {\n if (UNSAFE_MERGE_KEYS.has(key)) continue;\n const sourceValue = (source as Record<string, unknown>)[key];\n const targetValue = result[key];\n if (isPlainRecord(targetValue) && isPlainRecord(sourceValue)) {\n result[key] = deepMerge(targetValue, sourceValue);\n } else {\n result[key] = sourceValue;\n }\n }\n return result as T;\n}\n\n/**\n * Check whether a value is considered \"empty\".\n *\n * Returns `true` for `null`, `undefined`, empty strings, empty arrays, empty\n * plain objects, and empty `Map`/`Set` instances. Returns `false` for numbers\n * (including `0` and `NaN`), booleans, and dates, which are never empty.\n *\n * @example\n * isEmpty(null); // true\n * isEmpty(\"\"); // true\n * isEmpty([]); // true\n * isEmpty({}); // true\n * isEmpty(new Map()); // true\n * isEmpty(0); // false\n * isEmpty(false); // false\n * isEmpty(new Date()); // false\n */\nexport function isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\") return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n if (value instanceof Map || value instanceof Set) return value.size === 0;\n if (isPlainRecord(value)) return Object.keys(value).length === 0;\n return false;\n}\n"],"mappings":";AAOA,SAASA,EAAc,GAAkD;CACrE,IAAI,OAAOC,KAAU,aAAYA,GAAgB,OAAO;CACxD,IAAMC,IAAQ,OAAO,eAAeD,CAAK;CACzC,OAAOC,MAAU,OAAO,aAAaA,MAAU;AACnD;AAWA,SAAgBC,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAOC,GACd,AAAI,OAAO,UAAU,eAAe,KAAKC,GAAKF,CAAG,MAC7C,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAWA,SAAgBI,EAA0C,GAAQ,GAAuB;CACrF,IAAMC,IAAU,IAAI,IAAiBH,CAAI,GACnCF,IAAS,CAAC;CAChB,KAAK,IAAMC,KAAO,OAAO,KAAKE,CAAG,GAC7B,AAAKE,EAAQ,IAAIJ,CAAG,MAChB,EAAOA,KAAOE,EAAIF;CAG1B,OAAOD;AACX;AAGA,IAAMM,oBAAyC,IAAI,IAAI;CAAC;CAAa;CAAe;AAAW,CAAC;AA4BhG,SAAgBC,EAAa,GAAW,GAAuB;CAC3D,IAAI,CAACX,EAAcY,CAAM,KAAK,CAACZ,EAAca,CAAM,GAC/C,OAAQA,MAAW,KAAA,IAAYD,IAAUC;CAE7C,IAAMT,IAAkC,EAAE,GAAGQ,EAAO;CACpD,KAAK,IAAMP,KAAO,OAAO,KAAKQ,CAAM,GAAG;EACnC,IAAIH,EAAkB,IAAIL,CAAG,GAAG;EAChC,IAAMS,IAAeD,EAAmCR,IAClDU,IAAcX,EAAOC;EAC3B,AAGID,EAAOC,KAHPL,EAAce,CAAW,KAAKf,EAAcc,CAAW,IACzCH,EAAUI,GAAaD,CAAW,IAElCA;CAEtB;CACA,OAAOV;AACX;AAmBA,SAAgBY,EAAQ,GAAyB;CAM7C,OALIf,KAAU,OAAoC,KAC9C,OAAOA,KAAU,YACjB,MAAM,QAAQA,CAAK,IAAUA,EAAM,WAAW,IAC9CA,aAAiB,OAAOA,aAAiB,MAAYA,EAAM,SAAS,IACpED,EAAcC,CAAK,IAAU,OAAO,KAAKA,CAAK,CAAC,CAAC,WAAW,IACxD;AACX"}
@@ -1,2 +1,2 @@
1
- var e=new Set([`http:`,`https:`,`mailto:`,`tel:`,`sms:`]),t=new Set([`http:`,`https:`,`blob:`]),n=/^data:image\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\s]+$/i;function r(e){return e.replace(/[\u0000-\u0020\u007F]/g,``).trim()}function i(e){return/^[a-z][a-z0-9+.-]*:/i.test(e)?e.slice(0,e.indexOf(`:`)+1).toLowerCase():null}function a(t){let n=r(t);if(!n)return null;let a=i(n);return a===null||e.has(a)?n:null}function o(e){let t=r(e);return t?n.test(t)?t:/^data:/i.test(t)?null:a(t):null}function s(e){let a=r(e);if(!a)return null;if(n.test(a))return e;let o=i(a);return o===null||t.has(o)?e:null}exports.safeAttachmentUrl=s,exports.safeImageUrl=o,exports.safeLinkUrl=a;
1
+ var e=new Set([`http:`,`https:`,`mailto:`,`tel:`,`sms:`]),t=new Set([`http:`,`https:`,`blob:`]),n=/^data:image\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\s]+$/i;function r(e){return e.replace(/[\u0000-\u0020\u007F]/g,``).trim()}function i(e){return/^[a-z][a-z0-9+.-]*:/i.test(e)?e.slice(0,e.indexOf(`:`)+1).toLowerCase():null}function a(t){let n=r(t);if(!n)return null;let a=i(n);return a===null||e.has(a)?n:null}function o(e){let t=r(e);return t?n.test(t)?t:/^data:/i.test(t)?null:a(t):null}function s(e){let a=r(e);if(!a)return null;if(n.test(a))return e;let o=i(a);return o===null||t.has(o)?e:null}function c(e){if(e!==void 0&&a(e)!==null)return e}exports.safeAttachmentUrl=s,exports.safeImageUrl=o,exports.safeLinkUrl=a,exports.vettedHref=c;
2
2
  //# sourceMappingURL=safe-url.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"safe-url.cjs","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `&#106;avascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n"],"mappings":"AAiBA,IAAMA,EAAe,IAAI,IAAI,CAAC,QAAS,SAAU,UAAW,OAAQ,MAAM,CAAC,EASrEC,EAAmB,IAAI,IAAI,CAAC,QAAS,SAAU,OAAO,CAAC,EASvDC,EAAmB,qEAQzB,SAASC,EAAU,EAAqB,CAEpC,OAAOC,EAAI,QAAQ,yBAA0B,EAAE,CAAC,CAAC,KAAK,CAC1D,CAMA,SAASC,EAAS,EAA4B,CAE1C,MADK,uBAAuB,KAAKC,CAAG,EAC7BA,EAAI,MAAM,EAAGA,EAAI,QAAQ,GAAG,EAAI,CAAC,CAAC,CAAC,YAAY,EADR,IAElD,CAiBA,SAAgBC,EAAY,EAA4B,CACpD,IAAMD,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,IAAME,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQR,EAAa,IAAIQ,CAAM,EAAIF,EAAM,IAC/D,CAcA,SAAgBG,EAAa,EAA4B,CACrD,IAAMH,EAAMH,EAAUC,CAAG,EAIzB,OAHKE,EACDJ,EAAiB,KAAKI,CAAG,EAAUA,EACnC,UAAU,KAAKA,CAAG,EAAU,KACzBC,EAAYD,CAAG,EAHL,IAIrB,CAyBA,SAAgBI,EAAkB,EAA4B,CAC1D,IAAMJ,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,GAAIJ,EAAiB,KAAKI,CAAG,EAAG,OAAOF,EACvC,IAAMI,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQP,EAAiB,IAAIO,CAAM,EAAIJ,EAAM,IACnE,C"}
1
+ {"version":3,"file":"safe-url.cjs","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl","vettedHref"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `&#106;avascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n\n/**\n * The `href` a component renders from a navigation prop, or `undefined`.\n *\n * {@link safeLinkUrl}'s allowlist, but the URL comes back **as given**, like\n * {@link safeAttachmentUrl}: an app route such as `/arquivos/relatório final` must\n * keep its space, and the normalization only removes characters the browser's\n * parser also strips, so it cannot hide a scheme. `Button`, `Breadcrumbs`,\n * `NavigationMenu`, `Sidebar`, `AuthForm` and `AuthPage` call it so a menu or a\n * call to action built from API data never puts a clickable `javascript:` link\n * on the page — React 19 blocks one, React 18 (inside the peer range) does not.\n *\n * @internal\n * @param raw - The prop as the app passed it.\n * @returns `raw` when it is safe to link, `undefined` when it is absent or not.\n */\nexport function vettedHref(raw: string | undefined): string | undefined {\n if (raw === undefined || safeLinkUrl(raw) === null) return undefined;\n return raw;\n}\n"],"mappings":"AAiBA,IAAMA,EAAe,IAAI,IAAI,CAAC,QAAS,SAAU,UAAW,OAAQ,MAAM,CAAC,EASrEC,EAAmB,IAAI,IAAI,CAAC,QAAS,SAAU,OAAO,CAAC,EASvDC,EAAmB,qEAQzB,SAASC,EAAU,EAAqB,CAEpC,OAAOC,EAAI,QAAQ,yBAA0B,EAAE,CAAC,CAAC,KAAK,CAC1D,CAMA,SAASC,EAAS,EAA4B,CAE1C,MADK,uBAAuB,KAAKC,CAAG,EAC7BA,EAAI,MAAM,EAAGA,EAAI,QAAQ,GAAG,EAAI,CAAC,CAAC,CAAC,YAAY,EADR,IAElD,CAiBA,SAAgBC,EAAY,EAA4B,CACpD,IAAMD,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,IAAME,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQR,EAAa,IAAIQ,CAAM,EAAIF,EAAM,IAC/D,CAcA,SAAgBG,EAAa,EAA4B,CACrD,IAAMH,EAAMH,EAAUC,CAAG,EAIzB,OAHKE,EACDJ,EAAiB,KAAKI,CAAG,EAAUA,EACnC,UAAU,KAAKA,CAAG,EAAU,KACzBC,EAAYD,CAAG,EAHL,IAIrB,CAyBA,SAAgBI,EAAkB,EAA4B,CAC1D,IAAMJ,EAAMH,EAAUC,CAAG,EACzB,GAAI,CAACE,EAAK,OAAO,KACjB,GAAIJ,EAAiB,KAAKI,CAAG,EAAG,OAAOF,EACvC,IAAMI,EAASH,EAASC,CAAG,EAC3B,OAAOE,IAAW,MAAQP,EAAiB,IAAIO,CAAM,EAAIJ,EAAM,IACnE,CAiBA,SAAgBO,EAAW,EAA6C,CACpE,GAAIP,IAAQ,IAAA,IAAaG,EAAYH,CAAG,IAAM,KAC9C,OAAOA,CACX,C"}
@@ -33,7 +33,10 @@ function s(e) {
33
33
  let o = i(a);
34
34
  return o === null || t.has(o) ? e : null;
35
35
  }
36
+ function c(e) {
37
+ if (e !== void 0 && a(e) !== null) return e;
38
+ }
36
39
  //#endregion
37
- export { s as safeAttachmentUrl, o as safeImageUrl, a as safeLinkUrl };
40
+ export { s as safeAttachmentUrl, o as safeImageUrl, a as safeLinkUrl, c as vettedHref };
38
41
 
39
42
  //# sourceMappingURL=safe-url.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"safe-url.js","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `&#106;avascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n"],"mappings":";AAiBA,IAAMA,oBAAe,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;CAAQ;AAAM,CAAC,GASrEC,oBAAmB,IAAI,IAAI;CAAC;CAAS;CAAU;AAAO,CAAC,GASvDC,IAAmB;AAQzB,SAASC,EAAU,GAAqB;CAEpC,OAAOC,EAAI,QAAQ,0BAA0B,EAAE,CAAC,CAAC,KAAK;AAC1D;AAMA,SAASC,EAAS,GAA4B;CAE1C,OADK,uBAAuB,KAAKC,CAAG,IAC7BA,EAAI,MAAM,GAAGA,EAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,IADR;AAElD;AAiBA,SAAgBC,EAAY,GAA4B;CACpD,IAAMD,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAME,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQR,EAAa,IAAIQ,CAAM,IAAIF,IAAM;AAC/D;AAcA,SAAgBG,EAAa,GAA4B;CACrD,IAAMH,IAAMH,EAAUC,CAAG;CAIzB,OAHKE,IACDJ,EAAiB,KAAKI,CAAG,IAAUA,IACnC,UAAU,KAAKA,CAAG,IAAU,OACzBC,EAAYD,CAAG,IAHL;AAIrB;AAyBA,SAAgBI,EAAkB,GAA4B;CAC1D,IAAMJ,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAIJ,EAAiB,KAAKI,CAAG,GAAG,OAAOF;CACvC,IAAMI,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQP,EAAiB,IAAIO,CAAM,IAAIJ,IAAM;AACnE"}
1
+ {"version":3,"file":"safe-url.js","names":["LINK_SCHEMES","RESOURCE_SCHEMES","IMAGE_DATA_TYPES","normalize","raw","schemeOf","url","safeLinkUrl","scheme","safeImageUrl","safeAttachmentUrl","vettedHref"],"sources":["../../src/utils/safe-url.ts"],"sourcesContent":["/**\n * URL vetting for every `href` and `src` the SDK fills from somebody else's data:\n * links and images in rendered Markdown, and the file row of a chat attachment.\n *\n * This is a security boundary. Text that arrives from a user — a comment, a ticket\n * description, a chat attachment — reaches the DOM as a real `href`, and\n * `javascript:alert(1)` in a link is script execution on click. React 19 replaces\n * a `javascript:` href with a throwing stub, but the peer range admits React 18,\n * which only warns: measured with react-dom 18.3.1, `renderToStaticMarkup` wrote\n * `href=\"javascript:...\"` verbatim, and clicking it ran the script in Chromium 153\n * and Firefox 155 — `download` on the anchor did not stop it (#447). So the\n * scheme is checked against an allowlist rather than a blocklist: a blocklist has\n * to enumerate `javascript:`, `JaVaScRiPt:`, `java\\tscript:`, `&#106;avascript:`\n * and whatever comes next, and misses the one nobody thought of.\n */\n\n/** Schemes a link may use. Anything else is dropped. */\nconst LINK_SCHEMES = new Set([\"http:\", \"https:\", \"mailto:\", \"tel:\", \"sms:\"]);\n\n/**\n * Schemes a fetched resource — an attachment, a download — may use.\n *\n * `blob:` is here because an app shows a message while its upload is still in\n * flight by pointing at `URL.createObjectURL(file)`. A blob URL is same-origin\n * bytes the page already holds; it cannot run code by being followed.\n */\nconst RESOURCE_SCHEMES = new Set([\"http:\", \"https:\", \"blob:\"]);\n\n/**\n * Image `data:` types that are safe to inline.\n *\n * `svg+xml` is deliberately absent: an SVG is a document, it can carry `<script>`\n * and event handlers, and browsers execute them when it loads from a `data:` URL in\n * some contexts. The raster formats cannot carry code.\n */\nconst IMAGE_DATA_TYPES = /^data:image\\/(png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\\s]+$/i;\n\n/**\n * Strip the characters a scheme can be smuggled through.\n *\n * `java\\tscript:` and `java\\nscript:` are both parsed as `javascript:` by browsers,\n * and a leading control character defeats a naive `startsWith` check.\n */\nfunction normalize(raw: string): string {\n // eslint-disable-next-line no-control-regex -- stripping control characters is the point\n return raw.replace(/[\\u0000-\\u0020\\u007F]/g, \"\").trim();\n}\n\n/**\n * The scheme of a normalized URL, lower-cased with its colon, or `null` for a\n * relative URL.\n */\nfunction schemeOf(url: string): string | null {\n if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) return null;\n return url.slice(0, url.indexOf(\":\") + 1).toLowerCase();\n}\n\n/**\n * The href to render, or `null` when the URL must not be linked.\n *\n * Relative URLs (`/docs`, `./x`, `#anchor`, `foo/bar`) are kept: they cannot carry\n * a scheme, so they cannot execute anything, and dropping them would break every\n * in-app link. Protocol-relative (`//host/path`) inherits the page scheme and is\n * kept for the same reason.\n *\n * @example\n * safeLinkUrl(\"https://x.dev\"); // \"https://x.dev\"\n * safeLinkUrl(\"java\\tscript:alert(1)\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe href, or `null`.\n */\nexport function safeLinkUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n const scheme = schemeOf(url);\n return scheme === null || LINK_SCHEMES.has(scheme) ? url : null;\n}\n\n/**\n * The `src` to render for an image, or `null`.\n *\n * Accepts what {@link safeLinkUrl} accepts, plus raster `data:image/...;base64`.\n *\n * @example\n * safeImageUrl(\"data:image/png;base64,iVBORw0KGgo=\"); // kept\n * safeImageUrl(\"data:image/svg+xml;base64,PHN2Zz4=\"); // null\n *\n * @param raw - URL as written in the Markdown.\n * @returns A safe src, or `null`.\n */\nexport function safeImageUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return url;\n if (/^data:/i.test(url)) return null;\n return safeLinkUrl(url);\n}\n\n/**\n * The URL to fetch or download an attachment from, or `null`.\n *\n * The allowlist is `http:`, `https:`, `blob:`, relative URLs and the raster\n * `data:image/` types {@link safeImageUrl} accepts. A file row is an `<a href\n * download>`, and the `download` attribute does not stop a `javascript:` href\n * from running (measured in Chromium 153 and Firefox 155, #447), so the scheme\n * has to be checked before the anchor exists. `mailto:`/`tel:` are left out: an\n * attachment is bytes, not a contact.\n *\n * The scheme is judged on the normalized form, but the URL comes back **as given**:\n * normalizing deletes every space, and `/files/relatório final.pdf` is a real\n * attachment path. Returning the original is safe because the normalization only\n * removes characters, a superset of what the browser's URL parser strips, so it\n * can never hide a scheme the browser would see.\n *\n * @example\n * const href = safeAttachmentUrl(attachment.url);\n * return href ? <a href={href} download>{name}</a> : <span>{name}</span>;\n *\n * @param raw - URL as the sender supplied it.\n * @returns A safe URL, or `null` when it must not reach an `href`.\n */\nexport function safeAttachmentUrl(raw: string): string | null {\n const url = normalize(raw);\n if (!url) return null;\n if (IMAGE_DATA_TYPES.test(url)) return raw;\n const scheme = schemeOf(url);\n return scheme === null || RESOURCE_SCHEMES.has(scheme) ? raw : null;\n}\n\n/**\n * The `href` a component renders from a navigation prop, or `undefined`.\n *\n * {@link safeLinkUrl}'s allowlist, but the URL comes back **as given**, like\n * {@link safeAttachmentUrl}: an app route such as `/arquivos/relatório final` must\n * keep its space, and the normalization only removes characters the browser's\n * parser also strips, so it cannot hide a scheme. `Button`, `Breadcrumbs`,\n * `NavigationMenu`, `Sidebar`, `AuthForm` and `AuthPage` call it so a menu or a\n * call to action built from API data never puts a clickable `javascript:` link\n * on the page — React 19 blocks one, React 18 (inside the peer range) does not.\n *\n * @internal\n * @param raw - The prop as the app passed it.\n * @returns `raw` when it is safe to link, `undefined` when it is absent or not.\n */\nexport function vettedHref(raw: string | undefined): string | undefined {\n if (raw === undefined || safeLinkUrl(raw) === null) return undefined;\n return raw;\n}\n"],"mappings":";AAiBA,IAAMA,oBAAe,IAAI,IAAI;CAAC;CAAS;CAAU;CAAW;CAAQ;AAAM,CAAC,GASrEC,oBAAmB,IAAI,IAAI;CAAC;CAAS;CAAU;AAAO,CAAC,GASvDC,IAAmB;AAQzB,SAASC,EAAU,GAAqB;CAEpC,OAAOC,EAAI,QAAQ,0BAA0B,EAAE,CAAC,CAAC,KAAK;AAC1D;AAMA,SAASC,EAAS,GAA4B;CAE1C,OADK,uBAAuB,KAAKC,CAAG,IAC7BA,EAAI,MAAM,GAAGA,EAAI,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,YAAY,IADR;AAElD;AAiBA,SAAgBC,EAAY,GAA4B;CACpD,IAAMD,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAME,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQR,EAAa,IAAIQ,CAAM,IAAIF,IAAM;AAC/D;AAcA,SAAgBG,EAAa,GAA4B;CACrD,IAAMH,IAAMH,EAAUC,CAAG;CAIzB,OAHKE,IACDJ,EAAiB,KAAKI,CAAG,IAAUA,IACnC,UAAU,KAAKA,CAAG,IAAU,OACzBC,EAAYD,CAAG,IAHL;AAIrB;AAyBA,SAAgBI,EAAkB,GAA4B;CAC1D,IAAMJ,IAAMH,EAAUC,CAAG;CACzB,IAAI,CAACE,GAAK,OAAO;CACjB,IAAIJ,EAAiB,KAAKI,CAAG,GAAG,OAAOF;CACvC,IAAMI,IAASH,EAASC,CAAG;CAC3B,OAAOE,MAAW,QAAQP,EAAiB,IAAIO,CAAM,IAAIJ,IAAM;AACnE;AAiBA,SAAgBO,EAAW,GAA6C;CACpE,IAAIP,MAAQ,KAAA,KAAaG,EAAYH,CAAG,MAAM,MAC9C,OAAOA;AACX"}
@@ -1,2 +1,2 @@
1
- const e=require("./link-stats.cjs"),t=require("./mesh-quality.cjs"),n=require("./peer-link.cjs");var r=Symbol(`gone`);function i(i){let{slots:a,send:o}=i,s=new Map,c=new Map,l=new Map,u=new Map,d=i.iceServers??[],f=i.setLocalDescription??((e,t)=>e.setLocalDescription(t)),p=i.quality??{},m=!1,h=new Map,g=null,_=()=>Object.fromEntries(a.map(e=>[e.name,null])),v=()=>{i.onPeers?.([...c.values()].map(e=>({...e})))},y=e=>!m&&s.get(e.peerId)===e,b=async(e,t)=>{try{let n=await t;return y(e)?n:r}catch(t){if(!y(e))return r;throw t}},x=async t=>{if(m)return;let n=!1;for(let r of[...s.values()]){if(r.pc.connectionState!==`connected`)continue;let i=h.get(r.peerId);i===void 0&&(i=e.createLinkStatsSampler(t.kind===void 0?{}:{kind:t.kind}),h.set(r.peerId,i));let a;try{a=await i.sample(r.pc)}catch{continue}let o=c.get(r.peerId);y(r)&&o!==void 0&&(o.stats=a,n=!0,t.onStats?.(r.peerId,a))}n&&v()},S=()=>{let e=i.stats;if(e===void 0)return;let t=!m&&s.size>0;t&&g===null?g=setInterval(()=>void x(e),e.intervalMs??2e3):!t&&g!==null&&(clearInterval(g),g=null)},C=()=>{if(m)return;let e=[...s.values()].map(e=>e.pc.connectionState);e.some(e=>e===`connected`)?i.onState?.(`connected`):e.length>0?i.onState?.(`connecting`,`negotiating`):i.onState?.(`connected`,`alone`)},w=async e=>{if(m)return;p=e;let n=t.scaleForRoom(e,s.size),r=t.resolveDegradation(e,n);for(let e of s.values())await t.applyQualityToLink(e.transceivers,a,n,r)},T=async e=>{if(!e.makingOffer){e.makingOffer=!0;try{let t=await e.pc.createOffer();if(!y(e))return;if(!t.sdp){i.onNotice?.(`empty_local_offer_sdp`);return}if(await f(e.pc,t),!y(e))return;o({type:`offer`,to:e.peerId,sdp:e.pc.localDescription?.sdp??t.sdp})}catch{y(e)&&i.onNotice?.(`offer_failed`)}finally{e.makingOffer=!1}}},E=e=>e.streams?.[0]||(typeof MediaStream>`u`?null:new MediaStream([e.track])),D=(e,t)=>{let r=n.slotOf(t.transceiver,e,a),i=c.get(e.peerId);r&&i&&(i.streams={...i.streams,[r.name]:E(t)},t.track.onended=()=>{let t=c.get(e.peerId);t&&(t.streams={...t.streams,[r.name]:null},v())},v())},O=(e,t)=>{let n=(u.get(e)??Promise.resolve()).then(t);return u.set(e,n),n},k=async(e,t,n)=>{let r=e.transceivers[n];if(r&&y(e))try{await r.sender.replaceTrack(l.get(t)??null)}catch{return}},A=e=>Promise.all(a.map((t,n)=>O(t.name,()=>k(e,t.name,n)))),j=e=>{if(m)return;let t=s.get(e);t&&(t.pc.close(),s.delete(e)),c.delete(e),h.delete(e),w(p),S(),v(),C()},M=async(e,{offerer:t})=>{if(m||s.has(e))return;let r=n.createPeerConnection(d),i=[];if(t)try{i=a.map(e=>r.addTransceiver(e.kind,{direction:`sendrecv`}))}catch(e){throw r.close(),e}let l={peerId:e,pc:r,transceivers:i,pendingCandidates:[],remoteReady:!1,makingOffer:!1,isOfferer:t};s.set(e,l),c.set(e,{peerId:e,connection:r.connectionState,streams:c.get(e)?.streams??_()}),!(t&&(await A(l),!y(l)))&&(r.onicecandidate=t=>{o({type:`ice`,to:e,candidate:t.candidate?.candidate??null,sdpMid:t.candidate?.sdpMid??null,sdpMLineIndex:t.candidate?.sdpMLineIndex??null})},r.ontrack=e=>D(l,e),r.onconnectionstatechange=()=>{if(!y(l))return;let t=c.get(e);t&&(t.connection=r.connectionState),r.connectionState===`failed`||r.connectionState===`closed`?j(e):(v(),C())},r.onnegotiationneeded=()=>{l.isOfferer&&T(l)},S(),v(),C(),t&&await T(l))},N=async(e,t)=>{s.has(e)||await M(e,{offerer:!1});let a=s.get(e);if(!a||!y(a)||await b(a,a.pc.setRemoteDescription({type:`offer`,sdp:t}))===r||(a.remoteReady=!0,await n.drainCandidates(a),a.isOfferer||(n.adoptTransceivers(a),await A(a)),!y(a)))return;let c=await b(a,a.pc.createAnswer());c!==r&&(c.sdp?await b(a,f(a.pc,c))!==r&&(o({type:`answer`,to:e,sdp:a.pc.localDescription?.sdp??c.sdp}),await w(p)):i.onNotice?.(`empty_local_answer_sdp`))};return i.onState?.(`connecting`,`signaling`),{addPeer:M,removePeer:j,accept:async e=>{if(m)return;if(e.type===`offer`){await N(e.from,e.sdp);return}let t=s.get(e.from);if(t){if(e.type===`answer`){let i=t.pc.setRemoteDescription({type:`answer`,sdp:e.sdp});if(await b(t,i)===r)return;t.remoteReady=!0,await n.drainCandidates(t),await w(p)}else e.candidate!==null&&await n.acceptCandidate(t,{candidate:e.candidate,sdpMid:e.sdpMid??void 0,sdpMLineIndex:e.sdpMLineIndex??void 0})}},setLocalTrack:async(e,t)=>{if(m)return;l.set(e,t);let n=a.findIndex(t=>t.name===e);n<0||(await O(e,async()=>{for(let t of[...s.values()])await k(t,e,n)}),await w(p))},applyQuality:w,setIceServers:e=>{d=e},stop:()=>{if(!m){m=!0;for(let e of s.values())e.pc.close();s.clear(),c.clear(),h.clear(),l.clear(),S(),v(),i.onState?.(`closed`)}},getConnection:e=>s.get(e)?.pc??null,get peers(){return[...c.values()].map(e=>({...e}))}}}exports.createPeerMesh=i;
1
+ const e=require("./link-stats.cjs"),t=require("./mesh-quality.cjs"),n=require("./peer-link.cjs");var r=Symbol(`gone`);function i(i){let{slots:a,send:o}=i,s=new Map,c=new Map,l=new Map,u=new Map,d=i.iceServers??[],f=i.setLocalDescription??((e,t)=>e.setLocalDescription(t)),p=i.quality??{},m=!1,h=new Map,g=null,_=()=>Object.fromEntries(a.map(e=>[e.name,null])),v=()=>{i.onPeers?.([...c.values()].map(e=>({...e})))},y=e=>!m&&s.get(e.peerId)===e,b=async(e,t)=>{try{let n=await t;return y(e)?n:r}catch(t){if(!y(e))return r;throw t}},x=async t=>{if(m)return;let n=!1;for(let r of[...s.values()]){if(r.pc.connectionState!==`connected`)continue;let i=h.get(r.peerId);i===void 0&&(i=e.createLinkStatsSampler(t.kind===void 0?{}:{kind:t.kind}),h.set(r.peerId,i));let a;try{a=await i.sample(r.pc)}catch{continue}let o=c.get(r.peerId);y(r)&&o!==void 0&&(o.stats=a,n=!0,t.onStats?.(r.peerId,a))}n&&v()},S=()=>{let e=i.stats;if(e===void 0)return;let t=!m&&s.size>0;t&&g===null?g=setInterval(()=>void x(e),e.intervalMs??2e3):!t&&g!==null&&(clearInterval(g),g=null)},C=()=>{if(m)return;let e=[...s.values()].map(e=>e.pc.connectionState);e.some(e=>e===`connected`)?i.onState?.(`connected`):e.length>0?i.onState?.(`connecting`,`negotiating`):i.onState?.(`connected`,`alone`)},w=async e=>{if(m)return;p=e;let n=t.scaleForRoom(e,s.size),r=t.resolveDegradation(e,n);for(let e of s.values())await t.applyQualityToLink(e.transceivers,a,n,r)},T=async e=>{if(!e.makingOffer){e.makingOffer=!0;try{let t=await e.pc.createOffer();if(!y(e))return;if(!t.sdp){i.onNotice?.(`empty_local_offer_sdp`);return}if(await f(e.pc,t),!y(e))return;o({type:`offer`,to:e.peerId,sdp:e.pc.localDescription?.sdp??t.sdp})}catch{y(e)&&i.onNotice?.(`offer_failed`)}finally{e.makingOffer=!1}}},E=e=>e.streams?.[0]||(typeof MediaStream>`u`?null:new MediaStream([e.track])),D=(e,t)=>{let r=n.slotOf(t.transceiver,e,a),i=c.get(e.peerId);r&&i&&(i.streams={...i.streams,[r.name]:E(t)},t.track.onended=()=>{let t=c.get(e.peerId);t&&(t.streams={...t.streams,[r.name]:null},v())},v())},O=(e,t)=>{let n=(u.get(e)??Promise.resolve()).then(t);return u.set(e,n),n},k=async(e,t,n)=>{let r=e.transceivers[n];if(r&&y(e))try{await r.sender.replaceTrack(l.get(t)??null)}catch{return}},A=e=>Promise.all(a.map((t,n)=>O(t.name,()=>k(e,t.name,n)))),j=e=>{if(m)return;let t=s.get(e);t&&(t.pc.close(),s.delete(e)),c.delete(e),h.delete(e),w(p),S(),v(),C()},M=async(e,{offerer:t})=>{if(m||s.has(e))return;let r=n.createPeerConnection(d),i=[];if(t)try{i=a.map(e=>r.addTransceiver(e.kind,{direction:`sendrecv`}))}catch(e){throw r.close(),e}let l={peerId:e,pc:r,transceivers:i,pendingCandidates:[],remoteReady:!1,makingOffer:!1,isOfferer:t};s.set(e,l),c.set(e,{peerId:e,connection:r.connectionState,streams:c.get(e)?.streams??_()}),!(t&&(await A(l),!y(l)))&&(r.onicecandidate=t=>{o({type:`ice`,to:e,candidate:t.candidate?.candidate??null,sdpMid:t.candidate?.sdpMid??null,sdpMLineIndex:t.candidate?.sdpMLineIndex??null})},r.ontrack=e=>D(l,e),r.onconnectionstatechange=()=>{if(!y(l))return;let t=c.get(e);t&&(t.connection=r.connectionState),r.connectionState===`failed`||r.connectionState===`closed`?j(e):(v(),C())},r.onnegotiationneeded=()=>{l.isOfferer&&T(l)},S(),v(),C(),t&&await T(l))},N=async(e,t)=>{if(!s.has(e)){if(i.acceptPeer&&!i.acceptPeer(e)){i.onNotice?.(`offer_refused`);return}await M(e,{offerer:!1})}let a=s.get(e);if(!a||!y(a)||await b(a,a.pc.setRemoteDescription({type:`offer`,sdp:t}))===r||(a.remoteReady=!0,await n.drainCandidates(a),a.isOfferer||(n.adoptTransceivers(a),await A(a)),!y(a)))return;let c=await b(a,a.pc.createAnswer());c!==r&&(c.sdp?await b(a,f(a.pc,c))!==r&&(o({type:`answer`,to:e,sdp:a.pc.localDescription?.sdp??c.sdp}),await w(p)):i.onNotice?.(`empty_local_answer_sdp`))};return i.onState?.(`connecting`,`signaling`),{addPeer:M,removePeer:j,accept:async e=>{if(m)return;if(e.type===`offer`){await N(e.from,e.sdp);return}let t=s.get(e.from);if(t){if(e.type===`answer`){let i=t.pc.setRemoteDescription({type:`answer`,sdp:e.sdp});if(await b(t,i)===r)return;t.remoteReady=!0,await n.drainCandidates(t),await w(p)}else e.candidate!==null&&await n.acceptCandidate(t,{candidate:e.candidate,sdpMid:e.sdpMid??void 0,sdpMLineIndex:e.sdpMLineIndex??void 0})}},setLocalTrack:async(e,t)=>{if(m)return;l.set(e,t);let n=a.findIndex(t=>t.name===e);n<0||(await O(e,async()=>{for(let t of[...s.values()])await k(t,e,n)}),await w(p))},applyQuality:w,setIceServers:e=>{d=e},stop:()=>{if(!m){m=!0;for(let e of s.values())e.pc.close();s.clear(),c.clear(),h.clear(),l.clear(),S(),v(),i.onState?.(`closed`)}},getConnection:e=>s.get(e)?.pc??null,get peers(){return[...c.values()].map(e=>({...e}))}}}exports.createPeerMesh=i;
2
2
  //# sourceMappingURL=peer-mesh.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"peer-mesh.cjs","names":["createLinkStatsSampler","scaleForRoom","resolveDegradation","applyQualityToLink","slotOf","createPeerConnection","drainCandidates","acceptCandidate","require_link_stats","require_mesh_quality","require_peer_link","GONE","createPeerMesh","options","links","peers","localTracks","slotWrites","iceServers","describe","connection","description","quality","stopped","samplers","statsTimer","emptyStreams","slots","slot","emitPeers","peer","isLive","link","whileLive","value","step","error","sampleLinks","changed","sampler","settings","stats","syncStatsTimer","wanted","refreshState","states","state","applyQuality","next","effective","degradation","makeOffer","offer","send","streamFor","event","attachRemoteTrack","current","enqueueWrite","queued","write","writeSlot","transceiver","index","attachLocalTracks","removePeer","peerId","addPeer","pc","transceivers","offerer","acceptOffer","from","answer","message","applied","track","entry","servers"],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n acceptCandidate,\n adoptTransceivers,\n createPeerConnection,\n drainCandidates,\n slotOf,\n type PeerLink,\n} from \"./peer-link\";\nimport { createLinkStatsSampler, type LinkStats, type LinkStatsSampler } from \"./link-stats\";\nimport type {\n MeshMessage,\n MeshPeer,\n MeshQuality,\n MeshStatsOptions,\n PeerMeshOptions,\n} from \"./mesh-types\";\n\n/** What {@link createPeerMesh}'s negotiation steps return once their link is gone. */\nconst GONE = Symbol(\"gone\");\n\n/** A running mesh. */\nexport interface PeerMesh {\n /**\n * Open a link to a peer.\n *\n * `offerer` decides which side sends the offer, and exactly one side of each\n * link must say `true`. Drive it from the arrival order your server already\n * imposes — the peer that joined **later** offers, the one already in the\n * room answers. That avoids glare by construction, without the rollback\n * dance of perfect negotiation and without depending on a browser\n * implementing argument-less `setLocalDescription`.\n */\n addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n /** Close one link and give its share of the uplink back to the others. */\n removePeer: (peerId: string) => void;\n /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n accept: (message: MeshMessage & { from: string }) => Promise<void>;\n /**\n * Publish or retract one slot across every live link.\n *\n * `null` retracts: the sender keeps its negotiated transceiver and stops\n * producing, which is what mutes a microphone without disturbing the other\n * slots or the connection. Nothing renegotiates — the m-lines were allocated\n * before any track existed, which is what makes a mesh survive toggling: a\n * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n * somebody unmutes.\n */\n setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n /** Apply encoder limits, divided by the current size of the room. */\n applyQuality: (quality: MeshQuality) => Promise<void>;\n /** Replace the ICE servers used for links opened from now on. */\n setIceServers: (servers: RTCIceServer[]) => void;\n /**\n * Close every link, forget the local tracks and report `closed` — for good.\n *\n * Stopping is terminal. The signalling socket usually outlives the call, so\n * an offer still in flight from a former participant, or from anyone the\n * server relays to this user, keeps reaching {@link PeerMesh.accept} after\n * the hang-up. A stopped mesh drops it: no connection is opened, nothing is\n * answered and no track is published, so a call that reports `closed` never\n * sends the microphone or camera again. The same holds for `addPeer`,\n * `setLocalTrack` and `applyQuality`, and for every negotiation that was\n * already awaiting the browser when `stop()` ran. Calling it twice is a\n * no-op.\n *\n * The tracks themselves are not stopped: they belong to the caller, who may\n * still be showing them in a preview or reusing them on the next call. To\n * join again, create a new mesh.\n */\n stop: () => void;\n /** The peers as the mesh currently sees them. */\n readonly peers: MeshPeer[];\n /**\n * The connection carrying one peer's link, or `null`.\n *\n * The escape hatch for everything this mesh does not model. `getStats()`,\n * `getSenders()`, an `RTCDataChannel`, an encoding tweak the `quality`\n * shape has no field for — all of it lives on the connection, and a mesh\n * that keeps it private turns adopting the mesh into losing a feature.\n *\n * {@link MeshPeer.connection} is the *state*, which is what a view needs;\n * this is the object, which is what a measurement needs. For the common\n * case of measuring, {@link PeerMeshOptions.stats} spares you the loop —\n * including the one-sampler-per-connection rule that a hand-rolled version\n * gets wrong.\n *\n * @param peerId - The id the peer was added under.\n * @returns The connection, or `null` when there is no link for that id.\n */\n getConnection: (peerId: string) => RTCPeerConnection | null;\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n * slots: [\n * { name: \"mic\", kind: \"audio\" },\n * { name: \"cam\", kind: \"video\" },\n * ],\n * send: (message) => socket.send(message),\n * onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n const { slots, send } = options;\n const links = new Map<string, PeerLink>();\n const peers = new Map<string, MeshPeer>();\n const localTracks = new Map<string, MediaStreamTrack | null>();\n const slotWrites = new Map<string, Promise<void>>();\n let iceServers = options.iceServers ?? [];\n const describe =\n options.setLocalDescription ??\n ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n connection.setLocalDescription(description));\n let quality: MeshQuality = options.quality ?? {};\n let stopped = false;\n const samplers = new Map<string, LinkStatsSampler>();\n let statsTimer: ReturnType<typeof setInterval> | null = null;\n\n const emptyStreams = (): Record<string, MediaStream | null> =>\n Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n const emitPeers = (): void => {\n options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n };\n\n /**\n * Whether a link is still the one the mesh holds for its peer.\n *\n * Every negotiation step awaits the browser, and `stop()` or `removePeer()`\n * can close the link inside any of those awaits. A continuation that does\n * not re-check keeps negotiating on a closed connection — which a browser\n * answers with `InvalidStateError` — or, worse, signals an answer for a call\n * that already hung up.\n *\n * @param link - The link a continuation is about to touch.\n * @returns `false` once the mesh stopped or the link was removed or replaced.\n */\n const isLive = (link: PeerLink): boolean => !stopped && links.get(link.peerId) === link;\n\n /**\n * Await one negotiation step, and tell the caller whether to go on.\n *\n * A browser settles a call that `close()` ran under in one of two ways:\n * it rejects with `InvalidStateError`, or — what Chromium was measured\n * doing — it never settles at all. The second needs nothing here; the\n * first would surface as a rejected `accept()`, which the documented\n * `void mesh.accept(message)` wiring turns into an unhandled rejection\n * for a call that already hung up. So a rejection on a link that is no\n * longer live is swallowed, and one on a live link is rethrown untouched.\n *\n * @param link - The link the step negotiates on.\n * @param step - The pending browser call.\n * @returns The step's value, or {@link GONE} when the link stopped being live.\n */\n const whileLive = async <T>(link: PeerLink, step: Promise<T>): Promise<T | typeof GONE> => {\n try {\n const value = await step;\n return isLive(link) ? value : GONE;\n } catch (error) {\n if (!isLive(link)) return GONE;\n throw error;\n }\n };\n\n /**\n * Sample every connected link once.\n *\n * Only `connected` links are asked. A link still gathering candidates has\n * no traffic to measure, and asking anyway spends a `getStats()` round trip\n * to learn that — per link, on every tick.\n *\n * `settings` arrives as an argument rather than being read from `options`\n * here: the timer is only ever armed past the check that they exist, so\n * reading them again would add a branch that nothing can reach.\n *\n * Each link is sampled on its own: a `getStats()` that rejects — a link\n * closing under the call does that — skips that link for this tick and\n * leaves the others measured. Across the await the link is re-checked by\n * identity, not by id: a peer removed and re-added under the same id holds\n * a new connection, and the old one's sample must not land on it.\n *\n * @param settings - The measuring options, already known to be present.\n */\n const sampleLinks = async (settings: MeshStatsOptions): Promise<void> => {\n if (stopped) return;\n let changed = false;\n\n for (const link of [...links.values()]) {\n if (link.pc.connectionState !== \"connected\") continue;\n let sampler = samplers.get(link.peerId);\n if (sampler === undefined) {\n sampler = createLinkStatsSampler(\n settings.kind === undefined ? {} : { kind: settings.kind },\n );\n samplers.set(link.peerId, sampler);\n }\n let stats: LinkStats;\n try {\n stats = await sampler.sample(link.pc);\n } catch {\n continue;\n }\n const peer = peers.get(link.peerId);\n if (!isLive(link) || peer === undefined) continue;\n peer.stats = stats;\n changed = true;\n settings.onStats?.(link.peerId, stats);\n }\n\n if (changed) emitPeers();\n };\n\n /**\n * Keep the timer alive exactly while there is something to measure.\n *\n * One timer for the whole mesh, not one per peer — and none at all in an\n * empty room, which is where a call sits for as long as the first person is\n * early.\n */\n const syncStatsTimer = (): void => {\n const settings = options.stats;\n if (settings === undefined) return;\n const wanted = !stopped && links.size > 0;\n if (wanted && statsTimer === null) {\n statsTimer = setInterval(() => void sampleLinks(settings), settings.intervalMs ?? 2000);\n return;\n }\n if (!wanted && statsTimer !== null) {\n clearInterval(statsTimer);\n statsTimer = null;\n }\n };\n\n /**\n * Collapse the per-link states into the one badge a call shows.\n *\n * No links at all is `connected`, not a failure: it means nobody else is in\n * the room yet, which is a legitimate state a call sits in for as long as the\n * first person is early.\n */\n const refreshState = (): void => {\n if (stopped) return;\n const states = [...links.values()].map((link) => link.pc.connectionState);\n if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n else options.onState?.(\"connected\", \"alone\");\n };\n\n const applyQuality = async (next: MeshQuality): Promise<void> => {\n if (stopped) return;\n quality = next;\n const effective = scaleForRoom(next, links.size);\n const degradation = resolveDegradation(next, effective);\n for (const link of links.values()) {\n await applyQualityToLink(link.transceivers, slots, effective, degradation);\n }\n };\n\n const makeOffer = async (link: PeerLink): Promise<void> => {\n if (link.makingOffer) return;\n link.makingOffer = true;\n try {\n const offer = await link.pc.createOffer();\n if (!isLive(link)) return;\n if (!offer.sdp) {\n options.onNotice?.(\"empty_local_offer_sdp\");\n return;\n }\n await describe(link.pc, offer);\n if (!isLive(link)) return;\n send({\n type: \"offer\",\n to: link.peerId,\n sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n });\n } catch {\n if (isLive(link)) options.onNotice?.(\"offer_failed\");\n } finally {\n link.makingOffer = false;\n }\n };\n\n /**\n * Wrap an inbound track in the stream the caller renders.\n *\n * The event's own stream is preferred: it is the one the sending side\n * grouped the track into, so a caller that reads `stream.id` sees what the\n * peer published rather than a wrapper invented here. Browsers only fill it\n * when the offer named a stream, hence the fallback — and the guard after\n * that is for a runtime with no `MediaStream` at all, where routing still\n * has to not throw.\n */\n const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n const provided = event.streams?.[0];\n if (provided) return provided;\n return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n };\n\n const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n const slot = slotOf(event.transceiver, link, slots);\n const peer = peers.get(link.peerId);\n if (!slot || !peer) return;\n\n peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n event.track.onended = (): void => {\n const current = peers.get(link.peerId);\n if (!current) return;\n current.streams = { ...current.streams, [slot.name]: null };\n emitPeers();\n };\n emitPeers();\n };\n\n /**\n * Queue one write behind every earlier write to the same slot.\n *\n * Every write that puts a local track on a sender goes through here — the\n * fan-out of `setLocalTrack` and the first write onto a link that just\n * got its transceivers alike. Two writes to one slot therefore never run\n * at once, and a write queued later always finishes later.\n *\n * @param slot - The slot name.\n * @param write - The write to run once the earlier ones have finished.\n * @returns The queued write.\n */\n const enqueueWrite = (slot: string, write: () => Promise<void>): Promise<void> => {\n const queued = (slotWrites.get(slot) ?? Promise.resolve()).then(write);\n slotWrites.set(slot, queued);\n return queued;\n };\n\n /**\n * Put a slot's current track on one link's sender.\n *\n * The track is read when the write runs, not when it was queued, so the\n * last `setLocalTrack` wins whichever write lands last. A refusal\n * belongs to a link that is tearing down, whose state handler removes it,\n * so it is not an error here.\n *\n * @param link - The link to write onto.\n * @param slot - The slot name.\n * @param index - The slot's position in `slots`, which is its transceiver.\n */\n const writeSlot = async (link: PeerLink, slot: string, index: number): Promise<void> => {\n const transceiver = link.transceivers[index];\n if (!transceiver || !isLive(link)) return;\n try {\n await transceiver.sender.replaceTrack(localTracks.get(slot) ?? null);\n } catch {\n return;\n }\n };\n\n /**\n * Publish every held local track onto a link whose transceivers now exist.\n *\n * Up front for the offerer, after the offer lands for the answerer, so a\n * microphone that was already live when the peer joined starts flowing\n * without waiting for a toggle. Each slot's write joins that slot's queue,\n * so a mute issued while it is in flight still has the last word.\n *\n * @param link - The link to publish onto.\n */\n const attachLocalTracks = (link: PeerLink): Promise<unknown> =>\n Promise.all(\n slots.map((slot, index) =>\n enqueueWrite(slot.name, () => writeSlot(link, slot.name, index)),\n ),\n );\n\n const removePeer = (peerId: string): void => {\n if (stopped) return;\n const link = links.get(peerId);\n if (link) {\n link.pc.close();\n links.delete(peerId);\n }\n peers.delete(peerId);\n samplers.delete(peerId);\n void applyQuality(quality);\n syncStatsTimer();\n emitPeers();\n refreshState();\n };\n\n const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n if (stopped || links.has(peerId)) return;\n\n const pc = createPeerConnection(iceServers);\n let transceivers: RTCRtpTransceiver[] = [];\n if (offerer) {\n try {\n transceivers = slots.map((slot) =>\n pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n );\n } catch (error) {\n pc.close();\n throw error;\n }\n }\n const link: PeerLink = {\n peerId,\n pc,\n transceivers,\n pendingCandidates: [],\n remoteReady: false,\n makingOffer: false,\n isOfferer: offerer,\n };\n links.set(peerId, link);\n peers.set(peerId, {\n peerId,\n connection: pc.connectionState,\n streams: peers.get(peerId)?.streams ?? emptyStreams(),\n });\n\n if (offerer) {\n await attachLocalTracks(link);\n if (!isLive(link)) return;\n }\n\n pc.onicecandidate = (event): void => {\n send({\n type: \"ice\",\n to: peerId,\n candidate: event.candidate?.candidate ?? null,\n sdpMid: event.candidate?.sdpMid ?? null,\n sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n });\n };\n\n pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n pc.onconnectionstatechange = (): void => {\n if (!isLive(link)) return;\n const peer = peers.get(peerId);\n if (peer) peer.connection = pc.connectionState;\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n removePeer(peerId);\n return;\n }\n emitPeers();\n refreshState();\n };\n\n pc.onnegotiationneeded = (): void => {\n if (link.isOfferer) void makeOffer(link);\n };\n\n syncStatsTimer();\n emitPeers();\n refreshState();\n if (offerer) await makeOffer(link);\n };\n\n const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n if (!links.has(from)) await addPeer(from, { offerer: false });\n const link = links.get(from);\n if (!link || !isLive(link)) return;\n\n if (\n (await whileLive(link, link.pc.setRemoteDescription({ type: \"offer\", sdp }))) === GONE\n ) {\n return;\n }\n link.remoteReady = true;\n await drainCandidates(link);\n\n if (!link.isOfferer) {\n adoptTransceivers(link);\n await attachLocalTracks(link);\n }\n if (!isLive(link)) return;\n\n const answer = await whileLive(link, link.pc.createAnswer());\n if (answer === GONE) return;\n if (!answer.sdp) {\n options.onNotice?.(\"empty_local_answer_sdp\");\n return;\n }\n if ((await whileLive(link, describe(link.pc, answer))) === GONE) return;\n send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n await applyQuality(quality);\n };\n\n const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n if (stopped) return;\n if (message.type === \"offer\") {\n await acceptOffer(message.from, message.sdp);\n return;\n }\n\n const link = links.get(message.from);\n if (!link) return;\n\n if (message.type === \"answer\") {\n const applied = link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n if ((await whileLive(link, applied)) === GONE) return;\n link.remoteReady = true;\n await drainCandidates(link);\n await applyQuality(quality);\n return;\n }\n\n if (message.candidate === null) return;\n await acceptCandidate(link, {\n candidate: message.candidate,\n sdpMid: message.sdpMid ?? undefined,\n sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n });\n };\n\n /**\n * Publish or retract one slot, one write at a time per slot.\n *\n * Writes to a slot are chained, and each publishes whatever the slot holds\n * when it runs. Without the chain, an unmute whose `replaceTrack` is still\n * in flight could finish after the mute that followed it, leaving the\n * sender on the microphone while the mesh believes the slot is empty.\n */\n const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n if (stopped) return;\n localTracks.set(slot, track);\n const index = slots.findIndex((entry) => entry.name === slot);\n if (index < 0) return;\n\n await enqueueWrite(slot, async () => {\n for (const link of [...links.values()]) await writeSlot(link, slot, index);\n });\n await applyQuality(quality);\n };\n\n const stop = (): void => {\n if (stopped) return;\n stopped = true;\n for (const link of links.values()) link.pc.close();\n links.clear();\n peers.clear();\n samplers.clear();\n localTracks.clear();\n syncStatsTimer();\n emitPeers();\n options.onState?.(\"closed\");\n };\n\n options.onState?.(\"connecting\", \"signaling\");\n\n return {\n addPeer,\n removePeer,\n accept,\n setLocalTrack,\n applyQuality,\n setIceServers: (servers): void => {\n iceServers = servers;\n },\n stop,\n getConnection: (peerId: string): RTCPeerConnection | null => links.get(peerId)?.pc ?? null,\n get peers(): MeshPeer[] {\n return [...peers.values()].map((peer) => ({ ...peer }));\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"peer-mesh.cjs","names":["createLinkStatsSampler","scaleForRoom","resolveDegradation","applyQualityToLink","slotOf","createPeerConnection","drainCandidates","acceptCandidate","require_link_stats","require_mesh_quality","require_peer_link","GONE","createPeerMesh","options","links","peers","localTracks","slotWrites","iceServers","describe","connection","description","quality","stopped","samplers","statsTimer","emptyStreams","slots","slot","emitPeers","peer","isLive","link","whileLive","value","step","error","sampleLinks","changed","sampler","settings","stats","syncStatsTimer","wanted","refreshState","states","state","applyQuality","next","effective","degradation","makeOffer","offer","send","streamFor","event","attachRemoteTrack","current","enqueueWrite","queued","write","writeSlot","transceiver","index","attachLocalTracks","removePeer","peerId","addPeer","pc","transceivers","offerer","acceptOffer","from","answer","message","applied","track","entry","servers"],"sources":["../../src/webrtc/peer-mesh.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the mesh is one object's lifetime: a link's\n * transceivers, the offer it owes, the tracks published on it and the share of\n * the uplink it costs all change together, and every method here reads or writes\n * the same two maps. The pieces that *could* stand alone already do —\n * `peer-link.ts` for the per-connection mechanics and `mesh-quality.ts` for the\n * division — and what is left is the coordination between them.\n */\nimport { applyQualityToLink, resolveDegradation, scaleForRoom } from \"./mesh-quality\";\nimport {\n acceptCandidate,\n adoptTransceivers,\n createPeerConnection,\n drainCandidates,\n slotOf,\n type PeerLink,\n} from \"./peer-link\";\nimport { createLinkStatsSampler, type LinkStats, type LinkStatsSampler } from \"./link-stats\";\nimport type {\n MeshMessage,\n MeshPeer,\n MeshQuality,\n MeshStatsOptions,\n PeerMeshOptions,\n} from \"./mesh-types\";\n\n/** What {@link createPeerMesh}'s negotiation steps return once their link is gone. */\nconst GONE = Symbol(\"gone\");\n\n/** A running mesh. */\nexport interface PeerMesh {\n /**\n * Open a link to a peer.\n *\n * `offerer` decides which side sends the offer, and exactly one side of each\n * link must say `true`. Drive it from the arrival order your server already\n * imposes — the peer that joined **later** offers, the one already in the\n * room answers. That avoids glare by construction, without the rollback\n * dance of perfect negotiation and without depending on a browser\n * implementing argument-less `setLocalDescription`.\n */\n addPeer: (peerId: string, options: { offerer: boolean }) => Promise<void>;\n /** Close one link and give its share of the uplink back to the others. */\n removePeer: (peerId: string) => void;\n /** Feed the mesh an `offer`, `answer` or `ice` message from `from`. */\n accept: (message: MeshMessage & { from: string }) => Promise<void>;\n /**\n * Publish or retract one slot across every live link.\n *\n * `null` retracts: the sender keeps its negotiated transceiver and stops\n * producing, which is what mutes a microphone without disturbing the other\n * slots or the connection. Nothing renegotiates — the m-lines were allocated\n * before any track existed, which is what makes a mesh survive toggling: a\n * renegotiation per toggle is N-1 simultaneous offer/answer rounds every time\n * somebody unmutes.\n */\n setLocalTrack: (slot: string, track: MediaStreamTrack | null) => Promise<void>;\n /** Apply encoder limits, divided by the current size of the room. */\n applyQuality: (quality: MeshQuality) => Promise<void>;\n /** Replace the ICE servers used for links opened from now on. */\n setIceServers: (servers: RTCIceServer[]) => void;\n /**\n * Close every link, forget the local tracks and report `closed` — for good.\n *\n * Stopping is terminal. The signalling socket usually outlives the call, so\n * an offer still in flight from a former participant, or from anyone the\n * server relays to this user, keeps reaching {@link PeerMesh.accept} after\n * the hang-up. A stopped mesh drops it: no connection is opened, nothing is\n * answered and no track is published, so a call that reports `closed` never\n * sends the microphone or camera again. The same holds for `addPeer`,\n * `setLocalTrack` and `applyQuality`, and for every negotiation that was\n * already awaiting the browser when `stop()` ran. Calling it twice is a\n * no-op.\n *\n * The tracks themselves are not stopped: they belong to the caller, who may\n * still be showing them in a preview or reusing them on the next call. To\n * join again, create a new mesh.\n */\n stop: () => void;\n /** The peers as the mesh currently sees them. */\n readonly peers: MeshPeer[];\n /**\n * The connection carrying one peer's link, or `null`.\n *\n * The escape hatch for everything this mesh does not model. `getStats()`,\n * `getSenders()`, an `RTCDataChannel`, an encoding tweak the `quality`\n * shape has no field for — all of it lives on the connection, and a mesh\n * that keeps it private turns adopting the mesh into losing a feature.\n *\n * {@link MeshPeer.connection} is the *state*, which is what a view needs;\n * this is the object, which is what a measurement needs. For the common\n * case of measuring, {@link PeerMeshOptions.stats} spares you the loop —\n * including the one-sampler-per-connection rule that a hand-rolled version\n * gets wrong.\n *\n * @param peerId - The id the peer was added under.\n * @returns The connection, or `null` when there is no link for that id.\n */\n getConnection: (peerId: string) => RTCPeerConnection | null;\n}\n\n/**\n * One `RTCPeerConnection` per peer, with the slots negotiated up front.\n *\n * The signalling protocol stays yours: this produces and accepts three message\n * shapes (`offer`, `answer`, `ice`) and leaves delivery, room membership and\n * identity to your server. What it owns is the part that is the same in every\n * mesh and wrong in most of them — see {@link PeerMesh} for the traps each\n * method exists to avoid.\n *\n * @param options - See {@link PeerMeshOptions}.\n * @returns The mesh.\n *\n * @example\n * const mesh = createPeerMesh({\n * slots: [\n * { name: \"mic\", kind: \"audio\" },\n * { name: \"cam\", kind: \"video\" },\n * ],\n * send: (message) => socket.send(message),\n * onPeers: setPeers,\n * });\n * socket.onMessage = (message) => void mesh.accept(message);\n * await mesh.addPeer(\"peer-2\", { offerer: true });\n * await mesh.setLocalTrack(\"mic\", micTrack);\n */\nexport function createPeerMesh(options: PeerMeshOptions): PeerMesh {\n const { slots, send } = options;\n const links = new Map<string, PeerLink>();\n const peers = new Map<string, MeshPeer>();\n const localTracks = new Map<string, MediaStreamTrack | null>();\n const slotWrites = new Map<string, Promise<void>>();\n let iceServers = options.iceServers ?? [];\n const describe =\n options.setLocalDescription ??\n ((connection: RTCPeerConnection, description: RTCSessionDescriptionInit) =>\n connection.setLocalDescription(description));\n let quality: MeshQuality = options.quality ?? {};\n let stopped = false;\n const samplers = new Map<string, LinkStatsSampler>();\n let statsTimer: ReturnType<typeof setInterval> | null = null;\n\n const emptyStreams = (): Record<string, MediaStream | null> =>\n Object.fromEntries(slots.map((slot) => [slot.name, null]));\n\n const emitPeers = (): void => {\n options.onPeers?.([...peers.values()].map((peer) => ({ ...peer })));\n };\n\n /**\n * Whether a link is still the one the mesh holds for its peer.\n *\n * Every negotiation step awaits the browser, and `stop()` or `removePeer()`\n * can close the link inside any of those awaits. A continuation that does\n * not re-check keeps negotiating on a closed connection — which a browser\n * answers with `InvalidStateError` — or, worse, signals an answer for a call\n * that already hung up.\n *\n * @param link - The link a continuation is about to touch.\n * @returns `false` once the mesh stopped or the link was removed or replaced.\n */\n const isLive = (link: PeerLink): boolean => !stopped && links.get(link.peerId) === link;\n\n /**\n * Await one negotiation step, and tell the caller whether to go on.\n *\n * A browser settles a call that `close()` ran under in one of two ways:\n * it rejects with `InvalidStateError`, or — what Chromium was measured\n * doing — it never settles at all. The second needs nothing here; the\n * first would surface as a rejected `accept()`, which the documented\n * `void mesh.accept(message)` wiring turns into an unhandled rejection\n * for a call that already hung up. So a rejection on a link that is no\n * longer live is swallowed, and one on a live link is rethrown untouched.\n *\n * @param link - The link the step negotiates on.\n * @param step - The pending browser call.\n * @returns The step's value, or {@link GONE} when the link stopped being live.\n */\n const whileLive = async <T>(link: PeerLink, step: Promise<T>): Promise<T | typeof GONE> => {\n try {\n const value = await step;\n return isLive(link) ? value : GONE;\n } catch (error) {\n if (!isLive(link)) return GONE;\n throw error;\n }\n };\n\n /**\n * Sample every connected link once.\n *\n * Only `connected` links are asked. A link still gathering candidates has\n * no traffic to measure, and asking anyway spends a `getStats()` round trip\n * to learn that — per link, on every tick.\n *\n * `settings` arrives as an argument rather than being read from `options`\n * here: the timer is only ever armed past the check that they exist, so\n * reading them again would add a branch that nothing can reach.\n *\n * Each link is sampled on its own: a `getStats()` that rejects — a link\n * closing under the call does that — skips that link for this tick and\n * leaves the others measured. Across the await the link is re-checked by\n * identity, not by id: a peer removed and re-added under the same id holds\n * a new connection, and the old one's sample must not land on it.\n *\n * @param settings - The measuring options, already known to be present.\n */\n const sampleLinks = async (settings: MeshStatsOptions): Promise<void> => {\n if (stopped) return;\n let changed = false;\n\n for (const link of [...links.values()]) {\n if (link.pc.connectionState !== \"connected\") continue;\n let sampler = samplers.get(link.peerId);\n if (sampler === undefined) {\n sampler = createLinkStatsSampler(\n settings.kind === undefined ? {} : { kind: settings.kind },\n );\n samplers.set(link.peerId, sampler);\n }\n let stats: LinkStats;\n try {\n stats = await sampler.sample(link.pc);\n } catch {\n continue;\n }\n const peer = peers.get(link.peerId);\n if (!isLive(link) || peer === undefined) continue;\n peer.stats = stats;\n changed = true;\n settings.onStats?.(link.peerId, stats);\n }\n\n if (changed) emitPeers();\n };\n\n /**\n * Keep the timer alive exactly while there is something to measure.\n *\n * One timer for the whole mesh, not one per peer — and none at all in an\n * empty room, which is where a call sits for as long as the first person is\n * early.\n */\n const syncStatsTimer = (): void => {\n const settings = options.stats;\n if (settings === undefined) return;\n const wanted = !stopped && links.size > 0;\n if (wanted && statsTimer === null) {\n statsTimer = setInterval(() => void sampleLinks(settings), settings.intervalMs ?? 2000);\n return;\n }\n if (!wanted && statsTimer !== null) {\n clearInterval(statsTimer);\n statsTimer = null;\n }\n };\n\n /**\n * Collapse the per-link states into the one badge a call shows.\n *\n * No links at all is `connected`, not a failure: it means nobody else is in\n * the room yet, which is a legitimate state a call sits in for as long as the\n * first person is early.\n */\n const refreshState = (): void => {\n if (stopped) return;\n const states = [...links.values()].map((link) => link.pc.connectionState);\n if (states.some((state) => state === \"connected\")) options.onState?.(\"connected\");\n else if (states.length > 0) options.onState?.(\"connecting\", \"negotiating\");\n else options.onState?.(\"connected\", \"alone\");\n };\n\n const applyQuality = async (next: MeshQuality): Promise<void> => {\n if (stopped) return;\n quality = next;\n const effective = scaleForRoom(next, links.size);\n const degradation = resolveDegradation(next, effective);\n for (const link of links.values()) {\n await applyQualityToLink(link.transceivers, slots, effective, degradation);\n }\n };\n\n const makeOffer = async (link: PeerLink): Promise<void> => {\n if (link.makingOffer) return;\n link.makingOffer = true;\n try {\n const offer = await link.pc.createOffer();\n if (!isLive(link)) return;\n if (!offer.sdp) {\n options.onNotice?.(\"empty_local_offer_sdp\");\n return;\n }\n await describe(link.pc, offer);\n if (!isLive(link)) return;\n send({\n type: \"offer\",\n to: link.peerId,\n sdp: link.pc.localDescription?.sdp ?? offer.sdp,\n });\n } catch {\n if (isLive(link)) options.onNotice?.(\"offer_failed\");\n } finally {\n link.makingOffer = false;\n }\n };\n\n /**\n * Wrap an inbound track in the stream the caller renders.\n *\n * The event's own stream is preferred: it is the one the sending side\n * grouped the track into, so a caller that reads `stream.id` sees what the\n * peer published rather than a wrapper invented here. Browsers only fill it\n * when the offer named a stream, hence the fallback — and the guard after\n * that is for a runtime with no `MediaStream` at all, where routing still\n * has to not throw.\n */\n const streamFor = (event: RTCTrackEvent): MediaStream | null => {\n const provided = event.streams?.[0];\n if (provided) return provided;\n return typeof MediaStream === \"undefined\" ? null : new MediaStream([event.track]);\n };\n\n const attachRemoteTrack = (link: PeerLink, event: RTCTrackEvent): void => {\n const slot = slotOf(event.transceiver, link, slots);\n const peer = peers.get(link.peerId);\n if (!slot || !peer) return;\n\n peer.streams = { ...peer.streams, [slot.name]: streamFor(event) };\n event.track.onended = (): void => {\n const current = peers.get(link.peerId);\n if (!current) return;\n current.streams = { ...current.streams, [slot.name]: null };\n emitPeers();\n };\n emitPeers();\n };\n\n /**\n * Queue one write behind every earlier write to the same slot.\n *\n * Every write that puts a local track on a sender goes through here — the\n * fan-out of `setLocalTrack` and the first write onto a link that just\n * got its transceivers alike. Two writes to one slot therefore never run\n * at once, and a write queued later always finishes later.\n *\n * @param slot - The slot name.\n * @param write - The write to run once the earlier ones have finished.\n * @returns The queued write.\n */\n const enqueueWrite = (slot: string, write: () => Promise<void>): Promise<void> => {\n const queued = (slotWrites.get(slot) ?? Promise.resolve()).then(write);\n slotWrites.set(slot, queued);\n return queued;\n };\n\n /**\n * Put a slot's current track on one link's sender.\n *\n * The track is read when the write runs, not when it was queued, so the\n * last `setLocalTrack` wins whichever write lands last. A refusal\n * belongs to a link that is tearing down, whose state handler removes it,\n * so it is not an error here.\n *\n * @param link - The link to write onto.\n * @param slot - The slot name.\n * @param index - The slot's position in `slots`, which is its transceiver.\n */\n const writeSlot = async (link: PeerLink, slot: string, index: number): Promise<void> => {\n const transceiver = link.transceivers[index];\n if (!transceiver || !isLive(link)) return;\n try {\n await transceiver.sender.replaceTrack(localTracks.get(slot) ?? null);\n } catch {\n return;\n }\n };\n\n /**\n * Publish every held local track onto a link whose transceivers now exist.\n *\n * Up front for the offerer, after the offer lands for the answerer, so a\n * microphone that was already live when the peer joined starts flowing\n * without waiting for a toggle. Each slot's write joins that slot's queue,\n * so a mute issued while it is in flight still has the last word.\n *\n * @param link - The link to publish onto.\n */\n const attachLocalTracks = (link: PeerLink): Promise<unknown> =>\n Promise.all(\n slots.map((slot, index) =>\n enqueueWrite(slot.name, () => writeSlot(link, slot.name, index)),\n ),\n );\n\n const removePeer = (peerId: string): void => {\n if (stopped) return;\n const link = links.get(peerId);\n if (link) {\n link.pc.close();\n links.delete(peerId);\n }\n peers.delete(peerId);\n samplers.delete(peerId);\n void applyQuality(quality);\n syncStatsTimer();\n emitPeers();\n refreshState();\n };\n\n const addPeer = async (peerId: string, { offerer }: { offerer: boolean }): Promise<void> => {\n if (stopped || links.has(peerId)) return;\n\n const pc = createPeerConnection(iceServers);\n let transceivers: RTCRtpTransceiver[] = [];\n if (offerer) {\n try {\n transceivers = slots.map((slot) =>\n pc.addTransceiver(slot.kind, { direction: \"sendrecv\" }),\n );\n } catch (error) {\n pc.close();\n throw error;\n }\n }\n const link: PeerLink = {\n peerId,\n pc,\n transceivers,\n pendingCandidates: [],\n remoteReady: false,\n makingOffer: false,\n isOfferer: offerer,\n };\n links.set(peerId, link);\n peers.set(peerId, {\n peerId,\n connection: pc.connectionState,\n streams: peers.get(peerId)?.streams ?? emptyStreams(),\n });\n\n if (offerer) {\n await attachLocalTracks(link);\n if (!isLive(link)) return;\n }\n\n pc.onicecandidate = (event): void => {\n send({\n type: \"ice\",\n to: peerId,\n candidate: event.candidate?.candidate ?? null,\n sdpMid: event.candidate?.sdpMid ?? null,\n sdpMLineIndex: event.candidate?.sdpMLineIndex ?? null,\n });\n };\n\n pc.ontrack = (event): void => attachRemoteTrack(link, event);\n\n pc.onconnectionstatechange = (): void => {\n if (!isLive(link)) return;\n const peer = peers.get(peerId);\n if (peer) peer.connection = pc.connectionState;\n if (pc.connectionState === \"failed\" || pc.connectionState === \"closed\") {\n removePeer(peerId);\n return;\n }\n emitPeers();\n refreshState();\n };\n\n pc.onnegotiationneeded = (): void => {\n if (link.isOfferer) void makeOffer(link);\n };\n\n syncStatsTimer();\n emitPeers();\n refreshState();\n if (offerer) await makeOffer(link);\n };\n\n const acceptOffer = async (from: string, sdp: string): Promise<void> => {\n if (!links.has(from)) {\n if (options.acceptPeer && !options.acceptPeer(from)) {\n options.onNotice?.(\"offer_refused\");\n return;\n }\n await addPeer(from, { offerer: false });\n }\n const link = links.get(from);\n if (!link || !isLive(link)) return;\n\n if (\n (await whileLive(link, link.pc.setRemoteDescription({ type: \"offer\", sdp }))) === GONE\n ) {\n return;\n }\n link.remoteReady = true;\n await drainCandidates(link);\n\n if (!link.isOfferer) {\n adoptTransceivers(link);\n await attachLocalTracks(link);\n }\n if (!isLive(link)) return;\n\n const answer = await whileLive(link, link.pc.createAnswer());\n if (answer === GONE) return;\n if (!answer.sdp) {\n options.onNotice?.(\"empty_local_answer_sdp\");\n return;\n }\n if ((await whileLive(link, describe(link.pc, answer))) === GONE) return;\n send({ type: \"answer\", to: from, sdp: link.pc.localDescription?.sdp ?? answer.sdp });\n await applyQuality(quality);\n };\n\n const accept = async (message: MeshMessage & { from: string }): Promise<void> => {\n if (stopped) return;\n if (message.type === \"offer\") {\n await acceptOffer(message.from, message.sdp);\n return;\n }\n\n const link = links.get(message.from);\n if (!link) return;\n\n if (message.type === \"answer\") {\n const applied = link.pc.setRemoteDescription({ type: \"answer\", sdp: message.sdp });\n if ((await whileLive(link, applied)) === GONE) return;\n link.remoteReady = true;\n await drainCandidates(link);\n await applyQuality(quality);\n return;\n }\n\n if (message.candidate === null) return;\n await acceptCandidate(link, {\n candidate: message.candidate,\n sdpMid: message.sdpMid ?? undefined,\n sdpMLineIndex: message.sdpMLineIndex ?? undefined,\n });\n };\n\n /**\n * Publish or retract one slot, one write at a time per slot.\n *\n * Writes to a slot are chained, and each publishes whatever the slot holds\n * when it runs. Without the chain, an unmute whose `replaceTrack` is still\n * in flight could finish after the mute that followed it, leaving the\n * sender on the microphone while the mesh believes the slot is empty.\n */\n const setLocalTrack = async (slot: string, track: MediaStreamTrack | null): Promise<void> => {\n if (stopped) return;\n localTracks.set(slot, track);\n const index = slots.findIndex((entry) => entry.name === slot);\n if (index < 0) return;\n\n await enqueueWrite(slot, async () => {\n for (const link of [...links.values()]) await writeSlot(link, slot, index);\n });\n await applyQuality(quality);\n };\n\n const stop = (): void => {\n if (stopped) return;\n stopped = true;\n for (const link of links.values()) link.pc.close();\n links.clear();\n peers.clear();\n samplers.clear();\n localTracks.clear();\n syncStatsTimer();\n emitPeers();\n options.onState?.(\"closed\");\n };\n\n options.onState?.(\"connecting\", \"signaling\");\n\n return {\n addPeer,\n removePeer,\n accept,\n setLocalTrack,\n applyQuality,\n setIceServers: (servers): void => {\n iceServers = servers;\n },\n stop,\n getConnection: (peerId: string): RTCPeerConnection | null => links.get(peerId)?.pc ?? null,\n get peers(): MeshPeer[] {\n return [...peers.values()].map((peer) => ({ ...peer }));\n },\n };\n}\n"],"mappings":"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"}