@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/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 THEME_IDENTITY_FALLBACK, C as THEME_APPEARANCE_FIELD, D as THEME_DOCUMENT_CONTROLS, E as THEME_CONTRAST_REPORT, F as THEME_SECTION_HEADING, I as headingSelectValue, M as THEME_PAIRING_FIELD, N as THEME_PUBLISH_FIELD, O as THEME_FONT_FIELD, P as THEME_SAVE_BUTTON, S as LOOK_FIELD, T as THEME_COLOR_SCALE_FIELD, _ as docFromConfig, a as pageEditorLooks, b as validateSourceIncluded, c as deleteLibraryColor, d as SYSTEM_COLOR_KEYS, f as THEME_APPEARANCE_SLUG, g as THEME_TYPOGRAPHY_SLUG, h as THEME_SLUG, i as PAGE_EDITOR_SYSTEM_KEYS, j as THEME_LIBRARY_FIELD, k as THEME_GREY_SCALE_FIELD, l as rewriteColorToken, m as THEME_IDENTITY_SLUG, n as colorTokenField, o as pageEditorTokens, p as THEME_COLORS_SLUG, r as lookField, s as themeLibraryFromDoc, t as resolveThemeIdentity, u as themeColorKeys, w as THEME_COLOR_FIELD, x as validateThemeHex, y as themeConfigFromDoc } from "./identity-DZOb3_Gk.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";
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 the site's predicates; canManageBrand is the intended update predicate until BIS-43 moves src/platform here.");
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 the site's predicates; canManageBrand is the intended update predicate until BIS-43 moves src/platform here.");
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
- export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, DESCRIPTION_LENGTH, LOOK_FIELD, PAGE_EDITOR_SYSTEM_KEYS, 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, colorTokenField, createBrandAssets, createTheme, deleteLibraryColor, documentTitle, firstImageIn, lookField, noIndexField, pageEditorLooks, pageEditorTokens, persistThemeChild, publishThemeChild, resolveThemeIdentity, rewriteColorToken, sanitizeSvg, seedTheme, seoPlugin, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord };
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