@easy-cms/core 1.16.0 → 1.18.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.
@@ -45,6 +45,99 @@ var isStaff = ({ user }) => user !== null && user.member !== true;
45
45
  var isSignedIn = ({ user }) => user !== null;
46
46
  var isAdmin = ({ user }) => user?.role === "admin" && user.scoped !== true;
47
47
 
48
+ // src/auth/tokens.ts
49
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
50
+ function newToken() {
51
+ return randomBytes(32).toString("base64url");
52
+ }
53
+ function hashToken(token) {
54
+ return createHash("sha256").update(token).digest("hex");
55
+ }
56
+ function hmac(secret, value) {
57
+ return createHmac("sha256", secret).update(value).digest("base64url");
58
+ }
59
+ function secretsOf(config) {
60
+ return [config.secret, ...config.previousSecrets ?? []];
61
+ }
62
+ function signedWith(secrets, value, signature) {
63
+ return [secrets].flat().some((secret) => safeEqual(signature, hmac(secret, value)));
64
+ }
65
+ function signToken(secret, token) {
66
+ return `${token}.${hmac(secret, `session:${token}`)}`;
67
+ }
68
+ function unsignToken(secrets, value) {
69
+ const dot = value.lastIndexOf(".");
70
+ if (dot <= 0) return void 0;
71
+ const token = value.slice(0, dot);
72
+ return signedWith(secrets, `session:${token}`, value.slice(dot + 1)) ? token : void 0;
73
+ }
74
+ function csrfToken(secret, token) {
75
+ return hmac(secret, `csrf:${token}`);
76
+ }
77
+ function csrfMatches(secrets, token, given) {
78
+ return signedWith(secrets, `csrf:${token}`, given);
79
+ }
80
+ function safeEqual(a, b) {
81
+ const left = Buffer.from(a);
82
+ const right = Buffer.from(b);
83
+ return left.length === right.length && timingSafeEqual(left, right);
84
+ }
85
+ function signPreviewToken(secret, target, expiresAt, user) {
86
+ const payload = Buffer.from(
87
+ JSON.stringify({
88
+ ..."global" in target ? { g: target.global } : { c: target.collection, i: target.id },
89
+ e: expiresAt,
90
+ ...user !== void 0 ? { u: user } : {}
91
+ })
92
+ ).toString("base64url");
93
+ return `${payload}.${hmac(secret, `preview:${payload}`)}`;
94
+ }
95
+ function verifyPreviewToken(secrets, token, now = Date.now()) {
96
+ const dot = token.lastIndexOf(".");
97
+ if (dot <= 0) return null;
98
+ const payload = token.slice(0, dot);
99
+ if (!signedWith(secrets, `preview:${payload}`, token.slice(dot + 1))) return null;
100
+ try {
101
+ const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
102
+ if (typeof data.e !== "number" || data.e <= now) return null;
103
+ const user = typeof data.u === "string" ? { user: data.u } : {};
104
+ if (typeof data.g === "string") return { global: data.g, expiresAt: data.e, ...user };
105
+ if (typeof data.c === "string" && typeof data.i === "string")
106
+ return { collection: data.c, id: data.i, expiresAt: data.e, ...user };
107
+ return null;
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ function passwordFingerprint(passwordHash) {
113
+ return createHash("sha256").update(typeof passwordHash === "string" ? passwordHash : "no-password").digest("base64url").slice(0, 22);
114
+ }
115
+ function signPasswordToken(secret, args) {
116
+ const payload = Buffer.from(
117
+ JSON.stringify({ u: args.userId, p: args.purpose, e: args.expiresAt })
118
+ ).toString("base64url");
119
+ return `${payload}.${hmac(secret, `password:${payload}:${args.fingerprint}`)}`;
120
+ }
121
+ function readPasswordToken(token) {
122
+ const dot = token.lastIndexOf(".");
123
+ if (dot <= 0) return null;
124
+ const payload = token.slice(0, dot);
125
+ try {
126
+ const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
127
+ if (typeof data.u !== "string" || typeof data.e !== "number") return null;
128
+ if (data.p !== "reset" && data.p !== "invite" && data.p !== "verify") return null;
129
+ return { userId: data.u, purpose: data.p, expiresAt: data.e, payload };
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ function passwordTokenMatches(secrets, token, fingerprint) {
135
+ const dot = token.lastIndexOf(".");
136
+ if (dot <= 0) return false;
137
+ const payload = token.slice(0, dot);
138
+ return signedWith(secrets, `password:${payload}:${fingerprint}`, token.slice(dot + 1));
139
+ }
140
+
48
141
  // src/builtins.ts
49
142
  var USERS = "users";
50
143
  var MEDIA = "media";
@@ -563,7 +656,7 @@ var emailDeliveriesCollection = {
563
656
 
564
657
  // src/audit.ts
565
658
  import { AsyncLocalStorage } from "async_hooks";
566
- import { createHmac } from "crypto";
659
+ import { createHmac as createHmac2 } from "crypto";
567
660
  var DEFAULT_AUDIT_KEEP = 365;
568
661
  var DEFAULT_FAILED_LOGINS = 20;
569
662
  var MAX_VALUE = 500;
@@ -778,7 +871,7 @@ var AuditLog = class {
778
871
  });
779
872
  for (const row of result.docs) {
780
873
  checked++;
781
- if (row.signature !== this.sign(row)) invalid.push(row.id);
874
+ if (!this.signer(row)) invalid.push(row.id);
782
875
  const id = Number(row.id);
783
876
  if (previous !== void 0 && Number.isInteger(id) && id > previous + 1)
784
877
  gaps += id - previous - 1;
@@ -797,6 +890,49 @@ var AuditLog = class {
797
890
  );
798
891
  return verification;
799
892
  }
893
+ /** The secret an entry was signed with: the current one, a previous one, or none (changed). */
894
+ signer(row) {
895
+ return secretsOf(this.cms.config).find((secret) => row.signature === this.sign(row, secret));
896
+ }
897
+ /**
898
+ * After `secret` changed: signs again, with the current secret, the entries signed with one of
899
+ * `previousSecrets`, so they still verify once it is removed. Entries no secret verifies were
900
+ * changed: they are left as they are, and reported. Records `audit.resign`.
901
+ */
902
+ async resign() {
903
+ const invalid = [];
904
+ let checked = 0;
905
+ let resigned = 0;
906
+ for (let page = 1; ; page++) {
907
+ const result = await this.cms.db.find({
908
+ collection: AUDIT_LOGS,
909
+ sort: ["id"],
910
+ limit: 500,
911
+ page
912
+ });
913
+ for (const row of result.docs) {
914
+ checked++;
915
+ const secret = this.signer(row);
916
+ if (secret === void 0) invalid.push(row.id);
917
+ else if (secret !== this.cms.config.secret) {
918
+ const { id, ...data } = row;
919
+ await this.cms.db.update({
920
+ collection: AUDIT_LOGS,
921
+ id,
922
+ data: { ...data, signature: this.sign(row) }
923
+ });
924
+ resigned++;
925
+ }
926
+ }
927
+ if (page >= result.totalPages) break;
928
+ }
929
+ await this.record({
930
+ action: "audit.resign",
931
+ target: "audit",
932
+ detail: { checked, resigned, invalid: invalid.length }
933
+ });
934
+ return { checked, resigned, invalid };
935
+ }
800
936
  /**
801
937
  * Daily upkeep from `runJobs`: deletes entries older than `keep` days, then verifies the log.
802
938
  * Runs when the last check is more than a day old.
@@ -847,11 +983,11 @@ var AuditLog = class {
847
983
  const scope = this.settings.scope;
848
984
  return (scope && context ? scope({ context, user }) : null) ?? null;
849
985
  }
850
- sign(row) {
986
+ sign(row, secret = this.cms.config.secret) {
851
987
  const values = SIGNED.map((key) => row[key] ?? null);
852
988
  if (typeof row.scope === "string" && row.scope) values.push(row.scope);
853
989
  const payload = stable(values);
854
- return createHmac("sha256", this.cms.config.secret).update(`audit:${payload}`).digest("base64url");
990
+ return createHmac2("sha256", secret).update(`audit:${payload}`).digest("base64url");
855
991
  }
856
992
  async lastVerification() {
857
993
  const { docs } = await this.cms.db.find({
@@ -903,7 +1039,7 @@ function toEntry(row) {
903
1039
  }
904
1040
 
905
1041
  // src/api-keys.ts
906
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
1042
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
907
1043
  var API_KEYS = "api-keys";
908
1044
  var API_KEY_OPERATIONS = ["read", "create", "update", "delete", "publish"];
909
1045
  var API_KEY_GLOBAL_OPERATIONS = ["read", "update", "publish"];
@@ -913,12 +1049,12 @@ function isApiKey(token) {
913
1049
  return token.startsWith("ecms_");
914
1050
  }
915
1051
  function newApiKey() {
916
- const prefix = randomBytes(4).toString("hex");
917
- const secret = randomBytes(32).toString("base64url");
1052
+ const prefix = randomBytes2(4).toString("hex");
1053
+ const secret = randomBytes2(32).toString("base64url");
918
1054
  return { key: `ecms_${prefix}_${secret}`, prefix, hash: hashSecret(secret) };
919
1055
  }
920
1056
  function hashSecret(secret) {
921
- return createHash("sha256").update(secret).digest("hex");
1057
+ return createHash2("sha256").update(secret).digest("hex");
922
1058
  }
923
1059
  function parseApiKey(token) {
924
1060
  const match = KEY_PATTERN.exec(token);
@@ -928,7 +1064,7 @@ function secretMatches(secret, hash) {
928
1064
  if (typeof hash !== "string") return false;
929
1065
  const a = Buffer.from(hashSecret(secret), "hex");
930
1066
  const b = Buffer.from(hash, "hex");
931
- return a.length === b.length && timingSafeEqual(a, b);
1067
+ return a.length === b.length && timingSafeEqual2(a, b);
932
1068
  }
933
1069
  function keyAllows(user, target, operation) {
934
1070
  const key = user?.apiKey;
@@ -1035,20 +1171,23 @@ function cleanPermissions(value, known) {
1035
1171
  }
1036
1172
 
1037
1173
  // src/spam.ts
1038
- import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1174
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
1039
1175
  var MAX_AGE = 24 * 36e5;
1040
- var sign = (secret, value) => createHmac2("sha256", secret).update(value).digest("base64url");
1176
+ var sign = (secret, value) => createHmac3("sha256", secret).update(value).digest("base64url");
1041
1177
  function formToken(secret, purpose, now = Date.now()) {
1042
1178
  return `${now}.${sign(secret, `${purpose}:${now}`)}`;
1043
1179
  }
1044
- function checkFormToken(secret, purpose, token, minTime, now = Date.now()) {
1180
+ function checkFormToken(secrets, purpose, token, minTime, now = Date.now()) {
1045
1181
  if (typeof token !== "string") return "invalid";
1046
1182
  const [time, signature] = token.split(".");
1047
1183
  const issued = Number(time);
1048
1184
  if (!Number.isFinite(issued) || !signature) return "invalid";
1049
- const expected = Buffer.from(sign(secret, `${purpose}:${issued}`));
1050
1185
  const given = Buffer.from(signature);
1051
- if (expected.length !== given.length || !timingSafeEqual2(expected, given)) return "invalid";
1186
+ const genuine = [secrets].flat().some((secret) => {
1187
+ const expected = Buffer.from(sign(secret, `${purpose}:${issued}`));
1188
+ return expected.length === given.length && timingSafeEqual3(expected, given);
1189
+ });
1190
+ if (!genuine) return "invalid";
1052
1191
  if (now - issued < minTime) return "too-fast";
1053
1192
  if (now - issued > MAX_AGE) return "expired";
1054
1193
  return "ok";
@@ -1079,10 +1218,10 @@ async function verifyTurnstile(secretKey, response, ip, fetcher = fetch) {
1079
1218
  // src/auth/cookie.ts
1080
1219
  var SESSION_COOKIE = "ecms-session";
1081
1220
  var hostCookie = (name) => `__Host-${name}`;
1082
- function cookieValue(header, name) {
1083
- if (!header) return void 0;
1221
+ function cookieValue(header2, name) {
1222
+ if (!header2) return void 0;
1084
1223
  let plain;
1085
- for (const part of header.split(";")) {
1224
+ for (const part of header2.split(";")) {
1086
1225
  const eq = part.indexOf("=");
1087
1226
  if (eq <= 0) continue;
1088
1227
  const key = part.slice(0, eq).trim();
@@ -1172,7 +1311,7 @@ var DEFAULT_PASSWORD_EMAILS = {
1172
1311
  };
1173
1312
 
1174
1313
  // src/auth/password.ts
1175
- import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual3 } from "crypto";
1314
+ import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual4 } from "crypto";
1176
1315
  var MIN_PASSWORD_LENGTH = 8;
1177
1316
  var N = 2 ** 17;
1178
1317
  var R = 8;
@@ -1191,7 +1330,7 @@ function derive(password, salt, n, r, p, length) {
1191
1330
  });
1192
1331
  }
1193
1332
  async function hashPassword(password) {
1194
- const salt = randomBytes2(16);
1333
+ const salt = randomBytes3(16);
1195
1334
  const key = await derive(password, salt, N, R, P, KEY_LENGTH);
1196
1335
  return ["scrypt", N, R, P, salt.toString("base64url"), key.toString("base64url")].join("$");
1197
1336
  }
@@ -1207,7 +1346,7 @@ async function verifyPassword(password, stored) {
1207
1346
  Number(p),
1208
1347
  expected.length
1209
1348
  );
1210
- return key.length === expected.length && timingSafeEqual3(key, expected);
1349
+ return key.length === expected.length && timingSafeEqual4(key, expected);
1211
1350
  }
1212
1351
  var dummyHash;
1213
1352
  async function fakeVerify(password) {
@@ -1218,92 +1357,6 @@ async function fakeVerify(password) {
1218
1357
 
1219
1358
  // src/auth/sso.ts
1220
1359
  import { createHmac as createHmac4, randomBytes as randomBytes4 } from "crypto";
1221
-
1222
- // src/auth/tokens.ts
1223
- import { createHash as createHash2, createHmac as createHmac3, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual4 } from "crypto";
1224
- function newToken() {
1225
- return randomBytes3(32).toString("base64url");
1226
- }
1227
- function hashToken(token) {
1228
- return createHash2("sha256").update(token).digest("hex");
1229
- }
1230
- function hmac(secret, value) {
1231
- return createHmac3("sha256", secret).update(value).digest("base64url");
1232
- }
1233
- function signToken(secret, token) {
1234
- return `${token}.${hmac(secret, `session:${token}`)}`;
1235
- }
1236
- function unsignToken(secret, value) {
1237
- const dot = value.lastIndexOf(".");
1238
- if (dot <= 0) return void 0;
1239
- const token = value.slice(0, dot);
1240
- return safeEqual(value.slice(dot + 1), hmac(secret, `session:${token}`)) ? token : void 0;
1241
- }
1242
- function csrfToken(secret, token) {
1243
- return hmac(secret, `csrf:${token}`);
1244
- }
1245
- function safeEqual(a, b) {
1246
- const left = Buffer.from(a);
1247
- const right = Buffer.from(b);
1248
- return left.length === right.length && timingSafeEqual4(left, right);
1249
- }
1250
- function signPreviewToken(secret, target, expiresAt, user) {
1251
- const payload = Buffer.from(
1252
- JSON.stringify({
1253
- ..."global" in target ? { g: target.global } : { c: target.collection, i: target.id },
1254
- e: expiresAt,
1255
- ...user !== void 0 ? { u: user } : {}
1256
- })
1257
- ).toString("base64url");
1258
- return `${payload}.${hmac(secret, `preview:${payload}`)}`;
1259
- }
1260
- function verifyPreviewToken(secret, token, now = Date.now()) {
1261
- const dot = token.lastIndexOf(".");
1262
- if (dot <= 0) return null;
1263
- const payload = token.slice(0, dot);
1264
- if (!safeEqual(token.slice(dot + 1), hmac(secret, `preview:${payload}`))) return null;
1265
- try {
1266
- const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
1267
- if (typeof data.e !== "number" || data.e <= now) return null;
1268
- const user = typeof data.u === "string" ? { user: data.u } : {};
1269
- if (typeof data.g === "string") return { global: data.g, expiresAt: data.e, ...user };
1270
- if (typeof data.c === "string" && typeof data.i === "string")
1271
- return { collection: data.c, id: data.i, expiresAt: data.e, ...user };
1272
- return null;
1273
- } catch {
1274
- return null;
1275
- }
1276
- }
1277
- function passwordFingerprint(passwordHash) {
1278
- return createHash2("sha256").update(typeof passwordHash === "string" ? passwordHash : "no-password").digest("base64url").slice(0, 22);
1279
- }
1280
- function signPasswordToken(secret, args) {
1281
- const payload = Buffer.from(
1282
- JSON.stringify({ u: args.userId, p: args.purpose, e: args.expiresAt })
1283
- ).toString("base64url");
1284
- return `${payload}.${hmac(secret, `password:${payload}:${args.fingerprint}`)}`;
1285
- }
1286
- function readPasswordToken(token) {
1287
- const dot = token.lastIndexOf(".");
1288
- if (dot <= 0) return null;
1289
- const payload = token.slice(0, dot);
1290
- try {
1291
- const data = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
1292
- if (typeof data.u !== "string" || typeof data.e !== "number") return null;
1293
- if (data.p !== "reset" && data.p !== "invite" && data.p !== "verify") return null;
1294
- return { userId: data.u, purpose: data.p, expiresAt: data.e, payload };
1295
- } catch {
1296
- return null;
1297
- }
1298
- }
1299
- function passwordTokenMatches(secret, token, fingerprint) {
1300
- const dot = token.lastIndexOf(".");
1301
- if (dot <= 0) return false;
1302
- const payload = token.slice(0, dot);
1303
- return safeEqual(token.slice(dot + 1), hmac(secret, `password:${payload}:${fingerprint}`));
1304
- }
1305
-
1306
- // src/auth/sso.ts
1307
1360
  var SSO_COOKIE = "ecms-sso";
1308
1361
  var PENDING_TTL = 10 * 6e4;
1309
1362
  var random = () => randomBytes4(32).toString("base64url");
@@ -1314,12 +1367,13 @@ function seal(secret, pending) {
1314
1367
  const payload = Buffer.from(JSON.stringify(pending)).toString("base64url");
1315
1368
  return `${payload}.${sign2(secret, payload)}`;
1316
1369
  }
1317
- function unseal(secret, value) {
1370
+ function unseal(secrets, value) {
1318
1371
  if (!value) return null;
1319
1372
  const dot = value.lastIndexOf(".");
1320
1373
  if (dot <= 0) return null;
1321
1374
  const payload = value.slice(0, dot);
1322
- if (!safeEqual(value.slice(dot + 1), sign2(secret, payload))) return null;
1375
+ const signature = value.slice(dot + 1);
1376
+ if (![secrets].flat().some((secret) => safeEqual(signature, sign2(secret, payload)))) return null;
1323
1377
  try {
1324
1378
  const pending = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
1325
1379
  return typeof pending.e === "number" && pending.e > Date.now() ? pending : null;
@@ -1387,7 +1441,7 @@ var SingleSignOn = class {
1387
1441
  */
1388
1442
  async finish(providerId, options) {
1389
1443
  const admin = this.config.admin.path.replace(/\/+$/, "");
1390
- const pending = unseal(this.config.secret, options.cookie);
1444
+ const pending = unseal(secretsOf(this.config), options.cookie);
1391
1445
  let email2 = null;
1392
1446
  const back = async (page2, outcome) => {
1393
1447
  await this.cms.audit.record({
@@ -1839,7 +1893,7 @@ var Auth = class {
1839
1893
  await this.recordFailure(key);
1840
1894
  throw refuse("form", "could not be sent");
1841
1895
  }
1842
- const checked = checkFormToken(this.config.secret, SIGNUP_FORM, args.token, SIGNUP_MIN_TIME);
1896
+ const checked = checkFormToken(this.secrets, SIGNUP_FORM, args.token, SIGNUP_MIN_TIME);
1843
1897
  if (checked !== "ok")
1844
1898
  throw refuse(
1845
1899
  "form",
@@ -1899,7 +1953,7 @@ var Auth = class {
1899
1953
  const id = claims && (/^\d+$/.test(claims.userId) ? Number(claims.userId) : claims.userId);
1900
1954
  const user = claims && claims.purpose === "verify" && claims.expiresAt > Date.now() && id !== null ? await this.db.findById({ collection: USERS, id }) : null;
1901
1955
  if (!user || user.active === false || // A link confirms once: it doesn't sign in again afterwards.
1902
- user.emailVerified !== false || !passwordTokenMatches(this.config.secret, token, passwordFingerprint(user.passwordHash))) {
1956
+ user.emailVerified !== false || !passwordTokenMatches(this.secrets, token, passwordFingerprint(user.passwordHash))) {
1903
1957
  await this.recordFailure(key);
1904
1958
  throw new ValidationError(USERS, [
1905
1959
  { field: "token", message: "This link has expired or is not valid" }
@@ -1969,7 +2023,7 @@ var Auth = class {
1969
2023
  }
1970
2024
  /** Ends the session for a signed token. Unknown tokens are ignored. */
1971
2025
  async logout(signedToken) {
1972
- const token = unsignToken(this.config.secret, signedToken);
2026
+ const token = unsignToken(this.secrets, signedToken);
1973
2027
  if (!token) return;
1974
2028
  const session = await this.findSession(token);
1975
2029
  if (!session) return;
@@ -1999,7 +2053,7 @@ var Auth = class {
1999
2053
  async verify(signedToken) {
2000
2054
  if (!signedToken) return null;
2001
2055
  if (isApiKey(signedToken)) return this.verifyApiKey(signedToken);
2002
- const token = unsignToken(this.config.secret, signedToken);
2056
+ const token = unsignToken(this.secrets, signedToken);
2003
2057
  if (!token) return null;
2004
2058
  const session = await this.findSession(token);
2005
2059
  if (!session) return null;
@@ -2019,9 +2073,21 @@ var Auth = class {
2019
2073
  }
2020
2074
  /** The CSRF token clients must echo for a signed session token. @internal */
2021
2075
  csrfFor(signedToken) {
2022
- const token = unsignToken(this.config.secret, signedToken);
2076
+ const token = unsignToken(this.secrets, signedToken);
2023
2077
  return token ? csrfToken(this.config.secret, token) : void 0;
2024
2078
  }
2079
+ /**
2080
+ * Whether `given` is the session's CSRF token, made with the current secret or a previous one
2081
+ * (a client that fetched it before the secret changed). @internal
2082
+ */
2083
+ csrfMatches(signedToken, given) {
2084
+ const token = unsignToken(this.secrets, signedToken);
2085
+ return token !== void 0 && csrfMatches(this.secrets, token, given);
2086
+ }
2087
+ /** The current secret and `previousSecrets`, which still verify. */
2088
+ get secrets() {
2089
+ return secretsOf(this.config);
2090
+ }
2025
2091
  /** Starts a new session for a user, e.g. after they changed their own password. */
2026
2092
  async createSession(userId) {
2027
2093
  const user = await this.db.findById({ collection: USERS, id: userId });
@@ -2144,7 +2210,7 @@ var Auth = class {
2144
2210
  const user = await this.db.findById({ collection: USERS, id });
2145
2211
  if (!user || user.active === false || !this.sso.passwordAllowed(user)) return null;
2146
2212
  const fingerprint = passwordFingerprint(user.passwordHash);
2147
- if (!passwordTokenMatches(this.config.secret, token, fingerprint)) return null;
2213
+ if (!passwordTokenMatches(this.secrets, token, fingerprint)) return null;
2148
2214
  return { user, purpose: claims.purpose };
2149
2215
  }
2150
2216
  async mailPasswordLink(user, purpose, options) {
@@ -3911,7 +3977,7 @@ var VersionStore = class {
3911
3977
  };
3912
3978
 
3913
3979
  // src/local-api.ts
3914
- import { createHash as createHash5, createHmac as createHmac6, randomBytes as randomBytes6, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual5 } from "crypto";
3980
+ import { createHash as createHash5, createHmac as createHmac7, randomBytes as randomBytes6, randomUUID as randomUUID3 } from "crypto";
3915
3981
 
3916
3982
  // src/auth/common-passwords.ts
3917
3983
  var LIST = `
@@ -4647,7 +4713,7 @@ function read(response, maxBytes, deadline) {
4647
4713
 
4648
4714
  // src/config-keys.ts
4649
4715
  var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
4650
- var CONFIG = keys(`secret db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
4716
+ var CONFIG = keys(`secret previousSecrets db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
4651
4717
  upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
4652
4718
  audit logs countCacheSeconds maxPage httpCache outbox search telemetry plugins`);
4653
4719
  var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
@@ -5138,7 +5204,41 @@ var SearchIndex = class {
5138
5204
  };
5139
5205
 
5140
5206
  // src/webhooks.ts
5141
- import { createHmac as createHmac5, randomUUID as randomUUID2 } from "crypto";
5207
+ import { createHmac as createHmac6, randomUUID as randomUUID2 } from "crypto";
5208
+
5209
+ // src/webhook-signature.ts
5210
+ import { createHmac as createHmac5, timingSafeEqual as timingSafeEqual5 } from "crypto";
5211
+ var TOLERANCE_SECONDS = 5 * 60;
5212
+ function signWebhook(secret, delivery, timestamp, body) {
5213
+ return `v1=${createHmac5("sha256", secret).update(`${delivery}.${timestamp}.${body}`).digest("hex")}`;
5214
+ }
5215
+ function header(headers, name) {
5216
+ if (headers instanceof Headers) return headers.get(name) ?? void 0;
5217
+ const value = Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
5218
+ return Array.isArray(value) ? value[0] : value;
5219
+ }
5220
+ function equal(a, b) {
5221
+ const left = Buffer.from(a);
5222
+ const right = Buffer.from(b);
5223
+ return left.length === right.length && timingSafeEqual5(left, right);
5224
+ }
5225
+ function verifyWebhook(body, headers, secret, options = {}) {
5226
+ const delivery = header(headers, "x-easy-cms-delivery");
5227
+ const timestamp = header(headers, "x-easy-cms-timestamp");
5228
+ const signatures2 = header(headers, "x-easy-cms-signature-v2");
5229
+ if (!delivery || !timestamp || !signatures2)
5230
+ throw new UnauthorizedError("Webhook signature missing");
5231
+ const seconds = Number(timestamp);
5232
+ const now = (options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3;
5233
+ if (!/^\d+$/.test(timestamp) || Math.abs(now - seconds) > (options.toleranceSeconds ?? TOLERANCE_SECONDS))
5234
+ throw new UnauthorizedError("Webhook timestamp too old or in the future");
5235
+ const sent = signatures2.split(/[\s,]+/).filter((s) => s.startsWith("v1="));
5236
+ const valid = [secret].flat().some((s) => sent.some((given) => equal(given, signWebhook(s, delivery, seconds, body))));
5237
+ if (!valid) throw new UnauthorizedError("Webhook signature does not match");
5238
+ return JSON.parse(body);
5239
+ }
5240
+
5241
+ // src/webhooks.ts
5142
5242
  var WEBHOOK_EVENTS = [
5143
5243
  "create",
5144
5244
  "update",
@@ -5308,7 +5408,10 @@ var Webhooks = class {
5308
5408
  ...hook.headers
5309
5409
  };
5310
5410
  if (hook.secret) {
5311
- headers["x-easy-cms-signature"] = `sha256=${createHmac5("sha256", hook.secret).update(body).digest("hex")}`;
5411
+ const timestamp = Math.floor(Date.now() / 1e3);
5412
+ headers["x-easy-cms-timestamp"] = String(timestamp);
5413
+ headers["x-easy-cms-signature-v2"] = signWebhook(hook.secret, delivery, timestamp, body);
5414
+ headers["x-easy-cms-signature"] = `sha256=${createHmac6("sha256", hook.secret).update(body).digest("hex")}`;
5312
5415
  }
5313
5416
  try {
5314
5417
  const response = await this.fetcher(hook.url, {
@@ -5380,6 +5483,7 @@ function validateConfig(config) {
5380
5483
  return issues;
5381
5484
  }
5382
5485
  validateSecret(config.secret, add);
5486
+ validatePreviousSecrets(config.previousSecrets, config.secret, add);
5383
5487
  validateCronSecret(config.cronSecret, (issue) => issues.push(issue));
5384
5488
  checkConfigKeys(config, (issue) => issues.push(issue));
5385
5489
  validateAdapterVersions(config, add);
@@ -5465,6 +5569,22 @@ function validateSecret(secret, add) {
5465
5569
  add("secret", "looks like an example or a repeated pattern, not a random string", hint);
5466
5570
  }
5467
5571
  }
5572
+ function validatePreviousSecrets(value, secret, add) {
5573
+ if (value === void 0) return;
5574
+ if (!Array.isArray(value)) {
5575
+ add("previousSecrets", "must be an array of strings", "e.g. previousSecrets: [oldSecret]");
5576
+ return;
5577
+ }
5578
+ for (const [i, previous] of value.entries()) {
5579
+ if (typeof previous !== "string" || previous.length < MIN_SECRET_LENGTH)
5580
+ add(
5581
+ `previousSecrets[${i}]`,
5582
+ `must be a string of at least ${MIN_SECRET_LENGTH} characters, like secret`
5583
+ );
5584
+ else if (previous === secret)
5585
+ add(`previousSecrets[${i}]`, "is the same as secret", "list only secrets used before it");
5586
+ }
5587
+ }
5468
5588
  function validateCronSecret(value, push) {
5469
5589
  const hint = `use a random string of at least ${MIN_SECRET_LENGTH} characters, e.g. \`openssl rand -hex 32\``;
5470
5590
  if (value !== void 0) {
@@ -7025,11 +7145,11 @@ function cacheTimes(config, container) {
7025
7145
  staleWhileRevalidate: own?.staleWhileRevalidate ?? config.httpCache?.staleWhileRevalidate ?? 300
7026
7146
  };
7027
7147
  }
7028
- function matches2(header, etag) {
7029
- if (!header) return false;
7030
- if (header.trim() === "*") return true;
7148
+ function matches2(header2, etag) {
7149
+ if (!header2) return false;
7150
+ if (header2.trim() === "*") return true;
7031
7151
  const bare = (tag) => tag.trim().replace(/^W\//, "");
7032
- return header.split(",").some((tag) => bare(tag) === bare(etag));
7152
+ return header2.split(",").some((tag) => bare(tag) === bare(etag));
7033
7153
  }
7034
7154
  function cacheableResponse(request, body, status, headers, times, tags) {
7035
7155
  headers.set(
@@ -7046,7 +7166,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
7046
7166
  }
7047
7167
 
7048
7168
  // src/version.ts
7049
- var VERSION = true ? "1.16.0" : "0.0.0-dev";
7169
+ var VERSION = true ? "1.18.0" : "0.0.0-dev";
7050
7170
 
7051
7171
  // src/telemetry.ts
7052
7172
  var DB_CALLS = /* @__PURE__ */ new Set([
@@ -7710,18 +7830,14 @@ var EasyCMS = class {
7710
7830
  /** A value with its HMAC (`secret`), for `purpose` only: `<base64url JSON>.<signature>`. */
7711
7831
  sign(purpose, value) {
7712
7832
  const payload = Buffer.from(JSON.stringify(value)).toString("base64url");
7713
- const mac = createHmac6("sha256", this.config.secret).update(`${purpose}:${payload}`);
7833
+ const mac = createHmac7("sha256", this.config.secret).update(`${purpose}:${payload}`);
7714
7834
  return `${payload}.${mac.digest("base64url")}`;
7715
7835
  }
7716
7836
  /** The value of a token made by `sign`, or `undefined` when it isn't genuine. */
7717
7837
  verify(purpose, token) {
7718
7838
  const [payload, signature] = String(token).split(".");
7719
7839
  if (!payload || !signature) return void 0;
7720
- const expected = Buffer.from(
7721
- createHmac6("sha256", this.config.secret).update(`${purpose}:${payload}`).digest("base64url")
7722
- );
7723
- const given = Buffer.from(signature);
7724
- if (expected.length !== given.length || !timingSafeEqual5(expected, given)) return void 0;
7840
+ if (!signedWith(secretsOf(this.config), `${purpose}:${payload}`, signature)) return void 0;
7725
7841
  try {
7726
7842
  return JSON.parse(Buffer.from(payload, "base64url").toString());
7727
7843
  } catch {
@@ -7838,12 +7954,10 @@ var EasyCMS = class {
7838
7954
  verifyMediaSignature(key, expires, signature) {
7839
7955
  const at = Number(expires);
7840
7956
  if (!signature || !Number.isInteger(at) || at < Date.now() / 1e3) return false;
7841
- const expected = Buffer.from(this.mediaSignature(key, at));
7842
- const given = Buffer.from(signature);
7843
- return expected.length === given.length && timingSafeEqual5(expected, given);
7957
+ return signedWith(secretsOf(this.config), `media:${key}:${at}`, signature);
7844
7958
  }
7845
7959
  mediaSignature(key, expires) {
7846
- return createHmac6("sha256", this.config.secret).update(`media:${key}:${expires}`).digest("base64url");
7960
+ return createHmac7("sha256", this.config.secret).update(`media:${key}:${expires}`).digest("base64url");
7847
7961
  }
7848
7962
  /**
7849
7963
  * How many documents use any of these files in a top-level upload field (rich text and fields
@@ -8133,7 +8247,7 @@ var EasyCMS = class {
8133
8247
  }
8134
8248
  /** What a preview token opens, or `null` when it is invalid or expired. */
8135
8249
  verifyPreviewToken(token) {
8136
- return token ? verifyPreviewToken(this.config.secret, token) : null;
8250
+ return token ? verifyPreviewToken(secretsOf(this.config), token) : null;
8137
8251
  }
8138
8252
  /** Live preview of a global. See `preview`. */
8139
8253
  async previewGlobal(slug2, data, options = {}) {
@@ -12424,8 +12538,8 @@ async function route(cms, ctx, method, path, options) {
12424
12538
  if (first === "jobs" && second === "run" && third === void 0) {
12425
12539
  if (method !== "GET" && method !== "POST") throw methodNotAllowed(ctx, "GET, POST");
12426
12540
  const secret = cms.config.cronSecret ?? process.env.CRON_SECRET;
12427
- const header = ctx.request.headers.get("authorization") ?? "";
12428
- const byCron = !!secret && safeEqual(header, `Bearer ${secret}`);
12541
+ const header2 = ctx.request.headers.get("authorization") ?? "";
12542
+ const byCron = !!secret && safeEqual(header2, `Bearer ${secret}`);
12429
12543
  if (!byCron && !isSystemAdmin(ctx.user)) {
12430
12544
  throw ctx.user ? new HttpError("Forbidden", 403) : new UnauthorizedError();
12431
12545
  }
@@ -13099,9 +13213,8 @@ function checkCsrf(cms, ctx) {
13099
13213
  if (origin === null && fetchSite === "cross-site")
13100
13214
  throw new ForbiddenError("CSRF check failed: cross-site request");
13101
13215
  if (ctx.via === "cookie" && ctx.token) {
13102
- const expected = cms.auth.csrfFor(ctx.token);
13103
13216
  const given = ctx.request.headers.get(CSRF_HEADER);
13104
- if (!expected || !given || !safeEqual(given, expected)) {
13217
+ if (!given || !cms.auth.csrfMatches(ctx.token, given)) {
13105
13218
  throw new ForbiddenError(
13106
13219
  `CSRF check failed: send the ${CSRF_HEADER} header from GET /auth/me`
13107
13220
  );
@@ -13501,6 +13614,7 @@ export {
13501
13614
  isStaff,
13502
13615
  isSignedIn,
13503
13616
  isAdmin,
13617
+ secretsOf,
13504
13618
  MEDIA,
13505
13619
  MEDIA_FOLDERS,
13506
13620
  INTERNAL_COLLECTIONS,
@@ -13553,6 +13667,8 @@ export {
13553
13667
  checkRenamedOptions,
13554
13668
  BUILTIN_NAV,
13555
13669
  memorySearch,
13670
+ signWebhook,
13671
+ verifyWebhook,
13556
13672
  WEBHOOK_EVENTS,
13557
13673
  MIN_SECRET_LENGTH,
13558
13674
  BUILTIN_COLLECTIONS,
@@ -356,6 +356,16 @@ interface BundleHints {
356
356
  readonly traceInclude: readonly string[];
357
357
  }
358
358
 
359
+ /**
360
+ * Secrets that verify: the current one first, then `previousSecrets` while one is replaced. New
361
+ * values are always signed with the current one.
362
+ */
363
+ type Secrets = string | readonly string[];
364
+ /** The secrets of a config, the current one first. */
365
+ declare function secretsOf(config: {
366
+ readonly secret: string;
367
+ readonly previousSecrets?: readonly string[] | undefined;
368
+ }): readonly string[];
359
369
  /** What a preview token opens: one document of a collection, or one global. */
360
370
  type PreviewTarget = {
361
371
  readonly collection: string;
@@ -613,6 +623,8 @@ declare class Auth {
613
623
  * on the user), or `null` if it is invalid or expired.
614
624
  */
615
625
  verify(signedToken: string | undefined | null): Promise<AuthUser | null>;
626
+ /** The current secret and `previousSecrets`, which still verify. */
627
+ private get secrets();
616
628
  /** Starts a new session for a user, e.g. after they changed their own password. */
617
629
  createSession(userId: ID): Promise<Session>;
618
630
  /** Signs a user out everywhere. */
@@ -2460,6 +2472,18 @@ declare class AuditLog {
2460
2472
  csv(filter: AuditFilter): Promise<string>;
2461
2473
  /** Checks every entry's signature, and that no ids are missing. */
2462
2474
  verify(): Promise<AuditVerification>;
2475
+ /** The secret an entry was signed with: the current one, a previous one, or none (changed). */
2476
+ private signer;
2477
+ /**
2478
+ * After `secret` changed: signs again, with the current secret, the entries signed with one of
2479
+ * `previousSecrets`, so they still verify once it is removed. Entries no secret verifies were
2480
+ * changed: they are left as they are, and reported. Records `audit.resign`.
2481
+ */
2482
+ resign(): Promise<{
2483
+ checked: number;
2484
+ resigned: number;
2485
+ invalid: ID[];
2486
+ }>;
2463
2487
  /** What the dashboard warns about: many failed logins, entries not written, a failed check. */
2464
2488
  attention(now?: Date): Promise<({
2465
2489
  id: 'failed-logins';
@@ -2779,7 +2803,12 @@ interface WebhookConfig {
2779
2803
  readonly collections?: readonly string[];
2780
2804
  /** Globals to send events for. Default: all; `[]` for none. */
2781
2805
  readonly globals?: readonly string[];
2782
- /** Signs each body: `x-easy-cms-signature: sha256=<hex HMAC-SHA256 of the body>`. */
2806
+ /**
2807
+ * Signs each attempt: `x-easy-cms-timestamp` and `x-easy-cms-signature-v2` (`v1=<hex HMAC-SHA256
2808
+ * of "<delivery>.<timestamp>.<body>">`, checked by `verifyWebhook()`). Also, until 2.0, the
2809
+ * deprecated `x-easy-cms-signature: sha256=<hex HMAC-SHA256 of the body>`, which a captured
2810
+ * request can be replayed with.
2811
+ */
2783
2812
  readonly secret?: string;
2784
2813
  /** Extra request headers, e.g. a token the receiver checks. */
2785
2814
  readonly headers?: Readonly<Record<string, string>>;
@@ -3526,6 +3555,13 @@ interface BackupsConfig {
3526
3555
  interface Config {
3527
3556
  /** Signs sessions. At least 32 characters; read it from `process.env.EASY_CMS_SECRET`. */
3528
3557
  readonly secret: string;
3558
+ /**
3559
+ * Secrets used before `secret`, while it is replaced: what they signed (sessions, links,
3560
+ * tokens, the audit log) still verifies; new values are signed with `secret` only. Keep one
3561
+ * until sessions and links made with it have expired (7 days by default), and run
3562
+ * `easy-cms audit:resign` before removing it. At least 32 characters each, like `secret`.
3563
+ */
3564
+ readonly previousSecrets?: readonly string[];
3529
3565
  readonly db: DatabaseAdapter;
3530
3566
  /**
3531
3567
  * Public origin of the app, e.g. `https://example.com`. When set, media URLs are absolute
@@ -3682,5 +3718,5 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3682
3718
  */
3683
3719
  declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3684
3720
 
3685
- export { ADAPTER_API_VERSION as A, AuditLog as J, Auth as M, ADMIN_ICONS as a, ForbiddenError as a$, FIELD_TYPES as aE, ConfigError as ad, EasyCMS as aw, EasyCMSError as ax, PayloadTooLargeError as bE, QueryError as bK, ROLE_OPERATIONS as bL, Roles as bW, SchemaError as bZ, NotFoundError as bt, PLUGIN_API_VERSION as bz, API_KEYS as c$, WEBHOOK_EVENTS as cB, anyone as cH, consoleEmail as cI, consoleLogger as cJ, createEasyCMS as cK, defineConfig as cL, defineFieldType as cM, definePlugin as cN, diskStorage as cO, hasRows as cP, isAdmin as cQ, isHasMany as cR, isSignedIn as cS, isStaff as cT, isSystemAdmin as cU, jsonLogger as cV, memorySearch as cW, mimeAllowedBy as cX, rowFields as cY, silentLogger as cZ, uniqueWithinOf as c_, SingleSignOn as cc, TRIGRAM_LIKE_MIN as cj, TooManyRequestsError as cn, UnauthorizedError as cp, ValidationError as cx, API_KEY_GLOBAL_OPERATIONS as d0, API_KEY_OPERATIONS as d1, DEFAULT_MAX_VERSIONS as d2, DEFAULT_PASSWORD_EMAILS as d3, FIELD_TYPE_BASES as d4, FOLDER_LEVELS as d5, MediaFolders as d6, SSO_COOKIE as d7, codeOfStatus as d8, isJsonLogger as d9, keyAllows as da, logFormat as db };
3686
- export type { BuiltinMediaCollection as $, ApiKeyOperation as B, ApiKeyPermissions as C, ArrayField as D, EmailSetting as E, AuditChange as F, AuditConfig as G, AuditEntry as H, ID as I, AuditPage as K, AuditVerification as L, AuthConfig as N, AuthProvider as O, PaginatedDocs as P, AuthProviderProfile as Q, AuthProviderRequest as R, AuthUser as S, BackupsConfig as T, BeforeChangeHook as U, BeforeDeleteHook as V, BeforeValidateHook as W, Block as X, BlocksField as Y, BooleanField as Z, BuiltinApiKeysCollection as _, BuiltinMediaFoldersCollection as a0, BuiltinUsersCollection as a1, BundleHints as a2, CliCommand as a3, CollectionAccess as a4, CollectionAdmin as a5, CollectionConfig as a6, CollectionDocument as a7, CollectionHooks as a8, CollectionListAdmin as a9, EmailMessage as aA, Endpoint as aB, EndpointRequest as aC, ErrorCode as aD, Field as aF, FieldAccess as aG, FieldAccessFn as aH, FieldAdmin as aI, FieldError as aJ, FieldRule as aK, FieldType as aL, FieldTypeBase as aM, FieldTypeDefinition as aN, FieldTypeValidateContext as aO, FieldValidate as aP, FieldValidateContext as aQ, FieldValue as aR, FieldsInput as aS, FieldsValue as aT, FilterOptions as aU, FilterOptionsArgs as aV, FindAfterArgs as aW, FindArgs as aX, FindOptions as aY, FolderLevel as aZ, FolderPermissions as a_, CollectionSearch as aa, CollectionSlug as ab, Config as ac, ConfigIssue as ae, ContainerAdmin as af, CreateEasyCMSOptions as ag, CreateInput as ah, CreateMigrationResult as ai, CursorDocs as aj, CustomField as ak, CustomFieldTypes as al, DashboardWidget as am, Database as an, DatabaseAdapter as ao, DatabaseInitArgs as ap, DatabaseTransfer as aq, DateField as ar, DepthOptions as as, DirectUpload as at, DiskStorageOptions as au, DocumentOf as av, EmailAdapter as ay, EmailField as az, Access as b, SearchAdapter as b$, GlobalAccess as b0, GlobalConfig as b1, GlobalDocument as b2, GlobalHooks as b3, GlobalInput as b4, GlobalSlug as b5, GroupField as b6, ImageSize as b7, IncrementOptions as b8, InferCollection as b9, PasswordEmail as bA, PasswordEmailArgs as bB, PasswordEmailFn as bC, PasswordLinkOptions as bD, Plugin as bF, PluginInfo as bG, PluginTypes as bH, PreviewTarget as bI, PreviewURL as bJ, RawDocument as bM, ReadOptions as bN, RelationshipField as bO, RequestContext as bP, ResolvedConfig as bQ, RichTextDocument as bR, RichTextField as bS, Role as bT, RoleOperation as bU, RolePermissions as bV, RoutesConfig as bX, ScheduledPublish as bY, SchemaMode as b_, InferGlobal as ba, JobConfig as bb, JsonField as bc, KeysetPage as bd, Label as be, LayoutNode as bf, LivePreview as bg, LocaleOf as bh, LocalizationConfig as bi, LogDetails as bj, Logger as bk, LoginArgs as bl, LogsConfig as bm, MediaDocument as bn, MediaSize as bo, MembersConfig as bp, MembersSignup as bq, MigrationInfo as br, NavGroup as bs, NumberField as bu, OnRequest as bv, OnRequestArgs as bw, Operation as bx, OutboxRow as by, AccessArgs as c, SearchEntry as c0, SearchHits as c1, SearchOptions as c2, SearchQuery as c3, SearchResults as c4, SearchSetting as c5, SelectField as c6, SelectOption as c7, Session as c8, SidebarPanel as c9, VersionsConfig as cA, WebhookConfig as cC, WebhookEvent as cD, WebhookPayload as cE, Where as cF, WhereOperators as cG, SignupArgs as ca, SignupResult as cb, SlugField as cd, SlugOf as ce, SsoOutcome as cf, SsoProviderRef as cg, StorageAdapter as ch, StoredFile as ci, TelemetryConfig as ck, TextField as cl, TextareaField as cm, TypedPlugin as co, UncountedDocs as cq, UpdateInput as cr, UpdateOptions as cs, UploadConfig as ct, UploadField as cu, UploadFromURLConfig as cv, UserIdentity as cw, Version as cy, VersionSummary as cz, AccessOptions as d, AdminBrand as e, AdminCommand as f, AdminComponent as g, AdminConfig as h, AdminFieldArgs as i, AdminIcon as j, AdminLocale as k, AdminPage as l, AdminRole as m, AdminRoleChange as n, AdminRoleField as o, AdminRoleTarget as p, AdminRoleView as q, AdminRoles as r, AdminSso as s, AdminSwitcher as t, AdminViewAccess as u, AfterChangeHook as v, AfterDeleteHook as w, AfterReadHook as x, ApiKeyContext as y, ApiKeyGlobalOperation as z };
3721
+ export { ADAPTER_API_VERSION as A, EasyCMS as E, AuditLog as S, Auth as V, EasyCMSError as aD, FIELD_TYPES as aK, ConfigError as al, ForbiddenError as b2, PLUGIN_API_VERSION as bB, PayloadTooLargeError as bG, QueryError as bM, ROLE_OPERATIONS as bN, Roles as bX, SchemaError as b_, NotFoundError as bv, API_KEYS as c$, WEBHOOK_EVENTS as cC, anyone as cH, consoleEmail as cI, consoleLogger as cJ, createEasyCMS as cK, defineConfig as cL, defineFieldType as cM, definePlugin as cN, diskStorage as cO, hasRows as cP, isAdmin as cQ, isHasMany as cR, isSignedIn as cS, isStaff as cT, isSystemAdmin as cU, jsonLogger as cV, memorySearch as cW, mimeAllowedBy as cX, rowFields as cY, silentLogger as cZ, uniqueWithinOf as c_, SingleSignOn as cd, TRIGRAM_LIKE_MIN as ck, TooManyRequestsError as co, UnauthorizedError as cq, ValidationError as cy, API_KEY_GLOBAL_OPERATIONS as d0, API_KEY_OPERATIONS as d1, DEFAULT_MAX_VERSIONS as d2, DEFAULT_PASSWORD_EMAILS as d3, FIELD_TYPE_BASES as d4, FOLDER_LEVELS as d5, MediaFolders as d6, SSO_COOKIE as d7, codeOfStatus as d9, isJsonLogger as da, keyAllows as db, logFormat as dc, secretsOf as dd, ADMIN_ICONS as e };
3722
+ export type { AuthUser as $, AfterDeleteHook as B, Config as C, AfterReadHook as D, Field as F, ApiKeyContext as G, ApiKeyGlobalOperation as H, ID as I, ApiKeyOperation as J, ApiKeyPermissions as K, LocalizationConfig as L, ArrayField as M, AuditChange as N, AuditConfig as O, PaginatedDocs as P, AuditEntry as Q, ResolvedConfig as R, AuditPage as T, AuditVerification as U, WebhookPayload as W, AuthConfig as X, AuthProvider as Y, AuthProviderProfile as Z, AuthProviderRequest as _, FieldError as a, FindOptions as a$, BackupsConfig as a0, BeforeChangeHook as a1, BeforeDeleteHook as a2, BeforeValidateHook as a3, Block as a4, BlocksField as a5, BooleanField as a6, BuiltinApiKeysCollection as a7, BuiltinMediaCollection as a8, BuiltinMediaFoldersCollection as a9, DirectUpload as aA, DiskStorageOptions as aB, DocumentOf as aC, EmailAdapter as aE, EmailField as aF, EmailMessage as aG, Endpoint as aH, EndpointRequest as aI, ErrorCode as aJ, FieldAccess as aL, FieldAccessFn as aM, FieldAdmin as aN, FieldRule as aO, FieldType as aP, FieldTypeBase as aQ, FieldTypeDefinition as aR, FieldTypeValidateContext as aS, FieldValidate as aT, FieldValidateContext as aU, FieldValue as aV, FieldsInput as aW, FieldsValue as aX, FilterOptionsArgs as aY, FindAfterArgs as aZ, FindArgs as a_, BuiltinUsersCollection as aa, BundleHints as ab, CliCommand as ac, CollectionAccess as ad, CollectionAdmin as ae, CollectionConfig as af, CollectionDocument as ag, CollectionHooks as ah, CollectionListAdmin as ai, CollectionSearch as aj, CollectionSlug as ak, ConfigIssue as am, ContainerAdmin as an, CreateInput as ao, CreateMigrationResult as ap, CursorDocs as aq, CustomField as ar, CustomFieldTypes as as, DashboardWidget as at, Database as au, DatabaseAdapter as av, DatabaseInitArgs as aw, DatabaseTransfer as ax, DateField as ay, DepthOptions as az, FilterOptions as b, SchemaMode as b$, FolderLevel as b0, FolderPermissions as b1, GlobalAccess as b3, GlobalConfig as b4, GlobalDocument as b5, GlobalHooks as b6, GlobalInput as b7, GlobalSlug as b8, GroupField as b9, OutboxRow as bA, PasswordEmail as bC, PasswordEmailArgs as bD, PasswordEmailFn as bE, PasswordLinkOptions as bF, Plugin as bH, PluginInfo as bI, PluginTypes as bJ, PreviewTarget as bK, PreviewURL as bL, RawDocument as bO, ReadOptions as bP, RelationshipField as bQ, RequestContext as bR, RichTextDocument as bS, RichTextField as bT, Role as bU, RoleOperation as bV, RolePermissions as bW, RoutesConfig as bY, ScheduledPublish as bZ, ImageSize as ba, IncrementOptions as bb, InferCollection as bc, InferGlobal as bd, JobConfig as be, JsonField as bf, KeysetPage as bg, Label as bh, LayoutNode as bi, LivePreview as bj, LocaleOf as bk, LogDetails as bl, Logger as bm, LoginArgs as bn, LogsConfig as bo, MediaDocument as bp, MediaSize as bq, MembersConfig as br, MembersSignup as bs, MigrationInfo as bt, NavGroup as bu, NumberField as bw, OnRequest as bx, OnRequestArgs as by, Operation as bz, CreateEasyCMSOptions as c, SearchAdapter as c0, SearchEntry as c1, SearchHits as c2, SearchOptions as c3, SearchQuery as c4, SearchResults as c5, SearchSetting as c6, SelectField as c7, SelectOption as c8, Session as c9, VersionSummary as cA, VersionsConfig as cB, WebhookConfig as cD, WebhookEvent as cE, Where as cF, WhereOperators as cG, SidebarPanel as ca, SignupArgs as cb, SignupResult as cc, SlugField as ce, SlugOf as cf, SsoOutcome as cg, SsoProviderRef as ch, StorageAdapter as ci, StoredFile as cj, TelemetryConfig as cl, TextField as cm, TextareaField as cn, TypedPlugin as cp, UncountedDocs as cr, UpdateInput as cs, UpdateOptions as ct, UploadConfig as cu, UploadField as cv, UploadFromURLConfig as cw, UserIdentity as cx, Version as cz, EmailSetting as d, Secrets as d8, Access as f, AccessArgs as g, AccessOptions as h, AdminBrand as i, AdminCommand as j, AdminComponent as k, AdminConfig as l, AdminFieldArgs as m, AdminIcon as n, AdminLocale as o, AdminPage as p, AdminRole as q, AdminRoleChange as r, AdminRoleField as s, AdminRoleTarget as t, AdminRoleView as u, AdminRoles as v, AdminSso as w, AdminSwitcher as x, AdminViewAccess as y, AfterChangeHook as z };
@@ -1,4 +1,4 @@
1
- import { I as ID, ac as Config, aw as EasyCMS, bi as LocalizationConfig, aF as Field, aJ as FieldError, aU as FilterOptions, ag as CreateEasyCMSOptions, bQ as ResolvedConfig } from './config-BODu0yjp.js';
1
+ import { I as ID, C as Config, E as EasyCMS, L as LocalizationConfig, F as Field, a as FieldError, b as FilterOptions, c as CreateEasyCMSOptions, R as ResolvedConfig } from './config-CpeC7EvC.js';
2
2
 
3
3
  /** Decrypts a file from `encryptBackup`; throws for another key or a changed file. */
4
4
  declare function decryptBackup(data: Uint8Array, encryptionKey: string): Uint8Array;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-BODu0yjp.js';
2
- export { A as ADAPTER_API_VERSION, a as ADMIN_ICONS, b as Access, c as AccessArgs, d as AccessOptions, e as AdminBrand, f as AdminCommand, g as AdminComponent, h as AdminConfig, i as AdminFieldArgs, j as AdminIcon, k as AdminLocale, l as AdminPage, m as AdminRole, n as AdminRoleChange, o as AdminRoleField, p as AdminRoleTarget, q as AdminRoleView, r as AdminRoles, s as AdminSso, t as AdminSwitcher, u as AdminViewAccess, v as AfterChangeHook, w as AfterDeleteHook, x as AfterReadHook, y as ApiKeyContext, z as ApiKeyGlobalOperation, B as ApiKeyOperation, C as ApiKeyPermissions, D as ArrayField, F as AuditChange, G as AuditConfig, H as AuditEntry, J as AuditLog, K as AuditPage, L as AuditVerification, M as Auth, N as AuthConfig, O as AuthProvider, Q as AuthProviderProfile, R as AuthProviderRequest, S as AuthUser, T as BackupsConfig, U as BeforeChangeHook, V as BeforeDeleteHook, W as BeforeValidateHook, X as Block, Y as BlocksField, Z as BooleanField, _ as BuiltinApiKeysCollection, $ as BuiltinMediaCollection, a0 as BuiltinMediaFoldersCollection, a1 as BuiltinUsersCollection, a2 as BundleHints, a3 as CliCommand, a4 as CollectionAccess, a5 as CollectionAdmin, a6 as CollectionConfig, a7 as CollectionDocument, a8 as CollectionHooks, a9 as CollectionListAdmin, aa as CollectionSearch, ab as CollectionSlug, ac as Config, ad as ConfigError, ae as ConfigIssue, af as ContainerAdmin, ag as CreateEasyCMSOptions, ah as CreateInput, ai as CreateMigrationResult, aj as CursorDocs, ak as CustomField, al as CustomFieldTypes, am as DashboardWidget, an as Database, ao as DatabaseAdapter, ap as DatabaseInitArgs, aq as DatabaseTransfer, ar as DateField, as as DepthOptions, at as DirectUpload, au as DiskStorageOptions, av as DocumentOf, aw as EasyCMS, ax as EasyCMSError, ay as EmailAdapter, az as EmailField, aA as EmailMessage, aB as Endpoint, aC as EndpointRequest, aD as ErrorCode, aE as FIELD_TYPES, aF as Field, aG as FieldAccess, aH as FieldAccessFn, aI as FieldAdmin, aJ as FieldError, aK as FieldRule, aL as FieldType, aM as FieldTypeBase, aN as FieldTypeDefinition, aO as FieldTypeValidateContext, aP as FieldValidate, aQ as FieldValidateContext, aR as FieldValue, aS as FieldsInput, aT as FieldsValue, aU as FilterOptions, aV as FilterOptionsArgs, aW as FindAfterArgs, aX as FindArgs, aY as FindOptions, aZ as FolderLevel, a_ as FolderPermissions, a$ as ForbiddenError, b0 as GlobalAccess, b1 as GlobalConfig, b2 as GlobalDocument, b3 as GlobalHooks, b4 as GlobalInput, b5 as GlobalSlug, b6 as GroupField, b7 as ImageSize, b8 as IncrementOptions, b9 as InferCollection, ba as InferGlobal, bb as JobConfig, bc as JsonField, bd as KeysetPage, be as Label, bf as LayoutNode, bg as LivePreview, bh as LocaleOf, bi as LocalizationConfig, bj as LogDetails, bk as Logger, bl as LoginArgs, bm as LogsConfig, bn as MediaDocument, bo as MediaSize, bp as MembersConfig, bq as MembersSignup, br as MigrationInfo, bs as NavGroup, bt as NotFoundError, bu as NumberField, bv as OnRequest, bw as OnRequestArgs, bx as Operation, by as OutboxRow, bz as PLUGIN_API_VERSION, bA as PasswordEmail, bB as PasswordEmailArgs, bC as PasswordEmailFn, bD as PasswordLinkOptions, bE as PayloadTooLargeError, bF as Plugin, bG as PluginInfo, bH as PluginTypes, bI as PreviewTarget, bJ as PreviewURL, bK as QueryError, bL as ROLE_OPERATIONS, bM as RawDocument, bN as ReadOptions, bO as RelationshipField, bP as RequestContext, bQ as ResolvedConfig, bR as RichTextDocument, bS as RichTextField, bT as Role, bU as RoleOperation, bV as RolePermissions, bW as Roles, bX as RoutesConfig, bY as ScheduledPublish, bZ as SchemaError, b_ as SchemaMode, b$ as SearchAdapter, c0 as SearchEntry, c1 as SearchHits, c2 as SearchOptions, c3 as SearchQuery, c4 as SearchResults, c5 as SearchSetting, c6 as SelectField, c7 as SelectOption, c8 as Session, c9 as SidebarPanel, ca as SignupArgs, cb as SignupResult, cc as SingleSignOn, cd as SlugField, ce as SlugOf, cf as SsoOutcome, cg as SsoProviderRef, ch as StorageAdapter, ci as StoredFile, cj as TRIGRAM_LIKE_MIN, ck as TelemetryConfig, cl as TextField, cm as TextareaField, cn as TooManyRequestsError, co as TypedPlugin, cp as UnauthorizedError, cq as UncountedDocs, cr as UpdateInput, cs as UpdateOptions, ct as UploadConfig, cu as UploadField, cv as UploadFromURLConfig, cw as UserIdentity, cx as ValidationError, cy as Version, cz as VersionSummary, cA as VersionsConfig, cB as WEBHOOK_EVENTS, cC as WebhookConfig, cD as WebhookEvent, cE as WebhookPayload, cF as Where, cG as WhereOperators, cH as anyone, cI as consoleEmail, cJ as consoleLogger, cK as createEasyCMS, cL as defineConfig, cM as defineFieldType, cN as definePlugin, cO as diskStorage, cP as hasRows, cQ as isAdmin, cR as isHasMany, cS as isSignedIn, cT as isStaff, cU as isSystemAdmin, cV as jsonLogger, cW as memorySearch, cX as mimeAllowedBy, cY as rowFields, cZ as silentLogger, c_ as uniqueWithinOf } from './config-BODu0yjp.js';
3
- export { A as ADMIN_API_VERSION, a as AdminCollection, b as AdminComponentRef, c as AdminElementProps, d as AdminField, e as AdminGlobal, f as AdminLayoutNode, g as AdminNavGroup, h as AdminNavItem, i as AdminNavNode, j as AdminPageRef, k as AdminPageRoute, l as AdminSchema, m as AdminWidgetRef, S as SetFieldDetail, w as warnDeprecated } from './plugin-PyUqbeK2.js';
4
- export { A as AdminBackup, a as AdminBackups, b as ApiHandlerOptions, B as BackupState, L as LoadConfigOptions, R as RestHandler, c as RestHandlerOptions, d as createApiHandler, e as createRestHandler, l as loadConfig, r as resolveConfig, s as sharedEasyCMS, f as slugify } from './handler-DOMtk2Vi.js';
1
+ import { P as PaginatedDocs, I as ID, d as EmailSetting, W as WebhookPayload } from './config-CpeC7EvC.js';
2
+ export { A as ADAPTER_API_VERSION, e as ADMIN_ICONS, f as Access, g as AccessArgs, h as AccessOptions, i as AdminBrand, j as AdminCommand, k as AdminComponent, l as AdminConfig, m as AdminFieldArgs, n as AdminIcon, o as AdminLocale, p as AdminPage, q as AdminRole, r as AdminRoleChange, s as AdminRoleField, t as AdminRoleTarget, u as AdminRoleView, v as AdminRoles, w as AdminSso, x as AdminSwitcher, y as AdminViewAccess, z as AfterChangeHook, B as AfterDeleteHook, D as AfterReadHook, G as ApiKeyContext, H as ApiKeyGlobalOperation, J as ApiKeyOperation, K as ApiKeyPermissions, M as ArrayField, N as AuditChange, O as AuditConfig, Q as AuditEntry, S as AuditLog, T as AuditPage, U as AuditVerification, V as Auth, X as AuthConfig, Y as AuthProvider, Z as AuthProviderProfile, _ as AuthProviderRequest, $ as AuthUser, a0 as BackupsConfig, a1 as BeforeChangeHook, a2 as BeforeDeleteHook, a3 as BeforeValidateHook, a4 as Block, a5 as BlocksField, a6 as BooleanField, a7 as BuiltinApiKeysCollection, a8 as BuiltinMediaCollection, a9 as BuiltinMediaFoldersCollection, aa as BuiltinUsersCollection, ab as BundleHints, ac as CliCommand, ad as CollectionAccess, ae as CollectionAdmin, af as CollectionConfig, ag as CollectionDocument, ah as CollectionHooks, ai as CollectionListAdmin, aj as CollectionSearch, ak as CollectionSlug, C as Config, al as ConfigError, am as ConfigIssue, an as ContainerAdmin, c as CreateEasyCMSOptions, ao as CreateInput, ap as CreateMigrationResult, aq as CursorDocs, ar as CustomField, as as CustomFieldTypes, at as DashboardWidget, au as Database, av as DatabaseAdapter, aw as DatabaseInitArgs, ax as DatabaseTransfer, ay as DateField, az as DepthOptions, aA as DirectUpload, aB as DiskStorageOptions, aC as DocumentOf, E as EasyCMS, aD as EasyCMSError, aE as EmailAdapter, aF as EmailField, aG as EmailMessage, aH as Endpoint, aI as EndpointRequest, aJ as ErrorCode, aK as FIELD_TYPES, F as Field, aL as FieldAccess, aM as FieldAccessFn, aN as FieldAdmin, a as FieldError, aO as FieldRule, aP as FieldType, aQ as FieldTypeBase, aR as FieldTypeDefinition, aS as FieldTypeValidateContext, aT as FieldValidate, aU as FieldValidateContext, aV as FieldValue, aW as FieldsInput, aX as FieldsValue, b as FilterOptions, aY as FilterOptionsArgs, aZ as FindAfterArgs, a_ as FindArgs, a$ as FindOptions, b0 as FolderLevel, b1 as FolderPermissions, b2 as ForbiddenError, b3 as GlobalAccess, b4 as GlobalConfig, b5 as GlobalDocument, b6 as GlobalHooks, b7 as GlobalInput, b8 as GlobalSlug, b9 as GroupField, ba as ImageSize, bb as IncrementOptions, bc as InferCollection, bd as InferGlobal, be as JobConfig, bf as JsonField, bg as KeysetPage, bh as Label, bi as LayoutNode, bj as LivePreview, bk as LocaleOf, L as LocalizationConfig, bl as LogDetails, bm as Logger, bn as LoginArgs, bo as LogsConfig, bp as MediaDocument, bq as MediaSize, br as MembersConfig, bs as MembersSignup, bt as MigrationInfo, bu as NavGroup, bv as NotFoundError, bw as NumberField, bx as OnRequest, by as OnRequestArgs, bz as Operation, bA as OutboxRow, bB as PLUGIN_API_VERSION, bC as PasswordEmail, bD as PasswordEmailArgs, bE as PasswordEmailFn, bF as PasswordLinkOptions, bG as PayloadTooLargeError, bH as Plugin, bI as PluginInfo, bJ as PluginTypes, bK as PreviewTarget, bL as PreviewURL, bM as QueryError, bN as ROLE_OPERATIONS, bO as RawDocument, bP as ReadOptions, bQ as RelationshipField, bR as RequestContext, R as ResolvedConfig, bS as RichTextDocument, bT as RichTextField, bU as Role, bV as RoleOperation, bW as RolePermissions, bX as Roles, bY as RoutesConfig, bZ as ScheduledPublish, b_ as SchemaError, b$ as SchemaMode, c0 as SearchAdapter, c1 as SearchEntry, c2 as SearchHits, c3 as SearchOptions, c4 as SearchQuery, c5 as SearchResults, c6 as SearchSetting, c7 as SelectField, c8 as SelectOption, c9 as Session, ca as SidebarPanel, cb as SignupArgs, cc as SignupResult, cd as SingleSignOn, ce as SlugField, cf as SlugOf, cg as SsoOutcome, ch as SsoProviderRef, ci as StorageAdapter, cj as StoredFile, ck as TRIGRAM_LIKE_MIN, cl as TelemetryConfig, cm as TextField, cn as TextareaField, co as TooManyRequestsError, cp as TypedPlugin, cq as UnauthorizedError, cr as UncountedDocs, cs as UpdateInput, ct as UpdateOptions, cu as UploadConfig, cv as UploadField, cw as UploadFromURLConfig, cx as UserIdentity, cy as ValidationError, cz as Version, cA as VersionSummary, cB as VersionsConfig, cC as WEBHOOK_EVENTS, cD as WebhookConfig, cE as WebhookEvent, cF as Where, cG as WhereOperators, cH as anyone, cI as consoleEmail, cJ as consoleLogger, cK as createEasyCMS, cL as defineConfig, cM as defineFieldType, cN as definePlugin, cO as diskStorage, cP as hasRows, cQ as isAdmin, cR as isHasMany, cS as isSignedIn, cT as isStaff, cU as isSystemAdmin, cV as jsonLogger, cW as memorySearch, cX as mimeAllowedBy, cY as rowFields, cZ as silentLogger, c_ as uniqueWithinOf } from './config-CpeC7EvC.js';
3
+ export { A as ADMIN_API_VERSION, a as AdminCollection, b as AdminComponentRef, c as AdminElementProps, d as AdminField, e as AdminGlobal, f as AdminLayoutNode, g as AdminNavGroup, h as AdminNavItem, i as AdminNavNode, j as AdminPageRef, k as AdminPageRoute, l as AdminSchema, m as AdminWidgetRef, S as SetFieldDetail, w as warnDeprecated } from './plugin-_XG-lqA5.js';
4
+ export { A as AdminBackup, a as AdminBackups, b as ApiHandlerOptions, B as BackupState, L as LoadConfigOptions, R as RestHandler, c as RestHandlerOptions, d as createApiHandler, e as createRestHandler, l as loadConfig, r as resolveConfig, s as sharedEasyCMS, f as slugify } from './handler-BpKo6gh0.js';
5
5
  export { FieldCondition, matchesCondition } from './conditions.js';
6
6
 
7
7
  type DeliveryKind = 'webhook' | 'email';
@@ -259,5 +259,27 @@ declare const cacheTags: {
259
259
  /** The version of `@easy-cms/core`, set when the package is built. */
260
260
  declare const VERSION: string;
261
261
 
262
- export { EmailSetting, ID, PaginatedDocs, VERSION, cacheTags };
263
- export type { AdminAttention, AdminDeliveries, AdminDelivery, AdminEmail, AdminEmailDelivery, AdminJob, AdminJobs, AdminOutbox, AdminOutboxEntry, AdminScheduledJob, AdminStatus, AdminWebhookDelivery, BuiltInJob, DeliveryKind, DeliveryState, EmailCheck, SearchHit };
262
+ /**
263
+ * `x-easy-cms-signature-v2` of one attempt: `v1=<hex HMAC-SHA256 of "<delivery>.<timestamp>.<body>">`.
264
+ * The timestamp (`x-easy-cms-timestamp`, Unix seconds) is the attempt's, so a captured request
265
+ * can't be replayed later, and the delivery id can't be paired with another body.
266
+ */
267
+ declare function signWebhook(secret: string, delivery: string, timestamp: number, body: string): string;
268
+ interface VerifyWebhookOptions {
269
+ /** How far the timestamp may be from now, in seconds. Default 300. */
270
+ readonly toleranceSeconds?: number;
271
+ /** Now, for tests. */
272
+ readonly now?: Date;
273
+ }
274
+ type HeaderSource = Headers | Readonly<Record<string, string | readonly string[] | undefined>>;
275
+ /**
276
+ * Checks a webhook from Easy CMS and returns its payload: the signature (with `secret`, or any of
277
+ * several while one is replaced) over the delivery id, the timestamp and the raw body, and a
278
+ * timestamp within `toleranceSeconds` of now. Throws `UnauthorizedError` otherwise. Pass the body
279
+ * exactly as received, before parsing it. Also skip deliveries already handled
280
+ * (`x-easy-cms-delivery`): a replay within the tolerance has a valid signature.
281
+ */
282
+ declare function verifyWebhook(body: string, headers: HeaderSource, secret: string | readonly string[], options?: VerifyWebhookOptions): WebhookPayload;
283
+
284
+ export { EmailSetting, ID, PaginatedDocs, VERSION, WebhookPayload, cacheTags, signWebhook, verifyWebhook };
285
+ export type { AdminAttention, AdminDeliveries, AdminDelivery, AdminEmail, AdminEmailDelivery, AdminJob, AdminJobs, AdminOutbox, AdminOutboxEntry, AdminScheduledJob, AdminStatus, AdminWebhookDelivery, BuiltInJob, DeliveryKind, DeliveryState, EmailCheck, SearchHit, VerifyWebhookOptions };
package/dist/index.js CHANGED
@@ -24,9 +24,11 @@ import {
24
24
  memorySearch,
25
25
  resolveConfig,
26
26
  sharedEasyCMS,
27
+ signWebhook,
27
28
  silentLogger,
28
- slugify
29
- } from "./chunk-WGECYA3K.js";
29
+ slugify,
30
+ verifyWebhook
31
+ } from "./chunk-NTAFZ4M6.js";
30
32
  import {
31
33
  matchesCondition
32
34
  } from "./chunk-U266LV4M.js";
@@ -96,7 +98,9 @@ export {
96
98
  memorySearch,
97
99
  resolveConfig,
98
100
  sharedEasyCMS,
101
+ signWebhook,
99
102
  silentLogger,
100
103
  slugify,
104
+ verifyWebhook,
101
105
  warnDeprecated
102
106
  };
@@ -1,6 +1,6 @@
1
1
  import { Config, Database, EasyCMS, NavGroup, CollectionConfig, ID, RawDocument, Field, ResolvedConfig, ConfigIssue } from './index.js';
2
- export { c$ as API_KEYS, d0 as API_KEY_GLOBAL_OPERATIONS, d1 as API_KEY_OPERATIONS, d2 as DEFAULT_MAX_VERSIONS, d3 as DEFAULT_PASSWORD_EMAILS, d4 as FIELD_TYPE_BASES, d5 as FOLDER_LEVELS, d6 as MediaFolders, d7 as SSO_COOKIE, cj as TRIGRAM_LIKE_MIN, d8 as codeOfStatus, cP as hasRows, cR as isHasMany, d9 as isJsonLogger, da as keyAllows, db as logFormat, cX as mimeAllowedBy, cY as rowFields, c_ as uniqueWithinOf } from './config-BODu0yjp.js';
3
- export { C as CONFIG_FILE_NAMES, g as CSRF_COOKIE, h as CSRF_HEADER, D as DEFAULT_ADMIN_PATH, i as DEFAULT_API_PATH, j as DEFAULT_MAX_FILE_SIZE, k as DEFAULT_TOKEN_EXPIRATION, m as applyDefaults, n as applyPlugins, o as createRootEndpointHandler, p as decryptBackup, q as fillMissing, t as findConfigFile, u as forwardedClientIp, v as generateSlugs, w as importConfig, x as isEmail, y as mergeForUpdate, z as parseId, E as platformClientIp, F as readCookie, G as validateFields, H as writeBackupFile } from './handler-DOMtk2Vi.js';
2
+ export { c$ as API_KEYS, d0 as API_KEY_GLOBAL_OPERATIONS, d1 as API_KEY_OPERATIONS, d2 as DEFAULT_MAX_VERSIONS, d3 as DEFAULT_PASSWORD_EMAILS, d4 as FIELD_TYPE_BASES, d5 as FOLDER_LEVELS, d6 as MediaFolders, d7 as SSO_COOKIE, d8 as Secrets, ck as TRIGRAM_LIKE_MIN, d9 as codeOfStatus, cP as hasRows, cR as isHasMany, da as isJsonLogger, db as keyAllows, dc as logFormat, cX as mimeAllowedBy, cY as rowFields, dd as secretsOf, c_ as uniqueWithinOf } from './config-CpeC7EvC.js';
3
+ export { C as CONFIG_FILE_NAMES, g as CSRF_COOKIE, h as CSRF_HEADER, D as DEFAULT_ADMIN_PATH, i as DEFAULT_API_PATH, j as DEFAULT_MAX_FILE_SIZE, k as DEFAULT_TOKEN_EXPIRATION, m as applyDefaults, n as applyPlugins, o as createRootEndpointHandler, p as decryptBackup, q as fillMissing, t as findConfigFile, u as forwardedClientIp, v as generateSlugs, w as importConfig, x as isEmail, y as mergeForUpdate, z as parseId, E as platformClientIp, F as readCookie, G as validateFields, H as writeBackupFile } from './handler-BpKo6gh0.js';
4
4
  export { conditionIssues } from './conditions.js';
5
5
 
6
6
  /** The session cookie the REST API sets at login. */
@@ -112,7 +112,7 @@ declare function formToken(secret: string, purpose: string, now?: number): strin
112
112
  * Checks a token: `ok` when the form was loaded at least `minTime` ms ago (people take a few
113
113
  * seconds; bots don't) and less than a day ago.
114
114
  */
115
- declare function checkFormToken(secret: string, purpose: string, token: unknown, minTime: number, now?: number): 'ok' | 'invalid' | 'too-fast' | 'expired';
115
+ declare function checkFormToken(secrets: string | readonly string[], purpose: string, token: unknown, minTime: number, now?: number): 'ok' | 'invalid' | 'too-fast' | 'expired';
116
116
  /**
117
117
  * Rate-limit keys for `subject` (e.g. an IP and a form) in the current and previous window of
118
118
  * `window` seconds. A hash with the window in it: no IP is kept, and keys from different
package/dist/internal.js CHANGED
@@ -52,12 +52,13 @@ import {
52
52
  rateKeys,
53
53
  readCookie,
54
54
  resolveAdminModule,
55
+ secretsOf,
55
56
  validateConfig,
56
57
  validateFields,
57
58
  verifyPassword,
58
59
  verifyTurnstile,
59
60
  writeBackupFile
60
- } from "./chunk-WGECYA3K.js";
61
+ } from "./chunk-NTAFZ4M6.js";
61
62
  import {
62
63
  conditionIssues
63
64
  } from "./chunk-U266LV4M.js";
@@ -311,6 +312,7 @@ export {
311
312
  readCookie,
312
313
  resolveAdminModule,
313
314
  rowFields,
315
+ secretsOf,
314
316
  singularize,
315
317
  sniffMimeType,
316
318
  uniqueWithinOf,
@@ -1,4 +1,4 @@
1
- import { be as Label, aF as Field, I as ID, k as AdminLocale, t as AdminSwitcher, cg as SsoProviderRef } from './config-BODu0yjp.js';
1
+ import { bh as Label, F as Field, I as ID, o as AdminLocale, x as AdminSwitcher, ch as SsoProviderRef } from './config-CpeC7EvC.js';
2
2
  import { FieldCondition } from './conditions.js';
3
3
 
4
4
  /** A Web Component from an admin module, with its `props` (plain JSON). */
package/dist/plugin.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { a as ADMIN_ICONS, g as AdminComponent, j as AdminIcon, S as AuthUser, a3 as CliCommand, a4 as CollectionAccess, a6 as CollectionConfig, ac as Config, ad as ConfigError, am as DashboardWidget, ax as EasyCMSError, aB as Endpoint, aC as EndpointRequest, aD as ErrorCode, aF as Field, aG as FieldAccess, aJ as FieldError, aM as FieldTypeBase, aN as FieldTypeDefinition, aO as FieldTypeValidateContext, a$ as ForbiddenError, b1 as GlobalConfig, I as ID, bb as JobConfig, be as Label, bs as NavGroup, bt as NotFoundError, bv as OnRequest, bw as OnRequestArgs, bz as PLUGIN_API_VERSION, bE as PayloadTooLargeError, bF as Plugin, bG as PluginInfo, bH as PluginTypes, bK as QueryError, bP as RequestContext, cn as TooManyRequestsError, co as TypedPlugin, cp as UnauthorizedError, cx as ValidationError, cF as Where, cM as defineFieldType, cN as definePlugin } from './config-BODu0yjp.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-PyUqbeK2.js';
1
+ export { e as ADMIN_ICONS, k as AdminComponent, n as AdminIcon, $ as AuthUser, ac as CliCommand, ad as CollectionAccess, af as CollectionConfig, C as Config, al as ConfigError, at as DashboardWidget, aD as EasyCMSError, aH as Endpoint, aI as EndpointRequest, aJ as ErrorCode, F as Field, aL as FieldAccess, a as FieldError, aQ as FieldTypeBase, aR as FieldTypeDefinition, aS as FieldTypeValidateContext, b2 as ForbiddenError, b4 as GlobalConfig, I as ID, be as JobConfig, bh as Label, bu as NavGroup, bv as NotFoundError, bx as OnRequest, by as OnRequestArgs, bB as PLUGIN_API_VERSION, bG as PayloadTooLargeError, bH as Plugin, bI as PluginInfo, bJ as PluginTypes, bM as QueryError, bR as RequestContext, co as TooManyRequestsError, cp as TypedPlugin, cq as UnauthorizedError, cy as ValidationError, cF as Where, cM as defineFieldType, cN as definePlugin } from './config-CpeC7EvC.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-_XG-lqA5.js';
3
3
  import './conditions.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",