@xcitedbs/client 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, CommitRecord, DatabaseContext, DiffRef, DiffResult, Flags, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, LogEntry, MergeResult, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, StoredPolicyResponse, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, CreateTestSessionOptions, 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, UnqueryTemplate, 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;
@@ -129,17 +129,131 @@ export declare class XCiteDBClient {
129
129
  deleteAppUser(userId: string): Promise<void>;
130
130
  updateAppUserGroups(userId: string, groups: string[]): Promise<void>;
131
131
  updateAppUserStatus(userId: string, status: 'active' | 'disabled' | 'pending_verification'): Promise<void>;
132
+ /**
133
+ * Create an ABAC policy (`POST /api/v1/security/policies`). Requires admin/editor role.
134
+ * Use `conditions.expression` for attribute-based rules (same predicate syntax as Unquery `?` conditions).
135
+ * Policy actions: `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
136
+ *
137
+ * @example Tenant isolation: first path segment must equal app-user attribute `tenant_code`.
138
+ * ```ts
139
+ * await client.createPolicy('tenant_isolation', {
140
+ * effect: 'allow',
141
+ * priority: 100,
142
+ * subjects: { type: 'app_user' },
143
+ * actions: ['read'],
144
+ * resources: { identifiers: [{ match_start: '/' }] },
145
+ * conditions: { expression: 'resource.path[0] = subject.attr.tenant_code' },
146
+ * });
147
+ * ```
148
+ *
149
+ * @example Deny viewers write access under `/admin/`.
150
+ * ```ts
151
+ * await client.createPolicy('deny_viewers_admin', {
152
+ * effect: 'deny',
153
+ * priority: 10,
154
+ * subjects: { type: 'app_user', groups: ['viewers'] },
155
+ * resources: { identifiers: [{ match_start: '/admin/' }] },
156
+ * actions: ['write', 'delete', 'meta:write'],
157
+ * });
158
+ * ```
159
+ *
160
+ * @example Branch-only rule via `conditions.branches`.
161
+ * ```ts
162
+ * await client.createPolicy('staging_only', {
163
+ * effect: 'allow',
164
+ * priority: 50,
165
+ * subjects: { type: 'app_user', groups: ['qa'] },
166
+ * actions: ['read', 'write'],
167
+ * resources: { identifiers: [{ match_start: '/staging/' }] },
168
+ * conditions: { branches: ['stg', 'stg*'] },
169
+ * });
170
+ * ```
171
+ */
132
172
  createPolicy(policyId: string, policy: SecurityPolicy): Promise<StoredPolicyResponse>;
133
173
  listPolicies(): Promise<Record<string, SecurityPolicy>>;
134
174
  getPolicy(policyId: string): Promise<StoredPolicyResponse>;
135
175
  updatePolicy(policyId: string, policy: SecurityPolicy): Promise<PolicyUpdateResponse>;
136
176
  deletePolicy(policyId: string): Promise<void>;
177
+ /**
178
+ * Create or update a trigger (`POST /api/v1/triggers`). Definitions live under `/_xcitedb/triggers`.
179
+ * After matching events, the server runs `action.unquery` over `action.query` and writes JSON to
180
+ * `action.meta_path` on `action.target_identifier` (use `"$trigger_identifier"` for the firing doc).
181
+ * Unquery can use `$trigger_identifier`, `$trigger_meta_path`, `$trigger_operation`, `$trigger_value`.
182
+ *
183
+ * @example On meta change under `/projects/`, append a snapshot entry to an index document.
184
+ * ```ts
185
+ * await client.upsertTrigger('project_meta_index', {
186
+ * event: 'meta_changed',
187
+ * match: {
188
+ * identifiers: [{ match_start: '/projects/' }],
189
+ * match_meta_path: 'status',
190
+ * },
191
+ * action: {
192
+ * query: { match: '$trigger_identifier' },
193
+ * unquery: { lastId: '$trigger_identifier', path: '$trigger_meta_path' },
194
+ * target_identifier: '/indexes/project_meta',
195
+ * meta_path: 'entries',
196
+ * mode: 'append',
197
+ * },
198
+ * });
199
+ * ```
200
+ *
201
+ * @example On document write, write a computed summary onto the same identifier's meta.
202
+ * ```ts
203
+ * await client.upsertTrigger('doc_written_summary', {
204
+ * event: 'document_written',
205
+ * match: { identifiers: [{ match_start: '/articles/' }] },
206
+ * action: {
207
+ * query: { match: '$trigger_identifier' },
208
+ * unquery: { id: '$identifier', nodeCount: '$count' },
209
+ * target_identifier: '$trigger_identifier',
210
+ * meta_path: 'stats',
211
+ * },
212
+ * });
213
+ * ```
214
+ */
137
215
  upsertTrigger(triggerId: string, trigger: TriggerDefinition): Promise<StoredTriggerResponse>;
138
216
  listTriggers(): Promise<Record<string, TriggerDefinition>>;
139
217
  getTrigger(name: string): Promise<StoredTriggerResponse>;
140
218
  deleteTrigger(name: string): Promise<void>;
219
+ /**
220
+ * Dry-run access check (`POST /api/v1/security/check`). Returns `effect` and optional `matched_policy_id`.
221
+ * Useful for debugging policies. Actions: `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * const r = await client.checkAccess(
226
+ * {
227
+ * type: 'app_user',
228
+ * user_id: 'u1',
229
+ * email: 'alice@example.com',
230
+ * groups: ['editors'],
231
+ * role: 'app_user',
232
+ * attributes: { tenant_code: 'acme', level: 5 },
233
+ * },
234
+ * '/acme/reports/q1',
235
+ * 'read',
236
+ * undefined,
237
+ * 'main'
238
+ * );
239
+ * console.log(r.effect, r.matched_policy_id);
240
+ * ```
241
+ */
141
242
  checkAccess(subject: PolicySubjectInput, identifier: string, action: string, metaPath?: string, branch?: string): Promise<AccessCheckResult>;
142
243
  getSecurityConfig(): Promise<SecurityConfig>;
244
+ /**
245
+ * Update tenant security defaults (`PUT /api/v1/security/config`). When ABAC is enabled, pair
246
+ * `app_user_default_effect: 'deny'` with explicit allow policies for least privilege.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * await client.updateSecurityConfig({
251
+ * app_user_default_effect: 'deny',
252
+ * default_effect: 'allow',
253
+ * developer_bypass: true,
254
+ * });
255
+ * ```
256
+ */
143
257
  updateSecurityConfig(config: Partial<SecurityConfig>): Promise<void>;
144
258
  createBranch(name: string, fromBranch?: string, fromDate?: string): Promise<void>;
145
259
  deleteBranch(name: string): Promise<void>;
@@ -230,7 +344,32 @@ export declare class XCiteDBClient {
230
344
  acquireLock(identifier: string, expires?: number): Promise<LockInfo>;
231
345
  releaseLock(identifier: string, lockId: string): Promise<boolean>;
232
346
  findLocks(identifier: string): Promise<LockInfo[]>;
233
- unquery<T = UnqueryResult>(query: XCiteQuery, unquery: unknown): Promise<T>;
347
+ /**
348
+ * Run Unquery (`POST /api/v1/unquery`): declarative analytics over documents matching `query`.
349
+ * The `unquery` argument is a JSON template; keys are output fields, string values are expressions.
350
+ *
351
+ * @example Project a meta field into a result key.
352
+ * ```ts
353
+ * const out = await client.unquery<{ tag: string }>(
354
+ * { match_start: '/manual/' },
355
+ * { title: 'chapter_title' }
356
+ * );
357
+ * ```
358
+ *
359
+ * @example Count matching documents.
360
+ * ```ts
361
+ * const out = await client.unquery<{ total: number }>({ match_start: '/orders/' }, { total: '$count' });
362
+ * ```
363
+ *
364
+ * @example XPath from XML (string value must escape quotes for TS).
365
+ * ```ts
366
+ * const out = await client.unquery<{ heading: string }>(
367
+ * { match: '/book/ch1' },
368
+ * { heading: '$xpath("//h1")' }
369
+ * );
370
+ * ```
371
+ */
372
+ unquery<T = UnqueryResult>(query: XCiteQuery, unquery: UnqueryTemplate): Promise<T>;
234
373
  search(q: TextSearchQuery): Promise<TextSearchResult>;
235
374
  reindex(): Promise<{
236
375
  status: string;
package/dist/client.js CHANGED
@@ -502,6 +502,46 @@ class XCiteDBClient {
502
502
  await this.request('PUT', `/api/v1/app/users/${encodeURIComponent(userId)}/status`, { status });
503
503
  }
504
504
  // --- Security policies (developer admin/editor) ---
505
+ /**
506
+ * Create an ABAC policy (`POST /api/v1/security/policies`). Requires admin/editor role.
507
+ * Use `conditions.expression` for attribute-based rules (same predicate syntax as Unquery `?` conditions).
508
+ * Policy actions: `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
509
+ *
510
+ * @example Tenant isolation: first path segment must equal app-user attribute `tenant_code`.
511
+ * ```ts
512
+ * await client.createPolicy('tenant_isolation', {
513
+ * effect: 'allow',
514
+ * priority: 100,
515
+ * subjects: { type: 'app_user' },
516
+ * actions: ['read'],
517
+ * resources: { identifiers: [{ match_start: '/' }] },
518
+ * conditions: { expression: 'resource.path[0] = subject.attr.tenant_code' },
519
+ * });
520
+ * ```
521
+ *
522
+ * @example Deny viewers write access under `/admin/`.
523
+ * ```ts
524
+ * await client.createPolicy('deny_viewers_admin', {
525
+ * effect: 'deny',
526
+ * priority: 10,
527
+ * subjects: { type: 'app_user', groups: ['viewers'] },
528
+ * resources: { identifiers: [{ match_start: '/admin/' }] },
529
+ * actions: ['write', 'delete', 'meta:write'],
530
+ * });
531
+ * ```
532
+ *
533
+ * @example Branch-only rule via `conditions.branches`.
534
+ * ```ts
535
+ * await client.createPolicy('staging_only', {
536
+ * effect: 'allow',
537
+ * priority: 50,
538
+ * subjects: { type: 'app_user', groups: ['qa'] },
539
+ * actions: ['read', 'write'],
540
+ * resources: { identifiers: [{ match_start: '/staging/' }] },
541
+ * conditions: { branches: ['stg', 'stg*'] },
542
+ * });
543
+ * ```
544
+ */
505
545
  async createPolicy(policyId, policy) {
506
546
  return this.request('POST', '/api/v1/security/policies', {
507
547
  policy_id: policyId,
@@ -524,6 +564,44 @@ class XCiteDBClient {
524
564
  await this.request('DELETE', `/api/v1/security/policies/${encodeURIComponent(policyId)}`);
525
565
  }
526
566
  // --- Triggers (developer admin/editor) ---
567
+ /**
568
+ * Create or update a trigger (`POST /api/v1/triggers`). Definitions live under `/_xcitedb/triggers`.
569
+ * After matching events, the server runs `action.unquery` over `action.query` and writes JSON to
570
+ * `action.meta_path` on `action.target_identifier` (use `"$trigger_identifier"` for the firing doc).
571
+ * Unquery can use `$trigger_identifier`, `$trigger_meta_path`, `$trigger_operation`, `$trigger_value`.
572
+ *
573
+ * @example On meta change under `/projects/`, append a snapshot entry to an index document.
574
+ * ```ts
575
+ * await client.upsertTrigger('project_meta_index', {
576
+ * event: 'meta_changed',
577
+ * match: {
578
+ * identifiers: [{ match_start: '/projects/' }],
579
+ * match_meta_path: 'status',
580
+ * },
581
+ * action: {
582
+ * query: { match: '$trigger_identifier' },
583
+ * unquery: { lastId: '$trigger_identifier', path: '$trigger_meta_path' },
584
+ * target_identifier: '/indexes/project_meta',
585
+ * meta_path: 'entries',
586
+ * mode: 'append',
587
+ * },
588
+ * });
589
+ * ```
590
+ *
591
+ * @example On document write, write a computed summary onto the same identifier's meta.
592
+ * ```ts
593
+ * await client.upsertTrigger('doc_written_summary', {
594
+ * event: 'document_written',
595
+ * match: { identifiers: [{ match_start: '/articles/' }] },
596
+ * action: {
597
+ * query: { match: '$trigger_identifier' },
598
+ * unquery: { id: '$identifier', nodeCount: '$count' },
599
+ * target_identifier: '$trigger_identifier',
600
+ * meta_path: 'stats',
601
+ * },
602
+ * });
603
+ * ```
604
+ */
527
605
  async upsertTrigger(triggerId, trigger) {
528
606
  return this.request('POST', '/api/v1/triggers', {
529
607
  trigger_id: triggerId,
@@ -544,6 +622,29 @@ class XCiteDBClient {
544
622
  const q = buildQuery({ name });
545
623
  await this.request('DELETE', `/api/v1/triggers${q}`);
546
624
  }
625
+ /**
626
+ * Dry-run access check (`POST /api/v1/security/check`). Returns `effect` and optional `matched_policy_id`.
627
+ * Useful for debugging policies. Actions: `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
628
+ *
629
+ * @example
630
+ * ```ts
631
+ * const r = await client.checkAccess(
632
+ * {
633
+ * type: 'app_user',
634
+ * user_id: 'u1',
635
+ * email: 'alice@example.com',
636
+ * groups: ['editors'],
637
+ * role: 'app_user',
638
+ * attributes: { tenant_code: 'acme', level: 5 },
639
+ * },
640
+ * '/acme/reports/q1',
641
+ * 'read',
642
+ * undefined,
643
+ * 'main'
644
+ * );
645
+ * console.log(r.effect, r.matched_policy_id);
646
+ * ```
647
+ */
547
648
  async checkAccess(subject, identifier, action, metaPath, branch) {
548
649
  const body = { subject, identifier, action };
549
650
  if (metaPath !== undefined)
@@ -555,6 +656,19 @@ class XCiteDBClient {
555
656
  async getSecurityConfig() {
556
657
  return this.request('GET', '/api/v1/security/config');
557
658
  }
659
+ /**
660
+ * Update tenant security defaults (`PUT /api/v1/security/config`). When ABAC is enabled, pair
661
+ * `app_user_default_effect: 'deny'` with explicit allow policies for least privilege.
662
+ *
663
+ * @example
664
+ * ```ts
665
+ * await client.updateSecurityConfig({
666
+ * app_user_default_effect: 'deny',
667
+ * default_effect: 'allow',
668
+ * developer_bypass: true,
669
+ * });
670
+ * ```
671
+ */
558
672
  async updateSecurityConfig(config) {
559
673
  await this.request('PUT', '/api/v1/security/config', config);
560
674
  }
@@ -887,6 +1001,31 @@ class XCiteDBClient {
887
1001
  async findLocks(identifier) {
888
1002
  return this.request('GET', `/api/v1/locks${buildQuery({ identifier })}`);
889
1003
  }
1004
+ /**
1005
+ * Run Unquery (`POST /api/v1/unquery`): declarative analytics over documents matching `query`.
1006
+ * The `unquery` argument is a JSON template; keys are output fields, string values are expressions.
1007
+ *
1008
+ * @example Project a meta field into a result key.
1009
+ * ```ts
1010
+ * const out = await client.unquery<{ tag: string }>(
1011
+ * { match_start: '/manual/' },
1012
+ * { title: 'chapter_title' }
1013
+ * );
1014
+ * ```
1015
+ *
1016
+ * @example Count matching documents.
1017
+ * ```ts
1018
+ * const out = await client.unquery<{ total: number }>({ match_start: '/orders/' }, { total: '$count' });
1019
+ * ```
1020
+ *
1021
+ * @example XPath from XML (string value must escape quotes for TS).
1022
+ * ```ts
1023
+ * const out = await client.unquery<{ heading: string }>(
1024
+ * { match: '/book/ch1' },
1025
+ * { heading: '$xpath("//h1")' }
1026
+ * );
1027
+ * ```
1028
+ */
890
1029
  async unquery(query, unquery) {
891
1030
  return this.request('POST', '/api/v1/unquery', { query, unquery });
892
1031
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { XCiteDBClient } from './client';
2
2
  export { WebSocketSubscription } from './websocket';
3
- export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, DatabaseContext, Flags, JsonDocumentData, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TokenPair, UserInfo, WriteDocumentOptions, CreateTestSessionOptions, XCiteDBClientOptions, UnqueryResult, 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, UnqueryTemplate, XCiteQuery, } from './types';
4
4
  export { XCiteDBError } from './types';
package/dist/types.d.ts CHANGED
@@ -20,6 +20,13 @@ export type JsonDocumentData = Record<string, unknown>;
20
20
  export type MetaValue = unknown;
21
21
  /** Result of `unquery` (shape depends on the unquery definition). */
22
22
  export type UnqueryResult = unknown;
23
+ /**
24
+ * Unquery DSL template document. Object keys become output field names; string values are
25
+ * expressions (e.g. `"$count"`, `"$xpath(\"//title\")"`, or a meta field name). Supports nested
26
+ * objects, arrays, and `$`-prefixed builtins. See `llms-full.txt` shipped with this package for
27
+ * the full DSL reference.
28
+ */
29
+ export type UnqueryTemplate = Record<string, unknown>;
23
30
  /** Typical `POST /api/v1/platform/auth/register` response (fields vary by registration policy). */
24
31
  export interface PlatformRegisterResult {
25
32
  user_id?: string;
@@ -340,6 +347,12 @@ export interface PolicyResources {
340
347
  }
341
348
  export interface PolicyConditions {
342
349
  branches?: string[];
350
+ /**
351
+ * Unquery condition expression.
352
+ * Policies: evaluated against `{ subject, resource, env }`.
353
+ * Triggers: evaluated against `{ trigger, value, resource, env }` (see server docs).
354
+ */
355
+ expression?: string;
343
356
  }
344
357
  export interface SecurityPolicy {
345
358
  description?: string;
@@ -358,6 +371,8 @@ export interface PolicySubjectInput {
358
371
  groups?: string[];
359
372
  role?: string;
360
373
  username?: string;
374
+ /** Custom app-user attributes (mirrors stored user `attributes`); used by `conditions.expression`. */
375
+ attributes?: Record<string, unknown>;
361
376
  }
362
377
  export interface SecurityConfig {
363
378
  default_effect: 'allow' | 'deny';
@@ -386,8 +401,29 @@ export interface PolicyUpdateResponse {
386
401
  policy: SecurityPolicy;
387
402
  warnings?: string[];
388
403
  }
389
- /** Stored trigger document under /_xcitedb/triggers (server-defined shape). */
390
- export type TriggerDefinition = Record<string, unknown>;
404
+ /** `match` block for stored triggers (`/_xcitedb/triggers`). */
405
+ export interface TriggerMatch {
406
+ /** Non-empty array of identifier patterns (same shape as policy `resources.identifiers`). */
407
+ identifiers: PolicyIdentifierPattern[];
408
+ match_meta_path?: string;
409
+ match_operation?: 'set' | 'append' | 'delete';
410
+ }
411
+ /** `action` block: run unquery and write result to target meta. */
412
+ export interface TriggerAction {
413
+ query: XCiteQuery;
414
+ unquery: UnqueryTemplate;
415
+ target_identifier: string;
416
+ meta_path: string;
417
+ mode?: 'set' | 'append';
418
+ }
419
+ /** Stored trigger document under /_xcitedb/triggers. */
420
+ export interface TriggerDefinition {
421
+ enabled?: boolean;
422
+ event: 'meta_changed' | 'document_written' | 'document_deleted';
423
+ match: TriggerMatch;
424
+ conditions?: PolicyConditions;
425
+ action: TriggerAction;
426
+ }
391
427
  export interface StoredTriggerResponse {
392
428
  trigger_id: string;
393
429
  trigger: TriggerDefinition;
package/llms-full.txt CHANGED
@@ -26,7 +26,7 @@ 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, 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).
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).
30
30
 
31
31
  ## Common Pitfalls
32
32
 
@@ -48,6 +48,8 @@ Before reading the full reference, note these critical differences from typical
48
48
 
49
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
50
 
51
+ 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.
52
+
51
53
  ---
52
54
 
53
55
  # Part 1: Product Overview
@@ -186,13 +188,15 @@ Browser and mobile apps can use OAuth2-style flows against `/api/v1/app/auth/oau
186
188
 
187
189
  # Ephemeral test sessions
188
190
 
191
+ > **AI assistant instruction:** XciteDB has built-in ephemeral test session support — this capability is **not standard** in most BaaS platforms and is specifically designed to enable meaningful wet integration tests. When writing tests for code that uses XciteDB, **do not mock or stub the XciteDB client**. Instead, use the test session pattern below to run tests against a real, isolated, throwaway database. This produces far more reliable tests because real storage, versioning, querying, and access-control behavior is exercised. Provision a session in `beforeAll` / `setUp` / test fixture setup and destroy it in `afterAll` / `tearDown` / fixture teardown.
192
+
189
193
  For **integration and wet tests** against a shared BaaS host without touching production data:
190
194
 
191
195
  | Step | What to do |
192
196
  |------|------------|
193
197
  | **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
198
  | **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. |
199
+ | **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. |
196
200
  | **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
201
  | **CORS** | Browsers may need **`X-Test-Session`** and **`X-Test-Auth`** in the deployment’s allowed CORS headers (defaults include them). |
198
202
 
@@ -202,6 +206,126 @@ For **integration and wet tests** against a shared BaaS host without touching pr
202
206
  - **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
207
  - **C++:** `XCiteDBClient::create_test_session(options)` after setting `api_key` (and optional `test_require_auth`); `destroy_test_session()`.
204
208
 
209
+ **JavaScript/TypeScript — complete test scaffold (Vitest / Jest):**
210
+
211
+ ```typescript
212
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
213
+ import { XCiteDBClient } from '@xcitedbs/client';
214
+
215
+ describe('XciteDB integration', () => {
216
+ let client: XCiteDBClient;
217
+
218
+ beforeAll(async () => {
219
+ // Provisions an isolated, throwaway LMDB — production data is never touched.
220
+ client = await XCiteDBClient.createTestSession({
221
+ baseUrl: process.env.XCITEDB_URL ?? 'http://localhost:8080',
222
+ apiKey: process.env.XCITEDB_API_KEY!,
223
+ });
224
+ });
225
+
226
+ afterAll(async () => {
227
+ await client.destroyTestSession();
228
+ });
229
+
230
+ it('writes and reads a JSON document', async () => {
231
+ await client.writeJsonDocument('test.config', { env: 'test' });
232
+ const doc = await client.readJsonDocument<{ env: string }>('test.config');
233
+ expect(doc).toMatchObject({ env: 'test' });
234
+ });
235
+
236
+ it('writes and reads an XML document', async () => {
237
+ await client.writeXmlDocument(
238
+ '<doc db:identifier="/test/doc1"><title>Hello</title></doc>'
239
+ );
240
+ const xml = await client.queryByIdentifier('/test/doc1', 'FirstMatch');
241
+ expect(xml).toContain('<title>Hello</title>');
242
+ });
243
+
244
+ it('creates a branch, commits, and merges', async () => {
245
+ await client.withBranch('feature-test', async (c) => {
246
+ await c.writeJsonDocument('test.feature', { active: true });
247
+ }, { message: 'Add feature flag', autoMerge: true });
248
+ const doc = await client.readJsonDocument<{ active: boolean }>('test.feature');
249
+ expect(doc.active).toBe(true);
250
+ });
251
+ });
252
+ ```
253
+
254
+ **Python — complete test scaffold (pytest + pytest-asyncio):**
255
+
256
+ ```python
257
+ import os
258
+ import pytest
259
+ import pytest_asyncio
260
+ from xcitedb import XCiteDBClient
261
+
262
+ @pytest_asyncio.fixture
263
+ async def db():
264
+ # Provisions an isolated, throwaway LMDB — production data is never touched.
265
+ async with XCiteDBClient.test_session(
266
+ os.environ.get("XCITEDB_URL", "http://localhost:8080"),
267
+ api_key=os.environ["XCITEDB_API_KEY"],
268
+ ) as client:
269
+ yield client # session is destroyed automatically on exit
270
+
271
+ @pytest.mark.asyncio
272
+ async def test_json_document(db):
273
+ await db.write_json_document("test.config", {"env": "test"})
274
+ doc = await db.read_json_document("test.config")
275
+ assert doc == {"env": "test"}
276
+
277
+ @pytest.mark.asyncio
278
+ async def test_xml_document(db):
279
+ await db.write_xml_document(
280
+ '<doc db:identifier="/test/doc1"><title>Hello</title></doc>'
281
+ )
282
+ result = await db.query_by_identifier("/test/doc1", "FirstMatch")
283
+ assert "<title>Hello</title>" in result
284
+
285
+ @pytest.mark.asyncio
286
+ async def test_branch_and_merge(db):
287
+ async with db.with_branch("feature-test", message="Add flag", auto_merge=True):
288
+ await db.put("test.feature", {"active": True})
289
+ doc = await db.get("test.feature")
290
+ assert doc["active"] is True
291
+ ```
292
+
293
+ **C++ — complete test scaffold (Catch2):**
294
+
295
+ ```cpp
296
+ #include <xcitedb/xcitedb.hpp>
297
+ #include <catch2/catch_test_macros.hpp>
298
+ #include <cstdlib>
299
+
300
+ // Provisions an isolated, throwaway LMDB — production data is never touched.
301
+ static xcitedb::XCiteDBClient make_test_client() {
302
+ xcitedb::XCiteDBClientOptions opt;
303
+ opt.base_url = std::getenv("XCITEDB_URL") ? std::getenv("XCITEDB_URL")
304
+ : "http://127.0.0.1:8080";
305
+ opt.api_key = std::getenv("XCITEDB_API_KEY");
306
+ return xcitedb::XCiteDBClient::create_test_session(opt);
307
+ }
308
+
309
+ TEST_CASE("XciteDB JSON document round-trip") {
310
+ auto client = make_test_client();
311
+ client.write_json_document("test.config", R"({"env":"test"})");
312
+ auto doc = client.read_json_document("test.config");
313
+ REQUIRE(doc.find("\"env\"") != std::string::npos);
314
+ REQUIRE(doc.find("\"test\"") != std::string::npos);
315
+ client.destroy_test_session();
316
+ }
317
+
318
+ TEST_CASE("XciteDB XML document round-trip") {
319
+ auto client = make_test_client();
320
+ client.write_xml_document(
321
+ R"(<doc db:identifier="/test/doc1"><title>Hello</title></doc>)"
322
+ );
323
+ auto xml = client.query_by_identifier("/test/doc1", "FirstMatch");
324
+ REQUIRE(xml.find("<title>Hello</title>") != std::string::npos);
325
+ client.destroy_test_session();
326
+ }
327
+ ```
328
+
205
329
  ---
206
330
 
207
331
  # Health, version & discovery
@@ -435,6 +559,122 @@ Executes a structured query document (JSON DSL) for advanced analytics, bulk exp
435
559
 
436
560
  Unquery supports: hierarchical navigation (self, parents, ancestors, children, descendants), XPath queries on XML, JSON path navigation, counts, sums, string operations, type casting, and conditional branching. Results are always structured JSON.
437
561
 
562
+ ## Unquery DSL reference
563
+
564
+ The `unquery` body is a **JSON template**: each **object key** names an output field; each **value** is either a nested object/array (recursed) or a **string parsed as an expression**. Arrays produce JSON arrays in order.
565
+
566
+ ### Core expression builtins (string values)
567
+
568
+ - **Document / path:** `$identifier`, `$identifier(n)` (path segment by index), `$key`, `$index`, `$path`
569
+ - **XML:** `$xpath("expr")`, `$lxpath("expr")` (leaf / no-children variant), `$xml`, `$xml_no_children`, `$node`, `$attr("name")`, `$child("name")`, `$text(expr)`
570
+ - **Aggregates / size:** `$count`, `$sum(expr)`, `$avg(expr)`, `$min(expr)`, `$max(expr)`, `$size(expr)` (array length), `$length(expr)` (string length)
571
+ - **String ops:** `$lower(expr)`, `$upper(expr)`, `$substr(s,start,len?)`, `$replace(src,from,to,all?)`, `$split(s,delim)`, `$join(arr,delim?)`, `$find`, `$ifind`
572
+ - **Time / casts:** `$now`, `$to_time(expr,fmt?)`, `$time_to_str(expr,fmt?)`, `$string`, `$number`, `$int`, `$float`, `$bool(expr)`
573
+ - **Control:** `$if(cond, then, else)`, `$var(name)` or `%name` (template variables), `$call(name)` (user `#func`), `$prev(default)`
574
+ - **Other:** `$node_date`, `$data_date(expr?)`, `$in_filter("name")`, `$file`, `$csv`, `$env`, `$filename` (last three blocked in safety mode for standalone `evaluateCondition`)
575
+
576
+ ### Key-side syntax (JSON object member **names**, not values)
577
+
578
+ - Plain names, dotted paths, `[index]`, `$(expression)` / `$expr` for dynamic keys
579
+ - **`#if`**, **`#var`**, **`#assign`**, **`#func name (a,b,…)`**, **`#exists`**, **`#notexists`**, **`#return`**, **`#returnif`**
580
+ - Suffixes on keys: `?condition` (filter branch), `@ ascending` / `@ descending` / `@ unique_ascending` / `@ unique_descending` on values
581
+ - **Context navigation** after `->` (in key paths): `$self`, `$parent`, `$ancestors`, `$ancestors_and_self`, `$children`, `$descendants`, `$descendants_and_self`, `$date`, `$branch`, `$all`, `$var`, `$file`, `$csv`
582
+
583
+ ### Conditions (same grammar as policy `conditions.expression` and Unquery `?` on keys)
584
+
585
+ Operators: `=`, `!=`, `<`, `>`, `<=`, `>=`, `in`, `not_in`, `contains`, `starts_with`, `ends_with`, `matches`, `is_array`, `is_object`, `is_string`, `is_number`, `is_int`, `is_float`, `is_bool`, `is_literal`. Logic: `&` (AND), `|` (OR), `!` (NOT). Postfix `!` on an expression tests **field exists**.
586
+
587
+ ### Graded examples
588
+
589
+ **1. Project one meta field into an output key** (per matching document, then combined per engine rules):
590
+
591
+ ```json
592
+ {
593
+ "query": { "match_start": "/manual/" },
594
+ "unquery": { "title": "chapter_title" }
595
+ }
596
+ ```
597
+
598
+ **2. Count documents in a prefix:**
599
+
600
+ ```json
601
+ {
602
+ "query": { "match_start": "/orders/" },
603
+ "unquery": { "total": "$count" }
604
+ }
605
+ ```
606
+
607
+ **3. XPath from XML:**
608
+
609
+ ```json
610
+ {
611
+ "query": { "match": "/book/ch1" },
612
+ "unquery": { "heading": "$xpath(\"//h1\")" }
613
+ }
614
+ ```
615
+
616
+ **4. Nested object with conditional key** (illustrative; exact key syntax may use `#if` blocks for complex trees):
617
+
618
+ ```json
619
+ {
620
+ "query": { "match_start": "/items/" },
621
+ "unquery": {
622
+ "id": "$identifier",
623
+ "summary": {
624
+ "len": "$length(title_field)"
625
+ }
626
+ }
627
+ }
628
+ ```
629
+
630
+ SDK: `UnqueryTemplate` in `@xcitedbs/client` types; method `unquery(query, unquery)`.
631
+
632
+ ## ABAC policy expression language
633
+
634
+ Policies may set **`conditions.expression`** (optional) and **`conditions.branches`** (optional array). If `branches` is non-empty, the request branch must match an entry (`"*"`, exact name, or `prefix*`).
635
+
636
+ ### Evaluation context for **policies** (`conditions.expression`)
637
+
638
+ ```json
639
+ {
640
+ "subject": {
641
+ "id": "...",
642
+ "email": "...",
643
+ "type": "app_user|developer|...",
644
+ "role": "...",
645
+ "groups": ["..."],
646
+ "attr": { }
647
+ },
648
+ "resource": {
649
+ "identifier": "/full/path",
650
+ "path": ["segment", "segment"]
651
+ },
652
+ "env": { "branch": "..." }
653
+ }
654
+ ```
655
+
656
+ `subject.attr` mirrors app-user **custom attributes** from the user record. `resource.path` is the identifier split on `/` (non-empty segments only).
657
+
658
+ ### Operators (predicates)
659
+
660
+ `=`, `!=`, `>=`, `<=`, `>`, `<`, `in`, `contains`, `starts_with`, `ends_with`, `&`, `|`, `!`, `+` (string concat), postfix `!` (exists).
661
+
662
+ ### Policy action strings (in `actions` arrays)
663
+
664
+ Use: **`read`**, **`write`**, **`delete`**, **`list`**, **`meta:read`**, **`meta:write`**, **`unquery`**. (Do not use legacy `document:write`-style names in new policies.)
665
+
666
+ ### Copy-pasteable expression recipes (from integration tests)
667
+
668
+ | Scenario | `conditions.expression` |
669
+ |----------|-------------------------|
670
+ | Tenant isolation (first path segment = user attr) | `resource.path[0] = subject.attr.tenant_code` |
671
+ | Numeric level | `subject.attr.level >= 5` |
672
+ | Project membership (second segment in array attr) | `resource.path[1] in subject.attr.projects` |
673
+ | Boolean feature flag | `subject.attr.beta = true` |
674
+ | Compound + branch (also use `conditions.branches` when needed) | `subject.email ends_with '@company.com' \| (subject.email ends_with '@partner.com' & env.branch = 'stg')` |
675
+
676
+ Dry-run: **`POST /api/v1/security/check`** with `subject`, `identifier`, `action`, optional `meta_path`, `branch`.
677
+
438
678
  ---
439
679
 
440
680
  # Branches
@@ -569,27 +809,111 @@ Returns lock info. **`409`** if already locked.
569
809
 
570
810
  **Base path:** `/api/v1/triggers`
571
811
 
812
+ Definitions are stored as JSON under **`/_xcitedb/triggers`**. After a matching event, the server runs **`action.unquery`** over documents selected by **`action.query`**, then writes the resulting JSON to **`action.meta_path`** on **`action.target_identifier`** (elevated privileges inside the same transaction). Nested trigger evaluation is blocked (no infinite loops).
813
+
572
814
  ## Create or update trigger
573
815
 
574
816
  **`POST /api/v1/triggers`**
575
817
 
576
818
  ```json
577
819
  {
578
- "trigger_id": "on_status_review",
820
+ "trigger_id": "on_project_status_meta",
579
821
  "trigger": {
580
- "match": { "meta_key": "status", "meta_value": "review" },
581
- "action": { "type": "webhook", "url": "https://hooks.example.com/xcite" }
822
+ "enabled": true,
823
+ "event": "meta_changed",
824
+ "match": {
825
+ "identifiers": [{ "match_start": "/projects/" }],
826
+ "match_meta_path": "status",
827
+ "match_operation": "set"
828
+ },
829
+ "conditions": {
830
+ "branches": ["*", "main"],
831
+ "expression": "trigger.meta_path = \"status\""
832
+ },
833
+ "action": {
834
+ "query": { "match": "$trigger_identifier" },
835
+ "unquery": { "id": "$identifier", "status": "status" },
836
+ "target_identifier": "/indexes/project_status",
837
+ "meta_path": "entries",
838
+ "mode": "append"
839
+ }
840
+ }
841
+ }
842
+ ```
843
+
844
+ ### Trigger fields
845
+
846
+ | Field | Required | Description |
847
+ |--------|----------|-------------|
848
+ | `enabled` | No (default true) | If false, trigger is skipped. |
849
+ | `event` | Yes | `meta_changed`, `document_written`, or `document_deleted`. |
850
+ | `match` | Yes | Must include **`identifiers`**: non-empty array of identifier patterns (`exact`, `match_start`, `match_end`, `contains`, `regex`). Optional **`match_meta_path`**: exact path or prefix ending with `*`. Optional **`match_operation`**: `set`, `append`, or `delete` (meta / delete ops). |
851
+ | `conditions` | No | Optional **`branches`** (same as policies) and **`expression`** (see below). |
852
+ | `action` | Yes | **`query`** (`XCiteQuery`), **`unquery`** (Unquery DSL template), **`target_identifier`** (literal or `"$trigger_identifier"`), **`meta_path`**, optional **`mode`**: `set` (default) or `append`. |
853
+
854
+ ### Expression context for **triggers** (`conditions.expression`)
855
+
856
+ Not the same as policies: there is **no `subject`**. Context object:
857
+
858
+ - **`trigger`**: `{ "event", "meta_path", "operation" }` (`operation`: `set` / `append` / `delete`)
859
+ - **`value`**: JSON written at `meta_path` for `meta_changed`, or null
860
+ - **`resource`**: `{ "identifier", "path" }` for the firing identifier
861
+ - **`env`**: `{ "branch" }`
862
+
863
+ ### Unquery variables injected for triggers
864
+
865
+ String template vars: **`$trigger_identifier`**, **`$trigger_meta_path`**, **`$trigger_operation`**. JSON var: **`$trigger_value`** (the written value when applicable).
866
+
867
+ ### Recipes
868
+
869
+ **Meta change → append row to an index document**
870
+
871
+ ```json
872
+ {
873
+ "trigger_id": "meta_index",
874
+ "trigger": {
875
+ "event": "meta_changed",
876
+ "match": {
877
+ "identifiers": [{ "match_start": "/docs/" }],
878
+ "match_meta_path": "reviewed"
879
+ },
880
+ "action": {
881
+ "query": { "match": "$trigger_identifier" },
882
+ "unquery": { "doc": "$trigger_identifier", "at": "$trigger_meta_path" },
883
+ "target_identifier": "/indexes/reviews",
884
+ "meta_path": "log",
885
+ "mode": "append"
886
+ }
887
+ }
888
+ }
889
+ ```
890
+
891
+ **Document written → write stats meta on the same identifier**
892
+
893
+ ```json
894
+ {
895
+ "trigger_id": "article_stats",
896
+ "trigger": {
897
+ "event": "document_written",
898
+ "match": { "identifiers": [{ "match_start": "/articles/" }] },
899
+ "action": {
900
+ "query": { "match": "$trigger_identifier" },
901
+ "unquery": { "id": "$identifier", "nodes": "$count" },
902
+ "target_identifier": "$trigger_identifier",
903
+ "meta_path": "stats",
904
+ "mode": "set"
905
+ }
582
906
  }
583
907
  }
584
908
  ```
585
909
 
586
910
  ## List triggers
587
911
 
588
- **`GET /api/v1/triggers`** — Returns map of `{ trigger_id: trigger_definition }`.
912
+ **`GET /api/v1/triggers`** — Returns map of `{ trigger_id: trigger_definition }`. Optional **`?name=`** returns one trigger as `{ trigger_id, trigger }`.
589
913
 
590
914
  ## Delete trigger
591
915
 
592
- **`DELETE /api/v1/triggers?name=on_status_review`**
916
+ **`DELETE /api/v1/triggers?name=on_project_status_meta`**
593
917
 
594
918
  ---
595
919
 
@@ -609,7 +933,7 @@ Returns lock info. **`409`** if already locked.
609
933
  "priority": 10,
610
934
  "subjects": { "type": "app_user", "groups": ["viewers"] },
611
935
  "resources": { "identifiers": [{ "match_start": "/admin/" }] },
612
- "actions": ["document:write", "document:delete"],
936
+ "actions": ["write", "delete", "meta:write"],
613
937
  "conditions": {}
614
938
  }
615
939
  }
@@ -630,11 +954,11 @@ Returns lock info. **`409`** if already locked.
630
954
  {
631
955
  "subject": { "type": "app_user", "groups": ["editors"] },
632
956
  "identifier": "/admin/config",
633
- "action": "document:write"
957
+ "action": "write"
634
958
  }
635
959
  ```
636
960
 
637
- Returns `{ effect: "allow"|"deny", matched_policy_id? }`.
961
+ Returns `{ effect: "allow"|"deny", matched_policy_id?, expression_context? }`.
638
962
 
639
963
  ## Security config
640
964
 
@@ -1084,6 +1408,9 @@ interface DatabaseContext {
1084
1408
  - `deleteTrigger(name)` → `void`
1085
1409
 
1086
1410
  ### App User Auth
1411
+
1412
+ **App-user auth in test sessions.** The full app-user lifecycle works inside a test session: `registerAppUser` → `loginAppUser` → `appUserMe` / `updateAppUserProfile` / etc. App user records are stored in the test session’s isolated LMDB and cleaned up with the session. No `X-Test-Auth: required` is needed for this flow — the default bypass mode recognizes app-user tokens while still skipping developer auth.
1413
+
1087
1414
  - `registerAppUser(email, password, displayName?, attributes?)` → `AppUser` (groups assigned from server config)
1088
1415
  - `loginAppUser(email, password)` → `AppUserTokenPair`
1089
1416
  - `refreshAppUser()` → `AppUserTokenPair`
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 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).
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).
26
26
 
27
27
  ## Common Pitfalls
28
28
 
@@ -38,11 +38,15 @@ These are the most common sources of confusion for developers and AI assistants:
38
38
 
39
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
40
 
41
+ 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.
42
+
41
43
  ## Test mode (how to use)
42
44
 
45
+ > **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.
46
+
43
47
  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
48
  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.
49
+ 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.
46
50
  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
51
  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
52
 
@@ -206,6 +210,26 @@ interface XCiteDBClientOptions {
206
210
  **Triggers:**
207
211
  - `upsertTrigger(id, trigger)` / `listTriggers()` / `deleteTrigger(name)` — Automation triggers
208
212
 
213
+ ### Advanced: policy expressions (ABAC)
214
+
215
+ - **Actions** (use in `policy.actions`): `read`, `write`, `delete`, `list`, `meta:read`, `meta:write`, `unquery`.
216
+ - **`conditions.expression`** uses the same predicate syntax as Unquery `?` filters. **Context:** `subject.id`, `subject.email`, `subject.role`, `subject.groups`, `subject.attr.*` (app-user JSON attributes), `resource.identifier`, `resource.path` (array of path segments), `env.branch`.
217
+ - **Operators:** `=`, `!=`, `>=`, `<=`, `>`, `<`, `in`, `contains`, `starts_with`, `ends_with`, `&`, `|`, `!`, `+` (string concat), postfix `!` (exists).
218
+ - **Examples:** `resource.path[0] = subject.attr.tenant_code` — tenant isolation; `subject.attr.level >= 5` — numeric attribute gate.
219
+
220
+ ### Advanced: triggers (stored under `/_xcitedb/triggers`)
221
+
222
+ - **Events:** `meta_changed`, `document_written`, `document_deleted`.
223
+ - **`match`:** required non-empty **`identifiers`** (same pattern objects as policy `resources.identifiers`); optional **`match_meta_path`** (exact or `prefix*`); optional **`match_operation`:** `set` | `append` | `delete`.
224
+ - **`action`:** **`query`** (document query), **`unquery`** (Unquery template), **`target_identifier`** (or `"$trigger_identifier"`), **`meta_path`**, **`mode`:** `set` | `append`.
225
+ - **Unquery vars:** `$trigger_identifier`, `$trigger_meta_path`, `$trigger_operation`, `$trigger_value`. **Trigger `conditions.expression` context:** `trigger.{event,meta_path,operation}`, `value`, `resource`, `env.branch` (no `subject`).
226
+
227
+ ### Advanced: Unquery DSL (`unquery(query, unqueryDoc)`)
228
+
229
+ - JSON **keys** = output fields; **string values** = expressions referencing meta/XML (e.g. field name `"title"` reads meta key `title`).
230
+ - **Common builtins:** `$count`, `$sum(expr)`, `$avg(expr)`, `$min(expr)`, `$max(expr)`, `$xpath("…")`, `$identifier`, `$size(expr)`, `$length(expr)`, `$if(cond, a, b)`, `$var(name)` / `%name`.
231
+ - **Example:** `{ "query": { "match_start": "/orders/" }, "unquery": { "n": "$count" } }` — count documents. Full grammar: **`llms-full.txt`** in this package.
232
+
209
233
  **App Users (admin):**
210
234
  - `listAppUsers()` / `createAppUser()` / `deleteAppUser()` — Manage end-user accounts
211
235
  - `registerAppUser(email, password, displayName?, attributes?)` — Self-registration (groups assigned from server config)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",