@xcitedbs/client 0.2.9 → 0.2.10
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 +54 -1
- package/dist/client.js +427 -62
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +153 -0
- package/dist/user-isolation.test.d.ts +1 -0
- package/dist/user-isolation.test.js +175 -0
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +4 -2
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, CommitRecord, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, XCiteQuery } from './types';
|
|
1
|
+
import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, CommitRecord, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, XCiteQuery, UserIsolationConfig } from './types';
|
|
2
2
|
import { WebSocketSubscription } from './websocket';
|
|
3
3
|
export declare class XCiteDBClient {
|
|
4
4
|
private baseUrl;
|
|
@@ -16,6 +16,8 @@ export declare class XCiteDBClient {
|
|
|
16
16
|
private onSessionInvalid?;
|
|
17
17
|
private testSessionToken?;
|
|
18
18
|
private testRequireAuth?;
|
|
19
|
+
private userIsolation?;
|
|
20
|
+
private cachedAppUserId?;
|
|
19
21
|
constructor(options: XCiteDBClientOptions);
|
|
20
22
|
/**
|
|
21
23
|
* Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
|
|
@@ -33,6 +35,19 @@ export declare class XCiteDBClient {
|
|
|
33
35
|
* Use for debugging ABAC (compare `tenant_id` and `groups` to `project:<…>:role` in policies).
|
|
34
36
|
*/
|
|
35
37
|
getTokenClaims(): XCiteDBJwtClaims | null;
|
|
38
|
+
private cacheAppUserIdFromPair;
|
|
39
|
+
private clearAppUserIdCache;
|
|
40
|
+
private getAppUserId;
|
|
41
|
+
private normalizeIsolationNamespaceTemplate;
|
|
42
|
+
private canonicalId;
|
|
43
|
+
/** Resolved namespace root, e.g. `/users/abc`, or `null` if isolation is off or no app user id. */
|
|
44
|
+
private userIsolationNamespace;
|
|
45
|
+
private allSharedPassthroughPrefixes;
|
|
46
|
+
private pathMatchesSharedPassthrough;
|
|
47
|
+
private isoPrefixId;
|
|
48
|
+
private isoUnprefixId;
|
|
49
|
+
private isoPrefixQuery;
|
|
50
|
+
private isoApplyXmlDbIdentifier;
|
|
36
51
|
/** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
|
|
37
52
|
destroyTestSession(): Promise<{
|
|
38
53
|
message: string;
|
|
@@ -57,6 +72,7 @@ export declare class XCiteDBClient {
|
|
|
57
72
|
private mergeAppTenant;
|
|
58
73
|
private authHeaders;
|
|
59
74
|
private testHeaders;
|
|
75
|
+
private requestHeaders;
|
|
60
76
|
private request;
|
|
61
77
|
/** Developer Bearer refresh first, then app-user refresh (no API key refresh). */
|
|
62
78
|
private tryRefreshSessionAfter401;
|
|
@@ -266,6 +282,17 @@ export declare class XCiteDBClient {
|
|
|
266
282
|
* ```
|
|
267
283
|
*/
|
|
268
284
|
updateSecurityConfig(config: Partial<SecurityConfig>): Promise<void>;
|
|
285
|
+
/** Per-tenant user data spaces (`GET /api/v1/security/user-isolation`). Requires security admin. */
|
|
286
|
+
getUserIsolationConfig(): Promise<UserIsolationConfig>;
|
|
287
|
+
/** Enable or reconfigure user isolation (`PUT /api/v1/security/user-isolation`). */
|
|
288
|
+
setUserIsolationConfig(config: Partial<UserIsolationConfig>): Promise<UserIsolationConfig>;
|
|
289
|
+
/** Disable user isolation and remove generated policies (`DELETE /api/v1/security/user-isolation`). */
|
|
290
|
+
disableUserIsolation(): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Loads server isolation config; when enabled, configures client-side identifier prefixing to match
|
|
293
|
+
* the server (namespace + shared paths). Does not send `X-Prefix`; identifiers in requests are rewritten.
|
|
294
|
+
*/
|
|
295
|
+
enableUserIsolation(): Promise<UserIsolationConfig>;
|
|
269
296
|
createBranch(name: string, fromBranch?: string, fromDate?: string): Promise<void>;
|
|
270
297
|
deleteBranch(name: string): Promise<void>;
|
|
271
298
|
deleteRevision(branch: string, date: string): Promise<void>;
|
|
@@ -386,6 +413,32 @@ export declare class XCiteDBClient {
|
|
|
386
413
|
status: string;
|
|
387
414
|
message: string;
|
|
388
415
|
}>;
|
|
416
|
+
reembedVector(): Promise<{
|
|
417
|
+
status: string;
|
|
418
|
+
message: string;
|
|
419
|
+
}>;
|
|
420
|
+
/** Project search / FTS / vector / LLM configuration (`GET /api/v1/project/settings/search`). */
|
|
421
|
+
getProjectSearchSettings(): Promise<ProjectSearchSettings>;
|
|
422
|
+
/** Update project search settings (`PUT /api/v1/project/settings/search`). Returns the same shape as GET. */
|
|
423
|
+
updateProjectSearchSettings(patch: ProjectSearchSettingsUpdate): Promise<ProjectSearchSettings>;
|
|
424
|
+
/** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
|
|
425
|
+
getVectorIndexEstimate(): Promise<VectorIndexEstimate>;
|
|
426
|
+
/** Start background estimate (202); cancel prior session for this tenant. */
|
|
427
|
+
postVectorIndexEstimateSession(): Promise<VectorIndexEstimate>;
|
|
428
|
+
getVectorIndexEstimateSession(sessionId: string): Promise<VectorIndexEstimate>;
|
|
429
|
+
deleteVectorIndexEstimateSession(sessionId: string): Promise<{
|
|
430
|
+
status?: string;
|
|
431
|
+
}>;
|
|
432
|
+
/**
|
|
433
|
+
* RAG over indexed documents (`POST /api/v1/rag/query` with JSON body).
|
|
434
|
+
* Requires LLM completion; embedding required when retrieval uses semantic or hybrid.
|
|
435
|
+
*/
|
|
436
|
+
ragQuery(options: Omit<RagQueryOptions, 'stream'>): Promise<RagQueryResult>;
|
|
437
|
+
/**
|
|
438
|
+
* Streaming RAG (`POST /api/v1/rag/query` with `stream: true`). Parses SSE `data: {...}` lines.
|
|
439
|
+
* The final event has `done: true` and may include `sources`.
|
|
440
|
+
*/
|
|
441
|
+
ragQueryStream(options: Omit<RagQueryOptions, 'stream'>, onEvent: (ev: RagStreamEvent) => void): Promise<void>;
|
|
389
442
|
writeJsonDocument(identifier: string, data: unknown): Promise<void>;
|
|
390
443
|
readJsonDocument<T = unknown>(identifier: string): Promise<T>;
|
|
391
444
|
deleteJsonDocument(identifier: string): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -33,6 +33,7 @@ class XCiteDBClient {
|
|
|
33
33
|
this.onSessionInvalid = options.onSessionInvalid;
|
|
34
34
|
this.testSessionToken = options.testSessionToken;
|
|
35
35
|
this.testRequireAuth = options.testRequireAuth === true;
|
|
36
|
+
this.userIsolation = options.userIsolation;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
38
39
|
* Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
|
|
@@ -51,6 +52,7 @@ class XCiteDBClient {
|
|
|
51
52
|
onSessionTokensUpdated: opts.onSessionTokensUpdated,
|
|
52
53
|
onAppUserTokensUpdated: opts.onAppUserTokensUpdated,
|
|
53
54
|
onSessionInvalid: opts.onSessionInvalid,
|
|
55
|
+
userIsolation: opts.userIsolation,
|
|
54
56
|
});
|
|
55
57
|
const data = await temp.request('POST', '/api/v1/test/sessions', undefined, undefined, { no401Retry: true });
|
|
56
58
|
return new XCiteDBClient({
|
|
@@ -67,6 +69,7 @@ class XCiteDBClient {
|
|
|
67
69
|
onSessionInvalid: opts.onSessionInvalid,
|
|
68
70
|
testSessionToken: data.session_token,
|
|
69
71
|
testRequireAuth: opts.testRequireAuth,
|
|
72
|
+
userIsolation: opts.userIsolation,
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
@@ -144,6 +147,154 @@ class XCiteDBClient {
|
|
|
144
147
|
jti: typeof jti === 'string' ? jti : undefined,
|
|
145
148
|
};
|
|
146
149
|
}
|
|
150
|
+
cacheAppUserIdFromPair(pair) {
|
|
151
|
+
if (pair?.user?.user_id) {
|
|
152
|
+
this.cachedAppUserId = pair.user.user_id;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
clearAppUserIdCache() {
|
|
156
|
+
this.cachedAppUserId = undefined;
|
|
157
|
+
}
|
|
158
|
+
getAppUserId() {
|
|
159
|
+
if (this.cachedAppUserId) {
|
|
160
|
+
return this.cachedAppUserId;
|
|
161
|
+
}
|
|
162
|
+
const raw = this.appUserAccessToken;
|
|
163
|
+
if (!raw) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const p = XCiteDBClient.decodeJwtPayloadJson(raw);
|
|
167
|
+
if (!p) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const sub = p['sub'];
|
|
171
|
+
if (typeof sub === 'string' && sub.length > 0) {
|
|
172
|
+
return sub;
|
|
173
|
+
}
|
|
174
|
+
const uid = p['user_id'];
|
|
175
|
+
if (typeof uid === 'string' && uid.length > 0) {
|
|
176
|
+
return uid;
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
normalizeIsolationNamespaceTemplate(tpl) {
|
|
181
|
+
return tpl.replace(/\$\{user\.id\}/g, '{userId}');
|
|
182
|
+
}
|
|
183
|
+
canonicalId(s) {
|
|
184
|
+
let t = s.trim().replace(/\/+/g, '/');
|
|
185
|
+
if (!t.startsWith('/')) {
|
|
186
|
+
t = `/${t}`;
|
|
187
|
+
}
|
|
188
|
+
return t;
|
|
189
|
+
}
|
|
190
|
+
/** Resolved namespace root, e.g. `/users/abc`, or `null` if isolation is off or no app user id. */
|
|
191
|
+
userIsolationNamespace() {
|
|
192
|
+
if (!this.userIsolation?.enabled) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const uid = this.getAppUserId();
|
|
196
|
+
if (!uid) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const rawTpl = this.normalizeIsolationNamespaceTemplate(this.userIsolation.namespace ?? '/users/{userId}');
|
|
200
|
+
const trimmed = rawTpl.replace(/\/+$/, '') || '';
|
|
201
|
+
if (!trimmed) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return trimmed.replace(/{userId}/g, uid).replace(/{user_id}/g, uid);
|
|
205
|
+
}
|
|
206
|
+
allSharedPassthroughPrefixes() {
|
|
207
|
+
const o = this.userIsolation;
|
|
208
|
+
if (!o) {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return [...(o.shared_read_paths ?? []), ...(o.shared_write_paths ?? [])];
|
|
212
|
+
}
|
|
213
|
+
pathMatchesSharedPassthrough(path) {
|
|
214
|
+
const canon = this.canonicalId(path);
|
|
215
|
+
for (const raw of this.allSharedPassthroughPrefixes()) {
|
|
216
|
+
if (!raw) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const p = this.canonicalId(raw);
|
|
220
|
+
if (canon === p) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (p.endsWith('/') && canon.startsWith(p)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (!p.endsWith('/') && (canon === p || canon.startsWith(`${p}/`))) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
isoPrefixId(id) {
|
|
233
|
+
const ns = this.userIsolationNamespace();
|
|
234
|
+
if (!ns) {
|
|
235
|
+
return id;
|
|
236
|
+
}
|
|
237
|
+
if (id.includes('..')) {
|
|
238
|
+
throw new types_1.XCiteDBError('Invalid identifier: path traversal not allowed', 400, null);
|
|
239
|
+
}
|
|
240
|
+
const canonical = this.canonicalId(id);
|
|
241
|
+
if (canonical.startsWith(`${ns}/`) || canonical === ns) {
|
|
242
|
+
return canonical;
|
|
243
|
+
}
|
|
244
|
+
if (this.pathMatchesSharedPassthrough(canonical)) {
|
|
245
|
+
return canonical;
|
|
246
|
+
}
|
|
247
|
+
if (canonical.startsWith('/users/') && !canonical.startsWith(`${ns}/`) && canonical !== ns) {
|
|
248
|
+
return canonical;
|
|
249
|
+
}
|
|
250
|
+
const combined = `${ns}${canonical === '/' ? '/' : canonical}`;
|
|
251
|
+
const finalId = combined.replace(/\/+/g, '/');
|
|
252
|
+
if (!finalId.startsWith(ns) || (finalId.length > ns.length && finalId.charAt(ns.length) !== '/')) {
|
|
253
|
+
throw new types_1.XCiteDBError('Identifier escapes user namespace', 400, null);
|
|
254
|
+
}
|
|
255
|
+
return finalId;
|
|
256
|
+
}
|
|
257
|
+
isoUnprefixId(id) {
|
|
258
|
+
const ns = this.userIsolationNamespace();
|
|
259
|
+
if (!ns) {
|
|
260
|
+
return id;
|
|
261
|
+
}
|
|
262
|
+
const canonical = this.canonicalId(id);
|
|
263
|
+
if (this.pathMatchesSharedPassthrough(canonical)) {
|
|
264
|
+
return canonical;
|
|
265
|
+
}
|
|
266
|
+
if (canonical.startsWith(`${ns}/`)) {
|
|
267
|
+
return canonical.slice(ns.length);
|
|
268
|
+
}
|
|
269
|
+
if (canonical === ns) {
|
|
270
|
+
return '/';
|
|
271
|
+
}
|
|
272
|
+
return canonical;
|
|
273
|
+
}
|
|
274
|
+
isoPrefixQuery(q) {
|
|
275
|
+
if (!this.userIsolationNamespace()) {
|
|
276
|
+
return q;
|
|
277
|
+
}
|
|
278
|
+
const out = { ...q };
|
|
279
|
+
if (out.match) {
|
|
280
|
+
out.match = this.isoPrefixId(out.match);
|
|
281
|
+
}
|
|
282
|
+
if (out.match_start) {
|
|
283
|
+
out.match_start = this.isoPrefixId(out.match_start);
|
|
284
|
+
}
|
|
285
|
+
if (out.match_end) {
|
|
286
|
+
out.match_end = this.isoPrefixId(out.match_end);
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
isoApplyXmlDbIdentifier(xml) {
|
|
291
|
+
if (!this.userIsolationNamespace()) {
|
|
292
|
+
return xml;
|
|
293
|
+
}
|
|
294
|
+
return xml.replace(/(db:identifier\s*=\s*")([^"]*)(")/, (_m, p1, mid, p3) => {
|
|
295
|
+
return `${p1}${this.isoPrefixId(String(mid))}${p3}`;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
147
298
|
/** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
|
|
148
299
|
async destroyTestSession() {
|
|
149
300
|
if (!this.testSessionToken) {
|
|
@@ -206,10 +357,12 @@ class XCiteDBClient {
|
|
|
206
357
|
this.appUserAccessToken = access;
|
|
207
358
|
if (refresh !== undefined)
|
|
208
359
|
this.appUserRefreshToken = refresh;
|
|
360
|
+
this.clearAppUserIdCache();
|
|
209
361
|
}
|
|
210
362
|
clearAppUserTokens() {
|
|
211
363
|
this.appUserAccessToken = undefined;
|
|
212
364
|
this.appUserRefreshToken = undefined;
|
|
365
|
+
this.clearAppUserIdCache();
|
|
213
366
|
}
|
|
214
367
|
contextHeaders() {
|
|
215
368
|
const h = {};
|
|
@@ -261,14 +414,19 @@ class XCiteDBClient {
|
|
|
261
414
|
}
|
|
262
415
|
return h;
|
|
263
416
|
}
|
|
417
|
+
requestHeaders() {
|
|
418
|
+
return {
|
|
419
|
+
...this.authHeaders(),
|
|
420
|
+
...this.contextHeaders(),
|
|
421
|
+
...this.testHeaders(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
264
424
|
async request(method, path, body, extraHeaders, opts) {
|
|
265
425
|
const no401Retry = opts?.no401Retry === true;
|
|
266
426
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
267
427
|
const url = joinUrl(this.baseUrl, path);
|
|
268
428
|
const headers = {
|
|
269
|
-
...this.
|
|
270
|
-
...this.contextHeaders(),
|
|
271
|
-
...this.testHeaders(),
|
|
429
|
+
...this.requestHeaders(),
|
|
272
430
|
...extraHeaders,
|
|
273
431
|
};
|
|
274
432
|
let init = { method, headers };
|
|
@@ -336,6 +494,7 @@ class XCiteDBClient {
|
|
|
336
494
|
const pair = await this.request('POST', '/api/v1/app/auth/refresh', this.mergeAppTenant({ refresh_token: this.appUserRefreshToken }), undefined, { no401Retry: true });
|
|
337
495
|
this.appUserAccessToken = pair.access_token;
|
|
338
496
|
this.appUserRefreshToken = pair.refresh_token;
|
|
497
|
+
this.cacheAppUserIdFromPair(pair);
|
|
339
498
|
this.onAppUserTokensUpdated?.(pair);
|
|
340
499
|
return pair;
|
|
341
500
|
}
|
|
@@ -474,12 +633,14 @@ class XCiteDBClient {
|
|
|
474
633
|
const pair = await this.request('POST', '/api/v1/app/auth/oauth/exchange', this.mergeAppTenant({ code }));
|
|
475
634
|
this.appUserAccessToken = pair.access_token;
|
|
476
635
|
this.appUserRefreshToken = pair.refresh_token;
|
|
636
|
+
this.cacheAppUserIdFromPair(pair);
|
|
477
637
|
return pair;
|
|
478
638
|
}
|
|
479
639
|
async loginAppUser(email, password) {
|
|
480
640
|
const pair = await this.request('POST', '/api/v1/app/auth/login', this.mergeAppTenant({ email, password }));
|
|
481
641
|
this.appUserAccessToken = pair.access_token;
|
|
482
642
|
this.appUserRefreshToken = pair.refresh_token;
|
|
643
|
+
this.cacheAppUserIdFromPair(pair);
|
|
483
644
|
return pair;
|
|
484
645
|
}
|
|
485
646
|
async refreshAppUser() {
|
|
@@ -489,6 +650,7 @@ class XCiteDBClient {
|
|
|
489
650
|
await this.request('POST', '/api/v1/app/auth/logout', this.mergeAppTenant({ refresh_token: this.appUserRefreshToken }));
|
|
490
651
|
this.appUserAccessToken = undefined;
|
|
491
652
|
this.appUserRefreshToken = undefined;
|
|
653
|
+
this.clearAppUserIdCache();
|
|
492
654
|
}
|
|
493
655
|
async appUserMe() {
|
|
494
656
|
return this.request('GET', '/api/v1/app/auth/me');
|
|
@@ -505,6 +667,7 @@ class XCiteDBClient {
|
|
|
505
667
|
const pair = await this.request('POST', '/api/v1/app/auth/custom-token', { token });
|
|
506
668
|
this.appUserAccessToken = pair.access_token;
|
|
507
669
|
this.appUserRefreshToken = pair.refresh_token;
|
|
670
|
+
this.cacheAppUserIdFromPair(pair);
|
|
508
671
|
return pair;
|
|
509
672
|
}
|
|
510
673
|
/** Change app-user password (requires valid app-user access token). */
|
|
@@ -721,7 +884,7 @@ class XCiteDBClient {
|
|
|
721
884
|
* ```
|
|
722
885
|
*/
|
|
723
886
|
async checkAccess(subject, identifier, action, metaPath, branch) {
|
|
724
|
-
const body = { subject, identifier, action };
|
|
887
|
+
const body = { subject, identifier: this.isoPrefixId(identifier), action };
|
|
725
888
|
if (metaPath !== undefined)
|
|
726
889
|
body.meta_path = metaPath;
|
|
727
890
|
if (branch !== undefined)
|
|
@@ -747,6 +910,37 @@ class XCiteDBClient {
|
|
|
747
910
|
async updateSecurityConfig(config) {
|
|
748
911
|
await this.request('PUT', '/api/v1/security/config', config);
|
|
749
912
|
}
|
|
913
|
+
/** Per-tenant user data spaces (`GET /api/v1/security/user-isolation`). Requires security admin. */
|
|
914
|
+
async getUserIsolationConfig() {
|
|
915
|
+
return this.request('GET', '/api/v1/security/user-isolation');
|
|
916
|
+
}
|
|
917
|
+
/** Enable or reconfigure user isolation (`PUT /api/v1/security/user-isolation`). */
|
|
918
|
+
async setUserIsolationConfig(config) {
|
|
919
|
+
return this.request('PUT', '/api/v1/security/user-isolation', config);
|
|
920
|
+
}
|
|
921
|
+
/** Disable user isolation and remove generated policies (`DELETE /api/v1/security/user-isolation`). */
|
|
922
|
+
async disableUserIsolation() {
|
|
923
|
+
await this.request('DELETE', '/api/v1/security/user-isolation');
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Loads server isolation config; when enabled, configures client-side identifier prefixing to match
|
|
927
|
+
* the server (namespace + shared paths). Does not send `X-Prefix`; identifiers in requests are rewritten.
|
|
928
|
+
*/
|
|
929
|
+
async enableUserIsolation() {
|
|
930
|
+
const cfg = await this.getUserIsolationConfig();
|
|
931
|
+
if (cfg.enabled) {
|
|
932
|
+
this.userIsolation = {
|
|
933
|
+
enabled: true,
|
|
934
|
+
namespace: cfg.namespace_pattern,
|
|
935
|
+
shared_read_paths: cfg.shared_read_paths,
|
|
936
|
+
shared_write_paths: cfg.shared_write_paths,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
this.userIsolation = { enabled: false };
|
|
941
|
+
}
|
|
942
|
+
return cfg;
|
|
943
|
+
}
|
|
750
944
|
async createBranch(name, fromBranch, fromDate) {
|
|
751
945
|
const body = { name };
|
|
752
946
|
if (fromBranch)
|
|
@@ -864,7 +1058,8 @@ class XCiteDBClient {
|
|
|
864
1058
|
}
|
|
865
1059
|
/** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
|
|
866
1060
|
async writeXML(xml, _options) {
|
|
867
|
-
|
|
1061
|
+
const payload = this.isoApplyXmlDbIdentifier(xml);
|
|
1062
|
+
await this.request('POST', '/api/v1/documents', payload, {
|
|
868
1063
|
'Content-Type': 'application/xml',
|
|
869
1064
|
});
|
|
870
1065
|
}
|
|
@@ -874,7 +1069,7 @@ class XCiteDBClient {
|
|
|
874
1069
|
*/
|
|
875
1070
|
async writeXmlDocument(xml, options) {
|
|
876
1071
|
await this.request('POST', '/api/v1/documents', {
|
|
877
|
-
xml,
|
|
1072
|
+
xml: this.isoApplyXmlDbIdentifier(xml),
|
|
878
1073
|
is_top: options?.is_top ?? true,
|
|
879
1074
|
compare_attributes: options?.compare_attributes ?? false,
|
|
880
1075
|
});
|
|
@@ -887,92 +1082,97 @@ class XCiteDBClient {
|
|
|
887
1082
|
}
|
|
888
1083
|
async queryByIdentifier(identifier, flags, filter, pathFilter) {
|
|
889
1084
|
const q = buildQuery({
|
|
890
|
-
identifier,
|
|
1085
|
+
identifier: this.isoPrefixId(identifier),
|
|
891
1086
|
flags: flags,
|
|
892
1087
|
filter,
|
|
893
1088
|
path_filter: pathFilter,
|
|
894
1089
|
});
|
|
895
|
-
|
|
1090
|
+
const rows = await this.request('GET', `/api/v1/documents/by-id${q}`);
|
|
1091
|
+
return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
|
|
896
1092
|
}
|
|
897
1093
|
async queryDocuments(query, flags, filter, pathFilter) {
|
|
1094
|
+
const pq = this.isoPrefixQuery(query);
|
|
898
1095
|
const params = {
|
|
899
|
-
match:
|
|
900
|
-
match_start:
|
|
901
|
-
match_end:
|
|
902
|
-
regex:
|
|
1096
|
+
match: pq.match,
|
|
1097
|
+
match_start: pq.match_start,
|
|
1098
|
+
match_end: pq.match_end,
|
|
1099
|
+
regex: pq.regex,
|
|
903
1100
|
flags: flags,
|
|
904
1101
|
filter,
|
|
905
1102
|
path_filter: pathFilter,
|
|
906
1103
|
};
|
|
907
|
-
if (
|
|
908
|
-
const c = Array.isArray(
|
|
1104
|
+
if (pq.contains !== undefined) {
|
|
1105
|
+
const c = Array.isArray(pq.contains) ? pq.contains.join(',') : pq.contains;
|
|
909
1106
|
params.contains = c;
|
|
910
1107
|
}
|
|
911
|
-
if (
|
|
912
|
-
params.required_meta_paths =
|
|
1108
|
+
if (pq.required_meta_paths !== undefined && pq.required_meta_paths.length > 0) {
|
|
1109
|
+
params.required_meta_paths = pq.required_meta_paths.join(',');
|
|
913
1110
|
}
|
|
914
|
-
if (
|
|
1111
|
+
if (pq.filter_any_meta === true) {
|
|
915
1112
|
params.any_meta = '1';
|
|
916
1113
|
}
|
|
917
|
-
|
|
1114
|
+
const rows = await this.request('GET', `/api/v1/documents${buildQuery(params)}`);
|
|
1115
|
+
return Array.isArray(rows) ? rows.map((x) => this.isoUnprefixId(String(x))) : rows;
|
|
918
1116
|
}
|
|
919
1117
|
async deleteDocument(identifier) {
|
|
920
|
-
await this.request('DELETE', `/api/v1/documents/by-id${buildQuery({ identifier })}`);
|
|
1118
|
+
await this.request('DELETE', `/api/v1/documents/by-id${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
921
1119
|
}
|
|
922
1120
|
async addIdentifier(identifier) {
|
|
923
1121
|
const r = await this.request('POST', '/api/v1/documents/identifiers', {
|
|
924
|
-
identifier,
|
|
1122
|
+
identifier: this.isoPrefixId(identifier),
|
|
925
1123
|
});
|
|
926
1124
|
return r?.ok !== false;
|
|
927
1125
|
}
|
|
928
1126
|
async addAlias(original, alias) {
|
|
929
|
-
const r = await this.request('POST', '/api/v1/documents/identifiers/alias', { original, alias });
|
|
1127
|
+
const r = await this.request('POST', '/api/v1/documents/identifiers/alias', { original: this.isoPrefixId(original), alias: this.isoPrefixId(alias) });
|
|
930
1128
|
return r?.ok !== false;
|
|
931
1129
|
}
|
|
932
1130
|
async queryChangeDate(identifier) {
|
|
933
|
-
const r = await this.request('GET', `/api/v1/documents/change-date${buildQuery({ identifier })}`);
|
|
1131
|
+
const r = await this.request('GET', `/api/v1/documents/change-date${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
934
1132
|
return r?.change_date ?? r?.date ?? '';
|
|
935
1133
|
}
|
|
936
1134
|
async getXcitepath(identifier) {
|
|
937
|
-
const r = await this.request('GET', `/api/v1/documents/xcitepath${buildQuery({ identifier })}`);
|
|
1135
|
+
const r = await this.request('GET', `/api/v1/documents/xcitepath${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
938
1136
|
return r?.xcitepath ?? '';
|
|
939
1137
|
}
|
|
940
1138
|
async changedIdentifiers(branch, fromDate, toDate) {
|
|
941
|
-
|
|
1139
|
+
const ids = await this.request('GET', `/api/v1/documents/changed${buildQuery({ branch, from_date: fromDate, to_date: toDate })}`);
|
|
1140
|
+
return Array.isArray(ids) ? ids.map((x) => this.isoUnprefixId(String(x))) : ids;
|
|
942
1141
|
}
|
|
943
1142
|
async listIdentifiers(query) {
|
|
1143
|
+
const pq = this.isoPrefixQuery(query);
|
|
944
1144
|
const params = {
|
|
945
|
-
match:
|
|
946
|
-
match_start:
|
|
947
|
-
match_end:
|
|
948
|
-
regex:
|
|
949
|
-
limit:
|
|
950
|
-
offset:
|
|
1145
|
+
match: pq.match,
|
|
1146
|
+
match_start: pq.match_start,
|
|
1147
|
+
match_end: pq.match_end,
|
|
1148
|
+
regex: pq.regex,
|
|
1149
|
+
limit: pq.limit,
|
|
1150
|
+
offset: pq.offset,
|
|
951
1151
|
};
|
|
952
|
-
if (
|
|
953
|
-
params.contains = Array.isArray(
|
|
954
|
-
? query.contains.join(',')
|
|
955
|
-
: query.contains;
|
|
1152
|
+
if (pq.contains !== undefined) {
|
|
1153
|
+
params.contains = Array.isArray(pq.contains) ? pq.contains.join(',') : pq.contains;
|
|
956
1154
|
}
|
|
957
|
-
if (
|
|
958
|
-
params.required_meta_paths =
|
|
1155
|
+
if (pq.required_meta_paths !== undefined && pq.required_meta_paths.length > 0) {
|
|
1156
|
+
params.required_meta_paths = pq.required_meta_paths.join(',');
|
|
959
1157
|
}
|
|
960
|
-
if (
|
|
1158
|
+
if (pq.filter_any_meta === true) {
|
|
961
1159
|
params.any_meta = '1';
|
|
962
1160
|
}
|
|
963
1161
|
const data = await this.request('GET', `/api/v1/documents/identifiers${buildQuery(params)}`);
|
|
1162
|
+
const un = (ids) => ids.map((id) => this.isoUnprefixId(id));
|
|
964
1163
|
// Older servers returned a bare string[]; paginated API returns { identifiers, total, offset, limit }.
|
|
965
1164
|
if (Array.isArray(data)) {
|
|
1165
|
+
const ids = un(data);
|
|
966
1166
|
return {
|
|
967
|
-
identifiers:
|
|
968
|
-
total:
|
|
1167
|
+
identifiers: ids,
|
|
1168
|
+
total: ids.length,
|
|
969
1169
|
offset: 0,
|
|
970
|
-
limit:
|
|
1170
|
+
limit: ids.length,
|
|
971
1171
|
};
|
|
972
1172
|
}
|
|
973
1173
|
if (data !== null && typeof data === 'object') {
|
|
974
1174
|
const o = data;
|
|
975
|
-
const ids = Array.isArray(o.identifiers) ? o.identifiers : [];
|
|
1175
|
+
const ids = Array.isArray(o.identifiers) ? un(o.identifiers) : [];
|
|
976
1176
|
return {
|
|
977
1177
|
identifiers: ids,
|
|
978
1178
|
total: typeof o.total === 'number' ? o.total : ids.length,
|
|
@@ -985,7 +1185,7 @@ class XCiteDBClient {
|
|
|
985
1185
|
async listIdentifierChildren(parentPath) {
|
|
986
1186
|
const params = {};
|
|
987
1187
|
if (parentPath !== undefined && parentPath !== '') {
|
|
988
|
-
params.parent_path = parentPath;
|
|
1188
|
+
params.parent_path = this.isoPrefixId(parentPath);
|
|
989
1189
|
}
|
|
990
1190
|
const data = await this.request('GET', `/api/v1/documents/identifier-children${buildQuery(params)}`);
|
|
991
1191
|
if (data !== null && typeof data === 'object') {
|
|
@@ -998,7 +1198,7 @@ class XCiteDBClient {
|
|
|
998
1198
|
const r = row;
|
|
999
1199
|
children.push({
|
|
1000
1200
|
segment: typeof r.segment === 'string' ? r.segment : '',
|
|
1001
|
-
full_path: typeof r.full_path === 'string' ? r.full_path : '',
|
|
1201
|
+
full_path: typeof r.full_path === 'string' ? this.isoUnprefixId(r.full_path) : '',
|
|
1002
1202
|
is_identifier: r.is_identifier === true,
|
|
1003
1203
|
has_children: r.has_children === true,
|
|
1004
1204
|
});
|
|
@@ -1006,7 +1206,7 @@ class XCiteDBClient {
|
|
|
1006
1206
|
}
|
|
1007
1207
|
}
|
|
1008
1208
|
return {
|
|
1009
|
-
parent_path: typeof o.parent_path === 'string' ? o.parent_path : '',
|
|
1209
|
+
parent_path: typeof o.parent_path === 'string' ? this.isoUnprefixId(o.parent_path) : '',
|
|
1010
1210
|
parent_is_identifier: o.parent_is_identifier === true,
|
|
1011
1211
|
hierarchy_index_available: o.hierarchy_index_available === true,
|
|
1012
1212
|
children,
|
|
@@ -1020,16 +1220,17 @@ class XCiteDBClient {
|
|
|
1020
1220
|
};
|
|
1021
1221
|
}
|
|
1022
1222
|
async queryLog(query, fromDate, toDate) {
|
|
1223
|
+
const pq = this.isoPrefixQuery(query);
|
|
1023
1224
|
const params = {
|
|
1024
|
-
match_start:
|
|
1025
|
-
match:
|
|
1225
|
+
match_start: pq.match_start,
|
|
1226
|
+
match: pq.match,
|
|
1026
1227
|
from_date: fromDate,
|
|
1027
1228
|
to_date: toDate,
|
|
1028
1229
|
};
|
|
1029
1230
|
return this.request('GET', `/api/v1/documents/log${buildQuery(params)}`);
|
|
1030
1231
|
}
|
|
1031
1232
|
async addMeta(identifier, value, path = '', opts) {
|
|
1032
|
-
const body = { identifier, value, path };
|
|
1233
|
+
const body = { identifier: this.isoPrefixId(identifier), value, path };
|
|
1033
1234
|
if (opts?.mode === 'append')
|
|
1034
1235
|
body.mode = 'append';
|
|
1035
1236
|
const r = await this.request('POST', '/api/v1/meta', body);
|
|
@@ -1037,7 +1238,7 @@ class XCiteDBClient {
|
|
|
1037
1238
|
}
|
|
1038
1239
|
async addMetaByQuery(query, value, path = '', firstMatch = false, opts) {
|
|
1039
1240
|
const body = {
|
|
1040
|
-
query,
|
|
1241
|
+
query: this.isoPrefixQuery(query),
|
|
1041
1242
|
value,
|
|
1042
1243
|
path,
|
|
1043
1244
|
first_match: firstMatch,
|
|
@@ -1054,27 +1255,36 @@ class XCiteDBClient {
|
|
|
1054
1255
|
return this.addMetaByQuery(query, value, path, firstMatch, { mode: 'append' });
|
|
1055
1256
|
}
|
|
1056
1257
|
async queryMeta(identifier, path = '') {
|
|
1057
|
-
return this.request('GET', `/api/v1/meta${buildQuery({ identifier, path })}`);
|
|
1258
|
+
return this.request('GET', `/api/v1/meta${buildQuery({ identifier: this.isoPrefixId(identifier), path })}`);
|
|
1058
1259
|
}
|
|
1059
1260
|
async queryMetaByQuery(query, path = '') {
|
|
1060
|
-
return this.request('POST', '/api/v1/meta/query', { query, path });
|
|
1261
|
+
return this.request('POST', '/api/v1/meta/query', { query: this.isoPrefixQuery(query), path });
|
|
1061
1262
|
}
|
|
1062
1263
|
async clearMeta(query) {
|
|
1063
|
-
const r = await this.request('DELETE', '/api/v1/meta', {
|
|
1264
|
+
const r = await this.request('DELETE', '/api/v1/meta', {
|
|
1265
|
+
query: this.isoPrefixQuery(query),
|
|
1266
|
+
});
|
|
1064
1267
|
return r?.ok !== false;
|
|
1065
1268
|
}
|
|
1066
1269
|
async acquireLock(identifier, expires = 0) {
|
|
1067
|
-
|
|
1270
|
+
const lock = await this.request('POST', '/api/v1/locks', {
|
|
1271
|
+
identifier: this.isoPrefixId(identifier),
|
|
1272
|
+
expires,
|
|
1273
|
+
});
|
|
1274
|
+
return { ...lock, identifier: this.isoUnprefixId(lock.identifier) };
|
|
1068
1275
|
}
|
|
1069
1276
|
async releaseLock(identifier, lockId) {
|
|
1070
1277
|
const r = await this.request('DELETE', '/api/v1/locks', {
|
|
1071
|
-
identifier,
|
|
1278
|
+
identifier: this.isoPrefixId(identifier),
|
|
1072
1279
|
lock_id: lockId,
|
|
1073
1280
|
});
|
|
1074
1281
|
return r?.ok !== false;
|
|
1075
1282
|
}
|
|
1076
1283
|
async findLocks(identifier) {
|
|
1077
|
-
|
|
1284
|
+
const locks = await this.request('GET', `/api/v1/locks${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
1285
|
+
return Array.isArray(locks)
|
|
1286
|
+
? locks.map((L) => ({ ...L, identifier: this.isoUnprefixId(L.identifier) }))
|
|
1287
|
+
: locks;
|
|
1078
1288
|
}
|
|
1079
1289
|
/**
|
|
1080
1290
|
* Run Unquery (`POST /api/v1/unquery`): declarative analytics over documents matching `query`.
|
|
@@ -1102,7 +1312,7 @@ class XCiteDBClient {
|
|
|
1102
1312
|
* ```
|
|
1103
1313
|
*/
|
|
1104
1314
|
async unquery(query, unquery) {
|
|
1105
|
-
return this.request('POST', '/api/v1/unquery', { query, unquery });
|
|
1315
|
+
return this.request('POST', '/api/v1/unquery', { query: this.isoPrefixQuery(query), unquery });
|
|
1106
1316
|
}
|
|
1107
1317
|
async search(q) {
|
|
1108
1318
|
const body = { query: q.query };
|
|
@@ -1114,6 +1324,12 @@ class XCiteDBClient {
|
|
|
1114
1324
|
body.offset = q.offset;
|
|
1115
1325
|
if (q.limit !== undefined)
|
|
1116
1326
|
body.limit = q.limit;
|
|
1327
|
+
if (q.mode)
|
|
1328
|
+
body.mode = q.mode;
|
|
1329
|
+
if (q.min_score !== undefined)
|
|
1330
|
+
body.min_score = q.min_score;
|
|
1331
|
+
if (q.semantic_weight !== undefined)
|
|
1332
|
+
body.semantic_weight = q.semantic_weight;
|
|
1117
1333
|
const data = await this.request('POST', '/api/v1/search', body);
|
|
1118
1334
|
const hits = [];
|
|
1119
1335
|
if (Array.isArray(data.hits)) {
|
|
@@ -1121,7 +1337,7 @@ class XCiteDBClient {
|
|
|
1121
1337
|
if (h && typeof h === 'object') {
|
|
1122
1338
|
const o = h;
|
|
1123
1339
|
const hit = {
|
|
1124
|
-
identifier: String(o.identifier ?? ''),
|
|
1340
|
+
identifier: this.isoUnprefixId(String(o.identifier ?? '')),
|
|
1125
1341
|
path: String(o.path ?? ''),
|
|
1126
1342
|
doc_type: o.doc_type === 'json' ? 'json' : 'xml',
|
|
1127
1343
|
branch: String(o.branch ?? ''),
|
|
@@ -1131,6 +1347,9 @@ class XCiteDBClient {
|
|
|
1131
1347
|
if (typeof o.xcitepath === 'string' && o.xcitepath.length > 0) {
|
|
1132
1348
|
hit.xcitepath = o.xcitepath;
|
|
1133
1349
|
}
|
|
1350
|
+
if (o.source === 'fts' || o.source === 'semantic' || o.source === 'both') {
|
|
1351
|
+
hit.source = o.source;
|
|
1352
|
+
}
|
|
1134
1353
|
hits.push(hit);
|
|
1135
1354
|
}
|
|
1136
1355
|
}
|
|
@@ -1144,24 +1363,170 @@ class XCiteDBClient {
|
|
|
1144
1363
|
async reindex() {
|
|
1145
1364
|
return this.request('POST', '/api/v1/search/reindex', {});
|
|
1146
1365
|
}
|
|
1366
|
+
async reembedVector() {
|
|
1367
|
+
return this.request('POST', '/api/v1/search/reembed-vector', {});
|
|
1368
|
+
}
|
|
1369
|
+
/** Project search / FTS / vector / LLM configuration (`GET /api/v1/project/settings/search`). */
|
|
1370
|
+
async getProjectSearchSettings() {
|
|
1371
|
+
return this.request('GET', '/api/v1/project/settings/search');
|
|
1372
|
+
}
|
|
1373
|
+
/** Update project search settings (`PUT /api/v1/project/settings/search`). Returns the same shape as GET. */
|
|
1374
|
+
async updateProjectSearchSettings(patch) {
|
|
1375
|
+
return this.request('PUT', '/api/v1/project/settings/search', patch);
|
|
1376
|
+
}
|
|
1377
|
+
/** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
|
|
1378
|
+
async getVectorIndexEstimate() {
|
|
1379
|
+
return this.request('GET', '/api/v1/project/settings/search/vector-index-estimate', undefined);
|
|
1380
|
+
}
|
|
1381
|
+
/** Start background estimate (202); cancel prior session for this tenant. */
|
|
1382
|
+
async postVectorIndexEstimateSession() {
|
|
1383
|
+
return this.request('POST', '/api/v1/project/settings/search/vector-index-estimate', {});
|
|
1384
|
+
}
|
|
1385
|
+
async getVectorIndexEstimateSession(sessionId) {
|
|
1386
|
+
const q = buildQuery({ session: sessionId });
|
|
1387
|
+
return this.request('GET', `/api/v1/project/settings/search/vector-index-estimate${q}`, undefined);
|
|
1388
|
+
}
|
|
1389
|
+
async deleteVectorIndexEstimateSession(sessionId) {
|
|
1390
|
+
const q = buildQuery({ session: sessionId });
|
|
1391
|
+
return this.request('DELETE', `/api/v1/project/settings/search/vector-index-estimate${q}`, undefined);
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* RAG over indexed documents (`POST /api/v1/rag/query` with JSON body).
|
|
1395
|
+
* Requires LLM completion; embedding required when retrieval uses semantic or hybrid.
|
|
1396
|
+
*/
|
|
1397
|
+
async ragQuery(options) {
|
|
1398
|
+
const body = {
|
|
1399
|
+
question: options.question,
|
|
1400
|
+
stream: false,
|
|
1401
|
+
};
|
|
1402
|
+
if (options.branch !== undefined)
|
|
1403
|
+
body.branch = options.branch;
|
|
1404
|
+
if (options.max_context_docs !== undefined)
|
|
1405
|
+
body.max_context_docs = options.max_context_docs;
|
|
1406
|
+
if (options.doc_types?.length)
|
|
1407
|
+
body.doc_types = options.doc_types;
|
|
1408
|
+
if (options.search_mode !== undefined)
|
|
1409
|
+
body.search_mode = options.search_mode;
|
|
1410
|
+
if (options.min_score !== undefined)
|
|
1411
|
+
body.min_score = options.min_score;
|
|
1412
|
+
if (options.semantic_weight !== undefined)
|
|
1413
|
+
body.semantic_weight = options.semantic_weight;
|
|
1414
|
+
const data = await this.request('POST', '/api/v1/rag/query', body);
|
|
1415
|
+
if (!data || typeof data !== 'object') {
|
|
1416
|
+
throw new types_1.XCiteDBError('Invalid RAG response', 500, data);
|
|
1417
|
+
}
|
|
1418
|
+
const answer = typeof data.answer === 'string' ? data.answer : '';
|
|
1419
|
+
const sources = 'sources' in data ? data.sources : [];
|
|
1420
|
+
return { answer, sources };
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Streaming RAG (`POST /api/v1/rag/query` with `stream: true`). Parses SSE `data: {...}` lines.
|
|
1424
|
+
* The final event has `done: true` and may include `sources`.
|
|
1425
|
+
*/
|
|
1426
|
+
async ragQueryStream(options, onEvent) {
|
|
1427
|
+
const path = '/api/v1/rag/query';
|
|
1428
|
+
const payload = JSON.stringify({
|
|
1429
|
+
question: options.question,
|
|
1430
|
+
stream: true,
|
|
1431
|
+
...(options.branch !== undefined ? { branch: options.branch } : {}),
|
|
1432
|
+
...(options.max_context_docs !== undefined ? { max_context_docs: options.max_context_docs } : {}),
|
|
1433
|
+
...(options.doc_types?.length ? { doc_types: options.doc_types } : {}),
|
|
1434
|
+
...(options.search_mode !== undefined ? { search_mode: options.search_mode } : {}),
|
|
1435
|
+
...(options.min_score !== undefined ? { min_score: options.min_score } : {}),
|
|
1436
|
+
...(options.semantic_weight !== undefined ? { semantic_weight: options.semantic_weight } : {}),
|
|
1437
|
+
});
|
|
1438
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1439
|
+
const url = joinUrl(this.baseUrl, path);
|
|
1440
|
+
const headers = {
|
|
1441
|
+
...this.requestHeaders(),
|
|
1442
|
+
'Content-Type': 'application/json',
|
|
1443
|
+
};
|
|
1444
|
+
const res = await fetch(url, { method: 'POST', headers, body: payload });
|
|
1445
|
+
if (res.status === 401 &&
|
|
1446
|
+
attempt === 0 &&
|
|
1447
|
+
(await this.tryRefreshSessionAfter401())) {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (!res.ok) {
|
|
1451
|
+
const text = await res.text();
|
|
1452
|
+
let data;
|
|
1453
|
+
try {
|
|
1454
|
+
data = text ? JSON.parse(text) : null;
|
|
1455
|
+
}
|
|
1456
|
+
catch {
|
|
1457
|
+
data = text;
|
|
1458
|
+
}
|
|
1459
|
+
const msg = typeof data === 'object' && data !== null && 'message' in data
|
|
1460
|
+
? String(data.message)
|
|
1461
|
+
: res.statusText;
|
|
1462
|
+
this.notifySessionInvalidIfNeeded(path, res.status);
|
|
1463
|
+
throw new types_1.XCiteDBError(msg || `HTTP ${res.status}`, res.status, data);
|
|
1464
|
+
}
|
|
1465
|
+
const streamBody = res.body;
|
|
1466
|
+
if (!streamBody) {
|
|
1467
|
+
const text = await res.text();
|
|
1468
|
+
throw new types_1.XCiteDBError('RAG stream: empty response body', res.status, text);
|
|
1469
|
+
}
|
|
1470
|
+
const reader = streamBody.getReader();
|
|
1471
|
+
const decoder = new TextDecoder();
|
|
1472
|
+
let buf = '';
|
|
1473
|
+
try {
|
|
1474
|
+
for (;;) {
|
|
1475
|
+
const { done, value } = await reader.read();
|
|
1476
|
+
if (value) {
|
|
1477
|
+
buf += decoder.decode(value, { stream: !done });
|
|
1478
|
+
}
|
|
1479
|
+
let sep;
|
|
1480
|
+
while ((sep = buf.indexOf('\n\n')) >= 0) {
|
|
1481
|
+
const block = buf.slice(0, sep);
|
|
1482
|
+
buf = buf.slice(sep + 2);
|
|
1483
|
+
const lines = block.split('\n');
|
|
1484
|
+
const dataLine = lines.find((l) => l.startsWith('data:'));
|
|
1485
|
+
if (!dataLine)
|
|
1486
|
+
continue;
|
|
1487
|
+
const jsonStr = dataLine.replace(/^data:\s*/, '').trim();
|
|
1488
|
+
if (!jsonStr)
|
|
1489
|
+
continue;
|
|
1490
|
+
let ev;
|
|
1491
|
+
try {
|
|
1492
|
+
ev = JSON.parse(jsonStr);
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
onEvent(ev);
|
|
1498
|
+
}
|
|
1499
|
+
if (done)
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
finally {
|
|
1504
|
+
reader.releaseLock();
|
|
1505
|
+
}
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
throw new types_1.XCiteDBError('RAG stream failed after retry', 401, null);
|
|
1509
|
+
}
|
|
1147
1510
|
async writeJsonDocument(identifier, data) {
|
|
1148
|
-
await this.request('POST', '/api/v1/json-documents', { identifier, data });
|
|
1511
|
+
await this.request('POST', '/api/v1/json-documents', { identifier: this.isoPrefixId(identifier), data });
|
|
1149
1512
|
}
|
|
1150
1513
|
async readJsonDocument(identifier) {
|
|
1151
|
-
return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier })}`);
|
|
1514
|
+
return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
1152
1515
|
}
|
|
1153
1516
|
async deleteJsonDocument(identifier) {
|
|
1154
|
-
await this.request('DELETE', `/api/v1/json-documents${buildQuery({ identifier })}`);
|
|
1517
|
+
await this.request('DELETE', `/api/v1/json-documents${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
|
|
1155
1518
|
}
|
|
1156
1519
|
async listJsonDocuments(match, limit, offset) {
|
|
1157
|
-
const
|
|
1520
|
+
const m = match !== undefined && match !== '' ? this.isoPrefixId(match) : match;
|
|
1521
|
+
const data = await this.request('GET', `/api/v1/json-documents/list${buildQuery({ match: m, limit, offset })}`);
|
|
1522
|
+
const un = (ids) => ids.map((id) => this.isoUnprefixId(id));
|
|
1158
1523
|
if (Array.isArray(data)) {
|
|
1159
|
-
const identifiers = data;
|
|
1524
|
+
const identifiers = un(data);
|
|
1160
1525
|
return { identifiers, total: identifiers.length, offset: 0, limit: identifiers.length };
|
|
1161
1526
|
}
|
|
1162
1527
|
if (data !== null && typeof data === 'object') {
|
|
1163
1528
|
const o = data;
|
|
1164
|
-
const ids = Array.isArray(o.identifiers) ? o.identifiers : [];
|
|
1529
|
+
const ids = Array.isArray(o.identifiers) ? un(o.identifiers) : [];
|
|
1165
1530
|
return {
|
|
1166
1531
|
identifiers: ids,
|
|
1167
1532
|
total: typeof o.total === 'number' ? o.total : ids.length,
|
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, DatabaseContext, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
3
|
+
export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, DatabaseContext, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationOptions, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
|
|
4
4
|
export { XCiteDBError } from './types';
|
package/dist/types.d.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface TextSearchQuery {
|
|
|
41
41
|
branch?: string;
|
|
42
42
|
offset?: number;
|
|
43
43
|
limit?: number;
|
|
44
|
+
/** Default `fts`. `semantic` / `hybrid` require vector search (and hybrid requires FTS). */
|
|
45
|
+
mode?: 'fts' | 'semantic' | 'hybrid';
|
|
46
|
+
/** Minimum cosine similarity for semantic / hybrid vector leg (default 0.3). */
|
|
47
|
+
min_score?: number;
|
|
48
|
+
/** Hybrid only: weight of semantic vs FTS in weighted RRF (0–1, default 0.5). */
|
|
49
|
+
semantic_weight?: number;
|
|
44
50
|
}
|
|
45
51
|
export interface TextSearchHit {
|
|
46
52
|
identifier: string;
|
|
@@ -51,12 +57,138 @@ export interface TextSearchHit {
|
|
|
51
57
|
branch: string;
|
|
52
58
|
snippet: string;
|
|
53
59
|
score: number;
|
|
60
|
+
/** Present for semantic / hybrid search. */
|
|
61
|
+
source?: 'fts' | 'semantic' | 'both';
|
|
54
62
|
}
|
|
55
63
|
export interface TextSearchResult {
|
|
56
64
|
hits: TextSearchHit[];
|
|
57
65
|
total: number;
|
|
58
66
|
query: string;
|
|
59
67
|
}
|
|
68
|
+
/** Progress object when the server is indexing (FTS or vector). */
|
|
69
|
+
export interface SearchIndexingProgress {
|
|
70
|
+
done: number;
|
|
71
|
+
total: number;
|
|
72
|
+
}
|
|
73
|
+
/** Last finished vector re-embed job (`vector_index_job` in project settings). */
|
|
74
|
+
export interface VectorIndexingLastJob {
|
|
75
|
+
ok?: boolean;
|
|
76
|
+
finished_at_ms?: number;
|
|
77
|
+
message?: string;
|
|
78
|
+
detail?: string;
|
|
79
|
+
phase?: string;
|
|
80
|
+
rows_indexed?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Vector index workload + indicative cost.
|
|
84
|
+
* Blocking scan: `GET /api/v1/project/settings/search/vector-index-estimate` (no `session`).
|
|
85
|
+
* Incremental UI: `POST` same path, then `GET` / `DELETE` with `?session=`.
|
|
86
|
+
*/
|
|
87
|
+
export interface VectorIndexEstimate {
|
|
88
|
+
document_count?: number;
|
|
89
|
+
chunk_count?: number;
|
|
90
|
+
approx_input_tokens?: number;
|
|
91
|
+
approx_input_tokens_xml?: number;
|
|
92
|
+
approx_input_tokens_json?: number;
|
|
93
|
+
/** Serialized XML on disk (only docs that produced ≥1 embed chunk). */
|
|
94
|
+
xml_source_serialized_bytes_total?: number;
|
|
95
|
+
/** UTF-8 bytes of XML embed text (text nodes only; same as embedding input). */
|
|
96
|
+
xml_embed_text_bytes_total?: number;
|
|
97
|
+
/** Raw JSON document bytes (only docs that produced ≥1 embed chunk). */
|
|
98
|
+
json_source_bytes_total?: number;
|
|
99
|
+
/** UTF-8 bytes of JSON scalar chunks sent to the embedder. */
|
|
100
|
+
json_embed_text_bytes_total?: number;
|
|
101
|
+
token_estimate_note?: string;
|
|
102
|
+
embedding_provider?: string;
|
|
103
|
+
embedding_model?: string;
|
|
104
|
+
embedding_configured?: boolean;
|
|
105
|
+
pricing_info_url?: string;
|
|
106
|
+
/** Null when unknown (e.g. openai_compatible). */
|
|
107
|
+
usd_per_million_tokens_indicative?: number | null;
|
|
108
|
+
estimated_cost_usd_indicative?: number | null;
|
|
109
|
+
cost_disclaimer?: string;
|
|
110
|
+
/** Async session responses only (`POST` / `GET ?session=`). */
|
|
111
|
+
session_id?: string;
|
|
112
|
+
branches_done?: number;
|
|
113
|
+
branches_total?: number;
|
|
114
|
+
estimate_complete?: boolean;
|
|
115
|
+
estimate_cancelled?: boolean;
|
|
116
|
+
estimate_error?: string;
|
|
117
|
+
}
|
|
118
|
+
/** `GET /api/v1/project/settings/search` (and returned after `PUT`). */
|
|
119
|
+
export interface ProjectSearchSettings {
|
|
120
|
+
search_enabled?: boolean;
|
|
121
|
+
search_language?: string;
|
|
122
|
+
indexing_status?: 'indexing' | 'idle';
|
|
123
|
+
indexing_progress?: SearchIndexingProgress;
|
|
124
|
+
vector_search_enabled?: boolean;
|
|
125
|
+
embedding_provider?: string;
|
|
126
|
+
embedding_model?: string;
|
|
127
|
+
embedding_base_url?: string;
|
|
128
|
+
llm_provider?: string;
|
|
129
|
+
llm_model?: string;
|
|
130
|
+
llm_base_url?: string;
|
|
131
|
+
embedding_configured?: boolean;
|
|
132
|
+
llm_configured?: boolean;
|
|
133
|
+
vector_indexing_status?: 'indexing' | 'scheduled' | 'idle';
|
|
134
|
+
vector_indexing_progress?: SearchIndexingProgress | null;
|
|
135
|
+
vector_indexing_last_job?: VectorIndexingLastJob | null;
|
|
136
|
+
[key: string]: unknown;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `PUT /api/v1/project/settings/search` body (all keys optional).
|
|
140
|
+
* `embedding_api_key` / `llm_api_key` are write-only: send a new secret or empty string to clear.
|
|
141
|
+
* While `vector_search_enabled` is true, changing embedding provider/model/base URL triggers a full vector re-embed on the server.
|
|
142
|
+
*/
|
|
143
|
+
export interface ProjectSearchSettingsUpdate {
|
|
144
|
+
search_enabled?: boolean;
|
|
145
|
+
search_language?: string;
|
|
146
|
+
vector_search_enabled?: boolean;
|
|
147
|
+
embedding_provider?: string;
|
|
148
|
+
embedding_model?: string;
|
|
149
|
+
embedding_base_url?: string | null;
|
|
150
|
+
embedding_api_key?: string;
|
|
151
|
+
llm_provider?: string;
|
|
152
|
+
llm_model?: string;
|
|
153
|
+
llm_base_url?: string | null;
|
|
154
|
+
llm_api_key?: string;
|
|
155
|
+
}
|
|
156
|
+
/** `POST /api/v1/rag/query` (non-streaming: set `stream: false` or omit). */
|
|
157
|
+
export interface RagQueryOptions {
|
|
158
|
+
question: string;
|
|
159
|
+
branch?: string;
|
|
160
|
+
max_context_docs?: number;
|
|
161
|
+
doc_types?: ('xml' | 'json')[];
|
|
162
|
+
/** Default `false` for JSON response with `answer` and `sources`. */
|
|
163
|
+
stream?: boolean;
|
|
164
|
+
/** Default `auto`: hybrid if FTS+vector enabled, else best available mode. */
|
|
165
|
+
search_mode?: 'auto' | 'hybrid' | 'semantic' | 'fts';
|
|
166
|
+
/** Minimum cosine similarity for semantic / hybrid vector leg (default 0.35). */
|
|
167
|
+
min_score?: number;
|
|
168
|
+
/** Hybrid retrieval only: semantic vs FTS weight in weighted RRF (0–1, default 0.5). */
|
|
169
|
+
semantic_weight?: number;
|
|
170
|
+
}
|
|
171
|
+
export interface RagQueryResult {
|
|
172
|
+
answer: string;
|
|
173
|
+
sources: unknown;
|
|
174
|
+
}
|
|
175
|
+
/** One SSE JSON payload from streaming RAG (`stream: true`). */
|
|
176
|
+
export type RagStreamEvent = {
|
|
177
|
+
text: string;
|
|
178
|
+
done: false;
|
|
179
|
+
sources?: undefined;
|
|
180
|
+
error?: undefined;
|
|
181
|
+
} | {
|
|
182
|
+
text?: string;
|
|
183
|
+
done: true;
|
|
184
|
+
sources?: unknown;
|
|
185
|
+
error?: undefined;
|
|
186
|
+
} | {
|
|
187
|
+
error: string;
|
|
188
|
+
done: true;
|
|
189
|
+
text?: undefined;
|
|
190
|
+
sources?: undefined;
|
|
191
|
+
};
|
|
60
192
|
/** Response from `GET /api/v1/documents/identifiers` */
|
|
61
193
|
export interface ListIdentifiersResult {
|
|
62
194
|
identifiers: string[];
|
|
@@ -206,6 +338,24 @@ export interface RealtimeEvent {
|
|
|
206
338
|
timestamp?: number;
|
|
207
339
|
tenant_id?: string;
|
|
208
340
|
}
|
|
341
|
+
/** When enabled, prefixes document/meta paths with the app user's namespace (e.g. `/users/{userId}/…`) for convenience; ABAC still enforces access on the server. */
|
|
342
|
+
export interface UserIsolationOptions {
|
|
343
|
+
enabled: boolean;
|
|
344
|
+
/** Template with `{userId}` or server-style `${user.id}` replaced from JWT or login response; default `/users/{userId}`. */
|
|
345
|
+
namespace?: string;
|
|
346
|
+
/** Paths left unchanged by prefixing (optional; {@link XCiteDBClient.enableUserIsolation} fills from server). */
|
|
347
|
+
shared_read_paths?: string[];
|
|
348
|
+
shared_write_paths?: string[];
|
|
349
|
+
}
|
|
350
|
+
/** Effective user isolation settings from `GET /api/v1/security/user-isolation`. */
|
|
351
|
+
export interface UserIsolationConfig {
|
|
352
|
+
enabled: boolean;
|
|
353
|
+
namespace_pattern: string;
|
|
354
|
+
shared_read_paths: string[];
|
|
355
|
+
shared_write_paths: string[];
|
|
356
|
+
shared_write_groups: string[];
|
|
357
|
+
generated_policies?: string[];
|
|
358
|
+
}
|
|
209
359
|
export interface XCiteDBClientOptions {
|
|
210
360
|
baseUrl: string;
|
|
211
361
|
apiKey?: string;
|
|
@@ -234,6 +384,8 @@ export interface XCiteDBClientOptions {
|
|
|
234
384
|
testSessionToken?: string;
|
|
235
385
|
/** When true with `testSessionToken`, sends `X-Test-Auth: required` so real credentials are validated. */
|
|
236
386
|
testRequireAuth?: boolean;
|
|
387
|
+
/** Auto-prefix identifiers for app-user sessions (see {@link UserIsolationOptions}). */
|
|
388
|
+
userIsolation?: UserIsolationOptions;
|
|
237
389
|
}
|
|
238
390
|
/** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
|
|
239
391
|
export interface CreateTestSessionOptions {
|
|
@@ -250,6 +402,7 @@ export interface CreateTestSessionOptions {
|
|
|
250
402
|
onSessionTokensUpdated?: (pair: TokenPair) => void;
|
|
251
403
|
onAppUserTokensUpdated?: (pair: AppUserTokenPair) => void;
|
|
252
404
|
onSessionInvalid?: () => void;
|
|
405
|
+
userIsolation?: UserIsolationOptions;
|
|
253
406
|
}
|
|
254
407
|
/** Application user (tenant-scoped), distinct from developer users. */
|
|
255
408
|
export interface AppUser {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
* Wet tests: set XCITEDB_BASE_URL, XCITEDB_ADMIN_TOKEN, XCITEDB_TENANT_ID (platform + project).
|
|
8
|
+
* Run: npm test (builds then runs node:test).
|
|
9
|
+
*/
|
|
10
|
+
const node_test_1 = require("node:test");
|
|
11
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const client_js_1 = require("./client.js");
|
|
14
|
+
function wetEnv() {
|
|
15
|
+
const baseUrl = process.env.XCITEDB_BASE_URL?.trim();
|
|
16
|
+
const accessToken = process.env.XCITEDB_ADMIN_TOKEN?.trim();
|
|
17
|
+
const tenantId = process.env.XCITEDB_TENANT_ID?.trim();
|
|
18
|
+
if (!baseUrl || !accessToken || !tenantId)
|
|
19
|
+
return null;
|
|
20
|
+
return { baseUrl, accessToken, tenantId };
|
|
21
|
+
}
|
|
22
|
+
function adminClient(e) {
|
|
23
|
+
return new client_js_1.XCiteDBClient({
|
|
24
|
+
baseUrl: e.baseUrl,
|
|
25
|
+
accessToken: e.accessToken,
|
|
26
|
+
platformConsole: true,
|
|
27
|
+
projectId: e.tenantId,
|
|
28
|
+
context: { branch: 'main', project_id: e.tenantId },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async function openTestSession(e) {
|
|
32
|
+
const url = `${e.baseUrl.replace(/\/+$/, '')}/api/v1/test/sessions`;
|
|
33
|
+
const r = await fetch(url, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
Authorization: `Bearer ${e.accessToken}`,
|
|
38
|
+
'X-Project-Id': e.tenantId,
|
|
39
|
+
'X-Branch': 'main',
|
|
40
|
+
},
|
|
41
|
+
body: '{}',
|
|
42
|
+
});
|
|
43
|
+
const data = (await r.json());
|
|
44
|
+
if (!r.ok || !data.session_token) {
|
|
45
|
+
throw new Error(`test session failed ${r.status}: ${JSON.stringify(data)}`);
|
|
46
|
+
}
|
|
47
|
+
const client = new client_js_1.XCiteDBClient({
|
|
48
|
+
baseUrl: e.baseUrl,
|
|
49
|
+
accessToken: e.accessToken,
|
|
50
|
+
platformConsole: true,
|
|
51
|
+
projectId: e.tenantId,
|
|
52
|
+
context: { branch: 'main', project_id: e.tenantId },
|
|
53
|
+
testSessionToken: data.session_token,
|
|
54
|
+
testRequireAuth: true,
|
|
55
|
+
});
|
|
56
|
+
return { client, token: data.session_token };
|
|
57
|
+
}
|
|
58
|
+
const w = wetEnv();
|
|
59
|
+
const wd = w ? node_test_1.describe : node_test_1.describe.skip;
|
|
60
|
+
wd('user isolation (wet)', () => {
|
|
61
|
+
(0, node_test_1.it)('setUserIsolationConfig enables isolation (round-trip)', async () => {
|
|
62
|
+
const e = wetEnv();
|
|
63
|
+
if (!e)
|
|
64
|
+
throw new Error('missing env');
|
|
65
|
+
const c = adminClient(e);
|
|
66
|
+
try {
|
|
67
|
+
const out = await c.setUserIsolationConfig({
|
|
68
|
+
enabled: true,
|
|
69
|
+
namespace_pattern: '/users/${user.id}',
|
|
70
|
+
});
|
|
71
|
+
strict_1.default.equal(out.enabled, true);
|
|
72
|
+
const again = await c.getUserIsolationConfig();
|
|
73
|
+
strict_1.default.equal(again.enabled, true);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await c.disableUserIsolation().catch(() => { });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
(0, node_test_1.it)('getUserIsolationConfig returns disabled in fresh test session', async () => {
|
|
80
|
+
const e = wetEnv();
|
|
81
|
+
if (!e)
|
|
82
|
+
throw new Error('missing env');
|
|
83
|
+
const { client: s, token } = await openTestSession(e);
|
|
84
|
+
try {
|
|
85
|
+
const cfg = await s.getUserIsolationConfig();
|
|
86
|
+
strict_1.default.equal(cfg.enabled, false);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
await fetch(`${e.baseUrl.replace(/\/+$/, '')}/api/v1/test/sessions/current`, {
|
|
90
|
+
method: 'DELETE',
|
|
91
|
+
headers: { 'X-Test-Session': token },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
(0, node_test_1.it)('disableUserIsolation after enable', async () => {
|
|
96
|
+
const e = wetEnv();
|
|
97
|
+
if (!e)
|
|
98
|
+
throw new Error('missing env');
|
|
99
|
+
const c = adminClient(e);
|
|
100
|
+
try {
|
|
101
|
+
await c.setUserIsolationConfig({ enabled: true, namespace_pattern: '/users/${user.id}' });
|
|
102
|
+
await c.disableUserIsolation();
|
|
103
|
+
const cfg = await c.getUserIsolationConfig();
|
|
104
|
+
strict_1.default.equal(cfg.enabled, false);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await c.disableUserIsolation().catch(() => { });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
(0, node_test_1.it)('document write uses transparent prefix for app user', async () => {
|
|
111
|
+
const e = wetEnv();
|
|
112
|
+
if (!e)
|
|
113
|
+
throw new Error('missing env');
|
|
114
|
+
const admin = adminClient(e);
|
|
115
|
+
const suffix = (0, node_crypto_1.randomUUID)().slice(0, 8);
|
|
116
|
+
const email = `js_iso_${suffix}@apitest.invalid`;
|
|
117
|
+
const password = `Js_${suffix}!aA1`;
|
|
118
|
+
const slug = `js-iso-doc-${suffix}`;
|
|
119
|
+
try {
|
|
120
|
+
await admin.setUserIsolationConfig({ enabled: true, namespace_pattern: '/users/${user.id}' });
|
|
121
|
+
const u = await admin.createAppUser(email, password, undefined, [
|
|
122
|
+
client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
|
|
123
|
+
]);
|
|
124
|
+
const app = new client_js_1.XCiteDBClient({
|
|
125
|
+
baseUrl: e.baseUrl,
|
|
126
|
+
context: { branch: 'main', project_id: e.tenantId },
|
|
127
|
+
userIsolation: { enabled: true },
|
|
128
|
+
});
|
|
129
|
+
await app.loginAppUser(email, password);
|
|
130
|
+
await app.writeJsonDocument(`/${slug}`, { _xcite_json_doc: true, v: 1 });
|
|
131
|
+
const doc = await app.readJsonDocument(`/${slug}`);
|
|
132
|
+
strict_1.default.equal(doc.v, 1);
|
|
133
|
+
await admin.deleteJsonDocument(`/users/${u.user_id}/${slug}`);
|
|
134
|
+
await admin.deleteAppUser(u.user_id);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await admin.disableUserIsolation().catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
(0, node_test_1.it)('shared_read_paths passthrough: app reads shared path without double-prefix', async () => {
|
|
141
|
+
const e = wetEnv();
|
|
142
|
+
if (!e)
|
|
143
|
+
throw new Error('missing env');
|
|
144
|
+
const admin = adminClient(e);
|
|
145
|
+
const suffix = (0, node_crypto_1.randomUUID)().slice(0, 8);
|
|
146
|
+
const email = `js_shr_${suffix}@apitest.invalid`;
|
|
147
|
+
const password = `Js_${suffix}!aA1`;
|
|
148
|
+
const sharedPath = `/shared-read-${suffix}`;
|
|
149
|
+
const docPath = `${sharedPath}/doc`;
|
|
150
|
+
try {
|
|
151
|
+
await admin.setUserIsolationConfig({
|
|
152
|
+
enabled: true,
|
|
153
|
+
namespace_pattern: '/users/${user.id}',
|
|
154
|
+
shared_read_paths: [sharedPath],
|
|
155
|
+
});
|
|
156
|
+
await admin.writeJsonDocument(docPath, { _xcite_json_doc: true, tag: 'shared' });
|
|
157
|
+
const created = await admin.createAppUser(email, password, undefined, [
|
|
158
|
+
client_js_1.XCiteDBClient.buildProjectGroup(e.tenantId, 'editor'),
|
|
159
|
+
]);
|
|
160
|
+
const app = new client_js_1.XCiteDBClient({
|
|
161
|
+
baseUrl: e.baseUrl,
|
|
162
|
+
context: { branch: 'main', project_id: e.tenantId },
|
|
163
|
+
userIsolation: { enabled: true, shared_read_paths: [sharedPath] },
|
|
164
|
+
});
|
|
165
|
+
await app.loginAppUser(email, password);
|
|
166
|
+
const doc = await app.readJsonDocument(docPath);
|
|
167
|
+
strict_1.default.equal(doc.tag, 'shared');
|
|
168
|
+
await admin.deleteJsonDocument(docPath);
|
|
169
|
+
await admin.deleteAppUser(created.user_id);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await admin.disableUserIsolation().catch(() => { });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
package/llms-full.txt
CHANGED
|
@@ -518,7 +518,7 @@ Can also use `"query"` instead of `"identifier"` to target multiple documents by
|
|
|
518
518
|
|
|
519
519
|
# Search
|
|
520
520
|
|
|
521
|
-
Full-text search (
|
|
521
|
+
Full-text search uses embedded XciteFTS (enable per project in server settings).
|
|
522
522
|
|
|
523
523
|
**Base path:** `/api/v1/search`
|
|
524
524
|
|
package/llms.txt
CHANGED
|
@@ -161,7 +161,7 @@ const lock = await client.acquireLock('/manual/v1/intro');
|
|
|
161
161
|
// ... edit ...
|
|
162
162
|
await client.releaseLock('/manual/v1/intro', lock.lock_id);
|
|
163
163
|
|
|
164
|
-
// Full-text search (
|
|
164
|
+
// Full-text search (embedded XciteFTS; enable per project in server settings)
|
|
165
165
|
const results = await client.search({ query: 'installation guide', limit: 20 });
|
|
166
166
|
|
|
167
167
|
// Subscribe to real-time changes via WebSocket
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcitedbs/client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "XCiteDB BaaS client SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"prepublishOnly": "npm run build",
|
|
10
|
-
"test": "node --test dist/**/*.test.js
|
|
10
|
+
"test": "npm run build && node --test dist/**/*.test.js",
|
|
11
|
+
"test:only": "node --test dist/**/*.test.js"
|
|
11
12
|
},
|
|
12
13
|
"keywords": [
|
|
13
14
|
"xcitedb",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {},
|
|
36
37
|
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.14.0",
|
|
37
39
|
"typescript": "^5.0.0"
|
|
38
40
|
},
|
|
39
41
|
"files": [
|