@xcitedbs/client 0.2.17 → 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 +25 -2
- package/dist/client.js +91 -8
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +48 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -1
- package/dist/locks.test.js +1 -2
- package/dist/types.d.ts +69 -2
- package/dist/types.js +39 -2
- package/llms-full.txt +90 -4
- package/llms.txt +39 -1
- package/package.json +1 -1
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
|
|
@@ -422,10 +428,13 @@ export declare class XCiteDBClient {
|
|
|
422
428
|
/**
|
|
423
429
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
424
430
|
* For storing JSON data by key, use `writeJsonDocument`.
|
|
431
|
+
*
|
|
432
|
+
* On cooperative-lock conflict the server returns **423** with a {@link LockConflictBody} shape in {@link XCiteDBError.body}.
|
|
425
433
|
*/
|
|
426
434
|
writeXmlDocument(xml: string, options?: WriteDocumentOptions): Promise<void>;
|
|
427
435
|
/**
|
|
428
436
|
* Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
|
|
437
|
+
* Rows with `error === 'lock_conflict'` include `current_lock` (code **423** per row).
|
|
429
438
|
*/
|
|
430
439
|
writeXmlDocumentsBatch(items: XmlDocumentBatchItem[]): Promise<DocumentBatchResponse>;
|
|
431
440
|
/**
|
|
@@ -465,14 +474,28 @@ export declare class XCiteDBClient {
|
|
|
465
474
|
* {@link LockConflictBody} body (thrown as {@link XCiteDBError} with `.status === 409`).
|
|
466
475
|
*/
|
|
467
476
|
acquireLock(identifier: string, expiresOrOpts?: number | AcquireLockOptions): Promise<LockInfo>;
|
|
477
|
+
/**
|
|
478
|
+
* Release a lock. **404** with {@link LockUnknownBody} if the lock id is unknown; **410** with
|
|
479
|
+
* {@link LockExpiredBody} if the lock TTL expired and was garbage-collected (same server process hint).
|
|
480
|
+
*/
|
|
468
481
|
releaseLock(identifier: string, lockId: string): Promise<boolean>;
|
|
469
482
|
/**
|
|
470
483
|
* Force-release a lock using `force_unlock` ABAC authority. Pass the **raw** stored identifier
|
|
471
484
|
* from {@link findLocks} (no iso-prefix); often the path as returned by the server for another principal.
|
|
472
485
|
*/
|
|
473
486
|
forceReleaseLock(identifier: string, lockId: string): Promise<boolean>;
|
|
487
|
+
/**
|
|
488
|
+
* Extend lock TTL (holder must match). **404** {@link LockUnknownBody} / **410** {@link LockExpiredBody}
|
|
489
|
+
* behave like {@link releaseLock}.
|
|
490
|
+
*/
|
|
474
491
|
extendLock(identifier: string, lockId: string, ttlSeconds: number): Promise<LockInfo>;
|
|
475
|
-
|
|
492
|
+
/**
|
|
493
|
+
* List locks visible under the given path. Pass `metaId` style paths (e.g. `/spaces/owner/docs/docId`).
|
|
494
|
+
* With `{ asPrefix: true }`, calls `GET /api/v1/locks?prefix=...` (same truncation rules as `identifier`).
|
|
495
|
+
*/
|
|
496
|
+
findLocks(identifier: string, opts?: {
|
|
497
|
+
asPrefix?: boolean;
|
|
498
|
+
}): Promise<LockInfo[]>;
|
|
476
499
|
/**
|
|
477
500
|
* Run Unquery (`POST /api/v1/unquery`): declarative analytics over documents matching `query`.
|
|
478
501
|
* The `unquery` argument is a JSON template; keys are output fields, string values are expressions.
|
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
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1194,6 +1256,8 @@ class XCiteDBClient {
|
|
|
1194
1256
|
/**
|
|
1195
1257
|
* Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
|
|
1196
1258
|
* For storing JSON data by key, use `writeJsonDocument`.
|
|
1259
|
+
*
|
|
1260
|
+
* On cooperative-lock conflict the server returns **423** with a {@link LockConflictBody} shape in {@link XCiteDBError.body}.
|
|
1197
1261
|
*/
|
|
1198
1262
|
async writeXmlDocument(xml, options) {
|
|
1199
1263
|
await this.request('POST', '/api/v1/documents', {
|
|
@@ -1204,6 +1268,7 @@ class XCiteDBClient {
|
|
|
1204
1268
|
}
|
|
1205
1269
|
/**
|
|
1206
1270
|
* Best-effort batch XML writes (`POST /api/v1/documents/batch`). Each item is independent; check `results[].ok`.
|
|
1271
|
+
* Rows with `error === 'lock_conflict'` include `current_lock` (code **423** per row).
|
|
1207
1272
|
*/
|
|
1208
1273
|
async writeXmlDocumentsBatch(items) {
|
|
1209
1274
|
const body = {
|
|
@@ -1443,6 +1508,10 @@ class XCiteDBClient {
|
|
|
1443
1508
|
const lock = await this.request('POST', '/api/v1/locks', body);
|
|
1444
1509
|
return { ...lock, identifier: this.isoUnprefixId(lock.identifier) };
|
|
1445
1510
|
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Release a lock. **404** with {@link LockUnknownBody} if the lock id is unknown; **410** with
|
|
1513
|
+
* {@link LockExpiredBody} if the lock TTL expired and was garbage-collected (same server process hint).
|
|
1514
|
+
*/
|
|
1446
1515
|
async releaseLock(identifier, lockId) {
|
|
1447
1516
|
const r = await this.request('DELETE', '/api/v1/locks', {
|
|
1448
1517
|
identifier: this.isoPrefixId(identifier),
|
|
@@ -1462,6 +1531,10 @@ class XCiteDBClient {
|
|
|
1462
1531
|
});
|
|
1463
1532
|
return r?.ok !== false;
|
|
1464
1533
|
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Extend lock TTL (holder must match). **404** {@link LockUnknownBody} / **410** {@link LockExpiredBody}
|
|
1536
|
+
* behave like {@link releaseLock}.
|
|
1537
|
+
*/
|
|
1465
1538
|
async extendLock(identifier, lockId, ttlSeconds) {
|
|
1466
1539
|
const lock = await this.request('PATCH', '/api/v1/locks', {
|
|
1467
1540
|
identifier: this.isoPrefixId(identifier),
|
|
@@ -1470,8 +1543,13 @@ class XCiteDBClient {
|
|
|
1470
1543
|
});
|
|
1471
1544
|
return { ...lock, identifier: this.isoUnprefixId(lock.identifier) };
|
|
1472
1545
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1546
|
+
/**
|
|
1547
|
+
* List locks visible under the given path. Pass `metaId` style paths (e.g. `/spaces/owner/docs/docId`).
|
|
1548
|
+
* With `{ asPrefix: true }`, calls `GET /api/v1/locks?prefix=...` (same truncation rules as `identifier`).
|
|
1549
|
+
*/
|
|
1550
|
+
async findLocks(identifier, opts) {
|
|
1551
|
+
const key = opts?.asPrefix ? 'prefix' : 'identifier';
|
|
1552
|
+
const locks = await this.request('GET', `/api/v1/locks${buildQuery({ [key]: this.isoPrefixId(identifier) })}`);
|
|
1475
1553
|
return Array.isArray(locks)
|
|
1476
1554
|
? locks.map((L) => ({ ...L, identifier: this.isoUnprefixId(L.identifier) }))
|
|
1477
1555
|
: locks;
|
|
@@ -1664,9 +1742,11 @@ class XCiteDBClient {
|
|
|
1664
1742
|
});
|
|
1665
1743
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1666
1744
|
const url = joinUrl(this.baseUrl, path);
|
|
1745
|
+
const outgoingRequestId = newClientRequestId();
|
|
1667
1746
|
const headers = {
|
|
1668
1747
|
...this.requestHeaders(),
|
|
1669
1748
|
'Content-Type': 'application/json',
|
|
1749
|
+
'X-Request-Id': outgoingRequestId,
|
|
1670
1750
|
};
|
|
1671
1751
|
const res = await fetch(url, { method: 'POST', headers, body: payload });
|
|
1672
1752
|
if (res.status === 401 &&
|
|
@@ -1687,12 +1767,15 @@ class XCiteDBClient {
|
|
|
1687
1767
|
? String(data.message)
|
|
1688
1768
|
: res.statusText;
|
|
1689
1769
|
this.notifySessionInvalidIfNeeded(path, res.status);
|
|
1690
|
-
|
|
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);
|
|
1691
1771
|
}
|
|
1692
1772
|
const streamBody = res.body;
|
|
1693
1773
|
if (!streamBody) {
|
|
1694
1774
|
const text = await res.text();
|
|
1695
|
-
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
|
+
});
|
|
1696
1779
|
}
|
|
1697
1780
|
const reader = streamBody.getReader();
|
|
1698
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, 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/locks.test.js
CHANGED
|
@@ -26,7 +26,6 @@ async function openTestSession(e) {
|
|
|
26
26
|
'Content-Type': 'application/json',
|
|
27
27
|
Authorization: `Bearer ${e.accessToken}`,
|
|
28
28
|
'X-Project-Id': e.tenantId,
|
|
29
|
-
'X-Branch': 'main',
|
|
30
29
|
},
|
|
31
30
|
body: '{}',
|
|
32
31
|
});
|
|
@@ -39,7 +38,7 @@ async function openTestSession(e) {
|
|
|
39
38
|
accessToken: e.accessToken,
|
|
40
39
|
platformConsole: true,
|
|
41
40
|
projectId: e.tenantId,
|
|
42
|
-
context: {
|
|
41
|
+
context: { project_id: e.tenantId },
|
|
43
42
|
testSessionToken: data.session_token,
|
|
44
43
|
testRequireAuth: true,
|
|
45
44
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -267,11 +267,25 @@ export interface AcquireLockOptions {
|
|
|
267
267
|
/** Max milliseconds to wait when another exclusive lock is held. */
|
|
268
268
|
waitMs?: number;
|
|
269
269
|
}
|
|
270
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* Body returned with HTTP **409** on `acquireLock` conflict, or **423** on XML document write blocked by a lock.
|
|
272
|
+
* Inspect via {@link XCiteDBError} (`status` and `body`).
|
|
273
|
+
*/
|
|
271
274
|
export interface LockConflictBody {
|
|
272
275
|
error: 'lock_conflict';
|
|
273
276
|
current_lock: LockInfo;
|
|
274
277
|
}
|
|
278
|
+
/** Body returned with HTTP **410** when `extendLock` / `releaseLock` target a lock id that TTL-expired recently. */
|
|
279
|
+
export interface LockExpiredBody {
|
|
280
|
+
error: 'lock_expired';
|
|
281
|
+
message?: string;
|
|
282
|
+
expired_at?: number;
|
|
283
|
+
}
|
|
284
|
+
/** Body returned with HTTP **404** when the lock id does not exist (or never existed) for the given identifier. */
|
|
285
|
+
export interface LockUnknownBody {
|
|
286
|
+
error: 'lock_unknown';
|
|
287
|
+
message?: string;
|
|
288
|
+
}
|
|
275
289
|
export interface TokenPair {
|
|
276
290
|
access_token: string;
|
|
277
291
|
refresh_token: string;
|
|
@@ -392,6 +406,8 @@ export interface DocumentBatchResultRow {
|
|
|
392
406
|
ok: boolean;
|
|
393
407
|
error?: string;
|
|
394
408
|
code?: number;
|
|
409
|
+
/** Present when `error === 'lock_conflict'` (HTTP-style code 423 in batch row). */
|
|
410
|
+
current_lock?: LockInfo;
|
|
395
411
|
}
|
|
396
412
|
export interface DocumentBatchResponse {
|
|
397
413
|
results: DocumentBatchResultRow[];
|
|
@@ -488,6 +504,26 @@ export interface XCiteDBClientOptions {
|
|
|
488
504
|
*/
|
|
489
505
|
requestTimeoutMs?: number;
|
|
490
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
|
+
}
|
|
491
527
|
/** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
|
|
492
528
|
export interface CreateTestSessionOptions {
|
|
493
529
|
baseUrl: string;
|
|
@@ -502,6 +538,8 @@ export interface CreateTestSessionOptions {
|
|
|
502
538
|
* When true, creates an overlay test session: writable ephemeral LMDB with production project data as read-only base.
|
|
503
539
|
*/
|
|
504
540
|
overlay?: boolean;
|
|
541
|
+
/** Optional server-side bootstrap (user isolation, developer_bypass, policies). */
|
|
542
|
+
bootstrap?: TestSessionBootstrap;
|
|
505
543
|
/** Keep `apiKey` / `accessToken` on the client and send `X-Test-Auth: required` on each request. */
|
|
506
544
|
testRequireAuth?: boolean;
|
|
507
545
|
onSessionTokensUpdated?: (pair: TokenPair) => void;
|
|
@@ -661,6 +699,7 @@ export interface SecurityConfig {
|
|
|
661
699
|
export interface AccessCheckResult {
|
|
662
700
|
effect: 'allow' | 'deny';
|
|
663
701
|
matched_policy_id?: string;
|
|
702
|
+
reason?: string;
|
|
664
703
|
}
|
|
665
704
|
export interface StoredPolicyResponse {
|
|
666
705
|
policy_id: string;
|
|
@@ -810,8 +849,36 @@ export interface PublishResult {
|
|
|
810
849
|
}
|
|
811
850
|
/** @deprecated Use {@link PublishResult}. */
|
|
812
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
|
+
};
|
|
813
861
|
export declare class XCiteDBError extends Error {
|
|
814
862
|
readonly status: number;
|
|
815
863
|
readonly body?: unknown | undefined;
|
|
816
|
-
|
|
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);
|
|
817
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,
|
|
219
|
-
- **Python:** `async with XCiteDBClient.test_session(base_url, api_key=…, …)` provisions and tears down; or
|
|
220
|
-
- **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key`, optional **`test_session_overlay
|
|
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` | <your-rule-id> | 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()
|
|
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,
|
|
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
|
|