@sanity-labs/client-fake-for-test 0.5.0 → 0.6.0

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/index.d.cts CHANGED
@@ -110,6 +110,37 @@ export declare interface CreateTestClientOptions {
110
110
  * unconditionally — the legacy behavior.
111
111
  */
112
112
  accessControl?: TestClientAccessControl;
113
+ /**
114
+ * Token→identity registry, shared by the whole `withConfig` family:
115
+ * multiple identities over one store, keyed by the token a handle is
116
+ * bound to — the token determines identity, exactly like production.
117
+ *
118
+ * ```ts
119
+ * const client = createTestClient({
120
+ * token: "t-admin",
121
+ * identities: {
122
+ * "t-admin": { user: { id: "admin", roles: [{ name: "administrator" }] }, grants: WILDCARD },
123
+ * "t-bob": { user: { id: "bob", roles: [{ name: "editor" }] }, grants: READ_ONLY },
124
+ * },
125
+ * });
126
+ * const bob = client.withConfig({ token: "t-bob" });
127
+ * // same store; bob's /users/me → bob; bob's ACL path → bob's grants
128
+ * ```
129
+ *
130
+ * A token-bound handle behaves exactly like one configured with
131
+ * `accessControl` derived from its identity (identity serving + grant
132
+ * gating; the ACL path defaults per `(projectId, dataset)`). Register
133
+ * more identities later with `addIdentity`.
134
+ */
135
+ identities?: Record<string, TestClientIdentity>;
136
+ /**
137
+ * Token the root handle is bound to — must name an entry in
138
+ * `identities` (unknown tokens throw). Omit to leave the root handle
139
+ * identity-free (a bare factory for token-bound siblings): its
140
+ * `request` is undefined and its reads/writes are ungated. Mutually
141
+ * exclusive with `accessControl`.
142
+ */
143
+ token?: string;
113
144
  /**
114
145
  * Pluggable write interceptor — runs on each write through the document
115
146
  * mutation API (create/patch/delete/transaction; not the release/version
@@ -659,7 +690,7 @@ export declare const SUPPORTED_SURFACE: readonly [
659
690
  "releases",
660
691
  "createVersion",
661
692
  "unpublishVersion",
662
- "request (only with accessControl)",
693
+ "request (only with accessControl or a bound token)",
663
694
  "config",
664
695
  "withConfig",
665
696
  ];
@@ -685,16 +716,12 @@ export declare interface TestClient {
685
716
  options?: FetchOptions,
686
717
  ) => Promise<(T | null)[]>;
687
718
  /**
688
- * Sanity-style HTTP request hook. Present iff the client was
689
- * created with `accessControl`. The engine's access resolver pokes
690
- * `/users/me` and the configured ACL path through this — without it
691
- * the engine throws on any verb that needs an actor (which is the
692
- * cue for the bench to supply an `access` override).
693
- *
694
- * Test cases that want to exercise the engine's token-resolution
695
- * path should configure `accessControl`; cases that want bench-style
696
- * always-allow should leave it off and pass `access` through the
697
- * bench.
719
+ * Sanity-style HTTP request hook. Present iff the handle has an
720
+ * acting identity — created with `accessControl`, or bound to a
721
+ * registered token (`identities` + `token`). The engine's access
722
+ * resolver pokes `/users/me` and the configured ACL path through
723
+ * this — without it the engine throws on any verb that needs an
724
+ * actor.
698
725
  */
699
726
  request?: <T>(opts: {
700
727
  url?: string;
@@ -752,11 +779,15 @@ export declare interface TestClient {
752
779
  releaseId: string;
753
780
  publishedId: string;
754
781
  }) => Promise<ActionResult>;
755
- /** Current `(projectId, dataset)` and active perspective. */
782
+ /**
783
+ * Current `(projectId, dataset)` and active perspective. `token` is
784
+ * present iff the handle is bound to a registered identity.
785
+ */
756
786
  config: () => {
757
787
  projectId: string;
758
788
  dataset: string;
759
789
  perspective: Perspective;
790
+ token?: string;
760
791
  };
761
792
  /**
762
793
  * Return a new client handle pointing at a different `(projectId, dataset)`
@@ -773,8 +804,18 @@ export declare interface TestClient {
773
804
  * Rebind the acting identity (and its grants) on a sibling that shares
774
805
  * the same store — the test analogue of a token-scoped client. Lets two
775
806
  * actors write the same dataset, e.g. for `identity()`-based guards.
807
+ * Mutually exclusive with `token`.
776
808
  */
777
809
  accessControl: TestClientAccessControl;
810
+ /**
811
+ * Bind the sibling to a token registered in the family's identity
812
+ * registry (`createTestClient({ identities })` / `addIdentity`): its
813
+ * `/users/me` serves that identity's user, its ACL path serves that
814
+ * identity's grants, and reads/writes are gated through them — the
815
+ * token determines identity, exactly like production. Unknown tokens
816
+ * throw. Mutually exclusive with `accessControl`.
817
+ */
818
+ token: string;
778
819
  }>,
779
820
  ) => TestClient;
780
821
  /** Replace the current dataset with the given documents. */
@@ -791,9 +832,21 @@ export declare interface TestClient {
791
832
  * Simulate `count` concurrent edits to `id` in the current dataset: the
792
833
  * next `count` revision-guarded mutations (those passing `ifRevisionId`)
793
834
  * to that document throw a 409-shaped conflict, as if another client
794
- * committed between the caller's read and write. Lets a test exercise
795
- * optimistic-locking retry paths deterministically, without timing or
796
- * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
835
+ * committed between the caller's read and write. Mutations that don't
836
+ * pass `ifRevisionId` are unaffected.
837
+ *
838
+ * Reach for this when the code under test reads a document's `_rev` and
839
+ * commits with `ifRevisionId` internally — so you can't stage the race
840
+ * by mutating the document first (the code would just re-read the new
841
+ * revision and its commit would match). The counter is consumed one per
842
+ * guarded commit, so `count` lets a test drive a recover-after-N-losses
843
+ * path or exhaust a retry budget, deterministically and without relying
844
+ * on promise interleaving.
845
+ *
846
+ * Note: this forces the conflict *outcome* — it short-circuits the
847
+ * revision comparison rather than changing the stored `_rev`. The code
848
+ * under test sees the identical 409 error a real lost race produces; the
849
+ * stored revision is left untouched.
797
850
  */
798
851
  simulateConcurrentEdit: (id: string, count?: number) => void;
799
852
  /** List `(projectId, dataset)` pairs known in this registry. */
@@ -811,6 +864,16 @@ export declare interface TestClient {
811
864
  stub: (name: string, handler: unknown) => void;
812
865
  /** The currently-registered stubs, keyed by member name. */
813
866
  stubs: () => Record<string, unknown>;
867
+ /**
868
+ * Register (or upsert) a token→identity entry in the family's shared
869
+ * registry. Siblings already bound to `token` resolve lazily, so a
870
+ * re-registration takes effect on them immediately. Registering an
871
+ * identity does not bind it — pass `token` at `createTestClient` or
872
+ * `withConfig({ token })` to act as it.
873
+ */
874
+ addIdentity: (token: string, identity: TestClientIdentity) => void;
875
+ /** Snapshot of the registered token→identity entries. */
876
+ identities: () => Record<string, TestClientIdentity>;
814
877
  /**
815
878
  * Every mutation committed through this client's registry, flattened
816
879
  * and in order — `create` / `createOrReplace` / `createIfNotExists` /
@@ -855,6 +918,20 @@ export declare interface TestClientAccessControl {
855
918
  aclPath?: string;
856
919
  }
857
920
 
921
+ /**
922
+ * One entry in the token→identity registry (`createTestClient({ identities })`).
923
+ * A handle bound to the entry's token behaves exactly like one configured
924
+ * with `accessControl: { currentUser: user, grants }` — `/users/me` serves
925
+ * `user`, the default ACL path serves `grants`, and reads/writes are gated
926
+ * through them.
927
+ */
928
+ export declare interface TestClientIdentity {
929
+ /** What `/users/me` returns for clients bound to this token. */
930
+ user: CurrentUser;
931
+ /** Grants served at the ACL path and enforced on reads/writes. */
932
+ grants: Grant[];
933
+ }
934
+
858
935
  /**
859
936
  * In-memory document store backing a TestClient. Closure-scoped — created
860
937
  * fresh per test, no global state.
package/dist/index.d.ts CHANGED
@@ -110,6 +110,37 @@ export declare interface CreateTestClientOptions {
110
110
  * unconditionally — the legacy behavior.
111
111
  */
112
112
  accessControl?: TestClientAccessControl;
113
+ /**
114
+ * Token→identity registry, shared by the whole `withConfig` family:
115
+ * multiple identities over one store, keyed by the token a handle is
116
+ * bound to — the token determines identity, exactly like production.
117
+ *
118
+ * ```ts
119
+ * const client = createTestClient({
120
+ * token: "t-admin",
121
+ * identities: {
122
+ * "t-admin": { user: { id: "admin", roles: [{ name: "administrator" }] }, grants: WILDCARD },
123
+ * "t-bob": { user: { id: "bob", roles: [{ name: "editor" }] }, grants: READ_ONLY },
124
+ * },
125
+ * });
126
+ * const bob = client.withConfig({ token: "t-bob" });
127
+ * // same store; bob's /users/me → bob; bob's ACL path → bob's grants
128
+ * ```
129
+ *
130
+ * A token-bound handle behaves exactly like one configured with
131
+ * `accessControl` derived from its identity (identity serving + grant
132
+ * gating; the ACL path defaults per `(projectId, dataset)`). Register
133
+ * more identities later with `addIdentity`.
134
+ */
135
+ identities?: Record<string, TestClientIdentity>;
136
+ /**
137
+ * Token the root handle is bound to — must name an entry in
138
+ * `identities` (unknown tokens throw). Omit to leave the root handle
139
+ * identity-free (a bare factory for token-bound siblings): its
140
+ * `request` is undefined and its reads/writes are ungated. Mutually
141
+ * exclusive with `accessControl`.
142
+ */
143
+ token?: string;
113
144
  /**
114
145
  * Pluggable write interceptor — runs on each write through the document
115
146
  * mutation API (create/patch/delete/transaction; not the release/version
@@ -659,7 +690,7 @@ export declare const SUPPORTED_SURFACE: readonly [
659
690
  "releases",
660
691
  "createVersion",
661
692
  "unpublishVersion",
662
- "request (only with accessControl)",
693
+ "request (only with accessControl or a bound token)",
663
694
  "config",
664
695
  "withConfig",
665
696
  ];
@@ -685,16 +716,12 @@ export declare interface TestClient {
685
716
  options?: FetchOptions,
686
717
  ) => Promise<(T | null)[]>;
687
718
  /**
688
- * Sanity-style HTTP request hook. Present iff the client was
689
- * created with `accessControl`. The engine's access resolver pokes
690
- * `/users/me` and the configured ACL path through this — without it
691
- * the engine throws on any verb that needs an actor (which is the
692
- * cue for the bench to supply an `access` override).
693
- *
694
- * Test cases that want to exercise the engine's token-resolution
695
- * path should configure `accessControl`; cases that want bench-style
696
- * always-allow should leave it off and pass `access` through the
697
- * bench.
719
+ * Sanity-style HTTP request hook. Present iff the handle has an
720
+ * acting identity — created with `accessControl`, or bound to a
721
+ * registered token (`identities` + `token`). The engine's access
722
+ * resolver pokes `/users/me` and the configured ACL path through
723
+ * this — without it the engine throws on any verb that needs an
724
+ * actor.
698
725
  */
699
726
  request?: <T>(opts: {
700
727
  url?: string;
@@ -752,11 +779,15 @@ export declare interface TestClient {
752
779
  releaseId: string;
753
780
  publishedId: string;
754
781
  }) => Promise<ActionResult>;
755
- /** Current `(projectId, dataset)` and active perspective. */
782
+ /**
783
+ * Current `(projectId, dataset)` and active perspective. `token` is
784
+ * present iff the handle is bound to a registered identity.
785
+ */
756
786
  config: () => {
757
787
  projectId: string;
758
788
  dataset: string;
759
789
  perspective: Perspective;
790
+ token?: string;
760
791
  };
761
792
  /**
762
793
  * Return a new client handle pointing at a different `(projectId, dataset)`
@@ -773,8 +804,18 @@ export declare interface TestClient {
773
804
  * Rebind the acting identity (and its grants) on a sibling that shares
774
805
  * the same store — the test analogue of a token-scoped client. Lets two
775
806
  * actors write the same dataset, e.g. for `identity()`-based guards.
807
+ * Mutually exclusive with `token`.
776
808
  */
777
809
  accessControl: TestClientAccessControl;
810
+ /**
811
+ * Bind the sibling to a token registered in the family's identity
812
+ * registry (`createTestClient({ identities })` / `addIdentity`): its
813
+ * `/users/me` serves that identity's user, its ACL path serves that
814
+ * identity's grants, and reads/writes are gated through them — the
815
+ * token determines identity, exactly like production. Unknown tokens
816
+ * throw. Mutually exclusive with `accessControl`.
817
+ */
818
+ token: string;
778
819
  }>,
779
820
  ) => TestClient;
780
821
  /** Replace the current dataset with the given documents. */
@@ -791,9 +832,21 @@ export declare interface TestClient {
791
832
  * Simulate `count` concurrent edits to `id` in the current dataset: the
792
833
  * next `count` revision-guarded mutations (those passing `ifRevisionId`)
793
834
  * to that document throw a 409-shaped conflict, as if another client
794
- * committed between the caller's read and write. Lets a test exercise
795
- * optimistic-locking retry paths deterministically, without timing or
796
- * interleaving. Mutations that don't pass `ifRevisionId` are unaffected.
835
+ * committed between the caller's read and write. Mutations that don't
836
+ * pass `ifRevisionId` are unaffected.
837
+ *
838
+ * Reach for this when the code under test reads a document's `_rev` and
839
+ * commits with `ifRevisionId` internally — so you can't stage the race
840
+ * by mutating the document first (the code would just re-read the new
841
+ * revision and its commit would match). The counter is consumed one per
842
+ * guarded commit, so `count` lets a test drive a recover-after-N-losses
843
+ * path or exhaust a retry budget, deterministically and without relying
844
+ * on promise interleaving.
845
+ *
846
+ * Note: this forces the conflict *outcome* — it short-circuits the
847
+ * revision comparison rather than changing the stored `_rev`. The code
848
+ * under test sees the identical 409 error a real lost race produces; the
849
+ * stored revision is left untouched.
797
850
  */
798
851
  simulateConcurrentEdit: (id: string, count?: number) => void;
799
852
  /** List `(projectId, dataset)` pairs known in this registry. */
@@ -811,6 +864,16 @@ export declare interface TestClient {
811
864
  stub: (name: string, handler: unknown) => void;
812
865
  /** The currently-registered stubs, keyed by member name. */
813
866
  stubs: () => Record<string, unknown>;
867
+ /**
868
+ * Register (or upsert) a token→identity entry in the family's shared
869
+ * registry. Siblings already bound to `token` resolve lazily, so a
870
+ * re-registration takes effect on them immediately. Registering an
871
+ * identity does not bind it — pass `token` at `createTestClient` or
872
+ * `withConfig({ token })` to act as it.
873
+ */
874
+ addIdentity: (token: string, identity: TestClientIdentity) => void;
875
+ /** Snapshot of the registered token→identity entries. */
876
+ identities: () => Record<string, TestClientIdentity>;
814
877
  /**
815
878
  * Every mutation committed through this client's registry, flattened
816
879
  * and in order — `create` / `createOrReplace` / `createIfNotExists` /
@@ -855,6 +918,20 @@ export declare interface TestClientAccessControl {
855
918
  aclPath?: string;
856
919
  }
857
920
 
921
+ /**
922
+ * One entry in the token→identity registry (`createTestClient({ identities })`).
923
+ * A handle bound to the entry's token behaves exactly like one configured
924
+ * with `accessControl: { currentUser: user, grants }` — `/users/me` serves
925
+ * `user`, the default ACL path serves `grants`, and reads/writes are gated
926
+ * through them.
927
+ */
928
+ export declare interface TestClientIdentity {
929
+ /** What `/users/me` returns for clients bound to this token. */
930
+ user: CurrentUser;
931
+ /** Grants served at the ACL path and enforced on reads/writes. */
932
+ grants: Grant[];
933
+ }
934
+
858
935
  /**
859
936
  * In-memory document store backing a TestClient. Closure-scoped — created
860
937
  * fresh per test, no global state.
package/dist/index.js CHANGED
@@ -59,16 +59,15 @@ async function enforceWriteAccess(args) {
59
59
  function defaultAclPath(projectId, dataset) {
60
60
  return `/projects/${projectId}/datasets/${dataset}/acl`;
61
61
  }
62
- function makeRequestImpl(accessControl, projectId, dataset) {
63
- const aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset);
62
+ function makeRequestImpl(getAccess, projectId, dataset) {
64
63
  return async (opts) => {
65
- const path = opts.uri ?? opts.url;
64
+ const accessControl = getAccess(), aclPath = accessControl.aclPath ?? defaultAclPath(projectId, dataset), path = opts.uri ?? opts.url;
66
65
  if (path === "/users/me")
67
66
  return accessControl.currentUser;
68
67
  if (path === aclPath)
69
68
  return accessControl.grants;
70
69
  throw fakeError(
71
- `request(): no handler for path "${path}". With accessControl configured, the fake only serves "/users/me" and "${aclPath}". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
70
+ `request(): no handler for path "${path}". With access control configured, the fake only serves "/users/me" and "${aclPath}". For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
72
71
  );
73
72
  };
74
73
  }
@@ -688,7 +687,7 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
688
687
  "releases",
689
688
  "createVersion",
690
689
  "unpublishVersion",
691
- "request (only with accessControl)",
690
+ "request (only with accessControl or a bound token)",
692
691
  "config",
693
692
  "withConfig"
694
693
  ], UNSUPPORTED_MEMBERS = {
@@ -737,8 +736,8 @@ const ISSUES_URL = "https://github.com/sanity-labs/client-fake-for-test/issues",
737
736
  workaround: "Out of scope \u2014 seed documents directly or stub the call site."
738
737
  },
739
738
  request: {
740
- what: "generic HTTP request hook \u2014 only wired up when accessControl is configured",
741
- workaround: "Create the client with `accessControl` to enable request() (it serves /users/me + the ACL path), or register a stub."
739
+ what: "generic HTTP request hook \u2014 only wired up when the handle has an acting identity",
740
+ workaround: "Create the client with `accessControl`, or bind a registered token (`identities` + `token` / `withConfig({ token })`), to enable request() (it serves /users/me + the ACL path). Or register a stub."
742
741
  },
743
742
  dryRun: {
744
743
  what: "not a method \u2014 did you mean the `{ dryRun }` mutation option?",
@@ -854,7 +853,21 @@ const RECORDED_METHODS = /* @__PURE__ */ new Set([
854
853
  "createVersion",
855
854
  "unpublishVersion",
856
855
  "request"
857
- ]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test", registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
856
+ ]), DEFAULT_PROJECT_ID = "test", DEFAULT_DATASET = "test";
857
+ function requireIdentity(backend, token, site) {
858
+ if (backend.identities.has(token)) return;
859
+ const known = [...backend.identities.keys()];
860
+ throw fakeError(
861
+ `${site}: unknown token "${token}" \u2014 no identity is registered for it. ` + (known.length > 0 ? `Registered tokens: ${known.map((t) => `"${t}"`).join(", ")}. ` : "No identities are registered. ") + "Register identities via createTestClient({ identities }) or client.addIdentity(token, identity)."
862
+ );
863
+ }
864
+ function rejectAmbiguousIdentity(site, config) {
865
+ if (config.token !== void 0 && config.accessControl !== void 0)
866
+ throw fakeError(
867
+ `${site}: \`token\` and \`accessControl\` are mutually exclusive \u2014 a handle acts either as a registered identity (token) or as an inline accessControl config, not both.`
868
+ );
869
+ }
870
+ const registryKey = (projectId, dataset) => `${projectId}:${dataset}`, parseRegistryKey = (key) => {
858
871
  const [projectId, dataset] = key.split(":");
859
872
  return { projectId, dataset };
860
873
  };
@@ -864,8 +877,11 @@ function createTestClient(options = {}) {
864
877
  log: [],
865
878
  stubs: /* @__PURE__ */ new Map(),
866
879
  calls: [],
867
- concurrentEdits: /* @__PURE__ */ new Map()
868
- }, registry = backend.registry, ensureStore = (projectId, dataset) => {
880
+ concurrentEdits: /* @__PURE__ */ new Map(),
881
+ identities: new Map(Object.entries(options.identities ?? {}))
882
+ }, registry = backend.registry;
883
+ rejectAmbiguousIdentity("createTestClient()", options), options.token !== void 0 && requireIdentity(backend, options.token, "createTestClient()");
884
+ const ensureStore = (projectId, dataset) => {
869
885
  const key = registryKey(projectId, dataset);
870
886
  let store = registry.get(key);
871
887
  return store === void 0 && (store = createTestStore(), registry.set(key, store)), store;
@@ -881,7 +897,8 @@ function createTestClient(options = {}) {
881
897
  defaultDataset,
882
898
  options.accessControl,
883
899
  options.perspective ?? "raw",
884
- options.mutationGuard
900
+ options.mutationGuard,
901
+ options.token
885
902
  );
886
903
  }
887
904
  function dedupeIds(results) {
@@ -934,8 +951,19 @@ function toTxnMutations(operations) {
934
951
  }
935
952
  ];
936
953
  }
937
- function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard) {
938
- const { registry } = backend, store = (() => {
954
+ function makeHandle(backend, projectId, dataset, accessControl, perspective, mutationGuard, token) {
955
+ const { registry } = backend, activeAccess = () => {
956
+ if (token === void 0) return accessControl;
957
+ const identity = backend.identities.get(token);
958
+ if (identity === void 0)
959
+ throw fakeError(`no identity is registered for token "${token}" (was it removed?).`);
960
+ return { currentUser: identity.user, grants: identity.grants };
961
+ }, requireActiveAccess = () => {
962
+ const access = activeAccess();
963
+ if (access === void 0)
964
+ throw fakeError("this handle has no acting identity (no token or accessControl bound).");
965
+ return access;
966
+ }, store = (() => {
939
967
  const key = registryKey(projectId, dataset);
940
968
  let s = registry.get(key);
941
969
  return s === void 0 && (s = createTestStore(), registry.set(key, s)), s;
@@ -982,20 +1010,24 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
982
1010
  `ifRevisionId check failed: document "${id}" is at _rev="${existing._rev}" but the mutation required _rev="${ifRevisionId}". The document changed since you read it \u2014 re-read it and retry. (This mirrors Sanity's optimistic-locking 409.)`
983
1011
  );
984
1012
  }, gate = async (document, permission) => {
985
- accessControl !== void 0 && await enforceWriteAccess({
1013
+ const access = activeAccess();
1014
+ access !== void 0 && await enforceWriteAccess({
986
1015
  document,
987
- grants: accessControl.grants,
1016
+ grants: access.grants,
988
1017
  permission,
989
- userId: accessControl.currentUser.id
1018
+ userId: access.currentUser.id
990
1019
  });
991
1020
  }, guardGate = async (action, before, after) => {
992
- mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: accessControl?.currentUser.id });
993
- }, canRead = async (document) => accessControl === void 0 ? !0 : canAccess({
994
- document,
995
- grants: accessControl.grants,
996
- permission: "read",
997
- userId: accessControl.currentUser.id
998
- }), applyPatch = async (documentId, ops, ifRev) => {
1021
+ mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: activeAccess()?.currentUser.id });
1022
+ }, canRead = async (document) => {
1023
+ const access = activeAccess();
1024
+ return access === void 0 ? !0 : canAccess({
1025
+ document,
1026
+ grants: access.grants,
1027
+ permission: "read",
1028
+ userId: access.currentUser.id
1029
+ });
1030
+ }, applyPatch = async (documentId, ops, ifRev) => {
999
1031
  const existing = store.get(documentId);
1000
1032
  if (existing === void 0)
1001
1033
  throw fakeError(
@@ -1071,7 +1103,7 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1071
1103
  );
1072
1104
  }, accessVisibleDocs = async () => {
1073
1105
  const all = store.all();
1074
- if (accessControl === void 0) return all;
1106
+ if (activeAccess() === void 0) return all;
1075
1107
  const out = [];
1076
1108
  for (const doc of all)
1077
1109
  await canRead(doc) && out.push(doc);
@@ -1226,16 +1258,21 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1226
1258
  versionId: `versions.${releaseId}.${publishedId}`,
1227
1259
  publishedId
1228
1260
  }),
1229
- ...accessControl !== void 0 ? { request: makeRequestImpl(accessControl, projectId, dataset) } : {},
1230
- config: () => ({ projectId, dataset, perspective }),
1231
- withConfig: (config) => makeHandle(
1232
- backend,
1233
- config.projectId ?? projectId,
1234
- config.dataset ?? dataset,
1235
- config.accessControl ?? accessControl,
1236
- config.perspective ?? perspective,
1237
- mutationGuard
1238
- ),
1261
+ ...token !== void 0 || accessControl !== void 0 ? { request: makeRequestImpl(requireActiveAccess, projectId, dataset) } : {},
1262
+ config: () => ({ projectId, dataset, perspective, ...token !== void 0 ? { token } : {} }),
1263
+ withConfig: (config) => {
1264
+ rejectAmbiguousIdentity("withConfig()", config), config.token !== void 0 && requireIdentity(backend, config.token, "withConfig()");
1265
+ const rebinds = config.token !== void 0 || config.accessControl !== void 0;
1266
+ return makeHandle(
1267
+ backend,
1268
+ config.projectId ?? projectId,
1269
+ config.dataset ?? dataset,
1270
+ rebinds ? config.accessControl : accessControl,
1271
+ config.perspective ?? perspective,
1272
+ mutationGuard,
1273
+ rebinds ? config.token : token
1274
+ );
1275
+ },
1239
1276
  seed: (documents) => store.seed(documents),
1240
1277
  add: (documents) => store.add(documents),
1241
1278
  reset: () => store.reset(),
@@ -1250,6 +1287,10 @@ function makeHandle(backend, projectId, dataset, accessControl, perspective, mut
1250
1287
  backend.stubs.set(name, handler);
1251
1288
  },
1252
1289
  stubs: () => Object.fromEntries(backend.stubs),
1290
+ addIdentity: (identityToken, identity) => {
1291
+ backend.identities.set(identityToken, identity);
1292
+ },
1293
+ identities: () => Object.fromEntries(backend.identities),
1253
1294
  mutations: () => backend.log.flatMap((entry) => entry.mutations),
1254
1295
  transactions: () => [...backend.log],
1255
1296
  clearLog: () => {