@dyrected/core 2.5.59 → 2.5.60

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.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-C0twGIaF.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-C0twGIaF.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-2WODKOUB.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
  }
@@ -608,7 +613,7 @@ var CollectionController = class {
608
613
  const readonlyDb = createReadonlyDb(db);
609
614
  const limit = Number(c.req.query("limit")) || 10;
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;
@@ -724,7 +729,7 @@ var CollectionController = class {
724
729
  }
725
730
  const readonlyDb = createReadonlyDb(db);
726
731
  const id = c.req.param("id");
727
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
732
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
728
733
  const user = c.get("user");
729
734
  if (!id) return c.json({ message: "Missing ID" }, 400);
730
735
  let doc = await db.findOne({ collection: this.collection.slug, id });
@@ -813,7 +818,8 @@ var CollectionController = class {
813
818
  fields: this.collection.fields,
814
819
  user,
815
820
  req: this.toHookRequestContext(c),
816
- data
821
+ data,
822
+ operation: "create"
817
823
  }, data);
818
824
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
819
825
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
@@ -908,7 +914,8 @@ var CollectionController = class {
908
914
  fields: this.collection.fields,
909
915
  user,
910
916
  req: this.toHookRequestContext(c),
911
- data
917
+ data,
918
+ operation: "create"
912
919
  }, data);
913
920
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
914
921
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
@@ -1330,7 +1337,7 @@ var GlobalController = class {
1330
1337
  const db = config.db;
1331
1338
  if (!db) return c.json({ message: "Database not configured" }, 500);
1332
1339
  const readonlyDb = createReadonlyDb(db);
1333
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
1340
+ const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 1;
1334
1341
  const user = c.get("user");
1335
1342
  let query = void 0;
1336
1343
  const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
@@ -2554,6 +2561,150 @@ var PreviewController = class {
2554
2561
  }
2555
2562
  };
2556
2563
 
2564
+ // src/controllers/audit.controller.ts
2565
+ var AuditController = class {
2566
+ parseQuery(c) {
2567
+ const limit = Math.min(Number(c.req.query("limit")) || 50, 100);
2568
+ const page = Math.max(Number(c.req.query("page")) || 1, 1);
2569
+ const sort = c.req.query("sort") || "-timestamp";
2570
+ const whereRaw = c.req.query("where");
2571
+ if (!whereRaw) {
2572
+ return { limit, page, sort };
2573
+ }
2574
+ try {
2575
+ return {
2576
+ limit,
2577
+ page,
2578
+ sort,
2579
+ where: JSON.parse(decodeURIComponent(whereRaw))
2580
+ };
2581
+ } catch {
2582
+ return { limit, page, sort };
2583
+ }
2584
+ }
2585
+ emptyResult(limit, page) {
2586
+ return {
2587
+ docs: [],
2588
+ total: 0,
2589
+ limit,
2590
+ page,
2591
+ totalPages: 0,
2592
+ hasNextPage: false,
2593
+ hasPrevPage: page > 1
2594
+ };
2595
+ }
2596
+ async collectAccessibleDocumentIds(db, collection, constraint) {
2597
+ const ids = [];
2598
+ const limit = 100;
2599
+ let page = 1;
2600
+ while (true) {
2601
+ const result = await db.find({
2602
+ collection: collection.slug,
2603
+ where: constraint,
2604
+ limit,
2605
+ page
2606
+ });
2607
+ for (const doc of result.docs) {
2608
+ if (typeof doc?.id === "string" && doc.id.length > 0) {
2609
+ ids.push(doc.id);
2610
+ }
2611
+ }
2612
+ if (!result.hasNextPage || page >= result.totalPages) {
2613
+ break;
2614
+ }
2615
+ page += 1;
2616
+ }
2617
+ return ids;
2618
+ }
2619
+ async buildCollectionAuditScope(c, collection) {
2620
+ const config = c.get("config");
2621
+ const auditAccess = collection.access?.readAudit ?? collection.access?.read;
2622
+ const access = await resolveCollectionAccess(
2623
+ config,
2624
+ collection.slug,
2625
+ "read",
2626
+ auditAccess,
2627
+ {
2628
+ user: c.get("user"),
2629
+ req: toHookRequestContext(c.req)
2630
+ }
2631
+ );
2632
+ if (!access.allowed) {
2633
+ return { denied: true, where: {} };
2634
+ }
2635
+ let where = { collection: { equals: collection.slug } };
2636
+ if (access.constraint) {
2637
+ const ids = await this.collectAccessibleDocumentIds(config.db, collection, access.constraint);
2638
+ where = mergeWhereConstraint(where, { documentId: { in: ids } });
2639
+ }
2640
+ return { denied: false, where };
2641
+ }
2642
+ async getVisibleCollections(c) {
2643
+ const config = c.get("config");
2644
+ const collections = /* @__PURE__ */ new Map();
2645
+ for (const collection of config.collections) {
2646
+ collections.set(collection.slug, collection);
2647
+ }
2648
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2649
+ if (config.onSchemaFetch && siteId) {
2650
+ const dynamic = await config.onSchemaFetch(siteId);
2651
+ for (const collection of dynamic.collections || []) {
2652
+ if (!collections.has(collection.slug)) {
2653
+ collections.set(collection.slug, collection);
2654
+ }
2655
+ }
2656
+ }
2657
+ return Array.from(collections.values());
2658
+ }
2659
+ async findForCollection(c, collection) {
2660
+ const config = c.get("config");
2661
+ if (!config.db) return c.json({ message: "Database not configured" }, 500);
2662
+ if (!collection.audit) return c.json({ message: "Audit is not enabled for this collection" }, 404);
2663
+ const query = this.parseQuery(c);
2664
+ const scope = await this.buildCollectionAuditScope(c, collection);
2665
+ if (scope.denied) {
2666
+ return c.json({ error: true, message: `Access denied: read on ${collection.slug}` }, 403);
2667
+ }
2668
+ const where = query.where ? mergeWhereConstraint(scope.where, query.where) : scope.where;
2669
+ const result = await config.db.find({
2670
+ collection: "__audit",
2671
+ where,
2672
+ limit: query.limit,
2673
+ page: query.page,
2674
+ sort: query.sort
2675
+ });
2676
+ return c.json(result);
2677
+ }
2678
+ async findAll(c) {
2679
+ const config = c.get("config");
2680
+ if (!config.db) return c.json({ message: "Database not configured" }, 500);
2681
+ const query = this.parseQuery(c);
2682
+ const collections = (await this.getVisibleCollections(c)).filter((collection) => collection.audit);
2683
+ const scopes = [];
2684
+ for (const collection of collections) {
2685
+ const scope = await this.buildCollectionAuditScope(c, collection);
2686
+ if (!scope.denied) {
2687
+ scopes.push(scope.where);
2688
+ }
2689
+ }
2690
+ if (scopes.length === 0) {
2691
+ return c.json(this.emptyResult(query.limit, query.page));
2692
+ }
2693
+ let where = scopes.length === 1 ? scopes[0] : { OR: scopes };
2694
+ if (query.where) {
2695
+ where = mergeWhereConstraint(where, query.where);
2696
+ }
2697
+ const result = await config.db.find({
2698
+ collection: "__audit",
2699
+ where,
2700
+ limit: query.limit,
2701
+ page: query.page,
2702
+ sort: query.sort
2703
+ });
2704
+ return c.json(result);
2705
+ }
2706
+ };
2707
+
2557
2708
  // src/middleware/auth.ts
2558
2709
  function getBearerToken(c) {
2559
2710
  const authHeader = c.req.header("Authorization");
@@ -2730,6 +2881,7 @@ function serializeFieldForApi(f) {
2730
2881
  return serialized;
2731
2882
  }
2732
2883
  function registerRoutes(app, config) {
2884
+ const optionsCache = /* @__PURE__ */ new Map();
2733
2885
  app.get("/api/schemas", optionalAuth(config), async (c) => {
2734
2886
  const siteId = c.req.header("X-Site-Id");
2735
2887
  let collections = [...config.collections];
@@ -2750,6 +2902,10 @@ function registerRoutes(app, config) {
2750
2902
  const serializeAccess = async (access) => {
2751
2903
  if (typeof access === "string") return access;
2752
2904
  if (typeof access === "boolean") return access;
2905
+ if (access && typeof access === "object" && typeof access.policy === "string") {
2906
+ const policy = config.accessPolicies?.[access.policy];
2907
+ if (typeof policy === "string" || typeof policy === "boolean") return policy;
2908
+ }
2753
2909
  return resolveBooleanAccess(config, access, accessArgs);
2754
2910
  };
2755
2911
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
@@ -2778,6 +2934,7 @@ function registerRoutes(app, config) {
2778
2934
  admin: f.admin,
2779
2935
  access: {
2780
2936
  read: await serializeAccess(f.access?.read),
2937
+ create: await serializeAccess(f.access?.create),
2781
2938
  update: await serializeAccess(f.access?.update)
2782
2939
  }
2783
2940
  }))),
@@ -2885,10 +3042,12 @@ function registerRoutes(app, config) {
2885
3042
  return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
2886
3043
  }
2887
3044
  let resolver;
3045
+ let cacheTTL;
2888
3046
  if (typeof field.options === "function") {
2889
3047
  resolver = field.options;
2890
3048
  } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
2891
3049
  resolver = field.options.resolve;
3050
+ cacheTTL = field.options.cacheTTL;
2892
3051
  }
2893
3052
  if (!resolver) {
2894
3053
  return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
@@ -2901,11 +3060,32 @@ function registerRoutes(app, config) {
2901
3060
  headers: c.req.header(),
2902
3061
  raw: c.req.raw
2903
3062
  };
3063
+ const shouldCache = typeof cacheTTL === "number" && cacheTTL > 0;
3064
+ let cacheKey = "";
3065
+ if (shouldCache) {
3066
+ cacheKey = JSON.stringify([
3067
+ siteId ?? "",
3068
+ colSlug,
3069
+ fieldName,
3070
+ user?.id ?? null,
3071
+ queryParams
3072
+ ]);
3073
+ const hit = optionsCache.get(cacheKey);
3074
+ if (hit && hit.expires > Date.now()) {
3075
+ return c.json(hit.value);
3076
+ }
3077
+ }
2904
3078
  const result = await resolver({
2905
3079
  db,
2906
3080
  user,
2907
3081
  req: reqContext
2908
3082
  });
3083
+ if (shouldCache) {
3084
+ optionsCache.set(cacheKey, {
3085
+ expires: Date.now() + cacheTTL * 1e3,
3086
+ value: result
3087
+ });
3088
+ }
2909
3089
  return c.json(result);
2910
3090
  } catch (err) {
2911
3091
  console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
@@ -3050,6 +3230,7 @@ function registerRoutes(app, config) {
3050
3230
  app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
3051
3231
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
3052
3232
  }
3233
+ const auditController = new AuditController();
3053
3234
  for (const collection of config.collections) {
3054
3235
  const path = `/api/collections/${collection.slug}`;
3055
3236
  const controller = new CollectionController(collection);
@@ -3057,6 +3238,9 @@ function registerRoutes(app, config) {
3057
3238
  app.post(path, (c) => controller.create(c));
3058
3239
  app.post(`${path}/media`, (c) => controller.create(c));
3059
3240
  app.delete(`${path}/delete-many`, (c) => controller.deleteMany(c));
3241
+ if (collection.audit) {
3242
+ app.get(`${path}/__audit`, (c) => auditController.findForCollection(c, collection));
3243
+ }
3060
3244
  app.get(`${path}/:id`, (c) => controller.findOne(c));
3061
3245
  app.patch(`${path}/:id`, (c) => controller.update(c));
3062
3246
  app.delete(`${path}/:id`, (c) => controller.delete(c));
@@ -3079,6 +3263,24 @@ function registerRoutes(app, config) {
3079
3263
  const previewController = new PreviewController();
3080
3264
  app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
3081
3265
  app.get("/api/preview-data", (c) => previewController.getData(c));
3266
+ app.get("/api/audit", (c) => auditController.findAll(c));
3267
+ app.get("/api/collections/:slug/__audit", async (c) => {
3268
+ const slug = c.req.param("slug");
3269
+ const siteId = c.req.header("X-Site-Id") || c.get("siteId");
3270
+ const config2 = c.get("config");
3271
+ if (config2.collections.some((col) => col.slug === slug)) {
3272
+ return c.json({ message: "Not Found" }, 404);
3273
+ }
3274
+ if (!config2.onSchemaFetch || !siteId) {
3275
+ return c.json({ message: `Collection "${slug}" not found` }, 404);
3276
+ }
3277
+ const dynamic = await config2.onSchemaFetch(siteId);
3278
+ const collection = dynamic.collections?.find((col) => col.slug === slug);
3279
+ if (!collection?.audit) {
3280
+ return c.json({ message: `Collection "${slug}" not found or has no audit log` }, 404);
3281
+ }
3282
+ return auditController.findForCollection(c, collection);
3283
+ });
3082
3284
  app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
3083
3285
  const slug = c.req.param("slug");
3084
3286
  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.60",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",