@sanity-labs/client-fake-for-test 0.14.0 → 0.15.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
@@ -841,6 +841,12 @@ export declare type ReleaseType = "asap" | "scheduled" | "undecided";
841
841
  export declare interface ResourceAccess {
842
842
  roles?: NonNullable<CurrentUser["roles"]>;
843
843
  permissions?: string[];
844
+ /**
845
+ * The user's attributes in the organization, as an administrator or SAML
846
+ * sync set them; read on `"organization:<id>"` entries only. Served by
847
+ * `GET /organizations/<id>/users/me/attributes`.
848
+ */
849
+ attributes?: UserAttribute[];
844
850
  }
845
851
 
846
852
  /** Resource types `@sanity/client`'s `resource` config can name (the ones staging probed). */
@@ -1365,6 +1371,29 @@ declare interface UnsupportedEntry {
1365
1371
  workaround: string;
1366
1372
  }
1367
1373
 
1374
+ /**
1375
+ * One user attribute: its key, its type, and its value from each source.
1376
+ * The served row adds `activeSource` and `activeValue`: the Sanity value
1377
+ * when there is one, else the SAML value.
1378
+ */
1379
+ export declare interface UserAttribute {
1380
+ key: string;
1381
+ type: string;
1382
+ values: {
1383
+ saml?: UserAttributeValue;
1384
+ sanity?: UserAttributeValue;
1385
+ };
1386
+ }
1387
+
1388
+ /** A value a user attribute can hold. */
1389
+ export declare type UserAttributeValue =
1390
+ | string
1391
+ | number
1392
+ | boolean
1393
+ | string[]
1394
+ | number[]
1395
+ | boolean[];
1396
+
1368
1397
  export declare type VersionAction =
1369
1398
  | {
1370
1399
  actionType: "sanity.action.document.version.create";
package/dist/index.d.ts CHANGED
@@ -841,6 +841,12 @@ export declare type ReleaseType = "asap" | "scheduled" | "undecided";
841
841
  export declare interface ResourceAccess {
842
842
  roles?: NonNullable<CurrentUser["roles"]>;
843
843
  permissions?: string[];
844
+ /**
845
+ * The user's attributes in the organization, as an administrator or SAML
846
+ * sync set them; read on `"organization:<id>"` entries only. Served by
847
+ * `GET /organizations/<id>/users/me/attributes`.
848
+ */
849
+ attributes?: UserAttribute[];
844
850
  }
845
851
 
846
852
  /** Resource types `@sanity/client`'s `resource` config can name (the ones staging probed). */
@@ -1365,6 +1371,29 @@ declare interface UnsupportedEntry {
1365
1371
  workaround: string;
1366
1372
  }
1367
1373
 
1374
+ /**
1375
+ * One user attribute: its key, its type, and its value from each source.
1376
+ * The served row adds `activeSource` and `activeValue`: the Sanity value
1377
+ * when there is one, else the SAML value.
1378
+ */
1379
+ export declare interface UserAttribute {
1380
+ key: string;
1381
+ type: string;
1382
+ values: {
1383
+ saml?: UserAttributeValue;
1384
+ sanity?: UserAttributeValue;
1385
+ };
1386
+ }
1387
+
1388
+ /** A value a user attribute can hold. */
1389
+ export declare type UserAttributeValue =
1390
+ | string
1391
+ | number
1392
+ | boolean
1393
+ | string[]
1394
+ | number[]
1395
+ | boolean[];
1396
+
1368
1397
  export declare type VersionAction =
1369
1398
  | {
1370
1399
  actionType: "sanity.action.document.version.create";
package/dist/index.js CHANGED
@@ -290,8 +290,9 @@ const ACCESS_CAPABILITIES = {
290
290
  memberRoles: ({ identity, id }) => (identity.usersByProject?.[id] ?? identity.user).roles ?? []
291
291
  },
292
292
  "media-library": {
293
- // Unproven on staging (no token held roles read): refused like a missing permission.
294
- roles: noPermission,
293
+ // Staging refuses it only because no probe token held roles read.
294
+ roles: "served",
295
+ rolesPermission: "sanity.media-library.roles.read",
295
296
  users: "served",
296
297
  // Unproven on staging (only the listing was probed); refused as on `project`.
297
298
  user: noPermission,
@@ -330,7 +331,7 @@ function dataUrl(args) {
330
331
  const base = `/${operation}/${dataset}`;
331
332
  return `/data${path !== void 0 ? `${base}/${path}` : base}`.replace(/\/($|\?)/, "$1");
332
333
  }
333
- const ACCESS_PATH = /^\/access\/([^/]+)\/([^/]+)(\/.*)?$/, ACCESS_USER_PATH = /^\/users\/([^/]+)$/, DATA_HISTORY_PATH = /^\/data\/history\/([^/]+)(\/.*)$/;
334
+ const ACCESS_PATH = /^\/access\/([^/]+)\/([^/]+)(\/.*)?$/, ACCESS_USER_PATH = /^\/users\/([^/]+)$/, ACCESS_ROLE_PATH = /^\/roles\/([^/]+)$/, MY_ATTRIBUTES_PATH = /^\/organizations\/([^/]+)\/users\/me\/attributes$/, DATA_HISTORY_PATH = /^\/data\/history\/([^/]+)(\/.*)$/;
334
335
  function page(data) {
335
336
  return { data, nextCursor: null };
336
337
  }
@@ -339,7 +340,12 @@ function usersPage(data) {
339
340
  }
340
341
  const UNMODELLED_PROFILE = { provider: "sanity", createdAt: (/* @__PURE__ */ new Date(0)).toISOString() };
341
342
  async function servePlatformRequest(ctx, url) {
342
- const queryStart = url.indexOf("?"), path = queryStart === -1 ? url : url.slice(0, queryStart), query = new URLSearchParams(queryStart === -1 ? "" : url.slice(queryStart + 1)), request = { method: "GET", path }, access = path.match(ACCESS_PATH);
343
+ const queryStart = url.indexOf("?"), path = queryStart === -1 ? url : url.slice(0, queryStart), query = new URLSearchParams(queryStart === -1 ? "" : url.slice(queryStart + 1)), request = { method: "GET", path }, myAttributes = path.match(MY_ATTRIBUTES_PATH);
344
+ if (myAttributes !== null) {
345
+ const organizationId = decodeURIComponent(myAttributes[1]);
346
+ return serveMyAttributes({ ctx, request, organizationId, query });
347
+ }
348
+ const access = path.match(ACCESS_PATH);
343
349
  if (access !== null) {
344
350
  const [, type, id, rest2] = access;
345
351
  return serveAccess({ ctx, request, type, id: decodeURIComponent(id), rest: rest2 ?? "", query });
@@ -421,11 +427,11 @@ function serveAccess(args) {
421
427
  }
422
428
  if (row.unknown !== void 0 && !row.unknown.holds(registry, id))
423
429
  throw row.unknown.refusal(request);
424
- const users = (capability, serve) => serveCapability(capability, request, () => {
425
- if (row.usersPermission !== void 0 && !permissions.includes(row.usersPermission))
430
+ const gated = (capability, permission, serve) => serveCapability(capability, request, () => {
431
+ if (permission !== void 0 && !permissions.includes(permission))
426
432
  throw noPermission(request);
427
433
  return serve();
428
- }), user = rest.match(ACCESS_USER_PATH);
434
+ }), users = (capability, serve) => gated(capability, row.usersPermission, serve), roles = () => rolesOn({ resource, identities: ctx.identities() }), user = rest.match(ACCESS_USER_PATH);
429
435
  if (user !== null)
430
436
  return users(row.user, () => {
431
437
  const sanityUserId = decodeURIComponent(user[1]), record = usersOn({ resource, registry, identities: ctx.identities() }).find(
@@ -434,11 +440,16 @@ function serveAccess(args) {
434
440
  if (record === void 0) throw notFound("User")(request);
435
441
  return { value: record };
436
442
  });
443
+ const role = rest.match(ACCESS_ROLE_PATH);
444
+ if (role !== null)
445
+ return gated(row.roles, row.rolesPermission, () => {
446
+ const name = decodeURIComponent(role[1]), record = roles().find((candidate) => candidate.name === name);
447
+ if (record === void 0) throw notFound("Role")(request);
448
+ return { value: record };
449
+ });
437
450
  switch (rest) {
438
451
  case "/roles":
439
- return serveCapability(row.roles, request, () => ({
440
- value: page(rolesOn({ resource, identities: ctx.identities() }))
441
- }));
452
+ return gated(row.roles, row.rolesPermission, () => ({ value: page(roles()) }));
442
453
  case "/users":
443
454
  return users(row.users, () => ({
444
455
  value: usersPage(usersOn({ resource, registry, identities: ctx.identities() }))
@@ -461,6 +472,7 @@ function serveAccess(args) {
461
472
  return;
462
473
  }
463
474
  }
475
+ const UNMODELLED_ROLE = { appliesToUsers: !0, appliesToRobots: !0 };
464
476
  function rolesOn(args) {
465
477
  const { resource } = args, byName = /* @__PURE__ */ new Map();
466
478
  for (const identity of args.identities) {
@@ -468,8 +480,12 @@ function rolesOn(args) {
468
480
  for (const role of roles ?? [])
469
481
  byName.set(role.name, {
470
482
  name: role.name,
471
- ...role.title !== void 0 ? { title: role.title } : {},
472
- ...role.description !== void 0 ? { description: role.description } : {}
483
+ title: role.title ?? role.name,
484
+ ...role.description !== void 0 ? { description: role.description } : {},
485
+ resourceType: resource.type,
486
+ resourceId: resource.id,
487
+ ...UNMODELLED_ROLE,
488
+ permissions: []
473
489
  });
474
490
  }
475
491
  return [...byName.values()].toSorted((a, b) => a.name.localeCompare(b.name));
@@ -507,12 +523,40 @@ function usersOn(args) {
507
523
  }, existing = bySanityUserId.get(record.sanityUserId);
508
524
  if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
509
525
  throw fakeError(
510
- `request(): two registered identities resolve to user "${record.sanityUserId}" on ${resource.type} "${resource.id}" with different records. Fix the identity registrations so duplicate ids carry identical records.`
526
+ `request(): two identities (registered, or the handle's inline accessControl) resolve to user "${record.sanityUserId}" on ${resource.type} "${resource.id}" with different records. Fix the identities so duplicate ids carry identical records.`
511
527
  );
512
528
  bySanityUserId.set(record.sanityUserId, record);
513
529
  }
514
530
  return [...bySanityUserId.values()];
515
531
  }
532
+ const DEFAULT_ATTRIBUTES_LIMIT = 50;
533
+ function serveMyAttributes(args) {
534
+ const { ctx, request, organizationId, query } = args, organization = ACCESS_CAPABILITIES.organization, registry = { organizationId: ctx.organizationId, resources: ctx.resources };
535
+ if (organization.unknown !== void 0 && !organization.unknown.holds(registry, organizationId))
536
+ throw organization.unknown.refusal(request);
537
+ const entry = ctx.actorAccess()?.[accessKey("organization", organizationId)];
538
+ if (entry?.roles === void 0) throw notFound("User")(request);
539
+ const limit = attributesLimit(query), cursor = query.get("cursor"), attributes = (entry.attributes ?? []).map(attributeRecord).toSorted((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0).filter((attribute) => cursor === null || attribute.key > cursor).slice(0, limit);
540
+ return { value: { sanityUserId: ctx.actorUser().id, organizationId, attributes } };
541
+ }
542
+ function attributesLimit(query) {
543
+ const raw = query.get("limit");
544
+ if (raw === null) return DEFAULT_ATTRIBUTES_LIMIT;
545
+ const limit = Number(raw);
546
+ if (!Number.isInteger(limit) || limit < 1)
547
+ throw fakeError(
548
+ `request(): users/me/attributes got limit=${JSON.stringify(raw)}; the fake serves positive integer limits only (the platform's answer to others is not simulated).`
549
+ );
550
+ return limit;
551
+ }
552
+ function attributeRecord(attribute) {
553
+ const { key, type, values } = attribute, activeSource = values.sanity !== void 0 ? "sanity" : "saml", activeValue = values[activeSource];
554
+ if (activeValue === void 0)
555
+ throw fakeError(
556
+ `request(): user attribute "${key}" is seeded with no value; give it a \`saml\` or \`sanity\` value.`
557
+ );
558
+ return { type, key, values: { ...values }, activeSource, activeValue };
559
+ }
516
560
  class WriteAccessDeniedError extends Error {
517
561
  documentId;
518
562
  permission;
@@ -584,7 +628,7 @@ function consistentProjectRecords(args) {
584
628
  const existing = byId.get(record.id);
585
629
  if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(record))
586
630
  throw fakeError(
587
- `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.`
631
+ `request(): two identities (registered, or the handle's inline accessControl) resolve to project user "${record.id}" in project "${args.projectId}" with different records. The project records cannot be served unambiguously \u2014 fix the identities so duplicate ids carry identical records.`
588
632
  );
589
633
  byId.set(record.id, record);
590
634
  }
@@ -669,7 +713,7 @@ function makeRequestImpl(ctx) {
669
713
  const served = path === void 0 ? void 0 : await servePlatformRequest(platform, path);
670
714
  if (served !== void 0) return served.value;
671
715
  throw fakeError(
672
- `request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", the handle's ACL ("<data base>/acl"), "/projects/<projectId>" (members), "/projects/<projectId>/users/<ids>", the History API (client.getDataUrl("history", \u2026)), resource lookup ("/media-libraries/<id>", "/canvases/<id>", "/dashboards/<id>") and the Access API ("/access/<type>/<id>/roles | users[/<id>] | user-permissions/me[/check]"). For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
716
+ `request(): no handler for path "${path}". With access control configured, the fake serves "/users/me", the handle's ACL ("<data base>/acl"), "/projects/<projectId>" (members), "/projects/<projectId>/users/<ids>", the History API (client.getDataUrl("history", \u2026)), resource lookup ("/media-libraries/<id>", "/canvases/<id>", "/dashboards/<id>") and the Access API ("/access/<type>/<id>/roles[/<name>] | users[/<id>] | user-permissions/me[/check]") and the caller's attributes ("/organizations/<id>/users/me/attributes"). For any other endpoint, register a stub (client.stub("request", fn)) or intercept the HTTP call with a tool like MSW.`
673
717
  );
674
718
  };
675
719
  }
@@ -1732,7 +1776,17 @@ function makeHandle(handleArgs) {
1732
1776
  if (access === void 0)
1733
1777
  throw fakeError("this handle has no acting identity (no token or accessControl bound).");
1734
1778
  return access;
1735
- }, capabilities = capabilitiesOf(target.type), targetLabel = capabilities.label(target.id), { store, history } = ensureEntry(registry, target), concurrentEditKey = (id) => registryKey(target) + " " + id, actorId = () => {
1779
+ }, members = () => [
1780
+ ...backend.identities.values(),
1781
+ ...token === void 0 && accessControl !== void 0 ? [
1782
+ {
1783
+ user: accessControl.currentUser,
1784
+ ...accessControl.globalUser !== void 0 ? { globalUser: accessControl.globalUser } : {},
1785
+ grants: accessControl.grants,
1786
+ ...accessControl.access !== void 0 ? { access: accessControl.access } : {}
1787
+ }
1788
+ ] : []
1789
+ ], capabilities = capabilitiesOf(target.type), targetLabel = capabilities.label(target.id), { store, history } = ensureEntry(registry, target), concurrentEditKey = (id) => registryKey(target) + " " + id, actorId = () => {
1736
1790
  const access = activeAccess();
1737
1791
  if (access !== void 0)
1738
1792
  return capabilities.identity === "global" ? (access.globalUser ?? access.currentUser).id : access.currentUser.id;
@@ -2088,7 +2142,7 @@ function makeHandle(handleArgs) {
2088
2142
  // client to the global API host.
2089
2143
  useProjectHostname: useProjectHostname && resource === void 0,
2090
2144
  organizationId: backend.organizationId,
2091
- listIdentities: () => [...backend.identities.values()],
2145
+ listIdentities: members,
2092
2146
  platform: {
2093
2147
  projectId,
2094
2148
  organizationId: backend.organizationId,
@@ -2098,8 +2152,12 @@ function makeHandle(handleArgs) {
2098
2152
  history,
2099
2153
  canRead
2100
2154
  } : void 0,
2155
+ actorUser: () => {
2156
+ const access = requireActiveAccess();
2157
+ return access.globalUser ?? access.currentUser;
2158
+ },
2101
2159
  actorAccess: () => requireActiveAccess().access,
2102
- identities: () => [...backend.identities.values()],
2160
+ identities: members,
2103
2161
  resources: () => [...registry.values()].map((entry) => entry.resource)
2104
2162
  }
2105
2163
  })