@agent-native/core 0.101.7 → 0.101.11
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/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +23 -1
- package/corpus/core/src/a2a/handlers.ts +65 -1
- package/corpus/core/src/a2a/server.ts +124 -0
- package/corpus/core/src/a2a/task-store.ts +253 -0
- package/corpus/core/src/a2a/types.ts +26 -0
- package/corpus/core/src/agent/production-agent.ts +5 -2
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
- package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
- package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
- package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
- package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
- package/corpus/core/src/mcp/builtin-tools.ts +25 -1
- package/corpus/core/src/scripts/call-agent.ts +22 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +86 -1
- package/corpus/core/src/server/auth.ts +25 -1
- package/corpus/core/src/server/builder-browser.ts +353 -1
- package/corpus/core/src/server/core-routes-plugin.ts +601 -223
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
- package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
- package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
- package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +23 -0
- package/corpus/templates/content/AGENTS.md +3 -2
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
- package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
- package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
- package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
- package/corpus/templates/content/actions/_database-utils.ts +89 -12
- package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
- package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
- package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
- package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
- package/corpus/templates/content/app/components/editor/MathRenderer.tsx +43 -0
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +260 -4
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
- package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
- package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
- package/corpus/templates/content/app/components/editor/extensions/NotionExtensions.tsx +78 -0
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
- package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
- package/corpus/templates/content/app/global.css +71 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
- package/corpus/templates/content/app/i18n/zh-TW.ts +38 -0
- package/corpus/templates/content/app/i18n-data.ts +450 -0
- package/corpus/templates/content/app/root.tsx +2 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
- package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
- package/corpus/templates/content/changelog/2026-07-14-latex-equations-render-in-documents-public-pages-and-exports.md +6 -0
- package/corpus/templates/content/package.json +1 -0
- package/corpus/templates/content/parity/matrix.md +27 -25
- package/corpus/templates/content/parity/matrix.ts +50 -2
- package/corpus/templates/content/server/db/schema.ts +47 -0
- package/corpus/templates/content/server/plugins/auth.ts +1 -0
- package/corpus/templates/content/server/plugins/db.ts +53 -0
- package/corpus/templates/content/shared/api.ts +70 -1
- package/corpus/templates/content/shared/builder-mdx.ts +705 -15
- package/corpus/templates/content/shared/document-export.ts +296 -25
- package/corpus/templates/content/shared/inline-math.ts +128 -0
- package/corpus/templates/content/shared/math-rendering.ts +50 -0
- package/corpus/templates/content/shared/nfm.ts +20 -17
- package/corpus/templates/content/vite.config.ts +0 -1
- package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
- package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
- package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/design/app/i18n/en-US.ts +1 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
- package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
- package/corpus/templates/design/shared/review-anchor.ts +48 -0
- package/corpus/templates/mail/.agents/skills/email-drafts/SKILL.md +17 -0
- package/corpus/templates/mail/AGENTS.md +21 -19
- package/corpus/templates/mail/actions/create-attachment-upload.ts +49 -0
- package/corpus/templates/mail/changelog/2026-07-14-connected-agents-can-now-securely-upload-local-files-for-mai.md +6 -0
- package/corpus/templates/mail/server/handlers/media.ts +84 -61
- package/corpus/templates/mail/server/lib/attachment-upload-ticket.ts +234 -0
- package/corpus/templates/mail/server/lib/mail-connector-catalog.ts +8 -4
- package/corpus/templates/mail/server/lib/media-upload.ts +114 -0
- package/corpus/templates/mail/server/plugins/auth.ts +9 -1
- package/corpus/templates/mail/server/routes/api/media/attachment-upload/[uploadId].put.ts +1 -0
- package/corpus/templates/plan/server/plan-mdx.ts +53 -7
- package/dist/a2a/client.d.ts +5 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +18 -1
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +57 -8
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +87 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +27 -0
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +216 -0
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +24 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +5 -2
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
- package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
- package/dist/client/review/ReviewThreadPanel.js +6 -4
- package/dist/client/review/ReviewThreadPanel.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +11 -4
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +8 -4
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +19 -6
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
- package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
- package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/mcp/builtin-tools.d.ts.map +1 -1
- package/dist/mcp/builtin-tools.js +22 -3
- package/dist/mcp/builtin-tools.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +19 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +71 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +19 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +78 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +246 -1
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +41 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +260 -14
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
- package/docs/content/deployment.mdx +9 -7
- package/docs/content/locales/ar-SA/deployment.mdx +9 -7
- package/docs/content/locales/de-DE/deployment.mdx +9 -7
- package/docs/content/locales/es-ES/deployment.mdx +9 -7
- package/docs/content/locales/fr-FR/deployment.mdx +9 -7
- package/docs/content/locales/hi-IN/deployment.mdx +9 -7
- package/docs/content/locales/ja-JP/deployment.mdx +9 -7
- package/docs/content/locales/ko-KR/deployment.mdx +9 -7
- package/docs/content/locales/pt-BR/deployment.mdx +9 -7
- package/docs/content/locales/zh-CN/deployment.mdx +9 -7
- package/docs/content/locales/zh-TW/deployment.mdx +9 -7
- package/package.json +2 -2
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +25 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
assertBodySize,
|
|
2
3
|
defineEventHandler,
|
|
3
4
|
setResponseStatus,
|
|
4
5
|
setResponseHeader,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
deleteCookie,
|
|
10
11
|
getRequestURL,
|
|
11
12
|
getRequestIP,
|
|
13
|
+
readRawBody,
|
|
12
14
|
} from "h3";
|
|
13
15
|
import type { H3Event } from "h3";
|
|
14
16
|
import { readMultipartFormData } from "h3";
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
runDatabaseSchemaHealthCheck,
|
|
54
56
|
type DatabaseSchemaHealthResult,
|
|
55
57
|
} from "../db/runtime-diagnostics.js";
|
|
58
|
+
import { ssrfSafeFetch } from "../extensions/url-safety.js";
|
|
56
59
|
import {
|
|
57
60
|
uploadFile,
|
|
58
61
|
getActiveFileUploadProviderForRequest,
|
|
@@ -107,12 +110,17 @@ import {
|
|
|
107
110
|
BUILDER_CONNECT_OWNER_COOKIE,
|
|
108
111
|
BUILDER_ENV_KEYS,
|
|
109
112
|
BUILDER_OPENER_PARAM,
|
|
113
|
+
BUILDER_RELAY_FLOW_HEADER,
|
|
114
|
+
BUILDER_RELAY_SIGNATURE_HEADER,
|
|
115
|
+
BUILDER_RELAY_STATE_PARAM,
|
|
116
|
+
BUILDER_RELAY_TIMESTAMP_HEADER,
|
|
110
117
|
BUILDER_STATE_PARAM,
|
|
111
118
|
appendBuilderConnectToken,
|
|
112
119
|
builderConnectTrackingProperties,
|
|
113
120
|
buildBuilderCliAuthUrl,
|
|
114
121
|
createBuilderBrowserCallbackErrorPage,
|
|
115
122
|
createBuilderBrowserCallbackPage,
|
|
123
|
+
createBuilderRelayRequest,
|
|
116
124
|
getBuilderConnectTrackingParams,
|
|
117
125
|
getBuilderCliAuthCallbackOriginForEvent,
|
|
118
126
|
getBuilderBrowserOriginForEvent,
|
|
@@ -122,10 +130,15 @@ import {
|
|
|
122
130
|
resolveSafePreviewUrl,
|
|
123
131
|
runBuilderAgent,
|
|
124
132
|
signBuilderCallbackState,
|
|
133
|
+
signBuilderPreviewRelayState,
|
|
134
|
+
verifyBuilderRelayRequest,
|
|
135
|
+
verifyBuilderPreviewRelayStateForCallback,
|
|
125
136
|
verifyBuilderConnectTokenAndGetOwner,
|
|
126
137
|
verifyBuilderCallbackStateAndGetOwner,
|
|
127
138
|
signBuilderConnectToken,
|
|
128
139
|
type BuilderConnectTrackingParams,
|
|
140
|
+
type BuilderRelayCredentials,
|
|
141
|
+
type BuilderPreviewRelayState,
|
|
129
142
|
} from "./builder-browser.js";
|
|
130
143
|
import { captureError } from "./capture-error.js";
|
|
131
144
|
import {
|
|
@@ -336,6 +349,21 @@ export function resolveFrameworkSseRoutes(sseRoute?: string): string[] {
|
|
|
336
349
|
);
|
|
337
350
|
}
|
|
338
351
|
|
|
352
|
+
export const BUILDER_STATUS_ROUTE_SUFFIXES = [
|
|
353
|
+
"/builder/status",
|
|
354
|
+
"/connection-status/builder",
|
|
355
|
+
] as const;
|
|
356
|
+
|
|
357
|
+
export function mountBuilderStatusRouteAliases<T>(
|
|
358
|
+
mount: (path: string, handler: T) => void,
|
|
359
|
+
prefix: string,
|
|
360
|
+
handler: T,
|
|
361
|
+
): void {
|
|
362
|
+
for (const routeSuffix of BUILDER_STATUS_ROUTE_SUFFIXES) {
|
|
363
|
+
mount(`${prefix}${routeSuffix}`, handler);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
339
367
|
registerBuiltinEngines();
|
|
340
368
|
|
|
341
369
|
function cleanBuilderWaitlistText(
|
|
@@ -848,6 +876,141 @@ export function getFrameworkRouteRequestUrl(event: H3Event): URL {
|
|
|
848
876
|
return url;
|
|
849
877
|
}
|
|
850
878
|
|
|
879
|
+
export interface BuilderRelayPendingRecord {
|
|
880
|
+
ownerEmail: string;
|
|
881
|
+
orgId: string | null;
|
|
882
|
+
role: string | null;
|
|
883
|
+
targetOrigin: string;
|
|
884
|
+
basePath: string;
|
|
885
|
+
expiresAt: number;
|
|
886
|
+
tracking?: BuilderConnectTrackingParams;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export interface ConsumeBuilderRelayDependencies {
|
|
890
|
+
getPending: (key: string) => Promise<Record<string, unknown> | null>;
|
|
891
|
+
deletePending: (key: string) => Promise<boolean>;
|
|
892
|
+
writeCredentials: (
|
|
893
|
+
ownerEmail: string,
|
|
894
|
+
credentials: BuilderRelayCredentials,
|
|
895
|
+
scope: { orgId: string | null; role: string | null },
|
|
896
|
+
) => Promise<unknown>;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function builderRelayPendingKey(flowId: string): string {
|
|
900
|
+
return `builder-pending-relay:${flowId}`;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function parseBuilderRelayPendingRecord(
|
|
904
|
+
value: Record<string, unknown> | null,
|
|
905
|
+
): BuilderRelayPendingRecord | null {
|
|
906
|
+
if (
|
|
907
|
+
!value ||
|
|
908
|
+
typeof value.ownerEmail !== "string" ||
|
|
909
|
+
typeof value.targetOrigin !== "string" ||
|
|
910
|
+
typeof value.basePath !== "string" ||
|
|
911
|
+
typeof value.expiresAt !== "number"
|
|
912
|
+
) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
ownerEmail: value.ownerEmail,
|
|
917
|
+
orgId: typeof value.orgId === "string" ? value.orgId : null,
|
|
918
|
+
role: typeof value.role === "string" ? value.role : null,
|
|
919
|
+
targetOrigin: value.targetOrigin,
|
|
920
|
+
basePath: value.basePath,
|
|
921
|
+
expiresAt: value.expiresAt,
|
|
922
|
+
tracking:
|
|
923
|
+
value.tracking && typeof value.tracking === "object"
|
|
924
|
+
? (value.tracking as BuilderConnectTrackingParams)
|
|
925
|
+
: undefined,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Authenticated one-shot receiver for the second hop of Builder preview auth.
|
|
931
|
+
* Owner and org scope always come from the preview's pending record; the
|
|
932
|
+
* corporate callback cannot choose them in its POST body.
|
|
933
|
+
*/
|
|
934
|
+
export async function consumeBuilderRelayRequest(
|
|
935
|
+
input: {
|
|
936
|
+
rawBody: string;
|
|
937
|
+
timestamp: string | null | undefined;
|
|
938
|
+
flowId: string | null | undefined;
|
|
939
|
+
signature: string | null | undefined;
|
|
940
|
+
requestOrigin: string;
|
|
941
|
+
requestBasePath: string;
|
|
942
|
+
now?: number;
|
|
943
|
+
},
|
|
944
|
+
dependencies: ConsumeBuilderRelayDependencies,
|
|
945
|
+
): Promise<{ ok: true } | { ok: false; status: number; error: string }> {
|
|
946
|
+
if (input.rawBody.length > 64 * 1024) {
|
|
947
|
+
return {
|
|
948
|
+
ok: false,
|
|
949
|
+
status: 413,
|
|
950
|
+
error: "Builder relay request is too large",
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
let verified: ReturnType<typeof verifyBuilderRelayRequest>;
|
|
954
|
+
try {
|
|
955
|
+
verified = verifyBuilderRelayRequest({
|
|
956
|
+
body: input.rawBody,
|
|
957
|
+
timestamp: input.timestamp,
|
|
958
|
+
flowId: input.flowId,
|
|
959
|
+
signature: input.signature,
|
|
960
|
+
requestOrigin: input.requestOrigin,
|
|
961
|
+
requestBasePath: input.requestBasePath,
|
|
962
|
+
now: input.now,
|
|
963
|
+
});
|
|
964
|
+
} catch {
|
|
965
|
+
return { ok: false, status: 503, error: "Builder relay is not configured" };
|
|
966
|
+
}
|
|
967
|
+
if (!verified) {
|
|
968
|
+
return { ok: false, status: 401, error: "Invalid Builder relay request" };
|
|
969
|
+
}
|
|
970
|
+
const pendingKey = builderRelayPendingKey(verified.payload.flowId);
|
|
971
|
+
const pending = parseBuilderRelayPendingRecord(
|
|
972
|
+
await dependencies.getPending(pendingKey).catch(() => null),
|
|
973
|
+
);
|
|
974
|
+
const now = input.now ?? Date.now();
|
|
975
|
+
if (
|
|
976
|
+
!pending ||
|
|
977
|
+
pending.expiresAt < now ||
|
|
978
|
+
pending.ownerEmail !== verified.payload.ownerEmail ||
|
|
979
|
+
pending.targetOrigin !== verified.payload.targetOrigin ||
|
|
980
|
+
pending.basePath !== verified.payload.basePath
|
|
981
|
+
) {
|
|
982
|
+
return { ok: false, status: 403, error: "No active Builder relay flow" };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// A successful delete, not merely a resolved promise, is the one-shot gate.
|
|
986
|
+
// It happens before credential persistence so replay is impossible even if
|
|
987
|
+
// the downstream write fails and the human has to start a fresh flow.
|
|
988
|
+
const consumed = await dependencies
|
|
989
|
+
.deletePending(pendingKey)
|
|
990
|
+
.catch(() => false);
|
|
991
|
+
if (consumed !== true) {
|
|
992
|
+
return {
|
|
993
|
+
ok: false,
|
|
994
|
+
status: 409,
|
|
995
|
+
error: "Builder relay flow was already consumed",
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
await dependencies.writeCredentials(
|
|
1000
|
+
pending.ownerEmail,
|
|
1001
|
+
verified.body.credentials,
|
|
1002
|
+
{ orgId: pending.orgId, role: pending.role },
|
|
1003
|
+
);
|
|
1004
|
+
return { ok: true };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export async function readBuilderRelayRequestBody(
|
|
1008
|
+
event: H3Event,
|
|
1009
|
+
): Promise<string> {
|
|
1010
|
+
await assertBodySize(event, 64 * 1024);
|
|
1011
|
+
return (await readRawBody(event, "utf8")) ?? "";
|
|
1012
|
+
}
|
|
1013
|
+
|
|
851
1014
|
function redactValues(text: string, values: Array<string | null | undefined>) {
|
|
852
1015
|
let out = text;
|
|
853
1016
|
for (const value of values) {
|
|
@@ -1436,128 +1599,79 @@ export function createCoreRoutesPlugin(
|
|
|
1436
1599
|
mode,
|
|
1437
1600
|
);
|
|
1438
1601
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
signBuilderCallbackState(userEmail),
|
|
1473
|
-
{ previewOrigin },
|
|
1474
|
-
);
|
|
1475
|
-
return {
|
|
1476
|
-
...statusWithConnectToken,
|
|
1477
|
-
cliAuthUrl,
|
|
1478
|
-
};
|
|
1602
|
+
const builderStatusHandler = defineEventHandler(async (event) => {
|
|
1603
|
+
const envStatus = getBuilderBrowserStatusForEvent(event);
|
|
1604
|
+
const ownerContext = await resolveBuilderOwnerContext(event);
|
|
1605
|
+
const userEmail = ownerContext.email;
|
|
1606
|
+
const withConnectToken = <T extends { connectUrl: string }>(
|
|
1607
|
+
status: T,
|
|
1608
|
+
): T & { cliAuthUrl?: string } => {
|
|
1609
|
+
if (!userEmail) return status;
|
|
1610
|
+
const previewOrigin = getBuilderBrowserOriginForEvent(event);
|
|
1611
|
+
const callbackOrigin = getBuilderCliAuthCallbackOriginForEvent(event);
|
|
1612
|
+
const statusWithConnectToken = {
|
|
1613
|
+
...status,
|
|
1614
|
+
connectUrl: appendBuilderConnectToken(status.connectUrl, userEmail),
|
|
1615
|
+
} as T & { cliAuthUrl?: string };
|
|
1616
|
+
// Direct cli-auth only works when the callback lands on the same
|
|
1617
|
+
// deployment that minted the signed state. Builder/Fusion previews
|
|
1618
|
+
// often need a gateway callback origin; in that case use the
|
|
1619
|
+
// /builder/connect trampoline so it can write the pending-connect
|
|
1620
|
+
// row that the gateway callback validates against.
|
|
1621
|
+
if (
|
|
1622
|
+
previewOrigin.replace(/\/+$/, "") !==
|
|
1623
|
+
callbackOrigin.replace(/\/+$/, "")
|
|
1624
|
+
) {
|
|
1625
|
+
return statusWithConnectToken;
|
|
1626
|
+
}
|
|
1627
|
+
const cliAuthUrl = buildBuilderCliAuthUrl(
|
|
1628
|
+
callbackOrigin,
|
|
1629
|
+
signBuilderCallbackState(userEmail),
|
|
1630
|
+
{ previewOrigin },
|
|
1631
|
+
);
|
|
1632
|
+
return {
|
|
1633
|
+
...statusWithConnectToken,
|
|
1634
|
+
cliAuthUrl,
|
|
1479
1635
|
};
|
|
1636
|
+
};
|
|
1480
1637
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
}
|
|
1638
|
+
// Pass the user's active orgId so status reads can fall back to
|
|
1639
|
+
// org-scoped credentials and branch project IDs. Without it, an
|
|
1640
|
+
// admin's org-scope OAuth result is invisible to every other org
|
|
1641
|
+
// member's status poller and the UI would show "not connected" forever
|
|
1642
|
+
// even though the chat actually resolves the org-shared credential.
|
|
1643
|
+
let orgId: string | null = null;
|
|
1644
|
+
if (!ownerContext.anonymous) {
|
|
1645
|
+
try {
|
|
1646
|
+
const { getOrgContext } = await import("../org/context.js");
|
|
1647
|
+
const orgCtx = await getOrgContext(event);
|
|
1648
|
+
orgId = orgCtx.orgId ?? null;
|
|
1649
|
+
} catch {
|
|
1650
|
+
/* org module not present in this template — keep userEmail-only */
|
|
1495
1651
|
}
|
|
1652
|
+
}
|
|
1496
1653
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
// Surface a recent OAuth callback failure before reporting a
|
|
1509
|
-
// deployment fallback as "connected"; otherwise a failed personal
|
|
1510
|
-
// connect attempt on a deploy that also has BUILDER_PRIVATE_KEY set
|
|
1511
|
-
// looks successful even though the user's credentials were not saved.
|
|
1512
|
-
try {
|
|
1513
|
-
if (userEmail) {
|
|
1514
|
-
const errKey = `builder-connect-error:${userEmail}`;
|
|
1515
|
-
const errRow = await getSetting(errKey);
|
|
1516
|
-
if (errRow && typeof errRow.message === "string") {
|
|
1517
|
-
await deleteSetting(errKey).catch(() => {});
|
|
1518
|
-
return withConnectToken({
|
|
1519
|
-
...requestStatus,
|
|
1520
|
-
configured: false,
|
|
1521
|
-
privateKeyConfigured: false,
|
|
1522
|
-
publicKeyConfigured: false,
|
|
1523
|
-
userId: undefined,
|
|
1524
|
-
orgName: undefined,
|
|
1525
|
-
orgKind: undefined,
|
|
1526
|
-
subscription: undefined,
|
|
1527
|
-
subscriptionLevel: undefined,
|
|
1528
|
-
subscriptionName: undefined,
|
|
1529
|
-
isEnterprise: undefined,
|
|
1530
|
-
isFreeAccount: undefined,
|
|
1531
|
-
connectError: {
|
|
1532
|
-
message: errRow.message as string,
|
|
1533
|
-
at:
|
|
1534
|
-
typeof errRow.at === "number"
|
|
1535
|
-
? (errRow.at as number)
|
|
1536
|
-
: Date.now(),
|
|
1537
|
-
},
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
} catch {
|
|
1542
|
-
// settings store unavailable — fall through
|
|
1543
|
-
}
|
|
1654
|
+
return runWithRequestContext(
|
|
1655
|
+
{ userEmail, orgId: orgId ?? undefined },
|
|
1656
|
+
async () => {
|
|
1657
|
+
const projectId = await resolveBuilderBranchProjectId();
|
|
1658
|
+
const requestStatus = {
|
|
1659
|
+
...envStatus,
|
|
1660
|
+
builderEnabled: !!projectId,
|
|
1661
|
+
branchProjectIdConfigured: !!projectId,
|
|
1662
|
+
branchProjectId: projectId || undefined,
|
|
1663
|
+
};
|
|
1544
1664
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
resolveBuilderCredentials(),
|
|
1556
|
-
resolveBuilderCredentialSource(),
|
|
1557
|
-
]);
|
|
1558
|
-
const authFailure =
|
|
1559
|
-
await getBuilderCredentialAuthFailure(creds);
|
|
1560
|
-
if (authFailure) {
|
|
1665
|
+
// Surface a recent OAuth callback failure before reporting a
|
|
1666
|
+
// deployment fallback as "connected"; otherwise a failed personal
|
|
1667
|
+
// connect attempt on a deploy that also has BUILDER_PRIVATE_KEY set
|
|
1668
|
+
// looks successful even though the user's credentials were not saved.
|
|
1669
|
+
try {
|
|
1670
|
+
if (userEmail) {
|
|
1671
|
+
const errKey = `builder-connect-error:${userEmail}`;
|
|
1672
|
+
const errRow = await getSetting(errKey);
|
|
1673
|
+
if (errRow && typeof errRow.message === "string") {
|
|
1674
|
+
await deleteSetting(errKey).catch(() => {});
|
|
1561
1675
|
return withConnectToken({
|
|
1562
1676
|
...requestStatus,
|
|
1563
1677
|
configured: false,
|
|
@@ -1571,113 +1685,157 @@ export function createCoreRoutesPlugin(
|
|
|
1571
1685
|
subscriptionName: undefined,
|
|
1572
1686
|
isEnterprise: undefined,
|
|
1573
1687
|
isFreeAccount: undefined,
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
authError: {
|
|
1581
|
-
message: authFailure.message,
|
|
1582
|
-
at: authFailure.at,
|
|
1688
|
+
connectError: {
|
|
1689
|
+
message: errRow.message as string,
|
|
1690
|
+
at:
|
|
1691
|
+
typeof errRow.at === "number"
|
|
1692
|
+
? (errRow.at as number)
|
|
1693
|
+
: Date.now(),
|
|
1583
1694
|
},
|
|
1584
1695
|
});
|
|
1585
1696
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1697
|
+
}
|
|
1698
|
+
} catch {
|
|
1699
|
+
// settings store unavailable — fall through
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// Read request-scoped Builder credentials first; deploy env is only
|
|
1703
|
+
// the fallback. This keeps a root/local BUILDER_PRIVATE_KEY from
|
|
1704
|
+
// blocking a user from connecting their own Builder account.
|
|
1705
|
+
try {
|
|
1706
|
+
const {
|
|
1707
|
+
resolveBuilderCredentials,
|
|
1708
|
+
resolveBuilderCredentialSource,
|
|
1709
|
+
getBuilderCredentialAuthFailure,
|
|
1710
|
+
} = await import("./credential-provider.js");
|
|
1711
|
+
const [creds, credentialSource] = await Promise.all([
|
|
1712
|
+
resolveBuilderCredentials(),
|
|
1713
|
+
resolveBuilderCredentialSource(),
|
|
1714
|
+
]);
|
|
1715
|
+
const authFailure = await getBuilderCredentialAuthFailure(creds);
|
|
1716
|
+
if (authFailure) {
|
|
1717
|
+
return withConnectToken({
|
|
1718
|
+
...requestStatus,
|
|
1719
|
+
configured: false,
|
|
1720
|
+
privateKeyConfigured: false,
|
|
1721
|
+
publicKeyConfigured: false,
|
|
1722
|
+
userId: undefined,
|
|
1723
|
+
orgName: undefined,
|
|
1724
|
+
orgKind: undefined,
|
|
1725
|
+
subscription: undefined,
|
|
1726
|
+
subscriptionLevel: undefined,
|
|
1727
|
+
subscriptionName: undefined,
|
|
1728
|
+
isEnterprise: undefined,
|
|
1729
|
+
isFreeAccount: undefined,
|
|
1730
|
+
credentialSource: credentialSource ?? undefined,
|
|
1731
|
+
// Surface durable credential rejection separately from
|
|
1732
|
+
// one-shot cli-auth callback failures. The reconnect UI keeps
|
|
1733
|
+
// polling through authError while the user chooses a new
|
|
1734
|
+
// Builder space; connectError means the active callback itself
|
|
1735
|
+
// failed and should stop the flow.
|
|
1736
|
+
authError: {
|
|
1737
|
+
message: authFailure.message,
|
|
1738
|
+
at: authFailure.at,
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
if (creds.privateKey && creds.publicKey) {
|
|
1743
|
+
// Best-effort: surface the real space name(s) from Builder's
|
|
1744
|
+
// Admin API. Stay NON-BLOCKING — return whatever is cached now
|
|
1745
|
+
// and refresh in the background for the next poll. Falls back
|
|
1746
|
+
// to orgName until the cache warms.
|
|
1747
|
+
let spaces: Array<{ id: string; name: string }> | undefined;
|
|
1748
|
+
try {
|
|
1749
|
+
const { getCachedBuilderSpaces, listBuilderSpaces } =
|
|
1750
|
+
await import("./builder-space.js");
|
|
1751
|
+
const privateKey = creds.privateKey;
|
|
1752
|
+
const cachedSpaces = getCachedBuilderSpaces(privateKey);
|
|
1753
|
+
if (cachedSpaces && cachedSpaces.length > 0) {
|
|
1754
|
+
spaces = cachedSpaces;
|
|
1606
1755
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
orgName: creds.orgName || envStatus.orgName,
|
|
1614
|
-
spaces,
|
|
1615
|
-
orgKind: creds.orgKind || envStatus.orgKind,
|
|
1616
|
-
subscription:
|
|
1617
|
-
creds.subscription || envStatus.subscription || undefined,
|
|
1618
|
-
subscriptionLevel:
|
|
1619
|
-
creds.subscriptionLevel ||
|
|
1620
|
-
envStatus.subscriptionLevel ||
|
|
1621
|
-
undefined,
|
|
1622
|
-
subscriptionName:
|
|
1623
|
-
creds.subscriptionName ||
|
|
1624
|
-
envStatus.subscriptionName ||
|
|
1625
|
-
undefined,
|
|
1626
|
-
isEnterprise:
|
|
1627
|
-
creds.isEnterprise ?? envStatus.isEnterprise ?? undefined,
|
|
1628
|
-
isFreeAccount:
|
|
1629
|
-
creds.isFreeAccount ??
|
|
1630
|
-
envStatus.isFreeAccount ??
|
|
1631
|
-
undefined,
|
|
1632
|
-
credentialSource: credentialSource ?? undefined,
|
|
1633
|
-
});
|
|
1756
|
+
if (!cachedSpaces) {
|
|
1757
|
+
// Warm the cache without blocking this response.
|
|
1758
|
+
void listBuilderSpaces(privateKey).catch(() => {});
|
|
1759
|
+
}
|
|
1760
|
+
} catch {
|
|
1761
|
+
// Admin API helper unavailable — leave spaces undefined.
|
|
1634
1762
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1763
|
+
return withConnectToken({
|
|
1764
|
+
...requestStatus,
|
|
1765
|
+
configured: true,
|
|
1766
|
+
privateKeyConfigured: true,
|
|
1767
|
+
publicKeyConfigured: !!creds.publicKey,
|
|
1768
|
+
userId: creds.userId || envStatus.userId,
|
|
1769
|
+
orgName: creds.orgName || envStatus.orgName,
|
|
1770
|
+
spaces,
|
|
1771
|
+
orgKind: creds.orgKind || envStatus.orgKind,
|
|
1772
|
+
subscription:
|
|
1773
|
+
creds.subscription || envStatus.subscription || undefined,
|
|
1774
|
+
subscriptionLevel:
|
|
1775
|
+
creds.subscriptionLevel ||
|
|
1776
|
+
envStatus.subscriptionLevel ||
|
|
1777
|
+
undefined,
|
|
1778
|
+
subscriptionName:
|
|
1779
|
+
creds.subscriptionName ||
|
|
1780
|
+
envStatus.subscriptionName ||
|
|
1781
|
+
undefined,
|
|
1782
|
+
isEnterprise:
|
|
1783
|
+
creds.isEnterprise ?? envStatus.isEnterprise ?? undefined,
|
|
1784
|
+
isFreeAccount:
|
|
1785
|
+
creds.isFreeAccount ?? envStatus.isFreeAccount ?? undefined,
|
|
1786
|
+
credentialSource: credentialSource ?? undefined,
|
|
1787
|
+
});
|
|
1637
1788
|
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
// Secrets table not ready — fall through to env status
|
|
1791
|
+
}
|
|
1638
1792
|
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
}
|
|
1658
|
-
} catch {
|
|
1659
|
-
// DB not reachable
|
|
1793
|
+
// Honor legacy disconnect flag for existing deployments.
|
|
1794
|
+
try {
|
|
1795
|
+
const disconnected = await getSetting("builder-disconnected");
|
|
1796
|
+
if (disconnected) {
|
|
1797
|
+
return withConnectToken({
|
|
1798
|
+
...requestStatus,
|
|
1799
|
+
configured: false,
|
|
1800
|
+
privateKeyConfigured: false,
|
|
1801
|
+
publicKeyConfigured: false,
|
|
1802
|
+
userId: undefined,
|
|
1803
|
+
orgName: undefined,
|
|
1804
|
+
orgKind: undefined,
|
|
1805
|
+
subscription: undefined,
|
|
1806
|
+
subscriptionLevel: undefined,
|
|
1807
|
+
subscriptionName: undefined,
|
|
1808
|
+
isEnterprise: undefined,
|
|
1809
|
+
isFreeAccount: undefined,
|
|
1810
|
+
});
|
|
1660
1811
|
}
|
|
1661
|
-
|
|
1662
|
-
//
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1812
|
+
} catch {
|
|
1813
|
+
// DB not reachable
|
|
1814
|
+
}
|
|
1815
|
+
// No env, no per-user creds → not configured. Both authenticated
|
|
1816
|
+
// and unauthenticated callers see "not connected" so they can
|
|
1817
|
+
// run through the OAuth flow.
|
|
1818
|
+
return withConnectToken({
|
|
1819
|
+
...requestStatus,
|
|
1820
|
+
configured: false,
|
|
1821
|
+
privateKeyConfigured: false,
|
|
1822
|
+
publicKeyConfigured: false,
|
|
1823
|
+
userId: undefined,
|
|
1824
|
+
orgName: undefined,
|
|
1825
|
+
orgKind: undefined,
|
|
1826
|
+
subscription: undefined,
|
|
1827
|
+
subscriptionLevel: undefined,
|
|
1828
|
+
subscriptionName: undefined,
|
|
1829
|
+
isEnterprise: undefined,
|
|
1830
|
+
isFreeAccount: undefined,
|
|
1831
|
+
});
|
|
1832
|
+
},
|
|
1833
|
+
);
|
|
1834
|
+
});
|
|
1835
|
+
mountBuilderStatusRouteAliases(
|
|
1836
|
+
(path, handler) => getH3App(nitroApp).use(path, handler),
|
|
1837
|
+
P,
|
|
1838
|
+
builderStatusHandler,
|
|
1681
1839
|
);
|
|
1682
1840
|
|
|
1683
1841
|
// How long a pending-connect row is valid. Must be long enough for
|
|
@@ -1827,15 +1985,77 @@ export function createCoreRoutesPlugin(
|
|
|
1827
1985
|
// No prior error row — fine
|
|
1828
1986
|
}
|
|
1829
1987
|
|
|
1988
|
+
const previewOrigin = getBuilderBrowserOriginForEvent(event).replace(
|
|
1989
|
+
/\/+$/,
|
|
1990
|
+
"",
|
|
1991
|
+
);
|
|
1992
|
+
const callbackOrigin = getBuilderCliAuthCallbackOriginForEvent(
|
|
1993
|
+
event,
|
|
1994
|
+
).replace(/\/+$/, "");
|
|
1995
|
+
let relay:
|
|
1996
|
+
| { state: string; payload: BuilderPreviewRelayState }
|
|
1997
|
+
| undefined;
|
|
1998
|
+
if (previewOrigin !== callbackOrigin) {
|
|
1999
|
+
try {
|
|
2000
|
+
relay = signBuilderPreviewRelayState({
|
|
2001
|
+
ownerEmail,
|
|
2002
|
+
targetOrigin: previewOrigin,
|
|
2003
|
+
basePath: getAppBasePath(),
|
|
2004
|
+
});
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
const msg =
|
|
2007
|
+
err instanceof Error
|
|
2008
|
+
? err.message
|
|
2009
|
+
: "Builder preview relay is not configured.";
|
|
2010
|
+
setResponseStatus(event, 503);
|
|
2011
|
+
setResponseHeader(
|
|
2012
|
+
event,
|
|
2013
|
+
"Content-Type",
|
|
2014
|
+
"text/html; charset=utf-8",
|
|
2015
|
+
);
|
|
2016
|
+
return createBuilderBrowserCallbackErrorPage(msg, {
|
|
2017
|
+
title: "Builder preview connection isn't configured",
|
|
2018
|
+
body: "This preview needs its secure Builder callback relay configured before authorization can start.",
|
|
2019
|
+
closeHint:
|
|
2020
|
+
"Close this popup and ask the preview owner to finish Builder relay setup.",
|
|
2021
|
+
parentOrigin: previewOrigin,
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
let pendingOrgId: string | null = null;
|
|
2027
|
+
let pendingRole: string | null = null;
|
|
2028
|
+
if (!ownerContext.anonymous) {
|
|
2029
|
+
try {
|
|
2030
|
+
const orgContext = await getOrgContext(event);
|
|
2031
|
+
pendingOrgId = orgContext.orgId ?? null;
|
|
2032
|
+
pendingRole = orgContext.role ?? null;
|
|
2033
|
+
} catch {
|
|
2034
|
+
// The pending owner remains user-scoped when org context is absent.
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1830
2038
|
// Store a short-lived pending row. If the DB is unavailable we
|
|
1831
2039
|
// surface a popup-renderable error page that signals the parent
|
|
1832
2040
|
// via BroadcastChannel, rather than letting the popup show raw
|
|
1833
2041
|
// JSON and the parent poll for 5 minutes.
|
|
1834
2042
|
try {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2043
|
+
if (relay) {
|
|
2044
|
+
await putSetting(builderRelayPendingKey(relay.payload.flowId), {
|
|
2045
|
+
ownerEmail,
|
|
2046
|
+
orgId: pendingOrgId,
|
|
2047
|
+
role: pendingRole,
|
|
2048
|
+
targetOrigin: relay.payload.targetOrigin,
|
|
2049
|
+
basePath: relay.payload.basePath,
|
|
2050
|
+
expiresAt: relay.payload.exp,
|
|
2051
|
+
tracking: connectTracking,
|
|
2052
|
+
});
|
|
2053
|
+
} else {
|
|
2054
|
+
await putSetting(`builder-pending-connect:${ownerEmail}`, {
|
|
2055
|
+
expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
|
|
2056
|
+
tracking: connectTracking,
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
1839
2059
|
} catch (err) {
|
|
1840
2060
|
await trackBuilderLifecycle(
|
|
1841
2061
|
event,
|
|
@@ -1886,10 +2106,11 @@ export function createCoreRoutesPlugin(
|
|
|
1886
2106
|
// clients, but still send it to Builder immediately and include signed
|
|
1887
2107
|
// callback state so the callback does not depend on popup cookies.
|
|
1888
2108
|
const cliAuthUrl = buildBuilderCliAuthUrl(
|
|
1889
|
-
|
|
2109
|
+
callbackOrigin,
|
|
1890
2110
|
signBuilderCallbackState(ownerEmail),
|
|
1891
2111
|
{
|
|
1892
|
-
previewOrigin
|
|
2112
|
+
previewOrigin,
|
|
2113
|
+
relayState: relay?.state,
|
|
1893
2114
|
tracking: connectTracking,
|
|
1894
2115
|
},
|
|
1895
2116
|
);
|
|
@@ -2067,6 +2288,51 @@ export function createCoreRoutesPlugin(
|
|
|
2067
2288
|
}),
|
|
2068
2289
|
);
|
|
2069
2290
|
|
|
2291
|
+
getH3App(nitroApp).use(
|
|
2292
|
+
`${P}/builder/relay`,
|
|
2293
|
+
defineEventHandler(async (event: H3Event) => {
|
|
2294
|
+
if (getMethod(event) !== "POST") {
|
|
2295
|
+
setResponseStatus(event, 405);
|
|
2296
|
+
return { error: "Method not allowed" };
|
|
2297
|
+
}
|
|
2298
|
+
const rawBody = await readBuilderRelayRequestBody(event);
|
|
2299
|
+
const result = await consumeBuilderRelayRequest(
|
|
2300
|
+
{
|
|
2301
|
+
rawBody,
|
|
2302
|
+
timestamp: getHeader(event, BUILDER_RELAY_TIMESTAMP_HEADER),
|
|
2303
|
+
flowId: getHeader(event, BUILDER_RELAY_FLOW_HEADER),
|
|
2304
|
+
signature: getHeader(event, BUILDER_RELAY_SIGNATURE_HEADER),
|
|
2305
|
+
requestOrigin: getFrameworkRouteRequestUrl(event).origin,
|
|
2306
|
+
requestBasePath: getAppBasePath(),
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
getPending: getSetting,
|
|
2310
|
+
deletePending: deleteSetting,
|
|
2311
|
+
writeCredentials: async (ownerEmail, credentials, scope) => {
|
|
2312
|
+
const { writeBuilderCredentials } =
|
|
2313
|
+
await import("./credential-provider.js");
|
|
2314
|
+
await writeBuilderCredentials(ownerEmail, credentials, scope);
|
|
2315
|
+
await Promise.all([
|
|
2316
|
+
deleteSetting("builder-disconnected").catch(() => false),
|
|
2317
|
+
deleteSetting(`builder-connect-error:${ownerEmail}`).catch(
|
|
2318
|
+
() => false,
|
|
2319
|
+
),
|
|
2320
|
+
]);
|
|
2321
|
+
},
|
|
2322
|
+
},
|
|
2323
|
+
).catch(() => ({
|
|
2324
|
+
ok: false as const,
|
|
2325
|
+
status: 500,
|
|
2326
|
+
error: "Builder relay credential persistence failed",
|
|
2327
|
+
}));
|
|
2328
|
+
if (!result.ok) {
|
|
2329
|
+
setResponseStatus(event, result.status);
|
|
2330
|
+
return { error: result.error };
|
|
2331
|
+
}
|
|
2332
|
+
return { ok: true };
|
|
2333
|
+
}),
|
|
2334
|
+
);
|
|
2335
|
+
|
|
2070
2336
|
getH3App(nitroApp).use(
|
|
2071
2337
|
`${P}/builder/callback`,
|
|
2072
2338
|
defineEventHandler(async (event: H3Event) => {
|
|
@@ -2074,6 +2340,119 @@ export function createCoreRoutesPlugin(
|
|
|
2074
2340
|
setResponseStatus(event, 405);
|
|
2075
2341
|
return { error: "Method not allowed" };
|
|
2076
2342
|
}
|
|
2343
|
+
// Builder's provider contract puts credentials on this first-hop
|
|
2344
|
+
// URL. Keep the response out of caches and suppress referrer
|
|
2345
|
+
// propagation even though the second hop carries secrets only in
|
|
2346
|
+
// its authenticated POST body.
|
|
2347
|
+
setResponseHeader(event, "Cache-Control", "no-store");
|
|
2348
|
+
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
2349
|
+
|
|
2350
|
+
const requestUrl = getFrameworkRouteRequestUrl(event);
|
|
2351
|
+
const relayStateRaw = requestUrl.searchParams.get(
|
|
2352
|
+
BUILDER_RELAY_STATE_PARAM,
|
|
2353
|
+
);
|
|
2354
|
+
if (relayStateRaw) {
|
|
2355
|
+
let relayPayload: BuilderPreviewRelayState | null = null;
|
|
2356
|
+
try {
|
|
2357
|
+
relayPayload =
|
|
2358
|
+
verifyBuilderPreviewRelayStateForCallback(relayStateRaw);
|
|
2359
|
+
} catch {
|
|
2360
|
+
// A preview relay must fail closed when its dedicated shared
|
|
2361
|
+
// secret is absent on the corporate callback deployment.
|
|
2362
|
+
}
|
|
2363
|
+
if (!relayPayload) {
|
|
2364
|
+
setResponseStatus(event, 403);
|
|
2365
|
+
setResponseHeader(
|
|
2366
|
+
event,
|
|
2367
|
+
"Content-Type",
|
|
2368
|
+
"text/html; charset=utf-8",
|
|
2369
|
+
);
|
|
2370
|
+
return createBuilderBrowserCallbackErrorPage(
|
|
2371
|
+
"Builder preview relay state is invalid or expired.",
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
const privateKey = requestUrl.searchParams.get("p-key");
|
|
2376
|
+
const publicKey = requestUrl.searchParams.get("api-key");
|
|
2377
|
+
if (!privateKey || !publicKey) {
|
|
2378
|
+
setResponseStatus(event, 400);
|
|
2379
|
+
setResponseHeader(
|
|
2380
|
+
event,
|
|
2381
|
+
"Content-Type",
|
|
2382
|
+
"text/html; charset=utf-8",
|
|
2383
|
+
);
|
|
2384
|
+
return createBuilderBrowserCallbackErrorPage(
|
|
2385
|
+
"Builder didn't return credentials. Restart the connect flow from settings.",
|
|
2386
|
+
{ parentOrigin: relayPayload.targetOrigin },
|
|
2387
|
+
);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
const credentials: BuilderRelayCredentials = {
|
|
2391
|
+
privateKey,
|
|
2392
|
+
publicKey,
|
|
2393
|
+
userId: requestUrl.searchParams.get("user-id"),
|
|
2394
|
+
orgName: requestUrl.searchParams.get("org-name"),
|
|
2395
|
+
orgKind: requestUrl.searchParams.get("kind"),
|
|
2396
|
+
subscription: requestUrl.searchParams.get("subscription"),
|
|
2397
|
+
subscriptionLevel:
|
|
2398
|
+
requestUrl.searchParams.get("subscription-level"),
|
|
2399
|
+
subscriptionName:
|
|
2400
|
+
requestUrl.searchParams.get("subscription-name"),
|
|
2401
|
+
isEnterprise: parseBuilderCallbackBoolean(
|
|
2402
|
+
requestUrl.searchParams.get("is-enterprise"),
|
|
2403
|
+
),
|
|
2404
|
+
isFreeAccount: parseBuilderCallbackBoolean(
|
|
2405
|
+
requestUrl.searchParams.get("is-free-account"),
|
|
2406
|
+
),
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
try {
|
|
2410
|
+
const relayRequest = createBuilderRelayRequest(
|
|
2411
|
+
relayStateRaw,
|
|
2412
|
+
credentials,
|
|
2413
|
+
);
|
|
2414
|
+
const response = await ssrfSafeFetch(
|
|
2415
|
+
relayRequest.url,
|
|
2416
|
+
{
|
|
2417
|
+
method: "POST",
|
|
2418
|
+
headers: relayRequest.headers,
|
|
2419
|
+
body: relayRequest.body,
|
|
2420
|
+
},
|
|
2421
|
+
{ maxRedirects: 0, httpsOnly: true },
|
|
2422
|
+
);
|
|
2423
|
+
if (!response.ok) {
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
`Preview relay rejected the callback (${response.status}).`,
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
} catch (err) {
|
|
2429
|
+
const message =
|
|
2430
|
+
err instanceof Error
|
|
2431
|
+
? err.message
|
|
2432
|
+
: "Builder preview relay failed.";
|
|
2433
|
+
// Never log the first-hop URL or relay body: both contain
|
|
2434
|
+
// credentials. The popup gets a bounded, credential-free error.
|
|
2435
|
+
setResponseStatus(event, 502);
|
|
2436
|
+
setResponseHeader(
|
|
2437
|
+
event,
|
|
2438
|
+
"Content-Type",
|
|
2439
|
+
"text/html; charset=utf-8",
|
|
2440
|
+
);
|
|
2441
|
+
return createBuilderBrowserCallbackErrorPage(message, {
|
|
2442
|
+
parentOrigin: relayPayload.targetOrigin,
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
setResponseHeader(
|
|
2447
|
+
event,
|
|
2448
|
+
"Content-Type",
|
|
2449
|
+
"text/html; charset=utf-8",
|
|
2450
|
+
);
|
|
2451
|
+
return createBuilderBrowserCallbackPage(
|
|
2452
|
+
`${relayPayload.targetOrigin}${relayPayload.basePath || "/"}`,
|
|
2453
|
+
{ parentOrigin: relayPayload.targetOrigin },
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2077
2456
|
|
|
2078
2457
|
// A real session or a template-approved anonymous owner is required;
|
|
2079
2458
|
// the pending-row check below (combined with the same-origin gate on
|
|
@@ -2105,7 +2484,6 @@ export function createCoreRoutesPlugin(
|
|
|
2105
2484
|
}
|
|
2106
2485
|
clearBuilderConnectOwnerCookie(event);
|
|
2107
2486
|
|
|
2108
|
-
const requestUrl = getFrameworkRouteRequestUrl(event);
|
|
2109
2487
|
let connectTracking = getBuilderConnectTrackingParams(
|
|
2110
2488
|
requestUrl.searchParams,
|
|
2111
2489
|
);
|