@dyrected/core 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/app-config-7G-3tF_I.d.cts +2259 -0
  2. package/dist/app-config-7G-3tF_I.d.ts +2259 -0
  3. package/dist/app-config-B3ByBjCN.d.cts +2222 -0
  4. package/dist/app-config-B3ByBjCN.d.ts +2222 -0
  5. package/dist/app-config-BIkjQWzU.d.cts +2218 -0
  6. package/dist/app-config-BIkjQWzU.d.ts +2218 -0
  7. package/dist/app-config-BfpeE3bF.d.cts +2100 -0
  8. package/dist/app-config-BfpeE3bF.d.ts +2100 -0
  9. package/dist/app-config-CCe-yq9q.d.cts +2222 -0
  10. package/dist/app-config-CCe-yq9q.d.ts +2222 -0
  11. package/dist/app-config-CPOs7THF.d.cts +2217 -0
  12. package/dist/app-config-CPOs7THF.d.ts +2217 -0
  13. package/dist/app-config-Ctp454zB.d.cts +2214 -0
  14. package/dist/app-config-Ctp454zB.d.ts +2214 -0
  15. package/dist/app-config-DJmNYgYh.d.cts +2305 -0
  16. package/dist/app-config-DJmNYgYh.d.ts +2305 -0
  17. package/dist/app-config-DO3laV58.d.cts +2397 -0
  18. package/dist/app-config-DO3laV58.d.ts +2397 -0
  19. package/dist/app-config-DZf_mmi9.d.cts +2211 -0
  20. package/dist/app-config-DZf_mmi9.d.ts +2211 -0
  21. package/dist/app-config-SkUoN5JU.d.cts +2192 -0
  22. package/dist/app-config-SkUoN5JU.d.ts +2192 -0
  23. package/dist/app-config-oz8H_MOX.d.cts +2224 -0
  24. package/dist/app-config-oz8H_MOX.d.ts +2224 -0
  25. package/dist/chunk-2UXLA4YN.js +3139 -0
  26. package/dist/chunk-3TRRTN6U.js +3441 -0
  27. package/dist/chunk-5U2WRCNH.js +3140 -0
  28. package/dist/chunk-5ZIA5COH.js +3414 -0
  29. package/dist/chunk-6TWQ6GTD.js +3480 -0
  30. package/dist/chunk-AKYYGMFS.js +3365 -0
  31. package/dist/chunk-AQO4HI2Y.js +3204 -0
  32. package/dist/chunk-DFBLNYUH.js +3402 -0
  33. package/dist/chunk-EJG6AMYB.js +3537 -0
  34. package/dist/chunk-FRSJJO27.js +519 -0
  35. package/dist/chunk-G43REXPX.js +3147 -0
  36. package/dist/chunk-GFKBPBJJ.js +3637 -0
  37. package/dist/chunk-KEKOEAX6.js +3139 -0
  38. package/dist/chunk-LFGSRQLX.js +3568 -0
  39. package/dist/chunk-NNZRYF3U.js +3537 -0
  40. package/dist/chunk-PRQ2RF5C.js +3485 -0
  41. package/dist/chunk-PYE2JSY4.js +3568 -0
  42. package/dist/chunk-PZMDK3MH.js +3203 -0
  43. package/dist/chunk-QCXLDKZO.js +3272 -0
  44. package/dist/chunk-SM32ND5M.js +3472 -0
  45. package/dist/chunk-UKUEKTAO.js +513 -0
  46. package/dist/chunk-VM43G3IG.js +506 -0
  47. package/dist/chunk-VWJKFZQU.js +3139 -0
  48. package/dist/chunk-XX7N64NL.js +3550 -0
  49. package/dist/chunk-YSSHLGQ6.js +3139 -0
  50. package/dist/chunk-Z7CFUYVP.js +3653 -0
  51. package/dist/index.cjs +1421 -82
  52. package/dist/index.d.cts +362 -3
  53. package/dist/index.d.ts +362 -3
  54. package/dist/index.js +119 -1
  55. package/dist/observability-CNM4QFE4.js +34 -0
  56. package/dist/observability-KCMTV7GO.js +34 -0
  57. package/dist/observability-L5BORBB2.js +34 -0
  58. package/dist/server.cjs +1879 -1247
  59. package/dist/server.d.cts +52 -8
  60. package/dist/server.d.ts +52 -8
  61. package/dist/server.js +449 -387
  62. package/package.json +1 -1
package/dist/server.cjs CHANGED
@@ -30,838 +30,885 @@ 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
+ serializeDetailForApi: () => serializeDetailForApi,
631
+ shouldSampleRequest: () => shouldSampleRequest
632
+ });
633
+ module.exports = __toCommonJS(server_exports);
634
+
635
+ // src/app.ts
636
+ var import_hono = require("hono");
637
+ var import_cors = require("hono/cors");
638
+ var import_request_id = require("hono/request-id");
639
+
640
+ // src/services/defaults.service.ts
641
+ var DefaultsService = class {
642
+ /**
643
+ * Recursively apply default values to a data object based on field definitions.
644
+ */
645
+ static apply(fields, data = {}) {
646
+ const result = { ...data || {} };
647
+ fields.forEach((field) => {
648
+ if (field.type === "join") return;
649
+ if (field.type === "row" && field.fields) {
650
+ Object.assign(result, this.apply(field.fields, data));
651
+ return;
652
+ }
653
+ if (!field.name) return;
654
+ let value = result[field.name];
655
+ if ((value === void 0 || value === null) && field.renameTo) {
656
+ const legacyValue = result[field.renameTo];
657
+ if (legacyValue !== void 0 && legacyValue !== null) {
658
+ value = legacyValue;
659
+ result[field.name] = legacyValue;
660
+ }
661
+ }
662
+ if (value === void 0 || value === null) {
663
+ if (field.defaultValue !== void 0) {
664
+ result[field.name] = field.defaultValue;
665
+ } else {
666
+ if (field.type === "boolean") result[field.name] = false;
667
+ else if (field.type === "array") result[field.name] = [];
668
+ else if (field.type === "multiSelect") result[field.name] = [];
669
+ else if (field.type === "object") {
670
+ result[field.name] = this.apply(field.fields || [], {});
671
+ }
672
+ }
673
+ } else if (field.type === "object" && field.fields) {
674
+ result[field.name] = this.apply(field.fields, value);
675
+ } else if (field.type === "array" && field.fields && Array.isArray(value)) {
676
+ result[field.name] = value.map((item) => this.apply(field.fields, item));
677
+ }
588
678
  });
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
- );
679
+ return result;
612
680
  }
613
681
  };
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
- };
682
+
683
+ // src/services/population.service.ts
684
+ var import_jexl = __toESM(require("jexl"), 1);
685
+ var PopulationService = class {
686
+ db;
687
+ collections;
688
+ constructor(db, collections) {
689
+ this.db = db;
690
+ this.collections = collections;
712
691
  }
713
- try {
714
- const text = await request.clone().text();
715
- if (!text) {
716
- return { attempted: true, contentType, contentLength };
692
+ /**
693
+ * Recursively populate relationship fields in a document or array of documents.
694
+ */
695
+ async populate(args) {
696
+ const { data, fields, currentDepth = 0, maxDepth = 10 } = args;
697
+ if (currentDepth >= maxDepth || !data) {
698
+ return data;
717
699
  }
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
- };
700
+ if (Array.isArray(data)) {
701
+ return Promise.all(
702
+ data.map((item) => this.populate({ ...args, data: item }))
703
+ );
737
704
  }
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);
705
+ const populatedDoc = { ...data };
706
+ for (const field of fields) {
707
+ if (field.type === "join" && field.collection && field.on && currentDepth < maxDepth) {
708
+ const targetCollection = this.collections.find(
709
+ (c) => c.slug === field.collection
710
+ );
711
+ if (targetCollection) {
712
+ const docId = populatedDoc.id;
713
+ if (docId) {
714
+ const where = { [field.on]: { equals: docId } };
715
+ const joinLimit = field.limit ?? 10;
716
+ const result = await this.db.find({
717
+ collection: field.collection,
718
+ where,
719
+ limit: joinLimit
720
+ });
721
+ const populatedDocs = await this.populate({
722
+ data: result.docs.map(
723
+ (doc) => DefaultsService.apply(targetCollection.fields, doc)
724
+ ),
725
+ fields: targetCollection.fields,
726
+ currentDepth: currentDepth + 1,
727
+ maxDepth
728
+ });
729
+ populatedDoc[field.name] = {
730
+ docs: populatedDocs,
731
+ hasNextPage: result.page * result.limit < result.total,
732
+ totalDocs: result.total
733
+ };
734
+ }
735
+ }
736
+ continue;
763
737
  }
764
- }
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)));
804
- }
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();
738
+ if (field.type === "row" && field.fields) {
739
+ const rowPopulated = await this.populate({
740
+ data,
741
+ fields: field.fields,
742
+ currentDepth,
743
+ maxDepth
744
+ });
745
+ Object.assign(populatedDoc, rowPopulated);
746
+ continue;
747
+ }
748
+ if (!field.name) continue;
749
+ const value = populatedDoc[field.name];
750
+ if (field.type === "relationship" && field.relationTo && value) {
751
+ const relatedCollection = this.collections.find(
752
+ (c) => c.slug === field.relationTo
753
+ );
754
+ if (!relatedCollection) continue;
755
+ if (Array.isArray(value)) {
756
+ populatedDoc[field.name] = await Promise.all(
757
+ value.map(async (id) => {
758
+ if (!id) return id;
759
+ let doc = id;
760
+ if (typeof id === "string") {
761
+ doc = await this.db.findOne({
762
+ collection: field.relationTo,
763
+ id
764
+ });
765
+ }
766
+ if (!doc || typeof doc !== "object") return id;
767
+ const docWithDefaults = DefaultsService.apply(
768
+ relatedCollection.fields,
769
+ doc
770
+ );
771
+ return this.populate({
772
+ data: docWithDefaults,
773
+ fields: relatedCollection.fields,
774
+ currentDepth: currentDepth + 1,
775
+ maxDepth
776
+ });
777
+ })
778
+ );
779
+ } else if (value) {
780
+ let doc = value;
781
+ if (typeof value === "string") {
782
+ doc = await this.db.findOne({
783
+ collection: field.relationTo,
784
+ id: value
785
+ });
786
+ }
787
+ if (doc && typeof doc === "object") {
788
+ const docWithDefaults = DefaultsService.apply(
789
+ relatedCollection.fields,
790
+ doc
791
+ );
792
+ populatedDoc[field.name] = await this.populate({
793
+ data: docWithDefaults,
794
+ fields: relatedCollection.fields,
795
+ currentDepth: currentDepth + 1,
796
+ maxDepth
797
+ });
798
+ }
799
+ }
800
+ }
801
+ if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
802
+ const relatedCollection = this.collections.find(
803
+ (c) => c.slug === value.relationTo
804
+ );
805
+ if (relatedCollection) {
806
+ const doc = await this.db.findOne({
807
+ collection: value.relationTo,
808
+ id: value.value
809
+ });
810
+ if (doc && typeof doc === "object") {
811
+ const docWithDefaults = DefaultsService.apply(
812
+ relatedCollection.fields,
813
+ doc
814
+ );
815
+ const populatedDocValue = await this.populate({
816
+ data: docWithDefaults,
817
+ fields: relatedCollection.fields,
818
+ currentDepth: currentDepth + 1,
819
+ maxDepth
820
+ });
821
+ const identifier = docWithDefaults.slug || docWithDefaults.id;
822
+ let resolvedUrl = `/collections/${value.relationTo}/${identifier}`;
823
+ const previewUrlPattern = relatedCollection.admin?.previewUrl || relatedCollection.admin?.urlPattern;
824
+ if (previewUrlPattern) {
825
+ if (typeof previewUrlPattern === "function") {
826
+ try {
827
+ resolvedUrl = previewUrlPattern(docWithDefaults, { locale: "en" }) || resolvedUrl;
828
+ } catch {
829
+ }
830
+ } else if (typeof previewUrlPattern === "string") {
831
+ if (previewUrlPattern.includes("{{")) {
832
+ resolvedUrl = previewUrlPattern.replace(
833
+ /{{(.*?)}}/g,
834
+ (_, key) => String(docWithDefaults[key.trim()] || "")
835
+ );
836
+ } else if (previewUrlPattern.includes("+") || previewUrlPattern.includes("?") || previewUrlPattern.includes("==") || previewUrlPattern.includes("siteUrl")) {
837
+ try {
838
+ resolvedUrl = import_jexl.default.evalSync(
839
+ previewUrlPattern,
840
+ docWithDefaults
841
+ );
842
+ } catch (_err) {
843
+ resolvedUrl = previewUrlPattern.replace(
844
+ /:([a-zA-Z0-9_]+)/g,
845
+ (_, key) => String(docWithDefaults[key] || "")
846
+ );
847
+ }
848
+ } else {
849
+ resolvedUrl = previewUrlPattern.replace(
850
+ /:([a-zA-Z0-9_]+)/g,
851
+ (_, key) => String(docWithDefaults[key] || "")
852
+ );
853
+ }
854
+ }
855
+ }
856
+ populatedDoc[field.name] = {
857
+ ...value,
858
+ url: resolvedUrl,
859
+ doc: populatedDocValue
860
+ };
861
+ }
862
+ }
863
+ }
864
+ if ((field.type === "array" || field.type === "object") && field.fields && value) {
865
+ populatedDoc[field.name] = await this.populate({
866
+ data: value,
867
+ fields: field.fields,
868
+ currentDepth,
869
+ // Nested fields don't consume depth, only relationships do
870
+ maxDepth
871
+ });
833
872
  }
873
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
874
+ populatedDoc[field.name] = await Promise.all(
875
+ value.map(async (blockData) => {
876
+ const blockConfig = field.blocks.find(
877
+ (b) => b.slug === blockData.blockType
878
+ );
879
+ if (!blockConfig) return blockData;
880
+ return this.populate({
881
+ data: blockData,
882
+ fields: blockConfig.fields,
883
+ currentDepth,
884
+ maxDepth
885
+ });
886
+ })
887
+ );
888
+ }
889
+ }
890
+ return populatedDoc;
891
+ }
892
+ /**
893
+ * Helper to populate a PaginatedResult
894
+ */
895
+ async populateResult(result, fields, maxDepth) {
896
+ if (maxDepth <= 0) return result;
897
+ const populatedDocs = await this.populate({
898
+ data: result.docs,
899
+ fields,
900
+ currentDepth: 0,
901
+ maxDepth
902
+ });
903
+ return {
904
+ ...result,
905
+ docs: populatedDocs
834
906
  };
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
907
  }
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
- }
908
+ };
863
909
 
864
910
  // src/services/audit.service.ts
911
+ init_observability();
865
912
  var AuditService = class {
866
913
  /**
867
914
  * Writes a single entry to the __audit collection.
@@ -921,7 +968,6 @@ async function verifyPassword(plain, stored) {
921
968
 
922
969
  // src/auth/token.ts
923
970
  var import_jose = require("jose");
924
- var import_node_util2 = require("util");
925
971
  function getSecret() {
926
972
  const secret = process.env.DYRECTED_JWT_SECRET;
927
973
  if (!secret) {
@@ -929,7 +975,7 @@ function getSecret() {
929
975
  "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
930
976
  );
931
977
  }
932
- return new import_node_util2.TextEncoder().encode(secret);
978
+ return new TextEncoder().encode(secret);
933
979
  }
934
980
  var DEFAULT_EXPIRY = "7d";
935
981
  async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
@@ -951,7 +997,7 @@ function decodeCollectionToken(token) {
951
997
  var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
952
998
  async function issueAuthSessionToken(args) {
953
999
  const sessionId = createSessionId();
954
- const now = (/* @__PURE__ */ new Date()).toISOString();
1000
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
955
1001
  const db = args.config.db;
956
1002
  if (db) {
957
1003
  await db.create({
@@ -964,7 +1010,7 @@ async function issueAuthSessionToken(args) {
964
1010
  authSource: args.authSource ?? "local",
965
1011
  providerId: args.providerId,
966
1012
  lastIp: args.ip,
967
- lastSeenAt: now,
1013
+ lastSeenAt: now2,
968
1014
  revokedAt: null,
969
1015
  expiresAt: null
970
1016
  }
@@ -989,7 +1035,7 @@ async function issueAuthSessionToken(args) {
989
1035
  id: sessionId,
990
1036
  data: {
991
1037
  expiresAt,
992
- lastSeenAt: now,
1038
+ lastSeenAt: now2,
993
1039
  lastIp: args.ip
994
1040
  }
995
1041
  });
@@ -1065,28 +1111,358 @@ function createSessionId() {
1065
1111
  }
1066
1112
 
1067
1113
  // src/utils/hooks.ts
1068
- var import_jexl3 = __toESM(require("jexl"), 1);
1114
+ var import_jexl4 = __toESM(require("jexl"), 1);
1069
1115
 
1070
1116
  // src/utils/declarative-hooks.ts
1117
+ var import_jexl3 = __toESM(require("jexl"), 1);
1118
+
1119
+ // src/utils/jexl-helpers.ts
1071
1120
  var import_jexl2 = __toESM(require("jexl"), 1);
1121
+ function slugify(val) {
1122
+ if (val === void 0 || val === null) return "";
1123
+ return String(val).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
1124
+ }
1125
+ function lower(val) {
1126
+ if (val === void 0 || val === null) return "";
1127
+ return String(val).toLowerCase();
1128
+ }
1129
+ function upper(val) {
1130
+ if (val === void 0 || val === null) return "";
1131
+ return String(val).toUpperCase();
1132
+ }
1133
+ function trim(val) {
1134
+ if (val === void 0 || val === null) return "";
1135
+ return String(val).trim();
1136
+ }
1137
+ function capitalize(val) {
1138
+ if (val === void 0 || val === null) return "";
1139
+ const str = String(val).trim();
1140
+ if (!str) return "";
1141
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
1142
+ }
1143
+ function truncate(val, length2, ellipsis = "...") {
1144
+ if (val === void 0 || val === null) return "";
1145
+ const str = String(val);
1146
+ const maxLen = typeof length2 === "number" && length2 > 0 ? length2 : 100;
1147
+ if (str.length <= maxLen) return str;
1148
+ return str.slice(0, maxLen).trim() + String(ellipsis ?? "...");
1149
+ }
1150
+ function readingTime(val, wpm = 200) {
1151
+ if (val === void 0 || val === null) return 1;
1152
+ const str = String(val).trim();
1153
+ if (!str) return 1;
1154
+ const words = str.split(/\s+/).filter(Boolean).length;
1155
+ return Math.max(1, Math.ceil(words / (typeof wpm === "number" && wpm > 0 ? wpm : 200)));
1156
+ }
1157
+ function wordCount(val) {
1158
+ if (val === void 0 || val === null) return 0;
1159
+ const str = String(val).trim();
1160
+ if (!str) return 0;
1161
+ return str.split(/\s+/).filter(Boolean).length;
1162
+ }
1163
+ function replace(val, search, replacement) {
1164
+ if (val === void 0 || val === null) return "";
1165
+ return String(val).replace(new RegExp(String(search || ""), "g"), String(replacement ?? ""));
1166
+ }
1167
+ function startsWith(str, prefix) {
1168
+ if (str === void 0 || str === null) return false;
1169
+ return String(str).startsWith(String(prefix ?? ""));
1170
+ }
1171
+ function endsWith(str, suffix) {
1172
+ if (str === void 0 || str === null) return false;
1173
+ return String(str).endsWith(String(suffix ?? ""));
1174
+ }
1175
+ function now() {
1176
+ return (/* @__PURE__ */ new Date()).toISOString();
1177
+ }
1178
+ function today() {
1179
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1180
+ }
1181
+ function formatDate(val, style = "short") {
1182
+ if (!val) return "";
1183
+ const d = new Date(val);
1184
+ if (Number.isNaN(d.getTime())) return "";
1185
+ const styleStr = String(style).toLowerCase();
1186
+ if (styleStr === "iso" || styleStr === "date") return d.toISOString().slice(0, 10);
1187
+ if (styleStr === "datetime" || styleStr === "full") return d.toISOString();
1188
+ return d.toLocaleDateString("en-US", {
1189
+ year: "numeric",
1190
+ month: "short",
1191
+ day: "numeric"
1192
+ });
1193
+ }
1194
+ function addDays(val, days) {
1195
+ const d = val ? new Date(val) : /* @__PURE__ */ new Date();
1196
+ if (Number.isNaN(d.getTime())) return "";
1197
+ const daysNum = typeof days === "number" ? days : Number(days) || 0;
1198
+ d.setDate(d.getDate() + daysNum);
1199
+ return d.toISOString();
1200
+ }
1201
+ function diffDays(dateA, dateB) {
1202
+ const dA = dateA ? new Date(dateA) : /* @__PURE__ */ new Date();
1203
+ const dB = dateB ? new Date(dateB) : /* @__PURE__ */ new Date();
1204
+ if (Number.isNaN(dA.getTime()) || Number.isNaN(dB.getTime())) return 0;
1205
+ const diffMs = Math.abs(dA.getTime() - dB.getTime());
1206
+ return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1207
+ }
1208
+ function isPast(val) {
1209
+ if (!val) return false;
1210
+ const d = new Date(val);
1211
+ if (Number.isNaN(d.getTime())) return false;
1212
+ return d.getTime() < Date.now();
1213
+ }
1214
+ function isFuture(val) {
1215
+ if (!val) return false;
1216
+ const d = new Date(val);
1217
+ if (Number.isNaN(d.getTime())) return false;
1218
+ return d.getTime() > Date.now();
1219
+ }
1220
+ function includes(arrOrStr, item) {
1221
+ if (Array.isArray(arrOrStr)) {
1222
+ return arrOrStr.includes(item);
1223
+ }
1224
+ if (typeof arrOrStr === "string") {
1225
+ return arrOrStr.includes(String(item ?? ""));
1226
+ }
1227
+ return false;
1228
+ }
1229
+ function join(arr, separator = ", ") {
1230
+ if (!Array.isArray(arr)) return "";
1231
+ return arr.filter((item) => item !== void 0 && item !== null).join(String(separator));
1232
+ }
1233
+ function first(arr) {
1234
+ if (Array.isArray(arr)) return arr[0] ?? null;
1235
+ return null;
1236
+ }
1237
+ function last(arr) {
1238
+ if (Array.isArray(arr)) return arr[arr.length - 1] ?? null;
1239
+ return null;
1240
+ }
1241
+ function compact(arr) {
1242
+ if (!Array.isArray(arr)) return [];
1243
+ return arr.filter((item) => item !== void 0 && item !== null && item !== "");
1244
+ }
1245
+ function unique(arr) {
1246
+ if (!Array.isArray(arr)) return [];
1247
+ return Array.from(new Set(arr));
1248
+ }
1249
+ function length(val) {
1250
+ if (Array.isArray(val) || typeof val === "string") return val.length;
1251
+ if (val && typeof val === "object") return Object.keys(val).length;
1252
+ return 0;
1253
+ }
1254
+ function round(val, decimals = 0) {
1255
+ const num = Number(val);
1256
+ if (Number.isNaN(num)) return 0;
1257
+ const dec = typeof decimals === "number" ? decimals : Number(decimals) || 0;
1258
+ const factor = Math.pow(10, dec);
1259
+ return Math.round(num * factor) / factor;
1260
+ }
1261
+ function clamp(val, minVal, maxVal) {
1262
+ const num = Number(val) || 0;
1263
+ const min2 = Number(minVal) || 0;
1264
+ const max2 = Number(maxVal) || 0;
1265
+ return Math.min(Math.max(num, min2), max2);
1266
+ }
1267
+ function ceil(val) {
1268
+ return Math.ceil(Number(val) || 0);
1269
+ }
1270
+ function floor(val) {
1271
+ return Math.floor(Number(val) || 0);
1272
+ }
1273
+ function abs(val) {
1274
+ return Math.abs(Number(val) || 0);
1275
+ }
1276
+ function min(...args) {
1277
+ const nums = args.flat().map((n) => Number(n) || 0);
1278
+ return nums.length ? Math.min(...nums) : 0;
1279
+ }
1280
+ function max(...args) {
1281
+ const nums = args.flat().map((n) => Number(n) || 0);
1282
+ return nums.length ? Math.max(...nums) : 0;
1283
+ }
1284
+ function pow(base, exp) {
1285
+ return Math.pow(Number(base) || 0, Number(exp) || 0);
1286
+ }
1287
+ function sqrt(val) {
1288
+ return Math.sqrt(Number(val) || 0);
1289
+ }
1290
+ function sum(arr) {
1291
+ if (!Array.isArray(arr)) return Number(arr) || 0;
1292
+ return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
1293
+ }
1294
+ function count(val) {
1295
+ if (Array.isArray(val)) return val.length;
1296
+ if (typeof val === "string") return val.length;
1297
+ if (val !== void 0 && val !== null && val !== false) return 1;
1298
+ return 0;
1299
+ }
1300
+ function defaultValue(val, fallback) {
1301
+ if (val === void 0 || val === null || val === "") return fallback;
1302
+ return val;
1303
+ }
1304
+ function coalesce(...args) {
1305
+ for (const arg of args) {
1306
+ if (arg !== void 0 && arg !== null && arg !== "") return arg;
1307
+ }
1308
+ return null;
1309
+ }
1310
+ function isEmpty(val) {
1311
+ if (val === void 0 || val === null || val === "") return true;
1312
+ if (Array.isArray(val)) return val.length === 0;
1313
+ if (typeof val === "object") return Object.keys(val).length === 0;
1314
+ return false;
1315
+ }
1316
+ function get(obj, path, fallback = null) {
1317
+ if (obj === void 0 || obj === null || typeof obj !== "object") return fallback;
1318
+ if (typeof path !== "string") return fallback;
1319
+ const parts = path.split(".").filter(Boolean);
1320
+ let curr = obj;
1321
+ for (const part of parts) {
1322
+ if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
1323
+ curr = curr[part];
1324
+ }
1325
+ return curr === void 0 || curr === null ? fallback : curr;
1326
+ }
1327
+ function registerJexlHelpers(jexlInstance = import_jexl2.default) {
1328
+ const helpers = {
1329
+ // Strings
1330
+ slugify,
1331
+ lower,
1332
+ upper,
1333
+ trim,
1334
+ capitalize,
1335
+ truncate,
1336
+ readingTime,
1337
+ wordCount,
1338
+ replace,
1339
+ startsWith,
1340
+ endsWith,
1341
+ // Dates
1342
+ now,
1343
+ today,
1344
+ formatDate,
1345
+ addDays,
1346
+ diffDays,
1347
+ isPast,
1348
+ isFuture,
1349
+ // Arrays
1350
+ includes,
1351
+ join,
1352
+ first,
1353
+ last,
1354
+ compact,
1355
+ unique,
1356
+ length,
1357
+ // Math
1358
+ round,
1359
+ clamp,
1360
+ ceil,
1361
+ floor,
1362
+ abs,
1363
+ min,
1364
+ max,
1365
+ pow,
1366
+ sqrt,
1367
+ sum,
1368
+ count,
1369
+ // Objects & Logical
1370
+ default: defaultValue,
1371
+ coalesce,
1372
+ isEmpty,
1373
+ get
1374
+ };
1375
+ for (const [name, fn] of Object.entries(helpers)) {
1376
+ try {
1377
+ jexlInstance.addFunction(name, fn);
1378
+ } catch {
1379
+ }
1380
+ try {
1381
+ jexlInstance.addTransform(name, fn);
1382
+ } catch {
1383
+ }
1384
+ }
1385
+ try {
1386
+ jexlInstance.addBinaryOp("and", 20, (left, right) => Boolean(left && right));
1387
+ jexlInstance.addBinaryOp("or", 10, (left, right) => Boolean(left || right));
1388
+ } catch {
1389
+ }
1390
+ }
1391
+ registerJexlHelpers(import_jexl2.default);
1392
+ async function evaluateJexl(expression, context = {}) {
1393
+ if (!expression || typeof expression !== "string") return void 0;
1394
+ try {
1395
+ const evalContext = {
1396
+ null: null,
1397
+ undefined: void 0,
1398
+ ...context,
1399
+ math: Math,
1400
+ Math
1401
+ };
1402
+ return await import_jexl2.default.eval(expression, evalContext);
1403
+ } catch (_err) {
1404
+ return void 0;
1405
+ }
1406
+ }
1407
+ var BUILTIN_JEXL_HELPERS = [
1408
+ // Strings
1409
+ "slugify",
1410
+ "lower",
1411
+ "upper",
1412
+ "trim",
1413
+ "capitalize",
1414
+ "truncate",
1415
+ "readingTime",
1416
+ "wordCount",
1417
+ "replace",
1418
+ "startsWith",
1419
+ "endsWith",
1420
+ // Dates
1421
+ "now",
1422
+ "today",
1423
+ "formatDate",
1424
+ "addDays",
1425
+ "diffDays",
1426
+ "isPast",
1427
+ "isFuture",
1428
+ // Arrays
1429
+ "includes",
1430
+ "join",
1431
+ "first",
1432
+ "last",
1433
+ "compact",
1434
+ "unique",
1435
+ "length",
1436
+ // Math
1437
+ "round",
1438
+ "clamp",
1439
+ // Objects & Logical
1440
+ "default",
1441
+ "coalesce",
1442
+ "isEmpty",
1443
+ "get"
1444
+ ];
1445
+
1446
+ // src/utils/declarative-hooks.ts
1447
+ registerJexlHelpers(import_jexl3.default);
1072
1448
  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"]
1449
+ "collection.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1450
+ "collection.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1451
+ "collection.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1452
+ "global.beforeRead": ["req", "user", "query", ...BUILTIN_JEXL_HELPERS],
1453
+ "global.afterRead": ["req", "user", "doc", ...BUILTIN_JEXL_HELPERS],
1454
+ "global.beforeChange": ["req", "user", "data", "doc", "operation", ...BUILTIN_JEXL_HELPERS],
1455
+ "field.beforeChange": ["value", "data", "originalDoc", "user", ...BUILTIN_JEXL_HELPERS],
1456
+ "field.admin.onChange": ["value", "siblingData", "data", ...BUILTIN_JEXL_HELPERS]
1081
1457
  };
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"];
1458
+ var ACCESS_ROOTS = ["user", "req", "doc", "data", "id", "config", ...BUILTIN_JEXL_HELPERS];
1459
+ var ADMIN_CONDITION_RESERVED_ROOTS = ["data", "siblingData", "user", "id", ...BUILTIN_JEXL_HELPERS];
1460
+ var PREVIEW_URL_RESERVED_ROOTS = ["siteUrl", "id", ...BUILTIN_JEXL_HELPERS];
1085
1461
  var COMPILE_CACHE = /* @__PURE__ */ new Map();
1086
1462
  function compileExpression(expression) {
1087
1463
  const cached = COMPILE_CACHE.get(expression);
1088
1464
  if (cached) return cached;
1089
- const compiled = import_jexl2.default.compile(expression);
1465
+ const compiled = import_jexl3.default.compile(expression);
1090
1466
  const next = { ast: compiled._ast };
1091
1467
  COMPILE_CACHE.set(expression, next);
1092
1468
  return next;
@@ -1609,13 +1985,16 @@ function collectConfigDiagnostics(config) {
1609
1985
  function formatConfigDiagnostics(source, diagnostics, options = {}) {
1610
1986
  const color = options.color ?? false;
1611
1987
  const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
1988
+ const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/editor-experience/form-and-field-hooks", 34, color)}`;
1612
1989
  const lines = diagnostics.flatMap((issue, index) => [
1613
1990
  `${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
1614
1991
  ` ${issue.message}`,
1615
1992
  ` ${colorize("surface:", 90, color)} ${issue.surface}`
1616
1993
  ]);
1617
1994
  return `${header}
1618
- ${lines.join("\n")}`;
1995
+ ${lines.join("\n")}
1996
+
1997
+ ${docLink}`;
1619
1998
  }
1620
1999
  var ConfigValidationError = class extends Error {
1621
2000
  diagnostics;
@@ -1673,7 +2052,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1673
2052
  options.path ?? options.surface
1674
2053
  );
1675
2054
  }
1676
- return import_jexl3.default.eval(hook, hookArgs);
2055
+ return import_jexl4.default.eval(hook, hookArgs);
1677
2056
  })() : await hook(hookArgs);
1678
2057
  if (result !== void 0) {
1679
2058
  if (typeof hook === "string" && options.surface) {
@@ -1690,10 +2069,7 @@ async function runCollectionHooks(hooks, args, options = {}) {
1690
2069
  }
1691
2070
  } catch (err) {
1692
2071
  if (options.isolated) {
1693
- getConfigLogger(void 0, "hooks").error({
1694
- err,
1695
- msg: "Side-effect hook failed after a successful write"
1696
- });
2072
+ console.error("Side-effect hook failed after a successful write", err);
1697
2073
  } else {
1698
2074
  throw err;
1699
2075
  }
@@ -1725,7 +2101,7 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db, pat
1725
2101
  "field.beforeChange",
1726
2102
  fieldPath
1727
2103
  );
1728
- return import_jexl3.default.eval(hook, hookArgs);
2104
+ return import_jexl4.default.eval(hook, hookArgs);
1729
2105
  })() : await hook(hookArgs);
1730
2106
  }
1731
2107
  result[field.name] = updatedValue;
@@ -1903,22 +2279,18 @@ function validateUpload(file, config) {
1903
2279
  }
1904
2280
 
1905
2281
  // src/auth/jexl.ts
1906
- var import_jexl4 = __toESM(require("jexl"), 1);
2282
+ var import_jexl5 = __toESM(require("jexl"), 1);
1907
2283
  async function evaluateAccess(expression, context) {
1908
2284
  if (expression === void 0 || expression === null) return false;
1909
2285
  if (typeof expression === "boolean") return expression;
1910
2286
  try {
1911
- const result = await import_jexl4.default.eval(expression, context);
2287
+ const result = await import_jexl5.default.eval(expression, context);
1912
2288
  if (result && typeof result === "object" && !Array.isArray(result)) {
1913
2289
  return result;
1914
2290
  }
1915
2291
  return !!result;
1916
2292
  } catch (err) {
1917
- getConfigLogger(context.config, "access").error({
1918
- err,
1919
- msg: "Jexl evaluation failed",
1920
- expression
1921
- });
2293
+ console.error("[dyrected:access] Failed to evaluate access expression:", err);
1922
2294
  return false;
1923
2295
  }
1924
2296
  }
@@ -1936,20 +2308,14 @@ async function resolveAccess(config, access, args) {
1936
2308
  try {
1937
2309
  return await access(args);
1938
2310
  } catch (err) {
1939
- getConfigLogger(config, "access").error({
1940
- err,
1941
- msg: "Functional access check failed"
1942
- });
2311
+ console.error("[dyrected:access] Functional access check failed:", err);
1943
2312
  return false;
1944
2313
  }
1945
2314
  }
1946
2315
  if (isNamedAccessPolicy(access)) {
1947
2316
  const policy = config.accessPolicies?.[access.policy];
1948
2317
  if (policy === void 0) {
1949
- getConfigLogger(config, "access").error({
1950
- msg: "Unknown access policy",
1951
- policy: access.policy
1952
- });
2318
+ console.error(`[dyrected:access] Unknown access policy "${access.policy}"`);
1953
2319
  return false;
1954
2320
  }
1955
2321
  if (typeof policy === "string" || typeof policy === "boolean") {
@@ -1959,11 +2325,7 @@ async function resolveAccess(config, access, args) {
1959
2325
  const resolver = policy;
1960
2326
  return await resolver({ ...args, params: access.params });
1961
2327
  } catch (err) {
1962
- getConfigLogger(config, "access").error({
1963
- err,
1964
- msg: "Access policy failed",
1965
- policy: access.policy
1966
- });
2328
+ console.error(`[dyrected:access] Access policy "${access.policy}" failed:`, err);
1967
2329
  return false;
1968
2330
  }
1969
2331
  }
@@ -2383,7 +2745,7 @@ function simplePublishingWorkflow() {
2383
2745
  { name: "published", label: "Published", color: "success", published: true }
2384
2746
  ],
2385
2747
  transitions: [
2386
- { name: "publish", label: "Publish", from: "draft", to: "published" },
2748
+ { name: "publish", label: "Publish", from: ["draft", "published"], to: "published" },
2387
2749
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
2388
2750
  ]
2389
2751
  // Intentionally no `roles`: this lightweight workflow (synthesized from
@@ -2490,7 +2852,11 @@ async function dispatchLifecycleEvent(config, event) {
2490
2852
  const retryDelayMs = config.events.retryDelayMs ?? 1e3;
2491
2853
  const attempts = event.attempts + 1;
2492
2854
  try {
2493
- await db.update({ collection: LIFECYCLE_EVENTS_COLLECTION, id: event.id, data: { status: "processing", attempts } });
2855
+ await db.update({
2856
+ collection: LIFECYCLE_EVENTS_COLLECTION,
2857
+ id: event.id,
2858
+ data: { status: "processing", attempts }
2859
+ });
2494
2860
  for (const handler of config.events.handlers) await handler({ ...event, status: "processing", attempts });
2495
2861
  await db.update({
2496
2862
  collection: LIFECYCLE_EVENTS_COLLECTION,
@@ -2581,12 +2947,16 @@ async function transitionWorkflow(args) {
2581
2947
  const meta = original.__workflow;
2582
2948
  if (!meta) throw Object.assign(new Error("Entry has no workflow metadata"), { statusCode: 409 });
2583
2949
  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 });
2950
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
2951
+ statusCode: 409
2952
+ });
2585
2953
  }
2586
2954
  const transition = workflow.transitions.find((item) => item.name === transitionName);
2587
2955
  const fromStates = transition ? Array.isArray(transition.from) ? transition.from : [transition.from] : [];
2588
2956
  if (!transition || !fromStates.includes(meta.state)) {
2589
- throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), { statusCode: 409 });
2957
+ throw Object.assign(new Error(`Transition "${transitionName}" is not valid from "${meta.state}".`), {
2958
+ statusCode: 409
2959
+ });
2590
2960
  }
2591
2961
  if (!availableWorkflowTransitions(workflow, meta.state, user).some((item) => item.name === transition.name)) {
2592
2962
  throw Object.assign(new Error(`You do not have permission to perform "${transition.label}".`), { statusCode: 403 });
@@ -2596,32 +2966,52 @@ async function transitionWorkflow(args) {
2596
2966
  }
2597
2967
  const hookContext = { transition, from: meta.state, to: transition.to, doc: original, user, comment, req, db };
2598
2968
  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
- })];
2969
+ const events = [
2970
+ createLifecycleEvent({
2971
+ name: "workflow.transitioned",
2972
+ collection: collection.slug,
2973
+ documentId: id,
2974
+ actorId: user?.sub,
2975
+ payload: { transition: transition.name, from: meta.state, to: transition.to, revision: meta.revision, comment }
2976
+ })
2977
+ ];
2606
2978
  const targetState = workflow.states.find((state) => state.name === transition.to);
2607
2979
  if (targetState.published) {
2608
- events.push(createLifecycleEvent({ name: "entry.published", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.revision } }));
2980
+ events.push(
2981
+ createLifecycleEvent({
2982
+ name: "entry.published",
2983
+ collection: collection.slug,
2984
+ documentId: id,
2985
+ actorId: user?.sub,
2986
+ payload: { revision: meta.revision }
2987
+ })
2988
+ );
2609
2989
  } else if (transition.unpublish) {
2610
- events.push(createLifecycleEvent({ name: "entry.unpublished", collection: collection.slug, documentId: id, actorId: user?.sub, payload: { revision: meta.publishedRevision } }));
2990
+ events.push(
2991
+ createLifecycleEvent({
2992
+ name: "entry.unpublished",
2993
+ collection: collection.slug,
2994
+ documentId: id,
2995
+ actorId: user?.sub,
2996
+ payload: { revision: meta.publishedRevision }
2997
+ })
2998
+ );
2611
2999
  }
2612
3000
  const updated = await db.transaction(async (tx) => {
2613
3001
  const locked = await tx.findOne({ collection: collection.slug, id });
2614
3002
  if (!locked) throw Object.assign(new Error("Not Found"), { statusCode: 404 });
2615
3003
  const lockedMeta = locked.__workflow;
2616
3004
  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 });
3005
+ throw Object.assign(new Error("This entry changed since it was loaded. Refresh before transitioning it."), {
3006
+ statusCode: 409
3007
+ });
2618
3008
  }
2619
- const now = (/* @__PURE__ */ new Date()).toISOString();
3009
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
2620
3010
  const { __published: _published, __workflow: _workflow, id: _id, ...working } = locked;
2621
3011
  const nextMeta = {
2622
3012
  ...lockedMeta,
2623
3013
  state: transition.to,
2624
- ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now, publishedBy: user?.sub } : {},
3014
+ ...targetState.published ? { publishedRevision: lockedMeta.revision, publishedAt: now2, publishedBy: user?.sub } : {},
2625
3015
  ...transition.unpublish ? { publishedRevision: void 0, publishedAt: void 0, publishedBy: void 0 } : {}
2626
3016
  };
2627
3017
  const data = { __workflow: nextMeta };
@@ -2630,11 +3020,36 @@ async function transitionWorkflow(args) {
2630
3020
  const next = await tx.update({ collection: collection.slug, id, data });
2631
3021
  await tx.create({
2632
3022
  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 }
3023
+ data: {
3024
+ collection: collection.slug,
3025
+ documentId: id,
3026
+ transition: transition.name,
3027
+ from: lockedMeta.state,
3028
+ to: transition.to,
3029
+ revision: lockedMeta.revision,
3030
+ comment: comment ?? null,
3031
+ actorId: user?.sub ?? null,
3032
+ createdAt: now2
3033
+ }
2634
3034
  });
2635
3035
  for (const event of events) await persistEvent(tx, event);
2636
3036
  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 }) } });
3037
+ await tx.create({
3038
+ collection: "__audit",
3039
+ data: {
3040
+ collection: collection.slug,
3041
+ documentId: id,
3042
+ operation: "workflow.transition",
3043
+ user: user?.sub ?? null,
3044
+ timestamp: now2,
3045
+ changes: JSON.stringify({
3046
+ transition: transition.name,
3047
+ from: lockedMeta.state,
3048
+ to: transition.to,
3049
+ revision: lockedMeta.revision
3050
+ })
3051
+ }
3052
+ });
2638
3053
  }
2639
3054
  return next;
2640
3055
  });
@@ -2643,22 +3058,93 @@ async function transitionWorkflow(args) {
2643
3058
  try {
2644
3059
  await hook({ ...hookContext, doc: updated, event: events[0] });
2645
3060
  } 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
- });
3061
+ try {
3062
+ const { getConfigLogger: getConfigLogger2, getObservabilityRuntime: getObservabilityRuntime2 } = await Promise.resolve().then(() => (init_observability(), observability_exports));
3063
+ getObservabilityRuntime2(config)?.recordWorkflowHookFailure({
3064
+ collection: collection.slug,
3065
+ transition: transition.name
3066
+ });
3067
+ getConfigLogger2(config, "workflow").error({
3068
+ err: error,
3069
+ msg: "afterTransition hook failed",
3070
+ collection: collection.slug,
3071
+ transition: transition.name,
3072
+ documentId: id
3073
+ });
3074
+ } catch {
3075
+ console.error("afterTransition hook failed:", error);
3076
+ }
2657
3077
  }
2658
3078
  }
2659
3079
  return updated;
2660
3080
  }
2661
3081
 
3082
+ // src/controllers/collection.controller.ts
3083
+ init_observability();
3084
+
3085
+ // src/detail.ts
3086
+ function normalizeDetailItem(item) {
3087
+ if (typeof item === "string") {
3088
+ return {
3089
+ type: "field",
3090
+ field: item
3091
+ };
3092
+ }
3093
+ return item;
3094
+ }
3095
+ async function evaluateDetailComputed(detail, doc, user, db) {
3096
+ if (!doc || !detail || !Array.isArray(detail)) {
3097
+ return doc;
3098
+ }
3099
+ const computedItems = [];
3100
+ const extractComputed = (items) => {
3101
+ for (const rawItem of items) {
3102
+ if (!rawItem) continue;
3103
+ const item = normalizeDetailItem(rawItem);
3104
+ if (item.type === "computed") {
3105
+ const id = item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_");
3106
+ if (id) {
3107
+ computedItems.push({
3108
+ id,
3109
+ label: item.label,
3110
+ expression: item.expression,
3111
+ handler: item.handler
3112
+ });
3113
+ }
3114
+ } else if (item.type === "section" || item.type === "grid" || item.type === "repeat") {
3115
+ if (item.items && Array.isArray(item.items)) {
3116
+ extractComputed(item.items);
3117
+ }
3118
+ } else if (item.type === "tabs" && item.tabs && Array.isArray(item.tabs)) {
3119
+ for (const tab of item.tabs) {
3120
+ if (tab.items) extractComputed(tab.items);
3121
+ }
3122
+ }
3123
+ }
3124
+ };
3125
+ extractComputed(detail);
3126
+ if (computedItems.length === 0) return doc;
3127
+ const evaluated = {};
3128
+ for (const item of computedItems) {
3129
+ try {
3130
+ if (typeof item.handler === "function") {
3131
+ evaluated[item.id] = await item.handler({ doc, user, db });
3132
+ } else if (item.expression) {
3133
+ evaluated[item.id] = await evaluateJexl(item.expression, { doc, user });
3134
+ }
3135
+ } catch (_err) {
3136
+ evaluated[item.id] = null;
3137
+ }
3138
+ }
3139
+ return {
3140
+ ...doc,
3141
+ _meta: {
3142
+ ...doc._meta || {},
3143
+ computed: evaluated
3144
+ }
3145
+ };
3146
+ }
3147
+
2662
3148
  // src/controllers/collection.controller.ts
2663
3149
  var CollectionController = class {
2664
3150
  collection;
@@ -2676,6 +3162,22 @@ var CollectionController = class {
2676
3162
  toHookRequestContext(c) {
2677
3163
  return toHookRequestContext(c.req);
2678
3164
  }
3165
+ sanitizeDoc(doc) {
3166
+ if (!doc || typeof doc !== "object") return doc;
3167
+ if (this.collection.auth) {
3168
+ const {
3169
+ password: _p,
3170
+ salt: _s,
3171
+ resetPasswordToken: _rpt,
3172
+ resetPasswordExpires: _rpe,
3173
+ loginAttempts: _la,
3174
+ lockUntil: _lu,
3175
+ ...safeDoc
3176
+ } = doc;
3177
+ return safeDoc;
3178
+ }
3179
+ return doc;
3180
+ }
2679
3181
  async evaluateAccess(c, action, options = {}) {
2680
3182
  const config = c.get("config");
2681
3183
  return resolveCollectionAccess(
@@ -2908,6 +3410,9 @@ var CollectionController = class {
2908
3410
  depth
2909
3411
  );
2910
3412
  }
3413
+ if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
3414
+ result.docs = result.docs.map((d) => this.sanitizeDoc(d));
3415
+ }
2911
3416
  return c.json(result);
2912
3417
  }
2913
3418
  async findOne(c) {
@@ -2924,32 +3429,51 @@ var CollectionController = class {
2924
3429
  id: id2
2925
3430
  });
2926
3431
  const externalSubject = localDoc?.externalSubject || id2;
2927
- let member = null;
2928
3432
  if (provider.members.get) {
2929
- member = await provider.members.get({ externalSubject, req: hookReq });
3433
+ const member = await provider.members.get({
3434
+ externalSubject,
3435
+ req: hookReq
3436
+ });
3437
+ if (member) {
3438
+ return c.json({
3439
+ ...member,
3440
+ id: localDoc ? localDoc.id : member.id,
3441
+ externalSubject: member.id
3442
+ });
3443
+ }
2930
3444
  } else if (provider.members.list) {
2931
3445
  const listResult = await provider.members.list({ req: hookReq });
2932
- member = listResult.docs.find((m) => m.id === externalSubject) || null;
3446
+ const matched = listResult.docs.find((m) => m.id === externalSubject || m.id === id2);
3447
+ if (matched) {
3448
+ return c.json({
3449
+ ...matched,
3450
+ id: localDoc ? localDoc.id : matched.id,
3451
+ externalSubject: matched.id
3452
+ });
3453
+ }
2933
3454
  }
2934
- if (!member) return c.json({ message: "Not Found" }, 404);
2935
- return c.json({
2936
- ...member,
2937
- id: localDoc ? localDoc.id : member.id,
2938
- externalSubject: member.id
2939
- });
2940
3455
  }
2941
- const readonlyDb = createReadonlyDb(db);
2942
3456
  const id = c.req.param("id");
2943
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
2944
- const user = c.get("user");
2945
3457
  if (!id) return c.json({ message: "Missing ID" }, 400);
2946
- let doc = await db.findOne({ collection: this.collection.slug, id });
2947
- if (!doc) return c.json({ message: "Not Found" }, 404);
3458
+ const depth = Number.parseInt(c.req.query("depth") || "0", 10);
3459
+ const user = c.get("user");
3460
+ const doc = await db.findOne({
3461
+ collection: this.collection.slug,
3462
+ id
3463
+ });
3464
+ if (!doc) {
3465
+ return c.json({ message: "Document not found" }, 404);
3466
+ }
3467
+ let effectiveDoc = doc;
2948
3468
  if (this.collection.workflow) {
2949
- doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
2950
- if (!doc) return c.json({ message: "Not Found" }, 404);
3469
+ effectiveDoc = materializeWorkflowDocument(
3470
+ doc,
3471
+ this.collection.workflow,
3472
+ user
3473
+ );
3474
+ if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
2951
3475
  }
2952
- const access = await this.evaluateAccess(c, "read", { id, doc });
3476
+ const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
2953
3477
  if (!access.allowed) {
2954
3478
  return c.json(
2955
3479
  {
@@ -2959,7 +3483,11 @@ var CollectionController = class {
2959
3483
  403
2960
3484
  );
2961
3485
  }
2962
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
3486
+ const docWithDefaults = DefaultsService.apply(
3487
+ this.collection.fields,
3488
+ effectiveDoc
3489
+ );
3490
+ const readonlyDb = createReadonlyDb(db);
2963
3491
  const docWithCollectionHooks = await runCollectionHooks(
2964
3492
  this.collection.hooks?.afterRead,
2965
3493
  {
@@ -2985,17 +3513,20 @@ var CollectionController = class {
2985
3513
  },
2986
3514
  docWithFieldHooks
2987
3515
  );
3516
+ let finalDoc = docWithFieldAccess;
2988
3517
  if (depth > 0 && docWithFieldAccess) {
2989
3518
  const populationService = new PopulationService(db, config.collections);
2990
- const populatedDoc = await populationService.populate({
3519
+ finalDoc = await populationService.populate({
2991
3520
  data: docWithFieldAccess,
2992
3521
  fields: this.collection.fields,
2993
3522
  currentDepth: 0,
2994
3523
  maxDepth: depth
2995
3524
  });
2996
- return c.json(populatedDoc);
2997
3525
  }
2998
- return c.json(docWithFieldAccess);
3526
+ if (finalDoc && this.collection.detail) {
3527
+ finalDoc = await evaluateDetailComputed(this.collection.detail, finalDoc, user, readonlyDb);
3528
+ }
3529
+ return c.json(this.sanitizeDoc(finalDoc));
2999
3530
  }
3000
3531
  async create(c) {
3001
3532
  const config = c.get("config");
@@ -3029,11 +3560,11 @@ var CollectionController = class {
3029
3560
  }
3030
3561
  const body = await c.req.json();
3031
3562
  const user = c.get("user");
3032
- const now = (/* @__PURE__ */ new Date()).toISOString();
3563
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3033
3564
  let data = {
3034
3565
  ...body,
3035
- createdAt: now,
3036
- updatedAt: now,
3566
+ createdAt: now2,
3567
+ updatedAt: now2,
3037
3568
  createdBy: user?.sub ?? null,
3038
3569
  updatedBy: user?.sub ?? null
3039
3570
  };
@@ -3171,12 +3702,12 @@ var CollectionController = class {
3171
3702
  }
3172
3703
  });
3173
3704
  const user = c.get("user");
3174
- const now = (/* @__PURE__ */ new Date()).toISOString();
3705
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
3175
3706
  let data = {
3176
3707
  ...otherData,
3177
3708
  ...fileData,
3178
- createdAt: now,
3179
- updatedAt: now,
3709
+ createdAt: now2,
3710
+ updatedAt: now2,
3180
3711
  createdBy: user?.sub ?? null,
3181
3712
  updatedBy: user?.sub ?? null
3182
3713
  };
@@ -3762,14 +4293,72 @@ var CollectionController = class {
3762
4293
  const doc = await db.create({ collection: this.collection.slug, data });
3763
4294
  createdDocs.push(doc);
3764
4295
  }
3765
- return c.json(
3766
- { message: "Seed successful", count: createdDocs.length },
3767
- 201
3768
- );
4296
+ return c.json(
4297
+ { message: "Seed successful", count: createdDocs.length },
4298
+ 201
4299
+ );
4300
+ }
4301
+ async aggregate(c) {
4302
+ const config = c.get("config");
4303
+ const db = config.db;
4304
+ if (!db) return c.json({ message: "Database not configured" }, 500);
4305
+ const access = await this.evaluateAccess(c, "read");
4306
+ if (!access.allowed) {
4307
+ return c.json(
4308
+ {
4309
+ error: true,
4310
+ message: `Access denied: read on ${this.collection.slug}`
4311
+ },
4312
+ 403
4313
+ );
4314
+ }
4315
+ let body;
4316
+ try {
4317
+ body = await c.req.json();
4318
+ } catch {
4319
+ return c.json({ message: "Invalid JSON body" }, 400);
4320
+ }
4321
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4322
+ return c.json(
4323
+ { message: "Aggregate request body must be a JSON object" },
4324
+ 400
4325
+ );
4326
+ }
4327
+ const { sanitizeWhereClause: sanitizeWhereClause2 } = await Promise.resolve().then(() => (init_where_sanitizer(), where_sanitizer_exports));
4328
+ const sanitizedAggregates = {};
4329
+ for (const [key, op] of Object.entries(body)) {
4330
+ if (!op || typeof op !== "object" || Array.isArray(op)) {
4331
+ return c.json(
4332
+ {
4333
+ message: `Aggregate operation "${key}" must be an object`
4334
+ },
4335
+ 400
4336
+ );
4337
+ }
4338
+ const operation = op;
4339
+ let opWhere = operation.where && typeof operation.where === "object" ? operation.where : void 0;
4340
+ if (opWhere) {
4341
+ opWhere = sanitizeWhereClause2(opWhere, this.collection.fields);
4342
+ if (Object.keys(opWhere).length === 0) opWhere = void 0;
4343
+ }
4344
+ if (access.constraint) {
4345
+ opWhere = opWhere ? mergeWhereConstraint(opWhere, access.constraint) : access.constraint;
4346
+ }
4347
+ sanitizedAggregates[key] = {
4348
+ ...operation,
4349
+ ...opWhere !== void 0 ? { where: opWhere } : { where: void 0 }
4350
+ };
4351
+ }
4352
+ const result = await db.aggregate({
4353
+ collection: this.collection.slug,
4354
+ aggregates: sanitizedAggregates
4355
+ });
4356
+ return c.json(result);
3769
4357
  }
3770
4358
  };
3771
4359
 
3772
4360
  // src/controllers/global.controller.ts
4361
+ init_observability();
3773
4362
  var GlobalController = class {
3774
4363
  global;
3775
4364
  constructor(global) {
@@ -3792,8 +4381,8 @@ var GlobalController = class {
3792
4381
  path: `global:${this.global.slug}.hooks.beforeRead`
3793
4382
  });
3794
4383
  let data = await db.getGlobal({ slug: this.global.slug });
3795
- const isEmpty = !data || isFunctionallyEmpty(data);
3796
- if (isEmpty && this.global.initialData) {
4384
+ const isEmpty2 = !data || isFunctionallyEmpty(data);
4385
+ if (isEmpty2 && this.global.initialData) {
3797
4386
  getRequestLogger(c, "global").info({
3798
4387
  msg: "Auto-seeding global from config.initialData",
3799
4388
  global: this.global.slug
@@ -3849,17 +4438,25 @@ var GlobalController = class {
3849
4438
  },
3850
4439
  docWithFieldHooks
3851
4440
  );
4441
+ let finalDoc = docWithFieldAccess;
3852
4442
  if (depth > 0 && docWithFieldAccess) {
3853
4443
  const populationService = new PopulationService(db, config.collections);
3854
- const populatedData = await populationService.populate({
4444
+ finalDoc = await populationService.populate({
3855
4445
  data: docWithFieldAccess,
3856
4446
  fields: this.global.fields,
3857
4447
  currentDepth: 0,
3858
4448
  maxDepth: depth
3859
4449
  });
3860
- return c.json(populatedData);
3861
4450
  }
3862
- return c.json(docWithFieldAccess);
4451
+ if (finalDoc && this.global.detail) {
4452
+ finalDoc = await evaluateDetailComputed(
4453
+ this.global.detail,
4454
+ finalDoc,
4455
+ user,
4456
+ readonlyDb
4457
+ );
4458
+ }
4459
+ return c.json(finalDoc);
3863
4460
  }
3864
4461
  async update(c) {
3865
4462
  const config = c.get("config");
@@ -4105,6 +4702,7 @@ function mergeDynamicConfig(baseConfig, dynamic) {
4105
4702
  }
4106
4703
 
4107
4704
  // src/controllers/media.controller.ts
4705
+ init_observability();
4108
4706
  var MediaController = class {
4109
4707
  collection;
4110
4708
  loggerComponent = "media";
@@ -4428,6 +5026,7 @@ function layout({ preheader, title, content, footer }) {
4428
5026
  }
4429
5027
 
4430
5028
  // src/services/email.service.ts
5029
+ init_observability();
4431
5030
  var _devSend = null;
4432
5031
  var _devSendPromise = null;
4433
5032
  async function getDevSend() {
@@ -4548,6 +5147,7 @@ function buildPasswordChangedEmail(config, args) {
4548
5147
  }
4549
5148
 
4550
5149
  // src/controllers/auth.controller.ts
5150
+ init_observability();
4551
5151
  var AuthController = class {
4552
5152
  collection;
4553
5153
  constructor(collection) {
@@ -4612,13 +5212,13 @@ var AuthController = class {
4612
5212
  if (!lockout.enabled) {
4613
5213
  return { justLocked: false, retryAfterSeconds: null };
4614
5214
  }
4615
- const now = Date.now();
5215
+ const now2 = Date.now();
4616
5216
  const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
4617
- const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now;
5217
+ const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now2;
4618
5218
  const currentAttempts = !lockExpired && typeof user.loginAttempts === "number" && user.loginAttempts > 0 ? user.loginAttempts : 0;
4619
5219
  const loginAttempts = currentAttempts + 1;
4620
5220
  const justLocked = loginAttempts >= lockout.maxLoginAttempts;
4621
- const nextLockedUntil = justLocked ? new Date(now + lockout.lockTime).toISOString() : null;
5221
+ const nextLockedUntil = justLocked ? new Date(now2 + lockout.lockTime).toISOString() : null;
4622
5222
  await db.update({
4623
5223
  collection: this.collection.slug,
4624
5224
  id: String(user.id),
@@ -5152,7 +5752,7 @@ var AuthController = class {
5152
5752
 
5153
5753
  // src/controllers/admin-auth.controller.ts
5154
5754
  var import_node_crypto3 = require("crypto");
5155
- var import_node_util3 = require("util");
5755
+ var import_node_util2 = require("util");
5156
5756
  var import_jose2 = require("jose");
5157
5757
  var AdminAuthController = class {
5158
5758
  constructor(config) {
@@ -5309,6 +5909,7 @@ var AdminAuthController = class {
5309
5909
  });
5310
5910
  }
5311
5911
  getProvider(config, id) {
5912
+ if (!id) return void 0;
5312
5913
  if (config.adminAuth?.mode !== "external") return void 0;
5313
5914
  return config.adminAuth.providers.find((provider) => provider.id === id);
5314
5915
  }
@@ -5507,17 +6108,16 @@ var AdminAuthController = class {
5507
6108
  throw new Error("OIDC nonce verification failed.");
5508
6109
  }
5509
6110
  claims = payload;
5510
- } else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
5511
- const userInfo = await fetch(
5512
- provider.userInfoEndpoint || discovery.userinfo_endpoint,
5513
- {
5514
- headers: { Authorization: `Bearer ${tokenBody.access_token}` }
5515
- }
5516
- );
6111
+ } else {
6112
+ const userInfoEndpoint = provider.userInfoEndpoint ?? discovery.userinfo_endpoint;
6113
+ if (!tokenBody.access_token || !userInfoEndpoint) {
6114
+ throw new Error("OIDC provider did not return usable identity claims.");
6115
+ }
6116
+ const userInfo = await fetch(userInfoEndpoint, {
6117
+ headers: { Authorization: `Bearer ${tokenBody.access_token}` }
6118
+ });
5517
6119
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
5518
6120
  claims = await userInfo.json();
5519
- } else {
5520
- throw new Error("OIDC provider did not return usable identity claims.");
5521
6121
  }
5522
6122
  return {
5523
6123
  identity: this.mapClaims(claims, provider.claimMapping),
@@ -5533,7 +6133,7 @@ var AdminAuthController = class {
5533
6133
  }
5534
6134
  let claims;
5535
6135
  if (provider.secret) {
5536
- const secret = new import_node_util3.TextEncoder().encode(provider.secret);
6136
+ const secret = new import_node_util2.TextEncoder().encode(provider.secret);
5537
6137
  const verified = await (0, import_jose2.jwtVerify)(token, secret, {
5538
6138
  issuer: provider.issuer,
5539
6139
  audience: provider.audience
@@ -5649,7 +6249,7 @@ var AdminAuthController = class {
5649
6249
  if (!secret) {
5650
6250
  throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
5651
6251
  }
5652
- return new import_node_util3.TextEncoder().encode(secret);
6252
+ return new import_node_util2.TextEncoder().encode(secret);
5653
6253
  }
5654
6254
  normalizeReturnTo(returnTo, c) {
5655
6255
  if (returnTo) return returnTo;
@@ -5660,11 +6260,11 @@ var AdminAuthController = class {
5660
6260
 
5661
6261
  // src/controllers/preview.controller.ts
5662
6262
  var import_jose3 = require("jose");
5663
- var import_node_util4 = require("util");
6263
+ var import_node_util3 = require("util");
5664
6264
  var PreviewController = class {
5665
6265
  getSecret() {
5666
6266
  const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
5667
- return new import_node_util4.TextEncoder().encode(secret);
6267
+ return new import_node_util3.TextEncoder().encode(secret);
5668
6268
  }
5669
6269
  /**
5670
6270
  * POST /api/preview-token
@@ -5873,6 +6473,7 @@ var AuditController = class {
5873
6473
  };
5874
6474
 
5875
6475
  // src/middleware/auth.ts
6476
+ init_observability();
5876
6477
  function getBearerToken(c) {
5877
6478
  const authHeader = c.req.header("Authorization");
5878
6479
  return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
@@ -6431,6 +7032,71 @@ function generateOpenApi(config) {
6431
7032
  responses: { 200: { description: "Deleted and failed document IDs" } }
6432
7033
  }
6433
7034
  };
7035
+ spec.paths[`${path}/aggregate`] = {
7036
+ post: {
7037
+ tags: [collectionTag],
7038
+ summary: `Aggregate ${labels.plural}`,
7039
+ description: `Compute statistical aggregations (count, sum, avg, min, max) across ${labels.plural}.`,
7040
+ requestBody: {
7041
+ required: true,
7042
+ content: {
7043
+ "application/json": {
7044
+ schema: {
7045
+ type: "object",
7046
+ additionalProperties: {
7047
+ type: "object",
7048
+ properties: {
7049
+ count: { type: "string", example: "*" },
7050
+ sum: { type: "string", description: "Field to sum" },
7051
+ avg: { type: "string", description: "Field to average" },
7052
+ min: { type: "string", description: "Field for minimum" },
7053
+ max: { type: "string", description: "Field for maximum" },
7054
+ cast: {
7055
+ type: "string",
7056
+ enum: [
7057
+ "number",
7058
+ "integer",
7059
+ "float",
7060
+ "boolean",
7061
+ "date",
7062
+ "string"
7063
+ ]
7064
+ },
7065
+ where: {
7066
+ type: "object",
7067
+ description: "Per-aggregate filter condition",
7068
+ additionalProperties: true
7069
+ }
7070
+ }
7071
+ }
7072
+ }
7073
+ }
7074
+ }
7075
+ },
7076
+ responses: {
7077
+ 200: {
7078
+ description: "Success",
7079
+ content: {
7080
+ "application/json": {
7081
+ schema: {
7082
+ type: "object",
7083
+ additionalProperties: {
7084
+ type: "number",
7085
+ nullable: true
7086
+ }
7087
+ }
7088
+ }
7089
+ }
7090
+ },
7091
+ 400: {
7092
+ description: "Bad Request - Invalid aggregate input"
7093
+ },
7094
+ 403: {
7095
+ description: "Forbidden - Access denied by read rule"
7096
+ }
7097
+ }
7098
+ }
7099
+ };
6434
7100
  if (collection.audit) {
6435
7101
  spec.paths[`${path}/__audit`] = {
6436
7102
  get: {
@@ -7013,6 +7679,7 @@ function getSwaggerHtml(specUrl) {
7013
7679
  }
7014
7680
 
7015
7681
  // src/router.ts
7682
+ init_observability();
7016
7683
  var SERIALIZED_ADMIN_HOOK_PREFIX = "__dyrected_fn__:";
7017
7684
  function accessGate(config, target, action) {
7018
7685
  return async (c, next) => {
@@ -7026,10 +7693,7 @@ function accessGate(config, target, action) {
7026
7693
  req: toHookRequestContext(c.req)
7027
7694
  });
7028
7695
  if (!allowed) {
7029
- return c.json(
7030
- { error: true, message: `Access denied: ${action} on ${target.slug}` },
7031
- 403
7032
- );
7696
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
7033
7697
  }
7034
7698
  await next();
7035
7699
  };
@@ -7069,6 +7733,80 @@ function serializeBlockForApi(block) {
7069
7733
  fields: block.fields?.map(serializeFieldForApi)
7070
7734
  };
7071
7735
  }
7736
+ function serializeDetailItemForApi(item) {
7737
+ if (!item) return item;
7738
+ if (typeof item === "string") {
7739
+ return { type: "field", field: item };
7740
+ }
7741
+ if (item.type === "section") {
7742
+ return {
7743
+ type: "section",
7744
+ title: item.title,
7745
+ items: (item.items || []).map(serializeDetailItemForApi),
7746
+ options: item.options
7747
+ };
7748
+ }
7749
+ if (item.type === "tab") {
7750
+ return {
7751
+ type: "tab",
7752
+ label: item.label,
7753
+ items: (item.items || []).map(serializeDetailItemForApi),
7754
+ options: item.options
7755
+ };
7756
+ }
7757
+ if (item.type === "tabs") {
7758
+ return {
7759
+ type: "tabs",
7760
+ tabs: (item.tabs || []).map(serializeDetailItemForApi),
7761
+ options: item.options
7762
+ };
7763
+ }
7764
+ if (item.type === "grid") {
7765
+ return {
7766
+ type: "grid",
7767
+ columns: item.columns,
7768
+ items: (item.items || []).map(serializeDetailItemForApi),
7769
+ options: item.options
7770
+ };
7771
+ }
7772
+ if (item.type === "repeat") {
7773
+ return {
7774
+ type: "repeat",
7775
+ field: item.field,
7776
+ items: (item.items || []).map(serializeDetailItemForApi),
7777
+ options: item.options
7778
+ };
7779
+ }
7780
+ if (item.type === "computed") {
7781
+ return {
7782
+ type: "computed",
7783
+ id: item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
7784
+ label: item.label,
7785
+ expression: item.expression,
7786
+ options: {
7787
+ id: item.id,
7788
+ span: item.options?.span,
7789
+ format: item.options?.format,
7790
+ currency: item.options?.currency,
7791
+ visible: item.options?.visible
7792
+ }
7793
+ };
7794
+ }
7795
+ if (item.type === "field") {
7796
+ return {
7797
+ type: "field",
7798
+ field: item.field,
7799
+ options: item.options
7800
+ };
7801
+ }
7802
+ return item;
7803
+ }
7804
+ function serializeDetailForApi(detail) {
7805
+ if (detail === false) return false;
7806
+ if (detail === true) return true;
7807
+ if (!detail || !Array.isArray(detail)) return void 0;
7808
+ return detail.map(serializeDetailItemForApi);
7809
+ }
7072
7810
  function registerRoutes(app, config) {
7073
7811
  const optionsCache = /* @__PURE__ */ new Map();
7074
7812
  app.get("/api/schemas", optionalAuth(config), async (c) => {
@@ -7087,15 +7825,12 @@ function registerRoutes(app, config) {
7087
7825
  if (typeof access === "boolean") return access;
7088
7826
  if (access && typeof access === "object" && typeof access.policy === "string") {
7089
7827
  const policy = requestConfig.accessPolicies?.[access.policy];
7090
- if (typeof policy === "string" || typeof policy === "boolean")
7091
- return policy;
7828
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
7092
7829
  }
7093
7830
  return resolveBooleanAccess(requestConfig, access, accessArgs);
7094
7831
  };
7095
7832
  const filteredCollections = await Promise.all(
7096
- collections.filter(
7097
- (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
7098
- ).map(async (col) => ({
7833
+ collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
7099
7834
  slug: col.slug,
7100
7835
  labels: col.labels,
7101
7836
  access: {
@@ -7133,6 +7868,7 @@ function registerRoutes(app, config) {
7133
7868
  audit: !!col.audit,
7134
7869
  drafts: !!col.drafts,
7135
7870
  admin: col.admin,
7871
+ detail: serializeDetailForApi(col.detail),
7136
7872
  workflow: col.workflow ? {
7137
7873
  initialState: col.workflow.initialState,
7138
7874
  draftState: col.workflow.draftState,
@@ -7151,9 +7887,7 @@ function registerRoutes(app, config) {
7151
7887
  }))
7152
7888
  );
7153
7889
  const filteredGlobals = await Promise.all(
7154
- globals.filter(
7155
- (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
7156
- ).map(async (glb) => ({
7890
+ globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
7157
7891
  slug: glb.slug,
7158
7892
  label: glb.label,
7159
7893
  access: {
@@ -7183,7 +7917,8 @@ function registerRoutes(app, config) {
7183
7917
  }
7184
7918
  }))
7185
7919
  ),
7186
- admin: glb.admin
7920
+ admin: glb.admin,
7921
+ detail: serializeDetailForApi(glb.detail)
7187
7922
  }))
7188
7923
  );
7189
7924
  return c.json({
@@ -7197,148 +7932,137 @@ function registerRoutes(app, config) {
7197
7932
  adminHealth: {
7198
7933
  emailConfigured: !!requestConfig.email,
7199
7934
  secureAuthSecretConfigured: !!process.env.DYRECTED_JWT_SECRET,
7200
- authCollectionConfigured: requestConfig.collections.some(
7201
- (collection) => !!collection.auth
7202
- ),
7203
- uploadCollectionConfigured: requestConfig.collections.some(
7204
- (collection) => !!collection.upload
7205
- )
7935
+ authCollectionConfigured: requestConfig.collections.some((collection) => !!collection.auth),
7936
+ uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload)
7206
7937
  }
7207
7938
  });
7208
7939
  });
7209
- app.get(
7210
- "/api/dyrected/options/:collection/:field",
7211
- optionalAuth(config),
7212
- async (c) => {
7213
- const { collection: colSlug, field: fieldName } = c.req.param();
7214
- const siteId = c.req.header("X-Site-Id");
7215
- const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
7216
- let collections = [...requestConfig.collections];
7217
- const user = c.get("user");
7218
- let collection = collections.find((col) => col.slug === colSlug);
7219
- let field;
7220
- if (collection) {
7221
- const accessExpr = collection.access?.read;
7222
- if (accessExpr !== void 0 && accessExpr !== null) {
7223
- const allowed = await resolveBooleanAccess(config, accessExpr, {
7224
- user,
7225
- req: toHookRequestContext(c.req)
7226
- });
7227
- if (!allowed) {
7228
- return c.json(
7229
- { error: true, message: `Access denied: read on ${colSlug}` },
7230
- 403
7231
- );
7232
- }
7233
- }
7234
- field = collection.fields.find((f) => f.name === fieldName);
7235
- } else {
7236
- const globals = [...requestConfig.globals];
7237
- const glb = globals.find((g) => g.slug === colSlug);
7238
- if (!glb) {
7239
- return c.json(
7240
- {
7241
- error: true,
7242
- message: `${colSlug} not found as collection or global`
7243
- },
7244
- 404
7245
- );
7246
- }
7247
- const accessExpr = glb.access?.read;
7248
- if (accessExpr !== void 0 && accessExpr !== null) {
7249
- const allowed = await resolveBooleanAccess(config, accessExpr, {
7250
- user,
7251
- req: toHookRequestContext(c.req)
7252
- });
7253
- if (!allowed) {
7254
- return c.json(
7255
- {
7256
- error: true,
7257
- message: `Access denied: read on global ${colSlug}`
7258
- },
7259
- 403
7260
- );
7261
- }
7940
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
7941
+ const { collection: colSlug, field: fieldName } = c.req.param();
7942
+ const siteId = c.req.header("X-Site-Id");
7943
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
7944
+ let collections = [...requestConfig.collections];
7945
+ const user = c.get("user");
7946
+ let collection = collections.find((col) => col.slug === colSlug);
7947
+ let field;
7948
+ if (collection) {
7949
+ const accessExpr = collection.access?.read;
7950
+ if (accessExpr !== void 0 && accessExpr !== null) {
7951
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
7952
+ user,
7953
+ req: toHookRequestContext(c.req)
7954
+ });
7955
+ if (!allowed) {
7956
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
7262
7957
  }
7263
- field = glb.fields.find((f) => f.name === fieldName);
7264
7958
  }
7265
- if (!field) {
7959
+ field = collection.fields.find((f) => f.name === fieldName);
7960
+ } else {
7961
+ const globals = [...requestConfig.globals];
7962
+ const glb = globals.find((g) => g.slug === colSlug);
7963
+ if (!glb) {
7266
7964
  return c.json(
7267
7965
  {
7268
7966
  error: true,
7269
- message: `Field ${fieldName} not found in ${colSlug}`
7967
+ message: `${colSlug} not found as collection or global`
7270
7968
  },
7271
7969
  404
7272
7970
  );
7273
7971
  }
7274
- let resolver;
7275
- let cacheTTL;
7276
- if (typeof field.options === "function") {
7277
- resolver = field.options;
7278
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
7279
- resolver = field.options.resolve;
7280
- cacheTTL = field.options.cacheTTL;
7281
- }
7282
- if (!resolver) {
7283
- return c.json(
7284
- {
7285
- error: true,
7286
- message: `Field ${fieldName} in ${colSlug} is not dynamic`
7287
- },
7288
- 400
7289
- );
7290
- }
7291
- try {
7292
- const db = c.get("db") || config.db;
7293
- const queryParams = c.req.query();
7294
- const reqContext = {
7295
- query: queryParams,
7296
- headers: c.req.header(),
7297
- raw: c.req.raw
7298
- };
7299
- const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
7300
- let cacheKey = "";
7301
- if (shouldCache) {
7302
- cacheKey = JSON.stringify([
7303
- siteId ?? "",
7304
- colSlug,
7305
- fieldName,
7306
- user?.id ?? null,
7307
- queryParams
7308
- ]);
7309
- const hit = optionsCache.get(cacheKey);
7310
- if (hit && hit.expires > Date.now()) {
7311
- return c.json(hit.value);
7312
- }
7313
- }
7314
- const result = await resolver({
7315
- db,
7972
+ const accessExpr = glb.access?.read;
7973
+ if (accessExpr !== void 0 && accessExpr !== null) {
7974
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
7316
7975
  user,
7317
- req: reqContext
7976
+ req: toHookRequestContext(c.req)
7318
7977
  });
7319
- if (shouldCache) {
7320
- optionsCache.set(cacheKey, {
7321
- expires: Date.now() + cacheTTL * 1e3,
7322
- value: result
7323
- });
7978
+ if (!allowed) {
7979
+ return c.json(
7980
+ {
7981
+ error: true,
7982
+ message: `Access denied: read on global ${colSlug}`
7983
+ },
7984
+ 403
7985
+ );
7324
7986
  }
7325
- return c.json(result);
7326
- } catch (err) {
7327
- getRequestLogger(c, "router").error({
7328
- err,
7329
- msg: "Failed to resolve dynamic field options",
7330
- fieldName
7987
+ }
7988
+ field = glb.fields.find((f) => f.name === fieldName);
7989
+ }
7990
+ if (!field) {
7991
+ return c.json(
7992
+ {
7993
+ error: true,
7994
+ message: `Field ${fieldName} not found in ${colSlug}`
7995
+ },
7996
+ 404
7997
+ );
7998
+ }
7999
+ let resolver;
8000
+ let cacheTTL;
8001
+ if (typeof field.options === "function") {
8002
+ resolver = field.options;
8003
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
8004
+ resolver = field.options.resolve;
8005
+ cacheTTL = field.options.cacheTTL;
8006
+ }
8007
+ if (!resolver) {
8008
+ return c.json(
8009
+ {
8010
+ error: true,
8011
+ message: `Field ${fieldName} in ${colSlug} is not dynamic`
8012
+ },
8013
+ 400
8014
+ );
8015
+ }
8016
+ try {
8017
+ const db = c.get("db") || config.db;
8018
+ const queryParams = c.req.query();
8019
+ const reqContext = {
8020
+ query: queryParams,
8021
+ headers: c.req.header(),
8022
+ raw: c.req.raw
8023
+ };
8024
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
8025
+ let cacheKey = "";
8026
+ if (shouldCache) {
8027
+ cacheKey = JSON.stringify([
8028
+ siteId ?? "",
8029
+ colSlug,
8030
+ fieldName,
8031
+ user?.id ?? null,
8032
+ queryParams
8033
+ ]);
8034
+ const hit = optionsCache.get(cacheKey);
8035
+ if (hit && hit.expires > Date.now()) {
8036
+ return c.json(hit.value);
8037
+ }
8038
+ }
8039
+ const result = await resolver({
8040
+ db,
8041
+ user,
8042
+ req: reqContext
8043
+ });
8044
+ if (shouldCache) {
8045
+ optionsCache.set(cacheKey, {
8046
+ expires: Date.now() + cacheTTL * 1e3,
8047
+ value: result
7331
8048
  });
7332
- return c.json(
7333
- {
7334
- error: true,
7335
- message: err.message || "Failed to resolve dynamic options"
7336
- },
7337
- 500
7338
- );
7339
8049
  }
8050
+ return c.json(result);
8051
+ } catch (err) {
8052
+ getRequestLogger(c, "router").error({
8053
+ err,
8054
+ msg: "Failed to resolve dynamic field options",
8055
+ fieldName
8056
+ });
8057
+ return c.json(
8058
+ {
8059
+ error: true,
8060
+ message: err.message || "Failed to resolve dynamic options"
8061
+ },
8062
+ 500
8063
+ );
7340
8064
  }
7341
- );
8065
+ });
7342
8066
  app.get("/api/openapi.json", (c) => {
7343
8067
  return c.json(generateOpenApi(config));
7344
8068
  });
@@ -7351,13 +8075,8 @@ function registerRoutes(app, config) {
7351
8075
  const key = c.req.param("key");
7352
8076
  const scope = c.req.query("scope");
7353
8077
  if (!db) return c.json({ message: "Database not configured" }, 500);
7354
- if (!user?.collection || !user.sub)
7355
- return c.json({ error: true, message: "Authentication required." }, 401);
7356
- if (!key)
7357
- return c.json(
7358
- { error: true, message: "Preference key is required." },
7359
- 400
7360
- );
8078
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8079
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7361
8080
  const getGlobalPreference = async () => {
7362
8081
  const globalDoc = await db.findOne({
7363
8082
  collection: "__global_preferences",
@@ -7384,13 +8103,8 @@ function registerRoutes(app, config) {
7384
8103
  const key = c.req.param("key");
7385
8104
  const scope = c.req.query("scope");
7386
8105
  if (!db) return c.json({ message: "Database not configured" }, 500);
7387
- if (!user?.collection || !user.sub)
7388
- return c.json({ error: true, message: "Authentication required." }, 401);
7389
- if (!key)
7390
- return c.json(
7391
- { error: true, message: "Preference key is required." },
7392
- 400
7393
- );
8106
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8107
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7394
8108
  const body = await c.req.json().catch(() => ({}));
7395
8109
  if (scope === "global") {
7396
8110
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
@@ -7438,13 +8152,8 @@ function registerRoutes(app, config) {
7438
8152
  const key = c.req.param("key");
7439
8153
  const scope = c.req.query("scope");
7440
8154
  if (!db) return c.json({ message: "Database not configured" }, 500);
7441
- if (!user?.collection || !user.sub)
7442
- return c.json({ error: true, message: "Authentication required." }, 401);
7443
- if (!key)
7444
- return c.json(
7445
- { error: true, message: "Preference key is required." },
7446
- 400
7447
- );
8155
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8156
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7448
8157
  if (scope === "global") {
7449
8158
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
7450
8159
  if (!isAdminUser) {
@@ -7483,41 +8192,17 @@ function registerRoutes(app, config) {
7483
8192
  for (const col of uploadCollections) {
7484
8193
  const mediaController = new MediaController(col.slug);
7485
8194
  const prefix = `/api/collections/${col.slug}`;
7486
- app.get(
7487
- `${prefix}/media`,
7488
- accessGate(config, col, "read"),
7489
- (c) => mediaController.find(c)
7490
- );
7491
- app.get(
7492
- `${prefix}/media/:filename{.+$}`,
7493
- (c) => mediaController.serve(c)
7494
- );
7495
- app.post(
7496
- `${prefix}/media`,
7497
- accessGate(config, col, "create"),
7498
- (c) => mediaController.upload(c)
7499
- );
7500
- app.delete(
7501
- `${prefix}/media/:id`,
7502
- accessGate(config, col, "delete"),
7503
- (c) => mediaController.delete(c)
7504
- );
8195
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
8196
+ app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
8197
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
8198
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
7505
8199
  }
7506
8200
  }
7507
8201
  const adminAuthController = new AdminAuthController(config);
7508
8202
  app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
7509
- app.get(
7510
- "/api/admin/auth/:provider/start",
7511
- (c) => adminAuthController.start(c)
7512
- );
7513
- app.get(
7514
- "/api/admin/auth/:provider/callback",
7515
- (c) => adminAuthController.callback(c)
7516
- );
7517
- app.post(
7518
- "/api/admin/auth/:provider/exchange",
7519
- (c) => adminAuthController.exchange(c)
7520
- );
8203
+ app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
8204
+ app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
8205
+ app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
7521
8206
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
7522
8207
  for (const collection of config.collections) {
7523
8208
  if (!collection.auth) continue;
@@ -7528,21 +8213,10 @@ function registerRoutes(app, config) {
7528
8213
  app.get(`${path}/init`, (c) => authController.init(c));
7529
8214
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
7530
8215
  app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
7531
- app.post(
7532
- `${path}/refresh-token`,
7533
- requireAuth(config),
7534
- (c) => authController.refreshToken(c)
7535
- );
7536
- app.post(
7537
- `${path}/forgot-password`,
7538
- (c) => authController.forgotPassword(c)
7539
- );
8216
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
8217
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
7540
8218
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
7541
- app.post(
7542
- `${path}/invite`,
7543
- requireAuth(config),
7544
- (c) => authController.invite(c)
7545
- );
8219
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
7546
8220
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
7547
8221
  }
7548
8222
  const auditController = new AuditController();
@@ -7553,33 +8227,19 @@ function registerRoutes(app, config) {
7553
8227
  app.post(path, (c) => controller.create(c));
7554
8228
  app.post(`${path}/media`, (c) => controller.create(c));
7555
8229
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
8230
+ app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
7556
8231
  if (collection.audit) {
7557
- app.get(
7558
- `${path}/__audit`,
7559
- (c) => auditController.findForCollection(c, collection)
7560
- );
8232
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
7561
8233
  }
7562
8234
  app.get(`${path}/:id`, (c) => controller.findOne(c));
7563
8235
  app.patch(`${path}/:id`, (c) => controller.update(c));
7564
8236
  app.delete(`${path}/:id`, (c) => controller.delete(c));
7565
8237
  if (collection.auth) {
7566
- app.post(
7567
- `${path}/:id/change-password`,
7568
- requireAuth(config),
7569
- (c) => controller.changePassword(c)
7570
- );
8238
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
7571
8239
  }
7572
8240
  if (collection.workflow) {
7573
- app.post(
7574
- `${path}/:id/transitions/:transition`,
7575
- requireAuth(config),
7576
- (c) => controller.transition(c)
7577
- );
7578
- app.get(
7579
- `${path}/:id/workflow-history`,
7580
- requireAuth(config),
7581
- (c) => controller.workflowHistory(c)
7582
- );
8241
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
8242
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
7583
8243
  }
7584
8244
  }
7585
8245
  for (const global of config.globals) {
@@ -7594,11 +8254,7 @@ function registerRoutes(app, config) {
7594
8254
  });
7595
8255
  }
7596
8256
  const previewController = new PreviewController();
7597
- app.post(
7598
- "/api/preview-token",
7599
- requireAuth(config),
7600
- (c) => previewController.createToken(c)
7601
- );
8257
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
7602
8258
  app.get("/api/preview-data", (c) => previewController.getData(c));
7603
8259
  app.get("/api/audit", (c) => auditController.findAll(c));
7604
8260
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -7611,81 +8267,49 @@ function registerRoutes(app, config) {
7611
8267
  if (!config2.onSchemaFetch || !siteId) {
7612
8268
  return c.json({ message: `Collection "${slug}" not found` }, 404);
7613
8269
  }
7614
- const requestConfig = mergeDynamicConfig(
7615
- config2,
7616
- await config2.onSchemaFetch(siteId)
7617
- );
7618
- const collection = requestConfig.collections.find(
7619
- (col) => col.slug === slug
7620
- );
8270
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8271
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
7621
8272
  if (!collection?.audit) {
7622
- return c.json(
7623
- { message: `Collection "${slug}" not found or has no audit log` },
7624
- 404
7625
- );
8273
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
7626
8274
  }
7627
8275
  return auditController.findForCollection(c, collection);
7628
8276
  });
7629
- app.post(
7630
- "/api/collections/:slug/:id/transitions/:transition",
7631
- requireAuth(config),
7632
- async (c) => {
7633
- const slug = c.req.param("slug");
7634
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
7635
- const config2 = c.get("config");
7636
- if (config2.collections.some((col) => col.slug === slug)) {
7637
- return c.json({ message: "Not Found" }, 404);
7638
- }
7639
- if (!config2.onSchemaFetch || !siteId) {
7640
- return c.json({ message: `Collection "${slug}" not found` }, 404);
7641
- }
7642
- const requestConfig = mergeDynamicConfig(
7643
- config2,
7644
- await config2.onSchemaFetch(siteId)
7645
- );
7646
- const collection = requestConfig.collections.find(
7647
- (col) => col.slug === slug
7648
- );
7649
- if (!collection?.workflow) {
7650
- return c.json(
7651
- { message: `Collection "${slug}" not found or has no workflow` },
7652
- 404
7653
- );
7654
- }
7655
- const controller = new CollectionController(collection);
7656
- return controller.transition(c);
8277
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
8278
+ const slug = c.req.param("slug");
8279
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8280
+ const config2 = c.get("config");
8281
+ if (config2.collections.some((col) => col.slug === slug)) {
8282
+ return c.json({ message: "Not Found" }, 404);
7657
8283
  }
7658
- );
7659
- app.get(
7660
- "/api/collections/:slug/:id/workflow-history",
7661
- requireAuth(config),
7662
- async (c) => {
7663
- const slug = c.req.param("slug");
7664
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
7665
- const config2 = c.get("config");
7666
- if (config2.collections.some((col) => col.slug === slug)) {
7667
- return c.json({ message: "Not Found" }, 404);
7668
- }
7669
- if (!config2.onSchemaFetch || !siteId) {
7670
- return c.json({ message: `Collection "${slug}" not found` }, 404);
7671
- }
7672
- const requestConfig = mergeDynamicConfig(
7673
- config2,
7674
- await config2.onSchemaFetch(siteId)
7675
- );
7676
- const collection = requestConfig.collections.find(
7677
- (col) => col.slug === slug
7678
- );
7679
- if (!collection?.workflow) {
7680
- return c.json(
7681
- { message: `Collection "${slug}" not found or has no workflow` },
7682
- 404
7683
- );
7684
- }
7685
- const controller = new CollectionController(collection);
7686
- return controller.workflowHistory(c);
8284
+ if (!config2.onSchemaFetch || !siteId) {
8285
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
7687
8286
  }
7688
- );
8287
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8288
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
8289
+ if (!collection?.workflow) {
8290
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
8291
+ }
8292
+ const controller = new CollectionController(collection);
8293
+ return controller.transition(c);
8294
+ });
8295
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
8296
+ const slug = c.req.param("slug");
8297
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8298
+ const config2 = c.get("config");
8299
+ if (config2.collections.some((col) => col.slug === slug)) {
8300
+ return c.json({ message: "Not Found" }, 404);
8301
+ }
8302
+ if (!config2.onSchemaFetch || !siteId) {
8303
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
8304
+ }
8305
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8306
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
8307
+ if (!collection?.workflow) {
8308
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
8309
+ }
8310
+ const controller = new CollectionController(collection);
8311
+ return controller.workflowHistory(c);
8312
+ });
7689
8313
  app.all("/api/collections/:slug/:id?", async (c) => {
7690
8314
  const slug = c.req.param("slug");
7691
8315
  const id = c.req.param("id");
@@ -7695,13 +8319,8 @@ function registerRoutes(app, config) {
7695
8319
  return c.json({ message: "Method Not Allowed" }, 405);
7696
8320
  }
7697
8321
  if (config2.onSchemaFetch && siteId) {
7698
- const requestConfig = mergeDynamicConfig(
7699
- config2,
7700
- await config2.onSchemaFetch(siteId)
7701
- );
7702
- let collection = requestConfig.collections.find(
7703
- (col) => col.slug === slug
7704
- );
8322
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8323
+ let collection = requestConfig.collections.find((col) => col.slug === slug);
7705
8324
  if (!collection && slug === "media") {
7706
8325
  collection = {
7707
8326
  slug: "media",
@@ -7714,25 +8333,19 @@ function registerRoutes(app, config) {
7714
8333
  if (collection.auth && id) {
7715
8334
  const authController = new AuthController(collection);
7716
8335
  const method2 = c.req.method;
7717
- if (method2 === "POST" && id === "login")
7718
- return authController.login(c);
7719
- if (method2 === "POST" && id === "logout")
7720
- return authController.logout(c);
8336
+ if (method2 === "POST" && id === "login") return authController.login(c);
8337
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
7721
8338
  if (method2 === "GET" && id === "me") return authController.me(c);
7722
- if (method2 === "POST" && id === "refresh-token")
7723
- return authController.refreshToken(c);
7724
- if (method2 === "POST" && id === "forgot-password")
7725
- return authController.forgotPassword(c);
7726
- if (method2 === "POST" && id === "reset-password")
7727
- return authController.resetPassword(c);
8339
+ if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
8340
+ if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
8341
+ if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
7728
8342
  }
7729
8343
  const controller = new CollectionController(collection);
7730
8344
  const method = c.req.method;
7731
8345
  if (id) {
7732
8346
  if (method === "GET") return controller.findOne(c);
7733
8347
  if (method === "PATCH") return controller.update(c);
7734
- if (method === "DELETE" && id === "delete-many")
7735
- return controller.deleteMany(c);
8348
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
7736
8349
  if (method === "DELETE") return controller.delete(c);
7737
8350
  if (method === "POST" && id === "media") return controller.create(c);
7738
8351
  } else {
@@ -7752,10 +8365,7 @@ function registerRoutes(app, config) {
7752
8365
  return c.json({ message: "Method Not Allowed" }, 405);
7753
8366
  }
7754
8367
  if (config2.onSchemaFetch && siteId) {
7755
- const requestConfig = mergeDynamicConfig(
7756
- config2,
7757
- await config2.onSchemaFetch(siteId)
7758
- );
8368
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
7759
8369
  const global = requestConfig.globals.find((glb) => glb.slug === slug);
7760
8370
  if (global) {
7761
8371
  const controller = new GlobalController(global);
@@ -8121,7 +8731,7 @@ function resolveClientIp(req, trustProxy = false) {
8121
8731
  const value = normalizeIp(req.headers[header]);
8122
8732
  if (value) return value;
8123
8733
  }
8124
- if (trustProxy) {
8734
+ if (trustProxy === true || typeof trustProxy === "number") {
8125
8735
  const forwardedFor = pickForwardedFor(req.headers, trustProxy);
8126
8736
  if (forwardedFor) return forwardedFor;
8127
8737
  }
@@ -8133,6 +8743,7 @@ function pickForwardedFor(headers, trustProxy) {
8133
8743
  const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
8134
8744
  if (chain2.length > 0) {
8135
8745
  if (trustProxy === true) return chain2[0];
8746
+ if (trustProxy <= 0) return null;
8136
8747
  const trustedHops2 = Math.max(1, Math.floor(trustProxy));
8137
8748
  const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
8138
8749
  return chain2[candidateIndex2] ?? chain2[0];
@@ -8145,6 +8756,7 @@ function pickForwardedFor(headers, trustProxy) {
8145
8756
  const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
8146
8757
  if (chain.length === 0) return null;
8147
8758
  if (trustProxy === true) return chain[0];
8759
+ if (trustProxy <= 0) return null;
8148
8760
  const trustedHops = Math.max(1, Math.floor(trustProxy));
8149
8761
  const candidateIndex = Math.max(0, chain.length - trustedHops);
8150
8762
  return chain[candidateIndex] ?? chain[0];
@@ -8182,15 +8794,15 @@ function createRateLimitMiddleware(config) {
8182
8794
  })) {
8183
8795
  return next();
8184
8796
  }
8185
- const now = Date.now();
8797
+ const now2 = Date.now();
8186
8798
  const key = ip;
8187
8799
  const existing = buckets.get(key);
8188
- const bucket = existing && existing.resetAt > now ? existing : { count: 0, resetAt: now + settings.window };
8800
+ const bucket = existing && existing.resetAt > now2 ? existing : { count: 0, resetAt: now2 + settings.window };
8189
8801
  bucket.count += 1;
8190
8802
  buckets.set(key, bucket);
8191
- pruneExpiredBuckets(buckets, now);
8803
+ pruneExpiredBuckets(buckets, now2);
8192
8804
  const remaining = Math.max(0, settings.max - bucket.count);
8193
- const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
8805
+ const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now2) / 1e3));
8194
8806
  c.res.headers.set("X-RateLimit-Limit", String(settings.max));
8195
8807
  c.res.headers.set("X-RateLimit-Remaining", String(remaining));
8196
8808
  c.res.headers.set("X-RateLimit-Reset", String(Math.ceil(bucket.resetAt / 1e3)));
@@ -8221,16 +8833,17 @@ function normalizeRateLimitConfig(config) {
8221
8833
  function matchesLimitedPath(path, prefixes) {
8222
8834
  return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
8223
8835
  }
8224
- function pruneExpiredBuckets(buckets, now) {
8836
+ function pruneExpiredBuckets(buckets, now2) {
8225
8837
  if (buckets.size < 1e3) return;
8226
8838
  for (const [key, bucket] of buckets) {
8227
- if (bucket.resetAt <= now) {
8839
+ if (bucket.resetAt <= now2) {
8228
8840
  buckets.delete(key);
8229
8841
  }
8230
8842
  }
8231
8843
  }
8232
8844
 
8233
8845
  // src/app.ts
8846
+ init_observability();
8234
8847
  async function createDyrectedApp(rawConfig) {
8235
8848
  const config = normalizeConfig(rawConfig);
8236
8849
  const observability = createObservabilityRuntime(config);
@@ -8348,7 +8961,7 @@ async function createDyrectedApp(rawConfig) {
8348
8961
  app.use("*", (0, import_cors.cors)(config.cors ? { origin: config.cors.origins } : {}));
8349
8962
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
8350
8963
  if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
8351
- app.get(observability.config.metrics.path, async (c) => {
8964
+ app.get(observability.config.metrics.path, async (_c) => {
8352
8965
  const metrics = await renderPrometheusMetrics(
8353
8966
  observability.prometheusExporter
8354
8967
  );
@@ -8382,6 +8995,9 @@ async function createDyrectedApp(rawConfig) {
8382
8995
  registerRoutes(app, config);
8383
8996
  return app;
8384
8997
  }
8998
+
8999
+ // src/server.ts
9000
+ init_observability();
8385
9001
  // Annotate the CommonJS export names for ESM import in node:
8386
9002
  0 && (module.exports = {
8387
9003
  AuthController,
@@ -8389,6 +9005,22 @@ async function createDyrectedApp(rawConfig) {
8389
9005
  GlobalController,
8390
9006
  MediaController,
8391
9007
  PreviewController,
9008
+ attachRequestMetrics,
9009
+ bindObservabilityRuntime,
9010
+ buildRequestLogger,
9011
+ captureRequestBody,
8392
9012
  createDyrectedApp,
8393
- registerRoutes
9013
+ createObservabilityRuntime,
9014
+ createRequestSpan,
9015
+ endRequestSpan,
9016
+ getConfigLogger,
9017
+ getObservabilityRuntime,
9018
+ getRequestLogger,
9019
+ redactBody,
9020
+ redactHeaders,
9021
+ registerRoutes,
9022
+ renderPrometheusMetrics,
9023
+ resolveObservabilityConfig,
9024
+ serializeDetailForApi,
9025
+ shouldSampleRequest
8394
9026
  });