@dyrected/core 2.8.0 → 2.8.2

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.
@@ -0,0 +1,2404 @@
1
+ import { icons } from 'lucide-react';
2
+ import { LoggerOptions, DestinationStream, Logger } from 'pino';
3
+
4
+ /**
5
+ * Minimum HTTP request context passed to every server-side hook and resolver.
6
+ *
7
+ * The full Web Standard `Request` is available as `raw` when you need it, but
8
+ * most hooks only need `query` (URL search parameters).
9
+ */
10
+ interface HookRequestContext {
11
+ /** Parsed URL query-string parameters, e.g. `{ page: '2', search: 'hello' }`. */
12
+ query: Record<string, string>;
13
+ /** Incoming HTTP headers, lowercased. */
14
+ headers: Record<string, string>;
15
+ /** The raw Web Standard `Request` object. Useful for streaming or advanced header inspection. */
16
+ raw?: Request;
17
+ }
18
+ /**
19
+ * Base shape of an authenticated user as decoded from the JWT.
20
+ *
21
+ * The actual shape will include every field on your auth collection — this
22
+ * interface only guarantees the properties that Dyrected always stamps on the
23
+ * token. Extend it in your own codebase for stronger typing:
24
+ *
25
+ * @example
26
+ * declare module '@dyrected/core' {
27
+ * interface AuthenticatedUser {
28
+ * role: 'admin' | 'editor'
29
+ * organizationId: string
30
+ * }
31
+ * }
32
+ */
33
+ interface AuthenticatedUser {
34
+ /** The user's document ID in the database. */
35
+ sub: string;
36
+ /** The user's email address. */
37
+ email?: string;
38
+ /** Slug of the collection this user was authenticated against. */
39
+ collection: string;
40
+ /** Array of role strings, if your auth collection has a `roles` field. */
41
+ roles?: string[];
42
+ /** Any additional fields from the auth collection document. */
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ interface AccessFunctionArgs<TDoc extends object = Record<string, unknown>, TUser extends AuthenticatedUser = AuthenticatedUser> {
47
+ user: TUser | undefined;
48
+ doc?: TDoc;
49
+ data?: Partial<TDoc>;
50
+ req: HookRequestContext;
51
+ }
52
+ interface NamedAccessPolicy<TDoc extends object = Record<string, unknown>> {
53
+ policy: string;
54
+ params?: Record<string, unknown>;
55
+ }
56
+ type AccessResult = boolean | Record<string, unknown>;
57
+ /**
58
+ * A function that determines whether the current user can perform an operation.
59
+ *
60
+ * Return `true` to allow, `false` to deny.
61
+ * Return a `where`-style object to allow access only to matching documents
62
+ * (useful for multi-tenant setups where users can only see their own data).
63
+ *
64
+ * Can also be expressed as a Jexl expression **string** for simple role checks
65
+ * that need to be serialised (e.g. stored in the database or sent to the Admin UI).
66
+ *
67
+ * @template TDoc The shape of the collection's document.
68
+ *
69
+ * @example
70
+ * // Simple role check
71
+ * access: {
72
+ * delete: ({ user }) => user?.roles?.includes('admin') ?? false,
73
+ * }
74
+ *
75
+ * @example
76
+ * // Row-level: users can only read their own documents
77
+ * access: {
78
+ * read: ({ user }) => ({ owner: { equals: user?.sub } }),
79
+ * }
80
+ *
81
+ * @example
82
+ * // Jexl string — evaluated server-side
83
+ * access: {
84
+ * update: "user.roles contains 'editor'",
85
+ * }
86
+ */
87
+ type AccessFunction<TDoc extends object = Record<string, unknown>, TUser extends AuthenticatedUser = AuthenticatedUser> = (args: AccessFunctionArgs<TDoc, TUser>) => AccessResult | Promise<AccessResult>;
88
+ type AccessRule<TDoc extends object = Record<string, unknown>, TUser extends AuthenticatedUser = AuthenticatedUser> = boolean | string | AccessFunction<TDoc, TUser> | NamedAccessPolicy<TDoc>;
89
+ type AccessPolicyResolver<TDoc extends object = Record<string, unknown>, TUser extends AuthenticatedUser = AuthenticatedUser> = (args: AccessFunctionArgs<TDoc, TUser> & {
90
+ params?: Record<string, unknown>;
91
+ }) => AccessResult | Promise<AccessResult>;
92
+
93
+ /**
94
+ * The minimum shape of every document returned by the database layer.
95
+ *
96
+ * All documents have an `id` field assigned by the adapter. Additional fields
97
+ * are stored as `unknown` until you narrow them with your own interface.
98
+ *
99
+ * Use this as the base when declaring your collection document types:
100
+ * ```ts
101
+ * interface Post extends BaseDocument {
102
+ * title: string
103
+ * slug: string
104
+ * }
105
+ * ```
106
+ */
107
+ interface BaseDocument {
108
+ /** The document's unique identifier, assigned by the database adapter. */
109
+ id: string;
110
+ [key: string]: any;
111
+ }
112
+ /**
113
+ * The envelope returned by collection list endpoints (`GET /api/collections/:slug`).
114
+ *
115
+ * @template T The document type.
116
+ */
117
+ interface PaginatedResult<T = Record<string, any>> {
118
+ /** The documents on the current page. */
119
+ docs: T[];
120
+ /** Total number of documents matching the query (across all pages). */
121
+ total: number;
122
+ /** Maximum number of documents per page as requested. */
123
+ limit: number;
124
+ /** The current page number (1-indexed). */
125
+ page: number;
126
+ /** Total number of pages given the current `limit`. */
127
+ totalPages: number;
128
+ /** Whether a next page exists. */
129
+ hasNextPage: boolean;
130
+ /** Whether a previous page exists. */
131
+ hasPrevPage: boolean;
132
+ }
133
+ /**
134
+ * Metadata returned after a file is uploaded and stored.
135
+ * Stored on the document in upload collections.
136
+ */
137
+ interface FileData {
138
+ filename: string;
139
+ filesize?: number;
140
+ mimeType: string;
141
+ /** Public URL of the stored file. */
142
+ url: string;
143
+ width?: number;
144
+ height?: number;
145
+ focalPoint?: {
146
+ x: number;
147
+ y: number;
148
+ };
149
+ /** Base64-encoded BlurHash string for progressive image loading. */
150
+ blurhash?: string;
151
+ /** `'upload'` for server-stored files; `'external'` for provider-managed files. */
152
+ type?: "upload" | "external";
153
+ provider?: string;
154
+ provider_metadata?: unknown;
155
+ [key: string]: unknown;
156
+ }
157
+
158
+ /** A valid icon component name from the Lucide icon set bundled with Dyrected Admin. */
159
+ type AdminIconName = keyof typeof icons;
160
+ /** Ordered custom component keys rendered around the built-in Admin dashboard. */
161
+ interface AdminDashboardComponentSlots {
162
+ beforeDashboard?: string[];
163
+ afterDashboard?: string[];
164
+ }
165
+ /** Ordered custom component keys rendered around a collection's list content. */
166
+ interface CollectionListComponentSlots {
167
+ beforeList?: string[];
168
+ beforeListTable?: string[];
169
+ afterListTable?: string[];
170
+ afterList?: string[];
171
+ }
172
+ /**
173
+ * Branding and metadata options for the Dyrected Admin UI.
174
+ *
175
+ * @example
176
+ * admin: {
177
+ * branding: {
178
+ * logo: '/logo.svg',
179
+ * primaryColor: '#6366f1',
180
+ * },
181
+ * meta: {
182
+ * titleSuffix: '- My App',
183
+ * },
184
+ * }
185
+ */
186
+ interface AdminConfig {
187
+ /** Custom component slots around the built-in dashboard. */
188
+ components?: AdminDashboardComponentSlots;
189
+ branding?: {
190
+ /** Full logo image shown in the expanded sidebar. URL or imported image asset. */
191
+ logo?: string;
192
+ /** Compact logo mark used in the collapsed sidebar state. */
193
+ logoMark?: string;
194
+ /** Text alternative or addition to the logo image. */
195
+ logoText?: string;
196
+ /**
197
+ * Brand colour used for committed, filled actions — Save, Create, Upload —
198
+ * and active/selected states. Accepts a hex string, a named colour
199
+ * (`amber`, `lime`, `violet`, `green`, `blue`, `red`, `purple`, `orange`),
200
+ * or a raw HSL triplet (`"217 91% 60%"`). Applied in both light and dark mode.
201
+ *
202
+ * This is one half of the two-colour brand model. Use {@link accentColor}
203
+ * for links and navigation accents. If you only set `primaryColor`, it is
204
+ * reused for accents too, matching the single-brand-colour look.
205
+ * @example '#6366f1'
206
+ * @example 'blue'
207
+ * @example 'hsl(240 50% 60%)'
208
+ */
209
+ primaryColor?: string;
210
+ /**
211
+ * Brand colour used for links, active navigation details, hover text, and
212
+ * focus rings — the "accent" half of the two-colour brand model, mapped to
213
+ * the admin's `--intelligence` token. Accepts the same formats as
214
+ * {@link primaryColor} and applies in both light and dark mode.
215
+ *
216
+ * Set this when your brand has a distinct link/accent colour separate from
217
+ * your primary action colour. When omitted, accents fall back to
218
+ * `primaryColor` (if set) or the built-in default.
219
+ * @example '#8b5cf6'
220
+ * @example 'violet'
221
+ */
222
+ accentColor?: string;
223
+ /** Browser tab favicon URL. */
224
+ favicon?: string;
225
+ /** Font family for body and UI text. Must be loaded separately. */
226
+ fontSans?: string;
227
+ /** Font family for headings. Must be loaded separately. */
228
+ fontSerif?: string;
229
+ };
230
+ meta?: {
231
+ /**
232
+ * String appended to every Admin page's `<title>`.
233
+ * @default '- Dyrected'
234
+ */
235
+ titleSuffix?: string;
236
+ };
237
+ /**
238
+ * The canonical/base URL of the frontend website for links and iframe live previews.
239
+ */
240
+ siteUrl?: string;
241
+ }
242
+
243
+ type DeclarativeHookExpression = string;
244
+ /**
245
+ * @deprecated Use {@link FieldBeforeChangeHook} or `FieldAfterReadHook` instead.
246
+ * This alias remains for backwards compatibility.
247
+ */
248
+ type FieldHook<TDoc extends object = Record<string, unknown>, TValue = unknown> = FieldBeforeChangeHook<TValue, TDoc>;
249
+ /**
250
+ * Runs before Dyrected queries the database for a list or single-document fetch.
251
+ *
252
+ * Return a new `where` query object to override or extend the current filter.
253
+ * Return `undefined` (or nothing) to leave the query unchanged.
254
+ */
255
+ type CollectionBeforeReadHook = (args: {
256
+ req: HookRequestContext;
257
+ query?: Record<string, unknown>;
258
+ user?: AuthenticatedUser;
259
+ db: ReadonlyDatabaseAdapter;
260
+ }) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;
261
+ type CollectionBeforeReadHookEntry = CollectionBeforeReadHook | DeclarativeHookExpression;
262
+ /**
263
+ * Runs after a document (or list of documents) is fetched from the database,
264
+ * before the response is sent to the client.
265
+ */
266
+ type CollectionAfterReadHook<TDoc extends object = Record<string, unknown>> = (args: {
267
+ doc: TDoc;
268
+ req: HookRequestContext;
269
+ user?: AuthenticatedUser;
270
+ db: ReadonlyDatabaseAdapter;
271
+ }) => TDoc | Promise<TDoc>;
272
+ type CollectionAfterReadHookEntry<TDoc extends object = Record<string, unknown>> = CollectionAfterReadHook<TDoc> | DeclarativeHookExpression;
273
+ /**
274
+ * Runs **before** a document is created or updated in the database.
275
+ */
276
+ type CollectionBeforeChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
277
+ data: Partial<TDoc>;
278
+ doc?: TDoc;
279
+ req: HookRequestContext;
280
+ user?: AuthenticatedUser;
281
+ operation: "create" | "update";
282
+ db: ReadonlyDatabaseAdapter;
283
+ }) => Partial<TDoc> | void | Promise<Partial<TDoc> | void>;
284
+ type CollectionBeforeChangeHookEntry<TDoc extends object = Record<string, unknown>> = CollectionBeforeChangeHook<TDoc> | DeclarativeHookExpression;
285
+ /**
286
+ * Runs **after** a document is created or updated in the database.
287
+ */
288
+ type CollectionAfterChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
289
+ doc: TDoc;
290
+ previousDoc?: TDoc;
291
+ req: HookRequestContext;
292
+ user?: AuthenticatedUser;
293
+ operation: "create" | "update";
294
+ db: DatabaseAdapter;
295
+ }) => void | Promise<void>;
296
+ /**
297
+ * Runs **before** a document is deleted from the database.
298
+ */
299
+ type CollectionBeforeDeleteHook<TDoc extends object = Record<string, unknown>> = (args: {
300
+ id: string;
301
+ doc: TDoc;
302
+ req: HookRequestContext;
303
+ user?: AuthenticatedUser;
304
+ db: ReadonlyDatabaseAdapter;
305
+ }) => void | Promise<void>;
306
+ /**
307
+ * Runs **after** a document has been deleted from the database.
308
+ */
309
+ type CollectionAfterDeleteHook<TDoc extends object = Record<string, unknown>> = (args: {
310
+ id: string;
311
+ doc: TDoc;
312
+ req: HookRequestContext;
313
+ user?: AuthenticatedUser;
314
+ db: DatabaseAdapter;
315
+ }) => void | Promise<void>;
316
+ /** @see {@link CollectionBeforeReadHook} */
317
+ type GlobalBeforeReadHook = CollectionBeforeReadHook;
318
+ type GlobalBeforeReadHookEntry = CollectionBeforeReadHookEntry;
319
+ /**
320
+ * Runs after the global document is fetched, before the response is sent.
321
+ */
322
+ type GlobalAfterReadHook<TDoc extends object = Record<string, unknown>> = (args: {
323
+ doc: TDoc;
324
+ req: HookRequestContext;
325
+ user?: AuthenticatedUser;
326
+ db: ReadonlyDatabaseAdapter;
327
+ }) => TDoc | Promise<TDoc>;
328
+ type GlobalAfterReadHookEntry<TDoc extends object = Record<string, unknown>> = GlobalAfterReadHook<TDoc> | DeclarativeHookExpression;
329
+ /**
330
+ * Runs before the global document is updated.
331
+ * Operation is always `'update'` (globals cannot be created or deleted).
332
+ */
333
+ type GlobalBeforeChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
334
+ data: Partial<TDoc>;
335
+ doc?: TDoc;
336
+ req: HookRequestContext;
337
+ user?: AuthenticatedUser;
338
+ operation: "update";
339
+ db: ReadonlyDatabaseAdapter;
340
+ }) => Partial<TDoc> | void | Promise<Partial<TDoc> | void>;
341
+ type GlobalBeforeChangeHookEntry<TDoc extends object = Record<string, unknown>> = GlobalBeforeChangeHook<TDoc> | DeclarativeHookExpression;
342
+ /**
343
+ * Runs after the global document is updated. Side-effects only.
344
+ */
345
+ type GlobalAfterChangeHook<TDoc extends object = Record<string, unknown>> = (args: {
346
+ doc: TDoc;
347
+ previousDoc?: TDoc;
348
+ req: HookRequestContext;
349
+ user?: AuthenticatedUser;
350
+ operation: "update";
351
+ db: DatabaseAdapter;
352
+ }) => void | Promise<void>;
353
+ /**
354
+ * @deprecated Use the specific hook types instead:
355
+ * `CollectionBeforeChangeHook`, `CollectionAfterReadHook`, etc.
356
+ *
357
+ * This broad type remains for backwards compatibility with the internal hook runner.
358
+ */
359
+ type HookFunction<TDoc extends object = Record<string, unknown>> = (args: {
360
+ data?: Partial<TDoc>;
361
+ doc?: TDoc;
362
+ user?: AuthenticatedUser;
363
+ req?: HookRequestContext;
364
+ operation?: "create" | "update" | "delete";
365
+ db?: DatabaseAdapter;
366
+ [key: string]: unknown;
367
+ }) => unknown | Promise<unknown>;
368
+
369
+ declare const LIFECYCLE_EVENT_NAMES: readonly ["revision.created", "workflow.transitioned", "entry.published", "entry.unpublished"];
370
+ type LifecycleEventName = (typeof LIFECYCLE_EVENT_NAMES)[number];
371
+ interface WorkflowState {
372
+ /** Stable machine-readable state key. */
373
+ name: string;
374
+ /** Label rendered in the Admin UI. */
375
+ label: string;
376
+ /** Marks the state whose revision is visible to public readers. */
377
+ published?: boolean;
378
+ /** Optional visual tone used by the Admin UI. */
379
+ color?: "neutral" | "warning" | "success" | "danger" | "info";
380
+ }
381
+ interface WorkflowTransition {
382
+ /** Stable transition key used by the REST and SDK APIs. */
383
+ name: string;
384
+ label: string;
385
+ from: string | string[];
386
+ to: string;
387
+ /** Every listed capability is required. */
388
+ requiredCapabilities?: string[];
389
+ /** Require a non-empty comment when performing the transition. */
390
+ requireComment?: boolean;
391
+ /** Remove the public snapshot after this transition commits. */
392
+ unpublish?: boolean;
393
+ }
394
+ interface WorkflowRole {
395
+ /** Existing user role value, for example `editor` or `publisher`. */
396
+ role: string;
397
+ capabilities: string[];
398
+ }
399
+ /**
400
+ * Configuration for a collection's editorial workflow: the states a document
401
+ * can be in, the transitions between them, and the roles allowed to perform
402
+ * each. Attach it to a collection's `workflow`, or build one with
403
+ * `definePublishingWorkflow`.
404
+ */
405
+ interface WorkflowConfig<TDoc extends object = Record<string, unknown>> {
406
+ /** The state every new document starts in, e.g. `"draft"`. */
407
+ initialState: string;
408
+ /** State used for a new working revision created from published content. */
409
+ draftState?: string;
410
+ /** All states a document can occupy. Exactly one should be marked `published`. */
411
+ states: WorkflowState[];
412
+ /** The allowed moves between states, each with its own capability and comment rules. */
413
+ transitions: WorkflowTransition[];
414
+ /** Maps values in `user.roles` to workflow capabilities. */
415
+ roles?: WorkflowRole[];
416
+ /** Server-side hooks that run around every transition. */
417
+ hooks?: {
418
+ /** Runs before a transition commits — throw to validate or block the move. */
419
+ beforeTransition?: CollectionBeforeTransitionHook<TDoc>[];
420
+ /** Runs after a transition commits — trigger notifications or downstream work. */
421
+ afterTransition?: CollectionAfterTransitionHook<TDoc>[];
422
+ };
423
+ }
424
+ interface WorkflowMetadata {
425
+ /** The document's current workflow state, e.g. `"draft"` or `"published"`. */
426
+ state: string;
427
+ /** Revision counter, incremented on every committed transition. */
428
+ revision: number;
429
+ /** Revision number currently exposed as the public snapshot, once published. */
430
+ publishedRevision?: number;
431
+ /** ISO timestamp of the most recent publish. */
432
+ publishedAt?: string;
433
+ /** Id of the user who last published this document. */
434
+ publishedBy?: string;
435
+ /** Transitions currently allowed for the requesting user. Response-only. */
436
+ availableTransitions?: string[];
437
+ }
438
+ interface WorkflowTransitionContext<TDoc extends object = Record<string, unknown>> {
439
+ /** The transition being performed. */
440
+ transition: WorkflowTransition;
441
+ /** State the document is moving out of. */
442
+ from: string;
443
+ /** State the document is moving into. */
444
+ to: string;
445
+ /** The document as it stands before the transition commits. */
446
+ doc: TDoc;
447
+ /** The user performing the transition, when the request is authenticated. */
448
+ user?: AuthenticatedUser;
449
+ /** Comment supplied with the transition; required when `requireComment` is set. */
450
+ comment?: string;
451
+ /** Request context for the transition. */
452
+ req: HookRequestContext;
453
+ /** Transaction-scoped database adapter for reads and writes inside the hook. */
454
+ db: DatabaseAdapter;
455
+ }
456
+ type CollectionBeforeTransitionHook<TDoc extends object = Record<string, unknown>> = (args: WorkflowTransitionContext<TDoc>) => void | Promise<void>;
457
+ type CollectionAfterTransitionHook<TDoc extends object = Record<string, unknown>> = (args: WorkflowTransitionContext<TDoc> & {
458
+ event: LifecycleEvent;
459
+ }) => void | Promise<void>;
460
+ interface LifecycleEvent<TPayload = Record<string, unknown>> {
461
+ /** Unique id for this event. */
462
+ id: string;
463
+ /** The lifecycle event name, e.g. `"workflow.transitioned"` or `"entry.published"`. */
464
+ name: LifecycleEventName;
465
+ /** Slug of the collection the event is about. */
466
+ collection: string;
467
+ /** Id of the document the event is about. */
468
+ documentId: string;
469
+ /** ISO timestamp of when the event occurred. */
470
+ occurredAt: string;
471
+ /** Id of the user who triggered the event, when known. */
472
+ actorId?: string;
473
+ /** Event-specific data, such as the transition name and affected revision. */
474
+ payload: TPayload;
475
+ /** Number of delivery attempts made so far. */
476
+ attempts: number;
477
+ /** Current delivery status as the dispatcher works through the queue. */
478
+ status: "pending" | "processing" | "delivered" | "failed";
479
+ /** ISO timestamp of the next delivery retry, while pending. */
480
+ nextAttemptAt?: string;
481
+ /** ISO timestamp of successful delivery. */
482
+ deliveredAt?: string;
483
+ /** Message from the most recent failed delivery attempt. */
484
+ lastError?: string;
485
+ }
486
+ type LifecycleEventHandler = (event: LifecycleEvent) => void | Promise<void>;
487
+
488
+ type DetailSpan = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
489
+ type DisplayVariant = "text" | "badge" | "code-badge" | "code" | "copyable" | "link" | "url" | "email" | "phone" | "currency" | "percent" | "progress" | "star" | "star-rating" | "boolean" | "date" | "datetime" | "time" | "relative" | "image" | "avatar" | "color" | "color-swatches" | "icon" | "key-value" | "table" | "tags" | "badges" | "json";
490
+ interface DisplayFieldOptions {
491
+ label?: string;
492
+ hideLabel?: boolean;
493
+ tooltip?: string;
494
+ span?: DetailSpan;
495
+ display?: DisplayVariant;
496
+ format?: "currency" | "date" | "datetime" | "relative" | "number" | "percent" | string;
497
+ currency?: string;
498
+ badgeColors?: Record<string, string>;
499
+ keyLabel?: string;
500
+ valueLabel?: string;
501
+ emptyText?: string;
502
+ hideIfEmpty?: boolean;
503
+ /**
504
+ * When true, allows inline editing of this field in the Detail View
505
+ * via an interactive toggle.
506
+ */
507
+ editable?: boolean;
508
+ /**
509
+ * Visibility condition for this field item.
510
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
511
+ * When false or evaluating to falsy, this item is hidden in the Detail View.
512
+ *
513
+ * @example `visible: "doc.status == 'published'"`
514
+ * @example `visible: false`
515
+ */
516
+ visible?: string | boolean;
517
+ }
518
+ interface DetailField {
519
+ type: "field";
520
+ field: string;
521
+ options?: DisplayFieldOptions;
522
+ }
523
+ interface DetailSectionOptions {
524
+ icon?: string;
525
+ badge?: string;
526
+ badgeColor?: string;
527
+ description?: string;
528
+ span?: DetailSpan;
529
+ columns?: number;
530
+ collapsible?: boolean;
531
+ collapsedByDefault?: boolean;
532
+ /**
533
+ * Visibility condition for this section.
534
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
535
+ *
536
+ * @example `visible: "user.roles != null and 'admin' in user.roles"`
537
+ */
538
+ visible?: string | boolean;
539
+ }
540
+ interface DetailSection {
541
+ type: "section";
542
+ title: string;
543
+ items: DetailItem[];
544
+ options?: DetailSectionOptions;
545
+ }
546
+ interface DetailTabOptions {
547
+ icon?: string;
548
+ badge?: string;
549
+ badgeColor?: string;
550
+ /**
551
+ * Visibility condition for this individual tab.
552
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
553
+ */
554
+ visible?: string | boolean;
555
+ }
556
+ interface DetailTab {
557
+ type: "tab";
558
+ label: string;
559
+ items: DetailItem[];
560
+ options?: DetailTabOptions;
561
+ }
562
+ interface DetailTabsOptions {
563
+ span?: DetailSpan;
564
+ defaultTab?: string;
565
+ /**
566
+ * Visibility condition for this tab container.
567
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
568
+ */
569
+ visible?: string | boolean;
570
+ }
571
+ interface DetailTabs {
572
+ type: "tabs";
573
+ tabs: DetailTab[];
574
+ options?: DetailTabsOptions;
575
+ }
576
+ interface DetailGridOptions {
577
+ span?: DetailSpan;
578
+ /**
579
+ * Visibility condition for this grid container.
580
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
581
+ */
582
+ visible?: string | boolean;
583
+ }
584
+ interface DetailGrid {
585
+ type: "grid";
586
+ columns: number;
587
+ items: DetailItem[];
588
+ options?: DetailGridOptions;
589
+ }
590
+ interface DetailRepeatOptions {
591
+ layout?: "table" | "cards" | "list";
592
+ emptyText?: string;
593
+ span?: DetailSpan;
594
+ /**
595
+ * Visibility condition for this repeat container.
596
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
597
+ */
598
+ visible?: string | boolean;
599
+ /** Field name in each row to use as the card header title (e.g. 'title', 'key', 'name') */
600
+ useAsTitle?: string;
601
+ /** Field name in each row to use as the card header title (alias for useAsTitle) */
602
+ titleField?: string;
603
+ /** Static card title prefix or template (e.g. 'Category' or 'Card #{index}') */
604
+ title?: string;
605
+ /** Optional icon name for card header */
606
+ icon?: string;
607
+ /** Number of grid columns for cards layout (1, 2, 3, or 4; defaults to 3) */
608
+ columns?: 1 | 2 | 3 | 4;
609
+ }
610
+ interface DetailRepeat {
611
+ type: "repeat";
612
+ field: string;
613
+ items: DetailItem[];
614
+ options?: DetailRepeatOptions;
615
+ }
616
+ type ComputedHandler<TDoc = any> = (context: {
617
+ doc: TDoc;
618
+ user?: any;
619
+ db?: any;
620
+ }) => any | Promise<any>;
621
+ interface DetailComputedOptions<TDoc = any> {
622
+ id?: string;
623
+ expression?: string;
624
+ handler?: ComputedHandler<TDoc>;
625
+ span?: DetailSpan;
626
+ format?: string;
627
+ currency?: string;
628
+ /**
629
+ * Visibility condition for this computed card.
630
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
631
+ */
632
+ visible?: string | boolean;
633
+ }
634
+ interface DetailComputed<TDoc = any> {
635
+ type: "computed";
636
+ id?: string;
637
+ label: string;
638
+ expression?: string;
639
+ handler?: ComputedHandler<TDoc>;
640
+ options?: DetailComputedOptions<TDoc>;
641
+ }
642
+ interface DetailDividerOptions {
643
+ span?: DetailSpan;
644
+ /**
645
+ * Visibility condition for this divider.
646
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
647
+ */
648
+ visible?: string | boolean;
649
+ spacing?: "sm" | "md" | "lg" | "none";
650
+ }
651
+ interface DetailDivider {
652
+ type: "divider";
653
+ options?: DetailDividerOptions;
654
+ }
655
+ interface DetailTextOptions {
656
+ span?: DetailSpan;
657
+ /**
658
+ * Visibility condition for this text block.
659
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
660
+ */
661
+ visible?: string | boolean;
662
+ variant?: "body" | "heading" | "subheading" | "muted" | "caption" | "callout" | "info" | "warning";
663
+ className?: string;
664
+ }
665
+ interface DetailText {
666
+ type: "text";
667
+ content: string;
668
+ options?: DetailTextOptions;
669
+ }
670
+ interface DetailCustomOptions<TDoc = any> {
671
+ span?: DetailSpan;
672
+ /**
673
+ * Visibility condition for this custom component.
674
+ * Can be a boolean or a JEXL expression evaluated against `{ doc, user }`.
675
+ */
676
+ visible?: string | boolean;
677
+ props?: Record<string, any>;
678
+ render?: (context: {
679
+ doc: TDoc;
680
+ user?: any;
681
+ [key: string]: any;
682
+ }) => any;
683
+ }
684
+ interface DetailCustom<TDoc = any> {
685
+ type: "custom";
686
+ name: string;
687
+ options?: DetailCustomOptions<TDoc>;
688
+ }
689
+ type DetailItem<TDoc = any> = DetailSection | DetailTabs | DetailGrid | DetailField | DetailRepeat | DetailComputed<TDoc> | DetailDivider | DetailText | DetailCustom<TDoc> | string;
690
+ type DetailSchema<TDoc = any> = DetailItem<TDoc>[];
691
+
692
+ /**
693
+ * Configures account lockout behavior for an auth-enabled collection.
694
+ *
695
+ * This protects collection login endpoints from repeated password guessing by
696
+ * temporarily locking an account after too many failed attempts.
697
+ */
698
+ interface AuthConfig {
699
+ /**
700
+ * How many failed login attempts are allowed before the account is locked.
701
+ *
702
+ * Defaults to `5`. Set to `0` to disable Dyrected's built-in account lockout
703
+ * for this collection.
704
+ */
705
+ maxLoginAttempts?: number;
706
+ /**
707
+ * How long, in milliseconds, an account stays locked after reaching the
708
+ * failed-attempt limit.
709
+ *
710
+ * Defaults to `10 * 60 * 1000` (10 minutes).
711
+ */
712
+ lockTime?: number;
713
+ }
714
+ /**
715
+ * Use this contract when you want the exact shape of a collection config.
716
+ *
717
+ * Most collection work comes down to a small set of top-level options: giving
718
+ * the collection a stable slug, defining its fields, deciding how it should
719
+ * appear in the Admin UI, and choosing whether it also handles access, hooks,
720
+ * auth, uploads, workflows, or other optional behavior.
721
+ *
722
+ * Pass your document's TypeScript type as the generic parameter `TDoc` to get
723
+ * fully typed hooks and access functions.
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * interface Post {
728
+ * id: string
729
+ * title: string
730
+ * slug: string
731
+ * status: 'draft' | 'published'
732
+ * publishedAt?: string
733
+ * }
734
+ *
735
+ * export const Posts = defineCollection<Post>({
736
+ * slug: 'posts',
737
+ * hooks: {
738
+ * beforeChange: [({ data, operation }) => {
739
+ * // `data` is typed as Partial<Post>
740
+ * if (operation === 'create') return { ...data, status: 'draft' }
741
+ * return data
742
+ * }],
743
+ * afterChange: [({ doc, previousDoc }) => {
744
+ * // `doc` and `previousDoc` are typed as Post
745
+ * if (doc.status !== previousDoc?.status) notifySubscribers(doc)
746
+ * }],
747
+ * },
748
+ * fields: [...],
749
+ * })
750
+ * ```
751
+ *
752
+ * @see {@link https://dyrected.com/docs/model-content/configuration/collections Collections documentation}
753
+ * @template TDoc The TypeScript shape of a document in this collection.
754
+ * Defaults to `Record<string, unknown>` for untyped usage.
755
+ */
756
+ interface CollectionConfig<TDoc extends object = Record<string, unknown>> {
757
+ /**
758
+ * Unique identifier for this collection.
759
+ *
760
+ * Dyrected uses the slug for API routes, SDK calls, Admin URLs, and as the
761
+ * underlying database table or collection name. Treat it as part of the
762
+ * long-term data contract rather than a cosmetic label.
763
+ *
764
+ * Use kebab-case, for example `'blog-posts'`, `'team-members'`, or
765
+ * `'contact-submissions'`.
766
+ */
767
+ slug: string;
768
+ /**
769
+ * Restricts this collection to one specific site in a multi-tenant setup.
770
+ *
771
+ * Use this when the collection should belong to a single site rather than
772
+ * the whole installation. When set, only requests bearing a matching
773
+ * `X-Site-Id` header can access it.
774
+ */
775
+ siteId?: string;
776
+ /**
777
+ * If `true`, this collection is shared across all sites in a multi-tenant
778
+ * setup and accessible regardless of the `X-Site-Id` header.
779
+ *
780
+ * Use this for content that should stay common across sites, such as shared
781
+ * taxonomies, reusable assets, or centrally managed reference data.
782
+ */
783
+ shared?: boolean;
784
+ /**
785
+ * Human-readable names for documents in this collection, shown in the Admin UI.
786
+ *
787
+ * Use this when the slug is technical or when you want the dashboard to read
788
+ * more naturally. For example, `slug: 'people'` might use
789
+ * `labels: { singular: 'Person', plural: 'People' }`.
790
+ *
791
+ * @see {@link https://dyrected.com/docs/model-content/configuration/collections#labels Collections labels}
792
+ */
793
+ labels?: {
794
+ singular: string;
795
+ plural: string;
796
+ };
797
+ /**
798
+ * If `true` or an auth config object, this collection is an auth collection. It gains
799
+ * `POST /api/collections/:slug/login` and `POST /api/collections/:slug/logout`
800
+ * endpoints, and documents are expected to have a `password` field.
801
+ *
802
+ * Turn this on when each document should behave like an account that can log
803
+ * in, hold credentials, and participate in user flows. Typical examples are
804
+ * `users`, `admins`, `members`, or `customers`.
805
+ *
806
+ * Pass an object when you want to tune built-in account lockout behavior for
807
+ * repeated failed logins.
808
+ *
809
+ * @see {@link https://dyrected.com/docs/editor-experience/editor-accounts Authentication overview}
810
+ */
811
+ auth?: boolean | AuthConfig;
812
+ /**
813
+ * If `true` or a config object, this collection supports file uploads.
814
+ * Documents gain file-related fields (`url`, `filename`, `mimeType`, etc.)
815
+ * and the create endpoint accepts `multipart/form-data`.
816
+ *
817
+ * Turn this on when each document in the collection should represent a
818
+ * stored file, such as an image, PDF, video, or downloadable asset.
819
+ *
820
+ * @see {@link https://docs.dyrected.com/docs/self-hosted/model-content/media/overview Upload overview}
821
+ */
822
+ upload?: boolean | UploadConfig;
823
+ /**
824
+ * Field definitions that make up the document schema for this collection.
825
+ *
826
+ * This is the main schema contract for every document in the collection. It
827
+ * decides what editors can fill in, how data is validated, how records are
828
+ * stored, and what the API and SDK return.
829
+ *
830
+ * In practice, fields are where you model the actual content structure of the
831
+ * collection: simple values such as text and dates, relationships to other
832
+ * collections, nested objects and arrays, and flexible `blocks` fields for
833
+ * reusable page sections or long-form layouts.
834
+ *
835
+ * @see {@link https://dyrected.com/docs/model-content/fields/overview Fields overview}
836
+ * @see {@link https://dyrected.com/docs/model-content/fields/blocks Blocks and page sections}
837
+ */
838
+ fields: Field[];
839
+ /**
840
+ * If `true`, Dyrected automatically adds the built-in system fields
841
+ * `createdAt`, `updatedAt`, `createdBy`, and `updatedBy` to every document.
842
+ * Defaults to `true`.
843
+ */
844
+ timestamps?: boolean;
845
+ /**
846
+ * Initial documents to seed into this collection the first time it is
847
+ * fetched and found to be empty.
848
+ *
849
+ * Use this for starter records, demo content, or sensible defaults that
850
+ * should appear automatically before editors create anything themselves.
851
+ */
852
+ initialData?: Partial<TDoc>[];
853
+ /**
854
+ * If `true`, every create, update, and delete operation on this collection
855
+ * is logged to the `__audit` collection with before/after snapshots and the
856
+ * acting user's identity.
857
+ *
858
+ * Turn this on when you need accountability around changes, such as knowing
859
+ * who changed what, inspecting before-and-after state, or supporting
860
+ * compliance and operational review.
861
+ */
862
+ audit?: boolean;
863
+ /**
864
+ * Optional state-machine workflow for this collection. Workflow-enabled
865
+ * entries keep an editable working revision and an independent public
866
+ * snapshot, so editing published content never changes the live response.
867
+ *
868
+ * Use this when content moves through stages such as draft, review, and
869
+ * published, or when teams need an approval process before changes go live.
870
+ */
871
+ workflow?: WorkflowConfig<TDoc>;
872
+ /**
873
+ * If `true`, enables zero-config draft and publish functionality.
874
+ * Documents start as drafts, editors can save working drafts without affecting
875
+ * live content, and any authorized editor can publish or unpublish entries.
876
+ */
877
+ drafts?: boolean;
878
+ /**
879
+ * Collection-level access control.
880
+ *
881
+ * Each key is an operation; the value can be a function, a Jexl string, a
882
+ * boolean, or a named policy reference. Returning `true` allows access and
883
+ * `false` denies it. Returning a `where`-style object grants access only to
884
+ * matching documents.
885
+ *
886
+ * @example
887
+ * access: {
888
+ * read: () => true,
889
+ * create: ({ user }) => !!user,
890
+ * update: ({ user }) => user?.roles?.includes('editor') ?? false,
891
+ * delete: ({ user }) => user?.roles?.includes('admin') ?? false,
892
+ * }
893
+ *
894
+ * @see {@link https://docs.dyrected.com/docs/self-hosted/model-content/content-rules/access-control/overview Access control overview}
895
+ */
896
+ access?: {
897
+ read?: AccessRule<TDoc>;
898
+ create?: AccessRule<TDoc>;
899
+ update?: AccessRule<TDoc>;
900
+ delete?: AccessRule<TDoc>;
901
+ /**
902
+ * Controls who can read this collection's audit log (`GET /:slug/__audit`),
903
+ * for collections with `audit` enabled. Falls back to the `read` rule when
904
+ * omitted, so the audit trail is visible to whoever can read the documents.
905
+ * Set it explicitly to gate the audit log separately — for example, admins
906
+ * only, even on a collection anyone can read.
907
+ */
908
+ readAudit?: AccessRule<TDoc>;
909
+ };
910
+ /**
911
+ * Collection-level lifecycle hooks.
912
+ *
913
+ * Hooks run in the order they appear in the array. The return value of each
914
+ * hook is passed as the input to the next. Throwing inside any hook aborts
915
+ * the operation and returns a `500` error.
916
+ *
917
+ * See the Hooks reference for the full lifecycle diagram.
918
+ *
919
+ * @see {@link https://docs.dyrected.com/docs/self-hosted/model-content/content-rules/hooks Hooks overview}
920
+ * @see {@link https://docs.dyrected.com/docs/self-hosted/deployment-and-operations/server-runtime/hooks/collections Collection hooks}
921
+ */
922
+ hooks?: {
923
+ /**
924
+ * Runs before the database is queried. Return a modified `where` object
925
+ * to override the query filter.
926
+ */
927
+ beforeRead?: CollectionBeforeReadHookEntry[];
928
+ /**
929
+ * Runs after documents are fetched. Return a modified doc to change what
930
+ * the client receives. Runs on every document in a list response.
931
+ */
932
+ afterRead?: CollectionAfterReadHookEntry<TDoc>[];
933
+ /**
934
+ * Runs before create or update. Return modified data to change what is
935
+ * written to the database. Throw to abort the write entirely.
936
+ */
937
+ beforeChange?: CollectionBeforeChangeHookEntry<TDoc>[];
938
+ /**
939
+ * Runs after create or update is committed. For side-effects only:
940
+ * webhooks, cache busting, and notifications. Return value is ignored.
941
+ *
942
+ * Errors are isolated: caught, logged, and discarded so a failing
943
+ * side-effect never turns a successful write into an HTTP 500.
944
+ * See `CollectionAfterChangeHook` for await-vs-fire-and-forget guidance.
945
+ */
946
+ afterChange?: CollectionAfterChangeHook<TDoc>[];
947
+ /** Runs before a document is deleted. Throw to cancel the deletion. */
948
+ beforeDelete?: CollectionBeforeDeleteHook<TDoc>[];
949
+ /**
950
+ * Runs after a document has been deleted. For cleanup side-effects only.
951
+ *
952
+ * Errors are isolated: caught, logged, and discarded. The deletion is
953
+ * already committed and will not be undone.
954
+ */
955
+ afterDelete?: CollectionAfterDeleteHook<TDoc>[];
956
+ };
957
+ /**
958
+ * Admin UI configuration for this collection.
959
+ *
960
+ * @see {@link https://dyrected.com/docs/model-content/configuration/collections#admin-options Admin options}
961
+ */
962
+ admin?: {
963
+ /**
964
+ * Lucide icon displayed beside this collection in the Admin sidebar.
965
+ * Uses Lucide component names, e.g. `'Newspaper'` or `'ShoppingBag'`.
966
+ */
967
+ icon?: AdminIconName;
968
+ /** Custom component slots for this collection's list view. */
969
+ components?: CollectionListComponentSlots;
970
+ /**
971
+ * The field name used as the document's display title in the Admin list
972
+ * view and breadcrumbs. Defaults to `'title'` if the field exists.
973
+ */
974
+ useAsTitle?: string;
975
+ /**
976
+ * Field names to show as columns in the Admin list view.
977
+ * Defaults to a sensible set of the first few non-structural fields.
978
+ */
979
+ defaultColumns?: string[];
980
+ /** Short helper copy rendered under the collection title in the Admin list view. */
981
+ description?: string;
982
+ /**
983
+ * Field names included in backend free-text search for this collection.
984
+ * When omitted, Dyrected infers a conservative default from common text-like fields.
985
+ */
986
+ searchableFields?: string[];
987
+ /**
988
+ * Groups this collection under a named section in the Admin sidebar.
989
+ * Collections with the same `group` are visually grouped together.
990
+ */
991
+ group?: string;
992
+ /** If `true`, this collection is not shown in the Admin UI sidebar. */
993
+ hidden?: boolean;
994
+ /** If `false`, disables the filter UI entirely for this collection. Defaults to `true`. */
995
+ filterable?: boolean;
996
+ /**
997
+ * Enables draft autosave in the Admin editor for workflow-enabled
998
+ * collections. Defaults to `true` when the collection uses `workflow` or
999
+ * `drafts: true`.
1000
+ */
1001
+ autosave?: boolean;
1002
+ /**
1003
+ * Debounce duration in milliseconds for Admin draft autosave.
1004
+ * Defaults to `1500`.
1005
+ */
1006
+ autosaveDelayMs?: number;
1007
+ /**
1008
+ * URL to open in the Live Preview pane when editing a document.
1009
+ *
1010
+ * Pass a Jexl string to keep the config serializable, for example
1011
+ * `'slug == "home" ? "/" : "/" + slug'`. This is usually the best
1012
+ * default, especially when the schema needs to stay portable across
1013
+ * environments such as Dyrected Cloud.
1014
+ *
1015
+ * Pass a function when you need custom runtime logic in a self-hosted
1016
+ * project.
1017
+ *
1018
+ * @example
1019
+ * previewUrl: 'slug == "home" ? "/" : "/" + slug'
1020
+ *
1021
+ * @example
1022
+ * previewUrl: (doc) => `/blog/${doc.slug}`
1023
+ */
1024
+ previewUrl?: string | ((doc: TDoc, opts: {
1025
+ locale?: string;
1026
+ }) => string | null);
1027
+ /**
1028
+ * How the Live Preview pane communicates with the frontend.
1029
+ * - `postMessage` sends a `postMessage` with the current doc data.
1030
+ * - `token` passes a short-lived preview token as a query parameter.
1031
+ */
1032
+ previewMode?: "postMessage" | "token";
1033
+ /**
1034
+ * Frontend URL pattern for this collection, used by `url` fields to
1035
+ * resolve internal links. Use `{fieldName}` placeholders.
1036
+ *
1037
+ * This is a plain route pattern string, not a Jexl expression.
1038
+ *
1039
+ * @example
1040
+ * urlPattern: '/blog/{slug}' // /blog/my-post
1041
+ * urlPattern: '/{slug}' // /about
1042
+ */
1043
+ urlPattern?: string;
1044
+ };
1045
+ /**
1046
+ * Custom detail view layout configuration for the Admin UI.
1047
+ */
1048
+ detail?: DetailSchema<TDoc> | false;
1049
+ }
1050
+ /**
1051
+ * Defines a Dyrected global — a singleton document without pagination or IDs.
1052
+ *
1053
+ * Globals are ideal for site-wide settings, feature flags, or any data where
1054
+ * there is always exactly one record, such as `site-settings`, `navigation`,
1055
+ * or `theme`.
1056
+ *
1057
+ * Pass your document's TypeScript type as the generic parameter `TDoc` to get
1058
+ * fully typed hooks.
1059
+ *
1060
+ * @example
1061
+ * ```ts
1062
+ * interface SiteSettings {
1063
+ * siteName: string
1064
+ * tagline: string
1065
+ * maintenanceMode: boolean
1066
+ * }
1067
+ *
1068
+ * export const Settings = defineGlobal<SiteSettings>({
1069
+ * slug: 'site-settings',
1070
+ * hooks: {
1071
+ * afterChange: [({ doc }) => {
1072
+ * // `doc` is typed as SiteSettings
1073
+ * if (doc.maintenanceMode) alertOnCall()
1074
+ * }],
1075
+ * },
1076
+ * fields: [...],
1077
+ * })
1078
+ * ```
1079
+ *
1080
+ * @template TDoc The TypeScript shape of this global's document.
1081
+ */
1082
+ interface GlobalConfig<TDoc extends object = Record<string, unknown>> {
1083
+ /**
1084
+ * Unique identifier for this global.
1085
+ * Used as the URL segment (`/api/globals/:slug`) and the storage key.
1086
+ */
1087
+ slug: string;
1088
+ /** Restricts this global to a specific site in a multi-tenant deployment. */
1089
+ siteId?: string;
1090
+ /**
1091
+ * If `true`, this global is shared across all sites in a multi-tenant
1092
+ * deployment.
1093
+ */
1094
+ shared?: boolean;
1095
+ /** Human-readable label shown in the Admin UI sidebar. */
1096
+ label?: string;
1097
+ /** Field definitions for this global's document schema. */
1098
+ fields: Field[];
1099
+ /** Access control for reading and updating this global. */
1100
+ access?: {
1101
+ read?: AccessRule<TDoc>;
1102
+ update?: AccessRule<TDoc>;
1103
+ };
1104
+ /**
1105
+ * Global-level lifecycle hooks.
1106
+ * Globals support `beforeRead`, `afterRead`, `beforeChange`, and `afterChange`.
1107
+ * There are no delete hooks since globals cannot be deleted.
1108
+ */
1109
+ hooks?: {
1110
+ beforeRead?: GlobalBeforeReadHookEntry[];
1111
+ afterRead?: GlobalAfterReadHookEntry<TDoc>[];
1112
+ beforeChange?: GlobalBeforeChangeHookEntry<TDoc>[];
1113
+ afterChange?: GlobalAfterChangeHook<TDoc>[];
1114
+ };
1115
+ /** Admin UI configuration for this global. */
1116
+ admin?: {
1117
+ /**
1118
+ * Lucide icon displayed beside this global in the Admin sidebar.
1119
+ * Uses Lucide component names, e.g. `'Settings2'` or `'Palette'`.
1120
+ */
1121
+ icon?: AdminIconName;
1122
+ /** Groups this global under a named section in the Admin sidebar. */
1123
+ group?: string;
1124
+ /** If `true`, this global is not shown in the Admin UI sidebar. */
1125
+ hidden?: boolean;
1126
+ };
1127
+ /**
1128
+ * Initial data to seed this global with the first time it is fetched and
1129
+ * found to be empty.
1130
+ */
1131
+ initialData?: Partial<TDoc>;
1132
+ /**
1133
+ * Custom detail view layout configuration for the Admin UI.
1134
+ */
1135
+ detail?: DetailSchema<TDoc> | false;
1136
+ }
1137
+
1138
+ /**
1139
+ * The set of type-cast operations supported during aggregation.
1140
+ *
1141
+ * Each adapter maps these to its own native conversion primitive
1142
+ * (e.g. `$toDouble` in MongoDB, `CAST(x AS REAL)` in SQLite).
1143
+ * The public API is always database-independent.
1144
+ */
1145
+ type AggregateCastType = "number" | "integer" | "float" | "string" | "boolean" | "date";
1146
+ /**
1147
+ * A `count` aggregate operation.
1148
+ *
1149
+ * - `count: "*"` — count every document matched by `where`.
1150
+ * - `cast` is intentionally absent: counting is document-level,
1151
+ * not field-value-level, so type conversion has no meaning here.
1152
+ */
1153
+ interface CountOperation {
1154
+ count: "*";
1155
+ where?: Record<string, unknown>;
1156
+ }
1157
+ /**
1158
+ * A `sum`, `avg`, `min`, or `max` aggregate operation on a named field.
1159
+ *
1160
+ * - `cast` converts stored values before the aggregation runs.
1161
+ * Invalid values (e.g. `"unknown"` cast to `number`) become `null`
1162
+ * and are ignored by `sum`, `avg`, `min`, and `max`.
1163
+ */
1164
+ interface NumericOperation {
1165
+ sum?: string;
1166
+ avg?: string;
1167
+ min?: string;
1168
+ max?: string;
1169
+ cast?: AggregateCastType;
1170
+ where?: Record<string, unknown>;
1171
+ }
1172
+ /**
1173
+ * A single named aggregate request — either a count or a numeric operation.
1174
+ */
1175
+ type AggregateOperation = CountOperation | NumericOperation;
1176
+ /**
1177
+ * The map of named aggregate operations sent in a single aggregate request.
1178
+ *
1179
+ * @example
1180
+ * ```ts
1181
+ * {
1182
+ * totalSubmitted: { count: "*" },
1183
+ * totalAsoebiYards: { sum: "asoebiYards", cast: "number", where: { wantsAsoebi: { equals: true } } },
1184
+ * }
1185
+ * ```
1186
+ */
1187
+ type AggregateInput = Record<string, AggregateOperation>;
1188
+ /**
1189
+ * The arguments passed to `DatabaseAdapter.aggregate`.
1190
+ */
1191
+ interface AggregateArgs {
1192
+ /** Collection slug. */
1193
+ collection: string;
1194
+ /** Named aggregate operations to compute. */
1195
+ aggregates: AggregateInput;
1196
+ }
1197
+ /**
1198
+ * The result returned by `DatabaseAdapter.aggregate`.
1199
+ *
1200
+ * Every named key maps to a `number | null`.
1201
+ * `null` is returned when no documents matched the aggregate's `where`
1202
+ * (applicable to `sum`, `avg`, `min`, `max`).
1203
+ * `count` also returns `null` only if the database errors — it normally
1204
+ * returns `0` for an empty match set.
1205
+ */
1206
+ type AggregateResult = Record<string, number | null>;
1207
+ /**
1208
+ * Derives the typed result shape from an `AggregateInput`.
1209
+ * Every named key maps to `number | null`.
1210
+ *
1211
+ * @example
1212
+ * ```ts
1213
+ * type Input = { totalSubmitted: CountOperation; totalYards: NumericOperation };
1214
+ * type Result = InferAggregateResult<Input>;
1215
+ * // => { totalSubmitted: number | null; totalYards: number | null }
1216
+ * ```
1217
+ */
1218
+ type InferAggregateResult<T extends AggregateInput> = {
1219
+ [K in keyof T]: number | null;
1220
+ };
1221
+
1222
+ /**
1223
+ * The interface every database adapter must implement.
1224
+ *
1225
+ * Dyrected ships adapters for PostgreSQL, MySQL, SQLite, and MongoDB.
1226
+ * Implement this interface to connect any other database.
1227
+ */
1228
+ interface DatabaseAdapter {
1229
+ /** Find a paginated list of documents in a collection. */
1230
+ find(args: {
1231
+ collection: string;
1232
+ where?: Record<string, unknown>;
1233
+ limit?: number;
1234
+ page?: number;
1235
+ sort?: string;
1236
+ /**
1237
+ * The collection's field definitions. Optional, so simple adapters can
1238
+ * ignore it; SQL adapters use it to sort numeric fields by magnitude.
1239
+ */
1240
+ fields?: Field[];
1241
+ }): Promise<PaginatedResult>;
1242
+ /** Find a single document by its ID. Returns `null` if not found. */
1243
+ findOne(args: {
1244
+ collection: string;
1245
+ id: string;
1246
+ }): Promise<BaseDocument | null>;
1247
+ /** Insert a new document and return it with its generated `id`. */
1248
+ create(args: {
1249
+ collection: string;
1250
+ data: Record<string, unknown>;
1251
+ }): Promise<BaseDocument>;
1252
+ /** Update a document by ID and return the updated document. */
1253
+ update(args: {
1254
+ collection: string;
1255
+ id: string;
1256
+ data: Record<string, unknown>;
1257
+ }): Promise<BaseDocument>;
1258
+ /** Delete a document by ID. Return value is intentionally untyped — callers do not use it. */
1259
+ delete(args: {
1260
+ collection: string;
1261
+ id: string;
1262
+ }): Promise<unknown>;
1263
+ /**
1264
+ * Compute aggregate statistics across a collection without returning documents.
1265
+ *
1266
+ * Each named key in `args.aggregates` maps to a `count`, `sum`, `avg`, `min`,
1267
+ * or `max` operation, with optional `where` filtering and `cast` conversion.
1268
+ * The result is a flat object of the same named keys mapped to `number | null`.
1269
+ */
1270
+ aggregate(args: AggregateArgs): Promise<AggregateResult>;
1271
+ /** Fetch the singleton document for a global. Returns an empty object if not yet initialised. */
1272
+ getGlobal(args: {
1273
+ slug: string;
1274
+ }): Promise<Record<string, unknown>>;
1275
+ /** Create or replace the singleton document for a global. */
1276
+ updateGlobal(args: {
1277
+ slug: string;
1278
+ data: Record<string, unknown>;
1279
+ }): Promise<Record<string, unknown>>;
1280
+ /**
1281
+ * Sync the database schema with the current collection and global configs.
1282
+ * Called on startup to create tables/collections that don't exist yet.
1283
+ * Not all adapters implement this (e.g. MongoDB is schema-less).
1284
+ */
1285
+ sync?(collections: CollectionConfig[], globals: GlobalConfig[]): Promise<void>;
1286
+ /**
1287
+ * Execute a raw SQL query or database command.
1288
+ * Optional — not all adapters support raw access.
1289
+ */
1290
+ execute?(query: string, params?: unknown[]): Promise<unknown>;
1291
+ /**
1292
+ * Run all adapter operations in `callback` as one atomic transaction.
1293
+ * Shipped adapters implement this; workflow transitions require it.
1294
+ */
1295
+ transaction?<T>(callback: (db: DatabaseAdapter) => Promise<T>): Promise<T>;
1296
+ }
1297
+ /**
1298
+ * Read-only view of the database adapter. Exposes only `find`, `findOne`,
1299
+ * `getGlobal`, and `aggregate` — no write operations.
1300
+ *
1301
+ * Passed to `beforeChange`, `beforeDelete`, `beforeRead`, `afterRead`,
1302
+ * and field-level hooks. Write operations are available in `afterChange`
1303
+ * and `afterDelete` hooks where the full {@link DatabaseAdapter} is provided.
1304
+ */
1305
+ type ReadonlyDatabaseAdapter = Pick<DatabaseAdapter, "find" | "findOne" | "getGlobal" | "aggregate">;
1306
+ /**
1307
+ * The interface every storage adapter must implement.
1308
+ *
1309
+ * Dyrected ships adapters for local disk, AWS S3 and other S3-compatible
1310
+ * services through the S3 adapter, Cloudinary, and Backblaze B2. Implement
1311
+ * this interface to use any other storage provider.
1312
+ */
1313
+ interface StorageAdapter {
1314
+ /**
1315
+ * Upload a file and return its metadata (URL, dimensions, etc.).
1316
+ * The `prefix` is a path prefix used for multi-tenant setups.
1317
+ */
1318
+ upload(args: {
1319
+ filename: string;
1320
+ buffer: Uint8Array;
1321
+ mimeType: string;
1322
+ prefix?: string;
1323
+ }): Promise<FileData>;
1324
+ /** Delete a file by its stored filename. */
1325
+ delete(args: {
1326
+ filename: string;
1327
+ }): Promise<void>;
1328
+ /** Return the public URL for a stored file. */
1329
+ getURL(args: {
1330
+ filename: string;
1331
+ }): string;
1332
+ /**
1333
+ * Retrieve the file's raw bytes and MIME type for serving via the API.
1334
+ * Only needed by adapters that serve files through the Dyrected API
1335
+ * (e.g. `LocalStorage`). Cloud adapters return `null` here and rely on
1336
+ * direct CDN URLs instead.
1337
+ */
1338
+ resolve?(args: {
1339
+ filename: string;
1340
+ }): Promise<{
1341
+ buffer: Uint8Array;
1342
+ mimeType: string;
1343
+ } | null>;
1344
+ }
1345
+ /**
1346
+ * Processes uploaded images — generates metadata (dimensions, BlurHash) and
1347
+ * produces resized variants defined in `UploadConfig.imageSizes`.
1348
+ *
1349
+ * @example
1350
+ * import { SharpImageService } from '@dyrected/image-sharp'
1351
+ * defineConfig({ image: new SharpImageService(), ... })
1352
+ */
1353
+ interface ImageService {
1354
+ process(args: {
1355
+ buffer: Uint8Array;
1356
+ mimeType: string;
1357
+ config?: boolean | UploadConfig;
1358
+ focalPoint?: {
1359
+ x: number;
1360
+ y: number;
1361
+ };
1362
+ }): Promise<{
1363
+ metadata: {
1364
+ width?: number;
1365
+ height?: number;
1366
+ /** Base64-encoded BlurHash for progressive loading. */
1367
+ blurhash?: string;
1368
+ };
1369
+ /** Generated image sizes keyed by their `name`. */
1370
+ sizes?: Record<string, {
1371
+ buffer: Uint8Array;
1372
+ width: number;
1373
+ height: number;
1374
+ filename: string;
1375
+ }>;
1376
+ }>;
1377
+ }
1378
+
1379
+ type FieldType = "text" | "textarea" | "richText" | "number" | "boolean" | "date" | "datetime" | "time" | "select" | "multiSelect" | "radio" | "relationship" | "array" | "object" | "json" | "blocks" | "image" | "email" | "url" | "icon" | "join" | "row";
1380
+ interface DynamicOptionsResolverArgs {
1381
+ /** Database adapter available to server-side option resolvers. */
1382
+ db?: DatabaseAdapter;
1383
+ /** Authenticated user making the request, if any. */
1384
+ user?: AuthenticatedUser;
1385
+ /** Current HTTP request context, including query parameters. */
1386
+ req: HookRequestContext;
1387
+ }
1388
+ type DynamicOptionsResolver = (args: DynamicOptionsResolverArgs) => Promise<DynamicOptionItem[]> | DynamicOptionItem[];
1389
+ interface DynamicOptionsConfig {
1390
+ /** Resolver function executed on the server to produce option items. */
1391
+ resolve: DynamicOptionsResolver;
1392
+ /** Cache duration in seconds for identical resolver calls. */
1393
+ cacheTTL?: number;
1394
+ }
1395
+ type DynamicOptionItem = string | {
1396
+ label: string;
1397
+ value: unknown;
1398
+ };
1399
+ interface Block {
1400
+ /** Stable identifier stored in each block row as `blockType`. */
1401
+ slug: string;
1402
+ /** Human-readable labels shown in the Admin block picker. */
1403
+ labels?: {
1404
+ /** Singular label, for example `Hero`. */
1405
+ singular: string;
1406
+ /** Plural label, for example `Heroes`. */
1407
+ plural: string;
1408
+ };
1409
+ /**
1410
+ * Lucide icon name shown on the block card and in the block library.
1411
+ * Falls back to a generic layout icon when omitted.
1412
+ */
1413
+ icon?: AdminIconName;
1414
+ /** Short one-line summary shown under the block name (block card subtitle). */
1415
+ description?: string;
1416
+ /**
1417
+ * Presentation variants for this block. All variants share the same `fields`;
1418
+ * only the rendered layout differs. The chosen variant is stored on each block
1419
+ * row under the reserved `variant` key and passed to the render component as a
1420
+ * `variant` prop. Switching variant preserves the author's content.
1421
+ */
1422
+ variants?: BlockVariant[];
1423
+ /** Fields that make up this block's payload. */
1424
+ fields: Field[];
1425
+ }
1426
+ /**
1427
+ * A single presentation variant of a {@link Block}. Variants are a layout choice
1428
+ * over a shared field set — for example a Hero rendered "centered" vs "split".
1429
+ */
1430
+ interface BlockVariant {
1431
+ /** Stable identifier stored on the block row as `variant`. */
1432
+ slug: string;
1433
+ /** Human-readable label shown in the variant switcher. Defaults to `slug`. */
1434
+ label?: string;
1435
+ /** Lucide icon name shown beside the variant label. */
1436
+ icon?: AdminIconName;
1437
+ /** Short one-line summary of what this variant looks like. */
1438
+ description?: string;
1439
+ }
1440
+ interface FieldBase {
1441
+ /** Stored key for this field. Omit only for layout-only fields such as `row` or `join`. */
1442
+ name?: string;
1443
+ /** Human-readable label shown in the Admin UI. */
1444
+ label?: string;
1445
+ /** Whether the field must have a value when saving. */
1446
+ required?: boolean;
1447
+ /** Whether values for this field must be unique across the collection. */
1448
+ unique?: boolean;
1449
+ /** Default value used when a new document omits this field. */
1450
+ defaultValue?: unknown;
1451
+ /** Static or dynamic option source for supported selection fields. */
1452
+ options?: string[] | {
1453
+ label: string;
1454
+ value: unknown;
1455
+ }[] | DynamicOptionsResolver | DynamicOptionsConfig;
1456
+ /** Target collection slug for `relationship` fields. */
1457
+ relationTo?: string;
1458
+ /** Whether the field stores multiple values instead of one. */
1459
+ hasMany?: boolean;
1460
+ /** Child fields for `object` and `array` field types. */
1461
+ fields?: Field[];
1462
+ /** Allowed block definitions for a `blocks` field. */
1463
+ blocks?: Block[];
1464
+ /**
1465
+ * Shared block slugs pulled from the root `defineConfig({ blocks: [...] })`
1466
+ * registry for a `blocks` field.
1467
+ *
1468
+ * Use this when the same block types should be reused across multiple fields
1469
+ * without inlining the full block schema into each field definition.
1470
+ */
1471
+ blockReferences?: string[];
1472
+ /** Target collection slug for `join` fields. */
1473
+ collection?: string;
1474
+ /** Back-reference field name on the joined collection. */
1475
+ on?: string;
1476
+ /** Maximum number of joined documents returned by a `join` field. */
1477
+ limit?: number;
1478
+ /** Field-level read, create, and update access rules. Supports functions, Jexl strings, booleans, and named policies. */
1479
+ access?: {
1480
+ /** Controls whether this field is returned in API responses. */
1481
+ read?: AccessRule;
1482
+ /** Controls whether this field may be set when creating a document. Falls back to `update` when omitted. */
1483
+ create?: AccessRule;
1484
+ /** Controls whether incoming writes may change this field on update. */
1485
+ update?: AccessRule;
1486
+ };
1487
+ /** Admin-only presentation options for this field. */
1488
+ admin?: BaseFieldAdmin;
1489
+ /** Previous storage key this field falls back to. When the field has no value, its value is read from the old key at read time, then rewritten under the new key on the next save. */
1490
+ renameTo?: string;
1491
+ /** Whether SQL adapters should promote this field into a first-class column. */
1492
+ promoted?: boolean;
1493
+ }
1494
+ interface BaseFieldAdmin {
1495
+ /** Placeholder text shown when the input has no value. */
1496
+ placeholder?: string;
1497
+ /** For nested object-like fields, choose which child field should summarize the value in admin surfaces. */
1498
+ useAsTitle?: string;
1499
+ /** Custom component key registered in the Admin UI. */
1500
+ component?: string;
1501
+ /** Help text rendered below the field. */
1502
+ description?: string;
1503
+ /** Hides the field from the Admin form without deleting stored data. */
1504
+ hidden?: boolean;
1505
+ /** Excludes the field from Admin list filtering. */
1506
+ filterable?: boolean;
1507
+ /** Renders the field as non-editable in the Admin UI. */
1508
+ readOnly?: boolean;
1509
+ /** Hides the field's label in the Admin form (e.g. single-field array rows where the label is redundant). */
1510
+ hideLabel?: boolean;
1511
+ /** Reactive condition controlling whether the field is visible in the Admin UI. */
1512
+ condition?: ((data: Record<string, unknown>, siblingData: Record<string, unknown>) => boolean) | string;
1513
+ /** Tab name used when the edit form is rendered as tabs. */
1514
+ tab?: string;
1515
+ /** CSS width hint used when the field appears inside a `row`. */
1516
+ width?: string;
1517
+ }
1518
+ interface FieldBeforeChangeHookArgs<TValue = unknown, TDoc extends object = Record<string, unknown>> {
1519
+ /** Current field value after previous hooks in the chain. */
1520
+ value: TValue;
1521
+ /** Existing stored document before the write, if this is an update. */
1522
+ originalDoc?: TDoc;
1523
+ /** Full incoming payload being written. */
1524
+ data: Record<string, unknown>;
1525
+ /** Authenticated user performing the write, if any. */
1526
+ user?: AuthenticatedUser;
1527
+ /** Read-only database adapter for related lookups. */
1528
+ db: ReadonlyDatabaseAdapter;
1529
+ }
1530
+ type FieldBeforeChangeHook<TValue = unknown, TDoc extends object = Record<string, unknown>> = (args: FieldBeforeChangeHookArgs<TValue, TDoc>) => unknown;
1531
+ interface FieldAfterReadHookArgs<TValue = unknown, TDoc extends object = Record<string, unknown>> {
1532
+ /** Raw stored field value before this hook transforms it. */
1533
+ value: TValue;
1534
+ /** Full document currently being returned to the caller. */
1535
+ doc: TDoc;
1536
+ /** Authenticated user requesting the document, if any. */
1537
+ user?: AuthenticatedUser;
1538
+ /** Read-only database adapter for related lookups. */
1539
+ db: ReadonlyDatabaseAdapter;
1540
+ }
1541
+ type FieldAfterReadHook<TValue = unknown, TDoc extends object = Record<string, unknown>> = (args: FieldAfterReadHookArgs<TValue, TDoc>) => unknown;
1542
+ type FieldHooks<TValue> = {
1543
+ hooks?: {
1544
+ beforeChange?: Array<FieldBeforeChangeHook<TValue> | DeclarativeHookExpression>;
1545
+ afterRead?: Array<FieldAfterReadHook<TValue>>;
1546
+ };
1547
+ };
1548
+ interface FieldAdminOnChangeHookArgs<TValue = unknown> {
1549
+ /** Current field value in the form state. */
1550
+ value: TValue;
1551
+ /** Current values for sibling fields at the same nesting level. */
1552
+ siblingData: Record<string, unknown>;
1553
+ /** Current values for the entire form. */
1554
+ data: Record<string, unknown>;
1555
+ /** Imperative setter for async or derived updates. */
1556
+ setValue: (value: unknown) => void;
1557
+ }
1558
+ type FieldAdminOnChangeHook<TValue = unknown> = (args: FieldAdminOnChangeHookArgs<TValue>) => unknown;
1559
+ type FieldAdminHooks<TValue> = {
1560
+ admin?: {
1561
+ hooks?: {
1562
+ onChange?: FieldAdminOnChangeHook<TValue> | DeclarativeHookExpression;
1563
+ };
1564
+ };
1565
+ };
1566
+ interface FieldAdminOptionsHookArgs {
1567
+ /** Current values for sibling fields at the same nesting level. */
1568
+ siblingData: Record<string, unknown>;
1569
+ /** Current values for the entire form. */
1570
+ data: Record<string, unknown>;
1571
+ }
1572
+ type FieldAdminOptionsHookResult = Array<string | {
1573
+ label: string;
1574
+ value: unknown;
1575
+ }>;
1576
+ type FieldAdminOptionsHook = (args: FieldAdminOptionsHookArgs) => FieldAdminOptionsHookResult | Promise<FieldAdminOptionsHookResult>;
1577
+ type TypedField<TType extends FieldType, TValue, TAdminExtra = Record<never, never>> = Omit<FieldBase, "admin"> & {
1578
+ type: TType;
1579
+ admin?: BaseFieldAdmin & TAdminExtra;
1580
+ } & FieldHooks<TValue> & FieldAdminHooks<TValue>;
1581
+ /**
1582
+ * A semantic color for a badge or status pill in the Admin UI. The Admin panel
1583
+ * maps each tone to a themed color, so you pick meaning (`success`, `danger`)
1584
+ * rather than a raw color and it stays consistent in light and dark mode.
1585
+ */
1586
+ type DisplayTone = "neutral" | "primary" | "success" | "warning" | "danger" | "info";
1587
+ /**
1588
+ * How a `select`, `radio`, or `multiSelect` value is presented in read-only
1589
+ * Admin surfaces (list cells). Renders the chosen option as a colored badge —
1590
+ * ideal for statuses like `draft`/`published`. Display only and
1591
+ * JSON-serializable so it round-trips through Dyrected Cloud.
1592
+ *
1593
+ * Pass the shorthand `"badge"` for neutral badges, or an object to color and
1594
+ * relabel each value.
1595
+ */
1596
+ type OptionFormat =
1597
+ /** Shorthand for `{ type: "badge" }` — every value renders as a neutral badge. */
1598
+ "badge" | {
1599
+ type: "badge";
1600
+ /** Maps an option value to a color tone. Values not listed use `defaultTone`. */
1601
+ tones?: Record<string, DisplayTone>;
1602
+ /** Overrides the displayed text per option value. Falls back to the option's label. */
1603
+ labels?: Record<string, string>;
1604
+ /** Tone for values missing from `tones`. Defaults to `"neutral"`. */
1605
+ defaultTone?: DisplayTone;
1606
+ };
1607
+ /**
1608
+ * How a `boolean` value is presented in read-only Admin surfaces. Replaces the
1609
+ * default `Yes`/`No` badge with your own labels and tones — for example
1610
+ * `Active`/`Inactive` or `In stock`/`Sold out`. Display only.
1611
+ */
1612
+ type BooleanFormat = {
1613
+ type: "boolean";
1614
+ /** Presentation for a `true` value. */
1615
+ true?: {
1616
+ label?: string;
1617
+ tone?: DisplayTone;
1618
+ };
1619
+ /** Presentation for a `false` value. */
1620
+ false?: {
1621
+ label?: string;
1622
+ tone?: DisplayTone;
1623
+ };
1624
+ };
1625
+ /**
1626
+ * How a `text` or `textarea` value is presented in read-only Admin surfaces.
1627
+ * Display only — the stored string is unchanged. Pass a shorthand string for the
1628
+ * simple transforms, or an object for `truncate` and `mask`.
1629
+ */
1630
+ type TextFormat =
1631
+ /** Shorthand for the matching object form. */
1632
+ "uppercase" | "lowercase" | "capitalize" | "code"
1633
+ /** Change the letter case for display. */
1634
+ | {
1635
+ type: "uppercase" | "lowercase" | "capitalize";
1636
+ }
1637
+ /** Render in a monospace pill — good for IDs, SKUs, and short codes. */
1638
+ | {
1639
+ type: "code";
1640
+ }
1641
+ /** Cut the text to `length` characters with a trailing ellipsis. */
1642
+ | {
1643
+ type: "truncate";
1644
+ length: number;
1645
+ }
1646
+ /**
1647
+ * Hide all but the last few characters — for tokens, keys, or reference
1648
+ * numbers you don't want fully visible in a list.
1649
+ */
1650
+ | {
1651
+ type: "mask";
1652
+ /** How many trailing characters stay visible. Defaults to `4`. */
1653
+ reveal?: number;
1654
+ /** Character used for the hidden portion. Defaults to `"•"`. */
1655
+ character?: string;
1656
+ };
1657
+ /**
1658
+ * How a `url` or `email` value is presented in read-only Admin surfaces.
1659
+ * Renders the value as a clickable link (a `mailto:` link for `email`) instead
1660
+ * of plain text. Display only.
1661
+ */
1662
+ type LinkFormat =
1663
+ /** Shorthand for `{ type: "link" }`. */
1664
+ "link" | {
1665
+ type: "link";
1666
+ /** Open the link in a new tab. Defaults to `true`. */
1667
+ newTab?: boolean;
1668
+ };
1669
+ /**
1670
+ * How a `json` value is presented in read-only Admin surfaces. Display only.
1671
+ */
1672
+ type JsonFormat =
1673
+ /** Shorthand for the matching object form. */
1674
+ "summary" | "code"
1675
+ /** A compact key count, e.g. `{ 3 keys }`. */
1676
+ | {
1677
+ type: "summary";
1678
+ }
1679
+ /** A truncated inline monospace preview of the raw JSON. */
1680
+ | {
1681
+ type: "code";
1682
+ };
1683
+ type BooleanFieldAdmin = {
1684
+ /** Boolean presentation style. */
1685
+ layout?: "checkbox" | "switch";
1686
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1687
+ format?: BooleanFormat;
1688
+ };
1689
+ type SelectFieldAdmin = {
1690
+ /** Select presentation style. */
1691
+ layout?: "radio" | "select";
1692
+ /** Radio orientation when `layout: 'radio'` is used. */
1693
+ direction?: "horizontal" | "vertical";
1694
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1695
+ format?: OptionFormat;
1696
+ hooks?: {
1697
+ /** Client-side option recalculation for dependent dropdowns or radios. */
1698
+ options?: FieldAdminOptionsHook;
1699
+ };
1700
+ };
1701
+ type RadioFieldAdmin = {
1702
+ /** Radio group orientation. */
1703
+ direction?: "horizontal" | "vertical";
1704
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1705
+ format?: OptionFormat;
1706
+ hooks?: {
1707
+ /** Client-side option recalculation for dependent radio groups. */
1708
+ options?: FieldAdminOptionsHook;
1709
+ };
1710
+ };
1711
+ type MultiSelectFieldAdmin = {
1712
+ /** How each selected value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1713
+ format?: OptionFormat;
1714
+ hooks?: {
1715
+ /** Client-side option recalculation for dependent multi-select fields. */
1716
+ options?: FieldAdminOptionsHook;
1717
+ };
1718
+ };
1719
+ interface CharacterLimitFieldConfig {
1720
+ /** Advisory maximum character count exposed to editors and client tooling. */
1721
+ maxLength?: number;
1722
+ }
1723
+ interface WordLimitFieldConfig {
1724
+ /** Advisory maximum word count exposed to editors and client tooling. */
1725
+ maxWords?: number;
1726
+ }
1727
+ interface NumberLimitFieldConfig {
1728
+ /** Advisory minimum numeric value exposed to editors and client tooling. */
1729
+ min?: number;
1730
+ /** Advisory maximum numeric value exposed to editors and client tooling. */
1731
+ max?: number;
1732
+ }
1733
+ type CharacterLimitFieldAdmin = {
1734
+ /** Admin-only compatibility alias for `field.maxLength`. Prefer the top-level field property. */
1735
+ maxLength?: number;
1736
+ };
1737
+ type WordLimitFieldAdmin = {
1738
+ /** Admin-only compatibility alias for `field.maxWords`. Prefer the top-level field property. */
1739
+ maxWords?: number;
1740
+ };
1741
+ type NumberLimitFieldAdmin = {
1742
+ /** Admin-only compatibility alias for `field.min`. Prefer the top-level field property. */
1743
+ min?: number;
1744
+ /** Admin-only compatibility alias for `field.max`. Prefer the top-level field property. */
1745
+ max?: number;
1746
+ };
1747
+ /**
1748
+ * How a `number` field's value is presented in read-only Admin surfaces (list
1749
+ * cells and read-only inputs). Display only — the stored value is unchanged, and
1750
+ * editing still uses a plain numeric input. Every option is JSON-serializable so
1751
+ * it round-trips through Dyrected Cloud.
1752
+ *
1753
+ * Pass a shorthand string for defaults (`format: "currency"`) or an object to
1754
+ * configure it (`format: { type: "currency", currency: "NGN" }`).
1755
+ */
1756
+ type NumberFormat =
1757
+ /** Shorthand for the matching object form, using that format's defaults. */
1758
+ "decimal" | "currency" | "percent" | "compact" | "bytes" | "rating"
1759
+ /** Grouped number, e.g. `1234.5` → `1,234.5`. */
1760
+ | {
1761
+ type: "decimal";
1762
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1763
+ locale?: string;
1764
+ minimumFractionDigits?: number;
1765
+ maximumFractionDigits?: number;
1766
+ }
1767
+ /** Currency amount, e.g. `1234.5` → `$1,234.50`. */
1768
+ | {
1769
+ type: "currency";
1770
+ /** ISO 4217 currency code, e.g. `"USD"`, `"NGN"`, `"EUR"`. Defaults to `"USD"`. */
1771
+ currency?: string;
1772
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1773
+ locale?: string;
1774
+ minimumFractionDigits?: number;
1775
+ maximumFractionDigits?: number;
1776
+ }
1777
+ /**
1778
+ * Percentage. By default the stored value is a ratio, so `0.5` → `50%`. Set
1779
+ * `scale: false` when the stored value is already a percentage, so `50` → `50%`.
1780
+ */
1781
+ | {
1782
+ type: "percent";
1783
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1784
+ locale?: string;
1785
+ /** `false` when the stored number is already scaled to 0–100. Defaults to `true`. */
1786
+ scale?: boolean;
1787
+ minimumFractionDigits?: number;
1788
+ maximumFractionDigits?: number;
1789
+ }
1790
+ /** A measurement unit, e.g. `5` → `5 km` with `unit: "kilometer"`. */
1791
+ | {
1792
+ type: "unit";
1793
+ /** A [sanctioned Intl unit](https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier), e.g. `"kilometer"`, `"liter"`, `"celsius"`. */
1794
+ unit: string;
1795
+ unitDisplay?: "short" | "long" | "narrow";
1796
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1797
+ locale?: string;
1798
+ maximumFractionDigits?: number;
1799
+ }
1800
+ /** Abbreviated large numbers, e.g. `1200` → `1.2K`. */
1801
+ | {
1802
+ type: "compact";
1803
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1804
+ locale?: string;
1805
+ maximumFractionDigits?: number;
1806
+ }
1807
+ /** A byte count rendered with units, e.g. `1536` → `1.5 KB`. */
1808
+ | {
1809
+ type: "bytes";
1810
+ /** Use 1024-based units (`KiB`, `MiB`) instead of 1000-based (`KB`, `MB`). Defaults to `false`. */
1811
+ binary?: boolean;
1812
+ maximumFractionDigits?: number;
1813
+ }
1814
+ /** A star rating, e.g. `4` → `★★★★☆` with `max: 5`. */
1815
+ | {
1816
+ type: "rating";
1817
+ /** Total number of stars. Defaults to `5`. */
1818
+ max?: number;
1819
+ };
1820
+ /**
1821
+ * How a `date`, `datetime`, or `time` field's value is presented in read-only
1822
+ * Admin surfaces. Display only and JSON-serializable so it round-trips through
1823
+ * Dyrected Cloud.
1824
+ *
1825
+ * Pass a shorthand string (`format: "relative"`) or an object for finer control
1826
+ * (`format: { type: "date", dateStyle: "long" }`).
1827
+ */
1828
+ type DateFormat =
1829
+ /** Shorthand for the matching object form, using that format's defaults. */
1830
+ "date" | "datetime" | "time" | "relative"
1831
+ /** Calendar date, e.g. `Jan 5, 2026`. */
1832
+ | {
1833
+ type: "date";
1834
+ dateStyle?: "short" | "medium" | "long" | "full";
1835
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1836
+ locale?: string;
1837
+ }
1838
+ /** Date and time together, e.g. `Jan 5, 2026, 2:30 PM`. */
1839
+ | {
1840
+ type: "datetime";
1841
+ dateStyle?: "short" | "medium" | "long" | "full";
1842
+ timeStyle?: "short" | "medium" | "long" | "full";
1843
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1844
+ locale?: string;
1845
+ }
1846
+ /** Time of day, e.g. `2:30 PM`. */
1847
+ | {
1848
+ type: "time";
1849
+ timeStyle?: "short" | "medium" | "long" | "full";
1850
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1851
+ locale?: string;
1852
+ }
1853
+ /** Relative to now, e.g. `3 days ago`, `in 2 hours`. */
1854
+ | {
1855
+ type: "relative";
1856
+ /** BCP 47 locale tag. Defaults to the viewer's browser locale. */
1857
+ locale?: string;
1858
+ };
1859
+ type TextFieldAdmin = CharacterLimitFieldAdmin & WordLimitFieldAdmin & {
1860
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1861
+ format?: TextFormat;
1862
+ };
1863
+ type TextareaFieldAdmin = CharacterLimitFieldAdmin & WordLimitFieldAdmin & {
1864
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1865
+ format?: TextFormat;
1866
+ };
1867
+ type EmailFieldAdmin = CharacterLimitFieldAdmin & {
1868
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1869
+ format?: LinkFormat;
1870
+ };
1871
+ type UrlFieldAdmin = CharacterLimitFieldAdmin & {
1872
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1873
+ format?: LinkFormat;
1874
+ };
1875
+ type IconFieldAdmin = CharacterLimitFieldAdmin;
1876
+ type JsonFieldAdmin = {
1877
+ /** How the value is displayed in read-only Admin surfaces. Does not affect storage or editing. */
1878
+ format?: JsonFormat;
1879
+ };
1880
+ type NumberFieldAdmin = NumberLimitFieldAdmin & {
1881
+ /** How the value is displayed in read-only Admin surfaces (list cells, read-only inputs). Does not affect storage or editing. */
1882
+ format?: NumberFormat;
1883
+ };
1884
+ type DateFieldAdmin = {
1885
+ /** How the value is displayed in read-only Admin surfaces (list cells, read-only inputs). Does not affect storage or editing. */
1886
+ format?: DateFormat;
1887
+ };
1888
+ type JoinFieldAdmin = {
1889
+ /** Whether the join field should show the "Create new" action in the Admin UI. Defaults to `true`. */
1890
+ showCreateButton?: boolean;
1891
+ /** Whether the join field should show the "View all" action in the Admin UI. Defaults to `true`. */
1892
+ showViewButton?: boolean;
1893
+ };
1894
+ interface UrlLinkValue {
1895
+ /** Whether the link is a custom URL or a reference to an internal document. */
1896
+ type: "custom" | "internal";
1897
+ /** The link URL. Absolute for custom links; may be a site-relative path for internal links. */
1898
+ url?: string;
1899
+ /** For internal links, the collection slug of the referenced document. */
1900
+ relationTo?: string;
1901
+ /** For internal links, the ID of the referenced document. */
1902
+ value?: string;
1903
+ /** Optional display label shown for the link. */
1904
+ label?: string;
1905
+ }
1906
+ /**
1907
+ * Editor capabilities available on a `richText` field. Each feature maps to a
1908
+ * formatting control in the Admin editor toolbar and to the underlying editor
1909
+ * schema, so disabling a feature removes both its toolbar button and the
1910
+ * capability itself (including keyboard shortcuts and paste handling).
1911
+ */
1912
+ type RichTextFeature = "bold" | "italic" | "underline" | "strike" | "heading" | "bulletList" | "orderedList" | "blockquote" | "align" | "link" | "table" | "image";
1913
+ /** Heading levels offered by the `heading` rich-text feature. */
1914
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
1915
+ interface RichTextFieldConfig {
1916
+ /**
1917
+ * Editor capabilities to enable, in toolbar order. When omitted, every
1918
+ * {@link RichTextFeature} is enabled (the default toolbar). Provide a subset
1919
+ * to restrict what editors can do — for example `['bold', 'italic', 'link']`
1920
+ * for a lightweight inline editor.
1921
+ */
1922
+ features?: RichTextFeature[];
1923
+ /**
1924
+ * Heading levels offered when the `heading` feature is enabled.
1925
+ * Defaults to `[1, 2, 3]`.
1926
+ */
1927
+ headingLevels?: HeadingLevel[];
1928
+ /**
1929
+ * Upload collection slug used by the `image` feature's media picker.
1930
+ * Defaults to the first collection configured with `upload: true`.
1931
+ */
1932
+ uploadCollection?: string;
1933
+ }
1934
+ type TextField = TypedField<"text", string, TextFieldAdmin> & CharacterLimitFieldConfig & WordLimitFieldConfig;
1935
+ type TextareaField = TypedField<"textarea", string, TextareaFieldAdmin> & CharacterLimitFieldConfig & WordLimitFieldConfig;
1936
+ type EmailField = TypedField<"email", string, EmailFieldAdmin> & CharacterLimitFieldConfig;
1937
+ type UrlField = TypedField<"url", string | UrlLinkValue, UrlFieldAdmin> & CharacterLimitFieldConfig;
1938
+ type IconField = TypedField<"icon", string, IconFieldAdmin> & CharacterLimitFieldConfig;
1939
+ /** A calendar day, stored and returned as an ISO date string. */
1940
+ type DateField = TypedField<"date", string, DateFieldAdmin>;
1941
+ /** A specific instant, stored and returned as an ISO date-time string. */
1942
+ type DateTimeField = TypedField<"datetime", string, DateFieldAdmin>;
1943
+ /** A local time of day, stored as a string when the date is modeled elsewhere. */
1944
+ type TimeField = TypedField<"time", string, DateFieldAdmin>;
1945
+ /** A single choice from a fixed or dynamically-resolved set of options, stored as the chosen value. */
1946
+ type SelectField = TypedField<"select", string, SelectFieldAdmin>;
1947
+ /** A single choice shown as radio buttons, stored as the chosen value. */
1948
+ type RadioField = TypedField<"radio", string, RadioFieldAdmin>;
1949
+ /** A numeric value. Optional advisory `min`/`max` guide editors without enforcing server-side validation. */
1950
+ type NumberField = TypedField<"number", number, NumberFieldAdmin> & NumberLimitFieldConfig;
1951
+ /** A `true`/`false` value, shown to editors as a checkbox or switch. */
1952
+ type BooleanField = TypedField<"boolean", boolean, BooleanFieldAdmin>;
1953
+ /** Several choices from a fixed or dynamically-resolved set, stored as an array of the chosen values. */
1954
+ type MultiSelectField = TypedField<"multiSelect", string[], MultiSelectFieldAdmin>;
1955
+ /** A reference to one or more documents in another collection, stored as an ID or array of IDs. Use `relationTo` to name the target and `hasMany` for multiple. */
1956
+ type RelationshipField = TypedField<"relationship", string | string[]>;
1957
+ /** A reference to one or more documents in an upload-enabled collection, stored as an ID or array of IDs. Use `relationTo` to name the target and `hasMany` for multiple. */
1958
+ type ImageField = TypedField<"image", string | string[]>;
1959
+ /** Formatted content authored in the admin editor, stored as an HTML string. */
1960
+ type RichTextField = TypedField<"richText", string> & RichTextFieldConfig;
1961
+ /** An arbitrary JSON value. Dyrected stores it as-is and does not validate its shape. */
1962
+ type JsonField = TypedField<"json", Record<string, unknown>, JsonFieldAdmin>;
1963
+ /** A group of nested `fields` stored as an embedded object under this field's `name`. */
1964
+ type ObjectField = TypedField<"object", unknown>;
1965
+ /** A repeatable list of rows that all share the same `fields`, stored as an array of objects. */
1966
+ type ArrayField = TypedField<"array", unknown>;
1967
+ /** Flexible content built from a controlled set of typed `blocks`, stored as an ordered array where each row records its `blockType`. */
1968
+ type BlocksField = TypedField<"blocks", unknown>;
1969
+ /** A virtual reverse relationship that surfaces documents pointing back at this one via `collection` and `on`. Read-only; nothing is stored on this document. */
1970
+ type JoinField = TypedField<"join", unknown, JoinFieldAdmin>;
1971
+ /** A layout-only container that arranges its child `fields` horizontally in the admin UI. Stores no value of its own. */
1972
+ type RowField = TypedField<"row", unknown>;
1973
+ type Field = TextField | TextareaField | EmailField | UrlField | IconField | DateField | DateTimeField | TimeField | SelectField | RadioField | NumberField | BooleanField | MultiSelectField | RelationshipField | ImageField | RichTextField | JsonField | ObjectField | ArrayField | BlocksField | JoinField | RowField;
1974
+ interface UploadConfig {
1975
+ /** Allowed MIME types for uploaded files. */
1976
+ allowedMimeTypes?: string[];
1977
+ /** Maximum upload size in bytes. */
1978
+ maxFileSize?: number;
1979
+ /** Local filesystem destination used by disk-based storage adapters. */
1980
+ staticDir?: string;
1981
+ /** Public URL prefix for disk-based uploads. */
1982
+ staticURL?: string;
1983
+ /** Generated image size name used as the Admin media thumbnail. */
1984
+ adminThumbnail?: string;
1985
+ imageSizes?: {
1986
+ /** Stable name used to reference this generated size. */
1987
+ name: string;
1988
+ /** Target width in pixels. */
1989
+ width?: number;
1990
+ /** Target height in pixels. */
1991
+ height?: number;
1992
+ /** Crop strategy forwarded to the image processor. */
1993
+ crop?: string;
1994
+ /** Resize fit strategy forwarded to the image processor. */
1995
+ fit?: string;
1996
+ /** Prevents upscaling smaller source images. */
1997
+ withoutEnlargement?: boolean;
1998
+ /** Format-specific options forwarded to the image processor. */
1999
+ formatOptions?: Record<string, unknown>;
2000
+ }[];
2001
+ }
2002
+
2003
+ interface AdminAuthMember {
2004
+ id: string;
2005
+ email: string;
2006
+ name?: string;
2007
+ roles: string[];
2008
+ siteAccess?: string[];
2009
+ status?: "active" | "pending";
2010
+ [key: string]: any;
2011
+ }
2012
+ type AdminAuthProvisioningMode = "jit_only" | "jit_plus_membership_management" | "preprovisioned_only";
2013
+ interface AdminAuthClaimMapping {
2014
+ sub?: string;
2015
+ email?: string;
2016
+ name?: string;
2017
+ roles?: string;
2018
+ groups?: string;
2019
+ siteIds?: string;
2020
+ workspaceIds?: string;
2021
+ }
2022
+ interface AdminAuthResolvedIdentity {
2023
+ sub: string;
2024
+ email?: string;
2025
+ name?: string;
2026
+ roles?: string[];
2027
+ groups?: string[];
2028
+ siteIds?: string[];
2029
+ workspaceIds?: string[];
2030
+ rawClaims?: Record<string, unknown>;
2031
+ }
2032
+ interface AdminAuthAccessResolution {
2033
+ allowed: boolean;
2034
+ roles?: string[];
2035
+ data?: Record<string, unknown>;
2036
+ }
2037
+ interface AdminAuthResolveAccessArgs {
2038
+ identity: AdminAuthResolvedIdentity;
2039
+ providerId: string;
2040
+ siteId?: string;
2041
+ workspaceId?: string;
2042
+ req: {
2043
+ method: string;
2044
+ path: string;
2045
+ url: string;
2046
+ headers: Record<string, string | undefined>;
2047
+ };
2048
+ user: Record<string, unknown> | null;
2049
+ }
2050
+ interface AdminAuthProviderMembersConfig {
2051
+ list?: (args: {
2052
+ limit?: number;
2053
+ page?: number;
2054
+ sort?: string;
2055
+ where?: Record<string, any>;
2056
+ req: HookRequestContext;
2057
+ }) => Promise<{
2058
+ docs: AdminAuthMember[];
2059
+ totalDocs: number;
2060
+ limit: number;
2061
+ page: number;
2062
+ totalPages: number;
2063
+ hasNextPage: boolean;
2064
+ hasPrevPage: boolean;
2065
+ }>;
2066
+ get?: (args: {
2067
+ externalSubject: string;
2068
+ req: HookRequestContext;
2069
+ }) => Promise<AdminAuthMember | null>;
2070
+ create?: (args: {
2071
+ data: Record<string, any>;
2072
+ req: HookRequestContext;
2073
+ }) => Promise<AdminAuthMember>;
2074
+ update?: (args: {
2075
+ externalSubject: string;
2076
+ data: Record<string, any>;
2077
+ req: HookRequestContext;
2078
+ }) => Promise<AdminAuthMember>;
2079
+ delete?: (args: {
2080
+ externalSubject: string;
2081
+ req: HookRequestContext;
2082
+ }) => Promise<void>;
2083
+ }
2084
+ interface BaseAdminAuthProvider {
2085
+ id: string;
2086
+ type: "oidc" | "custom" | "cloud";
2087
+ displayName?: string;
2088
+ autoRedirect?: boolean;
2089
+ allowJitProvisioning?: boolean;
2090
+ claimMapping?: AdminAuthClaimMapping;
2091
+ members?: AdminAuthProviderMembersConfig;
2092
+ }
2093
+ interface OIDCAdminAuthProvider extends BaseAdminAuthProvider {
2094
+ type: "oidc";
2095
+ issuer: string;
2096
+ clientId: string;
2097
+ clientSecret: string;
2098
+ redirectUri?: string;
2099
+ scopes?: string[];
2100
+ authorizationEndpoint?: string;
2101
+ tokenEndpoint?: string;
2102
+ userInfoEndpoint?: string;
2103
+ }
2104
+ interface CustomAdminAuthProvider extends BaseAdminAuthProvider {
2105
+ type: "custom" | "cloud";
2106
+ startUrl?: string;
2107
+ issuer?: string;
2108
+ audience?: string;
2109
+ secret?: string;
2110
+ jwksUri?: string;
2111
+ tokenParam?: string;
2112
+ }
2113
+ type AdminAuthProvider = OIDCAdminAuthProvider | CustomAdminAuthProvider;
2114
+ interface PublicAdminAuthProvider {
2115
+ id: string;
2116
+ type: AdminAuthProvider["type"];
2117
+ displayName: string;
2118
+ autoRedirect?: boolean;
2119
+ }
2120
+ interface PublicAdminAuthConfig {
2121
+ mode: "local" | "external";
2122
+ collectionSlug?: string;
2123
+ provisioningMode?: AdminAuthProvisioningMode;
2124
+ providers: PublicAdminAuthProvider[];
2125
+ }
2126
+ interface AdminAuthConfig {
2127
+ mode: "local" | "external";
2128
+ collectionSlug?: string;
2129
+ provisioningMode?: AdminAuthProvisioningMode;
2130
+ providers: AdminAuthProvider[];
2131
+ resolveAccess?: (args: AdminAuthResolveAccessArgs) => Promise<AdminAuthAccessResolution> | AdminAuthAccessResolution;
2132
+ }
2133
+
2134
+ type DyrectedLoggerConfig = {
2135
+ options: LoggerOptions;
2136
+ destination?: DestinationStream;
2137
+ } | Logger;
2138
+ interface DyrectedObservabilityConfig {
2139
+ requestLogging?: {
2140
+ enabled?: boolean;
2141
+ logBodies?: boolean;
2142
+ maxBodyBytes?: number;
2143
+ redactPaths?: string[];
2144
+ includeHeaders?: string[];
2145
+ redactHeaders?: string[];
2146
+ };
2147
+ sampling?: {
2148
+ successRate?: number;
2149
+ traceSuccessRate?: number;
2150
+ bodySuccessRate?: number;
2151
+ alwaysKeep4xx?: boolean;
2152
+ alwaysKeep5xx?: boolean;
2153
+ };
2154
+ tracing?: {
2155
+ enabled?: boolean;
2156
+ serviceName?: string;
2157
+ exporter?: "otlp" | "console" | "none";
2158
+ headers?: Record<string, string>;
2159
+ endpoint?: string;
2160
+ };
2161
+ metrics?: {
2162
+ enabled?: boolean;
2163
+ exporter?: "otlp" | "prometheus" | "none";
2164
+ endpoint?: string;
2165
+ path?: string;
2166
+ };
2167
+ transports?: {
2168
+ targets?: Array<{
2169
+ type: "stdout";
2170
+ } | {
2171
+ type: "stderr";
2172
+ } | {
2173
+ type: "file";
2174
+ path: string;
2175
+ } | {
2176
+ type: "otlp";
2177
+ endpoint: string;
2178
+ headers?: Record<string, string>;
2179
+ }>;
2180
+ };
2181
+ }
2182
+ type TrustProxyConfig = boolean | number;
2183
+ interface RateLimitConfig {
2184
+ /**
2185
+ * Enable in-process request rate limiting for HTTP API routes.
2186
+ *
2187
+ * This protects routes at the Dyrected app layer. For production you should
2188
+ * still prefer an edge or proxy limit in front of it when possible.
2189
+ */
2190
+ enabled?: boolean;
2191
+ /**
2192
+ * Time window, in milliseconds, used to count requests from the same client.
2193
+ *
2194
+ * Defaults to `15 * 60 * 1000` (15 minutes).
2195
+ */
2196
+ window?: number;
2197
+ /**
2198
+ * Maximum number of requests a client may make within the current window.
2199
+ *
2200
+ * Defaults to `500`.
2201
+ */
2202
+ max?: number;
2203
+ /**
2204
+ * Trust upstream proxy forwarding headers when resolving the client IP.
2205
+ *
2206
+ * Set to `true` when Dyrected is deployed behind a reverse proxy or
2207
+ * platform edge that correctly sets `X-Forwarded-For`. Set to a number to
2208
+ * trust that many proxy hops from the right side of the header chain.
2209
+ */
2210
+ trustProxy?: TrustProxyConfig;
2211
+ /**
2212
+ * Route prefixes that should be subject to rate limiting.
2213
+ *
2214
+ * Defaults to `['/api']`.
2215
+ */
2216
+ paths?: string[];
2217
+ /**
2218
+ * Optional escape hatch for requests that should bypass the in-app limiter.
2219
+ *
2220
+ * Return `true` to skip limiting for the current request.
2221
+ */
2222
+ skip?: (args: {
2223
+ ip: string;
2224
+ path: string;
2225
+ method: string;
2226
+ req: HookRequestContext;
2227
+ }) => boolean | Promise<boolean>;
2228
+ }
2229
+ /**
2230
+ * The root configuration object passed to `createDyrectedApp`.
2231
+ *
2232
+ * This is the single source of truth for your entire Dyrected instance —
2233
+ * collections, globals, database adapter, storage, email, and more.
2234
+ *
2235
+ * @example
2236
+ * import { defineConfig } from '@dyrected/core'
2237
+ * import { SQLiteAdapter } from '@dyrected/db-sqlite'
2238
+ *
2239
+ * export default defineConfig({
2240
+ * db: new SQLiteAdapter({ filename: './db.sqlite' }),
2241
+ * collections: [Posts, Users],
2242
+ * globals: [SiteSettings],
2243
+ * })
2244
+ */
2245
+ interface DyrectedConfig<TUser extends AuthenticatedUser = AuthenticatedUser> {
2246
+ /**
2247
+ * Reusable block definitions that `blocks` fields can reference by slug via
2248
+ * `blockReferences`.
2249
+ */
2250
+ blocks?: Block[];
2251
+ /** Collection definitions. Each collection maps to a database table/collection. */
2252
+ collections: CollectionConfig<any>[];
2253
+ /** Global (singleton) definitions. Each global maps to a single document. */
2254
+ globals: GlobalConfig<any>[];
2255
+ /**
2256
+ * The database adapter. Required for all data operations.
2257
+ * @see DatabaseAdapter
2258
+ */
2259
+ db?: DatabaseAdapter;
2260
+ /**
2261
+ * The storage adapter for file uploads.
2262
+ * Required when any collection has `upload: true`.
2263
+ * @see StorageAdapter
2264
+ */
2265
+ storage?: StorageAdapter;
2266
+ /**
2267
+ * The image processing service. Required when any upload collection
2268
+ * defines `imageSizes`.
2269
+ * @see ImageService
2270
+ */
2271
+ image?: ImageService;
2272
+ /**
2273
+ * Runtime logger configuration. Accepts either logger options/destination or
2274
+ * a fully-instantiated Pino logger.
2275
+ */
2276
+ logger?: DyrectedLoggerConfig;
2277
+ /**
2278
+ * Request logging, redaction, sampling, tracing, metrics, and transport
2279
+ * configuration for the Dyrected server runtime.
2280
+ */
2281
+ observability?: DyrectedObservabilityConfig;
2282
+ /** Admin UI branding and metadata. */
2283
+ admin?: AdminConfig;
2284
+ /**
2285
+ * Deployment-level authentication strategy for the CMS dashboard (`/admin`).
2286
+ * This is separate from collection-level `auth: true`, which continues to
2287
+ * power application/customer auth independently.
2288
+ */
2289
+ adminAuth?: AdminAuthConfig;
2290
+ /**
2291
+ * Named access policies available to collection, global, and field access
2292
+ * rules via `{ policy: 'name' }`.
2293
+ *
2294
+ * A policy can be a **function** (full server logic, evaluated to a static
2295
+ * boolean when serialized for the admin panel) or a **Jexl string** (or
2296
+ * boolean). String policies are inlined when the schema is sent to the admin,
2297
+ * so the admin panel evaluates them live against the current form — the same
2298
+ * way it evaluates inline Jexl rules.
2299
+ */
2300
+ accessPolicies?: Record<string, AccessPolicyResolver<Record<string, unknown>, TUser> | string | boolean>;
2301
+ /**
2302
+ * Email transport configuration. Required for welcome emails, password
2303
+ * resets, and invite links.
2304
+ *
2305
+ * @example
2306
+ * email: {
2307
+ * from: 'no-reply@myapp.com',
2308
+ * send: async ({ to, subject, html }) => {
2309
+ * await resend.emails.send({ from, to, subject, html })
2310
+ * },
2311
+ * }
2312
+ */
2313
+ email?: {
2314
+ /** The `From` address for all outbound emails. */
2315
+ from: string;
2316
+ /** The send function. Wire in any email provider (Resend, SendGrid, SES, etc.). */
2317
+ send: (args: {
2318
+ to: string;
2319
+ subject: string;
2320
+ html: string;
2321
+ }) => Promise<void>;
2322
+ /** Override the default email templates. */
2323
+ templates?: {
2324
+ welcome?: (args: {
2325
+ email: string;
2326
+ }) => {
2327
+ subject?: string;
2328
+ html: string;
2329
+ };
2330
+ invite?: (args: {
2331
+ token: string;
2332
+ invitedByEmail?: string;
2333
+ url?: string;
2334
+ }) => {
2335
+ subject?: string;
2336
+ html: string;
2337
+ };
2338
+ resetPassword?: (args: {
2339
+ token: string;
2340
+ url?: string;
2341
+ }) => {
2342
+ subject?: string;
2343
+ html: string;
2344
+ };
2345
+ passwordChanged?: (args: {
2346
+ email: string;
2347
+ }) => {
2348
+ subject?: string;
2349
+ html: string;
2350
+ };
2351
+ };
2352
+ };
2353
+ /**
2354
+ * Redis connection URL. Required for distributed caching of dynamic option
2355
+ * resolvers and other server-side caches in multi-instance deployments.
2356
+ *
2357
+ * @example
2358
+ * redis: { url: process.env.REDIS_URL }
2359
+ */
2360
+ redis?: {
2361
+ url: string;
2362
+ };
2363
+ /** Durable lifecycle-event delivery configuration. */
2364
+ events?: {
2365
+ handlers: LifecycleEventHandler[];
2366
+ /** Maximum delivery attempts before an event remains failed. Defaults to 8. */
2367
+ maxAttempts?: number;
2368
+ /** Initial exponential-backoff delay in milliseconds. Defaults to 1000. */
2369
+ retryDelayMs?: number;
2370
+ };
2371
+ /**
2372
+ * Cross-Origin Resource Sharing (CORS) configuration.
2373
+ * List all origins that are allowed to call the Dyrected API.
2374
+ *
2375
+ * @example
2376
+ * cors: { origins: ['https://myapp.com', 'https://www.myapp.com'] }
2377
+ */
2378
+ cors?: {
2379
+ origins: string[];
2380
+ };
2381
+ /**
2382
+ * App-level HTTP request rate limiting.
2383
+ *
2384
+ * Similar to Payload's `rateLimit` option, this counts requests by client IP
2385
+ * over a rolling time window and returns `429` responses once the limit is
2386
+ * exhausted.
2387
+ */
2388
+ rateLimit?: RateLimitConfig;
2389
+ /**
2390
+ * Callback to dynamically fetch additional collections and globals for a
2391
+ * given site ID at request time. Used in multi-tenant deployments where each
2392
+ * site has its own schema stored in the database.
2393
+ */
2394
+ onSchemaFetch?: (siteId: string) => Promise<{
2395
+ blocks?: Block[];
2396
+ collections?: CollectionConfig<any>[];
2397
+ globals?: GlobalConfig<any>[];
2398
+ accessPolicies?: Record<string, AccessPolicyResolver<Record<string, unknown>, TUser> | string | boolean>;
2399
+ admin?: AdminConfig;
2400
+ adminAuth?: AdminAuthConfig;
2401
+ }>;
2402
+ }
2403
+
2404
+ export { type RadioField as $, type AdminAuthConfig as A, type Block as B, type CollectionConfig as C, type DyrectedConfig as D, type DetailText as E, type Field as F, type GlobalConfig as G, type HookRequestContext as H, type DetailSchema as I, type ArrayField as J, type BlocksField as K, type LifecycleEventName as L, type BooleanField as M, type AuthConfig as N, type AccessPolicyResolver as O, type PublicAdminAuthConfig as P, type DateField as Q, type DateTimeField as R, type EmailField as S, type IconField as T, type ImageField as U, type JoinField as V, type WorkflowConfig as W, type JsonField as X, type MultiSelectField as Y, type NumberField as Z, type ObjectField as _, type DeclarativeHookExpression as a, type FieldAdminOnChangeHook as a$, type RelationshipField as a0, type RichTextField as a1, type RowField as a2, type SelectField as a3, type TextField as a4, type TextareaField as a5, type TimeField as a6, type UrlField as a7, type AccessFunction as a8, type AccessFunctionArgs as a9, type CollectionAfterDeleteHook as aA, type CollectionAfterReadHook as aB, type CollectionAfterReadHookEntry as aC, type CollectionAfterTransitionHook as aD, type CollectionBeforeChangeHook as aE, type CollectionBeforeChangeHookEntry as aF, type CollectionBeforeDeleteHook as aG, type CollectionBeforeReadHook as aH, type CollectionBeforeReadHookEntry as aI, type CollectionBeforeTransitionHook as aJ, type CollectionListComponentSlots as aK, type CountOperation as aL, type CustomAdminAuthProvider as aM, type DatabaseAdapter as aN, type DateFieldAdmin as aO, type DateFormat as aP, type DetailSpan as aQ, type DisplayTone as aR, type DisplayVariant as aS, type DynamicOptionItem as aT, type DynamicOptionsConfig as aU, type DynamicOptionsResolver as aV, type DynamicOptionsResolverArgs as aW, type DyrectedLoggerConfig as aX, type DyrectedObservabilityConfig as aY, type EmailFieldAdmin as aZ, type FieldAdminHooks as a_, type AccessResult as aa, type AccessRule as ab, type AdminAuthAccessResolution as ac, type AdminAuthClaimMapping as ad, type AdminAuthMember as ae, type AdminAuthProvider as af, type AdminAuthProviderMembersConfig as ag, type AdminAuthProvisioningMode as ah, type AdminAuthResolveAccessArgs as ai, type AdminAuthResolvedIdentity as aj, type AdminConfig as ak, type AdminDashboardComponentSlots as al, type AdminIconName as am, type AggregateArgs as an, type AggregateCastType as ao, type AggregateInput as ap, type AggregateOperation as aq, type AggregateResult as ar, type BaseAdminAuthProvider as as, type BaseFieldAdmin as at, type BlockVariant as au, type BooleanFieldAdmin as av, type BooleanFormat as aw, type CharacterLimitFieldAdmin as ax, type CharacterLimitFieldConfig as ay, type CollectionAfterChangeHook as az, type AuthenticatedUser as b, type FieldAdminOnChangeHookArgs as b0, type FieldAdminOptionsHook as b1, type FieldAdminOptionsHookArgs as b2, type FieldAdminOptionsHookResult as b3, type FieldAfterReadHook as b4, type FieldAfterReadHookArgs as b5, type FieldBase as b6, type FieldBeforeChangeHook as b7, type FieldBeforeChangeHookArgs as b8, type FieldHook as b9, type NumberLimitFieldConfig as bA, type NumericOperation as bB, type OIDCAdminAuthProvider as bC, type OptionFormat as bD, type PaginatedResult as bE, type PublicAdminAuthProvider as bF, type RadioFieldAdmin as bG, type RateLimitConfig as bH, type ReadonlyDatabaseAdapter as bI, type RichTextFeature as bJ, type RichTextFieldConfig as bK, type SelectFieldAdmin as bL, type StorageAdapter as bM, type TextFieldAdmin as bN, type TextFormat as bO, type TextareaFieldAdmin as bP, type TrustProxyConfig as bQ, type TypedField as bR, type UploadConfig as bS, type UrlFieldAdmin as bT, type UrlLinkValue as bU, type WordLimitFieldAdmin as bV, type WordLimitFieldConfig as bW, type WorkflowMetadata as bX, type WorkflowRole as bY, type WorkflowState as bZ, type WorkflowTransitionContext as b_, type FieldHooks as ba, type FieldType as bb, type FileData as bc, type GlobalAfterChangeHook as bd, type GlobalAfterReadHook as be, type GlobalAfterReadHookEntry as bf, type GlobalBeforeChangeHook as bg, type GlobalBeforeChangeHookEntry as bh, type GlobalBeforeReadHook as bi, type GlobalBeforeReadHookEntry as bj, type HeadingLevel as bk, type HookFunction as bl, type IconFieldAdmin as bm, type ImageService as bn, type InferAggregateResult as bo, type JoinFieldAdmin as bp, type JsonFieldAdmin as bq, type JsonFormat as br, LIFECYCLE_EVENT_NAMES as bs, type LifecycleEventHandler as bt, type LinkFormat as bu, type MultiSelectFieldAdmin as bv, type NamedAccessPolicy as bw, type NumberFieldAdmin as bx, type NumberFormat as by, type NumberLimitFieldAdmin as bz, type WorkflowTransition as c, type LifecycleEvent as d, type BaseDocument as e, type DetailComputedOptions as f, type ComputedHandler as g, type DetailComputed as h, type DetailCustomOptions as i, type DetailCustom as j, type DetailDividerOptions as k, type DetailDivider as l, type DisplayFieldOptions as m, type DetailField as n, type DetailItem as o, type DetailGridOptions as p, type DetailGrid as q, type DetailRepeatOptions as r, type DetailRepeat as s, type DetailSectionOptions as t, type DetailSection as u, type DetailTabOptions as v, type DetailTab as w, type DetailTabsOptions as x, type DetailTabs as y, type DetailTextOptions as z };