@agent-native/core-corpus 0.216.0 → 0.217.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +1 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/calendar/CHANGELOG.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +8 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/Index.tsx +10 -3
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/slides/CHANGELOG.md +6 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { fail, isActionContractError } from "@agent-native/core/action";
|
|
1
2
|
import {
|
|
3
|
+
BUILDER_CONTENT_READ_SCOPE,
|
|
2
4
|
BUILDER_PUBLISH_MCP_RESOURCE,
|
|
3
5
|
resolveBuilderCredential,
|
|
4
6
|
resolveBuilderRequestAuthorization,
|
|
@@ -35,10 +37,14 @@ export interface BuilderCmsReadProgress {
|
|
|
35
37
|
pageSize: number;
|
|
36
38
|
startOffset: number;
|
|
37
39
|
nextOffset: number;
|
|
40
|
+
nextCursor?: string;
|
|
38
41
|
fetchedEntryCount: number;
|
|
39
42
|
hasMore: boolean;
|
|
40
43
|
partial: boolean;
|
|
41
44
|
readMode: "builder-api" | "mcp" | "none";
|
|
45
|
+
sourceSpacePublicKey?: string;
|
|
46
|
+
sourceConnectionId?: string;
|
|
47
|
+
observedCollectionCount?: number;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
export interface BuilderCmsEntryLiveState {
|
|
@@ -167,6 +173,7 @@ export type BuilderCmsContentEntryReadResult =
|
|
|
167
173
|
entry: null;
|
|
168
174
|
providerStatus:
|
|
169
175
|
| "http_404"
|
|
176
|
+
| "http_200_not_found"
|
|
170
177
|
| "http_200_unexpected_entry"
|
|
171
178
|
| "mcp_not_found";
|
|
172
179
|
};
|
|
@@ -177,6 +184,7 @@ export class BuilderCmsContentEntryReadError extends Error {
|
|
|
177
184
|
readonly reason:
|
|
178
185
|
| "auth_failed"
|
|
179
186
|
| "access_denied"
|
|
187
|
+
| "not_found"
|
|
180
188
|
| "transient_read_failure"
|
|
181
189
|
| "malformed_body",
|
|
182
190
|
readonly providerStatus: string,
|
|
@@ -196,6 +204,15 @@ type BuilderMcpToolResult = {
|
|
|
196
204
|
content?: BuilderMcpContentPart[];
|
|
197
205
|
};
|
|
198
206
|
|
|
207
|
+
export const RETRYABLE_BUILDER_SOURCE_BINDING_ERRORS = new Set([
|
|
208
|
+
"builder_connection_required",
|
|
209
|
+
"builder_oauth_reauthorization_required",
|
|
210
|
+
"builder_source_binding_invalid",
|
|
211
|
+
"builder_source_binding_required",
|
|
212
|
+
"builder_source_connection_mismatch",
|
|
213
|
+
"builder_source_space_mismatch",
|
|
214
|
+
]);
|
|
215
|
+
|
|
199
216
|
const BUILDER_CMS_DEFAULT_READ_LIMIT = 500;
|
|
200
217
|
const BUILDER_CMS_MAX_READ_LIMIT = 10_000;
|
|
201
218
|
const BUILDER_CMS_PAGE_SIZE = 100;
|
|
@@ -322,6 +339,12 @@ function builderContentApiHost() {
|
|
|
322
339
|
).replace(/\/+$/, "");
|
|
323
340
|
}
|
|
324
341
|
|
|
342
|
+
// The general read routes (/api/v1/models, /api/v1/query-data) carry the
|
|
343
|
+
// user's OAuth bearer, and Builder serves them on this host (api.builder.io
|
|
344
|
+
// answers them with 404). It is a constant on purpose: no host override may
|
|
345
|
+
// redirect the bearer.
|
|
346
|
+
const BUILDER_OAUTH_READ_HOST = "https://cdn.builder.io";
|
|
347
|
+
|
|
325
348
|
function entryArrayFromResponse(value: unknown) {
|
|
326
349
|
if (Array.isArray(value)) return value;
|
|
327
350
|
if (!value || typeof value !== "object") return [];
|
|
@@ -329,6 +352,33 @@ function entryArrayFromResponse(value: unknown) {
|
|
|
329
352
|
return Array.isArray(record.results) ? record.results : [];
|
|
330
353
|
}
|
|
331
354
|
|
|
355
|
+
function parseBuilderGeneralQueryDataResponse(value: unknown) {
|
|
356
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
357
|
+
fail("Builder query-data returned a malformed response.", {
|
|
358
|
+
errorCode: "builder_response_invalid",
|
|
359
|
+
statusCode: 424,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const record = value as Record<string, unknown>;
|
|
363
|
+
if (!Array.isArray(record.results)) {
|
|
364
|
+
fail("Builder query-data did not return a results array.", {
|
|
365
|
+
errorCode: "builder_response_invalid",
|
|
366
|
+
statusCode: 424,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (
|
|
370
|
+
typeof record.totalCount !== "number" ||
|
|
371
|
+
!Number.isInteger(record.totalCount) ||
|
|
372
|
+
record.totalCount < 0
|
|
373
|
+
) {
|
|
374
|
+
fail("Builder query-data read did not return a valid totalCount.", {
|
|
375
|
+
errorCode: "builder_response_invalid",
|
|
376
|
+
statusCode: 424,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return { results: record.results, totalCount: record.totalCount };
|
|
380
|
+
}
|
|
381
|
+
|
|
332
382
|
function stringFromUnknown(value: unknown) {
|
|
333
383
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
334
384
|
}
|
|
@@ -436,13 +486,22 @@ async function fetchBuilderContentPage(args: {
|
|
|
436
486
|
fetchImpl: FetchLike;
|
|
437
487
|
url: URL;
|
|
438
488
|
privateKey?: string;
|
|
489
|
+
signal?: AbortSignal;
|
|
439
490
|
}): Promise<Response> {
|
|
440
491
|
let lastError: unknown = null;
|
|
441
492
|
for (let attempt = 0; attempt <= BUILDER_CMS_READ_RETRIES; attempt += 1) {
|
|
493
|
+
if (args.signal?.aborted) {
|
|
494
|
+
throw args.signal.reason instanceof Error
|
|
495
|
+
? args.signal.reason
|
|
496
|
+
: new Error("Builder request was aborted.");
|
|
497
|
+
}
|
|
442
498
|
try {
|
|
443
499
|
const headers: Record<string, string> = { accept: "application/json" };
|
|
444
500
|
if (args.privateKey) headers.authorization = `Bearer ${args.privateKey}`;
|
|
445
|
-
const response = await args.fetchImpl(args.url, {
|
|
501
|
+
const response = await args.fetchImpl(args.url, {
|
|
502
|
+
headers,
|
|
503
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
504
|
+
});
|
|
446
505
|
if (
|
|
447
506
|
!retryableBuilderReadStatus(response.status) ||
|
|
448
507
|
attempt === BUILDER_CMS_READ_RETRIES
|
|
@@ -451,7 +510,7 @@ async function fetchBuilderContentPage(args: {
|
|
|
451
510
|
}
|
|
452
511
|
} catch (error) {
|
|
453
512
|
lastError = error;
|
|
454
|
-
if (attempt === BUILDER_CMS_READ_RETRIES) {
|
|
513
|
+
if (args.signal?.aborted || attempt === BUILDER_CMS_READ_RETRIES) {
|
|
455
514
|
throw error;
|
|
456
515
|
}
|
|
457
516
|
}
|
|
@@ -486,11 +545,221 @@ function builderMcpEndpoint(
|
|
|
486
545
|
).replace(/\/+$/, "");
|
|
487
546
|
}
|
|
488
547
|
|
|
489
|
-
|
|
490
|
-
return
|
|
491
|
-
|
|
492
|
-
|
|
548
|
+
function isBuilderOAuthReauthorizationRequired(error: unknown) {
|
|
549
|
+
return (
|
|
550
|
+
isActionContractError(error) &&
|
|
551
|
+
error.errorCode === "builder_oauth_reauthorization_required"
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isBuilderReadSourceBound(
|
|
556
|
+
expectedSourceSpace?: string | null,
|
|
557
|
+
expectedSourceConnectionId?: string | null,
|
|
558
|
+
) {
|
|
559
|
+
const hasSourceSpace = Boolean(expectedSourceSpace?.trim());
|
|
560
|
+
const hasSourceConnectionId = Boolean(expectedSourceConnectionId?.trim());
|
|
561
|
+
if (hasSourceSpace !== hasSourceConnectionId) {
|
|
562
|
+
fail(
|
|
563
|
+
"Builder source binding must include both its space and connection. Reconnect the source before reading.",
|
|
564
|
+
{ errorCode: "builder_source_binding_invalid", statusCode: 412 },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return hasSourceSpace;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function readGeneralBuilderContentAuthorization(sourceBound: boolean) {
|
|
571
|
+
try {
|
|
572
|
+
return {
|
|
573
|
+
authorization: await resolveBuilderRequestAuthorization({
|
|
574
|
+
oauthResource: "general",
|
|
575
|
+
requiredScope: BUILDER_CONTENT_READ_SCOPE,
|
|
576
|
+
legacyCredentialKeys: [],
|
|
577
|
+
}),
|
|
578
|
+
reauthorization: null,
|
|
579
|
+
};
|
|
580
|
+
} catch (error) {
|
|
581
|
+
// The shared resolver throws instead of returning null when a grant
|
|
582
|
+
// predates builder:content:read. Only an unbound source may keep its
|
|
583
|
+
// previous read path until the user re-authorizes.
|
|
584
|
+
if (sourceBound || !isBuilderOAuthReauthorizationRequired(error)) {
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
return { authorization: null, reauthorization: error };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function readBuilderCmsAuthorization(sourceBound = false) {
|
|
592
|
+
const general = await readGeneralBuilderContentAuthorization(sourceBound);
|
|
593
|
+
if (general.authorization) return general.authorization;
|
|
594
|
+
try {
|
|
595
|
+
const publish = await resolveBuilderRequestAuthorization({
|
|
596
|
+
oauthResource: "publish",
|
|
597
|
+
legacyCredentialKeys: ["BUILDER_PRIVATE_KEY", "BUILDER_CMS_PRIVATE_KEY"],
|
|
598
|
+
});
|
|
599
|
+
if (publish || !general.reauthorization) return publish;
|
|
600
|
+
} catch (error) {
|
|
601
|
+
if (!general.reauthorization) throw error;
|
|
602
|
+
}
|
|
603
|
+
throw general.reauthorization;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function isGeneralBuilderOAuth(
|
|
607
|
+
authorization: BuilderRequestAuthorization | null,
|
|
608
|
+
): authorization is BuilderRequestAuthorization & {
|
|
609
|
+
source: "oauth";
|
|
610
|
+
oauthResource: "general";
|
|
611
|
+
} {
|
|
612
|
+
return (
|
|
613
|
+
authorization?.source === "oauth" &&
|
|
614
|
+
authorization.oauthResource === "general"
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function assertBuilderReadSourceAuthorization(
|
|
619
|
+
authorization: BuilderRequestAuthorization | null,
|
|
620
|
+
expectedSourceSpace?: string | null,
|
|
621
|
+
expectedSourceConnectionId?: string | null,
|
|
622
|
+
) {
|
|
623
|
+
if (
|
|
624
|
+
isBuilderReadSourceBound(expectedSourceSpace, expectedSourceConnectionId) &&
|
|
625
|
+
!isGeneralBuilderOAuth(authorization)
|
|
626
|
+
) {
|
|
627
|
+
fail(
|
|
628
|
+
"This Builder source's OAuth connection is unavailable. Reconnect the source before reading.",
|
|
629
|
+
{ errorCode: "builder_connection_required", statusCode: 412 },
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function requireBuilderOAuthPublicKey(
|
|
635
|
+
authorization: BuilderRequestAuthorization,
|
|
636
|
+
expectedSourceSpace?: string | null,
|
|
637
|
+
expectedSourceConnectionId?: string | null,
|
|
638
|
+
) {
|
|
639
|
+
const selected = authorization.oauthSelectedPublicKey;
|
|
640
|
+
if (!selected) {
|
|
641
|
+
fail(
|
|
642
|
+
"Builder OAuth access did not identify its selected space. Reconnect Builder.io in Settings.",
|
|
643
|
+
{ errorCode: "builder_connection_required", statusCode: 412 },
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (expectedSourceSpace && selected !== expectedSourceSpace) {
|
|
647
|
+
fail(
|
|
648
|
+
"The connected Builder space does not match this Content source. Reconnect the source's Builder space before continuing.",
|
|
649
|
+
{ errorCode: "builder_source_space_mismatch", statusCode: 409 },
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
if (
|
|
653
|
+
expectedSourceConnectionId &&
|
|
654
|
+
authorization.oauthConnectionId !== expectedSourceConnectionId
|
|
655
|
+
) {
|
|
656
|
+
fail(
|
|
657
|
+
"The connected Builder credential does not match this Content source. Reconnect the source before continuing.",
|
|
658
|
+
{ errorCode: "builder_source_connection_mismatch", statusCode: 409 },
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return selected;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function readBuilderCmsGeneralModels(args: {
|
|
665
|
+
authorization: BuilderRequestAuthorization;
|
|
666
|
+
expectedSourceSpace?: string | null;
|
|
667
|
+
expectedSourceConnectionId?: string | null;
|
|
668
|
+
fetchImpl: FetchLike;
|
|
669
|
+
signal?: AbortSignal;
|
|
670
|
+
}) {
|
|
671
|
+
const publicKey = requireBuilderOAuthPublicKey(
|
|
672
|
+
args.authorization,
|
|
673
|
+
args.expectedSourceSpace,
|
|
674
|
+
args.expectedSourceConnectionId,
|
|
675
|
+
);
|
|
676
|
+
const url = new URL("/api/v1/models", BUILDER_OAUTH_READ_HOST);
|
|
677
|
+
url.searchParams.set("apiKey", publicKey);
|
|
678
|
+
const response = await fetchBuilderContentPage({
|
|
679
|
+
fetchImpl: args.fetchImpl,
|
|
680
|
+
url,
|
|
681
|
+
privateKey: args.authorization.token,
|
|
682
|
+
signal: args.signal,
|
|
683
|
+
});
|
|
684
|
+
if (response.status === 401) {
|
|
685
|
+
fail(
|
|
686
|
+
"Builder rejected this Builder.io connection. Reconnect Builder.io in Settings.",
|
|
687
|
+
{ errorCode: "builder_connection_required", statusCode: 412 },
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (!response.ok) {
|
|
691
|
+
fail(`Builder model discovery failed with HTTP ${response.status}.`, {
|
|
692
|
+
errorCode: "builder_upstream_failed",
|
|
693
|
+
statusCode: 424,
|
|
694
|
+
details: { httpStatus: response.status },
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
return builderMcpModelsFromToolResponse((await response.json()) as unknown);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Body hydration reads entries one at a time and each read needs the model id.
|
|
701
|
+
// Rediscovering it per entry costs a request per row, so a large source floods
|
|
702
|
+
// Builder.
|
|
703
|
+
const BUILDER_MODEL_ID_TTL_MS = 60_000;
|
|
704
|
+
const builderModelIdCache = new WeakMap<
|
|
705
|
+
FetchLike,
|
|
706
|
+
Map<string, { expiresAt: number; modelId: string }>
|
|
707
|
+
>();
|
|
708
|
+
|
|
709
|
+
async function resolveBuilderCmsGeneralModelId(args: {
|
|
710
|
+
model: string;
|
|
711
|
+
authorization: BuilderRequestAuthorization;
|
|
712
|
+
expectedSourceSpace?: string | null;
|
|
713
|
+
expectedSourceConnectionId?: string | null;
|
|
714
|
+
fetchImpl: FetchLike;
|
|
715
|
+
signal?: AbortSignal;
|
|
716
|
+
}) {
|
|
717
|
+
const publicKey = requireBuilderOAuthPublicKey(
|
|
718
|
+
args.authorization,
|
|
719
|
+
args.expectedSourceSpace,
|
|
720
|
+
args.expectedSourceConnectionId,
|
|
721
|
+
);
|
|
722
|
+
const key = [
|
|
723
|
+
publicKey,
|
|
724
|
+
args.authorization.oauthConnectionId ?? "",
|
|
725
|
+
args.model.trim().toLowerCase(),
|
|
726
|
+
].join("\u0000");
|
|
727
|
+
const entries = builderModelIdCache.get(args.fetchImpl) ?? new Map();
|
|
728
|
+
builderModelIdCache.set(args.fetchImpl, entries);
|
|
729
|
+
const cached = entries.get(key);
|
|
730
|
+
if (cached && cached.expiresAt > Date.now()) return cached.modelId;
|
|
731
|
+
const modelId = await discoverBuilderCmsGeneralModelId(args);
|
|
732
|
+
entries.set(key, {
|
|
733
|
+
expiresAt: Date.now() + BUILDER_MODEL_ID_TTL_MS,
|
|
734
|
+
modelId,
|
|
493
735
|
});
|
|
736
|
+
return modelId;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function discoverBuilderCmsGeneralModelId(args: {
|
|
740
|
+
model: string;
|
|
741
|
+
authorization: BuilderRequestAuthorization;
|
|
742
|
+
expectedSourceSpace?: string | null;
|
|
743
|
+
expectedSourceConnectionId?: string | null;
|
|
744
|
+
fetchImpl: FetchLike;
|
|
745
|
+
signal?: AbortSignal;
|
|
746
|
+
}) {
|
|
747
|
+
const requestedModel = args.model.trim().toLowerCase();
|
|
748
|
+
const models = await readBuilderCmsGeneralModels(args);
|
|
749
|
+
const idMatch = models.find(
|
|
750
|
+
(candidate) => candidate.id.trim().toLowerCase() === requestedModel,
|
|
751
|
+
);
|
|
752
|
+
const nameMatches = models.filter(
|
|
753
|
+
(candidate) => candidate.name.trim().toLowerCase() === requestedModel,
|
|
754
|
+
);
|
|
755
|
+
const model = nameMatches.length === 1 ? nameMatches[0] : idMatch;
|
|
756
|
+
if (!model) {
|
|
757
|
+
fail(`Builder model ${args.model} was not found.`, {
|
|
758
|
+
errorCode: "builder_model_not_found",
|
|
759
|
+
statusCode: 404,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return model.id;
|
|
494
763
|
}
|
|
495
764
|
|
|
496
765
|
function parseBuilderMcpToolJson(value: unknown) {
|
|
@@ -760,17 +1029,40 @@ function stringOptionsFromUnknown(value: unknown): string[] | undefined {
|
|
|
760
1029
|
function builderMcpModelsFromToolResponse(
|
|
761
1030
|
response: unknown,
|
|
762
1031
|
): BuilderCmsModelSummary[] {
|
|
763
|
-
if (!response || typeof response !== "object")
|
|
1032
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
1033
|
+
fail("Builder model discovery returned a malformed response.", {
|
|
1034
|
+
errorCode: "builder_response_invalid",
|
|
1035
|
+
statusCode: 424,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
764
1038
|
const record = response as Record<string, unknown>;
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
.sort((a, b) => {
|
|
770
|
-
if (a.name === "agent-native-blog-article-test") return -1;
|
|
771
|
-
if (b.name === "agent-native-blog-article-test") return 1;
|
|
772
|
-
return a.displayName.localeCompare(b.displayName);
|
|
1039
|
+
if (!Array.isArray(record.models)) {
|
|
1040
|
+
fail("Builder model discovery did not return a models array.", {
|
|
1041
|
+
errorCode: "builder_response_invalid",
|
|
1042
|
+
statusCode: 424,
|
|
773
1043
|
});
|
|
1044
|
+
}
|
|
1045
|
+
// /api/v1/models also lists archived models, which Builder's own model list
|
|
1046
|
+
// hides; without this the source picker offers them.
|
|
1047
|
+
const models = record.models
|
|
1048
|
+
.filter(
|
|
1049
|
+
(model) =>
|
|
1050
|
+
!model ||
|
|
1051
|
+
typeof model !== "object" ||
|
|
1052
|
+
(model as Record<string, unknown>).archived !== true,
|
|
1053
|
+
)
|
|
1054
|
+
.map((model) => normalizeBuilderCmsModel(model));
|
|
1055
|
+
if (models.some((model) => !model)) {
|
|
1056
|
+
fail("Builder model discovery returned a malformed model.", {
|
|
1057
|
+
errorCode: "builder_response_invalid",
|
|
1058
|
+
statusCode: 424,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
return (models as BuilderCmsModelSummary[]).sort((a, b) => {
|
|
1062
|
+
if (a.name === "agent-native-blog-article-test") return -1;
|
|
1063
|
+
if (b.name === "agent-native-blog-article-test") return 1;
|
|
1064
|
+
return a.displayName.localeCompare(b.displayName);
|
|
1065
|
+
});
|
|
774
1066
|
}
|
|
775
1067
|
|
|
776
1068
|
async function initializeBuilderMcp(args: {
|
|
@@ -882,6 +1174,8 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
|
|
|
882
1174
|
limit?: number;
|
|
883
1175
|
maxPages?: number;
|
|
884
1176
|
offset?: number;
|
|
1177
|
+
startingFetchedEntryCount?: number;
|
|
1178
|
+
expectedCollectionCount?: number;
|
|
885
1179
|
fetchImpl: FetchLike;
|
|
886
1180
|
privateKey: string;
|
|
887
1181
|
endpoint: string;
|
|
@@ -894,6 +1188,8 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
|
|
|
894
1188
|
typeof args.offset === "number" && Number.isFinite(args.offset)
|
|
895
1189
|
? Math.max(0, Math.floor(args.offset))
|
|
896
1190
|
: 0;
|
|
1191
|
+
const startingFetchedEntryCount =
|
|
1192
|
+
args.startingFetchedEntryCount ?? startOffset;
|
|
897
1193
|
const connection = await initializeBuilderMcp({
|
|
898
1194
|
endpoint,
|
|
899
1195
|
privateKey: args.privateKey,
|
|
@@ -909,6 +1205,7 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
|
|
|
909
1205
|
let pagesRead = 0;
|
|
910
1206
|
let hasMore = false;
|
|
911
1207
|
let offset = startOffset;
|
|
1208
|
+
let observedCollectionCount: number | null = null;
|
|
912
1209
|
while (contentEntries.length < requestedLimit) {
|
|
913
1210
|
const pageLimit = Math.min(
|
|
914
1211
|
BUILDER_CMS_PAGE_SIZE,
|
|
@@ -955,8 +1252,35 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
|
|
|
955
1252
|
pageEntries,
|
|
956
1253
|
);
|
|
957
1254
|
pagesRead += 1;
|
|
958
|
-
offset +=
|
|
1255
|
+
offset += providerEntryCount;
|
|
959
1256
|
const totalCount = builderMcpTotalCount(contentJson);
|
|
1257
|
+
if (observedCollectionCount === null && totalCount !== null) {
|
|
1258
|
+
observedCollectionCount = totalCount;
|
|
1259
|
+
if (
|
|
1260
|
+
args.expectedCollectionCount !== undefined &&
|
|
1261
|
+
totalCount !== args.expectedCollectionCount
|
|
1262
|
+
) {
|
|
1263
|
+
fail(
|
|
1264
|
+
"Builder MCP collection changed between refresh pages; restart the source refresh before mutating the snapshot.",
|
|
1265
|
+
{ errorCode: "builder_source_changed", statusCode: 409 },
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
} else if (
|
|
1269
|
+
totalCount !== null &&
|
|
1270
|
+
observedCollectionCount !== null &&
|
|
1271
|
+
totalCount !== observedCollectionCount
|
|
1272
|
+
) {
|
|
1273
|
+
fail(
|
|
1274
|
+
"Builder MCP collection changed during pagination; refresh the source to restart the observed collection read.",
|
|
1275
|
+
{ errorCode: "builder_source_changed", statusCode: 409 },
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (startOffset > 0 && observedCollectionCount === null) {
|
|
1279
|
+
fail(
|
|
1280
|
+
"Builder MCP continuation has no saved collection count. Start a full refresh before continuing.",
|
|
1281
|
+
{ errorCode: "builder_source_continuation_mismatch", statusCode: 409 },
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
960
1284
|
hasMore =
|
|
961
1285
|
totalCount !== null
|
|
962
1286
|
? offset < totalCount
|
|
@@ -979,10 +1303,11 @@ async function readBuilderCmsContentEntriesViaMcp(args: {
|
|
|
979
1303
|
pageSize: BUILDER_CMS_PAGE_SIZE,
|
|
980
1304
|
startOffset,
|
|
981
1305
|
nextOffset: offset,
|
|
982
|
-
fetchedEntryCount:
|
|
1306
|
+
fetchedEntryCount: startingFetchedEntryCount + contentEntries.length,
|
|
983
1307
|
hasMore,
|
|
984
1308
|
partial: hasMore,
|
|
985
1309
|
readMode: "mcp",
|
|
1310
|
+
observedCollectionCount: observedCollectionCount ?? undefined,
|
|
986
1311
|
},
|
|
987
1312
|
};
|
|
988
1313
|
}
|
|
@@ -1150,11 +1475,334 @@ async function readBuilderCmsContentEntriesViaContentApi(args: {
|
|
|
1150
1475
|
};
|
|
1151
1476
|
}
|
|
1152
1477
|
|
|
1478
|
+
async function readBuilderCmsContentEntriesViaGeneralApi(args: {
|
|
1479
|
+
model: string;
|
|
1480
|
+
limit?: number;
|
|
1481
|
+
maxPages?: number;
|
|
1482
|
+
offset?: number;
|
|
1483
|
+
afterId?: string;
|
|
1484
|
+
fetchImpl: FetchLike;
|
|
1485
|
+
authorization: BuilderRequestAuthorization;
|
|
1486
|
+
expectedSourceSpace?: string | null;
|
|
1487
|
+
expectedSourceConnectionId?: string | null;
|
|
1488
|
+
expectedCollectionCount?: number;
|
|
1489
|
+
}): Promise<BuilderCmsReadResult> {
|
|
1490
|
+
const fetchedAt = new Date().toISOString();
|
|
1491
|
+
const requestedLimit = readLimit(args.limit);
|
|
1492
|
+
const startOffset =
|
|
1493
|
+
typeof args.offset === "number" && Number.isFinite(args.offset)
|
|
1494
|
+
? Math.max(0, Math.floor(args.offset))
|
|
1495
|
+
: 0;
|
|
1496
|
+
const startAfterId = args.afterId?.trim() || null;
|
|
1497
|
+
if (startOffset > 0 && !startAfterId) {
|
|
1498
|
+
fail(
|
|
1499
|
+
"Builder query-data continuation needs a stable entry cursor. Restart the source refresh to establish one.",
|
|
1500
|
+
{ errorCode: "builder_source_continuation_mismatch", statusCode: 409 },
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
const publicKey = requireBuilderOAuthPublicKey(
|
|
1504
|
+
args.authorization,
|
|
1505
|
+
args.expectedSourceSpace,
|
|
1506
|
+
args.expectedSourceConnectionId,
|
|
1507
|
+
);
|
|
1508
|
+
const modelId = await resolveBuilderCmsGeneralModelId(args);
|
|
1509
|
+
const entries: BuilderCmsSourceEntry[] = [];
|
|
1510
|
+
const seenIds = new Set<string>();
|
|
1511
|
+
let offset = startOffset;
|
|
1512
|
+
let cursor = startAfterId;
|
|
1513
|
+
let pagesRead = 0;
|
|
1514
|
+
let observedCollectionCount: number | null = null;
|
|
1515
|
+
let hasMore = false;
|
|
1516
|
+
|
|
1517
|
+
while (entries.length < requestedLimit) {
|
|
1518
|
+
if (args.maxPages && pagesRead >= args.maxPages) break;
|
|
1519
|
+
const pageLimit = readPageLimit(requestedLimit - entries.length);
|
|
1520
|
+
// Each page re-reads the previous page's last entry so a count-preserving
|
|
1521
|
+
// edit before the offset (delete + append) cannot silently skip a row.
|
|
1522
|
+
const anchored = offset > 0 && Boolean(cursor);
|
|
1523
|
+
const url = new URL("/api/v1/query-data", BUILDER_OAUTH_READ_HOST);
|
|
1524
|
+
url.searchParams.set("apiKey", publicKey);
|
|
1525
|
+
url.searchParams.set("query.modelId", modelId);
|
|
1526
|
+
url.searchParams.set("sort.id", "1");
|
|
1527
|
+
url.searchParams.set("offset", String(anchored ? offset - 1 : offset));
|
|
1528
|
+
url.searchParams.set("limit", String(anchored ? pageLimit + 1 : pageLimit));
|
|
1529
|
+
url.searchParams.set("fetchTotalCount", "true");
|
|
1530
|
+
url.searchParams.set("noCache", "true");
|
|
1531
|
+
url.searchParams.set("cachebust", String(Date.now() + pagesRead));
|
|
1532
|
+
const response = await fetchBuilderContentPage({
|
|
1533
|
+
fetchImpl: args.fetchImpl,
|
|
1534
|
+
url,
|
|
1535
|
+
privateKey: args.authorization.token,
|
|
1536
|
+
});
|
|
1537
|
+
if (!response.ok) {
|
|
1538
|
+
return {
|
|
1539
|
+
state: "error",
|
|
1540
|
+
entries: [],
|
|
1541
|
+
fetchedAt,
|
|
1542
|
+
message: `Builder CMS read failed with HTTP ${response.status}.`,
|
|
1543
|
+
progress: {
|
|
1544
|
+
requestedLimit,
|
|
1545
|
+
pageSize: BUILDER_CMS_PAGE_SIZE,
|
|
1546
|
+
startOffset,
|
|
1547
|
+
nextOffset: offset,
|
|
1548
|
+
nextCursor: cursor ?? undefined,
|
|
1549
|
+
fetchedEntryCount: startOffset + entries.length,
|
|
1550
|
+
hasMore: false,
|
|
1551
|
+
partial: entries.length > 0,
|
|
1552
|
+
readMode: "builder-api",
|
|
1553
|
+
},
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
const json = (await response.json()) as unknown;
|
|
1557
|
+
const { results: rawPageEntries, totalCount } =
|
|
1558
|
+
parseBuilderGeneralQueryDataResponse(json);
|
|
1559
|
+
const pageCollectionCount = totalCount;
|
|
1560
|
+
if (observedCollectionCount === null) {
|
|
1561
|
+
observedCollectionCount = pageCollectionCount;
|
|
1562
|
+
if (startOffset > 0 && args.expectedCollectionCount === undefined) {
|
|
1563
|
+
fail(
|
|
1564
|
+
"Builder source continuation has no saved collection count. Start a full refresh before continuing.",
|
|
1565
|
+
{
|
|
1566
|
+
errorCode: "builder_source_continuation_mismatch",
|
|
1567
|
+
statusCode: 409,
|
|
1568
|
+
},
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
if (
|
|
1572
|
+
args.expectedCollectionCount !== undefined &&
|
|
1573
|
+
pageCollectionCount !== args.expectedCollectionCount
|
|
1574
|
+
) {
|
|
1575
|
+
fail(
|
|
1576
|
+
"Builder query-data collection changed between refresh pages; restart the source refresh before mutating the snapshot.",
|
|
1577
|
+
{ errorCode: "builder_source_changed", statusCode: 409 },
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (
|
|
1582
|
+
observedCollectionCount !== null &&
|
|
1583
|
+
pageCollectionCount !== observedCollectionCount
|
|
1584
|
+
) {
|
|
1585
|
+
fail(
|
|
1586
|
+
"Builder query-data collection changed during cursor pagination; refresh the source to restart the observed collection read.",
|
|
1587
|
+
{ errorCode: "builder_source_changed", statusCode: 409 },
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
const normalizedEntries = rawPageEntries
|
|
1591
|
+
.map((entry) => normalizeBuilderCmsApiEntry(entry, args.model))
|
|
1592
|
+
.filter((entry): entry is BuilderCmsSourceEntry => Boolean(entry));
|
|
1593
|
+
if (normalizedEntries.length !== rawPageEntries.length) {
|
|
1594
|
+
fail(
|
|
1595
|
+
"Builder query-data returned a malformed entry before the observed collection was complete.",
|
|
1596
|
+
{ errorCode: "builder_response_invalid", statusCode: 424 },
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
if (anchored && normalizedEntries[0]?.id !== cursor) {
|
|
1600
|
+
fail(
|
|
1601
|
+
"Builder query-data collection changed before the saved cursor; restart the source refresh.",
|
|
1602
|
+
{ errorCode: "builder_source_changed", statusCode: 409 },
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
const pageEntries = anchored
|
|
1606
|
+
? normalizedEntries.slice(1)
|
|
1607
|
+
: normalizedEntries;
|
|
1608
|
+
const priorCursor = cursor;
|
|
1609
|
+
if (priorCursor && pageEntries.some((entry) => entry.id <= priorCursor)) {
|
|
1610
|
+
fail(
|
|
1611
|
+
"Builder query-data cursor pagination repeated or reordered an entry.",
|
|
1612
|
+
{ errorCode: "builder_response_invalid", statusCode: 424 },
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
const appended = appendUniqueBuilderEntries(entries, seenIds, pageEntries);
|
|
1616
|
+
if (appended !== pageEntries.length) {
|
|
1617
|
+
fail(
|
|
1618
|
+
"Builder query-data returned duplicate entries before the observed collection was complete.",
|
|
1619
|
+
{ errorCode: "builder_response_invalid", statusCode: 424 },
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
if (offset + pageEntries.length > totalCount) {
|
|
1623
|
+
fail(
|
|
1624
|
+
"Builder query-data returned more entries than its reported totalCount.",
|
|
1625
|
+
{ errorCode: "builder_response_invalid", statusCode: 424 },
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
pagesRead += 1;
|
|
1629
|
+
offset += pageEntries.length;
|
|
1630
|
+
cursor = pageEntries[pageEntries.length - 1]?.id ?? cursor;
|
|
1631
|
+
hasMore = offset < totalCount;
|
|
1632
|
+
if (hasMore && (pageEntries.length === 0 || appended === 0)) {
|
|
1633
|
+
fail(
|
|
1634
|
+
"Builder query-data pagination returned no new entries before the observed collection was complete.",
|
|
1635
|
+
{ errorCode: "builder_response_invalid", statusCode: 424 },
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
if (!hasMore) break;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
return {
|
|
1642
|
+
state: "live",
|
|
1643
|
+
entries,
|
|
1644
|
+
fetchedAt,
|
|
1645
|
+
message: null,
|
|
1646
|
+
progress: {
|
|
1647
|
+
requestedLimit,
|
|
1648
|
+
pageSize: BUILDER_CMS_PAGE_SIZE,
|
|
1649
|
+
startOffset,
|
|
1650
|
+
nextOffset: offset,
|
|
1651
|
+
nextCursor: cursor ?? undefined,
|
|
1652
|
+
fetchedEntryCount: offset,
|
|
1653
|
+
hasMore,
|
|
1654
|
+
partial: hasMore,
|
|
1655
|
+
readMode: "builder-api",
|
|
1656
|
+
sourceSpacePublicKey: publicKey,
|
|
1657
|
+
sourceConnectionId: args.authorization.oauthConnectionId,
|
|
1658
|
+
observedCollectionCount: observedCollectionCount ?? undefined,
|
|
1659
|
+
},
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
async function readBuilderCmsGeneralApiEntry(args: {
|
|
1664
|
+
model: string;
|
|
1665
|
+
entryId: string;
|
|
1666
|
+
authorization: BuilderRequestAuthorization;
|
|
1667
|
+
expectedSourceSpace?: string | null;
|
|
1668
|
+
expectedSourceConnectionId?: string | null;
|
|
1669
|
+
fetchImpl: FetchLike;
|
|
1670
|
+
signal?: AbortSignal;
|
|
1671
|
+
}) {
|
|
1672
|
+
const publicKey = requireBuilderOAuthPublicKey(
|
|
1673
|
+
args.authorization,
|
|
1674
|
+
args.expectedSourceSpace,
|
|
1675
|
+
args.expectedSourceConnectionId,
|
|
1676
|
+
);
|
|
1677
|
+
const modelId = await resolveBuilderCmsGeneralModelId(args);
|
|
1678
|
+
const url = new URL("/api/v1/query-data", BUILDER_OAUTH_READ_HOST);
|
|
1679
|
+
url.searchParams.set("apiKey", publicKey);
|
|
1680
|
+
url.searchParams.set("query.modelId", modelId);
|
|
1681
|
+
url.searchParams.set("query.id", args.entryId);
|
|
1682
|
+
url.searchParams.set("sort.id", "1");
|
|
1683
|
+
url.searchParams.set("offset", "0");
|
|
1684
|
+
url.searchParams.set("limit", "1");
|
|
1685
|
+
url.searchParams.set("fetchTotalCount", "true");
|
|
1686
|
+
// This read decides whether a write may overwrite the entry, and the host is
|
|
1687
|
+
// a CDN that may serve a cached copy (including stale-if-error), so each
|
|
1688
|
+
// request gets its own cache key.
|
|
1689
|
+
url.searchParams.set("noCache", "true");
|
|
1690
|
+
url.searchParams.set("cachebust", String(Date.now()));
|
|
1691
|
+
return fetchBuilderContentPage({
|
|
1692
|
+
fetchImpl: args.fetchImpl,
|
|
1693
|
+
url,
|
|
1694
|
+
privateKey: args.authorization.token,
|
|
1695
|
+
signal: args.signal,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function builderGeneralEntryHttpFailure(message: string, status: number) {
|
|
1700
|
+
const reason =
|
|
1701
|
+
status === 401
|
|
1702
|
+
? "auth_failed"
|
|
1703
|
+
: status === 403
|
|
1704
|
+
? "access_denied"
|
|
1705
|
+
: status === 429 || status >= 500
|
|
1706
|
+
? "transient_read_failure"
|
|
1707
|
+
: "malformed_body";
|
|
1708
|
+
return new BuilderCmsContentEntryReadError(
|
|
1709
|
+
message,
|
|
1710
|
+
reason,
|
|
1711
|
+
`http_${status}`,
|
|
1712
|
+
reason === "transient_read_failure",
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function builderGeneralEntryRequestFailure(error: unknown) {
|
|
1717
|
+
if (!isActionContractError(error)) {
|
|
1718
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1719
|
+
return new BuilderCmsContentEntryReadError(
|
|
1720
|
+
`Builder CMS entry read failed before a response was received: ${message}`,
|
|
1721
|
+
"transient_read_failure",
|
|
1722
|
+
"network_error",
|
|
1723
|
+
true,
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
const httpStatus = error.details?.httpStatus;
|
|
1727
|
+
if (typeof httpStatus === "number") {
|
|
1728
|
+
return builderGeneralEntryHttpFailure(error.message, httpStatus);
|
|
1729
|
+
}
|
|
1730
|
+
const reason =
|
|
1731
|
+
error.statusCode === 404
|
|
1732
|
+
? "not_found"
|
|
1733
|
+
: error.statusCode === 424
|
|
1734
|
+
? "malformed_body"
|
|
1735
|
+
: "access_denied";
|
|
1736
|
+
return new BuilderCmsContentEntryReadError(
|
|
1737
|
+
error.message,
|
|
1738
|
+
reason,
|
|
1739
|
+
error.errorCode,
|
|
1740
|
+
RETRYABLE_BUILDER_SOURCE_BINDING_ERRORS.has(error.errorCode),
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1153
1744
|
export async function readBuilderCmsEntryLiveState(args: {
|
|
1154
1745
|
model: string;
|
|
1155
1746
|
entryId: string;
|
|
1747
|
+
expectedSourceSpace?: string | null;
|
|
1748
|
+
expectedSourceConnectionId?: string | null;
|
|
1749
|
+
requireBoundOAuth?: boolean;
|
|
1156
1750
|
fetchImpl?: FetchLike;
|
|
1751
|
+
signal?: AbortSignal;
|
|
1157
1752
|
}): Promise<BuilderCmsEntryLiveState> {
|
|
1753
|
+
const { authorization: generalAuthorization } =
|
|
1754
|
+
await readGeneralBuilderContentAuthorization(
|
|
1755
|
+
isBuilderReadSourceBound(
|
|
1756
|
+
args.expectedSourceSpace,
|
|
1757
|
+
args.expectedSourceConnectionId,
|
|
1758
|
+
),
|
|
1759
|
+
);
|
|
1760
|
+
assertBuilderReadSourceAuthorization(
|
|
1761
|
+
generalAuthorization,
|
|
1762
|
+
args.expectedSourceSpace,
|
|
1763
|
+
args.expectedSourceConnectionId,
|
|
1764
|
+
);
|
|
1765
|
+
if (
|
|
1766
|
+
args.requireBoundOAuth === true &&
|
|
1767
|
+
isGeneralBuilderOAuth(generalAuthorization) &&
|
|
1768
|
+
!isBuilderReadSourceBound(
|
|
1769
|
+
args.expectedSourceSpace,
|
|
1770
|
+
args.expectedSourceConnectionId,
|
|
1771
|
+
)
|
|
1772
|
+
) {
|
|
1773
|
+
fail(
|
|
1774
|
+
"Builder OAuth reads require a saved space and connection binding. Reattach the source with mode replace to bind the current Builder connection before reading.",
|
|
1775
|
+
{ errorCode: "builder_source_binding_required", statusCode: 409 },
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
if (isGeneralBuilderOAuth(generalAuthorization)) {
|
|
1779
|
+
const response = await readBuilderCmsGeneralApiEntry({
|
|
1780
|
+
model: args.model,
|
|
1781
|
+
entryId: args.entryId,
|
|
1782
|
+
authorization: generalAuthorization,
|
|
1783
|
+
expectedSourceSpace: args.expectedSourceSpace,
|
|
1784
|
+
expectedSourceConnectionId: args.expectedSourceConnectionId,
|
|
1785
|
+
fetchImpl: args.fetchImpl ?? fetch,
|
|
1786
|
+
signal: args.signal,
|
|
1787
|
+
});
|
|
1788
|
+
if (!response.ok) {
|
|
1789
|
+
fail(`Builder CMS live entry read failed with HTTP ${response.status}.`, {
|
|
1790
|
+
errorCode: "builder_upstream_failed",
|
|
1791
|
+
statusCode: 424,
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
const json = (await response.json()) as unknown;
|
|
1795
|
+
const { results } = parseBuilderGeneralQueryDataResponse(json);
|
|
1796
|
+
if (results.length === 0) return liveStateFromBuilderEntry([]);
|
|
1797
|
+
const entry = normalizeBuilderCmsApiEntry(results[0], args.model);
|
|
1798
|
+
if (!entry || entry.id !== args.entryId) {
|
|
1799
|
+
fail(
|
|
1800
|
+
"Builder CMS live entry read returned an unexpected entry payload.",
|
|
1801
|
+
{ errorCode: "builder_response_invalid", statusCode: 424 },
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
return liveStateFromBuilderEntry(entry.rawEntry);
|
|
1805
|
+
}
|
|
1158
1806
|
const publicKey = await resolveBuilderCredential("BUILDER_PUBLIC_KEY");
|
|
1159
1807
|
if (!publicKey) {
|
|
1160
1808
|
throw new Error(
|
|
@@ -1173,6 +1821,7 @@ export async function readBuilderCmsEntryLiveState(args: {
|
|
|
1173
1821
|
const response = await fetchBuilderContentPage({
|
|
1174
1822
|
fetchImpl: args.fetchImpl ?? fetch,
|
|
1175
1823
|
url,
|
|
1824
|
+
signal: args.signal,
|
|
1176
1825
|
});
|
|
1177
1826
|
if (response.status === 404) {
|
|
1178
1827
|
return {
|
|
@@ -1196,6 +1845,9 @@ export async function readBuilderCmsEntryLiveState(args: {
|
|
|
1196
1845
|
export async function readBuilderCmsContentEntry(args: {
|
|
1197
1846
|
model: string;
|
|
1198
1847
|
entryId: string;
|
|
1848
|
+
expectedSourceSpace?: string | null;
|
|
1849
|
+
expectedSourceConnectionId?: string | null;
|
|
1850
|
+
requireBoundOAuth?: boolean;
|
|
1199
1851
|
fetchImpl?: FetchLike;
|
|
1200
1852
|
}): Promise<BuilderCmsSourceEntry | null> {
|
|
1201
1853
|
const result = await readBuilderCmsContentEntryResult({
|
|
@@ -1208,10 +1860,36 @@ export async function readBuilderCmsContentEntry(args: {
|
|
|
1208
1860
|
export async function readBuilderCmsContentEntryResult(args: {
|
|
1209
1861
|
model: string;
|
|
1210
1862
|
entryId: string;
|
|
1863
|
+
expectedSourceSpace?: string | null;
|
|
1864
|
+
expectedSourceConnectionId?: string | null;
|
|
1865
|
+
requireBoundOAuth?: boolean;
|
|
1211
1866
|
fetchImpl?: FetchLike;
|
|
1212
1867
|
strictEntryIdentity?: boolean;
|
|
1213
1868
|
}): Promise<BuilderCmsContentEntryReadResult> {
|
|
1214
|
-
const authorization = await readBuilderCmsAuthorization(
|
|
1869
|
+
const authorization = await readBuilderCmsAuthorization(
|
|
1870
|
+
isBuilderReadSourceBound(
|
|
1871
|
+
args.expectedSourceSpace,
|
|
1872
|
+
args.expectedSourceConnectionId,
|
|
1873
|
+
),
|
|
1874
|
+
);
|
|
1875
|
+
assertBuilderReadSourceAuthorization(
|
|
1876
|
+
authorization,
|
|
1877
|
+
args.expectedSourceSpace,
|
|
1878
|
+
args.expectedSourceConnectionId,
|
|
1879
|
+
);
|
|
1880
|
+
if (
|
|
1881
|
+
args.requireBoundOAuth === true &&
|
|
1882
|
+
isGeneralBuilderOAuth(authorization) &&
|
|
1883
|
+
!isBuilderReadSourceBound(
|
|
1884
|
+
args.expectedSourceSpace,
|
|
1885
|
+
args.expectedSourceConnectionId,
|
|
1886
|
+
)
|
|
1887
|
+
) {
|
|
1888
|
+
fail(
|
|
1889
|
+
"Builder OAuth reads require a saved space and connection binding. Reattach the source with mode replace to bind the current Builder connection before reading.",
|
|
1890
|
+
{ errorCode: "builder_source_binding_required", statusCode: 409 },
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1215
1893
|
const privateKey = authorization?.token ?? null;
|
|
1216
1894
|
const publicKey = await resolveBuilderCredential("BUILDER_PUBLIC_KEY");
|
|
1217
1895
|
if (!publicKey && !privateKey) {
|
|
@@ -1223,6 +1901,85 @@ export async function readBuilderCmsContentEntryResult(args: {
|
|
|
1223
1901
|
);
|
|
1224
1902
|
}
|
|
1225
1903
|
|
|
1904
|
+
if (isGeneralBuilderOAuth(authorization)) {
|
|
1905
|
+
let response: Response;
|
|
1906
|
+
try {
|
|
1907
|
+
response = await readBuilderCmsGeneralApiEntry({
|
|
1908
|
+
model: args.model,
|
|
1909
|
+
entryId: args.entryId,
|
|
1910
|
+
authorization,
|
|
1911
|
+
expectedSourceSpace: args.expectedSourceSpace,
|
|
1912
|
+
expectedSourceConnectionId: args.expectedSourceConnectionId,
|
|
1913
|
+
fetchImpl: args.fetchImpl ?? fetch,
|
|
1914
|
+
});
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
throw builderGeneralEntryRequestFailure(error);
|
|
1917
|
+
}
|
|
1918
|
+
if (!response.ok) {
|
|
1919
|
+
throw builderGeneralEntryHttpFailure(
|
|
1920
|
+
`Builder CMS entry read failed with HTTP ${response.status}.`,
|
|
1921
|
+
response.status,
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
let json: unknown;
|
|
1925
|
+
try {
|
|
1926
|
+
json = (await response.json()) as unknown;
|
|
1927
|
+
} catch {
|
|
1928
|
+
throw new BuilderCmsContentEntryReadError(
|
|
1929
|
+
"Builder CMS entry read returned malformed JSON.",
|
|
1930
|
+
"malformed_body",
|
|
1931
|
+
"http_200_invalid_json",
|
|
1932
|
+
false,
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
let entries: unknown[];
|
|
1936
|
+
try {
|
|
1937
|
+
entries = parseBuilderGeneralQueryDataResponse(json).results;
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
throw new BuilderCmsContentEntryReadError(
|
|
1940
|
+
error instanceof Error
|
|
1941
|
+
? error.message
|
|
1942
|
+
: "Builder query-data returned a malformed response.",
|
|
1943
|
+
"malformed_body",
|
|
1944
|
+
"http_200_malformed_body",
|
|
1945
|
+
false,
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
if (entries.length === 0) {
|
|
1949
|
+
return {
|
|
1950
|
+
state: "not_found",
|
|
1951
|
+
entry: null,
|
|
1952
|
+
providerStatus: "http_200_not_found",
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
const rawEntry = entries[0];
|
|
1956
|
+
const entry = normalizeBuilderCmsApiEntry(rawEntry, args.model);
|
|
1957
|
+
if (!entry) {
|
|
1958
|
+
throw new BuilderCmsContentEntryReadError(
|
|
1959
|
+
"Builder CMS entry read returned a malformed entry payload.",
|
|
1960
|
+
"malformed_body",
|
|
1961
|
+
"http_200_invalid_entry",
|
|
1962
|
+
false,
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (entry.id !== args.entryId) {
|
|
1966
|
+
if (args.strictEntryIdentity === false) {
|
|
1967
|
+
return {
|
|
1968
|
+
state: "not_found",
|
|
1969
|
+
entry: null,
|
|
1970
|
+
providerStatus: "http_200_unexpected_entry",
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
throw new BuilderCmsContentEntryReadError(
|
|
1974
|
+
"Builder CMS entry read returned an unexpected entry payload.",
|
|
1975
|
+
"malformed_body",
|
|
1976
|
+
"http_200_unexpected_entry",
|
|
1977
|
+
false,
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
return { state: "found", entry, providerStatus: "http_200" };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1226
1983
|
if (privateKey && (authorization?.source === "oauth" || !publicKey)) {
|
|
1227
1984
|
const endpoint = builderMcpEndpoint(authorization?.source);
|
|
1228
1985
|
try {
|
|
@@ -1396,13 +2153,20 @@ export async function readBuilderCmsContentEntryResult(args: {
|
|
|
1396
2153
|
export async function listBuilderCmsModels(
|
|
1397
2154
|
args: {
|
|
1398
2155
|
fetchImpl?: FetchLike;
|
|
2156
|
+
expectedSourceSpace?: string | null;
|
|
2157
|
+
expectedSourceConnectionId?: string | null;
|
|
1399
2158
|
} = {},
|
|
1400
2159
|
): Promise<BuilderCmsModelsResponse> {
|
|
1401
2160
|
const fetchedAt = new Date().toISOString();
|
|
1402
2161
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
1403
2162
|
let authorization: BuilderRequestAuthorization | null;
|
|
1404
2163
|
try {
|
|
1405
|
-
authorization = await readBuilderCmsAuthorization(
|
|
2164
|
+
authorization = await readBuilderCmsAuthorization(
|
|
2165
|
+
isBuilderReadSourceBound(
|
|
2166
|
+
args.expectedSourceSpace,
|
|
2167
|
+
args.expectedSourceConnectionId,
|
|
2168
|
+
),
|
|
2169
|
+
);
|
|
1406
2170
|
} catch (error) {
|
|
1407
2171
|
return {
|
|
1408
2172
|
state: "error",
|
|
@@ -1415,6 +2179,11 @@ export async function listBuilderCmsModels(
|
|
|
1415
2179
|
};
|
|
1416
2180
|
}
|
|
1417
2181
|
const privateKey = authorization?.token ?? null;
|
|
2182
|
+
assertBuilderReadSourceAuthorization(
|
|
2183
|
+
authorization,
|
|
2184
|
+
args.expectedSourceSpace,
|
|
2185
|
+
args.expectedSourceConnectionId,
|
|
2186
|
+
);
|
|
1418
2187
|
if (!privateKey) {
|
|
1419
2188
|
return {
|
|
1420
2189
|
state: "unconfigured",
|
|
@@ -1426,6 +2195,20 @@ export async function listBuilderCmsModels(
|
|
|
1426
2195
|
}
|
|
1427
2196
|
|
|
1428
2197
|
try {
|
|
2198
|
+
if (isGeneralBuilderOAuth(authorization)) {
|
|
2199
|
+
const models = await readBuilderCmsGeneralModels({
|
|
2200
|
+
authorization,
|
|
2201
|
+
fetchImpl,
|
|
2202
|
+
expectedSourceSpace: args.expectedSourceSpace,
|
|
2203
|
+
expectedSourceConnectionId: args.expectedSourceConnectionId,
|
|
2204
|
+
});
|
|
2205
|
+
return {
|
|
2206
|
+
state: "live",
|
|
2207
|
+
models,
|
|
2208
|
+
fetchedAt,
|
|
2209
|
+
message: null,
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
1429
2212
|
const endpoint = builderMcpEndpoint(authorization?.source);
|
|
1430
2213
|
const connection = await initializeBuilderMcp({
|
|
1431
2214
|
endpoint,
|
|
@@ -1475,8 +2258,14 @@ export async function listBuilderCmsModels(
|
|
|
1475
2258
|
export async function readBuilderCmsModelFields(args: {
|
|
1476
2259
|
model: string;
|
|
1477
2260
|
fetchImpl?: FetchLike;
|
|
2261
|
+
expectedSourceSpace?: string | null;
|
|
2262
|
+
expectedSourceConnectionId?: string | null;
|
|
1478
2263
|
}): Promise<BuilderCmsModelFieldSummary[]> {
|
|
1479
|
-
const models = await listBuilderCmsModels({
|
|
2264
|
+
const models = await listBuilderCmsModels({
|
|
2265
|
+
fetchImpl: args.fetchImpl,
|
|
2266
|
+
expectedSourceSpace: args.expectedSourceSpace,
|
|
2267
|
+
expectedSourceConnectionId: args.expectedSourceConnectionId,
|
|
2268
|
+
});
|
|
1480
2269
|
if (models.state === "unconfigured") return [];
|
|
1481
2270
|
if (models.state === "error") {
|
|
1482
2271
|
throw new Error(models.message ?? "Builder CMS model discovery failed.");
|
|
@@ -1495,6 +2284,8 @@ export async function readBuilderCmsModelFields(args: {
|
|
|
1495
2284
|
|
|
1496
2285
|
export async function readBuilderCmsContentEntries(args: {
|
|
1497
2286
|
model: string;
|
|
2287
|
+
expectedSourceSpace?: string | null;
|
|
2288
|
+
expectedSourceConnectionId?: string | null;
|
|
1498
2289
|
fieldPaths?: readonly string[];
|
|
1499
2290
|
includeBodies?: boolean;
|
|
1500
2291
|
allowCached?: boolean;
|
|
@@ -1503,13 +2294,22 @@ export async function readBuilderCmsContentEntries(args: {
|
|
|
1503
2294
|
limit?: number;
|
|
1504
2295
|
maxPages?: number;
|
|
1505
2296
|
offset?: number;
|
|
2297
|
+
afterId?: string;
|
|
2298
|
+
startingFetchedEntryCount?: number;
|
|
2299
|
+
expectedCollectionCount?: number;
|
|
2300
|
+
requireBoundOAuth?: boolean;
|
|
1506
2301
|
fetchImpl?: FetchLike;
|
|
1507
2302
|
}): Promise<BuilderCmsReadResult> {
|
|
1508
2303
|
const fetchedAt = new Date().toISOString();
|
|
1509
2304
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
1510
2305
|
let authorization: BuilderRequestAuthorization | null;
|
|
1511
2306
|
try {
|
|
1512
|
-
authorization = await readBuilderCmsAuthorization(
|
|
2307
|
+
authorization = await readBuilderCmsAuthorization(
|
|
2308
|
+
isBuilderReadSourceBound(
|
|
2309
|
+
args.expectedSourceSpace,
|
|
2310
|
+
args.expectedSourceConnectionId,
|
|
2311
|
+
),
|
|
2312
|
+
);
|
|
1513
2313
|
} catch (error) {
|
|
1514
2314
|
return {
|
|
1515
2315
|
state: "error",
|
|
@@ -1531,6 +2331,11 @@ export async function readBuilderCmsContentEntries(args: {
|
|
|
1531
2331
|
},
|
|
1532
2332
|
};
|
|
1533
2333
|
}
|
|
2334
|
+
assertBuilderReadSourceAuthorization(
|
|
2335
|
+
authorization,
|
|
2336
|
+
args.expectedSourceSpace,
|
|
2337
|
+
args.expectedSourceConnectionId,
|
|
2338
|
+
);
|
|
1534
2339
|
const privateKey = authorization?.token ?? null;
|
|
1535
2340
|
const publicKey = await resolveBuilderCredential("BUILDER_PUBLIC_KEY");
|
|
1536
2341
|
if (args.requirePrivateKey === true && !privateKey) {
|
|
@@ -1552,6 +2357,59 @@ export async function readBuilderCmsContentEntries(args: {
|
|
|
1552
2357
|
},
|
|
1553
2358
|
};
|
|
1554
2359
|
}
|
|
2360
|
+
if (isGeneralBuilderOAuth(authorization)) {
|
|
2361
|
+
try {
|
|
2362
|
+
if (
|
|
2363
|
+
args.requireBoundOAuth === true &&
|
|
2364
|
+
!isBuilderReadSourceBound(
|
|
2365
|
+
args.expectedSourceSpace,
|
|
2366
|
+
args.expectedSourceConnectionId,
|
|
2367
|
+
)
|
|
2368
|
+
) {
|
|
2369
|
+
fail(
|
|
2370
|
+
"Builder OAuth refresh requires a saved space and connection binding. Reattach this source with mode replace to bind the current Builder connection before refreshing.",
|
|
2371
|
+
{ errorCode: "builder_source_binding_required", statusCode: 409 },
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
return preserveProjectedBuilderFieldAbsence(
|
|
2375
|
+
await readBuilderCmsContentEntriesViaGeneralApi({
|
|
2376
|
+
model: args.model,
|
|
2377
|
+
limit: args.limit,
|
|
2378
|
+
maxPages: args.maxPages,
|
|
2379
|
+
offset: args.offset,
|
|
2380
|
+
afterId: args.afterId,
|
|
2381
|
+
expectedCollectionCount: args.expectedCollectionCount,
|
|
2382
|
+
fetchImpl,
|
|
2383
|
+
authorization,
|
|
2384
|
+
expectedSourceSpace: args.expectedSourceSpace,
|
|
2385
|
+
expectedSourceConnectionId: args.expectedSourceConnectionId,
|
|
2386
|
+
}),
|
|
2387
|
+
args.fieldPaths,
|
|
2388
|
+
args.rawData,
|
|
2389
|
+
);
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
return {
|
|
2392
|
+
state: "error",
|
|
2393
|
+
entries: [],
|
|
2394
|
+
fetchedAt,
|
|
2395
|
+
message:
|
|
2396
|
+
error instanceof Error
|
|
2397
|
+
? error.message
|
|
2398
|
+
: "Builder CMS authenticated read failed.",
|
|
2399
|
+
progress: {
|
|
2400
|
+
requestedLimit: readLimit(args.limit),
|
|
2401
|
+
pageSize: BUILDER_CMS_PAGE_SIZE,
|
|
2402
|
+
startOffset: args.offset ?? 0,
|
|
2403
|
+
nextOffset: args.offset ?? 0,
|
|
2404
|
+
nextCursor: args.afterId,
|
|
2405
|
+
fetchedEntryCount: args.offset ?? 0,
|
|
2406
|
+
hasMore: false,
|
|
2407
|
+
partial: false,
|
|
2408
|
+
readMode: "builder-api",
|
|
2409
|
+
},
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
1555
2413
|
if (publicKey && authorization?.source !== "oauth") {
|
|
1556
2414
|
const contentApiRead = await readBuilderCmsContentEntriesViaContentApi({
|
|
1557
2415
|
model: args.model,
|
|
@@ -1587,6 +2445,8 @@ export async function readBuilderCmsContentEntries(args: {
|
|
|
1587
2445
|
limit: args.limit,
|
|
1588
2446
|
maxPages: args.maxPages,
|
|
1589
2447
|
offset: args.offset,
|
|
2448
|
+
startingFetchedEntryCount: args.startingFetchedEntryCount,
|
|
2449
|
+
expectedCollectionCount: args.expectedCollectionCount,
|
|
1590
2450
|
fetchImpl,
|
|
1591
2451
|
privateKey,
|
|
1592
2452
|
endpoint: builderMcpEndpoint(authorization?.source),
|