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