@dyrected/core 2.5.58 → 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-oU1EGHND.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-oU1EGHND.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);
@@ -425,6 +429,10 @@ async function resolveCollectionAccess(config, collection, action, access, args)
425
429
  allowed: await matchesAccessConstraint(config, collection, args.id, result)
426
430
  };
427
431
  }
432
+ function resolveFieldAccessId(context) {
433
+ const candidate = context.id ?? context.doc?.id ?? context.data?.id;
434
+ return typeof candidate === "string" && candidate.length > 0 ? candidate : void 0;
435
+ }
428
436
  async function applyFieldReadAccess(context, doc) {
429
437
  if (!doc || typeof doc !== "object") return doc;
430
438
  const result = { ...doc };
@@ -439,7 +447,8 @@ async function applyFieldReadAccess(context, doc) {
439
447
  user: context.user,
440
448
  req: context.req,
441
449
  doc: context.doc,
442
- data: context.data
450
+ data: context.data,
451
+ id: resolveFieldAccessId(context)
443
452
  });
444
453
  if (!canRead) {
445
454
  delete result[field.name];
@@ -451,9 +460,10 @@ async function applyFieldReadAccess(context, doc) {
451
460
  continue;
452
461
  }
453
462
  if (field.type === "array" && field.fields && Array.isArray(value)) {
463
+ const childFields = field.fields;
454
464
  result[field.name] = await Promise.all(
455
465
  value.map(
456
- (item) => typeof item === "object" && item !== null ? applyFieldReadAccess({ ...context, fields: field.fields }, item) : item
466
+ (item) => typeof item === "object" && item !== null ? applyFieldReadAccess({ ...context, fields: childFields }, item) : item
457
467
  )
458
468
  );
459
469
  continue;
@@ -464,7 +474,7 @@ async function applyFieldReadAccess(context, doc) {
464
474
  if (typeof item !== "object" || item === null) return item;
465
475
  const typedItem = item;
466
476
  const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
467
- if (!block) return item;
477
+ if (!block || !block.fields) return item;
468
478
  return applyFieldReadAccess({ ...context, fields: block.fields }, typedItem);
469
479
  })
470
480
  );
@@ -481,13 +491,15 @@ async function applyFieldWriteAccess(context, data) {
481
491
  continue;
482
492
  }
483
493
  if (!field.name || !(field.name in result)) continue;
484
- 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, {
485
496
  user: context.user,
486
497
  req: context.req,
487
498
  doc: context.doc,
488
- data: context.data
499
+ data: context.data,
500
+ id: resolveFieldAccessId(context)
489
501
  });
490
- if (!canUpdate) {
502
+ if (!canWrite) {
491
503
  delete result[field.name];
492
504
  continue;
493
505
  }
@@ -498,9 +510,10 @@ async function applyFieldWriteAccess(context, data) {
498
510
  continue;
499
511
  }
500
512
  if (field.type === "array" && field.fields && Array.isArray(value)) {
513
+ const childFields = field.fields;
501
514
  result[field.name] = await Promise.all(
502
515
  value.map(
503
- (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess({ ...context, fields: field.fields }, item) : item
516
+ (item) => typeof item === "object" && item !== null ? applyFieldWriteAccess({ ...context, fields: childFields }, item) : item
504
517
  )
505
518
  );
506
519
  continue;
@@ -511,7 +524,7 @@ async function applyFieldWriteAccess(context, data) {
511
524
  if (typeof item !== "object" || item === null) return item;
512
525
  const typedItem = item;
513
526
  const block = field.blocks?.find((candidate) => candidate.slug === typedItem.blockType);
514
- if (!block) return item;
527
+ if (!block || !block.fields) return item;
515
528
  return applyFieldWriteAccess({ ...context, fields: block.fields }, typedItem);
516
529
  })
517
530
  );
@@ -600,7 +613,7 @@ var CollectionController = class {
600
613
  const readonlyDb = createReadonlyDb(db);
601
614
  const limit = Number(c.req.query("limit")) || 10;
602
615
  const page = Number(c.req.query("page")) || 1;
603
- 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;
604
617
  const sort = c.req.query("sort") || void 0;
605
618
  const user = c.get("user");
606
619
  let where = void 0;
@@ -716,7 +729,7 @@ var CollectionController = class {
716
729
  }
717
730
  const readonlyDb = createReadonlyDb(db);
718
731
  const id = c.req.param("id");
719
- 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;
720
733
  const user = c.get("user");
721
734
  if (!id) return c.json({ message: "Missing ID" }, 400);
722
735
  let doc = await db.findOne({ collection: this.collection.slug, id });
@@ -805,7 +818,8 @@ var CollectionController = class {
805
818
  fields: this.collection.fields,
806
819
  user,
807
820
  req: this.toHookRequestContext(c),
808
- data
821
+ data,
822
+ operation: "create"
809
823
  }, data);
810
824
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
811
825
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
@@ -900,7 +914,8 @@ var CollectionController = class {
900
914
  fields: this.collection.fields,
901
915
  user,
902
916
  req: this.toHookRequestContext(c),
903
- data
917
+ data,
918
+ operation: "create"
904
919
  }, data);
905
920
  data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
906
921
  data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
@@ -1322,7 +1337,7 @@ var GlobalController = class {
1322
1337
  const db = config.db;
1323
1338
  if (!db) return c.json({ message: "Database not configured" }, 500);
1324
1339
  const readonlyDb = createReadonlyDb(db);
1325
- 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;
1326
1341
  const user = c.get("user");
1327
1342
  let query = void 0;
1328
1343
  const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
@@ -2546,6 +2561,150 @@ var PreviewController = class {
2546
2561
  }
2547
2562
  };
2548
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
+
2549
2708
  // src/middleware/auth.ts
2550
2709
  function getBearerToken(c) {
2551
2710
  const authHeader = c.req.header("Authorization");
@@ -2722,6 +2881,7 @@ function serializeFieldForApi(f) {
2722
2881
  return serialized;
2723
2882
  }
2724
2883
  function registerRoutes(app, config) {
2884
+ const optionsCache = /* @__PURE__ */ new Map();
2725
2885
  app.get("/api/schemas", optionalAuth(config), async (c) => {
2726
2886
  const siteId = c.req.header("X-Site-Id");
2727
2887
  let collections = [...config.collections];
@@ -2742,6 +2902,10 @@ function registerRoutes(app, config) {
2742
2902
  const serializeAccess = async (access) => {
2743
2903
  if (typeof access === "string") return access;
2744
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
+ }
2745
2909
  return resolveBooleanAccess(config, access, accessArgs);
2746
2910
  };
2747
2911
  const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
@@ -2770,6 +2934,7 @@ function registerRoutes(app, config) {
2770
2934
  admin: f.admin,
2771
2935
  access: {
2772
2936
  read: await serializeAccess(f.access?.read),
2937
+ create: await serializeAccess(f.access?.create),
2773
2938
  update: await serializeAccess(f.access?.update)
2774
2939
  }
2775
2940
  }))),
@@ -2877,10 +3042,12 @@ function registerRoutes(app, config) {
2877
3042
  return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
2878
3043
  }
2879
3044
  let resolver;
3045
+ let cacheTTL;
2880
3046
  if (typeof field.options === "function") {
2881
3047
  resolver = field.options;
2882
3048
  } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
2883
3049
  resolver = field.options.resolve;
3050
+ cacheTTL = field.options.cacheTTL;
2884
3051
  }
2885
3052
  if (!resolver) {
2886
3053
  return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
@@ -2893,11 +3060,32 @@ function registerRoutes(app, config) {
2893
3060
  headers: c.req.header(),
2894
3061
  raw: c.req.raw
2895
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
+ }
2896
3078
  const result = await resolver({
2897
3079
  db,
2898
3080
  user,
2899
3081
  req: reqContext
2900
3082
  });
3083
+ if (shouldCache) {
3084
+ optionsCache.set(cacheKey, {
3085
+ expires: Date.now() + cacheTTL * 1e3,
3086
+ value: result
3087
+ });
3088
+ }
2901
3089
  return c.json(result);
2902
3090
  } catch (err) {
2903
3091
  console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
@@ -3042,6 +3230,7 @@ function registerRoutes(app, config) {
3042
3230
  app.post(`${path}/invite`, requireAuth(config), (c) => authController.invite(c));
3043
3231
  app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
3044
3232
  }
3233
+ const auditController = new AuditController();
3045
3234
  for (const collection of config.collections) {
3046
3235
  const path = `/api/collections/${collection.slug}`;
3047
3236
  const controller = new CollectionController(collection);
@@ -3049,6 +3238,9 @@ function registerRoutes(app, config) {
3049
3238
  app.post(path, (c) => controller.create(c));
3050
3239
  app.post(`${path}/media`, (c) => controller.create(c));
3051
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
+ }
3052
3244
  app.get(`${path}/:id`, (c) => controller.findOne(c));
3053
3245
  app.patch(`${path}/:id`, (c) => controller.update(c));
3054
3246
  app.delete(`${path}/:id`, (c) => controller.delete(c));
@@ -3071,6 +3263,24 @@ function registerRoutes(app, config) {
3071
3263
  const previewController = new PreviewController();
3072
3264
  app.post("/api/preview-token", requireAuth(config), (c) => previewController.createToken(c));
3073
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
+ });
3074
3284
  app.post("/api/collections/:slug/:id/transitions/:transition", requireAuth(config), async (c) => {
3075
3285
  const slug = c.req.param("slug");
3076
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.58",
3
+ "version": "2.5.60",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",