@easy-cms/core 1.14.0 → 1.15.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.
@@ -3377,6 +3377,10 @@ function errorFields(error) {
3377
3377
  return { name: error.name, message: error.message, stack: error.stack };
3378
3378
  return { message: String(error) };
3379
3379
  }
3380
+ var traceIds;
3381
+ function setTraceIds(read2) {
3382
+ traceIds = read2;
3383
+ }
3380
3384
  function jsonLogger(write = (line, level) => (level === "info" ? process.stdout : process.stderr).write(`${line}
3381
3385
  `)) {
3382
3386
  const log = (level) => (message2, details) => {
@@ -3389,6 +3393,7 @@ function jsonLogger(write = (line, level) => (level === "info" ? process.stdout
3389
3393
  message: message2,
3390
3394
  ...store2?.requestId ? { requestId: store2.requestId } : {},
3391
3395
  ...store2?.runId ? { runId: store2.runId } : {},
3396
+ ...traceIds?.(),
3392
3397
  ...rest,
3393
3398
  ...error === void 0 ? {} : { error: errorFields(error) }
3394
3399
  };
@@ -4637,7 +4642,7 @@ function read(response, maxBytes, deadline) {
4637
4642
  var keys = (list) => new Set(list.split(/\s+/).filter(Boolean));
4638
4643
  var CONFIG = keys(`secret db serverURL cors cronSecret timeZone webhooks events jobs localization routes admin
4639
4644
  upload auth collections globals endpoints cliCommands onRequest fieldTypes apiKeys email backups
4640
- audit logs countCacheSeconds maxPage httpCache outbox search plugins`);
4645
+ audit logs countCacheSeconds maxPage httpCache outbox search telemetry plugins`);
4641
4646
  var ADMIN = keys("path locale brand siteURL nav commands modules pages dashboard switcher");
4642
4647
  var AUTH = keys(`roles rbac tokenExpiration maxLoginAttempts lockWindow trustedOrigins
4643
4648
  resetPasswordExpiration inviteExpiration providers providerSignUp password setupCode members emails`);
@@ -5383,6 +5388,7 @@ function validateConfig(config) {
5383
5388
  validateCountCache(config, add);
5384
5389
  validateOutbox(config, add);
5385
5390
  validateSearch(config, add);
5391
+ validateTelemetry(config, add);
5386
5392
  validateMaxPage(config, add);
5387
5393
  validateHttpCache(config, add);
5388
5394
  validateAuth(config, add);
@@ -6036,6 +6042,18 @@ function validateOutbox(config, add) {
6036
6042
  if (config.outbox !== void 0 && typeof config.outbox !== "boolean")
6037
6043
  add("outbox", "must be true or false");
6038
6044
  }
6045
+ function validateTelemetry(config, add) {
6046
+ const value = config.telemetry;
6047
+ if (value === void 0 || typeof value === "boolean") return;
6048
+ if (typeof value !== "object" || value === null) {
6049
+ add("telemetry", "must be true, false or { traces, metrics }");
6050
+ return;
6051
+ }
6052
+ for (const [key, v] of Object.entries(value))
6053
+ if (key !== "traces" && key !== "metrics")
6054
+ add(`telemetry.${key}`, "is not an option", "traces or metrics");
6055
+ else if (typeof v !== "boolean") add(`telemetry.${key}`, "must be true or false");
6056
+ }
6039
6057
  function validateSearch(config, add) {
6040
6058
  const adapter = config.search;
6041
6059
  if (adapter !== void 0) {
@@ -7011,6 +7029,201 @@ function cacheableResponse(request, body, status, headers, times, tags) {
7011
7029
  return new Response(request.method.toUpperCase() === "HEAD" ? null : body, { status, headers });
7012
7030
  }
7013
7031
 
7032
+ // src/version.ts
7033
+ var VERSION = true ? "1.15.0" : "0.0.0-dev";
7034
+
7035
+ // src/telemetry.ts
7036
+ var DB_CALLS = /* @__PURE__ */ new Set([
7037
+ "find",
7038
+ "findPage",
7039
+ "findAfter",
7040
+ "findById",
7041
+ "findByIds",
7042
+ "count",
7043
+ "create",
7044
+ "update",
7045
+ "increment",
7046
+ "delete",
7047
+ "findGlobal",
7048
+ "updateGlobal"
7049
+ ]);
7050
+ var Telemetry = class _Telemetry {
7051
+ constructor(api, traces, metrics) {
7052
+ this.api = api;
7053
+ this.tracer = traces ? api.trace.getTracer("easy-cms", VERSION) : void 0;
7054
+ if (metrics) {
7055
+ const meter = api.metrics.getMeter("easy-cms", VERSION);
7056
+ this.httpDuration = meter.createHistogram("http.server.request.duration", {
7057
+ unit: "s",
7058
+ description: "Duration of API requests"
7059
+ });
7060
+ this.dbDuration = meter.createHistogram("db.client.operation.duration", {
7061
+ unit: "s",
7062
+ description: "Duration of database calls"
7063
+ });
7064
+ this.jobRuns = meter.createCounter("easy_cms.jobs.runs", {
7065
+ description: "Runs of the config\u2019s jobs, by job and result"
7066
+ });
7067
+ this.jobDuration = meter.createHistogram("easy_cms.jobs.duration", {
7068
+ unit: "s",
7069
+ description: "Duration of the config\u2019s jobs"
7070
+ });
7071
+ meter.createObservableGauge("easy_cms.queue.size", {
7072
+ description: "Background work waiting or failed, as of the last job run"
7073
+ }).addCallback((result) => {
7074
+ for (const [queue, sizes] of Object.entries(this.queues))
7075
+ for (const state of ["pending", "failed"])
7076
+ result.observe(sizes[state], { queue, state });
7077
+ });
7078
+ meter.createObservableGauge("easy_cms.scheduled.late", {
7079
+ description: "Scheduled publishes more than 10 minutes late, as of the last job run"
7080
+ }).addCallback((result) => result.observe(this.late));
7081
+ }
7082
+ if (traces)
7083
+ setTraceIds(() => {
7084
+ const context = api.trace.getActiveSpan()?.spanContext();
7085
+ return context && api.isSpanContextValid(context) ? { traceId: context.traceId, spanId: context.spanId } : void 0;
7086
+ });
7087
+ }
7088
+ api;
7089
+ tracer;
7090
+ httpDuration;
7091
+ dbDuration;
7092
+ jobRuns;
7093
+ jobDuration;
7094
+ queues = {};
7095
+ late = 0;
7096
+ /** `undefined` without `telemetry`; throws when `@opentelemetry/api` isn't installed. */
7097
+ static async load(config) {
7098
+ if (!config) return void 0;
7099
+ const { traces = true, metrics = true } = config === true ? {} : config;
7100
+ if (!traces && !metrics) return void 0;
7101
+ let api;
7102
+ try {
7103
+ api = await import("@opentelemetry/api");
7104
+ } catch {
7105
+ throw new ConfigError([
7106
+ {
7107
+ path: "telemetry",
7108
+ message: "needs @opentelemetry/api",
7109
+ hint: "npm install @opentelemetry/api, and set up the OpenTelemetry SDK in your app"
7110
+ }
7111
+ ]);
7112
+ }
7113
+ return new _Telemetry(api, traces, metrics);
7114
+ }
7115
+ /** Runs `fn` in a span; with traces off, just runs it. Errors mark the span and go on. */
7116
+ async span(name, attributes, fn, kind = "internal") {
7117
+ if (!this.tracer) return fn(void 0);
7118
+ const spanKind = { internal: 0, server: 1, client: 2 }[kind];
7119
+ return this.tracer.startActiveSpan(name, { kind: spanKind, attributes }, async (span) => {
7120
+ try {
7121
+ return await fn(span);
7122
+ } catch (error) {
7123
+ span.setStatus({ code: this.api.SpanStatusCode.ERROR });
7124
+ span.setAttribute("error.type", errorType(error));
7125
+ throw error;
7126
+ } finally {
7127
+ span.end();
7128
+ }
7129
+ });
7130
+ }
7131
+ /**
7132
+ * An API request: a server span when nothing traces it yet (`easy-cms serve`), else a span
7133
+ * inside the framework's (Next.js, Nuxt with an SDK).
7134
+ */
7135
+ async request(method, route2, fn) {
7136
+ const started = performance.now();
7137
+ const kind = this.api.trace.getActiveSpan() ? "internal" : "server";
7138
+ const attributes = { "http.request.method": method, "http.route": route2 };
7139
+ let status = 500;
7140
+ try {
7141
+ return await this.span(
7142
+ `easy-cms ${method} ${route2}`,
7143
+ attributes,
7144
+ async (span) => {
7145
+ const response = await fn();
7146
+ status = response.status;
7147
+ span?.setAttribute("http.response.status_code", status);
7148
+ if (status >= 500) span?.setStatus({ code: this.api.SpanStatusCode.ERROR });
7149
+ return response;
7150
+ },
7151
+ kind
7152
+ );
7153
+ } finally {
7154
+ this.httpDuration?.record((performance.now() - started) / 1e3, {
7155
+ ...attributes,
7156
+ "http.response.status_code": status
7157
+ });
7158
+ }
7159
+ }
7160
+ /** The database with a client span and a duration per call (`db <call> <collection>`). */
7161
+ database(db, system2) {
7162
+ return new Proxy(db, {
7163
+ get: (target, key, receiver) => {
7164
+ const value = Reflect.get(target, key, receiver);
7165
+ if (typeof key !== "string" || !DB_CALLS.has(key) || typeof value !== "function")
7166
+ return value;
7167
+ return (args, ...rest) => {
7168
+ const name = args?.collection ?? args?.slug ?? "";
7169
+ const attributes = {
7170
+ "db.system.name": system2,
7171
+ "db.operation.name": key,
7172
+ "db.collection.name": name
7173
+ };
7174
+ const started = performance.now();
7175
+ return this.span(
7176
+ `db ${key} ${name}`,
7177
+ attributes,
7178
+ () => Promise.resolve(value.call(target, args, ...rest)),
7179
+ "client"
7180
+ ).finally(() => this.dbDuration?.record((performance.now() - started) / 1e3, attributes));
7181
+ };
7182
+ }
7183
+ });
7184
+ }
7185
+ /** A run of one of the config's jobs, for `easy_cms.jobs.*`. */
7186
+ jobRan(name, ok, ms) {
7187
+ const attributes = { job: name, result: ok ? "ok" : "failed" };
7188
+ this.jobRuns?.add(1, attributes);
7189
+ this.jobDuration?.record(ms / 1e3, { job: name });
7190
+ }
7191
+ /** Whether the queue gauges are reported (and worth counting for). */
7192
+ get measuresQueues() {
7193
+ return this.jobRuns !== void 0;
7194
+ }
7195
+ /** Queue sizes found by the last job run, for the gauges. */
7196
+ queuesCounted(queues, late) {
7197
+ this.queues = queues;
7198
+ this.late = late;
7199
+ }
7200
+ };
7201
+ function errorType(error) {
7202
+ if (error && typeof error === "object") {
7203
+ const code = error.code;
7204
+ if (typeof code === "string" && code) return code;
7205
+ const name = error.name;
7206
+ if (typeof name === "string" && name) return name;
7207
+ }
7208
+ return "Error";
7209
+ }
7210
+ function routeOf(path, root) {
7211
+ if (path === null) return "other";
7212
+ if (root) return path;
7213
+ const parts = path.split("/").filter(Boolean);
7214
+ const [first] = parts;
7215
+ const fixed = /* @__PURE__ */ new Set(["auth", "admin", "jobs", "search", "healthz", "readyz", "api-keys"]);
7216
+ const word = /^[a-z][a-z.-]{0,39}$/;
7217
+ const route2 = parts.map((part, i) => {
7218
+ if (i === 0) return fixed.has(part) || part === "globals" ? part : ":collection";
7219
+ if (first === "globals" && i === 1) return ":global";
7220
+ if (first === "admin" || first === "auth") return word.test(part) ? part : ":param";
7221
+ if (i === 1) return /^\d+$/.test(part) || !word.test(part) ? ":id" : part;
7222
+ return word.test(part) ? part : ":id";
7223
+ });
7224
+ return `/${route2.join("/")}`;
7225
+ }
7226
+
7014
7227
  // src/local-api.ts
7015
7228
  async function createEasyCMS(config, options = {}) {
7016
7229
  const resolved3 = await resolveConfig(config);
@@ -7023,13 +7236,15 @@ async function createEasyCMS(config, options = {}) {
7023
7236
  const storage = resolved3.upload.storage ?? diskStorage({ dir: resolved3.upload.dir });
7024
7237
  await storage.init?.({ cwd });
7025
7238
  if (resolved3.upload.privateStorage) await resolved3.upload.privateStorage.init?.({ cwd });
7026
- const db = await resolved3.db.init({
7239
+ const telemetry = await Telemetry.load(resolved3.telemetry);
7240
+ const connected = await resolved3.db.init({
7027
7241
  config: resolved3,
7028
7242
  cwd,
7029
7243
  schema: options.schema ?? (process.env.NODE_ENV === "production" ? "verify" : "push"),
7030
7244
  logger,
7031
7245
  interactive: options.interactive ?? false
7032
7246
  });
7247
+ const db = telemetry ? telemetry.database(connected, dbSystem(resolved3.db.name)) : connected;
7033
7248
  if (resolved3.outbox && db.outbox !== true) {
7034
7249
  await db.destroy();
7035
7250
  throw new ConfigError([
@@ -7041,6 +7256,7 @@ async function createEasyCMS(config, options = {}) {
7041
7256
  ]);
7042
7257
  }
7043
7258
  const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
7259
+ cms.telemetry = telemetry;
7044
7260
  if (resolved3.search?.ephemeral && process.env.NODE_ENV === "production")
7045
7261
  logger.warn(
7046
7262
  `Search: the "${resolved3.search.name ?? "custom"}" index lives in this process's memory: it is built again at each start, and each instance has its own. Use a search engine adapter for large sites or several instances.`
@@ -7054,6 +7270,10 @@ async function createEasyCMS(config, options = {}) {
7054
7270
  );
7055
7271
  return cms;
7056
7272
  }
7273
+ function dbSystem(name) {
7274
+ if (name.includes("postgres") || name.includes("pglite")) return "postgresql";
7275
+ return name.includes("sqlite") ? "sqlite" : name;
7276
+ }
7057
7277
  var toJob = (row) => ({
7058
7278
  id: row.id,
7059
7279
  action: row.action,
@@ -7090,6 +7310,8 @@ var EasyCMS = class {
7090
7310
  outbox;
7091
7311
  /** With `search`: keeps the index in step with published documents. */
7092
7312
  searchIndex;
7313
+ /** @internal With `telemetry`: OpenTelemetry spans and metrics (set by `createEasyCMS`). */
7314
+ telemetry;
7093
7315
  /** @internal */
7094
7316
  constructor(config, db, logger = consoleLogger, storage = diskStorage({ dir: config.upload.dir }), cwd = process.cwd(), privateStorage) {
7095
7317
  this.config = config;
@@ -7117,7 +7339,15 @@ var EasyCMS = class {
7117
7339
  logger,
7118
7340
  config.collections.some((c) => c.slug === EMAIL_DELIVERIES) ? this.emailQueue() : void 0
7119
7341
  );
7120
- this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(db, logger, (entry, done, mark) => this.runOutbox(entry, done, mark)) : void 0;
7342
+ this.outbox = config.collections.some((c) => c.slug === OUTBOX) ? new Outbox(
7343
+ db,
7344
+ logger,
7345
+ (entry, done, mark) => this.telemetry ? this.telemetry.span(
7346
+ `easy-cms outbox ${entry.event} ${entry.target}`,
7347
+ { "easy_cms.event": entry.event, "easy_cms.collection": entry.target },
7348
+ () => this.runOutbox(entry, done, mark)
7349
+ ) : this.runOutbox(entry, done, mark)
7350
+ ) : void 0;
7121
7351
  this.searchIndex = config.search ? new SearchIndex(
7122
7352
  config.search,
7123
7353
  config.collections,
@@ -7164,99 +7394,112 @@ var EasyCMS = class {
7164
7394
  };
7165
7395
  }
7166
7396
  async find(collection2, options = {}) {
7167
- const config = this.collection(collection2);
7168
- const guard = guardOf(options);
7169
- const limit = options.limit ?? 10;
7170
- const page = options.page ?? 1;
7171
- if (!Number.isInteger(limit) || limit < 0)
7172
- throw new QueryError("limit must be a non-negative integer");
7173
- if (!Number.isInteger(page) || page < 1) throw new QueryError("page must be a positive integer");
7174
- const draft = readsDrafts(guard, options.draft);
7175
- const where = this.whereFor(
7176
- config,
7177
- draftWhere(config, draft, await this.readWhere(config, guard, options.where)),
7178
- options
7179
- );
7180
- const requested = options.sort === void 0 ? ["-createdAt"] : [options.sort].flat();
7181
- await this.checkQueryFields(config, guard, void 0, requested);
7182
- const locale = this.localeOf(options);
7183
- const sort = locale === void 0 ? requested : localizeSort(
7184
- config.fields,
7185
- requested,
7186
- locale === ALL_LOCALES ? this.config.localization?.defaultLocale ?? locale : locale,
7187
- this.config.localization?.locales ?? []
7188
- );
7189
- if (options.cursor !== void 0) {
7190
- if (options.page !== void 0) throw new QueryError("Use either cursor or page, not both");
7191
- if (limit === 0) throw new QueryError("A cursor needs a limit above 0");
7192
- if (!this.db.findAfter) throw new QueryError("This database adapter has no cursor pagination");
7193
- const query = cursorQuery(collection2, options.where, requested, locale, draft);
7194
- const after = readCursor(options.cursor, query);
7195
- const result2 = await this.db.findAfter({ collection: collection2, where, sort, limit, after });
7196
- const found2 = draft ? await this.withDrafts(config, result2.docs) : result2.docs;
7197
- const docs2 = await this.output(config, found2, guard, { ...options, draft });
7198
- return {
7199
- docs: docs2,
7200
- limit,
7201
- nextCursor: result2.hasNextPage && result2.next ? writeCursor(result2.next, query) : null,
7202
- hasNextPage: result2.hasNextPage,
7203
- hasPrevPage: after !== null,
7204
- totalDocs: null,
7205
- totalPages: null,
7206
- page: null
7207
- };
7208
- }
7209
- const args = { collection: collection2, where, sort, limit, page };
7210
- const count = options.count !== false || limit === 0;
7211
- const result = count ? await this.db.find(args) : this.db.findPage ? await this.db.findPage(args) : { ...await this.db.find(args), totalDocs: null, totalPages: null };
7212
- const found = draft ? await this.withDrafts(config, result.docs) : result.docs;
7213
- const docs = await this.output(config, found, guard, { ...options, draft });
7214
- return { ...result, docs };
7397
+ return this.traced("find", String(collection2), async () => {
7398
+ const config = this.collection(collection2);
7399
+ const guard = guardOf(options);
7400
+ const limit = options.limit ?? 10;
7401
+ const page = options.page ?? 1;
7402
+ if (!Number.isInteger(limit) || limit < 0)
7403
+ throw new QueryError("limit must be a non-negative integer");
7404
+ if (!Number.isInteger(page) || page < 1)
7405
+ throw new QueryError("page must be a positive integer");
7406
+ const draft = readsDrafts(guard, options.draft);
7407
+ const where = this.whereFor(
7408
+ config,
7409
+ draftWhere(config, draft, await this.readWhere(config, guard, options.where)),
7410
+ options
7411
+ );
7412
+ const requested = options.sort === void 0 ? ["-createdAt"] : [options.sort].flat();
7413
+ await this.checkQueryFields(config, guard, void 0, requested);
7414
+ const locale = this.localeOf(options);
7415
+ const sort = locale === void 0 ? requested : localizeSort(
7416
+ config.fields,
7417
+ requested,
7418
+ locale === ALL_LOCALES ? this.config.localization?.defaultLocale ?? locale : locale,
7419
+ this.config.localization?.locales ?? []
7420
+ );
7421
+ if (options.cursor !== void 0) {
7422
+ if (options.page !== void 0) throw new QueryError("Use either cursor or page, not both");
7423
+ if (limit === 0) throw new QueryError("A cursor needs a limit above 0");
7424
+ if (!this.db.findAfter)
7425
+ throw new QueryError("This database adapter has no cursor pagination");
7426
+ const query = cursorQuery(collection2, options.where, requested, locale, draft);
7427
+ const after = readCursor(options.cursor, query);
7428
+ const result2 = await this.db.findAfter({ collection: collection2, where, sort, limit, after });
7429
+ const found2 = draft ? await this.withDrafts(config, result2.docs) : result2.docs;
7430
+ const docs2 = await this.output(config, found2, guard, { ...options, draft });
7431
+ return {
7432
+ docs: docs2,
7433
+ limit,
7434
+ nextCursor: result2.hasNextPage && result2.next ? writeCursor(result2.next, query) : null,
7435
+ hasNextPage: result2.hasNextPage,
7436
+ hasPrevPage: after !== null,
7437
+ totalDocs: null,
7438
+ totalPages: null,
7439
+ page: null
7440
+ };
7441
+ }
7442
+ const args = { collection: collection2, where, sort, limit, page };
7443
+ const count = options.count !== false || limit === 0;
7444
+ const result = count ? await this.db.find(args) : this.db.findPage ? await this.db.findPage(args) : { ...await this.db.find(args), totalDocs: null, totalPages: null };
7445
+ const found = draft ? await this.withDrafts(config, result.docs) : result.docs;
7446
+ const docs = await this.output(config, found, guard, { ...options, draft });
7447
+ return { ...result, docs };
7448
+ });
7215
7449
  }
7216
7450
  async findById(collection2, id, options = {}) {
7217
- const config = this.collection(collection2);
7218
- const guard = guardOf(options);
7219
- const parsed = parseId(id);
7220
- if (parsed === void 0) return null;
7221
- const draft = readsDrafts(guard, options.draft);
7222
- const where = this.whereFor(
7223
- config,
7224
- draftWhere(config, draft, await this.readWhere(config, guard, void 0)),
7225
- options
7226
- );
7227
- const doc = where ? (await this.db.find({
7228
- collection: collection2,
7229
- where: andWhere(where, { id: { equals: parsed } }),
7230
- sort: [],
7231
- limit: 1,
7232
- page: 1
7233
- })).docs[0] : await this.db.findById({ collection: collection2, id: parsed });
7234
- if (!doc) return null;
7235
- const [current] = draft ? await this.withDrafts(config, [doc]) : [doc];
7236
- const [out] = await this.output(config, [current], guard, { ...options, draft });
7237
- return out ?? null;
7451
+ return this.traced("findById", String(collection2), async () => {
7452
+ const config = this.collection(collection2);
7453
+ const guard = guardOf(options);
7454
+ const parsed = parseId(id);
7455
+ if (parsed === void 0) return null;
7456
+ const draft = readsDrafts(guard, options.draft);
7457
+ const where = this.whereFor(
7458
+ config,
7459
+ draftWhere(config, draft, await this.readWhere(config, guard, void 0)),
7460
+ options
7461
+ );
7462
+ const doc = where ? (await this.db.find({
7463
+ collection: collection2,
7464
+ where: andWhere(where, { id: { equals: parsed } }),
7465
+ sort: [],
7466
+ limit: 1,
7467
+ page: 1
7468
+ })).docs[0] : await this.db.findById({ collection: collection2, id: parsed });
7469
+ if (!doc) return null;
7470
+ const [current] = draft ? await this.withDrafts(config, [doc]) : [doc];
7471
+ const [out] = await this.output(config, [current], guard, {
7472
+ ...options,
7473
+ draft
7474
+ });
7475
+ return out ?? null;
7476
+ });
7238
7477
  }
7239
7478
  async count(collection2, options = {}) {
7240
- const config = this.collection(collection2);
7241
- const where = this.whereFor(
7242
- config,
7243
- draftWhere(
7479
+ return this.traced("count", String(collection2), async () => {
7480
+ const config = this.collection(collection2);
7481
+ const where = this.whereFor(
7244
7482
  config,
7245
- readsDrafts(guardOf(options), options.draft),
7246
- await this.readWhere(config, guardOf(options), options.where)
7247
- ),
7248
- options
7249
- );
7250
- return this.db.count({ collection: collection2, where });
7483
+ draftWhere(
7484
+ config,
7485
+ readsDrafts(guardOf(options), options.draft),
7486
+ await this.readWhere(config, guardOf(options), options.where)
7487
+ ),
7488
+ options
7489
+ );
7490
+ return this.db.count({ collection: collection2, where });
7491
+ });
7251
7492
  }
7252
7493
  async create(collection2, data, options = {}) {
7253
- const config = this.collection(collection2);
7254
- if (config.slug === MEDIA) {
7255
- throw new ValidationError(MEDIA, [
7256
- { field: "file", message: "upload files with cms.upload() or POST multipart" }
7257
- ]);
7258
- }
7259
- return await this.createDocument(config, asObject(data, collection2), options);
7494
+ return this.traced("create", String(collection2), async () => {
7495
+ const config = this.collection(collection2);
7496
+ if (config.slug === MEDIA) {
7497
+ throw new ValidationError(MEDIA, [
7498
+ { field: "file", message: "upload files with cms.upload() or POST multipart" }
7499
+ ]);
7500
+ }
7501
+ return await this.createDocument(config, asObject(data, collection2), options);
7502
+ });
7260
7503
  }
7261
7504
  /**
7262
7505
  * Stores a file and creates its `media` document (FR-UPL). The type is detected from
@@ -7711,7 +7954,14 @@ var EasyCMS = class {
7711
7954
  }
7712
7955
  }
7713
7956
  async update(collection2, id, data, options = {}) {
7714
- return await this.updateDocument(collection2, id, asObject(data, collection2), options);
7957
+ return this.traced("update", String(collection2), async () => {
7958
+ return await this.updateDocument(
7959
+ collection2,
7960
+ id,
7961
+ asObject(data, collection2),
7962
+ options
7963
+ );
7964
+ });
7715
7965
  }
7716
7966
  /**
7717
7967
  * Adds `by` (negative to take away) to a top-level number field in one database statement, so
@@ -8082,65 +8332,69 @@ var EasyCMS = class {
8082
8332
  * gives the documents they own to another user; without it they have no owner.
8083
8333
  */
8084
8334
  async delete(collection2, id, options = {}) {
8085
- const config = this.collection(collection2);
8086
- const guard = guardOf(options);
8087
- const parsed = parseId(id);
8088
- const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
8089
- if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
8090
- await this.checkDocumentAccess(config, "delete", guard, parsed, void 0);
8091
- if (config.slug === USERS)
8092
- await this.guardLastAdmin(parsed, existing, { ...existing, active: false });
8093
- const base = this.hookArgs(config, guard);
8094
- for (const hook of config.hooks?.beforeDelete ?? []) await hook({ ...base, id: parsed });
8095
- if (config.slug === USERS) {
8096
- const to = options.transferTo;
8097
- if (to !== void 0 && to !== null) {
8098
- const target = parseId(to);
8099
- const heir = target === void 0 ? null : await this.db.findById({ collection: USERS, id: target });
8100
- if (!heir || String(heir.id) === String(parsed))
8101
- throw new ValidationError(USERS, [
8102
- { field: "transferTo", message: "must be another existing user" }
8103
- ]);
8104
- await this.roles.transfer(parsed, heir.id);
8105
- } else await this.roles.transfer(parsed, null);
8106
- await this.auth.revokeSessions(parsed);
8107
- await this.auth.sso.forget(parsed);
8108
- }
8109
- if (collection2 === MEDIA_FOLDERS) {
8110
- await this.checkMoveLimit(await this.folders.privacyAfter(parsed, { private: false }));
8111
- await this.folders.release(existing);
8112
- }
8113
- const outbox = this.outboxRow(config, "delete", guard, existing, { docId: parsed });
8114
- await this.db.delete({ collection: collection2, id: parsed, outbox });
8115
- if (collection2 === MEDIA_FOLDERS) {
8116
- this.folders.invalidate();
8117
- const parent = existing.parent ?? null;
8118
- if (parent !== null) await this.settlePrivacy(parent);
8119
- else await this.settleTopLevel();
8120
- }
8121
- if (versionLimit(config)) await this.versions.deleteAll(collectionParent(collection2), parsed);
8122
- if (config.schedule) {
8123
- for (const job of await this.pendingJobs(collection2, parsed))
8124
- await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
8125
- }
8126
- if (outbox) this.outbox?.kick();
8127
- else {
8128
- await this.notify(config.hooks?.afterDelete, "afterDelete", config.slug, {
8129
- ...base,
8130
- id: parsed,
8131
- doc: existing
8132
- });
8133
- this.emitChange(config, "delete", existing);
8134
- }
8135
- await this.audited(config, "delete", existing, existing, guard);
8136
- const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
8137
- return out;
8335
+ return this.traced("delete", String(collection2), async () => {
8336
+ const config = this.collection(collection2);
8337
+ const guard = guardOf(options);
8338
+ const parsed = parseId(id);
8339
+ const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
8340
+ if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
8341
+ await this.checkDocumentAccess(config, "delete", guard, parsed, void 0);
8342
+ if (config.slug === USERS)
8343
+ await this.guardLastAdmin(parsed, existing, { ...existing, active: false });
8344
+ const base = this.hookArgs(config, guard);
8345
+ for (const hook of config.hooks?.beforeDelete ?? []) await hook({ ...base, id: parsed });
8346
+ if (config.slug === USERS) {
8347
+ const to = options.transferTo;
8348
+ if (to !== void 0 && to !== null) {
8349
+ const target = parseId(to);
8350
+ const heir = target === void 0 ? null : await this.db.findById({ collection: USERS, id: target });
8351
+ if (!heir || String(heir.id) === String(parsed))
8352
+ throw new ValidationError(USERS, [
8353
+ { field: "transferTo", message: "must be another existing user" }
8354
+ ]);
8355
+ await this.roles.transfer(parsed, heir.id);
8356
+ } else await this.roles.transfer(parsed, null);
8357
+ await this.auth.revokeSessions(parsed);
8358
+ await this.auth.sso.forget(parsed);
8359
+ }
8360
+ if (collection2 === MEDIA_FOLDERS) {
8361
+ await this.checkMoveLimit(await this.folders.privacyAfter(parsed, { private: false }));
8362
+ await this.folders.release(existing);
8363
+ }
8364
+ const outbox = this.outboxRow(config, "delete", guard, existing, { docId: parsed });
8365
+ await this.db.delete({ collection: collection2, id: parsed, outbox });
8366
+ if (collection2 === MEDIA_FOLDERS) {
8367
+ this.folders.invalidate();
8368
+ const parent = existing.parent ?? null;
8369
+ if (parent !== null) await this.settlePrivacy(parent);
8370
+ else await this.settleTopLevel();
8371
+ }
8372
+ if (versionLimit(config)) await this.versions.deleteAll(collectionParent(collection2), parsed);
8373
+ if (config.schedule) {
8374
+ for (const job of await this.pendingJobs(collection2, parsed))
8375
+ await this.db.delete({ collection: SCHEDULED_JOBS, id: job.id });
8376
+ }
8377
+ if (outbox) this.outbox?.kick();
8378
+ else {
8379
+ await this.notify(config.hooks?.afterDelete, "afterDelete", config.slug, {
8380
+ ...base,
8381
+ id: parsed,
8382
+ doc: existing
8383
+ });
8384
+ this.emitChange(config, "delete", existing);
8385
+ }
8386
+ await this.audited(config, "delete", existing, existing, guard);
8387
+ const [out] = await this.output(config, [existing], guard, { depth: 0, draft: true });
8388
+ return out;
8389
+ });
8138
8390
  }
8139
8391
  async findGlobal(slug2, options = {}) {
8140
- const config = this.global(slug2);
8141
- const guard = guardOf(options);
8142
- await this.checkGlobalAccess(config, "read", guard);
8143
- return this.readGlobal(config, this.globalKey(config, guard), guard, options);
8392
+ return this.traced("findGlobal", String(slug2), async () => {
8393
+ const config = this.global(slug2);
8394
+ const guard = guardOf(options);
8395
+ await this.checkGlobalAccess(config, "read", guard);
8396
+ return this.readGlobal(config, this.globalKey(config, guard), guard, options);
8397
+ });
8144
8398
  }
8145
8399
  /** A global as stored under `key` (see `globalKey`); empty when there is no key. */
8146
8400
  async readGlobal(config, key, guard, options) {
@@ -8180,7 +8434,9 @@ var EasyCMS = class {
8180
8434
  return key;
8181
8435
  }
8182
8436
  async updateGlobal(slug2, data, options = {}) {
8183
- return this.saveGlobal(slug2, asObject(data, slug2), options);
8437
+ return this.traced("updateGlobal", String(slug2), async () => {
8438
+ return this.saveGlobal(slug2, asObject(data, slug2), options);
8439
+ });
8184
8440
  }
8185
8441
  /** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
8186
8442
  async unpublishGlobal(slug2, options = {}) {
@@ -8447,36 +8703,39 @@ var EasyCMS = class {
8447
8703
  * hold fewer documents than `limit`.
8448
8704
  */
8449
8705
  async search(query, options = {}) {
8450
- const index = this.searchIndex;
8451
- if (!index) throw new QueryError("Search needs a `search` adapter in the config");
8452
- const limit = options.limit ?? 10;
8453
- const page = options.page ?? 1;
8454
- if (!Number.isInteger(limit) || limit < 1 || limit > 100)
8455
- throw new QueryError("limit must be an integer from 1 to 100");
8456
- if (!Number.isInteger(page) || page < 1) throw new QueryError("page must be an integer from 1");
8457
- const collections = options.collections ?? index.searchable;
8458
- for (const slug2 of collections)
8459
- if (!index.configOf(slug2)) throw new QueryError(`"${slug2}" is not searchable`);
8460
- const localization = this.config.localization;
8461
- const asked = options.locale && options.locale !== ALL_LOCALES ? options.locale : void 0;
8462
- if (asked !== void 0 && localization && !localization.locales.includes(asked))
8463
- throw new QueryError(`Unknown locale "${asked}"`);
8464
- const locale = localization ? asked ?? localization.defaultLocale ?? null : null;
8465
- const text3 = query.trim().slice(0, 200);
8466
- const empty = { hits: [], total: 0, page, limit, hasNextPage: false };
8467
- if (!text3 || collections.length === 0) return empty;
8468
- const { collections: _, limit: __, page: ___, ...read2 } = options;
8469
- const { hits, total } = await index.search(
8470
- { query: text3, collections, locale, limit, offset: (page - 1) * limit },
8471
- async (collection2, ids) => (await this.find(collection2, {
8472
- ...read2,
8473
- ...locale ? { locale } : {},
8474
- where: { id: { in: ids.map((id) => parseId(id) ?? id) } },
8475
- limit: ids.length,
8476
- draft: false
8477
- })).docs
8478
- );
8479
- return { hits, total, page, limit, hasNextPage: page * limit < total };
8706
+ return this.traced("search", "", async () => {
8707
+ const index = this.searchIndex;
8708
+ if (!index) throw new QueryError("Search needs a `search` adapter in the config");
8709
+ const limit = options.limit ?? 10;
8710
+ const page = options.page ?? 1;
8711
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
8712
+ throw new QueryError("limit must be an integer from 1 to 100");
8713
+ if (!Number.isInteger(page) || page < 1)
8714
+ throw new QueryError("page must be an integer from 1");
8715
+ const collections = options.collections ?? index.searchable;
8716
+ for (const slug2 of collections)
8717
+ if (!index.configOf(slug2)) throw new QueryError(`"${slug2}" is not searchable`);
8718
+ const localization = this.config.localization;
8719
+ const asked = options.locale && options.locale !== ALL_LOCALES ? options.locale : void 0;
8720
+ if (asked !== void 0 && localization && !localization.locales.includes(asked))
8721
+ throw new QueryError(`Unknown locale "${asked}"`);
8722
+ const locale = localization ? asked ?? localization.defaultLocale ?? null : null;
8723
+ const text3 = query.trim().slice(0, 200);
8724
+ const empty = { hits: [], total: 0, page, limit, hasNextPage: false };
8725
+ if (!text3 || collections.length === 0) return empty;
8726
+ const { collections: _, limit: __, page: ___, ...read2 } = options;
8727
+ const { hits, total } = await index.search(
8728
+ { query: text3, collections, locale, limit, offset: (page - 1) * limit },
8729
+ async (collection2, ids) => (await this.find(collection2, {
8730
+ ...read2,
8731
+ ...locale ? { locale } : {},
8732
+ where: { id: { in: ids.map((id) => parseId(id) ?? id) } },
8733
+ limit: ids.length,
8734
+ draft: false
8735
+ })).docs
8736
+ );
8737
+ return { hits, total, page, limit, hasNextPage: page * limit < total };
8738
+ });
8480
8739
  }
8481
8740
  /**
8482
8741
  * Builds the search index again from the database, for every searchable collection or one:
@@ -8515,6 +8774,18 @@ var EasyCMS = class {
8515
8774
  if (!found.hasNextPage) return;
8516
8775
  }
8517
8776
  }
8777
+ /** A Local API call in a span (`easy-cms <operation> <collection>`) with `telemetry`. */
8778
+ traced(operation, target, fn) {
8779
+ if (!this.telemetry) return fn();
8780
+ return this.telemetry.span(
8781
+ `easy-cms ${operation}${target ? ` ${target}` : ""}`,
8782
+ {
8783
+ "easy_cms.operation": operation,
8784
+ ...target ? { "easy_cms.collection": target } : {}
8785
+ },
8786
+ fn
8787
+ );
8788
+ }
8518
8789
  /** @internal Puts a failed outbox entry back in line (Settings → Jobs). */
8519
8790
  retryOutbox(id) {
8520
8791
  return this.outbox?.retry(id) ?? Promise.resolve(false);
@@ -8546,7 +8817,9 @@ var EasyCMS = class {
8546
8817
  * scheduler and the cron endpoint (`GET <api>/jobs/run`) call.
8547
8818
  */
8548
8819
  runJobs(now = /* @__PURE__ */ new Date()) {
8549
- const run = this.running.then(() => this.runJobsNow(now));
8820
+ const run = this.running.then(
8821
+ () => this.telemetry ? this.telemetry.span("easy-cms jobs.run", {}, () => this.runJobsNow(now)) : this.runJobsNow(now)
8822
+ );
8550
8823
  this.running = run.catch(() => {
8551
8824
  });
8552
8825
  return run;
@@ -8573,6 +8846,10 @@ var EasyCMS = class {
8573
8846
  jobs: await this.runConfigJobs(now),
8574
8847
  outbox: await this.outbox?.run(now) ?? { ran: 0, failed: 0 }
8575
8848
  };
8849
+ if (this.telemetry?.measuresQueues)
8850
+ await this.countQueues(now).catch(
8851
+ (error) => this.logger.error(`Telemetry: ${error.message}`, { error })
8852
+ );
8576
8853
  const ranAt = (/* @__PURE__ */ new Date()).toISOString();
8577
8854
  await this.db.updateGlobal({ slug: JOBS_RAN, data: { at: ranAt, updatedAt: ranAt } }).catch((error) => this.logger.error(`Jobs: ${error.message}`, { error }));
8578
8855
  return result;
@@ -8641,19 +8918,47 @@ var EasyCMS = class {
8641
8918
  const started = Date.now();
8642
8919
  let error = null;
8643
8920
  try {
8644
- await job.run({ cms: this, now });
8921
+ const work = async () => job.run({ cms: this, now });
8922
+ await (this.telemetry ? this.telemetry.span(`easy-cms job ${job.name}`, { "easy_cms.job": job.name }, work) : work());
8645
8923
  ran++;
8646
8924
  } catch (caught) {
8647
8925
  failed++;
8648
8926
  error = (caught.message || String(caught)).slice(0, 500);
8649
8927
  this.logger.error(`Job ${job.name} failed: ${error}`, { error: caught, job: job.name });
8650
8928
  }
8929
+ this.telemetry?.jobRan(job.name, error === null, Date.now() - started);
8651
8930
  await this.recordJobRun(job.name, stamp, error, Date.now() - started).catch(
8652
8931
  (caught) => this.logger.error(`Job ${job.name}: ${caught.message}`, { error: caught })
8653
8932
  );
8654
8933
  }
8655
8934
  return { ran, failed };
8656
8935
  }
8936
+ /** Background work waiting and failed, for `telemetry`'s queue gauges. */
8937
+ async countQueues(now) {
8938
+ const has3 = (slug2) => this.config.collections.some((c) => c.slug === slug2);
8939
+ const sizes = async (collection2, pending) => ({
8940
+ pending: await this.db.count({ collection: collection2, where: { state: { in: pending } } }),
8941
+ failed: await this.db.count({ collection: collection2, where: { state: { equals: "failed" } } })
8942
+ });
8943
+ const queues = {};
8944
+ if (has3(OUTBOX)) queues.outbox = await sizes(OUTBOX, ["pending", "running"]);
8945
+ if (has3(WEBHOOK_DELIVERIES)) queues.webhooks = await sizes(WEBHOOK_DELIVERIES, ["pending"]);
8946
+ if (has3(EMAIL_DELIVERIES)) queues.emails = await sizes(EMAIL_DELIVERIES, ["pending"]);
8947
+ let late = 0;
8948
+ if (has3(SCHEDULED_JOBS)) {
8949
+ queues.scheduled = await sizes(SCHEDULED_JOBS, ["pending", "running"]);
8950
+ late = await this.db.count({
8951
+ collection: SCHEDULED_JOBS,
8952
+ where: {
8953
+ and: [
8954
+ { state: { equals: "pending" } },
8955
+ { runAt: { lt: new Date(now.getTime() - 10 * 6e4).toISOString() } }
8956
+ ]
8957
+ }
8958
+ });
8959
+ }
8960
+ this.telemetry?.queuesCounted(queues, late);
8961
+ }
8657
8962
  async recordJobRun(name, at, error, ms) {
8658
8963
  const { docs } = await this.db.find({
8659
8964
  collection: JOB_RUNS,
@@ -10750,9 +11055,6 @@ async function readAdminModule(cms, index) {
10750
11055
  return { body, etag: `"${createHash6("sha1").update(body).digest("base64url")}"` };
10751
11056
  }
10752
11057
 
10753
- // src/version.ts
10754
- var VERSION = true ? "1.14.0" : "0.0.0-dev";
10755
-
10756
11058
  // src/rest/handler.ts
10757
11059
  import { randomUUID as randomUUID4 } from "crypto";
10758
11060
 
@@ -11528,6 +11830,7 @@ async function adminStatus(cms) {
11528
11830
  storage: config.upload.storage?.name ?? "local",
11529
11831
  email: config.email ? config.email.name ?? "custom" : null,
11530
11832
  search: config.search ? config.search.name ?? "custom" : null,
11833
+ telemetry: !!config.telemetry,
11531
11834
  plugins: config.installedPlugins.map((p) => ({ ...p })),
11532
11835
  fieldTypes: config.fieldTypes.map((t) => t.name)
11533
11836
  },
@@ -11726,7 +12029,14 @@ var HEALTH_CHECKS = /* @__PURE__ */ new Map([
11726
12029
  ["readyz", "readyz"]
11727
12030
  ]);
11728
12031
  var REQUEST_ID = /^[\w.-]{1,128}$/;
11729
- async function respond(cms, request, url, path, root, options, production) {
12032
+ function respond(cms, request, url, path, root, options, production) {
12033
+ const run = () => respondLogged(cms, request, url, path, root, options, production);
12034
+ const probe = !root && (path === "/healthz" || path === "/readyz");
12035
+ if (!cms.telemetry || probe) return run();
12036
+ const route2 = root || path === null ? routeOf(path, root) : `${cms.config.routes.api}${routeOf(path, root)}`;
12037
+ return cms.telemetry.request(request.method, route2, run);
12038
+ }
12039
+ async function respondLogged(cms, request, url, path, root, options, production) {
11730
12040
  const started = performance.now();
11731
12041
  const incoming = request.headers.get("x-request-id");
11732
12042
  const requestId = incoming && REQUEST_ID.test(incoming) ? incoming : randomUUID4();
@@ -13227,10 +13537,10 @@ export {
13227
13537
  applyPlugins,
13228
13538
  resolveConfig,
13229
13539
  cacheTags,
13540
+ VERSION,
13230
13541
  createEasyCMS,
13231
13542
  EasyCMS,
13232
13543
  resolveAdminModule,
13233
- VERSION,
13234
13544
  CSRF_COOKIE,
13235
13545
  CSRF_HEADER,
13236
13546
  createRestHandler,
@@ -119,7 +119,8 @@ declare function logFormat(logs: LogsConfig | undefined): 'pretty' | 'json';
119
119
  declare const isJsonLogger: (logger: Logger) => boolean;
120
120
  /**
121
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.
122
+ * the job run's `runId`), with `telemetry` the active span's `traceId` and `spanId`, `error`
123
+ * with its stack, and any other details.
123
124
  */
124
125
  declare function jsonLogger(write?: (line: string, level: 'info' | 'warn' | 'error') => void): Logger;
125
126
 
@@ -2107,6 +2108,8 @@ declare class EasyCMS<C extends Config = Config> {
2107
2108
  }[]>;
2108
2109
  /** Published documents of a collection in a locale, a page at a time, for the search index. */
2109
2110
  private publishedPages;
2111
+ /** A Local API call in a span (`easy-cms <operation> <collection>`) with `telemetry`. */
2112
+ private traced;
2110
2113
  destroy(): Promise<void>;
2111
2114
  private schedulerTimer;
2112
2115
  private running;
@@ -2147,6 +2150,8 @@ declare class EasyCMS<C extends Config = Config> {
2147
2150
  * many instances run jobs.
2148
2151
  */
2149
2152
  private runConfigJobs;
2153
+ /** Background work waiting and failed, for `telemetry`'s queue gauges. */
2154
+ private countQueues;
2150
2155
  private recordJobRun;
2151
2156
  /**
2152
2157
  * Sends an event of the app or a plugin (`events` in the config, e.g. `order.paid`) to the
@@ -2732,6 +2737,14 @@ interface CollectionSearch {
2732
2737
  */
2733
2738
  declare function memorySearch(): SearchAdapter;
2734
2739
 
2740
+ /** `telemetry` in the config: OpenTelemetry traces and metrics, both on with `true`. */
2741
+ interface TelemetryConfig {
2742
+ /** Spans for API requests, Local API calls, database calls and background jobs. Default true. */
2743
+ readonly traces?: boolean;
2744
+ /** Request and database durations, job runs and queue sizes. Default true. */
2745
+ readonly metrics?: boolean;
2746
+ }
2747
+
2735
2748
  declare const WEBHOOK_EVENTS: readonly ["create", "update", "delete", "publish", "unpublish", "draft"];
2736
2749
  /**
2737
2750
  * `create`, `update`, `delete`: the stored document changed. `publish` / `unpublish`: its status
@@ -3616,6 +3629,13 @@ interface Config {
3616
3629
  * (with `index: 'trigram'` on Postgres) still finds text.
3617
3630
  */
3618
3631
  readonly search?: SearchAdapter;
3632
+ /**
3633
+ * OpenTelemetry traces and metrics through `@opentelemetry/api` (install it, and set up the SDK
3634
+ * in your app): spans for API requests, Local API calls, database calls and jobs, and metrics
3635
+ * for request and database durations, job runs and queue sizes. `true` turns both on. Spans
3636
+ * carry names of collections, routes and operations, never query values or documents.
3637
+ */
3638
+ readonly telemetry?: boolean | TelemetryConfig;
3619
3639
  readonly plugins?: readonly Plugin[];
3620
3640
  }
3621
3641
  /** The config after plugins ran, validation passed and defaults were applied. */
@@ -3647,5 +3667,5 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3647
3667
  */
3648
3668
  declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3649
3669
 
3650
- export { ADAPTER_API_VERSION as A, AuditLog as J, Auth as M, ADMIN_ICONS as a, ForbiddenError as a$, FIELD_TYPES as aE, ConfigError as ad, EasyCMS as aw, EasyCMSError as ax, PayloadTooLargeError as bE, QueryError as bK, ROLE_OPERATIONS as bL, Roles as bW, SchemaError as bZ, NotFoundError as bt, PLUGIN_API_VERSION as bz, API_KEY_OPERATIONS as c$, anyone as cF, consoleEmail as cG, consoleLogger as cH, createEasyCMS as cI, defineConfig as cJ, defineFieldType as cK, definePlugin as cL, diskStorage as cM, hasRows as cN, isAdmin as cO, isHasMany as cP, isSignedIn as cQ, isStaff as cR, isSystemAdmin as cS, jsonLogger as cT, memorySearch as cU, mimeAllowedBy as cV, rowFields as cW, silentLogger as cX, uniqueWithinOf as cY, API_KEYS as cZ, API_KEY_GLOBAL_OPERATIONS as c_, SingleSignOn as cc, TooManyRequestsError as cl, UnauthorizedError as cn, ValidationError as cv, WEBHOOK_EVENTS as cz, DEFAULT_MAX_VERSIONS as d0, DEFAULT_PASSWORD_EMAILS as d1, FIELD_TYPE_BASES as d2, FOLDER_LEVELS as d3, MediaFolders as d4, SSO_COOKIE as d5, codeOfStatus as d6, isJsonLogger as d7, keyAllows as d8, logFormat as d9 };
3651
- export type { BuiltinMediaCollection as $, ApiKeyOperation as B, ApiKeyPermissions as C, ArrayField as D, EmailSetting as E, AuditChange as F, AuditConfig as G, AuditEntry as H, ID as I, AuditPage as K, AuditVerification as L, AuthConfig as N, AuthProvider as O, PaginatedDocs as P, AuthProviderProfile as Q, AuthProviderRequest as R, AuthUser as S, BackupsConfig as T, BeforeChangeHook as U, BeforeDeleteHook as V, BeforeValidateHook as W, Block as X, BlocksField as Y, BooleanField as Z, BuiltinApiKeysCollection as _, BuiltinMediaFoldersCollection as a0, BuiltinUsersCollection as a1, BundleHints as a2, CliCommand as a3, CollectionAccess as a4, CollectionAdmin as a5, CollectionConfig as a6, CollectionDocument as a7, CollectionHooks as a8, CollectionListAdmin as a9, EmailMessage as aA, Endpoint as aB, EndpointRequest as aC, ErrorCode as aD, Field as aF, FieldAccess as aG, FieldAccessFn as aH, FieldAdmin as aI, FieldError as aJ, FieldRule as aK, FieldType as aL, FieldTypeBase as aM, FieldTypeDefinition as aN, FieldTypeValidateContext as aO, FieldValidate as aP, FieldValidateContext as aQ, FieldValue as aR, FieldsInput as aS, FieldsValue as aT, FilterOptions as aU, FilterOptionsArgs as aV, FindAfterArgs as aW, FindArgs as aX, FindOptions as aY, FolderLevel as aZ, FolderPermissions as a_, CollectionSearch as aa, CollectionSlug as ab, Config as ac, ConfigIssue as ae, ContainerAdmin as af, CreateEasyCMSOptions as ag, CreateInput as ah, CreateMigrationResult as ai, CursorDocs as aj, CustomField as ak, CustomFieldTypes as al, DashboardWidget as am, Database as an, DatabaseAdapter as ao, DatabaseInitArgs as ap, DatabaseTransfer as aq, DateField as ar, DepthOptions as as, DirectUpload as at, DiskStorageOptions as au, DocumentOf as av, EmailAdapter as ay, EmailField as az, Access as b, SearchAdapter as b$, GlobalAccess as b0, GlobalConfig as b1, GlobalDocument as b2, GlobalHooks as b3, GlobalInput as b4, GlobalSlug as b5, GroupField as b6, ImageSize as b7, IncrementOptions as b8, InferCollection as b9, PasswordEmail as bA, PasswordEmailArgs as bB, PasswordEmailFn as bC, PasswordLinkOptions as bD, Plugin as bF, PluginInfo as bG, PluginTypes as bH, PreviewTarget as bI, PreviewURL as bJ, RawDocument as bM, ReadOptions as bN, RelationshipField as bO, RequestContext as bP, ResolvedConfig as bQ, RichTextDocument as bR, RichTextField as bS, Role as bT, RoleOperation as bU, RolePermissions as bV, RoutesConfig as bX, ScheduledPublish as bY, SchemaMode as b_, InferGlobal as ba, JobConfig as bb, JsonField as bc, KeysetPage as bd, Label as be, LayoutNode as bf, LivePreview as bg, LocaleOf as bh, LocalizationConfig as bi, LogDetails as bj, Logger as bk, LoginArgs as bl, LogsConfig as bm, MediaDocument as bn, MediaSize as bo, MembersConfig as bp, MembersSignup as bq, MigrationInfo as br, NavGroup as bs, NumberField as bu, OnRequest as bv, OnRequestArgs as bw, Operation as bx, OutboxRow as by, AccessArgs as c, SearchEntry as c0, SearchHits as c1, SearchOptions as c2, SearchQuery as c3, SearchResults as c4, SearchSetting as c5, SelectField as c6, SelectOption as c7, Session as c8, SidebarPanel as c9, WebhookConfig as cA, WebhookEvent as cB, WebhookPayload as cC, Where as cD, WhereOperators as cE, SignupArgs as ca, SignupResult as cb, SlugField as cd, SlugOf as ce, SsoOutcome as cf, SsoProviderRef as cg, StorageAdapter as ch, StoredFile as ci, TextField as cj, TextareaField as ck, TypedPlugin as cm, UncountedDocs as co, UpdateInput as cp, UpdateOptions as cq, UploadConfig as cr, UploadField as cs, UploadFromURLConfig as ct, UserIdentity as cu, Version as cw, VersionSummary as cx, VersionsConfig as cy, AccessOptions as d, AdminBrand as e, AdminCommand as f, AdminComponent as g, AdminConfig as h, AdminFieldArgs as i, AdminIcon as j, AdminLocale as k, AdminPage as l, AdminRole as m, AdminRoleChange as n, AdminRoleField as o, AdminRoleTarget as p, AdminRoleView as q, AdminRoles as r, AdminSso as s, AdminSwitcher as t, AdminViewAccess as u, AfterChangeHook as v, AfterDeleteHook as w, AfterReadHook as x, ApiKeyContext as y, ApiKeyGlobalOperation as z };
3670
+ export { ADAPTER_API_VERSION as A, AuditLog as J, Auth as M, ADMIN_ICONS as a, ForbiddenError as a$, FIELD_TYPES as aE, ConfigError as ad, EasyCMS as aw, EasyCMSError as ax, PayloadTooLargeError as bE, QueryError as bK, ROLE_OPERATIONS as bL, Roles as bW, SchemaError as bZ, NotFoundError as bt, PLUGIN_API_VERSION as bz, API_KEY_GLOBAL_OPERATIONS as c$, WEBHOOK_EVENTS as cA, anyone as cG, consoleEmail as cH, consoleLogger as cI, createEasyCMS as cJ, defineConfig as cK, defineFieldType as cL, definePlugin as cM, diskStorage as cN, hasRows as cO, isAdmin as cP, isHasMany as cQ, isSignedIn as cR, isStaff as cS, isSystemAdmin as cT, jsonLogger as cU, memorySearch as cV, mimeAllowedBy as cW, rowFields as cX, silentLogger as cY, uniqueWithinOf as cZ, API_KEYS as c_, SingleSignOn as cc, TooManyRequestsError as cm, UnauthorizedError as co, ValidationError as cw, API_KEY_OPERATIONS as d0, DEFAULT_MAX_VERSIONS as d1, DEFAULT_PASSWORD_EMAILS as d2, FIELD_TYPE_BASES as d3, FOLDER_LEVELS as d4, MediaFolders as d5, SSO_COOKIE as d6, codeOfStatus as d7, isJsonLogger as d8, keyAllows as d9, logFormat as da };
3671
+ export type { BuiltinMediaCollection as $, ApiKeyOperation as B, ApiKeyPermissions as C, ArrayField as D, EmailSetting as E, AuditChange as F, AuditConfig as G, AuditEntry as H, ID as I, AuditPage as K, AuditVerification as L, AuthConfig as N, AuthProvider as O, PaginatedDocs as P, AuthProviderProfile as Q, AuthProviderRequest as R, AuthUser as S, BackupsConfig as T, BeforeChangeHook as U, BeforeDeleteHook as V, BeforeValidateHook as W, Block as X, BlocksField as Y, BooleanField as Z, BuiltinApiKeysCollection as _, BuiltinMediaFoldersCollection as a0, BuiltinUsersCollection as a1, BundleHints as a2, CliCommand as a3, CollectionAccess as a4, CollectionAdmin as a5, CollectionConfig as a6, CollectionDocument as a7, CollectionHooks as a8, CollectionListAdmin as a9, EmailMessage as aA, Endpoint as aB, EndpointRequest as aC, ErrorCode as aD, Field as aF, FieldAccess as aG, FieldAccessFn as aH, FieldAdmin as aI, FieldError as aJ, FieldRule as aK, FieldType as aL, FieldTypeBase as aM, FieldTypeDefinition as aN, FieldTypeValidateContext as aO, FieldValidate as aP, FieldValidateContext as aQ, FieldValue as aR, FieldsInput as aS, FieldsValue as aT, FilterOptions as aU, FilterOptionsArgs as aV, FindAfterArgs as aW, FindArgs as aX, FindOptions as aY, FolderLevel as aZ, FolderPermissions as a_, CollectionSearch as aa, CollectionSlug as ab, Config as ac, ConfigIssue as ae, ContainerAdmin as af, CreateEasyCMSOptions as ag, CreateInput as ah, CreateMigrationResult as ai, CursorDocs as aj, CustomField as ak, CustomFieldTypes as al, DashboardWidget as am, Database as an, DatabaseAdapter as ao, DatabaseInitArgs as ap, DatabaseTransfer as aq, DateField as ar, DepthOptions as as, DirectUpload as at, DiskStorageOptions as au, DocumentOf as av, EmailAdapter as ay, EmailField as az, Access as b, SearchAdapter as b$, GlobalAccess as b0, GlobalConfig as b1, GlobalDocument as b2, GlobalHooks as b3, GlobalInput as b4, GlobalSlug as b5, GroupField as b6, ImageSize as b7, IncrementOptions as b8, InferCollection as b9, PasswordEmail as bA, PasswordEmailArgs as bB, PasswordEmailFn as bC, PasswordLinkOptions as bD, Plugin as bF, PluginInfo as bG, PluginTypes as bH, PreviewTarget as bI, PreviewURL as bJ, RawDocument as bM, ReadOptions as bN, RelationshipField as bO, RequestContext as bP, ResolvedConfig as bQ, RichTextDocument as bR, RichTextField as bS, Role as bT, RoleOperation as bU, RolePermissions as bV, RoutesConfig as bX, ScheduledPublish as bY, SchemaMode as b_, InferGlobal as ba, JobConfig as bb, JsonField as bc, KeysetPage as bd, Label as be, LayoutNode as bf, LivePreview as bg, LocaleOf as bh, LocalizationConfig as bi, LogDetails as bj, Logger as bk, LoginArgs as bl, LogsConfig as bm, MediaDocument as bn, MediaSize as bo, MembersConfig as bp, MembersSignup as bq, MigrationInfo as br, NavGroup as bs, NumberField as bu, OnRequest as bv, OnRequestArgs as bw, Operation as bx, OutboxRow as by, AccessArgs as c, SearchEntry as c0, SearchHits as c1, SearchOptions as c2, SearchQuery as c3, SearchResults as c4, SearchSetting as c5, SelectField as c6, SelectOption as c7, Session as c8, SidebarPanel as c9, WebhookConfig as cB, WebhookEvent as cC, WebhookPayload as cD, Where as cE, WhereOperators as cF, SignupArgs as ca, SignupResult as cb, SlugField as cd, SlugOf as ce, SsoOutcome as cf, SsoProviderRef as cg, StorageAdapter as ch, StoredFile as ci, TelemetryConfig as cj, TextField as ck, TextareaField as cl, TypedPlugin as cn, UncountedDocs as cp, UpdateInput as cq, UpdateOptions as cr, UploadConfig as cs, UploadField as ct, UploadFromURLConfig as cu, UserIdentity as cv, Version as cx, VersionSummary as cy, VersionsConfig as cz, AccessOptions as d, AdminBrand as e, AdminCommand as f, AdminComponent as g, AdminConfig as h, AdminFieldArgs as i, AdminIcon as j, AdminLocale as k, AdminPage as l, AdminRole as m, AdminRoleChange as n, AdminRoleField as o, AdminRoleTarget as p, AdminRoleView as q, AdminRoles as r, AdminSso as s, AdminSwitcher as t, AdminViewAccess as u, AfterChangeHook as v, AfterDeleteHook as w, AfterReadHook as x, ApiKeyContext as y, ApiKeyGlobalOperation as z };
@@ -1,4 +1,4 @@
1
- import { I as ID, ac as Config, aw as EasyCMS, bi as LocalizationConfig, aF as Field, aJ as FieldError, aU as FilterOptions, ag as CreateEasyCMSOptions, bQ as ResolvedConfig } from './config-B-bT0hIn.js';
1
+ import { I as ID, ac as Config, aw as EasyCMS, bi as LocalizationConfig, aF as Field, aJ as FieldError, aU as FilterOptions, ag as CreateEasyCMSOptions, bQ as ResolvedConfig } from './config-BLxN8qcy.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-B-bT0hIn.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 CollectionSearch, ab as CollectionSlug, ac as Config, ad as ConfigError, ae as ConfigIssue, af as ContainerAdmin, ag as CreateEasyCMSOptions, ah as CreateInput, ai as CreateMigrationResult, aj as CursorDocs, ak as CustomField, al as CustomFieldTypes, am as DashboardWidget, an as Database, ao as DatabaseAdapter, ap as DatabaseInitArgs, aq as DatabaseTransfer, ar as DateField, as as DepthOptions, at as DirectUpload, au as DiskStorageOptions, av as DocumentOf, aw as EasyCMS, ax as EasyCMSError, ay as EmailAdapter, az as EmailField, aA as EmailMessage, aB as Endpoint, aC as EndpointRequest, aD as ErrorCode, aE as FIELD_TYPES, aF as Field, aG as FieldAccess, aH as FieldAccessFn, aI as FieldAdmin, aJ as FieldError, aK as FieldRule, aL as FieldType, aM as FieldTypeBase, aN as FieldTypeDefinition, aO as FieldTypeValidateContext, aP as FieldValidate, aQ as FieldValidateContext, aR as FieldValue, aS as FieldsInput, aT as FieldsValue, aU as FilterOptions, aV as FilterOptionsArgs, aW as FindAfterArgs, aX as FindArgs, aY as FindOptions, aZ as FolderLevel, a_ as FolderPermissions, a$ as ForbiddenError, b0 as GlobalAccess, b1 as GlobalConfig, b2 as GlobalDocument, b3 as GlobalHooks, b4 as GlobalInput, b5 as GlobalSlug, b6 as GroupField, b7 as ImageSize, b8 as IncrementOptions, b9 as InferCollection, ba as InferGlobal, bb as JobConfig, bc as JsonField, bd as KeysetPage, be as Label, bf as LayoutNode, bg as LivePreview, bh as LocaleOf, bi as LocalizationConfig, bj as LogDetails, bk as Logger, bl as LoginArgs, bm as LogsConfig, bn as MediaDocument, bo as MediaSize, bp as MembersConfig, bq as MembersSignup, br as MigrationInfo, bs as NavGroup, bt as NotFoundError, bu as NumberField, bv as OnRequest, bw as OnRequestArgs, bx as Operation, by as OutboxRow, bz as PLUGIN_API_VERSION, bA as PasswordEmail, bB as PasswordEmailArgs, bC as PasswordEmailFn, bD as PasswordLinkOptions, bE as PayloadTooLargeError, bF as Plugin, bG as PluginInfo, bH as PluginTypes, bI as PreviewTarget, bJ as PreviewURL, bK as QueryError, bL as ROLE_OPERATIONS, bM as RawDocument, bN as ReadOptions, bO as RelationshipField, bP as RequestContext, bQ as ResolvedConfig, bR as RichTextDocument, bS as RichTextField, bT as Role, bU as RoleOperation, bV as RolePermissions, bW as Roles, bX as RoutesConfig, bY as ScheduledPublish, bZ as SchemaError, b_ as SchemaMode, b$ as SearchAdapter, c0 as SearchEntry, c1 as SearchHits, c2 as SearchOptions, c3 as SearchQuery, c4 as SearchResults, c5 as SearchSetting, c6 as SelectField, c7 as SelectOption, c8 as Session, c9 as SidebarPanel, ca as SignupArgs, cb as SignupResult, cc as SingleSignOn, cd as SlugField, ce as SlugOf, cf as SsoOutcome, cg as SsoProviderRef, ch as StorageAdapter, ci as StoredFile, cj as TextField, ck as TextareaField, cl as TooManyRequestsError, cm as TypedPlugin, cn as UnauthorizedError, co as UncountedDocs, cp as UpdateInput, cq as UpdateOptions, cr as UploadConfig, cs as UploadField, ct as UploadFromURLConfig, cu as UserIdentity, cv as ValidationError, cw as Version, cx as VersionSummary, cy as VersionsConfig, cz as WEBHOOK_EVENTS, cA as WebhookConfig, cB as WebhookEvent, cC as WebhookPayload, cD as Where, cE as WhereOperators, cF as anyone, cG as consoleEmail, cH as consoleLogger, cI as createEasyCMS, cJ as defineConfig, cK as defineFieldType, cL as definePlugin, cM as diskStorage, cN as hasRows, cO as isAdmin, cP as isHasMany, cQ as isSignedIn, cR as isStaff, cS as isSystemAdmin, cT as jsonLogger, cU as memorySearch, cV as mimeAllowedBy, cW as rowFields, cX as silentLogger, cY as uniqueWithinOf } from './config-B-bT0hIn.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-CI6r3SK1.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-COsNEVx0.js';
1
+ import { P as PaginatedDocs, I as ID, E as EmailSetting } from './config-BLxN8qcy.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 CollectionSearch, ab as CollectionSlug, ac as Config, ad as ConfigError, ae as ConfigIssue, af as ContainerAdmin, ag as CreateEasyCMSOptions, ah as CreateInput, ai as CreateMigrationResult, aj as CursorDocs, ak as CustomField, al as CustomFieldTypes, am as DashboardWidget, an as Database, ao as DatabaseAdapter, ap as DatabaseInitArgs, aq as DatabaseTransfer, ar as DateField, as as DepthOptions, at as DirectUpload, au as DiskStorageOptions, av as DocumentOf, aw as EasyCMS, ax as EasyCMSError, ay as EmailAdapter, az as EmailField, aA as EmailMessage, aB as Endpoint, aC as EndpointRequest, aD as ErrorCode, aE as FIELD_TYPES, aF as Field, aG as FieldAccess, aH as FieldAccessFn, aI as FieldAdmin, aJ as FieldError, aK as FieldRule, aL as FieldType, aM as FieldTypeBase, aN as FieldTypeDefinition, aO as FieldTypeValidateContext, aP as FieldValidate, aQ as FieldValidateContext, aR as FieldValue, aS as FieldsInput, aT as FieldsValue, aU as FilterOptions, aV as FilterOptionsArgs, aW as FindAfterArgs, aX as FindArgs, aY as FindOptions, aZ as FolderLevel, a_ as FolderPermissions, a$ as ForbiddenError, b0 as GlobalAccess, b1 as GlobalConfig, b2 as GlobalDocument, b3 as GlobalHooks, b4 as GlobalInput, b5 as GlobalSlug, b6 as GroupField, b7 as ImageSize, b8 as IncrementOptions, b9 as InferCollection, ba as InferGlobal, bb as JobConfig, bc as JsonField, bd as KeysetPage, be as Label, bf as LayoutNode, bg as LivePreview, bh as LocaleOf, bi as LocalizationConfig, bj as LogDetails, bk as Logger, bl as LoginArgs, bm as LogsConfig, bn as MediaDocument, bo as MediaSize, bp as MembersConfig, bq as MembersSignup, br as MigrationInfo, bs as NavGroup, bt as NotFoundError, bu as NumberField, bv as OnRequest, bw as OnRequestArgs, bx as Operation, by as OutboxRow, bz as PLUGIN_API_VERSION, bA as PasswordEmail, bB as PasswordEmailArgs, bC as PasswordEmailFn, bD as PasswordLinkOptions, bE as PayloadTooLargeError, bF as Plugin, bG as PluginInfo, bH as PluginTypes, bI as PreviewTarget, bJ as PreviewURL, bK as QueryError, bL as ROLE_OPERATIONS, bM as RawDocument, bN as ReadOptions, bO as RelationshipField, bP as RequestContext, bQ as ResolvedConfig, bR as RichTextDocument, bS as RichTextField, bT as Role, bU as RoleOperation, bV as RolePermissions, bW as Roles, bX as RoutesConfig, bY as ScheduledPublish, bZ as SchemaError, b_ as SchemaMode, b$ as SearchAdapter, c0 as SearchEntry, c1 as SearchHits, c2 as SearchOptions, c3 as SearchQuery, c4 as SearchResults, c5 as SearchSetting, c6 as SelectField, c7 as SelectOption, c8 as Session, c9 as SidebarPanel, ca as SignupArgs, cb as SignupResult, cc as SingleSignOn, cd as SlugField, ce as SlugOf, cf as SsoOutcome, cg as SsoProviderRef, ch as StorageAdapter, ci as StoredFile, cj as TelemetryConfig, ck as TextField, cl as TextareaField, cm as TooManyRequestsError, cn as TypedPlugin, co as UnauthorizedError, cp as UncountedDocs, cq as UpdateInput, cr as UpdateOptions, cs as UploadConfig, ct as UploadField, cu as UploadFromURLConfig, cv as UserIdentity, cw as ValidationError, cx as Version, cy as VersionSummary, cz as VersionsConfig, cA as WEBHOOK_EVENTS, cB as WebhookConfig, cC as WebhookEvent, cD as WebhookPayload, cE as Where, cF as WhereOperators, cG as anyone, cH as consoleEmail, cI as consoleLogger, cJ as createEasyCMS, cK as defineConfig, cL as defineFieldType, cM as definePlugin, cN as diskStorage, cO as hasRows, cP as isAdmin, cQ as isHasMany, cR as isSignedIn, cS as isStaff, cT as isSystemAdmin, cU as jsonLogger, cV as memorySearch, cW as mimeAllowedBy, cX as rowFields, cY as silentLogger, cZ as uniqueWithinOf } from './config-BLxN8qcy.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-JIb70PKm.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-BjlBwaiO.js';
5
5
  export { FieldCondition, matchesCondition } from './conditions.js';
6
6
 
7
7
  type DeliveryKind = 'webhook' | 'email';
@@ -232,6 +232,8 @@ interface AdminStatus {
232
232
  email: string | null;
233
233
  /** The search adapter's name, `custom` when it has none, or `null` without `search`. */
234
234
  search: string | null;
235
+ /** `telemetry` is on: OpenTelemetry traces or metrics. */
236
+ telemetry: boolean;
235
237
  /** The config's plugins, with their name and version when they give them. */
236
238
  plugins: {
237
239
  name?: string;
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  sharedEasyCMS,
27
27
  silentLogger,
28
28
  slugify
29
- } from "./chunk-Q7MVE3BT.js";
29
+ } from "./chunk-63UEVLN6.js";
30
30
  import {
31
31
  matchesCondition
32
32
  } 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 { cZ as API_KEYS, c_ as API_KEY_GLOBAL_OPERATIONS, c$ as API_KEY_OPERATIONS, d0 as DEFAULT_MAX_VERSIONS, d1 as DEFAULT_PASSWORD_EMAILS, d2 as FIELD_TYPE_BASES, d3 as FOLDER_LEVELS, d4 as MediaFolders, d5 as SSO_COOKIE, d6 as codeOfStatus, cN as hasRows, cP as isHasMany, d7 as isJsonLogger, d8 as keyAllows, d9 as logFormat, cV as mimeAllowedBy, cW as rowFields, cY as uniqueWithinOf } from './config-B-bT0hIn.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-COsNEVx0.js';
2
+ export { c_ as API_KEYS, c$ as API_KEY_GLOBAL_OPERATIONS, d0 as API_KEY_OPERATIONS, d1 as DEFAULT_MAX_VERSIONS, d2 as DEFAULT_PASSWORD_EMAILS, d3 as FIELD_TYPE_BASES, d4 as FOLDER_LEVELS, d5 as MediaFolders, d6 as SSO_COOKIE, d7 as codeOfStatus, cO as hasRows, cQ as isHasMany, d8 as isJsonLogger, d9 as keyAllows, da as logFormat, cW as mimeAllowedBy, cX as rowFields, cZ as uniqueWithinOf } from './config-BLxN8qcy.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-BjlBwaiO.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
@@ -56,7 +56,7 @@ import {
56
56
  verifyPassword,
57
57
  verifyTurnstile,
58
58
  writeBackupFile
59
- } from "./chunk-Q7MVE3BT.js";
59
+ } from "./chunk-63UEVLN6.js";
60
60
  import {
61
61
  conditionIssues
62
62
  } from "./chunk-U266LV4M.js";
@@ -1,4 +1,4 @@
1
- import { be as Label, aF as Field, I as ID, k as AdminLocale, t as AdminSwitcher, cg as SsoProviderRef } from './config-B-bT0hIn.js';
1
+ import { be as Label, aF as Field, I as ID, k as AdminLocale, t as AdminSwitcher, cg as SsoProviderRef } from './config-BLxN8qcy.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, ac as Config, ad as ConfigError, am as DashboardWidget, ax as EasyCMSError, aB as Endpoint, aC as EndpointRequest, aD as ErrorCode, aF as Field, aG as FieldAccess, aJ as FieldError, aM as FieldTypeBase, aN as FieldTypeDefinition, aO as FieldTypeValidateContext, a$ as ForbiddenError, b1 as GlobalConfig, I as ID, bb as JobConfig, be as Label, bs as NavGroup, bt as NotFoundError, bv as OnRequest, bw as OnRequestArgs, bz as PLUGIN_API_VERSION, bE as PayloadTooLargeError, bF as Plugin, bG as PluginInfo, bH as PluginTypes, bK as QueryError, bP as RequestContext, cl as TooManyRequestsError, cm as TypedPlugin, cn as UnauthorizedError, cv as ValidationError, cD as Where, cK as defineFieldType, cL as definePlugin } from './config-B-bT0hIn.js';
2
- export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-CI6r3SK1.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, ac as Config, ad as ConfigError, am as DashboardWidget, ax as EasyCMSError, aB as Endpoint, aC as EndpointRequest, aD as ErrorCode, aF as Field, aG as FieldAccess, aJ as FieldError, aM as FieldTypeBase, aN as FieldTypeDefinition, aO as FieldTypeValidateContext, a$ as ForbiddenError, b1 as GlobalConfig, I as ID, bb as JobConfig, be as Label, bs as NavGroup, bt as NotFoundError, bv as OnRequest, bw as OnRequestArgs, bz as PLUGIN_API_VERSION, bE as PayloadTooLargeError, bF as Plugin, bG as PluginInfo, bH as PluginTypes, bK as QueryError, bP as RequestContext, cm as TooManyRequestsError, cn as TypedPlugin, co as UnauthorizedError, cw as ValidationError, cE as Where, cL as defineFieldType, cM as definePlugin } from './config-BLxN8qcy.js';
2
+ export { A as ADMIN_API_VERSION, c as AdminElementProps, k as AdminPageRoute, S as SetFieldDetail, w as warnDeprecated } from './plugin-JIb70PKm.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.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Embedded, code-first headless CMS for Nuxt and Next.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,14 +54,19 @@
54
54
  "react"
55
55
  ],
56
56
  "peerDependencies": {
57
- "sharp": ">=0.33.0"
57
+ "sharp": ">=0.33.0",
58
+ "@opentelemetry/api": ">=1.9.0 <2"
58
59
  },
59
60
  "peerDependenciesMeta": {
60
61
  "sharp": {
61
62
  "optional": true
63
+ },
64
+ "@opentelemetry/api": {
65
+ "optional": true
62
66
  }
63
67
  },
64
68
  "devDependencies": {
69
+ "@opentelemetry/api": "~1.9.1",
65
70
  "sharp": "^0.35.5"
66
71
  },
67
72
  "scripts": {