@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.cjs CHANGED
@@ -627,6 +627,7 @@ __export(server_exports, {
627
627
  registerRoutes: () => registerRoutes,
628
628
  renderPrometheusMetrics: () => renderPrometheusMetrics,
629
629
  resolveObservabilityConfig: () => resolveObservabilityConfig,
630
+ serializeDetailForApi: () => serializeDetailForApi,
630
631
  shouldSampleRequest: () => shouldSampleRequest
631
632
  });
632
633
  module.exports = __toCommonJS(server_exports);
@@ -746,9 +747,13 @@ var PopulationService = class {
746
747
  }
747
748
  if (!field.name) continue;
748
749
  const value = populatedDoc[field.name];
749
- if (field.type === "relationship" && field.relationTo && value) {
750
+ const fieldType = field.type;
751
+ const isRelationship = fieldType === "relationship";
752
+ const isUpload = fieldType === "upload" || fieldType === "image" || fieldType === "media";
753
+ const targetRelationTo = field.relationTo || (isUpload ? this.collections.find((c) => c.upload)?.slug || "media" : void 0);
754
+ if ((isRelationship || isUpload) && targetRelationTo && value) {
750
755
  const relatedCollection = this.collections.find(
751
- (c) => c.slug === field.relationTo
756
+ (c) => c.slug === targetRelationTo
752
757
  );
753
758
  if (!relatedCollection) continue;
754
759
  if (Array.isArray(value)) {
@@ -758,7 +763,7 @@ var PopulationService = class {
758
763
  let doc = id;
759
764
  if (typeof id === "string") {
760
765
  doc = await this.db.findOne({
761
- collection: field.relationTo,
766
+ collection: targetRelationTo,
762
767
  id
763
768
  });
764
769
  }
@@ -779,7 +784,7 @@ var PopulationService = class {
779
784
  let doc = value;
780
785
  if (typeof value === "string") {
781
786
  doc = await this.db.findOne({
782
- collection: field.relationTo,
787
+ collection: targetRelationTo,
783
788
  id: value
784
789
  });
785
790
  }
@@ -1259,9 +1264,42 @@ function round(val, decimals = 0) {
1259
1264
  }
1260
1265
  function clamp(val, minVal, maxVal) {
1261
1266
  const num = Number(val) || 0;
1262
- const min = Number(minVal) || 0;
1263
- const max = Number(maxVal) || 0;
1264
- return Math.min(Math.max(num, min), max);
1267
+ const min2 = Number(minVal) || 0;
1268
+ const max2 = Number(maxVal) || 0;
1269
+ return Math.min(Math.max(num, min2), max2);
1270
+ }
1271
+ function ceil(val) {
1272
+ return Math.ceil(Number(val) || 0);
1273
+ }
1274
+ function floor(val) {
1275
+ return Math.floor(Number(val) || 0);
1276
+ }
1277
+ function abs(val) {
1278
+ return Math.abs(Number(val) || 0);
1279
+ }
1280
+ function min(...args) {
1281
+ const nums = args.flat().map((n) => Number(n) || 0);
1282
+ return nums.length ? Math.min(...nums) : 0;
1283
+ }
1284
+ function max(...args) {
1285
+ const nums = args.flat().map((n) => Number(n) || 0);
1286
+ return nums.length ? Math.max(...nums) : 0;
1287
+ }
1288
+ function pow(base, exp) {
1289
+ return Math.pow(Number(base) || 0, Number(exp) || 0);
1290
+ }
1291
+ function sqrt(val) {
1292
+ return Math.sqrt(Number(val) || 0);
1293
+ }
1294
+ function sum(arr) {
1295
+ if (!Array.isArray(arr)) return Number(arr) || 0;
1296
+ return arr.reduce((acc, val) => acc + (Number(val) || 0), 0);
1297
+ }
1298
+ function count(val) {
1299
+ if (Array.isArray(val)) return val.length;
1300
+ if (typeof val === "string") return val.length;
1301
+ if (val !== void 0 && val !== null && val !== false) return 1;
1302
+ return 0;
1265
1303
  }
1266
1304
  function defaultValue(val, fallback) {
1267
1305
  if (val === void 0 || val === null || val === "") return fallback;
@@ -1280,13 +1318,12 @@ function isEmpty(val) {
1280
1318
  return false;
1281
1319
  }
1282
1320
  function get(obj, path, fallback = null) {
1283
- if (!obj || typeof obj !== "object") return fallback;
1284
- const pathStr = String(path || "").trim();
1285
- if (!pathStr) return fallback;
1286
- const parts = pathStr.split(".");
1321
+ if (obj === void 0 || obj === null || typeof obj !== "object") return fallback;
1322
+ if (typeof path !== "string") return fallback;
1323
+ const parts = path.split(".").filter(Boolean);
1287
1324
  let curr = obj;
1288
1325
  for (const part of parts) {
1289
- if (curr === void 0 || curr === null) return fallback;
1326
+ if (curr === void 0 || curr === null || typeof curr !== "object") return fallback;
1290
1327
  curr = curr[part];
1291
1328
  }
1292
1329
  return curr === void 0 || curr === null ? fallback : curr;
@@ -1324,6 +1361,15 @@ function registerJexlHelpers(jexlInstance = import_jexl2.default) {
1324
1361
  // Math
1325
1362
  round,
1326
1363
  clamp,
1364
+ ceil,
1365
+ floor,
1366
+ abs,
1367
+ min,
1368
+ max,
1369
+ pow,
1370
+ sqrt,
1371
+ sum,
1372
+ count,
1327
1373
  // Objects & Logical
1328
1374
  default: defaultValue,
1329
1375
  coalesce,
@@ -1335,9 +1381,33 @@ function registerJexlHelpers(jexlInstance = import_jexl2.default) {
1335
1381
  jexlInstance.addFunction(name, fn);
1336
1382
  } catch {
1337
1383
  }
1384
+ try {
1385
+ jexlInstance.addTransform(name, fn);
1386
+ } catch {
1387
+ }
1388
+ }
1389
+ try {
1390
+ jexlInstance.addBinaryOp("and", 20, (left, right) => Boolean(left && right));
1391
+ jexlInstance.addBinaryOp("or", 10, (left, right) => Boolean(left || right));
1392
+ } catch {
1338
1393
  }
1339
1394
  }
1340
1395
  registerJexlHelpers(import_jexl2.default);
1396
+ async function evaluateJexl(expression, context = {}) {
1397
+ if (!expression || typeof expression !== "string") return void 0;
1398
+ try {
1399
+ const evalContext = {
1400
+ null: null,
1401
+ undefined: void 0,
1402
+ ...context,
1403
+ math: Math,
1404
+ Math
1405
+ };
1406
+ return await import_jexl2.default.eval(expression, evalContext);
1407
+ } catch (_err) {
1408
+ return void 0;
1409
+ }
1410
+ }
1341
1411
  var BUILTIN_JEXL_HELPERS = [
1342
1412
  // Strings
1343
1413
  "slugify",
@@ -1919,7 +1989,7 @@ function collectConfigDiagnostics(config) {
1919
1989
  function formatConfigDiagnostics(source, diagnostics, options = {}) {
1920
1990
  const color = options.color ?? false;
1921
1991
  const header = `${colorize("Config validation failed", 31, color)} in ${colorize(source, 36, color)}`;
1922
- const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/features/admin/form-and-field-hooks", 34, color)}`;
1992
+ const docLink = `${colorize(" Learn how to write declarative rules:", 90, color)} ${colorize("https://docs.dyrected.com/docs/editor-experience/form-and-field-hooks", 34, color)}`;
1923
1993
  const lines = diagnostics.flatMap((issue, index) => [
1924
1994
  `${colorize(`${index + 1}.`, 31, color)} ${colorize(issue.path, 1, color)}`,
1925
1995
  ` ${issue.message}`,
@@ -3015,6 +3085,71 @@ async function transitionWorkflow(args) {
3015
3085
 
3016
3086
  // src/controllers/collection.controller.ts
3017
3087
  init_observability();
3088
+
3089
+ // src/detail.ts
3090
+ function normalizeDetailItem(item) {
3091
+ if (typeof item === "string") {
3092
+ return {
3093
+ type: "field",
3094
+ field: item
3095
+ };
3096
+ }
3097
+ return item;
3098
+ }
3099
+ async function evaluateDetailComputed(detail, doc, user, db) {
3100
+ if (!doc || !detail || !Array.isArray(detail)) {
3101
+ return doc;
3102
+ }
3103
+ const computedItems = [];
3104
+ const extractComputed = (items) => {
3105
+ for (const rawItem of items) {
3106
+ if (!rawItem) continue;
3107
+ const item = normalizeDetailItem(rawItem);
3108
+ if (item.type === "computed") {
3109
+ const id = item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_");
3110
+ if (id) {
3111
+ computedItems.push({
3112
+ id,
3113
+ label: item.label,
3114
+ expression: item.expression,
3115
+ handler: item.handler
3116
+ });
3117
+ }
3118
+ } else if (item.type === "section" || item.type === "grid" || item.type === "repeat") {
3119
+ if (item.items && Array.isArray(item.items)) {
3120
+ extractComputed(item.items);
3121
+ }
3122
+ } else if (item.type === "tabs" && item.tabs && Array.isArray(item.tabs)) {
3123
+ for (const tab of item.tabs) {
3124
+ if (tab.items) extractComputed(tab.items);
3125
+ }
3126
+ }
3127
+ }
3128
+ };
3129
+ extractComputed(detail);
3130
+ if (computedItems.length === 0) return doc;
3131
+ const evaluated = {};
3132
+ for (const item of computedItems) {
3133
+ try {
3134
+ if (typeof item.handler === "function") {
3135
+ evaluated[item.id] = await item.handler({ doc, user, db });
3136
+ } else if (item.expression) {
3137
+ evaluated[item.id] = await evaluateJexl(item.expression, { doc, user });
3138
+ }
3139
+ } catch (_err) {
3140
+ evaluated[item.id] = null;
3141
+ }
3142
+ }
3143
+ return {
3144
+ ...doc,
3145
+ _meta: {
3146
+ ...doc._meta || {},
3147
+ computed: evaluated
3148
+ }
3149
+ };
3150
+ }
3151
+
3152
+ // src/controllers/collection.controller.ts
3018
3153
  var CollectionController = class {
3019
3154
  collection;
3020
3155
  constructor(collection) {
@@ -3031,6 +3166,22 @@ var CollectionController = class {
3031
3166
  toHookRequestContext(c) {
3032
3167
  return toHookRequestContext(c.req);
3033
3168
  }
3169
+ sanitizeDoc(doc) {
3170
+ if (!doc || typeof doc !== "object") return doc;
3171
+ if (this.collection.auth) {
3172
+ const {
3173
+ password: _p,
3174
+ salt: _s,
3175
+ resetPasswordToken: _rpt,
3176
+ resetPasswordExpires: _rpe,
3177
+ loginAttempts: _la,
3178
+ lockUntil: _lu,
3179
+ ...safeDoc
3180
+ } = doc;
3181
+ return safeDoc;
3182
+ }
3183
+ return doc;
3184
+ }
3034
3185
  async evaluateAccess(c, action, options = {}) {
3035
3186
  const config = c.get("config");
3036
3187
  return resolveCollectionAccess(
@@ -3263,6 +3414,9 @@ var CollectionController = class {
3263
3414
  depth
3264
3415
  );
3265
3416
  }
3417
+ if (this.collection.auth && result.docs && Array.isArray(result.docs)) {
3418
+ result.docs = result.docs.map((d) => this.sanitizeDoc(d));
3419
+ }
3266
3420
  return c.json(result);
3267
3421
  }
3268
3422
  async findOne(c) {
@@ -3279,32 +3433,51 @@ var CollectionController = class {
3279
3433
  id: id2
3280
3434
  });
3281
3435
  const externalSubject = localDoc?.externalSubject || id2;
3282
- let member = null;
3283
3436
  if (provider.members.get) {
3284
- member = await provider.members.get({ externalSubject, req: hookReq });
3437
+ const member = await provider.members.get({
3438
+ externalSubject,
3439
+ req: hookReq
3440
+ });
3441
+ if (member) {
3442
+ return c.json({
3443
+ ...member,
3444
+ id: localDoc ? localDoc.id : member.id,
3445
+ externalSubject: member.id
3446
+ });
3447
+ }
3285
3448
  } else if (provider.members.list) {
3286
3449
  const listResult = await provider.members.list({ req: hookReq });
3287
- member = listResult.docs.find((m) => m.id === externalSubject) || null;
3450
+ const matched = listResult.docs.find((m) => m.id === externalSubject || m.id === id2);
3451
+ if (matched) {
3452
+ return c.json({
3453
+ ...matched,
3454
+ id: localDoc ? localDoc.id : matched.id,
3455
+ externalSubject: matched.id
3456
+ });
3457
+ }
3288
3458
  }
3289
- if (!member) return c.json({ message: "Not Found" }, 404);
3290
- return c.json({
3291
- ...member,
3292
- id: localDoc ? localDoc.id : member.id,
3293
- externalSubject: member.id
3294
- });
3295
3459
  }
3296
- const readonlyDb = createReadonlyDb(db);
3297
3460
  const id = c.req.param("id");
3298
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
3299
- const user = c.get("user");
3300
3461
  if (!id) return c.json({ message: "Missing ID" }, 400);
3301
- let doc = await db.findOne({ collection: this.collection.slug, id });
3302
- if (!doc) return c.json({ message: "Not Found" }, 404);
3462
+ const depth = Number.parseInt(c.req.query("depth") || "0", 10);
3463
+ const user = c.get("user");
3464
+ const doc = await db.findOne({
3465
+ collection: this.collection.slug,
3466
+ id
3467
+ });
3468
+ if (!doc) {
3469
+ return c.json({ message: "Document not found" }, 404);
3470
+ }
3471
+ let effectiveDoc = doc;
3303
3472
  if (this.collection.workflow) {
3304
- doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
3305
- if (!doc) return c.json({ message: "Not Found" }, 404);
3473
+ effectiveDoc = materializeWorkflowDocument(
3474
+ doc,
3475
+ this.collection.workflow,
3476
+ user
3477
+ );
3478
+ if (!effectiveDoc) return c.json({ message: "Document not found" }, 404);
3306
3479
  }
3307
- const access = await this.evaluateAccess(c, "read", { id, doc });
3480
+ const access = await this.evaluateAccess(c, "read", { id, doc: effectiveDoc });
3308
3481
  if (!access.allowed) {
3309
3482
  return c.json(
3310
3483
  {
@@ -3314,7 +3487,11 @@ var CollectionController = class {
3314
3487
  403
3315
3488
  );
3316
3489
  }
3317
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
3490
+ const docWithDefaults = DefaultsService.apply(
3491
+ this.collection.fields,
3492
+ effectiveDoc
3493
+ );
3494
+ const readonlyDb = createReadonlyDb(db);
3318
3495
  const docWithCollectionHooks = await runCollectionHooks(
3319
3496
  this.collection.hooks?.afterRead,
3320
3497
  {
@@ -3340,17 +3517,20 @@ var CollectionController = class {
3340
3517
  },
3341
3518
  docWithFieldHooks
3342
3519
  );
3520
+ let finalDoc = docWithFieldAccess;
3343
3521
  if (depth > 0 && docWithFieldAccess) {
3344
3522
  const populationService = new PopulationService(db, config.collections);
3345
- const populatedDoc = await populationService.populate({
3523
+ finalDoc = await populationService.populate({
3346
3524
  data: docWithFieldAccess,
3347
3525
  fields: this.collection.fields,
3348
3526
  currentDepth: 0,
3349
3527
  maxDepth: depth
3350
3528
  });
3351
- return c.json(populatedDoc);
3352
3529
  }
3353
- return c.json(docWithFieldAccess);
3530
+ if (finalDoc && this.collection.detail) {
3531
+ finalDoc = await evaluateDetailComputed(this.collection.detail, finalDoc, user, readonlyDb);
3532
+ }
3533
+ return c.json(this.sanitizeDoc(finalDoc));
3354
3534
  }
3355
3535
  async create(c) {
3356
3536
  const config = c.get("config");
@@ -4122,6 +4302,63 @@ var CollectionController = class {
4122
4302
  201
4123
4303
  );
4124
4304
  }
4305
+ async aggregate(c) {
4306
+ const config = c.get("config");
4307
+ const db = config.db;
4308
+ if (!db) return c.json({ message: "Database not configured" }, 500);
4309
+ const access = await this.evaluateAccess(c, "read");
4310
+ if (!access.allowed) {
4311
+ return c.json(
4312
+ {
4313
+ error: true,
4314
+ message: `Access denied: read on ${this.collection.slug}`
4315
+ },
4316
+ 403
4317
+ );
4318
+ }
4319
+ let body;
4320
+ try {
4321
+ body = await c.req.json();
4322
+ } catch {
4323
+ return c.json({ message: "Invalid JSON body" }, 400);
4324
+ }
4325
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4326
+ return c.json(
4327
+ { message: "Aggregate request body must be a JSON object" },
4328
+ 400
4329
+ );
4330
+ }
4331
+ const { sanitizeWhereClause: sanitizeWhereClause2 } = await Promise.resolve().then(() => (init_where_sanitizer(), where_sanitizer_exports));
4332
+ const sanitizedAggregates = {};
4333
+ for (const [key, op] of Object.entries(body)) {
4334
+ if (!op || typeof op !== "object" || Array.isArray(op)) {
4335
+ return c.json(
4336
+ {
4337
+ message: `Aggregate operation "${key}" must be an object`
4338
+ },
4339
+ 400
4340
+ );
4341
+ }
4342
+ const operation = op;
4343
+ let opWhere = operation.where && typeof operation.where === "object" ? operation.where : void 0;
4344
+ if (opWhere) {
4345
+ opWhere = sanitizeWhereClause2(opWhere, this.collection.fields);
4346
+ if (Object.keys(opWhere).length === 0) opWhere = void 0;
4347
+ }
4348
+ if (access.constraint) {
4349
+ opWhere = opWhere ? mergeWhereConstraint(opWhere, access.constraint) : access.constraint;
4350
+ }
4351
+ sanitizedAggregates[key] = {
4352
+ ...operation,
4353
+ ...opWhere !== void 0 ? { where: opWhere } : { where: void 0 }
4354
+ };
4355
+ }
4356
+ const result = await db.aggregate({
4357
+ collection: this.collection.slug,
4358
+ aggregates: sanitizedAggregates
4359
+ });
4360
+ return c.json(result);
4361
+ }
4125
4362
  };
4126
4363
 
4127
4364
  // src/controllers/global.controller.ts
@@ -4205,17 +4442,25 @@ var GlobalController = class {
4205
4442
  },
4206
4443
  docWithFieldHooks
4207
4444
  );
4445
+ let finalDoc = docWithFieldAccess;
4208
4446
  if (depth > 0 && docWithFieldAccess) {
4209
4447
  const populationService = new PopulationService(db, config.collections);
4210
- const populatedData = await populationService.populate({
4448
+ finalDoc = await populationService.populate({
4211
4449
  data: docWithFieldAccess,
4212
4450
  fields: this.global.fields,
4213
4451
  currentDepth: 0,
4214
4452
  maxDepth: depth
4215
4453
  });
4216
- return c.json(populatedData);
4217
4454
  }
4218
- return c.json(docWithFieldAccess);
4455
+ if (finalDoc && this.global.detail) {
4456
+ finalDoc = await evaluateDetailComputed(
4457
+ this.global.detail,
4458
+ finalDoc,
4459
+ user,
4460
+ readonlyDb
4461
+ );
4462
+ }
4463
+ return c.json(finalDoc);
4219
4464
  }
4220
4465
  async update(c) {
4221
4466
  const config = c.get("config");
@@ -5668,6 +5913,7 @@ var AdminAuthController = class {
5668
5913
  });
5669
5914
  }
5670
5915
  getProvider(config, id) {
5916
+ if (!id) return void 0;
5671
5917
  if (config.adminAuth?.mode !== "external") return void 0;
5672
5918
  return config.adminAuth.providers.find((provider) => provider.id === id);
5673
5919
  }
@@ -5866,17 +6112,16 @@ var AdminAuthController = class {
5866
6112
  throw new Error("OIDC nonce verification failed.");
5867
6113
  }
5868
6114
  claims = payload;
5869
- } else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
5870
- const userInfo = await fetch(
5871
- provider.userInfoEndpoint || discovery.userinfo_endpoint,
5872
- {
5873
- headers: { Authorization: `Bearer ${tokenBody.access_token}` }
5874
- }
5875
- );
6115
+ } else {
6116
+ const userInfoEndpoint = provider.userInfoEndpoint ?? discovery.userinfo_endpoint;
6117
+ if (!tokenBody.access_token || !userInfoEndpoint) {
6118
+ throw new Error("OIDC provider did not return usable identity claims.");
6119
+ }
6120
+ const userInfo = await fetch(userInfoEndpoint, {
6121
+ headers: { Authorization: `Bearer ${tokenBody.access_token}` }
6122
+ });
5876
6123
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
5877
6124
  claims = await userInfo.json();
5878
- } else {
5879
- throw new Error("OIDC provider did not return usable identity claims.");
5880
6125
  }
5881
6126
  return {
5882
6127
  identity: this.mapClaims(claims, provider.claimMapping),
@@ -6791,6 +7036,71 @@ function generateOpenApi(config) {
6791
7036
  responses: { 200: { description: "Deleted and failed document IDs" } }
6792
7037
  }
6793
7038
  };
7039
+ spec.paths[`${path}/aggregate`] = {
7040
+ post: {
7041
+ tags: [collectionTag],
7042
+ summary: `Aggregate ${labels.plural}`,
7043
+ description: `Compute statistical aggregations (count, sum, avg, min, max) across ${labels.plural}.`,
7044
+ requestBody: {
7045
+ required: true,
7046
+ content: {
7047
+ "application/json": {
7048
+ schema: {
7049
+ type: "object",
7050
+ additionalProperties: {
7051
+ type: "object",
7052
+ properties: {
7053
+ count: { type: "string", example: "*" },
7054
+ sum: { type: "string", description: "Field to sum" },
7055
+ avg: { type: "string", description: "Field to average" },
7056
+ min: { type: "string", description: "Field for minimum" },
7057
+ max: { type: "string", description: "Field for maximum" },
7058
+ cast: {
7059
+ type: "string",
7060
+ enum: [
7061
+ "number",
7062
+ "integer",
7063
+ "float",
7064
+ "boolean",
7065
+ "date",
7066
+ "string"
7067
+ ]
7068
+ },
7069
+ where: {
7070
+ type: "object",
7071
+ description: "Per-aggregate filter condition",
7072
+ additionalProperties: true
7073
+ }
7074
+ }
7075
+ }
7076
+ }
7077
+ }
7078
+ }
7079
+ },
7080
+ responses: {
7081
+ 200: {
7082
+ description: "Success",
7083
+ content: {
7084
+ "application/json": {
7085
+ schema: {
7086
+ type: "object",
7087
+ additionalProperties: {
7088
+ type: "number",
7089
+ nullable: true
7090
+ }
7091
+ }
7092
+ }
7093
+ }
7094
+ },
7095
+ 400: {
7096
+ description: "Bad Request - Invalid aggregate input"
7097
+ },
7098
+ 403: {
7099
+ description: "Forbidden - Access denied by read rule"
7100
+ }
7101
+ }
7102
+ }
7103
+ };
6794
7104
  if (collection.audit) {
6795
7105
  spec.paths[`${path}/__audit`] = {
6796
7106
  get: {
@@ -7387,10 +7697,7 @@ function accessGate(config, target, action) {
7387
7697
  req: toHookRequestContext(c.req)
7388
7698
  });
7389
7699
  if (!allowed) {
7390
- return c.json(
7391
- { error: true, message: `Access denied: ${action} on ${target.slug}` },
7392
- 403
7393
- );
7700
+ return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
7394
7701
  }
7395
7702
  await next();
7396
7703
  };
@@ -7430,6 +7737,80 @@ function serializeBlockForApi(block) {
7430
7737
  fields: block.fields?.map(serializeFieldForApi)
7431
7738
  };
7432
7739
  }
7740
+ function serializeDetailItemForApi(item) {
7741
+ if (!item) return item;
7742
+ if (typeof item === "string") {
7743
+ return { type: "field", field: item };
7744
+ }
7745
+ if (item.type === "section") {
7746
+ return {
7747
+ type: "section",
7748
+ title: item.title,
7749
+ items: (item.items || []).map(serializeDetailItemForApi),
7750
+ options: item.options
7751
+ };
7752
+ }
7753
+ if (item.type === "tab") {
7754
+ return {
7755
+ type: "tab",
7756
+ label: item.label,
7757
+ items: (item.items || []).map(serializeDetailItemForApi),
7758
+ options: item.options
7759
+ };
7760
+ }
7761
+ if (item.type === "tabs") {
7762
+ return {
7763
+ type: "tabs",
7764
+ tabs: (item.tabs || []).map(serializeDetailItemForApi),
7765
+ options: item.options
7766
+ };
7767
+ }
7768
+ if (item.type === "grid") {
7769
+ return {
7770
+ type: "grid",
7771
+ columns: item.columns,
7772
+ items: (item.items || []).map(serializeDetailItemForApi),
7773
+ options: item.options
7774
+ };
7775
+ }
7776
+ if (item.type === "repeat") {
7777
+ return {
7778
+ type: "repeat",
7779
+ field: item.field,
7780
+ items: (item.items || []).map(serializeDetailItemForApi),
7781
+ options: item.options
7782
+ };
7783
+ }
7784
+ if (item.type === "computed") {
7785
+ return {
7786
+ type: "computed",
7787
+ id: item.id || item.label?.toLowerCase().replace(/[^a-z0-9]+/g, "_"),
7788
+ label: item.label,
7789
+ expression: item.expression,
7790
+ options: {
7791
+ id: item.id,
7792
+ span: item.options?.span,
7793
+ format: item.options?.format,
7794
+ currency: item.options?.currency,
7795
+ visible: item.options?.visible
7796
+ }
7797
+ };
7798
+ }
7799
+ if (item.type === "field") {
7800
+ return {
7801
+ type: "field",
7802
+ field: item.field,
7803
+ options: item.options
7804
+ };
7805
+ }
7806
+ return item;
7807
+ }
7808
+ function serializeDetailForApi(detail) {
7809
+ if (detail === false) return false;
7810
+ if (detail === true) return true;
7811
+ if (!detail || !Array.isArray(detail)) return void 0;
7812
+ return detail.map(serializeDetailItemForApi);
7813
+ }
7433
7814
  function registerRoutes(app, config) {
7434
7815
  const optionsCache = /* @__PURE__ */ new Map();
7435
7816
  app.get("/api/schemas", optionalAuth(config), async (c) => {
@@ -7448,15 +7829,12 @@ function registerRoutes(app, config) {
7448
7829
  if (typeof access === "boolean") return access;
7449
7830
  if (access && typeof access === "object" && typeof access.policy === "string") {
7450
7831
  const policy = requestConfig.accessPolicies?.[access.policy];
7451
- if (typeof policy === "string" || typeof policy === "boolean")
7452
- return policy;
7832
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
7453
7833
  }
7454
7834
  return resolveBooleanAccess(requestConfig, access, accessArgs);
7455
7835
  };
7456
7836
  const filteredCollections = await Promise.all(
7457
- collections.filter(
7458
- (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
7459
- ).map(async (col) => ({
7837
+ collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
7460
7838
  slug: col.slug,
7461
7839
  labels: col.labels,
7462
7840
  access: {
@@ -7494,6 +7872,7 @@ function registerRoutes(app, config) {
7494
7872
  audit: !!col.audit,
7495
7873
  drafts: !!col.drafts,
7496
7874
  admin: col.admin,
7875
+ detail: serializeDetailForApi(col.detail),
7497
7876
  workflow: col.workflow ? {
7498
7877
  initialState: col.workflow.initialState,
7499
7878
  draftState: col.workflow.draftState,
@@ -7512,9 +7891,7 @@ function registerRoutes(app, config) {
7512
7891
  }))
7513
7892
  );
7514
7893
  const filteredGlobals = await Promise.all(
7515
- globals.filter(
7516
- (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
7517
- ).map(async (glb) => ({
7894
+ globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
7518
7895
  slug: glb.slug,
7519
7896
  label: glb.label,
7520
7897
  access: {
@@ -7544,7 +7921,8 @@ function registerRoutes(app, config) {
7544
7921
  }
7545
7922
  }))
7546
7923
  ),
7547
- admin: glb.admin
7924
+ admin: glb.admin,
7925
+ detail: serializeDetailForApi(glb.detail)
7548
7926
  }))
7549
7927
  );
7550
7928
  return c.json({
@@ -7558,148 +7936,137 @@ function registerRoutes(app, config) {
7558
7936
  adminHealth: {
7559
7937
  emailConfigured: !!requestConfig.email,
7560
7938
  secureAuthSecretConfigured: !!process.env.DYRECTED_JWT_SECRET,
7561
- authCollectionConfigured: requestConfig.collections.some(
7562
- (collection) => !!collection.auth
7563
- ),
7564
- uploadCollectionConfigured: requestConfig.collections.some(
7565
- (collection) => !!collection.upload
7566
- )
7939
+ authCollectionConfigured: requestConfig.collections.some((collection) => !!collection.auth),
7940
+ uploadCollectionConfigured: requestConfig.collections.some((collection) => !!collection.upload)
7567
7941
  }
7568
7942
  });
7569
7943
  });
7570
- app.get(
7571
- "/api/dyrected/options/:collection/:field",
7572
- optionalAuth(config),
7573
- async (c) => {
7574
- const { collection: colSlug, field: fieldName } = c.req.param();
7575
- const siteId = c.req.header("X-Site-Id");
7576
- const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
7577
- let collections = [...requestConfig.collections];
7578
- const user = c.get("user");
7579
- let collection = collections.find((col) => col.slug === colSlug);
7580
- let field;
7581
- if (collection) {
7582
- const accessExpr = collection.access?.read;
7583
- if (accessExpr !== void 0 && accessExpr !== null) {
7584
- const allowed = await resolveBooleanAccess(config, accessExpr, {
7585
- user,
7586
- req: toHookRequestContext(c.req)
7587
- });
7588
- if (!allowed) {
7589
- return c.json(
7590
- { error: true, message: `Access denied: read on ${colSlug}` },
7591
- 403
7592
- );
7593
- }
7594
- }
7595
- field = collection.fields.find((f) => f.name === fieldName);
7596
- } else {
7597
- const globals = [...requestConfig.globals];
7598
- const glb = globals.find((g) => g.slug === colSlug);
7599
- if (!glb) {
7600
- return c.json(
7601
- {
7602
- error: true,
7603
- message: `${colSlug} not found as collection or global`
7604
- },
7605
- 404
7606
- );
7607
- }
7608
- const accessExpr = glb.access?.read;
7609
- if (accessExpr !== void 0 && accessExpr !== null) {
7610
- const allowed = await resolveBooleanAccess(config, accessExpr, {
7611
- user,
7612
- req: toHookRequestContext(c.req)
7613
- });
7614
- if (!allowed) {
7615
- return c.json(
7616
- {
7617
- error: true,
7618
- message: `Access denied: read on global ${colSlug}`
7619
- },
7620
- 403
7621
- );
7622
- }
7944
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
7945
+ const { collection: colSlug, field: fieldName } = c.req.param();
7946
+ const siteId = c.req.header("X-Site-Id");
7947
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
7948
+ let collections = [...requestConfig.collections];
7949
+ const user = c.get("user");
7950
+ let collection = collections.find((col) => col.slug === colSlug);
7951
+ let field;
7952
+ if (collection) {
7953
+ const accessExpr = collection.access?.read;
7954
+ if (accessExpr !== void 0 && accessExpr !== null) {
7955
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
7956
+ user,
7957
+ req: toHookRequestContext(c.req)
7958
+ });
7959
+ if (!allowed) {
7960
+ return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
7623
7961
  }
7624
- field = glb.fields.find((f) => f.name === fieldName);
7625
7962
  }
7626
- if (!field) {
7963
+ field = collection.fields.find((f) => f.name === fieldName);
7964
+ } else {
7965
+ const globals = [...requestConfig.globals];
7966
+ const glb = globals.find((g) => g.slug === colSlug);
7967
+ if (!glb) {
7627
7968
  return c.json(
7628
7969
  {
7629
7970
  error: true,
7630
- message: `Field ${fieldName} not found in ${colSlug}`
7971
+ message: `${colSlug} not found as collection or global`
7631
7972
  },
7632
7973
  404
7633
7974
  );
7634
7975
  }
7635
- let resolver;
7636
- let cacheTTL;
7637
- if (typeof field.options === "function") {
7638
- resolver = field.options;
7639
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
7640
- resolver = field.options.resolve;
7641
- cacheTTL = field.options.cacheTTL;
7642
- }
7643
- if (!resolver) {
7644
- return c.json(
7645
- {
7646
- error: true,
7647
- message: `Field ${fieldName} in ${colSlug} is not dynamic`
7648
- },
7649
- 400
7650
- );
7651
- }
7652
- try {
7653
- const db = c.get("db") || config.db;
7654
- const queryParams = c.req.query();
7655
- const reqContext = {
7656
- query: queryParams,
7657
- headers: c.req.header(),
7658
- raw: c.req.raw
7659
- };
7660
- const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
7661
- let cacheKey = "";
7662
- if (shouldCache) {
7663
- cacheKey = JSON.stringify([
7664
- siteId ?? "",
7665
- colSlug,
7666
- fieldName,
7667
- user?.id ?? null,
7668
- queryParams
7669
- ]);
7670
- const hit = optionsCache.get(cacheKey);
7671
- if (hit && hit.expires > Date.now()) {
7672
- return c.json(hit.value);
7673
- }
7674
- }
7675
- const result = await resolver({
7676
- db,
7976
+ const accessExpr = glb.access?.read;
7977
+ if (accessExpr !== void 0 && accessExpr !== null) {
7978
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
7677
7979
  user,
7678
- req: reqContext
7980
+ req: toHookRequestContext(c.req)
7679
7981
  });
7680
- if (shouldCache) {
7681
- optionsCache.set(cacheKey, {
7682
- expires: Date.now() + cacheTTL * 1e3,
7683
- value: result
7684
- });
7982
+ if (!allowed) {
7983
+ return c.json(
7984
+ {
7985
+ error: true,
7986
+ message: `Access denied: read on global ${colSlug}`
7987
+ },
7988
+ 403
7989
+ );
7685
7990
  }
7686
- return c.json(result);
7687
- } catch (err) {
7688
- getRequestLogger(c, "router").error({
7689
- err,
7690
- msg: "Failed to resolve dynamic field options",
7691
- fieldName
7991
+ }
7992
+ field = glb.fields.find((f) => f.name === fieldName);
7993
+ }
7994
+ if (!field) {
7995
+ return c.json(
7996
+ {
7997
+ error: true,
7998
+ message: `Field ${fieldName} not found in ${colSlug}`
7999
+ },
8000
+ 404
8001
+ );
8002
+ }
8003
+ let resolver;
8004
+ let cacheTTL;
8005
+ if (typeof field.options === "function") {
8006
+ resolver = field.options;
8007
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
8008
+ resolver = field.options.resolve;
8009
+ cacheTTL = field.options.cacheTTL;
8010
+ }
8011
+ if (!resolver) {
8012
+ return c.json(
8013
+ {
8014
+ error: true,
8015
+ message: `Field ${fieldName} in ${colSlug} is not dynamic`
8016
+ },
8017
+ 400
8018
+ );
8019
+ }
8020
+ try {
8021
+ const db = c.get("db") || config.db;
8022
+ const queryParams = c.req.query();
8023
+ const reqContext = {
8024
+ query: queryParams,
8025
+ headers: c.req.header(),
8026
+ raw: c.req.raw
8027
+ };
8028
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
8029
+ let cacheKey = "";
8030
+ if (shouldCache) {
8031
+ cacheKey = JSON.stringify([
8032
+ siteId ?? "",
8033
+ colSlug,
8034
+ fieldName,
8035
+ user?.id ?? null,
8036
+ queryParams
8037
+ ]);
8038
+ const hit = optionsCache.get(cacheKey);
8039
+ if (hit && hit.expires > Date.now()) {
8040
+ return c.json(hit.value);
8041
+ }
8042
+ }
8043
+ const result = await resolver({
8044
+ db,
8045
+ user,
8046
+ req: reqContext
8047
+ });
8048
+ if (shouldCache) {
8049
+ optionsCache.set(cacheKey, {
8050
+ expires: Date.now() + cacheTTL * 1e3,
8051
+ value: result
7692
8052
  });
7693
- return c.json(
7694
- {
7695
- error: true,
7696
- message: err.message || "Failed to resolve dynamic options"
7697
- },
7698
- 500
7699
- );
7700
8053
  }
8054
+ return c.json(result);
8055
+ } catch (err) {
8056
+ getRequestLogger(c, "router").error({
8057
+ err,
8058
+ msg: "Failed to resolve dynamic field options",
8059
+ fieldName
8060
+ });
8061
+ return c.json(
8062
+ {
8063
+ error: true,
8064
+ message: err.message || "Failed to resolve dynamic options"
8065
+ },
8066
+ 500
8067
+ );
7701
8068
  }
7702
- );
8069
+ });
7703
8070
  app.get("/api/openapi.json", (c) => {
7704
8071
  return c.json(generateOpenApi(config));
7705
8072
  });
@@ -7712,13 +8079,8 @@ function registerRoutes(app, config) {
7712
8079
  const key = c.req.param("key");
7713
8080
  const scope = c.req.query("scope");
7714
8081
  if (!db) return c.json({ message: "Database not configured" }, 500);
7715
- if (!user?.collection || !user.sub)
7716
- return c.json({ error: true, message: "Authentication required." }, 401);
7717
- if (!key)
7718
- return c.json(
7719
- { error: true, message: "Preference key is required." },
7720
- 400
7721
- );
8082
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8083
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7722
8084
  const getGlobalPreference = async () => {
7723
8085
  const globalDoc = await db.findOne({
7724
8086
  collection: "__global_preferences",
@@ -7745,13 +8107,8 @@ function registerRoutes(app, config) {
7745
8107
  const key = c.req.param("key");
7746
8108
  const scope = c.req.query("scope");
7747
8109
  if (!db) return c.json({ message: "Database not configured" }, 500);
7748
- if (!user?.collection || !user.sub)
7749
- return c.json({ error: true, message: "Authentication required." }, 401);
7750
- if (!key)
7751
- return c.json(
7752
- { error: true, message: "Preference key is required." },
7753
- 400
7754
- );
8110
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8111
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7755
8112
  const body = await c.req.json().catch(() => ({}));
7756
8113
  if (scope === "global") {
7757
8114
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
@@ -7799,13 +8156,8 @@ function registerRoutes(app, config) {
7799
8156
  const key = c.req.param("key");
7800
8157
  const scope = c.req.query("scope");
7801
8158
  if (!db) return c.json({ message: "Database not configured" }, 500);
7802
- if (!user?.collection || !user.sub)
7803
- return c.json({ error: true, message: "Authentication required." }, 401);
7804
- if (!key)
7805
- return c.json(
7806
- { error: true, message: "Preference key is required." },
7807
- 400
7808
- );
8159
+ if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
8160
+ if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
7809
8161
  if (scope === "global") {
7810
8162
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
7811
8163
  if (!isAdminUser) {
@@ -7844,41 +8196,17 @@ function registerRoutes(app, config) {
7844
8196
  for (const col of uploadCollections) {
7845
8197
  const mediaController = new MediaController(col.slug);
7846
8198
  const prefix = `/api/collections/${col.slug}`;
7847
- app.get(
7848
- `${prefix}/media`,
7849
- accessGate(config, col, "read"),
7850
- (c) => mediaController.find(c)
7851
- );
7852
- app.get(
7853
- `${prefix}/media/:filename{.+$}`,
7854
- (c) => mediaController.serve(c)
7855
- );
7856
- app.post(
7857
- `${prefix}/media`,
7858
- accessGate(config, col, "create"),
7859
- (c) => mediaController.upload(c)
7860
- );
7861
- app.delete(
7862
- `${prefix}/media/:id`,
7863
- accessGate(config, col, "delete"),
7864
- (c) => mediaController.delete(c)
7865
- );
8199
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
8200
+ app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
8201
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
8202
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
7866
8203
  }
7867
8204
  }
7868
8205
  const adminAuthController = new AdminAuthController(config);
7869
8206
  app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
7870
- app.get(
7871
- "/api/admin/auth/:provider/start",
7872
- (c) => adminAuthController.start(c)
7873
- );
7874
- app.get(
7875
- "/api/admin/auth/:provider/callback",
7876
- (c) => adminAuthController.callback(c)
7877
- );
7878
- app.post(
7879
- "/api/admin/auth/:provider/exchange",
7880
- (c) => adminAuthController.exchange(c)
7881
- );
8207
+ app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
8208
+ app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
8209
+ app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
7882
8210
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
7883
8211
  for (const collection of config.collections) {
7884
8212
  if (!collection.auth) continue;
@@ -7889,21 +8217,10 @@ function registerRoutes(app, config) {
7889
8217
  app.get(`${path}/init`, (c) => authController.init(c));
7890
8218
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
7891
8219
  app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
7892
- app.post(
7893
- `${path}/refresh-token`,
7894
- requireAuth(config),
7895
- (c) => authController.refreshToken(c)
7896
- );
7897
- app.post(
7898
- `${path}/forgot-password`,
7899
- (c) => authController.forgotPassword(c)
7900
- );
8220
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
8221
+ app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
7901
8222
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
7902
- app.post(
7903
- `${path}/invite`,
7904
- requireAuth(config),
7905
- (c) => authController.invite(c)
7906
- );
8223
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
7907
8224
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
7908
8225
  }
7909
8226
  const auditController = new AuditController();
@@ -7914,33 +8231,19 @@ function registerRoutes(app, config) {
7914
8231
  app.post(path, (c) => controller.create(c));
7915
8232
  app.post(`${path}/media`, (c) => controller.create(c));
7916
8233
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
8234
+ app.post(`${path}/aggregate`, (c) => controller.aggregate(c));
7917
8235
  if (collection.audit) {
7918
- app.get(
7919
- `${path}/__audit`,
7920
- (c) => auditController.findForCollection(c, collection)
7921
- );
8236
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
7922
8237
  }
7923
8238
  app.get(`${path}/:id`, (c) => controller.findOne(c));
7924
8239
  app.patch(`${path}/:id`, (c) => controller.update(c));
7925
8240
  app.delete(`${path}/:id`, (c) => controller.delete(c));
7926
8241
  if (collection.auth) {
7927
- app.post(
7928
- `${path}/:id/change-password`,
7929
- requireAuth(config),
7930
- (c) => controller.changePassword(c)
7931
- );
8242
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
7932
8243
  }
7933
8244
  if (collection.workflow) {
7934
- app.post(
7935
- `${path}/:id/transitions/:transition`,
7936
- requireAuth(config),
7937
- (c) => controller.transition(c)
7938
- );
7939
- app.get(
7940
- `${path}/:id/workflow-history`,
7941
- requireAuth(config),
7942
- (c) => controller.workflowHistory(c)
7943
- );
8245
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
8246
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
7944
8247
  }
7945
8248
  }
7946
8249
  for (const global of config.globals) {
@@ -7955,11 +8258,7 @@ function registerRoutes(app, config) {
7955
8258
  });
7956
8259
  }
7957
8260
  const previewController = new PreviewController();
7958
- app.post(
7959
- "/api/preview-token",
7960
- requireAuth(config),
7961
- (c) => previewController.createToken(c)
7962
- );
8261
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
7963
8262
  app.get("/api/preview-data", (c) => previewController.getData(c));
7964
8263
  app.get("/api/audit", (c) => auditController.findAll(c));
7965
8264
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -7972,81 +8271,49 @@ function registerRoutes(app, config) {
7972
8271
  if (!config2.onSchemaFetch || !siteId) {
7973
8272
  return c.json({ message: `Collection "${slug}" not found` }, 404);
7974
8273
  }
7975
- const requestConfig = mergeDynamicConfig(
7976
- config2,
7977
- await config2.onSchemaFetch(siteId)
7978
- );
7979
- const collection = requestConfig.collections.find(
7980
- (col) => col.slug === slug
7981
- );
8274
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8275
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
7982
8276
  if (!collection?.audit) {
7983
- return c.json(
7984
- { message: `Collection "${slug}" not found or has no audit log` },
7985
- 404
7986
- );
8277
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
7987
8278
  }
7988
8279
  return auditController.findForCollection(c, collection);
7989
8280
  });
7990
- app.post(
7991
- "/api/collections/:slug/:id/transitions/:transition",
7992
- requireAuth(config),
7993
- async (c) => {
7994
- const slug = c.req.param("slug");
7995
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
7996
- const config2 = c.get("config");
7997
- if (config2.collections.some((col) => col.slug === slug)) {
7998
- return c.json({ message: "Not Found" }, 404);
7999
- }
8000
- if (!config2.onSchemaFetch || !siteId) {
8001
- return c.json({ message: `Collection "${slug}" not found` }, 404);
8002
- }
8003
- const requestConfig = mergeDynamicConfig(
8004
- config2,
8005
- await config2.onSchemaFetch(siteId)
8006
- );
8007
- const collection = requestConfig.collections.find(
8008
- (col) => col.slug === slug
8009
- );
8010
- if (!collection?.workflow) {
8011
- return c.json(
8012
- { message: `Collection "${slug}" not found or has no workflow` },
8013
- 404
8014
- );
8015
- }
8016
- const controller = new CollectionController(collection);
8017
- return controller.transition(c);
8281
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
8282
+ const slug = c.req.param("slug");
8283
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8284
+ const config2 = c.get("config");
8285
+ if (config2.collections.some((col) => col.slug === slug)) {
8286
+ return c.json({ message: "Not Found" }, 404);
8018
8287
  }
8019
- );
8020
- app.get(
8021
- "/api/collections/:slug/:id/workflow-history",
8022
- requireAuth(config),
8023
- async (c) => {
8024
- const slug = c.req.param("slug");
8025
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8026
- const config2 = c.get("config");
8027
- if (config2.collections.some((col) => col.slug === slug)) {
8028
- return c.json({ message: "Not Found" }, 404);
8029
- }
8030
- if (!config2.onSchemaFetch || !siteId) {
8031
- return c.json({ message: `Collection "${slug}" not found` }, 404);
8032
- }
8033
- const requestConfig = mergeDynamicConfig(
8034
- config2,
8035
- await config2.onSchemaFetch(siteId)
8036
- );
8037
- const collection = requestConfig.collections.find(
8038
- (col) => col.slug === slug
8039
- );
8040
- if (!collection?.workflow) {
8041
- return c.json(
8042
- { message: `Collection "${slug}" not found or has no workflow` },
8043
- 404
8044
- );
8045
- }
8046
- const controller = new CollectionController(collection);
8047
- return controller.workflowHistory(c);
8288
+ if (!config2.onSchemaFetch || !siteId) {
8289
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
8048
8290
  }
8049
- );
8291
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8292
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
8293
+ if (!collection?.workflow) {
8294
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
8295
+ }
8296
+ const controller = new CollectionController(collection);
8297
+ return controller.transition(c);
8298
+ });
8299
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
8300
+ const slug = c.req.param("slug");
8301
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
8302
+ const config2 = c.get("config");
8303
+ if (config2.collections.some((col) => col.slug === slug)) {
8304
+ return c.json({ message: "Not Found" }, 404);
8305
+ }
8306
+ if (!config2.onSchemaFetch || !siteId) {
8307
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
8308
+ }
8309
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8310
+ const collection = requestConfig.collections.find((col) => col.slug === slug);
8311
+ if (!collection?.workflow) {
8312
+ return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
8313
+ }
8314
+ const controller = new CollectionController(collection);
8315
+ return controller.workflowHistory(c);
8316
+ });
8050
8317
  app.all("/api/collections/:slug/:id?", async (c) => {
8051
8318
  const slug = c.req.param("slug");
8052
8319
  const id = c.req.param("id");
@@ -8056,13 +8323,8 @@ function registerRoutes(app, config) {
8056
8323
  return c.json({ message: "Method Not Allowed" }, 405);
8057
8324
  }
8058
8325
  if (config2.onSchemaFetch && siteId) {
8059
- const requestConfig = mergeDynamicConfig(
8060
- config2,
8061
- await config2.onSchemaFetch(siteId)
8062
- );
8063
- let collection = requestConfig.collections.find(
8064
- (col) => col.slug === slug
8065
- );
8326
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8327
+ let collection = requestConfig.collections.find((col) => col.slug === slug);
8066
8328
  if (!collection && slug === "media") {
8067
8329
  collection = {
8068
8330
  slug: "media",
@@ -8075,25 +8337,19 @@ function registerRoutes(app, config) {
8075
8337
  if (collection.auth && id) {
8076
8338
  const authController = new AuthController(collection);
8077
8339
  const method2 = c.req.method;
8078
- if (method2 === "POST" && id === "login")
8079
- return authController.login(c);
8080
- if (method2 === "POST" && id === "logout")
8081
- return authController.logout(c);
8340
+ if (method2 === "POST" && id === "login") return authController.login(c);
8341
+ if (method2 === "POST" && id === "logout") return authController.logout(c);
8082
8342
  if (method2 === "GET" && id === "me") return authController.me(c);
8083
- if (method2 === "POST" && id === "refresh-token")
8084
- return authController.refreshToken(c);
8085
- if (method2 === "POST" && id === "forgot-password")
8086
- return authController.forgotPassword(c);
8087
- if (method2 === "POST" && id === "reset-password")
8088
- return authController.resetPassword(c);
8343
+ if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
8344
+ if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
8345
+ if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
8089
8346
  }
8090
8347
  const controller = new CollectionController(collection);
8091
8348
  const method = c.req.method;
8092
8349
  if (id) {
8093
8350
  if (method === "GET") return controller.findOne(c);
8094
8351
  if (method === "PATCH") return controller.update(c);
8095
- if (method === "DELETE" && id === "delete-many")
8096
- return controller.deleteMany(c);
8352
+ if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
8097
8353
  if (method === "DELETE") return controller.delete(c);
8098
8354
  if (method === "POST" && id === "media") return controller.create(c);
8099
8355
  } else {
@@ -8113,10 +8369,7 @@ function registerRoutes(app, config) {
8113
8369
  return c.json({ message: "Method Not Allowed" }, 405);
8114
8370
  }
8115
8371
  if (config2.onSchemaFetch && siteId) {
8116
- const requestConfig = mergeDynamicConfig(
8117
- config2,
8118
- await config2.onSchemaFetch(siteId)
8119
- );
8372
+ const requestConfig = mergeDynamicConfig(config2, await config2.onSchemaFetch(siteId));
8120
8373
  const global = requestConfig.globals.find((glb) => glb.slug === slug);
8121
8374
  if (global) {
8122
8375
  const controller = new GlobalController(global);
@@ -8482,7 +8735,7 @@ function resolveClientIp(req, trustProxy = false) {
8482
8735
  const value = normalizeIp(req.headers[header]);
8483
8736
  if (value) return value;
8484
8737
  }
8485
- if (trustProxy) {
8738
+ if (trustProxy === true || typeof trustProxy === "number") {
8486
8739
  const forwardedFor = pickForwardedFor(req.headers, trustProxy);
8487
8740
  if (forwardedFor) return forwardedFor;
8488
8741
  }
@@ -8494,6 +8747,7 @@ function pickForwardedFor(headers, trustProxy) {
8494
8747
  const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
8495
8748
  if (chain2.length > 0) {
8496
8749
  if (trustProxy === true) return chain2[0];
8750
+ if (trustProxy <= 0) return null;
8497
8751
  const trustedHops2 = Math.max(1, Math.floor(trustProxy));
8498
8752
  const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
8499
8753
  return chain2[candidateIndex2] ?? chain2[0];
@@ -8506,6 +8760,7 @@ function pickForwardedFor(headers, trustProxy) {
8506
8760
  const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
8507
8761
  if (chain.length === 0) return null;
8508
8762
  if (trustProxy === true) return chain[0];
8763
+ if (trustProxy <= 0) return null;
8509
8764
  const trustedHops = Math.max(1, Math.floor(trustProxy));
8510
8765
  const candidateIndex = Math.max(0, chain.length - trustedHops);
8511
8766
  return chain[candidateIndex] ?? chain[0];
@@ -8710,7 +8965,7 @@ async function createDyrectedApp(rawConfig) {
8710
8965
  app.use("*", (0, import_cors.cors)(config.cors ? { origin: config.cors.origins } : {}));
8711
8966
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
8712
8967
  if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
8713
- app.get(observability.config.metrics.path, async (c) => {
8968
+ app.get(observability.config.metrics.path, async (_c) => {
8714
8969
  const metrics = await renderPrometheusMetrics(
8715
8970
  observability.prometheusExporter
8716
8971
  );
@@ -8770,5 +9025,6 @@ init_observability();
8770
9025
  registerRoutes,
8771
9026
  renderPrometheusMetrics,
8772
9027
  resolveObservabilityConfig,
9028
+ serializeDetailForApi,
8773
9029
  shouldSampleRequest
8774
9030
  });