@easy-cms/core 0.63.0 → 0.65.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.
@@ -33,8 +33,9 @@ import {
33
33
  typeFromName,
34
34
  uniqueWithinOf,
35
35
  warnDeprecated,
36
+ withCharset,
36
37
  withPrivacy
37
- } from "./chunk-FPFWLQB5.js";
38
+ } from "./chunk-RTKWMVAC.js";
38
39
 
39
40
  // src/access.ts
40
41
  var isSystemAdmin = (user) => user?.role === "admin" && !user.apiKey && user.scoped !== true;
@@ -521,6 +522,7 @@ var CSV_LIMIT = 1e4;
521
522
  var DAY = 864e5;
522
523
  var BULKY = /* @__PURE__ */ new Set(["richText", "blocks", "array", "json"]);
523
524
  var auditContext = new AsyncLocalStorage();
525
+ var SIGN_IN_FAILURES = /* @__PURE__ */ new Set(["login.failed", "login.locked", "sso.failed"]);
524
526
  function stable(value) {
525
527
  if (value === void 0 || value === null) return "null";
526
528
  if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
@@ -577,7 +579,15 @@ var AuditLog = class {
577
579
  * counted, and the dashboard says so.
578
580
  */
579
581
  async record(entry) {
580
- if (!this.enabled) return;
582
+ if (!this.enabled) {
583
+ if (SIGN_IN_FAILURES.has(entry.action)) {
584
+ const ip = auditContext.getStore()?.ip;
585
+ this.cms.logger.warn(
586
+ `${entry.action}: ${entry.email ?? entry.user?.email ?? "unknown"}${ip ? ` from ${ip}` : ""}`
587
+ );
588
+ }
589
+ return;
590
+ }
581
591
  try {
582
592
  const context = auditContext.getStore();
583
593
  const user = entry.user !== void 0 ? entry.user : context?.user ?? null;
@@ -730,8 +740,11 @@ var AuditLog = class {
730
740
  await this.record({
731
741
  action: "audit.verify",
732
742
  target: "audit",
733
- detail: { checked, invalid: invalid.length, gaps }
743
+ detail: { checked, invalid: invalid.length, gaps, lastId: previous ?? null }
734
744
  });
745
+ this.cms.logger.info(
746
+ `Audit log checked: ${checked} entries, last #${previous ?? "-"}; ${invalid.length} changed, ${gaps} missing`
747
+ );
735
748
  return verification;
736
749
  }
737
750
  /**
@@ -1015,6 +1028,21 @@ async function verifyTurnstile(secretKey, response, ip, fetcher = fetch) {
1015
1028
 
1016
1029
  // src/auth/cookie.ts
1017
1030
  var SESSION_COOKIE = "ecms-session";
1031
+ var hostCookie = (name) => `__Host-${name}`;
1032
+ function cookieValue(header, name) {
1033
+ if (!header) return void 0;
1034
+ let plain;
1035
+ for (const part of header.split(";")) {
1036
+ const eq = part.indexOf("=");
1037
+ if (eq <= 0) continue;
1038
+ const key = part.slice(0, eq).trim();
1039
+ if (key !== name && key !== hostCookie(name)) continue;
1040
+ const value = decodeURIComponent(part.slice(eq + 1).trim());
1041
+ if (key !== name) return value;
1042
+ plain ??= value;
1043
+ }
1044
+ return plain;
1045
+ }
1018
1046
 
1019
1047
  // src/auth/emails.ts
1020
1048
  var esc = (value) => value.replace(
@@ -1911,12 +1939,8 @@ var Auth = class {
1911
1939
  async userFromHeaders(headers) {
1912
1940
  const authorization = headers.get("authorization");
1913
1941
  if (authorization?.startsWith("Bearer ")) return this.verify(authorization.slice(7).trim());
1914
- for (const part of headers.get("cookie")?.split(";") ?? []) {
1915
- const eq = part.indexOf("=");
1916
- if (eq > 0 && part.slice(0, eq).trim() === SESSION_COOKIE)
1917
- return this.verify(decodeURIComponent(part.slice(eq + 1).trim()));
1918
- }
1919
- return null;
1942
+ const cookie2 = cookieValue(headers.get("cookie"), SESSION_COOKIE);
1943
+ return cookie2 ? this.verify(cookie2) : null;
1920
1944
  }
1921
1945
  /**
1922
1946
  * Returns the user for a signed session token, or for an API key (`ecms_…`, with `apiKey` set
@@ -2820,12 +2844,38 @@ function diskStorage(options = {}) {
2820
2844
  }
2821
2845
 
2822
2846
  // src/backups.ts
2847
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes5 } from "crypto";
2823
2848
  import { mkdtemp, readFile as readFile2, rm as rm2 } from "fs/promises";
2824
2849
  import { tmpdir } from "os";
2825
2850
  import { join as join2 } from "path";
2826
2851
  import { promisify } from "util";
2827
2852
  import { gzip } from "zlib";
2828
2853
  var gzipAsync = promisify(gzip);
2854
+ var MAGIC = Buffer.from("ECMSBAK1");
2855
+ var IV_BYTES = 12;
2856
+ var TAG_BYTES = 16;
2857
+ var keyOf = (encryptionKey) => createHash3("sha256").update(encryptionKey).digest();
2858
+ function encryptBackup(data, encryptionKey) {
2859
+ const iv = randomBytes5(IV_BYTES);
2860
+ const cipher = createCipheriv("aes-256-gcm", keyOf(encryptionKey), iv);
2861
+ const body = Buffer.concat([cipher.update(data), cipher.final()]);
2862
+ return new Uint8Array(Buffer.concat([MAGIC, iv, body, cipher.getAuthTag()]));
2863
+ }
2864
+ function decryptBackup(data, encryptionKey) {
2865
+ const buffer = Buffer.from(data);
2866
+ if (buffer.length < MAGIC.length + IV_BYTES + TAG_BYTES || !buffer.subarray(0, 8).equals(MAGIC))
2867
+ throw new EasyCMSError("Not an encrypted Easy CMS backup", 400);
2868
+ const iv = buffer.subarray(MAGIC.length, MAGIC.length + IV_BYTES);
2869
+ const tag = buffer.subarray(buffer.length - TAG_BYTES);
2870
+ const body = buffer.subarray(MAGIC.length + IV_BYTES, buffer.length - TAG_BYTES);
2871
+ const decipher = createDecipheriv("aes-256-gcm", keyOf(encryptionKey), iv);
2872
+ decipher.setAuthTag(tag);
2873
+ try {
2874
+ return new Uint8Array(Buffer.concat([decipher.update(body), decipher.final()]));
2875
+ } catch {
2876
+ throw new EasyCMSError("The backup could not be decrypted: wrong key, or a changed file", 400);
2877
+ }
2878
+ }
2829
2879
  var DEFAULT_BACKUP_TIME = "03:00";
2830
2880
  var DEFAULT_BACKUP_KEEP = 7;
2831
2881
  var DEFAULT_DIR = "backups";
@@ -2970,11 +3020,15 @@ async function runBackup(cms, record) {
2970
3020
  try {
2971
3021
  const file = join2(dir, "backup.db");
2972
3022
  await writeBackupFile(cms, file);
2973
- const data = new Uint8Array(await gzipAsync(await readFile2(file)));
3023
+ const encryptionKey = cms.config.backups?.encryptionKey;
3024
+ const gzipped = new Uint8Array(await gzipAsync(await readFile2(file)));
3025
+ const data = encryptionKey ? encryptBackup(gzipped, encryptionKey) : gzipped;
2974
3026
  const storage = await backupStorage(cms);
2975
- const filename = filenameFor(cms, started);
2976
- const key = await storage.get(filename) ? filename.replace(/\.db\.gz$/, `-${row.id}.db.gz`) : filename;
2977
- await storage.put(key, data, { contentType: "application/gzip" });
3027
+ const filename = `${filenameFor(cms, started)}${encryptionKey ? ".enc" : ""}`;
3028
+ const key = await storage.get(filename) ? filename.replace(/\.db\.gz(\.enc)?$/, `-${row.id}.db.gz$1`) : filename;
3029
+ await storage.put(key, data, {
3030
+ contentType: encryptionKey ? "application/octet-stream" : "application/gzip"
3031
+ });
2978
3032
  await pruneBackups(cms, 1);
2979
3033
  row = await update(cms, row, {
2980
3034
  state: "done",
@@ -3101,8 +3155,20 @@ async function downloadBackup(cms, id, by) {
3101
3155
  const file = await (await backupStorage(cms)).get(row.filename);
3102
3156
  if (!file)
3103
3157
  throw new EasyCMSError(`The file ${row.filename} is no longer in the backup storage`, 404);
3158
+ let { filename } = row;
3159
+ let body = file.body;
3160
+ if (filename.endsWith(".enc")) {
3161
+ const encryptionKey = cms.config.backups?.encryptionKey;
3162
+ if (!encryptionKey)
3163
+ throw new EasyCMSError(
3164
+ "This backup is encrypted: set backups.encryptionKey to download it",
3165
+ 400
3166
+ );
3167
+ body = decryptBackup(body, encryptionKey);
3168
+ filename = filename.slice(0, -".enc".length);
3169
+ }
3104
3170
  await update(cms, row, { downloadedBy: by, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
3105
- return { filename: row.filename, body: file.body };
3171
+ return { filename, body };
3106
3172
  }
3107
3173
  async function deleteBackup(cms, id) {
3108
3174
  const row = await cms.db.findById({ collection: DATABASE_BACKUPS, id });
@@ -4195,7 +4261,7 @@ var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedO
4195
4261
  resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
4196
4262
  var MEMBERS = keys("roles signUp pages emails");
4197
4263
  var UPLOAD = keys("dir maxFileSize mimeTypes storage imageSizes fromURL folders privateStorage");
4198
- var BACKUPS = keys("frequency at keep dir storage sqlite");
4264
+ var BACKUPS = keys("frequency at keep dir storage sqlite encryptionKey");
4199
4265
  var AUDIT = keys("keepDays values failedLogins scope");
4200
4266
  var COLLECTION = keys(`slug labels fields useAsTitle drafts versions schedule preview access hooks
4201
4267
  admin`);
@@ -4637,7 +4703,7 @@ function validateConfig(config) {
4637
4703
  validateAdmin(config, add);
4638
4704
  validateAdminViews(config, add);
4639
4705
  validateUpload(config, add);
4640
- validateBackups(config, add);
4706
+ validateBackups(config, add, (issue) => issues.push(issue));
4641
4707
  validateAudit(config, add);
4642
4708
  validateAuth(config, add);
4643
4709
  validateCors(config.cors, add);
@@ -4725,6 +4791,14 @@ function validateCronSecret(value, push) {
4725
4791
  severity: "warning"
4726
4792
  });
4727
4793
  }
4794
+ function publicURLs(storage) {
4795
+ const url = storage.url;
4796
+ try {
4797
+ return typeof url === "function" && url.call(storage, "backup.db.gz") !== void 0;
4798
+ } catch {
4799
+ return false;
4800
+ }
4801
+ }
4728
4802
  function guessable(secret) {
4729
4803
  return /change[-_ ]?me|example|placeholder|secret-?key/i.test(secret) || new Set(secret).size < 8;
4730
4804
  }
@@ -5242,14 +5316,21 @@ function validateAudit(config, add) {
5242
5316
  }
5243
5317
  }
5244
5318
  }
5245
- function validateBackups(config, add) {
5319
+ function validateBackups(config, add, push) {
5320
+ const pushWarning = (path, message2, hint) => push({ path, message: message2, hint, severity: "warning" });
5246
5321
  const backups = config.backups;
5247
5322
  if (backups === void 0) return;
5248
5323
  if (typeof backups !== "object" || backups === null) {
5249
5324
  add("backups", "must be an object", "e.g. backups: { frequency: 'daily', keep: 7 }");
5250
5325
  return;
5251
5326
  }
5252
- const { frequency, at, keep, dir, storage, sqlite } = backups;
5327
+ const { frequency, at, keep, dir, storage, sqlite, encryptionKey } = backups;
5328
+ if (encryptionKey !== void 0 && (typeof encryptionKey !== "string" || encryptionKey.length < MIN_SECRET_LENGTH))
5329
+ add(
5330
+ "backups.encryptionKey",
5331
+ `must be at least ${MIN_SECRET_LENGTH} characters`,
5332
+ "e.g. `openssl rand -hex 32`, kept somewhere other than the backups"
5333
+ );
5253
5334
  if (frequency !== void 0 && frequency !== "daily" && frequency !== "weekly")
5254
5335
  add("backups.frequency", 'must be "daily" or "weekly"');
5255
5336
  if (at !== void 0 && (typeof at !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(at)))
@@ -5262,6 +5343,14 @@ function validateBackups(config, add) {
5262
5343
  add("backups.dir", "must not be the uploads folder: uploads are served publicly");
5263
5344
  if (storage !== void 0 && (typeof storage !== "object" || storage === null || typeof storage.put !== "function"))
5264
5345
  add("backups.storage", "must be a storage adapter, e.g. s3Storage()");
5346
+ else if (storage !== void 0 && publicURLs(storage)) {
5347
+ const message2 = encryptionKey ? "gives files public URLs; the backups are encrypted, but keep them in a private bucket" : "gives files public URLs: anyone with the address of a backup could download the whole database";
5348
+ pushWarning(
5349
+ "backups.storage",
5350
+ message2,
5351
+ "use a private bucket (no publicURL), or set backups.encryptionKey"
5352
+ );
5353
+ }
5265
5354
  if (sqlite !== void 0 && typeof sqlite !== "function")
5266
5355
  add(
5267
5356
  "backups.sqlite",
@@ -6057,7 +6146,7 @@ function resolveAudit(audit) {
6057
6146
  }
6058
6147
 
6059
6148
  // src/local-api.ts
6060
- import { createHmac as createHmac6, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual5 } from "crypto";
6149
+ import { createHmac as createHmac6, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual5 } from "crypto";
6061
6150
 
6062
6151
  // src/auth/common-passwords.ts
6063
6152
  var LIST = `
@@ -6821,13 +6910,13 @@ var EasyCMS = class {
6821
6910
  }
6822
6911
  ]);
6823
6912
  }
6824
- const random2 = randomBytes5(4).toString("hex");
6913
+ const random2 = randomBytes6(4).toString("hex");
6825
6914
  const isPrivate = await this.folders.isPrivate(folderOf(MEDIA, data));
6826
6915
  const storage = this.storageFor(isPrivate);
6827
6916
  const filename = withPrivacy(storageKey(file.name, mimeType, random2), isPrivate);
6828
6917
  const stored = [];
6829
6918
  try {
6830
- await storage.put(filename, file.data, { contentType: mimeType });
6919
+ await storage.put(filename, file.data, { contentType: withCharset(mimeType) });
6831
6920
  stored.push(filename);
6832
6921
  const dimensions = imageDimensions(file.data, mimeType);
6833
6922
  const sizes = await this.resizeImage(
@@ -6889,7 +6978,7 @@ var EasyCMS = class {
6889
6978
  ]);
6890
6979
  const isPrivate = await this.folders.isPrivate(folder);
6891
6980
  const storage = this.storageFor(isPrivate);
6892
- const key = withPrivacy(storageKey(name, type, randomBytes5(4).toString("hex")), isPrivate);
6981
+ const key = withPrivacy(storageKey(name, type, randomBytes6(4).toString("hex")), isPrivate);
6893
6982
  const upload = storage.uploadURL ? await storage.uploadURL(key, { contentType: type, size, expiresIn: UPLOAD_URL_SECONDS }) : null;
6894
6983
  const ticket = this.sign("upload", {
6895
6984
  key,
@@ -7173,7 +7262,7 @@ var EasyCMS = class {
7173
7262
  const old = [String(doc.filename)];
7174
7263
  const copy = async (source, target) => {
7175
7264
  const file = await from.get(source);
7176
- if (file) await to.put(target, file.body, { contentType });
7265
+ if (file) await to.put(target, file.body, { contentType: withCharset(contentType) });
7177
7266
  };
7178
7267
  await copy(String(doc.filename), filename);
7179
7268
  const sizes = {};
@@ -9048,7 +9137,7 @@ function durationSeconds(value) {
9048
9137
  }
9049
9138
 
9050
9139
  // src/rest/admin-modules.ts
9051
- import { createHash as createHash3 } from "crypto";
9140
+ import { createHash as createHash4 } from "crypto";
9052
9141
  import { readFile as readFile3 } from "fs/promises";
9053
9142
  import { createRequire } from "module";
9054
9143
  import { join as join3 } from "path";
@@ -9089,11 +9178,11 @@ async function readAdminModule(cms, index) {
9089
9178
  if (!file) return void 0;
9090
9179
  const body = await readFile3(file, "utf8").catch(() => void 0);
9091
9180
  if (body === void 0) return void 0;
9092
- return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
9181
+ return { body, etag: `"${createHash4("sha1").update(body).digest("base64url")}"` };
9093
9182
  }
9094
9183
 
9095
9184
  // src/version.ts
9096
- var VERSION = true ? "0.63.0" : "0.0.0-dev";
9185
+ var VERSION = true ? "0.65.0" : "0.0.0-dev";
9097
9186
 
9098
9187
  // src/rest/admin-deliveries.ts
9099
9188
  var PER_PAGE = 50;
@@ -10029,7 +10118,8 @@ async function respond(cms, request, url, path, root, options, production) {
10029
10118
  const headers = new Headers({
10030
10119
  "content-type": "application/json; charset=utf-8",
10031
10120
  "cache-control": "no-store",
10032
- "x-content-type-options": "nosniff"
10121
+ "x-content-type-options": "nosniff",
10122
+ ...production ? { "strict-transport-security": "max-age=31536000" } : {}
10033
10123
  });
10034
10124
  try {
10035
10125
  if (path === null) throw new HttpError("Not found", 404);
@@ -10046,7 +10136,8 @@ async function respond(cms, request, url, path, root, options, production) {
10046
10136
  context,
10047
10137
  via: signedIn ? via : null,
10048
10138
  token,
10049
- headers
10139
+ headers,
10140
+ trustProxy: options.trustProxy === true
10050
10141
  };
10051
10142
  const method = request.method.toUpperCase();
10052
10143
  if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
@@ -10222,6 +10313,7 @@ async function route(cms, ctx, method, path, options) {
10222
10313
  case "POST logout": {
10223
10314
  if (ctx.token) await cms.auth.logout(ctx.token);
10224
10315
  clearSessionCookies(ctx);
10316
+ ctx.headers.set("clear-site-data", '"cache"');
10225
10317
  return { body: { message: "Logged out" } };
10226
10318
  }
10227
10319
  case "GET me": {
@@ -10916,23 +11008,17 @@ function readToken(request) {
10916
11008
  return { token, via: token ? "cookie" : null };
10917
11009
  }
10918
11010
  function readCookie(request, name) {
10919
- const header = request.headers.get("cookie");
10920
- if (!header) return void 0;
10921
- for (const part of header.split(";")) {
10922
- const eq = part.indexOf("=");
10923
- if (eq > 0 && part.slice(0, eq).trim() === name)
10924
- return decodeURIComponent(part.slice(eq + 1).trim());
10925
- }
10926
- return void 0;
11011
+ return cookieValue(request.headers.get("cookie"), name);
10927
11012
  }
10928
- function sameHost(origin, request) {
11013
+ function sameHost(origin, request, trustProxy) {
10929
11014
  let host;
10930
11015
  try {
10931
11016
  host = new URL(origin).host;
10932
11017
  } catch {
10933
11018
  return false;
10934
11019
  }
10935
- const forwarded = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
11020
+ const trusted = trustProxy || !!process.env.VERCEL || !!process.env.NETLIFY;
11021
+ const forwarded = trusted ? request.headers.get("x-forwarded-host")?.split(",")[0]?.trim() : void 0;
10936
11022
  return host === (forwarded || request.headers.get("host"));
10937
11023
  }
10938
11024
  function checkCsrf(cms, ctx) {
@@ -10940,7 +11026,7 @@ function checkCsrf(cms, ctx) {
10940
11026
  const fetchSite = ctx.request.headers.get("sec-fetch-site");
10941
11027
  const cookie2 = ctx.via === "cookie" && ctx.token !== void 0;
10942
11028
  const { cors } = cms.config;
10943
- const trusted = origin !== null && (origin === ctx.url.origin || sameHost(origin, ctx.request) || cms.config.auth.trustedOrigins.includes(origin) || !cookie2 && (cors === "*" || cors.includes(origin)));
11029
+ const trusted = origin !== null && (origin === ctx.url.origin || sameHost(origin, ctx.request, ctx.trustProxy) || cms.config.auth.trustedOrigins.includes(origin) || !cookie2 && (cors === "*" || cors.includes(origin)));
10944
11030
  if (origin !== null && !trusted) throw new ForbiddenError("CSRF check failed: untrusted origin");
10945
11031
  if (origin === null && fetchSite === "cross-site")
10946
11032
  throw new ForbiddenError("CSRF check failed: cross-site request");
@@ -10962,7 +11048,7 @@ async function serveFile(cms, key, head) {
10962
11048
  const file = await cms.storage.get(key);
10963
11049
  if (!file) throw new HttpError("Not found", 404);
10964
11050
  const extension = key.slice(key.lastIndexOf(".") + 1);
10965
- const type = TYPE_BY_EXTENSION[extension] ?? "application/octet-stream";
11051
+ const type = withCharset(TYPE_BY_EXTENSION[extension] ?? "application/octet-stream");
10966
11052
  return new Response(
10967
11053
  head ? null : file.body,
10968
11054
  {
@@ -11005,7 +11091,9 @@ async function servePrivateFile(cms, ctx, key, head) {
11005
11091
  }
11006
11092
  const file = await cms.privateStorage.get(key);
11007
11093
  if (!file) throw new HttpError("Not found", 404);
11008
- const type = TYPE_BY_EXTENSION[key.slice(key.lastIndexOf(".") + 1)] ?? "application/octet-stream";
11094
+ const type = withCharset(
11095
+ TYPE_BY_EXTENSION[key.slice(key.lastIndexOf(".") + 1)] ?? "application/octet-stream"
11096
+ );
11009
11097
  return new Response(
11010
11098
  head ? null : file.body,
11011
11099
  {
@@ -11116,9 +11204,14 @@ function isSecure(ctx) {
11116
11204
  return ctx.url.protocol === "https:" || process.env.NODE_ENV === "production";
11117
11205
  }
11118
11206
  function cookie(ctx, name, value, opts) {
11119
- const parts = [`${name}=${encodeURIComponent(value)}`, "Path=/", "SameSite=Lax"];
11207
+ const secure = isSecure(ctx);
11208
+ const parts = [
11209
+ `${secure ? hostCookie(name) : name}=${encodeURIComponent(value)}`,
11210
+ "Path=/",
11211
+ "SameSite=Lax"
11212
+ ];
11120
11213
  if (opts.httpOnly) parts.push("HttpOnly");
11121
- if (isSecure(ctx)) parts.push("Secure");
11214
+ if (secure) parts.push("Secure");
11122
11215
  if (opts.maxAge !== void 0) parts.push(`Max-Age=${opts.maxAge}`);
11123
11216
  return parts.join("; ");
11124
11217
  }
@@ -11197,6 +11290,9 @@ function setSessionCookies(cms, ctx, session) {
11197
11290
  function clearSessionCookies(ctx) {
11198
11291
  ctx.headers.append("set-cookie", cookie(ctx, SESSION_COOKIE, "", { httpOnly: true, maxAge: 0 }));
11199
11292
  ctx.headers.append("set-cookie", cookie(ctx, CSRF_COOKIE, "", { httpOnly: false, maxAge: 0 }));
11293
+ if (isSecure(ctx))
11294
+ for (const name of [SESSION_COOKIE, CSRF_COOKIE])
11295
+ ctx.headers.append("set-cookie", `${name}=; Path=/; Max-Age=0; Secure; SameSite=Lax`);
11200
11296
  }
11201
11297
  function errorResponse(cms, error, headers, production) {
11202
11298
  if (error instanceof EasyCMSError) {
@@ -11262,6 +11358,7 @@ function createApiHandler(config, options = {}) {
11262
11358
  if (!handler) {
11263
11359
  handler = createRestHandler(cms, {
11264
11360
  getClientIp,
11361
+ trustProxy: options.trustProxy === true,
11265
11362
  ...options.basePath ? { basePath: options.basePath } : {}
11266
11363
  });
11267
11364
  handlers.set(cms, handler);
@@ -11356,6 +11453,7 @@ export {
11356
11453
  configSignature,
11357
11454
  copyDatabase,
11358
11455
  diskStorage,
11456
+ decryptBackup,
11359
11457
  writeBackupFile,
11360
11458
  consoleLogger,
11361
11459
  silentLogger,
@@ -271,6 +271,12 @@ function sizeKey(filename, size) {
271
271
  return filename.replace(/\.([^.]+)$/, `-${size}.$1`);
272
272
  }
273
273
  var PRIVATE_KEY = /^[\p{L}\p{M}\p{N}-]+\.private(-[\p{L}\p{N}_-]+)?\.[a-z0-9]+$/u;
274
+ function withCharset(type) {
275
+ if (/;\s*charset=/i.test(type)) return type;
276
+ const base = type.split(";")[0]?.trim().toLowerCase() ?? "";
277
+ const text = base.startsWith("text/") || /^application\/(json|xml|javascript|ecmascript)$/.test(base) || /\+(json|xml)$/.test(base);
278
+ return text ? `${type}; charset=utf-8` : type;
279
+ }
274
280
 
275
281
  // src/fields.ts
276
282
  var uniqueWithinOf = (field) => field.uniqueWithin === void 0 ? [] : [field.uniqueWithin].flat();
@@ -534,6 +540,7 @@ export {
534
540
  withPrivacy,
535
541
  sizeKey,
536
542
  PRIVATE_KEY,
543
+ withCharset,
537
544
  uniqueWithinOf,
538
545
  isHasMany,
539
546
  mimeAllowedBy,
@@ -2148,7 +2148,11 @@ interface AuditVerification {
2148
2148
  checked: number;
2149
2149
  /** Entries whose signature does not match: changed after they were written. */
2150
2150
  invalid: ID[];
2151
- /** Missing ids between entries: deleted entries, or (rarely) inserts that failed. */
2151
+ /**
2152
+ * Missing ids between entries: deleted entries, or (rarely) inserts that failed. Ids are never
2153
+ * reused, so the newest entries deleted show up here once anything is written after them (a
2154
+ * check writes an entry itself). The server's log has each check's last id, to compare with.
2155
+ */
2152
2156
  gaps: number;
2153
2157
  at: string;
2154
2158
  }
@@ -3102,6 +3106,13 @@ interface BackupsConfig {
3102
3106
  readonly dir?: string;
3103
3107
  /** Where backups are stored instead, e.g. `s3Storage()` with a private bucket. */
3104
3108
  readonly storage?: StorageAdapter;
3109
+ /**
3110
+ * Encrypts the backup files (AES-256-GCM), so a leaked bucket or disk holds nothing readable.
3111
+ * At least 32 characters, e.g. from `process.env.EASY_CMS_BACKUP_KEY`; keep a copy somewhere
3112
+ * other than the backups: without it they can't be restored. Downloads from the admin are
3113
+ * decrypted; `easy-cms backup:decrypt <file>` decrypts one by hand.
3114
+ */
3115
+ readonly encryptionKey?: string;
3105
3116
  /**
3106
3117
  * For Postgres: the SQLite adapter that writes the backup file, `sqlite` from
3107
3118
  * `@easy-cms/db-sqlite`. SQLite databases back themselves up and don't need it.
@@ -1,5 +1,7 @@
1
- import { I as ID, ab as Config, au as EasyCMS, bd as LocalizationConfig, aD as Field, aH as FieldError, aS as FilterOptions, af as CreateEasyCMSOptions, bI as ResolvedConfig } from './config-Dgs6FE0S.js';
1
+ import { I as ID, ab as Config, au as EasyCMS, bd as LocalizationConfig, aD as Field, aH as FieldError, aS as FilterOptions, af as CreateEasyCMSOptions, bI as ResolvedConfig } from './config-DE8ez-a9.js';
2
2
 
3
+ /** Decrypts a file from `encryptBackup`; throws for another key or a changed file. */
4
+ declare function decryptBackup(data: Uint8Array, encryptionKey: string): Uint8Array;
3
5
  type BackupState = 'pending' | 'running' | 'done' | 'failed';
4
6
  /** One backup as the admin shows it (`GET <api>/admin/backups`). */
5
7
  interface AdminBackup {
@@ -172,6 +174,11 @@ interface RestHandlerOptions {
172
174
  readonly basePath?: string;
173
175
  /** Client IP, used with the email to rate-limit logins. Adapters provide it. */
174
176
  readonly getClientIp?: (request: Request) => string | undefined;
177
+ /**
178
+ * Behind a reverse proxy you control: its `X-Forwarded-Host` names the public host (for the
179
+ * CSRF check of a request's Origin). Vercel and Netlify are recognized without it.
180
+ */
181
+ readonly trustProxy?: boolean;
175
182
  }
176
183
  type RestHandler = (request: Request) => Promise<Response>;
177
184
  /** Creates the REST API as a Web-standard `(Request) => Response` handler. */
@@ -183,6 +190,7 @@ declare function createRestHandler<C extends Config>(instance: EasyCMS<C>, optio
183
190
  * use it; the standalone server does.
184
191
  */
185
192
  declare function createRootEndpointHandler<C extends Config>(instance: EasyCMS<C>, options?: Pick<RestHandlerOptions, 'getClientIp'>): (request: Request) => Promise<Response | undefined>;
193
+ /** A cookie of the request; the `__Host-` one (set over HTTPS) first. */
186
194
  declare function readCookie(request: Request, name: string): string | undefined;
187
195
 
188
- export { type AdminBackup as A, type BackupState as B, CONFIG_FILE_NAMES as C, DEFAULT_ADMIN_PATH as D, validateFields as E, writeBackupFile as F, type LoadConfigOptions as L, type RestHandler as R, type AdminBackups as a, type ApiHandlerOptions as b, type RestHandlerOptions as c, createApiHandler as d, createRestHandler as e, slugify as f, CSRF_COOKIE as g, CSRF_HEADER as h, DEFAULT_API_PATH as i, DEFAULT_MAX_FILE_SIZE as j, DEFAULT_TOKEN_EXPIRATION as k, loadConfig as l, applyDefaults as m, applyPlugins as n, createRootEndpointHandler as o, fillMissing as p, findConfigFile as q, resolveConfig as r, sharedEasyCMS as s, forwardedClientIp as t, generateSlugs as u, importConfig as v, mergeForUpdate as w, parseId as x, platformClientIp as y, readCookie as z };
196
+ export { type AdminBackup as A, type BackupState as B, CONFIG_FILE_NAMES as C, DEFAULT_ADMIN_PATH as D, readCookie as E, validateFields as F, writeBackupFile as G, type LoadConfigOptions as L, type RestHandler as R, type AdminBackups as a, type ApiHandlerOptions as b, type RestHandlerOptions as c, createApiHandler as d, createRestHandler as e, slugify as f, CSRF_COOKIE as g, CSRF_HEADER as h, DEFAULT_API_PATH as i, DEFAULT_MAX_FILE_SIZE as j, DEFAULT_TOKEN_EXPIRATION as k, loadConfig as l, applyDefaults as m, applyPlugins as n, createRootEndpointHandler as o, decryptBackup as p, fillMissing as q, resolveConfig as r, sharedEasyCMS as s, findConfigFile as t, forwardedClientIp as u, generateSlugs as v, importConfig as w, mergeForUpdate as x, parseId as y, platformClientIp as z };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-Dgs6FE0S.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 CollectionSlug, ab as Config, ac as ConfigError, ad as ConfigIssue, ae as ContainerAdmin, af as CreateEasyCMSOptions, ag as CreateInput, ah as CreateMigrationResult, ai as CustomField, aj as CustomFieldTypes, ak as DashboardWidget, al as Database, am as DatabaseAdapter, an as DatabaseInitArgs, ao as DatabaseTransfer, ap as DateField, aq as DepthOptions, ar as DirectUpload, as as DiskStorageOptions, at as DocumentOf, au as EasyCMS, av as EasyCMSError, aw as EmailAdapter, ax as EmailField, ay as EmailMessage, az as Endpoint, aA as EndpointRequest, aB as ErrorCode, aC as FIELD_TYPES, aD as Field, aE as FieldAccess, aF as FieldAccessFn, aG as FieldAdmin, aH as FieldError, aI as FieldRule, aJ as FieldType, aK as FieldTypeBase, aL as FieldTypeDefinition, aM as FieldTypeValidateContext, aN as FieldValidate, aO as FieldValidateContext, aP as FieldValue, aQ as FieldsInput, aR as FieldsValue, aS as FilterOptions, aT as FilterOptionsArgs, aU as FindArgs, aV as FindOptions, aW as FolderLevel, aX as FolderPermissions, aY as ForbiddenError, aZ as GlobalAccess, a_ as GlobalConfig, a$ as GlobalDocument, b0 as GlobalHooks, b1 as GlobalInput, b2 as GlobalSlug, b3 as GroupField, b4 as ImageSize, b5 as IncrementOptions, b6 as InferCollection, b7 as InferGlobal, b8 as JobConfig, b9 as JsonField, ba as Label, bb as LayoutNode, bc as LivePreview, bd as LocalizationConfig, be as Logger, bf as LoginArgs, bg as MediaDocument, bh as MediaSize, bi as MembersConfig, bj as MembersSignup, bk as MigrationInfo, bl as NavGroup, bm as NotFoundError, bn as NumberField, bo as OnRequest, bp as OnRequestArgs, bq as Operation, br as PLUGIN_API_VERSION, bs as PasswordEmail, bt as PasswordEmailArgs, bu as PasswordEmailFn, bv as PasswordLinkOptions, bw as PayloadTooLargeError, bx as Plugin, by as PluginInfo, bz as PluginTypes, bA as PreviewTarget, bB as PreviewURL, bC as QueryError, bD as ROLE_OPERATIONS, bE as RawDocument, bF as ReadOptions, bG as RelationshipField, bH as RequestContext, bI as ResolvedConfig, bJ as RichTextDocument, bK as RichTextField, bL as Role, bM as RoleOperation, bN as RolePermissions, bO as Roles, bP as RoutesConfig, bQ as ScheduledPublish, bR as SchemaError, bS as SchemaMode, bT as SelectField, bU as SelectOption, bV as Session, bW as SidebarPanel, bX as SignupArgs, bY as SignupResult, bZ as SingleSignOn, b_ as SlugField, b$ as SlugOf, c0 as SsoOutcome, c1 as SsoProviderRef, c2 as StorageAdapter, c3 as StoredFile, c4 as TextField, c5 as TextareaField, c6 as TooManyRequestsError, c7 as TypedPlugin, c8 as UnauthorizedError, c9 as UpdateInput, ca as UpdateOptions, cb as UploadConfig, cc as UploadField, cd as UploadFromURLConfig, ce as UserIdentity, cf as ValidationError, cg as Version, ch as VersionSummary, ci as VersionsConfig, cj as WEBHOOK_EVENTS, ck as WebhookConfig, cl as WebhookEvent, cm as WebhookPayload, cn as Where, co as WhereOperators, cp as anyone, cq as consoleEmail, cr as consoleLogger, cs as createEasyCMS, ct as defineConfig, cu as defineFieldType, cv as definePlugin, cw as diskStorage, cx as hasRows, cy as isAdmin, cz as isHasMany, cA as isSignedIn, cB as isStaff, cC as isSystemAdmin, cD as mimeAllowedBy, cE as rowFields, cF as silentLogger, cG as uniqueWithinOf } from './config-Dgs6FE0S.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-8hrkYMuR.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-CcQjkj5Q.js';
1
+ import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-DE8ez-a9.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 CollectionSlug, ab as Config, ac as ConfigError, ad as ConfigIssue, ae as ContainerAdmin, af as CreateEasyCMSOptions, ag as CreateInput, ah as CreateMigrationResult, ai as CustomField, aj as CustomFieldTypes, ak as DashboardWidget, al as Database, am as DatabaseAdapter, an as DatabaseInitArgs, ao as DatabaseTransfer, ap as DateField, aq as DepthOptions, ar as DirectUpload, as as DiskStorageOptions, at as DocumentOf, au as EasyCMS, av as EasyCMSError, aw as EmailAdapter, ax as EmailField, ay as EmailMessage, az as Endpoint, aA as EndpointRequest, aB as ErrorCode, aC as FIELD_TYPES, aD as Field, aE as FieldAccess, aF as FieldAccessFn, aG as FieldAdmin, aH as FieldError, aI as FieldRule, aJ as FieldType, aK as FieldTypeBase, aL as FieldTypeDefinition, aM as FieldTypeValidateContext, aN as FieldValidate, aO as FieldValidateContext, aP as FieldValue, aQ as FieldsInput, aR as FieldsValue, aS as FilterOptions, aT as FilterOptionsArgs, aU as FindArgs, aV as FindOptions, aW as FolderLevel, aX as FolderPermissions, aY as ForbiddenError, aZ as GlobalAccess, a_ as GlobalConfig, a$ as GlobalDocument, b0 as GlobalHooks, b1 as GlobalInput, b2 as GlobalSlug, b3 as GroupField, b4 as ImageSize, b5 as IncrementOptions, b6 as InferCollection, b7 as InferGlobal, b8 as JobConfig, b9 as JsonField, ba as Label, bb as LayoutNode, bc as LivePreview, bd as LocalizationConfig, be as Logger, bf as LoginArgs, bg as MediaDocument, bh as MediaSize, bi as MembersConfig, bj as MembersSignup, bk as MigrationInfo, bl as NavGroup, bm as NotFoundError, bn as NumberField, bo as OnRequest, bp as OnRequestArgs, bq as Operation, br as PLUGIN_API_VERSION, bs as PasswordEmail, bt as PasswordEmailArgs, bu as PasswordEmailFn, bv as PasswordLinkOptions, bw as PayloadTooLargeError, bx as Plugin, by as PluginInfo, bz as PluginTypes, bA as PreviewTarget, bB as PreviewURL, bC as QueryError, bD as ROLE_OPERATIONS, bE as RawDocument, bF as ReadOptions, bG as RelationshipField, bH as RequestContext, bI as ResolvedConfig, bJ as RichTextDocument, bK as RichTextField, bL as Role, bM as RoleOperation, bN as RolePermissions, bO as Roles, bP as RoutesConfig, bQ as ScheduledPublish, bR as SchemaError, bS as SchemaMode, bT as SelectField, bU as SelectOption, bV as Session, bW as SidebarPanel, bX as SignupArgs, bY as SignupResult, bZ as SingleSignOn, b_ as SlugField, b$ as SlugOf, c0 as SsoOutcome, c1 as SsoProviderRef, c2 as StorageAdapter, c3 as StoredFile, c4 as TextField, c5 as TextareaField, c6 as TooManyRequestsError, c7 as TypedPlugin, c8 as UnauthorizedError, c9 as UpdateInput, ca as UpdateOptions, cb as UploadConfig, cc as UploadField, cd as UploadFromURLConfig, ce as UserIdentity, cf as ValidationError, cg as Version, ch as VersionSummary, ci as VersionsConfig, cj as WEBHOOK_EVENTS, ck as WebhookConfig, cl as WebhookEvent, cm as WebhookPayload, cn as Where, co as WhereOperators, cp as anyone, cq as consoleEmail, cr as consoleLogger, cs as createEasyCMS, ct as defineConfig, cu as defineFieldType, cv as definePlugin, cw as diskStorage, cx as hasRows, cy as isAdmin, cz as isHasMany, cA as isSignedIn, cB as isStaff, cC as isSystemAdmin, cD as mimeAllowedBy, cE as rowFields, cF as silentLogger, cG as uniqueWithinOf } from './config-DE8ez-a9.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-CX7E7RZZ.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-Cm1ehtkm.js';
5
5
  export { FieldCondition, matchesCondition } from './conditions.js';
6
6
 
7
7
  type DeliveryKind = 'webhook' | 'email';
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  sharedEasyCMS,
24
24
  silentLogger,
25
25
  slugify
26
- } from "./chunk-LN4M4BUP.js";
26
+ } from "./chunk-BGGQ36LR.js";
27
27
  import {
28
28
  matchesCondition
29
29
  } from "./chunk-U266LV4M.js";
@@ -48,7 +48,7 @@ import {
48
48
  defineFieldType,
49
49
  definePlugin,
50
50
  warnDeprecated
51
- } from "./chunk-FPFWLQB5.js";
51
+ } from "./chunk-RTKWMVAC.js";
52
52
  export {
53
53
  ADAPTER_API_VERSION,
54
54
  ADMIN_API_VERSION,
@@ -1,6 +1,6 @@
1
- import { ab as Config, al as Database, bl as NavGroup, a6 as CollectionConfig, I as ID, bE as RawDocument, aD as Field, bI as ResolvedConfig, ad as ConfigIssue } from './config-Dgs6FE0S.js';
2
- export { cH as API_KEYS, cI as API_KEY_GLOBAL_OPERATIONS, cJ as API_KEY_OPERATIONS, cK as DEFAULT_MAX_VERSIONS, cL as DEFAULT_PASSWORD_EMAILS, cM as FIELD_TYPE_BASES, cN as FOLDER_LEVELS, cO as MediaFolders, cP as SSO_COOKIE, cQ as codeOfStatus, cx as hasRows, cz as isHasMany, cR as keyAllows, cD as mimeAllowedBy, cE as rowFields, cG as uniqueWithinOf } from './config-Dgs6FE0S.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 fillMissing, q as findConfigFile, t as forwardedClientIp, u as generateSlugs, v as importConfig, w as mergeForUpdate, x as parseId, y as platformClientIp, z as readCookie, E as validateFields, F as writeBackupFile } from './handler-CcQjkj5Q.js';
1
+ import { ab as Config, al as Database, bl as NavGroup, a6 as CollectionConfig, I as ID, bE as RawDocument, aD as Field, bI as ResolvedConfig, ad as ConfigIssue } from './config-DE8ez-a9.js';
2
+ export { cH as API_KEYS, cI as API_KEY_GLOBAL_OPERATIONS, cJ as API_KEY_OPERATIONS, cK as DEFAULT_MAX_VERSIONS, cL as DEFAULT_PASSWORD_EMAILS, cM as FIELD_TYPE_BASES, cN as FOLDER_LEVELS, cO as MediaFolders, cP as SSO_COOKIE, cQ as codeOfStatus, cx as hasRows, cz as isHasMany, cR as keyAllows, cD as mimeAllowedBy, cE as rowFields, cG as uniqueWithinOf } from './config-DE8ez-a9.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 mergeForUpdate, y as parseId, z as platformClientIp, E as readCookie, F as validateFields, G as writeBackupFile } from './handler-Cm1ehtkm.js';
4
4
  export { conditionIssues } from './conditions.js';
5
5
 
6
6
  /** The session cookie the REST API sets at login. */
package/dist/internal.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  configSignature,
32
32
  copyDatabase,
33
33
  createRootEndpointHandler,
34
+ decryptBackup,
34
35
  fillMissing,
35
36
  findConfigFile,
36
37
  formToken,
@@ -52,7 +53,7 @@ import {
52
53
  verifyPassword,
53
54
  verifyTurnstile,
54
55
  writeBackupFile
55
- } from "./chunk-LN4M4BUP.js";
56
+ } from "./chunk-BGGQ36LR.js";
56
57
  import {
57
58
  conditionIssues
58
59
  } from "./chunk-U266LV4M.js";
@@ -70,7 +71,7 @@ import {
70
71
  rowFields,
71
72
  sniffMimeType,
72
73
  uniqueWithinOf
73
- } from "./chunk-FPFWLQB5.js";
74
+ } from "./chunk-RTKWMVAC.js";
74
75
 
75
76
  // src/typegen.ts
76
77
  var pascal = (name) => name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
@@ -275,6 +276,7 @@ export {
275
276
  configSignature,
276
277
  copyDatabase,
277
278
  createRootEndpointHandler,
279
+ decryptBackup,
278
280
  expandMimeTypes,
279
281
  fillMissing,
280
282
  findConfigFile,
@@ -1,4 +1,4 @@
1
- import { ba as Label, aD as Field, I as ID, k as AdminLocale, t as AdminSwitcher, c1 as SsoProviderRef } from './config-Dgs6FE0S.js';
1
+ import { ba as Label, aD as Field, I as ID, k as AdminLocale, t as AdminSwitcher, c1 as SsoProviderRef } from './config-DE8ez-a9.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, ab as Config, ac as ConfigError, ak as DashboardWidget, av as EasyCMSError, az as Endpoint, aA as EndpointRequest, aB as ErrorCode, aD as Field, aE as FieldAccess, aH as FieldError, aK as FieldTypeBase, aL as FieldTypeDefinition, aM as FieldTypeValidateContext, aY as ForbiddenError, a_ as GlobalConfig, I as ID, b8 as JobConfig, ba as Label, bl as NavGroup, bm as NotFoundError, bo as OnRequest, bp as OnRequestArgs, br as PLUGIN_API_VERSION, bw as PayloadTooLargeError, bx as Plugin, by as PluginInfo, bz as PluginTypes, bC as QueryError, bH as RequestContext, c6 as TooManyRequestsError, c7 as TypedPlugin, c8 as UnauthorizedError, cf as ValidationError, cn as Where, cu as defineFieldType, cv as definePlugin } from './config-Dgs6FE0S.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-8hrkYMuR.js';
1
+ export { a as ADMIN_ICONS, g as AdminComponent, j as AdminIcon, S as AuthUser, a3 as CliCommand, a4 as CollectionAccess, a6 as CollectionConfig, ab as Config, ac as ConfigError, ak as DashboardWidget, av as EasyCMSError, az as Endpoint, aA as EndpointRequest, aB as ErrorCode, aD as Field, aE as FieldAccess, aH as FieldError, aK as FieldTypeBase, aL as FieldTypeDefinition, aM as FieldTypeValidateContext, aY as ForbiddenError, a_ as GlobalConfig, I as ID, b8 as JobConfig, ba as Label, bl as NavGroup, bm as NotFoundError, bo as OnRequest, bp as OnRequestArgs, br as PLUGIN_API_VERSION, bw as PayloadTooLargeError, bx as Plugin, by as PluginInfo, bz as PluginTypes, bC as QueryError, bH as RequestContext, c6 as TooManyRequestsError, c7 as TypedPlugin, c8 as UnauthorizedError, cf as ValidationError, cn as Where, cu as defineFieldType, cv as definePlugin } from './config-DE8ez-a9.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-CX7E7RZZ.js';
3
3
  import './conditions.js';
package/dist/plugin.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  defineFieldType,
17
17
  definePlugin,
18
18
  warnDeprecated
19
- } from "./chunk-FPFWLQB5.js";
19
+ } from "./chunk-RTKWMVAC.js";
20
20
  export {
21
21
  ADMIN_API_VERSION,
22
22
  ADMIN_ICONS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.63.0",
3
+ "version": "0.65.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",