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