@agent-native/core 0.12.5 → 0.12.7

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 (117) hide show
  1. package/dist/agent/engine/index.d.ts +1 -1
  2. package/dist/agent/engine/index.d.ts.map +1 -1
  3. package/dist/agent/engine/index.js +1 -1
  4. package/dist/agent/engine/index.js.map +1 -1
  5. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  6. package/dist/agent/thread-data-builder.js +2 -0
  7. package/dist/agent/thread-data-builder.js.map +1 -1
  8. package/dist/cli/templates-meta.d.ts.map +1 -1
  9. package/dist/cli/templates-meta.js +12 -14
  10. package/dist/cli/templates-meta.js.map +1 -1
  11. package/dist/client/AgentPanel.js +3 -2
  12. package/dist/client/AgentPanel.js.map +1 -1
  13. package/dist/client/CommandMenu.d.ts +1 -0
  14. package/dist/client/CommandMenu.d.ts.map +1 -1
  15. package/dist/client/CommandMenu.js +11 -3
  16. package/dist/client/CommandMenu.js.map +1 -1
  17. package/dist/client/ErrorBoundary.d.ts.map +1 -1
  18. package/dist/client/ErrorBoundary.js +15 -5
  19. package/dist/client/ErrorBoundary.js.map +1 -1
  20. package/dist/client/FeedbackButton.d.ts.map +1 -1
  21. package/dist/client/FeedbackButton.js +7 -3
  22. package/dist/client/FeedbackButton.js.map +1 -1
  23. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  24. package/dist/client/MultiTabAssistantChat.js +112 -33
  25. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  26. package/dist/client/NewWorkspaceAppFlow.js +1 -1
  27. package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
  28. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  29. package/dist/client/agent-chat-adapter.js +63 -14
  30. package/dist/client/agent-chat-adapter.js.map +1 -1
  31. package/dist/client/components/icons/AgentNativeIcon.d.ts +20 -0
  32. package/dist/client/components/icons/AgentNativeIcon.d.ts.map +1 -0
  33. package/dist/client/components/icons/AgentNativeIcon.js +12 -0
  34. package/dist/client/components/icons/AgentNativeIcon.js.map +1 -0
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.d.ts.map +1 -1
  37. package/dist/client/index.js +1 -0
  38. package/dist/client/index.js.map +1 -1
  39. package/dist/client/notifications/NotificationsBell.d.ts +5 -1
  40. package/dist/client/notifications/NotificationsBell.d.ts.map +1 -1
  41. package/dist/client/notifications/NotificationsBell.js +2 -2
  42. package/dist/client/notifications/NotificationsBell.js.map +1 -1
  43. package/dist/client/onboarding/OnboardingPanel.js +19 -4
  44. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  45. package/dist/client/org/TeamPage.d.ts.map +1 -1
  46. package/dist/client/org/TeamPage.js +4 -1
  47. package/dist/client/org/TeamPage.js.map +1 -1
  48. package/dist/client/settings/UsageSection.d.ts.map +1 -1
  49. package/dist/client/settings/UsageSection.js +41 -8
  50. package/dist/client/settings/UsageSection.js.map +1 -1
  51. package/dist/client/sharing/ShareButton.d.ts +14 -0
  52. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  53. package/dist/client/sharing/ShareButton.js +36 -14
  54. package/dist/client/sharing/ShareButton.js.map +1 -1
  55. package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
  56. package/dist/client/sharing/ShareDialog.js +16 -6
  57. package/dist/client/sharing/ShareDialog.js.map +1 -1
  58. package/dist/client/sse-event-processor.d.ts.map +1 -1
  59. package/dist/client/sse-event-processor.js +43 -4
  60. package/dist/client/sse-event-processor.js.map +1 -1
  61. package/dist/client/use-chat-threads.d.ts +1 -1
  62. package/dist/client/use-chat-threads.d.ts.map +1 -1
  63. package/dist/client/use-chat-threads.js +2 -2
  64. package/dist/client/use-chat-threads.js.map +1 -1
  65. package/dist/client/useProductionAgent.js +2 -2
  66. package/dist/client/useProductionAgent.js.map +1 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/onboarding/types.d.ts +6 -2
  72. package/dist/onboarding/types.d.ts.map +1 -1
  73. package/dist/onboarding/types.js.map +1 -1
  74. package/dist/server/auth.d.ts +11 -0
  75. package/dist/server/auth.d.ts.map +1 -1
  76. package/dist/server/auth.js +7 -1
  77. package/dist/server/auth.js.map +1 -1
  78. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  79. package/dist/server/core-routes-plugin.js +45 -5
  80. package/dist/server/core-routes-plugin.js.map +1 -1
  81. package/dist/server/credential-provider.d.ts +5 -0
  82. package/dist/server/credential-provider.d.ts.map +1 -1
  83. package/dist/server/credential-provider.js +8 -0
  84. package/dist/server/credential-provider.js.map +1 -1
  85. package/dist/server/index.d.ts +1 -1
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js +1 -1
  88. package/dist/server/index.js.map +1 -1
  89. package/dist/server/onboarding-html.d.ts +12 -0
  90. package/dist/server/onboarding-html.d.ts.map +1 -1
  91. package/dist/server/onboarding-html.js +86 -0
  92. package/dist/server/onboarding-html.js.map +1 -1
  93. package/dist/server/ssr-handler.d.ts.map +1 -1
  94. package/dist/server/ssr-handler.js +16 -6
  95. package/dist/server/ssr-handler.js.map +1 -1
  96. package/dist/sharing/actions/share-resource.d.ts +1 -0
  97. package/dist/sharing/actions/share-resource.d.ts.map +1 -1
  98. package/dist/sharing/actions/share-resource.js +65 -3
  99. package/dist/sharing/actions/share-resource.js.map +1 -1
  100. package/dist/sharing/registry.d.ts +5 -0
  101. package/dist/sharing/registry.d.ts.map +1 -1
  102. package/dist/sharing/registry.js.map +1 -1
  103. package/dist/usage/store.d.ts +16 -0
  104. package/dist/usage/store.d.ts.map +1 -1
  105. package/dist/usage/store.js +31 -0
  106. package/dist/usage/store.js.map +1 -1
  107. package/dist/vite/client.d.ts.map +1 -1
  108. package/dist/vite/client.js +11 -6
  109. package/dist/vite/client.js.map +1 -1
  110. package/docs/content/faq.md +1 -2
  111. package/docs/content/getting-started.md +1 -2
  112. package/docs/content/onboarding.md +8 -6
  113. package/docs/content/sharing.md +9 -7
  114. package/docs/content/template-images.md +2 -1
  115. package/docs/content/template-slides.md +2 -2
  116. package/docs/content/what-is-agent-native.md +1 -1
  117. package/package.json +1 -1
@@ -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 @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"]}
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 * Optional preflight copy shown before redirecting through Google sign-in.\n * Use this when a hosted app needs to warn about provider-specific consent\n * screens while leaving self-hosted deployments untouched.\n */\n googleSignInNotice?: {\n host?: string;\n title: string;\n body: string;\n continueLabel?: string;\n cancelLabel?: 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 const googleSignInNotice = opts.googleSignInNotice;\n const googleNoticeHtml =\n showGoogle && googleSignInNotice\n ? `\n <div\n class=\"google-preflight\"\n id=\"google-preflight\"\n data-host=\"${esc(googleSignInNotice.host ?? \"\")}\"\n role=\"dialog\"\n aria-labelledby=\"google-preflight-title\"\n aria-describedby=\"google-preflight-copy\"\n >\n <p class=\"google-preflight-title\" id=\"google-preflight-title\">${esc(googleSignInNotice.title)}</p>\n <p class=\"google-preflight-copy\" id=\"google-preflight-copy\">${esc(googleSignInNotice.body)}</p>\n <div class=\"google-preflight-actions\">\n <button type=\"button\" class=\"btn-primary\" id=\"google-preflight-continue\" onclick=\"__anAcceptGoogleNotice()\">${esc(googleSignInNotice.continueLabel ?? \"Continue\")}</button>\n <button type=\"button\" class=\"btn-secondary\" onclick=\"__anHideGoogleNotice()\">${esc(googleSignInNotice.cancelLabel ?? \"Cancel\")}</button>\n </div>\n </div>`\n : \"\";\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 .google-preflight {\n display: none;\n margin-top: 0.75rem;\n padding: 0.875rem;\n border: 1px solid rgba(255,255,255,0.12);\n border-radius: 10px;\n background: rgba(255,255,255,0.05);\n box-shadow: 0 14px 36px rgba(0,0,0,0.28);\n }\n .google-preflight.show { display: block; }\n .google-preflight-title {\n margin-bottom: 0.375rem;\n color: #fff;\n font-size: 0.8125rem;\n font-weight: 600;\n }\n .google-preflight-copy {\n color: #b4b4b8;\n font-size: 0.75rem;\n line-height: 1.55;\n }\n .google-preflight-actions {\n display: flex;\n gap: 0.5rem;\n margin-top: 0.875rem;\n }\n .google-preflight-actions .btn-primary,\n .google-preflight-actions .btn-secondary {\n flex: 1;\n width: auto;\n margin-top: 0;\n }\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${googleNoticeHtml}\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 if (__anShouldShowGoogleNotice()) {\n __anShowGoogleNotice();\n return;\n }\n return __anStartGoogleSignIn();\n }\n async function __anStartGoogleSignIn() {\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${\n googleSignInNotice\n ? `\n window.__anGoogleNoticeAccepted = false;\n function __anShouldShowGoogleNotice() {\n var notice = document.getElementById('google-preflight');\n if (!notice || window.__anGoogleNoticeAccepted) return false;\n var host = notice.getAttribute('data-host');\n return !host || window.location.hostname === host;\n }\n function __anShowGoogleNotice() {\n var notice = document.getElementById('google-preflight');\n if (!notice) return;\n notice.classList.add('show');\n var continueBtn = document.getElementById('google-preflight-continue');\n if (continueBtn) continueBtn.focus();\n }\n function __anHideGoogleNotice() {\n var notice = document.getElementById('google-preflight');\n if (notice) notice.classList.remove('show');\n }\n function __anAcceptGoogleNotice() {\n window.__anGoogleNoticeAccepted = true;\n __anHideGoogleNotice();\n __anStartGoogleSignIn();\n }`\n : `\n function __anShouldShowGoogleNotice() { return false; }`\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 +1 @@
1
- {"version":3,"file":"ssr-handler.d.ts","sourceRoot":"","sources":["../../src/server/ssr-handler.ts"],"names":[],"mappings":"AA+IA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,2FAkD5E"}
1
+ {"version":3,"file":"ssr-handler.d.ts","sourceRoot":"","sources":["../../src/server/ssr-handler.ts"],"names":[],"mappings":"AAiKA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,2FA2C5E"}
@@ -97,6 +97,21 @@ function prefixMountedHtml(html, basePath) {
97
97
  return `url(${q}${prefixMountedPath(path, basePath)}${q})`;
98
98
  });
99
99
  }
100
+ function isFrameworkOrAssetPath(pathname) {
101
+ return (pathname.startsWith("/.well-known/") ||
102
+ pathname.startsWith("/_agent_native/") ||
103
+ pathname.startsWith("/_agent-native/") ||
104
+ pathname.startsWith("/api/") ||
105
+ pathname.startsWith("/@vite/") ||
106
+ pathname.startsWith("/@id/") ||
107
+ pathname.startsWith("/@fs/") ||
108
+ pathname === "/@react-refresh" ||
109
+ pathname === "/__vite_ping" ||
110
+ pathname === "/__open-in-editor" ||
111
+ pathname === "/favicon.ico" ||
112
+ pathname === "/favicon.png" ||
113
+ (/\.\w+$/.test(pathname) && !pathname.endsWith(".data")));
114
+ }
100
115
  async function rewriteMountedResponse(response, basePath) {
101
116
  if (!basePath)
102
117
  return response;
@@ -130,12 +145,7 @@ export function createH3SSRHandler(getBuild) {
130
145
  return defineEventHandler(async (event) => {
131
146
  const basePath = getAppBasePath();
132
147
  const p = stripAppBasePath(event.url.pathname);
133
- if (p.startsWith("/.well-known/") ||
134
- p.startsWith("/_agent-native/") ||
135
- p.startsWith("/api/") ||
136
- p === "/favicon.ico" ||
137
- p === "/favicon.png" ||
138
- (/\.\w+$/.test(p) && !p.endsWith(".data"))) {
148
+ if (isFrameworkOrAssetPath(p)) {
139
149
  return new Response(null, { status: 404 });
140
150
  }
141
151
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-handler.js","sourceRoot":"","sources":["../../src/server/ssr-handler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared SSR catch-all handler for React Router framework mode.\n *\n * Templates wire this up via:\n *\n * // server/routes/[...page].get.ts\n * import { createH3SSRHandler } from \"@agent-native/core/server/ssr-handler\";\n * export default createH3SSRHandler(\n * () => import(\"virtual:react-router/server-build\"),\n * );\n *\n * The `getBuild` callback MUST live in the template's own source so Vite's\n * @react-router/dev plugin can resolve the `virtual:` module. Pulling the\n * import into core (e.g. via a re-export) puts it in node_modules where\n * Vite's SSR externalizer leaves it untouched and Node's ESM loader rejects\n * the unknown scheme — silently 302'ing every request to \"/\".\n */\nimport { createRequestHandler } from \"react-router\";\nimport { defineEventHandler } from \"h3\";\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 getAppBasePath(): string {\n const metaEnv = (\n import.meta as unknown as {\n env?: Record<string, string | undefined>;\n }\n ).env;\n return normalizeAppBasePath(\n process.env.VITE_APP_BASE_PATH ||\n process.env.APP_BASE_PATH ||\n metaEnv?.VITE_APP_BASE_PATH ||\n metaEnv?.APP_BASE_PATH ||\n metaEnv?.BASE_URL,\n );\n}\n\nfunction stripAppBasePath(pathname: string): string {\n const basePath = getAppBasePath();\n return stripBasePath(pathname, basePath);\n}\n\nfunction stripBasePath(pathname: string, basePath: string): string {\n if (!basePath) return pathname;\n if (pathname === basePath) return \"/\";\n if (pathname.startsWith(`${basePath}/`)) {\n return pathname.slice(basePath.length) || \"/\";\n }\n return pathname;\n}\n\nfunction requestWithPathname(\n request: Request,\n pathname: string,\n basePath: string,\n): Request {\n const url = new URL(request.url);\n let changed = false;\n if (basePath && pathname === \"/__manifest\") {\n const paths = url.searchParams.get(\"paths\");\n if (paths) {\n const strippedPaths = paths\n .split(\",\")\n .map((path) => stripBasePath(path, basePath))\n .join(\",\");\n if (strippedPaths !== paths) {\n url.searchParams.set(\"paths\", strippedPaths);\n changed = true;\n }\n }\n }\n if (url.pathname !== pathname) {\n url.pathname = pathname;\n changed = true;\n }\n if (!changed) return request;\n const init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n headers: request.headers,\n signal: request.signal,\n };\n if (request.body && ![\"GET\", \"HEAD\"].includes(request.method.toUpperCase())) {\n init.body = request.body;\n init.duplex = \"half\";\n }\n return new Request(url, init);\n}\n\nfunction prefixMountedPath(path: string, basePath: string): string {\n if (!basePath || !path.startsWith(\"/\") || path.startsWith(\"//\")) return path;\n if (path === basePath || path.startsWith(`${basePath}/`)) return path;\n return `${basePath}${path}`;\n}\n\nfunction prefixMountedHtml(html: string, basePath: string): string {\n if (!basePath) return html;\n return html\n .replace(\n /\\b(href|src|action|formaction|poster)=([\"'])(\\/(?!\\/)[^\"']*)\\2/g,\n (_match, attr: string, quote: string, path: string) =>\n `${attr}=${quote}${prefixMountedPath(path, basePath)}${quote}`,\n )\n .replace(/url\\(([\"']?)(\\/(?!\\/)[^)'\" ]+)\\1\\)/g, (_match, quote, path) => {\n const q = quote || \"\";\n return `url(${q}${prefixMountedPath(path, basePath)}${q})`;\n });\n}\n\nasync function rewriteMountedResponse(\n response: Response,\n basePath: string,\n): Promise<Response> {\n if (!basePath) return response;\n\n const headers = new Headers(response.headers);\n const location = headers.get(\"location\");\n if (location?.startsWith(\"/\") && !location.startsWith(\"//\")) {\n headers.set(\"location\", prefixMountedPath(location, basePath));\n }\n\n const contentType = headers.get(\"content-type\") ?? \"\";\n if (!contentType.toLowerCase().includes(\"text/html\") || !response.body) {\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n }\n\n const html = await response.text();\n headers.delete(\"content-length\");\n return new Response(prefixMountedHtml(html, basePath), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n\n/**\n * Create an h3 catch-all that hands page routes to React Router and\n * returns 404 for framework / asset paths that React Router doesn't own.\n */\nexport function createH3SSRHandler(getBuild: () => Promise<unknown> | unknown) {\n const handler = createRequestHandler(getBuild as any);\n return defineEventHandler(async (event) => {\n const basePath = getAppBasePath();\n const p = stripAppBasePath(event.url.pathname);\n if (\n p.startsWith(\"/.well-known/\") ||\n p.startsWith(\"/_agent-native/\") ||\n p.startsWith(\"/api/\") ||\n p === \"/favicon.ico\" ||\n p === \"/favicon.png\" ||\n (/\\.\\w+$/.test(p) && !p.endsWith(\".data\"))\n ) {\n return new Response(null, { status: 404 });\n }\n try {\n const request = requestWithPathname(event.req as Request, p, basePath);\n if (request.method === \"HEAD\") {\n const getRequest = new Request(request.url, {\n method: \"GET\",\n headers: request.headers,\n signal: request.signal,\n });\n const response = await handler(getRequest);\n return await rewriteMountedResponse(\n new Response(null, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n }),\n basePath,\n );\n }\n return await rewriteMountedResponse(await handler(request), basePath);\n } catch (err) {\n // Log the full stack server-side, but never leak it to the client.\n // Stack traces expose file paths, library versions, and code structure\n // that aid reconnaissance attacks. In dev we surface the message text\n // so devtools shows something useful; in prod we return a bare 500.\n console.error(\"[ssr-handler] SSR error:\", err);\n const isProd = process.env.NODE_ENV === \"production\";\n const body = isProd\n ? \"Internal Server Error\"\n : `Internal Server Error: ${(err as Error)?.message ?? err}`;\n return new Response(body, {\n status: 500,\n headers: { \"content-type\": \"text/plain\" },\n });\n }\n });\n}\n"]}
1
+ {"version":3,"file":"ssr-handler.js","sourceRoot":"","sources":["../../src/server/ssr-handler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared SSR catch-all handler for React Router framework mode.\n *\n * Templates wire this up via:\n *\n * // server/routes/[...page].get.ts\n * import { createH3SSRHandler } from \"@agent-native/core/server/ssr-handler\";\n * export default createH3SSRHandler(\n * () => import(\"virtual:react-router/server-build\"),\n * );\n *\n * The `getBuild` callback MUST live in the template's own source so Vite's\n * @react-router/dev plugin can resolve the `virtual:` module. Pulling the\n * import into core (e.g. via a re-export) puts it in node_modules where\n * Vite's SSR externalizer leaves it untouched and Node's ESM loader rejects\n * the unknown scheme — silently 302'ing every request to \"/\".\n */\nimport { createRequestHandler } from \"react-router\";\nimport { defineEventHandler } from \"h3\";\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 getAppBasePath(): string {\n const metaEnv = (\n import.meta as unknown as {\n env?: Record<string, string | undefined>;\n }\n ).env;\n return normalizeAppBasePath(\n process.env.VITE_APP_BASE_PATH ||\n process.env.APP_BASE_PATH ||\n metaEnv?.VITE_APP_BASE_PATH ||\n metaEnv?.APP_BASE_PATH ||\n metaEnv?.BASE_URL,\n );\n}\n\nfunction stripAppBasePath(pathname: string): string {\n const basePath = getAppBasePath();\n return stripBasePath(pathname, basePath);\n}\n\nfunction stripBasePath(pathname: string, basePath: string): string {\n if (!basePath) return pathname;\n if (pathname === basePath) return \"/\";\n if (pathname.startsWith(`${basePath}/`)) {\n return pathname.slice(basePath.length) || \"/\";\n }\n return pathname;\n}\n\nfunction requestWithPathname(\n request: Request,\n pathname: string,\n basePath: string,\n): Request {\n const url = new URL(request.url);\n let changed = false;\n if (basePath && pathname === \"/__manifest\") {\n const paths = url.searchParams.get(\"paths\");\n if (paths) {\n const strippedPaths = paths\n .split(\",\")\n .map((path) => stripBasePath(path, basePath))\n .join(\",\");\n if (strippedPaths !== paths) {\n url.searchParams.set(\"paths\", strippedPaths);\n changed = true;\n }\n }\n }\n if (url.pathname !== pathname) {\n url.pathname = pathname;\n changed = true;\n }\n if (!changed) return request;\n const init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n headers: request.headers,\n signal: request.signal,\n };\n if (request.body && ![\"GET\", \"HEAD\"].includes(request.method.toUpperCase())) {\n init.body = request.body;\n init.duplex = \"half\";\n }\n return new Request(url, init);\n}\n\nfunction prefixMountedPath(path: string, basePath: string): string {\n if (!basePath || !path.startsWith(\"/\") || path.startsWith(\"//\")) return path;\n if (path === basePath || path.startsWith(`${basePath}/`)) return path;\n return `${basePath}${path}`;\n}\n\nfunction prefixMountedHtml(html: string, basePath: string): string {\n if (!basePath) return html;\n return html\n .replace(\n /\\b(href|src|action|formaction|poster)=([\"'])(\\/(?!\\/)[^\"']*)\\2/g,\n (_match, attr: string, quote: string, path: string) =>\n `${attr}=${quote}${prefixMountedPath(path, basePath)}${quote}`,\n )\n .replace(/url\\(([\"']?)(\\/(?!\\/)[^)'\" ]+)\\1\\)/g, (_match, quote, path) => {\n const q = quote || \"\";\n return `url(${q}${prefixMountedPath(path, basePath)}${q})`;\n });\n}\n\nfunction isFrameworkOrAssetPath(pathname: string): boolean {\n return (\n pathname.startsWith(\"/.well-known/\") ||\n pathname.startsWith(\"/_agent_native/\") ||\n pathname.startsWith(\"/_agent-native/\") ||\n pathname.startsWith(\"/api/\") ||\n pathname.startsWith(\"/@vite/\") ||\n pathname.startsWith(\"/@id/\") ||\n pathname.startsWith(\"/@fs/\") ||\n pathname === \"/@react-refresh\" ||\n pathname === \"/__vite_ping\" ||\n pathname === \"/__open-in-editor\" ||\n pathname === \"/favicon.ico\" ||\n pathname === \"/favicon.png\" ||\n (/\\.\\w+$/.test(pathname) && !pathname.endsWith(\".data\"))\n );\n}\n\nasync function rewriteMountedResponse(\n response: Response,\n basePath: string,\n): Promise<Response> {\n if (!basePath) return response;\n\n const headers = new Headers(response.headers);\n const location = headers.get(\"location\");\n if (location?.startsWith(\"/\") && !location.startsWith(\"//\")) {\n headers.set(\"location\", prefixMountedPath(location, basePath));\n }\n\n const contentType = headers.get(\"content-type\") ?? \"\";\n if (!contentType.toLowerCase().includes(\"text/html\") || !response.body) {\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n }\n\n const html = await response.text();\n headers.delete(\"content-length\");\n return new Response(prefixMountedHtml(html, basePath), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n\n/**\n * Create an h3 catch-all that hands page routes to React Router and\n * returns 404 for framework / asset paths that React Router doesn't own.\n */\nexport function createH3SSRHandler(getBuild: () => Promise<unknown> | unknown) {\n const handler = createRequestHandler(getBuild as any);\n return defineEventHandler(async (event) => {\n const basePath = getAppBasePath();\n const p = stripAppBasePath(event.url.pathname);\n if (isFrameworkOrAssetPath(p)) {\n return new Response(null, { status: 404 });\n }\n try {\n const request = requestWithPathname(event.req as Request, p, basePath);\n if (request.method === \"HEAD\") {\n const getRequest = new Request(request.url, {\n method: \"GET\",\n headers: request.headers,\n signal: request.signal,\n });\n const response = await handler(getRequest);\n return await rewriteMountedResponse(\n new Response(null, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n }),\n basePath,\n );\n }\n return await rewriteMountedResponse(await handler(request), basePath);\n } catch (err) {\n // Log the full stack server-side, but never leak it to the client.\n // Stack traces expose file paths, library versions, and code structure\n // that aid reconnaissance attacks. In dev we surface the message text\n // so devtools shows something useful; in prod we return a bare 500.\n console.error(\"[ssr-handler] SSR error:\", err);\n const isProd = process.env.NODE_ENV === \"production\";\n const body = isProd\n ? \"Internal Server Error\"\n : `Internal Server Error: ${(err as Error)?.message ?? err}`;\n return new Response(body, {\n status: 500,\n headers: { \"content-type\": \"text/plain\" },\n });\n }\n });\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  export declare function isSyntheticQaEmail(email: string): boolean;
2
+ export declare function resolveShareNotificationUrl(explicitUrl: string | undefined, fallbackPath: string | undefined, appUrl?: string): string;
2
3
  declare const _default: any;
3
4
  export default _default;
4
5
  //# sourceMappingURL=share-resource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"share-resource.d.ts","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"AAUA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAazD;;AAWD,wBAmGG"}
1
+ {"version":3,"file":"share-resource.d.ts","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"AAUA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAazD;AAwCD,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,SAAwB,GAC7B,MAAM,CAOR;;AAWD,wBAyHG"}
@@ -20,6 +20,55 @@ export function isSyntheticQaEmail(email) {
20
20
  domain === "example.invalid" ||
21
21
  domain.endsWith(".invalid")));
22
22
  }
23
+ function appPath(path) {
24
+ if (!path.startsWith("/"))
25
+ return path;
26
+ const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || "";
27
+ const base = raw.trim().replace(/^\/+/, "").replace(/\/+$/, "");
28
+ if (!base)
29
+ return path;
30
+ const normalizedBase = `/${base}`;
31
+ if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {
32
+ return path;
33
+ }
34
+ return `${normalizedBase}${path}`;
35
+ }
36
+ function safeNotificationUrl(value, appUrl) {
37
+ const trimmed = value.trim();
38
+ if (!trimmed)
39
+ return null;
40
+ try {
41
+ const base = new URL(appUrl);
42
+ if (trimmed.startsWith("/")) {
43
+ const path = appPath(trimmed);
44
+ const basePath = base.pathname.replace(/\/+$/, "");
45
+ const alreadyIncludesBase = basePath && basePath !== "/" && path.startsWith(`${basePath}/`);
46
+ const joined = alreadyIncludesBase
47
+ ? `${base.origin}${path}`
48
+ : `${appUrl.replace(/\/+$/, "")}${path}`;
49
+ return new URL(joined).toString();
50
+ }
51
+ const url = new URL(trimmed);
52
+ if (!["http:", "https:"].includes(url.protocol))
53
+ return null;
54
+ if (url.origin !== base.origin)
55
+ return null;
56
+ return url.toString();
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ export function resolveShareNotificationUrl(explicitUrl, fallbackPath, appUrl = getAppProductionUrl()) {
63
+ for (const candidate of [explicitUrl, fallbackPath]) {
64
+ if (!candidate)
65
+ continue;
66
+ const url = safeNotificationUrl(candidate, appUrl);
67
+ if (url)
68
+ return url;
69
+ }
70
+ return appUrl;
71
+ }
23
72
  function nanoid(size = 12) {
24
73
  const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
25
74
  let id = "";
@@ -50,6 +99,14 @@ export default defineAction({
50
99
  .enum(["viewer", "editor", "admin"])
51
100
  .default("viewer")
52
101
  .describe("Role to grant."),
102
+ notify: z
103
+ .boolean()
104
+ .default(true)
105
+ .describe("Whether to email the user about a new individual share. Defaults to true."),
106
+ resourceUrl: z
107
+ .string()
108
+ .optional()
109
+ .describe("Optional app-relative or same-origin URL recipients should open. External origins are ignored."),
53
110
  }),
54
111
  run: async (args) => {
55
112
  const reg = requireShareableResource(args.resourceType);
@@ -79,7 +136,8 @@ export default defineAction({
79
136
  createdBy: actor,
80
137
  createdAt: new Date().toISOString(),
81
138
  });
82
- if (args.principalType === "user" &&
139
+ if (args.notify !== false &&
140
+ args.principalType === "user" &&
83
141
  isEmailConfigured() &&
84
142
  !isSyntheticQaEmail(args.principalId)) {
85
143
  try {
@@ -90,6 +148,10 @@ export default defineAction({
90
148
  .where(eq(reg.resourceTable.id, args.resourceId));
91
149
  const resourceTitle = resource?.[titleCol] ?? args.resourceType;
92
150
  const appUrl = getAppProductionUrl();
151
+ const resourcePath = resource && reg.getResourcePath
152
+ ? reg.getResourcePath(resource)
153
+ : undefined;
154
+ const notificationUrl = resolveShareNotificationUrl(args.resourceUrl, resourcePath, appUrl);
93
155
  const appName = process.env.APP_NAME || process.env.VITE_APP_NAME || "Agent Native";
94
156
  const subject = `${actor} shared "${resourceTitle}" with you on ${appName}`;
95
157
  const { html, text } = renderEmail({
@@ -97,9 +159,9 @@ export default defineAction({
97
159
  heading: "You've been given access",
98
160
  paragraphs: [
99
161
  `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
100
- `You can access it by visiting ${emailStrong(appName)}.`,
162
+ `Use the button below to open it. If prompted, sign in with ${emailStrong(args.principalId)}.`,
101
163
  ],
102
- cta: { label: `Open ${reg.displayName}`, url: appUrl },
164
+ cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
103
165
  footer: `You received this because ${actor} granted you ${args.role} access.`,
104
166
  });
105
167
  await sendEmail({ to: args.principalId, subject, html, text });
@@ -1 +1 @@
1
- {"version":3,"file":"share-resource.js","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\nimport { defineAction } from \"../../action.js\";\nimport { getRequestUserEmail } from \"../../server/request-context.js\";\nimport { assertAccess, ForbiddenError } from \"../access.js\";\nimport { requireShareableResource } from \"../registry.js\";\nimport { sendEmail, isEmailConfigured } from \"../../server/email.js\";\nimport { renderEmail, emailStrong } from \"../../server/email-template.js\";\nimport { getAppProductionUrl } from \"../../server/app-url.js\";\n\nexport function isSyntheticQaEmail(email: string): boolean {\n const trimmed = email.trim().toLowerCase();\n const at = trimmed.lastIndexOf(\"@\");\n if (at <= 0) return false;\n const local = trimmed.slice(0, at);\n const domain = trimmed.slice(at + 1);\n return (\n local.includes(\"+qa\") &&\n (domain === \"example.test\" ||\n domain.endsWith(\".test\") ||\n domain === \"example.invalid\" ||\n domain.endsWith(\".invalid\"))\n );\n}\n\nfunction nanoid(size = 12): string {\n const chars =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let id = \"\";\n const bytes = crypto.getRandomValues(new Uint8Array(size));\n for (const byte of bytes) id += chars[byte % chars.length];\n return id;\n}\n\nexport default defineAction({\n description:\n \"Grant a user or org access to a shareable resource. Owner or admin role required.\",\n // (audit H5) Sharing-grant operations are admin-tier and let a caller\n // expand who can read/write a resource. Refuse from the tools iframe\n // bridge so a malicious shared tool can't silently re-share its\n // viewer's resources to an attacker-controlled email.\n toolCallable: false,\n schema: z.object({\n resourceType: z\n .string()\n .describe(\"Registered resource type, e.g. 'document', 'form'.\"),\n resourceId: z.string().describe(\"Id of the resource to share.\"),\n principalType: z\n .enum([\"user\", \"org\"])\n .describe(\"'user' for an individual, 'org' for a whole organization.\"),\n principalId: z\n .string()\n .describe(\"Email (user) or org id (org) of the principal.\"),\n role: z\n .enum([\"viewer\", \"editor\", \"admin\"])\n .default(\"viewer\")\n .describe(\"Role to grant.\"),\n }),\n run: async (args) => {\n const reg = requireShareableResource(args.resourceType);\n await assertAccess(args.resourceType, args.resourceId, \"admin\");\n const actor = getRequestUserEmail();\n if (!actor) throw new ForbiddenError(\"Not signed in\");\n\n const db = reg.getDb() as any;\n const [existing] = await db\n .select()\n .from(reg.sharesTable)\n .where(\n and(\n eq(reg.sharesTable.resourceId, args.resourceId),\n eq(reg.sharesTable.principalType, args.principalType),\n eq(reg.sharesTable.principalId, args.principalId),\n ),\n );\n\n if (existing) {\n await db\n .update(reg.sharesTable)\n .set({ role: args.role })\n .where(eq(reg.sharesTable.id, existing.id));\n return { id: existing.id, updated: true };\n }\n\n const id = nanoid();\n await db.insert(reg.sharesTable).values({\n id,\n resourceId: args.resourceId,\n principalType: args.principalType,\n principalId: args.principalId,\n role: args.role,\n createdBy: actor,\n createdAt: new Date().toISOString(),\n });\n\n if (\n args.principalType === \"user\" &&\n isEmailConfigured() &&\n !isSyntheticQaEmail(args.principalId)\n ) {\n try {\n const titleCol = reg.titleColumn ?? \"title\";\n const [resource] = await db\n .select()\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, args.resourceId));\n const resourceTitle: string =\n (resource?.[titleCol] as string | undefined) ?? args.resourceType;\n const appUrl = getAppProductionUrl();\n const appName =\n process.env.APP_NAME || process.env.VITE_APP_NAME || \"Agent Native\";\n const subject = `${actor} shared \"${resourceTitle}\" with you on ${appName}`;\n const { html, text } = renderEmail({\n preheader: subject,\n heading: \"You've been given access\",\n paragraphs: [\n `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,\n `You can access it by visiting ${emailStrong(appName)}.`,\n ],\n cta: { label: `Open ${reg.displayName}`, url: appUrl },\n footer: `You received this because ${actor} granted you ${args.role} access.`,\n });\n await sendEmail({ to: args.principalId, subject, html, text });\n } catch (err) {\n console.error(\n \"[share-resource] failed to send share notification:\",\n err,\n );\n }\n }\n\n return { id, updated: false };\n },\n});\n"]}
1
+ {"version":3,"file":"share-resource.js","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\nimport { defineAction } from \"../../action.js\";\nimport { getRequestUserEmail } from \"../../server/request-context.js\";\nimport { assertAccess, ForbiddenError } from \"../access.js\";\nimport { requireShareableResource } from \"../registry.js\";\nimport { sendEmail, isEmailConfigured } from \"../../server/email.js\";\nimport { renderEmail, emailStrong } from \"../../server/email-template.js\";\nimport { getAppProductionUrl } from \"../../server/app-url.js\";\n\nexport function isSyntheticQaEmail(email: string): boolean {\n const trimmed = email.trim().toLowerCase();\n const at = trimmed.lastIndexOf(\"@\");\n if (at <= 0) return false;\n const local = trimmed.slice(0, at);\n const domain = trimmed.slice(at + 1);\n return (\n local.includes(\"+qa\") &&\n (domain === \"example.test\" ||\n domain.endsWith(\".test\") ||\n domain === \"example.invalid\" ||\n domain.endsWith(\".invalid\"))\n );\n}\n\nfunction appPath(path: string): string {\n if (!path.startsWith(\"/\")) return path;\n const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || \"\";\n const base = raw.trim().replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (!base) return path;\n const normalizedBase = `/${base}`;\n if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {\n return path;\n }\n return `${normalizedBase}${path}`;\n}\n\nfunction safeNotificationUrl(value: string, appUrl: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n\n try {\n const base = new URL(appUrl);\n if (trimmed.startsWith(\"/\")) {\n const path = appPath(trimmed);\n const basePath = base.pathname.replace(/\\/+$/, \"\");\n const alreadyIncludesBase =\n basePath && basePath !== \"/\" && path.startsWith(`${basePath}/`);\n const joined = alreadyIncludesBase\n ? `${base.origin}${path}`\n : `${appUrl.replace(/\\/+$/, \"\")}${path}`;\n return new URL(joined).toString();\n }\n\n const url = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(url.protocol)) return null;\n if (url.origin !== base.origin) return null;\n return url.toString();\n } catch {\n return null;\n }\n}\n\nexport function resolveShareNotificationUrl(\n explicitUrl: string | undefined,\n fallbackPath: string | undefined,\n appUrl = getAppProductionUrl(),\n): string {\n for (const candidate of [explicitUrl, fallbackPath]) {\n if (!candidate) continue;\n const url = safeNotificationUrl(candidate, appUrl);\n if (url) return url;\n }\n return appUrl;\n}\n\nfunction nanoid(size = 12): string {\n const chars =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let id = \"\";\n const bytes = crypto.getRandomValues(new Uint8Array(size));\n for (const byte of bytes) id += chars[byte % chars.length];\n return id;\n}\n\nexport default defineAction({\n description:\n \"Grant a user or org access to a shareable resource. Owner or admin role required.\",\n // (audit H5) Sharing-grant operations are admin-tier and let a caller\n // expand who can read/write a resource. Refuse from the tools iframe\n // bridge so a malicious shared tool can't silently re-share its\n // viewer's resources to an attacker-controlled email.\n toolCallable: false,\n schema: z.object({\n resourceType: z\n .string()\n .describe(\"Registered resource type, e.g. 'document', 'form'.\"),\n resourceId: z.string().describe(\"Id of the resource to share.\"),\n principalType: z\n .enum([\"user\", \"org\"])\n .describe(\"'user' for an individual, 'org' for a whole organization.\"),\n principalId: z\n .string()\n .describe(\"Email (user) or org id (org) of the principal.\"),\n role: z\n .enum([\"viewer\", \"editor\", \"admin\"])\n .default(\"viewer\")\n .describe(\"Role to grant.\"),\n notify: z\n .boolean()\n .default(true)\n .describe(\n \"Whether to email the user about a new individual share. Defaults to true.\",\n ),\n resourceUrl: z\n .string()\n .optional()\n .describe(\n \"Optional app-relative or same-origin URL recipients should open. External origins are ignored.\",\n ),\n }),\n run: async (args) => {\n const reg = requireShareableResource(args.resourceType);\n await assertAccess(args.resourceType, args.resourceId, \"admin\");\n const actor = getRequestUserEmail();\n if (!actor) throw new ForbiddenError(\"Not signed in\");\n\n const db = reg.getDb() as any;\n const [existing] = await db\n .select()\n .from(reg.sharesTable)\n .where(\n and(\n eq(reg.sharesTable.resourceId, args.resourceId),\n eq(reg.sharesTable.principalType, args.principalType),\n eq(reg.sharesTable.principalId, args.principalId),\n ),\n );\n\n if (existing) {\n await db\n .update(reg.sharesTable)\n .set({ role: args.role })\n .where(eq(reg.sharesTable.id, existing.id));\n return { id: existing.id, updated: true };\n }\n\n const id = nanoid();\n await db.insert(reg.sharesTable).values({\n id,\n resourceId: args.resourceId,\n principalType: args.principalType,\n principalId: args.principalId,\n role: args.role,\n createdBy: actor,\n createdAt: new Date().toISOString(),\n });\n\n if (\n args.notify !== false &&\n args.principalType === \"user\" &&\n isEmailConfigured() &&\n !isSyntheticQaEmail(args.principalId)\n ) {\n try {\n const titleCol = reg.titleColumn ?? \"title\";\n const [resource] = await db\n .select()\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, args.resourceId));\n const resourceTitle: string =\n (resource?.[titleCol] as string | undefined) ?? args.resourceType;\n const appUrl = getAppProductionUrl();\n const resourcePath =\n resource && reg.getResourcePath\n ? reg.getResourcePath(resource)\n : undefined;\n const notificationUrl = resolveShareNotificationUrl(\n args.resourceUrl,\n resourcePath,\n appUrl,\n );\n const appName =\n process.env.APP_NAME || process.env.VITE_APP_NAME || \"Agent Native\";\n const subject = `${actor} shared \"${resourceTitle}\" with you on ${appName}`;\n const { html, text } = renderEmail({\n preheader: subject,\n heading: \"You've been given access\",\n paragraphs: [\n `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,\n `Use the button below to open it. If prompted, sign in with ${emailStrong(args.principalId)}.`,\n ],\n cta: { label: `Open ${reg.displayName}`, url: notificationUrl },\n footer: `You received this because ${actor} granted you ${args.role} access.`,\n });\n await sendEmail({ to: args.principalId, subject, html, text });\n } catch (err) {\n console.error(\n \"[share-resource] failed to send share notification:\",\n err,\n );\n }\n }\n\n return { id, updated: false };\n },\n});\n"]}