@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/LICENSE +192 -0
- package/README.md +53 -0
- package/dist/index.cjs +2513 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +600 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.js +2476 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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
|