@xcitedbs/client 0.2.24 → 0.2.26

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.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, 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';
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, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, 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;
@@ -509,12 +509,18 @@ export declare class XCiteDBClient {
509
509
  * Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
510
510
  * For storing JSON data by key, use `writeJsonDocument`.
511
511
  *
512
+ * **Pruned writes:** you may include **`db:stub="true"`** (or legacy **`<db:N…/>`**) under a parent to preserve shredded children
513
+ * without inlining their bodies; stubs must be empty and reference an existing row (see product `llms.txt` / OpenAPI notes).
514
+ * Malformed hierarchical identifiers return **400** `invalid_identifier`; invalid stub/placeholder XML returns **400** `xml_write_rejected`.
515
+ *
512
516
  * On cooperative-lock conflict the server returns **423** with a {@link LockConflictBody} shape in {@link XCiteDBError.body}.
513
517
  */
514
518
  writeXmlDocument(xml: string, options?: WriteDocumentOptions): Promise<void>;
515
519
  /**
516
520
  * Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
517
521
  * Rows with `error === 'lock_conflict'` include `current_lock` (code **423** per row).
522
+ * **`409`** precheck rows may include **`detail`** (`identifier` for duplicates; `parent` + `inlined_child` for parent inline).
523
+ * Identifiers on **`db:stub="true"`** only are batch references and do not count as inlined bodies for precheck.
518
524
  */
519
525
  writeXmlDocumentsBatch(items: XmlDocumentBatchItem[]): Promise<DocumentBatchResponse>;
520
526
  /**
@@ -534,13 +540,15 @@ export declare class XCiteDBClient {
534
540
  writeDocumentJson(xml: string, options?: WriteDocumentOptions): Promise<void>;
535
541
  queryByIdentifier(identifier: string, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
536
542
  /**
537
- * Shallow read: root element with inline leaves plus `db:N*` placeholders for shredded child slots
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).
543
+ * Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
544
+ * root element with inline leaves plus **identifier-bearing stub elements** (`db:stub="true"`) for shredded
545
+ * child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
546
+ * For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
540
547
  */
541
548
  queryByIdentifierShallow(identifier: string, filter?: string, pathFilter?: string): Promise<string[]>;
542
549
  /**
543
550
  * Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
551
+ * The returned array has **length 0 or 1**; when present, index `0` is the full XML string.
544
552
  * Use this for editor round-trips. For navigation without loading the full subtree, use
545
553
  * {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
546
554
  */
@@ -678,6 +686,8 @@ export declare class XCiteDBClient {
678
686
  headAsset(uriOrIdentifier: string, opts?: {
679
687
  ml?: string;
680
688
  }): Promise<AssetHeadResult>;
689
+ /** `GET /api/v1/assets` — paginated, ABAC-filtered listing. Magic-link auth is rejected by the server. */
690
+ listAssets(opts?: ListAssetsOptions): Promise<AssetListResponse>;
681
691
  /** Admin: GC dry-run — manifest vs live meta references (`POST /api/v1/admin/assets/gc/dry-run`). */
682
692
  adminAssetsGcDryRun(opts?: {
683
693
  ownerUserId?: string;
package/dist/client.js CHANGED
@@ -1538,6 +1538,10 @@ class XCiteDBClient {
1538
1538
  * Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
1539
1539
  * For storing JSON data by key, use `writeJsonDocument`.
1540
1540
  *
1541
+ * **Pruned writes:** you may include **`db:stub="true"`** (or legacy **`<db:N…/>`**) under a parent to preserve shredded children
1542
+ * without inlining their bodies; stubs must be empty and reference an existing row (see product `llms.txt` / OpenAPI notes).
1543
+ * Malformed hierarchical identifiers return **400** `invalid_identifier`; invalid stub/placeholder XML returns **400** `xml_write_rejected`.
1544
+ *
1541
1545
  * On cooperative-lock conflict the server returns **423** with a {@link LockConflictBody} shape in {@link XCiteDBError.body}.
1542
1546
  */
1543
1547
  async writeXmlDocument(xml, options) {
@@ -1550,6 +1554,8 @@ class XCiteDBClient {
1550
1554
  /**
1551
1555
  * Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
1552
1556
  * Rows with `error === 'lock_conflict'` include `current_lock` (code **423** per row).
1557
+ * **`409`** precheck rows may include **`detail`** (`identifier` for duplicates; `parent` + `inlined_child` for parent inline).
1558
+ * Identifiers on **`db:stub="true"`** only are batch references and do not count as inlined bodies for precheck.
1553
1559
  */
1554
1560
  async writeXmlDocumentsBatch(items) {
1555
1561
  const body = {
@@ -1630,19 +1636,20 @@ class XCiteDBClient {
1630
1636
  filter,
1631
1637
  path_filter: pathFilter,
1632
1638
  });
1633
- const rows = await this.request('GET', `/api/v1/documents/by-id${q}`);
1634
- return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
1639
+ return this.request('GET', `/api/v1/documents/by-id${q}`);
1635
1640
  }
1636
1641
  /**
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).
1642
+ * Shallow read (`flags=NoChildren,KeepIndexNodes,FirstMatch` on `GET /api/v1/documents/by-id`):
1643
+ * root element with inline leaves plus **identifier-bearing stub elements** (`db:stub="true"`) for shredded
1644
+ * child slots instead of opaque `db:N*` placeholders. Safe to round-trip with {@link writeXmlDocument}.
1645
+ * For sidebar / AST navigation, pair with {@link listIdentifierChildren} (e.g. `Promise.all` of shallow + children).
1640
1646
  */
1641
1647
  async queryByIdentifierShallow(identifier, filter, pathFilter) {
1642
1648
  return this.queryByIdentifier(identifier, 'NoChildren,KeepIndexNodes,FirstMatch', filter, pathFilter);
1643
1649
  }
1644
1650
  /**
1645
1651
  * Load a document with all shredded children inlined (`FirstMatch,IncludeChildren` on `GET /by-id`).
1652
+ * The returned array has **length 0 or 1**; when present, index `0` is the full XML string.
1646
1653
  * Use this for editor round-trips. For navigation without loading the full subtree, use
1647
1654
  * {@link queryByIdentifierShallow} plus {@link listIdentifierChildren}.
1648
1655
  */
@@ -1674,8 +1681,7 @@ class XCiteDBClient {
1674
1681
  if (pq.filter_any_meta === true) {
1675
1682
  params.any_meta = '1';
1676
1683
  }
1677
- const rows = await this.request('GET', `/api/v1/documents${buildQuery(params)}`);
1678
- return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
1684
+ return this.request('GET', `/api/v1/documents${buildQuery(params)}`);
1679
1685
  }
1680
1686
  async deleteDocument(identifier) {
1681
1687
  await this.request('DELETE', `/api/v1/documents/by-id${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
@@ -2257,6 +2263,20 @@ class XCiteDBClient {
2257
2263
  this.notifySessionInvalidIfNeeded(path, 401);
2258
2264
  throw new types_1.XCiteDBError('Request failed after retry', 401, null);
2259
2265
  }
2266
+ /** `GET /api/v1/assets` — paginated, ABAC-filtered listing. Magic-link auth is rejected by the server. */
2267
+ async listAssets(opts) {
2268
+ const params = {};
2269
+ if (opts?.prefix)
2270
+ params.prefix = opts.prefix;
2271
+ if (typeof opts?.limit === 'number')
2272
+ params.limit = String(opts.limit);
2273
+ if (opts?.cursor)
2274
+ params.cursor = opts.cursor;
2275
+ if (opts?.includeHeader)
2276
+ params.include_header = 'true';
2277
+ const q = buildQuery(params);
2278
+ return this.request('GET', `/api/v1/assets${q}`, undefined);
2279
+ }
2260
2280
  /** Admin: GC dry-run — manifest vs live meta references (`POST /api/v1/admin/assets/gc/dry-run`). */
2261
2281
  async adminAssetsGcDryRun(opts) {
2262
2282
  const q = opts?.ownerUserId && opts.ownerUserId.trim()
@@ -32,6 +32,28 @@ const types_js_1 = require("./types.js");
32
32
  globalThis.fetch = orig;
33
33
  }
34
34
  });
35
+ (0, node_test_1.it)('queryByIdentifier / queryDocuments return XML bodies unmodified under userIsolation', async () => {
36
+ const xml = '<?xml version="1.0"?><doc xmlns:db="http://www.xcitedb.com/schema"><a href="https://example.com//path">x</a></doc>';
37
+ const orig = globalThis.fetch;
38
+ globalThis.fetch = node_test_1.mock.fn(async () => {
39
+ return new Response(JSON.stringify([xml]), { status: 200 });
40
+ });
41
+ try {
42
+ const c = new client_js_1.XCiteDBClient({
43
+ baseUrl: 'http://127.0.0.1:9',
44
+ apiKey: 'test-key',
45
+ userIsolation: { enabled: true, namespace: '/users/{userId}' },
46
+ });
47
+ c.setAppUserTokens('header.eyJzdWIiOiJ1c2VyLTEifQ.sig');
48
+ const a = await c.queryByIdentifier('/x');
49
+ strict_1.default.deepEqual(a, [xml]);
50
+ const b = await c.queryDocuments({ match: '/x' });
51
+ strict_1.default.deepEqual(b, [xml]);
52
+ }
53
+ finally {
54
+ globalThis.fetch = orig;
55
+ }
56
+ });
35
57
  (0, node_test_1.it)('403 becomes XCiteDBForbiddenError with policy_id and request ids', async () => {
36
58
  const orig = globalThis.fetch;
37
59
  globalThis.fetch = node_test_1.mock.fn(async () => {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { XCiteDBClient } from './client';
2
2
  export { parseAssetUri, formatAssetUri, collectIdentifiersFromText, ASSET_URI_PREFIX } from './assetUri';
3
3
  export { WebSocketSubscription } from './websocket';
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
+ 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, AssetListItem, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, 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';
5
5
  export { XCiteDBError, XCiteDBForbiddenError, XCiteDBNotFoundError, XCiteDBAuthError, XCiteDBLockConflictError, } from './types';
package/dist/types.d.ts CHANGED
@@ -194,6 +194,34 @@ export interface AssetHeadResult {
194
194
  size: number;
195
195
  etag?: string;
196
196
  }
197
+ /** One row of `GET /api/v1/assets`. Header fields are present only when `include_header=true`. */
198
+ export interface AssetListItem {
199
+ identifier: string;
200
+ uri: string;
201
+ content_type?: string;
202
+ size?: number;
203
+ sha256?: string;
204
+ etag?: string;
205
+ target_name?: string;
206
+ owner_user_id?: string;
207
+ created?: string;
208
+ }
209
+ /** `GET /api/v1/assets` paginated, ABAC-filtered. */
210
+ export interface AssetListResponse {
211
+ items: AssetListItem[];
212
+ next_cursor: string | null;
213
+ }
214
+ /** Options for {@link XCiteDBClient.listAssets}. */
215
+ export interface ListAssetsOptions {
216
+ /** Filter by canonical id prefix (e.g. `/users/abc/assets/`). */
217
+ prefix?: string;
218
+ /** Page size, 1..1000 (default 200). */
219
+ limit?: number;
220
+ /** Last identifier from previous page (exclusive). */
221
+ cursor?: string;
222
+ /** Include per-asset header fields (`content_type`, `size`, …). */
223
+ includeHeader?: boolean;
224
+ }
197
225
  /** `POST /api/v1/admin/assets/gc/dry-run` (admin). */
198
226
  export interface AssetGcDryRunResult {
199
227
  manifest_count: number;
@@ -555,6 +583,11 @@ export interface DocumentBatchResultRow {
555
583
  */
556
584
  error?: string;
557
585
  code?: number;
586
+ /**
587
+ * Structured context for XML batch **`409`** precheck rows, e.g. `{ identifier }` for
588
+ * `duplicate_identifier_in_batch` or `{ parent, inlined_child }` for `parent_inlines_batch_member`.
589
+ */
590
+ detail?: Record<string, string>;
558
591
  /** Present when `error === 'lock_conflict'` (HTTP-style code 423 in batch row). */
559
592
  current_lock?: LockInfo;
560
593
  }
package/llms-full.txt CHANGED
@@ -65,7 +65,7 @@ Legacy REST paths under `/api/v1/branches`, `/commits`, `/tags`, `/diff` remain
65
65
 
66
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.
67
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.
68
+ 8. **Identifier strings (minting).** The API canonicalizes hierarchical paths (**leading `/`**, **no trailing `/`**). **`POST /api/v1/documents`** rejects malformed ids with **`400`** JSON **`{ "error": "invalid_identifier", "reason", "identifier", "detail" }`**: no commas, no ASCII control characters (including **`0x01`**), no `//` or empty segments, no adjacent duplicate segments (e.g. `/a/b/b/c`). 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
69
 
70
70
  ---
71
71
 
@@ -430,7 +430,7 @@ With `Content-Type: application/json`:
430
430
 
431
431
  With `Content-Type: application/xml`: send raw XML body.
432
432
 
433
- The identifier is extracted from the `db:identifier` attribute in the root XML element.
433
+ The identifier is extracted from the `db:identifier` attribute in the root XML element. Malformed root identifiers return **`400`** (`invalid_identifier`). Invalid stub/placeholder XML on write returns **`400`** with `error: "xml_write_rejected"` and a `message` string.
434
434
 
435
435
  ## Get document by identifier
436
436
 
@@ -440,7 +440,8 @@ Query parameters: `identifier` (required), `flags` (optional: `FirstMatch`, `Inc
440
440
 
441
441
  ### Shallow node + subtree navigation
442
442
 
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).
443
+ - **`flags=NoChildren,KeepIndexNodes,FirstMatch`**: returns the matching node with inline leaf content plus **identifier-bearing stub elements** **`db:stub="true"`** for shredded child slots (real element names; avoids loading the full deep subtree in one response). The engine still stores internal **`<db:N…/>`** placeholders on disk.
444
+ - **`GET …/by-id`** with **`FirstMatch,IncludeChildren`** returns a JSON array of **length 0 or 1**; index `0` is the full assembled XML when present.
444
445
  - Combine with **`GET /api/v1/documents/identifier-children?parent_path=…`** for the next level of hierarchy (`segment`, `full_path`, `is_identifier`, `has_children`).
445
446
  - **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.
446
447
 
@@ -450,16 +451,17 @@ You may `POST /api/v1/documents` with a root element whose `db:identifier` is a
450
451
 
451
452
  ### XML shredding semantics (must read)
452
453
 
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
+ - **Auto-shred.** Any element with **`db:identifier`** is stored as its own LMDB row. The parent’s stored XML keeps an internal **`<db:N…/>`** placeholder for that slot (engine-only). There is **no** size threshold and **no** opt-out.
455
+ - **Pruned writes.** Clients may send **stub** elements (**`db:stub="true"`**) or legacy **`<db:N…/>`** under a parent to preserve subtrees without inlining full XML. Stubs must be empty, reference an **existing** identifier, and must not appear at the document root; violations are **`400`** (`xml_write_rejected`).
454
456
  - **`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
+ - **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 **except** ids that appear **only** on **`db:stub="true"`** elements (references, not inlined bodies). 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 **fully inlines** another item’s **root** id in the same batch (stubs referencing another batch member are allowed).
458
+ - **Idempotency (retries).** Re-sending the same XML with the same **`X-Date`** (or unversioned mode) is a no-op when the revision is unchanged; reuse the date key on retries.
457
459
 
458
460
  **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
461
 
460
462
  ### Batch XML writes
461
463
 
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`**.
464
+ **`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"?, "detail"?, "current_lock"? } ] }`** (`detail` for **`409`** precheck rows: duplicate → `{ "identifier": "<root>" }`; parent inline → `{ "parent": "<root>", "inlined_child": "<other>" }`). Best-effort: later rows still run when earlier rows fail. JavaScript: **`writeXmlDocumentsBatch`**. Python: **`write_xml_documents_batch`**. C++: **`write_xml_documents_batch`**.
463
465
 
464
466
  ### Minimal SPA editor recipe
465
467
 
@@ -487,7 +489,7 @@ async function saveDirty(items: XmlDocumentBatchItem[]) {
487
489
  }
488
490
  ```
489
491
 
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).
492
+ **Batch save rules:** (1) At most **one** batch item per flush may use a given root **`db:identifier`** (`duplicate_identifier_in_batch`). (2) Do not ship a parent that **fully inlines** another item’s root id in the same batch (`parent_inlines_batch_member`); **`db:stub="true"`** references to another batch member are **allowed**. (3) **`is_top`** does not relax (1) or (2).
491
493
 
492
494
  ## Delete document
493
495
 
@@ -1583,10 +1585,10 @@ interface DatabaseContext {
1583
1585
  - `importDocument(file, options?)` → `ImportDocumentResult` — multipart `POST /api/v1/documents/import`
1584
1586
  - `exportDocument(identifier, format?, options?)` → `ExportDocumentResult` — binary `GET /api/v1/documents/export`
1585
1587
  - `queryByIdentifier(identifier, flags?, filter?, pathFilter?)` → `string[]`
1586
- - `queryByIdentifierFull(identifier, filter?, pathFilter?)` → `string[]` — `FirstMatch,IncludeChildren` (editor load)
1587
- - `queryByIdentifierShallow(identifier, filter?, pathFilter?)` → `string[]` — `NoChildren,KeepIndexNodes,FirstMatch`
1588
+ - `queryByIdentifierFull(identifier, filter?, pathFilter?)` → `string[]` — `FirstMatch,IncludeChildren` (editor load); array length **0 or 1**
1589
+ - `queryByIdentifierShallow(identifier, filter?, pathFilter?)` → `string[]` — `NoChildren,KeepIndexNodes,FirstMatch` (skeleton with **`db:stub="true"`** child markers)
1588
1590
  - `listChildIdentifiers(parentPath?)` → `ListIdentifierChildrenResult` — alias of `listIdentifierChildren`
1589
- - `writeXmlDocumentsBatch(items)` → `DocumentBatchResponse` — `POST /api/v1/documents/batch` (per-row `409`: `duplicate_identifier_in_batch`, `parent_inlines_batch_member`)
1591
+ - `writeXmlDocumentsBatch(items)` → `DocumentBatchResponse` — `POST /api/v1/documents/batch` (per-row `409` + optional **`detail`**; stubs exempt from duplicate-root / parent-inline checks per semantics above)
1590
1592
  - `queryDocuments(query: XCiteQuery, flags?, filter?, pathFilter?)` → `string[]`
1591
1593
  - `deleteDocument(identifier)` → `void`
1592
1594
  - `listIdentifiers(query: XCiteQuery)` → `ListIdentifiersResult`
package/llms.txt CHANGED
@@ -74,7 +74,7 @@ Legacy REST paths (`/api/v1/branches`, `/commits`, `/tags`, `/diff`) remain as *
74
74
 
75
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).
76
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.
77
+ 13. **Identifier strings (minting).** The API canonicalizes paths (**leading `/`**, **no trailing `/`**). **`POST /api/v1/documents`** rejects malformed hierarchical ids with **`400`** and JSON **`{ "error": "invalid_identifier", "reason", "identifier", "detail" }`**: no commas, no ASCII control characters (including **`0x01`** LMDB separator), no `//` or empty path segments, no **adjacent duplicate** path segments (e.g. `/a/b/b/c`). 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
78
 
79
79
  ## API key capability matrix (typical)
80
80
 
@@ -168,12 +168,14 @@ When you build a backend that calls XCiteDB on behalf of users:
168
168
 
169
169
  ## XML shredding semantics (must read)
170
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.
171
+ - **Auto-shred.** Any element with **`db:identifier`** is stored as its own LMDB row. The parent’s stored XML keeps an internal **`<db:N…/>`** placeholder for that slot (engine-only). There is **no** size threshold and **no** opt-out.
172
+ - **Shallow reads (`NoChildren,KeepIndexNodes`).** The API returns **identifier-bearing stubs** instead of opaque placeholders: **`<elem db:identifier="…" db:xcitepath="…" db:order="…" db:stub="true"/>`** (real element name, empty element). Clients may round-trip these stubs through **`writeXmlDocument`**; preserved subtrees are not deleted. You may also send legacy **`<db:N…/>`** placeholders from older clients; the engine resolves them under the parent’s **`db:xcitepath`**.
173
+ - **Pruned writes.** You may mix **full** identified subtrees and **stub** markers (or placeholders) under one parent. Stubs must be **empty** (no element children, no non-whitespace text), carry **`db:identifier`** (or legacy **`identifier`**) pointing at an **existing** row, and optional **`db:order`**, **`db:xcitepath`**, **`xmlns:db`** only. The document root must not be a stub. Violations yield **`400`** with `error: "xml_write_rejected"` and a **`message`** string (`stub_*` / `placeholder_*`).
172
174
  - **`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 ida 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
+ - **`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 **except** identifiers that appear only on **`db:stub="true"`** elements (those are references, not inlined document bodies). 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 **fully inlines** another item’s root id). Failed rows include a JSON **`detail`** object: for duplicates **`{ "identifier": "<root>" }`**; for parent inline **`{ "parent": "<root>", "inlined_child": "<other root>" }`**. Check **`results[i].ok`** on the JSON response.
176
+ - **Idempotency (retries).** Re-sending the same XML body with the same **`X-Date`** (or unversioned mode) is a safe no-op when the stored revision is unchanged; use the same date key on client retries to avoid duplicate logical revisions.
175
177
 
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.
178
+ **Round-trip:** Element structure, child order, and text are preserved. The server may add **`db:order`**, **`db:xcitepath`**, **`xmlns:db`**. Any app-specific structural metadata that must survive write→read (e.g. numbering aligned with an editor) must live **inside the stored XML** (or JSON metadata via **`addMeta`**); the server does not sync arbitrary sidecar fields. Attribute order and insignificant whitespace are **not** byte-stable—compare by DOM, not raw bytes.
177
179
 
178
180
  ## Minimal SPA editor recipe
179
181
 
@@ -184,7 +186,7 @@ client.setContext({ workspace: '', project_id: projectId });
184
186
  /** Full document for the editor (all shredded children inlined). */
185
187
  async function loadDocForEditor(id: string) {
186
188
  return client.queryByIdentifierFull(id);
187
- // same as: queryByIdentifier(id, 'FirstMatch,IncludeChildren')
189
+ // Returns JSON array length 0 or 1: when present, index 0 is one fully assembled XML string (document order).
188
190
  }
189
191
 
190
192
  /** Sidebar / tree: shallow shell + one hierarchy level (two parallel requests). */
@@ -204,7 +206,7 @@ async function saveDirty(items: XmlDocumentBatchItem[]) {
204
206
  }
205
207
  ```
206
208
 
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).
209
+ **Batch save rules:** (1) Each intended root **`db:identifier`** appears as the **root** of **at most one** batch item per flush (**`duplicate_identifier_in_batch`**). (2) Do not ship a parent’s **full** subtree for a child id that is also its **own** batch row in the same request (**`parent_inlines_batch_member`**). Stubs **`db:stub="true"`** referencing another batch member are **allowed** and do not trigger (2). (3) **`is_top`** does not relax (1) or (2).
208
210
 
209
211
  ## JavaScript/TypeScript SDK (`@xcitedbs/client`)
210
212
 
@@ -326,13 +328,13 @@ interface XCiteDBClientOptions {
326
328
 
327
329
  **XML Documents:**
328
330
  - `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)
331
+ - `writeXmlDocumentsBatch(items)` — `POST /api/v1/documents/batch`; per-row `ok` / `error` / `code` / optional `detail` (see shredding section for `409` batch conflicts)
330
332
  - `writeXML(xml)` — Store raw XML with `Content-Type: application/xml`
331
333
  - `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`.
332
334
  - `exportDocument(id, format?, options?)` — `GET /api/v1/documents/export` binary (`xml` | `docx` | `odt` | `pdf` | `txt` | `md` | `adoc`). Optional `strict` for text exports.
333
335
  - `queryByIdentifier(id, flags?, filter?)` — Get document(s) by identifier
334
- - `queryByIdentifierFull(id, filter?, pathFilter?)` — Same as `flags=FirstMatch,IncludeChildren` (editor load: full subtree)
335
- - `queryByIdentifierShallow(id, filter?, pathFilter?)` — Same as `flags=NoChildren,KeepIndexNodes,FirstMatch` (skeleton: placeholders `db:N*` for shredded slots; avoids full subtree)
336
+ - `queryByIdentifierFull(id, filter?, pathFilter?)` — Same as `flags=FirstMatch,IncludeChildren` (editor load: full subtree). Response array is **length 0 or 1**; the single string is the full document.
337
+ - `queryByIdentifierShallow(id, filter?, pathFilter?)` — Same as `flags=NoChildren,KeepIndexNodes,FirstMatch` (skeleton: **`db:stub="true"`** elements for shredded child slots; avoids full subtree)
336
338
  - `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)
337
339
  - `queryDocuments(query, flags?)` — List/filter documents
338
340
  - `deleteDocument(identifier)` — Delete by identifier
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",