@dyrected/core 2.5.59 → 2.5.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/app-config-9hs8DRED.d.cts +1718 -0
  2. package/dist/app-config-9hs8DRED.d.ts +1718 -0
  3. package/dist/app-config-C0twGIaF.d.cts +1597 -0
  4. package/dist/app-config-C0twGIaF.d.ts +1597 -0
  5. package/dist/app-config-C0uxEU7Q.d.cts +1874 -0
  6. package/dist/app-config-C0uxEU7Q.d.ts +1874 -0
  7. package/dist/app-config-CBOn8IyZ.d.cts +1838 -0
  8. package/dist/app-config-CBOn8IyZ.d.ts +1838 -0
  9. package/dist/app-config-CstocO9M.d.cts +1580 -0
  10. package/dist/app-config-CstocO9M.d.ts +1580 -0
  11. package/dist/app-config-CwaU1de2.d.cts +1868 -0
  12. package/dist/app-config-CwaU1de2.d.ts +1868 -0
  13. package/dist/app-config-DIwvzbpe.d.cts +1555 -0
  14. package/dist/app-config-DIwvzbpe.d.ts +1555 -0
  15. package/dist/app-config-DVdSospO.d.cts +1842 -0
  16. package/dist/app-config-DVdSospO.d.ts +1842 -0
  17. package/dist/app-config-Dkndq5ni.d.cts +1580 -0
  18. package/dist/app-config-Dkndq5ni.d.ts +1580 -0
  19. package/dist/app-config-P-SF5nnR.d.cts +1568 -0
  20. package/dist/app-config-P-SF5nnR.d.ts +1568 -0
  21. package/dist/app-config-ouBRb6Bf.d.cts +1716 -0
  22. package/dist/app-config-ouBRb6Bf.d.ts +1716 -0
  23. package/dist/chunk-2WODKOUB.js +1627 -0
  24. package/dist/chunk-35NM2WPO.js +1658 -0
  25. package/dist/chunk-3CMG2BH2.js +1618 -0
  26. package/dist/chunk-CUOPCOU2.js +1644 -0
  27. package/dist/chunk-DCY7QHRB.js +1637 -0
  28. package/dist/chunk-EOW4YMAV.js +1637 -0
  29. package/dist/chunk-JFA22YFB.js +1625 -0
  30. package/dist/chunk-WF5A6PDJ.js +1618 -0
  31. package/dist/index.cjs +209 -47
  32. package/dist/index.d.cts +79 -4
  33. package/dist/index.d.ts +79 -4
  34. package/dist/index.js +42 -4
  35. package/dist/server.cjs +388 -56
  36. package/dist/server.d.cts +1 -1
  37. package/dist/server.d.ts +1 -1
  38. package/dist/server.js +224 -13
  39. package/package.json +1 -1
package/dist/server.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import * as hono from 'hono';
3
3
  import { Hono, Context } from 'hono';
4
- import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-C3X5BAyK.cjs';
4
+ import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-C0uxEU7Q.cjs';
5
5
  import * as hono_utils_http_status from 'hono/utils/http-status';
6
6
  import * as hono_utils_types from 'hono/utils/types';
7
7
  import 'lucide-react';
package/dist/server.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import * as hono from 'hono';
3
3
  import { Hono, Context } from 'hono';
4
- import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-C3X5BAyK.js';
4
+ import { D as DyrectedConfig, C as CollectionConfig, G as GlobalConfig } from './app-config-C0uxEU7Q.js';
5
5
  import * as hono_utils_http_status from 'hono/utils/http-status';
6
6
  import * as hono_utils_types from 'hono/utils/types';
7
7
  import 'lucide-react';
package/dist/server.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  runCollectionHooks,
14
14
  saveWorkflowDraft,
15
15
  transitionWorkflow
16
- } from "./chunk-L65PQYDP.js";
16
+ } from "./chunk-35NM2WPO.js";
17
17
 
18
18
  // src/app.ts
19
19
  import { Hono } from "hono";
@@ -374,12 +374,16 @@ async function resolveAccess(config, access, args) {
374
374
  }
375
375
  }
376
376
  if (isNamedAccessPolicy(access)) {
377
- const resolver = config.accessPolicies?.[access.policy];
378
- if (!resolver) {
377
+ const policy = config.accessPolicies?.[access.policy];
378
+ if (policy === void 0) {
379
379
  console.error(`[dyrected/core] Unknown access policy "${access.policy}".`);
380
380
  return false;
381
381
  }
382
+ if (typeof policy === "string" || typeof policy === "boolean") {
383
+ return evaluateAccess(policy, args);
384
+ }
382
385
  try {
386
+ const resolver = policy;
383
387
  return await resolver({ ...args, params: access.params });
384
388
  } catch (err) {
385
389
  console.error(`[dyrected/core] Access policy "${access.policy}" failed:`, err);
@@ -487,14 +491,15 @@ async function applyFieldWriteAccess(context, data) {
487
491
  continue;
488
492
  }
489
493
  if (!field.name || !(field.name in result)) continue;
490
- const canUpdate = await resolveBooleanAccess(context.config, field.access?.update, {
494
+ const writeRule = context.operation === "create" ? field.access?.create ?? field.access?.update : field.access?.update;
495
+ const canWrite = await resolveBooleanAccess(context.config, writeRule, {
491
496
  user: context.user,
492
497
  req: context.req,
493
498
  doc: context.doc,
494
499
  data: context.data,
495
500
  id: resolveFieldAccessId(context)
496
501
  });
497
- if (!canUpdate) {
502
+ if (!canWrite) {
498
503
  delete result[field.name];
499
504
  continue;
500
505
  }
@@ -606,9 +611,9 @@ var CollectionController = class {
606
611
  });
607
612
  }
608
613
  const readonlyDb = createReadonlyDb(db);
609
- const limit = Number(c.req.query("limit")) || 10;
614
+ const limit = Math.min(Number(c.req.query("limit")) || 10, 100);
610
615
  const page = Number(c.req.query("page")) || 1;
611
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 2;
616
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
612
617
  const sort = c.req.query("sort") || void 0;
613
618
  const user = c.get("user");
614
619
  let where = void 0;
@@ -654,7 +659,8 @@ var CollectionController = class {
654
659
  limit,
655
660
  page,
656
661
  sort,
657
- where
662
+ where,
663
+ fields: this.collection.fields
658
664
  });
659
665
  if (result.total === 0 && this.collection.initialData && !where && page === 1) {
660
666
  console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
@@ -666,7 +672,8 @@ var CollectionController = class {
666
672
  limit,
667
673
  page,
668
674
  sort,
669
- where
675
+ where,
676
+ fields: this.collection.fields
670
677
  });
671
678
  }
672
679
  result.docs = result.docs.map((doc) => this.collection.workflow ? materializeWorkflowDocument(doc, this.collection.workflow, user) : doc).filter((doc) => doc !== null);
@@ -724,7 +731,7 @@ var CollectionController = class {
724
731
  }
725
732
  const readonlyDb = createReadonlyDb(db);
726
733
  const id = c.req.param("id");
727
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
734
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
728
735
  const user = c.get("user");
729
736
  if (!id) return c.json({ message: "Missing ID" }, 400);
730
737
  let doc = await db.findOne({ collection: this.collection.slug, id });
@@ -813,7 +820,8 @@ var CollectionController = class {
813
820
  fields: this.collection.fields,
814
821
  user,
815
822
  req: this.toHookRequestContext(c),
816
- data
823
+ data,
824
+ operation: "create"
817
825
  }, data);
818
826
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
819
827
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
@@ -908,7 +916,8 @@ var CollectionController = class {
908
916
  fields: this.collection.fields,
909
917
  user,
910
918
  req: this.toHookRequestContext(c),
911
- data
919
+ data,
920
+ operation: "create"
912
921
  }, data);
913
922
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
914
923
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
@@ -1330,7 +1339,7 @@ var GlobalController = class {
1330
1339
  const db = config.db;
1331
1340
  if (!db) return c.json({ message: "Database not configured" }, 500);
1332
1341
  const readonlyDb = createReadonlyDb(db);
1333
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
1342
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1334
1343
  const user = c.get("user");
1335
1344
  let query = void 0;
1336
1345
  const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
@@ -2554,6 +2563,150 @@ var PreviewController = class {
2554
2563
  }
2555
2564
  };
2556
2565
 
2566
+ // src/controllers/audit.controller.ts
2567
+ var AuditController = class {
2568
+ parseQuery(c) {
2569
+ const limit = Math.min(Number(c.req.query("limit")) || 50, 100);
2570
+ const page = Math.max(Number(c.req.query("page")) || 1, 1);
2571
+ const sort = c.req.query("sort") || "-timestamp";
2572
+ const whereRaw = c.req.query("where");
2573
+ if (!whereRaw) {
2574
+ return { limit, page, sort };
2575
+ }
2576
+ try {
2577
+ return {
2578
+ limit,
2579
+ page,
2580
+ sort,
2581
+ where: JSON.parse(decodeURIComponent(whereRaw))
2582
+ };
2583
+ } catch {
2584
+ return { limit, page, sort };
2585
+ }
2586
+ }
2587
+ emptyResult(limit, page) {
2588
+ return {
2589
+ docs: [],
2590
+ total: 0,
2591
+ limit,
2592
+ page,
2593
+ totalPages: 0,
2594
+ hasNextPage: false,
2595
+ hasPrevPage: page > 1
2596
+ };
2597
+ }
2598
+ async collectAccessibleDocumentIds(db, collection, constraint) {
2599
+ const ids = [];
2600
+ const limit = 100;
2601
+ let page = 1;
2602
+ while (true) {
2603
+ const result = await db.find({
2604
+ collection: collection.slug,
2605
+ where: constraint,
2606
+ limit,
2607
+ page
2608
+ });
2609
+ for (const doc of result.docs) {
2610
+ if (typeof doc?.id === "string" && doc.id.length > 0) {
2611
+ ids.push(doc.id);
2612
+ }
2613
+ }
2614
+ if (!result.hasNextPage || page >= result.totalPages) {
2615
+ break;
2616
+ }
2617
+ page += 1;
2618
+ }
2619
+ return ids;
2620
+ }
2621
+ async buildCollectionAuditScope(c, collection) {
2622
+ const config = c.get("config");
2623
+ const auditAccess = collection.access?.readAudit ?? collection.access?.read;
2624
+ const access = await resolveCollectionAccess(
2625
+ config,
2626
+ collection.slug,
2627
+ "read",
2628
+ auditAccess,
2629
+ {
2630
+ user: c.get("user"),
2631
+ req: toHookRequestContext(c.req)
2632
+ }
2633
+ );
2634
+ if (!access.allowed) {
2635
+ return { denied: true, where: {} };
2636
+ }
2637
+ let where = { collection: { equals: collection.slug } };
2638
+ if (access.constraint) {
2639
+ const ids = await this.collectAccessibleDocumentIds(config.db, collection, access.constraint);
2640
+ where = mergeWhereConstraint(where, { documentId: { in: ids } });
2641
+ }
2642
+ return { denied: false, where };
2643
+ }
2644
+ async getVisibleCollections(c) {
2645
+ const config = c.get("config");
2646
+ const collections = /* @__PURE__ */ new Map();
2647
+ for (const collection of config.collections) {
2648
+ collections.set(collection.slug, collection);
2649
+ }
2650
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2651
+ if (config.onSchemaFetch && siteId) {
2652
+ const dynamic = await config.onSchemaFetch(siteId);
2653
+ for (const collection of dynamic.collections || []) {
2654
+ if (!collections.has(collection.slug)) {
2655
+ collections.set(collection.slug, collection);
2656
+ }
2657
+ }
2658
+ }
2659
+ return Array.from(collections.values());
2660
+ }
2661
+ async findForCollection(c, collection) {
2662
+ const config = c.get("config");
2663
+ if (!config.db) return c.json({ message: "Database not configured" }, 500);
2664
+ if (!collection.audit) return c.json({ message: "Audit is not enabled for this collection" }, 404);
2665
+ const query = this.parseQuery(c);
2666
+ const scope = await this.buildCollectionAuditScope(c, collection);
2667
+ if (scope.denied) {
2668
+ return c.json({ error: true, message: `Access denied: read on ${collection.slug}` }, 403);
2669
+ }
2670
+ const where = query.where ? mergeWhereConstraint(scope.where, query.where) : scope.where;
2671
+ const result = await config.db.find({
2672
+ collection: "__audit",
2673
+ where,
2674
+ limit: query.limit,
2675
+ page: query.page,
2676
+ sort: query.sort
2677
+ });
2678
+ return c.json(result);
2679
+ }
2680
+ async findAll(c) {
2681
+ const config = c.get("config");
2682
+ if (!config.db) return c.json({ message: "Database not configured" }, 500);
2683
+ const query = this.parseQuery(c);
2684
+ const collections = (await this.getVisibleCollections(c)).filter((collection) => collection.audit);
2685
+ const scopes = [];
2686
+ for (const collection of collections) {
2687
+ const scope = await this.buildCollectionAuditScope(c, collection);
2688
+ if (!scope.denied) {
2689
+ scopes.push(scope.where);
2690
+ }
2691
+ }
2692
+ if (scopes.length === 0) {
2693
+ return c.json(this.emptyResult(query.limit, query.page));
2694
+ }
2695
+ let where = scopes.length === 1 ? scopes[0] : { OR: scopes };
2696
+ if (query.where) {
2697
+ where = mergeWhereConstraint(where, query.where);
2698
+ }
2699
+ const result = await config.db.find({
2700
+ collection: "__audit",
2701
+ where,
2702
+ limit: query.limit,
2703
+ page: query.page,
2704
+ sort: query.sort
2705
+ });
2706
+ return c.json(result);
2707
+ }
2708
+ };
2709
+
2557
2710
  // src/middleware/auth.ts
2558
2711
  function getBearerToken(c) {
2559
2712
  const authHeader = c.req.header("Authorization");
@@ -2730,6 +2883,7 @@ function serializeFieldForApi(f) {
2730
2883
  return serialized;
2731
2884
  }
2732
2885
  function registerRoutes(app, config) {
2886
+ const optionsCache = /* @__PURE__ */ new Map();
2733
2887
  app.get("/api/schemas", optionalAuth(config), async (c) => {
2734
2888
  const siteId = c.req.header("X-Site-Id");
2735
2889
  let collections = [...config.collections];
@@ -2750,6 +2904,10 @@ function registerRoutes(app, config) {
2750
2904
  const serializeAccess = async (access) => {
2751
2905
  if (typeof access === "string") return access;
2752
2906
  if (typeof access === "boolean") return access;
2907
+ if (access && typeof access === "object" && typeof access.policy === "string") {
2908
+ const policy = config.accessPolicies?.[access.policy];
2909
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
2910
+ }
2753
2911
  return resolveBooleanAccess(config, access, accessArgs);
2754
2912
  };
2755
2913
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
@@ -2778,11 +2936,14 @@ function registerRoutes(app, config) {
2778
2936
  admin: f.admin,
2779
2937
  access: {
2780
2938
  read: await serializeAccess(f.access?.read),
2939
+ create: await serializeAccess(f.access?.create),
2781
2940
  update: await serializeAccess(f.access?.update)
2782
2941
  }
2783
2942
  }))),
2784
2943
  upload: !!col.upload,
2785
2944
  auth: !!col.auth,
2945
+ audit: !!col.audit,
2946
+ drafts: !!col.drafts,
2786
2947
  admin: col.admin,
2787
2948
  workflow: col.workflow ? {
2788
2949
  initialState: col.workflow.initialState,
@@ -2885,10 +3046,12 @@ function registerRoutes(app, config) {
2885
3046
  return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
2886
3047
  }
2887
3048
  let resolver;
3049
+ let cacheTTL;
2888
3050
  if (typeof field.options === "function") {
2889
3051
  resolver = field.options;
2890
3052
  } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
2891
3053
  resolver = field.options.resolve;
3054
+ cacheTTL = field.options.cacheTTL;
2892
3055
  }
2893
3056
  if (!resolver) {
2894
3057
  return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
@@ -2901,11 +3064,32 @@ function registerRoutes(app, config) {
2901
3064
  headers: c.req.header(),
2902
3065
  raw: c.req.raw
2903
3066
  };
3067
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
3068
+ let cacheKey = "";
3069
+ if (shouldCache) {
3070
+ cacheKey = JSON.stringify([
3071
+ siteId ?? "",
3072
+ colSlug,
3073
+ fieldName,
3074
+ user?.id ?? null,
3075
+ queryParams
3076
+ ]);
3077
+ const hit = optionsCache.get(cacheKey);
3078
+ if (hit && hit.expires > Date.now()) {
3079
+ return c.json(hit.value);
3080
+ }
3081
+ }
2904
3082
  const result = await resolver({
2905
3083
  db,
2906
3084
  user,
2907
3085
  req: reqContext
2908
3086
  });
3087
+ if (shouldCache) {
3088
+ optionsCache.set(cacheKey, {
3089
+ expires: Date.now() + cacheTTL * 1e3,
3090
+ value: result
3091
+ });
3092
+ }
2909
3093
  return c.json(result);
2910
3094
  } catch (err) {
2911
3095
  console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
@@ -3050,6 +3234,7 @@ function registerRoutes(app, config) {
3050
3234
  app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
3051
3235
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
3052
3236
  }
3237
+ const auditController = new AuditController();
3053
3238
  for (const collection of config.collections) {
3054
3239
  const path = `/api/collections/${collection.slug}`;
3055
3240
  const controller = new CollectionController(collection);
@@ -3057,6 +3242,9 @@ function registerRoutes(app, config) {
3057
3242
  app.post(path, (c) => controller.create(c));
3058
3243
  app.post(`${path}/media`, (c) => controller.create(c));
3059
3244
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
3245
+ if (collection.audit) {
3246
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
3247
+ }
3060
3248
  app.get(`${path}/:id`, (c) => controller.findOne(c));
3061
3249
  app.patch(`${path}/:id`, (c) => controller.update(c));
3062
3250
  app.delete(`${path}/:id`, (c) => controller.delete(c));
@@ -3076,9 +3264,32 @@ function registerRoutes(app, config) {
3076
3264
  app.patch(path, (c) => controller.update(c));
3077
3265
  app.post(`${path}/seed`, (c) => controller.seed(c));
3078
3266
  }
3267
+ if (!process.env.DYRECTED_JWT_SECRET) {
3268
+ console.warn(
3269
+ '[dyrected] DYRECTED_JWT_SECRET is not set \u2014 token-mode live preview is signing with an insecure default. Set DYRECTED_JWT_SECRET before relying on `previewMode: "token"` in production.'
3270
+ );
3271
+ }
3079
3272
  const previewController = new PreviewController();
3080
3273
  app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
3081
3274
  app.get("/api/preview-data", (c) => previewController.getData(c));
3275
+ app.get("/api/audit", (c) => auditController.findAll(c));
3276
+ app.get("/api/collections/:slug/__audit", async (c) => {
3277
+ const slug = c.req.param("slug");
3278
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
3279
+ const config2 = c.get("config");
3280
+ if (config2.collections.some((col) => col.slug === slug)) {
3281
+ return c.json({ message: "Not Found" }, 404);
3282
+ }
3283
+ if (!config2.onSchemaFetch || !siteId) {
3284
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
3285
+ }
3286
+ const dynamic = await config2.onSchemaFetch(siteId);
3287
+ const collection = dynamic.collections?.find((col) => col.slug === slug);
3288
+ if (!collection?.audit) {
3289
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
3290
+ }
3291
+ return auditController.findForCollection(c, collection);
3292
+ });
3082
3293
  app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
3083
3294
  const slug = c.req.param("slug");
3084
3295
  const siteId = c.req.header("X-Site-Id") || c.get("siteId");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dyrected/core",
3
- "version": "2.5.59",
3
+ "version": "2.5.61",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",