@xcitedbs/client 0.2.18 → 0.2.19

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, Flags, JsonDocumentBatchItem, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, 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, 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, Flags, JsonDocumentBatchItem, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, 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, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult } from './types';
2
2
  import { WebSocketSubscription } from './websocket';
3
3
  export declare class XCiteDBClient {
4
4
  private baseUrl;
@@ -19,6 +19,8 @@ export declare class XCiteDBClient {
19
19
  private userIsolation?;
20
20
  private cachedAppUserId?;
21
21
  private readonly requestTimeoutMs?;
22
+ /** Set by {@link XCiteDBClient.createTestSession} when the server returns a `bootstrap` summary. */
23
+ lastTestSessionBootstrap?: TestSessionBootstrapSummary;
22
24
  constructor(options: XCiteDBClientOptions);
23
25
  /**
24
26
  * Create an ephemeral test database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
@@ -269,6 +271,10 @@ export declare class XCiteDBClient {
269
271
  * ```
270
272
  */
271
273
  checkAccess(subject: PolicySubjectInput, identifier: string, action: string, branch?: string): Promise<AccessCheckResult>;
274
+ /**
275
+ * Dry-run access (`POST /api/v1/security/check`). Same as {@link checkAccess}; use for debugging 403s.
276
+ */
277
+ diagnoseAccess(subject: PolicySubjectInput, identifier: string, action: string, branch?: string): Promise<AccessCheckResult>;
272
278
  getSecurityConfig(): Promise<SecurityConfig>;
273
279
  /**
274
280
  * Update tenant security defaults (`PUT /api/v1/security/config`). When ABAC is enabled, pair
package/dist/client.js CHANGED
@@ -8,6 +8,44 @@ function joinUrl(base, path) {
8
8
  const p = path.startsWith('/') ? path : `/${path}`;
9
9
  return `${b}${p}`;
10
10
  }
11
+ function newClientRequestId() {
12
+ const c = globalThis.crypto?.randomUUID?.();
13
+ if (c)
14
+ return c;
15
+ const a = new Uint8Array(16);
16
+ if (globalThis.crypto?.getRandomValues)
17
+ globalThis.crypto.getRandomValues(a);
18
+ else
19
+ for (let i = 0; i < 16; i++)
20
+ a[i] = Math.floor(Math.random() * 256);
21
+ return Array.from(a, (x) => x.toString(16).padStart(2, '0')).join('');
22
+ }
23
+ function extrasFromErrorBody(data, serverRequestId, clientRequestId) {
24
+ const bodyObj = typeof data === 'object' && data !== null ? data : undefined;
25
+ return {
26
+ reason: bodyObj && typeof bodyObj.reason === 'string' ? bodyObj.reason : undefined,
27
+ policyId: bodyObj && typeof bodyObj.policy_id === 'string' ? bodyObj.policy_id : undefined,
28
+ hint: bodyObj && typeof bodyObj.hint === 'string' ? bodyObj.hint : undefined,
29
+ expectedRole: bodyObj && typeof bodyObj.expected_role === 'string' ? bodyObj.expected_role : undefined,
30
+ actualRole: bodyObj && typeof bodyObj.actual_role === 'string' ? bodyObj.actual_role : undefined,
31
+ serverRequestId,
32
+ clientRequestId,
33
+ };
34
+ }
35
+ /** @internal */
36
+ function throwForFailedHttp(status, path, data, msg, serverRequestId, clientRequestId) {
37
+ const extras = extrasFromErrorBody(data, serverRequestId, clientRequestId);
38
+ const Ctor = status === 403
39
+ ? types_1.XCiteDBForbiddenError
40
+ : status === 404
41
+ ? types_1.XCiteDBNotFoundError
42
+ : status === 401
43
+ ? types_1.XCiteDBAuthError
44
+ : status === 409 || status === 423
45
+ ? types_1.XCiteDBLockConflictError
46
+ : types_1.XCiteDBError;
47
+ throw new Ctor(msg || `HTTP ${status}`, status, data, extras);
48
+ }
11
49
  function buildQuery(params) {
12
50
  const sp = new URLSearchParams();
13
51
  for (const [k, v] of Object.entries(params)) {
@@ -83,8 +121,13 @@ class XCiteDBClient {
83
121
  userIsolation: opts.userIsolation,
84
122
  requestTimeoutMs: opts.requestTimeoutMs,
85
123
  });
86
- const data = await temp.request('POST', '/api/v1/test/sessions', opts.overlay === true ? { overlay: true } : undefined, undefined, { no401Retry: true });
87
- return new XCiteDBClient({
124
+ const sessionBody = {};
125
+ if (opts.overlay === true)
126
+ sessionBody.overlay = true;
127
+ if (opts.bootstrap !== undefined)
128
+ sessionBody.bootstrap = opts.bootstrap;
129
+ const data = await temp.request('POST', '/api/v1/test/sessions', Object.keys(sessionBody).length ? sessionBody : undefined, undefined, { no401Retry: true });
130
+ const child = new XCiteDBClient({
88
131
  baseUrl: opts.baseUrl,
89
132
  apiKey: opts.testRequireAuth ? opts.apiKey : undefined,
90
133
  accessToken: opts.testRequireAuth ? opts.accessToken : undefined,
@@ -101,6 +144,10 @@ class XCiteDBClient {
101
144
  userIsolation: opts.userIsolation,
102
145
  requestTimeoutMs: opts.requestTimeoutMs,
103
146
  });
147
+ if (data.bootstrap && typeof data.bootstrap === 'object') {
148
+ child.lastTestSessionBootstrap = data.bootstrap;
149
+ }
150
+ return child;
104
151
  }
105
152
  /**
106
153
  * Canonical `project:<tenant_id>:<role>` group string. The middle segment must match the app JWT `tenant_id`
@@ -464,6 +511,8 @@ class XCiteDBClient {
464
511
  ...this.requestHeaders(),
465
512
  ...extraHeaders,
466
513
  };
514
+ const outgoingRequestId = newClientRequestId();
515
+ headers['X-Request-Id'] = outgoingRequestId;
467
516
  let init = { method, headers };
468
517
  if (body !== undefined) {
469
518
  if (typeof body === 'string') {
@@ -477,7 +526,14 @@ class XCiteDBClient {
477
526
  const sig = requestTimeoutSignal(this.requestTimeoutMs);
478
527
  if (sig)
479
528
  init.signal = sig;
480
- const res = await fetch(url, init);
529
+ let res;
530
+ try {
531
+ res = await fetch(url, init);
532
+ }
533
+ catch (e) {
534
+ const m = e instanceof Error ? e.message : 'Network error';
535
+ throw new types_1.XCiteDBError(m, 0, null, { clientRequestId: outgoingRequestId });
536
+ }
481
537
  const text = await res.text();
482
538
  let data;
483
539
  try {
@@ -499,7 +555,7 @@ class XCiteDBClient {
499
555
  ? String(data.message)
500
556
  : res.statusText;
501
557
  this.notifySessionInvalidIfNeeded(path, res.status);
502
- throw new types_1.XCiteDBError(msg || `HTTP ${res.status}`, res.status, data);
558
+ throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
503
559
  }
504
560
  this.notifySessionInvalidIfNeeded(path, 401);
505
561
  throw new types_1.XCiteDBError('Request failed after retry', 401, null);
@@ -926,6 +982,12 @@ class XCiteDBClient {
926
982
  body.branch = branch;
927
983
  return this.request('POST', '/api/v1/security/check', body);
928
984
  }
985
+ /**
986
+ * Dry-run access (`POST /api/v1/security/check`). Same as {@link checkAccess}; use for debugging 403s.
987
+ */
988
+ async diagnoseAccess(subject, identifier, action, branch) {
989
+ return this.checkAccess(subject, identifier, action, branch);
990
+ }
929
991
  async getSecurityConfig() {
930
992
  return this.request('GET', '/api/v1/security/config');
931
993
  }
@@ -1680,9 +1742,11 @@ class XCiteDBClient {
1680
1742
  });
1681
1743
  for (let attempt = 0; attempt < 2; attempt++) {
1682
1744
  const url = joinUrl(this.baseUrl, path);
1745
+ const outgoingRequestId = newClientRequestId();
1683
1746
  const headers = {
1684
1747
  ...this.requestHeaders(),
1685
1748
  'Content-Type': 'application/json',
1749
+ 'X-Request-Id': outgoingRequestId,
1686
1750
  };
1687
1751
  const res = await fetch(url, { method: 'POST', headers, body: payload });
1688
1752
  if (res.status === 401 &&
@@ -1703,12 +1767,15 @@ class XCiteDBClient {
1703
1767
  ? String(data.message)
1704
1768
  : res.statusText;
1705
1769
  this.notifySessionInvalidIfNeeded(path, res.status);
1706
- throw new types_1.XCiteDBError(msg || `HTTP ${res.status}`, res.status, data);
1770
+ throwForFailedHttp(res.status, path, data, msg || `HTTP ${res.status}`, res.headers.get('X-Request-Id') ?? undefined, res.headers.get('X-Client-Request-Id') ?? undefined);
1707
1771
  }
1708
1772
  const streamBody = res.body;
1709
1773
  if (!streamBody) {
1710
1774
  const text = await res.text();
1711
- throw new types_1.XCiteDBError('RAG stream: empty response body', res.status, text);
1775
+ throw new types_1.XCiteDBError('RAG stream: empty response body', res.status, text, {
1776
+ serverRequestId: res.headers.get('X-Request-Id') ?? undefined,
1777
+ clientRequestId: res.headers.get('X-Client-Request-Id') ?? undefined,
1778
+ });
1712
1779
  }
1713
1780
  const reader = streamBody.getReader();
1714
1781
  const decoder = new TextDecoder();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
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
+ /**
7
+ * Unit tests for HTTP error mapping (mocked fetch).
8
+ */
9
+ const node_test_1 = require("node:test");
10
+ const strict_1 = __importDefault(require("node:assert/strict"));
11
+ const client_js_1 = require("./client.js");
12
+ const types_js_1 = require("./types.js");
13
+ (0, node_test_1.describe)('XCiteDBClient error mapping', () => {
14
+ (0, node_test_1.it)('403 becomes XCiteDBForbiddenError with policy_id and request ids', async () => {
15
+ const orig = globalThis.fetch;
16
+ globalThis.fetch = node_test_1.mock.fn(async () => {
17
+ return new Response(JSON.stringify({
18
+ message: 'Forbidden',
19
+ reason: 'tenant_security_unconfigured',
20
+ policy_id: 'abac_denied',
21
+ hint: 'add policies',
22
+ }), {
23
+ status: 403,
24
+ headers: {
25
+ 'X-Request-Id': 'srv-req-1',
26
+ 'X-Client-Request-Id': 'cli-req-1',
27
+ },
28
+ });
29
+ });
30
+ try {
31
+ const c = new client_js_1.XCiteDBClient({ baseUrl: 'http://127.0.0.1:9', apiKey: 'test-key' });
32
+ await c.health();
33
+ strict_1.default.fail('expected error');
34
+ }
35
+ catch (e) {
36
+ strict_1.default.ok(e instanceof types_js_1.XCiteDBForbiddenError);
37
+ const err = e;
38
+ strict_1.default.equal(err.policyId, 'abac_denied');
39
+ strict_1.default.equal(err.reason, 'tenant_security_unconfigured');
40
+ strict_1.default.equal(err.hint, 'add policies');
41
+ strict_1.default.equal(err.serverRequestId, 'srv-req-1');
42
+ strict_1.default.equal(err.clientRequestId, 'cli-req-1');
43
+ }
44
+ finally {
45
+ globalThis.fetch = orig;
46
+ }
47
+ });
48
+ });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { XCiteDBClient } from './client';
2
2
  export { WebSocketSubscription } from './websocket';
3
- export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, DocumentBatchResponse, DocumentBatchResultRow, Flags, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
- export { XCiteDBError } from './types';
3
+ export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, DocumentBatchResponse, DocumentBatchResultRow, Flags, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
+ export { XCiteDBError, XCiteDBForbiddenError, XCiteDBNotFoundError, XCiteDBAuthError, XCiteDBLockConflictError, } from './types';
package/dist/index.js CHANGED
@@ -1,9 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.XCiteDBError = exports.WebSocketSubscription = exports.XCiteDBClient = void 0;
3
+ exports.XCiteDBLockConflictError = exports.XCiteDBAuthError = exports.XCiteDBNotFoundError = exports.XCiteDBForbiddenError = exports.XCiteDBError = exports.WebSocketSubscription = 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
6
  var websocket_1 = require("./websocket");
7
7
  Object.defineProperty(exports, "WebSocketSubscription", { enumerable: true, get: function () { return websocket_1.WebSocketSubscription; } });
8
8
  var types_1 = require("./types");
9
9
  Object.defineProperty(exports, "XCiteDBError", { enumerable: true, get: function () { return types_1.XCiteDBError; } });
10
+ Object.defineProperty(exports, "XCiteDBForbiddenError", { enumerable: true, get: function () { return types_1.XCiteDBForbiddenError; } });
11
+ Object.defineProperty(exports, "XCiteDBNotFoundError", { enumerable: true, get: function () { return types_1.XCiteDBNotFoundError; } });
12
+ Object.defineProperty(exports, "XCiteDBAuthError", { enumerable: true, get: function () { return types_1.XCiteDBAuthError; } });
13
+ Object.defineProperty(exports, "XCiteDBLockConflictError", { enumerable: true, get: function () { return types_1.XCiteDBLockConflictError; } });
package/dist/types.d.ts CHANGED
@@ -504,6 +504,26 @@ export interface XCiteDBClientOptions {
504
504
  */
505
505
  requestTimeoutMs?: number;
506
506
  }
507
+ /** `bootstrap` payload for `POST /api/v1/test/sessions` (see server docs / `llms.txt`). */
508
+ export interface TestSessionBootstrap {
509
+ user_isolation?: {
510
+ enabled: boolean;
511
+ namespace_pattern: string;
512
+ shared_read_paths?: string[];
513
+ shared_write_paths?: string[];
514
+ };
515
+ developer_bypass?: boolean;
516
+ policies?: Array<{
517
+ policy_id: string;
518
+ policy: SecurityPolicy;
519
+ }>;
520
+ }
521
+ /** `bootstrap` summary returned by the server after a bootstrapped test session is created. */
522
+ export interface TestSessionBootstrapSummary {
523
+ user_isolation_applied?: boolean;
524
+ developer_bypass_applied?: boolean;
525
+ policies_created?: string[];
526
+ }
507
527
  /** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
508
528
  export interface CreateTestSessionOptions {
509
529
  baseUrl: string;
@@ -518,6 +538,8 @@ export interface CreateTestSessionOptions {
518
538
  * When true, creates an overlay test session: writable ephemeral LMDB with production project data as read-only base.
519
539
  */
520
540
  overlay?: boolean;
541
+ /** Optional server-side bootstrap (user isolation, developer_bypass, policies). */
542
+ bootstrap?: TestSessionBootstrap;
521
543
  /** Keep `apiKey` / `accessToken` on the client and send `X-Test-Auth: required` on each request. */
522
544
  testRequireAuth?: boolean;
523
545
  onSessionTokensUpdated?: (pair: TokenPair) => void;
@@ -677,6 +699,7 @@ export interface SecurityConfig {
677
699
  export interface AccessCheckResult {
678
700
  effect: 'allow' | 'deny';
679
701
  matched_policy_id?: string;
702
+ reason?: string;
680
703
  }
681
704
  export interface StoredPolicyResponse {
682
705
  policy_id: string;
@@ -826,8 +849,36 @@ export interface PublishResult {
826
849
  }
827
850
  /** @deprecated Use {@link PublishResult}. */
828
851
  export type MergeResult = PublishResult;
852
+ export type XCiteDBErrorExtras = {
853
+ reason?: string;
854
+ policyId?: string;
855
+ hint?: string;
856
+ expectedRole?: string;
857
+ actualRole?: string;
858
+ serverRequestId?: string;
859
+ clientRequestId?: string;
860
+ };
829
861
  export declare class XCiteDBError extends Error {
830
862
  readonly status: number;
831
863
  readonly body?: unknown | undefined;
832
- constructor(message: string, status: number, body?: unknown | undefined);
864
+ reason?: string;
865
+ policyId?: string;
866
+ hint?: string;
867
+ expectedRole?: string;
868
+ actualRole?: string;
869
+ serverRequestId?: string;
870
+ clientRequestId?: string;
871
+ constructor(message: string, status: number, body?: unknown | undefined, extras?: XCiteDBErrorExtras);
872
+ }
873
+ export declare class XCiteDBForbiddenError extends XCiteDBError {
874
+ constructor(message: string, status: number, body?: unknown, extras?: XCiteDBErrorExtras);
875
+ }
876
+ export declare class XCiteDBNotFoundError extends XCiteDBError {
877
+ constructor(message: string, status: number, body?: unknown, extras?: XCiteDBErrorExtras);
878
+ }
879
+ export declare class XCiteDBAuthError extends XCiteDBError {
880
+ constructor(message: string, status: number, body?: unknown, extras?: XCiteDBErrorExtras);
881
+ }
882
+ export declare class XCiteDBLockConflictError extends XCiteDBError {
883
+ constructor(message: string, status: number, body?: unknown, extras?: XCiteDBErrorExtras);
833
884
  }
package/dist/types.js CHANGED
@@ -1,12 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.XCiteDBError = void 0;
3
+ exports.XCiteDBLockConflictError = exports.XCiteDBAuthError = exports.XCiteDBNotFoundError = exports.XCiteDBForbiddenError = exports.XCiteDBError = void 0;
4
4
  class XCiteDBError extends Error {
5
- constructor(message, status, body) {
5
+ constructor(message, status, body, extras) {
6
6
  super(message);
7
7
  this.status = status;
8
8
  this.body = body;
9
9
  this.name = 'XCiteDBError';
10
+ if (extras) {
11
+ this.reason = extras.reason;
12
+ this.policyId = extras.policyId;
13
+ this.hint = extras.hint;
14
+ this.expectedRole = extras.expectedRole;
15
+ this.actualRole = extras.actualRole;
16
+ this.serverRequestId = extras.serverRequestId;
17
+ this.clientRequestId = extras.clientRequestId;
18
+ }
10
19
  }
11
20
  }
12
21
  exports.XCiteDBError = XCiteDBError;
22
+ class XCiteDBForbiddenError extends XCiteDBError {
23
+ constructor(message, status, body, extras) {
24
+ super(message, status, body, extras);
25
+ this.name = 'XCiteDBForbiddenError';
26
+ }
27
+ }
28
+ exports.XCiteDBForbiddenError = XCiteDBForbiddenError;
29
+ class XCiteDBNotFoundError extends XCiteDBError {
30
+ constructor(message, status, body, extras) {
31
+ super(message, status, body, extras);
32
+ this.name = 'XCiteDBNotFoundError';
33
+ }
34
+ }
35
+ exports.XCiteDBNotFoundError = XCiteDBNotFoundError;
36
+ class XCiteDBAuthError extends XCiteDBError {
37
+ constructor(message, status, body, extras) {
38
+ super(message, status, body, extras);
39
+ this.name = 'XCiteDBAuthError';
40
+ }
41
+ }
42
+ exports.XCiteDBAuthError = XCiteDBAuthError;
43
+ class XCiteDBLockConflictError extends XCiteDBError {
44
+ constructor(message, status, body, extras) {
45
+ super(message, status, body, extras);
46
+ this.name = 'XCiteDBLockConflictError';
47
+ }
48
+ }
49
+ exports.XCiteDBLockConflictError = XCiteDBLockConflictError;
package/llms-full.txt CHANGED
@@ -199,6 +199,19 @@ Browser and mobile apps can use OAuth2-style flows against `/api/v1/app/auth/oau
199
199
 
200
200
  ---
201
201
 
202
+ ## When a request returns 403
203
+
204
+ XCiteDB returns one of two 403 shapes; both include `reason`:
205
+
206
+ - Generic role/credential 403: `{ "error": "Error", "message": "Forbidden", "code": 403, "reason": "role_forbidden_*"|"auth_*" }`. The credential failed before ABAC ran. Verify that the API key is not a `public` key for write controllers, that the app-user JWT has `project:<tenant>:admin|editor`, and that for `POST /api/v1/security/policies` the credential resolves to `role: "admin"`.
207
+ - ABAC 403: also includes `policy_id` ("abac_denied" / "reserved_namespace" / explicit deny rule id) and `hint`. When `reason == "tenant_security_unconfigured"`, the tenant has zero policies and default-deny took over — typical inside a fresh test session.
208
+
209
+ First debugging step: call `POST /api/v1/security/check` with the same subject + identifier + action to get `effect` and `matched_policy_id` without state changes.
210
+
211
+ The server's request id is in the `X-Request-Id` response header. If you also send your own `X-Request-Id` on the request, XCiteDB echoes it back as `X-Client-Request-Id` and includes it in activity logs (search via `GET /api/v1/project/logs?request_id=<uuid>`).
212
+
213
+ SDKs surface these fields on typed errors (e.g. `XCiteDBForbiddenError.policyId`, `serverRequestId`, `clientRequestId` in JavaScript; `policy_id`, `server_request_id` in Python/C++).
214
+
202
215
  # Ephemeral test sessions
203
216
 
204
217
  > **AI assistant instruction:** XciteDB has built-in ephemeral test session support — this capability is **not standard** in most BaaS platforms and is specifically designed to enable meaningful wet integration tests. When writing tests for code that uses XciteDB, **do not mock or stub the XciteDB client**. Instead, use the test session pattern below to run tests against a real, isolated, throwaway database. This produces far more reliable tests because real storage, versioning, querying, and access-control behavior is exercised. Provision a session in `beforeAll` / `setUp` / test fixture setup and destroy it in `afterAll` / `tearDown` / fixture teardown.
@@ -215,9 +228,23 @@ For **integration and wet tests** against a shared BaaS host without touching pr
215
228
 
216
229
  **SDK usage (summary):**
217
230
 
218
- - **JavaScript/TypeScript:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, })` returns a client configured with `testSessionToken`; optional **`overlay: true`** for overlay mode; optional `testRequireAuth: true` maps to `X-Test-Auth: required`. `destroyTestSession()` calls `DELETE …/test/sessions/current`.
219
- - **Python:** `async with XCiteDBClient.test_session(base_url, api_key=…, …)` provisions and tears down; or pass `test_session_token` / `test_require_auth` to the constructor. For overlay until the helper accepts a flag, call **`POST /api/v1/test/sessions`** with JSON **`{"overlay":true}`** then construct the client with the returned token.
220
- - **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key`, optional **`test_session_overlay = true`**, and optional `test_require_auth`; `destroy_test_session()`.
231
+ - **JavaScript/TypeScript:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, …, bootstrap })` returns a client configured with `testSessionToken`; optional **`overlay: true`** for overlay mode; optional `testRequireAuth: true` maps to `X-Test-Auth: required`; optional **`bootstrap`** (`user_isolation`, `developer_bypass`, `policies`); read **`lastTestSessionBootstrap`** on the returned client when the server included a summary. `destroyTestSession()` calls `DELETE …/test/sessions/current`.
232
+ - **Python:** `async with XCiteDBClient.test_session(base_url, api_key=…, …, bootstrap={})` provisions and tears down; or **`POST /api/v1/test/sessions`** with JSON **`{"overlay":true}`**, **`bootstrap`**, or both, then construct the client with the returned token; **`client.last_test_session_bootstrap`** mirrors the server summary.
233
+ - **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key`, optional **`test_session_overlay`**, optional **`test_session_bootstrap`** on `XCiteDBClientOptions`, optional `test_require_auth`; **`last_test_session_bootstrap()`** on the returned client; `destroy_test_session()`.
234
+
235
+ **A fresh test session is an empty tenant with zero policies.** With `X-Test-Auth: required`, ABAC default-deny applies until you bootstrap user isolation and/or policies. Typical `POST /api/v1/test/sessions` body:
236
+
237
+ ```json
238
+ {
239
+ "bootstrap": {
240
+ "user_isolation": { "enabled": true, "namespace_pattern": "/spaces/${user.id}" },
241
+ "developer_bypass": true,
242
+ "policies": []
243
+ }
244
+ }
245
+ ```
246
+
247
+ Skipping bootstrap yields 403 with `reason: "tenant_security_unconfigured"` for app-user reads/writes under `/spaces/<userId>/...`.
221
248
 
222
249
  **JavaScript/TypeScript — complete test scaffold (Vitest / Jest):**
223
250
 
@@ -681,6 +708,15 @@ Operators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `in`, `not_in`, `contains`, `starts_
681
708
 
682
709
  SDK: `UnqueryTemplate` in `@xcitedbs/client` types; method `unquery(query, unquery)`.
683
710
 
711
+ ### `reason` / `policy_id` markers (403 debugging)
712
+
713
+ | reason | policy_id | Meaning |
714
+ |--------|------------|---------|
715
+ | `abac_explicit_deny` | &lt;your-rule-id&gt; | An explicit deny rule matched. |
716
+ | `abac_default_deny` | `abac_denied` | No allow policy matched; tenant `default_effect` (or `app_user_default_effect`) deny took over. |
717
+ | `tenant_security_unconfigured` | `abac_denied` | Same as default deny but the tenant has **zero** policies — usual for a fresh test session without bootstrap. |
718
+ | `abac_reserved_namespace` | `reserved_namespace` | App-user or public-key request touched `_xcitedb*` reserved identifiers. |
719
+
684
720
  ## ABAC policy expression language
685
721
 
686
722
  Policies may set **`conditions.expression`** (optional) and **`conditions.branches`** (optional array). If `branches` is non-empty, the request branch must match an entry (`"*"`, exact name, or `prefix*`).
@@ -1314,12 +1350,34 @@ For logged-in users (not only platform admins):
1314
1350
 
1315
1351
  ---
1316
1352
 
1353
+ ## Wrapping XCiteDB in a higher-level service
1354
+
1355
+ When you build a backend that calls XCiteDB on behalf of users:
1356
+
1357
+ - Never collapse upstream errors to a generic `xcitedb_upstream` code. Pass `body.policy_id`, `body.hint`, `body.reason`, `body.expected_role`, `body.actual_role` through your error wrapper. They are stable, low-cardinality, and safe to surface to test fixtures.
1358
+ - Capture `X-Request-Id` from XCiteDB's response and log it next to your own request id. Send your service's own request id as `X-Request-Id` on outgoing calls; XCiteDB will echo it as `X-Client-Request-Id`.
1359
+ - Distinguish service-key calls vs end-user-token calls in your error trace; tag the call site so a 403 from a user-token spine read and a 403 from an admin `createPolicy` are distinguishable.
1360
+ - Do not assume an end-user implicitly has read access to documents under `/spaces/<their_user_id>/....` That is only true when user isolation is enabled on the tenant with the matching `namespace_pattern`. In production tenants this is one-time setup; in test sessions use the bootstrap option on `POST /api/v1/test/sessions`.
1361
+
1317
1362
  # Part 3: SDK Reference
1318
1363
 
1319
1364
  # JavaScript/TypeScript SDK (`@xcitedbs/client`)
1320
1365
 
1321
1366
  Install: `npm install @xcitedbs/client`
1322
1367
 
1368
+ ### Typed 403 handling
1369
+
1370
+ ```typescript
1371
+ import { XCiteDBClient, XCiteDBForbiddenError } from '@xcitedbs/client';
1372
+ try {
1373
+ await client.readJsonDocument('/spaces/u1/docs/d/spine.json');
1374
+ } catch (e) {
1375
+ if (e instanceof XCiteDBForbiddenError) {
1376
+ console.error({ reason: e.reason, policyId: e.policyId, hint: e.hint, requestId: e.serverRequestId });
1377
+ }
1378
+ }
1379
+ ```
1380
+
1323
1381
  ## Quick Start
1324
1382
 
1325
1383
  ```typescript
@@ -1592,6 +1650,19 @@ class XCiteDBError extends Error {
1592
1650
 
1593
1651
  Install: `pip install xcitedb`
1594
1652
 
1653
+ ### Typed 403 handling
1654
+
1655
+ ```python
1656
+ from xcitedb import XCiteDBClient, XCiteDBForbiddenError
1657
+
1658
+ async def main():
1659
+ client = XCiteDBClient("http://localhost:8080", api_key="your-key")
1660
+ try:
1661
+ await client.read_json_document("/spaces/u1/docs/d/spine.json")
1662
+ except XCiteDBForbiddenError as e:
1663
+ print(e.reason, e.policy_id, e.hint, e.server_request_id)
1664
+ ```
1665
+
1595
1666
  ```python
1596
1667
  import asyncio
1597
1668
  from xcitedb import (
@@ -1644,6 +1715,21 @@ q.match_start = "/manual/";
1644
1715
  auto ids = client.query_documents(q);
1645
1716
  ```
1646
1717
 
1647
- Synchronous (blocking) HTTP client. Methods mirror the JavaScript SDK with C++ naming (`write_xml_document`, deprecated `write_document_json`). Errors throw `xcitedb::XCiteDBError` with `.status()` and `.body()`.
1718
+ Synchronous (blocking) HTTP client. Methods mirror the JavaScript SDK with C++ naming (`write_xml_document`, deprecated `write_document_json`). Errors throw `xcitedb::XCiteDBError` (or subclasses such as `xcitedb::XCiteDBForbiddenError`) with `.status()`, `.body()`, `.reason()`, `.policy_id()`, `.hint()`, `.server_request_id()`, and `.client_request_id()`.
1719
+
1720
+ ### Typed 403 handling
1721
+
1722
+ ```cpp
1723
+ #include <xcitedb/xcitedb.hpp>
1724
+ #include <iostream>
1725
+
1726
+ void example(xcitedb::XCiteDBClient& client) {
1727
+ try {
1728
+ client.read_json_document("/spaces/u1/docs/d/spine.json");
1729
+ } catch (const xcitedb::XCiteDBForbiddenError& e) {
1730
+ std::cerr << e.reason() << " " << e.policy_id() << " " << e.hint() << " " << e.server_request_id() << "\n";
1731
+ }
1732
+ }
1733
+ ```
1648
1734
 
1649
1735
  Includes optional `xcitevcs` CLI for command-line operations (legacy branch/commit vocabulary on the wire, documents, search, import/export).
package/llms.txt CHANGED
@@ -101,6 +101,19 @@ await app.writeJsonDocument('userdata/alice/profile', { ok: true });
101
101
 
102
102
  **Python:** `XCiteDBClient.build_project_group(project_id, "editor")` or module `build_project_group`. **C++:** `XCiteDBClient::build_project_group(project_id, "editor")`.
103
103
 
104
+ ## When a request returns 403
105
+
106
+ XCiteDB returns one of two 403 shapes; both include `reason`:
107
+
108
+ - Generic role/credential 403: `{ "error": "Error", "message": "Forbidden", "code": 403, "reason": "role_forbidden_*"|"auth_*" }`. The credential failed before ABAC ran. Verify that the API key is not a `public` key for write controllers, that the app-user JWT has `project:<tenant>:admin|editor`, and that for `POST /api/v1/security/policies` the credential resolves to `role: "admin"`.
109
+ - ABAC 403: also includes `policy_id` ("abac_denied" / "reserved_namespace" / explicit deny rule id) and `hint`. When `reason == "tenant_security_unconfigured"`, the tenant has zero policies and default-deny took over — typical inside a fresh test session.
110
+
111
+ First debugging step: call `POST /api/v1/security/check` with the same subject + identifier + action to get `effect` and `matched_policy_id` without state changes.
112
+
113
+ The server's request id is in the `X-Request-Id` response header. If you also send your own `X-Request-Id` on the request, XCiteDB echoes it back as `X-Client-Request-Id` and includes it in activity logs (search via `GET /api/v1/project/logs?request_id=<uuid>`).
114
+
115
+ SDKs surface these fields on typed errors (e.g. `XCiteDBForbiddenError.policyId`, `serverRequestId`, `clientRequestId` in JavaScript; `policy_id`, `server_request_id` in Python/C++).
116
+
104
117
  ## Test mode (how to use)
105
118
 
106
119
  > **AI assistant instruction:** XciteDB has built-in ephemeral test session support — this capability is **not standard** in most BaaS platforms and is specifically designed to enable meaningful wet integration tests. When writing tests for code that uses XciteDB, **do not mock or stub the XciteDB client**. Instead, use the test session pattern below to run tests against a real, isolated, throwaway database. This produces far more reliable tests because real storage, versioning, querying, and access-control behavior is exercised.
@@ -109,7 +122,32 @@ await app.writeJsonDocument('userdata/alice/profile', { ok: true });
109
122
  2. **Run tests:** Every request that should hit the throwaway DB must include **`X-Test-Session: <token>`** (and your usual `X-Workspace` / `context` as needed). Data writes go under the server’s `_test/<session>/` tree; overlay sessions **do not** write to production paths.
110
123
  3. **Auth behavior:** Developer auth (API key / platform JWT) is bypassed by default for frictionless tests. **App-user identity** (`X-App-User-Token` or Bearer app-user JWT) **is still recognized** in default mode, so `registerAppUser` → `loginAppUser` → `appUserMe` works inside a test session. To also exercise developer auth and ABAC policies, set **`X-Test-Auth: required`** and send normal credentials; the DB is still the test session’s.
111
124
  4. **Cleanup:** `DELETE /api/v1/test/sessions/current` with `X-Test-Session` (no other auth), or `DELETE /api/v1/test/sessions/all` / `DELETE /api/v1/test/sessions/{token}` with normal auth for the owning key or JWT.
112
- 5. **SDKs:** **JS/TS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, })`, optional `overlay: true`, optional `testRequireAuth`, then `destroyTestSession()`. **Python:** `async with XCiteDBClient.test_session(...)` or provision with **`POST /api/v1/test/sessions`** and JSON **`{"overlay":true}`** when you need overlay, then pass `test_session_token` / `test_require_auth` to the constructor. **C++:** `XCiteDBClient::create_test_session(options)` with optional `test_session_overlay = true`, `destroy_test_session()`, optional `test_require_auth` in options.
125
+ 5. **SDKs:** **JS/TS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, …, bootstrap })`, optional `overlay: true`, optional `testRequireAuth`, optional `bootstrap` (`user_isolation`, `developer_bypass`, `policies`), then read `lastTestSessionBootstrap` on the returned client; `destroyTestSession()`. **Python:** `async with XCiteDBClient.test_session(..., bootstrap={...})` or provision with **`POST /api/v1/test/sessions`** (JSON may include **`{"overlay":true}`** and/or **`bootstrap`**); use `client.last_test_session_bootstrap` after creation; pass `test_session_token` / `test_require_auth` to the constructor as before. **C++:** `XCiteDBClient::create_test_session(options)` with optional `test_session_overlay`, optional `test_session_bootstrap` on `XCiteDBClientOptions`, `last_test_session_bootstrap()` on the returned client, `destroy_test_session()`, optional `test_require_auth`.
126
+
127
+ **A fresh test session is an empty tenant with zero policies.** With `X-Test-Auth: required` the AuthFilter runs normally and ABAC is enforced against the test tenant — default-deny rejects every app-user read/write until you either enable user isolation or add explicit allow policies. The standard recipe is to use the bootstrap option on `POST /api/v1/test/sessions`:
128
+
129
+ ```json
130
+ {
131
+ "bootstrap": {
132
+ "user_isolation": { "enabled": true, "namespace_pattern": "/spaces/${user.id}" },
133
+ "developer_bypass": true,
134
+ "policies": []
135
+ }
136
+ }
137
+ ```
138
+
139
+ If you skip the bootstrap, an app-user JWT cannot read or write `/spaces/<userId>/...` — even paths the user "owns" — and you will see 403 with reason `tenant_security_unconfigured`.
140
+
141
+ **SDK one-liners for bootstrap:** **JS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, testRequireAuth: true, bootstrap: { user_isolation: { enabled: true, namespace_pattern: '/spaces/${user.id}' }, developer_bypass: true } })`. **Python:** `async with XCiteDBClient.test_session(base_url, api_key=sk, test_require_auth=True, bootstrap={...}) as c:`. **C++:** set `options.test_session_bootstrap = nlohmann::json::parse(R"(...)");` then `XCiteDBClient::create_test_session(options)`.
142
+
143
+ ## Wrapping XCiteDB in a higher-level service
144
+
145
+ When you build a backend that calls XCiteDB on behalf of users:
146
+
147
+ - Never collapse upstream errors to a generic `xcitedb_upstream` code. Pass `body.policy_id`, `body.hint`, `body.reason`, `body.expected_role`, `body.actual_role` through your error wrapper. They are stable, low-cardinality, and safe to surface to test fixtures.
148
+ - Capture `X-Request-Id` from XCiteDB's response and log it next to your own request id. Send your service's own request id as `X-Request-Id` on outgoing calls; XCiteDB will echo it as `X-Client-Request-Id`.
149
+ - Distinguish service-key calls vs end-user-token calls in your error trace; tag the call site so a 403 from a user-token spine read and a 403 from an admin `createPolicy` are distinguishable.
150
+ - Do not assume an end-user implicitly has read access to documents under `/spaces/<their_user_id>/....` That is only true when user isolation is enabled on the tenant with the matching `namespace_pattern`. In production tenants this is one-time setup; in test sessions use the bootstrap option.
113
151
 
114
152
  ## JSON documents and metadata (merge, overwrite, efficiency)
115
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",