@agent-native/core 0.163.3 → 0.164.1

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 (202) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
  3. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
  4. package/corpus/templates/analytics/netlify.toml +2 -0
  5. package/corpus/templates/assets/netlify.toml +2 -0
  6. package/corpus/templates/brain/netlify.toml +2 -0
  7. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
  8. package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
  9. package/corpus/templates/calendar/netlify.toml +2 -0
  10. package/corpus/templates/chat/netlify.toml +2 -0
  11. package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
  12. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
  13. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
  14. package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
  15. package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
  16. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
  17. package/corpus/templates/clips/app/routes/_index.tsx +10 -2
  18. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
  19. package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
  20. package/corpus/templates/clips/netlify.toml +2 -0
  21. package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
  22. package/corpus/templates/content/netlify.toml +2 -0
  23. package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
  24. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
  25. package/corpus/templates/design/AGENTS.md +3 -1
  26. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
  27. package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
  28. package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
  29. package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
  30. package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
  31. package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
  32. package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
  33. package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
  34. package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
  35. package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
  36. package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
  37. package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
  38. package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
  39. package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
  40. package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
  41. package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
  42. package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
  43. package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
  44. package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
  45. package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
  46. package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
  47. package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
  48. package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
  49. package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
  50. package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
  51. package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
  52. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
  53. package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
  54. package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
  55. package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
  56. package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
  57. package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
  58. package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
  59. package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
  60. package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
  61. package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
  62. package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
  63. package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
  64. package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
  65. package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
  66. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
  67. package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
  68. package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
  69. package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
  70. package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
  71. package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
  72. package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
  73. package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
  74. package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
  75. package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
  76. package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
  77. package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
  78. package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
  79. package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
  80. package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
  81. package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
  82. package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
  83. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
  84. package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
  85. package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
  86. package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
  87. package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
  88. package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
  89. package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
  90. package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
  91. package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
  92. package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
  93. package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
  94. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
  95. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
  96. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
  97. package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
  98. package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
  99. package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
  100. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
  101. package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
  102. package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
  103. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
  104. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
  105. package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
  106. package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
  107. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
  108. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
  109. package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
  110. package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
  111. package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
  112. package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
  113. package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
  114. package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
  115. package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
  116. package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
  117. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
  118. package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
  119. package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
  120. package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
  121. package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
  122. package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
  123. package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
  124. package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
  125. package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
  126. package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
  127. package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
  128. package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
  129. package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
  130. package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
  131. package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
  132. package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
  133. package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
  134. package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
  135. package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
  136. package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
  137. package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
  138. package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
  139. package/corpus/templates/design/netlify.toml +2 -0
  140. package/corpus/templates/design/shared/board-file.ts +2 -1
  141. package/corpus/templates/dispatch/netlify.toml +2 -0
  142. package/corpus/templates/forms/netlify.toml +2 -0
  143. package/corpus/templates/mail/netlify.toml +2 -0
  144. package/corpus/templates/plan/netlify.toml +2 -0
  145. package/corpus/templates/slides/netlify.toml +2 -0
  146. package/dist/agent/engine/builder-engine.d.ts +6 -4
  147. package/dist/agent/engine/builder-engine.js +91 -26
  148. package/dist/agent/engine/registry.js +16 -0
  149. package/dist/cli/code-agent-agent-tools.d.ts +2 -0
  150. package/dist/cli/code-agent-agent-tools.js +188 -0
  151. package/dist/cli/code-agent-collaboration.d.ts +34 -0
  152. package/dist/cli/code-agent-collaboration.js +104 -0
  153. package/dist/cli/code-agent-executor.js +6 -0
  154. package/dist/cli/code-agent-schedules.d.ts +54 -0
  155. package/dist/cli/code-agent-schedules.js +198 -0
  156. package/dist/client/AgentPanel.js +1 -1
  157. package/dist/client/ConnectBuilderCard.js +5 -2
  158. package/dist/client/EnvironmentBadge.js +6 -5
  159. package/dist/client/chat/run-recovery.js +3 -2
  160. package/dist/client/chat/tool-call-display.js +2 -3
  161. package/dist/client/components/CodeRequiredDialog.js +4 -2
  162. package/dist/client/i18n.js +5 -4
  163. package/dist/client/settings/SettingsPanel.js +1 -1
  164. package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
  165. package/dist/client/settings/useBuilderStatus.d.ts +6 -1
  166. package/dist/client/settings/useBuilderStatus.js +22 -33
  167. package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
  168. package/dist/code-agents/index.d.ts +2 -0
  169. package/dist/code-agents/index.js +2 -0
  170. package/dist/collab/routes.d.ts +1 -1
  171. package/dist/localization/shared.d.ts +2 -0
  172. package/dist/localization/shared.js +5 -0
  173. package/dist/mcp-client/oauth-client.d.ts +1 -0
  174. package/dist/mcp-client/oauth-client.js +50 -1
  175. package/dist/observability/routes.d.ts +1 -1
  176. package/dist/provider-api/actions/provider-api.d.ts +2 -2
  177. package/dist/resources/handlers.d.ts +1 -1
  178. package/dist/server/auth-marketing-locales.d.ts +7 -0
  179. package/dist/server/auth-marketing-locales.js +1322 -0
  180. package/dist/server/auth-marketing.d.ts +1 -0
  181. package/dist/server/auth-marketing.js +35 -6
  182. package/dist/server/auth.js +4 -17
  183. package/dist/server/builder-browser.d.ts +22 -0
  184. package/dist/server/builder-browser.js +77 -1
  185. package/dist/server/builder-oauth.d.ts +50 -0
  186. package/dist/server/builder-oauth.js +263 -0
  187. package/dist/server/core-routes-plugin.d.ts +11 -0
  188. package/dist/server/core-routes-plugin.js +291 -415
  189. package/dist/server/credential-provider.d.ts +1 -1
  190. package/dist/server/credential-provider.js +1 -1
  191. package/dist/server/csrf.js +2 -2
  192. package/dist/server/onboarding-html.js +52 -120
  193. package/dist/server/realtime-token.d.ts +1 -1
  194. package/dist/server/transcribe-voice.d.ts +1 -1
  195. package/dist/settings/store.d.ts +9 -0
  196. package/dist/settings/store.js +19 -0
  197. package/dist/templates/chat/netlify.toml +2 -0
  198. package/dist/vite/client.js +35 -37
  199. package/docs/content/deployment.mdx +2 -0
  200. package/docs/content/multi-app-workspace.mdx +1 -1
  201. package/package.json +2 -2
  202. package/src/templates/chat/netlify.toml +2 -0
@@ -1,4 +1,4 @@
1
- import { assertBodySize, defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader, getCookie, setCookie, deleteCookie, getRequestURL, getRequestIP, readRawBody, } from "h3";
1
+ import { assertBodySize, defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader, getRequestURL, getRequestIP, readRawBody, } from "h3";
2
2
  import { readMultipartFormData } from "h3";
3
3
  import { DEFAULT_MODEL } from "../agent/default-model.js";
4
4
  import { registerBuiltinEngines } from "../agent/engine/builtin.js";
@@ -21,9 +21,10 @@ import { registerBuiltinNotificationChannels } from "../notifications/channels.j
21
21
  import { createNotificationsHandler } from "../notifications/routes.js";
22
22
  import { getOrgContext } from "../org/context.js";
23
23
  import { createProgressHandler } from "../progress/routes.js";
24
+ import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
24
25
  import { registerFrameworkSecrets } from "../secrets/register-framework-secrets.js";
25
26
  import { createListSecretsHandler, createWriteSecretHandler, createTestSecretHandler, createAdHocSecretHandler, } from "../secrets/routes.js";
26
- import { getSetting, putSetting, deleteSetting } from "../settings/store.js";
27
+ import { getSetting, putSetting, deleteSetting, mutateSetting, listSettingsByPrefix, } from "../settings/store.js";
27
28
  import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
28
29
  import { ANALYTICS_CLIENT_PLATFORM_PROPERTY } from "../shared/analytics-platform.js";
29
30
  import { EMPTY_SPECULATION_RULES, resolveSsrCacheHeaders, } from "../shared/cache-control.js";
@@ -41,7 +42,8 @@ import { readAnalyticsClientPlatformHeader, readBrowserSessionIdHeader, } from "
41
42
  import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js";
42
43
  import { getAppName } from "./app-name.js";
43
44
  import { getSession } from "./auth.js";
44
- import { BUILDER_CONNECT_PARAM, BUILDER_CONNECT_OWNER_COOKIE, BUILDER_ENV_KEYS, BUILDER_OPENER_PARAM, BUILDER_RELAY_FLOW_HEADER, BUILDER_RELAY_SIGNATURE_HEADER, BUILDER_RELAY_STATE_PARAM, BUILDER_RELAY_TIMESTAMP_HEADER, BUILDER_STATE_PARAM, appendBuilderConnectToken, builderConnectTrackingProperties, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, createBuilderRelayRequest, getBuilderConnectTrackingParams, getBuilderCliAuthCallbackOriginForEvent, getBuilderBrowserOriginForEvent, resolveBuilderCallbackReturnUrl, getBuilderBrowserStatusForEvent, resolveBuilderBranchProjectId, resolveBuilderPreviewRelayParentOrigin, resolveBuilderPreviewRelayTargetOrigin, resolveSafePreviewUrl, runBuilderAgent, signBuilderCallbackState, signBuilderPreviewRelayState, verifyBuilderRelayRequest, verifyBuilderPreviewRelayStateForCallback, verifyBuilderConnectTokenAndGetOwner, verifyBuilderCallbackStateAndGetOwner, signBuilderConnectToken, } from "./builder-browser.js";
45
+ import { BUILDER_CONNECT_PARAM, BUILDER_ENV_KEYS, BUILDER_OPENER_PARAM, BUILDER_RELAY_FLOW_HEADER, BUILDER_RELAY_SIGNATURE_HEADER, BUILDER_RELAY_STATE_PARAM, BUILDER_RELAY_TIMESTAMP_HEADER, appendBuilderConnectToken, builderConnectTrackingProperties, createBuilderConnectState, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, createBuilderRelayRequest, getBuilderConnectTrackingParams, getBuilderBrowserOriginForEvent, getBuilderBrowserStatusForEvent, isBuilderConnectCallbackUrlAllowed, isSignedBuilderConnectState, resolveBuilderBranchProjectId, resolveBuilderConnectCallbackUrl, resolveBuilderPreviewRelayParentOrigin, runBuilderAgent, verifyBuilderRelayRequest, verifyBuilderPreviewRelayStateForCallback, verifyBuilderConnectTokenAndGetOwner, } from "./builder-browser.js";
46
+ import { BUILDER_OAUTH_SCOPE, deleteBuilderOAuthSession, finishBuilderOAuthAuthorization, hasBuilderOAuthSession, resolveBuilderOAuthRequestAccess, startBuilderOAuthAuthorization, } from "./builder-oauth.js";
45
47
  import { captureError, registerErrorCaptureProvider } from "./capture-error.js";
46
48
  import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
47
49
  import { canUseDeployCredentialFallbackForRequest, prefetchSecrets, readDeployCredentialEnv, resolveSecret, } from "./credential-provider.js";
@@ -601,37 +603,68 @@ async function trackBuilderLifecycle(event, name, userEmail, properties = {}) {
601
603
  ...properties,
602
604
  }, { userId: userEmail });
603
605
  }
604
- function getBuilderConnectOwnerCookiePath() {
605
- return getAppBasePath() || "/";
606
+ function isAgentNativeAnonymousOwner(email) {
607
+ return /^anon-[^@]+@agent-native\.com$/i.test(email ?? "");
606
608
  }
607
- function readBuilderConnectOwnerCookie(event) {
608
- return verifyBuilderConnectTokenAndGetOwner(getCookie(event, BUILDER_CONNECT_OWNER_COOKIE));
609
+ export function isBuilderConnectCallbackOwner(pendingOwner, sessionOwner) {
610
+ return Boolean(sessionOwner && sessionOwner === pendingOwner);
609
611
  }
610
- function setBuilderConnectOwnerCookie(event, ownerEmail) {
611
- setCookie(event, BUILDER_CONNECT_OWNER_COOKIE, signBuilderConnectToken(ownerEmail), {
612
- httpOnly: true,
613
- sameSite: "lax",
614
- secure: getOrigin(event).startsWith("https://"),
615
- path: getBuilderConnectOwnerCookiePath(),
616
- maxAge: 10 * 60,
617
- });
612
+ export async function consumeBuilderConnectPendingState(state, dependencies = { mutate: mutateSetting, remove: deleteSetting }) {
613
+ if (!isSignedBuilderConnectState(state))
614
+ return null;
615
+ const pendingKey = `builder-connect-pending:${state}`;
616
+ try {
617
+ const pending = await dependencies.mutate(pendingKey, (current) => {
618
+ if (!current || current.consumed === true) {
619
+ throw new Error("Builder connect flow is missing or consumed.");
620
+ }
621
+ return { ...current, consumed: true, consumedAt: Date.now() };
622
+ });
623
+ await dependencies.remove(pendingKey).catch(() => false); // coercion-ok: row already marked consumed; delete is cleanup
624
+ return pending;
625
+ }
626
+ catch {
627
+ // coercion-ok: missing, consumed, or unreadable pending rows all deny the
628
+ // callback the same way so attackers cannot probe storage errors.
629
+ return null;
630
+ }
618
631
  }
619
- function clearBuilderConnectOwnerCookie(event) {
620
- deleteCookie(event, BUILDER_CONNECT_OWNER_COOKIE, {
621
- path: getBuilderConnectOwnerCookiePath(),
622
- });
632
+ export async function readBuilderConnectPendingState(state, read = getSetting) {
633
+ if (!isSignedBuilderConnectState(state))
634
+ return null;
635
+ try {
636
+ const pending = await read(`builder-connect-pending:${state}`);
637
+ if (!pending || pending.consumed === true)
638
+ return null;
639
+ return pending;
640
+ }
641
+ catch {
642
+ // coercion-ok: missing, consumed, or unreadable pending rows all deny the
643
+ // callback the same way so attackers cannot probe storage errors.
644
+ return null;
645
+ }
623
646
  }
624
- function isAgentNativeAnonymousOwner(email) {
625
- return /^anon-[^@]+@agent-native\.com$/i.test(email ?? "");
647
+ const BUILDER_CONNECT_PENDING_PREFIX = "builder-connect-pending:";
648
+ export async function purgeExpiredBuilderConnectPendingStates(now = Date.now(), dependencies = { list: listSettingsByPrefix, remove: deleteSetting }) {
649
+ const rows = await dependencies.list(BUILDER_CONNECT_PENDING_PREFIX);
650
+ let deleted = 0;
651
+ for (const row of rows) {
652
+ const expiresAt = row.value.expiresAt;
653
+ if (typeof expiresAt === "number" && expiresAt > now)
654
+ continue;
655
+ const removed = await dependencies.remove(row.key).catch(() => false); // coercion-ok: expired pending cleanup is best-effort; a failed delete is retried on the next connect
656
+ if (removed)
657
+ deleted += 1;
658
+ }
659
+ return deleted;
626
660
  }
627
661
  export async function resolveBuilderOwnerContextForRequest(event, options = {}, mode) {
628
662
  const searchParams = getFrameworkRouteRequestUrl(event).searchParams;
663
+ // OAuth callback is session-bound; only the connect trampoline still uses a
664
+ // signed connect token for anonymous docs/app popup ownership.
629
665
  const signedOwner = mode === "connect"
630
666
  ? verifyBuilderConnectTokenAndGetOwner(searchParams.get(BUILDER_CONNECT_PARAM))
631
- : mode === "callback"
632
- ? verifyBuilderCallbackStateAndGetOwner(searchParams.get(BUILDER_STATE_PARAM))
633
- : null;
634
- const cookieOwner = mode === "callback" ? readBuilderConnectOwnerCookie(event) : null;
667
+ : null;
635
668
  const session = await (options.getSessionForEvent ?? getSession)(event).catch(() => null);
636
669
  if (session?.email) {
637
670
  if (signedOwner &&
@@ -656,9 +689,6 @@ export async function resolveBuilderOwnerContextForRequest(event, options = {},
656
689
  anonymous: isAgentNativeAnonymousOwner(signedOwner),
657
690
  };
658
691
  }
659
- if (cookieOwner) {
660
- return { email: cookieOwner, session: null, anonymous: false };
661
- }
662
692
  const anonymousOwner = await options.anonymousOwner?.(event);
663
693
  if (anonymousOwner) {
664
694
  return { email: anonymousOwner, session: null, anonymous: true };
@@ -1417,32 +1447,17 @@ export function createCoreRoutesPlugin(options = {}) {
1417
1447
  mountDbAdminRoutes(nitroApp, { routePrefix: P });
1418
1448
  const resolveBuilderOwnerContext = async (event, mode) => resolveBuilderOwnerContextForRequest(event, { anonymousOwner: options.anonymousOwner }, mode);
1419
1449
  const builderStatusHandler = defineEventHandler(async (event) => {
1450
+ setResponseHeader(event, "Cache-Control", "no-store");
1420
1451
  const envStatus = getBuilderBrowserStatusForEvent(event);
1421
1452
  const ownerContext = await resolveBuilderOwnerContext(event);
1422
1453
  const userEmail = ownerContext.email;
1423
1454
  const withConnectToken = (status) => {
1424
1455
  if (!userEmail)
1425
1456
  return status;
1426
- const previewOrigin = getBuilderBrowserOriginForEvent(event);
1427
- const callbackOrigin = getBuilderCliAuthCallbackOriginForEvent(event);
1428
- const statusWithConnectToken = {
1457
+ return {
1429
1458
  ...status,
1430
1459
  connectUrl: appendBuilderConnectToken(status.connectUrl, userEmail),
1431
1460
  };
1432
- // Direct cli-auth only works when the callback lands on the same
1433
- // deployment that minted the signed state. Builder/Fusion previews
1434
- // often need a gateway callback origin; in that case use the
1435
- // /builder/connect trampoline so it can write the pending-connect
1436
- // row that the gateway callback validates against.
1437
- if (previewOrigin.replace(/\/+$/, "") !==
1438
- callbackOrigin.replace(/\/+$/, "")) {
1439
- return statusWithConnectToken;
1440
- }
1441
- const cliAuthUrl = buildBuilderCliAuthUrl(callbackOrigin, signBuilderCallbackState(userEmail), { previewOrigin });
1442
- return {
1443
- ...statusWithConnectToken,
1444
- cliAuthUrl,
1445
- };
1446
1461
  };
1447
1462
  // Pass the user's active orgId so status reads can fall back to
1448
1463
  // org-scoped credentials and branch project IDs. Without it, an
@@ -1504,6 +1519,77 @@ export function createCoreRoutesPlugin(options = {}) {
1504
1519
  catch {
1505
1520
  // settings store unavailable — fall through
1506
1521
  }
1522
+ if (userEmail) {
1523
+ let oauthAccess = null;
1524
+ let hasOAuthCustody = false;
1525
+ try {
1526
+ hasOAuthCustody = await hasBuilderOAuthSession(userEmail);
1527
+ }
1528
+ catch {
1529
+ return withConnectToken({
1530
+ ...requestStatus,
1531
+ configured: false,
1532
+ credentialSource: "user",
1533
+ privateKeyConfigured: false,
1534
+ publicKeyConfigured: false,
1535
+ connectError: {
1536
+ message: "Builder connection status could not be read. Retry in a moment.",
1537
+ at: Date.now(),
1538
+ },
1539
+ });
1540
+ }
1541
+ if (hasOAuthCustody) {
1542
+ try {
1543
+ oauthAccess = await resolveBuilderOAuthRequestAccess({
1544
+ ownerEmail: userEmail,
1545
+ requiredScope: BUILDER_OAUTH_SCOPE,
1546
+ });
1547
+ }
1548
+ catch {
1549
+ oauthAccess = null;
1550
+ }
1551
+ }
1552
+ if (oauthAccess) {
1553
+ let privateKeyConfigured = false;
1554
+ let publicKeyConfigured = false;
1555
+ let orgName = "Builder OAuth";
1556
+ try {
1557
+ const { resolveBuilderCredentials } = await import("./credential-provider.js");
1558
+ const creds = await resolveBuilderCredentials();
1559
+ privateKeyConfigured = !!creds.privateKey;
1560
+ publicKeyConfigured = !!creds.publicKey;
1561
+ if (typeof creds.orgName === "string" && creds.orgName) {
1562
+ orgName = creds.orgName;
1563
+ }
1564
+ }
1565
+ catch {
1566
+ // coercion-ok: OAuth already proves the chat gateway; missing
1567
+ // key flags only hide code-change send until secrets are readable.
1568
+ }
1569
+ return withConnectToken({
1570
+ ...requestStatus,
1571
+ configured: true,
1572
+ credentialSource: "user",
1573
+ privateKeyConfigured,
1574
+ publicKeyConfigured,
1575
+ orgName,
1576
+ spaces: [],
1577
+ });
1578
+ }
1579
+ if (hasOAuthCustody) {
1580
+ return withConnectToken({
1581
+ ...requestStatus,
1582
+ configured: false,
1583
+ credentialSource: "user",
1584
+ privateKeyConfigured: false,
1585
+ publicKeyConfigured: false,
1586
+ connectError: {
1587
+ message: "Builder access expired. Reconnect Builder.io.",
1588
+ at: Date.now(),
1589
+ },
1590
+ });
1591
+ }
1592
+ }
1507
1593
  // Read request-scoped Builder credentials first; deploy env is only
1508
1594
  // the fallback. This keeps a root/local BUILDER_PRIVATE_KEY from
1509
1595
  // blocking a user from connecting their own Builder account.
@@ -1530,7 +1616,7 @@ export function createCoreRoutesPlugin(options = {}) {
1530
1616
  isFreeAccount: undefined,
1531
1617
  credentialSource: credentialSource ?? undefined,
1532
1618
  // Surface durable credential rejection separately from
1533
- // one-shot cli-auth callback failures. The reconnect UI keeps
1619
+ // one-shot OAuth callback failures. The reconnect UI keeps
1534
1620
  // polling through authError while the user chooses a new
1535
1621
  // Builder space; connectError means the active callback itself
1536
1622
  // failed and should stop the flow.
@@ -1630,7 +1716,7 @@ export function createCoreRoutesPlugin(options = {}) {
1630
1716
  });
1631
1717
  mountBuilderStatusRouteAliases((path, handler) => getH3App(nitroApp).use(path, handler), P, builderStatusHandler);
1632
1718
  // How long a pending-connect row is valid. Must be long enough for
1633
- // the user to complete the Builder CLI-auth flow, but short enough
1719
+ // the user to complete the Builder OAuth flow, but short enough
1634
1720
  // that a stale row from an abandoned attempt doesn't accept a new
1635
1721
  // callback minutes later.
1636
1722
  const BUILDER_CONNECT_PENDING_TTL_MS = 10 * 60 * 1000; // 10 min
@@ -1667,7 +1753,7 @@ export function createCoreRoutesPlugin(options = {}) {
1667
1753
  // session-bound so the worst case degrades to the prior behavior.
1668
1754
  return true;
1669
1755
  }
1670
- // Lightweight 302 to the Builder CLI-auth URL. Lets clients do
1756
+ // Lightweight 302 to Builder's authorization endpoint. Lets clients do
1671
1757
  // `window.open('/_agent-native/builder/connect', '_blank')` synchronously
1672
1758
  // inside a click handler, avoiding the popup-blocker downgrade that
1673
1759
  // happens when an await sits before window.open.
@@ -1684,13 +1770,12 @@ export function createCoreRoutesPlugin(options = {}) {
1684
1770
  // request with the navigation context. We allow `same-origin` or
1685
1771
  // `none` (typed/bookmark/extension); cross-site / same-site without
1686
1772
  // a valid connect token are rejected.
1687
- // 3. Pending row keyed by session email + bound nonce — the callback
1773
+ // 3. Pending row keyed by signed OAuth state — the callback
1688
1774
  // requires both a valid session and a one-time row that this
1689
1775
  // handler wrote during the same flow. Without the same-origin
1690
1776
  // gate or connect token above, an attacker could prime the row from
1691
- // cross-site and then trick the victim into hitting a callback URL
1692
- // with attacker-controlled p-key/api-key, hijacking the victim's
1693
- // account.
1777
+ // cross-site and then trick the victim into completing an
1778
+ // attacker-initiated authorization flow.
1694
1779
  getH3App(nitroApp).use(`${P}/builder/connect`, defineEventHandler(async (event) => {
1695
1780
  const ownerContext = await resolveBuilderOwnerContext(event, "connect");
1696
1781
  const ownerEmail = ownerContext.email;
@@ -1698,6 +1783,16 @@ export function createCoreRoutesPlugin(options = {}) {
1698
1783
  setResponseStatus(event, 401);
1699
1784
  return { error: "Authentication required" };
1700
1785
  }
1786
+ if (ownerContext.anonymous ||
1787
+ isAgentNativeAnonymousOwner(ownerEmail)) {
1788
+ setResponseStatus(event, 401);
1789
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1790
+ return createBuilderBrowserCallbackErrorPage("Sign in to connect Builder.", {
1791
+ title: "Sign in required",
1792
+ body: "Builder OAuth is tied to a signed-in account. Sign in, then try Connect Builder again.",
1793
+ parentOrigin: getBuilderBrowserOriginForEvent(event),
1794
+ });
1795
+ }
1701
1796
  const requestUrl = getFrameworkRouteRequestUrl(event);
1702
1797
  const connectToken = requestUrl.searchParams.get(BUILDER_CONNECT_PARAM);
1703
1798
  const connectTokenOwner = verifyBuilderConnectTokenAndGetOwner(connectToken);
@@ -1751,65 +1846,42 @@ export function createCoreRoutesPlugin(options = {}) {
1751
1846
  catch {
1752
1847
  // No prior error row — fine
1753
1848
  }
1754
- const previewOrigin = getBuilderBrowserOriginForEvent(event).replace(/\/+$/, "");
1755
- const callbackOrigin = getBuilderCliAuthCallbackOriginForEvent(event).replace(/\/+$/, "");
1756
- let relay;
1757
- if (previewOrigin !== callbackOrigin) {
1758
- try {
1759
- relay = signBuilderPreviewRelayState({
1760
- ownerEmail,
1761
- targetOrigin: resolveBuilderPreviewRelayTargetOrigin(previewOrigin),
1762
- basePath: getAppBasePath(),
1763
- });
1764
- }
1765
- catch (err) {
1766
- const msg = err instanceof Error
1767
- ? err.message
1768
- : "Builder preview relay is not configured.";
1769
- setResponseStatus(event, 503);
1770
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1771
- return createBuilderBrowserCallbackErrorPage(msg, {
1772
- title: "Builder preview connection isn't configured",
1773
- body: "This preview needs its secure Builder callback relay configured before authorization can start.",
1774
- closeHint: "Close this popup and ask the preview owner to finish Builder relay setup.",
1775
- parentOrigin: previewOrigin,
1776
- });
1777
- }
1778
- }
1779
- let pendingOrgId = null;
1780
- let pendingRole = null;
1781
- if (!ownerContext.anonymous) {
1782
- try {
1783
- const orgContext = await getOrgContext(event);
1784
- pendingOrgId = orgContext.orgId ?? null;
1785
- pendingRole = orgContext.role ?? null;
1786
- }
1787
- catch {
1788
- // The pending owner remains user-scoped when org context is absent.
1789
- }
1849
+ const callbackUrl = resolveBuilderConnectCallbackUrl(event);
1850
+ if (!callbackUrl ||
1851
+ !isBuilderConnectCallbackUrlAllowed(callbackUrl, event)) {
1852
+ const msg = "This deployment cannot start Builder OAuth from the current origin.";
1853
+ setResponseStatus(event, 400);
1854
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1855
+ return createBuilderBrowserCallbackErrorPage(msg, {
1856
+ title: "Builder connection is unavailable here",
1857
+ body: "Open this app on its public HTTPS URL, then try again.",
1858
+ parentOrigin: getBuilderBrowserOriginForEvent(event),
1859
+ });
1790
1860
  }
1791
- // Store a short-lived pending row. If the DB is unavailable we
1792
- // surface a popup-renderable error page that signals the parent
1793
- // via BroadcastChannel, rather than letting the popup show raw
1794
- // JSON and the parent poll for 5 minutes.
1861
+ const state = createBuilderConnectState();
1862
+ // The standard OAuth client discovers Builder's protected-resource
1863
+ // metadata, dynamically registers, and creates its S256 verifier.
1864
+ // Persist that opaque protocol state encrypted and consume it once.
1865
+ let oauthFlow;
1866
+ let authorizationUrl;
1795
1867
  try {
1796
- if (relay) {
1797
- await putSetting(builderRelayPendingKey(relay.payload.flowId), {
1798
- ownerEmail,
1799
- orgId: pendingOrgId,
1800
- role: pendingRole,
1801
- targetOrigin: relay.payload.targetOrigin,
1802
- basePath: relay.payload.basePath,
1803
- expiresAt: relay.payload.exp,
1804
- tracking: connectTracking,
1805
- });
1806
- }
1807
- else {
1808
- await putSetting(`builder-pending-connect:${ownerEmail}`, {
1809
- expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
1810
- tracking: connectTracking,
1811
- });
1812
- }
1868
+ const started = await startBuilderOAuthAuthorization({
1869
+ ownerEmail,
1870
+ redirectUri: callbackUrl,
1871
+ state,
1872
+ });
1873
+ oauthFlow = started.pending;
1874
+ authorizationUrl = started.authorizationUrl;
1875
+ await putSetting(`builder-connect-pending:${state}`, {
1876
+ ownerEmail,
1877
+ orgId: null,
1878
+ role: null,
1879
+ encryptedOAuthFlow: encryptSecretValue(JSON.stringify(oauthFlow)),
1880
+ redirectUri: callbackUrl,
1881
+ expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
1882
+ tracking: connectTracking,
1883
+ });
1884
+ await purgeExpiredBuilderConnectPendingStates().catch(() => 0); // coercion-ok: connect already persisted the new pending row; purge of abandoned rows must not fail OAuth start
1813
1885
  }
1814
1886
  catch (err) {
1815
1887
  await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
@@ -1836,18 +1908,9 @@ export function createCoreRoutesPlugin(options = {}) {
1836
1908
  stage: "connect",
1837
1909
  connect_token_owner_matches_context: !connectTokenOwner || connectTokenOwner === ownerEmail,
1838
1910
  });
1839
- setBuilderConnectOwnerCookie(event, ownerEmail);
1840
- // The primary UI now opens the signed Builder /cli-auth URL directly
1841
- // from /builder/status. Keep this legacy trampoline working for older
1842
- // clients, but still send it to Builder immediately and include signed
1843
- // callback state so the callback does not depend on popup cookies.
1844
- const cliAuthUrl = buildBuilderCliAuthUrl(callbackOrigin, signBuilderCallbackState(ownerEmail), {
1845
- previewOrigin,
1846
- relayState: relay?.state,
1847
- tracking: connectTracking,
1848
- });
1849
1911
  setResponseStatus(event, 302);
1850
- setResponseHeader(event, "Location", cliAuthUrl);
1912
+ setResponseHeader(event, "Cache-Control", "no-store");
1913
+ setResponseHeader(event, "Location", authorizationUrl);
1851
1914
  return "";
1852
1915
  }));
1853
1916
  getH3App(nitroApp).use(`${P}/builder/run`, defineEventHandler(async (event) => {
@@ -2017,6 +2080,7 @@ export function createCoreRoutesPlugin(options = {}) {
2017
2080
  // propagation even though the second hop carries secrets only in
2018
2081
  // its authenticated POST body.
2019
2082
  setResponseHeader(event, "Cache-Control", "no-store");
2083
+ setResponseHeader(event, "Pragma", "no-cache");
2020
2084
  setResponseHeader(event, "Referrer-Policy", "no-referrer");
2021
2085
  const requestUrl = getFrameworkRouteRequestUrl(event);
2022
2086
  const relayStateRaw = requestUrl.searchParams.get(BUILDER_RELAY_STATE_PARAM);
@@ -2085,301 +2149,104 @@ export function createCoreRoutesPlugin(options = {}) {
2085
2149
  setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
2086
2150
  return createBuilderBrowserCallbackPage(`${relayParentOrigin}${relayPayload.basePath || "/"}`, { parentOrigin: relayParentOrigin });
2087
2151
  }
2088
- // A real session or a template-approved anonymous owner is required;
2089
- // the pending-row check below (combined with the same-origin gate on
2090
- // /builder/connect) blocks CSRF and callback replay.
2091
- const ownerContext = await resolveBuilderOwnerContext(event, "callback");
2092
- const ownerEmail = ownerContext.email;
2093
- // Diagnostic: log the resolver's inputs for debugging "No active
2094
- // connect flow found" reports. Reveals session-vs-state owner
2095
- // mismatches and missing/forged _an_state without leaking the
2096
- // signed token itself.
2097
- try {
2098
- const debugSearch = getFrameworkRouteRequestUrl(event).searchParams;
2099
- const stateRaw = debugSearch.get(BUILDER_STATE_PARAM);
2100
- const stateOwnerProbe = verifyBuilderCallbackStateAndGetOwner(stateRaw);
2101
- const session = await getSession(event).catch(() => null);
2102
- console.log(`[builder-callback] resolved-owner=${ownerEmail ?? "(none)"} session-email=${session?.email ?? "(none)"} state-owner=${stateOwnerProbe ?? "(none)"} state-present=${Boolean(stateRaw)} anon=${ownerContext.anonymous} host=${getHeader(event, "host") ?? "(none)"} sec-fetch-site=${getHeader(event, "sec-fetch-site") ?? "(none)"} origin=${getHeader(event, "origin") ?? "(none)"} referer=${getHeader(event, "referer") ?? "(none)"}`);
2103
- }
2104
- catch {
2105
- // Diagnostic logging is best-effort; do not break the callback.
2106
- }
2107
- if (!ownerEmail) {
2108
- setResponseStatus(event, 401);
2109
- return { error: "Authentication required" };
2110
- }
2111
- clearBuilderConnectOwnerCookie(event);
2112
- let connectTracking = getBuilderConnectTrackingParams(requestUrl.searchParams);
2113
- // postMessage from the callback success/error pages must target the
2114
- // original preview opener, not the callback server. On the fallback
2115
- // path the callback is served from the env-configured gateway while
2116
- // the opener lives on the preview origin. Three sources of opener
2117
- // origin, in priority order:
2118
- // 1. `_an_opener` — written into the callback URL's query by
2119
- // buildBuilderCliAuthUrl when cli-auth's allow-list forced
2120
- // preview_url onto the gateway. Survives Builder's redirect
2121
- // verbatim (Builder preserves redirect_url's query string).
2122
- // 2. `preview-url` — Builder echoes the top-level preview_url back
2123
- // as a query param on the callback. Reflects the gateway on
2124
- // the fallback path, but matches the opener on the happy path.
2125
- // 3. The event's own origin — last-resort fallback.
2126
- const openerOriginFromQuery = requestUrl.searchParams.get(BUILDER_OPENER_PARAM);
2127
- const callbackParentOrigin = resolveSafePreviewUrl(openerOriginFromQuery, event) ||
2128
- resolveSafePreviewUrl(requestUrl.searchParams.get("preview-url"), event) ||
2129
- getBuilderBrowserOriginForEvent(event);
2130
- const callbackStateOwner = verifyBuilderCallbackStateAndGetOwner(requestUrl.searchParams.get(BUILDER_STATE_PARAM));
2131
- const hasValidCallbackState = callbackStateOwner === ownerEmail;
2132
- // Verify either:
2133
- // 1. the signed callback state embedded in redirect_url by
2134
- // /builder/status (primary flow), or
2135
- // 2. the server-side pending-connect row written by the legacy
2136
- // /builder/connect trampoline.
2137
- //
2138
- // For the pending-row path, delete must succeed before we proceed;
2139
- // otherwise a DB blip
2140
- // leaves the row in place and the same callback URL can be
2141
- // replayed against the same session for up to 10 minutes (the
2142
- // TTL window). Treat a delete failure as a hard failure: the
2143
- // user retries, the next /builder/connect call rewrites the
2144
- // pending row.
2145
- let pendingValid = hasValidCallbackState;
2146
- let pendingError = null;
2147
- try {
2148
- const pending = (await getSetting(`builder-pending-connect:${ownerEmail}`));
2149
- if (pending?.tracking) {
2150
- connectTracking = {
2151
- signupSource: connectTracking.signupSource ?? pending.tracking.signupSource,
2152
- agentNativeFlow: connectTracking.agentNativeFlow ??
2153
- pending.tracking.agentNativeFlow,
2154
- agentNativeConnectSource: connectTracking.agentNativeConnectSource ??
2155
- pending.tracking.agentNativeConnectSource,
2156
- agentNativeApp: connectTracking.agentNativeApp ??
2157
- pending.tracking.agentNativeApp,
2158
- agentNativeTemplate: connectTracking.agentNativeTemplate ??
2159
- pending.tracking.agentNativeTemplate,
2160
- };
2161
- }
2162
- if (pending &&
2163
- typeof pending.expiresAt === "number" &&
2164
- Date.now() < pending.expiresAt) {
2165
- try {
2166
- await deleteSetting(`builder-pending-connect:${ownerEmail}`);
2167
- pendingValid = true;
2168
- }
2169
- catch (err) {
2170
- if (!hasValidCallbackState) {
2171
- pendingError =
2172
- "Could not consume pending-connect token (storage error). Please retry.";
2173
- console.error("[builder] deleteSetting failed for pending-connect — refusing to proceed (replay risk):", err?.message ?? err);
2174
- }
2175
- }
2176
- }
2177
- }
2178
- catch {
2179
- // DB temporarily unavailable — treat as missing.
2180
- }
2181
- if (pendingError) {
2182
- await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
2183
- ...builderConnectTrackingProperties(connectTracking),
2184
- reason: "pending_consume_storage_error",
2185
- stage: "callback",
2186
- });
2187
- // Best-effort signal to the parent's poll loop, then render the
2188
- // popup-friendly error page so the BroadcastChannel notify fires.
2189
- await putSetting(`builder-connect-error:${ownerEmail}`, {
2190
- message: pendingError,
2191
- at: Date.now(),
2192
- }).catch(() => { });
2193
- setResponseStatus(event, 503);
2194
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
2195
- return createBuilderBrowserCallbackErrorPage(pendingError, {
2196
- parentOrigin: callbackParentOrigin,
2197
- });
2198
- }
2199
- if (!pendingValid) {
2200
- // Diagnostic: log the exact reason pendingValid is false so we can
2201
- // distinguish "state didn't validate" from "no pending row" in
2202
- // production "No active connect flow found" reports.
2203
- console.warn(`[builder-callback] pending-invalid owner=${ownerEmail} has-state-param=${Boolean(requestUrl.searchParams.get(BUILDER_STATE_PARAM))} state-validated=${hasValidCallbackState} pending-error=${pendingError ?? "(none)"}`);
2204
- await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
2205
- ...builderConnectTrackingProperties(connectTracking),
2206
- reason: hasValidCallbackState
2207
- ? "callback_state_unexpectedly_rejected"
2208
- : "missing_pending_connect",
2209
- stage: "callback",
2210
- has_callback_state: Boolean(requestUrl.searchParams.get(BUILDER_STATE_PARAM)),
2211
- });
2212
- const msg = "No active connect flow found. Restart the Builder connect flow from Settings.";
2213
- // Write an error signal so the polling loop in the parent tab
2214
- // terminates quickly instead of waiting 5 minutes for the timeout.
2215
- try {
2152
+ const state = requestUrl.searchParams.get("state");
2153
+ const parentOrigin = getBuilderBrowserOriginForEvent(event);
2154
+ const fail = async (status, message, ownerEmail, reason, tracking = {}) => {
2155
+ if (ownerEmail) {
2216
2156
  await putSetting(`builder-connect-error:${ownerEmail}`, {
2217
- message: msg,
2157
+ message,
2218
2158
  at: Date.now(),
2159
+ }).catch(() => { });
2160
+ await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
2161
+ ...builderConnectTrackingProperties(tracking),
2162
+ reason: reason ?? "callback_failed",
2163
+ stage: "callback",
2219
2164
  });
2220
2165
  }
2221
- catch {
2222
- // DB unavailable — parent will time out naturally.
2223
- }
2224
- setResponseStatus(event, 403);
2166
+ setResponseStatus(event, status);
2225
2167
  setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
2226
- return createBuilderBrowserCallbackErrorPage(msg, {
2227
- parentOrigin: callbackParentOrigin,
2228
- });
2229
- }
2230
- const privateKey = requestUrl.searchParams.get("p-key");
2231
- const publicKey = requestUrl.searchParams.get("api-key");
2232
- if (!privateKey || !publicKey) {
2233
- await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
2234
- ...builderConnectTrackingProperties(connectTracking),
2235
- reason: "missing_credentials",
2236
- stage: "callback",
2237
- });
2238
- // Render the popup-friendly error page (and write a status row)
2239
- // instead of bare JSON, so the parent tab's poll loop terminates
2240
- // immediately via BroadcastChannel rather than hanging until the
2241
- // 5-minute timeout.
2242
- const msg = "Builder didn't return credentials. Restart the connect flow from settings.";
2243
- await putSetting(`builder-connect-error:${ownerEmail}`, {
2244
- message: msg,
2245
- at: Date.now(),
2246
- }).catch(() => { });
2247
- setResponseStatus(event, 400);
2248
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
2249
- return createBuilderBrowserCallbackErrorPage(msg, {
2250
- parentOrigin: callbackParentOrigin,
2168
+ return createBuilderBrowserCallbackErrorPage(message, {
2169
+ parentOrigin,
2251
2170
  });
2171
+ };
2172
+ if (!state || !isSignedBuilderConnectState(state)) {
2173
+ return fail(403, "No active Builder connect flow found. Restart the connection from Settings.");
2174
+ }
2175
+ const pending = await readBuilderConnectPendingState(state);
2176
+ if (!pending) {
2177
+ return fail(403, "No active Builder connect flow found. Restart the connection from Settings.");
2178
+ }
2179
+ const ownerEmail = typeof pending.ownerEmail === "string" ? pending.ownerEmail : null;
2180
+ const tracking = pending.tracking && typeof pending.tracking === "object"
2181
+ ? pending.tracking
2182
+ : {};
2183
+ const session = await getSession(event).catch(() => null); // coercion-ok: callback treats session read failure as unauthenticated
2184
+ const expiresAt = typeof pending.expiresAt === "number" ? pending.expiresAt : 0;
2185
+ const encryptedOAuthFlow = typeof pending.encryptedOAuthFlow === "string"
2186
+ ? pending.encryptedOAuthFlow
2187
+ : null;
2188
+ const redirectUri = typeof pending.redirectUri === "string"
2189
+ ? pending.redirectUri
2190
+ : null;
2191
+ const expectedRedirectUri = resolveBuilderConnectCallbackUrl(event);
2192
+ if (!ownerEmail ||
2193
+ !isBuilderConnectCallbackOwner(ownerEmail, session?.email) ||
2194
+ Date.now() >= expiresAt ||
2195
+ !encryptedOAuthFlow ||
2196
+ !redirectUri ||
2197
+ !expectedRedirectUri ||
2198
+ redirectUri !== expectedRedirectUri ||
2199
+ !isBuilderConnectCallbackUrlAllowed(redirectUri, event)) {
2200
+ if (Date.now() >= expiresAt) {
2201
+ await deleteSetting(`builder-connect-pending:${state}`).catch(() => false); // coercion-ok: expired pending cleanup is best-effort; callback already failed verification
2202
+ }
2203
+ return fail(403, "Builder connect callback could not be verified. Restart the connection.", ownerEmail ?? undefined, "callback_verification_failed", tracking);
2204
+ }
2205
+ const consumed = await consumeBuilderConnectPendingState(state);
2206
+ if (!consumed) {
2207
+ return fail(403, "No active Builder connect flow found. Restart the connection from Settings.", ownerEmail, "callback_verification_failed", tracking);
2208
+ }
2209
+ const denied = requestUrl.searchParams.get("error");
2210
+ const code = requestUrl.searchParams.get("code");
2211
+ const iss = requestUrl.searchParams.get("iss") ?? undefined;
2212
+ if (denied || !code) {
2213
+ return fail(400, denied
2214
+ ? "Builder connection was cancelled."
2215
+ : "Builder did not return an authorization code.", ownerEmail, denied ? "authorization_denied" : "missing_code", tracking);
2252
2216
  }
2253
- const userId = requestUrl.searchParams.get("user-id");
2254
- const orgName = requestUrl.searchParams.get("org-name");
2255
- const orgKind = requestUrl.searchParams.get("kind");
2256
- const subscription = requestUrl.searchParams.get("subscription");
2257
- const subscriptionLevel = requestUrl.searchParams.get("subscription-level");
2258
- const subscriptionName = requestUrl.searchParams.get("subscription-name");
2259
- const isEnterprise = parseBuilderCallbackBoolean(requestUrl.searchParams.get("is-enterprise"));
2260
- const isFreeAccount = parseBuilderCallbackBoolean(requestUrl.searchParams.get("is-free-account"));
2261
- // Store per-user in app_secrets so each user's Builder connection
2262
- // is independent. No more shared env vars that the last connector
2263
- // overwrites.
2264
- //
2265
- // Failure handling: a silent catch here (returning the success page
2266
- // anyway) was Midhun's bug on 2026-04-28 — popup said "yay", parent
2267
- // window polled `/builder/status` for 5 minutes seeing
2268
- // configured:false, never got a real error. Now we surface the
2269
- // failure two ways: (a) a settings row that the next /builder/status
2270
- // poll picks up, and (b) postMessage from the error page itself,
2271
- // wired into the popup HTML, so the parent stops polling immediately.
2272
- let writeError = null;
2273
2217
  try {
2274
- const { writeBuilderCredentials } = await import("./credential-provider.js");
2275
- // Resolve the user's active org / role so the credentials land
2276
- // at org scope when an owner/admin is connecting (everyone in
2277
- // the org auto-resolves them on next chat call). Members and
2278
- // users with no active org silently fall back to user scope.
2279
- // Failure to read org context is non-fatal — we just keep the
2280
- // legacy per-user behaviour for that connection.
2281
- let orgId = null;
2282
- let role = null;
2283
- if (!ownerContext.anonymous) {
2284
- try {
2285
- const { getOrgContext } = await import("../org/context.js");
2286
- const orgCtx = await getOrgContext(event);
2287
- orgId = orgCtx.orgId ?? null;
2288
- role = orgCtx.role ?? null;
2289
- }
2290
- catch {
2291
- /* org module not present in this template — keep user scope */
2292
- }
2293
- }
2294
- const target = await writeBuilderCredentials(ownerEmail, {
2295
- privateKey,
2296
- publicKey,
2297
- userId,
2298
- orgName,
2299
- orgKind,
2300
- subscription,
2301
- subscriptionLevel,
2302
- subscriptionName,
2303
- isEnterprise,
2304
- isFreeAccount,
2305
- }, { orgId, role });
2306
- console.log(`[builder-connect] wrote credentials email=${ownerEmail} requestOrgId=${orgId ?? "(none)"} role=${role ?? "(none)"} scope=${target.scope} scopeId=${target.scopeId}`);
2307
- }
2308
- catch (err) {
2309
- writeError = err?.message ?? String(err);
2310
- console.error("[builder] Failed to persist Builder credentials:", writeError);
2311
- }
2312
- if (writeError) {
2313
- await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
2314
- ...builderConnectTrackingProperties(connectTracking),
2315
- reason: "credential_write_failed",
2316
- stage: "callback",
2317
- });
2318
- // Best-effort signal to /builder/status. If putSetting also fails
2319
- // (entire DB unreachable) the popup's postMessage still notifies
2320
- // the parent. If both fail the parent times out at 5min as today.
2321
- try {
2322
- await putSetting(`builder-connect-error:${ownerEmail}`, {
2323
- message: writeError,
2324
- at: Date.now(),
2325
- });
2326
- }
2327
- catch (settingsErr) {
2328
- console.error("[builder] Couldn't even record connect-error to settings:", settingsErr?.message ?? settingsErr);
2329
- }
2330
- setResponseStatus(event, 500);
2331
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
2332
- return createBuilderBrowserCallbackErrorPage(writeError, {
2333
- parentOrigin: callbackParentOrigin,
2218
+ const oauthFlow = JSON.parse(decryptSecretValue(encryptedOAuthFlow));
2219
+ await finishBuilderOAuthAuthorization({
2220
+ ownerEmail,
2221
+ code,
2222
+ iss,
2223
+ pending: oauthFlow,
2334
2224
  });
2335
2225
  }
2336
- // Clear any legacy disconnect flag and any prior connect-error row
2337
- // (so a successful retry doesn't surface the previous failure).
2338
- try {
2339
- await deleteSetting("builder-disconnected");
2340
- }
2341
2226
  catch {
2342
- // DB not ready — proceed
2227
+ return fail(502, "Builder could not exchange the authorization code. Restart the connection.", ownerEmail, "code_exchange_failed", tracking);
2343
2228
  }
2344
2229
  try {
2345
- await deleteSetting(`builder-connect-error:${ownerEmail}`);
2230
+ await Promise.all([
2231
+ deleteSetting("builder-disconnected").catch(() => false), // coercion-ok: best-effort cleanup after successful OAuth save
2232
+ deleteSetting(`builder-connect-error:${ownerEmail}`).catch(() => false),
2233
+ ]);
2346
2234
  }
2347
2235
  catch {
2348
- // No prior error row — fine
2236
+ return fail(500, "Builder credentials could not be saved. Restart the connection.", ownerEmail, "credential_write_failed", tracking);
2349
2237
  }
2350
- const previewUrl = resolveBuilderCallbackReturnUrl({
2351
- event,
2352
- openerOrigin: openerOriginFromQuery,
2353
- previewUrl: requestUrl.searchParams.get("preview-url"),
2354
- });
2355
2238
  await trackBuilderLifecycle(event, "builder connect succeeded", ownerEmail, {
2356
- ...builderConnectTrackingProperties(connectTracking),
2239
+ ...builderConnectTrackingProperties(tracking),
2357
2240
  stage: "callback",
2358
- has_preview_url: Boolean(previewUrl),
2359
- org_kind: orgKind || undefined,
2360
- subscription: subscription || undefined,
2361
- subscription_level: subscriptionLevel || undefined,
2362
- subscription_name: subscriptionName || undefined,
2363
- is_enterprise: isEnterprise ?? undefined,
2364
- is_free_account: isFreeAccount ?? undefined,
2241
+ credential_scope: "user",
2365
2242
  });
2366
2243
  setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
2367
- // The parent (opener) is the original preview surface that started the
2368
- // connect flow, NOT the callback server's own origin — when the
2369
- // env-configured gateway is used as the callback fallback (because
2370
- // Builder rejects the preview host), the callback server and the
2371
- // opener live on different origins, and postMessage to the gateway
2372
- // origin would be dropped by the preview opener. callbackParentOrigin
2373
- // is the precomputed best-available opener origin (`_an_opener` →
2374
- // `preview-url` → event origin).
2375
- return createBuilderBrowserCallbackPage(previewUrl, {
2376
- parentOrigin: callbackParentOrigin,
2377
- });
2244
+ return createBuilderBrowserCallbackPage(`${parentOrigin}${getAppBasePath() || "/"}`, { parentOrigin });
2378
2245
  }));
2379
- // POST /_agent-native/builder/disconnect — revoke the user's per-user
2380
- // or org-scoped Builder credentials in app_secrets. Deploy-level env
2381
- // credentials are never mutated here; if env is configured it remains as
2382
- // the fallback after request-scoped credentials are removed.
2246
+ // POST /_agent-native/builder/disconnect — remove this user's OAuth
2247
+ // custody. Legacy BUILDER_* secrets are cleared at user scope when OAuth
2248
+ // was present, so an admin disconnect cannot delete the org-wide keys.
2249
+ // A legacy-only disconnect still uses the owner/admin org write gate.
2383
2250
  getH3App(nitroApp).use(`${P}/builder/disconnect`, defineEventHandler(async (event) => {
2384
2251
  if (getMethod(event) !== "POST") {
2385
2252
  setResponseStatus(event, 405);
@@ -2390,26 +2257,37 @@ export function createCoreRoutesPlugin(options = {}) {
2390
2257
  setResponseStatus(event, 401);
2391
2258
  return { error: "unauthorized" };
2392
2259
  }
2393
- const { deleteBuilderCredentials } = await import("./credential-provider.js");
2394
- // Mirror the connect-side scope decision so disconnect undoes
2395
- // exactly what connect wrote: owner/admin connections land at
2396
- // org scope and tear down at org scope; member or no-org
2397
- // connections stay user-scoped on both ends. Symmetric, so a
2398
- // single Disconnect press always reverses what the same user's
2399
- // Connect press did.
2400
- let orgId = null;
2401
- let role = null;
2402
- try {
2403
- const { getOrgContext } = await import("../org/context.js");
2404
- const orgCtx = await getOrgContext(event);
2405
- orgId = orgCtx.orgId ?? null;
2406
- role = orgCtx.role ?? null;
2407
- }
2408
- catch {
2409
- /* org module not present — keep user scope */
2410
- }
2411
2260
  try {
2412
- await deleteBuilderCredentials(session.email, { orgId, role });
2261
+ const hadOAuth = await hasBuilderOAuthSession(session.email);
2262
+ const oauthResult = hadOAuth
2263
+ ? await deleteBuilderOAuthSession(session.email)
2264
+ : { localDeleted: false, remoteRevoked: false };
2265
+ const { deleteBuilderCredentials } = await import("./credential-provider.js");
2266
+ let orgId = null;
2267
+ let role = null;
2268
+ try {
2269
+ const { getOrgContext } = await import("../org/context.js");
2270
+ const orgCtx = await getOrgContext(event);
2271
+ orgId = orgCtx.orgId ?? null;
2272
+ role = orgCtx.role ?? null;
2273
+ }
2274
+ catch {
2275
+ // coercion-ok: org module is optional; disconnect still clears user-scoped custody.
2276
+ }
2277
+ await deleteBuilderCredentials(session.email, hadOAuth ? undefined : { orgId, role });
2278
+ await trackBuilderLifecycle(event, "builder disconnect succeeded", session.email, {
2279
+ oauth_present: hadOAuth,
2280
+ remote_revoked: hadOAuth
2281
+ ? oauthResult.remoteRevoked
2282
+ : undefined,
2283
+ });
2284
+ return {
2285
+ ok: true,
2286
+ remoteRevoked: hadOAuth ? oauthResult.remoteRevoked : undefined,
2287
+ warning: hadOAuth && !oauthResult.remoteRevoked
2288
+ ? "Local Builder access was removed, but remote revocation could not be confirmed."
2289
+ : undefined,
2290
+ };
2413
2291
  }
2414
2292
  catch (err) {
2415
2293
  await trackBuilderLifecycle(event, "builder disconnect failed", session.email, {
@@ -2422,8 +2300,6 @@ export function createCoreRoutesPlugin(options = {}) {
2422
2300
  cause: err instanceof Error ? err.message : String(err),
2423
2301
  };
2424
2302
  }
2425
- await trackBuilderLifecycle(event, "builder disconnect succeeded", session.email);
2426
- return { ok: true };
2427
2303
  }));
2428
2304
  // Proxy to Builder's agents-run API for background code changes.
2429
2305
  getH3App(nitroApp).use(`${P}/builder/agents-run`, defineEventHandler(async (event) => {