@agent-native/core 0.101.7 → 0.101.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +22 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  16. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  17. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  18. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  19. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  20. package/corpus/core/src/server/auth.ts +25 -1
  21. package/corpus/core/src/server/builder-browser.ts +353 -1
  22. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  23. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  24. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  26. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  27. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  28. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  29. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  30. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  31. package/corpus/templates/content/AGENTS.md +3 -2
  32. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  33. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  34. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  35. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  36. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  37. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  38. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  39. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  40. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  41. package/corpus/templates/content/actions/_database-source-utils.ts +1041 -154
  42. package/corpus/templates/content/actions/_database-utils.ts +89 -12
  43. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  44. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  45. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  46. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  47. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  48. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  49. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  50. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  51. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  52. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  53. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  54. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +27 -15
  55. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  56. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  57. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2353 -576
  58. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  59. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  60. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  61. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  62. package/corpus/templates/content/app/global.css +18 -1
  63. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  64. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  65. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -0
  66. package/corpus/templates/content/app/i18n-data.ts +287 -0
  67. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  68. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  69. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  70. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  71. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  72. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  73. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  74. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  75. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  76. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  77. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  78. package/corpus/templates/content/parity/matrix.md +27 -25
  79. package/corpus/templates/content/parity/matrix.ts +50 -2
  80. package/corpus/templates/content/server/db/schema.ts +47 -0
  81. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  82. package/corpus/templates/content/server/plugins/db.ts +53 -0
  83. package/corpus/templates/content/shared/api.ts +70 -1
  84. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  85. package/corpus/templates/content/shared/nfm.ts +3 -3
  86. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  87. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  88. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  89. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  90. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  91. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  92. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  93. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  94. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  95. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  96. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  97. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  98. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  99. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  100. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  101. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  102. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  103. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  104. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  105. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  106. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  107. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  108. package/dist/client/review/ReviewCommentComposer.js +3 -3
  109. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  110. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  111. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  112. package/dist/client/review/ReviewThreadPanel.js +6 -4
  113. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  114. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  115. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  116. package/dist/client/settings/SettingsPanel.js +11 -4
  117. package/dist/client/settings/SettingsPanel.js.map +1 -1
  118. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  119. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  120. package/dist/client/settings/useBuilderStatus.js +19 -6
  121. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  122. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  123. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  124. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  125. package/dist/collab/awareness.d.ts +2 -2
  126. package/dist/collab/awareness.d.ts.map +1 -1
  127. package/dist/observability/routes.d.ts +5 -5
  128. package/dist/resources/handlers.d.ts +1 -1
  129. package/dist/secrets/routes.d.ts +9 -9
  130. package/dist/server/auth.d.ts +1 -0
  131. package/dist/server/auth.d.ts.map +1 -1
  132. package/dist/server/auth.js +19 -2
  133. package/dist/server/auth.js.map +1 -1
  134. package/dist/server/builder-browser.d.ts +78 -0
  135. package/dist/server/builder-browser.d.ts.map +1 -1
  136. package/dist/server/builder-browser.js +246 -1
  137. package/dist/server/builder-browser.js.map +1 -1
  138. package/dist/server/core-routes-plugin.d.ts +41 -0
  139. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  140. package/dist/server/core-routes-plugin.js +260 -14
  141. package/dist/server/core-routes-plugin.js.map +1 -1
  142. package/docs/content/deployment.mdx +9 -7
  143. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  144. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  145. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  146. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  147. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  148. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  149. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  150. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  151. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  152. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  153. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader, getCookie, setCookie, deleteCookie, getRequestURL, getRequestIP, } from "h3";
1
+ import { assertBodySize, defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getHeader, getCookie, setCookie, deleteCookie, 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";
@@ -10,6 +10,7 @@ import { mountBrowserSessionRoutes } from "../browser-sessions/routes.js";
10
10
  import { mountDbAdminRoutes } from "../db-admin/routes.js";
11
11
  import { getDbExec } from "../db/client.js";
12
12
  import { getDatabaseRuntimeFingerprint, getRuntimeDebugFingerprint, runDatabaseSchemaHealthCheck, } from "../db/runtime-diagnostics.js";
13
+ import { ssrfSafeFetch } from "../extensions/url-safety.js";
13
14
  import { uploadFile, getActiveFileUploadProviderForRequest, listFileUploadProviders, } from "../file-upload/index.js";
14
15
  import { handleMcpConnect } from "../mcp/connect-route.js";
15
16
  import { handleMcpOAuth, handleMcpOAuthAuthorizationServerMetadata, handleMcpOAuthProtectedResourceMetadata, } from "../mcp/oauth-route.js";
@@ -34,7 +35,7 @@ import { createAgentEngineApiKeyHandler } from "./agent-engine-api-key-route.js"
34
35
  import { getConfiguredAppBasePath, stripAppBasePath } from "./app-base-path.js";
35
36
  import { getAppName } from "./app-name.js";
36
37
  import { getSession } from "./auth.js";
37
- import { BUILDER_CONNECT_PARAM, BUILDER_CONNECT_OWNER_COOKIE, BUILDER_ENV_KEYS, BUILDER_OPENER_PARAM, BUILDER_STATE_PARAM, appendBuilderConnectToken, builderConnectTrackingProperties, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, getBuilderConnectTrackingParams, getBuilderCliAuthCallbackOriginForEvent, getBuilderBrowserOriginForEvent, resolveBuilderCallbackReturnUrl, getBuilderBrowserStatusForEvent, resolveBuilderBranchProjectId, resolveSafePreviewUrl, runBuilderAgent, signBuilderCallbackState, verifyBuilderConnectTokenAndGetOwner, verifyBuilderCallbackStateAndGetOwner, signBuilderConnectToken, } from "./builder-browser.js";
38
+ import { BUILDER_CONNECT_PARAM, BUILDER_CONNECT_OWNER_COOKIE, BUILDER_ENV_KEYS, BUILDER_OPENER_PARAM, BUILDER_RELAY_FLOW_HEADER, BUILDER_RELAY_SIGNATURE_HEADER, BUILDER_RELAY_STATE_PARAM, BUILDER_RELAY_TIMESTAMP_HEADER, BUILDER_STATE_PARAM, appendBuilderConnectToken, builderConnectTrackingProperties, buildBuilderCliAuthUrl, createBuilderBrowserCallbackErrorPage, createBuilderBrowserCallbackPage, createBuilderRelayRequest, getBuilderConnectTrackingParams, getBuilderCliAuthCallbackOriginForEvent, getBuilderBrowserOriginForEvent, resolveBuilderCallbackReturnUrl, getBuilderBrowserStatusForEvent, resolveBuilderBranchProjectId, resolveSafePreviewUrl, runBuilderAgent, signBuilderCallbackState, signBuilderPreviewRelayState, verifyBuilderRelayRequest, verifyBuilderPreviewRelayStateForCallback, verifyBuilderConnectTokenAndGetOwner, verifyBuilderCallbackStateAndGetOwner, signBuilderConnectToken, } from "./builder-browser.js";
38
39
  import { captureError } from "./capture-error.js";
39
40
  import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
40
41
  import { canUseDeployCredentialFallbackForRequest, readDeployCredentialEnv, resolveSecret, } from "./credential-provider.js";
@@ -161,6 +162,15 @@ export function resolveFrameworkSseRoutes(sseRoute) {
161
162
  LEGACY_FRAMEWORK_EVENTS_ROUTE,
162
163
  ]));
163
164
  }
165
+ export const BUILDER_STATUS_ROUTE_SUFFIXES = [
166
+ "/builder/status",
167
+ "/connection-status/builder",
168
+ ];
169
+ export function mountBuilderStatusRouteAliases(mount, prefix, handler) {
170
+ for (const routeSuffix of BUILDER_STATUS_ROUTE_SUFFIXES) {
171
+ mount(`${prefix}${routeSuffix}`, handler);
172
+ }
173
+ }
164
174
  registerBuiltinEngines();
165
175
  function cleanBuilderWaitlistText(value, maxLength = BUILDER_WAITLIST_TEXT_LIMIT) {
166
176
  if (typeof value !== "string")
@@ -532,6 +542,90 @@ export function getFrameworkRouteRequestUrl(event) {
532
542
  url.search = rawUrl.slice(queryStart);
533
543
  return url;
534
544
  }
545
+ function builderRelayPendingKey(flowId) {
546
+ return `builder-pending-relay:${flowId}`;
547
+ }
548
+ function parseBuilderRelayPendingRecord(value) {
549
+ if (!value ||
550
+ typeof value.ownerEmail !== "string" ||
551
+ typeof value.targetOrigin !== "string" ||
552
+ typeof value.basePath !== "string" ||
553
+ typeof value.expiresAt !== "number") {
554
+ return null;
555
+ }
556
+ return {
557
+ ownerEmail: value.ownerEmail,
558
+ orgId: typeof value.orgId === "string" ? value.orgId : null,
559
+ role: typeof value.role === "string" ? value.role : null,
560
+ targetOrigin: value.targetOrigin,
561
+ basePath: value.basePath,
562
+ expiresAt: value.expiresAt,
563
+ tracking: value.tracking && typeof value.tracking === "object"
564
+ ? value.tracking
565
+ : undefined,
566
+ };
567
+ }
568
+ /**
569
+ * Authenticated one-shot receiver for the second hop of Builder preview auth.
570
+ * Owner and org scope always come from the preview's pending record; the
571
+ * corporate callback cannot choose them in its POST body.
572
+ */
573
+ export async function consumeBuilderRelayRequest(input, dependencies) {
574
+ if (input.rawBody.length > 64 * 1024) {
575
+ return {
576
+ ok: false,
577
+ status: 413,
578
+ error: "Builder relay request is too large",
579
+ };
580
+ }
581
+ let verified;
582
+ try {
583
+ verified = verifyBuilderRelayRequest({
584
+ body: input.rawBody,
585
+ timestamp: input.timestamp,
586
+ flowId: input.flowId,
587
+ signature: input.signature,
588
+ requestOrigin: input.requestOrigin,
589
+ requestBasePath: input.requestBasePath,
590
+ now: input.now,
591
+ });
592
+ }
593
+ catch {
594
+ return { ok: false, status: 503, error: "Builder relay is not configured" };
595
+ }
596
+ if (!verified) {
597
+ return { ok: false, status: 401, error: "Invalid Builder relay request" };
598
+ }
599
+ const pendingKey = builderRelayPendingKey(verified.payload.flowId);
600
+ const pending = parseBuilderRelayPendingRecord(await dependencies.getPending(pendingKey).catch(() => null));
601
+ const now = input.now ?? Date.now();
602
+ if (!pending ||
603
+ pending.expiresAt < now ||
604
+ pending.ownerEmail !== verified.payload.ownerEmail ||
605
+ pending.targetOrigin !== verified.payload.targetOrigin ||
606
+ pending.basePath !== verified.payload.basePath) {
607
+ return { ok: false, status: 403, error: "No active Builder relay flow" };
608
+ }
609
+ // A successful delete, not merely a resolved promise, is the one-shot gate.
610
+ // It happens before credential persistence so replay is impossible even if
611
+ // the downstream write fails and the human has to start a fresh flow.
612
+ const consumed = await dependencies
613
+ .deletePending(pendingKey)
614
+ .catch(() => false);
615
+ if (consumed !== true) {
616
+ return {
617
+ ok: false,
618
+ status: 409,
619
+ error: "Builder relay flow was already consumed",
620
+ };
621
+ }
622
+ await dependencies.writeCredentials(pending.ownerEmail, verified.body.credentials, { orgId: pending.orgId, role: pending.role });
623
+ return { ok: true };
624
+ }
625
+ export async function readBuilderRelayRequestBody(event) {
626
+ await assertBodySize(event, 64 * 1024);
627
+ return (await readRawBody(event, "utf8")) ?? "";
628
+ }
535
629
  function redactValues(text, values) {
536
630
  let out = text;
537
631
  for (const value of values) {
@@ -943,7 +1037,7 @@ export function createCoreRoutesPlugin(options = {}) {
943
1037
  // db-admin/routes.ts), so on a deployed / production app it always 403s.
944
1038
  mountDbAdminRoutes(nitroApp, { routePrefix: P });
945
1039
  const resolveBuilderOwnerContext = async (event, mode) => resolveBuilderOwnerContextForRequest(event, { anonymousOwner: options.anonymousOwner }, mode);
946
- getH3App(nitroApp).use(`${P}/builder/status`, defineEventHandler(async (event) => {
1040
+ const builderStatusHandler = defineEventHandler(async (event) => {
947
1041
  const envStatus = getBuilderBrowserStatusForEvent(event);
948
1042
  const ownerContext = await resolveBuilderOwnerContext(event);
949
1043
  const userEmail = ownerContext.email;
@@ -1105,9 +1199,7 @@ export function createCoreRoutesPlugin(options = {}) {
1105
1199
  envStatus.subscriptionName ||
1106
1200
  undefined,
1107
1201
  isEnterprise: creds.isEnterprise ?? envStatus.isEnterprise ?? undefined,
1108
- isFreeAccount: creds.isFreeAccount ??
1109
- envStatus.isFreeAccount ??
1110
- undefined,
1202
+ isFreeAccount: creds.isFreeAccount ?? envStatus.isFreeAccount ?? undefined,
1111
1203
  credentialSource: credentialSource ?? undefined,
1112
1204
  });
1113
1205
  }
@@ -1156,7 +1248,8 @@ export function createCoreRoutesPlugin(options = {}) {
1156
1248
  isFreeAccount: undefined,
1157
1249
  });
1158
1250
  });
1159
- }));
1251
+ });
1252
+ mountBuilderStatusRouteAliases((path, handler) => getH3App(nitroApp).use(path, handler), P, builderStatusHandler);
1160
1253
  // How long a pending-connect row is valid. Must be long enough for
1161
1254
  // the user to complete the Builder CLI-auth flow, but short enough
1162
1255
  // that a stale row from an abandoned attempt doesn't accept a new
@@ -1279,15 +1372,65 @@ export function createCoreRoutesPlugin(options = {}) {
1279
1372
  catch {
1280
1373
  // No prior error row — fine
1281
1374
  }
1375
+ const previewOrigin = getBuilderBrowserOriginForEvent(event).replace(/\/+$/, "");
1376
+ const callbackOrigin = getBuilderCliAuthCallbackOriginForEvent(event).replace(/\/+$/, "");
1377
+ let relay;
1378
+ if (previewOrigin !== callbackOrigin) {
1379
+ try {
1380
+ relay = signBuilderPreviewRelayState({
1381
+ ownerEmail,
1382
+ targetOrigin: previewOrigin,
1383
+ basePath: getAppBasePath(),
1384
+ });
1385
+ }
1386
+ catch (err) {
1387
+ const msg = err instanceof Error
1388
+ ? err.message
1389
+ : "Builder preview relay is not configured.";
1390
+ setResponseStatus(event, 503);
1391
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1392
+ return createBuilderBrowserCallbackErrorPage(msg, {
1393
+ title: "Builder preview connection isn't configured",
1394
+ body: "This preview needs its secure Builder callback relay configured before authorization can start.",
1395
+ closeHint: "Close this popup and ask the preview owner to finish Builder relay setup.",
1396
+ parentOrigin: previewOrigin,
1397
+ });
1398
+ }
1399
+ }
1400
+ let pendingOrgId = null;
1401
+ let pendingRole = null;
1402
+ if (!ownerContext.anonymous) {
1403
+ try {
1404
+ const orgContext = await getOrgContext(event);
1405
+ pendingOrgId = orgContext.orgId ?? null;
1406
+ pendingRole = orgContext.role ?? null;
1407
+ }
1408
+ catch {
1409
+ // The pending owner remains user-scoped when org context is absent.
1410
+ }
1411
+ }
1282
1412
  // Store a short-lived pending row. If the DB is unavailable we
1283
1413
  // surface a popup-renderable error page that signals the parent
1284
1414
  // via BroadcastChannel, rather than letting the popup show raw
1285
1415
  // JSON and the parent poll for 5 minutes.
1286
1416
  try {
1287
- await putSetting(`builder-pending-connect:${ownerEmail}`, {
1288
- expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
1289
- tracking: connectTracking,
1290
- });
1417
+ if (relay) {
1418
+ await putSetting(builderRelayPendingKey(relay.payload.flowId), {
1419
+ ownerEmail,
1420
+ orgId: pendingOrgId,
1421
+ role: pendingRole,
1422
+ targetOrigin: relay.payload.targetOrigin,
1423
+ basePath: relay.payload.basePath,
1424
+ expiresAt: relay.payload.exp,
1425
+ tracking: connectTracking,
1426
+ });
1427
+ }
1428
+ else {
1429
+ await putSetting(`builder-pending-connect:${ownerEmail}`, {
1430
+ expiresAt: Date.now() + BUILDER_CONNECT_PENDING_TTL_MS,
1431
+ tracking: connectTracking,
1432
+ });
1433
+ }
1291
1434
  }
1292
1435
  catch (err) {
1293
1436
  await trackBuilderLifecycle(event, "builder connect failed", ownerEmail, {
@@ -1319,8 +1462,9 @@ export function createCoreRoutesPlugin(options = {}) {
1319
1462
  // from /builder/status. Keep this legacy trampoline working for older
1320
1463
  // clients, but still send it to Builder immediately and include signed
1321
1464
  // callback state so the callback does not depend on popup cookies.
1322
- const cliAuthUrl = buildBuilderCliAuthUrl(getBuilderCliAuthCallbackOriginForEvent(event), signBuilderCallbackState(ownerEmail), {
1323
- previewOrigin: getBuilderBrowserOriginForEvent(event),
1465
+ const cliAuthUrl = buildBuilderCliAuthUrl(callbackOrigin, signBuilderCallbackState(ownerEmail), {
1466
+ previewOrigin,
1467
+ relayState: relay?.state,
1324
1468
  tracking: connectTracking,
1325
1469
  });
1326
1470
  setResponseStatus(event, 302);
@@ -1449,11 +1593,114 @@ export function createCoreRoutesPlugin(options = {}) {
1449
1593
  });
1450
1594
  return { ok: true, formSubmitted: formSubmission.submitted };
1451
1595
  }));
1596
+ getH3App(nitroApp).use(`${P}/builder/relay`, defineEventHandler(async (event) => {
1597
+ if (getMethod(event) !== "POST") {
1598
+ setResponseStatus(event, 405);
1599
+ return { error: "Method not allowed" };
1600
+ }
1601
+ const rawBody = await readBuilderRelayRequestBody(event);
1602
+ const result = await consumeBuilderRelayRequest({
1603
+ rawBody,
1604
+ timestamp: getHeader(event, BUILDER_RELAY_TIMESTAMP_HEADER),
1605
+ flowId: getHeader(event, BUILDER_RELAY_FLOW_HEADER),
1606
+ signature: getHeader(event, BUILDER_RELAY_SIGNATURE_HEADER),
1607
+ requestOrigin: getFrameworkRouteRequestUrl(event).origin,
1608
+ requestBasePath: getAppBasePath(),
1609
+ }, {
1610
+ getPending: getSetting,
1611
+ deletePending: deleteSetting,
1612
+ writeCredentials: async (ownerEmail, credentials, scope) => {
1613
+ const { writeBuilderCredentials } = await import("./credential-provider.js");
1614
+ await writeBuilderCredentials(ownerEmail, credentials, scope);
1615
+ await Promise.all([
1616
+ deleteSetting("builder-disconnected").catch(() => false),
1617
+ deleteSetting(`builder-connect-error:${ownerEmail}`).catch(() => false),
1618
+ ]);
1619
+ },
1620
+ }).catch(() => ({
1621
+ ok: false,
1622
+ status: 500,
1623
+ error: "Builder relay credential persistence failed",
1624
+ }));
1625
+ if (!result.ok) {
1626
+ setResponseStatus(event, result.status);
1627
+ return { error: result.error };
1628
+ }
1629
+ return { ok: true };
1630
+ }));
1452
1631
  getH3App(nitroApp).use(`${P}/builder/callback`, defineEventHandler(async (event) => {
1453
1632
  if (getMethod(event) !== "GET") {
1454
1633
  setResponseStatus(event, 405);
1455
1634
  return { error: "Method not allowed" };
1456
1635
  }
1636
+ // Builder's provider contract puts credentials on this first-hop
1637
+ // URL. Keep the response out of caches and suppress referrer
1638
+ // propagation even though the second hop carries secrets only in
1639
+ // its authenticated POST body.
1640
+ setResponseHeader(event, "Cache-Control", "no-store");
1641
+ setResponseHeader(event, "Referrer-Policy", "no-referrer");
1642
+ const requestUrl = getFrameworkRouteRequestUrl(event);
1643
+ const relayStateRaw = requestUrl.searchParams.get(BUILDER_RELAY_STATE_PARAM);
1644
+ if (relayStateRaw) {
1645
+ let relayPayload = null;
1646
+ try {
1647
+ relayPayload =
1648
+ verifyBuilderPreviewRelayStateForCallback(relayStateRaw);
1649
+ }
1650
+ catch {
1651
+ // A preview relay must fail closed when its dedicated shared
1652
+ // secret is absent on the corporate callback deployment.
1653
+ }
1654
+ if (!relayPayload) {
1655
+ setResponseStatus(event, 403);
1656
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1657
+ return createBuilderBrowserCallbackErrorPage("Builder preview relay state is invalid or expired.");
1658
+ }
1659
+ const privateKey = requestUrl.searchParams.get("p-key");
1660
+ const publicKey = requestUrl.searchParams.get("api-key");
1661
+ if (!privateKey || !publicKey) {
1662
+ setResponseStatus(event, 400);
1663
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1664
+ return createBuilderBrowserCallbackErrorPage("Builder didn't return credentials. Restart the connect flow from settings.", { parentOrigin: relayPayload.targetOrigin });
1665
+ }
1666
+ const credentials = {
1667
+ privateKey,
1668
+ publicKey,
1669
+ userId: requestUrl.searchParams.get("user-id"),
1670
+ orgName: requestUrl.searchParams.get("org-name"),
1671
+ orgKind: requestUrl.searchParams.get("kind"),
1672
+ subscription: requestUrl.searchParams.get("subscription"),
1673
+ subscriptionLevel: requestUrl.searchParams.get("subscription-level"),
1674
+ subscriptionName: requestUrl.searchParams.get("subscription-name"),
1675
+ isEnterprise: parseBuilderCallbackBoolean(requestUrl.searchParams.get("is-enterprise")),
1676
+ isFreeAccount: parseBuilderCallbackBoolean(requestUrl.searchParams.get("is-free-account")),
1677
+ };
1678
+ try {
1679
+ const relayRequest = createBuilderRelayRequest(relayStateRaw, credentials);
1680
+ const response = await ssrfSafeFetch(relayRequest.url, {
1681
+ method: "POST",
1682
+ headers: relayRequest.headers,
1683
+ body: relayRequest.body,
1684
+ }, { maxRedirects: 0, httpsOnly: true });
1685
+ if (!response.ok) {
1686
+ throw new Error(`Preview relay rejected the callback (${response.status}).`);
1687
+ }
1688
+ }
1689
+ catch (err) {
1690
+ const message = err instanceof Error
1691
+ ? err.message
1692
+ : "Builder preview relay failed.";
1693
+ // Never log the first-hop URL or relay body: both contain
1694
+ // credentials. The popup gets a bounded, credential-free error.
1695
+ setResponseStatus(event, 502);
1696
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1697
+ return createBuilderBrowserCallbackErrorPage(message, {
1698
+ parentOrigin: relayPayload.targetOrigin,
1699
+ });
1700
+ }
1701
+ setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
1702
+ return createBuilderBrowserCallbackPage(`${relayPayload.targetOrigin}${relayPayload.basePath || "/"}`, { parentOrigin: relayPayload.targetOrigin });
1703
+ }
1457
1704
  // A real session or a template-approved anonymous owner is required;
1458
1705
  // the pending-row check below (combined with the same-origin gate on
1459
1706
  // /builder/connect) blocks CSRF and callback replay.
@@ -1478,7 +1725,6 @@ export function createCoreRoutesPlugin(options = {}) {
1478
1725
  return { error: "Authentication required" };
1479
1726
  }
1480
1727
  clearBuilderConnectOwnerCookie(event);
1481
- const requestUrl = getFrameworkRouteRequestUrl(event);
1482
1728
  let connectTracking = getBuilderConnectTrackingParams(requestUrl.searchParams);
1483
1729
  // postMessage from the callback success/error pages must target the
1484
1730
  // original preview opener, not the callback server. On the fallback