@cloudpdf/engine 3.0.0-next.1 → 3.0.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +366 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -4
- package/dist/index.d.ts +146 -4
- package/dist/index.js +365 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -77,6 +77,9 @@ function _defineProperty(e, r, t) {
|
|
|
77
77
|
}
|
|
78
78
|
//#endregion
|
|
79
79
|
//#region src/document/CloudDocumentAnnotationsService.ts
|
|
80
|
+
/** Bulk-read restarts after a mid-flight mutation staled the pinned
|
|
81
|
+
* version (404 on the immutable leaf → refresh the manifest → retry). */
|
|
82
|
+
const MAX_COHERENCE_RESTARTS = 2;
|
|
80
83
|
var CloudDocumentAnnotationsService = class {
|
|
81
84
|
constructor(http, docId, layerName, isClosed, manifest) {
|
|
82
85
|
this.http = http;
|
|
@@ -85,16 +88,57 @@ var CloudDocumentAnnotationsService = class {
|
|
|
85
88
|
this.isClosed = isClosed;
|
|
86
89
|
this.manifest = manifest;
|
|
87
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* One COHERENT whole-document snapshot: a single read of the immutable
|
|
93
|
+
* `annotations/items@annotationsVersion=N` leaf at the manifest's pin —
|
|
94
|
+
* materialized server-side by one raw (no page-load) sweep, CDN-cacheable
|
|
95
|
+
* because the pin bumps only when annotation list bodies actually change.
|
|
96
|
+
* A stale pin mid-read (a concurrent mutation → 404) refreshes the
|
|
97
|
+
* manifest and retries, so the result always belongs to one document
|
|
98
|
+
* moment (the torn read this method exists to prevent).
|
|
99
|
+
*/
|
|
88
100
|
listRawAll() {
|
|
89
101
|
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
90
102
|
return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
for (let attempt = 0;; attempt++) {
|
|
104
|
+
const manifest = attempt === 0 ? await this.manifest.get(signal) : await this.manifest.refresh(signal);
|
|
105
|
+
try {
|
|
106
|
+
return await this.readBulkAt(manifest, signal);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (!_embedpdf_engine_core_runtime.EngineError.is(err, _embedpdf_engine_core_runtime.EngineErrorCode.NotFound) || attempt >= MAX_COHERENCE_RESTARTS) throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
93
111
|
});
|
|
94
112
|
}
|
|
113
|
+
/** One bulk leaf read at the manifest's pin. The server stamps the body's
|
|
114
|
+
* `auditHead` at materialization time; a CDN-cached body may carry a
|
|
115
|
+
* cursor older than the current manifest's, which is still safe — the
|
|
116
|
+
* pin proves no items-affecting mutation landed in between, so replaying
|
|
117
|
+
* that window over the body is all no-ops. */
|
|
118
|
+
async readBulkAt(manifest, signal) {
|
|
119
|
+
const path = planesInherited(manifest, ["annotations"]) ? _embedpdf_engine_core_wire.wirePaths.docAnnotationsAll(this.docId, manifest.annotationsVersion) : _embedpdf_engine_core_wire.wirePaths.layerAnnotationsAll(this.docId, this.layerName, manifest.annotationsVersion);
|
|
120
|
+
const body = await this.http.getJson(path, (raw) => _embedpdf_engine_core_wire.AnnotationListSnapshotAllPagesSchema.parse(raw), signal);
|
|
121
|
+
return {
|
|
122
|
+
...body,
|
|
123
|
+
auditHead: body.auditHead ?? manifest.auditHead
|
|
124
|
+
};
|
|
125
|
+
}
|
|
95
126
|
listRaw(pageObjectNumber) {
|
|
96
127
|
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
97
|
-
return _embedpdf_engine_core_runtime.AbortablePromise.run((signal) => this.
|
|
128
|
+
return _embedpdf_engine_core_runtime.AbortablePromise.run((signal) => this.http.getJsonWithRefresh(async (s) => this.versionedPagePath(await this.manifest.get(s), pageObjectNumber), (raw) => _embedpdf_engine_core_wire.AnnotationListPageSnapshotSchema.parse(raw), async (s) => {
|
|
129
|
+
await this.manifest.refresh(s);
|
|
130
|
+
}, signal));
|
|
131
|
+
}
|
|
132
|
+
/** Versioned leaf URL for one manifest page entry, with the same
|
|
133
|
+
* plane routing as `CloudPageAnnotationsService.list()`: an inherited
|
|
134
|
+
* `annotations` plane reads the doc-level base leaf. */
|
|
135
|
+
pagePathAt(manifest, page) {
|
|
136
|
+
return planesInherited(manifest, ["annotations"]) ? _embedpdf_engine_core_wire.wirePaths.docPageAnnotations(this.docId, page.state.pageObjectNumber, page.cache.annotationVersion) : _embedpdf_engine_core_wire.wirePaths.layerPageAnnotations(this.docId, this.layerName, page.state.pageObjectNumber, page.cache.annotationVersion);
|
|
137
|
+
}
|
|
138
|
+
versionedPagePath(manifest, pageObjectNumber) {
|
|
139
|
+
const page = manifest.pages.find((p) => p.state.pageObjectNumber === pageObjectNumber);
|
|
140
|
+
if (!page) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `no page with object number ${pageObjectNumber} in document ${this.docId}`);
|
|
141
|
+
return this.pagePathAt(manifest, page);
|
|
98
142
|
}
|
|
99
143
|
beginWeakEdit(pageObjectNumbers) {
|
|
100
144
|
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
@@ -103,9 +147,6 @@ var CloudDocumentAnnotationsService = class {
|
|
|
103
147
|
return new CloudWeakAnnotationEditSession(this.http, this.docId, this.layerName, () => this.isClosed(), response);
|
|
104
148
|
});
|
|
105
149
|
}
|
|
106
|
-
async readCurrentPage(pageObjectNumber, signal) {
|
|
107
|
-
return this.http.getJson(_embedpdf_engine_core_wire.wirePaths.layerPageAnnotationsCurrent(this.docId, this.layerName, pageObjectNumber), (raw) => _embedpdf_engine_core_wire.AnnotationListPageSnapshotSchema.parse(raw), signal);
|
|
108
|
-
}
|
|
109
150
|
};
|
|
110
151
|
var CloudWeakAnnotationEditSession = class {
|
|
111
152
|
constructor(http, docId, layerName, isClosed, response) {
|
|
@@ -450,6 +491,13 @@ var CloudDocumentFormsService = class {
|
|
|
450
491
|
};
|
|
451
492
|
//#endregion
|
|
452
493
|
//#region src/document/CloudDocumentPagesService.ts
|
|
494
|
+
/** Detach a Uint8Array view into a standalone ArrayBuffer (the resource-map
|
|
495
|
+
* shape) without disturbing a larger buffer the caller still owns. */
|
|
496
|
+
function copyToExactBuffer(view) {
|
|
497
|
+
const copy = new ArrayBuffer(view.byteLength);
|
|
498
|
+
new Uint8Array(copy).set(view);
|
|
499
|
+
return copy;
|
|
500
|
+
}
|
|
453
501
|
/**
|
|
454
502
|
* Cloud-side document pages service. Mirrors `LocalDocumentPagesService`
|
|
455
503
|
* over HTTP: GET /pages for `list`, POST /pages/move for the reorder.
|
|
@@ -542,6 +590,46 @@ var CloudDocumentPagesService = class {
|
|
|
542
590
|
return result;
|
|
543
591
|
});
|
|
544
592
|
}
|
|
593
|
+
insert(bytes, destIndex) {
|
|
594
|
+
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
595
|
+
return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
|
|
596
|
+
const buffer = bytes instanceof ArrayBuffer ? bytes : copyToExactBuffer(bytes);
|
|
597
|
+
const form = buildMutationForm(destIndex !== void 0 ? { destIndex } : {}, { source: {
|
|
598
|
+
bytes: buffer,
|
|
599
|
+
mimeType: "application/pdf",
|
|
600
|
+
name: "source.pdf"
|
|
601
|
+
} });
|
|
602
|
+
const result = await this.http.postMultipartJson(_embedpdf_engine_core_wire.wirePaths.layerPagesInsert(this.docId, this.layerName), form, (raw) => _embedpdf_engine_core_wire.PageInsertResultSchema.parse(raw), signal);
|
|
603
|
+
if (result.cache) this.manifest.applyPageInsert(result.cache);
|
|
604
|
+
this.publisher.publishLocal({
|
|
605
|
+
type: "pages.inserted",
|
|
606
|
+
destIndex,
|
|
607
|
+
...result
|
|
608
|
+
});
|
|
609
|
+
return result;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
insertBlank(spec, destIndex) {
|
|
613
|
+
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
614
|
+
return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
|
|
615
|
+
const result = await this.http.postJson(_embedpdf_engine_core_wire.wirePaths.layerPagesInsertBlank(this.docId, this.layerName), {
|
|
616
|
+
size: spec.size,
|
|
617
|
+
...spec.count !== void 0 ? { count: spec.count } : {},
|
|
618
|
+
...destIndex !== void 0 ? { destIndex } : {}
|
|
619
|
+
}, (raw) => _embedpdf_engine_core_wire.PageInsertResultSchema.parse(raw), signal);
|
|
620
|
+
if (result.cache) this.manifest.applyPageInsert(result.cache);
|
|
621
|
+
this.publisher.publishLocal({
|
|
622
|
+
type: "pages.inserted",
|
|
623
|
+
destIndex,
|
|
624
|
+
...result
|
|
625
|
+
});
|
|
626
|
+
return result;
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
extract(pageObjectNumbers) {
|
|
630
|
+
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
631
|
+
return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => this.http.postJsonBytes(_embedpdf_engine_core_wire.wirePaths.layerPagesExtract(this.docId, this.layerName), { pageObjectNumbers }, signal));
|
|
632
|
+
}
|
|
545
633
|
flatten(pageObjectNumbers, usage = "display") {
|
|
546
634
|
if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
|
|
547
635
|
return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
|
|
@@ -754,6 +842,33 @@ var CloudDocumentSecurityService = class {
|
|
|
754
842
|
return (0, _embedpdf_engine_core_runtime.checkCapability)(cap, this.tokenScope, bits);
|
|
755
843
|
}
|
|
756
844
|
/**
|
|
845
|
+
* Per-record annotation authorization mirrors — the same
|
|
846
|
+
* `checkCollab`/`checkSetGroup` resolvers the server's route layer
|
|
847
|
+
* enforces with, over the same inputs: the raw scope
|
|
848
|
+
* (server-canonical post-/access, else the JWT's own claim), the
|
|
849
|
+
* caller's identity, and /head's PDF bits. A control gated on these
|
|
850
|
+
* matches the server's allow/deny for the same mutation. False
|
|
851
|
+
* before any token/access context exists — fail closed, same rule
|
|
852
|
+
* as `allows`.
|
|
853
|
+
*/
|
|
854
|
+
allowsAnnotationCreate() {
|
|
855
|
+
const id = this.identity ?? {};
|
|
856
|
+
return (0, _embedpdf_engine_core_runtime.checkCollab)("create", selfTarget(id), this.rawScope(), id, this.pdfBits());
|
|
857
|
+
}
|
|
858
|
+
allowsAnnotationMutation(action, target) {
|
|
859
|
+
return (0, _embedpdf_engine_core_runtime.checkCollab)(action, target, this.rawScope(), this.identity ?? {}, this.pdfBits());
|
|
860
|
+
}
|
|
861
|
+
allowsAnnotationGroupAssignment(groupId) {
|
|
862
|
+
return (0, _embedpdf_engine_core_runtime.checkSetGroup)(groupId, this.identity?.group_id, this.rawScope(), this.pdfBits());
|
|
863
|
+
}
|
|
864
|
+
/** Raw scope for the collab resolver: server-canonical post-/access, else the JWT claim. */
|
|
865
|
+
rawScope() {
|
|
866
|
+
return this.access?.scope ?? this.tokenScope;
|
|
867
|
+
}
|
|
868
|
+
pdfBits() {
|
|
869
|
+
return (0, _embedpdf_engine_core_runtime.decodePdfBits)(this.state.permissions.bits);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
757
872
|
* Identity of the current caller, or null when anonymous.
|
|
758
873
|
*
|
|
759
874
|
* Cloud-canonical post-/access; otherwise the identity claims from
|
|
@@ -810,9 +925,7 @@ var CloudDocumentSecurityService = class {
|
|
|
810
925
|
* and `applyCdnAccess` short-circuits to origin; safe to call always.
|
|
811
926
|
*/
|
|
812
927
|
async postAccess(signal, body) {
|
|
813
|
-
const response = await this.http.postJson(_embedpdf_engine_core_wire.wirePaths.access, {
|
|
814
|
-
docId: this.docId,
|
|
815
|
-
layerName: this.layerName,
|
|
928
|
+
const response = await this.http.postJson(_embedpdf_engine_core_wire.wirePaths.access(this.docId, this.layerName), {
|
|
816
929
|
...body.password ? { password: body.password } : {},
|
|
817
930
|
mode: body.mode
|
|
818
931
|
}, (raw) => _embedpdf_engine_core_wire.AccessResponseSchema.parse(raw), signal);
|
|
@@ -853,6 +966,18 @@ function safeDecodeClaims(token) {
|
|
|
853
966
|
return null;
|
|
854
967
|
}
|
|
855
968
|
}
|
|
969
|
+
/**
|
|
970
|
+
* CREATE's CollabTarget is the caller's own identity — the same
|
|
971
|
+
* derivation as engine-local's `ScopeGuard.targetForSelfCreate`, so
|
|
972
|
+
* `:self` trivially passes and `:group=X` matches the caller's
|
|
973
|
+
* default group.
|
|
974
|
+
*/
|
|
975
|
+
function selfTarget(id) {
|
|
976
|
+
return {
|
|
977
|
+
...id.user_id !== void 0 ? { userId: id.user_id } : {},
|
|
978
|
+
...id.group_id !== void 0 ? { groupId: id.group_id } : {}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
856
981
|
function identityFromClaims(claims) {
|
|
857
982
|
const out = {};
|
|
858
983
|
if (typeof claims["user_id"] === "string") out.user_id = claims["user_id"];
|
|
@@ -1342,6 +1467,12 @@ function auditRowToEvent(row, mySessionId) {
|
|
|
1342
1467
|
origin,
|
|
1343
1468
|
...row.payload
|
|
1344
1469
|
};
|
|
1470
|
+
case "pages.insert":
|
|
1471
|
+
case "pages.insertBlank": return {
|
|
1472
|
+
type: "pages.inserted",
|
|
1473
|
+
origin,
|
|
1474
|
+
...row.payload
|
|
1475
|
+
};
|
|
1345
1476
|
case "pages.flatten": return {
|
|
1346
1477
|
type: "pages.flattened",
|
|
1347
1478
|
origin,
|
|
@@ -1584,6 +1715,7 @@ var CloudDocumentHandle = class {
|
|
|
1584
1715
|
apply: (meta, owns) => this.absorbMutation(meta, owns),
|
|
1585
1716
|
applyPageStructure: (cache) => this.absorbPageStructure(cache),
|
|
1586
1717
|
applyPageDelete: (cache, deletedPages) => this.absorbPageDelete(cache, deletedPages),
|
|
1718
|
+
applyPageInsert: (cache) => this.absorbPageInsert(cache),
|
|
1587
1719
|
applyMetadata: (cache) => this.absorbMetadata(cache),
|
|
1588
1720
|
applyAttachments: (cache) => this.absorbAttachments(cache)
|
|
1589
1721
|
};
|
|
@@ -1692,6 +1824,7 @@ var CloudDocumentHandle = class {
|
|
|
1692
1824
|
this.manifestCache = {
|
|
1693
1825
|
...this.manifestCache,
|
|
1694
1826
|
docVersion: delta?.docVersion ?? this.manifestCache.docVersion,
|
|
1827
|
+
...delta?.annotationsVersion !== void 0 ? { annotationsVersion: delta.annotationsVersion } : {},
|
|
1695
1828
|
pages: Array.from(byPageObjectNumber.values()).sort((a, b) => a.state.pageObjectNumber - b.state.pageObjectNumber)
|
|
1696
1829
|
};
|
|
1697
1830
|
}
|
|
@@ -1749,6 +1882,27 @@ var CloudDocumentHandle = class {
|
|
|
1749
1882
|
};
|
|
1750
1883
|
}
|
|
1751
1884
|
/**
|
|
1885
|
+
* Patch bookkeeping after a page insert — the mirror of
|
|
1886
|
+
* {@link absorbPageDelete}, with one asymmetry: delete can patch the
|
|
1887
|
+
* cached manifest losslessly (it only REMOVES rows), but an insert needs
|
|
1888
|
+
* manifest rows for the fresh PONs and the result doesn't carry them. So
|
|
1889
|
+
* this absorb flips the planes (insert changes the page SET, so like
|
|
1890
|
+
* delete it owns content + annotations alongside layout), raises the
|
|
1891
|
+
* version floor, and DROPS the cache — the next read refetches a manifest
|
|
1892
|
+
* that includes the new pages' rows. The UI never waits on that refetch:
|
|
1893
|
+
* the mutation result / event already carries the full new layout.
|
|
1894
|
+
*/
|
|
1895
|
+
absorbPageInsert(cache) {
|
|
1896
|
+
this.flipScopes([
|
|
1897
|
+
"layout",
|
|
1898
|
+
"content",
|
|
1899
|
+
"annotations"
|
|
1900
|
+
]);
|
|
1901
|
+
this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
|
|
1902
|
+
this.inflightManifest = null;
|
|
1903
|
+
this.manifestCache = null;
|
|
1904
|
+
}
|
|
1905
|
+
/**
|
|
1752
1906
|
* Patch the cached manifest after a metadata write. Symmetric with
|
|
1753
1907
|
* {@link absorbPageMove}: a metadata edit advances `docVersion` (so leaf
|
|
1754
1908
|
* URLs re-resolve) and `metadataVersion` (so the /metadata leaf re-fetches),
|
|
@@ -1844,6 +1998,10 @@ var CloudDocumentHandle = class {
|
|
|
1844
1998
|
path: _embedpdf_engine_core_wire.wirePaths.layerEvents(this.id, this.layerName),
|
|
1845
1999
|
initialCursor: this.hub.lastServerId() ?? this.manifestCache?.auditHead ?? null,
|
|
1846
2000
|
onRow: (row) => {
|
|
2001
|
+
if (this.manifestCache && row.id > this.manifestCache.auditHead) this.manifestCache = {
|
|
2002
|
+
...this.manifestCache,
|
|
2003
|
+
auditHead: row.id
|
|
2004
|
+
};
|
|
1847
2005
|
const event = auditRowToEvent(row, this.sessionId);
|
|
1848
2006
|
if (!event) return;
|
|
1849
2007
|
this.absorbRemoteEvent(event);
|
|
@@ -1852,6 +2010,11 @@ var CloudDocumentHandle = class {
|
|
|
1852
2010
|
onFullRefresh: () => {
|
|
1853
2011
|
this.manifestCache = null;
|
|
1854
2012
|
this.inflightManifest = null;
|
|
2013
|
+
this.hub.publish({
|
|
2014
|
+
type: "stream.desynced",
|
|
2015
|
+
reason: "backlog-overflow",
|
|
2016
|
+
ts: Date.now()
|
|
2017
|
+
});
|
|
1855
2018
|
},
|
|
1856
2019
|
onAuthLost: () => {
|
|
1857
2020
|
this.sseClient = null;
|
|
@@ -1884,6 +2047,9 @@ var CloudDocumentHandle = class {
|
|
|
1884
2047
|
case "pages.deleted":
|
|
1885
2048
|
if (event.cache) this.absorbPageDelete(event.cache, event.pageObjectNumbers);
|
|
1886
2049
|
return;
|
|
2050
|
+
case "pages.inserted":
|
|
2051
|
+
if (event.cache) this.absorbPageInsert(event.cache);
|
|
2052
|
+
return;
|
|
1887
2053
|
case "metadata.updated":
|
|
1888
2054
|
if (event.cache) this.absorbMetadata(event.cache);
|
|
1889
2055
|
return;
|
|
@@ -1935,7 +2101,7 @@ function fallbackUnknownHead(id) {
|
|
|
1935
2101
|
access: {
|
|
1936
2102
|
required: true,
|
|
1937
2103
|
reasons: ["permissions-unknown"],
|
|
1938
|
-
endpoint: _embedpdf_engine_core_wire.wirePaths.access
|
|
2104
|
+
endpoint: _embedpdf_engine_core_wire.wirePaths.access(id, _embedpdf_engine_core_wire.DEFAULT_LAYER_NAME)
|
|
1939
2105
|
}
|
|
1940
2106
|
};
|
|
1941
2107
|
}
|
|
@@ -1956,19 +2122,117 @@ function awaitSignal(promise, signal) {
|
|
|
1956
2122
|
});
|
|
1957
2123
|
}
|
|
1958
2124
|
//#endregion
|
|
2125
|
+
//#region src/share.ts
|
|
2126
|
+
/**
|
|
2127
|
+
* Thrown on a non-OK exchange. `code` is the server's wire code:
|
|
2128
|
+
* `SharePasswordRequired` (prompt and retry with a password),
|
|
2129
|
+
* `OriginNotAllowed` / `OriginRequired`, `ShareExpired`, `NotFound`
|
|
2130
|
+
* (unknown or revoked — indistinguishable by design), or
|
|
2131
|
+
* `TooManyRequests`.
|
|
2132
|
+
*/
|
|
2133
|
+
var ShareExchangeError = class extends Error {
|
|
2134
|
+
constructor(code, message, status) {
|
|
2135
|
+
super(message);
|
|
2136
|
+
this.code = code;
|
|
2137
|
+
this.status = status;
|
|
2138
|
+
this.name = "ShareExchangeError";
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
async function exchangeShareToken(baseUrl, shareToken, opts = {}) {
|
|
2142
|
+
const res = await (opts.fetch ?? globalThis.fetch)(`${baseUrl.replace(/\/$/, "")}/v1/share-sessions`, {
|
|
2143
|
+
method: "POST",
|
|
2144
|
+
headers: { "content-type": "application/json" },
|
|
2145
|
+
body: JSON.stringify({
|
|
2146
|
+
shareToken,
|
|
2147
|
+
...opts.password !== void 0 ? { password: opts.password } : {}
|
|
2148
|
+
})
|
|
2149
|
+
});
|
|
2150
|
+
if (!res.ok) {
|
|
2151
|
+
let code = "Unknown";
|
|
2152
|
+
let message = `share exchange failed with status ${res.status}`;
|
|
2153
|
+
try {
|
|
2154
|
+
const payload = await res.json();
|
|
2155
|
+
if (payload.error?.code) code = payload.error.code;
|
|
2156
|
+
if (payload.error?.message) message = payload.error.message;
|
|
2157
|
+
} catch {}
|
|
2158
|
+
throw new ShareExchangeError(code, message, res.status);
|
|
2159
|
+
}
|
|
2160
|
+
return await res.json();
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Lift a {@link ShareExchangeError} into the engine's error taxonomy, so
|
|
2164
|
+
* `open({ kind: 'share' })` and every later renewal fail with the same
|
|
2165
|
+
* `EngineError` family as all other engine calls. The wire code and HTTP
|
|
2166
|
+
* status survive in `details`; the original error rides as `cause`.
|
|
2167
|
+
*
|
|
2168
|
+
* `SharePasswordRequired` gets its own engine code — it is the one outcome
|
|
2169
|
+
* callers branch on programmatically (prompt for the grant passphrase and
|
|
2170
|
+
* retry). Everything else maps by meaning, with the HTTP status as the
|
|
2171
|
+
* fallback discriminator.
|
|
2172
|
+
*/
|
|
2173
|
+
function engineErrorFromShareExchange(error) {
|
|
2174
|
+
return new _embedpdf_engine_core_runtime.EngineError(error.code === "SharePasswordRequired" ? _embedpdf_engine_core_runtime.EngineErrorCode.SharePasswordRequired : error.code === "NotFound" ? _embedpdf_engine_core_runtime.EngineErrorCode.NotFound : error.code === "ShareExpired" || error.code === "OriginNotAllowed" || error.code === "OriginRequired" ? _embedpdf_engine_core_runtime.EngineErrorCode.Forbidden : error.status === 401 ? _embedpdf_engine_core_runtime.EngineErrorCode.Unauthenticated : error.status === 400 ? _embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg : _embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `share exchange failed: ${error.message}`, {
|
|
2175
|
+
details: {
|
|
2176
|
+
shareCode: error.code,
|
|
2177
|
+
status: error.status
|
|
2178
|
+
},
|
|
2179
|
+
cause: error
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
/** Renew this many seconds before `expiresAt` so in-flight requests never race expiry. */
|
|
2183
|
+
const RENEW_MARGIN_SECONDS = 60;
|
|
2184
|
+
/**
|
|
2185
|
+
* A caching, self-renewing token source over the exchange — the callable
|
|
2186
|
+
* member of the engine's `TokenSource` union, assignable wherever one is
|
|
2187
|
+
* taken.
|
|
2188
|
+
* Concurrent callers share one in-flight exchange; a failed exchange
|
|
2189
|
+
* clears it so the next request retries rather than caching the error.
|
|
2190
|
+
*/
|
|
2191
|
+
function shareSessionSource(baseUrl, shareToken, opts = {}) {
|
|
2192
|
+
let session = null;
|
|
2193
|
+
let inflight = null;
|
|
2194
|
+
return async () => {
|
|
2195
|
+
if (session && session.expiresAt - Date.now() / 1e3 > RENEW_MARGIN_SECONDS) return session.token;
|
|
2196
|
+
inflight ?? (inflight = exchangeShareToken(baseUrl, shareToken, opts).then((next) => {
|
|
2197
|
+
session = next;
|
|
2198
|
+
return next.token;
|
|
2199
|
+
}).finally(() => {
|
|
2200
|
+
inflight = null;
|
|
2201
|
+
}));
|
|
2202
|
+
return inflight;
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
//#endregion
|
|
1959
2206
|
//#region src/transport/HttpClient.ts
|
|
1960
2207
|
var HttpClient = class HttpClient {
|
|
1961
2208
|
constructor(opts) {
|
|
1962
|
-
_defineProperty(this, "
|
|
2209
|
+
_defineProperty(this, "base", void 0);
|
|
1963
2210
|
_defineProperty(this, "tokenFn", void 0);
|
|
1964
2211
|
_defineProperty(this, "fetchFn", void 0);
|
|
2212
|
+
_defineProperty(this, "docAffinityHeader", void 0);
|
|
2213
|
+
_defineProperty(this, "onRetry", void 0);
|
|
1965
2214
|
_defineProperty(this, "sessionId", void 0);
|
|
1966
2215
|
_defineProperty(this, "cdnBinding", null);
|
|
1967
|
-
this.
|
|
2216
|
+
this.base = opts.baseUrl.replace(/\/$/, "");
|
|
1968
2217
|
const token = opts.token;
|
|
1969
2218
|
this.tokenFn = token === void 0 ? null : typeof token === "function" ? token : () => token;
|
|
1970
2219
|
this.sessionId = opts.sessionId ?? null;
|
|
1971
2220
|
this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2221
|
+
this.docAffinityHeader = opts.docAffinityHeader ?? true;
|
|
2222
|
+
this.onRetry = opts.onRetry;
|
|
2223
|
+
}
|
|
2224
|
+
/** Normalized server base URL (no trailing slash). */
|
|
2225
|
+
get baseUrl() {
|
|
2226
|
+
return this.base;
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* The fetch implementation this client sends requests through (injected
|
|
2230
|
+
* or global). Exposed so sibling transports spawned per-open — the
|
|
2231
|
+
* share-session exchange, most of all — ride the same fetch the engine
|
|
2232
|
+
* was configured with (test doubles, SSR `Origin`-attaching wrappers).
|
|
2233
|
+
*/
|
|
2234
|
+
get fetchImpl() {
|
|
2235
|
+
return this.fetchFn;
|
|
1972
2236
|
}
|
|
1973
2237
|
/**
|
|
1974
2238
|
* Return a clone of this client bound to a different bearer. Used
|
|
@@ -2186,6 +2450,12 @@ var HttpClient = class HttpClient {
|
|
|
2186
2450
|
const res = await this.requestJson(path, "POST", body, signal);
|
|
2187
2451
|
return await this.parseJsonResponse(res, parser);
|
|
2188
2452
|
}
|
|
2453
|
+
/** POST a JSON body and return the raw binary response (pages.extract). */
|
|
2454
|
+
async postJsonBytes(path, body, signal) {
|
|
2455
|
+
const res = await this.requestJson(path, "POST", body, signal);
|
|
2456
|
+
if (!res.ok) await this.throwFromBody(res);
|
|
2457
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
2458
|
+
}
|
|
2189
2459
|
/** POST a raw binary body (FDF/XFDF import) and parse the JSON response. */
|
|
2190
2460
|
async postBytesJson(path, bytes, contentType, parser, signal) {
|
|
2191
2461
|
const headers = new Headers({ "Content-Type": contentType });
|
|
@@ -2232,7 +2502,7 @@ var HttpClient = class HttpClient {
|
|
|
2232
2502
|
});
|
|
2233
2503
|
}
|
|
2234
2504
|
async request(path, init) {
|
|
2235
|
-
const { url, extraHeader } = this.resolveOutgoing(path);
|
|
2505
|
+
const { url, extraHeader, routedToCdn } = this.resolveOutgoing(path);
|
|
2236
2506
|
const headers = new Headers(init.headers ?? {});
|
|
2237
2507
|
if (this.tokenFn) {
|
|
2238
2508
|
const token = await this.tokenFn();
|
|
@@ -2241,11 +2511,32 @@ var HttpClient = class HttpClient {
|
|
|
2241
2511
|
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2242
2512
|
if (this.sessionId) headers.set("X-Engine-Session-Id", this.sessionId);
|
|
2243
2513
|
if (extraHeader) headers.set(extraHeader.name, extraHeader.value);
|
|
2244
|
-
|
|
2245
|
-
|
|
2514
|
+
if (this.docAffinityHeader && !routedToCdn) {
|
|
2515
|
+
const doc = /^\/v1\/docs\/([^/]+)\//.exec(path);
|
|
2516
|
+
if (doc) headers.set("X-CloudPDF-Doc", decodeURIComponent(doc[1]));
|
|
2517
|
+
}
|
|
2518
|
+
for (let attempt = 0;; attempt++) {
|
|
2519
|
+
const res = await this.execute(url, {
|
|
2246
2520
|
...init,
|
|
2247
2521
|
headers
|
|
2248
2522
|
});
|
|
2523
|
+
if (res.status !== 503 || attempt >= HttpClient.MAX_RETRIES || isStreamBody(init.body ?? null)) return res;
|
|
2524
|
+
const code = await peekErrorCode(res);
|
|
2525
|
+
if (code === null || !RETRYABLE_503.has(code)) return res;
|
|
2526
|
+
const hinted = Number(res.headers.get("retry-after"));
|
|
2527
|
+
const waitMs = (Number.isFinite(hinted) ? Math.min(Math.max(hinted, 0) * 1e3, HttpClient.RETRY_AFTER_CAP_MS) : HttpClient.RETRY_AFTER_DEFAULT_MS) * (.9 + .2 * Math.random());
|
|
2528
|
+
this.onRetry?.({
|
|
2529
|
+
path,
|
|
2530
|
+
code,
|
|
2531
|
+
attempt,
|
|
2532
|
+
waitMs
|
|
2533
|
+
});
|
|
2534
|
+
await sleepRacingAbort(waitMs, init.signal ?? null);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
async execute(url, init) {
|
|
2538
|
+
try {
|
|
2539
|
+
return await this.fetchFn(url, init);
|
|
2249
2540
|
} catch (err) {
|
|
2250
2541
|
if (init.signal?.aborted) throw new _embedpdf_engine_core_runtime.AbortError(init.signal.reason ?? err);
|
|
2251
2542
|
if (err?.name === "AbortError") throw new _embedpdf_engine_core_runtime.AbortError(err.message);
|
|
@@ -2271,7 +2562,8 @@ var HttpClient = class HttpClient {
|
|
|
2271
2562
|
resolveOutgoing(path) {
|
|
2272
2563
|
if (!this.cdnBinding || !this.cdnBinding.cdn) return {
|
|
2273
2564
|
url: `${this.baseUrl}${path}`,
|
|
2274
|
-
extraHeader: null
|
|
2565
|
+
extraHeader: null,
|
|
2566
|
+
routedToCdn: false
|
|
2275
2567
|
};
|
|
2276
2568
|
const applied = (0, _embedpdf_engine_core_wire.applyCdnAccess)({
|
|
2277
2569
|
path,
|
|
@@ -2282,7 +2574,8 @@ var HttpClient = class HttpClient {
|
|
|
2282
2574
|
});
|
|
2283
2575
|
return {
|
|
2284
2576
|
url: applied.url,
|
|
2285
|
-
extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null
|
|
2577
|
+
extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null,
|
|
2578
|
+
routedToCdn: applied.routedToCdn
|
|
2286
2579
|
};
|
|
2287
2580
|
}
|
|
2288
2581
|
async throwFromBody(res) {
|
|
@@ -2302,6 +2595,9 @@ var HttpClient = class HttpClient {
|
|
|
2302
2595
|
throw new _embedpdf_engine_core_runtime.EngineError(code, `HTTP ${res.status}`, { details: { status: res.status } });
|
|
2303
2596
|
}
|
|
2304
2597
|
};
|
|
2598
|
+
_defineProperty(HttpClient, "MAX_RETRIES", 2);
|
|
2599
|
+
_defineProperty(HttpClient, "RETRY_AFTER_CAP_MS", 5e3);
|
|
2600
|
+
_defineProperty(HttpClient, "RETRY_AFTER_DEFAULT_MS", 1e3);
|
|
2305
2601
|
function mapStatusToCode(status) {
|
|
2306
2602
|
if (status === 401) return _embedpdf_engine_core_runtime.EngineErrorCode.Unauthenticated;
|
|
2307
2603
|
if (status === 403) return _embedpdf_engine_core_runtime.EngineErrorCode.Forbidden;
|
|
@@ -2316,6 +2612,36 @@ function trySerialized(value) {
|
|
|
2316
2612
|
const result = _embedpdf_engine_core_wire.EngineErrorPayloadSchema.safeParse(value);
|
|
2317
2613
|
return result.success ? result.data : null;
|
|
2318
2614
|
}
|
|
2615
|
+
const RETRYABLE_503 = /* @__PURE__ */ new Set(["EngineBusy", "EngineRestarting"]);
|
|
2616
|
+
function isStreamBody(body) {
|
|
2617
|
+
return typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
|
|
2618
|
+
}
|
|
2619
|
+
/** Read the error code from a 503 body without consuming the response. */
|
|
2620
|
+
async function peekErrorCode(res) {
|
|
2621
|
+
try {
|
|
2622
|
+
const code = (await res.clone().json())?.error?.code;
|
|
2623
|
+
return typeof code === "string" ? code : null;
|
|
2624
|
+
} catch {
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
function sleepRacingAbort(ms, signal) {
|
|
2629
|
+
return new Promise((resolve, reject) => {
|
|
2630
|
+
if (signal?.aborted) {
|
|
2631
|
+
reject(new _embedpdf_engine_core_runtime.AbortError(signal.reason ?? "aborted"));
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const timer = setTimeout(() => {
|
|
2635
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2636
|
+
resolve();
|
|
2637
|
+
}, ms);
|
|
2638
|
+
const onAbort = () => {
|
|
2639
|
+
clearTimeout(timer);
|
|
2640
|
+
reject(new _embedpdf_engine_core_runtime.AbortError(signal?.reason ?? "aborted"));
|
|
2641
|
+
};
|
|
2642
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2319
2645
|
//#endregion
|
|
2320
2646
|
//#region src/CloudEngine.ts
|
|
2321
2647
|
/**
|
|
@@ -2341,6 +2667,24 @@ var CloudEngine = class CloudEngine {
|
|
|
2341
2667
|
options?.scope;
|
|
2342
2668
|
options?.identity;
|
|
2343
2669
|
const effectivePassword = options && "password" in options ? options.password ?? null : input.password ?? null;
|
|
2670
|
+
if (input.kind === "share") {
|
|
2671
|
+
const raw = shareSessionSource(this.http.baseUrl, input.shareToken, {
|
|
2672
|
+
...input.sharePassword !== void 0 ? { password: input.sharePassword } : {},
|
|
2673
|
+
fetch: this.http.fetchImpl
|
|
2674
|
+
});
|
|
2675
|
+
const token = async () => {
|
|
2676
|
+
try {
|
|
2677
|
+
return await raw();
|
|
2678
|
+
} catch (error) {
|
|
2679
|
+
throw error instanceof ShareExchangeError ? engineErrorFromShareExchange(error) : error;
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
return this.open({
|
|
2683
|
+
kind: "token",
|
|
2684
|
+
token,
|
|
2685
|
+
password: input.password ?? null
|
|
2686
|
+
}, options);
|
|
2687
|
+
}
|
|
2344
2688
|
if (input.kind === "token") {
|
|
2345
2689
|
const tokenSource = input.token;
|
|
2346
2690
|
return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
|
|
@@ -2377,7 +2721,7 @@ var CloudEngine = class CloudEngine {
|
|
|
2377
2721
|
return handle;
|
|
2378
2722
|
});
|
|
2379
2723
|
}
|
|
2380
|
-
return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `cloud engine supports OpenInput.kind === 'token' or '
|
|
2724
|
+
return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `cloud engine supports OpenInput.kind === 'token', 'id', or 'share' (got '${input.kind}')`));
|
|
2381
2725
|
}
|
|
2382
2726
|
destroy() {
|
|
2383
2727
|
if (this.destroyed) return _embedpdf_engine_core_runtime.AbortablePromise.resolveValue(void 0);
|
|
@@ -2506,8 +2850,12 @@ Object.defineProperty(exports, "EngineErrorCode", {
|
|
|
2506
2850
|
}
|
|
2507
2851
|
});
|
|
2508
2852
|
exports.HttpClient = HttpClient;
|
|
2853
|
+
exports.ShareExchangeError = ShareExchangeError;
|
|
2509
2854
|
exports.cloudEngine = cloudEngine;
|
|
2510
2855
|
exports.createCloudEngine = createCloudEngine;
|
|
2511
2856
|
exports.decodeUnverifiedClaims = decodeUnverifiedClaims;
|
|
2857
|
+
exports.engineErrorFromShareExchange = engineErrorFromShareExchange;
|
|
2858
|
+
exports.exchangeShareToken = exchangeShareToken;
|
|
2859
|
+
exports.shareSessionSource = shareSessionSource;
|
|
2512
2860
|
|
|
2513
2861
|
//# sourceMappingURL=index.cjs.map
|