@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 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.authHeaders(),
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
- await this.request('POST', '/api/v1/documents', xml, {
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
- return this.request('GET', `/api/v1/documents/by-id${q}`);
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: query.match,
900
- match_start: query.match_start,
901
- match_end: query.match_end,
902
- regex: query.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 (query.contains !== undefined) {
908
- const c = Array.isArray(query.contains) ? query.contains.join(',') : query.contains;
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 (query.required_meta_paths !== undefined && query.required_meta_paths.length > 0) {
912
- params.required_meta_paths = query.required_meta_paths.join(',');
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 (query.filter_any_meta === true) {
1111
+ if (pq.filter_any_meta === true) {
915
1112
  params.any_meta = '1';
916
1113
  }
917
- return this.request('GET', `/api/v1/documents${buildQuery(params)}`);
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
- return this.request('GET', `/api/v1/documents/changed${buildQuery({ branch, from_date: fromDate, to_date: toDate })}`);
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: query.match,
946
- match_start: query.match_start,
947
- match_end: query.match_end,
948
- regex: query.regex,
949
- limit: query.limit,
950
- offset: query.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 (query.contains !== undefined) {
953
- params.contains = Array.isArray(query.contains)
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 (query.required_meta_paths !== undefined && query.required_meta_paths.length > 0) {
958
- params.required_meta_paths = query.required_meta_paths.join(',');
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 (query.filter_any_meta === true) {
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: data,
968
- total: data.length,
1167
+ identifiers: ids,
1168
+ total: ids.length,
969
1169
  offset: 0,
970
- limit: data.length,
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: query.match_start,
1025
- match: query.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', { query });
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
- return this.request('POST', '/api/v1/locks', { identifier, expires });
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
- return this.request('GET', `/api/v1/locks${buildQuery({ identifier })}`);
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 data = await this.request('GET', `/api/v1/json-documents/list${buildQuery({ match, limit, offset })}`);
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 (backed by Meilisearch or Elasticsearch).
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 (requires Meilisearch or Elasticsearch backend)
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.9",
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 2>/dev/null || echo 'No tests'"
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": [