@easy-cms/core 0.64.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.
@@ -522,6 +522,7 @@ var CSV_LIMIT = 1e4;
522
522
  var DAY = 864e5;
523
523
  var BULKY = /* @__PURE__ */ new Set(["richText", "blocks", "array", "json"]);
524
524
  var auditContext = new AsyncLocalStorage();
525
+ var SIGN_IN_FAILURES = /* @__PURE__ */ new Set(["login.failed", "login.locked", "sso.failed"]);
525
526
  function stable(value) {
526
527
  if (value === void 0 || value === null) return "null";
527
528
  if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
@@ -578,7 +579,15 @@ var AuditLog = class {
578
579
  * counted, and the dashboard says so.
579
580
  */
580
581
  async record(entry) {
581
- 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
+ }
582
591
  try {
583
592
  const context = auditContext.getStore();
584
593
  const user = entry.user !== void 0 ? entry.user : context?.user ?? null;
@@ -731,8 +740,11 @@ var AuditLog = class {
731
740
  await this.record({
732
741
  action: "audit.verify",
733
742
  target: "audit",
734
- detail: { checked, invalid: invalid.length, gaps }
743
+ detail: { checked, invalid: invalid.length, gaps, lastId: previous ?? null }
735
744
  });
745
+ this.cms.logger.info(
746
+ `Audit log checked: ${checked} entries, last #${previous ?? "-"}; ${invalid.length} changed, ${gaps} missing`
747
+ );
736
748
  return verification;
737
749
  }
738
750
  /**
@@ -2832,12 +2844,38 @@ function diskStorage(options = {}) {
2832
2844
  }
2833
2845
 
2834
2846
  // src/backups.ts
2847
+ import { createCipheriv, createDecipheriv, createHash as createHash3, randomBytes as randomBytes5 } from "crypto";
2835
2848
  import { mkdtemp, readFile as readFile2, rm as rm2 } from "fs/promises";
2836
2849
  import { tmpdir } from "os";
2837
2850
  import { join as join2 } from "path";
2838
2851
  import { promisify } from "util";
2839
2852
  import { gzip } from "zlib";
2840
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
+ }
2841
2879
  var DEFAULT_BACKUP_TIME = "03:00";
2842
2880
  var DEFAULT_BACKUP_KEEP = 7;
2843
2881
  var DEFAULT_DIR = "backups";
@@ -2982,11 +3020,15 @@ async function runBackup(cms, record) {
2982
3020
  try {
2983
3021
  const file = join2(dir, "backup.db");
2984
3022
  await writeBackupFile(cms, file);
2985
- 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;
2986
3026
  const storage = await backupStorage(cms);
2987
- const filename = filenameFor(cms, started);
2988
- const key = await storage.get(filename) ? filename.replace(/\.db\.gz$/, `-${row.id}.db.gz`) : filename;
2989
- 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
+ });
2990
3032
  await pruneBackups(cms, 1);
2991
3033
  row = await update(cms, row, {
2992
3034
  state: "done",
@@ -3113,8 +3155,20 @@ async function downloadBackup(cms, id, by) {
3113
3155
  const file = await (await backupStorage(cms)).get(row.filename);
3114
3156
  if (!file)
3115
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
+ }
3116
3170
  await update(cms, row, { downloadedBy: by, downloadedAt: (/* @__PURE__ */ new Date()).toISOString() });
3117
- return { filename: row.filename, body: file.body };
3171
+ return { filename, body };
3118
3172
  }
3119
3173
  async function deleteBackup(cms, id) {
3120
3174
  const row = await cms.db.findById({ collection: DATABASE_BACKUPS, id });
@@ -4207,7 +4261,7 @@ var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedO
4207
4261
  resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
4208
4262
  var MEMBERS = keys("roles signUp pages emails");
4209
4263
  var UPLOAD = keys("dir maxFileSize mimeTypes storage imageSizes fromURL folders privateStorage");
4210
- var BACKUPS = keys("frequency at keep dir storage sqlite");
4264
+ var BACKUPS = keys("frequency at keep dir storage sqlite encryptionKey");
4211
4265
  var AUDIT = keys("keepDays values failedLogins scope");
4212
4266
  var COLLECTION = keys(`slug labels fields useAsTitle drafts versions schedule preview access hooks
4213
4267
  admin`);
@@ -4649,7 +4703,7 @@ function validateConfig(config) {
4649
4703
  validateAdmin(config, add);
4650
4704
  validateAdminViews(config, add);
4651
4705
  validateUpload(config, add);
4652
- validateBackups(config, add);
4706
+ validateBackups(config, add, (issue) => issues.push(issue));
4653
4707
  validateAudit(config, add);
4654
4708
  validateAuth(config, add);
4655
4709
  validateCors(config.cors, add);
@@ -4737,6 +4791,14 @@ function validateCronSecret(value, push) {
4737
4791
  severity: "warning"
4738
4792
  });
4739
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
+ }
4740
4802
  function guessable(secret) {
4741
4803
  return /change[-_ ]?me|example|placeholder|secret-?key/i.test(secret) || new Set(secret).size < 8;
4742
4804
  }
@@ -5254,14 +5316,21 @@ function validateAudit(config, add) {
5254
5316
  }
5255
5317
  }
5256
5318
  }
5257
- function validateBackups(config, add) {
5319
+ function validateBackups(config, add, push) {
5320
+ const pushWarning = (path, message2, hint) => push({ path, message: message2, hint, severity: "warning" });
5258
5321
  const backups = config.backups;
5259
5322
  if (backups === void 0) return;
5260
5323
  if (typeof backups !== "object" || backups === null) {
5261
5324
  add("backups", "must be an object", "e.g. backups: { frequency: 'daily', keep: 7 }");
5262
5325
  return;
5263
5326
  }
5264
- 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
+ );
5265
5334
  if (frequency !== void 0 && frequency !== "daily" && frequency !== "weekly")
5266
5335
  add("backups.frequency", 'must be "daily" or "weekly"');
5267
5336
  if (at !== void 0 && (typeof at !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(at)))
@@ -5274,6 +5343,14 @@ function validateBackups(config, add) {
5274
5343
  add("backups.dir", "must not be the uploads folder: uploads are served publicly");
5275
5344
  if (storage !== void 0 && (typeof storage !== "object" || storage === null || typeof storage.put !== "function"))
5276
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
+ }
5277
5354
  if (sqlite !== void 0 && typeof sqlite !== "function")
5278
5355
  add(
5279
5356
  "backups.sqlite",
@@ -6069,7 +6146,7 @@ function resolveAudit(audit) {
6069
6146
  }
6070
6147
 
6071
6148
  // src/local-api.ts
6072
- 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";
6073
6150
 
6074
6151
  // src/auth/common-passwords.ts
6075
6152
  var LIST = `
@@ -6833,7 +6910,7 @@ var EasyCMS = class {
6833
6910
  }
6834
6911
  ]);
6835
6912
  }
6836
- const random2 = randomBytes5(4).toString("hex");
6913
+ const random2 = randomBytes6(4).toString("hex");
6837
6914
  const isPrivate = await this.folders.isPrivate(folderOf(MEDIA, data));
6838
6915
  const storage = this.storageFor(isPrivate);
6839
6916
  const filename = withPrivacy(storageKey(file.name, mimeType, random2), isPrivate);
@@ -6901,7 +6978,7 @@ var EasyCMS = class {
6901
6978
  ]);
6902
6979
  const isPrivate = await this.folders.isPrivate(folder);
6903
6980
  const storage = this.storageFor(isPrivate);
6904
- const key = withPrivacy(storageKey(name, type, randomBytes5(4).toString("hex")), isPrivate);
6981
+ const key = withPrivacy(storageKey(name, type, randomBytes6(4).toString("hex")), isPrivate);
6905
6982
  const upload = storage.uploadURL ? await storage.uploadURL(key, { contentType: type, size, expiresIn: UPLOAD_URL_SECONDS }) : null;
6906
6983
  const ticket = this.sign("upload", {
6907
6984
  key,
@@ -9060,7 +9137,7 @@ function durationSeconds(value) {
9060
9137
  }
9061
9138
 
9062
9139
  // src/rest/admin-modules.ts
9063
- import { createHash as createHash3 } from "crypto";
9140
+ import { createHash as createHash4 } from "crypto";
9064
9141
  import { readFile as readFile3 } from "fs/promises";
9065
9142
  import { createRequire } from "module";
9066
9143
  import { join as join3 } from "path";
@@ -9101,11 +9178,11 @@ async function readAdminModule(cms, index) {
9101
9178
  if (!file) return void 0;
9102
9179
  const body = await readFile3(file, "utf8").catch(() => void 0);
9103
9180
  if (body === void 0) return void 0;
9104
- return { body, etag: `"${createHash3("sha1").update(body).digest("base64url")}"` };
9181
+ return { body, etag: `"${createHash4("sha1").update(body).digest("base64url")}"` };
9105
9182
  }
9106
9183
 
9107
9184
  // src/version.ts
9108
- var VERSION = true ? "0.64.0" : "0.0.0-dev";
9185
+ var VERSION = true ? "0.65.0" : "0.0.0-dev";
9109
9186
 
9110
9187
  // src/rest/admin-deliveries.ts
9111
9188
  var PER_PAGE = 50;
@@ -11376,6 +11453,7 @@ export {
11376
11453
  configSignature,
11377
11454
  copyDatabase,
11378
11455
  diskStorage,
11456
+ decryptBackup,
11379
11457
  writeBackupFile,
11380
11458
  consoleLogger,
11381
11459
  silentLogger,
@@ -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 {
@@ -191,4 +193,4 @@ declare function createRootEndpointHandler<C extends Config>(instance: EasyCMS<C
191
193
  /** A cookie of the request; the `__Host-` one (set over HTTPS) first. */
192
194
  declare function readCookie(request: Request, name: string): string | undefined;
193
195
 
194
- 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-CcmsrLs8.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-JZW6G6XI.js";
26
+ } from "./chunk-BGGQ36LR.js";
27
27
  import {
28
28
  matchesCondition
29
29
  } from "./chunk-U266LV4M.js";
@@ -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-CcmsrLs8.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-JZW6G6XI.js";
56
+ } from "./chunk-BGGQ36LR.js";
56
57
  import {
57
58
  conditionIssues
58
59
  } from "./chunk-U266LV4M.js";
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "0.64.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",