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