better-ship 0.10.0 → 0.12.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/{application-DYfcAy-Q.js → application-CIxo_BpK.js} +32 -3
- package/dist/application-CIxo_BpK.js.map +1 -0
- package/dist/application.d.ts +3 -3
- package/dist/application.js +2 -2
- package/dist/better-auth/apple-client-secret.d.ts +1 -1
- package/dist/better-auth/apple-client-secret.js +1 -1
- package/dist/better-auth/auth-client.js +1 -1
- package/dist/better-auth/rate-limit.d.ts +1 -1
- package/dist/better-auth/rate-limit.js +1 -1
- package/dist/better-auth.js +1 -1
- package/dist/cloudflare/auth-secondary-storage.d.ts +1 -1
- package/dist/cloudflare/auth-secondary-storage.js +1 -1
- package/dist/cloudflare/queue.d.ts +1 -1
- package/dist/cloudflare/queue.js +2 -2
- package/dist/cloudflare/scheduled.d.ts +1 -1
- package/dist/cloudflare/scheduled.js +1 -1
- package/dist/cloudflare.d.ts +3 -3
- package/dist/cloudflare.js +3 -3
- package/dist/{core-DC_4pOrI.js → core-DG_cC8YT.js} +110 -2
- package/dist/{core-DC_4pOrI.js.map → core-DG_cC8YT.js.map} +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/http.d.ts +19 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +35 -0
- package/dist/http.js.map +1 -0
- package/dist/{index-6XMPJu7u.d.ts → index-Bl0K1RQW.d.ts} +90 -2
- package/dist/index-Bl0K1RQW.d.ts.map +1 -0
- package/dist/{index-_Cihfq1J.d.ts → index-DwWxIdQP.d.ts} +137 -55
- package/dist/index-DwWxIdQP.d.ts.map +1 -0
- package/dist/postgres.d.ts +3 -3
- package/dist/postgres.js +3 -3
- package/dist/resend.d.ts +73 -0
- package/dist/resend.d.ts.map +1 -0
- package/dist/resend.js +284 -0
- package/dist/resend.js.map +1 -0
- package/dist/tanstack/server.d.ts +8 -0
- package/dist/tanstack/server.d.ts.map +1 -0
- package/dist/tanstack/server.js +16 -0
- package/dist/tanstack/server.js.map +1 -0
- package/dist/tanstack.d.ts +1 -8
- package/dist/tanstack.d.ts.map +1 -1
- package/dist/tanstack.js +15 -21
- package/dist/tanstack.js.map +1 -1
- package/dist/{unit-of-work-context-BFCmU92Z.js → unit-of-work-context-BPE1In2f.js} +2 -2
- package/dist/{unit-of-work-context-BFCmU92Z.js.map → unit-of-work-context-BPE1In2f.js.map} +1 -1
- package/dist/{unit-of-work-context-BWTJfYPW.d.ts → unit-of-work-context-D5ytJIa-.d.ts} +3 -3
- package/dist/{unit-of-work-context-BWTJfYPW.d.ts.map → unit-of-work-context-D5ytJIa-.d.ts.map} +1 -1
- package/dist/webhook-verification-BQ9qZnVK.js +18 -0
- package/dist/webhook-verification-BQ9qZnVK.js.map +1 -0
- package/dist/webhook-verification-sZtwTI5n.d.ts +29 -0
- package/dist/webhook-verification-sZtwTI5n.d.ts.map +1 -0
- package/package.json +10 -1
- package/src/application/email-service.ts +57 -0
- package/src/application/index.ts +16 -0
- package/src/application/record-email-event.ts +53 -0
- package/src/core/email-address.ts +7 -0
- package/src/core/email-deliverability.ts +31 -0
- package/src/core/email-idempotency-key.ts +34 -0
- package/src/core/email-service.error.ts +51 -0
- package/src/core/index.ts +18 -0
- package/src/core/record-email-event.ts +23 -0
- package/src/infrastructure/http/index.ts +6 -0
- package/src/infrastructure/http/webhook-handler.ts +35 -0
- package/src/infrastructure/http/webhook-verification.ts +27 -0
- package/src/infrastructure/resend/email-event.ts +142 -0
- package/src/infrastructure/resend/index.ts +3 -0
- package/src/infrastructure/resend/resend-email-service.ts +125 -0
- package/src/infrastructure/resend/resend-webhook.ts +70 -0
- package/src/infrastructure/tanstack/auth.middleware.ts +9 -3
- package/src/infrastructure/tanstack/error.middleware.ts +1 -1
- package/src/infrastructure/tanstack/index.ts +0 -1
- package/src/infrastructure/tanstack/server.ts +7 -0
- package/src/infrastructure/tanstack/session-cookies.ts +6 -4
- package/dist/application-DYfcAy-Q.js.map +0 -1
- package/dist/index-6XMPJu7u.d.ts.map +0 -1
- package/dist/index-_Cihfq1J.d.ts.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as DomainMessage, K as DomainQuery, U as DomainCommand, W as DomainEvent, kt as
|
|
1
|
+
import { G as DomainMessage, K as DomainQuery, Rt as EmailIdempotencyKey, U as DomainCommand, Ut as AppError, Vt as EmailAddress, W as DomainEvent, jt as BounceType, kt as RecordEmailEventPayload, q as MessageId } from "./index-Bl0K1RQW.js";
|
|
2
2
|
//#region src/application/handlers.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Handles one command with the dependency container and returns the caller's value.
|
|
@@ -13,6 +13,63 @@ type QueryHandler<TQuery extends DomainQuery, QueryDeps, Value> = (query: TQuery
|
|
|
13
13
|
/** Completes one subscriber's work for an event. Failures throw. */
|
|
14
14
|
type EventHandler<TEvent extends DomainEvent, Deps> = (event: TEvent, deps: Deps) => Promise<void>;
|
|
15
15
|
//#endregion
|
|
16
|
+
//#region src/application/email-service.d.ts
|
|
17
|
+
/** A media type such as `application/pdf`. */
|
|
18
|
+
type MimeType = `${string}/${string}`;
|
|
19
|
+
/** A file to attach. Binary content is bytes; the adapter encodes it for the provider. */
|
|
20
|
+
type EmailAttachment = {
|
|
21
|
+
readonly filename: string;
|
|
22
|
+
readonly content: string | Uint8Array;
|
|
23
|
+
/** Optional MIME metadata; Resend accepts filename and content without it. */
|
|
24
|
+
readonly contentType?: MimeType;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* One transactional email. Resend accepts 50 recipients, 75 tags with ASCII names and values,
|
|
28
|
+
* and 40 MB of attachments after base64. Sources: https://resend.com/docs/api-reference/emails/send-email
|
|
29
|
+
* and https://resend.com/docs/dashboard/emails/tags
|
|
30
|
+
*/
|
|
31
|
+
type OutboundEmail = {
|
|
32
|
+
/** Override the default for this email, for example "Display Name <address@domain.com>". */
|
|
33
|
+
readonly from?: string;
|
|
34
|
+
readonly to: string | readonly string[];
|
|
35
|
+
readonly subject: string;
|
|
36
|
+
readonly html: string;
|
|
37
|
+
/** The stable identity of this send. Resend deduplicates it for 24 hours. */
|
|
38
|
+
readonly idempotencyKey: EmailIdempotencyKey;
|
|
39
|
+
/** Resend generates plain text from html when omitted. An empty string disables that generation. */
|
|
40
|
+
readonly text?: string;
|
|
41
|
+
readonly replyTo?: string;
|
|
42
|
+
/** Labels the provider stores with the email and returns on every delivery event. */
|
|
43
|
+
readonly tags?: Readonly<Record<string, string>>;
|
|
44
|
+
readonly attachments?: readonly EmailAttachment[];
|
|
45
|
+
};
|
|
46
|
+
/** The provider accepted the email and assigned its id. */
|
|
47
|
+
type EmailAccepted = {
|
|
48
|
+
readonly providerId: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The adapter supplies the default sender and maps provider errors. Each email can override the sender.
|
|
52
|
+
* A provider rejection throws `EmailServiceError`; unexpected SDK rejections propagate unchanged.
|
|
53
|
+
*/
|
|
54
|
+
interface IEmailService {
|
|
55
|
+
/** Resolve after provider acceptance; this does not confirm delivery to the recipient. */
|
|
56
|
+
sendEmail(email: OutboundEmail): Promise<EmailAccepted>;
|
|
57
|
+
}
|
|
58
|
+
/** Records every send for a test. Provider ids count up as `recorded-1`, `recorded-2`. */
|
|
59
|
+
declare class RecordingEmailService implements IEmailService {
|
|
60
|
+
/** Sends in call order, including repeated idempotency keys. */
|
|
61
|
+
readonly sent: OutboundEmail[];
|
|
62
|
+
/** Record the send before returning its provider id. */
|
|
63
|
+
sendEmail(email: OutboundEmail): Promise<EmailAccepted>;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/application/event-collector.d.ts
|
|
67
|
+
/** Records events in the current unit of work; the unit of work collects a snapshot for outbox persistence. */
|
|
68
|
+
interface IEventCollector {
|
|
69
|
+
emit(event: DomainEvent): void;
|
|
70
|
+
collect(): ReadonlyArray<DomainEvent>;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
16
73
|
//#region src/application/message-store.d.ts
|
|
17
74
|
/**
|
|
18
75
|
* Stores processed messages and outgoing events in the same transaction as application writes.
|
|
@@ -25,6 +82,83 @@ interface IMessageStore {
|
|
|
25
82
|
persistEvents(events: ReadonlyArray<DomainEvent>): Promise<void>;
|
|
26
83
|
}
|
|
27
84
|
//#endregion
|
|
85
|
+
//#region src/application/outbox-relay.d.ts
|
|
86
|
+
/**
|
|
87
|
+
* Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote
|
|
88
|
+
* events; the app decides what a wake is: `waitUntil`, a Durable Object alarm, nothing.
|
|
89
|
+
* The sweep is the source of truth, so a lost wake loses no events.
|
|
90
|
+
*/
|
|
91
|
+
interface IOutboxRelay {
|
|
92
|
+
wake(): void;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/application/transaction.d.ts
|
|
96
|
+
/**
|
|
97
|
+
* Supplies `Tx` to participating repositories and resolves after their writes commit. Failure rolls back or discards writes.
|
|
98
|
+
* Nested calls reject with `UnitOfWorkAlreadyActiveError` before opening another boundary.
|
|
99
|
+
* Independent concurrent calls are allowed.
|
|
100
|
+
*/
|
|
101
|
+
interface ITransaction<Tx> {
|
|
102
|
+
transaction<T>(work: (tx: Tx) => Promise<T>): Promise<T>;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/application/unit-of-work.d.ts
|
|
106
|
+
/**
|
|
107
|
+
* Commits the processed-message record, repository writes, and emitted events together before resolving.
|
|
108
|
+
* An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.
|
|
109
|
+
* Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.
|
|
110
|
+
*/
|
|
111
|
+
interface IUnitOfWork<Repos> {
|
|
112
|
+
run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>;
|
|
113
|
+
}
|
|
114
|
+
type UnitOfWorkDeps<Tx, Repos> = {
|
|
115
|
+
readonly transaction: ITransaction<Tx>;
|
|
116
|
+
/** The processed-messages and outbox repository for this transaction. */
|
|
117
|
+
readonly messageStore: (tx: Tx) => IMessageStore;
|
|
118
|
+
/** Repositories participate in the current transaction. */
|
|
119
|
+
readonly createRepositories: (tx: Tx) => Repos;
|
|
120
|
+
readonly eventCollector: IEventCollector;
|
|
121
|
+
readonly relay: IOutboxRelay;
|
|
122
|
+
};
|
|
123
|
+
/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */
|
|
124
|
+
declare class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {
|
|
125
|
+
private readonly deps;
|
|
126
|
+
constructor(deps: UnitOfWorkDeps<Tx, Repos>);
|
|
127
|
+
run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>;
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/application/record-email-event.d.ts
|
|
131
|
+
/** What a bounce records for one address. */
|
|
132
|
+
type EmailBounceRecord = {
|
|
133
|
+
readonly type: BounceType;
|
|
134
|
+
readonly subType: string | null;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* The application's deliverability state per address. An implementation keeps the higher bounce
|
|
138
|
+
* severity with `shouldUpdateBounce`, and treats an unknown address as nothing to record.
|
|
139
|
+
*/
|
|
140
|
+
interface IEmailDeliverabilityStore {
|
|
141
|
+
/** Keep the higher severity atomically. Equal severity permits replacement; the store owns timestamp ordering. */
|
|
142
|
+
recordBounce(address: EmailAddress, bounce: EmailBounceRecord): Promise<void>;
|
|
143
|
+
/** Record a complaint without clearing existing suppression state. */
|
|
144
|
+
recordComplaint(address: EmailAddress): Promise<void>;
|
|
145
|
+
}
|
|
146
|
+
/** The repositories the handler needs from the unit of work. */
|
|
147
|
+
type RecordEmailEventRepos = {
|
|
148
|
+
readonly emailDeliverability: IEmailDeliverabilityStore;
|
|
149
|
+
};
|
|
150
|
+
/** The container slice the handler reads. */
|
|
151
|
+
type RecordEmailEventDeps = {
|
|
152
|
+
readonly uow: IUnitOfWork<RecordEmailEventRepos>;
|
|
153
|
+
};
|
|
154
|
+
/** The command, under whatever name the application registered the payload. */
|
|
155
|
+
type RecordEmailEventCommand = DomainCommand & RecordEmailEventPayload;
|
|
156
|
+
/**
|
|
157
|
+
* Record a bounce or a complaint for every recipient inside the command's unit of work.
|
|
158
|
+
* The command id is the provider's event id, so a redelivered webhook is `MessageAlreadyProcessedError`.
|
|
159
|
+
*/
|
|
160
|
+
declare function recordEmailEvent(command: RecordEmailEventCommand, deps: RecordEmailEventDeps): Promise<void>;
|
|
161
|
+
//#endregion
|
|
28
162
|
//#region src/application/registry.d.ts
|
|
29
163
|
/** One slot per command. `unknown` keeps the slot covariant; `MessageResult` recovers the value. */
|
|
30
164
|
type CommandRegistry<TCommand extends DomainCommand, Deps> = { readonly [M in TCommand as M['name']]: CommandHandler<M, Deps, unknown>; };
|
|
@@ -94,58 +228,6 @@ declare class MessageBus<Deps, TRegistry extends Handlers<Deps>> implements IMes
|
|
|
94
228
|
private handleEvent;
|
|
95
229
|
}
|
|
96
230
|
//#endregion
|
|
97
|
-
//#region src/application/outbox-relay.d.ts
|
|
98
|
-
/**
|
|
99
|
-
* Delivers committed outbox rows. The unit of work only wakes it after a commit that wrote
|
|
100
|
-
* events; the app decides what a wake is: `waitUntil`, a Durable Object alarm, nothing.
|
|
101
|
-
* The sweep is the source of truth, so a lost wake loses no events.
|
|
102
|
-
*/
|
|
103
|
-
interface IOutboxRelay {
|
|
104
|
-
wake(): void;
|
|
105
|
-
}
|
|
106
|
-
//#endregion
|
|
107
|
-
//#region src/application/transaction.d.ts
|
|
108
|
-
/**
|
|
109
|
-
* Supplies `Tx` to participating repositories and resolves after their writes commit. Failure rolls back or discards writes.
|
|
110
|
-
* Nested calls reject with `UnitOfWorkAlreadyActiveError` before opening another boundary.
|
|
111
|
-
* Independent concurrent calls are allowed.
|
|
112
|
-
*/
|
|
113
|
-
interface ITransaction<Tx> {
|
|
114
|
-
transaction<T>(work: (tx: Tx) => Promise<T>): Promise<T>;
|
|
115
|
-
}
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/application/event-collector.d.ts
|
|
118
|
-
/** Records events in the current unit of work; the unit of work collects a snapshot for outbox persistence. */
|
|
119
|
-
interface IEventCollector {
|
|
120
|
-
emit(event: DomainEvent): void;
|
|
121
|
-
collect(): ReadonlyArray<DomainEvent>;
|
|
122
|
-
}
|
|
123
|
-
//#endregion
|
|
124
|
-
//#region src/application/unit-of-work.d.ts
|
|
125
|
-
/**
|
|
126
|
-
* Commits the processed-message record, repository writes, and emitted events together before resolving.
|
|
127
|
-
* An existing record rejects with `MessageAlreadyProcessedError`; external effects must run through the outbox after commit.
|
|
128
|
-
* Nested calls reject with `UnitOfWorkAlreadyActiveError`; independent concurrent calls are allowed.
|
|
129
|
-
*/
|
|
130
|
-
interface IUnitOfWork<Repos> {
|
|
131
|
-
run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>;
|
|
132
|
-
}
|
|
133
|
-
type UnitOfWorkDeps<Tx, Repos> = {
|
|
134
|
-
readonly transaction: ITransaction<Tx>;
|
|
135
|
-
/** The processed-messages and outbox repository for this transaction. */
|
|
136
|
-
readonly messageStore: (tx: Tx) => IMessageStore;
|
|
137
|
-
/** Repositories participate in the current transaction. */
|
|
138
|
-
readonly createRepositories: (tx: Tx) => Repos;
|
|
139
|
-
readonly eventCollector: IEventCollector;
|
|
140
|
-
readonly relay: IOutboxRelay;
|
|
141
|
-
};
|
|
142
|
-
/** The unit of work: the boundary, the repositories, the events. Every database call is a port. */
|
|
143
|
-
declare class UnitOfWork<Tx, Repos> implements IUnitOfWork<Repos> {
|
|
144
|
-
private readonly deps;
|
|
145
|
-
constructor(deps: UnitOfWorkDeps<Tx, Repos>);
|
|
146
|
-
run<T>(messageId: MessageId, work: (repos: Repos) => Promise<T>): Promise<T>;
|
|
147
|
-
}
|
|
148
|
-
//#endregion
|
|
149
231
|
//#region src/application/message-already-processed.error.d.ts
|
|
150
232
|
/** The message's effects already committed. A caller can skip duplicate delivery without repeating those effects. */
|
|
151
233
|
declare class MessageAlreadyProcessedError extends AppError {
|
|
@@ -167,5 +249,5 @@ declare class UnitOfWorkAlreadyActiveError extends AppError {
|
|
|
167
249
|
constructor();
|
|
168
250
|
}
|
|
169
251
|
//#endregion
|
|
170
|
-
export {
|
|
171
|
-
//# sourceMappingURL=index-
|
|
252
|
+
export { RecordingEmailService as A, IMessageStore as C, IEmailService as D, EmailAttachment as E, EventHandler as M, QueryHandler as N, MimeType as O, IOutboxRelay as S, EmailAccepted as T, recordEmailEvent as _, CommandRegistry as a, UnitOfWorkDeps as b, MessageRegistry as c, RegistryMessage as d, EmailBounceRecord as f, RecordEmailEventRepos as g, RecordEmailEventDeps as h, MessageBus as i, CommandHandler as j, OutboundEmail as k, MessageResult as l, RecordEmailEventCommand as m, MessageAlreadyProcessedError as n, EventRegistry as o, IEmailDeliverabilityStore as p, IMessageBus as r, Handlers as s, UnitOfWorkAlreadyActiveError as t, QueryRegistry as u, IUnitOfWork as v, IEventCollector as w, ITransaction as x, UnitOfWork as y };
|
|
253
|
+
//# sourceMappingURL=index-DwWxIdQP.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-DwWxIdQP.d.ts","names":[],"sources":["../src/application/handlers.ts","../src/application/email-service.ts","../src/application/event-collector.ts","../src/application/message-store.ts","../src/application/outbox-relay.ts","../src/application/transaction.ts","../src/application/unit-of-work.ts","../src/application/record-email-event.ts","../src/application/registry.ts","../src/application/message-bus.ts","../src/application/message-already-processed.error.ts","../src/application/unit-of-work.errors.ts"],"mappings":"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"}
|
package/dist/postgres.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { W as DomainEvent, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-
|
|
2
|
-
import {
|
|
3
|
-
import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-
|
|
1
|
+
import { W as DomainEvent, nt as DatabaseFailure, q as MessageId, tt as DatabaseError } from "./index-Bl0K1RQW.js";
|
|
2
|
+
import { C as IMessageStore, x as ITransaction } from "./index-DwWxIdQP.js";
|
|
3
|
+
import { n as DrizzleSchema, t as eventCollector } from "./unit-of-work-context-D5ytJIa-.js";
|
|
4
4
|
import { ExtractTablesWithRelations } from "drizzle-orm";
|
|
5
5
|
import { PostgresJsDatabase, PostgresJsTransaction, drizzle } from "drizzle-orm/postgres-js";
|
|
6
6
|
//#region src/infrastructure/postgres/postgres-database-types.d.ts
|
package/dist/postgres.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { E as DatabaseError,
|
|
2
|
-
import { i as MessageAlreadyProcessedError } from "./application-
|
|
3
|
-
import { n as eventCollector, r as runWithUnitOfWorkContext } from "./unit-of-work-context-
|
|
1
|
+
import { E as DatabaseError, Q as AppError } from "./core-DG_cC8YT.js";
|
|
2
|
+
import { i as MessageAlreadyProcessedError } from "./application-CIxo_BpK.js";
|
|
3
|
+
import { n as eventCollector, r as runWithUnitOfWorkContext } from "./unit-of-work-context-BPE1In2f.js";
|
|
4
4
|
import { DrizzleQueryError } from "drizzle-orm/errors";
|
|
5
5
|
import { isNull } from "drizzle-orm";
|
|
6
6
|
import { index, integer, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
package/dist/resend.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jt as BounceType } from "./index-Bl0K1RQW.js";
|
|
2
|
+
import { D as IEmailService, T as EmailAccepted, k as OutboundEmail } from "./index-DwWxIdQP.js";
|
|
3
|
+
import { r as WebhookVerifier, t as WebhookVerification } from "./webhook-verification-sZtwTI5n.js";
|
|
4
|
+
import { Resend } from "resend";
|
|
5
|
+
//#region src/infrastructure/resend/resend-email-service.d.ts
|
|
6
|
+
/** The SDK operations required by the adapter; a test factory can return a plain object. */
|
|
7
|
+
type ResendEmailClient = {
|
|
8
|
+
readonly emails: Pick<Resend['emails'], 'send'>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.
|
|
12
|
+
* The Resend SDK itself logs API errors outside production.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ResendEmailService implements IEmailService {
|
|
15
|
+
private readonly createClient;
|
|
16
|
+
private readonly fromAddress;
|
|
17
|
+
/** Load the SDK on the first send. Constructing the service does not load it. */
|
|
18
|
+
static create(apiKey: string, fromAddress: string): ResendEmailService;
|
|
19
|
+
private client;
|
|
20
|
+
/** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */
|
|
21
|
+
constructor(createClient: () => Promise<ResendEmailClient>, fromAddress: string);
|
|
22
|
+
/** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
|
|
23
|
+
sendEmail(email: OutboundEmail): Promise<EmailAccepted>;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/infrastructure/resend/email-event.d.ts
|
|
27
|
+
declare const SIMPLE_EMAIL_TYPES: readonly ['email.sent', 'email.scheduled', 'email.delivered', 'email.delivery_delayed', 'email.complained', 'email.opened', 'email.received', 'email.suppressed'];
|
|
28
|
+
/** Resend email event names supported by this module. */
|
|
29
|
+
type EmailEventType = (typeof SIMPLE_EMAIL_TYPES)[number] | 'email.bounced' | 'email.failed' | 'email.clicked';
|
|
30
|
+
type EmailEventBase = {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly emailId: string;
|
|
33
|
+
readonly recipients: readonly string[];
|
|
34
|
+
readonly subject: string;
|
|
35
|
+
readonly tags: Readonly<Record<string, string>>;
|
|
36
|
+
readonly occurredAt: string;
|
|
37
|
+
};
|
|
38
|
+
/** A verified event. `id` is the Svix identity; use `resend:event:${id}` as the command id. */
|
|
39
|
+
type EmailEvent = (EmailEventBase & {
|
|
40
|
+
readonly type: 'email.bounced';
|
|
41
|
+
readonly bounce: {
|
|
42
|
+
readonly type: BounceType;
|
|
43
|
+
readonly subType: string | null;
|
|
44
|
+
readonly message: string;
|
|
45
|
+
};
|
|
46
|
+
}) | (EmailEventBase & {
|
|
47
|
+
readonly type: 'email.failed';
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
}) | (EmailEventBase & {
|
|
50
|
+
readonly type: 'email.clicked';
|
|
51
|
+
readonly link: string;
|
|
52
|
+
}) | (EmailEventBase & {
|
|
53
|
+
readonly type: (typeof SIMPLE_EMAIL_TYPES)[number];
|
|
54
|
+
});
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/infrastructure/resend/resend-webhook.d.ts
|
|
57
|
+
/** The SDK's local signature verifier. It makes no API request. */
|
|
58
|
+
type ResendWebhookClient = Pick<Resend['webhooks'], 'verify'>;
|
|
59
|
+
/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */
|
|
60
|
+
export declare class ResendWebhook implements WebhookVerifier<EmailEvent> {
|
|
61
|
+
private readonly webhookSecret;
|
|
62
|
+
private readonly verifier;
|
|
63
|
+
/** Load the SDK on the first verification. Only the webhook secret is required. */
|
|
64
|
+
static create(webhookSecret: string): ResendWebhook;
|
|
65
|
+
private client;
|
|
66
|
+
/** Inject the local SDK verifier when testing. */
|
|
67
|
+
constructor(webhookSecret: string, verifier: () => Promise<ResendWebhookClient>);
|
|
68
|
+
/** Verify the raw body and Svix headers. Invalid requests return rejected; configuration and SDK defects propagate. */
|
|
69
|
+
verify(body: string, headers: Headers): Promise<WebhookVerification<EmailEvent>>;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export type { EmailEvent, EmailEventType, ResendEmailClient, ResendWebhookClient };
|
|
73
|
+
//# sourceMappingURL=resend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resend.d.ts","names":[],"sources":["../src/infrastructure/resend/resend-email-service.ts","../src/infrastructure/resend/email-event.ts","../src/infrastructure/resend/resend-webhook.ts"],"mappings":";;;;;;KAsEY;WAA+B,QAAQ,KAAK;;;;;;qBAM3C,8BAA8B;mBAatB;mBACA;;SAZZ,OAAO,gBAAgB,sBAAsB;UAO5C;;EAGR,YACmB,oBAAoB,QAAQ,oBAC5B;;EAIb,UAAU,OAAO,gBAAgB,QAAQ;;;;cCzF3C;;KAYM,yBACA;KAgDP;WACM;WACA;WACA;WACA;WACA,MAAM,SAAS;WACf;;;KAIC,cACP;WACU;WACA;aACE,MAAM;aACN;aACA;;MAGZ;WAA4B;WAA+B;MAC3D;WAA4B;WAAgC;MAC5D;WAA4B,cAAc;;;;;KCvEnC,sBAAsB,KAAK;;qBAG1B,yBAAyB,gBAAgB;mBAcjC;mBACA;;SAbZ,OAAO,wBAAwB;UAQ9B;;EAGR,YACmB,uBACA,gBAAgB,QAAQ;;EAIrC,OAAO,cAAc,SAAS,UAAU,QAAQ,oBAAoB"}
|
package/dist/resend.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { J as EmailServiceError, K as parseBounceType, Z as EmailAddressSchema } from "./core-DG_cC8YT.js";
|
|
2
|
+
import { t as WebhookVerificationError } from "./webhook-verification-BQ9qZnVK.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
|
|
6
|
+
//#region src/infrastructure/resend/resend-email-service.ts
|
|
7
|
+
const SDK_REASONS = {
|
|
8
|
+
invalid_idempotency_key: "invalid_request",
|
|
9
|
+
validation_error: "invalid_request",
|
|
10
|
+
missing_api_key: "configuration",
|
|
11
|
+
restricted_api_key: "configuration",
|
|
12
|
+
invalid_api_key: "configuration",
|
|
13
|
+
not_found: "invalid_request",
|
|
14
|
+
method_not_allowed: "invalid_request",
|
|
15
|
+
invalid_idempotent_request: "invalid_request",
|
|
16
|
+
concurrent_idempotent_requests: "idempotency_conflict",
|
|
17
|
+
invalid_attachment: "invalid_request",
|
|
18
|
+
invalid_from_address: "invalid_request",
|
|
19
|
+
invalid_access: "configuration",
|
|
20
|
+
invalid_parameter: "invalid_request",
|
|
21
|
+
invalid_region: "invalid_request",
|
|
22
|
+
missing_required_field: "invalid_request",
|
|
23
|
+
monthly_quota_exceeded: "quota_exceeded",
|
|
24
|
+
daily_quota_exceeded: "quota_exceeded",
|
|
25
|
+
rate_limit_exceeded: "rate_limited",
|
|
26
|
+
security_error: "configuration",
|
|
27
|
+
application_error: "provider_error",
|
|
28
|
+
internal_server_error: "provider_error"
|
|
29
|
+
};
|
|
30
|
+
const DOCUMENTED_REASONS = /* @__PURE__ */ new Map([
|
|
31
|
+
["email_above_quota", "quota_exceeded"],
|
|
32
|
+
["invalid_permission", "configuration"],
|
|
33
|
+
["suspended_api_key", "configuration"],
|
|
34
|
+
["resource_locked", "idempotency_conflict"],
|
|
35
|
+
["service_unavailable", "provider_error"],
|
|
36
|
+
["missing_required_parameter", "invalid_request"]
|
|
37
|
+
]);
|
|
38
|
+
function reasonFromResend(error) {
|
|
39
|
+
if (Object.hasOwn(SDK_REASONS, error.name)) return SDK_REASONS[error.name];
|
|
40
|
+
const reason = DOCUMENTED_REASONS.get(error.name);
|
|
41
|
+
if (reason !== void 0) return reason;
|
|
42
|
+
if (error.statusCode === 429) return "rate_limited";
|
|
43
|
+
if (error.statusCode !== null && error.statusCode >= 400 && error.statusCode < 500) return "invalid_request";
|
|
44
|
+
return "provider_error";
|
|
45
|
+
}
|
|
46
|
+
function retryAfterSeconds(value) {
|
|
47
|
+
if (value === void 0 || !/^\d+$/.test(value)) return null;
|
|
48
|
+
const seconds = Number(value);
|
|
49
|
+
return Number.isSafeInteger(seconds) ? seconds : null;
|
|
50
|
+
}
|
|
51
|
+
function toAttachment(attachment) {
|
|
52
|
+
const result = {
|
|
53
|
+
filename: attachment.filename,
|
|
54
|
+
content: ArrayBuffer.isView(attachment.content) ? Buffer.from(attachment.content) : attachment.content
|
|
55
|
+
};
|
|
56
|
+
if (attachment.contentType !== void 0) result.contentType = attachment.contentType;
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.
|
|
61
|
+
* The Resend SDK itself logs API errors outside production.
|
|
62
|
+
*/
|
|
63
|
+
var ResendEmailService = class ResendEmailService {
|
|
64
|
+
createClient;
|
|
65
|
+
fromAddress;
|
|
66
|
+
/** Load the SDK on the first send. Constructing the service does not load it. */
|
|
67
|
+
static create(apiKey, fromAddress) {
|
|
68
|
+
return new ResendEmailService(() => import("resend").then(({ Resend }) => new Resend(apiKey)), fromAddress);
|
|
69
|
+
}
|
|
70
|
+
client;
|
|
71
|
+
/** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */
|
|
72
|
+
constructor(createClient, fromAddress) {
|
|
73
|
+
this.createClient = createClient;
|
|
74
|
+
this.fromAddress = fromAddress;
|
|
75
|
+
}
|
|
76
|
+
/** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
|
|
77
|
+
async sendEmail(email) {
|
|
78
|
+
if (this.client === void 0) this.client = Promise.resolve().then(this.createClient);
|
|
79
|
+
const client = await this.client;
|
|
80
|
+
const payload = {
|
|
81
|
+
from: email.from ?? this.fromAddress,
|
|
82
|
+
to: [email.to].flat(),
|
|
83
|
+
subject: email.subject,
|
|
84
|
+
html: email.html
|
|
85
|
+
};
|
|
86
|
+
if (email.text !== void 0) payload.text = email.text;
|
|
87
|
+
if (email.replyTo !== void 0) payload.replyTo = email.replyTo;
|
|
88
|
+
if (email.tags !== void 0) payload.tags = Object.entries(email.tags).map(([name, value]) => ({
|
|
89
|
+
name,
|
|
90
|
+
value
|
|
91
|
+
}));
|
|
92
|
+
if (email.attachments !== void 0) payload.attachments = email.attachments.map(toAttachment);
|
|
93
|
+
const result = await client.emails.send(payload, { idempotencyKey: email.idempotencyKey });
|
|
94
|
+
if (result.error !== null) throw new EmailServiceError({
|
|
95
|
+
reason: reasonFromResend(result.error),
|
|
96
|
+
provider: "resend",
|
|
97
|
+
providerCode: result.error.name,
|
|
98
|
+
statusCode: result.error.statusCode,
|
|
99
|
+
retryAfter: retryAfterSeconds(result.headers?.["retry-after"]),
|
|
100
|
+
cause: result.error
|
|
101
|
+
});
|
|
102
|
+
return { providerId: result.data.id };
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/infrastructure/resend/email-event.ts
|
|
108
|
+
const SIMPLE_EMAIL_TYPES = [
|
|
109
|
+
"email.sent",
|
|
110
|
+
"email.scheduled",
|
|
111
|
+
"email.delivered",
|
|
112
|
+
"email.delivery_delayed",
|
|
113
|
+
"email.complained",
|
|
114
|
+
"email.opened",
|
|
115
|
+
"email.received",
|
|
116
|
+
"email.suppressed"
|
|
117
|
+
];
|
|
118
|
+
const EMAIL_TYPES = {
|
|
119
|
+
"email.sent": true,
|
|
120
|
+
"email.scheduled": true,
|
|
121
|
+
"email.delivered": true,
|
|
122
|
+
"email.delivery_delayed": true,
|
|
123
|
+
"email.complained": true,
|
|
124
|
+
"email.opened": true,
|
|
125
|
+
"email.received": true,
|
|
126
|
+
"email.suppressed": true,
|
|
127
|
+
"email.bounced": true,
|
|
128
|
+
"email.failed": true,
|
|
129
|
+
"email.clicked": true
|
|
130
|
+
};
|
|
131
|
+
const BaseDataSchema = z.object({
|
|
132
|
+
email_id: z.string().min(1),
|
|
133
|
+
to: z.array(EmailAddressSchema).min(1),
|
|
134
|
+
subject: z.string(),
|
|
135
|
+
tags: z.record(z.string(), z.string()).default({})
|
|
136
|
+
});
|
|
137
|
+
const EmailPayloadSchema = z.discriminatedUnion("type", [
|
|
138
|
+
z.object({
|
|
139
|
+
type: z.enum(SIMPLE_EMAIL_TYPES),
|
|
140
|
+
data: BaseDataSchema
|
|
141
|
+
}),
|
|
142
|
+
z.object({
|
|
143
|
+
type: z.literal("email.bounced"),
|
|
144
|
+
data: BaseDataSchema.extend({ bounce: z.object({
|
|
145
|
+
type: z.string().optional(),
|
|
146
|
+
subType: z.string().nullable().optional(),
|
|
147
|
+
message: z.string()
|
|
148
|
+
}) })
|
|
149
|
+
}),
|
|
150
|
+
z.object({
|
|
151
|
+
type: z.literal("email.failed"),
|
|
152
|
+
data: BaseDataSchema.extend({ failed: z.object({ reason: z.string() }) })
|
|
153
|
+
}),
|
|
154
|
+
z.object({
|
|
155
|
+
type: z.literal("email.clicked"),
|
|
156
|
+
data: BaseDataSchema.extend({ click: z.object({ link: z.string() }) })
|
|
157
|
+
})
|
|
158
|
+
]);
|
|
159
|
+
const EventHeaderSchema = z.object({
|
|
160
|
+
type: z.string(),
|
|
161
|
+
created_at: z.iso.datetime({ offset: true })
|
|
162
|
+
});
|
|
163
|
+
/** Validate signed provider data before projecting it into the public event contract. */
|
|
164
|
+
function parseEmailEvent(payload, id) {
|
|
165
|
+
const header = EventHeaderSchema.safeParse(payload);
|
|
166
|
+
if (!header.success) return { outcome: "invalid" };
|
|
167
|
+
if (!Object.hasOwn(EMAIL_TYPES, header.data.type)) return {
|
|
168
|
+
outcome: "ignored",
|
|
169
|
+
type: header.data.type
|
|
170
|
+
};
|
|
171
|
+
const parsed = EmailPayloadSchema.safeParse(payload);
|
|
172
|
+
if (!parsed.success) return { outcome: "invalid" };
|
|
173
|
+
const event = parsed.data;
|
|
174
|
+
const base = {
|
|
175
|
+
id,
|
|
176
|
+
emailId: event.data.email_id,
|
|
177
|
+
recipients: event.data.to,
|
|
178
|
+
subject: event.data.subject,
|
|
179
|
+
tags: event.data.tags,
|
|
180
|
+
occurredAt: header.data.created_at
|
|
181
|
+
};
|
|
182
|
+
switch (event.type) {
|
|
183
|
+
case "email.bounced": return {
|
|
184
|
+
outcome: "event",
|
|
185
|
+
event: {
|
|
186
|
+
...base,
|
|
187
|
+
type: event.type,
|
|
188
|
+
bounce: {
|
|
189
|
+
type: parseBounceType(event.data.bounce.type),
|
|
190
|
+
subType: event.data.bounce.subType ?? null,
|
|
191
|
+
message: event.data.bounce.message
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
case "email.failed": return {
|
|
196
|
+
outcome: "event",
|
|
197
|
+
event: {
|
|
198
|
+
...base,
|
|
199
|
+
type: event.type,
|
|
200
|
+
reason: event.data.failed.reason
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
case "email.clicked": return {
|
|
204
|
+
outcome: "event",
|
|
205
|
+
event: {
|
|
206
|
+
...base,
|
|
207
|
+
type: event.type,
|
|
208
|
+
link: event.data.click.link
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
default: return {
|
|
212
|
+
outcome: "event",
|
|
213
|
+
event: {
|
|
214
|
+
...base,
|
|
215
|
+
type: event.type
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/infrastructure/resend/resend-webhook.ts
|
|
223
|
+
const SignatureErrorSchema = z.instanceof(Error).refine((error) => error.name === "WebhookVerificationError");
|
|
224
|
+
/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */
|
|
225
|
+
var ResendWebhook = class ResendWebhook {
|
|
226
|
+
webhookSecret;
|
|
227
|
+
verifier;
|
|
228
|
+
/** Load the SDK on the first verification. Only the webhook secret is required. */
|
|
229
|
+
static create(webhookSecret) {
|
|
230
|
+
return new ResendWebhook(webhookSecret, async () => {
|
|
231
|
+
const { Resend } = await import("resend");
|
|
232
|
+
return new Resend("re_webhook_verification_only").webhooks;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
client;
|
|
236
|
+
/** Inject the local SDK verifier when testing. */
|
|
237
|
+
constructor(webhookSecret, verifier) {
|
|
238
|
+
this.webhookSecret = webhookSecret;
|
|
239
|
+
this.verifier = verifier;
|
|
240
|
+
}
|
|
241
|
+
/** Verify the raw body and Svix headers. Invalid requests return rejected; configuration and SDK defects propagate. */
|
|
242
|
+
async verify(body, headers) {
|
|
243
|
+
const id = headers.get("svix-id");
|
|
244
|
+
const timestamp = headers.get("svix-timestamp");
|
|
245
|
+
const signature = headers.get("svix-signature");
|
|
246
|
+
if (!id || !timestamp || !signature) return {
|
|
247
|
+
outcome: "rejected",
|
|
248
|
+
error: new WebhookVerificationError("missing_headers")
|
|
249
|
+
};
|
|
250
|
+
const client = await (this.client ??= this.verifier());
|
|
251
|
+
let payload;
|
|
252
|
+
try {
|
|
253
|
+
payload = client.verify({
|
|
254
|
+
payload: body,
|
|
255
|
+
headers: {
|
|
256
|
+
id,
|
|
257
|
+
timestamp,
|
|
258
|
+
signature
|
|
259
|
+
},
|
|
260
|
+
webhookSecret: this.webhookSecret
|
|
261
|
+
});
|
|
262
|
+
} catch (cause) {
|
|
263
|
+
if (cause instanceof SyntaxError) return {
|
|
264
|
+
outcome: "rejected",
|
|
265
|
+
error: new WebhookVerificationError("invalid_payload", cause)
|
|
266
|
+
};
|
|
267
|
+
if (!SignatureErrorSchema.safeParse(cause).success) throw cause;
|
|
268
|
+
return {
|
|
269
|
+
outcome: "rejected",
|
|
270
|
+
error: new WebhookVerificationError("invalid_signature", cause)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const result = parseEmailEvent(payload, id);
|
|
274
|
+
if (result.outcome === "invalid") return {
|
|
275
|
+
outcome: "rejected",
|
|
276
|
+
error: new WebhookVerificationError("invalid_payload")
|
|
277
|
+
};
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
//#endregion
|
|
283
|
+
export { ResendEmailService, ResendWebhook };
|
|
284
|
+
//# sourceMappingURL=resend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resend.js","names":[],"sources":["../src/infrastructure/resend/resend-email-service.ts","../src/infrastructure/resend/email-event.ts","../src/infrastructure/resend/resend-webhook.ts"],"sourcesContent":["import { Buffer } from 'node:buffer'\n\nimport type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'\n\nimport type { EmailAccepted, EmailAttachment, IEmailService, OutboundEmail } from '@/application'\nimport { EmailServiceError, type EmailSendFailureReason } from '@/core'\n\n// SDK additions must receive an explicit classification before the package builds.\nconst SDK_REASONS = {\n invalid_idempotency_key: 'invalid_request',\n validation_error: 'invalid_request',\n missing_api_key: 'configuration',\n restricted_api_key: 'configuration',\n invalid_api_key: 'configuration',\n not_found: 'invalid_request',\n method_not_allowed: 'invalid_request',\n invalid_idempotent_request: 'invalid_request',\n concurrent_idempotent_requests: 'idempotency_conflict',\n invalid_attachment: 'invalid_request',\n invalid_from_address: 'invalid_request',\n invalid_access: 'configuration',\n invalid_parameter: 'invalid_request',\n invalid_region: 'invalid_request',\n missing_required_field: 'invalid_request',\n monthly_quota_exceeded: 'quota_exceeded',\n daily_quota_exceeded: 'quota_exceeded',\n rate_limit_exceeded: 'rate_limited',\n security_error: 'configuration',\n application_error: 'provider_error',\n internal_server_error: 'provider_error',\n} satisfies Record<ErrorResponse['name'], EmailSendFailureReason>\n\n// Resend documents these codes before its SDK includes them in ErrorResponse.\nconst DOCUMENTED_REASONS = new Map<string, EmailSendFailureReason>([\n ['email_above_quota', 'quota_exceeded'],\n ['invalid_permission', 'configuration'],\n ['suspended_api_key', 'configuration'],\n ['resource_locked', 'idempotency_conflict'],\n ['service_unavailable', 'provider_error'],\n ['missing_required_parameter', 'invalid_request'],\n])\n\nfunction reasonFromResend(error: ErrorResponse): EmailSendFailureReason {\n if (Object.hasOwn(SDK_REASONS, error.name)) return SDK_REASONS[error.name]\n const reason = DOCUMENTED_REASONS.get(error.name)\n if (reason !== undefined) return reason\n if (error.statusCode === 429) return 'rate_limited'\n if (error.statusCode !== null && error.statusCode >= 400 && error.statusCode < 500)\n return 'invalid_request'\n return 'provider_error'\n}\n\nfunction retryAfterSeconds(value: string | undefined): number | null {\n if (value === undefined || !/^\\d+$/.test(value)) return null\n const seconds = Number(value)\n return Number.isSafeInteger(seconds) ? seconds : null\n}\n\nfunction toAttachment(attachment: EmailAttachment): Attachment {\n const result: Attachment = {\n filename: attachment.filename,\n content: ArrayBuffer.isView(attachment.content)\n ? Buffer.from(attachment.content)\n : attachment.content,\n }\n if (attachment.contentType !== undefined) result.contentType = attachment.contentType\n return result\n}\n\n/** The SDK operations required by the adapter; a test factory can return a plain object. */\nexport type ResendEmailClient = { readonly emails: Pick<Resend['emails'], 'send'> }\n\n/**\n * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.\n * The Resend SDK itself logs API errors outside production.\n */\nexport class ResendEmailService implements IEmailService {\n /** Load the SDK on the first send. Constructing the service does not load it. */\n static create(apiKey: string, fromAddress: string): ResendEmailService {\n return new ResendEmailService(\n () => import('resend').then(({ Resend }) => new Resend(apiKey)),\n fromAddress,\n )\n }\n\n private client: Promise<ResendEmailClient> | undefined\n\n /** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */\n constructor(\n private readonly createClient: () => Promise<ResendEmailClient>,\n private readonly fromAddress: string,\n ) {}\n\n /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */\n async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {\n if (this.client === undefined) {\n // Cache synchronous factory throws as rejections too, so later sends cannot initialize again.\n this.client = Promise.resolve().then(this.createClient)\n }\n const client = await this.client\n const payload: CreateEmailOptions = {\n from: email.from ?? this.fromAddress,\n to: [email.to].flat(),\n subject: email.subject,\n html: email.html,\n }\n if (email.text !== undefined) payload.text = email.text\n if (email.replyTo !== undefined) payload.replyTo = email.replyTo\n if (email.tags !== undefined)\n payload.tags = Object.entries(email.tags).map(([name, value]) => ({ name, value }))\n if (email.attachments !== undefined) payload.attachments = email.attachments.map(toAttachment)\n const result = await client.emails.send(payload, { idempotencyKey: email.idempotencyKey })\n if (result.error !== null) {\n throw new EmailServiceError({\n reason: reasonFromResend(result.error),\n provider: 'resend',\n providerCode: result.error.name,\n statusCode: result.error.statusCode,\n retryAfter: retryAfterSeconds(result.headers?.['retry-after']),\n cause: result.error,\n })\n }\n return { providerId: result.data.id }\n }\n}\n","import type { WebhookEventPayload } from 'resend'\nimport { z } from 'zod'\n\nimport { EmailAddressSchema, parseBounceType, type BounceType } from '@/core'\n\nconst SIMPLE_EMAIL_TYPES = [\n 'email.sent',\n 'email.scheduled',\n 'email.delivered',\n 'email.delivery_delayed',\n 'email.complained',\n 'email.opened',\n 'email.received',\n 'email.suppressed',\n] as const\n\n/** Resend email event names supported by this module. */\nexport type EmailEventType =\n | (typeof SIMPLE_EMAIL_TYPES)[number]\n | 'email.bounced'\n | 'email.failed'\n | 'email.clicked'\n\nconst EMAIL_TYPES = {\n 'email.sent': true,\n 'email.scheduled': true,\n 'email.delivered': true,\n 'email.delivery_delayed': true,\n 'email.complained': true,\n 'email.opened': true,\n 'email.received': true,\n 'email.suppressed': true,\n 'email.bounced': true,\n 'email.failed': true,\n 'email.clicked': true,\n} satisfies Record<Extract<WebhookEventPayload['type'], `email.${string}`>, true>\n\nconst BaseDataSchema = z.object({\n email_id: z.string().min(1),\n to: z.array(EmailAddressSchema).min(1),\n subject: z.string(),\n tags: z.record(z.string(), z.string()).default({}),\n})\n\nconst EmailPayloadSchema = z.discriminatedUnion('type', [\n z.object({ type: z.enum(SIMPLE_EMAIL_TYPES), data: BaseDataSchema }),\n z.object({\n type: z.literal('email.bounced'),\n data: BaseDataSchema.extend({\n bounce: z.object({\n type: z.string().optional(),\n subType: z.string().nullable().optional(),\n message: z.string(),\n }),\n }),\n }),\n z.object({\n type: z.literal('email.failed'),\n data: BaseDataSchema.extend({ failed: z.object({ reason: z.string() }) }),\n }),\n z.object({\n type: z.literal('email.clicked'),\n data: BaseDataSchema.extend({ click: z.object({ link: z.string() }) }),\n }),\n])\n\ntype EmailEventBase = {\n readonly id: string\n readonly emailId: string\n readonly recipients: readonly string[]\n readonly subject: string\n readonly tags: Readonly<Record<string, string>>\n readonly occurredAt: string\n}\n\n/** A verified event. `id` is the Svix identity; use `resend:event:${id}` as the command id. */\nexport type EmailEvent =\n | (EmailEventBase & {\n readonly type: 'email.bounced'\n readonly bounce: {\n readonly type: BounceType\n readonly subType: string | null\n readonly message: string\n }\n })\n | (EmailEventBase & { readonly type: 'email.failed'; readonly reason: string })\n | (EmailEventBase & { readonly type: 'email.clicked'; readonly link: string })\n | (EmailEventBase & { readonly type: (typeof SIMPLE_EMAIL_TYPES)[number] })\n\nconst EventHeaderSchema = z.object({\n type: z.string(),\n created_at: z.iso.datetime({ offset: true }),\n})\n\n/** Validate signed provider data before projecting it into the public event contract. */\nexport function parseEmailEvent(\n payload: WebhookEventPayload,\n id: string,\n):\n | { readonly outcome: 'event'; readonly event: EmailEvent }\n | { readonly outcome: 'ignored'; readonly type: string }\n | { readonly outcome: 'invalid' } {\n const header = EventHeaderSchema.safeParse(payload)\n if (!header.success) return { outcome: 'invalid' }\n if (!Object.hasOwn(EMAIL_TYPES, header.data.type))\n return { outcome: 'ignored', type: header.data.type }\n const parsed = EmailPayloadSchema.safeParse(payload)\n if (!parsed.success) return { outcome: 'invalid' }\n const event = parsed.data\n const base: EmailEventBase = {\n id,\n emailId: event.data.email_id,\n recipients: event.data.to,\n subject: event.data.subject,\n tags: event.data.tags,\n occurredAt: header.data.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\n outcome: 'event',\n event: {\n ...base,\n type: event.type,\n bounce: {\n type: parseBounceType(event.data.bounce.type),\n subType: event.data.bounce.subType ?? null,\n message: event.data.bounce.message,\n },\n },\n }\n case 'email.failed':\n return {\n outcome: 'event',\n event: { ...base, type: event.type, reason: event.data.failed.reason },\n }\n case 'email.clicked':\n return { outcome: 'event', event: { ...base, type: event.type, link: event.data.click.link } }\n default:\n return { outcome: 'event', event: { ...base, type: event.type } }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport {\n WebhookVerificationError,\n type WebhookVerification,\n type WebhookVerifier,\n} from '../http/webhook-verification.ts'\nimport { parseEmailEvent, type EmailEvent } from './email-event.ts'\n\n// Separate standardwebhooks versions have different prototypes; its error name remains the same.\nconst SignatureErrorSchema = z\n .instanceof(Error)\n .refine((error) => error.name === 'WebhookVerificationError')\n\n/** The SDK's local signature verifier. It makes no API request. */\nexport type ResendWebhookClient = Pick<Resend['webhooks'], 'verify'>\n\n/** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */\nexport class ResendWebhook implements WebhookVerifier<EmailEvent> {\n /** Load the SDK on the first verification. Only the webhook secret is required. */\n static create(webhookSecret: string): ResendWebhook {\n return new ResendWebhook(webhookSecret, async () => {\n const { Resend } = await import('resend')\n // The SDK requires a nonempty key even though its local verifier never uses that key.\n return new Resend('re_webhook_verification_only').webhooks\n })\n }\n\n private client: Promise<ResendWebhookClient> | undefined\n\n /** Inject the local SDK verifier when testing. */\n constructor(\n private readonly webhookSecret: string,\n private readonly verifier: () => Promise<ResendWebhookClient>,\n ) {}\n\n /** Verify the raw body and Svix headers. Invalid requests return rejected; configuration and SDK defects propagate. */\n async verify(body: string, headers: Headers): Promise<WebhookVerification<EmailEvent>> {\n const id = headers.get('svix-id')\n const timestamp = headers.get('svix-timestamp')\n const signature = headers.get('svix-signature')\n if (!id || !timestamp || !signature)\n return { outcome: 'rejected', error: new WebhookVerificationError('missing_headers') }\n const client = await (this.client ??= this.verifier())\n let payload: ReturnType<ResendWebhookClient['verify']>\n try {\n payload = client.verify({\n payload: body,\n headers: { id, timestamp, signature },\n webhookSecret: this.webhookSecret,\n })\n } catch (cause) {\n if (cause instanceof SyntaxError)\n return {\n outcome: 'rejected',\n error: new WebhookVerificationError('invalid_payload', cause),\n }\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n return {\n outcome: 'rejected',\n error: new WebhookVerificationError('invalid_signature', cause),\n }\n }\n const result = parseEmailEvent(payload, id)\n if (result.outcome === 'invalid')\n return { outcome: 'rejected', error: new WebhookVerificationError('invalid_payload') }\n return result\n }\n}\n"],"mappings":"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"}
|