@xcitedbs/client 0.3.10 → 0.3.11

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
@@ -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;
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 });
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 {
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.11",
4
4
  "description": "XCiteDB BaaS client SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",