@dyrected/core 2.5.61 → 2.5.63

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 (36) hide show
  1. package/dist/app-config-BcuZIpvL.d.cts +1951 -0
  2. package/dist/app-config-BcuZIpvL.d.ts +1951 -0
  3. package/dist/app-config-Bm9-2Am6.d.cts +1875 -0
  4. package/dist/app-config-Bm9-2Am6.d.ts +1875 -0
  5. package/dist/app-config-CJAGGPrk.d.cts +1900 -0
  6. package/dist/app-config-CJAGGPrk.d.ts +1900 -0
  7. package/dist/app-config-CiEDJm0e.d.cts +1912 -0
  8. package/dist/app-config-CiEDJm0e.d.ts +1912 -0
  9. package/dist/app-config-DCDh8Gbx.d.cts +1926 -0
  10. package/dist/app-config-DCDh8Gbx.d.ts +1926 -0
  11. package/dist/app-config-Dv5XACR4.d.cts +2065 -0
  12. package/dist/app-config-Dv5XACR4.d.ts +2065 -0
  13. package/dist/app-config-_kkj71CB.d.cts +2010 -0
  14. package/dist/app-config-_kkj71CB.d.ts +2010 -0
  15. package/dist/app-config-tITj_0sn.d.cts +1926 -0
  16. package/dist/app-config-tITj_0sn.d.ts +1926 -0
  17. package/dist/chunk-57FNM42D.js +2392 -0
  18. package/dist/chunk-BAMX7YUC.js +1815 -0
  19. package/dist/chunk-BQV3QW3Y.js +2588 -0
  20. package/dist/chunk-EH3MJGB5.js +2571 -0
  21. package/dist/chunk-M3HKRN7E.js +1665 -0
  22. package/dist/chunk-MQZH7RQC.js +1667 -0
  23. package/dist/chunk-T626OZMH.js +2602 -0
  24. package/dist/chunk-TEGRS6J7.js +2609 -0
  25. package/dist/chunk-VCYYBN5J.js +1873 -0
  26. package/dist/chunk-WVD7PORQ.js +1672 -0
  27. package/dist/chunk-XZLIBQSO.js +2397 -0
  28. package/dist/index.cjs +364 -45
  29. package/dist/index.d.cts +36 -4
  30. package/dist/index.d.ts +36 -4
  31. package/dist/index.js +5 -1
  32. package/dist/server.cjs +2981 -716
  33. package/dist/server.d.cts +124 -12
  34. package/dist/server.d.ts +124 -12
  35. package/dist/server.js +2085 -703
  36. package/package.json +10 -1
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -46,6 +56,7 @@ __export(index_exports, {
46
56
  defineMultiSelectField: () => defineMultiSelectField,
47
57
  defineNumberField: () => defineNumberField,
48
58
  defineObjectField: () => defineObjectField,
59
+ definePublishingWorkflow: () => definePublishingWorkflow,
49
60
  defineRadioField: () => defineRadioField,
50
61
  defineRelationshipField: () => defineRelationshipField,
51
62
  defineRichTextField: () => defineRichTextField,
@@ -70,6 +81,7 @@ __export(index_exports, {
70
81
  parseMongoWhere: () => parseMongoWhere,
71
82
  parseSort: () => parseSort,
72
83
  parseSqlWhere: () => parseSqlWhere,
84
+ publishedStateName: () => publishedStateName,
73
85
  publishingWorkflow: () => publishingWorkflow,
74
86
  resolveAdminAuthCollection: () => resolveAdminAuthCollection,
75
87
  runCollectionHooks: () => runCollectionHooks,
@@ -88,10 +100,47 @@ var LIFECYCLE_EVENT_NAMES = [
88
100
  "entry.unpublished"
89
101
  ];
90
102
 
103
+ // src/auth/token.ts
104
+ var import_jose = require("jose");
105
+ var import_node_util = require("util");
106
+
107
+ // src/auth/sessions.ts
108
+ var AUTH_SESSIONS_COLLECTION = "__auth_sessions";
109
+
110
+ // src/observability.ts
111
+ var import_api = require("@opentelemetry/api");
112
+ var import_exporter_metrics_otlp_http = require("@opentelemetry/exporter-metrics-otlp-http");
113
+ var import_exporter_prometheus = require("@opentelemetry/exporter-prometheus");
114
+ var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
115
+ var import_resources = require("@opentelemetry/resources");
116
+ var import_sdk_metrics = require("@opentelemetry/sdk-metrics");
117
+ var import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
118
+ var import_pino = __toESM(require("pino"), 1);
119
+ var import_pino_pretty = __toESM(require("pino-pretty"), 1);
120
+ var import_node_stream = require("stream");
121
+ var runtimeByConfig = /* @__PURE__ */ new WeakMap();
122
+ var fallbackLogger = (0, import_pino.default)({
123
+ name: "dyrected",
124
+ enabled: process.env.DISABLE_LOGGING !== "true",
125
+ timestamp: import_pino.stdTimeFunctions.isoTime
126
+ });
127
+ function getObservabilityRuntime(config) {
128
+ if (!config || typeof config !== "object") return void 0;
129
+ return runtimeByConfig.get(config);
130
+ }
131
+ function getConfigLogger(config, component) {
132
+ const base = config?.logger && !("options" in config.logger) ? config.logger : fallbackLogger;
133
+ return base.child({ component });
134
+ }
135
+
91
136
  // src/workflows.ts
92
137
  var WORKFLOW_HISTORY_COLLECTION = "__workflow_history";
93
138
  var LIFECYCLE_EVENTS_COLLECTION = "__lifecycle_events";
94
- function publishingWorkflow() {
139
+ var EDITOR_CAPABILITIES = ["entry.edit", "entry.submit"];
140
+ var PUBLISHER_CAPABILITIES = ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"];
141
+ function definePublishingWorkflow(options = {}) {
142
+ const editors = options.editors ?? ["editor"];
143
+ const publishers = options.publishers ?? ["publisher", "admin"];
95
144
  return {
96
145
  initialState: "draft",
97
146
  draftState: "draft",
@@ -107,12 +156,14 @@ function publishingWorkflow() {
107
156
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", requiredCapabilities: ["entry.unpublish"], unpublish: true }
108
157
  ],
109
158
  roles: [
110
- { role: "editor", capabilities: ["entry.edit", "entry.submit"] },
111
- { role: "publisher", capabilities: ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"] },
112
- { role: "admin", capabilities: ["entry.edit", "entry.submit", "entry.publish", "entry.unpublish"] }
159
+ ...editors.map((role) => ({ role, capabilities: [...EDITOR_CAPABILITIES] })),
160
+ ...publishers.map((role) => ({ role, capabilities: [...PUBLISHER_CAPABILITIES] }))
113
161
  ]
114
162
  };
115
163
  }
164
+ function publishingWorkflow() {
165
+ return definePublishingWorkflow();
166
+ }
116
167
  function simplePublishingWorkflow() {
117
168
  return {
118
169
  initialState: "draft",
@@ -125,6 +176,9 @@ function simplePublishingWorkflow() {
125
176
  { name: "publish", label: "Publish", from: "draft", to: "published" },
126
177
  { name: "unpublish", label: "Unpublish", from: "published", to: "draft", unpublish: true }
127
178
  ]
179
+ // Intentionally no `roles`: this lightweight workflow (synthesized from
180
+ // `drafts: true`) does no capability-based gating. Draft visibility for a
181
+ // no-roles workflow is handled in `canViewWorkflowDraft`.
128
182
  };
129
183
  }
130
184
  function publicMetadata(meta) {
@@ -143,9 +197,13 @@ function canViewWorkflowDraft(workflow, user) {
143
197
  if (!user) return false;
144
198
  const capabilities = workflowCapabilities(workflow, user);
145
199
  if (capabilities.has("entry.edit")) return true;
146
- return workflow.transitions.some(
200
+ if (workflow.transitions.some(
147
201
  (transition) => (transition.requiredCapabilities ?? []).some((capability) => capabilities.has(capability))
148
- );
202
+ )) {
203
+ return true;
204
+ }
205
+ if (!workflow.roles || workflow.roles.length === 0) return true;
206
+ return false;
149
207
  }
150
208
  function availableWorkflowTransitions(workflow, state, user) {
151
209
  const capabilities = workflowCapabilities(workflow, user);
@@ -165,6 +223,7 @@ function publishedStateName(workflow) {
165
223
  }
166
224
  function materializeWorkflowDocument(doc, workflow, user) {
167
225
  const meta = doc.__workflow;
226
+ const publishedState = publishedStateName(workflow);
168
227
  if (!meta) {
169
228
  const { __published: _legacyPublished, __workflow: _legacyWorkflow, ...working2 } = doc;
170
229
  const state = publishedStateName(workflow);
@@ -179,7 +238,12 @@ function materializeWorkflowDocument(doc, workflow, user) {
179
238
  }
180
239
  const { __published, __workflow, ...working } = doc;
181
240
  if (!canViewWorkflowDraft(workflow, user)) {
182
- if (!__published || typeof __published !== "object") return null;
241
+ if (!__published || typeof __published !== "object") {
242
+ if (meta.state === publishedState) {
243
+ return { ...working, _workflow: publicMetadata(meta) };
244
+ }
245
+ return null;
246
+ }
183
247
  return { id: doc.id, ...__published, _workflow: publicMetadata(meta) };
184
248
  }
185
249
  return {
@@ -259,20 +323,34 @@ async function saveWorkflowDraft(args) {
259
323
  const workflow = collection.workflow;
260
324
  if (!db.transaction) throw new Error(`The configured database adapter does not support workflow transactions.`);
261
325
  const previous = originalDoc.__workflow;
326
+ const isLegacyPublishedDoc = !previous;
327
+ const baselineRevision = previous?.revision ?? 1;
262
328
  const event = createLifecycleEvent({
263
329
  name: "revision.created",
264
330
  collection: collection.slug,
265
331
  documentId: id,
266
332
  actorId: user?.sub,
267
- payload: { revision: previous.revision + 1, previousRevision: previous.revision }
333
+ payload: {
334
+ revision: baselineRevision + 1,
335
+ previousRevision: isLegacyPublishedDoc ? baselineRevision : previous?.revision ?? null
336
+ }
268
337
  });
269
338
  const doc = await db.transaction(async (tx) => {
270
339
  const nextMeta = {
271
- ...previous,
272
- state: originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
273
- revision: previous.revision + 1
340
+ ...previous ?? {},
341
+ state: isLegacyPublishedDoc ? workflow.draftState ?? workflow.initialState : originalDoc.__published ? workflow.draftState ?? workflow.initialState : previous.state,
342
+ revision: baselineRevision + 1,
343
+ ...isLegacyPublishedDoc ? { publishedRevision: baselineRevision } : {}
344
+ };
345
+ const updateData = {
346
+ ...data,
347
+ __workflow: nextMeta
274
348
  };
275
- const updated = await tx.update({ collection: collection.slug, id, data: { ...data, __workflow: nextMeta } });
349
+ if (isLegacyPublishedDoc) {
350
+ const { __workflow: _legacyWorkflow, __published: _legacyPublished, ...publishedSnapshot } = originalDoc;
351
+ updateData.__published = publishedSnapshot;
352
+ }
353
+ const updated = await tx.update({ collection: collection.slug, id, data: updateData });
276
354
  await persistEvent(tx, event);
277
355
  return updated;
278
356
  });
@@ -371,7 +449,17 @@ async function transitionWorkflow(args) {
371
449
  try {
372
450
  await hook({ ...hookContext, doc: updated, event: events[0] });
373
451
  } catch (error) {
374
- console.error("[dyrected/workflow] afterTransition hook failed:", error);
452
+ getObservabilityRuntime(config)?.recordWorkflowHookFailure({
453
+ collection: collection.slug,
454
+ transition: transition.name
455
+ });
456
+ getConfigLogger(config, "workflow").error({
457
+ err: error,
458
+ msg: "afterTransition hook failed",
459
+ collection: collection.slug,
460
+ transition: transition.name,
461
+ documentId: id
462
+ });
375
463
  }
376
464
  }
377
465
  return updated;
@@ -412,6 +500,98 @@ function humanizeProviderName(id, type) {
412
500
  return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
413
501
  }
414
502
 
503
+ // src/utils/block-references.ts
504
+ function mergeUniqueBlocks(blocks) {
505
+ const seen = /* @__PURE__ */ new Map();
506
+ for (const block of blocks) {
507
+ const existing = seen.get(block.slug);
508
+ if (existing && existing !== block) {
509
+ throw new Error(
510
+ `Duplicate block slug "${block.slug}" found in the reusable block registry. Block slugs must be unique.`
511
+ );
512
+ }
513
+ if (!existing) seen.set(block.slug, block);
514
+ }
515
+ return Array.from(seen.values());
516
+ }
517
+ function buildRegistry(blocks) {
518
+ return new Map(blocks.map((block) => [block.slug, block]));
519
+ }
520
+ function resolveField(field, registry, blockCache) {
521
+ const next = { ...field };
522
+ if (next.fields) {
523
+ next.fields = next.fields.map(
524
+ (child) => resolveField(child, registry, blockCache)
525
+ );
526
+ }
527
+ if (next.type !== "blocks") return next;
528
+ const hasInlineBlocks = Array.isArray(next.blocks) && next.blocks.length > 0;
529
+ const hasReferences = Array.isArray(next.blockReferences) && next.blockReferences.length > 0;
530
+ if (hasInlineBlocks && hasReferences) {
531
+ throw new Error(
532
+ `Blocks field "${next.name ?? "(unnamed)"}" cannot define both "blocks" and "blockReferences". Use one or the other.`
533
+ );
534
+ }
535
+ if (hasReferences) {
536
+ next.blocks = next.blockReferences.map((slug) => {
537
+ const block = registry.get(slug);
538
+ if (!block) {
539
+ throw new Error(
540
+ `Unknown block reference "${slug}" on blocks field "${next.name ?? "(unnamed)"}". Add it to defineConfig({ blocks: [...] }).`
541
+ );
542
+ }
543
+ return resolveBlock(block, registry, blockCache);
544
+ });
545
+ return next;
546
+ }
547
+ if (hasInlineBlocks) {
548
+ next.blocks = next.blocks.map(
549
+ (block) => resolveBlock(block, registry, blockCache)
550
+ );
551
+ }
552
+ return next;
553
+ }
554
+ function resolveBlock(block, registry, blockCache) {
555
+ const cached = blockCache.get(block);
556
+ if (cached) return cached;
557
+ const resolved = {
558
+ ...block,
559
+ fields: []
560
+ };
561
+ blockCache.set(block, resolved);
562
+ resolved.fields = block.fields.map(
563
+ (field) => resolveField(field, registry, blockCache)
564
+ );
565
+ return resolved;
566
+ }
567
+ function resolveFields(fields, registry, blockCache) {
568
+ return fields.map((field) => resolveField(field, registry, blockCache));
569
+ }
570
+ function normalizeSchemaFragment(fragment) {
571
+ const reusableBlocks = mergeUniqueBlocks(fragment.blocks ?? []);
572
+ const registry = buildRegistry(reusableBlocks);
573
+ const blockCache = /* @__PURE__ */ new WeakMap();
574
+ const resolvedBlocks = reusableBlocks.map(
575
+ (block) => resolveBlock(block, registry, blockCache)
576
+ );
577
+ const resolvedCollections = (fragment.collections ?? []).map(
578
+ (collection) => ({
579
+ ...collection,
580
+ fields: resolveFields(collection.fields ?? [], registry, blockCache)
581
+ })
582
+ );
583
+ const resolvedGlobals = (fragment.globals ?? []).map((global) => ({
584
+ ...global,
585
+ fields: resolveFields(global.fields ?? [], registry, blockCache)
586
+ }));
587
+ return {
588
+ ...fragment,
589
+ ...fragment.blocks ? { blocks: resolvedBlocks } : {},
590
+ ...fragment.collections ? { collections: resolvedCollections } : {},
591
+ ...fragment.globals ? { globals: resolvedGlobals } : {}
592
+ };
593
+ }
594
+
415
595
  // src/utils/config.ts
416
596
  var AUDIT_COLLECTION_SLUG = "__audit";
417
597
  var SYSTEM_FIELDS = [
@@ -446,12 +626,23 @@ var AUDIT_COLLECTION = {
446
626
  fields: [
447
627
  { name: "collection", type: "text", label: "Collection", required: true },
448
628
  { name: "documentId", type: "text", label: "Document ID" },
449
- { name: "operation", type: "select", label: "Operation", options: ["create", "update", "delete"], required: true },
629
+ {
630
+ name: "operation",
631
+ type: "select",
632
+ label: "Operation",
633
+ options: ["create", "update", "delete"],
634
+ required: true
635
+ },
450
636
  { name: "user", type: "text", label: "User ID" },
451
637
  { name: "timestamp", type: "date", label: "Timestamp", required: true },
452
638
  { name: "changes", type: "json", label: "Changes" }
453
639
  ],
454
- access: { read: () => false, create: () => false, update: () => false, delete: () => false },
640
+ access: {
641
+ read: () => false,
642
+ create: () => false,
643
+ update: () => false,
644
+ delete: () => false
645
+ },
455
646
  admin: { hidden: true }
456
647
  };
457
648
  var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
@@ -468,7 +659,12 @@ var WORKFLOW_HISTORY_COLLECTION_CONFIG = {
468
659
  { name: "actorId", type: "text" },
469
660
  { name: "createdAt", type: "date", required: true }
470
661
  ],
471
- access: { read: ({ user }) => !!user, create: () => false, update: () => false, delete: () => false },
662
+ access: {
663
+ read: ({ user }) => !!user,
664
+ create: () => false,
665
+ update: () => false,
666
+ delete: () => false
667
+ },
472
668
  admin: { hidden: true }
473
669
  };
474
670
  var LIFECYCLE_EVENTS_COLLECTION_CONFIG = {
@@ -482,22 +678,56 @@ var LIFECYCLE_EVENTS_COLLECTION_CONFIG = {
482
678
  { name: "actorId", type: "text" },
483
679
  { name: "payload", type: "json", required: true },
484
680
  { name: "attempts", type: "number", required: true },
485
- { name: "status", type: "select", options: ["pending", "processing", "delivered", "failed"], required: true },
681
+ {
682
+ name: "status",
683
+ type: "select",
684
+ options: ["pending", "processing", "delivered", "failed"],
685
+ required: true
686
+ },
486
687
  { name: "nextAttemptAt", type: "date" },
487
688
  { name: "deliveredAt", type: "date" },
488
689
  { name: "lastError", type: "textarea" }
489
690
  ],
490
- access: { read: ({ user }) => !!user?.roles?.includes("admin"), create: () => false, update: () => false, delete: () => false },
691
+ access: {
692
+ read: ({ user }) => !!user?.roles?.includes("admin"),
693
+ create: () => false,
694
+ update: () => false,
695
+ delete: () => false
696
+ },
697
+ admin: { hidden: true }
698
+ };
699
+ var AUTH_SESSIONS_COLLECTION_CONFIG = {
700
+ slug: AUTH_SESSIONS_COLLECTION,
701
+ labels: { singular: "Auth session", plural: "Auth sessions" },
702
+ fields: [
703
+ { name: "userId", type: "text", required: true },
704
+ { name: "email", type: "email", required: true },
705
+ { name: "collection", type: "text", required: true },
706
+ { name: "authSource", type: "text" },
707
+ { name: "providerId", type: "text" },
708
+ { name: "lastIp", type: "text" },
709
+ { name: "lastSeenAt", type: "date" },
710
+ { name: "expiresAt", type: "date" },
711
+ { name: "revokedAt", type: "date" }
712
+ ],
713
+ access: {
714
+ read: () => false,
715
+ create: () => false,
716
+ update: () => false,
717
+ delete: () => false
718
+ },
491
719
  admin: { hidden: true }
492
720
  };
493
721
  function normalizeConfig(config) {
494
- const collections = config?.collections || [];
495
- const globals = config?.globals || [];
722
+ const schemaAwareConfig = normalizeSchemaFragment(config);
723
+ const collections = schemaAwareConfig?.collections || [];
724
+ const globals = schemaAwareConfig?.globals || [];
496
725
  const needsAudit = collections.some((col) => col.audit);
497
726
  const needsWorkflow = collections.some((col) => col.workflow || col.drafts);
727
+ const needsAuthSessions = collections.some((col) => !!col.auth);
498
728
  const adminAuthCollectionSlug = getAdminAuthCollection({
499
729
  collections,
500
- adminAuth: config.adminAuth
730
+ adminAuth: schemaAwareConfig.adminAuth
501
731
  })?.slug;
502
732
  const normalizedCollections = collections.map((col) => {
503
733
  let fields = col.fields || [];
@@ -594,7 +824,7 @@ function normalizeConfig(config) {
594
824
  return field;
595
825
  });
596
826
  }
597
- if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && config.adminAuth?.mode === "external") {
827
+ if (adminAuthCollectionSlug && col.slug === adminAuthCollectionSlug && schemaAwareConfig.adminAuth?.mode === "external") {
598
828
  const externalAdminFields = [
599
829
  {
600
830
  name: "authProvider",
@@ -628,7 +858,9 @@ function normalizeConfig(config) {
628
858
  }
629
859
  }
630
860
  const updatedFieldNames = new Set(fields.map((f) => f.name));
631
- const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
861
+ const fieldsToInject = SYSTEM_FIELDS.filter(
862
+ (f) => !updatedFieldNames.has(f.name)
863
+ );
632
864
  const workflow = col.workflow || (col.drafts ? simplePublishingWorkflow() : void 0);
633
865
  return {
634
866
  ...col,
@@ -636,17 +868,27 @@ function normalizeConfig(config) {
636
868
  fields: [...fields, ...fieldsToInject]
637
869
  };
638
870
  });
639
- const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);
871
+ const hasAuditCollection = normalizedCollections.some(
872
+ (col) => col.slug === AUDIT_COLLECTION_SLUG
873
+ );
640
874
  const systemCollections = [];
641
- if (needsAudit && !hasAuditCollection) systemCollections.push(AUDIT_COLLECTION);
642
- if (needsWorkflow && !normalizedCollections.some((col) => col.slug === WORKFLOW_HISTORY_COLLECTION)) {
875
+ if (needsAudit && !hasAuditCollection)
876
+ systemCollections.push(AUDIT_COLLECTION);
877
+ if (needsWorkflow && !normalizedCollections.some(
878
+ (col) => col.slug === WORKFLOW_HISTORY_COLLECTION
879
+ )) {
643
880
  systemCollections.push(WORKFLOW_HISTORY_COLLECTION_CONFIG);
644
881
  }
645
- if (needsWorkflow && !normalizedCollections.some((col) => col.slug === LIFECYCLE_EVENTS_COLLECTION)) {
882
+ if (needsWorkflow && !normalizedCollections.some(
883
+ (col) => col.slug === LIFECYCLE_EVENTS_COLLECTION
884
+ )) {
646
885
  systemCollections.push(LIFECYCLE_EVENTS_COLLECTION_CONFIG);
647
886
  }
887
+ if (needsAuthSessions && !normalizedCollections.some((col) => col.slug === AUTH_SESSIONS_COLLECTION)) {
888
+ systemCollections.push(AUTH_SESSIONS_COLLECTION_CONFIG);
889
+ }
648
890
  return {
649
- ...config,
891
+ ...schemaAwareConfig,
650
892
  collections: [...normalizedCollections, ...systemCollections],
651
893
  globals
652
894
  };
@@ -872,7 +1114,10 @@ async function runCollectionHooks(hooks, args, options = {}) {
872
1114
  }
873
1115
  } catch (err) {
874
1116
  if (options.isolated) {
875
- console.error("[dyrected/core] Side-effect hook failed (error isolated \u2014 DB write was successful):", err);
1117
+ getConfigLogger(void 0, "hooks").error({
1118
+ err,
1119
+ msg: "Side-effect hook failed after a successful write"
1120
+ });
876
1121
  } else {
877
1122
  throw err;
878
1123
  }
@@ -917,7 +1162,13 @@ async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
917
1162
  const item = updatedValue[i];
918
1163
  const origItem = Array.isArray(origValue) ? origValue[i] : null;
919
1164
  arrayResult.push(
920
- await executeFieldBeforeChange(field.fields, item, origItem, user, db)
1165
+ await executeFieldBeforeChange(
1166
+ field.fields,
1167
+ item,
1168
+ origItem,
1169
+ user,
1170
+ db
1171
+ )
921
1172
  );
922
1173
  }
923
1174
  result[field.name] = arrayResult;
@@ -1058,7 +1309,15 @@ function generateOpenApi(config) {
1058
1309
  },
1059
1310
  WorkflowHistoryEntry: {
1060
1311
  type: "object",
1061
- required: ["collection", "documentId", "transition", "from", "to", "revision", "createdAt"],
1312
+ required: [
1313
+ "collection",
1314
+ "documentId",
1315
+ "transition",
1316
+ "from",
1317
+ "to",
1318
+ "revision",
1319
+ "createdAt"
1320
+ ],
1062
1321
  properties: {
1063
1322
  id: { type: "string" },
1064
1323
  collection: { type: "string" },
@@ -1083,7 +1342,11 @@ function generateOpenApi(config) {
1083
1342
  user: { type: "string", nullable: true },
1084
1343
  timestamp: { type: "string", format: "date-time" },
1085
1344
  changes: {
1086
- oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }, { type: "null" }]
1345
+ oneOf: [
1346
+ { type: "string" },
1347
+ { type: "object", additionalProperties: true },
1348
+ { type: "null" }
1349
+ ]
1087
1350
  }
1088
1351
  }
1089
1352
  },
@@ -1175,13 +1438,17 @@ function generateOpenApi(config) {
1175
1438
  get: {
1176
1439
  tags: ["Preferences"],
1177
1440
  summary: "Get an authenticated user preference",
1178
- parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
1441
+ parameters: [
1442
+ { name: "key", in: "path", required: true, schema: { type: "string" } }
1443
+ ],
1179
1444
  responses: { 200: { description: "Preference value" } }
1180
1445
  },
1181
1446
  put: {
1182
1447
  tags: ["Preferences"],
1183
1448
  summary: "Set an authenticated user preference",
1184
- parameters: [{ name: "key", in: "path", required: true, schema: { type: "string" } }],
1449
+ parameters: [
1450
+ { name: "key", in: "path", required: true, schema: { type: "string" } }
1451
+ ],
1185
1452
  requestBody: {
1186
1453
  required: true,
1187
1454
  content: { "application/json": { schema: { type: "object" } } }
@@ -1217,10 +1484,23 @@ function generateOpenApi(config) {
1217
1484
  tags: ["Audit"],
1218
1485
  summary: "Get audit entries across all readable audited collections",
1219
1486
  parameters: [
1220
- { name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
1487
+ {
1488
+ name: "limit",
1489
+ in: "query",
1490
+ schema: { type: "integer", default: 50, maximum: 100 }
1491
+ },
1221
1492
  { name: "page", in: "query", schema: { type: "integer", default: 1 } },
1222
- { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
1223
- { name: "sort", in: "query", schema: { type: "string", default: "-timestamp" } }
1493
+ {
1494
+ name: "where",
1495
+ in: "query",
1496
+ schema: { type: "string" },
1497
+ description: "JSON filter"
1498
+ },
1499
+ {
1500
+ name: "sort",
1501
+ in: "query",
1502
+ schema: { type: "string", default: "-timestamp" }
1503
+ }
1224
1504
  ],
1225
1505
  responses: {
1226
1506
  200: {
@@ -1425,10 +1705,27 @@ function generateOpenApi(config) {
1425
1705
  tags: [collectionTag],
1426
1706
  summary: `Get ${labels.singular} audit entries`,
1427
1707
  parameters: [
1428
- { name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 100 } },
1429
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
1430
- { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
1431
- { name: "sort", in: "query", schema: { type: "string", default: "-timestamp" } }
1708
+ {
1709
+ name: "limit",
1710
+ in: "query",
1711
+ schema: { type: "integer", default: 50, maximum: 100 }
1712
+ },
1713
+ {
1714
+ name: "page",
1715
+ in: "query",
1716
+ schema: { type: "integer", default: 1 }
1717
+ },
1718
+ {
1719
+ name: "where",
1720
+ in: "query",
1721
+ schema: { type: "string" },
1722
+ description: "JSON filter"
1723
+ },
1724
+ {
1725
+ name: "sort",
1726
+ in: "query",
1727
+ schema: { type: "string", default: "-timestamp" }
1728
+ }
1432
1729
  ],
1433
1730
  responses: {
1434
1731
  200: {
@@ -1554,6 +1851,15 @@ function generateOpenApi(config) {
1554
1851
  post: {
1555
1852
  tags: [collectionTag],
1556
1853
  summary: `Log out of ${labels.plural}`,
1854
+ parameters: [
1855
+ {
1856
+ name: "allSessions",
1857
+ in: "query",
1858
+ required: false,
1859
+ schema: { type: "boolean" },
1860
+ description: "Revoke every active session for this account instead of only the current one."
1861
+ }
1862
+ ],
1557
1863
  responses: { 200: { description: "Logged out" } }
1558
1864
  }
1559
1865
  };
@@ -1579,7 +1885,11 @@ function generateOpenApi(config) {
1579
1885
  post: {
1580
1886
  tags: [collectionTag],
1581
1887
  summary: "Refresh an authentication token",
1582
- responses: { 200: { description: "Refreshed token" } }
1888
+ responses: {
1889
+ 200: {
1890
+ description: "Refreshed token for the current active session"
1891
+ }
1892
+ }
1583
1893
  }
1584
1894
  };
1585
1895
  spec.paths[`${path}/forgot-password`] = {
@@ -1610,7 +1920,11 @@ function generateOpenApi(config) {
1610
1920
  schema: { type: "string" }
1611
1921
  }
1612
1922
  ],
1613
- responses: { 200: { description: "Password changed" } }
1923
+ responses: {
1924
+ 200: {
1925
+ description: "Password changed and active sessions revoked"
1926
+ }
1927
+ }
1614
1928
  }
1615
1929
  };
1616
1930
  }
@@ -1819,7 +2133,7 @@ function fieldsToProperties(fields) {
1819
2133
  return { properties: props, required };
1820
2134
  }
1821
2135
  function fieldToSchema(field) {
1822
- let schema = {};
2136
+ let schema;
1823
2137
  switch (field.type) {
1824
2138
  case "text":
1825
2139
  case "textarea":
@@ -1828,7 +2142,10 @@ function fieldToSchema(field) {
1828
2142
  break;
1829
2143
  case "url":
1830
2144
  schema = {
1831
- oneOf: [{ type: "string" }, { type: "object", additionalProperties: true }]
2145
+ oneOf: [
2146
+ { type: "string" },
2147
+ { type: "object", additionalProperties: true }
2148
+ ]
1832
2149
  };
1833
2150
  break;
1834
2151
  case "icon":
@@ -1998,6 +2315,7 @@ function defineTab(args) {
1998
2315
  defineMultiSelectField,
1999
2316
  defineNumberField,
2000
2317
  defineObjectField,
2318
+ definePublishingWorkflow,
2001
2319
  defineRadioField,
2002
2320
  defineRelationshipField,
2003
2321
  defineRichTextField,
@@ -2022,6 +2340,7 @@ function defineTab(args) {
2022
2340
  parseMongoWhere,
2023
2341
  parseSort,
2024
2342
  parseSqlWhere,
2343
+ publishedStateName,
2025
2344
  publishingWorkflow,
2026
2345
  resolveAdminAuthCollection,
2027
2346
  runCollectionHooks,