@agent-native/core 0.98.3 → 0.98.5

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 (230) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +29 -0
  3. package/corpus/core/docs/content/durable-background-runs.mdx +37 -0
  4. package/corpus/core/docs/content/locales/ar-SA/tracking.mdx +2 -0
  5. package/corpus/core/docs/content/locales/de-DE/tracking.mdx +2 -0
  6. package/corpus/core/docs/content/locales/es-ES/tracking.mdx +2 -0
  7. package/corpus/core/docs/content/locales/fr-FR/tracking.mdx +4 -2
  8. package/corpus/core/docs/content/locales/hi-IN/tracking.mdx +2 -0
  9. package/corpus/core/docs/content/locales/ja-JP/tracking.mdx +2 -0
  10. package/corpus/core/docs/content/locales/ko-KR/tracking.mdx +2 -0
  11. package/corpus/core/docs/content/locales/pt-BR/tracking.mdx +2 -0
  12. package/corpus/core/docs/content/locales/zh-CN/tracking.mdx +2 -0
  13. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  14. package/corpus/core/docs/content/locales/zh-TW/tracking.mdx +2 -0
  15. package/corpus/core/docs/content/tracking.mdx +2 -0
  16. package/corpus/core/package.json +1 -1
  17. package/corpus/core/src/agent/durable-background.ts +16 -16
  18. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +25 -8
  19. package/corpus/core/src/agent/engine/anthropic-engine.ts +17 -3
  20. package/corpus/core/src/agent/engine/output-tokens.ts +6 -6
  21. package/corpus/core/src/agent/production-agent.ts +200 -27
  22. package/corpus/core/src/agent/run-manager.ts +81 -1
  23. package/corpus/core/src/agent/run-store.ts +301 -41
  24. package/corpus/core/src/client/AgentPanel.tsx +6 -2
  25. package/corpus/core/src/client/AssistantChat.tsx +364 -69
  26. package/corpus/core/src/client/agent-chat-adapter.ts +169 -1
  27. package/corpus/core/src/client/analytics.ts +12 -0
  28. package/corpus/core/src/client/chat/message-components.tsx +3 -0
  29. package/corpus/core/src/client/chat/tool-call-display.tsx +20 -36
  30. package/corpus/core/src/client/conversation/use-near-bottom-autoscroll.ts +7 -0
  31. package/corpus/core/src/client/extensions/AgentNativeExtensionFrame.tsx +2 -0
  32. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +2 -0
  33. package/corpus/core/src/client/extensions/ExtensionEditor.tsx +9 -2
  34. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +2 -0
  35. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +2 -0
  36. package/corpus/core/src/client/extensions/portable-extension.ts +2 -0
  37. package/corpus/core/src/client/session-replay.ts +971 -53
  38. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -3
  39. package/corpus/core/src/client/sse-event-processor.ts +43 -1
  40. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  41. package/corpus/core/src/demo/config.ts +6 -6
  42. package/corpus/core/src/demo/fetch-interceptor.ts +34 -2
  43. package/corpus/core/src/demo/redact.ts +71 -158
  44. package/corpus/core/src/extensions/html-shell.ts +3 -0
  45. package/corpus/core/src/extensions/session-replay-iframe.ts +131 -0
  46. package/corpus/core/src/scripts/agent-engines/list-agent-engines.ts +1 -20
  47. package/corpus/core/src/session-replay-iframe-protocol.ts +58 -0
  48. package/corpus/core/src/shared/reasoning-effort.ts +35 -0
  49. package/corpus/core/src/styles/agent-conversation.css +26 -0
  50. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  51. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +67 -16
  52. package/corpus/templates/analytics/AGENTS.md +12 -0
  53. package/corpus/templates/analytics/actions/get-error-issue.ts +1 -0
  54. package/corpus/templates/analytics/actions/get-session-replay-events.ts +1 -0
  55. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +1 -0
  56. package/corpus/templates/analytics/actions/list-error-issues.ts +1 -0
  57. package/corpus/templates/analytics/actions/list-session-recordings.ts +1 -0
  58. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -1
  59. package/corpus/templates/analytics/app/global.css +6 -64
  60. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +41 -4
  61. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +143 -121
  62. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +34 -12
  63. package/corpus/templates/analytics/changelog/2026-07-12-analytics-chat-retries-before-showing-a-generic-no-data-mess.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-07-12-analytics-sessions-search-no-longer-loses-fast-keystrokes-an.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-07-12-chart-selection-respects-chat-state.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-anonymizes-error-reporting-emails.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-07-12-fixed-session-replays-that-appeared-ultra-wide-hid-recorded-.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-load-faster-in-demo-mode-while-visitor-email.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-07-12-session-replays-show-iframe-content.md +6 -0
  70. package/corpus/templates/analytics/netlify.toml +0 -3
  71. package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +14 -0
  72. package/corpus/templates/analytics/server/lib/error-capture.ts +59 -2
  73. package/corpus/templates/analytics/server/lib/real-data-actions.ts +24 -0
  74. package/corpus/templates/analytics/server/lib/session-replay.ts +2 -9
  75. package/corpus/templates/analytics/server/plugins/agent-chat.ts +39 -6
  76. package/corpus/templates/assets/app/global.css +1 -1
  77. package/corpus/templates/brain/app/global.css +2 -2
  78. package/corpus/templates/brain/changelog/2026-07-12-fix-long-response-progress.md +6 -0
  79. package/corpus/templates/calendar/app/components/calendar/DeleteEventDialog.tsx +11 -2
  80. package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +14 -2
  81. package/corpus/templates/calendar/changelog/2026-07-12-calendar-responses-can-now-be-saved-with-command-enter-or-ct.md +6 -0
  82. package/corpus/templates/forms/app/components/CloudUpgrade.tsx +1 -1
  83. package/corpus/templates/forms/app/components/ThemeToggle.tsx +22 -6
  84. package/corpus/templates/forms/app/components/builder/FieldPropertiesPanel.tsx +5 -5
  85. package/corpus/templates/forms/app/components/builder/FieldRenderer.tsx +3 -3
  86. package/corpus/templates/forms/app/components/layout/Sidebar.tsx +16 -16
  87. package/corpus/templates/forms/app/global.css +110 -5
  88. package/corpus/templates/forms/app/pages/AskPage.tsx +3 -3
  89. package/corpus/templates/forms/app/pages/FormBuilderPage.tsx +134 -52
  90. package/corpus/templates/forms/app/pages/FormFillPage.tsx +44 -11
  91. package/corpus/templates/forms/app/pages/FormsListPage.tsx +42 -24
  92. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +28 -15
  93. package/corpus/templates/forms/app/pages/ResponsesPage.tsx +38 -15
  94. package/corpus/templates/forms/app/routes/_app.settings.tsx +1 -1
  95. package/corpus/templates/forms/changelog/2026-07-12-form-builder-surfaces-are-calmer-fields-stay-stable-on-hover.md +6 -0
  96. package/corpus/templates/forms/changelog/2026-07-12-smoother-more-tactile-controls-throughout-animated-icon-and-.md +6 -0
  97. package/corpus/templates/mail/app/components/email/EmailThread.tsx +21 -13
  98. package/corpus/templates/mail/app/components/email/email-iframe-document.ts +18 -0
  99. package/dist/agent/durable-background.d.ts +10 -10
  100. package/dist/agent/durable-background.d.ts.map +1 -1
  101. package/dist/agent/durable-background.js +16 -16
  102. package/dist/agent/durable-background.js.map +1 -1
  103. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  104. package/dist/agent/engine/ai-sdk-engine.js +19 -8
  105. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  106. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  107. package/dist/agent/engine/anthropic-engine.js +11 -3
  108. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  109. package/dist/agent/engine/output-tokens.js +6 -6
  110. package/dist/agent/engine/output-tokens.js.map +1 -1
  111. package/dist/agent/production-agent.d.ts +52 -0
  112. package/dist/agent/production-agent.d.ts.map +1 -1
  113. package/dist/agent/production-agent.js +154 -26
  114. package/dist/agent/production-agent.js.map +1 -1
  115. package/dist/agent/run-manager.d.ts +9 -0
  116. package/dist/agent/run-manager.d.ts.map +1 -1
  117. package/dist/agent/run-manager.js +71 -1
  118. package/dist/agent/run-manager.js.map +1 -1
  119. package/dist/agent/run-store.d.ts +9 -0
  120. package/dist/agent/run-store.d.ts.map +1 -1
  121. package/dist/agent/run-store.js +243 -48
  122. package/dist/agent/run-store.js.map +1 -1
  123. package/dist/client/AgentPanel.d.ts.map +1 -1
  124. package/dist/client/AgentPanel.js +6 -2
  125. package/dist/client/AgentPanel.js.map +1 -1
  126. package/dist/client/AssistantChat.d.ts +21 -0
  127. package/dist/client/AssistantChat.d.ts.map +1 -1
  128. package/dist/client/AssistantChat.js +298 -67
  129. package/dist/client/AssistantChat.js.map +1 -1
  130. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  131. package/dist/client/agent-chat-adapter.js +146 -1
  132. package/dist/client/agent-chat-adapter.js.map +1 -1
  133. package/dist/client/analytics.d.ts.map +1 -1
  134. package/dist/client/analytics.js +11 -0
  135. package/dist/client/analytics.js.map +1 -1
  136. package/dist/client/chat/message-components.d.ts.map +1 -1
  137. package/dist/client/chat/message-components.js +2 -1
  138. package/dist/client/chat/message-components.js.map +1 -1
  139. package/dist/client/chat/tool-call-display.d.ts +3 -1
  140. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  141. package/dist/client/chat/tool-call-display.js +13 -37
  142. package/dist/client/chat/tool-call-display.js.map +1 -1
  143. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts +1 -0
  144. package/dist/client/conversation/use-near-bottom-autoscroll.d.ts.map +1 -1
  145. package/dist/client/conversation/use-near-bottom-autoscroll.js +6 -0
  146. package/dist/client/conversation/use-near-bottom-autoscroll.js.map +1 -1
  147. package/dist/client/extensions/AgentNativeExtensionFrame.d.ts.map +1 -1
  148. package/dist/client/extensions/AgentNativeExtensionFrame.js +2 -1
  149. package/dist/client/extensions/AgentNativeExtensionFrame.js.map +1 -1
  150. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  151. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  152. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  153. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  154. package/dist/client/extensions/ExtensionEditor.js +5 -2
  155. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  156. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  157. package/dist/client/extensions/ExtensionViewer.js +2 -1
  158. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  159. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -1
  160. package/dist/client/extensions/InlineExtensionFrame.js +2 -1
  161. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -1
  162. package/dist/client/extensions/portable-extension.d.ts.map +1 -1
  163. package/dist/client/extensions/portable-extension.js +2 -0
  164. package/dist/client/extensions/portable-extension.js.map +1 -1
  165. package/dist/client/session-replay.d.ts +12 -3
  166. package/dist/client/session-replay.d.ts.map +1 -1
  167. package/dist/client/session-replay.js +793 -49
  168. package/dist/client/session-replay.js.map +1 -1
  169. package/dist/client/settings/DemoModeSection.js +1 -1
  170. package/dist/client/settings/DemoModeSection.js.map +1 -1
  171. package/dist/client/sse-event-processor.d.ts +5 -0
  172. package/dist/client/sse-event-processor.d.ts.map +1 -1
  173. package/dist/client/sse-event-processor.js +37 -1
  174. package/dist/client/sse-event-processor.js.map +1 -1
  175. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  176. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  177. package/dist/demo/config.js +6 -6
  178. package/dist/demo/config.js.map +1 -1
  179. package/dist/demo/fetch-interceptor.d.ts +1 -0
  180. package/dist/demo/fetch-interceptor.d.ts.map +1 -1
  181. package/dist/demo/fetch-interceptor.js +28 -2
  182. package/dist/demo/fetch-interceptor.js.map +1 -1
  183. package/dist/demo/redact.d.ts +14 -7
  184. package/dist/demo/redact.d.ts.map +1 -1
  185. package/dist/demo/redact.js +27 -155
  186. package/dist/demo/redact.js.map +1 -1
  187. package/dist/extensions/html-shell.d.ts.map +1 -1
  188. package/dist/extensions/html-shell.js +2 -0
  189. package/dist/extensions/html-shell.js.map +1 -1
  190. package/dist/extensions/session-replay-iframe.d.ts +10 -0
  191. package/dist/extensions/session-replay-iframe.d.ts.map +1 -0
  192. package/dist/extensions/session-replay-iframe.js +121 -0
  193. package/dist/extensions/session-replay-iframe.js.map +1 -0
  194. package/dist/notifications/routes.d.ts +1 -1
  195. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  196. package/dist/scripts/agent-engines/list-agent-engines.d.ts.map +1 -1
  197. package/dist/scripts/agent-engines/list-agent-engines.js +1 -19
  198. package/dist/scripts/agent-engines/list-agent-engines.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  200. package/dist/server/transcribe-voice.d.ts +1 -1
  201. package/dist/session-replay-iframe-protocol.d.ts +36 -0
  202. package/dist/session-replay-iframe-protocol.d.ts.map +1 -0
  203. package/dist/session-replay-iframe-protocol.js +24 -0
  204. package/dist/session-replay-iframe-protocol.js.map +1 -0
  205. package/dist/shared/reasoning-effort.d.ts +11 -0
  206. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  207. package/dist/shared/reasoning-effort.js +36 -0
  208. package/dist/shared/reasoning-effort.js.map +1 -1
  209. package/dist/styles/agent-conversation.css +26 -0
  210. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  211. package/docs/content/durable-background-runs.mdx +37 -0
  212. package/docs/content/locales/ar-SA/tracking.mdx +2 -0
  213. package/docs/content/locales/de-DE/tracking.mdx +2 -0
  214. package/docs/content/locales/es-ES/tracking.mdx +2 -0
  215. package/docs/content/locales/fr-FR/tracking.mdx +4 -2
  216. package/docs/content/locales/hi-IN/tracking.mdx +2 -0
  217. package/docs/content/locales/ja-JP/tracking.mdx +2 -0
  218. package/docs/content/locales/ko-KR/tracking.mdx +2 -0
  219. package/docs/content/locales/pt-BR/tracking.mdx +2 -0
  220. package/docs/content/locales/zh-CN/tracking.mdx +2 -0
  221. package/docs/content/locales/zh-TW/durable-background-runs.mdx +14 -0
  222. package/docs/content/locales/zh-TW/tracking.mdx +2 -0
  223. package/docs/content/tracking.mdx +2 -0
  224. package/package.json +2 -2
  225. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +0 -15
  226. package/corpus/core/src/observability/hosted-model-experiment.ts +0 -118
  227. package/dist/observability/hosted-model-experiment.d.ts +0 -39
  228. package/dist/observability/hosted-model-experiment.d.ts.map +0 -1
  229. package/dist/observability/hosted-model-experiment.js +0 -90
  230. package/dist/observability/hosted-model-experiment.js.map +0 -1
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Demo mode gate.
3
3
  *
4
- * Demo mode replaces email addresses and numbers in every action result with
5
- * deterministic fake data — for both the UI and what the agent sees — while
6
- * preserving names, free text, labels, IDs, dates, URLs, and structure so the
7
- * app keeps working. The redaction WALK (see ./redact.ts) is expensive on
8
- * large payloads, so callers MUST gate it behind this function and only walk
9
- * when it returns true.
4
+ * Demo mode replaces every email address with `anonymous@builder.io` and
5
+ * anonymizes numbers in every action result — for both the UI and what the
6
+ * agent sees — while preserving names, free text, labels, IDs, dates, URLs,
7
+ * and structure so the app keeps working. The redaction WALK (see ./redact.ts)
8
+ * is expensive on large payloads, so callers MUST gate it behind this function
9
+ * and only walk when it returns true.
10
10
  *
11
11
  * This gate itself is intentionally cheap:
12
12
  * - An env-forced deployment (a hosted demo site) short-circuits with zero
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/demo/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAEtE,MAAM,MAAM,GAAG,KAAK,CAAC;AACrB,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0C,CAAC;AAEhE,oEAAoE;AACpE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,MAAM,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,gBAAgB,EAAE;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC;QACH,IAAI,UAAU,GAAG,GAAG,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,EAAE,mBAAmB,EAAE,GAC3B,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAC;YAC/C,UAAU,GAAG,mBAAmB,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,GAAG,CAAC;QAC5E,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;QACrE,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,MAAM;YAAE,OAAO,GAAG,CAAC,KAAK,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,OAAO,GAAG,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;QACxC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACnD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC","sourcesContent":["/**\n * Demo mode gate.\n *\n * Demo mode replaces email addresses and numbers in every action result with\n * deterministic fake data — for both the UI and what the agent sees — while\n * preserving names, free text, labels, IDs, dates, URLs, and structure so the\n * app keeps working. The redaction WALK (see ./redact.ts) is expensive on\n * large payloads, so callers MUST gate it behind this function and only walk\n * when it returns true.\n *\n * This gate itself is intentionally cheap:\n * - An env-forced deployment (a hosted demo site) short-circuits with zero\n * I/O — `DEMO_MODE=true`.\n * - The per-user runtime toggle lives in `application_state` under the\n * `demo-mode` key (`{ enabled: boolean }`), written by the settings UI and\n * the `toggle-demo-mode` agent action. It's read behind a short in-process\n * TTL cache keyed by user, so a tight agent tool-call loop doesn't hit the\n * DB on every result.\n */\nimport { readAppState } from \"../application-state/script-helpers.js\";\n\nconst TTL_MS = 3_000;\nconst cache = new Map<string, { value: boolean; at: number }>();\n\n/** Deployment-wide force (hosted demo site). Zero cost — no I/O. */\nexport function isDemoModeForced(): boolean {\n return process.env.DEMO_MODE === \"true\";\n}\n\n/**\n * Whether demo-mode redaction should run for the current request/user.\n * Cheap by design — safe to call before every action result; the expensive\n * walk only happens when this is true.\n */\nexport async function isDemoModeEnabled(): Promise<boolean> {\n if (isDemoModeForced()) return true;\n try {\n let sessionKey = \"_\";\n try {\n const { getRequestUserEmail } =\n await import(\"../server/request-context.js\");\n sessionKey = getRequestUserEmail() ?? process.env.AGENT_USER_EMAIL ?? \"_\";\n } catch {\n // request-context unavailable (CLI / non-server) — use default key\n }\n const now = Date.now();\n const hit = cache.get(sessionKey);\n if (hit && now - hit.at < TTL_MS) return hit.value;\n const state = await readAppState(\"demo-mode\");\n const enabled = state?.enabled === true;\n cache.set(sessionKey, { value: enabled, at: now });\n return enabled;\n } catch {\n // No request context / DB unavailable — fail closed (no redaction).\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/demo/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,wCAAwC,CAAC;AAEtE,MAAM,MAAM,GAAG,KAAK,CAAC;AACrB,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0C,CAAC;AAEhE,oEAAoE;AACpE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,MAAM,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,IAAI,gBAAgB,EAAE;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,CAAC;QACH,IAAI,UAAU,GAAG,GAAG,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,EAAE,mBAAmB,EAAE,GAC3B,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAC;YAC/C,UAAU,GAAG,mBAAmB,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,GAAG,CAAC;QAC5E,CAAC;QAAC,MAAM,CAAC;YACP,mEAAmE;QACrE,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,CAAC,EAAE,GAAG,MAAM;YAAE,OAAO,GAAG,CAAC,KAAK,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAC;QAC9C,MAAM,OAAO,GAAG,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;QACxC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACnD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC","sourcesContent":["/**\n * Demo mode gate.\n *\n * Demo mode replaces every email address with `anonymous@builder.io` and\n * anonymizes numbers in every action result — for both the UI and what the\n * agent sees — while preserving names, free text, labels, IDs, dates, URLs,\n * and structure so the app keeps working. The redaction WALK (see ./redact.ts)\n * is expensive on large payloads, so callers MUST gate it behind this function\n * and only walk when it returns true.\n *\n * This gate itself is intentionally cheap:\n * - An env-forced deployment (a hosted demo site) short-circuits with zero\n * I/O — `DEMO_MODE=true`.\n * - The per-user runtime toggle lives in `application_state` under the\n * `demo-mode` key (`{ enabled: boolean }`), written by the settings UI and\n * the `toggle-demo-mode` agent action. It's read behind a short in-process\n * TTL cache keyed by user, so a tight agent tool-call loop doesn't hit the\n * DB on every result.\n */\nimport { readAppState } from \"../application-state/script-helpers.js\";\n\nconst TTL_MS = 3_000;\nconst cache = new Map<string, { value: boolean; at: number }>();\n\n/** Deployment-wide force (hosted demo site). Zero cost — no I/O. */\nexport function isDemoModeForced(): boolean {\n return process.env.DEMO_MODE === \"true\";\n}\n\n/**\n * Whether demo-mode redaction should run for the current request/user.\n * Cheap by design — safe to call before every action result; the expensive\n * walk only happens when this is true.\n */\nexport async function isDemoModeEnabled(): Promise<boolean> {\n if (isDemoModeForced()) return true;\n try {\n let sessionKey = \"_\";\n try {\n const { getRequestUserEmail } =\n await import(\"../server/request-context.js\");\n sessionKey = getRequestUserEmail() ?? process.env.AGENT_USER_EMAIL ?? \"_\";\n } catch {\n // request-context unavailable (CLI / non-server) — use default key\n }\n const now = Date.now();\n const hit = cache.get(sessionKey);\n if (hit && now - hit.at < TTL_MS) return hit.value;\n const state = await readAppState(\"demo-mode\");\n const enabled = state?.enabled === true;\n cache.set(sessionKey, { value: enabled, at: now });\n return enabled;\n } catch {\n // No request context / DB unavailable — fail closed (no redaction).\n return false;\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ export declare function shouldSkipDemoResponseRedaction(url: string): boolean;
1
2
  /** Refresh demo mode after the shared DB-sync stream reports a real change. */
2
3
  export declare function refreshDemoModeFetchInterceptor(): Promise<void>;
3
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-interceptor.d.ts","sourceRoot":"","sources":["../../src/demo/fetch-interceptor.ts"],"names":[],"mappings":"AA2GA,+EAA+E;AAC/E,wBAAgB,+BAA+B,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ/D;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CA+DrD"}
1
+ {"version":3,"file":"fetch-interceptor.d.ts","sourceRoot":"","sources":["../../src/demo/fetch-interceptor.ts"],"names":[],"mappings":"AAkEA,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMpE;AA6DD,+EAA+E;AAC/E,wBAAgB,+BAA+B,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ/D;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAqErD"}
@@ -44,6 +44,26 @@ const SKIP_SUBSTRINGS = [
44
44
  // exhaust its retries ("agent connection kept failing").
45
45
  "/_agent-native/runs",
46
46
  ];
47
+ // Raw rrweb payloads are playback data, not ordinary app UI records. Skipping
48
+ // these is not just a perf optimization that avoids walking/cloning huge
49
+ // DOM/event trees on the main thread: demo number
50
+ // redaction previously ran over this exact raw replay JSON and faked any
51
+ // integer >= 1000 it found — Meta/ViewportResize widths, pointer x/y
52
+ // coordinates, and numeric values inside `_cssText` and SVG attributes. That
53
+ // was the root cause of the 2026-07 "ultra-wide replay" bugs (stages
54
+ // rendered thousands of pixels wide, frozen/teleporting cursors, giant
55
+ // icons) even though every stored recording was always geometrically sane —
56
+ // the corruption happened at *view* time, not at capture/storage time. Small
57
+ // list/summary/manifest responses stay eligible so rendered visitor identities
58
+ // are still anonymized. NEVER remove the raw payload skips or broaden them to
59
+ // metadata endpoints that the UI renders directly.
60
+ const RAW_REPLAY_PAYLOAD_RE = /\/api\/session-replay\/recordings\/[^/?#]+\/(?:chunks(?:\/[^/?#]+)?|events)(?:[/?#]|$)/;
61
+ const RAW_AGENT_REPLAY_EVENTS_RE = /\/api\/session-replay\/agent-events\.json(?:[?#]|$)/;
62
+ export function shouldSkipDemoResponseRedaction(url) {
63
+ return (SKIP_SUBSTRINGS.some((substring) => url.includes(substring)) ||
64
+ RAW_REPLAY_PAYLOAD_RE.test(url) ||
65
+ RAW_AGENT_REPLAY_EVENTS_RE.test(url));
66
+ }
47
67
  let installed = false;
48
68
  let demoEnabled = false;
49
69
  let originalFetch = null;
@@ -138,7 +158,7 @@ export function ensureDemoModeFetchInterceptor() {
138
158
  return res;
139
159
  try {
140
160
  const url = urlOf(input);
141
- if (SKIP_SUBSTRINGS.some((s) => url.includes(s)))
161
+ if (shouldSkipDemoResponseRedaction(url))
142
162
  return res;
143
163
  // Only buffered, finite JSON. SSE / streaming / chunked-forever bodies
144
164
  // never reach `redactDemoData`: streaming content-types are excluded,
@@ -156,7 +176,13 @@ export function ensureDemoModeFetchInterceptor() {
156
176
  if (res.bodyUsed)
157
177
  return res;
158
178
  const data = await withTimeout(res.clone().json(), 3_000);
159
- const redacted = redactDemoData(data);
179
+ // Frontend reads only need identity privacy. Dashboard charts apply
180
+ // their purpose-built demo trend transform at render time, so mutating
181
+ // every numeric field in every JSON response is unnecessary work.
182
+ const redacted = redactDemoData(data, {
183
+ redactNumbers: false,
184
+ redactProtectedEmails: true,
185
+ });
160
186
  const headers = new Headers(res.headers);
161
187
  // Body is re-serialized — these would be wrong now.
162
188
  headers.delete("content-length");
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-interceptor.js","sourceRoot":"","sources":["../../src/demo/fetch-interceptor.ts"],"names":[],"mappings":"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","sourcesContent":["import { agentNativePath } from \"../client/api-path.js\";\n/**\n * Client-side demo-mode redaction.\n *\n * Why client-side and not (only) at the server action boundary: templates\n * serve a lot of their UI data through their OWN custom Nitro `/api/*`\n * handlers (e.g. mail's `/api/emails`, `/api/threads/:id/messages`,\n * `/api/contacts`, `/api/apollo/person`), which never pass through the\n * framework action runtime. On this stack (Nitro v3 / h3 v2) there is no\n * single server hook that can safely rewrite every JSON body (the `response`\n * hook hands you an immutable Web `Response` and returns `void`). Patching the\n * browser's `fetch` is the one place that is BOTH universal (every template's\n * reads — actions and custom routes alike — go through it) AND low-risk: it\n * can only ever post-process JSON the app already parses for display, so it\n * physically cannot break auth, SSE streams, SSR HTML, or binary downloads.\n *\n * The agent is handled separately and in-process (its action tool results are\n * redacted in `production-agent.ts`), so it doesn't depend on this at all.\n *\n * Scope intentionally narrow:\n * - Only same-document `GET` requests are redacted. Mutation responses\n * (POST/PUT/PATCH/DELETE) pass through untouched so a draft you just\n * typed isn't echoed back as fake data mid-demo.\n * - Only `application/json` 2xx bodies. Streams (`text/event-stream`),\n * HTML, and binary are skipped by content-type.\n * - Framework infra endpoints (poll, events, the demo-status endpoint\n * itself) are skipped — no PII and avoids self-recursion.\n * - Any error during interception falls back to the original response.\n */\nimport { redactDemoData } from \"./redact.js\";\n\nconst STATUS_PATH = agentNativePath(\"/_agent-native/demo/status\");\nconst SKIP_SUBSTRINGS = [\n \"/_agent-native/demo/status\",\n \"/_agent-native/poll\",\n \"/_agent-native/events\",\n // Never touch agent transport. The agent already gets in-process\n // redaction of its tool results; faking its own transcript adds no demo\n // value and must stay clear of the tool_use/tool_result protocol. Covers\n // \"/_agent-native/agent\" (stream) and \"/_agent-native/agent-chat\"\n // (thread history) and any sub-paths.\n \"/_agent-native/agent\",\n // Run-manager state read by the reconnect/recovery loop. Faking numeric\n // run state here would make recovery think it's not progressing and\n // exhaust its retries (\"agent connection kept failing\").\n \"/_agent-native/runs\",\n];\n\nlet installed = false;\nlet demoEnabled = false;\nlet originalFetch: typeof fetch | null = null;\nlet refreshPromise: Promise<void> | null = null;\n\n// Set once the first demo-status check completes. We DO NOT block requests on\n// it — if status isn't known yet a response is simply passed through\n// un-redacted (a brief first-paint window) rather than delaying transport.\n// Injecting latency into early GETs previously risked the agent's streaming\n// reconnect logic; transport safety wins over redacting the first paint.\nlet firstStatusDone = false;\n\n/** Reject after `ms` so a misclassified streaming body can never hang. */\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return Promise.race([\n p,\n new Promise<T>((_, reject) =>\n setTimeout(() => reject(new Error(\"redact-timeout\")), ms),\n ),\n ]);\n}\n\nfunction urlOf(input: RequestInfo | URL): string {\n try {\n if (typeof input === \"string\") return input;\n if (input instanceof URL) return input.href;\n return (input as Request).url ?? \"\";\n } catch {\n return \"\";\n }\n}\n\nfunction methodOf(input: RequestInfo | URL, init?: RequestInit): string {\n const m =\n init?.method ??\n (typeof input !== \"string\" && !(input instanceof URL)\n ? (input as Request).method\n : undefined) ??\n \"GET\";\n return m.toUpperCase();\n}\n\nasync function refreshDemoFlag(): Promise<void> {\n const f = originalFetch ?? fetch;\n try {\n const res = await f(STATUS_PATH, { credentials: \"same-origin\" });\n if (!res.ok) return;\n const json = (await res.json()) as {\n enabled?: boolean;\n forced?: boolean;\n } | null;\n demoEnabled = json?.enabled === true || json?.forced === true;\n } catch {\n // Status endpoint unreachable — leave the last known value.\n } finally {\n firstStatusDone = true;\n }\n}\n\n/** Refresh demo mode after the shared DB-sync stream reports a real change. */\nexport function refreshDemoModeFetchInterceptor(): Promise<void> {\n if (!installed) return Promise.resolve();\n if (!refreshPromise) {\n refreshPromise = refreshDemoFlag().finally(() => {\n refreshPromise = null;\n });\n }\n return refreshPromise;\n}\n\n/**\n * Install the demo-mode fetch interceptor and read demo status once. Later\n * changes are driven by the shared DB-sync transport instead of a separate\n * fixed polling loop.\n * Idempotent and browser-only — safe to call from any hook that runs in\n * every template root (we call it from `useDbSync`). A no-op until demo\n * mode is actually on.\n */\nexport function ensureDemoModeFetchInterceptor(): void {\n if (typeof window === \"undefined\") return;\n if (installed) return;\n installed = true;\n\n originalFetch = window.fetch.bind(window);\n const base = originalFetch;\n\n window.fetch = async function patchedFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> {\n const res = await base(input, init);\n\n // Fast path: anything that isn't a demo-enabled, plain GET returns the\n // ORIGINAL response with zero body work and zero extra awaits — when\n // demo mode is off this wrapper is byte-for-byte native fetch, so it\n // cannot influence agent/run/stream transport.\n if (!demoEnabled || !firstStatusDone) return res;\n if (methodOf(input, init) !== \"GET\") return res;\n if (!res.ok) return res;\n\n try {\n const url = urlOf(input);\n if (SKIP_SUBSTRINGS.some((s) => url.includes(s))) return res;\n\n // Only buffered, finite JSON. SSE / streaming / chunked-forever bodies\n // never reach `redactDemoData`: streaming content-types are excluded,\n // and the JSON read is hard-timeout-bounded so a misclassified stream\n // degrades to \"return the original response\" instead of hanging the\n // request (which is what tripped the reconnect/recovery loop).\n const contentType = res.headers.get(\"content-type\") ?? \"\";\n if (!contentType.includes(\"application/json\")) return res;\n if (\n contentType.includes(\"event-stream\") ||\n contentType.includes(\"ndjson\") ||\n contentType.includes(\"stream\")\n ) {\n return res;\n }\n if (res.bodyUsed) return res;\n\n const data = await withTimeout(res.clone().json(), 3_000);\n const redacted = redactDemoData(data);\n\n const headers = new Headers(res.headers);\n // Body is re-serialized — these would be wrong now.\n headers.delete(\"content-length\");\n headers.delete(\"content-encoding\");\n\n return new Response(JSON.stringify(redacted), {\n status: res.status,\n statusText: res.statusText,\n headers,\n });\n } catch {\n // Never let redaction break a request — fall back to the real\n // response (its body stream is untouched; we only ever read a clone).\n return res;\n }\n };\n\n void refreshDemoModeFetchInterceptor();\n}\n"]}
1
+ {"version":3,"file":"fetch-interceptor.js","sourceRoot":"","sources":["../../src/demo/fetch-interceptor.ts"],"names":[],"mappings":"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","sourcesContent":["import { agentNativePath } from \"../client/api-path.js\";\n/**\n * Client-side demo-mode redaction.\n *\n * Why client-side and not (only) at the server action boundary: templates\n * serve a lot of their UI data through their OWN custom Nitro `/api/*`\n * handlers (e.g. mail's `/api/emails`, `/api/threads/:id/messages`,\n * `/api/contacts`, `/api/apollo/person`), which never pass through the\n * framework action runtime. On this stack (Nitro v3 / h3 v2) there is no\n * single server hook that can safely rewrite every JSON body (the `response`\n * hook hands you an immutable Web `Response` and returns `void`). Patching the\n * browser's `fetch` is the one place that is BOTH universal (every template's\n * reads — actions and custom routes alike — go through it) AND low-risk: it\n * can only ever post-process JSON the app already parses for display, so it\n * physically cannot break auth, SSE streams, SSR HTML, or binary downloads.\n *\n * The agent is handled separately and in-process (its action tool results are\n * redacted in `production-agent.ts`), so it doesn't depend on this at all.\n *\n * Scope intentionally narrow:\n * - Only same-document `GET` requests are redacted. Mutation responses\n * (POST/PUT/PATCH/DELETE) pass through untouched so a draft you just\n * typed isn't echoed back as fake data mid-demo.\n * - Only `application/json` 2xx bodies. Streams (`text/event-stream`),\n * HTML, and binary are skipped by content-type.\n * - Framework infra endpoints (poll, events, the demo-status endpoint\n * itself) are skipped — no PII and avoids self-recursion.\n * - Any error during interception falls back to the original response.\n */\nimport { redactDemoData } from \"./redact.js\";\n\nconst STATUS_PATH = agentNativePath(\"/_agent-native/demo/status\");\nconst SKIP_SUBSTRINGS = [\n \"/_agent-native/demo/status\",\n \"/_agent-native/poll\",\n \"/_agent-native/events\",\n // Never touch agent transport. The agent already gets in-process\n // redaction of its tool results; faking its own transcript adds no demo\n // value and must stay clear of the tool_use/tool_result protocol. Covers\n // \"/_agent-native/agent\" (stream) and \"/_agent-native/agent-chat\"\n // (thread history) and any sub-paths.\n \"/_agent-native/agent\",\n // Run-manager state read by the reconnect/recovery loop. Faking numeric\n // run state here would make recovery think it's not progressing and\n // exhaust its retries (\"agent connection kept failing\").\n \"/_agent-native/runs\",\n];\n\n// Raw rrweb payloads are playback data, not ordinary app UI records. Skipping\n// these is not just a perf optimization that avoids walking/cloning huge\n// DOM/event trees on the main thread: demo number\n// redaction previously ran over this exact raw replay JSON and faked any\n// integer >= 1000 it found — Meta/ViewportResize widths, pointer x/y\n// coordinates, and numeric values inside `_cssText` and SVG attributes. That\n// was the root cause of the 2026-07 \"ultra-wide replay\" bugs (stages\n// rendered thousands of pixels wide, frozen/teleporting cursors, giant\n// icons) even though every stored recording was always geometrically sane —\n// the corruption happened at *view* time, not at capture/storage time. Small\n// list/summary/manifest responses stay eligible so rendered visitor identities\n// are still anonymized. NEVER remove the raw payload skips or broaden them to\n// metadata endpoints that the UI renders directly.\nconst RAW_REPLAY_PAYLOAD_RE =\n /\\/api\\/session-replay\\/recordings\\/[^/?#]+\\/(?:chunks(?:\\/[^/?#]+)?|events)(?:[/?#]|$)/;\nconst RAW_AGENT_REPLAY_EVENTS_RE =\n /\\/api\\/session-replay\\/agent-events\\.json(?:[?#]|$)/;\n\nexport function shouldSkipDemoResponseRedaction(url: string): boolean {\n return (\n SKIP_SUBSTRINGS.some((substring) => url.includes(substring)) ||\n RAW_REPLAY_PAYLOAD_RE.test(url) ||\n RAW_AGENT_REPLAY_EVENTS_RE.test(url)\n );\n}\n\nlet installed = false;\nlet demoEnabled = false;\nlet originalFetch: typeof fetch | null = null;\nlet refreshPromise: Promise<void> | null = null;\n\n// Set once the first demo-status check completes. We DO NOT block requests on\n// it — if status isn't known yet a response is simply passed through\n// un-redacted (a brief first-paint window) rather than delaying transport.\n// Injecting latency into early GETs previously risked the agent's streaming\n// reconnect logic; transport safety wins over redacting the first paint.\nlet firstStatusDone = false;\n\n/** Reject after `ms` so a misclassified streaming body can never hang. */\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return Promise.race([\n p,\n new Promise<T>((_, reject) =>\n setTimeout(() => reject(new Error(\"redact-timeout\")), ms),\n ),\n ]);\n}\n\nfunction urlOf(input: RequestInfo | URL): string {\n try {\n if (typeof input === \"string\") return input;\n if (input instanceof URL) return input.href;\n return (input as Request).url ?? \"\";\n } catch {\n return \"\";\n }\n}\n\nfunction methodOf(input: RequestInfo | URL, init?: RequestInit): string {\n const m =\n init?.method ??\n (typeof input !== \"string\" && !(input instanceof URL)\n ? (input as Request).method\n : undefined) ??\n \"GET\";\n return m.toUpperCase();\n}\n\nasync function refreshDemoFlag(): Promise<void> {\n const f = originalFetch ?? fetch;\n try {\n const res = await f(STATUS_PATH, { credentials: \"same-origin\" });\n if (!res.ok) return;\n const json = (await res.json()) as {\n enabled?: boolean;\n forced?: boolean;\n } | null;\n demoEnabled = json?.enabled === true || json?.forced === true;\n } catch {\n // Status endpoint unreachable — leave the last known value.\n } finally {\n firstStatusDone = true;\n }\n}\n\n/** Refresh demo mode after the shared DB-sync stream reports a real change. */\nexport function refreshDemoModeFetchInterceptor(): Promise<void> {\n if (!installed) return Promise.resolve();\n if (!refreshPromise) {\n refreshPromise = refreshDemoFlag().finally(() => {\n refreshPromise = null;\n });\n }\n return refreshPromise;\n}\n\n/**\n * Install the demo-mode fetch interceptor and read demo status once. Later\n * changes are driven by the shared DB-sync transport instead of a separate\n * fixed polling loop.\n * Idempotent and browser-only — safe to call from any hook that runs in\n * every template root (we call it from `useDbSync`). A no-op until demo\n * mode is actually on.\n */\nexport function ensureDemoModeFetchInterceptor(): void {\n if (typeof window === \"undefined\") return;\n if (installed) return;\n installed = true;\n\n originalFetch = window.fetch.bind(window);\n const base = originalFetch;\n\n window.fetch = async function patchedFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> {\n const res = await base(input, init);\n\n // Fast path: anything that isn't a demo-enabled, plain GET returns the\n // ORIGINAL response with zero body work and zero extra awaits — when\n // demo mode is off this wrapper is byte-for-byte native fetch, so it\n // cannot influence agent/run/stream transport.\n if (!demoEnabled || !firstStatusDone) return res;\n if (methodOf(input, init) !== \"GET\") return res;\n if (!res.ok) return res;\n\n try {\n const url = urlOf(input);\n if (shouldSkipDemoResponseRedaction(url)) return res;\n\n // Only buffered, finite JSON. SSE / streaming / chunked-forever bodies\n // never reach `redactDemoData`: streaming content-types are excluded,\n // and the JSON read is hard-timeout-bounded so a misclassified stream\n // degrades to \"return the original response\" instead of hanging the\n // request (which is what tripped the reconnect/recovery loop).\n const contentType = res.headers.get(\"content-type\") ?? \"\";\n if (!contentType.includes(\"application/json\")) return res;\n if (\n contentType.includes(\"event-stream\") ||\n contentType.includes(\"ndjson\") ||\n contentType.includes(\"stream\")\n ) {\n return res;\n }\n if (res.bodyUsed) return res;\n\n const data = await withTimeout(res.clone().json(), 3_000);\n // Frontend reads only need identity privacy. Dashboard charts apply\n // their purpose-built demo trend transform at render time, so mutating\n // every numeric field in every JSON response is unnecessary work.\n const redacted = redactDemoData(data, {\n redactNumbers: false,\n redactProtectedEmails: true,\n });\n\n const headers = new Headers(res.headers);\n // Body is re-serialized — these would be wrong now.\n headers.delete(\"content-length\");\n headers.delete(\"content-encoding\");\n\n return new Response(JSON.stringify(redacted), {\n status: res.status,\n statusText: res.statusText,\n headers,\n });\n } catch {\n // Never let redaction break a request — fall back to the real\n // response (its body stream is untouched; we only ever read a clone).\n return res;\n }\n };\n\n void refreshDemoModeFetchInterceptor();\n}\n"]}
@@ -1,19 +1,26 @@
1
1
  /**
2
2
  * Pure, dependency-free, deterministic demo-mode redactor.
3
3
  *
4
- * Replaces sensitive values (emails and free numbers) with stable fake
5
- * substitutes so a demo looks coherent the same input always maps to the
6
- * same fake. Crucially, it NEVER rewrites identifiers, structural tokens, or
7
- * timestamps. Names and other free text are deliberately left alone because
8
- * guessing whether arbitrary text is a person's name is too inaccurate. The
9
- * string redactor uses a protect-first strategy (mask IDs with opaque
10
- * placeholders before any transform runs, restore them byte-identical
4
+ * Replaces every email with one canonical anonymous address and free numbers
5
+ * with stable fake substitutes. Crucially, it NEVER rewrites identifiers,
6
+ * structural tokens, or timestamps. Names and other free text are deliberately
7
+ * left alone because guessing whether arbitrary text is a person's name is too
8
+ * inaccurate. The string redactor uses a protect-first strategy (mask IDs with
9
+ * opaque placeholders before any transform runs, restore them byte-identical
11
10
  * afterwards), and the structure-aware walker additionally protects leaf
12
11
  * values by key name.
13
12
  */
14
13
  export interface RedactOptions {
15
14
  salt?: string;
15
+ /** Redact numeric values. Defaults to true for backward compatibility. */
16
+ redactNumbers?: boolean;
17
+ /**
18
+ * Redact emails stored in otherwise protected structural fields such as
19
+ * `userId` and `userKey`. Intended for display-only frontend responses.
20
+ */
21
+ redactProtectedEmails?: boolean;
16
22
  }
23
+ export declare const DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
17
24
  export declare function redactDemoString(text: string, opts?: RedactOptions): string;
18
25
  export declare function redactDemoData<T>(value: T, opts?: RedactOptions): T;
19
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/demo/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAghBD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAE3E;AAsGD,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,CAAC,CAGnE;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAGrD"}
1
+ {"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/demo/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAwID,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAwQ3D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAM3E;AA8HD,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,CAAC,CAUnE;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAGrD"}
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * Pure, dependency-free, deterministic demo-mode redactor.
3
3
  *
4
- * Replaces sensitive values (emails and free numbers) with stable fake
5
- * substitutes so a demo looks coherent the same input always maps to the
6
- * same fake. Crucially, it NEVER rewrites identifiers, structural tokens, or
7
- * timestamps. Names and other free text are deliberately left alone because
8
- * guessing whether arbitrary text is a person's name is too inaccurate. The
9
- * string redactor uses a protect-first strategy (mask IDs with opaque
10
- * placeholders before any transform runs, restore them byte-identical
4
+ * Replaces every email with one canonical anonymous address and free numbers
5
+ * with stable fake substitutes. Crucially, it NEVER rewrites identifiers,
6
+ * structural tokens, or timestamps. Names and other free text are deliberately
7
+ * left alone because guessing whether arbitrary text is a person's name is too
8
+ * inaccurate. The string redactor uses a protect-first strategy (mask IDs with
9
+ * opaque placeholders before any transform runs, restore them byte-identical
11
10
  * afterwards), and the structure-aware walker additionally protects leaf
12
11
  * values by key name.
13
12
  */
@@ -42,9 +41,6 @@ function seededRng(value, salt) {
42
41
  const seedFn = xmur3(`${value}${salt}`);
43
42
  return mulberry32(seedFn());
44
43
  }
45
- function pick(rng, pool) {
46
- return pool[Math.floor(rng() * pool.length) % pool.length];
47
- }
48
44
  /* ------------------------------------------------------------------ *
49
45
  * Stable mapping cache (bounded, TTL, leak-free)
50
46
  *
@@ -135,145 +131,12 @@ idempotent = true) {
135
131
  rememberProduced(value);
136
132
  return value;
137
133
  }
138
- /* ------------------------------------------------------------------ *
139
- * Curated name pools
140
- * ------------------------------------------------------------------ */
141
- const FIRST_NAMES = [
142
- "Jane",
143
- "John",
144
- "Alex",
145
- "Maria",
146
- "Sam",
147
- "Olivia",
148
- "Liam",
149
- "Emma",
150
- "Noah",
151
- "Ava",
152
- "Lucas",
153
- "Mia",
154
- "Ethan",
155
- "Sofia",
156
- "Mason",
157
- "Isla",
158
- "Leo",
159
- "Aria",
160
- "Henry",
161
- "Zoe",
162
- "Owen",
163
- "Nora",
164
- "Caleb",
165
- "Lily",
166
- "Ryan",
167
- "Ruby",
168
- "Adam",
169
- "Chloe",
170
- "Eli",
171
- "Hazel",
172
- "Max",
173
- "Iris",
174
- "Ben",
175
- "Clara",
176
- "Theo",
177
- "Maya",
178
- "Felix",
179
- "Elena",
180
- "Jonah",
181
- "Greta",
182
- ];
183
- const LAST_NAMES = [
184
- "Doe",
185
- "Smith",
186
- "Johnson",
187
- "Lee",
188
- "Brown",
189
- "Garcia",
190
- "Miller",
191
- "Davis",
192
- "Wilson",
193
- "Moore",
194
- "Taylor",
195
- "Anderson",
196
- "Thomas",
197
- "Jackson",
198
- "White",
199
- "Harris",
200
- "Martin",
201
- "Clark",
202
- "Lewis",
203
- "Walker",
204
- "Hall",
205
- "Young",
206
- "King",
207
- "Wright",
208
- "Hill",
209
- "Green",
210
- "Adams",
211
- "Baker",
212
- "Nelson",
213
- "Carter",
214
- "Mitchell",
215
- "Perez",
216
- "Roberts",
217
- "Turner",
218
- "Phillips",
219
- "Campbell",
220
- "Parker",
221
- "Evans",
222
- "Edwards",
223
- "Collins",
224
- ];
225
134
  /* ------------------------------------------------------------------ *
226
135
  * Fake value generators (deterministic in value + salt)
227
136
  * ------------------------------------------------------------------ */
228
- // Realistic-looking but safe stand-in domains: real consumer providers (the
229
- // local part is fake, so no real mailbox is implied) plus the canonical
230
- // reserved fictional-company domains (Contoso/Fabrikam/Northwind), which read
231
- // as real businesses but are intentional placeholders. No example.com.
232
- const FAKE_EMAIL_DOMAINS = [
233
- "gmail.com",
234
- "outlook.com",
235
- "icloud.com",
236
- "yahoo.com",
237
- "proton.me",
238
- "hey.com",
239
- "fastmail.com",
240
- "contoso.com",
241
- "fabrikam.com",
242
- "northwind.com",
243
- "acme.io",
244
- "globex.net",
245
- ];
137
+ export const DEMO_ANONYMOUS_EMAIL = "anonymous@builder.io";
246
138
  function fakeEmail(original, salt) {
247
- return memoFake("email", original.toLowerCase(), salt, () => {
248
- const rng = seededRng(`email:${original.toLowerCase()}`, salt);
249
- const first = pick(rng, FIRST_NAMES).toLowerCase();
250
- const last = pick(rng, LAST_NAMES).toLowerCase();
251
- const domain = pick(rng, FAKE_EMAIL_DOMAINS);
252
- // Vary the local part so addresses don't all look templated.
253
- const shape = Math.floor(rng() * 6);
254
- let local;
255
- switch (shape) {
256
- case 0:
257
- local = `${first}.${last}`;
258
- break;
259
- case 1:
260
- local = `${first}${last}`;
261
- break;
262
- case 2:
263
- local = `${first[0]}${last}`;
264
- break;
265
- case 3:
266
- local = `${first}.${last}${1 + Math.floor(rng() * 89)}`;
267
- break;
268
- case 4:
269
- local = `${first}_${last}`;
270
- break;
271
- default:
272
- local = `${last}.${first}`;
273
- break;
274
- }
275
- return `${local}@${domain}`;
276
- });
139
+ return memoFake("email", original.toLowerCase(), salt, () => DEMO_ANONYMOUS_EMAIL);
277
140
  }
278
141
  /**
279
142
  * Replace every digit in `numericBody` with a freshly-generated digit while
@@ -463,17 +326,21 @@ function transformNumbers(text, salt) {
463
326
  /* ------------------------------------------------------------------ *
464
327
  * Public: string redactor
465
328
  * ------------------------------------------------------------------ */
466
- function redactDemoStringInternal(text, salt) {
329
+ function redactDemoStringInternal(text, salt, redactNumbers) {
467
330
  if (typeof text !== "string" || text.length === 0)
468
331
  return text;
332
+ if (!text.includes("@") && (!redactNumbers || !/[\d$€£¥]/.test(text))) {
333
+ return text;
334
+ }
469
335
  const { text: masked, restore } = protect(text);
470
336
  let out = masked;
471
337
  out = transformEmails(out, salt);
472
- out = transformNumbers(out, salt);
338
+ if (redactNumbers)
339
+ out = transformNumbers(out, salt);
473
340
  return unprotect(out, restore);
474
341
  }
475
342
  export function redactDemoString(text, opts) {
476
- return redactDemoStringInternal(text, opts?.salt ?? "");
343
+ return redactDemoStringInternal(text, opts?.salt ?? "", opts?.redactNumbers !== false);
477
344
  }
478
345
  /* ------------------------------------------------------------------ *
479
346
  * Public: structure-aware redactor
@@ -510,17 +377,17 @@ function redactNumberLeaf(value, salt) {
510
377
  return value;
511
378
  return n;
512
379
  }
513
- function walk(value, salt, depth, seen) {
380
+ function walk(value, salt, depth, seen, redactNumbers, redactProtectedEmails) {
514
381
  if (depth > MAX_DEPTH)
515
382
  return value;
516
383
  if (value === null || value === undefined)
517
384
  return value;
518
385
  const t = typeof value;
519
386
  if (t === "string") {
520
- return redactDemoStringInternal(value, salt);
387
+ return redactDemoStringInternal(value, salt, redactNumbers);
521
388
  }
522
389
  if (t === "number") {
523
- return redactNumberLeaf(value, salt);
390
+ return redactNumbers ? redactNumberLeaf(value, salt) : value;
524
391
  }
525
392
  if (t === "boolean" || t === "bigint" || t === "function" || t === "symbol") {
526
393
  return value;
@@ -531,7 +398,7 @@ function walk(value, salt, depth, seen) {
531
398
  if (seen.has(value))
532
399
  return value;
533
400
  seen.add(value);
534
- const out = value.map((item) => walk(item, salt, depth + 1, seen));
401
+ const out = value.map((item) => walk(item, salt, depth + 1, seen, redactNumbers, redactProtectedEmails));
535
402
  seen.delete(value);
536
403
  return out;
537
404
  }
@@ -548,7 +415,12 @@ function walk(value, salt, depth, seen) {
548
415
  !(entry instanceof Date)) {
549
416
  // Still recurse into nested structures, but the protected key does
550
417
  // not transform its own leaf value.
551
- out[key] = walk(entry, salt, depth + 1, seen);
418
+ out[key] = walk(entry, salt, depth + 1, seen, redactNumbers, redactProtectedEmails);
419
+ }
420
+ else if (redactProtectedEmails &&
421
+ typeof entry === "string" &&
422
+ entry.includes("@")) {
423
+ out[key] = redactDemoStringInternal(entry, salt, false);
552
424
  }
553
425
  else {
554
426
  // Leaf under a protected key: pass through completely untouched.
@@ -556,7 +428,7 @@ function walk(value, salt, depth, seen) {
556
428
  }
557
429
  continue;
558
430
  }
559
- out[key] = walk(entry, salt, depth + 1, seen);
431
+ out[key] = walk(entry, salt, depth + 1, seen, redactNumbers, redactProtectedEmails);
560
432
  }
561
433
  seen.delete(value);
562
434
  return out;
@@ -566,7 +438,7 @@ function walk(value, salt, depth, seen) {
566
438
  }
567
439
  export function redactDemoData(value, opts) {
568
440
  const salt = opts?.salt ?? "";
569
- return walk(value, salt, 0, new WeakSet());
441
+ return walk(value, salt, 0, new WeakSet(), opts?.redactNumbers !== false, opts?.redactProtectedEmails === true);
570
442
  }
571
443
  /**
572
444
  * Clear the stable-mapping caches. Test-only — the caches are process-global