@cloudpdf/engine 3.0.0-next.0 → 3.0.0-next.10

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 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
- const manifest = await this.manifest.get(signal);
92
- return { pages: await Promise.all(manifest.pages.map((page) => this.readCurrentPage(page.state.pageObjectNumber, signal))) };
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.readCurrentPage(pageObjectNumber, signal));
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, "baseUrl", void 0);
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.baseUrl = opts.baseUrl.replace(/\/$/, "");
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
- try {
2245
- return await this.fetchFn(url, {
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 'id' (got '${input.kind}')`));
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