@agent-native/core 0.163.4 → 0.164.3
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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
- package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
- package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
- package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
- package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
- package/corpus/templates/analytics/netlify.toml +2 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
- package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
- package/corpus/templates/assets/app/routes/picker.tsx +4 -1
- package/corpus/templates/assets/netlify.toml +2 -0
- package/corpus/templates/brain/netlify.toml +2 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
- package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
- package/corpus/templates/calendar/netlify.toml +2 -0
- package/corpus/templates/chat/netlify.toml +2 -0
- package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
- package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
- package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
- package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
- package/corpus/templates/clips/app/routes/_index.tsx +7 -3
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
- package/corpus/templates/clips/netlify.toml +2 -0
- package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
- package/corpus/templates/content/netlify.toml +2 -0
- package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
- package/corpus/templates/design/AGENTS.md +3 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
- package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
- package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
- package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
- package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
- package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
- package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
- package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
- package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
- package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
- package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
- package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
- package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
- package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
- package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
- package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
- package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
- package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
- package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
- package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
- package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
- package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
- package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
- package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
- package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
- package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
- package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
- package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
- package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
- package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
- package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
- package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
- package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
- package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
- package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
- package/corpus/templates/design/app/routes/examples.tsx +2 -1
- package/corpus/templates/design/netlify.toml +2 -0
- package/corpus/templates/design/shared/board-file.ts +2 -1
- package/corpus/templates/dispatch/netlify.toml +2 -0
- package/corpus/templates/forms/app/routes/_index.tsx +3 -2
- package/corpus/templates/forms/netlify.toml +2 -0
- package/corpus/templates/mail/app/routes/_index.tsx +3 -2
- package/corpus/templates/mail/netlify.toml +2 -0
- package/corpus/templates/plan/netlify.toml +2 -0
- package/corpus/templates/slides/netlify.toml +2 -0
- package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
- package/dist/agent/engine/builder-engine.d.ts +6 -4
- package/dist/agent/engine/builder-engine.js +91 -26
- package/dist/agent/engine/registry.js +16 -0
- package/dist/cli/code-agent-agent-tools.d.ts +2 -0
- package/dist/cli/code-agent-agent-tools.js +188 -0
- package/dist/cli/code-agent-collaboration.d.ts +34 -0
- package/dist/cli/code-agent-collaboration.js +104 -0
- package/dist/cli/code-agent-executor.js +6 -0
- package/dist/cli/code-agent-schedules.d.ts +54 -0
- package/dist/cli/code-agent-schedules.js +198 -0
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -2
- package/dist/client/EnvironmentBadge.js +6 -5
- package/dist/client/chat/run-recovery.js +3 -2
- package/dist/client/chat/tool-call-display.js +2 -3
- package/dist/client/components/CodeRequiredDialog.js +4 -2
- package/dist/client/i18n.js +5 -4
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +6 -1
- package/dist/client/settings/useBuilderStatus.js +22 -33
- package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
- package/dist/code-agents/index.d.ts +2 -0
- package/dist/code-agents/index.js +2 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +10 -4
- package/dist/localization/shared.d.ts +2 -0
- package/dist/localization/shared.js +5 -0
- package/dist/mcp-client/oauth-client.d.ts +1 -0
- package/dist/mcp-client/oauth-client.js +50 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/server/auth-marketing-locales.d.ts +7 -0
- package/dist/server/auth-marketing-locales.js +1322 -0
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +35 -6
- package/dist/server/auth.js +4 -17
- package/dist/server/builder-browser.d.ts +22 -0
- package/dist/server/builder-browser.js +77 -1
- package/dist/server/builder-oauth.d.ts +50 -0
- package/dist/server/builder-oauth.js +263 -0
- package/dist/server/core-routes-plugin.d.ts +11 -0
- package/dist/server/core-routes-plugin.js +291 -415
- package/dist/server/credential-provider.d.ts +1 -1
- package/dist/server/credential-provider.js +1 -1
- package/dist/server/csrf.js +2 -2
- package/dist/server/onboarding-html.js +52 -120
- package/dist/server/ssr-handler.js +13 -4
- package/dist/settings/store.d.ts +9 -0
- package/dist/settings/store.js +19 -0
- package/dist/shared/cache-control.d.ts +22 -0
- package/dist/shared/cache-control.js +25 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/chat/netlify.toml +2 -0
- package/docs/content/deployment.mdx +2 -0
- package/docs/content/multi-app-workspace.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/netlify.toml +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertBodySize, defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader,
|
|
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,
|
|
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
|
|
605
|
-
return
|
|
606
|
+
function isAgentNativeAnonymousOwner(email) {
|
|
607
|
+
return /^anon-[^@]+@agent-native\.com$/i.test(email ?? "");
|
|
606
608
|
}
|
|
607
|
-
function
|
|
608
|
-
return
|
|
609
|
+
export function isBuilderConnectCallbackOwner(pendingOwner, sessionOwner) {
|
|
610
|
+
return Boolean(sessionOwner && sessionOwner === pendingOwner);
|
|
609
611
|
}
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
625
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1692
|
-
//
|
|
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
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
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
|
-
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
//
|
|
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
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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, "
|
|
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
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
2227
|
-
parentOrigin
|
|
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
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
2239
|
+
...builderConnectTrackingProperties(tracking),
|
|
2357
2240
|
stage: "callback",
|
|
2358
|
-
|
|
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
|
-
|
|
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 —
|
|
2380
|
-
//
|
|
2381
|
-
//
|
|
2382
|
-
//
|
|
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
|
|
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) => {
|