@easy-cms/core 0.18.0 → 0.20.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.
- package/dist/index.d.ts +69 -3
- package/dist/index.js +238 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -196,6 +196,40 @@ interface BundleHints {
|
|
|
196
196
|
readonly traceInclude: readonly string[];
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/** An email to send. Addresses may be `name <address>`. */
|
|
200
|
+
interface EmailMessage {
|
|
201
|
+
readonly to: string | readonly string[];
|
|
202
|
+
readonly cc?: string | readonly string[];
|
|
203
|
+
readonly bcc?: string | readonly string[];
|
|
204
|
+
/** Default: the adapter's `from`. */
|
|
205
|
+
readonly from?: string;
|
|
206
|
+
readonly replyTo?: string;
|
|
207
|
+
readonly subject: string;
|
|
208
|
+
readonly html?: string;
|
|
209
|
+
readonly text?: string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Sends email for Easy CMS and its plugins (form notifications, and more later). Set one in
|
|
213
|
+
* the config as `email`, e.g. `smtp()` from `@easy-cms/email-smtp`, or `consoleEmail()` in
|
|
214
|
+
* development. Throw to report a failure: the message is retried later.
|
|
215
|
+
*/
|
|
216
|
+
interface EmailAdapter {
|
|
217
|
+
/** The sender when a message has none, e.g. `Easy CMS <no-reply@example.com>`. */
|
|
218
|
+
readonly from?: string | undefined;
|
|
219
|
+
send(message: EmailMessage & {
|
|
220
|
+
readonly from: string;
|
|
221
|
+
}): Promise<void>;
|
|
222
|
+
}
|
|
223
|
+
/** Prints emails to the log instead of sending them: for development and tests. */
|
|
224
|
+
declare function consoleEmail(options?: {
|
|
225
|
+
from?: string;
|
|
226
|
+
log?: (text: string) => void;
|
|
227
|
+
}): EmailAdapter & {
|
|
228
|
+
readonly sent: (EmailMessage & {
|
|
229
|
+
from: string;
|
|
230
|
+
})[];
|
|
231
|
+
};
|
|
232
|
+
|
|
199
233
|
/** A label shown in the admin UI. Either one string or one string per admin locale. */
|
|
200
234
|
type Label = string | {
|
|
201
235
|
readonly [locale: string]: string;
|
|
@@ -779,6 +813,7 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
779
813
|
readonly cwd: string;
|
|
780
814
|
private readonly versions;
|
|
781
815
|
private readonly webhooks;
|
|
816
|
+
private readonly mailer;
|
|
782
817
|
constructor(config: ResolvedConfig, db: Database, logger?: Logger, storage?: StorageAdapter, cwd?: string);
|
|
783
818
|
find<S extends Slug<C>>(collection: S, options?: FindOptions): Promise<PaginatedDocs<Doc<C, S>>>;
|
|
784
819
|
findById<S extends Slug<C>>(collection: S, id: ID, options?: ReadOptions): Promise<Doc<C, S> | null>;
|
|
@@ -895,6 +930,16 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
895
930
|
* returns, which may otherwise stop them.
|
|
896
931
|
*/
|
|
897
932
|
flushWebhooks(): Promise<void>;
|
|
933
|
+
/**
|
|
934
|
+
* Sends an email with the config's `email` adapter, e.g. from a hook or plugin. Resolves once
|
|
935
|
+
* the email is queued; it is sent in the background and retried from the queue if sending
|
|
936
|
+
* fails. Without `email` in the config, the email is skipped with a warning.
|
|
937
|
+
*/
|
|
938
|
+
sendEmail(message: EmailMessage): Promise<void>;
|
|
939
|
+
/** Waits for emails being sent, e.g. before a serverless function returns. */
|
|
940
|
+
flushEmails(): Promise<void>;
|
|
941
|
+
/** Emails queued until sent, stored in `email-deliveries`. */
|
|
942
|
+
private emailQueue;
|
|
898
943
|
destroy(): Promise<void>;
|
|
899
944
|
private schedulerTimer;
|
|
900
945
|
private running;
|
|
@@ -911,6 +956,10 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
911
956
|
sent: number;
|
|
912
957
|
failed: number;
|
|
913
958
|
};
|
|
959
|
+
emails: {
|
|
960
|
+
sent: number;
|
|
961
|
+
failed: number;
|
|
962
|
+
};
|
|
914
963
|
}>;
|
|
915
964
|
/**
|
|
916
965
|
* Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
|
|
@@ -1124,6 +1173,13 @@ interface ContainerAdmin {
|
|
|
1124
1173
|
/** Panels in the edit page's side column, below publishing (components from admin modules). */
|
|
1125
1174
|
readonly sidebar?: readonly AdminComponent[];
|
|
1126
1175
|
}
|
|
1176
|
+
interface CollectionAdmin extends ContainerAdmin {
|
|
1177
|
+
/**
|
|
1178
|
+
* `settings` lists the collection under Settings in the admin menu (with Users and API keys)
|
|
1179
|
+
* instead of with the content, e.g. for redirects or forms settings.
|
|
1180
|
+
*/
|
|
1181
|
+
readonly group?: 'settings';
|
|
1182
|
+
}
|
|
1127
1183
|
interface CollectionConfig {
|
|
1128
1184
|
/** URL and table name. Lowercase letters, digits, `-` and `_`. */
|
|
1129
1185
|
readonly slug: string;
|
|
@@ -1156,8 +1212,8 @@ interface CollectionConfig {
|
|
|
1156
1212
|
readonly preview?: PreviewURL;
|
|
1157
1213
|
readonly access?: CollectionAccess;
|
|
1158
1214
|
readonly hooks?: CollectionHooks;
|
|
1159
|
-
/** Custom admin components. */
|
|
1160
|
-
readonly admin?:
|
|
1215
|
+
/** Custom admin components, and where the collection is in the menu. */
|
|
1216
|
+
readonly admin?: CollectionAdmin;
|
|
1161
1217
|
}
|
|
1162
1218
|
interface GlobalConfig {
|
|
1163
1219
|
readonly slug: string;
|
|
@@ -1263,6 +1319,8 @@ interface EndpointRequest {
|
|
|
1263
1319
|
readonly params: Readonly<Record<string, string>>;
|
|
1264
1320
|
/** The logged-in user (session cookie or Bearer token), or `null`. */
|
|
1265
1321
|
readonly user: AuthUser | null;
|
|
1322
|
+
/** The client's IP address, when the adapter knows it (e.g. to rate-limit a public form). */
|
|
1323
|
+
readonly ip: string | undefined;
|
|
1266
1324
|
/** The Local API. Pass `{ user, overrideAccess: false }` to apply the user's access rules. */
|
|
1267
1325
|
readonly cms: EasyCMS;
|
|
1268
1326
|
/** The request's JSON body, which must be an object (at most 1 MB). */
|
|
@@ -1336,6 +1394,12 @@ interface Config {
|
|
|
1336
1394
|
* under Settings. A key acts as its owner, limited to the collections and operations it lists.
|
|
1337
1395
|
*/
|
|
1338
1396
|
readonly apiKeys?: boolean;
|
|
1397
|
+
/**
|
|
1398
|
+
* Sends email for plugins such as the form builder, e.g. `smtp()` from
|
|
1399
|
+
* `@easy-cms/email-smtp`, or `consoleEmail()` in development. Emails are queued in the
|
|
1400
|
+
* database and retried until sent.
|
|
1401
|
+
*/
|
|
1402
|
+
readonly email?: EmailAdapter;
|
|
1339
1403
|
readonly plugins?: readonly Plugin[];
|
|
1340
1404
|
}
|
|
1341
1405
|
/** The config after plugins ran, validation passed and defaults were applied. */
|
|
@@ -1654,6 +1718,8 @@ interface AdminCollection {
|
|
|
1654
1718
|
fields: AdminField[];
|
|
1655
1719
|
/** Panels from admin modules in the edit page's side column. */
|
|
1656
1720
|
sidebar?: AdminComponentRef[];
|
|
1721
|
+
/** Listed under Settings in the menu (users, API keys, `admin.group: 'settings'`). */
|
|
1722
|
+
group?: 'settings';
|
|
1657
1723
|
permissions: {
|
|
1658
1724
|
read: boolean;
|
|
1659
1725
|
create: boolean;
|
|
@@ -1729,4 +1795,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
1729
1795
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
1730
1796
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
1731
1797
|
|
|
1732
|
-
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, 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 CollectionAccess, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailField, type Endpoint, type EndpointRequest, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, 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, PayloadTooLargeError, type Plugin, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, UnauthorizedError, type UpdateInput, type UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, WEBHOOK_EVENTS, type WebhookConfig, type WebhookEvent, type WebhookPayload, type Where, type WhereOperators, anyone, applyDefaults, applyPlugins, configSignature, consoleLogger, copyDatabase, createEasyCMS, createRestHandler, createRootEndpointHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
|
1798
|
+
export { ADMIN_ICONS, API_KEYS, API_KEY_GLOBAL_OPERATIONS, API_KEY_OPERATIONS, type Access, type AccessArgs, type AccessOptions, type AdminBrand, type AdminCollection, type AdminComponent, type AdminComponentRef, type AdminConfig, type AdminField, type AdminGlobal, type AdminIcon, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ApiKeyContext, type ApiKeyGlobalOperation, type ApiKeyOperation, type ApiKeyPermissions, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, 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 CollectionAccess, type CollectionAdmin, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type ContainerAdmin, type CopyProgress, type CopyResult, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DatabaseTransfer, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailAdapter, type EmailField, type EmailMessage, type Endpoint, type EndpointRequest, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldAdmin, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, 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, PayloadTooLargeError, type Plugin, type PreviewTarget, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, type ScheduledJob, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, UnauthorizedError, type UpdateInput, type UploadConfig, type UploadField, 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, fillMissing, findConfigFile, generateSlugs, generateTypes, hasRows, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, keyAllows, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveAdminModule, resolveConfig, rowFields, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
package/dist/index.js
CHANGED
|
@@ -316,12 +316,14 @@ var LOGIN_ATTEMPTS = "login-attempts";
|
|
|
316
316
|
var VERSIONS = "document-versions";
|
|
317
317
|
var SCHEDULED_JOBS = "scheduled-jobs";
|
|
318
318
|
var WEBHOOK_DELIVERIES = "webhook-deliveries";
|
|
319
|
+
var EMAIL_DELIVERIES = "email-deliveries";
|
|
319
320
|
var INTERNAL_COLLECTIONS = /* @__PURE__ */ new Set([
|
|
320
321
|
SESSIONS,
|
|
321
322
|
LOGIN_ATTEMPTS,
|
|
322
323
|
VERSIONS,
|
|
323
324
|
SCHEDULED_JOBS,
|
|
324
|
-
WEBHOOK_DELIVERIES
|
|
325
|
+
WEBHOOK_DELIVERIES,
|
|
326
|
+
EMAIL_DELIVERIES
|
|
325
327
|
]);
|
|
326
328
|
var DEFAULT_ROLES = ["admin", "editor"];
|
|
327
329
|
var adminOrSelf = ({ user }) => {
|
|
@@ -536,6 +538,19 @@ var webhookDeliveriesCollection = {
|
|
|
536
538
|
{ name: "error", type: "text" }
|
|
537
539
|
]
|
|
538
540
|
};
|
|
541
|
+
var emailDeliveriesCollection = {
|
|
542
|
+
slug: EMAIL_DELIVERIES,
|
|
543
|
+
access: { read: nobody, create: nobody, update: nobody, delete: nobody },
|
|
544
|
+
fields: [
|
|
545
|
+
// The message as JSON.
|
|
546
|
+
{ name: "message", type: "textarea", required: true },
|
|
547
|
+
{ name: "attempts", type: "number", required: true },
|
|
548
|
+
{ name: "nextAttemptAt", type: "text", required: true, index: true },
|
|
549
|
+
// pending | failed
|
|
550
|
+
{ name: "state", type: "text", required: true, index: true },
|
|
551
|
+
{ name: "error", type: "text" }
|
|
552
|
+
]
|
|
553
|
+
};
|
|
539
554
|
|
|
540
555
|
// src/auth/password.ts
|
|
541
556
|
import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
@@ -1290,6 +1305,140 @@ function normalizeSelect(field, raw, fail) {
|
|
|
1290
1305
|
return field.hasMany ? [...new Set(values)] : values[0];
|
|
1291
1306
|
}
|
|
1292
1307
|
|
|
1308
|
+
// src/email.ts
|
|
1309
|
+
function consoleEmail(options = {}) {
|
|
1310
|
+
const sent = [];
|
|
1311
|
+
const adapter = {
|
|
1312
|
+
from: options.from ?? "Easy CMS <no-reply@localhost>",
|
|
1313
|
+
sent,
|
|
1314
|
+
async send(message) {
|
|
1315
|
+
sent.push(message);
|
|
1316
|
+
const list = (value) => value === void 0 ? void 0 : typeof value === "string" ? value : value.join(", ");
|
|
1317
|
+
const lines = [
|
|
1318
|
+
`Email to ${list(message.to)}${message.cc ? `, cc ${list(message.cc)}` : ""}`,
|
|
1319
|
+
`From: ${message.from}${message.replyTo ? ` (reply to ${message.replyTo})` : ""}`,
|
|
1320
|
+
`Subject: ${message.subject}`,
|
|
1321
|
+
"",
|
|
1322
|
+
message.text ?? message.html ?? ""
|
|
1323
|
+
];
|
|
1324
|
+
(options.log ?? console.log)(lines.join("\n"));
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
return adapter;
|
|
1328
|
+
}
|
|
1329
|
+
var QUEUE_DELAYS = [6e4, 5 * 6e4, 30 * 6e4, 2 * 36e5, 6 * 36e5, 12 * 36e5];
|
|
1330
|
+
var CLAIM = 5 * 6e4;
|
|
1331
|
+
var later = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
1332
|
+
var Mailer = class {
|
|
1333
|
+
constructor(adapter, logger, queue) {
|
|
1334
|
+
this.adapter = adapter;
|
|
1335
|
+
this.logger = logger;
|
|
1336
|
+
this.queue = queue;
|
|
1337
|
+
}
|
|
1338
|
+
adapter;
|
|
1339
|
+
logger;
|
|
1340
|
+
queue;
|
|
1341
|
+
pending = /* @__PURE__ */ new Set();
|
|
1342
|
+
get configured() {
|
|
1343
|
+
return this.adapter !== void 0;
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Queues an email and sends it without waiting. Resolves once it is saved; `flush()` waits
|
|
1347
|
+
* for the sending too. Without an `email` adapter in the config, the email is skipped with a
|
|
1348
|
+
* warning.
|
|
1349
|
+
*/
|
|
1350
|
+
async send(message) {
|
|
1351
|
+
if (!this.adapter) {
|
|
1352
|
+
this.logger.warn(
|
|
1353
|
+
`Email "${message.subject}" not sent: set \`email\` in the config (e.g. consoleEmail() in development)`
|
|
1354
|
+
);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
const body = JSON.stringify(message);
|
|
1358
|
+
let id;
|
|
1359
|
+
try {
|
|
1360
|
+
id = await this.queue?.add({
|
|
1361
|
+
message: body,
|
|
1362
|
+
attempts: 0,
|
|
1363
|
+
nextAttemptAt: later(/* @__PURE__ */ new Date(), CLAIM),
|
|
1364
|
+
state: "pending",
|
|
1365
|
+
error: null
|
|
1366
|
+
});
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
this.logger.error(`Could not save email: ${error.message}`);
|
|
1369
|
+
}
|
|
1370
|
+
this.track(this.deliver(message, body, id));
|
|
1371
|
+
}
|
|
1372
|
+
/** Waits for emails being sent, e.g. before a serverless function returns. */
|
|
1373
|
+
async flush() {
|
|
1374
|
+
while (this.pending.size > 0) await Promise.allSettled([...this.pending]);
|
|
1375
|
+
}
|
|
1376
|
+
/** Sends queued emails that are due for another attempt. Runs with scheduled jobs. */
|
|
1377
|
+
async retry(now = /* @__PURE__ */ new Date()) {
|
|
1378
|
+
if (!this.queue || !this.adapter) return { sent: 0, failed: 0 };
|
|
1379
|
+
let sent = 0;
|
|
1380
|
+
let failed = 0;
|
|
1381
|
+
for (const { id, ...queued } of await this.queue.due(now, 50)) {
|
|
1382
|
+
const attempts = queued.attempts + 1;
|
|
1383
|
+
await this.queue.update(id, { ...queued, attempts, nextAttemptAt: later(now, CLAIM) });
|
|
1384
|
+
const error = await this.attempt(JSON.parse(queued.message));
|
|
1385
|
+
if (!error) {
|
|
1386
|
+
await this.queue.remove(id);
|
|
1387
|
+
sent++;
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
const delay = QUEUE_DELAYS[attempts - 1];
|
|
1391
|
+
if (delay === void 0) {
|
|
1392
|
+
failed++;
|
|
1393
|
+
this.logger.error(`Email gave up after ${attempts} attempts: ${error}`);
|
|
1394
|
+
}
|
|
1395
|
+
await this.queue.update(id, {
|
|
1396
|
+
...queued,
|
|
1397
|
+
attempts,
|
|
1398
|
+
nextAttemptAt: later(now, delay ?? 0),
|
|
1399
|
+
state: delay === void 0 ? "failed" : "pending",
|
|
1400
|
+
error
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
return { sent, failed };
|
|
1404
|
+
}
|
|
1405
|
+
track(delivery) {
|
|
1406
|
+
const tracked = delivery.catch((error) => this.logger.error(`Email queue failed: ${error.message}`)).finally(() => this.pending.delete(tracked));
|
|
1407
|
+
this.pending.add(tracked);
|
|
1408
|
+
}
|
|
1409
|
+
async deliver(message, body, id) {
|
|
1410
|
+
const error = await this.attempt(message);
|
|
1411
|
+
if (!error) {
|
|
1412
|
+
if (id !== void 0) await this.queue?.remove(id);
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (id === void 0 || !this.queue) {
|
|
1416
|
+
this.logger.error(`Email "${message.subject}" failed: ${error}`);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
this.logger.warn(`Email "${message.subject}" failed, retrying later: ${error}`);
|
|
1420
|
+
await this.queue.update(id, {
|
|
1421
|
+
message: body,
|
|
1422
|
+
attempts: 1,
|
|
1423
|
+
nextAttemptAt: later(/* @__PURE__ */ new Date(), QUEUE_DELAYS[0]),
|
|
1424
|
+
state: "pending",
|
|
1425
|
+
error
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
/** `undefined` when sent, else the error. */
|
|
1429
|
+
async attempt(message) {
|
|
1430
|
+
const adapter = this.adapter;
|
|
1431
|
+
const from = message.from ?? adapter.from;
|
|
1432
|
+
if (!from) return "no sender: set `from` on the message or the email adapter";
|
|
1433
|
+
try {
|
|
1434
|
+
await adapter.send({ ...message, from });
|
|
1435
|
+
return void 0;
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
return error.message || String(error);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1293
1442
|
// src/load-config.ts
|
|
1294
1443
|
import { existsSync } from "fs";
|
|
1295
1444
|
import { resolve } from "path";
|
|
@@ -1306,8 +1455,8 @@ var WEBHOOK_EVENTS = [
|
|
|
1306
1455
|
"draft"
|
|
1307
1456
|
];
|
|
1308
1457
|
var RETRY_DELAYS = [1e3, 5e3];
|
|
1309
|
-
var
|
|
1310
|
-
var
|
|
1458
|
+
var QUEUE_DELAYS2 = [6e4, 5 * 6e4, 30 * 6e4, 2 * 36e5, 6 * 36e5, 12 * 36e5];
|
|
1459
|
+
var CLAIM2 = 5 * 6e4;
|
|
1311
1460
|
var TIMEOUT = 1e4;
|
|
1312
1461
|
var Webhooks = class {
|
|
1313
1462
|
constructor(hooks, logger, queue, fetcher = (...args) => fetch(...args), retryDelays = RETRY_DELAYS) {
|
|
@@ -1353,19 +1502,19 @@ var Webhooks = class {
|
|
|
1353
1502
|
continue;
|
|
1354
1503
|
}
|
|
1355
1504
|
const attempts = queued.attempts + 1;
|
|
1356
|
-
await this.queue.update(id, { ...queued, attempts, nextAttemptAt:
|
|
1505
|
+
await this.queue.update(id, { ...queued, attempts, nextAttemptAt: later2(now, CLAIM2) });
|
|
1357
1506
|
const result = await this.attempt(hook, queued.event, queued.body, queued.delivery);
|
|
1358
1507
|
if (result.ok) {
|
|
1359
1508
|
await this.queue.remove(id);
|
|
1360
1509
|
sent++;
|
|
1361
1510
|
continue;
|
|
1362
1511
|
}
|
|
1363
|
-
const delay =
|
|
1512
|
+
const delay = QUEUE_DELAYS2[Math.max(0, attempts - this.retryDelays.length - 1)];
|
|
1364
1513
|
if (delay === void 0) failed++;
|
|
1365
1514
|
await this.queue.update(id, {
|
|
1366
1515
|
...queued,
|
|
1367
1516
|
attempts,
|
|
1368
|
-
nextAttemptAt:
|
|
1517
|
+
nextAttemptAt: later2(now, delay ?? 0),
|
|
1369
1518
|
state: delay === void 0 ? "failed" : "pending",
|
|
1370
1519
|
error: result.error
|
|
1371
1520
|
});
|
|
@@ -1392,7 +1541,7 @@ var Webhooks = class {
|
|
|
1392
1541
|
error
|
|
1393
1542
|
});
|
|
1394
1543
|
const { queue } = this;
|
|
1395
|
-
const saved = queue ? this.saving.then(() => queue.add(entry(0,
|
|
1544
|
+
const saved = queue ? this.saving.then(() => queue.add(entry(0, later2(/* @__PURE__ */ new Date(), CLAIM2), null))) : Promise.resolve(void 0);
|
|
1396
1545
|
this.saving = saved.catch(() => {
|
|
1397
1546
|
});
|
|
1398
1547
|
let queued;
|
|
@@ -1411,7 +1560,7 @@ var Webhooks = class {
|
|
|
1411
1560
|
if (delay === void 0) {
|
|
1412
1561
|
if (queued === void 0 || !this.queue)
|
|
1413
1562
|
return this.giveUp(hook, event, result.error, attempt + 1);
|
|
1414
|
-
const next =
|
|
1563
|
+
const next = later2(/* @__PURE__ */ new Date(), QUEUE_DELAYS2[0]);
|
|
1415
1564
|
await this.queue.update(queued, entry(attempt + 1, next, result.error));
|
|
1416
1565
|
this.logger.warn(
|
|
1417
1566
|
`Webhook ${hook.url} failed for ${event} (${result.error}); will retry later`
|
|
@@ -1456,7 +1605,7 @@ var Webhooks = class {
|
|
|
1456
1605
|
);
|
|
1457
1606
|
}
|
|
1458
1607
|
};
|
|
1459
|
-
var
|
|
1608
|
+
var later2 = (from, ms) => new Date(from.getTime() + ms).toISOString();
|
|
1460
1609
|
function matches(hook, payload) {
|
|
1461
1610
|
if (hook.events && !hook.events.includes(payload.event)) return false;
|
|
1462
1611
|
if (payload.collection !== void 0)
|
|
@@ -1727,6 +1876,13 @@ function validateLocalization(config, add) {
|
|
|
1727
1876
|
for (const [i, g] of (config.globals ?? []).entries()) walk(g?.fields, `globals[${i}].fields`);
|
|
1728
1877
|
}
|
|
1729
1878
|
function validateWebhooks(config, add) {
|
|
1879
|
+
const email = config.email;
|
|
1880
|
+
if (email !== void 0 && (typeof email !== "object" || email === null || typeof email.send !== "function"))
|
|
1881
|
+
add(
|
|
1882
|
+
"email",
|
|
1883
|
+
"must be an email adapter with a send() function",
|
|
1884
|
+
"e.g. email: consoleEmail() or smtp({ \u2026 }) from @easy-cms/email-smtp"
|
|
1885
|
+
);
|
|
1730
1886
|
const hooks = config.webhooks;
|
|
1731
1887
|
if (hooks === void 0) return;
|
|
1732
1888
|
if (!Array.isArray(hooks)) {
|
|
@@ -1838,6 +1994,9 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
1838
1994
|
} else {
|
|
1839
1995
|
const sidebar = admin.sidebar;
|
|
1840
1996
|
if (sidebar !== void 0) validateComponents(sidebar, `${path}.admin.sidebar`, add);
|
|
1997
|
+
const group = admin.group;
|
|
1998
|
+
if (group !== void 0 && group !== "settings")
|
|
1999
|
+
add(`${path}.admin.group`, `must be 'settings' (got ${JSON.stringify(group)})`);
|
|
1841
2000
|
}
|
|
1842
2001
|
}
|
|
1843
2002
|
const reserved = new Set(SYSTEM_FIELD_NAMES);
|
|
@@ -2160,7 +2319,8 @@ async function resolveConfig(input) {
|
|
|
2160
2319
|
...usesVersions(config) ? [versionsCollection] : [],
|
|
2161
2320
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
2162
2321
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
2163
|
-
...config.apiKeys ? [apiKeysCollection] : []
|
|
2322
|
+
...config.apiKeys ? [apiKeysCollection] : [],
|
|
2323
|
+
...config.email ? [emailDeliveriesCollection] : []
|
|
2164
2324
|
],
|
|
2165
2325
|
globals: config.globals ?? [],
|
|
2166
2326
|
endpoints: config.endpoints ?? []
|
|
@@ -2684,7 +2844,7 @@ async function createEasyCMS(config, options = {}) {
|
|
|
2684
2844
|
interactive: options.interactive ?? false
|
|
2685
2845
|
});
|
|
2686
2846
|
const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
|
|
2687
|
-
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0;
|
|
2847
|
+
const scheduling = [...resolved3.collections, ...resolved3.globals].some((c) => c.schedule) || (resolved3.webhooks?.length ?? 0) > 0 || resolved3.email !== void 0;
|
|
2688
2848
|
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
2689
2849
|
return cms;
|
|
2690
2850
|
}
|
|
@@ -2708,6 +2868,7 @@ var EasyCMS = class {
|
|
|
2708
2868
|
cwd;
|
|
2709
2869
|
versions;
|
|
2710
2870
|
webhooks;
|
|
2871
|
+
mailer;
|
|
2711
2872
|
constructor(config, db, logger = consoleLogger, storage = localStorage({ dir: config.upload.dir }), cwd = process.cwd()) {
|
|
2712
2873
|
this.config = config;
|
|
2713
2874
|
this.db = db;
|
|
@@ -2720,6 +2881,11 @@ var EasyCMS = class {
|
|
|
2720
2881
|
logger,
|
|
2721
2882
|
config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES) ? this.webhookQueue() : void 0
|
|
2722
2883
|
);
|
|
2884
|
+
this.mailer = new Mailer(
|
|
2885
|
+
config.email,
|
|
2886
|
+
logger,
|
|
2887
|
+
config.collections.some((c) => c.slug === EMAIL_DELIVERIES) ? this.emailQueue() : void 0
|
|
2888
|
+
);
|
|
2723
2889
|
this.auth = new Auth(this);
|
|
2724
2890
|
}
|
|
2725
2891
|
async find(collection2, options = {}) {
|
|
@@ -3367,10 +3533,57 @@ var EasyCMS = class {
|
|
|
3367
3533
|
flushWebhooks() {
|
|
3368
3534
|
return this.webhooks.flush();
|
|
3369
3535
|
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Sends an email with the config's `email` adapter, e.g. from a hook or plugin. Resolves once
|
|
3538
|
+
* the email is queued; it is sent in the background and retried from the queue if sending
|
|
3539
|
+
* fails. Without `email` in the config, the email is skipped with a warning.
|
|
3540
|
+
*/
|
|
3541
|
+
sendEmail(message) {
|
|
3542
|
+
return this.mailer.send(message);
|
|
3543
|
+
}
|
|
3544
|
+
/** Waits for emails being sent, e.g. before a serverless function returns. */
|
|
3545
|
+
flushEmails() {
|
|
3546
|
+
return this.mailer.flush();
|
|
3547
|
+
}
|
|
3548
|
+
/** Emails queued until sent, stored in `email-deliveries`. */
|
|
3549
|
+
emailQueue() {
|
|
3550
|
+
const collection2 = EMAIL_DELIVERIES;
|
|
3551
|
+
const toRow = (email, createdAt) => {
|
|
3552
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3553
|
+
return { ...email, createdAt: createdAt ?? now, updatedAt: now };
|
|
3554
|
+
};
|
|
3555
|
+
return {
|
|
3556
|
+
add: async (email) => (await this.db.create({ collection: collection2, data: toRow(email) })).id,
|
|
3557
|
+
update: async (id, email) => {
|
|
3558
|
+
const current = await this.db.findById({ collection: collection2, id });
|
|
3559
|
+
await this.db.update({
|
|
3560
|
+
collection: collection2,
|
|
3561
|
+
id,
|
|
3562
|
+
data: toRow(email, current?.createdAt)
|
|
3563
|
+
});
|
|
3564
|
+
},
|
|
3565
|
+
remove: async (id) => {
|
|
3566
|
+
await this.db.delete({ collection: collection2, id });
|
|
3567
|
+
},
|
|
3568
|
+
due: async (now, limit) => {
|
|
3569
|
+
const { docs } = await this.db.find({
|
|
3570
|
+
collection: collection2,
|
|
3571
|
+
where: {
|
|
3572
|
+
and: [{ state: { equals: "pending" } }, { nextAttemptAt: { lte: now.toISOString() } }]
|
|
3573
|
+
},
|
|
3574
|
+
sort: ["nextAttemptAt"],
|
|
3575
|
+
limit,
|
|
3576
|
+
page: 1
|
|
3577
|
+
});
|
|
3578
|
+
return docs;
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3370
3582
|
async destroy() {
|
|
3371
3583
|
if (this.schedulerTimer) clearInterval(this.schedulerTimer);
|
|
3372
3584
|
await this.running;
|
|
3373
3585
|
await this.webhooks.flush();
|
|
3586
|
+
await this.mailer.flush();
|
|
3374
3587
|
await this.db.destroy();
|
|
3375
3588
|
}
|
|
3376
3589
|
// --- Scheduled publishing --------------------------------------------------
|
|
@@ -3394,7 +3607,11 @@ var EasyCMS = class {
|
|
|
3394
3607
|
*/
|
|
3395
3608
|
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
3396
3609
|
const scheduled = await this.runScheduled(now);
|
|
3397
|
-
return {
|
|
3610
|
+
return {
|
|
3611
|
+
...scheduled,
|
|
3612
|
+
webhooks: await this.retryWebhooks(now),
|
|
3613
|
+
emails: await this.mailer.retry(now)
|
|
3614
|
+
};
|
|
3398
3615
|
}
|
|
3399
3616
|
/**
|
|
3400
3617
|
* Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
|
|
@@ -4130,6 +4347,7 @@ async function readAdminModule(cms, index) {
|
|
|
4130
4347
|
}
|
|
4131
4348
|
|
|
4132
4349
|
// src/rest/admin-schema.ts
|
|
4350
|
+
var SETTINGS = /* @__PURE__ */ new Set([USERS, API_KEYS]);
|
|
4133
4351
|
function componentRef(component) {
|
|
4134
4352
|
if (typeof component === "string") return { tag: component };
|
|
4135
4353
|
return component.props ? { tag: component.tag, props: JSON.parse(JSON.stringify(component.props)) } : { tag: component.tag };
|
|
@@ -4228,6 +4446,7 @@ async function collection(config, user, localized) {
|
|
|
4228
4446
|
if (config.useAsTitle) result.useAsTitle = config.useAsTitle;
|
|
4229
4447
|
if (config.icon) result.icon = config.icon;
|
|
4230
4448
|
if (config.admin?.sidebar?.length) result.sidebar = config.admin.sidebar.map(componentRef);
|
|
4449
|
+
if (config.admin?.group === "settings" || SETTINGS.has(config.slug)) result.group = "settings";
|
|
4231
4450
|
if (config.editIn === "drawer" && !result.drafts && !result.versions && !result.preview)
|
|
4232
4451
|
result.editIn = "drawer";
|
|
4233
4452
|
return result;
|
|
@@ -4431,7 +4650,7 @@ async function respond(cms, request, url, path, root, options, production) {
|
|
|
4431
4650
|
const ctx = { request, url, user, via: user ? via : null, token, headers };
|
|
4432
4651
|
const method = request.method.toUpperCase();
|
|
4433
4652
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
4434
|
-
const result = root ? await customEndpoint(cms, ctx, method, segments, true) : await customEndpoint(cms, ctx, method, segments, false) ?? await route(cms, ctx, method, segments, options);
|
|
4653
|
+
const result = root ? await customEndpoint(cms, ctx, method, segments, true, options) : await customEndpoint(cms, ctx, method, segments, false, options) ?? await route(cms, ctx, method, segments, options);
|
|
4435
4654
|
if (!result) throw new HttpError("Not found", 404);
|
|
4436
4655
|
if (result.body instanceof Response) return result.body;
|
|
4437
4656
|
return new Response(JSON.stringify(result.body), { status: result.status ?? 200, headers });
|
|
@@ -4469,7 +4688,7 @@ function withHeaders(response, extra) {
|
|
|
4469
4688
|
headers
|
|
4470
4689
|
});
|
|
4471
4690
|
}
|
|
4472
|
-
async function customEndpoint(cms, ctx, method, segments, root) {
|
|
4691
|
+
async function customEndpoint(cms, ctx, method, segments, root, options) {
|
|
4473
4692
|
let pathMatched = false;
|
|
4474
4693
|
const own = cms.config.endpoints.filter((e) => !!e.root === root);
|
|
4475
4694
|
const endpoints = [...own].sort((a, b) => fixedSegments(b) - fixedSegments(a));
|
|
@@ -4484,6 +4703,7 @@ async function customEndpoint(cms, ctx, method, segments, root) {
|
|
|
4484
4703
|
url: ctx.url,
|
|
4485
4704
|
params,
|
|
4486
4705
|
user: ctx.user,
|
|
4706
|
+
ip: options.getClientIp?.(ctx.request),
|
|
4487
4707
|
cms,
|
|
4488
4708
|
json: () => readJson(ctx.request)
|
|
4489
4709
|
});
|
|
@@ -4846,7 +5066,9 @@ function readCookie(request, name) {
|
|
|
4846
5066
|
function checkCsrf(cms, ctx) {
|
|
4847
5067
|
const origin = ctx.request.headers.get("origin");
|
|
4848
5068
|
const fetchSite = ctx.request.headers.get("sec-fetch-site");
|
|
4849
|
-
const
|
|
5069
|
+
const cookie2 = ctx.via === "cookie" && ctx.token !== void 0;
|
|
5070
|
+
const { cors } = cms.config;
|
|
5071
|
+
const trusted = origin !== null && (origin === ctx.url.origin || cms.config.auth.trustedOrigins.includes(origin) || !cookie2 && (cors === "*" || cors.includes(origin)));
|
|
4850
5072
|
if (origin !== null && !trusted) throw new ForbiddenError("CSRF check failed: untrusted origin");
|
|
4851
5073
|
if (origin === null && fetchSite === "cross-site")
|
|
4852
5074
|
throw new ForbiddenError("CSRF check failed: cross-site request");
|
|
@@ -5166,6 +5388,7 @@ export {
|
|
|
5166
5388
|
applyDefaults,
|
|
5167
5389
|
applyPlugins,
|
|
5168
5390
|
configSignature,
|
|
5391
|
+
consoleEmail,
|
|
5169
5392
|
consoleLogger,
|
|
5170
5393
|
copyDatabase,
|
|
5171
5394
|
createEasyCMS,
|