@opengeni/core 0.21.10 → 0.24.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/access/index.d.ts +1 -1
- package/dist/application/new-session-drafts.d.ts +1 -1
- package/dist/application/session-commands.d.ts +2 -2
- package/dist/billing/limits.d.ts +1 -1
- package/dist/chunk-6WKPWE5S.js +2380 -0
- package/dist/chunk-6WKPWE5S.js.map +1 -0
- package/dist/chunk-JJU6I5XD.js +2713 -0
- package/dist/chunk-JJU6I5XD.js.map +1 -0
- package/dist/chunk-NYPMQ7NO.js +2270 -0
- package/dist/chunk-NYPMQ7NO.js.map +1 -0
- package/dist/dependencies.d.ts +26 -6
- package/dist/domain/capabilities.d.ts +28 -3
- package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
- package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
- package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
- package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
- package/dist/domain/editable-artifacts/errors.d.ts +47 -0
- package/dist/domain/editable-artifacts/hash.d.ts +11 -0
- package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
- package/dist/domain/editable-artifacts/index.d.ts +14 -0
- package/dist/domain/editable-artifacts/ports.d.ts +341 -0
- package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
- package/dist/domain/editable-artifacts/service.d.ts +105 -0
- package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
- package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
- package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
- package/dist/domain/editable-artifacts/types.d.ts +410 -0
- package/dist/domain/memory-slack-delivery.d.ts +19 -0
- package/dist/domain/packs.d.ts +29 -1
- package/dist/domain/personal-connection-delegations.d.ts +9 -1
- package/dist/domain/scheduled-tasks.d.ts +2 -2
- package/dist/domain/session-tool-policy.d.ts +2 -2
- package/dist/domain/sessions.d.ts +14 -6
- package/dist/domain/skill-imports.d.ts +32 -0
- package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
- package/dist/domain/timeline-annotations.d.ts +5 -0
- package/dist/domain/video-generation-capabilities.d.ts +9 -0
- package/dist/domain/video-generation.d.ts +47 -0
- package/dist/editable-artifact-live/application.d.ts +69 -0
- package/dist/editable-artifact-live/errors.d.ts +1 -0
- package/dist/editable-artifact-live/index.d.ts +8 -0
- package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
- package/dist/editable-artifact-live/ports.d.ts +100 -0
- package/dist/editable-artifact-live/server.d.ts +71 -0
- package/dist/editable-artifact-live/ticket.d.ts +40 -0
- package/dist/editable-artifact-live/types.d.ts +213 -0
- package/dist/editable-artifact-live/wire.d.ts +34 -0
- package/dist/editable-artifact-live.d.ts +2 -0
- package/dist/editable-artifact-live.js +58 -0
- package/dist/editable-artifact-live.js.map +1 -0
- package/dist/editable-artifacts.d.ts +2 -0
- package/dist/editable-artifacts.js +179 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/index.d.ts +35 -26
- package/dist/index.js +2792 -609
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +1 -1
- package/dist/rigs/index.d.ts +1 -1
- package/dist/rigs/provider-images.d.ts +31 -0
- package/dist/sandbox/routing.d.ts +1 -0
- package/dist/session-authorization.d.ts +1 -1
- package/dist/workflow-wake-contract.d.ts +7 -0
- package/package.json +18 -10
- package/src/application/new-session-drafts.ts +2 -0
- package/src/application/session-commands.ts +188 -107
- package/src/dependencies.ts +30 -2
- package/src/domain/capabilities.ts +604 -220
- package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
- package/src/domain/durable-learning-slack-publication.ts +202 -0
- package/src/domain/editable-artifacts/agent-application.ts +687 -0
- package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
- package/src/domain/editable-artifacts/durable-export.ts +734 -0
- package/src/domain/editable-artifacts/errors.ts +115 -0
- package/src/domain/editable-artifacts/hash.ts +131 -0
- package/src/domain/editable-artifacts/in-memory.ts +1662 -0
- package/src/domain/editable-artifacts/index.ts +14 -0
- package/src/domain/editable-artifacts/ports.ts +462 -0
- package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
- package/src/domain/editable-artifacts/service.ts +2402 -0
- package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
- package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
- package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
- package/src/domain/editable-artifacts/types.ts +835 -0
- package/src/domain/insights.ts +136 -33
- package/src/domain/memory-slack-delivery.ts +204 -0
- package/src/domain/memory-slack-publication.ts +5 -2
- package/src/domain/packs.ts +266 -5
- package/src/domain/personal-connection-delegations.ts +97 -6
- package/src/domain/scheduled-tasks.ts +155 -29
- package/src/domain/session-tool-policy.ts +8 -2
- package/src/domain/sessions.ts +274 -131
- package/src/domain/skill-imports.ts +350 -0
- package/src/domain/slack-publication-secret-safety.ts +42 -0
- package/src/domain/timeline-annotations.ts +204 -0
- package/src/domain/video-generation-capabilities.ts +69 -0
- package/src/domain/video-generation.ts +249 -0
- package/src/editable-artifact-live/application.ts +182 -0
- package/src/editable-artifact-live/errors.ts +4 -0
- package/src/editable-artifact-live/index.ts +8 -0
- package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
- package/src/editable-artifact-live/ports.ts +139 -0
- package/src/editable-artifact-live/server.ts +1707 -0
- package/src/editable-artifact-live/ticket.ts +217 -0
- package/src/editable-artifact-live/types.ts +286 -0
- package/src/editable-artifact-live/wire.ts +95 -0
- package/src/editable-artifact-live.ts +2 -0
- package/src/editable-artifacts.ts +2 -0
- package/src/index.ts +13 -0
- package/src/rigs/index.ts +26 -19
- package/src/rigs/provider-images.ts +110 -0
- package/src/sandbox/routing.ts +6 -3
- package/src/session-authorization.ts +3 -0
- package/src/workflow-wake-contract.ts +8 -0
|
@@ -0,0 +1,2380 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EditableArtifactDomainError,
|
|
3
|
+
EditableArtifactIdempotencyConflictError,
|
|
4
|
+
EditableArtifactNotFoundError,
|
|
5
|
+
EditableArtifactOutboxLeaseConflictError,
|
|
6
|
+
EditableArtifactSnapshotVerificationError,
|
|
7
|
+
EditableArtifactStaleBaseError,
|
|
8
|
+
MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES,
|
|
9
|
+
assertBoundedArtifactTitle,
|
|
10
|
+
assertBoundedKernelVersion,
|
|
11
|
+
assertIsoTimestamp,
|
|
12
|
+
assertNonnegativeSafeInteger,
|
|
13
|
+
assertPositiveSafeInteger,
|
|
14
|
+
causalFrontiersEqual,
|
|
15
|
+
editableArtifactActorKey,
|
|
16
|
+
editableArtifactCausalFrontier,
|
|
17
|
+
editableArtifactClientTransactionId,
|
|
18
|
+
editableArtifactContentHash,
|
|
19
|
+
editableArtifactId,
|
|
20
|
+
editableArtifactOperationId,
|
|
21
|
+
editableArtifactOutboxId,
|
|
22
|
+
editableArtifactReceiptId,
|
|
23
|
+
editableArtifactRequestHash,
|
|
24
|
+
editableArtifactScope,
|
|
25
|
+
editableArtifactSnapshotId,
|
|
26
|
+
editableArtifactStateHash,
|
|
27
|
+
validateEditableArtifactActor
|
|
28
|
+
} from "./chunk-JJU6I5XD.js";
|
|
29
|
+
|
|
30
|
+
// src/domain/editable-artifacts/durable-export.ts
|
|
31
|
+
import { createHash } from "crypto";
|
|
32
|
+
var EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
|
|
33
|
+
var EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES = 256 * 1024;
|
|
34
|
+
var EditableArtifactDurableExportError = class extends Error {
|
|
35
|
+
constructor(code) {
|
|
36
|
+
super(code);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.name = "EditableArtifactDurableExportError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var MAX_AUTHORIZATION_RETRIES = 4;
|
|
42
|
+
var PORTABLE_ID = /^[0-9a-f]{32}$/u;
|
|
43
|
+
var IDEMPOTENCY_KEY = /^[A-Za-z0-9._:-]{1,200}$/u;
|
|
44
|
+
var SHA256 = /^sha256:[0-9a-f]{64}$/u;
|
|
45
|
+
var PROFILE_TEXT = /^[\x21-\x7e]+$/u;
|
|
46
|
+
var EditableArtifactDurableExportService = class {
|
|
47
|
+
constructor(dependencies) {
|
|
48
|
+
this.dependencies = dependencies;
|
|
49
|
+
}
|
|
50
|
+
async pinVersion(input) {
|
|
51
|
+
const context = normalizeContext(input);
|
|
52
|
+
const idempotencyKey = portableIdempotencyKey(input.idempotencyKey);
|
|
53
|
+
const name = versionName(input.name);
|
|
54
|
+
const snapshot = await this.dependencies.exactSnapshots.ensure({
|
|
55
|
+
...context,
|
|
56
|
+
...input.signal ? { signal: input.signal } : {}
|
|
57
|
+
});
|
|
58
|
+
if (snapshot.scope.accountId !== context.scope.accountId || snapshot.scope.workspaceId !== context.scope.workspaceId || snapshot.artifactId !== context.artifactId) {
|
|
59
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
60
|
+
}
|
|
61
|
+
const requestHash = hashPublicRequest("pin-version", [idempotencyKey, name]);
|
|
62
|
+
const authorityKey = editableArtifactActorKey(context.actor);
|
|
63
|
+
const receiptId = stableId(this.dependencies.ids.next("receipt"), "receipt id");
|
|
64
|
+
const versionId = stableId(this.dependencies.ids.next("version"), "version id");
|
|
65
|
+
for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
|
|
66
|
+
const revision = await this.requireExportPermission(context);
|
|
67
|
+
const result = await this.dependencies.store.pinVersion({
|
|
68
|
+
...context,
|
|
69
|
+
expectedAuthorizationRevision: revision,
|
|
70
|
+
authorityKey,
|
|
71
|
+
receiptId,
|
|
72
|
+
versionId,
|
|
73
|
+
idempotencyKey,
|
|
74
|
+
requestHash,
|
|
75
|
+
name,
|
|
76
|
+
snapshot
|
|
77
|
+
});
|
|
78
|
+
if (result.kind === "result") {
|
|
79
|
+
return Object.freeze({ version: result.version, replayed: result.replayed });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw new EditableArtifactDurableExportError("conflict");
|
|
83
|
+
}
|
|
84
|
+
async enqueueMaterialization(input) {
|
|
85
|
+
const context = normalizeContext(input);
|
|
86
|
+
const idempotencyKey = portableIdempotencyKey(input.idempotencyKey);
|
|
87
|
+
const versionId = stableId(input.versionId, "version id");
|
|
88
|
+
const format = materializationFormat(input.format);
|
|
89
|
+
const options = exactOptions(input.options ?? Object.freeze({}));
|
|
90
|
+
const version = await this.getVersion(context, versionId);
|
|
91
|
+
const profile = await this.dependencies.profiles.resolve({
|
|
92
|
+
modality: version.modality,
|
|
93
|
+
format,
|
|
94
|
+
options
|
|
95
|
+
});
|
|
96
|
+
if (!profile) throw new EditableArtifactDurableExportError("unsupported_format");
|
|
97
|
+
validateProfile(profile, version.modality, format);
|
|
98
|
+
const requestHash = hashPublicRequest("materialize", [
|
|
99
|
+
idempotencyKey,
|
|
100
|
+
versionId,
|
|
101
|
+
format,
|
|
102
|
+
canonicalJson(options)
|
|
103
|
+
]);
|
|
104
|
+
const authorityKey = editableArtifactActorKey(context.actor);
|
|
105
|
+
const receiptId = stableId(this.dependencies.ids.next("receipt"), "receipt id");
|
|
106
|
+
const jobId = stableId(this.dependencies.ids.next("materialization_job"), "job id");
|
|
107
|
+
for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
|
|
108
|
+
const revision = await this.requireExportPermission(context);
|
|
109
|
+
const result = await this.dependencies.store.enqueueMaterialization({
|
|
110
|
+
...context,
|
|
111
|
+
expectedAuthorizationRevision: revision,
|
|
112
|
+
authorityKey,
|
|
113
|
+
receiptId,
|
|
114
|
+
jobId,
|
|
115
|
+
idempotencyKey,
|
|
116
|
+
requestHash,
|
|
117
|
+
versionId,
|
|
118
|
+
profile
|
|
119
|
+
});
|
|
120
|
+
if (result.kind === "result") {
|
|
121
|
+
return Object.freeze({ job: result.job, replayed: result.replayed });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new EditableArtifactDurableExportError("conflict");
|
|
125
|
+
}
|
|
126
|
+
async getMaterialization(input) {
|
|
127
|
+
const context = normalizeContext(input);
|
|
128
|
+
const jobId = stableId(input.jobId, "job id");
|
|
129
|
+
for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
|
|
130
|
+
const revision = await this.requireExportPermission(context);
|
|
131
|
+
const result = await this.dependencies.store.readMaterialization({
|
|
132
|
+
...context,
|
|
133
|
+
expectedAuthorizationRevision: revision,
|
|
134
|
+
jobId
|
|
135
|
+
});
|
|
136
|
+
if (result.kind === "authorization_stale") continue;
|
|
137
|
+
if (!result.job) throw new EditableArtifactDurableExportError("not_found");
|
|
138
|
+
return result.job;
|
|
139
|
+
}
|
|
140
|
+
throw new EditableArtifactDurableExportError("conflict");
|
|
141
|
+
}
|
|
142
|
+
async openMaterializationDownload(input) {
|
|
143
|
+
const context = normalizeContext(input);
|
|
144
|
+
const jobId = stableId(input.jobId, "job id");
|
|
145
|
+
for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
|
|
146
|
+
const revision = await this.requireExportPermission(context);
|
|
147
|
+
const result = await this.dependencies.store.readMaterializationDownload({
|
|
148
|
+
...context,
|
|
149
|
+
expectedAuthorizationRevision: revision,
|
|
150
|
+
jobId
|
|
151
|
+
});
|
|
152
|
+
if (result.kind === "authorization_stale") continue;
|
|
153
|
+
if (!result.job) throw new EditableArtifactDurableExportError("not_found");
|
|
154
|
+
const materialization = result.job.result;
|
|
155
|
+
if (result.job.state !== "succeeded" || !materialization || !result.objectReference) {
|
|
156
|
+
throw new EditableArtifactDurableExportError("not_ready");
|
|
157
|
+
}
|
|
158
|
+
let object;
|
|
159
|
+
try {
|
|
160
|
+
object = await this.dependencies.materializationObjects.open({
|
|
161
|
+
opaqueReference: result.objectReference,
|
|
162
|
+
maxBytes: EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES,
|
|
163
|
+
expectedByteSize: materialization.byteSize,
|
|
164
|
+
...input.signal ? { signal: input.signal } : {}
|
|
165
|
+
});
|
|
166
|
+
} catch {
|
|
167
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
168
|
+
}
|
|
169
|
+
if (object.contentType !== void 0 && object.contentType !== materialization.mimeType) {
|
|
170
|
+
await object.close();
|
|
171
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
172
|
+
}
|
|
173
|
+
return Object.freeze({
|
|
174
|
+
artifactId: context.artifactId,
|
|
175
|
+
jobId,
|
|
176
|
+
format: result.job.format,
|
|
177
|
+
byteSize: materialization.byteSize,
|
|
178
|
+
contentHash: materialization.contentHash,
|
|
179
|
+
mimeType: materialization.mimeType,
|
|
180
|
+
chunks: (chunkInput = {}) => object.chunks(chunkInput.signal ? { signal: chunkInput.signal } : {}),
|
|
181
|
+
assertUnchanged: async (signal) => await object.assertUnchanged(signal),
|
|
182
|
+
close: async () => await object.close()
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
throw new EditableArtifactDurableExportError("conflict");
|
|
186
|
+
}
|
|
187
|
+
async getVersion(context, versionId) {
|
|
188
|
+
for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
|
|
189
|
+
const revision = await this.requireExportPermission(context);
|
|
190
|
+
const result = await this.dependencies.store.readVersion({
|
|
191
|
+
...context,
|
|
192
|
+
expectedAuthorizationRevision: revision,
|
|
193
|
+
versionId
|
|
194
|
+
});
|
|
195
|
+
if (result.kind === "authorization_stale") continue;
|
|
196
|
+
if (!result.version) throw new EditableArtifactDurableExportError("not_found");
|
|
197
|
+
return result.version;
|
|
198
|
+
}
|
|
199
|
+
throw new EditableArtifactDurableExportError("conflict");
|
|
200
|
+
}
|
|
201
|
+
async requireExportPermission(context) {
|
|
202
|
+
const decision = await this.dependencies.authorization.authorize({
|
|
203
|
+
...context,
|
|
204
|
+
permission: "export"
|
|
205
|
+
});
|
|
206
|
+
if (!decision.allowed) throw new EditableArtifactDurableExportError("forbidden");
|
|
207
|
+
if (!Number.isSafeInteger(decision.revision) || decision.revision < 1) {
|
|
208
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
209
|
+
}
|
|
210
|
+
return decision.revision;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
function editableArtifactMaterializationMimeType(format) {
|
|
214
|
+
switch (format) {
|
|
215
|
+
case "xlsx":
|
|
216
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
217
|
+
case "pptx":
|
|
218
|
+
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
219
|
+
case "docx":
|
|
220
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
221
|
+
case "pdf":
|
|
222
|
+
return "application/pdf";
|
|
223
|
+
case "png":
|
|
224
|
+
return "image/png";
|
|
225
|
+
case "webp":
|
|
226
|
+
return "image/webp";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function editableArtifactFormatMatchesModality(modality, format) {
|
|
230
|
+
return modality === "spreadsheet" && ["xlsx", "pdf", "png", "webp"].includes(format) || modality === "presentation" && ["pptx", "pdf", "png", "webp"].includes(format) || modality === "document" && ["docx", "pdf", "png", "webp"].includes(format);
|
|
231
|
+
}
|
|
232
|
+
function normalizeContext(input) {
|
|
233
|
+
const scope = editableArtifactScope(input.scope);
|
|
234
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
235
|
+
validateEditableArtifactActor(input.actor);
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
scope,
|
|
238
|
+
artifactId,
|
|
239
|
+
actor: Object.freeze({ ...input.actor })
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function stableId(value, _label) {
|
|
243
|
+
if (!PORTABLE_ID.test(value) || /^0+$/u.test(value)) {
|
|
244
|
+
throw new EditableArtifactDurableExportError("invalid_request");
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
function portableIdempotencyKey(value) {
|
|
249
|
+
if (!IDEMPOTENCY_KEY.test(value)) {
|
|
250
|
+
throw new EditableArtifactDurableExportError("invalid_request");
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
function versionName(value) {
|
|
255
|
+
if (typeof value !== "string" || value.trim() !== value || /[\u0000-\u001f\u007f]/u.test(value) || new TextEncoder().encode(value).byteLength < 1 || new TextEncoder().encode(value).byteLength > 256 || !wellFormedText(value)) {
|
|
256
|
+
throw new EditableArtifactDurableExportError("invalid_request");
|
|
257
|
+
}
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
function materializationFormat(value) {
|
|
261
|
+
if (!["xlsx", "pptx", "docx", "pdf", "png", "webp"].includes(value)) {
|
|
262
|
+
throw new EditableArtifactDurableExportError("invalid_request");
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
function exactOptions(value) {
|
|
267
|
+
if (!plainRecord(value)) throw new EditableArtifactDurableExportError("invalid_request");
|
|
268
|
+
const canonical = canonicalJson(value);
|
|
269
|
+
if (new TextEncoder().encode(canonical).byteLength > EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES) {
|
|
270
|
+
throw new EditableArtifactDurableExportError("invalid_request");
|
|
271
|
+
}
|
|
272
|
+
return Object.freeze({ ...value });
|
|
273
|
+
}
|
|
274
|
+
function canonicalJson(value) {
|
|
275
|
+
const keys = Object.keys(value).sort(compareCodeUnits);
|
|
276
|
+
const normalized = {};
|
|
277
|
+
for (const key of keys) {
|
|
278
|
+
if (!wellFormedText(key) || !jsonValue(value[key])) {
|
|
279
|
+
throw new EditableArtifactDurableExportError("invalid_request");
|
|
280
|
+
}
|
|
281
|
+
normalized[key] = value[key];
|
|
282
|
+
}
|
|
283
|
+
const encoded = JSON.stringify(normalized);
|
|
284
|
+
if (encoded === void 0) throw new EditableArtifactDurableExportError("invalid_request");
|
|
285
|
+
return encoded;
|
|
286
|
+
}
|
|
287
|
+
function validateProfile(profile, modality, format) {
|
|
288
|
+
if (!plainRecord(profile) || profile.modality !== modality || profile.format !== format || !editableArtifactFormatMatchesModality(modality, format) || !boundedProfileText(profile.codecId, 128) || !boundedProfileText(profile.codecVersion, 128) || !boundedProfileText(profile.kernelVersion, 512) || !SHA256.test(profile.fontRegistryHash) || !SHA256.test(profile.policyHash) || profile.normalizedOptions.trim() !== profile.normalizedOptions || new TextEncoder().encode(profile.normalizedOptions).byteLength > EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES) {
|
|
289
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
290
|
+
}
|
|
291
|
+
let decoded;
|
|
292
|
+
try {
|
|
293
|
+
decoded = JSON.parse(profile.normalizedOptions);
|
|
294
|
+
} catch {
|
|
295
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
296
|
+
}
|
|
297
|
+
if (!plainRecord(decoded) || canonicalJson(decoded) !== profile.normalizedOptions) {
|
|
298
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function boundedProfileText(value, maximumBytes) {
|
|
302
|
+
return typeof value === "string" && PROFILE_TEXT.test(value) && new TextEncoder().encode(value).byteLength <= maximumBytes;
|
|
303
|
+
}
|
|
304
|
+
function hashPublicRequest(kind, fields) {
|
|
305
|
+
const digest = createHash("sha256");
|
|
306
|
+
digest.update("OpenGeni editable artifact durable export\0v1\0", "utf8");
|
|
307
|
+
digest.update(kind, "utf8");
|
|
308
|
+
digest.update("\0", "utf8");
|
|
309
|
+
for (const field of fields) {
|
|
310
|
+
const bytes = Buffer.from(field, "utf8");
|
|
311
|
+
const length = Buffer.allocUnsafe(4);
|
|
312
|
+
length.writeUInt32BE(bytes.byteLength);
|
|
313
|
+
digest.update(length);
|
|
314
|
+
digest.update(bytes);
|
|
315
|
+
}
|
|
316
|
+
return `sha256:${digest.digest("hex")}`;
|
|
317
|
+
}
|
|
318
|
+
function plainRecord(value) {
|
|
319
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
320
|
+
const prototype = Object.getPrototypeOf(value);
|
|
321
|
+
return prototype === Object.prototype || prototype === null;
|
|
322
|
+
}
|
|
323
|
+
function jsonValue(value, depth = 0) {
|
|
324
|
+
if (depth > 32) return false;
|
|
325
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
326
|
+
return typeof value !== "string" || wellFormedText(value);
|
|
327
|
+
}
|
|
328
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
329
|
+
if (Array.isArray(value))
|
|
330
|
+
return value.length <= 1e4 && value.every((item) => jsonValue(item, depth + 1));
|
|
331
|
+
if (!plainRecord(value) || Object.keys(value).length > 1e4) return false;
|
|
332
|
+
return Object.entries(value).every(
|
|
333
|
+
([key, item]) => wellFormedText(key) && jsonValue(item, depth + 1)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
function wellFormedText(value) {
|
|
337
|
+
if (value.includes("\0")) return false;
|
|
338
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
339
|
+
const unit = value.charCodeAt(index);
|
|
340
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
341
|
+
const next = value.charCodeAt(index + 1);
|
|
342
|
+
if (!Number.isFinite(next) || next < 56320 || next > 57343) return false;
|
|
343
|
+
index += 1;
|
|
344
|
+
} else if (unit >= 56320 && unit <= 57343) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
function compareCodeUnits(left, right) {
|
|
351
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
352
|
+
}
|
|
353
|
+
function validateEditableArtifactPinnedVersion(version) {
|
|
354
|
+
const scope = editableArtifactScope(version.scope);
|
|
355
|
+
const artifactId = editableArtifactId(version.artifactId);
|
|
356
|
+
const id = stableId(version.id, "version id");
|
|
357
|
+
const snapshotId = editableArtifactSnapshotId(version.snapshotId);
|
|
358
|
+
if (!Number.isSafeInteger(version.headSequence) || version.headSequence < 0) {
|
|
359
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
360
|
+
}
|
|
361
|
+
const stateHash = editableArtifactStateHash(version.stateHash);
|
|
362
|
+
versionName(version.name);
|
|
363
|
+
assertIsoTimestamp(version.createdAt, "version creation time");
|
|
364
|
+
if (version.pinned !== true || !wellFormedText(version.createdBySubjectId)) {
|
|
365
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
366
|
+
}
|
|
367
|
+
if (version.modality === "spreadsheet" && (!Array.isArray(version.causalFrontier) || version.nativeRevision !== null) || version.modality !== "spreadsheet" && (version.causalFrontier !== null || !Number.isSafeInteger(version.nativeRevision) || version.nativeRevision < 0)) {
|
|
368
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
369
|
+
}
|
|
370
|
+
return Object.freeze({
|
|
371
|
+
...version,
|
|
372
|
+
scope,
|
|
373
|
+
artifactId,
|
|
374
|
+
id,
|
|
375
|
+
snapshotId,
|
|
376
|
+
stateHash,
|
|
377
|
+
causalFrontier: version.causalFrontier === null ? null : Object.freeze(version.causalFrontier.map((entry) => Object.freeze({ ...entry })))
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function validateEditableArtifactMaterializationJob(job) {
|
|
381
|
+
const scope = editableArtifactScope(job.scope);
|
|
382
|
+
const artifactId = editableArtifactId(job.artifactId);
|
|
383
|
+
const id = stableId(job.id, "job id");
|
|
384
|
+
const versionId = stableId(job.versionId, "version id");
|
|
385
|
+
const inputSnapshotId = stableId(job.inputSnapshotId, "snapshot id");
|
|
386
|
+
const stateHash = editableArtifactStateHash(job.stateHash);
|
|
387
|
+
const format = materializationFormat(job.format);
|
|
388
|
+
if (!Number.isSafeInteger(job.targetHeadSequence) || job.targetHeadSequence < 0 || !Number.isSafeInteger(job.attemptCount) || job.attemptCount < 0 || !["pending", "running", "succeeded", "failed"].includes(job.state)) {
|
|
389
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
390
|
+
}
|
|
391
|
+
assertIsoTimestamp(job.createdAt, "job creation time");
|
|
392
|
+
if (job.startedAt !== null) assertIsoTimestamp(job.startedAt, "job start time");
|
|
393
|
+
if (job.completedAt !== null) assertIsoTimestamp(job.completedAt, "job completion time");
|
|
394
|
+
let result = null;
|
|
395
|
+
if (job.result) {
|
|
396
|
+
result = Object.freeze({
|
|
397
|
+
...job.result,
|
|
398
|
+
id: stableId(job.result.id, "result id"),
|
|
399
|
+
contentHash: editableArtifactContentHash(job.result.contentHash),
|
|
400
|
+
mimeType: editableArtifactMaterializationMimeType(format)
|
|
401
|
+
});
|
|
402
|
+
if (job.result.mimeType !== result.mimeType || !Number.isSafeInteger(job.result.byteSize) || job.result.byteSize < 1 || job.result.byteSize > EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES) {
|
|
403
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
404
|
+
}
|
|
405
|
+
assertIsoTimestamp(job.result.verifiedAt, "result verification time");
|
|
406
|
+
assertIsoTimestamp(job.result.createdAt, "result creation time");
|
|
407
|
+
}
|
|
408
|
+
if (job.state === "succeeded" !== (result !== null)) {
|
|
409
|
+
throw new EditableArtifactDurableExportError("unavailable");
|
|
410
|
+
}
|
|
411
|
+
return Object.freeze({
|
|
412
|
+
...job,
|
|
413
|
+
scope,
|
|
414
|
+
artifactId,
|
|
415
|
+
id,
|
|
416
|
+
versionId,
|
|
417
|
+
inputSnapshotId,
|
|
418
|
+
stateHash,
|
|
419
|
+
format,
|
|
420
|
+
result
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/domain/editable-artifacts/agent-application.ts
|
|
425
|
+
import { createHash as createHash2 } from "crypto";
|
|
426
|
+
import {
|
|
427
|
+
COMMITTED_TRANSACTION_PROTOCOL_VERSION,
|
|
428
|
+
EDITABLE_ARTIFACT_MODEL_SCHEMA_VERSION
|
|
429
|
+
} from "@opengeni/contracts/editable-artifact-versions";
|
|
430
|
+
import {
|
|
431
|
+
DOCUMENT_ARTIFACT_COMMAND_VERSION,
|
|
432
|
+
EDITABLE_ARTIFACT_INTENT_VERSION,
|
|
433
|
+
PRESENTATION_ARTIFACT_COMMAND_VERSION,
|
|
434
|
+
SPREADSHEET_ARTIFACT_COMMAND_VERSION,
|
|
435
|
+
decodeEditableArtifactMutationIntent,
|
|
436
|
+
decodeDocumentArtifactQueryResponse,
|
|
437
|
+
decodePresentationArtifactQueryResponse,
|
|
438
|
+
decodeSpreadsheetArtifactKernelProjection,
|
|
439
|
+
encodeDocumentArtifactCommandBatch,
|
|
440
|
+
encodeDocumentArtifactQuery,
|
|
441
|
+
encodePresentationArtifactCommandBatch,
|
|
442
|
+
encodePresentationArtifactQuery,
|
|
443
|
+
encodeSpreadsheetArtifactCommandBatch,
|
|
444
|
+
encodeSpreadsheetArtifactKernelQuery,
|
|
445
|
+
hashEditableArtifactMutationIntent
|
|
446
|
+
} from "@opengeni/contracts/editable-artifacts";
|
|
447
|
+
var EditableArtifactOfficeImportError = class extends Error {
|
|
448
|
+
constructor(code) {
|
|
449
|
+
super(code);
|
|
450
|
+
this.code = code;
|
|
451
|
+
this.name = "EditableArtifactOfficeImportError";
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
var EditableArtifactAgentApplication = class {
|
|
455
|
+
constructor(dependencies) {
|
|
456
|
+
this.dependencies = dependencies;
|
|
457
|
+
}
|
|
458
|
+
async list(input) {
|
|
459
|
+
const context = agentContext(input);
|
|
460
|
+
const limit = boundedLimit(input.limit ?? 64);
|
|
461
|
+
const ids = await this.dependencies.associations.listArtifactIds({
|
|
462
|
+
scope: context.scope,
|
|
463
|
+
sessionId: context.sessionId,
|
|
464
|
+
limit
|
|
465
|
+
});
|
|
466
|
+
const artifacts = [];
|
|
467
|
+
for (const artifactId of ids) {
|
|
468
|
+
try {
|
|
469
|
+
const artifact = await this.dependencies.domain.getArtifact({
|
|
470
|
+
scope: context.scope,
|
|
471
|
+
actor: context.actor,
|
|
472
|
+
artifactId
|
|
473
|
+
});
|
|
474
|
+
artifacts.push(projectArtifact(artifact));
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error instanceof EditableArtifactNotFoundError || error instanceof EditableArtifactDomainError && error.code === "forbidden") {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return Object.freeze(artifacts);
|
|
483
|
+
}
|
|
484
|
+
async create(input) {
|
|
485
|
+
const context = agentContext(input);
|
|
486
|
+
const result = await this.dependencies.domain.createArtifact({
|
|
487
|
+
scope: context.scope,
|
|
488
|
+
actor: context.actor,
|
|
489
|
+
request: {
|
|
490
|
+
idempotencyKey: editableArtifactClientTransactionId(input.idempotencyKey),
|
|
491
|
+
modality: input.modality,
|
|
492
|
+
title: input.title
|
|
493
|
+
},
|
|
494
|
+
...input.signal ? { signal: input.signal } : {}
|
|
495
|
+
});
|
|
496
|
+
await this.touch(context, result.artifact.id);
|
|
497
|
+
return projectArtifact(result.artifact);
|
|
498
|
+
}
|
|
499
|
+
async import(input) {
|
|
500
|
+
const context = agentContext(input);
|
|
501
|
+
const prepared = await this.dependencies.officeImports.prepare({
|
|
502
|
+
scope: context.scope,
|
|
503
|
+
actor: context.actor,
|
|
504
|
+
fileId: input.fileId,
|
|
505
|
+
modality: input.modality,
|
|
506
|
+
...input.signal ? { signal: input.signal } : {}
|
|
507
|
+
});
|
|
508
|
+
const result = await this.dependencies.domain.importArtifact({
|
|
509
|
+
scope: context.scope,
|
|
510
|
+
actor: context.actor,
|
|
511
|
+
request: {
|
|
512
|
+
idempotencyKey: editableArtifactClientTransactionId(input.idempotencyKey),
|
|
513
|
+
modality: input.modality,
|
|
514
|
+
title: input.title,
|
|
515
|
+
originalImport: prepared.originalImport,
|
|
516
|
+
snapshot: prepared.snapshot
|
|
517
|
+
},
|
|
518
|
+
...input.signal ? { signal: input.signal } : {}
|
|
519
|
+
});
|
|
520
|
+
await this.touch(context, result.artifact.id);
|
|
521
|
+
return projectArtifact(result.artifact);
|
|
522
|
+
}
|
|
523
|
+
async get(input) {
|
|
524
|
+
const context = agentContext(input);
|
|
525
|
+
const artifact = await this.dependencies.domain.getArtifact({
|
|
526
|
+
scope: context.scope,
|
|
527
|
+
actor: context.actor,
|
|
528
|
+
artifactId: editableArtifactId(input.artifactId)
|
|
529
|
+
});
|
|
530
|
+
await this.touch(context, artifact.id);
|
|
531
|
+
return projectArtifact(artifact);
|
|
532
|
+
}
|
|
533
|
+
async inspect(input) {
|
|
534
|
+
const context = agentContext(input);
|
|
535
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
536
|
+
const state = await this.dependencies.domain.readCurrentKernelState({
|
|
537
|
+
scope: context.scope,
|
|
538
|
+
actor: context.actor,
|
|
539
|
+
artifactId
|
|
540
|
+
});
|
|
541
|
+
if (state.modality !== input.request.modality)
|
|
542
|
+
throw new TypeError("Artifact modality mismatch");
|
|
543
|
+
const queryBytes = encodeQuery(input.request);
|
|
544
|
+
const responseBytes = await this.dependencies.inspector.query({ state, queryBytes });
|
|
545
|
+
const projection = decodeQueryResponse(input.request.modality, responseBytes);
|
|
546
|
+
await this.touch(context, artifactId);
|
|
547
|
+
return Object.freeze({ artifact: projectArtifact(state.artifact), projection });
|
|
548
|
+
}
|
|
549
|
+
async apply(input) {
|
|
550
|
+
const context = agentContext(input);
|
|
551
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
552
|
+
const commandBytes = encodeCommands(input.batch);
|
|
553
|
+
const clientTransactionId = editableArtifactClientTransactionId(input.clientTransactionId);
|
|
554
|
+
const expectedHeadSequence = boundedHeadSequence(input.expectedHeadSequence);
|
|
555
|
+
const expectedStateHash = editableArtifactStateHash(input.expectedStateHash);
|
|
556
|
+
const artifact = await this.dependencies.domain.getArtifact({
|
|
557
|
+
scope: context.scope,
|
|
558
|
+
actor: context.actor,
|
|
559
|
+
artifactId
|
|
560
|
+
});
|
|
561
|
+
if (artifact.modality !== input.batch.modality)
|
|
562
|
+
throw new TypeError("Artifact modality mismatch");
|
|
563
|
+
if (artifact.headSequence !== expectedHeadSequence || artifact.stateHash !== expectedStateHash) {
|
|
564
|
+
const existing = await this.dependencies.domain.findTransactionReceipt({
|
|
565
|
+
scope: context.scope,
|
|
566
|
+
actor: context.actor,
|
|
567
|
+
artifactId,
|
|
568
|
+
clientTransactionId
|
|
569
|
+
});
|
|
570
|
+
if (existing) {
|
|
571
|
+
if (!receiptMatchesAgentMutation(
|
|
572
|
+
existing,
|
|
573
|
+
expectedHeadSequence,
|
|
574
|
+
expectedStateHash,
|
|
575
|
+
commandBytes
|
|
576
|
+
)) {
|
|
577
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
578
|
+
}
|
|
579
|
+
await this.touch(context, artifactId);
|
|
580
|
+
return Object.freeze({
|
|
581
|
+
artifact: projectArtifact(artifact),
|
|
582
|
+
transaction: projectReceipt(existing, true)
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
throw new EditableArtifactStaleBaseError();
|
|
586
|
+
}
|
|
587
|
+
const authored = hashEditableArtifactMutationIntent({
|
|
588
|
+
envelopeVersion: EDITABLE_ARTIFACT_INTENT_VERSION,
|
|
589
|
+
protocolVersion: COMMITTED_TRANSACTION_PROTOCOL_VERSION,
|
|
590
|
+
modelSchemaVersion: EDITABLE_ARTIFACT_MODEL_SCHEMA_VERSION,
|
|
591
|
+
commandProtocolVersion: commandProtocolVersion(input.batch.modality),
|
|
592
|
+
artifactId,
|
|
593
|
+
clientTransactionId,
|
|
594
|
+
replicaId: context.actor.replicaId,
|
|
595
|
+
replicaCounter: 1,
|
|
596
|
+
previousLocalTransactionId: null,
|
|
597
|
+
observedHeadSequence: expectedHeadSequence,
|
|
598
|
+
causalBase: artifact.modality === "spreadsheet" ? artifact.causalFrontier : [],
|
|
599
|
+
selectiveUndoOperationIds: [],
|
|
600
|
+
commandBytes
|
|
601
|
+
});
|
|
602
|
+
let applied;
|
|
603
|
+
try {
|
|
604
|
+
applied = await this.dependencies.domain.applyTransaction({
|
|
605
|
+
scope: context.scope,
|
|
606
|
+
actor: context.actor,
|
|
607
|
+
artifactId,
|
|
608
|
+
request: {
|
|
609
|
+
intentBytes: authored.bytes,
|
|
610
|
+
requestHash: editableArtifactRequestHash(authored.requestHash),
|
|
611
|
+
expectedHead: Object.freeze({
|
|
612
|
+
sequence: expectedHeadSequence,
|
|
613
|
+
stateHash: expectedStateHash
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (!(error instanceof EditableArtifactIdempotencyConflictError)) throw error;
|
|
619
|
+
const receipt = await this.dependencies.domain.findTransactionReceipt({
|
|
620
|
+
scope: context.scope,
|
|
621
|
+
actor: context.actor,
|
|
622
|
+
artifactId,
|
|
623
|
+
clientTransactionId
|
|
624
|
+
});
|
|
625
|
+
if (!receipt || !receiptMatchesAgentMutation(receipt, expectedHeadSequence, expectedStateHash, commandBytes)) {
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
applied = Object.freeze({ receipt, replayed: true });
|
|
629
|
+
}
|
|
630
|
+
const current = await this.dependencies.domain.getArtifact({
|
|
631
|
+
scope: context.scope,
|
|
632
|
+
actor: context.actor,
|
|
633
|
+
artifactId
|
|
634
|
+
});
|
|
635
|
+
await this.touch(context, artifactId);
|
|
636
|
+
return Object.freeze({
|
|
637
|
+
artifact: projectArtifact(current),
|
|
638
|
+
transaction: projectReceipt(applied.receipt, applied.replayed)
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
async startExport(input) {
|
|
642
|
+
const context = agentContext(input);
|
|
643
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
644
|
+
const artifact = await this.dependencies.domain.getArtifact({
|
|
645
|
+
scope: context.scope,
|
|
646
|
+
actor: context.actor,
|
|
647
|
+
artifactId
|
|
648
|
+
});
|
|
649
|
+
const version = await this.dependencies.exports.pinVersion({
|
|
650
|
+
scope: context.scope,
|
|
651
|
+
actor: context.actor,
|
|
652
|
+
artifactId,
|
|
653
|
+
idempotencyKey: exportChildIdempotencyKey(input.idempotencyKey, "version"),
|
|
654
|
+
name: `Export ${artifact.title}`,
|
|
655
|
+
...input.signal ? { signal: input.signal } : {}
|
|
656
|
+
});
|
|
657
|
+
const materialization = await this.dependencies.exports.enqueueMaterialization({
|
|
658
|
+
scope: context.scope,
|
|
659
|
+
actor: context.actor,
|
|
660
|
+
artifactId,
|
|
661
|
+
idempotencyKey: exportChildIdempotencyKey(input.idempotencyKey, "materialization"),
|
|
662
|
+
versionId: version.version.id,
|
|
663
|
+
format: input.format,
|
|
664
|
+
...input.options ? { options: input.options } : {}
|
|
665
|
+
});
|
|
666
|
+
const current = await this.dependencies.domain.getArtifact({
|
|
667
|
+
scope: context.scope,
|
|
668
|
+
actor: context.actor,
|
|
669
|
+
artifactId
|
|
670
|
+
});
|
|
671
|
+
await this.touch(context, artifactId);
|
|
672
|
+
return Object.freeze({
|
|
673
|
+
artifact: projectArtifact(current),
|
|
674
|
+
versionId: version.version.id,
|
|
675
|
+
jobId: materialization.job.id,
|
|
676
|
+
sourceHeadSequence: materialization.job.targetHeadSequence,
|
|
677
|
+
sourceStateHash: materialization.job.stateHash,
|
|
678
|
+
state: materialization.job.state
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async exportStatus(input) {
|
|
682
|
+
const context = agentContext(input);
|
|
683
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
684
|
+
const artifact = await this.dependencies.domain.getArtifact({
|
|
685
|
+
scope: context.scope,
|
|
686
|
+
actor: context.actor,
|
|
687
|
+
artifactId
|
|
688
|
+
});
|
|
689
|
+
const job = await this.dependencies.exports.getMaterialization({
|
|
690
|
+
scope: context.scope,
|
|
691
|
+
actor: context.actor,
|
|
692
|
+
artifactId,
|
|
693
|
+
jobId: input.jobId
|
|
694
|
+
});
|
|
695
|
+
if (job.versionId !== input.versionId) throw new TypeError("Export version mismatch");
|
|
696
|
+
const file = job.state === "succeeded" && job.result ? await this.dependencies.workspaceFiles.ensureMaterializationFile({
|
|
697
|
+
scope: context.scope,
|
|
698
|
+
actor: context.actor,
|
|
699
|
+
artifact,
|
|
700
|
+
versionId: job.versionId,
|
|
701
|
+
jobId: job.id,
|
|
702
|
+
filename: defaultExportFilename(artifact.title, job.format),
|
|
703
|
+
sourceHeadSequence: job.targetHeadSequence,
|
|
704
|
+
sourceStateHash: job.stateHash,
|
|
705
|
+
...input.signal ? { signal: input.signal } : {}
|
|
706
|
+
}) : null;
|
|
707
|
+
await this.touch(context, artifactId);
|
|
708
|
+
return Object.freeze({
|
|
709
|
+
artifact: projectArtifact(artifact),
|
|
710
|
+
versionId: job.versionId,
|
|
711
|
+
jobId: job.id,
|
|
712
|
+
sourceHeadSequence: job.targetHeadSequence,
|
|
713
|
+
sourceStateHash: job.stateHash,
|
|
714
|
+
state: job.state,
|
|
715
|
+
errorCode: job.errorCode,
|
|
716
|
+
file
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
async touch(context, artifactId) {
|
|
720
|
+
await this.dependencies.associations.touch({
|
|
721
|
+
scope: context.scope,
|
|
722
|
+
sessionId: context.sessionId,
|
|
723
|
+
artifactId
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
function agentContext(input) {
|
|
728
|
+
const scope = editableArtifactScope(input.scope);
|
|
729
|
+
validateEditableArtifactActor(input.actor);
|
|
730
|
+
if (input.actor.kind !== "agent" || input.actor.sessionId !== input.sessionId) {
|
|
731
|
+
throw new TypeError("Artifact agent context is not bound to its attempt session");
|
|
732
|
+
}
|
|
733
|
+
return Object.freeze({
|
|
734
|
+
scope,
|
|
735
|
+
actor: Object.freeze({ ...input.actor }),
|
|
736
|
+
sessionId: boundedSessionId(input.sessionId)
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function encodeCommands(batch) {
|
|
740
|
+
if (batch.modality === "spreadsheet") {
|
|
741
|
+
return encodeSpreadsheetArtifactCommandBatch({ version: 1, commands: batch.commands });
|
|
742
|
+
}
|
|
743
|
+
if (batch.modality === "document") {
|
|
744
|
+
return encodeDocumentArtifactCommandBatch({ version: 1, commands: batch.commands });
|
|
745
|
+
}
|
|
746
|
+
return encodePresentationArtifactCommandBatch({ version: 1, commands: batch.commands });
|
|
747
|
+
}
|
|
748
|
+
function encodeQuery(request) {
|
|
749
|
+
if (request.modality === "spreadsheet") {
|
|
750
|
+
return encodeSpreadsheetArtifactKernelQuery(request.query);
|
|
751
|
+
}
|
|
752
|
+
if (request.modality === "document") return encodeDocumentArtifactQuery(request.query);
|
|
753
|
+
return encodePresentationArtifactQuery(request.query);
|
|
754
|
+
}
|
|
755
|
+
function decodeQueryResponse(modality, bytes) {
|
|
756
|
+
if (modality === "spreadsheet") return decodeSpreadsheetArtifactKernelProjection(bytes);
|
|
757
|
+
if (modality === "document") return decodeDocumentArtifactQueryResponse(bytes);
|
|
758
|
+
return decodePresentationArtifactQueryResponse(bytes);
|
|
759
|
+
}
|
|
760
|
+
function commandProtocolVersion(modality) {
|
|
761
|
+
if (modality === "spreadsheet") return SPREADSHEET_ARTIFACT_COMMAND_VERSION;
|
|
762
|
+
if (modality === "document") return DOCUMENT_ARTIFACT_COMMAND_VERSION;
|
|
763
|
+
return PRESENTATION_ARTIFACT_COMMAND_VERSION;
|
|
764
|
+
}
|
|
765
|
+
function projectArtifact(artifact) {
|
|
766
|
+
return Object.freeze({
|
|
767
|
+
id: artifact.id,
|
|
768
|
+
modality: artifact.modality,
|
|
769
|
+
title: artifact.title,
|
|
770
|
+
lifecycle: artifact.lifecycle,
|
|
771
|
+
headSequence: artifact.headSequence,
|
|
772
|
+
stateHash: artifact.stateHash,
|
|
773
|
+
createdAt: artifact.createdAt,
|
|
774
|
+
updatedAt: artifact.updatedAt
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function projectReceipt(receipt, replayed) {
|
|
778
|
+
return Object.freeze({
|
|
779
|
+
id: receipt.serverTransactionId,
|
|
780
|
+
clientTransactionId: receipt.clientTransactionId,
|
|
781
|
+
sequenceStart: receipt.sequenceStart,
|
|
782
|
+
sequenceEnd: receipt.sequenceEnd,
|
|
783
|
+
stateHash: receipt.stateHash,
|
|
784
|
+
committedAt: receipt.committedAt,
|
|
785
|
+
replayed
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function receiptMatchesAgentMutation(receipt, expectedHeadSequence, expectedStateHash, commandBytes) {
|
|
789
|
+
const intent = decodeEditableArtifactMutationIntent(receipt.intentBytes);
|
|
790
|
+
return intent.observedHeadSequence === expectedHeadSequence && receipt.priorStateHash === expectedStateHash && sameBytes(commandBytes, intent.commandBytes);
|
|
791
|
+
}
|
|
792
|
+
function sameBytes(left, right) {
|
|
793
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
794
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
795
|
+
if (left[index] !== right[index]) return false;
|
|
796
|
+
}
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
function boundedSessionId(value) {
|
|
800
|
+
if (!/^[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)) {
|
|
801
|
+
throw new TypeError("Artifact session id is invalid");
|
|
802
|
+
}
|
|
803
|
+
return value;
|
|
804
|
+
}
|
|
805
|
+
function boundedLimit(value) {
|
|
806
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 64) {
|
|
807
|
+
throw new TypeError("Artifact list limit must be between 1 and 64");
|
|
808
|
+
}
|
|
809
|
+
return value;
|
|
810
|
+
}
|
|
811
|
+
function boundedHeadSequence(value) {
|
|
812
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
813
|
+
throw new TypeError("Artifact expected head sequence must be a nonnegative safe integer");
|
|
814
|
+
}
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
817
|
+
function defaultExportFilename(title, format) {
|
|
818
|
+
const stem = title.normalize("NFKC").replace(/[\\/:*?"<>|\u0000-\u001f\u007f]+/gu, "-").replace(/\s+/gu, " ").trim().slice(0, 180);
|
|
819
|
+
return `${stem || "artifact"}.${format}`;
|
|
820
|
+
}
|
|
821
|
+
function exportChildIdempotencyKey(value, operation) {
|
|
822
|
+
const digest = createHash2("sha256").update("opengeni:editable-artifact-export:v1\0", "utf8").update(operation, "utf8").update("\0", "utf8").update(value, "utf8").digest("hex");
|
|
823
|
+
return `artifact-export:${operation}:${digest}`;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/domain/editable-artifacts/durable-export-postgres.ts
|
|
827
|
+
import {
|
|
828
|
+
EditableArtifactDurableExportPersistenceError
|
|
829
|
+
} from "@opengeni/db/editable-artifact-durable-export";
|
|
830
|
+
function editableArtifactDurableExportStorePortFromPostgres(store) {
|
|
831
|
+
return Object.freeze({
|
|
832
|
+
async pinVersion(input) {
|
|
833
|
+
try {
|
|
834
|
+
const result = await store.pinVersion(persistedPinRequest(input));
|
|
835
|
+
if (result.kind === "authorization_stale") return result;
|
|
836
|
+
return Object.freeze({
|
|
837
|
+
kind: "result",
|
|
838
|
+
version: versionFromPersisted(result.value.version),
|
|
839
|
+
replayed: result.value.replayed
|
|
840
|
+
});
|
|
841
|
+
} catch (error) {
|
|
842
|
+
throw mapPersistenceError(error);
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
async enqueueMaterialization(input) {
|
|
846
|
+
try {
|
|
847
|
+
const result = await store.enqueueMaterialization(persistedEnqueueRequest(input));
|
|
848
|
+
if (result.kind === "authorization_stale") return result;
|
|
849
|
+
return Object.freeze({
|
|
850
|
+
kind: "result",
|
|
851
|
+
job: jobFromPersisted(result.value.job),
|
|
852
|
+
replayed: result.value.replayed
|
|
853
|
+
});
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw mapPersistenceError(error);
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
async readVersion(input) {
|
|
859
|
+
try {
|
|
860
|
+
const result = await store.readVersion(input);
|
|
861
|
+
if (result.kind === "authorization_stale") return result;
|
|
862
|
+
return Object.freeze({
|
|
863
|
+
kind: "result",
|
|
864
|
+
version: result.value ? versionFromPersisted(result.value) : null
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
throw mapPersistenceError(error);
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
async readMaterialization(input) {
|
|
871
|
+
try {
|
|
872
|
+
const result = await store.readMaterialization(input);
|
|
873
|
+
if (result.kind === "authorization_stale") return result;
|
|
874
|
+
return Object.freeze({
|
|
875
|
+
kind: "result",
|
|
876
|
+
job: result.value ? jobFromPersisted(result.value) : null
|
|
877
|
+
});
|
|
878
|
+
} catch (error) {
|
|
879
|
+
throw mapPersistenceError(error);
|
|
880
|
+
}
|
|
881
|
+
},
|
|
882
|
+
async readMaterializationDownload(input) {
|
|
883
|
+
try {
|
|
884
|
+
const result = await store.readMaterializationDownload(input);
|
|
885
|
+
if (result.kind === "authorization_stale") return result;
|
|
886
|
+
return Object.freeze({
|
|
887
|
+
kind: "result",
|
|
888
|
+
job: result.value.job ? jobFromPersisted(result.value.job) : null,
|
|
889
|
+
objectReference: result.value.objectReference
|
|
890
|
+
});
|
|
891
|
+
} catch (error) {
|
|
892
|
+
throw mapPersistenceError(error);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function persistedPinRequest(input) {
|
|
898
|
+
const snapshot = input.snapshot;
|
|
899
|
+
return {
|
|
900
|
+
...input,
|
|
901
|
+
snapshot: {
|
|
902
|
+
modality: snapshot.modality,
|
|
903
|
+
snapshotId: snapshot.snapshotId,
|
|
904
|
+
coveredHeadSequence: snapshot.coveredHeadSequence,
|
|
905
|
+
stateHash: snapshot.stateHash,
|
|
906
|
+
coveredCausalFrontier: snapshot.modality === "spreadsheet" ? snapshot.coveredCausalFrontier : null,
|
|
907
|
+
nativeRevision: snapshot.modality === "spreadsheet" ? null : snapshot.nativeRevision
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function persistedEnqueueRequest(input) {
|
|
912
|
+
return { ...input };
|
|
913
|
+
}
|
|
914
|
+
function versionFromPersisted(value) {
|
|
915
|
+
return validateEditableArtifactPinnedVersion(value);
|
|
916
|
+
}
|
|
917
|
+
function jobFromPersisted(value) {
|
|
918
|
+
return validateEditableArtifactMaterializationJob(value);
|
|
919
|
+
}
|
|
920
|
+
function mapPersistenceError(error) {
|
|
921
|
+
if (!(error instanceof EditableArtifactDurableExportPersistenceError)) return error;
|
|
922
|
+
switch (error.code) {
|
|
923
|
+
case "not_found":
|
|
924
|
+
return new EditableArtifactDurableExportError("not_found");
|
|
925
|
+
case "idempotency_conflict":
|
|
926
|
+
case "snapshot_conflict":
|
|
927
|
+
return new EditableArtifactDurableExportError("conflict");
|
|
928
|
+
case "unsupported_format":
|
|
929
|
+
return new EditableArtifactDurableExportError("unsupported_format");
|
|
930
|
+
case "database_contract_violation":
|
|
931
|
+
return new EditableArtifactDurableExportError("unavailable");
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/domain/editable-artifacts/in-memory.ts
|
|
936
|
+
import { decodeCommittedTransactionSummary } from "@opengeni/contracts/editable-artifact-committed-transaction";
|
|
937
|
+
import { decodeEditableArtifactSerializedCommit } from "@opengeni/contracts/editable-artifact-serialized-commit";
|
|
938
|
+
var InMemoryEditableArtifactStore = class {
|
|
939
|
+
constructor(now = () => /* @__PURE__ */ new Date()) {
|
|
940
|
+
this.now = now;
|
|
941
|
+
}
|
|
942
|
+
aggregates = /* @__PURE__ */ new Map();
|
|
943
|
+
creationReceipts = /* @__PURE__ */ new Map();
|
|
944
|
+
scopeCreateAuthorizationRevisions = /* @__PURE__ */ new Map();
|
|
945
|
+
exclusiveTail = Promise.resolve();
|
|
946
|
+
async findArtifactCreation(scopeInput, operationKind, authorityKey, idempotencyKeyInput) {
|
|
947
|
+
const scope = editableArtifactScope(scopeInput);
|
|
948
|
+
const idempotencyKey = editableArtifactClientTransactionId(idempotencyKeyInput);
|
|
949
|
+
const key = creationReceiptKey(scope, operationKind, authorityKey, idempotencyKey);
|
|
950
|
+
return this.exclusive(async () => {
|
|
951
|
+
const receipt = this.creationReceipts.get(key);
|
|
952
|
+
if (!receipt) return null;
|
|
953
|
+
const aggregate = this.aggregates.get(aggregateKey(scope, receipt.artifactId));
|
|
954
|
+
const snapshot = aggregate?.snapshots.get(receipt.genesisSnapshotId);
|
|
955
|
+
if (!aggregate || !snapshot) {
|
|
956
|
+
throw new Error("Artifact creation receipt is corrupt");
|
|
957
|
+
}
|
|
958
|
+
return Object.freeze({
|
|
959
|
+
artifact: cloneArtifact(aggregate.artifact),
|
|
960
|
+
genesisSnapshot: cloneSnapshot(snapshot),
|
|
961
|
+
creationReceipt: cloneCreationReceipt(receipt),
|
|
962
|
+
replayed: true
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async createArtifact(input) {
|
|
967
|
+
const scope = editableArtifactScope(input.scope);
|
|
968
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
969
|
+
const receiptId = editableArtifactReceiptId(input.receiptId);
|
|
970
|
+
const idempotencyKey = editableArtifactClientTransactionId(input.idempotencyKey);
|
|
971
|
+
const requestHash = editableArtifactRequestHash(input.requestHash);
|
|
972
|
+
if (input.operationKind !== "create" && input.operationKind !== "import") {
|
|
973
|
+
throw new TypeError("Unknown editable artifact origin operation");
|
|
974
|
+
}
|
|
975
|
+
if (input.operationKind === "import" !== Boolean(input.originalImport)) {
|
|
976
|
+
throw new TypeError("Editable artifact import source does not match its origin operation");
|
|
977
|
+
}
|
|
978
|
+
assertBoundedArtifactTitle(input.title);
|
|
979
|
+
assertPositiveSafeInteger(
|
|
980
|
+
input.expectedScopeAuthorizationRevision,
|
|
981
|
+
"expected scope create authorization revision"
|
|
982
|
+
);
|
|
983
|
+
assertPositiveSafeInteger(
|
|
984
|
+
input.initialArtifactAuthorizationRevision,
|
|
985
|
+
"initial artifact authorization revision"
|
|
986
|
+
);
|
|
987
|
+
const genesisSnapshot = cloneSnapshot(input.genesisSnapshot);
|
|
988
|
+
const outbox = cloneOutbox(input.outbox);
|
|
989
|
+
assertGenesisCreateRequest(scope, artifactId, input.operationKind, genesisSnapshot, outbox);
|
|
990
|
+
if (!["spreadsheet", "presentation", "document"].includes(input.modality)) {
|
|
991
|
+
throw new TypeError("Unknown editable artifact modality");
|
|
992
|
+
}
|
|
993
|
+
if (genesisSnapshot.modality !== input.modality) {
|
|
994
|
+
throw new TypeError("Genesis snapshot modality does not match artifact modality");
|
|
995
|
+
}
|
|
996
|
+
const creationKey = creationReceiptKey(
|
|
997
|
+
scope,
|
|
998
|
+
input.operationKind,
|
|
999
|
+
input.authorityKey,
|
|
1000
|
+
idempotencyKey
|
|
1001
|
+
);
|
|
1002
|
+
return this.exclusive(async () => {
|
|
1003
|
+
const prior = this.creationReceipts.get(creationKey);
|
|
1004
|
+
if (prior) {
|
|
1005
|
+
if (prior.requestHash !== requestHash) {
|
|
1006
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
1007
|
+
}
|
|
1008
|
+
const replay = this.aggregates.get(aggregateKey(scope, prior.artifactId));
|
|
1009
|
+
if (!replay) throw new Error("Artifact creation receipt is corrupt");
|
|
1010
|
+
const replaySnapshot = replay.snapshots.get(prior.genesisSnapshotId);
|
|
1011
|
+
if (!replaySnapshot) {
|
|
1012
|
+
throw new Error("Artifact creation receipt has no genesis snapshot");
|
|
1013
|
+
}
|
|
1014
|
+
return Object.freeze({
|
|
1015
|
+
kind: "result",
|
|
1016
|
+
value: Object.freeze({
|
|
1017
|
+
artifact: cloneArtifact(replay.artifact),
|
|
1018
|
+
genesisSnapshot: cloneSnapshot(replaySnapshot),
|
|
1019
|
+
creationReceipt: cloneCreationReceipt(prior),
|
|
1020
|
+
replayed: true
|
|
1021
|
+
})
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
const scopeKey = authorizationScopeKey(scope);
|
|
1025
|
+
const currentScopeRevision = this.scopeCreateAuthorizationRevisions.get(scopeKey) ?? 1;
|
|
1026
|
+
if (currentScopeRevision !== input.expectedScopeAuthorizationRevision) {
|
|
1027
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
1028
|
+
}
|
|
1029
|
+
const key = aggregateKey(scope, artifactId);
|
|
1030
|
+
if (this.aggregates.has(key)) {
|
|
1031
|
+
throw new Error("Editable artifact identity already exists");
|
|
1032
|
+
}
|
|
1033
|
+
const commonArtifact = {
|
|
1034
|
+
scope,
|
|
1035
|
+
id: artifactId,
|
|
1036
|
+
title: input.title,
|
|
1037
|
+
lifecycle: "active",
|
|
1038
|
+
authorizationRevision: input.initialArtifactAuthorizationRevision,
|
|
1039
|
+
headSequence: 0,
|
|
1040
|
+
stateHash: genesisSnapshot.stateHash,
|
|
1041
|
+
currentSnapshotId: genesisSnapshot.snapshotId,
|
|
1042
|
+
createdAt: genesisSnapshot.publishedAt,
|
|
1043
|
+
updatedAt: genesisSnapshot.publishedAt
|
|
1044
|
+
};
|
|
1045
|
+
const artifact = input.modality === "spreadsheet" ? {
|
|
1046
|
+
...commonArtifact,
|
|
1047
|
+
modality: "spreadsheet",
|
|
1048
|
+
causalFrontier: editableArtifactCausalFrontier(
|
|
1049
|
+
genesisSnapshot.modality === "spreadsheet" ? genesisSnapshot.coveredCausalFrontier : []
|
|
1050
|
+
)
|
|
1051
|
+
} : { ...commonArtifact, modality: input.modality };
|
|
1052
|
+
const creationReceipt = Object.freeze({
|
|
1053
|
+
receiptId,
|
|
1054
|
+
scope,
|
|
1055
|
+
artifactId,
|
|
1056
|
+
operationKind: input.operationKind,
|
|
1057
|
+
authorityKey: input.authorityKey,
|
|
1058
|
+
idempotencyKey,
|
|
1059
|
+
requestHash,
|
|
1060
|
+
genesisSnapshotId: genesisSnapshot.snapshotId,
|
|
1061
|
+
createdAt: genesisSnapshot.publishedAt
|
|
1062
|
+
});
|
|
1063
|
+
this.aggregates.set(key, emptyAggregate(artifact, genesisSnapshot, outbox));
|
|
1064
|
+
this.creationReceipts.set(creationKey, creationReceipt);
|
|
1065
|
+
return Object.freeze({
|
|
1066
|
+
kind: "result",
|
|
1067
|
+
value: Object.freeze({
|
|
1068
|
+
artifact: cloneArtifact(artifact),
|
|
1069
|
+
genesisSnapshot: cloneSnapshot(genesisSnapshot),
|
|
1070
|
+
creationReceipt: cloneCreationReceipt(creationReceipt),
|
|
1071
|
+
replayed: false
|
|
1072
|
+
})
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
async readScopeCreateAuthorizationRevision(scope) {
|
|
1077
|
+
const key = authorizationScopeKey(editableArtifactScope(scope));
|
|
1078
|
+
return this.exclusive(async () => {
|
|
1079
|
+
const revision = this.scopeCreateAuthorizationRevisions.get(key) ?? 1;
|
|
1080
|
+
this.scopeCreateAuthorizationRevisions.set(key, revision);
|
|
1081
|
+
return revision;
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
async advanceScopeCreateAuthorizationRevision(scope, expectedRevision, nextRevision) {
|
|
1085
|
+
assertPositiveSafeInteger(expectedRevision, "expected scope create authorization revision");
|
|
1086
|
+
assertPositiveSafeInteger(nextRevision, "next scope create authorization revision");
|
|
1087
|
+
if (nextRevision <= expectedRevision) {
|
|
1088
|
+
throw new TypeError(
|
|
1089
|
+
"Next scope create authorization revision must exceed the expected revision"
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const key = authorizationScopeKey(editableArtifactScope(scope));
|
|
1093
|
+
return this.exclusive(async () => {
|
|
1094
|
+
const current = this.scopeCreateAuthorizationRevisions.get(key) ?? 1;
|
|
1095
|
+
if (current !== expectedRevision) {
|
|
1096
|
+
return Object.freeze({
|
|
1097
|
+
applied: false,
|
|
1098
|
+
authorizationRevision: current
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
this.scopeCreateAuthorizationRevisions.set(key, nextRevision);
|
|
1102
|
+
return Object.freeze({
|
|
1103
|
+
applied: true,
|
|
1104
|
+
authorizationRevision: nextRevision
|
|
1105
|
+
});
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
async seedArtifact(input) {
|
|
1109
|
+
const scope = editableArtifactScope(input.scope);
|
|
1110
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
1111
|
+
assertBoundedArtifactTitle(input.title);
|
|
1112
|
+
const stateHash = editableArtifactStateHash(input.stateHash);
|
|
1113
|
+
const createdAt = input.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1114
|
+
assertIsoTimestamp(createdAt, "artifact creation timestamp");
|
|
1115
|
+
if (!["spreadsheet", "presentation", "document"].includes(input.modality)) {
|
|
1116
|
+
throw new TypeError("Unknown editable artifact modality");
|
|
1117
|
+
}
|
|
1118
|
+
const lifecycle = input.lifecycle ?? "active";
|
|
1119
|
+
if (lifecycle !== "active" && lifecycle !== "archived") {
|
|
1120
|
+
throw new TypeError("Unknown editable artifact lifecycle state");
|
|
1121
|
+
}
|
|
1122
|
+
const authorizationRevision = input.authorizationRevision ?? 1;
|
|
1123
|
+
assertPositiveSafeInteger(authorizationRevision, "artifact authorization revision");
|
|
1124
|
+
const title = input.title;
|
|
1125
|
+
const modality = input.modality;
|
|
1126
|
+
const nativeRevision = input.nativeRevision ?? 0;
|
|
1127
|
+
assertNonnegativeSafeInteger(nativeRevision, "artifact native revision");
|
|
1128
|
+
return this.exclusive(async () => {
|
|
1129
|
+
const key = aggregateKey(scope, artifactId);
|
|
1130
|
+
if (this.aggregates.has(key)) throw new Error("Editable artifact is already seeded");
|
|
1131
|
+
const commonArtifact = {
|
|
1132
|
+
scope,
|
|
1133
|
+
id: artifactId,
|
|
1134
|
+
title,
|
|
1135
|
+
lifecycle,
|
|
1136
|
+
authorizationRevision,
|
|
1137
|
+
headSequence: 0,
|
|
1138
|
+
stateHash,
|
|
1139
|
+
currentSnapshotId: null,
|
|
1140
|
+
createdAt,
|
|
1141
|
+
updatedAt: createdAt
|
|
1142
|
+
};
|
|
1143
|
+
const artifact = modality === "spreadsheet" ? {
|
|
1144
|
+
...commonArtifact,
|
|
1145
|
+
modality: "spreadsheet",
|
|
1146
|
+
causalFrontier: editableArtifactCausalFrontier([])
|
|
1147
|
+
} : { ...commonArtifact, modality };
|
|
1148
|
+
this.aggregates.set(key, {
|
|
1149
|
+
artifact,
|
|
1150
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
1151
|
+
transactions: /* @__PURE__ */ new Map(),
|
|
1152
|
+
committedTransactions: /* @__PURE__ */ new Map(),
|
|
1153
|
+
operations: /* @__PURE__ */ new Map(),
|
|
1154
|
+
undoClaims: /* @__PURE__ */ new Map(),
|
|
1155
|
+
snapshots: /* @__PURE__ */ new Map(),
|
|
1156
|
+
checkpoints: /* @__PURE__ */ new Map([[0, checkpointAtGenesis(artifact, nativeRevision)]]),
|
|
1157
|
+
outbox: /* @__PURE__ */ new Map()
|
|
1158
|
+
});
|
|
1159
|
+
return cloneArtifact(artifact);
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async getArtifact(scope, artifactId) {
|
|
1163
|
+
const key = aggregateKey(editableArtifactScope(scope), editableArtifactId(artifactId));
|
|
1164
|
+
return this.exclusive(async () => {
|
|
1165
|
+
const aggregate = this.aggregates.get(key);
|
|
1166
|
+
return aggregate ? cloneArtifact(aggregate.artifact) : null;
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
async readArtifactAtAuthorizationRevision(scope, artifactId, expectedAuthorizationRevision) {
|
|
1170
|
+
assertPositiveSafeInteger(
|
|
1171
|
+
expectedAuthorizationRevision,
|
|
1172
|
+
"expected artifact authorization revision"
|
|
1173
|
+
);
|
|
1174
|
+
const key = aggregateKey(editableArtifactScope(scope), editableArtifactId(artifactId));
|
|
1175
|
+
return this.exclusive(async () => {
|
|
1176
|
+
const aggregate = this.aggregates.get(key);
|
|
1177
|
+
if (!aggregate) {
|
|
1178
|
+
return Object.freeze({ kind: "result", artifact: null });
|
|
1179
|
+
}
|
|
1180
|
+
if (aggregate.artifact.authorizationRevision !== expectedAuthorizationRevision) {
|
|
1181
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
1182
|
+
}
|
|
1183
|
+
return Object.freeze({
|
|
1184
|
+
kind: "result",
|
|
1185
|
+
artifact: cloneArtifact(aggregate.artifact)
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/** Test/admin seam modeling the local policy invalidation transaction. */
|
|
1190
|
+
async advanceAuthorizationRevision(scope, artifactId, expectedRevision, nextRevision) {
|
|
1191
|
+
assertPositiveSafeInteger(expectedRevision, "expected artifact authorization revision");
|
|
1192
|
+
assertPositiveSafeInteger(nextRevision, "next artifact authorization revision");
|
|
1193
|
+
if (nextRevision <= expectedRevision) {
|
|
1194
|
+
throw new TypeError("Next artifact authorization revision must exceed the expected revision");
|
|
1195
|
+
}
|
|
1196
|
+
const key = aggregateKey(editableArtifactScope(scope), editableArtifactId(artifactId));
|
|
1197
|
+
return this.exclusive(async () => {
|
|
1198
|
+
const aggregate = this.aggregates.get(key);
|
|
1199
|
+
if (!aggregate) throw new EditableArtifactNotFoundError();
|
|
1200
|
+
if (aggregate.artifact.authorizationRevision !== expectedRevision) {
|
|
1201
|
+
return Object.freeze({
|
|
1202
|
+
applied: false,
|
|
1203
|
+
authorizationRevision: aggregate.artifact.authorizationRevision
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
const updatedAt = authoritativeNow(this.now).toISOString();
|
|
1207
|
+
aggregate.artifact = {
|
|
1208
|
+
...aggregate.artifact,
|
|
1209
|
+
authorizationRevision: nextRevision,
|
|
1210
|
+
updatedAt: laterIsoTimestamp(aggregate.artifact.updatedAt, updatedAt)
|
|
1211
|
+
};
|
|
1212
|
+
return Object.freeze({
|
|
1213
|
+
applied: true,
|
|
1214
|
+
authorizationRevision: nextRevision
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
async readTransactionBasis(scope, artifactId, request) {
|
|
1219
|
+
const normalizedScope = editableArtifactScope(scope);
|
|
1220
|
+
const normalizedArtifactId = editableArtifactId(artifactId);
|
|
1221
|
+
const key = aggregateKey(normalizedScope, normalizedArtifactId);
|
|
1222
|
+
return this.exclusive(async () => {
|
|
1223
|
+
const aggregate = this.aggregates.get(key);
|
|
1224
|
+
if (!aggregate) throw new EditableArtifactNotFoundError();
|
|
1225
|
+
const priorReceipt = aggregate.receipts.get(
|
|
1226
|
+
receiptKey(request.actorKey, request.clientTransactionId)
|
|
1227
|
+
);
|
|
1228
|
+
if (priorReceipt) {
|
|
1229
|
+
return Object.freeze({
|
|
1230
|
+
kind: "existing",
|
|
1231
|
+
receipt: cloneReceipt(priorReceipt)
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
const predecessor = request.previousLocalTransactionId ? aggregate.receipts.get(receiptKey(request.actorKey, request.previousLocalTransactionId)) : void 0;
|
|
1235
|
+
const undoTargets = Object.freeze(
|
|
1236
|
+
request.selectiveUndoOperationIds.map(
|
|
1237
|
+
(operationId) => Object.freeze({
|
|
1238
|
+
operationId,
|
|
1239
|
+
operation: aggregate.operations.has(operationId) ? cloneOperation(aggregate.operations.get(operationId)) : null,
|
|
1240
|
+
claimedBy: aggregate.undoClaims.get(operationId) ?? null
|
|
1241
|
+
})
|
|
1242
|
+
)
|
|
1243
|
+
);
|
|
1244
|
+
const kernelState = kernelStateFromAggregate(aggregate);
|
|
1245
|
+
return Object.freeze({
|
|
1246
|
+
kind: "basis",
|
|
1247
|
+
artifact: kernelState.artifact,
|
|
1248
|
+
predecessor: predecessor ? cloneReceipt(predecessor) : null,
|
|
1249
|
+
undoTargets,
|
|
1250
|
+
kernelState
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
async readCurrentKernelState(scope, artifactId, expectedAuthorizationRevision) {
|
|
1255
|
+
const normalizedScope = editableArtifactScope(scope);
|
|
1256
|
+
const normalizedArtifactId = editableArtifactId(artifactId);
|
|
1257
|
+
const key = aggregateKey(normalizedScope, normalizedArtifactId);
|
|
1258
|
+
return this.exclusive(async () => {
|
|
1259
|
+
const aggregate = this.aggregates.get(key);
|
|
1260
|
+
if (!aggregate) throw new EditableArtifactNotFoundError();
|
|
1261
|
+
if (aggregate.artifact.authorizationRevision !== expectedAuthorizationRevision) {
|
|
1262
|
+
return Object.freeze({ kind: "authorization_stale" });
|
|
1263
|
+
}
|
|
1264
|
+
return Object.freeze({ kind: "basis", state: kernelStateFromAggregate(aggregate) });
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
async tryCommitAppliedTransaction(request) {
|
|
1268
|
+
const scope = editableArtifactScope(request.scope);
|
|
1269
|
+
const artifactId = editableArtifactId(request.artifactId);
|
|
1270
|
+
const key = aggregateKey(scope, artifactId);
|
|
1271
|
+
return this.exclusive(async () => {
|
|
1272
|
+
const aggregate = this.aggregates.get(key);
|
|
1273
|
+
if (!aggregate) throw new EditableArtifactNotFoundError();
|
|
1274
|
+
const prior = aggregate.receipts.get(
|
|
1275
|
+
receiptKey(request.actorKey, request.clientTransactionId)
|
|
1276
|
+
);
|
|
1277
|
+
if (prior) {
|
|
1278
|
+
if (prior.requestHash !== request.requestHash) {
|
|
1279
|
+
throw new EditableArtifactIdempotencyConflictError();
|
|
1280
|
+
}
|
|
1281
|
+
return Object.freeze({
|
|
1282
|
+
kind: "replayed",
|
|
1283
|
+
receipt: cloneReceipt(prior)
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
const artifact = aggregate.artifact;
|
|
1287
|
+
if (artifact.lifecycle !== request.expectedLifecycle || artifact.authorizationRevision !== request.expectedAuthorizationRevision || artifact.headSequence !== request.expectedHeadSequence) {
|
|
1288
|
+
return Object.freeze({ kind: "stale" });
|
|
1289
|
+
}
|
|
1290
|
+
if (!sameExpectedPredecessor(aggregate, request.expectedPredecessor)) {
|
|
1291
|
+
return Object.freeze({ kind: "stale" });
|
|
1292
|
+
}
|
|
1293
|
+
if (request.expectedUnclaimedUndoTargets.some(
|
|
1294
|
+
(operationId) => aggregate.undoClaims.has(operationId)
|
|
1295
|
+
)) {
|
|
1296
|
+
return Object.freeze({ kind: "stale" });
|
|
1297
|
+
}
|
|
1298
|
+
const working = cloneAggregate(aggregate);
|
|
1299
|
+
commitAppliedTransaction(working, request);
|
|
1300
|
+
this.aggregates.set(key, working);
|
|
1301
|
+
return Object.freeze({
|
|
1302
|
+
kind: "committed",
|
|
1303
|
+
receipt: cloneReceipt(request.receipt)
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
async withSnapshotPublicationLock(scope, artifactId, callback) {
|
|
1308
|
+
const key = aggregateKey(editableArtifactScope(scope), editableArtifactId(artifactId));
|
|
1309
|
+
return this.exclusive(async () => {
|
|
1310
|
+
const aggregate = this.aggregates.get(key);
|
|
1311
|
+
if (!aggregate) throw new EditableArtifactNotFoundError();
|
|
1312
|
+
const working = cloneAggregate(aggregate);
|
|
1313
|
+
const unitOfWork = new InMemoryEditableArtifactSnapshotUnitOfWork(working);
|
|
1314
|
+
try {
|
|
1315
|
+
const result = await callback(unitOfWork);
|
|
1316
|
+
this.aggregates.set(key, cloneAggregate(working));
|
|
1317
|
+
return result;
|
|
1318
|
+
} finally {
|
|
1319
|
+
unitOfWork.close();
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
async claimLiveOutbox(request) {
|
|
1324
|
+
assertLeaseRequest(request);
|
|
1325
|
+
const owner = request.owner;
|
|
1326
|
+
const leaseDurationMs = request.leaseDurationMs;
|
|
1327
|
+
const limit = request.limit;
|
|
1328
|
+
return this.exclusive(async () => {
|
|
1329
|
+
const nowMs = authoritativeNow(this.now).getTime();
|
|
1330
|
+
const eligible = [...this.aggregates.values()].flatMap(
|
|
1331
|
+
(aggregate) => [...aggregate.outbox.values()].map((record) => ({
|
|
1332
|
+
aggregate,
|
|
1333
|
+
record
|
|
1334
|
+
}))
|
|
1335
|
+
).filter(({ record }) => {
|
|
1336
|
+
if (record.state === "pending") {
|
|
1337
|
+
return new Date(record.nextAttemptAt).getTime() <= nowMs;
|
|
1338
|
+
}
|
|
1339
|
+
if (record.state !== "publishing" || !record.leaseExpiresAt) return false;
|
|
1340
|
+
return new Date(record.leaseExpiresAt).getTime() <= nowMs;
|
|
1341
|
+
}).sort(
|
|
1342
|
+
(left, right) => compareText(left.record.createdAt, right.record.createdAt) || compareText(left.record.outboxId, right.record.outboxId)
|
|
1343
|
+
).slice(0, limit);
|
|
1344
|
+
const leaseExpiresAt = new Date(nowMs + leaseDurationMs).toISOString();
|
|
1345
|
+
return Object.freeze(
|
|
1346
|
+
eligible.map(({ aggregate, record }) => {
|
|
1347
|
+
if (record.attemptCount >= Number.MAX_SAFE_INTEGER) {
|
|
1348
|
+
throw new Error("Editable artifact outbox attempt counter exhausted");
|
|
1349
|
+
}
|
|
1350
|
+
const claimed = {
|
|
1351
|
+
...record,
|
|
1352
|
+
state: "publishing",
|
|
1353
|
+
attemptCount: record.attemptCount + 1,
|
|
1354
|
+
leaseOwner: owner,
|
|
1355
|
+
leaseExpiresAt
|
|
1356
|
+
};
|
|
1357
|
+
aggregate.outbox.set(record.outboxId, claimed);
|
|
1358
|
+
return cloneOutbox(claimed);
|
|
1359
|
+
})
|
|
1360
|
+
);
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
async markLiveOutboxPublished(input) {
|
|
1364
|
+
const outboxId = editableArtifactOutboxId(input.outboxId);
|
|
1365
|
+
const owner = assertLeaseOwner(input.owner);
|
|
1366
|
+
assertPositiveSafeInteger(input.attemptCount, "outbox attempt count");
|
|
1367
|
+
await this.exclusive(async () => {
|
|
1368
|
+
const publishedAt = authoritativeNow(this.now);
|
|
1369
|
+
const publishedAtMs = publishedAt.getTime();
|
|
1370
|
+
const publishedAtIso = publishedAt.toISOString();
|
|
1371
|
+
const located = this.findOutbox(outboxId);
|
|
1372
|
+
if (!located) throw new EditableArtifactOutboxLeaseConflictError();
|
|
1373
|
+
const record = located.record;
|
|
1374
|
+
if (record.state === "published") {
|
|
1375
|
+
if (record.attemptCount === input.attemptCount) return;
|
|
1376
|
+
throw new EditableArtifactOutboxLeaseConflictError();
|
|
1377
|
+
}
|
|
1378
|
+
if (record.state !== "publishing" || record.leaseOwner !== owner || record.attemptCount !== input.attemptCount || !record.leaseExpiresAt || publishedAtMs >= new Date(record.leaseExpiresAt).getTime()) {
|
|
1379
|
+
throw new EditableArtifactOutboxLeaseConflictError();
|
|
1380
|
+
}
|
|
1381
|
+
located.aggregate.outbox.set(outboxId, {
|
|
1382
|
+
...record,
|
|
1383
|
+
state: "published",
|
|
1384
|
+
leaseOwner: null,
|
|
1385
|
+
leaseExpiresAt: null,
|
|
1386
|
+
publishedAt: publishedAtIso
|
|
1387
|
+
});
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
async releaseLiveOutbox(input) {
|
|
1391
|
+
const outboxId = editableArtifactOutboxId(input.outboxId);
|
|
1392
|
+
const owner = assertLeaseOwner(input.owner);
|
|
1393
|
+
assertPositiveSafeInteger(input.attemptCount, "outbox attempt count");
|
|
1394
|
+
await this.exclusive(async () => {
|
|
1395
|
+
const releasedAt = authoritativeNow(this.now).toISOString();
|
|
1396
|
+
const located = this.findOutbox(outboxId);
|
|
1397
|
+
if (!located) throw new EditableArtifactOutboxLeaseConflictError();
|
|
1398
|
+
if ((located.record.state === "published" || located.record.state === "pending" && located.record.leaseOwner === null && located.record.leaseExpiresAt === null) && located.record.attemptCount === input.attemptCount) {
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (located.record.state !== "publishing" || located.record.leaseOwner !== owner || located.record.attemptCount !== input.attemptCount) {
|
|
1402
|
+
throw new EditableArtifactOutboxLeaseConflictError();
|
|
1403
|
+
}
|
|
1404
|
+
located.aggregate.outbox.set(outboxId, {
|
|
1405
|
+
...located.record,
|
|
1406
|
+
state: "pending",
|
|
1407
|
+
leaseOwner: null,
|
|
1408
|
+
leaseExpiresAt: null,
|
|
1409
|
+
nextAttemptAt: releasedAt
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
async renewLiveOutbox(input) {
|
|
1414
|
+
const outboxId = editableArtifactOutboxId(input.outboxId);
|
|
1415
|
+
const owner = assertLeaseOwner(input.owner);
|
|
1416
|
+
assertPositiveSafeInteger(input.attemptCount, "outbox attempt count");
|
|
1417
|
+
assertLeaseDuration(input.leaseDurationMs);
|
|
1418
|
+
await this.exclusive(async () => {
|
|
1419
|
+
const nowMs = authoritativeNow(this.now).getTime();
|
|
1420
|
+
const located = this.findOutbox(outboxId);
|
|
1421
|
+
if (!located) throw new EditableArtifactOutboxLeaseConflictError();
|
|
1422
|
+
assertActiveOutboxLease(located.record, owner, input.attemptCount, nowMs);
|
|
1423
|
+
located.aggregate.outbox.set(outboxId, {
|
|
1424
|
+
...located.record,
|
|
1425
|
+
leaseExpiresAt: new Date(nowMs + input.leaseDurationMs).toISOString()
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
async retryLiveOutbox(input) {
|
|
1430
|
+
const outboxId = editableArtifactOutboxId(input.outboxId);
|
|
1431
|
+
const owner = assertLeaseOwner(input.owner);
|
|
1432
|
+
assertPositiveSafeInteger(input.attemptCount, "outbox attempt count");
|
|
1433
|
+
assertRetryDelay(input.retryDelayMs);
|
|
1434
|
+
assertRetryErrorCode(input.errorCode);
|
|
1435
|
+
await this.exclusive(async () => {
|
|
1436
|
+
const nowMs = authoritativeNow(this.now).getTime();
|
|
1437
|
+
const located = this.findOutbox(outboxId);
|
|
1438
|
+
if (!located) throw new EditableArtifactOutboxLeaseConflictError();
|
|
1439
|
+
const record = located.record;
|
|
1440
|
+
if (record.state === "pending" && record.attemptCount === input.attemptCount && record.leaseOwner === null && record.leaseExpiresAt === null && record.lastErrorCode === input.errorCode) {
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
assertActiveOutboxLease(record, owner, input.attemptCount, nowMs);
|
|
1444
|
+
located.aggregate.outbox.set(outboxId, {
|
|
1445
|
+
...record,
|
|
1446
|
+
state: "pending",
|
|
1447
|
+
leaseOwner: null,
|
|
1448
|
+
leaseExpiresAt: null,
|
|
1449
|
+
nextAttemptAt: new Date(nowMs + input.retryDelayMs).toISOString(),
|
|
1450
|
+
lastErrorCode: input.errorCode
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
async deadLetterLiveOutbox(input) {
|
|
1455
|
+
const outboxId = editableArtifactOutboxId(input.outboxId);
|
|
1456
|
+
const owner = assertLeaseOwner(input.owner);
|
|
1457
|
+
assertPositiveSafeInteger(input.attemptCount, "outbox attempt count");
|
|
1458
|
+
assertDeadLetterErrorCode(input.errorCode);
|
|
1459
|
+
await this.exclusive(async () => {
|
|
1460
|
+
const now = authoritativeNow(this.now);
|
|
1461
|
+
const nowMs = now.getTime();
|
|
1462
|
+
const located = this.findOutbox(outboxId);
|
|
1463
|
+
if (!located) throw new EditableArtifactOutboxLeaseConflictError();
|
|
1464
|
+
const record = located.record;
|
|
1465
|
+
if (record.state === "dead_lettered" && record.attemptCount === input.attemptCount && record.leaseOwner === null && record.leaseExpiresAt === null && record.lastErrorCode === input.errorCode) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
assertActiveOutboxLease(record, owner, input.attemptCount, nowMs);
|
|
1469
|
+
located.aggregate.outbox.set(outboxId, {
|
|
1470
|
+
...record,
|
|
1471
|
+
state: "dead_lettered",
|
|
1472
|
+
leaseOwner: null,
|
|
1473
|
+
leaseExpiresAt: null,
|
|
1474
|
+
lastErrorCode: input.errorCode,
|
|
1475
|
+
deadLetteredAt: now.toISOString()
|
|
1476
|
+
});
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
async listReceipts(scope, artifactId) {
|
|
1480
|
+
return this.inspectAggregate(
|
|
1481
|
+
scope,
|
|
1482
|
+
artifactId,
|
|
1483
|
+
(aggregate) => Object.freeze(
|
|
1484
|
+
[...aggregate.transactions.values()].sort((left, right) => left.sequenceStart - right.sequenceStart).map(cloneReceipt)
|
|
1485
|
+
)
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
async listOperations(scope, artifactId) {
|
|
1489
|
+
return this.inspectAggregate(
|
|
1490
|
+
scope,
|
|
1491
|
+
artifactId,
|
|
1492
|
+
(aggregate) => Object.freeze(
|
|
1493
|
+
[...aggregate.operations.values()].sort((left, right) => left.sequence - right.sequence).map(cloneOperation)
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
async listCommittedTransactions(scope, artifactId) {
|
|
1498
|
+
return this.inspectAggregate(
|
|
1499
|
+
scope,
|
|
1500
|
+
artifactId,
|
|
1501
|
+
(aggregate) => Object.freeze(
|
|
1502
|
+
[...aggregate.committedTransactions.values()].sort((left, right) => left.sequenceStart - right.sequenceStart).map(cloneCommittedTransaction)
|
|
1503
|
+
)
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
async listSnapshots(scope, artifactId) {
|
|
1507
|
+
return this.inspectAggregate(
|
|
1508
|
+
scope,
|
|
1509
|
+
artifactId,
|
|
1510
|
+
(aggregate) => Object.freeze(
|
|
1511
|
+
[...aggregate.snapshots.values()].sort((left, right) => left.coveredHeadSequence - right.coveredHeadSequence).map(cloneSnapshot)
|
|
1512
|
+
)
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
async listOutbox() {
|
|
1516
|
+
return this.exclusive(
|
|
1517
|
+
async () => Object.freeze(
|
|
1518
|
+
[...this.aggregates.values()].flatMap((aggregate) => [...aggregate.outbox.values()]).sort(
|
|
1519
|
+
(left, right) => compareText(left.createdAt, right.createdAt) || compareText(left.outboxId, right.outboxId)
|
|
1520
|
+
).map(cloneOutbox)
|
|
1521
|
+
)
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
async inspectAggregate(scope, artifactId, inspect) {
|
|
1525
|
+
const key = aggregateKey(editableArtifactScope(scope), editableArtifactId(artifactId));
|
|
1526
|
+
return this.exclusive(async () => {
|
|
1527
|
+
const aggregate = this.aggregates.get(key);
|
|
1528
|
+
if (!aggregate) throw new EditableArtifactNotFoundError();
|
|
1529
|
+
return inspect(aggregate);
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
findOutbox(outboxId) {
|
|
1533
|
+
for (const aggregate of this.aggregates.values()) {
|
|
1534
|
+
const record = aggregate.outbox.get(outboxId);
|
|
1535
|
+
if (record) return { aggregate, record };
|
|
1536
|
+
}
|
|
1537
|
+
return void 0;
|
|
1538
|
+
}
|
|
1539
|
+
async exclusive(operation) {
|
|
1540
|
+
const prior = this.exclusiveTail;
|
|
1541
|
+
let release;
|
|
1542
|
+
this.exclusiveTail = new Promise((resolve) => {
|
|
1543
|
+
release = resolve;
|
|
1544
|
+
});
|
|
1545
|
+
await prior;
|
|
1546
|
+
try {
|
|
1547
|
+
return await operation();
|
|
1548
|
+
} finally {
|
|
1549
|
+
release();
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
function commitAppliedTransaction(aggregate, input) {
|
|
1554
|
+
const artifact = aggregate.artifact;
|
|
1555
|
+
if (input.expectedHeadSequence !== artifact.headSequence) {
|
|
1556
|
+
throw new Error("In-memory artifact head changed before its CAS commit");
|
|
1557
|
+
}
|
|
1558
|
+
if (input.receipt.modality !== artifact.modality || input.committedTransaction.modality !== artifact.modality || input.receipt.sequenceStart !== artifact.headSequence + 1) {
|
|
1559
|
+
throw new Error("Committed transaction modality or interval is invalid");
|
|
1560
|
+
}
|
|
1561
|
+
if (input.receipt.serverTransactionId !== input.serverTransactionId || input.operations.some(
|
|
1562
|
+
(operation, index) => operation.serverTransactionId !== input.serverTransactionId || operation.sequence !== input.receipt.sequenceStart + index
|
|
1563
|
+
)) {
|
|
1564
|
+
throw new Error("Committed operations are not bound to the transaction interval");
|
|
1565
|
+
}
|
|
1566
|
+
const committed = input.committedTransaction;
|
|
1567
|
+
if (committed.serverTransactionId !== input.serverTransactionId || committed.scope.accountId !== artifact.scope.accountId || committed.scope.workspaceId !== artifact.scope.workspaceId || committed.artifactId !== artifact.id || committed.requestHash !== input.receipt.requestHash || committed.sequenceStart !== input.receipt.sequenceStart || committed.sequenceEnd !== input.receipt.sequenceEnd || committed.priorStateHash !== artifact.stateHash || committed.stateHash !== input.receipt.stateHash || !(committed.committedTransactionBytes instanceof Uint8Array) || committed.committedTransactionBytes.byteLength < 1) {
|
|
1568
|
+
throw new Error("Canonical committed transaction disagrees with its receipt");
|
|
1569
|
+
}
|
|
1570
|
+
if (input.outbox.event.kind !== "transaction_committed" || input.outbox.event.modality !== artifact.modality || input.outbox.event.serverTransactionId !== input.serverTransactionId || input.outbox.event.sequenceStart !== input.receipt.sequenceStart || input.outbox.event.sequenceEnd !== input.receipt.sequenceEnd || input.outbox.event.stateHash !== input.receipt.stateHash) {
|
|
1571
|
+
throw new Error("Transaction outbox projection is inconsistent");
|
|
1572
|
+
}
|
|
1573
|
+
if (artifact.modality === "spreadsheet") {
|
|
1574
|
+
if (input.receipt.modality !== "spreadsheet" || committed.modality !== "spreadsheet" || input.outbox.event.modality !== "spreadsheet" || input.operations.length !== input.receipt.operationCount || input.receipt.sequenceEnd !== artifact.headSequence + input.operations.length || committed.operationIds.length !== input.operations.length || committed.operationIds.some(
|
|
1575
|
+
(operationId, index) => operationId !== input.operations[index]?.operationId
|
|
1576
|
+
) || !causalFrontiersEqual(
|
|
1577
|
+
committed.resultingCausalFrontier,
|
|
1578
|
+
input.receipt.resultingCausalFrontier
|
|
1579
|
+
) || input.outbox.event.operationProtocolVersion !== input.receipt.operationProtocolVersion) {
|
|
1580
|
+
throw new Error("Spreadsheet commit disagrees with its CRDT receipt");
|
|
1581
|
+
}
|
|
1582
|
+
const summary = decodeCommittedTransactionSummary(committed.committedTransactionBytes);
|
|
1583
|
+
if (summary.transactionId !== input.serverTransactionId || summary.priorStateHash !== committed.priorStateHash || summary.stateHash !== committed.stateHash || summary.operationIds.length !== committed.operationIds.length) {
|
|
1584
|
+
throw new Error("Spreadsheet OGACO metadata disagrees with its record");
|
|
1585
|
+
}
|
|
1586
|
+
} else {
|
|
1587
|
+
if (input.receipt.modality !== artifact.modality || committed.modality !== artifact.modality || input.outbox.event.modality !== artifact.modality || input.operations.length !== 0 || input.receipt.sequenceEnd !== input.receipt.sequenceStart || committed.sequenceEnd !== committed.sequenceStart || input.receipt.commitProtocolVersion !== committed.commitProtocolVersion || input.receipt.priorNativeRevision !== committed.priorNativeRevision || input.receipt.nativeRevision !== committed.nativeRevision || input.receipt.commandCount !== committed.commandCount || input.outbox.event.commitProtocolVersion !== input.receipt.commitProtocolVersion) {
|
|
1588
|
+
throw new Error("Serialized commit disagrees with its authoritative receipt");
|
|
1589
|
+
}
|
|
1590
|
+
const summary = decodeEditableArtifactSerializedCommit(
|
|
1591
|
+
committed.committedTransactionBytes,
|
|
1592
|
+
artifact.modality
|
|
1593
|
+
);
|
|
1594
|
+
if (summary.transactionId !== input.serverTransactionId || summary.parentHeadSequence !== artifact.headSequence || summary.resultHeadSequence !== input.receipt.sequenceEnd || summary.priorNativeRevision !== committed.priorNativeRevision || summary.nativeReceipt.revision !== committed.nativeRevision || summary.nativeReceipt.commandCount !== committed.commandCount || summary.priorStateHash !== artifact.stateHash || summary.stateHash !== input.receipt.stateHash || summary.requestHash !== input.receipt.requestHash || !sameBytes2(summary.intentBytes, input.receipt.intentBytes) || !sameBytes2(summary.nativeReceiptBytes, committed.nativeReceiptBytes)) {
|
|
1595
|
+
throw new Error("Serialized OGAST metadata disagrees with its record");
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const idempotencyKey = receiptKey(input.receipt.actorKey, input.receipt.clientTransactionId);
|
|
1599
|
+
if (aggregate.receipts.has(idempotencyKey) || aggregate.transactions.has(input.serverTransactionId) || aggregate.committedTransactions.has(input.serverTransactionId) || aggregate.outbox.has(input.outbox.outboxId)) {
|
|
1600
|
+
throw new Error("Duplicate transaction, receipt, or outbox identity");
|
|
1601
|
+
}
|
|
1602
|
+
for (const operation of input.operations) {
|
|
1603
|
+
if (aggregate.operations.has(operation.operationId)) {
|
|
1604
|
+
throw new Error(`Duplicate operation identity: ${operation.operationId}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (input.receipt.modality === "spreadsheet") {
|
|
1608
|
+
for (const target of input.receipt.selectiveUndoOperationIds) {
|
|
1609
|
+
if (aggregate.undoClaims.has(target)) {
|
|
1610
|
+
throw new Error(`Duplicate selective undo claim: ${target}`);
|
|
1611
|
+
}
|
|
1612
|
+
aggregate.undoClaims.set(target, input.serverTransactionId);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
aggregate.receipts.set(idempotencyKey, cloneReceipt(input.receipt));
|
|
1616
|
+
aggregate.transactions.set(input.serverTransactionId, cloneReceipt(input.receipt));
|
|
1617
|
+
aggregate.committedTransactions.set(
|
|
1618
|
+
input.serverTransactionId,
|
|
1619
|
+
cloneCommittedTransaction(input.committedTransaction)
|
|
1620
|
+
);
|
|
1621
|
+
for (const operation of input.operations) {
|
|
1622
|
+
aggregate.operations.set(operation.operationId, cloneOperation(operation));
|
|
1623
|
+
}
|
|
1624
|
+
aggregate.outbox.set(input.outbox.outboxId, cloneOutbox(input.outbox));
|
|
1625
|
+
if (artifact.modality === "spreadsheet") {
|
|
1626
|
+
if (input.receipt.modality !== "spreadsheet") {
|
|
1627
|
+
throw new Error("Spreadsheet artifact received a serialized receipt");
|
|
1628
|
+
}
|
|
1629
|
+
aggregate.artifact = {
|
|
1630
|
+
...artifact,
|
|
1631
|
+
headSequence: input.receipt.sequenceEnd,
|
|
1632
|
+
causalFrontier: cloneFrontier(input.receipt.resultingCausalFrontier),
|
|
1633
|
+
stateHash: input.receipt.stateHash,
|
|
1634
|
+
updatedAt: laterIsoTimestamp(artifact.updatedAt, input.receipt.committedAt)
|
|
1635
|
+
};
|
|
1636
|
+
aggregate.checkpoints.set(input.receipt.sequenceEnd, {
|
|
1637
|
+
modality: "spreadsheet",
|
|
1638
|
+
headSequence: input.receipt.sequenceEnd,
|
|
1639
|
+
causalFrontier: cloneFrontier(input.receipt.resultingCausalFrontier),
|
|
1640
|
+
stateHash: input.receipt.stateHash
|
|
1641
|
+
});
|
|
1642
|
+
} else {
|
|
1643
|
+
if (input.receipt.modality === "spreadsheet") {
|
|
1644
|
+
throw new Error("Serialized artifact received a spreadsheet receipt");
|
|
1645
|
+
}
|
|
1646
|
+
aggregate.artifact = {
|
|
1647
|
+
...artifact,
|
|
1648
|
+
headSequence: input.receipt.sequenceEnd,
|
|
1649
|
+
stateHash: input.receipt.stateHash,
|
|
1650
|
+
updatedAt: laterIsoTimestamp(artifact.updatedAt, input.receipt.committedAt)
|
|
1651
|
+
};
|
|
1652
|
+
aggregate.checkpoints.set(input.receipt.sequenceEnd, {
|
|
1653
|
+
modality: artifact.modality,
|
|
1654
|
+
headSequence: input.receipt.sequenceEnd,
|
|
1655
|
+
nativeRevision: input.receipt.nativeRevision,
|
|
1656
|
+
stateHash: input.receipt.stateHash
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
var InMemoryEditableArtifactSnapshotUnitOfWork = class {
|
|
1661
|
+
constructor(aggregate) {
|
|
1662
|
+
this.aggregate = aggregate;
|
|
1663
|
+
}
|
|
1664
|
+
committed = false;
|
|
1665
|
+
closed = false;
|
|
1666
|
+
artifact() {
|
|
1667
|
+
this.assertOpen();
|
|
1668
|
+
return cloneArtifact(this.aggregate.artifact);
|
|
1669
|
+
}
|
|
1670
|
+
async checkpoint(headSequence) {
|
|
1671
|
+
this.assertOpen();
|
|
1672
|
+
const checkpoint = this.aggregate.checkpoints.get(headSequence);
|
|
1673
|
+
return checkpoint ? cloneCheckpoint(checkpoint) : null;
|
|
1674
|
+
}
|
|
1675
|
+
async findSnapshot(snapshotId) {
|
|
1676
|
+
this.assertOpen();
|
|
1677
|
+
const snapshot = this.aggregate.snapshots.get(snapshotId);
|
|
1678
|
+
return snapshot ? cloneSnapshot(snapshot) : null;
|
|
1679
|
+
}
|
|
1680
|
+
async commitSnapshot(input) {
|
|
1681
|
+
this.assertUncommitted();
|
|
1682
|
+
if (input.authorizationPermission !== void 0 && input.authorizationPermission !== "manage" && input.authorizationPermission !== "read" && input.authorizationPermission !== "edit") {
|
|
1683
|
+
throw new TypeError("Snapshot authorization permission is invalid");
|
|
1684
|
+
}
|
|
1685
|
+
const artifact = this.aggregate.artifact;
|
|
1686
|
+
if (input.expectedCurrentSnapshotId !== artifact.currentSnapshotId) {
|
|
1687
|
+
throw new Error("In-memory current snapshot changed inside its transaction lock");
|
|
1688
|
+
}
|
|
1689
|
+
if (input.expectedAuthorizationRevision !== artifact.authorizationRevision) {
|
|
1690
|
+
throw new Error("In-memory artifact authorization changed before snapshot commit");
|
|
1691
|
+
}
|
|
1692
|
+
if (this.aggregate.snapshots.has(input.snapshot.snapshotId) || this.aggregate.outbox.has(input.outbox.outboxId)) {
|
|
1693
|
+
throw new Error("Duplicate snapshot or outbox identity");
|
|
1694
|
+
}
|
|
1695
|
+
const checkpoint = this.aggregate.checkpoints.get(input.snapshot.coveredHeadSequence);
|
|
1696
|
+
if (!checkpoint || checkpoint.modality !== artifact.modality || input.snapshot.modality !== artifact.modality || checkpoint.stateHash !== input.snapshot.stateHash || input.snapshot.modality === "spreadsheet" && (checkpoint.modality !== "spreadsheet" || !causalFrontiersEqual(
|
|
1697
|
+
checkpoint.causalFrontier,
|
|
1698
|
+
input.snapshot.coveredCausalFrontier
|
|
1699
|
+
)) || input.snapshot.modality !== "spreadsheet" && (checkpoint.modality === "spreadsheet" || checkpoint.nativeRevision !== input.snapshot.nativeRevision)) {
|
|
1700
|
+
throw new Error("Snapshot does not match an authoritative checkpoint");
|
|
1701
|
+
}
|
|
1702
|
+
this.aggregate.snapshots.set(input.snapshot.snapshotId, cloneSnapshot(input.snapshot));
|
|
1703
|
+
this.aggregate.outbox.set(input.outbox.outboxId, cloneOutbox(input.outbox));
|
|
1704
|
+
this.aggregate.artifact = {
|
|
1705
|
+
...artifact,
|
|
1706
|
+
currentSnapshotId: input.snapshot.snapshotId,
|
|
1707
|
+
updatedAt: laterIsoTimestamp(artifact.updatedAt, input.snapshot.publishedAt)
|
|
1708
|
+
};
|
|
1709
|
+
this.committed = true;
|
|
1710
|
+
return Object.freeze({ kind: "committed" });
|
|
1711
|
+
}
|
|
1712
|
+
assertUncommitted() {
|
|
1713
|
+
this.assertOpen();
|
|
1714
|
+
if (this.committed) throw new Error("Artifact unit of work may commit exactly once");
|
|
1715
|
+
}
|
|
1716
|
+
close() {
|
|
1717
|
+
this.closed = true;
|
|
1718
|
+
}
|
|
1719
|
+
assertOpen() {
|
|
1720
|
+
if (this.closed) {
|
|
1721
|
+
throw new Error("Artifact unit of work is no longer inside its lock");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
var InMemoryEditableArtifactStableIdFactory = class {
|
|
1726
|
+
nextCounter;
|
|
1727
|
+
namespace;
|
|
1728
|
+
constructor(namespace, initialCounter = 1n) {
|
|
1729
|
+
this.namespace = typeof namespace === "bigint" ? namespace : BigInt(`0x${namespace.replace(/^0x/, "")}`);
|
|
1730
|
+
if (this.namespace < 1n || this.namespace > 0xffffffffffffffffn) {
|
|
1731
|
+
throw new TypeError("Stable id namespace must be a nonzero unsigned 64-bit integer");
|
|
1732
|
+
}
|
|
1733
|
+
if (initialCounter < 1n || initialCounter > 0xffffffffffffffffn) {
|
|
1734
|
+
throw new TypeError("Stable id counter must be a nonzero unsigned 64-bit integer");
|
|
1735
|
+
}
|
|
1736
|
+
this.nextCounter = initialCounter;
|
|
1737
|
+
}
|
|
1738
|
+
next(_kind) {
|
|
1739
|
+
if (this.nextCounter > 0xffffffffffffffffn) {
|
|
1740
|
+
throw new Error("Stable id namespace exhausted");
|
|
1741
|
+
}
|
|
1742
|
+
const value = `${this.namespace.toString(16).padStart(16, "0")}${this.nextCounter.toString(16).padStart(16, "0")}`;
|
|
1743
|
+
this.nextCounter += 1n;
|
|
1744
|
+
return editableArtifactOperationId(value);
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
var SystemEditableArtifactClock = class {
|
|
1748
|
+
now() {
|
|
1749
|
+
return /* @__PURE__ */ new Date();
|
|
1750
|
+
}
|
|
1751
|
+
};
|
|
1752
|
+
function aggregateKey(scope, artifactId) {
|
|
1753
|
+
return JSON.stringify([scope.accountId, scope.workspaceId, artifactId]);
|
|
1754
|
+
}
|
|
1755
|
+
function authorizationScopeKey(scope) {
|
|
1756
|
+
return JSON.stringify([scope.accountId, scope.workspaceId]);
|
|
1757
|
+
}
|
|
1758
|
+
function creationReceiptKey(scope, operationKind, authorityKey, idempotencyKey) {
|
|
1759
|
+
return JSON.stringify([
|
|
1760
|
+
scope.accountId,
|
|
1761
|
+
scope.workspaceId,
|
|
1762
|
+
operationKind,
|
|
1763
|
+
authorityKey,
|
|
1764
|
+
idempotencyKey
|
|
1765
|
+
]);
|
|
1766
|
+
}
|
|
1767
|
+
function emptyAggregate(artifact, genesisSnapshot, outbox) {
|
|
1768
|
+
return {
|
|
1769
|
+
artifact,
|
|
1770
|
+
receipts: /* @__PURE__ */ new Map(),
|
|
1771
|
+
transactions: /* @__PURE__ */ new Map(),
|
|
1772
|
+
committedTransactions: /* @__PURE__ */ new Map(),
|
|
1773
|
+
operations: /* @__PURE__ */ new Map(),
|
|
1774
|
+
undoClaims: /* @__PURE__ */ new Map(),
|
|
1775
|
+
snapshots: /* @__PURE__ */ new Map([[genesisSnapshot.snapshotId, cloneSnapshot(genesisSnapshot)]]),
|
|
1776
|
+
checkpoints: /* @__PURE__ */ new Map([
|
|
1777
|
+
[
|
|
1778
|
+
0,
|
|
1779
|
+
checkpointAtGenesis(
|
|
1780
|
+
artifact,
|
|
1781
|
+
genesisSnapshot.modality === "spreadsheet" ? 0 : genesisSnapshot.nativeRevision
|
|
1782
|
+
)
|
|
1783
|
+
]
|
|
1784
|
+
]),
|
|
1785
|
+
outbox: /* @__PURE__ */ new Map([[outbox.outboxId, cloneOutbox(outbox)]])
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
function assertGenesisCreateRequest(scope, artifactId, operationKind, snapshot, outbox) {
|
|
1789
|
+
assertIsoTimestamp(snapshot.verifiedAt, "genesis snapshot verification time");
|
|
1790
|
+
assertIsoTimestamp(snapshot.publishedAt, "genesis snapshot publication time");
|
|
1791
|
+
if (snapshot.scope.accountId !== scope.accountId || snapshot.scope.workspaceId !== scope.workspaceId || snapshot.artifactId !== artifactId || snapshot.coveredHeadSequence !== 0 || operationKind === "create" && snapshot.modality === "spreadsheet" && snapshot.coveredCausalFrontier.length !== 0 || Date.parse(snapshot.verifiedAt) > Date.parse(snapshot.publishedAt)) {
|
|
1792
|
+
throw new TypeError(
|
|
1793
|
+
"Genesis snapshot is not the verified sequence-zero state for this artifact"
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
if (outbox.state !== "pending" || outbox.attemptCount !== 0 || outbox.leaseOwner !== null || outbox.leaseExpiresAt !== null || outbox.nextAttemptAt !== snapshot.publishedAt || outbox.lastErrorCode !== null || outbox.publishedAt !== null || outbox.deadLetteredAt !== null || outbox.createdAt !== snapshot.publishedAt || outbox.event.kind !== "snapshot_published" || outbox.event.scope.accountId !== scope.accountId || outbox.event.scope.workspaceId !== scope.workspaceId || outbox.event.artifactId !== artifactId || outbox.event.modality !== snapshot.modality || outbox.event.snapshotId !== snapshot.snapshotId || outbox.event.coveredHeadSequence !== 0 || outbox.event.stateHash !== snapshot.stateHash || snapshot.modality === "spreadsheet" && (outbox.event.modality !== "spreadsheet" || outbox.event.operationProtocolVersion !== snapshot.operationProtocolVersion) || outbox.event.publishedAt !== snapshot.publishedAt) {
|
|
1797
|
+
throw new TypeError("Genesis snapshot outbox projection is inconsistent");
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
function receiptKey(actorKey, clientTransactionId) {
|
|
1801
|
+
return JSON.stringify([actorKey, clientTransactionId]);
|
|
1802
|
+
}
|
|
1803
|
+
function sameExpectedPredecessor(aggregate, expected) {
|
|
1804
|
+
if (expected === null) return true;
|
|
1805
|
+
const actual = aggregate.receipts.get(
|
|
1806
|
+
receiptKey(expected.actorKey, expected.clientTransactionId)
|
|
1807
|
+
);
|
|
1808
|
+
return actual !== void 0 && actual.receiptId === expected.receiptId && actual.serverTransactionId === expected.serverTransactionId && actual.replicaId === expected.replicaId && actual.replicaCounter === expected.replicaCounter;
|
|
1809
|
+
}
|
|
1810
|
+
function assertLeaseRequest(request) {
|
|
1811
|
+
assertLeaseOwner(request.owner);
|
|
1812
|
+
assertLeaseDuration(request.leaseDurationMs);
|
|
1813
|
+
assertPositiveSafeInteger(request.limit, "outbox claim limit");
|
|
1814
|
+
if (request.limit > 1e3) throw new TypeError("Outbox claim limit must not exceed 1000");
|
|
1815
|
+
}
|
|
1816
|
+
function assertLeaseDuration(value) {
|
|
1817
|
+
assertPositiveSafeInteger(value, "outbox lease duration");
|
|
1818
|
+
if (value > 24 * 60 * 60 * 1e3) {
|
|
1819
|
+
throw new TypeError("Outbox lease duration must not exceed 24 hours");
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
function assertRetryDelay(value) {
|
|
1823
|
+
assertPositiveSafeInteger(value, "outbox retry delay");
|
|
1824
|
+
if (value > 24 * 60 * 60 * 1e3) {
|
|
1825
|
+
throw new TypeError("Outbox retry delay must not exceed 24 hours");
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function assertActiveOutboxLease(record, owner, attemptCount, nowMs) {
|
|
1829
|
+
if (record.state !== "publishing" || record.leaseOwner !== owner || record.attemptCount !== attemptCount || !record.leaseExpiresAt || nowMs >= Date.parse(record.leaseExpiresAt)) {
|
|
1830
|
+
throw new EditableArtifactOutboxLeaseConflictError();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function assertRetryErrorCode(value) {
|
|
1834
|
+
if (value !== "broker_unavailable" && value !== "broker_backpressure" && value !== "publish_timeout") {
|
|
1835
|
+
throw new TypeError("Outbox retry error code is invalid");
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function assertDeadLetterErrorCode(value) {
|
|
1839
|
+
if (value !== "invalid_hint" && value !== "oversized_hint") {
|
|
1840
|
+
throw new TypeError("Outbox dead-letter error code is invalid");
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
function assertLeaseOwner(owner) {
|
|
1844
|
+
if (owner.length < 1 || owner.length > 256) {
|
|
1845
|
+
throw new TypeError("Outbox lease owner must contain 1-256 characters");
|
|
1846
|
+
}
|
|
1847
|
+
return owner;
|
|
1848
|
+
}
|
|
1849
|
+
function authoritativeNow(now) {
|
|
1850
|
+
const value = now();
|
|
1851
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
1852
|
+
throw new TypeError("Editable artifact store clock returned an invalid date");
|
|
1853
|
+
}
|
|
1854
|
+
return new Date(value);
|
|
1855
|
+
}
|
|
1856
|
+
function laterIsoTimestamp(left, right) {
|
|
1857
|
+
return Date.parse(left) >= Date.parse(right) ? left : right;
|
|
1858
|
+
}
|
|
1859
|
+
function cloneAggregate(aggregate) {
|
|
1860
|
+
return {
|
|
1861
|
+
artifact: cloneArtifact(aggregate.artifact),
|
|
1862
|
+
receipts: new Map([...aggregate.receipts].map(([key, value]) => [key, cloneReceipt(value)])),
|
|
1863
|
+
transactions: new Map(
|
|
1864
|
+
[...aggregate.transactions].map(([key, value]) => [key, cloneReceipt(value)])
|
|
1865
|
+
),
|
|
1866
|
+
committedTransactions: new Map(
|
|
1867
|
+
[...aggregate.committedTransactions].map(([key, value]) => [
|
|
1868
|
+
key,
|
|
1869
|
+
cloneCommittedTransaction(value)
|
|
1870
|
+
])
|
|
1871
|
+
),
|
|
1872
|
+
operations: new Map(
|
|
1873
|
+
[...aggregate.operations].map(([key, value]) => [key, cloneOperation(value)])
|
|
1874
|
+
),
|
|
1875
|
+
undoClaims: new Map(aggregate.undoClaims),
|
|
1876
|
+
snapshots: new Map([...aggregate.snapshots].map(([key, value]) => [key, cloneSnapshot(value)])),
|
|
1877
|
+
checkpoints: new Map(
|
|
1878
|
+
[...aggregate.checkpoints].map(([key, value]) => [key, cloneCheckpoint(value)])
|
|
1879
|
+
),
|
|
1880
|
+
outbox: new Map([...aggregate.outbox].map(([key, value]) => [key, cloneOutbox(value)]))
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function kernelStateFromAggregate(aggregate) {
|
|
1884
|
+
const snapshot = aggregate.artifact.currentSnapshotId ? aggregate.snapshots.get(aggregate.artifact.currentSnapshotId) ?? null : null;
|
|
1885
|
+
const artifact = cloneArtifact(aggregate.artifact);
|
|
1886
|
+
const covered = snapshot?.coveredHeadSequence ?? 0;
|
|
1887
|
+
const coveredCheckpoint = aggregate.checkpoints.get(covered);
|
|
1888
|
+
if (!coveredCheckpoint || coveredCheckpoint.modality !== artifact.modality) {
|
|
1889
|
+
throw new Error("Artifact replay base has no matching checkpoint");
|
|
1890
|
+
}
|
|
1891
|
+
const tail = [...aggregate.committedTransactions.values()].filter((transaction) => transaction.sequenceEnd > covered).sort((left, right) => left.sequenceStart - right.sequenceStart).map(cloneCommittedTransaction);
|
|
1892
|
+
const tailByteSize = tail.reduce(
|
|
1893
|
+
(total, transaction) => total + transaction.committedTransactionBytes.byteLength,
|
|
1894
|
+
0
|
|
1895
|
+
);
|
|
1896
|
+
const common = {
|
|
1897
|
+
tailTransactionCount: tail.length,
|
|
1898
|
+
tailByteSize
|
|
1899
|
+
};
|
|
1900
|
+
if (artifact.modality === "spreadsheet") {
|
|
1901
|
+
return Object.freeze({
|
|
1902
|
+
...common,
|
|
1903
|
+
modality: artifact.modality,
|
|
1904
|
+
artifact,
|
|
1905
|
+
snapshot: snapshot?.modality === "spreadsheet" ? cloneSnapshot(snapshot) : null,
|
|
1906
|
+
committedTransactionTail: Object.freeze(
|
|
1907
|
+
tail.map((transaction) => {
|
|
1908
|
+
if (transaction.modality !== "spreadsheet") {
|
|
1909
|
+
throw new Error("Spreadsheet artifact contains a cross-modality transaction");
|
|
1910
|
+
}
|
|
1911
|
+
return transaction;
|
|
1912
|
+
})
|
|
1913
|
+
)
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
return Object.freeze({
|
|
1917
|
+
...common,
|
|
1918
|
+
modality: artifact.modality,
|
|
1919
|
+
artifact,
|
|
1920
|
+
snapshot: snapshot && snapshot.modality !== "spreadsheet" ? cloneSnapshot(snapshot) : null,
|
|
1921
|
+
baseNativeRevision: coveredCheckpoint.modality === "spreadsheet" ? (() => {
|
|
1922
|
+
throw new Error("Serialized artifact has a spreadsheet replay checkpoint");
|
|
1923
|
+
})() : coveredCheckpoint.nativeRevision,
|
|
1924
|
+
committedTransactionTail: Object.freeze(
|
|
1925
|
+
tail.map((transaction) => {
|
|
1926
|
+
if (transaction.modality !== artifact.modality) {
|
|
1927
|
+
throw new Error("Serialized artifact contains a cross-modality transaction");
|
|
1928
|
+
}
|
|
1929
|
+
return transaction;
|
|
1930
|
+
})
|
|
1931
|
+
)
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
function cloneArtifact(artifact) {
|
|
1935
|
+
if (artifact.modality !== "spreadsheet") {
|
|
1936
|
+
return Object.freeze({
|
|
1937
|
+
...artifact,
|
|
1938
|
+
scope: Object.freeze({ ...artifact.scope })
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
return Object.freeze({
|
|
1942
|
+
...artifact,
|
|
1943
|
+
scope: Object.freeze({ ...artifact.scope }),
|
|
1944
|
+
causalFrontier: cloneFrontier(artifact.causalFrontier)
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
function cloneCreationReceipt(receipt) {
|
|
1948
|
+
return Object.freeze({
|
|
1949
|
+
...receipt,
|
|
1950
|
+
scope: Object.freeze({ ...receipt.scope })
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
function cloneReceipt(receipt) {
|
|
1954
|
+
if (receipt.modality !== "spreadsheet") {
|
|
1955
|
+
return Object.freeze({
|
|
1956
|
+
...receipt,
|
|
1957
|
+
scope: Object.freeze({ ...receipt.scope }),
|
|
1958
|
+
intentBytes: receipt.intentBytes.slice()
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
return Object.freeze({
|
|
1962
|
+
...receipt,
|
|
1963
|
+
scope: Object.freeze({ ...receipt.scope }),
|
|
1964
|
+
intentBytes: receipt.intentBytes.slice(),
|
|
1965
|
+
causalBase: cloneFrontier(receipt.causalBase),
|
|
1966
|
+
resolvedCausalBase: cloneFrontier(receipt.resolvedCausalBase),
|
|
1967
|
+
resultingCausalFrontier: cloneFrontier(receipt.resultingCausalFrontier),
|
|
1968
|
+
selectiveUndoOperationIds: Object.freeze([...receipt.selectiveUndoOperationIds])
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
function cloneOperation(operation) {
|
|
1972
|
+
return Object.freeze({
|
|
1973
|
+
...operation,
|
|
1974
|
+
scope: Object.freeze({ ...operation.scope }),
|
|
1975
|
+
dot: Object.freeze({ ...operation.dot })
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
function cloneCommittedTransaction(transaction) {
|
|
1979
|
+
if (transaction.modality !== "spreadsheet") {
|
|
1980
|
+
return Object.freeze({
|
|
1981
|
+
...transaction,
|
|
1982
|
+
scope: Object.freeze({ ...transaction.scope }),
|
|
1983
|
+
nativeReceiptBytes: transaction.nativeReceiptBytes.slice(),
|
|
1984
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
return Object.freeze({
|
|
1988
|
+
...transaction,
|
|
1989
|
+
scope: Object.freeze({ ...transaction.scope }),
|
|
1990
|
+
dot: Object.freeze({ ...transaction.dot }),
|
|
1991
|
+
resolvedCausalBase: cloneFrontier(transaction.resolvedCausalBase),
|
|
1992
|
+
resultingCausalFrontier: cloneFrontier(transaction.resultingCausalFrontier),
|
|
1993
|
+
operationIds: Object.freeze([...transaction.operationIds]),
|
|
1994
|
+
committedTransactionBytes: transaction.committedTransactionBytes.slice()
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function cloneSnapshot(snapshot) {
|
|
1998
|
+
if (snapshot.modality !== "spreadsheet") {
|
|
1999
|
+
return Object.freeze({
|
|
2000
|
+
...snapshot,
|
|
2001
|
+
scope: Object.freeze({ ...snapshot.scope })
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
return Object.freeze({
|
|
2005
|
+
...snapshot,
|
|
2006
|
+
scope: Object.freeze({ ...snapshot.scope }),
|
|
2007
|
+
coveredCausalFrontier: cloneFrontier(snapshot.coveredCausalFrontier)
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
function cloneCheckpoint(checkpoint) {
|
|
2011
|
+
if (checkpoint.modality !== "spreadsheet") {
|
|
2012
|
+
return Object.freeze({ ...checkpoint });
|
|
2013
|
+
}
|
|
2014
|
+
return Object.freeze({
|
|
2015
|
+
...checkpoint,
|
|
2016
|
+
causalFrontier: cloneFrontier(checkpoint.causalFrontier)
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
function checkpointAtGenesis(artifact, nativeRevision = 0) {
|
|
2020
|
+
return artifact.modality === "spreadsheet" ? Object.freeze({
|
|
2021
|
+
modality: "spreadsheet",
|
|
2022
|
+
headSequence: 0,
|
|
2023
|
+
causalFrontier: editableArtifactCausalFrontier([]),
|
|
2024
|
+
stateHash: artifact.stateHash
|
|
2025
|
+
}) : Object.freeze({
|
|
2026
|
+
modality: artifact.modality,
|
|
2027
|
+
headSequence: 0,
|
|
2028
|
+
nativeRevision,
|
|
2029
|
+
stateHash: artifact.stateHash
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
function sameBytes2(left, right) {
|
|
2033
|
+
if (left.byteLength !== right.byteLength) return false;
|
|
2034
|
+
let difference = 0;
|
|
2035
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
2036
|
+
difference |= left[index] ^ right[index];
|
|
2037
|
+
}
|
|
2038
|
+
return difference === 0;
|
|
2039
|
+
}
|
|
2040
|
+
function cloneOutbox(record) {
|
|
2041
|
+
return Object.freeze({
|
|
2042
|
+
...record,
|
|
2043
|
+
event: Object.freeze({
|
|
2044
|
+
...record.event,
|
|
2045
|
+
scope: Object.freeze({ ...record.event.scope })
|
|
2046
|
+
})
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
function cloneFrontier(frontier) {
|
|
2050
|
+
return Object.freeze(frontier.map((entry) => Object.freeze({ ...entry })));
|
|
2051
|
+
}
|
|
2052
|
+
function compareText(left, right) {
|
|
2053
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/domain/editable-artifacts/postgres-adapters.ts
|
|
2057
|
+
function editableArtifactStorePortFromPostgres(store) {
|
|
2058
|
+
return store;
|
|
2059
|
+
}
|
|
2060
|
+
function editableArtifactAuthorizationRevisionPortFromPostgres(store) {
|
|
2061
|
+
return store;
|
|
2062
|
+
}
|
|
2063
|
+
function editableArtifactScopeAuthorizationRevisionPortFromPostgres(store) {
|
|
2064
|
+
return store;
|
|
2065
|
+
}
|
|
2066
|
+
function editableArtifactLiveReadPortFromPostgres(store) {
|
|
2067
|
+
return store;
|
|
2068
|
+
}
|
|
2069
|
+
function editableArtifactLiveTicketStorePortFromPostgres(store) {
|
|
2070
|
+
return store;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// src/domain/editable-artifacts/snapshot-compaction.ts
|
|
2074
|
+
import { BoundedObjectWriteError } from "@opengeni/storage";
|
|
2075
|
+
var SNAPSHOT_MIME_TYPE = "application/vnd.opengeni.editable-artifact-snapshot";
|
|
2076
|
+
var EditableArtifactCompactionPipeline = class {
|
|
2077
|
+
#dependencies;
|
|
2078
|
+
constructor(dependencies) {
|
|
2079
|
+
const maxSnapshotBytes = dependencies.maxSnapshotBytes ?? MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES;
|
|
2080
|
+
if (!Number.isSafeInteger(maxSnapshotBytes) || maxSnapshotBytes <= 0 || maxSnapshotBytes > MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES) {
|
|
2081
|
+
throw new TypeError("Compaction snapshot limit is invalid");
|
|
2082
|
+
}
|
|
2083
|
+
this.#dependencies = Object.freeze({ ...dependencies, maxSnapshotBytes });
|
|
2084
|
+
}
|
|
2085
|
+
async prepare(input) {
|
|
2086
|
+
const scope = editableArtifactScope(input.scope);
|
|
2087
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
2088
|
+
const snapshotId = editableArtifactSnapshotId(input.snapshotId);
|
|
2089
|
+
validateBasis(input.state, scope, artifactId);
|
|
2090
|
+
throwIfCancelled(input.signal);
|
|
2091
|
+
try {
|
|
2092
|
+
const generated = await this.#dependencies.kernel.createCanonicalSnapshot({
|
|
2093
|
+
state: input.state,
|
|
2094
|
+
...input.signal ? { signal: input.signal } : {}
|
|
2095
|
+
});
|
|
2096
|
+
validateGenerated(generated, input.state, scope, artifactId);
|
|
2097
|
+
const stored = await this.#dependencies.objects.write({
|
|
2098
|
+
chunks: generated.canonicalChunks,
|
|
2099
|
+
contentType: SNAPSHOT_MIME_TYPE,
|
|
2100
|
+
maxBytes: this.#dependencies.maxSnapshotBytes,
|
|
2101
|
+
expectedByteSize: generated.canonicalByteSize,
|
|
2102
|
+
expectedContentHash: generated.canonicalContentHash,
|
|
2103
|
+
...input.signal ? { signal: input.signal } : {}
|
|
2104
|
+
});
|
|
2105
|
+
const common = {
|
|
2106
|
+
snapshotId,
|
|
2107
|
+
blobReference: stored.opaqueReference,
|
|
2108
|
+
byteSize: stored.byteSize,
|
|
2109
|
+
contentHash: editableArtifactContentHash(stored.contentHash),
|
|
2110
|
+
mimeType: SNAPSHOT_MIME_TYPE,
|
|
2111
|
+
modality: generated.modality,
|
|
2112
|
+
coveredHeadSequence: generated.coveredHeadSequence,
|
|
2113
|
+
stateHash: generated.stateHash,
|
|
2114
|
+
modelSchemaVersion: generated.modelSchemaVersion,
|
|
2115
|
+
kernelVersion: generated.kernelVersion,
|
|
2116
|
+
verifiedAt: canonicalTimestamp(this.#dependencies.now())
|
|
2117
|
+
};
|
|
2118
|
+
return generated.modality === "spreadsheet" ? Object.freeze({
|
|
2119
|
+
...common,
|
|
2120
|
+
modality: generated.modality,
|
|
2121
|
+
coveredCausalFrontier: generated.coveredCausalFrontier,
|
|
2122
|
+
operationProtocolVersion: generated.operationProtocolVersion,
|
|
2123
|
+
crdtStateVersion: generated.crdtStateVersion
|
|
2124
|
+
}) : Object.freeze({
|
|
2125
|
+
...common,
|
|
2126
|
+
modality: generated.modality,
|
|
2127
|
+
nativeRevision: generated.nativeRevision
|
|
2128
|
+
});
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
if (error instanceof EditableArtifactSnapshotVerificationError || error instanceof BoundedObjectWriteError) {
|
|
2131
|
+
throw error;
|
|
2132
|
+
}
|
|
2133
|
+
if (input.signal?.aborted) {
|
|
2134
|
+
throw new EditableArtifactSnapshotVerificationError("cancelled");
|
|
2135
|
+
}
|
|
2136
|
+
throw new EditableArtifactSnapshotVerificationError("kernel_rejected");
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
function validateBasis(state, scope, artifactId) {
|
|
2141
|
+
if (state.artifact.scope.accountId !== scope.accountId || state.artifact.scope.workspaceId !== scope.workspaceId || state.artifact.id !== artifactId || state.artifact.modality !== state.modality) {
|
|
2142
|
+
throw new EditableArtifactSnapshotVerificationError("identity_mismatch");
|
|
2143
|
+
}
|
|
2144
|
+
for (const value of [state.tailTransactionCount, state.tailByteSize]) {
|
|
2145
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2146
|
+
throw new EditableArtifactSnapshotVerificationError("coverage_mismatch");
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
function validateGenerated(value, state, scope, artifactId) {
|
|
2151
|
+
if (value.scope.accountId !== scope.accountId || value.scope.workspaceId !== scope.workspaceId || value.artifactId !== artifactId || value.modality !== state.modality) {
|
|
2152
|
+
throw new EditableArtifactSnapshotVerificationError("identity_mismatch");
|
|
2153
|
+
}
|
|
2154
|
+
if (value.coveredHeadSequence !== state.artifact.headSequence || editableArtifactStateHash(value.stateHash) !== state.artifact.stateHash || value.modality === "spreadsheet" && (state.modality !== "spreadsheet" || !causalFrontiersEqual(
|
|
2155
|
+
editableArtifactCausalFrontier(value.coveredCausalFrontier),
|
|
2156
|
+
state.artifact.causalFrontier
|
|
2157
|
+
)) || value.modality !== "spreadsheet" && (state.modality === "spreadsheet" || value.nativeRevision !== expectedNativeRevision(state))) {
|
|
2158
|
+
throw new EditableArtifactSnapshotVerificationError("coverage_mismatch");
|
|
2159
|
+
}
|
|
2160
|
+
if (!Number.isSafeInteger(value.canonicalByteSize) || value.canonicalByteSize <= 0 || value.canonicalByteSize > MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES) {
|
|
2161
|
+
throw new EditableArtifactSnapshotVerificationError("limit_exceeded");
|
|
2162
|
+
}
|
|
2163
|
+
editableArtifactContentHash(value.canonicalContentHash);
|
|
2164
|
+
const versions = value.modality === "spreadsheet" ? [value.modelSchemaVersion, value.operationProtocolVersion, value.crdtStateVersion] : [value.modelSchemaVersion];
|
|
2165
|
+
if (versions.some((version) => !Number.isSafeInteger(version) || version <= 0)) {
|
|
2166
|
+
throw new EditableArtifactSnapshotVerificationError("version_mismatch");
|
|
2167
|
+
}
|
|
2168
|
+
try {
|
|
2169
|
+
assertBoundedKernelVersion(value.kernelVersion);
|
|
2170
|
+
} catch {
|
|
2171
|
+
throw new EditableArtifactSnapshotVerificationError("version_mismatch");
|
|
2172
|
+
}
|
|
2173
|
+
if (!value.canonicalChunks || typeof value.canonicalChunks[Symbol.asyncIterator] !== "function") {
|
|
2174
|
+
throw new EditableArtifactSnapshotVerificationError("kernel_rejected");
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
function expectedNativeRevision(state) {
|
|
2178
|
+
return state.committedTransactionTail.at(-1)?.nativeRevision ?? state.snapshot?.nativeRevision ?? state.baseNativeRevision;
|
|
2179
|
+
}
|
|
2180
|
+
function canonicalTimestamp(value) {
|
|
2181
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
2182
|
+
throw new TypeError("Compaction verification clock returned an invalid date");
|
|
2183
|
+
}
|
|
2184
|
+
return value.toISOString();
|
|
2185
|
+
}
|
|
2186
|
+
function throwIfCancelled(signal) {
|
|
2187
|
+
if (signal?.aborted) throw new EditableArtifactSnapshotVerificationError("cancelled");
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/domain/editable-artifacts/snapshot-verifier-genesis.ts
|
|
2191
|
+
import { BoundedObjectWriteError as BoundedObjectWriteError2 } from "@opengeni/storage";
|
|
2192
|
+
var SNAPSHOT_MIME_TYPE2 = "application/vnd.opengeni.editable-artifact-snapshot";
|
|
2193
|
+
var EditableArtifactGenesisPipeline = class {
|
|
2194
|
+
#dependencies;
|
|
2195
|
+
constructor(dependencies) {
|
|
2196
|
+
const maxSnapshotBytes = dependencies.maxSnapshotBytes ?? MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES;
|
|
2197
|
+
if (!Number.isSafeInteger(maxSnapshotBytes) || maxSnapshotBytes <= 0 || maxSnapshotBytes > MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES) {
|
|
2198
|
+
throw new TypeError("Genesis snapshot limit is invalid");
|
|
2199
|
+
}
|
|
2200
|
+
this.#dependencies = Object.freeze({
|
|
2201
|
+
kernel: dependencies.kernel,
|
|
2202
|
+
objects: dependencies.objects,
|
|
2203
|
+
now: dependencies.now,
|
|
2204
|
+
maxSnapshotBytes
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
async prepare(input) {
|
|
2208
|
+
const scope = editableArtifactScope(input.scope);
|
|
2209
|
+
const artifactId = editableArtifactId(input.artifactId);
|
|
2210
|
+
const snapshotId = editableArtifactSnapshotId(input.snapshotId);
|
|
2211
|
+
if (!isModality(input.modality)) {
|
|
2212
|
+
throw new TypeError("Genesis modality is invalid");
|
|
2213
|
+
}
|
|
2214
|
+
throwIfGenesisCancelled(input.signal);
|
|
2215
|
+
try {
|
|
2216
|
+
const generated = await this.#dependencies.kernel.createCanonicalEmptySnapshot({
|
|
2217
|
+
scope,
|
|
2218
|
+
artifactId,
|
|
2219
|
+
modality: input.modality,
|
|
2220
|
+
...input.signal ? { signal: input.signal } : {}
|
|
2221
|
+
});
|
|
2222
|
+
validateGeneratedGenesis(generated, scope, artifactId, input.modality);
|
|
2223
|
+
const stored = await this.#dependencies.objects.write({
|
|
2224
|
+
chunks: generated.canonicalChunks,
|
|
2225
|
+
contentType: SNAPSHOT_MIME_TYPE2,
|
|
2226
|
+
maxBytes: this.#dependencies.maxSnapshotBytes,
|
|
2227
|
+
expectedByteSize: generated.canonicalByteSize,
|
|
2228
|
+
expectedContentHash: generated.canonicalContentHash,
|
|
2229
|
+
...input.signal ? { signal: input.signal } : {}
|
|
2230
|
+
});
|
|
2231
|
+
const verifiedAt = canonicalTimestamp2(this.#dependencies.now());
|
|
2232
|
+
const common = {
|
|
2233
|
+
snapshotId,
|
|
2234
|
+
blobReference: stored.opaqueReference,
|
|
2235
|
+
byteSize: stored.byteSize,
|
|
2236
|
+
contentHash: editableArtifactContentHash(stored.contentHash),
|
|
2237
|
+
mimeType: SNAPSHOT_MIME_TYPE2,
|
|
2238
|
+
modality: generated.modality,
|
|
2239
|
+
coveredHeadSequence: 0,
|
|
2240
|
+
stateHash: generated.stateHash,
|
|
2241
|
+
modelSchemaVersion: generated.modelSchemaVersion,
|
|
2242
|
+
kernelVersion: generated.kernelVersion,
|
|
2243
|
+
verifiedAt
|
|
2244
|
+
};
|
|
2245
|
+
return generated.modality === "spreadsheet" ? Object.freeze({
|
|
2246
|
+
...common,
|
|
2247
|
+
modality: "spreadsheet",
|
|
2248
|
+
coveredCausalFrontier: editableArtifactCausalFrontier([]),
|
|
2249
|
+
operationProtocolVersion: generated.operationProtocolVersion,
|
|
2250
|
+
crdtStateVersion: generated.crdtStateVersion
|
|
2251
|
+
}) : Object.freeze({
|
|
2252
|
+
...common,
|
|
2253
|
+
modality: generated.modality,
|
|
2254
|
+
nativeRevision: generated.nativeRevision
|
|
2255
|
+
});
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
if (error instanceof EditableArtifactSnapshotVerificationError || error instanceof BoundedObjectWriteError2) {
|
|
2258
|
+
throw error;
|
|
2259
|
+
}
|
|
2260
|
+
if (input.signal?.aborted) {
|
|
2261
|
+
throw new EditableArtifactSnapshotVerificationError("cancelled");
|
|
2262
|
+
}
|
|
2263
|
+
throw new EditableArtifactSnapshotVerificationError("kernel_rejected");
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
function validateGeneratedGenesis(value, scope, artifactId, modality) {
|
|
2268
|
+
if (value.scope.accountId !== scope.accountId || value.scope.workspaceId !== scope.workspaceId || value.artifactId !== artifactId || value.modality !== modality) {
|
|
2269
|
+
throw new EditableArtifactSnapshotVerificationError("identity_mismatch");
|
|
2270
|
+
}
|
|
2271
|
+
if (value.coveredHeadSequence !== 0 || (value.modality === "spreadsheet" ? editableArtifactCausalFrontier(value.coveredCausalFrontier).length !== 0 : value.nativeRevision !== 0)) {
|
|
2272
|
+
throw new EditableArtifactSnapshotVerificationError("coverage_mismatch");
|
|
2273
|
+
}
|
|
2274
|
+
if (!Number.isSafeInteger(value.canonicalByteSize) || value.canonicalByteSize <= 0 || value.canonicalByteSize > MAX_EDITABLE_ARTIFACT_SNAPSHOT_BYTES) {
|
|
2275
|
+
throw new EditableArtifactSnapshotVerificationError("limit_exceeded");
|
|
2276
|
+
}
|
|
2277
|
+
editableArtifactContentHash(value.canonicalContentHash);
|
|
2278
|
+
editableArtifactStateHash(value.stateHash);
|
|
2279
|
+
const versions = value.modality === "spreadsheet" ? [value.modelSchemaVersion, value.operationProtocolVersion, value.crdtStateVersion] : [value.modelSchemaVersion];
|
|
2280
|
+
for (const version of versions) {
|
|
2281
|
+
if (!Number.isSafeInteger(version) || version <= 0) {
|
|
2282
|
+
throw new EditableArtifactSnapshotVerificationError("version_mismatch");
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
assertBoundedKernelVersion(value.kernelVersion);
|
|
2287
|
+
} catch {
|
|
2288
|
+
throw new EditableArtifactSnapshotVerificationError("version_mismatch");
|
|
2289
|
+
}
|
|
2290
|
+
if (!value.canonicalChunks || typeof value.canonicalChunks[Symbol.asyncIterator] !== "function") {
|
|
2291
|
+
throw new EditableArtifactSnapshotVerificationError("kernel_rejected");
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function canonicalTimestamp2(value) {
|
|
2295
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
2296
|
+
throw new TypeError("Genesis verification clock returned an invalid date");
|
|
2297
|
+
}
|
|
2298
|
+
return value.toISOString();
|
|
2299
|
+
}
|
|
2300
|
+
function isModality(value) {
|
|
2301
|
+
return value === "spreadsheet" || value === "presentation" || value === "document";
|
|
2302
|
+
}
|
|
2303
|
+
function throwIfGenesisCancelled(signal) {
|
|
2304
|
+
if (signal?.aborted) {
|
|
2305
|
+
throw new EditableArtifactSnapshotVerificationError("cancelled");
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/domain/editable-artifacts/snapshot-verifier-materialization.ts
|
|
2310
|
+
import { createHash as createHash3 } from "crypto";
|
|
2311
|
+
import { EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES } from "@opengeni/contracts/editable-artifacts";
|
|
2312
|
+
var SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
2313
|
+
var FORMAT_PATTERN = /^[a-z0-9][a-z0-9.+_-]{0,63}$/;
|
|
2314
|
+
var VERSION_PATTERN = /^[\x21-\x7e]+$/;
|
|
2315
|
+
function editableArtifactMaterializationKey(input) {
|
|
2316
|
+
for (const [label, value] of [
|
|
2317
|
+
["state hash", input.stateHash],
|
|
2318
|
+
["options hash", input.optionsHash],
|
|
2319
|
+
["font registry hash", input.fontRegistryHash],
|
|
2320
|
+
["policy hash", input.policyHash]
|
|
2321
|
+
]) {
|
|
2322
|
+
if (!SHA256_PATTERN.test(value)) {
|
|
2323
|
+
throw new TypeError(`${label} must be a canonical SHA-256 digest`);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (!FORMAT_PATTERN.test(input.format)) {
|
|
2327
|
+
throw new TypeError("materialization format is invalid");
|
|
2328
|
+
}
|
|
2329
|
+
if (!VERSION_PATTERN.test(input.codecVersion) || input.codecVersion.length > 128) {
|
|
2330
|
+
throw new TypeError("codec version is invalid");
|
|
2331
|
+
}
|
|
2332
|
+
if (!VERSION_PATTERN.test(input.kernelVersion) || input.kernelVersion.length > EDITABLE_ARTIFACT_KERNEL_VERSION_MAX_BYTES) {
|
|
2333
|
+
throw new TypeError("kernel version is invalid");
|
|
2334
|
+
}
|
|
2335
|
+
const fields = [
|
|
2336
|
+
input.stateHash,
|
|
2337
|
+
input.format,
|
|
2338
|
+
input.optionsHash,
|
|
2339
|
+
input.codecVersion,
|
|
2340
|
+
input.kernelVersion,
|
|
2341
|
+
input.fontRegistryHash,
|
|
2342
|
+
input.policyHash
|
|
2343
|
+
];
|
|
2344
|
+
const digest = createHash3("sha256");
|
|
2345
|
+
digest.update("OpenGeni editable artifact materialization\0v1\0", "utf8");
|
|
2346
|
+
for (const field of fields) {
|
|
2347
|
+
const bytes = Buffer.from(field, "utf8");
|
|
2348
|
+
const length = Buffer.allocUnsafe(4);
|
|
2349
|
+
length.writeUInt32BE(bytes.byteLength);
|
|
2350
|
+
digest.update(length);
|
|
2351
|
+
digest.update(bytes);
|
|
2352
|
+
}
|
|
2353
|
+
return `materialization:v1:sha256:${digest.digest("hex")}`;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
export {
|
|
2357
|
+
EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES,
|
|
2358
|
+
EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES,
|
|
2359
|
+
EditableArtifactDurableExportError,
|
|
2360
|
+
EditableArtifactDurableExportService,
|
|
2361
|
+
editableArtifactMaterializationMimeType,
|
|
2362
|
+
editableArtifactFormatMatchesModality,
|
|
2363
|
+
validateEditableArtifactPinnedVersion,
|
|
2364
|
+
validateEditableArtifactMaterializationJob,
|
|
2365
|
+
EditableArtifactOfficeImportError,
|
|
2366
|
+
EditableArtifactAgentApplication,
|
|
2367
|
+
editableArtifactDurableExportStorePortFromPostgres,
|
|
2368
|
+
InMemoryEditableArtifactStore,
|
|
2369
|
+
InMemoryEditableArtifactStableIdFactory,
|
|
2370
|
+
SystemEditableArtifactClock,
|
|
2371
|
+
editableArtifactStorePortFromPostgres,
|
|
2372
|
+
editableArtifactAuthorizationRevisionPortFromPostgres,
|
|
2373
|
+
editableArtifactScopeAuthorizationRevisionPortFromPostgres,
|
|
2374
|
+
editableArtifactLiveReadPortFromPostgres,
|
|
2375
|
+
editableArtifactLiveTicketStorePortFromPostgres,
|
|
2376
|
+
EditableArtifactCompactionPipeline,
|
|
2377
|
+
EditableArtifactGenesisPipeline,
|
|
2378
|
+
editableArtifactMaterializationKey
|
|
2379
|
+
};
|
|
2380
|
+
//# sourceMappingURL=chunk-6WKPWE5S.js.map
|