@xcitedbs/client 0.3.10 → 0.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,85 @@ Call **`POST /api/v1/test/sessions`** with your normal API key or Bearer (same p
67
67
 
68
68
  Tear down with **`destroyTestSession()`** (or **`DELETE /api/v1/test/sessions/current`** with the session header). With the **same** API key / Bearer and **no** `X-Test-Session`, use **`listTestSessions()`**, **`destroyAllTestSessions()`**, and **`destroyTestSessionByToken(token)`** to clear leaked sessions before large suites (avoids the default per-credential concurrent cap). See **`llms.txt`** / **`llms-full.txt`** in this package for full behavior, limits, and auth notes.
69
69
 
70
+ ## Gotchas and conventions
71
+
72
+ These trip up most first-time users; they're listed once here so you don't hit them twice.
73
+
74
+ ### Search (`search`) tokenizes on hyphens
75
+
76
+ The full-text index treats `-` as a token boundary, so `client.search({ query: 'Test - Article' })` won't match a document titled exactly `"Test - Article"` — it tokenizes to `Test`, `Article` and ranks any doc containing both. Use `search` only for relevance queries.
77
+
78
+ For exact-title or exact-meta lookups, use **`unquery`** with a meta-equality filter:
79
+
80
+ ```ts
81
+ client.unquery(
82
+ { match_start: '/spaces/<u>/docs/' },
83
+ [{ title: 'title', match: '$equals(title, $string("Test - Article"))' }]
84
+ );
85
+ ```
86
+
87
+ ### `list*` methods return wrapper objects, not bare arrays
88
+
89
+ For consistency, every `list*` family method returns an object with the data nested under a named key:
90
+
91
+ | Method | Wrapper shape |
92
+ |---|---|
93
+ | `listIdentifiers` | `{ identifiers: string[], total, offset, limit }` |
94
+ | `listCheckpoints` | `{ checkpoints: CheckpointRecord[], total, branch }` |
95
+ | `listUserWorkspaces` | `{ user_workspaces: [...] }` |
96
+ | `listBookmarks` | `{ bookmarks: [...], total }` |
97
+
98
+ Don't `for…of` the result directly — use `for (const x of ids.identifiers)`, not `for (const x of ids)`.
99
+
100
+ ### Reading meta — `getMeta` and `queryMeta`
101
+
102
+ The read sibling of `addMeta` / `appendMeta` / `clearMeta` is **`getMeta`** (alias of `queryMeta`). With an empty `path` it returns the whole top-level meta object, which by convention is keyed by tool/flow:
103
+
104
+ ```ts
105
+ const meta = await client.getMeta(docId); // { lint: {...}, summary: {...}, ... }
106
+ const lint = await client.getMeta(docId, 'lint'); // just the lint sub-object
107
+ ```
108
+
109
+ For an array-typed or keyed-object meta path (e.g. an items list, or a per-tool dictionary), pair with **`appendItem`** to push and **`removeItem`** to atomically delete by key — `removeItem` matches against `id` / `item_id` (or by string equality for primitive arrays) and writes back inside one server-side write txn, avoiding the read/modify/write race that read-then-set on the client would create.
110
+
111
+ ### Overlay test sessions — visibility matrix
112
+
113
+ `createTestSession({ overlay: true })` lets you read production data and write into an isolated overlay. The visibility model is **not "read-through everywhere"** — these are the layers that participate in the overlay→base fallback:
114
+
115
+ | API | Sees prod data? |
116
+ |---|---|
117
+ | Document and meta reads (anywhere in the doc store) | **yes** |
118
+ | Branch table existence checks (e.g. `_uw/<owner>/<slug>`) | **yes** |
119
+ | Per-branch identifier scans on base-only branches (`listIdentifiers`, `compare`) | **yes** |
120
+ | User-workspace records (`listUserWorkspaces`, `getUserWorkspace`) | **yes** |
121
+ | Branch tip / commit lookups (`listCheckpoints`, `compare` against checkpoints) | **yes** |
122
+ | `publishUserWorkspace` writes | sandboxed in overlay — does **not** touch prod |
123
+ | `createCheckpoint`, `addMeta`, `addDocument`, etc. | sandboxed in overlay |
124
+
125
+ Anything you write inside an overlay session lives under `_test/<uuid>/` and is dropped when the session is destroyed. To inspect a real production conflict mutation-free, call the production admin client directly with `{ autoResolve: 'none' }`.
126
+
127
+ ### `compare()` ref shapes
128
+
129
+ `{ branch }` without `date` or `checkpoint_id` resolves to the branch's **live tip**, not its base. On a branch with zero checkpoints the server returns 400 `branch_has_no_commits` rather than producing an ambiguous diff — pass an explicit `date` or `checkpoint_id` instead.
130
+
131
+ `compare()` accepts **both** `matchStart` (camelCase, our convention) and `match_start` (snake_case, matching `XCiteQuery`) on the options object; they're equivalent.
132
+
133
+ ### `withContext` / `fork`
134
+
135
+ `fork(partial)` returns a child client with the merged context (workspace, date, prefix). **`withContext(partial)`** is a one-line alias for the same operation.
136
+
137
+ ### `_xcite_json_doc` sentinel
138
+
139
+ JSON documents written via `writeJsonDocument` no longer leak the internal `_xcite_json_doc: true` marker into the read payload — the server strips it before serializing. Older clients that filtered it out can drop that workaround. Likewise, `deleteDocument` now correctly removes JSON documents (it routes JSON ids through `deleteJSON` automatically); you don't need to pick `deleteJsonDocument` based on the document's storage shape.
140
+
141
+ ### `testAuth: 'bypass'` requires the creator credential
142
+
143
+ When you create a test session, the issuing API key/Bearer is the **creator credential**. Calling endpoints under `X-Test-Auth: bypass` requires presenting that same credential alongside `X-Test-Session`; it isn't only a login restriction. If you can't present the creator credential (e.g. you're operating from a different identity), switch to `X-Test-Auth: required` and the session itself authenticates the request.
144
+
145
+ ### BFF/SPA-side concerns (tracked separately)
146
+
147
+ The following are app/BFF-repo concerns rather than SDK or server bugs: `getOrCreateDraft` check-then-act races (TOCTOU on per-`(user, key)` resource minting); coexisting legacy and modern `last-drafts` storage formats with different identifier encodings; and project-level workspace wiring of `@xcitedbs/client` for top-level diagnostic scripts. They're tracked in the BFF/SPA repo and aren't fixed here.
148
+
70
149
  ## Build
71
150
 
72
151
  ```bash
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, SmartDiffRef, SmartDiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, TriggerEventsResponse, StoredPolicyResponse, TxnRequest, TxnResponse, VerifyAppUserTokenOptions, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult, UserIsolationShareListResponse, UserIsolationUnshareParams, ShareDirection, AppUserGroup, CreateGroupParams, ListGroupsResponse } from './types';
1
+ import { AccessCheckResult, AppAuthConfig, AppEmailConfig, AppEmailTemplates, AppUser, AppUserTokenPair, EmailTestResponse, ForgotPasswordResponse, SendVerificationResponse, BranchInfo, BookmarkRecord, CheckpointRecord, CommitRecord, CompareRef, CompareResult, DatabaseContext, DiffRef, DiffResult, SmartDiffRef, SmartDiffResult, DocumentBatchResponse, DocumentExportFormat, ExportDocumentResult, Flags, JsonDocumentBatchItem, ImportDocumentOptions, ImportDocumentResult, ListIdentifierChildrenResult, ListIdentifiersResult, LockInfo, AcquireLockOptions, LogEntry, MergeResult, PublishResult, RebaseUserWorkspaceResult, WorkspaceInfo, MetaValue, PlatformRegisterResult, PolicySubjectInput, UnqueryResult, UnqueryTemplate, PolicyUpdateResponse, RealtimeEvent, SecurityConfig, SecurityPolicy, StoredTriggerResponse, TriggerDefinition, TriggerEventsResponse, StoredPolicyResponse, TxnRequest, TxnResponse, VerifyAppUserTokenOptions, SubscriptionOptions, TagRecord, TextSearchQuery, TextSearchResult, ProjectSearchSettings, ProjectSearchSettingsUpdate, ProjectDocConfResponse, AssetGcDryRunResult, AssetHeadResult, AssetListResponse, AssetMagicLinkListResponse, AssetMagicLinkResult, AssetShareListResponse, AssetShareRequest, AssetUnshareRequest, AssetUploadResult, CreateAssetMagicLinkRequest, ListAssetsOptions, ProjectAssetStorageConfig, UploadAssetOptions, PlatformDefaultDocConfResponse, VectorIndexEstimate, RagQueryOptions, RagQueryResult, RagStreamEvent, OAuthProvidersResponse, ProjectInfo, PlatformRegistrationConfig, PlatformWorkspacesResponse, TokenPair, UserInfo, ApiKeyInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateTestSessionOptions, XCiteDBClientOptions, XCiteDBJwtClaims, TestSessionBootstrapSummary, TestSessionInfo, XCiteQuery, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationShareResult, UserIsolationShareListResponse, UserIsolationUnshareParams, ShareDirection, AppUserGroup, CreateGroupParams, ListGroupsResponse, FunctionManifest, FunctionListItem, FunctionDeployRequest, FunctionSecretsList } from './types';
2
2
  import { WebSocketSubscription } from './websocket';
3
3
  export declare class XCiteDBClient {
4
4
  private baseUrl;
@@ -143,6 +143,22 @@ export declare class XCiteDBClient {
143
143
  message: string;
144
144
  session_token: string;
145
145
  }>;
146
+ /**
147
+ * Seal a *standalone* sandbox as a fixture base (`POST /api/v1/sandboxes/{name}/seal`).
148
+ *
149
+ * Stamps the supplied fingerprint on the fixture's metadata. Test sessions and dev sandboxes
150
+ * can then attach to this fixture by passing the matching `expectedBaseFingerprint`. Mismatch
151
+ * is `409 fingerprint_mismatch` — consumers MUST re-run setup, not retry.
152
+ */
153
+ sealSandbox(name: string, opts: import('./types').SealSandboxOptions): Promise<import('./types').SandboxInfo>;
154
+ /**
155
+ * Unseal a fixture sandbox (`POST /api/v1/sandboxes/{name}/unseal`).
156
+ *
157
+ * Clears the seal + fingerprint and bumps `reset_generation` so any cached overlay routing
158
+ * forces a fresh validation. Typical re-seed flow: `unseal → resetSandbox → re-populate →
159
+ * sealSandbox` at a new fingerprint.
160
+ */
161
+ unsealSandbox(name: string): Promise<import('./types').SandboxInfo>;
146
162
  /** Destroy a sandbox; revokes bound API keys and removes membership rows. Owner only. */
147
163
  destroySandbox(name: string): Promise<{
148
164
  message: string;
@@ -201,6 +217,8 @@ export declare class XCiteDBClient {
201
217
  * the `onSessionTokensUpdated` / `onAppUserTokensUpdated` callbacks supplied at construction.
202
218
  */
203
219
  fork(partial?: Partial<DatabaseContext>): XCiteDBClient;
220
+ /** Alias for {@link fork}. Both names are commonly guessed; this avoids a discoverability cliff. */
221
+ withContext(partial?: Partial<DatabaseContext>): XCiteDBClient;
204
222
  setTokens(access: string, refresh?: string): void;
205
223
  /** End-user (app) tokens. With developer `accessToken`/`apiKey`, sent as `X-App-User-Token`. */
206
224
  setAppUserTokens(access: string, refresh?: string): void;
@@ -438,6 +456,56 @@ export declare class XCiteDBClient {
438
456
  * `admin`/`editor` roles can read this; public-key requests are rejected.
439
457
  */
440
458
  listTriggerEvents(limit?: number): Promise<TriggerEventsResponse>;
459
+ /**
460
+ * List deployed functions for the current project. Returns metadata only
461
+ * (no wasm bytes). Requires admin or editor role on the project.
462
+ */
463
+ listFunctions(): Promise<FunctionListItem[]>;
464
+ /**
465
+ * Fetch a single function's manifest. Wasm bytes are omitted unless
466
+ * `includeWasm` is true (admin export path).
467
+ */
468
+ getFunction(name: string, opts?: {
469
+ includeWasm?: boolean;
470
+ }): Promise<FunctionManifest>;
471
+ /**
472
+ * Deploy / replace a function. Body MUST include `wasm_b64` (base64 wasm
473
+ * bytes) plus the manifest fields. Server validates the wasm magic and
474
+ * compiles it before persisting.
475
+ */
476
+ deployFunction(name: string, manifest: FunctionDeployRequest): Promise<FunctionManifest>;
477
+ deleteFunction(name: string): Promise<{
478
+ deleted: string;
479
+ }>;
480
+ invokeFunction<T = unknown>(name: string, body?: unknown): Promise<T>;
481
+ /**
482
+ * List the secret-name slots for a function. Each entry carries `set:
483
+ * true|false` — values are NEVER returned. The `orphan_secrets` field, if
484
+ * present, lists names that were once set but no longer appear in the
485
+ * manifest's `secrets[]` declaration; clean up via deleteFunctionSecret.
486
+ * Requires admin role.
487
+ */
488
+ listFunctionSecrets(name: string): Promise<FunctionSecretsList>;
489
+ /**
490
+ * Set one or more secret values. Each KEY must be declared in the
491
+ * manifest's `secrets[]` array; values are encrypted at rest and never
492
+ * round-trip on any GET. Requires admin role.
493
+ *
494
+ * @example
495
+ * ```ts
496
+ * await client.setFunctionSecrets('charge', {
497
+ * STRIPE_KEY: 'sk_live_...',
498
+ * WEBHOOK_SIGNING_KEY: 'whsec_...',
499
+ * });
500
+ * ```
501
+ */
502
+ setFunctionSecrets(name: string, values: Record<string, string>): Promise<{
503
+ function: string;
504
+ set: string[];
505
+ }>;
506
+ deleteFunctionSecret(name: string, key: string): Promise<{
507
+ deleted: string;
508
+ }>;
441
509
  /**
442
510
  * Dry-run access check (`POST /api/v1/security/check`). Returns `effect` and optional `matched_policy_id`.
443
511
  * Useful for debugging policies. Actions: `read`, `write`, `delete`, `list`, `unquery`.
@@ -657,12 +725,14 @@ export declare class XCiteDBClient {
657
725
  compare(from: CompareRef, to: CompareRef, options?: {
658
726
  includeContent?: boolean;
659
727
  matchStart?: string;
728
+ match_start?: string;
660
729
  }): Promise<CompareResult>;
661
730
  /** @deprecated Use {@link compare}. */
662
731
  diff(from: DiffRef, to: DiffRef, includeContent?: boolean): Promise<DiffResult>;
663
732
  diff(from: DiffRef, to: DiffRef, options?: {
664
733
  includeContent?: boolean;
665
734
  matchStart?: string;
735
+ match_start?: string;
666
736
  }): Promise<DiffResult>;
667
737
  /**
668
738
  * Compare two XML provision trees structurally + textually and write the resulting
@@ -873,6 +943,20 @@ export declare class XCiteDBClient {
873
943
  overwrite?: boolean;
874
944
  context?: Partial<DatabaseContext>;
875
945
  }): Promise<boolean>;
946
+ /**
947
+ * Atomic single-item removal — sibling to {@link appendItem}. Resolves to true when
948
+ * the item was found and removed, false (HTTP 404) when no match. Behavior depends on
949
+ * the meta shape at `path`:
950
+ * - object → drops the field named `itemKey` (siblings untouched).
951
+ * - array → drops entries equal to `itemKey` (string), or whose `id`/`item_id`
952
+ * matches `itemKey`.
953
+ * Read+filter+write happens server-side inside one write txn so concurrent removes
954
+ * on the same key elect exactly one winner — use this instead of read-then-set on the
955
+ * client to avoid the race-loser cleanup the convention warns about.
956
+ */
957
+ removeItem(identifier: string, itemKey: string, path?: string, opts?: {
958
+ context?: Partial<DatabaseContext>;
959
+ }): Promise<boolean>;
876
960
  /** Convenience: push a single item via query. Wraps `item` in `[item]` and calls {@link appendMetaByQuery}. */
877
961
  appendItemByQuery<T = unknown>(query: XCiteQuery, item: T, path?: string, firstMatch?: boolean, opts?: {
878
962
  overwrite?: boolean;
@@ -881,6 +965,14 @@ export declare class XCiteDBClient {
881
965
  queryMeta<T = MetaValue>(identifier: string, path?: string, opts?: {
882
966
  context?: Partial<DatabaseContext>;
883
967
  }): Promise<T>;
968
+ /**
969
+ * Alias for {@link queryMeta} that pairs by name with {@link addMeta} / {@link appendMeta} /
970
+ * {@link clearMeta}. With `path` empty this returns the whole top-level meta object — which
971
+ * by convention is keyed per tool/flow.
972
+ */
973
+ getMeta<T = MetaValue>(identifier: string, path?: string, opts?: {
974
+ context?: Partial<DatabaseContext>;
975
+ }): Promise<T>;
884
976
  queryMetaByQuery<T = MetaValue>(query: XCiteQuery, path?: string, opts?: {
885
977
  context?: Partial<DatabaseContext>;
886
978
  }): Promise<T>;
package/dist/client.js CHANGED
@@ -201,6 +201,11 @@ class XCiteDBClient {
201
201
  sessionBody.additional_keys = opts.additionalKeys;
202
202
  if (opts.bootstrap !== undefined)
203
203
  sessionBody.bootstrap = opts.bootstrap;
204
+ if (opts.baseSandboxName)
205
+ sessionBody.base_sandbox_name = opts.baseSandboxName;
206
+ if (opts.expectedBaseFingerprint) {
207
+ sessionBody.expected_base_fingerprint = opts.expectedBaseFingerprint;
208
+ }
204
209
  const data = await temp.request('POST', '/api/v1/test/sessions', Object.keys(sessionBody).length ? sessionBody : undefined, undefined, { no401Retry: true });
205
210
  const mode = resolveTestAuthMode(opts.testAuth, opts.testRequireAuth) ?? 'required';
206
211
  const keepCreds = mode !== 'bypass';
@@ -727,7 +732,18 @@ class XCiteDBClient {
727
732
  * a fresh client constructed with that key (no header threading needed).
728
733
  */
729
734
  async createSandbox(opts) {
730
- return this.request('POST', '/api/v1/sandboxes', opts, undefined, { suppressTestSessionHeader: true, no401Retry: true });
735
+ // Translate camelCase fixture-attach options to the snake_case wire format
736
+ // the server expects. Other fields pass through unchanged.
737
+ const body = { ...opts };
738
+ if (opts.baseSandboxName) {
739
+ body.base_sandbox_name = opts.baseSandboxName;
740
+ delete body.baseSandboxName;
741
+ }
742
+ if (opts.expectedBaseFingerprint) {
743
+ body.expected_base_fingerprint = opts.expectedBaseFingerprint;
744
+ delete body.expectedBaseFingerprint;
745
+ }
746
+ return this.request('POST', '/api/v1/sandboxes', body, undefined, { suppressTestSessionHeader: true, no401Retry: true });
731
747
  }
732
748
  /** List sandboxes for the current project (`GET /api/v1/sandboxes`). */
733
749
  async listSandboxes() {
@@ -746,6 +762,26 @@ class XCiteDBClient {
746
762
  async resetSandbox(name) {
747
763
  return this.request('POST', `/api/v1/sandboxes/${encodeURIComponent(name)}/reset`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
748
764
  }
765
+ /**
766
+ * Seal a *standalone* sandbox as a fixture base (`POST /api/v1/sandboxes/{name}/seal`).
767
+ *
768
+ * Stamps the supplied fingerprint on the fixture's metadata. Test sessions and dev sandboxes
769
+ * can then attach to this fixture by passing the matching `expectedBaseFingerprint`. Mismatch
770
+ * is `409 fingerprint_mismatch` — consumers MUST re-run setup, not retry.
771
+ */
772
+ async sealSandbox(name, opts) {
773
+ return this.request('POST', `/api/v1/sandboxes/${encodeURIComponent(name)}/seal`, { fingerprint: opts.fingerprint }, undefined, { suppressTestSessionHeader: true, no401Retry: true });
774
+ }
775
+ /**
776
+ * Unseal a fixture sandbox (`POST /api/v1/sandboxes/{name}/unseal`).
777
+ *
778
+ * Clears the seal + fingerprint and bumps `reset_generation` so any cached overlay routing
779
+ * forces a fresh validation. Typical re-seed flow: `unseal → resetSandbox → re-populate →
780
+ * sealSandbox` at a new fingerprint.
781
+ */
782
+ async unsealSandbox(name) {
783
+ return this.request('POST', `/api/v1/sandboxes/${encodeURIComponent(name)}/unseal`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
784
+ }
749
785
  /** Destroy a sandbox; revokes bound API keys and removes membership rows. Owner only. */
750
786
  async destroySandbox(name) {
751
787
  return this.request('DELETE', `/api/v1/sandboxes/${encodeURIComponent(name)}`, undefined, undefined, { suppressTestSessionHeader: true, no401Retry: true });
@@ -858,6 +894,10 @@ class XCiteDBClient {
858
894
  child.cachedAppUserId = undefined;
859
895
  return child;
860
896
  }
897
+ /** Alias for {@link fork}. Both names are commonly guessed; this avoids a discoverability cliff. */
898
+ withContext(partial) {
899
+ return this.fork(partial);
900
+ }
861
901
  setTokens(access, refresh) {
862
902
  this.accessToken = access;
863
903
  if (refresh !== undefined)
@@ -1570,6 +1610,72 @@ class XCiteDBClient {
1570
1610
  limit: typeof r?.limit === 'number' ? r.limit : 0,
1571
1611
  };
1572
1612
  }
1613
+ // ----- Functions / BFF (Phase 2 + 2.5 + 9) -----
1614
+ // Server-side WASM functions stored at /_xcitedb/functions/<name> per
1615
+ // tenant. Manifests carry runtime, identity_mode, limits, env (non-secret
1616
+ // config), egress_allowlist, secrets[] (capability declaration), and the
1617
+ // raw wasm bytes (base64). Secret VALUES live in a separate per-function
1618
+ // store and are never returned by any GET path — only set via PUT and
1619
+ // observed as `set: true|false` via list.
1620
+ /**
1621
+ * List deployed functions for the current project. Returns metadata only
1622
+ * (no wasm bytes). Requires admin or editor role on the project.
1623
+ */
1624
+ async listFunctions() {
1625
+ const r = await this.request('GET', '/api/v1/functions');
1626
+ return Array.isArray(r?.functions) ? r.functions : [];
1627
+ }
1628
+ /**
1629
+ * Fetch a single function's manifest. Wasm bytes are omitted unless
1630
+ * `includeWasm` is true (admin export path).
1631
+ */
1632
+ async getFunction(name, opts) {
1633
+ const q = buildQuery({ include_wasm: opts?.includeWasm ? 'true' : undefined });
1634
+ return this.request('GET', `/api/v1/functions/${encodeURIComponent(name)}${q}`);
1635
+ }
1636
+ /**
1637
+ * Deploy / replace a function. Body MUST include `wasm_b64` (base64 wasm
1638
+ * bytes) plus the manifest fields. Server validates the wasm magic and
1639
+ * compiles it before persisting.
1640
+ */
1641
+ async deployFunction(name, manifest) {
1642
+ return this.request('PUT', `/api/v1/functions/${encodeURIComponent(name)}`, manifest);
1643
+ }
1644
+ async deleteFunction(name) {
1645
+ return this.request('DELETE', `/api/v1/functions/${encodeURIComponent(name)}`);
1646
+ }
1647
+ async invokeFunction(name, body) {
1648
+ return this.request('POST', `/api/v1/functions/${encodeURIComponent(name)}/invoke`, body);
1649
+ }
1650
+ /**
1651
+ * List the secret-name slots for a function. Each entry carries `set:
1652
+ * true|false` — values are NEVER returned. The `orphan_secrets` field, if
1653
+ * present, lists names that were once set but no longer appear in the
1654
+ * manifest's `secrets[]` declaration; clean up via deleteFunctionSecret.
1655
+ * Requires admin role.
1656
+ */
1657
+ async listFunctionSecrets(name) {
1658
+ return this.request('GET', `/api/v1/functions/${encodeURIComponent(name)}/secrets`);
1659
+ }
1660
+ /**
1661
+ * Set one or more secret values. Each KEY must be declared in the
1662
+ * manifest's `secrets[]` array; values are encrypted at rest and never
1663
+ * round-trip on any GET. Requires admin role.
1664
+ *
1665
+ * @example
1666
+ * ```ts
1667
+ * await client.setFunctionSecrets('charge', {
1668
+ * STRIPE_KEY: 'sk_live_...',
1669
+ * WEBHOOK_SIGNING_KEY: 'whsec_...',
1670
+ * });
1671
+ * ```
1672
+ */
1673
+ async setFunctionSecrets(name, values) {
1674
+ return this.request('PUT', `/api/v1/functions/${encodeURIComponent(name)}/secrets`, values);
1675
+ }
1676
+ async deleteFunctionSecret(name, key) {
1677
+ return this.request('DELETE', `/api/v1/functions/${encodeURIComponent(name)}/secrets/${encodeURIComponent(key)}`);
1678
+ }
1573
1679
  /**
1574
1680
  * Dry-run access check (`POST /api/v1/security/check`). Returns `effect` and optional `matched_policy_id`.
1575
1681
  * Useful for debugging policies. Actions: `read`, `write`, `delete`, `list`, `unquery`.
@@ -1989,7 +2095,9 @@ class XCiteDBClient {
1989
2095
  }
1990
2096
  else if (third !== undefined && typeof third === 'object') {
1991
2097
  include_content = third.includeContent ?? false;
1992
- match_start = third.matchStart;
2098
+ // Accept both casings: matchStart (camelCase, our convention) and match_start
2099
+ // (snake_case, matching XCiteQuery and the wire field). camelCase wins on conflict.
2100
+ match_start = third.matchStart ?? third.match_start;
1993
2101
  }
1994
2102
  const body = {
1995
2103
  from,
@@ -2442,6 +2550,33 @@ class XCiteDBClient {
2442
2550
  async appendItem(identifier, item, path = '', opts) {
2443
2551
  return this.appendMeta(identifier, [item], path, opts);
2444
2552
  }
2553
+ /**
2554
+ * Atomic single-item removal — sibling to {@link appendItem}. Resolves to true when
2555
+ * the item was found and removed, false (HTTP 404) when no match. Behavior depends on
2556
+ * the meta shape at `path`:
2557
+ * - object → drops the field named `itemKey` (siblings untouched).
2558
+ * - array → drops entries equal to `itemKey` (string), or whose `id`/`item_id`
2559
+ * matches `itemKey`.
2560
+ * Read+filter+write happens server-side inside one write txn so concurrent removes
2561
+ * on the same key elect exactly one winner — use this instead of read-then-set on the
2562
+ * client to avoid the race-loser cleanup the convention warns about.
2563
+ */
2564
+ async removeItem(identifier, itemKey, path = '', opts) {
2565
+ const body = {
2566
+ identifier: this.isoPrefixId(identifier),
2567
+ item_key: itemKey,
2568
+ path,
2569
+ };
2570
+ try {
2571
+ const r = await this.request('DELETE', '/api/v1/meta/item', body, undefined, { contextOverride: opts?.context });
2572
+ return r?.ok !== false;
2573
+ }
2574
+ catch (e) {
2575
+ if (e instanceof types_1.XCiteDBError && e.status === 404)
2576
+ return false;
2577
+ throw e;
2578
+ }
2579
+ }
2445
2580
  /** Convenience: push a single item via query. Wraps `item` in `[item]` and calls {@link appendMetaByQuery}. */
2446
2581
  async appendItemByQuery(query, item, path = '', firstMatch = false, opts) {
2447
2582
  return this.appendMetaByQuery(query, [item], path, firstMatch, opts);
@@ -2449,6 +2584,14 @@ class XCiteDBClient {
2449
2584
  async queryMeta(identifier, path = '', opts) {
2450
2585
  return this.request('GET', `/api/v1/meta${buildQuery({ identifier: this.isoPrefixId(identifier), path })}`, undefined, undefined, { contextOverride: opts?.context });
2451
2586
  }
2587
+ /**
2588
+ * Alias for {@link queryMeta} that pairs by name with {@link addMeta} / {@link appendMeta} /
2589
+ * {@link clearMeta}. With `path` empty this returns the whole top-level meta object — which
2590
+ * by convention is keyed per tool/flow.
2591
+ */
2592
+ async getMeta(identifier, path = '', opts) {
2593
+ return this.queryMeta(identifier, path, opts);
2594
+ }
2452
2595
  async queryMetaByQuery(query, path = '', opts) {
2453
2596
  return this.request('POST', '/api/v1/meta/query', { query: this.isoPrefixQuery(query), path }, undefined, { contextOverride: opts?.context });
2454
2597
  }
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, 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, UserIsolationShareListEntry, UserIsolationShareListResponse, UserIsolationUnshareParams, ShareTarget, ShareDirection, AppUserGroup, AppUserGroupKind, CreateGroupParams, ListGroupsResponse, WorkspaceInfo, WriteDocumentOptions, XmlDocumentBatchItem, CreateSandboxOptions, 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, FunctionManifest, FunctionListItem, FunctionDeployRequest, FunctionLimits, FunctionSecretEntry, FunctionSecretsList, TxnOperation, TxnOperationResult, TxnPrecondition, TxnPreconditionResult, TxnRequest, TxnResponse, JwksKey, JwksResponse, VerifyAppUserTokenOptions, TokenPair, UserInfo, UserIsolationConfig, UserIsolationCreateShareParams, UserIsolationOptions, UserIsolationShareMode, UserIsolationShareResult, UserIsolationShareListEntry, UserIsolationShareListResponse, UserIsolationUnshareParams, ShareTarget, ShareDirection, AppUserGroup, AppUserGroupKind, CreateGroupParams, ListGroupsResponse, 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
@@ -854,6 +854,26 @@ export interface CreateTestSessionOptions {
854
854
  * When true, creates an overlay test session: writable ephemeral LMDB with production project data as read-only base.
855
855
  */
856
856
  overlay?: boolean;
857
+ /**
858
+ * When set, attach the new test session to a sealed standalone sandbox (a "fixture") instead of the
859
+ * production project as the read-only base. Implies `overlay: true`.
860
+ *
861
+ * Use this for e2e suites where many tests share the same expensive setup (created users, seeded
862
+ * data, etc.) — set up the fixture once via the sandbox API, seal it with a content fingerprint,
863
+ * then point each test session at it. Multiple test sessions can attach to the same fixture
864
+ * concurrently; each gets its own ephemeral overlay layer.
865
+ */
866
+ baseSandboxName?: string;
867
+ /**
868
+ * Required iff {@link baseSandboxName} is set. The fingerprint your setup script stamped on the
869
+ * fixture (typically `hash(seed_inputs) + schema_version + git_sha`).
870
+ *
871
+ * On mismatch the server returns **`409 fingerprint_mismatch`** — the recovery action is to
872
+ * **re-run setup** to refresh the fixture, NOT to retry. The whole feature's safety property
873
+ * hinges on consumers handling this error correctly: a stale fixture must never silently serve
874
+ * old data.
875
+ */
876
+ expectedBaseFingerprint?: string;
857
877
  /**
858
878
  * API keys to snapshot into the new session's config DB so requests carrying any of these keys
859
879
  * (alongside `X-Test-Session: <token>`) authenticate against the session, not production.
@@ -910,6 +930,22 @@ export interface SandboxInfo {
910
930
  owner_member_id: string;
911
931
  /** Present in list responses: the requesting member's role in this sandbox, or "" if not a member. */
912
932
  my_role?: 'owner' | 'editor' | 'viewer' | '';
933
+ /**
934
+ * True for *fixture* sandboxes — empty initial DB, no production overlay underneath. Eligible
935
+ * to be sealed and used as a base for test sessions or other sandboxes. Mutually exclusive
936
+ * with `base_sandbox_name` on create.
937
+ */
938
+ standalone?: boolean;
939
+ /** True when this standalone sandbox has been sealed as a fixture base. */
940
+ sealed?: boolean;
941
+ /** Content fingerprint set when this sandbox was last sealed (only present on fixtures). */
942
+ base_fingerprint?: string;
943
+ /** Unix seconds at which the fixture was last sealed. */
944
+ sealed_at?: number;
945
+ /** First 8 chars of the parent fixture token, when this sandbox attached to a fixture base. */
946
+ base_sandbox_token_prefix?: string;
947
+ /** Echoed at create time when this sandbox is attached to a fixture base. */
948
+ expected_base_fingerprint?: string;
913
949
  }
914
950
  /** Options for {@link XCiteDBClient.createSandbox}. */
915
951
  export interface CreateSandboxOptions {
@@ -923,6 +959,31 @@ export interface CreateSandboxOptions {
923
959
  base_branch?: string;
924
960
  /** Defaults to "log" — webhooks/auth-emails get captured instead of firing. */
925
961
  effects_policy?: 'real' | 'log' | 'mock';
962
+ /**
963
+ * Fixture mode: an empty initial DB with no production overlay. Required for sealing the
964
+ * sandbox as a fixture base later. At most one standalone sandbox is allowed per
965
+ * `(org_id, project_id)`. Mutually exclusive with {@link baseSandboxName} and
966
+ * `effects_policy: 'real'`.
967
+ */
968
+ standalone?: boolean;
969
+ /**
970
+ * Attach this dev sandbox to a sealed standalone fixture as its base. The fixture provides the
971
+ * read-only baseline; this sandbox holds the writable overlay. Same fingerprint contract as
972
+ * {@link CreateTestSessionOptions.baseSandboxName} — mismatch is `409 fingerprint_mismatch`.
973
+ * Mutually exclusive with {@link standalone}.
974
+ */
975
+ baseSandboxName?: string;
976
+ /** Required iff {@link baseSandboxName} is set. */
977
+ expectedBaseFingerprint?: string;
978
+ }
979
+ /** Body for {@link XCiteDBClient.sealSandbox}. */
980
+ export interface SealSandboxOptions {
981
+ /**
982
+ * Content fingerprint to stamp on the fixture. Should be derived from your seed inputs +
983
+ * schema version + app version (typically git SHA). On a subsequent attach, consumers must
984
+ * pass the same fingerprint via `expectedBaseFingerprint` — mismatch is a hard failure.
985
+ */
986
+ fingerprint: string;
926
987
  }
927
988
  /** Response from {@link XCiteDBClient.mintSandboxApiKey}. The `api_key` value is shown once. */
928
989
  export interface SandboxApiKeyMintResult {
@@ -1162,6 +1223,93 @@ export interface StoredTriggerResponse {
1162
1223
  trigger_id: string;
1163
1224
  trigger: TriggerDefinition;
1164
1225
  }
1226
+ export interface FunctionLimits {
1227
+ /** Per-invocation fuel budget (0 = use server default). */
1228
+ fuel?: number;
1229
+ /** Per-invocation wall-clock cap in ms (0 = use server default). */
1230
+ wall_ms?: number;
1231
+ /** Per-invocation linear-memory cap in MiB (0 = use server default). */
1232
+ mem_mb?: number;
1233
+ }
1234
+ /**
1235
+ * Wire-format function manifest as surfaced by `GET /api/v1/functions/{name}`.
1236
+ * Note: `wasm_b64` is included only when `?include_wasm=true` was requested
1237
+ * (admin export path); the standard read path returns `wasm_size` instead.
1238
+ * Secret VALUES are NEVER returned by any GET path — `secrets` here is the
1239
+ * capability-list declaration (just names).
1240
+ */
1241
+ export interface FunctionManifest {
1242
+ name: string;
1243
+ version: number;
1244
+ entrypoint: string;
1245
+ /** Phase 2.5: only `"raw_wasm"`. Phase 4 adds `"javy"` (server-compiled JS/TS). */
1246
+ runtime: 'raw_wasm' | 'javy';
1247
+ /** Phase 2.5: only `"as_caller"`. Phase 5 adds `"as_role"`. */
1248
+ identity_mode: 'as_caller' | 'as_role';
1249
+ role?: string;
1250
+ limits: FunctionLimits;
1251
+ /**
1252
+ * Per-function egress allowlist applied by `xcite.fetch`. Hostnames and
1253
+ * `*.example.com` glob patterns; empty = no external HTTP allowed.
1254
+ */
1255
+ egress_allowlist: string[];
1256
+ /**
1257
+ * Capability-style declaration of secret names this function may read at
1258
+ * runtime. Set values via `setFunctionSecrets` — they are never echoed
1259
+ * back. To remove a name from the function's capability set, drop it
1260
+ * from this array on the next deploy AND call `deleteFunctionSecret` for
1261
+ * any orphaned value.
1262
+ */
1263
+ secrets: string[];
1264
+ /**
1265
+ * Non-secret config: URLs, feature flags, model names. Round-trips on
1266
+ * GET — DO NOT put API keys / passwords here.
1267
+ */
1268
+ env: Record<string, string | number | boolean | null>;
1269
+ /** Size of the stored .wasm bytes in bytes. */
1270
+ wasm_size?: number;
1271
+ /** Only present when `getFunction(name, { includeWasm: true })`. */
1272
+ wasm_b64?: string;
1273
+ }
1274
+ /** Compact metadata used by the list endpoint (`GET /api/v1/functions`). */
1275
+ export type FunctionListItem = FunctionManifest;
1276
+ /**
1277
+ * Body for `PUT /api/v1/functions/{name}` (function deploy). The server
1278
+ * fills `name` from the URL path; sending it in the body is ignored.
1279
+ * Validation: `wasm_b64` is required and must decode to a wasm module
1280
+ * starting with the magic header `\\0asm`.
1281
+ */
1282
+ export interface FunctionDeployRequest {
1283
+ version?: number;
1284
+ entrypoint?: string;
1285
+ runtime?: 'raw_wasm' | 'javy';
1286
+ identity_mode?: 'as_caller' | 'as_role';
1287
+ role?: string;
1288
+ limits?: FunctionLimits;
1289
+ egress_allowlist?: string[];
1290
+ secrets?: string[];
1291
+ env?: Record<string, string | number | boolean | null>;
1292
+ /** Required: base64-encoded wasm bytes. */
1293
+ wasm_b64: string;
1294
+ }
1295
+ export interface FunctionSecretEntry {
1296
+ name: string;
1297
+ /** True iff a value is currently stored. Values themselves are never returned. */
1298
+ set: boolean;
1299
+ }
1300
+ export interface FunctionSecretsList {
1301
+ function: string;
1302
+ secrets: FunctionSecretEntry[];
1303
+ /**
1304
+ * Names that have a stored value but are no longer declared in the
1305
+ * manifest's `secrets[]` array. Operators should either re-declare or
1306
+ * delete each one — orphans are unreachable by guests but still take
1307
+ * storage and audit attention.
1308
+ */
1309
+ orphan_secrets?: string[];
1310
+ /** "project" | "sealed" | "none" — which encryption backend the server picked. */
1311
+ backend: string;
1312
+ }
1165
1313
  /** Named snapshot on a workspace (checkpoint). */
1166
1314
  export interface CheckpointRecord {
1167
1315
  id: string;
package/llms.txt CHANGED
@@ -159,6 +159,24 @@ If you skip the bootstrap, an app-user JWT cannot read or write `/spaces/<userId
159
159
 
160
160
  **SDK one-liners for bootstrap:** **JS:** `XCiteDBClient.createTestSession({ baseUrl, apiKey, testRequireAuth: true, bootstrap: { user_isolation: { enabled: true, namespace_pattern: '/spaces/${user.id}' }, developer_bypass: true } })`. **Python:** `async with XCiteDBClient.test_session(base_url, api_key=sk, test_require_auth=True, bootstrap={...}) as c:`. **C++:** set `options.test_session_bootstrap = nlohmann::json::parse(R"(...)");` then `XCiteDBClient::create_test_session(options)`.
161
161
 
162
+ ### Fixture-base test sessions (e2e shared setup)
163
+
164
+ When an e2e suite (Playwright etc.) re-runs the same expensive setup before every test — create users, seed documents, configure policies — that setup dominates wall-clock time. **Fixture-base mode** lets you do the setup **once per CI run**, share it across many tests in parallel, and treat staleness as a hard failure rather than a silent bug.
165
+
166
+ Lifecycle:
167
+
168
+ 1. **Create a fixture sandbox** (one-time per project): `POST /api/v1/sandboxes` with `{"name":"e2e-fixture","standalone":true}`. A standalone sandbox has an empty initial DB and no production overlay underneath. **Only one standalone sandbox is allowed per `(org, project)`** — bumping fingerprints, not creating new fixtures, is the way to evolve.
169
+ 2. **Populate it** via the regular API (open a sandbox session against it, run your seed script, etc.). Effects policy `real` is rejected for standalone sandboxes; `log` (default) or `mock` are valid.
170
+ 3. **Seal it** with a content fingerprint: `POST /api/v1/sandboxes/e2e-fixture/seal` with `{"fingerprint":"<hash>"}`. The fingerprint should encode anything that affects stored shape: at minimum `hash(seed_inputs) + schema_version + git_sha` (every commit pessimistically invalidates the fixture — the friction of a manual version bump is worse than rerunning a fast API-only setup).
171
+ 4. **Attach test sessions to the fixture:** `POST /api/v1/test/sessions` with `{"overlay":true,"base_sandbox_name":"e2e-fixture","expected_base_fingerprint":"<hash>"}`. Many test sessions can attach concurrently; each gets its own ephemeral overlay layer; reads see the fixture's data, writes go to the test's overlay only. Mismatch on `expected_base_fingerprint` is **`409 fingerprint_mismatch` — the recovery action is to RE-RUN SETUP, not to retry**. This is the safety property of the cache: a stale fixture must never silently serve old data.
172
+ 5. **Re-seed** when the app or seed inputs change: `POST /api/v1/sandboxes/e2e-fixture/unseal` → `POST /api/v1/sandboxes/e2e-fixture/reset` → repopulate via API → `POST /api/v1/sandboxes/e2e-fixture/seal` at the new fingerprint.
173
+
174
+ **Dev sandboxes can also attach to a fixture** (interactive testing on top of fixture data instead of production): `POST /api/v1/sandboxes` with `{"name":"my-feature","base_sandbox_name":"e2e-fixture","expected_base_fingerprint":"<hash>"}`. Same fingerprint contract; if the fixture is later re-sealed, the dev sandbox's next request fails with `fixture_fingerprint_changed` — recovery in v1 is to delete and recreate the dev sandbox.
175
+
176
+ **Security:** the fixture-base resolver is keyed on the **caller's** `(org, project)`, not user input — there is no way for tenant A to attach to tenant B's fixture by name. The membership check (caller must be owner/editor of the fixture) returns a generic 404 for both not-found and not-a-member, so cross-tenant existence cannot be probed.
177
+
178
+ **SDK helpers:** **JS:** `client.createTestSession({ baseUrl, apiKey, baseSandboxName, expectedBaseFingerprint })`, `client.createSandbox({ name, baseSandboxName, expectedBaseFingerprint })`, `client.sealSandbox(name, { fingerprint })`, `client.unsealSandbox(name)`. **Python:** `XCiteDBClient.test_session(base_url, base_sandbox_name=..., expected_base_fingerprint=...)`, `client.seal_sandbox(name, fingerprint=...)`, `client.unseal_sandbox(name)`. **C++:** set `options.test_session_base_sandbox_name` + `options.test_session_expected_base_fingerprint` then `create_test_session`; `seal_sandbox(name, fingerprint)`, `unseal_sandbox(name)`. **MCP:** tools `seal_sandbox`, `unseal_sandbox`; the `create_sandbox` and `create_test_session` tools accept the new fields.
179
+
162
180
  ## Wrapping XCiteDB in a higher-level service
163
181
 
164
182
  When you build a backend that calls XCiteDB on behalf of users:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcitedbs/client",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",