@easy-cms/core 1.5.0 → 1.7.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.
@@ -2678,7 +2678,7 @@ var Mailer = class {
2678
2678
  error: null
2679
2679
  });
2680
2680
  } catch (error) {
2681
- this.logger.error(`Could not save email: ${error.message}`);
2681
+ this.logger.error(`Could not save email: ${error.message}`, { error });
2682
2682
  }
2683
2683
  this.track(this.deliver(message2, body, id));
2684
2684
  }
@@ -2726,7 +2726,9 @@ var Mailer = class {
2726
2726
  return this.attempt(JSON.parse(message2));
2727
2727
  }
2728
2728
  track(delivery) {
2729
- const tracked = delivery.catch((error) => this.logger.error(`Email queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
2729
+ const tracked = delivery.catch(
2730
+ (error) => this.logger.error(`Email queue failed: ${error.message}`, { error })
2731
+ ).finally(() => this.pending.delete(tracked));
2730
2732
  this.pending.add(tracked);
2731
2733
  }
2732
2734
  async deliver(message2, body, id) {
@@ -2787,6 +2789,42 @@ function configSignature(config) {
2787
2789
  }
2788
2790
  var signatures = /* @__PURE__ */ new WeakMap();
2789
2791
 
2792
+ // src/health.ts
2793
+ var HEADERS = { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" };
2794
+ var migrated = /* @__PURE__ */ new WeakSet();
2795
+ async function pendingMigrations(cms) {
2796
+ if (migrated.has(cms)) return 0;
2797
+ const pending = (await cms.db.migrationStatus()).filter((m) => !m.applied).length;
2798
+ if (pending === 0) migrated.add(cms);
2799
+ return pending;
2800
+ }
2801
+ async function healthResponse(check3, instance) {
2802
+ const cms = instance;
2803
+ if (check3 === "healthz") return new Response("ok", { headers: { "cache-control": "no-store" } });
2804
+ if (!cms) {
2805
+ return Response.json(
2806
+ { status: "error", checks: { startup: "error" } },
2807
+ { status: 503, headers: HEADERS }
2808
+ );
2809
+ }
2810
+ const checks = {};
2811
+ try {
2812
+ await cms.db.count({ collection: USERS });
2813
+ checks.database = "ok";
2814
+ const pending = await pendingMigrations(cms);
2815
+ checks.migrations = pending === 0 ? "ok" : `${pending} pending`;
2816
+ } catch (error) {
2817
+ if (checks.database) checks.migrations = "error";
2818
+ else Object.assign(checks, { database: "error", migrations: "not checked" });
2819
+ cms.logger.error(`Readiness check failed: ${error.message}`, { error });
2820
+ }
2821
+ const ok = Object.values(checks).every((value) => value === "ok");
2822
+ return Response.json(
2823
+ { status: ok ? "ok" : "error", checks },
2824
+ { status: ok ? 200 : 503, headers: HEADERS }
2825
+ );
2826
+ }
2827
+
2790
2828
  // src/copy.ts
2791
2829
  var BATCH = 200;
2792
2830
  async function copyDatabase(source, target, options = {}) {
@@ -3112,7 +3150,7 @@ async function startBackup(cms, trigger, author) {
3112
3150
  }
3113
3151
  });
3114
3152
  void runBackup(cms, row).catch(
3115
- (error) => cms.logger.error(`Backup failed: ${error.message}`)
3153
+ (error) => cms.logger.error(`Backup failed: ${error.message}`, { error })
3116
3154
  );
3117
3155
  return toAdmin(row);
3118
3156
  }
@@ -3221,7 +3259,7 @@ async function runDueBackups(cms, now = /* @__PURE__ */ new Date()) {
3221
3259
  if (!due) return;
3222
3260
  if (await inProgress(cms, now.getTime())) return;
3223
3261
  await startBackup(cms, "scheduled").catch(
3224
- (error) => cms.logger.error(`Scheduled backup could not start: ${error.message}`)
3262
+ (error) => cms.logger.error(`Scheduled backup could not start: ${error.message}`, { error })
3225
3263
  );
3226
3264
  }
3227
3265
  async function backupAttention(cms, now = Date.now()) {
@@ -3290,12 +3328,58 @@ async function deleteBackup(cms, id) {
3290
3328
  var consoleLogger = {
3291
3329
  info: (message2) => console.info(`[easy-cms] ${message2}`),
3292
3330
  warn: (message2) => console.warn(`[easy-cms] ${message2}`),
3293
- error: (message2) => console.error(`[easy-cms] ${message2}`)
3331
+ error: (message2, details) => {
3332
+ const stack = details?.error instanceof Error ? details.error.stack : void 0;
3333
+ console.error(`[easy-cms] ${message2}${stack ? `
3334
+ ${stack}` : ""}`);
3335
+ }
3294
3336
  };
3295
3337
  var silentLogger = { info() {
3296
3338
  }, warn() {
3297
3339
  }, error() {
3298
3340
  } };
3341
+ function logFormat(logs) {
3342
+ const env = process.env.EASY_CMS_LOG_FORMAT;
3343
+ if (env === "json" || env === "pretty") return env;
3344
+ return logs?.format ?? "pretty";
3345
+ }
3346
+ var jsonLoggers = /* @__PURE__ */ new WeakSet();
3347
+ var isJsonLogger = (logger) => jsonLoggers.has(logger);
3348
+ function errorFields(error) {
3349
+ if (error instanceof Error)
3350
+ return { name: error.name, message: error.message, stack: error.stack };
3351
+ return { message: String(error) };
3352
+ }
3353
+ function jsonLogger(write = (line, level) => (level === "info" ? process.stdout : process.stderr).write(`${line}
3354
+ `)) {
3355
+ const log = (level) => (message2, details) => {
3356
+ const store2 = auditContext.getStore();
3357
+ const { error, ...rest } = details ?? {};
3358
+ const entry = {
3359
+ time: (/* @__PURE__ */ new Date()).toISOString(),
3360
+ level,
3361
+ name: "easy-cms",
3362
+ message: message2,
3363
+ ...store2?.requestId ? { requestId: store2.requestId } : {},
3364
+ ...store2?.runId ? { runId: store2.runId } : {},
3365
+ ...rest,
3366
+ ...error === void 0 ? {} : { error: errorFields(error) }
3367
+ };
3368
+ let line;
3369
+ try {
3370
+ line = JSON.stringify(entry);
3371
+ } catch {
3372
+ line = JSON.stringify({
3373
+ ...entry,
3374
+ ...Object.fromEntries(Object.keys(rest).map((k) => [k, "[unserializable]"]))
3375
+ });
3376
+ }
3377
+ write(line, level);
3378
+ };
3379
+ const logger = { info: log("info"), warn: log("warn"), error: log("error") };
3380
+ jsonLoggers.add(logger);
3381
+ return logger;
3382
+ }
3299
3383
 
3300
3384
  // src/media-folders.ts
3301
3385
  var FOLDER_LEVELS = ["view", "edit", "manage"];
@@ -3790,7 +3874,7 @@ var VersionStore = class {
3790
3874
  };
3791
3875
 
3792
3876
  // src/local-api.ts
3793
- import { createHmac as createHmac6, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual5 } from "crypto";
3877
+ import { createHmac as createHmac6, randomBytes as randomBytes6, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual5 } from "crypto";
3794
3878
 
3795
3879
  // src/auth/common-passwords.ts
3796
3880
  var LIST = `
@@ -4342,7 +4426,7 @@ function read(response, maxBytes, deadline) {
4342
4426
  var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
4343
4427
  var CONFIG = keys(`secret db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
4344
4428
  upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
4345
- audit plugins`);
4429
+ audit logs plugins`);
4346
4430
  var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
4347
4431
  var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedOrigins
4348
4432
  resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
@@ -4350,6 +4434,7 @@ var MEMBERS = keys("roles signUp pages emails");
4350
4434
  var UPLOAD = keys("dir maxFileSize mimeTypes storage imageSizes fromURL folders privateStorage");
4351
4435
  var BACKUPS = keys("frequency at keep dir storage sqlite encryptionKey");
4352
4436
  var AUDIT = keys("keepDays values failedLogins scope");
4437
+ var LOGS = keys("format requests");
4353
4438
  var COLLECTION = keys(`slug labels fields useAsTitle drafts versions schedule preview access hooks
4354
4439
  admin`);
4355
4440
  var GLOBAL = keys("slug label fields drafts versions schedule preview access hooks scope admin");
@@ -4472,6 +4557,7 @@ function checkConfigKeys(config, add) {
4472
4557
  check2(c.upload, "upload", "upload", UPLOAD, add);
4473
4558
  check2(c.backups, "backups", "backups", BACKUPS, add);
4474
4559
  if (isObject(c.audit)) check2(c.audit, "audit", "audit", AUDIT, add);
4560
+ check2(c.logs, "logs", "logs", LOGS, add);
4475
4561
  for (const [kind, list] of [
4476
4562
  ["collections", c.collections],
4477
4563
  ["globals", c.globals]
@@ -4661,7 +4747,9 @@ var Webhooks = class {
4661
4747
  return this.attempt(hook, delivery.event, delivery.body, delivery.delivery);
4662
4748
  }
4663
4749
  track(delivery) {
4664
- const tracked = delivery.catch((error) => this.logger.error(`Webhook queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
4750
+ const tracked = delivery.catch(
4751
+ (error) => this.logger.error(`Webhook queue failed: ${error.message}`, { error })
4752
+ ).finally(() => this.pending.delete(tracked));
4665
4753
  this.pending.add(tracked);
4666
4754
  }
4667
4755
  async deliver(hook, payload) {
@@ -4686,7 +4774,7 @@ var Webhooks = class {
4686
4774
  try {
4687
4775
  queued = await saved;
4688
4776
  } catch (error) {
4689
- this.logger.error(`Could not save webhook delivery: ${error.message}`);
4777
+ this.logger.error(`Could not save webhook delivery: ${error.message}`, { error });
4690
4778
  }
4691
4779
  for (let attempt = 0; ; attempt++) {
4692
4780
  const result = await this.attempt(hook, event, body, delivery);
@@ -4773,7 +4861,9 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
4773
4861
  "jobs",
4774
4862
  "auth",
4775
4863
  "migrations",
4776
- "access"
4864
+ "access",
4865
+ "healthz",
4866
+ "readyz"
4777
4867
  ]);
4778
4868
  var SYSTEM_FIELD_NAMES = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
4779
4869
  var SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
@@ -4797,6 +4887,7 @@ function validateConfig(config) {
4797
4887
  validateUpload(config, add);
4798
4888
  validateBackups(config, add, (issue) => issues.push(issue));
4799
4889
  validateAudit(config, add);
4890
+ validateLogs(config, add);
4800
4891
  validateAuth(config, add);
4801
4892
  validateCors(config.cors, add);
4802
4893
  validateLocalization(config, add);
@@ -5419,6 +5510,19 @@ function validateUpload(config, add) {
5419
5510
  var FOLDER_KEY = /^[a-z0-9][a-z0-9_-]{0,59}$/;
5420
5511
  var MIME_PATTERN = /^([\w.+-]+\/(\*|[\w.+-]+)|documents|office|archives)$/i;
5421
5512
  var HOST_PATTERN = /^(\*|(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*)$/i;
5513
+ function validateLogs(config, add) {
5514
+ const logs = config.logs;
5515
+ if (logs === void 0) return;
5516
+ if (typeof logs !== "object" || logs === null) {
5517
+ add("logs", "must be an object", "e.g. logs: { format: 'json' }");
5518
+ return;
5519
+ }
5520
+ const { format, requests } = logs;
5521
+ if (format !== void 0 && format !== "pretty" && format !== "json")
5522
+ add("logs.format", "must be 'pretty' or 'json'");
5523
+ if (requests !== void 0 && typeof requests !== "boolean")
5524
+ add("logs.requests", "must be true or false");
5525
+ }
5422
5526
  function validateAudit(config, add) {
5423
5527
  const audit = config.audit;
5424
5528
  if (audit !== void 0 && typeof audit !== "boolean") {
@@ -5933,6 +6037,8 @@ var RESERVED_ENDPOINT_ROOTS = /* @__PURE__ */ new Set([
5933
6037
  "media",
5934
6038
  "api-keys",
5935
6039
  "auth",
6040
+ "healthz",
6041
+ "readyz",
5936
6042
  // Kept free for later versions.
5937
6043
  "system",
5938
6044
  "health",
@@ -6056,12 +6162,13 @@ function validateEndpoints(config, collectionSlugs, add) {
6056
6162
  const taken = [
6057
6163
  config.routes?.api ?? "/api/cms",
6058
6164
  `/${(config.admin?.path ?? "/admin").replace(/^\/+|\/+$/g, "")}`,
6059
- "/healthz"
6165
+ "/healthz",
6166
+ "/readyz"
6060
6167
  ].map((p) => p.replace(/\/+$/, "")).find((p) => route2 === p || route2.startsWith(`${p}/`) || p.startsWith(`${route2}/`));
6061
6168
  if (taken)
6062
6169
  add(
6063
6170
  `${path}.path`,
6064
- `"${route2}" is served by ${taken === "/healthz" ? "the standalone server" : `"${taken}"`}`
6171
+ `"${route2}" is served by ${taken === "/healthz" || taken === "/readyz" ? "the standalone server" : `"${taken}"`}`
6065
6172
  );
6066
6173
  } else if (RESERVED_ENDPOINT_ROOTS.has(first) || collectionSlugs.has(first)) {
6067
6174
  add(
@@ -6287,7 +6394,7 @@ function resolveAudit(audit) {
6287
6394
  // src/local-api.ts
6288
6395
  async function createEasyCMS(config, options = {}) {
6289
6396
  const resolved3 = await resolveConfig(config);
6290
- const logger = options.logger ?? consoleLogger;
6397
+ const logger = options.logger ?? (logFormat(resolved3.logs) === "json" ? jsonLogger() : consoleLogger);
6291
6398
  for (const warning of resolved3.warnings)
6292
6399
  logger.warn(
6293
6400
  `Config: ${warning.path} ${warning.message}${warning.hint ? ` (${warning.hint})` : ""}`
@@ -7142,7 +7249,9 @@ var EasyCMS = class {
7142
7249
  try {
7143
7250
  return config.preview({ doc, locale }) ?? null;
7144
7251
  } catch (error) {
7145
- this.logger.error(`preview URL of "${config.slug}" failed: ${error.message}`);
7252
+ this.logger.error(`preview URL of "${config.slug}" failed: ${error.message}`, {
7253
+ error
7254
+ });
7146
7255
  return null;
7147
7256
  }
7148
7257
  }
@@ -7628,7 +7737,7 @@ var EasyCMS = class {
7628
7737
  if (this.schedulerTimer) return;
7629
7738
  this.schedulerTimer = setInterval(() => {
7630
7739
  void this.runJobs().catch(
7631
- (error) => this.logger.error(`Scheduled jobs failed: ${error.message}`)
7740
+ (error) => this.logger.error(`Scheduled jobs failed: ${error.message}`, { error })
7632
7741
  );
7633
7742
  }, interval);
7634
7743
  this.schedulerTimer.unref?.();
@@ -7644,13 +7753,20 @@ var EasyCMS = class {
7644
7753
  return run;
7645
7754
  }
7646
7755
  runJobsNow(now) {
7647
- return auditContext.run({ via: "scheduler", user: null }, async () => {
7756
+ const requestId = auditContext.getStore()?.requestId;
7757
+ const run = {
7758
+ via: "scheduler",
7759
+ user: null,
7760
+ runId: randomUUID3(),
7761
+ ...requestId ? { requestId } : {}
7762
+ };
7763
+ return auditContext.run(run, async () => {
7648
7764
  const scheduled = await this.runScheduled(now);
7649
7765
  await this.pruneFailedDeliveries(now);
7650
7766
  await runDueBackups(this, now).catch(
7651
- (error) => this.logger.error(`Backups: ${error.message}`)
7767
+ (error) => this.logger.error(`Backups: ${error.message}`, { error })
7652
7768
  );
7653
- await this.audit.upkeep(now).catch((error) => this.logger.error(`Audit log: ${error.message}`));
7769
+ await this.audit.upkeep(now).catch((error) => this.logger.error(`Audit log: ${error.message}`, { error }));
7654
7770
  const result = {
7655
7771
  scheduled,
7656
7772
  webhooks: await this.retryWebhooks(now),
@@ -7658,7 +7774,7 @@ var EasyCMS = class {
7658
7774
  jobs: await this.runConfigJobs(now)
7659
7775
  };
7660
7776
  const ranAt = (/* @__PURE__ */ new Date()).toISOString();
7661
- await this.db.updateGlobal({ slug: JOBS_RAN, data: { at: ranAt, updatedAt: ranAt } }).catch((error) => this.logger.error(`Jobs: ${error.message}`));
7777
+ await this.db.updateGlobal({ slug: JOBS_RAN, data: { at: ranAt, updatedAt: ranAt } }).catch((error) => this.logger.error(`Jobs: ${error.message}`, { error }));
7662
7778
  return result;
7663
7779
  });
7664
7780
  }
@@ -7730,10 +7846,10 @@ var EasyCMS = class {
7730
7846
  } catch (caught) {
7731
7847
  failed++;
7732
7848
  error = (caught.message || String(caught)).slice(0, 500);
7733
- this.logger.error(`Job ${job.name} failed: ${error}`);
7849
+ this.logger.error(`Job ${job.name} failed: ${error}`, { error: caught, job: job.name });
7734
7850
  }
7735
7851
  await this.recordJobRun(job.name, stamp, error, Date.now() - started).catch(
7736
- (caught) => this.logger.error(`Job ${job.name}: ${caught.message}`)
7852
+ (caught) => this.logger.error(`Job ${job.name}: ${caught.message}`, { error: caught })
7737
7853
  );
7738
7854
  }
7739
7855
  return { ran, failed };
@@ -7898,7 +8014,9 @@ var EasyCMS = class {
7898
8014
  } catch (error) {
7899
8015
  failed++;
7900
8016
  const message2 = error.message;
7901
- this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message2}`);
8017
+ this.logger.error(`Scheduled ${action} of ${parent} ${row.doc} failed: ${message2}`, {
8018
+ error
8019
+ });
7902
8020
  await this.db.update({
7903
8021
  collection: SCHEDULED_JOBS,
7904
8022
  id: row.id,
@@ -9688,7 +9806,10 @@ async function readAdminModule(cms, index) {
9688
9806
  }
9689
9807
 
9690
9808
  // src/version.ts
9691
- var VERSION = true ? "1.5.0" : "0.0.0-dev";
9809
+ var VERSION = true ? "1.7.0" : "0.0.0-dev";
9810
+
9811
+ // src/rest/handler.ts
9812
+ import { randomUUID as randomUUID4 } from "crypto";
9692
9813
 
9693
9814
  // src/rest/admin-deliveries.ts
9694
9815
  var PER_PAGE = 50;
@@ -10635,7 +10756,43 @@ function withCors(cms, handle) {
10635
10756
  return withHeaders(await handle(request), cors);
10636
10757
  };
10637
10758
  }
10759
+ var HEALTH_CHECKS = /* @__PURE__ */ new Map([
10760
+ ["healthz", "healthz"],
10761
+ ["readyz", "readyz"]
10762
+ ]);
10763
+ var REQUEST_ID = /^[\w.-]{1,128}$/;
10638
10764
  async function respond(cms, request, url, path, root, options, production) {
10765
+ const started = performance.now();
10766
+ const incoming = request.headers.get("x-request-id");
10767
+ const requestId = incoming && REQUEST_ID.test(incoming) ? incoming : randomUUID4();
10768
+ const requestLog = { userId: void 0 };
10769
+ const response = await auditContext.run(
10770
+ { requestId },
10771
+ () => respondNow(cms, request, url, path, root, options, production, requestId, requestLog)
10772
+ );
10773
+ let tagged;
10774
+ try {
10775
+ response.headers.set("x-request-id", requestId);
10776
+ tagged = response;
10777
+ } catch {
10778
+ tagged = withHeaders(response, new Headers({ "x-request-id": requestId }));
10779
+ }
10780
+ const probe = !root && (path === "/healthz" || path === "/readyz");
10781
+ if (!probe && cms.config.logs?.requests !== false && isJsonLogger(cms.logger)) {
10782
+ auditContext.run(
10783
+ { requestId },
10784
+ () => cms.logger.info(`${request.method} ${url.pathname} ${tagged.status}`, {
10785
+ method: request.method,
10786
+ path: url.pathname,
10787
+ status: tagged.status,
10788
+ durationMs: Math.round(performance.now() - started),
10789
+ ...requestLog.userId === void 0 ? {} : { userId: requestLog.userId }
10790
+ })
10791
+ );
10792
+ }
10793
+ return tagged;
10794
+ }
10795
+ async function respondNow(cms, request, url, path, root, options, production, requestId, requestLog) {
10639
10796
  const headers = new Headers({
10640
10797
  "content-type": "application/json; charset=utf-8",
10641
10798
  "cache-control": "no-store",
@@ -10645,11 +10802,21 @@ async function respond(cms, request, url, path, root, options, production) {
10645
10802
  try {
10646
10803
  if (path === null) throw new HttpError("Not found", 404);
10647
10804
  const segments = pathSegments(path);
10805
+ const probe = !root && segments.length === 1 ? HEALTH_CHECKS.get(segments[0]) : void 0;
10806
+ if (probe) {
10807
+ const method2 = request.method.toUpperCase();
10808
+ if (method2 !== "GET" && method2 !== "HEAD") {
10809
+ headers.set("allow", "GET, HEAD");
10810
+ throw new HttpError("Method not allowed", 405);
10811
+ }
10812
+ return await healthResponse(probe, cms);
10813
+ }
10648
10814
  const { token, via } = readToken(request);
10649
10815
  const signedIn = token ? await cms.auth.verify(token) : null;
10650
10816
  if (!signedIn && via === "bearer" && token?.startsWith("ecms_"))
10651
10817
  throw new UnauthorizedError("Invalid or expired API key");
10652
10818
  const { user, context } = await cms.applyOnRequest(request.headers, url, signedIn);
10819
+ requestLog.userId = user?.id;
10653
10820
  const ctx = {
10654
10821
  request,
10655
10822
  url,
@@ -10666,7 +10833,8 @@ async function respond(cms, request, url, path, root, options, production) {
10666
10833
  user,
10667
10834
  context,
10668
10835
  ip: options.getClientIp?.(request),
10669
- userAgent: request.headers.get("user-agent")
10836
+ userAgent: request.headers.get("user-agent"),
10837
+ requestId
10670
10838
  };
10671
10839
  const result = await auditContext.run(
10672
10840
  audit,
@@ -11851,7 +12019,7 @@ function errorResponse(cms, error, headers, production) {
11851
12019
  if (error instanceof TooManyRequestsError && error.retryAfter !== void 0)
11852
12020
  headers.set("retry-after", String(error.retryAfter));
11853
12021
  if (error.status >= 500) {
11854
- cms.logger.error(`REST request failed: ${error.stack ?? error.message}`);
12022
+ cms.logger.error(`REST request failed: ${error.message}`, { error });
11855
12023
  if (production)
11856
12024
  return new Response(
11857
12025
  JSON.stringify({ errors: [{ message: "Internal Server Error", code }] }),
@@ -11861,9 +12029,7 @@ function errorResponse(cms, error, headers, production) {
11861
12029
  return new Response(JSON.stringify({ errors }), { status: error.status, headers });
11862
12030
  }
11863
12031
  const message2 = error instanceof Error ? error.message : String(error);
11864
- cms.logger.error(
11865
- `REST request failed: ${error instanceof Error ? error.stack ?? message2 : message2}`
11866
- );
12032
+ cms.logger.error(`REST request failed: ${message2}`, { error });
11867
12033
  const body = {
11868
12034
  errors: [
11869
12035
  { message: production ? "Internal Server Error" : message2, code: "INTERNAL_SERVER_ERROR" }
@@ -11901,8 +12067,21 @@ function platformClientIp(headers) {
11901
12067
  function createApiHandler(config, options = {}) {
11902
12068
  const handlers = /* @__PURE__ */ new WeakMap();
11903
12069
  const getClientIp = options.getClientIp ?? (options.trustProxy ? (r) => forwardedClientIp(r.headers.get("x-forwarded-for")) : (r) => platformClientIp(r.headers));
12070
+ const base = (options.basePath ?? config.routes?.api ?? "/api/cms").replace(/\/+$/, "");
11904
12071
  return async (request) => {
11905
- const cms = await sharedEasyCMS(config);
12072
+ let cms;
12073
+ try {
12074
+ cms = await sharedEasyCMS(config);
12075
+ } catch (error) {
12076
+ const { pathname } = new URL(request.url);
12077
+ const probe = pathname === `${base}/healthz` ? "healthz" : pathname === `${base}/readyz` ? "readyz" : null;
12078
+ if (!probe) throw error;
12079
+ if (probe === "readyz") {
12080
+ const logger = logFormat(config.logs) === "json" ? jsonLogger() : consoleLogger;
12081
+ logger.error(`Readiness check failed: ${error.message}`, { error });
12082
+ }
12083
+ return healthResponse(probe, null);
12084
+ }
11906
12085
  let handler = handlers.get(cms);
11907
12086
  if (!handler) {
11908
12087
  handler = createRestHandler(cms, {
@@ -12000,12 +12179,16 @@ export {
12000
12179
  validateFields,
12001
12180
  consoleEmail,
12002
12181
  configSignature,
12182
+ healthResponse,
12003
12183
  copyDatabase,
12004
12184
  diskStorage,
12005
12185
  decryptBackup,
12006
12186
  writeBackupFile,
12007
12187
  consoleLogger,
12008
12188
  silentLogger,
12189
+ logFormat,
12190
+ isJsonLogger,
12191
+ jsonLogger,
12009
12192
  FOLDER_LEVELS,
12010
12193
  MediaFolders,
12011
12194
  DEFAULT_DEPTH,
@@ -93,13 +93,35 @@ declare const isSignedIn: Access;
93
93
  */
94
94
  declare const isAdmin: Access;
95
95
 
96
+ /** More about a log line: `error` for what failed (its stack goes to the log). */
97
+ interface LogDetails {
98
+ readonly error?: unknown;
99
+ readonly [key: string]: unknown;
100
+ }
96
101
  interface Logger {
97
- info(message: string): void;
98
- warn(message: string): void;
99
- error(message: string): void;
102
+ info(message: string, details?: LogDetails): void;
103
+ warn(message: string, details?: LogDetails): void;
104
+ error(message: string, details?: LogDetails): void;
100
105
  }
106
+ /** Readable lines for development; an error's stack follows its line. */
101
107
  declare const consoleLogger: Logger;
102
108
  declare const silentLogger: Logger;
109
+ /** Log line formats: `pretty` for people, `json` (one object per line) for log pipelines. */
110
+ interface LogsConfig {
111
+ /** Default `pretty`. The `EASY_CMS_LOG_FORMAT` environment variable wins over this. */
112
+ readonly format?: 'pretty' | 'json';
113
+ /** In `json`, a line for every API request (method, path, status, duration). Default true. */
114
+ readonly requests?: boolean;
115
+ }
116
+ /** The format in use: `EASY_CMS_LOG_FORMAT`, then the config, then `pretty`. */
117
+ declare function logFormat(logs: LogsConfig | undefined): 'pretty' | 'json';
118
+ /** Whether a logger writes JSON: it then also gets a line per API request (`logs.requests`). */
119
+ declare const isJsonLogger: (logger: Logger) => boolean;
120
+ /**
121
+ * One JSON object per line: `time`, `level`, `name`, `message`, the request's `requestId` (or
122
+ * the job run's `runId`), `error` with its stack, and any other details.
123
+ */
124
+ declare function jsonLogger(write?: (line: string, level: 'info' | 'warn' | 'error') => void): Logger;
103
125
 
104
126
  /** A document as stored: relationships and uploads are ids, never populated. */
105
127
  type RawDocument = Record<string, unknown> & {
@@ -3281,6 +3303,11 @@ interface Config {
3281
3303
  * actions, kept for `keep` days. `true` for the defaults.
3282
3304
  */
3283
3305
  readonly audit?: boolean | AuditConfig;
3306
+ /**
3307
+ * How Easy CMS writes its logs: `{ format: 'json' }` for one JSON object per line, with the
3308
+ * request id. Ignored when `createEasyCMS` is given a `logger`.
3309
+ */
3310
+ readonly logs?: LogsConfig;
3284
3311
  readonly plugins?: readonly Plugin[];
3285
3312
  }
3286
3313
  /** The config after plugins ran, validation passed and defaults were applied. */
@@ -3312,4 +3339,4 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3312
3339
  */
3313
3340
  declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3314
3341
 
3315
- export { type BuiltinMediaCollection as $, ADAPTER_API_VERSION as A, type ApiKeyOperation as B, type ApiKeyPermissions as C, type ArrayField as D, type EmailSetting as E, type AuditChange as F, type AuditConfig as G, type AuditEntry as H, type ID as I, AuditLog as J, type AuditPage as K, type AuditVerification as L, Auth as M, type AuthConfig as N, type AuthProvider as O, type PaginatedDocs as P, type AuthProviderProfile as Q, type AuthProviderRequest as R, type AuthUser as S, type BackupsConfig as T, type BeforeChangeHook as U, type BeforeDeleteHook as V, type BeforeValidateHook as W, type Block as X, type BlocksField as Y, type BooleanField as Z, type BuiltinApiKeysCollection as _, ADMIN_ICONS as a, type GlobalDocument as a$, type BuiltinMediaFoldersCollection as a0, type BuiltinUsersCollection as a1, type BundleHints as a2, type CliCommand as a3, type CollectionAccess as a4, type CollectionAdmin as a5, type CollectionConfig as a6, type CollectionDocument as a7, type CollectionHooks as a8, type CollectionListAdmin as a9, type EndpointRequest as aA, type ErrorCode as aB, FIELD_TYPES as aC, type Field as aD, type FieldAccess as aE, type FieldAccessFn as aF, type FieldAdmin as aG, type FieldError as aH, type FieldRule as aI, type FieldType as aJ, type FieldTypeBase as aK, type FieldTypeDefinition as aL, type FieldTypeValidateContext as aM, type FieldValidate as aN, type FieldValidateContext as aO, type FieldValue as aP, type FieldsInput as aQ, type FieldsValue as aR, type FilterOptions as aS, type FilterOptionsArgs as aT, type FindArgs as aU, type FindOptions as aV, type FolderLevel as aW, type FolderPermissions as aX, ForbiddenError as aY, type GlobalAccess as aZ, type GlobalConfig as a_, type CollectionSlug as aa, type Config as ab, ConfigError as ac, type ConfigIssue as ad, type ContainerAdmin as ae, type CreateEasyCMSOptions as af, type CreateInput as ag, type CreateMigrationResult as ah, type CustomField as ai, type CustomFieldTypes as aj, type DashboardWidget as ak, type Database as al, type DatabaseAdapter as am, type DatabaseInitArgs as an, type DatabaseTransfer as ao, type DateField as ap, type DepthOptions as aq, type DirectUpload as ar, type DiskStorageOptions as as, type DocumentOf as at, EasyCMS as au, EasyCMSError as av, type EmailAdapter as aw, type EmailField as ax, type EmailMessage as ay, type Endpoint as az, type Access as b, type SlugField as b$, type GlobalHooks as b0, type GlobalInput as b1, type GlobalSlug as b2, type GroupField as b3, type ImageSize as b4, type IncrementOptions as b5, type InferCollection as b6, type InferGlobal as b7, type JobConfig as b8, type JsonField as b9, type PluginTypes as bA, type PreviewTarget as bB, type PreviewURL as bC, QueryError as bD, ROLE_OPERATIONS as bE, type RawDocument as bF, type ReadOptions as bG, type RelationshipField as bH, type RequestContext as bI, type ResolvedConfig as bJ, type RichTextDocument as bK, type RichTextField as bL, type Role as bM, type RoleOperation as bN, type RolePermissions as bO, Roles as bP, type RoutesConfig as bQ, type ScheduledPublish as bR, SchemaError as bS, type SchemaMode as bT, type SelectField as bU, type SelectOption as bV, type Session as bW, type SidebarPanel as bX, type SignupArgs as bY, type SignupResult as bZ, SingleSignOn as b_, type Label as ba, type LayoutNode as bb, type LivePreview as bc, type LocaleOf as bd, type LocalizationConfig as be, type Logger as bf, type LoginArgs as bg, type MediaDocument as bh, type MediaSize as bi, type MembersConfig as bj, type MembersSignup as bk, type MigrationInfo as bl, type NavGroup as bm, NotFoundError as bn, type NumberField as bo, type OnRequest as bp, type OnRequestArgs as bq, type Operation as br, PLUGIN_API_VERSION as bs, type PasswordEmail as bt, type PasswordEmailArgs as bu, type PasswordEmailFn as bv, type PasswordLinkOptions as bw, PayloadTooLargeError as bx, type Plugin as by, type PluginInfo as bz, type AccessArgs as c, type SlugOf as c0, type SsoOutcome as c1, type SsoProviderRef as c2, type StorageAdapter as c3, type StoredFile as c4, type TextField as c5, type TextareaField as c6, TooManyRequestsError as c7, type TypedPlugin as c8, UnauthorizedError as c9, isHasMany as cA, isSignedIn as cB, isStaff as cC, isSystemAdmin as cD, mimeAllowedBy as cE, rowFields as cF, silentLogger as cG, uniqueWithinOf as cH, API_KEYS as cI, API_KEY_GLOBAL_OPERATIONS as cJ, API_KEY_OPERATIONS as cK, DEFAULT_MAX_VERSIONS as cL, DEFAULT_PASSWORD_EMAILS as cM, FIELD_TYPE_BASES as cN, FOLDER_LEVELS as cO, MediaFolders as cP, SSO_COOKIE as cQ, codeOfStatus as cR, keyAllows as cS, type UpdateInput as ca, type UpdateOptions as cb, type UploadConfig as cc, type UploadField as cd, type UploadFromURLConfig as ce, type UserIdentity as cf, ValidationError as cg, type Version as ch, type VersionSummary as ci, type VersionsConfig as cj, WEBHOOK_EVENTS as ck, type WebhookConfig as cl, type WebhookEvent as cm, type WebhookPayload as cn, type Where as co, type WhereOperators as cp, anyone as cq, consoleEmail as cr, consoleLogger as cs, createEasyCMS as ct, defineConfig as cu, defineFieldType as cv, definePlugin as cw, diskStorage as cx, hasRows as cy, isAdmin as cz, type AccessOptions as d, type AdminBrand as e, type AdminCommand as f, type AdminComponent as g, type AdminConfig as h, type AdminFieldArgs as i, type AdminIcon as j, type AdminLocale as k, type AdminPage as l, type AdminRole as m, type AdminRoleChange as n, type AdminRoleField as o, type AdminRoleTarget as p, type AdminRoleView as q, type AdminRoles as r, type AdminSso as s, type AdminSwitcher as t, type AdminViewAccess as u, type AfterChangeHook as v, type AfterDeleteHook as w, type AfterReadHook as x, type ApiKeyContext as y, type ApiKeyGlobalOperation as z };
3342
+ export { type BuiltinMediaCollection as $, ADAPTER_API_VERSION as A, type ApiKeyOperation as B, type ApiKeyPermissions as C, type ArrayField as D, type EmailSetting as E, type AuditChange as F, type AuditConfig as G, type AuditEntry as H, type ID as I, AuditLog as J, type AuditPage as K, type AuditVerification as L, Auth as M, type AuthConfig as N, type AuthProvider as O, type PaginatedDocs as P, type AuthProviderProfile as Q, type AuthProviderRequest as R, type AuthUser as S, type BackupsConfig as T, type BeforeChangeHook as U, type BeforeDeleteHook as V, type BeforeValidateHook as W, type Block as X, type BlocksField as Y, type BooleanField as Z, type BuiltinApiKeysCollection as _, ADMIN_ICONS as a, type GlobalDocument as a$, type BuiltinMediaFoldersCollection as a0, type BuiltinUsersCollection as a1, type BundleHints as a2, type CliCommand as a3, type CollectionAccess as a4, type CollectionAdmin as a5, type CollectionConfig as a6, type CollectionDocument as a7, type CollectionHooks as a8, type CollectionListAdmin as a9, type EndpointRequest as aA, type ErrorCode as aB, FIELD_TYPES as aC, type Field as aD, type FieldAccess as aE, type FieldAccessFn as aF, type FieldAdmin as aG, type FieldError as aH, type FieldRule as aI, type FieldType as aJ, type FieldTypeBase as aK, type FieldTypeDefinition as aL, type FieldTypeValidateContext as aM, type FieldValidate as aN, type FieldValidateContext as aO, type FieldValue as aP, type FieldsInput as aQ, type FieldsValue as aR, type FilterOptions as aS, type FilterOptionsArgs as aT, type FindArgs as aU, type FindOptions as aV, type FolderLevel as aW, type FolderPermissions as aX, ForbiddenError as aY, type GlobalAccess as aZ, type GlobalConfig as a_, type CollectionSlug as aa, type Config as ab, ConfigError as ac, type ConfigIssue as ad, type ContainerAdmin as ae, type CreateEasyCMSOptions as af, type CreateInput as ag, type CreateMigrationResult as ah, type CustomField as ai, type CustomFieldTypes as aj, type DashboardWidget as ak, type Database as al, type DatabaseAdapter as am, type DatabaseInitArgs as an, type DatabaseTransfer as ao, type DateField as ap, type DepthOptions as aq, type DirectUpload as ar, type DiskStorageOptions as as, type DocumentOf as at, EasyCMS as au, EasyCMSError as av, type EmailAdapter as aw, type EmailField as ax, type EmailMessage as ay, type Endpoint as az, type Access as b, type SignupResult as b$, type GlobalHooks as b0, type GlobalInput as b1, type GlobalSlug as b2, type GroupField as b3, type ImageSize as b4, type IncrementOptions as b5, type InferCollection as b6, type InferGlobal as b7, type JobConfig as b8, type JsonField as b9, type Plugin as bA, type PluginInfo as bB, type PluginTypes as bC, type PreviewTarget as bD, type PreviewURL as bE, QueryError as bF, ROLE_OPERATIONS as bG, type RawDocument as bH, type ReadOptions as bI, type RelationshipField as bJ, type RequestContext as bK, type ResolvedConfig as bL, type RichTextDocument as bM, type RichTextField as bN, type Role as bO, type RoleOperation as bP, type RolePermissions as bQ, Roles as bR, type RoutesConfig as bS, type ScheduledPublish as bT, SchemaError as bU, type SchemaMode as bV, type SelectField as bW, type SelectOption as bX, type Session as bY, type SidebarPanel as bZ, type SignupArgs as b_, type Label as ba, type LayoutNode as bb, type LivePreview as bc, type LocaleOf as bd, type LocalizationConfig as be, type LogDetails as bf, type Logger as bg, type LoginArgs as bh, type LogsConfig as bi, type MediaDocument as bj, type MediaSize as bk, type MembersConfig as bl, type MembersSignup as bm, type MigrationInfo as bn, type NavGroup as bo, NotFoundError as bp, type NumberField as bq, type OnRequest as br, type OnRequestArgs as bs, type Operation as bt, PLUGIN_API_VERSION as bu, type PasswordEmail as bv, type PasswordEmailArgs as bw, type PasswordEmailFn as bx, type PasswordLinkOptions as by, PayloadTooLargeError as bz, type AccessArgs as c, SingleSignOn as c0, type SlugField as c1, type SlugOf as c2, type SsoOutcome as c3, type SsoProviderRef as c4, type StorageAdapter as c5, type StoredFile as c6, type TextField as c7, type TextareaField as c8, TooManyRequestsError as c9, hasRows as cA, isAdmin as cB, isHasMany as cC, isSignedIn as cD, isStaff as cE, isSystemAdmin as cF, jsonLogger as cG, mimeAllowedBy as cH, rowFields as cI, silentLogger as cJ, uniqueWithinOf as cK, API_KEYS as cL, API_KEY_GLOBAL_OPERATIONS as cM, API_KEY_OPERATIONS as cN, DEFAULT_MAX_VERSIONS as cO, DEFAULT_PASSWORD_EMAILS as cP, FIELD_TYPE_BASES as cQ, FOLDER_LEVELS as cR, MediaFolders as cS, SSO_COOKIE as cT, codeOfStatus as cU, isJsonLogger as cV, keyAllows as cW, logFormat as cX, type TypedPlugin as ca, UnauthorizedError as cb, type UpdateInput as cc, type UpdateOptions as cd, type UploadConfig as ce, type UploadField as cf, type UploadFromURLConfig as cg, type UserIdentity as ch, ValidationError as ci, type Version as cj, type VersionSummary as ck, type VersionsConfig as cl, WEBHOOK_EVENTS as cm, type WebhookConfig as cn, type WebhookEvent as co, type WebhookPayload as cp, type Where as cq, type WhereOperators as cr, anyone as cs, consoleEmail as ct, consoleLogger as cu, createEasyCMS as cv, defineConfig as cw, defineFieldType as cx, definePlugin as cy, diskStorage as cz, type AccessOptions as d, type AdminBrand as e, type AdminCommand as f, type AdminComponent as g, type AdminConfig as h, type AdminFieldArgs as i, type AdminIcon as j, type AdminLocale as k, type AdminPage as l, type AdminRole as m, type AdminRoleChange as n, type AdminRoleField as o, type AdminRoleTarget as p, type AdminRoleView as q, type AdminRoles as r, type AdminSso as s, type AdminSwitcher as t, type AdminViewAccess as u, type AfterChangeHook as v, type AfterDeleteHook as w, type AfterReadHook as x, type ApiKeyContext as y, type ApiKeyGlobalOperation as z };
@@ -1,4 +1,4 @@
1
- import { I as ID, ab as Config, au as EasyCMS, be as LocalizationConfig, aD as Field, aH as FieldError, aS as FilterOptions, af as CreateEasyCMSOptions, bJ as ResolvedConfig } from './config-CB8mhU2a.js';
1
+ import { I as ID, ab as Config, au as EasyCMS, be as LocalizationConfig, aD as Field, aH as FieldError, aS as FilterOptions, af as CreateEasyCMSOptions, bL as ResolvedConfig } from './config-CzW6kBeb.js';
2
2
 
3
3
  /** Decrypts a file from `encryptBackup`; throws for another key or a changed file. */
4
4
  declare function decryptBackup(data: Uint8Array, encryptionKey: string): Uint8Array;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-CB8mhU2a.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 LocaleOf, be as LocalizationConfig, bf as Logger, bg as LoginArgs, bh as MediaDocument, bi as MediaSize, bj as MembersConfig, bk as MembersSignup, bl as MigrationInfo, bm as NavGroup, bn as NotFoundError, bo as NumberField, bp as OnRequest, bq as OnRequestArgs, br as Operation, bs as PLUGIN_API_VERSION, bt as PasswordEmail, bu as PasswordEmailArgs, bv as PasswordEmailFn, bw as PasswordLinkOptions, bx as PayloadTooLargeError, by as Plugin, bz as PluginInfo, bA as PluginTypes, bB as PreviewTarget, bC as PreviewURL, bD as QueryError, bE as ROLE_OPERATIONS, bF as RawDocument, bG as ReadOptions, bH as RelationshipField, bI as RequestContext, bJ as ResolvedConfig, bK as RichTextDocument, bL as RichTextField, bM as Role, bN as RoleOperation, bO as RolePermissions, bP as Roles, bQ as RoutesConfig, bR as ScheduledPublish, bS as SchemaError, bT as SchemaMode, bU as SelectField, bV as SelectOption, bW as Session, bX as SidebarPanel, bY as SignupArgs, bZ as SignupResult, b_ as SingleSignOn, b$ as SlugField, c0 as SlugOf, c1 as SsoOutcome, c2 as SsoProviderRef, c3 as StorageAdapter, c4 as StoredFile, c5 as TextField, c6 as TextareaField, c7 as TooManyRequestsError, c8 as TypedPlugin, c9 as UnauthorizedError, ca as UpdateInput, cb as UpdateOptions, cc as UploadConfig, cd as UploadField, ce as UploadFromURLConfig, cf as UserIdentity, cg as ValidationError, ch as Version, ci as VersionSummary, cj as VersionsConfig, ck as WEBHOOK_EVENTS, cl as WebhookConfig, cm as WebhookEvent, cn as WebhookPayload, co as Where, cp as WhereOperators, cq as anyone, cr as consoleEmail, cs as consoleLogger, ct as createEasyCMS, cu as defineConfig, cv as defineFieldType, cw as definePlugin, cx as diskStorage, cy as hasRows, cz as isAdmin, cA as isHasMany, cB as isSignedIn, cC as isStaff, cD as isSystemAdmin, cE as mimeAllowedBy, cF as rowFields, cG as silentLogger, cH as uniqueWithinOf } from './config-CB8mhU2a.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-CJCqvyo_.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-bybEUkpd.js';
1
+ import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-CzW6kBeb.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 LocaleOf, be as LocalizationConfig, bf as LogDetails, bg as Logger, bh as LoginArgs, bi as LogsConfig, bj as MediaDocument, bk as MediaSize, bl as MembersConfig, bm as MembersSignup, bn as MigrationInfo, bo as NavGroup, bp as NotFoundError, bq as NumberField, br as OnRequest, bs as OnRequestArgs, bt as Operation, bu as PLUGIN_API_VERSION, bv as PasswordEmail, bw as PasswordEmailArgs, bx as PasswordEmailFn, by as PasswordLinkOptions, bz as PayloadTooLargeError, bA as Plugin, bB as PluginInfo, bC as PluginTypes, bD as PreviewTarget, bE as PreviewURL, bF as QueryError, bG as ROLE_OPERATIONS, bH as RawDocument, bI as ReadOptions, bJ as RelationshipField, bK as RequestContext, bL as ResolvedConfig, bM as RichTextDocument, bN as RichTextField, bO as Role, bP as RoleOperation, bQ as RolePermissions, bR as Roles, bS as RoutesConfig, bT as ScheduledPublish, bU as SchemaError, bV as SchemaMode, bW as SelectField, bX as SelectOption, bY as Session, bZ as SidebarPanel, b_ as SignupArgs, b$ as SignupResult, c0 as SingleSignOn, c1 as SlugField, c2 as SlugOf, c3 as SsoOutcome, c4 as SsoProviderRef, c5 as StorageAdapter, c6 as StoredFile, c7 as TextField, c8 as TextareaField, c9 as TooManyRequestsError, ca as TypedPlugin, cb as UnauthorizedError, cc as UpdateInput, cd as UpdateOptions, ce as UploadConfig, cf as UploadField, cg as UploadFromURLConfig, ch as UserIdentity, ci as ValidationError, cj as Version, ck as VersionSummary, cl as VersionsConfig, cm as WEBHOOK_EVENTS, cn as WebhookConfig, co as WebhookEvent, cp as WebhookPayload, cq as Where, cr as WhereOperators, cs as anyone, ct as consoleEmail, cu as consoleLogger, cv as createEasyCMS, cw as defineConfig, cx as defineFieldType, cy as definePlugin, cz as diskStorage, cA as hasRows, cB as isAdmin, cC as isHasMany, cD as isSignedIn, cE as isStaff, cF as isSystemAdmin, cG as jsonLogger, cH as mimeAllowedBy, cI as rowFields, cJ as silentLogger, cK as uniqueWithinOf } from './config-CzW6kBeb.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-BmKxHxuu.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-BsBQ-2qf.js';
5
5
  export { FieldCondition, matchesCondition } from './conditions.js';
6
6
 
7
7
  type DeliveryKind = 'webhook' | 'email';
package/dist/index.js CHANGED
@@ -18,12 +18,13 @@ import {
18
18
  isSignedIn,
19
19
  isStaff,
20
20
  isSystemAdmin,
21
+ jsonLogger,
21
22
  loadConfig,
22
23
  resolveConfig,
23
24
  sharedEasyCMS,
24
25
  silentLogger,
25
26
  slugify
26
- } from "./chunk-NFUXZ3FX.js";
27
+ } from "./chunk-AVUGFJ4Q.js";
27
28
  import {
28
29
  matchesCondition
29
30
  } from "./chunk-U266LV4M.js";
@@ -86,6 +87,7 @@ export {
86
87
  isSignedIn,
87
88
  isStaff,
88
89
  isSystemAdmin,
90
+ jsonLogger,
89
91
  loadConfig,
90
92
  matchesCondition,
91
93
  resolveConfig,
@@ -1,6 +1,6 @@
1
- import { ab as Config, al as Database, bm as NavGroup, a6 as CollectionConfig, I as ID, bF as RawDocument, aD as Field, bJ as ResolvedConfig, ad as ConfigIssue } from './config-CB8mhU2a.js';
2
- export { cI as API_KEYS, cJ as API_KEY_GLOBAL_OPERATIONS, cK as API_KEY_OPERATIONS, cL as DEFAULT_MAX_VERSIONS, cM as DEFAULT_PASSWORD_EMAILS, cN as FIELD_TYPE_BASES, cO as FOLDER_LEVELS, cP as MediaFolders, cQ as SSO_COOKIE, cR as codeOfStatus, cy as hasRows, cA as isHasMany, cS as keyAllows, cE as mimeAllowedBy, cF as rowFields, cH as uniqueWithinOf } from './config-CB8mhU2a.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-bybEUkpd.js';
1
+ import { ab as Config, al as Database, au as EasyCMS, bo as NavGroup, a6 as CollectionConfig, I as ID, bH as RawDocument, aD as Field, bL as ResolvedConfig, ad as ConfigIssue } from './config-CzW6kBeb.js';
2
+ export { cL as API_KEYS, cM as API_KEY_GLOBAL_OPERATIONS, cN as API_KEY_OPERATIONS, cO as DEFAULT_MAX_VERSIONS, cP as DEFAULT_PASSWORD_EMAILS, cQ as FIELD_TYPE_BASES, cR as FOLDER_LEVELS, cS as MediaFolders, cT as SSO_COOKIE, cU as codeOfStatus, cA as hasRows, cC as isHasMany, cV as isJsonLogger, cW as keyAllows, cX as logFormat, cH as mimeAllowedBy, cI as rowFields, cK as uniqueWithinOf } from './config-CzW6kBeb.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-BsBQ-2qf.js';
4
4
  export { conditionIssues } from './conditions.js';
5
5
 
6
6
  /** The session cookie the REST API sets at login. */
@@ -51,6 +51,14 @@ declare function copyDatabase(source: Database, target: Database, options?: {
51
51
  onTable?: (progress: CopyProgress) => void;
52
52
  }): Promise<CopyResult>;
53
53
 
54
+ type HealthCheck = 'healthz' | 'readyz';
55
+ /**
56
+ * `healthz`: the process answers (liveness), without touching the database. `readyz`: the CMS
57
+ * started, its database answers and no migration is pending (readiness); `503` otherwise, naming
58
+ * the check that failed, with the error in the log only. `cms` is `null` when it failed to start.
59
+ */
60
+ declare function healthResponse<C extends Config>(check: HealthCheck, instance: EasyCMS<C> | null): Promise<Response>;
61
+
54
62
  /** File types Easy CMS recognizes from their contents (FR-UPL-03). */
55
63
  declare const EXTENSIONS: Readonly<Record<string, string>>;
56
64
  /**
@@ -133,4 +141,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
133
141
  /** Checks a config and returns every problem found. An empty array means it is valid. */
134
142
  declare function validateConfig(config: Config): ConfigIssue[];
135
143
 
136
- export { BUILTIN_COLLECTIONS, BUILTIN_NAV, type CopyProgress, type CopyResult, DEFAULT_DEPTH, EXTENSIONS, INTERNAL_COLLECTIONS, MAX_DEPTH, MEDIA, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, SESSION_COOKIE, checkFormToken, checkRenamedOptions, configSignature, copyDatabase, expandMimeTypes, formToken, generateTypes, hashPassword, honeypotName, imageDimensions, mimeAllowed, populate, rateKeys, resolveAdminModule, singularize, sniffMimeType, validateConfig, verifyPassword, verifyTurnstile };
144
+ export { BUILTIN_COLLECTIONS, BUILTIN_NAV, type CopyProgress, type CopyResult, DEFAULT_DEPTH, EXTENSIONS, type HealthCheck, INTERNAL_COLLECTIONS, MAX_DEPTH, MEDIA, MEDIA_FOLDERS, MIME_GROUPS, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, SESSION_COOKIE, checkFormToken, checkRenamedOptions, configSignature, copyDatabase, expandMimeTypes, formToken, generateTypes, hashPassword, healthResponse, honeypotName, imageDimensions, mimeAllowed, populate, rateKeys, resolveAdminModule, singularize, sniffMimeType, validateConfig, verifyPassword, verifyTurnstile };
package/dist/internal.js CHANGED
@@ -38,9 +38,12 @@ import {
38
38
  forwardedClientIp,
39
39
  generateSlugs,
40
40
  hashPassword,
41
+ healthResponse,
41
42
  honeypotName,
42
43
  importConfig,
44
+ isJsonLogger,
43
45
  keyAllows,
46
+ logFormat,
44
47
  mergeForUpdate,
45
48
  parseId,
46
49
  platformClientIp,
@@ -53,7 +56,7 @@ import {
53
56
  verifyPassword,
54
57
  verifyTurnstile,
55
58
  writeBackupFile
56
- } from "./chunk-NFUXZ3FX.js";
59
+ } from "./chunk-AVUGFJ4Q.js";
57
60
  import {
58
61
  conditionIssues
59
62
  } from "./chunk-U266LV4M.js";
@@ -286,11 +289,14 @@ export {
286
289
  generateTypes,
287
290
  hasRows,
288
291
  hashPassword,
292
+ healthResponse,
289
293
  honeypotName,
290
294
  imageDimensions,
291
295
  importConfig,
292
296
  isHasMany,
297
+ isJsonLogger,
293
298
  keyAllows,
299
+ logFormat,
294
300
  mergeForUpdate,
295
301
  mimeAllowed,
296
302
  mimeAllowedBy,
@@ -1,4 +1,4 @@
1
- import { ba as Label, aD as Field, I as ID, k as AdminLocale, t as AdminSwitcher, c2 as SsoProviderRef } from './config-CB8mhU2a.js';
1
+ import { ba as Label, aD as Field, I as ID, k as AdminLocale, t as AdminSwitcher, c4 as SsoProviderRef } from './config-CzW6kBeb.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, bm as NavGroup, bn as NotFoundError, bp as OnRequest, bq as OnRequestArgs, bs as PLUGIN_API_VERSION, bx as PayloadTooLargeError, by as Plugin, bz as PluginInfo, bA as PluginTypes, bD as QueryError, bI as RequestContext, c7 as TooManyRequestsError, c8 as TypedPlugin, c9 as UnauthorizedError, cg as ValidationError, co as Where, cv as defineFieldType, cw as definePlugin } from './config-CB8mhU2a.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-CJCqvyo_.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, bo as NavGroup, bp as NotFoundError, br as OnRequest, bs as OnRequestArgs, bu as PLUGIN_API_VERSION, bz as PayloadTooLargeError, bA as Plugin, bB as PluginInfo, bC as PluginTypes, bF as QueryError, bK as RequestContext, c9 as TooManyRequestsError, ca as TypedPlugin, cb as UnauthorizedError, ci as ValidationError, cq as Where, cx as defineFieldType, cy as definePlugin } from './config-CzW6kBeb.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-BmKxHxuu.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.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",