@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/server.cjs CHANGED
@@ -30,838 +30,884 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
- // src/utils/where-sanitizer.ts
34
- var where_sanitizer_exports = {};
35
- __export(where_sanitizer_exports, {
36
- sanitizeWhereClause: () => sanitizeWhereClause
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
37
51
  });
38
- function sanitizeWhereClause(where, fields) {
39
- const fieldMap = /* @__PURE__ */ new Map();
40
- function addFieldsToMap(fList, prefix = "") {
41
- for (const f of fList) {
42
- if ("name" in f && f.name) {
43
- fieldMap.set(prefix + f.name, f);
44
- }
45
- if (f.type === "object" || f.type === "row" || f.type === "array") {
46
- if ("fields" in f && Array.isArray(f.fields)) {
47
- addFieldsToMap(f.fields, f.type === "object" || f.type === "array" ? `${prefix}${f.name}.` : prefix);
48
- }
49
- }
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" }]
50
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());
51
137
  }
52
- addFieldsToMap(fields);
53
- function walk(node) {
54
- const result = {};
55
- for (const [key, value] of Object.entries(node)) {
56
- if (key === "AND" || key === "OR") {
57
- if (Array.isArray(value)) {
58
- const processed = value.map((v) => walk(v)).filter((v) => Object.keys(v).length > 0);
59
- if (processed.length > 0) {
60
- result[key] = processed;
61
- }
62
- }
63
- continue;
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();
64
150
  }
65
- if (key === "id") {
66
- result[key] = value;
67
- continue;
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
+ });
68
227
  }
69
- const fieldDef = fieldMap.get(key);
70
- if (!fieldDef) continue;
71
- if (fieldDef.admin?.filterable === false) continue;
72
- if (NEVER_FILTERABLE_TYPES.includes(fieldDef.type)) continue;
73
- result[key] = value;
228
+ } catch {
74
229
  }
75
- return result;
76
230
  }
77
- return walk(where);
231
+ return (0, import_pino.destination)(1);
78
232
  }
79
- var NEVER_FILTERABLE_TYPES;
80
- var init_where_sanitizer = __esm({
81
- "src/utils/where-sanitizer.ts"() {
82
- "use strict";
83
- NEVER_FILTERABLE_TYPES = [
84
- "password",
85
- "richText",
86
- "json",
87
- "file",
88
- "image",
89
- "join",
90
- "collapsible"
91
- ];
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;
92
255
  }
93
- });
94
-
95
- // src/server.ts
96
- var server_exports = {};
97
- __export(server_exports, {
98
- AuthController: () => AuthController,
99
- CollectionController: () => CollectionController,
100
- GlobalController: () => GlobalController,
101
- MediaController: () => MediaController,
102
- PreviewController: () => PreviewController,
103
- createDyrectedApp: () => createDyrectedApp,
104
- registerRoutes: () => registerRoutes
105
- });
106
- module.exports = __toCommonJS(server_exports);
107
-
108
- // src/app.ts
109
- var import_hono = require("hono");
110
- var import_cors = require("hono/cors");
111
- var import_request_id = require("hono/request-id");
112
-
113
- // src/services/defaults.service.ts
114
- var DefaultsService = class {
115
- /**
116
- * Recursively apply default values to a data object based on field definitions.
117
- */
118
- static apply(fields, data = {}) {
119
- const result = { ...data || {} };
120
- fields.forEach((field) => {
121
- if (field.type === "join") return;
122
- if (field.type === "row" && field.fields) {
123
- Object.assign(result, this.apply(field.fields, data));
124
- return;
125
- }
126
- if (!field.name) return;
127
- let value = result[field.name];
128
- if ((value === void 0 || value === null) && field.renameTo) {
129
- const legacyValue = result[field.renameTo];
130
- if (legacyValue !== void 0 && legacyValue !== null) {
131
- value = legacyValue;
132
- result[field.name] = legacyValue;
133
- }
134
- }
135
- if (value === void 0 || value === null) {
136
- if (field.defaultValue !== void 0) {
137
- result[field.name] = field.defaultValue;
138
- } else {
139
- if (field.type === "boolean") result[field.name] = false;
140
- else if (field.type === "array") result[field.name] = [];
141
- else if (field.type === "multiSelect") result[field.name] = [];
142
- else if (field.type === "object") {
143
- result[field.name] = this.apply(field.fields || [], {});
144
- }
145
- }
146
- } else if (field.type === "object" && field.fields) {
147
- result[field.name] = this.apply(field.fields, value);
148
- } else if (field.type === "array" && field.fields && Array.isArray(value)) {
149
- result[field.name] = value.map((item) => this.apply(field.fields, item));
150
- }
151
- });
152
- return result;
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;
153
277
  }
154
- };
155
-
156
- // src/services/population.service.ts
157
- var import_jexl = __toESM(require("jexl"), 1);
158
- var PopulationService = class {
159
- db;
160
- collections;
161
- constructor(db, collections) {
162
- this.db = db;
163
- this.collections = collections;
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, requestId2) {
295
+ const trace2 = vars.requestTrace;
296
+ return runtime.logger.child({
297
+ requestId: requestId2,
298
+ siteId: vars.siteId,
299
+ workspaceId: vars.workspaceId,
300
+ traceId: trace2?.traceId,
301
+ spanId: trace2?.spanId
302
+ });
303
+ }
304
+ function shouldSampleRequest(requestId2, 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(requestId2 ?? "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;
164
318
  }
165
- /**
166
- * Recursively populate relationship fields in a document or array of documents.
167
- */
168
- async populate(args) {
169
- const { data, fields, currentDepth = 0, maxDepth = 10 } = args;
170
- if (currentDepth >= maxDepth || !data) {
171
- return data;
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 };
172
336
  }
173
- if (Array.isArray(data)) {
174
- return Promise.all(
175
- data.map((item) => this.populate({ ...args, data: item }))
176
- );
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
+ };
177
356
  }
178
- const populatedDoc = { ...data };
179
- for (const field of fields) {
180
- if (field.type === "join" && field.collection && field.on && currentDepth < maxDepth) {
181
- const targetCollection = this.collections.find(
182
- (c) => c.slug === field.collection
183
- );
184
- if (targetCollection) {
185
- const docId = populatedDoc.id;
186
- if (docId) {
187
- const where = { [field.on]: { equals: docId } };
188
- const joinLimit = field.limit ?? 10;
189
- const result = await this.db.find({
190
- collection: field.collection,
191
- where,
192
- limit: joinLimit
193
- });
194
- const populatedDocs = await this.populate({
195
- data: result.docs.map(
196
- (doc) => DefaultsService.apply(targetCollection.fields, doc)
197
- ),
198
- fields: targetCollection.fields,
199
- currentDepth: currentDepth + 1,
200
- maxDepth
201
- });
202
- populatedDoc[field.name] = {
203
- docs: populatedDocs,
204
- hasNextPage: result.page * result.limit < result.total,
205
- totalDocs: result.total
206
- };
207
- }
208
- }
209
- continue;
210
- }
211
- if (field.type === "row" && field.fields) {
212
- const rowPopulated = await this.populate({
213
- data,
214
- fields: field.fields,
215
- currentDepth,
216
- maxDepth
217
- });
218
- Object.assign(populatedDoc, rowPopulated);
219
- continue;
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);
220
382
  }
221
- if (!field.name) continue;
222
- const value = populatedDoc[field.name];
223
- if (field.type === "relationship" && field.relationTo && value) {
224
- const relatedCollection = this.collections.find(
225
- (c) => c.slug === field.relationTo
226
- );
227
- if (!relatedCollection) continue;
228
- if (Array.isArray(value)) {
229
- populatedDoc[field.name] = await Promise.all(
230
- value.map(async (id) => {
231
- if (!id) return id;
232
- let doc = id;
233
- if (typeof id === "string") {
234
- doc = await this.db.findOne({
235
- collection: field.relationTo,
236
- id
237
- });
238
- }
239
- if (!doc || typeof doc !== "object") return id;
240
- const docWithDefaults = DefaultsService.apply(
241
- relatedCollection.fields,
242
- doc
243
- );
244
- return this.populate({
245
- data: docWithDefaults,
246
- fields: relatedCollection.fields,
247
- currentDepth: currentDepth + 1,
248
- maxDepth
249
- });
250
- })
251
- );
252
- } else if (value) {
253
- let doc = value;
254
- if (typeof value === "string") {
255
- doc = await this.db.findOne({
256
- collection: field.relationTo,
257
- id: value
258
- });
259
- }
260
- if (doc && typeof doc === "object") {
261
- const docWithDefaults = DefaultsService.apply(
262
- relatedCollection.fields,
263
- doc
264
- );
265
- populatedDoc[field.name] = await this.populate({
266
- data: docWithDefaults,
267
- fields: relatedCollection.fields,
268
- currentDepth: currentDepth + 1,
269
- maxDepth
270
- });
271
- }
272
- }
273
- }
274
- if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
275
- const relatedCollection = this.collections.find(
276
- (c) => c.slug === value.relationTo
277
- );
278
- if (relatedCollection) {
279
- const doc = await this.db.findOne({
280
- collection: value.relationTo,
281
- id: value.value
282
- });
283
- if (doc && typeof doc === "object") {
284
- const docWithDefaults = DefaultsService.apply(
285
- relatedCollection.fields,
286
- doc
287
- );
288
- const populatedDocValue = await this.populate({
289
- data: docWithDefaults,
290
- fields: relatedCollection.fields,
291
- currentDepth: currentDepth + 1,
292
- maxDepth
293
- });
294
- const identifier = docWithDefaults.slug || docWithDefaults.id;
295
- let resolvedUrl = `/collections/${value.relationTo}/${identifier}`;
296
- const previewUrlPattern = relatedCollection.admin?.previewUrl || relatedCollection.admin?.urlPattern;
297
- if (previewUrlPattern) {
298
- if (typeof previewUrlPattern === "function") {
299
- try {
300
- resolvedUrl = previewUrlPattern(docWithDefaults, { locale: "en" }) || resolvedUrl;
301
- } catch {
302
- }
303
- } else if (typeof previewUrlPattern === "string") {
304
- if (previewUrlPattern.includes("{{")) {
305
- resolvedUrl = previewUrlPattern.replace(
306
- /{{(.*?)}}/g,
307
- (_, key) => String(docWithDefaults[key.trim()] || "")
308
- );
309
- } else if (previewUrlPattern.includes("+") || previewUrlPattern.includes("?") || previewUrlPattern.includes("==") || previewUrlPattern.includes("siteUrl")) {
310
- try {
311
- resolvedUrl = import_jexl.default.evalSync(
312
- previewUrlPattern,
313
- docWithDefaults
314
- );
315
- } catch (_err) {
316
- resolvedUrl = previewUrlPattern.replace(
317
- /:([a-zA-Z0-9_]+)/g,
318
- (_, key) => String(docWithDefaults[key] || "")
319
- );
320
- }
321
- } else {
322
- resolvedUrl = previewUrlPattern.replace(
323
- /:([a-zA-Z0-9_]+)/g,
324
- (_, key) => String(docWithDefaults[key] || "")
325
- );
326
- }
327
- }
328
- }
329
- populatedDoc[field.name] = {
330
- ...value,
331
- url: resolvedUrl,
332
- doc: populatedDocValue
333
- };
334
- }
335
- }
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();
336
452
  }
337
- if ((field.type === "array" || field.type === "object") && field.fields && value) {
338
- populatedDoc[field.name] = await this.populate({
339
- data: value,
340
- fields: field.fields,
341
- currentDepth,
342
- // Nested fields don't consume depth, only relationships do
343
- maxDepth
344
- });
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;
345
524
  }
346
- if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
347
- populatedDoc[field.name] = await Promise.all(
348
- value.map(async (blockData) => {
349
- const blockConfig = field.blocks.find(
350
- (b) => b.slug === blockData.blockType
351
- );
352
- if (!blockConfig) return blockData;
353
- return this.populate({
354
- data: blockData,
355
- fields: blockConfig.fields,
356
- currentDepth,
357
- maxDepth
358
- });
359
- })
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)))
360
538
  );
361
539
  }
362
- }
363
- return populatedDoc;
364
- }
365
- /**
366
- * Helper to populate a PaginatedResult
367
- */
368
- async populateResult(result, fields, maxDepth) {
369
- if (maxDepth <= 0) return result;
370
- const populatedDocs = await this.populate({
371
- data: result.docs,
372
- fields,
373
- currentDepth: 0,
374
- maxDepth
375
- });
376
- return {
377
- ...result,
378
- docs: populatedDocs
379
540
  };
380
541
  }
381
- };
542
+ });
382
543
 
383
- // src/observability.ts
384
- var import_api = require("@opentelemetry/api");
385
- var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
386
- var import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
387
- var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
388
- var import_resources = require("@opentelemetry/resources");
389
- var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
390
- var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
391
- var import_pino = __toESM(require("pino"), 1);
392
- var import_pino_pretty = __toESM(require("pino-pretty"), 1);
393
- var import_node_stream = require("stream");
394
- var DEFAULT_REDACT_HEADERS = ["authorization", "cookie", "set-cookie", "x-api-key"];
395
- var DEFAULT_REDACT_PATHS = [
396
- "password",
397
- "currentPassword",
398
- "newPassword",
399
- "confirmPassword",
400
- "token",
401
- "refreshToken",
402
- "accessToken",
403
- "secret",
404
- "apiKey",
405
- "inviteToken",
406
- "resetToken"
407
- ];
408
- var DEFAULT_MAX_BODY_BYTES = 8192;
409
- var DEFAULT_SUCCESS_SAMPLE_RATE = 0.1;
410
- var DEFAULT_BODY_SAMPLE_RATE = 0.02;
411
- var REDACTED_VALUE = "[REDACTED]";
412
- var runtimeByConfig = /* @__PURE__ */ new WeakMap();
413
- var fallbackLogger = (0, import_pino.default)({
414
- name: "dyrected",
415
- enabled: process.env.DISABLE_LOGGING !== "true",
416
- timestamp: import_pino.stdTimeFunctions.isoTime
544
+ // src/utils/where-sanitizer.ts
545
+ var where_sanitizer_exports = {};
546
+ __export(where_sanitizer_exports, {
547
+ sanitizeWhereClause: () => sanitizeWhereClause
417
548
  });
418
- function resolveObservabilityConfig(config) {
419
- const requestLogging = config?.requestLogging;
420
- const sampling = config?.sampling;
421
- const tracing = config?.tracing;
422
- const metrics = config?.metrics;
423
- return {
424
- requestLogging: {
425
- enabled: requestLogging?.enabled ?? true,
426
- logBodies: requestLogging?.logBodies ?? false,
427
- maxBodyBytes: requestLogging?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
428
- redactPaths: uniqueValues([
429
- ...DEFAULT_REDACT_PATHS,
430
- ...requestLogging?.redactPaths ?? []
431
- ]),
432
- includeHeaders: normalizeHeaderNames(requestLogging?.includeHeaders ?? []),
433
- redactHeaders: uniqueValues(
434
- normalizeHeaderNames([
435
- ...DEFAULT_REDACT_HEADERS,
436
- ...requestLogging?.redactHeaders ?? []
437
- ])
438
- )
439
- },
440
- sampling: {
441
- successRate: clampRate(sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE),
442
- traceSuccessRate: clampRate(
443
- sampling?.traceSuccessRate ?? sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE
444
- ),
445
- bodySuccessRate: clampRate(
446
- sampling?.bodySuccessRate ?? Math.min(DEFAULT_BODY_SAMPLE_RATE, sampling?.successRate ?? DEFAULT_SUCCESS_SAMPLE_RATE)
447
- ),
448
- alwaysKeep4xx: sampling?.alwaysKeep4xx ?? true,
449
- alwaysKeep5xx: sampling?.alwaysKeep5xx ?? true
450
- },
451
- tracing: {
452
- enabled: tracing?.enabled ?? false,
453
- serviceName: tracing?.serviceName ?? "dyrected",
454
- exporter: tracing?.exporter ?? "none",
455
- headers: tracing?.headers ?? {},
456
- endpoint: tracing?.endpoint
457
- },
458
- metrics: {
459
- enabled: metrics?.enabled ?? false,
460
- exporter: metrics?.exporter ?? "none",
461
- endpoint: metrics?.endpoint,
462
- path: metrics?.path ?? "/metrics"
463
- },
464
- transports: {
465
- targets: config?.transports?.targets && config.transports.targets.length > 0 ? config.transports.targets : [{ type: "stdout" }]
549
+ function sanitizeWhereClause(where, fields) {
550
+ const fieldMap = /* @__PURE__ */ new Map();
551
+ function addFieldsToMap(fList, prefix = "") {
552
+ for (const f of fList) {
553
+ if ("name" in f && f.name) {
554
+ fieldMap.set(prefix + f.name, f);
555
+ }
556
+ if (f.type === "object" || f.type === "row" || f.type === "array") {
557
+ if ("fields" in f && Array.isArray(f.fields)) {
558
+ addFieldsToMap(f.fields, f.type === "object" || f.type === "array" ? `${prefix}${f.name}.` : prefix);
559
+ }
560
+ }
466
561
  }
467
- };
468
- }
469
- function createObservabilityRuntime(config) {
470
- const resolved = resolveObservabilityConfig(config.observability);
471
- const logger = createRootLogger(config.logger, resolved);
472
- const shutdownTasks = [];
473
- const meterRuntime = resolved.metrics.enabled || resolved.metrics.exporter === "prometheus" ? createMeterProvider(resolved) : void 0;
474
- const tracerProvider = resolved.tracing.enabled ? createTracerProvider(resolved) : void 0;
475
- const tracer = tracerProvider?.getTracer("dyrected");
476
- const meter = meterRuntime?.provider.getMeter("dyrected");
477
- const metrics = meter ? {
478
- requestCount: meter.createCounter("dyrected_http_requests_total", {
479
- description: "Total HTTP requests served by Dyrected"
480
- }),
481
- authFailureCount: meter.createCounter("dyrected_auth_failures_total", {
482
- description: "Authentication failures"
483
- }),
484
- uncaughtErrorCount: meter.createCounter("dyrected_uncaught_errors_total", {
485
- description: "Uncaught application errors"
486
- }),
487
- auditWriteFailureCount: meter.createCounter("dyrected_audit_write_failures_total", {
488
- description: "Audit log persistence failures"
489
- }),
490
- emailSendFailureCount: meter.createCounter("dyrected_email_send_failures_total", {
491
- description: "Transactional email delivery failures"
492
- }),
493
- workflowHookFailureCount: meter.createCounter("dyrected_workflow_hook_failures_total", {
494
- description: "Workflow hook failures isolated after successful writes"
495
- }),
496
- requestDuration: meter.createHistogram("dyrected_http_request_duration_ms", {
497
- description: "HTTP request duration in milliseconds",
498
- unit: "ms"
499
- })
500
- } : void 0;
501
- if (meterRuntime) {
502
- shutdownTasks.push(() => meterRuntime.provider.shutdown());
503
- }
504
- if (tracerProvider) {
505
- shutdownTasks.push(() => tracerProvider.shutdown());
506
562
  }
507
- return {
508
- logger,
509
- config: resolved,
510
- tracer,
511
- metrics,
512
- prometheusExporter: meterRuntime?.prometheusExporter,
513
- shutdown: async () => {
514
- for (const task of shutdownTasks) {
515
- await task();
563
+ addFieldsToMap(fields);
564
+ function walk(node) {
565
+ const result = {};
566
+ for (const [key, value] of Object.entries(node)) {
567
+ if (key === "AND" || key === "OR") {
568
+ if (Array.isArray(value)) {
569
+ const processed = value.map((v) => walk(v)).filter((v) => Object.keys(v).length > 0);
570
+ if (processed.length > 0) {
571
+ result[key] = processed;
572
+ }
573
+ }
574
+ continue;
516
575
  }
517
- },
518
- recordAuthFailure: (attributes) => metrics?.authFailureCount.add(1, attributes),
519
- recordUncaughtError: (attributes) => metrics?.uncaughtErrorCount.add(1, attributes),
520
- recordAuditWriteFailure: (attributes) => metrics?.auditWriteFailureCount.add(1, attributes),
521
- recordEmailSendFailure: (attributes) => metrics?.emailSendFailureCount.add(1, attributes),
522
- recordWorkflowHookFailure: (attributes) => metrics?.workflowHookFailureCount.add(1, attributes)
523
- };
524
- }
525
- function bindObservabilityRuntime(config, runtime) {
526
- runtimeByConfig.set(config, runtime);
527
- }
528
- function getObservabilityRuntime(config) {
529
- if (!config || typeof config !== "object") return void 0;
530
- return runtimeByConfig.get(config);
531
- }
532
- function createRootLogger(loggerConfig, observability) {
533
- if (loggerConfig && !("options" in loggerConfig)) {
534
- return loggerConfig;
535
- }
536
- const options = loggerConfig?.options ?? {};
537
- const enabled = options.enabled ?? process.env.DISABLE_LOGGING !== "true";
538
- const name = options.name ?? "dyrected";
539
- if (loggerConfig?.destination) {
540
- return (0, import_pino.default)(
541
- {
542
- ...options,
543
- enabled,
544
- name,
545
- timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
546
- },
547
- loggerConfig.destination
548
- );
576
+ if (key === "id") {
577
+ result[key] = value;
578
+ continue;
579
+ }
580
+ const fieldDef = fieldMap.get(key);
581
+ if (!fieldDef) continue;
582
+ if (fieldDef.admin?.filterable === false) continue;
583
+ if (NEVER_FILTERABLE_TYPES.includes(fieldDef.type)) continue;
584
+ result[key] = value;
585
+ }
586
+ return result;
549
587
  }
550
- const streams = buildTransportStreams(observability);
551
- const destination = streams.length === 1 ? streams[0] : (0, import_pino.multistream)(streams.map((stream) => ({ stream })));
552
- return (0, import_pino.default)(
553
- {
554
- ...options,
555
- enabled,
556
- name,
557
- timestamp: options.timestamp ?? import_pino.stdTimeFunctions.isoTime
558
- },
559
- destination
560
- );
588
+ return walk(where);
561
589
  }
562
- function buildTransportStreams(observability) {
563
- const targets = observability.transports.targets;
564
- if (targets.length === 0) {
565
- return [defaultLoggerDestination()];
590
+ var NEVER_FILTERABLE_TYPES;
591
+ var init_where_sanitizer = __esm({
592
+ "src/utils/where-sanitizer.ts"() {
593
+ "use strict";
594
+ NEVER_FILTERABLE_TYPES = [
595
+ "password",
596
+ "richText",
597
+ "json",
598
+ "file",
599
+ "image",
600
+ "join",
601
+ "collapsible"
602
+ ];
566
603
  }
567
- return targets.map((target) => {
568
- switch (target.type) {
569
- case "stdout":
570
- return defaultLoggerDestination();
571
- case "stderr":
572
- return (0, import_pino.destination)(2);
573
- case "file":
574
- return (0, import_pino.destination)(target.path);
575
- case "otlp":
576
- return new OtlpLogWritable(target.endpoint, target.headers);
577
- }
578
- });
579
- }
580
- function defaultLoggerDestination() {
581
- if (process.env.NODE_ENV !== "production") {
582
- return (0, import_pino_pretty.default)({
583
- colorize: true,
584
- ignore: "pid,hostname",
585
- translateTime: "SYS:HH:MM:ss",
586
- destination: 1,
587
- sync: true
604
+ });
605
+
606
+ // src/server.ts
607
+ var server_exports = {};
608
+ __export(server_exports, {
609
+ AuthController: () => AuthController,
610
+ CollectionController: () => CollectionController,
611
+ GlobalController: () => GlobalController,
612
+ MediaController: () => MediaController,
613
+ PreviewController: () => PreviewController,
614
+ attachRequestMetrics: () => attachRequestMetrics,
615
+ bindObservabilityRuntime: () => bindObservabilityRuntime,
616
+ buildRequestLogger: () => buildRequestLogger,
617
+ captureRequestBody: () => captureRequestBody,
618
+ createDyrectedApp: () => createDyrectedApp,
619
+ createObservabilityRuntime: () => createObservabilityRuntime,
620
+ createRequestSpan: () => createRequestSpan,
621
+ endRequestSpan: () => endRequestSpan,
622
+ getConfigLogger: () => getConfigLogger,
623
+ getObservabilityRuntime: () => getObservabilityRuntime,
624
+ getRequestLogger: () => getRequestLogger,
625
+ redactBody: () => redactBody,
626
+ redactHeaders: () => redactHeaders,
627
+ registerRoutes: () => registerRoutes,
628
+ renderPrometheusMetrics: () => renderPrometheusMetrics,
629
+ resolveObservabilityConfig: () => resolveObservabilityConfig,
630
+ shouldSampleRequest: () => shouldSampleRequest
631
+ });
632
+ module.exports = __toCommonJS(server_exports);
633
+
634
+ // src/app.ts
635
+ var import_hono = require("hono");
636
+ var import_cors = require("hono/cors");
637
+ var import_request_id = require("hono/request-id");
638
+
639
+ // src/services/defaults.service.ts
640
+ var DefaultsService = class {
641
+ /**
642
+ * Recursively apply default values to a data object based on field definitions.
643
+ */
644
+ static apply(fields, data = {}) {
645
+ const result = { ...data || {} };
646
+ fields.forEach((field) => {
647
+ if (field.type === "join") return;
648
+ if (field.type === "row" && field.fields) {
649
+ Object.assign(result, this.apply(field.fields, data));
650
+ return;
651
+ }
652
+ if (!field.name) return;
653
+ let value = result[field.name];
654
+ if ((value === void 0 || value === null) && field.renameTo) {
655
+ const legacyValue = result[field.renameTo];
656
+ if (legacyValue !== void 0 && legacyValue !== null) {
657
+ value = legacyValue;
658
+ result[field.name] = legacyValue;
659
+ }
660
+ }
661
+ if (value === void 0 || value === null) {
662
+ if (field.defaultValue !== void 0) {
663
+ result[field.name] = field.defaultValue;
664
+ } else {
665
+ if (field.type === "boolean") result[field.name] = false;
666
+ else if (field.type === "array") result[field.name] = [];
667
+ else if (field.type === "multiSelect") result[field.name] = [];
668
+ else if (field.type === "object") {
669
+ result[field.name] = this.apply(field.fields || [], {});
670
+ }
671
+ }
672
+ } else if (field.type === "object" && field.fields) {
673
+ result[field.name] = this.apply(field.fields, value);
674
+ } else if (field.type === "array" && field.fields && Array.isArray(value)) {
675
+ result[field.name] = value.map((item) => this.apply(field.fields, item));
676
+ }
588
677
  });
589
- }
590
- return (0, import_pino.destination)(1);
591
- }
592
- var OtlpLogWritable = class extends import_node_stream.Writable {
593
- constructor(endpoint, headers = {}) {
594
- super();
595
- this.endpoint = endpoint;
596
- this.headers = headers;
597
- }
598
- endpoint;
599
- headers;
600
- _write(chunk, _encoding, callback) {
601
- const payload = chunk.toString();
602
- void fetch(this.endpoint, {
603
- method: "POST",
604
- headers: {
605
- "content-type": "application/json",
606
- ...this.headers
607
- },
608
- body: JSON.stringify({ logs: payload.trim().split("\n").filter(Boolean) })
609
- }).then(() => callback()).catch(
610
- (error) => callback(error instanceof Error ? error : new Error(String(error)))
611
- );
678
+ return result;
612
679
  }
613
680
  };
614
- function createTracerProvider(observability) {
615
- const exporter = createTraceExporter(observability);
616
- const provider = new import_sdk_trace_base.BasicTracerProvider({
617
- resource: (0, import_resources.resourceFromAttributes)({
618
- "service.name": observability.tracing.serviceName
619
- }),
620
- spanProcessors: exporter ? [new import_sdk_trace_base.BatchSpanProcessor(exporter)] : []
621
- });
622
- import_api.trace.setGlobalTracerProvider(provider);
623
- return provider;
624
- }
625
- function createTraceExporter(observability) {
626
- switch (observability.tracing.exporter) {
627
- case "console":
628
- return new import_sdk_trace_base.ConsoleSpanExporter();
629
- case "otlp":
630
- return new import_exporter_trace_otlp_http.OTLPTraceExporter({
631
- url: observability.tracing.endpoint,
632
- headers: observability.tracing.headers
633
- });
634
- default:
635
- return void 0;
636
- }
637
- }
638
- function createMeterProvider(observability) {
639
- const readers = [];
640
- let prometheusExporter;
641
- switch (observability.metrics.exporter) {
642
- case "otlp":
643
- readers.push(
644
- new import_sdk_metrics.PeriodicExportingMetricReader({
645
- exporter: new import_exporter_metrics_otlp_http.OTLPMetricExporter({
646
- url: observability.metrics.endpoint
647
- })
648
- })
649
- );
650
- break;
651
- case "prometheus":
652
- prometheusExporter = new import_exporter_prometheus.PrometheusExporter({
653
- endpoint: observability.metrics.path,
654
- preventServerStart: true
655
- });
656
- readers.push(prometheusExporter);
657
- break;
658
- }
659
- const provider = new import_sdk_metrics.MeterProvider({
660
- resource: (0, import_resources.resourceFromAttributes)({
661
- "service.name": observability.tracing.serviceName
662
- }),
663
- readers
664
- });
665
- return { provider, prometheusExporter };
666
- }
667
- function getConfigLogger(config, component) {
668
- const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
669
- return base.child({ component });
670
- }
671
- function getRequestLogger(c, component) {
672
- const base = c.get("logger") ?? fallbackLogger;
673
- return component ? base.child({ component }) : base;
674
- }
675
- function buildRequestLogger(runtime, vars, requestId2) {
676
- const trace2 = vars.requestTrace;
677
- return runtime.logger.child({
678
- requestId: requestId2,
679
- siteId: vars.siteId,
680
- workspaceId: vars.workspaceId,
681
- traceId: trace2?.traceId,
682
- spanId: trace2?.spanId
683
- });
684
- }
685
- function shouldSampleRequest(requestId2, statusCode, sampling, kind) {
686
- if (statusCode >= 500 && sampling.alwaysKeep5xx) return true;
687
- if (statusCode >= 400 && sampling.alwaysKeep4xx) return true;
688
- const rate = kind === "trace" ? sampling.traceSuccessRate : kind === "body" ? sampling.bodySuccessRate : sampling.successRate;
689
- return deterministicSample(requestId2 ?? "no-request-id", rate);
690
- }
691
- function redactHeaders(headers, observability) {
692
- const includeHeaders = new Set(observability.requestLogging.includeHeaders);
693
- const redactHeaders2 = new Set(observability.requestLogging.redactHeaders);
694
- const output = {};
695
- for (const [key, value] of Object.entries(headers)) {
696
- const normalizedKey = key.toLowerCase();
697
- if (includeHeaders.size > 0 && !includeHeaders.has(normalizedKey)) continue;
698
- output[normalizedKey] = redactHeaders2.has(normalizedKey) ? REDACTED_VALUE : value;
699
- }
700
- return output;
701
- }
702
- async function captureRequestBody(request, observability) {
703
- const contentType = request.headers.get("content-type") ?? void 0;
704
- const contentLength = toNumber(request.headers.get("content-length"));
705
- const jsonBody = contentType?.includes("application/json");
706
- if (!jsonBody) {
707
- return {
708
- attempted: false,
709
- contentType,
710
- contentLength
711
- };
681
+
682
+ // src/services/population.service.ts
683
+ var import_jexl = __toESM(require("jexl"), 1);
684
+ var PopulationService = class {
685
+ db;
686
+ collections;
687
+ constructor(db, collections) {
688
+ this.db = db;
689
+ this.collections = collections;
712
690
  }
713
- try {
714
- const text = await request.clone().text();
715
- if (!text) {
716
- return { attempted: true, contentType, contentLength };
691
+ /**
692
+ * Recursively populate relationship fields in a document or array of documents.
693
+ */
694
+ async populate(args) {
695
+ const { data, fields, currentDepth = 0, maxDepth = 10 } = args;
696
+ if (currentDepth >= maxDepth || !data) {
697
+ return data;
717
698
  }
718
- const truncated = text.length > observability.requestLogging.maxBodyBytes;
719
- const raw = truncated ? text.slice(0, observability.requestLogging.maxBodyBytes) : text;
720
- try {
721
- const parsed = JSON.parse(raw);
722
- return {
723
- attempted: true,
724
- contentType,
725
- contentLength: contentLength ?? text.length,
726
- truncated,
727
- body: redactBody(parsed, observability.requestLogging.redactPaths)
728
- };
729
- } catch {
730
- return {
731
- attempted: true,
732
- contentType,
733
- contentLength: contentLength ?? text.length,
734
- truncated,
735
- parseFailed: true
736
- };
699
+ if (Array.isArray(data)) {
700
+ return Promise.all(
701
+ data.map((item) => this.populate({ ...args, data: item }))
702
+ );
737
703
  }
738
- } catch {
739
- return {
740
- attempted: true,
741
- contentType,
742
- contentLength,
743
- parseFailed: true
744
- };
745
- }
746
- }
747
- function redactBody(value, redactPaths) {
748
- const cloned = deepClone(value);
749
- for (const path of redactPaths) {
750
- applyRedaction(cloned, path.split("."));
751
- }
752
- return cloned;
753
- }
754
- function applyRedaction(target, segments) {
755
- if (!target || segments.length === 0) return;
756
- const [segment, ...rest] = segments;
757
- if (Array.isArray(target)) {
758
- for (const item of target) {
759
- if (segment === "*") {
760
- applyRedaction(item, rest);
761
- } else {
762
- applyRedaction(item, segments);
704
+ const populatedDoc = { ...data };
705
+ for (const field of fields) {
706
+ if (field.type === "join" && field.collection && field.on && currentDepth < maxDepth) {
707
+ const targetCollection = this.collections.find(
708
+ (c) => c.slug === field.collection
709
+ );
710
+ if (targetCollection) {
711
+ const docId = populatedDoc.id;
712
+ if (docId) {
713
+ const where = { [field.on]: { equals: docId } };
714
+ const joinLimit = field.limit ?? 10;
715
+ const result = await this.db.find({
716
+ collection: field.collection,
717
+ where,
718
+ limit: joinLimit
719
+ });
720
+ const populatedDocs = await this.populate({
721
+ data: result.docs.map(
722
+ (doc) => DefaultsService.apply(targetCollection.fields, doc)
723
+ ),
724
+ fields: targetCollection.fields,
725
+ currentDepth: currentDepth + 1,
726
+ maxDepth
727
+ });
728
+ populatedDoc[field.name] = {
729
+ docs: populatedDocs,
730
+ hasNextPage: result.page * result.limit < result.total,
731
+ totalDocs: result.total
732
+ };
733
+ }
734
+ }
735
+ continue;
736
+ }
737
+ if (field.type === "row" && field.fields) {
738
+ const rowPopulated = await this.populate({
739
+ data,
740
+ fields: field.fields,
741
+ currentDepth,
742
+ maxDepth
743
+ });
744
+ Object.assign(populatedDoc, rowPopulated);
745
+ continue;
746
+ }
747
+ if (!field.name) continue;
748
+ const value = populatedDoc[field.name];
749
+ if (field.type === "relationship" && field.relationTo && value) {
750
+ const relatedCollection = this.collections.find(
751
+ (c) => c.slug === field.relationTo
752
+ );
753
+ if (!relatedCollection) continue;
754
+ if (Array.isArray(value)) {
755
+ populatedDoc[field.name] = await Promise.all(
756
+ value.map(async (id) => {
757
+ if (!id) return id;
758
+ let doc = id;
759
+ if (typeof id === "string") {
760
+ doc = await this.db.findOne({
761
+ collection: field.relationTo,
762
+ id
763
+ });
764
+ }
765
+ if (!doc || typeof doc !== "object") return id;
766
+ const docWithDefaults = DefaultsService.apply(
767
+ relatedCollection.fields,
768
+ doc
769
+ );
770
+ return this.populate({
771
+ data: docWithDefaults,
772
+ fields: relatedCollection.fields,
773
+ currentDepth: currentDepth + 1,
774
+ maxDepth
775
+ });
776
+ })
777
+ );
778
+ } else if (value) {
779
+ let doc = value;
780
+ if (typeof value === "string") {
781
+ doc = await this.db.findOne({
782
+ collection: field.relationTo,
783
+ id: value
784
+ });
785
+ }
786
+ if (doc && typeof doc === "object") {
787
+ const docWithDefaults = DefaultsService.apply(
788
+ relatedCollection.fields,
789
+ doc
790
+ );
791
+ populatedDoc[field.name] = await this.populate({
792
+ data: docWithDefaults,
793
+ fields: relatedCollection.fields,
794
+ currentDepth: currentDepth + 1,
795
+ maxDepth
796
+ });
797
+ }
798
+ }
799
+ }
800
+ if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
801
+ const relatedCollection = this.collections.find(
802
+ (c) => c.slug === value.relationTo
803
+ );
804
+ if (relatedCollection) {
805
+ const doc = await this.db.findOne({
806
+ collection: value.relationTo,
807
+ id: value.value
808
+ });
809
+ if (doc && typeof doc === "object") {
810
+ const docWithDefaults = DefaultsService.apply(
811
+ relatedCollection.fields,
812
+ doc
813
+ );
814
+ const populatedDocValue = await this.populate({
815
+ data: docWithDefaults,
816
+ fields: relatedCollection.fields,
817
+ currentDepth: currentDepth + 1,
818
+ maxDepth
819
+ });
820
+ const identifier = docWithDefaults.slug || docWithDefaults.id;
821
+ let resolvedUrl = `/collections/${value.relationTo}/${identifier}`;
822
+ const previewUrlPattern = relatedCollection.admin?.previewUrl || relatedCollection.admin?.urlPattern;
823
+ if (previewUrlPattern) {
824
+ if (typeof previewUrlPattern === "function") {
825
+ try {
826
+ resolvedUrl = previewUrlPattern(docWithDefaults, { locale: "en" }) || resolvedUrl;
827
+ } catch {
828
+ }
829
+ } else if (typeof previewUrlPattern === "string") {
830
+ if (previewUrlPattern.includes("{{")) {
831
+ resolvedUrl = previewUrlPattern.replace(
832
+ /{{(.*?)}}/g,
833
+ (_, key) => String(docWithDefaults[key.trim()] || "")
834
+ );
835
+ } else if (previewUrlPattern.includes("+") || previewUrlPattern.includes("?") || previewUrlPattern.includes("==") || previewUrlPattern.includes("siteUrl")) {
836
+ try {
837
+ resolvedUrl = import_jexl.default.evalSync(
838
+ previewUrlPattern,
839
+ docWithDefaults
840
+ );
841
+ } catch (_err) {
842
+ resolvedUrl = previewUrlPattern.replace(
843
+ /:([a-zA-Z0-9_]+)/g,
844
+ (_, key) => String(docWithDefaults[key] || "")
845
+ );
846
+ }
847
+ } else {
848
+ resolvedUrl = previewUrlPattern.replace(
849
+ /:([a-zA-Z0-9_]+)/g,
850
+ (_, key) => String(docWithDefaults[key] || "")
851
+ );
852
+ }
853
+ }
854
+ }
855
+ populatedDoc[field.name] = {
856
+ ...value,
857
+ url: resolvedUrl,
858
+ doc: populatedDocValue
859
+ };
860
+ }
861
+ }
862
+ }
863
+ if ((field.type === "array" || field.type === "object") && field.fields && value) {
864
+ populatedDoc[field.name] = await this.populate({
865
+ data: value,
866
+ fields: field.fields,
867
+ currentDepth,
868
+ // Nested fields don't consume depth, only relationships do
869
+ maxDepth
870
+ });
871
+ }
872
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
873
+ populatedDoc[field.name] = await Promise.all(
874
+ value.map(async (blockData) => {
875
+ const blockConfig = field.blocks.find(
876
+ (b) => b.slug === blockData.blockType
877
+ );
878
+ if (!blockConfig) return blockData;
879
+ return this.populate({
880
+ data: blockData,
881
+ fields: blockConfig.fields,
882
+ currentDepth,
883
+ maxDepth
884
+ });
885
+ })
886
+ );
763
887
  }
764
888
  }
765
- return;
766
- }
767
- if (typeof target !== "object") return;
768
- const record = target;
769
- if (segment === "*") {
770
- for (const value of Object.values(record)) {
771
- applyRedaction(value, rest);
772
- }
773
- return;
774
- }
775
- if (!(segment in record)) return;
776
- if (rest.length === 0) {
777
- record[segment] = REDACTED_VALUE;
778
- return;
779
- }
780
- applyRedaction(record[segment], rest);
781
- }
782
- function deepClone(value) {
783
- if (typeof structuredClone === "function") {
784
- return structuredClone(value);
785
- }
786
- return JSON.parse(JSON.stringify(value));
787
- }
788
- function createRequestSpan(runtime, name, attributes) {
789
- if (!runtime.tracer) return void 0;
790
- const span = runtime.tracer.startSpan(name, { attributes }, import_api.context.active());
791
- const spanContext = span.spanContext();
792
- return {
793
- span,
794
- traceId: spanContext.traceId,
795
- spanId: spanContext.spanId,
796
- sampled: spanContext.traceFlags === import_api.TraceFlags.SAMPLED
797
- };
798
- }
799
- function endRequestSpan(requestTrace, statusCode, error) {
800
- if (!requestTrace) return;
801
- requestTrace.span.setAttribute("http.status_code", statusCode);
802
- if (error) {
803
- requestTrace.span.recordException(error instanceof Error ? error : new Error(String(error)));
889
+ return populatedDoc;
804
890
  }
805
- requestTrace.span.end();
806
- }
807
- function attachRequestMetrics(runtime, args) {
808
- runtime.metrics?.requestCount.add(1, {
809
- method: args.method,
810
- route: args.route,
811
- status_class: `${Math.floor(args.statusCode / 100)}xx`
812
- });
813
- runtime.metrics?.requestDuration.record(args.durationMs, {
814
- method: args.method,
815
- route: args.route,
816
- status_class: `${Math.floor(args.statusCode / 100)}xx`
817
- });
818
- }
819
- async function renderPrometheusMetrics(exporter) {
820
- let statusCode = 200;
821
- const headers = {};
822
- let body = "";
823
- await new Promise((resolve) => {
824
- const response = {
825
- statusCode,
826
- setHeader(name, value) {
827
- headers[name.toLowerCase()] = value;
828
- },
829
- end(chunk) {
830
- if (chunk) body += chunk;
831
- statusCode = this.statusCode;
832
- resolve();
833
- }
891
+ /**
892
+ * Helper to populate a PaginatedResult
893
+ */
894
+ async populateResult(result, fields, maxDepth) {
895
+ if (maxDepth <= 0) return result;
896
+ const populatedDocs = await this.populate({
897
+ data: result.docs,
898
+ fields,
899
+ currentDepth: 0,
900
+ maxDepth
901
+ });
902
+ return {
903
+ ...result,
904
+ docs: populatedDocs
834
905
  };
835
- exporter.getMetricsRequestHandler({}, response);
836
- });
837
- return { body, headers, statusCode };
838
- }
839
- function normalizeHeaderNames(headers) {
840
- return headers.map((value) => value.toLowerCase());
841
- }
842
- function uniqueValues(values) {
843
- return [...new Set(values)];
844
- }
845
- function clampRate(value) {
846
- if (Number.isNaN(value)) return 0;
847
- return Math.max(0, Math.min(1, value));
848
- }
849
- function deterministicSample(seed, rate) {
850
- if (rate >= 1) return true;
851
- if (rate <= 0) return false;
852
- let hash = 0;
853
- for (let index = 0; index < seed.length; index += 1) {
854
- hash = hash * 31 + seed.charCodeAt(index) >>> 0;
855
906
  }
856
- return hash / 4294967295 <= rate;
857
- }
858
- function toNumber(value) {
859
- if (!value) return void 0;
860
- const parsed = Number(value);
861
- return Number.isFinite(parsed) ? parsed : void 0;
862
- }
907
+ };
863
908
 
864
909
  // src/services/audit.service.ts
910
+ init_observability();
865
911
  var AuditService = class {
866
912
  /**
867
913
  * Writes a single entry to the __audit collection.
@@ -921,7 +967,6 @@ async function verifyPassword(plain, stored) {
921
967
 
922
968
  // src/auth/token.ts
923
969
  var import_jose = require("jose");
924
- var import_node_util2 = require("util");
925
970
  function getSecret() {
926
971
  const secret = process.env.DYRECTED_JWT_SECRET;
927
972
  if (!secret) {
@@ -929,7 +974,7 @@ function getSecret() {
929
974
  "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
930
975
  );
931
976
  }
932
- return new import_node_util2.TextEncoder().encode(secret);
977
+ return new TextEncoder().encode(secret);
933
978
  }
934
979
  var DEFAULT_EXPIRY = "7d";
935
980
  async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
@@ -951,7 +996,7 @@ function decodeCollectionToken(token) {
951
996
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
952
997
  async function issueAuthSessionToken(args) {
953
998
  const sessionId = createSessionId();
954
- const now = (/* @__PURE__ */ new Date()).toISOString();
999
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
955
1000
  const db = args.config.db;
956
1001
  if (db) {
957
1002
  await db.create({
@@ -964,7 +1009,7 @@ async function issueAuthSessionToken(args) {
964
1009
  authSource: args.authSource ?? "local",
965
1010
  providerId: args.providerId,
966
1011
  lastIp: args.ip,
967
- lastSeenAt: now,
1012
+ lastSeenAt: now2,
968
1013
  revokedAt: null,
969
1014
  expiresAt: null
970
1015
  }
@@ -989,7 +1034,7 @@ async function issueAuthSessionToken(args) {
989
1034
  id: sessionId,
990
1035
  data: {
991
1036
  expiresAt,
992
- lastSeenAt: now,
1037
+ lastSeenAt: now2,
993
1038
  lastIp: args.ip
994
1039
  }
995
1040
  });
@@ -1065,28 +1110,293 @@ function createSessionId() {
1065
1110
  }
1066
1111
 
1067
1112
  // src/utils/hooks.ts
1068
- var import_jexl3 = __toESM(require("jexl"), 1);
1113
+ var import_jexl4 = __toESM(require("jexl"), 1);
1069
1114
 
1070
1115
  // src/utils/declarative-hooks.ts
1116
+ var import_jexl3 = __toESM(require("jexl"), 1);
1117
+
1118
+ // src/utils/jexl-helpers.ts
1071
1119
  var import_jexl2 = __toESM(require("jexl"), 1);
1120
+ function slugify(val) {
1121
+ if (val === void 0 || val === null) return "";
1122
+ return String(val).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1123
+ }
1124
+ function lower(val) {
1125
+ if (val === void 0 || val === null) return "";
1126
+ return String(val).toLowerCase();
1127
+ }
1128
+ function upper(val) {
1129
+ if (val === void 0 || val === null) return "";
1130
+ return String(val).toUpperCase();
1131
+ }
1132
+ function trim(val) {
1133
+ if (val === void 0 || val === null) return "";
1134
+ return String(val).trim();
1135
+ }
1136
+ function capitalize(val) {
1137
+ if (val === void 0 || val === null) return "";
1138
+ const str = String(val).trim();
1139
+ if (!str) return "";
1140
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1141
+ }
1142
+ function truncate(val, length2, ellipsis = "...") {
1143
+ if (val === void 0 || val === null) return "";
1144
+ const str = String(val);
1145
+ const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
1146
+ if (str.length <= maxLen) return str;
1147
+ return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
1148
+ }
1149
+ function readingTime(val, wpm = 200) {
1150
+ if (val === void 0 || val === null) return 1;
1151
+ const str = String(val).trim();
1152
+ if (!str) return 1;
1153
+ const words = str.split(/\s+/).filter(Boolean).length;
1154
+ return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
1155
+ }
1156
+ function wordCount(val) {
1157
+ if (val === void 0 || val === null) return 0;
1158
+ const str = String(val).trim();
1159
+ if (!str) return 0;
1160
+ return str.split(/\s+/).filter(Boolean).length;
1161
+ }
1162
+ function replace(val, search, replacement) {
1163
+ if (val === void 0 || val === null) return "";
1164
+ return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
1165
+ }
1166
+ function startsWith(str, prefix) {
1167
+ if (str === void 0 || str === null) return false;
1168
+ return String(str).startsWith(String(prefix ?? ""));
1169
+ }
1170
+ function endsWith(str, suffix) {
1171
+ if (str === void 0 || str === null) return false;
1172
+ return String(str).endsWith(String(suffix ?? ""));
1173
+ }
1174
+ function now() {
1175
+ return (/* @__PURE__ */ new Date()).toISOString();
1176
+ }
1177
+ function today() {
1178
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1179
+ }
1180
+ function formatDate(val, style = "short") {
1181
+ if (!val) return "";
1182
+ const d = new Date(val);
1183
+ if (Number.isNaN(d.getTime())) return "";
1184
+ const styleStr = String(style).toLowerCase();
1185
+ if (styleStr === "iso" || styleStr === "date") return d.toISOString().slice(0, 10);
1186
+ if (styleStr === "datetime" || styleStr === "full") return d.toISOString();
1187
+ return d.toLocaleDateString("en-US", {
1188
+ year: "numeric",
1189
+ month: "short",
1190
+ day: "numeric"
1191
+ });
1192
+ }
1193
+ function addDays(val, days) {
1194
+ const d = val ? new Date(val) : /* @__PURE__ */ new Date();
1195
+ if (Number.isNaN(d.getTime())) return "";
1196
+ const daysNum = typeof days === "number" ? days : Number(days) || 0;
1197
+ d.setDate(d.getDate() + daysNum);
1198
+ return d.toISOString();
1199
+ }
1200
+ function diffDays(dateA, dateB) {
1201
+ const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
1202
+ const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
1203
+ if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
1204
+ const diffMs = Math.abs(dA.getTime() - dB.getTime());
1205
+ return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1206
+ }
1207
+ function isPast(val) {
1208
+ if (!val) return false;
1209
+ const d = new Date(val);
1210
+ if (Number.isNaN(d.getTime())) return false;
1211
+ return d.getTime() < Date.now();
1212
+ }
1213
+ function isFuture(val) {
1214
+ if (!val) return false;
1215
+ const d = new Date(val);
1216
+ if (Number.isNaN(d.getTime())) return false;
1217
+ return d.getTime() > Date.now();
1218
+ }
1219
+ function includes(arrOrStr, item) {
1220
+ if (Array.isArray(arrOrStr)) {
1221
+ return arrOrStr.includes(item);
1222
+ }
1223
+ if (typeof arrOrStr === "string") {
1224
+ return arrOrStr.includes(String(item ?? ""));
1225
+ }
1226
+ return false;
1227
+ }
1228
+ function join(arr, separator = ", ") {
1229
+ if (!Array.isArray(arr)) return "";
1230
+ return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
1231
+ }
1232
+ function first(arr) {
1233
+ if (Array.isArray(arr)) return arr[0] ?? null;
1234
+ return null;
1235
+ }
1236
+ function last(arr) {
1237
+ if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
1238
+ return null;
1239
+ }
1240
+ function compact(arr) {
1241
+ if (!Array.isArray(arr)) return [];
1242
+ return arr.filter((item) => item !== void 0 && item !== null && item !== "");
1243
+ }
1244
+ function unique(arr) {
1245
+ if (!Array.isArray(arr)) return [];
1246
+ return Array.from(new Set(arr));
1247
+ }
1248
+ function length(val) {
1249
+ if (Array.isArray(val) || typeof val === "string") return val.length;
1250
+ if (val && typeof val === "object") return Object.keys(val).length;
1251
+ return 0;
1252
+ }
1253
+ function round(val, decimals = 0) {
1254
+ const num = Number(val);
1255
+ if (Number.isNaN(num)) return 0;
1256
+ const dec = typeof decimals === "number" ? decimals : Number(decimals) || 0;
1257
+ const factor = Math.pow(10, dec);
1258
+ return Math.round(num * factor) / factor;
1259
+ }
1260
+ function clamp(val, minVal, maxVal) {
1261
+ const num = Number(val) || 0;
1262
+ const min = Number(minVal) || 0;
1263
+ const max = Number(maxVal) || 0;
1264
+ return Math.min(Math.max(num, min), max);
1265
+ }
1266
+ function defaultValue(val, fallback) {
1267
+ if (val === void 0 || val === null || val === "") return fallback;
1268
+ return val;
1269
+ }
1270
+ function coalesce(...args) {
1271
+ for (const arg of args) {
1272
+ if (arg !== void 0 && arg !== null && arg !== "") return arg;
1273
+ }
1274
+ return null;
1275
+ }
1276
+ function isEmpty(val) {
1277
+ if (val === void 0 || val === null || val === "") return true;
1278
+ if (Array.isArray(val)) return val.length === 0;
1279
+ if (typeof val === "object") return Object.keys(val).length === 0;
1280
+ return false;
1281
+ }
1282
+ function get(obj, path, fallback = null) {
1283
+ if (!obj || typeof obj !== "object") return fallback;
1284
+ const pathStr = String(path || "").trim();
1285
+ if (!pathStr) return fallback;
1286
+ const parts = pathStr.split(".");
1287
+ let curr = obj;
1288
+ for (const part of parts) {
1289
+ if (curr === void 0 || curr === null) return fallback;
1290
+ curr = curr[part];
1291
+ }
1292
+ return curr === void 0 || curr === null ? fallback : curr;
1293
+ }
1294
+ function registerJexlHelpers(jexlInstance = import_jexl2.default) {
1295
+ const helpers = {
1296
+ // Strings
1297
+ slugify,
1298
+ lower,
1299
+ upper,
1300
+ trim,
1301
+ capitalize,
1302
+ truncate,
1303
+ readingTime,
1304
+ wordCount,
1305
+ replace,
1306
+ startsWith,
1307
+ endsWith,
1308
+ // Dates
1309
+ now,
1310
+ today,
1311
+ formatDate,
1312
+ addDays,
1313
+ diffDays,
1314
+ isPast,
1315
+ isFuture,
1316
+ // Arrays
1317
+ includes,
1318
+ join,
1319
+ first,
1320
+ last,
1321
+ compact,
1322
+ unique,
1323
+ length,
1324
+ // Math
1325
+ round,
1326
+ clamp,
1327
+ // Objects & Logical
1328
+ default: defaultValue,
1329
+ coalesce,
1330
+ isEmpty,
1331
+ get
1332
+ };
1333
+ for (const [name, fn] of Object.entries(helpers)) {
1334
+ try {
1335
+ jexlInstance.addFunction(name, fn);
1336
+ } catch {
1337
+ }
1338
+ }
1339
+ }
1340
+ registerJexlHelpers(import_jexl2.default);
1341
+ var BUILTIN_JEXL_HELPERS = [
1342
+ // Strings
1343
+ "slugify",
1344
+ "lower",
1345
+ "upper",
1346
+ "trim",
1347
+ "capitalize",
1348
+ "truncate",
1349
+ "readingTime",
1350
+ "wordCount",
1351
+ "replace",
1352
+ "startsWith",
1353
+ "endsWith",
1354
+ // Dates
1355
+ "now",
1356
+ "today",
1357
+ "formatDate",
1358
+ "addDays",
1359
+ "diffDays",
1360
+ "isPast",
1361
+ "isFuture",
1362
+ // Arrays
1363
+ "includes",
1364
+ "join",
1365
+ "first",
1366
+ "last",
1367
+ "compact",
1368
+ "unique",
1369
+ "length",
1370
+ // Math
1371
+ "round",
1372
+ "clamp",
1373
+ // Objects & Logical
1374
+ "default",
1375
+ "coalesce",
1376
+ "isEmpty",
1377
+ "get"
1378
+ ];
1379
+
1380
+ // src/utils/declarative-hooks.ts
1381
+ registerJexlHelpers(import_jexl3.default);
1072
1382
  var SURFACE_ROOTS = {
1073
- "collection.beforeRead": ["req", "user", "query"],
1074
- "collection.afterRead": ["req", "user", "doc"],
1075
- "collection.beforeChange": ["req", "user", "data", "doc", "operation"],
1076
- "global.beforeRead": ["req", "user", "query"],
1077
- "global.afterRead": ["req", "user", "doc"],
1078
- "global.beforeChange": ["req", "user", "data", "doc", "operation"],
1079
- "field.beforeChange": ["value", "data", "originalDoc", "user"],
1080
- "field.admin.onChange": ["value", "siblingData", "data"]
1383
+ "collection.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1384
+ "collection.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1385
+ "collection.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1386
+ "global.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1387
+ "global.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1388
+ "global.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1389
+ "field.beforeChange": ["value", "data", "originalDoc", "user", ...BUILTIN_JEXL_HELPERS],
1390
+ "field.admin.onChange": ["value", "siblingData", "data", ...BUILTIN_JEXL_HELPERS]
1081
1391
  };
1082
- var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config"];
1083
- var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id"];
1084
- var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id"];
1392
+ var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config", ...BUILTIN_JEXL_HELPERS];
1393
+ var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id", ...BUILTIN_JEXL_HELPERS];
1394
+ var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id", ...BUILTIN_JEXL_HELPERS];
1085
1395
  var COMPILE_CACHE = /* @__PURE__ */ new Map();
1086
1396
  function compileExpression(expression) {
1087
1397
  const cached = COMPILE_CACHE.get(expression);
1088
1398
  if (cached) return cached;
1089
- const compiled = import_jexl2.default.compile(expression);
1399
+ const compiled = import_jexl3.default.compile(expression);
1090
1400
  const next = { ast: compiled._ast };
1091
1401
  COMPILE_CACHE.set(expression, next);
1092
1402
  return next;
@@ -1609,13 +1919,16 @@ function collectConfigDiagnostics(config) {
1609
1919
  function formatConfigDiagnostics(source, diagnostics, options = {}) {
1610
1920
  const color = options.color ?? false;
1611
1921
  const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
1922
+ 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)}`;
1612
1923
  const lines = diagnostics.flatMap((issue, index) => [
1613
1924
  `${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
1614
1925
  ` ${issue.message}`,
1615
1926
  ` ${colorize("surface:", 90, color)} ${issue.surface}`
1616
1927
  ]);
1617
1928
  return `${header}
1618
- ${lines.join("\n")}`;
1929
+ ${lines.join("\n")}
1930
+
1931
+ ${docLink}`;
1619
1932
  }
1620
1933
  var ConfigValidationError = class extends Error {
1621
1934
  diagnostics;
@@ -1673,7 +1986,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1673
1986
  options.path ?? options.surface
1674
1987
  );
1675
1988
  }
1676
- return import_jexl3.default.eval(hook, hookArgs);
1989
+ return import_jexl4.default.eval(hook, hookArgs);
1677
1990
  })() : await hook(hookArgs);
1678
1991
  if (result !== void 0) {
1679
1992
  if (typeof hook === "string" && options.surface) {
@@ -1690,10 +2003,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1690
2003
  }
1691
2004
  } catch (err) {
1692
2005
  if (options.isolated) {
1693
- getConfigLogger(void 0, "hooks").error({
1694
- err,
1695
- msg: "Side-effect hook failed after a successful write"
1696
- });
2006
+ console.error("Side-effect hook failed after a successful write", err);
1697
2007
  } else {
1698
2008
  throw err;
1699
2009
  }
@@ -1725,7 +2035,7 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pat
1725
2035
  "field.beforeChange",
1726
2036
  fieldPath
1727
2037
  );
1728
- return import_jexl3.default.eval(hook, hookArgs);
2038
+ return import_jexl4.default.eval(hook, hookArgs);
1729
2039
  })() : await hook(hookArgs);
1730
2040
  }
1731
2041
  result[field.name] = updatedValue;
@@ -1903,22 +2213,18 @@ function validateUpload(file, config) {
1903
2213
  }
1904
2214
 
1905
2215
  // src/auth/jexl.ts
1906
- var import_jexl4 = __toESM(require("jexl"), 1);
2216
+ var import_jexl5 = __toESM(require("jexl"), 1);
1907
2217
  async function evaluateAccess(expression, context) {
1908
2218
  if (expression === void 0 || expression === null) return false;
1909
2219
  if (typeof expression === "boolean") return expression;
1910
2220
  try {
1911
- const result = await import_jexl4.default.eval(expression, context);
2221
+ const result = await import_jexl5.default.eval(expression, context);
1912
2222
  if (result && typeof result === "object" && !Array.isArray(result)) {
1913
2223
  return result;
1914
2224
  }
1915
2225
  return !!result;
1916
2226
  } catch (err) {
1917
- getConfigLogger(context.config, "access").error({
1918
- err,
1919
- msg: "Jexl evaluation failed",
1920
- expression
1921
- });
2227
+ console.error("[dyrected:access] Failed to evaluate access expression:", err);
1922
2228
  return false;
1923
2229
  }
1924
2230
  }
@@ -1936,20 +2242,14 @@ async function resolveAccess(config, access, args) {
1936
2242
  try {
1937
2243
  return await access(args);
1938
2244
  } catch (err) {
1939
- getConfigLogger(config, "access").error({
1940
- err,
1941
- msg: "Functional access check failed"
1942
- });
2245
+ console.error("[dyrected:access] Functional access check failed:", err);
1943
2246
  return false;
1944
2247
  }
1945
2248
  }
1946
2249
  if (isNamedAccessPolicy(access)) {
1947
2250
  const policy = config.accessPolicies?.[access.policy];
1948
2251
  if (policy === void 0) {
1949
- getConfigLogger(config, "access").error({
1950
- msg: "Unknown access policy",
1951
- policy: access.policy
1952
- });
2252
+ console.error(`[dyrected:access] Unknown access policy "${access.policy}"`);
1953
2253
  return false;
1954
2254
  }
1955
2255
  if (typeof policy === "string" || typeof policy === "boolean") {
@@ -1959,11 +2259,7 @@ async function resolveAccess(config, access, args) {
1959
2259
  const resolver = policy;
1960
2260
  return await resolver({ ...args, params: access.params });
1961
2261
  } catch (err) {
1962
- getConfigLogger(config, "access").error({
1963
- err,
1964
- msg: "Access policy failed",
1965
- policy: access.policy
1966
- });
2262
+ console.error(`[dyrected:access] Access policy "${access.policy}" failed:`, err);
1967
2263
  return false;
1968
2264
  }
1969
2265
  }
@@ -2383,7 +2679,7 @@ function simplePublishingWorkflow() {
2383
2679
  { name: "published", label: "Published", color: "success", published: true }
2384
2680
  ],
2385
2681
  transitions: [
2386
- { name: "publish", label: "Publish", from: "draft", to: "published" },
2682
+ { name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
2387
2683
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
2388
2684
  ]
2389
2685
  // Intentionally no `roles`: this lightweight workflow (synthesized from
@@ -2490,7 +2786,11 @@ async function dispatchLifecycleEvent(config, event) {
2490
2786
  const retryDelayMs = config.events.retryDelayMs ?? 1e3;
2491
2787
  const attempts = event.attempts + 1;
2492
2788
  try {
2493
- await db.update({ collection: LIFECYCLE_EVENTS_COLLECTION, id: event.id, data: { status: "processing", attempts } });
2789
+ await db.update({
2790
+ collection: LIFECYCLE_EVENTS_COLLECTION,
2791
+ id: event.id,
2792
+ data: { status: "processing", attempts }
2793
+ });
2494
2794
  for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
2495
2795
  await db.update({
2496
2796
  collection: LIFECYCLE_EVENTS_COLLECTION,
@@ -2581,12 +2881,16 @@ async function transitionWorkflow(args) {
2581
2881
  const meta = original.__workflow;
2582
2882
  if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
2583
2883
  if (expectedRevision !== void 0 && expectedRevision !== meta.revision) {
2584
- throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
2884
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
2885
+ statusCode: 409
2886
+ });
2585
2887
  }
2586
2888
  const transition = workflow.transitions.find((item) => item.name === transitionName);
2587
2889
  const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
2588
2890
  if (!transition || !fromStates.includes(meta.state)) {
2589
- throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), { statusCode: 409 });
2891
+ throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
2892
+ statusCode: 409
2893
+ });
2590
2894
  }
2591
2895
  if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
2592
2896
  throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
@@ -2596,32 +2900,52 @@ async function transitionWorkflow(args) {
2596
2900
  }
2597
2901
  const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
2598
2902
  for (const hook of workflow.hooks?.beforeTransition ?? []) await hook(hookContext);
2599
- const events = [createLifecycleEvent({
2600
- name: "workflow.transitioned",
2601
- collection: collection.slug,
2602
- documentId: id,
2603
- actorId: user?.sub,
2604
- payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
2605
- })];
2903
+ const events = [
2904
+ createLifecycleEvent({
2905
+ name: "workflow.transitioned",
2906
+ collection: collection.slug,
2907
+ documentId: id,
2908
+ actorId: user?.sub,
2909
+ payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
2910
+ })
2911
+ ];
2606
2912
  const targetState = workflow.states.find((state) => state.name === transition.to);
2607
2913
  if (targetState.published) {
2608
- events.push(createLifecycleEvent({ name: "entry.published", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.revision } }));
2914
+ events.push(
2915
+ createLifecycleEvent({
2916
+ name: "entry.published",
2917
+ collection: collection.slug,
2918
+ documentId: id,
2919
+ actorId: user?.sub,
2920
+ payload: { revision: meta.revision }
2921
+ })
2922
+ );
2609
2923
  } else if (transition.unpublish) {
2610
- events.push(createLifecycleEvent({ name: "entry.unpublished", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.publishedRevision } }));
2924
+ events.push(
2925
+ createLifecycleEvent({
2926
+ name: "entry.unpublished",
2927
+ collection: collection.slug,
2928
+ documentId: id,
2929
+ actorId: user?.sub,
2930
+ payload: { revision: meta.publishedRevision }
2931
+ })
2932
+ );
2611
2933
  }
2612
2934
  const updated = await db.transaction(async (tx) => {
2613
2935
  const locked = await tx.findOne({ collection: collection.slug, id });
2614
2936
  if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
2615
2937
  const lockedMeta = locked.__workflow;
2616
2938
  if (lockedMeta.revision !== meta.revision || lockedMeta.state !== meta.state || expectedRevision !== void 0 && lockedMeta.revision !== expectedRevision) {
2617
- throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), { statusCode: 409 });
2939
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
2940
+ statusCode: 409
2941
+ });
2618
2942
  }
2619
- const now = (/* @__PURE__ */ new Date()).toISOString();
2943
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
2620
2944
  const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
2621
2945
  const nextMeta = {
2622
2946
  ...lockedMeta,
2623
2947
  state: transition.to,
2624
- ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now, publishedBy: user?.sub } : {},
2948
+ ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
2625
2949
  ...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
2626
2950
  };
2627
2951
  const data = { __workflow: nextMeta };
@@ -2630,11 +2954,36 @@ async function transitionWorkflow(args) {
2630
2954
  const next = await tx.update({ collection: collection.slug, id, data });
2631
2955
  await tx.create({
2632
2956
  collection: WORKFLOW_HISTORY_COLLECTION,
2633
- 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 }
2957
+ data: {
2958
+ collection: collection.slug,
2959
+ documentId: id,
2960
+ transition: transition.name,
2961
+ from: lockedMeta.state,
2962
+ to: transition.to,
2963
+ revision: lockedMeta.revision,
2964
+ comment: comment ?? null,
2965
+ actorId: user?.sub ?? null,
2966
+ createdAt: now2
2967
+ }
2634
2968
  });
2635
2969
  for (const event of events) await persistEvent(tx, event);
2636
2970
  if (collection.audit) {
2637
- 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 }) } });
2971
+ await tx.create({
2972
+ collection: "__audit",
2973
+ data: {
2974
+ collection: collection.slug,
2975
+ documentId: id,
2976
+ operation: "workflow.transition",
2977
+ user: user?.sub ?? null,
2978
+ timestamp: now2,
2979
+ changes: JSON.stringify({
2980
+ transition: transition.name,
2981
+ from: lockedMeta.state,
2982
+ to: transition.to,
2983
+ revision: lockedMeta.revision
2984
+ })
2985
+ }
2986
+ });
2638
2987
  }
2639
2988
  return next;
2640
2989
  });
@@ -2643,23 +2992,29 @@ async function transitionWorkflow(args) {
2643
2992
  try {
2644
2993
  await hook({ ...hookContext, doc: updated, event: events[0] });
2645
2994
  } catch (error) {
2646
- getObservabilityRuntime(config)?.recordWorkflowHookFailure({
2647
- collection: collection.slug,
2648
- transition: transition.name
2649
- });
2650
- getConfigLogger(config, "workflow").error({
2651
- err: error,
2652
- msg: "afterTransition hook failed",
2653
- collection: collection.slug,
2654
- transition: transition.name,
2655
- documentId: id
2656
- });
2995
+ try {
2996
+ const { getConfigLogger: getConfigLogger2, getObservabilityRuntime: getObservabilityRuntime2 } = await Promise.resolve().then(() => (init_observability(), observability_exports));
2997
+ getObservabilityRuntime2(config)?.recordWorkflowHookFailure({
2998
+ collection: collection.slug,
2999
+ transition: transition.name
3000
+ });
3001
+ getConfigLogger2(config, "workflow").error({
3002
+ err: error,
3003
+ msg: "afterTransition hook failed",
3004
+ collection: collection.slug,
3005
+ transition: transition.name,
3006
+ documentId: id
3007
+ });
3008
+ } catch {
3009
+ console.error("afterTransition hook failed:", error);
3010
+ }
2657
3011
  }
2658
3012
  }
2659
3013
  return updated;
2660
3014
  }
2661
3015
 
2662
3016
  // src/controllers/collection.controller.ts
3017
+ init_observability();
2663
3018
  var CollectionController = class {
2664
3019
  collection;
2665
3020
  constructor(collection) {
@@ -3029,11 +3384,11 @@ var CollectionController = class {
3029
3384
  }
3030
3385
  const body = await c.req.json();
3031
3386
  const user = c.get("user");
3032
- const now = (/* @__PURE__ */ new Date()).toISOString();
3387
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3033
3388
  let data = {
3034
3389
  ...body,
3035
- createdAt: now,
3036
- updatedAt: now,
3390
+ createdAt: now2,
3391
+ updatedAt: now2,
3037
3392
  createdBy: user?.sub ?? null,
3038
3393
  updatedBy: user?.sub ?? null
3039
3394
  };
@@ -3171,12 +3526,12 @@ var CollectionController = class {
3171
3526
  }
3172
3527
  });
3173
3528
  const user = c.get("user");
3174
- const now = (/* @__PURE__ */ new Date()).toISOString();
3529
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3175
3530
  let data = {
3176
3531
  ...otherData,
3177
3532
  ...fileData,
3178
- createdAt: now,
3179
- updatedAt: now,
3533
+ createdAt: now2,
3534
+ updatedAt: now2,
3180
3535
  createdBy: user?.sub ?? null,
3181
3536
  updatedBy: user?.sub ?? null
3182
3537
  };
@@ -3770,6 +4125,7 @@ var CollectionController = class {
3770
4125
  };
3771
4126
 
3772
4127
  // src/controllers/global.controller.ts
4128
+ init_observability();
3773
4129
  var GlobalController = class {
3774
4130
  global;
3775
4131
  constructor(global) {
@@ -3792,8 +4148,8 @@ var GlobalController = class {
3792
4148
  path: `global:${this.global.slug}.hooks.beforeRead`
3793
4149
  });
3794
4150
  let data = await db.getGlobal({ slug: this.global.slug });
3795
- const isEmpty = !data || isFunctionallyEmpty(data);
3796
- if (isEmpty && this.global.initialData) {
4151
+ const isEmpty2 = !data || isFunctionallyEmpty(data);
4152
+ if (isEmpty2 && this.global.initialData) {
3797
4153
  getRequestLogger(c, "global").info({
3798
4154
  msg: "Auto-seeding global from config.initialData",
3799
4155
  global: this.global.slug
@@ -4105,6 +4461,7 @@ function mergeDynamicConfig(baseConfig, dynamic) {
4105
4461
  }
4106
4462
 
4107
4463
  // src/controllers/media.controller.ts
4464
+ init_observability();
4108
4465
  var MediaController = class {
4109
4466
  collection;
4110
4467
  loggerComponent = "media";
@@ -4428,6 +4785,7 @@ function layout({ preheader, title, content, footer }) {
4428
4785
  }
4429
4786
 
4430
4787
  // src/services/email.service.ts
4788
+ init_observability();
4431
4789
  var _devSend = null;
4432
4790
  var _devSendPromise = null;
4433
4791
  async function getDevSend() {
@@ -4548,6 +4906,7 @@ function buildPasswordChangedEmail(config, args) {
4548
4906
  }
4549
4907
 
4550
4908
  // src/controllers/auth.controller.ts
4909
+ init_observability();
4551
4910
  var AuthController = class {
4552
4911
  collection;
4553
4912
  constructor(collection) {
@@ -4612,13 +4971,13 @@ var AuthController = class {
4612
4971
  if (!lockout.enabled) {
4613
4972
  return { justLocked: false, retryAfterSeconds: null };
4614
4973
  }
4615
- const now = Date.now();
4974
+ const now2 = Date.now();
4616
4975
  const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
4617
- const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now;
4976
+ const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now2;
4618
4977
  const currentAttempts = !lockExpired && typeof user.loginAttempts === "number" && user.loginAttempts > 0 ? user.loginAttempts : 0;
4619
4978
  const loginAttempts = currentAttempts + 1;
4620
4979
  const justLocked = loginAttempts >= lockout.maxLoginAttempts;
4621
- const nextLockedUntil = justLocked ? new Date(now + lockout.lockTime).toISOString() : null;
4980
+ const nextLockedUntil = justLocked ? new Date(now2 + lockout.lockTime).toISOString() : null;
4622
4981
  await db.update({
4623
4982
  collection: this.collection.slug,
4624
4983
  id: String(user.id),
@@ -5152,7 +5511,7 @@ var AuthController = class {
5152
5511
 
5153
5512
  // src/controllers/admin-auth.controller.ts
5154
5513
  var import_node_crypto3 = require("crypto");
5155
- var import_node_util3 = require("util");
5514
+ var import_node_util2 = require("util");
5156
5515
  var import_jose2 = require("jose");
5157
5516
  var AdminAuthController = class {
5158
5517
  constructor(config) {
@@ -5533,7 +5892,7 @@ var AdminAuthController = class {
5533
5892
  }
5534
5893
  let claims;
5535
5894
  if (provider.secret) {
5536
- const secret = new import_node_util3.TextEncoder().encode(provider.secret);
5895
+ const secret = new import_node_util2.TextEncoder().encode(provider.secret);
5537
5896
  const verified = await (0, import_jose2.jwtVerify)(token, secret, {
5538
5897
  issuer: provider.issuer,
5539
5898
  audience: provider.audience
@@ -5649,7 +6008,7 @@ var AdminAuthController = class {
5649
6008
  if (!secret) {
5650
6009
  throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
5651
6010
  }
5652
- return new import_node_util3.TextEncoder().encode(secret);
6011
+ return new import_node_util2.TextEncoder().encode(secret);
5653
6012
  }
5654
6013
  normalizeReturnTo(returnTo, c) {
5655
6014
  if (returnTo) return returnTo;
@@ -5660,11 +6019,11 @@ var AdminAuthController = class {
5660
6019
 
5661
6020
  // src/controllers/preview.controller.ts
5662
6021
  var import_jose3 = require("jose");
5663
- var import_node_util4 = require("util");
6022
+ var import_node_util3 = require("util");
5664
6023
  var PreviewController = class {
5665
6024
  getSecret() {
5666
6025
  const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
5667
- return new import_node_util4.TextEncoder().encode(secret);
6026
+ return new import_node_util3.TextEncoder().encode(secret);
5668
6027
  }
5669
6028
  /**
5670
6029
  * POST /api/preview-token
@@ -5873,6 +6232,7 @@ var AuditController = class {
5873
6232
  };
5874
6233
 
5875
6234
  // src/middleware/auth.ts
6235
+ init_observability();
5876
6236
  function getBearerToken(c) {
5877
6237
  const authHeader = c.req.header("Authorization");
5878
6238
  return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
@@ -7013,6 +7373,7 @@ function getSwaggerHtml(specUrl) {
7013
7373
  }
7014
7374
 
7015
7375
  // src/router.ts
7376
+ init_observability();
7016
7377
  var SERIALIZED_ADMIN_HOOK_PREFIX = "__dyrected_fn__:";
7017
7378
  function accessGate(config, target, action) {
7018
7379
  return async (c, next) => {
@@ -8182,15 +8543,15 @@ function createRateLimitMiddleware(config) {
8182
8543
  })) {
8183
8544
  return next();
8184
8545
  }
8185
- const now = Date.now();
8546
+ const now2 = Date.now();
8186
8547
  const key = ip;
8187
8548
  const existing = buckets.get(key);
8188
- const bucket = existing && existing.resetAt > now ? existing : { count: 0, resetAt: now + settings.window };
8549
+ const bucket = existing && existing.resetAt > now2 ? existing : { count: 0, resetAt: now2 + settings.window };
8189
8550
  bucket.count += 1;
8190
8551
  buckets.set(key, bucket);
8191
- pruneExpiredBuckets(buckets, now);
8552
+ pruneExpiredBuckets(buckets, now2);
8192
8553
  const remaining = Math.max(0, settings.max - bucket.count);
8193
- const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
8554
+ const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now2) / 1e3));
8194
8555
  c.res.headers.set("X-RateLimit-Limit", String(settings.max));
8195
8556
  c.res.headers.set("X-RateLimit-Remaining", String(remaining));
8196
8557
  c.res.headers.set("X-RateLimit-Reset", String(Math.ceil(bucket.resetAt / 1e3)));
@@ -8221,16 +8582,17 @@ function normalizeRateLimitConfig(config) {
8221
8582
  function matchesLimitedPath(path, prefixes) {
8222
8583
  return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
8223
8584
  }
8224
- function pruneExpiredBuckets(buckets, now) {
8585
+ function pruneExpiredBuckets(buckets, now2) {
8225
8586
  if (buckets.size < 1e3) return;
8226
8587
  for (const [key, bucket] of buckets) {
8227
- if (bucket.resetAt <= now) {
8588
+ if (bucket.resetAt <= now2) {
8228
8589
  buckets.delete(key);
8229
8590
  }
8230
8591
  }
8231
8592
  }
8232
8593
 
8233
8594
  // src/app.ts
8595
+ init_observability();
8234
8596
  async function createDyrectedApp(rawConfig) {
8235
8597
  const config = normalizeConfig(rawConfig);
8236
8598
  const observability = createObservabilityRuntime(config);
@@ -8382,6 +8744,9 @@ async function createDyrectedApp(rawConfig) {
8382
8744
  registerRoutes(app, config);
8383
8745
  return app;
8384
8746
  }
8747
+
8748
+ // src/server.ts
8749
+ init_observability();
8385
8750
  // Annotate the CommonJS export names for ESM import in node:
8386
8751
  0 && (module.exports = {
8387
8752
  AuthController,
@@ -8389,6 +8754,21 @@ async function createDyrectedApp(rawConfig) {
8389
8754
  GlobalController,
8390
8755
  MediaController,
8391
8756
  PreviewController,
8757
+ attachRequestMetrics,
8758
+ bindObservabilityRuntime,
8759
+ buildRequestLogger,
8760
+ captureRequestBody,
8392
8761
  createDyrectedApp,
8393
- registerRoutes
8762
+ createObservabilityRuntime,
8763
+ createRequestSpan,
8764
+ endRequestSpan,
8765
+ getConfigLogger,
8766
+ getObservabilityRuntime,
8767
+ getRequestLogger,
8768
+ redactBody,
8769
+ redactHeaders,
8770
+ registerRoutes,
8771
+ renderPrometheusMetrics,
8772
+ resolveObservabilityConfig,
8773
+ shouldSampleRequest
8394
8774
  });