@dyrected/core 2.5.55 → 2.5.58

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.
package/dist/server.js CHANGED
@@ -346,6 +346,9 @@ async function evaluateAccess(expression, context) {
346
346
  if (typeof expression === "boolean") return expression;
347
347
  try {
348
348
  const result = await jexl2.eval(expression, context);
349
+ if (result && typeof result === "object" && !Array.isArray(result)) {
350
+ return result;
351
+ }
349
352
  return !!result;
350
353
  } catch (err) {
351
354
  console.error("[dyrected/core] Jexl evaluation failed:", err);
@@ -353,6 +356,173 @@ async function evaluateAccess(expression, context) {
353
356
  }
354
357
  }
355
358
 
359
+ // src/auth/access.ts
360
+ function isNamedAccessPolicy(value) {
361
+ return !!value && typeof value === "object" && "policy" in value && typeof value.policy === "string";
362
+ }
363
+ async function resolveAccess(config, access, args) {
364
+ if (access === void 0 || access === null) return void 0;
365
+ if (typeof access === "boolean" || typeof access === "string") {
366
+ return evaluateAccess(access, args);
367
+ }
368
+ if (typeof access === "function") {
369
+ try {
370
+ return await access(args);
371
+ } catch (err) {
372
+ console.error("[dyrected/core] Functional access check failed:", err);
373
+ return false;
374
+ }
375
+ }
376
+ if (isNamedAccessPolicy(access)) {
377
+ const resolver = config.accessPolicies?.[access.policy];
378
+ if (!resolver) {
379
+ console.error(`[dyrected/core] Unknown access policy "${access.policy}".`);
380
+ return false;
381
+ }
382
+ try {
383
+ return await resolver({ ...args, params: access.params });
384
+ } catch (err) {
385
+ console.error(`[dyrected/core] Access policy "${access.policy}" failed:`, err);
386
+ return false;
387
+ }
388
+ }
389
+ return false;
390
+ }
391
+
392
+ // src/utils/access-control.ts
393
+ function toHookRequestContext(req) {
394
+ return {
395
+ query: req.query(),
396
+ headers: req.header(),
397
+ raw: req.raw
398
+ };
399
+ }
400
+ async function resolveBooleanAccess(config, access, args) {
401
+ const result = await resolveAccess(config, access, args);
402
+ if (result === void 0) return true;
403
+ return typeof result === "boolean" ? result : false;
404
+ }
405
+ async function matchesAccessConstraint(config, collection, id, constraint) {
406
+ if (!config.db) return false;
407
+ const match = await config.db.find({
408
+ collection,
409
+ where: { AND: [{ id: { equals: id } }, constraint] },
410
+ limit: 1
411
+ });
412
+ return match.total > 0;
413
+ }
414
+ async function resolveCollectionAccess(config, collection, action, access, args) {
415
+ const result = await resolveAccess(config, access, args);
416
+ if (result === void 0) return { allowed: true };
417
+ if (typeof result === "boolean") return { allowed: result };
418
+ if (action === "read" && !args.id) {
419
+ return { allowed: true, constraint: result };
420
+ }
421
+ if (!args.id) {
422
+ return { allowed: false };
423
+ }
424
+ return {
425
+ allowed: await matchesAccessConstraint(config, collection, args.id, result)
426
+ };
427
+ }
428
+ async function applyFieldReadAccess(context, doc) {
429
+ if (!doc || typeof doc !== "object") return doc;
430
+ const result = { ...doc };
431
+ for (const field of context.fields) {
432
+ if (field.type === "row" && field.fields) {
433
+ Object.assign(result, await applyFieldReadAccess({ ...context, fields: field.fields }, result));
434
+ continue;
435
+ }
436
+ if (!field.name) continue;
437
+ const value = result[field.name];
438
+ const canRead = await resolveBooleanAccess(context.config, field.access?.read, {
439
+ user: context.user,
440
+ req: context.req,
441
+ doc: context.doc,
442
+ data: context.data
443
+ });
444
+ if (!canRead) {
445
+ delete result[field.name];
446
+ continue;
447
+ }
448
+ if (value === void 0 || value === null) continue;
449
+ if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
450
+ result[field.name] = await applyFieldReadAccess({ ...context, fields: field.fields }, value);
451
+ continue;
452
+ }
453
+ if (field.type === "array" && field.fields && Array.isArray(value)) {
454
+ result[field.name] = await Promise.all(
455
+ value.map(
456
+ (item) => typeof item === "object" && item !== null ? applyFieldReadAccess({ ...context, fields: field.fields }, item) : item
457
+ )
458
+ );
459
+ continue;
460
+ }
461
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
462
+ result[field.name] = await Promise.all(
463
+ value.map(async (item) => {
464
+ if (typeof item !== "object" || item === null) return item;
465
+ const typedItem = item;
466
+ const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
467
+ if (!block) return item;
468
+ return applyFieldReadAccess({ ...context, fields: block.fields }, typedItem);
469
+ })
470
+ );
471
+ }
472
+ }
473
+ return result;
474
+ }
475
+ async function applyFieldWriteAccess(context, data) {
476
+ if (!data || typeof data !== "object") return data;
477
+ const result = { ...data };
478
+ for (const field of context.fields) {
479
+ if (field.type === "row" && field.fields) {
480
+ Object.assign(result, await applyFieldWriteAccess({ ...context, fields: field.fields }, result));
481
+ continue;
482
+ }
483
+ if (!field.name || !(field.name in result)) continue;
484
+ const canUpdate = await resolveBooleanAccess(context.config, field.access?.update, {
485
+ user: context.user,
486
+ req: context.req,
487
+ doc: context.doc,
488
+ data: context.data
489
+ });
490
+ if (!canUpdate) {
491
+ delete result[field.name];
492
+ continue;
493
+ }
494
+ const value = result[field.name];
495
+ if (value === void 0 || value === null) continue;
496
+ if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
497
+ result[field.name] = await applyFieldWriteAccess({ ...context, fields: field.fields }, value);
498
+ continue;
499
+ }
500
+ if (field.type === "array" && field.fields && Array.isArray(value)) {
501
+ result[field.name] = await Promise.all(
502
+ value.map(
503
+ (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess({ ...context, fields: field.fields }, item) : item
504
+ )
505
+ );
506
+ continue;
507
+ }
508
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
509
+ result[field.name] = await Promise.all(
510
+ value.map(async (item) => {
511
+ if (typeof item !== "object" || item === null) return item;
512
+ const typedItem = item;
513
+ const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
514
+ if (!block) return item;
515
+ return applyFieldWriteAccess({ ...context, fields: block.fields }, typedItem);
516
+ })
517
+ );
518
+ }
519
+ }
520
+ return result;
521
+ }
522
+ function mergeWhereConstraint(where, constraint) {
523
+ return where ? { AND: [where, constraint] } : constraint;
524
+ }
525
+
356
526
  // src/controllers/collection.controller.ts
357
527
  var CollectionController = class {
358
528
  collection;
@@ -368,11 +538,23 @@ var CollectionController = class {
368
538
  return config.adminAuth?.providers?.find((p) => p.members) || null;
369
539
  }
370
540
  toHookRequestContext(c) {
371
- return {
372
- query: c.req.query(),
373
- headers: c.req.header(),
374
- raw: c.req.raw
375
- };
541
+ return toHookRequestContext(c.req);
542
+ }
543
+ async evaluateAccess(c, action, options = {}) {
544
+ const config = c.get("config");
545
+ return resolveCollectionAccess(
546
+ config,
547
+ this.collection.slug,
548
+ action,
549
+ this.collection.access?.[action],
550
+ {
551
+ id: options.id,
552
+ user: c.get("user"),
553
+ req: this.toHookRequestContext(c),
554
+ doc: options.doc ?? void 0,
555
+ data: options.data
556
+ }
557
+ );
376
558
  }
377
559
  async find(c) {
378
560
  const config = c.get("config");
@@ -452,6 +634,13 @@ var CollectionController = class {
452
634
  if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
453
635
  where = where ? { AND: [where, { __published: { exists: true } }] } : { __published: { exists: true } };
454
636
  }
637
+ const access = await this.evaluateAccess(c, "read");
638
+ if (!access.allowed) {
639
+ return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
640
+ }
641
+ if (access.constraint) {
642
+ where = mergeWhereConstraint(where, access.constraint);
643
+ }
455
644
  let result = await db.find({
456
645
  collection: this.collection.slug,
457
646
  limit,
@@ -484,7 +673,14 @@ var CollectionController = class {
484
673
  db: readonlyDb
485
674
  });
486
675
  const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
487
- processedDocs.push(docWithFieldHooks);
676
+ const docWithFieldAccess = await applyFieldReadAccess({
677
+ config,
678
+ fields: this.collection.fields,
679
+ user,
680
+ req: this.toHookRequestContext(c),
681
+ doc: docWithFieldHooks
682
+ }, docWithFieldHooks);
683
+ processedDocs.push(docWithFieldAccess);
488
684
  }
489
685
  result.docs = processedDocs;
490
686
  if (depth > 0) {
@@ -529,6 +725,10 @@ var CollectionController = class {
529
725
  doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
530
726
  if (!doc) return c.json({ message: "Not Found" }, 404);
531
727
  }
728
+ const access = await this.evaluateAccess(c, "read", { id, doc });
729
+ if (!access.allowed) {
730
+ return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
731
+ }
532
732
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
533
733
  const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
534
734
  doc: docWithDefaults,
@@ -537,17 +737,24 @@ var CollectionController = class {
537
737
  db: readonlyDb
538
738
  });
539
739
  const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
540
- if (depth > 0 && docWithFieldHooks) {
740
+ const docWithFieldAccess = await applyFieldReadAccess({
741
+ config,
742
+ fields: this.collection.fields,
743
+ user,
744
+ req: this.toHookRequestContext(c),
745
+ doc: docWithFieldHooks
746
+ }, docWithFieldHooks);
747
+ if (depth > 0 && docWithFieldAccess) {
541
748
  const populationService = new PopulationService(db, config.collections);
542
749
  const populatedDoc = await populationService.populate({
543
- data: docWithFieldHooks,
750
+ data: docWithFieldAccess,
544
751
  fields: this.collection.fields,
545
752
  currentDepth: 0,
546
753
  maxDepth: depth
547
754
  });
548
755
  return c.json(populatedDoc);
549
756
  }
550
- return c.json(docWithFieldHooks);
757
+ return c.json(docWithFieldAccess);
551
758
  }
552
759
  async create(c) {
553
760
  const config = c.get("config");
@@ -586,9 +793,20 @@ var CollectionController = class {
586
793
  if (this.collection.workflow) {
587
794
  data = initializeWorkflowDocument(data, this.collection.workflow);
588
795
  }
796
+ const createAccess = await this.evaluateAccess(c, "create", { data });
797
+ if (!createAccess.allowed) {
798
+ return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
799
+ }
589
800
  if (this.collection.auth && data.password) {
590
801
  data.password = await hashPassword(data.password);
591
802
  }
803
+ data = await applyFieldWriteAccess({
804
+ config,
805
+ fields: this.collection.fields,
806
+ user,
807
+ req: this.toHookRequestContext(c),
808
+ data
809
+ }, data);
592
810
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
593
811
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
594
812
  data,
@@ -623,7 +841,14 @@ var CollectionController = class {
623
841
  db: readonlyDb
624
842
  });
625
843
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
626
- return c.json(finalDoc, 201);
844
+ const accessibleDoc = await applyFieldReadAccess({
845
+ config,
846
+ fields: this.collection.fields,
847
+ user,
848
+ req: this.toHookRequestContext(c),
849
+ doc: finalDoc
850
+ }, finalDoc);
851
+ return c.json(accessibleDoc, 201);
627
852
  }
628
853
  async upload(c) {
629
854
  const config = c.get("config");
@@ -666,6 +891,17 @@ var CollectionController = class {
666
891
  createdBy: user?.sub ?? null,
667
892
  updatedBy: user?.sub ?? null
668
893
  };
894
+ const createAccess = await this.evaluateAccess(c, "create", { data });
895
+ if (!createAccess.allowed) {
896
+ return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
897
+ }
898
+ data = await applyFieldWriteAccess({
899
+ config,
900
+ fields: this.collection.fields,
901
+ user,
902
+ req: this.toHookRequestContext(c),
903
+ data
904
+ }, data);
669
905
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
670
906
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
671
907
  data,
@@ -693,7 +929,14 @@ var CollectionController = class {
693
929
  db: readonlyDb
694
930
  });
695
931
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
696
- return c.json(finalDoc, 201);
932
+ const accessibleDoc = await applyFieldReadAccess({
933
+ config,
934
+ fields: this.collection.fields,
935
+ user,
936
+ req: this.toHookRequestContext(c),
937
+ doc: finalDoc
938
+ }, finalDoc);
939
+ return c.json(accessibleDoc, 201);
697
940
  }
698
941
  async update(c) {
699
942
  const config = c.get("config");
@@ -731,10 +974,22 @@ var CollectionController = class {
731
974
  });
732
975
  const originalDoc = await db.findOne({ collection: this.collection.slug, id });
733
976
  if (!originalDoc) return c.json({ message: "Not Found" }, 404);
977
+ const updateAccess = await this.evaluateAccess(c, "update", { id, doc: originalDoc, data });
978
+ if (!updateAccess.allowed) {
979
+ return c.json({ error: true, message: `Access denied: update on ${this.collection.slug}` }, 403);
980
+ }
734
981
  let before = null;
735
982
  if (this.collection.audit) {
736
983
  before = originalDoc;
737
984
  }
985
+ data = await applyFieldWriteAccess({
986
+ config,
987
+ fields: this.collection.fields,
988
+ user,
989
+ req: this.toHookRequestContext(c),
990
+ doc: originalDoc,
991
+ data
992
+ }, data);
738
993
  data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user, readonlyDb);
739
994
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
740
995
  data,
@@ -770,7 +1025,14 @@ var CollectionController = class {
770
1025
  db: readonlyDb
771
1026
  });
772
1027
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
773
- return c.json(finalDoc);
1028
+ const accessibleDoc = await applyFieldReadAccess({
1029
+ config,
1030
+ fields: this.collection.fields,
1031
+ user,
1032
+ req: this.toHookRequestContext(c),
1033
+ doc: finalDoc
1034
+ }, finalDoc);
1035
+ return c.json(accessibleDoc);
774
1036
  }
775
1037
  async transition(c) {
776
1038
  const config = c.get("config");
@@ -807,7 +1069,7 @@ var CollectionController = class {
807
1069
  const readAccess = this.collection.access?.read;
808
1070
  if (readAccess !== void 0 && readAccess !== null) {
809
1071
  const args = { user: c.get("user"), req: c.req, doc: document };
810
- const result2 = typeof readAccess === "function" ? await readAccess(args) : await evaluateAccess(readAccess, args);
1072
+ const result2 = await resolveAccess(config, readAccess, args);
811
1073
  let allowed = result2 === true;
812
1074
  if (result2 && typeof result2 === "object") {
813
1075
  const match = await config.db.find({
@@ -918,6 +1180,10 @@ var CollectionController = class {
918
1180
  const user = c.get("user");
919
1181
  const doc = await db.findOne({ collection: this.collection.slug, id });
920
1182
  if (!doc) return c.json({ message: "Not Found" }, 404);
1183
+ const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1184
+ if (!deleteAccess.allowed) {
1185
+ return c.json({ error: true, message: `Access denied: delete on ${this.collection.slug}` }, 403);
1186
+ }
921
1187
  let before = null;
922
1188
  if (this.collection.audit) {
923
1189
  before = doc;
@@ -977,6 +1243,11 @@ var CollectionController = class {
977
1243
  failed.push({ id, error: "Not Found" });
978
1244
  continue;
979
1245
  }
1246
+ const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1247
+ if (!deleteAccess.allowed) {
1248
+ failed.push({ id, error: "Access denied" });
1249
+ continue;
1250
+ }
980
1251
  let before = null;
981
1252
  if (this.collection.audit) {
982
1253
  before = doc;
@@ -1070,6 +1341,14 @@ var GlobalController = class {
1070
1341
  await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
1071
1342
  data = this.global.initialData;
1072
1343
  }
1344
+ const canRead = await resolveBooleanAccess(config, this.global.access?.read, {
1345
+ user,
1346
+ req: toHookRequestContext(c.req),
1347
+ doc: data
1348
+ });
1349
+ if (!canRead) {
1350
+ return c.json({ error: true, message: `Access denied: read on ${this.global.slug}` }, 403);
1351
+ }
1073
1352
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1074
1353
  const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
1075
1354
  doc: dataWithDefaults,
@@ -1078,17 +1357,24 @@ var GlobalController = class {
1078
1357
  db: readonlyDb
1079
1358
  });
1080
1359
  const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user, readonlyDb);
1081
- if (depth > 0 && docWithFieldHooks) {
1360
+ const docWithFieldAccess = await applyFieldReadAccess({
1361
+ config,
1362
+ fields: this.global.fields,
1363
+ user,
1364
+ req: toHookRequestContext(c.req),
1365
+ doc: docWithFieldHooks
1366
+ }, docWithFieldHooks);
1367
+ if (depth > 0 && docWithFieldAccess) {
1082
1368
  const populationService = new PopulationService(db, config.collections);
1083
1369
  const populatedData = await populationService.populate({
1084
- data: docWithFieldHooks,
1370
+ data: docWithFieldAccess,
1085
1371
  fields: this.global.fields,
1086
1372
  currentDepth: 0,
1087
1373
  maxDepth: depth
1088
1374
  });
1089
1375
  return c.json(populatedData);
1090
1376
  }
1091
- return c.json(docWithFieldHooks);
1377
+ return c.json(docWithFieldAccess);
1092
1378
  }
1093
1379
  async update(c) {
1094
1380
  const config = c.get("config");
@@ -1098,7 +1384,24 @@ var GlobalController = class {
1098
1384
  const body = await c.req.json();
1099
1385
  const user = c.get("user");
1100
1386
  const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
1101
- let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user, readonlyDb);
1387
+ const canUpdate = await resolveBooleanAccess(config, this.global.access?.update, {
1388
+ user,
1389
+ req: toHookRequestContext(c.req),
1390
+ doc: originalDoc,
1391
+ data: body
1392
+ });
1393
+ if (!canUpdate) {
1394
+ return c.json({ error: true, message: `Access denied: update on ${this.global.slug}` }, 403);
1395
+ }
1396
+ let sanitizedBody = await applyFieldWriteAccess({
1397
+ config,
1398
+ fields: this.global.fields,
1399
+ user,
1400
+ req: toHookRequestContext(c.req),
1401
+ doc: originalDoc,
1402
+ data: body
1403
+ }, body);
1404
+ let data = await executeFieldBeforeChange(this.global.fields, sanitizedBody, originalDoc, user, readonlyDb);
1102
1405
  data = await runCollectionHooks(this.global.hooks?.beforeChange, {
1103
1406
  data,
1104
1407
  doc: originalDoc,
@@ -1123,7 +1426,14 @@ var GlobalController = class {
1123
1426
  db: readonlyDb
1124
1427
  });
1125
1428
  const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user, readonlyDb);
1126
- return c.json(finalDoc);
1429
+ const accessibleDoc = await applyFieldReadAccess({
1430
+ config,
1431
+ fields: this.global.fields,
1432
+ user,
1433
+ req: toHookRequestContext(c.req),
1434
+ doc: finalDoc
1435
+ }, finalDoc);
1436
+ return c.json(accessibleDoc);
1127
1437
  }
1128
1438
  async seed(c) {
1129
1439
  const config = c.get("config");
@@ -1305,7 +1615,7 @@ var MediaController = class {
1305
1615
  import { SignJWT, jwtVerify, decodeJwt } from "jose";
1306
1616
  import { TextEncoder } from "util";
1307
1617
  function getSecret() {
1308
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
1618
+ const secret = process.env.DYRECTED_JWT_SECRET;
1309
1619
  if (!secret) {
1310
1620
  throw new Error(
1311
1621
  "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
@@ -2180,7 +2490,7 @@ var AdminAuthController = class {
2180
2490
  return state;
2181
2491
  }
2182
2492
  getStateSecret() {
2183
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
2493
+ const secret = process.env.DYRECTED_JWT_SECRET;
2184
2494
  if (!secret) {
2185
2495
  throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
2186
2496
  }
@@ -2198,7 +2508,7 @@ import { SignJWT as SignJWT3, jwtVerify as jwtVerify3 } from "jose";
2198
2508
  import { TextEncoder as TextEncoder3 } from "util";
2199
2509
  var PreviewController = class {
2200
2510
  getSecret() {
2201
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
2511
+ const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
2202
2512
  return new TextEncoder3().encode(secret);
2203
2513
  }
2204
2514
  /**
@@ -2237,30 +2547,74 @@ var PreviewController = class {
2237
2547
  };
2238
2548
 
2239
2549
  // src/middleware/auth.ts
2240
- function requireAuth() {
2550
+ function getBearerToken(c) {
2551
+ const authHeader = c.req.header("Authorization");
2552
+ return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
2553
+ }
2554
+ async function resolveUser(token, config) {
2555
+ const payload = await verifyCollectionToken(token);
2556
+ if (payload.purpose) {
2557
+ return payload;
2558
+ }
2559
+ const db = config?.db;
2560
+ if (!db) {
2561
+ return payload;
2562
+ }
2563
+ let doc;
2564
+ try {
2565
+ doc = await db.findOne({
2566
+ collection: payload.collection,
2567
+ id: payload.sub
2568
+ });
2569
+ } catch (err) {
2570
+ console.error("[dyrected/core] Failed to hydrate user from token:", err);
2571
+ return payload;
2572
+ }
2573
+ if (!doc) {
2574
+ return null;
2575
+ }
2576
+ const { password: _password, ...safe } = doc;
2577
+ return {
2578
+ ...safe,
2579
+ sub: payload.sub,
2580
+ email: payload.email ?? safe.email,
2581
+ collection: payload.collection
2582
+ };
2583
+ }
2584
+ function requireAuth(config) {
2241
2585
  return async (c, next) => {
2242
- const authHeader = c.req.header("Authorization");
2243
- const token = authHeader?.replace(/^Bearer\s+/i, "");
2586
+ if (c.get("user")) {
2587
+ return next();
2588
+ }
2589
+ const token = getBearerToken(c);
2244
2590
  if (!token) {
2245
2591
  return c.json({ error: true, message: "Authentication required." }, 401);
2246
2592
  }
2593
+ let user;
2247
2594
  try {
2248
- const user = await verifyCollectionToken(token);
2249
- c.set("user", user);
2250
- await next();
2595
+ user = await resolveUser(token, config ?? c.get("config"));
2251
2596
  } catch {
2252
2597
  return c.json({ error: true, message: "Invalid or expired token." }, 401);
2253
2598
  }
2599
+ if (!user) {
2600
+ return c.json({ error: true, message: "Invalid or expired token." }, 401);
2601
+ }
2602
+ c.set("user", user);
2603
+ await next();
2254
2604
  };
2255
2605
  }
2256
- function optionalAuth() {
2606
+ function optionalAuth(config) {
2257
2607
  return async (c, next) => {
2258
- const authHeader = c.req.header("Authorization");
2259
- const token = authHeader?.replace(/^Bearer\s+/i, "");
2608
+ if (c.get("user")) {
2609
+ return next();
2610
+ }
2611
+ const token = getBearerToken(c);
2260
2612
  if (token) {
2261
2613
  try {
2262
- const user = await verifyCollectionToken(token);
2263
- c.set("user", user);
2614
+ const user = await resolveUser(token, config ?? c.get("config"));
2615
+ if (user) {
2616
+ c.set("user", user);
2617
+ }
2264
2618
  } catch {
2265
2619
  }
2266
2620
  }
@@ -2323,37 +2677,23 @@ function getSwaggerHtml(specUrl) {
2323
2677
  }
2324
2678
 
2325
2679
  // src/router.ts
2326
- function accessGate(target, action) {
2680
+ function accessGate(config, target, action) {
2327
2681
  return async (c, next) => {
2328
2682
  const user = c.get("user");
2329
2683
  const accessExpr = target.access?.[action];
2330
2684
  if (accessExpr === void 0 || accessExpr === null) {
2331
2685
  return await next();
2332
2686
  }
2333
- const accessArgs = { user, req: c.req, doc: null };
2334
- const allowed = await evaluateAccess(accessExpr, accessArgs);
2687
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
2688
+ user,
2689
+ req: toHookRequestContext(c.req)
2690
+ });
2335
2691
  if (!allowed) {
2336
2692
  return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
2337
2693
  }
2338
2694
  await next();
2339
2695
  };
2340
2696
  }
2341
- async function checkAccess(access, accessArgs) {
2342
- if (access === void 0 || access === null) return true;
2343
- if (typeof access === "function") {
2344
- try {
2345
- const result = await access(accessArgs);
2346
- return typeof result === "boolean" ? result : !!result;
2347
- } catch (err) {
2348
- console.error("[dyrected/core] Functional access check failed:", err);
2349
- return false;
2350
- }
2351
- }
2352
- if (typeof access === "string" || typeof access === "boolean") {
2353
- return evaluateAccess(access, accessArgs);
2354
- }
2355
- return true;
2356
- }
2357
2697
  function serializeFieldForApi(f) {
2358
2698
  if (!f) return f;
2359
2699
  const serialized = { ...f };
@@ -2382,7 +2722,7 @@ function serializeFieldForApi(f) {
2382
2722
  return serialized;
2383
2723
  }
2384
2724
  function registerRoutes(app, config) {
2385
- app.get("/api/schemas", optionalAuth(), async (c) => {
2725
+ app.get("/api/schemas", optionalAuth(config), async (c) => {
2386
2726
  const siteId = c.req.header("X-Site-Id");
2387
2727
  let collections = [...config.collections];
2388
2728
  let globals = [...config.globals];
@@ -2398,11 +2738,11 @@ function registerRoutes(app, config) {
2398
2738
  }
2399
2739
  }
2400
2740
  const user = c.get("user");
2401
- const accessArgs = { user, req: c.req, doc: null };
2741
+ const accessArgs = { user, req: toHookRequestContext(c.req) };
2402
2742
  const serializeAccess = async (access) => {
2403
2743
  if (typeof access === "string") return access;
2404
2744
  if (typeof access === "boolean") return access;
2405
- return checkAccess(access, accessArgs);
2745
+ return resolveBooleanAccess(config, access, accessArgs);
2406
2746
  };
2407
2747
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
2408
2748
  slug: col.slug,
@@ -2488,7 +2828,7 @@ function registerRoutes(app, config) {
2488
2828
  adminAuth: getPublicAdminAuthConfig(config.adminAuth, collections)
2489
2829
  });
2490
2830
  });
2491
- app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
2831
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
2492
2832
  const { collection: colSlug, field: fieldName } = c.req.param();
2493
2833
  const siteId = c.req.header("X-Site-Id");
2494
2834
  let collections = [...config.collections];
@@ -2502,8 +2842,10 @@ function registerRoutes(app, config) {
2502
2842
  if (collection) {
2503
2843
  const accessExpr = collection.access?.read;
2504
2844
  if (accessExpr !== void 0 && accessExpr !== null) {
2505
- const accessArgs = { user, req: c.req, doc: null };
2506
- const allowed = await checkAccess(accessExpr, accessArgs);
2845
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
2846
+ user,
2847
+ req: toHookRequestContext(c.req)
2848
+ });
2507
2849
  if (!allowed) {
2508
2850
  return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
2509
2851
  }
@@ -2521,8 +2863,10 @@ function registerRoutes(app, config) {
2521
2863
  }
2522
2864
  const accessExpr = glb.access?.read;
2523
2865
  if (accessExpr !== void 0 && accessExpr !== null) {
2524
- const accessArgs = { user, req: c.req, doc: null };
2525
- const allowed = await checkAccess(accessExpr, accessArgs);
2866
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
2867
+ user,
2868
+ req: toHookRequestContext(c.req)
2869
+ });
2526
2870
  if (!allowed) {
2527
2871
  return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
2528
2872
  }
@@ -2566,7 +2910,7 @@ function registerRoutes(app, config) {
2566
2910
  app.get("/api/docs", (c) => {
2567
2911
  return c.html(getSwaggerHtml());
2568
2912
  });
2569
- app.get("/api/preferences/:key", requireAuth(), async (c) => {
2913
+ app.get("/api/preferences/:key", requireAuth(config), async (c) => {
2570
2914
  const db = config.db;
2571
2915
  const user = c.get("user");
2572
2916
  const key = c.req.param("key");
@@ -2591,7 +2935,7 @@ function registerRoutes(app, config) {
2591
2935
  const globalValue = await getGlobalPreference();
2592
2936
  return c.json({ key, value: globalValue });
2593
2937
  });
2594
- app.put("/api/preferences/:key", requireAuth(), async (c) => {
2938
+ app.put("/api/preferences/:key", requireAuth(config), async (c) => {
2595
2939
  const db = config.db;
2596
2940
  const user = c.get("user");
2597
2941
  const key = c.req.param("key");
@@ -2631,7 +2975,7 @@ function registerRoutes(app, config) {
2631
2975
  });
2632
2976
  return c.json({ key, value: body.value });
2633
2977
  });
2634
- app.delete("/api/preferences/:key", requireAuth(), async (c) => {
2978
+ app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
2635
2979
  const db = config.db;
2636
2980
  const user = c.get("user");
2637
2981
  const key = c.req.param("key");
@@ -2671,10 +3015,10 @@ function registerRoutes(app, config) {
2671
3015
  for (const col of uploadCollections) {
2672
3016
  const mediaController = new MediaController(col.slug);
2673
3017
  const prefix = `/api/collections/${col.slug}`;
2674
- app.get(`${prefix}/media`, accessGate(col, "read"), (c) => mediaController.find(c));
3018
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
2675
3019
  app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
2676
- app.post(`${prefix}/media`, accessGate(col, "create"), (c) => mediaController.upload(c));
2677
- app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
3020
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
3021
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
2678
3022
  }
2679
3023
  }
2680
3024
  const adminAuthController = new AdminAuthController(config);
@@ -2691,43 +3035,43 @@ function registerRoutes(app, config) {
2691
3035
  app.post(`${path}/logout`, (c) => authController.logout(c));
2692
3036
  app.get(`${path}/init`, (c) => authController.init(c));
2693
3037
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
2694
- app.get(`${path}/me`, requireAuth(), (c) => authController.me(c));
2695
- app.post(`${path}/refresh-token`, requireAuth(), (c) => authController.refreshToken(c));
3038
+ app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
3039
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
2696
3040
  app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
2697
3041
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
2698
- app.post(`${path}/invite`, requireAuth(), (c) => authController.invite(c));
3042
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
2699
3043
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
2700
3044
  }
2701
3045
  for (const collection of config.collections) {
2702
3046
  const path = `/api/collections/${collection.slug}`;
2703
3047
  const controller = new CollectionController(collection);
2704
- app.get(path, accessGate(collection, "read"), (c) => controller.find(c));
2705
- app.post(path, accessGate(collection, "create"), (c) => controller.create(c));
2706
- app.post(`${path}/media`, accessGate(collection, "create"), (c) => controller.create(c));
2707
- app.delete(`${path}/delete-many`, accessGate(collection, "delete"), (c) => controller.deleteMany(c));
2708
- app.get(`${path}/:id`, accessGate(collection, "read"), (c) => controller.findOne(c));
2709
- app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
2710
- app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
3048
+ app.get(path, (c) => controller.find(c));
3049
+ app.post(path, (c) => controller.create(c));
3050
+ app.post(`${path}/media`, (c) => controller.create(c));
3051
+ app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
3052
+ app.get(`${path}/:id`, (c) => controller.findOne(c));
3053
+ app.patch(`${path}/:id`, (c) => controller.update(c));
3054
+ app.delete(`${path}/:id`, (c) => controller.delete(c));
2711
3055
  app.post(`${path}/seed`, (c) => controller.seed(c));
2712
3056
  if (collection.auth) {
2713
- app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
3057
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
2714
3058
  }
2715
3059
  if (collection.workflow) {
2716
- app.post(`${path}/:id/transitions/:transition`, requireAuth(), (c) => controller.transition(c));
2717
- app.get(`${path}/:id/workflow-history`, requireAuth(), (c) => controller.workflowHistory(c));
3060
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
3061
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
2718
3062
  }
2719
3063
  }
2720
3064
  for (const global of config.globals) {
2721
3065
  const path = `/api/globals/${global.slug}`;
2722
3066
  const controller = new GlobalController(global);
2723
- app.get(path, accessGate(global, "read"), (c) => controller.get(c));
2724
- app.patch(path, accessGate(global, "update"), (c) => controller.update(c));
3067
+ app.get(path, (c) => controller.get(c));
3068
+ app.patch(path, (c) => controller.update(c));
2725
3069
  app.post(`${path}/seed`, (c) => controller.seed(c));
2726
3070
  }
2727
3071
  const previewController = new PreviewController();
2728
- app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
3072
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
2729
3073
  app.get("/api/preview-data", (c) => previewController.getData(c));
2730
- app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(), async (c) => {
3074
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
2731
3075
  const slug = c.req.param("slug");
2732
3076
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2733
3077
  const config2 = c.get("config");
@@ -2745,7 +3089,7 @@ function registerRoutes(app, config) {
2745
3089
  const controller = new CollectionController(collection);
2746
3090
  return controller.transition(c);
2747
3091
  });
2748
- app.get("/api/collections/:slug/:id/workflow-history", requireAuth(), async (c) => {
3092
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
2749
3093
  const slug = c.req.param("slug");
2750
3094
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2751
3095
  const config2 = c.get("config");
@@ -2844,7 +3188,7 @@ async function createDyrectedApp(rawConfig) {
2844
3188
  await config.db.sync(config.collections, config.globals);
2845
3189
  }
2846
3190
  app.use("*", requestId());
2847
- app.use("*", optionalAuth());
3191
+ app.use("*", optionalAuth(config));
2848
3192
  app.use("*", async (c, next) => {
2849
3193
  const start = Date.now();
2850
3194
  await next();