@agent-native/core 0.9.1 → 0.10.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 (209) hide show
  1. package/README.md +4 -4
  2. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  3. package/dist/agent/engine/builder-engine.js +5 -4
  4. package/dist/agent/engine/builder-engine.js.map +1 -1
  5. package/dist/agent/engine/registry.d.ts +6 -3
  6. package/dist/agent/engine/registry.d.ts.map +1 -1
  7. package/dist/agent/engine/registry.js +8 -17
  8. package/dist/agent/engine/registry.js.map +1 -1
  9. package/dist/agent/production-agent.d.ts +1 -1
  10. package/dist/agent/production-agent.d.ts.map +1 -1
  11. package/dist/agent/production-agent.js +28 -11
  12. package/dist/agent/production-agent.js.map +1 -1
  13. package/dist/agent/run-manager.d.ts.map +1 -1
  14. package/dist/agent/run-manager.js +12 -3
  15. package/dist/agent/run-manager.js.map +1 -1
  16. package/dist/agent/thread-data-builder.d.ts +12 -0
  17. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  18. package/dist/agent/thread-data-builder.js +96 -0
  19. package/dist/agent/thread-data-builder.js.map +1 -1
  20. package/dist/cli/create.d.ts.map +1 -1
  21. package/dist/cli/create.js +16 -10
  22. package/dist/cli/create.js.map +1 -1
  23. package/dist/client/AgentPanel.d.ts.map +1 -1
  24. package/dist/client/AgentPanel.js +6 -20
  25. package/dist/client/AgentPanel.js.map +1 -1
  26. package/dist/client/AssistantChat.d.ts.map +1 -1
  27. package/dist/client/AssistantChat.js +113 -28
  28. package/dist/client/AssistantChat.js.map +1 -1
  29. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  30. package/dist/client/agent-chat-adapter.js +21 -7
  31. package/dist/client/agent-chat-adapter.js.map +1 -1
  32. package/dist/client/agent-sidebar-state.d.ts +3 -0
  33. package/dist/client/agent-sidebar-state.d.ts.map +1 -0
  34. package/dist/client/agent-sidebar-state.js +24 -0
  35. package/dist/client/agent-sidebar-state.js.map +1 -0
  36. package/dist/client/analytics.d.ts +25 -0
  37. package/dist/client/analytics.d.ts.map +1 -1
  38. package/dist/client/analytics.js +40 -0
  39. package/dist/client/analytics.js.map +1 -1
  40. package/dist/client/components/ui/tooltip.d.ts +2 -1
  41. package/dist/client/components/ui/tooltip.d.ts.map +1 -1
  42. package/dist/client/components/ui/tooltip.js +9 -2
  43. package/dist/client/components/ui/tooltip.js.map +1 -1
  44. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  45. package/dist/client/composer/ComposerPlusMenu.js +41 -8
  46. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  47. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  48. package/dist/client/composer/PromptComposer.js +30 -0
  49. package/dist/client/composer/PromptComposer.js.map +1 -1
  50. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  51. package/dist/client/composer/TiptapComposer.js +26 -1
  52. package/dist/client/composer/TiptapComposer.js.map +1 -1
  53. package/dist/client/dev-overlay/DevOverlay.d.ts.map +1 -1
  54. package/dist/client/dev-overlay/DevOverlay.js +4 -4
  55. package/dist/client/dev-overlay/DevOverlay.js.map +1 -1
  56. package/dist/client/error-format.d.ts.map +1 -1
  57. package/dist/client/error-format.js +6 -0
  58. package/dist/client/error-format.js.map +1 -1
  59. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  60. package/dist/client/extensions/EmbeddedExtension.js +14 -4
  61. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  62. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  63. package/dist/client/extensions/ExtensionEditor.js +6 -6
  64. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  65. package/dist/client/extensions/ExtensionSlot.d.ts.map +1 -1
  66. package/dist/client/extensions/ExtensionSlot.js +2 -2
  67. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  68. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  69. package/dist/client/extensions/ExtensionViewer.js +39 -19
  70. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  71. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  72. package/dist/client/extensions/ExtensionsSidebarSection.js +52 -52
  73. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  74. package/dist/client/index.d.ts +1 -1
  75. package/dist/client/index.d.ts.map +1 -1
  76. package/dist/client/index.js +1 -1
  77. package/dist/client/index.js.map +1 -1
  78. package/dist/client/notifications/NotificationsBell.d.ts.map +1 -1
  79. package/dist/client/notifications/NotificationsBell.js +42 -6
  80. package/dist/client/notifications/NotificationsBell.js.map +1 -1
  81. package/dist/client/org/InvitationBanner.d.ts.map +1 -1
  82. package/dist/client/org/InvitationBanner.js +5 -5
  83. package/dist/client/org/InvitationBanner.js.map +1 -1
  84. package/dist/client/org/TeamPage.d.ts.map +1 -1
  85. package/dist/client/org/TeamPage.js +3 -2
  86. package/dist/client/org/TeamPage.js.map +1 -1
  87. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  88. package/dist/client/resources/ResourcesPanel.js +41 -7
  89. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  90. package/dist/client/resources/use-mcp-servers.d.ts +2 -0
  91. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  92. package/dist/client/resources/use-mcp-servers.js +59 -3
  93. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  94. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  95. package/dist/client/settings/SecretsSection.js +9 -0
  96. package/dist/client/settings/SecretsSection.js.map +1 -1
  97. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  98. package/dist/client/settings/SettingsPanel.js +12 -10
  99. package/dist/client/settings/SettingsPanel.js.map +1 -1
  100. package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -1
  101. package/dist/client/settings/VoiceTranscriptionSection.js +13 -30
  102. package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
  103. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  104. package/dist/client/settings/useBuilderStatus.js +27 -1
  105. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  106. package/dist/client/sharing/ShareButton.d.ts +4 -0
  107. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  108. package/dist/client/sharing/ShareButton.js +5 -1
  109. package/dist/client/sharing/ShareButton.js.map +1 -1
  110. package/dist/client/sse-event-processor.d.ts +1 -1
  111. package/dist/client/sse-event-processor.d.ts.map +1 -1
  112. package/dist/client/sse-event-processor.js +14 -7
  113. package/dist/client/sse-event-processor.js.map +1 -1
  114. package/dist/client/use-db-sync.d.ts.map +1 -1
  115. package/dist/client/use-db-sync.js +100 -19
  116. package/dist/client/use-db-sync.js.map +1 -1
  117. package/dist/deploy/build.d.ts.map +1 -1
  118. package/dist/deploy/build.js +5 -0
  119. package/dist/deploy/build.js.map +1 -1
  120. package/dist/deploy/route-discovery.d.ts.map +1 -1
  121. package/dist/deploy/route-discovery.js +1 -0
  122. package/dist/deploy/route-discovery.js.map +1 -1
  123. package/dist/deploy/workspace-core.d.ts +1 -1
  124. package/dist/deploy/workspace-core.d.ts.map +1 -1
  125. package/dist/deploy/workspace-core.js +1 -0
  126. package/dist/deploy/workspace-core.js.map +1 -1
  127. package/dist/extensions/actions.d.ts.map +1 -1
  128. package/dist/extensions/actions.js +17 -3
  129. package/dist/extensions/actions.js.map +1 -1
  130. package/dist/extensions/routes.js +1 -1
  131. package/dist/extensions/routes.js.map +1 -1
  132. package/dist/extensions/schema.d.ts +14 -14
  133. package/dist/extensions/schema.d.ts.map +1 -1
  134. package/dist/extensions/schema.js +4 -4
  135. package/dist/extensions/schema.js.map +1 -1
  136. package/dist/extensions/store.d.ts.map +1 -1
  137. package/dist/extensions/store.js +23 -0
  138. package/dist/extensions/store.js.map +1 -1
  139. package/dist/extensions/theme.d.ts +8 -1
  140. package/dist/extensions/theme.d.ts.map +1 -1
  141. package/dist/extensions/theme.js +43 -34
  142. package/dist/extensions/theme.js.map +1 -1
  143. package/dist/mcp-client/routes.d.ts +1 -0
  144. package/dist/mcp-client/routes.d.ts.map +1 -1
  145. package/dist/mcp-client/routes.js +28 -1
  146. package/dist/mcp-client/routes.js.map +1 -1
  147. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  148. package/dist/server/agent-chat-plugin.js +77 -102
  149. package/dist/server/agent-chat-plugin.js.map +1 -1
  150. package/dist/server/auth.d.ts.map +1 -1
  151. package/dist/server/auth.js +33 -0
  152. package/dist/server/auth.js.map +1 -1
  153. package/dist/server/builder-browser.d.ts.map +1 -1
  154. package/dist/server/builder-browser.js +169 -68
  155. package/dist/server/builder-browser.js.map +1 -1
  156. package/dist/server/credential-provider.d.ts +2 -2
  157. package/dist/server/credential-provider.d.ts.map +1 -1
  158. package/dist/server/credential-provider.js +31 -12
  159. package/dist/server/credential-provider.js.map +1 -1
  160. package/dist/server/framework-request-handler.d.ts.map +1 -1
  161. package/dist/server/framework-request-handler.js +31 -0
  162. package/dist/server/framework-request-handler.js.map +1 -1
  163. package/dist/server/google-realtime-session.d.ts.map +1 -1
  164. package/dist/server/google-realtime-session.js +19 -6
  165. package/dist/server/google-realtime-session.js.map +1 -1
  166. package/dist/server/index.d.ts +2 -0
  167. package/dist/server/index.d.ts.map +1 -1
  168. package/dist/server/index.js +2 -0
  169. package/dist/server/index.js.map +1 -1
  170. package/dist/server/onboarding-html.d.ts.map +1 -1
  171. package/dist/server/onboarding-html.js +45 -6
  172. package/dist/server/onboarding-html.js.map +1 -1
  173. package/dist/server/request-context.d.ts +17 -0
  174. package/dist/server/request-context.d.ts.map +1 -1
  175. package/dist/server/request-context.js +40 -1
  176. package/dist/server/request-context.js.map +1 -1
  177. package/dist/server/sentry-plugin.d.ts +11 -0
  178. package/dist/server/sentry-plugin.d.ts.map +1 -0
  179. package/dist/server/sentry-plugin.js +116 -0
  180. package/dist/server/sentry-plugin.js.map +1 -0
  181. package/dist/server/sentry.d.ts +92 -0
  182. package/dist/server/sentry.d.ts.map +1 -0
  183. package/dist/server/sentry.js +287 -0
  184. package/dist/server/sentry.js.map +1 -0
  185. package/dist/server/transcribe-voice.d.ts +2 -4
  186. package/dist/server/transcribe-voice.d.ts.map +1 -1
  187. package/dist/server/transcribe-voice.js +4 -16
  188. package/dist/server/transcribe-voice.js.map +1 -1
  189. package/dist/server/voice-providers-status.d.ts.map +1 -1
  190. package/dist/server/voice-providers-status.js +19 -35
  191. package/dist/server/voice-providers-status.js.map +1 -1
  192. package/dist/styles/agent-native.css +15 -0
  193. package/docs/content/cloneable-saas.md +7 -9
  194. package/docs/content/deployment.md +6 -2
  195. package/docs/content/dispatch.md +1 -1
  196. package/docs/content/extensions.md +177 -142
  197. package/docs/content/faq.md +2 -2
  198. package/docs/content/getting-started.md +13 -11
  199. package/docs/content/multi-app-workspace.md +2 -2
  200. package/docs/content/observability.md +47 -0
  201. package/docs/content/pure-agent-apps.md +1 -1
  202. package/docs/content/template-clips.md +3 -3
  203. package/docs/content/template-design.md +3 -3
  204. package/docs/content/template-dispatch.md +1 -1
  205. package/docs/content/template-forms.md +1 -1
  206. package/docs/content/template-mail.md +1 -1
  207. package/docs/content/what-is-agent-native.md +4 -4
  208. package/docs/content/workspace.md +1 -1
  209. package/package.json +1 -1
@@ -63,6 +63,9 @@ export function getOnboardingHtml(opts = {}) {
63
63
  pointer-events: none;
64
64
  z-index: 0;
65
65
  }
66
+ @media (prefers-reduced-motion: reduce) {
67
+ #starfield { opacity: 0.18; }
68
+ }
66
69
  .split {
67
70
  position: relative;
68
71
  z-index: 1;
@@ -255,6 +258,8 @@ ${marketing.description ? ` <p class="app-desc">${esc(marketing.description
255
258
 
256
259
  var uTime = gl.getUniformLocation(prog, 'iTime');
257
260
  var uRes = gl.getUniformLocation(prog, 'iResolution');
261
+ var reducedMotionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
262
+ var reducedMotion = reducedMotionQuery ? reducedMotionQuery.matches : false;
258
263
 
259
264
  function resize() {
260
265
  var w = window.innerWidth, h = window.innerHeight;
@@ -265,16 +270,50 @@ ${marketing.description ? ` <p class="app-desc">${esc(marketing.description
265
270
  resize();
266
271
  window.addEventListener('resize', resize);
267
272
 
268
- var start = performance.now(), last = 0;
273
+ var start = performance.now(), last = 0, raf = 0, reducedMotionStaticTime = 20;
274
+ function draw(timeSeconds) {
275
+ gl.uniform1f(uTime, timeSeconds);
276
+ gl.uniform2f(uRes, canvas.width, canvas.height);
277
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
278
+ }
269
279
  function render(now) {
270
- requestAnimationFrame(render);
280
+ if (reducedMotion) {
281
+ raf = 0;
282
+ return;
283
+ }
284
+ raf = requestAnimationFrame(render);
271
285
  if (now - last < 33) return;
272
286
  last = now;
273
- gl.uniform1f(uTime, (now - start) * 0.001);
274
- gl.uniform2f(uRes, canvas.width, canvas.height);
275
- gl.drawArrays(gl.TRIANGLES, 0, 6);
287
+ draw((now - start) * 0.001);
288
+ }
289
+ function startAnimation() {
290
+ if (!raf) raf = requestAnimationFrame(render);
291
+ }
292
+ function stopAnimation() {
293
+ if (raf) {
294
+ cancelAnimationFrame(raf);
295
+ raf = 0;
296
+ }
297
+ }
298
+ function onReducedMotionChange() {
299
+ reducedMotion = reducedMotionQuery ? reducedMotionQuery.matches : false;
300
+ if (reducedMotion) {
301
+ stopAnimation();
302
+ last = 0;
303
+ draw(reducedMotionStaticTime);
304
+ } else {
305
+ startAnimation();
306
+ }
307
+ }
308
+ draw(reducedMotion ? reducedMotionStaticTime : 0);
309
+ if (reducedMotionQuery) {
310
+ if (reducedMotionQuery.addEventListener) {
311
+ reducedMotionQuery.addEventListener('change', onReducedMotionChange);
312
+ } else {
313
+ reducedMotionQuery.addListener(onReducedMotionChange);
314
+ }
276
315
  }
277
- requestAnimationFrame(render);
316
+ if (!reducedMotion) startAnimation();
278
317
  })();`
279
318
  : "";
280
319
  return `<!DOCTYPE html>
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding-html.js","sourceRoot":"","sources":["../../src/server/onboarding-html.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First-run onboarding page for agent-native apps.\n *\n * Shown when Better Auth is active and the user isn't signed in.\n * Provides a path to create or sign into an account from day one.\n *\n * After first account exists, this page acts as a normal login page.\n */\n\nfunction hasGoogleOAuth(): boolean {\n return !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);\n}\n\nfunction getConnectionLabel(): string {\n const url = process.env.DATABASE_URL || \"\";\n if (!url) return \"SQLite (local file)\";\n if (url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\")) {\n if (url.includes(\"neon.tech\")) return \"Neon Postgres\";\n if (url.includes(\"supabase\")) return \"Supabase Postgres\";\n return \"Postgres\";\n }\n if (url.startsWith(\"file:\")) return \"SQLite (local file)\";\n if (url.startsWith(\"libsql://\") || url.includes(\"turso.io\")) return \"Turso\";\n return \"SQL database\";\n}\n\nfunction normalizeAppBasePath(value: string | undefined): string {\n if (!value || value === \"/\") return \"\";\n const trimmed = value.trim();\n if (!trimmed || trimmed === \"/\") return \"\";\n return `/${trimmed.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")}`;\n}\n\nfunction withAppBasePath(path: string): string {\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n const basePath = normalizeAppBasePath(\n process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH,\n );\n return `${basePath}${cleanPath}`;\n}\n\nexport interface OnboardingHtmlOptions {\n /**\n * Hide email/password forms and show ONLY the Google sign-in button.\n * Useful for templates (mail, calendar) where Google is required anyway.\n * If Google OAuth env vars are not configured, an error message is shown.\n */\n googleOnly?: boolean;\n /**\n * Product marketing content shown alongside the sign-in form.\n * When provided, the page uses a split layout: marketing on the left,\n * sign-in form on the right (stacked on mobile).\n */\n marketing?: {\n appName: string;\n tagline: string;\n description?: string;\n features?: string[];\n };\n}\n\nexport function getOnboardingHtml(opts: OnboardingHtmlOptions = {}): string {\n const showGoogle = hasGoogleOAuth();\n const googleOnly = !!opts.googleOnly;\n const appBasePath = normalizeAppBasePath(\n process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH,\n );\n\n const marketing = opts.marketing;\n const hasMarketing = !!marketing;\n const brandMarkSrc = withAppBasePath(\"/agent-native-icon-dark.svg\");\n const esc = (s: string) =>\n s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n\n const marketingStyles = hasMarketing\n ? `\n body.has-marketing { padding: 0; position: relative; overflow-x: hidden; }\n #starfield {\n position: fixed;\n inset: 0;\n width: 100%;\n height: 100%;\n opacity: 0.35;\n pointer-events: none;\n z-index: 0;\n }\n .split {\n position: relative;\n z-index: 1;\n display: flex;\n min-height: 100vh;\n width: 100%;\n max-width: 1100px;\n margin: 0 auto;\n }\n .marketing-panel {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n padding: 3rem 3.5rem;\n }\n .marketing-content { max-width: 480px; }\n .app-name {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 2rem;\n font-weight: 700;\n color: #fff;\n margin-bottom: 0.625rem;\n letter-spacing: -0.02em;\n }\n .app-name img.brand-mark {\n height: 2.21375rem;\n width: auto;\n display: block;\n flex-shrink: 0;\n }\n .app-tagline {\n font-size: 1.25rem;\n color: #a1a1aa;\n line-height: 1.6;\n margin-bottom: 2rem;\n }\n .app-desc {\n font-size: 1rem;\n color: #71717a;\n line-height: 1.6;\n margin-bottom: 2rem;\n }\n .feature-list {\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 0.875rem;\n }\n .feature-list li {\n display: flex;\n align-items: flex-start;\n gap: 0.625rem;\n font-size: 1rem;\n color: #a1a1aa;\n line-height: 1.5;\n }\n .feature-list li::before {\n content: '';\n flex-shrink: 0;\n width: 8px;\n height: 8px;\n margin-top: 6px;\n border-radius: 50%;\n background: #3f3f46;\n border: 1px solid #52525b;\n }\n .oss-link {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n margin-top: 2rem;\n font-size: 0.8125rem;\n color: #71717a;\n text-decoration: none;\n }\n .oss-link:hover { color: #a1a1aa; }\n .oss-link svg { width: 15px; height: 15px; flex-shrink: 0; }\n .form-panel {\n flex: 0 0 440px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n }\n .form-panel .card { max-width: 400px; }\n .form-panel .local-note { max-width: 400px; }\n @media (max-width: 900px) {\n .split { flex-direction: column; min-height: auto; }\n .marketing-panel { padding: 2rem 1.5rem 1.5rem; }\n .app-name { font-size: 1.375rem; }\n .app-name img.brand-mark { height: 1.58125rem; }\n .app-tagline { font-size: 1rem; margin-bottom: 1rem; }\n .app-desc { margin-bottom: 1rem; }\n .feature-list { gap: 0.5rem; }\n .form-panel { flex: none; padding: 1.5rem 1rem; }\n }\n`\n : \"\";\n\n const marketingPanelHtml = hasMarketing\n ? `<canvas id=\"starfield\"></canvas>\n<div class=\"split\">\n <div class=\"marketing-panel\">\n <div class=\"marketing-content\">\n <h2 class=\"app-name\">\n <img class=\"brand-mark\" src=\"${esc(brandMarkSrc)}\" alt=\"\" aria-hidden=\"true\" />\n <span>${esc(marketing!.appName)}</span>\n </h2>\n <p class=\"app-tagline\">${esc(marketing!.tagline)}</p>\n${marketing!.description ? ` <p class=\"app-desc\">${esc(marketing!.description)}</p>\\n` : \"\"}${\n marketing!.features?.length\n ? ` <ul class=\"feature-list\">\\n${marketing!.features.map((f) => ` <li>${esc(f)}</li>`).join(\"\\n\")}\\n </ul>\\n`\n : \"\"\n } <a class=\"oss-link\" href=\"https://github.com/BuilderIO/agent-native\" target=\"_blank\" rel=\"noreferrer\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 00-1.3-3.2 4.2 4.2 0 00-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 00-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 00-.1 3.2A4.6 4.6 0 004 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21\"/></svg>\n Open source\n </a>\n </div>\n </div>\n <div class=\"form-panel\">`\n : \"\";\n\n const marketingCloseHtml = hasMarketing ? `\\n </div>\\n</div>` : \"\";\n\n const starfieldScript = hasMarketing\n ? `\n (function initStarfield() {\n var canvas = document.getElementById('starfield');\n if (!canvas) return;\n var gl = canvas.getContext('webgl', { alpha: false, antialias: false });\n if (!gl) return;\n\n var vs = gl.createShader(gl.VERTEX_SHADER);\n gl.shaderSource(vs, 'attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}');\n gl.compileShader(vs);\n\n var fs = gl.createShader(gl.FRAGMENT_SHADER);\n gl.shaderSource(fs, [\n 'precision highp float;',\n 'uniform float iTime;uniform vec2 iResolution;',\n '#define S(a,b,t) smoothstep(a,b,t)',\n '#define NUM_LAYERS 4.',\n 'float N21(vec2 p){vec3 a=fract(vec3(p.xyx)*vec3(213.897,653.453,253.098));a+=dot(a,a.yzx+79.76);return fract((a.x+a.y)*a.z);}',\n 'vec2 GetPos(vec2 id,vec2 offs,float t){float n=N21(id+offs);float n1=fract(n*10.);float n2=fract(n*100.);float a=t+n;return offs+vec2(sin(a*n1),cos(a*n2))*.4;}',\n 'float df_line(vec2 a,vec2 b,vec2 p){vec2 pa=p-a,ba=b-a;float h=clamp(dot(pa,ba)/dot(ba,ba),0.,1.);return length(pa-ba*h);}',\n 'float line(vec2 a,vec2 b,vec2 uv){float r1=.025;float r2=.006;float d=df_line(a,b,uv);float d2=length(a-b);float fade=S(1.5,.5,d2);fade+=S(.05,.02,abs(d2-.75));return S(r1,r2,d)*fade;}',\n 'float NetLayer(vec2 st,float n,float t){',\n ' vec2 id=floor(st)+n;st=fract(st)-.5;',\n ' vec2 p0=GetPos(id,vec2(-1,-1),t);vec2 p1=GetPos(id,vec2(0,-1),t);vec2 p2=GetPos(id,vec2(1,-1),t);',\n ' vec2 p3=GetPos(id,vec2(-1,0),t);vec2 p4=GetPos(id,vec2(0,0),t);vec2 p5=GetPos(id,vec2(1,0),t);',\n ' vec2 p6=GetPos(id,vec2(-1,1),t);vec2 p7=GetPos(id,vec2(0,1),t);vec2 p8=GetPos(id,vec2(1,1),t);',\n ' float m=0.;float sparkle=0.;float d;float s;float pulse;',\n ' m+=line(p4,p0,st);d=length(st-p0);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p0.x)+fract(p0.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p1,st);d=length(st-p1);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p1.x)+fract(p1.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p2,st);d=length(st-p2);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p2.x)+fract(p2.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p3,st);d=length(st-p3);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p3.x)+fract(p3.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p4,st);d=length(st-p4);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p4.x)+fract(p4.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p5,st);d=length(st-p5);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p5.x)+fract(p5.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p6,st);d=length(st-p6);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p6.x)+fract(p6.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p7,st);d=length(st-p7);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p7.x)+fract(p7.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p8,st);d=length(st-p8);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p8.x)+fract(p8.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p1,p3,st);m+=line(p1,p5,st);m+=line(p7,p5,st);m+=line(p7,p3,st);',\n ' float sPhase=(sin(t+n)+sin(t*.1))*.25+.5;sPhase+=pow(sin(t*.1)*.5+.5,50.)*5.;m+=sparkle*sPhase;',\n ' return m;',\n '}',\n 'void mainImage(out vec4 fragColor,in vec2 fragCoord){',\n ' vec2 uv=(fragCoord-iResolution.xy*.5)/iResolution.y;',\n ' float t=iTime*.03;float s=sin(t);float c=cos(t);mat2 rot=mat2(c,-s,s,c);vec2 st=uv*rot;',\n ' float m=0.;',\n ' for(float i=0.;i<1.;i+=1./NUM_LAYERS){float z=fract(t+i);float size=mix(15.,1.,z);float fade=S(0.,.6,z)*S(1.,.8,z);m+=fade*NetLayer(st*size,i,iTime*0.3);}',\n ' vec3 col=vec3(0.35)*m;col*=1.-dot(uv,uv);',\n ' float tt=min(iTime,5.0);col*=S(0.,20.,tt);',\n ' col=clamp(col,0.,1.);fragColor=vec4(col,1.);',\n '}',\n 'void main(){mainImage(gl_FragColor,gl_FragCoord.xy);}'\n ].join('\\\\n'));\n gl.compileShader(fs);\n\n var prog = gl.createProgram();\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n gl.useProgram(prog);\n\n var buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), gl.STATIC_DRAW);\n var pos = gl.getAttribLocation(prog, 'position');\n gl.enableVertexAttribArray(pos);\n gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);\n\n var uTime = gl.getUniformLocation(prog, 'iTime');\n var uRes = gl.getUniformLocation(prog, 'iResolution');\n\n function resize() {\n var w = window.innerWidth, h = window.innerHeight;\n var dpr = Math.min(window.devicePixelRatio, 1.5);\n canvas.width = w * dpr; canvas.height = h * dpr;\n gl.viewport(0, 0, canvas.width, canvas.height);\n }\n resize();\n window.addEventListener('resize', resize);\n\n var start = performance.now(), last = 0;\n function render(now) {\n requestAnimationFrame(render);\n if (now - last < 33) return;\n last = now;\n gl.uniform1f(uTime, (now - start) * 0.001);\n gl.uniform2f(uRes, canvas.width, canvas.height);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n }\n requestAnimationFrame(render);\n })();`\n : \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n<title>${hasMarketing ? esc(marketing!.appName) + \" — Sign in\" : \"Welcome\"}</title>\n${\n hasMarketing\n ? `<meta name=\"description\" content=\"${esc(marketing!.tagline)}\">\n<meta property=\"og:title\" content=\"${esc(marketing!.appName)}\">\n<meta property=\"og:description\" content=\"${esc(marketing!.tagline)}\">`\n : \"\"\n}\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n padding: 1rem;\n }\n .card {\n width: 100%;\n max-width: 400px;\n padding: 2rem;\n background: #141414;\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 12px;\n }\n h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.25rem; color: #fff; }\n .subtitle { font-size: 0.8125rem; color: #888; margin-bottom: 1.5rem; }\n .tabs {\n display: inline-flex;\n width: 100%;\n padding: 4px;\n margin-bottom: 1.5rem;\n background: rgba(255,255,255,0.06);\n border-radius: 8px;\n }\n .tab {\n flex: 1;\n padding: 0.5rem 0.75rem;\n background: none;\n border: none;\n color: #888;\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n border-radius: 6px;\n }\n .tab.active {\n background: #1e1e1e;\n color: #fff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.3);\n }\n .tab:hover:not(.active) { color: #bbb; }\n .form { display: none; }\n .form.active { display: block; }\n .card.verifying .tabs,\n .card.verifying #google-btn,\n .card.verifying #google-err,\n .card.verifying #auth-divider,\n .card.verifying #upgrade-note {\n display: none;\n }\n label { display: block; font-size: 0.8125rem; color: #888; margin-bottom: 0.375rem; }\n input {\n width: 100%;\n padding: 0.5rem 0.75rem;\n background: transparent;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n color: #e5e5e5;\n font-size: 0.875rem;\n outline: none;\n margin-bottom: 0.875rem;\n }\n input:focus { border-color: rgba(255,255,255,0.3); box-shadow: 0 0 0 1px rgba(255,255,255,0.1); }\n input::placeholder { color: #555; }\n button[type=\"submit\"], .btn-primary {\n width: 100%;\n margin-top: 0.25rem;\n padding: 0.5rem;\n background: #fff;\n color: #000;\n border: none;\n border-radius: 6px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n }\n button[type=\"submit\"]:hover, .btn-primary:hover { background: #e5e5e5; }\n button[type=\"submit\"]:disabled { opacity: 0.5; cursor: not-allowed; }\n .btn-secondary {\n width: 100%;\n margin-top: 0.75rem;\n padding: 0.5rem;\n background: transparent;\n color: #888;\n border: 1px solid rgba(255,255,255,0.1);\n border-radius: 6px;\n font-size: 0.8125rem;\n cursor: pointer;\n }\n .btn-secondary:hover { color: #bbb; border-color: rgba(255,255,255,0.2); }\n .msg { margin-top: 0.75rem; font-size: 0.8125rem; display: none; }\n .msg.error { color: #f87171; }\n .msg.success { color: #4ade80; }\n .msg.show { display: block; }\n .step-progress {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 0.5rem;\n margin-bottom: 1.25rem;\n }\n .progress-step {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.375rem;\n color: #666;\n font-size: 0.6875rem;\n line-height: 1.2;\n text-align: center;\n }\n .progress-step::before {\n content: '';\n position: absolute;\n top: 11px;\n left: calc(-50% + 16px);\n width: calc(100% - 32px);\n height: 1px;\n background: rgba(255,255,255,0.1);\n }\n .progress-step:first-child::before { display: none; }\n .progress-step span {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 999px;\n border: 1px solid rgba(255,255,255,0.14);\n background: #151515;\n color: #777;\n font-size: 0.6875rem;\n font-weight: 600;\n }\n .progress-step strong { font-weight: 500; }\n .progress-step.complete,\n .progress-step.current { color: #e5e5e5; }\n .progress-step.complete span {\n background: #d9f99d;\n border-color: #d9f99d;\n color: #111;\n }\n .progress-step.current span {\n background: #fff;\n border-color: #fff;\n color: #000;\n box-shadow: 0 0 0 4px rgba(255,255,255,0.08);\n }\n .verification-panel {\n padding: 1rem;\n margin-bottom: 0.875rem;\n background: rgba(255,255,255,0.04);\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 8px;\n }\n .verification-kicker {\n margin-bottom: 0.5rem;\n color: #bef264;\n font-size: 0.75rem;\n font-weight: 500;\n }\n .verification-copy {\n color: #d4d4d8;\n font-size: 0.875rem;\n line-height: 1.55;\n }\n .verification-copy strong {\n color: #fff;\n font-weight: 600;\n word-break: break-word;\n }\n .verification-note {\n margin-top: 0.75rem;\n color: #71717a;\n font-size: 0.75rem;\n line-height: 1.45;\n }\n .inline-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n margin-top: 0.75rem;\n }\n .link-button {\n padding: 0.25rem 0;\n background: none;\n border: none;\n color: #888;\n cursor: pointer;\n font-size: 0.75rem;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .link-button:hover { color: #bbb; }\n .link-button:disabled { cursor: wait; opacity: 0.5; }\n .divider {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin: 1.25rem 0;\n font-size: 0.75rem;\n color: #555;\n }\n .divider::before, .divider::after {\n content: '';\n flex: 1;\n height: 1px;\n background: rgba(255,255,255,0.08);\n }\n .upgrade-note {\n margin-bottom: 1rem;\n padding: 0.75rem;\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 8px;\n background: rgba(255,255,255,0.03);\n font-size: 0.75rem;\n line-height: 1.5;\n color: #a1a1aa;\n display: none;\n }\n .upgrade-note.show { display: block; }\n .btn-google {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.625rem;\n padding: 0.5rem;\n background: #fff;\n color: #000;\n border: none;\n border-radius: 6px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n }\n .btn-google:hover { background: #e5e5e5; }\n .btn-google:disabled { opacity: 0.5; cursor: wait; }\n .btn-google svg { width: 18px; height: 18px; flex-shrink: 0; }\n .google-error { margin-top: 0.5rem; font-size: 0.8125rem; color: #f87171; display: none; }\n .google-error.show { display: block; }\n .local-note {\n display: none;\n max-width: 400px;\n width: 100%;\n margin-top: 1rem;\n padding: 0.625rem 0.875rem;\n font-size: 0.6875rem;\n line-height: 1.5;\n color: #666;\n border: 1px dashed rgba(255,255,255,0.08);\n border-radius: 8px;\n text-align: center;\n }\n .local-note.show { display: block; }\n .local-note strong { color: #999; font-weight: 500; }\n .local-note a { color: #888; text-decoration: none; }\n .local-note a:hover { color: #bbb; }\n${marketingStyles}\n</style>\n</head>\n<body${hasMarketing ? ' class=\"has-marketing\"' : \"\"}>\n${marketingPanelHtml}\n<div class=\"card\">\n <h1 id=\"heading\">${googleOnly ? \"Sign in\" : \"Welcome\"}</h1>\n <p class=\"subtitle\" id=\"subtitle\">${googleOnly ? \"Use your workspace Google account to continue\" : \"Create an account to get started\"}</p>\n <p\n class=\"upgrade-note\"\n id=\"upgrade-note\"\n data-upgrade-copy=\"Continue signing in to attach this app to your account and migrate local data.\"\n ></p>\n\n${\n showGoogle\n ? `\n <button class=\"btn-google\" id=\"google-btn\" onclick=\"signInWithGoogle()\">\n <svg viewBox=\"0 0 24 24\"><path fill=\"#4285F4\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z\"/><path fill=\"#34A853\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/><path fill=\"#FBBC05\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/><path fill=\"#EA4335\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/></svg>\n Sign in with Google\n </button>\n <p class=\"google-error\" id=\"google-err\"></p>\n${googleOnly ? \"\" : `\\n <div class=\"divider\" id=\"auth-divider\">or</div>\\n`}\n`\n : googleOnly\n ? `\n <p style=\"color:#f87171;font-size:0.875rem;text-align:center;padding:1rem 0\">\n Google sign-in is not configured. Set <code>GOOGLE_CLIENT_ID</code> and\n <code>GOOGLE_CLIENT_SECRET</code> environment variables to enable login.\n </p>\n`\n : \"\"\n}\n${\n googleOnly\n ? \"\"\n : ` <div class=\"tabs\">\n <button class=\"tab\" data-tab=\"signup\">Create account</button>\n <button class=\"tab\" data-tab=\"login\">Sign in</button>\n </div>\n\n <form id=\"signup-form\" class=\"form\">\n <label for=\"s-email\">Email</label>\n <input id=\"s-email\" type=\"email\" autocomplete=\"email\" autofocus placeholder=\"you@example.com\" required />\n <label for=\"s-pass\">Password</label>\n <input id=\"s-pass\" type=\"password\" autocomplete=\"new-password\" placeholder=\"At least 8 characters\" required minlength=\"8\" />\n <label for=\"s-pass2\">Confirm password</label>\n <input id=\"s-pass2\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Confirm password\" required minlength=\"8\" />\n <button type=\"submit\">Create account</button>\n <p class=\"msg\" id=\"s-msg\"></p>\n </form>\n\n <div id=\"verification-step\" class=\"form verification-step\" aria-live=\"polite\">\n <div class=\"step-progress\" aria-label=\"Signup progress\">\n <div class=\"progress-step complete\"><span>1</span><strong>Account</strong></div>\n <div class=\"progress-step current\"><span>2</span><strong>Verify</strong></div>\n <div class=\"progress-step\"><span>3</span><strong>Start</strong></div>\n </div>\n <div class=\"verification-panel\">\n <p class=\"verification-kicker\">Verification email sent</p>\n <p class=\"verification-copy\">We sent a secure link to <strong id=\"verify-email\"></strong>. Click it, return here, and this app will finish signing you in automatically.</p>\n <p class=\"verification-note\">You can keep this tab open. If it has not refreshed after you come back, use Continue.</p>\n </div>\n <button type=\"button\" class=\"btn-primary\" id=\"verify-continue\">Continue</button>\n <div class=\"inline-actions\">\n <button type=\"button\" class=\"link-button\" id=\"resend-verification\">Resend email</button>\n <button type=\"button\" class=\"link-button\" id=\"back-to-signup\">Back</button>\n </div>\n <p class=\"msg\" id=\"verify-msg\"></p>\n </div>\n\n <form id=\"login-form\" class=\"form\">\n <label for=\"l-email\">Email</label>\n <input id=\"l-email\" type=\"email\" autocomplete=\"email\" placeholder=\"you@example.com\" required />\n <label for=\"l-pass\">Password</label>\n <input id=\"l-pass\" type=\"password\" autocomplete=\"current-password\" placeholder=\"Enter password\" required />\n <button type=\"submit\">Sign in</button>\n <p class=\"msg error\" id=\"l-msg\"></p>\n <p style=\"margin-top:0.75rem;font-size:0.75rem;text-align:right\">\n <a href=\"#\" id=\"forgot-link\" style=\"color:#888;text-decoration:underline;text-underline-offset:2px\">Forgot password?</a>\n </p>\n </form>\n\n <form id=\"forgot-form\" class=\"form\">\n <label for=\"f-email\">Email</label>\n <input id=\"f-email\" type=\"email\" autocomplete=\"email\" placeholder=\"you@example.com\" required />\n <button type=\"submit\">Send reset link</button>\n <p class=\"msg\" id=\"f-msg\"></p>\n <p style=\"margin-top:0.75rem;font-size:0.75rem;text-align:center\">\n <a href=\"#\" id=\"back-to-login\" style=\"color:#888;text-decoration:underline;text-underline-offset:2px\">Back to sign in</a>\n </p>\n </form>`\n}\n</div>\n<p class=\"local-note\" id=\"local-note\">\n Your account is stored in this app's own DB (<strong>${getConnectionLabel()}</strong>), not a third-party service.\n</p>${marketingCloseHtml}\n<script>\n function __anBasePath() {\n var configured = ${JSON.stringify(appBasePath)};\n if (configured) return configured;\n var marker = '/_agent-native';\n var idx = window.location.pathname.indexOf(marker);\n return idx > 0 ? window.location.pathname.slice(0, idx) : '';\n }\n function __anPath(path) {\n return __anBasePath() + path;\n }\n function __anGetReturnPath() {\n try {\n var inner = new URLSearchParams(window.location.search).get('return');\n if (inner) return inner;\n } catch(e) {}\n return window.location.pathname + window.location.search;\n }\n (function revealLocalNote() {\n var h = location.hostname;\n if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h.endsWith('.local')) {\n var n = document.getElementById('local-note');\n if (n) n.classList.add('show');\n }\n })();\n (function revealUpgradeNote() {\n var shouldShow = false;\n try {\n var params = new URLSearchParams(location.search);\n shouldShow = params.get('signin') === '1' || params.get('upgrade-from-local') === '1';\n } catch(e) {}\n if (!shouldShow) {\n try { shouldShow = localStorage.getItem('an_migrate_from_local') === '1'; } catch(e) {}\n }\n if (!shouldShow) return;\n var n = document.getElementById('upgrade-note');\n if (!n) return;\n n.textContent = n.getAttribute('data-upgrade-copy') || 'Continue signing in to migrate local data.';\n n.classList.add('show');\n })();\n${\n googleOnly\n ? \"\"\n : ` var TAB_STORAGE_KEY = 'an.onboarding.tab';\n var tabs = document.querySelectorAll('.tab');\n var forms = document.querySelectorAll('.form');\n var subtitles = { signup: 'Create an account to get started', login: 'Sign in to your account' };\n var headings = { signup: 'Welcome', login: 'Welcome back' };\n var pendingSignupEmail = '';\n var pendingSignupPassword = '';\n var verificationCheckInFlight = false;\n function setActiveTab(name, opts) {\n if (name !== 'signup' && name !== 'login') return;\n var form = document.getElementById(name + '-form');\n if (!form) return;\n var card = document.querySelector('.card');\n if (card) card.classList.remove('verifying');\n tabs.forEach(function(x) { x.classList.remove('active'); });\n forms.forEach(function(x) { x.classList.remove('active'); });\n var btn = document.querySelector('.tab[data-tab=\"' + name + '\"]');\n if (btn) btn.classList.add('active');\n form.classList.add('active');\n var sub = document.getElementById('subtitle');\n if (sub && subtitles[name]) sub.textContent = subtitles[name];\n var heading = document.getElementById('heading');\n if (heading && headings[name]) heading.textContent = headings[name];\n if (opts && opts.persist) {\n try { localStorage.setItem(TAB_STORAGE_KEY, name); } catch (e) {}\n }\n }\n function showVerificationStep(email, password) {\n pendingSignupEmail = email || '';\n pendingSignupPassword = password || '';\n tabs.forEach(function(x) { x.classList.remove('active'); });\n forms.forEach(function(x) { x.classList.remove('active'); });\n var card = document.querySelector('.card');\n if (card) card.classList.add('verifying');\n var step = document.getElementById('verification-step');\n if (step) step.classList.add('active');\n var emailNode = document.getElementById('verify-email');\n if (emailNode) emailNode.textContent = pendingSignupEmail;\n var heading = document.getElementById('heading');\n if (heading) heading.textContent = 'Check your email';\n var sub = document.getElementById('subtitle');\n if (sub) sub.textContent = 'Finish creating your account';\n var msg = document.getElementById('verify-msg');\n if (msg) {\n msg.classList.remove('show', 'error', 'success');\n msg.textContent = '';\n }\n try { localStorage.setItem(TAB_STORAGE_KEY, 'signup'); } catch (e) {}\n }\n function getVerificationMessageNode() {\n var verifyStep = document.getElementById('verification-step');\n if (verifyStep && verifyStep.classList.contains('active')) {\n return document.getElementById('verify-msg');\n }\n return document.getElementById('l-msg') || document.getElementById('verify-msg');\n }\n function isVerificationStepActive() {\n var verifyStep = document.getElementById('verification-step');\n return !!(verifyStep && verifyStep.classList.contains('active'));\n }\n function getPendingSignupEmail() {\n var signupEmail = document.getElementById('s-email');\n var loginEmail = document.getElementById('l-email');\n return (pendingSignupEmail || (signupEmail && signupEmail.value) || (loginEmail && loginEmail.value) || '').trim();\n }\n function getPendingSignupPassword() {\n var signupPassword = document.getElementById('s-pass');\n return pendingSignupPassword || (signupPassword && signupPassword.value) || '';\n }\n function movePendingSignupToLogin(message) {\n var email = getPendingSignupEmail();\n setActiveTab('login', { persist: true });\n var loginEmail = document.getElementById('l-email');\n var loginPassword = document.getElementById('l-pass');\n var msg = document.getElementById('l-msg');\n if (loginEmail && email) loginEmail.value = email;\n if (msg) {\n msg.textContent = message || 'Sign in to continue.';\n msg.classList.remove('error');\n msg.classList.add('show', 'success');\n }\n setTimeout(function() { if (loginPassword) loginPassword.focus(); }, 0);\n }\n async function signInWithPendingSignup() {\n var email = getPendingSignupEmail();\n var password = getPendingSignupPassword();\n if (!email || !password) {\n return { ok: false, needsManualSignIn: true };\n }\n var res = await fetch(__anPath('/_agent-native/auth/login'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email, password: password }),\n });\n if (res.ok) {\n window.location.reload();\n return { ok: true };\n }\n var data = await res.json().catch(function() { return {}; });\n var error = (data && (data.error || data.message)) || 'Could not finish sign-in automatically.';\n return {\n ok: false,\n error: error,\n isWaitingForVerification: /not verified|verification/i.test(error),\n };\n }\n async function checkVerificationSession(fallbackText, opts) {\n opts = opts || {};\n if (verificationCheckInFlight) return;\n verificationCheckInFlight = true;\n var msg = getVerificationMessageNode();\n var continueBtn = document.getElementById('verify-continue');\n if (continueBtn && !opts.silent) {\n continueBtn.disabled = true;\n continueBtn.textContent = 'Checking...';\n }\n if (msg && !opts.silent) {\n msg.textContent = 'Checking your verification...';\n msg.classList.remove('error');\n msg.classList.add('show', 'success');\n }\n try {\n var res = await fetch(__anPath('/_agent-native/auth/session'), {\n headers: { 'Accept': 'application/json' },\n });\n var data = await res.json().catch(function() { return {}; });\n if (res.ok && data && data.email && !data.error) {\n window.location.reload();\n return;\n }\n var loginResult = await signInWithPendingSignup();\n if (loginResult.ok) return;\n if (loginResult.needsManualSignIn) {\n if (!opts.silent) {\n movePendingSignupToLogin(fallbackText || 'Enter your password after verifying your email.');\n }\n return;\n }\n if (loginResult.error && !loginResult.isWaitingForVerification) {\n if (!opts.silent) {\n movePendingSignupToLogin('We could not finish sign-in automatically. Sign in to continue.');\n }\n return;\n }\n if (msg && !opts.silent) {\n msg.textContent = fallbackText || 'Still waiting on verification. Click the link in your email, then try Continue again.';\n msg.classList.remove('success');\n msg.classList.add('show', 'error');\n }\n } catch (err) {\n if (msg && !opts.silent) {\n msg.textContent = 'Could not check verification. Please try again.';\n msg.classList.remove('success');\n msg.classList.add('show', 'error');\n }\n } finally {\n verificationCheckInFlight = false;\n if (continueBtn && !opts.silent) {\n continueBtn.disabled = false;\n continueBtn.textContent = 'Continue';\n }\n }\n }\n function maybeCompleteVerificationAfterReturn() {\n if (!isVerificationStepActive()) return;\n checkVerificationSession(null, { silent: true });\n }\n async function resendVerificationEmail() {\n var btn = document.getElementById('resend-verification');\n var msg = document.getElementById('verify-msg');\n var email = pendingSignupEmail || document.getElementById('s-email').value;\n if (!email) return;\n var original = btn ? btn.textContent : '';\n if (btn) {\n btn.disabled = true;\n btn.textContent = 'Sending...';\n }\n if (msg) msg.classList.remove('show', 'error', 'success');\n try {\n var res = await fetch(__anPath('/_agent-native/auth/ba/send-verification-email'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email, callbackURL: __anGetReturnPath() }),\n });\n if (res.ok) {\n if (msg) {\n msg.textContent = 'Sent a fresh verification link.';\n msg.classList.add('show', 'success');\n }\n if (btn) btn.textContent = 'Sent';\n setTimeout(function() {\n if (btn) {\n btn.disabled = false;\n btn.textContent = original;\n }\n }, 1600);\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n if (msg) {\n msg.textContent = (data && (data.message || data.error)) || 'Could not resend the verification email.';\n msg.classList.add('show', 'error');\n }\n if (btn) {\n btn.disabled = false;\n btn.textContent = original;\n }\n } catch (err) {\n if (msg) {\n msg.textContent = 'Network error. Please try again.';\n msg.classList.add('show', 'error');\n }\n if (btn) {\n btn.disabled = false;\n btn.textContent = original;\n }\n }\n }\n (function initActiveTab() {\n var initial = 'signup';\n try {\n var params = new URLSearchParams(location.search);\n var qp = params.get('tab');\n var path = location.pathname;\n while (path.length > 1 && path.charAt(path.length - 1) === '/') path = path.slice(0, -1);\n if (qp === 'login' || qp === 'signup') {\n initial = qp;\n } else if (params.has('verified')) {\n initial = 'login';\n } else if (path === '/login' || path.endsWith('/login')) {\n initial = 'login';\n } else if (path === '/signup' || path.endsWith('/signup')) {\n initial = 'signup';\n } else {\n var stored = localStorage.getItem(TAB_STORAGE_KEY);\n if (stored === 'login' || stored === 'signup') initial = stored;\n }\n } catch (e) {}\n setActiveTab(initial, { persist: false });\n try {\n if (new URLSearchParams(location.search).has('verified')) {\n var msg = document.getElementById('l-msg');\n if (msg) {\n msg.textContent = 'Email verified. Finishing sign-in...';\n msg.classList.remove('error');\n msg.classList.add('show', 'success');\n }\n checkVerificationSession('Email verified. Sign in to continue.');\n }\n } catch (e) {}\n })();\n tabs.forEach(function(t) { t.addEventListener('click', function() {\n setActiveTab(t.dataset.tab, { persist: true });\n }); });\n\n document.getElementById('signup-form').addEventListener('submit', async function(e) {\n e.preventDefault();\n var form = e.currentTarget;\n var btn = form.querySelector('button[type=\"submit\"]');\n var msg = document.getElementById('s-msg');\n msg.classList.remove('show', 'error', 'success');\n var pass = document.getElementById('s-pass').value;\n var pass2 = document.getElementById('s-pass2').value;\n if (pass !== pass2) {\n msg.textContent = 'Passwords do not match';\n msg.classList.add('show', 'error');\n return;\n }\n var originalLabel = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Creating account…';\n try {\n var email = document.getElementById('s-email').value;\n var res = await fetch(__anPath('/_agent-native/auth/register'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email: email,\n password: pass,\n callbackURL: __anGetReturnPath(),\n }),\n });\n var data = await res.json().catch(function() { return {}; });\n if (res.ok) {\n // If email verification is required, the server won't return a session.\n // Try logging in — if it fails (unverified), show a \"check your email\" message.\n var loginRes = await fetch(__anPath('/_agent-native/auth/login'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email, password: pass }),\n });\n if (loginRes.ok) {\n msg.textContent = 'Account created — signing you in…';\n msg.classList.add('show', 'success');\n window.location.reload();\n return;\n }\n btn.disabled = false;\n btn.textContent = originalLabel;\n showVerificationStep(email, pass);\n return;\n }\n msg.textContent = data.error || 'Registration failed';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = originalLabel;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = originalLabel;\n }\n });\n\n var verifyContinue = document.getElementById('verify-continue');\n if (verifyContinue) verifyContinue.addEventListener('click', function(e) {\n e.preventDefault();\n checkVerificationSession();\n });\n window.addEventListener('focus', maybeCompleteVerificationAfterReturn);\n document.addEventListener('visibilitychange', function() {\n if (document.visibilityState === 'visible') maybeCompleteVerificationAfterReturn();\n });\n var resendBtn = document.getElementById('resend-verification');\n if (resendBtn) resendBtn.addEventListener('click', function(e) {\n e.preventDefault();\n resendVerificationEmail();\n });\n var backToSignup = document.getElementById('back-to-signup');\n if (backToSignup) backToSignup.addEventListener('click', function(e) {\n e.preventDefault();\n setActiveTab('signup', { persist: true });\n var email = document.getElementById('s-email');\n setTimeout(function() { if (email) email.focus(); }, 0);\n });\n\n var forgotLink = document.getElementById('forgot-link');\n var backToLogin = document.getElementById('back-to-login');\n if (forgotLink) forgotLink.addEventListener('click', function(e) {\n e.preventDefault();\n document.getElementById('login-form').classList.remove('active');\n document.getElementById('forgot-form').classList.add('active');\n var sub = document.getElementById('subtitle');\n if (sub) sub.textContent = 'Reset your password';\n var heading = document.getElementById('heading');\n if (heading) heading.textContent = 'Reset password';\n var fEmail = document.getElementById('f-email');\n var lEmail = document.getElementById('l-email');\n if (lEmail && lEmail.value) fEmail.value = lEmail.value;\n setTimeout(function() { fEmail.focus(); }, 0);\n });\n if (backToLogin) backToLogin.addEventListener('click', function(e) {\n e.preventDefault();\n document.getElementById('forgot-form').classList.remove('active');\n document.getElementById('login-form').classList.add('active');\n var sub = document.getElementById('subtitle');\n if (sub) sub.textContent = subtitles.login;\n var heading = document.getElementById('heading');\n if (heading) heading.textContent = headings.login;\n });\n\n var forgotForm = document.getElementById('forgot-form');\n if (forgotForm) forgotForm.addEventListener('submit', async function(e) {\n e.preventDefault();\n var btn = e.currentTarget.querySelector('button[type=\"submit\"]');\n var msg = document.getElementById('f-msg');\n msg.classList.remove('show', 'error', 'success');\n var original = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Sending…';\n try {\n var email = document.getElementById('f-email').value;\n var res = await fetch(__anPath('/_agent-native/auth/ba/request-password-reset'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email }),\n });\n if (res.ok) {\n msg.textContent = 'If that email exists, a reset link is on its way.';\n msg.classList.add('show', 'success');\n btn.textContent = 'Sent';\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n msg.textContent = (data && (data.message || data.error)) || 'Could not send reset email.';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n }\n });\n\n document.getElementById('login-form').addEventListener('submit', async function(e) {\n e.preventDefault();\n var form = e.currentTarget;\n var btn = form.querySelector('button[type=\"submit\"]');\n var msg = document.getElementById('l-msg');\n msg.classList.remove('show', 'success');\n msg.classList.add('error');\n var originalLabel = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Signing in…';\n try {\n var res = await fetch(__anPath('/_agent-native/auth/login'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email: document.getElementById('l-email').value,\n password: document.getElementById('l-pass').value,\n }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n msg.textContent = data.error || 'Invalid email or password';\n msg.classList.add('show');\n btn.disabled = false;\n btn.textContent = originalLabel;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show');\n btn.disabled = false;\n btn.textContent = originalLabel;\n }\n });\n`\n}\n${\n showGoogle\n ? `\n async function signInWithGoogle() {\n var btn = document.getElementById('google-btn');\n var err = document.getElementById('google-err');\n btn.disabled = true;\n err.classList.remove('show');\n try {\n var ret = __anGetReturnPath();\n var authUrl = __anPath('/_agent-native/google/auth-url') + '?return=' + encodeURIComponent(ret);\n var res = await fetch(authUrl);\n var data = await res.json();\n if (data.url) {\n try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}\n window.location.href = data.url;\n } else {\n err.textContent = data.message || 'Google OAuth is not configured.';\n err.classList.add('show');\n btn.disabled = false;\n }\n } catch (e) {\n err.textContent = 'Failed to connect. Please try again.';\n err.classList.add('show');\n btn.disabled = false;\n }\n }`\n : \"\"\n}\n${starfieldScript}\n</script>\n</body>\n</html>`;\n}\n\n/** @deprecated Use getOnboardingHtml() instead */\nexport const ONBOARDING_HTML = getOnboardingHtml();\n\n/**\n * HTML for the password reset page — shown when the user clicks the link in\n * their reset email. Posts `{ newPassword, token }` to Better Auth's\n * `/reset-password` endpoint, then redirects to the login page.\n */\nexport function getResetPasswordHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n<title>Reset password</title>\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; }\n .card { width: 100%; max-width: 400px; padding: 2rem; background: #141414; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; }\n h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.25rem; color: #fff; }\n .subtitle { font-size: 0.8125rem; color: #888; margin-bottom: 1.5rem; }\n label { display: block; font-size: 0.8125rem; color: #888; margin-bottom: 0.375rem; }\n input { width: 100%; padding: 0.5rem 0.75rem; background: transparent; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; color: #e5e5e5; font-size: 0.875rem; outline: none; margin-bottom: 0.875rem; }\n input:focus { border-color: rgba(255,255,255,0.3); box-shadow: 0 0 0 1px rgba(255,255,255,0.1); }\n input::placeholder { color: #555; }\n button[type=\"submit\"] { width: 100%; margin-top: 0.25rem; padding: 0.5rem; background: #fff; color: #000; border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 500; cursor: pointer; }\n button[type=\"submit\"]:hover { background: #e5e5e5; }\n button[type=\"submit\"]:disabled { opacity: 0.5; cursor: not-allowed; }\n .msg { margin-top: 0.75rem; font-size: 0.8125rem; display: none; }\n .msg.error { color: #f87171; }\n .msg.success { color: #4ade80; }\n .msg.show { display: block; }\n .back { display: inline-block; margin-top: 1rem; font-size: 0.75rem; color: #888; text-decoration: none; }\n .back:hover { color: #bbb; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <h1>Choose a new password</h1>\n <p class=\"subtitle\">Set a new password for your account.</p>\n <form id=\"reset-form\">\n <label for=\"p1\">New password</label>\n <input id=\"p1\" type=\"password\" autocomplete=\"new-password\" autofocus placeholder=\"At least 8 characters\" required minlength=\"8\" />\n <label for=\"p2\">Confirm password</label>\n <input id=\"p2\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Confirm password\" required minlength=\"8\" />\n <button type=\"submit\">Save new password</button>\n <p class=\"msg\" id=\"msg\"></p>\n </form>\n <a class=\"back\" id=\"back-link\" href=\"/\">Back to sign in</a>\n</div>\n<script>\n (function() {\n // Derive the app's base path so apps mounted under a prefix\n // (e.g. /mail, /calendar) get sent home instead of to the root domain.\n var RESET_PATH = '/_agent-native/auth/reset';\n var pathname = window.location.pathname;\n var idx = pathname.indexOf(RESET_PATH);\n var basePath = (idx >= 0 ? pathname.slice(0, idx) : '') || '';\n var homeHref = basePath + '/';\n var backLink = document.getElementById('back-link');\n if (backLink) backLink.setAttribute('href', homeHref);\n var params = new URLSearchParams(location.search);\n var token = params.get('token') || '';\n var msg = document.getElementById('msg');\n if (!token) {\n msg.textContent = 'Missing or invalid reset token. Request a new reset link.';\n msg.classList.add('show', 'error');\n document.getElementById('reset-form').style.display = 'none';\n return;\n }\n document.getElementById('reset-form').addEventListener('submit', async function(e) {\n e.preventDefault();\n var btn = e.currentTarget.querySelector('button[type=\"submit\"]');\n var p1 = document.getElementById('p1').value;\n var p2 = document.getElementById('p2').value;\n msg.classList.remove('show', 'error', 'success');\n if (p1 !== p2) {\n msg.textContent = 'Passwords do not match';\n msg.classList.add('show', 'error');\n return;\n }\n var original = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Saving…';\n try {\n var res = await fetch(basePath + '/_agent-native/auth/ba/reset-password', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ newPassword: p1, token: token }),\n });\n if (res.ok) {\n msg.textContent = 'Password updated — redirecting to sign in…';\n msg.classList.add('show', 'success');\n setTimeout(function() { window.location.href = homeHref; }, 1200);\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n msg.textContent = (data && (data.message || data.error)) || 'Reset failed. The link may have expired — request a new one.';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n }\n });\n })();\n</script>\n</body>\n</html>`;\n}\n"]}
1
+ {"version":3,"file":"onboarding-html.js","sourceRoot":"","sources":["../../src/server/onboarding-html.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * First-run onboarding page for agent-native apps.\n *\n * Shown when Better Auth is active and the user isn't signed in.\n * Provides a path to create or sign into an account from day one.\n *\n * After first account exists, this page acts as a normal login page.\n */\n\nfunction hasGoogleOAuth(): boolean {\n return !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);\n}\n\nfunction getConnectionLabel(): string {\n const url = process.env.DATABASE_URL || \"\";\n if (!url) return \"SQLite (local file)\";\n if (url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\")) {\n if (url.includes(\"neon.tech\")) return \"Neon Postgres\";\n if (url.includes(\"supabase\")) return \"Supabase Postgres\";\n return \"Postgres\";\n }\n if (url.startsWith(\"file:\")) return \"SQLite (local file)\";\n if (url.startsWith(\"libsql://\") || url.includes(\"turso.io\")) return \"Turso\";\n return \"SQL database\";\n}\n\nfunction normalizeAppBasePath(value: string | undefined): string {\n if (!value || value === \"/\") return \"\";\n const trimmed = value.trim();\n if (!trimmed || trimmed === \"/\") return \"\";\n return `/${trimmed.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")}`;\n}\n\nfunction withAppBasePath(path: string): string {\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n const basePath = normalizeAppBasePath(\n process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH,\n );\n return `${basePath}${cleanPath}`;\n}\n\nexport interface OnboardingHtmlOptions {\n /**\n * Hide email/password forms and show ONLY the Google sign-in button.\n * Useful for templates (mail, calendar) where Google is required anyway.\n * If Google OAuth env vars are not configured, an error message is shown.\n */\n googleOnly?: boolean;\n /**\n * Product marketing content shown alongside the sign-in form.\n * When provided, the page uses a split layout: marketing on the left,\n * sign-in form on the right (stacked on mobile).\n */\n marketing?: {\n appName: string;\n tagline: string;\n description?: string;\n features?: string[];\n };\n}\n\nexport function getOnboardingHtml(opts: OnboardingHtmlOptions = {}): string {\n const showGoogle = hasGoogleOAuth();\n const googleOnly = !!opts.googleOnly;\n const appBasePath = normalizeAppBasePath(\n process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH,\n );\n\n const marketing = opts.marketing;\n const hasMarketing = !!marketing;\n const brandMarkSrc = withAppBasePath(\"/agent-native-icon-dark.svg\");\n const esc = (s: string) =>\n s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n\n const marketingStyles = hasMarketing\n ? `\n body.has-marketing { padding: 0; position: relative; overflow-x: hidden; }\n #starfield {\n position: fixed;\n inset: 0;\n width: 100%;\n height: 100%;\n opacity: 0.35;\n pointer-events: none;\n z-index: 0;\n }\n @media (prefers-reduced-motion: reduce) {\n #starfield { opacity: 0.18; }\n }\n .split {\n position: relative;\n z-index: 1;\n display: flex;\n min-height: 100vh;\n width: 100%;\n max-width: 1100px;\n margin: 0 auto;\n }\n .marketing-panel {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n padding: 3rem 3.5rem;\n }\n .marketing-content { max-width: 480px; }\n .app-name {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n font-size: 2rem;\n font-weight: 700;\n color: #fff;\n margin-bottom: 0.625rem;\n letter-spacing: -0.02em;\n }\n .app-name img.brand-mark {\n height: 2.21375rem;\n width: auto;\n display: block;\n flex-shrink: 0;\n }\n .app-tagline {\n font-size: 1.25rem;\n color: #a1a1aa;\n line-height: 1.6;\n margin-bottom: 2rem;\n }\n .app-desc {\n font-size: 1rem;\n color: #71717a;\n line-height: 1.6;\n margin-bottom: 2rem;\n }\n .feature-list {\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 0.875rem;\n }\n .feature-list li {\n display: flex;\n align-items: flex-start;\n gap: 0.625rem;\n font-size: 1rem;\n color: #a1a1aa;\n line-height: 1.5;\n }\n .feature-list li::before {\n content: '';\n flex-shrink: 0;\n width: 8px;\n height: 8px;\n margin-top: 6px;\n border-radius: 50%;\n background: #3f3f46;\n border: 1px solid #52525b;\n }\n .oss-link {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n margin-top: 2rem;\n font-size: 0.8125rem;\n color: #71717a;\n text-decoration: none;\n }\n .oss-link:hover { color: #a1a1aa; }\n .oss-link svg { width: 15px; height: 15px; flex-shrink: 0; }\n .form-panel {\n flex: 0 0 440px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n }\n .form-panel .card { max-width: 400px; }\n .form-panel .local-note { max-width: 400px; }\n @media (max-width: 900px) {\n .split { flex-direction: column; min-height: auto; }\n .marketing-panel { padding: 2rem 1.5rem 1.5rem; }\n .app-name { font-size: 1.375rem; }\n .app-name img.brand-mark { height: 1.58125rem; }\n .app-tagline { font-size: 1rem; margin-bottom: 1rem; }\n .app-desc { margin-bottom: 1rem; }\n .feature-list { gap: 0.5rem; }\n .form-panel { flex: none; padding: 1.5rem 1rem; }\n }\n`\n : \"\";\n\n const marketingPanelHtml = hasMarketing\n ? `<canvas id=\"starfield\"></canvas>\n<div class=\"split\">\n <div class=\"marketing-panel\">\n <div class=\"marketing-content\">\n <h2 class=\"app-name\">\n <img class=\"brand-mark\" src=\"${esc(brandMarkSrc)}\" alt=\"\" aria-hidden=\"true\" />\n <span>${esc(marketing!.appName)}</span>\n </h2>\n <p class=\"app-tagline\">${esc(marketing!.tagline)}</p>\n${marketing!.description ? ` <p class=\"app-desc\">${esc(marketing!.description)}</p>\\n` : \"\"}${\n marketing!.features?.length\n ? ` <ul class=\"feature-list\">\\n${marketing!.features.map((f) => ` <li>${esc(f)}</li>`).join(\"\\n\")}\\n </ul>\\n`\n : \"\"\n } <a class=\"oss-link\" href=\"https://github.com/BuilderIO/agent-native\" target=\"_blank\" rel=\"noreferrer\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 00-1.3-3.2 4.2 4.2 0 00-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 00-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 00-.1 3.2A4.6 4.6 0 004 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21\"/></svg>\n Open source\n </a>\n </div>\n </div>\n <div class=\"form-panel\">`\n : \"\";\n\n const marketingCloseHtml = hasMarketing ? `\\n </div>\\n</div>` : \"\";\n\n const starfieldScript = hasMarketing\n ? `\n (function initStarfield() {\n var canvas = document.getElementById('starfield');\n if (!canvas) return;\n var gl = canvas.getContext('webgl', { alpha: false, antialias: false });\n if (!gl) return;\n\n var vs = gl.createShader(gl.VERTEX_SHADER);\n gl.shaderSource(vs, 'attribute vec2 position;void main(){gl_Position=vec4(position,0.0,1.0);}');\n gl.compileShader(vs);\n\n var fs = gl.createShader(gl.FRAGMENT_SHADER);\n gl.shaderSource(fs, [\n 'precision highp float;',\n 'uniform float iTime;uniform vec2 iResolution;',\n '#define S(a,b,t) smoothstep(a,b,t)',\n '#define NUM_LAYERS 4.',\n 'float N21(vec2 p){vec3 a=fract(vec3(p.xyx)*vec3(213.897,653.453,253.098));a+=dot(a,a.yzx+79.76);return fract((a.x+a.y)*a.z);}',\n 'vec2 GetPos(vec2 id,vec2 offs,float t){float n=N21(id+offs);float n1=fract(n*10.);float n2=fract(n*100.);float a=t+n;return offs+vec2(sin(a*n1),cos(a*n2))*.4;}',\n 'float df_line(vec2 a,vec2 b,vec2 p){vec2 pa=p-a,ba=b-a;float h=clamp(dot(pa,ba)/dot(ba,ba),0.,1.);return length(pa-ba*h);}',\n 'float line(vec2 a,vec2 b,vec2 uv){float r1=.025;float r2=.006;float d=df_line(a,b,uv);float d2=length(a-b);float fade=S(1.5,.5,d2);fade+=S(.05,.02,abs(d2-.75));return S(r1,r2,d)*fade;}',\n 'float NetLayer(vec2 st,float n,float t){',\n ' vec2 id=floor(st)+n;st=fract(st)-.5;',\n ' vec2 p0=GetPos(id,vec2(-1,-1),t);vec2 p1=GetPos(id,vec2(0,-1),t);vec2 p2=GetPos(id,vec2(1,-1),t);',\n ' vec2 p3=GetPos(id,vec2(-1,0),t);vec2 p4=GetPos(id,vec2(0,0),t);vec2 p5=GetPos(id,vec2(1,0),t);',\n ' vec2 p6=GetPos(id,vec2(-1,1),t);vec2 p7=GetPos(id,vec2(0,1),t);vec2 p8=GetPos(id,vec2(1,1),t);',\n ' float m=0.;float sparkle=0.;float d;float s;float pulse;',\n ' m+=line(p4,p0,st);d=length(st-p0);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p0.x)+fract(p0.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p1,st);d=length(st-p1);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p1.x)+fract(p1.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p2,st);d=length(st-p2);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p2.x)+fract(p2.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p3,st);d=length(st-p3);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p3.x)+fract(p3.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p4,st);d=length(st-p4);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p4.x)+fract(p4.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p5,st);d=length(st-p5);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p5.x)+fract(p5.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p6,st);d=length(st-p6);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p6.x)+fract(p6.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p7,st);d=length(st-p7);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p7.x)+fract(p7.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p4,p8,st);d=length(st-p8);s=(.005/(d*d));s*=S(1.,.7,d);pulse=sin((fract(p8.x)+fract(p8.y)+t)*5.)*.4+.6;pulse=pow(pulse,20.);sparkle+=s*pulse;',\n ' m+=line(p1,p3,st);m+=line(p1,p5,st);m+=line(p7,p5,st);m+=line(p7,p3,st);',\n ' float sPhase=(sin(t+n)+sin(t*.1))*.25+.5;sPhase+=pow(sin(t*.1)*.5+.5,50.)*5.;m+=sparkle*sPhase;',\n ' return m;',\n '}',\n 'void mainImage(out vec4 fragColor,in vec2 fragCoord){',\n ' vec2 uv=(fragCoord-iResolution.xy*.5)/iResolution.y;',\n ' float t=iTime*.03;float s=sin(t);float c=cos(t);mat2 rot=mat2(c,-s,s,c);vec2 st=uv*rot;',\n ' float m=0.;',\n ' for(float i=0.;i<1.;i+=1./NUM_LAYERS){float z=fract(t+i);float size=mix(15.,1.,z);float fade=S(0.,.6,z)*S(1.,.8,z);m+=fade*NetLayer(st*size,i,iTime*0.3);}',\n ' vec3 col=vec3(0.35)*m;col*=1.-dot(uv,uv);',\n ' float tt=min(iTime,5.0);col*=S(0.,20.,tt);',\n ' col=clamp(col,0.,1.);fragColor=vec4(col,1.);',\n '}',\n 'void main(){mainImage(gl_FragColor,gl_FragCoord.xy);}'\n ].join('\\\\n'));\n gl.compileShader(fs);\n\n var prog = gl.createProgram();\n gl.attachShader(prog, vs);\n gl.attachShader(prog, fs);\n gl.linkProgram(prog);\n gl.useProgram(prog);\n\n var buf = gl.createBuffer();\n gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1,1,-1,-1,1,-1,1,1,-1,1,1]), gl.STATIC_DRAW);\n var pos = gl.getAttribLocation(prog, 'position');\n gl.enableVertexAttribArray(pos);\n gl.vertexAttribPointer(pos, 2, gl.FLOAT, false, 0, 0);\n\n var uTime = gl.getUniformLocation(prog, 'iTime');\n var uRes = gl.getUniformLocation(prog, 'iResolution');\n var reducedMotionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;\n var reducedMotion = reducedMotionQuery ? reducedMotionQuery.matches : false;\n\n function resize() {\n var w = window.innerWidth, h = window.innerHeight;\n var dpr = Math.min(window.devicePixelRatio, 1.5);\n canvas.width = w * dpr; canvas.height = h * dpr;\n gl.viewport(0, 0, canvas.width, canvas.height);\n }\n resize();\n window.addEventListener('resize', resize);\n\n var start = performance.now(), last = 0, raf = 0, reducedMotionStaticTime = 20;\n function draw(timeSeconds) {\n gl.uniform1f(uTime, timeSeconds);\n gl.uniform2f(uRes, canvas.width, canvas.height);\n gl.drawArrays(gl.TRIANGLES, 0, 6);\n }\n function render(now) {\n if (reducedMotion) {\n raf = 0;\n return;\n }\n raf = requestAnimationFrame(render);\n if (now - last < 33) return;\n last = now;\n draw((now - start) * 0.001);\n }\n function startAnimation() {\n if (!raf) raf = requestAnimationFrame(render);\n }\n function stopAnimation() {\n if (raf) {\n cancelAnimationFrame(raf);\n raf = 0;\n }\n }\n function onReducedMotionChange() {\n reducedMotion = reducedMotionQuery ? reducedMotionQuery.matches : false;\n if (reducedMotion) {\n stopAnimation();\n last = 0;\n draw(reducedMotionStaticTime);\n } else {\n startAnimation();\n }\n }\n draw(reducedMotion ? reducedMotionStaticTime : 0);\n if (reducedMotionQuery) {\n if (reducedMotionQuery.addEventListener) {\n reducedMotionQuery.addEventListener('change', onReducedMotionChange);\n } else {\n reducedMotionQuery.addListener(onReducedMotionChange);\n }\n }\n if (!reducedMotion) startAnimation();\n })();`\n : \"\";\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n<title>${hasMarketing ? esc(marketing!.appName) + \" — Sign in\" : \"Welcome\"}</title>\n${\n hasMarketing\n ? `<meta name=\"description\" content=\"${esc(marketing!.tagline)}\">\n<meta property=\"og:title\" content=\"${esc(marketing!.appName)}\">\n<meta property=\"og:description\" content=\"${esc(marketing!.tagline)}\">`\n : \"\"\n}\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n padding: 1rem;\n }\n .card {\n width: 100%;\n max-width: 400px;\n padding: 2rem;\n background: #141414;\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 12px;\n }\n h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.25rem; color: #fff; }\n .subtitle { font-size: 0.8125rem; color: #888; margin-bottom: 1.5rem; }\n .tabs {\n display: inline-flex;\n width: 100%;\n padding: 4px;\n margin-bottom: 1.5rem;\n background: rgba(255,255,255,0.06);\n border-radius: 8px;\n }\n .tab {\n flex: 1;\n padding: 0.5rem 0.75rem;\n background: none;\n border: none;\n color: #888;\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n border-radius: 6px;\n }\n .tab.active {\n background: #1e1e1e;\n color: #fff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.3);\n }\n .tab:hover:not(.active) { color: #bbb; }\n .form { display: none; }\n .form.active { display: block; }\n .card.verifying .tabs,\n .card.verifying #google-btn,\n .card.verifying #google-err,\n .card.verifying #auth-divider,\n .card.verifying #upgrade-note {\n display: none;\n }\n label { display: block; font-size: 0.8125rem; color: #888; margin-bottom: 0.375rem; }\n input {\n width: 100%;\n padding: 0.5rem 0.75rem;\n background: transparent;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 6px;\n color: #e5e5e5;\n font-size: 0.875rem;\n outline: none;\n margin-bottom: 0.875rem;\n }\n input:focus { border-color: rgba(255,255,255,0.3); box-shadow: 0 0 0 1px rgba(255,255,255,0.1); }\n input::placeholder { color: #555; }\n button[type=\"submit\"], .btn-primary {\n width: 100%;\n margin-top: 0.25rem;\n padding: 0.5rem;\n background: #fff;\n color: #000;\n border: none;\n border-radius: 6px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n }\n button[type=\"submit\"]:hover, .btn-primary:hover { background: #e5e5e5; }\n button[type=\"submit\"]:disabled { opacity: 0.5; cursor: not-allowed; }\n .btn-secondary {\n width: 100%;\n margin-top: 0.75rem;\n padding: 0.5rem;\n background: transparent;\n color: #888;\n border: 1px solid rgba(255,255,255,0.1);\n border-radius: 6px;\n font-size: 0.8125rem;\n cursor: pointer;\n }\n .btn-secondary:hover { color: #bbb; border-color: rgba(255,255,255,0.2); }\n .msg { margin-top: 0.75rem; font-size: 0.8125rem; display: none; }\n .msg.error { color: #f87171; }\n .msg.success { color: #4ade80; }\n .msg.show { display: block; }\n .step-progress {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 0.5rem;\n margin-bottom: 1.25rem;\n }\n .progress-step {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.375rem;\n color: #666;\n font-size: 0.6875rem;\n line-height: 1.2;\n text-align: center;\n }\n .progress-step::before {\n content: '';\n position: absolute;\n top: 11px;\n left: calc(-50% + 16px);\n width: calc(100% - 32px);\n height: 1px;\n background: rgba(255,255,255,0.1);\n }\n .progress-step:first-child::before { display: none; }\n .progress-step span {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 999px;\n border: 1px solid rgba(255,255,255,0.14);\n background: #151515;\n color: #777;\n font-size: 0.6875rem;\n font-weight: 600;\n }\n .progress-step strong { font-weight: 500; }\n .progress-step.complete,\n .progress-step.current { color: #e5e5e5; }\n .progress-step.complete span {\n background: #d9f99d;\n border-color: #d9f99d;\n color: #111;\n }\n .progress-step.current span {\n background: #fff;\n border-color: #fff;\n color: #000;\n box-shadow: 0 0 0 4px rgba(255,255,255,0.08);\n }\n .verification-panel {\n padding: 1rem;\n margin-bottom: 0.875rem;\n background: rgba(255,255,255,0.04);\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 8px;\n }\n .verification-kicker {\n margin-bottom: 0.5rem;\n color: #bef264;\n font-size: 0.75rem;\n font-weight: 500;\n }\n .verification-copy {\n color: #d4d4d8;\n font-size: 0.875rem;\n line-height: 1.55;\n }\n .verification-copy strong {\n color: #fff;\n font-weight: 600;\n word-break: break-word;\n }\n .verification-note {\n margin-top: 0.75rem;\n color: #71717a;\n font-size: 0.75rem;\n line-height: 1.45;\n }\n .inline-actions {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n margin-top: 0.75rem;\n }\n .link-button {\n padding: 0.25rem 0;\n background: none;\n border: none;\n color: #888;\n cursor: pointer;\n font-size: 0.75rem;\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n .link-button:hover { color: #bbb; }\n .link-button:disabled { cursor: wait; opacity: 0.5; }\n .divider {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin: 1.25rem 0;\n font-size: 0.75rem;\n color: #555;\n }\n .divider::before, .divider::after {\n content: '';\n flex: 1;\n height: 1px;\n background: rgba(255,255,255,0.08);\n }\n .upgrade-note {\n margin-bottom: 1rem;\n padding: 0.75rem;\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 8px;\n background: rgba(255,255,255,0.03);\n font-size: 0.75rem;\n line-height: 1.5;\n color: #a1a1aa;\n display: none;\n }\n .upgrade-note.show { display: block; }\n .btn-google {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.625rem;\n padding: 0.5rem;\n background: #fff;\n color: #000;\n border: none;\n border-radius: 6px;\n font-size: 0.875rem;\n font-weight: 500;\n cursor: pointer;\n }\n .btn-google:hover { background: #e5e5e5; }\n .btn-google:disabled { opacity: 0.5; cursor: wait; }\n .btn-google svg { width: 18px; height: 18px; flex-shrink: 0; }\n .google-error { margin-top: 0.5rem; font-size: 0.8125rem; color: #f87171; display: none; }\n .google-error.show { display: block; }\n .local-note {\n display: none;\n max-width: 400px;\n width: 100%;\n margin-top: 1rem;\n padding: 0.625rem 0.875rem;\n font-size: 0.6875rem;\n line-height: 1.5;\n color: #666;\n border: 1px dashed rgba(255,255,255,0.08);\n border-radius: 8px;\n text-align: center;\n }\n .local-note.show { display: block; }\n .local-note strong { color: #999; font-weight: 500; }\n .local-note a { color: #888; text-decoration: none; }\n .local-note a:hover { color: #bbb; }\n${marketingStyles}\n</style>\n</head>\n<body${hasMarketing ? ' class=\"has-marketing\"' : \"\"}>\n${marketingPanelHtml}\n<div class=\"card\">\n <h1 id=\"heading\">${googleOnly ? \"Sign in\" : \"Welcome\"}</h1>\n <p class=\"subtitle\" id=\"subtitle\">${googleOnly ? \"Use your workspace Google account to continue\" : \"Create an account to get started\"}</p>\n <p\n class=\"upgrade-note\"\n id=\"upgrade-note\"\n data-upgrade-copy=\"Continue signing in to attach this app to your account and migrate local data.\"\n ></p>\n\n${\n showGoogle\n ? `\n <button class=\"btn-google\" id=\"google-btn\" onclick=\"signInWithGoogle()\">\n <svg viewBox=\"0 0 24 24\"><path fill=\"#4285F4\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z\"/><path fill=\"#34A853\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/><path fill=\"#FBBC05\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/><path fill=\"#EA4335\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/></svg>\n Sign in with Google\n </button>\n <p class=\"google-error\" id=\"google-err\"></p>\n${googleOnly ? \"\" : `\\n <div class=\"divider\" id=\"auth-divider\">or</div>\\n`}\n`\n : googleOnly\n ? `\n <p style=\"color:#f87171;font-size:0.875rem;text-align:center;padding:1rem 0\">\n Google sign-in is not configured. Set <code>GOOGLE_CLIENT_ID</code> and\n <code>GOOGLE_CLIENT_SECRET</code> environment variables to enable login.\n </p>\n`\n : \"\"\n}\n${\n googleOnly\n ? \"\"\n : ` <div class=\"tabs\">\n <button class=\"tab\" data-tab=\"signup\">Create account</button>\n <button class=\"tab\" data-tab=\"login\">Sign in</button>\n </div>\n\n <form id=\"signup-form\" class=\"form\">\n <label for=\"s-email\">Email</label>\n <input id=\"s-email\" type=\"email\" autocomplete=\"email\" autofocus placeholder=\"you@example.com\" required />\n <label for=\"s-pass\">Password</label>\n <input id=\"s-pass\" type=\"password\" autocomplete=\"new-password\" placeholder=\"At least 8 characters\" required minlength=\"8\" />\n <label for=\"s-pass2\">Confirm password</label>\n <input id=\"s-pass2\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Confirm password\" required minlength=\"8\" />\n <button type=\"submit\">Create account</button>\n <p class=\"msg\" id=\"s-msg\"></p>\n </form>\n\n <div id=\"verification-step\" class=\"form verification-step\" aria-live=\"polite\">\n <div class=\"step-progress\" aria-label=\"Signup progress\">\n <div class=\"progress-step complete\"><span>1</span><strong>Account</strong></div>\n <div class=\"progress-step current\"><span>2</span><strong>Verify</strong></div>\n <div class=\"progress-step\"><span>3</span><strong>Start</strong></div>\n </div>\n <div class=\"verification-panel\">\n <p class=\"verification-kicker\">Verification email sent</p>\n <p class=\"verification-copy\">We sent a secure link to <strong id=\"verify-email\"></strong>. Click it, return here, and this app will finish signing you in automatically.</p>\n <p class=\"verification-note\">You can keep this tab open. If it has not refreshed after you come back, use Continue.</p>\n </div>\n <button type=\"button\" class=\"btn-primary\" id=\"verify-continue\">Continue</button>\n <div class=\"inline-actions\">\n <button type=\"button\" class=\"link-button\" id=\"resend-verification\">Resend email</button>\n <button type=\"button\" class=\"link-button\" id=\"back-to-signup\">Back</button>\n </div>\n <p class=\"msg\" id=\"verify-msg\"></p>\n </div>\n\n <form id=\"login-form\" class=\"form\">\n <label for=\"l-email\">Email</label>\n <input id=\"l-email\" type=\"email\" autocomplete=\"email\" placeholder=\"you@example.com\" required />\n <label for=\"l-pass\">Password</label>\n <input id=\"l-pass\" type=\"password\" autocomplete=\"current-password\" placeholder=\"Enter password\" required />\n <button type=\"submit\">Sign in</button>\n <p class=\"msg error\" id=\"l-msg\"></p>\n <p style=\"margin-top:0.75rem;font-size:0.75rem;text-align:right\">\n <a href=\"#\" id=\"forgot-link\" style=\"color:#888;text-decoration:underline;text-underline-offset:2px\">Forgot password?</a>\n </p>\n </form>\n\n <form id=\"forgot-form\" class=\"form\">\n <label for=\"f-email\">Email</label>\n <input id=\"f-email\" type=\"email\" autocomplete=\"email\" placeholder=\"you@example.com\" required />\n <button type=\"submit\">Send reset link</button>\n <p class=\"msg\" id=\"f-msg\"></p>\n <p style=\"margin-top:0.75rem;font-size:0.75rem;text-align:center\">\n <a href=\"#\" id=\"back-to-login\" style=\"color:#888;text-decoration:underline;text-underline-offset:2px\">Back to sign in</a>\n </p>\n </form>`\n}\n</div>\n<p class=\"local-note\" id=\"local-note\">\n Your account is stored in this app's own DB (<strong>${getConnectionLabel()}</strong>), not a third-party service.\n</p>${marketingCloseHtml}\n<script>\n function __anBasePath() {\n var configured = ${JSON.stringify(appBasePath)};\n if (configured) return configured;\n var marker = '/_agent-native';\n var idx = window.location.pathname.indexOf(marker);\n return idx > 0 ? window.location.pathname.slice(0, idx) : '';\n }\n function __anPath(path) {\n return __anBasePath() + path;\n }\n function __anGetReturnPath() {\n try {\n var inner = new URLSearchParams(window.location.search).get('return');\n if (inner) return inner;\n } catch(e) {}\n return window.location.pathname + window.location.search;\n }\n (function revealLocalNote() {\n var h = location.hostname;\n if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h.endsWith('.local')) {\n var n = document.getElementById('local-note');\n if (n) n.classList.add('show');\n }\n })();\n (function revealUpgradeNote() {\n var shouldShow = false;\n try {\n var params = new URLSearchParams(location.search);\n shouldShow = params.get('signin') === '1' || params.get('upgrade-from-local') === '1';\n } catch(e) {}\n if (!shouldShow) {\n try { shouldShow = localStorage.getItem('an_migrate_from_local') === '1'; } catch(e) {}\n }\n if (!shouldShow) return;\n var n = document.getElementById('upgrade-note');\n if (!n) return;\n n.textContent = n.getAttribute('data-upgrade-copy') || 'Continue signing in to migrate local data.';\n n.classList.add('show');\n })();\n${\n googleOnly\n ? \"\"\n : ` var TAB_STORAGE_KEY = 'an.onboarding.tab';\n var tabs = document.querySelectorAll('.tab');\n var forms = document.querySelectorAll('.form');\n var subtitles = { signup: 'Create an account to get started', login: 'Sign in to your account' };\n var headings = { signup: 'Welcome', login: 'Welcome back' };\n var pendingSignupEmail = '';\n var pendingSignupPassword = '';\n var verificationCheckInFlight = false;\n function setActiveTab(name, opts) {\n if (name !== 'signup' && name !== 'login') return;\n var form = document.getElementById(name + '-form');\n if (!form) return;\n var card = document.querySelector('.card');\n if (card) card.classList.remove('verifying');\n tabs.forEach(function(x) { x.classList.remove('active'); });\n forms.forEach(function(x) { x.classList.remove('active'); });\n var btn = document.querySelector('.tab[data-tab=\"' + name + '\"]');\n if (btn) btn.classList.add('active');\n form.classList.add('active');\n var sub = document.getElementById('subtitle');\n if (sub && subtitles[name]) sub.textContent = subtitles[name];\n var heading = document.getElementById('heading');\n if (heading && headings[name]) heading.textContent = headings[name];\n if (opts && opts.persist) {\n try { localStorage.setItem(TAB_STORAGE_KEY, name); } catch (e) {}\n }\n }\n function showVerificationStep(email, password) {\n pendingSignupEmail = email || '';\n pendingSignupPassword = password || '';\n tabs.forEach(function(x) { x.classList.remove('active'); });\n forms.forEach(function(x) { x.classList.remove('active'); });\n var card = document.querySelector('.card');\n if (card) card.classList.add('verifying');\n var step = document.getElementById('verification-step');\n if (step) step.classList.add('active');\n var emailNode = document.getElementById('verify-email');\n if (emailNode) emailNode.textContent = pendingSignupEmail;\n var heading = document.getElementById('heading');\n if (heading) heading.textContent = 'Check your email';\n var sub = document.getElementById('subtitle');\n if (sub) sub.textContent = 'Finish creating your account';\n var msg = document.getElementById('verify-msg');\n if (msg) {\n msg.classList.remove('show', 'error', 'success');\n msg.textContent = '';\n }\n try { localStorage.setItem(TAB_STORAGE_KEY, 'signup'); } catch (e) {}\n }\n function getVerificationMessageNode() {\n var verifyStep = document.getElementById('verification-step');\n if (verifyStep && verifyStep.classList.contains('active')) {\n return document.getElementById('verify-msg');\n }\n return document.getElementById('l-msg') || document.getElementById('verify-msg');\n }\n function isVerificationStepActive() {\n var verifyStep = document.getElementById('verification-step');\n return !!(verifyStep && verifyStep.classList.contains('active'));\n }\n function getPendingSignupEmail() {\n var signupEmail = document.getElementById('s-email');\n var loginEmail = document.getElementById('l-email');\n return (pendingSignupEmail || (signupEmail && signupEmail.value) || (loginEmail && loginEmail.value) || '').trim();\n }\n function getPendingSignupPassword() {\n var signupPassword = document.getElementById('s-pass');\n return pendingSignupPassword || (signupPassword && signupPassword.value) || '';\n }\n function movePendingSignupToLogin(message) {\n var email = getPendingSignupEmail();\n setActiveTab('login', { persist: true });\n var loginEmail = document.getElementById('l-email');\n var loginPassword = document.getElementById('l-pass');\n var msg = document.getElementById('l-msg');\n if (loginEmail && email) loginEmail.value = email;\n if (msg) {\n msg.textContent = message || 'Sign in to continue.';\n msg.classList.remove('error');\n msg.classList.add('show', 'success');\n }\n setTimeout(function() { if (loginPassword) loginPassword.focus(); }, 0);\n }\n async function signInWithPendingSignup() {\n var email = getPendingSignupEmail();\n var password = getPendingSignupPassword();\n if (!email || !password) {\n return { ok: false, needsManualSignIn: true };\n }\n var res = await fetch(__anPath('/_agent-native/auth/login'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email, password: password }),\n });\n if (res.ok) {\n window.location.reload();\n return { ok: true };\n }\n var data = await res.json().catch(function() { return {}; });\n var error = (data && (data.error || data.message)) || 'Could not finish sign-in automatically.';\n return {\n ok: false,\n error: error,\n isWaitingForVerification: /not verified|verification/i.test(error),\n };\n }\n async function checkVerificationSession(fallbackText, opts) {\n opts = opts || {};\n if (verificationCheckInFlight) return;\n verificationCheckInFlight = true;\n var msg = getVerificationMessageNode();\n var continueBtn = document.getElementById('verify-continue');\n if (continueBtn && !opts.silent) {\n continueBtn.disabled = true;\n continueBtn.textContent = 'Checking...';\n }\n if (msg && !opts.silent) {\n msg.textContent = 'Checking your verification...';\n msg.classList.remove('error');\n msg.classList.add('show', 'success');\n }\n try {\n var res = await fetch(__anPath('/_agent-native/auth/session'), {\n headers: { 'Accept': 'application/json' },\n });\n var data = await res.json().catch(function() { return {}; });\n if (res.ok && data && data.email && !data.error) {\n window.location.reload();\n return;\n }\n var loginResult = await signInWithPendingSignup();\n if (loginResult.ok) return;\n if (loginResult.needsManualSignIn) {\n if (!opts.silent) {\n movePendingSignupToLogin(fallbackText || 'Enter your password after verifying your email.');\n }\n return;\n }\n if (loginResult.error && !loginResult.isWaitingForVerification) {\n if (!opts.silent) {\n movePendingSignupToLogin('We could not finish sign-in automatically. Sign in to continue.');\n }\n return;\n }\n if (msg && !opts.silent) {\n msg.textContent = fallbackText || 'Still waiting on verification. Click the link in your email, then try Continue again.';\n msg.classList.remove('success');\n msg.classList.add('show', 'error');\n }\n } catch (err) {\n if (msg && !opts.silent) {\n msg.textContent = 'Could not check verification. Please try again.';\n msg.classList.remove('success');\n msg.classList.add('show', 'error');\n }\n } finally {\n verificationCheckInFlight = false;\n if (continueBtn && !opts.silent) {\n continueBtn.disabled = false;\n continueBtn.textContent = 'Continue';\n }\n }\n }\n function maybeCompleteVerificationAfterReturn() {\n if (!isVerificationStepActive()) return;\n checkVerificationSession(null, { silent: true });\n }\n async function resendVerificationEmail() {\n var btn = document.getElementById('resend-verification');\n var msg = document.getElementById('verify-msg');\n var email = pendingSignupEmail || document.getElementById('s-email').value;\n if (!email) return;\n var original = btn ? btn.textContent : '';\n if (btn) {\n btn.disabled = true;\n btn.textContent = 'Sending...';\n }\n if (msg) msg.classList.remove('show', 'error', 'success');\n try {\n var res = await fetch(__anPath('/_agent-native/auth/ba/send-verification-email'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email, callbackURL: __anGetReturnPath() }),\n });\n if (res.ok) {\n if (msg) {\n msg.textContent = 'Sent a fresh verification link.';\n msg.classList.add('show', 'success');\n }\n if (btn) btn.textContent = 'Sent';\n setTimeout(function() {\n if (btn) {\n btn.disabled = false;\n btn.textContent = original;\n }\n }, 1600);\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n if (msg) {\n msg.textContent = (data && (data.message || data.error)) || 'Could not resend the verification email.';\n msg.classList.add('show', 'error');\n }\n if (btn) {\n btn.disabled = false;\n btn.textContent = original;\n }\n } catch (err) {\n if (msg) {\n msg.textContent = 'Network error. Please try again.';\n msg.classList.add('show', 'error');\n }\n if (btn) {\n btn.disabled = false;\n btn.textContent = original;\n }\n }\n }\n (function initActiveTab() {\n var initial = 'signup';\n try {\n var params = new URLSearchParams(location.search);\n var qp = params.get('tab');\n var path = location.pathname;\n while (path.length > 1 && path.charAt(path.length - 1) === '/') path = path.slice(0, -1);\n if (qp === 'login' || qp === 'signup') {\n initial = qp;\n } else if (params.has('verified')) {\n initial = 'login';\n } else if (path === '/login' || path.endsWith('/login')) {\n initial = 'login';\n } else if (path === '/signup' || path.endsWith('/signup')) {\n initial = 'signup';\n } else {\n var stored = localStorage.getItem(TAB_STORAGE_KEY);\n if (stored === 'login' || stored === 'signup') initial = stored;\n }\n } catch (e) {}\n setActiveTab(initial, { persist: false });\n try {\n if (new URLSearchParams(location.search).has('verified')) {\n var msg = document.getElementById('l-msg');\n if (msg) {\n msg.textContent = 'Email verified. Finishing sign-in...';\n msg.classList.remove('error');\n msg.classList.add('show', 'success');\n }\n checkVerificationSession('Email verified. Sign in to continue.');\n }\n } catch (e) {}\n })();\n tabs.forEach(function(t) { t.addEventListener('click', function() {\n setActiveTab(t.dataset.tab, { persist: true });\n }); });\n\n document.getElementById('signup-form').addEventListener('submit', async function(e) {\n e.preventDefault();\n var form = e.currentTarget;\n var btn = form.querySelector('button[type=\"submit\"]');\n var msg = document.getElementById('s-msg');\n msg.classList.remove('show', 'error', 'success');\n var pass = document.getElementById('s-pass').value;\n var pass2 = document.getElementById('s-pass2').value;\n if (pass !== pass2) {\n msg.textContent = 'Passwords do not match';\n msg.classList.add('show', 'error');\n return;\n }\n var originalLabel = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Creating account…';\n try {\n var email = document.getElementById('s-email').value;\n var res = await fetch(__anPath('/_agent-native/auth/register'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email: email,\n password: pass,\n callbackURL: __anGetReturnPath(),\n }),\n });\n var data = await res.json().catch(function() { return {}; });\n if (res.ok) {\n // If email verification is required, the server won't return a session.\n // Try logging in — if it fails (unverified), show a \"check your email\" message.\n var loginRes = await fetch(__anPath('/_agent-native/auth/login'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email, password: pass }),\n });\n if (loginRes.ok) {\n msg.textContent = 'Account created — signing you in…';\n msg.classList.add('show', 'success');\n window.location.reload();\n return;\n }\n btn.disabled = false;\n btn.textContent = originalLabel;\n showVerificationStep(email, pass);\n return;\n }\n msg.textContent = data.error || 'Registration failed';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = originalLabel;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = originalLabel;\n }\n });\n\n var verifyContinue = document.getElementById('verify-continue');\n if (verifyContinue) verifyContinue.addEventListener('click', function(e) {\n e.preventDefault();\n checkVerificationSession();\n });\n window.addEventListener('focus', maybeCompleteVerificationAfterReturn);\n document.addEventListener('visibilitychange', function() {\n if (document.visibilityState === 'visible') maybeCompleteVerificationAfterReturn();\n });\n var resendBtn = document.getElementById('resend-verification');\n if (resendBtn) resendBtn.addEventListener('click', function(e) {\n e.preventDefault();\n resendVerificationEmail();\n });\n var backToSignup = document.getElementById('back-to-signup');\n if (backToSignup) backToSignup.addEventListener('click', function(e) {\n e.preventDefault();\n setActiveTab('signup', { persist: true });\n var email = document.getElementById('s-email');\n setTimeout(function() { if (email) email.focus(); }, 0);\n });\n\n var forgotLink = document.getElementById('forgot-link');\n var backToLogin = document.getElementById('back-to-login');\n if (forgotLink) forgotLink.addEventListener('click', function(e) {\n e.preventDefault();\n document.getElementById('login-form').classList.remove('active');\n document.getElementById('forgot-form').classList.add('active');\n var sub = document.getElementById('subtitle');\n if (sub) sub.textContent = 'Reset your password';\n var heading = document.getElementById('heading');\n if (heading) heading.textContent = 'Reset password';\n var fEmail = document.getElementById('f-email');\n var lEmail = document.getElementById('l-email');\n if (lEmail && lEmail.value) fEmail.value = lEmail.value;\n setTimeout(function() { fEmail.focus(); }, 0);\n });\n if (backToLogin) backToLogin.addEventListener('click', function(e) {\n e.preventDefault();\n document.getElementById('forgot-form').classList.remove('active');\n document.getElementById('login-form').classList.add('active');\n var sub = document.getElementById('subtitle');\n if (sub) sub.textContent = subtitles.login;\n var heading = document.getElementById('heading');\n if (heading) heading.textContent = headings.login;\n });\n\n var forgotForm = document.getElementById('forgot-form');\n if (forgotForm) forgotForm.addEventListener('submit', async function(e) {\n e.preventDefault();\n var btn = e.currentTarget.querySelector('button[type=\"submit\"]');\n var msg = document.getElementById('f-msg');\n msg.classList.remove('show', 'error', 'success');\n var original = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Sending…';\n try {\n var email = document.getElementById('f-email').value;\n var res = await fetch(__anPath('/_agent-native/auth/ba/request-password-reset'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ email: email }),\n });\n if (res.ok) {\n msg.textContent = 'If that email exists, a reset link is on its way.';\n msg.classList.add('show', 'success');\n btn.textContent = 'Sent';\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n msg.textContent = (data && (data.message || data.error)) || 'Could not send reset email.';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n }\n });\n\n document.getElementById('login-form').addEventListener('submit', async function(e) {\n e.preventDefault();\n var form = e.currentTarget;\n var btn = form.querySelector('button[type=\"submit\"]');\n var msg = document.getElementById('l-msg');\n msg.classList.remove('show', 'success');\n msg.classList.add('error');\n var originalLabel = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Signing in…';\n try {\n var res = await fetch(__anPath('/_agent-native/auth/login'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n email: document.getElementById('l-email').value,\n password: document.getElementById('l-pass').value,\n }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n msg.textContent = data.error || 'Invalid email or password';\n msg.classList.add('show');\n btn.disabled = false;\n btn.textContent = originalLabel;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show');\n btn.disabled = false;\n btn.textContent = originalLabel;\n }\n });\n`\n}\n${\n showGoogle\n ? `\n async function signInWithGoogle() {\n var btn = document.getElementById('google-btn');\n var err = document.getElementById('google-err');\n btn.disabled = true;\n err.classList.remove('show');\n try {\n var ret = __anGetReturnPath();\n var authUrl = __anPath('/_agent-native/google/auth-url') + '?return=' + encodeURIComponent(ret);\n var res = await fetch(authUrl);\n var data = await res.json();\n if (data.url) {\n try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}\n window.location.href = data.url;\n } else {\n err.textContent = data.message || 'Google OAuth is not configured.';\n err.classList.add('show');\n btn.disabled = false;\n }\n } catch (e) {\n err.textContent = 'Failed to connect. Please try again.';\n err.classList.add('show');\n btn.disabled = false;\n }\n }`\n : \"\"\n}\n${starfieldScript}\n</script>\n</body>\n</html>`;\n}\n\n/** @deprecated Use getOnboardingHtml() instead */\nexport const ONBOARDING_HTML = getOnboardingHtml();\n\n/**\n * HTML for the password reset page — shown when the user clicks the link in\n * their reset email. Posts `{ newPassword, token }` to Better Auth's\n * `/reset-password` endpoint, then redirects to the login page.\n */\nexport function getResetPasswordHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n<title>Reset password</title>\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; }\n .card { width: 100%; max-width: 400px; padding: 2rem; background: #141414; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; }\n h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.25rem; color: #fff; }\n .subtitle { font-size: 0.8125rem; color: #888; margin-bottom: 1.5rem; }\n label { display: block; font-size: 0.8125rem; color: #888; margin-bottom: 0.375rem; }\n input { width: 100%; padding: 0.5rem 0.75rem; background: transparent; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; color: #e5e5e5; font-size: 0.875rem; outline: none; margin-bottom: 0.875rem; }\n input:focus { border-color: rgba(255,255,255,0.3); box-shadow: 0 0 0 1px rgba(255,255,255,0.1); }\n input::placeholder { color: #555; }\n button[type=\"submit\"] { width: 100%; margin-top: 0.25rem; padding: 0.5rem; background: #fff; color: #000; border: none; border-radius: 6px; font-size: 0.875rem; font-weight: 500; cursor: pointer; }\n button[type=\"submit\"]:hover { background: #e5e5e5; }\n button[type=\"submit\"]:disabled { opacity: 0.5; cursor: not-allowed; }\n .msg { margin-top: 0.75rem; font-size: 0.8125rem; display: none; }\n .msg.error { color: #f87171; }\n .msg.success { color: #4ade80; }\n .msg.show { display: block; }\n .back { display: inline-block; margin-top: 1rem; font-size: 0.75rem; color: #888; text-decoration: none; }\n .back:hover { color: #bbb; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <h1>Choose a new password</h1>\n <p class=\"subtitle\">Set a new password for your account.</p>\n <form id=\"reset-form\">\n <label for=\"p1\">New password</label>\n <input id=\"p1\" type=\"password\" autocomplete=\"new-password\" autofocus placeholder=\"At least 8 characters\" required minlength=\"8\" />\n <label for=\"p2\">Confirm password</label>\n <input id=\"p2\" type=\"password\" autocomplete=\"new-password\" placeholder=\"Confirm password\" required minlength=\"8\" />\n <button type=\"submit\">Save new password</button>\n <p class=\"msg\" id=\"msg\"></p>\n </form>\n <a class=\"back\" id=\"back-link\" href=\"/\">Back to sign in</a>\n</div>\n<script>\n (function() {\n // Derive the app's base path so apps mounted under a prefix\n // (e.g. /mail, /calendar) get sent home instead of to the root domain.\n var RESET_PATH = '/_agent-native/auth/reset';\n var pathname = window.location.pathname;\n var idx = pathname.indexOf(RESET_PATH);\n var basePath = (idx >= 0 ? pathname.slice(0, idx) : '') || '';\n var homeHref = basePath + '/';\n var backLink = document.getElementById('back-link');\n if (backLink) backLink.setAttribute('href', homeHref);\n var params = new URLSearchParams(location.search);\n var token = params.get('token') || '';\n var msg = document.getElementById('msg');\n if (!token) {\n msg.textContent = 'Missing or invalid reset token. Request a new reset link.';\n msg.classList.add('show', 'error');\n document.getElementById('reset-form').style.display = 'none';\n return;\n }\n document.getElementById('reset-form').addEventListener('submit', async function(e) {\n e.preventDefault();\n var btn = e.currentTarget.querySelector('button[type=\"submit\"]');\n var p1 = document.getElementById('p1').value;\n var p2 = document.getElementById('p2').value;\n msg.classList.remove('show', 'error', 'success');\n if (p1 !== p2) {\n msg.textContent = 'Passwords do not match';\n msg.classList.add('show', 'error');\n return;\n }\n var original = btn.textContent;\n btn.disabled = true;\n btn.textContent = 'Saving…';\n try {\n var res = await fetch(basePath + '/_agent-native/auth/ba/reset-password', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ newPassword: p1, token: token }),\n });\n if (res.ok) {\n msg.textContent = 'Password updated — redirecting to sign in…';\n msg.classList.add('show', 'success');\n setTimeout(function() { window.location.href = homeHref; }, 1200);\n return;\n }\n var data = await res.json().catch(function() { return {}; });\n msg.textContent = (data && (data.message || data.error)) || 'Reset failed. The link may have expired — request a new one.';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n } catch (err) {\n msg.textContent = 'Network error — please try again';\n msg.classList.add('show', 'error');\n btn.disabled = false;\n btn.textContent = original;\n }\n });\n })();\n</script>\n</body>\n</html>`;\n}\n"]}
@@ -54,9 +54,25 @@ export interface RequestContext {
54
54
  */
55
55
  run?: RequestRunContext;
56
56
  }
57
+ type RequestContextObserver = (ctx: RequestContext) => void;
58
+ /**
59
+ * Register a callback fired every time `runWithRequestContext` enters a new
60
+ * scope. The hook runs INSIDE the AsyncLocalStorage scope, so observability
61
+ * helpers that read the current isolation scope (e.g. Sentry) attach to the
62
+ * right per-request context.
63
+ *
64
+ * Returned function unregisters the observer. Observers must never throw —
65
+ * any error is swallowed so a misbehaving observer can't break the request
66
+ * path.
67
+ */
68
+ export declare function addRequestContextObserver(observer: RequestContextObserver): () => void;
57
69
  /**
58
70
  * Run a callback within a per-request context. The context is available to all
59
71
  * async operations spawned from `fn` via `getRequestUserEmail()` / `getRequestOrgId()`.
72
+ *
73
+ * Any registered `addRequestContextObserver` callbacks fire inside the new
74
+ * scope before `fn` runs, so observability code can pin user/org info onto
75
+ * isolation-scoped backends (Sentry, OpenTelemetry, etc.).
60
76
  */
61
77
  export declare function runWithRequestContext<T>(ctx: RequestContext, fn: () => T | Promise<T>): T | Promise<T>;
62
78
  /**
@@ -137,4 +153,5 @@ export declare function getRequestRunContext(): RequestRunContext | undefined;
137
153
  * active request store (caller should not be invoking this outside ALS).
138
154
  */
139
155
  export declare function ensureRequestRunContext(): RequestRunContext | undefined;
156
+ export {};
140
157
  //# sourceMappingURL=request-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,OAAO,0BAA0B,EAAE,WAAW,CAAC;IACxD,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,OAAO,0BAA0B,EAAE,eAAe,CAAC;QAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF;;;OAGG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAYD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,GAAG,EAAE,cAAc,EACnB,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAEhB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAIxD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAIpD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAIvD;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED,wBAAgB,4BAA4B,IACxC,WAAW,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,GAC1C,SAAS,CAEZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,IAAI;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,GAAG,IAAI,CAIP;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,SAAS,CAIpE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,GAAG,SAAS,CAKvE"}
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,OAAO,0BAA0B,EAAE,WAAW,CAAC;IACxD,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,OAAO,0BAA0B,EAAE,eAAe,CAAC;QAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF;;;OAGG;IACH,GAAG,CAAC,EAAE,iBAAiB,CAAC;CACzB;AAID,KAAK,sBAAsB,GAAG,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;AAe5D;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,sBAAsB,GAC/B,MAAM,IAAI,CAMZ;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,GAAG,EAAE,cAAc,EACnB,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvB,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAahB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,GAAG,SAAS,CAIxD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAIpD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAIvD;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED,wBAAgB,4BAA4B,IACxC,WAAW,CAAC,cAAc,CAAC,aAAa,CAAC,CAAC,GAC1C,SAAS,CAEZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,IAAI;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,GAAG,IAAI,CAIP;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,SAAS,CAIpE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,GAAG,SAAS,CAKvE"}
@@ -17,17 +17,56 @@
17
17
  */
18
18
  import { AsyncLocalStorage } from "node:async_hooks";
19
19
  const GLOBAL_KEY = "__agentNativeRequestContextAls";
20
+ const OBSERVERS_KEY = "__agentNativeRequestContextObservers";
20
21
  const globalRef = globalThis;
21
22
  if (!globalRef[GLOBAL_KEY]) {
22
23
  globalRef[GLOBAL_KEY] = new AsyncLocalStorage();
23
24
  }
25
+ if (!globalRef[OBSERVERS_KEY]) {
26
+ globalRef[OBSERVERS_KEY] = [];
27
+ }
24
28
  const als = globalRef[GLOBAL_KEY];
29
+ const observers = globalRef[OBSERVERS_KEY];
30
+ /**
31
+ * Register a callback fired every time `runWithRequestContext` enters a new
32
+ * scope. The hook runs INSIDE the AsyncLocalStorage scope, so observability
33
+ * helpers that read the current isolation scope (e.g. Sentry) attach to the
34
+ * right per-request context.
35
+ *
36
+ * Returned function unregisters the observer. Observers must never throw —
37
+ * any error is swallowed so a misbehaving observer can't break the request
38
+ * path.
39
+ */
40
+ export function addRequestContextObserver(observer) {
41
+ observers.push(observer);
42
+ return () => {
43
+ const i = observers.indexOf(observer);
44
+ if (i !== -1)
45
+ observers.splice(i, 1);
46
+ };
47
+ }
25
48
  /**
26
49
  * Run a callback within a per-request context. The context is available to all
27
50
  * async operations spawned from `fn` via `getRequestUserEmail()` / `getRequestOrgId()`.
51
+ *
52
+ * Any registered `addRequestContextObserver` callbacks fire inside the new
53
+ * scope before `fn` runs, so observability code can pin user/org info onto
54
+ * isolation-scoped backends (Sentry, OpenTelemetry, etc.).
28
55
  */
29
56
  export function runWithRequestContext(ctx, fn) {
30
- return als.run(ctx, fn);
57
+ return als.run(ctx, () => {
58
+ if (observers.length > 0) {
59
+ for (const obs of observers) {
60
+ try {
61
+ obs(ctx);
62
+ }
63
+ catch {
64
+ // Observers must never break the request path.
65
+ }
66
+ }
67
+ }
68
+ return fn();
69
+ });
31
70
  }
32
71
  /**
33
72
  * Return the active request context, if this call chain is running under one.
@@ -1 +1 @@
1
- {"version":3,"file":"request-context.js","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA4DrD,MAAM,UAAU,GAAG,gCAAyC,CAAC;AAI7D,MAAM,SAAS,GAAG,UAAsC,CAAC;AACzD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC;IAC3B,SAAS,CAAC,UAAU,CAAC,GAAG,IAAI,iBAAiB,EAAkB,CAAC;AAClE,CAAC;AACD,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAE,CAAC;AAEnC;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAAmB,EACnB,EAAwB;IAExB,OAAO,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,CAAC,QAAQ,EAAE,KAAK,SAAS,CAAC;AACtC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB;IACjC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,SAAS,CAAC;IAChD,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;AACtC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,KAAK,CAAC;IAC5C,OAAO,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAClC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,QAAQ,CAAC;IAC/C,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;AACzC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,0BAA0B;IACxC,OAAO,GAAG,CAAC,QAAQ,EAAE,EAAE,mBAAmB,KAAK,IAAI,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,4BAA4B;IAG1C,OAAO,GAAG,CAAC,QAAQ,EAAE,EAAE,WAAW,CAAC;AACrC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB;IAIlC,MAAM,SAAS,GAAG,mBAAmB,EAAE,CAAC;IACxC,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,IAAI,EAAE,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,KAAK,CAAC,GAAG,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB;IACrC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;IAC7B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC,GAAG;QAAE,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;IAC/B,OAAO,KAAK,CAAC,GAAG,CAAC;AACnB,CAAC","sourcesContent":["/**\n * Per-request context using AsyncLocalStorage.\n *\n * Replaces the unsafe pattern of mutating `process.env.AGENT_USER_EMAIL` /\n * `process.env.AGENT_ORG_ID` on every request. On Node.js (Netlify, self-hosted)\n * concurrent requests would overwrite each other's env vars. AsyncLocalStorage\n * gives each async call-chain its own isolated context.\n *\n * Supported on all deployment targets:\n * - Node.js (native)\n * - Cloudflare Workers (via nodejs_compat flag)\n * - Deno Deploy (via node:async_hooks compat)\n *\n * For CLI scripts that run outside a request context, the getters fall back to\n * process.env so existing `AGENT_USER_EMAIL=x pnpm action foo` invocations\n * continue to work.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Per-request agent-run state. Lives on `RequestContext.run` so the\n * agent-chat plugin can populate fields as the run progresses (owner,\n * resolved API key, system prompt, engine, model, threadId) without\n * mutating module-scope `let` bindings — those leak across concurrent\n * requests on a single Node.js process.\n *\n * Mutated in-place by `prepareRun`, `onEngineResolved`, `onRunStart` so\n * tool factory closures (automation, fetch, team, builder-browser) read\n * the live per-request value via `getRequestRunContext()`.\n */\nexport interface RequestRunContext {\n /** Origin of the current request (used by the builder-browser tool). */\n requestOrigin?: string;\n /** Resolved owner email (set by prepareRun). */\n owner?: string;\n /** Owner's active Anthropic API key (set by prepareRun). */\n userApiKey?: string;\n /** Thread ID for the current run (set by onRunStart). */\n threadId?: string;\n /** System prompt actually sent to the model for this run. */\n systemPrompt?: string;\n /** Engine instance for this run (set by onEngineResolved). */\n engine?: import(\"../agent/engine/types.js\").AgentEngine;\n /** Model name for this run (set by onEngineResolved). */\n model?: string;\n}\n\nexport interface RequestContext {\n userEmail?: string;\n orgId?: string;\n timezone?: string;\n /**\n * True when this request is being processed by an integration-platform\n * webhook (Slack, Telegram, etc.) where the function timeout is the\n * binding constraint. Code that calls slow remote APIs can use this to apply\n * tighter budgets on this path while leaving normal agent-chat callers\n * (5+ min budget) unaffected.\n */\n isIntegrationCaller?: boolean;\n /**\n * Metadata for the currently-processing integration task. This lets tools\n * that start long-running remote work persist a continuation that can update\n * the originating platform thread after the current function budget ends.\n */\n integration?: {\n taskId: string;\n attempts?: number;\n incoming: import(\"../integrations/types.js\").IncomingMessage;\n placeholderRef?: string;\n };\n /**\n * Mutable per-request agent-run state. Populated by the agent-chat plugin\n * during a run; tool closures dereference it on each invocation.\n */\n run?: RequestRunContext;\n}\n\nconst GLOBAL_KEY = \"__agentNativeRequestContextAls\" as const;\ntype GlobalWithRequestContext = typeof globalThis & {\n [GLOBAL_KEY]?: AsyncLocalStorage<RequestContext>;\n};\nconst globalRef = globalThis as GlobalWithRequestContext;\nif (!globalRef[GLOBAL_KEY]) {\n globalRef[GLOBAL_KEY] = new AsyncLocalStorage<RequestContext>();\n}\nconst als = globalRef[GLOBAL_KEY]!;\n\n/**\n * Run a callback within a per-request context. The context is available to all\n * async operations spawned from `fn` via `getRequestUserEmail()` / `getRequestOrgId()`.\n */\nexport function runWithRequestContext<T>(\n ctx: RequestContext,\n fn: () => T | Promise<T>,\n): T | Promise<T> {\n return als.run(ctx, fn);\n}\n\n/**\n * Return the active request context, if this call chain is running under one.\n *\n * This is intentionally distinct from `getRequestUserEmail()`: callers that\n * have an active context with no authenticated user must not fall through to\n * process-wide CLI fallbacks such as `AGENT_USER_EMAIL` or \"latest session\".\n */\nexport function getRequestContext(): RequestContext | undefined {\n return als.getStore();\n}\n\n/**\n * True when AsyncLocalStorage has an active context for this call chain.\n * Useful for helpers that support both HTTP requests and standalone CLI runs.\n */\nexport function hasRequestContext(): boolean {\n return als.getStore() !== undefined;\n}\n\n/**\n * Get the current request's user email.\n *\n * - If a request context exists (HTTP/A2A path), returns its `userEmail` —\n * even when that value is `undefined`. The env fallback MUST NOT fire here:\n * a stale process-wide `AGENT_USER_EMAIL` from a CLI run or previous bug\n * would leak into an unauthenticated A2A/API call (e.g. unsigned or API-key\n * modes where `runWithRequestContext({ userEmail: undefined })` is used).\n * - Only when there is NO request context (CLI scripts) do we fall back to\n * `process.env.AGENT_USER_EMAIL`.\n */\nexport function getRequestUserEmail(): string | undefined {\n const store = als.getStore();\n if (store !== undefined) return store.userEmail;\n return process.env.AGENT_USER_EMAIL;\n}\n\n/**\n * Get the current request's org ID.\n *\n * Same store-aware semantics as `getRequestUserEmail()` — env fallback is\n * CLI-only, so a request that explicitly has no org doesn't inherit a stale\n * `process.env.AGENT_ORG_ID` from a prior request on the same Lambda instance.\n */\nexport function getRequestOrgId(): string | undefined {\n const store = als.getStore();\n if (store !== undefined) return store.orgId;\n return process.env.AGENT_ORG_ID;\n}\n\n/**\n * Get the current request's IANA timezone (e.g. \"America/Los_Angeles\").\n * The UI sends this via the `x-user-timezone` header on every action call, and\n * the agent chat plugin propagates it into the request context so that\n * agent-initiated tool calls also see the user's timezone. Falls back to\n * `process.env.AGENT_USER_TIMEZONE` only for CLI scripts (no request context).\n */\nexport function getRequestTimezone(): string | undefined {\n const store = als.getStore();\n if (store !== undefined) return store.timezone;\n return process.env.AGENT_USER_TIMEZONE;\n}\n\n/**\n * Returns true when this request is on an integration-platform path (Slack,\n * Telegram, etc.) — i.e. we're inside the integration plugin's processor\n * function and the platform's deliver-by deadline plus the host's function\n * timeout are the binding budget. Non-integration callers (CLI, normal\n * agent chat) should treat this as `false`.\n */\nexport function isIntegrationCallerRequest(): boolean {\n return als.getStore()?.isIntegrationCaller === true;\n}\n\nexport function getIntegrationRequestContext():\n | NonNullable<RequestContext[\"integration\"]>\n | undefined {\n return als.getStore()?.integration;\n}\n\n/**\n * Convenience: returns `{ userEmail, orgId }` from the active request context,\n * suitable for passing to `resolveCredential(key, ctx)`. Returns `null` when\n * no user is associated with the call (e.g. an unauthenticated public route).\n *\n * For framework actions auto-mounted at `/_agent-native/actions/...` this is\n * always populated because action-routes wraps every invocation in\n * `runWithRequestContext`. For hand-written `/api/*` routes the calling code\n * is responsible for setting up the context (see `runWithRequestContext`).\n */\nexport function getCredentialContext(): {\n userEmail: string;\n orgId: string | null;\n} | null {\n const userEmail = getRequestUserEmail();\n if (!userEmail) return null;\n return { userEmail, orgId: getRequestOrgId() ?? null };\n}\n\n/**\n * Get the active request's mutable agent-run state. Returns `undefined` when\n * called outside an agent run (e.g. before `prepareRun` or in a non-agent\n * code path). Callers must tolerate the field absence; use the helper\n * `requireRequestRunContext()` if missing context is a programming error.\n */\nexport function getRequestRunContext(): RequestRunContext | undefined {\n const store = als.getStore();\n if (!store) return undefined;\n return store.run;\n}\n\n/**\n * Ensure a `RequestRunContext` exists on the active request store and\n * return it. Used by the agent-chat handler to attach run state once it\n * starts processing a chat request. Returns `undefined` if there is no\n * active request store (caller should not be invoking this outside ALS).\n */\nexport function ensureRequestRunContext(): RequestRunContext | undefined {\n const store = als.getStore();\n if (!store) return undefined;\n if (!store.run) store.run = {};\n return store.run;\n}\n"]}
1
+ {"version":3,"file":"request-context.js","sourceRoot":"","sources":["../../src/server/request-context.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-request context using AsyncLocalStorage.\n *\n * Replaces the unsafe pattern of mutating `process.env.AGENT_USER_EMAIL` /\n * `process.env.AGENT_ORG_ID` on every request. On Node.js (Netlify, self-hosted)\n * concurrent requests would overwrite each other's env vars. AsyncLocalStorage\n * gives each async call-chain its own isolated context.\n *\n * Supported on all deployment targets:\n * - Node.js (native)\n * - Cloudflare Workers (via nodejs_compat flag)\n * - Deno Deploy (via node:async_hooks compat)\n *\n * For CLI scripts that run outside a request context, the getters fall back to\n * process.env so existing `AGENT_USER_EMAIL=x pnpm action foo` invocations\n * continue to work.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\n/**\n * Per-request agent-run state. Lives on `RequestContext.run` so the\n * agent-chat plugin can populate fields as the run progresses (owner,\n * resolved API key, system prompt, engine, model, threadId) without\n * mutating module-scope `let` bindings — those leak across concurrent\n * requests on a single Node.js process.\n *\n * Mutated in-place by `prepareRun`, `onEngineResolved`, `onRunStart` so\n * tool factory closures (automation, fetch, team, builder-browser) read\n * the live per-request value via `getRequestRunContext()`.\n */\nexport interface RequestRunContext {\n /** Origin of the current request (used by the builder-browser tool). */\n requestOrigin?: string;\n /** Resolved owner email (set by prepareRun). */\n owner?: string;\n /** Owner's active Anthropic API key (set by prepareRun). */\n userApiKey?: string;\n /** Thread ID for the current run (set by onRunStart). */\n threadId?: string;\n /** System prompt actually sent to the model for this run. */\n systemPrompt?: string;\n /** Engine instance for this run (set by onEngineResolved). */\n engine?: import(\"../agent/engine/types.js\").AgentEngine;\n /** Model name for this run (set by onEngineResolved). */\n model?: string;\n}\n\nexport interface RequestContext {\n userEmail?: string;\n orgId?: string;\n timezone?: string;\n /**\n * True when this request is being processed by an integration-platform\n * webhook (Slack, Telegram, etc.) where the function timeout is the\n * binding constraint. Code that calls slow remote APIs can use this to apply\n * tighter budgets on this path while leaving normal agent-chat callers\n * (5+ min budget) unaffected.\n */\n isIntegrationCaller?: boolean;\n /**\n * Metadata for the currently-processing integration task. This lets tools\n * that start long-running remote work persist a continuation that can update\n * the originating platform thread after the current function budget ends.\n */\n integration?: {\n taskId: string;\n attempts?: number;\n incoming: import(\"../integrations/types.js\").IncomingMessage;\n placeholderRef?: string;\n };\n /**\n * Mutable per-request agent-run state. Populated by the agent-chat plugin\n * during a run; tool closures dereference it on each invocation.\n */\n run?: RequestRunContext;\n}\n\nconst GLOBAL_KEY = \"__agentNativeRequestContextAls\" as const;\nconst OBSERVERS_KEY = \"__agentNativeRequestContextObservers\" as const;\ntype RequestContextObserver = (ctx: RequestContext) => void;\ntype GlobalWithRequestContext = typeof globalThis & {\n [GLOBAL_KEY]?: AsyncLocalStorage<RequestContext>;\n [OBSERVERS_KEY]?: RequestContextObserver[];\n};\nconst globalRef = globalThis as GlobalWithRequestContext;\nif (!globalRef[GLOBAL_KEY]) {\n globalRef[GLOBAL_KEY] = new AsyncLocalStorage<RequestContext>();\n}\nif (!globalRef[OBSERVERS_KEY]) {\n globalRef[OBSERVERS_KEY] = [];\n}\nconst als = globalRef[GLOBAL_KEY]!;\nconst observers = globalRef[OBSERVERS_KEY]!;\n\n/**\n * Register a callback fired every time `runWithRequestContext` enters a new\n * scope. The hook runs INSIDE the AsyncLocalStorage scope, so observability\n * helpers that read the current isolation scope (e.g. Sentry) attach to the\n * right per-request context.\n *\n * Returned function unregisters the observer. Observers must never throw —\n * any error is swallowed so a misbehaving observer can't break the request\n * path.\n */\nexport function addRequestContextObserver(\n observer: RequestContextObserver,\n): () => void {\n observers.push(observer);\n return () => {\n const i = observers.indexOf(observer);\n if (i !== -1) observers.splice(i, 1);\n };\n}\n\n/**\n * Run a callback within a per-request context. The context is available to all\n * async operations spawned from `fn` via `getRequestUserEmail()` / `getRequestOrgId()`.\n *\n * Any registered `addRequestContextObserver` callbacks fire inside the new\n * scope before `fn` runs, so observability code can pin user/org info onto\n * isolation-scoped backends (Sentry, OpenTelemetry, etc.).\n */\nexport function runWithRequestContext<T>(\n ctx: RequestContext,\n fn: () => T | Promise<T>,\n): T | Promise<T> {\n return als.run(ctx, () => {\n if (observers.length > 0) {\n for (const obs of observers) {\n try {\n obs(ctx);\n } catch {\n // Observers must never break the request path.\n }\n }\n }\n return fn();\n });\n}\n\n/**\n * Return the active request context, if this call chain is running under one.\n *\n * This is intentionally distinct from `getRequestUserEmail()`: callers that\n * have an active context with no authenticated user must not fall through to\n * process-wide CLI fallbacks such as `AGENT_USER_EMAIL` or \"latest session\".\n */\nexport function getRequestContext(): RequestContext | undefined {\n return als.getStore();\n}\n\n/**\n * True when AsyncLocalStorage has an active context for this call chain.\n * Useful for helpers that support both HTTP requests and standalone CLI runs.\n */\nexport function hasRequestContext(): boolean {\n return als.getStore() !== undefined;\n}\n\n/**\n * Get the current request's user email.\n *\n * - If a request context exists (HTTP/A2A path), returns its `userEmail` —\n * even when that value is `undefined`. The env fallback MUST NOT fire here:\n * a stale process-wide `AGENT_USER_EMAIL` from a CLI run or previous bug\n * would leak into an unauthenticated A2A/API call (e.g. unsigned or API-key\n * modes where `runWithRequestContext({ userEmail: undefined })` is used).\n * - Only when there is NO request context (CLI scripts) do we fall back to\n * `process.env.AGENT_USER_EMAIL`.\n */\nexport function getRequestUserEmail(): string | undefined {\n const store = als.getStore();\n if (store !== undefined) return store.userEmail;\n return process.env.AGENT_USER_EMAIL;\n}\n\n/**\n * Get the current request's org ID.\n *\n * Same store-aware semantics as `getRequestUserEmail()` — env fallback is\n * CLI-only, so a request that explicitly has no org doesn't inherit a stale\n * `process.env.AGENT_ORG_ID` from a prior request on the same Lambda instance.\n */\nexport function getRequestOrgId(): string | undefined {\n const store = als.getStore();\n if (store !== undefined) return store.orgId;\n return process.env.AGENT_ORG_ID;\n}\n\n/**\n * Get the current request's IANA timezone (e.g. \"America/Los_Angeles\").\n * The UI sends this via the `x-user-timezone` header on every action call, and\n * the agent chat plugin propagates it into the request context so that\n * agent-initiated tool calls also see the user's timezone. Falls back to\n * `process.env.AGENT_USER_TIMEZONE` only for CLI scripts (no request context).\n */\nexport function getRequestTimezone(): string | undefined {\n const store = als.getStore();\n if (store !== undefined) return store.timezone;\n return process.env.AGENT_USER_TIMEZONE;\n}\n\n/**\n * Returns true when this request is on an integration-platform path (Slack,\n * Telegram, etc.) — i.e. we're inside the integration plugin's processor\n * function and the platform's deliver-by deadline plus the host's function\n * timeout are the binding budget. Non-integration callers (CLI, normal\n * agent chat) should treat this as `false`.\n */\nexport function isIntegrationCallerRequest(): boolean {\n return als.getStore()?.isIntegrationCaller === true;\n}\n\nexport function getIntegrationRequestContext():\n | NonNullable<RequestContext[\"integration\"]>\n | undefined {\n return als.getStore()?.integration;\n}\n\n/**\n * Convenience: returns `{ userEmail, orgId }` from the active request context,\n * suitable for passing to `resolveCredential(key, ctx)`. Returns `null` when\n * no user is associated with the call (e.g. an unauthenticated public route).\n *\n * For framework actions auto-mounted at `/_agent-native/actions/...` this is\n * always populated because action-routes wraps every invocation in\n * `runWithRequestContext`. For hand-written `/api/*` routes the calling code\n * is responsible for setting up the context (see `runWithRequestContext`).\n */\nexport function getCredentialContext(): {\n userEmail: string;\n orgId: string | null;\n} | null {\n const userEmail = getRequestUserEmail();\n if (!userEmail) return null;\n return { userEmail, orgId: getRequestOrgId() ?? null };\n}\n\n/**\n * Get the active request's mutable agent-run state. Returns `undefined` when\n * called outside an agent run (e.g. before `prepareRun` or in a non-agent\n * code path). Callers must tolerate the field absence; use the helper\n * `requireRequestRunContext()` if missing context is a programming error.\n */\nexport function getRequestRunContext(): RequestRunContext | undefined {\n const store = als.getStore();\n if (!store) return undefined;\n return store.run;\n}\n\n/**\n * Ensure a `RequestRunContext` exists on the active request store and\n * return it. Used by the agent-chat handler to attach run state once it\n * starts processing a chat request. Returns `undefined` if there is no\n * active request store (caller should not be invoking this outside ALS).\n */\nexport function ensureRequestRunContext(): RequestRunContext | undefined {\n const store = als.getStore();\n if (!store) return undefined;\n if (!store.run) store.run = {};\n return store.run;\n}\n"]}
@@ -0,0 +1,11 @@
1
+ type NitroPluginDef = (nitroApp: any) => void | Promise<void>;
2
+ export declare function createSentryPlugin(): NitroPluginDef;
3
+ /**
4
+ * Default Sentry plugin — auto-mounts when a template doesn't define its
5
+ * own `server/plugins/sentry.ts`. Reads `SENTRY_SERVER_DSN` from env and
6
+ * silently no-ops when it's unset, so this is safe to default-mount in
7
+ * every template (including local dev with no DSN configured).
8
+ */
9
+ export declare const defaultSentryPlugin: NitroPluginDef;
10
+ export {};
11
+ //# sourceMappingURL=sentry-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry-plugin.d.ts","sourceRoot":"","sources":["../../src/server/sentry-plugin.ts"],"names":[],"mappings":"AAkCA,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAqC9D,wBAAgB,kBAAkB,IAAI,cAAc,CAsDnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAAqC,CAAC"}