@bison-lab/payload-core 3.11.0 → 3.13.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 +91 -19
- package/dist/admin.d.mts +39 -5
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +666 -10
- package/dist/admin.mjs.map +1 -1
- package/dist/{identity-DZOb3_Gk.mjs → identity-LAzSeorC.mjs} +48 -2
- package/dist/identity-LAzSeorC.mjs.map +1 -0
- package/dist/index.d.mts +469 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1200 -5
- package/dist/index.mjs.map +1 -1
- package/dist/looks-BbL309kO.mjs.map +1 -1
- package/dist/{looks-DsizRfFV.d.mts → looks-C6kTjvk3.d.mts} +23 -5
- package/dist/looks-C6kTjvk3.d.mts.map +1 -0
- package/dist/theme.d.mts +2 -2
- package/dist/theme.mjs +2 -2
- package/package.json +4 -4
- package/dist/identity-DZOb3_Gk.mjs.map +0 -1
- package/dist/looks-DsizRfFV.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as THEME_FONT_FIELD, C as validateThemeHex, D as THEME_COLOR_SCALE_FIELD, E as THEME_COLOR_FIELD, F as THEME_PUBLISH_FIELD, I as THEME_SAVE_BUTTON, L as THEME_SECTION_HEADING, M as THEME_IDENTITY_FALLBACK, N as THEME_LIBRARY_FIELD, O as THEME_CONTRAST_REPORT, P as THEME_PAIRING_FIELD, R as headingSelectValue, S as validateSourceIncluded, T as THEME_APPEARANCE_FIELD, _ as THEME_SLUG, a as pageEditorLooks, c as findColorTokens, d as rewriteColorToken, f as themeColorKeys, g as THEME_IDENTITY_SLUG, h as THEME_COLORS_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_GREY_SCALE_FIELD, k as THEME_DOCUMENT_CONTROLS, l as rewriteColorTokens, m as THEME_APPEARANCE_SLUG, n as colorTokenField, o as pageEditorTokens, p as SYSTEM_COLOR_KEYS, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as deleteLibraryColor, v as THEME_TYPOGRAPHY_SLUG, w as LOOK_FIELD, x as themeConfigFromDoc, y as docFromConfig } from "./identity-LAzSeorC.mjs";
|
|
3
3
|
import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
|
|
4
4
|
import { catalog } from "@bison-lab/fonts";
|
|
5
5
|
import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
|
|
6
|
+
import { APIError, ValidationError, definePlugin, validations } from "payload";
|
|
7
|
+
import { select, text } from "payload/shared";
|
|
6
8
|
//#region src/seo/fields.ts
|
|
7
9
|
/**
|
|
8
10
|
* The index switch, last in the SEO tab. Off by default: a page is public
|
|
@@ -162,6 +164,145 @@ const APPEARANCE_CHOICES = {
|
|
|
162
164
|
}
|
|
163
165
|
};
|
|
164
166
|
//#endregion
|
|
167
|
+
//#region src/theme/color-usages.ts
|
|
168
|
+
/** Theme-store REST path. LibraryField calls `/api/globals/theme` + this. */
|
|
169
|
+
const COLOR_USAGES_PATH = "/color-usages";
|
|
170
|
+
/**
|
|
171
|
+
* Walk every current document in the named collections and globals
|
|
172
|
+
* (drafts included). A color only on an unpublished page is still in use.
|
|
173
|
+
*/
|
|
174
|
+
async function findColorUsages(payload, scopes, key) {
|
|
175
|
+
const usages = [];
|
|
176
|
+
for (const collection of scopes?.collections ?? []) for await (const doc of eachCollectionDoc(payload, collection)) for (const hit of findColorTokens(doc, key)) usages.push({
|
|
177
|
+
collection,
|
|
178
|
+
id: idOf(doc),
|
|
179
|
+
path: hit.path,
|
|
180
|
+
token: hit.token
|
|
181
|
+
});
|
|
182
|
+
for (const slug of scopes?.globals ?? []) {
|
|
183
|
+
const doc = await payload.findGlobal({
|
|
184
|
+
slug,
|
|
185
|
+
draft: true,
|
|
186
|
+
depth: 0,
|
|
187
|
+
overrideAccess: true
|
|
188
|
+
});
|
|
189
|
+
if (!doc) continue;
|
|
190
|
+
for (const hit of findColorTokens(doc, key)) usages.push({
|
|
191
|
+
global: slug,
|
|
192
|
+
path: hit.path,
|
|
193
|
+
token: hit.token
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return usages;
|
|
197
|
+
}
|
|
198
|
+
async function rewriteColorUsages(payload, scopes, fromKey, toKey) {
|
|
199
|
+
for (const collection of scopes?.collections ?? []) for await (const doc of eachCollectionDoc(payload, collection)) {
|
|
200
|
+
if (findColorTokens(doc, fromKey).length === 0) continue;
|
|
201
|
+
const { id: _id, ...data } = rewriteColorTokens(doc, fromKey, toKey);
|
|
202
|
+
const id = idOf(doc);
|
|
203
|
+
if (id == null) continue;
|
|
204
|
+
await payload.update({
|
|
205
|
+
collection,
|
|
206
|
+
id,
|
|
207
|
+
data,
|
|
208
|
+
draft: true,
|
|
209
|
+
overrideAccess: true
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
for (const slug of scopes?.globals ?? []) {
|
|
213
|
+
const doc = await payload.findGlobal({
|
|
214
|
+
slug,
|
|
215
|
+
draft: true,
|
|
216
|
+
depth: 0,
|
|
217
|
+
overrideAccess: true
|
|
218
|
+
});
|
|
219
|
+
if (!doc || findColorTokens(doc, fromKey).length === 0) continue;
|
|
220
|
+
await payload.updateGlobal({
|
|
221
|
+
slug,
|
|
222
|
+
data: rewriteColorTokens(doc, fromKey, toKey),
|
|
223
|
+
draft: true,
|
|
224
|
+
overrideAccess: true
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function replacementKeys(payload, except) {
|
|
229
|
+
return pageEditorLooks(await payload.findGlobal({
|
|
230
|
+
slug: THEME_SLUG,
|
|
231
|
+
depth: 0,
|
|
232
|
+
overrideAccess: true
|
|
233
|
+
})).map((look) => look.value).filter((key) => key !== except);
|
|
234
|
+
}
|
|
235
|
+
async function* eachCollectionDoc(payload, collection) {
|
|
236
|
+
let page = 1;
|
|
237
|
+
for (;;) {
|
|
238
|
+
const result = await payload.find({
|
|
239
|
+
collection,
|
|
240
|
+
draft: true,
|
|
241
|
+
depth: 0,
|
|
242
|
+
limit: 100,
|
|
243
|
+
page,
|
|
244
|
+
overrideAccess: true
|
|
245
|
+
});
|
|
246
|
+
for (const doc of result.docs) yield doc;
|
|
247
|
+
const totalPages = result.totalPages ?? (result.docs.length < 100 ? page : page + 1);
|
|
248
|
+
if (page >= totalPages || result.docs.length === 0) break;
|
|
249
|
+
page += 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function idOf(doc) {
|
|
253
|
+
const id = doc.id;
|
|
254
|
+
return typeof id === "string" || typeof id === "number" ? id : void 0;
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/theme/color-usage-endpoints.ts
|
|
258
|
+
function colorUsageEndpoints(access, scopes) {
|
|
259
|
+
return [{
|
|
260
|
+
path: COLOR_USAGES_PATH,
|
|
261
|
+
method: "get",
|
|
262
|
+
handler: async (req) => {
|
|
263
|
+
const denied = await denyUnlessUpdate(req, access.update);
|
|
264
|
+
if (denied) return denied;
|
|
265
|
+
const key = queryKey(req);
|
|
266
|
+
if (!key) return Response.json({ error: "key is required" }, { status: 400 });
|
|
267
|
+
const usages = await findColorUsages(req.payload, scopes, key);
|
|
268
|
+
return Response.json({ usages });
|
|
269
|
+
}
|
|
270
|
+
}, {
|
|
271
|
+
path: COLOR_USAGES_PATH,
|
|
272
|
+
method: "post",
|
|
273
|
+
handler: async (req) => {
|
|
274
|
+
const denied = await denyUnlessUpdate(req, access.update);
|
|
275
|
+
if (denied) return denied;
|
|
276
|
+
const body = await readJson(req);
|
|
277
|
+
const key = typeof body?.key === "string" ? body.key : "";
|
|
278
|
+
const replacement = typeof body?.replacement === "string" ? body.replacement : "";
|
|
279
|
+
if (!key || !replacement) return Response.json({ error: "key and replacement are required" }, { status: 400 });
|
|
280
|
+
if (!(await replacementKeys(req.payload, key)).includes(replacement)) return Response.json({ error: "Replacement must be a different live system or remaining custom key" }, { status: 400 });
|
|
281
|
+
await rewriteColorUsages(req.payload, scopes, key, replacement);
|
|
282
|
+
return Response.json({ ok: true });
|
|
283
|
+
}
|
|
284
|
+
}];
|
|
285
|
+
}
|
|
286
|
+
async function denyUnlessUpdate(req, update) {
|
|
287
|
+
if (await update({ req })) return null;
|
|
288
|
+
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
289
|
+
}
|
|
290
|
+
function queryKey(req) {
|
|
291
|
+
const fromQuery = req.query?.key;
|
|
292
|
+
if (typeof fromQuery === "string") return fromQuery;
|
|
293
|
+
if (Array.isArray(fromQuery) && typeof fromQuery[0] === "string") return fromQuery[0];
|
|
294
|
+
if (req.url) try {
|
|
295
|
+
return new URL(req.url, "http://local").searchParams.get("key") ?? "";
|
|
296
|
+
} catch {
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
async function readJson(req) {
|
|
302
|
+
if (typeof req.json === "function") return req.json();
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
//#endregion
|
|
165
306
|
//#region src/theme/publish.ts
|
|
166
307
|
/**
|
|
167
308
|
* Publishing a child writes that slice only. Other responsibilities stay
|
|
@@ -239,7 +380,7 @@ const LIBRARY_SUCCESS_HEX = "#22c55e";
|
|
|
239
380
|
function requireAccess$1(options) {
|
|
240
381
|
const read = options.access?.read;
|
|
241
382
|
const update = options.access?.update;
|
|
242
|
-
if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass
|
|
383
|
+
if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass canManageBrand from this package as update.");
|
|
243
384
|
return {
|
|
244
385
|
read,
|
|
245
386
|
update
|
|
@@ -568,7 +709,7 @@ function pageHooks(child) {
|
|
|
568
709
|
function createTheme(options) {
|
|
569
710
|
const access = requireAccess$1(options);
|
|
570
711
|
const destructive = requireDestructive(options);
|
|
571
|
-
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish } = options;
|
|
712
|
+
const { seed, fonts = catalog, fontsBaseUrl = "/fonts", contrastTarget = 7, logo, identity, onPublish, colorUsages } = options;
|
|
572
713
|
const adminCustom = {
|
|
573
714
|
contrastTarget,
|
|
574
715
|
fontsBaseUrl,
|
|
@@ -597,6 +738,7 @@ function createTheme(options) {
|
|
|
597
738
|
...lookFields,
|
|
598
739
|
...markFields
|
|
599
740
|
],
|
|
741
|
+
endpoints: colorUsageEndpoints(access, colorUsages),
|
|
600
742
|
hooks: { afterChange: [async ({ doc }) => {
|
|
601
743
|
if (!onPublish) return;
|
|
602
744
|
const themeDoc = doc;
|
|
@@ -674,7 +816,7 @@ const BRAND_ASSETS_MIME_TYPES = [
|
|
|
674
816
|
function requireAccess(options) {
|
|
675
817
|
const read = options.access?.read;
|
|
676
818
|
const update = options.access?.update;
|
|
677
|
-
if (!read || !update) throw new Error("createBrandAssets requires access.read and access.update. Pass
|
|
819
|
+
if (!read || !update) throw new Error("createBrandAssets requires access.read and access.update. Pass canManageBrand from this package as update.");
|
|
678
820
|
return {
|
|
679
821
|
read,
|
|
680
822
|
update
|
|
@@ -777,6 +919,1059 @@ const THEME_PREVIEW_BREAKPOINTS = [
|
|
|
777
919
|
}
|
|
778
920
|
];
|
|
779
921
|
//#endregion
|
|
780
|
-
|
|
922
|
+
//#region src/roles/types.ts
|
|
923
|
+
/**
|
|
924
|
+
* The Roles Global a site's generated types will describe. Optional and
|
|
925
|
+
* nullable, no index signature: a generated Global is assignable to this,
|
|
926
|
+
* never the reverse.
|
|
927
|
+
*/
|
|
928
|
+
const ROLES = [
|
|
929
|
+
"developer",
|
|
930
|
+
"admin",
|
|
931
|
+
"designer",
|
|
932
|
+
"author"
|
|
933
|
+
];
|
|
934
|
+
const ROLE_LABELS = {
|
|
935
|
+
developer: "Developer",
|
|
936
|
+
admin: "Admin",
|
|
937
|
+
designer: "Designer",
|
|
938
|
+
author: "Author"
|
|
939
|
+
};
|
|
940
|
+
const CAPABILITIES = [
|
|
941
|
+
"content",
|
|
942
|
+
"brand",
|
|
943
|
+
"publish",
|
|
944
|
+
"users"
|
|
945
|
+
];
|
|
946
|
+
function isRole(value) {
|
|
947
|
+
return typeof value === "string" && ROLES.includes(value);
|
|
948
|
+
}
|
|
949
|
+
/** Lowercase slug: `editor`, `site-editor`. Empty and punctuation are out. */
|
|
950
|
+
function isRoleSlug(value) {
|
|
951
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
952
|
+
}
|
|
953
|
+
function roleLabel(role, label) {
|
|
954
|
+
const trimmed = typeof label === "string" ? label.trim() : "";
|
|
955
|
+
if (trimmed) return trimmed;
|
|
956
|
+
return isRole(role) ? ROLE_LABELS[role] : role;
|
|
957
|
+
}
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/roles/matrix.ts
|
|
960
|
+
const ROLES_SLUG = "roles";
|
|
961
|
+
const ROLES_GLOBAL_DESCRIPTION = "Who may do what. Drag to change rank.";
|
|
962
|
+
const ROLES_FIELD_DESCRIPTION = "Drag to change rank. Ticks are Content, Brand, Publish, and Users.";
|
|
963
|
+
/**
|
|
964
|
+
* Default rank and ticks. Brand is Designer only. The API tab is not a
|
|
965
|
+
* column — it is locked to Developer in `isDeveloper`.
|
|
966
|
+
*/
|
|
967
|
+
const DEFAULT_ROLE_MATRIX = [
|
|
968
|
+
{
|
|
969
|
+
role: "developer",
|
|
970
|
+
content: true,
|
|
971
|
+
brand: true,
|
|
972
|
+
publish: true,
|
|
973
|
+
users: true
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
role: "admin",
|
|
977
|
+
content: true,
|
|
978
|
+
brand: false,
|
|
979
|
+
publish: true,
|
|
980
|
+
users: true
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
role: "designer",
|
|
984
|
+
content: false,
|
|
985
|
+
brand: true,
|
|
986
|
+
publish: false,
|
|
987
|
+
users: false
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
role: "author",
|
|
991
|
+
content: true,
|
|
992
|
+
brand: false,
|
|
993
|
+
publish: false,
|
|
994
|
+
users: false
|
|
995
|
+
}
|
|
996
|
+
];
|
|
997
|
+
const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
|
|
998
|
+
const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
|
|
999
|
+
const MISSING_SEED_ROLES_MESSAGE = "Roles must include Developer, Admin, Designer, and Author.";
|
|
1000
|
+
const DUPLICATE_ROLE_SLUG_MESSAGE = "Each role needs a unique slug.";
|
|
1001
|
+
const CAPABILITY_LABELS = {
|
|
1002
|
+
content: "Content",
|
|
1003
|
+
brand: "Brand",
|
|
1004
|
+
publish: "Publish",
|
|
1005
|
+
users: "Users"
|
|
1006
|
+
};
|
|
1007
|
+
function seedRoleRows(extras = []) {
|
|
1008
|
+
return [...DEFAULT_ROLE_MATRIX.map((row) => ({
|
|
1009
|
+
...row,
|
|
1010
|
+
label: isRole(row.role) ? ROLE_LABELS[row.role] : row.role
|
|
1011
|
+
})), ...extras.map((extra) => ({ ...extra }))];
|
|
1012
|
+
}
|
|
1013
|
+
function defaultRolesFieldValue(extras = []) {
|
|
1014
|
+
return seedRoleRows(extras).map((row) => ({
|
|
1015
|
+
id: row.role,
|
|
1016
|
+
...row
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
function readRoleRow(item) {
|
|
1020
|
+
const role = "role" in item ? item.role : void 0;
|
|
1021
|
+
if (typeof role !== "string" || role.trim() === "") return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
1022
|
+
if (!isRoleSlug(role)) return { error: DUPLICATE_ROLE_SLUG_MESSAGE };
|
|
1023
|
+
return {
|
|
1024
|
+
id: role,
|
|
1025
|
+
role,
|
|
1026
|
+
label: roleLabel(role, "label" in item && typeof item.label === "string" ? item.label : void 0),
|
|
1027
|
+
content: Boolean("content" in item && item.content),
|
|
1028
|
+
brand: Boolean("brand" in item && item.brand),
|
|
1029
|
+
publish: Boolean("publish" in item && item.publish),
|
|
1030
|
+
users: Boolean("users" in item && item.users)
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function parseRolesMatrix(value) {
|
|
1034
|
+
if (!Array.isArray(value)) return {
|
|
1035
|
+
ok: false,
|
|
1036
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
1037
|
+
};
|
|
1038
|
+
const rows = [];
|
|
1039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1040
|
+
for (const item of value) {
|
|
1041
|
+
if (!item || typeof item !== "object") continue;
|
|
1042
|
+
const parsed = readRoleRow(item);
|
|
1043
|
+
if ("error" in parsed) return {
|
|
1044
|
+
ok: false,
|
|
1045
|
+
message: parsed.error
|
|
1046
|
+
};
|
|
1047
|
+
if (seen.has(parsed.role)) return {
|
|
1048
|
+
ok: false,
|
|
1049
|
+
message: DUPLICATE_ROLE_SLUG_MESSAGE
|
|
1050
|
+
};
|
|
1051
|
+
seen.add(parsed.role);
|
|
1052
|
+
rows.push(parsed);
|
|
1053
|
+
}
|
|
1054
|
+
if (ROLES.some((role) => !seen.has(role))) return {
|
|
1055
|
+
ok: false,
|
|
1056
|
+
message: MISSING_SEED_ROLES_MESSAGE
|
|
1057
|
+
};
|
|
1058
|
+
return {
|
|
1059
|
+
ok: true,
|
|
1060
|
+
rows
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function hasPrivilegedUsersTick(rows) {
|
|
1064
|
+
return rows.some((row) => (row.role === "admin" || row.role === "developer") && row.users);
|
|
1065
|
+
}
|
|
1066
|
+
function validateRolesMatrix(value) {
|
|
1067
|
+
const parsed = parseRolesMatrix(value);
|
|
1068
|
+
if (!parsed.ok) return parsed.message;
|
|
1069
|
+
if (!hasPrivilegedUsersTick(parsed.rows)) return LAST_USERS_TICK_MESSAGE;
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Developer is exclusive. Admin does not swallow Designer: both store.
|
|
1074
|
+
* Unknown slugs (dropped catalogue keys such as `approver`) fall away unless
|
|
1075
|
+
* they appear on the matrix.
|
|
1076
|
+
*/
|
|
1077
|
+
function normalizeStoredRoles(value, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1078
|
+
if (!Array.isArray(value)) return [];
|
|
1079
|
+
const allowed = new Set(matrix.map((row) => row.role));
|
|
1080
|
+
const roles = [...new Set(value.filter((entry) => typeof entry === "string" && allowed.has(entry)))];
|
|
1081
|
+
if (roles.includes("developer")) return ["developer"];
|
|
1082
|
+
return roles;
|
|
1083
|
+
}
|
|
1084
|
+
function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
|
|
1085
|
+
return matrix.map((row) => ({
|
|
1086
|
+
label: roleLabel(row.role, row.label),
|
|
1087
|
+
value: row.role
|
|
1088
|
+
}));
|
|
1089
|
+
}
|
|
1090
|
+
/** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
|
|
1091
|
+
function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1092
|
+
if (role === "developer") return DEVELOPER_DESCRIPTION;
|
|
1093
|
+
const row = matrix.find((entry) => entry.role === role);
|
|
1094
|
+
if (!row) return "No capabilities";
|
|
1095
|
+
return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Seed ticks and package labels come back; extra rows stay as they are.
|
|
1099
|
+
*/
|
|
1100
|
+
function resetRolesMatrix(current) {
|
|
1101
|
+
const extras = [];
|
|
1102
|
+
if (Array.isArray(current)) for (const item of current) {
|
|
1103
|
+
if (!item || typeof item !== "object") continue;
|
|
1104
|
+
const parsed = readRoleRow(item);
|
|
1105
|
+
if ("error" in parsed || isRole(parsed.role)) continue;
|
|
1106
|
+
extras.push(parsed);
|
|
1107
|
+
}
|
|
1108
|
+
return [...defaultRolesFieldValue(), ...extras];
|
|
1109
|
+
}
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region src/roles/access.ts
|
|
1112
|
+
function isAccessArgs$1(value) {
|
|
1113
|
+
return typeof value === "object" && value !== null && "req" in value;
|
|
1114
|
+
}
|
|
1115
|
+
function storedRoles(user) {
|
|
1116
|
+
return Array.isArray(user?.roles) ? user.roles : [];
|
|
1117
|
+
}
|
|
1118
|
+
function hasRole(user, role) {
|
|
1119
|
+
return storedRoles(user).includes(role);
|
|
1120
|
+
}
|
|
1121
|
+
/** Not a tick. True only when `developer` is stored on the row. */
|
|
1122
|
+
function isDeveloper(user) {
|
|
1123
|
+
return hasRole(user, "developer");
|
|
1124
|
+
}
|
|
1125
|
+
function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1126
|
+
return storedRoles(user).some((role) => {
|
|
1127
|
+
return matrix.find((entry) => entry.role === role)?.[capability] === true;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Reads the saved Roles Global, falling back to the seed when the row is
|
|
1132
|
+
* empty, missing, or unreadable. Always override-access so a Designer
|
|
1133
|
+
* evaluating Theme does not have to read Settings → Roles.
|
|
1134
|
+
*/
|
|
1135
|
+
async function getRolesMatrix(req = {}, extras = []) {
|
|
1136
|
+
const fallback = seedRoleRows(extras);
|
|
1137
|
+
const findGlobal = req.payload?.findGlobal;
|
|
1138
|
+
if (typeof findGlobal !== "function") return fallback;
|
|
1139
|
+
try {
|
|
1140
|
+
const doc = await findGlobal({
|
|
1141
|
+
slug: ROLES_SLUG,
|
|
1142
|
+
overrideAccess: true,
|
|
1143
|
+
req
|
|
1144
|
+
});
|
|
1145
|
+
const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
|
|
1146
|
+
return parsed.ok ? parsed.rows : fallback;
|
|
1147
|
+
} catch {
|
|
1148
|
+
return fallback;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function capabilityPredicate(capability) {
|
|
1152
|
+
function predicate(userOrArgs, matrix) {
|
|
1153
|
+
if (isAccessArgs$1(userOrArgs)) {
|
|
1154
|
+
const user = userOrArgs.req.user;
|
|
1155
|
+
if (matrix) return hasCapability(user, capability, matrix);
|
|
1156
|
+
return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
|
|
1157
|
+
}
|
|
1158
|
+
return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
|
|
1159
|
+
}
|
|
1160
|
+
return predicate;
|
|
1161
|
+
}
|
|
1162
|
+
const canManageContent = capabilityPredicate("content");
|
|
1163
|
+
const canManageBrand = capabilityPredicate("brand");
|
|
1164
|
+
const canPublish = capabilityPredicate("publish");
|
|
1165
|
+
/** Users tick on any held role. */
|
|
1166
|
+
const isAdmin = capabilityPredicate("users");
|
|
1167
|
+
/** This role id currently has the Users tick. */
|
|
1168
|
+
function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1169
|
+
return typeof role === "string" && matrix.some((row) => row.role === role && row.users);
|
|
1170
|
+
}
|
|
1171
|
+
function isAuthenticated(userOrArgs) {
|
|
1172
|
+
if (isAccessArgs$1(userOrArgs)) return Boolean(userOrArgs.req.user);
|
|
1173
|
+
return Boolean(userOrArgs);
|
|
1174
|
+
}
|
|
1175
|
+
const isAdminOrSelf = async ({ req }) => {
|
|
1176
|
+
if (!req.user) return false;
|
|
1177
|
+
if (await isAdmin({ req })) return true;
|
|
1178
|
+
return { id: { equals: req.user.id } };
|
|
1179
|
+
};
|
|
1180
|
+
const authenticatedOrPublished = ({ req: { user } }) => {
|
|
1181
|
+
if (isAuthenticated(user)) return true;
|
|
1182
|
+
return { _status: { equals: "published" } };
|
|
1183
|
+
};
|
|
1184
|
+
/** API tab condition: Developer only, not the Users tick. */
|
|
1185
|
+
function isDeveloperTab({ req }) {
|
|
1186
|
+
return isDeveloper(req.user);
|
|
1187
|
+
}
|
|
1188
|
+
//#endregion
|
|
1189
|
+
//#region src/roles/fields.ts
|
|
1190
|
+
const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
|
|
1191
|
+
const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
|
|
1192
|
+
const ROLE_SLUG_FIELD = "@bison-lab/payload-core/admin#RoleSlugField";
|
|
1193
|
+
const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
|
|
1194
|
+
//#endregion
|
|
1195
|
+
//#region src/roles/global.ts
|
|
1196
|
+
const roleSlugValidate = (value, options) => {
|
|
1197
|
+
if (options.operation === "update" && isRole(options.previousValue)) return value === options.previousValue || "Each role needs a unique slug.";
|
|
1198
|
+
if (typeof value !== "string" || value === "") return DUPLICATE_ROLE_SLUG_MESSAGE;
|
|
1199
|
+
return isRoleSlug(value) || "Each role needs a unique slug.";
|
|
1200
|
+
};
|
|
1201
|
+
/**
|
|
1202
|
+
* Settings → Roles. Rank is the array order (Payload's drag handle). Ticks
|
|
1203
|
+
* are Content, Brand, Publish, Users. The API tab is not a column.
|
|
1204
|
+
*/
|
|
1205
|
+
function createRoles({ extras = [] } = {}) {
|
|
1206
|
+
const rolesField = {
|
|
1207
|
+
name: "roles",
|
|
1208
|
+
type: "array",
|
|
1209
|
+
label: "Roles",
|
|
1210
|
+
labels: {
|
|
1211
|
+
singular: "Role",
|
|
1212
|
+
plural: "Roles"
|
|
1213
|
+
},
|
|
1214
|
+
minRows: 4,
|
|
1215
|
+
required: true,
|
|
1216
|
+
defaultValue: defaultRolesFieldValue(extras),
|
|
1217
|
+
validate: validateRolesMatrix,
|
|
1218
|
+
admin: {
|
|
1219
|
+
components: {
|
|
1220
|
+
Field: ROLES_MATRIX_FIELD,
|
|
1221
|
+
RowLabel: ROLES_ROW_LABEL
|
|
1222
|
+
},
|
|
1223
|
+
description: ROLES_FIELD_DESCRIPTION,
|
|
1224
|
+
initCollapsed: false
|
|
1225
|
+
},
|
|
1226
|
+
fields: [
|
|
1227
|
+
{
|
|
1228
|
+
name: "role",
|
|
1229
|
+
type: "text",
|
|
1230
|
+
label: "Slug",
|
|
1231
|
+
required: true,
|
|
1232
|
+
validate: roleSlugValidate,
|
|
1233
|
+
hooks: { beforeChange: [({ value, previousValue }) => isRole(previousValue) ? previousValue : value] },
|
|
1234
|
+
admin: { components: { Field: ROLE_SLUG_FIELD } }
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
name: "label",
|
|
1238
|
+
type: "text",
|
|
1239
|
+
label: "Name",
|
|
1240
|
+
required: true
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
name: "content",
|
|
1244
|
+
type: "checkbox",
|
|
1245
|
+
label: "Content"
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
name: "brand",
|
|
1249
|
+
type: "checkbox",
|
|
1250
|
+
label: "Brand"
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: "publish",
|
|
1254
|
+
type: "checkbox",
|
|
1255
|
+
label: "Publish"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
name: "users",
|
|
1259
|
+
type: "checkbox",
|
|
1260
|
+
label: "Users"
|
|
1261
|
+
}
|
|
1262
|
+
]
|
|
1263
|
+
};
|
|
1264
|
+
return {
|
|
1265
|
+
slug: ROLES_SLUG,
|
|
1266
|
+
label: "Roles",
|
|
1267
|
+
admin: {
|
|
1268
|
+
group: "Settings",
|
|
1269
|
+
hidden: ({ user }) => !isAdmin(user),
|
|
1270
|
+
description: ROLES_GLOBAL_DESCRIPTION
|
|
1271
|
+
},
|
|
1272
|
+
access: {
|
|
1273
|
+
read: (args) => isAdmin(args),
|
|
1274
|
+
update: (args) => isAdmin(args)
|
|
1275
|
+
},
|
|
1276
|
+
fields: [rolesField]
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/roles/seed.ts
|
|
1281
|
+
/**
|
|
1282
|
+
* Writes the default matrix (seed four plus any site extras). For a site's
|
|
1283
|
+
* migration `up()`. Never writes a user row — seeding `developer` on a
|
|
1284
|
+
* person stays a site concern.
|
|
1285
|
+
*/
|
|
1286
|
+
async function seedRoles(payload, extras = []) {
|
|
1287
|
+
return payload.updateGlobal({
|
|
1288
|
+
slug: ROLES_SLUG,
|
|
1289
|
+
data: { roles: defaultRolesFieldValue(extras) }
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
//#endregion
|
|
1293
|
+
//#region src/features/fields.ts
|
|
1294
|
+
const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField";
|
|
1295
|
+
//#endregion
|
|
1296
|
+
//#region src/features/types.ts
|
|
1297
|
+
const FEATURES_SLUG = "features";
|
|
1298
|
+
const PACKAGE_FEATURE_SLUGS = [
|
|
1299
|
+
"pages",
|
|
1300
|
+
"media",
|
|
1301
|
+
"theme",
|
|
1302
|
+
"brand-assets",
|
|
1303
|
+
"users",
|
|
1304
|
+
"roles",
|
|
1305
|
+
"features"
|
|
1306
|
+
];
|
|
1307
|
+
/**
|
|
1308
|
+
* Package catalogue. Locked rows cannot be turned off. Empty Global falls
|
|
1309
|
+
* back to the named capability (Content → Pages/Media, Brand → Theme /
|
|
1310
|
+
* Brand assets, Users → Users/Roles/Features).
|
|
1311
|
+
*/
|
|
1312
|
+
const PACKAGE_FEATURES = [
|
|
1313
|
+
{
|
|
1314
|
+
slug: "pages",
|
|
1315
|
+
label: "Pages",
|
|
1316
|
+
fallback: "content",
|
|
1317
|
+
locked: false
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
slug: "media",
|
|
1321
|
+
label: "Media",
|
|
1322
|
+
fallback: "content",
|
|
1323
|
+
locked: false
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
slug: "theme",
|
|
1327
|
+
label: "Theme",
|
|
1328
|
+
fallback: "brand",
|
|
1329
|
+
locked: false
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
slug: "brand-assets",
|
|
1333
|
+
label: "Brand assets",
|
|
1334
|
+
fallback: "brand",
|
|
1335
|
+
locked: false
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
slug: "users",
|
|
1339
|
+
label: "Users",
|
|
1340
|
+
fallback: "users",
|
|
1341
|
+
locked: true
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
slug: "roles",
|
|
1345
|
+
label: "Roles",
|
|
1346
|
+
fallback: "users",
|
|
1347
|
+
locked: true
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
slug: "features",
|
|
1351
|
+
label: "Features",
|
|
1352
|
+
fallback: "users",
|
|
1353
|
+
locked: true
|
|
1354
|
+
}
|
|
1355
|
+
];
|
|
1356
|
+
function isPackageFeatureSlug(value) {
|
|
1357
|
+
return typeof value === "string" && PACKAGE_FEATURE_SLUGS.includes(value);
|
|
1358
|
+
}
|
|
1359
|
+
function isFeatureSlug(value) {
|
|
1360
|
+
return typeof value === "string" && /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(value);
|
|
1361
|
+
}
|
|
1362
|
+
function isLockedFeature(slug, locked) {
|
|
1363
|
+
if (locked === true) return true;
|
|
1364
|
+
return PACKAGE_FEATURES.find((feature) => feature.slug === slug)?.locked === true;
|
|
1365
|
+
}
|
|
1366
|
+
//#endregion
|
|
1367
|
+
//#region src/features/matrix.ts
|
|
1368
|
+
const MISSING_PACKAGE_FEATURES_MESSAGE = "Features must include Pages, Media, Theme, Brand assets, Users, Roles, and Features.";
|
|
1369
|
+
const LOCKED_FEATURE_MESSAGE = "Users, Roles, and Features cannot be turned off. Developer stays allowed on every row.";
|
|
1370
|
+
function featureCatalogue(extras = []) {
|
|
1371
|
+
return [...PACKAGE_FEATURES.map((feature) => ({ ...feature })), ...extras.map((extra) => ({
|
|
1372
|
+
slug: extra.slug,
|
|
1373
|
+
label: extra.label,
|
|
1374
|
+
fallback: null,
|
|
1375
|
+
locked: false
|
|
1376
|
+
}))];
|
|
1377
|
+
}
|
|
1378
|
+
function rolesForFeature(feature, matrix) {
|
|
1379
|
+
const allowed = new Set(["developer"]);
|
|
1380
|
+
if (feature.locked) {
|
|
1381
|
+
for (const row of matrix) allowed.add(row.role);
|
|
1382
|
+
return [...allowed];
|
|
1383
|
+
}
|
|
1384
|
+
if (feature.fallback) {
|
|
1385
|
+
for (const row of matrix) if (row[feature.fallback]) allowed.add(row.role);
|
|
1386
|
+
}
|
|
1387
|
+
return [...allowed];
|
|
1388
|
+
}
|
|
1389
|
+
function defaultFeaturesFieldValue(extras = [], matrix = DEFAULT_ROLE_MATRIX) {
|
|
1390
|
+
return featureCatalogue(extras).map((feature) => ({
|
|
1391
|
+
id: feature.slug,
|
|
1392
|
+
slug: feature.slug,
|
|
1393
|
+
label: feature.label,
|
|
1394
|
+
locked: feature.locked,
|
|
1395
|
+
roles: rolesForFeature(feature, matrix)
|
|
1396
|
+
}));
|
|
1397
|
+
}
|
|
1398
|
+
function parseFeaturesMatrix(value) {
|
|
1399
|
+
if (!Array.isArray(value) || value.length === 0) return {
|
|
1400
|
+
ok: false,
|
|
1401
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
1402
|
+
};
|
|
1403
|
+
const rows = [];
|
|
1404
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1405
|
+
for (const item of value) {
|
|
1406
|
+
if (!item || typeof item !== "object") continue;
|
|
1407
|
+
const slug = "slug" in item ? item.slug : void 0;
|
|
1408
|
+
if (!isFeatureSlug(slug) || seen.has(slug)) continue;
|
|
1409
|
+
seen.add(slug);
|
|
1410
|
+
const rawRoles = "roles" in item && Array.isArray(item.roles) ? item.roles : [];
|
|
1411
|
+
const roles = [];
|
|
1412
|
+
for (const role of rawRoles) if (typeof role === "string" && !roles.includes(role)) roles.push(role);
|
|
1413
|
+
const label = "label" in item && typeof item.label === "string" ? item.label : slug;
|
|
1414
|
+
rows.push({
|
|
1415
|
+
id: slug,
|
|
1416
|
+
slug,
|
|
1417
|
+
label,
|
|
1418
|
+
locked: isLockedFeature(slug, "locked" in item ? Boolean(item.locked) : void 0),
|
|
1419
|
+
roles
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
if (PACKAGE_FEATURES.some((feature) => !seen.has(feature.slug))) return {
|
|
1423
|
+
ok: false,
|
|
1424
|
+
message: MISSING_PACKAGE_FEATURES_MESSAGE
|
|
1425
|
+
};
|
|
1426
|
+
return {
|
|
1427
|
+
ok: true,
|
|
1428
|
+
rows
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function enforceFeatureLocks(rows, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1432
|
+
const roleSlugs = matrix.map((row) => row.role);
|
|
1433
|
+
return rows.map((row) => {
|
|
1434
|
+
const roles = new Set(row.roles ?? []);
|
|
1435
|
+
roles.add("developer");
|
|
1436
|
+
if (isLockedFeature(row.slug, row.locked)) for (const role of roleSlugs) roles.add(role);
|
|
1437
|
+
return {
|
|
1438
|
+
...row,
|
|
1439
|
+
roles: [...roles]
|
|
1440
|
+
};
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
function validateFeaturesMatrix(value, extras = [], matrix = DEFAULT_ROLE_MATRIX) {
|
|
1444
|
+
const parsed = parseFeaturesMatrix(value);
|
|
1445
|
+
if (!parsed.ok) return parsed.message;
|
|
1446
|
+
const allowed = new Set(featureCatalogue(extras).map((feature) => feature.slug));
|
|
1447
|
+
if (parsed.rows.some((row) => !allowed.has(row.slug))) return MISSING_PACKAGE_FEATURES_MESSAGE;
|
|
1448
|
+
for (const row of parsed.rows) {
|
|
1449
|
+
if (!row.roles?.includes("developer")) return LOCKED_FEATURE_MESSAGE;
|
|
1450
|
+
if (isLockedFeature(row.slug, row.locked) && matrix.some((role) => !row.roles?.includes(role.role))) return LOCKED_FEATURE_MESSAGE;
|
|
1451
|
+
}
|
|
1452
|
+
return true;
|
|
1453
|
+
}
|
|
1454
|
+
//#endregion
|
|
1455
|
+
//#region src/features/global.ts
|
|
1456
|
+
/**
|
|
1457
|
+
* Settings → Features. Rows are the catalogue (code). Columns are the
|
|
1458
|
+
* Roles Global. Locked rows and Developer cannot be turned off.
|
|
1459
|
+
*/
|
|
1460
|
+
function createFeatures({ extras = [] } = {}) {
|
|
1461
|
+
const catalogue = featureCatalogue(extras);
|
|
1462
|
+
return {
|
|
1463
|
+
slug: FEATURES_SLUG,
|
|
1464
|
+
label: "Features",
|
|
1465
|
+
admin: {
|
|
1466
|
+
group: "Settings",
|
|
1467
|
+
hidden: ({ user }) => !isAdmin(user),
|
|
1468
|
+
description: "Who may use which collections and globals."
|
|
1469
|
+
},
|
|
1470
|
+
access: {
|
|
1471
|
+
read: (args) => isAdmin(args),
|
|
1472
|
+
update: (args) => isAdmin(args)
|
|
1473
|
+
},
|
|
1474
|
+
fields: [{
|
|
1475
|
+
name: "features",
|
|
1476
|
+
type: "array",
|
|
1477
|
+
label: "Features",
|
|
1478
|
+
labels: {
|
|
1479
|
+
singular: "Feature",
|
|
1480
|
+
plural: "Features"
|
|
1481
|
+
},
|
|
1482
|
+
minRows: catalogue.length,
|
|
1483
|
+
maxRows: catalogue.length,
|
|
1484
|
+
required: true,
|
|
1485
|
+
defaultValue: defaultFeaturesFieldValue(extras),
|
|
1486
|
+
validate: async (value, { req }) => {
|
|
1487
|
+
return validateFeaturesMatrix(value, extras, await getRolesMatrix(req));
|
|
1488
|
+
},
|
|
1489
|
+
hooks: { beforeChange: [async ({ value, req }) => {
|
|
1490
|
+
if (!Array.isArray(value)) return value;
|
|
1491
|
+
return enforceFeatureLocks(value, await getRolesMatrix(req));
|
|
1492
|
+
}] },
|
|
1493
|
+
admin: {
|
|
1494
|
+
components: { Field: FEATURES_MATRIX_FIELD },
|
|
1495
|
+
description: "Turn a feature on or off per role. Users, Roles, and Features stay on. Developer is always allowed.",
|
|
1496
|
+
initCollapsed: false
|
|
1497
|
+
},
|
|
1498
|
+
fields: [
|
|
1499
|
+
{
|
|
1500
|
+
name: "slug",
|
|
1501
|
+
type: "text",
|
|
1502
|
+
label: "Slug",
|
|
1503
|
+
required: true,
|
|
1504
|
+
admin: { readOnly: true }
|
|
1505
|
+
},
|
|
1506
|
+
{
|
|
1507
|
+
name: "label",
|
|
1508
|
+
type: "text",
|
|
1509
|
+
label: "Name",
|
|
1510
|
+
required: true,
|
|
1511
|
+
admin: { readOnly: true }
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: "locked",
|
|
1515
|
+
type: "checkbox",
|
|
1516
|
+
label: "Locked",
|
|
1517
|
+
admin: { hidden: true }
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
name: "roles",
|
|
1521
|
+
type: "json",
|
|
1522
|
+
label: "Roles",
|
|
1523
|
+
required: true
|
|
1524
|
+
}
|
|
1525
|
+
]
|
|
1526
|
+
}]
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
//#endregion
|
|
1530
|
+
//#region src/features/seed.ts
|
|
1531
|
+
/**
|
|
1532
|
+
* Writes the default switchboard (package rows plus any site extras).
|
|
1533
|
+
* For a site's migration `up()`.
|
|
1534
|
+
*/
|
|
1535
|
+
async function seedFeatures(payload, extras = []) {
|
|
1536
|
+
return payload.updateGlobal({
|
|
1537
|
+
slug: FEATURES_SLUG,
|
|
1538
|
+
data: { features: defaultFeaturesFieldValue(extras) }
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
//#endregion
|
|
1542
|
+
//#region src/features/access.ts
|
|
1543
|
+
function isAccessArgs(value) {
|
|
1544
|
+
return typeof value === "object" && value !== null && "req" in value;
|
|
1545
|
+
}
|
|
1546
|
+
function fallbackFeature(user, slug, matrix) {
|
|
1547
|
+
const pack = PACKAGE_FEATURES.find((feature) => feature.slug === slug);
|
|
1548
|
+
if (!pack) return false;
|
|
1549
|
+
return hasCapability(user, pack.fallback, matrix);
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* True when any stored role is on the feature row. An empty grid falls
|
|
1553
|
+
* back to the capability map. Developer is always allowed.
|
|
1554
|
+
*/
|
|
1555
|
+
function hasFeature(user, slug, features = null, matrix = DEFAULT_ROLE_MATRIX) {
|
|
1556
|
+
if (isDeveloper(user)) return true;
|
|
1557
|
+
if (!user) return false;
|
|
1558
|
+
if (!features || features.length === 0) return fallbackFeature(user, slug, matrix);
|
|
1559
|
+
const row = features.find((entry) => entry.slug === slug);
|
|
1560
|
+
if (!row) return false;
|
|
1561
|
+
const allowed = row.roles ?? [];
|
|
1562
|
+
return storedRoles(user).some((role) => allowed.includes(role));
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Reads the saved Features Global. `null` means empty or unreadable — callers
|
|
1566
|
+
* fall back to the capability map. Always override-access.
|
|
1567
|
+
*/
|
|
1568
|
+
async function getFeaturesMatrix(req = {}) {
|
|
1569
|
+
const findGlobal = req.payload?.findGlobal;
|
|
1570
|
+
if (typeof findGlobal !== "function") return null;
|
|
1571
|
+
try {
|
|
1572
|
+
const doc = await findGlobal({
|
|
1573
|
+
slug: FEATURES_SLUG,
|
|
1574
|
+
overrideAccess: true,
|
|
1575
|
+
req
|
|
1576
|
+
});
|
|
1577
|
+
const parsed = parseFeaturesMatrix(doc && typeof doc === "object" && "features" in doc ? doc.features : void 0);
|
|
1578
|
+
if (!parsed.ok) return null;
|
|
1579
|
+
const matrix = await getRolesMatrix(req);
|
|
1580
|
+
return enforceFeatureLocks(parsed.rows, matrix);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Access / nav helper for one catalogue slug. Sync against a passed grid;
|
|
1587
|
+
* async when given `req` so it can read both Globals.
|
|
1588
|
+
*/
|
|
1589
|
+
function canUseFeature(slug) {
|
|
1590
|
+
function predicate(userOrArgs, features, roles) {
|
|
1591
|
+
if (isAccessArgs(userOrArgs)) {
|
|
1592
|
+
const user = userOrArgs.req.user;
|
|
1593
|
+
if (features !== void 0) return hasFeature(user, slug, features, roles ?? DEFAULT_ROLE_MATRIX);
|
|
1594
|
+
return Promise.all([getFeaturesMatrix(userOrArgs.req), getRolesMatrix(userOrArgs.req)]).then(([grid, matrix]) => hasFeature(user, slug, grid, matrix));
|
|
1595
|
+
}
|
|
1596
|
+
return hasFeature(userOrArgs, slug, features ?? null, roles ?? DEFAULT_ROLE_MATRIX);
|
|
1597
|
+
}
|
|
1598
|
+
return predicate;
|
|
1599
|
+
}
|
|
1600
|
+
/** `admin.hidden`: hide when the login cannot use the feature. */
|
|
1601
|
+
function hideUnlessFeature(slug) {
|
|
1602
|
+
return (args) => {
|
|
1603
|
+
const user = args.user;
|
|
1604
|
+
if (args.req) {
|
|
1605
|
+
const result = canUseFeature(slug)({ req: {
|
|
1606
|
+
...args.req,
|
|
1607
|
+
user
|
|
1608
|
+
} });
|
|
1609
|
+
if (result instanceof Promise) return result.then((ok) => !ok);
|
|
1610
|
+
return !result;
|
|
1611
|
+
}
|
|
1612
|
+
return !canUseFeature(slug)(user ?? null);
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1616
|
+
//#region src/fields/slug.ts
|
|
1617
|
+
/**
|
|
1618
|
+
* A stored slug from whatever was typed: lowercased, with any run of
|
|
1619
|
+
* whitespace and slashes stripped from either end in one pass, so `/ about-us /`
|
|
1620
|
+
* does not keep the inner spaces a trim-then-strip would leave.
|
|
1621
|
+
*/
|
|
1622
|
+
function normalizeSlug(value) {
|
|
1623
|
+
return value.replace(/^[\s/]+|[\s/]+$/g, "").toLowerCase();
|
|
1624
|
+
}
|
|
1625
|
+
async function slugProblem(slug, { collection, id, isReserved, req }) {
|
|
1626
|
+
if (!slug) return void 0;
|
|
1627
|
+
if (isReserved(slug)) return `"/${slug}" is a built-in page on this site, so a page here cannot use it. Please choose a different address.`;
|
|
1628
|
+
if (typeof req?.payload?.find !== "function") return void 0;
|
|
1629
|
+
const where = { slug: { equals: slug } };
|
|
1630
|
+
if (id !== void 0) where.id = { not_equals: id };
|
|
1631
|
+
const query = {
|
|
1632
|
+
collection,
|
|
1633
|
+
depth: 0,
|
|
1634
|
+
limit: 1,
|
|
1635
|
+
overrideAccess: true,
|
|
1636
|
+
pagination: false,
|
|
1637
|
+
req,
|
|
1638
|
+
where
|
|
1639
|
+
};
|
|
1640
|
+
return (await req.payload.find(query)).docs.length > 0 || (await req.payload.find({
|
|
1641
|
+
...query,
|
|
1642
|
+
draft: true
|
|
1643
|
+
})).docs.length > 0 ? `Another page is already using "/${slug}". Please choose a different address.` : void 0;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The path a document is published at, normalised on the way in. Unique
|
|
1647
|
+
* across the collection. `validate` is the message under the field;
|
|
1648
|
+
* `beforeChange` is the enforcement on a draft save, where Payload skips
|
|
1649
|
+
* field validation.
|
|
1650
|
+
*/
|
|
1651
|
+
function slugField({ collection, isReserved }) {
|
|
1652
|
+
const validate = async (value, options) => {
|
|
1653
|
+
try {
|
|
1654
|
+
const builtIn = await validations.text(value, options);
|
|
1655
|
+
if (builtIn !== true) return builtIn;
|
|
1656
|
+
} catch {}
|
|
1657
|
+
return await slugProblem(typeof value === "string" ? value : "", {
|
|
1658
|
+
collection,
|
|
1659
|
+
id: options.id,
|
|
1660
|
+
isReserved,
|
|
1661
|
+
req: options.req
|
|
1662
|
+
}) ?? true;
|
|
1663
|
+
};
|
|
1664
|
+
return {
|
|
1665
|
+
name: "slug",
|
|
1666
|
+
type: "text",
|
|
1667
|
+
required: true,
|
|
1668
|
+
unique: true,
|
|
1669
|
+
admin: { description: "Path under the site root, no leading slash: \"about-us\" or \"patients/stories\"." },
|
|
1670
|
+
hooks: {
|
|
1671
|
+
beforeValidate: [({ value }) => typeof value === "string" ? normalizeSlug(value) : value],
|
|
1672
|
+
beforeChange: [async ({ data, originalDoc, req, value }) => {
|
|
1673
|
+
if (typeof value !== "string") return value;
|
|
1674
|
+
const problem = await slugProblem(value, {
|
|
1675
|
+
collection,
|
|
1676
|
+
id: originalDoc?.id ?? data?.id,
|
|
1677
|
+
isReserved,
|
|
1678
|
+
req
|
|
1679
|
+
});
|
|
1680
|
+
if (problem) throw new ValidationError({
|
|
1681
|
+
collection,
|
|
1682
|
+
errors: [{
|
|
1683
|
+
message: problem,
|
|
1684
|
+
path: "slug"
|
|
1685
|
+
}],
|
|
1686
|
+
req
|
|
1687
|
+
}, req?.t);
|
|
1688
|
+
return value;
|
|
1689
|
+
}]
|
|
1690
|
+
},
|
|
1691
|
+
validate
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
//#endregion
|
|
1695
|
+
//#region src/collections/pages.ts
|
|
1696
|
+
/**
|
|
1697
|
+
* CMS-managed pages: one required hero, then a reorderable body of sections.
|
|
1698
|
+
* The CMS owns copy and the order of sections; what a section looks like is
|
|
1699
|
+
* code-owned, which is why the blocks are an argument.
|
|
1700
|
+
*/
|
|
1701
|
+
function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, previewButton }) {
|
|
1702
|
+
const [defaultHero] = heroBlocks;
|
|
1703
|
+
if (!defaultHero) throw new Error("createPages needs at least one hero block");
|
|
1704
|
+
return {
|
|
1705
|
+
slug: "pages",
|
|
1706
|
+
admin: {
|
|
1707
|
+
useAsTitle: "title",
|
|
1708
|
+
group: "Content",
|
|
1709
|
+
defaultColumns: [
|
|
1710
|
+
"title",
|
|
1711
|
+
"slug",
|
|
1712
|
+
"_status",
|
|
1713
|
+
"updatedAt"
|
|
1714
|
+
],
|
|
1715
|
+
hidden: hideUnlessFeature("pages"),
|
|
1716
|
+
preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
|
|
1717
|
+
...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
|
|
1718
|
+
},
|
|
1719
|
+
access: {
|
|
1720
|
+
read: authenticatedOrPublished,
|
|
1721
|
+
readVersions: isAuthenticated,
|
|
1722
|
+
create: canUseFeature("pages"),
|
|
1723
|
+
update: canUseFeature("pages"),
|
|
1724
|
+
delete: isAdmin
|
|
1725
|
+
},
|
|
1726
|
+
versions: {
|
|
1727
|
+
drafts: { autosave: { interval: 375 } },
|
|
1728
|
+
maxPerDoc: 50
|
|
1729
|
+
},
|
|
1730
|
+
fields: [
|
|
1731
|
+
{
|
|
1732
|
+
name: "title",
|
|
1733
|
+
type: "text",
|
|
1734
|
+
required: true
|
|
1735
|
+
},
|
|
1736
|
+
slugField({
|
|
1737
|
+
collection: "pages",
|
|
1738
|
+
isReserved: isReservedSlug
|
|
1739
|
+
}),
|
|
1740
|
+
{
|
|
1741
|
+
name: "hero",
|
|
1742
|
+
type: "blocks",
|
|
1743
|
+
required: true,
|
|
1744
|
+
minRows: 1,
|
|
1745
|
+
maxRows: 1,
|
|
1746
|
+
blocks: heroBlocks,
|
|
1747
|
+
defaultValue: [{ blockType: defaultHero.slug }],
|
|
1748
|
+
admin: { description: "Every page opens with one hero. A page cannot be published without one." }
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
name: "layout",
|
|
1752
|
+
type: "blocks",
|
|
1753
|
+
required: true,
|
|
1754
|
+
minRows: 1,
|
|
1755
|
+
blocks: layoutBlocks,
|
|
1756
|
+
labels: {
|
|
1757
|
+
singular: "Section",
|
|
1758
|
+
plural: "Sections"
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
]
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
//#endregion
|
|
1765
|
+
//#region src/collections/users.ts
|
|
1766
|
+
const nameValidate = (value, options) => {
|
|
1767
|
+
if (options.operation === "update" && value === "" && options.previousValue === "") return true;
|
|
1768
|
+
return text(value, options);
|
|
1769
|
+
};
|
|
1770
|
+
function nameField(name) {
|
|
1771
|
+
return {
|
|
1772
|
+
name,
|
|
1773
|
+
type: "text",
|
|
1774
|
+
required: true,
|
|
1775
|
+
validate: nameValidate
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
/** Refuses the save that would remove Users from the only privileged user. */
|
|
1779
|
+
const LAST_ADMIN_DEMOTE_MESSAGE = "Make another user an admin before removing it from this one.";
|
|
1780
|
+
/** Refuses the delete that would remove the only privileged user. */
|
|
1781
|
+
const LAST_ADMIN_DELETE_MESSAGE = "Make another user an admin before deleting this one.";
|
|
1782
|
+
const LAST_ADMIN_LOCK = "select pg_advisory_xact_lock(hashtext('users:last-admin'))";
|
|
1783
|
+
async function lockLastAdminDecision(req) {
|
|
1784
|
+
const { execute, sessions } = req.payload.db;
|
|
1785
|
+
const id = req.transactionID;
|
|
1786
|
+
const session = typeof id === "string" || typeof id === "number" ? sessions?.[id] : void 0;
|
|
1787
|
+
if (!session || typeof execute !== "function") return;
|
|
1788
|
+
await execute({
|
|
1789
|
+
db: session.db,
|
|
1790
|
+
raw: LAST_ADMIN_LOCK
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
function privilegedRoles(matrix) {
|
|
1794
|
+
return matrix.filter((row) => row.users).map((row) => row.role);
|
|
1795
|
+
}
|
|
1796
|
+
function holdsPrivilegedRole(roles, matrix) {
|
|
1797
|
+
return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
|
|
1798
|
+
}
|
|
1799
|
+
function privilegedWhere(matrix) {
|
|
1800
|
+
const roles = privilegedRoles(matrix);
|
|
1801
|
+
if (roles.length === 0) return { id: { equals: "__none__" } };
|
|
1802
|
+
return { or: roles.map((role) => ({ roles: { contains: role } })) };
|
|
1803
|
+
}
|
|
1804
|
+
async function adminCount(req, excluding) {
|
|
1805
|
+
await lockLastAdminDecision(req);
|
|
1806
|
+
const holdsPrivileged = privilegedWhere(await getRolesMatrix(req));
|
|
1807
|
+
const { totalDocs } = await req.payload.count({
|
|
1808
|
+
collection: "users",
|
|
1809
|
+
overrideAccess: true,
|
|
1810
|
+
req,
|
|
1811
|
+
where: excluding === void 0 ? holdsPrivileged : { and: [holdsPrivileged, { id: { not_equals: excluding } }] }
|
|
1812
|
+
});
|
|
1813
|
+
return totalDocs;
|
|
1814
|
+
}
|
|
1815
|
+
function rolesValidate(extras) {
|
|
1816
|
+
return async (value, options) => {
|
|
1817
|
+
const matrix = await getRolesMatrix(options.req, extras);
|
|
1818
|
+
const builtIn = select(value, {
|
|
1819
|
+
...options,
|
|
1820
|
+
options: roleSelectOptions(matrix)
|
|
1821
|
+
});
|
|
1822
|
+
if (builtIn !== true) return builtIn;
|
|
1823
|
+
if (options.operation !== "update" || options.id === void 0) return true;
|
|
1824
|
+
if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
|
|
1825
|
+
return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function createUsers({ secureCookies, rolesField, extras = [] }) {
|
|
1829
|
+
const fallback = seedRoleRows(extras);
|
|
1830
|
+
return {
|
|
1831
|
+
slug: "users",
|
|
1832
|
+
auth: {
|
|
1833
|
+
maxLoginAttempts: 5,
|
|
1834
|
+
lockTime: 600 * 1e3,
|
|
1835
|
+
tokenExpiration: 7200,
|
|
1836
|
+
cookies: {
|
|
1837
|
+
sameSite: "Lax",
|
|
1838
|
+
secure: secureCookies
|
|
1839
|
+
}
|
|
1840
|
+
},
|
|
1841
|
+
admin: {
|
|
1842
|
+
group: "Settings",
|
|
1843
|
+
useAsTitle: "email",
|
|
1844
|
+
defaultColumns: [
|
|
1845
|
+
"email",
|
|
1846
|
+
"firstName",
|
|
1847
|
+
"lastName",
|
|
1848
|
+
"roles"
|
|
1849
|
+
],
|
|
1850
|
+
hidden: ({ user }) => !isAdmin(user)
|
|
1851
|
+
},
|
|
1852
|
+
access: {
|
|
1853
|
+
create: isAdmin,
|
|
1854
|
+
delete: isAdmin,
|
|
1855
|
+
unlock: isAdmin,
|
|
1856
|
+
read: isAdminOrSelf,
|
|
1857
|
+
update: isAdminOrSelf
|
|
1858
|
+
},
|
|
1859
|
+
hooks: {
|
|
1860
|
+
beforeDelete: [async ({ id, req }) => {
|
|
1861
|
+
if (!isAdmin(await req.payload.findByID({
|
|
1862
|
+
collection: "users",
|
|
1863
|
+
id,
|
|
1864
|
+
depth: 0,
|
|
1865
|
+
disableErrors: true,
|
|
1866
|
+
overrideAccess: true,
|
|
1867
|
+
req
|
|
1868
|
+
}), await getRolesMatrix(req, extras)) || await adminCount(req, id) > 0) return;
|
|
1869
|
+
throw new APIError(LAST_ADMIN_DELETE_MESSAGE, 400);
|
|
1870
|
+
}],
|
|
1871
|
+
afterOperation: [async (arg) => {
|
|
1872
|
+
const { operation, req } = arg;
|
|
1873
|
+
const touchesRoles = (operation === "update" || operation === "updateByID") && arg.args.data?.roles !== void 0;
|
|
1874
|
+
const deletes = operation === "delete" || operation === "deleteByID";
|
|
1875
|
+
if ((touchesRoles || deletes) && await adminCount(req) === 0) throw new APIError(deletes ? LAST_ADMIN_DELETE_MESSAGE : LAST_ADMIN_DEMOTE_MESSAGE, 400);
|
|
1876
|
+
return arg.result;
|
|
1877
|
+
}]
|
|
1878
|
+
},
|
|
1879
|
+
fields: [{
|
|
1880
|
+
type: "row",
|
|
1881
|
+
fields: [nameField("firstName"), nameField("lastName")]
|
|
1882
|
+
}, {
|
|
1883
|
+
name: "roles",
|
|
1884
|
+
type: "select",
|
|
1885
|
+
hasMany: true,
|
|
1886
|
+
required: true,
|
|
1887
|
+
defaultValue: ["author"],
|
|
1888
|
+
options: roleSelectOptions(fallback),
|
|
1889
|
+
admin: {
|
|
1890
|
+
components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
|
|
1891
|
+
description: "One person can hold several, e.g. Author + Designer."
|
|
1892
|
+
},
|
|
1893
|
+
access: { update: isAdmin },
|
|
1894
|
+
validate: rolesValidate(extras),
|
|
1895
|
+
hooks: { beforeValidate: [async ({ value, req }) => {
|
|
1896
|
+
if (!Array.isArray(value)) return value;
|
|
1897
|
+
return normalizeStoredRoles(value, await getRolesMatrix(req, extras));
|
|
1898
|
+
}] }
|
|
1899
|
+
}]
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
//#endregion
|
|
1903
|
+
//#region src/collections/media.ts
|
|
1904
|
+
/**
|
|
1905
|
+
* Uploads, readable by anyone: the public site serves these files. Writing is
|
|
1906
|
+
* an editorial action, deleting is not — removing an asset a live page
|
|
1907
|
+
* references breaks that page.
|
|
1908
|
+
*/
|
|
1909
|
+
function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes } = {}) {
|
|
1910
|
+
return {
|
|
1911
|
+
slug: "media",
|
|
1912
|
+
admin: { hidden: hideUnlessFeature("media") },
|
|
1913
|
+
access: {
|
|
1914
|
+
read: () => true,
|
|
1915
|
+
create: canUseFeature("media"),
|
|
1916
|
+
update: canUseFeature("media"),
|
|
1917
|
+
delete: isAdmin
|
|
1918
|
+
},
|
|
1919
|
+
upload: {
|
|
1920
|
+
staticDir,
|
|
1921
|
+
mimeTypes,
|
|
1922
|
+
imageSizes
|
|
1923
|
+
},
|
|
1924
|
+
fields: [{
|
|
1925
|
+
name: "alt",
|
|
1926
|
+
type: "text",
|
|
1927
|
+
required: true
|
|
1928
|
+
}]
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
//#endregion
|
|
1932
|
+
//#region src/plugins/admin-only-api-tab.ts
|
|
1933
|
+
function gateApiTab(entity) {
|
|
1934
|
+
const components = entity.admin?.components;
|
|
1935
|
+
const edit = components?.views?.edit;
|
|
1936
|
+
const api = edit && "api" in edit ? edit.api : void 0;
|
|
1937
|
+
return {
|
|
1938
|
+
...entity,
|
|
1939
|
+
admin: {
|
|
1940
|
+
...entity.admin,
|
|
1941
|
+
components: {
|
|
1942
|
+
...components,
|
|
1943
|
+
views: {
|
|
1944
|
+
...components?.views,
|
|
1945
|
+
edit: {
|
|
1946
|
+
...edit,
|
|
1947
|
+
api: {
|
|
1948
|
+
...api,
|
|
1949
|
+
tab: {
|
|
1950
|
+
...api?.tab,
|
|
1951
|
+
condition: isDeveloperTab
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Gates the document API tab to Developer, config-wide. After plugins that
|
|
1962
|
+
* use `definePlugin` order (MCP is 10), so a collection those plugins
|
|
1963
|
+
* register is still covered.
|
|
1964
|
+
*/
|
|
1965
|
+
const adminOnlyApiTab = definePlugin({
|
|
1966
|
+
slug: "admin-only-api-tab",
|
|
1967
|
+
order: 1e3,
|
|
1968
|
+
plugin: ({ config }) => ({
|
|
1969
|
+
...config,
|
|
1970
|
+
collections: config.collections?.map(gateApiTab),
|
|
1971
|
+
globals: config.globals?.map(gateApiTab)
|
|
1972
|
+
})
|
|
1973
|
+
});
|
|
1974
|
+
//#endregion
|
|
1975
|
+
export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOCKED_FEATURE_MESSAGE, LOOK_FIELD, MISSING_PACKAGE_FEATURES_MESSAGE, MISSING_SEED_ROLES_MESSAGE, PACKAGE_FEATURES, PACKAGE_FEATURE_SLUGS, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_FIELD_DESCRIPTION, ROLES_GLOBAL_DESCRIPTION, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_SLUG_FIELD, SHARE_IMAGE_SIZE, SYSTEM_COLOR_KEYS, THEME_APPEARANCE_FIELD, THEME_APPEARANCE_SLUG, THEME_COLORS_SLUG, THEME_COLOR_FIELD, THEME_COLOR_SCALE_FIELD, THEME_CONTRAST_REPORT, THEME_DOCUMENT_CONTROLS, THEME_FONT_FIELD, THEME_GREY_SCALE_FIELD, THEME_IDENTITY_FALLBACK, THEME_IDENTITY_SLUG, THEME_LIBRARY_FIELD, THEME_PAIRING_FIELD, THEME_PREVIEW_BREAKPOINTS, THEME_PUBLISH_FIELD, THEME_SAVE_BUTTON, THEME_SECTION_HEADING, THEME_SLUG, THEME_TYPOGRAPHY_SLUG, adminOnlyApiTab, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, documentTitle, featureCatalogue, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasRole, hideUnlessFeature, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleSlug, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, resetRolesMatrix, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
|
|
781
1976
|
|
|
782
1977
|
//# sourceMappingURL=index.mjs.map
|