@easy-cms/core 1.18.0 → 1.19.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.
@@ -154,6 +154,7 @@ var ROLES = "user-roles";
154
154
  var USER_IDENTITIES = "user-identities";
155
155
  var AUDIT_LOGS = "audit-logs";
156
156
  var OUTBOX = "outbox";
157
+ var RATE_LIMITS = "rate-limits";
157
158
  var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
158
159
  SESSIONS,
159
160
  LOGIN_ATTEMPTS,
@@ -166,7 +167,8 @@ var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
166
167
  ROLES,
167
168
  USER_IDENTITIES,
168
169
  AUDIT_LOGS,
169
- OUTBOX
170
+ OUTBOX,
171
+ RATE_LIMITS
170
172
  ]);
171
173
  var DEFAULT_ROLES = ["admin", "editor"];
172
174
  var CREATED_BY_FIELD = {
@@ -640,6 +642,17 @@ var outboxCollection = {
640
642
  { name: "error", type: "text" }
641
643
  ]
642
644
  };
645
+ var rateLimitsCollection = {
646
+ slug: RATE_LIMITS,
647
+ access: { read: nobody, create: nobody, update: nobody, delete: nobody },
648
+ fields: [
649
+ // `<anonymous|user|apiKey>:<id or HMAC of the IP>:<window number>`
650
+ { name: "key", type: "text", required: true, unique: true },
651
+ { name: "count", type: "number", required: true },
652
+ // When the window is over, for clearing old rows.
653
+ { name: "expiresAt", type: "text", required: true, index: true }
654
+ ]
655
+ };
643
656
  var emailDeliveriesCollection = {
644
657
  slug: EMAIL_DELIVERIES,
645
658
  access: { read: nobody, create: nobody, update: nobody, delete: nobody },
@@ -3977,7 +3990,7 @@ var VersionStore = class {
3977
3990
  };
3978
3991
 
3979
3992
  // src/local-api.ts
3980
- import { createHash as createHash5, createHmac as createHmac7, randomBytes as randomBytes6, randomUUID as randomUUID3 } from "crypto";
3993
+ import { createHash as createHash5, createHmac as createHmac8, randomBytes as randomBytes6, randomUUID as randomUUID3 } from "crypto";
3981
3994
 
3982
3995
  // src/auth/common-passwords.ts
3983
3996
  var LIST = `
@@ -4473,6 +4486,141 @@ function entryOf(row) {
4473
4486
  };
4474
4487
  }
4475
4488
 
4489
+ // src/rate-limit.ts
4490
+ import { createHmac as createHmac5 } from "crypto";
4491
+ var SYNC_MS = 5e3;
4492
+ var DEFAULTS = { window: 60, anonymous: 300, users: 1200, apiKeys: 1200 };
4493
+ var RateLimiter = class {
4494
+ constructor(db, secret, logger, settings) {
4495
+ this.db = db;
4496
+ this.secret = secret;
4497
+ this.logger = logger;
4498
+ this.settings = { ...DEFAULTS, ...settings };
4499
+ }
4500
+ db;
4501
+ secret;
4502
+ logger;
4503
+ counts = /* @__PURE__ */ new Map();
4504
+ settings;
4505
+ lastSync = Date.now();
4506
+ syncing;
4507
+ warnedNoIp = false;
4508
+ /**
4509
+ * Counts a request, or returns the seconds until the client may send another when it is over
4510
+ * its limit. Health checks and the cron's `jobs/run` aren't counted (the caller skips them).
4511
+ */
4512
+ hit(user, ip, now = Date.now()) {
4513
+ const client = this.client(user, ip);
4514
+ if (!client) return void 0;
4515
+ const [kind, id, limit] = client;
4516
+ const windowMs = this.settings.window * 1e3;
4517
+ const bucket = Math.floor(now / windowMs);
4518
+ const key = `${kind}:${id}:${bucket}`;
4519
+ let count = this.counts.get(key);
4520
+ if (!count) {
4521
+ count = { total: 0, pending: 0, ends: (bucket + 1) * windowMs };
4522
+ this.counts.set(key, count);
4523
+ }
4524
+ if (now - this.lastSync >= SYNC_MS && !this.syncing) void this.sync(now);
4525
+ if (count.total + count.pending >= limit)
4526
+ return Math.max(1, Math.ceil((count.ends - now) / 1e3));
4527
+ count.pending++;
4528
+ return void 0;
4529
+ }
4530
+ /** Who is counted, and their limit; `undefined` when nothing limits them. */
4531
+ client(user, ip) {
4532
+ const { anonymous, users, apiKeys } = this.settings;
4533
+ const key = user?.apiKey;
4534
+ if (key) return apiKeys === false ? void 0 : ["apiKey", String(key.id), apiKeys];
4535
+ if (user) return users === false ? void 0 : ["user", String(user.id), users];
4536
+ if (anonymous === false) return void 0;
4537
+ if (!ip) {
4538
+ if (!this.warnedNoIp) {
4539
+ this.warnedNoIp = true;
4540
+ this.logger.warn(
4541
+ "rateLimit: the client IP is unknown, so visitors who are not signed in are not limited. Behind a proxy, set trustProxy on the adapter."
4542
+ );
4543
+ }
4544
+ return void 0;
4545
+ }
4546
+ const hashed = createHmac5("sha256", this.secret).update(`rate:${ip}`).digest("base64url");
4547
+ return ["anonymous", hashed.slice(0, 22), anonymous];
4548
+ }
4549
+ /**
4550
+ * Adds what this instance counted to the database and reads the totals back; forgets windows
4551
+ * that are over. Runs in the background every few seconds; `await` it in tests.
4552
+ */
4553
+ sync(now = Date.now()) {
4554
+ this.lastSync = now;
4555
+ this.syncing ??= this.write(now).catch((error) => this.logger.warn(`rateLimit: counts not saved: ${String(error)}`)).finally(() => {
4556
+ this.syncing = void 0;
4557
+ });
4558
+ return this.syncing;
4559
+ }
4560
+ async write(now) {
4561
+ for (const [key, count] of this.counts) {
4562
+ if (count.ends <= now) {
4563
+ this.counts.delete(key);
4564
+ continue;
4565
+ }
4566
+ const added = count.pending;
4567
+ count.pending = 0;
4568
+ count.total = await this.add(key, count, added) ?? count.total + added;
4569
+ }
4570
+ }
4571
+ /** Adds `by` to the key's row (made if missing) and returns the new total. */
4572
+ async add(key, count, by) {
4573
+ count.id ??= await this.rowId(key);
4574
+ if (count.id === void 0) {
4575
+ const stamp = (/* @__PURE__ */ new Date()).toISOString();
4576
+ try {
4577
+ const created = await this.db.create({
4578
+ collection: RATE_LIMITS,
4579
+ data: {
4580
+ key,
4581
+ count: by,
4582
+ expiresAt: new Date(count.ends).toISOString(),
4583
+ createdAt: stamp,
4584
+ updatedAt: stamp
4585
+ }
4586
+ });
4587
+ count.id = created.id;
4588
+ return by;
4589
+ } catch (error) {
4590
+ count.id = await this.rowId(key);
4591
+ if (count.id === void 0) throw error;
4592
+ }
4593
+ }
4594
+ return this.db.increment({ collection: RATE_LIMITS, id: count.id, field: "count", by });
4595
+ }
4596
+ async rowId(key) {
4597
+ const { docs } = await this.db.find({
4598
+ collection: RATE_LIMITS,
4599
+ where: { key: { equals: key } },
4600
+ sort: [],
4601
+ limit: 1,
4602
+ page: 1
4603
+ });
4604
+ return docs[0]?.id;
4605
+ }
4606
+ /** Deletes the rows of windows that are over. From `runJobs`. */
4607
+ async clear(now = /* @__PURE__ */ new Date()) {
4608
+ let deleted = 0;
4609
+ for (; ; ) {
4610
+ const { docs } = await this.db.find({
4611
+ collection: RATE_LIMITS,
4612
+ where: { expiresAt: { lt: now.toISOString() } },
4613
+ sort: ["id"],
4614
+ limit: 500,
4615
+ page: 1
4616
+ });
4617
+ for (const row of docs) await this.db.delete({ collection: RATE_LIMITS, id: row.id });
4618
+ deleted += docs.length;
4619
+ if (docs.length < 500) return deleted;
4620
+ }
4621
+ }
4622
+ };
4623
+
4476
4624
  // src/remote-file.ts
4477
4625
  import { lookup as dnsLookup } from "dns";
4478
4626
  import { request as httpRequest } from "http";
@@ -4715,7 +4863,7 @@ function read(response, maxBytes, deadline) {
4715
4863
  var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
4716
4864
  var CONFIG = keys(`secret previousSecrets db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
4717
4865
  upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
4718
- audit logs countCacheSeconds maxPage httpCache outbox search telemetry plugins`);
4866
+ audit logs countCacheSeconds maxPage httpCache outbox rateLimit search telemetry plugins`);
4719
4867
  var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
4720
4868
  var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedOrigins
4721
4869
  resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
@@ -4723,6 +4871,7 @@ var MEMBERS = keys("roles signUp pages emails");
4723
4871
  var UPLOAD = keys("dir maxFileSize mimeTypes storage imageSizes fromURL folders privateStorage");
4724
4872
  var BACKUPS = keys("frequency at keep dir storage sqlite encryptionKey");
4725
4873
  var AUDIT = keys("keepDays values failedLogins scope");
4874
+ var RATE_LIMIT = keys("window anonymous users apiKeys");
4726
4875
  var LOGS = keys("format requests");
4727
4876
  var COLLECTION = keys(`slug labels fields useAsTitle drafts versions schedule preview access hooks
4728
4877
  admin httpCache search`);
@@ -4848,6 +4997,7 @@ function checkConfigKeys(config, add) {
4848
4997
  check2(c.upload, "upload", "upload", UPLOAD, add);
4849
4998
  check2(c.backups, "backups", "backups", BACKUPS, add);
4850
4999
  if (isObject(c.audit)) check2(c.audit, "audit", "audit", AUDIT, add);
5000
+ if (isObject(c.rateLimit)) check2(c.rateLimit, "rateLimit", "rateLimit", RATE_LIMIT, add);
4851
5001
  check2(c.logs, "logs", "logs", LOGS, add);
4852
5002
  for (const [kind, list] of [
4853
5003
  ["collections", c.collections],
@@ -5204,13 +5354,13 @@ var SearchIndex = class {
5204
5354
  };
5205
5355
 
5206
5356
  // src/webhooks.ts
5207
- import { createHmac as createHmac6, randomUUID as randomUUID2 } from "crypto";
5357
+ import { createHmac as createHmac7, randomUUID as randomUUID2 } from "crypto";
5208
5358
 
5209
5359
  // src/webhook-signature.ts
5210
- import { createHmac as createHmac5, timingSafeEqual as timingSafeEqual5 } from "crypto";
5360
+ import { createHmac as createHmac6, timingSafeEqual as timingSafeEqual5 } from "crypto";
5211
5361
  var TOLERANCE_SECONDS = 5 * 60;
5212
5362
  function signWebhook(secret, delivery, timestamp, body) {
5213
- return `v1=${createHmac5("sha256", secret).update(`${delivery}.${timestamp}.${body}`).digest("hex")}`;
5363
+ return `v1=${createHmac6("sha256", secret).update(`${delivery}.${timestamp}.${body}`).digest("hex")}`;
5214
5364
  }
5215
5365
  function header(headers, name) {
5216
5366
  if (headers instanceof Headers) return headers.get(name) ?? void 0;
@@ -5411,7 +5561,7 @@ var Webhooks = class {
5411
5561
  const timestamp = Math.floor(Date.now() / 1e3);
5412
5562
  headers["x-easy-cms-timestamp"] = String(timestamp);
5413
5563
  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")}`;
5564
+ headers["x-easy-cms-signature"] = `sha256=${createHmac7("sha256", hook.secret).update(body).digest("hex")}`;
5415
5565
  }
5416
5566
  try {
5417
5567
  const response = await this.fetcher(hook.url, {
@@ -7096,6 +7246,7 @@ async function resolveConfig(input) {
7096
7246
  ...config.email ? [emailDeliveriesCollection] : [],
7097
7247
  ...(config.jobs?.length ?? 0) > 0 ? [jobRunsCollection] : [],
7098
7248
  ...config.outbox ? [outboxCollection] : [],
7249
+ ...config.rateLimit ? [rateLimitsCollection] : [],
7099
7250
  databaseBackupsCollection,
7100
7251
  ...config.auth?.rbac ? [rolesCollection] : [],
7101
7252
  ...(config.auth?.providers?.length ?? 0) > 0 ? [userIdentitiesCollection] : [],
@@ -7166,7 +7317,7 @@ function cacheableResponse(request, body, status, headers, times, tags) {
7166
7317
  }
7167
7318
 
7168
7319
  // src/version.ts
7169
- var VERSION = true ? "1.18.0" : "0.0.0-dev";
7320
+ var VERSION = true ? "1.19.0" : "0.0.0-dev";
7170
7321
 
7171
7322
  // src/telemetry.ts
7172
7323
  var DB_CALLS = /* @__PURE__ */ new Set([
@@ -7444,6 +7595,8 @@ var EasyCMS = class {
7444
7595
  mailer;
7445
7596
  /** With `outbox: true`: runs after hooks and events from rows saves write. */
7446
7597
  outbox;
7598
+ /** @internal Requests per client (`rateLimit`), for the REST API. */
7599
+ rateLimiter;
7447
7600
  /** With `search`: keeps the index in step with published documents. */
7448
7601
  searchIndex;
7449
7602
  /** @internal With `telemetry`: OpenTelemetry spans and metrics (set by `createEasyCMS`). */
@@ -7484,6 +7637,7 @@ var EasyCMS = class {
7484
7637
  () => this.runOutbox(entry, done, mark)
7485
7638
  ) : this.runOutbox(entry, done, mark)
7486
7639
  ) : void 0;
7640
+ this.rateLimiter = config.rateLimit ? new RateLimiter(db, config.secret, logger, config.rateLimit) : void 0;
7487
7641
  this.searchIndex = config.search ? new SearchIndex(
7488
7642
  config.search,
7489
7643
  config.collections,
@@ -7830,7 +7984,7 @@ var EasyCMS = class {
7830
7984
  /** A value with its HMAC (`secret`), for `purpose` only: `<base64url JSON>.<signature>`. */
7831
7985
  sign(purpose, value) {
7832
7986
  const payload = Buffer.from(JSON.stringify(value)).toString("base64url");
7833
- const mac = createHmac7("sha256", this.config.secret).update(`${purpose}:${payload}`);
7987
+ const mac = createHmac8("sha256", this.config.secret).update(`${purpose}:${payload}`);
7834
7988
  return `${payload}.${mac.digest("base64url")}`;
7835
7989
  }
7836
7990
  /** The value of a token made by `sign`, or `undefined` when it isn't genuine. */
@@ -7957,7 +8111,7 @@ var EasyCMS = class {
7957
8111
  return signedWith(secretsOf(this.config), `media:${key}:${at}`, signature);
7958
8112
  }
7959
8113
  mediaSignature(key, expires) {
7960
- return createHmac7("sha256", this.config.secret).update(`media:${key}:${expires}`).digest("base64url");
8114
+ return createHmac8("sha256", this.config.secret).update(`media:${key}:${expires}`).digest("base64url");
7961
8115
  }
7962
8116
  /**
7963
8117
  * How many documents use any of these files in a top-level upload field (rich text and fields
@@ -8974,6 +9128,7 @@ var EasyCMS = class {
8974
9128
  (error) => this.logger.error(`Backups: ${error.message}`, { error })
8975
9129
  );
8976
9130
  await this.audit.upkeep(now).catch((error) => this.logger.error(`Audit log: ${error.message}`, { error }));
9131
+ await this.rateLimiter?.clear(now).catch((error) => this.logger.error(`Rate limits: ${error.message}`, { error }));
8977
9132
  const result = {
8978
9133
  scheduled,
8979
9134
  webhooks: await this.retryWebhooks(now),
@@ -12242,6 +12397,10 @@ async function respondNow(cms, request, url, path, root, options, production, re
12242
12397
  trustProxy: options.trustProxy === true
12243
12398
  };
12244
12399
  const method = request.method.toUpperCase();
12400
+ const isJobsRun = !root && segments[0] === "jobs" && segments[1] === "run";
12401
+ const wait = isJobsRun ? void 0 : cms.rateLimiter?.hit(user, options.getClientIp?.(request));
12402
+ if (wait !== void 0)
12403
+ throw new TooManyRequestsError("Too many requests: try again later", wait);
12245
12404
  if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
12246
12405
  const audit = {
12247
12406
  user,
@@ -3366,6 +3366,20 @@ interface AuthConfig {
3366
3366
  readonly passwordChanged?: PasswordEmailFn;
3367
3367
  };
3368
3368
  }
3369
+ interface RateLimitConfig {
3370
+ /** Seconds each count lasts. Default 60. */
3371
+ readonly window?: number;
3372
+ /**
3373
+ * Requests per window from one IP without signing in. Default 300; `false`: no limit. Needs the
3374
+ * client's IP: behind a proxy, set `trustProxy` on the adapter (without it, visitors aren't
3375
+ * limited, and the log says so once).
3376
+ */
3377
+ readonly anonymous?: number | false;
3378
+ /** Requests per window from one signed-in user (the admin included). Default 1200; `false`: no limit. */
3379
+ readonly users?: number | false;
3380
+ /** Requests per window with one API key. Default 1200; `false`: no limit. */
3381
+ readonly apiKeys?: number | false;
3382
+ }
3369
3383
  /** What `onRequest` receives. */
3370
3384
  interface OnRequestArgs {
3371
3385
  readonly headers: Headers;
@@ -3673,6 +3687,13 @@ interface Config {
3673
3687
  * Default `false`: hooks run in the request, after the save.
3674
3688
  */
3675
3689
  readonly outbox?: boolean;
3690
+ /**
3691
+ * Limits requests to the API per client and minute, counted across instances: each visitor's
3692
+ * IP, each signed-in user and each API key. Over the limit, the API answers `429` with
3693
+ * `Retry-After`. `{}` uses the defaults. Needs a migration for the `rate-limits` table. Default
3694
+ * off.
3695
+ */
3696
+ readonly rateLimit?: RateLimitConfig;
3676
3697
  /**
3677
3698
  * Relevance-ranked search of the collections that have `search`, e.g. `memorySearch()` for
3678
3699
  * development and small sites, or an adapter for a search engine. Published documents are
@@ -1,4 +1,4 @@
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';
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-U1mRwD4t.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, 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';
1
+ import { P as PaginatedDocs, I as ID, d as EmailSetting, W as WebhookPayload } from './config-U1mRwD4t.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-U1mRwD4t.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-DRV5HA4a.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-D2y4WSSM.js';
5
5
  export { FieldCondition, matchesCondition } from './conditions.js';
6
6
 
7
7
  type DeliveryKind = 'webhook' | 'email';
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  silentLogger,
29
29
  slugify,
30
30
  verifyWebhook
31
- } from "./chunk-NTAFZ4M6.js";
31
+ } from "./chunk-FZCQXH5M.js";
32
32
  import {
33
33
  matchesCondition
34
34
  } from "./chunk-U266LV4M.js";
@@ -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, 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';
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-U1mRwD4t.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-D2y4WSSM.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
@@ -58,7 +58,7 @@ import {
58
58
  verifyPassword,
59
59
  verifyTurnstile,
60
60
  writeBackupFile
61
- } from "./chunk-NTAFZ4M6.js";
61
+ } from "./chunk-FZCQXH5M.js";
62
62
  import {
63
63
  conditionIssues
64
64
  } from "./chunk-U266LV4M.js";
@@ -1,4 +1,4 @@
1
- import { bh as Label, F as Field, I as ID, o as AdminLocale, x as AdminSwitcher, ch as SsoProviderRef } from './config-CpeC7EvC.js';
1
+ import { bh as Label, F as Field, I as ID, o as AdminLocale, x as AdminSwitcher, ch as SsoProviderRef } from './config-U1mRwD4t.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 { 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';
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-U1mRwD4t.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-DRV5HA4a.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.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",