@bison-lab/payload-core 3.11.0 → 3.12.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
@@ -3,6 +3,8 @@ import { A as THEME_IDENTITY_FALLBACK, C as THEME_APPEARANCE_FIELD, D as THEME_D
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
@@ -239,7 +241,7 @@ const LIBRARY_SUCCESS_HEX = "#22c55e";
239
241
  function requireAccess$1(options) {
240
242
  const read = options.access?.read;
241
243
  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.");
244
+ if (!read || !update) throw new Error("createTheme requires access.read and access.update. Pass canManageBrand from this package as update.");
243
245
  return {
244
246
  read,
245
247
  update
@@ -674,7 +676,7 @@ const BRAND_ASSETS_MIME_TYPES = [
674
676
  function requireAccess(options) {
675
677
  const read = options.access?.read;
676
678
  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.");
679
+ if (!read || !update) throw new Error("createBrandAssets requires access.read and access.update. Pass canManageBrand from this package as update.");
678
680
  return {
679
681
  read,
680
682
  update
@@ -777,6 +779,659 @@ const THEME_PREVIEW_BREAKPOINTS = [
777
779
  }
778
780
  ];
779
781
  //#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 };
782
+ //#region src/roles/types.ts
783
+ /**
784
+ * The Roles Global a site's generated types will describe. Optional and
785
+ * nullable, no index signature: a generated Global is assignable to this,
786
+ * never the reverse.
787
+ */
788
+ const ROLES = [
789
+ "developer",
790
+ "admin",
791
+ "designer",
792
+ "author"
793
+ ];
794
+ const ROLE_LABELS = {
795
+ developer: "Developer",
796
+ admin: "Admin",
797
+ designer: "Designer",
798
+ author: "Author"
799
+ };
800
+ const CAPABILITIES = [
801
+ "content",
802
+ "brand",
803
+ "publish",
804
+ "users"
805
+ ];
806
+ function isRole(value) {
807
+ return typeof value === "string" && ROLES.includes(value);
808
+ }
809
+ //#endregion
810
+ //#region src/roles/matrix.ts
811
+ const ROLES_SLUG = "roles";
812
+ /**
813
+ * Default rank and ticks. Brand is Designer only. The API tab is not a
814
+ * column — it is locked to Developer in `isDeveloper`.
815
+ */
816
+ const DEFAULT_ROLE_MATRIX = [
817
+ {
818
+ role: "developer",
819
+ content: true,
820
+ brand: true,
821
+ publish: true,
822
+ users: true
823
+ },
824
+ {
825
+ role: "admin",
826
+ content: true,
827
+ brand: false,
828
+ publish: true,
829
+ users: true
830
+ },
831
+ {
832
+ role: "designer",
833
+ content: false,
834
+ brand: true,
835
+ publish: false,
836
+ users: false
837
+ },
838
+ {
839
+ role: "author",
840
+ content: true,
841
+ brand: false,
842
+ publish: false,
843
+ users: false
844
+ }
845
+ ];
846
+ const DEVELOPER_DESCRIPTION = "Everything Admin can do, plus the document API tab.";
847
+ const LAST_USERS_TICK_MESSAGE = "Keep Users ticked on at least one of Admin or Developer.";
848
+ const CAPABILITY_LABELS = {
849
+ content: "Content",
850
+ brand: "Brand",
851
+ publish: "Publish",
852
+ users: "Users"
853
+ };
854
+ function defaultRolesFieldValue() {
855
+ return DEFAULT_ROLE_MATRIX.map((row) => ({
856
+ id: row.role,
857
+ ...row
858
+ }));
859
+ }
860
+ function parseRolesMatrix(value) {
861
+ if (!Array.isArray(value)) return {
862
+ ok: false,
863
+ message: "Roles must be Developer, Admin, Designer, and Author."
864
+ };
865
+ const rows = [];
866
+ const seen = /* @__PURE__ */ new Set();
867
+ for (const item of value) {
868
+ if (!item || typeof item !== "object") continue;
869
+ const role = "role" in item ? item.role : void 0;
870
+ if (!isRole(role) || seen.has(role)) continue;
871
+ seen.add(role);
872
+ rows.push({
873
+ id: role,
874
+ role,
875
+ content: Boolean("content" in item && item.content),
876
+ brand: Boolean("brand" in item && item.brand),
877
+ publish: Boolean("publish" in item && item.publish),
878
+ users: Boolean("users" in item && item.users)
879
+ });
880
+ }
881
+ if (rows.length !== ROLES.length || ROLES.some((role) => !seen.has(role))) return {
882
+ ok: false,
883
+ message: "Roles must be Developer, Admin, Designer, and Author."
884
+ };
885
+ return {
886
+ ok: true,
887
+ rows
888
+ };
889
+ }
890
+ function hasPrivilegedUsersTick(rows) {
891
+ return rows.some((row) => (row.role === "admin" || row.role === "developer") && row.users);
892
+ }
893
+ function validateRolesMatrix(value) {
894
+ const parsed = parseRolesMatrix(value);
895
+ if (!parsed.ok) return parsed.message;
896
+ if (!hasPrivilegedUsersTick(parsed.rows)) return LAST_USERS_TICK_MESSAGE;
897
+ return true;
898
+ }
899
+ /**
900
+ * Developer is exclusive. Admin does not swallow Designer: both store.
901
+ */
902
+ function normalizeStoredRoles(value) {
903
+ if (!Array.isArray(value)) return [];
904
+ const roles = [...new Set(value.filter(isRole))];
905
+ if (roles.includes("developer")) return ["developer"];
906
+ return roles;
907
+ }
908
+ function roleSelectOptions(matrix = DEFAULT_ROLE_MATRIX) {
909
+ return matrix.map((row) => ({
910
+ label: ROLE_LABELS[row.role],
911
+ value: row.role
912
+ }));
913
+ }
914
+ /** Capability copy only. Developer names the API tab; nothing about MCP or seed. */
915
+ function roleDescription(role, matrix = DEFAULT_ROLE_MATRIX) {
916
+ if (role === "developer") return DEVELOPER_DESCRIPTION;
917
+ const row = matrix.find((entry) => entry.role === role);
918
+ if (!row) return "No capabilities";
919
+ return CAPABILITIES.filter((capability) => row[capability]).map((capability) => CAPABILITY_LABELS[capability]).join(", ") || "No capabilities";
920
+ }
921
+ //#endregion
922
+ //#region src/roles/access.ts
923
+ function isAccessArgs(value) {
924
+ return typeof value === "object" && value !== null && "req" in value;
925
+ }
926
+ function storedRoles(user) {
927
+ return Array.isArray(user?.roles) ? user.roles : [];
928
+ }
929
+ function hasRole(user, role) {
930
+ return storedRoles(user).includes(role);
931
+ }
932
+ /** Not a tick. True only when `developer` is stored on the row. */
933
+ function isDeveloper(user) {
934
+ return hasRole(user, "developer");
935
+ }
936
+ function hasCapability(user, capability, matrix = DEFAULT_ROLE_MATRIX) {
937
+ return storedRoles(user).some((role) => {
938
+ return matrix.find((entry) => entry.role === role)?.[capability] === true;
939
+ });
940
+ }
941
+ /**
942
+ * Reads the saved Roles Global, falling back to the seed when the row is
943
+ * empty, missing, or unreadable. Always override-access so a Designer
944
+ * evaluating Theme does not have to read Settings → Roles.
945
+ */
946
+ async function getRolesMatrix(req = {}) {
947
+ const findGlobal = req.payload?.findGlobal;
948
+ if (typeof findGlobal !== "function") return [...DEFAULT_ROLE_MATRIX];
949
+ try {
950
+ const doc = await findGlobal({
951
+ slug: ROLES_SLUG,
952
+ overrideAccess: true,
953
+ req
954
+ });
955
+ const parsed = parseRolesMatrix(doc && typeof doc === "object" && "roles" in doc ? doc.roles : void 0);
956
+ return parsed.ok ? parsed.rows : [...DEFAULT_ROLE_MATRIX];
957
+ } catch {
958
+ return [...DEFAULT_ROLE_MATRIX];
959
+ }
960
+ }
961
+ function capabilityPredicate(capability) {
962
+ function predicate(userOrArgs, matrix) {
963
+ if (isAccessArgs(userOrArgs)) {
964
+ const user = userOrArgs.req.user;
965
+ if (matrix) return hasCapability(user, capability, matrix);
966
+ return getRolesMatrix(userOrArgs.req).then((rows) => hasCapability(user, capability, rows));
967
+ }
968
+ return hasCapability(userOrArgs, capability, matrix ?? DEFAULT_ROLE_MATRIX);
969
+ }
970
+ return predicate;
971
+ }
972
+ const canManageContent = capabilityPredicate("content");
973
+ const canManageBrand = capabilityPredicate("brand");
974
+ const canPublish = capabilityPredicate("publish");
975
+ /** Users tick on any held role. */
976
+ const isAdmin = capabilityPredicate("users");
977
+ /** This role id currently has the Users tick. */
978
+ function isPrivilegedRole(role, matrix = DEFAULT_ROLE_MATRIX) {
979
+ return isRole(role) && matrix.some((row) => row.role === role && row.users);
980
+ }
981
+ function isAuthenticated(userOrArgs) {
982
+ if (isAccessArgs(userOrArgs)) return Boolean(userOrArgs.req.user);
983
+ return Boolean(userOrArgs);
984
+ }
985
+ const isAdminOrSelf = async ({ req }) => {
986
+ if (!req.user) return false;
987
+ if (await isAdmin({ req })) return true;
988
+ return { id: { equals: req.user.id } };
989
+ };
990
+ const authenticatedOrPublished = ({ req: { user } }) => {
991
+ if (isAuthenticated(user)) return true;
992
+ return { _status: { equals: "published" } };
993
+ };
994
+ /** API tab condition: Developer only, not the Users tick. */
995
+ function isDeveloperTab({ req }) {
996
+ return isDeveloper(req.user);
997
+ }
998
+ //#endregion
999
+ //#region src/roles/fields.ts
1000
+ const ROLES_MATRIX_FIELD = "@bison-lab/payload-core/admin#RolesMatrixField";
1001
+ const ROLES_FIELD = "@bison-lab/payload-core/admin#RolesField";
1002
+ //#endregion
1003
+ //#region src/roles/global.ts
1004
+ /**
1005
+ * Settings → Roles. Rank is the array order (Payload's drag handle). Ticks
1006
+ * are Content, Brand, Publish, Users. The API tab is not a column.
1007
+ */
1008
+ function createRoles() {
1009
+ return {
1010
+ slug: ROLES_SLUG,
1011
+ label: "Roles",
1012
+ admin: {
1013
+ group: "Settings",
1014
+ hidden: ({ user }) => !isAdmin(user),
1015
+ description: "Who may do what. Drag to change rank. The API tab is locked to Developer."
1016
+ },
1017
+ access: {
1018
+ read: (args) => isAdmin(args),
1019
+ update: (args) => isAdmin(args)
1020
+ },
1021
+ fields: [{
1022
+ name: "roles",
1023
+ type: "array",
1024
+ label: "Roles",
1025
+ labels: {
1026
+ singular: "Role",
1027
+ plural: "Roles"
1028
+ },
1029
+ minRows: 4,
1030
+ maxRows: 4,
1031
+ required: true,
1032
+ defaultValue: defaultRolesFieldValue(),
1033
+ validate: validateRolesMatrix,
1034
+ admin: {
1035
+ components: { Field: ROLES_MATRIX_FIELD },
1036
+ description: "Drag to change rank. Ticks are Content, Brand, Publish, and Users. The API tab is locked to Developer.",
1037
+ initCollapsed: false
1038
+ },
1039
+ fields: [
1040
+ {
1041
+ name: "role",
1042
+ type: "select",
1043
+ label: "Role",
1044
+ required: true,
1045
+ options: ROLES.map((value) => ({
1046
+ label: ROLE_LABELS[value],
1047
+ value
1048
+ })),
1049
+ admin: { readOnly: true }
1050
+ },
1051
+ {
1052
+ name: "content",
1053
+ type: "checkbox",
1054
+ label: "Content"
1055
+ },
1056
+ {
1057
+ name: "brand",
1058
+ type: "checkbox",
1059
+ label: "Brand"
1060
+ },
1061
+ {
1062
+ name: "publish",
1063
+ type: "checkbox",
1064
+ label: "Publish"
1065
+ },
1066
+ {
1067
+ name: "users",
1068
+ type: "checkbox",
1069
+ label: "Users"
1070
+ }
1071
+ ]
1072
+ }]
1073
+ };
1074
+ }
1075
+ //#endregion
1076
+ //#region src/roles/seed.ts
1077
+ /**
1078
+ * Writes the default matrix. For a site's migration `up()`. Never writes a
1079
+ * user row — seeding `developer` on a person stays a site concern.
1080
+ */
1081
+ async function seedRoles(payload) {
1082
+ return payload.updateGlobal({
1083
+ slug: ROLES_SLUG,
1084
+ data: { roles: defaultRolesFieldValue() }
1085
+ });
1086
+ }
1087
+ //#endregion
1088
+ //#region src/fields/slug.ts
1089
+ /**
1090
+ * A stored slug from whatever was typed: lowercased, with any run of
1091
+ * whitespace and slashes stripped from either end in one pass, so `/ about-us /`
1092
+ * does not keep the inner spaces a trim-then-strip would leave.
1093
+ */
1094
+ function normalizeSlug(value) {
1095
+ return value.replace(/^[\s/]+|[\s/]+$/g, "").toLowerCase();
1096
+ }
1097
+ async function slugProblem(slug, { collection, id, isReserved, req }) {
1098
+ if (!slug) return void 0;
1099
+ 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.`;
1100
+ if (typeof req?.payload?.find !== "function") return void 0;
1101
+ const where = { slug: { equals: slug } };
1102
+ if (id !== void 0) where.id = { not_equals: id };
1103
+ const query = {
1104
+ collection,
1105
+ depth: 0,
1106
+ limit: 1,
1107
+ overrideAccess: true,
1108
+ pagination: false,
1109
+ req,
1110
+ where
1111
+ };
1112
+ return (await req.payload.find(query)).docs.length > 0 || (await req.payload.find({
1113
+ ...query,
1114
+ draft: true
1115
+ })).docs.length > 0 ? `Another page is already using "/${slug}". Please choose a different address.` : void 0;
1116
+ }
1117
+ /**
1118
+ * The path a document is published at, normalised on the way in. Unique
1119
+ * across the collection. `validate` is the message under the field;
1120
+ * `beforeChange` is the enforcement on a draft save, where Payload skips
1121
+ * field validation.
1122
+ */
1123
+ function slugField({ collection, isReserved }) {
1124
+ const validate = async (value, options) => {
1125
+ try {
1126
+ const builtIn = await validations.text(value, options);
1127
+ if (builtIn !== true) return builtIn;
1128
+ } catch {}
1129
+ return await slugProblem(typeof value === "string" ? value : "", {
1130
+ collection,
1131
+ id: options.id,
1132
+ isReserved,
1133
+ req: options.req
1134
+ }) ?? true;
1135
+ };
1136
+ return {
1137
+ name: "slug",
1138
+ type: "text",
1139
+ required: true,
1140
+ unique: true,
1141
+ admin: { description: "Path under the site root, no leading slash: \"about-us\" or \"patients/stories\"." },
1142
+ hooks: {
1143
+ beforeValidate: [({ value }) => typeof value === "string" ? normalizeSlug(value) : value],
1144
+ beforeChange: [async ({ data, originalDoc, req, value }) => {
1145
+ if (typeof value !== "string") return value;
1146
+ const problem = await slugProblem(value, {
1147
+ collection,
1148
+ id: originalDoc?.id ?? data?.id,
1149
+ isReserved,
1150
+ req
1151
+ });
1152
+ if (problem) throw new ValidationError({
1153
+ collection,
1154
+ errors: [{
1155
+ message: problem,
1156
+ path: "slug"
1157
+ }],
1158
+ req
1159
+ }, req?.t);
1160
+ return value;
1161
+ }]
1162
+ },
1163
+ validate
1164
+ };
1165
+ }
1166
+ //#endregion
1167
+ //#region src/collections/pages.ts
1168
+ /**
1169
+ * CMS-managed pages: one required hero, then a reorderable body of sections.
1170
+ * The CMS owns copy and the order of sections; what a section looks like is
1171
+ * code-owned, which is why the blocks are an argument.
1172
+ */
1173
+ function createPages({ heroBlocks, isReservedSlug, layoutBlocks, previewPath, previewButton }) {
1174
+ const [defaultHero] = heroBlocks;
1175
+ if (!defaultHero) throw new Error("createPages needs at least one hero block");
1176
+ return {
1177
+ slug: "pages",
1178
+ admin: {
1179
+ useAsTitle: "title",
1180
+ group: "Content",
1181
+ defaultColumns: [
1182
+ "title",
1183
+ "slug",
1184
+ "_status",
1185
+ "updatedAt"
1186
+ ],
1187
+ preview: (doc) => previewPath(typeof doc.slug === "string" ? doc.slug : ""),
1188
+ ...previewButton ? { components: { edit: { PreviewButton: previewButton } } } : {}
1189
+ },
1190
+ access: {
1191
+ read: authenticatedOrPublished,
1192
+ readVersions: isAuthenticated,
1193
+ create: canManageContent,
1194
+ update: canManageContent,
1195
+ delete: isAdmin
1196
+ },
1197
+ versions: {
1198
+ drafts: { autosave: { interval: 375 } },
1199
+ maxPerDoc: 50
1200
+ },
1201
+ fields: [
1202
+ {
1203
+ name: "title",
1204
+ type: "text",
1205
+ required: true
1206
+ },
1207
+ slugField({
1208
+ collection: "pages",
1209
+ isReserved: isReservedSlug
1210
+ }),
1211
+ {
1212
+ name: "hero",
1213
+ type: "blocks",
1214
+ required: true,
1215
+ minRows: 1,
1216
+ maxRows: 1,
1217
+ blocks: heroBlocks,
1218
+ defaultValue: [{ blockType: defaultHero.slug }],
1219
+ admin: { description: "Every page opens with one hero. A page cannot be published without one." }
1220
+ },
1221
+ {
1222
+ name: "layout",
1223
+ type: "blocks",
1224
+ required: true,
1225
+ minRows: 1,
1226
+ blocks: layoutBlocks,
1227
+ labels: {
1228
+ singular: "Section",
1229
+ plural: "Sections"
1230
+ }
1231
+ }
1232
+ ]
1233
+ };
1234
+ }
1235
+ //#endregion
1236
+ //#region src/collections/users.ts
1237
+ const nameValidate = (value, options) => {
1238
+ if (options.operation === "update" && value === "" && options.previousValue === "") return true;
1239
+ return text(value, options);
1240
+ };
1241
+ function nameField(name) {
1242
+ return {
1243
+ name,
1244
+ type: "text",
1245
+ required: true,
1246
+ validate: nameValidate
1247
+ };
1248
+ }
1249
+ /** Refuses the save that would remove Users from the only privileged user. */
1250
+ const LAST_ADMIN_DEMOTE_MESSAGE = "Make another user an admin before removing it from this one.";
1251
+ /** Refuses the delete that would remove the only privileged user. */
1252
+ const LAST_ADMIN_DELETE_MESSAGE = "Make another user an admin before deleting this one.";
1253
+ const LAST_ADMIN_LOCK = "select pg_advisory_xact_lock(hashtext('users:last-admin'))";
1254
+ async function lockLastAdminDecision(req) {
1255
+ const { execute, sessions } = req.payload.db;
1256
+ const id = req.transactionID;
1257
+ const session = typeof id === "string" || typeof id === "number" ? sessions?.[id] : void 0;
1258
+ if (!session || typeof execute !== "function") return;
1259
+ await execute({
1260
+ db: session.db,
1261
+ raw: LAST_ADMIN_LOCK
1262
+ });
1263
+ }
1264
+ function privilegedRoles(matrix) {
1265
+ return matrix.filter((row) => row.users).map((row) => row.role);
1266
+ }
1267
+ function holdsPrivilegedRole(roles, matrix) {
1268
+ return Array.isArray(roles) && roles.some((role) => isPrivilegedRole(role, matrix));
1269
+ }
1270
+ function privilegedWhere(matrix) {
1271
+ const roles = privilegedRoles(matrix);
1272
+ if (roles.length === 0) return { id: { equals: "__none__" } };
1273
+ return { or: roles.map((role) => ({ roles: { contains: role } })) };
1274
+ }
1275
+ async function adminCount(req, excluding) {
1276
+ await lockLastAdminDecision(req);
1277
+ const holdsPrivileged = privilegedWhere(await getRolesMatrix(req));
1278
+ const { totalDocs } = await req.payload.count({
1279
+ collection: "users",
1280
+ overrideAccess: true,
1281
+ req,
1282
+ where: excluding === void 0 ? holdsPrivileged : { and: [holdsPrivileged, { id: { not_equals: excluding } }] }
1283
+ });
1284
+ return totalDocs;
1285
+ }
1286
+ const rolesValidate = async (value, options) => {
1287
+ const builtIn = select(value, options);
1288
+ if (builtIn !== true) return builtIn;
1289
+ if (options.operation !== "update" || options.id === void 0) return true;
1290
+ const matrix = await getRolesMatrix(options.req);
1291
+ if (holdsPrivilegedRole(value, matrix) || !holdsPrivilegedRole(options.previousValue, matrix)) return true;
1292
+ return await adminCount(options.req, options.id) > 0 ? true : LAST_ADMIN_DEMOTE_MESSAGE;
1293
+ };
1294
+ function createUsers({ secureCookies, rolesField }) {
1295
+ return {
1296
+ slug: "users",
1297
+ auth: {
1298
+ maxLoginAttempts: 5,
1299
+ lockTime: 600 * 1e3,
1300
+ tokenExpiration: 7200,
1301
+ cookies: {
1302
+ sameSite: "Lax",
1303
+ secure: secureCookies
1304
+ }
1305
+ },
1306
+ admin: {
1307
+ useAsTitle: "email",
1308
+ defaultColumns: [
1309
+ "email",
1310
+ "firstName",
1311
+ "lastName",
1312
+ "roles"
1313
+ ],
1314
+ hidden: ({ user }) => !isAdmin(user)
1315
+ },
1316
+ access: {
1317
+ create: isAdmin,
1318
+ delete: isAdmin,
1319
+ unlock: isAdmin,
1320
+ read: isAdminOrSelf,
1321
+ update: isAdminOrSelf
1322
+ },
1323
+ hooks: {
1324
+ beforeDelete: [async ({ id, req }) => {
1325
+ if (!isAdmin(await req.payload.findByID({
1326
+ collection: "users",
1327
+ id,
1328
+ depth: 0,
1329
+ disableErrors: true,
1330
+ overrideAccess: true,
1331
+ req
1332
+ })) || await adminCount(req, id) > 0) return;
1333
+ throw new APIError(LAST_ADMIN_DELETE_MESSAGE, 400);
1334
+ }],
1335
+ afterOperation: [async (arg) => {
1336
+ const { operation, req } = arg;
1337
+ const touchesRoles = (operation === "update" || operation === "updateByID") && arg.args.data?.roles !== void 0;
1338
+ const deletes = operation === "delete" || operation === "deleteByID";
1339
+ if ((touchesRoles || deletes) && await adminCount(req) === 0) throw new APIError(deletes ? LAST_ADMIN_DELETE_MESSAGE : LAST_ADMIN_DEMOTE_MESSAGE, 400);
1340
+ return arg.result;
1341
+ }]
1342
+ },
1343
+ fields: [{
1344
+ type: "row",
1345
+ fields: [nameField("firstName"), nameField("lastName")]
1346
+ }, {
1347
+ name: "roles",
1348
+ type: "select",
1349
+ hasMany: true,
1350
+ required: true,
1351
+ defaultValue: ["author"],
1352
+ options: roleSelectOptions(),
1353
+ admin: {
1354
+ components: { Field: rolesField ?? "@bison-lab/payload-core/admin#RolesField" },
1355
+ description: "One person can hold several, e.g. Author + Designer."
1356
+ },
1357
+ access: { update: isAdmin },
1358
+ validate: rolesValidate,
1359
+ hooks: { beforeValidate: [({ value }) => Array.isArray(value) ? normalizeStoredRoles(value) : value] }
1360
+ }]
1361
+ };
1362
+ }
1363
+ //#endregion
1364
+ //#region src/collections/media.ts
1365
+ /**
1366
+ * Uploads, readable by anyone: the public site serves these files. Writing is
1367
+ * an editorial action, deleting is not — removing an asset a live page
1368
+ * references breaks that page.
1369
+ */
1370
+ function createMedia({ staticDir = "media", mimeTypes = ["image/*"], imageSizes } = {}) {
1371
+ return {
1372
+ slug: "media",
1373
+ access: {
1374
+ read: () => true,
1375
+ create: canManageContent,
1376
+ update: canManageContent,
1377
+ delete: isAdmin
1378
+ },
1379
+ upload: {
1380
+ staticDir,
1381
+ mimeTypes,
1382
+ imageSizes
1383
+ },
1384
+ fields: [{
1385
+ name: "alt",
1386
+ type: "text",
1387
+ required: true
1388
+ }]
1389
+ };
1390
+ }
1391
+ //#endregion
1392
+ //#region src/plugins/admin-only-api-tab.ts
1393
+ function gateApiTab(entity) {
1394
+ const components = entity.admin?.components;
1395
+ const edit = components?.views?.edit;
1396
+ const api = edit && "api" in edit ? edit.api : void 0;
1397
+ return {
1398
+ ...entity,
1399
+ admin: {
1400
+ ...entity.admin,
1401
+ components: {
1402
+ ...components,
1403
+ views: {
1404
+ ...components?.views,
1405
+ edit: {
1406
+ ...edit,
1407
+ api: {
1408
+ ...api,
1409
+ tab: {
1410
+ ...api?.tab,
1411
+ condition: isDeveloperTab
1412
+ }
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Gates the document API tab to Developer, config-wide. After plugins that
1422
+ * use `definePlugin` order (MCP is 10), so a collection those plugins
1423
+ * register is still covered.
1424
+ */
1425
+ const adminOnlyApiTab = definePlugin({
1426
+ slug: "admin-only-api-tab",
1427
+ order: 1e3,
1428
+ plugin: ({ config }) => ({
1429
+ ...config,
1430
+ collections: config.collections?.map(gateApiTab),
1431
+ globals: config.globals?.map(gateApiTab)
1432
+ })
1433
+ });
1434
+ //#endregion
1435
+ export { BRAND_ASSETS_MIME_TYPES, BRAND_ASSETS_SLUG, CAPABILITIES, DEFAULT_ROLE_MATRIX, DESCRIPTION_LENGTH, DEVELOPER_DESCRIPTION, LAST_ADMIN_DELETE_MESSAGE, LAST_ADMIN_DEMOTE_MESSAGE, LAST_USERS_TICK_MESSAGE, LOOK_FIELD, PAGE_EDITOR_SYSTEM_KEYS, ROLES, ROLES_FIELD, ROLES_MATRIX_FIELD, ROLES_SLUG, ROLE_LABELS, 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, colorTokenField, createBrandAssets, createMedia, createPages, createRoles, createTheme, createUsers, defaultRolesFieldValue, deleteLibraryColor, documentTitle, firstImageIn, getRolesMatrix, hasCapability, hasRole, isAdmin, isAdminOrSelf, isAuthenticated, isDeveloper, isDeveloperTab, isPrivilegedRole, isRole, lookField, noIndexField, normalizeSlug, normalizeStoredRoles, pageEditorLooks, pageEditorTokens, parseRolesMatrix, persistThemeChild, publishThemeChild, resolveThemeIdentity, rewriteColorToken, roleDescription, roleSelectOptions, sanitizeSvg, seedRoles, seedTheme, seoPlugin, slugField, storedRoles, themeColorKeys, themeLibraryFromDoc, titleTemplate, truncateAtWord, validateRolesMatrix };
781
1436
 
782
1437
  //# sourceMappingURL=index.mjs.map