@xcitedbs/client 0.2.11 → 0.2.13

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/README.md CHANGED
@@ -34,6 +34,19 @@ const docs = await client.queryByIdentifier('/test1', 'FirstMatch');
34
34
  await client.put('app.settings', { theme: 'dark' });
35
35
  ```
36
36
 
37
+ ### Full-text search (temporal)
38
+
39
+ `client.search()` accepts `TextSearchQuery` with optional **`at_date`** (point-in-time), **`date_from`** / **`date_to`** (range overlap), and **`mode`: `'fts'`** for pure keyword search. Omit temporal fields for the default “current” posting view. Hits may include **`valid_from`** / **`valid_to`** (7-character internal date keys returned by the server).
40
+
41
+ ```typescript
42
+ await client.search({
43
+ query: 'installation guide',
44
+ mode: 'fts',
45
+ at_date: '2024-06-01',
46
+ limit: 20,
47
+ });
48
+ ```
49
+
37
50
  ### WebSocket
38
51
 
39
52
  ```typescript
@@ -45,6 +58,15 @@ client.subscribe(
45
58
 
46
59
  With JWT in browsers, tokens are passed as `access_token` query parameter on the WebSocket URL. API keys can use `api_key` query parameter.
47
60
 
61
+ ## Test sessions (ephemeral and overlay)
62
+
63
+ Call **`POST /api/v1/test/sessions`** with your normal API key or Bearer (same project context as usual). Use **`XCiteDBClient.createTestSession({ baseUrl, apiKey, … })`** to get a client that sends **`X-Test-Session`** on requests.
64
+
65
+ - **Default:** isolated empty LMDB under the server’s `_test/<uuid>/` (writes never touch production).
66
+ - **Overlay:** pass **`overlay: true`** in **`createTestSession`** options (or **`POST`** body **`{"overlay":true}`**). The server layers a writable LMDB on top of the **current project’s on-disk data opened read-only** so you can debug against real data; changes still live only under `_test/<uuid>/`. Use project-scoped credentials or platform Bearer + **`X-Project-Id`** as when calling production APIs.
67
+
68
+ Tear down with **`destroyTestSession()`** (or **`DELETE /api/v1/test/sessions/current`** with the session header). See **`llms.txt`** / **`llms-full.txt`** in this package for full behavior, limits, and auth notes.
69
+
48
70
  ## Build
49
71
 
50
72
  ```bash
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, PublishResult, WorkspaceInfo, 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';
1
+ import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, PublishResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, XCiteQuery, UserIsolationConfig } from './types';
2
2
  import { WebSocketSubscription } from './websocket';
3
3
  export declare class XCiteDBClient {
4
4
  private baseUrl;
@@ -18,10 +18,13 @@ export declare class XCiteDBClient {
18
18
  private testRequireAuth?;
19
19
  private userIsolation?;
20
20
  private cachedAppUserId?;
21
+ private readonly requestTimeoutMs?;
21
22
  constructor(options: XCiteDBClientOptions);
22
23
  /**
23
- * Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
24
+ * Create an ephemeral test database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
24
25
  * then returns a client that sends `X-Test-Session` (auth-free by default).
26
+ * With `opts.overlay === true`, the server stores overlay mode: reads merge the empty `_test/...` LMDB
27
+ * over the current project's production data (read-only base); writes stay under `_test/...` only.
25
28
  */
26
29
  static createTestSession(opts: CreateTestSessionOptions): Promise<XCiteDBClient>;
27
30
  /**
@@ -433,12 +436,18 @@ export declare class XCiteDBClient {
433
436
  queryLog(query: XCiteQuery, fromDate: string, toDate: string): Promise<LogEntry[]>;
434
437
  addMeta(identifier: string, value: unknown, path?: string, opts?: {
435
438
  mode?: 'set' | 'append';
439
+ overwrite?: boolean;
436
440
  }): Promise<boolean>;
437
441
  addMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean, opts?: {
438
442
  mode?: 'set' | 'append';
443
+ overwrite?: boolean;
444
+ }): Promise<boolean>;
445
+ appendMeta(identifier: string, value: unknown, path?: string, opts?: {
446
+ overwrite?: boolean;
447
+ }): Promise<boolean>;
448
+ appendMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean, opts?: {
449
+ overwrite?: boolean;
439
450
  }): Promise<boolean>;
440
- appendMeta(identifier: string, value: unknown, path?: string): Promise<boolean>;
441
- appendMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean): Promise<boolean>;
442
451
  queryMeta<T = MetaValue>(identifier: string, path?: string): Promise<T>;
443
452
  queryMetaByQuery<T = MetaValue>(query: XCiteQuery, path?: string): Promise<T>;
444
453
  clearMeta(query: XCiteQuery): Promise<boolean>;
@@ -484,6 +493,18 @@ export declare class XCiteDBClient {
484
493
  getProjectSearchSettings(): Promise<ProjectSearchSettings>;
485
494
  /** Update project search settings (`PUT /api/v1/project/settings/search`). Returns the same shape as GET. */
486
495
  updateProjectSearchSettings(patch: ProjectSearchSettingsUpdate): Promise<ProjectSearchSettings>;
496
+ /** Per-project `document.conf` override (`GET /api/v1/project/settings/doc-conf`). */
497
+ getProjectDocConf(): Promise<ProjectDocConfResponse>;
498
+ /** Save or clear project `document.conf` (`PUT /api/v1/project/settings/doc-conf`). */
499
+ updateProjectDocConf(body: {
500
+ doc_conf_text: string;
501
+ } | {
502
+ clear: true;
503
+ }): Promise<ProjectDocConfResponse>;
504
+ /** Remove project override; server uses platform default (`DELETE /api/v1/project/settings/doc-conf`). */
505
+ deleteProjectDocConf(): Promise<ProjectDocConfResponse>;
506
+ /** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
507
+ getPlatformDefaultDocConf(): Promise<PlatformDefaultDocConfResponse>;
487
508
  /** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
488
509
  getVectorIndexEstimate(): Promise<VectorIndexEstimate>;
489
510
  /** Start background estimate (202); cancel prior session for this tenant. */
@@ -502,12 +523,16 @@ export declare class XCiteDBClient {
502
523
  * The final event has `done: true` and may include `sources`.
503
524
  */
504
525
  ragQueryStream(options: Omit<RagQueryOptions, 'stream'>, onEvent: (ev: RagStreamEvent) => void): Promise<void>;
505
- writeJsonDocument(identifier: string, data: unknown): Promise<void>;
526
+ writeJsonDocument(identifier: string, data: unknown, opts?: {
527
+ overwrite?: boolean;
528
+ }): Promise<void>;
506
529
  readJsonDocument<T = unknown>(identifier: string): Promise<T>;
507
530
  deleteJsonDocument(identifier: string): Promise<void>;
508
531
  listJsonDocuments(match?: string, limit?: number, offset?: number): Promise<ListIdentifiersResult>;
509
532
  /** JSON document shorthand — same as {@link writeJsonDocument}. */
510
- put(identifier: string, data: unknown): Promise<void>;
533
+ put(identifier: string, data: unknown, opts?: {
534
+ overwrite?: boolean;
535
+ }): Promise<void>;
511
536
  /** JSON document read — same as {@link readJsonDocument}. */
512
537
  get<T = unknown>(identifier: string): Promise<T>;
513
538
  /** JSON document delete — same as {@link deleteJsonDocument}. */
package/dist/client.js CHANGED
@@ -18,6 +18,30 @@ function buildQuery(params) {
18
18
  const s = sp.toString();
19
19
  return s ? `?${s}` : '';
20
20
  }
21
+ function warnIfHttpOnTlsPort(baseUrl) {
22
+ try {
23
+ const u = new URL(baseUrl);
24
+ if (u.protocol !== 'http:')
25
+ return;
26
+ if (u.port === '443') {
27
+ if (typeof console !== 'undefined' && typeof console.warn === 'function') {
28
+ console.warn('[@xcitedbs/client] baseUrl uses http: on port 443; use https:// to avoid hangs or TLS errors.');
29
+ }
30
+ }
31
+ }
32
+ catch {
33
+ /* ignore invalid baseUrl */
34
+ }
35
+ }
36
+ /** Uses `AbortSignal.timeout` when the runtime supports it. */
37
+ function requestTimeoutSignal(ms) {
38
+ if (ms === undefined || ms <= 0)
39
+ return undefined;
40
+ const ctor = AbortSignal;
41
+ if (typeof ctor.timeout === 'function')
42
+ return ctor.timeout(ms);
43
+ return undefined;
44
+ }
21
45
  class XCiteDBClient {
22
46
  constructor(options) {
23
47
  this.baseUrl = options.baseUrl.replace(/\/+$/, '');
@@ -34,10 +58,14 @@ class XCiteDBClient {
34
58
  this.testSessionToken = options.testSessionToken;
35
59
  this.testRequireAuth = options.testRequireAuth === true;
36
60
  this.userIsolation = options.userIsolation;
61
+ this.requestTimeoutMs = options.requestTimeoutMs;
62
+ warnIfHttpOnTlsPort(this.baseUrl);
37
63
  }
38
64
  /**
39
- * Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
65
+ * Create an ephemeral test database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
40
66
  * then returns a client that sends `X-Test-Session` (auth-free by default).
67
+ * With `opts.overlay === true`, the server stores overlay mode: reads merge the empty `_test/...` LMDB
68
+ * over the current project's production data (read-only base); writes stay under `_test/...` only.
41
69
  */
42
70
  static async createTestSession(opts) {
43
71
  const temp = new XCiteDBClient({
@@ -53,8 +81,9 @@ class XCiteDBClient {
53
81
  onAppUserTokensUpdated: opts.onAppUserTokensUpdated,
54
82
  onSessionInvalid: opts.onSessionInvalid,
55
83
  userIsolation: opts.userIsolation,
84
+ requestTimeoutMs: opts.requestTimeoutMs,
56
85
  });
57
- const data = await temp.request('POST', '/api/v1/test/sessions', undefined, undefined, { no401Retry: true });
86
+ const data = await temp.request('POST', '/api/v1/test/sessions', opts.overlay === true ? { overlay: true } : undefined, undefined, { no401Retry: true });
58
87
  return new XCiteDBClient({
59
88
  baseUrl: opts.baseUrl,
60
89
  apiKey: opts.testRequireAuth ? opts.apiKey : undefined,
@@ -70,6 +99,7 @@ class XCiteDBClient {
70
99
  testSessionToken: data.session_token,
71
100
  testRequireAuth: opts.testRequireAuth,
72
101
  userIsolation: opts.userIsolation,
102
+ requestTimeoutMs: opts.requestTimeoutMs,
73
103
  });
74
104
  }
75
105
  /**
@@ -444,6 +474,9 @@ class XCiteDBClient {
444
474
  init.body = JSON.stringify(body);
445
475
  }
446
476
  }
477
+ const sig = requestTimeoutSignal(this.requestTimeoutMs);
478
+ if (sig)
479
+ init.signal = sig;
447
480
  const res = await fetch(url, init);
448
481
  const text = await res.text();
449
482
  let data;
@@ -1319,6 +1352,8 @@ class XCiteDBClient {
1319
1352
  const body = { identifier: this.isoPrefixId(identifier), value, path };
1320
1353
  if (opts?.mode === 'append')
1321
1354
  body.mode = 'append';
1355
+ if (opts?.overwrite)
1356
+ body.overwrite = true;
1322
1357
  const r = await this.request('POST', '/api/v1/meta', body);
1323
1358
  return r?.ok !== false;
1324
1359
  }
@@ -1331,14 +1366,16 @@ class XCiteDBClient {
1331
1366
  };
1332
1367
  if (opts?.mode === 'append')
1333
1368
  body.mode = 'append';
1369
+ if (opts?.overwrite)
1370
+ body.overwrite = true;
1334
1371
  const r = await this.request('POST', '/api/v1/meta', body);
1335
1372
  return r?.ok !== false;
1336
1373
  }
1337
- async appendMeta(identifier, value, path = '') {
1338
- return this.addMeta(identifier, value, path, { mode: 'append' });
1374
+ async appendMeta(identifier, value, path = '', opts) {
1375
+ return this.addMeta(identifier, value, path, { mode: 'append', ...opts });
1339
1376
  }
1340
- async appendMetaByQuery(query, value, path = '', firstMatch = false) {
1341
- return this.addMetaByQuery(query, value, path, firstMatch, { mode: 'append' });
1377
+ async appendMetaByQuery(query, value, path = '', firstMatch = false, opts) {
1378
+ return this.addMetaByQuery(query, value, path, firstMatch, { mode: 'append', ...opts });
1342
1379
  }
1343
1380
  async queryMeta(identifier, path = '') {
1344
1381
  return this.request('GET', `/api/v1/meta${buildQuery({ identifier: this.isoPrefixId(identifier), path })}`);
@@ -1410,12 +1447,18 @@ class XCiteDBClient {
1410
1447
  body.offset = q.offset;
1411
1448
  if (q.limit !== undefined)
1412
1449
  body.limit = q.limit;
1413
- if (q.mode)
1450
+ if (q.mode !== undefined)
1414
1451
  body.mode = q.mode;
1415
1452
  if (q.min_score !== undefined)
1416
1453
  body.min_score = q.min_score;
1417
1454
  if (q.semantic_weight !== undefined)
1418
1455
  body.semantic_weight = q.semantic_weight;
1456
+ if (q.at_date !== undefined && q.at_date !== '')
1457
+ body.at_date = q.at_date;
1458
+ if (q.date_from !== undefined && q.date_from !== '')
1459
+ body.date_from = q.date_from;
1460
+ if (q.date_to !== undefined && q.date_to !== '')
1461
+ body.date_to = q.date_to;
1419
1462
  const data = await this.request('POST', '/api/v1/search', body);
1420
1463
  const hits = [];
1421
1464
  if (Array.isArray(data.hits)) {
@@ -1436,6 +1479,12 @@ class XCiteDBClient {
1436
1479
  if (o.source === 'fts' || o.source === 'semantic' || o.source === 'both') {
1437
1480
  hit.source = o.source;
1438
1481
  }
1482
+ if (typeof o.valid_from === 'string' && o.valid_from.length > 0) {
1483
+ hit.valid_from = o.valid_from;
1484
+ }
1485
+ if (typeof o.valid_to === 'string' && o.valid_to.length > 0) {
1486
+ hit.valid_to = o.valid_to;
1487
+ }
1439
1488
  hits.push(hit);
1440
1489
  }
1441
1490
  }
@@ -1460,6 +1509,22 @@ class XCiteDBClient {
1460
1509
  async updateProjectSearchSettings(patch) {
1461
1510
  return this.request('PUT', '/api/v1/project/settings/search', patch);
1462
1511
  }
1512
+ /** Per-project `document.conf` override (`GET /api/v1/project/settings/doc-conf`). */
1513
+ async getProjectDocConf() {
1514
+ return this.request('GET', '/api/v1/project/settings/doc-conf');
1515
+ }
1516
+ /** Save or clear project `document.conf` (`PUT /api/v1/project/settings/doc-conf`). */
1517
+ async updateProjectDocConf(body) {
1518
+ return this.request('PUT', '/api/v1/project/settings/doc-conf', body);
1519
+ }
1520
+ /** Remove project override; server uses platform default (`DELETE /api/v1/project/settings/doc-conf`). */
1521
+ async deleteProjectDocConf() {
1522
+ return this.request('DELETE', '/api/v1/project/settings/doc-conf');
1523
+ }
1524
+ /** Embedded platform default `document.conf` text (`GET /api/v1/platform/default-doc-conf`). */
1525
+ async getPlatformDefaultDocConf() {
1526
+ return this.request('GET', '/api/v1/platform/default-doc-conf');
1527
+ }
1463
1528
  /** Blocking full DB scan (admin; no calls to embedding API). Prefer {@link postVectorIndexEstimateSession} for UI. */
1464
1529
  async getVectorIndexEstimate() {
1465
1530
  return this.request('GET', '/api/v1/project/settings/search/vector-index-estimate', undefined);
@@ -1593,8 +1658,11 @@ class XCiteDBClient {
1593
1658
  }
1594
1659
  throw new types_1.XCiteDBError('RAG stream failed after retry', 401, null);
1595
1660
  }
1596
- async writeJsonDocument(identifier, data) {
1597
- await this.request('POST', '/api/v1/json-documents', { identifier: this.isoPrefixId(identifier), data });
1661
+ async writeJsonDocument(identifier, data, opts) {
1662
+ const body = { identifier: this.isoPrefixId(identifier), data };
1663
+ if (opts?.overwrite)
1664
+ body.overwrite = true;
1665
+ await this.request('POST', '/api/v1/json-documents', body);
1598
1666
  }
1599
1667
  async readJsonDocument(identifier) {
1600
1668
  return this.request('GET', `/api/v1/json-documents${buildQuery({ identifier: this.isoPrefixId(identifier) })}`);
@@ -1623,8 +1691,8 @@ class XCiteDBClient {
1623
1691
  return { identifiers: [], total: 0, offset: 0, limit: 0 };
1624
1692
  }
1625
1693
  /** JSON document shorthand — same as {@link writeJsonDocument}. */
1626
- async put(identifier, data) {
1627
- return this.writeJsonDocument(identifier, data);
1694
+ async put(identifier, data, opts) {
1695
+ return this.writeJsonDocument(identifier, data, opts);
1628
1696
  }
1629
1697
  /** JSON document read — same as {@link readJsonDocument}. */
1630
1698
  async get(identifier) {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { XCiteDBClient } from './client';
2
2
  export { WebSocketSubscription } from './websocket';
3
- export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationOptions, WorkspaceInfo, 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, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, UserIsolationConfig, UserIsolationOptions, WorkspaceInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
4
  export { XCiteDBError } from './types';
package/dist/types.d.ts CHANGED
@@ -41,12 +41,20 @@ 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';
44
+ /**
45
+ * Default server behavior when omitted: `auto` (hybrid if FTS+vector enabled, else semantic or FTS).
46
+ * Explicit `fts` / `semantic` / `hybrid` require the matching capabilities to be enabled.
47
+ */
48
+ mode?: 'auto' | 'fts' | 'semantic' | 'hybrid';
46
49
  /** Minimum cosine similarity for semantic / hybrid vector leg (default 0.3). */
47
50
  min_score?: number;
48
51
  /** Hybrid only: weight of semantic vs FTS in weighted RRF (0–1, default 0.5). */
49
52
  semantic_weight?: number;
53
+ /** Point-in-time FTS filter (ISO date string; server maps to internal date key). */
54
+ at_date?: string;
55
+ /** Range FTS filter: interval overlap with [date_from, date_to) (ISO dates). */
56
+ date_from?: string;
57
+ date_to?: string;
50
58
  }
51
59
  export interface TextSearchHit {
52
60
  identifier: string;
@@ -59,6 +67,9 @@ export interface TextSearchHit {
59
67
  score: number;
60
68
  /** Present for semantic / hybrid search. */
61
69
  source?: 'fts' | 'semantic' | 'both';
70
+ /** FTS temporal window for this hit (internal 7-char keys), when returned by the server. */
71
+ valid_from?: string;
72
+ valid_to?: string;
62
73
  }
63
74
  export interface TextSearchResult {
64
75
  hits: TextSearchHit[];
@@ -153,6 +164,15 @@ export interface ProjectSearchSettingsUpdate {
153
164
  llm_base_url?: string | null;
154
165
  llm_api_key?: string;
155
166
  }
167
+ /** `GET /api/v1/project/settings/doc-conf` (and returned after `PUT` / `DELETE`). */
168
+ export interface ProjectDocConfResponse {
169
+ has_project_override: boolean;
170
+ doc_conf_text: string | null;
171
+ }
172
+ /** `GET /api/v1/platform/default-doc-conf` */
173
+ export interface PlatformDefaultDocConfResponse {
174
+ doc_conf_text: string;
175
+ }
156
176
  /** `POST /api/v1/rag/query` (non-streaming: set `stream: false` or omit). */
157
177
  export interface RagQueryOptions {
158
178
  question: string;
@@ -314,6 +334,11 @@ export interface DatabaseContext {
314
334
  workspace?: string;
315
335
  /** @deprecated Prefer {@link DatabaseContext.workspace}. Sent as `X-Branch` when `workspace` is unset. */
316
336
  branch?: string;
337
+ /**
338
+ * As-of revision time, sent as `X-Date`. Accepted forms (whole string must match; trailing spaces ignored):
339
+ * `mm/dd/yyyy`, `mm/dd/yyyy:HH:MM:SS`, ISO `YYYY-MM-DDTHH:MM:SS` with optional numeric timezone,
340
+ * `YYYY-MM-DD HH:MM:SS`, and ISO date-only `YYYY-MM-DD`.
341
+ */
317
342
  date?: string;
318
343
  prefix?: string;
319
344
  /**
@@ -392,6 +417,11 @@ export interface XCiteDBClientOptions {
392
417
  testRequireAuth?: boolean;
393
418
  /** Auto-prefix identifiers for app-user sessions (see {@link UserIsolationOptions}). */
394
419
  userIsolation?: UserIsolationOptions;
420
+ /**
421
+ * Per-request timeout in milliseconds for normal REST calls (uses `AbortSignal.timeout` when available).
422
+ * Omit for no timeout. Streaming RAG (`ragQueryStream`) does not apply this.
423
+ */
424
+ requestTimeoutMs?: number;
395
425
  }
396
426
  /** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
397
427
  export interface CreateTestSessionOptions {
@@ -403,12 +433,17 @@ export interface CreateTestSessionOptions {
403
433
  context?: DatabaseContext;
404
434
  platformConsole?: boolean;
405
435
  projectId?: string;
436
+ /**
437
+ * When true, creates an overlay test session: writable ephemeral LMDB with production project data as read-only base.
438
+ */
439
+ overlay?: boolean;
406
440
  /** Keep `apiKey` / `accessToken` on the client and send `X-Test-Auth: required` on each request. */
407
441
  testRequireAuth?: boolean;
408
442
  onSessionTokensUpdated?: (pair: TokenPair) => void;
409
443
  onAppUserTokensUpdated?: (pair: AppUserTokenPair) => void;
410
444
  onSessionInvalid?: () => void;
411
445
  userIsolation?: UserIsolationOptions;
446
+ requestTimeoutMs?: number;
412
447
  }
413
448
  /** Application user (tenant-scoped), distinct from developer users. */
414
449
  export interface AppUser {
package/llms-full.txt CHANGED
@@ -26,7 +26,9 @@ Before reading the full reference, note these critical differences from typical
26
26
 
27
27
  9. **OpenAPI:** See repository `docs/openapi.yaml` for a machine-readable route map.
28
28
 
29
- 10. **Ephemeral test sessions.** `POST /api/v1/test/sessions` (authenticated) returns a UUID **`session_token`**. Clients send **`X-Test-Session: <token>`** on API calls to use an isolated, TTL- and quota-limited LMDB instead of production project data. Unless **`X-Test-Auth: required`** is set, **developer** JWT/API-key checks are bypassed (synthetic admin for wet tests), but **app-user** identity via **`X-App-User-Token`** or Bearer app-user JWT is still recognized. Management routes under **`/api/v1/test/*`** must not include `X-Test-Session`. The test store starts empty (no cloned production project config).
29
+ 10. **Ephemeral test sessions.** `POST /api/v1/test/sessions` (authenticated) returns a UUID **`session_token`**. Clients send **`X-Test-Session: <token>`** on API calls to use an isolated, TTL- and quota-limited LMDB instead of production project data. Unless **`X-Test-Auth: required`** is set, **developer** JWT/API-key checks are bypassed (synthetic admin for wet tests), but **app-user** identity via **`X-App-User-Token`** or Bearer app-user JWT is still recognized. Management routes under **`/api/v1/test/*`** must not include `X-Test-Session`. With a default body (omit or `{}`), the test LMDB starts **empty** (no cloned production project config).
30
+
31
+ 11. **Overlay test sessions.** Same **`POST`**, with JSON **`{"overlay":true}`**, while authenticated for the **project to debug** (project-scoped API key, or platform Bearer + **`X-Project-Id`**). The session metadata records overlay mode; subsequent requests need only **`X-Test-Session`**. The server opens **`XCiteDB(_test/<uuid>/data, <production data path>)`**: production is used as a **read-only base**; reads merge overlay + base; **writes never modify production**. If the production data directory is missing, opening the session database fails. JS **`createTestSession({ …, overlay: true })`**, C++ **`test_session_overlay`** + **`create_test_session`**, MCP **`create_test_session`** tool **`overlay: true`**.
30
32
 
31
33
  ## Choosing the Right Versioning Approach
32
34
 
@@ -58,7 +60,7 @@ Legacy REST paths under `/api/v1/branches`, `/commits`, `/tags`, `/diff` remain
58
60
 
59
61
  6. **Self-registration uses server-configured default groups.** `registerAppUser()` assigns groups from the server's `auth.app_users.default_groups` config, not from the client request. To assign specific groups, use the admin endpoint `createAppUser()` instead, or update groups after registration via `updateAppUserGroups()`.
60
62
 
61
- 7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real, empty, isolated LMDB that is automatically scoped away from production and destroyed after the test. See "Ephemeral test sessions" below.
63
+ 7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real LMDB under `_test/<uuid>/` (empty by default, or **overlay** on read-only production with **`{"overlay":true}`** / **`overlay: true`** / **`test_session_overlay`**). See "Ephemeral test sessions" below.
62
64
 
63
65
  ---
64
66
 
@@ -205,7 +207,7 @@ For **integration and wet tests** against a shared BaaS host without touching pr
205
207
 
206
208
  | Step | What to do |
207
209
  |------|------------|
208
- | **Create** | **`POST /api/v1/test/sessions`** with normal **`Authorization: Bearer …`** or **`X-API-Key`**. Response includes a **`session_token`** (UUID). Server enforces per-credential limits (`test.max_sessions_per_key`, `test.session_ttl_seconds`, `test.max_test_db_size_bytes` in server config). |
210
+ | **Create** | **`POST /api/v1/test/sessions`** with normal **`Authorization: Bearer …`** or **`X-API-Key`**. Response includes a **`session_token`** (UUID). Server enforces per-credential limits (`test.max_sessions_per_key`, `test.session_ttl_seconds`, `test.max_test_db_size_bytes` in server config). Optional JSON body **`{"overlay":true}`** provisions a **read-through production** session (writable delta only under `_test/<uuid>/`; production LMDB is read-only base). |
209
211
  | **Use** | Send **`X-Test-Session: <session_token>`** on document and other data API requests. The server routes to a dedicated LMDB under its data root (`_test/<id>/`), not the caller’s production tenant. **`tenant_id` / `X-Project-Id` semantics do not select production** while the test header is present—the synthetic test tenant is implied. |
210
212
  | **Auth** | **Default:** developer auth (API key / platform JWT) is **bypassed** with a synthetic admin identity. However, **app-user identity is still recognized**: if `X-App-User-Token` or a Bearer app-user JWT is present, the request runs as that app user (for routes like `/app/auth/me`). **`X-Test-Auth: required`:** all auth is validated normally; ABAC applies, but data still comes from the test session DB. |
211
213
  | **Manage** | **`GET /api/v1/test/sessions`** — list sessions for the current credential. **`DELETE /api/v1/test/sessions/current`** — destroy the session named by **`X-Test-Session`** (no other auth). **`DELETE /api/v1/test/sessions/all`** — destroy all sessions for the credential. **`DELETE /api/v1/test/sessions/{token}`** — destroy one session if owned by the credential. Do **not** send **`X-Test-Session`** on these `/api/v1/test/*` routes. |
@@ -213,9 +215,9 @@ For **integration and wet tests** against a shared BaaS host without touching pr
213
215
 
214
216
  **SDK usage (summary):**
215
217
 
216
- - **JavaScript/TypeScript:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, … })` returns a client configured with `testSessionToken`; optional `testRequireAuth: true` maps to `X-Test-Auth: required`. `destroyTestSession()` calls `DELETE …/test/sessions/current`.
217
- - **Python:** `async with XCiteDBClient.test_session(base_url, api_key=…, …)` provisions and tears down; or pass `test_session_token` / `test_require_auth` to the constructor.
218
- - **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key` (and optional `test_require_auth`); `destroy_test_session()`.
218
+ - **JavaScript/TypeScript:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, … })` returns a client configured with `testSessionToken`; optional **`overlay: true`** for overlay mode; optional `testRequireAuth: true` maps to `X-Test-Auth: required`. `destroyTestSession()` calls `DELETE …/test/sessions/current`.
219
+ - **Python:** `async with XCiteDBClient.test_session(base_url, api_key=…, …)` provisions and tears down; or pass `test_session_token` / `test_require_auth` to the constructor. For overlay until the helper accepts a flag, call **`POST /api/v1/test/sessions`** with JSON **`{"overlay":true}`** then construct the client with the returned token.
220
+ - **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key`, optional **`test_session_overlay = true`**, and optional `test_require_auth`; `destroy_test_session()`.
219
221
 
220
222
  **JavaScript/TypeScript — complete test scaffold (Vitest / Jest):**
221
223
 
@@ -450,14 +452,23 @@ Returns `{ parent_path, parent_is_identifier, children: [{ segment, full_path, i
450
452
 
451
453
  **`POST /api/v1/json-documents`**
452
454
 
455
+ JSON body fields:
456
+
457
+ | Field | Required | Meaning |
458
+ |-------|----------|---------|
459
+ | `identifier` | yes | Document path key |
460
+ | `data` | yes | JSON value to merge or store |
461
+ | `overwrite` | no (default `false`) | When `true`, delete all metadata under the document root first, then write `data` only. When `false`, **`data` is merged** into any existing document (nested objects combine fields; nested arrays follow meta merge rules—see Metadata). |
462
+
453
463
  ```json
454
464
  {
455
465
  "identifier": "app.settings",
456
- "data": { "theme": "dark", "maxUploadMb": 25 }
466
+ "data": { "theme": "dark", "maxUploadMb": 25 },
467
+ "overwrite": false
457
468
  }
458
469
  ```
459
470
 
460
- Note: The SDK method uses `identifier` and `data` fields. Some older documentation shows `key` and `value`.
471
+ Note: SDKs use `identifier` and `data` (optional `overwrite`). Some older documentation shows `key` and `value`.
461
472
 
462
473
  ## Read JSON document
463
474
 
@@ -501,15 +512,26 @@ Attach structured **JSON metadata** to documents or nodes.
501
512
 
502
513
  **`POST /api/v1/meta`**
503
514
 
515
+ | Field | Required | Meaning |
516
+ |-------|----------|---------|
517
+ | `identifier` **or** `query` | one required | Target document id or document query |
518
+ | `value` | yes | JSON to write (omit only for string-specific query batch paths handled by the server) |
519
+ | `path` | no (default `""`) | Meta path (dot-separated keys; `[i]` for array indices) |
520
+ | `mode` | no (default `"set"`) | `"set"` — write/replace at `path`. For **arrays** at `path`, indices `0..n-1` are written and any previous tail beyond the new length is cleared. `"append"` — for **arrays** at `path`, new elements are written after existing indices (extend in place). |
521
+ | `overwrite` | no (default `false`) | When `true`, delete existing metadata under `path` before applying `value`. |
522
+ | `first_match` | no | With `query`, only the first matching identifier is updated when `true`. |
523
+
504
524
  ```json
505
525
  {
506
526
  "identifier": "/book/ch1",
507
527
  "value": { "status": "review", "owner": "alice" },
508
- "path": ""
528
+ "path": "",
529
+ "mode": "set",
530
+ "overwrite": false
509
531
  }
510
532
  ```
511
533
 
512
- Optional `"mode": "append"` to append rather than overwrite.
534
+ Use `"mode": "append"` to extend arrays at `path` instead of replacing them.
513
535
 
514
536
  Can also use `"query"` instead of `"identifier"` to target multiple documents by query filter.
515
537
 
@@ -525,11 +547,17 @@ Can also use `"query"` instead of `"identifier"` to target multiple documents by
525
547
 
526
548
  **`DELETE /api/v1/meta`** — `{ "query": {...} }`
527
549
 
550
+ ## JSON metadata storage model (best practices)
551
+
552
+ - **Field-indexed objects.** Metadata and standalone JSON documents share the same shredded-key storage. **JSON objects** map to named paths so fields can be read or updated without loading a monolithic blob.
553
+ - **Dictionary threshold.** When the **set of distinct field names** on an object reaches the server threshold (default **32**, native setting `meta_dict_field_threshold`), XCiteDB switches that object to **dictionary storage** (`{*}` plus per-field keys) for scalable indexed access.
554
+ - **Modeling guidance.** Prefer stable **`{ "key": value, ... }`** / nested-object shapes for records you look up by key. Arrays are appropriate for ordered lists; use **`mode: "append"`** when appending to a stored array.
555
+
528
556
  ---
529
557
 
530
558
  # Search
531
559
 
532
- Full-text search (backed by Meilisearch or Elasticsearch).
560
+ Full-text search uses **embedded XciteFTS** (LMDB index per project). **Semantic** and **hybrid** (FTS + vector) search are available when vector search is enabled in project settings. The JSON field **`mode`** selects behavior (`auto` picks the best option for the project, or set `fts`, `semantic`, or `hybrid` explicitly).
533
561
 
534
562
  **Base path:** `/api/v1/search`
535
563
 
@@ -543,15 +571,26 @@ Full-text search (backed by Meilisearch or Elasticsearch).
543
571
  "doc_types": ["xml", "json"],
544
572
  "branch": "",
545
573
  "limit": 20,
546
- "offset": 0
574
+ "offset": 0,
575
+ "mode": "fts"
547
576
  }
548
577
  ```
549
578
 
550
- Response: `{ hits: [{ identifier, path, doc_type, branch, snippet, score, xcitepath? }], total, query }`.
579
+ Optional fields (also on SDK `TextSearchQuery` and the MCP `search` tool): **`min_score`**, **`semantic_weight`** for semantic/hybrid.
580
+
581
+ **Temporal FTS** (posting intervals; applies to the FTS keyword index—use **`mode`: `"fts"`** for pure temporal keyword search, or hybrid where the FTS leg participates):
582
+
583
+ | Field | Meaning |
584
+ |-------|---------|
585
+ | **`at_date`** | Date string (e.g. ISO `YYYY-MM-DD` or `mm/dd/yyyy` as accepted by the server). A posting matches if it is valid at that instant: `start <= at_date < end` (internal 7-char keys after `date2key`). |
586
+ | **`date_from`**, **`date_to`** | Date strings defining a half-open range `[date_from, date_to)`. A posting matches if its `[start, end)` **overlaps** that range. Either bound may be omitted (open-ended). |
587
+ | *(none of the three)* | **Current-time** view: only postings whose interval is still “open” (end at the internal max sentinel) are included. |
588
+
589
+ **Response:** `{ hits: [...], total, query }`. Each hit has **`identifier`**, **`path`**, **`doc_type`**, **`branch`**, **`snippet`**, **`score`**, and optionally **`xcitepath`** (XML). Semantic/hybrid hits may include **`source`**: `fts` \| `semantic` \| `both`. When the server infers a temporal window for the matched term(s), hits may include **`valid_from`** and **`valid_to`** as **7-character internal date keys** (same alphabet as revision keys—not ISO). Omitted for full-range/unversioned postings or when not computed.
551
590
 
552
591
  ## Reindex
553
592
 
554
- **`POST /api/v1/search/reindex`** — Rebuilds the search index.
593
+ **`POST /api/v1/search/reindex`** — Rebuilds the full-text search index.
555
594
 
556
595
  ---
557
596
 
@@ -1361,17 +1400,17 @@ interface DatabaseContext {
1361
1400
  - `queryLog(query, fromDate, toDate)` → `LogEntry[]`
1362
1401
 
1363
1402
  ### JSON Documents
1364
- - `writeJsonDocument(identifier, data)` → `void`
1403
+ - `writeJsonDocument(identifier, data, opts?)` → `void` — merge by default; `opts?.overwrite` replaces root
1365
1404
  - `readJsonDocument<T>(identifier)` → `T` (default `unknown`)
1366
1405
  - `deleteJsonDocument(identifier)` → `void`
1367
1406
  - `listJsonDocuments(match?, limit?, offset?)` → `ListIdentifiersResult`
1368
- - `put(identifier, data)` / `get<T>(identifier)` / `remove(identifier)` / `list(match?, limit?, offset?)` — JSON CRUD aliases
1407
+ - `put(identifier, data, opts?)` / `get<T>(identifier)` / `remove(identifier)` / `list(match?, limit?, offset?)` — JSON CRUD aliases (`opts` same as `writeJsonDocument`)
1369
1408
 
1370
1409
  ### Metadata
1371
- - `addMeta(identifier, value, path?, opts?)` → `boolean`
1410
+ - `addMeta(identifier, value, path?, opts?)` → `boolean` — `opts`: `mode?: 'set'|'append'`, `overwrite?: boolean`
1372
1411
  - `addMetaByQuery(query, value, path?, firstMatch?, opts?)` → `boolean`
1373
- - `appendMeta(identifier, value, path?)` → `boolean`
1374
- - `appendMetaByQuery(query, value, path?, firstMatch?)` → `boolean`
1412
+ - `appendMeta(identifier, value, path?, opts?)` → `boolean` — same as `addMeta` with `mode: 'append'`
1413
+ - `appendMetaByQuery(query, value, path?, firstMatch?, opts?)` → `boolean`
1375
1414
  - `queryMeta<T>(identifier, path?)` → `T`
1376
1415
  - `queryMetaByQuery<T>(query, path?)` → `T`
1377
1416
  - `clearMeta(query)` → `boolean`
@@ -1411,7 +1450,7 @@ interface DatabaseContext {
1411
1450
  - `findLocks(identifier)` → `LockInfo[]`
1412
1451
 
1413
1452
  ### Search
1414
- - `search(query: TextSearchQuery)` → `TextSearchResult`
1453
+ - `search(query: TextSearchQuery)` → `TextSearchResult`. Query may include **`at_date`**, **`date_from`**, **`date_to`** (ISO-style or `mm/dd/yyyy` strings) for temporal FTS; set **`mode`: `"fts"`** for keyword-only temporal search. Hits may include **`valid_from`** / **`valid_to`** (7-char internal keys) when the server returns an inferred validity window.
1415
1454
  - `reindex()` → `{ status, message }`
1416
1455
 
1417
1456
  ### Unquery
@@ -1544,6 +1583,7 @@ async def main():
1544
1583
  print(await client.read_json_document("app.settings"))
1545
1584
  print(await client.list_identifiers(XCiteQuery(match_start="/manual/")))
1546
1585
  print(await client.search(TextSearchQuery(query="guide", limit=10)))
1586
+ # Temporal FTS: await client.search(TextSearchQuery(query="guide", mode="fts", at_date="2024-06-01"))
1547
1587
  await client.platform_login("admin@localhost", "password")
1548
1588
  # await client.login_app_user("user@example.com", "pw") # set context.project_id / tenant_id if needed
1549
1589
  async with client.with_workspace("feature-x", message="WIP", auto_merge=True):
package/llms.txt CHANGED
@@ -22,7 +22,7 @@ These are the most common sources of confusion for developers and AI assistants:
22
22
 
23
23
  8. **Project vs tenant id.** In the SDK, prefer `context.project_id` (and `listMyProjects` / `switchProject`). Many JSON bodies and JWT claims still use the field name `tenant_id` for the same value — the client sends that wire name automatically.
24
24
 
25
- 9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). The test DB starts **empty** (no copy of production project config or keys).
25
+ 9. **Ephemeral test sessions (wet tests).** Call **`POST /api/v1/test/sessions`** with a normal API key or Bearer token to get a `session_token` (UUID). Send **`X-Test-Session: <token>`** on subsequent document/API calls to use an isolated, short-lived LMDB instead of production data. By default **developer** auth (API key / platform JWT) is bypassed, but **app-user** identity (`X-App-User-Token` or Bearer app-user JWT) is still recognized. Send **`X-Test-Auth: required`** to exercise full developer JWT/API-key auth and ABAC against the same test database. Do not send `X-Test-Session` on `/api/v1/test/*` management routes. Server limits apply (`test.session_ttl_seconds`, `test.max_sessions_per_key`, `test.max_test_db_size_bytes` in config). By default the test LMDB starts **empty** (no production data). **Overlay mode:** same **`POST`** with JSON body **`{"overlay":true}`** (while authenticated for the project you want to inspect—project-scoped API key, or platform Bearer + **`X-Project-Id`**). The server opens a **writable** LMDB under `_test/<uuid>/` with that project’s on-disk store as a **read-only base** (dual LMDB): reads see production + overlay deltas; **writes never touch production**. **`XCiteDBClient.createTestSession({ …, overlay: true })`** sends that body. After creation, only **`X-Test-Session`** is required on requests (overlay is stored in session metadata).
26
26
 
27
27
  ## Choosing the Right Versioning Approach
28
28
 
@@ -58,7 +58,7 @@ Legacy REST paths (`/api/v1/branches`, `/commits`, `/tags`, `/diff`) remain as *
58
58
 
59
59
  6. **Self-registration uses server-configured default groups.** `registerAppUser()` assigns groups from the server's `auth.app_users.default_groups` config, not from the client request. To set groups explicitly, use **`createAppUser`** with a `groups` array (e.g. `[XCiteDBClient.buildProjectGroup(projectId, 'editor')]`) or **`updateAppUserGroups`**. The server rejects `project:<x>:*` groups when `<x>` is not a known internal project id (avoids mistaking the display name for the tenant id).
60
60
 
61
- 7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real, empty, isolated LMDB that is automatically scoped away from production and destroyed after the test. See "Test mode" below.
61
+ 7. **Do not mock XciteDB in tests — use ephemeral test sessions instead.** Unlike most BaaS platforms, XciteDB has built-in support for isolated, throwaway database sessions specifically designed for wet integration tests. Mocking the client skips the actual storage, versioning, querying, and ABAC behavior, producing tests that don't catch real integration issues. Use `createTestSession()` / `test_session()` / `create_test_session()` (SDK helpers) or `POST /api/v1/test/sessions` directly to get a real LMDB that is scoped under `_test/<uuid>/` and destroyed after the test (empty by default, or **overlay** on read-only production when you pass **`{"overlay":true}`** / **`overlay: true`**). See "Test mode" below.
62
62
 
63
63
  8. **403 on writes with ABAC is often a JWT/group string mismatch.** Decode the app-user access token early: log **`tenant_id`**, **`groups`**, **`sub`**. The middle segment of every **`project:<x>:role`** group must equal **`tenant_id`** exactly. Document write denials may return JSON fields **`policy_id`** and **`hint`** alongside `"Forbidden"`.
64
64
 
@@ -105,11 +105,17 @@ await app.writeJsonDocument('userdata/alice/profile', { ok: true });
105
105
 
106
106
  > **AI assistant instruction:** XciteDB has built-in ephemeral test session support — this capability is **not standard** in most BaaS platforms and is specifically designed to enable meaningful wet integration tests. When writing tests for code that uses XciteDB, **do not mock or stub the XciteDB client**. Instead, use the test session pattern below to run tests against a real, isolated, throwaway database. This produces far more reliable tests because real storage, versioning, querying, and access-control behavior is exercised.
107
107
 
108
- 1. **Provision:** `POST /api/v1/test/sessions` with `Authorization: Bearer …` or `X-API-Key` (same as normal API access). Response JSON includes the session token.
109
- 2. **Run tests:** Every request that should hit the throwaway DB must include **`X-Test-Session: <token>`** (and your usual `X-Workspace` / `context` as needed). Data is stored under the server’s `_test/<session>/` area, not your production tenant.
108
+ 1. **Provision:** `POST /api/v1/test/sessions` with `Authorization: Bearer …` or `X-API-Key` (same as normal API access). Optional JSON body **`{"overlay":true}`** creates an **overlay** session (read-through production, writes only under `_test/<session>/`). Response JSON includes the session token (and **`"overlay": true`** when applicable).
109
+ 2. **Run tests:** Every request that should hit the throwaway DB must include **`X-Test-Session: <token>`** (and your usual `X-Workspace` / `context` as needed). Data writes go under the server’s `_test/<session>/` tree; overlay sessions **do not** write to production paths.
110
110
  3. **Auth behavior:** Developer auth (API key / platform JWT) is bypassed by default for frictionless tests. **App-user identity** (`X-App-User-Token` or Bearer app-user JWT) **is still recognized** in default mode, so `registerAppUser` → `loginAppUser` → `appUserMe` works inside a test session. To also exercise developer auth and ABAC policies, set **`X-Test-Auth: required`** and send normal credentials; the DB is still the test session’s.
111
111
  4. **Cleanup:** `DELETE /api/v1/test/sessions/current` with `X-Test-Session` (no other auth), or `DELETE /api/v1/test/sessions/all` / `DELETE /api/v1/test/sessions/{token}` with normal auth for the owning key or JWT.
112
- 5. **SDKs:** **JS/TS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, … })`, optional `testRequireAuth`, then `destroyTestSession()`. **Python:** `async with XCiteDBClient.test_session(...)` or manual token + `test_session_token` / `test_require_auth` constructor args. **C++:** `XCiteDBClient::create_test_session(options)`, `destroy_test_session()`, optional `test_require_auth` in options.
112
+ 5. **SDKs:** **JS/TS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, … })`, optional `overlay: true`, optional `testRequireAuth`, then `destroyTestSession()`. **Python:** `async with XCiteDBClient.test_session(...)` or provision with **`POST /api/v1/test/sessions`** and JSON **`{"overlay":true}`** when you need overlay, then pass `test_session_token` / `test_require_auth` to the constructor. **C++:** `XCiteDBClient::create_test_session(options)` with optional `test_session_overlay = true`, `destroy_test_session()`, optional `test_require_auth` in options.
113
+
114
+ ## JSON documents and metadata (merge, overwrite, efficiency)
115
+
116
+ - **JSON documents merge by default.** `POST /api/v1/json-documents` merges the posted `data` into the existing document (object fields combined per XCiteDB meta merge rules). Send **`overwrite: true`** to clear all stored JSON under that document root first, then write only the new payload. SDKs accept the same flag (e.g. `writeJsonDocument(id, data, { overwrite: true })` in JavaScript).
117
+ - **Metadata `mode` and arrays.** **`POST /api/v1/meta`** uses **`mode`**: default **`set`** writes or replaces at `path` (arrays are replaced in range; excess old indices cleared); **`append`** appends array elements after existing ones at `path`. Optional **`overwrite: true`** clears metadata under `path` before writing. JavaScript: **`appendMeta`** or **`addMeta(..., { mode: 'append' })`**; Python/C++: **`append_meta`** or **`add_meta`** with **`mode`** / **`overwrite`** (see SDK sections below).
118
+ - **Prefer dictionary-style objects.** Shredded JSON metadata is keyed by field names. **Object maps** get per-field storage; when an object accumulates enough distinct field names (server default threshold **32**), XCiteDB switches automatically to **dictionary storage** (`{*}` plus per-field keys) for efficient indexed access. For lookup-heavy or wide records, use **`{ "key": value, ... }`** shapes (or one document per logical row) rather than opaque arrays when you need keyed reads.
113
119
 
114
120
  ## JavaScript/TypeScript SDK (`@xcitedbs/client`)
115
121
 
@@ -177,8 +183,10 @@ const lock = await client.acquireLock('/manual/v1/intro');
177
183
  // ... edit ...
178
184
  await client.releaseLock('/manual/v1/intro', lock.lock_id);
179
185
 
180
- // Full-text search (requires Meilisearch or Elasticsearch backend)
181
- const results = await client.search({ query: 'installation guide', limit: 20 });
186
+ // Full-text search (embedded XciteFTS; optional temporal filters on the query body)
187
+ const results = await client.search({ query: 'installation guide', limit: 20, mode: 'fts' });
188
+ // Point-in-time keyword search: { query: '...', mode: 'fts', at_date: '2024-06-01' }
189
+ // Range overlap: { query: '...', mode: 'fts', date_from: '2024-01-01', date_to: '2025-01-01' }
182
190
 
183
191
  // Subscribe to real-time changes via WebSocket
184
192
  const sub = client.subscribe(
@@ -230,15 +238,15 @@ interface XCiteDBClientOptions {
230
238
  - `listIdentifierChildren(parentPath?)` — Navigate identifier hierarchy
231
239
 
232
240
  **JSON Documents:**
233
- - `writeJsonDocument(identifier, data)` — Store a JSON document
241
+ - `writeJsonDocument(identifier, data, opts?)` — Store or merge a JSON document (`opts?.overwrite` replaces entire document root)
234
242
  - `readJsonDocument(identifier)` — Read a JSON document (generic `readJsonDocument<T>()` supported)
235
243
  - `deleteJsonDocument(identifier)` — Delete a JSON document
236
244
  - `listJsonDocuments(match?, limit?, offset?)` — List JSON document keys
237
245
  - **Quick JSON aliases:** `put`, `get`, `remove`, `list` — same as the four methods above
238
246
 
239
247
  **Metadata (JSON on XML):**
240
- - `addMeta(identifier, value, path?)` — Set metadata on a document
241
- - `appendMeta(identifier, value, path?)` — Append to metadata
248
+ - `addMeta(identifier, value, path?, opts?)` — Set or append metadata (`opts?.mode`: `set`|`append`; `opts?.overwrite`)
249
+ - `appendMeta(identifier, value, path?, opts?)` — Append at `path` (same as `addMeta` with `mode: 'append'`)
242
250
  - `queryMeta(identifier, path?)` — Read metadata
243
251
  - `clearMeta(query)` — Remove metadata
244
252
 
@@ -263,7 +271,7 @@ interface XCiteDBClientOptions {
263
271
  - `findLocks(identifier)` — Query active locks
264
272
 
265
273
  **Search & Analytics:**
266
- - `search(query)` — Full-text search
274
+ - `search(query)` — Full-text search (embedded FTS). Optional **`at_date`**, **`date_from`**, **`date_to`** on the query for temporal keyword search (use **`mode: 'fts'`**). Hits may include **`valid_from`** / **`valid_to`** (7-char internal keys).
267
275
  - `reindex()` — Rebuild search index
268
276
  - `unquery(query, unqueryDoc)` — Execute Unquery DSL
269
277
 
@@ -338,6 +346,7 @@ async def main():
338
346
  await client.write_json_document("app.settings", {"theme": "dark"})
339
347
  print(await client.read_json_document("app.settings"))
340
348
  print(await client.search(TextSearchQuery(query="guide", limit=10)))
349
+ # Temporal FTS: TextSearchQuery(query="guide", mode="fts", at_date="2024-06-01")
341
350
  pair = await client.platform_login("admin@localhost", "password")
342
351
  # await client.login_app_user("user@example.com", "secret", tenant_id="...")
343
352
 
@@ -378,7 +387,7 @@ auto ids = client.query_documents(q);
378
387
  - Documents — XML document CRUD, identifiers, hierarchy
379
388
  - JSON documents — JSON document CRUD
380
389
  - Metadata — JSON metadata on documents
381
- - Search — Full-text search
390
+ - Search — Full-text (embedded FTS; optional temporal `at_date` / `date_from` / `date_to`; hit `valid_from` / `valid_to`)
382
391
  - Unquery — Declarative query DSL
383
392
  - Workspaces — Isolated editing environments
384
393
  - Checkpoints & bookmarks — Named snapshots and references
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",