@bison-lab/payload-core 3.13.0 → 3.14.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.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-C4ILz4p2.mjs";
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";
1
+ import { $ as defaultFeaturesFieldValue, A as ROLES_SLUG, B as CAPABILITIES, C as DUPLICATE_ROLE_NAME_MESSAGE, D as MISSING_SEED_ROLES_MESSAGE, E as LAST_USERS_TICK_MESSAGE, F as resetRolesMatrix, G as THEME_FEATURE_SLUGS, H as DEFAULT_ROLE_GRANTS, I as roleDescription, J as isRoleSlug, K as isRole, L as roleSelectOptions, M as defaultRolesFieldValue, N as normalizeStoredRoles, O as ROLES_FIELD_DESCRIPTION, P as parseRolesMatrix, Q as MISSING_PACKAGE_FEATURES_MESSAGE, R as seedRoleRows, S as DEVELOPER_DESCRIPTION, T as INVALID_ROLE_NAME_MESSAGE, U as ROLES, V as CAPABILITY_FEATURES, W as ROLE_LABELS, X as sanitizeRoleNameInput, Y as roleLabel, Z as slugifyRoleName, _ as isDeveloper, a as hideUnlessFeature, at as FEATURES_SLUG, b as storedRoles, c as canManageContent, ct as PACKAGE_FEATURES, d as hasCapability, dt as isFeatureSlug, et as featureCatalogue, f as hasGrant, ft as isLockedFeature, g as isAuthenticated, h as isAdminOrSelf, i as hasFeature, it as validateFeaturesMatrix, j as applyRoleSlugsFromNames, k as ROLES_GLOBAL_DESCRIPTION, l as canPublish, lt as PACKAGE_FEATURE_SLUGS, m as isAdmin, n as canUseFeature, nt as parseFeaturesMatrix, o as authenticatedOrPublished, ot as FEATURE_GROUPS, p as hasRole, pt as isPackageFeatureSlug, q as isRoleName, r as getFeaturesMatrix, rt as stampFeatureCatalogue, s as canManageBrand, st as FEATURE_GROUP_LABELS, tt as featureGroupLabel, u as getRolesMatrix, ut as isFeatureGroupId, v as isDeveloperTab, w as DUPLICATE_ROLE_SLUG_MESSAGE, x as DEFAULT_ROLE_MATRIX, y as isPrivilegedRole, z as validateRolesMatrix } from "./access-B3LglN2J.mjs";
2
+ import { n as documentTitle, r as titleTemplate, t as SHARE_IMAGE_SIZE } from "./share-image-BlFAJLnd.mjs";
3
+ 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-Dwyq8519.mjs";
3
4
  import { seoPlugin as seoPlugin$1 } from "@payloadcms/plugin-seo";
4
5
  import { catalog } from "@bison-lab/fonts";
5
6
  import { DESTRUCTIVE_SCALE_HEX, SHADE_STEPS, presetHints } from "@bison-lab/tokens";
@@ -752,6 +753,7 @@ function createTheme(options) {
752
753
  lockDocuments: false,
753
754
  admin: {
754
755
  group: "Theme",
756
+ hidden: hideUnlessFeature("theme"),
755
757
  hideAPIURL: true,
756
758
  custom: adminCustom,
757
759
  components: preview ? { elements: { beforeDocumentControls: [THEME_DOCUMENT_CONTROLS] } } : void 0
@@ -919,288 +921,27 @@ const THEME_PREVIEW_BREAKPOINTS = [
919
921
  }
920
922
  ];
921
923
  //#endregion
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
924
  //#region src/roles/fields.ts
1190
925
  const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
1191
926
  const ROLES_ROW_LABEL = "@bison-lab/payload-core/admin#RolesRowLabel";
1192
927
  const ROLE_SLUG_FIELD = "@bison-lab/payload-core/admin#RoleSlugField";
928
+ const ROLE_NAME_FIELD = "@bison-lab/payload-core/admin#RoleNameField";
1193
929
  const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
930
+ const ROLES_GRANTS_FIELD = "@bison-lab/payload-core/admin#RolesGrantsField";
1194
931
  //#endregion
1195
932
  //#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;
933
+ const roleSlugValidate = (value) => {
934
+ if (typeof value !== "string" || value === "") return true;
1199
935
  return isRoleSlug(value) || "Each role needs a unique slug.";
1200
936
  };
937
+ const roleNameValidate = (value) => {
938
+ if (typeof value !== "string" || value.trim() === "") return true;
939
+ return isRoleName(value) || "Use letters and spaces only.";
940
+ };
1201
941
  /**
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.
942
+ * Settings → Roles. Rank is the array order (Payload's drag handle).
943
+ * Ticks are released catalogue rows. Developer is implicit and shown
944
+ * only to a Developer, with every catalogue tick locked on.
1204
945
  */
1205
946
  function createRoles({ extras = [] } = {}) {
1206
947
  const rolesField = {
@@ -1215,6 +956,7 @@ function createRoles({ extras = [] } = {}) {
1215
956
  required: true,
1216
957
  defaultValue: defaultRolesFieldValue(extras),
1217
958
  validate: validateRolesMatrix,
959
+ hooks: { beforeChange: [({ value }) => applyRoleSlugsFromNames(value)] },
1218
960
  admin: {
1219
961
  components: {
1220
962
  Field: ROLES_MATRIX_FIELD,
@@ -1228,36 +970,23 @@ function createRoles({ extras = [] } = {}) {
1228
970
  name: "role",
1229
971
  type: "text",
1230
972
  label: "Slug",
1231
- required: true,
1232
973
  validate: roleSlugValidate,
1233
- hooks: { beforeChange: [({ value, previousValue }) => isRole(previousValue) ? previousValue : value] },
1234
- admin: { components: { Field: ROLE_SLUG_FIELD } }
974
+ admin: { hidden: true }
1235
975
  },
1236
976
  {
1237
977
  name: "label",
1238
978
  type: "text",
1239
979
  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"
980
+ required: true,
981
+ validate: roleNameValidate,
982
+ admin: { components: { Field: ROLE_NAME_FIELD } }
1256
983
  },
1257
984
  {
1258
- name: "users",
1259
- type: "checkbox",
1260
- label: "Users"
985
+ name: "grants",
986
+ type: "json",
987
+ label: "Grants",
988
+ defaultValue: [],
989
+ admin: { components: { Field: ROLES_GRANTS_FIELD } }
1261
990
  }
1262
991
  ]
1263
992
  };
@@ -1266,11 +995,11 @@ function createRoles({ extras = [] } = {}) {
1266
995
  label: "Roles",
1267
996
  admin: {
1268
997
  group: "Settings",
1269
- hidden: ({ user }) => !isAdmin(user),
998
+ hidden: hideUnlessFeature("roles"),
1270
999
  description: ROLES_GLOBAL_DESCRIPTION
1271
1000
  },
1272
1001
  access: {
1273
- read: (args) => isAdmin(args),
1002
+ read: (args) => canUseFeature("roles")(args),
1274
1003
  update: (args) => isAdmin(args)
1275
1004
  },
1276
1005
  fields: [rolesField]
@@ -1293,169 +1022,11 @@ async function seedRoles(payload, extras = []) {
1293
1022
  //#region src/features/fields.ts
1294
1023
  const FEATURES_MATRIX_FIELD = "@bison-lab/payload-core/admin#FeaturesMatrixField";
1295
1024
  //#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
1025
  //#region src/features/global.ts
1456
1026
  /**
1457
- * Settings → Features. Rows are the catalogue (code). Columns are the
1458
- * Roles Global. Locked rows and Developer cannot be turned off.
1027
+ * Settings → Features. One release switch per catalogue row. Off hides
1028
+ * the tick on Roles; Developer still has the feature. This screen is
1029
+ * not itself a row.
1459
1030
  */
1460
1031
  function createFeatures({ extras = [] } = {}) {
1461
1032
  const catalogue = featureCatalogue(extras);
@@ -1464,12 +1035,12 @@ function createFeatures({ extras = [] } = {}) {
1464
1035
  label: "Features",
1465
1036
  admin: {
1466
1037
  group: "Settings",
1467
- hidden: ({ user }) => !isAdmin(user),
1468
- description: "Who may use which collections and globals."
1038
+ hidden: ({ user }) => !isDeveloper(user),
1039
+ description: "Which features other people may be granted on Roles."
1469
1040
  },
1470
1041
  access: {
1471
- read: (args) => isAdmin(args),
1472
- update: (args) => isAdmin(args)
1042
+ read: ({ req }) => isDeveloper(req.user),
1043
+ update: ({ req }) => isDeveloper(req.user)
1473
1044
  },
1474
1045
  fields: [{
1475
1046
  name: "features",
@@ -1483,16 +1054,14 @@ function createFeatures({ extras = [] } = {}) {
1483
1054
  maxRows: catalogue.length,
1484
1055
  required: true,
1485
1056
  defaultValue: defaultFeaturesFieldValue(extras),
1486
- validate: async (value, { req }) => {
1487
- return validateFeaturesMatrix(value, extras, await getRolesMatrix(req));
1488
- },
1489
- hooks: { beforeChange: [async ({ value, req }) => {
1057
+ validate: (value) => validateFeaturesMatrix(value, extras),
1058
+ hooks: { beforeChange: [({ value }) => {
1490
1059
  if (!Array.isArray(value)) return value;
1491
- return enforceFeatureLocks(value, await getRolesMatrix(req));
1060
+ return stampFeatureCatalogue(value, extras);
1492
1061
  }] },
1493
1062
  admin: {
1494
1063
  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.",
1064
+ description: "Release a feature so it can be assigned on Roles. Off keeps it Developer-only.",
1496
1065
  initCollapsed: false
1497
1066
  },
1498
1067
  fields: [
@@ -1511,16 +1080,16 @@ function createFeatures({ extras = [] } = {}) {
1511
1080
  admin: { readOnly: true }
1512
1081
  },
1513
1082
  {
1514
- name: "locked",
1515
- type: "checkbox",
1516
- label: "Locked",
1083
+ name: "group",
1084
+ type: "text",
1085
+ label: "Group",
1517
1086
  admin: { hidden: true }
1518
1087
  },
1519
1088
  {
1520
- name: "roles",
1521
- type: "json",
1522
- label: "Roles",
1523
- required: true
1089
+ name: "released",
1090
+ type: "checkbox",
1091
+ label: "Released",
1092
+ admin: { hidden: true }
1524
1093
  }
1525
1094
  ]
1526
1095
  }]
@@ -1539,80 +1108,6 @@ async function seedFeatures(payload, extras = []) {
1539
1108
  });
1540
1109
  }
1541
1110
  //#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
1111
  //#region src/fields/slug.ts
1617
1112
  /**
1618
1113
  * A stored slug from whatever was typed: lowercased, with any run of
@@ -1712,15 +1207,15 @@ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, pr
1712
1207
  "_status",
1713
1208
  "updatedAt"
1714
1209
  ],
1715
- hidden: hideUnlessFeature("pages"),
1210
+ hidden: hideUnlessFeature("content"),
1716
1211
  preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
1717
1212
  ...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
1718
1213
  },
1719
1214
  access: {
1720
1215
  read: authenticatedOrPublished,
1721
1216
  readVersions: isAuthenticated,
1722
- create: canUseFeature("pages"),
1723
- update: canUseFeature("pages"),
1217
+ create: canUseFeature("content"),
1218
+ update: canUseFeature("content"),
1724
1219
  delete: isAdmin
1725
1220
  },
1726
1221
  versions: {
@@ -1791,7 +1286,7 @@ async function lockLastAdminDecision(req) {
1791
1286
  });
1792
1287
  }
1793
1288
  function privilegedRoles(matrix) {
1794
- return matrix.filter((row) => row.users).map((row) => row.role);
1289
+ return matrix.filter((row) => isPrivilegedRole(row.role, matrix)).map((row) => row.role);
1795
1290
  }
1796
1291
  function holdsPrivilegedRole(roles, matrix) {
1797
1292
  return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
@@ -1847,13 +1342,16 @@ function createUsers({ secureCookies, rolesField, extras = [] }) {
1847
1342
  "lastName",
1848
1343
  "roles"
1849
1344
  ],
1850
- hidden: ({ user }) => !isAdmin(user)
1345
+ hidden: hideUnlessFeature("users")
1851
1346
  },
1852
1347
  access: {
1853
1348
  create: isAdmin,
1854
1349
  delete: isAdmin,
1855
1350
  unlock: isAdmin,
1856
- read: isAdminOrSelf,
1351
+ read: async (args) => {
1352
+ if (await Promise.resolve(canUseFeature("users")(args))) return true;
1353
+ return isAdminOrSelf(args);
1354
+ },
1857
1355
  update: isAdminOrSelf
1858
1356
  },
1859
1357
  hooks: {
@@ -1972,6 +1470,6 @@ const adminOnlyApiTab = definePlugin({
1972
1470
  })
1973
1471
  });
1974
1472
  //#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 };
1473
+ export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, CAPABILITY_FEATURES, DEFAULT_ROLE_GRANTS, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, DUPLICATE_ROLE_NAME_MESSAGE, DUPLICATE_ROLE_SLUG_MESSAGE, FEATURES_MATRIX_FIELD, FEATURES_SLUG, FEATURE_GROUPS, FEATURE_GROUP_LABELS, INVALID_ROLE_NAME_MESSAGE, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_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_GRANTS_FIELD, ROLES_MATRIX_FIELD, ROLES_ROW_LABEL, ROLES_SLUG, ROLE_LABELS, ROLE_NAME_FIELD, 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_FEATURE_SLUGS, 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, applyRoleSlugsFromNames, authenticatedOrPublished, canManageBrand, canManageContent, canPublish, canUseFeature, colorTokenField, createBrandAssets, createFeatures, createMedia, createPages, createRoles, createTheme, createUsers, defaultFeaturesFieldValue, defaultRolesFieldValue, deleteLibraryColor, documentTitle, featureCatalogue, featureGroupLabel, findColorTokens, findColorUsages, firstImageIn, getFeaturesMatrix, getRolesMatrix, hasCapability, hasFeature, hasGrant, hasRole, hideUnlessFeature, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isFeatureGroupId, isFeatureSlug, isLockedFeature, isPackageFeatureSlug, isPrivilegedRole, isRole, isRoleName, isRoleSlug, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseFeaturesMatrix, parseRolesMatrix, persistThemeChild, publishThemeChild, resetRolesMatrix, resolveThemeIdentity, rewriteColorToken, rewriteColorTokens, rewriteColorUsages, roleDescription, roleLabel, roleSelectOptions, sanitizeRoleNameInput, sanitizeSvg, seedFeatures, seedRoleRows, seedRoles, seedTheme, seoPlugin, slugField, slugifyRoleName, stampFeatureCatalogue, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateFeaturesMatrix, validateRolesMatrix };
1976
1474
 
1977
1475
  //# sourceMappingURL=index.mjs.map