@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/README.md +4 -1
- package/dist/index.cjs +76 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +76 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
//
|
|
294
|
-
roles:
|
|
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 },
|
|
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
|
|
425
|
-
if (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
},
|
|
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:
|
|
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:
|
|
2160
|
+
identities: members,
|
|
2103
2161
|
resources: () => [...registry.values()].map((entry) => entry.resource)
|
|
2104
2162
|
}
|
|
2105
2163
|
})
|