@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
|
@@ -36,6 +36,8 @@ export interface BuilderCmsExecutionPayload {
|
|
|
36
36
|
sourceQualifiedId: string | null;
|
|
37
37
|
documentId: string | null;
|
|
38
38
|
databaseItemId: string | null;
|
|
39
|
+
/** The entry's lastUpdated when the change was reviewed; execute rejects a live value that differs. */
|
|
40
|
+
reviewedLastUpdated: string | null;
|
|
39
41
|
};
|
|
40
42
|
request: {
|
|
41
43
|
method: "POST" | "PATCH";
|
|
@@ -872,6 +874,14 @@ export function buildBuilderCmsExecutionPlan(args: {
|
|
|
872
874
|
targetRow,
|
|
873
875
|
request,
|
|
874
876
|
});
|
|
877
|
+
const reviewedLastUpdated = targetEntryId
|
|
878
|
+
? (targetRow?.lastSourceUpdatedAt ?? null)
|
|
879
|
+
: null;
|
|
880
|
+
if (targetEntryId && !reviewedLastUpdated?.trim()) {
|
|
881
|
+
fieldBlockers.push(
|
|
882
|
+
"Refresh this Builder entry before writing so its last update time can be checked.",
|
|
883
|
+
);
|
|
884
|
+
}
|
|
875
885
|
const safety = builderSafetyChecks({
|
|
876
886
|
source: args.source,
|
|
877
887
|
changeSet: args.changeSet,
|
|
@@ -934,6 +944,7 @@ export function buildBuilderCmsExecutionPlan(args: {
|
|
|
934
944
|
sourceQualifiedId: targetSourceQualifiedId,
|
|
935
945
|
documentId: args.changeSet.documentId,
|
|
936
946
|
databaseItemId: args.changeSet.databaseItemId,
|
|
947
|
+
reviewedLastUpdated,
|
|
937
948
|
},
|
|
938
949
|
pushMode: effectivePushMode,
|
|
939
950
|
request,
|
|
@@ -1003,11 +1014,26 @@ export function validateBuilderCmsExecutionDryRun(args: {
|
|
|
1003
1014
|
"Stored Builder effect no longer matches the approved write mode.",
|
|
1004
1015
|
);
|
|
1005
1016
|
}
|
|
1017
|
+
const { reviewedLastUpdated: storedReviewedLastUpdated, ...storedTarget } =
|
|
1018
|
+
(storedComparable.target ?? {}) as Partial<
|
|
1019
|
+
BuilderCmsExecutionPayload["target"]
|
|
1020
|
+
>;
|
|
1021
|
+
const { reviewedLastUpdated: planReviewedLastUpdated, ...planTarget } =
|
|
1022
|
+
(planComparable.target ?? {}) as Partial<
|
|
1023
|
+
BuilderCmsExecutionPayload["target"]
|
|
1024
|
+
>;
|
|
1025
|
+
if (stableJson(storedTarget) !== stableJson(planTarget)) {
|
|
1026
|
+
mismatches.push(
|
|
1027
|
+
"Stored Builder target no longer matches the current row identity.",
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
// A refresh that saw a newer Builder lastUpdated moves the row baseline, so
|
|
1031
|
+
// the value recorded at review is the one the live check must compare.
|
|
1006
1032
|
if (
|
|
1007
|
-
|
|
1033
|
+
(storedReviewedLastUpdated ?? null) !== (planReviewedLastUpdated ?? null)
|
|
1008
1034
|
) {
|
|
1009
1035
|
mismatches.push(
|
|
1010
|
-
"
|
|
1036
|
+
"Builder entry changed since this change was reviewed. Refresh and review it again.",
|
|
1011
1037
|
);
|
|
1012
1038
|
}
|
|
1013
1039
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fail, isActionContractError } from "@agent-native/core/action";
|
|
2
|
+
import {
|
|
3
|
+
BUILDER_CONTENT_WRITE_SCOPE,
|
|
4
|
+
BUILDER_OAUTH_RESOURCE,
|
|
5
|
+
resolveBuilderLegacyRequestAuthorization,
|
|
6
|
+
resolveBuilderRequestAuthorization,
|
|
7
|
+
type BuilderRequestAuthorization,
|
|
8
|
+
} from "@agent-native/core/server";
|
|
2
9
|
|
|
3
10
|
export interface BuilderCmsWriteRequest {
|
|
4
11
|
method: "POST" | "PATCH";
|
|
@@ -20,7 +27,10 @@ export const DEFAULT_BUILDER_CMS_WRITE_TIMEOUT_MS = 30_000;
|
|
|
20
27
|
|
|
21
28
|
type FetchLike = typeof fetch;
|
|
22
29
|
|
|
23
|
-
function builderWriteApiHost() {
|
|
30
|
+
function builderWriteApiHost(source: BuilderRequestAuthorization["source"]) {
|
|
31
|
+
// An OAuth bearer goes only to the Builder API resource it was issued for;
|
|
32
|
+
// the host overrides apply to private keys alone.
|
|
33
|
+
if (source === "oauth") return BUILDER_OAUTH_RESOURCE;
|
|
24
34
|
return (
|
|
25
35
|
process.env.BUILDER_CONTENT_API_HOST ??
|
|
26
36
|
process.env.BUILDER_CMS_API_HOST ??
|
|
@@ -28,11 +38,80 @@ function builderWriteApiHost() {
|
|
|
28
38
|
).replace(/\/+$/, "");
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
const BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS = [
|
|
42
|
+
"BUILDER_PRIVATE_KEY",
|
|
43
|
+
"BUILDER_CMS_PRIVATE_KEY",
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
async function readBuilderWriteAuthorization(sourceBound: boolean) {
|
|
47
|
+
try {
|
|
48
|
+
return await resolveBuilderRequestAuthorization({
|
|
49
|
+
oauthResource: "general",
|
|
50
|
+
requiredScope: BUILDER_CONTENT_WRITE_SCOPE,
|
|
51
|
+
legacyCredentialKeys: BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS,
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// The shared resolver throws instead of falling back when a grant predates
|
|
55
|
+
// builder:content:write. A caller with no Source binding wrote with the
|
|
56
|
+
// deploy keys before OAuth, so it keeps that path until re-authorizing.
|
|
57
|
+
if (
|
|
58
|
+
sourceBound ||
|
|
59
|
+
!isActionContractError(error) ||
|
|
60
|
+
error.errorCode !== "builder_oauth_reauthorization_required" ||
|
|
61
|
+
error.details?.reason !== "missing_scope" ||
|
|
62
|
+
error.details.scope !== BUILDER_CONTENT_WRITE_SCOPE
|
|
63
|
+
) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const legacy = await resolveBuilderLegacyRequestAuthorization(
|
|
67
|
+
BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS,
|
|
68
|
+
);
|
|
69
|
+
if (!legacy) throw error;
|
|
70
|
+
return legacy;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function assertBuilderWriteSourceBinding(
|
|
75
|
+
authorization: Awaited<ReturnType<typeof readBuilderWriteAuthorization>>,
|
|
76
|
+
expectedSourceSpace: string | null | undefined,
|
|
77
|
+
expectedSourceConnectionId: string | null | undefined,
|
|
78
|
+
sourceWrite: boolean,
|
|
79
|
+
) {
|
|
80
|
+
const sourceBound = Boolean(
|
|
81
|
+
expectedSourceSpace || expectedSourceConnectionId,
|
|
35
82
|
);
|
|
83
|
+
// An unbound Source keeps writing with its deploy keys, but OAuth may write
|
|
84
|
+
// only into the space a Source is bound to.
|
|
85
|
+
if (
|
|
86
|
+
sourceBound &&
|
|
87
|
+
(authorization?.source !== "oauth" ||
|
|
88
|
+
authorization.oauthResource !== "general")
|
|
89
|
+
) {
|
|
90
|
+
fail(
|
|
91
|
+
"This Builder source's OAuth connection is unavailable. Reconnect the source before writing.",
|
|
92
|
+
{ errorCode: "builder_connection_required", statusCode: 412 },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (authorization?.source !== "oauth") return;
|
|
96
|
+
if (!sourceWrite && !sourceBound) return;
|
|
97
|
+
if (!expectedSourceSpace || !expectedSourceConnectionId) {
|
|
98
|
+
fail(
|
|
99
|
+
"This Builder source is not bound to its connected space. Refresh the source before writing.",
|
|
100
|
+
{ errorCode: "builder_source_unbound", statusCode: 409 },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (authorization.oauthSelectedPublicKey !== expectedSourceSpace) {
|
|
104
|
+
fail(
|
|
105
|
+
"The connected Builder space does not match this Content source. Reconnect the source's Builder space before writing.",
|
|
106
|
+
{ errorCode: "builder_source_space_mismatch", statusCode: 409 },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (authorization.oauthConnectionId !== expectedSourceConnectionId) {
|
|
110
|
+
fail(
|
|
111
|
+
"The connected Builder credential does not match this Content source. Reconnect the source before writing.",
|
|
112
|
+
{ errorCode: "builder_source_connection_mismatch", statusCode: 409 },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
36
115
|
}
|
|
37
116
|
|
|
38
117
|
function parseResponseBody(text: string): unknown {
|
|
@@ -136,22 +215,41 @@ function buildWriteResult(args: {
|
|
|
136
215
|
|
|
137
216
|
export async function executeBuilderCmsWrite(args: {
|
|
138
217
|
request: BuilderCmsWriteRequest;
|
|
218
|
+
expectedSourceSpace?: string | null;
|
|
219
|
+
expectedSourceConnectionId?: string | null;
|
|
220
|
+
requireSourceBinding?: boolean;
|
|
221
|
+
legacyOnly?: boolean;
|
|
139
222
|
fetchImpl?: FetchLike;
|
|
140
223
|
/** @deprecated Never used: retrying another transport after dispatch is unsafe. */
|
|
141
224
|
nodeRequestImpl?: unknown;
|
|
142
225
|
timeoutMs?: number;
|
|
143
226
|
}): Promise<BuilderCmsWriteResult> {
|
|
144
|
-
const
|
|
145
|
-
|
|
227
|
+
const authorization = args.legacyOnly
|
|
228
|
+
? await resolveBuilderLegacyRequestAuthorization(
|
|
229
|
+
BUILDER_WRITE_LEGACY_CREDENTIAL_KEYS,
|
|
230
|
+
)
|
|
231
|
+
: await readBuilderWriteAuthorization(
|
|
232
|
+
Boolean(args.expectedSourceSpace || args.expectedSourceConnectionId),
|
|
233
|
+
);
|
|
234
|
+
if (!authorization) {
|
|
146
235
|
return {
|
|
147
236
|
ok: false,
|
|
148
237
|
status: 0,
|
|
149
238
|
responseBody: null,
|
|
150
|
-
error: "Builder
|
|
239
|
+
error: "Builder write access is not connected.",
|
|
151
240
|
};
|
|
152
241
|
}
|
|
242
|
+
assertBuilderWriteSourceBinding(
|
|
243
|
+
authorization,
|
|
244
|
+
args.expectedSourceSpace,
|
|
245
|
+
args.expectedSourceConnectionId,
|
|
246
|
+
args.requireSourceBinding === true,
|
|
247
|
+
);
|
|
153
248
|
|
|
154
|
-
const url = new URL(
|
|
249
|
+
const url = new URL(
|
|
250
|
+
args.request.path,
|
|
251
|
+
builderWriteApiHost(authorization.source),
|
|
252
|
+
);
|
|
155
253
|
for (const [key, value] of Object.entries(args.request.query ?? {})) {
|
|
156
254
|
url.searchParams.set(key, value);
|
|
157
255
|
}
|
|
@@ -159,7 +257,7 @@ export async function executeBuilderCmsWrite(args: {
|
|
|
159
257
|
const body = JSON.stringify(args.request.body);
|
|
160
258
|
const headers = {
|
|
161
259
|
accept: "application/json",
|
|
162
|
-
authorization:
|
|
260
|
+
authorization: authorization.authorization,
|
|
163
261
|
"content-type": "application/json",
|
|
164
262
|
};
|
|
165
263
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PrivateBlobError,
|
|
3
|
+
registerPrivateBlobProvider,
|
|
4
|
+
unregisterPrivateBlobProvider,
|
|
5
|
+
type PrivateBlobHandle,
|
|
6
|
+
type PrivateBlobProvider,
|
|
7
|
+
} from "@agent-native/core/private-blob";
|
|
8
|
+
import { expect } from "vitest";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
BUILDER_EXECUTION_PAYLOAD_BLOB_KEY,
|
|
12
|
+
readBuilderExecutionPayload,
|
|
13
|
+
} from "./_builder-cms-blob-custody";
|
|
14
|
+
|
|
15
|
+
const PROVIDER_ID = "content-test-memory";
|
|
16
|
+
|
|
17
|
+
export function registerMemoryPrivateBlobProvider() {
|
|
18
|
+
const blobs = new Map<string, Uint8Array>();
|
|
19
|
+
let nextId = 0;
|
|
20
|
+
let putsUnavailable = false;
|
|
21
|
+
let deletesUnavailable = false;
|
|
22
|
+
let deleteCalls = 0;
|
|
23
|
+
const provider: PrivateBlobProvider = {
|
|
24
|
+
id: PROVIDER_ID,
|
|
25
|
+
name: "Content test memory",
|
|
26
|
+
isConfigured: () => true,
|
|
27
|
+
put: async (input) => {
|
|
28
|
+
if (putsUnavailable) {
|
|
29
|
+
throw new PrivateBlobError("store unavailable", "unavailable");
|
|
30
|
+
}
|
|
31
|
+
const id = `${PROVIDER_ID}:${++nextId}`;
|
|
32
|
+
blobs.set(id, new Uint8Array(input.data));
|
|
33
|
+
return { id, provider: PROVIDER_ID, opaque: true, encrypted: false };
|
|
34
|
+
},
|
|
35
|
+
read: async (handle: PrivateBlobHandle) => {
|
|
36
|
+
const data = blobs.get(handle.id);
|
|
37
|
+
if (!data) throw new PrivateBlobError("missing", "not_found");
|
|
38
|
+
return { data, handle };
|
|
39
|
+
},
|
|
40
|
+
delete: async (handle: PrivateBlobHandle) => {
|
|
41
|
+
deleteCalls += 1;
|
|
42
|
+
return {
|
|
43
|
+
...(deletesUnavailable
|
|
44
|
+
? { deleted: false, reason: "temporary storage failure" }
|
|
45
|
+
: { deleted: blobs.delete(handle.id) }),
|
|
46
|
+
provider: PROVIDER_ID,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
registerPrivateBlobProvider(provider);
|
|
51
|
+
return {
|
|
52
|
+
blobs,
|
|
53
|
+
setPutsUnavailable: (value: boolean) => {
|
|
54
|
+
putsUnavailable = value;
|
|
55
|
+
},
|
|
56
|
+
setDeletesUnavailable: (value: boolean) => {
|
|
57
|
+
deletesUnavailable = value;
|
|
58
|
+
},
|
|
59
|
+
getDeleteCalls: () => deleteCalls,
|
|
60
|
+
unregister: () => unregisterPrivateBlobProvider(PROVIDER_ID),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function storedExecutionPayload(execution: {
|
|
65
|
+
id: string;
|
|
66
|
+
ownerEmail: string;
|
|
67
|
+
sourceId: string;
|
|
68
|
+
changeSetId: string;
|
|
69
|
+
idempotencyKey: string;
|
|
70
|
+
payloadJson: string;
|
|
71
|
+
}): Promise<any> {
|
|
72
|
+
// The reader also accepts legacy inline rows, so assert custody here: SQL
|
|
73
|
+
// holds only the blob reference and a receipt marker, never the request.
|
|
74
|
+
const stored = JSON.parse(execution.payloadJson) as Record<string, unknown>;
|
|
75
|
+
const { [BUILDER_EXECUTION_PAYLOAD_BLOB_KEY]: reference, ...rest } = stored;
|
|
76
|
+
expect(reference).toBeDefined();
|
|
77
|
+
expect(rest).toEqual(
|
|
78
|
+
"response" in rest ? { response: { stored: true } } : {},
|
|
79
|
+
);
|
|
80
|
+
return readBuilderExecutionPayload({
|
|
81
|
+
payloadJson: execution.payloadJson,
|
|
82
|
+
binding: {
|
|
83
|
+
ownerEmail: execution.ownerEmail,
|
|
84
|
+
sourceId: execution.sourceId,
|
|
85
|
+
changeSetId: execution.changeSetId,
|
|
86
|
+
executionId: execution.id,
|
|
87
|
+
idempotencyKey: execution.idempotencyKey,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fail } from "@agent-native/core/action";
|
|
2
|
+
import { and, inArray } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
5
|
+
import { isBuilderExecutionClaimStale } from "./_builder-cms-execution-lease.js";
|
|
6
|
+
|
|
7
|
+
export async function assertNoActiveBuilderSourceExecutions(
|
|
8
|
+
tx: ReturnType<typeof getDb>,
|
|
9
|
+
sourceIds: string[],
|
|
10
|
+
changeSetIds?: string[],
|
|
11
|
+
) {
|
|
12
|
+
if (sourceIds.length === 0 || changeSetIds?.length === 0) return;
|
|
13
|
+
|
|
14
|
+
const candidates = await tx
|
|
15
|
+
.select({
|
|
16
|
+
state: schema.contentDatabaseSourceExecutions.state,
|
|
17
|
+
updatedAt: schema.contentDatabaseSourceExecutions.updatedAt,
|
|
18
|
+
})
|
|
19
|
+
.from(schema.contentDatabaseSourceExecutions)
|
|
20
|
+
.where(
|
|
21
|
+
and(
|
|
22
|
+
inArray(schema.contentDatabaseSourceExecutions.sourceId, sourceIds),
|
|
23
|
+
changeSetIds
|
|
24
|
+
? inArray(
|
|
25
|
+
schema.contentDatabaseSourceExecutions.changeSetId,
|
|
26
|
+
changeSetIds,
|
|
27
|
+
)
|
|
28
|
+
: undefined,
|
|
29
|
+
inArray(schema.contentDatabaseSourceExecutions.state, [
|
|
30
|
+
"running",
|
|
31
|
+
"response_received",
|
|
32
|
+
"reconciliation_required",
|
|
33
|
+
]),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const active = candidates.some(
|
|
38
|
+
(execution) =>
|
|
39
|
+
execution.state !== "running" ||
|
|
40
|
+
!isBuilderExecutionClaimStale(execution.updatedAt, now),
|
|
41
|
+
);
|
|
42
|
+
if (active) {
|
|
43
|
+
fail(
|
|
44
|
+
"Cannot replace source records while a Builder execution is running or awaiting reconciliation.",
|
|
45
|
+
{ errorCode: "builder_execution_running", statusCode: 409 },
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -13,6 +13,7 @@ export type ContentSaveOutcome =
|
|
|
13
13
|
| "preserved_to_history"
|
|
14
14
|
| "merged"
|
|
15
15
|
| "merged_with_displaced_text"
|
|
16
|
+
| "live_copy_body"
|
|
16
17
|
| "refused"
|
|
17
18
|
| "applied"
|
|
18
19
|
| "displaced_preserved"
|
|
@@ -63,6 +64,10 @@ const reasons = new Set([
|
|
|
63
64
|
"stale_empty_body",
|
|
64
65
|
"stale_builder_image",
|
|
65
66
|
"recovery_base_changed",
|
|
67
|
+
"live_copy_save_held",
|
|
68
|
+
"live_copy_save_unheld",
|
|
69
|
+
"live_copy_save_noncanonical",
|
|
70
|
+
"live_copy_save_unchecked",
|
|
66
71
|
"title_base_changed",
|
|
67
72
|
"body_base_changed",
|
|
68
73
|
"body_revision_cas_conflict",
|