@dyrected/core 2.5.56 → 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.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,140 @@ 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
+ async function applyFieldReadAccess(context, doc) {
717
+ if (!doc || typeof doc !== "object") return doc;
718
+ const result = { ...doc };
719
+ for (const field of context.fields) {
720
+ if (field.type === "row" && field.fields) {
721
+ Object.assign(result, await applyFieldReadAccess({ ...context, fields: field.fields }, result));
722
+ continue;
723
+ }
724
+ if (!field.name) continue;
725
+ const value = result[field.name];
726
+ const canRead = await resolveBooleanAccess(context.config, field.access?.read, {
727
+ user: context.user,
728
+ req: context.req,
729
+ doc: context.doc,
730
+ data: context.data
731
+ });
732
+ if (!canRead) {
733
+ delete result[field.name];
734
+ continue;
735
+ }
736
+ if (value === void 0 || value === null) continue;
737
+ if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
738
+ result[field.name] = await applyFieldReadAccess({ ...context, fields: field.fields }, value);
739
+ continue;
740
+ }
741
+ if (field.type === "array" && field.fields && Array.isArray(value)) {
742
+ result[field.name] = await Promise.all(
743
+ value.map(
744
+ (item) => typeof item === "object" && item !== null ? applyFieldReadAccess({ ...context, fields: field.fields }, item) : item
745
+ )
746
+ );
747
+ continue;
748
+ }
749
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
750
+ result[field.name] = await Promise.all(
751
+ value.map(async (item) => {
752
+ if (typeof item !== "object" || item === null) return item;
753
+ const typedItem = item;
754
+ const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
755
+ if (!block) return item;
756
+ return applyFieldReadAccess({ ...context, fields: block.fields }, typedItem);
757
+ })
758
+ );
759
+ }
760
+ }
761
+ return result;
762
+ }
763
+ async function applyFieldWriteAccess(context, data) {
764
+ if (!data || typeof data !== "object") return data;
765
+ const result = { ...data };
766
+ for (const field of context.fields) {
767
+ if (field.type === "row" && field.fields) {
768
+ Object.assign(result, await applyFieldWriteAccess({ ...context, fields: field.fields }, result));
769
+ continue;
770
+ }
771
+ if (!field.name || !(field.name in result)) continue;
772
+ const canUpdate = await resolveBooleanAccess(context.config, field.access?.update, {
773
+ user: context.user,
774
+ req: context.req,
775
+ doc: context.doc,
776
+ data: context.data
777
+ });
778
+ if (!canUpdate) {
779
+ delete result[field.name];
780
+ continue;
781
+ }
782
+ const value = result[field.name];
783
+ if (value === void 0 || value === null) continue;
784
+ if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) {
785
+ result[field.name] = await applyFieldWriteAccess({ ...context, fields: field.fields }, value);
786
+ continue;
787
+ }
788
+ if (field.type === "array" && field.fields && Array.isArray(value)) {
789
+ result[field.name] = await Promise.all(
790
+ value.map(
791
+ (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess({ ...context, fields: field.fields }, item) : item
792
+ )
793
+ );
794
+ continue;
795
+ }
796
+ if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
797
+ result[field.name] = await Promise.all(
798
+ value.map(async (item) => {
799
+ if (typeof item !== "object" || item === null) return item;
800
+ const typedItem = item;
801
+ const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
802
+ if (!block) return item;
803
+ return applyFieldWriteAccess({ ...context, fields: block.fields }, typedItem);
804
+ })
805
+ );
806
+ }
807
+ }
808
+ return result;
809
+ }
810
+ function mergeWhereConstraint(where, constraint) {
811
+ return where ? { AND: [where, constraint] } : constraint;
812
+ }
813
+
644
814
  // src/workflows.ts
645
815
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
646
816
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
@@ -879,11 +1049,23 @@ var CollectionController = class {
879
1049
  return config.adminAuth?.providers?.find((p) => p.members) || null;
880
1050
  }
881
1051
  toHookRequestContext(c) {
882
- return {
883
- query: c.req.query(),
884
- headers: c.req.header(),
885
- raw: c.req.raw
886
- };
1052
+ return toHookRequestContext(c.req);
1053
+ }
1054
+ async evaluateAccess(c, action, options = {}) {
1055
+ const config = c.get("config");
1056
+ return resolveCollectionAccess(
1057
+ config,
1058
+ this.collection.slug,
1059
+ action,
1060
+ this.collection.access?.[action],
1061
+ {
1062
+ id: options.id,
1063
+ user: c.get("user"),
1064
+ req: this.toHookRequestContext(c),
1065
+ doc: options.doc ?? void 0,
1066
+ data: options.data
1067
+ }
1068
+ );
887
1069
  }
888
1070
  async find(c) {
889
1071
  const config = c.get("config");
@@ -963,6 +1145,13 @@ var CollectionController = class {
963
1145
  if (this.collection.workflow && !canViewWorkflowDraft(this.collection.workflow, user)) {
964
1146
  where = where ? { AND: [where, { __published: { exists: true } }] } : { __published: { exists: true } };
965
1147
  }
1148
+ const access = await this.evaluateAccess(c, "read");
1149
+ if (!access.allowed) {
1150
+ return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
1151
+ }
1152
+ if (access.constraint) {
1153
+ where = mergeWhereConstraint(where, access.constraint);
1154
+ }
966
1155
  let result = await db.find({
967
1156
  collection: this.collection.slug,
968
1157
  limit,
@@ -995,7 +1184,14 @@ var CollectionController = class {
995
1184
  db: readonlyDb
996
1185
  });
997
1186
  const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
998
- processedDocs.push(docWithFieldHooks);
1187
+ const docWithFieldAccess = await applyFieldReadAccess({
1188
+ config,
1189
+ fields: this.collection.fields,
1190
+ user,
1191
+ req: this.toHookRequestContext(c),
1192
+ doc: docWithFieldHooks
1193
+ }, docWithFieldHooks);
1194
+ processedDocs.push(docWithFieldAccess);
999
1195
  }
1000
1196
  result.docs = processedDocs;
1001
1197
  if (depth > 0) {
@@ -1040,6 +1236,10 @@ var CollectionController = class {
1040
1236
  doc = materializeWorkflowDocument(doc, this.collection.workflow, user);
1041
1237
  if (!doc) return c.json({ message: "Not Found" }, 404);
1042
1238
  }
1239
+ const access = await this.evaluateAccess(c, "read", { id, doc });
1240
+ if (!access.allowed) {
1241
+ return c.json({ error: true, message: `Access denied: read on ${this.collection.slug}` }, 403);
1242
+ }
1043
1243
  const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
1044
1244
  const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
1045
1245
  doc: docWithDefaults,
@@ -1048,17 +1248,24 @@ var CollectionController = class {
1048
1248
  db: readonlyDb
1049
1249
  });
1050
1250
  const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
1051
- if (depth > 0 && docWithFieldHooks) {
1251
+ const docWithFieldAccess = await applyFieldReadAccess({
1252
+ config,
1253
+ fields: this.collection.fields,
1254
+ user,
1255
+ req: this.toHookRequestContext(c),
1256
+ doc: docWithFieldHooks
1257
+ }, docWithFieldHooks);
1258
+ if (depth > 0 && docWithFieldAccess) {
1052
1259
  const populationService = new PopulationService(db, config.collections);
1053
1260
  const populatedDoc = await populationService.populate({
1054
- data: docWithFieldHooks,
1261
+ data: docWithFieldAccess,
1055
1262
  fields: this.collection.fields,
1056
1263
  currentDepth: 0,
1057
1264
  maxDepth: depth
1058
1265
  });
1059
1266
  return c.json(populatedDoc);
1060
1267
  }
1061
- return c.json(docWithFieldHooks);
1268
+ return c.json(docWithFieldAccess);
1062
1269
  }
1063
1270
  async create(c) {
1064
1271
  const config = c.get("config");
@@ -1097,9 +1304,20 @@ var CollectionController = class {
1097
1304
  if (this.collection.workflow) {
1098
1305
  data = initializeWorkflowDocument(data, this.collection.workflow);
1099
1306
  }
1307
+ const createAccess = await this.evaluateAccess(c, "create", { data });
1308
+ if (!createAccess.allowed) {
1309
+ return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
1310
+ }
1100
1311
  if (this.collection.auth && data.password) {
1101
1312
  data.password = await hashPassword(data.password);
1102
1313
  }
1314
+ data = await applyFieldWriteAccess({
1315
+ config,
1316
+ fields: this.collection.fields,
1317
+ user,
1318
+ req: this.toHookRequestContext(c),
1319
+ data
1320
+ }, data);
1103
1321
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
1104
1322
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1105
1323
  data,
@@ -1134,7 +1352,14 @@ var CollectionController = class {
1134
1352
  db: readonlyDb
1135
1353
  });
1136
1354
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1137
- return c.json(finalDoc, 201);
1355
+ const accessibleDoc = await applyFieldReadAccess({
1356
+ config,
1357
+ fields: this.collection.fields,
1358
+ user,
1359
+ req: this.toHookRequestContext(c),
1360
+ doc: finalDoc
1361
+ }, finalDoc);
1362
+ return c.json(accessibleDoc, 201);
1138
1363
  }
1139
1364
  async upload(c) {
1140
1365
  const config = c.get("config");
@@ -1177,6 +1402,17 @@ var CollectionController = class {
1177
1402
  createdBy: user?.sub ?? null,
1178
1403
  updatedBy: user?.sub ?? null
1179
1404
  };
1405
+ const createAccess = await this.evaluateAccess(c, "create", { data });
1406
+ if (!createAccess.allowed) {
1407
+ return c.json({ error: true, message: `Access denied: create on ${this.collection.slug}` }, 403);
1408
+ }
1409
+ data = await applyFieldWriteAccess({
1410
+ config,
1411
+ fields: this.collection.fields,
1412
+ user,
1413
+ req: this.toHookRequestContext(c),
1414
+ data
1415
+ }, data);
1180
1416
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
1181
1417
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1182
1418
  data,
@@ -1204,7 +1440,14 @@ var CollectionController = class {
1204
1440
  db: readonlyDb
1205
1441
  });
1206
1442
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1207
- return c.json(finalDoc, 201);
1443
+ const accessibleDoc = await applyFieldReadAccess({
1444
+ config,
1445
+ fields: this.collection.fields,
1446
+ user,
1447
+ req: this.toHookRequestContext(c),
1448
+ doc: finalDoc
1449
+ }, finalDoc);
1450
+ return c.json(accessibleDoc, 201);
1208
1451
  }
1209
1452
  async update(c) {
1210
1453
  const config = c.get("config");
@@ -1242,10 +1485,22 @@ var CollectionController = class {
1242
1485
  });
1243
1486
  const originalDoc = await db.findOne({ collection: this.collection.slug, id });
1244
1487
  if (!originalDoc) return c.json({ message: "Not Found" }, 404);
1488
+ const updateAccess = await this.evaluateAccess(c, "update", { id, doc: originalDoc, data });
1489
+ if (!updateAccess.allowed) {
1490
+ return c.json({ error: true, message: `Access denied: update on ${this.collection.slug}` }, 403);
1491
+ }
1245
1492
  let before = null;
1246
1493
  if (this.collection.audit) {
1247
1494
  before = originalDoc;
1248
1495
  }
1496
+ data = await applyFieldWriteAccess({
1497
+ config,
1498
+ fields: this.collection.fields,
1499
+ user,
1500
+ req: this.toHookRequestContext(c),
1501
+ doc: originalDoc,
1502
+ data
1503
+ }, data);
1249
1504
  data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user, readonlyDb);
1250
1505
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
1251
1506
  data,
@@ -1281,7 +1536,14 @@ var CollectionController = class {
1281
1536
  db: readonlyDb
1282
1537
  });
1283
1538
  const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
1284
- return c.json(finalDoc);
1539
+ const accessibleDoc = await applyFieldReadAccess({
1540
+ config,
1541
+ fields: this.collection.fields,
1542
+ user,
1543
+ req: this.toHookRequestContext(c),
1544
+ doc: finalDoc
1545
+ }, finalDoc);
1546
+ return c.json(accessibleDoc);
1285
1547
  }
1286
1548
  async transition(c) {
1287
1549
  const config = c.get("config");
@@ -1318,7 +1580,7 @@ var CollectionController = class {
1318
1580
  const readAccess = this.collection.access?.read;
1319
1581
  if (readAccess !== void 0 && readAccess !== null) {
1320
1582
  const args = { user: c.get("user"), req: c.req, doc: document };
1321
- const result2 = typeof readAccess === "function" ? await readAccess(args) : await evaluateAccess(readAccess, args);
1583
+ const result2 = await resolveAccess(config, readAccess, args);
1322
1584
  let allowed = result2 === true;
1323
1585
  if (result2 && typeof result2 === "object") {
1324
1586
  const match = await config.db.find({
@@ -1429,6 +1691,10 @@ var CollectionController = class {
1429
1691
  const user = c.get("user");
1430
1692
  const doc = await db.findOne({ collection: this.collection.slug, id });
1431
1693
  if (!doc) return c.json({ message: "Not Found" }, 404);
1694
+ const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1695
+ if (!deleteAccess.allowed) {
1696
+ return c.json({ error: true, message: `Access denied: delete on ${this.collection.slug}` }, 403);
1697
+ }
1432
1698
  let before = null;
1433
1699
  if (this.collection.audit) {
1434
1700
  before = doc;
@@ -1488,6 +1754,11 @@ var CollectionController = class {
1488
1754
  failed.push({ id, error: "Not Found" });
1489
1755
  continue;
1490
1756
  }
1757
+ const deleteAccess = await this.evaluateAccess(c, "delete", { id, doc });
1758
+ if (!deleteAccess.allowed) {
1759
+ failed.push({ id, error: "Access denied" });
1760
+ continue;
1761
+ }
1491
1762
  let before = null;
1492
1763
  if (this.collection.audit) {
1493
1764
  before = doc;
@@ -1581,6 +1852,14 @@ var GlobalController = class {
1581
1852
  await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
1582
1853
  data = this.global.initialData;
1583
1854
  }
1855
+ const canRead = await resolveBooleanAccess(config, this.global.access?.read, {
1856
+ user,
1857
+ req: toHookRequestContext(c.req),
1858
+ doc: data
1859
+ });
1860
+ if (!canRead) {
1861
+ return c.json({ error: true, message: `Access denied: read on ${this.global.slug}` }, 403);
1862
+ }
1584
1863
  const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1585
1864
  const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
1586
1865
  doc: dataWithDefaults,
@@ -1589,17 +1868,24 @@ var GlobalController = class {
1589
1868
  db: readonlyDb
1590
1869
  });
1591
1870
  const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user, readonlyDb);
1592
- if (depth > 0 && docWithFieldHooks) {
1871
+ const docWithFieldAccess = await applyFieldReadAccess({
1872
+ config,
1873
+ fields: this.global.fields,
1874
+ user,
1875
+ req: toHookRequestContext(c.req),
1876
+ doc: docWithFieldHooks
1877
+ }, docWithFieldHooks);
1878
+ if (depth > 0 && docWithFieldAccess) {
1593
1879
  const populationService = new PopulationService(db, config.collections);
1594
1880
  const populatedData = await populationService.populate({
1595
- data: docWithFieldHooks,
1881
+ data: docWithFieldAccess,
1596
1882
  fields: this.global.fields,
1597
1883
  currentDepth: 0,
1598
1884
  maxDepth: depth
1599
1885
  });
1600
1886
  return c.json(populatedData);
1601
1887
  }
1602
- return c.json(docWithFieldHooks);
1888
+ return c.json(docWithFieldAccess);
1603
1889
  }
1604
1890
  async update(c) {
1605
1891
  const config = c.get("config");
@@ -1609,7 +1895,24 @@ var GlobalController = class {
1609
1895
  const body = await c.req.json();
1610
1896
  const user = c.get("user");
1611
1897
  const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
1612
- let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user, readonlyDb);
1898
+ const canUpdate = await resolveBooleanAccess(config, this.global.access?.update, {
1899
+ user,
1900
+ req: toHookRequestContext(c.req),
1901
+ doc: originalDoc,
1902
+ data: body
1903
+ });
1904
+ if (!canUpdate) {
1905
+ return c.json({ error: true, message: `Access denied: update on ${this.global.slug}` }, 403);
1906
+ }
1907
+ let sanitizedBody = await applyFieldWriteAccess({
1908
+ config,
1909
+ fields: this.global.fields,
1910
+ user,
1911
+ req: toHookRequestContext(c.req),
1912
+ doc: originalDoc,
1913
+ data: body
1914
+ }, body);
1915
+ let data = await executeFieldBeforeChange(this.global.fields, sanitizedBody, originalDoc, user, readonlyDb);
1613
1916
  data = await runCollectionHooks(this.global.hooks?.beforeChange, {
1614
1917
  data,
1615
1918
  doc: originalDoc,
@@ -1634,7 +1937,14 @@ var GlobalController = class {
1634
1937
  db: readonlyDb
1635
1938
  });
1636
1939
  const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user, readonlyDb);
1637
- return c.json(finalDoc);
1940
+ const accessibleDoc = await applyFieldReadAccess({
1941
+ config,
1942
+ fields: this.global.fields,
1943
+ user,
1944
+ req: toHookRequestContext(c.req),
1945
+ doc: finalDoc
1946
+ }, finalDoc);
1947
+ return c.json(accessibleDoc);
1638
1948
  }
1639
1949
  async seed(c) {
1640
1950
  const config = c.get("config");
@@ -1816,7 +2126,7 @@ var MediaController = class {
1816
2126
  var import_jose = require("jose");
1817
2127
  var import_node_util2 = require("util");
1818
2128
  function getSecret() {
1819
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
2129
+ const secret = process.env.DYRECTED_JWT_SECRET;
1820
2130
  if (!secret) {
1821
2131
  throw new Error(
1822
2132
  "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
@@ -2686,7 +2996,7 @@ var AdminAuthController = class {
2686
2996
  return state;
2687
2997
  }
2688
2998
  getStateSecret() {
2689
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
2999
+ const secret = process.env.DYRECTED_JWT_SECRET;
2690
3000
  if (!secret) {
2691
3001
  throw new Error("[dyrected/core] DYRECTED_JWT_SECRET is not set.");
2692
3002
  }
@@ -2704,7 +3014,7 @@ var import_jose3 = require("jose");
2704
3014
  var import_node_util4 = require("util");
2705
3015
  var PreviewController = class {
2706
3016
  getSecret() {
2707
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
3017
+ const secret = process.env.DYRECTED_JWT_SECRET || "dyrected-preview-secret-change-me";
2708
3018
  return new import_node_util4.TextEncoder().encode(secret);
2709
3019
  }
2710
3020
  /**
@@ -2743,30 +3053,74 @@ var PreviewController = class {
2743
3053
  };
2744
3054
 
2745
3055
  // src/middleware/auth.ts
2746
- function requireAuth() {
3056
+ function getBearerToken(c) {
3057
+ const authHeader = c.req.header("Authorization");
3058
+ return authHeader?.replace(/^Bearer\s+/i, "") || void 0;
3059
+ }
3060
+ async function resolveUser(token, config) {
3061
+ const payload = await verifyCollectionToken(token);
3062
+ if (payload.purpose) {
3063
+ return payload;
3064
+ }
3065
+ const db = config?.db;
3066
+ if (!db) {
3067
+ return payload;
3068
+ }
3069
+ let doc;
3070
+ try {
3071
+ doc = await db.findOne({
3072
+ collection: payload.collection,
3073
+ id: payload.sub
3074
+ });
3075
+ } catch (err) {
3076
+ console.error("[dyrected/core] Failed to hydrate user from token:", err);
3077
+ return payload;
3078
+ }
3079
+ if (!doc) {
3080
+ return null;
3081
+ }
3082
+ const { password: _password, ...safe } = doc;
3083
+ return {
3084
+ ...safe,
3085
+ sub: payload.sub,
3086
+ email: payload.email ?? safe.email,
3087
+ collection: payload.collection
3088
+ };
3089
+ }
3090
+ function requireAuth(config) {
2747
3091
  return async (c, next) => {
2748
- const authHeader = c.req.header("Authorization");
2749
- const token = authHeader?.replace(/^Bearer\s+/i, "");
3092
+ if (c.get("user")) {
3093
+ return next();
3094
+ }
3095
+ const token = getBearerToken(c);
2750
3096
  if (!token) {
2751
3097
  return c.json({ error: true, message: "Authentication required." }, 401);
2752
3098
  }
3099
+ let user;
2753
3100
  try {
2754
- const user = await verifyCollectionToken(token);
2755
- c.set("user", user);
2756
- await next();
3101
+ user = await resolveUser(token, config ?? c.get("config"));
2757
3102
  } catch {
2758
3103
  return c.json({ error: true, message: "Invalid or expired token." }, 401);
2759
3104
  }
3105
+ if (!user) {
3106
+ return c.json({ error: true, message: "Invalid or expired token." }, 401);
3107
+ }
3108
+ c.set("user", user);
3109
+ await next();
2760
3110
  };
2761
3111
  }
2762
- function optionalAuth() {
3112
+ function optionalAuth(config) {
2763
3113
  return async (c, next) => {
2764
- const authHeader = c.req.header("Authorization");
2765
- const token = authHeader?.replace(/^Bearer\s+/i, "");
3114
+ if (c.get("user")) {
3115
+ return next();
3116
+ }
3117
+ const token = getBearerToken(c);
2766
3118
  if (token) {
2767
3119
  try {
2768
- const user = await verifyCollectionToken(token);
2769
- c.set("user", user);
3120
+ const user = await resolveUser(token, config ?? c.get("config"));
3121
+ if (user) {
3122
+ c.set("user", user);
3123
+ }
2770
3124
  } catch {
2771
3125
  }
2772
3126
  }
@@ -3654,37 +4008,23 @@ function getSwaggerHtml(specUrl) {
3654
4008
  }
3655
4009
 
3656
4010
  // src/router.ts
3657
- function accessGate(target, action) {
4011
+ function accessGate(config, target, action) {
3658
4012
  return async (c, next) => {
3659
4013
  const user = c.get("user");
3660
4014
  const accessExpr = target.access?.[action];
3661
4015
  if (accessExpr === void 0 || accessExpr === null) {
3662
4016
  return await next();
3663
4017
  }
3664
- const accessArgs = { user, req: c.req, doc: null };
3665
- const allowed = await evaluateAccess(accessExpr, accessArgs);
4018
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4019
+ user,
4020
+ req: toHookRequestContext(c.req)
4021
+ });
3666
4022
  if (!allowed) {
3667
4023
  return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
3668
4024
  }
3669
4025
  await next();
3670
4026
  };
3671
4027
  }
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
4028
  function serializeFieldForApi(f) {
3689
4029
  if (!f) return f;
3690
4030
  const serialized = { ...f };
@@ -3713,7 +4053,7 @@ function serializeFieldForApi(f) {
3713
4053
  return serialized;
3714
4054
  }
3715
4055
  function registerRoutes(app, config) {
3716
- app.get("/api/schemas", optionalAuth(), async (c) => {
4056
+ app.get("/api/schemas", optionalAuth(config), async (c) => {
3717
4057
  const siteId = c.req.header("X-Site-Id");
3718
4058
  let collections = [...config.collections];
3719
4059
  let globals = [...config.globals];
@@ -3729,11 +4069,11 @@ function registerRoutes(app, config) {
3729
4069
  }
3730
4070
  }
3731
4071
  const user = c.get("user");
3732
- const accessArgs = { user, req: c.req, doc: null };
4072
+ const accessArgs = { user, req: toHookRequestContext(c.req) };
3733
4073
  const serializeAccess = async (access) => {
3734
4074
  if (typeof access === "string") return access;
3735
4075
  if (typeof access === "boolean") return access;
3736
- return checkAccess(access, accessArgs);
4076
+ return resolveBooleanAccess(config, access, accessArgs);
3737
4077
  };
3738
4078
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
3739
4079
  slug: col.slug,
@@ -3819,7 +4159,7 @@ function registerRoutes(app, config) {
3819
4159
  adminAuth: getPublicAdminAuthConfig(config.adminAuth, collections)
3820
4160
  });
3821
4161
  });
3822
- app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
4162
+ app.get("/api/dyrected/options/:collection/:field", optionalAuth(config), async (c) => {
3823
4163
  const { collection: colSlug, field: fieldName } = c.req.param();
3824
4164
  const siteId = c.req.header("X-Site-Id");
3825
4165
  let collections = [...config.collections];
@@ -3833,8 +4173,10 @@ function registerRoutes(app, config) {
3833
4173
  if (collection) {
3834
4174
  const accessExpr = collection.access?.read;
3835
4175
  if (accessExpr !== void 0 && accessExpr !== null) {
3836
- const accessArgs = { user, req: c.req, doc: null };
3837
- const allowed = await checkAccess(accessExpr, accessArgs);
4176
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4177
+ user,
4178
+ req: toHookRequestContext(c.req)
4179
+ });
3838
4180
  if (!allowed) {
3839
4181
  return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
3840
4182
  }
@@ -3852,8 +4194,10 @@ function registerRoutes(app, config) {
3852
4194
  }
3853
4195
  const accessExpr = glb.access?.read;
3854
4196
  if (accessExpr !== void 0 && accessExpr !== null) {
3855
- const accessArgs = { user, req: c.req, doc: null };
3856
- const allowed = await checkAccess(accessExpr, accessArgs);
4197
+ const allowed = await resolveBooleanAccess(config, accessExpr, {
4198
+ user,
4199
+ req: toHookRequestContext(c.req)
4200
+ });
3857
4201
  if (!allowed) {
3858
4202
  return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
3859
4203
  }
@@ -3897,7 +4241,7 @@ function registerRoutes(app, config) {
3897
4241
  app.get("/api/docs", (c) => {
3898
4242
  return c.html(getSwaggerHtml());
3899
4243
  });
3900
- app.get("/api/preferences/:key", requireAuth(), async (c) => {
4244
+ app.get("/api/preferences/:key", requireAuth(config), async (c) => {
3901
4245
  const db = config.db;
3902
4246
  const user = c.get("user");
3903
4247
  const key = c.req.param("key");
@@ -3922,7 +4266,7 @@ function registerRoutes(app, config) {
3922
4266
  const globalValue = await getGlobalPreference();
3923
4267
  return c.json({ key, value: globalValue });
3924
4268
  });
3925
- app.put("/api/preferences/:key", requireAuth(), async (c) => {
4269
+ app.put("/api/preferences/:key", requireAuth(config), async (c) => {
3926
4270
  const db = config.db;
3927
4271
  const user = c.get("user");
3928
4272
  const key = c.req.param("key");
@@ -3962,7 +4306,7 @@ function registerRoutes(app, config) {
3962
4306
  });
3963
4307
  return c.json({ key, value: body.value });
3964
4308
  });
3965
- app.delete("/api/preferences/:key", requireAuth(), async (c) => {
4309
+ app.delete("/api/preferences/:key", requireAuth(config), async (c) => {
3966
4310
  const db = config.db;
3967
4311
  const user = c.get("user");
3968
4312
  const key = c.req.param("key");
@@ -4002,10 +4346,10 @@ function registerRoutes(app, config) {
4002
4346
  for (const col of uploadCollections) {
4003
4347
  const mediaController = new MediaController(col.slug);
4004
4348
  const prefix = `/api/collections/${col.slug}`;
4005
- app.get(`${prefix}/media`, accessGate(col, "read"), (c) => mediaController.find(c));
4349
+ app.get(`${prefix}/media`, accessGate(config, col, "read"), (c) => mediaController.find(c));
4006
4350
  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));
4351
+ app.post(`${prefix}/media`, accessGate(config, col, "create"), (c) => mediaController.upload(c));
4352
+ app.delete(`${prefix}/media/:id`, accessGate(config, col, "delete"), (c) => mediaController.delete(c));
4009
4353
  }
4010
4354
  }
4011
4355
  const adminAuthController = new AdminAuthController(config);
@@ -4022,43 +4366,43 @@ function registerRoutes(app, config) {
4022
4366
  app.post(`${path}/logout`, (c) => authController.logout(c));
4023
4367
  app.get(`${path}/init`, (c) => authController.init(c));
4024
4368
  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));
4369
+ app.get(`${path}/me`, requireAuth(config), (c) => authController.me(c));
4370
+ app.post(`${path}/refresh-token`, requireAuth(config), (c) => authController.refreshToken(c));
4027
4371
  app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
4028
4372
  app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
4029
- app.post(`${path}/invite`, requireAuth(), (c) => authController.invite(c));
4373
+ app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
4030
4374
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
4031
4375
  }
4032
4376
  for (const collection of config.collections) {
4033
4377
  const path = `/api/collections/${collection.slug}`;
4034
4378
  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));
4379
+ app.get(path, (c) => controller.find(c));
4380
+ app.post(path, (c) => controller.create(c));
4381
+ app.post(`${path}/media`, (c) => controller.create(c));
4382
+ app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
4383
+ app.get(`${path}/:id`, (c) => controller.findOne(c));
4384
+ app.patch(`${path}/:id`, (c) => controller.update(c));
4385
+ app.delete(`${path}/:id`, (c) => controller.delete(c));
4042
4386
  app.post(`${path}/seed`, (c) => controller.seed(c));
4043
4387
  if (collection.auth) {
4044
- app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
4388
+ app.post(`${path}/:id/change-password`, requireAuth(config), (c) => controller.changePassword(c));
4045
4389
  }
4046
4390
  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));
4391
+ app.post(`${path}/:id/transitions/:transition`, requireAuth(config), (c) => controller.transition(c));
4392
+ app.get(`${path}/:id/workflow-history`, requireAuth(config), (c) => controller.workflowHistory(c));
4049
4393
  }
4050
4394
  }
4051
4395
  for (const global of config.globals) {
4052
4396
  const path = `/api/globals/${global.slug}`;
4053
4397
  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));
4398
+ app.get(path, (c) => controller.get(c));
4399
+ app.patch(path, (c) => controller.update(c));
4056
4400
  app.post(`${path}/seed`, (c) => controller.seed(c));
4057
4401
  }
4058
4402
  const previewController = new PreviewController();
4059
- app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
4403
+ app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
4060
4404
  app.get("/api/preview-data", (c) => previewController.getData(c));
4061
- app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(), async (c) => {
4405
+ app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
4062
4406
  const slug = c.req.param("slug");
4063
4407
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4064
4408
  const config2 = c.get("config");
@@ -4076,7 +4420,7 @@ function registerRoutes(app, config) {
4076
4420
  const controller = new CollectionController(collection);
4077
4421
  return controller.transition(c);
4078
4422
  });
4079
- app.get("/api/collections/:slug/:id/workflow-history", requireAuth(), async (c) => {
4423
+ app.get("/api/collections/:slug/:id/workflow-history", requireAuth(config), async (c) => {
4080
4424
  const slug = c.req.param("slug");
4081
4425
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
4082
4426
  const config2 = c.get("config");
@@ -4403,7 +4747,7 @@ async function createDyrectedApp(rawConfig) {
4403
4747
  await config.db.sync(config.collections, config.globals);
4404
4748
  }
4405
4749
  app.use("*", (0, import_request_id.requestId)());
4406
- app.use("*", optionalAuth());
4750
+ app.use("*", optionalAuth(config));
4407
4751
  app.use("*", async (c, next) => {
4408
4752
  const start = Date.now();
4409
4753
  await next();