@easy-cms/core 1.31.0 → 1.33.0

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.
@@ -93,6 +93,67 @@ declare const isSignedIn: Access;
93
93
  */
94
94
  declare const isAdmin: Access;
95
95
 
96
+ /**
97
+ * Secrets that verify: the current one first, then `previousSecrets` while one is replaced. New
98
+ * values are always signed with the current one.
99
+ */
100
+ type Secrets = string | readonly string[];
101
+ /** The secrets of a config, the current one first. */
102
+ declare function secretsOf(config: {
103
+ readonly secret: string;
104
+ readonly previousSecrets?: readonly string[] | undefined;
105
+ }): readonly string[];
106
+ /** What a preview token opens: one document of a collection, or one global. */
107
+ type PreviewTarget = {
108
+ readonly collection: string;
109
+ readonly id: string;
110
+ } | {
111
+ readonly global: string;
112
+ };
113
+ /**
114
+ * Why a link was sent: a forgotten password, an invitation to a new account, or (`verify`) to
115
+ * confirm the email of an account made by signing up.
116
+ */
117
+ type PasswordPurpose = 'reset' | 'invite' | 'verify';
118
+
119
+ declare const API_KEYS = "api-keys";
120
+ /** What a key may do with a collection. `create` on `media` means uploading. */
121
+ declare const API_KEY_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
122
+ type ApiKeyOperation = (typeof API_KEY_OPERATIONS)[number];
123
+ /** What a key may do with a global. */
124
+ declare const API_KEY_GLOBAL_OPERATIONS: readonly ["read", "update", "publish"];
125
+ type ApiKeyGlobalOperation = (typeof API_KEY_GLOBAL_OPERATIONS)[number];
126
+ /** Stored in the key's `permissions` field. Anything not listed is not allowed. */
127
+ interface ApiKeyPermissions {
128
+ readonly collections?: Readonly<Record<string, readonly ApiKeyOperation[]>>;
129
+ readonly globals?: Readonly<Record<string, readonly ApiKeyGlobalOperation[]>>;
130
+ /**
131
+ * With `upload.folders`: the media folders (ids) the key may use, with their subfolders. Empty
132
+ * or missing: every folder.
133
+ */
134
+ readonly folders?: readonly (string | number)[];
135
+ /**
136
+ * The context the key was created in (`onRequest`, e.g. its tenant), kept for good: requests
137
+ * with the key get it as `user.apiKey.permissions.context`. Set by Easy CMS, not by input.
138
+ */
139
+ readonly context?: RequestContext;
140
+ }
141
+ /** Set on `AuthUser.apiKey` when a request authenticated with an API key. */
142
+ interface ApiKeyContext {
143
+ readonly id: number | string;
144
+ readonly name: string;
145
+ readonly permissions: ApiKeyPermissions;
146
+ }
147
+ /**
148
+ * Whether a request's API key allows an operation. Always true without a key (sessions are
149
+ * governed by access rules alone); a key only narrows what its user may do.
150
+ */
151
+ declare function keyAllows(user: AuthUser | null, target: {
152
+ collection: string;
153
+ } | {
154
+ global: string;
155
+ }, operation: ApiKeyOperation): boolean;
156
+
96
157
  /** More about a log line: `error` for what failed (its stack goes to the log). */
97
158
  interface LogDetails {
98
159
  readonly error?: unknown;
@@ -361,66 +422,155 @@ interface BundleHints {
361
422
  readonly traceInclude: readonly string[];
362
423
  }
363
424
 
364
- /**
365
- * Secrets that verify: the current one first, then `previousSecrets` while one is replaced. New
366
- * values are always signed with the current one.
367
- */
368
- type Secrets = string | readonly string[];
369
- /** The secrets of a config, the current one first. */
370
- declare function secretsOf(config: {
371
- readonly secret: string;
372
- readonly previousSecrets?: readonly string[] | undefined;
373
- }): readonly string[];
374
- /** What a preview token opens: one document of a collection, or one global. */
375
- type PreviewTarget = {
376
- readonly collection: string;
377
- readonly id: string;
378
- } | {
379
- readonly global: string;
380
- };
381
- /**
382
- * Why a link was sent: a forgotten password, an invitation to a new account, or (`verify`) to
383
- * confirm the email of an account made by signing up.
384
- */
385
- type PasswordPurpose = 'reset' | 'invite' | 'verify';
386
-
387
- declare const API_KEYS = "api-keys";
388
- /** What a key may do with a collection. `create` on `media` means uploading. */
389
- declare const API_KEY_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
390
- type ApiKeyOperation = (typeof API_KEY_OPERATIONS)[number];
391
- /** What a key may do with a global. */
392
- declare const API_KEY_GLOBAL_OPERATIONS: readonly ["read", "update", "publish"];
393
- type ApiKeyGlobalOperation = (typeof API_KEY_GLOBAL_OPERATIONS)[number];
394
- /** Stored in the key's `permissions` field. Anything not listed is not allowed. */
395
- interface ApiKeyPermissions {
396
- readonly collections?: Readonly<Record<string, readonly ApiKeyOperation[]>>;
397
- readonly globals?: Readonly<Record<string, readonly ApiKeyGlobalOperation[]>>;
425
+ /** Audit log settings (`audit`). */
426
+ interface AuditConfig {
427
+ /** Days to keep entries; older ones are deleted. `0`: keep them all. Default 365. */
428
+ readonly keepDays?: number;
429
+ /** Keep the values before and after a change, not only which fields changed. Default `true`. */
430
+ readonly values?: boolean;
431
+ /** Failed logins within an hour that the dashboard warns about. Default 20. */
432
+ readonly failedLogins?: number;
398
433
  /**
399
- * With `upload.folders`: the media folders (ids) the key may use, with their subfolders. Empty
400
- * or missing: every folder.
434
+ * The part of the site an entry belongs to, from the request's context, e.g. its tenant
435
+ * (usually set by a plugin). Users whose role holds in a part only (`scoped`) see that part's
436
+ * entries; others see the chosen part's, or all.
401
437
  */
402
- readonly folders?: readonly (string | number)[];
438
+ readonly scope?: ((args: {
439
+ readonly context: RequestContext;
440
+ readonly user: AuthUser | null;
441
+ }) => string | null | undefined) | false;
442
+ }
443
+ /** A field that changed: values before and after, unless they are not kept. */
444
+ interface AuditChange {
445
+ field: string;
446
+ before?: unknown;
447
+ after?: unknown;
448
+ }
449
+ /** One entry (`GET <api>/admin/audit`). */
450
+ interface AuditEntry {
451
+ id: ID;
452
+ /** When, as an ISO date. */
453
+ at: string;
454
+ /** E.g. `create`, `update`, `publish`, `delete`, `login`, `login.failed`, `role.update`. */
455
+ action: string;
456
+ /** A collection slug, `global:<slug>`, or an area such as `auth`, `roles`, `backups`. */
457
+ target: string | null;
458
+ /** The document's id. */
459
+ doc: string | null;
460
+ /** The document's title then (kept after it is deleted). */
461
+ title: string | null;
462
+ actor: {
463
+ id: string | null;
464
+ email: string | null;
465
+ /** `user` (admin or REST), `api-key`, `system` (code), `scheduler`. */
466
+ via: string;
467
+ ip: string | null;
468
+ userAgent: string | null;
469
+ };
470
+ changes: AuditChange[] | null;
471
+ detail: Record<string, unknown> | null;
472
+ }
473
+ /** `GET <api>/admin/audit`: a page of entries, newest first. */
474
+ /** A page of the audit log: the list shape of the rest of the API. */
475
+ type AuditPage = PaginatedDocs<AuditEntry>;
476
+ interface AuditFilter {
477
+ action?: string | null;
478
+ target?: string | null;
479
+ doc?: string | null;
480
+ /** An email, or part of one. */
481
+ actor?: string | null;
482
+ /** ISO dates. */
483
+ from?: string | null;
484
+ to?: string | null;
485
+ /** Only entries of this part of the site (`scope`). */
486
+ scope?: string | null;
487
+ }
488
+ /** `POST <api>/admin/audit/verify`. */
489
+ interface AuditVerification {
490
+ checked: number;
491
+ /** Entries whose signature does not match: changed after they were written. */
492
+ invalid: ID[];
403
493
  /**
404
- * The context the key was created in (`onRequest`, e.g. its tenant), kept for good: requests
405
- * with the key get it as `user.apiKey.permissions.context`. Set by Easy CMS, not by input.
494
+ * Missing ids between entries: deleted entries, or (rarely) inserts that failed. Ids are never
495
+ * reused, so the newest entries deleted show up here once anything is written after them (a
496
+ * check writes an entry itself). The server's log has each check's last id, to compare with.
406
497
  */
407
- readonly context?: RequestContext;
408
- }
409
- /** Set on `AuthUser.apiKey` when a request authenticated with an API key. */
410
- interface ApiKeyContext {
411
- readonly id: number | string;
412
- readonly name: string;
413
- readonly permissions: ApiKeyPermissions;
498
+ gaps: number;
499
+ at: string;
414
500
  }
415
501
  /**
416
- * Whether a request's API key allows an operation. Always true without a key (sessions are
417
- * governed by access rules alone); a key only narrows what its user may do.
502
+ * The audit log (`audit` in the config): who did what, when, from where. Entries can't be
503
+ * changed through Easy CMS; each is signed with the secret, so changes in the database show.
504
+ * Available as `cms.audit`.
418
505
  */
419
- declare function keyAllows(user: AuthUser | null, target: {
420
- collection: string;
421
- } | {
422
- global: string;
423
- }, operation: ApiKeyOperation): boolean;
506
+ declare class AuditLog {
507
+ private readonly cms;
508
+ /** Entries that could not be written since the server started. */
509
+ failures: number;
510
+ constructor(cms: EasyCMS);
511
+ get enabled(): boolean;
512
+ private get settings();
513
+ /**
514
+ * Writes an entry. The user defaults to the request's. Never throws: a failure is logged and
515
+ * counted, and the dashboard says so.
516
+ */
517
+ record(entry: {
518
+ action: string;
519
+ target?: string | null;
520
+ doc?: ID | null;
521
+ title?: string | null;
522
+ user?: AuthUser | null;
523
+ /** Who it was when there is no user, e.g. the email of a failed login. */
524
+ email?: string | null;
525
+ changes?: AuditChange[] | null;
526
+ detail?: Record<string, unknown> | null;
527
+ /** The call's context, when it is not the request's. */
528
+ context?: RequestContext;
529
+ }): Promise<void>;
530
+ /** Entries, newest first: `page` (from 1) of `limit` (default 50, at most 100). */
531
+ list(options?: AuditFilter & {
532
+ page?: number;
533
+ limit?: number;
534
+ }): Promise<AuditPage>;
535
+ /** The filtered entries as CSV (the newest 10,000). */
536
+ csv(filter: AuditFilter): Promise<string>;
537
+ /** Checks every entry's signature, and that no ids are missing. */
538
+ verify(): Promise<AuditVerification>;
539
+ /** The secret an entry was signed with: the current one, a previous one, or none (changed). */
540
+ private signer;
541
+ /**
542
+ * After `secret` changed: signs again, with the current secret, the entries signed with one of
543
+ * `previousSecrets`, so they still verify once it is removed. Entries no secret verifies were
544
+ * changed: they are left as they are, and reported. Records `audit.resign`.
545
+ */
546
+ resign(): Promise<{
547
+ checked: number;
548
+ resigned: number;
549
+ invalid: ID[];
550
+ }>;
551
+ /**
552
+ * What the dashboard warns about: many failed logins, sign-ins of break-glass accounts in the
553
+ * last day, entries not written, a failed check.
554
+ */
555
+ attention(now?: Date): Promise<({
556
+ id: 'failed-logins';
557
+ count: number;
558
+ } | {
559
+ id: 'break-glass';
560
+ count: number;
561
+ emails: string[];
562
+ } | {
563
+ id: 'audit-failures';
564
+ count: number;
565
+ } | {
566
+ id: 'audit-tampered';
567
+ invalid: number;
568
+ at: string;
569
+ })[]>;
570
+ private sign;
571
+ private lastVerification;
572
+ private where;
573
+ }
424
574
 
425
575
  /** The cookie that carries a sign-in through the provider and back. */
426
576
  declare const SSO_COOKIE = "ecms-sso";
@@ -752,304 +902,6 @@ declare function consoleEmail(options?: {
752
902
  })[];
753
903
  };
754
904
 
755
- /** A label shown in the admin UI. Either one string or one string per admin locale. */
756
- type Label = string | {
757
- readonly [locale: string]: string;
758
- };
759
- interface FieldValidateContext {
760
- /** The whole document being validated, including sibling fields. */
761
- readonly data: Readonly<Record<string, unknown>>;
762
- readonly operation: 'create' | 'update';
763
- }
764
- /** Return `true` when valid, or an error message to show on the field. */
765
- type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
766
- /**
767
- * A Web Component from an admin module (`admin.modules`), by its tag name, which must start
768
- * with `ecms-`. `props` (plain JSON) reach the element as its `options` property.
769
- */
770
- type AdminComponent = string | {
771
- readonly tag: string;
772
- readonly props?: Readonly<Record<string, unknown>>;
773
- };
774
- /** How the admin shows a field, with components from admin modules. */
775
- interface FieldAdmin {
776
- /** Help below the field's label. */
777
- readonly description?: Label;
778
- /** Its share of a row (`admin.layout`'s `row`). Default: an equal share. */
779
- readonly width?: '1/4' | '1/3' | '1/2' | '2/3' | '3/4' | 'full';
780
- /**
781
- * Shown only when its sibling fields match, e.g. `{ field: 'linkType', equals: 'external' }`.
782
- * A hidden field isn't required. See `FieldCondition`.
783
- */
784
- readonly condition?: FieldCondition;
785
- /** Shown instead of the field's input; the admin keeps the label and error messages. */
786
- readonly component?: AdminComponent;
787
- /** Shown below the field, e.g. a length meter or a preview. */
788
- readonly after?: readonly AdminComponent[];
789
- /** Shows the value in the admin's lists (e.g. a color swatch). Default: the plain value. */
790
- readonly cell?: AdminComponent;
791
- /**
792
- * A column of the list at first (each user can change it): `true`, or per request, e.g.
793
- * only when the admin shows every tenant.
794
- */
795
- readonly column?: boolean | ((args: AdminFieldArgs) => boolean | Promise<boolean>);
796
- /** Relationships: offer to create the related document in place. Default true. */
797
- readonly allowCreate?: boolean;
798
- /**
799
- * The value a new document's form starts with, worked out for each user, e.g. from the
800
- * request's context. The server's own default (`defaultValue`, hooks) still applies.
801
- */
802
- readonly initialValue?: (args: AdminFieldArgs) => unknown;
803
- /** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
804
- readonly position?: 'sidebar';
805
- /** `false`: the list's filters don't offer it. Default: every field the user may read. */
806
- readonly filter?: boolean;
807
- }
808
- /** What per-request admin options of a field receive. */
809
- interface AdminFieldArgs {
810
- readonly user: AuthUser;
811
- readonly context: RequestContext;
812
- }
813
- /**
814
- * Field types that packages add (`fieldTypes` in the config), for the types inferred from the
815
- * config. A package declares its own by augmenting this interface:
816
- *
817
- * ```ts
818
- * declare module '@easy-cms/core' {
819
- * interface CustomFieldTypes {
820
- * color: { value: string; options: { readonly presets?: readonly string[] } }
821
- * }
822
- * }
823
- * ```
824
- */
825
- interface CustomFieldTypes {
826
- }
827
- /** A field of a type from `CustomFieldTypes`: the common options, plus the type's own. */
828
- type CustomField = {
829
- [K in keyof CustomFieldTypes]: BaseField<K & string, CustomFieldTypes[K] extends {
830
- value: infer V;
831
- } ? V : unknown> & (CustomFieldTypes[K] extends {
832
- options: infer O;
833
- } ? O : unknown);
834
- }[keyof CustomFieldTypes];
835
- interface BaseField<TType extends string, TValue> {
836
- readonly type: TType;
837
- readonly name: string;
838
- readonly label?: Label;
839
- readonly required?: boolean;
840
- readonly unique?: boolean;
841
- /**
842
- * With `unique` (or on a slug field): a sibling field (e.g. `parent` or `tenant`), or several,
843
- * whose documents only need to differ among themselves, so `/about/team` and `/careers/team`
844
- * can both be `team`. Single, unlocalized relationship, select, text or number fields.
845
- */
846
- readonly uniqueWithin?: string | readonly string[];
847
- /**
848
- * `true` for a database index (filters and sorts on this field). `'trigram'`, on text, textarea
849
- * and email fields: an index for `like`, which finds any part of the text, in Thai as in other
850
- * languages. Postgres only (pg_trgm); nothing on SQLite. There, `like` needs at least three
851
- * characters, and the admin's searches wait for them.
852
- */
853
- readonly index?: boolean | 'trigram';
854
- readonly defaultValue?: TValue;
855
- readonly validate?: FieldValidate<TValue>;
856
- readonly access?: FieldAccess;
857
- /** Stored but never returned by the API nor accepted as input (e.g. a password hash). */
858
- readonly hidden?: boolean;
859
- /**
860
- * One value per locale (needs `localization` in the config). Not for groups, arrays or
861
- * hasMany fields: localize the fields inside a group or array instead.
862
- */
863
- readonly localized?: boolean;
864
- /** How the field looks in the admin: components, position, width, conditions. */
865
- readonly admin?: FieldAdmin;
866
- /** Set by Easy CMS on fields of an added type (`fieldTypes`): the type's name, e.g. `color`. */
867
- readonly customType?: string;
868
- }
869
- interface TextField extends BaseField<'text', string> {
870
- readonly minLength?: number;
871
- readonly maxLength?: number;
872
- }
873
- interface TextareaField extends BaseField<'textarea', string> {
874
- readonly minLength?: number;
875
- readonly maxLength?: number;
876
- }
877
- interface NumberField extends BaseField<'number', number> {
878
- readonly min?: number;
879
- readonly max?: number;
880
- }
881
- interface BooleanField extends BaseField<'boolean', boolean> {
882
- }
883
- /** Stored and returned as an ISO 8601 string. */
884
- interface DateField extends BaseField<'date', string> {
885
- }
886
- interface EmailField extends BaseField<'email', string> {
887
- }
888
- interface JsonField extends BaseField<'json', unknown> {
889
- }
890
- type SelectOption = string | {
891
- readonly label: Label;
892
- readonly value: string;
893
- };
894
- interface SelectField extends BaseField<'select', string | readonly string[]> {
895
- readonly options: readonly SelectOption[];
896
- readonly hasMany?: boolean;
897
- }
898
- interface SlugField extends BaseField<'slug', string> {
899
- /** Name of a sibling `text` field to generate the slug from. */
900
- readonly from?: string;
901
- }
902
- /** Tiptap / ProseMirror JSON document. */
903
- interface RichTextDocument {
904
- readonly type: 'doc';
905
- readonly content?: readonly unknown[];
906
- }
907
- interface RichTextField extends BaseField<'richText', RichTextDocument> {
908
- /**
909
- * Blocks editors place between paragraphs (#328): a card, a callout, a video. Each is a node
910
- * `{ type: 'block', attrs: { blockType, id, fields } }` in the document.
911
- */
912
- readonly blocks?: readonly RichTextBlock[];
913
- }
914
- /** Arguments of a rich text block's `admin.summary` function. */
915
- interface BlockSummaryArgs {
916
- readonly fields: Record<string, unknown>;
917
- readonly user: AuthUser | null;
918
- readonly cms: EasyCMS;
919
- }
920
- /** A block in rich text (`richText.blocks`): the fields of `Block`, and how its card reads. */
921
- interface RichTextBlock extends Block {
922
- readonly admin?: {
923
- /**
924
- * The text of the block's card in the editor: a function, run on the server, or the tag of a
925
- * custom element from an admin module, which gets `fields`. Default: the block's label and
926
- * its first value.
927
- */
928
- readonly summary?: string | ((args: BlockSummaryArgs) => string | Promise<string>);
929
- };
930
- }
931
- /** References a document in the built-in `media` collection: a file or an image. */
932
- interface UploadField extends BaseField<'upload', never> {
933
- /** Several files, e.g. a gallery: the value is a list, in the order editors arrange. */
934
- readonly hasMany?: boolean;
935
- /** With `hasMany`: the fewest files. */
936
- readonly minRows?: number;
937
- /** With `hasMany`: the most files. */
938
- readonly maxRows?: number;
939
- /**
940
- * The file types allowed, e.g. `['image/*']` or `['image/*', 'application/pdf']`. The admin
941
- * offers only these, and saving refuses others.
942
- */
943
- readonly mimeTypes?: readonly string[];
944
- /**
945
- * With `upload.folders`: the `key` of the media folder this field's files go in, e.g.
946
- * `'banners'`. Its picker opens there and its uploads land there; the folder is made when it
947
- * is first needed.
948
- */
949
- readonly folder?: string;
950
- /** With `folder`: only files in that folder (and its subfolders) may be chosen. */
951
- readonly folderOnly?: boolean;
952
- /** Which media documents may be chosen, as for relationships: offered and checked on save. */
953
- readonly filterOptions?: FilterOptions;
954
- }
955
- interface FilterOptionsArgs {
956
- /** The document being edited; `undefined` while it is being created. */
957
- readonly id: ID | undefined;
958
- /** The collection the choices come from: one of the field's `to`. */
959
- readonly relationTo: string;
960
- readonly user: AuthUser | null;
961
- readonly cms: EasyCMS;
962
- /** The request's context (`onRequest`), `{}` when there is none. */
963
- readonly context: RequestContext;
964
- }
965
- /**
966
- * Which documents a relationship may point to: a `where` on the target collection, or `true`
967
- * for any. Runs on the server: the admin's picker offers only these, and saving checks them.
968
- */
969
- type FilterOptions = (args: FilterOptionsArgs) => Where | true | Promise<Where | true>;
970
- interface RelationshipField extends BaseField<'relationship', never> {
971
- /**
972
- * Slug of the target collection, or several: then each value is `{ relationTo, value }`, the
973
- * collection and the document's id (#327).
974
- */
975
- readonly to: string | readonly string[];
976
- readonly hasMany?: boolean;
977
- /** With `hasMany`: the fewest documents. */
978
- readonly minRows?: number;
979
- /** With `hasMany`: the most documents. */
980
- readonly maxRows?: number;
981
- readonly filterOptions?: FilterOptions;
982
- }
983
- interface ArrayField extends BaseField<'array', never> {
984
- readonly fields: readonly Field[];
985
- readonly minRows?: number;
986
- readonly maxRows?: number;
987
- }
988
- interface GroupField extends BaseField<'group', never> {
989
- readonly fields: readonly Field[];
990
- }
991
- /** One kind of block in a `blocks` field. */
992
- interface Block {
993
- /** Stored in each row as `blockType`. */
994
- readonly slug: string;
995
- readonly labels?: {
996
- readonly singular?: Label;
997
- readonly plural?: Label;
998
- };
999
- readonly fields: readonly Field[];
1000
- }
1001
- /**
1002
- * A list of rows, each one of several block kinds (e.g. hero, text, gallery), for pages that
1003
- * editors lay out themselves. Rows are `{ id, blockType, ...fields }`, stored as JSON.
1004
- */
1005
- interface BlocksField extends BaseField<'blocks', never> {
1006
- readonly blocks: readonly Block[];
1007
- readonly minRows?: number;
1008
- readonly maxRows?: number;
1009
- }
1010
- /** The fields `uniqueWithin` names, as a list. */
1011
- declare const uniqueWithinOf: (field: Field) => readonly string[];
1012
- /** Fields with `hasMany`: their value is a list of options, documents or files. */
1013
- declare const isHasMany: (field: Field) => boolean;
1014
- /**
1015
- * Whether a MIME type is one of `patterns` (`image/*` matches every image but SVG, which can
1016
- * carry scripts and is allowed only when listed by name: `image/svg+xml`).
1017
- */
1018
- declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): boolean;
1019
- /** A relationship to several collections: its values are `{ relationTo, value }` (#327). */
1020
- declare const isPolymorphic: (field: Field) => field is RelationshipField & {
1021
- readonly to: readonly string[];
1022
- };
1023
- /** The collections a relationship or upload field points to. */
1024
- declare function relationTargets(field: Field): readonly string[];
1025
- /** A `{ relationTo, value }` of a relationship to several collections, or `undefined`. */
1026
- declare function polymorphicValue(value: unknown): {
1027
- relationTo: string;
1028
- value: unknown;
1029
- } | undefined;
1030
- /** Fields whose value is a list of rows: `array` and `blocks`. */
1031
- declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
1032
- /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
1033
- declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
1034
- /**
1035
- * Stores nothing: a component in the edit form (`admin.component`), e.g. a calculator, a preview
1036
- * or a button. Placed like any field (layout, groups, conditions); not in the database, the API or
1037
- * the generated types.
1038
- */
1039
- interface UiField extends BaseField<'ui', never> {
1040
- readonly admin: FieldAdmin & {
1041
- readonly component: AdminComponent;
1042
- };
1043
- }
1044
- type Field = CustomField | TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField | UiField;
1045
- type FieldType = Field['type'];
1046
- /**
1047
- * The shortest `like` on a field with `index: 'trigram'`: trigrams are three characters, so a
1048
- * shorter search can't use the index and would read every row.
1049
- */
1050
- declare const TRIGRAM_LIKE_MIN = 3;
1051
- declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks", "ui"];
1052
-
1053
905
  type Simplify<T> = {
1054
906
  [K in keyof T]: T[K];
1055
907
  } & {};
@@ -1469,8 +1321,315 @@ type RichTextBlocks<C extends Config, S extends CollectionSlug<C>, F extends str
1469
1321
  } ? {
1470
1322
  blockType: Slug;
1471
1323
  id: string;
1472
- fields: Simplify<FieldsValue<Fs, C, false>>;
1473
- } : never : never;
1324
+ fields: Simplify<FieldsValue<Fs, C, false>>;
1325
+ } : never : never;
1326
+
1327
+ type IntegrationCategory = 'social' | 'ai' | 'notifications' | 'automation';
1328
+ /**
1329
+ * A provider's logo: SVG paths, e.g. from simple-icons. Without `fill`, a path takes `color` (the
1330
+ * brand's), or the text color where that wouldn't show on the admin's background.
1331
+ */
1332
+ interface IntegrationLogo {
1333
+ readonly paths: readonly {
1334
+ readonly d: string;
1335
+ readonly fill?: string;
1336
+ }[];
1337
+ /** Default `0 0 24 24`. */
1338
+ readonly viewBox?: string;
1339
+ /** `#rrggbb`. */
1340
+ readonly color?: string;
1341
+ }
1342
+ /** A value to fill when connecting (`kind: 'fields'`). */
1343
+ interface IntegrationField {
1344
+ readonly name: string;
1345
+ readonly label?: Label;
1346
+ readonly description?: Label;
1347
+ /** `multiselect`: ticked options, kept as `a,b,c`. */
1348
+ readonly type?: 'text' | 'url' | 'textarea' | 'select' | 'multiselect';
1349
+ /** For `select` and `multiselect`: values, or `{ value, label }`. */
1350
+ readonly options?: readonly (string | {
1351
+ readonly value: string;
1352
+ readonly label: Label;
1353
+ })[];
1354
+ /**
1355
+ * Choices from the provider for this field (#356): a Find button beside it runs this with the
1356
+ * values typed so far, e.g. the chats a Telegram bot was written to.
1357
+ */
1358
+ readonly lookup?: (values: Readonly<Record<string, string>>, args: {
1359
+ cms: EasyCMS;
1360
+ }) => Promise<readonly {
1361
+ value: string;
1362
+ label: string;
1363
+ }[]> | readonly {
1364
+ value: string;
1365
+ label: string;
1366
+ }[];
1367
+ readonly placeholder?: string;
1368
+ readonly required?: boolean;
1369
+ /** Encrypted at rest and never sent back to the admin (shown as `••••1234`). */
1370
+ readonly secret?: boolean;
1371
+ }
1372
+ interface IntegrationTestResult {
1373
+ readonly ok: boolean;
1374
+ /** The account it reaches, e.g. a Page's or a bot's name. */
1375
+ readonly account?: string;
1376
+ /** Why it failed. */
1377
+ readonly message?: string;
1378
+ }
1379
+ /** What `exchange` gets back from the provider: one account, or several to choose from. */
1380
+ interface IntegrationOAuthResult {
1381
+ readonly values?: Readonly<Record<string, string>>;
1382
+ readonly account?: string;
1383
+ readonly accounts?: readonly {
1384
+ readonly id: string;
1385
+ readonly name: string;
1386
+ readonly values: Readonly<Record<string, string>>;
1387
+ }[];
1388
+ }
1389
+ /** An outside provider a plugin connects to (`integrations` in the config). */
1390
+ interface IntegrationConfig {
1391
+ /** Lowercase letters, digits and `-`, e.g. `telegram`. */
1392
+ readonly id: string;
1393
+ readonly label: Label;
1394
+ readonly category: IntegrationCategory;
1395
+ readonly description?: Label;
1396
+ /** Its logo (#363), or the name of an admin icon (`ADMIN_ICONS`). Without one: its first letter. */
1397
+ readonly icon?: string | IntegrationLogo;
1398
+ /** A link to its guide. */
1399
+ readonly docs?: string;
1400
+ /** The plugin it comes from, e.g. `@easy-cms/plugin-notify`. */
1401
+ readonly plugin?: string;
1402
+ /** `fields`: values to paste (a token, a URL); `oauth`: a Connect button. */
1403
+ readonly kind: 'fields' | 'oauth';
1404
+ readonly fields?: readonly IntegrationField[];
1405
+ /**
1406
+ * Environment variables by field: set, they win over the admin. With all of `fields`, the
1407
+ * provider has an "Environment" connection that can't be changed in the admin.
1408
+ */
1409
+ readonly env?: Readonly<Record<string, string>>;
1410
+ /** For `kind: 'oauth'`: the provider's sign-in page, and turning its code into tokens. */
1411
+ readonly oauth?: {
1412
+ readonly authorizeUrl: (args: {
1413
+ redirectUri: string;
1414
+ state: string;
1415
+ }) => string;
1416
+ readonly exchange: (args: {
1417
+ code: string;
1418
+ redirectUri: string;
1419
+ }) => Promise<IntegrationOAuthResult> | IntegrationOAuthResult;
1420
+ };
1421
+ /** Checks the values without posting anything; `name` is the connection's. */
1422
+ readonly test?: (values: Readonly<Record<string, string>>, args: {
1423
+ cms: EasyCMS;
1424
+ name?: string;
1425
+ }) => Promise<IntegrationTestResult> | IntegrationTestResult;
1426
+ /**
1427
+ * Asked before the first connection (#357), e.g. that text sent to an AI provider leaves the
1428
+ * site: an admin accepts it, and who did is audit-logged.
1429
+ */
1430
+ readonly consent?: {
1431
+ readonly text: Label;
1432
+ readonly policy?: string;
1433
+ };
1434
+ }
1435
+ /** A connection as plugins read it: its values, secrets decrypted. */
1436
+ interface IntegrationConnection {
1437
+ readonly name: string;
1438
+ readonly values: Readonly<Record<string, string>>;
1439
+ /** From environment variables. */
1440
+ readonly env: boolean;
1441
+ }
1442
+ interface AdminConnection {
1443
+ id: ID | 'env';
1444
+ name: string;
1445
+ isDefault: boolean;
1446
+ env: boolean;
1447
+ account: string | null;
1448
+ lastUsedAt: string | null;
1449
+ lastError: string | null;
1450
+ /** Plain values; secrets as `••••1234`. */
1451
+ values: Record<string, string>;
1452
+ /** AI providers (#357): requests and tokens this month. */
1453
+ usage?: {
1454
+ requests: number;
1455
+ input: number;
1456
+ output: number;
1457
+ };
1458
+ }
1459
+ /** A field as the admin gets it: without its functions. */
1460
+ type AdminIntegrationField = Omit<IntegrationField, 'lookup'> & {
1461
+ findable?: true;
1462
+ };
1463
+ interface AdminIntegration {
1464
+ id: string;
1465
+ label: Label;
1466
+ category: IntegrationCategory;
1467
+ description?: Label;
1468
+ icon?: string | IntegrationLogo;
1469
+ docs?: string;
1470
+ plugin?: string;
1471
+ kind: 'fields' | 'oauth';
1472
+ fields: AdminIntegrationField[];
1473
+ enabled: boolean;
1474
+ /** Fields set in the environment. */
1475
+ envFields: string[];
1476
+ testable: boolean;
1477
+ connections: AdminConnection[];
1478
+ /** Asked before the first connection; `consented` says who accepted it. */
1479
+ consent?: {
1480
+ text: Label;
1481
+ policy?: string;
1482
+ };
1483
+ consented?: {
1484
+ at: string;
1485
+ by: string;
1486
+ } | null;
1487
+ }
1488
+ /** What the code configures: shown, not changed, on the Integrations page. */
1489
+ interface ConfiguredService {
1490
+ id: string;
1491
+ kind: 'email' | 'storage' | 'search' | 'sign-in' | 'payments';
1492
+ name: string;
1493
+ }
1494
+ /** Official plugins, offered when they aren't installed. */
1495
+ declare const OFFICIAL_PLUGINS: readonly {
1496
+ name: string;
1497
+ category: IntegrationCategory | 'payments';
1498
+ description: Label;
1499
+ docs: string;
1500
+ }[];
1501
+ /** The connections of plugins' integrations: kept, read and tested by Easy CMS. */
1502
+ declare class Integrations {
1503
+ private readonly cms;
1504
+ constructor(cms: EasyCMS);
1505
+ private get config();
1506
+ private get secrets();
1507
+ /** The integrations plugins declared. */
1508
+ get definitions(): readonly IntegrationConfig[];
1509
+ private definition;
1510
+ private rows;
1511
+ /** The values of the fields set in the environment. */
1512
+ private envValues;
1513
+ /** Every field is in the environment: an "Environment" connection. */
1514
+ private envComplete;
1515
+ private enabled;
1516
+ private decrypt;
1517
+ private reseal;
1518
+ /** Splits values into the plain ones and the secret ones (encrypted). */
1519
+ private seal;
1520
+ /**
1521
+ * A connection for a plugin: the default one, or the one named; `null` when there is none
1522
+ * or the integration is turned off. Values from the environment win.
1523
+ */
1524
+ get(id: string, options?: {
1525
+ name?: string;
1526
+ }): Promise<IntegrationConnection | null>;
1527
+ /**
1528
+ * Every connection of an integration (the environment's first), e.g. to notify each chat;
1529
+ * none when it is turned off.
1530
+ */
1531
+ all(id: string): Promise<IntegrationConnection[]>;
1532
+ /** The connection was used: its card says when, and an earlier error is gone. */
1533
+ markUsed(id: string, name?: string): Promise<void>;
1534
+ /** It failed: the card and the dashboard's "needs attention" say why. */
1535
+ markError(id: string, message: string, name?: string): Promise<void>;
1536
+ private mark;
1537
+ overview(): Promise<{
1538
+ integrations: AdminIntegration[];
1539
+ configured: ConfiguredService[];
1540
+ catalog: typeof OFFICIAL_PLUGINS;
1541
+ staleSecrets: number;
1542
+ }>;
1543
+ /** The adapters the config sets: email, storage, search, sign-in, payments. */
1544
+ private configured;
1545
+ private audit;
1546
+ private checkFields;
1547
+ private cleanValues;
1548
+ private uniqueName;
1549
+ private create;
1550
+ /** Connects with pasted values (`kind: 'fields'`); tested first when the plugin can. */
1551
+ connect(id: string, body: unknown, user: AuthUser): Promise<{
1552
+ id: ID;
1553
+ }>;
1554
+ private row;
1555
+ /** A new name or values (secrets left empty are kept), or the default. */
1556
+ update(id: string, cid: ID, body: unknown, user: AuthUser): Promise<{
1557
+ ok: boolean;
1558
+ }>;
1559
+ /** Removes the connection and its secrets; the oldest left becomes the default. */
1560
+ disconnect(id: string, cid: ID, user: AuthUser): Promise<{
1561
+ deleted: number;
1562
+ }>;
1563
+ /** Turns the integration on or off; its connections are kept. */
1564
+ setEnabled(id: string, enabled: boolean, user: AuthUser): Promise<{
1565
+ enabled: boolean;
1566
+ }>;
1567
+ /** Who accepted the integration's `consent`, and when; `null` before anyone did. */
1568
+ private consented;
1569
+ private checkConsent;
1570
+ /** An admin accepts the integration's `consent`; audit-logged. */
1571
+ consent(id: string, user: AuthUser): Promise<{
1572
+ consented: {
1573
+ at: string;
1574
+ by: string;
1575
+ };
1576
+ }>;
1577
+ /** The integration's own row (on or off, consent), made when first needed. */
1578
+ private saveIntegration;
1579
+ /** Checks a connection's values with the provider; the card shows the result. */
1580
+ test(id: string, cid: ID | 'env'): Promise<IntegrationTestResult>;
1581
+ /**
1582
+ * A field's choices from the provider, with the values typed (and, editing a connection, its
1583
+ * stored secrets for the fields left empty).
1584
+ */
1585
+ lookup(id: string, fieldName: string, body: unknown): Promise<{
1586
+ choices: ({
1587
+ value: string;
1588
+ label: string;
1589
+ } | {
1590
+ value: string;
1591
+ label: string;
1592
+ })[];
1593
+ }>;
1594
+ /** The provider's sign-in page to send the admin to. */
1595
+ authorize(id: string, user: AuthUser, redirectUri: string, name: string): Promise<{
1596
+ url: string;
1597
+ }>;
1598
+ /**
1599
+ * Back from the provider: one account is connected at once; several wait (encrypted, 10
1600
+ * minutes) for the admin to choose. Returns where the admin goes next.
1601
+ */
1602
+ callback(id: string, code: string, stateText: string, redirectUri: string, user: AuthUser): Promise<{
1603
+ connected: true;
1604
+ pending?: never;
1605
+ } | {
1606
+ pending: ID;
1607
+ connected?: never;
1608
+ }>;
1609
+ /** The accounts waiting to be chosen from, by name only. */
1610
+ pending(id: string, pid: ID, user: AuthUser): Promise<{
1611
+ name: string;
1612
+ accounts: {
1613
+ id: string;
1614
+ name: string;
1615
+ }[];
1616
+ }>;
1617
+ private pendingRow;
1618
+ /** Connects the account chosen. */
1619
+ choose(id: string, pid: ID, accountId: string, user: AuthUser): Promise<{
1620
+ id: ID;
1621
+ }>;
1622
+ /** Encrypts again, with the current secret, every connection still on an older one. */
1623
+ rotate(): Promise<{
1624
+ resealed: number;
1625
+ }>;
1626
+ /** Connections whose test or last use failed, for the dashboard's "needs attention". */
1627
+ broken(): Promise<{
1628
+ integration: string;
1629
+ name: string;
1630
+ error: string;
1631
+ }[]>;
1632
+ }
1474
1633
 
1475
1634
  /** What a role may do with a collection or global, ticked in Settings → Roles. */
1476
1635
  declare const ROLE_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
@@ -1494,6 +1653,8 @@ interface RolePermissions {
1494
1653
  readonly collections?: FieldRules;
1495
1654
  readonly globals?: FieldRules;
1496
1655
  };
1656
+ /** AI requests a user of the role makes a day (with `ai`); 0: none. Unset: everyone's limit. */
1657
+ readonly aiPerDay?: number;
1497
1658
  }
1498
1659
  /** One role as Settings → Roles shows it (`GET <api>/admin/roles`). */
1499
1660
  interface AdminRole {
@@ -1543,6 +1704,8 @@ interface AdminRoles {
1543
1704
  })[];
1544
1705
  globals: AdminRoleTarget[];
1545
1706
  views: AdminRoleView[];
1707
+ /** Roles can be given a daily AI limit (`ai` in the config). */
1708
+ ai: boolean;
1546
1709
  }
1547
1710
  /** One change to a role (`GET <api>/admin/roles/:id/history`). */
1548
1711
  interface AdminRoleChange {
@@ -1642,6 +1805,8 @@ declare class Roles {
1642
1805
  /** Sets one field of every matching document, keeping the rest as stored. */
1643
1806
  private setWhere;
1644
1807
  private cleanName;
1808
+ /** The role's daily AI limit set in Settings → Roles, if any (`auth.rbac`). */
1809
+ aiLimit(key: string): Promise<number | undefined>;
1645
1810
  private permissionsOf;
1646
1811
  private load;
1647
1812
  private rows;
@@ -1901,6 +2066,18 @@ interface SearchResults {
1901
2066
  readonly limit: number;
1902
2067
  readonly hasNextPage: boolean;
1903
2068
  }
2069
+ /** What a listener of `cms.on` gets: the event's name, its data, and the document it is about. */
2070
+ interface EventMessage {
2071
+ readonly event: string;
2072
+ readonly data: Record<string, unknown>;
2073
+ /** The document or global it is about, when there is one. */
2074
+ readonly about?: {
2075
+ readonly collection?: string;
2076
+ readonly id?: ID;
2077
+ readonly global?: string;
2078
+ };
2079
+ }
2080
+ type EventListener = (message: EventMessage) => unknown;
1904
2081
  interface CreateEasyCMSOptions {
1905
2082
  /** Project root. Default `process.cwd()`. */
1906
2083
  readonly cwd?: string;
@@ -1950,6 +2127,10 @@ declare class EasyCMS<C extends Config = Config> {
1950
2127
  readonly audit: AuditLog;
1951
2128
  /** Folders of the media library and their permissions (`upload.folders`). */
1952
2129
  readonly folders: MediaFolders;
2130
+ /** Accounts connected to plugins' integrations (`integrations`, Settings → Integrations). */
2131
+ readonly integrations: Integrations;
2132
+ /** AI for the admin's features (#357): `null` without an AI plugin (`ai` in the config). */
2133
+ readonly ai: Ai | null;
1953
2134
  private readonly versions;
1954
2135
  private readonly webhooks;
1955
2136
  private readonly mailer;
@@ -2305,6 +2486,17 @@ declare class EasyCMS<C extends Config = Config> {
2305
2486
  collection: string;
2306
2487
  id: ID;
2307
2488
  }): void;
2489
+ private readonly listeners;
2490
+ /** Listeners of every event (`events: '*'` in the config's `listeners`). */
2491
+ private readonly anyListeners;
2492
+ /**
2493
+ * Runs `handler` after each `event` on this server (#356): the config's and plugins' events, the
2494
+ * content changes webhooks get (`create`, `publish`…), and Easy CMS's own (`scheduled.failed`,
2495
+ * `delivery.failed`, `backup.failed`, `integration.failed`). It runs after the change, without
2496
+ * being waited for; its errors are logged. Returns a function that stops it.
2497
+ */
2498
+ on(event: string, handler: EventListener): () => void;
2499
+ private dispatch;
2308
2500
  /**
2309
2501
  * Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
2310
2502
  * database until they succeed, so they survive restarts.
@@ -2489,155 +2681,472 @@ interface LivePreview<T> {
2489
2681
  /** From the collection's `preview` function; `null` without one. */
2490
2682
  readonly url: string | null;
2491
2683
  }
2492
-
2493
- /** Audit log settings (`audit`). */
2494
- interface AuditConfig {
2495
- /** Days to keep entries; older ones are deleted. `0`: keep them all. Default 365. */
2496
- readonly keepDays?: number;
2497
- /** Keep the values before and after a change, not only which fields changed. Default `true`. */
2498
- readonly values?: boolean;
2499
- /** Failed logins within an hour that the dashboard warns about. Default 20. */
2500
- readonly failedLogins?: number;
2684
+
2685
+ /** A label shown in the admin UI. Either one string or one string per admin locale. */
2686
+ type Label = string | {
2687
+ readonly [locale: string]: string;
2688
+ };
2689
+ interface FieldValidateContext {
2690
+ /** The whole document being validated, including sibling fields. */
2691
+ readonly data: Readonly<Record<string, unknown>>;
2692
+ readonly operation: 'create' | 'update';
2693
+ }
2694
+ /** Return `true` when valid, or an error message to show on the field. */
2695
+ type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
2696
+ /**
2697
+ * A Web Component from an admin module (`admin.modules`), by its tag name, which must start
2698
+ * with `ecms-`. `props` (plain JSON) reach the element as its `options` property.
2699
+ */
2700
+ type AdminComponent = string | {
2701
+ readonly tag: string;
2702
+ readonly props?: Readonly<Record<string, unknown>>;
2703
+ };
2704
+ /** How the admin shows a field, with components from admin modules. */
2705
+ interface FieldAdmin {
2706
+ /** Help below the field's label. */
2707
+ readonly description?: Label;
2708
+ /** Its share of a row (`admin.layout`'s `row`). Default: an equal share. */
2709
+ readonly width?: '1/4' | '1/3' | '1/2' | '2/3' | '3/4' | 'full';
2710
+ /**
2711
+ * Shown only when its sibling fields match, e.g. `{ field: 'linkType', equals: 'external' }`.
2712
+ * A hidden field isn't required. See `FieldCondition`.
2713
+ */
2714
+ readonly condition?: FieldCondition;
2715
+ /** Shown instead of the field's input; the admin keeps the label and error messages. */
2716
+ readonly component?: AdminComponent;
2717
+ /** Shown below the field, e.g. a length meter or a preview. */
2718
+ readonly after?: readonly AdminComponent[];
2719
+ /** Shows the value in the admin's lists (e.g. a color swatch). Default: the plain value. */
2720
+ readonly cell?: AdminComponent;
2721
+ /**
2722
+ * A column of the list at first (each user can change it): `true`, or per request, e.g.
2723
+ * only when the admin shows every tenant.
2724
+ */
2725
+ readonly column?: boolean | ((args: AdminFieldArgs) => boolean | Promise<boolean>);
2726
+ /** Relationships: offer to create the related document in place. Default true. */
2727
+ readonly allowCreate?: boolean;
2728
+ /**
2729
+ * The value a new document's form starts with, worked out for each user, e.g. from the
2730
+ * request's context. The server's own default (`defaultValue`, hooks) still applies.
2731
+ */
2732
+ readonly initialValue?: (args: AdminFieldArgs) => unknown;
2733
+ /** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
2734
+ readonly position?: 'sidebar';
2735
+ /** `false`: the list's filters don't offer it. Default: every field the user may read. */
2736
+ readonly filter?: boolean;
2737
+ }
2738
+ /** What per-request admin options of a field receive. */
2739
+ interface AdminFieldArgs {
2740
+ readonly user: AuthUser;
2741
+ readonly context: RequestContext;
2742
+ }
2743
+ /**
2744
+ * Field types that packages add (`fieldTypes` in the config), for the types inferred from the
2745
+ * config. A package declares its own by augmenting this interface:
2746
+ *
2747
+ * ```ts
2748
+ * declare module '@easy-cms/core' {
2749
+ * interface CustomFieldTypes {
2750
+ * color: { value: string; options: { readonly presets?: readonly string[] } }
2751
+ * }
2752
+ * }
2753
+ * ```
2754
+ */
2755
+ interface CustomFieldTypes {
2756
+ }
2757
+ /** A field of a type from `CustomFieldTypes`: the common options, plus the type's own. */
2758
+ type CustomField = {
2759
+ [K in keyof CustomFieldTypes]: BaseField<K & string, CustomFieldTypes[K] extends {
2760
+ value: infer V;
2761
+ } ? V : unknown> & (CustomFieldTypes[K] extends {
2762
+ options: infer O;
2763
+ } ? O : unknown);
2764
+ }[keyof CustomFieldTypes];
2765
+ interface BaseField<TType extends string, TValue> {
2766
+ readonly type: TType;
2767
+ readonly name: string;
2768
+ readonly label?: Label;
2769
+ readonly required?: boolean;
2770
+ readonly unique?: boolean;
2771
+ /**
2772
+ * With `unique` (or on a slug field): a sibling field (e.g. `parent` or `tenant`), or several,
2773
+ * whose documents only need to differ among themselves, so `/about/team` and `/careers/team`
2774
+ * can both be `team`. Single, unlocalized relationship, select, text or number fields.
2775
+ */
2776
+ readonly uniqueWithin?: string | readonly string[];
2777
+ /**
2778
+ * `true` for a database index (filters and sorts on this field). `'trigram'`, on text, textarea
2779
+ * and email fields: an index for `like`, which finds any part of the text, in Thai as in other
2780
+ * languages. Postgres only (pg_trgm); nothing on SQLite. There, `like` needs at least three
2781
+ * characters, and the admin's searches wait for them.
2782
+ */
2783
+ readonly index?: boolean | 'trigram';
2784
+ readonly defaultValue?: TValue;
2785
+ readonly validate?: FieldValidate<TValue>;
2786
+ readonly access?: FieldAccess;
2787
+ /** Stored but never returned by the API nor accepted as input (e.g. a password hash). */
2788
+ readonly hidden?: boolean;
2789
+ /**
2790
+ * One value per locale (needs `localization` in the config). Not for groups, arrays or
2791
+ * hasMany fields: localize the fields inside a group or array instead.
2792
+ */
2793
+ readonly localized?: boolean;
2794
+ /** How the field looks in the admin: components, position, width, conditions. */
2795
+ readonly admin?: FieldAdmin;
2796
+ /** Set by Easy CMS on fields of an added type (`fieldTypes`): the type's name, e.g. `color`. */
2797
+ readonly customType?: string;
2798
+ }
2799
+ interface TextField extends BaseField<'text', string> {
2800
+ readonly minLength?: number;
2801
+ readonly maxLength?: number;
2802
+ }
2803
+ interface TextareaField extends BaseField<'textarea', string> {
2804
+ readonly minLength?: number;
2805
+ readonly maxLength?: number;
2806
+ }
2807
+ interface NumberField extends BaseField<'number', number> {
2808
+ readonly min?: number;
2809
+ readonly max?: number;
2810
+ }
2811
+ interface BooleanField extends BaseField<'boolean', boolean> {
2812
+ }
2813
+ /** Stored and returned as an ISO 8601 string. */
2814
+ interface DateField extends BaseField<'date', string> {
2815
+ }
2816
+ interface EmailField extends BaseField<'email', string> {
2817
+ }
2818
+ interface JsonField extends BaseField<'json', unknown> {
2819
+ }
2820
+ type SelectOption = string | {
2821
+ readonly label: Label;
2822
+ readonly value: string;
2823
+ };
2824
+ interface SelectField extends BaseField<'select', string | readonly string[]> {
2825
+ readonly options: readonly SelectOption[];
2826
+ readonly hasMany?: boolean;
2827
+ }
2828
+ interface SlugField extends BaseField<'slug', string> {
2829
+ /** Name of a sibling `text` field to generate the slug from. */
2830
+ readonly from?: string;
2831
+ }
2832
+ /** Tiptap / ProseMirror JSON document. */
2833
+ interface RichTextDocument {
2834
+ readonly type: 'doc';
2835
+ readonly content?: readonly unknown[];
2836
+ }
2837
+ interface RichTextField extends BaseField<'richText', RichTextDocument> {
2838
+ /**
2839
+ * Blocks editors place between paragraphs (#328): a card, a callout, a video. Each is a node
2840
+ * `{ type: 'block', attrs: { blockType, id, fields } }` in the document.
2841
+ */
2842
+ readonly blocks?: readonly RichTextBlock[];
2843
+ }
2844
+ /** Arguments of a rich text block's `admin.summary` function. */
2845
+ interface BlockSummaryArgs {
2846
+ readonly fields: Record<string, unknown>;
2847
+ readonly user: AuthUser | null;
2848
+ readonly cms: EasyCMS;
2849
+ }
2850
+ /** A block in rich text (`richText.blocks`): the fields of `Block`, and how its card reads. */
2851
+ interface RichTextBlock extends Block {
2852
+ readonly admin?: {
2853
+ /**
2854
+ * The text of the block's card in the editor: a function, run on the server, or the tag of a
2855
+ * custom element from an admin module, which gets `fields`. Default: the block's label and
2856
+ * its first value.
2857
+ */
2858
+ readonly summary?: string | ((args: BlockSummaryArgs) => string | Promise<string>);
2859
+ };
2860
+ }
2861
+ /** References a document in the built-in `media` collection: a file or an image. */
2862
+ interface UploadField extends BaseField<'upload', never> {
2863
+ /** Several files, e.g. a gallery: the value is a list, in the order editors arrange. */
2864
+ readonly hasMany?: boolean;
2865
+ /** With `hasMany`: the fewest files. */
2866
+ readonly minRows?: number;
2867
+ /** With `hasMany`: the most files. */
2868
+ readonly maxRows?: number;
2869
+ /**
2870
+ * The file types allowed, e.g. `['image/*']` or `['image/*', 'application/pdf']`. The admin
2871
+ * offers only these, and saving refuses others.
2872
+ */
2873
+ readonly mimeTypes?: readonly string[];
2874
+ /**
2875
+ * With `upload.folders`: the `key` of the media folder this field's files go in, e.g.
2876
+ * `'banners'`. Its picker opens there and its uploads land there; the folder is made when it
2877
+ * is first needed.
2878
+ */
2879
+ readonly folder?: string;
2880
+ /** With `folder`: only files in that folder (and its subfolders) may be chosen. */
2881
+ readonly folderOnly?: boolean;
2882
+ /** Which media documents may be chosen, as for relationships: offered and checked on save. */
2883
+ readonly filterOptions?: FilterOptions;
2884
+ }
2885
+ interface FilterOptionsArgs {
2886
+ /** The document being edited; `undefined` while it is being created. */
2887
+ readonly id: ID | undefined;
2888
+ /** The collection the choices come from: one of the field's `to`. */
2889
+ readonly relationTo: string;
2890
+ readonly user: AuthUser | null;
2891
+ readonly cms: EasyCMS;
2892
+ /** The request's context (`onRequest`), `{}` when there is none. */
2893
+ readonly context: RequestContext;
2894
+ }
2895
+ /**
2896
+ * Which documents a relationship may point to: a `where` on the target collection, or `true`
2897
+ * for any. Runs on the server: the admin's picker offers only these, and saving checks them.
2898
+ */
2899
+ type FilterOptions = (args: FilterOptionsArgs) => Where | true | Promise<Where | true>;
2900
+ interface RelationshipField extends BaseField<'relationship', never> {
2501
2901
  /**
2502
- * The part of the site an entry belongs to, from the request's context, e.g. its tenant
2503
- * (usually set by a plugin). Users whose role holds in a part only (`scoped`) see that part's
2504
- * entries; others see the chosen part's, or all.
2902
+ * Slug of the target collection, or several: then each value is `{ relationTo, value }`, the
2903
+ * collection and the document's id (#327).
2505
2904
  */
2506
- readonly scope?: ((args: {
2507
- readonly context: RequestContext;
2508
- readonly user: AuthUser | null;
2509
- }) => string | null | undefined) | false;
2905
+ readonly to: string | readonly string[];
2906
+ readonly hasMany?: boolean;
2907
+ /** With `hasMany`: the fewest documents. */
2908
+ readonly minRows?: number;
2909
+ /** With `hasMany`: the most documents. */
2910
+ readonly maxRows?: number;
2911
+ readonly filterOptions?: FilterOptions;
2510
2912
  }
2511
- /** A field that changed: values before and after, unless they are not kept. */
2512
- interface AuditChange {
2513
- field: string;
2514
- before?: unknown;
2515
- after?: unknown;
2913
+ interface ArrayField extends BaseField<'array', never> {
2914
+ readonly fields: readonly Field[];
2915
+ readonly minRows?: number;
2916
+ readonly maxRows?: number;
2516
2917
  }
2517
- /** One entry (`GET <api>/admin/audit`). */
2518
- interface AuditEntry {
2519
- id: ID;
2520
- /** When, as an ISO date. */
2521
- at: string;
2522
- /** E.g. `create`, `update`, `publish`, `delete`, `login`, `login.failed`, `role.update`. */
2523
- action: string;
2524
- /** A collection slug, `global:<slug>`, or an area such as `auth`, `roles`, `backups`. */
2525
- target: string | null;
2526
- /** The document's id. */
2527
- doc: string | null;
2528
- /** The document's title then (kept after it is deleted). */
2529
- title: string | null;
2530
- actor: {
2531
- id: string | null;
2532
- email: string | null;
2533
- /** `user` (admin or REST), `api-key`, `system` (code), `scheduler`. */
2534
- via: string;
2535
- ip: string | null;
2536
- userAgent: string | null;
2918
+ interface GroupField extends BaseField<'group', never> {
2919
+ readonly fields: readonly Field[];
2920
+ }
2921
+ /** One kind of block in a `blocks` field. */
2922
+ interface Block {
2923
+ /** Stored in each row as `blockType`. */
2924
+ readonly slug: string;
2925
+ readonly labels?: {
2926
+ readonly singular?: Label;
2927
+ readonly plural?: Label;
2537
2928
  };
2538
- changes: AuditChange[] | null;
2539
- detail: Record<string, unknown> | null;
2929
+ readonly fields: readonly Field[];
2540
2930
  }
2541
- /** `GET <api>/admin/audit`: a page of entries, newest first. */
2542
- /** A page of the audit log: the list shape of the rest of the API. */
2543
- type AuditPage = PaginatedDocs<AuditEntry>;
2544
- interface AuditFilter {
2545
- action?: string | null;
2546
- target?: string | null;
2547
- doc?: string | null;
2548
- /** An email, or part of one. */
2549
- actor?: string | null;
2550
- /** ISO dates. */
2551
- from?: string | null;
2552
- to?: string | null;
2553
- /** Only entries of this part of the site (`scope`). */
2554
- scope?: string | null;
2931
+ /**
2932
+ * A list of rows, each one of several block kinds (e.g. hero, text, gallery), for pages that
2933
+ * editors lay out themselves. Rows are `{ id, blockType, ...fields }`, stored as JSON.
2934
+ */
2935
+ interface BlocksField extends BaseField<'blocks', never> {
2936
+ readonly blocks: readonly Block[];
2937
+ readonly minRows?: number;
2938
+ readonly maxRows?: number;
2555
2939
  }
2556
- /** `POST <api>/admin/audit/verify`. */
2557
- interface AuditVerification {
2558
- checked: number;
2559
- /** Entries whose signature does not match: changed after they were written. */
2560
- invalid: ID[];
2561
- /**
2562
- * Missing ids between entries: deleted entries, or (rarely) inserts that failed. Ids are never
2563
- * reused, so the newest entries deleted show up here once anything is written after them (a
2564
- * check writes an entry itself). The server's log has each check's last id, to compare with.
2565
- */
2566
- gaps: number;
2567
- at: string;
2940
+ /** The fields `uniqueWithin` names, as a list. */
2941
+ declare const uniqueWithinOf: (field: Field) => readonly string[];
2942
+ /** Fields with `hasMany`: their value is a list of options, documents or files. */
2943
+ declare const isHasMany: (field: Field) => boolean;
2944
+ /**
2945
+ * Whether a MIME type is one of `patterns` (`image/*` matches every image but SVG, which can
2946
+ * carry scripts and is allowed only when listed by name: `image/svg+xml`).
2947
+ */
2948
+ declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): boolean;
2949
+ /** A relationship to several collections: its values are `{ relationTo, value }` (#327). */
2950
+ declare const isPolymorphic: (field: Field) => field is RelationshipField & {
2951
+ readonly to: readonly string[];
2952
+ };
2953
+ /** The collections a relationship or upload field points to. */
2954
+ declare function relationTargets(field: Field): readonly string[];
2955
+ /** A `{ relationTo, value }` of a relationship to several collections, or `undefined`. */
2956
+ declare function polymorphicValue(value: unknown): {
2957
+ relationTo: string;
2958
+ value: unknown;
2959
+ } | undefined;
2960
+ /** Fields whose value is a list of rows: `array` and `blocks`. */
2961
+ declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
2962
+ /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
2963
+ declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
2964
+ /**
2965
+ * Stores nothing: a component in the edit form (`admin.component`), e.g. a calculator, a preview
2966
+ * or a button. Placed like any field (layout, groups, conditions); not in the database, the API or
2967
+ * the generated types.
2968
+ */
2969
+ interface UiField extends BaseField<'ui', never> {
2970
+ readonly admin: FieldAdmin & {
2971
+ readonly component: AdminComponent;
2972
+ };
2568
2973
  }
2974
+ type Field = CustomField | TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField | UiField;
2975
+ type FieldType = Field['type'];
2569
2976
  /**
2570
- * The audit log (`audit` in the config): who did what, when, from where. Entries can't be
2571
- * changed through Easy CMS; each is signed with the secret, so changes in the database show.
2572
- * Available as `cms.audit`.
2977
+ * The shortest `like` on a field with `index: 'trigram'`: trigrams are three characters, so a
2978
+ * shorter search can't use the index and would read every row.
2573
2979
  */
2574
- declare class AuditLog {
2980
+ declare const TRIGRAM_LIKE_MIN = 3;
2981
+ declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks", "ui"];
2982
+
2983
+ /** What a provider is asked: the prompt, images as base64, and a JSON schema for the answer. */
2984
+ interface AiProviderRequest {
2985
+ readonly system?: string;
2986
+ readonly prompt: string;
2987
+ readonly images: readonly {
2988
+ readonly data: string;
2989
+ readonly mediaType: string;
2990
+ }[];
2991
+ /** A JSON schema: the answer is JSON of this shape. */
2992
+ readonly json?: Readonly<Record<string, unknown>>;
2993
+ readonly maxTokens: number;
2994
+ readonly signal: AbortSignal;
2995
+ }
2996
+ interface AiProviderResult {
2997
+ readonly text: string;
2998
+ readonly usage: {
2999
+ readonly input: number;
3000
+ readonly output: number;
3001
+ };
3002
+ }
3003
+ /** A provider's refusal: `status` says whether to retry (429, 5xx) or the key is wrong (401, 403). */
3004
+ declare class AiProviderError extends Error {
3005
+ readonly status: number | undefined;
3006
+ constructor(message: string, status?: number);
3007
+ }
3008
+ /** A provider a plugin adds: its integration (fields, Test) and how it answers. */
3009
+ interface AiProvider {
3010
+ /** The id of the integration whose connections it answers with, e.g. `anthropic`. */
3011
+ readonly integration: string;
3012
+ readonly generate: (values: Readonly<Record<string, string>>, request: AiProviderRequest) => Promise<AiProviderResult>;
3013
+ }
3014
+ /** `ai` in the config, set by the AI plugin. */
3015
+ interface AiConfig {
3016
+ readonly providers: readonly AiProvider[];
3017
+ readonly limits?: {
3018
+ /** Requests a user makes a day (in `timeZone`). Admins can change it. Default 100. */
3019
+ readonly perUserPerDay?: number;
3020
+ /** Per role, e.g. `{ editor: 50 }`; 0 keeps a role from using AI. Settings → Roles wins. */
3021
+ readonly roles?: Readonly<Record<string, number>>;
3022
+ };
3023
+ }
3024
+ /** A feature that uses AI (`aiFeatures` in the config): admins turn it off or pick its connection. */
3025
+ interface AiFeatureConfig {
3026
+ /** Lowercase letters, digits and `-`, e.g. `alt-text`. */
3027
+ readonly id: string;
3028
+ readonly label: Label;
3029
+ readonly description?: Label;
3030
+ /** On until an admin turns it off. Default true. */
3031
+ readonly default?: boolean;
3032
+ }
3033
+ interface AiImage {
3034
+ /** A media document: its smallest size at least 1024 pixels wide, or the file. */
3035
+ readonly media?: ID;
3036
+ /** Or the image itself, as base64. */
3037
+ readonly data?: string;
3038
+ readonly mediaType?: string;
3039
+ }
3040
+ interface AiRequest {
3041
+ /** The feature asking (`aiFeatures`): it must be on. */
3042
+ readonly feature: string;
3043
+ readonly system?: string;
3044
+ readonly prompt: string;
3045
+ readonly images?: readonly AiImage[];
3046
+ /** A JSON schema: the answer is parsed into `json`. */
3047
+ readonly json?: Readonly<Record<string, unknown>>;
3048
+ /** Default 1024. */
3049
+ readonly maxTokens?: number;
3050
+ }
3051
+ interface AiResult {
3052
+ readonly text: string;
3053
+ readonly json?: unknown;
3054
+ readonly usage: {
3055
+ readonly input: number;
3056
+ readonly output: number;
3057
+ };
3058
+ /** The connection that answered, e.g. `anthropic:Main`. */
3059
+ readonly connection: string;
3060
+ }
3061
+ interface AdminAi {
3062
+ main: string | null;
3063
+ perUserPerDay: number;
3064
+ /** The config's, for "back to the default". */
3065
+ defaultPerUserPerDay: number;
3066
+ connections: {
3067
+ id: string;
3068
+ integration: string;
3069
+ name: string;
3070
+ label: Label;
3071
+ }[];
3072
+ features: {
3073
+ id: string;
3074
+ label: Label;
3075
+ description?: Label;
3076
+ enabled: boolean;
3077
+ connection: string | null;
3078
+ }[];
3079
+ /** This month, in `timeZone`. */
3080
+ usage: {
3081
+ byUser: {
3082
+ user: string;
3083
+ requests: number;
3084
+ input: number;
3085
+ output: number;
3086
+ }[];
3087
+ byFeature: {
3088
+ feature: string;
3089
+ requests: number;
3090
+ input: number;
3091
+ output: number;
3092
+ }[];
3093
+ };
3094
+ }
3095
+ declare class Ai {
2575
3096
  private readonly cms;
2576
- /** Entries that could not be written since the server started. */
2577
- failures: number;
2578
- constructor(cms: EasyCMS);
2579
- get enabled(): boolean;
2580
- private get settings();
2581
- /**
2582
- * Writes an entry. The user defaults to the request's. Never throws: a failure is logged and
2583
- * counted, and the dashboard says so.
2584
- */
2585
- record(entry: {
2586
- action: string;
2587
- target?: string | null;
2588
- doc?: ID | null;
2589
- title?: string | null;
2590
- user?: AuthUser | null;
2591
- /** Who it was when there is no user, e.g. the email of a failed login. */
2592
- email?: string | null;
2593
- changes?: AuditChange[] | null;
2594
- detail?: Record<string, unknown> | null;
2595
- /** The call's context, when it is not the request's. */
2596
- context?: RequestContext;
2597
- }): Promise<void>;
2598
- /** Entries, newest first: `page` (from 1) of `limit` (default 50, at most 100). */
2599
- list(options?: AuditFilter & {
2600
- page?: number;
2601
- limit?: number;
2602
- }): Promise<AuditPage>;
2603
- /** The filtered entries as CSV (the newest 10,000). */
2604
- csv(filter: AuditFilter): Promise<string>;
2605
- /** Checks every entry's signature, and that no ids are missing. */
2606
- verify(): Promise<AuditVerification>;
2607
- /** The secret an entry was signed with: the current one, a previous one, or none (changed). */
2608
- private signer;
2609
- /**
2610
- * After `secret` changed: signs again, with the current secret, the entries signed with one of
2611
- * `previousSecrets`, so they still verify once it is removed. Entries no secret verifies were
2612
- * changed: they are left as they are, and reported. Records `audit.resign`.
2613
- */
2614
- resign(): Promise<{
2615
- checked: number;
2616
- resigned: number;
2617
- invalid: ID[];
3097
+ private readonly ai;
3098
+ constructor(cms: EasyCMS, ai: AiConfig);
3099
+ private get config();
3100
+ private get defaultLimit();
3101
+ /** The features of the config. */
3102
+ get features(): readonly AiFeatureConfig[];
3103
+ private settingsRow;
3104
+ private settings;
3105
+ /** Every connection of every provider, in the providers' order. */
3106
+ private connections;
3107
+ /** Whether a feature is on: admins' choice, else its `default`. */
3108
+ enabled(feature: string): Promise<boolean>;
3109
+ /** Requests a user may make today: their role's, else everyone's. */
3110
+ limitFor(user: AuthUser): Promise<number>;
3111
+ /** Requests the user made today (in `timeZone`) and how many are left. */
3112
+ remaining(user: AuthUser, now?: Date): Promise<{
3113
+ used: number;
3114
+ limit: number;
2618
3115
  }>;
2619
3116
  /**
2620
- * What the dashboard warns about: many failed logins, sign-ins of break-glass accounts in the
2621
- * last day, entries not written, a failed check.
3117
+ * Asks the feature's connection (or the main one). With `user`, the request counts toward their
3118
+ * daily limit; it is refused once they reach it. Usage is recorded, never the prompt.
2622
3119
  */
2623
- attention(now?: Date): Promise<({
2624
- id: 'failed-logins';
2625
- count: number;
2626
- } | {
2627
- id: 'break-glass';
2628
- count: number;
2629
- emails: string[];
2630
- } | {
2631
- id: 'audit-failures';
2632
- count: number;
2633
- } | {
2634
- id: 'audit-tampered';
2635
- invalid: number;
2636
- at: string;
2637
- })[]>;
2638
- private sign;
2639
- private lastVerification;
2640
- private where;
3120
+ generate(request: AiRequest, options?: {
3121
+ user?: AuthUser | null;
3122
+ about?: {
3123
+ collection: string;
3124
+ id: ID;
3125
+ };
3126
+ }): Promise<AiResult>;
3127
+ /**
3128
+ * The Test button of a provider's connection: a short prompt; its tokens count on the card,
3129
+ * not toward anyone's limit.
3130
+ */
3131
+ test(integration: string, values: Readonly<Record<string, string>>, name?: string): Promise<IntegrationTestResult>;
3132
+ private connectionFor;
3133
+ private ask;
3134
+ /** One try, and another after a second when the provider is busy or failing (429, 5xx). */
3135
+ private call;
3136
+ /** An image as base64: given, or read from a media document. */
3137
+ private image;
3138
+ /** Settings → Integrations, AI: the main connection, the features, the limit and this month. */
3139
+ overview(now?: Date): Promise<AdminAi>;
3140
+ /** Tokens and requests of each connection this month, for the cards. */
3141
+ usageByConnection(now?: Date): Promise<Map<string, {
3142
+ requests: number;
3143
+ input: number;
3144
+ output: number;
3145
+ }>>;
3146
+ /** Saves admins' choices; audit-logged. */
3147
+ update(body: unknown, user: AuthUser): Promise<AdminAi>;
3148
+ /** Forgets usage older than 90 days (with the scheduled jobs). */
3149
+ upkeep(now?: Date): Promise<void>;
2641
3150
  }
2642
3151
 
2643
3152
  /** What the email functions in `auth.emails` receive. */
@@ -3792,6 +4301,16 @@ interface Config {
3792
4301
  readonly events?: readonly string[];
3793
4302
  /** Work run with the scheduled jobs, e.g. a plugin's clean-up. */
3794
4303
  readonly jobs?: readonly JobConfig[];
4304
+ /**
4305
+ * Run after events on this server (#356), as `cms.on` does: plugins use this, e.g. to send a
4306
+ * notification. `events`: names, or `'*'` for every event.
4307
+ */
4308
+ readonly listeners?: readonly {
4309
+ readonly events: readonly string[] | '*';
4310
+ readonly handler: (message: EventMessage, args: {
4311
+ readonly cms: EasyCMS;
4312
+ }) => unknown;
4313
+ }[];
3795
4314
  /** Content in several languages: fields with `localized: true` hold one value per locale. */
3796
4315
  readonly localization?: LocalizationConfig | false;
3797
4316
  readonly routes?: RoutesConfig;
@@ -3893,9 +4412,24 @@ interface Config {
3893
4412
  */
3894
4413
  readonly telemetry?: boolean | TelemetryConfig;
3895
4414
  readonly plugins?: readonly Plugin[];
4415
+ /**
4416
+ * Outside providers admins connect accounts to on the Integrations page (#311), usually added
4417
+ * by plugins: a Telegram bot, an AI provider, a Facebook Page. See `IntegrationConfig`.
4418
+ */
4419
+ readonly integrations?: readonly IntegrationConfig[];
4420
+ /**
4421
+ * AI providers and limits (#357), set by `@easy-cms/plugin-ai`: `cms.ai` answers the admin's AI
4422
+ * features with them. See `AiConfig`.
4423
+ */
4424
+ readonly ai?: AiConfig;
4425
+ /**
4426
+ * Features that use AI, added by plugins and by Easy CMS itself: admins turn each off or pick
4427
+ * the connection it uses on the Integrations page. See `AiFeatureConfig`.
4428
+ */
4429
+ readonly aiFeatures?: readonly AiFeatureConfig[];
3896
4430
  }
3897
4431
  /** The config after plugins ran, validation passed and defaults were applied. */
3898
- interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'endpoints' | 'plugins' | 'audit'> {
4432
+ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'endpoints' | 'plugins' | 'audit' | 'integrations' | 'aiFeatures'> {
3899
4433
  readonly cors: readonly string[] | '*';
3900
4434
  /** `null` without localization. */
3901
4435
  readonly localization: Required<LocalizationConfig> | null;
@@ -3908,6 +4442,8 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3908
4442
  readonly collections: readonly CollectionConfig[];
3909
4443
  readonly globals: readonly GlobalConfig[];
3910
4444
  readonly endpoints: readonly Endpoint[];
4445
+ readonly integrations: readonly IntegrationConfig[];
4446
+ readonly aiFeatures: readonly AiFeatureConfig[];
3911
4447
  readonly cliCommands: readonly CliCommand[];
3912
4448
  readonly events: readonly string[];
3913
4449
  readonly jobs: readonly JobConfig[];
@@ -3923,5 +4459,5 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
3923
4459
  */
3924
4460
  declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3925
4461
 
3926
- export { ADAPTER_API_VERSION as A, EasyCMS as E, AuditLog as _, Auth as a1, EasyCMSError as aK, FIELD_TYPES as aR, ConfigError as at, ForbiddenError as b9, NotFoundError as bA, PLUGIN_API_VERSION as bG, PayloadTooLargeError as bL, QueryError as bR, ROLE_OPERATIONS as bS, isSystemAdmin as c$, Roles as c2, SchemaError as c5, ValidationError as cF, WEBHOOK_EVENTS as cJ, anyone as cN, consoleEmail as cO, consoleLogger as cP, createEasyCMS as cQ, defineConfig as cR, defineFieldType as cS, definePlugin as cT, diskStorage as cU, hasRows as cV, isAdmin as cW, isHasMany as cX, isPolymorphic as cY, isSignedIn as cZ, isStaff as c_, SingleSignOn as ck, TRIGRAM_LIKE_MIN as cr, TooManyRequestsError as cv, UnauthorizedError as cy, jsonLogger as d0, memorySearch as d1, mimeAllowedBy as d2, polymorphicValue as d3, relationTargets as d4, rowFields as d5, silentLogger as d6, uniqueWithinOf as d7, API_KEYS as d8, API_KEY_GLOBAL_OPERATIONS as d9, API_KEY_OPERATIONS as da, DEFAULT_MAX_VERSIONS as db, DEFAULT_PASSWORD_EMAILS as dc, FIELD_TYPE_BASES as dd, FOLDER_LEVELS as de, MediaFolders as df, SSO_COOKIE as dg, codeOfStatus as di, isJsonLogger as dj, keyAllows as dk, logFormat as dl, secretsOf as dm, ADMIN_ICONS as j };
3927
- export type { AuditPage as $, AdminRoles as B, Config as C, AdminSso as D, Field as F, GlobalSlug as G, AdminSwitcher as H, ID as I, AdminViewAccess as J, AfterChangeHook as K, LocalizationConfig as L, AfterDeleteHook as M, AfterReadHook as N, ApiKeyContext as O, PaginatedDocs as P, ApiKeyGlobalOperation as Q, ResolvedConfig as R, ApiKeyOperation as S, ApiKeyPermissions as T, UncountedDocs as U, ArrayField as V, Where as W, AuditChange as X, AuditConfig as Y, AuditEntry as Z, CollectionSlug as a, FieldValidateContext as a$, AuditVerification as a0, AuthConfig as a2, AuthProvider as a3, AuthProviderProfile as a4, AuthProviderRequest as a5, AuthUser as a6, BackupsConfig as a7, BeforeChangeHook as a8, BeforeDeleteHook as a9, DashboardWidget as aA, Database as aB, DatabaseAdapter as aC, DatabaseInitArgs as aD, DatabaseTransfer as aE, DateField as aF, DepthOptions as aG, DirectUpload as aH, DiskStorageOptions as aI, DocumentOf as aJ, EmailAdapter as aL, EmailField as aM, EmailMessage as aN, Endpoint as aO, EndpointRequest as aP, ErrorCode as aQ, FieldAccess as aS, FieldAccessFn as aT, FieldAdmin as aU, FieldRule as aV, FieldType as aW, FieldTypeBase as aX, FieldTypeDefinition as aY, FieldTypeValidateContext as aZ, FieldValidate as a_, BeforeValidateHook as aa, Block as ab, BlockSummaryArgs as ac, BlocksField as ad, BooleanField as ae, BuiltinApiKeysCollection as af, BuiltinMediaCollection as ag, BuiltinMediaFoldersCollection as ah, BuiltinUsersCollection as ai, BundleHints as aj, CliCommand as ak, CollectionAccess as al, CollectionAdmin as am, CollectionCalendarAdmin as an, CollectionConfig as ao, CollectionHooks as ap, CollectionListAdmin as aq, CollectionSearch as ar, CollectionView as as, ConfigIssue as au, ContainerAdmin as av, CreateInput as aw, CreateMigrationResult as ax, CustomField as ay, CustomFieldTypes as az, CollectionDocument as b, Role as b$, FieldValue as b0, FieldsInput as b1, FieldsValue as b2, FilterOptionsArgs as b3, FindAfterArgs as b4, FindArgs as b5, FindOptions as b6, FolderLevel as b7, FolderPermissions as b8, NumberField as bB, OnRequest as bC, OnRequestArgs as bD, Operation as bE, OutboxRow as bF, PasswordEmail as bH, PasswordEmailArgs as bI, PasswordEmailFn as bJ, PasswordLinkOptions as bK, Plugin as bM, PluginInfo as bN, PluginTypes as bO, PreviewTarget as bP, PreviewURL as bQ, RawDocument as bT, ReadOptions as bU, RelationshipField as bV, RequestContext as bW, RichTextBlock as bX, RichTextBlocks as bY, RichTextDocument as bZ, RichTextField as b_, GlobalAccess as ba, GlobalConfig as bb, GlobalHooks as bc, GlobalInput as bd, GroupField as be, ImageSize as bf, IncrementOptions as bg, InferCollection as bh, InferGlobal as bi, JobConfig as bj, JsonField as bk, KeysetPage as bl, Label as bm, LayoutNode as bn, LivePreview as bo, LocaleOf as bp, LogDetails as bq, Logger as br, LoginArgs as bs, LogsConfig as bt, MediaDocument as bu, MediaSize as bv, MembersConfig as bw, MembersSignup as bx, MigrationInfo as by, NavGroup as bz, CursorDocs as c, RoleOperation as c0, RolePermissions as c1, RoutesConfig as c3, ScheduledPublish as c4, SchemaMode as c6, SearchAdapter as c7, SearchEntry as c8, SearchHits as c9, UpdateOptions as cA, UploadConfig as cB, UploadField as cC, UploadFromURLConfig as cD, UserIdentity as cE, Version as cG, VersionSummary as cH, VersionsConfig as cI, WebhookConfig as cK, WebhookEvent as cL, WhereOperators as cM, SearchOptions as ca, SearchQuery as cb, SearchResults as cc, SearchSetting as cd, SelectField as ce, SelectOption as cf, Session as cg, SidebarPanel as ch, SignupArgs as ci, SignupResult as cj, SlugField as cl, SlugOf as cm, SsoOutcome as cn, SsoProviderRef as co, StorageAdapter as cp, StoredFile as cq, TelemetryConfig as cs, TextField as ct, TextareaField as cu, TypedPlugin as cw, UiField as cx, UpdateInput as cz, GlobalDocument as d, Secrets as dh, FieldError as e, FilterOptions as f, CreateEasyCMSOptions as g, EmailSetting as h, WebhookPayload as i, Access as k, AccessArgs as l, AccessOptions as m, AdminBrand as n, AdminCommand as o, AdminComponent as p, AdminConfig as q, AdminFieldArgs as r, AdminIcon as s, AdminLocale as t, AdminPage as u, AdminRole as v, AdminRoleChange as w, AdminRoleField as x, AdminRoleTarget as y, AdminRoleView as z };
4462
+ export { ADAPTER_API_VERSION as A, EasyCMS as E, Ai as Q, AiProviderError as Y, AuditLog as a9, ConfigError as aE, EasyCMSError as aV, Auth as ac, FIELD_TYPES as b2, NotFoundError as bU, PLUGIN_API_VERSION as b_, ForbiddenError as bm, PayloadTooLargeError as c3, QueryError as c9, SingleSignOn as cE, TRIGRAM_LIKE_MIN as cL, TooManyRequestsError as cP, UnauthorizedError as cS, ValidationError as cZ, ROLE_OPERATIONS as ca, Roles as cm, SchemaError as cp, WEBHOOK_EVENTS as d1, anyone as d5, consoleEmail as d6, consoleLogger as d7, createEasyCMS as d8, defineConfig as d9, MediaFolders as dA, SSO_COOKIE as dB, codeOfStatus as dD, isJsonLogger as dE, keyAllows as dF, logFormat as dG, secretsOf as dH, defineFieldType as da, definePlugin as db, diskStorage as dc, hasRows as dd, isAdmin as de, isHasMany as df, isPolymorphic as dg, isSignedIn as dh, isStaff as di, isSystemAdmin as dj, jsonLogger as dk, memorySearch as dl, mimeAllowedBy as dm, polymorphicValue as dn, relationTargets as dp, rowFields as dq, silentLogger as dr, uniqueWithinOf as ds, API_KEYS as dt, API_KEY_GLOBAL_OPERATIONS as du, API_KEY_OPERATIONS as dv, DEFAULT_MAX_VERSIONS as dw, DEFAULT_PASSWORD_EMAILS as dx, FIELD_TYPE_BASES as dy, FOLDER_LEVELS as dz, ADMIN_ICONS as j };
4463
+ export type { AiRequest as $, AdminRoleView as B, Config as C, AdminRoles as D, Field as F, GlobalSlug as G, AdminSso as H, ID as I, AdminSwitcher as J, AdminViewAccess as K, LocalizationConfig as L, AfterChangeHook as M, AfterDeleteHook as N, AfterReadHook as O, PaginatedDocs as P, ResolvedConfig as R, AiConfig as S, AiFeatureConfig as T, UncountedDocs as U, AiImage as V, Where as W, AiProvider as X, AiProviderRequest as Z, AiProviderResult as _, CollectionSlug as a, ErrorCode as a$, AiResult as a0, ApiKeyContext as a1, ApiKeyGlobalOperation as a2, ApiKeyOperation as a3, ApiKeyPermissions as a4, ArrayField as a5, AuditChange as a6, AuditConfig as a7, AuditEntry as a8, CollectionHooks as aA, CollectionListAdmin as aB, CollectionSearch as aC, CollectionView as aD, ConfigIssue as aF, ContainerAdmin as aG, CreateInput as aH, CreateMigrationResult as aI, CustomField as aJ, CustomFieldTypes as aK, DashboardWidget as aL, Database as aM, DatabaseAdapter as aN, DatabaseInitArgs as aO, DatabaseTransfer as aP, DateField as aQ, DepthOptions as aR, DirectUpload as aS, DiskStorageOptions as aT, DocumentOf as aU, EmailAdapter as aW, EmailField as aX, EmailMessage as aY, Endpoint as aZ, EndpointRequest as a_, AuditPage as aa, AuditVerification as ab, AuthConfig as ad, AuthProvider as ae, AuthProviderProfile as af, AuthProviderRequest as ag, AuthUser as ah, BackupsConfig as ai, BeforeChangeHook as aj, BeforeDeleteHook as ak, BeforeValidateHook as al, Block as am, BlockSummaryArgs as an, BlocksField as ao, BooleanField as ap, BuiltinApiKeysCollection as aq, BuiltinMediaCollection as ar, BuiltinMediaFoldersCollection as as, BuiltinUsersCollection as at, BundleHints as au, CliCommand as av, CollectionAccess as aw, CollectionAdmin as ax, CollectionCalendarAdmin as ay, CollectionConfig as az, CollectionDocument as b, PasswordEmail as b$, EventListener as b0, EventMessage as b1, FieldAccess as b3, FieldAccessFn as b4, FieldAdmin as b5, FieldRule as b6, FieldType as b7, FieldTypeBase as b8, FieldTypeDefinition as b9, IntegrationLogo as bA, IntegrationOAuthResult as bB, IntegrationTestResult as bC, JobConfig as bD, JsonField as bE, KeysetPage as bF, Label as bG, LayoutNode as bH, LivePreview as bI, LocaleOf as bJ, LogDetails as bK, Logger as bL, LoginArgs as bM, LogsConfig as bN, MediaDocument as bO, MediaSize as bP, MembersConfig as bQ, MembersSignup as bR, MigrationInfo as bS, NavGroup as bT, NumberField as bV, OnRequest as bW, OnRequestArgs as bX, Operation as bY, OutboxRow as bZ, FieldTypeValidateContext as ba, FieldValidate as bb, FieldValidateContext as bc, FieldValue as bd, FieldsInput as be, FieldsValue as bf, FilterOptionsArgs as bg, FindAfterArgs as bh, FindArgs as bi, FindOptions as bj, FolderLevel as bk, FolderPermissions as bl, GlobalAccess as bn, GlobalConfig as bo, GlobalHooks as bp, GlobalInput as bq, GroupField as br, ImageSize as bs, IncrementOptions as bt, InferCollection as bu, InferGlobal as bv, IntegrationCategory as bw, IntegrationConfig as bx, IntegrationConnection as by, IntegrationField as bz, CursorDocs as c, VersionSummary as c$, PasswordEmailArgs as c0, PasswordEmailFn as c1, PasswordLinkOptions as c2, Plugin as c4, PluginInfo as c5, PluginTypes as c6, PreviewTarget as c7, PreviewURL as c8, Session as cA, SidebarPanel as cB, SignupArgs as cC, SignupResult as cD, SlugField as cF, SlugOf as cG, SsoOutcome as cH, SsoProviderRef as cI, StorageAdapter as cJ, StoredFile as cK, TelemetryConfig as cM, TextField as cN, TextareaField as cO, TypedPlugin as cQ, UiField as cR, UpdateInput as cT, UpdateOptions as cU, UploadConfig as cV, UploadField as cW, UploadFromURLConfig as cX, UserIdentity as cY, Version as c_, RawDocument as cb, ReadOptions as cc, RelationshipField as cd, RequestContext as ce, RichTextBlock as cf, RichTextBlocks as cg, RichTextDocument as ch, RichTextField as ci, Role as cj, RoleOperation as ck, RolePermissions as cl, RoutesConfig as cn, ScheduledPublish as co, SchemaMode as cq, SearchAdapter as cr, SearchEntry as cs, SearchHits as ct, SearchOptions as cu, SearchQuery as cv, SearchResults as cw, SearchSetting as cx, SelectField as cy, SelectOption as cz, GlobalDocument as d, VersionsConfig as d0, WebhookConfig as d2, WebhookEvent as d3, WhereOperators as d4, Secrets as dC, FieldError as e, FilterOptions as f, CreateEasyCMSOptions as g, EmailSetting as h, WebhookPayload as i, Access as k, AccessArgs as l, AccessOptions as m, AdminAi as n, AdminBrand as o, AdminCommand as p, AdminComponent as q, AdminConfig as r, AdminFieldArgs as s, AdminIcon as t, AdminLocale as u, AdminPage as v, AdminRole as w, AdminRoleChange as x, AdminRoleField as y, AdminRoleTarget as z };