@dyrected/core 2.5.61 → 2.5.63

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