@dyrected/core 2.7.1 → 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 (43) 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-3TRRTN6U.js +3441 -0
  26. package/dist/chunk-5ZIA5COH.js +3414 -0
  27. package/dist/chunk-6TWQ6GTD.js +3480 -0
  28. package/dist/chunk-AQO4HI2Y.js +3204 -0
  29. package/dist/chunk-DFBLNYUH.js +3402 -0
  30. package/dist/chunk-KEKOEAX6.js +3139 -0
  31. package/dist/chunk-PRQ2RF5C.js +3485 -0
  32. package/dist/chunk-PZMDK3MH.js +3203 -0
  33. package/dist/chunk-SM32ND5M.js +3472 -0
  34. package/dist/chunk-XX7N64NL.js +3550 -0
  35. package/dist/index.cjs +446 -9
  36. package/dist/index.d.cts +166 -3
  37. package/dist/index.d.ts +166 -3
  38. package/dist/index.js +53 -1
  39. package/dist/server.cjs +615 -363
  40. package/dist/server.d.cts +26 -7
  41. package/dist/server.d.ts +26 -7
  42. package/dist/server.js +412 -355
  43. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  canViewWorkflowDraft,
8
8
  collectConfigDiagnostics,
9
9
  createWorkflowDocument,
10
+ evaluateDetailComputed,
10
11
  executeFieldAfterRead,
11
12
  executeFieldBeforeChange,
12
13
  generateOpenApi,
@@ -28,7 +29,7 @@ import {
28
29
  touchAuthSession,
29
30
  transitionWorkflow,
30
31
  verifyCollectionToken
31
- } from "./chunk-YSSHLGQ6.js";
32
+ } from "./chunk-XX7N64NL.js";
32
33
  import {
33
34
  attachRequestMetrics,
34
35
  bindObservabilityRuntime,
@@ -865,6 +866,22 @@ var CollectionController = class {
865
866
  toHookRequestContext(c) {
866
867
  return toHookRequestContext(c.req);
867
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
+ }
868
885
  async evaluateAccess(c, action, options = {}) {
869
886
  const config = c.get("config");
870
887
  return resolveCollectionAccess(
@@ -1097,6 +1114,9 @@ var CollectionController = class {
1097
1114
  depth
1098
1115
  );
1099
1116
  }
1117
+ if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
1118
+ result.docs = result.docs.map((d) => this.sanitizeDoc(d));
1119
+ }
1100
1120
  return c.json(result);
1101
1121
  }
1102
1122
  async findOne(c) {
@@ -1113,32 +1133,51 @@ var CollectionController = class {
1113
1133
  id: id2
1114
1134
  });
1115
1135
  const externalSubject = localDoc?.externalSubject || id2;
1116
- let member = null;
1117
1136
  if (provider.members.get) {
1118
- 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
+ }
1119
1148
  } else if (provider.members.list) {
1120
1149
  const listResult = await provider.members.list({ req: hookReq });
1121
- 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
+ }
1122
1158
  }
1123
- if (!member) return c.json({ message: "Not Found" }, 404);
1124
- return c.json({
1125
- ...member,
1126
- id: localDoc ? localDoc.id : member.id,
1127
- externalSubject: member.id
1128
- });
1129
1159
  }
1130
- const readonlyDb = createReadonlyDb(db);
1131
1160
  const id = c.req.param("id");
1132
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1133
- const user = c.get("user");
1134
1161
  if (!id) return c.json({ message: "Missing ID" }, 400);
1135
- let doc = await db.findOne({ collection: this.collection.slug, id });
1136
- 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;
1137
1172
  if (this.collection.workflow) {
1138
- doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
1139
- 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);
1140
1179
  }
1141
- const access = await this.evaluateAccess(c, "read", { id, doc });
1180
+ const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
1142
1181
  if (!access.allowed) {
1143
1182
  return c.json(
1144
1183
  {
@@ -1148,7 +1187,11 @@ var CollectionController = class {
1148
1187
  403
1149
1188
  );
1150
1189
  }
1151
- 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);
1152
1195
  const docWithCollectionHooks = await runCollectionHooks(
1153
1196
  this.collection.hooks?.afterRead,
1154
1197
  {
@@ -1174,17 +1217,20 @@ var CollectionController = class {
1174
1217
  },
1175
1218
  docWithFieldHooks
1176
1219
  );
1220
+ let finalDoc = docWithFieldAccess;
1177
1221
  if (depth > 0 && docWithFieldAccess) {
1178
1222
  const populationService = new PopulationService(db, config.collections);
1179
- const populatedDoc = await populationService.populate({
1223
+ finalDoc = await populationService.populate({
1180
1224
  data: docWithFieldAccess,
1181
1225
  fields: this.collection.fields,
1182
1226
  currentDepth: 0,
1183
1227
  maxDepth: depth
1184
1228
  });
1185
- return c.json(populatedDoc);
1186
1229
  }
1187
- 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));
1188
1234
  }
1189
1235
  async create(c) {
1190
1236
  const config = c.get("config");
@@ -1956,6 +2002,63 @@ var CollectionController = class {
1956
2002
  201
1957
2003
  );
1958
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
+ }
1959
2062
  };
1960
2063
 
1961
2064
  // src/controllers/global.controller.ts
@@ -2038,17 +2141,25 @@ var GlobalController = class {
2038
2141
  },
2039
2142
  docWithFieldHooks
2040
2143
  );
2144
+ let finalDoc = docWithFieldAccess;
2041
2145
  if (depth > 0 && docWithFieldAccess) {
2042
2146
  const populationService = new PopulationService(db, config.collections);
2043
- const populatedData = await populationService.populate({
2147
+ finalDoc = await populationService.populate({
2044
2148
  data: docWithFieldAccess,
2045
2149
  fields: this.global.fields,
2046
2150
  currentDepth: 0,
2047
2151
  maxDepth: depth
2048
2152
  });
2049
- return c.json(populatedData);
2050
2153
  }
2051
- 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);
2052
2163
  }
2053
2164
  async update(c) {
2054
2165
  const config = c.get("config");
@@ -3391,6 +3502,7 @@ var AdminAuthController = class {
3391
3502
  });
3392
3503
  }
3393
3504
  getProvider(config, id) {
3505
+ if (!id) return void 0;
3394
3506
  if (config.adminAuth?.mode !== "external") return void 0;
3395
3507
  return config.adminAuth.providers.find((provider) => provider.id === id);
3396
3508
  }
@@ -3589,17 +3701,16 @@ var AdminAuthController = class {
3589
3701
  throw new Error("OIDC nonce verification failed.");
3590
3702
  }
3591
3703
  claims = payload;
3592
- } else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
3593
- const userInfo = await fetch(
3594
- provider.userInfoEndpoint || discovery.userinfo_endpoint,
3595
- {
3596
- headers: { Authorization: `Bearer ${tokenBody.access_token}` }
3597
- }
3598
- );
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
+ });
3599
3712
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
3600
3713
  claims = await userInfo.json();
3601
- } else {
3602
- throw new Error("OIDC provider did not return usable identity claims.");
3603
3714
  }
3604
3715
  return {
3605
3716
  identity: this.mapClaims(claims, provider.claimMapping),
@@ -4144,10 +4255,7 @@ function accessGate(config, target, action) {
4144
4255
  req: toHookRequestContext(c.req)
4145
4256
  });
4146
4257
  if (!allowed) {
4147
- return c.json(
4148
- { error: true, message: `Access denied: ${action} on ${target.slug}` },
4149
- 403
4150
- );
4258
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
4151
4259
  }
4152
4260
  await next();
4153
4261
  };
@@ -4187,6 +4295,80 @@ function serializeBlockForApi(block) {
4187
4295
  fields: block.fields?.map(serializeFieldForApi)
4188
4296
  };
4189
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
+ }
4190
4372
  function registerRoutes(app, config) {
4191
4373
  const optionsCache = /* @__PURE__ */ new Map();
4192
4374
  app.get("/api/schemas", optionalAuth(config), async (c) => {
@@ -4205,15 +4387,12 @@ function registerRoutes(app, config) {
4205
4387
  if (typeof access === "boolean") return access;
4206
4388
  if (access && typeof access === "object" && typeof access.policy === "string") {
4207
4389
  const policy = requestConfig.accessPolicies?.[access.policy];
4208
- if (typeof policy === "string" || typeof policy === "boolean")
4209
- return policy;
4390
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
4210
4391
  }
4211
4392
  return resolveBooleanAccess(requestConfig, access, accessArgs);
4212
4393
  };
4213
4394
  const filteredCollections = await Promise.all(
4214
- collections.filter(
4215
- (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
4216
- ).map(async (col) => ({
4395
+ collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
4217
4396
  slug: col.slug,
4218
4397
  labels: col.labels,
4219
4398
  access: {
@@ -4251,6 +4430,7 @@ function registerRoutes(app, config) {
4251
4430
  audit: !!col.audit,
4252
4431
  drafts: !!col.drafts,
4253
4432
  admin: col.admin,
4433
+ detail: serializeDetailForApi(col.detail),
4254
4434
  workflow: col.workflow ? {
4255
4435
  initialState: col.workflow.initialState,
4256
4436
  draftState: col.workflow.draftState,
@@ -4269,9 +4449,7 @@ function registerRoutes(app, config) {
4269
4449
  }))
4270
4450
  );
4271
4451
  const filteredGlobals = await Promise.all(
4272
- globals.filter(
4273
- (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
4274
- ).map(async (glb) => ({
4452
+ globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
4275
4453
  slug: glb.slug,
4276
4454
  label: glb.label,
4277
4455
  access: {
@@ -4301,7 +4479,8 @@ function registerRoutes(app, config) {
4301
4479
  }
4302
4480
  }))
4303
4481
  ),
4304
- admin: glb.admin
4482
+ admin: glb.admin,
4483
+ detail: serializeDetailForApi(glb.detail)
4305
4484
  }))
4306
4485
  );
4307
4486
  return c.json({
@@ -4315,148 +4494,137 @@ function registerRoutes(app, config) {
4315
4494
  adminHealth: {
4316
4495
  emailConfigured: !!requestConfig.email,
4317
4496
  secureAuthSecretConfigured: !!process.env.DYRECTED_JWT_SECRET,
4318
- authCollectionConfigured: requestConfig.collections.some(
4319
- (collection) => !!collection.auth
4320
- ),
4321
- uploadCollectionConfigured: requestConfig.collections.some(
4322
- (collection) => !!collection.upload
4323
- )
4497
+ authCollectionConfigured: requestConfig.collections.some((collection) => !!collection.auth),
4498
+ uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload)
4324
4499
  }
4325
4500
  });
4326
4501
  });
4327
- app.get(
4328
- "/api/dyrected/options/:collection/:field",
4329
- optionalAuth(config),
4330
- async (c) => {
4331
- const { collection: colSlug, field: fieldName } = c.req.param();
4332
- const siteId = c.req.header("X-Site-Id");
4333
- const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
4334
- let collections = [...requestConfig.collections];
4335
- const user = c.get("user");
4336
- let collection = collections.find((col) => col.slug === colSlug);
4337
- let field;
4338
- if (collection) {
4339
- const accessExpr = collection.access?.read;
4340
- if (accessExpr !== void 0 && accessExpr !== null) {
4341
- const allowed = await resolveBooleanAccess(config, accessExpr, {
4342
- user,
4343
- req: toHookRequestContext(c.req)
4344
- });
4345
- if (!allowed) {
4346
- return c.json(
4347
- { error: true, message: `Access denied: read on ${colSlug}` },
4348
- 403
4349
- );
4350
- }
4351
- }
4352
- field = collection.fields.find((f) => f.name === fieldName);
4353
- } else {
4354
- const globals = [...requestConfig.globals];
4355
- const glb = globals.find((g) => g.slug === colSlug);
4356
- if (!glb) {
4357
- return c.json(
4358
- {
4359
- error: true,
4360
- message: `${colSlug} not found as collection or global`
4361
- },
4362
- 404
4363
- );
4364
- }
4365
- const accessExpr = glb.access?.read;
4366
- if (accessExpr !== void 0 && accessExpr !== null) {
4367
- const allowed = await resolveBooleanAccess(config, accessExpr, {
4368
- user,
4369
- req: toHookRequestContext(c.req)
4370
- });
4371
- if (!allowed) {
4372
- return c.json(
4373
- {
4374
- error: true,
4375
- message: `Access denied: read on global ${colSlug}`
4376
- },
4377
- 403
4378
- );
4379
- }
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);
4380
4519
  }
4381
- field = glb.fields.find((f) => f.name === fieldName);
4382
4520
  }
4383
- 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) {
4384
4526
  return c.json(
4385
4527
  {
4386
4528
  error: true,
4387
- message: `Field ${fieldName} not found in ${colSlug}`
4529
+ message: `${colSlug} not found as collection or global`
4388
4530
  },
4389
4531
  404
4390
4532
  );
4391
4533
  }
4392
- let resolver;
4393
- let cacheTTL;
4394
- if (typeof field.options === "function") {
4395
- resolver = field.options;
4396
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
4397
- resolver = field.options.resolve;
4398
- cacheTTL = field.options.cacheTTL;
4399
- }
4400
- if (!resolver) {
4401
- return c.json(
4402
- {
4403
- error: true,
4404
- message: `Field ${fieldName} in ${colSlug} is not dynamic`
4405
- },
4406
- 400
4407
- );
4408
- }
4409
- try {
4410
- const db = c.get("db") || config.db;
4411
- const queryParams = c.req.query();
4412
- const reqContext = {
4413
- query: queryParams,
4414
- headers: c.req.header(),
4415
- raw: c.req.raw
4416
- };
4417
- const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
4418
- let cacheKey = "";
4419
- if (shouldCache) {
4420
- cacheKey = JSON.stringify([
4421
- siteId ?? "",
4422
- colSlug,
4423
- fieldName,
4424
- user?.id ?? null,
4425
- queryParams
4426
- ]);
4427
- const hit = optionsCache.get(cacheKey);
4428
- if (hit && hit.expires > Date.now()) {
4429
- return c.json(hit.value);
4430
- }
4431
- }
4432
- const result = await resolver({
4433
- db,
4534
+ const accessExpr = glb.access?.read;
4535
+ if (accessExpr !== void 0 && accessExpr !== null) {
4536
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4434
4537
  user,
4435
- req: reqContext
4538
+ req: toHookRequestContext(c.req)
4436
4539
  });
4437
- if (shouldCache) {
4438
- optionsCache.set(cacheKey, {
4439
- expires: Date.now() + cacheTTL * 1e3,
4440
- value: result
4441
- });
4540
+ if (!allowed) {
4541
+ return c.json(
4542
+ {
4543
+ error: true,
4544
+ message: `Access denied: read on global ${colSlug}`
4545
+ },
4546
+ 403
4547
+ );
4442
4548
  }
4443
- return c.json(result);
4444
- } catch (err) {
4445
- getRequestLogger(c, "router").error({
4446
- err,
4447
- msg: "Failed to resolve dynamic field options",
4448
- 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
4449
4610
  });
4450
- return c.json(
4451
- {
4452
- error: true,
4453
- message: err.message || "Failed to resolve dynamic options"
4454
- },
4455
- 500
4456
- );
4457
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
+ );
4458
4626
  }
4459
- );
4627
+ });
4460
4628
  app.get("/api/openapi.json", (c) => {
4461
4629
  return c.json(generateOpenApi(config));
4462
4630
  });
@@ -4469,13 +4637,8 @@ function registerRoutes(app, config) {
4469
4637
  const key = c.req.param("key");
4470
4638
  const scope = c.req.query("scope");
4471
4639
  if (!db) return c.json({ message: "Database not configured" }, 500);
4472
- if (!user?.collection || !user.sub)
4473
- return c.json({ error: true, message: "Authentication required." }, 401);
4474
- if (!key)
4475
- return c.json(
4476
- { error: true, message: "Preference key is required." },
4477
- 400
4478
- );
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);
4479
4642
  const getGlobalPreference = async () => {
4480
4643
  const globalDoc = await db.findOne({
4481
4644
  collection: "__global_preferences",
@@ -4502,13 +4665,8 @@ function registerRoutes(app, config) {
4502
4665
  const key = c.req.param("key");
4503
4666
  const scope = c.req.query("scope");
4504
4667
  if (!db) return c.json({ message: "Database not configured" }, 500);
4505
- if (!user?.collection || !user.sub)
4506
- return c.json({ error: true, message: "Authentication required." }, 401);
4507
- if (!key)
4508
- return c.json(
4509
- { error: true, message: "Preference key is required." },
4510
- 400
4511
- );
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);
4512
4670
  const body = await c.req.json().catch(() => ({}));
4513
4671
  if (scope === "global") {
4514
4672
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
@@ -4556,13 +4714,8 @@ function registerRoutes(app, config) {
4556
4714
  const key = c.req.param("key");
4557
4715
  const scope = c.req.query("scope");
4558
4716
  if (!db) return c.json({ message: "Database not configured" }, 500);
4559
- if (!user?.collection || !user.sub)
4560
- return c.json({ error: true, message: "Authentication required." }, 401);
4561
- if (!key)
4562
- return c.json(
4563
- { error: true, message: "Preference key is required." },
4564
- 400
4565
- );
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);
4566
4719
  if (scope === "global") {
4567
4720
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
4568
4721
  if (!isAdminUser) {
@@ -4601,41 +4754,17 @@ function registerRoutes(app, config) {
4601
4754
  for (const col of uploadCollections) {
4602
4755
  const mediaController = new MediaController(col.slug);
4603
4756
  const prefix = `/api/collections/${col.slug}`;
4604
- app.get(
4605
- `${prefix}/media`,
4606
- accessGate(config, col, "read"),
4607
- (c) => mediaController.find(c)
4608
- );
4609
- app.get(
4610
- `${prefix}/media/:filename{.+$}`,
4611
- (c) => mediaController.serve(c)
4612
- );
4613
- app.post(
4614
- `${prefix}/media`,
4615
- accessGate(config, col, "create"),
4616
- (c) => mediaController.upload(c)
4617
- );
4618
- app.delete(
4619
- `${prefix}/media/:id`,
4620
- accessGate(config, col, "delete"),
4621
- (c) => mediaController.delete(c)
4622
- );
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));
4623
4761
  }
4624
4762
  }
4625
4763
  const adminAuthController = new AdminAuthController(config);
4626
4764
  app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
4627
- app.get(
4628
- "/api/admin/auth/:provider/start",
4629
- (c) => adminAuthController.start(c)
4630
- );
4631
- app.get(
4632
- "/api/admin/auth/:provider/callback",
4633
- (c) => adminAuthController.callback(c)
4634
- );
4635
- app.post(
4636
- "/api/admin/auth/:provider/exchange",
4637
- (c) => adminAuthController.exchange(c)
4638
- );
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));
4639
4768
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
4640
4769
  for (const collection of config.collections) {
4641
4770
  if (!collection.auth) continue;
@@ -4646,21 +4775,10 @@ function registerRoutes(app, config) {
4646
4775
  app.get(`${path}/init`, (c) => authController.init(c));
4647
4776
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
4648
4777
  app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
4649
- app.post(
4650
- `${path}/refresh-token`,
4651
- requireAuth(config),
4652
- (c) => authController.refreshToken(c)
4653
- );
4654
- app.post(
4655
- `${path}/forgot-password`,
4656
- (c) => authController.forgotPassword(c)
4657
- );
4778
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
4779
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
4658
4780
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
4659
- app.post(
4660
- `${path}/invite`,
4661
- requireAuth(config),
4662
- (c) => authController.invite(c)
4663
- );
4781
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
4664
4782
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
4665
4783
  }
4666
4784
  const auditController = new AuditController();
@@ -4671,33 +4789,19 @@ function registerRoutes(app, config) {
4671
4789
  app.post(path, (c) => controller.create(c));
4672
4790
  app.post(`${path}/media`, (c) => controller.create(c));
4673
4791
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
4792
+ app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
4674
4793
  if (collection.audit) {
4675
- app.get(
4676
- `${path}/__audit`,
4677
- (c) => auditController.findForCollection(c, collection)
4678
- );
4794
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
4679
4795
  }
4680
4796
  app.get(`${path}/:id`, (c) => controller.findOne(c));
4681
4797
  app.patch(`${path}/:id`, (c) => controller.update(c));
4682
4798
  app.delete(`${path}/:id`, (c) => controller.delete(c));
4683
4799
  if (collection.auth) {
4684
- app.post(
4685
- `${path}/:id/change-password`,
4686
- requireAuth(config),
4687
- (c) => controller.changePassword(c)
4688
- );
4800
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
4689
4801
  }
4690
4802
  if (collection.workflow) {
4691
- app.post(
4692
- `${path}/:id/transitions/:transition`,
4693
- requireAuth(config),
4694
- (c) => controller.transition(c)
4695
- );
4696
- app.get(
4697
- `${path}/:id/workflow-history`,
4698
- requireAuth(config),
4699
- (c) => controller.workflowHistory(c)
4700
- );
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));
4701
4805
  }
4702
4806
  }
4703
4807
  for (const global of config.globals) {
@@ -4712,11 +4816,7 @@ function registerRoutes(app, config) {
4712
4816
  });
4713
4817
  }
4714
4818
  const previewController = new PreviewController();
4715
- app.post(
4716
- "/api/preview-token",
4717
- requireAuth(config),
4718
- (c) => previewController.createToken(c)
4719
- );
4819
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
4720
4820
  app.get("/api/preview-data", (c) => previewController.getData(c));
4721
4821
  app.get("/api/audit", (c) => auditController.findAll(c));
4722
4822
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -4729,81 +4829,49 @@ function registerRoutes(app, config) {
4729
4829
  if (!config2.onSchemaFetch || !siteId) {
4730
4830
  return c.json({ message: `Collection "${slug}" not found` }, 404);
4731
4831
  }
4732
- const requestConfig = mergeDynamicConfig(
4733
- config2,
4734
- await config2.onSchemaFetch(siteId)
4735
- );
4736
- const collection = requestConfig.collections.find(
4737
- (col) => col.slug === slug
4738
- );
4832
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4833
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4739
4834
  if (!collection?.audit) {
4740
- return c.json(
4741
- { message: `Collection "${slug}" not found or has no audit log` },
4742
- 404
4743
- );
4835
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
4744
4836
  }
4745
4837
  return auditController.findForCollection(c, collection);
4746
4838
  });
4747
- app.post(
4748
- "/api/collections/:slug/:id/transitions/:transition",
4749
- requireAuth(config),
4750
- async (c) => {
4751
- const slug = c.req.param("slug");
4752
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4753
- const config2 = c.get("config");
4754
- if (config2.collections.some((col) => col.slug === slug)) {
4755
- return c.json({ message: "Not Found" }, 404);
4756
- }
4757
- if (!config2.onSchemaFetch || !siteId) {
4758
- return c.json({ message: `Collection "${slug}" not found` }, 404);
4759
- }
4760
- const requestConfig = mergeDynamicConfig(
4761
- config2,
4762
- await config2.onSchemaFetch(siteId)
4763
- );
4764
- const collection = requestConfig.collections.find(
4765
- (col) => col.slug === slug
4766
- );
4767
- if (!collection?.workflow) {
4768
- return c.json(
4769
- { message: `Collection "${slug}" not found or has no workflow` },
4770
- 404
4771
- );
4772
- }
4773
- const controller = new CollectionController(collection);
4774
- 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);
4775
4845
  }
4776
- );
4777
- app.get(
4778
- "/api/collections/:slug/:id/workflow-history",
4779
- requireAuth(config),
4780
- async (c) => {
4781
- const slug = c.req.param("slug");
4782
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4783
- const config2 = c.get("config");
4784
- if (config2.collections.some((col) => col.slug === slug)) {
4785
- return c.json({ message: "Not Found" }, 404);
4786
- }
4787
- if (!config2.onSchemaFetch || !siteId) {
4788
- return c.json({ message: `Collection "${slug}" not found` }, 404);
4789
- }
4790
- const requestConfig = mergeDynamicConfig(
4791
- config2,
4792
- await config2.onSchemaFetch(siteId)
4793
- );
4794
- const collection = requestConfig.collections.find(
4795
- (col) => col.slug === slug
4796
- );
4797
- if (!collection?.workflow) {
4798
- return c.json(
4799
- { message: `Collection "${slug}" not found or has no workflow` },
4800
- 404
4801
- );
4802
- }
4803
- const controller = new CollectionController(collection);
4804
- return controller.workflowHistory(c);
4846
+ if (!config2.onSchemaFetch || !siteId) {
4847
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4805
4848
  }
4806
- );
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
+ });
4807
4875
  app.all("/api/collections/:slug/:id?", async (c) => {
4808
4876
  const slug = c.req.param("slug");
4809
4877
  const id = c.req.param("id");
@@ -4813,13 +4881,8 @@ function registerRoutes(app, config) {
4813
4881
  return c.json({ message: "Method Not Allowed" }, 405);
4814
4882
  }
4815
4883
  if (config2.onSchemaFetch && siteId) {
4816
- const requestConfig = mergeDynamicConfig(
4817
- config2,
4818
- await config2.onSchemaFetch(siteId)
4819
- );
4820
- let collection = requestConfig.collections.find(
4821
- (col) => col.slug === slug
4822
- );
4884
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4885
+ let collection = requestConfig.collections.find((col) => col.slug === slug);
4823
4886
  if (!collection && slug === "media") {
4824
4887
  collection = {
4825
4888
  slug: "media",
@@ -4832,25 +4895,19 @@ function registerRoutes(app, config) {
4832
4895
  if (collection.auth && id) {
4833
4896
  const authController = new AuthController(collection);
4834
4897
  const method2 = c.req.method;
4835
- if (method2 === "POST" && id === "login")
4836
- return authController.login(c);
4837
- if (method2 === "POST" && id === "logout")
4838
- return authController.logout(c);
4898
+ if (method2 === "POST" && id === "login") return authController.login(c);
4899
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
4839
4900
  if (method2 === "GET" && id === "me") return authController.me(c);
4840
- if (method2 === "POST" && id === "refresh-token")
4841
- return authController.refreshToken(c);
4842
- if (method2 === "POST" && id === "forgot-password")
4843
- return authController.forgotPassword(c);
4844
- if (method2 === "POST" && id === "reset-password")
4845
- 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);
4846
4904
  }
4847
4905
  const controller = new CollectionController(collection);
4848
4906
  const method = c.req.method;
4849
4907
  if (id) {
4850
4908
  if (method === "GET") return controller.findOne(c);
4851
4909
  if (method === "PATCH") return controller.update(c);
4852
- if (method === "DELETE" && id === "delete-many")
4853
- return controller.deleteMany(c);
4910
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
4854
4911
  if (method === "DELETE") return controller.delete(c);
4855
4912
  if (method === "POST" && id === "media") return controller.create(c);
4856
4913
  } else {
@@ -4870,10 +4927,7 @@ function registerRoutes(app, config) {
4870
4927
  return c.json({ message: "Method Not Allowed" }, 405);
4871
4928
  }
4872
4929
  if (config2.onSchemaFetch && siteId) {
4873
- const requestConfig = mergeDynamicConfig(
4874
- config2,
4875
- await config2.onSchemaFetch(siteId)
4876
- );
4930
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4877
4931
  const global = requestConfig.globals.find((glb) => glb.slug === slug);
4878
4932
  if (global) {
4879
4933
  const controller = new GlobalController(global);
@@ -4912,7 +4966,7 @@ function resolveClientIp(req, trustProxy = false) {
4912
4966
  const value = normalizeIp(req.headers[header]);
4913
4967
  if (value) return value;
4914
4968
  }
4915
- if (trustProxy) {
4969
+ if (trustProxy === true || typeof trustProxy === "number") {
4916
4970
  const forwardedFor = pickForwardedFor(req.headers, trustProxy);
4917
4971
  if (forwardedFor) return forwardedFor;
4918
4972
  }
@@ -4924,6 +4978,7 @@ function pickForwardedFor(headers, trustProxy) {
4924
4978
  const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
4925
4979
  if (chain2.length > 0) {
4926
4980
  if (trustProxy === true) return chain2[0];
4981
+ if (trustProxy <= 0) return null;
4927
4982
  const trustedHops2 = Math.max(1, Math.floor(trustProxy));
4928
4983
  const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
4929
4984
  return chain2[candidateIndex2] ?? chain2[0];
@@ -4936,6 +4991,7 @@ function pickForwardedFor(headers, trustProxy) {
4936
4991
  const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
4937
4992
  if (chain.length === 0) return null;
4938
4993
  if (trustProxy === true) return chain[0];
4994
+ if (trustProxy <= 0) return null;
4939
4995
  const trustedHops = Math.max(1, Math.floor(trustProxy));
4940
4996
  const candidateIndex = Math.max(0, chain.length - trustedHops);
4941
4997
  return chain[candidateIndex] ?? chain[0];
@@ -5139,7 +5195,7 @@ async function createDyrectedApp(rawConfig) {
5139
5195
  app.use("*", cors(config.cors ? { origin: config.cors.origins } : {}));
5140
5196
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
5141
5197
  if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
5142
- app.get(observability.config.metrics.path, async (c) => {
5198
+ app.get(observability.config.metrics.path, async (_c) => {
5143
5199
  const metrics = await renderPrometheusMetrics(
5144
5200
  observability.prometheusExporter
5145
5201
  );
@@ -5195,5 +5251,6 @@ export {
5195
5251
  registerRoutes,
5196
5252
  renderPrometheusMetrics,
5197
5253
  resolveObservabilityConfig,
5254
+ serializeDetailForApi,
5198
5255
  shouldSampleRequest
5199
5256
  };