@xcitedbs/client 0.2.21 → 0.2.24

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/client.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.XCiteDBClient = void 0;
4
4
  const types_1 = require("./types");
5
5
  const websocket_1 = require("./websocket");
6
+ const assetUri_1 = require("./assetUri");
6
7
  function joinUrl(base, path) {
7
8
  const b = base.replace(/\/+$/, '');
8
9
  const p = path.startsWith('/') ? path : `/${path}`;
@@ -56,6 +57,24 @@ function buildQuery(params) {
56
57
  const s = sp.toString();
57
58
  return s ? `?${s}` : '';
58
59
  }
60
+ /** Best-effort filename from `Content-Disposition` (attachment; filename="…"). */
61
+ function parseContentDispositionFilename(cd) {
62
+ if (!cd)
63
+ return undefined;
64
+ const mStar = cd.match(/filename\*=(?:UTF-8'')?([^;]+)/i);
65
+ if (mStar?.[1]) {
66
+ try {
67
+ return decodeURIComponent(mStar[1].trim().replace(/^"|"$/g, ''));
68
+ }
69
+ catch {
70
+ return mStar[1].trim().replace(/^"|"$/g, '');
71
+ }
72
+ }
73
+ const m = cd.match(/filename\s*=\s*("?)([^";]+)\1/i);
74
+ if (m?.[2])
75
+ return m[2].trim();
76
+ return undefined;
77
+ }
59
78
  function warnIfHttpOnTlsPort(baseUrl) {
60
79
  try {
61
80
  const u = new URL(baseUrl);
@@ -590,6 +609,122 @@ class XCiteDBClient {
590
609
  this.notifySessionInvalidIfNeeded(path, 401);
591
610
  throw new types_1.XCiteDBError('Request failed after retry', 401, null);
592
611
  }
612
+ /**
613
+ * POST `multipart/form-data` without forcing JSON `Content-Type` (boundary is set by the runtime).
614
+ */
615
+ async requestFormPost(path, form, opts) {
616
+ const no401Retry = opts?.no401Retry === true;
617
+ const suppressTestSessionHeader = opts?.suppressTestSessionHeader === true;
618
+ for (let attempt = 0; attempt < 2; attempt++) {
619
+ const url = joinUrl(this.baseUrl, path);
620
+ const outgoingRequestId = newClientRequestId();
621
+ const headers = {
622
+ ...this.authHeaders(),
623
+ ...this.contextHeaders(),
624
+ ...(suppressTestSessionHeader ? {} : this.testHeaders()),
625
+ 'X-Request-Id': outgoingRequestId,
626
+ };
627
+ const init = { method: 'POST', headers, body: form };
628
+ const sig = requestTimeoutSignal(this.requestTimeoutMs);
629
+ if (sig)
630
+ init.signal = sig;
631
+ let res;
632
+ try {
633
+ res = await fetch(url, init);
634
+ }
635
+ catch (e) {
636
+ const m = e instanceof Error ? e.message : 'Network error';
637
+ throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
638
+ }
639
+ const text = await res.text();
640
+ let data;
641
+ try {
642
+ data = text ? JSON.parse(text) : null;
643
+ }
644
+ catch {
645
+ data = text;
646
+ }
647
+ if (res.ok) {
648
+ return data;
649
+ }
650
+ if (res.status === 401 &&
651
+ attempt === 0 &&
652
+ !no401Retry &&
653
+ (await this.tryRefreshSessionAfter401())) {
654
+ continue;
655
+ }
656
+ const msg = typeof data === 'object' && data !== null && 'message' in data
657
+ ? String(data.message)
658
+ : res.statusText;
659
+ this.notifySessionInvalidIfNeeded(path, res.status);
660
+ throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
661
+ }
662
+ this.notifySessionInvalidIfNeeded(path, 401);
663
+ throw new types_1.XCiteDBError('Request failed after retry', 401, null);
664
+ }
665
+ /** GET binary response (e.g. document export); errors are parsed as JSON when possible. */
666
+ async requestBinaryGet(path, opts) {
667
+ const no401Retry = opts?.no401Retry === true;
668
+ const suppressTestSessionHeader = opts?.suppressTestSessionHeader === true;
669
+ for (let attempt = 0; attempt < 2; attempt++) {
670
+ const url = joinUrl(this.baseUrl, path);
671
+ const outgoingRequestId = newClientRequestId();
672
+ const headers = {
673
+ ...this.authHeaders(),
674
+ ...this.contextHeaders(),
675
+ ...(suppressTestSessionHeader ? {} : this.testHeaders()),
676
+ 'X-Request-Id': outgoingRequestId,
677
+ };
678
+ const init = { method: 'GET', headers };
679
+ const sig = requestTimeoutSignal(this.requestTimeoutMs);
680
+ if (sig)
681
+ init.signal = sig;
682
+ let res;
683
+ try {
684
+ res = await fetch(url, init);
685
+ }
686
+ catch (e) {
687
+ const m = e instanceof Error ? e.message : 'Network error';
688
+ throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
689
+ }
690
+ if (res.ok) {
691
+ const buf = new Uint8Array(await res.arrayBuffer());
692
+ const contentType = res.headers.get('Content-Type') ?? 'application/octet-stream';
693
+ const fn = parseContentDispositionFilename(res.headers.get('Content-Disposition')) ?? 'export.bin';
694
+ const rt = res.headers.get('X-XciteDB-Export-RoundTrip');
695
+ const roundTrip = rt === 'exact' || rt === 'lossy' ? rt : undefined;
696
+ const w = res.headers.get('X-XciteDB-Export-Warning');
697
+ return {
698
+ bytes: buf,
699
+ contentType,
700
+ filename: fn,
701
+ ...(roundTrip ? { roundTrip } : {}),
702
+ ...(w ? { warning: w } : {}),
703
+ };
704
+ }
705
+ if (res.status === 401 &&
706
+ attempt === 0 &&
707
+ !no401Retry &&
708
+ (await this.tryRefreshSessionAfter401())) {
709
+ continue;
710
+ }
711
+ const text = await res.text();
712
+ let data;
713
+ try {
714
+ data = text ? JSON.parse(text) : null;
715
+ }
716
+ catch {
717
+ data = text;
718
+ }
719
+ const msg = typeof data === 'object' && data !== null && 'message' in data
720
+ ? String(data.message)
721
+ : res.statusText;
722
+ this.notifySessionInvalidIfNeeded(path, res.status);
723
+ throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
724
+ }
725
+ this.notifySessionInvalidIfNeeded(path, 401);
726
+ throw new types_1.XCiteDBError('Request failed after retry', 401, null);
727
+ }
593
728
  /** Developer Bearer refresh first, then app-user refresh (no API key refresh). */
594
729
  async tryRefreshSessionAfter401() {
595
730
  if (this.accessToken && this.refreshToken) {
@@ -1080,6 +1215,64 @@ class XCiteDBClient {
1080
1215
  mode: params.mode,
1081
1216
  });
1082
1217
  }
1218
+ /** Share an asset path via prefix alias (`POST /api/v1/security/user-isolation/asset-shares`). */
1219
+ async createAssetShare(params) {
1220
+ const body = {
1221
+ target_user_id: params.target_user_id,
1222
+ mode: params.mode,
1223
+ };
1224
+ if (params.source_uri !== undefined && params.source_uri !== '') {
1225
+ body.source_uri = params.source_uri;
1226
+ }
1227
+ else if (params.identifier !== undefined && params.identifier !== '') {
1228
+ body.identifier = this.isoPrefixId(params.identifier);
1229
+ }
1230
+ else {
1231
+ throw new types_1.XCiteDBError('createAssetShare requires identifier or source_uri', 400, null);
1232
+ }
1233
+ return this.request('POST', '/api/v1/security/user-isolation/asset-shares', body);
1234
+ }
1235
+ /** Remove an asset share (`DELETE /api/v1/security/user-isolation/asset-shares`). */
1236
+ async deleteAssetShare(params) {
1237
+ const body = {};
1238
+ if (params.source_uri !== undefined && params.source_uri !== '') {
1239
+ body.source_uri = params.source_uri;
1240
+ }
1241
+ else if (params.identifier !== undefined && params.identifier !== '') {
1242
+ body.identifier = this.isoPrefixId(params.identifier);
1243
+ }
1244
+ else {
1245
+ throw new types_1.XCiteDBError('deleteAssetShare requires identifier or source_uri', 400, null);
1246
+ }
1247
+ if (params.target_user_id !== undefined) {
1248
+ body.target_user_id = params.target_user_id;
1249
+ }
1250
+ if (params.sharer_user_id !== undefined) {
1251
+ body.sharer_user_id = params.sharer_user_id;
1252
+ }
1253
+ await this.request('DELETE', '/api/v1/security/user-isolation/asset-shares', body);
1254
+ }
1255
+ /** List incoming/public asset share aliases (`GET /api/v1/security/user-isolation/asset-shares`). */
1256
+ async listAssetShares(opts) {
1257
+ const q = buildQuery({
1258
+ direction: opts?.direction ?? 'incoming',
1259
+ ...(opts?.scope ? { scope: opts.scope } : {}),
1260
+ });
1261
+ return this.request('GET', `/api/v1/security/user-isolation/asset-shares${q}`);
1262
+ }
1263
+ /** Issue a time-limited magic link for one asset identifier (`POST /api/v1/security/asset-magic-links`). */
1264
+ async createAssetMagicLink(body) {
1265
+ return this.request('POST', '/api/v1/security/asset-magic-links', body);
1266
+ }
1267
+ /** List issued magic links (no secrets) (`GET /api/v1/security/asset-magic-links`). */
1268
+ async listAssetMagicLinks() {
1269
+ return this.request('GET', '/api/v1/security/asset-magic-links');
1270
+ }
1271
+ /** Revoke a magic link (`DELETE /api/v1/security/asset-magic-links/{token_id}`). */
1272
+ async revokeAssetMagicLink(tokenId) {
1273
+ const id = encodeURIComponent(tokenId.trim());
1274
+ await this.request('DELETE', `/api/v1/security/asset-magic-links/${id}`);
1275
+ }
1083
1276
  async createWorkspace(name, fromBranch, fromDate) {
1084
1277
  const body = { name };
1085
1278
  if (fromBranch)
@@ -1221,16 +1414,35 @@ class XCiteDBClient {
1221
1414
  async deleteTag(name) {
1222
1415
  return this.deleteBookmark(name);
1223
1416
  }
1224
- async compare(from, to, includeContent) {
1225
- return this.request('POST', '/api/v1/compare', {
1417
+ async compare(from, to, third) {
1418
+ let include_content = false;
1419
+ let match_start;
1420
+ if (typeof third === 'boolean') {
1421
+ include_content = third;
1422
+ }
1423
+ else if (third !== undefined && typeof third === 'object') {
1424
+ include_content = third.includeContent ?? false;
1425
+ match_start = third.matchStart;
1426
+ }
1427
+ const body = {
1226
1428
  from,
1227
1429
  to,
1228
- include_content: includeContent ?? false,
1229
- });
1430
+ include_content,
1431
+ };
1432
+ if (match_start !== undefined && match_start !== '') {
1433
+ body.match_start = this.isoPrefixId(match_start);
1434
+ }
1435
+ const r = await this.request('POST', '/api/v1/compare', body);
1436
+ if (r && typeof r === 'object' && typeof r.match_start === 'string' && r.match_start.length > 0) {
1437
+ r.match_start = this.isoUnprefixId(r.match_start);
1438
+ }
1439
+ return r;
1230
1440
  }
1231
- /** @deprecated Use {@link compare}. */
1232
- async diff(from, to, includeContent) {
1233
- return this.compare(from, to, includeContent);
1441
+ async diff(from, to, third) {
1442
+ if (third === undefined || typeof third === 'boolean') {
1443
+ return this.compare(from, to, third);
1444
+ }
1445
+ return this.compare(from, to, third);
1234
1446
  }
1235
1447
  async publishWorkspace(targetWorkspace, sourceWorkspace, options) {
1236
1448
  const body = {
@@ -1277,6 +1489,14 @@ class XCiteDBClient {
1277
1489
  body.message = options.message;
1278
1490
  return this.request('POST', `/api/v1/user-workspaces/${encodeURIComponent(id)}/publish`, body);
1279
1491
  }
1492
+ /**
1493
+ * Advance the user workspace fork to the current parent tip (`POST /api/v1/user-workspaces/{id}/rebase`).
1494
+ * On overlap with local edits, returns **409** with `conflicts` (same shape as publish) unless `autoResolve` resolves it.
1495
+ */
1496
+ async rebaseUserWorkspace(id, options) {
1497
+ const body = { auto_resolve: options?.autoResolve ?? 'none' };
1498
+ return this.request('POST', `/api/v1/user-workspaces/${encodeURIComponent(id)}/rebase`, body);
1499
+ }
1280
1500
  /**
1281
1501
  * Create `workspaceName` from {@link options.fromBranch} (or current context), run `fn` scoped to that workspace,
1282
1502
  * create a checkpoint, then publish back unless {@link options.autoMerge} is `false`.
@@ -1354,6 +1574,49 @@ class XCiteDBClient {
1354
1574
  }
1355
1575
  return r;
1356
1576
  }
1577
+ /**
1578
+ * Import a document (`POST /api/v1/documents/import`). Server converts DOCX, ODF, RTF, PDF, Markdown, AsciiDoc, or plain text
1579
+ * to shredded XML. Field name must be `file` (multipart).
1580
+ */
1581
+ async importDocument(file, options) {
1582
+ const form = new FormData();
1583
+ const blob = file instanceof Blob
1584
+ ? file
1585
+ : file instanceof ArrayBuffer
1586
+ ? new Blob([file], { type: 'application/octet-stream' })
1587
+ : new Blob([Uint8Array.from(file)], { type: 'application/octet-stream' });
1588
+ const uploadName = options?.filename ??
1589
+ (typeof File !== 'undefined' && file instanceof File ? file.name : 'upload.bin');
1590
+ form.append('file', blob, uploadName);
1591
+ const q = buildQuery({
1592
+ ...(options?.identifier !== undefined && options.identifier !== ''
1593
+ ? { identifier: this.isoPrefixId(options.identifier) }
1594
+ : {}),
1595
+ });
1596
+ const r = await this.requestFormPost(`/api/v1/documents/import${q}`, form);
1597
+ if (r && typeof r === 'object' && 'identifier' in r && typeof r.identifier === 'string') {
1598
+ r.identifier = this.isoUnprefixId(r.identifier);
1599
+ }
1600
+ return r;
1601
+ }
1602
+ /**
1603
+ * Export a document (`GET /api/v1/documents/export`). Returns binary bytes and response metadata.
1604
+ */
1605
+ async exportDocument(identifier, format = 'docx', options) {
1606
+ const q = buildQuery({
1607
+ identifier: this.isoPrefixId(identifier),
1608
+ format,
1609
+ strict: options?.strict ? '1' : undefined,
1610
+ });
1611
+ const r = await this.requestBinaryGet(`/api/v1/documents/export${q}`);
1612
+ return {
1613
+ bytes: r.bytes,
1614
+ contentType: r.contentType,
1615
+ filename: r.filename,
1616
+ ...(r.roundTrip ? { roundTrip: r.roundTrip } : {}),
1617
+ ...(r.warning ? { warning: r.warning } : {}),
1618
+ };
1619
+ }
1357
1620
  /**
1358
1621
  * @deprecated Use {@link writeXmlDocument}. This name was misleading: it writes **XML** via a JSON wrapper, not a JSON document.
1359
1622
  */
@@ -1370,6 +1633,26 @@ class XCiteDBClient {
1370
1633
  const rows = await this.request('GET', `/api/v1/documents/by-id${q}`);
1371
1634
  return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
1372
1635
  }
1636
+ /**
1637
+ * Shallow read: root element with inline leaves plus `db:N*` placeholders for shredded child slots
1638
+ * (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`). For sidebar / AST
1639
+ * navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
1640
+ */
1641
+ async queryByIdentifierShallow(identifier, filter, pathFilter) {
1642
+ return this.queryByIdentifier(identifier, 'NoChildren,KeepIndexNodes,FirstMatch', filter, pathFilter);
1643
+ }
1644
+ /**
1645
+ * Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
1646
+ * Use this for editor round-trips. For navigation without loading the full subtree, use
1647
+ * {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
1648
+ */
1649
+ async queryByIdentifierFull(identifier, filter, pathFilter) {
1650
+ return this.queryByIdentifier(identifier, 'FirstMatch,IncludeChildren', filter, pathFilter);
1651
+ }
1652
+ /** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
1653
+ async listChildIdentifiers(parentPath) {
1654
+ return this.listIdentifierChildren(parentPath);
1655
+ }
1373
1656
  async queryDocuments(query, flags, filter, pathFilter) {
1374
1657
  const pq = this.isoPrefixQuery(query);
1375
1658
  const params = {
@@ -1727,6 +2010,260 @@ class XCiteDBClient {
1727
2010
  async deleteProjectDocConf() {
1728
2011
  return this.request('DELETE', '/api/v1/project/settings/doc-conf');
1729
2012
  }
2013
+ /** Admin: asset storage v2 (`GET /api/v1/project/settings/asset-storage`). */
2014
+ async getProjectAssetStorage() {
2015
+ return this.request('GET', '/api/v1/project/settings/asset-storage');
2016
+ }
2017
+ /** Admin: save asset storage v2 (`PUT /api/v1/project/settings/asset-storage`). */
2018
+ async updateProjectAssetStorage(body) {
2019
+ return this.request('PUT', '/api/v1/project/settings/asset-storage', body);
2020
+ }
2021
+ resolveAssetIdentifier(uriOrIdentifier) {
2022
+ const trimmed = uriOrIdentifier.trim();
2023
+ const fromUri = (0, assetUri_1.parseAssetUri)(trimmed);
2024
+ if (fromUri) {
2025
+ return this.canonicalId(fromUri);
2026
+ }
2027
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {
2028
+ return `/assets/${trimmed.toLowerCase()}`;
2029
+ }
2030
+ return this.canonicalId(trimmed);
2031
+ }
2032
+ /** URL path under `/api/v1/assets/…` with per-segment encoding. */
2033
+ assetRequestPath(uriOrIdentifier) {
2034
+ const id = this.resolveAssetIdentifier(uriOrIdentifier);
2035
+ const segments = id.split('/').filter((s) => s.length > 0).map((s) => encodeURIComponent(s));
2036
+ return `/api/v1/assets/${segments.join('/')}`;
2037
+ }
2038
+ assetQuerySuffix(opts) {
2039
+ const ml = opts?.ml?.trim();
2040
+ if (!ml) {
2041
+ return '';
2042
+ }
2043
+ return `?ml=${encodeURIComponent(ml)}`;
2044
+ }
2045
+ /**
2046
+ * Upload raw bytes (`POST /api/v1/assets`). Returns canonical `identifier` and `xcitedb:asset:…` `uri`.
2047
+ */
2048
+ async uploadAsset(bytes, opts) {
2049
+ let bodyBytes;
2050
+ if (bytes instanceof Blob) {
2051
+ bodyBytes = await bytes.arrayBuffer();
2052
+ }
2053
+ else if (bytes instanceof ArrayBuffer) {
2054
+ bodyBytes = bytes;
2055
+ }
2056
+ else {
2057
+ bodyBytes = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2058
+ }
2059
+ const ct = opts.contentType.trim() || 'application/octet-stream';
2060
+ const qParts = [];
2061
+ if (opts.scope === 'public') {
2062
+ qParts.push('scope=public');
2063
+ }
2064
+ if (opts.identifier !== undefined && opts.identifier !== '') {
2065
+ qParts.push(`identifier=${encodeURIComponent(opts.identifier)}`);
2066
+ }
2067
+ const q = qParts.length ? `?${qParts.join('&')}` : '';
2068
+ const no401Retry = false;
2069
+ const suppressTestSessionHeader = false;
2070
+ for (let attempt = 0; attempt < 2; attempt++) {
2071
+ const url = joinUrl(this.baseUrl, `/api/v1/assets${q}`);
2072
+ const outgoingRequestId = newClientRequestId();
2073
+ const headers = {
2074
+ ...this.authHeaders(),
2075
+ ...this.contextHeaders(),
2076
+ ...(suppressTestSessionHeader ? {} : this.testHeaders()),
2077
+ 'X-Request-Id': outgoingRequestId,
2078
+ 'Content-Type': ct,
2079
+ };
2080
+ const init = { method: 'POST', headers, body: bodyBytes };
2081
+ const sig = requestTimeoutSignal(this.requestTimeoutMs);
2082
+ if (sig)
2083
+ init.signal = sig;
2084
+ let res;
2085
+ try {
2086
+ res = await fetch(url, init);
2087
+ }
2088
+ catch (e) {
2089
+ const m = e instanceof Error ? e.message : 'Network error';
2090
+ throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
2091
+ }
2092
+ const text = await res.text();
2093
+ let data;
2094
+ try {
2095
+ data = text ? JSON.parse(text) : null;
2096
+ }
2097
+ catch {
2098
+ data = text;
2099
+ }
2100
+ if (res.ok) {
2101
+ if (!data || typeof data !== 'object') {
2102
+ throw new types_1.XCiteDBError('Invalid asset upload response', res.status, data);
2103
+ }
2104
+ const o = data;
2105
+ const identifier = typeof o.identifier === 'string' ? o.identifier : '';
2106
+ const uri = typeof o.uri === 'string' ? o.uri : '';
2107
+ if (!identifier || !uri) {
2108
+ throw new types_1.XCiteDBError('Invalid asset upload response', res.status, data);
2109
+ }
2110
+ const out = { identifier, uri };
2111
+ if (typeof o.target_name === 'string')
2112
+ out.target_name = o.target_name;
2113
+ if (typeof o.content_type === 'string')
2114
+ out.content_type = o.content_type;
2115
+ if (typeof o.size === 'number')
2116
+ out.size = o.size;
2117
+ if (typeof o.sha256 === 'string')
2118
+ out.sha256 = o.sha256;
2119
+ if (typeof o.etag === 'string')
2120
+ out.etag = o.etag;
2121
+ return out;
2122
+ }
2123
+ if (res.status === 401 &&
2124
+ attempt === 0 &&
2125
+ !no401Retry &&
2126
+ (await this.tryRefreshSessionAfter401())) {
2127
+ continue;
2128
+ }
2129
+ const msg = typeof data === 'object' && data !== null && 'message' in data
2130
+ ? String(data.message)
2131
+ : res.statusText;
2132
+ this.notifySessionInvalidIfNeeded('/api/v1/assets', res.status);
2133
+ throwForFailedHttp(res.status, '/api/v1/assets', data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
2134
+ }
2135
+ this.notifySessionInvalidIfNeeded('/api/v1/assets', 401);
2136
+ throw new types_1.XCiteDBError('Request failed after retry', 401, null);
2137
+ }
2138
+ /**
2139
+ * Download asset bytes (`GET /api/v1/assets/…`). Follows `302` to presigned S3 URLs.
2140
+ * Pass `ml` for magic-link access (same token as `Authorization: MagicLink …` when not using cookies).
2141
+ */
2142
+ async getAsset(uriOrIdentifier, opts) {
2143
+ const path = `${this.assetRequestPath(uriOrIdentifier)}${this.assetQuerySuffix({ ml: opts?.ml })}`;
2144
+ const no401Retry = false;
2145
+ const suppressTestSessionHeader = false;
2146
+ for (let attempt = 0; attempt < 2; attempt++) {
2147
+ const url = joinUrl(this.baseUrl, path);
2148
+ const outgoingRequestId = newClientRequestId();
2149
+ const headers = {
2150
+ ...this.authHeaders(),
2151
+ ...this.contextHeaders(),
2152
+ ...(suppressTestSessionHeader ? {} : this.testHeaders()),
2153
+ 'X-Request-Id': outgoingRequestId,
2154
+ };
2155
+ if (opts?.ifNoneMatch) {
2156
+ headers['If-None-Match'] = opts.ifNoneMatch;
2157
+ }
2158
+ const init = { method: 'GET', headers, redirect: 'follow' };
2159
+ const sig = requestTimeoutSignal(this.requestTimeoutMs);
2160
+ if (sig)
2161
+ init.signal = sig;
2162
+ let res;
2163
+ try {
2164
+ res = await fetch(url, init);
2165
+ }
2166
+ catch (e) {
2167
+ const m = e instanceof Error ? e.message : 'Network error';
2168
+ throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
2169
+ }
2170
+ if (res.status === 304) {
2171
+ return new Blob();
2172
+ }
2173
+ if (res.ok) {
2174
+ return await res.blob();
2175
+ }
2176
+ if (res.status === 401 &&
2177
+ attempt === 0 &&
2178
+ !no401Retry &&
2179
+ (await this.tryRefreshSessionAfter401())) {
2180
+ continue;
2181
+ }
2182
+ const text = await res.text();
2183
+ let data;
2184
+ try {
2185
+ data = text ? JSON.parse(text) : null;
2186
+ }
2187
+ catch {
2188
+ data = text;
2189
+ }
2190
+ const msg = typeof data === 'object' && data !== null && 'message' in data
2191
+ ? String(data.message)
2192
+ : res.statusText;
2193
+ this.notifySessionInvalidIfNeeded(path, res.status);
2194
+ throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
2195
+ }
2196
+ this.notifySessionInvalidIfNeeded(path, 401);
2197
+ throw new types_1.XCiteDBError('Request failed after retry', 401, null);
2198
+ }
2199
+ /** Delete an asset (`DELETE /api/v1/assets/…`). */
2200
+ async deleteAsset(uriOrIdentifier, opts) {
2201
+ const path = `${this.assetRequestPath(uriOrIdentifier)}${this.assetQuerySuffix({ ml: opts?.ml })}`;
2202
+ await this.request('DELETE', path);
2203
+ }
2204
+ /** `HEAD /api/v1/assets/…` — metadata only. */
2205
+ async headAsset(uriOrIdentifier, opts) {
2206
+ const path = `${this.assetRequestPath(uriOrIdentifier)}${this.assetQuerySuffix({ ml: opts?.ml })}`;
2207
+ const no401Retry = false;
2208
+ const suppressTestSessionHeader = false;
2209
+ for (let attempt = 0; attempt < 2; attempt++) {
2210
+ const url = joinUrl(this.baseUrl, path);
2211
+ const outgoingRequestId = newClientRequestId();
2212
+ const headers = {
2213
+ ...this.authHeaders(),
2214
+ ...this.contextHeaders(),
2215
+ ...(suppressTestSessionHeader ? {} : this.testHeaders()),
2216
+ 'X-Request-Id': outgoingRequestId,
2217
+ };
2218
+ const init = { method: 'HEAD', headers };
2219
+ const sig = requestTimeoutSignal(this.requestTimeoutMs);
2220
+ if (sig)
2221
+ init.signal = sig;
2222
+ let res;
2223
+ try {
2224
+ res = await fetch(url, init);
2225
+ }
2226
+ catch (e) {
2227
+ const m = e instanceof Error ? e.message : 'Network error';
2228
+ throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
2229
+ }
2230
+ if (res.ok) {
2231
+ const etagRaw = res.headers.get('ETag') ?? undefined;
2232
+ const ct = res.headers.get('Content-Type') ?? 'application/octet-stream';
2233
+ const len = res.headers.get('Content-Length');
2234
+ const size = len ? parseInt(len, 10) : 0;
2235
+ return { contentType: ct, size: Number.isFinite(size) ? size : 0, etag: etagRaw };
2236
+ }
2237
+ if (res.status === 401 &&
2238
+ attempt === 0 &&
2239
+ !no401Retry &&
2240
+ (await this.tryRefreshSessionAfter401())) {
2241
+ continue;
2242
+ }
2243
+ const text = await res.text();
2244
+ let data;
2245
+ try {
2246
+ data = text ? JSON.parse(text) : null;
2247
+ }
2248
+ catch {
2249
+ data = text;
2250
+ }
2251
+ const msg = typeof data === 'object' && data !== null && 'message' in data
2252
+ ? String(data.message)
2253
+ : res.statusText;
2254
+ this.notifySessionInvalidIfNeeded(path, res.status);
2255
+ throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
2256
+ }
2257
+ this.notifySessionInvalidIfNeeded(path, 401);
2258
+ throw new types_1.XCiteDBError('Request failed after retry', 401, null);
2259
+ }
2260
+ /** Admin: GC dry-run — manifest vs live meta references (`POST /api/v1/admin/assets/gc/dry-run`). */
2261
+ async adminAssetsGcDryRun(opts) {
2262
+ const q = opts?.ownerUserId && opts.ownerUserId.trim()
2263
+ ? `?owner=${encodeURIComponent(opts.ownerUserId.trim())}`
2264
+ : '';
2265
+ return this.request('POST', `/api/v1/admin/assets/gc/dry-run${q}`, {});
2266
+ }
1730
2267
  /** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
1731
2268
  async getPlatformDefaultDocConf() {
1732
2269
  return this.request('GET', '/api/v1/platform/default-doc-conf');
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { XCiteDBClient } from './client';
2
+ export { parseAssetUri, formatAssetUri, collectIdentifiersFromText, ASSET_URI_PREFIX } from './assetUri';
2
3
  export { WebSocketSubscription } from './websocket';
3
- export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, DocumentBatchResponse, DocumentBatchResultRow, Flags, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
+ export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
5
  export { XCiteDBError, XCiteDBForbiddenError, XCiteDBNotFoundError, XCiteDBAuthError, XCiteDBLockConflictError, } from './types';
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.XCiteDBLockConflictError = exports.XCiteDBAuthError = exports.XCiteDBNotFoundError = exports.XCiteDBForbiddenError = exports.XCiteDBError = exports.WebSocketSubscription = exports.XCiteDBClient = void 0;
3
+ exports.XCiteDBLockConflictError = exports.XCiteDBAuthError = exports.XCiteDBNotFoundError = exports.XCiteDBForbiddenError = exports.XCiteDBError = exports.WebSocketSubscription = exports.ASSET_URI_PREFIX = exports.collectIdentifiersFromText = exports.formatAssetUri = exports.parseAssetUri = exports.XCiteDBClient = void 0;
4
4
  var client_1 = require("./client");
5
5
  Object.defineProperty(exports, "XCiteDBClient", { enumerable: true, get: function () { return client_1.XCiteDBClient; } });
6
+ var assetUri_1 = require("./assetUri");
7
+ Object.defineProperty(exports, "parseAssetUri", { enumerable: true, get: function () { return assetUri_1.parseAssetUri; } });
8
+ Object.defineProperty(exports, "formatAssetUri", { enumerable: true, get: function () { return assetUri_1.formatAssetUri; } });
9
+ Object.defineProperty(exports, "collectIdentifiersFromText", { enumerable: true, get: function () { return assetUri_1.collectIdentifiersFromText; } });
10
+ Object.defineProperty(exports, "ASSET_URI_PREFIX", { enumerable: true, get: function () { return assetUri_1.ASSET_URI_PREFIX; } });
6
11
  var websocket_1 = require("./websocket");
7
12
  Object.defineProperty(exports, "WebSocketSubscription", { enumerable: true, get: function () { return websocket_1.WebSocketSubscription; } });
8
13
  var types_1 = require("./types");