@xcitedbs/client 0.2.0 → 0.2.6

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
@@ -26,11 +26,12 @@ import { XCiteDBClient } from '@xcitedbs/client';
26
26
  const client = new XCiteDBClient({
27
27
  baseUrl: 'http://localhost:8080',
28
28
  apiKey: process.env.XCITEDB_API_KEY,
29
- context: { branch: 'main', date: '03/27/2026' },
29
+ context: { branch: '', date: '' },
30
30
  });
31
31
 
32
32
  await client.health();
33
33
  const docs = await client.queryByIdentifier('/test1', 'FirstMatch');
34
+ await client.put('app.settings', { theme: 'dark' });
34
35
  ```
35
36
 
36
37
  ### WebSocket
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, PolicySubjectInput, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, OAuthProvidersResponse, OwnedTenantInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XCiteDBClientOptions, 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, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, XCiteQuery } from './types';
2
2
  import { WebSocketSubscription } from './websocket';
3
3
  export declare class XCiteDBClient {
4
4
  private baseUrl;
@@ -14,7 +14,18 @@ export declare class XCiteDBClient {
14
14
  private onSessionTokensUpdated?;
15
15
  private onAppUserTokensUpdated?;
16
16
  private onSessionInvalid?;
17
+ private testSessionToken?;
18
+ private testRequireAuth?;
17
19
  constructor(options: XCiteDBClientOptions);
20
+ /**
21
+ * Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
22
+ * then returns a client that sends `X-Test-Session` (auth-free by default).
23
+ */
24
+ static createTestSession(opts: CreateTestSessionOptions): Promise<XCiteDBClient>;
25
+ /** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
26
+ destroyTestSession(): Promise<{
27
+ message: string;
28
+ }>;
18
29
  /** True if this client would send API key or Bearer credentials on a normal request. */
19
30
  private sentAuthCredentials;
20
31
  /** 401 on these paths is an expected auth flow outcome, not a dead session. */
@@ -34,6 +45,7 @@ export declare class XCiteDBClient {
34
45
  /** Include `tenant_id` for public app-auth routes when using only app-user tokens (no developer key/JWT). */
35
46
  private mergeAppTenant;
36
47
  private authHeaders;
48
+ private testHeaders;
37
49
  private request;
38
50
  /** Developer Bearer refresh first, then app-user refresh (no API key refresh). */
39
51
  private tryRefreshSessionAfter401;
@@ -48,8 +60,8 @@ export declare class XCiteDBClient {
48
60
  api_version: string;
49
61
  }>;
50
62
  /**
51
- * Platform console sign-in. The first argument is the account **email** (e.g. `admin@localhost`).
52
- * Legacy `/api/v1/auth/login` has been removed.
63
+ * @deprecated Use {@link platformLogin} for platform operator sign-in, or {@link loginAppUser} for app end-users.
64
+ * This method only performs platform console login.
53
65
  */
54
66
  login(email: string, password: string): Promise<TokenPair>;
55
67
  /** Platform console sign-in (`email` + `password`). Project context is `X-Project-Id`, not the JWT. */
@@ -67,14 +79,14 @@ export declare class XCiteDBClient {
67
79
  org_name?: string;
68
80
  org_id?: string;
69
81
  attributes?: Record<string, unknown>;
70
- }): Promise<unknown>;
82
+ }): Promise<PlatformRegisterResult>;
71
83
  platformWorkspaces(): Promise<PlatformWorkspacesResponse>;
72
- listMyTenants(): Promise<OwnedTenantInfo[]>;
73
- /** Alias for {@link listMyTenants} (organization/project terminology). */
74
- listMyProjects(): Promise<OwnedTenantInfo[]>;
84
+ /** @deprecated Prefer {@link listMyProjects}. */
85
+ listMyTenants(): Promise<ProjectInfo[]>;
86
+ /** Lists projects the platform user can access (from workspaces). */
87
+ listMyProjects(): Promise<ProjectInfo[]>;
75
88
  /**
76
- * Switch active tenant/project for API calls. Platform console: updates `X-Project-Id` only (no token exchange).
77
- * Legacy `/api/v1/auth/switch-tenant` has been removed; non-platform callers should set context instead.
89
+ * @deprecated Use {@link switchProject}. Platform console: updates `X-Project-Id` only (no token exchange).
78
90
  */
79
91
  switchTenant(tenantId: string): Promise<void>;
80
92
  /** Alias for {@link switchTenant}. */
@@ -83,7 +95,7 @@ export declare class XCiteDBClient {
83
95
  createApiKey(name: string, expiresAt?: number, keyType?: 'secret' | 'public'): Promise<unknown>;
84
96
  changePassword(currentPassword: string, newPassword: string): Promise<void>;
85
97
  revokeApiKey(keyId: string): Promise<void>;
86
- registerAppUser(email: string, password: string, displayName?: string, groups?: string[], attributes?: Record<string, unknown>): Promise<AppUser>;
98
+ registerAppUser(email: string, password: string, displayName?: string, attributes?: Record<string, unknown>): Promise<AppUser>;
87
99
  getOAuthProviders(): Promise<OAuthProvidersResponse>;
88
100
  /** Relative path + query for browser navigation to start OAuth (append to API base URL). */
89
101
  oauthAuthorizePath(provider: string): string;
@@ -165,8 +177,33 @@ export declare class XCiteDBClient {
165
177
  message?: string;
166
178
  autoResolve?: 'none' | 'source' | 'target';
167
179
  }): Promise<MergeResult>;
168
- /** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeDocumentJson`. */
180
+ /**
181
+ * Create `branchName` from {@link options.fromBranch} (or current context branch), run `fn` scoped to that branch,
182
+ * create a commit, then merge back into the parent branch unless {@link options.autoMerge} is `false`.
183
+ * Restores previous {@link DatabaseContext} afterward.
184
+ */
185
+ withBranch<T>(branchName: string, fn: (client: XCiteDBClient) => Promise<T>, options?: {
186
+ message?: string;
187
+ /** When true (default), merge `branchName` into the parent branch after commit. */
188
+ autoMerge?: boolean;
189
+ /** Branch to fork from (default: current `context.branch`, or `""`). */
190
+ fromBranch?: string;
191
+ author?: string;
192
+ }): Promise<{
193
+ result: T;
194
+ commit?: CommitRecord;
195
+ merge?: MergeResult;
196
+ }>;
197
+ /** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
169
198
  writeXML(xml: string, _options?: WriteDocumentOptions): Promise<void>;
199
+ /**
200
+ * Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
201
+ * For storing JSON data by key, use `writeJsonDocument`.
202
+ */
203
+ writeXmlDocument(xml: string, options?: WriteDocumentOptions): Promise<void>;
204
+ /**
205
+ * @deprecated Use {@link writeXmlDocument}. This name was misleading: it writes **XML** via a JSON wrapper, not a JSON document.
206
+ */
170
207
  writeDocumentJson(xml: string, options?: WriteDocumentOptions): Promise<void>;
171
208
  queryByIdentifier(identifier: string, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
172
209
  queryDocuments(query: XCiteQuery, flags?: Flags, filter?: string, pathFilter?: string): Promise<string[]>;
@@ -187,22 +224,30 @@ export declare class XCiteDBClient {
187
224
  }): Promise<boolean>;
188
225
  appendMeta(identifier: string, value: unknown, path?: string): Promise<boolean>;
189
226
  appendMetaByQuery(query: XCiteQuery, value: unknown, path?: string, firstMatch?: boolean): Promise<boolean>;
190
- queryMeta(identifier: string, path?: string): Promise<unknown>;
191
- queryMetaByQuery(query: XCiteQuery, path?: string): Promise<unknown>;
227
+ queryMeta<T = MetaValue>(identifier: string, path?: string): Promise<T>;
228
+ queryMetaByQuery<T = MetaValue>(query: XCiteQuery, path?: string): Promise<T>;
192
229
  clearMeta(query: XCiteQuery): Promise<boolean>;
193
230
  acquireLock(identifier: string, expires?: number): Promise<LockInfo>;
194
231
  releaseLock(identifier: string, lockId: string): Promise<boolean>;
195
232
  findLocks(identifier: string): Promise<LockInfo[]>;
196
- unquery(query: XCiteQuery, unquery: unknown): Promise<unknown>;
233
+ unquery<T = UnqueryResult>(query: XCiteQuery, unquery: unknown): Promise<T>;
197
234
  search(q: TextSearchQuery): Promise<TextSearchResult>;
198
235
  reindex(): Promise<{
199
236
  status: string;
200
237
  message: string;
201
238
  }>;
202
239
  writeJsonDocument(identifier: string, data: unknown): Promise<void>;
203
- readJsonDocument(identifier: string): Promise<unknown>;
240
+ readJsonDocument<T = unknown>(identifier: string): Promise<T>;
204
241
  deleteJsonDocument(identifier: string): Promise<void>;
205
242
  listJsonDocuments(match?: string, limit?: number, offset?: number): Promise<ListIdentifiersResult>;
243
+ /** JSON document shorthand — same as {@link writeJsonDocument}. */
244
+ put(identifier: string, data: unknown): Promise<void>;
245
+ /** JSON document read — same as {@link readJsonDocument}. */
246
+ get<T = unknown>(identifier: string): Promise<T>;
247
+ /** JSON document delete — same as {@link deleteJsonDocument}. */
248
+ remove(identifier: string): Promise<void>;
249
+ /** List JSON document keys — same as {@link listJsonDocuments}. */
250
+ list(match?: string, limit?: number, offset?: number): Promise<ListIdentifiersResult>;
206
251
  /**
207
252
  * WebSocket `/api/v1/ws` — optional initial subscription pattern.
208
253
  * Uses `access_token` or `api_key` query params when headers are not available (browser).
package/dist/client.js CHANGED
@@ -31,6 +31,50 @@ class XCiteDBClient {
31
31
  this.onSessionTokensUpdated = options.onSessionTokensUpdated;
32
32
  this.onAppUserTokensUpdated = options.onAppUserTokensUpdated;
33
33
  this.onSessionInvalid = options.onSessionInvalid;
34
+ this.testSessionToken = options.testSessionToken;
35
+ this.testRequireAuth = options.testRequireAuth === true;
36
+ }
37
+ /**
38
+ * Create an ephemeral isolated database: calls `POST /api/v1/test/sessions` with your API key or Bearer,
39
+ * then returns a client that sends `X-Test-Session` (auth-free by default).
40
+ */
41
+ static async createTestSession(opts) {
42
+ const temp = new XCiteDBClient({
43
+ baseUrl: opts.baseUrl,
44
+ apiKey: opts.apiKey,
45
+ accessToken: opts.accessToken,
46
+ appUserAccessToken: opts.appUserAccessToken,
47
+ appUserRefreshToken: opts.appUserRefreshToken,
48
+ context: opts.context,
49
+ platformConsole: opts.platformConsole,
50
+ projectId: opts.projectId,
51
+ onSessionTokensUpdated: opts.onSessionTokensUpdated,
52
+ onAppUserTokensUpdated: opts.onAppUserTokensUpdated,
53
+ onSessionInvalid: opts.onSessionInvalid,
54
+ });
55
+ const data = await temp.request('POST', '/api/v1/test/sessions', undefined, undefined, { no401Retry: true });
56
+ return new XCiteDBClient({
57
+ baseUrl: opts.baseUrl,
58
+ apiKey: opts.testRequireAuth ? opts.apiKey : undefined,
59
+ accessToken: opts.testRequireAuth ? opts.accessToken : undefined,
60
+ appUserAccessToken: opts.testRequireAuth ? opts.appUserAccessToken : undefined,
61
+ appUserRefreshToken: opts.testRequireAuth ? opts.appUserRefreshToken : undefined,
62
+ context: opts.context,
63
+ platformConsole: opts.platformConsole,
64
+ projectId: opts.projectId,
65
+ onSessionTokensUpdated: opts.onSessionTokensUpdated,
66
+ onAppUserTokensUpdated: opts.onAppUserTokensUpdated,
67
+ onSessionInvalid: opts.onSessionInvalid,
68
+ testSessionToken: data.session_token,
69
+ testRequireAuth: opts.testRequireAuth,
70
+ });
71
+ }
72
+ /** Destroy this test session on the server (`DELETE /api/v1/test/sessions/current`). */
73
+ async destroyTestSession() {
74
+ if (!this.testSessionToken) {
75
+ throw new Error('destroyTestSession: client has no testSessionToken');
76
+ }
77
+ return this.request('DELETE', '/api/v1/test/sessions/current', undefined, undefined, { no401Retry: true });
34
78
  }
35
79
  /** True if this client would send API key or Bearer credentials on a normal request. */
36
80
  sentAuthCredentials() {
@@ -101,11 +145,13 @@ class XCiteDBClient {
101
145
  h['X-Date'] = c.date;
102
146
  if (c.prefix)
103
147
  h['X-Prefix'] = c.prefix;
148
+ if (c.unversioned)
149
+ h['X-Unversioned'] = 'true';
104
150
  return h;
105
151
  }
106
152
  /** Include `tenant_id` for public app-auth routes when using only app-user tokens (no developer key/JWT). */
107
153
  mergeAppTenant(body) {
108
- const tid = this.defaultContext.tenant_id;
154
+ const tid = this.defaultContext.project_id ?? this.defaultContext.tenant_id;
109
155
  if (tid)
110
156
  return { ...body, tenant_id: tid };
111
157
  return body;
@@ -130,6 +176,16 @@ class XCiteDBClient {
130
176
  }
131
177
  return h;
132
178
  }
179
+ testHeaders() {
180
+ const h = {};
181
+ if (this.testSessionToken) {
182
+ h['X-Test-Session'] = this.testSessionToken;
183
+ if (this.testRequireAuth) {
184
+ h['X-Test-Auth'] = 'required';
185
+ }
186
+ }
187
+ return h;
188
+ }
133
189
  async request(method, path, body, extraHeaders, opts) {
134
190
  const no401Retry = opts?.no401Retry === true;
135
191
  for (let attempt = 0; attempt < 2; attempt++) {
@@ -137,6 +193,7 @@ class XCiteDBClient {
137
193
  const headers = {
138
194
  ...this.authHeaders(),
139
195
  ...this.contextHeaders(),
196
+ ...this.testHeaders(),
140
197
  ...extraHeaders,
141
198
  };
142
199
  let init = { method, headers };
@@ -214,8 +271,8 @@ class XCiteDBClient {
214
271
  return this.request('GET', '/api/v1/version');
215
272
  }
216
273
  /**
217
- * Platform console sign-in. The first argument is the account **email** (e.g. `admin@localhost`).
218
- * Legacy `/api/v1/auth/login` has been removed.
274
+ * @deprecated Use {@link platformLogin} for platform operator sign-in, or {@link loginAppUser} for app end-users.
275
+ * This method only performs platform console login.
219
276
  */
220
277
  async login(email, password) {
221
278
  return this.platformLogin(email, password);
@@ -257,12 +314,19 @@ class XCiteDBClient {
257
314
  async platformWorkspaces() {
258
315
  return this.request('GET', '/api/v1/platform/auth/workspaces');
259
316
  }
317
+ /** @deprecated Prefer {@link listMyProjects}. */
260
318
  async listMyTenants() {
319
+ return this.listMyProjects();
320
+ }
321
+ /** Lists projects the platform user can access (from workspaces). */
322
+ async listMyProjects() {
261
323
  const w = await this.platformWorkspaces();
262
324
  const tenants = (w.projects ?? []).map((p) => {
263
325
  const r = p;
326
+ const id = r.tenant_id || r.project_id || '';
264
327
  return {
265
- tenant_id: r.tenant_id || r.project_id || '',
328
+ project_id: id,
329
+ tenant_id: id,
266
330
  org_id: r.org_id,
267
331
  name: r.name,
268
332
  status: r.status,
@@ -277,13 +341,8 @@ class XCiteDBClient {
277
341
  }
278
342
  return tenants;
279
343
  }
280
- /** Alias for {@link listMyTenants} (organization/project terminology). */
281
- async listMyProjects() {
282
- return this.listMyTenants();
283
- }
284
344
  /**
285
- * Switch active tenant/project for API calls. Platform console: updates `X-Project-Id` only (no token exchange).
286
- * Legacy `/api/v1/auth/switch-tenant` has been removed; non-platform callers should set context instead.
345
+ * @deprecated Use {@link switchProject}. Platform console: updates `X-Project-Id` only (no token exchange).
287
346
  */
288
347
  async switchTenant(tenantId) {
289
348
  if (this.platformConsole) {
@@ -316,26 +375,23 @@ class XCiteDBClient {
316
375
  await this.request('DELETE', `/api/v1/project/keys/${encodeURIComponent(keyId)}`);
317
376
  }
318
377
  // --- App user auth (requires developer API key or JWT on the same tenant) ---
319
- async registerAppUser(email, password, displayName, groups, attributes) {
378
+ async registerAppUser(email, password, displayName, attributes) {
320
379
  const body = { email, password };
321
380
  if (displayName !== undefined)
322
381
  body.display_name = displayName;
323
- if (groups !== undefined)
324
- body.groups = groups;
325
382
  if (attributes !== undefined)
326
383
  body.attributes = attributes;
327
384
  return this.request('POST', '/api/v1/app/auth/register', this.mergeAppTenant(body));
328
385
  }
329
386
  async getOAuthProviders() {
330
- const tid = this.defaultContext.tenant_id;
387
+ const tid = this.defaultContext.project_id ?? this.defaultContext.tenant_id;
331
388
  const q = buildQuery({ tenant_id: tid && String(tid).length > 0 ? String(tid) : 'default' });
332
389
  return this.request('GET', `/api/v1/app/auth/oauth/providers${q}`);
333
390
  }
334
391
  /** Relative path + query for browser navigation to start OAuth (append to API base URL). */
335
392
  oauthAuthorizePath(provider) {
336
- const tid = this.defaultContext.tenant_id && String(this.defaultContext.tenant_id).length > 0
337
- ? String(this.defaultContext.tenant_id)
338
- : 'default';
393
+ const raw = this.defaultContext.project_id ?? this.defaultContext.tenant_id;
394
+ const tid = raw && String(raw).length > 0 ? String(raw) : 'default';
339
395
  return `/api/v1/app/auth/oauth/${encodeURIComponent(provider)}/authorize${buildQuery({ tenant_id: tid })}`;
340
396
  }
341
397
  /** Exchange one-time session code from OAuth browser redirect (public + tenant_id). */
@@ -591,19 +647,55 @@ class XCiteDBClient {
591
647
  body.auto_resolve = options?.autoResolve ?? 'none';
592
648
  return this.request('POST', `/api/v1/branches/${encodeURIComponent(targetBranch)}/merge`, body);
593
649
  }
594
- /** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeDocumentJson`. */
650
+ /**
651
+ * Create `branchName` from {@link options.fromBranch} (or current context branch), run `fn` scoped to that branch,
652
+ * create a commit, then merge back into the parent branch unless {@link options.autoMerge} is `false`.
653
+ * Restores previous {@link DatabaseContext} afterward.
654
+ */
655
+ async withBranch(branchName, fn, options) {
656
+ const prev = { ...this.defaultContext };
657
+ const fromBranch = options?.fromBranch ?? prev.branch ?? '';
658
+ let commit;
659
+ let merge;
660
+ try {
661
+ await this.createBranch(branchName, fromBranch || undefined, prev.date || undefined);
662
+ this.setContext({ branch: branchName });
663
+ const result = await fn(this);
664
+ commit = await this.createCommit(options?.message ?? `Branch ${branchName}`, options?.author);
665
+ if (options?.autoMerge !== false) {
666
+ merge = await this.mergeBranch(fromBranch, branchName, {
667
+ message: options?.message,
668
+ });
669
+ }
670
+ return { result, commit, merge };
671
+ }
672
+ finally {
673
+ this.setContext(prev);
674
+ }
675
+ }
676
+ /** Send raw XML body (`Content-Type: application/xml`). For JSON wrapper + options use `writeXmlDocument`. */
595
677
  async writeXML(xml, _options) {
596
678
  await this.request('POST', '/api/v1/documents', xml, {
597
679
  'Content-Type': 'application/xml',
598
680
  });
599
681
  }
600
- async writeDocumentJson(xml, options) {
682
+ /**
683
+ * Write an **XML** document using a JSON request body (`xml` field). The identifier is taken from `db:identifier` on the root element.
684
+ * For storing JSON data by key, use `writeJsonDocument`.
685
+ */
686
+ async writeXmlDocument(xml, options) {
601
687
  await this.request('POST', '/api/v1/documents', {
602
688
  xml,
603
689
  is_top: options?.is_top ?? true,
604
690
  compare_attributes: options?.compare_attributes ?? false,
605
691
  });
606
692
  }
693
+ /**
694
+ * @deprecated Use {@link writeXmlDocument}. This name was misleading: it writes **XML** via a JSON wrapper, not a JSON document.
695
+ */
696
+ async writeDocumentJson(xml, options) {
697
+ return this.writeXmlDocument(xml, options);
698
+ }
607
699
  async queryByIdentifier(identifier, flags, filter, pathFilter) {
608
700
  const q = buildQuery({
609
701
  identifier,
@@ -865,6 +957,22 @@ class XCiteDBClient {
865
957
  }
866
958
  return { identifiers: [], total: 0, offset: 0, limit: 0 };
867
959
  }
960
+ /** JSON document shorthand — same as {@link writeJsonDocument}. */
961
+ async put(identifier, data) {
962
+ return this.writeJsonDocument(identifier, data);
963
+ }
964
+ /** JSON document read — same as {@link readJsonDocument}. */
965
+ async get(identifier) {
966
+ return this.readJsonDocument(identifier);
967
+ }
968
+ /** JSON document delete — same as {@link deleteJsonDocument}. */
969
+ async remove(identifier) {
970
+ return this.deleteJsonDocument(identifier);
971
+ }
972
+ /** List JSON document keys — same as {@link listJsonDocuments}. */
973
+ async list(match, limit, offset) {
974
+ return this.listJsonDocuments(match, limit, offset);
975
+ }
868
976
  /**
869
977
  * WebSocket `/api/v1/ws` — optional initial subscription pattern.
870
978
  * Uses `access_token` or `api_key` query params when headers are not available (browser).
@@ -882,6 +990,9 @@ class XCiteDBClient {
882
990
  ...this.authHeaders(),
883
991
  ...this.contextHeaders(),
884
992
  };
993
+ const tid = this.defaultContext.project_id ?? this.defaultContext.tenant_id;
994
+ if (tid)
995
+ headers['tenant_id'] = tid;
885
996
  const sub = new websocket_1.WebSocketSubscription(wsBase, headers);
886
997
  sub.onMessage(callback);
887
998
  if (onError)
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, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, LogEntry, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, WriteDocumentOptions, XCiteDBClientOptions, 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, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, UnqueryResult, XCiteQuery, } from './types';
4
4
  export { XCiteDBError } from './types';
package/dist/types.d.ts CHANGED
@@ -14,6 +14,19 @@ export interface XCiteQuery {
14
14
  /** Skip this many rows (for pagination). */
15
15
  offset?: number;
16
16
  }
17
+ /** Root object returned by `readJsonDocument` (use generics for app-specific shapes). */
18
+ export type JsonDocumentData = Record<string, unknown>;
19
+ /** JSON metadata from meta APIs (structure is document-specific). */
20
+ export type MetaValue = unknown;
21
+ /** Result of `unquery` (shape depends on the unquery definition). */
22
+ export type UnqueryResult = unknown;
23
+ /** Typical `POST /api/v1/platform/auth/register` response (fields vary by registration policy). */
24
+ export interface PlatformRegisterResult {
25
+ user_id?: string;
26
+ status?: string;
27
+ message?: string;
28
+ [key: string]: unknown;
29
+ }
17
30
  /** Full-text search (`POST /api/v1/search`) */
18
31
  export interface TextSearchQuery {
19
32
  query: string;
@@ -77,8 +90,14 @@ export interface TokenPair {
77
90
  refresh_token: string;
78
91
  expires_in: number;
79
92
  }
80
- /** Projects from `GET /api/v1/platform/auth/workspaces` (mapped for compatibility). */
81
- export interface OwnedTenantInfo {
93
+ /**
94
+ * Project row from `GET /api/v1/platform/auth/workspaces` and related APIs.
95
+ * The server often labels the id as `tenant_id`; both fields are set when mapped by the SDK.
96
+ */
97
+ export interface ProjectInfo {
98
+ /** Project id (wire alias: `tenant_id` in many JSON bodies). */
99
+ project_id: string;
100
+ /** Same id as `project_id` (legacy / wire field name). */
82
101
  tenant_id: string;
83
102
  org_id?: string;
84
103
  name: string;
@@ -88,6 +107,8 @@ export interface OwnedTenantInfo {
88
107
  config?: unknown;
89
108
  owner_user_id?: string;
90
109
  }
110
+ /** @deprecated Use {@link ProjectInfo}. */
111
+ export type OwnedTenantInfo = ProjectInfo;
91
112
  export interface UserInfo {
92
113
  user_id: string;
93
114
  username: string;
@@ -117,7 +138,7 @@ export interface PlatformWorkspaceOrg {
117
138
  }
118
139
  export interface PlatformWorkspacesResponse {
119
140
  orgs: PlatformWorkspaceOrg[];
120
- projects: OwnedTenantInfo[];
141
+ projects: ProjectInfo[];
121
142
  }
122
143
  /** One row from `GET /api/v1/project/keys` (secret is never returned). */
123
144
  export interface ApiKeyInfo {
@@ -127,13 +148,24 @@ export interface ApiKeyInfo {
127
148
  expires_at: number;
128
149
  /** `secret` = server-side full access; `public` = client-safe, restricted. */
129
150
  key_type?: 'secret' | 'public';
151
+ /** Project id this key belongs to. */
152
+ tenant_id?: string;
130
153
  }
131
154
  export type Flags = 'None' | 'FirstMatch' | 'IncludeChildren' | 'NoChildren' | 'KeepIndexNodes' | 'PrevIfDeleted' | string;
132
155
  export interface DatabaseContext {
133
156
  branch?: string;
134
157
  date?: string;
135
158
  prefix?: string;
136
- /** Project/tenant id; sent as `tenant_id` in app-user public auth bodies when no developer token is used. */
159
+ /**
160
+ * When true, sends `X-Unversioned: true` so writes use flat LMDB keys (no date revision).
161
+ * Do not combine with `date`; the server returns 400 if both are set.
162
+ */
163
+ unversioned?: boolean;
164
+ /** Preferred: project id; sent as `tenant_id` in app-user public auth bodies when no developer token is used. */
165
+ project_id?: string;
166
+ /**
167
+ * @deprecated Use {@link DatabaseContext.project_id} (same JSON field name on the wire: `tenant_id`).
168
+ */
137
169
  tenant_id?: string;
138
170
  }
139
171
  export interface WriteDocumentOptions {
@@ -162,7 +194,7 @@ export interface XCiteDBClientOptions {
162
194
  context?: DatabaseContext;
163
195
  /** When true, sends `X-Project-Id` for console requests; developer JWTs are always platform tokens. */
164
196
  platformConsole?: boolean;
165
- /** Active project id for platform console requests (`X-Project-Id` header). */
197
+ /** Active project id for platform console requests (`X-Project-Id` header). Alias of {@link DatabaseContext.project_id} for console mode. */
166
198
  projectId?: string;
167
199
  /** Persist developer session tokens after refresh (e.g. localStorage). */
168
200
  onSessionTokensUpdated?: (pair: TokenPair) => void;
@@ -173,6 +205,29 @@ export interface XCiteDBClientOptions {
173
205
  * Use to clear stored credentials and redirect to sign-in. Not invoked for login/register-style paths.
174
206
  */
175
207
  onSessionInvalid?: () => void;
208
+ /**
209
+ * Ephemeral BaaS test session token from `POST /api/v1/test/sessions`.
210
+ * Sends `X-Test-Session`; use with {@link XCiteDBClient.createTestSession}.
211
+ */
212
+ testSessionToken?: string;
213
+ /** When true with `testSessionToken`, sends `X-Test-Auth: required` so real credentials are validated. */
214
+ testRequireAuth?: boolean;
215
+ }
216
+ /** Options for {@link XCiteDBClient.createTestSession} (provisions via API key or Bearer). */
217
+ export interface CreateTestSessionOptions {
218
+ baseUrl: string;
219
+ apiKey?: string;
220
+ accessToken?: string;
221
+ appUserAccessToken?: string;
222
+ appUserRefreshToken?: string;
223
+ context?: DatabaseContext;
224
+ platformConsole?: boolean;
225
+ projectId?: string;
226
+ /** Keep `apiKey` / `accessToken` on the client and send `X-Test-Auth: required` on each request. */
227
+ testRequireAuth?: boolean;
228
+ onSessionTokensUpdated?: (pair: TokenPair) => void;
229
+ onAppUserTokensUpdated?: (pair: AppUserTokenPair) => void;
230
+ onSessionInvalid?: () => void;
176
231
  }
177
232
  /** Application user (tenant-scoped), distinct from developer users. */
178
233
  export interface AppUser {
package/dist/websocket.js CHANGED
@@ -19,6 +19,11 @@ class WebSocketSubscription {
19
19
  const token = this.headerParams['Authorization']?.replace(/^Bearer\s+/i, '');
20
20
  if (token)
21
21
  u.searchParams.set('access_token', token);
22
+ if (!u.searchParams.has('access_token')) {
23
+ const appUser = this.headerParams['X-App-User-Token'];
24
+ if (appUser)
25
+ u.searchParams.set('access_token', appUser);
26
+ }
22
27
  const apiKey = this.headerParams['X-API-Key'];
23
28
  if (apiKey)
24
29
  u.searchParams.set('api_key', apiKey);
package/llms-full.txt CHANGED
@@ -14,7 +14,7 @@ Before reading the full reference, note these critical differences from typical
14
14
 
15
15
  3. **Documents are XML or JSON, not arbitrary blobs.** XML documents are the primary document type. JSON documents are a parallel store. Both are fully versioned.
16
16
 
17
- 4. **Context (branch + date) travels as HTTP headers** (`X-Branch`, `X-Date`), not URL path segments.
17
+ 4. **Context (branch + date) travels as HTTP headers** (`X-Branch`, `X-Date`, and optionally `X-Unversioned` for explicit flat writes), not URL path segments.
18
18
 
19
19
  5. **XML documents carry their identifier inside the XML** via a `db:identifier` attribute on the root element.
20
20
 
@@ -22,6 +22,32 @@ Before reading the full reference, note these critical differences from typical
22
22
 
23
23
  7. **The query model is NOT SQL.** Use REST query parameters (`match`, `match_start`, `contains`, `regex`) or the **Unquery** JSON DSL.
24
24
 
25
+ 8. **Project vs tenant id.** Prefer SDK `context.project_id` and `listMyProjects` / `switchProject`. JSON bodies often use the field name `tenant_id` for the same value.
26
+
27
+ 9. **OpenAPI:** See repository `docs/openapi.yaml` for a machine-readable route map.
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, normal JWT/API-key checks are bypassed for those requests (synthetic admin for wet tests). Management routes under **`/api/v1/test/*`** must not include `X-Test-Session`. The test store starts empty (no cloned production project config).
30
+
31
+ ## Common Pitfalls
32
+
33
+ 1. **`baseUrl` must be origin-only (no `/api` or `/api/v1` path).** The SDK prepends `/api/v1/…` to every request. If `baseUrl` is `https://host/api/v1`, requests hit `/api/v1/api/v1/…` which typically returns **405 Not Allowed** from the reverse proxy. Use only the scheme + host + optional port: `https://host` or `http://localhost:8080`.
34
+
35
+ 2. **Browser WebSocket requires correct CORS and proxy config.** Browsers add an `Origin` header to WebSocket connections — make sure your project's CORS allowed-origins includes your app's origin. Reverse proxies (nginx) must forward WebSocket `Upgrade` requests to XCiteDB's `/api/v1/ws` endpoint (the default `docker/nginx.conf` already does this). For production deployments that need fine-grained server-side filtering or short-lived auth tickets, consider proxying the WebSocket through your own backend:
36
+
37
+ ```
38
+ Browser ──ws──▶ Your API server ──ws──▶ XCiteDB /api/v1/ws
39
+ (same origin, (server-to-server,
40
+ short ticket) secret API key + user JWT)
41
+ ```
42
+
43
+ 3. **Prefer `context.project_id` over `platformConsole` for project-scoped API keys.** `platformConsole` / `projectId` is designed for platform-operator keys that select a project dynamically via the `X-Project-Id` header. For project-scoped keys (which already identify their project), set **`context.project_id`** instead — it adds `tenant_id` to app-auth JSON bodies via `mergeAppTenant()` without sending `X-Project-Id`.
44
+
45
+ 4. **Prefer `https://` in `baseUrl` for remote/production hosts.** XciteDB's default nginx uses a method-preserving **308** redirect (POST stays POST), but third-party proxies or older configurations may use **301** which silently downgrades POST to GET. Using `https://` directly avoids the redirect entirely.
46
+
47
+ 5. **`context.project_id` (or `tenant_id`) is required for app-user self-registration.** `registerAppUser()` uses `mergeAppTenant(body)` to add `tenant_id` to the JSON body only when `context.project_id` or `context.tenant_id` is set. If both are omitted, the server cannot determine which project to register the user in. Always set `project_id` in the constructor `context` when calling `registerAppUser`, `loginAppUser`, and other public app-auth methods.
48
+
49
+ 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()`.
50
+
25
51
  ---
26
52
 
27
53
  # Part 1: Product Overview
@@ -72,7 +98,7 @@ XciteDB shines in domains requiring strict auditability, collaborative authoring
72
98
  - **Response:** Data is returned as structured JSON or XML.
73
99
 
74
100
  ### 2.2 Storage & Document Model
75
- - **LMDB Backed:** Memory-mapped key-value store organized into specialized sub-databases (`nodes`, `ids`, `meta`, `json_docs`).
101
+ - **LMDB Backed:** Memory-mapped key-value store organized into specialized sub-databases (`nodes`, `ids`, `meta`, `vcs_data`).
76
102
  - **XML Documents:** Stored natively via pugixml and shredded into elements, attributes, and text nodes. Addressed by hierarchical, path-like identifiers (e.g., `/manual/v1/chapter1`).
77
103
  - **JSON Documents:** Stored as standalone structured documents keyed by string. Shredded into objects, fields, and array elements.
78
104
  - **JSON Metadata on XML:** JSON metadata can be attached to any XML document or path.
@@ -81,7 +107,7 @@ XciteDB shines in domains requiring strict auditability, collaborative authoring
81
107
  ### 2.3 Git-Like Document Versioning
82
108
  - **Branches & Commits:** Isolate collaborative edits into branches. Create atomic commits with descriptive messages.
83
109
  - **Merge & Cherry-Pick:** Merge branches or cherry-pick specific commits.
84
- - **Time Travel:** Read historical state at any point in time using the `X-Date` header.
110
+ - **Time Travel:** Read historical state at any point in time using the `X-Date` header. For **writes**, `X-Unversioned: true` explicitly requests flat (unversioned) storage; it conflicts with `X-Date` (**400**).
85
111
  - **Cooperative Locking:** TTL locks prevent conflicting writes (HTTP 409 on conflict).
86
112
 
87
113
  ### 2.4 Unquery: Declarative Query DSL
@@ -116,6 +142,7 @@ Welcome to the **XciteDB HTTP API**. This reference describes REST endpoints und
116
142
  | `X-Project-Id` | Platform console / multi-project JWT | Selects the **tenant project** when the token is not already bound to one tenant |
117
143
  | `X-Branch` | Optional | Active branch name for document and versioning operations |
118
144
  | `X-Date` | Optional | Point-in-time / revision context (ISO-like string as used by your deployment) |
145
+ | `X-Unversioned` | Optional | When `true` or `1`, **writes** use flat LMDB keys (no date revision). Must not be combined with `X-Date` (**400**). Omitting `X-Date` remains valid (implicit unversioned). |
119
146
 
120
147
  ## Errors
121
148
 
@@ -157,6 +184,26 @@ Browser and mobile apps can use OAuth2-style flows against `/api/v1/app/auth/oau
157
184
 
158
185
  ---
159
186
 
187
+ # Ephemeral test sessions
188
+
189
+ For **integration and wet tests** against a shared BaaS host without touching production data:
190
+
191
+ | Step | What to do |
192
+ |------|------------|
193
+ | **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). |
194
+ | **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. |
195
+ | **Auth** | **Default:** test requests **skip** normal auth (convenient for automated tests). **`X-Test-Auth: required`:** require normal Bearer or API key; identity and ABAC apply, but data still comes from the test session DB. |
196
+ | **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. |
197
+ | **CORS** | Browsers may need **`X-Test-Session`** and **`X-Test-Auth`** in the deployment’s allowed CORS headers (defaults include them). |
198
+
199
+ **SDK usage (summary):**
200
+
201
+ - **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`.
202
+ - **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.
203
+ - **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key` (and optional `test_require_auth`); `destroy_test_session()`.
204
+
205
+ ---
206
+
160
207
  # Health, version & discovery
161
208
 
162
209
  ## Health
@@ -876,7 +923,7 @@ const client = new XCiteDBClient({
876
923
  await client.health();
877
924
 
878
925
  // Write XML document
879
- await client.writeDocumentJson(
926
+ await client.writeXmlDocument(
880
927
  '<chapter db:identifier="/manual/v1/intro"><title>Introduction</title></chapter>'
881
928
  );
882
929
 
@@ -918,7 +965,9 @@ interface DatabaseContext {
918
965
  branch?: string; // '' = default (root timeline)
919
966
  date?: string; // Point-in-time
920
967
  prefix?: string; // Identifier prefix filter
921
- tenant_id?: string; // For app-user auth
968
+ unversioned?: boolean; // Sends X-Unversioned: true (flat writes; do not combine with date)
969
+ project_id?: string; // Preferred: project id for app-user public auth (`tenant_id` on wire)
970
+ tenant_id?: string; // Deprecated alias of project_id
922
971
  }
923
972
  ```
924
973
 
@@ -930,15 +979,17 @@ interface DatabaseContext {
930
979
 
931
980
  ### Platform Auth
932
981
  - `platformLogin(email, password)` → `TokenPair`
933
- - `login(email, password)` → `TokenPair` (alias for platformLogin)
982
+ - `login(email, password)` → `TokenPair` **deprecated**; use `platformLogin` (operator) or `loginAppUser` (end-user)
934
983
  - `refresh()` → `TokenPair`
935
984
  - `logout()` → `void`
936
985
  - `me()` → `UserInfo`
937
- - `platformRegister(body)` → `unknown`
986
+ - `platformRegister(body)` → `PlatformRegisterResult`
938
987
  - `platformRegistrationConfig()` → `PlatformRegistrationConfig`
939
988
  - `platformWorkspaces()` → `PlatformWorkspacesResponse`
940
- - `listMyTenants()` / `listMyProjects()` → `OwnedTenantInfo[]`
941
- - `switchTenant(tenantId)` / `switchProject(projectId)` `void` (platform console only)
989
+ - `listMyProjects()` → `ProjectInfo[]` — preferred
990
+ - `listMyTenants()` `ProjectInfo[]` **deprecated**; same as `listMyProjects`
991
+ - `switchProject(projectId)` → `void` (platform console only) — preferred
992
+ - `switchTenant(tenantId)` → `void` — **deprecated**; same as `switchProject`
942
993
  - `changePassword(current, new)` → `void`
943
994
 
944
995
  ### Context & Configuration
@@ -949,7 +1000,7 @@ interface DatabaseContext {
949
1000
  - `clearAppUserTokens()` → `void`
950
1001
 
951
1002
  ### XML Documents
952
- - `writeDocumentJson(xml, options?)` → `void` — JSON wrapper (recommended)
1003
+ - `writeXmlDocument(xml, options?)` → `void` — XML via JSON body (recommended). Deprecated: `writeDocumentJson`.
953
1004
  - `writeXML(xml)` → `void` — Raw XML body
954
1005
  - `queryByIdentifier(identifier, flags?, filter?, pathFilter?)` → `string[]`
955
1006
  - `queryDocuments(query: XCiteQuery, flags?, filter?, pathFilter?)` → `string[]`
@@ -965,20 +1016,22 @@ interface DatabaseContext {
965
1016
 
966
1017
  ### JSON Documents
967
1018
  - `writeJsonDocument(identifier, data)` → `void`
968
- - `readJsonDocument(identifier)` → `unknown`
1019
+ - `readJsonDocument<T>(identifier)` → `T` (default `unknown`)
969
1020
  - `deleteJsonDocument(identifier)` → `void`
970
1021
  - `listJsonDocuments(match?, limit?, offset?)` → `ListIdentifiersResult`
1022
+ - `put(identifier, data)` / `get<T>(identifier)` / `remove(identifier)` / `list(match?, limit?, offset?)` — JSON CRUD aliases
971
1023
 
972
1024
  ### Metadata
973
1025
  - `addMeta(identifier, value, path?, opts?)` → `boolean`
974
1026
  - `addMetaByQuery(query, value, path?, firstMatch?, opts?)` → `boolean`
975
1027
  - `appendMeta(identifier, value, path?)` → `boolean`
976
1028
  - `appendMetaByQuery(query, value, path?, firstMatch?)` → `boolean`
977
- - `queryMeta(identifier, path?)` → `unknown`
978
- - `queryMetaByQuery(query, path?)` → `unknown`
1029
+ - `queryMeta<T>(identifier, path?)` → `T`
1030
+ - `queryMetaByQuery<T>(query, path?)` → `T`
979
1031
  - `clearMeta(query)` → `boolean`
980
1032
 
981
1033
  ### Branches
1034
+ - `withBranch(name, fn, options?)` → `{ result, commit?, merge? }` — branch, callback, commit, merge back
982
1035
  - `createBranch(name, fromBranch?, fromDate?)` → `void`
983
1036
  - `listBranches()` → `BranchInfo[]`
984
1037
  - `getBranch(name)` → `BranchInfo`
@@ -1012,7 +1065,7 @@ interface DatabaseContext {
1012
1065
  - `reindex()` → `{ status, message }`
1013
1066
 
1014
1067
  ### Unquery
1015
- - `unquery(query: XCiteQuery, unqueryDoc)` → `unknown`
1068
+ - `unquery<T>(query: XCiteQuery, unqueryDoc)` → `T`
1016
1069
 
1017
1070
  ### Security Policies
1018
1071
  - `createPolicy(policyId, policy: SecurityPolicy)` → `StoredPolicyResponse`
@@ -1031,7 +1084,7 @@ interface DatabaseContext {
1031
1084
  - `deleteTrigger(name)` → `void`
1032
1085
 
1033
1086
  ### App User Auth
1034
- - `registerAppUser(email, password, displayName?, groups?, attributes?)` → `AppUser`
1087
+ - `registerAppUser(email, password, displayName?, attributes?)` → `AppUser` (groups assigned from server config)
1035
1088
  - `loginAppUser(email, password)` → `AppUserTokenPair`
1036
1089
  - `refreshAppUser()` → `AppUserTokenPair`
1037
1090
  - `logoutAppUser()` → `void`
@@ -1070,6 +1123,9 @@ interface DatabaseContext {
1070
1123
 
1071
1124
  ### WebSocket
1072
1125
  - `subscribe(options: SubscriptionOptions, callback, onError?)` → `WebSocketSubscription`
1126
+ - Browser WebSocket cannot send HTTP headers. The SDK maps credentials to query params:
1127
+ `Authorization: Bearer <jwt>` → `?access_token=<jwt>`, `X-App-User-Token` → `?access_token=<jwt>` (fallback),
1128
+ `X-API-Key` → `?api_key=<key>`. The SDK also propagates `context.project_id` as `?tenant_id=`.
1073
1129
 
1074
1130
  ## Key Types
1075
1131
 
@@ -1116,7 +1172,12 @@ Install: `pip install xcitedb`
1116
1172
 
1117
1173
  ```python
1118
1174
  import asyncio
1119
- from xcitedb import XCiteDBClient, XCiteQuery, DatabaseContext
1175
+ from xcitedb import (
1176
+ XCiteDBClient,
1177
+ XCiteQuery,
1178
+ DatabaseContext,
1179
+ TextSearchQuery,
1180
+ )
1120
1181
 
1121
1182
  async def main():
1122
1183
  async with XCiteDBClient(
@@ -1125,13 +1186,20 @@ async def main():
1125
1186
  context=DatabaseContext(branch="", date=""),
1126
1187
  ) as client:
1127
1188
  print(await client.health())
1128
- ids = await client.query_documents(XCiteQuery(match_start="/manual/"))
1129
- print(ids)
1189
+ print(await client.query_documents(XCiteQuery(match_start="/manual/")))
1190
+ await client.write_json_document("app.settings", {"theme": "dark"})
1191
+ print(await client.read_json_document("app.settings"))
1192
+ print(await client.list_identifiers(XCiteQuery(match_start="/manual/")))
1193
+ print(await client.search(TextSearchQuery(query="guide", limit=10)))
1194
+ await client.platform_login("admin@localhost", "password")
1195
+ # await client.login_app_user("user@example.com", "pw") # set context.project_id / tenant_id if needed
1196
+ async with client.with_branch("feature-x", message="WIP", auto_merge=True):
1197
+ await client.put("app.settings", {"theme": "light"})
1130
1198
 
1131
1199
  asyncio.run(main())
1132
1200
  ```
1133
1201
 
1134
- Methods mirror the JavaScript SDK with Python naming conventions (e.g. `query_documents`, `write_json_document`).
1202
+ Async client: `write_xml_document` / `write_document_json` (deprecated), `write_json_document`, `read_json_document`, `list_json_documents`, `list_identifiers`, `search`, `reindex`, `platform_login` / `login` (deprecated), `login_app_user`, `refresh_app_user`, `logout_app_user`, `register_app_user`, `put` / `get` / `remove` / `list_documents` (JSON aliases), `with_branch` (async context manager).
1135
1203
 
1136
1204
  ---
1137
1205
 
@@ -1153,6 +1221,6 @@ q.match_start = "/manual/";
1153
1221
  auto ids = client.query_documents(q);
1154
1222
  ```
1155
1223
 
1156
- Synchronous (blocking) HTTP client. Methods mirror the JavaScript SDK with C++ naming. Errors throw `xcitedb::XCiteDBError` with `.status()` and `.body()`.
1224
+ Synchronous (blocking) HTTP client. Methods mirror the JavaScript SDK with C++ naming (`write_xml_document`, deprecated `write_document_json`). Errors throw `xcitedb::XCiteDBError` with `.status()` and `.body()`.
1157
1225
 
1158
1226
  Includes optional `xcitevcs` CLI for command-line operations (branches, commits, documents, search, import/export).
package/llms.txt CHANGED
@@ -20,6 +20,32 @@ These are the most common sources of confusion for developers and AI assistants:
20
20
 
21
21
  7. **The query model is NOT SQL.** Listing/filtering documents uses query parameters on REST endpoints (`match`, `match_start`, `contains`, `regex`, `prefix`). Advanced analytics use **Unquery**, a JSON-defined declarative DSL posted to `POST /api/v1/unquery`.
22
22
 
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
+
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 those requests **skip** normal auth; send **`X-Test-Auth: required`** to exercise real JWT/API-key auth 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).
26
+
27
+ ## Common Pitfalls
28
+
29
+ 1. **`baseUrl` must be origin-only (no `/api` or `/api/v1` path).** The SDK prepends `/api/v1/…` to every request. If `baseUrl` is `https://host/api/v1`, requests hit `/api/v1/api/v1/…` which typically returns **405 Not Allowed** from the reverse proxy. Use only the scheme + host + optional port: `https://host` or `http://localhost:8080`.
30
+
31
+ 2. **Browser WebSocket requires correct CORS and proxy config.** Browsers add an `Origin` header to WebSocket connections — make sure your project's CORS allowed-origins includes your app's origin. Reverse proxies (nginx) must forward WebSocket `Upgrade` requests to XCiteDB's `/api/v1/ws` endpoint (the default `docker/nginx.conf` already does this). For production deployments that need fine-grained server-side filtering or short-lived auth tickets, consider proxying the WebSocket through your own backend.
32
+
33
+ 3. **Prefer `context.project_id` over `platformConsole` for project-scoped API keys.** `platformConsole` / `projectId` is designed for platform-operator keys that select a project dynamically via the `X-Project-Id` header. For project-scoped keys (which already identify their project), set **`context.project_id`** instead — it adds `tenant_id` to app-auth JSON bodies via `mergeAppTenant()` without sending `X-Project-Id`.
34
+
35
+ 4. **Prefer `https://` in `baseUrl` for remote/production hosts.** XciteDB's default nginx uses a method-preserving **308** redirect (POST stays POST), but third-party proxies or older configurations may use **301** which silently downgrades POST to GET. Using `https://` directly avoids the redirect entirely.
36
+
37
+ 5. **`context.project_id` (or `tenant_id`) is required for app-user self-registration.** `registerAppUser()` uses `mergeAppTenant(body)` to add `tenant_id` to the JSON body only when `context.project_id` or `context.tenant_id` is set. If both are omitted, the server cannot determine which project to register the user in. Always set `project_id` in the constructor `context` when calling `registerAppUser`, `loginAppUser`, and other public app-auth methods.
38
+
39
+ 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()`.
40
+
41
+ ## Test mode (how to use)
42
+
43
+ 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.
44
+ 2. **Run tests:** Every request that should hit the throwaway DB must include **`X-Test-Session: <token>`** (and your usual `X-Branch` / `context` as needed). Data is stored under the server’s `_test/<session>/` area, not your production tenant.
45
+ 3. **Auth behavior:** Omit extra headers for frictionless tests. To test policies and real identities, set **`X-Test-Auth: required`** and send normal credentials; the DB is still the test session’s.
46
+ 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.
47
+ 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.
48
+
23
49
  ## JavaScript/TypeScript SDK (`@xcitedbs/client`)
24
50
 
25
51
  Install: `npm install @xcitedbs/client`
@@ -49,7 +75,7 @@ const docs = await client.queryDocuments({ match_start: '/manual/' });
49
75
  const xml = await client.queryByIdentifier('/manual/v1/intro', 'FirstMatch');
50
76
 
51
77
  // Write an XML document (identifier is inside the XML body)
52
- await client.writeDocumentJson(
78
+ await client.writeXmlDocument(
53
79
  '<chapter db:identifier="/manual/v1/intro"><title>Introduction</title></chapter>'
54
80
  );
55
81
 
@@ -59,6 +85,18 @@ await client.writeJsonDocument('app.settings', { theme: 'dark', locale: 'en' });
59
85
  // Read a JSON document
60
86
  const settings = await client.readJsonDocument('app.settings');
61
87
 
88
+ // Quick JSON CRUD aliases (same as writeJsonDocument / readJsonDocument / deleteJsonDocument / listJsonDocuments)
89
+ await client.put('app.prefs', { theme: 'dark' });
90
+ const prefs = await client.get<Record<string, unknown>>('app.prefs');
91
+ await client.remove('app.prefs');
92
+ const keys = await client.list(undefined, 50, 0);
93
+
94
+ // Branch helper: create branch, run work, commit, merge back (restores context)
95
+ await client.withBranch('feature-x', async (c) => {
96
+ await c.writeJsonDocument('app.settings', { theme: 'light' });
97
+ return 'ok';
98
+ }, { message: 'Light theme', autoMerge: true, fromBranch: '' });
99
+
62
100
  // Create a branch, make changes, commit, merge
63
101
  await client.createBranch('feature-x');
64
102
  client.setContext({ branch: 'feature-x' });
@@ -96,7 +134,8 @@ interface XCiteDBClientOptions {
96
134
  branch?: string; // Branch name; '' = default/root timeline
97
135
  date?: string; // Point-in-time (ISO-like or internal date key)
98
136
  prefix?: string; // Optional identifier prefix filter
99
- tenant_id?: string; // Project/tenant ID for app-user auth
137
+ project_id?: string; // Preferred: project id for app-user public auth (`tenant_id` on wire)
138
+ tenant_id?: string; // Deprecated alias of project_id
100
139
  };
101
140
  platformConsole?: boolean; // true = use X-Project-Id header
102
141
  projectId?: string; // Active project for platform console mode
@@ -114,7 +153,7 @@ interface XCiteDBClientOptions {
114
153
  - `setProjectId(id)` — Switch active project (platform console)
115
154
 
116
155
  **XML Documents:**
117
- - `writeDocumentJson(xml, options?)` — Store XML (identifier inside XML body)
156
+ - `writeXmlDocument(xml, options?)` — Store XML via JSON body (identifier inside XML). Deprecated alias: `writeDocumentJson`.
118
157
  - `writeXML(xml)` — Store raw XML with `Content-Type: application/xml`
119
158
  - `queryByIdentifier(id, flags?, filter?)` — Get document(s) by identifier
120
159
  - `queryDocuments(query, flags?)` — List/filter documents
@@ -124,9 +163,10 @@ interface XCiteDBClientOptions {
124
163
 
125
164
  **JSON Documents:**
126
165
  - `writeJsonDocument(identifier, data)` — Store a JSON document
127
- - `readJsonDocument(identifier)` — Read a JSON document
166
+ - `readJsonDocument(identifier)` — Read a JSON document (generic `readJsonDocument<T>()` supported)
128
167
  - `deleteJsonDocument(identifier)` — Delete a JSON document
129
168
  - `listJsonDocuments(match?, limit?, offset?)` — List JSON document keys
169
+ - **Quick JSON aliases:** `put`, `get`, `remove`, `list` — same as the four methods above
130
170
 
131
171
  **Metadata (JSON on XML):**
132
172
  - `addMeta(identifier, value, path?)` — Set metadata on a document
@@ -135,6 +175,7 @@ interface XCiteDBClientOptions {
135
175
  - `clearMeta(query)` — Remove metadata
136
176
 
137
177
  **Versioning:**
178
+ - `withBranch(name, fn, options?)` — Create branch, run callback on client, commit, merge back (optional `autoMerge: false`)
138
179
  - `createBranch(name, fromBranch?, fromDate?)` — Create branch
139
180
  - `listBranches()` — List all branches
140
181
  - `mergeBranch(target, source, options?)` — Merge branches
@@ -167,11 +208,14 @@ interface XCiteDBClientOptions {
167
208
 
168
209
  **App Users (admin):**
169
210
  - `listAppUsers()` / `createAppUser()` / `deleteAppUser()` — Manage end-user accounts
170
- - `registerAppUser()` — Self-registration
211
+ - `registerAppUser(email, password, displayName?, attributes?)` — Self-registration (groups assigned from server config)
171
212
  - `getAppAuthConfig()` — Auth configuration
172
213
 
173
214
  **WebSocket:**
174
215
  - `subscribe(options, callback, onError?)` — Real-time document change notifications
216
+ - Browser WebSocket cannot send HTTP headers. The SDK maps credentials to query params:
217
+ `Authorization: Bearer <jwt>` → `?access_token=<jwt>`, `X-App-User-Token` → `?access_token=<jwt>` (fallback),
218
+ `X-API-Key` → `?api_key=<key>`. The SDK also propagates `context.project_id` as `?tenant_id=`.
175
219
 
176
220
  ### Query Flags
177
221
 
@@ -191,7 +235,7 @@ Install: `pip install xcitedb`
191
235
 
192
236
  ```python
193
237
  import asyncio
194
- from xcitedb import XCiteDBClient, XCiteQuery, DatabaseContext
238
+ from xcitedb import XCiteDBClient, XCiteQuery, DatabaseContext, TextSearchQuery
195
239
 
196
240
  async def main():
197
241
  async with XCiteDBClient(
@@ -202,6 +246,11 @@ async def main():
202
246
  print(await client.health())
203
247
  ids = await client.query_documents(XCiteQuery(match_start="/manual/"))
204
248
  print(ids)
249
+ await client.write_json_document("app.settings", {"theme": "dark"})
250
+ print(await client.read_json_document("app.settings"))
251
+ print(await client.search(TextSearchQuery(query="guide", limit=10)))
252
+ pair = await client.platform_login("admin@localhost", "password")
253
+ # await client.login_app_user("user@example.com", "secret", tenant_id="...")
205
254
 
206
255
  asyncio.run(main())
207
256
  ```
@@ -234,7 +283,8 @@ auto ids = client.query_documents(q);
234
283
 
235
284
  ## Documentation
236
285
 
237
- Full API reference is available at your XciteDB server's `/docs` path. Key sections:
286
+ - **OpenAPI 3.1:** Repository file `docs/openapi.yaml` (machine-readable route map and shared schemas).
287
+ - Full API reference is also available at your XciteDB server's `/docs` path. Key sections:
238
288
  - Getting started — Base URL, headers, errors, pagination
239
289
  - Documents — XML document CRUD, identifiers, hierarchy
240
290
  - JSON documents — JSON document CRUD
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.2.0",
3
+ "version": "0.2.6",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",