@easy-cms/core 0.47.4 → 0.49.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.
@@ -0,0 +1,3209 @@
1
+ import { FieldCondition } from './conditions.js';
2
+
3
+ /** A document id. SQLite uses integers, Postgres may use either. */
4
+ type ID = string | number;
5
+ type Role = 'admin' | 'editor' | (string & {});
6
+ /** The logged-in admin user, as seen by access functions and hooks. */
7
+ interface AuthUser {
8
+ readonly id: ID;
9
+ readonly email: string;
10
+ readonly role: Role;
11
+ /**
12
+ * Set by `onRequest` (e.g. a multi-tenant plugin) when `role` holds only in part of the site,
13
+ * such as one tenant: an `admin` there administers that part, not the whole system (Settings,
14
+ * backups, roles, other users' API keys).
15
+ */
16
+ readonly scoped?: boolean;
17
+ /**
18
+ * A member of the site, e.g. a customer (`auth.members.roles`): signs in on the site, never to
19
+ * the admin, and `isStaff` doesn't count them.
20
+ */
21
+ readonly member?: boolean;
22
+ readonly [field: string]: unknown;
23
+ }
24
+ /**
25
+ * An admin of the whole system: role `admin`, signed in (not an API key), and not an admin of a
26
+ * part only (`scoped`).
27
+ */
28
+ declare const isSystemAdmin: (user: AuthUser | null | undefined) => boolean;
29
+ interface WhereOperators {
30
+ readonly equals?: unknown;
31
+ readonly not_equals?: unknown;
32
+ readonly in?: readonly unknown[];
33
+ readonly not_in?: readonly unknown[];
34
+ readonly gt?: number | string;
35
+ readonly gte?: number | string;
36
+ readonly lt?: number | string;
37
+ readonly lte?: number | string;
38
+ readonly like?: string;
39
+ readonly exists?: boolean;
40
+ }
41
+ type Where = {
42
+ readonly and?: readonly Where[];
43
+ readonly or?: readonly Where[];
44
+ } & {
45
+ readonly [field: string]: WhereOperators | readonly Where[] | undefined;
46
+ };
47
+ /**
48
+ * What a request is about besides its user, e.g. the site or tenant it works in: set by
49
+ * `onRequest` in the config (usually a plugin's), or passed to the Local API as `context`.
50
+ */
51
+ type RequestContext = Readonly<Record<string, unknown>>;
52
+ interface AccessArgs {
53
+ /** `null` when the request is not logged in. */
54
+ readonly user: AuthUser | null;
55
+ /** The request's context (`onRequest`), `{}` when there is none. */
56
+ readonly context?: RequestContext;
57
+ readonly id?: ID;
58
+ readonly data?: Readonly<Record<string, unknown>>;
59
+ }
60
+ /**
61
+ * Decides whether an operation is allowed.
62
+ * Return a `Where` to allow it only for the documents that match.
63
+ */
64
+ type Access = (args: AccessArgs) => boolean | Where | Promise<boolean | Where>;
65
+ interface CollectionAccess {
66
+ readonly read?: Access;
67
+ readonly create?: Access;
68
+ readonly update?: Access;
69
+ readonly delete?: Access;
70
+ }
71
+ interface GlobalAccess {
72
+ readonly read?: Access;
73
+ readonly update?: Access;
74
+ }
75
+ type FieldAccessFn = (args: AccessArgs) => boolean | Promise<boolean>;
76
+ interface FieldAccess {
77
+ readonly read?: FieldAccessFn;
78
+ /** Who may set it when creating a document. Default: `update`. */
79
+ readonly create?: FieldAccessFn;
80
+ readonly update?: FieldAccessFn;
81
+ }
82
+ declare const anyone: Access;
83
+ /**
84
+ * A signed-in user of the admin: not a site member (`auth.members`). What collections allow by
85
+ * default.
86
+ */
87
+ declare const isStaff: Access;
88
+ /** Anyone signed in, site members (e.g. customers) included. */
89
+ declare const isSignedIn: Access;
90
+ /**
91
+ * Admins of the whole site, and their API keys (which can do only what they list): not admins of
92
+ * a part of it (e.g. a tenant).
93
+ */
94
+ declare const isAdmin: Access;
95
+
96
+ interface Logger {
97
+ info(message: string): void;
98
+ warn(message: string): void;
99
+ error(message: string): void;
100
+ }
101
+ declare const consoleLogger: Logger;
102
+ declare const silentLogger: Logger;
103
+
104
+ /** A document as stored: relationships and uploads are ids, never populated. */
105
+ type RawDocument = Record<string, unknown> & {
106
+ id: ID;
107
+ };
108
+ interface PaginatedDocs<T> {
109
+ docs: T[];
110
+ totalDocs: number;
111
+ limit: number;
112
+ page: number;
113
+ totalPages: number;
114
+ hasNextPage: boolean;
115
+ hasPrevPage: boolean;
116
+ }
117
+ /**
118
+ * How the adapter treats the database schema on startup.
119
+ * - `push`: apply schema changes directly (development).
120
+ * - `verify`: fail if migrations are pending or the config changed without a migration (production).
121
+ * - `skip`: do nothing (used by the migration CLI).
122
+ */
123
+ type SchemaMode = 'push' | 'verify' | 'skip';
124
+ interface DatabaseInitArgs {
125
+ readonly config: ResolvedConfig;
126
+ /** Project root; relative paths (database file, migrations) resolve from here. */
127
+ readonly cwd: string;
128
+ readonly schema: SchemaMode;
129
+ readonly logger: Logger;
130
+ /** Whether the process can prompt the user (the CLI in a terminal). */
131
+ readonly interactive?: boolean;
132
+ }
133
+ interface FindArgs {
134
+ readonly collection: string;
135
+ readonly where?: Where | undefined;
136
+ /** Field paths; prefix with `-` for descending. */
137
+ readonly sort: readonly string[];
138
+ /** `0` returns every matching document. */
139
+ readonly limit: number;
140
+ readonly page: number;
141
+ }
142
+ interface MigrationInfo {
143
+ readonly name: string;
144
+ readonly applied: boolean;
145
+ }
146
+ interface CreateMigrationResult {
147
+ readonly name: string;
148
+ readonly file: string;
149
+ readonly statements: readonly string[];
150
+ }
151
+ /** A connected database bound to one config. Returned by `DatabaseAdapter.init`. */
152
+ interface Database {
153
+ find(args: FindArgs): Promise<PaginatedDocs<RawDocument>>;
154
+ findById(args: {
155
+ collection: string;
156
+ id: ID;
157
+ }): Promise<RawDocument | null>;
158
+ /** Documents of one collection by id, in any order. Used to populate relationships. */
159
+ findByIds(args: {
160
+ collection: string;
161
+ ids: readonly ID[];
162
+ }): Promise<RawDocument[]>;
163
+ count(args: {
164
+ collection: string;
165
+ where?: Where | undefined;
166
+ }): Promise<number>;
167
+ /** `data` holds every field, `createdAt` and `updatedAt`. Runs in one transaction. */
168
+ create(args: {
169
+ collection: string;
170
+ data: Record<string, unknown>;
171
+ }): Promise<RawDocument>;
172
+ /**
173
+ * `data` holds the whole document after merging. Runs in one transaction. With `where`, only
174
+ * when the stored document matches it at the moment of writing (compare and set): `null` when
175
+ * it doesn't, and nothing is written.
176
+ */
177
+ update(args: {
178
+ collection: string;
179
+ id: ID;
180
+ data: Record<string, unknown>;
181
+ }): Promise<RawDocument>;
182
+ update(args: {
183
+ collection: string;
184
+ id: ID;
185
+ data: Record<string, unknown>;
186
+ where: Where;
187
+ }): Promise<RawDocument | null>;
188
+ /**
189
+ * Adds `by` to a top-level number field in one statement, safe against concurrent calls; an
190
+ * empty value counts as 0. With `min` / `max`, only when the result stays within them. Returns
191
+ * the new value, or `null` when it would leave the bounds or the document is gone.
192
+ */
193
+ increment(args: {
194
+ collection: string;
195
+ id: ID;
196
+ field: string;
197
+ by: number;
198
+ min?: number | undefined;
199
+ max?: number | undefined;
200
+ }): Promise<number | null>;
201
+ delete(args: {
202
+ collection: string;
203
+ id: ID;
204
+ }): Promise<void>;
205
+ findGlobal(args: {
206
+ slug: string;
207
+ }): Promise<Record<string, unknown> | null>;
208
+ updateGlobal(args: {
209
+ slug: string;
210
+ data: Record<string, unknown>;
211
+ }): Promise<Record<string, unknown>>;
212
+ /** Writes a migration file for the difference between the config and the latest migration. `null` when nothing changed. */
213
+ createMigration(args: {
214
+ name: string;
215
+ }): Promise<CreateMigrationResult | null>;
216
+ /** Applies pending migrations in order. Returns the names applied. */
217
+ migrate(): Promise<string[]>;
218
+ migrationStatus(): Promise<MigrationInfo[]>;
219
+ /**
220
+ * Copies the database to `file` (an absolute path) while it is in use, when the database can
221
+ * do that itself (SQLite). Others (Postgres) leave it out; use their own tools.
222
+ */
223
+ readonly backup?: ((file: string) => Promise<void>) | undefined;
224
+ /** Raw table access for copying a whole database to another (`easy-cms copy`). */
225
+ readonly transfer?: DatabaseTransfer | undefined;
226
+ /** The prefix of its tables (`tablePrefix`), so a copy (a backup) can use the same. */
227
+ readonly tablePrefix?: string | undefined;
228
+ destroy(): Promise<void>;
229
+ }
230
+ /**
231
+ * Reads and writes stored rows as they are, ids and hidden fields included, bypassing validation
232
+ * and hooks. Only for moving a database: `copyDatabase()`.
233
+ */
234
+ interface DatabaseTransfer {
235
+ /**
236
+ * Identifies the tables and columns the config produces, the same for every database kind.
237
+ * Two databases can be copied into each other when their hashes match.
238
+ */
239
+ readonly schemaHash: string;
240
+ /** Every table, in an order that is safe to write. */
241
+ tables(): string[];
242
+ count(table: string): Promise<number>;
243
+ /** Rows in a stable order (by primary key), `offset` and `limit` for batches. */
244
+ read(table: string, offset: number, limit: number): Promise<Record<string, unknown>[]>;
245
+ write(table: string, rows: readonly Record<string, unknown>[]): Promise<void>;
246
+ /** After all writes, e.g. moves id sequences past the copied ids so new documents fit. */
247
+ finish(): Promise<void>;
248
+ }
249
+ /** What `sqlite()` / `postgres()` return and what `config.db` holds. */
250
+ /** The version of the adapter interfaces (database, storage, email, sign-in providers). */
251
+ declare const ADAPTER_API_VERSION = 1;
252
+ interface DatabaseAdapter {
253
+ readonly name: string;
254
+ /**
255
+ * The adapter interface it is written for (`ADAPTER_API_VERSION`); Easy CMS refuses another.
256
+ * Leave it out to skip the check.
257
+ */
258
+ readonly apiVersion?: number;
259
+ init(args: DatabaseInitArgs): Promise<Database>;
260
+ /** Hints for bundlers (Nitro, Next.js output tracing) about files static analysis cannot find. */
261
+ readonly bundle?: BundleHints;
262
+ }
263
+ interface BundleHints {
264
+ /** Absolute paths of files to ship with the server build, e.g. native binaries loaded dynamically. */
265
+ readonly traceInclude: readonly string[];
266
+ }
267
+
268
+ /** What a preview token opens: one document of a collection, or one global. */
269
+ type PreviewTarget = {
270
+ readonly collection: string;
271
+ readonly id: string;
272
+ } | {
273
+ readonly global: string;
274
+ };
275
+ /**
276
+ * Why a link was sent: a forgotten password, an invitation to a new account, or (`verify`) to
277
+ * confirm the email of an account made by signing up.
278
+ */
279
+ type PasswordPurpose = 'reset' | 'invite' | 'verify';
280
+
281
+ declare const API_KEYS = "api-keys";
282
+ /** What a key may do with a collection. `create` on `media` means uploading. */
283
+ declare const API_KEY_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
284
+ type ApiKeyOperation = (typeof API_KEY_OPERATIONS)[number];
285
+ /** What a key may do with a global. */
286
+ declare const API_KEY_GLOBAL_OPERATIONS: readonly ["read", "update", "publish"];
287
+ type ApiKeyGlobalOperation = (typeof API_KEY_GLOBAL_OPERATIONS)[number];
288
+ /** Stored in the key's `permissions` field. Anything not listed is not allowed. */
289
+ interface ApiKeyPermissions {
290
+ readonly collections?: Readonly<Record<string, readonly ApiKeyOperation[]>>;
291
+ readonly globals?: Readonly<Record<string, readonly ApiKeyGlobalOperation[]>>;
292
+ /**
293
+ * With `upload.folders`: the media folders (ids) the key may use, with their subfolders. Empty
294
+ * or missing: every folder.
295
+ */
296
+ readonly folders?: readonly (string | number)[];
297
+ /**
298
+ * The context the key was created in (`onRequest`, e.g. its tenant), kept for good: requests
299
+ * with the key get it as `user.apiKey.permissions.context`. Set by Easy CMS, not by input.
300
+ */
301
+ readonly context?: RequestContext;
302
+ }
303
+ /** Set on `AuthUser.apiKey` when a request authenticated with an API key. */
304
+ interface ApiKeyContext {
305
+ readonly id: number | string;
306
+ readonly name: string;
307
+ readonly permissions: ApiKeyPermissions;
308
+ }
309
+ /**
310
+ * Whether a request's API key allows an operation. Always true without a key (sessions are
311
+ * governed by access rules alone); a key only narrows what its user may do.
312
+ */
313
+ declare function keyAllows(user: AuthUser | null, target: {
314
+ collection: string;
315
+ } | {
316
+ global: string;
317
+ }, operation: ApiKeyOperation): boolean;
318
+
319
+ /** The cookie that carries a sign-in through the provider and back. */
320
+ declare const SSO_COOKIE = "ecms-sso";
321
+ /** A provider as the login page shows it (`GET <api>/auth/init`). */
322
+ interface SsoProviderRef {
323
+ id: string;
324
+ name: string;
325
+ icon: string;
326
+ }
327
+ /** Settings → SSO (`GET <api>/admin/ui/sso`). */
328
+ interface AdminSso {
329
+ providers: (SsoProviderRef & {
330
+ /** Give this to the provider as the redirect (callback) URL. */
331
+ callbackURL: string | null;
332
+ details: Record<string, string>;
333
+ })[];
334
+ /** Who may sign in with a password: everyone, or only admins. */
335
+ password: 'everyone' | 'admins';
336
+ /** Email domains whose people get an account when they first sign in. */
337
+ signUp: {
338
+ domains: string[];
339
+ role: string;
340
+ } | null;
341
+ /** Without `serverURL`, sign-in only works in development. */
342
+ serverURL: boolean;
343
+ }
344
+ /** An outside account a user signs in with (`GET <api>/auth/identities`). */
345
+ interface UserIdentity {
346
+ id: ID;
347
+ provider: string;
348
+ /** The provider's name, e.g. `Google`. */
349
+ name: string;
350
+ email: string | null;
351
+ createdAt: string;
352
+ lastUsedAt: string | null;
353
+ }
354
+ /** Why a sign-in did not work, as `?sso=` on the page it ends on. */
355
+ type SsoOutcome = 'linked' | 'cancelled' | 'expired' | 'failed' | 'no-account' | 'link-first' | 'unverified' | 'inactive' | 'taken';
356
+ /** Single sign-on with `auth.providers`. Available as `cms.auth.sso`. */
357
+ declare class SingleSignOn {
358
+ private readonly cms;
359
+ constructor(cms: EasyCMS);
360
+ private get config();
361
+ get enabled(): boolean;
362
+ providers(): SsoProviderRef[];
363
+ /** Whether a user may sign in with a password (`auth.password: false` leaves only admins). */
364
+ passwordAllowed(user: {
365
+ readonly [key: string]: unknown;
366
+ }): boolean;
367
+ /** `<serverURL><api>/auth/<id>/callback`, or `null` without `serverURL` in production. */
368
+ callbackURL(provider: string, origin?: string): string | null;
369
+ /**
370
+ * Starts signing in (or, with `user`, linking an account): the provider's URL to send the
371
+ * browser to, and the sealed value of the cookie that must come back with it.
372
+ */
373
+ start(providerId: string, options: {
374
+ origin?: string;
375
+ redirect?: string | null;
376
+ user?: AuthUser;
377
+ }): Promise<{
378
+ url: string;
379
+ cookie: string;
380
+ }>;
381
+ /**
382
+ * Finishes at `<api>/auth/<id>/callback`: checks the response, finds (or makes) the user and
383
+ * starts a session. Returns where to send the browser; failures end on the login page (or the
384
+ * account page, when linking) with `?sso=<outcome>`.
385
+ */
386
+ finish(providerId: string, options: {
387
+ url: URL;
388
+ cookie: string | undefined;
389
+ origin?: string;
390
+ user: AuthUser | null;
391
+ }): Promise<{
392
+ redirect: string;
393
+ session?: Session;
394
+ }>;
395
+ /** A user's outside accounts. */
396
+ identities(userId: ID): Promise<UserIdentity[]>;
397
+ /**
398
+ * Unlinks an outside account: the user's own, or anyone's for admins. Refuses to unlink the
399
+ * last way a user has to sign in.
400
+ */
401
+ unlink(actor: AuthUser, identityId: ID): Promise<void>;
402
+ settings(origin?: string): AdminSso;
403
+ private provider;
404
+ /** Only pages of the admin: anything else (another site) goes to the admin's home. */
405
+ private adminPage;
406
+ private identity;
407
+ private addIdentity;
408
+ private userByEmail;
409
+ /** A new account for people from `providerSignUp.domains`. */
410
+ private signUp;
411
+ /** A site member (`auth.members`): matched by email, like any shop or site account. */
412
+ private isMember;
413
+ private defaultRole;
414
+ }
415
+
416
+ interface LoginArgs {
417
+ readonly email: string;
418
+ readonly password: string;
419
+ /** Client IP, used together with the email for rate limiting. */
420
+ readonly ip?: string | undefined;
421
+ }
422
+ interface Session {
423
+ readonly user: AuthUser;
424
+ /** Signed session token, the value of the session cookie. */
425
+ readonly token: string;
426
+ /** Send back in the `X-CSRF-Token` header on cookie-authenticated writes. */
427
+ readonly csrfToken: string;
428
+ readonly expiresAt: string;
429
+ }
430
+ interface SignupArgs {
431
+ readonly email: string;
432
+ readonly password: string;
433
+ readonly name?: string | undefined;
434
+ /** From `GET <api>/auth/signup`: the form was loaded a moment ago. */
435
+ readonly token?: unknown;
436
+ /** The hidden field; bots fill it in. */
437
+ readonly honeypot?: unknown;
438
+ /** The Cloudflare Turnstile response, with `signup.turnstile`. */
439
+ readonly turnstile?: unknown;
440
+ readonly ip?: string | undefined;
441
+ /** The request's origin: used for the link only in development, without a site URL. */
442
+ readonly origin?: string | undefined;
443
+ readonly locale?: string | undefined;
444
+ /** The request's context (`onRequest`), e.g. the tenant: given to the users' hooks. */
445
+ readonly context?: RequestContext;
446
+ }
447
+ /** What signing up did: a link was emailed to confirm, or (without `verifyEmail`) a session. */
448
+ type SignupResult = {
449
+ readonly verify: true;
450
+ } | (Session & {
451
+ readonly verify: false;
452
+ });
453
+ /** Where a password link is opened from, and the language of its email. */
454
+ interface PasswordLinkOptions {
455
+ /** The request's origin: used for the link only in development, when `serverURL` is not set. */
456
+ readonly origin?: string | undefined;
457
+ /** `en` or `th`. Default: `admin.locale`. */
458
+ readonly locale?: string | undefined;
459
+ }
460
+ /** Login, logout and session verification. Available as `cms.auth`. */
461
+ declare class Auth {
462
+ private readonly cms;
463
+ /** Signing in with outside accounts (`auth.providers`). */
464
+ readonly sso: SingleSignOn;
465
+ constructor(cms: EasyCMS);
466
+ private get db();
467
+ private get config();
468
+ /** Checks credentials and starts a session. Throws `UnauthorizedError` or `TooManyRequestsError`. */
469
+ login(args: LoginArgs): Promise<Session>;
470
+ /**
471
+ * Creates the first admin. Only works while there are no users, and with `auth.setupCode`
472
+ * (`EASY_CMS_SETUP_CODE`) only with that code.
473
+ */
474
+ registerFirstUser(args: {
475
+ email: string;
476
+ password: string;
477
+ name?: string;
478
+ setupCode?: string;
479
+ ip?: string | undefined;
480
+ }): Promise<Session>;
481
+ /** What a sign-up form needs: a token proving when it was loaded, and Turnstile's site key. */
482
+ signupForm(): {
483
+ token: string;
484
+ turnstile: string | null;
485
+ verifyEmail: boolean;
486
+ };
487
+ /**
488
+ * Creates a member's account (`auth.members.signUp`). With `verifyEmail` (the default), emails
489
+ * a link to confirm the address and answers the same whether or not the email has an account;
490
+ * otherwise starts a session. Throws `ValidationError` for spam checks and bad input.
491
+ */
492
+ signup(args: SignupArgs): Promise<SignupResult>;
493
+ /** Confirms a member's email from the link, and signs them in. */
494
+ verifyEmail(args: {
495
+ token: string;
496
+ ip?: string | undefined;
497
+ }): Promise<Session>;
498
+ private mailVerifyLink;
499
+ /**
500
+ * A link to one of the site's pages for members (`auth.members.pages`), else to the admin's
501
+ * page of the same purpose.
502
+ */
503
+ private memberLink;
504
+ hasUsers(): Promise<boolean>;
505
+ private warnedNoClientIp;
506
+ /**
507
+ * Without the client's address, failed logins count per email only, so anyone can lock an
508
+ * account for a while. Said once, in production.
509
+ */
510
+ private warnNoClientIp;
511
+ /** Ends the session for a signed token. Unknown tokens are ignored. */
512
+ logout(signedToken: string): Promise<void>;
513
+ /**
514
+ * The user a request's headers carry: a Bearer token (session or API key) or the session
515
+ * cookie; `null` when there is none or it is not valid. For frameworks' pages and handlers.
516
+ */
517
+ userFromHeaders(headers: {
518
+ get(name: string): string | null | undefined;
519
+ }): Promise<AuthUser | null>;
520
+ /**
521
+ * Returns the user for a signed session token, or for an API key (`ecms_…`, with `apiKey` set
522
+ * on the user), or `null` if it is invalid or expired.
523
+ */
524
+ verify(signedToken: string | undefined | null): Promise<AuthUser | null>;
525
+ /** Starts a new session for a user, e.g. after they changed their own password. */
526
+ createSession(userId: ID): Promise<Session>;
527
+ /** Signs a user out everywhere. */
528
+ revokeSessions(userId: ID): Promise<void>;
529
+ /** Whether "forgot password" and invitations work: `email` is set and links have an origin. */
530
+ canSendPasswordLinks(origin?: string): boolean;
531
+ /**
532
+ * Emails a link to set a new password, if an active account has this email. Always resolves the
533
+ * same way, so callers can't tell whether the email has an account; extra requests within
534
+ * `lockWindow` are ignored.
535
+ */
536
+ requestPasswordReset(args: PasswordLinkOptions & {
537
+ email: string;
538
+ ip?: string | undefined;
539
+ }): Promise<void>;
540
+ /**
541
+ * Emails a user a link to set their password: an invitation if they have none yet, else a
542
+ * reset link. For admins (the REST API checks the role). Returns which one was sent.
543
+ */
544
+ sendPasswordLink(userId: ID, options?: PasswordLinkOptions): Promise<PasswordPurpose>;
545
+ /** The user and purpose of a valid, unused password link, or `null`. */
546
+ checkPasswordToken(token: string): Promise<{
547
+ user: AuthUser;
548
+ purpose: PasswordPurpose;
549
+ } | null>;
550
+ /**
551
+ * Sets the password from a link, ends every session of the account and starts one here.
552
+ * Throws `ValidationError` when the link expired or was used, or the password is too short.
553
+ */
554
+ resetPassword(args: {
555
+ token: string;
556
+ password: string;
557
+ ip?: string | undefined;
558
+ locale?: string | undefined;
559
+ }): Promise<Session & {
560
+ purpose: PasswordPurpose;
561
+ }>;
562
+ private passwordTokenUser;
563
+ private mailPasswordLink;
564
+ private localeOf;
565
+ private findByEmail;
566
+ /** The owner of an API key, limited by the key; `null` when unknown, expired or disabled. */
567
+ private verifyApiKey;
568
+ private startSession;
569
+ private findSession;
570
+ private toAuthUser;
571
+ private deleteExpiredSessions;
572
+ private windowStart;
573
+ private checkRateLimit;
574
+ private recordFailure;
575
+ private clearFailures;
576
+ }
577
+
578
+ /** An email to send. Addresses may be `name <address>`. */
579
+ interface EmailMessage {
580
+ readonly to: string | readonly string[];
581
+ readonly cc?: string | readonly string[];
582
+ readonly bcc?: string | readonly string[];
583
+ /** Default: the adapter's `from`. */
584
+ readonly from?: string;
585
+ readonly replyTo?: string;
586
+ readonly subject: string;
587
+ readonly html?: string;
588
+ readonly text?: string;
589
+ }
590
+ /**
591
+ * Sends email for Easy CMS and its plugins (form notifications, and more later). Set one in
592
+ * the config as `email`, e.g. `smtp()` from `@easy-cms/email-smtp`, or `consoleEmail()` in
593
+ * development. Throw to report a failure: the message is retried later.
594
+ */
595
+ interface EmailAdapter {
596
+ /** What sends the email, e.g. `smtp`; shown to admins on the dashboard (System). */
597
+ readonly name?: string;
598
+ /**
599
+ * The adapter interface it is written for (`ADAPTER_API_VERSION`); Easy CMS refuses another.
600
+ * Leave it out to skip the check.
601
+ */
602
+ readonly apiVersion?: number;
603
+ /** The sender when a message has none, e.g. `Easy CMS <no-reply@example.com>`. */
604
+ readonly from?: string | undefined;
605
+ send(message: EmailMessage & {
606
+ readonly from: string;
607
+ }): Promise<void>;
608
+ /**
609
+ * The settings in use, shown to admins on Settings → Email. Never return secrets: say whether
610
+ * a password is set, not what it is.
611
+ */
612
+ describe?(): readonly EmailSetting[];
613
+ /** Checks the connection without sending (Settings → Email); throws with the reason. */
614
+ verify?(): Promise<void>;
615
+ }
616
+ /** One setting an email adapter shows to admins (`describe()`). */
617
+ interface EmailSetting {
618
+ /**
619
+ * `host`, `port`, `secure`, `user`, `password`, `from` and `delivery` (`log`: printed, not sent)
620
+ * are translated in the admin; others are shown as they are.
621
+ */
622
+ readonly key: string;
623
+ /** `null`: not set. */
624
+ readonly value: string | null;
625
+ /** Where it comes from, e.g. `SMTP_HOST` or `smtp({ host })`. */
626
+ readonly source?: string;
627
+ }
628
+ /** Prints emails to the log instead of sending them: for development and tests. */
629
+ declare function consoleEmail(options?: {
630
+ from?: string;
631
+ log?: (text: string) => void;
632
+ }): EmailAdapter & {
633
+ readonly sent: (EmailMessage & {
634
+ from: string;
635
+ })[];
636
+ };
637
+
638
+ /** A label shown in the admin UI. Either one string or one string per admin locale. */
639
+ type Label = string | {
640
+ readonly [locale: string]: string;
641
+ };
642
+ interface FieldValidateContext {
643
+ /** The whole document being validated, including sibling fields. */
644
+ readonly data: Readonly<Record<string, unknown>>;
645
+ readonly operation: 'create' | 'update';
646
+ }
647
+ /** Return `true` when valid, or an error message to show on the field. */
648
+ type FieldValidate<TValue> = (value: TValue | null | undefined, ctx: FieldValidateContext) => true | string | Promise<true | string>;
649
+ /**
650
+ * A Web Component from an admin module (`admin.modules`), by its tag name, which must start
651
+ * with `ecms-`. `props` (plain JSON) reach the element as its `options` property.
652
+ */
653
+ type AdminComponent = string | {
654
+ readonly tag: string;
655
+ readonly props?: Readonly<Record<string, unknown>>;
656
+ };
657
+ /** How the admin shows a field, with components from admin modules. */
658
+ interface FieldAdmin {
659
+ /** Help below the field's label. */
660
+ readonly description?: Label;
661
+ /** Its share of a row (`admin.layout`'s `row`). Default: an equal share. */
662
+ readonly width?: '1/4' | '1/3' | '1/2' | '2/3' | '3/4' | 'full';
663
+ /**
664
+ * Shown only when its sibling fields match, e.g. `{ field: 'linkType', equals: 'external' }`.
665
+ * A hidden field isn't required. See `FieldCondition`.
666
+ */
667
+ readonly condition?: FieldCondition;
668
+ /** Shown instead of the field's input; the admin keeps the label and error messages. */
669
+ readonly component?: AdminComponent;
670
+ /** Shown below the field, e.g. a length meter or a preview. */
671
+ readonly after?: readonly AdminComponent[];
672
+ /** Shows the value in the admin's lists (e.g. a color swatch). Default: the plain value. */
673
+ readonly cell?: AdminComponent;
674
+ /**
675
+ * A column of the list at first (each user can change it): `true`, or per request, e.g.
676
+ * only when the admin shows every tenant.
677
+ */
678
+ readonly column?: boolean | ((args: AdminFieldArgs) => boolean | Promise<boolean>);
679
+ /** Relationships: offer to create the related document in place. Default true. */
680
+ readonly allowCreate?: boolean;
681
+ /**
682
+ * The value a new document's form starts with, worked out for each user, e.g. from the
683
+ * request's context. The server's own default (`defaultValue`, hooks) still applies.
684
+ */
685
+ readonly initialValue?: (args: AdminFieldArgs) => unknown;
686
+ /** `sidebar`: shown in the edit page's side panel instead of the main form (top-level fields). */
687
+ readonly position?: 'sidebar';
688
+ }
689
+ /** What per-request admin options of a field receive. */
690
+ interface AdminFieldArgs {
691
+ readonly user: AuthUser;
692
+ readonly context: RequestContext;
693
+ }
694
+ /**
695
+ * Field types that packages add (`fieldTypes` in the config), for the types inferred from the
696
+ * config. A package declares its own by augmenting this interface:
697
+ *
698
+ * ```ts
699
+ * declare module '@easy-cms/core' {
700
+ * interface CustomFieldTypes {
701
+ * color: { value: string; options: { readonly presets?: readonly string[] } }
702
+ * }
703
+ * }
704
+ * ```
705
+ */
706
+ interface CustomFieldTypes {
707
+ }
708
+ /** A field of a type from `CustomFieldTypes`: the common options, plus the type's own. */
709
+ type CustomField = {
710
+ [K in keyof CustomFieldTypes]: BaseField<K & string, CustomFieldTypes[K] extends {
711
+ value: infer V;
712
+ } ? V : unknown> & (CustomFieldTypes[K] extends {
713
+ options: infer O;
714
+ } ? O : unknown);
715
+ }[keyof CustomFieldTypes];
716
+ interface BaseField<TType extends string, TValue> {
717
+ readonly type: TType;
718
+ readonly name: string;
719
+ readonly label?: Label;
720
+ readonly required?: boolean;
721
+ readonly unique?: boolean;
722
+ /**
723
+ * With `unique` (or on a slug field): a sibling field (e.g. `parent` or `tenant`), or several,
724
+ * whose documents only need to differ among themselves, so `/about/team` and `/careers/team`
725
+ * can both be `team`. Single, unlocalized relationship, select, text or number fields.
726
+ */
727
+ readonly uniqueWithin?: string | readonly string[];
728
+ readonly index?: boolean;
729
+ readonly defaultValue?: TValue;
730
+ readonly validate?: FieldValidate<TValue>;
731
+ readonly access?: FieldAccess;
732
+ /** Stored but never returned by the API nor accepted as input (e.g. a password hash). */
733
+ readonly hidden?: boolean;
734
+ /**
735
+ * One value per locale (needs `localization` in the config). Not for groups, arrays or
736
+ * hasMany fields: localize the fields inside a group or array instead.
737
+ */
738
+ readonly localized?: boolean;
739
+ /** How the field looks in the admin: components, position, width, conditions. */
740
+ readonly admin?: FieldAdmin;
741
+ /** Set by Easy CMS on fields of an added type (`fieldTypes`): the type's name, e.g. `color`. */
742
+ readonly customType?: string;
743
+ }
744
+ interface TextField extends BaseField<'text', string> {
745
+ readonly minLength?: number;
746
+ readonly maxLength?: number;
747
+ }
748
+ interface TextareaField extends BaseField<'textarea', string> {
749
+ readonly minLength?: number;
750
+ readonly maxLength?: number;
751
+ }
752
+ interface NumberField extends BaseField<'number', number> {
753
+ readonly min?: number;
754
+ readonly max?: number;
755
+ }
756
+ interface BooleanField extends BaseField<'boolean', boolean> {
757
+ }
758
+ /** Stored and returned as an ISO 8601 string. */
759
+ interface DateField extends BaseField<'date', string> {
760
+ }
761
+ interface EmailField extends BaseField<'email', string> {
762
+ }
763
+ interface JsonField extends BaseField<'json', unknown> {
764
+ }
765
+ type SelectOption = string | {
766
+ readonly label: Label;
767
+ readonly value: string;
768
+ };
769
+ interface SelectField extends BaseField<'select', string | readonly string[]> {
770
+ readonly options: readonly SelectOption[];
771
+ readonly hasMany?: boolean;
772
+ }
773
+ interface SlugField extends BaseField<'slug', string> {
774
+ /** Name of a sibling `text` field to generate the slug from. */
775
+ readonly from?: string;
776
+ }
777
+ /** Tiptap / ProseMirror JSON document. */
778
+ interface RichTextDocument {
779
+ readonly type: 'doc';
780
+ readonly content?: readonly unknown[];
781
+ }
782
+ interface RichTextField extends BaseField<'richText', RichTextDocument> {
783
+ }
784
+ /** References a document in the built-in `media` collection: a file or an image. */
785
+ interface UploadField extends BaseField<'upload', never> {
786
+ /** Several files, e.g. a gallery: the value is a list, in the order editors arrange. */
787
+ readonly hasMany?: boolean;
788
+ /** With `hasMany`: the fewest files. */
789
+ readonly minRows?: number;
790
+ /** With `hasMany`: the most files. */
791
+ readonly maxRows?: number;
792
+ /**
793
+ * The file types allowed, e.g. `['image/*']` or `['image/*', 'application/pdf']`. The admin
794
+ * offers only these, and saving refuses others.
795
+ */
796
+ readonly mimeTypes?: readonly string[];
797
+ /**
798
+ * With `upload.folders`: the `key` of the media folder this field's files go in, e.g.
799
+ * `'banners'`. Its picker opens there and its uploads land there; the folder is made when it
800
+ * is first needed.
801
+ */
802
+ readonly folder?: string;
803
+ /** With `folder`: only files in that folder (and its subfolders) may be chosen. */
804
+ readonly folderOnly?: boolean;
805
+ /** Which media documents may be chosen, as for relationships: offered and checked on save. */
806
+ readonly filterOptions?: FilterOptions;
807
+ }
808
+ interface FilterOptionsArgs {
809
+ /** The document being edited; `undefined` while it is being created. */
810
+ readonly id: ID | undefined;
811
+ readonly user: AuthUser | null;
812
+ readonly cms: EasyCMS;
813
+ /** The request's context (`onRequest`), `{}` when there is none. */
814
+ readonly context: RequestContext;
815
+ }
816
+ /**
817
+ * Which documents a relationship may point to: a `where` on the target collection, or `true`
818
+ * for any. Runs on the server: the admin's picker offers only these, and saving checks them.
819
+ */
820
+ type FilterOptions = (args: FilterOptionsArgs) => Where | true | Promise<Where | true>;
821
+ interface RelationshipField extends BaseField<'relationship', never> {
822
+ /** Slug of the target collection. */
823
+ readonly to: string;
824
+ readonly hasMany?: boolean;
825
+ /** With `hasMany`: the fewest documents. */
826
+ readonly minRows?: number;
827
+ /** With `hasMany`: the most documents. */
828
+ readonly maxRows?: number;
829
+ readonly filterOptions?: FilterOptions;
830
+ }
831
+ interface ArrayField extends BaseField<'array', never> {
832
+ readonly fields: readonly Field[];
833
+ readonly minRows?: number;
834
+ readonly maxRows?: number;
835
+ }
836
+ interface GroupField extends BaseField<'group', never> {
837
+ readonly fields: readonly Field[];
838
+ }
839
+ /** One kind of block in a `blocks` field. */
840
+ interface Block {
841
+ /** Stored in each row as `blockType`. */
842
+ readonly slug: string;
843
+ readonly labels?: {
844
+ readonly singular?: Label;
845
+ readonly plural?: Label;
846
+ };
847
+ readonly fields: readonly Field[];
848
+ }
849
+ /**
850
+ * A list of rows, each one of several block kinds (e.g. hero, text, gallery), for pages that
851
+ * editors lay out themselves. Rows are `{ id, blockType, ...fields }`, stored as JSON.
852
+ */
853
+ interface BlocksField extends BaseField<'blocks', never> {
854
+ readonly blocks: readonly Block[];
855
+ readonly minRows?: number;
856
+ readonly maxRows?: number;
857
+ }
858
+ /** The fields `uniqueWithin` names, as a list. */
859
+ declare const uniqueWithinOf: (field: Field) => readonly string[];
860
+ /** Fields with `hasMany`: their value is a list of options, documents or files. */
861
+ declare const isHasMany: (field: Field) => boolean;
862
+ /**
863
+ * Whether a MIME type is one of `patterns` (`image/*` matches every image but SVG, which can
864
+ * carry scripts and is allowed only when listed by name: `image/svg+xml`).
865
+ */
866
+ declare function mimeAllowedBy(mimeType: string, patterns: readonly string[]): boolean;
867
+ /** Fields whose value is a list of rows: `array` and `blocks`. */
868
+ declare const hasRows: (field: Field) => field is ArrayField | BlocksField;
869
+ /** The fields of one row of an `array` or `blocks` field (`undefined` for an unknown block). */
870
+ declare function rowFields(field: ArrayField | BlocksField, row: unknown): readonly Field[] | undefined;
871
+ type Field = CustomField | TextField | TextareaField | NumberField | BooleanField | DateField | EmailField | JsonField | SelectField | SlugField | RichTextField | UploadField | RelationshipField | ArrayField | GroupField | BlocksField;
872
+ type FieldType = Field['type'];
873
+ declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
874
+
875
+ type Simplify<T> = {
876
+ [K in keyof T]: T[K];
877
+ } & {};
878
+ type OptionValue<O> = O extends string ? O : O extends {
879
+ readonly value: infer V;
880
+ } ? V : never;
881
+ type SelectValue<F> = F extends {
882
+ readonly options: readonly (infer O extends SelectOption)[];
883
+ } ? F extends {
884
+ readonly hasMany: true;
885
+ } ? OptionValue<O>[] : OptionValue<O> : never;
886
+ /** The built-in users collection as seen by the type system (hidden fields left out). */
887
+ interface BuiltinUsersCollection {
888
+ readonly slug: 'users';
889
+ readonly fields: readonly [
890
+ {
891
+ readonly name: 'email';
892
+ readonly type: 'email';
893
+ readonly required: true;
894
+ },
895
+ {
896
+ readonly name: 'name';
897
+ readonly type: 'text';
898
+ },
899
+ {
900
+ readonly name: 'role';
901
+ readonly type: 'select';
902
+ readonly options: readonly string[];
903
+ readonly required: true;
904
+ },
905
+ {
906
+ readonly name: 'active';
907
+ readonly type: 'boolean';
908
+ }
909
+ ];
910
+ }
911
+ /** The built-in API keys collection (`apiKeys: true`); the key's hash is left out. */
912
+ interface BuiltinApiKeysCollection {
913
+ readonly slug: 'api-keys';
914
+ readonly fields: readonly [
915
+ {
916
+ readonly name: 'name';
917
+ readonly type: 'text';
918
+ readonly required: true;
919
+ },
920
+ {
921
+ readonly name: 'permissions';
922
+ readonly type: 'json';
923
+ },
924
+ {
925
+ readonly name: 'expiresAt';
926
+ readonly type: 'date';
927
+ },
928
+ {
929
+ readonly name: 'prefix';
930
+ readonly type: 'text';
931
+ },
932
+ {
933
+ readonly name: 'user';
934
+ readonly type: 'relationship';
935
+ readonly to: 'users';
936
+ },
937
+ {
938
+ readonly name: 'lastUsedAt';
939
+ readonly type: 'date';
940
+ }
941
+ ];
942
+ }
943
+ /** The built-in media folders (`upload.folders: true`). */
944
+ interface BuiltinMediaFoldersCollection {
945
+ readonly slug: 'media-folders';
946
+ readonly fields: readonly [
947
+ {
948
+ readonly name: 'name';
949
+ readonly type: 'text';
950
+ readonly required: true;
951
+ },
952
+ {
953
+ readonly name: 'parent';
954
+ readonly type: 'relationship';
955
+ readonly to: 'media-folders';
956
+ },
957
+ {
958
+ readonly name: 'key';
959
+ readonly type: 'text';
960
+ },
961
+ {
962
+ readonly name: 'private';
963
+ readonly type: 'boolean';
964
+ },
965
+ {
966
+ readonly name: 'permissions';
967
+ readonly type: 'json';
968
+ }
969
+ ];
970
+ }
971
+ /** `media-folders` when the config turns folders on. */
972
+ type FoldersOf<C extends Config> = C extends {
973
+ readonly upload: {
974
+ readonly folders: true;
975
+ };
976
+ } ? BuiltinMediaFoldersCollection : never;
977
+ /** `api-keys` when the config turns API keys on. */
978
+ type ApiKeysOf<C extends Config> = C extends {
979
+ readonly apiKeys: true;
980
+ } ? BuiltinApiKeysCollection : never;
981
+ /** The `PluginTypes` of each plugin in the config (a union). */
982
+ type PluginTypesOf<C extends Config> = C extends {
983
+ readonly plugins: readonly (infer P)[];
984
+ } ? P extends TypedPlugin<infer T extends PluginTypes> ? T : never : never;
985
+ /** Fields that plugins add to the collection (or global) `S`, as a union. */
986
+ type AddedFields<T, S> = T extends {
987
+ readonly [K in 'fields' | 'globalFields']?: infer M;
988
+ } ? M extends {
989
+ readonly [slug: string]: readonly Field[];
990
+ } ? string extends keyof M ? never : S extends keyof M ? M[S][number] : never : never : never;
991
+ type PluginFields<C extends Config, S, Key extends 'fields' | 'globalFields'> = AddedFields<PluginTypesOf<C> extends infer T ? T extends PluginTypes ? Pick<T, Key & keyof T> : never : never, S>;
992
+ /** Collections that plugins add. */
993
+ type PluginCollections<C extends Config> = PluginTypesOf<C> extends infer T ? T extends {
994
+ readonly collections: readonly (infer K extends CollectionConfig)[];
995
+ } ? K : never : never;
996
+ /** A collection or global with the fields plugins add to it. */
997
+ type WithPluginFields<C extends Config, T extends {
998
+ readonly slug: string;
999
+ readonly fields: readonly Field[];
1000
+ }, Key extends 'fields' | 'globalFields'> = [PluginFields<C, T['slug'], Key>] extends [never] ? T : Omit<T, 'fields'> & {
1001
+ readonly fields: readonly (T['fields'][number] | PluginFields<C, T['slug'], Key>)[];
1002
+ };
1003
+ /**
1004
+ * The config's own collections. A literal config without `collections` has none (only plugins
1005
+ * add some); a loosely typed `Config` has any.
1006
+ */
1007
+ type OwnCollections<C extends Config> = 'collections' extends keyof C ? NonNullable<C['collections']>[number] : never;
1008
+ /** Every collection of a config, including the built-in ones and those plugins add. */
1009
+ type AllCollections<C extends Config> = OwnCollections<C> | BuiltinUsersCollection | BuiltinMediaCollection | ApiKeysOf<C> | FoldersOf<C> | PluginCollections<C>;
1010
+ /** The built-in media collection; its documents are typed as `MediaDocument`. */
1011
+ interface BuiltinMediaCollection {
1012
+ readonly slug: 'media';
1013
+ readonly fields: readonly [];
1014
+ }
1015
+ type CollectionBySlug<C extends Config, S> = WithPluginFields<C, Extract<AllCollections<C>, {
1016
+ readonly slug: S;
1017
+ }>, 'fields'>;
1018
+ type GlobalBySlug<C extends Config, S> = WithPluginFields<C, Extract<NonNullable<C['globals']>[number], {
1019
+ readonly slug: S;
1020
+ }>, 'globalFields'>;
1021
+ /** Relationships are ids at depth 0 and documents when populated. */
1022
+ type RelationValue<C extends Config, S> = [CollectionBySlug<C, S>] extends [never] ? ID | Record<string, unknown> : ID | CollectionDocument<C, S & string>;
1023
+ interface MediaSize {
1024
+ filename: string;
1025
+ width: number;
1026
+ height: number;
1027
+ filesize: number;
1028
+ url: string;
1029
+ }
1030
+ /** A document of the built-in `media` collection, as returned by the API. */
1031
+ interface MediaDocument {
1032
+ id: ID;
1033
+ filename: string;
1034
+ originalName?: string | null;
1035
+ mimeType: string;
1036
+ filesize: number;
1037
+ width?: number | null;
1038
+ height?: number | null;
1039
+ alt?: string | null;
1040
+ /** Its media folder (`upload.folders`), when it is in one. */
1041
+ folder?: ID | null;
1042
+ /** In a private folder: `url` needs a signed-in user who may see it, or `cms.signedMediaURL()`. */
1043
+ private?: boolean | null;
1044
+ /** URL of the file: public, or for private files the API's, which checks who asks. */
1045
+ url: string;
1046
+ /** Resized copies, when `upload.imageSizes` is set and sharp is installed. */
1047
+ sizes: Record<string, MediaSize>;
1048
+ createdAt: string;
1049
+ updatedAt: string;
1050
+ [field: string]: unknown;
1051
+ }
1052
+ type FieldValue<F extends Field, C extends Config = Config> = F extends {
1053
+ readonly type: 'text' | 'textarea' | 'email' | 'slug' | 'date';
1054
+ } ? string : F extends {
1055
+ readonly type: 'number';
1056
+ } ? number : F extends {
1057
+ readonly type: 'boolean';
1058
+ } ? boolean : F extends {
1059
+ readonly type: 'json';
1060
+ } ? unknown : F extends {
1061
+ readonly type: 'select';
1062
+ } ? SelectValue<F> : F extends {
1063
+ readonly type: 'richText';
1064
+ } ? RichTextDocument : F extends {
1065
+ readonly type: 'upload';
1066
+ } ? F extends {
1067
+ readonly hasMany: true;
1068
+ } ? (ID | MediaDocument)[] : ID | MediaDocument : F extends {
1069
+ readonly type: 'relationship';
1070
+ readonly to: infer S;
1071
+ } ? F extends {
1072
+ readonly hasMany: true;
1073
+ } ? RelationValue<C, S>[] : RelationValue<C, S> : F extends {
1074
+ readonly type: 'array';
1075
+ readonly fields: infer Sub extends readonly Field[];
1076
+ } ? Simplify<FieldsValue<Sub, C> & {
1077
+ id: string;
1078
+ }>[] : F extends {
1079
+ readonly type: 'group';
1080
+ readonly fields: infer Sub extends readonly Field[];
1081
+ } ? FieldsValue<Sub, C> : F extends {
1082
+ readonly type: 'blocks';
1083
+ readonly blocks: infer B extends readonly Block[];
1084
+ } ? BlockValue<B[number], C>[] : CustomValue<F>;
1085
+ /** The value of a field of an added type (`CustomFieldTypes`). */
1086
+ type CustomValue<F> = F extends {
1087
+ readonly type: infer T extends keyof CustomFieldTypes;
1088
+ } ? CustomFieldTypes[T] extends {
1089
+ value: infer V;
1090
+ } ? V : unknown : never;
1091
+ /** One row of a `blocks` field: the block's fields plus `id` and `blockType` (distributes over kinds). */
1092
+ type BlockValue<B, C extends Config> = B extends {
1093
+ readonly slug: infer S;
1094
+ readonly fields: infer Sub extends readonly Field[];
1095
+ } ? Simplify<FieldsValue<Sub, C> & {
1096
+ id: string;
1097
+ blockType: S;
1098
+ }> : never;
1099
+ type BlockInput<B> = B extends {
1100
+ readonly slug: infer S;
1101
+ readonly fields: infer Sub extends readonly Field[];
1102
+ } ? Simplify<FieldsInput<Sub> & {
1103
+ id?: string;
1104
+ blockType: S;
1105
+ }> : never;
1106
+ type Visible<Fs extends readonly Field[]> = Exclude<Fs[number], {
1107
+ readonly hidden: true;
1108
+ }>;
1109
+ /** Always set when read: required fields, and lists and groups (empty `[]` / `{}` otherwise). */
1110
+ type Present = {
1111
+ readonly required: true;
1112
+ } | {
1113
+ readonly type: 'array' | 'blocks' | 'group';
1114
+ } | {
1115
+ readonly hasMany: true;
1116
+ };
1117
+ type RequiredFields<Fs extends readonly Field[]> = Extract<Visible<Fs>, Present>;
1118
+ type OptionalFields<Fs extends readonly Field[]> = Exclude<Visible<Fs>, Present>;
1119
+ /** The shape of the data described by a list of fields. */
1120
+ type FieldsValue<Fs extends readonly Field[], C extends Config = Config> = Simplify<{
1121
+ -readonly [F in RequiredFields<Fs> as F['name']]: FieldValue<F, C>;
1122
+ } & {
1123
+ -readonly [F in OptionalFields<Fs> as F['name']]?: FieldValue<F, C> | null;
1124
+ }>;
1125
+ type SystemFields<T extends {
1126
+ readonly drafts?: unknown;
1127
+ }> = {
1128
+ id: ID;
1129
+ createdAt: string;
1130
+ updatedAt: string;
1131
+ } & (T extends {
1132
+ readonly drafts: true;
1133
+ } ? {
1134
+ status: 'draft' | 'published';
1135
+ } : unknown);
1136
+ /** What document types are inferred from: a collection's or global's fields and drafts. */
1137
+ type FieldsOwner = {
1138
+ readonly fields: readonly Field[];
1139
+ readonly drafts?: unknown;
1140
+ };
1141
+ type InferCollection<T extends FieldsOwner, C extends Config = Config> = Simplify<SystemFields<T> & FieldsValue<T['fields'], C>>;
1142
+ type InferGlobal<T extends FieldsOwner, C extends Config = Config> = Simplify<
1143
+ /** `null` until the global is saved for the first time. */
1144
+ {
1145
+ updatedAt: string | null;
1146
+ } & (T extends {
1147
+ readonly drafts: true;
1148
+ } ? {
1149
+ status: 'draft' | 'published';
1150
+ } : unknown) & FieldsValue<T['fields'], C>>;
1151
+ type CollectionSlug<C extends Config> = OwnCollections<C>['slug'] | 'users' | 'media' | ApiKeysOf<C>['slug'] | FoldersOf<C>['slug'] | PluginCollections<C>['slug'];
1152
+ type GlobalSlug<C extends Config> = NonNullable<C['globals']>[number]['slug'];
1153
+ /** Document type of a collection, e.g. `CollectionDocument<typeof config, 'posts'>`. */
1154
+ type CollectionDocument<C extends Config, S extends CollectionSlug<C>> = S extends 'media' ? MediaDocument : InferCollection<CollectionBySlug<C, S>, C>;
1155
+ /** Data type of a global, e.g. `GlobalDocument<typeof config, 'site'>`. */
1156
+ type GlobalDocument<C extends Config, S extends GlobalSlug<C>> = InferGlobal<GlobalBySlug<C, S>, C>;
1157
+ type InputValue<F extends Field> = F extends {
1158
+ readonly type: 'relationship';
1159
+ } ? F extends {
1160
+ readonly hasMany: true;
1161
+ } ? readonly ID[] : ID : F extends {
1162
+ readonly type: 'select';
1163
+ readonly hasMany: true;
1164
+ } ? Readonly<SelectValue<F>> : F extends {
1165
+ readonly type: 'upload';
1166
+ } ? F extends {
1167
+ readonly hasMany: true;
1168
+ } ? readonly ID[] : ID : F extends {
1169
+ readonly type: 'date';
1170
+ } ? string | Date : F extends {
1171
+ readonly type: 'array';
1172
+ readonly fields: infer Sub extends readonly Field[];
1173
+ } ? readonly Simplify<FieldsInput<Sub> & {
1174
+ id?: string;
1175
+ }>[] : F extends {
1176
+ readonly type: 'group';
1177
+ readonly fields: infer Sub extends readonly Field[];
1178
+ } ? FieldsInput<Sub> : F extends {
1179
+ readonly type: 'blocks';
1180
+ readonly blocks: infer B extends readonly Block[];
1181
+ } ? readonly BlockInput<B[number]>[] : FieldValue<F>;
1182
+ /**
1183
+ * Required fields must be given, unless Easy CMS can fill them (default value, slug from another
1184
+ * field) or a condition may hide them (`admin.condition`).
1185
+ */
1186
+ type NeedsInput<F extends Field> = F extends {
1187
+ readonly required: true;
1188
+ } ? F extends {
1189
+ readonly defaultValue: unknown;
1190
+ } ? never : F extends {
1191
+ readonly type: 'slug';
1192
+ readonly from: string;
1193
+ } ? never : F extends {
1194
+ readonly admin: {
1195
+ readonly condition: object;
1196
+ };
1197
+ } ? never : F : never;
1198
+ type FieldsInput<Fs extends readonly Field[]> = Simplify<{
1199
+ -readonly [F in NeedsInput<Visible<Fs>> as F['name']]: InputValue<F>;
1200
+ } & {
1201
+ -readonly [F in Exclude<Visible<Fs>, NeedsInput<Visible<Fs>>> as F['name']]?: InputValue<F> | null;
1202
+ }>;
1203
+ type StatusInput<T> = T extends {
1204
+ readonly drafts: true;
1205
+ } ? {
1206
+ status?: 'draft' | 'published';
1207
+ } : unknown;
1208
+ /**
1209
+ * A user's password, hashed and never returned. Optional when creating: a user without one sets
1210
+ * it from an invitation link.
1211
+ */
1212
+ type PasswordInput<S, Required extends boolean> = S extends 'users' ? Required extends true ? {
1213
+ password: string;
1214
+ } : {
1215
+ password?: string;
1216
+ } : unknown;
1217
+ /** Data accepted by `create`, e.g. `CreateInput<typeof config, 'posts'>`. */
1218
+ type CreateInput<C extends Config, S extends CollectionSlug<C>> = Simplify<FieldsInput<CollectionBySlug<C, S>['fields']> & StatusInput<CollectionBySlug<C, S>> & PasswordInput<S, false>>;
1219
+ /** Data accepted by `update`: any subset of `CreateInput`. */
1220
+ type UpdateInput<C extends Config, S extends CollectionSlug<C>> = Partial<CreateInput<C, S>>;
1221
+ /** Data accepted by `updateGlobal`. */
1222
+ type GlobalInput<C extends Config, S extends GlobalSlug<C>> = Partial<Simplify<FieldsInput<GlobalBySlug<C, S>['fields']> & StatusInput<GlobalBySlug<C, S>>>>;
1223
+
1224
+ /** What a role may do with a collection or global, ticked in Settings → Roles. */
1225
+ declare const ROLE_OPERATIONS: readonly ["read", "create", "update", "delete", "publish"];
1226
+ type RoleOperation = (typeof ROLE_OPERATIONS)[number];
1227
+ /** A field a role may only read, or not see at all; fields not listed follow the collection. */
1228
+ type FieldRule = 'read' | 'hidden';
1229
+ type FieldRules = Readonly<Record<string, Readonly<Record<string, FieldRule>>>>;
1230
+ /**
1231
+ * Stored in a role's `permissions`. Anything not listed is not allowed. A slug listed with no
1232
+ * operations has been looked at and given nothing (it is not "new" in Settings → Roles).
1233
+ */
1234
+ interface RolePermissions {
1235
+ readonly collections?: Readonly<Record<string, readonly RoleOperation[]>>;
1236
+ readonly globals?: Readonly<Record<string, readonly RoleOperation[]>>;
1237
+ /** Admin pages: `status`, `deliveries`, `page:<path>` (`admin.pages`), `widget:<id>` (`admin.dashboard`, default the tag). */
1238
+ readonly admin?: readonly string[];
1239
+ /** Operations of `collections` allowed only on the role's own documents (see `admin.ownerField`). */
1240
+ readonly own?: Readonly<Record<string, readonly RoleOperation[]>>;
1241
+ /** Top-level fields the role may only read or not see, by collection or global slug. */
1242
+ readonly fields?: {
1243
+ readonly collections?: FieldRules;
1244
+ readonly globals?: FieldRules;
1245
+ };
1246
+ }
1247
+ /** One role as Settings → Roles shows it (`GET <api>/admin/roles`). */
1248
+ interface AdminRole {
1249
+ id: ID;
1250
+ key: string;
1251
+ /** The name shown in the admin; empty for roles from the config that were not renamed. */
1252
+ name: string;
1253
+ /** From `auth.roles`: can't be deleted. */
1254
+ system: boolean;
1255
+ /** Users with this role. */
1256
+ users: number;
1257
+ permissions: RolePermissions;
1258
+ updatedAt: string;
1259
+ }
1260
+ /** A top-level field whose permission can be set per role. */
1261
+ interface AdminRoleField {
1262
+ name: string;
1263
+ label?: Label;
1264
+ /** Required without a default value: roles that create documents must be able to fill it. */
1265
+ required: boolean;
1266
+ /** The collection's owner field (`admin.ownerField`). */
1267
+ owner?: boolean;
1268
+ }
1269
+ /** A row of the permissions table. */
1270
+ interface AdminRoleTarget {
1271
+ slug: string;
1272
+ /** Operations that make sense here (`publish` with drafts). */
1273
+ ops: RoleOperation[];
1274
+ /** No role has been given anything here yet (added to the config since). */
1275
+ new: boolean;
1276
+ fields: AdminRoleField[];
1277
+ }
1278
+ /** A page or dashboard panel that can be given to roles. */
1279
+ interface AdminRoleView {
1280
+ id: string;
1281
+ /** For plugin pages and panels; the admin names `status` and `deliveries` itself. */
1282
+ label?: Label;
1283
+ }
1284
+ /** Settings → Roles (`GET <api>/admin/roles`). */
1285
+ interface AdminRoles {
1286
+ roles: AdminRole[];
1287
+ collections: (AdminRoleTarget & {
1288
+ /** Collections its relationship and upload fields point to. */
1289
+ references: string[];
1290
+ /** Can be limited to own documents: the field that names the owner (`createdBy` by default). */
1291
+ owner?: string;
1292
+ })[];
1293
+ globals: AdminRoleTarget[];
1294
+ views: AdminRoleView[];
1295
+ }
1296
+ /** One change to a role (`GET <api>/admin/roles/:id/history`). */
1297
+ interface AdminRoleChange {
1298
+ id: ID;
1299
+ /** Email of who saved it, or `null` when it was the system (first start). */
1300
+ author: string | null;
1301
+ createdAt: string;
1302
+ name: string;
1303
+ permissions: RolePermissions;
1304
+ }
1305
+ /**
1306
+ * Roles and their permissions (`auth.rbac`), on top of access rules: an operation is allowed
1307
+ * when both allow it. Admins may do everything; requests that are not logged in are left to
1308
+ * access rules alone.
1309
+ */
1310
+ declare class Roles {
1311
+ private readonly config;
1312
+ private readonly db;
1313
+ private readonly log?;
1314
+ /** Writes to the audit log (`audit`). */
1315
+ private readonly audit?;
1316
+ readonly enabled: boolean;
1317
+ private readonly versions;
1318
+ private cache;
1319
+ private synced;
1320
+ constructor(config: ResolvedConfig, db: Database, log?: ((message: string) => void) | undefined,
1321
+ /** Writes to the audit log (`audit`). */
1322
+ audit?: ((entry: {
1323
+ action: string;
1324
+ target: string;
1325
+ doc: ID;
1326
+ title: string;
1327
+ changes?: {
1328
+ field: string;
1329
+ before?: unknown;
1330
+ after?: unknown;
1331
+ }[];
1332
+ }) => Promise<void>) | undefined);
1333
+ /**
1334
+ * Whether the user's role allows an operation: `true`, `false`, or for their own account
1335
+ * (users without read or update on `users`) a constraint to it.
1336
+ */
1337
+ allows(user: AuthUser | null, target: {
1338
+ collection: string;
1339
+ } | {
1340
+ global: string;
1341
+ }, operation: RoleOperation): Promise<boolean | Where>;
1342
+ /**
1343
+ * Whether the user may open an admin page: admins always; with roles, those whose role has
1344
+ * it; without roles, nobody else (`status`, `deliveries`) or everybody (plugin pages).
1345
+ */
1346
+ canView(user: AuthUser, id: string): Promise<boolean>;
1347
+ /** Documents a user owns, by collection (`createdBy` and owner fields), for deleting them. */
1348
+ owned(userId: ID): Promise<{
1349
+ collection: string;
1350
+ count: number;
1351
+ }[]>;
1352
+ private addMissing;
1353
+ /**
1354
+ * Documents from before roles have no `createdBy`: it is filled from who saved their first
1355
+ * version, for collections with versions. Only while no document of the collection has one,
1356
+ * so it runs once (and finds nothing to do afterwards).
1357
+ */
1358
+ private backfillCreators;
1359
+ /** Whether a role exists (users can only be given existing roles). */
1360
+ exists(key: string): Promise<boolean>;
1361
+ /** Roles a user can be given, those of `auth.roles` first; `name` may be empty. */
1362
+ options(): Promise<{
1363
+ key: string;
1364
+ name: string;
1365
+ }[]>;
1366
+ list(): Promise<AdminRoles>;
1367
+ create(input: {
1368
+ key?: unknown;
1369
+ name?: unknown;
1370
+ permissions?: unknown;
1371
+ }, author: AuthUser): Promise<AdminRole>;
1372
+ update(id: ID, input: {
1373
+ name?: unknown;
1374
+ permissions?: unknown;
1375
+ }, author: AuthUser): Promise<AdminRole>;
1376
+ delete(id: ID): Promise<void>;
1377
+ history(id: ID): Promise<AdminRoleChange[]>;
1378
+ /** Pages and panels roles can be given. */
1379
+ private views;
1380
+ /** Every collection and global, and the plugin pages and panels: what roles had before roles. */
1381
+ private everything;
1382
+ /** Keeps only known collections, globals, operations and pages; `read` comes with the others. */
1383
+ private clean;
1384
+ /**
1385
+ * Keeps rules for known top-level fields. A field that must be filled in (required, without a
1386
+ * default) can't be read-only or hidden for a role that creates documents.
1387
+ */
1388
+ private cleanRules;
1389
+ /** Fields that name a collection's owner (`createdBy`, and `admin.ownerField`). */
1390
+ private ownerColumns;
1391
+ /** Sets one field of every matching document, keeping the rest as stored. */
1392
+ private setWhere;
1393
+ private cleanName;
1394
+ private permissionsOf;
1395
+ private load;
1396
+ private rows;
1397
+ private row;
1398
+ private users;
1399
+ private insert;
1400
+ /** Keeps the role as saved, for its history. */
1401
+ private record;
1402
+ private toRole;
1403
+ }
1404
+
1405
+ /** What a role may do in a folder (`upload.folders` with `auth.rbac`), each including the last. */
1406
+ declare const FOLDER_LEVELS: readonly ["view", "edit", "manage"];
1407
+ /**
1408
+ * `view`: see its files and use them in documents. `edit`: also upload, rename, move and delete
1409
+ * files. `manage`: also create, rename, move and delete subfolders.
1410
+ */
1411
+ type FolderLevel = (typeof FOLDER_LEVELS)[number];
1412
+ /** A folder's `permissions`: each role's level; roles not listed get nothing. */
1413
+ type FolderPermissions = Readonly<Record<string, FolderLevel>>;
1414
+ /**
1415
+ * Folders of the media library and their permissions. A folder without its own permissions
1416
+ * follows its parent; the top level is open to every role that may use `media`. Folder
1417
+ * permissions only narrow what roles may do with `media`, for signed-in users (not API keys).
1418
+ */
1419
+ declare class MediaFolders {
1420
+ private readonly db;
1421
+ private readonly roles;
1422
+ readonly enabled: boolean;
1423
+ private cache;
1424
+ constructor(config: ResolvedConfig, db: Database, roles: Roles);
1425
+ /** Whether folder permissions apply to this user. */
1426
+ applies(user: AuthUser | null): user is AuthUser;
1427
+ private rows;
1428
+ /** A role's level in a folder (`null`: the top level): the nearest permissions up the tree. */
1429
+ private levelIn;
1430
+ /** Whether files in a folder (`null`: the top level) are private: it or a folder above is. */
1431
+ private privateIn;
1432
+ /** Whether files put in this folder are private. */
1433
+ isPrivate(id: ID | null): Promise<boolean>;
1434
+ /** The folder and every folder inside it. */
1435
+ private within;
1436
+ /**
1437
+ * The folder with this key (`folder: 'banners'` on upload fields), made at the top level when
1438
+ * there is none yet.
1439
+ */
1440
+ /**
1441
+ * The folder with this key, made when first needed. `scope`: the values the key is unique
1442
+ * within (`uniqueWithin` of the `key` field, e.g. the tenant), given to a new folder too.
1443
+ * `null` when there is none and `create` is false.
1444
+ */
1445
+ keyed(key: string, scope?: Readonly<Record<string, unknown>>): Promise<ID>;
1446
+ keyed(key: string, scope: Readonly<Record<string, unknown>>, create: boolean): Promise<ID | null>;
1447
+ /** The user's level in a folder (`null`: the top level). */
1448
+ level(user: AuthUser | null, id: ID | null): Promise<FolderLevel | 'none'>;
1449
+ /**
1450
+ * The constraint folder permissions add to an operation on files or folders, or `true` when
1451
+ * they add none (no folder limits this user).
1452
+ */
1453
+ where(user: AuthUser | null, collection: string, operation: string): Promise<true | Where>;
1454
+ private fresh;
1455
+ }
1456
+
1457
+ interface StoredFile {
1458
+ readonly body: Uint8Array;
1459
+ readonly size: number;
1460
+ /** The type the storage serves the file with, when it says (checked after direct uploads). */
1461
+ readonly contentType?: string;
1462
+ }
1463
+ /** Where uploaded files live. Keys are generated by Easy CMS: `<name>-<random>.<ext>`. */
1464
+ interface StorageAdapter {
1465
+ readonly name: string;
1466
+ /**
1467
+ * The adapter interface it is written for (`ADAPTER_API_VERSION`); Easy CMS refuses another.
1468
+ * Leave it out to skip the check.
1469
+ */
1470
+ readonly apiVersion?: number;
1471
+ /** Called once with the project root, before any other method. */
1472
+ init?(args: {
1473
+ cwd: string;
1474
+ }): void | Promise<void>;
1475
+ put(key: string, data: Uint8Array, options: {
1476
+ contentType: string;
1477
+ }): Promise<void>;
1478
+ get(key: string): Promise<StoredFile | null>;
1479
+ delete(key: string): Promise<void>;
1480
+ /**
1481
+ * Public URL of a file, or `undefined` to serve it through the REST API
1482
+ * (`<api>/media/file/<key>`), as the local adapter does.
1483
+ */
1484
+ url?(key: string): string | undefined;
1485
+ /**
1486
+ * Lets a browser send a file straight to the storage (large files, past a host's request
1487
+ * limit): where to send it and how. The URL must work for `expiresIn` seconds and accept at
1488
+ * most `size` bytes. Without it, files are uploaded through the server.
1489
+ */
1490
+ uploadURL?(key: string, options: {
1491
+ contentType: string;
1492
+ size: number;
1493
+ expiresIn: number;
1494
+ }): Promise<DirectUpload>;
1495
+ /**
1496
+ * The first `bytes` of a file and its whole size, without reading it all (to check a file
1497
+ * uploaded straight to the storage); `null` when there is no such file. Default: `get`.
1498
+ */
1499
+ getStart?(key: string, bytes: number): Promise<StoredFile | null>;
1500
+ }
1501
+ /** Where and how a browser sends a file straight to the storage (`StorageAdapter.uploadURL`). */
1502
+ interface DirectUpload {
1503
+ readonly url: string;
1504
+ readonly method: 'PUT' | 'POST';
1505
+ /** Headers to send with the file. */
1506
+ readonly headers: Readonly<Record<string, string>>;
1507
+ }
1508
+ interface DiskStorageOptions {
1509
+ /** Directory, relative to the project root. Default `uploads`. */
1510
+ readonly dir?: string;
1511
+ }
1512
+ /** Stores files on the local disk. Suitable for servers with a persistent filesystem. */
1513
+ declare function diskStorage(options?: DiskStorageOptions): StorageAdapter;
1514
+
1515
+ declare const DEFAULT_MAX_VERSIONS = 50;
1516
+ /** One saved state of a document or global. */
1517
+ interface VersionSummary {
1518
+ readonly id: ID;
1519
+ /** `draft` or `published` with drafts enabled, otherwise `null`. */
1520
+ readonly status: 'draft' | 'published' | null;
1521
+ /** The newest version: the document's current editable state. */
1522
+ readonly latest: boolean;
1523
+ /** Who saved it (user id), or `null` for saves without a user (scripts, seeds). */
1524
+ readonly author: ID | null;
1525
+ readonly createdAt: string;
1526
+ }
1527
+ interface Version<T = Record<string, unknown>> extends VersionSummary {
1528
+ /** The document as it was saved (hidden fields such as password hashes are never kept). */
1529
+ readonly data: T;
1530
+ }
1531
+
1532
+ type Data$1 = Record<string, unknown>;
1533
+ type Slug<C extends Config> = string extends CollectionSlug<C> ? string : CollectionSlug<C>;
1534
+ type GSlug<C extends Config> = string extends GlobalSlug<C> ? string : GlobalSlug<C>;
1535
+ type Doc<C extends Config, S> = string extends CollectionSlug<C> ? RawDocument : CollectionDocument<C, S & CollectionSlug<C>>;
1536
+ type Create<C extends Config, S> = string extends CollectionSlug<C> ? Data$1 : CreateInput<C, S & CollectionSlug<C>>;
1537
+ type Update<C extends Config, S> = string extends CollectionSlug<C> ? Data$1 : UpdateInput<C, S & CollectionSlug<C>>;
1538
+ type GDoc<C extends Config, S> = string extends GlobalSlug<C> ? Data$1 : GlobalDocument<C, S & GlobalSlug<C>>;
1539
+ type GInput<C extends Config, S> = string extends GlobalSlug<C> ? Data$1 : GlobalInput<C, S & GlobalSlug<C>>;
1540
+ /** A collection slug of the config `C` (any string when the config isn't a literal). */
1541
+ type SlugOf<C extends Config> = Slug<C>;
1542
+ /**
1543
+ * A document of collection `S` as the Local API returns it, e.g. for helpers that take an
1544
+ * `EasyCMS<C>`: inferred from a literal config, loose otherwise.
1545
+ */
1546
+ type DocumentOf<C extends Config, S> = Doc<C, S>;
1547
+ interface AccessOptions {
1548
+ /**
1549
+ * The Local API trusts its caller and skips access rules by default.
1550
+ * Pass `false` (with `user`) to apply them, as the REST API does.
1551
+ */
1552
+ readonly overrideAccess?: boolean;
1553
+ /** The user to check access for. `null` = not logged in. */
1554
+ readonly user?: AuthUser | null;
1555
+ /**
1556
+ * What the call is about besides its user, e.g. the tenant (see `onRequest`): given to access
1557
+ * functions, hooks and `filterOptions`.
1558
+ */
1559
+ readonly context?: RequestContext;
1560
+ }
1561
+ interface DepthOptions extends AccessOptions {
1562
+ /** How many levels of relationships to populate. Default 1, maximum 3. */
1563
+ readonly depth?: number;
1564
+ /**
1565
+ * With `localization`: the locale to read, and for writes the locale the data is in. Default:
1566
+ * the default locale. `'all'` reads (and writes) localized fields as `{ [locale]: value }`.
1567
+ */
1568
+ readonly locale?: string;
1569
+ }
1570
+ interface UpdateOptions extends DepthOptions {
1571
+ /**
1572
+ * Upkeep of the live document, e.g. values a plugin keeps up to date (a page's path): with
1573
+ * drafts and versions a pending draft stays as it is (edits go to it by default), the status
1574
+ * stays, and no version is added to the history.
1575
+ */
1576
+ readonly live?: boolean;
1577
+ /**
1578
+ * Saves only when the stored document still matches this at the moment of writing (compare and
1579
+ * set), e.g. `{ status: { equals: 'pending' } }` so that one of two concurrent calls wins.
1580
+ * `update()` then returns `null` when it doesn't match, after its `before*` hooks ran.
1581
+ */
1582
+ readonly where?: Where;
1583
+ }
1584
+ interface IncrementOptions {
1585
+ /** Only when the result stays at or above this, e.g. `0` for stock. */
1586
+ readonly min?: number;
1587
+ /** Only when the result stays at or below this. */
1588
+ readonly max?: number;
1589
+ }
1590
+ interface ReadOptions extends DepthOptions {
1591
+ /**
1592
+ * Include drafts. By default only published documents of collections with
1593
+ * `drafts: true` are returned, including when populating relationships (FR-DRF-04).
1594
+ */
1595
+ readonly draft?: boolean;
1596
+ /** Use the default locale's value when a localized value is empty. Default: `localization.fallback`. */
1597
+ readonly fallbackLocale?: boolean;
1598
+ }
1599
+ interface FindOptions extends ReadOptions {
1600
+ readonly where?: Where;
1601
+ /** Field path, `-` prefix for descending. Default `-createdAt`. */
1602
+ readonly sort?: string | readonly string[];
1603
+ /** Default 10. `0` returns every document. */
1604
+ readonly limit?: number;
1605
+ /** 1-based. Default 1. */
1606
+ readonly page?: number;
1607
+ }
1608
+ interface CreateEasyCMSOptions {
1609
+ /** Project root. Default `process.cwd()`. */
1610
+ readonly cwd?: string;
1611
+ /**
1612
+ * Run due scheduled jobs and webhook retries every minute in this process. Default: on when
1613
+ * a collection or global has `schedule`, or `webhooks`, `email` or `jobs` is set. Turn it off
1614
+ * where a cron calls `runJobs` (or `GET <api>/jobs/run`) instead.
1615
+ */
1616
+ readonly scheduler?: boolean;
1617
+ /** Default `verify` when `NODE_ENV=production`, otherwise `push`. */
1618
+ readonly schema?: SchemaMode;
1619
+ readonly logger?: Logger;
1620
+ readonly interactive?: boolean;
1621
+ }
1622
+ /** Connects to the database and returns the Local API. */
1623
+ declare function createEasyCMS<const C extends Config>(config: C | ResolvedConfig, options?: CreateEasyCMSOptions): Promise<EasyCMS<C>>;
1624
+ /** A scheduled publish or unpublish. */
1625
+ interface ScheduledPublish {
1626
+ readonly id: ID;
1627
+ readonly action: 'publish' | 'unpublish';
1628
+ /** ISO time the job runs at (or after). */
1629
+ readonly runAt: string;
1630
+ readonly state: 'pending' | 'done' | 'failed';
1631
+ readonly error: string | null;
1632
+ readonly author: ID | null;
1633
+ }
1634
+ declare class EasyCMS<C extends Config = Config> {
1635
+ readonly config: ResolvedConfig;
1636
+ readonly db: Database;
1637
+ readonly logger: Logger;
1638
+ /** Login, logout and session checks. */
1639
+ readonly auth: Auth;
1640
+ /** Where uploaded files are stored. */
1641
+ readonly storage: StorageAdapter;
1642
+ /**
1643
+ * Where files in private folders are stored (`upload.privateStorage`, else `storage` when it has
1644
+ * no public URLs); `null` when private folders can't be used.
1645
+ */
1646
+ readonly privateStorage: StorageAdapter | null;
1647
+ /** Project root: relative paths in the config (e.g. `admin.modules`) start here. */
1648
+ readonly cwd: string;
1649
+ /** Roles and their permissions (`auth.rbac`, Settings → Roles). */
1650
+ readonly roles: Roles;
1651
+ /** Who did what (`audit`, Settings → Audit log). */
1652
+ readonly audit: AuditLog;
1653
+ /** Folders of the media library and their permissions (`upload.folders`). */
1654
+ readonly folders: MediaFolders;
1655
+ private readonly versions;
1656
+ private readonly webhooks;
1657
+ private readonly mailer;
1658
+ /**
1659
+ * The user and context of a request, as the REST API works them out: its session cookie or
1660
+ * Bearer token, then `onRequest`. For framework routes and server code that act for a visitor;
1661
+ * pass both to the Local API with `overrideAccess: false`.
1662
+ */
1663
+ forRequest(request: Request | Headers): Promise<{
1664
+ user: AuthUser | null;
1665
+ context: RequestContext;
1666
+ }>;
1667
+ find<S extends Slug<C>>(collection: S, options?: FindOptions): Promise<PaginatedDocs<Doc<C, S>>>;
1668
+ findById<S extends Slug<C>>(collection: S, id: ID, options?: ReadOptions): Promise<Doc<C, S> | null>;
1669
+ count<S extends Slug<C>>(collection: S, options?: {
1670
+ where?: Where;
1671
+ draft?: boolean;
1672
+ locale?: string;
1673
+ } & AccessOptions): Promise<number>;
1674
+ create<S extends Slug<C>>(collection: S, data: Create<C, S>, options?: DepthOptions): Promise<Doc<C, S>>;
1675
+ /**
1676
+ * Stores a file and creates its `media` document (FR-UPL). The type is detected from
1677
+ * the contents; images get width/height, and resized copies when `sharp` is installed.
1678
+ */
1679
+ upload(file: {
1680
+ data: Uint8Array;
1681
+ name: string;
1682
+ }, data?: Record<string, unknown>, options?: DepthOptions): Promise<MediaDocument>;
1683
+ /**
1684
+ * Starts an upload straight to the storage, for files too large to send through the server
1685
+ * (`POST <api>/media/uploads`): checks who uploads what where, as `upload()` does, and returns
1686
+ * a signed ticket and where to send the file. `upload` is `null` when the storage can't take
1687
+ * files directly: send it to `upload()` instead. Then `completeUpload(ticket)`.
1688
+ */
1689
+ createUpload(file: {
1690
+ name: string;
1691
+ size: number;
1692
+ type?: string;
1693
+ }, data?: Record<string, unknown>, options?: AccessOptions): Promise<{
1694
+ ticket: string;
1695
+ upload: DirectUpload | null;
1696
+ }>;
1697
+ /**
1698
+ * Finishes an upload made with `createUpload()` (`POST <api>/media/uploads/complete`): checks
1699
+ * the file in the storage like any upload (its size, and its type from its contents), then
1700
+ * makes its media document. A file that fails is deleted.
1701
+ */
1702
+ completeUpload(ticket: string, options?: DepthOptions): Promise<MediaDocument>;
1703
+ /** Who may upload, and into which folder: `upload()` and direct uploads. */
1704
+ private checkUpload;
1705
+ /** A value with its HMAC (`secret`), for `purpose` only: `<base64url JSON>.<signature>`. */
1706
+ private sign;
1707
+ /** The value of a token made by `sign`, or `undefined` when it isn't genuine. */
1708
+ private verify;
1709
+ /**
1710
+ * Downloads a file from an `http(s)` link and uploads it like `upload()`. Private network
1711
+ * addresses are refused unless `upload.fromURL.allowPrivate`. Calls on behalf of a user (the
1712
+ * REST API, `overrideAccess: false`) also need `upload.fromURL` and a host in its `allowedHosts`.
1713
+ */
1714
+ uploadFromURL(url: string, data?: Record<string, unknown>, options?: DepthOptions): Promise<MediaDocument>;
1715
+ /**
1716
+ * Creates an API key (needs `apiKeys: true`) and returns it once: only a hash is stored.
1717
+ * With access enforced the key belongs to `user`; trusted calls name the owner in `data.user`.
1718
+ */
1719
+ createApiKey(data: {
1720
+ name: string;
1721
+ permissions?: ApiKeyPermissions;
1722
+ expiresAt?: string | Date | null;
1723
+ user?: ID;
1724
+ }, options?: AccessOptions): Promise<{
1725
+ key: string;
1726
+ doc: Record<string, unknown>;
1727
+ }>;
1728
+ /** Public URL of a stored file. */
1729
+ mediaURL(key: string): string;
1730
+ /**
1731
+ * A link to a private file that works without signing in until it expires: `expiresIn` in
1732
+ * seconds or as `'30m'`, `'1h'` (the default), `'7d'` (the most). Public files get their usual
1733
+ * URL. `size`: one of the file's resized copies. Links stop working when `secret` changes or
1734
+ * the file stops being private.
1735
+ */
1736
+ signedMediaURL(doc: {
1737
+ filename?: unknown;
1738
+ private?: unknown;
1739
+ sizes?: unknown;
1740
+ }, options?: {
1741
+ expiresIn?: number | string;
1742
+ size?: string;
1743
+ }): string;
1744
+ /** Whether a signed link to a private file is genuine and not expired. */
1745
+ verifyMediaSignature(key: string, expires: string | null, signature: string | null): boolean;
1746
+ private mediaSignature;
1747
+ /**
1748
+ * How many documents use any of these files in a top-level upload field (rich text and fields
1749
+ * inside groups, arrays and blocks are not counted). Drafts count too.
1750
+ */
1751
+ mediaUsage(ids: readonly ID[]): Promise<number>;
1752
+ /** The storage for public files, or for private ones. */
1753
+ storageFor(isPrivate: boolean): StorageAdapter;
1754
+ /**
1755
+ * Copies a file and its resized copies to the other storage under their new names (`.private`
1756
+ * added or removed). Returns the fields to save, and `cleanUp` to delete the old copies once
1757
+ * the document points to the new ones.
1758
+ */
1759
+ private relocate;
1760
+ /** A folder's name, parent and permissions; private only with somewhere private to keep files. */
1761
+ private validateFolder;
1762
+ /**
1763
+ * Refuses a change that would move more than 200 files between storages in one request: they
1764
+ * are copied one by one, which could outlast a serverless function.
1765
+ */
1766
+ private checkMoveLimit;
1767
+ /** After a folder became private or public (or moved): its files follow, in every subfolder. */
1768
+ private settlePrivacy;
1769
+ /** Files at the top level are public. */
1770
+ private settleTopLevel;
1771
+ private settleFiles;
1772
+ update<S extends Slug<C>>(collection: S, id: ID, data: Update<C, S>, options: UpdateOptions & {
1773
+ where: Where;
1774
+ }): Promise<Doc<C, S> | null>;
1775
+ update<S extends Slug<C>>(collection: S, id: ID, data: Update<C, S>, options?: UpdateOptions & {
1776
+ where?: undefined;
1777
+ }): Promise<Doc<C, S>>;
1778
+ /**
1779
+ * Adds `by` (negative to take away) to a top-level number field in one database statement, so
1780
+ * concurrent calls never lose a change, e.g. stock or a counter. Skips hooks, validation, access
1781
+ * and versions. Returns the new value, or `null` when `min` / `max` would be passed; throws
1782
+ * `NotFoundError` when there is no such document.
1783
+ */
1784
+ increment<S extends Slug<C>>(collection: S, id: ID, field: string, by: number, options?: IncrementOptions): Promise<number | null>;
1785
+ /**
1786
+ * Takes a document off the site: its status becomes `draft`. With versions and drafts, a plain
1787
+ * `update` with `status: 'draft'` only saves a draft and leaves the published document live;
1788
+ * this is how to unpublish it.
1789
+ */
1790
+ unpublish<S extends Slug<C>>(collection: S, id: ID, options?: DepthOptions): Promise<Doc<C, S>>;
1791
+ /** Throws away the unpublished draft of a published document, going back to what is live. */
1792
+ discardDraft<S extends Slug<C>>(collection: S, id: ID, options?: DepthOptions): Promise<Doc<C, S>>;
1793
+ /** Saved versions of a document, newest first. Needs update access to the document. */
1794
+ findVersions<S extends Slug<C>>(collection: S, id: ID, options?: AccessOptions & {
1795
+ limit?: number;
1796
+ page?: number;
1797
+ }): Promise<PaginatedDocs<VersionSummary>>;
1798
+ /** One saved version with the document as it was then. */
1799
+ findVersion<S extends Slug<C>>(collection: S, id: ID, versionId: ID, options?: DepthOptions): Promise<Version<Doc<C, S>> | null>;
1800
+ /**
1801
+ * Makes an old version the current content, as a new version. With drafts it is restored as
1802
+ * a draft (publish it to put it live); access, hooks and validation run as for `update`.
1803
+ */
1804
+ restoreVersion<S extends Slug<C>>(collection: S, id: ID, versionId: ID, options?: DepthOptions): Promise<Doc<C, S>>;
1805
+ /**
1806
+ * Live preview: the document as it would be read if `data` were saved on top of its current
1807
+ * state (or as a new document when `id` is `null`), with relationships populated and
1808
+ * `afterRead` hooks applied. Nothing is written. Also returns the page URL from `preview`.
1809
+ */
1810
+ preview<S extends Slug<C>>(collection: S, id: ID | null, data: Record<string, unknown>, options?: DepthOptions): Promise<LivePreview<Doc<C, S>>>;
1811
+ /**
1812
+ * A token that lets a page read one document's current draft for a while (default one hour),
1813
+ * without a login: `GET /api/cms/:collection/:id?preview=<token>`. The admin adds one to the
1814
+ * preview URL as `easy-cms-preview`, for sites on another origin.
1815
+ */
1816
+ createPreviewToken(target: {
1817
+ collection: string;
1818
+ id: ID;
1819
+ } | {
1820
+ global: string;
1821
+ }, options?: {
1822
+ expiresIn?: number;
1823
+ }): string;
1824
+ /** What a preview token opens, or `null` when it is invalid or expired. */
1825
+ verifyPreviewToken(token: string | null | undefined): (PreviewTarget & {
1826
+ expiresAt: number;
1827
+ }) | null;
1828
+ /** Live preview of a global. See `preview`. */
1829
+ previewGlobal<S extends GSlug<C>>(slug: S, data: Record<string, unknown>, options?: DepthOptions): Promise<LivePreview<GDoc<C, S>>>;
1830
+ /** Coerces preview input like a save would, keeping values that would fail validation. */
1831
+ private previewDoc;
1832
+ private previewURL;
1833
+ private versionTarget;
1834
+ private updateDocument;
1835
+ /**
1836
+ * Deletes a document. Deleting a user with roles from the admin (`auth.rbac`): `transferTo`
1837
+ * gives the documents they own to another user; without it they have no owner.
1838
+ */
1839
+ delete<S extends Slug<C>>(collection: S, id: ID, options?: AccessOptions & {
1840
+ transferTo?: ID | null;
1841
+ }): Promise<Doc<C, S>>;
1842
+ findGlobal<S extends GSlug<C>>(slug: S, options?: ReadOptions): Promise<GDoc<C, S>>;
1843
+ /** A global as stored under `key` (see `globalKey`); empty when there is no key. */
1844
+ private readGlobal;
1845
+ /**
1846
+ * Where a global is kept: its slug, or `<slug>@<scope>` for globals with `scope` (e.g. one per
1847
+ * tenant). `null` when the call has no scope: reads give the global empty. `scope` is given by
1848
+ * scheduled jobs, which have no request.
1849
+ */
1850
+ private globalKey;
1851
+ /** `globalKey` for changes, which need a scope. */
1852
+ private writeKey;
1853
+ updateGlobal<S extends GSlug<C>>(slug: S, data: GInput<C, S>, options?: DepthOptions): Promise<GDoc<C, S>>;
1854
+ /** Takes a global off the site (`status: 'draft'`). See `unpublish`. */
1855
+ unpublishGlobal<S extends GSlug<C>>(slug: S, options?: DepthOptions): Promise<GDoc<C, S>>;
1856
+ /** Throws away the unpublished draft of a published global. */
1857
+ discardGlobalDraft<S extends GSlug<C>>(slug: S, options?: DepthOptions): Promise<GDoc<C, S>>;
1858
+ /** Saved versions of a global, newest first. Needs update access. */
1859
+ findGlobalVersions<S extends GSlug<C>>(slug: S, options?: AccessOptions & {
1860
+ limit?: number;
1861
+ page?: number;
1862
+ }): Promise<PaginatedDocs<VersionSummary>>;
1863
+ findGlobalVersion<S extends GSlug<C>>(slug: S, versionId: ID, options?: DepthOptions): Promise<Version<GDoc<C, S>> | null>;
1864
+ /** Makes an old version of a global current (as a draft when the global has drafts). */
1865
+ restoreGlobalVersion<S extends GSlug<C>>(slug: S, versionId: ID, options?: DepthOptions): Promise<GDoc<C, S>>;
1866
+ private globalVersionTarget;
1867
+ /** The global's pending draft when it has one (see `withDrafts`), otherwise `saved`. */
1868
+ private globalDraft;
1869
+ private saveGlobal;
1870
+ /**
1871
+ * Users changing their own password or email say their current password first, so a session
1872
+ * someone else got hold of can't lock them out. Admins changing other users don't.
1873
+ */
1874
+ private checkOwnCredentials;
1875
+ /**
1876
+ * What `user` may do with one document. Resolves `where`-style access
1877
+ * against the document, so the admin UI can hide actions precisely.
1878
+ */
1879
+ documentPermissions(collection: Slug<C>, id: ID, user: AuthUser | null, context?: RequestContext): Promise<{
1880
+ update: boolean;
1881
+ delete: boolean;
1882
+ }>;
1883
+ /** Closes the database connection. */
1884
+ /**
1885
+ * Waits until webhook deliveries in progress are done. Call it before a serverless function
1886
+ * returns, which may otherwise stop them.
1887
+ */
1888
+ flushWebhooks(): Promise<void>;
1889
+ /**
1890
+ * Sends an email with the config's `email` adapter, e.g. from a hook or plugin. Resolves once
1891
+ * the email is queued; it is sent in the background and retried from the queue if sending
1892
+ * fails. Without `email` in the config, the email is skipped with a warning.
1893
+ */
1894
+ sendEmail(message: EmailMessage): Promise<void>;
1895
+ /** Waits for emails being sent, e.g. before a serverless function returns. */
1896
+ flushEmails(): Promise<void>;
1897
+ /** Emails queued until sent, stored in `email-deliveries`. */
1898
+ private emailQueue;
1899
+ destroy(): Promise<void>;
1900
+ private schedulerTimer;
1901
+ private running;
1902
+ /** Runs `runJobs` every `interval` ms in this process (`createEasyCMS` does this by default). */
1903
+ startScheduler(interval?: number): void;
1904
+ /**
1905
+ * Runs due scheduled publishes and unpublishes, and retries failed webhook deliveries. What the
1906
+ * scheduler and the cron endpoint (`GET <api>/jobs/run`) call.
1907
+ */
1908
+ runJobs(now?: Date): Promise<{
1909
+ /** Scheduled publishes and unpublishes. */
1910
+ scheduled: {
1911
+ ran: number;
1912
+ failed: number;
1913
+ };
1914
+ webhooks: {
1915
+ sent: number;
1916
+ failed: number;
1917
+ };
1918
+ emails: {
1919
+ sent: number;
1920
+ failed: number;
1921
+ };
1922
+ jobs: {
1923
+ ran: number;
1924
+ failed: number;
1925
+ };
1926
+ }>;
1927
+ /** `jobs` of the config that are due; when each last ran is kept in the database. */
1928
+ private runConfigJobs;
1929
+ /**
1930
+ * Sends an event of the app or a plugin (`events` in the config, e.g. `order.paid`) to the
1931
+ * webhooks that list it. Returns at once; deliveries are queued and retried like content changes.
1932
+ */
1933
+ emit(event: string, data: Record<string, unknown>, about?: {
1934
+ collection: string;
1935
+ id: ID;
1936
+ }): void;
1937
+ /**
1938
+ * Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
1939
+ * database until they succeed, so they survive restarts.
1940
+ */
1941
+ retryWebhooks(now?: Date): Promise<{
1942
+ sent: number;
1943
+ failed: number;
1944
+ }>;
1945
+ /** Webhook deliveries until they succeed, stored in `webhook-deliveries`. */
1946
+ private webhookQueue;
1947
+ /**
1948
+ * Publishes or unpublishes documents whose scheduled time has come. Safe to call often, e.g.
1949
+ * from a cron job (`GET <api>/jobs/run`) where no process keeps running.
1950
+ */
1951
+ runScheduled(now?: Date): Promise<{
1952
+ ran: number;
1953
+ failed: number;
1954
+ }>;
1955
+ /** Schedules a publish or unpublish, replacing a pending job with the same action. */
1956
+ schedule<S extends Slug<C>>(collection: S, id: ID, job: {
1957
+ action: 'publish' | 'unpublish';
1958
+ at: Date | string;
1959
+ }, options?: AccessOptions): Promise<ScheduledPublish>;
1960
+ /** Pending jobs of a document, soonest first. */
1961
+ findSchedule<S extends Slug<C>>(collection: S, id: ID, options?: AccessOptions): Promise<ScheduledPublish[]>;
1962
+ cancelSchedule<S extends Slug<C>>(collection: S, id: ID, jobId: ID, options?: AccessOptions): Promise<void>;
1963
+ scheduleGlobal<S extends GSlug<C>>(slug: S, job: {
1964
+ action: 'publish' | 'unpublish';
1965
+ at: Date | string;
1966
+ }, options?: AccessOptions): Promise<ScheduledPublish>;
1967
+ findGlobalSchedule<S extends GSlug<C>>(slug: S, options?: AccessOptions): Promise<ScheduledPublish[]>;
1968
+ cancelGlobalSchedule<S extends GSlug<C>>(slug: S, jobId: ID, options?: AccessOptions): Promise<void>;
1969
+ private scheduleTarget;
1970
+ private globalScheduleTarget;
1971
+ private addJob;
1972
+ /**
1973
+ * The next pending jobs across all collections and globals, soonest first; with access
1974
+ * enforced, only those the user may update.
1975
+ */
1976
+ upcomingSchedules(options?: AccessOptions & {
1977
+ limit?: number;
1978
+ }): Promise<(ScheduledPublish & {
1979
+ collection?: string;
1980
+ global?: string;
1981
+ doc: ID;
1982
+ })[]>;
1983
+ private pendingJobs;
1984
+ private removeJob;
1985
+ /** Removes webhook deliveries and emails that failed more than 30 days ago. */
1986
+ private pruneFailedDeliveries;
1987
+ private global;
1988
+ /**
1989
+ * Refuses what a request's API key (see `ApiKeyPermissions`) or the user's role (`auth.rbac`)
1990
+ * does not allow. Returns a constraint when the role allows only the user's own account.
1991
+ */
1992
+ private checkGrant;
1993
+ /** The query constraint read access adds, or `undefined` when there is none. Throws when denied. */
1994
+ private readWhere;
1995
+ private checkDocumentAccess;
1996
+ private checkGlobalAccess;
1997
+ private fieldChecker;
1998
+ /**
1999
+ * Refuses filtering or sorting by fields the caller may not read: the results would tell
2000
+ * their values. Checks each field along a dotted path, through groups, arrays and blocks.
2001
+ */
2002
+ private checkQueryFields;
2003
+ /** `checkGrant` for one document: a role limited to its own documents must own it. */
2004
+ private checkDocumentGrant;
2005
+ /** Runs afterRead hooks, populates relationships and removes what the caller may not see. */
2006
+ private output;
2007
+ /** Creates a document: access, input filtering, hooks, validation, then afterChange. */
2008
+ private createDocument;
2009
+ /** The locale for an operation; `undefined` without localization. Throws on unknown locales. */
2010
+ private localeOf;
2011
+ /** Stored localized maps → the values of the requested locale. */
2012
+ private pick;
2013
+ /** Input for one locale → stored localized maps, keeping the other locales from `current`. */
2014
+ private toMaps;
2015
+ /** Makes `where` match localized fields in the operation's locale. */
2016
+ private whereFor;
2017
+ /** Drafts are kept as versions (instead of unpublishing) when both are enabled. */
2018
+ private separateDrafts;
2019
+ /** Replaces published documents that have a newer draft with that draft. */
2020
+ private withDrafts;
2021
+ /** Sends webhook events for a change; `before` is the stored status before it, if any. */
2022
+ private emitChange;
2023
+ /**
2024
+ * Writes the audit log entry for a change (`audit`): publishing, unpublishing and restoring
2025
+ * are told apart from other saves.
2026
+ */
2027
+ private audited;
2028
+ /** Records a version after a save, when the collection or global keeps versions. */
2029
+ private saveVersion;
2030
+ private hookArgs;
2031
+ /**
2032
+ * Runs hooks that may replace a value, passed as `data` (before hooks) or `doc` (afterRead).
2033
+ * Returning `undefined` keeps the value; a throw cancels the operation.
2034
+ */
2035
+ private transform;
2036
+ /** Runs hooks after the change is saved: failures are logged, never undo the save (NFR-REL-03). */
2037
+ private notify;
2038
+ /** Writes resized copies with sharp, when installed and configured. */
2039
+ private resizeImage;
2040
+ /** Validates, checks uniqueness and references. Returns clean data or throws `ValidationError`. */
2041
+ private prepare;
2042
+ /** Refuses changes that would leave no active admin (and lock everyone out). */
2043
+ private guardLastAdmin;
2044
+ private makeSlugsUnique;
2045
+ private checkUnique;
2046
+ private isTaken;
2047
+ /**
2048
+ * What folder keys are unique within for a call (`uniqueWithin` of the `key` field of media
2049
+ * folders, e.g. the tenant): the value a folder made by this call would get from its hooks.
2050
+ */
2051
+ private folderScope;
2052
+ /**
2053
+ * Where a unique field's values must differ for a call (`uniqueWithin`, e.g. the tenant): the
2054
+ * value a document created by this call would get there from its hooks, as `{ [field]: value }`;
2055
+ * `{}` when the field is unique everywhere. For finding documents by such a field, e.g. a page
2056
+ * by its path in the current tenant.
2057
+ */
2058
+ uniqueScope(collection: string, field: string, options?: Pick<AccessOptions, 'user' | 'context'>): Promise<Record<string, unknown>>;
2059
+ /** Each reference is one its relationship's `filterOptions` allow. */
2060
+ private checkFilterOptions;
2061
+ private checkReferences;
2062
+ }
2063
+ /** A document as live preview shows it, and the page to show it on. */
2064
+ interface LivePreview<T> {
2065
+ readonly doc: T;
2066
+ /** From the collection's `preview` function; `null` without one. */
2067
+ readonly url: string | null;
2068
+ }
2069
+
2070
+ /** Audit log settings (`audit`). */
2071
+ interface AuditConfig {
2072
+ /** Days to keep entries; older ones are deleted. `0`: keep them all. Default 365. */
2073
+ readonly keepDays?: number;
2074
+ /** Keep the values before and after a change, not only which fields changed. Default `true`. */
2075
+ readonly values?: boolean;
2076
+ /** Failed logins within an hour that the dashboard warns about. Default 20. */
2077
+ readonly failedLogins?: number;
2078
+ /**
2079
+ * The part of the site an entry belongs to, from the request's context, e.g. its tenant
2080
+ * (usually set by a plugin). Users whose role holds in a part only (`scoped`) see that part's
2081
+ * entries; others see the chosen part's, or all.
2082
+ */
2083
+ readonly scope?: ((args: {
2084
+ readonly context: RequestContext;
2085
+ readonly user: AuthUser | null;
2086
+ }) => string | null | undefined) | false;
2087
+ }
2088
+ /** A field that changed: values before and after, unless they are not kept. */
2089
+ interface AuditChange {
2090
+ field: string;
2091
+ before?: unknown;
2092
+ after?: unknown;
2093
+ }
2094
+ /** One entry (`GET <api>/admin/audit`). */
2095
+ interface AuditEntry {
2096
+ id: ID;
2097
+ /** When, as an ISO date. */
2098
+ at: string;
2099
+ /** E.g. `create`, `update`, `publish`, `delete`, `login`, `login.failed`, `role.update`. */
2100
+ action: string;
2101
+ /** A collection slug, `global:<slug>`, or an area such as `auth`, `roles`, `backups`. */
2102
+ target: string | null;
2103
+ /** The document's id. */
2104
+ doc: string | null;
2105
+ /** The document's title then (kept after it is deleted). */
2106
+ title: string | null;
2107
+ actor: {
2108
+ id: string | null;
2109
+ email: string | null;
2110
+ /** `user` (admin or REST), `api-key`, `system` (code), `scheduler`. */
2111
+ via: string;
2112
+ ip: string | null;
2113
+ userAgent: string | null;
2114
+ };
2115
+ changes: AuditChange[] | null;
2116
+ detail: Record<string, unknown> | null;
2117
+ }
2118
+ /** `GET <api>/admin/audit`: a page of entries, newest first. */
2119
+ /** A page of the audit log: the list shape of the rest of the API. */
2120
+ type AuditPage = PaginatedDocs<AuditEntry>;
2121
+ interface AuditFilter {
2122
+ action?: string | null;
2123
+ target?: string | null;
2124
+ doc?: string | null;
2125
+ /** An email, or part of one. */
2126
+ actor?: string | null;
2127
+ /** ISO dates. */
2128
+ from?: string | null;
2129
+ to?: string | null;
2130
+ /** Only entries of this part of the site (`scope`). */
2131
+ scope?: string | null;
2132
+ }
2133
+ /** `POST <api>/admin/audit/verify`. */
2134
+ interface AuditVerification {
2135
+ checked: number;
2136
+ /** Entries whose signature does not match: changed after they were written. */
2137
+ invalid: ID[];
2138
+ /** Missing ids between entries: deleted entries, or (rarely) inserts that failed. */
2139
+ gaps: number;
2140
+ at: string;
2141
+ }
2142
+ /**
2143
+ * The audit log (`audit` in the config): who did what, when, from where. Entries can't be
2144
+ * changed through Easy CMS; each is signed with the secret, so changes in the database show.
2145
+ * Available as `cms.audit`.
2146
+ */
2147
+ declare class AuditLog {
2148
+ private readonly cms;
2149
+ /** Entries that could not be written since the server started. */
2150
+ failures: number;
2151
+ constructor(cms: EasyCMS);
2152
+ get enabled(): boolean;
2153
+ private get settings();
2154
+ /**
2155
+ * Writes an entry. The user defaults to the request's. Never throws: a failure is logged and
2156
+ * counted, and the dashboard says so.
2157
+ */
2158
+ record(entry: {
2159
+ action: string;
2160
+ target?: string | null;
2161
+ doc?: ID | null;
2162
+ title?: string | null;
2163
+ user?: AuthUser | null;
2164
+ /** Who it was when there is no user, e.g. the email of a failed login. */
2165
+ email?: string | null;
2166
+ changes?: AuditChange[] | null;
2167
+ detail?: Record<string, unknown> | null;
2168
+ /** The call's context, when it is not the request's. */
2169
+ context?: RequestContext;
2170
+ }): Promise<void>;
2171
+ /** Entries, newest first: `page` (from 1) of `limit` (default 50, at most 100). */
2172
+ list(options?: AuditFilter & {
2173
+ page?: number;
2174
+ limit?: number;
2175
+ }): Promise<AuditPage>;
2176
+ /** The filtered entries as CSV (the newest 10,000). */
2177
+ csv(filter: AuditFilter): Promise<string>;
2178
+ /** Checks every entry's signature, and that no ids are missing. */
2179
+ verify(): Promise<AuditVerification>;
2180
+ /** What the dashboard warns about: many failed logins, entries not written, a failed check. */
2181
+ attention(now?: Date): Promise<({
2182
+ id: 'failed-logins';
2183
+ count: number;
2184
+ } | {
2185
+ id: 'audit-failures';
2186
+ count: number;
2187
+ } | {
2188
+ id: 'audit-tampered';
2189
+ invalid: number;
2190
+ at: string;
2191
+ })[]>;
2192
+ private sign;
2193
+ private lastVerification;
2194
+ private where;
2195
+ }
2196
+
2197
+ /** What the email functions in `auth.emails` receive. */
2198
+ interface PasswordEmailArgs {
2199
+ readonly user: AuthUser;
2200
+ /** The page that sets the password; absent for `passwordChanged`. */
2201
+ readonly url?: string;
2202
+ /** The admin language for the text: `en` or `th`. */
2203
+ readonly locale: string;
2204
+ /** When the link stops working. */
2205
+ readonly expiresAt?: Date;
2206
+ /**
2207
+ * Invitations when passwords are off (`auth.password: false`): the providers to sign in with,
2208
+ * e.g. `['Google']`; `url` is then the login page.
2209
+ */
2210
+ readonly sso?: readonly string[];
2211
+ }
2212
+ /** An email's content: a subject, and text and/or HTML. */
2213
+ interface PasswordEmail {
2214
+ readonly subject: string;
2215
+ readonly text: string;
2216
+ readonly html?: string;
2217
+ }
2218
+ type PasswordEmailFn = (args: PasswordEmailArgs) => PasswordEmail | Promise<PasswordEmail>;
2219
+ /** The default emails, in English and Thai. */
2220
+ declare const DEFAULT_PASSWORD_EMAILS: {
2221
+ resetPassword: PasswordEmailFn;
2222
+ invite: PasswordEmailFn;
2223
+ passwordChanged: PasswordEmailFn;
2224
+ verifyEmail: PasswordEmailFn;
2225
+ };
2226
+
2227
+ /**
2228
+ * Signing in to the admin with an outside account (`auth.providers`): Google, Microsoft, GitHub
2229
+ * or any OpenID Connect provider. `@easy-cms/auth-oauth` makes these; Easy CMS runs the rest
2230
+ * (the redirect, matching users, sessions).
2231
+ */
2232
+ /** Values bound to one sign-in, checked when the browser comes back. */
2233
+ interface AuthProviderRequest {
2234
+ /** Where the provider sends the browser back: `<serverURL><api>/auth/<id>/callback`. */
2235
+ readonly redirectUri: string;
2236
+ readonly state: string;
2237
+ /** For PKCE: the provider gets its S256 challenge. */
2238
+ readonly codeVerifier: string;
2239
+ /** For OpenID Connect: must come back in the ID token. */
2240
+ readonly nonce: string;
2241
+ }
2242
+ /** Who signed in, as the provider says. */
2243
+ interface AuthProviderProfile {
2244
+ /** The provider's stable id for the account (`sub`). */
2245
+ readonly subject: string;
2246
+ readonly email: string | null;
2247
+ /** The provider vouches for the email. Only verified emails are matched to users. */
2248
+ readonly emailVerified: boolean;
2249
+ readonly name?: string | undefined;
2250
+ }
2251
+ interface AuthProvider {
2252
+ /** In URLs and stored identities, e.g. `google`: lowercase letters, digits and `-`. */
2253
+ readonly id: string;
2254
+ /**
2255
+ * The adapter interface it is written for (`ADAPTER_API_VERSION`); Easy CMS refuses another.
2256
+ * Leave it out to skip the check.
2257
+ */
2258
+ readonly apiVersion?: number;
2259
+ /** On the sign-in button, e.g. `Google`. */
2260
+ readonly name: string;
2261
+ /** The button's icon in the admin. Default `key`. */
2262
+ readonly icon?: 'google' | 'microsoft' | 'github' | 'key';
2263
+ /** The provider's page to send the browser to. */
2264
+ authorizationURL(request: AuthProviderRequest): Promise<URL>;
2265
+ /**
2266
+ * After the browser comes back to `redirectUri` (`url`, with its query): checks the response
2267
+ * and exchanges the code. Throws when anything does not check out.
2268
+ */
2269
+ callback(request: AuthProviderRequest & {
2270
+ readonly url: URL;
2271
+ }): Promise<AuthProviderProfile>;
2272
+ /** For Settings → SSO: what it connects to, never secrets. */
2273
+ describe?(): Readonly<Record<string, string>>;
2274
+ /**
2275
+ * Signs staff who have an account in by their verified email the first time, without linking
2276
+ * the provider from their account page. Only for a provider whose emails your organization
2277
+ * controls (e.g. its Google Workspace). Default `false`; site members are always matched.
2278
+ */
2279
+ readonly linkByEmail?: boolean;
2280
+ }
2281
+
2282
+ interface ConfigIssue {
2283
+ /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
2284
+ readonly path: string;
2285
+ readonly message: string;
2286
+ /** How to fix it. */
2287
+ readonly hint?: string;
2288
+ /** `warning`: reported, but the config is used (e.g. an option Easy CMS doesn't know). */
2289
+ readonly severity?: 'warning';
2290
+ }
2291
+ /**
2292
+ * What went wrong, for programs: in REST errors (`errors[].code`), GraphQL errors
2293
+ * (`extensions.code`) and on every `EasyCMSError` (`code`). New codes may be added; these keep
2294
+ * their meaning.
2295
+ */
2296
+ type ErrorCode = 'VALIDATION_ERROR' | 'BAD_USER_INPUT' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'METHOD_NOT_ALLOWED' | 'PAYLOAD_TOO_LARGE' | 'UNSUPPORTED_MEDIA_TYPE' | 'TOO_MANY_REQUESTS' | 'CONFIG_ERROR' | 'INTERNAL_SERVER_ERROR';
2297
+ /** The code of an HTTP status, for errors that only have a status. */
2298
+ declare function codeOfStatus(status: number): ErrorCode;
2299
+ /** Base class for Easy CMS's errors: an HTTP status for the REST API, and a code for programs. */
2300
+ declare class EasyCMSError extends Error {
2301
+ readonly status: number;
2302
+ readonly code: ErrorCode;
2303
+ constructor(message: string, status: number, code?: ErrorCode);
2304
+ }
2305
+ declare class ConfigError extends EasyCMSError {
2306
+ readonly issues: readonly ConfigIssue[];
2307
+ constructor(issues: readonly ConfigIssue[]);
2308
+ static format(issues: readonly ConfigIssue[]): string;
2309
+ }
2310
+ interface FieldError {
2311
+ /** Dotted path, e.g. `links.0.url`. */
2312
+ readonly field: string;
2313
+ readonly message: string;
2314
+ }
2315
+ declare class ValidationError extends EasyCMSError {
2316
+ readonly errors: readonly FieldError[];
2317
+ constructor(collection: string, errors: readonly FieldError[]);
2318
+ }
2319
+ declare class NotFoundError extends EasyCMSError {
2320
+ constructor(collection: string, id: unknown);
2321
+ }
2322
+ /** A bad `where`, `sort` or unknown collection/global. */
2323
+ declare class QueryError extends EasyCMSError {
2324
+ constructor(message: string);
2325
+ }
2326
+ /** The database schema does not match the config (pending or missing migrations). */
2327
+ declare class SchemaError extends EasyCMSError {
2328
+ constructor(message: string);
2329
+ }
2330
+ /** Not logged in, or the session is invalid. */
2331
+ declare class UnauthorizedError extends EasyCMSError {
2332
+ constructor(message?: string);
2333
+ }
2334
+ /** Logged in, but not allowed to do this. */
2335
+ declare class ForbiddenError extends EasyCMSError {
2336
+ constructor(message?: string);
2337
+ }
2338
+ declare class TooManyRequestsError extends EasyCMSError {
2339
+ /** When to try again, in seconds (the REST API sends it as `Retry-After`). */
2340
+ readonly retryAfter: number | undefined;
2341
+ constructor(message: string, retryAfter?: number);
2342
+ }
2343
+ declare class PayloadTooLargeError extends EasyCMSError {
2344
+ constructor(message: string);
2345
+ }
2346
+
2347
+ /** Built-in types a field type can be stored as. */
2348
+ declare const FIELD_TYPE_BASES: readonly ["text", "textarea", "email", "number", "boolean", "date", "json"];
2349
+ type FieldTypeBase = (typeof FIELD_TYPE_BASES)[number];
2350
+ /** What a field type's `validate` gets besides the value: the field, with its own options. */
2351
+ interface FieldTypeValidateContext extends FieldValidateContext {
2352
+ readonly field: Readonly<Record<string, unknown>> & {
2353
+ readonly name: string;
2354
+ };
2355
+ }
2356
+ /**
2357
+ * A field type a package adds, e.g. `color`: stored, queried and translated like its `base`
2358
+ * type, with its own checks and admin components. List it in the config's `fieldTypes`.
2359
+ */
2360
+ interface FieldTypeDefinition {
2361
+ /** The `type` fields use, e.g. `color`. Lowercase; not one of the built-in types. */
2362
+ readonly name: string;
2363
+ /** How values are stored and queried: one of the built-in scalar types. */
2364
+ readonly base: FieldTypeBase;
2365
+ /** Checks a value (not empty ones; `required` covers those). Return `true` or a message. */
2366
+ readonly validate?: (value: unknown, ctx: FieldTypeValidateContext) => true | string | Promise<true | string>;
2367
+ /** Checks a field's own options when the config loads; return a message for a mistake. */
2368
+ readonly checkOptions?: (field: Readonly<Record<string, unknown>>) => string | undefined;
2369
+ readonly admin?: {
2370
+ /** The input: a Web Component from `module`, with fixed `props` of its own if it needs them. */
2371
+ readonly component?: AdminComponent;
2372
+ /** Shown in the admin's lists. */
2373
+ readonly cell?: AdminComponent;
2374
+ /** The admin module (package export or file) that defines the components. */
2375
+ readonly module?: string;
2376
+ /** The field options its components get (as their `options`), e.g. `['presets']`. */
2377
+ readonly props?: readonly string[];
2378
+ };
2379
+ /** The value's TypeScript type for `generate:types`. Default: the base type's. */
2380
+ readonly typescript?: string;
2381
+ }
2382
+ /** Declares a field type; returns it unchanged. */
2383
+ declare function defineFieldType(definition: FieldTypeDefinition): FieldTypeDefinition;
2384
+
2385
+ declare const WEBHOOK_EVENTS: readonly ["create", "update", "delete", "publish", "unpublish", "draft"];
2386
+ /**
2387
+ * `create`, `update`, `delete`: the stored document changed. `publish` / `unpublish`: its status
2388
+ * changed (sent with `create` or `update`). `draft`: only a draft was saved; what is live did not
2389
+ * change (collections with versions and drafts).
2390
+ */
2391
+ type WebhookEvent = (typeof WEBHOOK_EVENTS)[number];
2392
+ interface WebhookConfig {
2393
+ /** Where to POST events. */
2394
+ readonly url: string;
2395
+ /**
2396
+ * Events to send. Default: every content change. Events of the app or its plugins (`events`
2397
+ * in the config, e.g. `order.paid`) are sent only when listed here.
2398
+ */
2399
+ readonly events?: readonly (WebhookEvent | (string & {}))[];
2400
+ /** Collections to send events for. Default: all but `users` (list it to get accounts); `[]` for none. */
2401
+ readonly collections?: readonly string[];
2402
+ /** Globals to send events for. Default: all; `[]` for none. */
2403
+ readonly globals?: readonly string[];
2404
+ /** Signs each body: `x-easy-cms-signature: sha256=<hex HMAC-SHA256 of the body>`. */
2405
+ readonly secret?: string;
2406
+ /** Extra request headers, e.g. a token the receiver checks. */
2407
+ readonly headers?: Readonly<Record<string, string>>;
2408
+ }
2409
+ /** What a webhook receives as JSON. */
2410
+ interface WebhookPayload {
2411
+ /** A content change (`WebhookEvent`), or an event of the app or a plugin, e.g. `order.paid`. */
2412
+ readonly event: string;
2413
+ readonly collection?: string;
2414
+ readonly global?: string;
2415
+ readonly id?: ID;
2416
+ /**
2417
+ * The stored document (every locale, no hidden fields); for `delete`, as it was. For an event of
2418
+ * the app or a plugin: what `cms.emit()` was given.
2419
+ */
2420
+ readonly doc: Record<string, unknown>;
2421
+ /** ISO time of the change. */
2422
+ readonly timestamp: string;
2423
+ }
2424
+
2425
+ type Data = Record<string, unknown>;
2426
+ type MaybePromise<T> = T | Promise<T>;
2427
+ type Operation = 'create' | 'update';
2428
+ interface HookBase {
2429
+ /** The user the operation runs for; `null` for anonymous or trusted Local API calls. */
2430
+ readonly user: AuthUser | null;
2431
+ /** The Local API, e.g. to query other collections or trigger a revalidation. */
2432
+ readonly cms: EasyCMS;
2433
+ /** Slug of the collection or global. */
2434
+ readonly slug: string;
2435
+ /** The request's context (`onRequest`, or `context` of a Local API call); `{}` when there is none. */
2436
+ readonly context: RequestContext;
2437
+ }
2438
+ type BeforeValidateHook = (args: HookBase & {
2439
+ data: Data;
2440
+ operation: Operation;
2441
+ previousDoc?: Data;
2442
+ }) => MaybePromise<Data | undefined>;
2443
+ type BeforeChangeHook = (args: HookBase & {
2444
+ data: Data;
2445
+ operation: Operation;
2446
+ previousDoc?: Data;
2447
+ }) => MaybePromise<Data | undefined>;
2448
+ type AfterChangeHook = (args: HookBase & {
2449
+ doc: Data;
2450
+ operation: Operation;
2451
+ previousDoc?: Data;
2452
+ }) => MaybePromise<void>;
2453
+ type BeforeDeleteHook = (args: HookBase & {
2454
+ id: ID;
2455
+ }) => MaybePromise<void>;
2456
+ type AfterDeleteHook = (args: HookBase & {
2457
+ id: ID;
2458
+ doc: Data;
2459
+ }) => MaybePromise<void>;
2460
+ type AfterReadHook = (args: HookBase & {
2461
+ doc: Data;
2462
+ }) => MaybePromise<Data | undefined>;
2463
+ interface CollectionHooks {
2464
+ readonly beforeValidate?: readonly BeforeValidateHook[];
2465
+ readonly beforeChange?: readonly BeforeChangeHook[];
2466
+ readonly afterChange?: readonly AfterChangeHook[];
2467
+ readonly beforeDelete?: readonly BeforeDeleteHook[];
2468
+ readonly afterDelete?: readonly AfterDeleteHook[];
2469
+ readonly afterRead?: readonly AfterReadHook[];
2470
+ }
2471
+ interface GlobalHooks {
2472
+ readonly beforeValidate?: readonly BeforeValidateHook[];
2473
+ readonly beforeChange?: readonly BeforeChangeHook[];
2474
+ readonly afterChange?: readonly AfterChangeHook[];
2475
+ readonly afterRead?: readonly AfterReadHook[];
2476
+ }
2477
+ interface LocalizationConfig {
2478
+ /** Content locales, e.g. `['th', 'en']`. */
2479
+ readonly locales: readonly string[];
2480
+ /** The locale used when none is given; its values also fill empty ones. Default: the first. */
2481
+ readonly defaultLocale?: string;
2482
+ /** Reads return the default locale's value when a locale's value is empty. Default true. */
2483
+ readonly fallback?: boolean;
2484
+ }
2485
+ /**
2486
+ * Where a document is shown on the site, for live preview in the admin: an absolute URL or a
2487
+ * path on the site's origin. Return `null` when the document has no page.
2488
+ */
2489
+ type PreviewURL = (args: {
2490
+ readonly doc: Record<string, unknown>;
2491
+ /** The content locale being edited (with `localization`), otherwise the admin locale. */
2492
+ readonly locale: string;
2493
+ }) => string | null | undefined;
2494
+ interface VersionsConfig {
2495
+ /** Versions kept per document; older ones are deleted. Default 50. */
2496
+ readonly keep?: number;
2497
+ }
2498
+ /** A panel of an edit page's side column: a component, at the top or (default) below the rest. */
2499
+ type SidebarPanel = AdminComponent | {
2500
+ readonly tag: string;
2501
+ readonly props?: Readonly<Record<string, unknown>>;
2502
+ /** `top`: right below publishing, above the side fields. Default: below them. */
2503
+ readonly position?: 'top';
2504
+ };
2505
+ /**
2506
+ * Where the edit page puts its fields: tabs, collapsible sections and rows, by field name. Only
2507
+ * how they look: the data stays as the fields say. Fields not placed follow at the end (in the
2508
+ * first tab). Top-level fields; sidebar fields stay in the side column.
2509
+ *
2510
+ * ```ts
2511
+ * layout: [
2512
+ * { tab: 'Content', fields: ['title', { row: ['author', 'category'] }, 'body'] },
2513
+ * { tab: 'SEO', fields: ['meta'] },
2514
+ * ]
2515
+ * ```
2516
+ */
2517
+ type LayoutNode = string | {
2518
+ readonly row: readonly string[];
2519
+ } | {
2520
+ readonly collapsible: Label;
2521
+ /** Starts folded. */
2522
+ readonly collapsed?: boolean;
2523
+ readonly description?: Label;
2524
+ readonly fields: readonly LayoutNode[];
2525
+ } | {
2526
+ readonly tab: Label;
2527
+ readonly description?: Label;
2528
+ readonly fields: readonly LayoutNode[];
2529
+ };
2530
+ /** Custom admin components for a collection's or global's edit page. */
2531
+ interface ContainerAdmin {
2532
+ /** Icon in the admin menu. Default `file-text` for collections, `settings` for globals. */
2533
+ readonly icon?: AdminIcon;
2534
+ /** Its place in its menu group: lower first. Default: after those with one, in config order. */
2535
+ readonly order?: number;
2536
+ /** Panels in the edit page's side column (components from admin modules). */
2537
+ readonly sidebar?: readonly SidebarPanel[];
2538
+ /** Tabs, collapsible sections and rows of the edit page. See `LayoutNode`. */
2539
+ readonly layout?: readonly LayoutNode[];
2540
+ /**
2541
+ * Its menu group: a group's id (`admin.nav`), e.g. `shop` or `shop.catalog`; `settings` for
2542
+ * Settings; or a label, which makes a group of that name. Default: Content for collections,
2543
+ * Settings › Site for globals. `false`: not in the menu, reached by links.
2544
+ */
2545
+ readonly group?: 'settings' | false | Label;
2546
+ }
2547
+ interface CollectionAdmin extends ContainerAdmin {
2548
+ /**
2549
+ * How the admin opens documents from the list: `drawer` slides a panel over the list, handy
2550
+ * for small collections (categories, tags). Default `page`. Collections with drafts, versions
2551
+ * or live preview always use the page.
2552
+ */
2553
+ readonly editIn?: 'page' | 'drawer';
2554
+ /**
2555
+ * A number that needs attention, next to its menu item: the documents matching `where` (that
2556
+ * the user may read), e.g. orders to send. `tone`: `accent` (default), `warning` or `danger`.
2557
+ */
2558
+ readonly badge?: {
2559
+ readonly where: Where;
2560
+ readonly tone?: 'accent' | 'warning' | 'danger';
2561
+ /** What the number counts, for screen readers, e.g. `{ en: 'to send', th: 'รอจัดส่ง' }`. */
2562
+ readonly label?: Label;
2563
+ };
2564
+ /** The number of documents beside its menu item. Default `true`. */
2565
+ readonly count?: boolean;
2566
+ /** The list with no documents yet: what to say, and a link, e.g. to a guide. */
2567
+ readonly empty?: {
2568
+ readonly description?: Label;
2569
+ readonly link?: {
2570
+ readonly label: Label;
2571
+ readonly href: string;
2572
+ };
2573
+ };
2574
+ readonly list?: CollectionListAdmin;
2575
+ /**
2576
+ * With `auth.rbac`: the relationship field to `users` that names a document's owner, e.g.
2577
+ * `author`, for roles given "own documents only". Default: who created it (`createdBy`).
2578
+ */
2579
+ readonly ownerField?: string;
2580
+ /**
2581
+ * Deleting asks more than "Are you sure?", for documents that take much with them (e.g. a
2582
+ * tenant): `typeTitle` has the user type the document's title; `impact` is a path under the
2583
+ * API, called with `?id=`, that returns `{ message }` (a label) saying what will be lost.
2584
+ * Documents are then deleted one at a time, not from a selection.
2585
+ */
2586
+ readonly confirmDelete?: {
2587
+ readonly typeTitle?: boolean;
2588
+ readonly impact?: string;
2589
+ };
2590
+ }
2591
+ interface CollectionListAdmin {
2592
+ /**
2593
+ * Show the list as a tree: the name of a relationship field to this same collection (e.g.
2594
+ * `parent`). Top-level documents come first; their children open below them.
2595
+ */
2596
+ readonly tree?: string;
2597
+ /** The list's default order, e.g. `title` or `-updatedAt` (the default). */
2598
+ readonly sort?: string;
2599
+ }
2600
+ interface CollectionConfig {
2601
+ /** URL and table name. Lowercase letters, digits, `-` and `_`. */
2602
+ readonly slug: string;
2603
+ readonly labels?: {
2604
+ readonly singular?: Label;
2605
+ readonly plural?: Label;
2606
+ };
2607
+ readonly fields: readonly Field[];
2608
+ /** Top-level field shown as the document title in the admin UI. */
2609
+ readonly useAsTitle?: string;
2610
+ /** Adds a `status` field (`draft` | `published`). */
2611
+ readonly drafts?: boolean;
2612
+ /**
2613
+ * Keep a snapshot of every save, to view history and restore. With `drafts`, a draft saved
2614
+ * on a published document is kept as a version and the published document stays live until
2615
+ * it is published again.
2616
+ */
2617
+ readonly versions?: boolean | VersionsConfig;
2618
+ /** Allow scheduling publish/unpublish for a later time (needs `drafts`). */
2619
+ readonly schedule?: boolean;
2620
+ /** Live preview: the page that shows a document, e.g. `({ doc }) => \`/posts/${doc.slug}\``. */
2621
+ readonly preview?: PreviewURL;
2622
+ readonly access?: CollectionAccess;
2623
+ readonly hooks?: CollectionHooks;
2624
+ /** How the collection looks in the admin: its menu item, list, edit page and components. */
2625
+ readonly admin?: CollectionAdmin;
2626
+ }
2627
+ interface GlobalConfig {
2628
+ readonly slug: string;
2629
+ readonly label?: Label;
2630
+ readonly fields: readonly Field[];
2631
+ readonly drafts?: boolean;
2632
+ /** Keep a snapshot of every save. See `CollectionConfig.versions`. */
2633
+ readonly versions?: boolean | VersionsConfig;
2634
+ /** Allow scheduling publish/unpublish for a later time (needs `drafts`). */
2635
+ readonly schedule?: boolean;
2636
+ /** Live preview: the page that shows the global. See `CollectionConfig.preview`. */
2637
+ readonly preview?: PreviewURL;
2638
+ readonly access?: GlobalAccess;
2639
+ readonly hooks?: GlobalHooks;
2640
+ /**
2641
+ * One value per scope, e.g. per tenant (usually set by a plugin): the scope of a call, from its
2642
+ * context (`onRequest`). A string keeps a value of its own; `undefined` the shared value; `null`
2643
+ * means the call has none, so reads give the global empty and changes are refused.
2644
+ */
2645
+ readonly scope?: (args: {
2646
+ readonly context: RequestContext;
2647
+ readonly user: AuthUser | null;
2648
+ }) => string | null | undefined;
2649
+ /** How the global looks in the admin: its menu item, edit page and components. */
2650
+ readonly admin?: ContainerAdmin;
2651
+ }
2652
+ type AdminLocale = 'en' | 'th';
2653
+ /** Icons a collection or global can show in the admin menu ([Lucide](https://lucide.dev) names). */
2654
+ declare const ADMIN_ICONS: readonly ["file-text", "newspaper", "book-open", "notebook", "folder", "tag", "tags", "image", "images", "video", "music", "file", "users", "user", "building", "store", "shopping-bag", "shopping-cart", "package", "box", "calendar", "calendar-days", "map-pin", "globe", "house", "layout-grid", "layers", "star", "heart", "message-square", "mail", "phone", "briefcase", "graduation-cap", "utensils", "car", "settings", "sliders-horizontal", "palette", "megaphone", "bell", "link", "quote", "circle-help", "award", "ticket", "camera", "key", "chart-column", "chart-line"];
2655
+ type AdminIcon = (typeof ADMIN_ICONS)[number];
2656
+ /** Your or your client's brand in the admin UI. */
2657
+ interface AdminBrand {
2658
+ /** Shown in the menu, on the login page and in the browser tab. Default "Easy CMS". */
2659
+ readonly name?: string;
2660
+ /** Logo URL: a path on your site (`/logo.svg`) or an `https://` address. */
2661
+ readonly logo?: string;
2662
+ /** Main color as `#rrggbb`; lighter and darker shades are derived from it. */
2663
+ readonly color?: string;
2664
+ }
2665
+ /** A group of the admin menu (`admin.nav`). */
2666
+ interface NavGroup {
2667
+ /** Lowercase letters, digits and `-`; children are named `<parent>.<child>`, e.g. `shop.catalog`. */
2668
+ readonly id: string;
2669
+ readonly label: Label;
2670
+ readonly icon?: AdminIcon;
2671
+ /** Its place among the groups: lower first. Content is 0, the media library 10, Settings 1000. */
2672
+ readonly order?: number;
2673
+ /** Groups inside it, one level. */
2674
+ readonly children?: readonly Omit<NavGroup, 'children'>[];
2675
+ }
2676
+ /** An entry of the command palette (⌘K) that opens a page of the admin. */
2677
+ interface AdminCommand {
2678
+ readonly label: Label;
2679
+ /** A path in the admin, e.g. `/collections/orders?f_status=paid`. */
2680
+ readonly href: string;
2681
+ readonly icon?: AdminIcon;
2682
+ /** Words that find it besides its label. */
2683
+ readonly keywords?: readonly string[];
2684
+ }
2685
+ /** Who may see an admin page or dashboard widget. Default: every logged-in user. */
2686
+ type AdminViewAccess = (args: {
2687
+ readonly user: AuthUser;
2688
+ }) => boolean | Promise<boolean>;
2689
+ /** A page of its own in the admin, e.g. a plugin's report, at `<admin>/p/<path>`. */
2690
+ interface AdminPage {
2691
+ /** Its address under `<admin>/p/`: lowercase letters, digits and `-`, e.g. `forms-overview`. */
2692
+ readonly path: string;
2693
+ /** The page's body: a Web Component from an admin module. The admin draws the header. */
2694
+ readonly component: AdminComponent;
2695
+ /** Its title in the header and the menu. */
2696
+ readonly label: Label;
2697
+ /** Icon in the admin menu. Default `file-text`. */
2698
+ readonly icon?: AdminIcon;
2699
+ /**
2700
+ * Its menu group: `content` (default), `settings`, a group's id (`admin.nav`) or a label; `false`:
2701
+ * not listed, reached by links.
2702
+ */
2703
+ readonly group?: 'content' | 'settings' | false | Label;
2704
+ /** Its place in its menu group: lower first. Default: after those with one, in config order. */
2705
+ readonly order?: number;
2706
+ /** Who may open it. Pages a user may not open are left out of their admin. */
2707
+ readonly access?: AdminViewAccess;
2708
+ }
2709
+ /** A panel on the admin's dashboard, after the built-in ones. */
2710
+ interface DashboardWidget {
2711
+ /**
2712
+ * What roles are given in Settings → Roles (`widget:<id>`): keep it when the component's tag
2713
+ * changes. Default: the component's tag.
2714
+ */
2715
+ readonly id?: string;
2716
+ /** A Web Component from an admin module. */
2717
+ readonly component: AdminComponent;
2718
+ /** `half` (default): the side column, beside drafts and recent edits; `full`: below both. */
2719
+ readonly width?: 'half' | 'full';
2720
+ /** Its name in Settings → Roles (`auth.rbac`). Default: the component's tag. */
2721
+ readonly label?: Label;
2722
+ /** Who sees it. */
2723
+ readonly access?: AdminViewAccess;
2724
+ }
2725
+ interface AdminConfig {
2726
+ /** Where the admin UI is served. Default `/admin`. */
2727
+ readonly path?: string;
2728
+ /** Default admin UI language. Default `en`. */
2729
+ readonly locale?: AdminLocale;
2730
+ /** Name, logo and color of the admin UI. */
2731
+ readonly brand?: AdminBrand;
2732
+ /**
2733
+ * The public site, for the admin's "View site" link: a path (`/`) or an `https://` URL.
2734
+ * Default `/` with Nuxt and Next.js; none with the standalone server.
2735
+ */
2736
+ readonly siteURL?: string;
2737
+ /**
2738
+ * The admin menu's groups, which collections, globals and pages name in `admin.group`, e.g.
2739
+ * `{ id: 'shop', label: 'Shop', icon: 'store', children: [{ id: 'catalog', label: 'Catalog' }] }`.
2740
+ * Built in: `content` and `settings` (with `site`, `people` and `system`); declaring one of
2741
+ * their ids changes its label, icon or order. Plugins declare their own.
2742
+ */
2743
+ readonly nav?: readonly NavGroup[];
2744
+ /** More entries of the command palette (⌘K), e.g. a plugin's page with a filter. */
2745
+ readonly commands?: readonly AdminCommand[];
2746
+ /**
2747
+ * JavaScript modules the admin loads after login, which define Web Components used by
2748
+ * fields (`admin.component`, `admin.after`) and edit pages (`admin.sidebar`). Each entry is a
2749
+ * package export (`'@easy-cms/plugin-seo/admin'`) or a file path relative to the project root
2750
+ * (`'./admin/color-picker.js'`), resolved on the server. A module must be one self-contained ES
2751
+ * module file. Remote URLs are not allowed.
2752
+ */
2753
+ readonly modules?: readonly string[];
2754
+ /** Pages of their own, e.g. from plugins, at `<admin>/p/<path>`. */
2755
+ readonly pages?: readonly AdminPage[];
2756
+ /** Panels on the dashboard, after the built-in ones. */
2757
+ readonly dashboard?: readonly DashboardWidget[];
2758
+ /**
2759
+ * A choice at the top of the menu that applies to everything in the admin, e.g. the tenant
2760
+ * (usually set by a plugin). The choice is kept in a cookie, which every request of the admin
2761
+ * sends; `onRequest` reads it.
2762
+ */
2763
+ readonly switcher?: AdminSwitcher | false;
2764
+ }
2765
+ /** See `AdminConfig.switcher`. */
2766
+ interface AdminSwitcher {
2767
+ /** Cookie that keeps the choice, e.g. `ecms-tenant`. */
2768
+ readonly cookie: string;
2769
+ readonly label: Label;
2770
+ /**
2771
+ * Path under the API that lists the choices for the logged-in user:
2772
+ * `{ options: [{ value, label }], all?: Label }`, with `all` to offer "all of them" (value `*`).
2773
+ */
2774
+ readonly options: string;
2775
+ }
2776
+ interface ImageSize {
2777
+ /** Key in `media.sizes`, e.g. `thumbnail`. */
2778
+ readonly name: string;
2779
+ readonly width: number;
2780
+ readonly height?: number;
2781
+ /** How to fit when both width and height are set. Default `cover`. */
2782
+ readonly fit?: 'cover' | 'contain' | 'inside';
2783
+ }
2784
+ /**
2785
+ * Uploads from a link: the server downloads the file (`cms.uploadFromURL`, `POST <api>/media`
2786
+ * with `{ url }`, "From a link" in the admin).
2787
+ */
2788
+ interface UploadFromURLConfig {
2789
+ /**
2790
+ * Hosts files may come from: `images.example.com`, `*.example.com` (its subdomains, not
2791
+ * `example.com` itself) or `*` for any public host. Checked at every redirect too.
2792
+ */
2793
+ readonly allowedHosts: readonly string[];
2794
+ /**
2795
+ * Also download from private network addresses (`10.x`, `192.168.x`, `localhost`…), e.g. an
2796
+ * intranet. Off by default: links could then reach services that are not meant to be public.
2797
+ */
2798
+ readonly allowPrivate?: boolean;
2799
+ }
2800
+ interface UploadConfig {
2801
+ /** Directory for uploaded files with the default local storage, relative to the project root. Default `uploads`. */
2802
+ readonly dir?: string;
2803
+ /** Maximum file size in bytes. Default 10 MB. */
2804
+ readonly maxFileSize?: number;
2805
+ /** Allowed MIME types, detected from file contents. `image/*` style wildcards are allowed. */
2806
+ readonly mimeTypes?: readonly string[];
2807
+ /** Where files are stored. Default: local disk in `dir`. */
2808
+ readonly storage?: StorageAdapter;
2809
+ /** Resized copies generated for images when `sharp` is installed. */
2810
+ readonly imageSizes?: readonly ImageSize[];
2811
+ /** Lets users upload from a link. Off by default. */
2812
+ readonly fromURL?: UploadFromURLConfig;
2813
+ /**
2814
+ * Folders in the media library (`media-folders`; a file is in one folder at most). With
2815
+ * `auth.rbac`, admins can also choose which roles see and change each folder. Off by default.
2816
+ */
2817
+ readonly folders?: boolean;
2818
+ /**
2819
+ * Where files in private folders are kept: never served publicly, only through the API to
2820
+ * users who may see them or with `cms.signedMediaURL()`. Default: `storage`, when it has no
2821
+ * public URL (the local disk, Netlify Blobs); a storage with public URLs (S3 with `publicURL`,
2822
+ * a public Vercel Blob store) needs one, e.g. `vercelBlobStorage({ access: 'private' })`.
2823
+ */
2824
+ readonly privateStorage?: StorageAdapter;
2825
+ }
2826
+ interface RoutesConfig {
2827
+ /** Where the REST API is served. Default `/api/cms`. */
2828
+ readonly api?: string;
2829
+ }
2830
+ /**
2831
+ * People who sign in on the site rather than to the admin, e.g. customers of a shop.
2832
+ */
2833
+ interface MembersConfig {
2834
+ /**
2835
+ * Their roles, also listed in `roles`. Members never get into the admin, and access that
2836
+ * defaults to logged-in users (`isStaff`) doesn't count them: give them access in the
2837
+ * collections they use, e.g. with `isSignedIn`.
2838
+ */
2839
+ readonly roles: readonly string[];
2840
+ /** Visitors create their own account: `POST <api>/auth/signup`. */
2841
+ readonly signUp?: MembersSignup;
2842
+ /**
2843
+ * The site's pages that open the links of members' emails, given `?token=`: paths on
2844
+ * `admin.siteURL` (else `serverURL`), or full URLs. Without them, links go to the admin's pages.
2845
+ */
2846
+ readonly pages?: {
2847
+ /** Confirms the email of a new account: `POST <api>/auth/verify-email` with the token. */
2848
+ readonly verifyEmail?: string;
2849
+ /** Sets a forgotten password: `POST <api>/auth/reset-password` with the token. */
2850
+ readonly resetPassword?: string;
2851
+ };
2852
+ /** Your own text for the email that confirms an address. Default: English or Thai. */
2853
+ readonly emails?: {
2854
+ readonly verifyEmail?: PasswordEmailFn;
2855
+ };
2856
+ }
2857
+ interface MembersSignup {
2858
+ /** The role new accounts get: one of `members.roles`. */
2859
+ readonly role: string;
2860
+ /** New accounts confirm their email with a link before signing in. Default `true`. */
2861
+ readonly verifyEmail?: boolean;
2862
+ /** Cloudflare Turnstile, checked on every sign-up when set. */
2863
+ readonly turnstile?: {
2864
+ readonly siteKey: string;
2865
+ readonly secretKey: string;
2866
+ };
2867
+ }
2868
+ interface AuthConfig {
2869
+ /**
2870
+ * Roles a user can have. Must include `admin`. Default `['admin', 'editor']`. With `rbac`, these
2871
+ * are the roles that always exist; admins can add more in Settings → Roles.
2872
+ */
2873
+ readonly roles?: readonly string[];
2874
+ /**
2875
+ * Roles and their permissions from the admin (Settings → Roles): admins tick what each role
2876
+ * may do with each collection, global and admin page, on top of access rules. Default `false`.
2877
+ */
2878
+ readonly rbac?: boolean;
2879
+ /** Session lifetime in seconds. Default 7 days. */
2880
+ readonly tokenExpiration?: number;
2881
+ /** Failed logins allowed per email (and IP) within `lockWindow`. Default 5. */
2882
+ readonly maxLoginAttempts?: number;
2883
+ /** Window for `maxLoginAttempts`, in seconds. Default 15 minutes. */
2884
+ readonly lockWindow?: number;
2885
+ /**
2886
+ * Origins allowed to send cookie-authenticated requests, besides the API's own origin.
2887
+ * Needed when the admin or frontend is served from another origin, or behind a proxy that rewrites the host.
2888
+ */
2889
+ readonly trustedOrigins?: readonly string[];
2890
+ /**
2891
+ * How long a "forgot password" link works, in seconds. Default 1 hour. Links work once: setting
2892
+ * the password ends them. Needs `email` and, in production, `serverURL` for the link.
2893
+ */
2894
+ readonly resetPasswordExpiration?: number;
2895
+ /** How long an invitation link works, in seconds. Default 7 days. */
2896
+ readonly inviteExpiration?: number;
2897
+ /**
2898
+ * Signing in to the admin with outside accounts, e.g. `[google({ clientId, clientSecret })]`
2899
+ * from `@easy-cms/auth-oauth`. Users are matched by their verified email.
2900
+ */
2901
+ readonly providers?: readonly AuthProvider[];
2902
+ /**
2903
+ * With `providers`: people from these email domains who sign in for the first time get an
2904
+ * account with `role` (default: as new users). Without it, only existing users can sign in.
2905
+ */
2906
+ readonly providerSignUp?: {
2907
+ readonly domains: readonly string[];
2908
+ readonly role?: string;
2909
+ };
2910
+ /**
2911
+ * `false`: only admins may sign in with a password (a way in when the provider is down);
2912
+ * everyone else signs in with `providers`. Default `true`.
2913
+ */
2914
+ readonly password?: boolean;
2915
+ /**
2916
+ * A code the first admin must enter to create their account, so nobody else can claim a
2917
+ * freshly deployed site. Default: the `EASY_CMS_SETUP_CODE` environment variable; without it,
2918
+ * no code is asked.
2919
+ */
2920
+ readonly setupCode?: string;
2921
+ /**
2922
+ * People who sign in on the site, not to the admin, e.g. customers: their roles, signing up,
2923
+ * and the pages their email links open.
2924
+ */
2925
+ readonly members?: MembersConfig;
2926
+ /** Your own text for the password emails, e.g. in your brand's voice. Default: English or Thai. */
2927
+ readonly emails?: {
2928
+ readonly resetPassword?: PasswordEmailFn;
2929
+ readonly invite?: PasswordEmailFn;
2930
+ readonly passwordChanged?: PasswordEmailFn;
2931
+ };
2932
+ }
2933
+ /** What `onRequest` receives. */
2934
+ interface OnRequestArgs {
2935
+ readonly headers: Headers;
2936
+ /** The request's URL; `undefined` when only headers are known (`cms.forRequest(headers)`). */
2937
+ readonly url: URL | undefined;
2938
+ /** The user of the session or API key, before `onRequest` changes it; `null` when anonymous. */
2939
+ readonly user: AuthUser | null;
2940
+ readonly cms: EasyCMS;
2941
+ }
2942
+ /**
2943
+ * Runs on each API request (REST, plugin endpoints, `cms.forRequest`) after the user is known:
2944
+ * returns the request's `context` (e.g. the tenant it works in, from a header) and may change the
2945
+ * user (e.g. their role in that tenant, `scoped`). Plugins that set it call the one before.
2946
+ */
2947
+ type OnRequest = (args: OnRequestArgs) => MaybePromise<{
2948
+ readonly user?: AuthUser | null;
2949
+ readonly context?: RequestContext;
2950
+ } | undefined>;
2951
+ /** What an endpoint's handler receives. */
2952
+ interface EndpointRequest {
2953
+ readonly request: Request;
2954
+ readonly url: URL;
2955
+ /** Values of `:name` segments in the endpoint's path. */
2956
+ readonly params: Readonly<Record<string, string>>;
2957
+ /** The logged-in user (session cookie or Bearer token), or `null`. */
2958
+ readonly user: AuthUser | null;
2959
+ /** The request's context (`onRequest`): pass it to the Local API with `user`. */
2960
+ readonly context: RequestContext;
2961
+ /** The client's IP address, when the adapter knows it (e.g. to rate-limit a public form). */
2962
+ readonly ip: string | undefined;
2963
+ /** The Local API. Pass `{ user, overrideAccess: false }` to apply the user's access rules. */
2964
+ readonly cms: EasyCMS;
2965
+ /** The request's JSON body, which must be an object (at most 1 MB). */
2966
+ json(): Promise<Record<string, unknown>>;
2967
+ /** The request's body as text, as sent (at most 1 MB), e.g. to check a webhook's signature. */
2968
+ text(): Promise<string>;
2969
+ }
2970
+ /**
2971
+ * Work that runs with the scheduled jobs: every minute in a server, or each time a cron calls
2972
+ * `GET <api>/jobs/run`. E.g. a plugin's clean-up.
2973
+ */
2974
+ interface JobConfig {
2975
+ /** Unique, e.g. `ecommerce:carts`. */
2976
+ readonly name: string;
2977
+ /**
2978
+ * At most this often, in seconds. Default: every run. The last run is kept in the database, so
2979
+ * it holds across processes and serverless invocations.
2980
+ */
2981
+ readonly every?: number;
2982
+ readonly run: (args: {
2983
+ readonly cms: EasyCMS;
2984
+ readonly now: Date;
2985
+ }) => MaybePromise<unknown>;
2986
+ }
2987
+ /** An `easy-cms <name> [args…]` command. Built-in commands keep their names. */
2988
+ interface CliCommand {
2989
+ /** Lowercase, e.g. `nested:rebuild`. */
2990
+ readonly name: string;
2991
+ /** One line for `easy-cms --help`. */
2992
+ readonly description: string;
2993
+ /** Shown by `easy-cms <name> --help`. */
2994
+ readonly help?: string;
2995
+ /** Runs with the CMS open (no schema push); return a non-zero exit code on failure. */
2996
+ readonly run: (args: {
2997
+ readonly cms: EasyCMS;
2998
+ /** Positional arguments after the command name. */
2999
+ readonly args: readonly string[];
3000
+ /** `--name value` and `--name` (true) after the command name, e.g. `{ dryRun: true }`. */
3001
+ readonly flags: Readonly<Record<string, string | boolean>>;
3002
+ readonly log: (line: string) => void;
3003
+ }) => MaybePromise<number | undefined>;
3004
+ }
3005
+ /**
3006
+ * A custom REST endpoint, served under `routes.api`. Writes from the browser pass the same
3007
+ * CSRF check as the built-in endpoints.
3008
+ */
3009
+ interface Endpoint {
3010
+ /**
3011
+ * Path under the API, e.g. `/seo/generate` or `/stats/:collection`. The first segment must
3012
+ * not be a collection slug or one of `users`, `globals`, `admin`, `jobs`. With `root`, the
3013
+ * path from the site's root, outside the API and the admin.
3014
+ */
3015
+ readonly path: string;
3016
+ readonly method: 'get' | 'post' | 'put' | 'patch' | 'delete';
3017
+ /**
3018
+ * Serve the path from the site's root instead of under `routes.api`, e.g. `/robots.txt`.
3019
+ * Only the standalone server (`easy-cms serve`) serves these; a Nuxt or Next.js app owns its
3020
+ * root, so plugins also offer a helper to use in the app's own route. Default false.
3021
+ */
3022
+ readonly root?: boolean;
3023
+ /**
3024
+ * Returns a `Response`, or a value that is sent as JSON. Throw an Easy CMS error
3025
+ * (`UnauthorizedError`, `ForbiddenError`, `ValidationError`, `NotFoundError`) for error responses.
3026
+ */
3027
+ readonly handler: (request: EndpointRequest) => MaybePromise<unknown>;
3028
+ }
3029
+ /** A plugin's package and version, listed for admins on the dashboard (System). */
3030
+ /** The version of what plugins are written against (`definePlugin`, `Config`, hooks, endpoints). */
3031
+ declare const PLUGIN_API_VERSION = 1;
3032
+ interface PluginInfo {
3033
+ /** Usually the package name, e.g. `@acme/easy-cms-plugin-stats`. */
3034
+ readonly name: string;
3035
+ readonly version?: string;
3036
+ /**
3037
+ * The plugin API the plugin is written for (`PLUGIN_API_VERSION`). Easy CMS refuses to start
3038
+ * with a plugin of another one, saying what to upgrade. Leave it out to skip the check.
3039
+ */
3040
+ readonly apiVersion?: number;
3041
+ }
3042
+ /** Receives the config and returns a modified copy. Runs before validation. */
3043
+ type Plugin = ((config: Config) => MaybePromise<Config>) & {
3044
+ /** Set by `definePlugin(plugin, info)`. */
3045
+ readonly info?: PluginInfo;
3046
+ };
3047
+ /** Holds a plugin's `PluginTypes` at the type level only: nothing is stored at runtime. */
3048
+ declare const PLUGIN_TYPES: unique symbol;
3049
+ /**
3050
+ * What a plugin adds, for the document types inferred from the config: fields on collections
3051
+ * and globals (by slug), and whole collections. Field types are written like config fields,
3052
+ * e.g. `{ fields: { posts: readonly [{ readonly name: 'meta'; readonly type: 'group'; … }] } }`.
3053
+ */
3054
+ interface PluginTypes {
3055
+ readonly fields?: {
3056
+ readonly [collection: string]: readonly Field[];
3057
+ };
3058
+ readonly globalFields?: {
3059
+ readonly [global: string]: readonly Field[];
3060
+ };
3061
+ readonly collections?: readonly CollectionConfig[];
3062
+ }
3063
+ /** A plugin that tells the type system what it adds (see `definePlugin`). */
3064
+ type TypedPlugin<T extends PluginTypes> = Plugin & {
3065
+ readonly [PLUGIN_TYPES]?: T;
3066
+ };
3067
+ /**
3068
+ * Returns the plugin with what it adds as types, so `cms.find()` and `CollectionDocument` know
3069
+ * the plugin's fields; `info` (its name and version) is listed for admins on the dashboard:
3070
+ *
3071
+ * ```ts
3072
+ * export const colorPlugin = (options: { collections: readonly string[] }) =>
3073
+ * definePlugin<{ fields: { posts: readonly [{ readonly name: 'color'; readonly type: 'text' }] } }>(
3074
+ * (config) => ({ ...config, collections: … }),
3075
+ * { name: '@acme/easy-cms-plugin-color', version: '1.2.0' },
3076
+ * )
3077
+ * ```
3078
+ */
3079
+ declare function definePlugin<const T extends PluginTypes = Record<never, never>>(plugin: Plugin, info?: PluginInfo): TypedPlugin<T>;
3080
+ /** Database backups (Settings → Backups): one compressed SQLite file each. */
3081
+ interface BackupsConfig {
3082
+ /** Back up automatically every day or week. Default: only by hand. */
3083
+ readonly frequency?: 'daily' | 'weekly';
3084
+ /** When, as `HH:MM` in the server's time zone. Default `03:00`. */
3085
+ readonly at?: string;
3086
+ /** How many finished backups to keep; older ones are deleted. Default 7. */
3087
+ readonly keep?: number;
3088
+ /** Folder for the default local storage, from the project root. Default `backups`. Never served publicly. */
3089
+ readonly dir?: string;
3090
+ /** Where backups are stored instead, e.g. `s3Storage()` with a private bucket. */
3091
+ readonly storage?: StorageAdapter;
3092
+ /**
3093
+ * For Postgres: the SQLite adapter that writes the backup file, `sqlite` from
3094
+ * `@easy-cms/db-sqlite`. SQLite databases back themselves up and don't need it.
3095
+ */
3096
+ readonly sqlite?: (options: {
3097
+ url: string;
3098
+ tablePrefix?: string;
3099
+ }) => DatabaseAdapter;
3100
+ }
3101
+ interface Config {
3102
+ /** Signs sessions. At least 32 characters; read it from `process.env.EASY_CMS_SECRET`. */
3103
+ readonly secret: string;
3104
+ readonly db: DatabaseAdapter;
3105
+ /**
3106
+ * Public origin of the app, e.g. `https://example.com`. When set, media URLs are absolute
3107
+ * so frontends on other origins can use them. Default: relative URLs.
3108
+ */
3109
+ readonly serverURL?: string;
3110
+ /**
3111
+ * Origins whose browser code may call the REST API (CORS), e.g. a Vite or React app on
3112
+ * another origin. `'*'` allows any origin for anonymous requests. Origins in
3113
+ * `auth.trustedOrigins` are always allowed, with cookies. Default: none.
3114
+ */
3115
+ readonly cors?: readonly string[] | '*';
3116
+ /**
3117
+ * Lets a cron service run due scheduled jobs: `GET <api>/jobs/run` with
3118
+ * `Authorization: Bearer <cronSecret>`. Default: the `CRON_SECRET` environment variable (the
3119
+ * convention of Vercel Cron). Logged-in admins can always run jobs.
3120
+ */
3121
+ readonly cronSecret?: string;
3122
+ /** Endpoints notified when content changes, e.g. to rebuild a static site. */
3123
+ readonly webhooks?: readonly WebhookConfig[];
3124
+ /**
3125
+ * Events of the app or its plugins besides content changes, e.g. `order.paid`: sent with
3126
+ * `cms.emit()` to the webhooks that list them in `events`. Named `<area>.<what>`.
3127
+ */
3128
+ readonly events?: readonly string[];
3129
+ /** Work run with the scheduled jobs, e.g. a plugin's clean-up. */
3130
+ readonly jobs?: readonly JobConfig[];
3131
+ /** Content in several languages: fields with `localized: true` hold one value per locale. */
3132
+ readonly localization?: LocalizationConfig | false;
3133
+ readonly routes?: RoutesConfig;
3134
+ readonly admin?: AdminConfig;
3135
+ readonly upload?: UploadConfig;
3136
+ readonly auth?: AuthConfig;
3137
+ /**
3138
+ * Collections. A collection with slug `users` adds fields, access or hooks
3139
+ * to the built-in users collection.
3140
+ */
3141
+ readonly collections?: readonly CollectionConfig[];
3142
+ readonly globals?: readonly GlobalConfig[];
3143
+ /** Custom REST endpoints, e.g. from plugins. */
3144
+ readonly endpoints?: readonly Endpoint[];
3145
+ /** Extra `easy-cms <name>` CLI commands, e.g. from plugins. */
3146
+ readonly cliCommands?: readonly CliCommand[];
3147
+ /**
3148
+ * The request's context and user, worked out on each API request (usually by a plugin). Several
3149
+ * run in order: each gets the user the one before set, and their contexts are merged.
3150
+ */
3151
+ readonly onRequest?: OnRequest | readonly OnRequest[];
3152
+ /**
3153
+ * Field types from packages, e.g. `color` from `@easy-cms/fields`: fields then use
3154
+ * `type: 'color'`. See `defineFieldType`.
3155
+ */
3156
+ readonly fieldTypes?: readonly FieldTypeDefinition[];
3157
+ /**
3158
+ * API keys for scripts and other apps (`Authorization: Bearer ecms_…`), managed in the admin
3159
+ * under Settings. A key acts as its owner, limited to the collections and operations it lists.
3160
+ */
3161
+ readonly apiKeys?: boolean;
3162
+ /**
3163
+ * Sends email for plugins such as the form builder, e.g. `smtp()` from
3164
+ * `@easy-cms/email-smtp`, or `consoleEmail()` in development. Emails are queued in the
3165
+ * database and retried until sent.
3166
+ */
3167
+ readonly email?: EmailAdapter;
3168
+ /**
3169
+ * Database backups, made on a schedule and from the admin (Settings → Backups). Without
3170
+ * `every`, admins can still back up by hand.
3171
+ */
3172
+ readonly backups?: BackupsConfig;
3173
+ /**
3174
+ * The audit log (Settings → Audit log): who changed what and when, sign-ins, and admin
3175
+ * actions, kept for `keep` days. `true` for the defaults.
3176
+ */
3177
+ readonly audit?: boolean | AuditConfig;
3178
+ readonly plugins?: readonly Plugin[];
3179
+ }
3180
+ /** The config after plugins ran, validation passed and defaults were applied. */
3181
+ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'endpoints' | 'plugins' | 'audit'> {
3182
+ readonly cors: readonly string[] | '*';
3183
+ /** `null` without localization. */
3184
+ readonly localization: Required<LocalizationConfig> | null;
3185
+ readonly routes: Required<RoutesConfig>;
3186
+ readonly admin: Required<AdminConfig>;
3187
+ readonly upload: Required<Omit<UploadConfig, 'storage' | 'fromURL' | 'privateStorage'>> & Pick<UploadConfig, 'storage' | 'fromURL' | 'privateStorage'>;
3188
+ readonly auth: Required<AuthConfig>;
3189
+ /** `false` without an audit log. */
3190
+ readonly audit: Required<AuditConfig> | false;
3191
+ readonly collections: readonly CollectionConfig[];
3192
+ readonly globals: readonly GlobalConfig[];
3193
+ readonly endpoints: readonly Endpoint[];
3194
+ readonly cliCommands: readonly CliCommand[];
3195
+ readonly events: readonly string[];
3196
+ readonly jobs: readonly JobConfig[];
3197
+ readonly fieldTypes: readonly FieldTypeDefinition[];
3198
+ /** The config's plugins in order, with their `info` where they give one. */
3199
+ readonly installedPlugins: readonly Partial<PluginInfo>[];
3200
+ /** What the config's checks warn about (e.g. options Easy CMS doesn't know); logged at start. */
3201
+ readonly warnings: readonly ConfigIssue[];
3202
+ }
3203
+ /**
3204
+ * Declares the Easy CMS config. Returns it unchanged, keeping literal types
3205
+ * so document types can be inferred from it.
3206
+ */
3207
+ declare function defineConfig<const TConfig extends Config>(config: TConfig): TConfig;
3208
+
3209
+ export { type BuiltinMediaCollection as $, ADAPTER_API_VERSION as A, type ApiKeyOperation as B, type ApiKeyPermissions as C, type ArrayField as D, type EmailSetting as E, type AuditChange as F, type AuditConfig as G, type AuditEntry as H, type ID as I, AuditLog as J, type AuditPage as K, type AuditVerification as L, Auth as M, type AuthConfig as N, type AuthProvider as O, type PaginatedDocs as P, type AuthProviderProfile as Q, type AuthProviderRequest as R, type AuthUser as S, type BackupsConfig as T, type BeforeChangeHook as U, type BeforeDeleteHook as V, type BeforeValidateHook as W, type Block as X, type BlocksField as Y, type BooleanField as Z, type BuiltinApiKeysCollection as _, ADMIN_ICONS as a, type GlobalDocument as a$, type BuiltinMediaFoldersCollection as a0, type BuiltinUsersCollection as a1, type BundleHints as a2, type CliCommand as a3, type CollectionAccess as a4, type CollectionAdmin as a5, type CollectionConfig as a6, type CollectionDocument as a7, type CollectionHooks as a8, type CollectionListAdmin as a9, type EndpointRequest as aA, type ErrorCode as aB, FIELD_TYPES as aC, type Field as aD, type FieldAccess as aE, type FieldAccessFn as aF, type FieldAdmin as aG, type FieldError as aH, type FieldRule as aI, type FieldType as aJ, type FieldTypeBase as aK, type FieldTypeDefinition as aL, type FieldTypeValidateContext as aM, type FieldValidate as aN, type FieldValidateContext as aO, type FieldValue as aP, type FieldsInput as aQ, type FieldsValue as aR, type FilterOptions as aS, type FilterOptionsArgs as aT, type FindArgs as aU, type FindOptions as aV, type FolderLevel as aW, type FolderPermissions as aX, ForbiddenError as aY, type GlobalAccess as aZ, type GlobalConfig as a_, type CollectionSlug as aa, type Config as ab, ConfigError as ac, type ConfigIssue as ad, type ContainerAdmin as ae, type CreateEasyCMSOptions as af, type CreateInput as ag, type CreateMigrationResult as ah, type CustomField as ai, type CustomFieldTypes as aj, type DashboardWidget as ak, type Database as al, type DatabaseAdapter as am, type DatabaseInitArgs as an, type DatabaseTransfer as ao, type DateField as ap, type DepthOptions as aq, type DirectUpload as ar, type DiskStorageOptions as as, type DocumentOf as at, EasyCMS as au, EasyCMSError as av, type EmailAdapter as aw, type EmailField as ax, type EmailMessage as ay, type Endpoint as az, type Access as b, type SlugOf as b$, type GlobalHooks as b0, type GlobalInput as b1, type GlobalSlug as b2, type GroupField as b3, type ImageSize as b4, type IncrementOptions as b5, type InferCollection as b6, type InferGlobal as b7, type JobConfig as b8, type JsonField as b9, type PreviewTarget as bA, type PreviewURL as bB, QueryError as bC, ROLE_OPERATIONS as bD, type RawDocument as bE, type ReadOptions as bF, type RelationshipField as bG, type RequestContext as bH, type ResolvedConfig as bI, type RichTextDocument as bJ, type RichTextField as bK, type Role as bL, type RoleOperation as bM, type RolePermissions as bN, Roles as bO, type RoutesConfig as bP, type ScheduledPublish as bQ, SchemaError as bR, type SchemaMode as bS, type SelectField as bT, type SelectOption as bU, type Session as bV, type SidebarPanel as bW, type SignupArgs as bX, type SignupResult as bY, SingleSignOn as bZ, type SlugField as b_, type Label as ba, type LayoutNode as bb, type LivePreview as bc, type LocalizationConfig as bd, type Logger as be, type LoginArgs as bf, type MediaDocument as bg, type MediaSize as bh, type MembersConfig as bi, type MembersSignup as bj, type MigrationInfo as bk, type NavGroup as bl, NotFoundError as bm, type NumberField as bn, type OnRequest as bo, type OnRequestArgs as bp, type Operation as bq, PLUGIN_API_VERSION as br, type PasswordEmail as bs, type PasswordEmailArgs as bt, type PasswordEmailFn as bu, type PasswordLinkOptions as bv, PayloadTooLargeError as bw, type Plugin as bx, type PluginInfo as by, type PluginTypes as bz, type AccessArgs as c, type SsoOutcome as c0, type SsoProviderRef as c1, type StorageAdapter as c2, type StoredFile as c3, type TextField as c4, type TextareaField as c5, TooManyRequestsError as c6, type TypedPlugin as c7, UnauthorizedError as c8, type UpdateInput as c9, isSignedIn as cA, isStaff as cB, isSystemAdmin as cC, mimeAllowedBy as cD, rowFields as cE, silentLogger as cF, uniqueWithinOf as cG, API_KEYS as cH, API_KEY_GLOBAL_OPERATIONS as cI, API_KEY_OPERATIONS as cJ, DEFAULT_MAX_VERSIONS as cK, DEFAULT_PASSWORD_EMAILS as cL, FIELD_TYPE_BASES as cM, FOLDER_LEVELS as cN, MediaFolders as cO, SSO_COOKIE as cP, codeOfStatus as cQ, keyAllows as cR, type UpdateOptions as ca, type UploadConfig as cb, type UploadField as cc, type UploadFromURLConfig as cd, type UserIdentity as ce, ValidationError as cf, type Version as cg, type VersionSummary as ch, type VersionsConfig as ci, WEBHOOK_EVENTS as cj, type WebhookConfig as ck, type WebhookEvent as cl, type WebhookPayload as cm, type Where as cn, type WhereOperators as co, anyone as cp, consoleEmail as cq, consoleLogger as cr, createEasyCMS as cs, defineConfig as ct, defineFieldType as cu, definePlugin as cv, diskStorage as cw, hasRows as cx, isAdmin as cy, isHasMany as cz, type AccessOptions as d, type AdminBrand as e, type AdminCommand as f, type AdminComponent as g, type AdminConfig as h, type AdminFieldArgs as i, type AdminIcon as j, type AdminLocale as k, type AdminPage as l, type AdminRole as m, type AdminRoleChange as n, type AdminRoleField as o, type AdminRoleTarget as p, type AdminRoleView as q, type AdminRoles as r, type AdminSso as s, type AdminSwitcher as t, type AdminViewAccess as u, type AfterChangeHook as v, type AfterDeleteHook as w, type AfterReadHook as x, type ApiKeyContext as y, type ApiKeyGlobalOperation as z };