@easy-cms/core 1.6.0 → 1.7.1

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.
@@ -2789,6 +2789,42 @@ function configSignature(config) {
2789
2789
  }
2790
2790
  var signatures = /* @__PURE__ */ new WeakMap();
2791
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
+
2792
2828
  // src/copy.ts
2793
2829
  var BATCH = 200;
2794
2830
  async function copyDatabase(source, target, options = {}) {
@@ -4825,7 +4861,9 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
4825
4861
  "jobs",
4826
4862
  "auth",
4827
4863
  "migrations",
4828
- "access"
4864
+ "access",
4865
+ "healthz",
4866
+ "readyz"
4829
4867
  ]);
4830
4868
  var SYSTEM_FIELD_NAMES = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
4831
4869
  var SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
@@ -5999,6 +6037,8 @@ var RESERVED_ENDPOINT_ROOTS = /* @__PURE__ */ new Set([
5999
6037
  "media",
6000
6038
  "api-keys",
6001
6039
  "auth",
6040
+ "healthz",
6041
+ "readyz",
6002
6042
  // Kept free for later versions.
6003
6043
  "system",
6004
6044
  "health",
@@ -6122,12 +6162,13 @@ function validateEndpoints(config, collectionSlugs, add) {
6122
6162
  const taken = [
6123
6163
  config.routes?.api ?? "/api/cms",
6124
6164
  `/${(config.admin?.path ?? "/admin").replace(/^\/+|\/+$/g, "")}`,
6125
- "/healthz"
6165
+ "/healthz",
6166
+ "/readyz"
6126
6167
  ].map((p) => p.replace(/\/+$/, "")).find((p) => route2 === p || route2.startsWith(`${p}/`) || p.startsWith(`${route2}/`));
6127
6168
  if (taken)
6128
6169
  add(
6129
6170
  `${path}.path`,
6130
- `"${route2}" is served by ${taken === "/healthz" ? "the standalone server" : `"${taken}"`}`
6171
+ `"${route2}" is served by ${taken === "/healthz" || taken === "/readyz" ? "the standalone server" : `"${taken}"`}`
6131
6172
  );
6132
6173
  } else if (RESERVED_ENDPOINT_ROOTS.has(first) || collectionSlugs.has(first)) {
6133
6174
  add(
@@ -9765,7 +9806,7 @@ async function readAdminModule(cms, index) {
9765
9806
  }
9766
9807
 
9767
9808
  // src/version.ts
9768
- var VERSION = true ? "1.6.0" : "0.0.0-dev";
9809
+ var VERSION = true ? "1.7.1" : "0.0.0-dev";
9769
9810
 
9770
9811
  // src/rest/handler.ts
9771
9812
  import { randomUUID as randomUUID4 } from "crypto";
@@ -10715,6 +10756,10 @@ function withCors(cms, handle) {
10715
10756
  return withHeaders(await handle(request), cors);
10716
10757
  };
10717
10758
  }
10759
+ var HEALTH_CHECKS = /* @__PURE__ */ new Map([
10760
+ ["healthz", "healthz"],
10761
+ ["readyz", "readyz"]
10762
+ ]);
10718
10763
  var REQUEST_ID = /^[\w.-]{1,128}$/;
10719
10764
  async function respond(cms, request, url, path, root, options, production) {
10720
10765
  const started = performance.now();
@@ -10732,7 +10777,8 @@ async function respond(cms, request, url, path, root, options, production) {
10732
10777
  } catch {
10733
10778
  tagged = withHeaders(response, new Headers({ "x-request-id": requestId }));
10734
10779
  }
10735
- if (cms.config.logs?.requests !== false && isJsonLogger(cms.logger)) {
10780
+ const probe = !root && (path === "/healthz" || path === "/readyz");
10781
+ if (!probe && cms.config.logs?.requests !== false && isJsonLogger(cms.logger)) {
10736
10782
  auditContext.run(
10737
10783
  { requestId },
10738
10784
  () => cms.logger.info(`${request.method} ${url.pathname} ${tagged.status}`, {
@@ -10756,6 +10802,15 @@ async function respondNow(cms, request, url, path, root, options, production, re
10756
10802
  try {
10757
10803
  if (path === null) throw new HttpError("Not found", 404);
10758
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
+ }
10759
10814
  const { token, via } = readToken(request);
10760
10815
  const signedIn = token ? await cms.auth.verify(token) : null;
10761
10816
  if (!signedIn && via === "bearer" && token?.startsWith("ecms_"))
@@ -12012,8 +12067,21 @@ function platformClientIp(headers) {
12012
12067
  function createApiHandler(config, options = {}) {
12013
12068
  const handlers = /* @__PURE__ */ new WeakMap();
12014
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(/\/+$/, "");
12015
12071
  return async (request) => {
12016
- 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
+ }
12017
12085
  let handler = handlers.get(cms);
12018
12086
  if (!handler) {
12019
12087
  handler = createRestHandler(cms, {
@@ -12111,6 +12179,7 @@ export {
12111
12179
  validateFields,
12112
12180
  consoleEmail,
12113
12181
  configSignature,
12182
+ healthResponse,
12114
12183
  copyDatabase,
12115
12184
  diskStorage,
12116
12185
  decryptBackup,
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  sharedEasyCMS,
25
25
  silentLogger,
26
26
  slugify
27
- } from "./chunk-GBEWSAZF.js";
27
+ } from "./chunk-ERXCJ3V6.js";
28
28
  import {
29
29
  matchesCondition
30
30
  } from "./chunk-U266LV4M.js";
@@ -1,4 +1,4 @@
1
- import { ab as Config, al as Database, 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';
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
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
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';
@@ -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,6 +38,7 @@ import {
38
38
  forwardedClientIp,
39
39
  generateSlugs,
40
40
  hashPassword,
41
+ healthResponse,
41
42
  honeypotName,
42
43
  importConfig,
43
44
  isJsonLogger,
@@ -55,7 +56,7 @@ import {
55
56
  verifyPassword,
56
57
  verifyTurnstile,
57
58
  writeBackupFile
58
- } from "./chunk-GBEWSAZF.js";
59
+ } from "./chunk-ERXCJ3V6.js";
59
60
  import {
60
61
  conditionIssues
61
62
  } from "./chunk-U266LV4M.js";
@@ -288,6 +289,7 @@ export {
288
289
  generateTypes,
289
290
  hasRows,
290
291
  hashPassword,
292
+ healthResponse,
291
293
  honeypotName,
292
294
  imageDimensions,
293
295
  importConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/core",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",