velocious 1.0.596 → 1.0.598
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/README.md +18 -2
- package/build/background-jobs/main.js +42 -32
- package/build/background-jobs/pooled-runner-broker-identity.js +22 -4
- package/build/background-jobs/store.js +383 -9
- package/build/background-jobs/types.js +1 -0
- package/build/cli/commands/db/tenants/migrations/pending.js +45 -0
- package/build/configuration-types.js +3 -0
- package/build/configuration.js +5 -2
- package/build/database/drivers/sqlite/base.js +3 -3
- package/build/database/tenants/migration-pending-inspector.js +77 -0
- package/build/environment-handlers/base.js +8 -0
- package/build/environment-handlers/node.js +14 -0
- package/build/mailer/backends/resend-smtp.js +121 -0
- package/build/mailer/base.js +52 -22
- package/build/mailer/delivery-operation-store.js +152 -0
- package/build/mailer/delivery-operation.js +205 -0
- package/build/mailer/delivery.js +9 -4
- package/build/mailer/index.js +5 -1
- package/build/mailer.js +6 -1
- package/build/src/background-jobs/main.d.ts +15 -10
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +42 -34
- package/build/src/background-jobs/pooled-runner-broker-identity.d.ts +10 -1
- package/build/src/background-jobs/pooled-runner-broker-identity.d.ts.map +1 -1
- package/build/src/background-jobs/pooled-runner-broker-identity.js +23 -5
- package/build/src/background-jobs/store.d.ts +154 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +350 -10
- package/build/src/background-jobs/types.d.ts +5 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +2 -1
- package/build/src/cli/commands/db/tenants/migrations/pending.d.ts +15 -0
- package/build/src/cli/commands/db/tenants/migrations/pending.d.ts.map +1 -0
- package/build/src/cli/commands/db/tenants/migrations/pending.js +40 -0
- package/build/src/configuration-types.d.ts +20 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +4 -1
- package/build/src/configuration.d.ts +2 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +6 -3
- package/build/src/database/drivers/sqlite/base.js +4 -4
- package/build/src/database/tenants/migration-pending-inspector.d.ts +38 -0
- package/build/src/database/tenants/migration-pending-inspector.d.ts.map +1 -0
- package/build/src/database/tenants/migration-pending-inspector.js +70 -0
- package/build/src/environment-handlers/base.d.ts +10 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +8 -1
- package/build/src/environment-handlers/node.d.ts +11 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +14 -1
- package/build/src/mailer/backends/resend-smtp.d.ts +43 -0
- package/build/src/mailer/backends/resend-smtp.d.ts.map +1 -0
- package/build/src/mailer/backends/resend-smtp.js +106 -0
- package/build/src/mailer/base.d.ts +9 -2
- package/build/src/mailer/base.d.ts.map +1 -1
- package/build/src/mailer/base.js +41 -19
- package/build/src/mailer/delivery-operation-store.d.ts +52 -0
- package/build/src/mailer/delivery-operation-store.d.ts.map +1 -0
- package/build/src/mailer/delivery-operation-store.js +130 -0
- package/build/src/mailer/delivery-operation.d.ts +73 -0
- package/build/src/mailer/delivery-operation.d.ts.map +1 -0
- package/build/src/mailer/delivery-operation.js +185 -0
- package/build/src/mailer/delivery.d.ts +4 -2
- package/build/src/mailer/delivery.d.ts.map +1 -1
- package/build/src/mailer/delivery.js +9 -5
- package/build/src/mailer/index.d.ts +24 -1
- package/build/src/mailer/index.d.ts.map +1 -1
- package/build/src/mailer/index.js +6 -2
- package/build/src/mailer.d.ts +11 -13
- package/build/src/mailer.d.ts.map +1 -1
- package/build/src/mailer.js +7 -2
- package/build/src/sync/local-mutation-log.d.ts +5 -5
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +34 -10
- package/build/src/sync/stable-json.d.ts +1 -8
- package/build/src/sync/stable-json.d.ts.map +1 -1
- package/build/src/sync/stable-json.js +2 -27
- package/build/src/sync/sync-envelope-replay-service.d.ts +3 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +12 -8
- package/build/src/testing/factory/node/definition-reload-policy.d.ts +97 -0
- package/build/src/testing/factory/node/definition-reload-policy.d.ts.map +1 -0
- package/build/src/testing/factory/node/definition-reload-policy.js +133 -0
- package/build/src/testing/factory/node/load-definitions.d.ts +4 -1
- package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -1
- package/build/src/testing/factory/node/load-definitions.js +27 -4
- package/build/src/testing/test-runner.js +7 -7
- package/build/src/utils/stable-json.d.ts +7 -0
- package/build/src/utils/stable-json.d.ts.map +1 -0
- package/build/src/utils/stable-json.js +25 -0
- package/build/sync/local-mutation-log.js +37 -6
- package/build/sync/stable-json.js +1 -28
- package/build/sync/sync-envelope-replay-service.js +11 -7
- package/build/testing/factory/node/definition-reload-policy.js +149 -0
- package/build/testing/factory/node/load-definitions.js +29 -5
- package/build/testing/test-runner.js +6 -6
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/stable-json.js +26 -0
- package/package.json +2 -1
- package/src/background-jobs/main.js +42 -32
- package/src/background-jobs/pooled-runner-broker-identity.js +22 -4
- package/src/background-jobs/store.js +383 -9
- package/src/background-jobs/types.js +1 -0
- package/src/cli/commands/db/tenants/migrations/pending.js +45 -0
- package/src/configuration-types.js +3 -0
- package/src/configuration.js +5 -2
- package/src/database/drivers/sqlite/base.js +3 -3
- package/src/database/tenants/migration-pending-inspector.js +77 -0
- package/src/environment-handlers/base.js +8 -0
- package/src/environment-handlers/node.js +14 -0
- package/src/mailer/backends/resend-smtp.js +121 -0
- package/src/mailer/base.js +52 -22
- package/src/mailer/delivery-operation-store.js +152 -0
- package/src/mailer/delivery-operation.js +205 -0
- package/src/mailer/delivery.js +9 -4
- package/src/mailer/index.js +5 -1
- package/src/mailer.js +6 -1
- package/src/sync/local-mutation-log.js +37 -6
- package/src/sync/stable-json.js +1 -28
- package/src/sync/sync-envelope-replay-service.js +11 -7
- package/src/testing/factory/node/definition-reload-policy.js +149 -0
- package/src/testing/factory/node/load-definitions.js +29 -5
- package/src/testing/test-runner.js +6 -6
- package/src/utils/stable-json.js +26 -0
|
@@ -32,6 +32,7 @@ import path from "path"
|
|
|
32
32
|
import {AsyncLocalStorage as NodeAsyncLocalStorage} from "node:async_hooks"
|
|
33
33
|
import {timingSafeEqual} from "node:crypto"
|
|
34
34
|
import requireContext from "require-context"
|
|
35
|
+
import AsyncTrackedMultiConnectionPool from "../database/pool/async-tracked-multi-connection.js"
|
|
35
36
|
import InitializerFromRequireContext from "../database/initializer-from-require-context.js"
|
|
36
37
|
import toImportSpecifier from "../utils/to-import-specifier.js"
|
|
37
38
|
import {validateTimeZone} from "../time-zone.js"
|
|
@@ -64,6 +65,19 @@ function pathWithinAllowedPrefixes(filePath, allowedPathPrefixes) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export default class VelociousEnvironmentHandlerNode extends Base{
|
|
68
|
+
/**
|
|
69
|
+
* Gives concurrent shared-transaction child jobs independent proxy sessions.
|
|
70
|
+
* A configured single-connection pool shares mutable transaction state between
|
|
71
|
+
* async jobs, while the broker requires one root-transaction lease per socket.
|
|
72
|
+
* @param {{configuredPoolType: typeof import("../database/pool/base.js").default, databaseIdentifier: string}} args - Configured pool and logical database identifier.
|
|
73
|
+
* @returns {typeof import("../database/pool/base.js").default} - Pool type for this context.
|
|
74
|
+
*/
|
|
75
|
+
resolveTestSharedTransactionPoolType({configuredPoolType, databaseIdentifier}) {
|
|
76
|
+
if (!sharedTransactionBrokerConfig(databaseIdentifier)) return configuredPoolType
|
|
77
|
+
|
|
78
|
+
return AsyncTrackedMultiConnectionPool
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
/**
|
|
68
82
|
* Creates a test-only child proxy when TestRunner supplied an active broker.
|
|
69
83
|
* @param {{DriverClass: typeof import("../database/drivers/base.js").default, config: import("../configuration-types.js").DatabaseConfigurationType, configuration: import("../configuration.js").default, databaseIdentifier: string}} args - Connection details.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import SmtpMailerBackend from "./smtp.js"
|
|
4
|
+
import VelociousError from "../../velocious-error.js"
|
|
5
|
+
import {deliveryOperationFromPayload} from "../delivery-operation.js"
|
|
6
|
+
|
|
7
|
+
const PROVIDER_KIND = "resend-smtp"
|
|
8
|
+
const RETENTION_MS = 24 * 60 * 60 * 1000
|
|
9
|
+
const IDEMPOTENCY_HEADER = "Resend-Idempotency-Key"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Checks whether a value contains an SMTP header control character.
|
|
13
|
+
* @param {string} value - Header value.
|
|
14
|
+
* @returns {boolean} - Whether a control character is present.
|
|
15
|
+
*/
|
|
16
|
+
function containsHeaderValueControlCharacter(value) {
|
|
17
|
+
for (const character of value) {
|
|
18
|
+
const codePoint = character.charCodeAt(0)
|
|
19
|
+
|
|
20
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Keeps the provider-owned operation header out of caller payloads.
|
|
28
|
+
* @param {import("../index.js").MailerDeliveryPayload} payload - Mail payload.
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
function rejectReservedHeaderOverride(payload) {
|
|
32
|
+
if (Object.keys(payload.headers || {}).some((name) => name.toLowerCase() === IDEMPOTENCY_HEADER.toLowerCase())) {
|
|
33
|
+
throw VelociousError.safe(`Reserved mail header ${IDEMPOTENCY_HEADER} is owned by ResendSmtpMailerBackend required delivery operations.`, {
|
|
34
|
+
code: "mail-delivery-idempotency-header-reserved"
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resend SMTP transport with Resend's documented 24-hour idempotency header.
|
|
41
|
+
*/
|
|
42
|
+
export default class ResendSmtpMailerBackend extends SmtpMailerBackend {
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the SMTP sender before it becomes part of the immutable digest.
|
|
45
|
+
* @param {object} args - Preparation input.
|
|
46
|
+
* @param {import("../index.js").MailerDeliveryPayload} args.payload - Rendered payload.
|
|
47
|
+
* @returns {import("../index.js").MailerDeliveryPayload} - Provider-ready payload.
|
|
48
|
+
*/
|
|
49
|
+
prepareDeliveryOperationPayload({payload}) {
|
|
50
|
+
const from = payload.from || this.defaultFrom
|
|
51
|
+
|
|
52
|
+
if (!from) {
|
|
53
|
+
throw VelociousError.safe("Required Resend mail delivery needs a from address.", {
|
|
54
|
+
code: "mail-delivery-from-missing"
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {...payload, from}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Advertises the provider-specific guarantee used by required operations.
|
|
63
|
+
* @returns {import("../index.js").MailerDeliveryIdempotencyCapability} - Capability.
|
|
64
|
+
*/
|
|
65
|
+
deliveryIdempotencyCapability() {
|
|
66
|
+
return {providerKind: PROVIDER_KIND, retentionMs: RETENTION_MS}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates Resend's documented length and SMTP header-value safety contract.
|
|
71
|
+
* @param {object} args - Validation input.
|
|
72
|
+
* @param {import("../index.js").MailerDeliveryOperationRequest | import("../index.js").MailerDeliveryOperation} args.deliveryOperation - Operation.
|
|
73
|
+
* @param {import("../index.js").MailerDeliveryPayload} args.payload - Rendered or persisted mail payload.
|
|
74
|
+
* @returns {void}
|
|
75
|
+
*/
|
|
76
|
+
validateDeliveryOperation({deliveryOperation, payload}) {
|
|
77
|
+
if (
|
|
78
|
+
typeof deliveryOperation.id !== "string" ||
|
|
79
|
+
deliveryOperation.id.length < 1 ||
|
|
80
|
+
deliveryOperation.id.length > 256 ||
|
|
81
|
+
containsHeaderValueControlCharacter(deliveryOperation.id)
|
|
82
|
+
) {
|
|
83
|
+
throw VelociousError.safe("Resend idempotency keys must contain between 1 and 256 characters without control characters.", {
|
|
84
|
+
code: "mail-delivery-idempotency-key-invalid"
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
rejectReservedHeaderOverride(payload)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Injects the framework-owned Resend operation header before generic SMTP serialization.
|
|
93
|
+
* @param {object} args - Delivery args.
|
|
94
|
+
* @param {import("../index.js").MailerDeliveryPayload} args.payload - Mail payload.
|
|
95
|
+
* @param {import("../../configuration.js").default} [args.configuration] - Active configuration.
|
|
96
|
+
* @returns {Promise<void>} - Resolves when accepted and shut down.
|
|
97
|
+
*/
|
|
98
|
+
async deliver({payload, configuration}) {
|
|
99
|
+
const headers = payload.headers || {}
|
|
100
|
+
|
|
101
|
+
if (payload.deliveryOperation) {
|
|
102
|
+
this.validateDeliveryOperation({deliveryOperation: payload.deliveryOperation, payload})
|
|
103
|
+
} else {
|
|
104
|
+
rejectReservedHeaderOverride(payload)
|
|
105
|
+
}
|
|
106
|
+
const operation = deliveryOperationFromPayload(payload)
|
|
107
|
+
|
|
108
|
+
if (!operation) {
|
|
109
|
+
await super.deliver({payload, configuration})
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await super.deliver({
|
|
114
|
+
configuration,
|
|
115
|
+
payload: {
|
|
116
|
+
...payload,
|
|
117
|
+
headers: {...headers, [IDEMPOTENCY_HEADER]: operation.id}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
package/build/mailer/base.js
CHANGED
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
import ejs from "ejs"
|
|
4
4
|
import {incorporate} from "incorporator"
|
|
5
5
|
import * as inflection from "inflection"
|
|
6
|
+
import BackgroundJobsClient from "../background-jobs/client.js"
|
|
6
7
|
import configurationResolver from "../configuration-resolver.js"
|
|
7
8
|
import restArgsError from "../utils/rest-args-error.js"
|
|
8
9
|
import MailerDelivery from "./delivery.js"
|
|
10
|
+
import MailerDeliveryOperationStore from "./delivery-operation-store.js"
|
|
11
|
+
import {
|
|
12
|
+
deliveryOperationFromPayload,
|
|
13
|
+
prepareRequiredDeliveryPayload,
|
|
14
|
+
requireDeliveryIdempotencyCapability
|
|
15
|
+
} from "./delivery-operation.js"
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Deliveries store.
|
|
@@ -66,16 +73,6 @@ function inferActionName(mailerClass, stack) {
|
|
|
66
73
|
return actionName
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
/**
|
|
70
|
-
* Runs is testing environment.
|
|
71
|
-
* @returns {Promise<boolean>} - Whether the current environment is test.
|
|
72
|
-
*/
|
|
73
|
-
async function isTestingEnvironment() {
|
|
74
|
-
const configuration = await configurationResolver()
|
|
75
|
-
|
|
76
|
-
return configuration.getEnvironment() === "test"
|
|
77
|
-
}
|
|
78
|
-
|
|
79
76
|
/**
|
|
80
77
|
* Base mailer with view rendering and delivery helpers.
|
|
81
78
|
*/
|
|
@@ -241,10 +238,13 @@ export class VelociousMailerBase {
|
|
|
241
238
|
/**
|
|
242
239
|
* Runs enqueue payload.
|
|
243
240
|
* @param {import("./index.js").MailerDeliveryPayload} payload - Mail delivery payload.
|
|
241
|
+
* @param {import("./index.js").MailerDeliveryLaterOptions} [options] - Delivery execution options.
|
|
244
242
|
* @returns {Promise<string | import("./index.js").MailerDeliveryPayload | null>} - Job id or payload in test mode.
|
|
245
243
|
*/
|
|
246
|
-
async _enqueuePayload(payload) {
|
|
247
|
-
|
|
244
|
+
async _enqueuePayload(payload, options) {
|
|
245
|
+
const configuration = await this._getConfiguration()
|
|
246
|
+
|
|
247
|
+
return await enqueuePayload(payload, {...options, configuration})
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -287,14 +287,22 @@ export function getDeliveryHandler() {
|
|
|
287
287
|
* @returns {Promise<import("./index.js").MailerDeliveryPayload | ReturnType<typeof JSON.parse>>} - Handler result.
|
|
288
288
|
*/
|
|
289
289
|
export async function deliverPayload(payload) {
|
|
290
|
-
|
|
290
|
+
const configuration = await configurationResolver()
|
|
291
|
+
const backend = configuration.getMailerBackend()
|
|
292
|
+
const deliveryOperation = deliveryOperationFromPayload(payload)
|
|
293
|
+
|
|
294
|
+
if (deliveryOperation) {
|
|
295
|
+
const capability = requireDeliveryIdempotencyCapability({backend, deliveryOperation, payload})
|
|
296
|
+
const operationStore = new MailerDeliveryOperationStore({configuration})
|
|
297
|
+
|
|
298
|
+
await operationStore.beginAttempt({capability, payload})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (configuration.getEnvironment() === "test") {
|
|
291
302
|
deliveriesStore.push(payload)
|
|
292
303
|
return payload
|
|
293
304
|
}
|
|
294
305
|
|
|
295
|
-
const configuration = await configurationResolver()
|
|
296
|
-
const backend = configuration.getMailerBackend()
|
|
297
|
-
|
|
298
306
|
if (backend?.deliver) {
|
|
299
307
|
return await backend.deliver({payload, configuration})
|
|
300
308
|
}
|
|
@@ -311,15 +319,37 @@ export async function deliverPayload(payload) {
|
|
|
311
319
|
/**
|
|
312
320
|
* Runs the enqueuePayload helper.
|
|
313
321
|
* @param {import("./index.js").MailerDeliveryPayload} payload - Mail delivery payload.
|
|
322
|
+
* @param {object} [options] - Enqueue options.
|
|
323
|
+
* @param {import("../configuration.js").default} [options.configuration] - Owning configuration.
|
|
324
|
+
* @param {import("./index.js").MailerDeliveryOperationRequest} [options.deliveryOperation] - Required provider-backed operation.
|
|
314
325
|
* @returns {Promise<string | import("./index.js").MailerDeliveryPayload | null>} - Job id or payload in test mode.
|
|
315
326
|
*/
|
|
316
|
-
export async function enqueuePayload(payload) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
327
|
+
export async function enqueuePayload(payload, {configuration: suppliedConfiguration, deliveryOperation} = {}) {
|
|
328
|
+
const configuration = suppliedConfiguration || await configurationResolver()
|
|
329
|
+
let persistedPayload = payload
|
|
330
|
+
|
|
331
|
+
if (deliveryOperation) {
|
|
332
|
+
const backend = configuration.getMailerBackend()
|
|
333
|
+
const operationPayload = typeof backend?.prepareDeliveryOperationPayload === "function"
|
|
334
|
+
? backend.prepareDeliveryOperationPayload({payload})
|
|
335
|
+
: payload
|
|
336
|
+
const capability = requireDeliveryIdempotencyCapability({backend, deliveryOperation, payload: operationPayload})
|
|
337
|
+
|
|
338
|
+
persistedPayload = prepareRequiredDeliveryPayload({capability, deliveryOperation, payload: operationPayload})
|
|
320
339
|
}
|
|
321
340
|
|
|
322
|
-
|
|
341
|
+
if (configuration.getEnvironment() === "test") {
|
|
342
|
+
deliveriesStore.push(persistedPayload)
|
|
343
|
+
return persistedPayload
|
|
344
|
+
}
|
|
323
345
|
|
|
324
|
-
|
|
346
|
+
const {default: mailDeliveryJob} = await import("../jobs/mail-delivery.js")
|
|
347
|
+
const client = new BackgroundJobsClient({configuration})
|
|
348
|
+
const jobOptions = mailDeliveryJob._withQueue(deliveryOperation ? {idempotencyKey: deliveryOperation.id} : undefined)
|
|
349
|
+
|
|
350
|
+
return await client.enqueue({
|
|
351
|
+
args: [persistedPayload],
|
|
352
|
+
jobName: mailDeliveryJob.jobName(),
|
|
353
|
+
options: jobOptions
|
|
354
|
+
})
|
|
325
355
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import BackgroundJobsStore from "../background-jobs/store.js"
|
|
4
|
+
import VelociousError from "../velocious-error.js"
|
|
5
|
+
import {
|
|
6
|
+
MAIL_DELIVERY_OPERATIONS_TABLE,
|
|
7
|
+
mailDeliveryOperationKey,
|
|
8
|
+
validateAttemptPayload
|
|
9
|
+
} from "./delivery-operation.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Durable first-attempt and retention guard for required mail operations.
|
|
13
|
+
*/
|
|
14
|
+
export default class MailerDeliveryOperationStore {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a durable mail-operation state guard.
|
|
17
|
+
* @param {object} args - Store input.
|
|
18
|
+
* @param {import("../configuration.js").default} args.configuration - Active configuration.
|
|
19
|
+
* @param {() => number} [args.clock] - Explicit clock dependency.
|
|
20
|
+
*/
|
|
21
|
+
constructor({configuration, clock = () => Date.now()}) {
|
|
22
|
+
this.backgroundJobsStore = new BackgroundJobsStore({configuration})
|
|
23
|
+
this.clock = clock
|
|
24
|
+
this.configuration = configuration
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Atomically starts the provider retention clock once and rejects expired attempts.
|
|
29
|
+
* @param {object} args - Attempt input.
|
|
30
|
+
* @param {import("./index.js").MailerDeliveryIdempotencyCapability} args.capability - Current backend capability.
|
|
31
|
+
* @param {import("./index.js").MailerDeliveryPayload} args.payload - Persisted payload.
|
|
32
|
+
* @returns {Promise<{expiresAtMs: number, firstAttemptStartedAtMs: number}>} - Attempt window.
|
|
33
|
+
*/
|
|
34
|
+
async beginAttempt({capability, payload}) {
|
|
35
|
+
const operation = validateAttemptPayload({capability, payload})
|
|
36
|
+
const nowMs = this.clock()
|
|
37
|
+
|
|
38
|
+
if (!Number.isSafeInteger(nowMs) || nowMs < 0) throw new Error("Mailer delivery operation clock must return a non-negative safe integer")
|
|
39
|
+
|
|
40
|
+
const databaseIdentifier = this.backgroundJobsStore.getDatabaseIdentifier()
|
|
41
|
+
|
|
42
|
+
return await this.configuration.ensureConnections({
|
|
43
|
+
databaseIdentifiers: [databaseIdentifier],
|
|
44
|
+
name: "Mailer delivery operation attempt"
|
|
45
|
+
}, async (dbs) => {
|
|
46
|
+
const db = dbs[databaseIdentifier]
|
|
47
|
+
|
|
48
|
+
if (db.insideTransaction()) {
|
|
49
|
+
throw VelociousError.safe("Required mail delivery cannot start inside an existing database transaction.", {
|
|
50
|
+
code: "mail-delivery-idempotency-transaction-active"
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await this.backgroundJobsStore.ensureSchema(db)
|
|
55
|
+
|
|
56
|
+
return await db.transaction(async () => {
|
|
57
|
+
const operationKey = mailDeliveryOperationKey(operation.id)
|
|
58
|
+
|
|
59
|
+
// A value-preserving structured update obtains the row's write lock on
|
|
60
|
+
// every driver before we inspect or initialize the one-shot timestamp.
|
|
61
|
+
await db.update({
|
|
62
|
+
tableName: MAIL_DELIVERY_OPERATIONS_TABLE,
|
|
63
|
+
data: {operation_key: operationKey},
|
|
64
|
+
conditions: {operation_key: operationKey}
|
|
65
|
+
})
|
|
66
|
+
let row = await this._operationRow(db, operation.id)
|
|
67
|
+
|
|
68
|
+
if (!row) throw new Error("Required mail delivery operation is not durably registered")
|
|
69
|
+
this._validateRow({operation, row})
|
|
70
|
+
|
|
71
|
+
let firstAttemptStartedAtMs = numberOrNull(row.first_attempt_started_at_ms)
|
|
72
|
+
|
|
73
|
+
if (firstAttemptStartedAtMs === null) {
|
|
74
|
+
await db.update({
|
|
75
|
+
tableName: MAIL_DELIVERY_OPERATIONS_TABLE,
|
|
76
|
+
data: {first_attempt_started_at_ms: nowMs},
|
|
77
|
+
conditions: {operation_key: operationKey}
|
|
78
|
+
})
|
|
79
|
+
row = await this._operationRow(db, operation.id)
|
|
80
|
+
firstAttemptStartedAtMs = row ? numberOrNull(row.first_attempt_started_at_ms) : null
|
|
81
|
+
|
|
82
|
+
if (firstAttemptStartedAtMs === null) throw new Error("Failed to durably start mail delivery provider retention")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const expiresAtMs = firstAttemptStartedAtMs + operation.providerRetentionMs
|
|
86
|
+
|
|
87
|
+
if (nowMs >= expiresAtMs) {
|
|
88
|
+
throw VelociousError.safe("The required mail delivery idempotency window expired before another attempt.", {
|
|
89
|
+
code: "mail-delivery-idempotency-expired",
|
|
90
|
+
details: {
|
|
91
|
+
expiresAtMs,
|
|
92
|
+
firstAttemptStartedAtMs,
|
|
93
|
+
operationId: operation.id,
|
|
94
|
+
providerKind: operation.providerKind
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {expiresAtMs, firstAttemptStartedAtMs}
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Loads one operation by its fixed-size digest key.
|
|
106
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
107
|
+
* @param {string} operationId - Operation id.
|
|
108
|
+
* @returns {Promise<Record<string, ReturnType<typeof JSON.parse>> | null>} - Row or null.
|
|
109
|
+
*/
|
|
110
|
+
async _operationRow(db, operationId) {
|
|
111
|
+
const rows = await db
|
|
112
|
+
.newQuery()
|
|
113
|
+
.from(MAIL_DELIVERY_OPERATIONS_TABLE)
|
|
114
|
+
.where({operation_key: mailDeliveryOperationKey(operationId)})
|
|
115
|
+
.limit(1)
|
|
116
|
+
.results()
|
|
117
|
+
|
|
118
|
+
return rows[0] ? /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (rows[0]) : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validates immutable durable fields without including mail content in failures.
|
|
123
|
+
* @param {object} args - Validation input.
|
|
124
|
+
* @param {import("./index.js").MailerDeliveryOperation} args.operation - Persisted payload operation.
|
|
125
|
+
* @param {Record<string, ReturnType<typeof JSON.parse>>} args.row - Durable operation row.
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
_validateRow({operation, row}) {
|
|
129
|
+
const matches = String(row.operation_id) === operation.id
|
|
130
|
+
&& String(row.payload_digest) === operation.payloadDigest
|
|
131
|
+
&& String(row.provider_kind) === operation.providerKind
|
|
132
|
+
&& numberOrNull(row.provider_retention_ms) === operation.providerRetentionMs
|
|
133
|
+
|
|
134
|
+
if (!matches) {
|
|
135
|
+
throw VelociousError.safe("The durable mail delivery operation does not match the persisted payload or provider.", {
|
|
136
|
+
code: "mail-delivery-idempotency-conflict"
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Normalizes a database integer.
|
|
144
|
+
* @param {ReturnType<typeof JSON.parse>} value - Database value.
|
|
145
|
+
* @returns {number | null} - Number or null.
|
|
146
|
+
*/
|
|
147
|
+
function numberOrNull(value) {
|
|
148
|
+
if (value === null || value === undefined || value === "") return null
|
|
149
|
+
const number = Number(value)
|
|
150
|
+
|
|
151
|
+
return Number.isNaN(number) ? null : number
|
|
152
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {createHash} from "crypto"
|
|
4
|
+
import VelociousError from "../velocious-error.js"
|
|
5
|
+
import stableJsonStringify from "../utils/stable-json.js"
|
|
6
|
+
|
|
7
|
+
export const MAIL_DELIVERY_JOB_NAME = "MailDeliveryJob"
|
|
8
|
+
export const MAIL_DELIVERY_OPERATIONS_TABLE = "mailer_delivery_operations"
|
|
9
|
+
const PAYLOAD_DIGEST_FORMAT = "velocious-mail-delivery-payload-v1"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reads and validates a backend's provider idempotency capability.
|
|
13
|
+
* @param {object} args - Capability input.
|
|
14
|
+
* @param {import("../configuration-types.js").MailerBackend | undefined} args.backend - Configured backend.
|
|
15
|
+
* @param {import("./index.js").MailerDeliveryOperationRequest | import("./index.js").MailerDeliveryOperation} args.deliveryOperation - Required operation.
|
|
16
|
+
* @param {import("./index.js").MailerDeliveryPayload} args.payload - Rendered or persisted payload.
|
|
17
|
+
* @returns {import("./index.js").MailerDeliveryIdempotencyCapability} - Capability.
|
|
18
|
+
*/
|
|
19
|
+
export function requireDeliveryIdempotencyCapability({backend, deliveryOperation, payload}) {
|
|
20
|
+
if (!backend || typeof backend.deliveryIdempotencyCapability !== "function") {
|
|
21
|
+
throw VelociousError.safe("The configured mailer backend does not support required provider idempotency.", {
|
|
22
|
+
code: "mail-delivery-idempotency-unsupported"
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const capability = backend.deliveryIdempotencyCapability()
|
|
27
|
+
|
|
28
|
+
if (!capability || typeof capability.providerKind !== "string" || capability.providerKind.length === 0) {
|
|
29
|
+
throw new Error("Mailer backend delivery idempotency capability requires a non-empty providerKind")
|
|
30
|
+
}
|
|
31
|
+
if (!Number.isSafeInteger(capability.retentionMs) || capability.retentionMs <= 0) {
|
|
32
|
+
throw new Error("Mailer backend delivery idempotency capability requires a positive safe-integer retentionMs")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof backend.validateDeliveryOperation === "function") {
|
|
36
|
+
backend.validateDeliveryOperation({deliveryOperation, payload})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return capability
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalizes one public required operation into immutable payload metadata.
|
|
44
|
+
* @param {object} args - Preparation input.
|
|
45
|
+
* @param {import("./index.js").MailerDeliveryIdempotencyCapability} args.capability - Backend capability.
|
|
46
|
+
* @param {import("./index.js").MailerDeliveryOperationRequest} args.deliveryOperation - Public operation request.
|
|
47
|
+
* @param {import("./index.js").MailerDeliveryPayload} args.payload - Rendered payload.
|
|
48
|
+
* @returns {import("./index.js").MailerDeliveryPayload} - Payload with immutable operation metadata.
|
|
49
|
+
*/
|
|
50
|
+
export function prepareRequiredDeliveryPayload({capability, deliveryOperation, payload}) {
|
|
51
|
+
validateDeliveryOperationRequest(deliveryOperation)
|
|
52
|
+
const payloadDigest = mailDeliveryPayloadDigest({operationId: deliveryOperation.id, payload})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...payload,
|
|
56
|
+
deliveryOperation: {
|
|
57
|
+
id: deliveryOperation.id,
|
|
58
|
+
idempotency: "required",
|
|
59
|
+
payloadDigest,
|
|
60
|
+
providerKind: capability.providerKind,
|
|
61
|
+
providerRetentionMs: capability.retentionMs
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds the versioned digest for every recipient-visible/provider-relevant payload field.
|
|
68
|
+
* @param {object} args - Digest input.
|
|
69
|
+
* @param {string} args.operationId - Stable operation id.
|
|
70
|
+
* @param {import("./index.js").MailerDeliveryPayload} args.payload - Rendered payload.
|
|
71
|
+
* @returns {string} - Versioned SHA-256 digest.
|
|
72
|
+
*/
|
|
73
|
+
export function mailDeliveryPayloadDigest({operationId, payload}) {
|
|
74
|
+
const canonicalPayload = {
|
|
75
|
+
action: payload.action,
|
|
76
|
+
bcc: payload.bcc ?? null,
|
|
77
|
+
cc: payload.cc ?? null,
|
|
78
|
+
format: PAYLOAD_DIGEST_FORMAT,
|
|
79
|
+
from: payload.from ?? null,
|
|
80
|
+
headers: canonicalHeaders(payload.headers),
|
|
81
|
+
html: payload.html,
|
|
82
|
+
mailer: payload.mailer,
|
|
83
|
+
operationId,
|
|
84
|
+
replyTo: payload.replyTo ?? null,
|
|
85
|
+
subject: payload.subject,
|
|
86
|
+
to: payload.to
|
|
87
|
+
}
|
|
88
|
+
const digest = createHash("sha256").update(stableJsonStringify(canonicalPayload)).digest("hex")
|
|
89
|
+
|
|
90
|
+
return `sha256:v1:${digest}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extracts validated persisted operation metadata from a payload.
|
|
95
|
+
* @param {import("./index.js").MailerDeliveryPayload} payload - Mail payload.
|
|
96
|
+
* @returns {import("./index.js").MailerDeliveryOperation | null} - Operation or null.
|
|
97
|
+
*/
|
|
98
|
+
export function deliveryOperationFromPayload(payload) {
|
|
99
|
+
const operation = payload.deliveryOperation
|
|
100
|
+
|
|
101
|
+
if (!operation) return null
|
|
102
|
+
if (operation.idempotency !== "required") throw new Error("Persisted mail delivery operation idempotency must be required")
|
|
103
|
+
if (typeof operation.id !== "string" || operation.id.length === 0) throw new Error("Persisted mail delivery operation requires an id")
|
|
104
|
+
if (typeof operation.payloadDigest !== "string" || !operation.payloadDigest.startsWith("sha256:v1:")) throw new Error("Persisted mail delivery operation requires a versioned payload digest")
|
|
105
|
+
if (typeof operation.providerKind !== "string" || operation.providerKind.length === 0) throw new Error("Persisted mail delivery operation requires a provider kind")
|
|
106
|
+
if (!Number.isSafeInteger(operation.providerRetentionMs) || operation.providerRetentionMs <= 0) throw new Error("Persisted mail delivery operation requires a positive retention")
|
|
107
|
+
|
|
108
|
+
return operation
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extracts a built-in mail operation from generic job arguments.
|
|
113
|
+
* @param {string} jobName - Job class name.
|
|
114
|
+
* @param {Array<ReturnType<typeof JSON.parse>>} args - Job arguments.
|
|
115
|
+
* @returns {{operation: import("./index.js").MailerDeliveryOperation, payload: import("./index.js").MailerDeliveryPayload} | null} - Mail operation input.
|
|
116
|
+
*/
|
|
117
|
+
export function mailDeliveryOperationForJob(jobName, args) {
|
|
118
|
+
if (jobName !== MAIL_DELIVERY_JOB_NAME || !args[0] || typeof args[0] !== "object" || Array.isArray(args[0])) return null
|
|
119
|
+
|
|
120
|
+
const payload = /** @type {import("./index.js").MailerDeliveryPayload} */ (args[0])
|
|
121
|
+
const operation = deliveryOperationFromPayload(payload)
|
|
122
|
+
|
|
123
|
+
return operation ? {operation, payload} : null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fixed-size primary key for a potentially long operation id.
|
|
128
|
+
* @param {string} operationId - Operation id.
|
|
129
|
+
* @returns {string} - SHA-256 operation key.
|
|
130
|
+
*/
|
|
131
|
+
export function mailDeliveryOperationKey(operationId) {
|
|
132
|
+
return createHash("sha256").update(`velocious-mail-delivery-operation:${operationId}`).digest("hex")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validates provider compatibility and payload integrity before an attempt.
|
|
137
|
+
* @param {object} args - Validation input.
|
|
138
|
+
* @param {import("./index.js").MailerDeliveryIdempotencyCapability} args.capability - Current backend capability.
|
|
139
|
+
* @param {import("./index.js").MailerDeliveryPayload} args.payload - Persisted payload.
|
|
140
|
+
* @returns {import("./index.js").MailerDeliveryOperation} - Persisted operation.
|
|
141
|
+
*/
|
|
142
|
+
export function validateAttemptPayload({capability, payload}) {
|
|
143
|
+
const operation = deliveryOperationFromPayload(payload)
|
|
144
|
+
|
|
145
|
+
if (!operation) throw new Error("Expected a persisted mail delivery operation")
|
|
146
|
+
if (operation.providerKind !== capability.providerKind || operation.providerRetentionMs !== capability.retentionMs) {
|
|
147
|
+
throw VelociousError.safe("The configured mailer backend no longer matches the required delivery operation provider.", {
|
|
148
|
+
code: "mail-delivery-idempotency-provider-mismatch"
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const currentDigest = mailDeliveryPayloadDigest({operationId: operation.id, payload})
|
|
153
|
+
|
|
154
|
+
if (currentDigest !== operation.payloadDigest) {
|
|
155
|
+
throw VelociousError.safe("The persisted mail delivery payload digest does not match its required operation.", {
|
|
156
|
+
code: "mail-delivery-idempotency-payload-mismatch"
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return operation
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validates the public operation shape without accepting future semantics silently.
|
|
165
|
+
* @param {import("./index.js").MailerDeliveryOperationRequest} deliveryOperation - Public operation.
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
function validateDeliveryOperationRequest(deliveryOperation) {
|
|
169
|
+
if (!deliveryOperation || typeof deliveryOperation !== "object" || Array.isArray(deliveryOperation)) {
|
|
170
|
+
throw VelociousError.safe("deliveryOperation must be an object.", {code: "mail-delivery-operation-invalid"})
|
|
171
|
+
}
|
|
172
|
+
if (typeof deliveryOperation.id !== "string" || deliveryOperation.id.length === 0) {
|
|
173
|
+
throw VelociousError.safe("deliveryOperation.id must be a non-empty string.", {code: "mail-delivery-operation-invalid"})
|
|
174
|
+
}
|
|
175
|
+
if (deliveryOperation.idempotency !== "required") {
|
|
176
|
+
throw VelociousError.safe('deliveryOperation.idempotency must be "required".', {code: "mail-delivery-operation-invalid"})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const keys = Object.keys(deliveryOperation)
|
|
180
|
+
|
|
181
|
+
if (keys.some((key) => key !== "id" && key !== "idempotency")) {
|
|
182
|
+
throw VelociousError.safe("deliveryOperation contains unsupported fields.", {code: "mail-delivery-operation-invalid"})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Canonicalizes case-insensitive custom headers without exposing values.
|
|
188
|
+
* @param {Record<string, string> | undefined} headers - Custom headers.
|
|
189
|
+
* @returns {Array<[string, string]>} - Sorted header pairs.
|
|
190
|
+
*/
|
|
191
|
+
function canonicalHeaders(headers) {
|
|
192
|
+
if (!headers) return []
|
|
193
|
+
|
|
194
|
+
const pairs = Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value])
|
|
195
|
+
const names = new Set()
|
|
196
|
+
|
|
197
|
+
for (const [name] of pairs) {
|
|
198
|
+
if (names.has(name)) {
|
|
199
|
+
throw VelociousError.safe("Mail headers contain duplicate case-insensitive names.", {code: "mail-delivery-headers-invalid"})
|
|
200
|
+
}
|
|
201
|
+
names.add(name)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return /** @type {Array<[string, string]>} */ (pairs.sort(([left], [right]) => left.localeCompare(right)))
|
|
205
|
+
}
|
package/build/mailer/delivery.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import restArgsError from "../utils/rest-args-error.js"
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Represents a prepared mail delivery.
|
|
5
7
|
*/
|
|
@@ -52,19 +54,22 @@ export default class MailerDelivery {
|
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
56
|
* Runs deliver later.
|
|
57
|
+
* @param {import("./index.js").MailerDeliveryLaterOptions} [options] - Delivery execution options.
|
|
55
58
|
* @returns {Promise<string | import("./index.js").MailerDeliveryPayload | null>} - Job id or payload in test mode.
|
|
56
59
|
*/
|
|
57
|
-
async deliverLater() {
|
|
60
|
+
async deliverLater({deliveryOperation, ...restArgs} = {}) {
|
|
61
|
+
restArgsError(restArgs)
|
|
58
62
|
const payload = await this.buildPayload()
|
|
59
63
|
|
|
60
|
-
return await this.mailer._enqueuePayload(payload)
|
|
64
|
+
return await this.mailer._enqueuePayload(payload, {deliveryOperation})
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/**
|
|
64
68
|
* Runs deliver laver.
|
|
69
|
+
* @param {import("./index.js").MailerDeliveryLaterOptions} [options] - Delivery execution options.
|
|
65
70
|
* @returns {Promise<string | import("./index.js").MailerDeliveryPayload | null>} - Job id or payload in test mode.
|
|
66
71
|
*/
|
|
67
|
-
async deliverLaver() {
|
|
68
|
-
return await this.deliverLater()
|
|
72
|
+
async deliverLaver(options) {
|
|
73
|
+
return await this.deliverLater(options)
|
|
69
74
|
}
|
|
70
75
|
}
|
package/build/mailer/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
/** @typedef {{
|
|
3
|
+
/** @typedef {{id: string, idempotency: "required"}} MailerDeliveryOperationRequest */
|
|
4
|
+
/** @typedef {{id: string, idempotency: "required", payloadDigest: string, providerKind: string, providerRetentionMs: number}} MailerDeliveryOperation */
|
|
5
|
+
/** @typedef {{providerKind: string, retentionMs: number}} MailerDeliveryIdempotencyCapability */
|
|
6
|
+
/** @typedef {{deliveryOperation?: MailerDeliveryOperationRequest}} MailerDeliveryLaterOptions */
|
|
7
|
+
/** @typedef {{to: ReturnType<typeof JSON.parse>, subject: string, from?: ReturnType<typeof JSON.parse>, cc?: ReturnType<typeof JSON.parse>, bcc?: ReturnType<typeof JSON.parse>, replyTo?: ReturnType<typeof JSON.parse>, headers?: Record<string, string>, html: string, mailer: string, action: string, deliveryOperation?: MailerDeliveryOperation}} MailerDeliveryPayload */
|
|
4
8
|
|
|
5
9
|
import {
|
|
6
10
|
clearDeliveries,
|