@dyrected/core 2.7.1 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) 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-B5tYsZaH.d.cts +2404 -0
  6. package/dist/app-config-B5tYsZaH.d.ts +2404 -0
  7. package/dist/app-config-BEuyPktb.d.cts +2556 -0
  8. package/dist/app-config-BEuyPktb.d.ts +2556 -0
  9. package/dist/app-config-BIkjQWzU.d.cts +2218 -0
  10. package/dist/app-config-BIkjQWzU.d.ts +2218 -0
  11. package/dist/app-config-BfpeE3bF.d.cts +2100 -0
  12. package/dist/app-config-BfpeE3bF.d.ts +2100 -0
  13. package/dist/app-config-CCe-yq9q.d.cts +2222 -0
  14. package/dist/app-config-CCe-yq9q.d.ts +2222 -0
  15. package/dist/app-config-CPOs7THF.d.cts +2217 -0
  16. package/dist/app-config-CPOs7THF.d.ts +2217 -0
  17. package/dist/app-config-Ctp454zB.d.cts +2214 -0
  18. package/dist/app-config-Ctp454zB.d.ts +2214 -0
  19. package/dist/app-config-DJmNYgYh.d.cts +2305 -0
  20. package/dist/app-config-DJmNYgYh.d.ts +2305 -0
  21. package/dist/app-config-DO3laV58.d.cts +2397 -0
  22. package/dist/app-config-DO3laV58.d.ts +2397 -0
  23. package/dist/app-config-DZf_mmi9.d.cts +2211 -0
  24. package/dist/app-config-DZf_mmi9.d.ts +2211 -0
  25. package/dist/app-config-SkUoN5JU.d.cts +2192 -0
  26. package/dist/app-config-SkUoN5JU.d.ts +2192 -0
  27. package/dist/app-config-WzqpuZMr.d.cts +2566 -0
  28. package/dist/app-config-WzqpuZMr.d.ts +2566 -0
  29. package/dist/app-config-fEjRm1o9.d.cts +2402 -0
  30. package/dist/app-config-fEjRm1o9.d.ts +2402 -0
  31. package/dist/app-config-oz8H_MOX.d.cts +2224 -0
  32. package/dist/app-config-oz8H_MOX.d.ts +2224 -0
  33. package/dist/chunk-3TRRTN6U.js +3441 -0
  34. package/dist/chunk-5ZIA5COH.js +3414 -0
  35. package/dist/chunk-6TWQ6GTD.js +3480 -0
  36. package/dist/chunk-AQO4HI2Y.js +3204 -0
  37. package/dist/chunk-DFBLNYUH.js +3402 -0
  38. package/dist/chunk-GFFGDHST.js +3543 -0
  39. package/dist/chunk-KEKOEAX6.js +3139 -0
  40. package/dist/chunk-PRQ2RF5C.js +3485 -0
  41. package/dist/chunk-PZMDK3MH.js +3203 -0
  42. package/dist/chunk-SM32ND5M.js +3472 -0
  43. package/dist/chunk-XX7N64NL.js +3550 -0
  44. package/dist/chunk-ZISFMAEL.js +3550 -0
  45. package/dist/index.cjs +439 -9
  46. package/dist/index.d.cts +166 -3
  47. package/dist/index.d.ts +166 -3
  48. package/dist/index.js +53 -1
  49. package/dist/server.cjs +623 -367
  50. package/dist/server.d.cts +26 -7
  51. package/dist/server.d.ts +26 -7
  52. package/dist/server.js +420 -359
  53. 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-GFFGDHST.js";
32
33
  import {
33
34
  attachRequestMetrics,
34
35
  bindObservabilityRuntime,
@@ -162,9 +163,13 @@ var PopulationService = class {
162
163
  }
163
164
  if (!field.name) continue;
164
165
  const value = populatedDoc[field.name];
165
- if (field.type === "relationship" && field.relationTo && value) {
166
+ const fieldType = field.type;
167
+ const isRelationship = fieldType === "relationship";
168
+ const isUpload = fieldType === "upload" || fieldType === "image" || fieldType === "media";
169
+ const targetRelationTo = field.relationTo || (isUpload ? this.collections.find((c) => c.upload)?.slug || "media" : void 0);
170
+ if ((isRelationship || isUpload) && targetRelationTo && value) {
166
171
  const relatedCollection = this.collections.find(
167
- (c) => c.slug === field.relationTo
172
+ (c) => c.slug === targetRelationTo
168
173
  );
169
174
  if (!relatedCollection) continue;
170
175
  if (Array.isArray(value)) {
@@ -174,7 +179,7 @@ var PopulationService = class {
174
179
  let doc = id;
175
180
  if (typeof id === "string") {
176
181
  doc = await this.db.findOne({
177
- collection: field.relationTo,
182
+ collection: targetRelationTo,
178
183
  id
179
184
  });
180
185
  }
@@ -195,7 +200,7 @@ var PopulationService = class {
195
200
  let doc = value;
196
201
  if (typeof value === "string") {
197
202
  doc = await this.db.findOne({
198
- collection: field.relationTo,
203
+ collection: targetRelationTo,
199
204
  id: value
200
205
  });
201
206
  }
@@ -865,6 +870,22 @@ var CollectionController = class {
865
870
  toHookRequestContext(c) {
866
871
  return toHookRequestContext(c.req);
867
872
  }
873
+ sanitizeDoc(doc) {
874
+ if (!doc || typeof doc !== "object") return doc;
875
+ if (this.collection.auth) {
876
+ const {
877
+ password: _p,
878
+ salt: _s,
879
+ resetPasswordToken: _rpt,
880
+ resetPasswordExpires: _rpe,
881
+ loginAttempts: _la,
882
+ lockUntil: _lu,
883
+ ...safeDoc
884
+ } = doc;
885
+ return safeDoc;
886
+ }
887
+ return doc;
888
+ }
868
889
  async evaluateAccess(c, action, options = {}) {
869
890
  const config = c.get("config");
870
891
  return resolveCollectionAccess(
@@ -1097,6 +1118,9 @@ var CollectionController = class {
1097
1118
  depth
1098
1119
  );
1099
1120
  }
1121
+ if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
1122
+ result.docs = result.docs.map((d) => this.sanitizeDoc(d));
1123
+ }
1100
1124
  return c.json(result);
1101
1125
  }
1102
1126
  async findOne(c) {
@@ -1113,32 +1137,51 @@ var CollectionController = class {
1113
1137
  id: id2
1114
1138
  });
1115
1139
  const externalSubject = localDoc?.externalSubject || id2;
1116
- let member = null;
1117
1140
  if (provider.members.get) {
1118
- member = await provider.members.get({ externalSubject, req: hookReq });
1141
+ const member = await provider.members.get({
1142
+ externalSubject,
1143
+ req: hookReq
1144
+ });
1145
+ if (member) {
1146
+ return c.json({
1147
+ ...member,
1148
+ id: localDoc ? localDoc.id : member.id,
1149
+ externalSubject: member.id
1150
+ });
1151
+ }
1119
1152
  } else if (provider.members.list) {
1120
1153
  const listResult = await provider.members.list({ req: hookReq });
1121
- member = listResult.docs.find((m) => m.id === externalSubject) || null;
1154
+ const matched = listResult.docs.find((m) => m.id === externalSubject || m.id === id2);
1155
+ if (matched) {
1156
+ return c.json({
1157
+ ...matched,
1158
+ id: localDoc ? localDoc.id : matched.id,
1159
+ externalSubject: matched.id
1160
+ });
1161
+ }
1122
1162
  }
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
1163
  }
1130
- const readonlyDb = createReadonlyDb(db);
1131
1164
  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
1165
  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);
1166
+ const depth = Number.parseInt(c.req.query("depth") || "0", 10);
1167
+ const user = c.get("user");
1168
+ const doc = await db.findOne({
1169
+ collection: this.collection.slug,
1170
+ id
1171
+ });
1172
+ if (!doc) {
1173
+ return c.json({ message: "Document not found" }, 404);
1174
+ }
1175
+ let effectiveDoc = doc;
1137
1176
  if (this.collection.workflow) {
1138
- doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
1139
- if (!doc) return c.json({ message: "Not Found" }, 404);
1177
+ effectiveDoc = materializeWorkflowDocument(
1178
+ doc,
1179
+ this.collection.workflow,
1180
+ user
1181
+ );
1182
+ if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
1140
1183
  }
1141
- const access = await this.evaluateAccess(c, "read", { id, doc });
1184
+ const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
1142
1185
  if (!access.allowed) {
1143
1186
  return c.json(
1144
1187
  {
@@ -1148,7 +1191,11 @@ var CollectionController = class {
1148
1191
  403
1149
1192
  );
1150
1193
  }
1151
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
1194
+ const docWithDefaults = DefaultsService.apply(
1195
+ this.collection.fields,
1196
+ effectiveDoc
1197
+ );
1198
+ const readonlyDb = createReadonlyDb(db);
1152
1199
  const docWithCollectionHooks = await runCollectionHooks(
1153
1200
  this.collection.hooks?.afterRead,
1154
1201
  {
@@ -1174,17 +1221,20 @@ var CollectionController = class {
1174
1221
  },
1175
1222
  docWithFieldHooks
1176
1223
  );
1224
+ let finalDoc = docWithFieldAccess;
1177
1225
  if (depth > 0 && docWithFieldAccess) {
1178
1226
  const populationService = new PopulationService(db, config.collections);
1179
- const populatedDoc = await populationService.populate({
1227
+ finalDoc = await populationService.populate({
1180
1228
  data: docWithFieldAccess,
1181
1229
  fields: this.collection.fields,
1182
1230
  currentDepth: 0,
1183
1231
  maxDepth: depth
1184
1232
  });
1185
- return c.json(populatedDoc);
1186
1233
  }
1187
- return c.json(docWithFieldAccess);
1234
+ if (finalDoc && this.collection.detail) {
1235
+ finalDoc = await evaluateDetailComputed(this.collection.detail, finalDoc, user, readonlyDb);
1236
+ }
1237
+ return c.json(this.sanitizeDoc(finalDoc));
1188
1238
  }
1189
1239
  async create(c) {
1190
1240
  const config = c.get("config");
@@ -1956,6 +2006,63 @@ var CollectionController = class {
1956
2006
  201
1957
2007
  );
1958
2008
  }
2009
+ async aggregate(c) {
2010
+ const config = c.get("config");
2011
+ const db = config.db;
2012
+ if (!db) return c.json({ message: "Database not configured" }, 500);
2013
+ const access = await this.evaluateAccess(c, "read");
2014
+ if (!access.allowed) {
2015
+ return c.json(
2016
+ {
2017
+ error: true,
2018
+ message: `Access denied: read on ${this.collection.slug}`
2019
+ },
2020
+ 403
2021
+ );
2022
+ }
2023
+ let body;
2024
+ try {
2025
+ body = await c.req.json();
2026
+ } catch {
2027
+ return c.json({ message: "Invalid JSON body" }, 400);
2028
+ }
2029
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
2030
+ return c.json(
2031
+ { message: "Aggregate request body must be a JSON object" },
2032
+ 400
2033
+ );
2034
+ }
2035
+ const { sanitizeWhereClause } = await import("./where-sanitizer-WJ2W2QCE.js");
2036
+ const sanitizedAggregates = {};
2037
+ for (const [key, op] of Object.entries(body)) {
2038
+ if (!op || typeof op !== "object" || Array.isArray(op)) {
2039
+ return c.json(
2040
+ {
2041
+ message: `Aggregate operation "${key}" must be an object`
2042
+ },
2043
+ 400
2044
+ );
2045
+ }
2046
+ const operation = op;
2047
+ let opWhere = operation.where && typeof operation.where === "object" ? operation.where : void 0;
2048
+ if (opWhere) {
2049
+ opWhere = sanitizeWhereClause(opWhere, this.collection.fields);
2050
+ if (Object.keys(opWhere).length === 0) opWhere = void 0;
2051
+ }
2052
+ if (access.constraint) {
2053
+ opWhere = opWhere ? mergeWhereConstraint(opWhere, access.constraint) : access.constraint;
2054
+ }
2055
+ sanitizedAggregates[key] = {
2056
+ ...operation,
2057
+ ...opWhere !== void 0 ? { where: opWhere } : { where: void 0 }
2058
+ };
2059
+ }
2060
+ const result = await db.aggregate({
2061
+ collection: this.collection.slug,
2062
+ aggregates: sanitizedAggregates
2063
+ });
2064
+ return c.json(result);
2065
+ }
1959
2066
  };
1960
2067
 
1961
2068
  // src/controllers/global.controller.ts
@@ -2038,17 +2145,25 @@ var GlobalController = class {
2038
2145
  },
2039
2146
  docWithFieldHooks
2040
2147
  );
2148
+ let finalDoc = docWithFieldAccess;
2041
2149
  if (depth > 0 && docWithFieldAccess) {
2042
2150
  const populationService = new PopulationService(db, config.collections);
2043
- const populatedData = await populationService.populate({
2151
+ finalDoc = await populationService.populate({
2044
2152
  data: docWithFieldAccess,
2045
2153
  fields: this.global.fields,
2046
2154
  currentDepth: 0,
2047
2155
  maxDepth: depth
2048
2156
  });
2049
- return c.json(populatedData);
2050
2157
  }
2051
- return c.json(docWithFieldAccess);
2158
+ if (finalDoc && this.global.detail) {
2159
+ finalDoc = await evaluateDetailComputed(
2160
+ this.global.detail,
2161
+ finalDoc,
2162
+ user,
2163
+ readonlyDb
2164
+ );
2165
+ }
2166
+ return c.json(finalDoc);
2052
2167
  }
2053
2168
  async update(c) {
2054
2169
  const config = c.get("config");
@@ -3391,6 +3506,7 @@ var AdminAuthController = class {
3391
3506
  });
3392
3507
  }
3393
3508
  getProvider(config, id) {
3509
+ if (!id) return void 0;
3394
3510
  if (config.adminAuth?.mode !== "external") return void 0;
3395
3511
  return config.adminAuth.providers.find((provider) => provider.id === id);
3396
3512
  }
@@ -3589,17 +3705,16 @@ var AdminAuthController = class {
3589
3705
  throw new Error("OIDC nonce verification failed.");
3590
3706
  }
3591
3707
  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
- );
3708
+ } else {
3709
+ const userInfoEndpoint = provider.userInfoEndpoint ?? discovery.userinfo_endpoint;
3710
+ if (!tokenBody.access_token || !userInfoEndpoint) {
3711
+ throw new Error("OIDC provider did not return usable identity claims.");
3712
+ }
3713
+ const userInfo = await fetch(userInfoEndpoint, {
3714
+ headers: { Authorization: `Bearer ${tokenBody.access_token}` }
3715
+ });
3599
3716
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
3600
3717
  claims = await userInfo.json();
3601
- } else {
3602
- throw new Error("OIDC provider did not return usable identity claims.");
3603
3718
  }
3604
3719
  return {
3605
3720
  identity: this.mapClaims(claims, provider.claimMapping),
@@ -4144,10 +4259,7 @@ function accessGate(config, target, action) {
4144
4259
  req: toHookRequestContext(c.req)
4145
4260
  });
4146
4261
  if (!allowed) {
4147
- return c.json(
4148
- { error: true, message: `Access denied: ${action} on ${target.slug}` },
4149
- 403
4150
- );
4262
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
4151
4263
  }
4152
4264
  await next();
4153
4265
  };
@@ -4187,6 +4299,80 @@ function serializeBlockForApi(block) {
4187
4299
  fields: block.fields?.map(serializeFieldForApi)
4188
4300
  };
4189
4301
  }
4302
+ function serializeDetailItemForApi(item) {
4303
+ if (!item) return item;
4304
+ if (typeof item === "string") {
4305
+ return { type: "field", field: item };
4306
+ }
4307
+ if (item.type === "section") {
4308
+ return {
4309
+ type: "section",
4310
+ title: item.title,
4311
+ items: (item.items || []).map(serializeDetailItemForApi),
4312
+ options: item.options
4313
+ };
4314
+ }
4315
+ if (item.type === "tab") {
4316
+ return {
4317
+ type: "tab",
4318
+ label: item.label,
4319
+ items: (item.items || []).map(serializeDetailItemForApi),
4320
+ options: item.options
4321
+ };
4322
+ }
4323
+ if (item.type === "tabs") {
4324
+ return {
4325
+ type: "tabs",
4326
+ tabs: (item.tabs || []).map(serializeDetailItemForApi),
4327
+ options: item.options
4328
+ };
4329
+ }
4330
+ if (item.type === "grid") {
4331
+ return {
4332
+ type: "grid",
4333
+ columns: item.columns,
4334
+ items: (item.items || []).map(serializeDetailItemForApi),
4335
+ options: item.options
4336
+ };
4337
+ }
4338
+ if (item.type === "repeat") {
4339
+ return {
4340
+ type: "repeat",
4341
+ field: item.field,
4342
+ items: (item.items || []).map(serializeDetailItemForApi),
4343
+ options: item.options
4344
+ };
4345
+ }
4346
+ if (item.type === "computed") {
4347
+ return {
4348
+ type: "computed",
4349
+ id: item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
4350
+ label: item.label,
4351
+ expression: item.expression,
4352
+ options: {
4353
+ id: item.id,
4354
+ span: item.options?.span,
4355
+ format: item.options?.format,
4356
+ currency: item.options?.currency,
4357
+ visible: item.options?.visible
4358
+ }
4359
+ };
4360
+ }
4361
+ if (item.type === "field") {
4362
+ return {
4363
+ type: "field",
4364
+ field: item.field,
4365
+ options: item.options
4366
+ };
4367
+ }
4368
+ return item;
4369
+ }
4370
+ function serializeDetailForApi(detail) {
4371
+ if (detail === false) return false;
4372
+ if (detail === true) return true;
4373
+ if (!detail || !Array.isArray(detail)) return void 0;
4374
+ return detail.map(serializeDetailItemForApi);
4375
+ }
4190
4376
  function registerRoutes(app, config) {
4191
4377
  const optionsCache = /* @__PURE__ */ new Map();
4192
4378
  app.get("/api/schemas", optionalAuth(config), async (c) => {
@@ -4205,15 +4391,12 @@ function registerRoutes(app, config) {
4205
4391
  if (typeof access === "boolean") return access;
4206
4392
  if (access && typeof access === "object" && typeof access.policy === "string") {
4207
4393
  const policy = requestConfig.accessPolicies?.[access.policy];
4208
- if (typeof policy === "string" || typeof policy === "boolean")
4209
- return policy;
4394
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
4210
4395
  }
4211
4396
  return resolveBooleanAccess(requestConfig, access, accessArgs);
4212
4397
  };
4213
4398
  const filteredCollections = await Promise.all(
4214
- collections.filter(
4215
- (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
4216
- ).map(async (col) => ({
4399
+ collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
4217
4400
  slug: col.slug,
4218
4401
  labels: col.labels,
4219
4402
  access: {
@@ -4251,6 +4434,7 @@ function registerRoutes(app, config) {
4251
4434
  audit: !!col.audit,
4252
4435
  drafts: !!col.drafts,
4253
4436
  admin: col.admin,
4437
+ detail: serializeDetailForApi(col.detail),
4254
4438
  workflow: col.workflow ? {
4255
4439
  initialState: col.workflow.initialState,
4256
4440
  draftState: col.workflow.draftState,
@@ -4269,9 +4453,7 @@ function registerRoutes(app, config) {
4269
4453
  }))
4270
4454
  );
4271
4455
  const filteredGlobals = await Promise.all(
4272
- globals.filter(
4273
- (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
4274
- ).map(async (glb) => ({
4456
+ globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
4275
4457
  slug: glb.slug,
4276
4458
  label: glb.label,
4277
4459
  access: {
@@ -4301,7 +4483,8 @@ function registerRoutes(app, config) {
4301
4483
  }
4302
4484
  }))
4303
4485
  ),
4304
- admin: glb.admin
4486
+ admin: glb.admin,
4487
+ detail: serializeDetailForApi(glb.detail)
4305
4488
  }))
4306
4489
  );
4307
4490
  return c.json({
@@ -4315,148 +4498,137 @@ function registerRoutes(app, config) {
4315
4498
  adminHealth: {
4316
4499
  emailConfigured: !!requestConfig.email,
4317
4500
  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
- )
4501
+ authCollectionConfigured: requestConfig.collections.some((collection) => !!collection.auth),
4502
+ uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload)
4324
4503
  }
4325
4504
  });
4326
4505
  });
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
- }
4506
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
4507
+ const { collection: colSlug, field: fieldName } = c.req.param();
4508
+ const siteId = c.req.header("X-Site-Id");
4509
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
4510
+ let collections = [...requestConfig.collections];
4511
+ const user = c.get("user");
4512
+ let collection = collections.find((col) => col.slug === colSlug);
4513
+ let field;
4514
+ if (collection) {
4515
+ const accessExpr = collection.access?.read;
4516
+ if (accessExpr !== void 0 && accessExpr !== null) {
4517
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4518
+ user,
4519
+ req: toHookRequestContext(c.req)
4520
+ });
4521
+ if (!allowed) {
4522
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
4380
4523
  }
4381
- field = glb.fields.find((f) => f.name === fieldName);
4382
4524
  }
4383
- if (!field) {
4525
+ field = collection.fields.find((f) => f.name === fieldName);
4526
+ } else {
4527
+ const globals = [...requestConfig.globals];
4528
+ const glb = globals.find((g) => g.slug === colSlug);
4529
+ if (!glb) {
4384
4530
  return c.json(
4385
4531
  {
4386
4532
  error: true,
4387
- message: `Field ${fieldName} not found in ${colSlug}`
4533
+ message: `${colSlug} not found as collection or global`
4388
4534
  },
4389
4535
  404
4390
4536
  );
4391
4537
  }
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,
4538
+ const accessExpr = glb.access?.read;
4539
+ if (accessExpr !== void 0 && accessExpr !== null) {
4540
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4434
4541
  user,
4435
- req: reqContext
4542
+ req: toHookRequestContext(c.req)
4436
4543
  });
4437
- if (shouldCache) {
4438
- optionsCache.set(cacheKey, {
4439
- expires: Date.now() + cacheTTL * 1e3,
4440
- value: result
4441
- });
4544
+ if (!allowed) {
4545
+ return c.json(
4546
+ {
4547
+ error: true,
4548
+ message: `Access denied: read on global ${colSlug}`
4549
+ },
4550
+ 403
4551
+ );
4442
4552
  }
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
4553
+ }
4554
+ field = glb.fields.find((f) => f.name === fieldName);
4555
+ }
4556
+ if (!field) {
4557
+ return c.json(
4558
+ {
4559
+ error: true,
4560
+ message: `Field ${fieldName} not found in ${colSlug}`
4561
+ },
4562
+ 404
4563
+ );
4564
+ }
4565
+ let resolver;
4566
+ let cacheTTL;
4567
+ if (typeof field.options === "function") {
4568
+ resolver = field.options;
4569
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
4570
+ resolver = field.options.resolve;
4571
+ cacheTTL = field.options.cacheTTL;
4572
+ }
4573
+ if (!resolver) {
4574
+ return c.json(
4575
+ {
4576
+ error: true,
4577
+ message: `Field ${fieldName} in ${colSlug} is not dynamic`
4578
+ },
4579
+ 400
4580
+ );
4581
+ }
4582
+ try {
4583
+ const db = c.get("db") || config.db;
4584
+ const queryParams = c.req.query();
4585
+ const reqContext = {
4586
+ query: queryParams,
4587
+ headers: c.req.header(),
4588
+ raw: c.req.raw
4589
+ };
4590
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
4591
+ let cacheKey = "";
4592
+ if (shouldCache) {
4593
+ cacheKey = JSON.stringify([
4594
+ siteId ?? "",
4595
+ colSlug,
4596
+ fieldName,
4597
+ user?.id ?? null,
4598
+ queryParams
4599
+ ]);
4600
+ const hit = optionsCache.get(cacheKey);
4601
+ if (hit && hit.expires > Date.now()) {
4602
+ return c.json(hit.value);
4603
+ }
4604
+ }
4605
+ const result = await resolver({
4606
+ db,
4607
+ user,
4608
+ req: reqContext
4609
+ });
4610
+ if (shouldCache) {
4611
+ optionsCache.set(cacheKey, {
4612
+ expires: Date.now() + cacheTTL * 1e3,
4613
+ value: result
4449
4614
  });
4450
- return c.json(
4451
- {
4452
- error: true,
4453
- message: err.message || "Failed to resolve dynamic options"
4454
- },
4455
- 500
4456
- );
4457
4615
  }
4616
+ return c.json(result);
4617
+ } catch (err) {
4618
+ getRequestLogger(c, "router").error({
4619
+ err,
4620
+ msg: "Failed to resolve dynamic field options",
4621
+ fieldName
4622
+ });
4623
+ return c.json(
4624
+ {
4625
+ error: true,
4626
+ message: err.message || "Failed to resolve dynamic options"
4627
+ },
4628
+ 500
4629
+ );
4458
4630
  }
4459
- );
4631
+ });
4460
4632
  app.get("/api/openapi.json", (c) => {
4461
4633
  return c.json(generateOpenApi(config));
4462
4634
  });
@@ -4469,13 +4641,8 @@ function registerRoutes(app, config) {
4469
4641
  const key = c.req.param("key");
4470
4642
  const scope = c.req.query("scope");
4471
4643
  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
- );
4644
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
4645
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4479
4646
  const getGlobalPreference = async () => {
4480
4647
  const globalDoc = await db.findOne({
4481
4648
  collection: "__global_preferences",
@@ -4502,13 +4669,8 @@ function registerRoutes(app, config) {
4502
4669
  const key = c.req.param("key");
4503
4670
  const scope = c.req.query("scope");
4504
4671
  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
- );
4672
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
4673
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4512
4674
  const body = await c.req.json().catch(() => ({}));
4513
4675
  if (scope === "global") {
4514
4676
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
@@ -4556,13 +4718,8 @@ function registerRoutes(app, config) {
4556
4718
  const key = c.req.param("key");
4557
4719
  const scope = c.req.query("scope");
4558
4720
  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
- );
4721
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
4722
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4566
4723
  if (scope === "global") {
4567
4724
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
4568
4725
  if (!isAdminUser) {
@@ -4601,41 +4758,17 @@ function registerRoutes(app, config) {
4601
4758
  for (const col of uploadCollections) {
4602
4759
  const mediaController = new MediaController(col.slug);
4603
4760
  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
- );
4761
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
4762
+ app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
4763
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
4764
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
4623
4765
  }
4624
4766
  }
4625
4767
  const adminAuthController = new AdminAuthController(config);
4626
4768
  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
- );
4769
+ app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
4770
+ app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
4771
+ app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
4639
4772
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
4640
4773
  for (const collection of config.collections) {
4641
4774
  if (!collection.auth) continue;
@@ -4646,21 +4779,10 @@ function registerRoutes(app, config) {
4646
4779
  app.get(`${path}/init`, (c) => authController.init(c));
4647
4780
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
4648
4781
  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
- );
4782
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
4783
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
4658
4784
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
4659
- app.post(
4660
- `${path}/invite`,
4661
- requireAuth(config),
4662
- (c) => authController.invite(c)
4663
- );
4785
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
4664
4786
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
4665
4787
  }
4666
4788
  const auditController = new AuditController();
@@ -4671,33 +4793,19 @@ function registerRoutes(app, config) {
4671
4793
  app.post(path, (c) => controller.create(c));
4672
4794
  app.post(`${path}/media`, (c) => controller.create(c));
4673
4795
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
4796
+ app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
4674
4797
  if (collection.audit) {
4675
- app.get(
4676
- `${path}/__audit`,
4677
- (c) => auditController.findForCollection(c, collection)
4678
- );
4798
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
4679
4799
  }
4680
4800
  app.get(`${path}/:id`, (c) => controller.findOne(c));
4681
4801
  app.patch(`${path}/:id`, (c) => controller.update(c));
4682
4802
  app.delete(`${path}/:id`, (c) => controller.delete(c));
4683
4803
  if (collection.auth) {
4684
- app.post(
4685
- `${path}/:id/change-password`,
4686
- requireAuth(config),
4687
- (c) => controller.changePassword(c)
4688
- );
4804
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
4689
4805
  }
4690
4806
  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
- );
4807
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
4808
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
4701
4809
  }
4702
4810
  }
4703
4811
  for (const global of config.globals) {
@@ -4712,11 +4820,7 @@ function registerRoutes(app, config) {
4712
4820
  });
4713
4821
  }
4714
4822
  const previewController = new PreviewController();
4715
- app.post(
4716
- "/api/preview-token",
4717
- requireAuth(config),
4718
- (c) => previewController.createToken(c)
4719
- );
4823
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
4720
4824
  app.get("/api/preview-data", (c) => previewController.getData(c));
4721
4825
  app.get("/api/audit", (c) => auditController.findAll(c));
4722
4826
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -4729,81 +4833,49 @@ function registerRoutes(app, config) {
4729
4833
  if (!config2.onSchemaFetch || !siteId) {
4730
4834
  return c.json({ message: `Collection "${slug}" not found` }, 404);
4731
4835
  }
4732
- const requestConfig = mergeDynamicConfig(
4733
- config2,
4734
- await config2.onSchemaFetch(siteId)
4735
- );
4736
- const collection = requestConfig.collections.find(
4737
- (col) => col.slug === slug
4738
- );
4836
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4837
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4739
4838
  if (!collection?.audit) {
4740
- return c.json(
4741
- { message: `Collection "${slug}" not found or has no audit log` },
4742
- 404
4743
- );
4839
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
4744
4840
  }
4745
4841
  return auditController.findForCollection(c, collection);
4746
4842
  });
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);
4843
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
4844
+ const slug = c.req.param("slug");
4845
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4846
+ const config2 = c.get("config");
4847
+ if (config2.collections.some((col) => col.slug === slug)) {
4848
+ return c.json({ message: "Not Found" }, 404);
4775
4849
  }
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);
4850
+ if (!config2.onSchemaFetch || !siteId) {
4851
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4805
4852
  }
4806
- );
4853
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4854
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4855
+ if (!collection?.workflow) {
4856
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
4857
+ }
4858
+ const controller = new CollectionController(collection);
4859
+ return controller.transition(c);
4860
+ });
4861
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
4862
+ const slug = c.req.param("slug");
4863
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4864
+ const config2 = c.get("config");
4865
+ if (config2.collections.some((col) => col.slug === slug)) {
4866
+ return c.json({ message: "Not Found" }, 404);
4867
+ }
4868
+ if (!config2.onSchemaFetch || !siteId) {
4869
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4870
+ }
4871
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4872
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
4873
+ if (!collection?.workflow) {
4874
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
4875
+ }
4876
+ const controller = new CollectionController(collection);
4877
+ return controller.workflowHistory(c);
4878
+ });
4807
4879
  app.all("/api/collections/:slug/:id?", async (c) => {
4808
4880
  const slug = c.req.param("slug");
4809
4881
  const id = c.req.param("id");
@@ -4813,13 +4885,8 @@ function registerRoutes(app, config) {
4813
4885
  return c.json({ message: "Method Not Allowed" }, 405);
4814
4886
  }
4815
4887
  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
- );
4888
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4889
+ let collection = requestConfig.collections.find((col) => col.slug === slug);
4823
4890
  if (!collection && slug === "media") {
4824
4891
  collection = {
4825
4892
  slug: "media",
@@ -4832,25 +4899,19 @@ function registerRoutes(app, config) {
4832
4899
  if (collection.auth && id) {
4833
4900
  const authController = new AuthController(collection);
4834
4901
  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);
4902
+ if (method2 === "POST" && id === "login") return authController.login(c);
4903
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
4839
4904
  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);
4905
+ if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
4906
+ if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
4907
+ if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
4846
4908
  }
4847
4909
  const controller = new CollectionController(collection);
4848
4910
  const method = c.req.method;
4849
4911
  if (id) {
4850
4912
  if (method === "GET") return controller.findOne(c);
4851
4913
  if (method === "PATCH") return controller.update(c);
4852
- if (method === "DELETE" && id === "delete-many")
4853
- return controller.deleteMany(c);
4914
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
4854
4915
  if (method === "DELETE") return controller.delete(c);
4855
4916
  if (method === "POST" && id === "media") return controller.create(c);
4856
4917
  } else {
@@ -4870,10 +4931,7 @@ function registerRoutes(app, config) {
4870
4931
  return c.json({ message: "Method Not Allowed" }, 405);
4871
4932
  }
4872
4933
  if (config2.onSchemaFetch && siteId) {
4873
- const requestConfig = mergeDynamicConfig(
4874
- config2,
4875
- await config2.onSchemaFetch(siteId)
4876
- );
4934
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
4877
4935
  const global = requestConfig.globals.find((glb) => glb.slug === slug);
4878
4936
  if (global) {
4879
4937
  const controller = new GlobalController(global);
@@ -4912,7 +4970,7 @@ function resolveClientIp(req, trustProxy = false) {
4912
4970
  const value = normalizeIp(req.headers[header]);
4913
4971
  if (value) return value;
4914
4972
  }
4915
- if (trustProxy) {
4973
+ if (trustProxy === true || typeof trustProxy === "number") {
4916
4974
  const forwardedFor = pickForwardedFor(req.headers, trustProxy);
4917
4975
  if (forwardedFor) return forwardedFor;
4918
4976
  }
@@ -4924,6 +4982,7 @@ function pickForwardedFor(headers, trustProxy) {
4924
4982
  const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
4925
4983
  if (chain2.length > 0) {
4926
4984
  if (trustProxy === true) return chain2[0];
4985
+ if (trustProxy <= 0) return null;
4927
4986
  const trustedHops2 = Math.max(1, Math.floor(trustProxy));
4928
4987
  const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
4929
4988
  return chain2[candidateIndex2] ?? chain2[0];
@@ -4936,6 +4995,7 @@ function pickForwardedFor(headers, trustProxy) {
4936
4995
  const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
4937
4996
  if (chain.length === 0) return null;
4938
4997
  if (trustProxy === true) return chain[0];
4998
+ if (trustProxy <= 0) return null;
4939
4999
  const trustedHops = Math.max(1, Math.floor(trustProxy));
4940
5000
  const candidateIndex = Math.max(0, chain.length - trustedHops);
4941
5001
  return chain[candidateIndex] ?? chain[0];
@@ -5139,7 +5199,7 @@ async function createDyrectedApp(rawConfig) {
5139
5199
  app.use("*", cors(config.cors ? { origin: config.cors.origins } : {}));
5140
5200
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
5141
5201
  if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
5142
- app.get(observability.config.metrics.path, async (c) => {
5202
+ app.get(observability.config.metrics.path, async (_c) => {
5143
5203
  const metrics = await renderPrometheusMetrics(
5144
5204
  observability.prometheusExporter
5145
5205
  );
@@ -5195,5 +5255,6 @@ export {
5195
5255
  registerRoutes,
5196
5256
  renderPrometheusMetrics,
5197
5257
  resolveObservabilityConfig,
5258
+ serializeDetailForApi,
5198
5259
  shouldSampleRequest
5199
5260
  };