@dyrected/core 2.5.56 → 2.5.59

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.cjs CHANGED
@@ -599,6 +599,9 @@ async function evaluateAccess(expression, context) {
599
599
  if (typeof expression === "boolean") return expression;
600
600
  try {
601
601
  const result = await import_jexl2.default.eval(expression, context);
602
+ if (result && typeof result === "object" && !Array.isArray(result)) {
603
+ return result;
604
+ }
602
605
  return !!result;
603
606
  } catch (err) {
604
607
  console.error("[dyrected/core] Jexl evaluation failed:", err);
@@ -606,6 +609,39 @@ async function evaluateAccess(expression, context) {
606
609
  }
607
610
  }
608
611
 
612
+ // src/auth/access.ts
613
+ function isNamedAccessPolicy(value) {
614
+ return !!value && typeof value === "object" && "policy" in value && typeof value.policy === "string";
615
+ }
616
+ async function resolveAccess(config, access, args) {
617
+ if (access === void 0 || access === null) return void 0;
618
+ if (typeof access === "boolean" || typeof access === "string") {
619
+ return evaluateAccess(access, args);
620
+ }
621
+ if (typeof access === "function") {
622
+ try {
623
+ return await access(args);
624
+ } catch (err) {
625
+ console.error("[dyrected/core] Functional access check failed:", err);
626
+ return false;
627
+ }
628
+ }
629
+ if (isNamedAccessPolicy(access)) {
630
+ const resolver = config.accessPolicies?.[access.policy];
631
+ if (!resolver) {
632
+ console.error(`[dyrected/core] Unknown access policy "${access.policy}".`);
633
+ return false;
634
+ }
635
+ try {
636
+ return await resolver({ ...args, params: access.params });
637
+ } catch (err) {
638
+ console.error(`[dyrected/core] Access policy "${access.policy}" failed:`, err);
639
+ return false;
640
+ }
641
+ }
642
+ return false;
643
+ }
644
+
609
645
  // src/utils/admin-auth.ts
610
646
  function getAdminAuthCollection(config) {
611
647
  const adminCollection = resolveAdminAuthCollection(config.collections, config.adminAuth?.collectionSlug);
@@ -641,6 +677,148 @@ function humanizeProviderName(id, type) {
641
677
  return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
642
678
  }
643
679
 
680
+ // src/utils/access-control.ts
681
+ function toHookRequestContext(req) {
682
+ return {
683
+ query: req.query(),
684
+ headers: req.header(),
685
+ raw: req.raw
686
+ };
687
+ }
688
+ async function resolveBooleanAccess(config, access, args) {
689
+ const result = await resolveAccess(config, access, args);
690
+ if (result === void 0) return true;
691
+ return typeof result === "boolean" ? result : false;
692
+ }
693
+ async function matchesAccessConstraint(config, collection, id, constraint) {
694
+ if (!config.db) return false;
695
+ const match = await config.db.find({
696
+ collection,
697
+ where: { AND: [{ id: { equals: id } }, constraint] },
698
+ limit: 1
699
+ });
700
+ return match.total > 0;
701
+ }
702
+ async function resolveCollectionAccess(config, collection, action, access, args) {
703
+ const result = await resolveAccess(config, access, args);
704
+ if (result === void 0) return { allowed: true };
705
+ if (typeof result === "boolean") return { allowed: result };
706
+ if (action === "read" && !args.id) {
707
+ return { allowed: true, constraint: result };
708
+ }
709
+ if (!args.id) {
710
+ return { allowed: false };
711
+ }
712
+ return {
713
+ allowed: await matchesAccessConstraint(config, collection, args.id, result)
714
+ };
715
+ }
716
+ function resolveFieldAccessId(context) {
717
+ const candidate = context.id ?? context.doc?.id ?? context.data?.id;
718
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
719
+ }
720
+ async function applyFieldReadAccess(context, doc) {
721
+ if (!doc || typeof doc !== "object") return doc;
722
+ const result = { ...doc };
723
+ for (const field of context.fields) {
724
+ if (field.type === "row" && field.fields) {
725
+ Object.assign(result, await applyFieldReadAccess({ ...context, fields: field.fields }, result));
726
+ continue;
727
+ }
728
+ if (!field.name) continue;
729
+ const value = result[field.name];
730
+ const canRead = await resolveBooleanAccess(context.config, field.access?.read, {
731
+ user: context.user,
732
+ req: context.req,
733
+ doc: context.doc,
734
+ data: context.data,
735
+ id: resolveFieldAccessId(context)
736
+ });
737
+ if (!canRead) {
738
+ delete result[field.name];
739
+ continue;
740
+ }
741
+ if (value === void 0 || value === null) continue;
742
+ if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
743
+ result[field.name] = await applyFieldReadAccess({ ...context, fields: field.fields }, value);
744
+ continue;
745
+ }
746
+ if (field.type === "array" && field.fields && Array.isArray(value)) {
747
+ const childFields = field.fields;
748
+ result[field.name] = await Promise.all(
749
+ value.map(
750
+ (item) => typeof item === "object" && item !== null ? applyFieldReadAccess({ ...context, fields: childFields }, item) : item
751
+ )
752
+ );
753
+ continue;
754
+ }
755
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
756
+ result[field.name] = await Promise.all(
757
+ value.map(async (item) => {
758
+ if (typeof item !== "object" || item === null) return item;
759
+ const typedItem = item;
760
+ const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
761
+ if (!block || !block.fields) return item;
762
+ return applyFieldReadAccess({ ...context, fields: block.fields }, typedItem);
763
+ })
764
+ );
765
+ }
766
+ }
767
+ return result;
768
+ }
769
+ async function applyFieldWriteAccess(context, data) {
770
+ if (!data || typeof data !== "object") return data;
771
+ const result = { ...data };
772
+ for (const field of context.fields) {
773
+ if (field.type === "row" && field.fields) {
774
+ Object.assign(result, await applyFieldWriteAccess({ ...context, fields: field.fields }, result));
775
+ continue;
776
+ }
777
+ if (!field.name || !(field.name in result)) continue;
778
+ const canUpdate = await resolveBooleanAccess(context.config, field.access?.update, {
779
+ user: context.user,
780
+ req: context.req,
781
+ doc: context.doc,
782
+ data: context.data,
783
+ id: resolveFieldAccessId(context)
784
+ });
785
+ if (!canUpdate) {
786
+ delete result[field.name];
787
+ continue;
788
+ }
789
+ const value = result[field.name];
790
+ if (value === void 0 || value === null) continue;
791
+ if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
792
+ result[field.name] = await applyFieldWriteAccess({ ...context, fields: field.fields }, value);
793
+ continue;
794
+ }
795
+ if (field.type === "array" && field.fields && Array.isArray(value)) {
796
+ const childFields = field.fields;
797
+ result[field.name] = await Promise.all(
798
+ value.map(
799
+ (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess({ ...context, fields: childFields }, item) : item
800
+ )
801
+ );
802
+ continue;
803
+ }
804
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
805
+ result[field.name] = await Promise.all(
806
+ value.map(async (item) => {
807
+ if (typeof item !== "object" || item === null) return item;
808
+ const typedItem = item;
809
+ const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
810
+ if (!block || !block.fields) return item;
811
+ return applyFieldWriteAccess({ ...context, fields: block.fields }, typedItem);
812
+ })
813
+ );
814
+ }
815
+ }
816
+ return result;
817
+ }
818
+ function mergeWhereConstraint(where, constraint) {
819
+ return where ? { AND: [where, constraint] } : constraint;
820
+ }
821
+
644
822
  // src/workflows.ts
645
823
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
646
824
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -879,11 +1057,23 @@ var CollectionController = class {
879
1057
  return config.adminAuth?.providers?.find((p) => p.members) || null;
880
1058
  }
881
1059
  toHookRequestContext(c) {
882
- return {
883
- query: c.req.query(),
884
- headers: c.req.header(),
885
- raw: c.req.raw
886
- };
1060
+ return toHookRequestContext(c.req);
1061
+ }
1062
+ async evaluateAccess(c, action, options = {}) {
1063
+ const config = c.get("config");
1064
+ return resolveCollectionAccess(
1065
+ config,
1066
+ this.collection.slug,
1067
+ action,
1068
+ this.collection.access?.[action],
1069
+ {
1070
+ id: options.id,
1071
+ user: c.get("user"),
1072
+ req: this.toHookRequestContext(c),
1073
+ doc: options.doc ?? void 0,
1074
+ data: options.data
1075
+ }
1076
+ );
887
1077
  }
888
1078
  async find(c) {
889
1079
  const config = c.get("config");
@@ -963,6 +1153,13 @@ var CollectionController = class {
963
1153
  if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
964
1154
  where = where ? { AND: [where, { __published: { exists: true } }] } : { __published: { exists: true } };
965
1155
  }
1156
+ const access = await this.evaluateAccess(c, "read");
1157
+ if (!access.allowed) {
1158
+ return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
1159
+ }
1160
+ if (access.constraint) {
1161
+ where = mergeWhereConstraint(where, access.constraint);
1162
+ }
966
1163
  let result = await db.find({
967
1164
  collection: this.collection.slug,
968
1165
  limit,
@@ -995,7 +1192,14 @@ var CollectionController = class {
995
1192
  db: readonlyDb
996
1193
  });
997
1194
  const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
998
- processedDocs.push(docWithFieldHooks);
1195
+ const docWithFieldAccess = await applyFieldReadAccess({
1196
+ config,
1197
+ fields: this.collection.fields,
1198
+ user,
1199
+ req: this.toHookRequestContext(c),
1200
+ doc: docWithFieldHooks
1201
+ }, docWithFieldHooks);
1202
+ processedDocs.push(docWithFieldAccess);
999
1203
  }
1000
1204
  result.docs = processedDocs;
1001
1205
  if (depth > 0) {
@@ -1040,6 +1244,10 @@ var CollectionController = class {
1040
1244
  doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
1041
1245
  if (!doc) return c.json({ message: "Not Found" }, 404);
1042
1246
  }
1247
+ const access = await this.evaluateAccess(c, "read", { id, doc });
1248
+ if (!access.allowed) {
1249
+ return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
1250
+ }
1043
1251
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
1044
1252
  const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
1045
1253
  doc: docWithDefaults,
@@ -1048,17 +1256,24 @@ var CollectionController = class {
1048
1256
  db: readonlyDb
1049
1257
  });
1050
1258
  const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
1051
- if (depth > 0 && docWithFieldHooks) {
1259
+ const docWithFieldAccess = await applyFieldReadAccess({
1260
+ config,
1261
+ fields: this.collection.fields,
1262
+ user,
1263
+ req: this.toHookRequestContext(c),
1264
+ doc: docWithFieldHooks
1265
+ }, docWithFieldHooks);
1266
+ if (depth > 0 && docWithFieldAccess) {
1052
1267
  const populationService = new PopulationService(db, config.collections);
1053
1268
  const populatedDoc = await populationService.populate({
1054
- data: docWithFieldHooks,
1269
+ data: docWithFieldAccess,
1055
1270
  fields: this.collection.fields,
1056
1271
  currentDepth: 0,
1057
1272
  maxDepth: depth
1058
1273
  });
1059
1274
  return c.json(populatedDoc);
1060
1275
  }
1061
- return c.json(docWithFieldHooks);
1276
+ return c.json(docWithFieldAccess);
1062
1277
  }
1063
1278
  async create(c) {
1064
1279
  const config = c.get("config");
@@ -1097,9 +1312,20 @@ var CollectionController = class {
1097
1312
  if (this.collection.workflow) {
1098
1313
  data = initializeWorkflowDocument(data, this.collection.workflow);
1099
1314
  }
1315
+ const createAccess = await this.evaluateAccess(c, "create", { data });
1316
+ if (!createAccess.allowed) {
1317
+ return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
1318
+ }
1100
1319
  if (this.collection.auth && data.password) {
1101
1320
  data.password = await hashPassword(data.password);
1102
1321
  }
1322
+ data = await applyFieldWriteAccess({
1323
+ config,
1324
+ fields: this.collection.fields,
1325
+ user,
1326
+ req: this.toHookRequestContext(c),
1327
+ data
1328
+ }, data);
1103
1329
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
1104
1330
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1105
1331
  data,
@@ -1134,7 +1360,14 @@ var CollectionController = class {
1134
1360
  db: readonlyDb
1135
1361
  });
1136
1362
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1137
- return c.json(finalDoc, 201);
1363
+ const accessibleDoc = await applyFieldReadAccess({
1364
+ config,
1365
+ fields: this.collection.fields,
1366
+ user,
1367
+ req: this.toHookRequestContext(c),
1368
+ doc: finalDoc
1369
+ }, finalDoc);
1370
+ return c.json(accessibleDoc, 201);
1138
1371
  }
1139
1372
  async upload(c) {
1140
1373
  const config = c.get("config");
@@ -1177,6 +1410,17 @@ var CollectionController = class {
1177
1410
  createdBy: user?.sub ?? null,
1178
1411
  updatedBy: user?.sub ?? null
1179
1412
  };
1413
+ const createAccess = await this.evaluateAccess(c, "create", { data });
1414
+ if (!createAccess.allowed) {
1415
+ return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
1416
+ }
1417
+ data = await applyFieldWriteAccess({
1418
+ config,
1419
+ fields: this.collection.fields,
1420
+ user,
1421
+ req: this.toHookRequestContext(c),
1422
+ data
1423
+ }, data);
1180
1424
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
1181
1425
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1182
1426
  data,
@@ -1204,7 +1448,14 @@ var CollectionController = class {
1204
1448
  db: readonlyDb
1205
1449
  });
1206
1450
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1207
- return c.json(finalDoc, 201);
1451
+ const accessibleDoc = await applyFieldReadAccess({
1452
+ config,
1453
+ fields: this.collection.fields,
1454
+ user,
1455
+ req: this.toHookRequestContext(c),
1456
+ doc: finalDoc
1457
+ }, finalDoc);
1458
+ return c.json(accessibleDoc, 201);
1208
1459
  }
1209
1460
  async update(c) {
1210
1461
  const config = c.get("config");
@@ -1242,10 +1493,22 @@ var CollectionController = class {
1242
1493
  });
1243
1494
  const originalDoc = await db.findOne({ collection: this.collection.slug, id });
1244
1495
  if (!originalDoc) return c.json({ message: "Not Found" }, 404);
1496
+ const updateAccess = await this.evaluateAccess(c, "update", { id, doc: originalDoc, data });
1497
+ if (!updateAccess.allowed) {
1498
+ return c.json({ error: true, message: `Access denied: update on ${this.collection.slug}` }, 403);
1499
+ }
1245
1500
  let before = null;
1246
1501
  if (this.collection.audit) {
1247
1502
  before = originalDoc;
1248
1503
  }
1504
+ data = await applyFieldWriteAccess({
1505
+ config,
1506
+ fields: this.collection.fields,
1507
+ user,
1508
+ req: this.toHookRequestContext(c),
1509
+ doc: originalDoc,
1510
+ data
1511
+ }, data);
1249
1512
  data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user, readonlyDb);
1250
1513
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1251
1514
  data,
@@ -1281,7 +1544,14 @@ var CollectionController = class {
1281
1544
  db: readonlyDb
1282
1545
  });
1283
1546
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1284
- return c.json(finalDoc);
1547
+ const accessibleDoc = await applyFieldReadAccess({
1548
+ config,
1549
+ fields: this.collection.fields,
1550
+ user,
1551
+ req: this.toHookRequestContext(c),
1552
+ doc: finalDoc
1553
+ }, finalDoc);
1554
+ return c.json(accessibleDoc);
1285
1555
  }
1286
1556
  async transition(c) {
1287
1557
  const config = c.get("config");
@@ -1318,7 +1588,7 @@ var CollectionController = class {
1318
1588
  const readAccess = this.collection.access?.read;
1319
1589
  if (readAccess !== void 0 && readAccess !== null) {
1320
1590
  const args = { user: c.get("user"), req: c.req, doc: document };
1321
- const result2 = typeof readAccess === "function" ? await readAccess(args) : await evaluateAccess(readAccess, args);
1591
+ const result2 = await resolveAccess(config, readAccess, args);
1322
1592
  let allowed = result2 === true;
1323
1593
  if (result2 && typeof result2 === "object") {
1324
1594
  const match = await config.db.find({
@@ -1429,6 +1699,10 @@ var CollectionController = class {
1429
1699
  const user = c.get("user");
1430
1700
  const doc = await db.findOne({ collection: this.collection.slug, id });
1431
1701
  if (!doc) return c.json({ message: "Not Found" }, 404);
1702
+ const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1703
+ if (!deleteAccess.allowed) {
1704
+ return c.json({ error: true, message: `Access denied: delete on ${this.collection.slug}` }, 403);
1705
+ }
1432
1706
  let before = null;
1433
1707
  if (this.collection.audit) {
1434
1708
  before = doc;
@@ -1488,6 +1762,11 @@ var CollectionController = class {
1488
1762
  failed.push({ id, error: "Not Found" });
1489
1763
  continue;
1490
1764
  }
1765
+ const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1766
+ if (!deleteAccess.allowed) {
1767
+ failed.push({ id, error: "Access denied" });
1768
+ continue;
1769
+ }
1491
1770
  let before = null;
1492
1771
  if (this.collection.audit) {
1493
1772
  before = doc;
@@ -1581,6 +1860,14 @@ var GlobalController = class {
1581
1860
  await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
1582
1861
  data = this.global.initialData;
1583
1862
  }
1863
+ const canRead = await resolveBooleanAccess(config, this.global.access?.read, {
1864
+ user,
1865
+ req: toHookRequestContext(c.req),
1866
+ doc: data
1867
+ });
1868
+ if (!canRead) {
1869
+ return c.json({ error: true, message: `Access denied: read on ${this.global.slug}` }, 403);
1870
+ }
1584
1871
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1585
1872
  const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
1586
1873
  doc: dataWithDefaults,
@@ -1589,17 +1876,24 @@ var GlobalController = class {
1589
1876
  db: readonlyDb
1590
1877
  });
1591
1878
  const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user, readonlyDb);
1592
- if (depth > 0 && docWithFieldHooks) {
1879
+ const docWithFieldAccess = await applyFieldReadAccess({
1880
+ config,
1881
+ fields: this.global.fields,
1882
+ user,
1883
+ req: toHookRequestContext(c.req),
1884
+ doc: docWithFieldHooks
1885
+ }, docWithFieldHooks);
1886
+ if (depth > 0 && docWithFieldAccess) {
1593
1887
  const populationService = new PopulationService(db, config.collections);
1594
1888
  const populatedData = await populationService.populate({
1595
- data: docWithFieldHooks,
1889
+ data: docWithFieldAccess,
1596
1890
  fields: this.global.fields,
1597
1891
  currentDepth: 0,
1598
1892
  maxDepth: depth
1599
1893
  });
1600
1894
  return c.json(populatedData);
1601
1895
  }
1602
- return c.json(docWithFieldHooks);
1896
+ return c.json(docWithFieldAccess);
1603
1897
  }
1604
1898
  async update(c) {
1605
1899
  const config = c.get("config");
@@ -1609,7 +1903,24 @@ var GlobalController = class {
1609
1903
  const body = await c.req.json();
1610
1904
  const user = c.get("user");
1611
1905
  const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
1612
- let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user, readonlyDb);
1906
+ const canUpdate = await resolveBooleanAccess(config, this.global.access?.update, {
1907
+ user,
1908
+ req: toHookRequestContext(c.req),
1909
+ doc: originalDoc,
1910
+ data: body
1911
+ });
1912
+ if (!canUpdate) {
1913
+ return c.json({ error: true, message: `Access denied: update on ${this.global.slug}` }, 403);
1914
+ }
1915
+ let sanitizedBody = await applyFieldWriteAccess({
1916
+ config,
1917
+ fields: this.global.fields,
1918
+ user,
1919
+ req: toHookRequestContext(c.req),
1920
+ doc: originalDoc,
1921
+ data: body
1922
+ }, body);
1923
+ let data = await executeFieldBeforeChange(this.global.fields, sanitizedBody, originalDoc, user, readonlyDb);
1613
1924
  data = await runCollectionHooks(this.global.hooks?.beforeChange, {
1614
1925
  data,
1615
1926
  doc: originalDoc,
@@ -1634,7 +1945,14 @@ var GlobalController = class {
1634
1945
  db: readonlyDb
1635
1946
  });
1636
1947
  const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user, readonlyDb);
1637
- return c.json(finalDoc);
1948
+ const accessibleDoc = await applyFieldReadAccess({
1949
+ config,
1950
+ fields: this.global.fields,
1951
+ user,
1952
+ req: toHookRequestContext(c.req),
1953
+ doc: finalDoc
1954
+ }, finalDoc);
1955
+ return c.json(accessibleDoc);
1638
1956
  }
1639
1957
  async seed(c) {
1640
1958
  const config = c.get("config");
@@ -1816,7 +2134,7 @@ var MediaController = class {
1816
2134
  var import_jose = require("jose");
1817
2135
  var import_node_util2 = require("util");
1818
2136
  function getSecret() {
1819
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
2137
+ const secret = process.env.DYRECTED_JWT_SECRET;
1820
2138
  if (!secret) {
1821
2139
  throw new Error(
1822
2140
  "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
@@ -2686,7 +3004,7 @@ var AdminAuthController = class {
2686
3004
  return state;
2687
3005
  }
2688
3006
  getStateSecret() {
2689
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
3007
+ const secret = process.env.DYRECTED_JWT_SECRET;
2690
3008
  if (!secret) {
2691
3009
  throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
2692
3010
  }
@@ -2704,7 +3022,7 @@ var import_jose3 = require("jose");
2704
3022
  var import_node_util4 = require("util");
2705
3023
  var PreviewController = class {
2706
3024
  getSecret() {
2707
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
3025
+ const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
2708
3026
  return new import_node_util4.TextEncoder().encode(secret);
2709
3027
  }
2710
3028
  /**
@@ -2743,30 +3061,74 @@ var PreviewController = class {
2743
3061
  };
2744
3062
 
2745
3063
  // src/middleware/auth.ts
2746
- function requireAuth() {
3064
+ function getBearerToken(c) {
3065
+ const authHeader = c.req.header("Authorization");
3066
+ return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
3067
+ }
3068
+ async function resolveUser(token, config) {
3069
+ const payload = await verifyCollectionToken(token);
3070
+ if (payload.purpose) {
3071
+ return payload;
3072
+ }
3073
+ const db = config?.db;
3074
+ if (!db) {
3075
+ return payload;
3076
+ }
3077
+ let doc;
3078
+ try {
3079
+ doc = await db.findOne({
3080
+ collection: payload.collection,
3081
+ id: payload.sub
3082
+ });
3083
+ } catch (err) {
3084
+ console.error("[dyrected/core] Failed to hydrate user from token:", err);
3085
+ return payload;
3086
+ }
3087
+ if (!doc) {
3088
+ return null;
3089
+ }
3090
+ const { password: _password, ...safe } = doc;
3091
+ return {
3092
+ ...safe,
3093
+ sub: payload.sub,
3094
+ email: payload.email ?? safe.email,
3095
+ collection: payload.collection
3096
+ };
3097
+ }
3098
+ function requireAuth(config) {
2747
3099
  return async (c, next) => {
2748
- const authHeader = c.req.header("Authorization");
2749
- const token = authHeader?.replace(/^Bearer\s+/i, "");
3100
+ if (c.get("user")) {
3101
+ return next();
3102
+ }
3103
+ const token = getBearerToken(c);
2750
3104
  if (!token) {
2751
3105
  return c.json({ error: true, message: "Authentication required." }, 401);
2752
3106
  }
3107
+ let user;
2753
3108
  try {
2754
- const user = await verifyCollectionToken(token);
2755
- c.set("user", user);
2756
- await next();
3109
+ user = await resolveUser(token, config ?? c.get("config"));
2757
3110
  } catch {
2758
3111
  return c.json({ error: true, message: "Invalid or expired token." }, 401);
2759
3112
  }
3113
+ if (!user) {
3114
+ return c.json({ error: true, message: "Invalid or expired token." }, 401);
3115
+ }
3116
+ c.set("user", user);
3117
+ await next();
2760
3118
  };
2761
3119
  }
2762
- function optionalAuth() {
3120
+ function optionalAuth(config) {
2763
3121
  return async (c, next) => {
2764
- const authHeader = c.req.header("Authorization");
2765
- const token = authHeader?.replace(/^Bearer\s+/i, "");
3122
+ if (c.get("user")) {
3123
+ return next();
3124
+ }
3125
+ const token = getBearerToken(c);
2766
3126
  if (token) {
2767
3127
  try {
2768
- const user = await verifyCollectionToken(token);
2769
- c.set("user", user);
3128
+ const user = await resolveUser(token, config ?? c.get("config"));
3129
+ if (user) {
3130
+ c.set("user", user);
3131
+ }
2770
3132
  } catch {
2771
3133
  }
2772
3134
  }
@@ -3654,37 +4016,23 @@ function getSwaggerHtml(specUrl) {
3654
4016
  }
3655
4017
 
3656
4018
  // src/router.ts
3657
- function accessGate(target, action) {
4019
+ function accessGate(config, target, action) {
3658
4020
  return async (c, next) => {
3659
4021
  const user = c.get("user");
3660
4022
  const accessExpr = target.access?.[action];
3661
4023
  if (accessExpr === void 0 || accessExpr === null) {
3662
4024
  return await next();
3663
4025
  }
3664
- const accessArgs = { user, req: c.req, doc: null };
3665
- const allowed = await evaluateAccess(accessExpr, accessArgs);
4026
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4027
+ user,
4028
+ req: toHookRequestContext(c.req)
4029
+ });
3666
4030
  if (!allowed) {
3667
4031
  return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
3668
4032
  }
3669
4033
  await next();
3670
4034
  };
3671
4035
  }
3672
- async function checkAccess(access, accessArgs) {
3673
- if (access === void 0 || access === null) return true;
3674
- if (typeof access === "function") {
3675
- try {
3676
- const result = await access(accessArgs);
3677
- return typeof result === "boolean" ? result : !!result;
3678
- } catch (err) {
3679
- console.error("[dyrected/core] Functional access check failed:", err);
3680
- return false;
3681
- }
3682
- }
3683
- if (typeof access === "string" || typeof access === "boolean") {
3684
- return evaluateAccess(access, accessArgs);
3685
- }
3686
- return true;
3687
- }
3688
4036
  function serializeFieldForApi(f) {
3689
4037
  if (!f) return f;
3690
4038
  const serialized = { ...f };
@@ -3713,7 +4061,7 @@ function serializeFieldForApi(f) {
3713
4061
  return serialized;
3714
4062
  }
3715
4063
  function registerRoutes(app, config) {
3716
- app.get("/api/schemas", optionalAuth(), async (c) => {
4064
+ app.get("/api/schemas", optionalAuth(config), async (c) => {
3717
4065
  const siteId = c.req.header("X-Site-Id");
3718
4066
  let collections = [...config.collections];
3719
4067
  let globals = [...config.globals];
@@ -3729,11 +4077,11 @@ function registerRoutes(app, config) {
3729
4077
  }
3730
4078
  }
3731
4079
  const user = c.get("user");
3732
- const accessArgs = { user, req: c.req, doc: null };
4080
+ const accessArgs = { user, req: toHookRequestContext(c.req) };
3733
4081
  const serializeAccess = async (access) => {
3734
4082
  if (typeof access === "string") return access;
3735
4083
  if (typeof access === "boolean") return access;
3736
- return checkAccess(access, accessArgs);
4084
+ return resolveBooleanAccess(config, access, accessArgs);
3737
4085
  };
3738
4086
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
3739
4087
  slug: col.slug,
@@ -3819,7 +4167,7 @@ function registerRoutes(app, config) {
3819
4167
  adminAuth: getPublicAdminAuthConfig(config.adminAuth, collections)
3820
4168
  });
3821
4169
  });
3822
- app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
4170
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
3823
4171
  const { collection: colSlug, field: fieldName } = c.req.param();
3824
4172
  const siteId = c.req.header("X-Site-Id");
3825
4173
  let collections = [...config.collections];
@@ -3833,8 +4181,10 @@ function registerRoutes(app, config) {
3833
4181
  if (collection) {
3834
4182
  const accessExpr = collection.access?.read;
3835
4183
  if (accessExpr !== void 0 && accessExpr !== null) {
3836
- const accessArgs = { user, req: c.req, doc: null };
3837
- const allowed = await checkAccess(accessExpr, accessArgs);
4184
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4185
+ user,
4186
+ req: toHookRequestContext(c.req)
4187
+ });
3838
4188
  if (!allowed) {
3839
4189
  return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
3840
4190
  }
@@ -3852,8 +4202,10 @@ function registerRoutes(app, config) {
3852
4202
  }
3853
4203
  const accessExpr = glb.access?.read;
3854
4204
  if (accessExpr !== void 0 && accessExpr !== null) {
3855
- const accessArgs = { user, req: c.req, doc: null };
3856
- const allowed = await checkAccess(accessExpr, accessArgs);
4205
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4206
+ user,
4207
+ req: toHookRequestContext(c.req)
4208
+ });
3857
4209
  if (!allowed) {
3858
4210
  return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
3859
4211
  }
@@ -3897,7 +4249,7 @@ function registerRoutes(app, config) {
3897
4249
  app.get("/api/docs", (c) => {
3898
4250
  return c.html(getSwaggerHtml());
3899
4251
  });
3900
- app.get("/api/preferences/:key", requireAuth(), async (c) => {
4252
+ app.get("/api/preferences/:key", requireAuth(config), async (c) => {
3901
4253
  const db = config.db;
3902
4254
  const user = c.get("user");
3903
4255
  const key = c.req.param("key");
@@ -3922,7 +4274,7 @@ function registerRoutes(app, config) {
3922
4274
  const globalValue = await getGlobalPreference();
3923
4275
  return c.json({ key, value: globalValue });
3924
4276
  });
3925
- app.put("/api/preferences/:key", requireAuth(), async (c) => {
4277
+ app.put("/api/preferences/:key", requireAuth(config), async (c) => {
3926
4278
  const db = config.db;
3927
4279
  const user = c.get("user");
3928
4280
  const key = c.req.param("key");
@@ -3962,7 +4314,7 @@ function registerRoutes(app, config) {
3962
4314
  });
3963
4315
  return c.json({ key, value: body.value });
3964
4316
  });
3965
- app.delete("/api/preferences/:key", requireAuth(), async (c) => {
4317
+ app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
3966
4318
  const db = config.db;
3967
4319
  const user = c.get("user");
3968
4320
  const key = c.req.param("key");
@@ -4002,10 +4354,10 @@ function registerRoutes(app, config) {
4002
4354
  for (const col of uploadCollections) {
4003
4355
  const mediaController = new MediaController(col.slug);
4004
4356
  const prefix = `/api/collections/${col.slug}`;
4005
- app.get(`${prefix}/media`, accessGate(col, "read"), (c) => mediaController.find(c));
4357
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
4006
4358
  app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
4007
- app.post(`${prefix}/media`, accessGate(col, "create"), (c) => mediaController.upload(c));
4008
- app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
4359
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
4360
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
4009
4361
  }
4010
4362
  }
4011
4363
  const adminAuthController = new AdminAuthController(config);
@@ -4022,43 +4374,43 @@ function registerRoutes(app, config) {
4022
4374
  app.post(`${path}/logout`, (c) => authController.logout(c));
4023
4375
  app.get(`${path}/init`, (c) => authController.init(c));
4024
4376
  app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
4025
- app.get(`${path}/me`, requireAuth(), (c) => authController.me(c));
4026
- app.post(`${path}/refresh-token`, requireAuth(), (c) => authController.refreshToken(c));
4377
+ app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
4378
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
4027
4379
  app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
4028
4380
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
4029
- app.post(`${path}/invite`, requireAuth(), (c) => authController.invite(c));
4381
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
4030
4382
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
4031
4383
  }
4032
4384
  for (const collection of config.collections) {
4033
4385
  const path = `/api/collections/${collection.slug}`;
4034
4386
  const controller = new CollectionController(collection);
4035
- app.get(path, accessGate(collection, "read"), (c) => controller.find(c));
4036
- app.post(path, accessGate(collection, "create"), (c) => controller.create(c));
4037
- app.post(`${path}/media`, accessGate(collection, "create"), (c) => controller.create(c));
4038
- app.delete(`${path}/delete-many`, accessGate(collection, "delete"), (c) => controller.deleteMany(c));
4039
- app.get(`${path}/:id`, accessGate(collection, "read"), (c) => controller.findOne(c));
4040
- app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
4041
- app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
4387
+ app.get(path, (c) => controller.find(c));
4388
+ app.post(path, (c) => controller.create(c));
4389
+ app.post(`${path}/media`, (c) => controller.create(c));
4390
+ app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
4391
+ app.get(`${path}/:id`, (c) => controller.findOne(c));
4392
+ app.patch(`${path}/:id`, (c) => controller.update(c));
4393
+ app.delete(`${path}/:id`, (c) => controller.delete(c));
4042
4394
  app.post(`${path}/seed`, (c) => controller.seed(c));
4043
4395
  if (collection.auth) {
4044
- app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
4396
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
4045
4397
  }
4046
4398
  if (collection.workflow) {
4047
- app.post(`${path}/:id/transitions/:transition`, requireAuth(), (c) => controller.transition(c));
4048
- app.get(`${path}/:id/workflow-history`, requireAuth(), (c) => controller.workflowHistory(c));
4399
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
4400
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
4049
4401
  }
4050
4402
  }
4051
4403
  for (const global of config.globals) {
4052
4404
  const path = `/api/globals/${global.slug}`;
4053
4405
  const controller = new GlobalController(global);
4054
- app.get(path, accessGate(global, "read"), (c) => controller.get(c));
4055
- app.patch(path, accessGate(global, "update"), (c) => controller.update(c));
4406
+ app.get(path, (c) => controller.get(c));
4407
+ app.patch(path, (c) => controller.update(c));
4056
4408
  app.post(`${path}/seed`, (c) => controller.seed(c));
4057
4409
  }
4058
4410
  const previewController = new PreviewController();
4059
- app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
4411
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
4060
4412
  app.get("/api/preview-data", (c) => previewController.getData(c));
4061
- app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(), async (c) => {
4413
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
4062
4414
  const slug = c.req.param("slug");
4063
4415
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4064
4416
  const config2 = c.get("config");
@@ -4076,7 +4428,7 @@ function registerRoutes(app, config) {
4076
4428
  const controller = new CollectionController(collection);
4077
4429
  return controller.transition(c);
4078
4430
  });
4079
- app.get("/api/collections/:slug/:id/workflow-history", requireAuth(), async (c) => {
4431
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
4080
4432
  const slug = c.req.param("slug");
4081
4433
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4082
4434
  const config2 = c.get("config");
@@ -4403,7 +4755,7 @@ async function createDyrectedApp(rawConfig) {
4403
4755
  await config.db.sync(config.collections, config.globals);
4404
4756
  }
4405
4757
  app.use("*", (0, import_request_id.requestId)());
4406
- app.use("*", optionalAuth());
4758
+ app.use("*", optionalAuth(config));
4407
4759
  app.use("*", async (c, next) => {
4408
4760
  const start = Date.now();
4409
4761
  await next();