@xcitedbs/client 0.2.23 → 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.
@@ -0,0 +1,14 @@
1
+ /** URI prefix for asset identifiers embedded in documents (`xcitedb:asset:/path/...`). */
2
+ export declare const ASSET_URI_PREFIX: "xcitedb:asset:";
3
+ /**
4
+ * Parses `xcitedb:asset:<identifier>` where identifier must start with `/`.
5
+ * Percent-decodes the path portion then normalizes slashes.
6
+ */
7
+ export declare function parseAssetUri(s: string): string | null;
8
+ /** Build `xcitedb:asset:<identifier>`; identifier must begin with `/` after normalization. */
9
+ export declare function formatAssetUri(identifier: string): string;
10
+ /**
11
+ * Scan arbitrary text for `xcitedb:asset:` paths (GC / reference discovery).
12
+ * Paths are normalized the same way as {@link parseAssetUri} would accept.
13
+ */
14
+ export declare function collectIdentifiersFromText(text: string, out: Set<string>): void;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ASSET_URI_PREFIX = void 0;
4
+ exports.parseAssetUri = parseAssetUri;
5
+ exports.formatAssetUri = formatAssetUri;
6
+ exports.collectIdentifiersFromText = collectIdentifiersFromText;
7
+ /** URI prefix for asset identifiers embedded in documents (`xcitedb:asset:/path/...`). */
8
+ exports.ASSET_URI_PREFIX = 'xcitedb:asset:';
9
+ function isPchar(c) {
10
+ if ((c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39)) {
11
+ return true;
12
+ }
13
+ switch (c) {
14
+ case 0x2d: // -
15
+ case 0x2e: // .
16
+ case 0x5f: // _
17
+ case 0x7e: // ~
18
+ case 0x25: // %
19
+ case 0x3a: // :
20
+ case 0x40: // @
21
+ case 0x21: // !
22
+ case 0x24: // $
23
+ case 0x26: // &
24
+ case 0x27: // '
25
+ case 0x28: // (
26
+ case 0x29: // )
27
+ case 0x2a: // *
28
+ case 0x2b: // +
29
+ case 0x2c: // ,
30
+ case 0x3b: // ;
31
+ case 0x3d: // =
32
+ return true;
33
+ default:
34
+ return false;
35
+ }
36
+ }
37
+ /** Normalize path: collapse slashes, ensure leading `/`. */
38
+ function normalizeIdentifierPath(p) {
39
+ let s = p.trim().replace(/\/+/g, '/');
40
+ if (!s.startsWith('/')) {
41
+ s = `/${s}`;
42
+ }
43
+ return s;
44
+ }
45
+ /**
46
+ * Parses `xcitedb:asset:<identifier>` where identifier must start with `/`.
47
+ * Percent-decodes the path portion then normalizes slashes.
48
+ */
49
+ function parseAssetUri(s) {
50
+ const t = s.trim();
51
+ if (!t.startsWith(exports.ASSET_URI_PREFIX)) {
52
+ return null;
53
+ }
54
+ let rest = t.slice(exports.ASSET_URI_PREFIX.length);
55
+ if (!rest.startsWith('/')) {
56
+ return null;
57
+ }
58
+ try {
59
+ rest = decodeURIComponent(rest);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ const id = normalizeIdentifierPath(rest);
65
+ if (id === '/' || id.length < 2) {
66
+ return null;
67
+ }
68
+ return id;
69
+ }
70
+ /** Build `xcitedb:asset:<identifier>`; identifier must begin with `/` after normalization. */
71
+ function formatAssetUri(identifier) {
72
+ const id = normalizeIdentifierPath(identifier);
73
+ if (id === '/' || !id.startsWith('/')) {
74
+ throw new Error('formatAssetUri: identifier must be a non-root absolute path');
75
+ }
76
+ return `${exports.ASSET_URI_PREFIX}${id}`;
77
+ }
78
+ /**
79
+ * Scan arbitrary text for `xcitedb:asset:` paths (GC / reference discovery).
80
+ * Paths are normalized the same way as {@link parseAssetUri} would accept.
81
+ */
82
+ function collectIdentifiersFromText(text, out) {
83
+ const p = exports.ASSET_URI_PREFIX;
84
+ let pos = 0;
85
+ while (pos < text.length) {
86
+ const f = text.indexOf(p, pos);
87
+ if (f < 0) {
88
+ break;
89
+ }
90
+ let i = f + p.length;
91
+ if (i >= text.length || text.charCodeAt(i) !== 0x2f) {
92
+ pos = f + 1;
93
+ continue;
94
+ }
95
+ let j = i;
96
+ while (j < text.length) {
97
+ const c = text.charCodeAt(j);
98
+ if (c === 0x2f || isPchar(c)) {
99
+ j += 1;
100
+ continue;
101
+ }
102
+ break;
103
+ }
104
+ if (j > i) {
105
+ const raw = text.slice(i, j);
106
+ try {
107
+ const decoded = decodeURIComponent(raw);
108
+ const id = normalizeIdentifierPath(decoded);
109
+ if (id.length > 1) {
110
+ out.add(id);
111
+ }
112
+ }
113
+ catch {
114
+ // skip malformed percent-encoding
115
+ }
116
+ }
117
+ pos = j;
118
+ }
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const assetUri_1 = require("./assetUri");
9
+ (0, node_test_1.default)('parseAssetUri accepts xcitedb:asset path', () => {
10
+ strict_1.default.equal((0, assetUri_1.parseAssetUri)(`${assetUri_1.ASSET_URI_PREFIX}/users/u1/assets/a.png`), '/users/u1/assets/a.png');
11
+ });
12
+ (0, node_test_1.default)('formatAssetUri round-trip', () => {
13
+ const id = '/assets/photos/x';
14
+ strict_1.default.equal((0, assetUri_1.parseAssetUri)((0, assetUri_1.formatAssetUri)(id)), id);
15
+ });
16
+ (0, node_test_1.default)('collectIdentifiersFromText', () => {
17
+ const s = new Set();
18
+ (0, assetUri_1.collectIdentifiersFromText)(`see ${assetUri_1.ASSET_URI_PREFIX}/a/b and done`, s);
19
+ strict_1.default.ok(s.has('/a/b'));
20
+ });
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult } from './types';
1
+ import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult } from './types';
2
2
  import { WebSocketSubscription } from './websocket';
3
3
  export declare class XCiteDBClient {
4
4
  private baseUrl;
@@ -327,6 +327,21 @@ export declare class XCiteDBClient {
327
327
  * When {@link XCiteDBClient.enableUserIsolation} / `userIsolation` is active, `identifier` is prefixed like other document APIs.
328
328
  */
329
329
  createUserIsolationShare(params: UserIsolationCreateShareParams): Promise<UserIsolationShareResult>;
330
+ /** Share an asset path via prefix alias (`POST /api/v1/security/user-isolation/asset-shares`). */
331
+ createAssetShare(params: AssetShareRequest): Promise<UserIsolationShareResult>;
332
+ /** Remove an asset share (`DELETE /api/v1/security/user-isolation/asset-shares`). */
333
+ deleteAssetShare(params: AssetUnshareRequest): Promise<void>;
334
+ /** List incoming/public asset share aliases (`GET /api/v1/security/user-isolation/asset-shares`). */
335
+ listAssetShares(opts?: {
336
+ direction?: 'incoming' | 'outgoing' | 'public';
337
+ scope?: string;
338
+ }): Promise<AssetShareListResponse>;
339
+ /** Issue a time-limited magic link for one asset identifier (`POST /api/v1/security/asset-magic-links`). */
340
+ createAssetMagicLink(body: CreateAssetMagicLinkRequest): Promise<AssetMagicLinkResult>;
341
+ /** List issued magic links (no secrets) (`GET /api/v1/security/asset-magic-links`). */
342
+ listAssetMagicLinks(): Promise<AssetMagicLinkListResponse>;
343
+ /** Revoke a magic link (`DELETE /api/v1/security/asset-magic-links/{token_id}`). */
344
+ revokeAssetMagicLink(tokenId: string): Promise<void>;
330
345
  createWorkspace(name: string, fromBranch?: string, fromDate?: string): Promise<void>;
331
346
  /** @deprecated Use {@link createWorkspace}. */
332
347
  createBranch(name: string, fromBranch?: string, fromDate?: string): Promise<void>;
@@ -520,19 +535,18 @@ export declare class XCiteDBClient {
520
535
  queryByIdentifier(identifier: string, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
521
536
  /**
522
537
  * Shallow read: root element with inline leaves plus `db:N*` placeholders for shredded child slots
523
- * (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`). Use with {@link listChildIdentifiers}
524
- * or {@link queryByIdentifierWithChildren} for sidebar / AST navigation without loading the full subtree.
538
+ * (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`). For sidebar / AST
539
+ * navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
525
540
  */
526
541
  queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string): Promise<string[]>;
527
- /** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
528
- listChildIdentifiers(parentPath?: string): Promise<ListIdentifierChildrenResult>;
529
542
  /**
530
- * Parallel shallow node + one level of identifier children (`GET /by-id` + `GET /identifier-children`).
543
+ * Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
544
+ * Use this for editor round-trips. For navigation without loading the full subtree, use
545
+ * {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
531
546
  */
532
- queryByIdentifierWithChildren(identifier: string, filter?: string, pathFilter?: string): Promise<{
533
- node: string[];
534
- children: IdentifierChildNode[];
535
- }>;
547
+ queryByIdentifierFull(identifier: string, filter?: string, pathFilter?: string): Promise<string[]>;
548
+ /** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
549
+ listChildIdentifiers(parentPath?: string): Promise<ListIdentifierChildrenResult>;
536
550
  queryDocuments(query: XCiteQuery, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
537
551
  deleteDocument(identifier: string): Promise<void>;
538
552
  addIdentifier(identifier: string): Promise<boolean>;
@@ -636,6 +650,38 @@ export declare class XCiteDBClient {
636
650
  }): Promise<ProjectDocConfResponse>;
637
651
  /** Remove project override; server uses platform default (`DELETE /api/v1/project/settings/doc-conf`). */
638
652
  deleteProjectDocConf(): Promise<ProjectDocConfResponse>;
653
+ /** Admin: asset storage v2 (`GET /api/v1/project/settings/asset-storage`). */
654
+ getProjectAssetStorage(): Promise<ProjectAssetStorageConfig>;
655
+ /** Admin: save asset storage v2 (`PUT /api/v1/project/settings/asset-storage`). */
656
+ updateProjectAssetStorage(body: ProjectAssetStorageConfig): Promise<ProjectAssetStorageConfig>;
657
+ private resolveAssetIdentifier;
658
+ /** URL path under `/api/v1/assets/…` with per-segment encoding. */
659
+ private assetRequestPath;
660
+ private assetQuerySuffix;
661
+ /**
662
+ * Upload raw bytes (`POST /api/v1/assets`). Returns canonical `identifier` and `xcitedb:asset:…` `uri`.
663
+ */
664
+ uploadAsset(bytes: Blob | ArrayBuffer | Uint8Array, opts: UploadAssetOptions): Promise<AssetUploadResult>;
665
+ /**
666
+ * Download asset bytes (`GET /api/v1/assets/…`). Follows `302` to presigned S3 URLs.
667
+ * Pass `ml` for magic-link access (same token as `Authorization: MagicLink …` when not using cookies).
668
+ */
669
+ getAsset(uriOrIdentifier: string, opts?: {
670
+ ifNoneMatch?: string;
671
+ ml?: string;
672
+ }): Promise<Blob>;
673
+ /** Delete an asset (`DELETE /api/v1/assets/…`). */
674
+ deleteAsset(uriOrIdentifier: string, opts?: {
675
+ ml?: string;
676
+ }): Promise<void>;
677
+ /** `HEAD /api/v1/assets/…` — metadata only. */
678
+ headAsset(uriOrIdentifier: string, opts?: {
679
+ ml?: string;
680
+ }): Promise<AssetHeadResult>;
681
+ /** Admin: GC dry-run — manifest vs live meta references (`POST /api/v1/admin/assets/gc/dry-run`). */
682
+ adminAssetsGcDryRun(opts?: {
683
+ ownerUserId?: string;
684
+ }): Promise<AssetGcDryRunResult>;
639
685
  /** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
640
686
  getPlatformDefaultDocConf(): Promise<PlatformDefaultDocConfResponse>;
641
687
  /** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
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}`;
@@ -1214,6 +1215,64 @@ class XCiteDBClient {
1214
1215
  mode: params.mode,
1215
1216
  });
1216
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
+ }
1217
1276
  async createWorkspace(name, fromBranch, fromDate) {
1218
1277
  const body = { name };
1219
1278
  if (fromBranch)
@@ -1576,26 +1635,24 @@ class XCiteDBClient {
1576
1635
  }
1577
1636
  /**
1578
1637
  * Shallow read: root element with inline leaves plus `db:N*` placeholders for shredded child slots
1579
- * (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`). Use with {@link listChildIdentifiers}
1580
- * or {@link queryByIdentifierWithChildren} for sidebar / AST navigation without loading the full subtree.
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).
1581
1640
  */
1582
1641
  async queryByIdentifierShallow(identifier, filter, pathFilter) {
1583
1642
  return this.queryByIdentifier(identifier, 'NoChildren,KeepIndexNodes,FirstMatch', filter, pathFilter);
1584
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
+ }
1585
1652
  /** Alias of {@link listIdentifierChildren} — immediate child segments under `parentPath` (identifier hierarchy). */
1586
1653
  async listChildIdentifiers(parentPath) {
1587
1654
  return this.listIdentifierChildren(parentPath);
1588
1655
  }
1589
- /**
1590
- * Parallel shallow node + one level of identifier children (`GET /by-id` + `GET /identifier-children`).
1591
- */
1592
- async queryByIdentifierWithChildren(identifier, filter, pathFilter) {
1593
- const [node, listed] = await Promise.all([
1594
- this.queryByIdentifierShallow(identifier, filter, pathFilter),
1595
- this.listIdentifierChildren(identifier),
1596
- ]);
1597
- return { node, children: listed.children };
1598
- }
1599
1656
  async queryDocuments(query, flags, filter, pathFilter) {
1600
1657
  const pq = this.isoPrefixQuery(query);
1601
1658
  const params = {
@@ -1953,6 +2010,260 @@ class XCiteDBClient {
1953
2010
  async deleteProjectDocConf() {
1954
2011
  return this.request('DELETE', '/api/v1/project/settings/doc-conf');
1955
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
+ }
1956
2267
  /** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
1957
2268
  async getPlatformDefaultDocConf() {
1958
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, 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, 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
+ 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");
package/dist/types.d.ts CHANGED
@@ -170,6 +170,125 @@ export interface ProjectDocConfResponse {
170
170
  has_project_override: boolean;
171
171
  doc_conf_text: string | null;
172
172
  }
173
+ /** `POST /api/v1/assets` success body (`201`). */
174
+ export interface AssetUploadResult {
175
+ identifier: string;
176
+ uri: string;
177
+ target_name?: string;
178
+ content_type?: string;
179
+ size?: number;
180
+ sha256?: string;
181
+ etag?: string;
182
+ }
183
+ /** Options for {@link XCiteDBClient.uploadAsset}. */
184
+ export interface UploadAssetOptions {
185
+ contentType: string;
186
+ /** When set (with default scope), uploaded to this path after user-isolation prefixing rules on the server. */
187
+ identifier?: string;
188
+ /** `public` writes under `/public/assets/…`. Default project/user scope when omitted. */
189
+ scope?: 'project' | 'public';
190
+ }
191
+ /** Result of {@link XCiteDBClient.headAsset}. */
192
+ export interface AssetHeadResult {
193
+ contentType: string;
194
+ size: number;
195
+ etag?: string;
196
+ }
197
+ /** `POST /api/v1/admin/assets/gc/dry-run` (admin). */
198
+ export interface AssetGcDryRunResult {
199
+ manifest_count: number;
200
+ referenced_asset_count: number;
201
+ orphan_count: number;
202
+ orphan_sample: string[];
203
+ }
204
+ export type AssetStorageTargetType = 'internal' | 's3';
205
+ /** One named storage target in `asset_storage_v2` (`GET/PUT /api/v1/project/settings/asset-storage`). */
206
+ export interface AssetStorageTarget {
207
+ type: AssetStorageTargetType;
208
+ writable?: boolean;
209
+ s3_endpoint_url?: string;
210
+ s3_bucket?: string;
211
+ s3_region?: string;
212
+ access_key_id?: string;
213
+ /** On GET, redacted as `***` when set. */
214
+ secret_access_key?: string;
215
+ small_file_threshold_bytes?: number;
216
+ redirect_threshold_bytes?: number;
217
+ import_head_cache_ttl_seconds?: number;
218
+ [key: string]: unknown;
219
+ }
220
+ export interface AssetStorageMount {
221
+ path: string;
222
+ target: string;
223
+ key_template: string;
224
+ }
225
+ export interface AssetStorageImport {
226
+ path: string;
227
+ target: string;
228
+ key: string;
229
+ tree?: boolean;
230
+ content_type?: string;
231
+ }
232
+ /** Project asset storage v2 (`GET/PUT /api/v1/project/settings/asset-storage`). */
233
+ export interface ProjectAssetStorageConfig {
234
+ targets: Record<string, AssetStorageTarget>;
235
+ mounts: AssetStorageMount[];
236
+ imports: AssetStorageImport[];
237
+ }
238
+ /** Body for `POST /api/v1/security/user-isolation/asset-shares`. */
239
+ export interface AssetShareRequest {
240
+ /** Canonical or app-relative asset path (subject to user-isolation prefixing when enabled). */
241
+ identifier?: string;
242
+ /** `xcitedb:asset:/…` or raw `/…` path; not prefixed by the client. */
243
+ source_uri?: string;
244
+ target_user_id: string;
245
+ mode: UserIsolationShareMode;
246
+ }
247
+ /** Body for `DELETE /api/v1/security/user-isolation/asset-shares`. */
248
+ export interface AssetUnshareRequest {
249
+ identifier?: string;
250
+ source_uri?: string;
251
+ target_user_id?: string;
252
+ sharer_user_id?: string;
253
+ }
254
+ export interface AssetShareListEntry {
255
+ identifier: string;
256
+ mode: string;
257
+ }
258
+ /** `GET /api/v1/security/user-isolation/asset-shares` */
259
+ export interface AssetShareListResponse {
260
+ direction: string;
261
+ identifiers: AssetShareListEntry[];
262
+ scope?: string;
263
+ note?: string;
264
+ }
265
+ /** `POST /api/v1/security/asset-magic-links` */
266
+ export interface CreateAssetMagicLinkRequest {
267
+ source_uri: string;
268
+ actions: ('read' | 'write')[];
269
+ expires_in_seconds: number;
270
+ max_uses?: number;
271
+ }
272
+ export interface AssetMagicLinkResult {
273
+ token: string;
274
+ token_id?: string;
275
+ expires_at: string;
276
+ link_url: string;
277
+ }
278
+ export interface AssetMagicLinkRecord {
279
+ token_id: string;
280
+ identifier: string;
281
+ actions?: unknown;
282
+ issued_by_user_id?: string;
283
+ issued_at?: string;
284
+ expires_at?: string;
285
+ max_uses?: number;
286
+ uses?: number;
287
+ revoked?: boolean;
288
+ }
289
+ export interface AssetMagicLinkListResponse {
290
+ tokens: AssetMagicLinkRecord[];
291
+ }
173
292
  /** `GET /api/v1/platform/default-doc-conf` */
174
293
  export interface PlatformDefaultDocConfResponse {
175
294
  doc_conf_text: string;
@@ -429,6 +548,11 @@ export interface DocumentBatchResultRow {
429
548
  index: number;
430
549
  identifier: string;
431
550
  ok: boolean;
551
+ /**
552
+ * When `ok` is false, server batch precheck / write reason. Includes:
553
+ * `duplicate_identifier_in_batch` (409), `parent_inlines_batch_member` (409),
554
+ * `lock_conflict` (423), and other parse/policy strings.
555
+ */
432
556
  error?: string;
433
557
  code?: number;
434
558
  /** Present when `error === 'lock_conflict'` (HTTP-style code 423 in batch row). */
package/llms-full.txt CHANGED
@@ -12,7 +12,7 @@ Before reading the full reference, note these critical differences from typical
12
12
 
13
13
  2. **Identifiers are hierarchical, path-like strings** — e.g. `/us/bills/hr1`, `/manual/v2/chapter3`. They are NOT auto-generated UUIDs. The leading `/` is part of the identifier. Parent/child relationships are derived from the path structure.
14
14
 
15
- 3. **Documents are XML or JSON, not arbitrary blobs.** XML documents are the primary document type. JSON documents are a parallel store. Both are fully versioned.
15
+ 3. **Documents are XML or JSON, not arbitrary blobs.** XML documents are the primary document type. JSON documents are a parallel store. Both are fully versioned. **Binary files** use the separate **assets** subsystem (not document shredding): canonical paths like `/assets/<uuid>`, stable **`xcitedb:asset:/…`** URIs, **`POST` / `GET` / `HEAD` / `DELETE /api/v1/assets/…`**, project **`asset-storage` v2** config, optional user-isolation **asset-shares**, and **asset magic links** (`?ml=`).
16
16
 
17
17
  4. **Context (workspace + date) travels as HTTP headers** (`X-Workspace` preferred, `X-Branch` alias, `X-Date`, and optionally `X-Unversioned` for explicit flat writes), not URL path segments.
18
18
 
@@ -28,7 +28,9 @@ Before reading the full reference, note these critical differences from typical
28
28
 
29
29
  10. **Ephemeral test sessions.** `POST /api/v1/test/sessions` (authenticated) returns a UUID **`session_token`**. Clients send **`X-Test-Session: <token>`** on API calls to use an isolated, TTL- and quota-limited LMDB instead of production project data. Unless **`X-Test-Auth: required`** is set, **developer** JWT/API-key checks are bypassed (synthetic admin for wet tests), but **app-user** identity via **`X-App-User-Token`** or Bearer app-user JWT is still recognized. Management routes under **`/api/v1/test/*`** must not include `X-Test-Session`. With a default body (omit or `{}`), the test LMDB starts **empty** (no cloned production project config).
30
30
 
31
- 11. **Overlay test sessions.** Same **`POST`**, with JSON **`{"overlay":true}`**, while authenticated for the **project to debug** (project-scoped API key, or platform Bearer + **`X-Project-Id`**). The session metadata records overlay mode; subsequent requests need only **`X-Test-Session`**. The server opens **`XCiteDB(_test/<uuid>/data, <production data path>)`**: production is used as a **read-only base**; reads merge overlay + base; **writes never modify production**. If the production data directory is missing, opening the session database fails. JS **`createTestSession({ …, overlay: true })`**, C++ **`test_session_overlay`** + **`create_test_session`**, MCP **`create_test_session`** tool **`overlay: true`**.
31
+ 11. **Overlay test sessions.** Same **`POST`**, with JSON **`{"overlay":true}`**, while authenticated for the **project to debug** (project-scoped API key, or platform Bearer + **`X-Project-Id`**). The session metadata records overlay mode; subsequent requests need only **`X-Test-Session`**. The server opens **`XCiteDB(_test/<uuid>/data, <production data path>)`**: production is used as a **read-only base**; reads merge overlay + base; **writes never modify production**. If the production data directory is missing, opening the session database fails. JS **`createTestSession({ …, overlay: true })`**, C++ **`test_session_overlay`** + **`create_test_session`**, MCP **`create_test_session`** tool **`overlay: true`**. **Locks:** cooperative lock metadata is read from the **base** layer as well as the overlay, but lock acquire/release only affects the overlay—so **`lock_conflict`** during overlay tests can reflect a lock still held in production; use **`findLocks` / `forceReleaseLock`** on your paths or a non-overlay session when testing locks.
32
+
33
+ 12. **SDK asset helpers.** JavaScript: `uploadAsset`, `getAsset`, `headAsset`, `deleteAsset`, `resolveAssetIdentifier`, `formatAssetUri`, `collectIdentifiersFromText`, plus `createAssetShare` / `createAssetMagicLink` and related APIs. Python: `upload_asset`, `get_asset`, `head_asset`, `xcitedb.asset_uri`. C++: `xcitedb/asset_uri.hpp`, `XCiteDBClient::upload_asset`, `get_asset_raw`, `head_asset_raw`, `delete_asset`, and security helpers mirroring the other SDKs.
32
34
 
33
35
  ## Choosing the Right Versioning Approach
34
36
 
@@ -63,6 +65,8 @@ Legacy REST paths under `/api/v1/branches`, `/commits`, `/tags`, `/diff` remain
63
65
 
64
66
  7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real LMDB under `_test/<uuid>/` (empty by default, or **overlay** on read-only production with **`{"overlay":true}`** / **`overlay: true`** / **`test_session_overlay`**). See "Ephemeral test sessions" below.
65
67
 
68
+ 8. **Identifier strings (minting).** The engine rejects identifiers containing byte **`0x01`**. The API canonicalizes hierarchical paths (**leading `/`**, **no trailing `/`**). Reserved namespaces include **`/_xcitedb/`**, **`/assets/`**, **`/public/assets/`**, and **`/public/shared/<tenant>/assets/`**. **`identifier_hierarchy_max_depth`** (default **4**) tunes hierarchy indexing; deeper paths still work but **`GET …/identifier-children`** may scan.
69
+
66
70
  ---
67
71
 
68
72
  # Part 1: Product Overview
@@ -438,12 +442,53 @@ Query parameters: `identifier` (required), `flags` (optional: `FirstMatch`, `Inc
438
442
 
439
443
  - **`flags=NoChildren,KeepIndexNodes,FirstMatch`**: returns the matching node with inline leaf content plus **`db:N1`**, **`db:N2`**, … placeholders for shredded child slots (avoids loading the full deep subtree in one response).
440
444
  - Combine with **`GET /api/v1/documents/identifier-children?parent_path=…`** for the next level of hierarchy (`segment`, `full_path`, `is_identifier`, `has_children`).
441
- - **JavaScript SDK:** `queryByIdentifierShallow(id)`, `listChildIdentifiers(parentPath?)`, `queryByIdentifierWithChildren(id)` (shallow XML + children in parallel).
445
+ - **JavaScript SDK:** `queryByIdentifierFull(id)` loads **`FirstMatch,IncludeChildren`** for editor round-trips; **`queryByIdentifierShallow(id)`** + **`listChildIdentifiers(id)`** (e.g. `Promise.all` client-side) for sidebar / tree expansion.
442
446
 
443
447
  ### Standalone subtree write (shredded model)
444
448
 
445
449
  You may `POST /api/v1/documents` with a root element whose `db:identifier` is a nested path (e.g. `/spaces/u/docs/doc1/sec-1`) without re-posting the parent document: the engine writes that subtree only and updates the identifier hierarchy index for the parent automatically. Keep `is_top: true` (default) on the write.
446
450
 
451
+ ### XML shredding semantics (must read)
452
+
453
+ - **Auto-shred.** Any element with **`db:identifier`** is stored as its own LMDB row. The parent’s stored XML keeps a **`<db:N…/>`** placeholder for that slot. There is **no** size threshold and **no** opt-out.
454
+ - **`is_top` is a marker only.** It registers a **`TOP:<xcitepath>`** alias for tooling; it does **not** change shredding, merge, or overwrite behavior.
455
+ - **Batch ordering.** **`POST /api/v1/documents/batch`** processes **`items[]`** **sequentially**. Each item’s XML is shredded so **every** `db:identifier` in that payload becomes (or overwrites) its own row. Precheck returns per-row **`409`**: **`duplicate_identifier_in_batch`** when the same canonical root id appears in more than one item (the **later** row is rejected), and **`parent_inlines_batch_member`** when one item’s XML still contains **full** markup for another item’s **root** id in the same batch (prevents silently overwriting a fresher child row with a stale parent).
456
+ - **Save the smallest dirty subtree.** Do not include live descendant XML for ids you also flush as standalone batch rows; use placeholders from a shallow read when you must touch the parent in the same request.
457
+
458
+ **Round-trip:** Element structure, child order, and text are preserved. The server may add **`db:order`**, **`db:xcitepath`**, **`xmlns:db`**. Attribute order and insignificant whitespace are **not** byte-stable—compare by DOM, not raw bytes.
459
+
460
+ ### Batch XML writes
461
+
462
+ **`POST /api/v1/documents/batch`** — JSON body **`{ "items": [ { "xml": "<…>", "is_top": true, "compare_attributes": false, "identifier": "/optional/match" }, … ] }`**. Response **`200`** with **`{ "results": [ { "index", "ok", "identifier", "error"?, "code"?, "current_lock"? } ] }`** (best-effort: later rows still run when earlier rows fail). JavaScript: **`writeXmlDocumentsBatch`**. Python: **`write_xml_documents_batch`**. C++: **`write_xml_documents_batch`**.
463
+
464
+ ### Minimal SPA editor recipe
465
+
466
+ ```typescript
467
+ await client.enableUserIsolation();
468
+ client.setContext({ workspace: '', project_id: projectId });
469
+
470
+ async function loadDocForEditor(id: string) {
471
+ return client.queryByIdentifierFull(id);
472
+ }
473
+
474
+ async function sidebarRow(id: string) {
475
+ const [node, listed] = await Promise.all([
476
+ client.queryByIdentifierShallow(id),
477
+ client.listIdentifierChildren(id),
478
+ ]);
479
+ return { node, children: listed.children };
480
+ }
481
+
482
+ async function saveDirty(items: XmlDocumentBatchItem[]) {
483
+ const { results } = await client.writeXmlDocumentsBatch(items);
484
+ results.forEach((r, i) => {
485
+ if (!r.ok) console.warn(i, r.error, r.code);
486
+ });
487
+ }
488
+ ```
489
+
490
+ **Save rules:** (1) At most **one** batch item per flush may use a given root **`db:identifier`**. (2) Never ship a parent that **inlines** another item’s root id in the same batch. (3) **`is_top`** does not relax (1) or (2).
491
+
447
492
  ## Delete document
448
493
 
449
494
  **`DELETE /api/v1/documents/by-id?identifier=/book/ch1`**
@@ -1538,9 +1583,10 @@ interface DatabaseContext {
1538
1583
  - `importDocument(file, options?)` → `ImportDocumentResult` — multipart `POST /api/v1/documents/import`
1539
1584
  - `exportDocument(identifier, format?, options?)` → `ExportDocumentResult` — binary `GET /api/v1/documents/export`
1540
1585
  - `queryByIdentifier(identifier, flags?, filter?, pathFilter?)` → `string[]`
1586
+ - `queryByIdentifierFull(identifier, filter?, pathFilter?)` → `string[]` — `FirstMatch,IncludeChildren` (editor load)
1541
1587
  - `queryByIdentifierShallow(identifier, filter?, pathFilter?)` → `string[]` — `NoChildren,KeepIndexNodes,FirstMatch`
1542
1588
  - `listChildIdentifiers(parentPath?)` → `ListIdentifierChildrenResult` — alias of `listIdentifierChildren`
1543
- - `queryByIdentifierWithChildren(identifier, filter?, pathFilter?)` → `{ node, children }`
1589
+ - `writeXmlDocumentsBatch(items)` → `DocumentBatchResponse` `POST /api/v1/documents/batch` (per-row `409`: `duplicate_identifier_in_batch`, `parent_inlines_batch_member`)
1544
1590
  - `queryDocuments(query: XCiteQuery, flags?, filter?, pathFilter?)` → `string[]`
1545
1591
  - `deleteDocument(identifier)` → `void`
1546
1592
  - `listIdentifiers(query: XCiteQuery)` → `ListIdentifiersResult`
package/llms.txt CHANGED
@@ -22,7 +22,9 @@ These are the most common sources of confusion for developers and AI assistants:
22
22
 
23
23
  8. **Project vs tenant id.** In the SDK, prefer `context.project_id` (and `listMyProjects` / `switchProject`). Many JSON bodies and JWT claims still use the field name `tenant_id` for the same value — the client sends that wire name automatically.
24
24
 
25
- 9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). By default the test LMDB starts **empty** (no production data). **Overlay mode:** same **`POST`** with JSON body **`{"overlay":true}`** (while authenticated for the project you want to inspect—project-scoped API key, or platform Bearer + **`X-Project-Id`**). The server opens a **writable** LMDB under `_test/<uuid>/` with that project’s on-disk store as a **read-only base** (dual LMDB): reads see production + overlay deltas; **writes never touch production**. **`XCiteDBClient.createTestSession({ …, overlay: true })`** sends that body. After creation, only **`X-Test-Session`** is required on requests (overlay is stored in session metadata).
25
+ 9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). By default the test LMDB starts **empty** (no production data). **Overlay mode:** same **`POST`** with JSON body **`{"overlay":true}`** (while authenticated for the project you want to inspect—project-scoped API key, or platform Bearer + **`X-Project-Id`**). The server opens a **writable** LMDB under `_test/<uuid>/` with that project’s on-disk store as a **read-only base** (dual LMDB): reads see production + overlay deltas; **writes never touch production**. **`XCiteDBClient.createTestSession({ …, overlay: true })`** sends that body. After creation, only **`X-Test-Session`** is required on requests (overlay is stored in session metadata). **Overlay + locks:** cooperative locks are stored in LMDB meta; overlay sessions **read through** to **base-layer** locks, but **`acquireLock` / `releaseLock`** only mutate the overlay—so a `lock_conflict` in an overlay test can still mean a lock held in production; clear with **`findLocks`** / **`forceReleaseLock`** or use a non-overlay session for lock-sensitive tests.
26
+
27
+ 10. **Binary assets (v2) are not XML/JSON documents.** Files live under canonical asset identifiers (e.g. `/assets/<uuid>`) and stable **`xcitedb:asset:/…`** URIs. **`POST /api/v1/assets`** uploads raw bytes; **`GET` / `HEAD` / `DELETE /api/v1/assets/…`** use per-segment URL encoding (optional **`?ml=`** magic-link bearer). Project routing is **`GET/PUT /api/v1/project/settings/asset-storage`** (`targets`, `mounts`, `imports`). User-isolation **asset-shares** and **asset magic links** live under **`/api/v1/security/user-isolation/asset-shares`** and **`/api/v1/security/asset-magic-links`**. JS/Python/C++ SDKs expose upload/get/head/delete + URI helpers (`parseAssetUri`, `asset_uri.parse`, `asset_uri::parse`) and share/magic-link clients.
26
28
 
27
29
  ## Choosing the Right Versioning Approach
28
30
 
@@ -72,6 +74,8 @@ Legacy REST paths (`/api/v1/branches`, `/commits`, `/tags`, `/diff`) remain as *
72
74
 
73
75
  12. **`X-Date` / `context.date` is not “server clock only.”** Revisions are keyed by the **instant you send**, not an implicit “now” when you set the header. To record a document under a **business date** (published, approved, effective), set **`X-Date`** or **`context.date`** to that instant before writing. Omitting **`X-Date`** does **not** substitute the current time on the write path — it selects **flat** writes (see convention 4).
74
76
 
77
+ 13. **Identifier strings (minting).** The engine rejects identifiers containing byte **`0x01`** (LMDB key separator). The API canonicalizes paths (**leading `/`**, **no trailing `/`**). Reserved namespaces include **`/_xcitedb/`**, **`/assets/`**, **`/public/assets/`**, and **`/public/shared/<tenant>/assets/`**. Server config **`identifier_hierarchy_max_depth`** defaults to **4**—deeper paths still work, but **`identifier-children`** listing may fall back to a scan.
78
+
75
79
  ## API key capability matrix (typical)
76
80
 
77
81
  | Capability | API key `role` | Public key allowed? |
@@ -162,6 +166,46 @@ When you build a backend that calls XCiteDB on behalf of users:
162
166
 
163
167
  - **Standalone subtree root.** Because XCiteDB shreds XML per identifier, you can `writeXmlDocument('<sec db:identifier="/spaces/u/docs/doc1/sec-1">…</sec>')` and the engine writes only that subtree — you do **not** need to re-send the parent document. The parent's children index (`id_hier`) is updated automatically. Keep `is_top: true` (default) on `POST /api/v1/documents` even when the `db:identifier` is nested under another path. Use `compare_attributes: true` only when you need attribute-level diffing for triggers.
164
168
 
169
+ ## XML shredding semantics (must read)
170
+
171
+ - **Auto-shred.** Any element with **`db:identifier`** is stored as its own LMDB row. The parent’s stored XML keeps a **`<db:N…/>`** placeholder for that slot. There is **no** size threshold and **no** opt-out.
172
+ - **`is_top` is a marker only.** It registers a **`TOP:<xcitepath>`** alias for tooling; it does **not** change shredding, merge, or overwrite behavior. Leaving **`true`** on every write is normal.
173
+ - **`POST /api/v1/documents/batch`** runs items **in order**; each item’s write replaces LMDB rows for **every** `db:identifier` present in that item’s XML. The server prechecks each row and may return **`409`** with **`duplicate_identifier_in_batch`** (same root id in two items) or **`parent_inlines_batch_member`** (a parent’s XML still contains another item’s root id—a stale inlined child). Check **`results[i].ok`** on the JSON response.
174
+ - **Save the smallest dirty subtree.** Do not flush a parent that still contains full XML for an identified child you also write in the same batch. Prefer placeholders from **`queryByIdentifierShallow`**, or write only the changed child documents.
175
+
176
+ **Round-trip:** Element structure, child order, and text are preserved. The server may add **`db:order`**, **`db:xcitepath`**, **`xmlns:db`**. Attribute order and insignificant whitespace are **not** byte-stable—compare by DOM, not raw bytes.
177
+
178
+ ## Minimal SPA editor recipe
179
+
180
+ ```typescript
181
+ await client.enableUserIsolation(); // if app users edit under namespaced paths
182
+ client.setContext({ workspace: '', project_id: projectId });
183
+
184
+ /** Full document for the editor (all shredded children inlined). */
185
+ async function loadDocForEditor(id: string) {
186
+ return client.queryByIdentifierFull(id);
187
+ // same as: queryByIdentifier(id, 'FirstMatch,IncludeChildren')
188
+ }
189
+
190
+ /** Sidebar / tree: shallow shell + one hierarchy level (two parallel requests). */
191
+ async function sidebarRow(id: string) {
192
+ const [node, listed] = await Promise.all([
193
+ client.queryByIdentifierShallow(id),
194
+ client.listIdentifierChildren(id),
195
+ ]);
196
+ return { node, children: listed.children };
197
+ }
198
+
199
+ async function saveDirty(items: XmlDocumentBatchItem[]) {
200
+ const { results } = await client.writeXmlDocumentsBatch(items);
201
+ results.forEach((r, i) => {
202
+ if (!r.ok) console.warn(i, r.error, r.code);
203
+ });
204
+ }
205
+ ```
206
+
207
+ **Three save rules:** (1) Each intended root **`db:identifier`** appears as the **root** of **at most one** batch item per flush. (2) Never ship a parent’s **full** subtree for a child id that is also its **own** batch row in the same request. (3) **`is_top`** does not relax (1) or (2).
208
+
165
209
  ## JavaScript/TypeScript SDK (`@xcitedbs/client`)
166
210
 
167
211
  Install: `npm install @xcitedbs/client`
@@ -282,13 +326,14 @@ interface XCiteDBClientOptions {
282
326
 
283
327
  **XML Documents:**
284
328
  - `writeXmlDocument(xml, options?)` — Store XML via JSON body (identifier inside XML). Deprecated alias: `writeDocumentJson`.
329
+ - `writeXmlDocumentsBatch(items)` — `POST /api/v1/documents/batch`; per-row `ok` / `error` / `code` (see shredding section for `409` batch conflicts)
285
330
  - `writeXML(xml)` — Store raw XML with `Content-Type: application/xml`
286
331
  - `importDocument(file, options?)` — `POST /api/v1/documents/import` (multipart `file`). Server converts DOCX, ODF, RTF, PDF, Markdown, AsciiDoc, or plain text into shredded XML. Optional `identifier` query override; optional `filename` for sniffing when not a `File`.
287
332
  - `exportDocument(id, format?, options?)` — `GET /api/v1/documents/export` binary (`xml` | `docx` | `odt` | `pdf` | `txt` | `md` | `adoc`). Optional `strict` for text exports.
288
333
  - `queryByIdentifier(id, flags?, filter?)` — Get document(s) by identifier
334
+ - `queryByIdentifierFull(id, filter?, pathFilter?)` — Same as `flags=FirstMatch,IncludeChildren` (editor load: full subtree)
289
335
  - `queryByIdentifierShallow(id, filter?, pathFilter?)` — Same as `flags=NoChildren,KeepIndexNodes,FirstMatch` (skeleton: placeholders `db:N*` for shredded slots; avoids full subtree)
290
- - `listChildIdentifiers(parentPath?)` — Alias of `listIdentifierChildren`; next level of identifier hierarchy
291
- - `queryByIdentifierWithChildren(id, filter?, pathFilter?)` — `Promise.all` of shallow node + `identifier-children` for one expansion step
336
+ - `listChildIdentifiers(parentPath?)` — Alias of `listIdentifierChildren`; next level of identifier hierarchy (pair with `queryByIdentifierShallow` for sidebars—`Promise.all` both if you want one round-trip client-side)
292
337
  - `queryDocuments(query, flags?)` — List/filter documents
293
338
  - `deleteDocument(identifier)` — Delete by identifier
294
339
  - `listIdentifiers(query)` — List known identifiers with pagination
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -4,6 +4,8 @@
4
4
 
5
5
  Unquery is a declarative language for querying and transforming structured documents (JSON and XML). It is the query language of XCiteDB and the `unq` command-line tool. Every query is itself a JSON document. The result is also JSON.
6
6
 
7
+ > **Maintainers — parse regression tests:** Fenced `json` / `text` examples in this file, plus Unquery listings in [`web/src/docs/content/unquery-tutorial.html`](../web/src/docs/content/unquery-tutorial.html), feed the generated fixture [`XCiteDB2/tests/fixtures/unquery_tutorial_parse_cases.json`](../XCiteDB2/tests/fixtures/unquery_tutorial_parse_cases.json) (do not edit by hand). After changing examples here or in that HTML file, regenerate from the repo root with `python3 scripts/gen_unquery_tutorial_parse_cases.py`, then commit the updated JSON. CI / local builds run `unquery_tutorial_parse_test` (see [`XCiteDB2/tests/run_tests.sh`](../XCiteDB2/tests/run_tests.sh)).
8
+
7
9
  ---
8
10
 
9
11
  ## 0. AI preamble — must-read before writing a query