@dyrected/core 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/app-config-7G-3tF_I.d.cts +2259 -0
  2. package/dist/app-config-7G-3tF_I.d.ts +2259 -0
  3. package/dist/app-config-B3ByBjCN.d.cts +2222 -0
  4. package/dist/app-config-B3ByBjCN.d.ts +2222 -0
  5. package/dist/app-config-BIkjQWzU.d.cts +2218 -0
  6. package/dist/app-config-BIkjQWzU.d.ts +2218 -0
  7. package/dist/app-config-BfpeE3bF.d.cts +2100 -0
  8. package/dist/app-config-BfpeE3bF.d.ts +2100 -0
  9. package/dist/app-config-CCe-yq9q.d.cts +2222 -0
  10. package/dist/app-config-CCe-yq9q.d.ts +2222 -0
  11. package/dist/app-config-CPOs7THF.d.cts +2217 -0
  12. package/dist/app-config-CPOs7THF.d.ts +2217 -0
  13. package/dist/app-config-Ctp454zB.d.cts +2214 -0
  14. package/dist/app-config-Ctp454zB.d.ts +2214 -0
  15. package/dist/app-config-DJmNYgYh.d.cts +2305 -0
  16. package/dist/app-config-DJmNYgYh.d.ts +2305 -0
  17. package/dist/app-config-DO3laV58.d.cts +2397 -0
  18. package/dist/app-config-DO3laV58.d.ts +2397 -0
  19. package/dist/app-config-DZf_mmi9.d.cts +2211 -0
  20. package/dist/app-config-DZf_mmi9.d.ts +2211 -0
  21. package/dist/app-config-SkUoN5JU.d.cts +2192 -0
  22. package/dist/app-config-SkUoN5JU.d.ts +2192 -0
  23. package/dist/app-config-oz8H_MOX.d.cts +2224 -0
  24. package/dist/app-config-oz8H_MOX.d.ts +2224 -0
  25. package/dist/chunk-2UXLA4YN.js +3139 -0
  26. package/dist/chunk-3TRRTN6U.js +3441 -0
  27. package/dist/chunk-5U2WRCNH.js +3140 -0
  28. package/dist/chunk-5ZIA5COH.js +3414 -0
  29. package/dist/chunk-6TWQ6GTD.js +3480 -0
  30. package/dist/chunk-AKYYGMFS.js +3365 -0
  31. package/dist/chunk-AQO4HI2Y.js +3204 -0
  32. package/dist/chunk-DFBLNYUH.js +3402 -0
  33. package/dist/chunk-EJG6AMYB.js +3537 -0
  34. package/dist/chunk-FRSJJO27.js +519 -0
  35. package/dist/chunk-G43REXPX.js +3147 -0
  36. package/dist/chunk-GFKBPBJJ.js +3637 -0
  37. package/dist/chunk-KEKOEAX6.js +3139 -0
  38. package/dist/chunk-LFGSRQLX.js +3568 -0
  39. package/dist/chunk-NNZRYF3U.js +3537 -0
  40. package/dist/chunk-PRQ2RF5C.js +3485 -0
  41. package/dist/chunk-PYE2JSY4.js +3568 -0
  42. package/dist/chunk-PZMDK3MH.js +3203 -0
  43. package/dist/chunk-QCXLDKZO.js +3272 -0
  44. package/dist/chunk-SM32ND5M.js +3472 -0
  45. package/dist/chunk-UKUEKTAO.js +513 -0
  46. package/dist/chunk-VM43G3IG.js +506 -0
  47. package/dist/chunk-VWJKFZQU.js +3139 -0
  48. package/dist/chunk-XX7N64NL.js +3550 -0
  49. package/dist/chunk-YSSHLGQ6.js +3139 -0
  50. package/dist/chunk-Z7CFUYVP.js +3653 -0
  51. package/dist/index.cjs +1421 -82
  52. package/dist/index.d.cts +362 -3
  53. package/dist/index.d.ts +362 -3
  54. package/dist/index.js +119 -1
  55. package/dist/observability-CNM4QFE4.js +34 -0
  56. package/dist/observability-KCMTV7GO.js +34 -0
  57. package/dist/observability-L5BORBB2.js +34 -0
  58. package/dist/server.cjs +1879 -1247
  59. package/dist/server.d.cts +52 -8
  60. package/dist/server.d.ts +52 -8
  61. package/dist/server.js +449 -387
  62. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -4,25 +4,16 @@ import {
4
4
  assertValidDeclarativeAccessInConfig,
5
5
  assertValidDeclarativeHooksInConfig,
6
6
  assertValidPreviewUrlsInConfig,
7
- attachRequestMetrics,
8
- bindObservabilityRuntime,
9
- buildRequestLogger,
10
7
  canViewWorkflowDraft,
11
- captureRequestBody,
12
8
  collectConfigDiagnostics,
13
- createObservabilityRuntime,
14
- createRequestSpan,
15
9
  createWorkflowDocument,
16
- endRequestSpan,
10
+ evaluateDetailComputed,
17
11
  executeFieldAfterRead,
18
12
  executeFieldBeforeChange,
19
13
  generateOpenApi,
20
14
  getAdminAuthCollection,
21
15
  getAuthSession,
22
- getConfigLogger,
23
- getObservabilityRuntime,
24
16
  getPublicAdminAuthConfig,
25
- getRequestLogger,
26
17
  initializeWorkflowDocument,
27
18
  isAuthSessionActive,
28
19
  issueAuthSessionToken,
@@ -30,18 +21,32 @@ import {
30
21
  mergeDynamicConfig,
31
22
  normalizeConfig,
32
23
  publishedStateName,
33
- redactHeaders,
34
- renderPrometheusMetrics,
35
24
  revokeAllAuthSessions,
36
25
  revokeAuthSession,
37
26
  runCollectionHooks,
38
27
  saveWorkflowDraft,
39
- shouldSampleRequest,
40
28
  signCollectionToken,
41
29
  touchAuthSession,
42
30
  transitionWorkflow,
43
31
  verifyCollectionToken
44
- } from "./chunk-YEBCT77K.js";
32
+ } from "./chunk-XX7N64NL.js";
33
+ import {
34
+ attachRequestMetrics,
35
+ bindObservabilityRuntime,
36
+ buildRequestLogger,
37
+ captureRequestBody,
38
+ createObservabilityRuntime,
39
+ createRequestSpan,
40
+ endRequestSpan,
41
+ getConfigLogger,
42
+ getObservabilityRuntime,
43
+ getRequestLogger,
44
+ redactBody,
45
+ redactHeaders,
46
+ renderPrometheusMetrics,
47
+ resolveObservabilityConfig,
48
+ shouldSampleRequest
49
+ } from "./chunk-FRSJJO27.js";
45
50
 
46
51
  // src/app.ts
47
52
  import { Hono } from "hono";
@@ -436,11 +441,7 @@ async function evaluateAccess(expression, context) {
436
441
  }
437
442
  return !!result;
438
443
  } catch (err) {
439
- getConfigLogger(context.config, "access").error({
440
- err,
441
- msg: "Jexl evaluation failed",
442
- expression
443
- });
444
+ console.error("[dyrected:access] Failed to evaluate access expression:", err);
444
445
  return false;
445
446
  }
446
447
  }
@@ -458,20 +459,14 @@ async function resolveAccess(config, access, args) {
458
459
  try {
459
460
  return await access(args);
460
461
  } catch (err) {
461
- getConfigLogger(config, "access").error({
462
- err,
463
- msg: "Functional access check failed"
464
- });
462
+ console.error("[dyrected:access] Functional access check failed:", err);
465
463
  return false;
466
464
  }
467
465
  }
468
466
  if (isNamedAccessPolicy(access)) {
469
467
  const policy = config.accessPolicies?.[access.policy];
470
468
  if (policy === void 0) {
471
- getConfigLogger(config, "access").error({
472
- msg: "Unknown access policy",
473
- policy: access.policy
474
- });
469
+ console.error(`[dyrected:access] Unknown access policy "${access.policy}"`);
475
470
  return false;
476
471
  }
477
472
  if (typeof policy === "string" || typeof policy === "boolean") {
@@ -481,11 +476,7 @@ async function resolveAccess(config, access, args) {
481
476
  const resolver = policy;
482
477
  return await resolver({ ...args, params: access.params });
483
478
  } catch (err) {
484
- getConfigLogger(config, "access").error({
485
- err,
486
- msg: "Access policy failed",
487
- policy: access.policy
488
- });
479
+ console.error(`[dyrected:access] Access policy "${access.policy}" failed:`, err);
489
480
  return false;
490
481
  }
491
482
  }
@@ -875,6 +866,22 @@ var CollectionController = class {
875
866
  toHookRequestContext(c) {
876
867
  return toHookRequestContext(c.req);
877
868
  }
869
+ sanitizeDoc(doc) {
870
+ if (!doc || typeof doc !== "object") return doc;
871
+ if (this.collection.auth) {
872
+ const {
873
+ password: _p,
874
+ salt: _s,
875
+ resetPasswordToken: _rpt,
876
+ resetPasswordExpires: _rpe,
877
+ loginAttempts: _la,
878
+ lockUntil: _lu,
879
+ ...safeDoc
880
+ } = doc;
881
+ return safeDoc;
882
+ }
883
+ return doc;
884
+ }
878
885
  async evaluateAccess(c, action, options = {}) {
879
886
  const config = c.get("config");
880
887
  return resolveCollectionAccess(
@@ -1107,6 +1114,9 @@ var CollectionController = class {
1107
1114
  depth
1108
1115
  );
1109
1116
  }
1117
+ if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
1118
+ result.docs = result.docs.map((d) => this.sanitizeDoc(d));
1119
+ }
1110
1120
  return c.json(result);
1111
1121
  }
1112
1122
  async findOne(c) {
@@ -1123,32 +1133,51 @@ var CollectionController = class {
1123
1133
  id: id2
1124
1134
  });
1125
1135
  const externalSubject = localDoc?.externalSubject || id2;
1126
- let member = null;
1127
1136
  if (provider.members.get) {
1128
- member = await provider.members.get({ externalSubject, req: hookReq });
1137
+ const member = await provider.members.get({
1138
+ externalSubject,
1139
+ req: hookReq
1140
+ });
1141
+ if (member) {
1142
+ return c.json({
1143
+ ...member,
1144
+ id: localDoc ? localDoc.id : member.id,
1145
+ externalSubject: member.id
1146
+ });
1147
+ }
1129
1148
  } else if (provider.members.list) {
1130
1149
  const listResult = await provider.members.list({ req: hookReq });
1131
- member = listResult.docs.find((m) => m.id === externalSubject) || null;
1150
+ const matched = listResult.docs.find((m) => m.id === externalSubject || m.id === id2);
1151
+ if (matched) {
1152
+ return c.json({
1153
+ ...matched,
1154
+ id: localDoc ? localDoc.id : matched.id,
1155
+ externalSubject: matched.id
1156
+ });
1157
+ }
1132
1158
  }
1133
- if (!member) return c.json({ message: "Not Found" }, 404);
1134
- return c.json({
1135
- ...member,
1136
- id: localDoc ? localDoc.id : member.id,
1137
- externalSubject: member.id
1138
- });
1139
1159
  }
1140
- const readonlyDb = createReadonlyDb(db);
1141
1160
  const id = c.req.param("id");
1142
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1143
- const user = c.get("user");
1144
1161
  if (!id) return c.json({ message: "Missing ID" }, 400);
1145
- let doc = await db.findOne({ collection: this.collection.slug, id });
1146
- if (!doc) return c.json({ message: "Not Found" }, 404);
1162
+ const depth = Number.parseInt(c.req.query("depth") || "0", 10);
1163
+ const user = c.get("user");
1164
+ const doc = await db.findOne({
1165
+ collection: this.collection.slug,
1166
+ id
1167
+ });
1168
+ if (!doc) {
1169
+ return c.json({ message: "Document not found" }, 404);
1170
+ }
1171
+ let effectiveDoc = doc;
1147
1172
  if (this.collection.workflow) {
1148
- doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
1149
- if (!doc) return c.json({ message: "Not Found" }, 404);
1173
+ effectiveDoc = materializeWorkflowDocument(
1174
+ doc,
1175
+ this.collection.workflow,
1176
+ user
1177
+ );
1178
+ if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
1150
1179
  }
1151
- const access = await this.evaluateAccess(c, "read", { id, doc });
1180
+ const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
1152
1181
  if (!access.allowed) {
1153
1182
  return c.json(
1154
1183
  {
@@ -1158,7 +1187,11 @@ var CollectionController = class {
1158
1187
  403
1159
1188
  );
1160
1189
  }
1161
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
1190
+ const docWithDefaults = DefaultsService.apply(
1191
+ this.collection.fields,
1192
+ effectiveDoc
1193
+ );
1194
+ const readonlyDb = createReadonlyDb(db);
1162
1195
  const docWithCollectionHooks = await runCollectionHooks(
1163
1196
  this.collection.hooks?.afterRead,
1164
1197
  {
@@ -1184,17 +1217,20 @@ var CollectionController = class {
1184
1217
  },
1185
1218
  docWithFieldHooks
1186
1219
  );
1220
+ let finalDoc = docWithFieldAccess;
1187
1221
  if (depth > 0 && docWithFieldAccess) {
1188
1222
  const populationService = new PopulationService(db, config.collections);
1189
- const populatedDoc = await populationService.populate({
1223
+ finalDoc = await populationService.populate({
1190
1224
  data: docWithFieldAccess,
1191
1225
  fields: this.collection.fields,
1192
1226
  currentDepth: 0,
1193
1227
  maxDepth: depth
1194
1228
  });
1195
- return c.json(populatedDoc);
1196
1229
  }
1197
- return c.json(docWithFieldAccess);
1230
+ if (finalDoc && this.collection.detail) {
1231
+ finalDoc = await evaluateDetailComputed(this.collection.detail, finalDoc, user, readonlyDb);
1232
+ }
1233
+ return c.json(this.sanitizeDoc(finalDoc));
1198
1234
  }
1199
1235
  async create(c) {
1200
1236
  const config = c.get("config");
@@ -1966,6 +2002,63 @@ var CollectionController = class {
1966
2002
  201
1967
2003
  );
1968
2004
  }
2005
+ async aggregate(c) {
2006
+ const config = c.get("config");
2007
+ const db = config.db;
2008
+ if (!db) return c.json({ message: "Database not configured" }, 500);
2009
+ const access = await this.evaluateAccess(c, "read");
2010
+ if (!access.allowed) {
2011
+ return c.json(
2012
+ {
2013
+ error: true,
2014
+ message: `Access denied: read on ${this.collection.slug}`
2015
+ },
2016
+ 403
2017
+ );
2018
+ }
2019
+ let body;
2020
+ try {
2021
+ body = await c.req.json();
2022
+ } catch {
2023
+ return c.json({ message: "Invalid JSON body" }, 400);
2024
+ }
2025
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
2026
+ return c.json(
2027
+ { message: "Aggregate request body must be a JSON object" },
2028
+ 400
2029
+ );
2030
+ }
2031
+ const { sanitizeWhereClause } = await import("./where-sanitizer-WJ2W2QCE.js");
2032
+ const sanitizedAggregates = {};
2033
+ for (const [key, op] of Object.entries(body)) {
2034
+ if (!op || typeof op !== "object" || Array.isArray(op)) {
2035
+ return c.json(
2036
+ {
2037
+ message: `Aggregate operation "${key}" must be an object`
2038
+ },
2039
+ 400
2040
+ );
2041
+ }
2042
+ const operation = op;
2043
+ let opWhere = operation.where && typeof operation.where === "object" ? operation.where : void 0;
2044
+ if (opWhere) {
2045
+ opWhere = sanitizeWhereClause(opWhere, this.collection.fields);
2046
+ if (Object.keys(opWhere).length === 0) opWhere = void 0;
2047
+ }
2048
+ if (access.constraint) {
2049
+ opWhere = opWhere ? mergeWhereConstraint(opWhere, access.constraint) : access.constraint;
2050
+ }
2051
+ sanitizedAggregates[key] = {
2052
+ ...operation,
2053
+ ...opWhere !== void 0 ? { where: opWhere } : { where: void 0 }
2054
+ };
2055
+ }
2056
+ const result = await db.aggregate({
2057
+ collection: this.collection.slug,
2058
+ aggregates: sanitizedAggregates
2059
+ });
2060
+ return c.json(result);
2061
+ }
1969
2062
  };
1970
2063
 
1971
2064
  // src/controllers/global.controller.ts
@@ -2048,17 +2141,25 @@ var GlobalController = class {
2048
2141
  },
2049
2142
  docWithFieldHooks
2050
2143
  );
2144
+ let finalDoc = docWithFieldAccess;
2051
2145
  if (depth > 0 && docWithFieldAccess) {
2052
2146
  const populationService = new PopulationService(db, config.collections);
2053
- const populatedData = await populationService.populate({
2147
+ finalDoc = await populationService.populate({
2054
2148
  data: docWithFieldAccess,
2055
2149
  fields: this.global.fields,
2056
2150
  currentDepth: 0,
2057
2151
  maxDepth: depth
2058
2152
  });
2059
- return c.json(populatedData);
2060
2153
  }
2061
- return c.json(docWithFieldAccess);
2154
+ if (finalDoc && this.global.detail) {
2155
+ finalDoc = await evaluateDetailComputed(
2156
+ this.global.detail,
2157
+ finalDoc,
2158
+ user,
2159
+ readonlyDb
2160
+ );
2161
+ }
2162
+ return c.json(finalDoc);
2062
2163
  }
2063
2164
  async update(c) {
2064
2165
  const config = c.get("config");
@@ -3401,6 +3502,7 @@ var AdminAuthController = class {
3401
3502
  });
3402
3503
  }
3403
3504
  getProvider(config, id) {
3505
+ if (!id) return void 0;
3404
3506
  if (config.adminAuth?.mode !== "external") return void 0;
3405
3507
  return config.adminAuth.providers.find((provider) => provider.id === id);
3406
3508
  }
@@ -3599,17 +3701,16 @@ var AdminAuthController = class {
3599
3701
  throw new Error("OIDC nonce verification failed.");
3600
3702
  }
3601
3703
  claims = payload;
3602
- } else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
3603
- const userInfo = await fetch(
3604
- provider.userInfoEndpoint || discovery.userinfo_endpoint,
3605
- {
3606
- headers: { Authorization: `Bearer ${tokenBody.access_token}` }
3607
- }
3608
- );
3704
+ } else {
3705
+ const userInfoEndpoint = provider.userInfoEndpoint ?? discovery.userinfo_endpoint;
3706
+ if (!tokenBody.access_token || !userInfoEndpoint) {
3707
+ throw new Error("OIDC provider did not return usable identity claims.");
3708
+ }
3709
+ const userInfo = await fetch(userInfoEndpoint, {
3710
+ headers: { Authorization: `Bearer ${tokenBody.access_token}` }
3711
+ });
3609
3712
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
3610
3713
  claims = await userInfo.json();
3611
- } else {
3612
- throw new Error("OIDC provider did not return usable identity claims.");
3613
3714
  }
3614
3715
  return {
3615
3716
  identity: this.mapClaims(claims, provider.claimMapping),
@@ -4154,10 +4255,7 @@ function accessGate(config, target, action) {
4154
4255
  req: toHookRequestContext(c.req)
4155
4256
  });
4156
4257
  if (!allowed) {
4157
- return c.json(
4158
- { error: true, message: `Access denied: ${action} on ${target.slug}` },
4159
- 403
4160
- );
4258
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
4161
4259
  }
4162
4260
  await next();
4163
4261
  };
@@ -4197,6 +4295,80 @@ function serializeBlockForApi(block) {
4197
4295
  fields: block.fields?.map(serializeFieldForApi)
4198
4296
  };
4199
4297
  }
4298
+ function serializeDetailItemForApi(item) {
4299
+ if (!item) return item;
4300
+ if (typeof item === "string") {
4301
+ return { type: "field", field: item };
4302
+ }
4303
+ if (item.type === "section") {
4304
+ return {
4305
+ type: "section",
4306
+ title: item.title,
4307
+ items: (item.items || []).map(serializeDetailItemForApi),
4308
+ options: item.options
4309
+ };
4310
+ }
4311
+ if (item.type === "tab") {
4312
+ return {
4313
+ type: "tab",
4314
+ label: item.label,
4315
+ items: (item.items || []).map(serializeDetailItemForApi),
4316
+ options: item.options
4317
+ };
4318
+ }
4319
+ if (item.type === "tabs") {
4320
+ return {
4321
+ type: "tabs",
4322
+ tabs: (item.tabs || []).map(serializeDetailItemForApi),
4323
+ options: item.options
4324
+ };
4325
+ }
4326
+ if (item.type === "grid") {
4327
+ return {
4328
+ type: "grid",
4329
+ columns: item.columns,
4330
+ items: (item.items || []).map(serializeDetailItemForApi),
4331
+ options: item.options
4332
+ };
4333
+ }
4334
+ if (item.type === "repeat") {
4335
+ return {
4336
+ type: "repeat",
4337
+ field: item.field,
4338
+ items: (item.items || []).map(serializeDetailItemForApi),
4339
+ options: item.options
4340
+ };
4341
+ }
4342
+ if (item.type === "computed") {
4343
+ return {
4344
+ type: "computed",
4345
+ id: item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
4346
+ label: item.label,
4347
+ expression: item.expression,
4348
+ options: {
4349
+ id: item.id,
4350
+ span: item.options?.span,
4351
+ format: item.options?.format,
4352
+ currency: item.options?.currency,
4353
+ visible: item.options?.visible
4354
+ }
4355
+ };
4356
+ }
4357
+ if (item.type === "field") {
4358
+ return {
4359
+ type: "field",
4360
+ field: item.field,
4361
+ options: item.options
4362
+ };
4363
+ }
4364
+ return item;
4365
+ }
4366
+ function serializeDetailForApi(detail) {
4367
+ if (detail === false) return false;
4368
+ if (detail === true) return true;
4369
+ if (!detail || !Array.isArray(detail)) return void 0;
4370
+ return detail.map(serializeDetailItemForApi);
4371
+ }
4200
4372
  function registerRoutes(app, config) {
4201
4373
  const optionsCache = /* @__PURE__ */ new Map();
4202
4374
  app.get("/api/schemas", optionalAuth(config), async (c) => {
@@ -4215,15 +4387,12 @@ function registerRoutes(app, config) {
4215
4387
  if (typeof access === "boolean") return access;
4216
4388
  if (access && typeof access === "object" && typeof access.policy === "string") {
4217
4389
  const policy = requestConfig.accessPolicies?.[access.policy];
4218
- if (typeof policy === "string" || typeof policy === "boolean")
4219
- return policy;
4390
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
4220
4391
  }
4221
4392
  return resolveBooleanAccess(requestConfig, access, accessArgs);
4222
4393
  };
4223
4394
  const filteredCollections = await Promise.all(
4224
- collections.filter(
4225
- (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
4226
- ).map(async (col) => ({
4395
+ collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
4227
4396
  slug: col.slug,
4228
4397
  labels: col.labels,
4229
4398
  access: {
@@ -4261,6 +4430,7 @@ function registerRoutes(app, config) {
4261
4430
  audit: !!col.audit,
4262
4431
  drafts: !!col.drafts,
4263
4432
  admin: col.admin,
4433
+ detail: serializeDetailForApi(col.detail),
4264
4434
  workflow: col.workflow ? {
4265
4435
  initialState: col.workflow.initialState,
4266
4436
  draftState: col.workflow.draftState,
@@ -4279,9 +4449,7 @@ function registerRoutes(app, config) {
4279
4449
  }))
4280
4450
  );
4281
4451
  const filteredGlobals = await Promise.all(
4282
- globals.filter(
4283
- (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
4284
- ).map(async (glb) => ({
4452
+ globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
4285
4453
  slug: glb.slug,
4286
4454
  label: glb.label,
4287
4455
  access: {
@@ -4311,7 +4479,8 @@ function registerRoutes(app, config) {
4311
4479
  }
4312
4480
  }))
4313
4481
  ),
4314
- admin: glb.admin
4482
+ admin: glb.admin,
4483
+ detail: serializeDetailForApi(glb.detail)
4315
4484
  }))
4316
4485
  );
4317
4486
  return c.json({
@@ -4325,148 +4494,137 @@ function registerRoutes(app, config) {
4325
4494
  adminHealth: {
4326
4495
  emailConfigured: !!requestConfig.email,
4327
4496
  secureAuthSecretConfigured: !!process.env.DYRECTED_JWT_SECRET,
4328
- authCollectionConfigured: requestConfig.collections.some(
4329
- (collection) => !!collection.auth
4330
- ),
4331
- uploadCollectionConfigured: requestConfig.collections.some(
4332
- (collection) => !!collection.upload
4333
- )
4497
+ authCollectionConfigured: requestConfig.collections.some((collection) => !!collection.auth),
4498
+ uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload)
4334
4499
  }
4335
4500
  });
4336
4501
  });
4337
- app.get(
4338
- "/api/dyrected/options/:collection/:field",
4339
- optionalAuth(config),
4340
- async (c) => {
4341
- const { collection: colSlug, field: fieldName } = c.req.param();
4342
- const siteId = c.req.header("X-Site-Id");
4343
- const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
4344
- let collections = [...requestConfig.collections];
4345
- const user = c.get("user");
4346
- let collection = collections.find((col) => col.slug === colSlug);
4347
- let field;
4348
- if (collection) {
4349
- const accessExpr = collection.access?.read;
4350
- if (accessExpr !== void 0 && accessExpr !== null) {
4351
- const allowed = await resolveBooleanAccess(config, accessExpr, {
4352
- user,
4353
- req: toHookRequestContext(c.req)
4354
- });
4355
- if (!allowed) {
4356
- return c.json(
4357
- { error: true, message: `Access denied: read on ${colSlug}` },
4358
- 403
4359
- );
4360
- }
4361
- }
4362
- field = collection.fields.find((f) => f.name === fieldName);
4363
- } else {
4364
- const globals = [...requestConfig.globals];
4365
- const glb = globals.find((g) => g.slug === colSlug);
4366
- if (!glb) {
4367
- return c.json(
4368
- {
4369
- error: true,
4370
- message: `${colSlug} not found as collection or global`
4371
- },
4372
- 404
4373
- );
4374
- }
4375
- const accessExpr = glb.access?.read;
4376
- if (accessExpr !== void 0 && accessExpr !== null) {
4377
- const allowed = await resolveBooleanAccess(config, accessExpr, {
4378
- user,
4379
- req: toHookRequestContext(c.req)
4380
- });
4381
- if (!allowed) {
4382
- return c.json(
4383
- {
4384
- error: true,
4385
- message: `Access denied: read on global ${colSlug}`
4386
- },
4387
- 403
4388
- );
4389
- }
4502
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
4503
+ const { collection: colSlug, field: fieldName } = c.req.param();
4504
+ const siteId = c.req.header("X-Site-Id");
4505
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
4506
+ let collections = [...requestConfig.collections];
4507
+ const user = c.get("user");
4508
+ let collection = collections.find((col) => col.slug === colSlug);
4509
+ let field;
4510
+ if (collection) {
4511
+ const accessExpr = collection.access?.read;
4512
+ if (accessExpr !== void 0 && accessExpr !== null) {
4513
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4514
+ user,
4515
+ req: toHookRequestContext(c.req)
4516
+ });
4517
+ if (!allowed) {
4518
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
4390
4519
  }
4391
- field = glb.fields.find((f) => f.name === fieldName);
4392
4520
  }
4393
- if (!field) {
4521
+ field = collection.fields.find((f) => f.name === fieldName);
4522
+ } else {
4523
+ const globals = [...requestConfig.globals];
4524
+ const glb = globals.find((g) => g.slug === colSlug);
4525
+ if (!glb) {
4394
4526
  return c.json(
4395
4527
  {
4396
4528
  error: true,
4397
- message: `Field ${fieldName} not found in ${colSlug}`
4529
+ message: `${colSlug} not found as collection or global`
4398
4530
  },
4399
4531
  404
4400
4532
  );
4401
4533
  }
4402
- let resolver;
4403
- let cacheTTL;
4404
- if (typeof field.options === "function") {
4405
- resolver = field.options;
4406
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
4407
- resolver = field.options.resolve;
4408
- cacheTTL = field.options.cacheTTL;
4409
- }
4410
- if (!resolver) {
4411
- return c.json(
4412
- {
4413
- error: true,
4414
- message: `Field ${fieldName} in ${colSlug} is not dynamic`
4415
- },
4416
- 400
4417
- );
4418
- }
4419
- try {
4420
- const db = c.get("db") || config.db;
4421
- const queryParams = c.req.query();
4422
- const reqContext = {
4423
- query: queryParams,
4424
- headers: c.req.header(),
4425
- raw: c.req.raw
4426
- };
4427
- const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
4428
- let cacheKey = "";
4429
- if (shouldCache) {
4430
- cacheKey = JSON.stringify([
4431
- siteId ?? "",
4432
- colSlug,
4433
- fieldName,
4434
- user?.id ?? null,
4435
- queryParams
4436
- ]);
4437
- const hit = optionsCache.get(cacheKey);
4438
- if (hit && hit.expires > Date.now()) {
4439
- return c.json(hit.value);
4440
- }
4441
- }
4442
- const result = await resolver({
4443
- db,
4534
+ const accessExpr = glb.access?.read;
4535
+ if (accessExpr !== void 0 && accessExpr !== null) {
4536
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4444
4537
  user,
4445
- req: reqContext
4538
+ req: toHookRequestContext(c.req)
4446
4539
  });
4447
- if (shouldCache) {
4448
- optionsCache.set(cacheKey, {
4449
- expires: Date.now() + cacheTTL * 1e3,
4450
- value: result
4451
- });
4540
+ if (!allowed) {
4541
+ return c.json(
4542
+ {
4543
+ error: true,
4544
+ message: `Access denied: read on global ${colSlug}`
4545
+ },
4546
+ 403
4547
+ );
4452
4548
  }
4453
- return c.json(result);
4454
- } catch (err) {
4455
- getRequestLogger(c, "router").error({
4456
- err,
4457
- msg: "Failed to resolve dynamic field options",
4458
- fieldName
4549
+ }
4550
+ field = glb.fields.find((f) => f.name === fieldName);
4551
+ }
4552
+ if (!field) {
4553
+ return c.json(
4554
+ {
4555
+ error: true,
4556
+ message: `Field ${fieldName} not found in ${colSlug}`
4557
+ },
4558
+ 404
4559
+ );
4560
+ }
4561
+ let resolver;
4562
+ let cacheTTL;
4563
+ if (typeof field.options === "function") {
4564
+ resolver = field.options;
4565
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
4566
+ resolver = field.options.resolve;
4567
+ cacheTTL = field.options.cacheTTL;
4568
+ }
4569
+ if (!resolver) {
4570
+ return c.json(
4571
+ {
4572
+ error: true,
4573
+ message: `Field ${fieldName} in ${colSlug} is not dynamic`
4574
+ },
4575
+ 400
4576
+ );
4577
+ }
4578
+ try {
4579
+ const db = c.get("db") || config.db;
4580
+ const queryParams = c.req.query();
4581
+ const reqContext = {
4582
+ query: queryParams,
4583
+ headers: c.req.header(),
4584
+ raw: c.req.raw
4585
+ };
4586
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
4587
+ let cacheKey = "";
4588
+ if (shouldCache) {
4589
+ cacheKey = JSON.stringify([
4590
+ siteId ?? "",
4591
+ colSlug,
4592
+ fieldName,
4593
+ user?.id ?? null,
4594
+ queryParams
4595
+ ]);
4596
+ const hit = optionsCache.get(cacheKey);
4597
+ if (hit && hit.expires > Date.now()) {
4598
+ return c.json(hit.value);
4599
+ }
4600
+ }
4601
+ const result = await resolver({
4602
+ db,
4603
+ user,
4604
+ req: reqContext
4605
+ });
4606
+ if (shouldCache) {
4607
+ optionsCache.set(cacheKey, {
4608
+ expires: Date.now() + cacheTTL * 1e3,
4609
+ value: result
4459
4610
  });
4460
- return c.json(
4461
- {
4462
- error: true,
4463
- message: err.message || "Failed to resolve dynamic options"
4464
- },
4465
- 500
4466
- );
4467
4611
  }
4612
+ return c.json(result);
4613
+ } catch (err) {
4614
+ getRequestLogger(c, "router").error({
4615
+ err,
4616
+ msg: "Failed to resolve dynamic field options",
4617
+ fieldName
4618
+ });
4619
+ return c.json(
4620
+ {
4621
+ error: true,
4622
+ message: err.message || "Failed to resolve dynamic options"
4623
+ },
4624
+ 500
4625
+ );
4468
4626
  }
4469
- );
4627
+ });
4470
4628
  app.get("/api/openapi.json", (c) => {
4471
4629
  return c.json(generateOpenApi(config));
4472
4630
  });
@@ -4479,13 +4637,8 @@ function registerRoutes(app, config) {
4479
4637
  const key = c.req.param("key");
4480
4638
  const scope = c.req.query("scope");
4481
4639
  if (!db) return c.json({ message: "Database not configured" }, 500);
4482
- if (!user?.collection || !user.sub)
4483
- return c.json({ error: true, message: "Authentication required." }, 401);
4484
- if (!key)
4485
- return c.json(
4486
- { error: true, message: "Preference key is required." },
4487
- 400
4488
- );
4640
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
4641
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4489
4642
  const getGlobalPreference = async () => {
4490
4643
  const globalDoc = await db.findOne({
4491
4644
  collection: "__global_preferences",
@@ -4512,13 +4665,8 @@ function registerRoutes(app, config) {
4512
4665
  const key = c.req.param("key");
4513
4666
  const scope = c.req.query("scope");
4514
4667
  if (!db) return c.json({ message: "Database not configured" }, 500);
4515
- if (!user?.collection || !user.sub)
4516
- return c.json({ error: true, message: "Authentication required." }, 401);
4517
- if (!key)
4518
- return c.json(
4519
- { error: true, message: "Preference key is required." },
4520
- 400
4521
- );
4668
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
4669
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4522
4670
  const body = await c.req.json().catch(() => ({}));
4523
4671
  if (scope === "global") {
4524
4672
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
@@ -4566,13 +4714,8 @@ function registerRoutes(app, config) {
4566
4714
  const key = c.req.param("key");
4567
4715
  const scope = c.req.query("scope");
4568
4716
  if (!db) return c.json({ message: "Database not configured" }, 500);
4569
- if (!user?.collection || !user.sub)
4570
- return c.json({ error: true, message: "Authentication required." }, 401);
4571
- if (!key)
4572
- return c.json(
4573
- { error: true, message: "Preference key is required." },
4574
- 400
4575
- );
4717
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
4718
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4576
4719
  if (scope === "global") {
4577
4720
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
4578
4721
  if (!isAdminUser) {
@@ -4611,41 +4754,17 @@ function registerRoutes(app, config) {
4611
4754
  for (const col of uploadCollections) {
4612
4755
  const mediaController = new MediaController(col.slug);
4613
4756
  const prefix = `/api/collections/${col.slug}`;
4614
- app.get(
4615
- `${prefix}/media`,
4616
- accessGate(config, col, "read"),
4617
- (c) => mediaController.find(c)
4618
- );
4619
- app.get(
4620
- `${prefix}/media/:filename{.+$}`,
4621
- (c) => mediaController.serve(c)
4622
- );
4623
- app.post(
4624
- `${prefix}/media`,
4625
- accessGate(config, col, "create"),
4626
- (c) => mediaController.upload(c)
4627
- );
4628
- app.delete(
4629
- `${prefix}/media/:id`,
4630
- accessGate(config, col, "delete"),
4631
- (c) => mediaController.delete(c)
4632
- );
4757
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
4758
+ app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
4759
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
4760
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
4633
4761
  }
4634
4762
  }
4635
4763
  const adminAuthController = new AdminAuthController(config);
4636
4764
  app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
4637
- app.get(
4638
- "/api/admin/auth/:provider/start",
4639
- (c) => adminAuthController.start(c)
4640
- );
4641
- app.get(
4642
- "/api/admin/auth/:provider/callback",
4643
- (c) => adminAuthController.callback(c)
4644
- );
4645
- app.post(
4646
- "/api/admin/auth/:provider/exchange",
4647
- (c) => adminAuthController.exchange(c)
4648
- );
4765
+ app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
4766
+ app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
4767
+ app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
4649
4768
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
4650
4769
  for (const collection of config.collections) {
4651
4770
  if (!collection.auth) continue;
@@ -4656,21 +4775,10 @@ function registerRoutes(app, config) {
4656
4775
  app.get(`${path}/init`, (c) => authController.init(c));
4657
4776
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
4658
4777
  app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
4659
- app.post(
4660
- `${path}/refresh-token`,
4661
- requireAuth(config),
4662
- (c) => authController.refreshToken(c)
4663
- );
4664
- app.post(
4665
- `${path}/forgot-password`,
4666
- (c) => authController.forgotPassword(c)
4667
- );
4778
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
4779
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
4668
4780
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
4669
- app.post(
4670
- `${path}/invite`,
4671
- requireAuth(config),
4672
- (c) => authController.invite(c)
4673
- );
4781
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
4674
4782
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
4675
4783
  }
4676
4784
  const auditController = new AuditController();
@@ -4681,33 +4789,19 @@ function registerRoutes(app, config) {
4681
4789
  app.post(path, (c) => controller.create(c));
4682
4790
  app.post(`${path}/media`, (c) => controller.create(c));
4683
4791
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
4792
+ app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
4684
4793
  if (collection.audit) {
4685
- app.get(
4686
- `${path}/__audit`,
4687
- (c) => auditController.findForCollection(c, collection)
4688
- );
4794
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
4689
4795
  }
4690
4796
  app.get(`${path}/:id`, (c) => controller.findOne(c));
4691
4797
  app.patch(`${path}/:id`, (c) => controller.update(c));
4692
4798
  app.delete(`${path}/:id`, (c) => controller.delete(c));
4693
4799
  if (collection.auth) {
4694
- app.post(
4695
- `${path}/:id/change-password`,
4696
- requireAuth(config),
4697
- (c) => controller.changePassword(c)
4698
- );
4800
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
4699
4801
  }
4700
4802
  if (collection.workflow) {
4701
- app.post(
4702
- `${path}/:id/transitions/:transition`,
4703
- requireAuth(config),
4704
- (c) => controller.transition(c)
4705
- );
4706
- app.get(
4707
- `${path}/:id/workflow-history`,
4708
- requireAuth(config),
4709
- (c) => controller.workflowHistory(c)
4710
- );
4803
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
4804
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
4711
4805
  }
4712
4806
  }
4713
4807
  for (const global of config.globals) {
@@ -4722,11 +4816,7 @@ function registerRoutes(app, config) {
4722
4816
  });
4723
4817
  }
4724
4818
  const previewController = new PreviewController();
4725
- app.post(
4726
- "/api/preview-token",
4727
- requireAuth(config),
4728
- (c) => previewController.createToken(c)
4729
- );
4819
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
4730
4820
  app.get("/api/preview-data", (c) => previewController.getData(c));
4731
4821
  app.get("/api/audit", (c) => auditController.findAll(c));
4732
4822
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -4739,81 +4829,49 @@ function registerRoutes(app, config) {
4739
4829
  if (!config2.onSchemaFetch || !siteId) {
4740
4830
  return c.json({ message: `Collection "${slug}" not found` }, 404);
4741
4831
  }
4742
- const requestConfig = mergeDynamicConfig(
4743
- config2,
4744
- await config2.onSchemaFetch(siteId)
4745
- );
4746
- const collection = requestConfig.collections.find(
4747
- (col) => col.slug === slug
4748
- );
4832
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4833
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4749
4834
  if (!collection?.audit) {
4750
- return c.json(
4751
- { message: `Collection "${slug}" not found or has no audit log` },
4752
- 404
4753
- );
4835
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
4754
4836
  }
4755
4837
  return auditController.findForCollection(c, collection);
4756
4838
  });
4757
- app.post(
4758
- "/api/collections/:slug/:id/transitions/:transition",
4759
- requireAuth(config),
4760
- async (c) => {
4761
- const slug = c.req.param("slug");
4762
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4763
- const config2 = c.get("config");
4764
- if (config2.collections.some((col) => col.slug === slug)) {
4765
- return c.json({ message: "Not Found" }, 404);
4766
- }
4767
- if (!config2.onSchemaFetch || !siteId) {
4768
- return c.json({ message: `Collection "${slug}" not found` }, 404);
4769
- }
4770
- const requestConfig = mergeDynamicConfig(
4771
- config2,
4772
- await config2.onSchemaFetch(siteId)
4773
- );
4774
- const collection = requestConfig.collections.find(
4775
- (col) => col.slug === slug
4776
- );
4777
- if (!collection?.workflow) {
4778
- return c.json(
4779
- { message: `Collection "${slug}" not found or has no workflow` },
4780
- 404
4781
- );
4782
- }
4783
- const controller = new CollectionController(collection);
4784
- return controller.transition(c);
4839
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
4840
+ const slug = c.req.param("slug");
4841
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4842
+ const config2 = c.get("config");
4843
+ if (config2.collections.some((col) => col.slug === slug)) {
4844
+ return c.json({ message: "Not Found" }, 404);
4785
4845
  }
4786
- );
4787
- app.get(
4788
- "/api/collections/:slug/:id/workflow-history",
4789
- requireAuth(config),
4790
- async (c) => {
4791
- const slug = c.req.param("slug");
4792
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4793
- const config2 = c.get("config");
4794
- if (config2.collections.some((col) => col.slug === slug)) {
4795
- return c.json({ message: "Not Found" }, 404);
4796
- }
4797
- if (!config2.onSchemaFetch || !siteId) {
4798
- return c.json({ message: `Collection "${slug}" not found` }, 404);
4799
- }
4800
- const requestConfig = mergeDynamicConfig(
4801
- config2,
4802
- await config2.onSchemaFetch(siteId)
4803
- );
4804
- const collection = requestConfig.collections.find(
4805
- (col) => col.slug === slug
4806
- );
4807
- if (!collection?.workflow) {
4808
- return c.json(
4809
- { message: `Collection "${slug}" not found or has no workflow` },
4810
- 404
4811
- );
4812
- }
4813
- const controller = new CollectionController(collection);
4814
- return controller.workflowHistory(c);
4846
+ if (!config2.onSchemaFetch || !siteId) {
4847
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4815
4848
  }
4816
- );
4849
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4850
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4851
+ if (!collection?.workflow) {
4852
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
4853
+ }
4854
+ const controller = new CollectionController(collection);
4855
+ return controller.transition(c);
4856
+ });
4857
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
4858
+ const slug = c.req.param("slug");
4859
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4860
+ const config2 = c.get("config");
4861
+ if (config2.collections.some((col) => col.slug === slug)) {
4862
+ return c.json({ message: "Not Found" }, 404);
4863
+ }
4864
+ if (!config2.onSchemaFetch || !siteId) {
4865
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4866
+ }
4867
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4868
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4869
+ if (!collection?.workflow) {
4870
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
4871
+ }
4872
+ const controller = new CollectionController(collection);
4873
+ return controller.workflowHistory(c);
4874
+ });
4817
4875
  app.all("/api/collections/:slug/:id?", async (c) => {
4818
4876
  const slug = c.req.param("slug");
4819
4877
  const id = c.req.param("id");
@@ -4823,13 +4881,8 @@ function registerRoutes(app, config) {
4823
4881
  return c.json({ message: "Method Not Allowed" }, 405);
4824
4882
  }
4825
4883
  if (config2.onSchemaFetch && siteId) {
4826
- const requestConfig = mergeDynamicConfig(
4827
- config2,
4828
- await config2.onSchemaFetch(siteId)
4829
- );
4830
- let collection = requestConfig.collections.find(
4831
- (col) => col.slug === slug
4832
- );
4884
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4885
+ let collection = requestConfig.collections.find((col) => col.slug === slug);
4833
4886
  if (!collection && slug === "media") {
4834
4887
  collection = {
4835
4888
  slug: "media",
@@ -4842,25 +4895,19 @@ function registerRoutes(app, config) {
4842
4895
  if (collection.auth && id) {
4843
4896
  const authController = new AuthController(collection);
4844
4897
  const method2 = c.req.method;
4845
- if (method2 === "POST" && id === "login")
4846
- return authController.login(c);
4847
- if (method2 === "POST" && id === "logout")
4848
- return authController.logout(c);
4898
+ if (method2 === "POST" && id === "login") return authController.login(c);
4899
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
4849
4900
  if (method2 === "GET" && id === "me") return authController.me(c);
4850
- if (method2 === "POST" && id === "refresh-token")
4851
- return authController.refreshToken(c);
4852
- if (method2 === "POST" && id === "forgot-password")
4853
- return authController.forgotPassword(c);
4854
- if (method2 === "POST" && id === "reset-password")
4855
- return authController.resetPassword(c);
4901
+ if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
4902
+ if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
4903
+ if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
4856
4904
  }
4857
4905
  const controller = new CollectionController(collection);
4858
4906
  const method = c.req.method;
4859
4907
  if (id) {
4860
4908
  if (method === "GET") return controller.findOne(c);
4861
4909
  if (method === "PATCH") return controller.update(c);
4862
- if (method === "DELETE" && id === "delete-many")
4863
- return controller.deleteMany(c);
4910
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
4864
4911
  if (method === "DELETE") return controller.delete(c);
4865
4912
  if (method === "POST" && id === "media") return controller.create(c);
4866
4913
  } else {
@@ -4880,10 +4927,7 @@ function registerRoutes(app, config) {
4880
4927
  return c.json({ message: "Method Not Allowed" }, 405);
4881
4928
  }
4882
4929
  if (config2.onSchemaFetch && siteId) {
4883
- const requestConfig = mergeDynamicConfig(
4884
- config2,
4885
- await config2.onSchemaFetch(siteId)
4886
- );
4930
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4887
4931
  const global = requestConfig.globals.find((glb) => glb.slug === slug);
4888
4932
  if (global) {
4889
4933
  const controller = new GlobalController(global);
@@ -4922,7 +4966,7 @@ function resolveClientIp(req, trustProxy = false) {
4922
4966
  const value = normalizeIp(req.headers[header]);
4923
4967
  if (value) return value;
4924
4968
  }
4925
- if (trustProxy) {
4969
+ if (trustProxy === true || typeof trustProxy === "number") {
4926
4970
  const forwardedFor = pickForwardedFor(req.headers, trustProxy);
4927
4971
  if (forwardedFor) return forwardedFor;
4928
4972
  }
@@ -4934,6 +4978,7 @@ function pickForwardedFor(headers, trustProxy) {
4934
4978
  const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
4935
4979
  if (chain2.length > 0) {
4936
4980
  if (trustProxy === true) return chain2[0];
4981
+ if (trustProxy <= 0) return null;
4937
4982
  const trustedHops2 = Math.max(1, Math.floor(trustProxy));
4938
4983
  const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
4939
4984
  return chain2[candidateIndex2] ?? chain2[0];
@@ -4946,6 +4991,7 @@ function pickForwardedFor(headers, trustProxy) {
4946
4991
  const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
4947
4992
  if (chain.length === 0) return null;
4948
4993
  if (trustProxy === true) return chain[0];
4994
+ if (trustProxy <= 0) return null;
4949
4995
  const trustedHops = Math.max(1, Math.floor(trustProxy));
4950
4996
  const candidateIndex = Math.max(0, chain.length - trustedHops);
4951
4997
  return chain[candidateIndex] ?? chain[0];
@@ -5149,7 +5195,7 @@ async function createDyrectedApp(rawConfig) {
5149
5195
  app.use("*", cors(config.cors ? { origin: config.cors.origins } : {}));
5150
5196
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
5151
5197
  if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
5152
- app.get(observability.config.metrics.path, async (c) => {
5198
+ app.get(observability.config.metrics.path, async (_c) => {
5153
5199
  const metrics = await renderPrometheusMetrics(
5154
5200
  observability.prometheusExporter
5155
5201
  );
@@ -5189,6 +5235,22 @@ export {
5189
5235
  GlobalController,
5190
5236
  MediaController,
5191
5237
  PreviewController,
5238
+ attachRequestMetrics,
5239
+ bindObservabilityRuntime,
5240
+ buildRequestLogger,
5241
+ captureRequestBody,
5192
5242
  createDyrectedApp,
5193
- registerRoutes
5243
+ createObservabilityRuntime,
5244
+ createRequestSpan,
5245
+ endRequestSpan,
5246
+ getConfigLogger,
5247
+ getObservabilityRuntime,
5248
+ getRequestLogger,
5249
+ redactBody,
5250
+ redactHeaders,
5251
+ registerRoutes,
5252
+ renderPrometheusMetrics,
5253
+ resolveObservabilityConfig,
5254
+ serializeDetailForApi,
5255
+ shouldSampleRequest
5194
5256
  };