@easy-cms/core 0.19.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 +58 -1
- package/dist/index.js +233 -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
|
|
@@ -1270,6 +1319,8 @@ interface EndpointRequest {
|
|
|
1270
1319
|
readonly params: Readonly<Record<string, string>>;
|
|
1271
1320
|
/** The logged-in user (session cookie or Bearer token), or `null`. */
|
|
1272
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;
|
|
1273
1324
|
/** The Local API. Pass `{ user, overrideAccess: false }` to apply the user's access rules. */
|
|
1274
1325
|
readonly cms: EasyCMS;
|
|
1275
1326
|
/** The request's JSON body, which must be an object (at most 1 MB). */
|
|
@@ -1343,6 +1394,12 @@ interface Config {
|
|
|
1343
1394
|
* under Settings. A key acts as its owner, limited to the collections and operations it lists.
|
|
1344
1395
|
*/
|
|
1345
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;
|
|
1346
1403
|
readonly plugins?: readonly Plugin[];
|
|
1347
1404
|
}
|
|
1348
1405
|
/** The config after plugins ran, validation passed and defaults were applied. */
|
|
@@ -1738,4 +1795,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
1738
1795
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
1739
1796
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
1740
1797
|
|
|
1741
|
-
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 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)) {
|
|
@@ -2163,7 +2319,8 @@ async function resolveConfig(input) {
|
|
|
2163
2319
|
...usesVersions(config) ? [versionsCollection] : [],
|
|
2164
2320
|
...usesSchedule(config) ? [scheduledJobsCollection] : [],
|
|
2165
2321
|
...(config.webhooks?.length ?? 0) > 0 ? [webhookDeliveriesCollection] : [],
|
|
2166
|
-
...config.apiKeys ? [apiKeysCollection] : []
|
|
2322
|
+
...config.apiKeys ? [apiKeysCollection] : [],
|
|
2323
|
+
...config.email ? [emailDeliveriesCollection] : []
|
|
2167
2324
|
],
|
|
2168
2325
|
globals: config.globals ?? [],
|
|
2169
2326
|
endpoints: config.endpoints ?? []
|
|
@@ -2687,7 +2844,7 @@ async function createEasyCMS(config, options = {}) {
|
|
|
2687
2844
|
interactive: options.interactive ?? false
|
|
2688
2845
|
});
|
|
2689
2846
|
const cms = new EasyCMS(resolved3, db, logger, storage, cwd);
|
|
2690
|
-
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;
|
|
2691
2848
|
if (scheduling && options.scheduler !== false) cms.startScheduler();
|
|
2692
2849
|
return cms;
|
|
2693
2850
|
}
|
|
@@ -2711,6 +2868,7 @@ var EasyCMS = class {
|
|
|
2711
2868
|
cwd;
|
|
2712
2869
|
versions;
|
|
2713
2870
|
webhooks;
|
|
2871
|
+
mailer;
|
|
2714
2872
|
constructor(config, db, logger = consoleLogger, storage = localStorage({ dir: config.upload.dir }), cwd = process.cwd()) {
|
|
2715
2873
|
this.config = config;
|
|
2716
2874
|
this.db = db;
|
|
@@ -2723,6 +2881,11 @@ var EasyCMS = class {
|
|
|
2723
2881
|
logger,
|
|
2724
2882
|
config.collections.some((c) => c.slug === WEBHOOK_DELIVERIES) ? this.webhookQueue() : void 0
|
|
2725
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
|
+
);
|
|
2726
2889
|
this.auth = new Auth(this);
|
|
2727
2890
|
}
|
|
2728
2891
|
async find(collection2, options = {}) {
|
|
@@ -3370,10 +3533,57 @@ var EasyCMS = class {
|
|
|
3370
3533
|
flushWebhooks() {
|
|
3371
3534
|
return this.webhooks.flush();
|
|
3372
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
|
+
}
|
|
3373
3582
|
async destroy() {
|
|
3374
3583
|
if (this.schedulerTimer) clearInterval(this.schedulerTimer);
|
|
3375
3584
|
await this.running;
|
|
3376
3585
|
await this.webhooks.flush();
|
|
3586
|
+
await this.mailer.flush();
|
|
3377
3587
|
await this.db.destroy();
|
|
3378
3588
|
}
|
|
3379
3589
|
// --- Scheduled publishing --------------------------------------------------
|
|
@@ -3397,7 +3607,11 @@ var EasyCMS = class {
|
|
|
3397
3607
|
*/
|
|
3398
3608
|
async runJobs(now = /* @__PURE__ */ new Date()) {
|
|
3399
3609
|
const scheduled = await this.runScheduled(now);
|
|
3400
|
-
return {
|
|
3610
|
+
return {
|
|
3611
|
+
...scheduled,
|
|
3612
|
+
webhooks: await this.retryWebhooks(now),
|
|
3613
|
+
emails: await this.mailer.retry(now)
|
|
3614
|
+
};
|
|
3401
3615
|
}
|
|
3402
3616
|
/**
|
|
3403
3617
|
* Tries webhook deliveries that are due for another attempt. Deliveries are kept in the
|
|
@@ -4436,7 +4650,7 @@ async function respond(cms, request, url, path, root, options, production) {
|
|
|
4436
4650
|
const ctx = { request, url, user, via: user ? via : null, token, headers };
|
|
4437
4651
|
const method = request.method.toUpperCase();
|
|
4438
4652
|
if (method !== "GET" && method !== "HEAD") checkCsrf(cms, ctx);
|
|
4439
|
-
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);
|
|
4440
4654
|
if (!result) throw new HttpError("Not found", 404);
|
|
4441
4655
|
if (result.body instanceof Response) return result.body;
|
|
4442
4656
|
return new Response(JSON.stringify(result.body), { status: result.status ?? 200, headers });
|
|
@@ -4474,7 +4688,7 @@ function withHeaders(response, extra) {
|
|
|
4474
4688
|
headers
|
|
4475
4689
|
});
|
|
4476
4690
|
}
|
|
4477
|
-
async function customEndpoint(cms, ctx, method, segments, root) {
|
|
4691
|
+
async function customEndpoint(cms, ctx, method, segments, root, options) {
|
|
4478
4692
|
let pathMatched = false;
|
|
4479
4693
|
const own = cms.config.endpoints.filter((e) => !!e.root === root);
|
|
4480
4694
|
const endpoints = [...own].sort((a, b) => fixedSegments(b) - fixedSegments(a));
|
|
@@ -4489,6 +4703,7 @@ async function customEndpoint(cms, ctx, method, segments, root) {
|
|
|
4489
4703
|
url: ctx.url,
|
|
4490
4704
|
params,
|
|
4491
4705
|
user: ctx.user,
|
|
4706
|
+
ip: options.getClientIp?.(ctx.request),
|
|
4492
4707
|
cms,
|
|
4493
4708
|
json: () => readJson(ctx.request)
|
|
4494
4709
|
});
|
|
@@ -4851,7 +5066,9 @@ function readCookie(request, name) {
|
|
|
4851
5066
|
function checkCsrf(cms, ctx) {
|
|
4852
5067
|
const origin = ctx.request.headers.get("origin");
|
|
4853
5068
|
const fetchSite = ctx.request.headers.get("sec-fetch-site");
|
|
4854
|
-
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)));
|
|
4855
5072
|
if (origin !== null && !trusted) throw new ForbiddenError("CSRF check failed: untrusted origin");
|
|
4856
5073
|
if (origin === null && fetchSite === "cross-site")
|
|
4857
5074
|
throw new ForbiddenError("CSRF check failed: cross-site request");
|
|
@@ -5171,6 +5388,7 @@ export {
|
|
|
5171
5388
|
applyDefaults,
|
|
5172
5389
|
applyPlugins,
|
|
5173
5390
|
configSignature,
|
|
5391
|
+
consoleEmail,
|
|
5174
5392
|
consoleLogger,
|
|
5175
5393
|
copyDatabase,
|
|
5176
5394
|
createEasyCMS,
|