@opengeni/db 0.28.1 → 0.31.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.
- package/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +958 -126
- package/dist/index.js +31204 -12172
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +19122 -8731
- package/dist/schema.js +173 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +8 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings-schema.d.ts +44 -6
- package/dist/transcription-recordings.d.ts +3 -3
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +434 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/environment-crypto.ts +22 -9
- package/src/generated-images.ts +740 -0
- package/src/index.ts +10117 -2730
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +26 -34
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +514 -5
- package/src/schema.ts +5177 -2957
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +133 -81
- package/src/session-queue-commands.ts +511 -247
- package/src/session-realtime-context.ts +20 -7
- package/src/session-realtime-ledger.ts +32 -10
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +10 -9
- package/src/session-tool-call-settlement.ts +38 -15
- package/src/slack-user-link-access.ts +702 -0
- package/src/transcription-recordings-schema.ts +5 -2
- package/src/transcription-recordings.ts +70 -40
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-DBS5HPEW.js.map +0 -1
- package/dist/chunk-HT5T62CC.js +0 -6803
- package/dist/chunk-HT5T62CC.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,3535 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rawRows,
|
|
3
|
+
withRlsContext
|
|
4
|
+
} from "./chunk-ISI2TIUG.js";
|
|
5
|
+
|
|
6
|
+
// src/editable-artifacts.ts
|
|
7
|
+
import { createHash } from "crypto";
|
|
8
|
+
import {
|
|
9
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION,
|
|
10
|
+
MAX_COMMITTED_TRANSACTION_BYTES,
|
|
11
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS,
|
|
12
|
+
decodeCommittedTransactionSummary
|
|
13
|
+
} from "@opengeni/contracts/editable-artifact-committed-transaction";
|
|
14
|
+
import {
|
|
15
|
+
EDITABLE_ARTIFACT_NATIVE_RECEIPT_MAX_BYTES,
|
|
16
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
17
|
+
decodeEditableArtifactSerializedCommit
|
|
18
|
+
} from "@opengeni/contracts/editable-artifact-serialized-commit";
|
|
19
|
+
import {
|
|
20
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES,
|
|
21
|
+
EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES
|
|
22
|
+
} from "@opengeni/contracts/editable-artifacts";
|
|
23
|
+
import { sql } from "drizzle-orm";
|
|
24
|
+
var EditableArtifactPersistenceError = class extends Error {
|
|
25
|
+
constructor(code, message, options) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.name = "EditableArtifactPersistenceError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
async function touchEditableArtifactSessionLink(db, scopeInput, sessionIdInput, artifactIdInput) {
|
|
32
|
+
const scope = validateScope(scopeInput);
|
|
33
|
+
const sessionId = validateUuid(sessionIdInput, "session id");
|
|
34
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
35
|
+
await withRlsContext(db, scope, async (tx) => {
|
|
36
|
+
await tx.execute(sql`insert into editable_artifact_session_links (
|
|
37
|
+
account_id, workspace_id, session_id, artifact_id
|
|
38
|
+
) values (
|
|
39
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, ${sessionId}::uuid, ${artifactId}
|
|
40
|
+
)
|
|
41
|
+
on conflict (account_id, workspace_id, session_id, artifact_id)
|
|
42
|
+
do update set last_used_at = greatest(
|
|
43
|
+
editable_artifact_session_links.last_used_at,
|
|
44
|
+
now()
|
|
45
|
+
)`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function listEditableArtifactIdsForSession(db, scopeInput, sessionIdInput, limitInput = 64) {
|
|
49
|
+
const scope = validateScope(scopeInput);
|
|
50
|
+
const sessionId = validateUuid(sessionIdInput, "session id");
|
|
51
|
+
const limit = validateInteger(limitInput, "editable artifact list limit", 1, 64);
|
|
52
|
+
return await withRlsContext(
|
|
53
|
+
db,
|
|
54
|
+
scope,
|
|
55
|
+
async (tx) => {
|
|
56
|
+
const rows = await rawRows(
|
|
57
|
+
tx,
|
|
58
|
+
sql`select artifact_id
|
|
59
|
+
from editable_artifact_session_links
|
|
60
|
+
where account_id = ${scope.accountId}::uuid
|
|
61
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
62
|
+
and session_id = ${sessionId}::uuid
|
|
63
|
+
order by last_used_at desc, artifact_id
|
|
64
|
+
limit ${limit}`
|
|
65
|
+
);
|
|
66
|
+
return Object.freeze(rows.map((row) => validateStableId(row.artifact_id, "artifact id")));
|
|
67
|
+
},
|
|
68
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
var EDITABLE_ARTIFACT_INTENT_MAX_BYTES = 5 * 1024 * 1024;
|
|
72
|
+
var EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_TRANSACTIONS = 1e5;
|
|
73
|
+
var EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_BYTES = 64 * 1024 * 1024;
|
|
74
|
+
var EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
|
|
75
|
+
var PostgresEditableArtifactLiveTicketStore = class {
|
|
76
|
+
constructor(db) {
|
|
77
|
+
this.db = db;
|
|
78
|
+
}
|
|
79
|
+
async put(recordInput) {
|
|
80
|
+
const record = validateLiveTicketRecord(recordInput);
|
|
81
|
+
const actor = record.actor;
|
|
82
|
+
try {
|
|
83
|
+
await withRlsContext(this.db, record.scope, async (tx) => {
|
|
84
|
+
await rawRows(
|
|
85
|
+
tx,
|
|
86
|
+
sql`select opengeni_private.put_editable_artifact_live_ticket(
|
|
87
|
+
${record.tokenDigest}, ${record.scope.accountId}::uuid,
|
|
88
|
+
${record.scope.workspaceId}::uuid, ${record.artifactId}, ${record.modality},
|
|
89
|
+
${actor.kind}, ${actor.subjectId}, ${actor.replicaId},
|
|
90
|
+
${actor.kind === "agent" ? actor.sessionId : null},
|
|
91
|
+
${actor.kind === "agent" ? actor.turnId : null},
|
|
92
|
+
${actor.kind === "agent" ? actor.attemptId : null},
|
|
93
|
+
${actor.kind === "agent" ? actor.generation : null},
|
|
94
|
+
${actor.kind === "service" ? actor.service : null},
|
|
95
|
+
${record.allowEdit}, ${record.protocolVersion}, ${record.issuedAt}::timestamptz,
|
|
96
|
+
${record.expiresAt}::timestamptz, current_schema()
|
|
97
|
+
) as stored`
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (errorCode(error) === "23505") {
|
|
102
|
+
throw new Error("Live ticket digest collision", { cause: error });
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async consume(tokenDigestInput) {
|
|
108
|
+
const tokenDigest = validateHash(tokenDigestInput, "live ticket digest");
|
|
109
|
+
const rows = await rawRows(
|
|
110
|
+
this.db,
|
|
111
|
+
sql`select * from opengeni_private.consume_editable_artifact_live_ticket(
|
|
112
|
+
${tokenDigest}, current_schema()
|
|
113
|
+
)`
|
|
114
|
+
);
|
|
115
|
+
if (rows.length > 1) throw corrupt("Live ticket consume returned multiple records");
|
|
116
|
+
return rows[0] ? liveTicketFromRow(rows[0]) : null;
|
|
117
|
+
}
|
|
118
|
+
async cleanupExpired(limitInput = 1e3) {
|
|
119
|
+
const limit = validateInteger(limitInput, "live ticket cleanup limit", 1, 1e4);
|
|
120
|
+
const rows = await rawRows(
|
|
121
|
+
this.db,
|
|
122
|
+
sql`select opengeni_private.cleanup_expired_editable_artifact_live_tickets(
|
|
123
|
+
${limit}, current_schema()
|
|
124
|
+
) as removed`
|
|
125
|
+
);
|
|
126
|
+
return safeInteger(rows[0]?.removed ?? null, "live ticket cleanup count", 0);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var PostgresEditableArtifactStore = class {
|
|
130
|
+
constructor(db) {
|
|
131
|
+
this.db = db;
|
|
132
|
+
}
|
|
133
|
+
async findArtifactCreation(scopeInput, operationKindInput, authorityKeyInput, idempotencyKeyInput) {
|
|
134
|
+
const scope = validateScope(scopeInput);
|
|
135
|
+
const operationKind = validateOriginOperation(operationKindInput);
|
|
136
|
+
const authorityKey = validateActorKey(authorityKeyInput).key;
|
|
137
|
+
const idempotencyKey = validateClientTransactionId(idempotencyKeyInput);
|
|
138
|
+
return await withRlsContext(
|
|
139
|
+
this.db,
|
|
140
|
+
scope,
|
|
141
|
+
async (tx) => {
|
|
142
|
+
const rows = await rawRows(
|
|
143
|
+
tx,
|
|
144
|
+
sql`select id, operation_kind, authority_key, idempotency_key, request_hash,
|
|
145
|
+
resource_type, resource_id, result, created_at
|
|
146
|
+
from editable_artifact_idempotency_receipts
|
|
147
|
+
where account_id = ${scope.accountId}::uuid
|
|
148
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
149
|
+
and operation_kind = ${operationKind}
|
|
150
|
+
and authority_key_digest = sha256(convert_to(${authorityKey}, 'UTF8'))
|
|
151
|
+
and authority_key = ${authorityKey}
|
|
152
|
+
and idempotency_key = ${idempotencyKey}
|
|
153
|
+
limit 1`
|
|
154
|
+
);
|
|
155
|
+
const row = rows[0];
|
|
156
|
+
if (!row) return null;
|
|
157
|
+
const creationReceipt = creationReceiptFromRow(scope, row);
|
|
158
|
+
const artifactRow = await loadArtifactRow(tx, scope, creationReceipt.artifactId, false);
|
|
159
|
+
if (!artifactRow) throw corrupt("Artifact creation receipt points at a missing artifact");
|
|
160
|
+
const artifact = artifactFromRow(artifactRow);
|
|
161
|
+
if (artifact.currentSnapshotId !== creationReceipt.genesisSnapshotId) {
|
|
162
|
+
throw corrupt("Artifact creation receipt disagrees with the genesis snapshot pointer");
|
|
163
|
+
}
|
|
164
|
+
const genesisSnapshot = await loadSnapshotMetadata(
|
|
165
|
+
tx,
|
|
166
|
+
scope,
|
|
167
|
+
artifact.id,
|
|
168
|
+
creationReceipt.genesisSnapshotId
|
|
169
|
+
);
|
|
170
|
+
if (!genesisSnapshot) {
|
|
171
|
+
throw corrupt("Artifact creation receipt points at a missing genesis snapshot");
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
artifact,
|
|
175
|
+
genesisSnapshot,
|
|
176
|
+
creationReceipt,
|
|
177
|
+
replayed: true
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
async createArtifact(input) {
|
|
184
|
+
const scope = validateScope(input.scope);
|
|
185
|
+
const artifactId = validateStableId(input.artifactId, "artifact id");
|
|
186
|
+
const authorizationActor = validateLiveTicketActor(input.authorizationActor);
|
|
187
|
+
const receiptId = validateStableId(input.receiptId, "creation receipt id");
|
|
188
|
+
const authorityKey = validateActorKey(input.authorityKey).key;
|
|
189
|
+
if (persistedEditableArtifactActorKey(authorizationActor) !== authorityKey) {
|
|
190
|
+
throw new TypeError("Artifact creation authority does not match its actor key");
|
|
191
|
+
}
|
|
192
|
+
const idempotencyKey = validateClientTransactionId(input.idempotencyKey);
|
|
193
|
+
const requestHash = validateHash(input.requestHash, "creation request hash");
|
|
194
|
+
const operationKind = validateOriginOperation(input.operationKind);
|
|
195
|
+
if (operationKind === "import" !== Boolean(input.originalImport)) {
|
|
196
|
+
throw new TypeError("Artifact import source does not match its origin operation");
|
|
197
|
+
}
|
|
198
|
+
const expectedScopeAuthorizationRevision = validateInteger(
|
|
199
|
+
input.expectedScopeAuthorizationRevision,
|
|
200
|
+
"expected scope create authorization revision",
|
|
201
|
+
1
|
|
202
|
+
);
|
|
203
|
+
const initialArtifactAuthorizationRevision = validateInteger(
|
|
204
|
+
input.initialArtifactAuthorizationRevision,
|
|
205
|
+
"initial artifact authorization revision",
|
|
206
|
+
1
|
|
207
|
+
);
|
|
208
|
+
const title = input.title;
|
|
209
|
+
validateTitle(title);
|
|
210
|
+
const createdBySubjectId = validateIdentity(input.createdBySubjectId, "creator subject id");
|
|
211
|
+
if (!["spreadsheet", "presentation", "document"].includes(input.modality)) {
|
|
212
|
+
throw new TypeError("Unsupported editable artifact modality");
|
|
213
|
+
}
|
|
214
|
+
const modality = input.modality;
|
|
215
|
+
const genesisSnapshot = ownSnapshotMetadata(input.genesisSnapshot);
|
|
216
|
+
const originalImport = input.originalImport ? validateOriginalImport(input.originalImport, modality) : null;
|
|
217
|
+
const outbox = cloneOutbox(input.outbox);
|
|
218
|
+
const createdAtIso = genesisSnapshot.publishedAt;
|
|
219
|
+
const commonCandidateArtifact = {
|
|
220
|
+
scope,
|
|
221
|
+
id: artifactId,
|
|
222
|
+
title,
|
|
223
|
+
lifecycle: "active",
|
|
224
|
+
authorizationRevision: initialArtifactAuthorizationRevision,
|
|
225
|
+
headSequence: 0,
|
|
226
|
+
stateHash: genesisSnapshot.stateHash,
|
|
227
|
+
currentSnapshotId: null,
|
|
228
|
+
createdAt: createdAtIso,
|
|
229
|
+
updatedAt: createdAtIso
|
|
230
|
+
};
|
|
231
|
+
const candidateArtifact = modality === "spreadsheet" ? Object.freeze({
|
|
232
|
+
...commonCandidateArtifact,
|
|
233
|
+
modality,
|
|
234
|
+
causalFrontier: genesisSnapshot.modality === "spreadsheet" ? genesisSnapshot.coveredCausalFrontier : Object.freeze([])
|
|
235
|
+
}) : Object.freeze({ ...commonCandidateArtifact, modality });
|
|
236
|
+
validateGenesisCreation(candidateArtifact, genesisSnapshot, outbox, operationKind);
|
|
237
|
+
return await withRlsContext(
|
|
238
|
+
this.db,
|
|
239
|
+
scope,
|
|
240
|
+
async (tx) => {
|
|
241
|
+
await tx.execute(
|
|
242
|
+
sql`select pg_advisory_xact_lock(hashtextextended(
|
|
243
|
+
${`editable-artifact:${operationKind}:${scope.accountId}:${scope.workspaceId}:${authorityKey}:${idempotencyKey}`},
|
|
244
|
+
0
|
|
245
|
+
))`
|
|
246
|
+
);
|
|
247
|
+
const authorization = await transactionallyAuthorizeEditableArtifactActor(tx, {
|
|
248
|
+
scope,
|
|
249
|
+
artifactId,
|
|
250
|
+
actor: authorizationActor,
|
|
251
|
+
permission: operationKind
|
|
252
|
+
});
|
|
253
|
+
if (!authorization.allowed) {
|
|
254
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
255
|
+
}
|
|
256
|
+
const prior = await rawRows(
|
|
257
|
+
tx,
|
|
258
|
+
sql`
|
|
259
|
+
select id, operation_kind, authority_key, idempotency_key, request_hash,
|
|
260
|
+
resource_type, resource_id, result, created_at
|
|
261
|
+
from editable_artifact_idempotency_receipts
|
|
262
|
+
where account_id = ${scope.accountId}::uuid
|
|
263
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
264
|
+
and operation_kind = ${operationKind}
|
|
265
|
+
and authority_key_digest = sha256(convert_to(${authorityKey}, 'UTF8'))
|
|
266
|
+
and authority_key = ${authorityKey}
|
|
267
|
+
and idempotency_key = ${idempotencyKey}
|
|
268
|
+
limit 1`
|
|
269
|
+
);
|
|
270
|
+
if (prior[0]) {
|
|
271
|
+
if (validateHash(prior[0].request_hash, "stored creation request hash") !== requestHash) {
|
|
272
|
+
throw conflict("Artifact create idempotency key was reused with a different request");
|
|
273
|
+
}
|
|
274
|
+
if (prior[0].resource_type !== "artifact") {
|
|
275
|
+
throw corrupt("Artifact creation receipt points at an invalid resource type");
|
|
276
|
+
}
|
|
277
|
+
const priorArtifactId = validateStableId(
|
|
278
|
+
prior[0].resource_id,
|
|
279
|
+
"created artifact resource id"
|
|
280
|
+
);
|
|
281
|
+
const replayRow = await loadArtifactRow(tx, scope, priorArtifactId, false);
|
|
282
|
+
if (!replayRow) throw corrupt("Artifact creation receipt points at a missing artifact");
|
|
283
|
+
const replayArtifact = artifactFromRow(replayRow);
|
|
284
|
+
const creationReceipt = creationReceiptFromRow(scope, prior[0]);
|
|
285
|
+
if (creationReceipt.artifactId !== priorArtifactId) {
|
|
286
|
+
throw corrupt("Artifact creation receipt result disagrees with its resource");
|
|
287
|
+
}
|
|
288
|
+
if (replayArtifact.currentSnapshotId !== creationReceipt.genesisSnapshotId) {
|
|
289
|
+
throw corrupt("Artifact creation receipt disagrees with the genesis snapshot pointer");
|
|
290
|
+
}
|
|
291
|
+
const replaySnapshot = await loadSnapshotMetadata(
|
|
292
|
+
tx,
|
|
293
|
+
scope,
|
|
294
|
+
priorArtifactId,
|
|
295
|
+
creationReceipt.genesisSnapshotId
|
|
296
|
+
);
|
|
297
|
+
if (!replaySnapshot) {
|
|
298
|
+
throw corrupt("Artifact creation receipt points at a missing genesis snapshot");
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
kind: "result",
|
|
302
|
+
value: Object.freeze({
|
|
303
|
+
artifact: replayArtifact,
|
|
304
|
+
genesisSnapshot: replaySnapshot,
|
|
305
|
+
creationReceipt,
|
|
306
|
+
replayed: true
|
|
307
|
+
})
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (authorization.revision !== expectedScopeAuthorizationRevision) {
|
|
311
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
312
|
+
}
|
|
313
|
+
const [authorizationHead] = await rawRows(
|
|
314
|
+
tx,
|
|
315
|
+
sql`select opengeni_private.ensure_editable_artifact_scope_authorization_head(
|
|
316
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, current_schema()
|
|
317
|
+
) as create_revision`
|
|
318
|
+
);
|
|
319
|
+
if (!authorizationHead || safeInteger(
|
|
320
|
+
authorizationHead.create_revision,
|
|
321
|
+
"scope create authorization revision",
|
|
322
|
+
1
|
|
323
|
+
) !== expectedScopeAuthorizationRevision) {
|
|
324
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await tx.execute(sql`
|
|
328
|
+
insert into editable_artifacts (
|
|
329
|
+
account_id, workspace_id, id, modality, title, lifecycle_state,
|
|
330
|
+
authorization_revision, head_sequence, causal_frontier, state_hash,
|
|
331
|
+
current_snapshot_id, created_by_subject_id, created_at, updated_at
|
|
332
|
+
) values (
|
|
333
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, ${artifactId},
|
|
334
|
+
${modality}, ${title}, 'active', ${initialArtifactAuthorizationRevision},
|
|
335
|
+
0, ${modality === "spreadsheet" ? json(
|
|
336
|
+
genesisSnapshot.modality === "spreadsheet" ? genesisSnapshot.coveredCausalFrontier : []
|
|
337
|
+
) : sql`null`}::jsonb,
|
|
338
|
+
${genesisSnapshot.stateHash}, ${genesisSnapshot.snapshotId},
|
|
339
|
+
${createdBySubjectId}, ${createdAtIso}, ${createdAtIso}
|
|
340
|
+
)`);
|
|
341
|
+
await tx.execute(sql`
|
|
342
|
+
insert into editable_artifact_sequence_checkpoints (
|
|
343
|
+
account_id, workspace_id, artifact_id, head_sequence,
|
|
344
|
+
modality, causal_frontier, native_revision, state_hash,
|
|
345
|
+
transaction_id, created_at
|
|
346
|
+
) values (
|
|
347
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, ${artifactId},
|
|
348
|
+
0, ${modality},
|
|
349
|
+
${modality === "spreadsheet" ? json(
|
|
350
|
+
genesisSnapshot.modality === "spreadsheet" ? genesisSnapshot.coveredCausalFrontier : []
|
|
351
|
+
) : sql`null`}::jsonb,
|
|
352
|
+
${genesisSnapshot.modality === "spreadsheet" ? null : genesisSnapshot.nativeRevision},
|
|
353
|
+
${genesisSnapshot.stateHash}, null, ${createdAtIso}
|
|
354
|
+
)`);
|
|
355
|
+
await insertSnapshotBlobReference(tx, genesisSnapshot);
|
|
356
|
+
if (originalImport) {
|
|
357
|
+
await insertOriginalImportBlobReference(
|
|
358
|
+
tx,
|
|
359
|
+
scope,
|
|
360
|
+
artifactId,
|
|
361
|
+
originalImport,
|
|
362
|
+
createdAtIso
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
await insertSnapshotMetadata(tx, genesisSnapshot);
|
|
366
|
+
const receiptResult = Object.freeze({
|
|
367
|
+
schemaVersion: 1,
|
|
368
|
+
artifactId,
|
|
369
|
+
genesisSnapshotId: genesisSnapshot.snapshotId
|
|
370
|
+
});
|
|
371
|
+
await tx.execute(sql`
|
|
372
|
+
insert into editable_artifact_idempotency_receipts (
|
|
373
|
+
account_id, workspace_id, id, artifact_id, operation_kind,
|
|
374
|
+
authority_key, idempotency_key, request_hash, resource_type,
|
|
375
|
+
resource_id, server_transaction_id, result, created_at
|
|
376
|
+
) values (
|
|
377
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, ${receiptId},
|
|
378
|
+
${artifactId}, ${operationKind}, ${authorityKey}, ${idempotencyKey},
|
|
379
|
+
${requestHash}, 'artifact', ${artifactId}, null,
|
|
380
|
+
${json(receiptResult)}::jsonb, ${createdAtIso}
|
|
381
|
+
)`);
|
|
382
|
+
await insertLiveOutbox(tx, candidateArtifact, outbox);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw mapPersistenceError(error, "Editable artifact creation failed");
|
|
385
|
+
}
|
|
386
|
+
const row = await loadArtifactRow(tx, scope, artifactId, false);
|
|
387
|
+
if (!row)
|
|
388
|
+
throw new EditableArtifactPersistenceError(
|
|
389
|
+
"corrupt_history",
|
|
390
|
+
"Created artifact vanished"
|
|
391
|
+
);
|
|
392
|
+
const artifact = artifactFromRow(row);
|
|
393
|
+
return Object.freeze({
|
|
394
|
+
kind: "result",
|
|
395
|
+
value: Object.freeze({
|
|
396
|
+
artifact,
|
|
397
|
+
genesisSnapshot,
|
|
398
|
+
creationReceipt: Object.freeze({
|
|
399
|
+
receiptId,
|
|
400
|
+
scope,
|
|
401
|
+
artifactId,
|
|
402
|
+
operationKind,
|
|
403
|
+
authorityKey,
|
|
404
|
+
idempotencyKey,
|
|
405
|
+
requestHash,
|
|
406
|
+
genesisSnapshotId: genesisSnapshot.snapshotId,
|
|
407
|
+
createdAt: createdAtIso
|
|
408
|
+
}),
|
|
409
|
+
replayed: false
|
|
410
|
+
})
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
{ isolationLevel: "read committed", accessMode: "read write" }
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
async ensureScopeCreateAuthorizationHead(scopeInput) {
|
|
417
|
+
const scope = validateScope(scopeInput);
|
|
418
|
+
return await withRlsContext(this.db, scope, async (tx) => {
|
|
419
|
+
const rows = await rawRows(
|
|
420
|
+
tx,
|
|
421
|
+
sql`select opengeni_private.ensure_editable_artifact_scope_authorization_head(
|
|
422
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, current_schema()
|
|
423
|
+
) as create_revision`
|
|
424
|
+
);
|
|
425
|
+
if (!rows[0]) throw corrupt("Scope create authorization head was not materialized");
|
|
426
|
+
return Object.freeze({
|
|
427
|
+
scope,
|
|
428
|
+
createRevision: safeInteger(
|
|
429
|
+
rows[0].create_revision,
|
|
430
|
+
"scope create authorization revision",
|
|
431
|
+
1
|
|
432
|
+
)
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
async advanceScopeCreateAuthorizationRevision(scopeInput, expectedRevisionInput, nextRevisionInput) {
|
|
437
|
+
const scope = validateScope(scopeInput);
|
|
438
|
+
const expectedRevision = validateInteger(
|
|
439
|
+
expectedRevisionInput,
|
|
440
|
+
"expected scope create authorization revision",
|
|
441
|
+
1
|
|
442
|
+
);
|
|
443
|
+
const nextRevision = validateInteger(
|
|
444
|
+
nextRevisionInput,
|
|
445
|
+
"next scope create authorization revision",
|
|
446
|
+
1
|
|
447
|
+
);
|
|
448
|
+
if (nextRevision <= expectedRevision) {
|
|
449
|
+
throw new TypeError("Next scope create authorization revision must advance");
|
|
450
|
+
}
|
|
451
|
+
return await withRlsContext(this.db, scope, async (tx) => {
|
|
452
|
+
const rows = await rawRows(
|
|
453
|
+
tx,
|
|
454
|
+
sql`select *
|
|
455
|
+
from opengeni_private.advance_editable_artifact_scope_authorization_revision(
|
|
456
|
+
${scope.accountId}::uuid,
|
|
457
|
+
${scope.workspaceId}::uuid,
|
|
458
|
+
${expectedRevision},
|
|
459
|
+
${nextRevision},
|
|
460
|
+
current_schema()
|
|
461
|
+
)`
|
|
462
|
+
);
|
|
463
|
+
if (!rows[0]) throw corrupt("Scope create authorization head advance returned no result");
|
|
464
|
+
return Object.freeze({
|
|
465
|
+
applied: rows[0].applied === true,
|
|
466
|
+
authorizationRevision: safeInteger(
|
|
467
|
+
rows[0].authorization_revision,
|
|
468
|
+
"scope create authorization revision",
|
|
469
|
+
1
|
|
470
|
+
)
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async getArtifact(scopeInput, artifactIdInput) {
|
|
475
|
+
const scope = validateScope(scopeInput);
|
|
476
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
477
|
+
return await withRlsContext(this.db, scope, async (tx) => {
|
|
478
|
+
const row = await loadArtifactRow(tx, scope, artifactId, false);
|
|
479
|
+
return row ? artifactFromRow(row) : null;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async readArtifactAtAuthorizationRevision(scopeInput, artifactIdInput, expectedAuthorizationRevisionInput) {
|
|
483
|
+
const scope = validateScope(scopeInput);
|
|
484
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
485
|
+
const expectedAuthorizationRevision = validateInteger(
|
|
486
|
+
expectedAuthorizationRevisionInput,
|
|
487
|
+
"expected artifact authorization revision",
|
|
488
|
+
1
|
|
489
|
+
);
|
|
490
|
+
return await withRlsContext(
|
|
491
|
+
this.db,
|
|
492
|
+
scope,
|
|
493
|
+
async (tx) => {
|
|
494
|
+
const row = await loadArtifactRow(tx, scope, artifactId, false);
|
|
495
|
+
if (!row) return Object.freeze({ kind: "result", artifact: null });
|
|
496
|
+
const artifact = artifactFromRow(row);
|
|
497
|
+
if (artifact.authorizationRevision !== expectedAuthorizationRevision) {
|
|
498
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
499
|
+
}
|
|
500
|
+
return Object.freeze({ kind: "result", artifact });
|
|
501
|
+
},
|
|
502
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
async advanceAuthorizationRevision(scopeInput, artifactIdInput, expectedRevisionInput, nextRevisionInput) {
|
|
506
|
+
const scope = validateScope(scopeInput);
|
|
507
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
508
|
+
const expectedRevision = validateInteger(
|
|
509
|
+
expectedRevisionInput,
|
|
510
|
+
"expected authorization revision",
|
|
511
|
+
0
|
|
512
|
+
);
|
|
513
|
+
const nextRevision = validateInteger(nextRevisionInput, "next authorization revision", 1);
|
|
514
|
+
if (nextRevision <= expectedRevision) {
|
|
515
|
+
throw new TypeError("Next authorization revision must advance the expected revision");
|
|
516
|
+
}
|
|
517
|
+
return await withRlsContext(this.db, scope, async (tx) => {
|
|
518
|
+
const rows = await rawRows(
|
|
519
|
+
tx,
|
|
520
|
+
sql`select *
|
|
521
|
+
from opengeni_private.advance_editable_artifact_authorization_revision(
|
|
522
|
+
${scope.accountId}::uuid,
|
|
523
|
+
${scope.workspaceId}::uuid,
|
|
524
|
+
${artifactId},
|
|
525
|
+
${expectedRevision},
|
|
526
|
+
${nextRevision},
|
|
527
|
+
current_schema()
|
|
528
|
+
)`
|
|
529
|
+
);
|
|
530
|
+
const row = rows[0];
|
|
531
|
+
if (!row || row.authorization_revision === null) {
|
|
532
|
+
throw new EditableArtifactPersistenceError(
|
|
533
|
+
"not_found",
|
|
534
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
return Object.freeze({
|
|
538
|
+
applied: row.applied === true,
|
|
539
|
+
authorizationRevision: safeInteger(
|
|
540
|
+
row.authorization_revision,
|
|
541
|
+
"artifact authorization revision"
|
|
542
|
+
)
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async readTransactionBasis(scopeInput, artifactIdInput, request) {
|
|
547
|
+
const scope = validateScope(scopeInput);
|
|
548
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
549
|
+
const actorKey = validateActorKey(request.actorKey).key;
|
|
550
|
+
const clientTransactionId = validateClientTransactionId(request.clientTransactionId);
|
|
551
|
+
const previousLocalTransactionId = request.previousLocalTransactionId === null ? null : validateClientTransactionId(request.previousLocalTransactionId);
|
|
552
|
+
const selectiveUndoOperationIds = parseStableIdArray(
|
|
553
|
+
request.selectiveUndoOperationIds,
|
|
554
|
+
"undo targets",
|
|
555
|
+
1e4
|
|
556
|
+
);
|
|
557
|
+
return await withRlsContext(
|
|
558
|
+
this.db,
|
|
559
|
+
scope,
|
|
560
|
+
async (tx) => {
|
|
561
|
+
const row = await loadArtifactRow(tx, scope, artifactId, false);
|
|
562
|
+
if (!row) {
|
|
563
|
+
throw new EditableArtifactPersistenceError(
|
|
564
|
+
"not_found",
|
|
565
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const unitOfWork = new PostgresEditableArtifactUnitOfWork(tx, artifactFromRow(row));
|
|
569
|
+
const priorReceipt = await unitOfWork.findReceipt(actorKey, clientTransactionId);
|
|
570
|
+
if (priorReceipt) {
|
|
571
|
+
return Object.freeze({ kind: "existing", receipt: priorReceipt });
|
|
572
|
+
}
|
|
573
|
+
const predecessor = previousLocalTransactionId ? await unitOfWork.findReceipt(actorKey, previousLocalTransactionId) : null;
|
|
574
|
+
const undoTargets = await loadUndoBasis(tx, scope, artifactId, selectiveUndoOperationIds);
|
|
575
|
+
const kernelState = await unitOfWork.kernelState();
|
|
576
|
+
return Object.freeze({
|
|
577
|
+
kind: "basis",
|
|
578
|
+
artifact: unitOfWork.artifact(),
|
|
579
|
+
predecessor,
|
|
580
|
+
undoTargets,
|
|
581
|
+
kernelState
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
// Every basis member must describe one MVCC snapshot, but no row lock may
|
|
585
|
+
// survive the read. The pure kernel can safely run after this resolves.
|
|
586
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
async readCurrentKernelState(scopeInput, artifactIdInput, expectedAuthorizationRevisionInput) {
|
|
590
|
+
const scope = validateScope(scopeInput);
|
|
591
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
592
|
+
const expectedAuthorizationRevision = validateInteger(
|
|
593
|
+
expectedAuthorizationRevisionInput,
|
|
594
|
+
"expected compaction authorization revision",
|
|
595
|
+
1
|
|
596
|
+
);
|
|
597
|
+
return await withRlsContext(
|
|
598
|
+
this.db,
|
|
599
|
+
scope,
|
|
600
|
+
async (tx) => {
|
|
601
|
+
const row = await loadArtifactRow(tx, scope, artifactId, false);
|
|
602
|
+
if (!row) {
|
|
603
|
+
throw new EditableArtifactPersistenceError(
|
|
604
|
+
"not_found",
|
|
605
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const artifact = artifactFromRow(row);
|
|
609
|
+
if (artifact.authorizationRevision !== expectedAuthorizationRevision) {
|
|
610
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
611
|
+
}
|
|
612
|
+
const unitOfWork = new PostgresEditableArtifactUnitOfWork(tx, artifact);
|
|
613
|
+
return Object.freeze({ kind: "basis", state: await unitOfWork.kernelState() });
|
|
614
|
+
},
|
|
615
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
async tryCommitAppliedTransaction(request) {
|
|
619
|
+
const scope = validateScope(request.scope);
|
|
620
|
+
const artifactId = validateStableId(request.artifactId, "artifact id");
|
|
621
|
+
if (request.expectedLifecycle !== "active") {
|
|
622
|
+
throw new TypeError("Optimistic artifact commit requires the active lifecycle");
|
|
623
|
+
}
|
|
624
|
+
const actorKey = validateActorKey(request.actorKey).key;
|
|
625
|
+
const clientTransactionId = validateClientTransactionId(request.clientTransactionId);
|
|
626
|
+
const requestHash = validateHash(request.requestHash, "request hash");
|
|
627
|
+
const authorizationActor = validateLiveTicketActor(request.authorizationActor);
|
|
628
|
+
if (persistedEditableArtifactActorKey(authorizationActor) !== actorKey) {
|
|
629
|
+
throw new TypeError("Artifact commit authority does not match its actor key");
|
|
630
|
+
}
|
|
631
|
+
const expectedAuthorizationRevision = validateInteger(
|
|
632
|
+
request.expectedAuthorizationRevision,
|
|
633
|
+
"expected authorization revision",
|
|
634
|
+
0
|
|
635
|
+
);
|
|
636
|
+
const expectedUndoTargets = parseStableIdArray(
|
|
637
|
+
request.expectedUnclaimedUndoTargets,
|
|
638
|
+
"expected unclaimed undo targets",
|
|
639
|
+
1e4
|
|
640
|
+
);
|
|
641
|
+
const expectedPredecessor = validateExpectedPredecessor(request.expectedPredecessor);
|
|
642
|
+
if (request.receipt.scope.accountId !== scope.accountId || request.receipt.scope.workspaceId !== scope.workspaceId || request.receipt.artifactId !== artifactId || request.receipt.actorKey !== actorKey || request.receipt.clientTransactionId !== clientTransactionId || request.receipt.requestHash !== requestHash || (request.receipt.modality === "spreadsheet" ? !sameStringArray(request.receipt.selectiveUndoOperationIds, expectedUndoTargets) : expectedUndoTargets.length !== 0) || expectedPredecessor === null !== (request.receipt.previousLocalTransactionId === null) || expectedPredecessor !== null && request.receipt.previousLocalTransactionId !== expectedPredecessor.clientTransactionId) {
|
|
643
|
+
throw new TypeError("Optimistic commit preconditions do not match its durable receipt");
|
|
644
|
+
}
|
|
645
|
+
if (expectedPredecessor && expectedPredecessor.actorKey !== actorKey) {
|
|
646
|
+
throw new TypeError("Optimistic predecessor authority does not match the durable receipt");
|
|
647
|
+
}
|
|
648
|
+
const candidate = Object.freeze({
|
|
649
|
+
...request,
|
|
650
|
+
scope,
|
|
651
|
+
artifactId,
|
|
652
|
+
actorKey,
|
|
653
|
+
clientTransactionId,
|
|
654
|
+
requestHash,
|
|
655
|
+
expectedAuthorizationRevision,
|
|
656
|
+
authorizationActor,
|
|
657
|
+
expectedPredecessor,
|
|
658
|
+
expectedUnclaimedUndoTargets: expectedUndoTargets,
|
|
659
|
+
receipt: cloneReceipt(request.receipt),
|
|
660
|
+
operations: Object.freeze(request.operations.map(cloneOperation)),
|
|
661
|
+
committedTransaction: cloneCommittedTransaction(request.committedTransaction),
|
|
662
|
+
outbox: cloneOutbox(request.outbox)
|
|
663
|
+
});
|
|
664
|
+
validateOwnedCommitCandidate(candidate);
|
|
665
|
+
return await withRlsContext(
|
|
666
|
+
this.db,
|
|
667
|
+
scope,
|
|
668
|
+
async (tx) => {
|
|
669
|
+
const row = await loadArtifactRow(tx, scope, artifactId, true);
|
|
670
|
+
if (!row) {
|
|
671
|
+
throw new EditableArtifactPersistenceError(
|
|
672
|
+
"not_found",
|
|
673
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const authorization = await transactionallyAuthorizeEditableArtifactActor(tx, {
|
|
677
|
+
scope,
|
|
678
|
+
artifactId,
|
|
679
|
+
actor: authorizationActor,
|
|
680
|
+
permission: "edit"
|
|
681
|
+
});
|
|
682
|
+
if (!authorization.allowed) {
|
|
683
|
+
return Object.freeze({ kind: "stale" });
|
|
684
|
+
}
|
|
685
|
+
const priorAfterLock = await loadReceipt(
|
|
686
|
+
tx,
|
|
687
|
+
scope,
|
|
688
|
+
artifactId,
|
|
689
|
+
actorKey,
|
|
690
|
+
clientTransactionId
|
|
691
|
+
);
|
|
692
|
+
if (priorAfterLock) {
|
|
693
|
+
return replayResult(priorAfterLock);
|
|
694
|
+
}
|
|
695
|
+
const artifact = artifactFromRow(row);
|
|
696
|
+
if (authorization.revision !== candidate.expectedAuthorizationRevision || artifact.lifecycle !== candidate.expectedLifecycle || artifact.authorizationRevision !== candidate.expectedAuthorizationRevision || artifact.headSequence !== candidate.expectedHeadSequence) {
|
|
697
|
+
return Object.freeze({ kind: "stale" });
|
|
698
|
+
}
|
|
699
|
+
const unitOfWork = new PostgresEditableArtifactUnitOfWork(tx, artifact);
|
|
700
|
+
if (expectedPredecessor) {
|
|
701
|
+
const predecessor = await unitOfWork.findReceipt(
|
|
702
|
+
expectedPredecessor.actorKey,
|
|
703
|
+
expectedPredecessor.clientTransactionId
|
|
704
|
+
);
|
|
705
|
+
if (!predecessorMatches(predecessor, expectedPredecessor)) {
|
|
706
|
+
return Object.freeze({ kind: "stale" });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (expectedUndoTargets.length > 0 && await anyUndoTargetClaimed(tx, scope, artifactId, expectedUndoTargets)) {
|
|
710
|
+
return Object.freeze({ kind: "stale" });
|
|
711
|
+
}
|
|
712
|
+
await unitOfWork.commitAppliedTransaction(candidate);
|
|
713
|
+
return Object.freeze({
|
|
714
|
+
kind: "committed",
|
|
715
|
+
receipt: cloneReceipt(candidate.receipt)
|
|
716
|
+
});
|
|
717
|
+
},
|
|
718
|
+
{ isolationLevel: "read committed", accessMode: "read write" }
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
async withSnapshotPublicationLock(scopeInput, artifactIdInput, callback) {
|
|
722
|
+
const scope = validateScope(scopeInput);
|
|
723
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
724
|
+
return await withRlsContext(
|
|
725
|
+
this.db,
|
|
726
|
+
scope,
|
|
727
|
+
async (tx) => {
|
|
728
|
+
const row = await loadArtifactRow(tx, scope, artifactId, true);
|
|
729
|
+
if (!row) {
|
|
730
|
+
throw new EditableArtifactPersistenceError(
|
|
731
|
+
"not_found",
|
|
732
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const unitOfWork = new PostgresEditableArtifactUnitOfWork(tx, artifactFromRow(row));
|
|
736
|
+
let open = true;
|
|
737
|
+
const assertOpen = () => {
|
|
738
|
+
if (!open) throw conflict("Snapshot publication transaction is already closed");
|
|
739
|
+
};
|
|
740
|
+
const snapshotUnitOfWork = Object.freeze({
|
|
741
|
+
artifact: () => {
|
|
742
|
+
assertOpen();
|
|
743
|
+
return unitOfWork.artifact();
|
|
744
|
+
},
|
|
745
|
+
checkpoint: (headSequence) => {
|
|
746
|
+
assertOpen();
|
|
747
|
+
return unitOfWork.checkpoint(headSequence);
|
|
748
|
+
},
|
|
749
|
+
findSnapshot: (snapshotId) => {
|
|
750
|
+
assertOpen();
|
|
751
|
+
return unitOfWork.findSnapshot(snapshotId);
|
|
752
|
+
},
|
|
753
|
+
commitSnapshot: (input) => {
|
|
754
|
+
assertOpen();
|
|
755
|
+
return unitOfWork.commitSnapshot(input);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
try {
|
|
759
|
+
return await callback(snapshotUnitOfWork);
|
|
760
|
+
} finally {
|
|
761
|
+
open = false;
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
{ isolationLevel: "read committed", accessMode: "read write" }
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
async claimLiveOutbox(request) {
|
|
768
|
+
const owner = validateLeaseOwner(request.owner);
|
|
769
|
+
const leaseDurationMs = validateInteger(
|
|
770
|
+
request.leaseDurationMs,
|
|
771
|
+
"lease duration",
|
|
772
|
+
1,
|
|
773
|
+
864e5
|
|
774
|
+
);
|
|
775
|
+
const limit = validateInteger(request.limit, "claim limit", 1, 1e3);
|
|
776
|
+
const rows = await rawRows(
|
|
777
|
+
this.db,
|
|
778
|
+
sql`select * from opengeni_private.claim_editable_artifact_live_outbox(
|
|
779
|
+
${owner}, ${leaseDurationMs}, ${limit}, current_schema()
|
|
780
|
+
)`
|
|
781
|
+
);
|
|
782
|
+
return Object.freeze(rows.map(outboxFromRow));
|
|
783
|
+
}
|
|
784
|
+
async markLiveOutboxPublished(input) {
|
|
785
|
+
const outboxId = validateStableId(input.outboxId, "outbox id");
|
|
786
|
+
const owner = validateLeaseOwner(input.owner);
|
|
787
|
+
const attemptCount = validateInteger(
|
|
788
|
+
input.attemptCount,
|
|
789
|
+
"outbox fencing attempt",
|
|
790
|
+
1,
|
|
791
|
+
1e6
|
|
792
|
+
);
|
|
793
|
+
const rows = await rawRows(
|
|
794
|
+
this.db,
|
|
795
|
+
sql`select opengeni_private.mark_editable_artifact_live_outbox_published(
|
|
796
|
+
${outboxId}, ${owner}, ${attemptCount}, current_schema()
|
|
797
|
+
) as applied`
|
|
798
|
+
);
|
|
799
|
+
if (rows[0]?.applied !== true) throw outboxLeaseConflict();
|
|
800
|
+
}
|
|
801
|
+
async renewLiveOutbox(input) {
|
|
802
|
+
const outboxId = validateStableId(input.outboxId, "outbox id");
|
|
803
|
+
const owner = validateLeaseOwner(input.owner);
|
|
804
|
+
const attemptCount = validateInteger(
|
|
805
|
+
input.attemptCount,
|
|
806
|
+
"outbox fencing attempt",
|
|
807
|
+
1,
|
|
808
|
+
1e6
|
|
809
|
+
);
|
|
810
|
+
const leaseDurationMs = validateInteger(input.leaseDurationMs, "lease duration", 1, 864e5);
|
|
811
|
+
const rows = await rawRows(
|
|
812
|
+
this.db,
|
|
813
|
+
sql`select opengeni_private.renew_editable_artifact_live_outbox(
|
|
814
|
+
${outboxId}, ${owner}, ${attemptCount}, ${leaseDurationMs}, current_schema()
|
|
815
|
+
) as applied`
|
|
816
|
+
);
|
|
817
|
+
if (rows[0]?.applied !== true) throw outboxLeaseConflict();
|
|
818
|
+
}
|
|
819
|
+
async retryLiveOutbox(input) {
|
|
820
|
+
const outboxId = validateStableId(input.outboxId, "outbox id");
|
|
821
|
+
const owner = validateLeaseOwner(input.owner);
|
|
822
|
+
const attemptCount = validateInteger(
|
|
823
|
+
input.attemptCount,
|
|
824
|
+
"outbox fencing attempt",
|
|
825
|
+
1,
|
|
826
|
+
1e6
|
|
827
|
+
);
|
|
828
|
+
const retryDelayMs = validateInteger(input.retryDelayMs, "outbox retry delay", 1, 864e5);
|
|
829
|
+
const validatedErrorCode = validateOutboxRetryErrorCode(input.errorCode);
|
|
830
|
+
const rows = await rawRows(
|
|
831
|
+
this.db,
|
|
832
|
+
sql`select opengeni_private.retry_editable_artifact_live_outbox(
|
|
833
|
+
${outboxId}, ${owner}, ${attemptCount}, ${retryDelayMs}, ${validatedErrorCode}, current_schema()
|
|
834
|
+
) as applied`
|
|
835
|
+
);
|
|
836
|
+
if (rows[0]?.applied !== true) throw outboxLeaseConflict();
|
|
837
|
+
}
|
|
838
|
+
async deadLetterLiveOutbox(input) {
|
|
839
|
+
const outboxId = validateStableId(input.outboxId, "outbox id");
|
|
840
|
+
const owner = validateLeaseOwner(input.owner);
|
|
841
|
+
const attemptCount = validateInteger(
|
|
842
|
+
input.attemptCount,
|
|
843
|
+
"outbox fencing attempt",
|
|
844
|
+
1,
|
|
845
|
+
1e6
|
|
846
|
+
);
|
|
847
|
+
const validatedErrorCode = validateOutboxDeadLetterErrorCode(input.errorCode);
|
|
848
|
+
const rows = await rawRows(
|
|
849
|
+
this.db,
|
|
850
|
+
sql`select opengeni_private.dead_letter_editable_artifact_live_outbox(
|
|
851
|
+
${outboxId}, ${owner}, ${attemptCount}, ${validatedErrorCode}, current_schema()
|
|
852
|
+
) as applied`
|
|
853
|
+
);
|
|
854
|
+
if (rows[0]?.applied !== true) throw outboxLeaseConflict();
|
|
855
|
+
}
|
|
856
|
+
async releaseLiveOutbox(input) {
|
|
857
|
+
const outboxId = validateStableId(input.outboxId, "outbox id");
|
|
858
|
+
const owner = validateLeaseOwner(input.owner);
|
|
859
|
+
const attemptCount = validateInteger(
|
|
860
|
+
input.attemptCount,
|
|
861
|
+
"outbox fencing attempt",
|
|
862
|
+
1,
|
|
863
|
+
1e6
|
|
864
|
+
);
|
|
865
|
+
const rows = await rawRows(
|
|
866
|
+
this.db,
|
|
867
|
+
sql`select opengeni_private.release_editable_artifact_live_outbox(
|
|
868
|
+
${outboxId}, ${owner}, ${attemptCount}, current_schema()
|
|
869
|
+
) as applied`
|
|
870
|
+
);
|
|
871
|
+
if (rows[0]?.applied !== true) throw outboxLeaseConflict();
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
var PostgresEditableArtifactLiveReadStore = class {
|
|
875
|
+
constructor(db, options) {
|
|
876
|
+
this.db = db;
|
|
877
|
+
this.options = options;
|
|
878
|
+
this.replicaLeaseTtlMs = validateInteger(
|
|
879
|
+
options.replicaLeaseTtlMs ?? 5 * 6e4,
|
|
880
|
+
"replica lease ttl",
|
|
881
|
+
1e3,
|
|
882
|
+
864e5
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
replicaLeaseTtlMs;
|
|
886
|
+
async readBootstrap(input) {
|
|
887
|
+
const scope = validateScope(input.scope);
|
|
888
|
+
const artifactId = validateStableId(input.artifactId, "artifact id");
|
|
889
|
+
validateInteger(input.protocolVersion, "live protocol version", 1);
|
|
890
|
+
const resume = validateLiveResume(input.resume);
|
|
891
|
+
const authority = await withRlsContext(
|
|
892
|
+
this.db,
|
|
893
|
+
scope,
|
|
894
|
+
async (tx) => {
|
|
895
|
+
const current = await loadLiveAuthority(tx, scope, artifactId);
|
|
896
|
+
if (resume.modality !== current.artifact.modality) {
|
|
897
|
+
throw new TypeError("Live resume modality differs from its durable artifact");
|
|
898
|
+
}
|
|
899
|
+
let resumeCheckpoint = null;
|
|
900
|
+
if (!resume.requireSnapshot && resume.localCursor !== null && resume.localStateHash !== null && resume.localCursor + 1 >= current.minimumReplaySequence && resume.localCursor <= current.artifact.headSequence) {
|
|
901
|
+
resumeCheckpoint = await loadCheckpoint(tx, scope, artifactId, resume.localCursor);
|
|
902
|
+
}
|
|
903
|
+
const resumeAccepted = resumeCheckpoint !== null && resumeCheckpoint.modality === resume.modality && resumeCheckpoint.stateHash === resume.localStateHash && (resume.modality === "spreadsheet" ? resumeCheckpoint.modality === "spreadsheet" && sameFrontier(resumeCheckpoint.causalFrontier, resume.localCausalFrontier) : resumeCheckpoint.modality !== "spreadsheet" && resumeCheckpoint.nativeRevision === resume.localNativeRevision);
|
|
904
|
+
return Object.freeze({ current, resumeAccepted });
|
|
905
|
+
},
|
|
906
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
907
|
+
);
|
|
908
|
+
const snapshot = authority.resumeAccepted ? null : await this.readVerifiedSnapshotBytes(authority.current.snapshot);
|
|
909
|
+
const resumeSequence = authority.resumeAccepted ? resume.localCursor : authority.current.snapshot.coveredHeadSequence;
|
|
910
|
+
const resumeStateHash = authority.resumeAccepted ? resume.localStateHash : authority.current.snapshot.stateHash;
|
|
911
|
+
return Object.freeze({
|
|
912
|
+
...liveHeadFromAuthority(authority.current),
|
|
913
|
+
resumeAccepted: authority.resumeAccepted,
|
|
914
|
+
resumeSequence,
|
|
915
|
+
resumeStateHash,
|
|
916
|
+
snapshot
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
async readHead(scopeInput, artifactIdInput) {
|
|
920
|
+
const scope = validateScope(scopeInput);
|
|
921
|
+
const artifactId = validateStableId(artifactIdInput, "artifact id");
|
|
922
|
+
return await withRlsContext(
|
|
923
|
+
this.db,
|
|
924
|
+
scope,
|
|
925
|
+
async (tx) => liveHeadFromAuthority(await loadLiveAuthority(tx, scope, artifactId)),
|
|
926
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
async readTransactions(input) {
|
|
930
|
+
const scope = validateScope(input.scope);
|
|
931
|
+
const artifactId = validateStableId(input.artifactId, "artifact id");
|
|
932
|
+
const after = validateInteger(input.after, "live replay cursor", 0);
|
|
933
|
+
const through = validateInteger(input.through, "live replay target", after);
|
|
934
|
+
const maxCount = validateInteger(input.maxCount, "live replay count", 1, 1e4);
|
|
935
|
+
const maxBytes = validateInteger(
|
|
936
|
+
input.maxBytes,
|
|
937
|
+
"live replay bytes",
|
|
938
|
+
1,
|
|
939
|
+
EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_BYTES
|
|
940
|
+
);
|
|
941
|
+
return await withRlsContext(
|
|
942
|
+
this.db,
|
|
943
|
+
scope,
|
|
944
|
+
async (tx) => {
|
|
945
|
+
const authority = await loadLiveAuthority(tx, scope, artifactId);
|
|
946
|
+
if (through > authority.artifact.headSequence) {
|
|
947
|
+
throw corrupt("Live replay target is beyond the durable head");
|
|
948
|
+
}
|
|
949
|
+
const rows = await loadCommittedTransactionRows(
|
|
950
|
+
tx,
|
|
951
|
+
scope,
|
|
952
|
+
artifactId,
|
|
953
|
+
after,
|
|
954
|
+
through,
|
|
955
|
+
maxCount
|
|
956
|
+
);
|
|
957
|
+
const transactions = [];
|
|
958
|
+
let byteSize = 0;
|
|
959
|
+
for (const row of rows) {
|
|
960
|
+
const committed = committedTransactionFromRow(row, authority.artifact.modality);
|
|
961
|
+
const nextSize = byteSize + committed.committedTransactionBytes.byteLength;
|
|
962
|
+
if (nextSize > maxBytes) break;
|
|
963
|
+
transactions.push(liveTransactionFromCommitted(committed));
|
|
964
|
+
byteSize = nextSize;
|
|
965
|
+
}
|
|
966
|
+
return Object.freeze({
|
|
967
|
+
transactions: Object.freeze(transactions),
|
|
968
|
+
headSequence: authority.artifact.headSequence,
|
|
969
|
+
minimumReplaySequence: authority.minimumReplaySequence
|
|
970
|
+
});
|
|
971
|
+
},
|
|
972
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
async readCommittedTransaction(input) {
|
|
976
|
+
const scope = validateScope(input.scope);
|
|
977
|
+
const artifactId = validateStableId(input.artifactId, "artifact id");
|
|
978
|
+
const transactionId = validateStableId(input.transactionId, "transaction id");
|
|
979
|
+
return await withRlsContext(
|
|
980
|
+
this.db,
|
|
981
|
+
scope,
|
|
982
|
+
async (tx) => {
|
|
983
|
+
const artifactRow = await loadArtifactRow(tx, scope, artifactId, false);
|
|
984
|
+
if (!artifactRow) return null;
|
|
985
|
+
const artifact = artifactFromRow(artifactRow);
|
|
986
|
+
const rows = await loadCommittedTransactionRowsById(tx, scope, artifactId, transactionId);
|
|
987
|
+
return rows[0] ? liveTransactionFromCommitted(committedTransactionFromRow(rows[0], artifact.modality)) : null;
|
|
988
|
+
},
|
|
989
|
+
{ isolationLevel: "repeatable read", accessMode: "read only" }
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
async acknowledgeReplica(input) {
|
|
993
|
+
const scope = validateScope(input.scope);
|
|
994
|
+
const artifactId = validateStableId(input.artifactId, "artifact id");
|
|
995
|
+
const replicaId = validateReplicaId(input.replicaId);
|
|
996
|
+
const actorKey = validateActorKey(input.actorKey).key;
|
|
997
|
+
validateBoundedText(input.streamEpoch, "stream epoch", 512);
|
|
998
|
+
const sequence = validateInteger(input.sequence, "applied sequence", 0);
|
|
999
|
+
const stateHash = validateHash(input.stateHash, "applied state hash");
|
|
1000
|
+
await withRlsContext(
|
|
1001
|
+
this.db,
|
|
1002
|
+
scope,
|
|
1003
|
+
async (tx) => {
|
|
1004
|
+
const artifactRow = await loadArtifactRow(tx, scope, artifactId, false);
|
|
1005
|
+
if (!artifactRow) {
|
|
1006
|
+
throw new EditableArtifactPersistenceError(
|
|
1007
|
+
"not_found",
|
|
1008
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
const artifact = artifactFromRow(artifactRow);
|
|
1012
|
+
const checkpoint = await loadCheckpoint(tx, scope, artifactId, sequence);
|
|
1013
|
+
if (!checkpoint || checkpoint.modality !== artifact.modality || checkpoint.stateHash !== stateHash) {
|
|
1014
|
+
throw corrupt("Replica ACK differs from its durable checkpoint");
|
|
1015
|
+
}
|
|
1016
|
+
const frontierSql = checkpoint.modality === "spreadsheet" ? sql`${json(checkpoint.causalFrontier)}::jsonb` : sql`null`;
|
|
1017
|
+
const nativeRevision = checkpoint.modality === "spreadsheet" ? null : checkpoint.nativeRevision;
|
|
1018
|
+
const applied = await rawRows(
|
|
1019
|
+
tx,
|
|
1020
|
+
sql`
|
|
1021
|
+
insert into editable_artifact_replica_leases (
|
|
1022
|
+
account_id, workspace_id, artifact_id, modality, replica_id, actor_key,
|
|
1023
|
+
applied_head_sequence, causal_frontier, native_revision,
|
|
1024
|
+
lease_expires_at, last_seen_at, revoked_at
|
|
1025
|
+
) values (
|
|
1026
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, ${artifactId},
|
|
1027
|
+
${artifact.modality}, ${replicaId}, ${actorKey}, ${sequence},
|
|
1028
|
+
${frontierSql}, ${nativeRevision},
|
|
1029
|
+
clock_timestamp() + (${this.replicaLeaseTtlMs} * interval '1 millisecond'),
|
|
1030
|
+
clock_timestamp(), null
|
|
1031
|
+
)
|
|
1032
|
+
on conflict (account_id, workspace_id, artifact_id, replica_id)
|
|
1033
|
+
do update set
|
|
1034
|
+
applied_head_sequence = excluded.applied_head_sequence,
|
|
1035
|
+
causal_frontier = excluded.causal_frontier,
|
|
1036
|
+
native_revision = excluded.native_revision,
|
|
1037
|
+
lease_expires_at = excluded.lease_expires_at,
|
|
1038
|
+
last_seen_at = excluded.last_seen_at
|
|
1039
|
+
where editable_artifact_replica_leases.revoked_at is null
|
|
1040
|
+
and editable_artifact_replica_leases.actor_key = excluded.actor_key
|
|
1041
|
+
and editable_artifact_replica_leases.applied_head_sequence <=
|
|
1042
|
+
excluded.applied_head_sequence
|
|
1043
|
+
returning applied_head_sequence
|
|
1044
|
+
`
|
|
1045
|
+
);
|
|
1046
|
+
if (applied.length !== 1) {
|
|
1047
|
+
throw conflict("Replica ACK did not advance its durable lease");
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
{ isolationLevel: "read committed", accessMode: "read write" }
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
async readVerifiedSnapshotBytes(snapshot) {
|
|
1054
|
+
const bytes = await this.options.snapshotBytes.readSnapshotBytes(snapshot);
|
|
1055
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
1056
|
+
throw corrupt("Snapshot byte reader returned a non-binary value");
|
|
1057
|
+
}
|
|
1058
|
+
if (bytes.byteLength !== snapshot.byteSize) {
|
|
1059
|
+
throw corrupt("Snapshot bytes differ from durable byte size");
|
|
1060
|
+
}
|
|
1061
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
1062
|
+
if (digest !== snapshot.contentHash) {
|
|
1063
|
+
throw corrupt("Snapshot bytes differ from durable content hash");
|
|
1064
|
+
}
|
|
1065
|
+
const owned = bytes.slice();
|
|
1066
|
+
const common = {
|
|
1067
|
+
artifactId: snapshot.artifactId,
|
|
1068
|
+
sequence: snapshot.coveredHeadSequence,
|
|
1069
|
+
stateHash: snapshot.stateHash,
|
|
1070
|
+
digest: snapshot.contentHash,
|
|
1071
|
+
kernelVersion: snapshot.kernelVersion,
|
|
1072
|
+
modelSchemaVersion: snapshot.modelSchemaVersion,
|
|
1073
|
+
bytes: owned
|
|
1074
|
+
};
|
|
1075
|
+
return snapshot.modality === "spreadsheet" ? Object.freeze({
|
|
1076
|
+
...common,
|
|
1077
|
+
modality: snapshot.modality,
|
|
1078
|
+
causalFrontier: snapshot.coveredCausalFrontier,
|
|
1079
|
+
operationProtocolVersion: snapshot.operationProtocolVersion
|
|
1080
|
+
}) : Object.freeze({
|
|
1081
|
+
...common,
|
|
1082
|
+
modality: snapshot.modality,
|
|
1083
|
+
nativeRevision: snapshot.nativeRevision
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
var PostgresEditableArtifactUnitOfWork = class {
|
|
1088
|
+
constructor(tx, currentArtifact) {
|
|
1089
|
+
this.tx = tx;
|
|
1090
|
+
this.currentArtifact = currentArtifact;
|
|
1091
|
+
}
|
|
1092
|
+
committed = false;
|
|
1093
|
+
artifact() {
|
|
1094
|
+
return cloneArtifact(this.currentArtifact);
|
|
1095
|
+
}
|
|
1096
|
+
async findReceipt(actorKeyInput, clientTransactionIdInput) {
|
|
1097
|
+
const actorKey = validateActorKey(actorKeyInput).key;
|
|
1098
|
+
const clientTransactionId = validateClientTransactionId(clientTransactionIdInput);
|
|
1099
|
+
return await loadReceipt(
|
|
1100
|
+
this.tx,
|
|
1101
|
+
this.currentArtifact.scope,
|
|
1102
|
+
this.currentArtifact.id,
|
|
1103
|
+
actorKey,
|
|
1104
|
+
clientTransactionId
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
async findOperation(operationIdInput) {
|
|
1108
|
+
const operationId = validateStableId(operationIdInput, "operation id");
|
|
1109
|
+
const rows = await rawRows(
|
|
1110
|
+
this.tx,
|
|
1111
|
+
sql`select account_id, workspace_id, artifact_id, transaction_id,
|
|
1112
|
+
operation_id, operation_index, sequence, dot_replica_id, dot_counter,
|
|
1113
|
+
actor_key, created_at
|
|
1114
|
+
from editable_artifact_operations
|
|
1115
|
+
where account_id = ${this.currentArtifact.scope.accountId}::uuid
|
|
1116
|
+
and workspace_id = ${this.currentArtifact.scope.workspaceId}::uuid
|
|
1117
|
+
and artifact_id = ${this.currentArtifact.id}
|
|
1118
|
+
and operation_id = ${operationId}
|
|
1119
|
+
limit 1`
|
|
1120
|
+
);
|
|
1121
|
+
return rows[0] ? operationFromRow(rows[0]) : null;
|
|
1122
|
+
}
|
|
1123
|
+
async findUndoClaim(operationIdInput) {
|
|
1124
|
+
const operationId = validateStableId(operationIdInput, "operation id");
|
|
1125
|
+
const rows = await rawRows(
|
|
1126
|
+
this.tx,
|
|
1127
|
+
sql`
|
|
1128
|
+
select claiming_transaction_id
|
|
1129
|
+
from editable_artifact_undo_claims
|
|
1130
|
+
where account_id = ${this.currentArtifact.scope.accountId}::uuid
|
|
1131
|
+
and workspace_id = ${this.currentArtifact.scope.workspaceId}::uuid
|
|
1132
|
+
and artifact_id = ${this.currentArtifact.id}
|
|
1133
|
+
and target_operation_id = ${operationId}
|
|
1134
|
+
limit 1`
|
|
1135
|
+
);
|
|
1136
|
+
return rows[0] ? validateStableId(rows[0].claiming_transaction_id, "claiming transaction id") : null;
|
|
1137
|
+
}
|
|
1138
|
+
async kernelState() {
|
|
1139
|
+
const snapshot = this.currentArtifact.currentSnapshotId ? await this.findSnapshot(this.currentArtifact.currentSnapshotId) : null;
|
|
1140
|
+
if (this.currentArtifact.currentSnapshotId && !snapshot) {
|
|
1141
|
+
throw corrupt("Artifact current snapshot pointer has no immutable snapshot");
|
|
1142
|
+
}
|
|
1143
|
+
if (snapshot && snapshot.modality !== this.currentArtifact.modality) {
|
|
1144
|
+
throw corrupt("Artifact current snapshot modality differs from its durable artifact");
|
|
1145
|
+
}
|
|
1146
|
+
const coveredSequence = snapshot?.coveredHeadSequence ?? 0;
|
|
1147
|
+
const expectedSequenceSpan = this.currentArtifact.headSequence - coveredSequence;
|
|
1148
|
+
if (expectedSequenceSpan < 0) throw corrupt("Snapshot coverage is beyond artifact head");
|
|
1149
|
+
const [tailFacts] = await rawRows(
|
|
1150
|
+
this.tx,
|
|
1151
|
+
sql`select
|
|
1152
|
+
count(*) as transaction_count,
|
|
1153
|
+
coalesce(sum(operation_count), 0) as operation_count,
|
|
1154
|
+
coalesce(sum(committed_transaction_byte_size), 0) as byte_size
|
|
1155
|
+
from editable_artifact_transactions
|
|
1156
|
+
where account_id = ${this.currentArtifact.scope.accountId}::uuid
|
|
1157
|
+
and workspace_id = ${this.currentArtifact.scope.workspaceId}::uuid
|
|
1158
|
+
and artifact_id = ${this.currentArtifact.id}
|
|
1159
|
+
and sequence_start > ${coveredSequence}`
|
|
1160
|
+
);
|
|
1161
|
+
const storedTransactionCount = safeInteger(
|
|
1162
|
+
tailFacts?.transaction_count ?? 0,
|
|
1163
|
+
"stored committed transaction tail count"
|
|
1164
|
+
);
|
|
1165
|
+
const storedOperationCount = safeInteger(
|
|
1166
|
+
tailFacts?.operation_count ?? 0,
|
|
1167
|
+
"stored committed transaction operation count"
|
|
1168
|
+
);
|
|
1169
|
+
const storedByteSize = safeInteger(
|
|
1170
|
+
tailFacts?.byte_size ?? 0,
|
|
1171
|
+
"stored committed transaction tail bytes"
|
|
1172
|
+
);
|
|
1173
|
+
if (this.currentArtifact.modality === "spreadsheet" && storedOperationCount !== expectedSequenceSpan || this.currentArtifact.modality !== "spreadsheet" && (storedOperationCount !== 0 || storedTransactionCount !== expectedSequenceSpan)) {
|
|
1174
|
+
throw corrupt("Committed transaction tail does not match the authoritative head");
|
|
1175
|
+
}
|
|
1176
|
+
if (storedTransactionCount > EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_TRANSACTIONS) {
|
|
1177
|
+
throw new EditableArtifactPersistenceError(
|
|
1178
|
+
"history_limit",
|
|
1179
|
+
"Artifact committed transaction tail exceeds its replay count limit; publish a snapshot"
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
if (storedByteSize > EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_BYTES) {
|
|
1183
|
+
throw new EditableArtifactPersistenceError(
|
|
1184
|
+
"history_limit",
|
|
1185
|
+
"Artifact committed transaction tail exceeds its byte replay limit; publish a snapshot"
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const rows = await rawRows(
|
|
1189
|
+
this.tx,
|
|
1190
|
+
sql`select account_id, workspace_id, artifact_id, id, request_hash,
|
|
1191
|
+
modality, sequence_start, sequence_end, prior_state_hash, state_hash,
|
|
1192
|
+
replica_id, replica_counter, resolved_causal_base, resulting_causal_frontier,
|
|
1193
|
+
operation_count, operation_ids, operation_protocol_version, model_schema_version,
|
|
1194
|
+
commit_protocol_version, prior_native_revision, native_revision, command_count,
|
|
1195
|
+
native_receipt_byte_size, native_receipt_hash, native_receipt_bytes,
|
|
1196
|
+
kernel_version, committed_transaction_byte_size, committed_transaction_hash,
|
|
1197
|
+
committed_transaction_bytes, committed_at
|
|
1198
|
+
from editable_artifact_transactions
|
|
1199
|
+
where account_id = ${this.currentArtifact.scope.accountId}::uuid
|
|
1200
|
+
and workspace_id = ${this.currentArtifact.scope.workspaceId}::uuid
|
|
1201
|
+
and artifact_id = ${this.currentArtifact.id}
|
|
1202
|
+
and sequence_start > ${coveredSequence}
|
|
1203
|
+
order by sequence_start
|
|
1204
|
+
limit ${EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_TRANSACTIONS + 1}`
|
|
1205
|
+
);
|
|
1206
|
+
if (rows.length !== storedTransactionCount) {
|
|
1207
|
+
throw corrupt("Committed transaction tail changed during its repeatable read");
|
|
1208
|
+
}
|
|
1209
|
+
const committedTransactionTail = rows.map(
|
|
1210
|
+
(row) => committedTransactionFromRow(row, this.currentArtifact.modality)
|
|
1211
|
+
);
|
|
1212
|
+
let expectedSequence = coveredSequence + 1;
|
|
1213
|
+
let reconstructedBytes = 0;
|
|
1214
|
+
for (const transaction of committedTransactionTail) {
|
|
1215
|
+
if (transaction.sequenceStart !== expectedSequence) {
|
|
1216
|
+
throw corrupt("Committed transaction history contains a sequence gap");
|
|
1217
|
+
}
|
|
1218
|
+
expectedSequence = transaction.sequenceEnd + 1;
|
|
1219
|
+
reconstructedBytes += transaction.committedTransactionBytes.byteLength;
|
|
1220
|
+
}
|
|
1221
|
+
if (expectedSequence !== this.currentArtifact.headSequence + 1) {
|
|
1222
|
+
throw corrupt("Committed transaction history does not end at the artifact head");
|
|
1223
|
+
}
|
|
1224
|
+
if (reconstructedBytes !== storedByteSize) {
|
|
1225
|
+
throw corrupt("Committed transaction tail byte preflight disagrees with reconstruction");
|
|
1226
|
+
}
|
|
1227
|
+
if (this.currentArtifact.modality === "spreadsheet") {
|
|
1228
|
+
if (snapshot?.modality !== void 0 && snapshot.modality !== "spreadsheet") {
|
|
1229
|
+
throw corrupt("Spreadsheet artifact has a serialized snapshot");
|
|
1230
|
+
}
|
|
1231
|
+
if (committedTransactionTail.some((entry) => entry.modality !== "spreadsheet")) {
|
|
1232
|
+
throw corrupt("Spreadsheet artifact has a serialized transaction tail");
|
|
1233
|
+
}
|
|
1234
|
+
return Object.freeze({
|
|
1235
|
+
modality: "spreadsheet",
|
|
1236
|
+
artifact: cloneArtifact(this.currentArtifact),
|
|
1237
|
+
snapshot: snapshot ?? null,
|
|
1238
|
+
tailTransactionCount: storedTransactionCount,
|
|
1239
|
+
tailByteSize: storedByteSize,
|
|
1240
|
+
committedTransactionTail: Object.freeze(
|
|
1241
|
+
committedTransactionTail
|
|
1242
|
+
)
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
if (snapshot?.modality === "spreadsheet" || committedTransactionTail.some((entry) => entry.modality !== this.currentArtifact.modality)) {
|
|
1246
|
+
throw corrupt("Serialized artifact history contains a cross-modality record");
|
|
1247
|
+
}
|
|
1248
|
+
return Object.freeze({
|
|
1249
|
+
modality: this.currentArtifact.modality,
|
|
1250
|
+
artifact: cloneArtifact(this.currentArtifact),
|
|
1251
|
+
snapshot: snapshot ?? null,
|
|
1252
|
+
tailTransactionCount: storedTransactionCount,
|
|
1253
|
+
tailByteSize: storedByteSize,
|
|
1254
|
+
baseNativeRevision: snapshot?.nativeRevision ?? 0,
|
|
1255
|
+
committedTransactionTail: Object.freeze(
|
|
1256
|
+
committedTransactionTail
|
|
1257
|
+
)
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
async checkpoint(headSequenceInput) {
|
|
1261
|
+
const headSequence = validateInteger(
|
|
1262
|
+
headSequenceInput,
|
|
1263
|
+
"checkpoint sequence",
|
|
1264
|
+
0,
|
|
1265
|
+
Number.MAX_SAFE_INTEGER
|
|
1266
|
+
);
|
|
1267
|
+
const rows = await rawRows(
|
|
1268
|
+
this.tx,
|
|
1269
|
+
sql`
|
|
1270
|
+
select head_sequence, modality, causal_frontier, native_revision, state_hash
|
|
1271
|
+
from editable_artifact_sequence_checkpoints
|
|
1272
|
+
where account_id = ${this.currentArtifact.scope.accountId}::uuid
|
|
1273
|
+
and workspace_id = ${this.currentArtifact.scope.workspaceId}::uuid
|
|
1274
|
+
and artifact_id = ${this.currentArtifact.id}
|
|
1275
|
+
and head_sequence = ${headSequence}
|
|
1276
|
+
limit 1`
|
|
1277
|
+
);
|
|
1278
|
+
if (!rows[0]) return null;
|
|
1279
|
+
const row = rows[0];
|
|
1280
|
+
if (row.modality !== this.currentArtifact.modality) {
|
|
1281
|
+
throw corrupt("Checkpoint modality differs from its durable artifact");
|
|
1282
|
+
}
|
|
1283
|
+
const common = {
|
|
1284
|
+
modality: row.modality,
|
|
1285
|
+
headSequence: safeInteger(row.head_sequence, "checkpoint sequence"),
|
|
1286
|
+
stateHash: validateHash(row.state_hash, "checkpoint state hash")
|
|
1287
|
+
};
|
|
1288
|
+
return row.modality === "spreadsheet" ? Object.freeze({
|
|
1289
|
+
...common,
|
|
1290
|
+
modality: "spreadsheet",
|
|
1291
|
+
causalFrontier: parseFrontier(row.causal_frontier)
|
|
1292
|
+
}) : Object.freeze({
|
|
1293
|
+
...common,
|
|
1294
|
+
modality: serializedModality(row.modality, "checkpoint modality"),
|
|
1295
|
+
nativeRevision: safeInteger(row.native_revision, "checkpoint native revision")
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
async findSnapshot(snapshotIdInput) {
|
|
1299
|
+
const snapshotId = validateStableId(snapshotIdInput, "snapshot id");
|
|
1300
|
+
return await loadSnapshotMetadata(
|
|
1301
|
+
this.tx,
|
|
1302
|
+
this.currentArtifact.scope,
|
|
1303
|
+
this.currentArtifact.id,
|
|
1304
|
+
snapshotId
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
async commitAppliedTransaction(input) {
|
|
1308
|
+
this.assertUncommitted();
|
|
1309
|
+
const intentBytes = validateAppliedCommit(this.currentArtifact, input);
|
|
1310
|
+
const receipt = input.receipt;
|
|
1311
|
+
const committedTransaction = input.committedTransaction;
|
|
1312
|
+
if (committedTransaction.modality !== receipt.modality) {
|
|
1313
|
+
throw new TypeError("Committed transaction modality differs from its receipt");
|
|
1314
|
+
}
|
|
1315
|
+
const authority = validateActorKey(receipt.actorKey);
|
|
1316
|
+
const replicaId = receipt.replicaId;
|
|
1317
|
+
if (receipt.modality === "spreadsheet" && input.operations.some((operation) => operation.dot.replicaId !== replicaId)) {
|
|
1318
|
+
throw new TypeError("One transaction may advance exactly one replica namespace");
|
|
1319
|
+
}
|
|
1320
|
+
if (receipt.previousLocalTransactionId === null) {
|
|
1321
|
+
if (receipt.replicaCounter !== 1) {
|
|
1322
|
+
throw new TypeError("A replica's first transaction must reserve counter one");
|
|
1323
|
+
}
|
|
1324
|
+
if (receipt.modality === "spreadsheet" && !sameFrontier(receipt.resolvedCausalBase, receipt.causalBase)) {
|
|
1325
|
+
throw new TypeError("First replica transaction must not invent a resolved predecessor");
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
const predecessor = await this.findReceipt(
|
|
1329
|
+
receipt.actorKey,
|
|
1330
|
+
receipt.previousLocalTransactionId
|
|
1331
|
+
);
|
|
1332
|
+
if (!predecessor || predecessor.replicaId !== receipt.replicaId || predecessor.replicaCounter + 1 !== receipt.replicaCounter) {
|
|
1333
|
+
throw conflict("Previous local transaction does not match the replica counter chain");
|
|
1334
|
+
}
|
|
1335
|
+
if (predecessor.modality !== receipt.modality) {
|
|
1336
|
+
throw conflict("Previous local transaction belongs to another artifact modality");
|
|
1337
|
+
}
|
|
1338
|
+
if (receipt.modality === "spreadsheet") {
|
|
1339
|
+
if (predecessor.modality !== "spreadsheet") {
|
|
1340
|
+
throw conflict("Spreadsheet predecessor belongs to another modality");
|
|
1341
|
+
}
|
|
1342
|
+
const expectedResolvedBase = mergeFrontiers(
|
|
1343
|
+
receipt.causalBase,
|
|
1344
|
+
predecessor.resolvedCausalBase,
|
|
1345
|
+
[
|
|
1346
|
+
{
|
|
1347
|
+
replicaId: predecessor.replicaId,
|
|
1348
|
+
counter: predecessor.replicaCounter
|
|
1349
|
+
}
|
|
1350
|
+
]
|
|
1351
|
+
);
|
|
1352
|
+
if (!sameFrontier(receipt.resolvedCausalBase, expectedResolvedBase)) {
|
|
1353
|
+
throw new TypeError("Resolved causal base does not exactly include its predecessor dot");
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
validateTransactionOutbox(input.outbox, receipt);
|
|
1358
|
+
const causalBase = receipt.modality === "spreadsheet" ? parseFrontier(receipt.causalBase) : null;
|
|
1359
|
+
const resolvedCausalBase = receipt.modality === "spreadsheet" ? parseFrontier(receipt.resolvedCausalBase) : null;
|
|
1360
|
+
const resultingCausalFrontier = receipt.modality === "spreadsheet" ? parseFrontier(receipt.resultingCausalFrontier) : null;
|
|
1361
|
+
const selectiveUndoOperationIds = receipt.modality === "spreadsheet" ? parseStableIdArray(receipt.selectiveUndoOperationIds, "undo targets", 1e4) : null;
|
|
1362
|
+
const causalBaseSql = causalBase === null ? sql`null` : sql`${json(causalBase)}::jsonb`;
|
|
1363
|
+
const resolvedCausalBaseSql = resolvedCausalBase === null ? sql`null` : sql`${json(resolvedCausalBase)}::jsonb`;
|
|
1364
|
+
const resultingCausalFrontierSql = resultingCausalFrontier === null ? sql`null` : sql`${json(resultingCausalFrontier)}::jsonb`;
|
|
1365
|
+
const operationIdsSql = committedTransaction.modality === "spreadsheet" ? sql`${json(committedTransaction.operationIds)}::jsonb` : sql`null`;
|
|
1366
|
+
const selectiveUndoSql = selectiveUndoOperationIds === null ? sql`null` : sql`${json(selectiveUndoOperationIds)}::jsonb`;
|
|
1367
|
+
let persistedUpdatedAt = receipt.committedAt;
|
|
1368
|
+
try {
|
|
1369
|
+
await this.tx.execute(sql`
|
|
1370
|
+
insert into editable_artifact_transactions (
|
|
1371
|
+
account_id, workspace_id, artifact_id, id, modality, parent_head_sequence,
|
|
1372
|
+
client_transaction_id, request_hash, sequence_start, sequence_end,
|
|
1373
|
+
prior_state_hash,
|
|
1374
|
+
intent_hash, intent_byte_size, intent_bytes,
|
|
1375
|
+
replica_counter, previous_local_transaction_id,
|
|
1376
|
+
causal_base, resolved_causal_base, resulting_causal_frontier,
|
|
1377
|
+
state_hash, operation_count, operation_ids,
|
|
1378
|
+
selective_undo_targets, intent_envelope_version, intent_protocol_version,
|
|
1379
|
+
command_protocol_version,
|
|
1380
|
+
actor_kind, actor_subject_id, actor_key,
|
|
1381
|
+
replica_id, agent_session_id, agent_turn_id, agent_attempt_id,
|
|
1382
|
+
agent_generation, service_name, kernel_version, model_schema_version,
|
|
1383
|
+
operation_protocol_version, commit_protocol_version,
|
|
1384
|
+
prior_native_revision, native_revision, command_count,
|
|
1385
|
+
native_receipt_byte_size, native_receipt_hash, native_receipt_bytes,
|
|
1386
|
+
committed_transaction_byte_size,
|
|
1387
|
+
committed_transaction_hash, committed_transaction_bytes, committed_at
|
|
1388
|
+
) values (
|
|
1389
|
+
${receipt.scope.accountId}::uuid, ${receipt.scope.workspaceId}::uuid,
|
|
1390
|
+
${receipt.artifactId}, ${input.serverTransactionId}, ${receipt.modality},
|
|
1391
|
+
${input.expectedHeadSequence},
|
|
1392
|
+
${receipt.clientTransactionId}, ${receipt.requestHash}, ${receipt.sequenceStart},
|
|
1393
|
+
${receipt.sequenceEnd}, ${receipt.priorStateHash}, ${receipt.requestHash},
|
|
1394
|
+
${intentBytes.byteLength},
|
|
1395
|
+
${intentBytes}, ${receipt.replicaCounter},
|
|
1396
|
+
${receipt.previousLocalTransactionId}, ${causalBaseSql},
|
|
1397
|
+
${resolvedCausalBaseSql},
|
|
1398
|
+
${resultingCausalFrontierSql}, ${receipt.stateHash},
|
|
1399
|
+
${receipt.modality === "spreadsheet" ? receipt.operationCount : null},
|
|
1400
|
+
${operationIdsSql}, ${selectiveUndoSql},
|
|
1401
|
+
${receipt.intentEnvelopeVersion}, ${receipt.intentProtocolVersion},
|
|
1402
|
+
${receipt.commandProtocolVersion},
|
|
1403
|
+
${authority.kind}, ${authority.subjectId}, ${authority.key}, ${replicaId},
|
|
1404
|
+
${authority.kind === "agent" ? authority.sessionId : null},
|
|
1405
|
+
${authority.kind === "agent" ? authority.turnId : null},
|
|
1406
|
+
${authority.kind === "agent" ? authority.attemptId : null},
|
|
1407
|
+
${authority.kind === "agent" ? authority.generation : null},
|
|
1408
|
+
${authority.kind === "service" ? authority.service : null},
|
|
1409
|
+
${receipt.kernelVersion}, ${receipt.modelSchemaVersion},
|
|
1410
|
+
${receipt.modality === "spreadsheet" ? receipt.operationProtocolVersion : null},
|
|
1411
|
+
${receipt.modality === "spreadsheet" ? null : receipt.commitProtocolVersion},
|
|
1412
|
+
${receipt.modality === "spreadsheet" ? null : receipt.priorNativeRevision},
|
|
1413
|
+
${receipt.modality === "spreadsheet" ? null : receipt.nativeRevision},
|
|
1414
|
+
${receipt.modality === "spreadsheet" ? null : receipt.commandCount},
|
|
1415
|
+
${committedTransaction.modality === "spreadsheet" ? null : committedTransaction.nativeReceiptBytes.byteLength},
|
|
1416
|
+
${committedTransaction.modality === "spreadsheet" ? null : `sha256:${createHash("sha256").update(committedTransaction.nativeReceiptBytes).digest("hex")}`},
|
|
1417
|
+
${committedTransaction.modality === "spreadsheet" ? null : committedTransaction.nativeReceiptBytes},
|
|
1418
|
+
${committedTransaction.committedTransactionBytes.byteLength},
|
|
1419
|
+
${`sha256:${createHash("sha256").update(committedTransaction.committedTransactionBytes).digest("hex")}`},
|
|
1420
|
+
${committedTransaction.committedTransactionBytes}, ${receipt.committedAt}
|
|
1421
|
+
)`);
|
|
1422
|
+
await this.tx.execute(sql`
|
|
1423
|
+
insert into editable_artifact_idempotency_receipts (
|
|
1424
|
+
account_id, workspace_id, id, artifact_id, operation_kind,
|
|
1425
|
+
authority_key, idempotency_key, request_hash, resource_type,
|
|
1426
|
+
resource_id, server_transaction_id, result, created_at
|
|
1427
|
+
) values (
|
|
1428
|
+
${receipt.scope.accountId}::uuid, ${receipt.scope.workspaceId}::uuid,
|
|
1429
|
+
${receipt.receiptId}, ${receipt.artifactId}, 'edit', ${receipt.actorKey},
|
|
1430
|
+
${receipt.clientTransactionId}, ${receipt.requestHash}, 'transaction',
|
|
1431
|
+
${receipt.serverTransactionId}, ${receipt.serverTransactionId},
|
|
1432
|
+
'{"schemaVersion":1}'::jsonb,
|
|
1433
|
+
${receipt.committedAt}
|
|
1434
|
+
)`);
|
|
1435
|
+
if (receipt.modality === "spreadsheet") {
|
|
1436
|
+
await this.insertOperations(input.operations);
|
|
1437
|
+
await this.insertUndoClaims(receipt);
|
|
1438
|
+
}
|
|
1439
|
+
await this.tx.execute(sql`
|
|
1440
|
+
insert into editable_artifact_sequence_checkpoints (
|
|
1441
|
+
account_id, workspace_id, artifact_id, head_sequence,
|
|
1442
|
+
modality, causal_frontier, native_revision, state_hash,
|
|
1443
|
+
transaction_id, created_at
|
|
1444
|
+
) values (
|
|
1445
|
+
${receipt.scope.accountId}::uuid, ${receipt.scope.workspaceId}::uuid,
|
|
1446
|
+
${receipt.artifactId}, ${receipt.sequenceEnd}, ${receipt.modality},
|
|
1447
|
+
${resultingCausalFrontierSql},
|
|
1448
|
+
${receipt.modality === "spreadsheet" ? null : receipt.nativeRevision},
|
|
1449
|
+
${receipt.stateHash},
|
|
1450
|
+
${receipt.serverTransactionId}, ${receipt.committedAt}
|
|
1451
|
+
)`);
|
|
1452
|
+
await this.insertOutbox(input.outbox);
|
|
1453
|
+
const updated = await rawRows(
|
|
1454
|
+
this.tx,
|
|
1455
|
+
sql`
|
|
1456
|
+
update editable_artifacts
|
|
1457
|
+
set head_sequence = ${receipt.sequenceEnd},
|
|
1458
|
+
causal_frontier = ${resultingCausalFrontierSql},
|
|
1459
|
+
state_hash = ${receipt.stateHash},
|
|
1460
|
+
updated_at = greatest(updated_at, ${receipt.committedAt}::timestamptz)
|
|
1461
|
+
where account_id = ${receipt.scope.accountId}::uuid
|
|
1462
|
+
and workspace_id = ${receipt.scope.workspaceId}::uuid
|
|
1463
|
+
and id = ${receipt.artifactId}
|
|
1464
|
+
and head_sequence = ${input.expectedHeadSequence}
|
|
1465
|
+
returning id, updated_at`
|
|
1466
|
+
);
|
|
1467
|
+
if (updated.length !== 1) throw conflict("Artifact head changed during locked commit");
|
|
1468
|
+
persistedUpdatedAt = iso(updated[0].updated_at, "artifact update time");
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
throw mapPersistenceError(error, "Editable artifact transaction commit failed");
|
|
1471
|
+
}
|
|
1472
|
+
this.currentArtifact = receipt.modality === "spreadsheet" && this.currentArtifact.modality === "spreadsheet" ? Object.freeze({
|
|
1473
|
+
...this.currentArtifact,
|
|
1474
|
+
headSequence: receipt.sequenceEnd,
|
|
1475
|
+
causalFrontier: cloneFrontier(receipt.resultingCausalFrontier),
|
|
1476
|
+
stateHash: receipt.stateHash,
|
|
1477
|
+
updatedAt: persistedUpdatedAt
|
|
1478
|
+
}) : Object.freeze({
|
|
1479
|
+
...this.currentArtifact,
|
|
1480
|
+
headSequence: receipt.sequenceEnd,
|
|
1481
|
+
stateHash: receipt.stateHash,
|
|
1482
|
+
updatedAt: persistedUpdatedAt
|
|
1483
|
+
});
|
|
1484
|
+
this.committed = true;
|
|
1485
|
+
}
|
|
1486
|
+
async commitSnapshot(input) {
|
|
1487
|
+
this.assertUncommitted();
|
|
1488
|
+
const candidate = ownSnapshotCommit(input);
|
|
1489
|
+
validateSnapshotCommit(this.currentArtifact, candidate);
|
|
1490
|
+
const snapshot = candidate.snapshot;
|
|
1491
|
+
const checkpoint = await this.checkpoint(snapshot.coveredHeadSequence);
|
|
1492
|
+
if (!checkpoint || checkpoint.modality !== snapshot.modality || checkpoint.stateHash !== snapshot.stateHash || (checkpoint.modality === "spreadsheet" && snapshot.modality === "spreadsheet" ? !sameFrontier(checkpoint.causalFrontier, snapshot.coveredCausalFrontier) : checkpoint.modality !== "spreadsheet" && snapshot.modality !== "spreadsheet" ? checkpoint.nativeRevision !== snapshot.nativeRevision : true)) {
|
|
1493
|
+
throw conflict("Snapshot does not match an authoritative transaction boundary");
|
|
1494
|
+
}
|
|
1495
|
+
if (this.currentArtifact.currentSnapshotId) {
|
|
1496
|
+
const current = await this.findSnapshot(this.currentArtifact.currentSnapshotId);
|
|
1497
|
+
if (!current) throw corrupt("Current snapshot pointer has no immutable metadata");
|
|
1498
|
+
if (current.modality !== snapshot.modality) {
|
|
1499
|
+
throw corrupt("Current and candidate snapshots have different modalities");
|
|
1500
|
+
}
|
|
1501
|
+
if (snapshot.coveredHeadSequence <= current.coveredHeadSequence) {
|
|
1502
|
+
throw conflict("Snapshot publication must advance authoritative coverage");
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
validateSnapshotOutbox(candidate.outbox, snapshot);
|
|
1506
|
+
const authorization = await transactionallyAuthorizeEditableArtifactActor(this.tx, {
|
|
1507
|
+
scope: this.currentArtifact.scope,
|
|
1508
|
+
artifactId: this.currentArtifact.id,
|
|
1509
|
+
actor: candidate.authorizationActor,
|
|
1510
|
+
permission: candidate.authorizationPermission ?? "manage"
|
|
1511
|
+
});
|
|
1512
|
+
if (!authorization.allowed || authorization.revision !== candidate.expectedAuthorizationRevision) {
|
|
1513
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
1514
|
+
}
|
|
1515
|
+
let persistedUpdatedAt = snapshot.publishedAt;
|
|
1516
|
+
try {
|
|
1517
|
+
const blobRefId = await this.ensureSnapshotBlobReference(snapshot);
|
|
1518
|
+
await insertSnapshotMetadata(this.tx, snapshot, blobRefId);
|
|
1519
|
+
await this.insertOutbox(candidate.outbox);
|
|
1520
|
+
const expectedSnapshot = candidate.expectedCurrentSnapshotId;
|
|
1521
|
+
const updated = await rawRows(
|
|
1522
|
+
this.tx,
|
|
1523
|
+
sql`
|
|
1524
|
+
update editable_artifacts
|
|
1525
|
+
set current_snapshot_id = ${snapshot.snapshotId},
|
|
1526
|
+
updated_at = greatest(updated_at, ${snapshot.publishedAt}::timestamptz)
|
|
1527
|
+
where account_id = ${snapshot.scope.accountId}::uuid
|
|
1528
|
+
and workspace_id = ${snapshot.scope.workspaceId}::uuid
|
|
1529
|
+
and id = ${snapshot.artifactId}
|
|
1530
|
+
and authorization_revision = ${candidate.expectedAuthorizationRevision}
|
|
1531
|
+
and ${expectedSnapshot === null ? sql`current_snapshot_id is null` : sql`current_snapshot_id = ${expectedSnapshot}`}
|
|
1532
|
+
returning id, updated_at`
|
|
1533
|
+
);
|
|
1534
|
+
if (updated.length !== 1) throw conflict("Current snapshot changed during locked commit");
|
|
1535
|
+
persistedUpdatedAt = iso(updated[0].updated_at, "artifact update time");
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
throw mapPersistenceError(error, "Editable artifact snapshot commit failed");
|
|
1538
|
+
}
|
|
1539
|
+
this.currentArtifact = Object.freeze({
|
|
1540
|
+
...this.currentArtifact,
|
|
1541
|
+
currentSnapshotId: snapshot.snapshotId,
|
|
1542
|
+
updatedAt: persistedUpdatedAt
|
|
1543
|
+
});
|
|
1544
|
+
this.committed = true;
|
|
1545
|
+
return Object.freeze({ kind: "committed" });
|
|
1546
|
+
}
|
|
1547
|
+
async insertOperations(operations) {
|
|
1548
|
+
for (let offset = 0; offset < operations.length; offset += 1e3) {
|
|
1549
|
+
const values = sql.join(
|
|
1550
|
+
operations.slice(offset, offset + 1e3).map((operation, chunkIndex) => {
|
|
1551
|
+
const operationIndex = offset + chunkIndex;
|
|
1552
|
+
return sql`(
|
|
1553
|
+
${operation.scope.accountId}::uuid, ${operation.scope.workspaceId}::uuid,
|
|
1554
|
+
${operation.artifactId}, ${operation.serverTransactionId},
|
|
1555
|
+
${operation.operationId}, ${operationIndex}, ${operation.sequence},
|
|
1556
|
+
${operation.dot.replicaId}, ${operation.dot.counter},
|
|
1557
|
+
${operation.actorKey}, ${operation.createdAt}
|
|
1558
|
+
)`;
|
|
1559
|
+
}),
|
|
1560
|
+
sql`, `
|
|
1561
|
+
);
|
|
1562
|
+
await this.tx.execute(sql`
|
|
1563
|
+
insert into editable_artifact_operations (
|
|
1564
|
+
account_id, workspace_id, artifact_id, transaction_id,
|
|
1565
|
+
operation_id, operation_index, sequence, dot_replica_id,
|
|
1566
|
+
dot_counter, actor_key, created_at
|
|
1567
|
+
) values ${values}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async insertUndoClaims(receipt) {
|
|
1571
|
+
for (let offset = 0; offset < receipt.selectiveUndoOperationIds.length; offset += 2e3) {
|
|
1572
|
+
const values = sql.join(
|
|
1573
|
+
receipt.selectiveUndoOperationIds.slice(offset, offset + 2e3).map(
|
|
1574
|
+
(target) => sql`(
|
|
1575
|
+
${receipt.scope.accountId}::uuid, ${receipt.scope.workspaceId}::uuid,
|
|
1576
|
+
${receipt.artifactId}, ${target}, ${receipt.serverTransactionId},
|
|
1577
|
+
${receipt.committedAt}
|
|
1578
|
+
)`
|
|
1579
|
+
),
|
|
1580
|
+
sql`, `
|
|
1581
|
+
);
|
|
1582
|
+
await this.tx.execute(sql`
|
|
1583
|
+
insert into editable_artifact_undo_claims (
|
|
1584
|
+
account_id, workspace_id, artifact_id, target_operation_id,
|
|
1585
|
+
claiming_transaction_id, created_at
|
|
1586
|
+
) values ${values}`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
async insertOutbox(record) {
|
|
1590
|
+
await insertLiveOutbox(this.tx, this.currentArtifact, record);
|
|
1591
|
+
}
|
|
1592
|
+
async ensureSnapshotBlobReference(snapshot) {
|
|
1593
|
+
const rows = await rawRows(
|
|
1594
|
+
this.tx,
|
|
1595
|
+
sql`
|
|
1596
|
+
select id, object_reference, byte_size, mime_type
|
|
1597
|
+
from editable_artifact_blob_refs
|
|
1598
|
+
where account_id = ${snapshot.scope.accountId}::uuid
|
|
1599
|
+
and workspace_id = ${snapshot.scope.workspaceId}::uuid
|
|
1600
|
+
and artifact_id = ${snapshot.artifactId}
|
|
1601
|
+
and kind = 'snapshot'
|
|
1602
|
+
and content_hash = ${snapshot.contentHash}
|
|
1603
|
+
limit 1`
|
|
1604
|
+
);
|
|
1605
|
+
const existing = rows[0];
|
|
1606
|
+
if (existing) {
|
|
1607
|
+
if (existing.object_reference !== snapshot.blobReference || safeInteger(existing.byte_size, "snapshot blob byte size", 1) !== snapshot.byteSize || existing.mime_type !== snapshot.mimeType) {
|
|
1608
|
+
throw conflict("Snapshot content hash is already bound to different blob facts");
|
|
1609
|
+
}
|
|
1610
|
+
return validateStableId(existing.id, "snapshot blob reference id");
|
|
1611
|
+
}
|
|
1612
|
+
await this.tx.execute(sql`
|
|
1613
|
+
insert into editable_artifact_blob_refs (
|
|
1614
|
+
account_id, workspace_id, artifact_id, id, kind, object_reference,
|
|
1615
|
+
byte_size, content_hash, mime_type, created_at
|
|
1616
|
+
) values (
|
|
1617
|
+
${snapshot.scope.accountId}::uuid, ${snapshot.scope.workspaceId}::uuid,
|
|
1618
|
+
${snapshot.artifactId}, ${snapshot.snapshotId}, 'snapshot',
|
|
1619
|
+
${snapshot.blobReference}, ${snapshot.byteSize}, ${snapshot.contentHash},
|
|
1620
|
+
${snapshot.mimeType}, ${snapshot.publishedAt}
|
|
1621
|
+
)`);
|
|
1622
|
+
return snapshot.snapshotId;
|
|
1623
|
+
}
|
|
1624
|
+
assertUncommitted() {
|
|
1625
|
+
if (this.committed) throw conflict("Artifact unit of work may commit exactly once");
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
async function loadArtifactRow(db, scope, artifactId, forUpdate) {
|
|
1629
|
+
const rows = await rawRows(
|
|
1630
|
+
db,
|
|
1631
|
+
sql`
|
|
1632
|
+
select account_id, workspace_id, id, modality, title, lifecycle_state,
|
|
1633
|
+
authorization_revision, head_sequence, causal_frontier, state_hash, current_snapshot_id,
|
|
1634
|
+
created_at, updated_at
|
|
1635
|
+
from editable_artifacts
|
|
1636
|
+
where account_id = ${scope.accountId}::uuid
|
|
1637
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
1638
|
+
and id = ${artifactId}
|
|
1639
|
+
${forUpdate ? sql`for update` : sql``}
|
|
1640
|
+
limit 1`
|
|
1641
|
+
);
|
|
1642
|
+
return rows[0] ?? null;
|
|
1643
|
+
}
|
|
1644
|
+
async function loadSnapshotMetadata(db, scope, artifactId, snapshotId) {
|
|
1645
|
+
const rows = await rawRows(
|
|
1646
|
+
db,
|
|
1647
|
+
sql`
|
|
1648
|
+
select s.account_id, s.workspace_id, s.artifact_id, s.id,
|
|
1649
|
+
s.modality,
|
|
1650
|
+
b.object_reference as blob_reference, b.byte_size as blob_byte_size,
|
|
1651
|
+
b.content_hash as blob_content_hash, b.mime_type as blob_mime_type,
|
|
1652
|
+
s.byte_size, s.content_hash, s.mime_type, s.covered_head_sequence,
|
|
1653
|
+
s.covered_causal_frontier, s.state_hash, s.model_schema_version,
|
|
1654
|
+
s.operation_protocol_version, s.kernel_version, s.crdt_state_version,
|
|
1655
|
+
s.native_revision,
|
|
1656
|
+
s.verified_at, s.published_at
|
|
1657
|
+
from editable_artifact_snapshots s
|
|
1658
|
+
join editable_artifact_blob_refs b
|
|
1659
|
+
on b.account_id = s.account_id
|
|
1660
|
+
and b.workspace_id = s.workspace_id
|
|
1661
|
+
and b.artifact_id = s.artifact_id
|
|
1662
|
+
and b.id = s.blob_ref_id
|
|
1663
|
+
where s.account_id = ${scope.accountId}::uuid
|
|
1664
|
+
and s.workspace_id = ${scope.workspaceId}::uuid
|
|
1665
|
+
and s.artifact_id = ${artifactId}
|
|
1666
|
+
and s.id = ${snapshotId}
|
|
1667
|
+
limit 1`
|
|
1668
|
+
);
|
|
1669
|
+
return rows[0] ? snapshotFromRow(rows[0]) : null;
|
|
1670
|
+
}
|
|
1671
|
+
function validateLiveResume(input) {
|
|
1672
|
+
if (!isPlainRecord(input)) throw new TypeError("Editable artifact live resume is invalid");
|
|
1673
|
+
const modality = input.modality ?? "spreadsheet";
|
|
1674
|
+
if (modality !== "spreadsheet" && modality !== "document" && modality !== "presentation") {
|
|
1675
|
+
throw new TypeError("Editable artifact live resume modality is invalid");
|
|
1676
|
+
}
|
|
1677
|
+
const localCursor = input.localCursor === null ? null : validateInteger(input.localCursor, "live resume cursor", 0);
|
|
1678
|
+
const localStateHash = input.localStateHash === null ? null : validateHash(input.localStateHash, "live resume state hash");
|
|
1679
|
+
if (localCursor === null !== (localStateHash === null)) {
|
|
1680
|
+
throw new TypeError("Live resume cursor and state hash must both be null or present");
|
|
1681
|
+
}
|
|
1682
|
+
if (typeof input.requireSnapshot !== "boolean") {
|
|
1683
|
+
throw new TypeError("Live resume snapshot requirement must be boolean");
|
|
1684
|
+
}
|
|
1685
|
+
if (modality === "spreadsheet") {
|
|
1686
|
+
if (!("localCausalFrontier" in input)) {
|
|
1687
|
+
throw new TypeError("Spreadsheet live resume requires a causal frontier");
|
|
1688
|
+
}
|
|
1689
|
+
return Object.freeze({
|
|
1690
|
+
modality,
|
|
1691
|
+
localCursor,
|
|
1692
|
+
localStateHash,
|
|
1693
|
+
localCausalFrontier: parseFrontier(input.localCausalFrontier),
|
|
1694
|
+
requireSnapshot: input.requireSnapshot
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
if (!("localNativeRevision" in input)) {
|
|
1698
|
+
throw new TypeError("Serialized live resume requires a native revision");
|
|
1699
|
+
}
|
|
1700
|
+
const localNativeRevision = input.localNativeRevision === null ? null : validateInteger(input.localNativeRevision, "live resume native revision", 0);
|
|
1701
|
+
if (localCursor === null !== (localNativeRevision === null)) {
|
|
1702
|
+
throw new TypeError("Live resume cursor and native revision must both be null or present");
|
|
1703
|
+
}
|
|
1704
|
+
return Object.freeze({
|
|
1705
|
+
modality,
|
|
1706
|
+
localCursor,
|
|
1707
|
+
localStateHash,
|
|
1708
|
+
localNativeRevision,
|
|
1709
|
+
requireSnapshot: input.requireSnapshot
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
async function loadCheckpoint(db, scope, artifactId, headSequence) {
|
|
1713
|
+
const rows = await rawRows(
|
|
1714
|
+
db,
|
|
1715
|
+
sql`
|
|
1716
|
+
select head_sequence, modality, causal_frontier, native_revision, state_hash
|
|
1717
|
+
from editable_artifact_sequence_checkpoints
|
|
1718
|
+
where account_id = ${scope.accountId}::uuid
|
|
1719
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
1720
|
+
and artifact_id = ${artifactId}
|
|
1721
|
+
and head_sequence = ${headSequence}
|
|
1722
|
+
limit 1`
|
|
1723
|
+
);
|
|
1724
|
+
const row = rows[0];
|
|
1725
|
+
if (!row) return null;
|
|
1726
|
+
const modality = artifactModality(row.modality, "checkpoint modality");
|
|
1727
|
+
const common = {
|
|
1728
|
+
headSequence: safeInteger(row.head_sequence, "checkpoint sequence"),
|
|
1729
|
+
stateHash: validateHash(row.state_hash, "checkpoint state hash")
|
|
1730
|
+
};
|
|
1731
|
+
if (modality === "spreadsheet") {
|
|
1732
|
+
if (row.native_revision !== null) {
|
|
1733
|
+
throw corrupt("Spreadsheet checkpoint contains a native revision");
|
|
1734
|
+
}
|
|
1735
|
+
return Object.freeze({
|
|
1736
|
+
...common,
|
|
1737
|
+
modality,
|
|
1738
|
+
causalFrontier: parseFrontier(row.causal_frontier)
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
if (row.causal_frontier !== null) {
|
|
1742
|
+
throw corrupt("Serialized checkpoint contains a causal frontier");
|
|
1743
|
+
}
|
|
1744
|
+
return Object.freeze({
|
|
1745
|
+
...common,
|
|
1746
|
+
modality,
|
|
1747
|
+
nativeRevision: safeInteger(row.native_revision, "checkpoint native revision")
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
async function loadLiveAuthority(db, scope, artifactId) {
|
|
1751
|
+
const artifactRow = await loadArtifactRow(db, scope, artifactId, false);
|
|
1752
|
+
if (!artifactRow) {
|
|
1753
|
+
throw new EditableArtifactPersistenceError(
|
|
1754
|
+
"not_found",
|
|
1755
|
+
"Editable artifact was not found in the requested tenant scope"
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
const artifact = artifactFromRow(artifactRow);
|
|
1759
|
+
if (artifact.currentSnapshotId === null) {
|
|
1760
|
+
throw corrupt("Live artifact has no authoritative snapshot");
|
|
1761
|
+
}
|
|
1762
|
+
const [checkpoint, snapshot] = await Promise.all([
|
|
1763
|
+
loadCheckpoint(db, scope, artifactId, artifact.headSequence),
|
|
1764
|
+
loadSnapshotMetadata(db, scope, artifactId, artifact.currentSnapshotId)
|
|
1765
|
+
]);
|
|
1766
|
+
if (!checkpoint) throw corrupt("Artifact head has no durable checkpoint");
|
|
1767
|
+
if (!snapshot) throw corrupt("Artifact snapshot pointer has no immutable snapshot");
|
|
1768
|
+
if (checkpoint.modality !== artifact.modality || checkpoint.headSequence !== artifact.headSequence || checkpoint.stateHash !== artifact.stateHash || snapshot.modality !== artifact.modality) {
|
|
1769
|
+
throw corrupt("Live authority projections disagree with the durable artifact");
|
|
1770
|
+
}
|
|
1771
|
+
if (artifact.modality === "spreadsheet" && (checkpoint.modality !== "spreadsheet" || snapshot.modality !== "spreadsheet" || !sameFrontier(checkpoint.causalFrontier, artifact.causalFrontier))) {
|
|
1772
|
+
throw corrupt("Spreadsheet live head differs from its durable causal authority");
|
|
1773
|
+
}
|
|
1774
|
+
const snapshotCheckpoint = snapshot.coveredHeadSequence === checkpoint.headSequence ? checkpoint : await loadCheckpoint(db, scope, artifactId, snapshot.coveredHeadSequence);
|
|
1775
|
+
if (!snapshotCheckpoint || snapshotCheckpoint.modality !== snapshot.modality || snapshotCheckpoint.headSequence !== snapshot.coveredHeadSequence || snapshotCheckpoint.stateHash !== snapshot.stateHash || (snapshot.modality === "spreadsheet" ? snapshotCheckpoint.modality !== "spreadsheet" || !sameFrontier(snapshotCheckpoint.causalFrontier, snapshot.coveredCausalFrontier) : snapshotCheckpoint.modality === "spreadsheet" || snapshotCheckpoint.nativeRevision !== snapshot.nativeRevision)) {
|
|
1776
|
+
throw corrupt("Current snapshot differs from its durable checkpoint");
|
|
1777
|
+
}
|
|
1778
|
+
const minimumReplaySequence = snapshot.coveredHeadSequence + 1;
|
|
1779
|
+
if (minimumReplaySequence > artifact.headSequence + 1) {
|
|
1780
|
+
throw corrupt("Current snapshot coverage is beyond the durable head");
|
|
1781
|
+
}
|
|
1782
|
+
return Object.freeze({ artifact, checkpoint, snapshot, minimumReplaySequence });
|
|
1783
|
+
}
|
|
1784
|
+
function liveHeadFromAuthority(authority) {
|
|
1785
|
+
const common = {
|
|
1786
|
+
headSequence: authority.artifact.headSequence,
|
|
1787
|
+
stateHash: authority.artifact.stateHash,
|
|
1788
|
+
minimumReplaySequence: authority.minimumReplaySequence
|
|
1789
|
+
};
|
|
1790
|
+
return authority.checkpoint.modality === "spreadsheet" ? Object.freeze({
|
|
1791
|
+
...common,
|
|
1792
|
+
modality: authority.checkpoint.modality,
|
|
1793
|
+
causalFrontier: authority.checkpoint.causalFrontier
|
|
1794
|
+
}) : Object.freeze({
|
|
1795
|
+
...common,
|
|
1796
|
+
modality: authority.checkpoint.modality,
|
|
1797
|
+
nativeRevision: authority.checkpoint.nativeRevision
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
var committedTransactionColumns = sql`
|
|
1801
|
+
account_id, workspace_id, artifact_id, id, request_hash,
|
|
1802
|
+
modality, sequence_start, sequence_end, prior_state_hash, state_hash,
|
|
1803
|
+
replica_id, replica_counter, resolved_causal_base, resulting_causal_frontier,
|
|
1804
|
+
operation_count, operation_ids, operation_protocol_version, model_schema_version,
|
|
1805
|
+
commit_protocol_version, prior_native_revision, native_revision, command_count,
|
|
1806
|
+
native_receipt_byte_size, native_receipt_hash, native_receipt_bytes,
|
|
1807
|
+
kernel_version, committed_transaction_byte_size, committed_transaction_hash,
|
|
1808
|
+
committed_transaction_bytes, committed_at`;
|
|
1809
|
+
async function loadCommittedTransactionRows(db, scope, artifactId, after, through, maxCount) {
|
|
1810
|
+
return await rawRows(
|
|
1811
|
+
db,
|
|
1812
|
+
sql`select ${committedTransactionColumns}
|
|
1813
|
+
from editable_artifact_transactions
|
|
1814
|
+
where account_id = ${scope.accountId}::uuid
|
|
1815
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
1816
|
+
and artifact_id = ${artifactId}
|
|
1817
|
+
and sequence_start > ${after}
|
|
1818
|
+
and sequence_end <= ${through}
|
|
1819
|
+
order by sequence_start
|
|
1820
|
+
limit ${maxCount}`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
async function loadCommittedTransactionRowsById(db, scope, artifactId, transactionId) {
|
|
1824
|
+
return await rawRows(
|
|
1825
|
+
db,
|
|
1826
|
+
sql`select ${committedTransactionColumns}
|
|
1827
|
+
from editable_artifact_transactions
|
|
1828
|
+
where account_id = ${scope.accountId}::uuid
|
|
1829
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
1830
|
+
and artifact_id = ${artifactId}
|
|
1831
|
+
and id = ${transactionId}
|
|
1832
|
+
limit 1`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
function liveTransactionFromCommitted(transaction) {
|
|
1836
|
+
const common = {
|
|
1837
|
+
artifactId: transaction.artifactId,
|
|
1838
|
+
transactionId: transaction.serverTransactionId,
|
|
1839
|
+
requestHash: transaction.requestHash,
|
|
1840
|
+
startSequence: transaction.sequenceStart,
|
|
1841
|
+
endSequence: transaction.sequenceEnd,
|
|
1842
|
+
priorStateHash: transaction.priorStateHash,
|
|
1843
|
+
stateHash: transaction.stateHash,
|
|
1844
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
1845
|
+
};
|
|
1846
|
+
return transaction.modality === "spreadsheet" ? Object.freeze({
|
|
1847
|
+
...common,
|
|
1848
|
+
modality: transaction.modality,
|
|
1849
|
+
causalFrontier: transaction.resultingCausalFrontier,
|
|
1850
|
+
operationProtocolVersion: transaction.operationProtocolVersion
|
|
1851
|
+
}) : Object.freeze({
|
|
1852
|
+
...common,
|
|
1853
|
+
modality: transaction.modality,
|
|
1854
|
+
priorNativeRevision: transaction.priorNativeRevision,
|
|
1855
|
+
nativeRevision: transaction.nativeRevision,
|
|
1856
|
+
commitProtocolVersion: transaction.commitProtocolVersion
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
async function insertSnapshotBlobReference(db, snapshot) {
|
|
1860
|
+
await db.execute(sql`
|
|
1861
|
+
insert into editable_artifact_blob_refs (
|
|
1862
|
+
account_id, workspace_id, artifact_id, id, kind, object_reference,
|
|
1863
|
+
byte_size, content_hash, mime_type, created_at
|
|
1864
|
+
) values (
|
|
1865
|
+
${snapshot.scope.accountId}::uuid, ${snapshot.scope.workspaceId}::uuid,
|
|
1866
|
+
${snapshot.artifactId}, ${snapshot.snapshotId}, 'snapshot',
|
|
1867
|
+
${snapshot.blobReference}, ${snapshot.byteSize}, ${snapshot.contentHash},
|
|
1868
|
+
${snapshot.mimeType}, ${snapshot.publishedAt}
|
|
1869
|
+
)`);
|
|
1870
|
+
}
|
|
1871
|
+
async function insertOriginalImportBlobReference(db, scope, artifactId, source, createdAt) {
|
|
1872
|
+
await db.execute(sql`
|
|
1873
|
+
insert into editable_artifact_blob_refs (
|
|
1874
|
+
account_id, workspace_id, artifact_id, id, kind, object_reference,
|
|
1875
|
+
byte_size, content_hash, mime_type, source_file_id, created_at
|
|
1876
|
+
) values (
|
|
1877
|
+
${scope.accountId}::uuid, ${scope.workspaceId}::uuid, ${artifactId},
|
|
1878
|
+
${source.blobRefId}, 'original_import', ${source.blobReference},
|
|
1879
|
+
${source.byteSize}, ${source.contentHash}, ${source.mimeType},
|
|
1880
|
+
${source.fileId}::uuid, ${createdAt}
|
|
1881
|
+
)`);
|
|
1882
|
+
}
|
|
1883
|
+
async function insertSnapshotMetadata(db, snapshot, blobRefId = snapshot.snapshotId) {
|
|
1884
|
+
const coveredCausalFrontierSql = snapshot.modality === "spreadsheet" ? sql`${json(snapshot.coveredCausalFrontier)}::jsonb` : sql`null`;
|
|
1885
|
+
await db.execute(sql`
|
|
1886
|
+
insert into editable_artifact_snapshots (
|
|
1887
|
+
account_id, workspace_id, artifact_id, id, modality, blob_ref_id, byte_size,
|
|
1888
|
+
content_hash, mime_type, covered_head_sequence, covered_causal_frontier,
|
|
1889
|
+
state_hash, model_schema_version, operation_protocol_version,
|
|
1890
|
+
kernel_version, crdt_state_version, native_revision, verified_at, published_at
|
|
1891
|
+
) values (
|
|
1892
|
+
${snapshot.scope.accountId}::uuid, ${snapshot.scope.workspaceId}::uuid,
|
|
1893
|
+
${snapshot.artifactId}, ${snapshot.snapshotId}, ${snapshot.modality}, ${blobRefId},
|
|
1894
|
+
${snapshot.byteSize}, ${snapshot.contentHash}, ${snapshot.mimeType},
|
|
1895
|
+
${snapshot.coveredHeadSequence}, ${coveredCausalFrontierSql}, ${snapshot.stateHash},
|
|
1896
|
+
${snapshot.modelSchemaVersion},
|
|
1897
|
+
${snapshot.modality === "spreadsheet" ? snapshot.operationProtocolVersion : null},
|
|
1898
|
+
${snapshot.kernelVersion},
|
|
1899
|
+
${snapshot.modality === "spreadsheet" ? snapshot.crdtStateVersion : null},
|
|
1900
|
+
${snapshot.modality === "spreadsheet" ? null : snapshot.nativeRevision},
|
|
1901
|
+
${snapshot.verifiedAt}, ${snapshot.publishedAt}
|
|
1902
|
+
)`);
|
|
1903
|
+
}
|
|
1904
|
+
async function insertLiveOutbox(db, artifact, record) {
|
|
1905
|
+
validateOutboxRecord(record, artifact);
|
|
1906
|
+
const event = parseLiveEvent(record.event);
|
|
1907
|
+
await db.execute(sql`
|
|
1908
|
+
insert into editable_artifact_live_outbox (
|
|
1909
|
+
account_id, workspace_id, artifact_id, id, transaction_id, snapshot_id,
|
|
1910
|
+
event_kind, event, state, attempt_count, lease_owner, lease_expires_at,
|
|
1911
|
+
next_attempt_at, last_error_code, published_at, dead_lettered_at, created_at
|
|
1912
|
+
) values (
|
|
1913
|
+
${artifact.scope.accountId}::uuid, ${artifact.scope.workspaceId}::uuid,
|
|
1914
|
+
${artifact.id}, ${record.outboxId},
|
|
1915
|
+
${event.kind === "transaction_committed" ? event.serverTransactionId : null},
|
|
1916
|
+
${event.kind === "snapshot_published" ? event.snapshotId : null},
|
|
1917
|
+
${event.kind}, ${json(event)}::jsonb, ${record.state}, ${record.attemptCount},
|
|
1918
|
+
${record.leaseOwner}, ${record.leaseExpiresAt}, ${record.nextAttemptAt},
|
|
1919
|
+
${record.lastErrorCode}, ${record.publishedAt}, ${record.deadLetteredAt},
|
|
1920
|
+
${record.createdAt}
|
|
1921
|
+
)`);
|
|
1922
|
+
}
|
|
1923
|
+
async function loadReceipt(db, scope, artifactId, actorKey, clientTransactionId) {
|
|
1924
|
+
const rows = await rawRows(
|
|
1925
|
+
db,
|
|
1926
|
+
sql`
|
|
1927
|
+
select
|
|
1928
|
+
t.account_id, t.workspace_id, t.artifact_id, t.id,
|
|
1929
|
+
t.modality, r.id as receipt_id, t.client_transaction_id, t.replica_id,
|
|
1930
|
+
t.replica_counter, t.previous_local_transaction_id, t.intent_bytes,
|
|
1931
|
+
r.request_hash, t.actor_key,
|
|
1932
|
+
t.sequence_start, t.sequence_end, t.prior_state_hash, t.causal_base,
|
|
1933
|
+
t.resolved_causal_base, t.resulting_causal_frontier,
|
|
1934
|
+
t.state_hash, t.operation_count, t.operation_ids,
|
|
1935
|
+
t.selective_undo_targets, t.intent_envelope_version,
|
|
1936
|
+
t.intent_protocol_version, t.command_protocol_version,
|
|
1937
|
+
t.kernel_version, t.model_schema_version,
|
|
1938
|
+
t.operation_protocol_version, t.commit_protocol_version,
|
|
1939
|
+
t.prior_native_revision, t.native_revision, t.command_count,
|
|
1940
|
+
t.native_receipt_byte_size, t.native_receipt_hash, t.native_receipt_bytes,
|
|
1941
|
+
t.committed_transaction_byte_size, t.committed_transaction_hash,
|
|
1942
|
+
t.committed_transaction_bytes, t.committed_at
|
|
1943
|
+
from editable_artifact_idempotency_receipts r
|
|
1944
|
+
join editable_artifact_transactions t
|
|
1945
|
+
on t.account_id = r.account_id
|
|
1946
|
+
and t.workspace_id = r.workspace_id
|
|
1947
|
+
and t.artifact_id = r.artifact_id
|
|
1948
|
+
and t.id = r.server_transaction_id
|
|
1949
|
+
and t.actor_key_digest = r.authority_key_digest
|
|
1950
|
+
and t.actor_key = r.authority_key
|
|
1951
|
+
and t.client_transaction_id = r.idempotency_key
|
|
1952
|
+
and t.request_hash = r.request_hash
|
|
1953
|
+
where r.account_id = ${scope.accountId}::uuid
|
|
1954
|
+
and r.workspace_id = ${scope.workspaceId}::uuid
|
|
1955
|
+
and r.artifact_id = ${artifactId}
|
|
1956
|
+
and r.operation_kind = 'edit'
|
|
1957
|
+
and r.authority_key_digest = sha256(convert_to(${actorKey}, 'UTF8'))
|
|
1958
|
+
and r.authority_key = ${actorKey}
|
|
1959
|
+
and r.idempotency_key = ${clientTransactionId}
|
|
1960
|
+
limit 1`
|
|
1961
|
+
);
|
|
1962
|
+
return rows[0] ? receiptFromRow(rows[0]) : null;
|
|
1963
|
+
}
|
|
1964
|
+
async function loadUndoBasis(db, scope, artifactId, operationIds) {
|
|
1965
|
+
if (operationIds.length === 0) return Object.freeze([]);
|
|
1966
|
+
const operationRows = [];
|
|
1967
|
+
const claimByOperationId = /* @__PURE__ */ new Map();
|
|
1968
|
+
for (let offset = 0; offset < operationIds.length; offset += 1e3) {
|
|
1969
|
+
const chunk = operationIds.slice(offset, offset + 1e3);
|
|
1970
|
+
const identifiers = sql.join(
|
|
1971
|
+
chunk.map((operationId) => sql`${operationId}`),
|
|
1972
|
+
sql`, `
|
|
1973
|
+
);
|
|
1974
|
+
operationRows.push(
|
|
1975
|
+
...await rawRows(
|
|
1976
|
+
db,
|
|
1977
|
+
sql`
|
|
1978
|
+
select account_id, workspace_id, artifact_id, transaction_id,
|
|
1979
|
+
operation_id, operation_index, sequence, dot_replica_id,
|
|
1980
|
+
dot_counter, actor_key, created_at
|
|
1981
|
+
from editable_artifact_operations
|
|
1982
|
+
where account_id = ${scope.accountId}::uuid
|
|
1983
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
1984
|
+
and artifact_id = ${artifactId}
|
|
1985
|
+
and operation_id in (${identifiers})
|
|
1986
|
+
order by sequence`
|
|
1987
|
+
)
|
|
1988
|
+
);
|
|
1989
|
+
const claims = await rawRows(
|
|
1990
|
+
db,
|
|
1991
|
+
sql`
|
|
1992
|
+
select target_operation_id, claiming_transaction_id
|
|
1993
|
+
from editable_artifact_undo_claims
|
|
1994
|
+
where account_id = ${scope.accountId}::uuid
|
|
1995
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
1996
|
+
and artifact_id = ${artifactId}
|
|
1997
|
+
and target_operation_id in (${identifiers})`
|
|
1998
|
+
);
|
|
1999
|
+
for (const claim of claims) {
|
|
2000
|
+
claimByOperationId.set(
|
|
2001
|
+
validateStableId(claim.target_operation_id, "undo target operation id"),
|
|
2002
|
+
validateStableId(claim.claiming_transaction_id, "claiming transaction id")
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
const operationById = new Map(
|
|
2007
|
+
operationRows.map(operationFromRow).map((operation) => [operation.operationId, operation])
|
|
2008
|
+
);
|
|
2009
|
+
return Object.freeze(
|
|
2010
|
+
operationIds.map(
|
|
2011
|
+
(operationId) => Object.freeze({
|
|
2012
|
+
operationId,
|
|
2013
|
+
operation: operationById.get(operationId) ?? null,
|
|
2014
|
+
claimedBy: claimByOperationId.get(operationId) ?? null
|
|
2015
|
+
})
|
|
2016
|
+
)
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
async function anyUndoTargetClaimed(db, scope, artifactId, operationIds) {
|
|
2020
|
+
for (let offset = 0; offset < operationIds.length; offset += 2e3) {
|
|
2021
|
+
const identifiers = sql.join(
|
|
2022
|
+
operationIds.slice(offset, offset + 2e3).map((operationId) => sql`${operationId}`),
|
|
2023
|
+
sql`, `
|
|
2024
|
+
);
|
|
2025
|
+
const rows = await rawRows(
|
|
2026
|
+
db,
|
|
2027
|
+
sql`select exists (
|
|
2028
|
+
select 1
|
|
2029
|
+
from editable_artifact_undo_claims
|
|
2030
|
+
where account_id = ${scope.accountId}::uuid
|
|
2031
|
+
and workspace_id = ${scope.workspaceId}::uuid
|
|
2032
|
+
and artifact_id = ${artifactId}
|
|
2033
|
+
and target_operation_id in (${identifiers})
|
|
2034
|
+
) as claimed`
|
|
2035
|
+
);
|
|
2036
|
+
if (rows[0]?.claimed === true) return true;
|
|
2037
|
+
}
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
function operationFromRow(row) {
|
|
2041
|
+
const operationIndex = validateInteger(
|
|
2042
|
+
row.operation_index,
|
|
2043
|
+
"operation index",
|
|
2044
|
+
0,
|
|
2045
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS - 1
|
|
2046
|
+
);
|
|
2047
|
+
const sequence = safeInteger(row.sequence, "operation sequence", 1);
|
|
2048
|
+
if (sequence <= operationIndex) {
|
|
2049
|
+
throw corrupt("Operation index is outside its authoritative sequence interval");
|
|
2050
|
+
}
|
|
2051
|
+
return Object.freeze({
|
|
2052
|
+
scope: validateScope({ accountId: row.account_id, workspaceId: row.workspace_id }),
|
|
2053
|
+
artifactId: validateStableId(row.artifact_id, "artifact id"),
|
|
2054
|
+
serverTransactionId: validateStableId(row.transaction_id, "transaction id"),
|
|
2055
|
+
sequence,
|
|
2056
|
+
actorKey: validateActorKey(row.actor_key).key,
|
|
2057
|
+
createdAt: iso(row.created_at, "operation creation time"),
|
|
2058
|
+
operationId: validateStableId(row.operation_id, "operation id"),
|
|
2059
|
+
dot: Object.freeze({
|
|
2060
|
+
replicaId: validateReplicaId(row.dot_replica_id),
|
|
2061
|
+
counter: safeInteger(row.dot_counter, "operation causal counter", 1)
|
|
2062
|
+
})
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
function committedTransactionFromRow(row, durableModality) {
|
|
2066
|
+
if (row.modality !== durableModality) {
|
|
2067
|
+
throw corrupt("Stored transaction modality differs from its durable artifact");
|
|
2068
|
+
}
|
|
2069
|
+
const bytes = Uint8Array.from(row.committed_transaction_bytes);
|
|
2070
|
+
const declaredByteSize = validateInteger(
|
|
2071
|
+
row.committed_transaction_byte_size,
|
|
2072
|
+
"committed transaction byte size",
|
|
2073
|
+
1,
|
|
2074
|
+
MAX_COMMITTED_TRANSACTION_BYTES
|
|
2075
|
+
);
|
|
2076
|
+
if (bytes.byteLength !== declaredByteSize) {
|
|
2077
|
+
throw corrupt("Canonical committed transaction size does not match its stored bytes");
|
|
2078
|
+
}
|
|
2079
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
2080
|
+
if (digest !== validateHash(row.committed_transaction_hash, "committed transaction hash")) {
|
|
2081
|
+
throw corrupt("Canonical committed transaction digest verification failed");
|
|
2082
|
+
}
|
|
2083
|
+
const sequenceStart = safeInteger(row.sequence_start, "transaction start sequence", 1);
|
|
2084
|
+
const sequenceEnd = safeInteger(row.sequence_end, "transaction end sequence", sequenceStart);
|
|
2085
|
+
const priorStateHash = validateHash(row.prior_state_hash, "transaction prior state hash");
|
|
2086
|
+
const stateHash = validateHash(row.state_hash, "transaction state hash");
|
|
2087
|
+
const common = {
|
|
2088
|
+
scope: validateScope({ accountId: row.account_id, workspaceId: row.workspace_id }),
|
|
2089
|
+
artifactId: validateStableId(row.artifact_id, "artifact id"),
|
|
2090
|
+
serverTransactionId: validateStableId(row.id, "transaction id"),
|
|
2091
|
+
requestHash: validateHash(row.request_hash, "transaction request hash"),
|
|
2092
|
+
sequenceStart,
|
|
2093
|
+
sequenceEnd,
|
|
2094
|
+
priorStateHash,
|
|
2095
|
+
stateHash,
|
|
2096
|
+
modelSchemaVersion: validateInteger(row.model_schema_version, "model schema version", 1),
|
|
2097
|
+
kernelVersion: validateBoundedText(
|
|
2098
|
+
row.kernel_version,
|
|
2099
|
+
"kernel version",
|
|
2100
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES
|
|
2101
|
+
),
|
|
2102
|
+
committedTransactionBytes: bytes,
|
|
2103
|
+
committedAt: iso(row.committed_at, "transaction commit time")
|
|
2104
|
+
};
|
|
2105
|
+
if (durableModality === "spreadsheet") {
|
|
2106
|
+
let summary2;
|
|
2107
|
+
try {
|
|
2108
|
+
summary2 = decodeCommittedTransactionSummary(bytes);
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
throw new EditableArtifactPersistenceError(
|
|
2111
|
+
"corrupt_history",
|
|
2112
|
+
"Stored canonical committed transaction is not a valid OGACO001 envelope",
|
|
2113
|
+
{ cause: error }
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
const operationCount = validateInteger(
|
|
2117
|
+
row.operation_count,
|
|
2118
|
+
"committed transaction operation count",
|
|
2119
|
+
1,
|
|
2120
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2121
|
+
);
|
|
2122
|
+
const operationIds = parseStableIdArray(
|
|
2123
|
+
row.operation_ids,
|
|
2124
|
+
"committed transaction operation ids",
|
|
2125
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2126
|
+
);
|
|
2127
|
+
const resolvedCausalBase = parseFrontier(row.resolved_causal_base);
|
|
2128
|
+
const resultingCausalFrontier = parseFrontier(row.resulting_causal_frontier);
|
|
2129
|
+
const dot = Object.freeze({
|
|
2130
|
+
replicaId: validateReplicaId(row.replica_id),
|
|
2131
|
+
counter: safeInteger(row.replica_counter, "transaction causal counter", 1)
|
|
2132
|
+
});
|
|
2133
|
+
const operationProtocolVersion = validateInteger(
|
|
2134
|
+
row.operation_protocol_version,
|
|
2135
|
+
"transaction operation protocol version",
|
|
2136
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION,
|
|
2137
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION
|
|
2138
|
+
);
|
|
2139
|
+
if (sequenceEnd - sequenceStart + 1 !== operationCount || operationIds.length !== operationCount || summary2.operationIds.length !== operationCount || summary2.transactionId !== row.id || summary2.dot.replicaId !== dot.replicaId || summary2.dot.counter !== dot.counter || summary2.priorStateHash !== priorStateHash || summary2.stateHash !== stateHash || summary2.operationProtocolVersion !== operationProtocolVersion || !sameStringArray(summary2.operationIds, operationIds) || !sameFrontier(summary2.resolvedCausalBase, resolvedCausalBase) || !sameFrontier(summary2.resultingCausalFrontier, resultingCausalFrontier)) {
|
|
2140
|
+
throw corrupt("Canonical committed transaction disagrees with its durable projection");
|
|
2141
|
+
}
|
|
2142
|
+
assertSerializedColumnsAbsent(row);
|
|
2143
|
+
return Object.freeze({
|
|
2144
|
+
...common,
|
|
2145
|
+
modality: "spreadsheet",
|
|
2146
|
+
dot,
|
|
2147
|
+
resolvedCausalBase,
|
|
2148
|
+
resultingCausalFrontier,
|
|
2149
|
+
operationIds,
|
|
2150
|
+
operationProtocolVersion
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
const modality = serializedModality(durableModality, "transaction modality");
|
|
2154
|
+
if (row.operation_count !== null || row.operation_ids !== null || row.operation_protocol_version !== null || row.resolved_causal_base !== null || row.resulting_causal_frontier !== null) {
|
|
2155
|
+
throw corrupt("Serialized transaction contains spreadsheet projection fields");
|
|
2156
|
+
}
|
|
2157
|
+
const nativeReceiptBytes = readNativeReceiptBytes(row);
|
|
2158
|
+
let summary;
|
|
2159
|
+
try {
|
|
2160
|
+
summary = decodeEditableArtifactSerializedCommit(bytes, modality);
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
throw new EditableArtifactPersistenceError(
|
|
2163
|
+
"corrupt_history",
|
|
2164
|
+
"Stored canonical committed transaction is not a valid OGAST001 envelope",
|
|
2165
|
+
{ cause: error }
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
const commitProtocolVersion = validateInteger(
|
|
2169
|
+
row.commit_protocol_version,
|
|
2170
|
+
"serialized commit protocol version",
|
|
2171
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
2172
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION
|
|
2173
|
+
);
|
|
2174
|
+
const priorNativeRevision = safeInteger(
|
|
2175
|
+
row.prior_native_revision,
|
|
2176
|
+
"serialized prior native revision"
|
|
2177
|
+
);
|
|
2178
|
+
const nativeRevision = safeInteger(row.native_revision, "serialized native revision");
|
|
2179
|
+
const commandCount = validateInteger(
|
|
2180
|
+
row.command_count,
|
|
2181
|
+
"serialized command count",
|
|
2182
|
+
1,
|
|
2183
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2184
|
+
);
|
|
2185
|
+
if (sequenceStart !== sequenceEnd || summary.commitProtocolVersion !== commitProtocolVersion || summary.transactionId !== row.id || summary.parentHeadSequence !== sequenceStart - 1 || summary.resultHeadSequence !== sequenceEnd || summary.priorNativeRevision !== priorNativeRevision || summary.nativeReceipt.revision !== nativeRevision || summary.nativeReceipt.commandCount !== commandCount || summary.priorStateHash !== priorStateHash || summary.stateHash !== stateHash || summary.requestHash !== common.requestHash || !equalByteArrays(summary.nativeReceiptBytes, nativeReceiptBytes)) {
|
|
2186
|
+
throw corrupt("Canonical serialized commit disagrees with its durable projection");
|
|
2187
|
+
}
|
|
2188
|
+
return Object.freeze({
|
|
2189
|
+
...common,
|
|
2190
|
+
modality,
|
|
2191
|
+
commitProtocolVersion,
|
|
2192
|
+
priorNativeRevision,
|
|
2193
|
+
nativeRevision,
|
|
2194
|
+
commandCount,
|
|
2195
|
+
nativeReceiptBytes
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
function artifactFromRow(row) {
|
|
2199
|
+
if (!["spreadsheet", "presentation", "document"].includes(row.modality)) {
|
|
2200
|
+
throw corrupt("Stored artifact modality is invalid");
|
|
2201
|
+
}
|
|
2202
|
+
if (!["active", "archived"].includes(row.lifecycle_state)) {
|
|
2203
|
+
throw corrupt("Stored artifact lifecycle is invalid");
|
|
2204
|
+
}
|
|
2205
|
+
validateTitle(row.title);
|
|
2206
|
+
const common = {
|
|
2207
|
+
scope: validateScope({ accountId: row.account_id, workspaceId: row.workspace_id }),
|
|
2208
|
+
id: validateStableId(row.id, "artifact id"),
|
|
2209
|
+
title: row.title,
|
|
2210
|
+
lifecycle: row.lifecycle_state,
|
|
2211
|
+
authorizationRevision: safeInteger(
|
|
2212
|
+
row.authorization_revision,
|
|
2213
|
+
"artifact authorization revision"
|
|
2214
|
+
),
|
|
2215
|
+
headSequence: safeInteger(row.head_sequence, "artifact head sequence"),
|
|
2216
|
+
stateHash: validateHash(row.state_hash, "artifact state hash"),
|
|
2217
|
+
currentSnapshotId: row.current_snapshot_id ? validateStableId(row.current_snapshot_id, "current snapshot id") : null,
|
|
2218
|
+
createdAt: iso(row.created_at, "artifact creation time"),
|
|
2219
|
+
updatedAt: iso(row.updated_at, "artifact update time")
|
|
2220
|
+
};
|
|
2221
|
+
if (row.modality === "spreadsheet") {
|
|
2222
|
+
return Object.freeze({
|
|
2223
|
+
...common,
|
|
2224
|
+
modality: "spreadsheet",
|
|
2225
|
+
causalFrontier: parseFrontier(row.causal_frontier)
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
if (row.causal_frontier !== null) {
|
|
2229
|
+
throw corrupt("Serialized artifact contains a spreadsheet causal frontier");
|
|
2230
|
+
}
|
|
2231
|
+
return Object.freeze({
|
|
2232
|
+
...common,
|
|
2233
|
+
modality: serializedModality(row.modality, "artifact modality")
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
function receiptFromRow(row) {
|
|
2237
|
+
const modality = artifactModality(row.modality, "receipt modality");
|
|
2238
|
+
const committed = committedTransactionFromRow(row, modality);
|
|
2239
|
+
const sequenceStart = safeInteger(row.sequence_start, "receipt start sequence", 1);
|
|
2240
|
+
const sequenceEnd = safeInteger(row.sequence_end, "receipt end sequence", sequenceStart);
|
|
2241
|
+
const common = {
|
|
2242
|
+
receiptId: validateStableId(row.receipt_id, "receipt id"),
|
|
2243
|
+
scope: validateScope({ accountId: row.account_id, workspaceId: row.workspace_id }),
|
|
2244
|
+
artifactId: validateStableId(row.artifact_id, "artifact id"),
|
|
2245
|
+
modality,
|
|
2246
|
+
serverTransactionId: validateStableId(row.id, "transaction id"),
|
|
2247
|
+
clientTransactionId: validateClientTransactionId(row.client_transaction_id),
|
|
2248
|
+
replicaId: validateReplicaId(row.replica_id),
|
|
2249
|
+
replicaCounter: safeInteger(row.replica_counter, "receipt replica counter", 1),
|
|
2250
|
+
previousLocalTransactionId: row.previous_local_transaction_id ? validateClientTransactionId(row.previous_local_transaction_id) : null,
|
|
2251
|
+
intentBytes: validateCanonicalIntentBytes(row.intent_bytes, row.request_hash),
|
|
2252
|
+
requestHash: validateHash(row.request_hash, "request hash"),
|
|
2253
|
+
actorKey: validateActorKey(row.actor_key).key,
|
|
2254
|
+
sequenceStart,
|
|
2255
|
+
sequenceEnd,
|
|
2256
|
+
priorStateHash: validateHash(row.prior_state_hash, "transaction prior state hash"),
|
|
2257
|
+
stateHash: validateHash(row.state_hash, "transaction state hash"),
|
|
2258
|
+
intentEnvelopeVersion: validateInteger(
|
|
2259
|
+
row.intent_envelope_version,
|
|
2260
|
+
"intent envelope version",
|
|
2261
|
+
1
|
|
2262
|
+
),
|
|
2263
|
+
intentProtocolVersion: validateIntentProtocolVersion(row.intent_protocol_version),
|
|
2264
|
+
commandProtocolVersion: validateInteger(
|
|
2265
|
+
row.command_protocol_version,
|
|
2266
|
+
"command protocol version",
|
|
2267
|
+
1
|
|
2268
|
+
),
|
|
2269
|
+
kernelVersion: validateBoundedText(
|
|
2270
|
+
row.kernel_version,
|
|
2271
|
+
"kernel version",
|
|
2272
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES
|
|
2273
|
+
),
|
|
2274
|
+
modelSchemaVersion: validateInteger(row.model_schema_version, "model schema version", 1),
|
|
2275
|
+
committedAt: iso(row.committed_at, "transaction commit time")
|
|
2276
|
+
};
|
|
2277
|
+
if (committed.sequenceStart !== sequenceStart || committed.sequenceEnd !== sequenceEnd || committed.requestHash !== common.requestHash || committed.priorStateHash !== common.priorStateHash || committed.stateHash !== common.stateHash) {
|
|
2278
|
+
throw corrupt("Receipt differs from its canonical committed transaction");
|
|
2279
|
+
}
|
|
2280
|
+
if (modality === "spreadsheet") {
|
|
2281
|
+
if (committed.modality !== "spreadsheet") {
|
|
2282
|
+
throw corrupt("Spreadsheet receipt resolves to a serialized commit");
|
|
2283
|
+
}
|
|
2284
|
+
const operationCount = validateInteger(
|
|
2285
|
+
row.operation_count,
|
|
2286
|
+
"operation count",
|
|
2287
|
+
1,
|
|
2288
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2289
|
+
);
|
|
2290
|
+
if (sequenceEnd - sequenceStart + 1 !== operationCount) {
|
|
2291
|
+
throw corrupt("Receipt operation interval does not match operation count");
|
|
2292
|
+
}
|
|
2293
|
+
return Object.freeze({
|
|
2294
|
+
...common,
|
|
2295
|
+
modality: "spreadsheet",
|
|
2296
|
+
causalBase: parseFrontier(row.causal_base),
|
|
2297
|
+
resolvedCausalBase: parseFrontier(row.resolved_causal_base),
|
|
2298
|
+
resultingCausalFrontier: parseFrontier(row.resulting_causal_frontier),
|
|
2299
|
+
operationCount,
|
|
2300
|
+
selectiveUndoOperationIds: parseStableIdArray(
|
|
2301
|
+
row.selective_undo_targets,
|
|
2302
|
+
"undo targets",
|
|
2303
|
+
1e4
|
|
2304
|
+
),
|
|
2305
|
+
operationProtocolVersion: committed.operationProtocolVersion
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
if (committed.modality === "spreadsheet" || row.causal_base !== null || row.selective_undo_targets !== null) {
|
|
2309
|
+
throw corrupt("Serialized receipt contains spreadsheet projection fields");
|
|
2310
|
+
}
|
|
2311
|
+
return Object.freeze({
|
|
2312
|
+
...common,
|
|
2313
|
+
modality,
|
|
2314
|
+
commitProtocolVersion: committed.commitProtocolVersion,
|
|
2315
|
+
priorNativeRevision: committed.priorNativeRevision,
|
|
2316
|
+
nativeRevision: committed.nativeRevision,
|
|
2317
|
+
commandCount: committed.commandCount
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function creationReceiptFromRow(scopeInput, row) {
|
|
2321
|
+
const scope = validateScope(scopeInput);
|
|
2322
|
+
if (row.resource_type !== "artifact") {
|
|
2323
|
+
throw corrupt("Artifact creation receipt has an invalid resource type");
|
|
2324
|
+
}
|
|
2325
|
+
if (!isPlainRecord(row.result)) {
|
|
2326
|
+
throw corrupt("Artifact creation receipt result is invalid");
|
|
2327
|
+
}
|
|
2328
|
+
assertExactKeys(
|
|
2329
|
+
row.result,
|
|
2330
|
+
["artifactId", "genesisSnapshotId", "schemaVersion"],
|
|
2331
|
+
"artifact creation receipt result"
|
|
2332
|
+
);
|
|
2333
|
+
if (row.result.schemaVersion !== 1) {
|
|
2334
|
+
throw corrupt("Artifact creation receipt schema version is invalid");
|
|
2335
|
+
}
|
|
2336
|
+
const artifactId = validateStableId(row.resource_id, "created artifact resource id");
|
|
2337
|
+
if (validateStableId(row.result.artifactId, "creation result artifact id") !== artifactId) {
|
|
2338
|
+
throw corrupt("Artifact creation receipt result disagrees with its resource");
|
|
2339
|
+
}
|
|
2340
|
+
return Object.freeze({
|
|
2341
|
+
receiptId: validateStableId(row.id, "creation receipt id"),
|
|
2342
|
+
scope,
|
|
2343
|
+
artifactId,
|
|
2344
|
+
operationKind: validateOriginOperation(row.operation_kind),
|
|
2345
|
+
authorityKey: validateActorKey(row.authority_key).key,
|
|
2346
|
+
idempotencyKey: validateClientTransactionId(row.idempotency_key),
|
|
2347
|
+
requestHash: validateHash(row.request_hash, "creation request hash"),
|
|
2348
|
+
genesisSnapshotId: validateStableId(
|
|
2349
|
+
row.result.genesisSnapshotId,
|
|
2350
|
+
"creation genesis snapshot id"
|
|
2351
|
+
),
|
|
2352
|
+
createdAt: iso(row.created_at, "creation receipt time")
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
function snapshotFromRow(row) {
|
|
2356
|
+
const modality = artifactModality(row.modality, "snapshot modality");
|
|
2357
|
+
if (row.mime_type !== "application/vnd.opengeni.editable-artifact-snapshot") {
|
|
2358
|
+
throw corrupt("Snapshot MIME type is invalid");
|
|
2359
|
+
}
|
|
2360
|
+
if (safeInteger(row.blob_byte_size, "snapshot blob byte size", 1) !== safeInteger(row.byte_size, "snapshot byte size", 1) || validateHash(row.blob_content_hash, "snapshot blob content hash") !== validateHash(row.content_hash, "snapshot content hash") || row.blob_mime_type !== row.mime_type) {
|
|
2361
|
+
throw corrupt("Snapshot metadata disagrees with its immutable blob reference");
|
|
2362
|
+
}
|
|
2363
|
+
const common = {
|
|
2364
|
+
scope: validateScope({ accountId: row.account_id, workspaceId: row.workspace_id }),
|
|
2365
|
+
artifactId: validateStableId(row.artifact_id, "artifact id"),
|
|
2366
|
+
modality,
|
|
2367
|
+
snapshotId: validateStableId(row.id, "snapshot id"),
|
|
2368
|
+
blobReference: validateBoundedText(row.blob_reference, "snapshot blob reference", 1024),
|
|
2369
|
+
byteSize: safeInteger(row.byte_size, "snapshot byte size", 1),
|
|
2370
|
+
contentHash: validateHash(row.content_hash, "snapshot content hash"),
|
|
2371
|
+
mimeType: row.mime_type,
|
|
2372
|
+
coveredHeadSequence: safeInteger(row.covered_head_sequence, "snapshot coverage"),
|
|
2373
|
+
stateHash: validateHash(row.state_hash, "snapshot state hash"),
|
|
2374
|
+
modelSchemaVersion: validateInteger(row.model_schema_version, "model schema version", 1),
|
|
2375
|
+
kernelVersion: validateBoundedText(
|
|
2376
|
+
row.kernel_version,
|
|
2377
|
+
"kernel version",
|
|
2378
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES
|
|
2379
|
+
),
|
|
2380
|
+
verifiedAt: iso(row.verified_at, "snapshot verification time"),
|
|
2381
|
+
publishedAt: iso(row.published_at, "snapshot publication time")
|
|
2382
|
+
};
|
|
2383
|
+
if (modality === "spreadsheet") {
|
|
2384
|
+
if (row.native_revision !== null) {
|
|
2385
|
+
throw corrupt("Spreadsheet snapshot contains a native revision");
|
|
2386
|
+
}
|
|
2387
|
+
return Object.freeze({
|
|
2388
|
+
...common,
|
|
2389
|
+
modality: "spreadsheet",
|
|
2390
|
+
coveredCausalFrontier: parseFrontier(row.covered_causal_frontier),
|
|
2391
|
+
operationProtocolVersion: validateInteger(
|
|
2392
|
+
row.operation_protocol_version,
|
|
2393
|
+
"operation protocol version",
|
|
2394
|
+
1
|
|
2395
|
+
),
|
|
2396
|
+
crdtStateVersion: validateInteger(row.crdt_state_version, "CRDT state version", 1)
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
if (row.covered_causal_frontier !== null || row.operation_protocol_version !== null || row.crdt_state_version !== null) {
|
|
2400
|
+
throw corrupt("Serialized snapshot contains spreadsheet projection fields");
|
|
2401
|
+
}
|
|
2402
|
+
return Object.freeze({
|
|
2403
|
+
...common,
|
|
2404
|
+
modality,
|
|
2405
|
+
nativeRevision: safeInteger(row.native_revision, "snapshot native revision")
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
function outboxFromRow(row) {
|
|
2409
|
+
const event = parseLiveEvent(row.event);
|
|
2410
|
+
if (!["pending", "publishing", "published", "dead_lettered"].includes(row.state)) {
|
|
2411
|
+
throw corrupt("Live outbox state is invalid");
|
|
2412
|
+
}
|
|
2413
|
+
const leaseOwner = row.lease_owner === null ? null : validateLeaseOwner(row.lease_owner);
|
|
2414
|
+
const leaseExpiresAt = row.lease_expires_at ? iso(row.lease_expires_at, "outbox lease expiry") : null;
|
|
2415
|
+
const publishedAt = row.published_at ? iso(row.published_at, "outbox publication time") : null;
|
|
2416
|
+
const deadLetteredAt = row.dead_lettered_at ? iso(row.dead_lettered_at, "outbox dead-letter time") : null;
|
|
2417
|
+
const nextAttemptAt = iso(row.next_attempt_at, "outbox next-attempt time");
|
|
2418
|
+
const lastErrorCode = row.last_error_code === null ? null : validateOutboxStoredErrorCode(row.last_error_code);
|
|
2419
|
+
if (row.state === "pending" && (leaseOwner !== null || leaseExpiresAt !== null || publishedAt !== null || deadLetteredAt !== null || lastErrorCode !== null && !isOutboxRetryErrorCode(lastErrorCode)) || row.state === "publishing" && (leaseOwner === null || leaseExpiresAt === null || publishedAt !== null || deadLetteredAt !== null || lastErrorCode !== null && !isOutboxRetryErrorCode(lastErrorCode)) || row.state === "published" && (leaseOwner !== null || leaseExpiresAt !== null || publishedAt === null || deadLetteredAt !== null || lastErrorCode !== null) || row.state === "dead_lettered" && (leaseOwner !== null || leaseExpiresAt !== null || publishedAt !== null || deadLetteredAt === null || lastErrorCode === null || !isOutboxStoredDeadLetterErrorCode(lastErrorCode))) {
|
|
2420
|
+
throw corrupt("Live outbox state and lease facts are inconsistent");
|
|
2421
|
+
}
|
|
2422
|
+
return Object.freeze({
|
|
2423
|
+
outboxId: validateStableId(row.outbox_id, "outbox id"),
|
|
2424
|
+
event,
|
|
2425
|
+
state: row.state,
|
|
2426
|
+
attemptCount: validateInteger(row.attempt_count, "outbox attempt count", 0, 1e6),
|
|
2427
|
+
leaseOwner,
|
|
2428
|
+
leaseExpiresAt,
|
|
2429
|
+
nextAttemptAt,
|
|
2430
|
+
lastErrorCode,
|
|
2431
|
+
publishedAt,
|
|
2432
|
+
deadLetteredAt,
|
|
2433
|
+
createdAt: iso(row.created_at, "outbox creation time")
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
function parseLiveEvent(value) {
|
|
2437
|
+
if (!isPlainRecord(value) || value.schemaVersion !== 1 || typeof value.kind !== "string") {
|
|
2438
|
+
throw corrupt("Live outbox event envelope is invalid");
|
|
2439
|
+
}
|
|
2440
|
+
const scope = validateScope(value.scope);
|
|
2441
|
+
const artifactId = validateStableId(value.artifactId, "event artifact id");
|
|
2442
|
+
const modality = artifactModality(value.modality, "event modality");
|
|
2443
|
+
if (value.kind === "transaction_committed") {
|
|
2444
|
+
assertExactKeys(
|
|
2445
|
+
value,
|
|
2446
|
+
[
|
|
2447
|
+
"artifactId",
|
|
2448
|
+
...modality === "spreadsheet" ? [] : ["commitProtocolVersion"],
|
|
2449
|
+
"committedAt",
|
|
2450
|
+
"kind",
|
|
2451
|
+
"modality",
|
|
2452
|
+
...modality === "spreadsheet" ? ["operationProtocolVersion"] : [],
|
|
2453
|
+
"schemaVersion",
|
|
2454
|
+
"scope",
|
|
2455
|
+
"sequenceEnd",
|
|
2456
|
+
"sequenceStart",
|
|
2457
|
+
"serverTransactionId",
|
|
2458
|
+
"stateHash"
|
|
2459
|
+
],
|
|
2460
|
+
"transaction outbox event"
|
|
2461
|
+
);
|
|
2462
|
+
const sequenceStart = validateInteger(value.sequenceStart, "event start sequence", 1);
|
|
2463
|
+
const sequenceEnd = validateInteger(value.sequenceEnd, "event end sequence", sequenceStart);
|
|
2464
|
+
const common = {
|
|
2465
|
+
kind: value.kind,
|
|
2466
|
+
schemaVersion: 1,
|
|
2467
|
+
scope,
|
|
2468
|
+
artifactId,
|
|
2469
|
+
modality,
|
|
2470
|
+
serverTransactionId: validateStableId(value.serverTransactionId, "event transaction id"),
|
|
2471
|
+
sequenceStart,
|
|
2472
|
+
sequenceEnd,
|
|
2473
|
+
stateHash: validateHash(value.stateHash, "event state hash"),
|
|
2474
|
+
committedAt: canonicalIsoString(value.committedAt, "event commit time")
|
|
2475
|
+
};
|
|
2476
|
+
return modality === "spreadsheet" ? Object.freeze({
|
|
2477
|
+
...common,
|
|
2478
|
+
modality: "spreadsheet",
|
|
2479
|
+
operationProtocolVersion: validateInteger(
|
|
2480
|
+
value.operationProtocolVersion,
|
|
2481
|
+
"event operation protocol",
|
|
2482
|
+
1
|
|
2483
|
+
)
|
|
2484
|
+
}) : Object.freeze({
|
|
2485
|
+
...common,
|
|
2486
|
+
modality,
|
|
2487
|
+
commitProtocolVersion: validateInteger(
|
|
2488
|
+
value.commitProtocolVersion,
|
|
2489
|
+
"event commit protocol",
|
|
2490
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
2491
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION
|
|
2492
|
+
)
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
if (value.kind === "snapshot_published") {
|
|
2496
|
+
assertExactKeys(
|
|
2497
|
+
value,
|
|
2498
|
+
[
|
|
2499
|
+
"artifactId",
|
|
2500
|
+
"coveredHeadSequence",
|
|
2501
|
+
"kind",
|
|
2502
|
+
"modality",
|
|
2503
|
+
...modality === "spreadsheet" ? ["operationProtocolVersion"] : [],
|
|
2504
|
+
"publishedAt",
|
|
2505
|
+
"schemaVersion",
|
|
2506
|
+
"scope",
|
|
2507
|
+
"snapshotId",
|
|
2508
|
+
"stateHash"
|
|
2509
|
+
],
|
|
2510
|
+
"snapshot outbox event"
|
|
2511
|
+
);
|
|
2512
|
+
const common = {
|
|
2513
|
+
kind: value.kind,
|
|
2514
|
+
schemaVersion: 1,
|
|
2515
|
+
scope,
|
|
2516
|
+
artifactId,
|
|
2517
|
+
modality,
|
|
2518
|
+
snapshotId: validateStableId(value.snapshotId, "event snapshot id"),
|
|
2519
|
+
coveredHeadSequence: validateInteger(value.coveredHeadSequence, "event snapshot coverage", 0),
|
|
2520
|
+
stateHash: validateHash(value.stateHash, "event state hash"),
|
|
2521
|
+
publishedAt: canonicalIsoString(value.publishedAt, "event publication time")
|
|
2522
|
+
};
|
|
2523
|
+
return modality === "spreadsheet" ? Object.freeze({
|
|
2524
|
+
...common,
|
|
2525
|
+
modality: "spreadsheet",
|
|
2526
|
+
operationProtocolVersion: validateInteger(
|
|
2527
|
+
value.operationProtocolVersion,
|
|
2528
|
+
"event operation protocol",
|
|
2529
|
+
1
|
|
2530
|
+
)
|
|
2531
|
+
}) : Object.freeze({ ...common, modality });
|
|
2532
|
+
}
|
|
2533
|
+
throw corrupt("Live outbox event kind is invalid");
|
|
2534
|
+
}
|
|
2535
|
+
function validateAppliedCommit(artifact, input) {
|
|
2536
|
+
if (input.expectedHeadSequence !== artifact.headSequence) throw conflict("Artifact head changed");
|
|
2537
|
+
if (artifact.lifecycle !== "active") throw conflict("Archived artifacts cannot accept edits");
|
|
2538
|
+
const receipt = input.receipt;
|
|
2539
|
+
const intentBytes = validateReceipt(receipt, artifact);
|
|
2540
|
+
validateStableId(input.serverTransactionId, "transaction id");
|
|
2541
|
+
if (receipt.modality !== artifact.modality || input.committedTransaction.modality !== artifact.modality || receipt.serverTransactionId !== input.serverTransactionId || receipt.priorStateHash !== artifact.stateHash || receipt.sequenceStart !== artifact.headSequence + 1) {
|
|
2542
|
+
throw new TypeError("Committed transaction does not extend its durable artifact head");
|
|
2543
|
+
}
|
|
2544
|
+
if (receipt.modality !== "spreadsheet") {
|
|
2545
|
+
if (input.operations.length !== 0 || receipt.sequenceEnd !== receipt.sequenceStart) {
|
|
2546
|
+
throw new TypeError("Serialized commit must advance one head without CRDT operations");
|
|
2547
|
+
}
|
|
2548
|
+
validateCommittedTransactionProjection(input.committedTransaction, receipt, input.operations);
|
|
2549
|
+
validateOutboxRecord(input.outbox, artifact);
|
|
2550
|
+
return intentBytes;
|
|
2551
|
+
}
|
|
2552
|
+
if (artifact.modality !== "spreadsheet") {
|
|
2553
|
+
throw new TypeError("Spreadsheet receipt cannot commit to a serialized artifact");
|
|
2554
|
+
}
|
|
2555
|
+
if (input.operations.length !== receipt.operationCount || receipt.sequenceEnd !== artifact.headSequence + input.operations.length) {
|
|
2556
|
+
throw new TypeError("Committed operation interval does not match its receipt");
|
|
2557
|
+
}
|
|
2558
|
+
const priorReplicaCounter = causalCounter(artifact.causalFrontier, receipt.replicaId);
|
|
2559
|
+
if (receipt.replicaCounter !== priorReplicaCounter + 1) {
|
|
2560
|
+
throw new TypeError("Receipt does not immediately advance its authoritative replica counter");
|
|
2561
|
+
}
|
|
2562
|
+
if (!frontierDominates(receipt.resolvedCausalBase, receipt.causalBase) || !frontierDominates(artifact.causalFrontier, receipt.resolvedCausalBase)) {
|
|
2563
|
+
throw new TypeError("Receipt resolved causal base is inconsistent with authoritative history");
|
|
2564
|
+
}
|
|
2565
|
+
const expectedResult = mergeFrontier(artifact.causalFrontier, {
|
|
2566
|
+
replicaId: receipt.replicaId,
|
|
2567
|
+
counter: receipt.replicaCounter
|
|
2568
|
+
});
|
|
2569
|
+
if (!sameFrontier(expectedResult, receipt.resultingCausalFrontier)) {
|
|
2570
|
+
throw new TypeError("Receipt resulting frontier does not exactly match its transaction dot");
|
|
2571
|
+
}
|
|
2572
|
+
for (let index = 0; index < input.operations.length; index += 1) {
|
|
2573
|
+
const operation = input.operations[index];
|
|
2574
|
+
if (operation.scope.accountId !== artifact.scope.accountId || operation.scope.workspaceId !== artifact.scope.workspaceId || operation.artifactId !== artifact.id || operation.serverTransactionId !== input.serverTransactionId || operation.sequence !== receipt.sequenceStart + index || operation.actorKey !== receipt.actorKey) {
|
|
2575
|
+
throw new TypeError("Committed operation is not bound to its authoritative interval");
|
|
2576
|
+
}
|
|
2577
|
+
validateStableId(operation.operationId, "operation id");
|
|
2578
|
+
validateReplicaId(operation.dot.replicaId);
|
|
2579
|
+
validateInteger(operation.dot.counter, "operation causal counter", 1);
|
|
2580
|
+
if (operation.dot.replicaId !== receipt.replicaId || operation.dot.counter !== receipt.replicaCounter) {
|
|
2581
|
+
throw new TypeError("Operation dot must equal its atomic transaction dot");
|
|
2582
|
+
}
|
|
2583
|
+
if (canonicalIsoString(operation.createdAt, "operation creation time") !== receipt.committedAt) {
|
|
2584
|
+
throw new TypeError("Operation creation time must equal its transaction commit time");
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
validateCommittedTransactionProjection(input.committedTransaction, receipt, input.operations);
|
|
2588
|
+
validateOutboxRecord(input.outbox, artifact);
|
|
2589
|
+
return intentBytes;
|
|
2590
|
+
}
|
|
2591
|
+
function validateOwnedCommitCandidate(input) {
|
|
2592
|
+
if (input.receipt.scope.accountId !== input.scope.accountId || input.receipt.scope.workspaceId !== input.scope.workspaceId || input.receipt.artifactId !== input.artifactId || input.receipt.serverTransactionId !== input.serverTransactionId || input.receipt.requestHash !== input.requestHash) {
|
|
2593
|
+
throw new TypeError("Optimistic commit identity does not match its durable receipt");
|
|
2594
|
+
}
|
|
2595
|
+
validateCommittedTransactionProjection(
|
|
2596
|
+
input.committedTransaction,
|
|
2597
|
+
input.receipt,
|
|
2598
|
+
input.operations
|
|
2599
|
+
);
|
|
2600
|
+
}
|
|
2601
|
+
function validateCommittedTransactionProjection(transaction, receipt, operations) {
|
|
2602
|
+
if (transaction.modality !== receipt.modality || transaction.scope.accountId !== receipt.scope.accountId || transaction.scope.workspaceId !== receipt.scope.workspaceId || transaction.artifactId !== receipt.artifactId || transaction.serverTransactionId !== receipt.serverTransactionId || transaction.requestHash !== receipt.requestHash || transaction.sequenceStart !== receipt.sequenceStart || transaction.sequenceEnd !== receipt.sequenceEnd || transaction.priorStateHash !== receipt.priorStateHash || transaction.stateHash !== receipt.stateHash || transaction.modelSchemaVersion !== receipt.modelSchemaVersion || transaction.kernelVersion !== receipt.kernelVersion || transaction.committedAt !== receipt.committedAt) {
|
|
2603
|
+
throw new TypeError("Canonical committed transaction disagrees with its receipt projection");
|
|
2604
|
+
}
|
|
2605
|
+
validateStableId(transaction.serverTransactionId, "committed transaction id");
|
|
2606
|
+
validateHash(transaction.requestHash, "committed transaction request hash");
|
|
2607
|
+
validateHash(transaction.priorStateHash, "committed transaction prior state hash");
|
|
2608
|
+
validateHash(transaction.stateHash, "committed transaction state hash");
|
|
2609
|
+
validateInteger(transaction.modelSchemaVersion, "committed transaction model schema version", 1);
|
|
2610
|
+
validateBoundedText(
|
|
2611
|
+
transaction.kernelVersion,
|
|
2612
|
+
"committed transaction kernel version",
|
|
2613
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES
|
|
2614
|
+
);
|
|
2615
|
+
canonicalIsoString(transaction.committedAt, "committed transaction commit time");
|
|
2616
|
+
if (!(transaction.committedTransactionBytes instanceof Uint8Array) || transaction.committedTransactionBytes.byteLength < 1 || transaction.committedTransactionBytes.byteLength > MAX_COMMITTED_TRANSACTION_BYTES) {
|
|
2617
|
+
throw new TypeError("Canonical committed transaction bytes are invalid or oversized");
|
|
2618
|
+
}
|
|
2619
|
+
if (transaction.modality === "spreadsheet" && receipt.modality === "spreadsheet") {
|
|
2620
|
+
if (transaction.dot.replicaId !== receipt.replicaId || transaction.dot.counter !== receipt.replicaCounter || !sameFrontier(transaction.resolvedCausalBase, receipt.resolvedCausalBase) || !sameFrontier(transaction.resultingCausalFrontier, receipt.resultingCausalFrontier) || transaction.operationProtocolVersion !== receipt.operationProtocolVersion || transaction.operationIds.length !== operations.length || transaction.operationIds.some(
|
|
2621
|
+
(operationId, index) => operationId !== operations[index]?.operationId
|
|
2622
|
+
)) {
|
|
2623
|
+
throw new TypeError("Canonical spreadsheet commit disagrees with its receipt projection");
|
|
2624
|
+
}
|
|
2625
|
+
validateReplicaId(transaction.dot.replicaId);
|
|
2626
|
+
validateInteger(transaction.dot.counter, "committed transaction causal counter", 1);
|
|
2627
|
+
const resolvedCausalBase = parseFrontier(transaction.resolvedCausalBase);
|
|
2628
|
+
const resultingCausalFrontier = parseFrontier(transaction.resultingCausalFrontier);
|
|
2629
|
+
const operationIds = parseStableIdArray(
|
|
2630
|
+
transaction.operationIds,
|
|
2631
|
+
"committed transaction operation ids",
|
|
2632
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2633
|
+
);
|
|
2634
|
+
validateInteger(
|
|
2635
|
+
transaction.operationProtocolVersion,
|
|
2636
|
+
"committed transaction operation protocol version",
|
|
2637
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION,
|
|
2638
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION
|
|
2639
|
+
);
|
|
2640
|
+
let summary2;
|
|
2641
|
+
try {
|
|
2642
|
+
summary2 = decodeCommittedTransactionSummary(transaction.committedTransactionBytes);
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
throw new TypeError("Canonical committed transaction is not a valid OGACO001 envelope", {
|
|
2645
|
+
cause: error
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
if (summary2.transactionId !== transaction.serverTransactionId || summary2.dot.replicaId !== transaction.dot.replicaId || summary2.dot.counter !== transaction.dot.counter || summary2.priorStateHash !== transaction.priorStateHash || summary2.stateHash !== transaction.stateHash || summary2.operationProtocolVersion !== transaction.operationProtocolVersion || !sameStringArray(summary2.operationIds, operationIds) || !sameFrontier(summary2.resolvedCausalBase, resolvedCausalBase) || !sameFrontier(summary2.resultingCausalFrontier, resultingCausalFrontier)) {
|
|
2649
|
+
throw new TypeError("Canonical OGACO001 bytes disagree with their durable projection");
|
|
2650
|
+
}
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (transaction.modality === "spreadsheet" || receipt.modality === "spreadsheet") {
|
|
2654
|
+
throw new TypeError("Committed transaction and receipt modalities disagree");
|
|
2655
|
+
}
|
|
2656
|
+
if (operations.length !== 0 || transaction.sequenceStart !== transaction.sequenceEnd || transaction.commitProtocolVersion !== receipt.commitProtocolVersion || transaction.priorNativeRevision !== receipt.priorNativeRevision || transaction.nativeRevision !== receipt.nativeRevision || transaction.commandCount !== receipt.commandCount) {
|
|
2657
|
+
throw new TypeError("Canonical serialized commit disagrees with its receipt projection");
|
|
2658
|
+
}
|
|
2659
|
+
validateInteger(
|
|
2660
|
+
transaction.commitProtocolVersion,
|
|
2661
|
+
"serialized commit protocol version",
|
|
2662
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
2663
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION
|
|
2664
|
+
);
|
|
2665
|
+
validateInteger(transaction.priorNativeRevision, "serialized prior native revision", 0);
|
|
2666
|
+
validateInteger(transaction.nativeRevision, "serialized native revision", 0);
|
|
2667
|
+
validateInteger(
|
|
2668
|
+
transaction.commandCount,
|
|
2669
|
+
"serialized command count",
|
|
2670
|
+
1,
|
|
2671
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2672
|
+
);
|
|
2673
|
+
if (!(transaction.nativeReceiptBytes instanceof Uint8Array) || transaction.nativeReceiptBytes.byteLength < 1 || transaction.nativeReceiptBytes.byteLength > EDITABLE_ARTIFACT_NATIVE_RECEIPT_MAX_BYTES) {
|
|
2674
|
+
throw new TypeError("Serialized native receipt bytes are invalid or oversized");
|
|
2675
|
+
}
|
|
2676
|
+
let summary;
|
|
2677
|
+
try {
|
|
2678
|
+
summary = decodeEditableArtifactSerializedCommit(
|
|
2679
|
+
transaction.committedTransactionBytes,
|
|
2680
|
+
transaction.modality
|
|
2681
|
+
);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
throw new TypeError("Canonical committed transaction is not a valid OGAST001 envelope", {
|
|
2684
|
+
cause: error
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
if (summary.transactionId !== transaction.serverTransactionId || summary.parentHeadSequence !== transaction.sequenceStart - 1 || summary.resultHeadSequence !== transaction.sequenceEnd || summary.priorNativeRevision !== transaction.priorNativeRevision || summary.priorStateHash !== transaction.priorStateHash || summary.stateHash !== transaction.stateHash || summary.requestHash !== transaction.requestHash || summary.nativeReceipt.revision !== transaction.nativeRevision || summary.nativeReceipt.commandCount !== transaction.commandCount || !equalByteArrays(summary.intentBytes, receipt.intentBytes) || !equalByteArrays(summary.nativeReceiptBytes, transaction.nativeReceiptBytes)) {
|
|
2688
|
+
throw new TypeError("Canonical OGAST001 bytes disagree with their durable projection");
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
function validateReceipt(receipt, artifact) {
|
|
2692
|
+
validateStableId(receipt.receiptId, "receipt id");
|
|
2693
|
+
validateStableId(receipt.serverTransactionId, "transaction id");
|
|
2694
|
+
if (receipt.scope.accountId !== artifact.scope.accountId || receipt.scope.workspaceId !== artifact.scope.workspaceId || receipt.artifactId !== artifact.id || receipt.modality !== artifact.modality)
|
|
2695
|
+
throw new TypeError("Receipt tenant identity does not match locked artifact");
|
|
2696
|
+
validateClientTransactionId(receipt.clientTransactionId);
|
|
2697
|
+
validateReplicaId(receipt.replicaId);
|
|
2698
|
+
validateInteger(receipt.replicaCounter, "receipt replica counter", 1);
|
|
2699
|
+
if (receipt.previousLocalTransactionId !== null) {
|
|
2700
|
+
validateClientTransactionId(receipt.previousLocalTransactionId);
|
|
2701
|
+
if (receipt.previousLocalTransactionId === receipt.clientTransactionId) {
|
|
2702
|
+
throw new TypeError("A transaction cannot name itself as its predecessor");
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
const intentBytes = validateCanonicalIntentBytes(receipt.intentBytes, receipt.requestHash);
|
|
2706
|
+
validateHash(receipt.requestHash, "request hash");
|
|
2707
|
+
validateActorKey(receipt.actorKey);
|
|
2708
|
+
validateInteger(receipt.sequenceStart, "receipt start sequence", 1);
|
|
2709
|
+
validateInteger(receipt.sequenceEnd, "receipt end sequence", receipt.sequenceStart);
|
|
2710
|
+
validateHash(receipt.priorStateHash, "receipt prior state hash");
|
|
2711
|
+
validateHash(receipt.stateHash, "receipt state hash");
|
|
2712
|
+
validateInteger(receipt.intentEnvelopeVersion, "intent envelope version", 1, 1);
|
|
2713
|
+
validateIntentProtocolVersion(receipt.intentProtocolVersion);
|
|
2714
|
+
validateInteger(receipt.commandProtocolVersion, "command protocol version", 1);
|
|
2715
|
+
validateBoundedText(
|
|
2716
|
+
receipt.kernelVersion,
|
|
2717
|
+
"kernel version",
|
|
2718
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES
|
|
2719
|
+
);
|
|
2720
|
+
validateInteger(receipt.modelSchemaVersion, "model schema version", 1);
|
|
2721
|
+
canonicalIsoString(receipt.committedAt, "receipt commit time");
|
|
2722
|
+
if (receipt.modality === "spreadsheet") {
|
|
2723
|
+
validateInteger(
|
|
2724
|
+
receipt.operationCount,
|
|
2725
|
+
"receipt operation count",
|
|
2726
|
+
1,
|
|
2727
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2728
|
+
);
|
|
2729
|
+
if (receipt.sequenceEnd - receipt.sequenceStart + 1 !== receipt.operationCount) {
|
|
2730
|
+
throw new TypeError("Receipt interval does not match operation count");
|
|
2731
|
+
}
|
|
2732
|
+
parseFrontier(receipt.causalBase);
|
|
2733
|
+
parseFrontier(receipt.resolvedCausalBase);
|
|
2734
|
+
parseFrontier(receipt.resultingCausalFrontier);
|
|
2735
|
+
parseStableIdArray(receipt.selectiveUndoOperationIds, "undo targets", 1e4);
|
|
2736
|
+
validateInteger(receipt.operationProtocolVersion, "operation protocol version", 1);
|
|
2737
|
+
} else {
|
|
2738
|
+
if (receipt.sequenceEnd !== receipt.sequenceStart) {
|
|
2739
|
+
throw new TypeError("Serialized receipt must advance the durable head exactly once");
|
|
2740
|
+
}
|
|
2741
|
+
validateInteger(
|
|
2742
|
+
receipt.commitProtocolVersion,
|
|
2743
|
+
"serialized commit protocol version",
|
|
2744
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION,
|
|
2745
|
+
EDITABLE_ARTIFACT_SERIALIZED_COMMIT_VERSION
|
|
2746
|
+
);
|
|
2747
|
+
validateInteger(receipt.priorNativeRevision, "serialized prior native revision", 0);
|
|
2748
|
+
validateInteger(receipt.nativeRevision, "serialized native revision", 0);
|
|
2749
|
+
validateInteger(
|
|
2750
|
+
receipt.commandCount,
|
|
2751
|
+
"serialized command count",
|
|
2752
|
+
1,
|
|
2753
|
+
MAX_COMMITTED_TRANSACTION_OPERATIONS
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
return intentBytes;
|
|
2757
|
+
}
|
|
2758
|
+
function ownSnapshotCommit(input) {
|
|
2759
|
+
const expectedCurrentSnapshotId = input.expectedCurrentSnapshotId === null ? null : validateStableId(input.expectedCurrentSnapshotId, "expected current snapshot id");
|
|
2760
|
+
const snapshot = ownSnapshotMetadata(input.snapshot);
|
|
2761
|
+
return Object.freeze({
|
|
2762
|
+
expectedCurrentSnapshotId,
|
|
2763
|
+
expectedAuthorizationRevision: validateInteger(
|
|
2764
|
+
input.expectedAuthorizationRevision,
|
|
2765
|
+
"expected snapshot authorization revision",
|
|
2766
|
+
0
|
|
2767
|
+
),
|
|
2768
|
+
authorizationActor: validateLiveTicketActor(input.authorizationActor),
|
|
2769
|
+
authorizationPermission: snapshotAuthorizationPermission(
|
|
2770
|
+
input.authorizationPermission ?? "manage"
|
|
2771
|
+
),
|
|
2772
|
+
snapshot,
|
|
2773
|
+
outbox: cloneOutbox(input.outbox)
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
function ownSnapshotMetadata(snapshot) {
|
|
2777
|
+
return snapshot.modality === "spreadsheet" ? Object.freeze({
|
|
2778
|
+
...snapshot,
|
|
2779
|
+
scope: validateScope(snapshot.scope),
|
|
2780
|
+
coveredCausalFrontier: parseFrontier(snapshot.coveredCausalFrontier)
|
|
2781
|
+
}) : Object.freeze({
|
|
2782
|
+
...snapshot,
|
|
2783
|
+
scope: validateScope(snapshot.scope),
|
|
2784
|
+
modality: serializedModality(snapshot.modality, "snapshot modality"),
|
|
2785
|
+
nativeRevision: validateInteger(snapshot.nativeRevision, "snapshot native revision", 0)
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
function validateSnapshotCommit(artifact, input) {
|
|
2789
|
+
if (input.expectedAuthorizationRevision !== artifact.authorizationRevision) {
|
|
2790
|
+
throw conflict("Artifact authorization changed before snapshot publication");
|
|
2791
|
+
}
|
|
2792
|
+
if (input.expectedCurrentSnapshotId !== artifact.currentSnapshotId) {
|
|
2793
|
+
throw conflict("Current snapshot changed");
|
|
2794
|
+
}
|
|
2795
|
+
const snapshot = input.snapshot;
|
|
2796
|
+
if (snapshot.scope.accountId !== artifact.scope.accountId || snapshot.scope.workspaceId !== artifact.scope.workspaceId || snapshot.artifactId !== artifact.id || snapshot.modality !== artifact.modality)
|
|
2797
|
+
throw new TypeError("Snapshot tenant identity does not match locked artifact");
|
|
2798
|
+
validateStableId(snapshot.snapshotId, "snapshot id");
|
|
2799
|
+
validateBoundedText(snapshot.blobReference, "snapshot blob reference", 1024);
|
|
2800
|
+
validateInteger(
|
|
2801
|
+
snapshot.byteSize,
|
|
2802
|
+
"snapshot byte size",
|
|
2803
|
+
1,
|
|
2804
|
+
EDITABLE_ARTIFACT_PRODUCT_MAX_SNAPSHOT_BYTES
|
|
2805
|
+
);
|
|
2806
|
+
validateHash(snapshot.contentHash, "snapshot content hash");
|
|
2807
|
+
if (snapshot.mimeType !== "application/vnd.opengeni.editable-artifact-snapshot") {
|
|
2808
|
+
throw new TypeError("Snapshot MIME type is invalid");
|
|
2809
|
+
}
|
|
2810
|
+
validateInteger(snapshot.coveredHeadSequence, "snapshot coverage", 0, artifact.headSequence);
|
|
2811
|
+
validateHash(snapshot.stateHash, "snapshot state hash");
|
|
2812
|
+
validateInteger(snapshot.modelSchemaVersion, "model schema version", 1);
|
|
2813
|
+
validateBoundedText(
|
|
2814
|
+
snapshot.kernelVersion,
|
|
2815
|
+
"kernel version",
|
|
2816
|
+
EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES
|
|
2817
|
+
);
|
|
2818
|
+
if (snapshot.modality === "spreadsheet") {
|
|
2819
|
+
parseFrontier(snapshot.coveredCausalFrontier);
|
|
2820
|
+
validateInteger(snapshot.operationProtocolVersion, "operation protocol version", 1);
|
|
2821
|
+
validateInteger(snapshot.crdtStateVersion, "CRDT state version", 1);
|
|
2822
|
+
} else {
|
|
2823
|
+
validateInteger(snapshot.nativeRevision, "snapshot native revision", 0);
|
|
2824
|
+
}
|
|
2825
|
+
canonicalIsoString(snapshot.verifiedAt, "snapshot verification time");
|
|
2826
|
+
canonicalIsoString(snapshot.publishedAt, "snapshot publication time");
|
|
2827
|
+
if (snapshot.verifiedAt > snapshot.publishedAt) {
|
|
2828
|
+
throw new TypeError("Snapshot verification time cannot follow publication time");
|
|
2829
|
+
}
|
|
2830
|
+
validateOutboxRecord(input.outbox, artifact);
|
|
2831
|
+
}
|
|
2832
|
+
function snapshotAuthorizationPermission(value) {
|
|
2833
|
+
if (value !== "manage" && value !== "read" && value !== "edit") {
|
|
2834
|
+
throw new TypeError("Snapshot authorization permission is invalid");
|
|
2835
|
+
}
|
|
2836
|
+
return value;
|
|
2837
|
+
}
|
|
2838
|
+
function validateGenesisCreation(artifact, snapshot, outbox, operationKind) {
|
|
2839
|
+
validateSnapshotCommit(artifact, {
|
|
2840
|
+
expectedCurrentSnapshotId: null,
|
|
2841
|
+
expectedAuthorizationRevision: artifact.authorizationRevision,
|
|
2842
|
+
snapshot,
|
|
2843
|
+
outbox
|
|
2844
|
+
});
|
|
2845
|
+
if (snapshot.coveredHeadSequence !== 0 || operationKind === "create" && (snapshot.modality === "spreadsheet" ? snapshot.coveredCausalFrontier.length !== 0 : snapshot.nativeRevision !== 0) || snapshot.stateHash !== artifact.stateHash || snapshot.publishedAt !== artifact.createdAt) {
|
|
2846
|
+
throw new TypeError("Genesis snapshot must exactly define the sequence-zero artifact state");
|
|
2847
|
+
}
|
|
2848
|
+
validateSnapshotOutbox(outbox, snapshot);
|
|
2849
|
+
}
|
|
2850
|
+
function validateExpectedPredecessor(value) {
|
|
2851
|
+
if (value === null) return null;
|
|
2852
|
+
if (!isPlainRecord(value)) throw new TypeError("Expected predecessor is invalid");
|
|
2853
|
+
assertExactKeys(
|
|
2854
|
+
value,
|
|
2855
|
+
[
|
|
2856
|
+
"actorKey",
|
|
2857
|
+
"clientTransactionId",
|
|
2858
|
+
"receiptId",
|
|
2859
|
+
"replicaCounter",
|
|
2860
|
+
"replicaId",
|
|
2861
|
+
"serverTransactionId"
|
|
2862
|
+
],
|
|
2863
|
+
"expected predecessor"
|
|
2864
|
+
);
|
|
2865
|
+
return Object.freeze({
|
|
2866
|
+
receiptId: validateStableId(value.receiptId, "predecessor receipt id"),
|
|
2867
|
+
serverTransactionId: validateStableId(value.serverTransactionId, "predecessor transaction id"),
|
|
2868
|
+
actorKey: validateActorKey(value.actorKey).key,
|
|
2869
|
+
clientTransactionId: validateClientTransactionId(value.clientTransactionId),
|
|
2870
|
+
replicaId: validateReplicaId(value.replicaId),
|
|
2871
|
+
replicaCounter: validateInteger(value.replicaCounter, "predecessor replica counter", 1)
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
function predecessorMatches(actual, expected) {
|
|
2875
|
+
return actual !== null && actual.receiptId === expected.receiptId && actual.serverTransactionId === expected.serverTransactionId && actual.actorKey === expected.actorKey && actual.clientTransactionId === expected.clientTransactionId && actual.replicaId === expected.replicaId && actual.replicaCounter === expected.replicaCounter;
|
|
2876
|
+
}
|
|
2877
|
+
function replayResult(receipt) {
|
|
2878
|
+
return Object.freeze({ kind: "replayed", receipt: cloneReceipt(receipt) });
|
|
2879
|
+
}
|
|
2880
|
+
function validateOutboxRecord(record, artifact) {
|
|
2881
|
+
validateStableId(record.outboxId, "outbox id");
|
|
2882
|
+
if (record.event.scope.accountId !== artifact.scope.accountId || record.event.scope.workspaceId !== artifact.scope.workspaceId || record.event.artifactId !== artifact.id || record.event.modality !== artifact.modality)
|
|
2883
|
+
throw new TypeError("Outbox event tenant identity does not match locked artifact");
|
|
2884
|
+
parseLiveEvent(record.event);
|
|
2885
|
+
if (record.state !== "pending" || record.attemptCount !== 0 || record.leaseOwner !== null || record.leaseExpiresAt !== null || record.nextAttemptAt !== record.createdAt || record.lastErrorCode !== null || record.publishedAt !== null || record.deadLetteredAt !== null) {
|
|
2886
|
+
throw new TypeError("New outbox record must be pending and unleased");
|
|
2887
|
+
}
|
|
2888
|
+
canonicalIsoString(record.createdAt, "outbox creation time");
|
|
2889
|
+
}
|
|
2890
|
+
function validateTransactionOutbox(record, receipt) {
|
|
2891
|
+
if (record.event.kind !== "transaction_committed") {
|
|
2892
|
+
throw new TypeError("Transaction commit requires a transaction outbox event");
|
|
2893
|
+
}
|
|
2894
|
+
const event = parseLiveEvent(record.event);
|
|
2895
|
+
if (event.kind !== "transaction_committed" || event.modality !== receipt.modality || event.serverTransactionId !== receipt.serverTransactionId || event.sequenceStart !== receipt.sequenceStart || event.sequenceEnd !== receipt.sequenceEnd || event.stateHash !== receipt.stateHash || (event.modality === "spreadsheet" && receipt.modality === "spreadsheet" ? event.operationProtocolVersion !== receipt.operationProtocolVersion : event.modality !== "spreadsheet" && receipt.modality !== "spreadsheet" ? event.commitProtocolVersion !== receipt.commitProtocolVersion : true) || event.committedAt !== receipt.committedAt || record.createdAt !== receipt.committedAt) {
|
|
2896
|
+
throw new TypeError("Transaction outbox event does not exactly describe its durable commit");
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function validateSnapshotOutbox(record, snapshot) {
|
|
2900
|
+
if (record.event.kind !== "snapshot_published") {
|
|
2901
|
+
throw new TypeError("Snapshot publication requires a snapshot outbox event");
|
|
2902
|
+
}
|
|
2903
|
+
const event = parseLiveEvent(record.event);
|
|
2904
|
+
if (event.kind !== "snapshot_published" || event.modality !== snapshot.modality || event.snapshotId !== snapshot.snapshotId || event.coveredHeadSequence !== snapshot.coveredHeadSequence || event.stateHash !== snapshot.stateHash || event.modality === "spreadsheet" && snapshot.modality === "spreadsheet" && event.operationProtocolVersion !== snapshot.operationProtocolVersion || event.publishedAt !== snapshot.publishedAt || record.createdAt !== snapshot.publishedAt) {
|
|
2905
|
+
throw new TypeError("Snapshot outbox event does not exactly describe its durable publication");
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
function validateActorKey(input) {
|
|
2909
|
+
validateBoundedText(input, "actor key", 8192);
|
|
2910
|
+
let value;
|
|
2911
|
+
try {
|
|
2912
|
+
value = JSON.parse(input);
|
|
2913
|
+
} catch {
|
|
2914
|
+
throw new TypeError("Actor key must be canonical JSON");
|
|
2915
|
+
}
|
|
2916
|
+
if (!Array.isArray(value)) throw new TypeError("Actor key must be a JSON tuple");
|
|
2917
|
+
let actor;
|
|
2918
|
+
if (value[0] === "human" && value.length === 2) {
|
|
2919
|
+
actor = {
|
|
2920
|
+
kind: "human",
|
|
2921
|
+
subjectId: validateIdentity(value[1], "actor subject id"),
|
|
2922
|
+
key: input
|
|
2923
|
+
};
|
|
2924
|
+
} else if (value[0] === "agent" && value.length === 6) {
|
|
2925
|
+
actor = {
|
|
2926
|
+
kind: "agent",
|
|
2927
|
+
subjectId: validateIdentity(value[1], "actor subject id"),
|
|
2928
|
+
sessionId: validateIdentity(value[2], "agent session id"),
|
|
2929
|
+
turnId: validateIdentity(value[3], "agent turn id"),
|
|
2930
|
+
attemptId: validateIdentity(value[4], "agent attempt id"),
|
|
2931
|
+
generation: validateInteger(value[5], "agent generation", 0),
|
|
2932
|
+
key: input
|
|
2933
|
+
};
|
|
2934
|
+
} else if (value[0] === "service" && value.length === 3) {
|
|
2935
|
+
actor = {
|
|
2936
|
+
kind: "service",
|
|
2937
|
+
subjectId: validateIdentity(value[1], "actor subject id"),
|
|
2938
|
+
service: validateIdentity(value[2], "service name"),
|
|
2939
|
+
key: input
|
|
2940
|
+
};
|
|
2941
|
+
} else {
|
|
2942
|
+
throw new TypeError("Actor key tuple has an unsupported authority shape");
|
|
2943
|
+
}
|
|
2944
|
+
if (JSON.stringify(value) !== input)
|
|
2945
|
+
throw new TypeError("Actor key must use canonical JSON encoding");
|
|
2946
|
+
return actor;
|
|
2947
|
+
}
|
|
2948
|
+
function parseFrontier(input) {
|
|
2949
|
+
if (!Array.isArray(input) || input.length > 65536)
|
|
2950
|
+
throw corrupt("Causal frontier is invalid or oversized");
|
|
2951
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2952
|
+
const output = input.map((raw) => {
|
|
2953
|
+
if (!isPlainRecord(raw)) throw corrupt("Causal frontier entry is invalid");
|
|
2954
|
+
const keys = Object.keys(raw).sort();
|
|
2955
|
+
if (keys.length !== 2 || keys[0] !== "counter" || keys[1] !== "replicaId") {
|
|
2956
|
+
throw corrupt("Causal frontier entry contains unknown fields");
|
|
2957
|
+
}
|
|
2958
|
+
const replicaId = validateReplicaId(raw.replicaId);
|
|
2959
|
+
if (seen.has(replicaId)) throw corrupt("Causal frontier contains duplicate replicas");
|
|
2960
|
+
seen.add(replicaId);
|
|
2961
|
+
return Object.freeze({ replicaId, counter: validateInteger(raw.counter, "causal counter", 1) });
|
|
2962
|
+
});
|
|
2963
|
+
output.sort(
|
|
2964
|
+
(left, right) => left.replicaId < right.replicaId ? -1 : left.replicaId > right.replicaId ? 1 : 0
|
|
2965
|
+
);
|
|
2966
|
+
for (let index = 0; index < output.length; index += 1) {
|
|
2967
|
+
const raw = input[index];
|
|
2968
|
+
if (raw.replicaId !== output[index].replicaId)
|
|
2969
|
+
throw corrupt("Causal frontier is not canonically sorted");
|
|
2970
|
+
}
|
|
2971
|
+
if (new TextEncoder().encode(JSON.stringify(output)).byteLength > 1024 * 1024) {
|
|
2972
|
+
throw corrupt("Causal frontier exceeds 1 MiB");
|
|
2973
|
+
}
|
|
2974
|
+
return Object.freeze(output);
|
|
2975
|
+
}
|
|
2976
|
+
function parseStableIdArray(input, label, max) {
|
|
2977
|
+
if (!Array.isArray(input) || input.length > max)
|
|
2978
|
+
throw corrupt(`${label} is invalid or oversized`);
|
|
2979
|
+
const values = input.map((value) => validateStableId(value, label));
|
|
2980
|
+
if (new Set(values).size !== values.length) throw corrupt(`${label} contains duplicates`);
|
|
2981
|
+
return Object.freeze(values);
|
|
2982
|
+
}
|
|
2983
|
+
function validateScope(scope) {
|
|
2984
|
+
if (!isPlainRecord(scope)) throw new TypeError("Editable artifact scope is invalid");
|
|
2985
|
+
assertExactKeys(scope, ["accountId", "workspaceId"], "editable artifact scope");
|
|
2986
|
+
return Object.freeze({
|
|
2987
|
+
accountId: validateIdentity(scope.accountId, "account id"),
|
|
2988
|
+
workspaceId: validateIdentity(scope.workspaceId, "workspace id")
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
function validateLiveTicketRecord(input) {
|
|
2992
|
+
if (!isPlainRecord(input)) throw new TypeError("Live ticket record is invalid");
|
|
2993
|
+
assertExactKeys(
|
|
2994
|
+
input,
|
|
2995
|
+
[
|
|
2996
|
+
"actor",
|
|
2997
|
+
"allowEdit",
|
|
2998
|
+
"artifactId",
|
|
2999
|
+
"expiresAt",
|
|
3000
|
+
"issuedAt",
|
|
3001
|
+
"modality",
|
|
3002
|
+
"protocolVersion",
|
|
3003
|
+
"scope",
|
|
3004
|
+
"tokenDigest"
|
|
3005
|
+
],
|
|
3006
|
+
"live ticket record"
|
|
3007
|
+
);
|
|
3008
|
+
const issuedAt = canonicalIsoString(input.issuedAt, "live ticket issuedAt");
|
|
3009
|
+
const expiresAt = canonicalIsoString(input.expiresAt, "live ticket expiresAt");
|
|
3010
|
+
const durationMs = Date.parse(expiresAt) - Date.parse(issuedAt);
|
|
3011
|
+
if (durationMs < 1e3 || durationMs > 6e4) {
|
|
3012
|
+
throw new TypeError("Live ticket lifetime must be 1-60 seconds");
|
|
3013
|
+
}
|
|
3014
|
+
const modality = validateLiveTicketModality(input.modality);
|
|
3015
|
+
return Object.freeze({
|
|
3016
|
+
tokenDigest: validateHash(input.tokenDigest, "live ticket digest"),
|
|
3017
|
+
scope: validateScope(input.scope),
|
|
3018
|
+
artifactId: validateStableId(input.artifactId, "artifact id"),
|
|
3019
|
+
modality,
|
|
3020
|
+
actor: validateLiveTicketActor(input.actor),
|
|
3021
|
+
allowEdit: validateBoolean(input.allowEdit, "live ticket allowEdit"),
|
|
3022
|
+
protocolVersion: validateInteger(input.protocolVersion, "live protocol version", 1),
|
|
3023
|
+
issuedAt,
|
|
3024
|
+
expiresAt
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
function validateLiveTicketActor(input) {
|
|
3028
|
+
if (!isPlainRecord(input)) throw new TypeError("Live ticket actor is invalid");
|
|
3029
|
+
const common = {
|
|
3030
|
+
subjectId: validateIdentity(input.subjectId, "actor subject id"),
|
|
3031
|
+
replicaId: validateReplicaId(input.replicaId)
|
|
3032
|
+
};
|
|
3033
|
+
if (input.kind === "human") {
|
|
3034
|
+
assertExactKeys(input, ["kind", "replicaId", "subjectId"], "human live ticket actor");
|
|
3035
|
+
return Object.freeze({ kind: "human", ...common });
|
|
3036
|
+
}
|
|
3037
|
+
if (input.kind === "agent") {
|
|
3038
|
+
assertExactKeys(
|
|
3039
|
+
input,
|
|
3040
|
+
["attemptId", "generation", "kind", "replicaId", "sessionId", "subjectId", "turnId"],
|
|
3041
|
+
"agent live ticket actor"
|
|
3042
|
+
);
|
|
3043
|
+
return Object.freeze({
|
|
3044
|
+
kind: "agent",
|
|
3045
|
+
...common,
|
|
3046
|
+
sessionId: validateIdentity(input.sessionId, "agent session id"),
|
|
3047
|
+
turnId: validateIdentity(input.turnId, "agent turn id"),
|
|
3048
|
+
attemptId: validateIdentity(input.attemptId, "agent attempt id"),
|
|
3049
|
+
generation: validateInteger(input.generation, "agent generation", 0, 2147483647)
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
if (input.kind === "service") {
|
|
3053
|
+
assertExactKeys(
|
|
3054
|
+
input,
|
|
3055
|
+
["kind", "replicaId", "service", "subjectId"],
|
|
3056
|
+
"service live ticket actor"
|
|
3057
|
+
);
|
|
3058
|
+
return Object.freeze({
|
|
3059
|
+
kind: "service",
|
|
3060
|
+
...common,
|
|
3061
|
+
service: validateIdentity(input.service, "service name")
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
throw new TypeError("Live ticket actor kind is invalid");
|
|
3065
|
+
}
|
|
3066
|
+
function persistedEditableArtifactActorKey(actor) {
|
|
3067
|
+
switch (actor.kind) {
|
|
3068
|
+
case "human":
|
|
3069
|
+
return JSON.stringify([actor.kind, actor.subjectId]);
|
|
3070
|
+
case "agent":
|
|
3071
|
+
return JSON.stringify([
|
|
3072
|
+
actor.kind,
|
|
3073
|
+
actor.subjectId,
|
|
3074
|
+
actor.sessionId,
|
|
3075
|
+
actor.turnId,
|
|
3076
|
+
actor.attemptId,
|
|
3077
|
+
actor.generation
|
|
3078
|
+
]);
|
|
3079
|
+
case "service":
|
|
3080
|
+
return JSON.stringify([actor.kind, actor.subjectId, actor.service]);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
async function transactionallyAuthorizeEditableArtifactActor(tx, input) {
|
|
3084
|
+
const actor = input.actor;
|
|
3085
|
+
const rows = await rawRows(
|
|
3086
|
+
tx,
|
|
3087
|
+
sql`select * from opengeni_private.authorize_editable_artifact_actor(
|
|
3088
|
+
${input.scope.accountId}::uuid,
|
|
3089
|
+
${input.scope.workspaceId}::uuid,
|
|
3090
|
+
${input.artifactId},
|
|
3091
|
+
${actor.kind},
|
|
3092
|
+
${actor.subjectId},
|
|
3093
|
+
${actor.kind === "agent" ? actor.sessionId : null},
|
|
3094
|
+
${actor.kind === "agent" ? actor.turnId : null},
|
|
3095
|
+
${actor.kind === "agent" ? actor.attemptId : null},
|
|
3096
|
+
${actor.kind === "agent" ? actor.generation : null},
|
|
3097
|
+
${actor.kind === "service" ? actor.service : null},
|
|
3098
|
+
${input.permission},
|
|
3099
|
+
current_schema()
|
|
3100
|
+
)`
|
|
3101
|
+
);
|
|
3102
|
+
const row = rows[0];
|
|
3103
|
+
if (rows.length !== 1 || !row) {
|
|
3104
|
+
throw corrupt("Editable artifact authorization returned no exact decision");
|
|
3105
|
+
}
|
|
3106
|
+
return Object.freeze({
|
|
3107
|
+
allowed: row.allowed === true,
|
|
3108
|
+
revision: safeInteger(row.authorization_revision, "authorization revision", 1)
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
function validateLiveTicketModality(input) {
|
|
3112
|
+
if (input !== "spreadsheet" && input !== "presentation" && input !== "document") {
|
|
3113
|
+
throw new TypeError("Live ticket modality is invalid");
|
|
3114
|
+
}
|
|
3115
|
+
return input;
|
|
3116
|
+
}
|
|
3117
|
+
function liveTicketFromRow(row) {
|
|
3118
|
+
let actor;
|
|
3119
|
+
if (row.actor_kind === "human") {
|
|
3120
|
+
if (row.agent_session_id !== null || row.agent_turn_id !== null || row.agent_attempt_id !== null || row.agent_generation !== null || row.service_name !== null) {
|
|
3121
|
+
throw corrupt("Human live ticket has non-human authority fields");
|
|
3122
|
+
}
|
|
3123
|
+
actor = { kind: "human", subjectId: row.actor_subject_id, replicaId: row.replica_id };
|
|
3124
|
+
} else if (row.actor_kind === "agent") {
|
|
3125
|
+
if (row.agent_session_id === null || row.agent_turn_id === null || row.agent_attempt_id === null || row.agent_generation === null || row.service_name !== null) {
|
|
3126
|
+
throw corrupt("Agent live ticket authority fields are incomplete");
|
|
3127
|
+
}
|
|
3128
|
+
actor = {
|
|
3129
|
+
kind: "agent",
|
|
3130
|
+
subjectId: row.actor_subject_id,
|
|
3131
|
+
replicaId: row.replica_id,
|
|
3132
|
+
sessionId: row.agent_session_id,
|
|
3133
|
+
turnId: row.agent_turn_id,
|
|
3134
|
+
attemptId: row.agent_attempt_id,
|
|
3135
|
+
generation: row.agent_generation
|
|
3136
|
+
};
|
|
3137
|
+
} else if (row.actor_kind === "service") {
|
|
3138
|
+
if (row.agent_session_id !== null || row.agent_turn_id !== null || row.agent_attempt_id !== null || row.agent_generation !== null || row.service_name === null) {
|
|
3139
|
+
throw corrupt("Service live ticket authority fields are incomplete");
|
|
3140
|
+
}
|
|
3141
|
+
actor = {
|
|
3142
|
+
kind: "service",
|
|
3143
|
+
subjectId: row.actor_subject_id,
|
|
3144
|
+
replicaId: row.replica_id,
|
|
3145
|
+
service: row.service_name
|
|
3146
|
+
};
|
|
3147
|
+
} else {
|
|
3148
|
+
throw corrupt("Live ticket actor kind is invalid");
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
return validateLiveTicketRecord({
|
|
3152
|
+
tokenDigest: row.token_digest,
|
|
3153
|
+
scope: { accountId: row.account_id, workspaceId: row.workspace_id },
|
|
3154
|
+
artifactId: row.artifact_id,
|
|
3155
|
+
modality: validateLiveTicketModality(row.modality),
|
|
3156
|
+
actor,
|
|
3157
|
+
allowEdit: row.allow_edit,
|
|
3158
|
+
protocolVersion: row.protocol_version,
|
|
3159
|
+
issuedAt: iso(row.issued_at, "live ticket issuedAt"),
|
|
3160
|
+
expiresAt: iso(row.expires_at, "live ticket expiresAt")
|
|
3161
|
+
});
|
|
3162
|
+
} catch (error) {
|
|
3163
|
+
throw corrupt(
|
|
3164
|
+
`Live ticket record is corrupt: ${error instanceof Error ? error.message : "unknown error"}`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
function validateStableId(value, label) {
|
|
3169
|
+
if (typeof value !== "string" || !/^[0-9a-f]{32}$/.test(value) || /^0+$/.test(value)) {
|
|
3170
|
+
throw new TypeError(`${label} must be fixed-width lowercase nonzero hexadecimal text`);
|
|
3171
|
+
}
|
|
3172
|
+
return value;
|
|
3173
|
+
}
|
|
3174
|
+
function validateUuid(value, label) {
|
|
3175
|
+
if (typeof value !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value)) {
|
|
3176
|
+
throw new TypeError(`${label} must be a canonical UUID`);
|
|
3177
|
+
}
|
|
3178
|
+
return value;
|
|
3179
|
+
}
|
|
3180
|
+
function validateOriginOperation(value) {
|
|
3181
|
+
if (value !== "create" && value !== "import") {
|
|
3182
|
+
throw new TypeError("Editable artifact origin operation is invalid");
|
|
3183
|
+
}
|
|
3184
|
+
return value;
|
|
3185
|
+
}
|
|
3186
|
+
function validateOriginalImport(input, modality) {
|
|
3187
|
+
if (typeof input.fileId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(input.fileId)) {
|
|
3188
|
+
throw new TypeError("Original import file id must be a canonical UUID");
|
|
3189
|
+
}
|
|
3190
|
+
const expectedMimeType = modality === "spreadsheet" ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : modality === "document" ? "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
3191
|
+
if (input.mimeType !== expectedMimeType) {
|
|
3192
|
+
throw new TypeError("Original import MIME type does not match artifact modality");
|
|
3193
|
+
}
|
|
3194
|
+
return Object.freeze({
|
|
3195
|
+
fileId: input.fileId,
|
|
3196
|
+
blobRefId: validateStableId(input.blobRefId, "original import blob id"),
|
|
3197
|
+
blobReference: validateBoundedText(input.blobReference, "original import blob reference", 1024),
|
|
3198
|
+
byteSize: validateInteger(
|
|
3199
|
+
input.byteSize,
|
|
3200
|
+
"original import byte size",
|
|
3201
|
+
1,
|
|
3202
|
+
EDITABLE_ARTIFACT_ORIGINAL_IMPORT_MAX_BYTES
|
|
3203
|
+
),
|
|
3204
|
+
contentHash: validateHash(input.contentHash, "original import content hash"),
|
|
3205
|
+
mimeType: expectedMimeType
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
function validateReplicaId(value) {
|
|
3209
|
+
if (typeof value !== "string" || !/^[0-9a-f]{16}$/.test(value) || /^0+$/.test(value)) {
|
|
3210
|
+
throw new TypeError("Replica id must be fixed-width lowercase nonzero hexadecimal text");
|
|
3211
|
+
}
|
|
3212
|
+
return value;
|
|
3213
|
+
}
|
|
3214
|
+
function validateBoolean(value, label) {
|
|
3215
|
+
if (typeof value !== "boolean") throw new TypeError(`${label} must be boolean`);
|
|
3216
|
+
return value;
|
|
3217
|
+
}
|
|
3218
|
+
function validateHash(value, label) {
|
|
3219
|
+
if (typeof value !== "string" || !/^sha256:[0-9a-f]{64}$/.test(value)) {
|
|
3220
|
+
throw new TypeError(`${label} must be a canonical SHA-256 digest`);
|
|
3221
|
+
}
|
|
3222
|
+
return value;
|
|
3223
|
+
}
|
|
3224
|
+
function validateCanonicalIntentBytes(value, requestHashInput) {
|
|
3225
|
+
if (!(value instanceof Uint8Array)) {
|
|
3226
|
+
throw new TypeError("Canonical transaction intent must be a Uint8Array");
|
|
3227
|
+
}
|
|
3228
|
+
const bytes = value.slice();
|
|
3229
|
+
if (bytes.byteLength < 8 || bytes.byteLength > EDITABLE_ARTIFACT_INTENT_MAX_BYTES) {
|
|
3230
|
+
throw new TypeError("Canonical transaction intent must contain 8 bytes through 5 MiB");
|
|
3231
|
+
}
|
|
3232
|
+
const magic = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, 8));
|
|
3233
|
+
if (magic !== "OGATX001") {
|
|
3234
|
+
throw new TypeError("Canonical transaction intent has an unsupported protocol magic");
|
|
3235
|
+
}
|
|
3236
|
+
const requestHash = validateHash(requestHashInput, "request hash");
|
|
3237
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
3238
|
+
if (digest !== requestHash) {
|
|
3239
|
+
throw new TypeError("Canonical transaction intent digest does not match request hash");
|
|
3240
|
+
}
|
|
3241
|
+
return bytes;
|
|
3242
|
+
}
|
|
3243
|
+
function validateIntentProtocolVersion(value) {
|
|
3244
|
+
if (value !== 1) throw new TypeError("Intent protocol version must be 1 for OGATX001");
|
|
3245
|
+
return 1;
|
|
3246
|
+
}
|
|
3247
|
+
function validateClientTransactionId(value) {
|
|
3248
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 200 || !/^[A-Za-z0-9._:-]+$/.test(value)) {
|
|
3249
|
+
throw new TypeError("Client transaction id is invalid");
|
|
3250
|
+
}
|
|
3251
|
+
return value;
|
|
3252
|
+
}
|
|
3253
|
+
function validateIdentity(value, label) {
|
|
3254
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 256 || value.trim() !== value) {
|
|
3255
|
+
throw new TypeError(`${label} must contain 1-256 non-padding characters`);
|
|
3256
|
+
}
|
|
3257
|
+
return value;
|
|
3258
|
+
}
|
|
3259
|
+
function validateLeaseOwner(value) {
|
|
3260
|
+
return validateIdentity(value, "outbox lease owner");
|
|
3261
|
+
}
|
|
3262
|
+
function validateOutboxRetryErrorCode(value) {
|
|
3263
|
+
if (value !== "broker_unavailable" && value !== "broker_backpressure" && value !== "publish_timeout") {
|
|
3264
|
+
throw new TypeError("Outbox retry error code is invalid");
|
|
3265
|
+
}
|
|
3266
|
+
return value;
|
|
3267
|
+
}
|
|
3268
|
+
function validateOutboxDeadLetterErrorCode(value) {
|
|
3269
|
+
if (value !== "invalid_hint" && value !== "oversized_hint") {
|
|
3270
|
+
throw new TypeError("Outbox dead-letter error code is invalid");
|
|
3271
|
+
}
|
|
3272
|
+
return value;
|
|
3273
|
+
}
|
|
3274
|
+
function validateOutboxStoredErrorCode(value) {
|
|
3275
|
+
if (isOutboxRetryErrorCode(value) || isOutboxStoredDeadLetterErrorCode(value)) return value;
|
|
3276
|
+
throw corrupt("Outbox error code is invalid");
|
|
3277
|
+
}
|
|
3278
|
+
function isOutboxRetryErrorCode(value) {
|
|
3279
|
+
return value === "broker_unavailable" || value === "broker_backpressure" || value === "publish_timeout";
|
|
3280
|
+
}
|
|
3281
|
+
function isOutboxStoredDeadLetterErrorCode(value) {
|
|
3282
|
+
return value === "invalid_hint" || value === "oversized_hint" || value === "attempts_exhausted";
|
|
3283
|
+
}
|
|
3284
|
+
function validateBoundedText(value, label, maxBytes) {
|
|
3285
|
+
if (typeof value !== "string" || value.trim() !== value)
|
|
3286
|
+
throw new TypeError(`${label} is invalid`);
|
|
3287
|
+
const bytes = new TextEncoder().encode(value).byteLength;
|
|
3288
|
+
if (bytes < 1 || bytes > maxBytes) throw new TypeError(`${label} exceeds its byte limit`);
|
|
3289
|
+
return value;
|
|
3290
|
+
}
|
|
3291
|
+
function validateTitle(value) {
|
|
3292
|
+
validateBoundedText(value, "artifact title", 512);
|
|
3293
|
+
}
|
|
3294
|
+
function artifactModality(value, label) {
|
|
3295
|
+
if (value !== "spreadsheet" && value !== "document" && value !== "presentation") {
|
|
3296
|
+
throw corrupt(`${label} is invalid`);
|
|
3297
|
+
}
|
|
3298
|
+
return value;
|
|
3299
|
+
}
|
|
3300
|
+
function serializedModality(value, label) {
|
|
3301
|
+
if (value !== "document" && value !== "presentation") {
|
|
3302
|
+
throw corrupt(`${label} is not a serialized modality`);
|
|
3303
|
+
}
|
|
3304
|
+
return value;
|
|
3305
|
+
}
|
|
3306
|
+
function equalByteArrays(left, right) {
|
|
3307
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
3308
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
3309
|
+
if (left[index] !== right[index]) return false;
|
|
3310
|
+
}
|
|
3311
|
+
return true;
|
|
3312
|
+
}
|
|
3313
|
+
function assertSerializedColumnsAbsent(row) {
|
|
3314
|
+
if (row.commit_protocol_version !== null || row.prior_native_revision !== null || row.native_revision !== null || row.command_count !== null || row.native_receipt_byte_size !== null || row.native_receipt_hash !== null || row.native_receipt_bytes !== null) {
|
|
3315
|
+
throw corrupt("Spreadsheet transaction contains serialized projection fields");
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
function readNativeReceiptBytes(row) {
|
|
3319
|
+
if (!(row.native_receipt_bytes instanceof Uint8Array)) {
|
|
3320
|
+
throw corrupt("Serialized native receipt bytes are missing");
|
|
3321
|
+
}
|
|
3322
|
+
const bytes = Uint8Array.from(row.native_receipt_bytes);
|
|
3323
|
+
const byteSize = validateInteger(
|
|
3324
|
+
row.native_receipt_byte_size,
|
|
3325
|
+
"serialized native receipt byte size",
|
|
3326
|
+
1,
|
|
3327
|
+
EDITABLE_ARTIFACT_NATIVE_RECEIPT_MAX_BYTES
|
|
3328
|
+
);
|
|
3329
|
+
if (bytes.byteLength !== byteSize) {
|
|
3330
|
+
throw corrupt("Serialized native receipt size disagrees with its bytes");
|
|
3331
|
+
}
|
|
3332
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
3333
|
+
if (digest !== validateHash(row.native_receipt_hash, "serialized native receipt hash")) {
|
|
3334
|
+
throw corrupt("Serialized native receipt digest verification failed");
|
|
3335
|
+
}
|
|
3336
|
+
return bytes;
|
|
3337
|
+
}
|
|
3338
|
+
function validateInteger(value, label, min, max = Number.MAX_SAFE_INTEGER) {
|
|
3339
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
3340
|
+
throw new TypeError(`${label} must be a safe integer from ${min} through ${max}`);
|
|
3341
|
+
}
|
|
3342
|
+
return value;
|
|
3343
|
+
}
|
|
3344
|
+
function safeInteger(value, label, min = 0) {
|
|
3345
|
+
if (value === null) throw corrupt(`${label} is missing`);
|
|
3346
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
3347
|
+
return validateInteger(number, label, min);
|
|
3348
|
+
}
|
|
3349
|
+
function validateDate(value, label) {
|
|
3350
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
3351
|
+
throw new TypeError(`${label} is invalid`);
|
|
3352
|
+
return new Date(value.getTime());
|
|
3353
|
+
}
|
|
3354
|
+
function canonicalIsoString(value, label) {
|
|
3355
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) {
|
|
3356
|
+
throw new TypeError(`${label} must be a canonical UTC timestamp`);
|
|
3357
|
+
}
|
|
3358
|
+
return value;
|
|
3359
|
+
}
|
|
3360
|
+
function iso(value, label) {
|
|
3361
|
+
if (value instanceof Date) return validateDate(value, label).toISOString();
|
|
3362
|
+
const date = new Date(value);
|
|
3363
|
+
if (!Number.isFinite(date.getTime())) throw corrupt(`${label} is invalid`);
|
|
3364
|
+
return date.toISOString();
|
|
3365
|
+
}
|
|
3366
|
+
function json(value) {
|
|
3367
|
+
return JSON.stringify(value);
|
|
3368
|
+
}
|
|
3369
|
+
function isPlainRecord(value) {
|
|
3370
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3371
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3372
|
+
return prototype === Object.prototype || prototype === null;
|
|
3373
|
+
}
|
|
3374
|
+
function assertExactKeys(value, expected, label) {
|
|
3375
|
+
const keys = Object.keys(value).sort();
|
|
3376
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
|
3377
|
+
throw new TypeError(`${label} contains unknown or missing fields`);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function sameFrontier(left, right) {
|
|
3381
|
+
return left.length === right.length && left.every(
|
|
3382
|
+
(entry, index) => entry.replicaId === right[index]?.replicaId && entry.counter === right[index]?.counter
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
function causalCounter(frontier, replicaId) {
|
|
3386
|
+
return frontier.find((entry) => entry.replicaId === replicaId)?.counter ?? 0;
|
|
3387
|
+
}
|
|
3388
|
+
function frontierDominates(candidate, required) {
|
|
3389
|
+
return required.every((entry) => causalCounter(candidate, entry.replicaId) >= entry.counter);
|
|
3390
|
+
}
|
|
3391
|
+
function mergeFrontier(frontier, entry) {
|
|
3392
|
+
return mergeFrontiers(frontier, [entry]);
|
|
3393
|
+
}
|
|
3394
|
+
function mergeFrontiers(...frontiers) {
|
|
3395
|
+
const counters = /* @__PURE__ */ new Map();
|
|
3396
|
+
for (const frontier of frontiers) {
|
|
3397
|
+
for (const entry of frontier) {
|
|
3398
|
+
counters.set(entry.replicaId, Math.max(counters.get(entry.replicaId) ?? 0, entry.counter));
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
return Object.freeze(
|
|
3402
|
+
[...counters].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([replicaId, counter]) => Object.freeze({ replicaId, counter }))
|
|
3403
|
+
);
|
|
3404
|
+
}
|
|
3405
|
+
function cloneFrontier(frontier) {
|
|
3406
|
+
return Object.freeze(frontier.map((entry) => Object.freeze({ ...entry })));
|
|
3407
|
+
}
|
|
3408
|
+
function cloneReceipt(receipt) {
|
|
3409
|
+
return receipt.modality === "spreadsheet" ? Object.freeze({
|
|
3410
|
+
...receipt,
|
|
3411
|
+
scope: Object.freeze({ ...receipt.scope }),
|
|
3412
|
+
intentBytes: receipt.intentBytes.slice(),
|
|
3413
|
+
modality: "spreadsheet",
|
|
3414
|
+
causalBase: cloneFrontier(receipt.causalBase),
|
|
3415
|
+
resolvedCausalBase: cloneFrontier(receipt.resolvedCausalBase),
|
|
3416
|
+
resultingCausalFrontier: cloneFrontier(receipt.resultingCausalFrontier),
|
|
3417
|
+
selectiveUndoOperationIds: Object.freeze([...receipt.selectiveUndoOperationIds])
|
|
3418
|
+
}) : Object.freeze({
|
|
3419
|
+
...receipt,
|
|
3420
|
+
scope: Object.freeze({ ...receipt.scope }),
|
|
3421
|
+
intentBytes: receipt.intentBytes.slice(),
|
|
3422
|
+
modality: receipt.modality
|
|
3423
|
+
});
|
|
3424
|
+
}
|
|
3425
|
+
function cloneOperation(operation) {
|
|
3426
|
+
return Object.freeze({
|
|
3427
|
+
...operation,
|
|
3428
|
+
scope: Object.freeze({ ...operation.scope }),
|
|
3429
|
+
dot: Object.freeze({ ...operation.dot })
|
|
3430
|
+
});
|
|
3431
|
+
}
|
|
3432
|
+
function cloneCommittedTransaction(transaction) {
|
|
3433
|
+
if (!(transaction.committedTransactionBytes instanceof Uint8Array)) {
|
|
3434
|
+
throw new TypeError("Canonical committed transaction bytes must be a Uint8Array");
|
|
3435
|
+
}
|
|
3436
|
+
if (transaction.modality === "spreadsheet") {
|
|
3437
|
+
decodeCommittedTransactionSummary(transaction.committedTransactionBytes);
|
|
3438
|
+
return Object.freeze({
|
|
3439
|
+
...transaction,
|
|
3440
|
+
scope: Object.freeze({ ...transaction.scope }),
|
|
3441
|
+
dot: Object.freeze({ ...transaction.dot }),
|
|
3442
|
+
resolvedCausalBase: cloneFrontier(transaction.resolvedCausalBase),
|
|
3443
|
+
resultingCausalFrontier: cloneFrontier(transaction.resultingCausalFrontier),
|
|
3444
|
+
operationIds: Object.freeze([...transaction.operationIds]),
|
|
3445
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
if (!(transaction.nativeReceiptBytes instanceof Uint8Array)) {
|
|
3449
|
+
throw new TypeError("Serialized native receipt bytes must be a Uint8Array");
|
|
3450
|
+
}
|
|
3451
|
+
decodeEditableArtifactSerializedCommit(
|
|
3452
|
+
transaction.committedTransactionBytes,
|
|
3453
|
+
transaction.modality
|
|
3454
|
+
);
|
|
3455
|
+
return Object.freeze({
|
|
3456
|
+
...transaction,
|
|
3457
|
+
scope: Object.freeze({ ...transaction.scope }),
|
|
3458
|
+
nativeReceiptBytes: transaction.nativeReceiptBytes.slice(),
|
|
3459
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
3462
|
+
function cloneOutbox(outbox) {
|
|
3463
|
+
return Object.freeze({
|
|
3464
|
+
...outbox,
|
|
3465
|
+
event: parseLiveEvent(outbox.event)
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
function sameStringArray(left, right) {
|
|
3469
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
3470
|
+
}
|
|
3471
|
+
function cloneArtifact(artifact) {
|
|
3472
|
+
return artifact.modality === "spreadsheet" ? Object.freeze({
|
|
3473
|
+
...artifact,
|
|
3474
|
+
scope: Object.freeze({ ...artifact.scope }),
|
|
3475
|
+
causalFrontier: cloneFrontier(artifact.causalFrontier)
|
|
3476
|
+
}) : Object.freeze({
|
|
3477
|
+
...artifact,
|
|
3478
|
+
scope: Object.freeze({ ...artifact.scope }),
|
|
3479
|
+
modality: artifact.modality
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
function corrupt(message) {
|
|
3483
|
+
return new EditableArtifactPersistenceError("corrupt_history", message);
|
|
3484
|
+
}
|
|
3485
|
+
function conflict(message) {
|
|
3486
|
+
return new EditableArtifactPersistenceError("conflict", message);
|
|
3487
|
+
}
|
|
3488
|
+
function outboxLeaseConflict() {
|
|
3489
|
+
return new EditableArtifactPersistenceError(
|
|
3490
|
+
"outbox_lease_conflict",
|
|
3491
|
+
"Live outbox record is not leased by this publisher"
|
|
3492
|
+
);
|
|
3493
|
+
}
|
|
3494
|
+
function mapPersistenceError(error, message) {
|
|
3495
|
+
if (error instanceof EditableArtifactPersistenceError) return error;
|
|
3496
|
+
const code = errorCode(error);
|
|
3497
|
+
if (code === "23503" || code === "23505" || code === "23514" || code === "23P01") {
|
|
3498
|
+
return new EditableArtifactPersistenceError("conflict", message, { cause: error });
|
|
3499
|
+
}
|
|
3500
|
+
if (code === "40001" || code === "40P01" || code === "55P03" || code === "57014" || code.startsWith("08") || code === "57P01" || code === "57P02" || code === "57P03") {
|
|
3501
|
+
return new EditableArtifactPersistenceError("transient", message, { cause: error });
|
|
3502
|
+
}
|
|
3503
|
+
return error instanceof Error ? error : new Error(message, { cause: error });
|
|
3504
|
+
}
|
|
3505
|
+
function errorCode(error) {
|
|
3506
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3507
|
+
let current = error;
|
|
3508
|
+
for (let depth = 0; depth < 8 && current !== null && current !== void 0; depth += 1) {
|
|
3509
|
+
if (seen.has(current)) break;
|
|
3510
|
+
seen.add(current);
|
|
3511
|
+
const value = objectProperty(current, "code");
|
|
3512
|
+
if (typeof value === "string" && /^[0-9A-Z]{5}$/.test(value)) return value;
|
|
3513
|
+
current = objectProperty(current, "cause");
|
|
3514
|
+
}
|
|
3515
|
+
return "";
|
|
3516
|
+
}
|
|
3517
|
+
function objectProperty(value, key) {
|
|
3518
|
+
if (typeof value !== "object" && typeof value !== "function" || value === null) {
|
|
3519
|
+
return void 0;
|
|
3520
|
+
}
|
|
3521
|
+
return Reflect.get(value, key);
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
export {
|
|
3525
|
+
EditableArtifactPersistenceError,
|
|
3526
|
+
touchEditableArtifactSessionLink,
|
|
3527
|
+
listEditableArtifactIdsForSession,
|
|
3528
|
+
EDITABLE_ARTIFACT_INTENT_MAX_BYTES,
|
|
3529
|
+
EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_TRANSACTIONS,
|
|
3530
|
+
EDITABLE_ARTIFACT_KERNEL_TAIL_MAX_BYTES,
|
|
3531
|
+
PostgresEditableArtifactLiveTicketStore,
|
|
3532
|
+
PostgresEditableArtifactStore,
|
|
3533
|
+
PostgresEditableArtifactLiveReadStore
|
|
3534
|
+
};
|
|
3535
|
+
//# sourceMappingURL=chunk-F5FT7BEZ.js.map
|