@cloudpdf/engine 3.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2476 @@
1
+ import { AbortError, AbortError as AbortError$1, AbortablePromise, AbortablePromise as AbortablePromise$1, CONTINUOUS_RENDER_POLICY, DEFAULT_PDF_SAVE_MODE, EngineError, EngineError as EngineError$1, EngineErrorCode, EngineErrorCode as EngineErrorCode$1, canonicalSearchQuery, checkCapability, createPageImageHandle, decodePdfBits, encodeFieldRefKey, encodeStableIdKey, expandRawScope, normalizeAnnotationDraft, normalizeAnnotationPatch, normalizeAttachmentFileSource, passwordPromptFromState, searchContentEpoch, securityStateFromHead } from "@embedpdf/engine-core/runtime";
2
+ import { AccessResponseSchema, AnnotationAppearanceManifestSchema, AnnotationCreateResultSchema, AnnotationDeleteResultSchema, AnnotationListPageSnapshotSchema, AnnotationMoveResultSchema, AnnotationUpdateResultSchema, AttachmentCreateResultSchema, AttachmentDeleteResultSchema, DEFAULT_LAYER_NAME, DocumentActionsSnapshotSchema, DocumentHeadSchema, DocumentManifestSchema, DocumentMetadataSchema, EmbeddedFileItemSchema, EngineErrorPayloadSchema, FormEffectsResultSchema, FormFieldCreateResultSchema, FormFieldDTOSchema, FormFieldDeleteResultSchema, FormFieldUpdateResultSchema, FormImportResultSchema, FormRepairResultSchema, FormSetValueResultSchema, FormSnapshotSchema, FormWidgetLinkResultSchema, MetadataUpdateResultSchema, PageDeleteResultSchema, PageFlattenResultSchema, PageGeometrySnapshotSchema, PageListSnapshotSchema, PageMoveResultSchema, PageRotateResultSchema, PageTextSnapshotSchema, RedactionApplyResultSchema, SearchSliceSchema, WeakAnnotationSessionResponseSchema, annotationAppearancesImageOptionsToWire, applyCdnAccess, decodeSearchToken, decodeTokenText, encodeSearchToken, renderImageOptionsToWire, wirePaths } from "@embedpdf/engine-core/wire";
3
+ import { EventHub, SessionEventPublisher, generateUuid } from "@embedpdf/engine-services";
4
+ //#region src/document/planes.ts
5
+ /**
6
+ * Plane-scope resolution — THE one rule every cloud service uses to pick a
7
+ * path family: a read resolves at the DOC-LEVEL (shared base) path iff EVERY
8
+ * plane it depends on is inherited (`'base'`) by this layer. One owned plane
9
+ * → the layer-scoped path.
10
+ *
11
+ * Absent `scopes` (base manifests never carry it; pre-plane servers omit
12
+ * it) means layer paths — never wrong, only unshared.
13
+ */
14
+ function planesInherited(manifest, planes) {
15
+ const scopes = manifest.scopes;
16
+ if (!scopes) return false;
17
+ return planes.every((plane) => scopes[plane] === "base");
18
+ }
19
+ //#endregion
20
+ //#region src/document/CloudDocumentActionsService.ts
21
+ var CloudDocumentActionsService = class {
22
+ constructor(http, docId, layerName, isClosed, manifest) {
23
+ this.http = http;
24
+ this.docId = docId;
25
+ this.layerName = layerName;
26
+ this.isClosed = isClosed;
27
+ this.manifest = manifest;
28
+ }
29
+ read() {
30
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
31
+ return AbortablePromise$1.run((signal) => this.http.getJsonWithRefresh(async (currentSignal) => {
32
+ const manifest = await this.manifest.get(currentSignal);
33
+ return planesInherited(manifest, ["actions"]) ? wirePaths.docActions(this.docId, manifest.actionsVersion) : wirePaths.layerActions(this.docId, this.layerName, manifest.actionsVersion);
34
+ }, (raw) => DocumentActionsSnapshotSchema.parse(raw), async (currentSignal) => {
35
+ await this.manifest.refresh(currentSignal);
36
+ }, signal));
37
+ }
38
+ };
39
+ //#endregion
40
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/typeof.js
41
+ function _typeof(o) {
42
+ "@babel/helpers - typeof";
43
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
44
+ return typeof o;
45
+ } : function(o) {
46
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
47
+ }, _typeof(o);
48
+ }
49
+ //#endregion
50
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/toPrimitive.js
51
+ function toPrimitive(t, r) {
52
+ if ("object" != _typeof(t) || !t) return t;
53
+ var e = t[Symbol.toPrimitive];
54
+ if (void 0 !== e) {
55
+ var i = e.call(t, r || "default");
56
+ if ("object" != _typeof(i)) return i;
57
+ throw new TypeError("@@toPrimitive must return a primitive value.");
58
+ }
59
+ return ("string" === r ? String : Number)(t);
60
+ }
61
+ //#endregion
62
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/toPropertyKey.js
63
+ function toPropertyKey(t) {
64
+ var i = toPrimitive(t, "string");
65
+ return "symbol" == _typeof(i) ? i : i + "";
66
+ }
67
+ //#endregion
68
+ //#region \0@oxc-project+runtime@0.140.0/helpers/esm/defineProperty.js
69
+ function _defineProperty(e, r, t) {
70
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
71
+ value: t,
72
+ enumerable: !0,
73
+ configurable: !0,
74
+ writable: !0
75
+ }) : e[r] = t, e;
76
+ }
77
+ //#endregion
78
+ //#region src/document/CloudDocumentAnnotationsService.ts
79
+ var CloudDocumentAnnotationsService = class {
80
+ constructor(http, docId, layerName, isClosed, manifest) {
81
+ this.http = http;
82
+ this.docId = docId;
83
+ this.layerName = layerName;
84
+ this.isClosed = isClosed;
85
+ this.manifest = manifest;
86
+ }
87
+ listRawAll() {
88
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
89
+ return AbortablePromise$1.run(async (signal) => {
90
+ const manifest = await this.manifest.get(signal);
91
+ return { pages: await Promise.all(manifest.pages.map((page) => this.readCurrentPage(page.state.pageObjectNumber, signal))) };
92
+ });
93
+ }
94
+ listRaw(pageObjectNumber) {
95
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
96
+ return AbortablePromise$1.run((signal) => this.readCurrentPage(pageObjectNumber, signal));
97
+ }
98
+ beginWeakEdit(pageObjectNumbers) {
99
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
100
+ return AbortablePromise$1.run(async (signal) => {
101
+ const response = await this.http.postJson(wirePaths.layerWeakAnnotationSession(this.docId, this.layerName), { pageObjectNumbers }, (raw) => WeakAnnotationSessionResponseSchema.parse(raw), signal);
102
+ return new CloudWeakAnnotationEditSession(this.http, this.docId, this.layerName, () => this.isClosed(), response);
103
+ });
104
+ }
105
+ async readCurrentPage(pageObjectNumber, signal) {
106
+ return this.http.getJson(wirePaths.layerPageAnnotationsCurrent(this.docId, this.layerName, pageObjectNumber), (raw) => AnnotationListPageSnapshotSchema.parse(raw), signal);
107
+ }
108
+ };
109
+ var CloudWeakAnnotationEditSession = class {
110
+ constructor(http, docId, layerName, isClosed, response) {
111
+ this.http = http;
112
+ this.docId = docId;
113
+ this.layerName = layerName;
114
+ this.isClosed = isClosed;
115
+ _defineProperty(this, "response", void 0);
116
+ _defineProperty(this, "released", false);
117
+ this.response = response;
118
+ }
119
+ get id() {
120
+ return this.response.sessionId;
121
+ }
122
+ get expiresAt() {
123
+ return this.response.expiresAt;
124
+ }
125
+ get heartbeatIntervalMs() {
126
+ return this.response.heartbeatIntervalMs;
127
+ }
128
+ get pageObjectNumbers() {
129
+ return this.response.pageObjectNumbers;
130
+ }
131
+ covers(pageObjectNumber) {
132
+ return this.response.pageObjectNumbers.includes(pageObjectNumber);
133
+ }
134
+ updatePages(pageObjectNumbers) {
135
+ if (this.isClosed() || this.released) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `weak annotation session ${this.id} is closed`));
136
+ return AbortablePromise$1.run(async (signal) => {
137
+ this.response = await this.http.postJson(wirePaths.layerWeakAnnotationSessionPages(this.docId, this.layerName, this.id), { pageObjectNumbers }, (raw) => WeakAnnotationSessionResponseSchema.parse(raw), signal);
138
+ });
139
+ }
140
+ heartbeat() {
141
+ if (this.isClosed() || this.released) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `weak annotation session ${this.id} is closed`));
142
+ return AbortablePromise$1.run(async (signal) => {
143
+ this.response = await this.http.postJson(wirePaths.layerWeakAnnotationSessionHeartbeat(this.docId, this.layerName, this.id), {}, (raw) => WeakAnnotationSessionResponseSchema.parse(raw), signal);
144
+ });
145
+ }
146
+ release() {
147
+ if (this.released) return AbortablePromise$1.resolveValue(void 0);
148
+ this.released = true;
149
+ return AbortablePromise$1.run((signal) => this.http.deleteEmpty(wirePaths.layerWeakAnnotationSessionRelease(this.docId, this.layerName, this.id), signal));
150
+ }
151
+ };
152
+ //#endregion
153
+ //#region src/document/buildMutationForm.ts
154
+ /**
155
+ * Multipart envelope for mutations that carry binaries: part `body` holds
156
+ * the exact JSON the plain request would have been, plus one
157
+ * `resource:{key}` file part per binary payload. Mirrors the appearance
158
+ * response protocol (`manifest` part + named image parts) in reverse.
159
+ * Shared by the annotation mutations (stamp images, attachment files) and
160
+ * the document-level `attachments.create` — one envelope, one parser on
161
+ * the server side.
162
+ */
163
+ function buildMutationForm(body, resources) {
164
+ const form = new FormData();
165
+ form.append("body", JSON.stringify(body));
166
+ for (const [key, resource] of Object.entries(resources)) form.append(`resource:${key}`, new Blob([resource.bytes], { type: resource.mimeType ?? "application/octet-stream" }), resource.name ?? key);
167
+ return form;
168
+ }
169
+ function hasResources(resources) {
170
+ return Object.keys(resources).length > 0;
171
+ }
172
+ //#endregion
173
+ //#region src/document/parseAttachmentContent.ts
174
+ /** Header carrying the file's name, token-text encoded (names are
175
+ * arbitrary UTF-8; HTTP header values are not). */
176
+ const FILE_NAME_HEADER = "X-EmbedPDF-File-Name";
177
+ /**
178
+ * Project an attachment-file response into `AttachmentContent`. The body
179
+ * IS the decoded bytes; the metadata rides as headers — `Content-Type`
180
+ * for the declared mime type, `X-EmbedPDF-File-Name` for the file name.
181
+ * Shared by the document-level `attachments.download()` and the
182
+ * annotation-level `annotations.downloadFile()` reads — one wire shape
183
+ * for both homes a file can live in.
184
+ */
185
+ function parseAttachmentContent(file) {
186
+ const encodedName = file.headers.get(FILE_NAME_HEADER);
187
+ if (encodedName === null) throw new EngineError$1(EngineErrorCode$1.WireFormat, `attachment response missing ${FILE_NAME_HEADER} header`);
188
+ let name;
189
+ try {
190
+ name = decodeTokenText(encodedName);
191
+ } catch (err) {
192
+ throw new EngineError$1(EngineErrorCode$1.WireFormat, `malformed ${FILE_NAME_HEADER} header: ${err?.message ?? err}`, { cause: err });
193
+ }
194
+ const mimeType = file.headers.get("Content-Type");
195
+ return {
196
+ bytes: file.bytes,
197
+ name,
198
+ ...mimeType !== null ? { mimeType } : {}
199
+ };
200
+ }
201
+ //#endregion
202
+ //#region src/document/CloudDocumentAttachmentsService.ts
203
+ /** The `/attachments@…` listing leaf is a bare array of name-tree entries. */
204
+ const EmbeddedFileListSchema = EmbeddedFileItemSchema.array();
205
+ /**
206
+ * Cloud-side document-level attachments (the catalog's `/EmbeddedFiles`
207
+ * name tree). Reads use immutable leaves pinned by the manifest's
208
+ * `attachmentsVersion`: `/attachments@…` for the metadata listing and
209
+ * `/attachment-files/…@…` for one file's decoded bytes — a separate
210
+ * capability tier, so a CDN credential for the listing can never egress
211
+ * content. Mutations use unversioned collection/member URLs and are
212
+ * never cached.
213
+ */
214
+ var CloudDocumentAttachmentsService = class {
215
+ constructor(http, docId, layerName, isClosed, manifest, publisher) {
216
+ this.http = http;
217
+ this.docId = docId;
218
+ this.layerName = layerName;
219
+ this.isClosed = isClosed;
220
+ this.manifest = manifest;
221
+ this.publisher = publisher;
222
+ }
223
+ /**
224
+ * Snapshot of the name tree, in tree (key-sorted) order. Pulls
225
+ * `attachmentsVersion` from the cached manifest and, on a 404 (stale
226
+ * pointer), transparently refreshes the manifest and retries once.
227
+ * The pin bumps only on attachment writes, so this leaf stays cached
228
+ * across page and annotation edits.
229
+ */
230
+ list() {
231
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
232
+ return AbortablePromise$1.run(async (signal) => {
233
+ const buildPath = async (s) => {
234
+ const manifest = await this.manifest.get(s);
235
+ return planesInherited(manifest, ["attachments"]) ? wirePaths.docAttachments(this.docId, manifest.attachmentsVersion) : wirePaths.layerAttachments(this.docId, this.layerName, manifest.attachmentsVersion);
236
+ };
237
+ return this.http.getJsonWithRefresh(buildPath, (raw) => EmbeddedFileListSchema.parse(raw), async (s) => {
238
+ await this.manifest.refresh(s);
239
+ }, signal);
240
+ });
241
+ }
242
+ /**
243
+ * Decode and return one embedded file's bytes. The body is the decoded
244
+ * stream; the name and mime type ride as response headers (see
245
+ * `parseAttachmentContent`). Same versioned-leaf + refresh-retry rails
246
+ * as {@link list}.
247
+ */
248
+ download(ref) {
249
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
250
+ return AbortablePromise$1.run(async (signal) => {
251
+ const buildPath = async (s) => {
252
+ const manifest = await this.manifest.get(s);
253
+ return planesInherited(manifest, ["attachments"]) ? wirePaths.docAttachmentFile(this.docId, ref.key, manifest.attachmentsVersion) : wirePaths.layerAttachmentFile(this.docId, this.layerName, ref.key, manifest.attachmentsVersion);
254
+ };
255
+ return parseAttachmentContent(await this.http.getFileWithRefresh(buildPath, async (s) => {
256
+ await this.manifest.refresh(s);
257
+ }, signal));
258
+ });
259
+ }
260
+ /**
261
+ * Create an embedded file in the name tree. Same splitter the
262
+ * file-attachment annotation draft uses — metadata into the `body`
263
+ * JSON part, bytes into the `resource:r0` file part — POSTed as the
264
+ * standard multipart mutation envelope.
265
+ */
266
+ create(file) {
267
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
268
+ return AbortablePromise$1.run(async (signal) => {
269
+ const { wireFile, resource } = await normalizeAttachmentFileSource(file, "r0");
270
+ const result = await this.http.postMultipartJson(wirePaths.layerAttachmentsCollection(this.docId, this.layerName), buildMutationForm(wireFile, { r0: resource }), (raw) => AttachmentCreateResultSchema.parse(raw), signal);
271
+ if (result.cache) this.manifest.applyAttachments(result.cache);
272
+ this.publisher.publishLocal({
273
+ type: "attachment.created",
274
+ ...result
275
+ });
276
+ return result;
277
+ });
278
+ }
279
+ /** Delete an embedded file from the name tree by its durable key. */
280
+ delete(ref) {
281
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
282
+ return AbortablePromise$1.run(async (signal) => {
283
+ const result = await this.http.deleteJson(wirePaths.layerAttachmentItem(this.docId, this.layerName, ref.key), (raw) => AttachmentDeleteResultSchema.parse(raw), signal);
284
+ if (result.cache) this.manifest.applyAttachments(result.cache);
285
+ this.publisher.publishLocal({
286
+ type: "attachment.deleted",
287
+ ...result
288
+ });
289
+ return result;
290
+ });
291
+ }
292
+ };
293
+ //#endregion
294
+ //#region src/document/CloudDocumentFormsService.ts
295
+ /** Content types the import POST body may carry; the server sniffs the
296
+ * actual format from the bytes, so this is advisory only. */
297
+ const IMPORT_CONTENT_TYPE = {
298
+ fdf: "application/vnd.fdf",
299
+ xfdf: "application/vnd.adobe.xfdf"
300
+ };
301
+ /**
302
+ * Cloud-side document forms service. Mirrors the local wiring: each call
303
+ * produces an `AbortablePromise` that propagates `signal.abort()` down to
304
+ * `fetch` and validates the JSON response with the wire-stable Zod schema.
305
+ *
306
+ * Forms are document-scoped, so reads use the unversioned `/form` URLs
307
+ * (always `no-store` — there is no content-addressed variant). Mutation
308
+ * results carry the per-page `cacheDelta` for pages whose widget
309
+ * appearances changed; `absorbMutation` folds it into the cached manifest
310
+ * so annotation/render reads stay coherent.
311
+ */
312
+ var CloudDocumentFormsService = class {
313
+ constructor(http, docId, layerName, isClosed, manifest, publisher) {
314
+ this.http = http;
315
+ this.docId = docId;
316
+ this.layerName = layerName;
317
+ this.isClosed = isClosed;
318
+ this.manifest = manifest;
319
+ this.publisher = publisher;
320
+ }
321
+ list() {
322
+ const rejected = this.rejectIfClosed();
323
+ if (rejected) return rejected;
324
+ return AbortablePromise$1.run((signal) => this.http.getJson(wirePaths.layerForm(this.docId, this.layerName), (raw) => FormSnapshotSchema.parse(raw), signal));
325
+ }
326
+ get(ref) {
327
+ const rejected = this.rejectIfClosed();
328
+ if (rejected) return rejected;
329
+ return AbortablePromise$1.run((signal) => this.http.getJson(wirePaths.layerFormFieldByKey(this.docId, this.layerName, encodeFieldRefKey(ref)), (raw) => FormFieldDTOSchema.parse(raw), signal));
330
+ }
331
+ setValue(ref, value) {
332
+ const rejected = this.rejectIfClosed();
333
+ if (rejected) return rejected;
334
+ return AbortablePromise$1.run(async (signal) => {
335
+ const result = await this.http.postJson(wirePaths.layerFormFieldValue(this.docId, this.layerName, encodeFieldRefKey(ref)), { value }, (raw) => FormSetValueResultSchema.parse(raw), signal);
336
+ return this.absorbMutation(result, "form.valueChanged");
337
+ });
338
+ }
339
+ reset(ref) {
340
+ const rejected = this.rejectIfClosed();
341
+ if (rejected) return rejected;
342
+ return AbortablePromise$1.run(async (signal) => {
343
+ const result = await this.http.postJson(wirePaths.layerFormFieldReset(this.docId, this.layerName, encodeFieldRefKey(ref)), {}, (raw) => FormSetValueResultSchema.parse(raw), signal);
344
+ return this.absorbMutation(result, "form.valueChanged");
345
+ });
346
+ }
347
+ applyEffects(effects) {
348
+ const rejected = this.rejectIfClosed();
349
+ if (rejected) return rejected;
350
+ return AbortablePromise$1.run(async (signal) => {
351
+ const result = await this.http.postJson(wirePaths.layerFormEffects(this.docId, this.layerName), { effects }, (raw) => FormEffectsResultSchema.parse(raw), signal);
352
+ if (result.meta === null) return result;
353
+ this.manifest.apply(result.meta, ["annotations"]);
354
+ this.publisher.publishLocal({
355
+ type: "form.effectsApplied",
356
+ ...result
357
+ });
358
+ return result;
359
+ });
360
+ }
361
+ exportData(format = "xfdf") {
362
+ const rejected = this.rejectIfClosed();
363
+ if (rejected) return rejected;
364
+ return AbortablePromise$1.run(async (signal) => {
365
+ return {
366
+ format,
367
+ bytes: await this.http.getBytes(wirePaths.layerFormData(this.docId, this.layerName, format), signal)
368
+ };
369
+ });
370
+ }
371
+ importData(data, format) {
372
+ const rejected = this.rejectIfClosed();
373
+ if (rejected) return rejected;
374
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
375
+ return AbortablePromise$1.run(async (signal) => {
376
+ const result = await this.http.postBytesJson(wirePaths.layerFormData(this.docId, this.layerName, format), bytes, format ? IMPORT_CONTENT_TYPE[format] : "application/octet-stream", (raw) => FormImportResultSchema.parse(raw), signal);
377
+ return this.absorbMutation(result, "form.imported");
378
+ });
379
+ }
380
+ createField(draft) {
381
+ const rejected = this.rejectIfClosed();
382
+ if (rejected) return rejected;
383
+ return AbortablePromise$1.run(async (signal) => {
384
+ const result = await this.http.postJson(wirePaths.layerFormFields(this.docId, this.layerName), draft, (raw) => FormFieldCreateResultSchema.parse(raw), signal);
385
+ return this.absorbMutation(result, "form.fieldCreated");
386
+ });
387
+ }
388
+ updateField(ref, patch) {
389
+ const rejected = this.rejectIfClosed();
390
+ if (rejected) return rejected;
391
+ return AbortablePromise$1.run(async (signal) => {
392
+ const result = await this.http.patchJson(wirePaths.layerFormFieldByKey(this.docId, this.layerName, encodeFieldRefKey(ref)), patch, (raw) => FormFieldUpdateResultSchema.parse(raw), signal);
393
+ return this.absorbMutation(result, "form.fieldUpdated");
394
+ });
395
+ }
396
+ deleteField(ref) {
397
+ const rejected = this.rejectIfClosed();
398
+ if (rejected) return rejected;
399
+ return AbortablePromise$1.run(async (signal) => {
400
+ const result = await this.http.deleteJson(wirePaths.layerFormFieldByKey(this.docId, this.layerName, encodeFieldRefKey(ref)), (raw) => FormFieldDeleteResultSchema.parse(raw), signal);
401
+ return this.absorbMutation(result, "form.fieldDeleted");
402
+ });
403
+ }
404
+ attachWidget(ref, widget, options) {
405
+ const rejected = this.rejectIfClosed();
406
+ if (rejected) return rejected;
407
+ const onState = options?.onState;
408
+ return AbortablePromise$1.run(async (signal) => {
409
+ const result = await this.http.postJson(wirePaths.layerFormFieldWidgets(this.docId, this.layerName, encodeFieldRefKey(ref)), {
410
+ widget,
411
+ ...onState ? { onState } : {}
412
+ }, (raw) => FormWidgetLinkResultSchema.parse(raw), signal);
413
+ return this.absorbMutation(result, "form.widgetAttached");
414
+ });
415
+ }
416
+ detachWidget(ref, widget) {
417
+ const rejected = this.rejectIfClosed();
418
+ if (rejected) return rejected;
419
+ return AbortablePromise$1.run(async (signal) => {
420
+ const result = await this.http.postJson(wirePaths.layerFormFieldWidgetsDetach(this.docId, this.layerName, encodeFieldRefKey(ref)), { widget }, (raw) => FormWidgetLinkResultSchema.parse(raw), signal);
421
+ return this.absorbMutation(result, "form.widgetDetached");
422
+ });
423
+ }
424
+ repair(options) {
425
+ const rejected = this.rejectIfClosed();
426
+ if (rejected) return rejected;
427
+ return AbortablePromise$1.run(async (signal) => {
428
+ const result = await this.http.postJson(wirePaths.layerFormRepair(this.docId, this.layerName), { bakeAppearances: options?.bakeAppearances ?? false }, (raw) => FormRepairResultSchema.parse(raw), signal);
429
+ return this.absorbMutation(result, "form.repaired");
430
+ });
431
+ }
432
+ rejectIfClosed() {
433
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
434
+ return null;
435
+ }
436
+ /**
437
+ * Patch the cached manifest, then publish the mutation to the document's
438
+ * event stream (in that order — listeners reading the manifest in their
439
+ * callback must see post-mutation state). Same rails as annotations.
440
+ */
441
+ absorbMutation(result, type) {
442
+ this.manifest.apply(result.meta, ["annotations"]);
443
+ this.publisher.publishLocal({
444
+ type,
445
+ ...result
446
+ });
447
+ return result;
448
+ }
449
+ };
450
+ //#endregion
451
+ //#region src/document/CloudDocumentPagesService.ts
452
+ /**
453
+ * Cloud-side document pages service. Mirrors `LocalDocumentPagesService`
454
+ * over HTTP: GET /pages for `list`, POST /pages/move for the reorder.
455
+ *
456
+ * Page identity rule (locked with the user, do not change):
457
+ * - Pages are addressed exclusively by their indirect
458
+ * `pageObjectNumber`. The wire never sends a page index for a
459
+ * mutation. This keeps multi-call client logic from having to
460
+ * account for index drift between requests.
461
+ * - Successful `move()` returns the new `layout` (order + geometry) plus
462
+ * cloud coherence pins. The server does NOT bump per-page revisions on a
463
+ * page move (page reorder is intentionally outside the weak-ref staleness
464
+ * model), only `docVersion` + `layoutVersion`.
465
+ */
466
+ var CloudDocumentPagesService = class {
467
+ constructor(http, docId, layerName, isClosed, manifest, publisher) {
468
+ this.http = http;
469
+ this.docId = docId;
470
+ this.layerName = layerName;
471
+ this.isClosed = isClosed;
472
+ this.manifest = manifest;
473
+ this.publisher = publisher;
474
+ }
475
+ /**
476
+ * Page-geometry list. The geometry bytes live at the content-addressed
477
+ * `/layout@layoutVersion=N` leaf (not in the manifest); the manifest only
478
+ * publishes the `layoutVersion` pointer. So `list()` reads `layoutVersion`
479
+ * from the cached manifest, fetches the layout leaf, and on a 404 (stale
480
+ * pointer) transparently refreshes the manifest and retries once — the
481
+ * same ladder the per-page text/geometry reads use. `layoutVersion` bumps
482
+ * only on structural page ops, so this leaf stays cached across content
483
+ * and annotation edits.
484
+ */
485
+ list() {
486
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
487
+ return AbortablePromise$1.run(async (signal) => {
488
+ const buildPath = async (s) => {
489
+ const manifest = await this.manifest.get(s);
490
+ return planesInherited(manifest, ["layout"]) ? wirePaths.docLayout(this.docId, manifest.layoutVersion) : wirePaths.layerLayout(this.docId, this.layerName, manifest.layoutVersion);
491
+ };
492
+ return this.http.getJsonWithRefresh(buildPath, (raw) => PageListSnapshotSchema.parse(raw), async (s) => {
493
+ await this.manifest.refresh(s);
494
+ }, signal);
495
+ });
496
+ }
497
+ move(pageObjectNumbers, destIndex) {
498
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
499
+ return AbortablePromise$1.run(async (signal) => {
500
+ const result = await this.http.postJson(wirePaths.layerPagesMove(this.docId, this.layerName), {
501
+ pageObjectNumbers,
502
+ destIndex
503
+ }, (raw) => PageMoveResultSchema.parse(raw), signal);
504
+ if (result.cache) this.manifest.applyPageStructure(result.cache);
505
+ this.publisher.publishLocal({
506
+ type: "pages.moved",
507
+ pageObjectNumbers,
508
+ destIndex,
509
+ ...result
510
+ });
511
+ return result;
512
+ });
513
+ }
514
+ rotate(pageObjectNumbers, rotation) {
515
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
516
+ return AbortablePromise$1.run(async (signal) => {
517
+ const result = await this.http.postJson(wirePaths.layerPagesRotate(this.docId, this.layerName), {
518
+ pageObjectNumbers,
519
+ rotation
520
+ }, (raw) => PageRotateResultSchema.parse(raw), signal);
521
+ if (result.cache) this.manifest.applyPageStructure(result.cache);
522
+ this.publisher.publishLocal({
523
+ type: "pages.rotated",
524
+ pageObjectNumbers,
525
+ rotation,
526
+ ...result
527
+ });
528
+ return result;
529
+ });
530
+ }
531
+ delete(pageObjectNumbers) {
532
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
533
+ return AbortablePromise$1.run(async (signal) => {
534
+ const result = await this.http.postJson(wirePaths.layerPagesDelete(this.docId, this.layerName), { pageObjectNumbers }, (raw) => PageDeleteResultSchema.parse(raw), signal);
535
+ if (result.cache) this.manifest.applyPageDelete(result.cache, pageObjectNumbers);
536
+ this.publisher.publishLocal({
537
+ type: "pages.deleted",
538
+ pageObjectNumbers,
539
+ ...result
540
+ });
541
+ return result;
542
+ });
543
+ }
544
+ flatten(pageObjectNumbers, usage = "display") {
545
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
546
+ return AbortablePromise$1.run(async (signal) => {
547
+ const result = await this.http.postJson(wirePaths.layerPagesFlatten(this.docId, this.layerName), {
548
+ pageObjectNumbers,
549
+ usage
550
+ }, (raw) => PageFlattenResultSchema.parse(raw), signal);
551
+ if (result.meta === null) return result;
552
+ this.manifest.apply(result.meta, ["content", "annotations"]);
553
+ this.publisher.publishLocal({
554
+ type: "pages.flattened",
555
+ ...result
556
+ });
557
+ return result;
558
+ });
559
+ }
560
+ };
561
+ //#endregion
562
+ //#region src/document/CloudDocumentRedactionService.ts
563
+ /**
564
+ * Cloud-side redaction apply. Mirrors `LocalDocumentRedactionService` over
565
+ * HTTP (POST /redactions/apply); the server enforces the capability gate
566
+ * (`doc.pages.modify` + `doc.annotate.modify` + `doc.redact`) and persists
567
+ * the rewritten layer artifact. See `DocumentRedactionService` for the
568
+ * two-stage model and the layer trust boundary — an apply rewrites THIS
569
+ * LAYER's bytes; the immutable base keeps the original.
570
+ */
571
+ var CloudDocumentRedactionService = class {
572
+ constructor(http, docId, layerName, isClosed, manifest, publisher) {
573
+ this.http = http;
574
+ this.docId = docId;
575
+ this.layerName = layerName;
576
+ this.isClosed = isClosed;
577
+ this.manifest = manifest;
578
+ this.publisher = publisher;
579
+ }
580
+ apply(scope) {
581
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
582
+ return AbortablePromise$1.run(async (signal) => {
583
+ const result = await this.http.postJson(wirePaths.layerRedactionsApply(this.docId, this.layerName), { scope }, (raw) => RedactionApplyResultSchema.parse(raw), signal);
584
+ if (result.meta === null) return result;
585
+ this.manifest.apply(result.meta, ["content", "annotations"]);
586
+ this.publisher.publishLocal({
587
+ type: "redaction.applied",
588
+ ...result
589
+ });
590
+ return result;
591
+ });
592
+ }
593
+ };
594
+ //#endregion
595
+ //#region src/document/CloudDocumentSearchService.ts
596
+ /**
597
+ * Cloud search transport: versioned GETs, one per budgeted slice.
598
+ *
599
+ * Every slice URL is a canonical cache key — the search token pins the
600
+ * layer's content epoch (computed from the cached manifest), the
601
+ * CANONICAL query (default-fold literals collapse case/diacritic
602
+ * variants into one entry), and the resume position. Identical searches
603
+ * from any reader of the document hit the same CDN entry; matches are
604
+ * all that ever crosses the trust boundary, and the rects/full endpoint
605
+ * split keeps permission tiers in disjoint cache namespaces.
606
+ *
607
+ * Staleness rides the standard versioned-read signal: a first slice
608
+ * answered `NotFound` means OUR manifest is stale → refresh and retry
609
+ * once; a continuation answered `NotFound` means the document changed
610
+ * mid-search → `InvalidArg`, same contract as the local engine's cursor.
611
+ */
612
+ var CloudDocumentSearchService = class {
613
+ constructor(http, docId, layerName, isClosed, manifest) {
614
+ this.http = http;
615
+ this.docId = docId;
616
+ this.layerName = layerName;
617
+ this.isClosed = isClosed;
618
+ this.manifest = manifest;
619
+ }
620
+ query(request) {
621
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document not open: ${this.docId}`));
622
+ return AbortablePromise$1.run(async (signal) => {
623
+ const path = (request.mode ?? "full") === "rects" ? wirePaths.layerSearchRects : wirePaths.layerSearchFull;
624
+ const get = (token) => this.http.getJson(path(this.docId, this.layerName, token), (raw) => SearchSliceSchema.parse(raw), signal);
625
+ const query = canonicalSearchQuery(request.query);
626
+ if (request.cursor !== void 0) {
627
+ let token;
628
+ try {
629
+ token = decodeSearchToken(request.cursor);
630
+ } catch {
631
+ throw new EngineError$1(EngineErrorCode$1.InvalidArg, "malformed search cursor");
632
+ }
633
+ if (queryIdentity(token.query) !== queryIdentity(query)) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "search cursor belongs to a different query — restart the search");
634
+ if (request.startPage !== void 0 && request.startPage !== token.startPage) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "startPage conflicts with the cursor — omit startPage when resuming");
635
+ try {
636
+ return await get(request.cursor);
637
+ } catch (err) {
638
+ if (EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "document changed since this search cursor was issued — restart the search");
639
+ throw err;
640
+ }
641
+ }
642
+ const mint = (epoch) => encodeSearchToken({
643
+ epoch,
644
+ query,
645
+ ...request.startPage !== void 0 ? { startPage: request.startPage } : {},
646
+ skip: 0,
647
+ ...request.budget !== void 0 ? { budget: request.budget } : {}
648
+ });
649
+ try {
650
+ return await get(mint(searchContentEpoch(await this.manifest.get(signal))));
651
+ } catch (err) {
652
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
653
+ return await get(mint(searchContentEpoch(await this.manifest.refresh(signal))));
654
+ }
655
+ });
656
+ }
657
+ };
658
+ /** Query identity for cursor validation (option order pinned by hand). */
659
+ function queryIdentity(query) {
660
+ return JSON.stringify([
661
+ query.regex ? "r" : "l",
662
+ query.text,
663
+ query.matchCase ? 1 : 0,
664
+ query.matchDiacritics ? 1 : 0,
665
+ query.wholeWord ? 1 : 0
666
+ ]);
667
+ }
668
+ //#endregion
669
+ //#region src/transport/decodeUnverifiedClaims.ts
670
+ function decodeUnverifiedClaims(token) {
671
+ const parts = token.split(".");
672
+ if (parts.length !== 3) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "malformed jwt: expected 3 segments");
673
+ const payloadB64Url = parts[1];
674
+ let bytes;
675
+ try {
676
+ bytes = base64UrlDecode(payloadB64Url);
677
+ } catch (err) {
678
+ throw new EngineError$1(EngineErrorCode$1.InvalidArg, `malformed jwt payload: ${err.message ?? err}`);
679
+ }
680
+ let json;
681
+ try {
682
+ json = new TextDecoder().decode(bytes);
683
+ } catch (err) {
684
+ throw new EngineError$1(EngineErrorCode$1.InvalidArg, `jwt payload is not valid utf-8: ${err.message ?? err}`);
685
+ }
686
+ let parsed;
687
+ try {
688
+ parsed = JSON.parse(json);
689
+ } catch (err) {
690
+ throw new EngineError$1(EngineErrorCode$1.InvalidArg, `jwt payload is not valid json: ${err.message ?? err}`);
691
+ }
692
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "jwt payload must be a json object");
693
+ return parsed;
694
+ }
695
+ /**
696
+ * Decode a base64url string into a `Uint8Array`. Works in both Node
697
+ * and modern browsers; we avoid `Buffer` so the SDK stays
698
+ * browser-friendly. `atob` lives on the global in Node 16+ as well.
699
+ */
700
+ function base64UrlDecode(b64url) {
701
+ const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
702
+ const pad = b64.length % 4;
703
+ const padded = pad === 0 ? b64 : b64 + "=".repeat(4 - pad);
704
+ const bin = atob(padded);
705
+ const out = new Uint8Array(bin.length);
706
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
707
+ return out;
708
+ }
709
+ //#endregion
710
+ //#region src/document/CloudDocumentSecurityService.ts
711
+ var CloudDocumentSecurityService = class {
712
+ constructor(http, docId, layerName, initialHead, view, initialToken = null) {
713
+ this.http = http;
714
+ this.docId = docId;
715
+ this.layerName = layerName;
716
+ this.view = view;
717
+ _defineProperty(this, "state", void 0);
718
+ _defineProperty(this, "access", null);
719
+ _defineProperty(this, "tokenScope", void 0);
720
+ _defineProperty(this, "tokenIdentity", void 0);
721
+ this.state = securityStateFromHead(initialHead);
722
+ const claims = initialToken ? safeDecodeClaims(initialToken) : null;
723
+ this.tokenScope = Array.isArray(claims?.scope) ? claims.scope : [];
724
+ this.tokenIdentity = claims ? identityFromClaims(claims) : null;
725
+ }
726
+ get current() {
727
+ return this.state;
728
+ }
729
+ get currentAccess() {
730
+ return this.access;
731
+ }
732
+ /**
733
+ * Expanded capability set. Cloud-canonical post-/access; otherwise
734
+ * computed locally from the JWT scope + /head's pdf bits using the
735
+ * SAME `expandRawScope` helper engine-local calls — so the value
736
+ * matches across engines bit-for-bit on the same inputs.
737
+ */
738
+ get effectiveScope() {
739
+ if (this.access) return this.access.effectiveScope;
740
+ const bits = decodePdfBits(this.state.permissions.bits);
741
+ return Array.from(expandRawScope(this.tokenScope, bits)).sort();
742
+ }
743
+ /**
744
+ * Wildcard-aware authorization check — mirrors what the server route layer
745
+ * enforces with. `effectiveScope` alone can't gate UI: it enumerates concrete
746
+ * grants and drops the `*` admin wildcard. So we honor a server-canonical
747
+ * concrete grant when present, then fall back to the same `checkCapability`
748
+ * predicate (which short-circuits `*`) against the JWT scope + /head bits.
749
+ */
750
+ allows(cap) {
751
+ if (this.access?.effectiveScope.includes(cap)) return true;
752
+ const bits = decodePdfBits(this.state.permissions.bits);
753
+ return checkCapability(cap, this.tokenScope, bits);
754
+ }
755
+ /**
756
+ * Identity of the current caller, or null when anonymous.
757
+ *
758
+ * Cloud-canonical post-/access; otherwise the identity claims from
759
+ * the JWT itself. Both are the same shape — the post-/access
760
+ * version is just refreshed to reflect any server-side identity
761
+ * augmentation (rare; reserved for future tenant hooks).
762
+ */
763
+ get identity() {
764
+ return this.access?.identity ?? this.tokenIdentity;
765
+ }
766
+ /**
767
+ * High-level "should I prompt for a password?" — single source of
768
+ * truth, computed via `passwordPromptFromState` so the answer is
769
+ * identical to what the local engine would say for the same
770
+ * security state. See `passwordPromptFromState` for the rules.
771
+ */
772
+ get passwordPrompt() {
773
+ return passwordPromptFromState(this.state);
774
+ }
775
+ unlock(input) {
776
+ if (this.view.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
777
+ return AbortablePromise$1.run(async (signal) => {
778
+ return await this.postAccess(signal, {
779
+ password: input.password,
780
+ mode: input.mode ?? "any"
781
+ });
782
+ });
783
+ }
784
+ /**
785
+ * Cloud-internal: call /v1/access with no password to establish
786
+ * a CDN-credentialed session. Used by `CloudEngine.open` when
787
+ * /head's `access.reasons` includes 'cdn' but NOT 'password' —
788
+ * the server accepts an authenticated /access POST without a
789
+ * password and returns the signed-URL block.
790
+ *
791
+ * Not on the public `DocumentSecurityService` interface — the
792
+ * "unlock" verb implies user action, and this path is automatic.
793
+ * Local engine has no equivalent (there's no CDN concept).
794
+ */
795
+ establishAccess() {
796
+ if (this.view.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
797
+ return AbortablePromise$1.run(async (signal) => {
798
+ return await this.postAccess(signal, { mode: "any" });
799
+ });
800
+ }
801
+ /**
802
+ * Single POST /v1/access implementation shared by `unlock()` (with
803
+ * password) and `establishAccess()` (no password). Updates cached
804
+ * security state, caches the access block, and pushes the CDN
805
+ * binding into the HttpClient so subsequent fetches apply CDN
806
+ * tokens via `applyCdnAccess`.
807
+ *
808
+ * `none` CDN adapter → the access block has null overrides/policies
809
+ * and `applyCdnAccess` short-circuits to origin; safe to call always.
810
+ */
811
+ async postAccess(signal, body) {
812
+ const response = await this.http.postJson(wirePaths.access, {
813
+ docId: this.docId,
814
+ layerName: this.layerName,
815
+ ...body.password ? { password: body.password } : {},
816
+ mode: body.mode
817
+ }, (raw) => AccessResponseSchema.parse(raw), signal);
818
+ this.state = response.security;
819
+ this.access = {
820
+ cdn: response.cdn,
821
+ passwordGrant: response.passwordGrant,
822
+ pdfPermissions: response.pdfPermissions,
823
+ scope: response.scope,
824
+ effectiveScope: response.effectiveScope,
825
+ identity: response.identity,
826
+ originPasswordPolicy: response.originPasswordPolicy,
827
+ expiresAt: response.expiresAt,
828
+ ...response.renderPolicy ? { renderPolicy: response.renderPolicy } : {}
829
+ };
830
+ this.http.setCdnAccess({
831
+ cdn: response.cdn,
832
+ docId: this.docId,
833
+ layerName: this.layerName
834
+ });
835
+ return {
836
+ security: this.state,
837
+ access: this.access ?? void 0
838
+ };
839
+ }
840
+ };
841
+ /**
842
+ * Decode the JWT's payload without verifying. The SDK never verifies
843
+ * tokens client-side — the server is the verifier of record — but it
844
+ * needs the `scope` and identity claims for the local-fallback path
845
+ * of `effectiveScope` / `identity`. Returns null if the token is
846
+ * malformed; the security service treats that as "no claims known".
847
+ */
848
+ function safeDecodeClaims(token) {
849
+ try {
850
+ return decodeUnverifiedClaims(token);
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ function identityFromClaims(claims) {
856
+ const out = {};
857
+ if (typeof claims["user_id"] === "string") out.user_id = claims["user_id"];
858
+ if (typeof claims["group_id"] === "string") out.group_id = claims["group_id"];
859
+ if (typeof claims["display_name"] === "string") out.display_name = claims["display_name"];
860
+ if (Array.isArray(claims["groups"])) {
861
+ const groups = claims["groups"].filter((g) => typeof g === "string");
862
+ if (groups.length > 0) out.groups = groups;
863
+ }
864
+ return Object.keys(out).length > 0 ? out : null;
865
+ }
866
+ //#endregion
867
+ //#region src/document/CloudMetadataService.ts
868
+ var CloudMetadataService = class {
869
+ constructor(http, docId, layerName, isClosed, manifest, publisher) {
870
+ this.http = http;
871
+ this.docId = docId;
872
+ this.layerName = layerName;
873
+ this.isClosed = isClosed;
874
+ this.manifest = manifest;
875
+ this.publisher = publisher;
876
+ }
877
+ /**
878
+ * Document metadata read. The Info-dict JSON lives at the
879
+ * content-addressed `/metadata@metadataVersion=N` leaf (not in the
880
+ * manifest); the manifest only publishes the `metadataVersion` pointer.
881
+ * So `read()` pulls `metadataVersion` from the cached manifest, fetches
882
+ * the leaf, and on a 404 (stale pointer) transparently refreshes the
883
+ * manifest and retries once. `metadataVersion` bumps only on metadata
884
+ * writes, so this leaf stays cached across page and annotation edits.
885
+ */
886
+ read() {
887
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
888
+ return AbortablePromise$1.run(async (signal) => {
889
+ const buildPath = async (s) => {
890
+ const manifest = await this.manifest.get(s);
891
+ return planesInherited(manifest, ["metadata"]) ? wirePaths.docMetadata(this.docId, manifest.metadataVersion) : wirePaths.layerMetadata(this.docId, this.layerName, manifest.metadataVersion);
892
+ };
893
+ return this.http.getJsonWithRefresh(buildPath, (raw) => DocumentMetadataSchema.parse(raw), async (s) => {
894
+ await this.manifest.refresh(s);
895
+ }, signal);
896
+ });
897
+ }
898
+ update(patch) {
899
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
900
+ return AbortablePromise$1.run(async (signal) => {
901
+ const result = await this.http.postJson(wirePaths.layerMetadataUpdate(this.docId, this.layerName), patch, (raw) => MetadataUpdateResultSchema.parse(raw), signal);
902
+ if (result.cache) this.manifest.applyMetadata(result.cache);
903
+ this.publisher.publishLocal({
904
+ type: "metadata.updated",
905
+ ...result
906
+ });
907
+ return result;
908
+ });
909
+ }
910
+ };
911
+ //#endregion
912
+ //#region src/document/CloudPageAnnotationsService.ts
913
+ /**
914
+ * Cloud-side page annotation service. Mirrors the local wiring: each
915
+ * call produces an `AbortablePromise` that propagates `signal.abort()`
916
+ * down to `fetch` and validates the JSON response with the wire-stable
917
+ * Zod schema.
918
+ *
919
+ * Reads use immutable versioned layer URLs discovered from the
920
+ * manifest. Mutations use unversioned layer URLs and are never cached.
921
+ */
922
+ var CloudPageAnnotationsService = class {
923
+ constructor(http, docId, layerName, pageObjectNumber, isClosed, manifest, publisher) {
924
+ this.http = http;
925
+ this.docId = docId;
926
+ this.layerName = layerName;
927
+ this.pageObjectNumber = pageObjectNumber;
928
+ this.isClosed = isClosed;
929
+ this.manifest = manifest;
930
+ this.publisher = publisher;
931
+ }
932
+ list() {
933
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
934
+ return AbortablePromise$1.run(async (signal) => {
935
+ const buildPath = async (s) => {
936
+ const manifest = await this.manifest.get(s);
937
+ const page = manifest.pages.find((p) => p.state.pageObjectNumber === this.pageObjectNumber);
938
+ if (!page) throw new EngineError$1(EngineErrorCode$1.NotFound, `no page with object number ${this.pageObjectNumber} in document ${this.docId}`);
939
+ return planesInherited(manifest, ["annotations"]) ? wirePaths.docPageAnnotations(this.docId, this.pageObjectNumber, page.cache.annotationVersion) : wirePaths.layerPageAnnotations(this.docId, this.layerName, this.pageObjectNumber, page.cache.annotationVersion);
940
+ };
941
+ return this.http.getJsonWithRefresh(buildPath, (raw) => AnnotationListPageSnapshotSchema.parse(raw), async (s) => {
942
+ await this.manifest.refresh(s);
943
+ }, signal);
944
+ });
945
+ }
946
+ renderAppearances(_options) {
947
+ return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.NotImplemented, "annotations.renderAppearances() raw rasters are not available in the cloud engine; use renderAppearanceImages()"));
948
+ }
949
+ renderAppearanceImages(options = {}) {
950
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
951
+ return AbortablePromise$1.run(async (signal) => {
952
+ const format = options.format ?? "webp";
953
+ const buildPath = async (s) => {
954
+ const manifest = await this.manifest.get(s);
955
+ const page = manifest.pages.find((p) => p.state.pageObjectNumber === this.pageObjectNumber);
956
+ if (!page) throw new EngineError$1(EngineErrorCode$1.NotFound, `no page with object number ${this.pageObjectNumber} in document ${this.docId}`);
957
+ const wireToken = annotationAppearancesImageOptionsToWire({
958
+ ...options,
959
+ format
960
+ }, { annotationVersion: page.cache.annotationVersion });
961
+ return planesInherited(manifest, ["annotations"]) ? wirePaths.docPageAnnotationAppearances(this.docId, this.pageObjectNumber, wireToken) : wirePaths.layerPageAnnotationAppearances(this.docId, this.layerName, this.pageObjectNumber, wireToken);
962
+ };
963
+ return parseAppearanceForm(await this.http.getFormDataWithRefresh(buildPath, async (s) => {
964
+ await this.manifest.refresh(s);
965
+ }, signal));
966
+ });
967
+ }
968
+ /**
969
+ * Decode and return the file embedded in a FileAttachment annotation.
970
+ * A read over the immutable `attachment-files` leaf, pinned by the
971
+ * manifest's `attachmentsVersion` (annotation-level files re-key on the
972
+ * same pin as document-level ones), with the same stale-404 refresh
973
+ * retry as {@link list}. Weak `index` refs cannot be spliced into a GET
974
+ * URL (no body to carry the revision), so bytes reads require a stable
975
+ * id — the same `:annotKey` routing update()/delete() use.
976
+ */
977
+ downloadFile(ref) {
978
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
979
+ if (ref.pageObjectNumber !== this.pageObjectNumber) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `ref.pageObjectNumber ${ref.pageObjectNumber} != page ${this.pageObjectNumber}`));
980
+ if (ref.kind === "index") return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, "downloadFile requires a stable ref (objectNumber or nm); index refs cannot address the content-addressed file leaf"));
981
+ const annotKey = encodeStableIdKey(refToStableId(ref));
982
+ return AbortablePromise$1.run(async (signal) => {
983
+ const buildPath = async (s) => {
984
+ const manifest = await this.manifest.get(s);
985
+ return planesInherited(manifest, ["annotations", "attachments"]) ? wirePaths.docAnnotationFile(this.docId, this.pageObjectNumber, annotKey, manifest.attachmentsVersion) : wirePaths.layerAnnotationFile(this.docId, this.layerName, this.pageObjectNumber, annotKey, manifest.attachmentsVersion);
986
+ };
987
+ return parseAttachmentContent(await this.http.getFileWithRefresh(buildPath, async (s) => {
988
+ await this.manifest.refresh(s);
989
+ }, signal));
990
+ });
991
+ }
992
+ create(draft) {
993
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
994
+ return AbortablePromise$1.run(async (signal) => {
995
+ const { wire, resources } = await normalizeAnnotationDraft(draft);
996
+ const path = wirePaths.layerPageAnnotationsCreate(this.docId, this.layerName, this.pageObjectNumber);
997
+ const parse = (raw) => AnnotationCreateResultSchema.parse(raw);
998
+ const result = hasResources(resources) ? await this.http.postMultipartJson(path, buildMutationForm(wire, resources), parse, signal) : await this.http.postJson(path, wire, parse, signal);
999
+ return this.absorbMutation(result, "annotation.created");
1000
+ });
1001
+ }
1002
+ update(ref, patch) {
1003
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
1004
+ if (ref.pageObjectNumber !== this.pageObjectNumber) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `ref.pageObjectNumber ${ref.pageObjectNumber} != page ${this.pageObjectNumber}`));
1005
+ if (ref.kind === "index") {
1006
+ const path = wirePaths.layerAnnotationByKey(this.docId, this.layerName, ref.pageObjectNumber, "index");
1007
+ return AbortablePromise$1.run(async (signal) => {
1008
+ const { wire, resources } = await normalizeAnnotationPatch(patch);
1009
+ const result = await this.patchMutation(path, {
1010
+ ref,
1011
+ patch: wire
1012
+ }, resources, signal);
1013
+ return this.absorbMutation(result, "annotation.updated");
1014
+ });
1015
+ }
1016
+ const stableKey = encodeStableIdKey(refToStableId(ref));
1017
+ const path = wirePaths.layerAnnotationByKey(this.docId, this.layerName, ref.pageObjectNumber, stableKey);
1018
+ return AbortablePromise$1.run(async (signal) => {
1019
+ const { wire, resources } = await normalizeAnnotationPatch(patch);
1020
+ const result = await this.patchMutation(path, { patch: wire }, resources, signal);
1021
+ return this.absorbMutation(result, "annotation.updated");
1022
+ });
1023
+ }
1024
+ /** PATCH as plain JSON, or as multipart when the patch carried binaries. */
1025
+ patchMutation(path, body, resources, signal) {
1026
+ const parse = (raw) => AnnotationUpdateResultSchema.parse(raw);
1027
+ if (hasResources(resources)) return this.http.patchMultipartJson(path, buildMutationForm(body, resources), parse, signal);
1028
+ return this.http.patchJson(path, body, parse, signal);
1029
+ }
1030
+ delete(ref) {
1031
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
1032
+ if (ref.pageObjectNumber !== this.pageObjectNumber) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `ref.pageObjectNumber ${ref.pageObjectNumber} != page ${this.pageObjectNumber}`));
1033
+ if (ref.kind === "index") {
1034
+ const path = wirePaths.layerAnnotationByKey(this.docId, this.layerName, ref.pageObjectNumber, "index");
1035
+ return AbortablePromise$1.run(async (signal) => {
1036
+ const result = await this.http.patchJson(path, {
1037
+ ref,
1038
+ op: "delete"
1039
+ }, (raw) => AnnotationDeleteResultSchema.parse(raw), signal);
1040
+ return this.absorbMutation(result, "annotation.deleted");
1041
+ });
1042
+ }
1043
+ const stableKey = encodeStableIdKey(refToStableId(ref));
1044
+ const path = wirePaths.layerAnnotationByKey(this.docId, this.layerName, ref.pageObjectNumber, stableKey);
1045
+ return AbortablePromise$1.run(async (signal) => {
1046
+ const result = await this.http.deleteJson(path, (raw) => AnnotationDeleteResultSchema.parse(raw), signal);
1047
+ return this.absorbMutation(result, "annotation.deleted");
1048
+ });
1049
+ }
1050
+ move(refs, toIndex) {
1051
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
1052
+ for (const r of refs) if (r.pageObjectNumber !== this.pageObjectNumber) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `move ref points at page ${r.pageObjectNumber}; service is bound to page ${this.pageObjectNumber}`));
1053
+ const path = wirePaths.layerPageAnnotationsMove(this.docId, this.layerName, this.pageObjectNumber);
1054
+ return AbortablePromise$1.run(async (signal) => {
1055
+ const result = await this.http.postJson(path, {
1056
+ refs,
1057
+ toIndex
1058
+ }, (raw) => AnnotationMoveResultSchema.parse(raw), signal);
1059
+ return this.absorbMutation(result, "annotation.moved");
1060
+ });
1061
+ }
1062
+ /**
1063
+ * Patch the cached manifest, then publish the mutation to the document's
1064
+ * event stream (in that order — listeners reading the manifest in their
1065
+ * callback must see post-mutation state). Each call site pairs the event
1066
+ * `type` with the matching result by construction; the cast localizes that
1067
+ * pairing here instead of widening every site.
1068
+ */
1069
+ absorbMutation(result, type) {
1070
+ this.manifest.apply(result.meta, ["annotations"]);
1071
+ this.publisher.publishLocal({
1072
+ type,
1073
+ pageObjectNumber: this.pageObjectNumber,
1074
+ ...result
1075
+ });
1076
+ return result;
1077
+ }
1078
+ };
1079
+ /**
1080
+ * Parse the appearance `multipart/form-data` response into the same
1081
+ * `AnnotationAppearanceImagesResult` shape the local engine produces. The
1082
+ * `manifest` part is validated against the wire schema; each image part is
1083
+ * wrapped in a `PageImageHandle` backed by the in-memory blob we already
1084
+ * downloaded.
1085
+ */
1086
+ async function parseAppearanceForm(form) {
1087
+ const manifestRaw = form.get("manifest");
1088
+ if (typeof manifestRaw !== "string") throw new EngineError$1(EngineErrorCode$1.WireFormat, "appearance response missing JSON manifest part");
1089
+ let parsedJson;
1090
+ try {
1091
+ parsedJson = JSON.parse(manifestRaw);
1092
+ } catch (err) {
1093
+ throw new EngineError$1(EngineErrorCode$1.WireFormat, `appearance manifest is not valid JSON: ${err?.message ?? err}`);
1094
+ }
1095
+ const manifest = AnnotationAppearanceManifestSchema.parse(parsedJson);
1096
+ const appearances = await Promise.all(manifest.appearances.map(async (entry) => {
1097
+ const partValue = form.get(entry.part);
1098
+ if (partValue === null || typeof partValue === "string") throw new EngineError$1(EngineErrorCode$1.WireFormat, `appearance response missing image part "${entry.part}"`);
1099
+ const blob = partValue;
1100
+ const bytes = new Uint8Array(await blob.arrayBuffer());
1101
+ const image = createPageImageHandle({
1102
+ width: entry.width,
1103
+ height: entry.height,
1104
+ format: entry.format,
1105
+ contentType: entry.contentType,
1106
+ source: {
1107
+ kind: "bytes",
1108
+ bytes
1109
+ }
1110
+ }, { async blob() {
1111
+ return blob;
1112
+ } });
1113
+ return {
1114
+ ref: entry.ref,
1115
+ mode: entry.mode,
1116
+ rect: entry.rect,
1117
+ image
1118
+ };
1119
+ }));
1120
+ return {
1121
+ pageState: manifest.pageState,
1122
+ appearances
1123
+ };
1124
+ }
1125
+ /**
1126
+ * Local helper: project a non-index `AnnotationRef` into the matching
1127
+ * `AnnotationStableId` shape so we can route by stable key. The compiler
1128
+ * narrows on `ref.kind` here so we can't accidentally pass an index ref.
1129
+ */
1130
+ function refToStableId(ref) {
1131
+ if (ref.kind === "objectNumber") return {
1132
+ kind: "objectNumber",
1133
+ value: ref.annotObjectNumber
1134
+ };
1135
+ return {
1136
+ kind: "nm",
1137
+ value: ref.nm
1138
+ };
1139
+ }
1140
+ //#endregion
1141
+ //#region src/document/CloudPageGeometryService.ts
1142
+ var CloudPageGeometryService = class {
1143
+ constructor(http, docId, layerName, pageObjectNumber, isClosed, manifest) {
1144
+ this.http = http;
1145
+ this.docId = docId;
1146
+ this.layerName = layerName;
1147
+ this.pageObjectNumber = pageObjectNumber;
1148
+ this.isClosed = isClosed;
1149
+ this.manifest = manifest;
1150
+ }
1151
+ read() {
1152
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
1153
+ return AbortablePromise$1.run(async (signal) => {
1154
+ const buildPath = async (s) => {
1155
+ const manifest = await this.manifest.get(s);
1156
+ const page = manifest.pages.find((p) => p.state.pageObjectNumber === this.pageObjectNumber);
1157
+ if (!page) throw new EngineError$1(EngineErrorCode$1.NotFound, `no page with object number ${this.pageObjectNumber} in document ${this.docId}`);
1158
+ return planesInherited(manifest, ["content"]) ? wirePaths.docPageGeometry(this.docId, this.pageObjectNumber, page.cache.contentVersion) : wirePaths.layerPageGeometry(this.docId, this.layerName, this.pageObjectNumber, page.cache.contentVersion);
1159
+ };
1160
+ return this.http.getJsonWithRefresh(buildPath, (raw) => PageGeometrySnapshotSchema.parse(raw), async (s) => {
1161
+ await this.manifest.refresh(s);
1162
+ }, signal);
1163
+ });
1164
+ }
1165
+ };
1166
+ //#endregion
1167
+ //#region src/document/CloudPageRenderService.ts
1168
+ var CloudPageRenderService = class {
1169
+ constructor(http, docId, layerName, pageObjectNumber, isClosed, manifest) {
1170
+ this.http = http;
1171
+ this.docId = docId;
1172
+ this.layerName = layerName;
1173
+ this.pageObjectNumber = pageObjectNumber;
1174
+ this.isClosed = isClosed;
1175
+ this.manifest = manifest;
1176
+ }
1177
+ image(options = {}) {
1178
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
1179
+ return AbortablePromise$1.run(async (signal) => {
1180
+ const format = normalizeFormat(options.format);
1181
+ const includeAnnotations = options.includeAnnotations ?? true;
1182
+ const buildPath = async (s) => {
1183
+ const manifest = await this.manifest.get(s);
1184
+ const page = manifest.pages.find((p) => p.state.pageObjectNumber === this.pageObjectNumber);
1185
+ if (!page) throw new EngineError$1(EngineErrorCode$1.NotFound, `no page with object number ${this.pageObjectNumber} in document ${this.docId}`);
1186
+ const wireToken = renderImageOptionsToWire({
1187
+ ...options,
1188
+ format
1189
+ }, {
1190
+ contentVersion: page.cache.contentVersion,
1191
+ ...includeAnnotations ? { annotationVersion: page.cache.annotationVersion } : {}
1192
+ });
1193
+ if (includeAnnotations) return planesInherited(manifest, ["content", "annotations"]) ? wirePaths.docPageRenderAnnotated(this.docId, this.pageObjectNumber, wireToken) : wirePaths.layerPageRenderAnnotated(this.docId, this.layerName, this.pageObjectNumber, wireToken);
1194
+ return planesInherited(manifest, ["content"]) ? wirePaths.docPageRender(this.docId, this.pageObjectNumber, wireToken) : wirePaths.layerPageRender(this.docId, this.layerName, this.pageObjectNumber, wireToken);
1195
+ };
1196
+ const requestPath = await buildPath(signal);
1197
+ return createCloudPageImageHandle({
1198
+ format,
1199
+ contentType: `image/${format}`,
1200
+ source: {
1201
+ kind: "url",
1202
+ url: this.http.absoluteUrl(requestPath)
1203
+ }
1204
+ }, this.http, buildPath, async (s) => {
1205
+ await this.manifest.refresh(s);
1206
+ });
1207
+ });
1208
+ }
1209
+ raw(_options) {
1210
+ return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.NotImplemented, "render.raw() is not available in the cloud engine; use render.image()"));
1211
+ }
1212
+ };
1213
+ function createCloudPageImageHandle(result, http, buildPath, onStaleVersion) {
1214
+ return createPageImageHandle(result, { blob: (signal) => http.getBlobWithRefresh(buildPath, onStaleVersion, signal ?? new AbortController().signal) });
1215
+ }
1216
+ function normalizeFormat(format) {
1217
+ if (format === void 0) return "webp";
1218
+ if (format === "png" || format === "webp") return format;
1219
+ throw new EngineError$1(EngineErrorCode$1.InvalidArg, `cloud render.image() supports only "png" and "webp" (got "${format}")`);
1220
+ }
1221
+ //#endregion
1222
+ //#region src/document/CloudPageTextService.ts
1223
+ /**
1224
+ * Cloud-side per-page text service. `read()` fetches the
1225
+ * content-addressed URL `/v1/docs/:id/text/pages/:pon/data@contentVersion=N`, where
1226
+ * `:P` is the page's current `contentVersion` from the cached
1227
+ * manifest. On a 404 (stale version) the SDK transparently refreshes
1228
+ * `/head` + `/manifest@docVersion=N`, rebuilds the URL with the fresh `cN`,
1229
+ * and retries exactly once.
1230
+ */
1231
+ var CloudPageTextService = class {
1232
+ constructor(http, docId, layerName, pageObjectNumber, isClosed, manifest) {
1233
+ this.http = http;
1234
+ this.docId = docId;
1235
+ this.layerName = layerName;
1236
+ this.pageObjectNumber = pageObjectNumber;
1237
+ this.isClosed = isClosed;
1238
+ this.manifest = manifest;
1239
+ }
1240
+ read() {
1241
+ if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
1242
+ return AbortablePromise$1.run(async (signal) => {
1243
+ const buildPath = async (s) => {
1244
+ const manifest = await this.manifest.get(s);
1245
+ const page = manifest.pages.find((p) => p.state.pageObjectNumber === this.pageObjectNumber);
1246
+ if (!page) throw new EngineError$1(EngineErrorCode$1.NotFound, `no page with object number ${this.pageObjectNumber} in document ${this.docId}`);
1247
+ return planesInherited(manifest, ["content"]) ? wirePaths.docPageText(this.docId, this.pageObjectNumber, page.cache.contentVersion) : wirePaths.layerPageText(this.docId, this.layerName, this.pageObjectNumber, page.cache.contentVersion);
1248
+ };
1249
+ return this.http.getJsonWithRefresh(buildPath, (raw) => PageTextSnapshotSchema.parse(raw), async (s) => {
1250
+ await this.manifest.refresh(s);
1251
+ }, signal);
1252
+ });
1253
+ }
1254
+ };
1255
+ //#endregion
1256
+ //#region src/document/CloudPageHandle.ts
1257
+ var CloudPageHandle = class {
1258
+ constructor(pageObjectNumber, pageIndex, http, docId, layerName, isClosed, manifest, publisher) {
1259
+ this.pageObjectNumber = pageObjectNumber;
1260
+ this.pageIndex = pageIndex;
1261
+ _defineProperty(this, "annotations", void 0);
1262
+ _defineProperty(this, "text", void 0);
1263
+ _defineProperty(this, "geometry", void 0);
1264
+ _defineProperty(this, "render", void 0);
1265
+ this.annotations = new CloudPageAnnotationsService(http, docId, layerName, pageObjectNumber, isClosed, manifest, publisher);
1266
+ this.text = new CloudPageTextService(http, docId, layerName, pageObjectNumber, isClosed, manifest);
1267
+ this.geometry = new CloudPageGeometryService(http, docId, layerName, pageObjectNumber, isClosed, manifest);
1268
+ this.render = new CloudPageRenderService(http, docId, layerName, pageObjectNumber, isClosed, manifest);
1269
+ }
1270
+ };
1271
+ //#endregion
1272
+ //#region src/realtime/auditRowToEvent.ts
1273
+ /**
1274
+ * Translate a remote audit row into a `DocumentEvent` — pure, so the
1275
+ * exactly-once and verbatim-payload invariants are unit-testable without a
1276
+ * server. Returns `null` for kinds this engine version doesn't know
1277
+ * (a NEWER server's events degrade to "ignored", never to a crash).
1278
+ *
1279
+ * Context-field fidelity differs by op, by design of the audit row:
1280
+ * - rotate/delete: `affectedPages` is exactly the op's page set; rotation
1281
+ * is recovered from the layout (it's absolute — every affected page
1282
+ * carries the value).
1283
+ * - move: the originator knows which block it moved; the audit row only
1284
+ * records the resulting order, so `pageObjectNumbers` is the full new
1285
+ * order and `destIndex` is absent (remote consumers use `layout`).
1286
+ */
1287
+ function auditRowToEvent(row, mySessionId) {
1288
+ if (row.originSessionId === mySessionId) return null;
1289
+ const origin = {
1290
+ kind: "remote",
1291
+ sessionId: row.originSessionId ?? `unknown:${row.sub}`,
1292
+ sub: row.sub,
1293
+ ts: row.ts,
1294
+ serverId: row.id
1295
+ };
1296
+ switch (row.kind) {
1297
+ case "annot.create": return {
1298
+ type: "annotation.created",
1299
+ pageObjectNumber: row.pageObjectNumber ?? row.affectedPages[0] ?? 0,
1300
+ origin,
1301
+ ...row.payload
1302
+ };
1303
+ case "annot.update": return {
1304
+ type: "annotation.updated",
1305
+ pageObjectNumber: row.pageObjectNumber ?? row.affectedPages[0] ?? 0,
1306
+ origin,
1307
+ ...row.payload
1308
+ };
1309
+ case "annot.delete": return {
1310
+ type: "annotation.deleted",
1311
+ pageObjectNumber: row.pageObjectNumber ?? row.affectedPages[0] ?? 0,
1312
+ origin,
1313
+ ...row.payload
1314
+ };
1315
+ case "annot.move": return {
1316
+ type: "annotation.moved",
1317
+ pageObjectNumber: row.pageObjectNumber ?? row.affectedPages[0] ?? 0,
1318
+ origin,
1319
+ ...row.payload
1320
+ };
1321
+ case "pages.move": return {
1322
+ type: "pages.moved",
1323
+ pageObjectNumbers: row.affectedPages,
1324
+ origin,
1325
+ ...row.payload
1326
+ };
1327
+ case "pages.rotate": {
1328
+ const payload = row.payload;
1329
+ const rotation = payload.layout.pages.find((page) => page.pageObjectNumber === row.affectedPages[0])?.rotation ?? 0;
1330
+ return {
1331
+ type: "pages.rotated",
1332
+ pageObjectNumbers: row.affectedPages,
1333
+ rotation,
1334
+ origin,
1335
+ ...payload
1336
+ };
1337
+ }
1338
+ case "pages.delete": return {
1339
+ type: "pages.deleted",
1340
+ pageObjectNumbers: row.affectedPages,
1341
+ origin,
1342
+ ...row.payload
1343
+ };
1344
+ case "pages.flatten": return {
1345
+ type: "pages.flattened",
1346
+ origin,
1347
+ ...row.payload
1348
+ };
1349
+ case "metadata.update": return {
1350
+ type: "metadata.updated",
1351
+ origin,
1352
+ ...row.payload
1353
+ };
1354
+ case "attachment.create": return {
1355
+ type: "attachment.created",
1356
+ origin,
1357
+ ...row.payload
1358
+ };
1359
+ case "attachment.delete": return {
1360
+ type: "attachment.deleted",
1361
+ origin,
1362
+ ...row.payload
1363
+ };
1364
+ case "form.setValue":
1365
+ case "form.reset": return {
1366
+ type: "form.valueChanged",
1367
+ origin,
1368
+ ...row.payload
1369
+ };
1370
+ case "form.import": return {
1371
+ type: "form.imported",
1372
+ origin,
1373
+ ...row.payload
1374
+ };
1375
+ case "form.repair": return {
1376
+ type: "form.repaired",
1377
+ origin,
1378
+ ...row.payload
1379
+ };
1380
+ case "form.createField": return {
1381
+ type: "form.fieldCreated",
1382
+ origin,
1383
+ ...row.payload
1384
+ };
1385
+ case "form.updateField": return {
1386
+ type: "form.fieldUpdated",
1387
+ origin,
1388
+ ...row.payload
1389
+ };
1390
+ case "form.deleteField": return {
1391
+ type: "form.fieldDeleted",
1392
+ origin,
1393
+ ...row.payload
1394
+ };
1395
+ case "form.attachWidget": return {
1396
+ type: "form.widgetAttached",
1397
+ origin,
1398
+ ...row.payload
1399
+ };
1400
+ case "form.detachWidget": return {
1401
+ type: "form.widgetDetached",
1402
+ origin,
1403
+ ...row.payload
1404
+ };
1405
+ case "form.applyEffects": return {
1406
+ type: "form.effectsApplied",
1407
+ origin,
1408
+ ...row.payload
1409
+ };
1410
+ default: return null;
1411
+ }
1412
+ }
1413
+ //#endregion
1414
+ //#region src/realtime/SseClient.ts
1415
+ /**
1416
+ * Fetch-based SSE consumer. Browser-native `EventSource` cannot send an
1417
+ * `Authorization` header, so the stream rides the same `HttpClient` as
1418
+ * every other request (same token provider, same session header).
1419
+ *
1420
+ * Resilience model: the cursor is the only state that matters. Every
1421
+ * (re)connect sends `Last-Event-ID: cursor`; the server replays the gap
1422
+ * from the audit log. So a dropped stream costs latency, never events —
1423
+ * the client needs no buffering and no acknowledgements.
1424
+ *
1425
+ * - network drop / server close / `auth-expiring` → reconnect with
1426
+ * jittered exponential backoff (500ms → 30s; reset after a stream
1427
+ * that lived ≥30s). `auth-expiring` reconnects pick up a fresh token
1428
+ * from the HttpClient's provider automatically.
1429
+ * - 401/403 → permanent: `onAuthLost`, no retry loop against a dead
1430
+ * credential.
1431
+ */
1432
+ var SseClient = class {
1433
+ constructor(opts) {
1434
+ this.opts = opts;
1435
+ _defineProperty(this, "cursor", void 0);
1436
+ _defineProperty(this, "closed", false);
1437
+ _defineProperty(this, "abort", null);
1438
+ _defineProperty(this, "retryTimer", null);
1439
+ _defineProperty(this, "retryDelayMs", 500);
1440
+ this.cursor = opts.initialCursor;
1441
+ }
1442
+ open() {
1443
+ if (this.closed) return;
1444
+ this.run();
1445
+ }
1446
+ close() {
1447
+ this.closed = true;
1448
+ if (this.retryTimer) clearTimeout(this.retryTimer);
1449
+ this.abort?.abort();
1450
+ }
1451
+ get lastEventId() {
1452
+ return this.cursor;
1453
+ }
1454
+ async run() {
1455
+ const startedAt = Date.now();
1456
+ this.abort = new AbortController();
1457
+ try {
1458
+ const res = await this.opts.http.stream(this.opts.path, this.cursor !== null ? { "Last-Event-ID": String(this.cursor) } : {}, this.abort.signal);
1459
+ if (res.status === 401 || res.status === 403) {
1460
+ this.closed = true;
1461
+ this.opts.onAuthLost();
1462
+ return;
1463
+ }
1464
+ if (!res.ok || !res.body) throw new EngineError$1(EngineErrorCode$1.Network, `events stream HTTP ${res.status}`);
1465
+ await this.consume(res.body);
1466
+ if (Date.now() - startedAt >= 3e4) this.retryDelayMs = 500;
1467
+ } catch {}
1468
+ if (this.closed) return;
1469
+ const jitter = this.retryDelayMs * (.5 + Math.random() * .5);
1470
+ this.retryTimer = setTimeout(() => void this.run(), jitter);
1471
+ this.retryDelayMs = Math.min(this.retryDelayMs * 2, 3e4);
1472
+ }
1473
+ async consume(body) {
1474
+ const reader = body.getReader();
1475
+ const decoder = new TextDecoder();
1476
+ let buffer = "";
1477
+ for (;;) {
1478
+ const { done, value } = await reader.read();
1479
+ if (done || this.closed) return;
1480
+ buffer += decoder.decode(value, { stream: true });
1481
+ for (;;) {
1482
+ const sep = buffer.indexOf("\n\n");
1483
+ if (sep < 0) break;
1484
+ const block = buffer.slice(0, sep);
1485
+ buffer = buffer.slice(sep + 2);
1486
+ this.dispatch(block);
1487
+ }
1488
+ }
1489
+ }
1490
+ dispatch(block) {
1491
+ if (block.startsWith(":")) return;
1492
+ let id = null;
1493
+ let event = "message";
1494
+ const data = [];
1495
+ for (const line of block.split("\n")) if (line.startsWith("id:")) id = Number(line.slice(3).trim());
1496
+ else if (line.startsWith("event:")) event = line.slice(6).trim();
1497
+ else if (line.startsWith("data:")) data.push(line.slice(5).trim());
1498
+ if (id !== null && Number.isFinite(id)) this.cursor = id;
1499
+ if (event === "mutation") try {
1500
+ this.opts.onRow(JSON.parse(data.join("\n")));
1501
+ } catch {}
1502
+ else if (event === "full-refresh") this.opts.onFullRefresh();
1503
+ else if (event === "auth-revoked") {
1504
+ this.closed = true;
1505
+ this.abort?.abort();
1506
+ this.opts.onAuthLost();
1507
+ }
1508
+ }
1509
+ };
1510
+ //#endregion
1511
+ //#region src/document/CloudDocumentHandle.ts
1512
+ var CloudDocumentHandle = class {
1513
+ constructor(http, id, layerName = DEFAULT_LAYER_NAME, initialHead, initialToken = null, sessionId = `cloud:anon:${id}`) {
1514
+ this.http = http;
1515
+ this.layerName = layerName;
1516
+ _defineProperty(this, "id", void 0);
1517
+ _defineProperty(this, "capabilities", {
1518
+ weakAnnotationEditSessions: "required",
1519
+ pageEditSessions: "unsupported"
1520
+ });
1521
+ _defineProperty(this, "metadata", void 0);
1522
+ _defineProperty(this, "annotations", void 0);
1523
+ _defineProperty(this, "actions", void 0);
1524
+ _defineProperty(this, "attachments", void 0);
1525
+ _defineProperty(this, "forms", void 0);
1526
+ _defineProperty(this, "search", void 0);
1527
+ _defineProperty(this, "pages", void 0);
1528
+ _defineProperty(this, "redaction", void 0);
1529
+ _defineProperty(this, "security", void 0);
1530
+ _defineProperty(this, "render", void 0);
1531
+ _defineProperty(this, "events", void 0);
1532
+ _defineProperty(this, "publisher", void 0);
1533
+ _defineProperty(this, "hub", void 0);
1534
+ _defineProperty(this, "sessionId", void 0);
1535
+ _defineProperty(this, "sseClient", null);
1536
+ _defineProperty(this, "sseSubscribers", 0);
1537
+ _defineProperty(this, "closed", false);
1538
+ _defineProperty(this, "manifestCache", null);
1539
+ _defineProperty(this, "inflightManifest", null);
1540
+ _defineProperty(this, "manifestFloorVersion", 0);
1541
+ _defineProperty(this, "pendingInitialHead", void 0);
1542
+ _defineProperty(this, "manifestAccessor", void 0);
1543
+ this.id = id;
1544
+ this.pendingInitialHead = initialHead ?? null;
1545
+ const security = new CloudDocumentSecurityService(http, id, layerName, initialHead ?? fallbackUnknownHead(id), { isClosed: () => this.closed }, initialToken);
1546
+ this.security = security;
1547
+ this.render = { policy: async () => {
1548
+ const advertised = (security.currentAccess ?? (await security.establishAccess()).access ?? null)?.renderPolicy;
1549
+ if (!advertised) return CONTINUOUS_RENDER_POLICY;
1550
+ return {
1551
+ kind: "lattice",
1552
+ fullPage: { widths: advertised.fullPage.widths },
1553
+ ...advertised.tiles ? { tiles: advertised.tiles } : {},
1554
+ ...advertised.appearances ? { appearances: advertised.appearances } : {},
1555
+ ...advertised.maxRenderPixels !== void 0 ? { maxRenderPixels: advertised.maxRenderPixels } : {},
1556
+ formats: advertised.formats,
1557
+ background: advertised.background,
1558
+ enforced: advertised.enforced
1559
+ };
1560
+ } };
1561
+ const hub = new EventHub();
1562
+ this.hub = hub;
1563
+ this.sessionId = sessionId;
1564
+ this.events = {
1565
+ subscribe: (listener) => {
1566
+ const unsubscribe = hub.subscribe(listener);
1567
+ this.retainRemoteStream();
1568
+ let released = false;
1569
+ return () => {
1570
+ unsubscribe();
1571
+ if (!released) {
1572
+ released = true;
1573
+ this.releaseRemoteStream();
1574
+ }
1575
+ };
1576
+ },
1577
+ lastServerId: () => hub.lastServerId()
1578
+ };
1579
+ this.publisher = new SessionEventPublisher(hub, sessionId);
1580
+ this.manifestAccessor = {
1581
+ get: (signal) => this.getManifest(signal),
1582
+ refresh: (signal) => this.refreshManifest(signal),
1583
+ apply: (meta, owns) => this.absorbMutation(meta, owns),
1584
+ applyPageStructure: (cache) => this.absorbPageStructure(cache),
1585
+ applyPageDelete: (cache, deletedPages) => this.absorbPageDelete(cache, deletedPages),
1586
+ applyMetadata: (cache) => this.absorbMetadata(cache),
1587
+ applyAttachments: (cache) => this.absorbAttachments(cache)
1588
+ };
1589
+ this.metadata = new CloudMetadataService(http, id, layerName, () => this.closed, this.manifestAccessor, this.publisher);
1590
+ this.annotations = new CloudDocumentAnnotationsService(http, id, layerName, () => this.closed, this.manifestAccessor);
1591
+ this.actions = new CloudDocumentActionsService(http, id, layerName, () => this.closed, this.manifestAccessor);
1592
+ this.attachments = new CloudDocumentAttachmentsService(http, id, layerName, () => this.closed, this.manifestAccessor, this.publisher);
1593
+ this.forms = new CloudDocumentFormsService(http, id, layerName, () => this.closed, this.manifestAccessor, this.publisher);
1594
+ this.search = new CloudDocumentSearchService(http, id, layerName, () => this.closed, this.manifestAccessor);
1595
+ this.pages = new CloudDocumentPagesService(http, id, layerName, () => this.closed, this.manifestAccessor, this.publisher);
1596
+ this.redaction = new CloudDocumentRedactionService(http, id, layerName, () => this.closed, this.manifestAccessor, this.publisher);
1597
+ }
1598
+ page(pageObjectNumber) {
1599
+ return new CloudPageHandle(pageObjectNumber, -1, this.http, this.id, this.layerName, () => this.closed, this.manifestAccessor, this.publisher);
1600
+ }
1601
+ download(opts = {}) {
1602
+ if (this.closed) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.id} is closed`));
1603
+ const mode = opts.mode ?? DEFAULT_PDF_SAVE_MODE;
1604
+ return AbortablePromise$1.run(async (signal) => {
1605
+ const buildPath = async () => {
1606
+ const manifest = await this.getManifest(signal);
1607
+ return wirePaths.layerDownloadVersioned(this.id, this.layerName, {
1608
+ docVersion: manifest.docVersion,
1609
+ mode
1610
+ });
1611
+ };
1612
+ try {
1613
+ return await this.http.getBytes(await buildPath(), signal);
1614
+ } catch (err) {
1615
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
1616
+ await this.refreshManifest(signal);
1617
+ return this.http.getBytes(await buildPath(), signal);
1618
+ }
1619
+ });
1620
+ }
1621
+ /**
1622
+ * Return the cached manifest, fetching cold-cache once if needed.
1623
+ * Concurrent callers share a single inflight request (singleflight)
1624
+ * so an N-page handle that opens N services in parallel still
1625
+ * triggers exactly one `/head` + `/manifest@docVersion=N` round-trip.
1626
+ */
1627
+ async getManifest(signal) {
1628
+ if (this.manifestCache) return this.manifestCache;
1629
+ if (!this.inflightManifest) this.startManifestFetch({ allowInitialHead: true });
1630
+ const promise = this.inflightManifest;
1631
+ if (!promise) throw new EngineError$1(EngineErrorCode$1.Unknown, `manifest fetch was not started for document ${this.id}`);
1632
+ return awaitSignal(promise, signal);
1633
+ }
1634
+ /**
1635
+ * Force re-fetch. Used by the transparent-retry path when a leaf
1636
+ * URL returns 404 (stale version). Replaces `manifestCache`
1637
+ * wholesale so the next `getManifest()` is a Map lookup.
1638
+ */
1639
+ async refreshManifest(signal) {
1640
+ return awaitSignal(this.startManifestFetch({ allowInitialHead: false }), signal);
1641
+ }
1642
+ /**
1643
+ * Monotone plane-scope flip: mark the planes a mutation OWNS as layer-scoped in
1644
+ * the cached manifest. Scopes only ever move base → layer (no revert op
1645
+ * exists), so flipping is safe under ANY event ordering — a duplicate or
1646
+ * out-of-order event still proves the plane diverged at some point — and
1647
+ * the manifest fetch stays the authoritative source (the 404 → refresh
1648
+ * rail heals any miss, e.g. a future mutation kind this client doesn't
1649
+ * know about).
1650
+ */
1651
+ flipScopes(owns) {
1652
+ const cache = this.manifestCache;
1653
+ const scopes = cache?.scopes;
1654
+ if (!cache || !scopes) return;
1655
+ if (owns.every((plane) => scopes[plane] === "layer")) return;
1656
+ const next = { ...scopes };
1657
+ for (const plane of owns) next[plane] = "layer";
1658
+ this.manifestCache = {
1659
+ ...cache,
1660
+ scopes: next
1661
+ };
1662
+ }
1663
+ absorbMutation(meta, owns) {
1664
+ this.flipScopes(owns);
1665
+ const delta = meta.cacheDelta;
1666
+ if (delta) this.manifestFloorVersion = Math.max(this.manifestFloorVersion, delta.docVersion);
1667
+ this.inflightManifest = null;
1668
+ if (!this.manifestCache) return;
1669
+ if (delta) {
1670
+ if (delta.docVersion <= this.manifestCache.docVersion) return;
1671
+ if (delta.previousDocVersion !== this.manifestCache.docVersion) {
1672
+ this.manifestCache = null;
1673
+ return;
1674
+ }
1675
+ }
1676
+ const byPageObjectNumber = new Map(this.manifestCache.pages.map((page) => [page.state.pageObjectNumber, page]));
1677
+ for (const pageState of meta.affectedPages) {
1678
+ const existing = byPageObjectNumber.get(pageState.pageObjectNumber);
1679
+ if (existing) byPageObjectNumber.set(pageState.pageObjectNumber, {
1680
+ ...existing,
1681
+ state: pageState
1682
+ });
1683
+ }
1684
+ if (delta) for (const page of delta.pages) {
1685
+ const existing = byPageObjectNumber.get(page.pageObjectNumber);
1686
+ if (existing) byPageObjectNumber.set(page.pageObjectNumber, {
1687
+ ...existing,
1688
+ cache: page.cache
1689
+ });
1690
+ }
1691
+ this.manifestCache = {
1692
+ ...this.manifestCache,
1693
+ docVersion: delta?.docVersion ?? this.manifestCache.docVersion,
1694
+ pages: Array.from(byPageObjectNumber.values()).sort((a, b) => a.state.pageObjectNumber - b.state.pageObjectNumber)
1695
+ };
1696
+ }
1697
+ /**
1698
+ * Patch the cached manifest after a set-preserving page-structure op (move,
1699
+ * rotate). Both are purely structural: they advance `docVersion` (so leaf
1700
+ * URLs re-resolve) and `layoutVersion` (so the /layout leaf re-fetches),
1701
+ * but leave every per-page pin untouched — a rotate renders the SAME
1702
+ * normalized bitmaps, a move the same pages. We raise the floor
1703
+ * unconditionally and, when our cache is exactly one version behind,
1704
+ * advance it in place; otherwise we drop it and refetch lazily.
1705
+ */
1706
+ absorbPageStructure(cache) {
1707
+ this.flipScopes(["layout"]);
1708
+ this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
1709
+ this.inflightManifest = null;
1710
+ if (!this.manifestCache) return;
1711
+ if (cache.docVersion <= this.manifestCache.docVersion) return;
1712
+ if (cache.previousDocVersion !== this.manifestCache.docVersion) {
1713
+ this.manifestCache = null;
1714
+ return;
1715
+ }
1716
+ this.manifestCache = {
1717
+ ...this.manifestCache,
1718
+ docVersion: cache.docVersion,
1719
+ layoutVersion: cache.layoutVersion
1720
+ };
1721
+ }
1722
+ /**
1723
+ * Patch the cached manifest after a page delete: the shared structural
1724
+ * advance plus dropping the deleted pages' manifest rows, so no leaf URL
1725
+ * for a retired PON can be built from the cache (a stale request would
1726
+ * 404 anyway — this keeps the failure local and instant).
1727
+ */
1728
+ absorbPageDelete(cache, deletedPages) {
1729
+ this.flipScopes([
1730
+ "layout",
1731
+ "content",
1732
+ "annotations"
1733
+ ]);
1734
+ this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
1735
+ this.inflightManifest = null;
1736
+ if (!this.manifestCache) return;
1737
+ if (cache.docVersion <= this.manifestCache.docVersion) return;
1738
+ if (cache.previousDocVersion !== this.manifestCache.docVersion) {
1739
+ this.manifestCache = null;
1740
+ return;
1741
+ }
1742
+ const deleted = new Set(deletedPages);
1743
+ this.manifestCache = {
1744
+ ...this.manifestCache,
1745
+ docVersion: cache.docVersion,
1746
+ layoutVersion: cache.layoutVersion,
1747
+ pages: this.manifestCache.pages.filter((page) => !deleted.has(page.state.pageObjectNumber))
1748
+ };
1749
+ }
1750
+ /**
1751
+ * Patch the cached manifest after a metadata write. Symmetric with
1752
+ * {@link absorbPageMove}: a metadata edit advances `docVersion` (so leaf
1753
+ * URLs re-resolve) and `metadataVersion` (so the /metadata leaf re-fetches),
1754
+ * but leaves `layoutVersion` and every per-page pin untouched. Raise the
1755
+ * floor unconditionally and, when our cache is exactly one version behind,
1756
+ * advance it in place; otherwise drop it and refetch lazily.
1757
+ */
1758
+ absorbMetadata(cache) {
1759
+ this.flipScopes(["metadata"]);
1760
+ this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
1761
+ this.inflightManifest = null;
1762
+ if (!this.manifestCache) return;
1763
+ if (cache.docVersion <= this.manifestCache.docVersion) return;
1764
+ if (cache.previousDocVersion !== this.manifestCache.docVersion) {
1765
+ this.manifestCache = null;
1766
+ return;
1767
+ }
1768
+ this.manifestCache = {
1769
+ ...this.manifestCache,
1770
+ docVersion: cache.docVersion,
1771
+ metadataVersion: cache.metadataVersion
1772
+ };
1773
+ }
1774
+ /**
1775
+ * Patch the cached manifest after an attachment create/delete. Symmetric
1776
+ * with {@link absorbMetadata}: an attachment write advances `docVersion`
1777
+ * (so leaf URLs re-resolve) and `attachmentsVersion` (so the /attachments
1778
+ * listing and /attachment-files leaves re-fetch), but leaves
1779
+ * `layoutVersion` and every per-page pin untouched. Raise the floor
1780
+ * unconditionally and, when our cache is exactly one version behind,
1781
+ * advance it in place; otherwise drop it and refetch lazily.
1782
+ */
1783
+ absorbAttachments(cache) {
1784
+ this.flipScopes(["attachments"]);
1785
+ this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
1786
+ this.inflightManifest = null;
1787
+ if (!this.manifestCache) return;
1788
+ if (cache.docVersion <= this.manifestCache.docVersion) return;
1789
+ if (cache.previousDocVersion !== this.manifestCache.docVersion) {
1790
+ this.manifestCache = null;
1791
+ return;
1792
+ }
1793
+ this.manifestCache = {
1794
+ ...this.manifestCache,
1795
+ docVersion: cache.docVersion,
1796
+ attachmentsVersion: cache.attachmentsVersion
1797
+ };
1798
+ }
1799
+ startManifestFetch(opts) {
1800
+ const ctrl = new AbortController();
1801
+ const promise = this.fetchManifest(ctrl.signal, opts);
1802
+ this.inflightManifest = promise;
1803
+ promise.then((manifest) => {
1804
+ if (manifest.docVersion >= this.manifestFloorVersion && (!this.manifestCache || manifest.docVersion >= this.manifestCache.docVersion)) this.manifestCache = manifest;
1805
+ }).catch(() => void 0).finally(() => {
1806
+ if (this.inflightManifest === promise) this.inflightManifest = null;
1807
+ });
1808
+ return promise;
1809
+ }
1810
+ async fetchManifest(signal, opts) {
1811
+ if (this.closed) throw new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.id} is closed`);
1812
+ const head = opts.allowInitialHead ? this.consumeInitialHead() : null;
1813
+ if (head) try {
1814
+ return await this.fetchManifestForHead(head, signal);
1815
+ } catch (err) {
1816
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
1817
+ }
1818
+ const freshHead = await this.http.getJson(wirePaths.layerHead(this.id, this.layerName), (raw) => DocumentHeadSchema.parse(raw), signal);
1819
+ return this.fetchManifestForHead(freshHead, signal);
1820
+ }
1821
+ consumeInitialHead() {
1822
+ const head = this.pendingInitialHead;
1823
+ this.pendingInitialHead = null;
1824
+ return head;
1825
+ }
1826
+ fetchManifestForHead(head, signal) {
1827
+ return this.http.getJson(wirePaths.layerManifest(this.id, this.layerName, head.docVersion), (raw) => DocumentManifestSchema.parse(raw), signal);
1828
+ }
1829
+ close() {
1830
+ if (this.closed) return AbortablePromise$1.resolveValue(void 0);
1831
+ this.closed = true;
1832
+ this.sseClient?.close();
1833
+ this.sseClient = null;
1834
+ this.manifestCache = null;
1835
+ this.inflightManifest = null;
1836
+ return AbortablePromise$1.resolveValue(void 0);
1837
+ }
1838
+ retainRemoteStream() {
1839
+ this.sseSubscribers += 1;
1840
+ if (this.sseSubscribers > 1 || this.closed || this.sseClient) return;
1841
+ this.sseClient = new SseClient({
1842
+ http: this.http,
1843
+ path: wirePaths.layerEvents(this.id, this.layerName),
1844
+ initialCursor: this.hub.lastServerId() ?? this.manifestCache?.auditHead ?? null,
1845
+ onRow: (row) => {
1846
+ const event = auditRowToEvent(row, this.sessionId);
1847
+ if (!event) return;
1848
+ this.absorbRemoteEvent(event);
1849
+ this.hub.publish(event);
1850
+ },
1851
+ onFullRefresh: () => {
1852
+ this.manifestCache = null;
1853
+ this.inflightManifest = null;
1854
+ },
1855
+ onAuthLost: () => {
1856
+ this.sseClient = null;
1857
+ }
1858
+ });
1859
+ this.sseClient.open();
1860
+ }
1861
+ releaseRemoteStream() {
1862
+ this.sseSubscribers = Math.max(0, this.sseSubscribers - 1);
1863
+ if (this.sseSubscribers === 0 && this.sseClient) {
1864
+ this.sseClient.close();
1865
+ this.sseClient = null;
1866
+ }
1867
+ }
1868
+ /** Patch the cached manifest from a REMOTE event's coherence pins — the
1869
+ * same absorb rails local mutations use, so reads stay warm no matter
1870
+ * whose hand caused the change. */
1871
+ absorbRemoteEvent(event) {
1872
+ switch (event.type) {
1873
+ case "annotation.created":
1874
+ case "annotation.updated":
1875
+ case "annotation.deleted":
1876
+ case "annotation.moved":
1877
+ this.absorbMutation(event.meta, ["annotations"]);
1878
+ return;
1879
+ case "pages.moved":
1880
+ case "pages.rotated":
1881
+ if (event.cache) this.absorbPageStructure(event.cache);
1882
+ return;
1883
+ case "pages.deleted":
1884
+ if (event.cache) this.absorbPageDelete(event.cache, event.pageObjectNumbers);
1885
+ return;
1886
+ case "metadata.updated":
1887
+ if (event.cache) this.absorbMetadata(event.cache);
1888
+ return;
1889
+ case "attachment.created":
1890
+ case "attachment.deleted":
1891
+ if (event.cache) this.absorbAttachments(event.cache);
1892
+ return;
1893
+ case "form.valueChanged":
1894
+ case "form.imported":
1895
+ case "form.repaired":
1896
+ case "form.fieldCreated":
1897
+ case "form.fieldUpdated":
1898
+ case "form.fieldDeleted":
1899
+ case "form.widgetAttached":
1900
+ case "form.widgetDetached":
1901
+ this.absorbMutation(event.meta, ["annotations"]);
1902
+ return;
1903
+ case "form.effectsApplied":
1904
+ if (event.meta) this.absorbMutation(event.meta, ["annotations"]);
1905
+ return;
1906
+ case "pages.flattened":
1907
+ if (event.meta) this.absorbMutation(event.meta, ["content", "annotations"]);
1908
+ return;
1909
+ case "redaction.applied":
1910
+ if (event.meta) this.absorbMutation(event.meta, ["content", "annotations"]);
1911
+ return;
1912
+ }
1913
+ }
1914
+ };
1915
+ function fallbackUnknownHead(id) {
1916
+ return {
1917
+ id,
1918
+ baseSha: "",
1919
+ storageSizeBytes: 0,
1920
+ docVersion: 1,
1921
+ state: "ready",
1922
+ encryption: {
1923
+ state: "unknown",
1924
+ requiresPassword: null
1925
+ },
1926
+ permissions: {
1927
+ known: false,
1928
+ bits: null,
1929
+ allAllowed: null,
1930
+ openedAs: null,
1931
+ securityHandlerRevision: null,
1932
+ canUpgradeToOwner: false
1933
+ },
1934
+ access: {
1935
+ required: true,
1936
+ reasons: ["permissions-unknown"],
1937
+ endpoint: wirePaths.access
1938
+ }
1939
+ };
1940
+ }
1941
+ function awaitSignal(promise, signal) {
1942
+ if (signal.aborted) return Promise.reject(new AbortError$1(signal.reason));
1943
+ return new Promise((resolve, reject) => {
1944
+ const onAbort = () => {
1945
+ reject(new AbortError$1(signal.reason));
1946
+ };
1947
+ signal.addEventListener("abort", onAbort, { once: true });
1948
+ promise.then((value) => {
1949
+ signal.removeEventListener("abort", onAbort);
1950
+ resolve(value);
1951
+ }, (reason) => {
1952
+ signal.removeEventListener("abort", onAbort);
1953
+ reject(reason);
1954
+ });
1955
+ });
1956
+ }
1957
+ //#endregion
1958
+ //#region src/transport/HttpClient.ts
1959
+ var HttpClient = class HttpClient {
1960
+ constructor(opts) {
1961
+ _defineProperty(this, "baseUrl", void 0);
1962
+ _defineProperty(this, "tokenFn", void 0);
1963
+ _defineProperty(this, "fetchFn", void 0);
1964
+ _defineProperty(this, "sessionId", void 0);
1965
+ _defineProperty(this, "cdnBinding", null);
1966
+ this.baseUrl = opts.baseUrl.replace(/\/$/, "");
1967
+ const token = opts.token;
1968
+ this.tokenFn = token === void 0 ? null : typeof token === "function" ? token : () => token;
1969
+ this.sessionId = opts.sessionId ?? null;
1970
+ this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
1971
+ }
1972
+ /**
1973
+ * Return a clone of this client bound to a different bearer. Used
1974
+ * by `CloudEngine.open` to mint a per-handle client carrying the
1975
+ * per-open token, so each opened document's RPCs go out under
1976
+ * the right authorization without disturbing the engine-level
1977
+ * token.
1978
+ */
1979
+ withToken(token) {
1980
+ return new HttpClient({
1981
+ baseUrl: this.baseUrl,
1982
+ token,
1983
+ ...this.sessionId ? { sessionId: this.sessionId } : {},
1984
+ fetch: this.fetchFn
1985
+ });
1986
+ }
1987
+ /**
1988
+ * Raw streaming GET (the SSE channel). Same auth/session headers and
1989
+ * error mapping as every other request; the caller owns the body stream.
1990
+ */
1991
+ async stream(path, headers, signal) {
1992
+ return this.request(path, {
1993
+ method: "GET",
1994
+ headers: new Headers({
1995
+ Accept: "text/event-stream",
1996
+ ...headers
1997
+ }),
1998
+ signal
1999
+ });
2000
+ }
2001
+ /**
2002
+ * Install (or clear) the per-session CDN binding. Pass `null` to
2003
+ * route everything to origin again — useful on session close or
2004
+ * when an adapter swap happens mid-session (rare).
2005
+ *
2006
+ * The HttpClient stays dumb about WHEN to do this; the cloud
2007
+ * document handle pushes the binding in after /access succeeds.
2008
+ *
2009
+ * Side effect: in the browser, signedCookies are written to
2010
+ * `document.cookie` once per setCdnAccess call so the CDN edge
2011
+ * receives them on subsequent fetches. Node side ignores cookies
2012
+ * for now (cookie jar wiring deferred until needed for tests).
2013
+ */
2014
+ setCdnAccess(binding) {
2015
+ this.cdnBinding = binding;
2016
+ if (binding?.cdn?.signedCookies && typeof globalThis.document !== "undefined") for (const cookie of binding.cdn.signedCookies) {
2017
+ const parts = [`${cookie.name}=${cookie.value}`];
2018
+ parts.push(`Path=${cookie.path ?? "/"}`);
2019
+ if (cookie.domain) parts.push(`Domain=${cookie.domain}`);
2020
+ if (cookie.expires) parts.push(`Expires=${(/* @__PURE__ */ new Date(cookie.expires * 1e3)).toUTCString()}`);
2021
+ parts.push("SameSite=None", "Secure");
2022
+ globalThis.document.cookie = parts.join("; ");
2023
+ }
2024
+ }
2025
+ /** Current CDN binding (or null). Exposed for diagnostics/tests. */
2026
+ get currentCdnBinding() {
2027
+ return this.cdnBinding;
2028
+ }
2029
+ absoluteUrl(path) {
2030
+ return `${this.baseUrl}${path}`;
2031
+ }
2032
+ async getBlob(path, signal) {
2033
+ const res = await this.request(path, {
2034
+ method: "GET",
2035
+ signal
2036
+ });
2037
+ if (!res.ok) await this.throwFromBody(res);
2038
+ return await res.blob();
2039
+ }
2040
+ async getBytes(path, signal) {
2041
+ const res = await this.request(path, {
2042
+ method: "GET",
2043
+ headers: new Headers({ Accept: "application/pdf" }),
2044
+ signal
2045
+ });
2046
+ if (!res.ok) await this.throwFromBody(res);
2047
+ return new Uint8Array(await res.arrayBuffer());
2048
+ }
2049
+ /**
2050
+ * Resolve the current bearer token. Awaits the user-supplied
2051
+ * token factory if it returns a promise. Used by the cloud
2052
+ * engine to read doc-scoped claims **without** verifying — the
2053
+ * server is the verifier of record. Throws when no token is
2054
+ * configured.
2055
+ */
2056
+ async currentToken() {
2057
+ if (!this.tokenFn) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "http client has no token configured");
2058
+ return await this.tokenFn();
2059
+ }
2060
+ async getJson(path, parser, signal) {
2061
+ const res = await this.request(path, {
2062
+ method: "GET",
2063
+ signal
2064
+ });
2065
+ return await this.parseJsonResponse(res, parser);
2066
+ }
2067
+ /**
2068
+ * GET a versioned URL with transparent stale-version recovery. If
2069
+ * the server returns 404 (the well-known "this version is gone"
2070
+ * signal for content-addressed reads), call `onStaleVersion` (which
2071
+ * the caller wires to a manifest refresh), then re-resolve `buildPath`
2072
+ * and retry the GET exactly once. Any non-404 error propagates.
2073
+ *
2074
+ * The retry budget is hard-capped at 1: after a refresh, if the
2075
+ * server's idea of the current version is still ahead of the
2076
+ * client's view, the second request fails normally — this matches
2077
+ * the CDN cache invariant ("a versioned URL's bytes are immutable
2078
+ * for its lifetime").
2079
+ */
2080
+ async getJsonWithRefresh(buildPath, parser, onStaleVersion, signal) {
2081
+ const initialPath = await buildPath(signal);
2082
+ try {
2083
+ return await this.getJson(initialPath, parser, signal);
2084
+ } catch (err) {
2085
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
2086
+ await onStaleVersion(signal);
2087
+ const retryPath = await buildPath(signal);
2088
+ return await this.getJson(retryPath, parser, signal);
2089
+ }
2090
+ }
2091
+ /**
2092
+ * GET a `multipart/form-data` body and parse it with the platform's
2093
+ * `Response.formData()` (works in browsers and undici). Text parts come
2094
+ * back as strings, parts with a filename as `Blob`s.
2095
+ */
2096
+ async getFormData(path, signal) {
2097
+ const res = await this.request(path, {
2098
+ method: "GET",
2099
+ headers: new Headers({ Accept: "multipart/form-data" }),
2100
+ signal
2101
+ });
2102
+ if (!res.ok) await this.throwFromBody(res);
2103
+ return await res.formData();
2104
+ }
2105
+ /**
2106
+ * `getFormData` with the same single-retry stale-version recovery as
2107
+ * {@link getJsonWithRefresh}: a 404 on a versioned URL triggers
2108
+ * `onStaleVersion` (a manifest refresh) and one re-resolve + retry.
2109
+ */
2110
+ async getFormDataWithRefresh(buildPath, onStaleVersion, signal) {
2111
+ const initialPath = await buildPath(signal);
2112
+ try {
2113
+ return await this.getFormData(initialPath, signal);
2114
+ } catch (err) {
2115
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
2116
+ await onStaleVersion(signal);
2117
+ const retryPath = await buildPath(signal);
2118
+ return await this.getFormData(retryPath, signal);
2119
+ }
2120
+ }
2121
+ /**
2122
+ * `getBlob` with the same single-retry stale-version recovery as
2123
+ * {@link getJsonWithRefresh}: a 404 on a versioned URL triggers
2124
+ * `onStaleVersion` (a manifest refresh) and one re-resolve + retry.
2125
+ * Render blobs ride this rail: a scope flip re-resolves the
2126
+ * doc-vs-layer path family instead of failing on a stale URL.
2127
+ */
2128
+ async getBlobWithRefresh(buildPath, onStaleVersion, signal) {
2129
+ const initialPath = await buildPath(signal);
2130
+ try {
2131
+ return await this.getBlob(initialPath, signal);
2132
+ } catch (err) {
2133
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
2134
+ await onStaleVersion(signal);
2135
+ const retryPath = await buildPath(signal);
2136
+ return await this.getBlob(retryPath, signal);
2137
+ }
2138
+ }
2139
+ /** GET raw bytes plus the response headers (attachment-file leaves). */
2140
+ async getFile(path, signal) {
2141
+ const res = await this.request(path, {
2142
+ method: "GET",
2143
+ signal
2144
+ });
2145
+ if (!res.ok) await this.throwFromBody(res);
2146
+ return {
2147
+ bytes: new Uint8Array(await res.arrayBuffer()),
2148
+ headers: res.headers
2149
+ };
2150
+ }
2151
+ /**
2152
+ * `getFile` with the same single-retry stale-version recovery as
2153
+ * {@link getJsonWithRefresh}: a 404 on a versioned URL triggers
2154
+ * `onStaleVersion` (a manifest refresh) and one re-resolve + retry.
2155
+ */
2156
+ async getFileWithRefresh(buildPath, onStaleVersion, signal) {
2157
+ const initialPath = await buildPath(signal);
2158
+ try {
2159
+ return await this.getFile(initialPath, signal);
2160
+ } catch (err) {
2161
+ if (!EngineError$1.is(err, EngineErrorCode$1.NotFound)) throw err;
2162
+ await onStaleVersion(signal);
2163
+ const retryPath = await buildPath(signal);
2164
+ return await this.getFile(retryPath, signal);
2165
+ }
2166
+ }
2167
+ async postMultipartJson(path, body, parser, signal) {
2168
+ const res = await this.request(path, {
2169
+ method: "POST",
2170
+ body,
2171
+ signal
2172
+ });
2173
+ return await this.parseJsonResponse(res, parser);
2174
+ }
2175
+ /** PATCH counterpart of {@link postMultipartJson} (binary-carrying mutations). */
2176
+ async patchMultipartJson(path, body, parser, signal) {
2177
+ const res = await this.request(path, {
2178
+ method: "PATCH",
2179
+ body,
2180
+ signal
2181
+ });
2182
+ return await this.parseJsonResponse(res, parser);
2183
+ }
2184
+ async postJson(path, body, parser, signal) {
2185
+ const res = await this.requestJson(path, "POST", body, signal);
2186
+ return await this.parseJsonResponse(res, parser);
2187
+ }
2188
+ /** POST a raw binary body (FDF/XFDF import) and parse the JSON response. */
2189
+ async postBytesJson(path, bytes, contentType, parser, signal) {
2190
+ const headers = new Headers({ "Content-Type": contentType });
2191
+ const body = new Uint8Array(bytes).buffer;
2192
+ const res = await this.request(path, {
2193
+ method: "POST",
2194
+ body,
2195
+ headers,
2196
+ signal
2197
+ });
2198
+ return await this.parseJsonResponse(res, parser);
2199
+ }
2200
+ async patchJson(path, body, parser, signal) {
2201
+ const res = await this.requestJson(path, "PATCH", body, signal);
2202
+ return await this.parseJsonResponse(res, parser);
2203
+ }
2204
+ async deleteJson(path, parser, signal) {
2205
+ const res = await this.request(path, {
2206
+ method: "DELETE",
2207
+ signal
2208
+ });
2209
+ return await this.parseJsonResponse(res, parser);
2210
+ }
2211
+ async deleteEmpty(path, signal) {
2212
+ const res = await this.request(path, {
2213
+ method: "DELETE",
2214
+ signal
2215
+ });
2216
+ if (res.status === 204) return;
2217
+ if (!res.ok) await this.throwFromBody(res);
2218
+ }
2219
+ async postEmpty(path, body, signal) {
2220
+ const res = await this.requestJson(path, "POST", body, signal);
2221
+ if (res.status === 204) return;
2222
+ if (!res.ok) await this.throwFromBody(res);
2223
+ }
2224
+ requestJson(path, method, body, signal) {
2225
+ const headers = new Headers({ "Content-Type": "application/json" });
2226
+ return this.request(path, {
2227
+ method,
2228
+ body: JSON.stringify(body ?? {}),
2229
+ headers,
2230
+ signal
2231
+ });
2232
+ }
2233
+ async request(path, init) {
2234
+ const { url, extraHeader } = this.resolveOutgoing(path);
2235
+ const headers = new Headers(init.headers ?? {});
2236
+ if (this.tokenFn) {
2237
+ const token = await this.tokenFn();
2238
+ headers.set("Authorization", `Bearer ${token}`);
2239
+ }
2240
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
2241
+ if (this.sessionId) headers.set("X-Engine-Session-Id", this.sessionId);
2242
+ if (extraHeader) headers.set(extraHeader.name, extraHeader.value);
2243
+ try {
2244
+ return await this.fetchFn(url, {
2245
+ ...init,
2246
+ headers
2247
+ });
2248
+ } catch (err) {
2249
+ if (init.signal?.aborted) throw new AbortError$1(init.signal.reason ?? err);
2250
+ if (err?.name === "AbortError") throw new AbortError$1(err.message);
2251
+ throw new EngineError$1(EngineErrorCode$1.Network, `network error: ${err?.message ?? err}`, { cause: err });
2252
+ }
2253
+ }
2254
+ async parseJsonResponse(res, parser) {
2255
+ if (!res.ok) await this.throwFromBody(res);
2256
+ const json = await res.json().catch(() => ({}));
2257
+ try {
2258
+ return parser(json);
2259
+ } catch (err) {
2260
+ throw new EngineError$1(EngineErrorCode$1.WireFormat, `unexpected response shape: ${err?.message ?? err}`, { cause: err });
2261
+ }
2262
+ }
2263
+ /**
2264
+ * Decide where this request actually goes. With no CDN binding (or
2265
+ * binding's `cdn` is null) every request stays on origin. Otherwise
2266
+ * `applyCdnAccess` decides per-request based on the resource the
2267
+ * path resolves to (and whether the caller's scope granted CDN
2268
+ * access to it).
2269
+ */
2270
+ resolveOutgoing(path) {
2271
+ if (!this.cdnBinding || !this.cdnBinding.cdn) return {
2272
+ url: `${this.baseUrl}${path}`,
2273
+ extraHeader: null
2274
+ };
2275
+ const applied = applyCdnAccess({
2276
+ path,
2277
+ originUrl: this.baseUrl,
2278
+ docId: this.cdnBinding.docId,
2279
+ layerName: this.cdnBinding.layerName,
2280
+ cdn: this.cdnBinding.cdn
2281
+ });
2282
+ return {
2283
+ url: applied.url,
2284
+ extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null
2285
+ };
2286
+ }
2287
+ async throwFromBody(res) {
2288
+ let payload;
2289
+ try {
2290
+ payload = await res.json();
2291
+ } catch {
2292
+ payload = null;
2293
+ }
2294
+ const code = mapStatusToCode(res.status);
2295
+ if (payload && typeof payload === "object" && "error" in payload) {
2296
+ const inner = payload.error;
2297
+ const parsed = trySerialized(inner);
2298
+ if (parsed) throw new EngineError$1(parsed.code, parsed.message, { details: parsed.details });
2299
+ throw new EngineError$1(code, typeof inner === "object" && inner && "message" in inner ? String(inner.message) : `HTTP ${res.status}`, { details: { status: res.status } });
2300
+ }
2301
+ throw new EngineError$1(code, `HTTP ${res.status}`, { details: { status: res.status } });
2302
+ }
2303
+ };
2304
+ function mapStatusToCode(status) {
2305
+ if (status === 401) return EngineErrorCode$1.Unauthenticated;
2306
+ if (status === 403) return EngineErrorCode$1.Forbidden;
2307
+ if (status === 409) return EngineErrorCode$1.WeakAnnotationSessionConflict;
2308
+ if (status === 404) return EngineErrorCode$1.NotFound;
2309
+ if (status === 422) return EngineErrorCode$1.DocOpenFailed;
2310
+ if (status === 499) return EngineErrorCode$1.Aborted;
2311
+ if (status === 400) return EngineErrorCode$1.InvalidArg;
2312
+ return EngineErrorCode$1.Unknown;
2313
+ }
2314
+ function trySerialized(value) {
2315
+ const result = EngineErrorPayloadSchema.safeParse(value);
2316
+ return result.success ? result.data : null;
2317
+ }
2318
+ //#endregion
2319
+ //#region src/CloudEngine.ts
2320
+ /**
2321
+ * Cloud engine: speaks the same Engine interface as @embedpdf/engine
2322
+ * but routes everything through HTTPS to a remote @cloudpdf/server (or
2323
+ * CloudPDF SaaS). Identical observable contract; only the transport differs.
2324
+ */
2325
+ var CloudEngine = class CloudEngine {
2326
+ static fromOptions(opts) {
2327
+ const sessionId = `cloud:${generateUuid()}`;
2328
+ return new CloudEngine(new HttpClient({
2329
+ ...opts,
2330
+ sessionId
2331
+ }), sessionId);
2332
+ }
2333
+ constructor(http, sessionId) {
2334
+ this.http = http;
2335
+ this.sessionId = sessionId;
2336
+ _defineProperty(this, "destroyed", false);
2337
+ }
2338
+ open(input, options) {
2339
+ if (this.destroyed) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.RuntimeUnavailable, "engine destroyed"));
2340
+ options?.scope;
2341
+ options?.identity;
2342
+ const effectivePassword = options && "password" in options ? options.password ?? null : input.password ?? null;
2343
+ if (input.kind === "token") {
2344
+ const tokenSource = input.token;
2345
+ return AbortablePromise$1.run(async (signal) => {
2346
+ const docHttp = this.http.withToken(tokenSource);
2347
+ const token = await docHttp.currentToken();
2348
+ const claims = decodeUnverifiedClaims(token);
2349
+ const docId = claims.doc_id;
2350
+ if (typeof docId !== "string" || docId.length === 0) throw new EngineError$1(EngineErrorCode$1.InvalidArg, "cloud engine: token has no doc_id claim — mint a doc-scoped JWT");
2351
+ const layerName = typeof claims.layer_name === "string" && claims.layer_name.length > 0 ? claims.layer_name : DEFAULT_LAYER_NAME;
2352
+ const head = await docHttp.getJson(wirePaths.layerHead(docId, layerName), (raw) => DocumentHeadSchema.parse(raw), signal);
2353
+ const handle = new CloudDocumentHandle(docHttp, head.id, layerName, head, token, this.sessionId);
2354
+ await maybeAutoEstablishAccess(handle, head, signal, effectivePassword);
2355
+ return handle;
2356
+ });
2357
+ }
2358
+ if (input.kind === "id") {
2359
+ const id = input.id;
2360
+ const docHttp = input.token ? this.http.withToken(input.token) : this.http;
2361
+ return AbortablePromise$1.run(async (signal) => {
2362
+ let layerName = input.layerName ?? DEFAULT_LAYER_NAME;
2363
+ let resolvedToken = null;
2364
+ try {
2365
+ resolvedToken = await docHttp.currentToken();
2366
+ } catch {
2367
+ resolvedToken = null;
2368
+ }
2369
+ if (!input.layerName && resolvedToken) {
2370
+ const claims = decodeUnverifiedClaims(resolvedToken);
2371
+ if (typeof claims.layer_name === "string" && claims.layer_name.length > 0) layerName = claims.layer_name;
2372
+ }
2373
+ const head = await docHttp.getJson(wirePaths.layerHead(id, layerName), (raw) => DocumentHeadSchema.parse(raw), signal);
2374
+ const handle = new CloudDocumentHandle(docHttp, head.id, layerName, head, resolvedToken, this.sessionId);
2375
+ await maybeAutoEstablishAccess(handle, head, signal, effectivePassword);
2376
+ return handle;
2377
+ });
2378
+ }
2379
+ return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `cloud engine supports OpenInput.kind === 'token' or 'id' (got '${input.kind}')`));
2380
+ }
2381
+ destroy() {
2382
+ if (this.destroyed) return AbortablePromise$1.resolveValue(void 0);
2383
+ this.destroyed = true;
2384
+ return AbortablePromise$1.resolveValue(void 0);
2385
+ }
2386
+ };
2387
+ /**
2388
+ * Post-/head access establishment, in two layers:
2389
+ *
2390
+ * 1. **Supplied password** — a password given at open is always TRIED
2391
+ * (PDFium parity: the local engine feeds it to the loader no matter
2392
+ * what). Two triggers, because `/head` only carries the `password`
2393
+ * reason when a password is needed to READ:
2394
+ * - `reasons` has `'password'` → required-to-read case
2395
+ * - `head.permissions.canUpgradeToOwner` → permission-only encrypted
2396
+ * doc; the password can still upgrade to owner. Skipping this arm
2397
+ * would silently drop supplied OWNER passwords — and diverge from
2398
+ * the local engine on identical input.
2399
+ * Outcomes follow "a password failure may only block what the password
2400
+ * was needed for": a rejection in the required case leaves the handle
2401
+ * locked (the caller's prompt takes over, showing "incorrect"); in the
2402
+ * upgrade case the document stays readable and the failure is
2403
+ * non-fatal. The one /access POST also installs the CDN binding, so a
2404
+ * successful unlock covers the `password + cdn` combined case.
2405
+ *
2406
+ * 2. **CDN-only** — unchanged: `/access` without a password, transparently.
2407
+ * A required password with NO supplied password never auto-calls; the
2408
+ * dev prompts and `unlock()` runs the same POST.
2409
+ *
2410
+ * Non-password /access failures never break `open()` (the first real
2411
+ * request still tries origin, where the JWT check produces a regular
2412
+ * Forbidden) — but they are also never mistaken for a wrong password:
2413
+ * only a DocPasswordRequired/Incorrect rejection means "wrong password".
2414
+ */
2415
+ async function maybeAutoEstablishAccess(handle, head, signal, password) {
2416
+ const reasons = new Set(head.access.reasons);
2417
+ const security = handle.security;
2418
+ if (password != null && (reasons.has("password") || head.permissions.canUpgradeToOwner)) {
2419
+ if (await settleLinked(security.unlock({
2420
+ password,
2421
+ mode: "any"
2422
+ }), signal)) return;
2423
+ if (reasons.has("password")) return;
2424
+ }
2425
+ if (!head.access.required) return;
2426
+ if (reasons.has("password")) return;
2427
+ if (!reasons.has("cdn")) return;
2428
+ await settleLinked(security.establishAccess(), signal);
2429
+ }
2430
+ /** Await an /access attempt with open()-abort linkage. Returns whether it
2431
+ * succeeded; failures are contained (see maybeAutoEstablishAccess). */
2432
+ async function settleLinked(pending, signal) {
2433
+ const onAbort = () => pending.abort(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
2434
+ if (signal.aborted) onAbort();
2435
+ else signal.addEventListener("abort", onAbort, { once: true });
2436
+ try {
2437
+ await pending;
2438
+ return true;
2439
+ } catch {
2440
+ return false;
2441
+ } finally {
2442
+ signal.removeEventListener("abort", onAbort);
2443
+ }
2444
+ }
2445
+ //#endregion
2446
+ //#region src/index.ts
2447
+ function createCloudEngine(opts) {
2448
+ return CloudEngine.fromOptions(opts);
2449
+ }
2450
+ /**
2451
+ * Create a cloud {@link CloudEngine} — the drop-in counterpart of
2452
+ * `localEngine()`, so swapping local for cloud is a one-import change.
2453
+ *
2454
+ * Synchronous and cheap: no WASM to compile, no worker to spawn — the engine
2455
+ * holds an HTTP client and nothing else. Like `localEngine()`, the returned
2456
+ * instance is yours (call `destroy()` when done, usually never for a
2457
+ * module-scope singleton); pass a thunk (`engine={() => cloudEngine(...)}`)
2458
+ * to let a `<Viewer>` own the lifetime instead.
2459
+ *
2460
+ * Note the deliberate asymmetry with `localEngine()`: there is no `fonts`
2461
+ * option. Fallback fonts are a SERVER policy on the cloud (`Engine.fonts` is
2462
+ * `undefined` cloud-side), so they cannot be configured from the client. This
2463
+ * is the local-vs-cloud split, surfaced in the API.
2464
+ *
2465
+ * ```ts
2466
+ * const engine = cloudEngine({ baseUrl: 'https://pdf.example.com', token });
2467
+ * <Viewer engine={engine} plugins={[stagePlugin(), renderPlugin()]} />
2468
+ * ```
2469
+ */
2470
+ function cloudEngine(opts) {
2471
+ return CloudEngine.fromOptions(opts);
2472
+ }
2473
+ //#endregion
2474
+ export { AbortError, AbortablePromise, CloudDocumentAnnotationsService, CloudDocumentHandle, CloudDocumentPagesService, CloudEngine, CloudMetadataService, CloudPageAnnotationsService, CloudPageGeometryService, CloudPageHandle, CloudPageRenderService, EngineError, EngineErrorCode, HttpClient, cloudEngine, createCloudEngine, decodeUnverifiedClaims };
2475
+
2476
+ //# sourceMappingURL=index.js.map