@dyrected/core 2.7.0 → 2.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.
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,13 +30,526 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/observability.ts
34
+ var observability_exports = {};
35
+ __export(observability_exports, {
36
+ attachRequestMetrics: () => attachRequestMetrics,
37
+ bindObservabilityRuntime: () => bindObservabilityRuntime,
38
+ buildRequestLogger: () => buildRequestLogger,
39
+ captureRequestBody: () => captureRequestBody,
40
+ createObservabilityRuntime: () => createObservabilityRuntime,
41
+ createRequestSpan: () => createRequestSpan,
42
+ endRequestSpan: () => endRequestSpan,
43
+ getConfigLogger: () => getConfigLogger,
44
+ getObservabilityRuntime: () => getObservabilityRuntime,
45
+ getRequestLogger: () => getRequestLogger,
46
+ redactBody: () => redactBody,
47
+ redactHeaders: () => redactHeaders,
48
+ renderPrometheusMetrics: () => renderPrometheusMetrics,
49
+ resolveObservabilityConfig: () => resolveObservabilityConfig,
50
+ shouldSampleRequest: () => shouldSampleRequest
51
+ });
52
+ function resolveObservabilityConfig(config) {
53
+ const requestLogging = config?.requestLogging;
54
+ const sampling = config?.sampling;
55
+ const tracing = config?.tracing;
56
+ const metrics = config?.metrics;
57
+ return {
58
+ requestLogging: {
59
+ enabled: requestLogging?.enabled ?? true,
60
+ logBodies: requestLogging?.logBodies ?? false,
61
+ maxBodyBytes: requestLogging?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
62
+ redactPaths: uniqueValues([
63
+ ...DEFAULT_REDACT_PATHS,
64
+ ...requestLogging?.redactPaths ?? []
65
+ ]),
66
+ includeHeaders: normalizeHeaderNames(requestLogging?.includeHeaders ?? []),
67
+ redactHeaders: uniqueValues(
68
+ normalizeHeaderNames([
69
+ ...DEFAULT_REDACT_HEADERS,
70
+ ...requestLogging?.redactHeaders ?? []
71
+ ])
72
+ )
73
+ },
74
+ sampling: {
75
+ successRate: clampRate(sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE),
76
+ traceSuccessRate: clampRate(
77
+ sampling?.traceSuccessRate ?? sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE
78
+ ),
79
+ bodySuccessRate: clampRate(
80
+ sampling?.bodySuccessRate ?? Math.min(DEFAULT_BODY_SAMPLE_RATE, sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE)
81
+ ),
82
+ alwaysKeep4xx: sampling?.alwaysKeep4xx ?? true,
83
+ alwaysKeep5xx: sampling?.alwaysKeep5xx ?? true
84
+ },
85
+ tracing: {
86
+ enabled: tracing?.enabled ?? false,
87
+ serviceName: tracing?.serviceName ?? "dyrected",
88
+ exporter: tracing?.exporter ?? "none",
89
+ headers: tracing?.headers ?? {},
90
+ endpoint: tracing?.endpoint
91
+ },
92
+ metrics: {
93
+ enabled: metrics?.enabled ?? false,
94
+ exporter: metrics?.exporter ?? "none",
95
+ endpoint: metrics?.endpoint,
96
+ path: metrics?.path ?? "/metrics"
97
+ },
98
+ transports: {
99
+ targets: config?.transports?.targets && config.transports.targets.length > 0 ? config.transports.targets : [{ type: "stdout" }]
100
+ }
101
+ };
102
+ }
103
+ function createObservabilityRuntime(config) {
104
+ const resolved = resolveObservabilityConfig(config.observability);
105
+ const logger = createRootLogger(config.logger, resolved);
106
+ const shutdownTasks = [];
107
+ const meterRuntime = resolved.metrics.enabled || resolved.metrics.exporter === "prometheus" ? createMeterProvider(resolved) : void 0;
108
+ const tracerProvider = resolved.tracing.enabled ? createTracerProvider(resolved) : void 0;
109
+ const tracer = tracerProvider?.getTracer("dyrected");
110
+ const meter = meterRuntime?.provider.getMeter("dyrected");
111
+ const metrics = meter ? {
112
+ requestCount: meter.createCounter("dyrected_http_requests_total", {
113
+ description: "Total HTTP requests served by Dyrected"
114
+ }),
115
+ authFailureCount: meter.createCounter("dyrected_auth_failures_total", {
116
+ description: "Authentication failures"
117
+ }),
118
+ uncaughtErrorCount: meter.createCounter("dyrected_uncaught_errors_total", {
119
+ description: "Uncaught application errors"
120
+ }),
121
+ auditWriteFailureCount: meter.createCounter("dyrected_audit_write_failures_total", {
122
+ description: "Audit log persistence failures"
123
+ }),
124
+ emailSendFailureCount: meter.createCounter("dyrected_email_send_failures_total", {
125
+ description: "Transactional email delivery failures"
126
+ }),
127
+ workflowHookFailureCount: meter.createCounter("dyrected_workflow_hook_failures_total", {
128
+ description: "Workflow hook failures isolated after successful writes"
129
+ }),
130
+ requestDuration: meter.createHistogram("dyrected_http_request_duration_ms", {
131
+ description: "HTTP request duration in milliseconds",
132
+ unit: "ms"
133
+ })
134
+ } : void 0;
135
+ if (meterRuntime) {
136
+ shutdownTasks.push(() => meterRuntime.provider.shutdown());
137
+ }
138
+ if (tracerProvider) {
139
+ shutdownTasks.push(() => tracerProvider.shutdown());
140
+ }
141
+ return {
142
+ logger,
143
+ config: resolved,
144
+ tracer,
145
+ metrics,
146
+ prometheusExporter: meterRuntime?.prometheusExporter,
147
+ shutdown: async () => {
148
+ for (const task of shutdownTasks) {
149
+ await task();
150
+ }
151
+ },
152
+ recordAuthFailure: (attributes) => metrics?.authFailureCount.add(1, attributes),
153
+ recordUncaughtError: (attributes) => metrics?.uncaughtErrorCount.add(1, attributes),
154
+ recordAuditWriteFailure: (attributes) => metrics?.auditWriteFailureCount.add(1, attributes),
155
+ recordEmailSendFailure: (attributes) => metrics?.emailSendFailureCount.add(1, attributes),
156
+ recordWorkflowHookFailure: (attributes) => metrics?.workflowHookFailureCount.add(1, attributes)
157
+ };
158
+ }
159
+ function bindObservabilityRuntime(config, runtime) {
160
+ runtimeByConfig.set(config, runtime);
161
+ }
162
+ function getObservabilityRuntime(config) {
163
+ if (!config || typeof config !== "object") return void 0;
164
+ return runtimeByConfig.get(config);
165
+ }
166
+ function createRootLogger(loggerConfig, observability) {
167
+ if (loggerConfig && !("options" in loggerConfig)) {
168
+ return loggerConfig;
169
+ }
170
+ const options = loggerConfig?.options ?? {};
171
+ const enabled = options.enabled ?? process.env.DISABLE_LOGGING !== "true";
172
+ const name = options.name ?? "dyrected";
173
+ if (loggerConfig?.destination) {
174
+ return (0, import_pino.default)(
175
+ {
176
+ ...options,
177
+ enabled,
178
+ name,
179
+ timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
180
+ },
181
+ loggerConfig.destination
182
+ );
183
+ }
184
+ const streams = buildTransportStreams(observability);
185
+ const destination = streams.length === 1 ? streams[0] : (0, import_pino.multistream)(streams.map((stream) => ({ stream })));
186
+ return (0, import_pino.default)(
187
+ {
188
+ ...options,
189
+ enabled,
190
+ name,
191
+ timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
192
+ },
193
+ destination
194
+ );
195
+ }
196
+ function buildTransportStreams(observability) {
197
+ const targets = observability.transports.targets;
198
+ if (targets.length === 0) {
199
+ return [defaultLoggerDestination()];
200
+ }
201
+ return targets.map((target) => {
202
+ switch (target.type) {
203
+ case "stdout":
204
+ return defaultLoggerDestination();
205
+ case "stderr":
206
+ return (0, import_pino.destination)(2);
207
+ case "file":
208
+ return (0, import_pino.destination)(target.path);
209
+ case "otlp":
210
+ return new OtlpLogWritable(target.endpoint, target.headers);
211
+ }
212
+ });
213
+ }
214
+ function defaultLoggerDestination() {
215
+ if (process.env.NODE_ENV !== "production" && typeof window === "undefined") {
216
+ try {
217
+ const req = globalThis.require;
218
+ if (req) {
219
+ const pinoPretty = req("pino-pretty");
220
+ return pinoPretty({
221
+ colorize: true,
222
+ ignore: "pid,hostname",
223
+ translateTime: "SYS:HH:MM:ss",
224
+ destination: 1,
225
+ sync: true
226
+ });
227
+ }
228
+ } catch {
229
+ }
230
+ }
231
+ return (0, import_pino.destination)(1);
232
+ }
233
+ function createTracerProvider(observability) {
234
+ const exporter = createTraceExporter(observability);
235
+ const provider = new import_sdk_trace_base.BasicTracerProvider({
236
+ resource: (0, import_resources.resourceFromAttributes)({
237
+ "service.name": observability.tracing.serviceName
238
+ }),
239
+ spanProcessors: exporter ? [new import_sdk_trace_base.BatchSpanProcessor(exporter)] : []
240
+ });
241
+ import_api.trace.setGlobalTracerProvider(provider);
242
+ return provider;
243
+ }
244
+ function createTraceExporter(observability) {
245
+ switch (observability.tracing.exporter) {
246
+ case "console":
247
+ return new import_sdk_trace_base.ConsoleSpanExporter();
248
+ case "otlp":
249
+ return new import_exporter_trace_otlp_http.OTLPTraceExporter({
250
+ url: observability.tracing.endpoint,
251
+ headers: observability.tracing.headers
252
+ });
253
+ default:
254
+ return void 0;
255
+ }
256
+ }
257
+ function createMeterProvider(observability) {
258
+ const readers = [];
259
+ let prometheusExporter;
260
+ switch (observability.metrics.exporter) {
261
+ case "otlp":
262
+ readers.push(
263
+ new import_sdk_metrics.PeriodicExportingMetricReader({
264
+ exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
265
+ url: observability.metrics.endpoint
266
+ })
267
+ })
268
+ );
269
+ break;
270
+ case "prometheus":
271
+ prometheusExporter = new import_exporter_prometheus.PrometheusExporter({
272
+ endpoint: observability.metrics.path,
273
+ preventServerStart: true
274
+ });
275
+ readers.push(prometheusExporter);
276
+ break;
277
+ }
278
+ const provider = new import_sdk_metrics.MeterProvider({
279
+ resource: (0, import_resources.resourceFromAttributes)({
280
+ "service.name": observability.tracing.serviceName
281
+ }),
282
+ readers
283
+ });
284
+ return { provider, prometheusExporter };
285
+ }
286
+ function getConfigLogger(config, component) {
287
+ const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
288
+ return base.child({ component });
289
+ }
290
+ function getRequestLogger(c, component) {
291
+ const base = c.get("logger") ?? fallbackLogger;
292
+ return component ? base.child({ component }) : base;
293
+ }
294
+ function buildRequestLogger(runtime, vars, requestId) {
295
+ const trace2 = vars.requestTrace;
296
+ return runtime.logger.child({
297
+ requestId,
298
+ siteId: vars.siteId,
299
+ workspaceId: vars.workspaceId,
300
+ traceId: trace2?.traceId,
301
+ spanId: trace2?.spanId
302
+ });
303
+ }
304
+ function shouldSampleRequest(requestId, statusCode, sampling, kind) {
305
+ if (statusCode >= 500 && sampling.alwaysKeep5xx) return true;
306
+ if (statusCode >= 400 && sampling.alwaysKeep4xx) return true;
307
+ const rate = kind === "trace" ? sampling.traceSuccessRate : kind === "body" ? sampling.bodySuccessRate : sampling.successRate;
308
+ return deterministicSample(requestId ?? "no-request-id", rate);
309
+ }
310
+ function redactHeaders(headers, observability) {
311
+ const includeHeaders = new Set(observability.requestLogging.includeHeaders);
312
+ const redactHeaders2 = new Set(observability.requestLogging.redactHeaders);
313
+ const output = {};
314
+ for (const [key, value] of Object.entries(headers)) {
315
+ const normalizedKey = key.toLowerCase();
316
+ if (includeHeaders.size > 0 && !includeHeaders.has(normalizedKey)) continue;
317
+ output[normalizedKey] = redactHeaders2.has(normalizedKey) ? REDACTED_VALUE : value;
318
+ }
319
+ return output;
320
+ }
321
+ async function captureRequestBody(request, observability) {
322
+ const contentType = request.headers.get("content-type") ?? void 0;
323
+ const contentLength = toNumber(request.headers.get("content-length"));
324
+ const jsonBody = contentType?.includes("application/json");
325
+ if (!jsonBody) {
326
+ return {
327
+ attempted: false,
328
+ contentType,
329
+ contentLength
330
+ };
331
+ }
332
+ try {
333
+ const text = await request.clone().text();
334
+ if (!text) {
335
+ return { attempted: true, contentType, contentLength };
336
+ }
337
+ const truncated = text.length > observability.requestLogging.maxBodyBytes;
338
+ const raw = truncated ? text.slice(0, observability.requestLogging.maxBodyBytes) : text;
339
+ try {
340
+ const parsed = JSON.parse(raw);
341
+ return {
342
+ attempted: true,
343
+ contentType,
344
+ contentLength: contentLength ?? text.length,
345
+ truncated,
346
+ body: redactBody(parsed, observability.requestLogging.redactPaths)
347
+ };
348
+ } catch {
349
+ return {
350
+ attempted: true,
351
+ contentType,
352
+ contentLength: contentLength ?? text.length,
353
+ truncated,
354
+ parseFailed: true
355
+ };
356
+ }
357
+ } catch {
358
+ return {
359
+ attempted: true,
360
+ contentType,
361
+ contentLength,
362
+ parseFailed: true
363
+ };
364
+ }
365
+ }
366
+ function redactBody(value, redactPaths) {
367
+ const cloned = deepClone(value);
368
+ for (const path of redactPaths) {
369
+ applyRedaction(cloned, path.split("."));
370
+ }
371
+ return cloned;
372
+ }
373
+ function applyRedaction(target, segments) {
374
+ if (!target || segments.length === 0) return;
375
+ const [segment, ...rest] = segments;
376
+ if (Array.isArray(target)) {
377
+ for (const item of target) {
378
+ if (segment === "*") {
379
+ applyRedaction(item, rest);
380
+ } else {
381
+ applyRedaction(item, segments);
382
+ }
383
+ }
384
+ return;
385
+ }
386
+ if (typeof target !== "object") return;
387
+ const record = target;
388
+ if (segment === "*") {
389
+ for (const value of Object.values(record)) {
390
+ applyRedaction(value, rest);
391
+ }
392
+ return;
393
+ }
394
+ if (!(segment in record)) return;
395
+ if (rest.length === 0) {
396
+ record[segment] = REDACTED_VALUE;
397
+ return;
398
+ }
399
+ applyRedaction(record[segment], rest);
400
+ }
401
+ function deepClone(value) {
402
+ if (typeof structuredClone === "function") {
403
+ return structuredClone(value);
404
+ }
405
+ return JSON.parse(JSON.stringify(value));
406
+ }
407
+ function createRequestSpan(runtime, name, attributes) {
408
+ if (!runtime.tracer) return void 0;
409
+ const span = runtime.tracer.startSpan(name, { attributes }, import_api.context.active());
410
+ const spanContext = span.spanContext();
411
+ return {
412
+ span,
413
+ traceId: spanContext.traceId,
414
+ spanId: spanContext.spanId,
415
+ sampled: spanContext.traceFlags === import_api.TraceFlags.SAMPLED
416
+ };
417
+ }
418
+ function endRequestSpan(requestTrace, statusCode, error) {
419
+ if (!requestTrace) return;
420
+ requestTrace.span.setAttribute("http.status_code", statusCode);
421
+ if (error) {
422
+ requestTrace.span.recordException(error instanceof Error ? error : new Error(String(error)));
423
+ }
424
+ requestTrace.span.end();
425
+ }
426
+ function attachRequestMetrics(runtime, args) {
427
+ runtime.metrics?.requestCount.add(1, {
428
+ method: args.method,
429
+ route: args.route,
430
+ status_class: `${Math.floor(args.statusCode / 100)}xx`
431
+ });
432
+ runtime.metrics?.requestDuration.record(args.durationMs, {
433
+ method: args.method,
434
+ route: args.route,
435
+ status_class: `${Math.floor(args.statusCode / 100)}xx`
436
+ });
437
+ }
438
+ async function renderPrometheusMetrics(exporter) {
439
+ let statusCode = 200;
440
+ const headers = {};
441
+ let body = "";
442
+ await new Promise((resolve) => {
443
+ const response = {
444
+ statusCode,
445
+ setHeader(name, value) {
446
+ headers[name.toLowerCase()] = value;
447
+ },
448
+ end(chunk) {
449
+ if (chunk) body += chunk;
450
+ statusCode = this.statusCode;
451
+ resolve();
452
+ }
453
+ };
454
+ exporter.getMetricsRequestHandler({}, response);
455
+ });
456
+ return { body, headers, statusCode };
457
+ }
458
+ function normalizeHeaderNames(headers) {
459
+ return headers.map((value) => value.toLowerCase());
460
+ }
461
+ function uniqueValues(values) {
462
+ return [...new Set(values)];
463
+ }
464
+ function clampRate(value) {
465
+ if (Number.isNaN(value)) return 0;
466
+ return Math.max(0, Math.min(1, value));
467
+ }
468
+ function deterministicSample(seed, rate) {
469
+ if (rate >= 1) return true;
470
+ if (rate <= 0) return false;
471
+ let hash = 0;
472
+ for (let index = 0; index < seed.length; index += 1) {
473
+ hash = hash * 31 + seed.charCodeAt(index) >>> 0;
474
+ }
475
+ return hash / 4294967295 <= rate;
476
+ }
477
+ function toNumber(value) {
478
+ if (!value) return void 0;
479
+ const parsed = Number(value);
480
+ return Number.isFinite(parsed) ? parsed : void 0;
481
+ }
482
+ var import_api, import_exporter_metrics_otlp_http, import_exporter_prometheus, import_exporter_trace_otlp_http, import_resources, import_sdk_metrics, import_sdk_trace_base, import_pino, import_node_stream, DEFAULT_REDACT_HEADERS, DEFAULT_REDACT_PATHS, DEFAULT_MAX_BODY_BYTES, DEFAULT_SUCCESS_SAMPLE_RATE, DEFAULT_BODY_SAMPLE_RATE, REDACTED_VALUE, runtimeByConfig, fallbackLogger, OtlpLogWritable;
483
+ var init_observability = __esm({
484
+ "src/observability.ts"() {
485
+ "use strict";
486
+ import_api = require("@opentelemetry/api");
487
+ import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
488
+ import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
489
+ import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
490
+ import_resources = require("@opentelemetry/resources");
491
+ import_sdk_metrics = require("@opentelemetry/sdk-metrics");
492
+ import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
493
+ import_pino = __toESM(require("pino"), 1);
494
+ import_node_stream = require("stream");
495
+ DEFAULT_REDACT_HEADERS = ["authorization", "cookie", "set-cookie", "x-api-key"];
496
+ DEFAULT_REDACT_PATHS = [
497
+ "password",
498
+ "currentPassword",
499
+ "newPassword",
500
+ "confirmPassword",
501
+ "token",
502
+ "refreshToken",
503
+ "accessToken",
504
+ "secret",
505
+ "apiKey",
506
+ "inviteToken",
507
+ "resetToken"
508
+ ];
509
+ DEFAULT_MAX_BODY_BYTES = 8192;
510
+ DEFAULT_SUCCESS_SAMPLE_RATE = 0.1;
511
+ DEFAULT_BODY_SAMPLE_RATE = 0.02;
512
+ REDACTED_VALUE = "[REDACTED]";
513
+ runtimeByConfig = /* @__PURE__ */ new WeakMap();
514
+ fallbackLogger = (0, import_pino.default)({
515
+ name: "dyrected",
516
+ enabled: process.env.DISABLE_LOGGING !== "true",
517
+ timestamp: import_pino.stdTimeFunctions.isoTime
518
+ });
519
+ OtlpLogWritable = class extends import_node_stream.Writable {
520
+ constructor(endpoint, headers = {}) {
521
+ super();
522
+ this.endpoint = endpoint;
523
+ this.headers = headers;
524
+ }
525
+ endpoint;
526
+ headers;
527
+ _write(chunk, _encoding, callback) {
528
+ const payload = chunk.toString();
529
+ void fetch(this.endpoint, {
530
+ method: "POST",
531
+ headers: {
532
+ "content-type": "application/json",
533
+ ...this.headers
534
+ },
535
+ body: JSON.stringify({ logs: payload.trim().split("\n").filter(Boolean) })
536
+ }).then(() => callback()).catch(
537
+ (error) => callback(error instanceof Error ? error : new Error(String(error)))
538
+ );
539
+ }
540
+ };
541
+ }
542
+ });
543
+
30
544
  // src/index.ts
31
545
  var index_exports = {};
32
546
  __export(index_exports, {
547
+ BUILTIN_JEXL_HELPERS: () => BUILTIN_JEXL_HELPERS,
33
548
  ConfigValidationError: () => ConfigValidationError,
34
549
  LIFECYCLE_EVENTS_COLLECTION: () => LIFECYCLE_EVENTS_COLLECTION,
35
550
  LIFECYCLE_EVENT_NAMES: () => LIFECYCLE_EVENT_NAMES,
36
551
  WORKFLOW_HISTORY_COLLECTION: () => WORKFLOW_HISTORY_COLLECTION,
552
+ addDays: () => addDays,
37
553
  assertValidAdminConditionsInConfig: () => assertValidAdminConditionsInConfig,
38
554
  assertValidDeclarativeAccessInConfig: () => assertValidDeclarativeAccessInConfig,
39
555
  assertValidDeclarativeHookExpression: () => assertValidDeclarativeHookExpression,
@@ -43,13 +559,18 @@ __export(index_exports, {
43
559
  availableWorkflowTransitions: () => availableWorkflowTransitions,
44
560
  buildSqlOrderBy: () => buildSqlOrderBy,
45
561
  canViewWorkflowDraft: () => canViewWorkflowDraft,
562
+ capitalize: () => capitalize,
563
+ clamp: () => clamp,
564
+ coalesce: () => coalesce,
46
565
  collectAdminConditionValidationIssues: () => collectAdminConditionValidationIssues,
47
566
  collectConfigDiagnostics: () => collectConfigDiagnostics,
48
567
  collectDeclarativeAccessValidationIssues: () => collectDeclarativeAccessValidationIssues,
49
568
  collectDeclarativeHookValidationIssues: () => collectDeclarativeHookValidationIssues,
50
569
  collectPreviewUrlValidationIssues: () => collectPreviewUrlValidationIssues,
570
+ compact: () => compact,
51
571
  createLifecycleEvent: () => createLifecycleEvent,
52
572
  createWorkflowDocument: () => createWorkflowDocument,
573
+ defaultValue: () => defaultValue,
53
574
  defineArrayField: () => defineArrayField,
54
575
  defineBlock: () => defineBlock,
55
576
  defineBlocksField: () => defineBlocksField,
@@ -79,33 +600,59 @@ __export(index_exports, {
79
600
  defineTextareaField: () => defineTextareaField,
80
601
  defineTimeField: () => defineTimeField,
81
602
  defineUrlField: () => defineUrlField,
603
+ diffDays: () => diffDays,
82
604
  dispatchLifecycleEvent: () => dispatchLifecycleEvent,
83
605
  dispatchPendingLifecycleEvents: () => dispatchPendingLifecycleEvents,
606
+ endsWith: () => endsWith,
84
607
  executeFieldAfterRead: () => executeFieldAfterRead,
85
608
  executeFieldBeforeChange: () => executeFieldBeforeChange,
609
+ first: () => first,
86
610
  formatConfigDiagnostics: () => formatConfigDiagnostics,
611
+ formatDate: () => formatDate,
87
612
  generateOpenApi: () => generateOpenApi,
613
+ get: () => get,
88
614
  getAdminAuthCollection: () => getAdminAuthCollection,
89
615
  getPublicAdminAuthConfig: () => getPublicAdminAuthConfig,
616
+ includes: () => includes,
90
617
  initializeWorkflowDocument: () => initializeWorkflowDocument,
91
618
  isConfigValidationError: () => isConfigValidationError,
619
+ isEmpty: () => isEmpty,
620
+ isFuture: () => isFuture,
621
+ isPast: () => isPast,
622
+ join: () => join,
623
+ last: () => last,
624
+ length: () => length,
625
+ lower: () => lower,
92
626
  materializeWorkflowDocument: () => materializeWorkflowDocument,
93
627
  normalizeConfig: () => normalizeConfig,
628
+ now: () => now,
94
629
  numericFieldNames: () => numericFieldNames,
95
630
  parseMongoWhere: () => parseMongoWhere,
96
631
  parseSort: () => parseSort,
97
632
  parseSqlWhere: () => parseSqlWhere,
98
633
  publishedStateName: () => publishedStateName,
99
634
  publishingWorkflow: () => publishingWorkflow,
635
+ readingTime: () => readingTime,
636
+ registerJexlHelpers: () => registerJexlHelpers,
637
+ replace: () => replace,
100
638
  resolveAdminAuthCollection: () => resolveAdminAuthCollection,
639
+ round: () => round,
101
640
  runCollectionHooks: () => runCollectionHooks,
102
641
  saveWorkflowDraft: () => saveWorkflowDraft,
103
642
  simplePublishingWorkflow: () => simplePublishingWorkflow,
643
+ slugify: () => slugify,
644
+ startsWith: () => startsWith,
645
+ today: () => today,
104
646
  transitionWorkflow: () => transitionWorkflow,
647
+ trim: () => trim,
648
+ truncate: () => truncate,
649
+ unique: () => unique,
650
+ upper: () => upper,
105
651
  validateAdminConditionExpression: () => validateAdminConditionExpression,
106
652
  validateDeclarativeAccessExpression: () => validateDeclarativeAccessExpression,
107
653
  validateDeclarativeHookExpression: () => validateDeclarativeHookExpression,
108
654
  validatePreviewUrlExpression: () => validatePreviewUrlExpression,
655
+ wordCount: () => wordCount,
109
656
  workflowCapabilities: () => workflowCapabilities
110
657
  });
111
658
  module.exports = __toCommonJS(index_exports);
@@ -120,37 +667,10 @@ var LIFECYCLE_EVENT_NAMES = [
120
667
 
121
668
  // src/auth/token.ts
122
669
  var import_jose = require("jose");
123
- var import_node_util = require("util");
124
670
 
125
671
  // src/auth/sessions.ts
126
672
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
127
673
 
128
- // src/observability.ts
129
- var import_api = require("@opentelemetry/api");
130
- var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
131
- var import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
132
- var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
133
- var import_resources = require("@opentelemetry/resources");
134
- var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
135
- var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
136
- var import_pino = __toESM(require("pino"), 1);
137
- var import_pino_pretty = __toESM(require("pino-pretty"), 1);
138
- var import_node_stream = require("stream");
139
- var runtimeByConfig = /* @__PURE__ */ new WeakMap();
140
- var fallbackLogger = (0, import_pino.default)({
141
- name: "dyrected",
142
- enabled: process.env.DISABLE_LOGGING !== "true",
143
- timestamp: import_pino.stdTimeFunctions.isoTime
144
- });
145
- function getObservabilityRuntime(config) {
146
- if (!config || typeof config !== "object") return void 0;
147
- return runtimeByConfig.get(config);
148
- }
149
- function getConfigLogger(config, component) {
150
- const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
151
- return base.child({ component });
152
- }
153
-
154
674
  // src/workflows.ts
155
675
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
156
676
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -168,10 +688,36 @@ function definePublishingWorkflow(options = {}) {
168
688
  { name: "published", label: "Published", color: "success", published: true }
169
689
  ],
170
690
  transitions: [
171
- { name: "submit", label: "Submit for review", from: "draft", to: "in_review", requiredCapabilities: ["entry.submit"] },
172
- { name: "publish", label: "Publish", from: "in_review", to: "published", requiredCapabilities: ["entry.publish"] },
173
- { name: "reject", label: "Request changes", from: "in_review", to: "draft", requiredCapabilities: ["entry.publish"], requireComment: true },
174
- { name: "unpublish", label: "Unpublish", from: "published", to: "draft", requiredCapabilities: ["entry.unpublish"], unpublish: true }
691
+ {
692
+ name: "submit",
693
+ label: "Submit for review",
694
+ from: "draft",
695
+ to: "in_review",
696
+ requiredCapabilities: ["entry.submit"]
697
+ },
698
+ {
699
+ name: "publish",
700
+ label: "Publish",
701
+ from: "in_review",
702
+ to: "published",
703
+ requiredCapabilities: ["entry.publish"]
704
+ },
705
+ {
706
+ name: "reject",
707
+ label: "Request changes",
708
+ from: "in_review",
709
+ to: "draft",
710
+ requiredCapabilities: ["entry.publish"],
711
+ requireComment: true
712
+ },
713
+ {
714
+ name: "unpublish",
715
+ label: "Unpublish",
716
+ from: "published",
717
+ to: "draft",
718
+ requiredCapabilities: ["entry.unpublish"],
719
+ unpublish: true
720
+ }
175
721
  ],
176
722
  roles: [
177
723
  ...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
@@ -191,7 +737,7 @@ function simplePublishingWorkflow() {
191
737
  { name: "published", label: "Published", color: "success", published: true }
192
738
  ],
193
739
  transitions: [
194
- { name: "publish", label: "Publish", from: "draft", to: "published" },
740
+ { name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
195
741
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
196
742
  ]
197
743
  // Intentionally no `roles`: this lightweight workflow (synthesized from
@@ -298,7 +844,11 @@ async function dispatchLifecycleEvent(config, event) {
298
844
  const retryDelayMs = config.events.retryDelayMs ?? 1e3;
299
845
  const attempts = event.attempts + 1;
300
846
  try {
301
- await db.update({ collection: LIFECYCLE_EVENTS_COLLECTION, id: event.id, data: { status: "processing", attempts } });
847
+ await db.update({
848
+ collection: LIFECYCLE_EVENTS_COLLECTION,
849
+ id: event.id,
850
+ data: { status: "processing", attempts }
851
+ });
302
852
  for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
303
853
  await db.update({
304
854
  collection: LIFECYCLE_EVENTS_COLLECTION,
@@ -328,9 +878,9 @@ async function dispatchPendingLifecycleEvents(config, limit = 50) {
328
878
  sort: "occurredAt",
329
879
  limit
330
880
  });
331
- const now = Date.now();
881
+ const now2 = Date.now();
332
882
  const due = result.docs.filter(
333
- (doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now)
883
+ (doc) => Number(doc.attempts ?? 0) < maxAttempts && (!doc.nextAttemptAt || new Date(doc.nextAttemptAt).getTime() <= now2)
334
884
  );
335
885
  for (const event of due) await dispatchLifecycleEvent(config, event);
336
886
  return due.length;
@@ -405,12 +955,16 @@ async function transitionWorkflow(args) {
405
955
  const meta = original.__workflow;
406
956
  if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
407
957
  if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
408
- throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
958
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
959
+ statusCode: 409
960
+ });
409
961
  }
410
962
  const transition = workflow.transitions.find((item) => item.name === transitionName);
411
963
  const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
412
964
  if (!transition || !fromStates.includes(meta.state)) {
413
- throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), { statusCode: 409 });
965
+ throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
966
+ statusCode: 409
967
+ });
414
968
  }
415
969
  if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
416
970
  throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
@@ -420,32 +974,52 @@ async function transitionWorkflow(args) {
420
974
  }
421
975
  const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
422
976
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
423
- const events = [createLifecycleEvent({
424
- name: "workflow.transitioned",
425
- collection: collection.slug,
426
- documentId: id,
427
- actorId: user?.sub,
428
- payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
429
- })];
977
+ const events = [
978
+ createLifecycleEvent({
979
+ name: "workflow.transitioned",
980
+ collection: collection.slug,
981
+ documentId: id,
982
+ actorId: user?.sub,
983
+ payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
984
+ })
985
+ ];
430
986
  const targetState = workflow.states.find((state) => state.name === transition.to);
431
987
  if (targetState.published) {
432
- events.push(createLifecycleEvent({ name: "entry.published", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.revision } }));
988
+ events.push(
989
+ createLifecycleEvent({
990
+ name: "entry.published",
991
+ collection: collection.slug,
992
+ documentId: id,
993
+ actorId: user?.sub,
994
+ payload: { revision: meta.revision }
995
+ })
996
+ );
433
997
  } else if (transition.unpublish) {
434
- events.push(createLifecycleEvent({ name: "entry.unpublished", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.publishedRevision } }));
998
+ events.push(
999
+ createLifecycleEvent({
1000
+ name: "entry.unpublished",
1001
+ collection: collection.slug,
1002
+ documentId: id,
1003
+ actorId: user?.sub,
1004
+ payload: { revision: meta.publishedRevision }
1005
+ })
1006
+ );
435
1007
  }
436
1008
  const updated = await db.transaction(async (tx) => {
437
1009
  const locked = await tx.findOne({ collection: collection.slug, id });
438
1010
  if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
439
1011
  const lockedMeta = locked.__workflow;
440
1012
  if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
441
- throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
1013
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
1014
+ statusCode: 409
1015
+ });
442
1016
  }
443
- const now = (/* @__PURE__ */ new Date()).toISOString();
1017
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
444
1018
  const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
445
1019
  const nextMeta = {
446
1020
  ...lockedMeta,
447
1021
  state: transition.to,
448
- ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now, publishedBy: user?.sub } : {},
1022
+ ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
449
1023
  ...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
450
1024
  };
451
1025
  const data = { __workflow: nextMeta };
@@ -454,11 +1028,36 @@ async function transitionWorkflow(args) {
454
1028
  const next = await tx.update({ collection: collection.slug, id, data });
455
1029
  await tx.create({
456
1030
  collection: WORKFLOW_HISTORY_COLLECTION,
457
- data: { collection: collection.slug, documentId: id, transition: transition.name, from: lockedMeta.state, to: transition.to, revision: lockedMeta.revision, comment: comment ?? null, actorId: user?.sub ?? null, createdAt: now }
1031
+ data: {
1032
+ collection: collection.slug,
1033
+ documentId: id,
1034
+ transition: transition.name,
1035
+ from: lockedMeta.state,
1036
+ to: transition.to,
1037
+ revision: lockedMeta.revision,
1038
+ comment: comment ?? null,
1039
+ actorId: user?.sub ?? null,
1040
+ createdAt: now2
1041
+ }
458
1042
  });
459
1043
  for (const event of events) await persistEvent(tx, event);
460
1044
  if (collection.audit) {
461
- await tx.create({ collection: "__audit", data: { collection: collection.slug, documentId: id, operation: "workflow.transition", user: user?.sub ?? null, timestamp: now, changes: JSON.stringify({ transition: transition.name, from: lockedMeta.state, to: transition.to, revision: lockedMeta.revision }) } });
1045
+ await tx.create({
1046
+ collection: "__audit",
1047
+ data: {
1048
+ collection: collection.slug,
1049
+ documentId: id,
1050
+ operation: "workflow.transition",
1051
+ user: user?.sub ?? null,
1052
+ timestamp: now2,
1053
+ changes: JSON.stringify({
1054
+ transition: transition.name,
1055
+ from: lockedMeta.state,
1056
+ to: transition.to,
1057
+ revision: lockedMeta.revision
1058
+ })
1059
+ }
1060
+ });
462
1061
  }
463
1062
  return next;
464
1063
  });
@@ -467,17 +1066,22 @@ async function transitionWorkflow(args) {
467
1066
  try {
468
1067
  await hook({ ...hookContext, doc: updated, event: events[0] });
469
1068
  } catch (error) {
470
- getObservabilityRuntime(config)?.recordWorkflowHookFailure({
471
- collection: collection.slug,
472
- transition: transition.name
473
- });
474
- getConfigLogger(config, "workflow").error({
475
- err: error,
476
- msg: "afterTransition hook failed",
477
- collection: collection.slug,
478
- transition: transition.name,
479
- documentId: id
480
- });
1069
+ try {
1070
+ const { getConfigLogger: getConfigLogger2, getObservabilityRuntime: getObservabilityRuntime2 } = await Promise.resolve().then(() => (init_observability(), observability_exports));
1071
+ getObservabilityRuntime2(config)?.recordWorkflowHookFailure({
1072
+ collection: collection.slug,
1073
+ transition: transition.name
1074
+ });
1075
+ getConfigLogger2(config, "workflow").error({
1076
+ err: error,
1077
+ msg: "afterTransition hook failed",
1078
+ collection: collection.slug,
1079
+ transition: transition.name,
1080
+ documentId: id
1081
+ });
1082
+ } catch {
1083
+ console.error("afterTransition hook failed:", error);
1084
+ }
481
1085
  }
482
1086
  }
483
1087
  return updated;
@@ -611,25 +1215,290 @@ function normalizeSchemaFragment(fragment) {
611
1215
  }
612
1216
 
613
1217
  // src/utils/declarative-hooks.ts
1218
+ var import_jexl2 = __toESM(require("jexl"), 1);
1219
+
1220
+ // src/utils/jexl-helpers.ts
614
1221
  var import_jexl = __toESM(require("jexl"), 1);
1222
+ function slugify(val) {
1223
+ if (val === void 0 || val === null) return "";
1224
+ return String(val).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1225
+ }
1226
+ function lower(val) {
1227
+ if (val === void 0 || val === null) return "";
1228
+ return String(val).toLowerCase();
1229
+ }
1230
+ function upper(val) {
1231
+ if (val === void 0 || val === null) return "";
1232
+ return String(val).toUpperCase();
1233
+ }
1234
+ function trim(val) {
1235
+ if (val === void 0 || val === null) return "";
1236
+ return String(val).trim();
1237
+ }
1238
+ function capitalize(val) {
1239
+ if (val === void 0 || val === null) return "";
1240
+ const str = String(val).trim();
1241
+ if (!str) return "";
1242
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1243
+ }
1244
+ function truncate(val, length2, ellipsis = "...") {
1245
+ if (val === void 0 || val === null) return "";
1246
+ const str = String(val);
1247
+ const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
1248
+ if (str.length <= maxLen) return str;
1249
+ return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
1250
+ }
1251
+ function readingTime(val, wpm = 200) {
1252
+ if (val === void 0 || val === null) return 1;
1253
+ const str = String(val).trim();
1254
+ if (!str) return 1;
1255
+ const words = str.split(/\s+/).filter(Boolean).length;
1256
+ return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
1257
+ }
1258
+ function wordCount(val) {
1259
+ if (val === void 0 || val === null) return 0;
1260
+ const str = String(val).trim();
1261
+ if (!str) return 0;
1262
+ return str.split(/\s+/).filter(Boolean).length;
1263
+ }
1264
+ function replace(val, search, replacement) {
1265
+ if (val === void 0 || val === null) return "";
1266
+ return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
1267
+ }
1268
+ function startsWith(str, prefix) {
1269
+ if (str === void 0 || str === null) return false;
1270
+ return String(str).startsWith(String(prefix ?? ""));
1271
+ }
1272
+ function endsWith(str, suffix) {
1273
+ if (str === void 0 || str === null) return false;
1274
+ return String(str).endsWith(String(suffix ?? ""));
1275
+ }
1276
+ function now() {
1277
+ return (/* @__PURE__ */ new Date()).toISOString();
1278
+ }
1279
+ function today() {
1280
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1281
+ }
1282
+ function formatDate(val, style = "short") {
1283
+ if (!val) return "";
1284
+ const d = new Date(val);
1285
+ if (Number.isNaN(d.getTime())) return "";
1286
+ const styleStr = String(style).toLowerCase();
1287
+ if (styleStr === "iso" || styleStr === "date") return d.toISOString().slice(0, 10);
1288
+ if (styleStr === "datetime" || styleStr === "full") return d.toISOString();
1289
+ return d.toLocaleDateString("en-US", {
1290
+ year: "numeric",
1291
+ month: "short",
1292
+ day: "numeric"
1293
+ });
1294
+ }
1295
+ function addDays(val, days) {
1296
+ const d = val ? new Date(val) : /* @__PURE__ */ new Date();
1297
+ if (Number.isNaN(d.getTime())) return "";
1298
+ const daysNum = typeof days === "number" ? days : Number(days) || 0;
1299
+ d.setDate(d.getDate() + daysNum);
1300
+ return d.toISOString();
1301
+ }
1302
+ function diffDays(dateA, dateB) {
1303
+ const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
1304
+ const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
1305
+ if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
1306
+ const diffMs = Math.abs(dA.getTime() - dB.getTime());
1307
+ return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1308
+ }
1309
+ function isPast(val) {
1310
+ if (!val) return false;
1311
+ const d = new Date(val);
1312
+ if (Number.isNaN(d.getTime())) return false;
1313
+ return d.getTime() < Date.now();
1314
+ }
1315
+ function isFuture(val) {
1316
+ if (!val) return false;
1317
+ const d = new Date(val);
1318
+ if (Number.isNaN(d.getTime())) return false;
1319
+ return d.getTime() > Date.now();
1320
+ }
1321
+ function includes(arrOrStr, item) {
1322
+ if (Array.isArray(arrOrStr)) {
1323
+ return arrOrStr.includes(item);
1324
+ }
1325
+ if (typeof arrOrStr === "string") {
1326
+ return arrOrStr.includes(String(item ?? ""));
1327
+ }
1328
+ return false;
1329
+ }
1330
+ function join(arr, separator = ", ") {
1331
+ if (!Array.isArray(arr)) return "";
1332
+ return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
1333
+ }
1334
+ function first(arr) {
1335
+ if (Array.isArray(arr)) return arr[0] ?? null;
1336
+ return null;
1337
+ }
1338
+ function last(arr) {
1339
+ if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
1340
+ return null;
1341
+ }
1342
+ function compact(arr) {
1343
+ if (!Array.isArray(arr)) return [];
1344
+ return arr.filter((item) => item !== void 0 && item !== null && item !== "");
1345
+ }
1346
+ function unique(arr) {
1347
+ if (!Array.isArray(arr)) return [];
1348
+ return Array.from(new Set(arr));
1349
+ }
1350
+ function length(val) {
1351
+ if (Array.isArray(val) || typeof val === "string") return val.length;
1352
+ if (val && typeof val === "object") return Object.keys(val).length;
1353
+ return 0;
1354
+ }
1355
+ function round(val, decimals = 0) {
1356
+ const num = Number(val);
1357
+ if (Number.isNaN(num)) return 0;
1358
+ const dec = typeof decimals === "number" ? decimals : Number(decimals) || 0;
1359
+ const factor = Math.pow(10, dec);
1360
+ return Math.round(num * factor) / factor;
1361
+ }
1362
+ function clamp(val, minVal, maxVal) {
1363
+ const num = Number(val) || 0;
1364
+ const min = Number(minVal) || 0;
1365
+ const max = Number(maxVal) || 0;
1366
+ return Math.min(Math.max(num, min), max);
1367
+ }
1368
+ function defaultValue(val, fallback) {
1369
+ if (val === void 0 || val === null || val === "") return fallback;
1370
+ return val;
1371
+ }
1372
+ function coalesce(...args) {
1373
+ for (const arg of args) {
1374
+ if (arg !== void 0 && arg !== null && arg !== "") return arg;
1375
+ }
1376
+ return null;
1377
+ }
1378
+ function isEmpty(val) {
1379
+ if (val === void 0 || val === null || val === "") return true;
1380
+ if (Array.isArray(val)) return val.length === 0;
1381
+ if (typeof val === "object") return Object.keys(val).length === 0;
1382
+ return false;
1383
+ }
1384
+ function get(obj, path, fallback = null) {
1385
+ if (!obj || typeof obj !== "object") return fallback;
1386
+ const pathStr = String(path || "").trim();
1387
+ if (!pathStr) return fallback;
1388
+ const parts = pathStr.split(".");
1389
+ let curr = obj;
1390
+ for (const part of parts) {
1391
+ if (curr === void 0 || curr === null) return fallback;
1392
+ curr = curr[part];
1393
+ }
1394
+ return curr === void 0 || curr === null ? fallback : curr;
1395
+ }
1396
+ function registerJexlHelpers(jexlInstance = import_jexl.default) {
1397
+ const helpers = {
1398
+ // Strings
1399
+ slugify,
1400
+ lower,
1401
+ upper,
1402
+ trim,
1403
+ capitalize,
1404
+ truncate,
1405
+ readingTime,
1406
+ wordCount,
1407
+ replace,
1408
+ startsWith,
1409
+ endsWith,
1410
+ // Dates
1411
+ now,
1412
+ today,
1413
+ formatDate,
1414
+ addDays,
1415
+ diffDays,
1416
+ isPast,
1417
+ isFuture,
1418
+ // Arrays
1419
+ includes,
1420
+ join,
1421
+ first,
1422
+ last,
1423
+ compact,
1424
+ unique,
1425
+ length,
1426
+ // Math
1427
+ round,
1428
+ clamp,
1429
+ // Objects & Logical
1430
+ default: defaultValue,
1431
+ coalesce,
1432
+ isEmpty,
1433
+ get
1434
+ };
1435
+ for (const [name, fn] of Object.entries(helpers)) {
1436
+ try {
1437
+ jexlInstance.addFunction(name, fn);
1438
+ } catch {
1439
+ }
1440
+ }
1441
+ }
1442
+ registerJexlHelpers(import_jexl.default);
1443
+ var BUILTIN_JEXL_HELPERS = [
1444
+ // Strings
1445
+ "slugify",
1446
+ "lower",
1447
+ "upper",
1448
+ "trim",
1449
+ "capitalize",
1450
+ "truncate",
1451
+ "readingTime",
1452
+ "wordCount",
1453
+ "replace",
1454
+ "startsWith",
1455
+ "endsWith",
1456
+ // Dates
1457
+ "now",
1458
+ "today",
1459
+ "formatDate",
1460
+ "addDays",
1461
+ "diffDays",
1462
+ "isPast",
1463
+ "isFuture",
1464
+ // Arrays
1465
+ "includes",
1466
+ "join",
1467
+ "first",
1468
+ "last",
1469
+ "compact",
1470
+ "unique",
1471
+ "length",
1472
+ // Math
1473
+ "round",
1474
+ "clamp",
1475
+ // Objects & Logical
1476
+ "default",
1477
+ "coalesce",
1478
+ "isEmpty",
1479
+ "get"
1480
+ ];
1481
+
1482
+ // src/utils/declarative-hooks.ts
1483
+ registerJexlHelpers(import_jexl2.default);
615
1484
  var SURFACE_ROOTS = {
616
- "collection.beforeRead": ["req", "user", "query"],
617
- "collection.afterRead": ["req", "user", "doc"],
618
- "collection.beforeChange": ["req", "user", "data", "doc", "operation"],
619
- "global.beforeRead": ["req", "user", "query"],
620
- "global.afterRead": ["req", "user", "doc"],
621
- "global.beforeChange": ["req", "user", "data", "doc", "operation"],
622
- "field.beforeChange": ["value", "data", "originalDoc", "user"],
623
- "field.admin.onChange": ["value", "siblingData", "data"]
1485
+ "collection.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1486
+ "collection.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1487
+ "collection.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1488
+ "global.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1489
+ "global.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1490
+ "global.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1491
+ "field.beforeChange": ["value", "data", "originalDoc", "user", ...BUILTIN_JEXL_HELPERS],
1492
+ "field.admin.onChange": ["value", "siblingData", "data", ...BUILTIN_JEXL_HELPERS]
624
1493
  };
625
- var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config"];
626
- var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id"];
627
- var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id"];
1494
+ var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config", ...BUILTIN_JEXL_HELPERS];
1495
+ var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id", ...BUILTIN_JEXL_HELPERS];
1496
+ var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id", ...BUILTIN_JEXL_HELPERS];
628
1497
  var COMPILE_CACHE = /* @__PURE__ */ new Map();
629
1498
  function compileExpression(expression) {
630
1499
  const cached = COMPILE_CACHE.get(expression);
631
1500
  if (cached) return cached;
632
- const compiled = import_jexl.default.compile(expression);
1501
+ const compiled = import_jexl2.default.compile(expression);
633
1502
  const next = { ast: compiled._ast };
634
1503
  COMPILE_CACHE.set(expression, next);
635
1504
  return next;
@@ -1152,13 +2021,16 @@ function collectConfigDiagnostics(config) {
1152
2021
  function formatConfigDiagnostics(source, diagnostics, options = {}) {
1153
2022
  const color = options.color ?? false;
1154
2023
  const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
2024
+ const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/features/admin/form-and-field-hooks", 34, color)}`;
1155
2025
  const lines = diagnostics.flatMap((issue, index) => [
1156
2026
  `${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
1157
2027
  ` ${issue.message}`,
1158
2028
  ` ${colorize("surface:", 90, color)} ${issue.surface}`
1159
2029
  ]);
1160
2030
  return `${header}
1161
- ${lines.join("\n")}`;
2031
+ ${lines.join("\n")}
2032
+
2033
+ ${docLink}`;
1162
2034
  }
1163
2035
  var ConfigValidationError = class extends Error {
1164
2036
  diagnostics;
@@ -1728,7 +2600,7 @@ function buildSqlOrderBy(sort, existingColumns, numericFields, dialect) {
1728
2600
  }
1729
2601
 
1730
2602
  // src/utils/hooks.ts
1731
- var import_jexl2 = __toESM(require("jexl"), 1);
2603
+ var import_jexl3 = __toESM(require("jexl"), 1);
1732
2604
  async function runCollectionHooks(hooks, args, options = {}) {
1733
2605
  if (!hooks || hooks.length === 0) {
1734
2606
  return args.data ?? args.doc ?? args.query ?? void 0;
@@ -1749,7 +2621,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1749
2621
  options.path ?? options.surface
1750
2622
  );
1751
2623
  }
1752
- return import_jexl2.default.eval(hook, hookArgs);
2624
+ return import_jexl3.default.eval(hook, hookArgs);
1753
2625
  })() : await hook(hookArgs);
1754
2626
  if (result !== void 0) {
1755
2627
  if (typeof hook === "string" && options.surface) {
@@ -1766,10 +2638,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1766
2638
  }
1767
2639
  } catch (err) {
1768
2640
  if (options.isolated) {
1769
- getConfigLogger(void 0, "hooks").error({
1770
- err,
1771
- msg: "Side-effect hook failed after a successful write"
1772
- });
2641
+ console.error("Side-effect hook failed after a successful write", err);
1773
2642
  } else {
1774
2643
  throw err;
1775
2644
  }
@@ -1801,7 +2670,7 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pat
1801
2670
  "field.beforeChange",
1802
2671
  fieldPath
1803
2672
  );
1804
- return import_jexl2.default.eval(hook, hookArgs);
2673
+ return import_jexl3.default.eval(hook, hookArgs);
1805
2674
  })() : await hook(hookArgs);
1806
2675
  }
1807
2676
  result[field.name] = updatedValue;
@@ -2943,10 +3812,12 @@ function defineTab(args) {
2943
3812
  }
2944
3813
  // Annotate the CommonJS export names for ESM import in node:
2945
3814
  0 && (module.exports = {
3815
+ BUILTIN_JEXL_HELPERS,
2946
3816
  ConfigValidationError,
2947
3817
  LIFECYCLE_EVENTS_COLLECTION,
2948
3818
  LIFECYCLE_EVENT_NAMES,
2949
3819
  WORKFLOW_HISTORY_COLLECTION,
3820
+ addDays,
2950
3821
  assertValidAdminConditionsInConfig,
2951
3822
  assertValidDeclarativeAccessInConfig,
2952
3823
  assertValidDeclarativeHookExpression,
@@ -2956,13 +3827,18 @@ function defineTab(args) {
2956
3827
  availableWorkflowTransitions,
2957
3828
  buildSqlOrderBy,
2958
3829
  canViewWorkflowDraft,
3830
+ capitalize,
3831
+ clamp,
3832
+ coalesce,
2959
3833
  collectAdminConditionValidationIssues,
2960
3834
  collectConfigDiagnostics,
2961
3835
  collectDeclarativeAccessValidationIssues,
2962
3836
  collectDeclarativeHookValidationIssues,
2963
3837
  collectPreviewUrlValidationIssues,
3838
+ compact,
2964
3839
  createLifecycleEvent,
2965
3840
  createWorkflowDocument,
3841
+ defaultValue,
2966
3842
  defineArrayField,
2967
3843
  defineBlock,
2968
3844
  defineBlocksField,
@@ -2992,32 +3868,58 @@ function defineTab(args) {
2992
3868
  defineTextareaField,
2993
3869
  defineTimeField,
2994
3870
  defineUrlField,
3871
+ diffDays,
2995
3872
  dispatchLifecycleEvent,
2996
3873
  dispatchPendingLifecycleEvents,
3874
+ endsWith,
2997
3875
  executeFieldAfterRead,
2998
3876
  executeFieldBeforeChange,
3877
+ first,
2999
3878
  formatConfigDiagnostics,
3879
+ formatDate,
3000
3880
  generateOpenApi,
3881
+ get,
3001
3882
  getAdminAuthCollection,
3002
3883
  getPublicAdminAuthConfig,
3884
+ includes,
3003
3885
  initializeWorkflowDocument,
3004
3886
  isConfigValidationError,
3887
+ isEmpty,
3888
+ isFuture,
3889
+ isPast,
3890
+ join,
3891
+ last,
3892
+ length,
3893
+ lower,
3005
3894
  materializeWorkflowDocument,
3006
3895
  normalizeConfig,
3896
+ now,
3007
3897
  numericFieldNames,
3008
3898
  parseMongoWhere,
3009
3899
  parseSort,
3010
3900
  parseSqlWhere,
3011
3901
  publishedStateName,
3012
3902
  publishingWorkflow,
3903
+ readingTime,
3904
+ registerJexlHelpers,
3905
+ replace,
3013
3906
  resolveAdminAuthCollection,
3907
+ round,
3014
3908
  runCollectionHooks,
3015
3909
  saveWorkflowDraft,
3016
3910
  simplePublishingWorkflow,
3911
+ slugify,
3912
+ startsWith,
3913
+ today,
3017
3914
  transitionWorkflow,
3915
+ trim,
3916
+ truncate,
3917
+ unique,
3918
+ upper,
3018
3919
  validateAdminConditionExpression,
3019
3920
  validateDeclarativeAccessExpression,
3020
3921
  validateDeclarativeHookExpression,
3021
3922
  validatePreviewUrlExpression,
3923
+ wordCount,
3022
3924
  workflowCapabilities
3023
3925
  });