@agent-native/core 0.159.5 → 0.160.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/server/lib/generation.ts +10 -6
  3. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
  4. package/corpus/templates/calendar/actions/create-event.ts +4 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
  6. package/corpus/templates/calendar/actions/update-event.ts +1 -0
  7. package/corpus/templates/calendar/agent-native.config.ts +1 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
  11. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
  12. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
  13. package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
  14. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
  15. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
  16. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
  17. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  18. package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
  19. package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
  20. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
  21. package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
  22. package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
  23. package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
  25. package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
  26. package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
  27. package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
  28. package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
  29. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
  30. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
  33. package/dist/agent/engine/builder-engine.d.ts +16 -2
  34. package/dist/agent/engine/builder-engine.js +178 -115
  35. package/dist/agent/engine/builtin.js +13 -4
  36. package/dist/agent/engine/credential-errors.d.ts +23 -0
  37. package/dist/agent/engine/credential-errors.js +19 -0
  38. package/dist/agent/engine/error-detail.d.ts +15 -0
  39. package/dist/agent/engine/error-detail.js +29 -0
  40. package/dist/agent/engine/registry.d.ts +14 -3
  41. package/dist/agent/engine/registry.js +106 -61
  42. package/dist/agent/engine/types.d.ts +16 -0
  43. package/dist/agent/engine/types.js +9 -0
  44. package/dist/agent/production-agent.d.ts +7 -0
  45. package/dist/agent/production-agent.js +68 -31
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  49. package/dist/agent/tool-approval-migrations.js +10 -0
  50. package/dist/agent/tool-approval-store.d.ts +5 -3
  51. package/dist/agent/tool-approval-store.js +11 -6
  52. package/dist/agent/types.d.ts +9 -0
  53. package/dist/cli/code-agent-connector.js +79 -7
  54. package/dist/cli/portal-transfer.d.ts +43 -0
  55. package/dist/cli/portal-transfer.js +201 -0
  56. package/dist/client/agent-chat-adapter.js +46 -15
  57. package/dist/client/error-format.js +25 -2
  58. package/dist/client/sse-event-processor.d.ts +2 -0
  59. package/dist/client/sse-event-processor.js +12 -1
  60. package/dist/collab/awareness.d.ts +2 -2
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/jobs/background-automation-runner.js +9 -15
  63. package/dist/notifications/routes.d.ts +6 -6
  64. package/dist/observability/routes.d.ts +5 -5
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/register-framework-secrets.js +25 -0
  67. package/dist/secrets/routes.d.ts +9 -9
  68. package/dist/server/agent-chat-plugin.js +9 -2
  69. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  70. package/dist/server/credential-provider.d.ts +80 -0
  71. package/dist/server/credential-provider.js +168 -1
  72. package/dist/server/google-realtime-session.d.ts +1 -1
  73. package/dist/server/google-realtime-session.js +6 -6
  74. package/dist/server/index.d.ts +1 -1
  75. package/dist/server/index.js +6 -1
  76. package/dist/server/realtime-token.d.ts +1 -1
  77. package/dist/server/realtime-voice.js +12 -6
  78. package/dist/server/transcribe-voice.d.ts +1 -1
  79. package/dist/server/transcribe-voice.js +18 -16
  80. package/dist/transcription/builder-transcription.js +8 -4
  81. package/package.json +3 -3
@@ -814,11 +814,11 @@ export function createAgentChatPlugin(options) {
814
814
  try {
815
815
  if (webGroupEnabled) {
816
816
  const { createWebSearchToolEntry } = await import("../extensions/web-search-tool.js");
817
- const { getBuilderWebSearchBaseUrl, resolveBuilderCredentials, resolveSecret, } = await import("./credential-provider.js");
817
+ const { getBuilderWebSearchBaseUrl, resolveBuilderGatewayCredentials, resolveSecret, } = await import("./credential-provider.js");
818
818
  const { getBuilderGatewayRequestHeaders } = await import("../agent/engine/builder-gateway-headers.js");
819
819
  webSearchTool = createWebSearchToolEntry({
820
820
  resolveSecret,
821
- resolveBuilderCredentials,
821
+ resolveBuilderCredentials: resolveBuilderGatewayCredentials,
822
822
  getBuilderWebSearchBaseUrl,
823
823
  getBuilderRequestHeaders: getBuilderGatewayRequestHeaders,
824
824
  });
@@ -4161,6 +4161,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4161
4161
  const workerClaim = run.runId
4162
4162
  ? await readBackgroundRunClaim(run.runId).catch(() => null)
4163
4163
  : null;
4164
+ const { isBuilderGatewayDeployConfigured } = await import("./credential-provider.js");
4164
4165
  return {
4165
4166
  active: true,
4166
4167
  runId: run.runId,
@@ -4177,6 +4178,12 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
4177
4178
  // `/runs/active?threadId=...` and inspect `diagStage`.
4178
4179
  dispatchMode: run.dispatchMode ?? null,
4179
4180
  terminalReason: run.terminalReason ?? null,
4181
+ // Who is paying for AI here, which is also who is reading a
4182
+ // failure. `terminalReason` is a bare error CODE, and the client
4183
+ // owns the copy for the handoff failures that never produce an
4184
+ // error event — so without this it has to author credential copy
4185
+ // for a reader it cannot identify, and picks the owner's.
4186
+ deploymentPaysForAi: isBuilderGatewayDeployConfigured(),
4180
4187
  diagStage: run.diagStage ?? null,
4181
4188
  workerStage: workerClaim?.workerStage ?? null,
4182
4189
  // Server clock so the client computes "stuck" elapsed time
@@ -27,10 +27,10 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
27
27
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
28
28
  error: any;
29
29
  } | {
30
+ error?: undefined;
30
31
  ok: boolean;
31
32
  key: string;
32
33
  baseUrlKey?: string;
33
34
  scope: AgentEngineApiKeyScope;
34
- error?: undefined;
35
35
  }>>;
36
36
  export {};
@@ -83,6 +83,10 @@ export declare function readDeployCredentialEnv(key: string): string | undefined
83
83
  */
84
84
  export declare function isDeployCredentialFallbackAllowed(): boolean;
85
85
  export declare function canUseDeployCredentialFallbackForRequest(key?: string): boolean;
86
+ /** The raw `btk-` gateway PAT a Builder-credits deploy injects. */
87
+ export declare const BUILDER_GATEWAY_TOKEN_ENV_VAR = "BUILDER_GATEWAY_TOKEN";
88
+ /** The space id that pairs with it, sent as `x-builder-api-key`. */
89
+ export declare const BUILDER_GATEWAY_SPACE_ID_ENV_VAR = "BUILDER_GATEWAY_SPACE_ID";
86
90
  type BuilderCredentialSource = "user" | "org" | "workspace" | "env";
87
91
  export declare function isBuilderPrivateKey(value: string | null | undefined): boolean;
88
92
  export interface BuilderCredentialLookupIdentity {
@@ -177,6 +181,67 @@ export declare function resolveBuilderCredentials(identity?: BuilderCredentialLo
177
181
  isEnterprise: boolean | null;
178
182
  isFreeAccount: boolean | null;
179
183
  }>;
184
+ /** `gateway-deploy` means the site pays, so owner-facing copy must not be shown. */
185
+ export type BuilderGatewayLane = "identity" | "gateway-deploy";
186
+ export interface BuilderGatewayCredentialsDetailed extends BuilderCredentialsDetailed {
187
+ lane: BuilderGatewayLane | null;
188
+ }
189
+ /**
190
+ * Both halves required: the gateway 403s a token with no space id before it
191
+ * consults any route policy, so half a pair is not a usable credential.
192
+ */
193
+ export declare function resolveUsableBuilderGatewayDeployCredentials(): Promise<{
194
+ token: string;
195
+ spaceId: string;
196
+ } | null>;
197
+ /**
198
+ * Weaker than "usable" on purpose: a present-but-rejected token still means the
199
+ * reader is a visitor. Excludes the dev preview, which carries the same token but
200
+ * is read by the project owner.
201
+ */
202
+ export declare function isBuilderGatewayDeployConfigured(): boolean;
203
+ /** One decision for every gateway-lane consumer; a per-consumer copy drifts. */
204
+ export declare function gatewayLaneUnavailableMessage(ownerFacing: string): string;
205
+ /**
206
+ * Gateway-lane credentials. Fall-through: the request's own Builder connection,
207
+ * then the deployment's credits pair, then the legacy pair.
208
+ *
209
+ * Identity-bearing calls (asset upload, design systems, Admin GraphQL, browser
210
+ * agent) must keep using `resolveBuilderCredentials`: a `['gateway']` token 403s
211
+ * on all of them.
212
+ */
213
+ export declare function resolveBuilderGatewayCredentialsDetailed(identity?: BuilderCredentialLookupIdentity): Promise<BuilderGatewayCredentialsDetailed>;
214
+ /**
215
+ * Gateway-lane credentials in the same shape as `resolveBuilderCredentials`, so
216
+ * a consumer moves lane by changing which resolver it calls and nothing else.
217
+ */
218
+ export declare function resolveBuilderGatewayCredentials(identity?: BuilderCredentialLookupIdentity): Promise<{
219
+ privateKey: string | null;
220
+ publicKey: string | null;
221
+ userId: string | null;
222
+ orgName: string | null;
223
+ orgKind: string | null;
224
+ subscription: string | null;
225
+ subscriptionLevel: string | null;
226
+ subscriptionName: string | null;
227
+ isEnterprise: boolean | null;
228
+ isFreeAccount: boolean | null;
229
+ }>;
230
+ export interface BuilderGatewayAuth {
231
+ /** `Bearer <token>` for the `Authorization` header. */
232
+ authorization: string;
233
+ /** Send as `x-builder-api-key`. Null only for a legacy single-key deployment. */
234
+ spaceId: string | null;
235
+ /** Send as `x-builder-user-id` when the lane carries a Builder user. */
236
+ userId: string | null;
237
+ }
238
+ /**
239
+ * The gate for gateway-lane features. Not `resolveHasBuilderPrivateKey`, which is
240
+ * identity-only and false on a credits site.
241
+ */
242
+ export declare function resolveHasBuilderGatewayCredential(): Promise<boolean>;
243
+ /** Gateway-lane `resolveBuilderAuthHeader`, same fall-through order. */
244
+ export declare function resolveBuilderGatewayAuth(): Promise<BuilderGatewayAuth | null>;
180
245
  /**
181
246
  * Stale markers expire so a rejected model or a transient gateway failure
182
247
  * cannot pin a signed-in user to "Builder not connected" forever. Only a
@@ -235,6 +300,21 @@ export declare function clearProviderCredentialAuthFailure(opts: {
235
300
  key?: string | null;
236
301
  value?: string | null;
237
302
  }): Promise<void>;
303
+ /**
304
+ * `builderCredentialFingerprint` needs both legacy keys, so on a credits site the
305
+ * legacy marker is a no-op and a rejected token would be resent forever. The
306
+ * gateway lane fingerprints its single token through the provider marker.
307
+ */
308
+ export declare function recordBuilderGatewayAuthFailure(details?: {
309
+ status?: number;
310
+ code?: string;
311
+ message?: string;
312
+ }): Promise<void>;
313
+ /** Each clear is a no-op off its own lane, so callers need not know which they are on. */
314
+ export declare function clearBuilderGatewayAuthFailure(creds: {
315
+ privateKey?: string | null;
316
+ publicKey?: string | null;
317
+ }): Promise<void>;
238
318
  /**
239
319
  * Write Builder credentials to `app_secrets`.
240
320
  *
@@ -18,7 +18,7 @@
18
18
  * (e.g. additional Builder-hosted services) without rewrites.
19
19
  */
20
20
  import { createHash } from "node:crypto";
21
- import { CREDENTIAL_STORE_UNAVAILABLE_ERROR_CODE } from "../agent/engine/credential-errors.js";
21
+ import { CREDENTIAL_STORE_UNAVAILABLE_ERROR_CODE, GATEWAY_UNAVAILABLE_VISITOR_MESSAGE, } from "../agent/engine/credential-errors.js";
22
22
  import { getAppConfig } from "../app-config/index.js";
23
23
  import { getDbExec, isLocalDatabase, isTransientDatabaseError, } from "../db/client.js";
24
24
  import { getOrgSetting } from "../settings/org-settings.js";
@@ -128,6 +128,12 @@ export function readDeployCredentialEnv(key) {
128
128
  }
129
129
  const APP_PROVIDED_DEPLOY_CREDENTIAL_KEYS = new Set([
130
130
  "ANTHROPIC_API_KEY",
131
+ // The Builder-credits pair pays for the deployed app's own model calls and
132
+ // carries no end-user identity — the token is scoped to ['gateway'] and can
133
+ // make no identity-bearing Builder call. The legacy BUILDER_PRIVATE_KEY /
134
+ // BUILDER_PUBLIC_KEY pair can, so it must never be added to this set.
135
+ "BUILDER_GATEWAY_TOKEN",
136
+ "BUILDER_GATEWAY_SPACE_ID",
131
137
  "EMAIL_FROM",
132
138
  "EMAIL_INBOUND_WEBHOOK_SECRET",
133
139
  "EMAIL_AGENT_ADDRESS",
@@ -180,6 +186,10 @@ export function canUseDeployCredentialFallbackForRequest(key) {
180
186
  return true;
181
187
  return isLocalDatabase();
182
188
  }
189
+ /** The raw `btk-` gateway PAT a Builder-credits deploy injects. */
190
+ export const BUILDER_GATEWAY_TOKEN_ENV_VAR = "BUILDER_GATEWAY_TOKEN";
191
+ /** The space id that pairs with it, sent as `x-builder-api-key`. */
192
+ export const BUILDER_GATEWAY_SPACE_ID_ENV_VAR = "BUILDER_GATEWAY_SPACE_ID";
183
193
  const BUILDER_CREDENTIAL_KEYS = [
184
194
  "BUILDER_PRIVATE_KEY",
185
195
  "BUILDER_PUBLIC_KEY",
@@ -702,6 +712,132 @@ export async function resolveBuilderCredentials(identity) {
702
712
  isFreeAccount,
703
713
  };
704
714
  }
715
+ /**
716
+ * Both halves required: the gateway 403s a token with no space id before it
717
+ * consults any route policy, so half a pair is not a usable credential.
718
+ */
719
+ export async function resolveUsableBuilderGatewayDeployCredentials() {
720
+ const token = canUseDeployCredentialFallbackForRequest(BUILDER_GATEWAY_TOKEN_ENV_VAR)
721
+ ? readDeployCredentialEnv(BUILDER_GATEWAY_TOKEN_ENV_VAR)
722
+ : undefined;
723
+ const spaceId = canUseDeployCredentialFallbackForRequest(BUILDER_GATEWAY_SPACE_ID_ENV_VAR)
724
+ ? readDeployCredentialEnv(BUILDER_GATEWAY_SPACE_ID_ENV_VAR)
725
+ : undefined;
726
+ if (!token || !spaceId)
727
+ return null;
728
+ const failure = await getProviderCredentialAuthFailure({
729
+ key: BUILDER_GATEWAY_TOKEN_ENV_VAR,
730
+ value: token,
731
+ });
732
+ return failure ? null : { token, spaceId };
733
+ }
734
+ /**
735
+ * Weaker than "usable" on purpose: a present-but-rejected token still means the
736
+ * reader is a visitor. Excludes the dev preview, which carries the same token but
737
+ * is read by the project owner.
738
+ */
739
+ export function isBuilderGatewayDeployConfigured() {
740
+ if (isHostedWorkspaceRuntime())
741
+ return false;
742
+ return Boolean(readDeployCredentialEnv(BUILDER_GATEWAY_TOKEN_ENV_VAR));
743
+ }
744
+ /** One decision for every gateway-lane consumer; a per-consumer copy drifts. */
745
+ export function gatewayLaneUnavailableMessage(ownerFacing) {
746
+ return isBuilderGatewayDeployConfigured()
747
+ ? GATEWAY_UNAVAILABLE_VISITOR_MESSAGE
748
+ : ownerFacing;
749
+ }
750
+ /**
751
+ * Gateway-lane credentials. Fall-through: the request's own Builder connection,
752
+ * then the deployment's credits pair, then the legacy pair.
753
+ *
754
+ * Identity-bearing calls (asset upload, design systems, Admin GraphQL, browser
755
+ * agent) must keep using `resolveBuilderCredentials`: a `['gateway']` token 403s
756
+ * on all of them.
757
+ */
758
+ export async function resolveBuilderGatewayCredentialsDetailed(identity) {
759
+ const scoped = await resolveBuilderCredentialsDetailed(identity);
760
+ if (scoped.source && scoped.source !== "env") {
761
+ return { ...scoped, lane: "identity" };
762
+ }
763
+ // A COMPLETE legacy pair in env is owner-configured, not deploy-injected, so it
764
+ // outranks the gateway here — `selectDetectedEngine` gives it priority for the
765
+ // same reason, and letting the gateway win would pick `builder` on the
766
+ // customer's own pair and then bill the call to the project's gateway space.
767
+ // An incomplete pair is not a usable credential and still falls through.
768
+ if (scoped.privateKey && scoped.publicKey) {
769
+ return { ...scoped, lane: "identity" };
770
+ }
771
+ const gateway = await resolveUsableBuilderGatewayDeployCredentials();
772
+ if (gateway) {
773
+ // Same discipline as `resolveBuilderCredential`: a deploy fallback must not
774
+ // turn an unreadable credential store into a clean connected result.
775
+ assertCredentialStoreReadable(scoped);
776
+ return {
777
+ privateKey: gateway.token,
778
+ publicKey: gateway.spaceId,
779
+ userId: null,
780
+ orgName: null,
781
+ orgKind: null,
782
+ subscription: null,
783
+ subscriptionLevel: null,
784
+ subscriptionName: null,
785
+ isEnterprise: null,
786
+ isFreeAccount: null,
787
+ source: "env",
788
+ lookupFailed: scoped.lookupFailed,
789
+ cause: scoped.cause,
790
+ lane: "gateway-deploy",
791
+ };
792
+ }
793
+ return { ...scoped, lane: scoped.source ? "identity" : null };
794
+ }
795
+ /**
796
+ * Gateway-lane credentials in the same shape as `resolveBuilderCredentials`, so
797
+ * a consumer moves lane by changing which resolver it calls and nothing else.
798
+ */
799
+ export async function resolveBuilderGatewayCredentials(identity) {
800
+ const { privateKey, publicKey, userId, orgName, orgKind, subscription, subscriptionLevel, subscriptionName, isEnterprise, isFreeAccount, } = await resolveBuilderGatewayCredentialsDetailed(identity);
801
+ return {
802
+ privateKey,
803
+ publicKey,
804
+ userId,
805
+ orgName,
806
+ orgKind,
807
+ subscription,
808
+ subscriptionLevel,
809
+ subscriptionName,
810
+ isEnterprise,
811
+ isFreeAccount,
812
+ };
813
+ }
814
+ /**
815
+ * The gate for gateway-lane features. Not `resolveHasBuilderPrivateKey`, which is
816
+ * identity-only and false on a credits site.
817
+ */
818
+ export async function resolveHasBuilderGatewayCredential() {
819
+ return Boolean(await resolveBuilderGatewayAuth());
820
+ }
821
+ /** Gateway-lane `resolveBuilderAuthHeader`, same fall-through order. */
822
+ export async function resolveBuilderGatewayAuth() {
823
+ const creds = await resolveBuilderGatewayCredentialsDetailed();
824
+ const token = creds.privateKey?.trim();
825
+ const spaceId = creds.publicKey?.trim();
826
+ if (token && spaceId) {
827
+ return {
828
+ authorization: `Bearer ${token}`,
829
+ spaceId,
830
+ userId: creds.userId?.trim() || null,
831
+ };
832
+ }
833
+ // Single-key deployments predate the space id and still authenticate on a
834
+ // `bpk-` private key alone. A gateway token never reaches this branch — its
835
+ // pair is required above.
836
+ const legacyKey = (await resolveBuilderPrivateKey())?.trim();
837
+ return legacyKey
838
+ ? { authorization: `Bearer ${legacyKey}`, spaceId: null, userId: null }
839
+ : null;
840
+ }
705
841
  const BUILDER_AUTH_FAILURE_SETTING_PREFIX = "builder-auth-failure:";
706
842
  /**
707
843
  * Stale markers expire so a rejected model or a transient gateway failure
@@ -882,6 +1018,37 @@ export async function clearProviderCredentialAuthFailure(opts) {
882
1018
  // A stale failure marker should not block writing or using fresh keys.
883
1019
  }
884
1020
  }
1021
+ /**
1022
+ * `builderCredentialFingerprint` needs both legacy keys, so on a credits site the
1023
+ * legacy marker is a no-op and a rejected token would be resent forever. The
1024
+ * gateway lane fingerprints its single token through the provider marker.
1025
+ */
1026
+ export async function recordBuilderGatewayAuthFailure(details) {
1027
+ try {
1028
+ const creds = await resolveBuilderGatewayCredentialsDetailed();
1029
+ if (creds.lane === "gateway-deploy" && creds.privateKey) {
1030
+ await recordProviderCredentialAuthFailure({
1031
+ key: BUILDER_GATEWAY_TOKEN_ENV_VAR,
1032
+ value: creds.privateKey,
1033
+ ...details,
1034
+ });
1035
+ return;
1036
+ }
1037
+ }
1038
+ catch {
1039
+ // Best-effort marker only; the chat error is still returned to the caller.
1040
+ return;
1041
+ }
1042
+ await recordBuilderCredentialAuthFailure(details);
1043
+ }
1044
+ /** Each clear is a no-op off its own lane, so callers need not know which they are on. */
1045
+ export async function clearBuilderGatewayAuthFailure(creds) {
1046
+ await clearBuilderCredentialAuthFailure(creds);
1047
+ await clearProviderCredentialAuthFailure({
1048
+ key: BUILDER_GATEWAY_TOKEN_ENV_VAR,
1049
+ value: creds.privateKey,
1050
+ });
1051
+ }
885
1052
  /**
886
1053
  * Write Builder credentials to `app_secrets`.
887
1054
  *
@@ -8,6 +8,6 @@ export declare function resolveGoogleRealtimeCredentials(opts: {
8
8
  orgId?: string | null;
9
9
  }): Promise<string | null>;
10
10
  export declare function createGoogleRealtimeSessionHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<GoogleRealtimeSessionResponse | {
11
- error: any;
11
+ error: string;
12
12
  }>>;
13
13
  export {};
@@ -4,7 +4,7 @@ import { resolveCredential } from "../credentials/index.js";
4
4
  import { getOrgContext } from "../org/context.js";
5
5
  import { readAppSecret } from "../secrets/storage.js";
6
6
  import { getSession } from "./auth.js";
7
- import { resolveBuilderCredentials } from "./credential-provider.js";
7
+ import { gatewayLaneUnavailableMessage, resolveBuilderGatewayCredentials, } from "./credential-provider.js";
8
8
  import { runWithRequestContext } from "./request-context.js";
9
9
  import { isSameOriginRequest } from "./request-origin.js";
10
10
  export async function resolveGoogleRealtimeCredentials(opts) {
@@ -85,11 +85,11 @@ export function createGoogleRealtimeSessionHandler() {
85
85
  error: "Configure GOOGLE_APPLICATION_CREDENTIALS in Settings to use Google realtime transcription.",
86
86
  };
87
87
  }
88
- const builderCreds = await resolveBuilderCredentials();
88
+ const builderCreds = await resolveBuilderGatewayCredentials();
89
89
  if (!builderCreds.privateKey || !builderCreds.publicKey) {
90
90
  setResponseStatus(event, 400);
91
91
  return {
92
- error: "Builder must be connected to mint a managed realtime transcription session.",
92
+ error: gatewayLaneUnavailableMessage("Builder must be connected to mint a managed realtime transcription session."),
93
93
  };
94
94
  }
95
95
  const apiHost = process.env.BUILDER_API_HOST || "https://api.builder.io";
@@ -119,16 +119,16 @@ export function createGoogleRealtimeSessionHandler() {
119
119
  .catch(() => ({ error: `HTTP ${res.status}` }));
120
120
  setResponseStatus(event, res.status);
121
121
  return {
122
- error: typeof errorBody?.error === "string"
122
+ error: gatewayLaneUnavailableMessage(typeof errorBody?.error === "string"
123
123
  ? errorBody.error
124
- : `Realtime session failed (${res.status})`,
124
+ : `Realtime session failed (${res.status})`),
125
125
  };
126
126
  }
127
127
  const payload = (await res.json());
128
128
  if (!payload?.websocketUrl) {
129
129
  setResponseStatus(event, 502);
130
130
  return {
131
- error: "Realtime transcription service did not return a websocket URL.",
131
+ error: gatewayLaneUnavailableMessage("Realtime transcription service did not return a websocket URL."),
132
132
  };
133
133
  }
134
134
  return payload;
@@ -70,7 +70,7 @@ export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploa
70
70
  export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, type PlatformAdapter, type IncomingMessage, type OutgoingMessage, type PlatformAdapterCapabilities, type ImmediateWebhookResponse, type IntegrationStatus, type IntegrationsPluginOptions, type IntegrationExecutionContext, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, type BuiltInChannelId, type ChannelCapabilities, type IntegrationAvailability, type IntegrationCatalogEntry, type IntegrationCategory, type IntegrationCredentialRequirement, type IntegrationIconKey, type IntegrationSupportMaturity, } from "../integrations/index.js";
71
71
  export { isElectron, isMobile, getOrigin, getAppBasePath, getAppUrl, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, encodeOAuthState, decodeOAuthState, resolveOAuthOwner, createOAuthSession, oauthCallbackResponse, oauthErrorPage, oauthDesktopExchangePage, type OAuthStatePayload, type OAuthOwnerResult, type OAuthSessionResult, } from "./google-oauth.js";
72
72
  export { buildWorkspaceProviderAuthorizationUrl, createWorkspaceProviderOAuthHandler, exchangeWorkspaceProviderOAuthCode, handleWorkspaceProviderOAuthCallback, handleWorkspaceProviderOAuthStart, isWorkspaceProviderOAuthFlowValid, mergeWorkspaceOAuthValues, resolveWorkspaceProviderIdentity, workspaceProviderOAuthPath, type GenericWorkspaceOAuthProvider, type WorkspaceProviderOAuthFlow, } from "./workspace-provider-oauth.js";
73
- export { FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, type BuilderCredentialsDetailed, } from "./credential-provider.js";
73
+ export { FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderGatewayCredentials, resolveBuilderGatewayCredentialsDetailed, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, type BuilderCredentialsDetailed, } from "./credential-provider.js";
74
74
  export { builderDesignSystemUrl, builderProjectBranchUrl, buildBuilderDesignSystemIndexFiles, collectBuilderDesignSystemGitHubFiles, createBuilderDesignSystemProxyFields, fetchBuilderDesignSystemDecodeJobStatus, fetchBuilderDesignSystemDocs, getBuilderDesignSystemsBaseUrl, hydrateBuilderDesignSystemReference, indexBuilderDesignSystem, localBuilderDesignSystemId, mimeTypeForBuilderDesignSystemFilename, parseBuilderDesignSystemProxyReference, startBuilderDesignSystemIndex, startBuilderDesignSystemUpload, type BuildBuilderDesignSystemIndexFilesOptions, type BuilderDesignSystemCodeFileInput, type BuilderDesignSystemDecodeJobStatus, type BuilderDesignSystemDocsOptions, type BuilderDesignSystemDocument, type BuilderDesignSystemHydratedReference, type BuilderDesignSystemIndexFile, type BuilderDesignSystemIndexFromSourcesOptions, type BuilderDesignSystemIndexOptions, type BuilderDesignSystemIndexResult, type BuilderDesignSystemGitHubFile, type BuilderDesignSystemGitHubFileCollection, type BuilderDesignSystemGitHubSource, type BuilderDesignSystemUploadAttachment, type BuilderDesignSystemUploadSlot, type BuilderDesignSystemProxyFields, type BuilderDesignSystemProxyFieldsOptions, type BuilderDesignSystemProxyReference, type BuilderDesignSystemSourceKind, } from "./builder-design-systems.js";
75
75
  export { createBuilderProject, ensureBuilderProject, findBuilderProjectForRepo, getBuilderBranchProjectId, isBuilderBranchingEnabled, requestBuilderBrowserConnection, resolveBuilderBranchProjectId, resolveIsBuilderBranchingEnabled, runBuilderAgent, type BuilderProjectResult, type RunBuilderAgentResult, } from "./builder-browser.js";
76
76
  export { ensureFusionContainer, sendFusionBranchMessage, pushFusionBranch, reserveFusionHostingSlug, deployFusionProject, getFusionDeploys, getFusionBranchEditorUrl, getFusionHostingUrl, type FusionBranchRef, type EnsureFusionContainerResult, type SendFusionMessageResult, } from "./fusion-app.js";
@@ -75,7 +75,12 @@ export { registerFileUploadProvider, unregisterFileUploadProvider, listFileUploa
75
75
  export { createIntegrationsPlugin, defaultIntegrationsPlugin, enqueueRemoteCommand, slackAdapter, telegramAdapter, whatsappAdapter, discordAdapter, microsoftTeamsAdapter, emailAdapter, assertPlatformCapability, BUILT_IN_INTEGRATION_CATALOG, INTEGRATION_CATEGORIES, getIntegrationCatalogEntry, listBuiltInChannelIntegrations, listIntegrationCatalog, } from "../integrations/index.js";
76
76
  export { isElectron, isMobile, getOrigin, getAppBasePath, getAppUrl, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, encodeOAuthState, decodeOAuthState, resolveOAuthOwner, createOAuthSession, oauthCallbackResponse, oauthErrorPage, oauthDesktopExchangePage, } from "./google-oauth.js";
77
77
  export { buildWorkspaceProviderAuthorizationUrl, createWorkspaceProviderOAuthHandler, exchangeWorkspaceProviderOAuthCode, handleWorkspaceProviderOAuthCallback, handleWorkspaceProviderOAuthStart, isWorkspaceProviderOAuthFlowValid, mergeWorkspaceOAuthValues, resolveWorkspaceProviderIdentity, workspaceProviderOAuthPath, } from "./workspace-provider-oauth.js";
78
- export { FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, } from "./credential-provider.js";
78
+ export { FeatureNotConfiguredError, hasBuilderPrivateKey, isBuilderEnvManaged, getBuilderProxyOrigin, getBuilderImageGenerationBaseUrl, getBuilderWebSearchBaseUrl, getBuilderAuthHeader, resolveBuilderPrivateKey, resolveBuilderAuthHeader, resolveHasBuilderPrivateKey, resolveHasCompleteBuilderConnection, resolveBuilderCredentials, resolveBuilderCredentialsDetailed,
79
+ // Gateway lane, for the metered surfaces a deployed site can call without an
80
+ // identity — image and video generation, realtime transcription. Falls
81
+ // through to the identity credential first, so a consumer moves lane by
82
+ // swapping the resolver and changing nothing else.
83
+ resolveBuilderGatewayCredentials, resolveBuilderGatewayCredentialsDetailed, resolveBuilderCredentialSource, resolveBuilderCredential, readDeployCredentialEnv, writeBuilderCredentials, deleteBuilderCredentials, resolveSecret, } from "./credential-provider.js";
79
84
  export { builderDesignSystemUrl, builderProjectBranchUrl, buildBuilderDesignSystemIndexFiles, collectBuilderDesignSystemGitHubFiles, createBuilderDesignSystemProxyFields, fetchBuilderDesignSystemDecodeJobStatus, fetchBuilderDesignSystemDocs, getBuilderDesignSystemsBaseUrl, hydrateBuilderDesignSystemReference, indexBuilderDesignSystem, localBuilderDesignSystemId, mimeTypeForBuilderDesignSystemFilename, parseBuilderDesignSystemProxyReference, startBuilderDesignSystemIndex, startBuilderDesignSystemUpload, } from "./builder-design-systems.js";
80
85
  export { createBuilderProject, ensureBuilderProject, findBuilderProjectForRepo, getBuilderBranchProjectId, isBuilderBranchingEnabled, requestBuilderBrowserConnection, resolveBuilderBranchProjectId, resolveIsBuilderBranchingEnabled, runBuilderAgent, } from "./builder-browser.js";
81
86
  export { ensureFusionContainer, sendFusionBranchMessage, pushFusionBranch, reserveFusionHostingSlug, deployFusionProject, getFusionDeploys, getFusionBranchEditorUrl, getFusionHostingUrl, } from "./fusion-app.js";
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
@@ -5,7 +5,7 @@ import { redactSensitiveFields, sanitizeToolErrorText, sanitizeToolErrorValue, }
5
5
  import { TOOL_SEARCH_ACTION_NAME } from "../agent/tool-search.js";
6
6
  import { parseAcceptLanguage } from "../localization/server.js";
7
7
  import { getSession } from "./auth.js";
8
- import { getBuilderGatewayBaseUrl, resolveBuilderCredentials, resolveSecret, } from "./credential-provider.js";
8
+ import { gatewayLaneUnavailableMessage, getBuilderGatewayBaseUrl, resolveBuilderGatewayCredentials, resolveSecret, } from "./credential-provider.js";
9
9
  import { getH3App } from "./framework-request-handler.js";
10
10
  import { runWithRequestContext } from "./request-context.js";
11
11
  import { isSameOriginRequest } from "./request-origin.js";
@@ -379,7 +379,7 @@ function createSessionHandler(tools, options) {
379
379
  ? { browserTabId: auth.browserTabId }
380
380
  : undefined,
381
381
  }, async () => {
382
- const builderCredentials = await resolveBuilderCredentials();
382
+ const builderCredentials = await resolveBuilderGatewayCredentials();
383
383
  const builderConfigured = Boolean(builderCredentials.privateKey?.trim() &&
384
384
  builderCredentials.publicKey?.trim());
385
385
  const apiKey = builderConfigured
@@ -388,7 +388,7 @@ function createSessionHandler(tools, options) {
388
388
  if (!builderConfigured && !apiKey) {
389
389
  setResponseStatus(event, 409);
390
390
  return {
391
- error: "Connect Builder (free tier available) or configure an OpenAI API key to use realtime voice.",
391
+ error: gatewayLaneUnavailableMessage("Connect Builder (free tier available) or configure an OpenAI API key to use realtime voice."),
392
392
  code: "realtime_voice_setup_required",
393
393
  };
394
394
  }
@@ -466,22 +466,28 @@ function createSessionHandler(tools, options) {
466
466
  setResponseStatus(event, 502);
467
467
  return {
468
468
  error: builderConfigured
469
- ? "Could not reach the Builder realtime voice gateway"
469
+ ? gatewayLaneUnavailableMessage("Could not reach the Builder realtime voice gateway")
470
470
  : "Could not reach the OpenAI Realtime API",
471
471
  };
472
472
  }
473
473
  if (!upstream.ok) {
474
474
  const detail = await safeOpenAiErrorDetail(upstream, builderConfigured ? builderCredentials.privateKey : apiKey);
475
475
  setResponseStatus(event, builderConfigured ? upstream.status : 502);
476
+ const rejection = `${builderConfigured ? "Builder" : "OpenAI"} rejected the realtime session (${upstream.status})${detail ? `: ${detail}` : ""}`;
476
477
  return {
477
- error: `${builderConfigured ? "Builder" : "OpenAI"} rejected the realtime session (${upstream.status})${detail ? `: ${detail}` : ""}`,
478
+ error: builderConfigured
479
+ ? gatewayLaneUnavailableMessage(rejection)
480
+ : rejection,
478
481
  };
479
482
  }
480
483
  const answerSdp = await upstream.text().catch(() => "");
481
484
  if (!answerSdp.trim()) {
482
485
  setResponseStatus(event, 502);
486
+ const emptyAnswer = `${builderConfigured ? "Builder" : "OpenAI"} returned an empty realtime session answer`;
483
487
  return {
484
- error: `${builderConfigured ? "Builder" : "OpenAI"} returned an empty realtime session answer`,
488
+ error: builderConfigured
489
+ ? gatewayLaneUnavailableMessage(emptyAnswer)
490
+ : emptyAnswer,
485
491
  };
486
492
  }
487
493
  setResponseStatus(event, upstream.status);
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -23,7 +23,7 @@ import { getOrgContext } from "../org/context.js";
23
23
  import { transcribeWithBuilder } from "../transcription/builder-transcription.js";
24
24
  import { applyVoiceContextReplacements, buildVoiceGuidanceBlock, parseVoiceContextPack, voiceContextTermsOnly, } from "../voice/index.js";
25
25
  import { getSession } from "./auth.js";
26
- import { resolveHasBuilderPrivateKey, resolveSecret, } from "./credential-provider.js";
26
+ import { gatewayLaneUnavailableMessage, resolveHasBuilderGatewayCredential, resolveSecret, } from "./credential-provider.js";
27
27
  import { runWithRequestContext } from "./request-context.js";
28
28
  import { isSameOriginRequest } from "./request-origin.js";
29
29
  const WHISPER_URL = "https://api.openai.com/v1/audio/transcriptions";
@@ -149,7 +149,7 @@ export function createTranscribeVoiceHandler() {
149
149
  const withRequestContext = async (fn) => requestContext.userEmail
150
150
  ? runWithRequestContext(requestContext, fn)
151
151
  : fn();
152
- const hasBuilderPrivateKey = async () => withRequestContext(() => resolveHasBuilderPrivateKey());
152
+ const hasBuilderCredential = async () => withRequestContext(() => resolveHasBuilderGatewayCredential());
153
153
  const transcribeWithBuilderForRequest = (opts) => withRequestContext(() => transcribeWithBuilder(opts));
154
154
  const sessionId = session?.email ?? "local";
155
155
  let providerPref;
@@ -212,7 +212,7 @@ export function createTranscribeVoiceHandler() {
212
212
  instructions: voiceGuidance,
213
213
  contextPack: voiceContext,
214
214
  providerPref,
215
- hasBuilderPrivateKey,
215
+ hasBuilderCredential,
216
216
  withRequestContext,
217
217
  resolveApiKey,
218
218
  clientAbortSignal: clientAbort,
@@ -266,10 +266,10 @@ export function createTranscribeVoiceHandler() {
266
266
  const label = providerPref === "builder-gemini"
267
267
  ? "Builder Gemini Flash-Lite"
268
268
  : "Builder";
269
- if (!(await hasBuilderPrivateKey())) {
269
+ if (!(await hasBuilderCredential())) {
270
270
  setResponseStatus(event, 400);
271
271
  return {
272
- error: `${label} is selected but Builder.io is not connected. Connect Builder.io (free tier available) in Settings, or change the provider preference.`,
272
+ error: gatewayLaneUnavailableMessage(`${label} is selected but Builder.io is not connected. Connect Builder.io (free tier available) in Settings, or change the provider preference.`),
273
273
  };
274
274
  }
275
275
  try {
@@ -288,10 +288,12 @@ export function createTranscribeVoiceHandler() {
288
288
  const message = err?.message ?? String(err);
289
289
  if (message.includes("credits exhausted")) {
290
290
  setResponseStatus(event, 402);
291
- return { error: message };
291
+ return { error: gatewayLaneUnavailableMessage(message) };
292
292
  }
293
293
  setResponseStatus(event, 502);
294
- return { error: `${label} transcription failed: ${message}` };
294
+ return {
295
+ error: gatewayLaneUnavailableMessage(`${label} transcription failed: ${message}`),
296
+ };
295
297
  }
296
298
  }
297
299
  if (providerPref === "groq") {
@@ -322,7 +324,7 @@ export function createTranscribeVoiceHandler() {
322
324
  // ── Builder Gemini Flash-Lite path ─────────────────────────────────
323
325
  // First-priority in auto mode when Builder is connected. This lets users
324
326
  // try Gemini 3.1 Flash-Lite without bringing their own Google key.
325
- if (providerPref !== "openai" && (await hasBuilderPrivateKey())) {
327
+ if (providerPref !== "openai" && (await hasBuilderCredential())) {
326
328
  try {
327
329
  const result = await transcribeWithBuilderForRequest({
328
330
  audioBytes,
@@ -339,7 +341,7 @@ export function createTranscribeVoiceHandler() {
339
341
  // a specific upgrade prompt.
340
342
  if (message.includes("credits exhausted")) {
341
343
  setResponseStatus(event, 402);
342
- return { error: message };
344
+ return { error: gatewayLaneUnavailableMessage(message) };
343
345
  }
344
346
  builderError = message;
345
347
  }
@@ -401,9 +403,9 @@ export function createTranscribeVoiceHandler() {
401
403
  if (!provider) {
402
404
  setResponseStatus(event, builderError ? 502 : 400);
403
405
  return {
404
- error: builderError
406
+ error: gatewayLaneUnavailableMessage(builderError
405
407
  ? `Builder transcription failed: ${builderError}. Add GEMINI_API_KEY, GROQ_API_KEY, or OPENAI_API_KEY in Settings → API Keys to enable a fallback provider.`
406
- : "No voice transcription provider configured. Connect Builder.io (free tier available) or add GEMINI_API_KEY / GROQ_API_KEY / OPENAI_API_KEY in Settings → API Keys.",
408
+ : "No voice transcription provider configured. Connect Builder.io (free tier available) or add GEMINI_API_KEY / GROQ_API_KEY / OPENAI_API_KEY in Settings → API Keys."),
407
409
  };
408
410
  }
409
411
  return await callWhisperCompat({
@@ -477,7 +479,7 @@ async function callWhisperCompat({ event, provider, audioBytes, mime, language,
477
479
  clearTimeout(timeout);
478
480
  }
479
481
  }
480
- async function cleanupTranscriptText({ event, text, instructions, contextPack, providerPref, hasBuilderPrivateKey, withRequestContext, resolveApiKey, clientAbortSignal, }) {
482
+ async function cleanupTranscriptText({ event, text, instructions, contextPack, providerPref, hasBuilderCredential, withRequestContext, resolveApiKey, clientAbortSignal, }) {
481
483
  const original = text.trim();
482
484
  if (!original)
483
485
  return { text: "" };
@@ -486,10 +488,10 @@ async function cleanupTranscriptText({ event, text, instructions, contextPack, p
486
488
  return { text: finalizeText(original) };
487
489
  }
488
490
  if (providerPref === "builder" || providerPref === "builder-gemini") {
489
- if (!(await hasBuilderPrivateKey())) {
491
+ if (!(await hasBuilderCredential())) {
490
492
  setResponseStatus(event, 400);
491
493
  return {
492
- error: "Builder.io cleanup is selected but Builder.io is not connected. Connect Builder.io (free tier available) in Settings, or change the provider preference.",
494
+ error: gatewayLaneUnavailableMessage("Builder.io cleanup is selected but Builder.io is not connected. Connect Builder.io (free tier available) in Settings, or change the provider preference."),
493
495
  };
494
496
  }
495
497
  try {
@@ -506,7 +508,7 @@ async function cleanupTranscriptText({ event, text, instructions, contextPack, p
506
508
  catch (err) {
507
509
  setResponseStatus(event, 502);
508
510
  return {
509
- error: `Builder.io cleanup failed: ${err?.message ?? String(err)}`,
511
+ error: gatewayLaneUnavailableMessage(`Builder.io cleanup failed: ${err?.message ?? String(err)}`),
510
512
  };
511
513
  }
512
514
  }
@@ -560,7 +562,7 @@ async function cleanupTranscriptText({ event, text, instructions, contextPack, p
560
562
  };
561
563
  }
562
564
  }
563
- if (await hasBuilderPrivateKey()) {
565
+ if (await hasBuilderCredential()) {
564
566
  try {
565
567
  const cleaned = await withRequestContext(() => cleanupWithBuilder({ text: original, instructions, clientAbortSignal }));
566
568
  if (cleaned)