@easy-cms/core 0.34.0 → 0.36.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +528 -188
  2. package/dist/index.js +1284 -258
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -56,29 +56,217 @@ declare const anyone: Access;
56
56
  declare const isLoggedIn: Access;
57
57
  declare const isAdmin: Access;
58
58
 
59
- /** What the email functions in `auth.emails` receive. */
60
- interface PasswordEmailArgs {
59
+ /** What a preview token opens: one document of a collection, or one global. */
60
+ type PreviewTarget = {
61
+ readonly collection: string;
62
+ readonly id: string;
63
+ } | {
64
+ readonly global: string;
65
+ };
66
+ /** Why a password link was sent: a forgotten password, or an invitation to a new account. */
67
+ type PasswordPurpose = 'reset' | 'invite';
68
+
69
+ /** The cookie that carries a sign-in through the provider and back. */
70
+ declare const SSO_COOKIE = "ecms-sso";
71
+ /** A provider as the login page shows it (`GET <api>/users/init`). */
72
+ interface SsoProviderRef {
73
+ id: string;
74
+ name: string;
75
+ icon: string;
76
+ }
77
+ /** Settings → SSO (`GET <api>/admin/sso`). */
78
+ interface AdminSso {
79
+ providers: (SsoProviderRef & {
80
+ /** Give this to the provider as the redirect (callback) URL. */
81
+ callbackURL: string | null;
82
+ details: Record<string, string>;
83
+ })[];
84
+ /** Who may sign in with a password: everyone, or only admins. */
85
+ password: 'everyone' | 'admins';
86
+ /** Email domains whose people get an account when they first sign in. */
87
+ signUp: {
88
+ domains: string[];
89
+ role: string;
90
+ } | null;
91
+ /** Without `serverURL`, sign-in only works in development. */
92
+ serverURL: boolean;
93
+ }
94
+ /** An outside account a user signs in with (`GET <api>/auth/identities`). */
95
+ interface UserIdentity {
96
+ id: ID;
97
+ provider: string;
98
+ /** The provider's name, e.g. `Google`. */
99
+ name: string;
100
+ email: string | null;
101
+ createdAt: string;
102
+ lastUsedAt: string | null;
103
+ }
104
+ /** Why a sign-in did not work, as `?sso=` on the page it ends on. */
105
+ type SsoOutcome = 'linked' | 'cancelled' | 'expired' | 'failed' | 'no-account' | 'unverified' | 'inactive' | 'taken';
106
+ /** Single sign-on with `auth.providers`. Available as `cms.auth.sso`. */
107
+ declare class SingleSignOn {
108
+ private readonly cms;
109
+ constructor(cms: EasyCMS);
110
+ private get config();
111
+ get enabled(): boolean;
112
+ providers(): SsoProviderRef[];
113
+ /** Whether a user may sign in with a password (`auth.password: false` leaves only admins). */
114
+ passwordAllowed(user: {
115
+ readonly [key: string]: unknown;
116
+ }): boolean;
117
+ /** `<serverURL><api>/auth/<id>/callback`, or `null` without `serverURL` in production. */
118
+ callbackURL(provider: string, origin?: string): string | null;
119
+ /**
120
+ * Starts signing in (or, with `user`, linking an account): the provider's URL to send the
121
+ * browser to, and the sealed value of the cookie that must come back with it.
122
+ */
123
+ start(providerId: string, options: {
124
+ origin?: string;
125
+ redirect?: string | null;
126
+ user?: AuthUser;
127
+ }): Promise<{
128
+ url: string;
129
+ cookie: string;
130
+ }>;
131
+ /**
132
+ * Finishes at `<api>/auth/<id>/callback`: checks the response, finds (or makes) the user and
133
+ * starts a session. Returns where to send the browser; failures end on the login page (or the
134
+ * account page, when linking) with `?sso=<outcome>`.
135
+ */
136
+ finish(providerId: string, options: {
137
+ url: URL;
138
+ cookie: string | undefined;
139
+ origin?: string;
140
+ user: AuthUser | null;
141
+ }): Promise<{
142
+ redirect: string;
143
+ session?: Session;
144
+ }>;
145
+ /** A user's outside accounts. */
146
+ identities(userId: ID): Promise<UserIdentity[]>;
147
+ /**
148
+ * Unlinks an outside account: the user's own, or anyone's for admins. Refuses to unlink the
149
+ * last way a user has to sign in.
150
+ */
151
+ unlink(actor: AuthUser, identityId: ID): Promise<void>;
152
+ /** Forgets a deleted user's outside accounts. */
153
+ forget(userId: ID): Promise<void>;
154
+ settings(origin?: string): AdminSso;
155
+ private provider;
156
+ /** Only pages of the admin: anything else (another site) goes to the admin's home. */
157
+ private adminPage;
158
+ private identity;
159
+ private addIdentity;
160
+ private userByEmail;
161
+ /** A new account for people from `allowSignUp.domains`. */
162
+ private signUp;
163
+ private defaultRole;
164
+ }
165
+
166
+ interface LoginArgs {
167
+ readonly email: string;
168
+ readonly password: string;
169
+ /** Client IP, used together with the email for rate limiting. */
170
+ readonly ip?: string | undefined;
171
+ }
172
+ interface Session {
61
173
  readonly user: AuthUser;
62
- /** The page that sets the password; absent for `passwordChanged`. */
63
- readonly url?: string;
64
- /** The admin language for the text: `en` or `th`. */
65
- readonly locale: string;
66
- /** When the link stops working. */
67
- readonly expiresAt?: Date;
174
+ /** Signed session token, the value of the session cookie. */
175
+ readonly token: string;
176
+ /** Send back in the `X-CSRF-Token` header on cookie-authenticated writes. */
177
+ readonly csrfToken: string;
178
+ readonly expiresAt: string;
68
179
  }
69
- /** An email's content: a subject, and text and/or HTML. */
70
- interface PasswordEmail {
71
- readonly subject: string;
72
- readonly text: string;
73
- readonly html?: string;
180
+ /** Where a password link is opened from, and the language of its email. */
181
+ interface PasswordLinkOptions {
182
+ /** The request's origin: used for the link only in development, when `serverURL` is not set. */
183
+ readonly origin?: string | undefined;
184
+ /** `en` or `th`. Default: `admin.locale`. */
185
+ readonly locale?: string | undefined;
186
+ }
187
+ /** Login, logout and session verification. Available as `cms.auth`. */
188
+ declare class Auth {
189
+ private readonly cms;
190
+ /** Signing in with outside accounts (`auth.providers`). */
191
+ readonly sso: SingleSignOn;
192
+ constructor(cms: EasyCMS);
193
+ private get db();
194
+ private get config();
195
+ /** Checks credentials and starts a session. Throws `UnauthorizedError` or `TooManyRequestsError`. */
196
+ login(args: LoginArgs): Promise<Session>;
197
+ /** Creates the first admin. Only works while there are no users. */
198
+ registerFirstUser(args: {
199
+ email: string;
200
+ password: string;
201
+ name?: string;
202
+ }): Promise<Session>;
203
+ hasUsers(): Promise<boolean>;
204
+ /** Ends the session for a signed token. Unknown tokens are ignored. */
205
+ logout(signedToken: string): Promise<void>;
206
+ /**
207
+ * Returns the user for a signed session token, or for an API key (`ecms_…`, with `apiKey` set
208
+ * on the user), or `null` if it is invalid or expired.
209
+ */
210
+ verify(signedToken: string | undefined | null): Promise<AuthUser | null>;
211
+ /** The CSRF token clients must echo for a signed session token. */
212
+ csrfFor(signedToken: string): string | undefined;
213
+ /** Starts a new session for a user, e.g. after they changed their own password. */
214
+ createSession(userId: ID): Promise<Session>;
215
+ /** Signs a user out everywhere. */
216
+ revokeSessions(userId: ID): Promise<void>;
217
+ /**
218
+ * The origin password links point to: `serverURL`, or the request's origin in development. In
219
+ * production the request's Host can't be trusted (a forged one would send the link elsewhere).
220
+ */
221
+ passwordLinkBase(origin?: string): string | null;
222
+ /** Whether "forgot password" and invitations work: `email` is set and links have an origin. */
223
+ canSendPasswordLinks(origin?: string): boolean;
224
+ /**
225
+ * Emails a link to set a new password, if an active account has this email. Always resolves the
226
+ * same way, so callers can't tell whether the email has an account; extra requests within
227
+ * `lockWindow` are ignored.
228
+ */
229
+ requestPasswordReset(args: PasswordLinkOptions & {
230
+ email: string;
231
+ ip?: string | undefined;
232
+ }): Promise<void>;
233
+ /**
234
+ * Emails a user a link to set their password: an invitation if they have none yet, else a
235
+ * reset link. For admins (the REST API checks the role). Returns which one was sent.
236
+ */
237
+ sendPasswordLink(userId: ID, options?: PasswordLinkOptions): Promise<PasswordPurpose>;
238
+ /** The user and purpose of a valid, unused password link, or `null`. */
239
+ checkPasswordToken(token: string): Promise<{
240
+ user: AuthUser;
241
+ purpose: PasswordPurpose;
242
+ } | null>;
243
+ /**
244
+ * Sets the password from a link, ends every session of the account and starts one here.
245
+ * Throws `ValidationError` when the link expired or was used, or the password is too short.
246
+ */
247
+ resetPassword(args: {
248
+ token: string;
249
+ password: string;
250
+ ip?: string | undefined;
251
+ locale?: string | undefined;
252
+ }): Promise<Session & {
253
+ purpose: PasswordPurpose;
254
+ }>;
255
+ private passwordTokenUser;
256
+ private mailPasswordLink;
257
+ private localeOf;
258
+ private findByEmail;
259
+ /** The owner of an API key, limited by the key; `null` when unknown, expired or disabled. */
260
+ private verifyApiKey;
261
+ private startSession;
262
+ private findSession;
263
+ private toAuthUser;
264
+ private deleteExpiredSessions;
265
+ private windowStart;
266
+ private checkRateLimit;
267
+ private recordFailure;
268
+ private clearFailures;
74
269
  }
75
- type PasswordEmailFn = (args: PasswordEmailArgs) => PasswordEmail | Promise<PasswordEmail>;
76
- /** The default emails, in English and Thai. */
77
- declare const DEFAULT_PASSWORD_EMAILS: {
78
- resetPassword: PasswordEmailFn;
79
- invite: PasswordEmailFn;
80
- passwordChanged: PasswordEmailFn;
81
- };
82
270
 
83
271
  interface Logger {
84
272
  info(message: string): void;
@@ -277,171 +465,6 @@ declare function consoleEmail(options?: {
277
465
  })[];
278
466
  };
279
467
 
280
- interface ConfigIssue {
281
- /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
282
- readonly path: string;
283
- readonly message: string;
284
- /** How to fix it. */
285
- readonly hint?: string;
286
- }
287
- declare class ConfigError extends Error {
288
- readonly issues: readonly ConfigIssue[];
289
- constructor(issues: readonly ConfigIssue[]);
290
- static format(issues: readonly ConfigIssue[]): string;
291
- }
292
- /** Base class for errors that map to an HTTP status in the REST API. */
293
- declare class EasyCMSError extends Error {
294
- readonly status: number;
295
- constructor(message: string, status: number);
296
- }
297
- interface FieldError {
298
- /** Dotted path, e.g. `links.0.url`. */
299
- readonly field: string;
300
- readonly message: string;
301
- }
302
- declare class ValidationError extends EasyCMSError {
303
- readonly errors: readonly FieldError[];
304
- constructor(collection: string, errors: readonly FieldError[]);
305
- }
306
- declare class NotFoundError extends EasyCMSError {
307
- constructor(collection: string, id: unknown);
308
- }
309
- /** A bad `where`, `sort` or unknown collection/global. */
310
- declare class QueryError extends EasyCMSError {
311
- constructor(message: string);
312
- }
313
- /** The database schema does not match the config (pending or missing migrations). */
314
- declare class SchemaError extends EasyCMSError {
315
- constructor(message: string);
316
- }
317
- /** Not logged in, or the session is invalid. */
318
- declare class UnauthorizedError extends EasyCMSError {
319
- constructor(message?: string);
320
- }
321
- /** Logged in, but not allowed to do this. */
322
- declare class ForbiddenError extends EasyCMSError {
323
- constructor(message?: string);
324
- }
325
- declare class TooManyRequestsError extends EasyCMSError {
326
- constructor(message: string);
327
- }
328
- declare class PayloadTooLargeError extends EasyCMSError {
329
- constructor(message: string);
330
- }
331
-
332
- /** What a preview token opens: one document of a collection, or one global. */
333
- type PreviewTarget = {
334
- readonly collection: string;
335
- readonly id: string;
336
- } | {
337
- readonly global: string;
338
- };
339
- /** Why a password link was sent: a forgotten password, or an invitation to a new account. */
340
- type PasswordPurpose = 'reset' | 'invite';
341
-
342
- interface LoginArgs {
343
- readonly email: string;
344
- readonly password: string;
345
- /** Client IP, used together with the email for rate limiting. */
346
- readonly ip?: string | undefined;
347
- }
348
- interface Session {
349
- readonly user: AuthUser;
350
- /** Signed session token, the value of the session cookie. */
351
- readonly token: string;
352
- /** Send back in the `X-CSRF-Token` header on cookie-authenticated writes. */
353
- readonly csrfToken: string;
354
- readonly expiresAt: string;
355
- }
356
- /** Where a password link is opened from, and the language of its email. */
357
- interface PasswordLinkOptions {
358
- /** The request's origin: used for the link only in development, when `serverURL` is not set. */
359
- readonly origin?: string | undefined;
360
- /** `en` or `th`. Default: `admin.locale`. */
361
- readonly locale?: string | undefined;
362
- }
363
- /** Login, logout and session verification. Available as `cms.auth`. */
364
- declare class Auth {
365
- private readonly cms;
366
- constructor(cms: EasyCMS);
367
- private get db();
368
- private get config();
369
- /** Checks credentials and starts a session. Throws `UnauthorizedError` or `TooManyRequestsError`. */
370
- login(args: LoginArgs): Promise<Session>;
371
- /** Creates the first admin. Only works while there are no users. */
372
- registerFirstUser(args: {
373
- email: string;
374
- password: string;
375
- name?: string;
376
- }): Promise<Session>;
377
- hasUsers(): Promise<boolean>;
378
- /** Ends the session for a signed token. Unknown tokens are ignored. */
379
- logout(signedToken: string): Promise<void>;
380
- /**
381
- * Returns the user for a signed session token, or for an API key (`ecms_…`, with `apiKey` set
382
- * on the user), or `null` if it is invalid or expired.
383
- */
384
- verify(signedToken: string | undefined | null): Promise<AuthUser | null>;
385
- /** The CSRF token clients must echo for a signed session token. */
386
- csrfFor(signedToken: string): string | undefined;
387
- /** Starts a new session for a user, e.g. after they changed their own password. */
388
- createSession(userId: ID): Promise<Session>;
389
- /** Signs a user out everywhere. */
390
- revokeSessions(userId: ID): Promise<void>;
391
- /**
392
- * The origin password links point to: `serverURL`, or the request's origin in development. In
393
- * production the request's Host can't be trusted (a forged one would send the link elsewhere).
394
- */
395
- passwordLinkBase(origin?: string): string | null;
396
- /** Whether "forgot password" and invitations work: `email` is set and links have an origin. */
397
- canSendPasswordLinks(origin?: string): boolean;
398
- /**
399
- * Emails a link to set a new password, if an active account has this email. Always resolves the
400
- * same way, so callers can't tell whether the email has an account; extra requests within
401
- * `lockWindow` are ignored.
402
- */
403
- requestPasswordReset(args: PasswordLinkOptions & {
404
- email: string;
405
- ip?: string | undefined;
406
- }): Promise<void>;
407
- /**
408
- * Emails a user a link to set their password: an invitation if they have none yet, else a
409
- * reset link. For admins (the REST API checks the role). Returns which one was sent.
410
- */
411
- sendPasswordLink(userId: ID, options?: PasswordLinkOptions): Promise<PasswordPurpose>;
412
- /** The user and purpose of a valid, unused password link, or `null`. */
413
- checkPasswordToken(token: string): Promise<{
414
- user: AuthUser;
415
- purpose: PasswordPurpose;
416
- } | null>;
417
- /**
418
- * Sets the password from a link, ends every session of the account and starts one here.
419
- * Throws `ValidationError` when the link expired or was used, or the password is too short.
420
- */
421
- resetPassword(args: {
422
- token: string;
423
- password: string;
424
- ip?: string | undefined;
425
- locale?: string | undefined;
426
- }): Promise<Session & {
427
- purpose: PasswordPurpose;
428
- }>;
429
- private passwordTokenUser;
430
- private mailPasswordLink;
431
- private localeOf;
432
- private findByEmail;
433
- /** The owner of an API key, limited by the key; `null` when unknown, expired or disabled. */
434
- private verifyApiKey;
435
- private startSession;
436
- private findSession;
437
- private toAuthUser;
438
- private deleteExpiredSessions;
439
- private windowStart;
440
- private checkRateLimit;
441
- private recordFailure;
442
- private clearFailures;
443
- }
444
-
445
468
  type Simplify<T> = {
446
469
  [K in keyof T]: T[K];
447
470
  } & {};
@@ -831,11 +854,25 @@ declare class Roles {
831
854
  private readonly config;
832
855
  private readonly db;
833
856
  private readonly log?;
857
+ /** Writes to the audit log (`audit`). */
858
+ private readonly audit?;
834
859
  readonly enabled: boolean;
835
860
  private readonly versions;
836
861
  private cache;
837
862
  private synced;
838
- constructor(config: ResolvedConfig, db: Database, log?: ((message: string) => void) | undefined);
863
+ constructor(config: ResolvedConfig, db: Database, log?: ((message: string) => void) | undefined,
864
+ /** Writes to the audit log (`audit`). */
865
+ audit?: ((entry: {
866
+ action: string;
867
+ target: string;
868
+ doc: ID;
869
+ title: string;
870
+ changes?: {
871
+ field: string;
872
+ before?: unknown;
873
+ after?: unknown;
874
+ }[];
875
+ }) => Promise<void>) | undefined);
839
876
  /**
840
877
  * Whether the user's role allows an operation: `true`, `false`, or for their own account
841
878
  * (users without read or update on `users`) a constraint to it.
@@ -1072,6 +1109,8 @@ declare class EasyCMS<C extends Config = Config> {
1072
1109
  readonly cwd: string;
1073
1110
  /** Roles and their permissions (`auth.rbac`, Settings → Roles). */
1074
1111
  readonly roles: Roles;
1112
+ /** Who did what (`audit`, Settings → Audit log). */
1113
+ readonly audit: AuditLog;
1075
1114
  private readonly versions;
1076
1115
  private readonly webhooks;
1077
1116
  private readonly mailer;
@@ -1332,6 +1371,11 @@ declare class EasyCMS<C extends Config = Config> {
1332
1371
  private withDrafts;
1333
1372
  /** Sends webhook events for a change; `before` is the stored status before it, if any. */
1334
1373
  private emit;
1374
+ /**
1375
+ * Writes the audit log entry for a change (`audit`): publishing, unpublishing and restoring
1376
+ * are told apart from other saves.
1377
+ */
1378
+ private audited;
1335
1379
  /** Records a version after a save, when the collection or global keeps versions. */
1336
1380
  private saveVersion;
1337
1381
  private hookArgs;
@@ -1555,6 +1599,257 @@ type Field = CustomField | TextField | TextareaField | NumberField | BooleanFiel
1555
1599
  type FieldType = Field['type'];
1556
1600
  declare const FIELD_TYPES: readonly ["text", "textarea", "number", "boolean", "date", "email", "json", "select", "slug", "richText", "upload", "relationship", "array", "group", "blocks"];
1557
1601
 
1602
+ /** Audit log settings (`audit`). */
1603
+ interface AuditConfig {
1604
+ /** Days to keep entries; older ones are deleted. `0`: keep them all. Default 365. */
1605
+ readonly keep?: number;
1606
+ /** Keep the values before and after a change, not only which fields changed. Default `true`. */
1607
+ readonly values?: boolean;
1608
+ /** Failed logins within an hour that the dashboard warns about. Default 20. */
1609
+ readonly failedLogins?: number;
1610
+ }
1611
+ /** A field that changed: values before and after, unless they are not kept. */
1612
+ interface AuditChange {
1613
+ field: string;
1614
+ before?: unknown;
1615
+ after?: unknown;
1616
+ }
1617
+ /** One entry (`GET <api>/admin/audit`). */
1618
+ interface AuditEntry {
1619
+ id: ID;
1620
+ /** When, as an ISO date. */
1621
+ at: string;
1622
+ /** E.g. `create`, `update`, `publish`, `delete`, `login`, `login.failed`, `role.update`. */
1623
+ action: string;
1624
+ /** A collection slug, `global:<slug>`, or an area such as `auth`, `roles`, `backups`. */
1625
+ target: string | null;
1626
+ /** The document's id. */
1627
+ doc: string | null;
1628
+ /** The document's title then (kept after it is deleted). */
1629
+ title: string | null;
1630
+ actor: {
1631
+ id: string | null;
1632
+ email: string | null;
1633
+ /** `user` (admin or REST), `api-key`, `system` (code), `scheduler`. */
1634
+ via: string;
1635
+ ip: string | null;
1636
+ userAgent: string | null;
1637
+ };
1638
+ changes: AuditChange[] | null;
1639
+ detail: Record<string, unknown> | null;
1640
+ }
1641
+ /** `GET <api>/admin/audit`: a page of entries, newest first. */
1642
+ interface AuditPage {
1643
+ docs: AuditEntry[];
1644
+ page: number;
1645
+ totalPages: number;
1646
+ totalDocs: number;
1647
+ }
1648
+ interface AuditFilter {
1649
+ action?: string | null;
1650
+ target?: string | null;
1651
+ doc?: string | null;
1652
+ /** An email, or part of one. */
1653
+ actor?: string | null;
1654
+ /** ISO dates. */
1655
+ from?: string | null;
1656
+ to?: string | null;
1657
+ }
1658
+ /** `POST <api>/admin/audit/verify`. */
1659
+ interface AuditVerification {
1660
+ checked: number;
1661
+ /** Entries whose signature does not match: changed after they were written. */
1662
+ invalid: ID[];
1663
+ /** Missing ids between entries: deleted entries, or (rarely) inserts that failed. */
1664
+ gaps: number;
1665
+ at: string;
1666
+ }
1667
+ /**
1668
+ * The audit log (`audit` in the config): who did what, when, from where. Entries can't be
1669
+ * changed through Easy CMS; each is signed with the secret, so changes in the database show.
1670
+ * Available as `cms.audit`.
1671
+ */
1672
+ declare class AuditLog {
1673
+ private readonly cms;
1674
+ /** Entries that could not be written since the server started. */
1675
+ failures: number;
1676
+ constructor(cms: EasyCMS);
1677
+ get enabled(): boolean;
1678
+ private get settings();
1679
+ /**
1680
+ * Writes an entry. The user defaults to the request's. Never throws: a failure is logged and
1681
+ * counted, and the dashboard says so.
1682
+ */
1683
+ record(entry: {
1684
+ action: string;
1685
+ target?: string | null;
1686
+ doc?: ID | null;
1687
+ title?: string | null;
1688
+ user?: AuthUser | null;
1689
+ /** Who it was when there is no user, e.g. the email of a failed login. */
1690
+ email?: string | null;
1691
+ changes?: AuditChange[] | null;
1692
+ detail?: Record<string, unknown> | null;
1693
+ }): Promise<void>;
1694
+ /**
1695
+ * A change to a document or global: what changed between `before` and `after`. Internal
1696
+ * collections (sessions, versions…) are not logged.
1697
+ */
1698
+ content(config: CollectionConfig | GlobalConfig, global: boolean, action: string, after: Record<string, unknown> | undefined, before: Record<string, unknown> | undefined, user: AuthUser | null): Promise<void>;
1699
+ /** Fields whose stored value changed. Hidden ones never (the password only as "changed"). */
1700
+ diff(fields: readonly Field[], before: Record<string, unknown>, after: Record<string, unknown>): AuditChange[];
1701
+ list(filter: AuditFilter, page?: number): Promise<AuditPage>;
1702
+ /** The filtered entries as CSV (the newest 10,000). */
1703
+ csv(filter: AuditFilter): Promise<string>;
1704
+ /** Checks every entry's signature, and that no ids are missing. */
1705
+ verify(): Promise<AuditVerification>;
1706
+ /**
1707
+ * Daily upkeep from `runJobs`: deletes entries older than `keep` days, then verifies the log.
1708
+ * Runs when the last check is more than a day old.
1709
+ */
1710
+ upkeep(now?: Date): Promise<void>;
1711
+ /** What the dashboard warns about: many failed logins, entries not written, a failed check. */
1712
+ attention(now?: Date): Promise<({
1713
+ id: 'failed-logins';
1714
+ count: number;
1715
+ } | {
1716
+ id: 'audit-failures';
1717
+ count: number;
1718
+ } | {
1719
+ id: 'audit-tampered';
1720
+ invalid: number;
1721
+ at: string;
1722
+ })[]>;
1723
+ private sign;
1724
+ private lastVerification;
1725
+ private where;
1726
+ }
1727
+
1728
+ /** What the email functions in `auth.emails` receive. */
1729
+ interface PasswordEmailArgs {
1730
+ readonly user: AuthUser;
1731
+ /** The page that sets the password; absent for `passwordChanged`. */
1732
+ readonly url?: string;
1733
+ /** The admin language for the text: `en` or `th`. */
1734
+ readonly locale: string;
1735
+ /** When the link stops working. */
1736
+ readonly expiresAt?: Date;
1737
+ /**
1738
+ * Invitations when passwords are off (`auth.password: false`): the providers to sign in with,
1739
+ * e.g. `['Google']`; `url` is then the login page.
1740
+ */
1741
+ readonly sso?: readonly string[];
1742
+ }
1743
+ /** An email's content: a subject, and text and/or HTML. */
1744
+ interface PasswordEmail {
1745
+ readonly subject: string;
1746
+ readonly text: string;
1747
+ readonly html?: string;
1748
+ }
1749
+ type PasswordEmailFn = (args: PasswordEmailArgs) => PasswordEmail | Promise<PasswordEmail>;
1750
+ /** The default emails, in English and Thai. */
1751
+ declare const DEFAULT_PASSWORD_EMAILS: {
1752
+ resetPassword: PasswordEmailFn;
1753
+ invite: PasswordEmailFn;
1754
+ passwordChanged: PasswordEmailFn;
1755
+ };
1756
+
1757
+ /**
1758
+ * Signing in to the admin with an outside account (`auth.providers`): Google, Microsoft, GitHub
1759
+ * or any OpenID Connect provider. `@easy-cms/auth-oauth` makes these; Easy CMS runs the rest
1760
+ * (the redirect, matching users, sessions).
1761
+ */
1762
+ /** Values bound to one sign-in, checked when the browser comes back. */
1763
+ interface AuthProviderRequest {
1764
+ /** Where the provider sends the browser back: `<serverURL><api>/auth/<id>/callback`. */
1765
+ readonly redirectUri: string;
1766
+ readonly state: string;
1767
+ /** For PKCE: the provider gets its S256 challenge. */
1768
+ readonly codeVerifier: string;
1769
+ /** For OpenID Connect: must come back in the ID token. */
1770
+ readonly nonce: string;
1771
+ }
1772
+ /** Who signed in, as the provider says. */
1773
+ interface AuthProviderProfile {
1774
+ /** The provider's stable id for the account (`sub`). */
1775
+ readonly subject: string;
1776
+ readonly email: string | null;
1777
+ /** The provider vouches for the email. Only verified emails are matched to users. */
1778
+ readonly emailVerified: boolean;
1779
+ readonly name?: string | undefined;
1780
+ }
1781
+ interface AuthProvider {
1782
+ /** In URLs and stored identities, e.g. `google`: lowercase letters, digits and `-`. */
1783
+ readonly id: string;
1784
+ /** On the sign-in button, e.g. `Google`. */
1785
+ readonly name: string;
1786
+ /** The button's icon in the admin. Default `key`. */
1787
+ readonly icon?: 'google' | 'microsoft' | 'github' | 'key';
1788
+ /** The provider's page to send the browser to. */
1789
+ authorizationURL(request: AuthProviderRequest): Promise<URL>;
1790
+ /**
1791
+ * After the browser comes back to `redirectUri` (`url`, with its query): checks the response
1792
+ * and exchanges the code. Throws when anything does not check out.
1793
+ */
1794
+ callback(request: AuthProviderRequest & {
1795
+ readonly url: URL;
1796
+ }): Promise<AuthProviderProfile>;
1797
+ /** For Settings → SSO: what it connects to, never secrets. */
1798
+ describe?(): Readonly<Record<string, string>>;
1799
+ }
1800
+
1801
+ interface ConfigIssue {
1802
+ /** Where the problem is, e.g. `collections.posts.fields.slug.from`. */
1803
+ readonly path: string;
1804
+ readonly message: string;
1805
+ /** How to fix it. */
1806
+ readonly hint?: string;
1807
+ }
1808
+ declare class ConfigError extends Error {
1809
+ readonly issues: readonly ConfigIssue[];
1810
+ constructor(issues: readonly ConfigIssue[]);
1811
+ static format(issues: readonly ConfigIssue[]): string;
1812
+ }
1813
+ /** Base class for errors that map to an HTTP status in the REST API. */
1814
+ declare class EasyCMSError extends Error {
1815
+ readonly status: number;
1816
+ constructor(message: string, status: number);
1817
+ }
1818
+ interface FieldError {
1819
+ /** Dotted path, e.g. `links.0.url`. */
1820
+ readonly field: string;
1821
+ readonly message: string;
1822
+ }
1823
+ declare class ValidationError extends EasyCMSError {
1824
+ readonly errors: readonly FieldError[];
1825
+ constructor(collection: string, errors: readonly FieldError[]);
1826
+ }
1827
+ declare class NotFoundError extends EasyCMSError {
1828
+ constructor(collection: string, id: unknown);
1829
+ }
1830
+ /** A bad `where`, `sort` or unknown collection/global. */
1831
+ declare class QueryError extends EasyCMSError {
1832
+ constructor(message: string);
1833
+ }
1834
+ /** The database schema does not match the config (pending or missing migrations). */
1835
+ declare class SchemaError extends EasyCMSError {
1836
+ constructor(message: string);
1837
+ }
1838
+ /** Not logged in, or the session is invalid. */
1839
+ declare class UnauthorizedError extends EasyCMSError {
1840
+ constructor(message?: string);
1841
+ }
1842
+ /** Logged in, but not allowed to do this. */
1843
+ declare class ForbiddenError extends EasyCMSError {
1844
+ constructor(message?: string);
1845
+ }
1846
+ declare class TooManyRequestsError extends EasyCMSError {
1847
+ constructor(message: string);
1848
+ }
1849
+ declare class PayloadTooLargeError extends EasyCMSError {
1850
+ constructor(message: string);
1851
+ }
1852
+
1558
1853
  /** Built-in types a field type can be stored as. */
1559
1854
  declare const FIELD_TYPE_BASES: readonly ["text", "textarea", "email", "number", "boolean", "date", "json"];
1560
1855
  type FieldTypeBase = (typeof FIELD_TYPE_BASES)[number];
@@ -1920,6 +2215,24 @@ interface AuthConfig {
1920
2215
  readonly resetPasswordExpiration?: number;
1921
2216
  /** How long an invitation link works, in seconds. Default 7 days. */
1922
2217
  readonly inviteExpiration?: number;
2218
+ /**
2219
+ * Signing in to the admin with outside accounts, e.g. `[google({ clientId, clientSecret })]`
2220
+ * from `@easy-cms/auth-oauth`. Users are matched by their verified email.
2221
+ */
2222
+ readonly providers?: readonly AuthProvider[];
2223
+ /**
2224
+ * With `providers`: people from these email domains who sign in for the first time get an
2225
+ * account with `role` (default: as new users). Without it, only existing users can sign in.
2226
+ */
2227
+ readonly allowSignUp?: {
2228
+ readonly domains: readonly string[];
2229
+ readonly role?: string;
2230
+ };
2231
+ /**
2232
+ * `false`: only admins may sign in with a password (a way in when the provider is down);
2233
+ * everyone else signs in with `providers`. Default `true`.
2234
+ */
2235
+ readonly password?: boolean;
1923
2236
  /** Your own text for the password emails, e.g. in your brand's voice. Default: English or Thai. */
1924
2237
  readonly emails?: {
1925
2238
  readonly resetPassword?: PasswordEmailFn;
@@ -2107,10 +2420,15 @@ interface Config {
2107
2420
  * `every`, admins can still back up by hand.
2108
2421
  */
2109
2422
  readonly backups?: BackupsConfig;
2423
+ /**
2424
+ * The audit log (Settings → Audit log): who changed what and when, sign-ins, and admin
2425
+ * actions, kept for `keep` days. `true` for the defaults.
2426
+ */
2427
+ readonly audit?: boolean | AuditConfig;
2110
2428
  readonly plugins?: readonly Plugin[];
2111
2429
  }
2112
2430
  /** The config after plugins ran, validation passed and defaults were applied. */
2113
- interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'endpoints' | 'plugins'> {
2431
+ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes' | 'admin' | 'upload' | 'auth' | 'collections' | 'globals' | 'endpoints' | 'plugins' | 'audit'> {
2114
2432
  readonly cors: readonly string[] | '*';
2115
2433
  /** `null` without localization. */
2116
2434
  readonly localization: Required<LocalizationConfig> | null;
@@ -2118,6 +2436,8 @@ interface ResolvedConfig extends Omit<Config, 'cors' | 'localization' | 'routes'
2118
2436
  readonly admin: Required<AdminConfig>;
2119
2437
  readonly upload: Required<Omit<UploadConfig, 'storage' | 'fromURL'>> & Pick<UploadConfig, 'storage' | 'fromURL'>;
2120
2438
  readonly auth: Required<AuthConfig>;
2439
+ /** `false` without an audit log. */
2440
+ readonly audit: Required<AuditConfig> | false;
2121
2441
  readonly collections: readonly CollectionConfig[];
2122
2442
  readonly globals: readonly GlobalConfig[];
2123
2443
  readonly endpoints: readonly Endpoint[];
@@ -2567,9 +2887,13 @@ interface AdminSchema {
2567
2887
  backups: boolean;
2568
2888
  email: boolean;
2569
2889
  roles: boolean;
2890
+ sso: boolean;
2891
+ audit: boolean;
2570
2892
  };
2571
2893
  /** Roles from the admin are on (`auth.rbac`): documents have owners. */
2572
2894
  rbac: boolean;
2895
+ /** Outside accounts users can sign in with and link (`auth.providers`). */
2896
+ providers: SsoProviderRef[];
2573
2897
  /** Pages this user may open (`admin.pages`). */
2574
2898
  pages: AdminPageRef[];
2575
2899
  /** Dashboard panels this user may see (`admin.dashboard`). */
@@ -2607,6 +2931,22 @@ type AdminAttention =
2607
2931
  id: 'backups';
2608
2932
  failed: boolean;
2609
2933
  lastDone: string | null;
2934
+ }
2935
+ /** Many failed logins in the last hour (`audit.failedLogins`): someone may be guessing. */
2936
+ | {
2937
+ id: 'failed-logins';
2938
+ count: number;
2939
+ }
2940
+ /** Audit log entries that could not be written since the server started. */
2941
+ | {
2942
+ id: 'audit-failures';
2943
+ count: number;
2944
+ }
2945
+ /** The last check of the audit log found entries changed in the database. */
2946
+ | {
2947
+ id: 'audit-tampered';
2948
+ invalid: number;
2949
+ at: string;
2610
2950
  };
2611
2951
  /** What the dashboard shows admins (`GET <api>/admin/status`). */
2612
2952
  interface AdminStatus {
@@ -2668,4 +3008,4 @@ declare function validateConfig(config: Config): ConfigIssue[];
2668
3008
  /** The version of `@easy-cms/core`, set when the package is built. */
2669
3009
  declare const VERSION: string;
2670
3010
 
2671
- export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBackup, type AdminBackups, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminRole, type AdminRoleChange, type AdminRoleField, type AdminRoleTarget, type AdminRoleView, type AdminRoles, type AdminSchema, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BackupState, type BackupsConfig, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldRule, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, ROLE_OPERATIONS, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };
3011
+ export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminAttention, type AdminBackup, type AdminBackups, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminDeliveries, type AdminDelivery, type AdminEmail, type AdminEmailDelivery, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminPage, type AdminPageRef, type AdminRole, type AdminRoleChange, type AdminRoleField, type AdminRoleTarget, type AdminRoleView, type AdminRoles, type AdminSchema, type AdminSso, type AdminStatus, type AdminViewAccess, type AdminWebhookDelivery, type AdminWidgetRef, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, type AuditChange, type AuditConfig, type AuditEntry, AuditLog, type AuditPage, type AuditVerification, Auth, type AuthConfig, type AuthProvider, type AuthProviderProfile, type AuthProviderRequest, type AuthUser, BUILTIN_COLLECTIONS, type BackupState, type BackupsConfig, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type Block, type BlocksField, type BooleanField, type BuiltinApiKeysCollection, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CliCommand, type CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionListAdmin, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, type CustomField, type CustomFieldTypes, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_PASSWORD_EMAILS, DEFAULT_TOKEN_EXPIRATION, type DashboardWidget, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DeliveryKind, type DeliveryState, type DepthOptions, type DocumentOf, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailCheck, type EmailField, type EmailMessage, type EmailSetting, type Endpoint, type EndpointRequest, FIELD_TYPES, FIELD_TYPE_BASES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldRule, type FieldType, type FieldTypeBase, type FieldTypeDefinition, type FieldTypeValidateContext, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FilterOptions, type FilterOptionsArgs, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type LocalizationConfig, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, type PasswordEmail, type PasswordEmailArgs, type PasswordEmailFn, type PasswordLinkOptions, PayloadTooLargeError, type Plugin, type PluginInfo, type PluginTypes, type PreviewTarget, type PreviewURL, QueryError, ROLE_OPERATIONS, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoleOperation, type RolePermissions, Roles, type RoutesConfig, SESSION_COOKIE, SSO_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type SlugOf, type SsoOutcome, type SsoProviderRef, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, type TypedPlugin, UnauthorizedError, type UpdateInput, type UpdateOptions, type UploadConfig, type UploadField, type UploadFromURLConfig, type UserIdentity, VERSION, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleEmail, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, defineFieldType, definePlugin, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isHasMany, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, mimeAllowedBy, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword, writeBackupFile };