@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.
Files changed (116) hide show
  1. package/dist/access/index.d.ts +1 -1
  2. package/dist/application/new-session-drafts.d.ts +1 -1
  3. package/dist/application/session-commands.d.ts +2 -2
  4. package/dist/billing/limits.d.ts +1 -1
  5. package/dist/chunk-6WKPWE5S.js +2380 -0
  6. package/dist/chunk-6WKPWE5S.js.map +1 -0
  7. package/dist/chunk-JJU6I5XD.js +2713 -0
  8. package/dist/chunk-JJU6I5XD.js.map +1 -0
  9. package/dist/chunk-NYPMQ7NO.js +2270 -0
  10. package/dist/chunk-NYPMQ7NO.js.map +1 -0
  11. package/dist/dependencies.d.ts +26 -6
  12. package/dist/domain/capabilities.d.ts +28 -3
  13. package/dist/domain/company-profile-durable-learning-adapter.d.ts +39 -0
  14. package/dist/domain/durable-learning-slack-publication.d.ts +72 -0
  15. package/dist/domain/editable-artifacts/agent-application.d.ts +192 -0
  16. package/dist/domain/editable-artifacts/durable-export-postgres.d.ts +4 -0
  17. package/dist/domain/editable-artifacts/durable-export.d.ts +212 -0
  18. package/dist/domain/editable-artifacts/errors.d.ts +47 -0
  19. package/dist/domain/editable-artifacts/hash.d.ts +11 -0
  20. package/dist/domain/editable-artifacts/in-memory.d.ts +99 -0
  21. package/dist/domain/editable-artifacts/index.d.ts +14 -0
  22. package/dist/domain/editable-artifacts/ports.d.ts +341 -0
  23. package/dist/domain/editable-artifacts/postgres-adapters.d.ts +13 -0
  24. package/dist/domain/editable-artifacts/service.d.ts +105 -0
  25. package/dist/domain/editable-artifacts/snapshot-compaction.d.ts +44 -0
  26. package/dist/domain/editable-artifacts/snapshot-verifier-genesis.d.ts +50 -0
  27. package/dist/domain/editable-artifacts/snapshot-verifier-materialization.d.ts +21 -0
  28. package/dist/domain/editable-artifacts/snapshot-verifier.d.ts +118 -0
  29. package/dist/domain/editable-artifacts/types.d.ts +410 -0
  30. package/dist/domain/memory-slack-delivery.d.ts +19 -0
  31. package/dist/domain/packs.d.ts +29 -1
  32. package/dist/domain/personal-connection-delegations.d.ts +9 -1
  33. package/dist/domain/scheduled-tasks.d.ts +2 -2
  34. package/dist/domain/session-tool-policy.d.ts +2 -2
  35. package/dist/domain/sessions.d.ts +14 -6
  36. package/dist/domain/skill-imports.d.ts +32 -0
  37. package/dist/domain/slack-publication-secret-safety.d.ts +8 -0
  38. package/dist/domain/timeline-annotations.d.ts +5 -0
  39. package/dist/domain/video-generation-capabilities.d.ts +9 -0
  40. package/dist/domain/video-generation.d.ts +47 -0
  41. package/dist/editable-artifact-live/application.d.ts +69 -0
  42. package/dist/editable-artifact-live/errors.d.ts +1 -0
  43. package/dist/editable-artifact-live/index.d.ts +8 -0
  44. package/dist/editable-artifact-live/outbox-dispatcher.d.ts +162 -0
  45. package/dist/editable-artifact-live/ports.d.ts +100 -0
  46. package/dist/editable-artifact-live/server.d.ts +71 -0
  47. package/dist/editable-artifact-live/ticket.d.ts +40 -0
  48. package/dist/editable-artifact-live/types.d.ts +213 -0
  49. package/dist/editable-artifact-live/wire.d.ts +34 -0
  50. package/dist/editable-artifact-live.d.ts +2 -0
  51. package/dist/editable-artifact-live.js +58 -0
  52. package/dist/editable-artifact-live.js.map +1 -0
  53. package/dist/editable-artifacts.d.ts +2 -0
  54. package/dist/editable-artifacts.js +179 -0
  55. package/dist/editable-artifacts.js.map +1 -0
  56. package/dist/index.d.ts +35 -26
  57. package/dist/index.js +2792 -609
  58. package/dist/index.js.map +1 -1
  59. package/dist/managed-session.d.ts +1 -1
  60. package/dist/rigs/index.d.ts +1 -1
  61. package/dist/rigs/provider-images.d.ts +31 -0
  62. package/dist/sandbox/routing.d.ts +1 -0
  63. package/dist/session-authorization.d.ts +1 -1
  64. package/dist/workflow-wake-contract.d.ts +7 -0
  65. package/package.json +18 -10
  66. package/src/application/new-session-drafts.ts +2 -0
  67. package/src/application/session-commands.ts +188 -107
  68. package/src/dependencies.ts +30 -2
  69. package/src/domain/capabilities.ts +604 -220
  70. package/src/domain/company-profile-durable-learning-adapter.ts +221 -0
  71. package/src/domain/durable-learning-slack-publication.ts +202 -0
  72. package/src/domain/editable-artifacts/agent-application.ts +687 -0
  73. package/src/domain/editable-artifacts/durable-export-postgres.ts +139 -0
  74. package/src/domain/editable-artifacts/durable-export.ts +734 -0
  75. package/src/domain/editable-artifacts/errors.ts +115 -0
  76. package/src/domain/editable-artifacts/hash.ts +131 -0
  77. package/src/domain/editable-artifacts/in-memory.ts +1662 -0
  78. package/src/domain/editable-artifacts/index.ts +14 -0
  79. package/src/domain/editable-artifacts/ports.ts +462 -0
  80. package/src/domain/editable-artifacts/postgres-adapters.ts +49 -0
  81. package/src/domain/editable-artifacts/service.ts +2402 -0
  82. package/src/domain/editable-artifacts/snapshot-compaction.ts +246 -0
  83. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +235 -0
  84. package/src/domain/editable-artifacts/snapshot-verifier-materialization.ts +74 -0
  85. package/src/domain/editable-artifacts/snapshot-verifier.ts +687 -0
  86. package/src/domain/editable-artifacts/types.ts +835 -0
  87. package/src/domain/insights.ts +136 -33
  88. package/src/domain/memory-slack-delivery.ts +204 -0
  89. package/src/domain/memory-slack-publication.ts +5 -2
  90. package/src/domain/packs.ts +266 -5
  91. package/src/domain/personal-connection-delegations.ts +97 -6
  92. package/src/domain/scheduled-tasks.ts +155 -29
  93. package/src/domain/session-tool-policy.ts +8 -2
  94. package/src/domain/sessions.ts +274 -131
  95. package/src/domain/skill-imports.ts +350 -0
  96. package/src/domain/slack-publication-secret-safety.ts +42 -0
  97. package/src/domain/timeline-annotations.ts +204 -0
  98. package/src/domain/video-generation-capabilities.ts +69 -0
  99. package/src/domain/video-generation.ts +249 -0
  100. package/src/editable-artifact-live/application.ts +182 -0
  101. package/src/editable-artifact-live/errors.ts +4 -0
  102. package/src/editable-artifact-live/index.ts +8 -0
  103. package/src/editable-artifact-live/outbox-dispatcher.ts +916 -0
  104. package/src/editable-artifact-live/ports.ts +139 -0
  105. package/src/editable-artifact-live/server.ts +1707 -0
  106. package/src/editable-artifact-live/ticket.ts +217 -0
  107. package/src/editable-artifact-live/types.ts +286 -0
  108. package/src/editable-artifact-live/wire.ts +95 -0
  109. package/src/editable-artifact-live.ts +2 -0
  110. package/src/editable-artifacts.ts +2 -0
  111. package/src/index.ts +13 -0
  112. package/src/rigs/index.ts +26 -19
  113. package/src/rigs/provider-images.ts +110 -0
  114. package/src/sandbox/routing.ts +6 -3
  115. package/src/session-authorization.ts +3 -0
  116. package/src/workflow-wake-contract.ts +8 -0
@@ -0,0 +1,734 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import type { BoundedObjectRead, BoundedObjectReadPort } from "@opengeni/storage";
4
+
5
+ import type { EditableArtifactAuthorizationPort } from "./ports";
6
+ import {
7
+ assertIsoTimestamp,
8
+ editableArtifactActorKey,
9
+ editableArtifactContentHash,
10
+ editableArtifactId,
11
+ editableArtifactScope,
12
+ editableArtifactSnapshotId,
13
+ editableArtifactStateHash,
14
+ validateEditableArtifactActor,
15
+ type EditableArtifactActor,
16
+ type EditableArtifactCausalFrontier,
17
+ type EditableArtifactContentHash,
18
+ type EditableArtifactId,
19
+ type EditableArtifactModality,
20
+ type EditableArtifactScope,
21
+ type EditableArtifactSnapshotMetadata,
22
+ type EditableArtifactStateHash,
23
+ } from "./types";
24
+
25
+ export const EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
26
+ export const EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES = 256 * 1024;
27
+
28
+ export type EditableArtifactMaterializationFormat =
29
+ | "xlsx"
30
+ | "pptx"
31
+ | "docx"
32
+ | "pdf"
33
+ | "png"
34
+ | "webp";
35
+
36
+ export type EditableArtifactMaterializationMimeType =
37
+ | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
38
+ | "application/vnd.openxmlformats-officedocument.presentationml.presentation"
39
+ | "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
40
+ | "application/pdf"
41
+ | "image/png"
42
+ | "image/webp";
43
+
44
+ export type EditableArtifactPinnedVersion = Readonly<{
45
+ scope: EditableArtifactScope;
46
+ artifactId: EditableArtifactId;
47
+ id: string;
48
+ modality: EditableArtifactModality;
49
+ snapshotId: string;
50
+ headSequence: number;
51
+ causalFrontier: EditableArtifactCausalFrontier | null;
52
+ nativeRevision: number | null;
53
+ stateHash: EditableArtifactStateHash;
54
+ name: string;
55
+ pinned: true;
56
+ createdBySubjectId: string;
57
+ createdAt: string;
58
+ }>;
59
+
60
+ export type EditableArtifactMaterializationResult = Readonly<{
61
+ id: string;
62
+ byteSize: number;
63
+ contentHash: EditableArtifactContentHash;
64
+ mimeType: EditableArtifactMaterializationMimeType;
65
+ verifiedAt: string;
66
+ createdAt: string;
67
+ }>;
68
+
69
+ export type EditableArtifactMaterializationJob = Readonly<{
70
+ scope: EditableArtifactScope;
71
+ artifactId: EditableArtifactId;
72
+ id: string;
73
+ versionId: string;
74
+ inputSnapshotId: string;
75
+ targetHeadSequence: number;
76
+ stateHash: EditableArtifactStateHash;
77
+ format: EditableArtifactMaterializationFormat;
78
+ state: "pending" | "running" | "succeeded" | "failed";
79
+ attemptCount: number;
80
+ errorCode: string | null;
81
+ createdAt: string;
82
+ startedAt: string | null;
83
+ completedAt: string | null;
84
+ result: EditableArtifactMaterializationResult | null;
85
+ }>;
86
+
87
+ export type EditableArtifactMaterializationProfile = Readonly<{
88
+ modality: EditableArtifactModality;
89
+ format: EditableArtifactMaterializationFormat;
90
+ codecId: string;
91
+ codecVersion: string;
92
+ kernelVersion: string;
93
+ fontRegistryHash: string;
94
+ policyHash: string;
95
+ /** Canonical JSON object understood by the pinned native materializer. */
96
+ normalizedOptions: string;
97
+ }>;
98
+
99
+ /** Server-owned profile lookup. Clients never supply executable identity facts. */
100
+ export interface EditableArtifactMaterializationProfilePort {
101
+ resolve(
102
+ input: Readonly<{
103
+ modality: EditableArtifactModality;
104
+ format: EditableArtifactMaterializationFormat;
105
+ options: Readonly<Record<string, unknown>>;
106
+ }>,
107
+ ): Promise<EditableArtifactMaterializationProfile | null>;
108
+ }
109
+
110
+ export interface EditableArtifactDurableExportIdFactoryPort {
111
+ next(kind: "version" | "materialization_job" | "receipt"): string;
112
+ }
113
+
114
+ export interface EditableArtifactExactSnapshotPort {
115
+ ensure(
116
+ input: Readonly<{
117
+ scope: EditableArtifactScope;
118
+ artifactId: EditableArtifactId;
119
+ actor: EditableArtifactActor;
120
+ signal?: AbortSignal;
121
+ }>,
122
+ ): Promise<EditableArtifactSnapshotMetadata>;
123
+ }
124
+
125
+ type DurableExportContext = Readonly<{
126
+ scope: EditableArtifactScope;
127
+ artifactId: EditableArtifactId;
128
+ actor: EditableArtifactActor;
129
+ }>;
130
+
131
+ export type PinEditableArtifactVersionStoreRequest = DurableExportContext &
132
+ Readonly<{
133
+ expectedAuthorizationRevision: number;
134
+ authorityKey: string;
135
+ receiptId: string;
136
+ versionId: string;
137
+ idempotencyKey: string;
138
+ requestHash: string;
139
+ name: string;
140
+ snapshot: EditableArtifactSnapshotMetadata;
141
+ }>;
142
+
143
+ export type PinEditableArtifactVersionStoreResult =
144
+ | Readonly<{ kind: "result"; version: EditableArtifactPinnedVersion; replayed: boolean }>
145
+ | Readonly<{ kind: "authorization_stale" }>;
146
+
147
+ export type EnqueueEditableArtifactMaterializationStoreRequest = DurableExportContext &
148
+ Readonly<{
149
+ expectedAuthorizationRevision: number;
150
+ authorityKey: string;
151
+ receiptId: string;
152
+ jobId: string;
153
+ idempotencyKey: string;
154
+ requestHash: string;
155
+ versionId: string;
156
+ profile: EditableArtifactMaterializationProfile;
157
+ }>;
158
+
159
+ export type EnqueueEditableArtifactMaterializationStoreResult =
160
+ | Readonly<{
161
+ kind: "result";
162
+ job: EditableArtifactMaterializationJob;
163
+ replayed: boolean;
164
+ }>
165
+ | Readonly<{ kind: "authorization_stale" }>;
166
+
167
+ export type ReadEditableArtifactMaterializationStoreResult =
168
+ | Readonly<{ kind: "result"; job: EditableArtifactMaterializationJob | null }>
169
+ | Readonly<{ kind: "authorization_stale" }>;
170
+
171
+ export type ReadEditableArtifactVersionStoreResult =
172
+ | Readonly<{ kind: "result"; version: EditableArtifactPinnedVersion | null }>
173
+ | Readonly<{ kind: "authorization_stale" }>;
174
+
175
+ export type ReadEditableArtifactMaterializationDownloadStoreResult =
176
+ | Readonly<{
177
+ kind: "result";
178
+ job: EditableArtifactMaterializationJob | null;
179
+ objectReference: string | null;
180
+ }>
181
+ | Readonly<{ kind: "authorization_stale" }>;
182
+
183
+ export interface EditableArtifactDurableExportStorePort {
184
+ pinVersion(
185
+ input: PinEditableArtifactVersionStoreRequest,
186
+ ): Promise<PinEditableArtifactVersionStoreResult>;
187
+ enqueueMaterialization(
188
+ input: EnqueueEditableArtifactMaterializationStoreRequest,
189
+ ): Promise<EnqueueEditableArtifactMaterializationStoreResult>;
190
+ readVersion(
191
+ input: DurableExportContext &
192
+ Readonly<{ expectedAuthorizationRevision: number; versionId: string }>,
193
+ ): Promise<ReadEditableArtifactVersionStoreResult>;
194
+ readMaterialization(
195
+ input: DurableExportContext &
196
+ Readonly<{ expectedAuthorizationRevision: number; jobId: string }>,
197
+ ): Promise<ReadEditableArtifactMaterializationStoreResult>;
198
+ readMaterializationDownload(
199
+ input: DurableExportContext &
200
+ Readonly<{ expectedAuthorizationRevision: number; jobId: string }>,
201
+ ): Promise<ReadEditableArtifactMaterializationDownloadStoreResult>;
202
+ }
203
+
204
+ export type EditableArtifactMaterializationDownload = Readonly<{
205
+ artifactId: EditableArtifactId;
206
+ jobId: string;
207
+ format: EditableArtifactMaterializationFormat;
208
+ byteSize: number;
209
+ contentHash: EditableArtifactContentHash;
210
+ mimeType: EditableArtifactMaterializationMimeType;
211
+ chunks(input?: { signal?: AbortSignal }): AsyncIterable<Uint8Array>;
212
+ assertUnchanged(signal?: AbortSignal): Promise<void>;
213
+ close(): Promise<void>;
214
+ }>;
215
+
216
+ export type EditableArtifactDurableExportErrorCode =
217
+ | "invalid_request"
218
+ | "not_found"
219
+ | "forbidden"
220
+ | "conflict"
221
+ | "unsupported_format"
222
+ | "not_ready"
223
+ | "unavailable";
224
+
225
+ export class EditableArtifactDurableExportError extends Error {
226
+ constructor(readonly code: EditableArtifactDurableExportErrorCode) {
227
+ super(code);
228
+ this.name = "EditableArtifactDurableExportError";
229
+ }
230
+ }
231
+
232
+ export type EditableArtifactDurableExportServiceDependencies = Readonly<{
233
+ authorization: EditableArtifactAuthorizationPort;
234
+ exactSnapshots: EditableArtifactExactSnapshotPort;
235
+ store: EditableArtifactDurableExportStorePort;
236
+ ids: EditableArtifactDurableExportIdFactoryPort;
237
+ profiles: EditableArtifactMaterializationProfilePort;
238
+ materializationObjects: BoundedObjectReadPort;
239
+ }>;
240
+
241
+ const MAX_AUTHORIZATION_RETRIES = 4;
242
+ const PORTABLE_ID = /^[0-9a-f]{32}$/u;
243
+ const IDEMPOTENCY_KEY = /^[A-Za-z0-9._:-]{1,200}$/u;
244
+ const SHA256 = /^sha256:[0-9a-f]{64}$/u;
245
+ const PROFILE_TEXT = /^[\x21-\x7e]+$/u;
246
+
247
+ export class EditableArtifactDurableExportService {
248
+ constructor(private readonly dependencies: EditableArtifactDurableExportServiceDependencies) {}
249
+
250
+ async pinVersion(
251
+ input: DurableExportContext & {
252
+ idempotencyKey: string;
253
+ name: string;
254
+ signal?: AbortSignal;
255
+ },
256
+ ): Promise<{ version: EditableArtifactPinnedVersion; replayed: boolean }> {
257
+ const context = normalizeContext(input);
258
+ const idempotencyKey = portableIdempotencyKey(input.idempotencyKey);
259
+ const name = versionName(input.name);
260
+ const snapshot = await this.dependencies.exactSnapshots.ensure({
261
+ ...context,
262
+ ...(input.signal ? { signal: input.signal } : {}),
263
+ });
264
+ if (
265
+ snapshot.scope.accountId !== context.scope.accountId ||
266
+ snapshot.scope.workspaceId !== context.scope.workspaceId ||
267
+ snapshot.artifactId !== context.artifactId
268
+ ) {
269
+ throw new EditableArtifactDurableExportError("unavailable");
270
+ }
271
+ const requestHash = hashPublicRequest("pin-version", [idempotencyKey, name]);
272
+ const authorityKey = editableArtifactActorKey(context.actor);
273
+ const receiptId = stableId(this.dependencies.ids.next("receipt"), "receipt id");
274
+ const versionId = stableId(this.dependencies.ids.next("version"), "version id");
275
+
276
+ for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
277
+ const revision = await this.requireExportPermission(context);
278
+ const result = await this.dependencies.store.pinVersion({
279
+ ...context,
280
+ expectedAuthorizationRevision: revision,
281
+ authorityKey,
282
+ receiptId,
283
+ versionId,
284
+ idempotencyKey,
285
+ requestHash,
286
+ name,
287
+ snapshot,
288
+ });
289
+ if (result.kind === "result") {
290
+ return Object.freeze({ version: result.version, replayed: result.replayed });
291
+ }
292
+ }
293
+ throw new EditableArtifactDurableExportError("conflict");
294
+ }
295
+
296
+ async enqueueMaterialization(
297
+ input: DurableExportContext & {
298
+ idempotencyKey: string;
299
+ versionId: string;
300
+ format: EditableArtifactMaterializationFormat;
301
+ options?: Readonly<Record<string, unknown>>;
302
+ },
303
+ ): Promise<{ job: EditableArtifactMaterializationJob; replayed: boolean }> {
304
+ const context = normalizeContext(input);
305
+ const idempotencyKey = portableIdempotencyKey(input.idempotencyKey);
306
+ const versionId = stableId(input.versionId, "version id");
307
+ const format = materializationFormat(input.format);
308
+ const options = exactOptions(input.options ?? Object.freeze({}));
309
+ const version = await this.getVersion(context, versionId);
310
+ const profile = await this.dependencies.profiles.resolve({
311
+ modality: version.modality,
312
+ format,
313
+ options,
314
+ });
315
+ if (!profile) throw new EditableArtifactDurableExportError("unsupported_format");
316
+ validateProfile(profile, version.modality, format);
317
+ const requestHash = hashPublicRequest("materialize", [
318
+ idempotencyKey,
319
+ versionId,
320
+ format,
321
+ canonicalJson(options),
322
+ ]);
323
+ const authorityKey = editableArtifactActorKey(context.actor);
324
+ const receiptId = stableId(this.dependencies.ids.next("receipt"), "receipt id");
325
+ const jobId = stableId(this.dependencies.ids.next("materialization_job"), "job id");
326
+
327
+ for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
328
+ const revision = await this.requireExportPermission(context);
329
+ const result = await this.dependencies.store.enqueueMaterialization({
330
+ ...context,
331
+ expectedAuthorizationRevision: revision,
332
+ authorityKey,
333
+ receiptId,
334
+ jobId,
335
+ idempotencyKey,
336
+ requestHash,
337
+ versionId,
338
+ profile,
339
+ });
340
+ if (result.kind === "result") {
341
+ return Object.freeze({ job: result.job, replayed: result.replayed });
342
+ }
343
+ }
344
+ throw new EditableArtifactDurableExportError("conflict");
345
+ }
346
+
347
+ async getMaterialization(
348
+ input: DurableExportContext & Readonly<{ jobId: string }>,
349
+ ): Promise<EditableArtifactMaterializationJob> {
350
+ const context = normalizeContext(input);
351
+ const jobId = stableId(input.jobId, "job id");
352
+ for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
353
+ const revision = await this.requireExportPermission(context);
354
+ const result = await this.dependencies.store.readMaterialization({
355
+ ...context,
356
+ expectedAuthorizationRevision: revision,
357
+ jobId,
358
+ });
359
+ if (result.kind === "authorization_stale") continue;
360
+ if (!result.job) throw new EditableArtifactDurableExportError("not_found");
361
+ return result.job;
362
+ }
363
+ throw new EditableArtifactDurableExportError("conflict");
364
+ }
365
+
366
+ async openMaterializationDownload(
367
+ input: DurableExportContext & Readonly<{ jobId: string; signal?: AbortSignal }>,
368
+ ): Promise<EditableArtifactMaterializationDownload> {
369
+ const context = normalizeContext(input);
370
+ const jobId = stableId(input.jobId, "job id");
371
+ for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
372
+ const revision = await this.requireExportPermission(context);
373
+ const result = await this.dependencies.store.readMaterializationDownload({
374
+ ...context,
375
+ expectedAuthorizationRevision: revision,
376
+ jobId,
377
+ });
378
+ if (result.kind === "authorization_stale") continue;
379
+ if (!result.job) throw new EditableArtifactDurableExportError("not_found");
380
+ const materialization = result.job.result;
381
+ if (result.job.state !== "succeeded" || !materialization || !result.objectReference) {
382
+ throw new EditableArtifactDurableExportError("not_ready");
383
+ }
384
+ let object: BoundedObjectRead;
385
+ try {
386
+ object = await this.dependencies.materializationObjects.open({
387
+ opaqueReference: result.objectReference,
388
+ maxBytes: EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES,
389
+ expectedByteSize: materialization.byteSize,
390
+ ...(input.signal ? { signal: input.signal } : {}),
391
+ });
392
+ } catch {
393
+ throw new EditableArtifactDurableExportError("unavailable");
394
+ }
395
+ if (object.contentType !== undefined && object.contentType !== materialization.mimeType) {
396
+ await object.close();
397
+ throw new EditableArtifactDurableExportError("unavailable");
398
+ }
399
+ return Object.freeze({
400
+ artifactId: context.artifactId,
401
+ jobId,
402
+ format: result.job.format,
403
+ byteSize: materialization.byteSize,
404
+ contentHash: materialization.contentHash,
405
+ mimeType: materialization.mimeType,
406
+ chunks: (chunkInput: { signal?: AbortSignal } = {}) =>
407
+ object.chunks(chunkInput.signal ? { signal: chunkInput.signal } : {}),
408
+ assertUnchanged: async (signal?: AbortSignal) => await object.assertUnchanged(signal),
409
+ close: async () => await object.close(),
410
+ });
411
+ }
412
+ throw new EditableArtifactDurableExportError("conflict");
413
+ }
414
+
415
+ private async getVersion(
416
+ context: DurableExportContext,
417
+ versionId: string,
418
+ ): Promise<EditableArtifactPinnedVersion> {
419
+ for (let attempt = 0; attempt < MAX_AUTHORIZATION_RETRIES; attempt += 1) {
420
+ const revision = await this.requireExportPermission(context);
421
+ const result = await this.dependencies.store.readVersion({
422
+ ...context,
423
+ expectedAuthorizationRevision: revision,
424
+ versionId,
425
+ });
426
+ if (result.kind === "authorization_stale") continue;
427
+ if (!result.version) throw new EditableArtifactDurableExportError("not_found");
428
+ return result.version;
429
+ }
430
+ throw new EditableArtifactDurableExportError("conflict");
431
+ }
432
+
433
+ private async requireExportPermission(context: DurableExportContext): Promise<number> {
434
+ const decision = await this.dependencies.authorization.authorize({
435
+ ...context,
436
+ permission: "export",
437
+ });
438
+ if (!decision.allowed) throw new EditableArtifactDurableExportError("forbidden");
439
+ if (!Number.isSafeInteger(decision.revision) || decision.revision < 1) {
440
+ throw new EditableArtifactDurableExportError("unavailable");
441
+ }
442
+ return decision.revision;
443
+ }
444
+ }
445
+
446
+ export function editableArtifactMaterializationMimeType(
447
+ format: EditableArtifactMaterializationFormat,
448
+ ): EditableArtifactMaterializationMimeType {
449
+ switch (format) {
450
+ case "xlsx":
451
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
452
+ case "pptx":
453
+ return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
454
+ case "docx":
455
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
456
+ case "pdf":
457
+ return "application/pdf";
458
+ case "png":
459
+ return "image/png";
460
+ case "webp":
461
+ return "image/webp";
462
+ }
463
+ }
464
+
465
+ export function editableArtifactFormatMatchesModality(
466
+ modality: EditableArtifactModality,
467
+ format: EditableArtifactMaterializationFormat,
468
+ ): boolean {
469
+ return (
470
+ (modality === "spreadsheet" && ["xlsx", "pdf", "png", "webp"].includes(format)) ||
471
+ (modality === "presentation" && ["pptx", "pdf", "png", "webp"].includes(format)) ||
472
+ (modality === "document" && ["docx", "pdf", "png", "webp"].includes(format))
473
+ );
474
+ }
475
+
476
+ function normalizeContext(input: DurableExportContext): DurableExportContext {
477
+ const scope = editableArtifactScope(input.scope);
478
+ const artifactId = editableArtifactId(input.artifactId);
479
+ validateEditableArtifactActor(input.actor);
480
+ return Object.freeze({
481
+ scope,
482
+ artifactId,
483
+ actor: Object.freeze({ ...input.actor }) as EditableArtifactActor,
484
+ });
485
+ }
486
+
487
+ function stableId(value: string, _label: string): string {
488
+ if (!PORTABLE_ID.test(value) || /^0+$/u.test(value)) {
489
+ throw new EditableArtifactDurableExportError("invalid_request");
490
+ }
491
+ return value;
492
+ }
493
+
494
+ function portableIdempotencyKey(value: string): string {
495
+ if (!IDEMPOTENCY_KEY.test(value)) {
496
+ throw new EditableArtifactDurableExportError("invalid_request");
497
+ }
498
+ return value;
499
+ }
500
+
501
+ function versionName(value: string): string {
502
+ if (
503
+ typeof value !== "string" ||
504
+ value.trim() !== value ||
505
+ /[\u0000-\u001f\u007f]/u.test(value) ||
506
+ new TextEncoder().encode(value).byteLength < 1 ||
507
+ new TextEncoder().encode(value).byteLength > 256 ||
508
+ !wellFormedText(value)
509
+ ) {
510
+ throw new EditableArtifactDurableExportError("invalid_request");
511
+ }
512
+ return value;
513
+ }
514
+
515
+ function materializationFormat(value: string): EditableArtifactMaterializationFormat {
516
+ if (!["xlsx", "pptx", "docx", "pdf", "png", "webp"].includes(value)) {
517
+ throw new EditableArtifactDurableExportError("invalid_request");
518
+ }
519
+ return value as EditableArtifactMaterializationFormat;
520
+ }
521
+
522
+ function exactOptions(value: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> {
523
+ if (!plainRecord(value)) throw new EditableArtifactDurableExportError("invalid_request");
524
+ const canonical = canonicalJson(value);
525
+ if (new TextEncoder().encode(canonical).byteLength > EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES) {
526
+ throw new EditableArtifactDurableExportError("invalid_request");
527
+ }
528
+ return Object.freeze({ ...value });
529
+ }
530
+
531
+ function canonicalJson(value: Readonly<Record<string, unknown>>): string {
532
+ const keys = Object.keys(value).sort(compareCodeUnits);
533
+ const normalized: Record<string, unknown> = {};
534
+ for (const key of keys) {
535
+ if (!wellFormedText(key) || !jsonValue(value[key])) {
536
+ throw new EditableArtifactDurableExportError("invalid_request");
537
+ }
538
+ normalized[key] = value[key];
539
+ }
540
+ const encoded = JSON.stringify(normalized);
541
+ if (encoded === undefined) throw new EditableArtifactDurableExportError("invalid_request");
542
+ return encoded;
543
+ }
544
+
545
+ function validateProfile(
546
+ profile: EditableArtifactMaterializationProfile,
547
+ modality: EditableArtifactModality,
548
+ format: EditableArtifactMaterializationFormat,
549
+ ): void {
550
+ if (
551
+ !plainRecord(profile) ||
552
+ profile.modality !== modality ||
553
+ profile.format !== format ||
554
+ !editableArtifactFormatMatchesModality(modality, format) ||
555
+ !boundedProfileText(profile.codecId, 128) ||
556
+ !boundedProfileText(profile.codecVersion, 128) ||
557
+ !boundedProfileText(profile.kernelVersion, 512) ||
558
+ !SHA256.test(profile.fontRegistryHash) ||
559
+ !SHA256.test(profile.policyHash) ||
560
+ profile.normalizedOptions.trim() !== profile.normalizedOptions ||
561
+ new TextEncoder().encode(profile.normalizedOptions).byteLength >
562
+ EDITABLE_ARTIFACT_EXPORT_MAX_OPTIONS_BYTES
563
+ ) {
564
+ throw new EditableArtifactDurableExportError("unavailable");
565
+ }
566
+ let decoded: unknown;
567
+ try {
568
+ decoded = JSON.parse(profile.normalizedOptions) as unknown;
569
+ } catch {
570
+ throw new EditableArtifactDurableExportError("unavailable");
571
+ }
572
+ if (!plainRecord(decoded) || canonicalJson(decoded) !== profile.normalizedOptions) {
573
+ throw new EditableArtifactDurableExportError("unavailable");
574
+ }
575
+ }
576
+
577
+ function boundedProfileText(value: string, maximumBytes: number): boolean {
578
+ return (
579
+ typeof value === "string" &&
580
+ PROFILE_TEXT.test(value) &&
581
+ new TextEncoder().encode(value).byteLength <= maximumBytes
582
+ );
583
+ }
584
+
585
+ function hashPublicRequest(kind: string, fields: readonly string[]): string {
586
+ const digest = createHash("sha256");
587
+ digest.update("OpenGeni editable artifact durable export\0v1\0", "utf8");
588
+ digest.update(kind, "utf8");
589
+ digest.update("\0", "utf8");
590
+ for (const field of fields) {
591
+ const bytes = Buffer.from(field, "utf8");
592
+ const length = Buffer.allocUnsafe(4);
593
+ length.writeUInt32BE(bytes.byteLength);
594
+ digest.update(length);
595
+ digest.update(bytes);
596
+ }
597
+ return `sha256:${digest.digest("hex")}`;
598
+ }
599
+
600
+ function plainRecord(value: unknown): value is Record<string, unknown> {
601
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
602
+ const prototype = Object.getPrototypeOf(value);
603
+ return prototype === Object.prototype || prototype === null;
604
+ }
605
+
606
+ function jsonValue(value: unknown, depth = 0): boolean {
607
+ if (depth > 32) return false;
608
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
609
+ return typeof value !== "string" || wellFormedText(value);
610
+ }
611
+ if (typeof value === "number") return Number.isFinite(value);
612
+ if (Array.isArray(value))
613
+ return value.length <= 10_000 && value.every((item) => jsonValue(item, depth + 1));
614
+ if (!plainRecord(value) || Object.keys(value).length > 10_000) return false;
615
+ return Object.entries(value).every(
616
+ ([key, item]) => wellFormedText(key) && jsonValue(item, depth + 1),
617
+ );
618
+ }
619
+
620
+ function wellFormedText(value: string): boolean {
621
+ if (value.includes("\0")) return false;
622
+ for (let index = 0; index < value.length; index += 1) {
623
+ const unit = value.charCodeAt(index);
624
+ if (unit >= 0xd800 && unit <= 0xdbff) {
625
+ const next = value.charCodeAt(index + 1);
626
+ if (!Number.isFinite(next) || next < 0xdc00 || next > 0xdfff) return false;
627
+ index += 1;
628
+ } else if (unit >= 0xdc00 && unit <= 0xdfff) {
629
+ return false;
630
+ }
631
+ }
632
+ return true;
633
+ }
634
+
635
+ function compareCodeUnits(left: string, right: string): number {
636
+ return left < right ? -1 : left > right ? 1 : 0;
637
+ }
638
+
639
+ export function validateEditableArtifactPinnedVersion(
640
+ version: EditableArtifactPinnedVersion,
641
+ ): EditableArtifactPinnedVersion {
642
+ const scope = editableArtifactScope(version.scope);
643
+ const artifactId = editableArtifactId(version.artifactId);
644
+ const id = stableId(version.id, "version id");
645
+ const snapshotId = editableArtifactSnapshotId(version.snapshotId);
646
+ if (!Number.isSafeInteger(version.headSequence) || version.headSequence < 0) {
647
+ throw new EditableArtifactDurableExportError("unavailable");
648
+ }
649
+ const stateHash = editableArtifactStateHash(version.stateHash);
650
+ versionName(version.name);
651
+ assertIsoTimestamp(version.createdAt, "version creation time");
652
+ if (version.pinned !== true || !wellFormedText(version.createdBySubjectId)) {
653
+ throw new EditableArtifactDurableExportError("unavailable");
654
+ }
655
+ if (
656
+ (version.modality === "spreadsheet" &&
657
+ (!Array.isArray(version.causalFrontier) || version.nativeRevision !== null)) ||
658
+ (version.modality !== "spreadsheet" &&
659
+ (version.causalFrontier !== null ||
660
+ !Number.isSafeInteger(version.nativeRevision) ||
661
+ version.nativeRevision! < 0))
662
+ ) {
663
+ throw new EditableArtifactDurableExportError("unavailable");
664
+ }
665
+ return Object.freeze({
666
+ ...version,
667
+ scope,
668
+ artifactId,
669
+ id,
670
+ snapshotId,
671
+ stateHash,
672
+ causalFrontier:
673
+ version.causalFrontier === null
674
+ ? null
675
+ : Object.freeze(version.causalFrontier.map((entry) => Object.freeze({ ...entry }))),
676
+ });
677
+ }
678
+
679
+ export function validateEditableArtifactMaterializationJob(
680
+ job: EditableArtifactMaterializationJob,
681
+ ): EditableArtifactMaterializationJob {
682
+ const scope = editableArtifactScope(job.scope);
683
+ const artifactId = editableArtifactId(job.artifactId);
684
+ const id = stableId(job.id, "job id");
685
+ const versionId = stableId(job.versionId, "version id");
686
+ const inputSnapshotId = stableId(job.inputSnapshotId, "snapshot id");
687
+ const stateHash = editableArtifactStateHash(job.stateHash);
688
+ const format = materializationFormat(job.format);
689
+ if (
690
+ !Number.isSafeInteger(job.targetHeadSequence) ||
691
+ job.targetHeadSequence < 0 ||
692
+ !Number.isSafeInteger(job.attemptCount) ||
693
+ job.attemptCount < 0 ||
694
+ !["pending", "running", "succeeded", "failed"].includes(job.state)
695
+ ) {
696
+ throw new EditableArtifactDurableExportError("unavailable");
697
+ }
698
+ assertIsoTimestamp(job.createdAt, "job creation time");
699
+ if (job.startedAt !== null) assertIsoTimestamp(job.startedAt, "job start time");
700
+ if (job.completedAt !== null) assertIsoTimestamp(job.completedAt, "job completion time");
701
+ let result: EditableArtifactMaterializationResult | null = null;
702
+ if (job.result) {
703
+ result = Object.freeze({
704
+ ...job.result,
705
+ id: stableId(job.result.id, "result id"),
706
+ contentHash: editableArtifactContentHash(job.result.contentHash),
707
+ mimeType: editableArtifactMaterializationMimeType(format),
708
+ });
709
+ if (
710
+ job.result.mimeType !== result.mimeType ||
711
+ !Number.isSafeInteger(job.result.byteSize) ||
712
+ job.result.byteSize < 1 ||
713
+ job.result.byteSize > EDITABLE_ARTIFACT_EXPORT_MAX_DOWNLOAD_BYTES
714
+ ) {
715
+ throw new EditableArtifactDurableExportError("unavailable");
716
+ }
717
+ assertIsoTimestamp(job.result.verifiedAt, "result verification time");
718
+ assertIsoTimestamp(job.result.createdAt, "result creation time");
719
+ }
720
+ if ((job.state === "succeeded") !== (result !== null)) {
721
+ throw new EditableArtifactDurableExportError("unavailable");
722
+ }
723
+ return Object.freeze({
724
+ ...job,
725
+ scope,
726
+ artifactId,
727
+ id,
728
+ versionId,
729
+ inputSnapshotId,
730
+ stateHash,
731
+ format,
732
+ result,
733
+ });
734
+ }