@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 +141 -2
- package/dist/client.js +139 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +38 -2
- package/llms-full.txt +337 -10
- package/llms.txt +26 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
/**
|
|
390
|
-
export
|
|
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,
|
|
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:**
|
|
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": "
|
|
820
|
+
"trigger_id": "on_project_status_meta",
|
|
579
821
|
"trigger": {
|
|
580
|
-
"
|
|
581
|
-
"
|
|
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=
|
|
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": ["
|
|
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": "
|
|
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
|
|
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:**
|
|
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)
|