@xcitedbs/client 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -125,6 +125,58 @@ export declare class XCiteDBClient {
125
125
  destroyTestSessionByToken(token: string): Promise<{
126
126
  message: string;
127
127
  }>;
128
+ /**
129
+ * Create a long-lived developer sandbox via `POST /api/v1/sandboxes`. Returns the server's
130
+ * `SandboxInfo`. To start using the sandbox immediately, follow up with `client.useSandbox(name)`,
131
+ * or mint a sandbox-bound API key with `mintSandboxApiKey` and route subsequent requests through
132
+ * a fresh client constructed with that key (no header threading needed).
133
+ */
134
+ createSandbox(opts: import('./types').CreateSandboxOptions): Promise<import('./types').SandboxInfo>;
135
+ /** List sandboxes for the current project (`GET /api/v1/sandboxes`). */
136
+ listSandboxes(): Promise<import('./types').SandboxInfo[]>;
137
+ /** Fetch a sandbox's detail by name (`GET /api/v1/sandboxes/{name}`). */
138
+ getSandbox(name: string): Promise<import('./types').SandboxInfo>;
139
+ /** Update mutable sandbox fields (`PATCH /api/v1/sandboxes/{name}`). */
140
+ updateSandbox(name: string, patch: Partial<Pick<import('./types').SandboxInfo, 'description' | 'pinned' | 'expires_at' | 'effects_policy'>>): Promise<import('./types').SandboxInfo>;
141
+ /** Drop overlay writes for a sandbox while preserving membership and bound keys. */
142
+ resetSandbox(name: string): Promise<{
143
+ message: string;
144
+ session_token: string;
145
+ }>;
146
+ /** Destroy a sandbox; revokes bound API keys and removes membership rows. Owner only. */
147
+ destroySandbox(name: string): Promise<{
148
+ message: string;
149
+ session_token: string;
150
+ }>;
151
+ listSandboxMembers(name: string): Promise<import('./types').SandboxMember[]>;
152
+ addSandboxMember(name: string, member_id: string, role: 'editor' | 'viewer'): Promise<{
153
+ message: string;
154
+ }>;
155
+ removeSandboxMember(name: string, member_id: string): Promise<{
156
+ message: string;
157
+ }>;
158
+ /**
159
+ * Mint an API key bound to the given sandbox. Requests presenting this key automatically route
160
+ * into the sandbox without an `X-Test-Session` header — paste it into your dev `.env` and
161
+ * existing client/curl/SDK calls just work. The `api_key` field is shown once; persist it now.
162
+ */
163
+ mintSandboxApiKey(name: string, opts?: {
164
+ name?: string;
165
+ role?: 'admin' | 'editor' | 'viewer';
166
+ key_type?: 'secret' | 'public';
167
+ expires_at?: number;
168
+ }): Promise<import('./types').SandboxApiKeyMintResult>;
169
+ /**
170
+ * Pin this client to a named sandbox: subsequent requests carry `X-Test-Session: <token>`.
171
+ * Returns the sandbox info. Throws if the name does not resolve.
172
+ *
173
+ * If you instead want zero-header DX, mint a sandbox-bound API key with
174
+ * `mintSandboxApiKey(name)` and construct a fresh client with that `apiKey` — the server
175
+ * pre-filter resolves the binding without touching the test-session header.
176
+ */
177
+ useSandbox(name: string): Promise<import('./types').SandboxInfo>;
178
+ /** Clear any pinned test/sandbox session token from this client (subsequent requests hit production). */
179
+ clearSandbox(): void;
128
180
  /** True if this client would send API key or Bearer credentials on a normal request. */
129
181
  private sentAuthCredentials;
130
182
  /** 401 on these paths is an expected auth flow outcome, not a dead session. */
package/dist/client.js CHANGED
@@ -699,6 +699,73 @@ class XCiteDBClient {
699
699
  const enc = encodeURIComponent(token);
700
700
  return this.request('DELETE', `/api/v1/test/sessions/${enc}`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
701
701
  }
702
+ /**
703
+ * Create a long-lived developer sandbox via `POST /api/v1/sandboxes`. Returns the server's
704
+ * `SandboxInfo`. To start using the sandbox immediately, follow up with `client.useSandbox(name)`,
705
+ * or mint a sandbox-bound API key with `mintSandboxApiKey` and route subsequent requests through
706
+ * a fresh client constructed with that key (no header threading needed).
707
+ */
708
+ async createSandbox(opts) {
709
+ return this.request('POST', '/api/v1/sandboxes', opts, undefined, { suppressTestSessionHeader: true, no401Retry: true });
710
+ }
711
+ /** List sandboxes for the current project (`GET /api/v1/sandboxes`). */
712
+ async listSandboxes() {
713
+ const r = await this.request('GET', '/api/v1/sandboxes', undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
714
+ return r.sandboxes ?? [];
715
+ }
716
+ /** Fetch a sandbox's detail by name (`GET /api/v1/sandboxes/{name}`). */
717
+ async getSandbox(name) {
718
+ return this.request('GET', `/api/v1/sandboxes/${encodeURIComponent(name)}`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
719
+ }
720
+ /** Update mutable sandbox fields (`PATCH /api/v1/sandboxes/{name}`). */
721
+ async updateSandbox(name, patch) {
722
+ return this.request('PATCH', `/api/v1/sandboxes/${encodeURIComponent(name)}`, patch, undefined, { suppressTestSessionHeader: true, no401Retry: true });
723
+ }
724
+ /** Drop overlay writes for a sandbox while preserving membership and bound keys. */
725
+ async resetSandbox(name) {
726
+ return this.request('POST', `/api/v1/sandboxes/${encodeURIComponent(name)}/reset`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
727
+ }
728
+ /** Destroy a sandbox; revokes bound API keys and removes membership rows. Owner only. */
729
+ async destroySandbox(name) {
730
+ return this.request('DELETE', `/api/v1/sandboxes/${encodeURIComponent(name)}`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
731
+ }
732
+ async listSandboxMembers(name) {
733
+ const r = await this.request('GET', `/api/v1/sandboxes/${encodeURIComponent(name)}/members`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
734
+ return r.members ?? [];
735
+ }
736
+ async addSandboxMember(name, member_id, role) {
737
+ return this.request('POST', `/api/v1/sandboxes/${encodeURIComponent(name)}/members`, { member_id, role }, undefined, { suppressTestSessionHeader: true, no401Retry: true });
738
+ }
739
+ async removeSandboxMember(name, member_id) {
740
+ return this.request('DELETE', `/api/v1/sandboxes/${encodeURIComponent(name)}/members/${encodeURIComponent(member_id)}`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
741
+ }
742
+ /**
743
+ * Mint an API key bound to the given sandbox. Requests presenting this key automatically route
744
+ * into the sandbox without an `X-Test-Session` header — paste it into your dev `.env` and
745
+ * existing client/curl/SDK calls just work. The `api_key` field is shown once; persist it now.
746
+ */
747
+ async mintSandboxApiKey(name, opts = {}) {
748
+ return this.request('POST', `/api/v1/sandboxes/${encodeURIComponent(name)}/api-keys`, opts, undefined, { suppressTestSessionHeader: true, no401Retry: true });
749
+ }
750
+ /**
751
+ * Pin this client to a named sandbox: subsequent requests carry `X-Test-Session: <token>`.
752
+ * Returns the sandbox info. Throws if the name does not resolve.
753
+ *
754
+ * If you instead want zero-header DX, mint a sandbox-bound API key with
755
+ * `mintSandboxApiKey(name)` and construct a fresh client with that `apiKey` — the server
756
+ * pre-filter resolves the binding without touching the test-session header.
757
+ */
758
+ async useSandbox(name) {
759
+ const info = await this.getSandbox(name);
760
+ if (!info.session_token)
761
+ throw new Error(`useSandbox: server returned no session_token for "${name}"`);
762
+ this.testSessionToken = info.session_token;
763
+ return info;
764
+ }
765
+ /** Clear any pinned test/sandbox session token from this client (subsequent requests hit production). */
766
+ clearSandbox() {
767
+ this.testSessionToken = undefined;
768
+ }
702
769
  /** True if this client would send API key or Bearer credentials on a normal request. */
703
770
  sentAuthCredentials() {
704
771
  return !!(this.apiKey || this.accessToken || this.appUserAccessToken);
@@ -2497,8 +2564,8 @@ class XCiteDBClient {
2497
2564
  }
2498
2565
  const ct = opts.contentType.trim() || 'application/octet-stream';
2499
2566
  const qParts = [];
2500
- if (opts.scope === 'public') {
2501
- qParts.push('scope=public');
2567
+ if (opts.scope === 'shared') {
2568
+ qParts.push('scope=shared');
2502
2569
  }
2503
2570
  if (opts.identifier !== undefined && opts.identifier !== '') {
2504
2571
  qParts.push(`identifier=${encodeURIComponent(opts.identifier)}`);
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { XCiteDBClient } from './client';
2
2
  export { parseAssetUri, formatAssetUri, collectIdentifiersFromText, ASSET_URI_PREFIX } from './assetUri';
3
3
  export { WebSocketSubscription } from './websocket';
4
- export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, SmartDiffRef, SmartDiffResult, SmartDiffStats, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListItem, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TriggerEvent, TriggerEventsResponse, TxnOperation, TxnOperationResult, TxnPrecondition, TxnPreconditionResult, TxnRequest, TxnResponse, JwksKey, JwksResponse, VerifyAppUserTokenOptions, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
4
+ export type { AccessCheckResult, ApiKeyInfo, AppAuthConfig, AppEmailConfig, AppEmailSmtpConfig, AppEmailTemplateEntry, AppEmailTemplates, AppEmailWebhookConfig, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BookmarkRecord, BranchInfo, BranchListItem, CheckpointRecord, CommitRecord, CompareEntry, CompareRef, CompareResult, DatabaseContext, DiffEntry, DiffRef, DiffResult, SmartDiffRef, SmartDiffResult, SmartDiffStats, DocumentBatchResponse, DocumentBatchResultRow, DocumentExportFormat, DocumentImportFormat, ExportDocumentResult, Flags, ImportDocumentOptions, ImportDocumentResult, JsonDocumentData, JsonDocumentBatchItem, IdentifierChildNode, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LockConflictBody, LockExpiredBody, LockUnknownBody, MergeConflict, MergeResult, OAuthProviderInfo, OAuthProvidersResponse, OwnedTenantInfo, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspaceOrg, PlatformWorkspacesResponse, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListItem, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkRecord, AssetMagicLinkResult, AssetShareListEntry, AssetShareListResponse, AssetShareRequest, AssetStorageImport, AssetStorageMount, AssetStorageTarget, AssetStorageTargetType, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, LogEntry, MetaValue, PlatformRegisterResult, PolicyUpdateResponse, PublishConflict, PublishResult, RebaseUserWorkspaceResult, PolicyConditions, PolicyIdentifierPattern, PolicyResources, PolicySubjectInput, PolicySubjects, RagQueryOptions, RagQueryResult, RagStreamEvent, RealtimeEvent, SandboxApiKeyMintResult, SandboxInfo, SandboxMember, SearchIndexingProgress, SecurityConfig, SecurityPolicy, StoredPolicyResponse, StoredTriggerResponse, SubscriptionOptions, TagRecord, TextSearchHit, TextSearchQuery, TextSearchResult, TriggerDefinition, TriggerEvent, TriggerEventsResponse, TxnOperation, TxnOperationResult, TxnPrecondition, TxnPreconditionResult, TxnRequest, TxnResponse, JwksKey, JwksResponse, VerifyAppUserTokenOptions, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateSandboxOptions, CreateTestSessionOptions, TestSessionBootstrap, TestSessionBootstrapSummary, TestSessionInfo, XCiteDBClientOptions, XCiteDBErrorExtras, XCiteDBJwtClaims, UnqueryResult, UnqueryTemplate, XCiteQuery, } from './types';
5
5
  export { XCiteDBError, XCiteDBForbiddenError, XCiteDBNotFoundError, XCiteDBAuthError, XCiteDBLockConflictError, } from './types';
package/dist/types.d.ts CHANGED
@@ -192,8 +192,10 @@ export interface UploadAssetOptions {
192
192
  contentType: string;
193
193
  /** When set (with default scope), uploaded to this path after user-isolation prefixing rules on the server. */
194
194
  identifier?: string;
195
- /** `public` writes under `/public/assets/…`. Default project/user scope when omitted. */
196
- scope?: 'project' | 'public';
195
+ /** `shared` writes under `/shared/assets/…` (tenant-common, readable by all logged-in app users).
196
+ * Default project/user scope when omitted. To publish anonymously, upload normally and use the
197
+ * share API with `target_user_id: "#anonymous"`. */
198
+ scope?: 'project' | 'shared';
197
199
  }
198
200
  /** Result of {@link XCiteDBClient.headAsset}. */
199
201
  export interface AssetHeadResult {
@@ -800,6 +802,53 @@ export interface CreateTestSessionOptions {
800
802
  userIsolation?: UserIsolationOptions;
801
803
  requestTimeoutMs?: number;
802
804
  }
805
+ /** A long-lived developer sandbox: named overlay on a project + branch. */
806
+ export interface SandboxInfo {
807
+ session_token: string;
808
+ name: string;
809
+ description: string;
810
+ org_id: string;
811
+ project_id: string;
812
+ base_branch: string;
813
+ pinned: boolean;
814
+ effects_policy: 'real' | 'log' | 'mock';
815
+ created_at: number;
816
+ last_used: number;
817
+ expires_at: number;
818
+ reset_generation: number;
819
+ owner_member_id: string;
820
+ /** Present in list responses: the requesting member's role in this sandbox, or "" if not a member. */
821
+ my_role?: 'owner' | 'editor' | 'viewer' | '';
822
+ }
823
+ /** Options for {@link XCiteDBClient.createSandbox}. */
824
+ export interface CreateSandboxOptions {
825
+ /** Required. Kebab-case, 1-64 chars, must start with a-z, no consecutive hyphens. */
826
+ name: string;
827
+ description?: string;
828
+ /** Pinned sandboxes are not garbage-collected by idle TTL. */
829
+ pinned?: boolean;
830
+ /** Unix seconds; 0 = no explicit expiry. */
831
+ expires_at?: number;
832
+ base_branch?: string;
833
+ /** Defaults to "log" — webhooks/auth-emails get captured instead of firing. */
834
+ effects_policy?: 'real' | 'log' | 'mock';
835
+ }
836
+ /** Response from {@link XCiteDBClient.mintSandboxApiKey}. The `api_key` value is shown once. */
837
+ export interface SandboxApiKeyMintResult {
838
+ api_key: string;
839
+ key_id: string;
840
+ prefix: string;
841
+ key_type: 'secret' | 'public';
842
+ bound_sandbox_id: string;
843
+ sandbox_name: string;
844
+ message: string;
845
+ }
846
+ export interface SandboxMember {
847
+ member_id: string;
848
+ role: 'owner' | 'editor' | 'viewer';
849
+ added_at: number;
850
+ added_by: string;
851
+ }
803
852
  /** Application user (tenant-scoped), distinct from developer users. */
804
853
  export interface AppUser {
805
854
  user_id: string;
@@ -978,7 +1027,17 @@ export interface PolicyUpdateResponse {
978
1027
  export interface TriggerMatch {
979
1028
  /** Non-empty array of identifier patterns (same shape as policy `resources.identifiers`). */
980
1029
  identifiers: PolicyIdentifierPattern[];
1030
+ /**
1031
+ * Single meta path pattern (exact, or `prefix*`). Mutually exclusive with `match_meta_paths`;
1032
+ * the server rejects writes that set both fields.
1033
+ */
981
1034
  match_meta_path?: string;
1035
+ /**
1036
+ * Array of meta path patterns (anyOf). Each entry is exact or `prefix*`. Useful for one
1037
+ * trigger gating multiple fields of the same shape (e.g. role-restricted profile fields).
1038
+ * Empty array means no constraint. Mutually exclusive with `match_meta_path`.
1039
+ */
1040
+ match_meta_paths?: string[];
982
1041
  match_operation?: 'set' | 'append' | 'delete';
983
1042
  }
984
1043
  /** `action` block: run unquery and write result to target meta. */
@@ -992,10 +1051,21 @@ export interface TriggerAction {
992
1051
  /** Stored trigger document under /_xcitedb/triggers. */
993
1052
  export interface TriggerDefinition {
994
1053
  enabled?: boolean;
1054
+ /**
1055
+ * `"after"` (default): run after the write commits, may run actions. `"before"`:
1056
+ * predicate-only — runs before the write applies and rejects with HTTP 422 when matched.
1057
+ * BEFORE triggers must not have `action` or `actions`.
1058
+ */
1059
+ phase?: 'before' | 'after';
995
1060
  event: 'meta_changed' | 'document_written' | 'document_deleted';
996
1061
  match: TriggerMatch;
997
1062
  conditions?: PolicyConditions;
998
- action: TriggerAction;
1063
+ /** AFTER-trigger single action (legacy). Mutually exclusive with `actions`. Forbidden on BEFORE triggers. */
1064
+ action?: TriggerAction;
1065
+ /** AFTER-trigger multi-action variant. Mutually exclusive with `action`. Forbidden on BEFORE triggers. */
1066
+ actions?: TriggerAction[];
1067
+ /** BEFORE-trigger reject message surfaced to clients on HTTP 422. */
1068
+ reject_reason?: string;
999
1069
  }
1000
1070
  export interface StoredTriggerResponse {
1001
1071
  trigger_id: string;
package/llms-full.txt CHANGED
@@ -1157,18 +1157,55 @@ Definitions are stored as JSON under **`/_xcitedb/triggers`**. After a matching
1157
1157
  | Field | Required | Description |
1158
1158
  |--------|----------|-------------|
1159
1159
  | `enabled` | No (default true) | If false, trigger is skipped. |
1160
+ | `phase` | No (default `"after"`) | `"after"` runs post-write actions; `"before"` is predicate-only and rejects the write with HTTP 422 when matched. BEFORE triggers must not have `action` or `actions`. |
1160
1161
  | `event` | Yes | `meta_changed`, `document_written`, or `document_deleted`. |
1161
- | `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). |
1162
+ | `match` | Yes | Must include **`identifiers`**: non-empty array of identifier patterns (`exact`, `match_start`, `match_end`, `contains`, `regex`). Optional **`match_meta_path`** (single exact path or `prefix*`) **or** **`match_meta_paths`** (array of patterns, anyOf — useful for one trigger gating multiple fields; mutually exclusive with `match_meta_path`). Optional **`match_operation`**: `set`, `append`, or `delete`. |
1162
1163
  | `conditions` | No | Optional **`branches`** (same as policies) and **`expression`** (see below). |
1163
- | `action` | Yes | **`query`** (`XCiteQuery`), **`unquery`** (Unquery DSL template), **`target_identifier`** (literal or `"$trigger_identifier"`), **`meta_path`**, optional **`mode`**: `set` (default), `append` (strict — unquery output must be a JSON array, stored target must be an array; trigger logs and skips on misuse), or `merge_append` (deep — recurses into objects to extend nested arrays). |
1164
+ | `action` | AFTER only | **`query`** (`XCiteQuery`), **`unquery`** (Unquery DSL template), **`target_identifier`** (literal or `"$trigger_identifier"`), **`meta_path`**, optional **`mode`**: `set` (default), `append` (strict — unquery output must be a JSON array, stored target must be an array; trigger logs and skips on misuse), or `merge_append` (deep — recurses into objects to extend nested arrays). Mutually exclusive with `actions`. |
1165
+ | `actions` | AFTER only | Array of action objects for multi-op fan-out. Each `kind` is one of `WriteMeta`, `AppendMeta`, `WriteJson`, `DeleteIdentifier`, `AddIdentifier`, `ClearMetaPath`. Mutually exclusive with `action`. |
1166
+ | `reject_reason` | BEFORE only | Message returned to clients on HTTP 422 when the trigger matches. |
1164
1167
 
1165
- ### Expression context for **triggers** (`conditions.expression`)
1168
+ ### BEFORE triggers (predicate-only)
1169
+
1170
+ `phase: "before"` triggers run *before* the write applies and reject it with **HTTP 422** when the match and conditions both hold. They are predicate-only — no `action` / `actions`. Fired only on writes routed through **`/api/v1/txn`** (regular `/api/v1/meta` and `/api/v1/json-documents` writes do not invoke BEFORE triggers).
1171
+
1172
+ The expression sees **`prior`** alongside `value`:
1173
+
1174
+ - **`prior`** (BEFORE + `meta_changed` only) — value at the same `meta_path` before the write. Bound only when at least one BEFORE trigger's expression references `prior`. JSON `null` when the path is currently absent.
1175
+
1176
+ Common patterns (the expression grammar uses `=` / `!=` / `<` / `>`, and single `&` / `|` for boolean):
1177
+
1178
+ - **Immutability after first set**: `prior is_literal & value != prior` — rejects only when a literal value already exists and differs.
1179
+ - **Monotonic non-decreasing**: `prior is_literal & value < prior`.
1180
+ - **Role-gated field**: `subject.role != 'system'` (no `prior` needed).
1181
+
1182
+ Example (`song.id` immutable once set):
1183
+
1184
+ ```json
1185
+ {
1186
+ "trigger_id": "song_id_immutable",
1187
+ "trigger": {
1188
+ "phase": "before",
1189
+ "event": "meta_changed",
1190
+ "match": {
1191
+ "identifiers": [{ "match_start": "/spaces/" }],
1192
+ "match_meta_path": "id"
1193
+ },
1194
+ "conditions": {
1195
+ "expression": "prior is_literal & value != prior"
1196
+ },
1197
+ "reject_reason": "song id is immutable once set"
1198
+ }
1199
+ }
1200
+ ```
1166
1201
 
1167
- Not the same as policies: there is **no `subject`**. Context object:
1202
+ ### Expression context for **triggers** (`conditions.expression`)
1168
1203
 
1169
1204
  - **`trigger`**: `{ "event", "meta_path", "operation" }` (`operation`: `set` / `append` / `delete`)
1170
1205
  - **`value`**: JSON written at `meta_path` for `meta_changed`, or null
1206
+ - **`prior`** (BEFORE + `meta_changed` only): value at `meta_path` before the write, or `null` if absent
1171
1207
  - **`resource`**: `{ "identifier", "path" }` for the firing identifier
1208
+ - **`subject`**: `{ "id", "user_id", "email", "type", "role", "groups", "attr" }` (BEFORE only — AFTER triggers historically have no subject)
1172
1209
  - **`env`**: `{ "branch" }`
1173
1210
 
1174
1211
  ### Unquery variables injected for triggers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",