@sanity-labs/client-fake-for-test 0.10.0 → 0.12.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/README.md CHANGED
@@ -80,7 +80,7 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
80
80
  | Config | `config()` | Returns `{ projectId, dataset, perspective, token?, useProjectHostname }` |
81
81
  | Config | `withConfig({ projectId?, dataset?, perspective?, accessControl?, token?, useProjectHostname? })` | Sibling handle into the same multi-dataset registry; `token` binds a registered identity (see the token→identity registry below), `accessControl` rebinds it inline — the two are mutually exclusive; `useProjectHostname: false` mints a global-hosted sibling |
82
82
  | Perspectives | `raw` / `published` / `drafts` / release stacks | Per-client default or per-`fetch` override; draft/version layering with `_originalId` projection |
83
- | HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted handle), the configured ACL path, `/projects/<projectId>` (the project's members: `{ id, isRobot? }` rows, one per distinct resolved project user), and `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`). Calling it without one throws a guidance error. |
83
+ | HTTP | `request({ url? \| uri? })` | Present **only** when the handle has an acting identity (`accessControl`, or a bound `token`). Serves `/users/me` (host-aware: project-scoped record on the project host, `globalUser` on a global-hosted handle), the configured ACL path, `/projects/<projectId>` (the project's members: `{ id, isRobot?, roles? }` rows, one per distinct resolved project user), and `/projects/<projectId>/users/<ids>` (project-user records with `id` + `sanityUserId`). Calling it without one throws a guidance error. |
84
84
 
85
85
  #### Mutation return shapes
86
86
 
@@ -98,9 +98,9 @@ More patterns — passing the fake to code typed against `SanityClient`, asserti
98
98
 
99
99
  ### Gating writes — `mutationGuard`
100
100
 
101
- `createTestClient({ mutationGuard })` installs a pluggable write interceptor. It runs on every write through the document mutation API — `create` / `createOrReplace` / `createIfNotExists` / `patch` / `delete` and the equivalent `transaction()` / `mutate()` operations — _before_ the store changes, and **throws to reject** the write (the throw propagates out of the client call, like a real lake rejection). The release/version action API (`action`, `releases.*`, `createVersion`) is **not** intercepted.
101
+ `createTestClient({ mutationGuard })` installs a pluggable write interceptor. It runs once for each document's net change in a commit through the document mutation API — `create` / `createOrReplace` / `createIfNotExists` / `patch` / `delete` and the equivalent `transaction()` / `mutate()` operations. Document publish and unpublish actions are expanded into their published and draft mutations. Release and version actions (`releases.*`, `createVersion`, and `unpublishVersion`) are not intercepted.
102
102
 
103
- The client owns the _seam_; the caller owns the _policy_. This is the hook a workflow test bench uses to enforce Content Lake **mutation guards** at the write path without re-implementing guard evaluation. Each call receives the before/after image (`before` is `null` on create, `after` is `null` on delete), the `action`, and the acting `identity`.
103
+ The client owns the _seam_; the caller owns the _policy_. This is the hook a workflow test bench uses to enforce Content Lake **mutation guards** at the write path without re-implementing guard evaluation. Each call receives the original/final image (`before` is `null` on create, `after` is `null` on delete), the `action`, and the acting `identity`. Return every denying guard as `{ guardID, name? }`; the fake evaluates every net document mutation, aggregates the returned denials, deduplicates them by `guardID` in first-observed order, rolls back the commit, and throws a 403-shaped `GuardDeniedError` with `type: "guardDeniedError"` and `denials`. Other thrown callback errors propagate unchanged.
104
104
 
105
105
  ```ts
106
106
  const client = createTestClient({
@@ -108,13 +108,16 @@ const client = createTestClient({
108
108
  mutationGuard: ({ action, before, after }) => {
109
109
  // Freeze `body` once an article is locked.
110
110
  if (action === "update" && before?.locked && before.body !== after?.body) {
111
- throw new Error("body is locked");
111
+ return [{ guardID: "_.guards.article-body", name: "Locked article body" }];
112
112
  }
113
113
  },
114
114
  });
115
115
 
116
116
  await client.patch("article-1").set({ title: "ok" }).commit(); // allowed
117
- await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.toThrow();
117
+ await expect(client.patch("article-1").set({ body: "no" }).commit()).rejects.toMatchObject({
118
+ statusCode: 403,
119
+ type: "guardDeniedError",
120
+ });
118
121
  ```
119
122
 
120
123
  > [!NOTE]
@@ -140,6 +143,10 @@ const bob = alice.withConfig({ token: "t-bob" });
140
143
 
141
144
  Register more identities after creation with `client.addIdentity(token, { user, grants })` — an upsert into the family-shared registry; already-bound siblings resolve lazily and pick the change up immediately. For a one-off identity without a registry, `withConfig({ accessControl })` still works (mutually exclusive with `token` in the same call).
142
145
 
146
+ #### System-document grants
147
+
148
+ User-issued writes to system document IDs beginning with `_.` require the `manage` permission. Ordinary `create` and `update` grants do not authorize these writes; add `manage` to identities whose tests intentionally create, replace, patch, delete, transact, mutate, publish, or unpublish system documents. The fake's internal release/version bookkeeping remains outside access-control interception.
149
+
143
150
  #### Dual-namespace identities: global vs project user ids
144
151
 
145
152
  In production the same token resolves to a **different project-scoped user id in every project** (what lake `identity()` returns) and **one account-global id** (what the global API host's `/users/me` returns); robot tokens carry a single id on every host. An identity can model that split with `globalUser` (served by `/users/me` on a `useProjectHostname: false` handle) and `usersByProject` (per-project records; the handle's project falls back to `user`):
@@ -162,6 +169,8 @@ await client.withConfig({ useProjectHostname: false }).request({ uri: "/users/me
162
169
  await client.request({ uri: "/projects/alpha/users/pAlpha" }); // { id: "pAlpha", sanityUserId: "gSnorre", ... } (ProjectUserRecord)
163
170
  ```
164
171
 
172
+ The project directory endpoint (`/projects/<projectId>`) returns each resolved project member's `id`, project-specific `roles`, and robot marker. Roles are served in canonical order, and duplicate-token agreement ignores role and property order. Registering two tokens with the same project user id is supported when their served member records agree; conflicting roles fail the request instead of making the result depend on registration order.
173
+
165
174
  Grant-filter and mutation-guard `identity()` stay bound to the **project-scoped** record, matching the lake. Identities without `globalUser` serve one record everywhere — the production robot-token shape.
166
175
 
167
176
  ### Test-only helpers (not on real `SanityClient`)
package/dist/index.cjs CHANGED
@@ -64,6 +64,28 @@ function defaultAclPath(projectId, dataset) {
64
64
  function projectUserOf(identity, projectId) {
65
65
  return identity.usersByProject?.[projectId] ?? identity.user;
66
66
  }
67
+ function consistentProjectRecords(args) {
68
+ const byId = /* @__PURE__ */ new Map();
69
+ for (const record of args.records) {
70
+ const existing = byId.get(record.id);
71
+ if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
72
+ throw fakeError(
73
+ `request(): two registered identities resolve to project user "${record.id}" in project "${args.projectId}" with different records. The project records cannot be served unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
74
+ );
75
+ byId.set(record.id, record);
76
+ }
77
+ return byId;
78
+ }
79
+ function canonicalProjectRoles(roles) {
80
+ return roles.map((role) => ({
81
+ name: role.name,
82
+ ...role.title !== void 0 ? { title: role.title } : {},
83
+ ...role.description !== void 0 ? { description: role.description } : {}
84
+ })).toSorted((left, right) => {
85
+ const leftKey = JSON.stringify(left), rightKey = JSON.stringify(right);
86
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
87
+ });
88
+ }
67
89
  function serveProjectUsers(args) {
68
90
  const { requestedIds, pathProjectId, identities } = args, toRecord = (identity) => {
69
91
  const projectUser = projectUserOf(identity, pathProjectId);
@@ -73,43 +95,37 @@ function serveProjectUsers(args) {
73
95
  displayName: projectUser.name ?? projectUser.id,
74
96
  ...projectUser.email !== void 0 ? { email: projectUser.email } : {}
75
97
  };
76
- }, byId = /* @__PURE__ */ new Map();
77
- for (const identity of identities) {
78
- const resolvedId = projectUserOf(identity, pathProjectId).id, existing = byId.get(resolvedId);
79
- if (existing !== void 0 && JSON.stringify(toRecord(existing)) !== JSON.stringify(toRecord(identity)))
80
- throw fakeError(
81
- `request(): two registered identities resolve to project user "${resolvedId}" in project "${pathProjectId}" with different records. The directory cannot answer unambiguously \u2014 fix the identity registrations so duplicate ids carry identical records.`
82
- );
83
- byId.set(resolvedId, identity);
84
- }
85
- const ids = requestedIds.split(","), [firstId] = ids;
98
+ }, byId = consistentProjectRecords({
99
+ projectId: pathProjectId,
100
+ records: identities.map(toRecord)
101
+ }), ids = requestedIds.split(","), [firstId] = ids;
86
102
  if (ids.length === 1 && firstId !== void 0) {
87
- const identity = byId.get(firstId);
88
- if (identity === void 0)
103
+ const record = byId.get(firstId);
104
+ if (record === void 0)
89
105
  throw Object.assign(
90
106
  fakeError(
91
107
  `request(): no registered identity resolves to project user "${firstId}" in project "${pathProjectId}". Register it via createTestClient({ identities }) or addIdentity().`
92
108
  ),
93
109
  { statusCode: 404 }
94
110
  );
95
- return toRecord(identity);
111
+ return record;
96
112
  }
97
113
  return ids.flatMap((id) => {
98
- const identity = byId.get(id);
99
- return identity === void 0 ? [] : [toRecord(identity)];
114
+ const record = byId.get(id);
115
+ return record === void 0 ? [] : [record];
100
116
  });
101
117
  }
102
118
  const PROJECT_USERS_PATH = /^\/projects\/([^/]+)\/users\/([^/]+)$/, PROJECT_PATH = /^\/projects\/([^/]+)$/;
103
119
  function serveProject(args) {
104
- const ids = /* @__PURE__ */ new Set();
105
- for (const identity of args.identities)
106
- ids.add(projectUserOf(identity, args.pathProjectId).id);
107
- return {
108
- members: [...ids].map((id) => ({
109
- id,
110
- ...id.startsWith("p-") ? { isRobot: !0 } : {}
111
- }))
112
- };
120
+ const records = args.identities.map((identity) => {
121
+ const projectUser = projectUserOf(identity, args.pathProjectId);
122
+ return {
123
+ id: projectUser.id,
124
+ ...projectUser.id.startsWith("p-") ? { isRobot: !0 } : {},
125
+ ...projectUser.roles !== void 0 ? { roles: canonicalProjectRoles(projectUser.roles) } : {}
126
+ };
127
+ });
128
+ return { members: [...consistentProjectRecords({ projectId: args.pathProjectId, records }).values()] };
113
129
  }
114
130
  function makeRequestImpl(ctx) {
115
131
  const { getAccess, projectId, dataset, useProjectHostname, listIdentities } = ctx;
@@ -137,6 +153,48 @@ function makeRequestImpl(ctx) {
137
153
  );
138
154
  };
139
155
  }
156
+ class GuardDeniedError extends Error {
157
+ statusCode = 403;
158
+ type = "guardDeniedError";
159
+ denials;
160
+ details;
161
+ response;
162
+ constructor(denials) {
163
+ const unique = uniqueDenials(denials);
164
+ super(`Mutation denied by guard(s) [${unique.map((denial) => denial.guardID).join(", ")}]`), this.name = "GuardDeniedError", this.denials = unique, this.details = { type: this.type, denials: unique }, this.response = {
165
+ body: { error: this.details },
166
+ url: "memory://client-fake-for-test/data/mutate",
167
+ method: "POST",
168
+ headers: {},
169
+ statusCode: this.statusCode,
170
+ statusMessage: "Forbidden"
171
+ };
172
+ }
173
+ }
174
+ function uniqueDenials(denials) {
175
+ const seen = /* @__PURE__ */ new Set();
176
+ return denials.filter((denial) => seen.has(denial.guardID) ? !1 : (seen.add(denial.guardID), !0));
177
+ }
178
+ function mutationContext(images) {
179
+ const { before, after } = images;
180
+ if (before !== after)
181
+ return before === void 0 ? { action: "create", before: null, after: after ?? null } : after === void 0 ? { action: "delete", before, after: null } : { action: "update", before, after };
182
+ }
183
+ function mutationContexts(images) {
184
+ return [...images].flatMap((image) => {
185
+ const context = mutationContext(image);
186
+ return context === void 0 ? [] : [context];
187
+ });
188
+ }
189
+ async function enforceMutationGuards(guard, contexts) {
190
+ if (guard === void 0) return;
191
+ const denials = [];
192
+ for (const context of contexts) {
193
+ const denied = await guard(context);
194
+ denied !== void 0 && denials.push(...denied);
195
+ }
196
+ if (denials.length > 0) throw new GuardDeniedError(denials);
197
+ }
140
198
  function describeType(value) {
141
199
  return value === null ? "null" : Array.isArray(value) ? "an array" : `a ${typeof value}`;
142
200
  }
@@ -670,7 +728,7 @@ async function applyAction(store, action, hooks) {
670
728
  const published = store.get(action.publishedId);
671
729
  requireRevisionMatch(action.draftId, draft._rev, action.ifDraftRevisionId), requireRevisionMatch(action.publishedId, published?._rev, action.ifPublishedRevisionId);
672
730
  const promoted = promote({ ...draft, _id: action.publishedId });
673
- return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), await hooks?.guard?.("publish", published ?? null, promoted), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
731
+ return published !== void 0 && (promoted._createdAt = published._createdAt), await hooks?.gate?.(promoted, published === void 0 ? "create" : "update"), store.put(promoted), store.remove(action.draftId), { transactionId: cryptoRandomKey() };
674
732
  }
675
733
  case "sanity.action.document.unpublish": {
676
734
  requireDraftOf(action);
@@ -679,7 +737,7 @@ async function applyAction(store, action, hooks) {
679
737
  throw fakeError(
680
738
  `Cannot unpublish "${action.publishedId}": no published document with that _id exists. document.unpublish retracts a live published document into "${action.draftId}".`
681
739
  );
682
- if (await hooks?.gate?.(published, "update"), await hooks?.guard?.("unpublish", published, null), store.get(action.draftId) === void 0) {
740
+ if (await hooks?.gate?.(published, "update"), store.get(action.draftId) === void 0) {
683
741
  const draft = promote({ ...published, _id: action.draftId });
684
742
  draft._createdAt = published._createdAt, store.put(draft);
685
743
  }
@@ -700,7 +758,7 @@ function requireRevisionMatch(documentId, actualRev, expectedRev) {
700
758
  );
701
759
  }
702
760
  async function applyActions(store, actions, hooks) {
703
- const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), remember = (id) => {
761
+ const list = Array.isArray(actions) ? actions : [actions], snapshot = /* @__PURE__ */ new Map(), guardedIds = /* @__PURE__ */ new Set(), remember = (id) => {
704
762
  snapshot.has(id) || snapshot.set(id, store.get(id));
705
763
  };
706
764
  try {
@@ -736,13 +794,18 @@ async function applyActions(store, actions, hooks) {
736
794
  remember(a.versionId);
737
795
  break;
738
796
  case "sanity.action.document.publish":
797
+ remember(a.draftId), remember(a.publishedId), guardedIds.add(a.publishedId), guardedIds.add(a.draftId);
798
+ break;
739
799
  case "sanity.action.document.unpublish":
740
- remember(a.draftId), remember(a.publishedId);
800
+ remember(a.draftId), remember(a.publishedId), guardedIds.add(a.draftId), guardedIds.add(a.publishedId);
741
801
  break;
742
802
  }
743
803
  last = await applyAction(store, a, hooks);
744
804
  }
745
- return last;
805
+ const contexts = mutationContexts(
806
+ [...guardedIds].map((id) => ({ before: snapshot.get(id), after: store.get(id) }))
807
+ );
808
+ return await hooks?.guard?.(contexts), last;
746
809
  } catch (err) {
747
810
  for (const [id, doc] of snapshot)
748
811
  doc === void 0 ? store.remove(id) : store.put(doc);
@@ -1173,11 +1236,17 @@ function makeHandle(handleArgs) {
1173
1236
  access !== void 0 && await enforceWriteAccess({
1174
1237
  document,
1175
1238
  grants: access.grants,
1176
- permission,
1239
+ permission: document._id !== void 0 && isSystemId(document._id) ? "manage" : permission,
1177
1240
  userId: access.currentUser.id
1178
1241
  });
1179
- }, guardGate = async (action, before, after) => {
1180
- mutationGuard !== void 0 && await mutationGuard({ action, before, after, identity: activeAccess()?.currentUser.id });
1242
+ }, guardGate = async (contexts) => {
1243
+ const identity = activeAccess()?.currentUser.id;
1244
+ await enforceMutationGuards(
1245
+ mutationGuard,
1246
+ contexts.map((context) => ({ ...context, identity }))
1247
+ );
1248
+ }, guardOne = async (action, before, after) => {
1249
+ await guardGate([{ action, before, after }]);
1181
1250
  }, canRead = async (document) => {
1182
1251
  const access = activeAccess();
1183
1252
  return access === void 0 ? !0 : canAccess({
@@ -1186,7 +1255,7 @@ function makeHandle(handleArgs) {
1186
1255
  permission: "read",
1187
1256
  userId: access.currentUser.id
1188
1257
  });
1189
- }, applyPatch = async (documentId, ops, ifRev) => {
1258
+ }, applyPatch = async (documentId, ops, ifRev, options = {}) => {
1190
1259
  const existing = store.get(documentId);
1191
1260
  if (existing === void 0)
1192
1261
  throw fakeError(
@@ -1198,7 +1267,7 @@ function makeHandle(handleArgs) {
1198
1267
  _rev: cryptoRandomKey(),
1199
1268
  _updatedAt: now
1200
1269
  };
1201
- return await guardGate("update", existing, next), store.put(next), next;
1270
+ return options.evaluateGuard !== !1 && await guardOne("update", existing, next), store.put(next), next;
1202
1271
  }, applyTransaction = async (mutations, txnOptions) => {
1203
1272
  const transactionId = txnOptions?.transactionId ?? cryptoRandomKey(), snapshot = /* @__PURE__ */ new Map(), touchedIds = [], operations = [], remember = (id) => {
1204
1273
  snapshot.has(id) || snapshot.set(id, store.get(id));
@@ -1213,17 +1282,13 @@ function makeHandle(handleArgs) {
1213
1282
  throw fakeError(
1214
1283
  `create() failed: a document with _id "${promoted._id}" already exists in dataset "${projectId}:${dataset}". Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
1215
1284
  );
1216
- await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1285
+ await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1217
1286
  break;
1218
1287
  }
1219
1288
  case "createOrReplace": {
1220
1289
  remember(mut.doc._id);
1221
1290
  const promoted = promote2(mut.doc), existing = store.get(promoted._id);
1222
- await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(
1223
- existing !== void 0 ? "update" : "create",
1224
- existing ?? null,
1225
- promoted
1226
- );
1291
+ await gate(existing ?? promoted, existing !== void 0 ? "update" : "create");
1227
1292
  const wasPresent = existing !== void 0;
1228
1293
  store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: wasPresent ? "update" : "create" });
1229
1294
  break;
@@ -1231,21 +1296,25 @@ function makeHandle(handleArgs) {
1231
1296
  case "createIfNotExists": {
1232
1297
  if (remember(mut.doc._id), store.get(mut.doc._id) === void 0) {
1233
1298
  const promoted = promote2(mut.doc);
1234
- await gate(promoted, "create"), await guardGate("create", null, promoted), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1299
+ await gate(promoted, "create"), store.put(promoted), touchedIds.push(promoted._id), operations.push({ id: promoted._id, operation: "create" });
1235
1300
  }
1236
1301
  break;
1237
1302
  }
1238
1303
  case "delete": {
1239
1304
  remember(mut.id);
1240
1305
  const existing = store.get(mut.id);
1241
- existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
1306
+ existing !== void 0 && await gate(existing, "update"), store.remove(mut.id) && (touchedIds.push(mut.id), operations.push({ id: mut.id, operation: "delete" }));
1242
1307
  break;
1243
1308
  }
1244
1309
  case "patch": {
1245
- remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
1310
+ remember(mut.documentId), await applyPatch(mut.documentId, mut.ops, mut.ifRev, { evaluateGuard: !1 }), touchedIds.push(mut.documentId), operations.push({ id: mut.documentId, operation: "update" });
1246
1311
  break;
1247
1312
  }
1248
1313
  }
1314
+ const contexts = mutationContexts(
1315
+ [...snapshot].map(([id, before]) => ({ before, after: store.get(id) }))
1316
+ );
1317
+ await guardGate(contexts);
1249
1318
  } catch (error) {
1250
1319
  for (const [id, doc] of snapshot)
1251
1320
  doc === void 0 ? store.remove(id) : store.put(doc);
@@ -1310,7 +1379,7 @@ function makeHandle(handleArgs) {
1310
1379
  `create() failed: a document with _id "${doc._id}" already exists in dataset "${projectId}:${dataset}". Use createOrReplace() to overwrite it, or createIfNotExists() to leave the existing one untouched.`
1311
1380
  );
1312
1381
  const promoted = promote2(doc);
1313
- await gate(promoted, "create"), await guardGate("create", null, promoted);
1382
+ await gate(promoted, "create"), await guardOne("create", null, promoted);
1314
1383
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1315
1384
  return opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "create", doc }], opts?.tag)), writeResult(
1316
1385
  [promoted],
@@ -1322,7 +1391,7 @@ function makeHandle(handleArgs) {
1322
1391
  createOrReplace: (doc, opts) => serializeWrite(backend, async () => {
1323
1392
  checkRevision(doc._id, opts?.ifRevisionId);
1324
1393
  const promoted = promote2(doc), existing = store.get(doc._id);
1325
- await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardGate(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
1394
+ await gate(existing ?? promoted, existing !== void 0 ? "update" : "create"), await guardOne(existing !== void 0 ? "update" : "create", existing ?? null, promoted);
1326
1395
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1327
1396
  opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createOrReplace", doc }], opts?.tag));
1328
1397
  const operation = existing !== void 0 ? "update" : "create";
@@ -1344,7 +1413,7 @@ function makeHandle(handleArgs) {
1344
1413
  opts
1345
1414
  );
1346
1415
  const promoted = promote2(doc);
1347
- return await gate(promoted, "create"), await guardGate("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
1416
+ return await gate(promoted, "create"), await guardOne("create", null, promoted), opts?.dryRun !== !0 && (store.put(promoted), record(transactionId, [{ kind: "createIfNotExists", doc }], opts?.tag)), writeResult(
1348
1417
  [promoted],
1349
1418
  [{ id: promoted._id, operation: "create" }],
1350
1419
  transactionId,
@@ -1354,7 +1423,7 @@ function makeHandle(handleArgs) {
1354
1423
  delete: (id, opts) => serializeWrite(backend, async () => {
1355
1424
  checkRevision(id, opts?.ifRevisionId);
1356
1425
  const existing = store.get(id);
1357
- existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardGate("delete", existing, null);
1426
+ existing !== void 0 && await gate(existing, "update"), existing !== void 0 && await guardOne("delete", existing, null);
1358
1427
  const transactionId = opts?.transactionId ?? cryptoRandomKey();
1359
1428
  let removed = existing;
1360
1429
  return opts?.dryRun !== !0 && (removed = store.remove(id), removed !== void 0 && record(transactionId, [{ kind: "delete", id }], opts?.tag)), opts?.returnDocuments === void 0 && opts?.returnFirst === void 0 ? removed !== void 0 ? { _id: id } : null : formatMutationResult(
@@ -1504,6 +1573,7 @@ function makeHandle(handleArgs) {
1504
1573
  });
1505
1574
  }
1506
1575
  exports.ERR_PREFIX = ERR_PREFIX;
1576
+ exports.GuardDeniedError = GuardDeniedError;
1507
1577
  exports.SUPPORTED_SURFACE = SUPPORTED_SURFACE;
1508
1578
  exports.UNSUPPORTED_MEMBERS = UNSUPPORTED_MEMBERS;
1509
1579
  exports.WriteAccessDeniedError = WriteAccessDeniedError;