@dyrected/core 2.5.60 → 2.5.62

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 (48) hide show
  1. package/dist/app-config-9hs8DRED.d.cts +1718 -0
  2. package/dist/app-config-9hs8DRED.d.ts +1718 -0
  3. package/dist/app-config-BcuZIpvL.d.cts +1951 -0
  4. package/dist/app-config-BcuZIpvL.d.ts +1951 -0
  5. package/dist/app-config-Bm9-2Am6.d.cts +1875 -0
  6. package/dist/app-config-Bm9-2Am6.d.ts +1875 -0
  7. package/dist/app-config-C0uxEU7Q.d.cts +1874 -0
  8. package/dist/app-config-C0uxEU7Q.d.ts +1874 -0
  9. package/dist/app-config-CBOn8IyZ.d.cts +1838 -0
  10. package/dist/app-config-CBOn8IyZ.d.ts +1838 -0
  11. package/dist/app-config-CJAGGPrk.d.cts +1900 -0
  12. package/dist/app-config-CJAGGPrk.d.ts +1900 -0
  13. package/dist/app-config-CiEDJm0e.d.cts +1912 -0
  14. package/dist/app-config-CiEDJm0e.d.ts +1912 -0
  15. package/dist/app-config-CwaU1de2.d.cts +1868 -0
  16. package/dist/app-config-CwaU1de2.d.ts +1868 -0
  17. package/dist/app-config-DCDh8Gbx.d.cts +1926 -0
  18. package/dist/app-config-DCDh8Gbx.d.ts +1926 -0
  19. package/dist/app-config-DVdSospO.d.cts +1842 -0
  20. package/dist/app-config-DVdSospO.d.ts +1842 -0
  21. package/dist/app-config-Dv5XACR4.d.cts +2065 -0
  22. package/dist/app-config-Dv5XACR4.d.ts +2065 -0
  23. package/dist/app-config-_kkj71CB.d.cts +2010 -0
  24. package/dist/app-config-_kkj71CB.d.ts +2010 -0
  25. package/dist/app-config-ouBRb6Bf.d.cts +1716 -0
  26. package/dist/app-config-ouBRb6Bf.d.ts +1716 -0
  27. package/dist/app-config-tITj_0sn.d.cts +1926 -0
  28. package/dist/app-config-tITj_0sn.d.ts +1926 -0
  29. package/dist/chunk-35NM2WPO.js +1658 -0
  30. package/dist/chunk-57FNM42D.js +2392 -0
  31. package/dist/chunk-BAMX7YUC.js +1815 -0
  32. package/dist/chunk-BQV3QW3Y.js +2588 -0
  33. package/dist/chunk-CUOPCOU2.js +1644 -0
  34. package/dist/chunk-EH3MJGB5.js +2571 -0
  35. package/dist/chunk-M3HKRN7E.js +1665 -0
  36. package/dist/chunk-MQZH7RQC.js +1667 -0
  37. package/dist/chunk-VCYYBN5J.js +1873 -0
  38. package/dist/chunk-WVD7PORQ.js +1672 -0
  39. package/dist/chunk-XZLIBQSO.js +2397 -0
  40. package/dist/index.cjs +404 -44
  41. package/dist/index.d.cts +77 -5
  42. package/dist/index.d.ts +77 -5
  43. package/dist/index.js +37 -4
  44. package/dist/server.cjs +2973 -708
  45. package/dist/server.d.cts +124 -12
  46. package/dist/server.d.ts +124 -12
  47. package/dist/server.js +2066 -695
  48. package/package.json +10 -1
package/dist/server.js CHANGED
@@ -1,19 +1,41 @@
1
1
  import {
2
2
  WORKFLOW_HISTORY_COLLECTION,
3
+ attachRequestMetrics,
4
+ bindObservabilityRuntime,
5
+ buildRequestLogger,
3
6
  canViewWorkflowDraft,
7
+ captureRequestBody,
8
+ createObservabilityRuntime,
9
+ createRequestSpan,
4
10
  createWorkflowDocument,
11
+ endRequestSpan,
5
12
  executeFieldAfterRead,
6
13
  executeFieldBeforeChange,
7
14
  generateOpenApi,
8
15
  getAdminAuthCollection,
16
+ getAuthSession,
17
+ getConfigLogger,
18
+ getObservabilityRuntime,
9
19
  getPublicAdminAuthConfig,
20
+ getRequestLogger,
10
21
  initializeWorkflowDocument,
22
+ isAuthSessionActive,
23
+ issueAuthSessionToken,
11
24
  materializeWorkflowDocument,
25
+ mergeDynamicConfig,
12
26
  normalizeConfig,
27
+ redactHeaders,
28
+ renderPrometheusMetrics,
29
+ revokeAllAuthSessions,
30
+ revokeAuthSession,
13
31
  runCollectionHooks,
14
32
  saveWorkflowDraft,
15
- transitionWorkflow
16
- } from "./chunk-2WODKOUB.js";
33
+ shouldSampleRequest,
34
+ signCollectionToken,
35
+ touchAuthSession,
36
+ transitionWorkflow,
37
+ verifyCollectionToken
38
+ } from "./chunk-BQV3QW3Y.js";
17
39
 
18
40
  // src/app.ts
19
41
  import { Hono } from "hono";
@@ -81,12 +103,16 @@ var PopulationService = class {
81
103
  return data;
82
104
  }
83
105
  if (Array.isArray(data)) {
84
- return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
106
+ return Promise.all(
107
+ data.map((item) => this.populate({ ...args, data: item }))
108
+ );
85
109
  }
86
110
  const populatedDoc = { ...data };
87
111
  for (const field of fields) {
88
112
  if (field.type === "join" && field.collection && field.on && currentDepth === 0) {
89
- const targetCollection = this.collections.find((c) => c.slug === field.collection);
113
+ const targetCollection = this.collections.find(
114
+ (c) => c.slug === field.collection
115
+ );
90
116
  if (targetCollection) {
91
117
  const docId = populatedDoc.id;
92
118
  if (docId) {
@@ -98,7 +124,9 @@ var PopulationService = class {
98
124
  limit: joinLimit
99
125
  });
100
126
  const populatedDocs = await this.populate({
101
- data: result.docs.map((doc) => DefaultsService.apply(targetCollection.fields, doc)),
127
+ data: result.docs.map(
128
+ (doc) => DefaultsService.apply(targetCollection.fields, doc)
129
+ ),
102
130
  fields: targetCollection.fields,
103
131
  currentDepth: 1,
104
132
  maxDepth
@@ -113,14 +141,21 @@ var PopulationService = class {
113
141
  continue;
114
142
  }
115
143
  if (field.type === "row" && field.fields) {
116
- const rowPopulated = await this.populate({ data, fields: field.fields, currentDepth, maxDepth });
144
+ const rowPopulated = await this.populate({
145
+ data,
146
+ fields: field.fields,
147
+ currentDepth,
148
+ maxDepth
149
+ });
117
150
  Object.assign(populatedDoc, rowPopulated);
118
151
  continue;
119
152
  }
120
153
  if (!field.name) continue;
121
154
  const value = populatedDoc[field.name];
122
155
  if (field.type === "relationship" && field.relationTo && value) {
123
- const relatedCollection = this.collections.find((c) => c.slug === field.relationTo);
156
+ const relatedCollection = this.collections.find(
157
+ (c) => c.slug === field.relationTo
158
+ );
124
159
  if (!relatedCollection) continue;
125
160
  if (Array.isArray(value)) {
126
161
  populatedDoc[field.name] = await Promise.all(
@@ -128,10 +163,16 @@ var PopulationService = class {
128
163
  if (!id) return id;
129
164
  let doc = id;
130
165
  if (typeof id === "string") {
131
- doc = await this.db.findOne({ collection: field.relationTo, id });
166
+ doc = await this.db.findOne({
167
+ collection: field.relationTo,
168
+ id
169
+ });
132
170
  }
133
171
  if (!doc || typeof doc !== "object") return id;
134
- const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
172
+ const docWithDefaults = DefaultsService.apply(
173
+ relatedCollection.fields,
174
+ doc
175
+ );
135
176
  return this.populate({
136
177
  data: docWithDefaults,
137
178
  fields: relatedCollection.fields,
@@ -143,10 +184,16 @@ var PopulationService = class {
143
184
  } else if (value) {
144
185
  let doc = value;
145
186
  if (typeof value === "string") {
146
- doc = await this.db.findOne({ collection: field.relationTo, id: value });
187
+ doc = await this.db.findOne({
188
+ collection: field.relationTo,
189
+ id: value
190
+ });
147
191
  }
148
192
  if (doc && typeof doc === "object") {
149
- const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
193
+ const docWithDefaults = DefaultsService.apply(
194
+ relatedCollection.fields,
195
+ doc
196
+ );
150
197
  populatedDoc[field.name] = await this.populate({
151
198
  data: docWithDefaults,
152
199
  fields: relatedCollection.fields,
@@ -157,11 +204,19 @@ var PopulationService = class {
157
204
  }
158
205
  }
159
206
  if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
160
- const relatedCollection = this.collections.find((c) => c.slug === value.relationTo);
207
+ const relatedCollection = this.collections.find(
208
+ (c) => c.slug === value.relationTo
209
+ );
161
210
  if (relatedCollection) {
162
- const doc = await this.db.findOne({ collection: value.relationTo, id: value.value });
211
+ const doc = await this.db.findOne({
212
+ collection: value.relationTo,
213
+ id: value.value
214
+ });
163
215
  if (doc && typeof doc === "object") {
164
- const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
216
+ const docWithDefaults = DefaultsService.apply(
217
+ relatedCollection.fields,
218
+ doc
219
+ );
165
220
  const populatedDocValue = await this.populate({
166
221
  data: docWithDefaults,
167
222
  fields: relatedCollection.fields,
@@ -179,15 +234,27 @@ var PopulationService = class {
179
234
  }
180
235
  } else if (typeof previewUrlPattern === "string") {
181
236
  if (previewUrlPattern.includes("{{")) {
182
- resolvedUrl = previewUrlPattern.replace(/{{(.*?)}}/g, (_, key) => String(docWithDefaults[key.trim()] || ""));
237
+ resolvedUrl = previewUrlPattern.replace(
238
+ /{{(.*?)}}/g,
239
+ (_, key) => String(docWithDefaults[key.trim()] || "")
240
+ );
183
241
  } else if (previewUrlPattern.includes("+") || previewUrlPattern.includes("?") || previewUrlPattern.includes("==") || previewUrlPattern.includes("siteUrl")) {
184
242
  try {
185
- resolvedUrl = jexl.evalSync(previewUrlPattern, docWithDefaults);
186
- } catch (err) {
187
- resolvedUrl = previewUrlPattern.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => String(docWithDefaults[key] || ""));
243
+ resolvedUrl = jexl.evalSync(
244
+ previewUrlPattern,
245
+ docWithDefaults
246
+ );
247
+ } catch (_err) {
248
+ resolvedUrl = previewUrlPattern.replace(
249
+ /:([a-zA-Z0-9_]+)/g,
250
+ (_, key) => String(docWithDefaults[key] || "")
251
+ );
188
252
  }
189
253
  } else {
190
- resolvedUrl = previewUrlPattern.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => String(docWithDefaults[key] || ""));
254
+ resolvedUrl = previewUrlPattern.replace(
255
+ /:([a-zA-Z0-9_]+)/g,
256
+ (_, key) => String(docWithDefaults[key] || "")
257
+ );
191
258
  }
192
259
  }
193
260
  }
@@ -211,7 +278,9 @@ var PopulationService = class {
211
278
  if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
212
279
  populatedDoc[field.name] = await Promise.all(
213
280
  value.map(async (blockData) => {
214
- const blockConfig = field.blocks.find((b) => b.slug === blockData.blockType);
281
+ const blockConfig = field.blocks.find(
282
+ (b) => b.slug === blockData.blockType
283
+ );
215
284
  if (!blockConfig) return blockData;
216
285
  return this.populate({
217
286
  data: blockData,
@@ -249,7 +318,7 @@ var AuditService = class {
249
318
  * Writes a single entry to the __audit collection.
250
319
  * Called without await — runs asynchronously and never blocks the primary operation.
251
320
  */
252
- static async log(db, args) {
321
+ static async log(db, args, config) {
253
322
  try {
254
323
  await db.create({
255
324
  collection: "__audit",
@@ -266,7 +335,17 @@ var AuditService = class {
266
335
  }
267
336
  });
268
337
  } catch (err) {
269
- console.error("[dyrected/audit] Failed to write audit log:", err);
338
+ getObservabilityRuntime(config)?.recordAuditWriteFailure({
339
+ collection: args.collection,
340
+ operation: args.operation
341
+ });
342
+ getConfigLogger(config, "audit").error({
343
+ err,
344
+ msg: "Failed to write audit log",
345
+ collection: args.collection,
346
+ operation: args.operation,
347
+ documentId: args.documentId
348
+ });
270
349
  }
271
350
  }
272
351
  };
@@ -351,7 +430,11 @@ async function evaluateAccess(expression, context) {
351
430
  }
352
431
  return !!result;
353
432
  } catch (err) {
354
- console.error("[dyrected/core] Jexl evaluation failed:", err);
433
+ getConfigLogger(context.config, "access").error({
434
+ err,
435
+ msg: "Jexl evaluation failed",
436
+ expression
437
+ });
355
438
  return false;
356
439
  }
357
440
  }
@@ -363,30 +446,40 @@ function isNamedAccessPolicy(value) {
363
446
  async function resolveAccess(config, access, args) {
364
447
  if (access === void 0 || access === null) return void 0;
365
448
  if (typeof access === "boolean" || typeof access === "string") {
366
- return evaluateAccess(access, args);
449
+ return evaluateAccess(access, { ...args, config });
367
450
  }
368
451
  if (typeof access === "function") {
369
452
  try {
370
453
  return await access(args);
371
454
  } catch (err) {
372
- console.error("[dyrected/core] Functional access check failed:", err);
455
+ getConfigLogger(config, "access").error({
456
+ err,
457
+ msg: "Functional access check failed"
458
+ });
373
459
  return false;
374
460
  }
375
461
  }
376
462
  if (isNamedAccessPolicy(access)) {
377
463
  const policy = config.accessPolicies?.[access.policy];
378
464
  if (policy === void 0) {
379
- console.error(`[dyrected/core] Unknown access policy "${access.policy}".`);
465
+ getConfigLogger(config, "access").error({
466
+ msg: "Unknown access policy",
467
+ policy: access.policy
468
+ });
380
469
  return false;
381
470
  }
382
471
  if (typeof policy === "string" || typeof policy === "boolean") {
383
- return evaluateAccess(policy, args);
472
+ return evaluateAccess(policy, { ...args, config });
384
473
  }
385
474
  try {
386
475
  const resolver = policy;
387
476
  return await resolver({ ...args, params: access.params });
388
477
  } catch (err) {
389
- console.error(`[dyrected/core] Access policy "${access.policy}" failed:`, err);
478
+ getConfigLogger(config, "access").error({
479
+ err,
480
+ msg: "Access policy failed",
481
+ policy: access.policy
482
+ });
390
483
  return false;
391
484
  }
392
485
  }
@@ -438,32 +531,48 @@ async function applyFieldReadAccess(context, doc) {
438
531
  const result = { ...doc };
439
532
  for (const field of context.fields) {
440
533
  if (field.type === "row" && field.fields) {
441
- Object.assign(result, await applyFieldReadAccess({ ...context, fields: field.fields }, result));
534
+ Object.assign(
535
+ result,
536
+ await applyFieldReadAccess(
537
+ { ...context, fields: field.fields },
538
+ result
539
+ )
540
+ );
442
541
  continue;
443
542
  }
444
543
  if (!field.name) continue;
445
544
  const value = result[field.name];
446
- const canRead = await resolveBooleanAccess(context.config, field.access?.read, {
447
- user: context.user,
448
- req: context.req,
449
- doc: context.doc,
450
- data: context.data,
451
- id: resolveFieldAccessId(context)
452
- });
545
+ const canRead = await resolveBooleanAccess(
546
+ context.config,
547
+ field.access?.read,
548
+ {
549
+ user: context.user,
550
+ req: context.req,
551
+ doc: context.doc,
552
+ data: context.data,
553
+ id: resolveFieldAccessId(context)
554
+ }
555
+ );
453
556
  if (!canRead) {
454
557
  delete result[field.name];
455
558
  continue;
456
559
  }
457
560
  if (value === void 0 || value === null) continue;
458
561
  if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
459
- result[field.name] = await applyFieldReadAccess({ ...context, fields: field.fields }, value);
562
+ result[field.name] = await applyFieldReadAccess(
563
+ { ...context, fields: field.fields },
564
+ value
565
+ );
460
566
  continue;
461
567
  }
462
568
  if (field.type === "array" && field.fields && Array.isArray(value)) {
463
569
  const childFields = field.fields;
464
570
  result[field.name] = await Promise.all(
465
571
  value.map(
466
- (item) => typeof item === "object" && item !== null ? applyFieldReadAccess({ ...context, fields: childFields }, item) : item
572
+ (item) => typeof item === "object" && item !== null ? applyFieldReadAccess(
573
+ { ...context, fields: childFields },
574
+ item
575
+ ) : item
467
576
  )
468
577
  );
469
578
  continue;
@@ -473,9 +582,14 @@ async function applyFieldReadAccess(context, doc) {
473
582
  value.map(async (item) => {
474
583
  if (typeof item !== "object" || item === null) return item;
475
584
  const typedItem = item;
476
- const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
585
+ const block = field.blocks?.find(
586
+ (candidate) => candidate.slug === typedItem.blockType
587
+ );
477
588
  if (!block || !block.fields) return item;
478
- return applyFieldReadAccess({ ...context, fields: block.fields }, typedItem);
589
+ return applyFieldReadAccess(
590
+ { ...context, fields: block.fields },
591
+ typedItem
592
+ );
479
593
  })
480
594
  );
481
595
  }
@@ -487,7 +601,13 @@ async function applyFieldWriteAccess(context, data) {
487
601
  const result = { ...data };
488
602
  for (const field of context.fields) {
489
603
  if (field.type === "row" && field.fields) {
490
- Object.assign(result, await applyFieldWriteAccess({ ...context, fields: field.fields }, result));
604
+ Object.assign(
605
+ result,
606
+ await applyFieldWriteAccess(
607
+ { ...context, fields: field.fields },
608
+ result
609
+ )
610
+ );
491
611
  continue;
492
612
  }
493
613
  if (!field.name || !(field.name in result)) continue;
@@ -506,14 +626,20 @@ async function applyFieldWriteAccess(context, data) {
506
626
  const value = result[field.name];
507
627
  if (value === void 0 || value === null) continue;
508
628
  if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
509
- result[field.name] = await applyFieldWriteAccess({ ...context, fields: field.fields }, value);
629
+ result[field.name] = await applyFieldWriteAccess(
630
+ { ...context, fields: field.fields },
631
+ value
632
+ );
510
633
  continue;
511
634
  }
512
635
  if (field.type === "array" && field.fields && Array.isArray(value)) {
513
636
  const childFields = field.fields;
514
637
  result[field.name] = await Promise.all(
515
638
  value.map(
516
- (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess({ ...context, fields: childFields }, item) : item
639
+ (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess(
640
+ { ...context, fields: childFields },
641
+ item
642
+ ) : item
517
643
  )
518
644
  );
519
645
  continue;
@@ -523,9 +649,14 @@ async function applyFieldWriteAccess(context, data) {
523
649
  value.map(async (item) => {
524
650
  if (typeof item !== "object" || item === null) return item;
525
651
  const typedItem = item;
526
- const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
652
+ const block = field.blocks?.find(
653
+ (candidate) => candidate.slug === typedItem.blockType
654
+ );
527
655
  if (!block || !block.fields) return item;
528
- return applyFieldWriteAccess({ ...context, fields: block.fields }, typedItem);
656
+ return applyFieldWriteAccess(
657
+ { ...context, fields: block.fields },
658
+ typedItem
659
+ );
529
660
  })
530
661
  );
531
662
  }
@@ -587,7 +718,13 @@ var CollectionController = class {
587
718
  } catch {
588
719
  }
589
720
  }
590
- const paginatedResult = await provider.members.list({ limit: limit2, page: page2, sort: sort2, where: where2, req: hookReq });
721
+ const paginatedResult = await provider.members.list({
722
+ limit: limit2,
723
+ page: page2,
724
+ sort: sort2,
725
+ where: where2,
726
+ req: hookReq
727
+ });
591
728
  const mappedDocs = [];
592
729
  for (const m of paginatedResult.docs) {
593
730
  let localId = m.id;
@@ -611,7 +748,7 @@ var CollectionController = class {
611
748
  });
612
749
  }
613
750
  const readonlyDb = createReadonlyDb(db);
614
- const limit = Number(c.req.query("limit")) || 10;
751
+ const limit = Math.min(Number(c.req.query("limit")) || 10, 100);
615
752
  const page = Number(c.req.query("page")) || 1;
616
753
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
617
754
  const sort = c.req.query("sort") || void 0;
@@ -635,12 +772,15 @@ var CollectionController = class {
635
772
  }
636
773
  }
637
774
  }
638
- const beforeReadResult = await runCollectionHooks(this.collection.hooks?.beforeRead, {
639
- req: c.req,
640
- query: where,
641
- user,
642
- db: readonlyDb
643
- });
775
+ const beforeReadResult = await runCollectionHooks(
776
+ this.collection.hooks?.beforeRead,
777
+ {
778
+ req: c.req,
779
+ query: where,
780
+ user,
781
+ db: readonlyDb
782
+ }
783
+ );
644
784
  if (beforeReadResult !== void 0) {
645
785
  where = beforeReadResult;
646
786
  }
@@ -649,7 +789,13 @@ var CollectionController = class {
649
789
  }
650
790
  const access = await this.evaluateAccess(c, "read");
651
791
  if (!access.allowed) {
652
- return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
792
+ return c.json(
793
+ {
794
+ error: true,
795
+ message: `Access denied: read on ${this.collection.slug}`
796
+ },
797
+ 403
798
+ );
653
799
  }
654
800
  if (access.constraint) {
655
801
  where = mergeWhereConstraint(where, access.constraint);
@@ -659,10 +805,14 @@ var CollectionController = class {
659
805
  limit,
660
806
  page,
661
807
  sort,
662
- where
808
+ where,
809
+ fields: this.collection.fields
663
810
  });
664
811
  if (result.total === 0 && this.collection.initialData && !where && page === 1) {
665
- console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
812
+ getRequestLogger(c, "collection").info({
813
+ msg: "Auto-seeding collection from config.initialData",
814
+ collection: this.collection.slug
815
+ });
666
816
  for (const data of this.collection.initialData) {
667
817
  await db.create({ collection: this.collection.slug, data });
668
818
  }
@@ -671,34 +821,58 @@ var CollectionController = class {
671
821
  limit,
672
822
  page,
673
823
  sort,
674
- where
824
+ where,
825
+ fields: this.collection.fields
675
826
  });
676
827
  }
677
- result.docs = result.docs.map((doc) => this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc).filter((doc) => doc !== null);
828
+ result.docs = result.docs.map(
829
+ (doc) => this.collection.workflow ? materializeWorkflowDocument(
830
+ doc,
831
+ this.collection.workflow,
832
+ user
833
+ ) : doc
834
+ ).filter((doc) => doc !== null);
678
835
  const processedDocs = [];
679
- const readonlyDbForHooks = createReadonlyDb(db);
680
836
  for (const doc of result.docs) {
681
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
682
- const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
683
- doc: docWithDefaults,
684
- req: c.req,
685
- user,
686
- db: readonlyDb
687
- });
688
- const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
689
- const docWithFieldAccess = await applyFieldReadAccess({
690
- config,
691
- fields: this.collection.fields,
837
+ const docWithDefaults = DefaultsService.apply(
838
+ this.collection.fields,
839
+ doc
840
+ );
841
+ const docWithCollectionHooks = await runCollectionHooks(
842
+ this.collection.hooks?.afterRead,
843
+ {
844
+ doc: docWithDefaults,
845
+ req: c.req,
846
+ user,
847
+ db: readonlyDb
848
+ }
849
+ );
850
+ const docWithFieldHooks = await executeFieldAfterRead(
851
+ this.collection.fields,
852
+ docWithCollectionHooks,
692
853
  user,
693
- req: this.toHookRequestContext(c),
694
- doc: docWithFieldHooks
695
- }, docWithFieldHooks);
854
+ readonlyDb
855
+ );
856
+ const docWithFieldAccess = await applyFieldReadAccess(
857
+ {
858
+ config,
859
+ fields: this.collection.fields,
860
+ user,
861
+ req: this.toHookRequestContext(c),
862
+ doc: docWithFieldHooks
863
+ },
864
+ docWithFieldHooks
865
+ );
696
866
  processedDocs.push(docWithFieldAccess);
697
867
  }
698
868
  result.docs = processedDocs;
699
869
  if (depth > 0) {
700
870
  const populationService = new PopulationService(db, config.collections);
701
- result = await populationService.populateResult(result, this.collection.fields, depth);
871
+ result = await populationService.populateResult(
872
+ result,
873
+ this.collection.fields,
874
+ depth
875
+ );
702
876
  }
703
877
  return c.json(result);
704
878
  }
@@ -711,7 +885,10 @@ var CollectionController = class {
711
885
  const hookReq = this.toHookRequestContext(c);
712
886
  const id2 = c.req.param("id");
713
887
  if (!id2) return c.json({ message: "Missing ID" }, 400);
714
- const localDoc = await db.findOne({ collection: this.collection.slug, id: id2 });
888
+ const localDoc = await db.findOne({
889
+ collection: this.collection.slug,
890
+ id: id2
891
+ });
715
892
  const externalSubject = localDoc?.externalSubject || id2;
716
893
  let member = null;
717
894
  if (provider.members.get) {
@@ -740,23 +917,40 @@ var CollectionController = class {
740
917
  }
741
918
  const access = await this.evaluateAccess(c, "read", { id, doc });
742
919
  if (!access.allowed) {
743
- return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
920
+ return c.json(
921
+ {
922
+ error: true,
923
+ message: `Access denied: read on ${this.collection.slug}`
924
+ },
925
+ 403
926
+ );
744
927
  }
745
928
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
746
- const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
747
- doc: docWithDefaults,
748
- req: c.req,
749
- user,
750
- db: readonlyDb
751
- });
752
- const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
753
- const docWithFieldAccess = await applyFieldReadAccess({
754
- config,
755
- fields: this.collection.fields,
929
+ const docWithCollectionHooks = await runCollectionHooks(
930
+ this.collection.hooks?.afterRead,
931
+ {
932
+ doc: docWithDefaults,
933
+ req: c.req,
934
+ user,
935
+ db: readonlyDb
936
+ }
937
+ );
938
+ const docWithFieldHooks = await executeFieldAfterRead(
939
+ this.collection.fields,
940
+ docWithCollectionHooks,
756
941
  user,
757
- req: this.toHookRequestContext(c),
758
- doc: docWithFieldHooks
759
- }, docWithFieldHooks);
942
+ readonlyDb
943
+ );
944
+ const docWithFieldAccess = await applyFieldReadAccess(
945
+ {
946
+ config,
947
+ fields: this.collection.fields,
948
+ user,
949
+ req: this.toHookRequestContext(c),
950
+ doc: docWithFieldHooks
951
+ },
952
+ docWithFieldHooks
953
+ );
760
954
  if (depth > 0 && docWithFieldAccess) {
761
955
  const populationService = new PopulationService(db, config.collections);
762
956
  const populatedDoc = await populationService.populate({
@@ -781,12 +975,18 @@ var CollectionController = class {
781
975
  return this.upload(c);
782
976
  }
783
977
  const body2 = await c.req.json();
784
- const member = await provider.members.create({ data: body2, req: hookReq });
785
- return c.json({
786
- ...member,
787
- id: member.id,
788
- externalSubject: member.id
789
- }, 201);
978
+ const member = await provider.members.create({
979
+ data: body2,
980
+ req: hookReq
981
+ });
982
+ return c.json(
983
+ {
984
+ ...member,
985
+ id: member.id,
986
+ externalSubject: member.id
987
+ },
988
+ 201
989
+ );
790
990
  }
791
991
  const readonlyDb = createReadonlyDb(db);
792
992
  const contentType = c.req.header("Content-Type") || "";
@@ -808,20 +1008,35 @@ var CollectionController = class {
808
1008
  }
809
1009
  const createAccess = await this.evaluateAccess(c, "create", { data });
810
1010
  if (!createAccess.allowed) {
811
- return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
1011
+ return c.json(
1012
+ {
1013
+ error: true,
1014
+ message: `Access denied: create on ${this.collection.slug}`
1015
+ },
1016
+ 403
1017
+ );
812
1018
  }
813
1019
  if (this.collection.auth && data.password) {
814
1020
  data.password = await hashPassword(data.password);
815
1021
  }
816
- data = await applyFieldWriteAccess({
817
- config,
818
- fields: this.collection.fields,
819
- user,
820
- req: this.toHookRequestContext(c),
1022
+ data = await applyFieldWriteAccess(
1023
+ {
1024
+ config,
1025
+ fields: this.collection.fields,
1026
+ user,
1027
+ req: this.toHookRequestContext(c),
1028
+ data,
1029
+ operation: "create"
1030
+ },
1031
+ data
1032
+ );
1033
+ data = await executeFieldBeforeChange(
1034
+ this.collection.fields,
821
1035
  data,
822
- operation: "create"
823
- }, data);
824
- data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
1036
+ null,
1037
+ user,
1038
+ readonlyDb
1039
+ );
825
1040
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
826
1041
  data,
827
1042
  req: c.req,
@@ -829,7 +1044,12 @@ var CollectionController = class {
829
1044
  operation: "create",
830
1045
  db: readonlyDb
831
1046
  });
832
- const doc = this.collection.workflow ? (await createWorkflowDocument({ config, collection: this.collection, data, user })).doc : await db.create({ collection: this.collection.slug, data });
1047
+ const doc = this.collection.workflow ? (await createWorkflowDocument({
1048
+ config,
1049
+ collection: this.collection,
1050
+ data,
1051
+ user
1052
+ })).doc : await db.create({ collection: this.collection.slug, data });
833
1053
  if (this.collection.audit && db) {
834
1054
  AuditService.log(db, {
835
1055
  operation: "create",
@@ -838,15 +1058,19 @@ var CollectionController = class {
838
1058
  user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
839
1059
  before: null,
840
1060
  after: doc
841
- });
1061
+ }, config);
842
1062
  }
843
- await runCollectionHooks(this.collection.hooks?.afterChange, {
844
- doc,
845
- user,
846
- req: c.req,
847
- operation: "create",
848
- db
849
- }, { isolated: true });
1063
+ await runCollectionHooks(
1064
+ this.collection.hooks?.afterChange,
1065
+ {
1066
+ doc,
1067
+ user,
1068
+ req: c.req,
1069
+ operation: "create",
1070
+ db
1071
+ },
1072
+ { isolated: true }
1073
+ );
850
1074
  const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
851
1075
  const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
852
1076
  doc: responseDoc,
@@ -854,14 +1078,22 @@ var CollectionController = class {
854
1078
  user,
855
1079
  db: readonlyDb
856
1080
  });
857
- const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
858
- const accessibleDoc = await applyFieldReadAccess({
859
- config,
860
- fields: this.collection.fields,
1081
+ const finalDoc = await executeFieldAfterRead(
1082
+ this.collection.fields,
1083
+ readDoc,
861
1084
  user,
862
- req: this.toHookRequestContext(c),
863
- doc: finalDoc
864
- }, finalDoc);
1085
+ readonlyDb
1086
+ );
1087
+ const accessibleDoc = await applyFieldReadAccess(
1088
+ {
1089
+ config,
1090
+ fields: this.collection.fields,
1091
+ user,
1092
+ req: this.toHookRequestContext(c),
1093
+ doc: finalDoc
1094
+ },
1095
+ finalDoc
1096
+ );
865
1097
  return c.json(accessibleDoc, 201);
866
1098
  }
867
1099
  async upload(c) {
@@ -877,7 +1109,10 @@ var CollectionController = class {
877
1109
  const uploadConfig = typeof this.collection.upload === "object" ? this.collection.upload : void 0;
878
1110
  const validationError = validateUpload(file, uploadConfig);
879
1111
  if (validationError) {
880
- return c.json({ message: validationError.message }, validationError.status);
1112
+ return c.json(
1113
+ { message: validationError.message },
1114
+ validationError.status
1115
+ );
881
1116
  }
882
1117
  const buffer = new Uint8Array(await file.arrayBuffer());
883
1118
  const siteId = c.get("siteId");
@@ -907,17 +1142,32 @@ var CollectionController = class {
907
1142
  };
908
1143
  const createAccess = await this.evaluateAccess(c, "create", { data });
909
1144
  if (!createAccess.allowed) {
910
- return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
1145
+ return c.json(
1146
+ {
1147
+ error: true,
1148
+ message: `Access denied: create on ${this.collection.slug}`
1149
+ },
1150
+ 403
1151
+ );
911
1152
  }
912
- data = await applyFieldWriteAccess({
913
- config,
914
- fields: this.collection.fields,
915
- user,
916
- req: this.toHookRequestContext(c),
1153
+ data = await applyFieldWriteAccess(
1154
+ {
1155
+ config,
1156
+ fields: this.collection.fields,
1157
+ user,
1158
+ req: this.toHookRequestContext(c),
1159
+ data,
1160
+ operation: "create"
1161
+ },
1162
+ data
1163
+ );
1164
+ data = await executeFieldBeforeChange(
1165
+ this.collection.fields,
917
1166
  data,
918
- operation: "create"
919
- }, data);
920
- data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
1167
+ null,
1168
+ user,
1169
+ readonlyDb
1170
+ );
921
1171
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
922
1172
  data,
923
1173
  req: c.req,
@@ -929,13 +1179,17 @@ var CollectionController = class {
929
1179
  collection: this.collection.slug,
930
1180
  data
931
1181
  });
932
- await runCollectionHooks(this.collection.hooks?.afterChange, {
933
- doc,
934
- user,
935
- req: c.req,
936
- operation: "create",
937
- db
938
- }, { isolated: true });
1182
+ await runCollectionHooks(
1183
+ this.collection.hooks?.afterChange,
1184
+ {
1185
+ doc,
1186
+ user,
1187
+ req: c.req,
1188
+ operation: "create",
1189
+ db
1190
+ },
1191
+ { isolated: true }
1192
+ );
939
1193
  const responseDoc = this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc;
940
1194
  const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
941
1195
  doc: responseDoc,
@@ -943,14 +1197,22 @@ var CollectionController = class {
943
1197
  user,
944
1198
  db: readonlyDb
945
1199
  });
946
- const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
947
- const accessibleDoc = await applyFieldReadAccess({
948
- config,
949
- fields: this.collection.fields,
1200
+ const finalDoc = await executeFieldAfterRead(
1201
+ this.collection.fields,
1202
+ readDoc,
950
1203
  user,
951
- req: this.toHookRequestContext(c),
952
- doc: finalDoc
953
- }, finalDoc);
1204
+ readonlyDb
1205
+ );
1206
+ const accessibleDoc = await applyFieldReadAccess(
1207
+ {
1208
+ config,
1209
+ fields: this.collection.fields,
1210
+ user,
1211
+ req: this.toHookRequestContext(c),
1212
+ doc: finalDoc
1213
+ },
1214
+ finalDoc
1215
+ );
954
1216
  return c.json(accessibleDoc, 201);
955
1217
  }
956
1218
  async update(c) {
@@ -963,9 +1225,16 @@ var CollectionController = class {
963
1225
  const id2 = c.req.param("id");
964
1226
  if (!id2) return c.json({ message: "Missing ID" }, 400);
965
1227
  const body2 = await c.req.json();
966
- const localDoc = await db.findOne({ collection: this.collection.slug, id: id2 });
1228
+ const localDoc = await db.findOne({
1229
+ collection: this.collection.slug,
1230
+ id: id2
1231
+ });
967
1232
  const externalSubject = localDoc?.externalSubject || id2;
968
- const member = await provider.members.update({ externalSubject, data: body2, req: hookReq });
1233
+ const member = await provider.members.update({
1234
+ externalSubject,
1235
+ data: body2,
1236
+ req: hookReq
1237
+ });
969
1238
  return c.json({
970
1239
  ...member,
971
1240
  id: localDoc ? localDoc.id : member.id,
@@ -987,25 +1256,47 @@ var CollectionController = class {
987
1256
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
988
1257
  updatedBy: user?.sub ?? null
989
1258
  });
990
- const originalDoc = await db.findOne({ collection: this.collection.slug, id });
1259
+ const originalDoc = await db.findOne({
1260
+ collection: this.collection.slug,
1261
+ id
1262
+ });
991
1263
  if (!originalDoc) return c.json({ message: "Not Found" }, 404);
992
- const updateAccess = await this.evaluateAccess(c, "update", { id, doc: originalDoc, data });
1264
+ const updateAccess = await this.evaluateAccess(c, "update", {
1265
+ id,
1266
+ doc: originalDoc,
1267
+ data
1268
+ });
993
1269
  if (!updateAccess.allowed) {
994
- return c.json({ error: true, message: `Access denied: update on ${this.collection.slug}` }, 403);
1270
+ return c.json(
1271
+ {
1272
+ error: true,
1273
+ message: `Access denied: update on ${this.collection.slug}`
1274
+ },
1275
+ 403
1276
+ );
995
1277
  }
996
1278
  let before = null;
997
1279
  if (this.collection.audit) {
998
1280
  before = originalDoc;
999
1281
  }
1000
- data = await applyFieldWriteAccess({
1001
- config,
1002
- fields: this.collection.fields,
1003
- user,
1004
- req: this.toHookRequestContext(c),
1005
- doc: originalDoc,
1282
+ data = await applyFieldWriteAccess(
1283
+ {
1284
+ config,
1285
+ fields: this.collection.fields,
1286
+ user,
1287
+ req: this.toHookRequestContext(c),
1288
+ doc: originalDoc,
1289
+ data
1290
+ },
1006
1291
  data
1007
- }, data);
1008
- data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user, readonlyDb);
1292
+ );
1293
+ data = await executeFieldBeforeChange(
1294
+ this.collection.fields,
1295
+ data,
1296
+ originalDoc,
1297
+ user,
1298
+ readonlyDb
1299
+ );
1009
1300
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1010
1301
  data,
1011
1302
  doc: originalDoc,
@@ -1014,7 +1305,14 @@ var CollectionController = class {
1014
1305
  operation: "update",
1015
1306
  db: readonlyDb
1016
1307
  });
1017
- const doc = this.collection.workflow ? (await saveWorkflowDraft({ config, collection: this.collection, id, originalDoc, data, user })).doc : await db.update({ collection: this.collection.slug, id, data });
1308
+ const doc = this.collection.workflow ? (await saveWorkflowDraft({
1309
+ config,
1310
+ collection: this.collection,
1311
+ id,
1312
+ originalDoc,
1313
+ data,
1314
+ user
1315
+ })).doc : await db.update({ collection: this.collection.slug, id, data });
1018
1316
  if (this.collection.audit && db) {
1019
1317
  AuditService.log(db, {
1020
1318
  operation: "update",
@@ -1023,36 +1321,52 @@ var CollectionController = class {
1023
1321
  user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1024
1322
  before,
1025
1323
  after: doc
1026
- });
1324
+ }, config);
1027
1325
  }
1028
- await runCollectionHooks(this.collection.hooks?.afterChange, {
1029
- doc,
1030
- previousDoc: originalDoc,
1031
- user,
1032
- req: c.req,
1033
- operation: "update",
1034
- db
1035
- }, { isolated: true });
1326
+ await runCollectionHooks(
1327
+ this.collection.hooks?.afterChange,
1328
+ {
1329
+ doc,
1330
+ previousDoc: originalDoc,
1331
+ user,
1332
+ req: c.req,
1333
+ operation: "update",
1334
+ db
1335
+ },
1336
+ { isolated: true }
1337
+ );
1036
1338
  const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
1037
1339
  doc,
1038
1340
  req: c.req,
1039
1341
  user,
1040
1342
  db: readonlyDb
1041
1343
  });
1042
- const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1043
- const accessibleDoc = await applyFieldReadAccess({
1044
- config,
1045
- fields: this.collection.fields,
1344
+ const finalDoc = await executeFieldAfterRead(
1345
+ this.collection.fields,
1346
+ readDoc,
1046
1347
  user,
1047
- req: this.toHookRequestContext(c),
1048
- doc: finalDoc
1049
- }, finalDoc);
1348
+ readonlyDb
1349
+ );
1350
+ const accessibleDoc = await applyFieldReadAccess(
1351
+ {
1352
+ config,
1353
+ fields: this.collection.fields,
1354
+ user,
1355
+ req: this.toHookRequestContext(c),
1356
+ doc: finalDoc
1357
+ },
1358
+ finalDoc
1359
+ );
1050
1360
  return c.json(accessibleDoc);
1051
1361
  }
1052
1362
  async transition(c) {
1053
1363
  const config = c.get("config");
1054
1364
  if (!config.db) return c.json({ message: "Database not configured" }, 500);
1055
- if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
1365
+ if (!this.collection.workflow)
1366
+ return c.json(
1367
+ { message: "Workflows are not enabled for this collection" },
1368
+ 404
1369
+ );
1056
1370
  const id = c.req.param("id");
1057
1371
  const transitionName = c.req.param("transition");
1058
1372
  const body = await c.req.json().catch(() => ({}));
@@ -1067,19 +1381,38 @@ var CollectionController = class {
1067
1381
  user: c.get("user"),
1068
1382
  req: { query: c.req.query(), headers: c.req.header(), raw: c.req.raw }
1069
1383
  });
1070
- return c.json(materializeWorkflowDocument(doc, this.collection.workflow, c.get("user")));
1384
+ return c.json(
1385
+ materializeWorkflowDocument(
1386
+ doc,
1387
+ this.collection.workflow,
1388
+ c.get("user")
1389
+ )
1390
+ );
1071
1391
  } catch (error) {
1072
1392
  const status = typeof error.statusCode === "number" ? error.statusCode : 500;
1073
- return c.json({ error: true, message: error instanceof Error ? error.message : String(error) }, status);
1393
+ return c.json(
1394
+ {
1395
+ error: true,
1396
+ message: error instanceof Error ? error.message : String(error)
1397
+ },
1398
+ status
1399
+ );
1074
1400
  }
1075
1401
  }
1076
1402
  async workflowHistory(c) {
1077
1403
  const config = c.get("config");
1078
1404
  if (!config.db) return c.json({ message: "Database not configured" }, 500);
1079
- if (!this.collection.workflow) return c.json({ message: "Workflows are not enabled for this collection" }, 404);
1405
+ if (!this.collection.workflow)
1406
+ return c.json(
1407
+ { message: "Workflows are not enabled for this collection" },
1408
+ 404
1409
+ );
1080
1410
  const documentId = c.req.param("id");
1081
1411
  if (!documentId) return c.json({ message: "Missing ID" }, 400);
1082
- const document = await config.db.findOne({ collection: this.collection.slug, id: documentId });
1412
+ const document = await config.db.findOne({
1413
+ collection: this.collection.slug,
1414
+ id: documentId
1415
+ });
1083
1416
  if (!document) return c.json({ message: "Not Found" }, 404);
1084
1417
  const readAccess = this.collection.access?.read;
1085
1418
  if (readAccess !== void 0 && readAccess !== null) {
@@ -1094,11 +1427,21 @@ var CollectionController = class {
1094
1427
  });
1095
1428
  allowed = match.total > 0;
1096
1429
  }
1097
- if (!allowed) return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
1430
+ if (!allowed)
1431
+ return c.json(
1432
+ {
1433
+ error: true,
1434
+ message: `Access denied: read on ${this.collection.slug}`
1435
+ },
1436
+ 403
1437
+ );
1098
1438
  }
1099
1439
  const result = await config.db.find({
1100
1440
  collection: WORKFLOW_HISTORY_COLLECTION,
1101
- where: { collection: { equals: this.collection.slug }, documentId: { equals: documentId } },
1441
+ where: {
1442
+ collection: { equals: this.collection.slug },
1443
+ documentId: { equals: documentId }
1444
+ },
1102
1445
  sort: "-createdAt",
1103
1446
  limit: Math.min(Number(c.req.query("limit")) || 50, 100)
1104
1447
  });
@@ -1120,7 +1463,10 @@ var CollectionController = class {
1120
1463
  const db = config.db;
1121
1464
  if (!db) return c.json({ message: "Database not configured" }, 500);
1122
1465
  if (!this.collection.auth) {
1123
- return c.json({ message: "This collection does not support authentication" }, 400);
1466
+ return c.json(
1467
+ { message: "This collection does not support authentication" },
1468
+ 400
1469
+ );
1124
1470
  }
1125
1471
  const id = c.req.param("id");
1126
1472
  if (!id) return c.json({ message: "Missing ID" }, 400);
@@ -1140,29 +1486,44 @@ var CollectionController = class {
1140
1486
  const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
1141
1487
  const isSelf = user.sub === id;
1142
1488
  if (!isAdmin && !isSelf) {
1143
- return c.json({ message: "You are not authorised to change this password" }, 403);
1489
+ return c.json(
1490
+ { message: "You are not authorised to change this password" },
1491
+ 403
1492
+ );
1144
1493
  }
1145
1494
  if (!isAdmin) {
1146
1495
  if (!oldPassword) {
1147
1496
  return c.json({ message: "Current password is required" }, 400);
1148
1497
  }
1149
- const existing = await db.findOne({ collection: this.collection.slug, id });
1498
+ const existing = await db.findOne({
1499
+ collection: this.collection.slug,
1500
+ id
1501
+ });
1150
1502
  if (!existing) return c.json({ message: "User not found" }, 404);
1151
- const valid = await verifyPassword(oldPassword, existing.password);
1503
+ const valid = await verifyPassword(
1504
+ oldPassword,
1505
+ existing.password
1506
+ );
1152
1507
  if (!valid) {
1153
1508
  return c.json({ message: "Invalid current password" }, 400);
1154
1509
  }
1155
1510
  }
1156
1511
  const hashed = await hashPassword(newPassword);
1157
- const doc = await db.update({
1512
+ await db.update({
1158
1513
  collection: this.collection.slug,
1159
1514
  id,
1160
1515
  data: {
1161
1516
  password: hashed,
1517
+ loginAttempts: 0,
1518
+ lockedUntil: null,
1162
1519
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1163
1520
  updatedBy: user.sub
1164
1521
  }
1165
1522
  });
1523
+ await revokeAllAuthSessions(config, {
1524
+ userId: id,
1525
+ collection: this.collection.slug
1526
+ });
1166
1527
  if (this.collection.audit) {
1167
1528
  AuditService.log(db, {
1168
1529
  operation: "update",
@@ -1171,7 +1532,7 @@ var CollectionController = class {
1171
1532
  user: { id: user.sub, collection: user.collection, email: user.email },
1172
1533
  before: null,
1173
1534
  after: { id }
1174
- });
1535
+ }, config);
1175
1536
  }
1176
1537
  return c.json({ success: true, message: "Password updated successfully" });
1177
1538
  }
@@ -1184,7 +1545,10 @@ var CollectionController = class {
1184
1545
  const hookReq = this.toHookRequestContext(c);
1185
1546
  const id2 = c.req.param("id");
1186
1547
  if (!id2) return c.json({ message: "Missing ID" }, 400);
1187
- const localDoc = await db.findOne({ collection: this.collection.slug, id: id2 });
1548
+ const localDoc = await db.findOne({
1549
+ collection: this.collection.slug,
1550
+ id: id2
1551
+ });
1188
1552
  const externalSubject = localDoc?.externalSubject || id2;
1189
1553
  await provider.members.delete({ externalSubject, req: hookReq });
1190
1554
  return c.json({ message: "Deleted" });
@@ -1197,7 +1561,13 @@ var CollectionController = class {
1197
1561
  if (!doc) return c.json({ message: "Not Found" }, 404);
1198
1562
  const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1199
1563
  if (!deleteAccess.allowed) {
1200
- return c.json({ error: true, message: `Access denied: delete on ${this.collection.slug}` }, 403);
1564
+ return c.json(
1565
+ {
1566
+ error: true,
1567
+ message: `Access denied: delete on ${this.collection.slug}`
1568
+ },
1569
+ 403
1570
+ );
1201
1571
  }
1202
1572
  let before = null;
1203
1573
  if (this.collection.audit) {
@@ -1219,15 +1589,19 @@ var CollectionController = class {
1219
1589
  user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1220
1590
  before,
1221
1591
  after: null
1222
- });
1592
+ }, config);
1223
1593
  }
1224
- await runCollectionHooks(this.collection.hooks?.afterDelete, {
1225
- id,
1226
- doc,
1227
- user,
1228
- req: c.req,
1229
- db
1230
- }, { isolated: true });
1594
+ await runCollectionHooks(
1595
+ this.collection.hooks?.afterDelete,
1596
+ {
1597
+ id,
1598
+ doc,
1599
+ user,
1600
+ req: c.req,
1601
+ db
1602
+ },
1603
+ { isolated: true }
1604
+ );
1231
1605
  return c.json({ message: "Deleted" });
1232
1606
  }
1233
1607
  async deleteMany(c) {
@@ -1258,7 +1632,10 @@ var CollectionController = class {
1258
1632
  failed.push({ id, error: "Not Found" });
1259
1633
  continue;
1260
1634
  }
1261
- const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1635
+ const deleteAccess = await this.evaluateAccess(c, "delete", {
1636
+ id,
1637
+ doc
1638
+ });
1262
1639
  if (!deleteAccess.allowed) {
1263
1640
  failed.push({ id, error: "Access denied" });
1264
1641
  continue;
@@ -1284,15 +1661,19 @@ var CollectionController = class {
1284
1661
  user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1285
1662
  before,
1286
1663
  after: null
1287
- });
1664
+ }, config);
1288
1665
  }
1289
- await runCollectionHooks(this.collection.hooks?.afterDelete, {
1290
- id,
1291
- doc,
1292
- user,
1293
- req: c.req,
1294
- db
1295
- }, { isolated: true });
1666
+ await runCollectionHooks(
1667
+ this.collection.hooks?.afterDelete,
1668
+ {
1669
+ id,
1670
+ doc,
1671
+ user,
1672
+ req: c.req,
1673
+ db
1674
+ },
1675
+ { isolated: true }
1676
+ );
1296
1677
  } catch (err) {
1297
1678
  failed.push({ id, error: err?.message ?? "Unknown error" });
1298
1679
  }
@@ -1312,17 +1693,26 @@ var CollectionController = class {
1312
1693
  if (!initialData || !Array.isArray(initialData)) {
1313
1694
  return c.json({ message: "Invalid initial data" }, 400);
1314
1695
  }
1315
- const result = await db.find({ collection: this.collection.slug, limit: 1 });
1696
+ const result = await db.find({
1697
+ collection: this.collection.slug,
1698
+ limit: 1
1699
+ });
1316
1700
  if (result.total > 0) {
1317
1701
  return c.json({ message: "Collection is not empty, skipping seed" });
1318
1702
  }
1319
- console.log(`[dyrected/core] Auto-seeding collection: ${this.collection.slug}`);
1703
+ getRequestLogger(c, "collection").info({
1704
+ msg: "Auto-seeding collection",
1705
+ collection: this.collection.slug
1706
+ });
1320
1707
  const createdDocs = [];
1321
1708
  for (const data of initialData) {
1322
1709
  const doc = await db.create({ collection: this.collection.slug, data });
1323
1710
  createdDocs.push(doc);
1324
1711
  }
1325
- return c.json({ message: "Seed successful", count: createdDocs.length }, 201);
1712
+ return c.json(
1713
+ { message: "Seed successful", count: createdDocs.length },
1714
+ 201
1715
+ );
1326
1716
  }
1327
1717
  };
1328
1718
 
@@ -1339,46 +1729,66 @@ var GlobalController = class {
1339
1729
  const readonlyDb = createReadonlyDb(db);
1340
1730
  const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1341
1731
  const user = c.get("user");
1342
- let query = void 0;
1343
- const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
1732
+ await runCollectionHooks(this.global.hooks?.beforeRead, {
1344
1733
  req: c.req,
1345
- query,
1734
+ query: void 0,
1346
1735
  user,
1347
1736
  db: readonlyDb
1348
1737
  });
1349
- if (beforeReadResult !== void 0) {
1350
- query = beforeReadResult;
1351
- }
1352
1738
  let data = await db.getGlobal({ slug: this.global.slug });
1353
1739
  const isEmpty = !data || isFunctionallyEmpty(data);
1354
1740
  if (isEmpty && this.global.initialData) {
1355
- console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
1356
- await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
1741
+ getRequestLogger(c, "global").info({
1742
+ msg: "Auto-seeding global from config.initialData",
1743
+ global: this.global.slug
1744
+ });
1745
+ await db.updateGlobal({
1746
+ slug: this.global.slug,
1747
+ data: this.global.initialData
1748
+ });
1357
1749
  data = this.global.initialData;
1358
1750
  }
1359
- const canRead = await resolveBooleanAccess(config, this.global.access?.read, {
1360
- user,
1361
- req: toHookRequestContext(c.req),
1362
- doc: data
1363
- });
1751
+ const canRead = await resolveBooleanAccess(
1752
+ config,
1753
+ this.global.access?.read,
1754
+ {
1755
+ user,
1756
+ req: toHookRequestContext(c.req),
1757
+ doc: data
1758
+ }
1759
+ );
1364
1760
  if (!canRead) {
1365
- return c.json({ error: true, message: `Access denied: read on ${this.global.slug}` }, 403);
1761
+ return c.json(
1762
+ { error: true, message: `Access denied: read on ${this.global.slug}` },
1763
+ 403
1764
+ );
1366
1765
  }
1367
1766
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1368
- const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
1369
- doc: dataWithDefaults,
1370
- req: c.req,
1371
- user,
1372
- db: readonlyDb
1373
- });
1374
- const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user, readonlyDb);
1375
- const docWithFieldAccess = await applyFieldReadAccess({
1376
- config,
1377
- fields: this.global.fields,
1767
+ const docWithCollectionHooks = await runCollectionHooks(
1768
+ this.global.hooks?.afterRead,
1769
+ {
1770
+ doc: dataWithDefaults,
1771
+ req: c.req,
1772
+ user,
1773
+ db: readonlyDb
1774
+ }
1775
+ );
1776
+ const docWithFieldHooks = await executeFieldAfterRead(
1777
+ this.global.fields,
1778
+ docWithCollectionHooks,
1378
1779
  user,
1379
- req: toHookRequestContext(c.req),
1380
- doc: docWithFieldHooks
1381
- }, docWithFieldHooks);
1780
+ readonlyDb
1781
+ );
1782
+ const docWithFieldAccess = await applyFieldReadAccess(
1783
+ {
1784
+ config,
1785
+ fields: this.global.fields,
1786
+ user,
1787
+ req: toHookRequestContext(c.req),
1788
+ doc: docWithFieldHooks
1789
+ },
1790
+ docWithFieldHooks
1791
+ );
1382
1792
  if (depth > 0 && docWithFieldAccess) {
1383
1793
  const populationService = new PopulationService(db, config.collections);
1384
1794
  const populatedData = await populationService.populate({
@@ -1399,24 +1809,43 @@ var GlobalController = class {
1399
1809
  const body = await c.req.json();
1400
1810
  const user = c.get("user");
1401
1811
  const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
1402
- const canUpdate = await resolveBooleanAccess(config, this.global.access?.update, {
1403
- user,
1404
- req: toHookRequestContext(c.req),
1405
- doc: originalDoc,
1406
- data: body
1407
- });
1812
+ const canUpdate = await resolveBooleanAccess(
1813
+ config,
1814
+ this.global.access?.update,
1815
+ {
1816
+ user,
1817
+ req: toHookRequestContext(c.req),
1818
+ doc: originalDoc,
1819
+ data: body
1820
+ }
1821
+ );
1408
1822
  if (!canUpdate) {
1409
- return c.json({ error: true, message: `Access denied: update on ${this.global.slug}` }, 403);
1823
+ return c.json(
1824
+ {
1825
+ error: true,
1826
+ message: `Access denied: update on ${this.global.slug}`
1827
+ },
1828
+ 403
1829
+ );
1410
1830
  }
1411
- let sanitizedBody = await applyFieldWriteAccess({
1412
- config,
1413
- fields: this.global.fields,
1831
+ const sanitizedBody = await applyFieldWriteAccess(
1832
+ {
1833
+ config,
1834
+ fields: this.global.fields,
1835
+ user,
1836
+ req: toHookRequestContext(c.req),
1837
+ doc: originalDoc,
1838
+ data: body
1839
+ },
1840
+ body
1841
+ );
1842
+ let data = await executeFieldBeforeChange(
1843
+ this.global.fields,
1844
+ sanitizedBody,
1845
+ originalDoc,
1414
1846
  user,
1415
- req: toHookRequestContext(c.req),
1416
- doc: originalDoc,
1417
- data: body
1418
- }, body);
1419
- let data = await executeFieldBeforeChange(this.global.fields, sanitizedBody, originalDoc, user, readonlyDb);
1847
+ readonlyDb
1848
+ );
1420
1849
  data = await runCollectionHooks(this.global.hooks?.beforeChange, {
1421
1850
  data,
1422
1851
  doc: originalDoc,
@@ -1426,28 +1855,40 @@ var GlobalController = class {
1426
1855
  db: readonlyDb
1427
1856
  });
1428
1857
  const updated = await db.updateGlobal({ slug: this.global.slug, data });
1429
- await runCollectionHooks(this.global.hooks?.afterChange, {
1430
- doc: updated,
1431
- previousDoc: originalDoc,
1432
- user,
1433
- req: c.req,
1434
- operation: "update",
1435
- db
1436
- }, { isolated: true });
1858
+ await runCollectionHooks(
1859
+ this.global.hooks?.afterChange,
1860
+ {
1861
+ doc: updated,
1862
+ previousDoc: originalDoc,
1863
+ user,
1864
+ req: c.req,
1865
+ operation: "update",
1866
+ db
1867
+ },
1868
+ { isolated: true }
1869
+ );
1437
1870
  const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
1438
1871
  doc: updated,
1439
1872
  req: c.req,
1440
1873
  user,
1441
1874
  db: readonlyDb
1442
1875
  });
1443
- const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user, readonlyDb);
1444
- const accessibleDoc = await applyFieldReadAccess({
1445
- config,
1446
- fields: this.global.fields,
1876
+ const finalDoc = await executeFieldAfterRead(
1877
+ this.global.fields,
1878
+ readDoc,
1447
1879
  user,
1448
- req: toHookRequestContext(c.req),
1449
- doc: finalDoc
1450
- }, finalDoc);
1880
+ readonlyDb
1881
+ );
1882
+ const accessibleDoc = await applyFieldReadAccess(
1883
+ {
1884
+ config,
1885
+ fields: this.global.fields,
1886
+ user,
1887
+ req: toHookRequestContext(c.req),
1888
+ doc: finalDoc
1889
+ },
1890
+ finalDoc
1891
+ );
1451
1892
  return c.json(accessibleDoc);
1452
1893
  }
1453
1894
  async seed(c) {
@@ -1463,7 +1904,10 @@ var GlobalController = class {
1463
1904
  if (existing && !isFunctionallyEmpty(existing)) {
1464
1905
  return c.json({ message: "Global is not empty, skipping seed" });
1465
1906
  }
1466
- console.log(`[dyrected/core] Auto-seeding global: ${this.global.slug}`);
1907
+ getRequestLogger(c, "global").info({
1908
+ msg: "Auto-seeding global",
1909
+ global: this.global.slug
1910
+ });
1467
1911
  await db.updateGlobal({ slug: this.global.slug, data: initialData });
1468
1912
  return c.json({ message: "Seed successful", data: initialData }, 201);
1469
1913
  }
@@ -1485,6 +1929,7 @@ function isFunctionallyEmpty(obj) {
1485
1929
  // src/controllers/media.controller.ts
1486
1930
  var MediaController = class {
1487
1931
  collection;
1932
+ loggerComponent = "media";
1488
1933
  constructor(collection = "media") {
1489
1934
  this.collection = collection;
1490
1935
  }
@@ -1503,15 +1948,25 @@ var MediaController = class {
1503
1948
  return c.json({ message: "No file uploaded" }, 400);
1504
1949
  }
1505
1950
  const siteId = c.get("siteId");
1506
- let colConfig = config.collections.find((col) => col.slug === this.collection);
1951
+ let colConfig = config.collections.find(
1952
+ (col) => col.slug === this.collection
1953
+ );
1507
1954
  if (!colConfig && config.onSchemaFetch && siteId) {
1508
- const dynamic = await config.onSchemaFetch(siteId);
1509
- colConfig = dynamic.collections?.find((col) => col.slug === this.collection);
1955
+ const requestConfig = mergeDynamicConfig(
1956
+ config,
1957
+ await config.onSchemaFetch(siteId)
1958
+ );
1959
+ colConfig = requestConfig.collections.find(
1960
+ (col) => col.slug === this.collection
1961
+ );
1510
1962
  }
1511
1963
  const uploadConfig = typeof colConfig?.upload === "object" ? colConfig.upload : void 0;
1512
1964
  const validationError = validateUpload(file, uploadConfig);
1513
1965
  if (validationError) {
1514
- return c.json({ message: validationError.message }, validationError.status);
1966
+ return c.json(
1967
+ { message: validationError.message },
1968
+ validationError.status
1969
+ );
1515
1970
  }
1516
1971
  const buffer = new Uint8Array(await file.arrayBuffer());
1517
1972
  const workspaceId = c.get("workspaceId");
@@ -1529,7 +1984,12 @@ var MediaController = class {
1529
1984
  imageMetadata = processed.metadata;
1530
1985
  imageSizes = processed.sizes;
1531
1986
  } catch (err) {
1532
- console.error("[MediaController] Image processing failed:", err);
1987
+ getRequestLogger(c, this.loggerComponent).error({
1988
+ err,
1989
+ msg: "Image processing failed",
1990
+ collection: this.collection,
1991
+ filename: file.name
1992
+ });
1533
1993
  }
1534
1994
  }
1535
1995
  const fileData = await storage.upload({
@@ -1562,7 +2022,13 @@ var MediaController = class {
1562
2022
  height: sizeData.height
1563
2023
  };
1564
2024
  } catch (err) {
1565
- console.error(`[MediaController] Failed to upload size ${sizeName}:`, err);
2025
+ getRequestLogger(c, this.loggerComponent).error({
2026
+ err,
2027
+ msg: "Failed to upload image size",
2028
+ collection: this.collection,
2029
+ filename: file.name,
2030
+ sizeName
2031
+ });
1566
2032
  }
1567
2033
  }
1568
2034
  }
@@ -1626,25 +2092,37 @@ var MediaController = class {
1626
2092
  }
1627
2093
  };
1628
2094
 
1629
- // src/auth/token.ts
1630
- import { SignJWT, jwtVerify, decodeJwt } from "jose";
1631
- import { TextEncoder } from "util";
1632
- function getSecret() {
1633
- const secret = process.env.DYRECTED_JWT_SECRET;
1634
- if (!secret) {
1635
- throw new Error(
1636
- "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
1637
- );
2095
+ // src/auth/lockout.ts
2096
+ var DEFAULT_MAX_LOGIN_ATTEMPTS = 5;
2097
+ var DEFAULT_LOCK_TIME_MS = 10 * 60 * 1e3;
2098
+ function resolveAuthLockoutConfig(collection) {
2099
+ const auth = collection.auth;
2100
+ if (!auth) {
2101
+ return {
2102
+ enabled: false,
2103
+ maxLoginAttempts: DEFAULT_MAX_LOGIN_ATTEMPTS,
2104
+ lockTime: DEFAULT_LOCK_TIME_MS
2105
+ };
1638
2106
  }
1639
- return new TextEncoder().encode(secret);
1640
- }
1641
- var DEFAULT_EXPIRY = "7d";
1642
- async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
1643
- return new SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
2107
+ if (auth === true) {
2108
+ return {
2109
+ enabled: true,
2110
+ maxLoginAttempts: DEFAULT_MAX_LOGIN_ATTEMPTS,
2111
+ lockTime: DEFAULT_LOCK_TIME_MS
2112
+ };
2113
+ }
2114
+ const maxLoginAttempts = typeof auth.maxLoginAttempts === "number" ? auth.maxLoginAttempts : DEFAULT_MAX_LOGIN_ATTEMPTS;
2115
+ const lockTime = typeof auth.lockTime === "number" && auth.lockTime > 0 ? auth.lockTime : DEFAULT_LOCK_TIME_MS;
2116
+ return {
2117
+ enabled: maxLoginAttempts > 0,
2118
+ maxLoginAttempts,
2119
+ lockTime
2120
+ };
1644
2121
  }
1645
- async function verifyCollectionToken(token) {
1646
- const { payload } = await jwtVerify(token, getSecret());
1647
- return payload;
2122
+ function getLockedUntilMs(value) {
2123
+ if (typeof value !== "string") return null;
2124
+ const parsed = Date.parse(value);
2125
+ return Number.isFinite(parsed) ? parsed : null;
1648
2126
  }
1649
2127
 
1650
2128
  // src/services/email-template.ts
@@ -1744,6 +2222,7 @@ async function getDevSend() {
1744
2222
  if (_devSend) return _devSend;
1745
2223
  if (_devSendPromise) return _devSendPromise;
1746
2224
  _devSendPromise = (async () => {
2225
+ const logger = getConfigLogger(void 0, "email");
1747
2226
  try {
1748
2227
  const nodemailer = await import("nodemailer");
1749
2228
  const account = await nodemailer.default.createTestAccount();
@@ -1752,23 +2231,50 @@ async function getDevSend() {
1752
2231
  port: 587,
1753
2232
  auth: { user: account.user, pass: account.pass }
1754
2233
  });
1755
- console.log("[dyrected/core] No email config \u2014 using Ethereal for dev email preview.");
1756
- console.log(`[dyrected/core] Ethereal login: https://ethereal.email user: ${account.user} pass: ${account.pass}`);
2234
+ logger.info({
2235
+ msg: "No email config; using Ethereal for development email preview"
2236
+ });
2237
+ logger.info({
2238
+ msg: "Ethereal credentials",
2239
+ loginUrl: "https://ethereal.email",
2240
+ user: account.user,
2241
+ password: account.pass
2242
+ });
1757
2243
  _devSend = async ({ to, subject, html }) => {
1758
2244
  const info = await transport.sendMail({ from: '"Dyrected Dev" <dev@dyrected.local>', to, subject, html });
1759
- console.log(`[dyrected/core] Email preview URL: ${nodemailer.default.getTestMessageUrl(info)}`);
2245
+ logger.info({
2246
+ msg: "Email preview URL",
2247
+ previewUrl: nodemailer.default.getTestMessageUrl(info)
2248
+ });
1760
2249
  };
1761
2250
  return _devSend;
1762
2251
  } catch {
1763
- console.warn("[dyrected/core] nodemailer not available \u2014 emails will not be sent in dev.");
2252
+ logger.warn({
2253
+ msg: "nodemailer not available; development emails will not be sent"
2254
+ });
1764
2255
  return null;
1765
2256
  }
1766
2257
  })();
1767
2258
  return _devSendPromise;
1768
2259
  }
1769
2260
  async function sendEmail(config, payload) {
2261
+ const logger = getConfigLogger(config, "email");
2262
+ const observability = getObservabilityRuntime(config);
1770
2263
  if (config.email) {
1771
- await config.email.send(payload);
2264
+ try {
2265
+ await config.email.send(payload);
2266
+ } catch (err) {
2267
+ observability?.recordEmailSendFailure({
2268
+ to: payload.to
2269
+ });
2270
+ logger.error({
2271
+ err,
2272
+ msg: "Email provider send failed",
2273
+ to: payload.to,
2274
+ subject: payload.subject
2275
+ });
2276
+ throw err;
2277
+ }
1772
2278
  return;
1773
2279
  }
1774
2280
  if (process.env.NODE_ENV !== "production") {
@@ -1832,6 +2338,55 @@ var AuthController = class {
1832
2338
  constructor(collection) {
1833
2339
  this.collection = collection;
1834
2340
  }
2341
+ sanitizeUser(user) {
2342
+ const {
2343
+ password: _password,
2344
+ loginAttempts: _loginAttempts,
2345
+ lockedUntil: _lockedUntil,
2346
+ ...safeUser
2347
+ } = user;
2348
+ return safeUser;
2349
+ }
2350
+ clearLockoutState(c, userId) {
2351
+ const db = c.get("config").db;
2352
+ if (!db) return Promise.resolve(null);
2353
+ return db.update({
2354
+ collection: this.collection.slug,
2355
+ id: userId,
2356
+ data: {
2357
+ loginAttempts: 0,
2358
+ lockedUntil: null
2359
+ }
2360
+ });
2361
+ }
2362
+ async recordFailedLogin(c, user) {
2363
+ const db = c.get("config").db;
2364
+ if (!db)
2365
+ return { justLocked: false, retryAfterSeconds: null };
2366
+ const lockout = resolveAuthLockoutConfig(this.collection);
2367
+ if (!lockout.enabled) {
2368
+ return { justLocked: false, retryAfterSeconds: null };
2369
+ }
2370
+ const now = Date.now();
2371
+ const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
2372
+ const lockExpired = lockedUntilMs !== null && lockedUntilMs <= now;
2373
+ const currentAttempts = !lockExpired && typeof user.loginAttempts === "number" && user.loginAttempts > 0 ? user.loginAttempts : 0;
2374
+ const loginAttempts = currentAttempts + 1;
2375
+ const justLocked = loginAttempts >= lockout.maxLoginAttempts;
2376
+ const nextLockedUntil = justLocked ? new Date(now + lockout.lockTime).toISOString() : null;
2377
+ await db.update({
2378
+ collection: this.collection.slug,
2379
+ id: String(user.id),
2380
+ data: {
2381
+ loginAttempts,
2382
+ lockedUntil: nextLockedUntil
2383
+ }
2384
+ });
2385
+ return {
2386
+ justLocked,
2387
+ retryAfterSeconds: justLocked ? Math.max(1, Math.ceil(lockout.lockTime / 1e3)) : null
2388
+ };
2389
+ }
1835
2390
  // ---------------------------------------------------------------------------
1836
2391
  // GET /init
1837
2392
  // Checks if the first user needs to be created.
@@ -1860,11 +2415,17 @@ var AuthController = class {
1860
2415
  limit: 1
1861
2416
  });
1862
2417
  if (check.total > 0) {
1863
- return c.json({ error: true, message: "Initial user already exists." }, 403);
2418
+ return c.json(
2419
+ { error: true, message: "Initial user already exists." },
2420
+ 403
2421
+ );
1864
2422
  }
1865
2423
  const body = await c.req.json().catch(() => null);
1866
2424
  if (!body?.email || !body?.password) {
1867
- return c.json({ error: true, message: "email and password are required." }, 400);
2425
+ return c.json(
2426
+ { error: true, message: "email and password are required." },
2427
+ 400
2428
+ );
1868
2429
  }
1869
2430
  const hashedPassword = await hashPassword(body.password);
1870
2431
  const user = await db.create({
@@ -1876,16 +2437,23 @@ var AuthController = class {
1876
2437
  // Default first user to admin
1877
2438
  }
1878
2439
  });
1879
- const token = await signCollectionToken({
1880
- sub: user.id,
2440
+ const token = await issueAuthSessionToken({
2441
+ config,
2442
+ userId: user.id,
1881
2443
  email: user.email,
1882
- collection: this.collection.slug
2444
+ collection: this.collection.slug,
2445
+ ip: c.get("clientIp"),
2446
+ authSource: "local"
1883
2447
  });
1884
2448
  const { subject, html } = buildWelcomeEmail(config, { email: body.email });
1885
2449
  sendEmail(config, { to: body.email, subject, html }).catch(
1886
- (err) => console.error("[dyrected/core] Failed to send welcome email:", err)
2450
+ (err) => getRequestLogger(c, "auth").error({
2451
+ err,
2452
+ msg: "Failed to send welcome email",
2453
+ email: body.email
2454
+ })
1887
2455
  );
1888
- const { password: _, ...safeUser } = user;
2456
+ const safeUser = this.sanitizeUser(user);
1889
2457
  return c.json({ token, user: safeUser });
1890
2458
  }
1891
2459
  // ---------------------------------------------------------------------------
@@ -1896,7 +2464,10 @@ var AuthController = class {
1896
2464
  if (!db) return c.json({ message: "Database not configured" }, 500);
1897
2465
  const body = await c.req.json().catch(() => null);
1898
2466
  if (!body?.email || !body?.password) {
1899
- return c.json({ error: true, message: "email and password are required." }, 400);
2467
+ return c.json(
2468
+ { error: true, message: "email and password are required." },
2469
+ 400
2470
+ );
1900
2471
  }
1901
2472
  const result = await db.find({
1902
2473
  collection: this.collection.slug,
@@ -1905,27 +2476,107 @@ var AuthController = class {
1905
2476
  });
1906
2477
  const user = result.docs[0];
1907
2478
  if (!user) {
1908
- return c.json({ error: true, message: "Invalid email or password." }, 401);
2479
+ return c.json(
2480
+ { error: true, message: "Invalid email or password." },
2481
+ 401
2482
+ );
2483
+ }
2484
+ const lockedUntilMs = getLockedUntilMs(user.lockedUntil);
2485
+ if (lockedUntilMs !== null && lockedUntilMs > Date.now()) {
2486
+ const retryAfterSeconds = Math.max(
2487
+ 1,
2488
+ Math.ceil((lockedUntilMs - Date.now()) / 1e3)
2489
+ );
2490
+ c.header("Retry-After", String(retryAfterSeconds));
2491
+ return c.json(
2492
+ {
2493
+ error: true,
2494
+ message: "Too many login attempts. Try again later.",
2495
+ retryAfterSeconds
2496
+ },
2497
+ 429
2498
+ );
1909
2499
  }
1910
2500
  const valid = await verifyPassword(body.password, user.password);
1911
2501
  if (!valid) {
1912
- return c.json({ error: true, message: "Invalid email or password." }, 401);
2502
+ const { justLocked, retryAfterSeconds } = await this.recordFailedLogin(
2503
+ c,
2504
+ user
2505
+ );
2506
+ if (justLocked && retryAfterSeconds) {
2507
+ c.header("Retry-After", String(retryAfterSeconds));
2508
+ return c.json(
2509
+ {
2510
+ error: true,
2511
+ message: "Too many login attempts. Try again later.",
2512
+ retryAfterSeconds
2513
+ },
2514
+ 429
2515
+ );
2516
+ }
2517
+ return c.json(
2518
+ { error: true, message: "Invalid email or password." },
2519
+ 401
2520
+ );
2521
+ }
2522
+ if (typeof user.loginAttempts === "number" && user.loginAttempts > 0 || user.lockedUntil != null) {
2523
+ await this.clearLockoutState(c, String(user.id));
1913
2524
  }
1914
- const token = await signCollectionToken({
1915
- sub: user.id,
2525
+ const token = await issueAuthSessionToken({
2526
+ config: c.get("config"),
2527
+ userId: user.id,
1916
2528
  email: user.email,
1917
- collection: this.collection.slug
2529
+ collection: this.collection.slug,
2530
+ ip: c.get("clientIp"),
2531
+ authSource: "local"
1918
2532
  });
1919
- const { password: _, ...safeUser } = user;
2533
+ const safeUser = this.sanitizeUser(user);
1920
2534
  return c.json({ token, user: safeUser });
1921
2535
  }
1922
2536
  // ---------------------------------------------------------------------------
1923
2537
  // POST /logout
1924
- // Auth collections use stateless JWTs logout is handled client-side.
1925
- // This endpoint exists so clients have a consistent API surface.
2538
+ // Revoke the current session by default. Pass `?allSessions=true` to revoke
2539
+ // every active session for the current account.
1926
2540
  // ---------------------------------------------------------------------------
1927
2541
  async logout(c) {
1928
- return c.json({ success: true, message: "Logged out. Discard your token." });
2542
+ const requestUser = c.get("user");
2543
+ const tokenPayload = c.get("authTokenPayload");
2544
+ const allSessions = ["1", "true", "yes"].includes(
2545
+ (c.req.query("allSessions") || "").toLowerCase()
2546
+ );
2547
+ if (!requestUser) {
2548
+ if (allSessions) {
2549
+ return c.json(
2550
+ { error: true, message: "Authentication required." },
2551
+ 401
2552
+ );
2553
+ }
2554
+ return c.json({
2555
+ success: true,
2556
+ message: "Logged out. Discard your token."
2557
+ });
2558
+ }
2559
+ if (allSessions) {
2560
+ await revokeAllAuthSessions(c.get("config"), {
2561
+ userId: requestUser.sub,
2562
+ collection: this.collection.slug
2563
+ });
2564
+ return c.json({
2565
+ success: true,
2566
+ message: "All sessions have been logged out."
2567
+ });
2568
+ }
2569
+ if (tokenPayload?.sid) {
2570
+ await revokeAuthSession(c.get("config"), tokenPayload.sid);
2571
+ return c.json({
2572
+ success: true,
2573
+ message: "Logged out."
2574
+ });
2575
+ }
2576
+ return c.json({
2577
+ success: true,
2578
+ message: "Logged out. Discard your token."
2579
+ });
1929
2580
  }
1930
2581
  // ---------------------------------------------------------------------------
1931
2582
  // GET /me
@@ -1937,11 +2588,14 @@ var AuthController = class {
1937
2588
  if (!requestUser) {
1938
2589
  return c.json({ error: true, message: "Authentication required." }, 401);
1939
2590
  }
1940
- const user = await db.findOne({ collection: this.collection.slug, id: requestUser.sub });
2591
+ const user = await db.findOne({
2592
+ collection: this.collection.slug,
2593
+ id: requestUser.sub
2594
+ });
1941
2595
  if (!user) {
1942
2596
  return c.json({ error: true, message: "User not found." }, 404);
1943
2597
  }
1944
- const { password: _, ...safeUser } = user;
2598
+ const safeUser = this.sanitizeUser(user);
1945
2599
  return c.json(safeUser);
1946
2600
  }
1947
2601
  // ---------------------------------------------------------------------------
@@ -1952,10 +2606,29 @@ var AuthController = class {
1952
2606
  if (!requestUser) {
1953
2607
  return c.json({ error: true, message: "Authentication required." }, 401);
1954
2608
  }
1955
- const token = await signCollectionToken({
1956
- sub: requestUser.sub,
2609
+ if (!requestUser.email) {
2610
+ return c.json(
2611
+ { error: true, message: "Authenticated user is missing an email." },
2612
+ 400
2613
+ );
2614
+ }
2615
+ const tokenPayload = c.get("authTokenPayload");
2616
+ if (tokenPayload?.sid) {
2617
+ const token2 = await signCollectionToken({
2618
+ sub: requestUser.sub,
2619
+ email: requestUser.email,
2620
+ collection: this.collection.slug,
2621
+ sid: tokenPayload.sid
2622
+ });
2623
+ return c.json({ token: token2 });
2624
+ }
2625
+ const token = await issueAuthSessionToken({
2626
+ config: c.get("config"),
2627
+ userId: requestUser.sub,
1957
2628
  email: requestUser.email,
1958
- collection: this.collection.slug
2629
+ collection: this.collection.slug,
2630
+ ip: c.get("clientIp"),
2631
+ authSource: "local"
1959
2632
  });
1960
2633
  return c.json({ token });
1961
2634
  }
@@ -1980,16 +2653,28 @@ var AuthController = class {
1980
2653
  const user = result.docs[0];
1981
2654
  if (user) {
1982
2655
  const resetToken = await signCollectionToken(
1983
- { sub: user.id, email: user.email, collection: this.collection.slug, purpose: "reset" },
2656
+ {
2657
+ sub: user.id,
2658
+ email: user.email,
2659
+ collection: this.collection.slug,
2660
+ purpose: "reset"
2661
+ },
1984
2662
  "1h"
1985
2663
  );
1986
2664
  const resetUrl = body?.resetUrl;
1987
2665
  const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
1988
2666
  try {
1989
- const { subject, html } = buildResetPasswordEmail(config, { token: resetToken, url });
2667
+ const { subject, html } = buildResetPasswordEmail(config, {
2668
+ token: resetToken,
2669
+ url
2670
+ });
1990
2671
  await sendEmail(config, { to: user.email, subject, html });
1991
2672
  } catch (err) {
1992
- console.error("[dyrected/core] Failed to send password reset email:", err);
2673
+ getRequestLogger(c, "auth").error({
2674
+ err,
2675
+ msg: "Failed to send password reset email",
2676
+ email: user.email
2677
+ });
1993
2678
  }
1994
2679
  }
1995
2680
  return c.json({
@@ -2008,28 +2693,54 @@ var AuthController = class {
2008
2693
  if (!db) return c.json({ message: "Database not configured" }, 500);
2009
2694
  const body = await c.req.json().catch(() => null);
2010
2695
  if (!body?.token || !body?.password) {
2011
- return c.json({ error: true, message: "token and password are required." }, 400);
2696
+ return c.json(
2697
+ { error: true, message: "token and password are required." },
2698
+ 400
2699
+ );
2012
2700
  }
2013
2701
  let payload;
2014
2702
  try {
2015
2703
  payload = await verifyCollectionToken(body.token);
2016
2704
  } catch {
2017
- return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
2705
+ return c.json(
2706
+ { error: true, message: "Reset token is invalid or has expired." },
2707
+ 400
2708
+ );
2018
2709
  }
2019
2710
  if (payload.collection !== this.collection.slug || payload.purpose !== "reset") {
2020
- return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
2711
+ return c.json(
2712
+ { error: true, message: "Reset token is invalid or has expired." },
2713
+ 400
2714
+ );
2021
2715
  }
2022
2716
  const hashedPassword = await hashPassword(body.password);
2023
2717
  await db.update({
2024
2718
  collection: this.collection.slug,
2025
2719
  id: payload.sub,
2026
- data: { password: hashedPassword }
2720
+ data: {
2721
+ password: hashedPassword,
2722
+ loginAttempts: 0,
2723
+ lockedUntil: null
2724
+ }
2725
+ });
2726
+ await revokeAllAuthSessions(config, {
2727
+ userId: payload.sub,
2728
+ collection: this.collection.slug
2729
+ });
2730
+ const { subject, html } = buildPasswordChangedEmail(config, {
2731
+ email: payload.email
2027
2732
  });
2028
- const { subject, html } = buildPasswordChangedEmail(config, { email: payload.email });
2029
2733
  sendEmail(config, { to: payload.email, subject, html }).catch(
2030
- (err) => console.error("[dyrected/core] Failed to send password-changed email:", err)
2734
+ (err) => getRequestLogger(c, "auth").error({
2735
+ err,
2736
+ msg: "Failed to send password-changed email",
2737
+ email: payload.email
2738
+ })
2031
2739
  );
2032
- return c.json({ success: true, message: "Password has been reset. You can now log in." });
2740
+ return c.json({
2741
+ success: true,
2742
+ message: "Password has been reset. You can now log in."
2743
+ });
2033
2744
  }
2034
2745
  // ---------------------------------------------------------------------------
2035
2746
  // POST /invite
@@ -2053,10 +2764,18 @@ var AuthController = class {
2053
2764
  limit: 1
2054
2765
  });
2055
2766
  if (existing.total > 0) {
2056
- return c.json({ error: true, message: "An account with that email already exists." }, 409);
2767
+ return c.json(
2768
+ { error: true, message: "An account with that email already exists." },
2769
+ 409
2770
+ );
2057
2771
  }
2058
2772
  const inviteToken = await signCollectionToken(
2059
- { sub: body.email, email: body.email, collection: this.collection.slug, purpose: "invite" },
2773
+ {
2774
+ sub: body.email,
2775
+ email: body.email,
2776
+ collection: this.collection.slug,
2777
+ purpose: "invite"
2778
+ },
2060
2779
  "7d"
2061
2780
  );
2062
2781
  try {
@@ -2066,7 +2785,11 @@ var AuthController = class {
2066
2785
  });
2067
2786
  await sendEmail(config, { to: body.email, subject, html });
2068
2787
  } catch (err) {
2069
- console.error("[dyrected/core] Failed to send invite email:", err);
2788
+ getRequestLogger(c, "auth").error({
2789
+ err,
2790
+ msg: "Failed to send invite email",
2791
+ email: body.email
2792
+ });
2070
2793
  }
2071
2794
  return c.json({ success: true, message: `Invite sent to ${body.email}.` });
2072
2795
  }
@@ -2081,16 +2804,25 @@ var AuthController = class {
2081
2804
  if (!db) return c.json({ message: "Database not configured" }, 500);
2082
2805
  const body = await c.req.json().catch(() => null);
2083
2806
  if (!body?.token || !body?.password) {
2084
- return c.json({ error: true, message: "token and password are required." }, 400);
2807
+ return c.json(
2808
+ { error: true, message: "token and password are required." },
2809
+ 400
2810
+ );
2085
2811
  }
2086
2812
  let payload;
2087
2813
  try {
2088
2814
  payload = await verifyCollectionToken(body.token);
2089
2815
  } catch {
2090
- return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
2816
+ return c.json(
2817
+ { error: true, message: "Invite token is invalid or has expired." },
2818
+ 400
2819
+ );
2091
2820
  }
2092
2821
  if (payload.collection !== this.collection.slug || payload.purpose !== "invite") {
2093
- return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
2822
+ return c.json(
2823
+ { error: true, message: "Invite token is invalid or has expired." },
2824
+ 400
2825
+ );
2094
2826
  }
2095
2827
  const inviteeEmail = payload.sub;
2096
2828
  const existing = await db.find({
@@ -2099,7 +2831,10 @@ var AuthController = class {
2099
2831
  limit: 1
2100
2832
  });
2101
2833
  if (existing.total > 0) {
2102
- return c.json({ error: true, message: "An account with that email already exists." }, 409);
2834
+ return c.json(
2835
+ { error: true, message: "An account with that email already exists." },
2836
+ 409
2837
+ );
2103
2838
  }
2104
2839
  const { token: _t, password: _p, ...extraFields } = body;
2105
2840
  const hashedPassword = await hashPassword(body.password);
@@ -2107,28 +2842,37 @@ var AuthController = class {
2107
2842
  collection: this.collection.slug,
2108
2843
  data: { ...extraFields, email: inviteeEmail, password: hashedPassword }
2109
2844
  });
2110
- const sessionToken = await signCollectionToken({
2111
- sub: user.id,
2845
+ const sessionToken = await issueAuthSessionToken({
2846
+ config,
2847
+ userId: user.id,
2112
2848
  email: inviteeEmail,
2113
- collection: this.collection.slug
2849
+ collection: this.collection.slug,
2850
+ ip: c.get("clientIp"),
2851
+ authSource: "local"
2852
+ });
2853
+ const { subject, html } = buildWelcomeEmail(config, {
2854
+ email: inviteeEmail
2114
2855
  });
2115
- const { subject, html } = buildWelcomeEmail(config, { email: inviteeEmail });
2116
2856
  sendEmail(config, { to: inviteeEmail, subject, html }).catch(
2117
- (err) => console.error("[dyrected/core] Failed to send welcome email:", err)
2857
+ (err) => getRequestLogger(c, "auth").error({
2858
+ err,
2859
+ msg: "Failed to send welcome email",
2860
+ email: inviteeEmail
2861
+ })
2118
2862
  );
2119
- const { password: _, ...safeUser } = user;
2863
+ const safeUser = this.sanitizeUser(user);
2120
2864
  return c.json({ token: sessionToken, user: safeUser }, 201);
2121
2865
  }
2122
2866
  };
2123
2867
 
2124
2868
  // src/controllers/admin-auth.controller.ts
2125
2869
  import { randomBytes as randomBytes2 } from "crypto";
2126
- import { TextEncoder as TextEncoder2 } from "util";
2870
+ import { TextEncoder } from "util";
2127
2871
  import {
2128
- SignJWT as SignJWT2,
2872
+ SignJWT,
2129
2873
  createRemoteJWKSet,
2130
- jwtVerify as jwtVerify2,
2131
- decodeJwt as decodeJwt2
2874
+ jwtVerify,
2875
+ decodeJwt
2132
2876
  } from "jose";
2133
2877
  var AdminAuthController = class {
2134
2878
  constructor(config) {
@@ -2137,13 +2881,21 @@ var AdminAuthController = class {
2137
2881
  config;
2138
2882
  async providers(c) {
2139
2883
  const requestConfig = await this.getRequestConfig(c);
2140
- return c.json(getPublicAdminAuthConfig(requestConfig.adminAuth, requestConfig.collections));
2884
+ return c.json(
2885
+ getPublicAdminAuthConfig(
2886
+ requestConfig.adminAuth,
2887
+ requestConfig.collections
2888
+ )
2889
+ );
2141
2890
  }
2142
2891
  async start(c) {
2143
2892
  const requestConfig = await this.getRequestConfig(c);
2144
2893
  const provider = this.getProvider(requestConfig, c.req.param("provider"));
2145
2894
  if (!provider) {
2146
- return c.json({ error: true, message: "Admin auth provider not found." }, 404);
2895
+ return c.json(
2896
+ { error: true, message: "Admin auth provider not found." },
2897
+ 404
2898
+ );
2147
2899
  }
2148
2900
  const siteId = this.getSiteId(c);
2149
2901
  const returnTo = this.normalizeReturnTo(c.req.query("returnTo"), c);
@@ -2157,9 +2909,15 @@ var AdminAuthController = class {
2157
2909
  return c.redirect(redirectUrl, 302);
2158
2910
  }
2159
2911
  if (!provider.startUrl) {
2160
- return c.json({ error: true, message: "This provider does not expose a start URL." }, 400);
2912
+ return c.json(
2913
+ { error: true, message: "This provider does not expose a start URL." },
2914
+ 400
2915
+ );
2161
2916
  }
2162
- const url = this.buildCustomProviderStartUrl(provider.startUrl, new URL(c.req.url).origin);
2917
+ const url = this.buildCustomProviderStartUrl(
2918
+ provider.startUrl,
2919
+ new URL(c.req.url).origin
2920
+ );
2163
2921
  if (!url) {
2164
2922
  return c.json(
2165
2923
  {
@@ -2178,13 +2936,23 @@ var AdminAuthController = class {
2178
2936
  const requestConfig = await this.getRequestConfig(c);
2179
2937
  const provider = this.getProvider(requestConfig, c.req.param("provider"));
2180
2938
  if (!provider) {
2181
- return c.json({ error: true, message: "Admin auth provider not found." }, 404);
2939
+ return c.json(
2940
+ { error: true, message: "Admin auth provider not found." },
2941
+ 404
2942
+ );
2182
2943
  }
2183
2944
  try {
2184
- const exchange = await this.completeProviderAuth(requestConfig, provider, c);
2945
+ const exchange = await this.completeProviderAuth(
2946
+ requestConfig,
2947
+ provider,
2948
+ c
2949
+ );
2185
2950
  const redirectUrl = new URL(exchange.returnTo);
2186
2951
  redirectUrl.searchParams.set("dyrectedExternalToken", exchange.token);
2187
- redirectUrl.searchParams.set("dyrectedAdminCollection", exchange.collectionSlug);
2952
+ redirectUrl.searchParams.set(
2953
+ "dyrectedAdminCollection",
2954
+ exchange.collectionSlug
2955
+ );
2188
2956
  return c.redirect(redirectUrl.toString(), 302);
2189
2957
  } catch (error) {
2190
2958
  const message = error instanceof Error ? error.message : "External authentication failed.";
@@ -2198,10 +2966,17 @@ var AdminAuthController = class {
2198
2966
  const requestConfig = await this.getRequestConfig(c);
2199
2967
  const provider = this.getProvider(requestConfig, c.req.param("provider"));
2200
2968
  if (!provider) {
2201
- return c.json({ error: true, message: "Admin auth provider not found." }, 404);
2969
+ return c.json(
2970
+ { error: true, message: "Admin auth provider not found." },
2971
+ 404
2972
+ );
2202
2973
  }
2203
2974
  try {
2204
- const exchange = await this.completeProviderAuth(requestConfig, provider, c);
2975
+ const exchange = await this.completeProviderAuth(
2976
+ requestConfig,
2977
+ provider,
2978
+ c
2979
+ );
2205
2980
  return c.json({
2206
2981
  token: exchange.token,
2207
2982
  collectionSlug: exchange.collectionSlug,
@@ -2212,9 +2987,46 @@ var AdminAuthController = class {
2212
2987
  const status = message === "Access denied for this site." ? 403 : 401;
2213
2988
  return c.json({ error: true, message }, status);
2214
2989
  }
2215
- }
2216
- async logout(c) {
2217
- return c.json({ success: true, message: "Logged out. Discard your token." });
2990
+ }
2991
+ async logout(c) {
2992
+ const requestUser = c.get("user");
2993
+ const tokenPayload = c.get("authTokenPayload");
2994
+ const allSessions = ["1", "true", "yes"].includes(
2995
+ (c.req.query("allSessions") || "").toLowerCase()
2996
+ );
2997
+ if (!requestUser) {
2998
+ if (allSessions) {
2999
+ return c.json(
3000
+ { error: true, message: "Authentication required." },
3001
+ 401
3002
+ );
3003
+ }
3004
+ return c.json({
3005
+ success: true,
3006
+ message: "Logged out. Discard your token."
3007
+ });
3008
+ }
3009
+ if (allSessions) {
3010
+ await revokeAllAuthSessions(c.get("config"), {
3011
+ userId: requestUser.sub,
3012
+ collection: requestUser.collection
3013
+ });
3014
+ return c.json({
3015
+ success: true,
3016
+ message: "All sessions have been logged out."
3017
+ });
3018
+ }
3019
+ if (tokenPayload?.sid) {
3020
+ await revokeAuthSession(c.get("config"), tokenPayload.sid);
3021
+ return c.json({
3022
+ success: true,
3023
+ message: "Logged out."
3024
+ });
3025
+ }
3026
+ return c.json({
3027
+ success: true,
3028
+ message: "Logged out. Discard your token."
3029
+ });
2218
3030
  }
2219
3031
  getProvider(config, id) {
2220
3032
  if (config.adminAuth?.mode !== "external") return void 0;
@@ -2230,7 +3042,12 @@ var AdminAuthController = class {
2230
3042
  }
2231
3043
  const db = c.get("config").db;
2232
3044
  if (!db) throw new Error("Database not configured.");
2233
- let user = await this.findUserByExternalIdentity(db, adminCollection.slug, provider.id, identity.sub) ?? (identity.email ? (await db.find({
3045
+ let user = await this.findUserByExternalIdentity(
3046
+ db,
3047
+ adminCollection.slug,
3048
+ provider.id,
3049
+ identity.sub
3050
+ ) ?? (identity.email ? (await db.find({
2234
3051
  collection: adminCollection.slug,
2235
3052
  where: { email: identity.email },
2236
3053
  limit: 1
@@ -2238,9 +3055,18 @@ var AdminAuthController = class {
2238
3055
  const provisioningMode = requestConfig.adminAuth?.provisioningMode ?? "jit_plus_membership_management";
2239
3056
  const allowJitProvisioning = provisioningMode !== "preprovisioned_only" && provider.allowJitProvisioning !== false;
2240
3057
  if (!user && !allowJitProvisioning) {
2241
- throw new Error("This account has not been provisioned for admin access.");
3058
+ throw new Error(
3059
+ "This account has not been provisioned for admin access."
3060
+ );
2242
3061
  }
2243
- const access = await this.resolveAccess(requestConfig, identity, provider.id, siteId, c, user);
3062
+ const access = await this.resolveAccess(
3063
+ requestConfig,
3064
+ identity,
3065
+ provider.id,
3066
+ siteId,
3067
+ c,
3068
+ user
3069
+ );
2244
3070
  if (!access.allowed) {
2245
3071
  throw new Error("Access denied for this site.");
2246
3072
  }
@@ -2248,7 +3074,12 @@ var AdminAuthController = class {
2248
3074
  user = await db.create({
2249
3075
  collection: adminCollection.slug,
2250
3076
  data: {
2251
- ...await this.buildUserData(identity, provider.id, access.roles, access.data),
3077
+ ...await this.buildUserData(
3078
+ identity,
3079
+ provider.id,
3080
+ access.roles,
3081
+ access.data
3082
+ ),
2252
3083
  password: await hashPassword(randomBytes2(32).toString("hex"))
2253
3084
  }
2254
3085
  });
@@ -2256,18 +3087,25 @@ var AdminAuthController = class {
2256
3087
  user = await db.update({
2257
3088
  collection: adminCollection.slug,
2258
3089
  id: user.id,
2259
- data: await this.buildUserData(identity, provider.id, access.roles, access.data)
3090
+ data: await this.buildUserData(
3091
+ identity,
3092
+ provider.id,
3093
+ access.roles,
3094
+ access.data
3095
+ )
2260
3096
  });
2261
3097
  }
2262
3098
  if (!user?.email) {
2263
3099
  throw new Error("Authenticated admin user is missing an email address.");
2264
3100
  }
2265
- const token = await signCollectionToken({
2266
- sub: user.id,
3101
+ const token = await issueAuthSessionToken({
3102
+ config: c.get("config"),
3103
+ userId: user.id,
2267
3104
  email: user.email,
2268
3105
  collection: adminCollection.slug,
2269
3106
  providerId: provider.id,
2270
- authSource: "external"
3107
+ authSource: "external",
3108
+ ip: c.get("clientIp")
2271
3109
  });
2272
3110
  return {
2273
3111
  token,
@@ -2321,14 +3159,10 @@ var AdminAuthController = class {
2321
3159
  async getRequestConfig(c) {
2322
3160
  const siteId = this.getSiteId(c);
2323
3161
  if (!siteId || !this.config.onSchemaFetch) return this.config;
2324
- const dynamic = await this.config.onSchemaFetch(siteId);
2325
- return {
2326
- ...this.config,
2327
- ...dynamic.admin ? { admin: { ...this.config.admin, ...dynamic.admin } } : {},
2328
- ...dynamic.adminAuth ? { adminAuth: { ...this.config.adminAuth, ...dynamic.adminAuth } } : {},
2329
- collections: dynamic.collections ? [...this.config.collections, ...dynamic.collections] : this.config.collections,
2330
- globals: dynamic.globals ? [...this.config.globals, ...dynamic.globals] : this.config.globals
2331
- };
3162
+ return mergeDynamicConfig(
3163
+ this.config,
3164
+ await this.config.onSchemaFetch(siteId)
3165
+ );
2332
3166
  }
2333
3167
  async buildOidcStartUrl(provider, returnTo, siteId, origin) {
2334
3168
  const discovery = await this.getOidcDiscovery(provider);
@@ -2339,11 +3173,19 @@ var AdminAuthController = class {
2339
3173
  siteId,
2340
3174
  nonce
2341
3175
  });
2342
- const url = new URL(provider.authorizationEndpoint || discovery.authorization_endpoint);
3176
+ const url = new URL(
3177
+ provider.authorizationEndpoint || discovery.authorization_endpoint
3178
+ );
2343
3179
  url.searchParams.set("response_type", "code");
2344
3180
  url.searchParams.set("client_id", provider.clientId);
2345
- url.searchParams.set("redirect_uri", provider.redirectUri || this.defaultCallbackPath(provider.id, origin));
2346
- url.searchParams.set("scope", (provider.scopes ?? ["openid", "profile", "email"]).join(" "));
3181
+ url.searchParams.set(
3182
+ "redirect_uri",
3183
+ provider.redirectUri || this.defaultCallbackPath(provider.id, origin)
3184
+ );
3185
+ url.searchParams.set(
3186
+ "scope",
3187
+ (provider.scopes ?? ["openid", "profile", "email"]).join(" ")
3188
+ );
2347
3189
  url.searchParams.set("state", state);
2348
3190
  url.searchParams.set("nonce", nonce);
2349
3191
  return url.toString();
@@ -2355,26 +3197,29 @@ var AdminAuthController = class {
2355
3197
  const parsedState = await this.verifyState(state, provider.id);
2356
3198
  const discovery = await this.getOidcDiscovery(provider);
2357
3199
  const redirectUri = provider.redirectUri || this.defaultCallbackPath(provider.id, new URL(c.req.url).origin);
2358
- const tokenResponse = await fetch(provider.tokenEndpoint || discovery.token_endpoint, {
2359
- method: "POST",
2360
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2361
- body: new URLSearchParams({
2362
- grant_type: "authorization_code",
2363
- code,
2364
- redirect_uri: redirectUri,
2365
- client_id: provider.clientId,
2366
- client_secret: provider.clientSecret
2367
- })
2368
- });
3200
+ const tokenResponse = await fetch(
3201
+ provider.tokenEndpoint || discovery.token_endpoint,
3202
+ {
3203
+ method: "POST",
3204
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
3205
+ body: new URLSearchParams({
3206
+ grant_type: "authorization_code",
3207
+ code,
3208
+ redirect_uri: redirectUri,
3209
+ client_id: provider.clientId,
3210
+ client_secret: provider.clientSecret
3211
+ })
3212
+ }
3213
+ );
2369
3214
  if (!tokenResponse.ok) {
2370
3215
  throw new Error("OIDC token exchange failed.");
2371
3216
  }
2372
3217
  const tokenBody = await tokenResponse.json();
2373
3218
  const idToken = tokenBody.id_token;
2374
- let claims = {};
3219
+ let claims;
2375
3220
  if (idToken) {
2376
3221
  const jwks = createRemoteJWKSet(new URL(discovery.jwks_uri));
2377
- const { payload } = await jwtVerify2(idToken, jwks, {
3222
+ const { payload } = await jwtVerify(idToken, jwks, {
2378
3223
  issuer: provider.issuer,
2379
3224
  audience: provider.clientId
2380
3225
  });
@@ -2383,9 +3228,12 @@ var AdminAuthController = class {
2383
3228
  }
2384
3229
  claims = payload;
2385
3230
  } else if (tokenBody.access_token && (provider.userInfoEndpoint || discovery.userinfo_endpoint)) {
2386
- const userInfo = await fetch(provider.userInfoEndpoint || discovery.userinfo_endpoint, {
2387
- headers: { Authorization: `Bearer ${tokenBody.access_token}` }
2388
- });
3231
+ const userInfo = await fetch(
3232
+ provider.userInfoEndpoint || discovery.userinfo_endpoint,
3233
+ {
3234
+ headers: { Authorization: `Bearer ${tokenBody.access_token}` }
3235
+ }
3236
+ );
2389
3237
  if (!userInfo.ok) throw new Error("OIDC userinfo request failed.");
2390
3238
  claims = await userInfo.json();
2391
3239
  } else {
@@ -2405,21 +3253,21 @@ var AdminAuthController = class {
2405
3253
  }
2406
3254
  let claims;
2407
3255
  if (provider.secret) {
2408
- const secret = new TextEncoder2().encode(provider.secret);
2409
- const verified = await jwtVerify2(token, secret, {
3256
+ const secret = new TextEncoder().encode(provider.secret);
3257
+ const verified = await jwtVerify(token, secret, {
2410
3258
  issuer: provider.issuer,
2411
3259
  audience: provider.audience
2412
3260
  });
2413
3261
  claims = verified.payload;
2414
3262
  } else if (provider.jwksUri) {
2415
3263
  const jwks = createRemoteJWKSet(new URL(provider.jwksUri));
2416
- const verified = await jwtVerify2(token, jwks, {
3264
+ const verified = await jwtVerify(token, jwks, {
2417
3265
  issuer: provider.issuer,
2418
3266
  audience: provider.audience
2419
3267
  });
2420
3268
  claims = verified.payload;
2421
3269
  } else {
2422
- claims = decodeJwt2(token);
3270
+ claims = decodeJwt(token);
2423
3271
  if (provider.issuer && claims.iss !== provider.issuer) {
2424
3272
  throw new Error("External auth issuer mismatch.");
2425
3273
  }
@@ -2430,7 +3278,10 @@ var AdminAuthController = class {
2430
3278
  }
2431
3279
  }
2432
3280
  return {
2433
- identity: this.mapClaims(claims, provider.claimMapping),
3281
+ identity: this.mapClaims(
3282
+ claims,
3283
+ provider.claimMapping
3284
+ ),
2434
3285
  returnTo: this.normalizeReturnTo(
2435
3286
  body?.returnTo || c.req.query("returnTo"),
2436
3287
  c
@@ -2454,9 +3305,15 @@ var AdminAuthController = class {
2454
3305
  email: this.readStringClaim(claims[emailKey]),
2455
3306
  name: this.readStringClaim(claims[nameKey]),
2456
3307
  roles: this.readStringArrayClaim(rolesKey ? claims[rolesKey] : void 0),
2457
- groups: this.readStringArrayClaim(groupsKey ? claims[groupsKey] : void 0),
2458
- siteIds: this.readStringArrayClaim(siteIdsKey ? claims[siteIdsKey] : void 0),
2459
- workspaceIds: this.readStringArrayClaim(workspaceIdsKey ? claims[workspaceIdsKey] : void 0),
3308
+ groups: this.readStringArrayClaim(
3309
+ groupsKey ? claims[groupsKey] : void 0
3310
+ ),
3311
+ siteIds: this.readStringArrayClaim(
3312
+ siteIdsKey ? claims[siteIdsKey] : void 0
3313
+ ),
3314
+ workspaceIds: this.readStringArrayClaim(
3315
+ workspaceIdsKey ? claims[workspaceIdsKey] : void 0
3316
+ ),
2460
3317
  rawClaims: claims
2461
3318
  };
2462
3319
  }
@@ -2466,7 +3323,9 @@ var AdminAuthController = class {
2466
3323
  readStringArrayClaim(value) {
2467
3324
  if (!value) return void 0;
2468
3325
  if (Array.isArray(value)) {
2469
- return value.filter((entry) => typeof entry === "string");
3326
+ return value.filter(
3327
+ (entry) => typeof entry === "string"
3328
+ );
2470
3329
  }
2471
3330
  if (typeof value === "string") return [value];
2472
3331
  return void 0;
@@ -2476,7 +3335,8 @@ var AdminAuthController = class {
2476
3335
  provider.issuer.replace(/\/$/, "") + "/.well-known/openid-configuration"
2477
3336
  );
2478
3337
  const response = await fetch(discoveryUrl.toString());
2479
- if (!response.ok) throw new Error("Failed to load OIDC discovery document.");
3338
+ if (!response.ok)
3339
+ throw new Error("Failed to load OIDC discovery document.");
2480
3340
  return response.json();
2481
3341
  }
2482
3342
  defaultCallbackPath(providerId, origin = "") {
@@ -2494,10 +3354,10 @@ var AdminAuthController = class {
2494
3354
  }
2495
3355
  }
2496
3356
  async signState(payload) {
2497
- return new SignJWT2(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getStateSecret());
3357
+ return new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getStateSecret());
2498
3358
  }
2499
3359
  async verifyState(token, providerId) {
2500
- const { payload } = await jwtVerify2(token, this.getStateSecret());
3360
+ const { payload } = await jwtVerify(token, this.getStateSecret());
2501
3361
  const state = payload;
2502
3362
  if (state.providerId !== providerId) {
2503
3363
  throw new Error("OIDC state verification failed.");
@@ -2509,7 +3369,7 @@ var AdminAuthController = class {
2509
3369
  if (!secret) {
2510
3370
  throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
2511
3371
  }
2512
- return new TextEncoder2().encode(secret);
3372
+ return new TextEncoder().encode(secret);
2513
3373
  }
2514
3374
  normalizeReturnTo(returnTo, c) {
2515
3375
  if (returnTo) return returnTo;
@@ -2519,12 +3379,12 @@ var AdminAuthController = class {
2519
3379
  };
2520
3380
 
2521
3381
  // src/controllers/preview.controller.ts
2522
- import { SignJWT as SignJWT3, jwtVerify as jwtVerify3 } from "jose";
2523
- import { TextEncoder as TextEncoder3 } from "util";
3382
+ import { SignJWT as SignJWT2, jwtVerify as jwtVerify2 } from "jose";
3383
+ import { TextEncoder as TextEncoder2 } from "util";
2524
3384
  var PreviewController = class {
2525
3385
  getSecret() {
2526
3386
  const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
2527
- return new TextEncoder3().encode(secret);
3387
+ return new TextEncoder2().encode(secret);
2528
3388
  }
2529
3389
  /**
2530
3390
  * POST /api/preview-token
@@ -2533,9 +3393,12 @@ var PreviewController = class {
2533
3393
  async createToken(c) {
2534
3394
  const body = await c.req.json().catch(() => null);
2535
3395
  if (!body?.collectionSlug || !body?.data) {
2536
- return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
3396
+ return c.json(
3397
+ { error: true, message: "collectionSlug and data are required." },
3398
+ 400
3399
+ );
2537
3400
  }
2538
- const token = await new SignJWT3({
3401
+ const token = await new SignJWT2({
2539
3402
  collectionSlug: body.collectionSlug,
2540
3403
  documentId: body.documentId,
2541
3404
  data: body.data
@@ -2550,13 +3413,19 @@ var PreviewController = class {
2550
3413
  async getData(c) {
2551
3414
  const token = c.req.query("token");
2552
3415
  if (!token) {
2553
- return c.json({ error: true, message: "token query parameter is required." }, 400);
3416
+ return c.json(
3417
+ { error: true, message: "token query parameter is required." },
3418
+ 400
3419
+ );
2554
3420
  }
2555
3421
  try {
2556
- const { payload } = await jwtVerify3(token, this.getSecret());
3422
+ const { payload } = await jwtVerify2(token, this.getSecret());
2557
3423
  return c.json(payload);
2558
- } catch (err) {
2559
- return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
3424
+ } catch (_err) {
3425
+ return c.json(
3426
+ { error: true, message: "Invalid or expired preview token." },
3427
+ 401
3428
+ );
2560
3429
  }
2561
3430
  }
2562
3431
  };
@@ -2632,9 +3501,15 @@ var AuditController = class {
2632
3501
  if (!access.allowed) {
2633
3502
  return { denied: true, where: {} };
2634
3503
  }
2635
- let where = { collection: { equals: collection.slug } };
3504
+ let where = {
3505
+ collection: { equals: collection.slug }
3506
+ };
2636
3507
  if (access.constraint) {
2637
- const ids = await this.collectAccessibleDocumentIds(config.db, collection, access.constraint);
3508
+ const ids = await this.collectAccessibleDocumentIds(
3509
+ config.db,
3510
+ collection,
3511
+ access.constraint
3512
+ );
2638
3513
  where = mergeWhereConstraint(where, { documentId: { in: ids } });
2639
3514
  }
2640
3515
  return { denied: false, where };
@@ -2647,8 +3522,11 @@ var AuditController = class {
2647
3522
  }
2648
3523
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2649
3524
  if (config.onSchemaFetch && siteId) {
2650
- const dynamic = await config.onSchemaFetch(siteId);
2651
- for (const collection of dynamic.collections || []) {
3525
+ const requestConfig = mergeDynamicConfig(
3526
+ config,
3527
+ await config.onSchemaFetch(siteId)
3528
+ );
3529
+ for (const collection of requestConfig.collections) {
2652
3530
  if (!collections.has(collection.slug)) {
2653
3531
  collections.set(collection.slug, collection);
2654
3532
  }
@@ -2659,11 +3537,18 @@ var AuditController = class {
2659
3537
  async findForCollection(c, collection) {
2660
3538
  const config = c.get("config");
2661
3539
  if (!config.db) return c.json({ message: "Database not configured" }, 500);
2662
- if (!collection.audit) return c.json({ message: "Audit is not enabled for this collection" }, 404);
3540
+ if (!collection.audit)
3541
+ return c.json(
3542
+ { message: "Audit is not enabled for this collection" },
3543
+ 404
3544
+ );
2663
3545
  const query = this.parseQuery(c);
2664
3546
  const scope = await this.buildCollectionAuditScope(c, collection);
2665
3547
  if (scope.denied) {
2666
- return c.json({ error: true, message: `Access denied: read on ${collection.slug}` }, 403);
3548
+ return c.json(
3549
+ { error: true, message: `Access denied: read on ${collection.slug}` },
3550
+ 403
3551
+ );
2667
3552
  }
2668
3553
  const where = query.where ? mergeWhereConstraint(scope.where, query.where) : scope.where;
2669
3554
  const result = await config.db.find({
@@ -2679,7 +3564,9 @@ var AuditController = class {
2679
3564
  const config = c.get("config");
2680
3565
  if (!config.db) return c.json({ message: "Database not configured" }, 500);
2681
3566
  const query = this.parseQuery(c);
2682
- const collections = (await this.getVisibleCollections(c)).filter((collection) => collection.audit);
3567
+ const collections = (await this.getVisibleCollections(c)).filter(
3568
+ (collection) => collection.audit
3569
+ );
2683
3570
  const scopes = [];
2684
3571
  for (const collection of collections) {
2685
3572
  const scope = await this.buildCollectionAuditScope(c, collection);
@@ -2710,8 +3597,7 @@ function getBearerToken(c) {
2710
3597
  const authHeader = c.req.header("Authorization");
2711
3598
  return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
2712
3599
  }
2713
- async function resolveUser(token, config) {
2714
- const payload = await verifyCollectionToken(token);
3600
+ async function resolveUser(payload, config) {
2715
3601
  if (payload.purpose) {
2716
3602
  return payload;
2717
3603
  }
@@ -2726,7 +3612,12 @@ async function resolveUser(token, config) {
2726
3612
  id: payload.sub
2727
3613
  });
2728
3614
  } catch (err) {
2729
- console.error("[dyrected/core] Failed to hydrate user from token:", err);
3615
+ getConfigLogger(config, "auth").error({
3616
+ err,
3617
+ msg: "Failed to hydrate user from token",
3618
+ collection: payload.collection,
3619
+ userId: payload.sub
3620
+ });
2730
3621
  return payload;
2731
3622
  }
2732
3623
  if (!doc) {
@@ -2740,6 +3631,18 @@ async function resolveUser(token, config) {
2740
3631
  collection: payload.collection
2741
3632
  };
2742
3633
  }
3634
+ async function resolveAuthenticatedRequest(token, config, clientIp) {
3635
+ const payload = await verifyCollectionToken(token);
3636
+ if (payload.sid && config?.db) {
3637
+ const session = await getAuthSession(config, payload.sid);
3638
+ if (!isAuthSessionActive(session) || session.userId !== payload.sub || session.collection !== payload.collection) {
3639
+ throw new Error("Invalid or expired session.");
3640
+ }
3641
+ await touchAuthSession(config, payload.sid, { ip: clientIp });
3642
+ }
3643
+ const user = await resolveUser(payload, config);
3644
+ return { user, payload };
3645
+ }
2743
3646
  function requireAuth(config) {
2744
3647
  return async (c, next) => {
2745
3648
  if (c.get("user")) {
@@ -2747,18 +3650,42 @@ function requireAuth(config) {
2747
3650
  }
2748
3651
  const token = getBearerToken(c);
2749
3652
  if (!token) {
3653
+ c.get("observability")?.recordAuthFailure({
3654
+ reason: "missing_token",
3655
+ path: c.req.path
3656
+ });
2750
3657
  return c.json({ error: true, message: "Authentication required." }, 401);
2751
3658
  }
2752
3659
  let user;
3660
+ let payload;
2753
3661
  try {
2754
- user = await resolveUser(token, config ?? c.get("config"));
2755
- } catch {
3662
+ const resolved = await resolveAuthenticatedRequest(
3663
+ token,
3664
+ config ?? c.get("config"),
3665
+ c.get("clientIp")
3666
+ );
3667
+ user = resolved.user;
3668
+ payload = resolved.payload;
3669
+ } catch (err) {
3670
+ c.get("observability")?.recordAuthFailure({
3671
+ reason: "invalid_token",
3672
+ path: c.req.path
3673
+ });
3674
+ getRequestLogger(c, "auth").warn({
3675
+ err,
3676
+ msg: "Rejected invalid or expired token"
3677
+ });
2756
3678
  return c.json({ error: true, message: "Invalid or expired token." }, 401);
2757
3679
  }
2758
3680
  if (!user) {
3681
+ c.get("observability")?.recordAuthFailure({
3682
+ reason: "missing_user",
3683
+ path: c.req.path
3684
+ });
2759
3685
  return c.json({ error: true, message: "Invalid or expired token." }, 401);
2760
3686
  }
2761
3687
  c.set("user", user);
3688
+ c.set("authTokenPayload", payload);
2762
3689
  await next();
2763
3690
  };
2764
3691
  }
@@ -2770,10 +3697,16 @@ function optionalAuth(config) {
2770
3697
  const token = getBearerToken(c);
2771
3698
  if (token) {
2772
3699
  try {
2773
- const user = await resolveUser(token, config ?? c.get("config"));
3700
+ const resolved = await resolveAuthenticatedRequest(
3701
+ token,
3702
+ config ?? c.get("config"),
3703
+ c.get("clientIp")
3704
+ );
3705
+ const user = resolved.user;
2774
3706
  if (user) {
2775
3707
  c.set("user", user);
2776
3708
  }
3709
+ c.set("authTokenPayload", resolved.payload);
2777
3710
  } catch {
2778
3711
  }
2779
3712
  }
@@ -2848,7 +3781,10 @@ function accessGate(config, target, action) {
2848
3781
  req: toHookRequestContext(c.req)
2849
3782
  });
2850
3783
  if (!allowed) {
2851
- return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
3784
+ return c.json(
3785
+ { error: true, message: `Access denied: ${action} on ${target.slug}` },
3786
+ 403
3787
+ );
2852
3788
  }
2853
3789
  await next();
2854
3790
  };
@@ -2872,226 +3808,276 @@ function serializeFieldForApi(f) {
2872
3808
  if (serialized.fields) {
2873
3809
  serialized.fields = serialized.fields.map(serializeFieldForApi);
2874
3810
  }
2875
- if (serialized.blocks) {
3811
+ if (serialized.blocks && !serialized.blockReferences?.length) {
2876
3812
  serialized.blocks = serialized.blocks.map((b) => ({
2877
3813
  ...b,
2878
3814
  fields: b.fields?.map(serializeFieldForApi)
2879
3815
  }));
3816
+ } else if (serialized.blockReferences?.length) {
3817
+ delete serialized.blocks;
2880
3818
  }
2881
3819
  return serialized;
2882
3820
  }
3821
+ function serializeBlockForApi(block) {
3822
+ return {
3823
+ ...block,
3824
+ fields: block.fields?.map(serializeFieldForApi)
3825
+ };
3826
+ }
2883
3827
  function registerRoutes(app, config) {
2884
3828
  const optionsCache = /* @__PURE__ */ new Map();
2885
3829
  app.get("/api/schemas", optionalAuth(config), async (c) => {
2886
3830
  const siteId = c.req.header("X-Site-Id");
2887
- let collections = [...config.collections];
2888
- let globals = [...config.globals];
2889
- if (siteId && config.onSchemaFetch) {
2890
- const dynamic = await config.onSchemaFetch(siteId);
2891
- if (dynamic.collections) collections = [...collections, ...dynamic.collections];
2892
- if (dynamic.globals) globals = [...globals, ...dynamic.globals];
2893
- if (dynamic.admin) {
2894
- config.admin = { ...config.admin, ...dynamic.admin };
2895
- }
2896
- if (dynamic.adminAuth) {
2897
- config.adminAuth = { ...config.adminAuth, ...dynamic.adminAuth };
2898
- }
2899
- }
3831
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
3832
+ const collections = [...requestConfig.collections];
3833
+ const globals = [...requestConfig.globals];
2900
3834
  const user = c.get("user");
2901
3835
  const accessArgs = { user, req: toHookRequestContext(c.req) };
2902
3836
  const serializeAccess = async (access) => {
2903
3837
  if (typeof access === "string") return access;
2904
3838
  if (typeof access === "boolean") return access;
2905
3839
  if (access && typeof access === "object" && typeof access.policy === "string") {
2906
- const policy = config.accessPolicies?.[access.policy];
2907
- if (typeof policy === "string" || typeof policy === "boolean") return policy;
3840
+ const policy = requestConfig.accessPolicies?.[access.policy];
3841
+ if (typeof policy === "string" || typeof policy === "boolean")
3842
+ return policy;
2908
3843
  }
2909
- return resolveBooleanAccess(config, access, accessArgs);
3844
+ return resolveBooleanAccess(requestConfig, access, accessArgs);
2910
3845
  };
2911
- const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
2912
- slug: col.slug,
2913
- labels: col.labels,
2914
- access: {
2915
- read: await serializeAccess(col.access?.read),
2916
- create: await serializeAccess(col.access?.create),
2917
- update: await serializeAccess(col.access?.update),
2918
- delete: await serializeAccess(col.access?.delete)
2919
- },
2920
- fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
2921
- name: f.name,
2922
- type: f.type,
2923
- label: f.label,
2924
- required: f.required,
2925
- defaultValue: f.defaultValue,
2926
- options: f.options,
2927
- relationTo: f.relationTo,
2928
- hasMany: f.hasMany,
2929
- fields: f.fields,
2930
- blocks: f.blocks,
2931
- collection: f.collection,
2932
- on: f.on,
2933
- limit: f.limit,
2934
- admin: f.admin,
3846
+ const filteredCollections = await Promise.all(
3847
+ collections.filter(
3848
+ (col) => !siteId || col.shared || !col.siteId || col.siteId === siteId
3849
+ ).map(async (col) => ({
3850
+ slug: col.slug,
3851
+ labels: col.labels,
2935
3852
  access: {
2936
- read: await serializeAccess(f.access?.read),
2937
- create: await serializeAccess(f.access?.create),
2938
- update: await serializeAccess(f.access?.update)
2939
- }
2940
- }))),
2941
- upload: !!col.upload,
2942
- auth: !!col.auth,
2943
- admin: col.admin,
2944
- workflow: col.workflow ? {
2945
- initialState: col.workflow.initialState,
2946
- draftState: col.workflow.draftState,
2947
- states: col.workflow.states,
2948
- transitions: col.workflow.transitions.map((t) => ({
2949
- name: t.name,
2950
- label: t.label,
2951
- from: t.from,
2952
- to: t.to,
2953
- requiredCapabilities: t.requiredCapabilities,
2954
- requireComment: t.requireComment,
2955
- unpublish: t.unpublish
2956
- })),
2957
- roles: col.workflow.roles
2958
- } : void 0
2959
- })));
2960
- const filteredGlobals = await Promise.all(globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
2961
- slug: glb.slug,
2962
- label: glb.label,
2963
- access: {
2964
- read: await serializeAccess(glb.access?.read),
2965
- update: await serializeAccess(glb.access?.update)
2966
- },
2967
- fields: await Promise.all(glb.fields.map(serializeFieldForApi).map(async (f) => ({
2968
- name: f.name,
2969
- type: f.type,
2970
- label: f.label,
2971
- required: f.required,
2972
- defaultValue: f.defaultValue,
2973
- options: f.options,
2974
- relationTo: f.relationTo,
2975
- hasMany: f.hasMany,
2976
- fields: f.fields,
2977
- blocks: f.blocks,
2978
- collection: f.collection,
2979
- on: f.on,
2980
- limit: f.limit,
2981
- admin: f.admin,
3853
+ read: await serializeAccess(col.access?.read),
3854
+ create: await serializeAccess(col.access?.create),
3855
+ update: await serializeAccess(col.access?.update),
3856
+ delete: await serializeAccess(col.access?.delete)
3857
+ },
3858
+ fields: await Promise.all(
3859
+ col.fields.map(serializeFieldForApi).map(async (f) => ({
3860
+ name: f.name,
3861
+ type: f.type,
3862
+ label: f.label,
3863
+ required: f.required,
3864
+ defaultValue: f.defaultValue,
3865
+ options: f.options,
3866
+ relationTo: f.relationTo,
3867
+ hasMany: f.hasMany,
3868
+ fields: f.fields,
3869
+ blocks: f.blocks,
3870
+ blockReferences: f.blockReferences,
3871
+ collection: f.collection,
3872
+ on: f.on,
3873
+ limit: f.limit,
3874
+ admin: f.admin,
3875
+ access: {
3876
+ read: await serializeAccess(f.access?.read),
3877
+ create: await serializeAccess(f.access?.create),
3878
+ update: await serializeAccess(f.access?.update)
3879
+ }
3880
+ }))
3881
+ ),
3882
+ upload: !!col.upload,
3883
+ auth: !!col.auth,
3884
+ audit: !!col.audit,
3885
+ drafts: !!col.drafts,
3886
+ admin: col.admin,
3887
+ workflow: col.workflow ? {
3888
+ initialState: col.workflow.initialState,
3889
+ draftState: col.workflow.draftState,
3890
+ states: col.workflow.states,
3891
+ transitions: col.workflow.transitions.map((t) => ({
3892
+ name: t.name,
3893
+ label: t.label,
3894
+ from: t.from,
3895
+ to: t.to,
3896
+ requiredCapabilities: t.requiredCapabilities,
3897
+ requireComment: t.requireComment,
3898
+ unpublish: t.unpublish
3899
+ })),
3900
+ roles: col.workflow.roles
3901
+ } : void 0
3902
+ }))
3903
+ );
3904
+ const filteredGlobals = await Promise.all(
3905
+ globals.filter(
3906
+ (glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId
3907
+ ).map(async (glb) => ({
3908
+ slug: glb.slug,
3909
+ label: glb.label,
2982
3910
  access: {
2983
- read: await serializeAccess(f.access?.read),
2984
- update: await serializeAccess(f.access?.update)
2985
- }
2986
- }))),
2987
- admin: glb.admin
2988
- })));
3911
+ read: await serializeAccess(glb.access?.read),
3912
+ update: await serializeAccess(glb.access?.update)
3913
+ },
3914
+ fields: await Promise.all(
3915
+ glb.fields.map(serializeFieldForApi).map(async (f) => ({
3916
+ name: f.name,
3917
+ type: f.type,
3918
+ label: f.label,
3919
+ required: f.required,
3920
+ defaultValue: f.defaultValue,
3921
+ options: f.options,
3922
+ relationTo: f.relationTo,
3923
+ hasMany: f.hasMany,
3924
+ fields: f.fields,
3925
+ blocks: f.blocks,
3926
+ blockReferences: f.blockReferences,
3927
+ collection: f.collection,
3928
+ on: f.on,
3929
+ limit: f.limit,
3930
+ admin: f.admin,
3931
+ access: {
3932
+ read: await serializeAccess(f.access?.read),
3933
+ update: await serializeAccess(f.access?.update)
3934
+ }
3935
+ }))
3936
+ ),
3937
+ admin: glb.admin
3938
+ }))
3939
+ );
2989
3940
  return c.json({
3941
+ blocks: requestConfig.blocks?.map(serializeBlockForApi),
2990
3942
  collections: filteredCollections,
2991
3943
  globals: filteredGlobals,
2992
- admin: config.admin || {},
2993
- adminAuth: getPublicAdminAuthConfig(config.adminAuth, collections)
3944
+ admin: requestConfig.admin || {},
3945
+ adminAuth: getPublicAdminAuthConfig(requestConfig.adminAuth, collections)
2994
3946
  });
2995
3947
  });
2996
- app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
2997
- const { collection: colSlug, field: fieldName } = c.req.param();
2998
- const siteId = c.req.header("X-Site-Id");
2999
- let collections = [...config.collections];
3000
- if (siteId && config.onSchemaFetch) {
3001
- const dynamic = await config.onSchemaFetch(siteId);
3002
- if (dynamic.collections) collections = [...collections, ...dynamic.collections];
3003
- }
3004
- const user = c.get("user");
3005
- let collection = collections.find((col) => col.slug === colSlug);
3006
- let field;
3007
- if (collection) {
3008
- const accessExpr = collection.access?.read;
3009
- if (accessExpr !== void 0 && accessExpr !== null) {
3010
- const allowed = await resolveBooleanAccess(config, accessExpr, {
3011
- user,
3012
- req: toHookRequestContext(c.req)
3013
- });
3014
- if (!allowed) {
3015
- return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
3948
+ app.get(
3949
+ "/api/dyrected/options/:collection/:field",
3950
+ optionalAuth(config),
3951
+ async (c) => {
3952
+ const { collection: colSlug, field: fieldName } = c.req.param();
3953
+ const siteId = c.req.header("X-Site-Id");
3954
+ const requestConfig = siteId && config.onSchemaFetch ? mergeDynamicConfig(config, await config.onSchemaFetch(siteId)) : config;
3955
+ let collections = [...requestConfig.collections];
3956
+ const user = c.get("user");
3957
+ let collection = collections.find((col) => col.slug === colSlug);
3958
+ let field;
3959
+ if (collection) {
3960
+ const accessExpr = collection.access?.read;
3961
+ if (accessExpr !== void 0 && accessExpr !== null) {
3962
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
3963
+ user,
3964
+ req: toHookRequestContext(c.req)
3965
+ });
3966
+ if (!allowed) {
3967
+ return c.json(
3968
+ { error: true, message: `Access denied: read on ${colSlug}` },
3969
+ 403
3970
+ );
3971
+ }
3972
+ }
3973
+ field = collection.fields.find((f) => f.name === fieldName);
3974
+ } else {
3975
+ const globals = [...requestConfig.globals];
3976
+ const glb = globals.find((g) => g.slug === colSlug);
3977
+ if (!glb) {
3978
+ return c.json(
3979
+ {
3980
+ error: true,
3981
+ message: `${colSlug} not found as collection or global`
3982
+ },
3983
+ 404
3984
+ );
3016
3985
  }
3986
+ const accessExpr = glb.access?.read;
3987
+ if (accessExpr !== void 0 && accessExpr !== null) {
3988
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
3989
+ user,
3990
+ req: toHookRequestContext(c.req)
3991
+ });
3992
+ if (!allowed) {
3993
+ return c.json(
3994
+ {
3995
+ error: true,
3996
+ message: `Access denied: read on global ${colSlug}`
3997
+ },
3998
+ 403
3999
+ );
4000
+ }
4001
+ }
4002
+ field = glb.fields.find((f) => f.name === fieldName);
3017
4003
  }
3018
- field = collection.fields.find((f) => f.name === fieldName);
3019
- } else {
3020
- let globals = [...config.globals];
3021
- if (siteId && config.onSchemaFetch) {
3022
- const dynamic = await config.onSchemaFetch(siteId);
3023
- if (dynamic.globals) globals = [...globals, ...dynamic.globals];
3024
- }
3025
- const glb = globals.find((g) => g.slug === colSlug);
3026
- if (!glb) {
3027
- return c.json({ error: true, message: `${colSlug} not found as collection or global` }, 404);
3028
- }
3029
- const accessExpr = glb.access?.read;
3030
- if (accessExpr !== void 0 && accessExpr !== null) {
3031
- const allowed = await resolveBooleanAccess(config, accessExpr, {
4004
+ if (!field) {
4005
+ return c.json(
4006
+ {
4007
+ error: true,
4008
+ message: `Field ${fieldName} not found in ${colSlug}`
4009
+ },
4010
+ 404
4011
+ );
4012
+ }
4013
+ let resolver;
4014
+ let cacheTTL;
4015
+ if (typeof field.options === "function") {
4016
+ resolver = field.options;
4017
+ } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
4018
+ resolver = field.options.resolve;
4019
+ cacheTTL = field.options.cacheTTL;
4020
+ }
4021
+ if (!resolver) {
4022
+ return c.json(
4023
+ {
4024
+ error: true,
4025
+ message: `Field ${fieldName} in ${colSlug} is not dynamic`
4026
+ },
4027
+ 400
4028
+ );
4029
+ }
4030
+ try {
4031
+ const db = c.get("db") || config.db;
4032
+ const queryParams = c.req.query();
4033
+ const reqContext = {
4034
+ query: queryParams,
4035
+ headers: c.req.header(),
4036
+ raw: c.req.raw
4037
+ };
4038
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
4039
+ let cacheKey = "";
4040
+ if (shouldCache) {
4041
+ cacheKey = JSON.stringify([
4042
+ siteId ?? "",
4043
+ colSlug,
4044
+ fieldName,
4045
+ user?.id ?? null,
4046
+ queryParams
4047
+ ]);
4048
+ const hit = optionsCache.get(cacheKey);
4049
+ if (hit && hit.expires > Date.now()) {
4050
+ return c.json(hit.value);
4051
+ }
4052
+ }
4053
+ const result = await resolver({
4054
+ db,
3032
4055
  user,
3033
- req: toHookRequestContext(c.req)
4056
+ req: reqContext
3034
4057
  });
3035
- if (!allowed) {
3036
- return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
3037
- }
3038
- }
3039
- field = glb.fields.find((f) => f.name === fieldName);
3040
- }
3041
- if (!field) {
3042
- return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
3043
- }
3044
- let resolver;
3045
- let cacheTTL;
3046
- if (typeof field.options === "function") {
3047
- resolver = field.options;
3048
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
3049
- resolver = field.options.resolve;
3050
- cacheTTL = field.options.cacheTTL;
3051
- }
3052
- if (!resolver) {
3053
- return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
3054
- }
3055
- try {
3056
- const db = c.get("db") || config.db;
3057
- const queryParams = c.req.query();
3058
- const reqContext = {
3059
- query: queryParams,
3060
- headers: c.req.header(),
3061
- raw: c.req.raw
3062
- };
3063
- const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
3064
- let cacheKey = "";
3065
- if (shouldCache) {
3066
- cacheKey = JSON.stringify([
3067
- siteId ?? "",
3068
- colSlug,
3069
- fieldName,
3070
- user?.id ?? null,
3071
- queryParams
3072
- ]);
3073
- const hit = optionsCache.get(cacheKey);
3074
- if (hit && hit.expires > Date.now()) {
3075
- return c.json(hit.value);
4058
+ if (shouldCache) {
4059
+ optionsCache.set(cacheKey, {
4060
+ expires: Date.now() + cacheTTL * 1e3,
4061
+ value: result
4062
+ });
3076
4063
  }
3077
- }
3078
- const result = await resolver({
3079
- db,
3080
- user,
3081
- req: reqContext
3082
- });
3083
- if (shouldCache) {
3084
- optionsCache.set(cacheKey, {
3085
- expires: Date.now() + cacheTTL * 1e3,
3086
- value: result
4064
+ return c.json(result);
4065
+ } catch (err) {
4066
+ getRequestLogger(c, "router").error({
4067
+ err,
4068
+ msg: "Failed to resolve dynamic field options",
4069
+ fieldName
3087
4070
  });
4071
+ return c.json(
4072
+ {
4073
+ error: true,
4074
+ message: err.message || "Failed to resolve dynamic options"
4075
+ },
4076
+ 500
4077
+ );
3088
4078
  }
3089
- return c.json(result);
3090
- } catch (err) {
3091
- console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
3092
- return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
3093
4079
  }
3094
- });
4080
+ );
3095
4081
  app.get("/api/openapi.json", (c) => {
3096
4082
  return c.json(generateOpenApi(config));
3097
4083
  });
@@ -3104,10 +4090,18 @@ function registerRoutes(app, config) {
3104
4090
  const key = c.req.param("key");
3105
4091
  const scope = c.req.query("scope");
3106
4092
  if (!db) return c.json({ message: "Database not configured" }, 500);
3107
- if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
3108
- if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4093
+ if (!user?.collection || !user.sub)
4094
+ return c.json({ error: true, message: "Authentication required." }, 401);
4095
+ if (!key)
4096
+ return c.json(
4097
+ { error: true, message: "Preference key is required." },
4098
+ 400
4099
+ );
3109
4100
  const getGlobalPreference = async () => {
3110
- const globalDoc = await db.findOne({ collection: "__global_preferences", id: key });
4101
+ const globalDoc = await db.findOne({
4102
+ collection: "__global_preferences",
4103
+ id: key
4104
+ });
3111
4105
  return globalDoc ? globalDoc.value : null;
3112
4106
  };
3113
4107
  if (scope === "global") {
@@ -3129,15 +4123,29 @@ function registerRoutes(app, config) {
3129
4123
  const key = c.req.param("key");
3130
4124
  const scope = c.req.query("scope");
3131
4125
  if (!db) return c.json({ message: "Database not configured" }, 500);
3132
- if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
3133
- if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4126
+ if (!user?.collection || !user.sub)
4127
+ return c.json({ error: true, message: "Authentication required." }, 401);
4128
+ if (!key)
4129
+ return c.json(
4130
+ { error: true, message: "Preference key is required." },
4131
+ 400
4132
+ );
3134
4133
  const body = await c.req.json().catch(() => ({}));
3135
4134
  if (scope === "global") {
3136
4135
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
3137
4136
  if (!isAdminUser) {
3138
- return c.json({ error: true, message: "Only administrators can save global preferences." }, 403);
4137
+ return c.json(
4138
+ {
4139
+ error: true,
4140
+ message: "Only administrators can save global preferences."
4141
+ },
4142
+ 403
4143
+ );
3139
4144
  }
3140
- const existing = await db.findOne({ collection: "__global_preferences", id: key });
4145
+ const existing = await db.findOne({
4146
+ collection: "__global_preferences",
4147
+ id: key
4148
+ });
3141
4149
  if (existing) {
3142
4150
  await db.update({
3143
4151
  collection: "__global_preferences",
@@ -3169,12 +4177,23 @@ function registerRoutes(app, config) {
3169
4177
  const key = c.req.param("key");
3170
4178
  const scope = c.req.query("scope");
3171
4179
  if (!db) return c.json({ message: "Database not configured" }, 500);
3172
- if (!user?.collection || !user.sub) return c.json({ error: true, message: "Authentication required." }, 401);
3173
- if (!key) return c.json({ error: true, message: "Preference key is required." }, 400);
4180
+ if (!user?.collection || !user.sub)
4181
+ return c.json({ error: true, message: "Authentication required." }, 401);
4182
+ if (!key)
4183
+ return c.json(
4184
+ { error: true, message: "Preference key is required." },
4185
+ 400
4186
+ );
3174
4187
  if (scope === "global") {
3175
4188
  const isAdminUser = Array.isArray(user?.roles) && user.roles.includes("admin");
3176
4189
  if (!isAdminUser) {
3177
- return c.json({ error: true, message: "Only administrators can delete global preferences." }, 403);
4190
+ return c.json(
4191
+ {
4192
+ error: true,
4193
+ message: "Only administrators can delete global preferences."
4194
+ },
4195
+ 403
4196
+ );
3178
4197
  }
3179
4198
  await db.delete({ collection: "__global_preferences", id: key });
3180
4199
  return c.json({ success: true });
@@ -3203,17 +4222,41 @@ function registerRoutes(app, config) {
3203
4222
  for (const col of uploadCollections) {
3204
4223
  const mediaController = new MediaController(col.slug);
3205
4224
  const prefix = `/api/collections/${col.slug}`;
3206
- app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
3207
- app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
3208
- app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
3209
- app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
4225
+ app.get(
4226
+ `${prefix}/media`,
4227
+ accessGate(config, col, "read"),
4228
+ (c) => mediaController.find(c)
4229
+ );
4230
+ app.get(
4231
+ `${prefix}/media/:filename{.+$}`,
4232
+ (c) => mediaController.serve(c)
4233
+ );
4234
+ app.post(
4235
+ `${prefix}/media`,
4236
+ accessGate(config, col, "create"),
4237
+ (c) => mediaController.upload(c)
4238
+ );
4239
+ app.delete(
4240
+ `${prefix}/media/:id`,
4241
+ accessGate(config, col, "delete"),
4242
+ (c) => mediaController.delete(c)
4243
+ );
3210
4244
  }
3211
4245
  }
3212
4246
  const adminAuthController = new AdminAuthController(config);
3213
4247
  app.get("/api/admin/auth/providers", (c) => adminAuthController.providers(c));
3214
- app.get("/api/admin/auth/:provider/start", (c) => adminAuthController.start(c));
3215
- app.get("/api/admin/auth/:provider/callback", (c) => adminAuthController.callback(c));
3216
- app.post("/api/admin/auth/:provider/exchange", (c) => adminAuthController.exchange(c));
4248
+ app.get(
4249
+ "/api/admin/auth/:provider/start",
4250
+ (c) => adminAuthController.start(c)
4251
+ );
4252
+ app.get(
4253
+ "/api/admin/auth/:provider/callback",
4254
+ (c) => adminAuthController.callback(c)
4255
+ );
4256
+ app.post(
4257
+ "/api/admin/auth/:provider/exchange",
4258
+ (c) => adminAuthController.exchange(c)
4259
+ );
3217
4260
  app.post("/api/admin/logout", (c) => adminAuthController.logout(c));
3218
4261
  for (const collection of config.collections) {
3219
4262
  if (!collection.auth) continue;
@@ -3224,10 +4267,21 @@ function registerRoutes(app, config) {
3224
4267
  app.get(`${path}/init`, (c) => authController.init(c));
3225
4268
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
3226
4269
  app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
3227
- app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
3228
- app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
4270
+ app.post(
4271
+ `${path}/refresh-token`,
4272
+ requireAuth(config),
4273
+ (c) => authController.refreshToken(c)
4274
+ );
4275
+ app.post(
4276
+ `${path}/forgot-password`,
4277
+ (c) => authController.forgotPassword(c)
4278
+ );
3229
4279
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
3230
- app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
4280
+ app.post(
4281
+ `${path}/invite`,
4282
+ requireAuth(config),
4283
+ (c) => authController.invite(c)
4284
+ );
3231
4285
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
3232
4286
  }
3233
4287
  const auditController = new AuditController();
@@ -3239,18 +4293,33 @@ function registerRoutes(app, config) {
3239
4293
  app.post(`${path}/media`, (c) => controller.create(c));
3240
4294
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
3241
4295
  if (collection.audit) {
3242
- app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
4296
+ app.get(
4297
+ `${path}/__audit`,
4298
+ (c) => auditController.findForCollection(c, collection)
4299
+ );
3243
4300
  }
3244
4301
  app.get(`${path}/:id`, (c) => controller.findOne(c));
3245
4302
  app.patch(`${path}/:id`, (c) => controller.update(c));
3246
4303
  app.delete(`${path}/:id`, (c) => controller.delete(c));
3247
4304
  app.post(`${path}/seed`, (c) => controller.seed(c));
3248
4305
  if (collection.auth) {
3249
- app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
4306
+ app.post(
4307
+ `${path}/:id/change-password`,
4308
+ requireAuth(config),
4309
+ (c) => controller.changePassword(c)
4310
+ );
3250
4311
  }
3251
4312
  if (collection.workflow) {
3252
- app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
3253
- app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
4313
+ app.post(
4314
+ `${path}/:id/transitions/:transition`,
4315
+ requireAuth(config),
4316
+ (c) => controller.transition(c)
4317
+ );
4318
+ app.get(
4319
+ `${path}/:id/workflow-history`,
4320
+ requireAuth(config),
4321
+ (c) => controller.workflowHistory(c)
4322
+ );
3254
4323
  }
3255
4324
  }
3256
4325
  for (const global of config.globals) {
@@ -3260,8 +4329,17 @@ function registerRoutes(app, config) {
3260
4329
  app.patch(path, (c) => controller.update(c));
3261
4330
  app.post(`${path}/seed`, (c) => controller.seed(c));
3262
4331
  }
4332
+ if (!process.env.DYRECTED_JWT_SECRET) {
4333
+ getConfigLogger(config, "router").warn({
4334
+ msg: "DYRECTED_JWT_SECRET is not set; token-mode live preview is signing with an insecure default"
4335
+ });
4336
+ }
3263
4337
  const previewController = new PreviewController();
3264
- app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
4338
+ app.post(
4339
+ "/api/preview-token",
4340
+ requireAuth(config),
4341
+ (c) => previewController.createToken(c)
4342
+ );
3265
4343
  app.get("/api/preview-data", (c) => previewController.getData(c));
3266
4344
  app.get("/api/audit", (c) => auditController.findAll(c));
3267
4345
  app.get("/api/collections/:slug/__audit", async (c) => {
@@ -3274,49 +4352,81 @@ function registerRoutes(app, config) {
3274
4352
  if (!config2.onSchemaFetch || !siteId) {
3275
4353
  return c.json({ message: `Collection "${slug}" not found` }, 404);
3276
4354
  }
3277
- const dynamic = await config2.onSchemaFetch(siteId);
3278
- const collection = dynamic.collections?.find((col) => col.slug === slug);
4355
+ const requestConfig = mergeDynamicConfig(
4356
+ config2,
4357
+ await config2.onSchemaFetch(siteId)
4358
+ );
4359
+ const collection = requestConfig.collections.find(
4360
+ (col) => col.slug === slug
4361
+ );
3279
4362
  if (!collection?.audit) {
3280
- return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
4363
+ return c.json(
4364
+ { message: `Collection "${slug}" not found or has no audit log` },
4365
+ 404
4366
+ );
3281
4367
  }
3282
4368
  return auditController.findForCollection(c, collection);
3283
4369
  });
3284
- app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
3285
- const slug = c.req.param("slug");
3286
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
3287
- const config2 = c.get("config");
3288
- if (config2.collections.some((col) => col.slug === slug)) {
3289
- return c.json({ message: "Not Found" }, 404);
3290
- }
3291
- if (!config2.onSchemaFetch || !siteId) {
3292
- return c.json({ message: `Collection "${slug}" not found` }, 404);
3293
- }
3294
- const dynamic = await config2.onSchemaFetch(siteId);
3295
- const collection = dynamic.collections?.find((col) => col.slug === slug);
3296
- if (!collection?.workflow) {
3297
- return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
3298
- }
3299
- const controller = new CollectionController(collection);
3300
- return controller.transition(c);
3301
- });
3302
- app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
3303
- const slug = c.req.param("slug");
3304
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
3305
- const config2 = c.get("config");
3306
- if (config2.collections.some((col) => col.slug === slug)) {
3307
- return c.json({ message: "Not Found" }, 404);
3308
- }
3309
- if (!config2.onSchemaFetch || !siteId) {
3310
- return c.json({ message: `Collection "${slug}" not found` }, 404);
3311
- }
3312
- const dynamic = await config2.onSchemaFetch(siteId);
3313
- const collection = dynamic.collections?.find((col) => col.slug === slug);
3314
- if (!collection?.workflow) {
3315
- return c.json({ message: `Collection "${slug}" not found or has no workflow` }, 404);
4370
+ app.post(
4371
+ "/api/collections/:slug/:id/transitions/:transition",
4372
+ requireAuth(config),
4373
+ async (c) => {
4374
+ const slug = c.req.param("slug");
4375
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4376
+ const config2 = c.get("config");
4377
+ if (config2.collections.some((col) => col.slug === slug)) {
4378
+ return c.json({ message: "Not Found" }, 404);
4379
+ }
4380
+ if (!config2.onSchemaFetch || !siteId) {
4381
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4382
+ }
4383
+ const requestConfig = mergeDynamicConfig(
4384
+ config2,
4385
+ await config2.onSchemaFetch(siteId)
4386
+ );
4387
+ const collection = requestConfig.collections.find(
4388
+ (col) => col.slug === slug
4389
+ );
4390
+ if (!collection?.workflow) {
4391
+ return c.json(
4392
+ { message: `Collection "${slug}" not found or has no workflow` },
4393
+ 404
4394
+ );
4395
+ }
4396
+ const controller = new CollectionController(collection);
4397
+ return controller.transition(c);
4398
+ }
4399
+ );
4400
+ app.get(
4401
+ "/api/collections/:slug/:id/workflow-history",
4402
+ requireAuth(config),
4403
+ async (c) => {
4404
+ const slug = c.req.param("slug");
4405
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4406
+ const config2 = c.get("config");
4407
+ if (config2.collections.some((col) => col.slug === slug)) {
4408
+ return c.json({ message: "Not Found" }, 404);
4409
+ }
4410
+ if (!config2.onSchemaFetch || !siteId) {
4411
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
4412
+ }
4413
+ const requestConfig = mergeDynamicConfig(
4414
+ config2,
4415
+ await config2.onSchemaFetch(siteId)
4416
+ );
4417
+ const collection = requestConfig.collections.find(
4418
+ (col) => col.slug === slug
4419
+ );
4420
+ if (!collection?.workflow) {
4421
+ return c.json(
4422
+ { message: `Collection "${slug}" not found or has no workflow` },
4423
+ 404
4424
+ );
4425
+ }
4426
+ const controller = new CollectionController(collection);
4427
+ return controller.workflowHistory(c);
3316
4428
  }
3317
- const controller = new CollectionController(collection);
3318
- return controller.workflowHistory(c);
3319
- });
4429
+ );
3320
4430
  app.all("/api/collections/:slug/:id?", async (c) => {
3321
4431
  const slug = c.req.param("slug");
3322
4432
  const id = c.req.param("id");
@@ -3326,8 +4436,13 @@ function registerRoutes(app, config) {
3326
4436
  return c.json({ message: "Method Not Allowed" }, 405);
3327
4437
  }
3328
4438
  if (config2.onSchemaFetch && siteId) {
3329
- const dynamic = await config2.onSchemaFetch(siteId);
3330
- let collection = dynamic.collections?.find((col) => col.slug === slug);
4439
+ const requestConfig = mergeDynamicConfig(
4440
+ config2,
4441
+ await config2.onSchemaFetch(siteId)
4442
+ );
4443
+ let collection = requestConfig.collections.find(
4444
+ (col) => col.slug === slug
4445
+ );
3331
4446
  if (!collection && slug === "media") {
3332
4447
  collection = {
3333
4448
  slug: "media",
@@ -3340,19 +4455,25 @@ function registerRoutes(app, config) {
3340
4455
  if (collection.auth && id) {
3341
4456
  const authController = new AuthController(collection);
3342
4457
  const method2 = c.req.method;
3343
- if (method2 === "POST" && id === "login") return authController.login(c);
3344
- if (method2 === "POST" && id === "logout") return authController.logout(c);
4458
+ if (method2 === "POST" && id === "login")
4459
+ return authController.login(c);
4460
+ if (method2 === "POST" && id === "logout")
4461
+ return authController.logout(c);
3345
4462
  if (method2 === "GET" && id === "me") return authController.me(c);
3346
- if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
3347
- if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
3348
- if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
4463
+ if (method2 === "POST" && id === "refresh-token")
4464
+ return authController.refreshToken(c);
4465
+ if (method2 === "POST" && id === "forgot-password")
4466
+ return authController.forgotPassword(c);
4467
+ if (method2 === "POST" && id === "reset-password")
4468
+ return authController.resetPassword(c);
3349
4469
  }
3350
4470
  const controller = new CollectionController(collection);
3351
4471
  const method = c.req.method;
3352
4472
  if (id) {
3353
4473
  if (method === "GET") return controller.findOne(c);
3354
4474
  if (method === "PATCH") return controller.update(c);
3355
- if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
4475
+ if (method === "DELETE" && id === "delete-many")
4476
+ return controller.deleteMany(c);
3356
4477
  if (method === "DELETE") return controller.delete(c);
3357
4478
  if (method === "POST" && id === "media") return controller.create(c);
3358
4479
  if (method === "POST" && id === "seed") return controller.seed(c);
@@ -3373,8 +4494,11 @@ function registerRoutes(app, config) {
3373
4494
  return c.json({ message: "Method Not Allowed" }, 405);
3374
4495
  }
3375
4496
  if (config2.onSchemaFetch && siteId) {
3376
- const dynamic = await config2.onSchemaFetch(siteId);
3377
- const global = dynamic.globals?.find((glb) => glb.slug === slug);
4497
+ const requestConfig = mergeDynamicConfig(
4498
+ config2,
4499
+ await config2.onSchemaFetch(siteId)
4500
+ );
4501
+ const global = requestConfig.globals.find((glb) => glb.slug === slug);
3378
4502
  if (global) {
3379
4503
  const controller = new GlobalController(global);
3380
4504
  const method = c.req.method;
@@ -3390,36 +4514,283 @@ function registerRoutes(app, config) {
3390
4514
  });
3391
4515
  }
3392
4516
 
4517
+ // src/network.ts
4518
+ var DIRECT_IP_HEADERS = [
4519
+ "cf-connecting-ip",
4520
+ "fly-client-ip",
4521
+ "true-client-ip",
4522
+ "x-real-ip",
4523
+ "x-client-ip",
4524
+ "fastly-client-ip"
4525
+ ];
4526
+ function toHookRequestContext2(c) {
4527
+ const headers = Object.fromEntries(
4528
+ Object.entries(c.req.header()).map(([key, value]) => [key.toLowerCase(), value])
4529
+ );
4530
+ return {
4531
+ query: c.req.query(),
4532
+ headers,
4533
+ raw: c.req.raw
4534
+ };
4535
+ }
4536
+ function resolveClientIp(req, trustProxy = false) {
4537
+ for (const header of DIRECT_IP_HEADERS) {
4538
+ const value = normalizeIp(req.headers[header]);
4539
+ if (value) return value;
4540
+ }
4541
+ if (trustProxy) {
4542
+ const forwardedFor = pickForwardedFor(req.headers, trustProxy);
4543
+ if (forwardedFor) return forwardedFor;
4544
+ }
4545
+ return "127.0.0.1";
4546
+ }
4547
+ function pickForwardedFor(headers, trustProxy) {
4548
+ const forwardedHeader = headers["x-forwarded-for"];
4549
+ if (forwardedHeader) {
4550
+ const chain2 = forwardedHeader.split(",").map((entry) => normalizeIp(entry)).filter((entry) => !!entry);
4551
+ if (chain2.length > 0) {
4552
+ if (trustProxy === true) return chain2[0];
4553
+ const trustedHops2 = Math.max(1, Math.floor(trustProxy));
4554
+ const candidateIndex2 = Math.max(0, chain2.length - trustedHops2);
4555
+ return chain2[candidateIndex2] ?? chain2[0];
4556
+ }
4557
+ }
4558
+ const forwarded = headers.forwarded;
4559
+ if (!forwarded) return null;
4560
+ const matches = [...forwarded.matchAll(/for=(?:"?\[?([^;\],"]+)\]?"?)/gi)];
4561
+ if (matches.length === 0) return null;
4562
+ const chain = matches.map((match) => normalizeIp(match[1])).filter((entry) => !!entry);
4563
+ if (chain.length === 0) return null;
4564
+ if (trustProxy === true) return chain[0];
4565
+ const trustedHops = Math.max(1, Math.floor(trustProxy));
4566
+ const candidateIndex = Math.max(0, chain.length - trustedHops);
4567
+ return chain[candidateIndex] ?? chain[0];
4568
+ }
4569
+ function normalizeIp(value) {
4570
+ if (!value) return null;
4571
+ const trimmed = value.trim();
4572
+ if (!trimmed || trimmed.toLowerCase() === "unknown") return null;
4573
+ return trimmed.replace(/^\[(.*)\]$/, "$1");
4574
+ }
4575
+
4576
+ // src/middleware/rate-limit.ts
4577
+ var DEFAULT_WINDOW_MS = 15 * 60 * 1e3;
4578
+ var DEFAULT_MAX_REQUESTS = 500;
4579
+ var DEFAULT_PATHS = ["/api"];
4580
+ function createRateLimitMiddleware(config) {
4581
+ const settings = normalizeRateLimitConfig(config.rateLimit);
4582
+ const buckets = /* @__PURE__ */ new Map();
4583
+ return async (c, next) => {
4584
+ const req = toHookRequestContext2(c);
4585
+ const ip = resolveClientIp(req, settings.trustProxy);
4586
+ c.set("clientIp", ip);
4587
+ if (!settings.enabled) {
4588
+ return next();
4589
+ }
4590
+ const path = c.req.path;
4591
+ if (!matchesLimitedPath(path, settings.paths)) {
4592
+ return next();
4593
+ }
4594
+ if (settings.skip && await settings.skip({
4595
+ ip,
4596
+ path,
4597
+ method: c.req.method,
4598
+ req
4599
+ })) {
4600
+ return next();
4601
+ }
4602
+ const now = Date.now();
4603
+ const key = ip;
4604
+ const existing = buckets.get(key);
4605
+ const bucket = existing && existing.resetAt > now ? existing : { count: 0, resetAt: now + settings.window };
4606
+ bucket.count += 1;
4607
+ buckets.set(key, bucket);
4608
+ pruneExpiredBuckets(buckets, now);
4609
+ const remaining = Math.max(0, settings.max - bucket.count);
4610
+ const resetSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3));
4611
+ c.res.headers.set("X-RateLimit-Limit", String(settings.max));
4612
+ c.res.headers.set("X-RateLimit-Remaining", String(remaining));
4613
+ c.res.headers.set("X-RateLimit-Reset", String(Math.ceil(bucket.resetAt / 1e3)));
4614
+ if (bucket.count > settings.max) {
4615
+ c.res.headers.set("Retry-After", String(resetSeconds));
4616
+ return c.json(
4617
+ {
4618
+ error: true,
4619
+ message: "Too many requests. Try again later.",
4620
+ retryAfterSeconds: resetSeconds
4621
+ },
4622
+ 429
4623
+ );
4624
+ }
4625
+ await next();
4626
+ };
4627
+ }
4628
+ function normalizeRateLimitConfig(config) {
4629
+ return {
4630
+ enabled: config?.enabled ?? true,
4631
+ window: config?.window ?? DEFAULT_WINDOW_MS,
4632
+ max: config?.max ?? DEFAULT_MAX_REQUESTS,
4633
+ paths: config?.paths?.length ? config.paths : DEFAULT_PATHS,
4634
+ skip: config?.skip,
4635
+ trustProxy: config?.trustProxy ?? false
4636
+ };
4637
+ }
4638
+ function matchesLimitedPath(path, prefixes) {
4639
+ return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
4640
+ }
4641
+ function pruneExpiredBuckets(buckets, now) {
4642
+ if (buckets.size < 1e3) return;
4643
+ for (const [key, bucket] of buckets) {
4644
+ if (bucket.resetAt <= now) {
4645
+ buckets.delete(key);
4646
+ }
4647
+ }
4648
+ }
4649
+
3393
4650
  // src/app.ts
3394
4651
  async function createDyrectedApp(rawConfig) {
3395
4652
  const config = normalizeConfig(rawConfig);
4653
+ const observability = createObservabilityRuntime(config);
4654
+ bindObservabilityRuntime(config, observability);
4655
+ config.logger = observability.logger;
3396
4656
  const app = new Hono();
3397
4657
  if (config.db?.sync) {
3398
4658
  await config.db.sync(config.collections, config.globals);
3399
4659
  }
3400
4660
  app.use("*", requestId());
3401
- app.use("*", optionalAuth(config));
3402
- app.use("*", async (c, next) => {
3403
- const start = Date.now();
3404
- await next();
3405
- const ms = Date.now() - start;
3406
- console.log(`[dyrected/api] ${c.req.method} ${c.req.path} ${c.res.status} - ${ms}ms`);
3407
- });
3408
- app.use("*", cors());
3409
4661
  app.use("*", async (c, next) => {
3410
4662
  c.set("config", config);
4663
+ c.set("observability", observability);
3411
4664
  if (!c.get("siteId")) {
3412
4665
  c.set("siteId", "default");
3413
4666
  }
4667
+ c.set("logger", buildRequestLogger(observability, c.var, c.get("requestId")));
3414
4668
  await next();
3415
4669
  });
4670
+ app.use("*", async (c, next) => {
4671
+ const start = Date.now();
4672
+ const method = c.req.method;
4673
+ const path = c.req.path;
4674
+ const requestId2 = c.get("requestId");
4675
+ const observabilityConfig = observability.config;
4676
+ const traceSampled = shouldSampleRequest(
4677
+ requestId2,
4678
+ 200,
4679
+ observabilityConfig.sampling,
4680
+ "trace"
4681
+ );
4682
+ const requestTrace = traceSampled ? createRequestSpan(observability, `${method} ${path}`, {
4683
+ "http.method": method,
4684
+ "http.route": path,
4685
+ requestId: requestId2,
4686
+ siteId: c.get("siteId"),
4687
+ workspaceId: c.get("workspaceId")
4688
+ }) : void 0;
4689
+ if (requestTrace) {
4690
+ c.set("requestTrace", requestTrace);
4691
+ c.set("logger", buildRequestLogger(observability, c.var, requestId2));
4692
+ }
4693
+ let bodyCapturePromise;
4694
+ if (observabilityConfig.requestLogging.logBodies) {
4695
+ const bodySampled = shouldSampleRequest(
4696
+ requestId2,
4697
+ 200,
4698
+ observabilityConfig.sampling,
4699
+ "body"
4700
+ );
4701
+ if (bodySampled) {
4702
+ bodyCapturePromise = captureRequestBody(c.req.raw, observabilityConfig);
4703
+ }
4704
+ }
4705
+ let caughtError;
4706
+ try {
4707
+ await next();
4708
+ } catch (error) {
4709
+ caughtError = error;
4710
+ throw error;
4711
+ } finally {
4712
+ const durationMs = Date.now() - start;
4713
+ const statusCode = c.res.status || (caughtError ? 500 : 200);
4714
+ const sampled = shouldSampleRequest(
4715
+ requestId2,
4716
+ statusCode,
4717
+ observabilityConfig.sampling,
4718
+ "log"
4719
+ );
4720
+ const requestLogger = getRequestLogger(c, "api");
4721
+ const requestHeaders = redactHeaders(
4722
+ Object.fromEntries(c.req.raw.headers.entries()),
4723
+ observabilityConfig
4724
+ );
4725
+ const requestBodyCapture = bodyCapturePromise ? await bodyCapturePromise : void 0;
4726
+ if (requestBodyCapture) {
4727
+ c.set("requestBodyCapture", requestBodyCapture);
4728
+ }
4729
+ attachRequestMetrics(observability, {
4730
+ method,
4731
+ route: path,
4732
+ statusCode,
4733
+ durationMs
4734
+ });
4735
+ if (observabilityConfig.requestLogging.enabled && sampled) {
4736
+ const level = statusCode >= 500 ? "error" : statusCode >= 400 ? "warn" : "info";
4737
+ requestLogger[level]({
4738
+ msg: "HTTP request completed",
4739
+ method,
4740
+ path,
4741
+ statusCode,
4742
+ durationMs,
4743
+ requestId: requestId2,
4744
+ siteId: c.get("siteId"),
4745
+ workspaceId: c.get("workspaceId"),
4746
+ contentType: c.req.header("content-type") ?? void 0,
4747
+ contentLength: c.req.header("content-length") ? Number(c.req.header("content-length")) : void 0,
4748
+ sampled,
4749
+ headers: Object.keys(requestHeaders).length > 0 ? requestHeaders : void 0,
4750
+ body: requestBodyCapture?.body,
4751
+ bodyCapture: requestBodyCapture && requestBodyCapture.body === void 0 ? {
4752
+ attempted: requestBodyCapture.attempted,
4753
+ contentType: requestBodyCapture.contentType,
4754
+ contentLength: requestBodyCapture.contentLength,
4755
+ truncated: requestBodyCapture.truncated,
4756
+ parseFailed: requestBodyCapture.parseFailed
4757
+ } : void 0
4758
+ });
4759
+ }
4760
+ endRequestSpan(requestTrace, statusCode, caughtError);
4761
+ }
4762
+ });
4763
+ app.use("*", createRateLimitMiddleware(config));
4764
+ app.use("*", optionalAuth(config));
4765
+ app.use("*", cors(config.cors ? { origin: config.cors.origins } : {}));
3416
4766
  app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
4767
+ if (observability.config.metrics.enabled && observability.config.metrics.exporter === "prometheus" && observability.prometheusExporter) {
4768
+ app.get(observability.config.metrics.path, async (c) => {
4769
+ const metrics = await renderPrometheusMetrics(
4770
+ observability.prometheusExporter
4771
+ );
4772
+ return new Response(metrics.body, {
4773
+ status: metrics.statusCode,
4774
+ headers: metrics.headers
4775
+ });
4776
+ });
4777
+ }
3417
4778
  app.get("/routes", (c) => {
3418
4779
  const routes = app.routes.map((r) => ({ method: r.method, path: r.path }));
3419
4780
  return c.json({ routes });
3420
4781
  });
3421
4782
  app.onError((err, c) => {
3422
- console.error(`[dyrected/core] Uncaught Error:`, err);
4783
+ const logger = getRequestLogger(c, "core");
4784
+ logger.error({
4785
+ err,
4786
+ msg: "Uncaught error",
4787
+ path: c.req.path,
4788
+ method: c.req.method
4789
+ });
4790
+ observability.recordUncaughtError({
4791
+ path: c.req.path,
4792
+ method: c.req.method
4793
+ });
3423
4794
  return c.json({
3424
4795
  message: err.message || "Internal Server Error",
3425
4796
  stack: process.env.NODE_ENV === "development" ? err.stack : void 0