better-ship 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/{application-Dg1jIKPe.js → application-D5tNECiC.js} +2 -2
  2. package/dist/application-D5tNECiC.js.map +1 -0
  3. package/dist/application.d.ts +2 -2
  4. package/dist/application.js +1 -1
  5. package/dist/better-auth/apple-client-secret.js +1 -1
  6. package/dist/better-auth/auth-client.js +1 -1
  7. package/dist/better-auth/rate-limit.js +1 -1
  8. package/dist/better-auth.js +1 -1
  9. package/dist/billing/client.js +1 -1
  10. package/dist/cloudflare/auth-secondary-storage.js +1 -1
  11. package/dist/cloudflare/durable-object-client.js +1 -1
  12. package/dist/cloudflare/durable-object-errors.js +1 -1
  13. package/dist/cloudflare/kv-client.js +1 -1
  14. package/dist/cloudflare/kv-errors.js +1 -1
  15. package/dist/cloudflare/queue.d.ts +1 -1
  16. package/dist/cloudflare/queue.js +2 -2
  17. package/dist/cloudflare/r2-client.js +1 -1
  18. package/dist/cloudflare/r2-errors.js +1 -1
  19. package/dist/cloudflare/scheduled.js +1 -1
  20. package/dist/cloudflare.d.ts +2 -2
  21. package/dist/cloudflare.js +3 -3
  22. package/dist/{core-BT1dSEQI.js → core-BIas38R0.js} +2 -2
  23. package/dist/core-BIas38R0.js.map +1 -0
  24. package/dist/core.js +1 -1
  25. package/dist/http.js +2 -2
  26. package/dist/{index-CYHXOH08.d.ts → index-tNZHSCpJ.d.ts} +6 -2
  27. package/dist/index-tNZHSCpJ.d.ts.map +1 -0
  28. package/dist/{outbox-repository-options-B8TnAE1h.js → outbox-repository-options-D0R8z1yS.js} +2 -2
  29. package/dist/{outbox-repository-options-B8TnAE1h.js.map → outbox-repository-options-D0R8z1yS.js.map} +1 -1
  30. package/dist/{outbox-repository-options-DjBgFhMi.d.ts → outbox-repository-options-_B8l8l6l.d.ts} +2 -2
  31. package/dist/{outbox-repository-options-DjBgFhMi.d.ts.map → outbox-repository-options-_B8l8l6l.d.ts.map} +1 -1
  32. package/dist/postgres.d.ts +2 -2
  33. package/dist/postgres.js +3 -3
  34. package/dist/posthog/client.d.ts +9 -8
  35. package/dist/posthog/client.d.ts.map +1 -1
  36. package/dist/posthog/client.js.map +1 -1
  37. package/dist/posthog/metrics.d.ts +1 -1
  38. package/dist/posthog/server.d.ts +1 -1
  39. package/dist/posthog/server.js +1 -1
  40. package/dist/resend.d.ts +14 -16
  41. package/dist/resend.d.ts.map +1 -1
  42. package/dist/resend.js +42 -19
  43. package/dist/resend.js.map +1 -1
  44. package/dist/sentry/client.js +1 -1
  45. package/dist/sentry/server.d.ts +1 -1
  46. package/dist/stripe/payment-service.error.js +1 -1
  47. package/dist/stripe/payment-webhook.js +1 -1
  48. package/dist/tanstack.js +1 -1
  49. package/dist/testing/outbox-repository.d.ts +1 -1
  50. package/dist/{webhook-verification-qSFXv5H3.js → webhook-verification-CEUARY9X.js} +2 -2
  51. package/dist/{webhook-verification-qSFXv5H3.js.map → webhook-verification-CEUARY9X.js.map} +1 -1
  52. package/package.json +1 -1
  53. package/src/application/email-service.ts +3 -0
  54. package/src/application/index.ts +1 -0
  55. package/src/core/email-service.error.ts +1 -1
  56. package/src/infrastructure/posthog/client.ts +10 -6
  57. package/src/infrastructure/resend/email-event.ts +3 -0
  58. package/src/infrastructure/resend/index.ts +2 -2
  59. package/src/infrastructure/resend/resend-email-service.ts +39 -17
  60. package/src/infrastructure/resend/resend-webhook.ts +15 -16
  61. package/dist/application-Dg1jIKPe.js.map +0 -1
  62. package/dist/core-BT1dSEQI.js.map +0 -1
  63. package/dist/index-CYHXOH08.d.ts.map +0 -1
@@ -1,20 +1,21 @@
1
1
  import { PostHog, PostHogConfig } from "posthog-js";
2
2
  import { PostHogProvider, usePostHog } from "@posthog/react";
3
3
  //#region src/infrastructure/posthog/client.d.ts
4
+ /** Application properties and native SDK options for browser initialization. */
5
+ export interface PostHogClientOptionsConfig {
6
+ readonly environment: string;
7
+ readonly source: string;
8
+ readonly options?: Partial<PostHogConfig>;
9
+ }
4
10
  /** The application controls enablement; disabled initialization needs no project key. */
5
11
  export type PostHogClientConfig = {
6
12
  readonly enabled: false;
7
- } | {
13
+ } | (PostHogClientOptionsConfig & {
8
14
  readonly enabled: true;
9
15
  readonly apiKey: string;
10
- readonly environment: string;
11
- readonly source: string;
12
- readonly options?: Partial<PostHogConfig>;
13
- };
16
+ });
14
17
  /** Browser options from Typist and Porte; SDK option names and application overrides stay intact. */
15
- export declare function createPostHogClientOptions(config: Extract<PostHogClientConfig, {
16
- enabled: true;
17
- }>): Partial<PostHogConfig>;
18
+ export declare function createPostHogClientOptions(config: PostHogClientOptionsConfig): Partial<PostHogConfig>;
18
19
  /** Initialize once in the browser entrypoint; pass the returned SDK client to PostHogProvider. */
19
20
  export declare function initializePostHog(config: PostHogClientConfig): PostHog;
20
21
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","names":[],"sources":["../../src/infrastructure/posthog/client.ts"],"mappings":";;;;YAGY;WACG;;WAEA;WACA;WACA;WACA;WACA,UAAU,QAAQ;;;wBAIjB,2BACd,QAAQ,QAAQ;EAAuB;KACtC,QAAQ;;wBAkBK,kBAAkB,QAAQ,sBAAsB"}
1
+ {"version":3,"file":"client.d.ts","names":[],"sources":["../../src/infrastructure/posthog/client.ts"],"mappings":";;;;iBAGiB;WACN;WACA;WACA,UAAU,QAAQ;;;YAIjB;WACG;KACV;WACU;WACA;;;wBAIC,2BACd,QAAQ,6BACP,QAAQ;;wBAkBK,kBAAkB,QAAQ,sBAAsB"}
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../src/infrastructure/posthog/client.ts"],"sourcesContent":["import posthog, { type PostHog, type PostHogConfig } from 'posthog-js'\n\n/** The application controls enablement; disabled initialization needs no project key. */\nexport type PostHogClientConfig =\n | { readonly enabled: false }\n | {\n readonly enabled: true\n readonly apiKey: string\n readonly environment: string\n readonly source: string\n readonly options?: Partial<PostHogConfig>\n }\n\n/** Browser options from Typist and Porte; SDK option names and application overrides stay intact. */\nexport function createPostHogClientOptions(\n config: Extract<PostHogClientConfig, { enabled: true }>,\n): Partial<PostHogConfig> {\n return {\n api_host: '/relay',\n ui_host: 'https://us.posthog.com',\n person_profiles: 'identified_only',\n defaults: '2026-08-29',\n disable_surveys: true,\n advanced_disable_feature_flags: true,\n capture_performance: { web_vitals: false },\n ...config.options,\n loaded: (client) => {\n client.register({ environment: config.environment, source: config.source })\n config.options?.loaded?.(client)\n },\n }\n}\n\n/** Initialize once in the browser entrypoint; pass the returned SDK client to PostHogProvider. */\nexport function initializePostHog(config: PostHogClientConfig): PostHog {\n if (!config.enabled) return posthog\n posthog.init(config.apiKey, createPostHogClientOptions(config))\n return posthog\n}\n\nexport { PostHogProvider, usePostHog } from '@posthog/react'\n"],"mappings":";;;;;AAcA,SAAgB,2BACd,QACwB;CACxB,OAAO;EACL,UAAU;EACV,SAAS;EACT,iBAAiB;EACjB,UAAU;EACV,iBAAiB;EACjB,gCAAgC;EAChC,qBAAqB,EAAE,YAAY,MAAM;EACzC,GAAG,OAAO;EACV,SAAS,WAAW;GAClB,OAAO,SAAS;IAAE,aAAa,OAAO;IAAa,QAAQ,OAAO;GAAO,CAAC;GAC1E,OAAO,SAAS,SAAS,MAAM;EACjC;CACF;AACF;;AAGA,SAAgB,kBAAkB,QAAsC;CACtE,IAAI,CAAC,OAAO,SAAS,OAAO;CAC5B,QAAQ,KAAK,OAAO,QAAQ,2BAA2B,MAAM,CAAC;CAC9D,OAAO;AACT"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../src/infrastructure/posthog/client.ts"],"sourcesContent":["import posthog, { type PostHog, type PostHogConfig } from 'posthog-js'\n\n/** Application properties and native SDK options for browser initialization. */\nexport interface PostHogClientOptionsConfig {\n readonly environment: string\n readonly source: string\n readonly options?: Partial<PostHogConfig>\n}\n\n/** The application controls enablement; disabled initialization needs no project key. */\nexport type PostHogClientConfig =\n | { readonly enabled: false }\n | (PostHogClientOptionsConfig & {\n readonly enabled: true\n readonly apiKey: string\n })\n\n/** Browser options from Typist and Porte; SDK option names and application overrides stay intact. */\nexport function createPostHogClientOptions(\n config: PostHogClientOptionsConfig,\n): Partial<PostHogConfig> {\n return {\n api_host: '/relay',\n ui_host: 'https://us.posthog.com',\n person_profiles: 'identified_only',\n defaults: '2026-08-29',\n disable_surveys: true,\n advanced_disable_feature_flags: true,\n capture_performance: { web_vitals: false },\n ...config.options,\n loaded: (client) => {\n client.register({ environment: config.environment, source: config.source })\n config.options?.loaded?.(client)\n },\n }\n}\n\n/** Initialize once in the browser entrypoint; pass the returned SDK client to PostHogProvider. */\nexport function initializePostHog(config: PostHogClientConfig): PostHog {\n if (!config.enabled) return posthog\n posthog.init(config.apiKey, createPostHogClientOptions(config))\n return posthog\n}\n\nexport { PostHogProvider, usePostHog } from '@posthog/react'\n"],"mappings":";;;;;AAkBA,SAAgB,2BACd,QACwB;CACxB,OAAO;EACL,UAAU;EACV,SAAS;EACT,iBAAiB;EACjB,UAAU;EACV,iBAAiB;EACjB,gCAAgC;EAChC,qBAAqB,EAAE,YAAY,MAAM;EACzC,GAAG,OAAO;EACV,SAAS,WAAW;GAClB,OAAO,SAAS;IAAE,aAAa,OAAO;IAAa,QAAQ,OAAO;GAAO,CAAC;GAC1E,OAAO,SAAS,SAAS,MAAM;EACjC;CACF;AACF;;AAGA,SAAgB,kBAAkB,QAAsC;CACtE,IAAI,CAAC,OAAO,SAAS,OAAO;CAC5B,QAAQ,KAAK,OAAO,QAAQ,2BAA2B,MAAM,CAAC;CAC9D,OAAO;AACT"}
@@ -1,4 +1,4 @@
1
- import { J as MetricsService, q as MetricOptions } from "../index-CYHXOH08.js";
1
+ import { J as MetricOptions, Y as MetricsService } from "../index-tNZHSCpJ.js";
2
2
  import { PostHog } from "posthog-node";
3
3
  //#region src/infrastructure/posthog/metrics.d.ts
4
4
  /** PostHog Metrics with Worker lifetime management, ported from Typist's metrics-service.ts. */
@@ -1,5 +1,5 @@
1
1
  import { Gt as AnalyticsEventIdentity, Kt as AnalyticsEventInput, qt as AnalyticsProperties } from "../index-DUFS0SJ_.js";
2
- import { X as AnalyticsService } from "../index-CYHXOH08.js";
2
+ import { Z as AnalyticsService } from "../index-tNZHSCpJ.js";
3
3
  import { PostHogMetricsService } from "./metrics.js";
4
4
  import { PostHog, PostHogOptions } from "posthog-node";
5
5
  //#region src/infrastructure/posthog/server.d.ts
@@ -1,4 +1,4 @@
1
- import { tt as MissingConfigurationError } from "../core-BT1dSEQI.js";
1
+ import { tt as MissingConfigurationError } from "../core-BIas38R0.js";
2
2
  import { PostHogMetricsService } from "./metrics.js";
3
3
  import { PostHog } from "posthog-node";
4
4
 
package/dist/resend.d.ts CHANGED
@@ -1,26 +1,25 @@
1
1
  import { jt as BounceType } from "./index-DUFS0SJ_.js";
2
- import { B as OutboundEmail, I as EmailAccepted, R as IEmailService } from "./index-CYHXOH08.js";
2
+ import { B as OutboundEmail, I as EmailAccepted, R as IEmailService, V as ReceivedEmail } from "./index-tNZHSCpJ.js";
3
3
  import { n as WebhookVerifier } from "./webhook-verification-D1c7BY4q.js";
4
- import { Resend } from "resend";
4
+ import "resend";
5
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
6
  /**
11
7
  * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.
12
8
  * The Resend SDK itself logs API errors outside production.
13
9
  */
14
10
  export declare class ResendEmailService implements IEmailService {
15
- private readonly createClient;
11
+ private readonly apiKey;
16
12
  private readonly fromAddress;
17
- /** Load the SDK on the first send. Constructing the service does not load it. */
13
+ /** Load the SDK on the first email operation. Constructing the service does not load it. */
18
14
  static create(apiKey: string, fromAddress: string): ResendEmailService;
19
15
  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);
16
+ /** Store configuration. Load the SDK and create its client on the first email operation. */
17
+ constructor(apiKey: string, fromAddress: string);
22
18
  /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
23
19
  sendEmail(email: OutboundEmail): Promise<EmailAccepted>;
20
+ /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */
21
+ getReceivedEmail(emailId: string): Promise<ReceivedEmail>;
22
+ private getClient;
24
23
  }
25
24
  //#endregion
26
25
  //#region src/infrastructure/resend/email-event.d.ts
@@ -30,6 +29,7 @@ type EmailEventType = (typeof SIMPLE_EMAIL_TYPES)[number] | 'email.bounced' | 'e
30
29
  type EmailEventBase = {
31
30
  readonly id: string;
32
31
  readonly emailId: string;
32
+ readonly messageId: string;
33
33
  readonly recipients: readonly string[];
34
34
  readonly subject: string;
35
35
  readonly tags: Readonly<Record<string, string>>;
@@ -54,20 +54,18 @@ type EmailEvent = (EmailEventBase & {
54
54
  });
55
55
  //#endregion
56
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
57
  /** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */
60
58
  export declare class ResendWebhook implements WebhookVerifier<EmailEvent> {
61
59
  private readonly webhookSecret;
62
- private readonly verifier;
63
60
  /** Load the SDK on the first verification. Only the webhook secret is required. */
64
61
  static create(webhookSecret: string): ResendWebhook;
65
62
  private client;
66
- /** Inject the local SDK verifier when testing. */
67
- constructor(webhookSecret: string, verifier: () => Promise<ResendWebhookClient>);
63
+ /** Store the webhook secret. Load the SDK on the first verification. */
64
+ constructor(webhookSecret: string);
68
65
  /** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */
69
66
  verify(body: string, headers: Headers): Promise<EmailEvent | null>;
67
+ private getClient;
70
68
  }
71
69
  //#endregion
72
- export type { EmailEvent, EmailEventType, ResendEmailClient, ResendWebhookClient };
70
+ export type { EmailEvent, EmailEventType };
73
71
  //# sourceMappingURL=resend.d.ts.map
@@ -1 +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;;;;;KC3EnC,sBAAsB,KAAK;;qBAG1B,yBAAyB,gBAAgB;mBAcjC;mBACA;;SAbZ,OAAO,wBAAwB;UAQ9B;;EAGR,YACmB,uBACA,gBAAgB,QAAQ;;EAIrC,OAAO,cAAc,SAAS,UAAU,QAAQ"}
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":";;;;;;;;;qBA+Ea,8BAA8B;mBAUtB;mBACA;;SATZ,OAAO,gBAAgB,sBAAsB;UAI5C;;EAGR,YACmB,gBACA;;EAIb,UAAU,OAAO,gBAAgB,QAAQ;;EA+BzC,iBAAiB,kBAAkB,QAAQ;UAgBnC;;;;cCxIV;;KAYM,yBACA;KAiDP;WACM;WACA;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;;;;;qBC7ElC,yBAAyB,gBAAgB;mBASvB;;SAPtB,OAAO,wBAAwB;UAI9B;;EAGR,YAA6B;;EAGvB,OAAO,cAAc,SAAS,UAAU,QAAQ;UA0BxC"}
package/dist/resend.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as EmailAddressSchema, J as parseBounceType, X as EmailServiceError } from "./core-BT1dSEQI.js";
2
- import { t as WebhookVerificationError } from "./webhook-verification-qSFXv5H3.js";
1
+ import { $ as EmailAddressSchema, J as parseBounceType, X as EmailServiceError } from "./core-BIas38R0.js";
2
+ import { t as WebhookVerificationError } from "./webhook-verification-CEUARY9X.js";
3
3
  import { z } from "zod";
4
4
  import { Buffer } from "node:buffer";
5
5
 
@@ -61,22 +61,21 @@ function toAttachment(attachment) {
61
61
  * The Resend SDK itself logs API errors outside production.
62
62
  */
63
63
  var ResendEmailService = class ResendEmailService {
64
- createClient;
64
+ apiKey;
65
65
  fromAddress;
66
- /** Load the SDK on the first send. Constructing the service does not load it. */
66
+ /** Load the SDK on the first email operation. Constructing the service does not load it. */
67
67
  static create(apiKey, fromAddress) {
68
- return new ResendEmailService(() => import("resend").then(({ Resend }) => new Resend(apiKey)), fromAddress);
68
+ return new ResendEmailService(apiKey, fromAddress);
69
69
  }
70
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;
71
+ /** Store configuration. Load the SDK and create its client on the first email operation. */
72
+ constructor(apiKey, fromAddress) {
73
+ this.apiKey = apiKey;
74
74
  this.fromAddress = fromAddress;
75
75
  }
76
76
  /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
77
77
  async sendEmail(email) {
78
- if (this.client === void 0) this.client = Promise.resolve().then(this.createClient);
79
- const client = await this.client;
78
+ const client = await this.getClient();
80
79
  const payload = {
81
80
  from: email.from ?? this.fromAddress,
82
81
  to: [email.to].flat(),
@@ -101,6 +100,26 @@ var ResendEmailService = class ResendEmailService {
101
100
  });
102
101
  return { providerId: result.data.id };
103
102
  }
103
+ /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */
104
+ async getReceivedEmail(emailId) {
105
+ const result = await (await this.getClient()).emails.receiving.get(emailId);
106
+ if (result.error !== null) throw new EmailServiceError({
107
+ reason: reasonFromResend(result.error),
108
+ provider: "resend",
109
+ providerCode: result.error.name,
110
+ statusCode: result.error.statusCode,
111
+ retryAfter: retryAfterSeconds(result.headers?.["retry-after"]),
112
+ cause: result.error
113
+ });
114
+ return { headers: result.data.headers ?? {} };
115
+ }
116
+ async getClient() {
117
+ if (this.client === void 0) {
118
+ const { Resend } = await import("resend");
119
+ this.client ??= new Resend(this.apiKey);
120
+ }
121
+ return this.client;
122
+ }
104
123
  };
105
124
 
106
125
  //#endregion
@@ -130,6 +149,7 @@ const EMAIL_TYPES = {
130
149
  };
131
150
  const BaseDataSchema = z.object({
132
151
  email_id: z.string().min(1),
152
+ message_id: z.string().min(1),
133
153
  to: z.array(EmailAddressSchema).min(1),
134
154
  subject: z.string(),
135
155
  tags: z.record(z.string(), z.string()).default({})
@@ -168,6 +188,7 @@ function parseEmailEvent(payload, id) {
168
188
  const base = {
169
189
  id,
170
190
  emailId: event.data.email_id,
191
+ messageId: event.data.message_id,
171
192
  recipients: event.data.to,
172
193
  subject: event.data.subject,
173
194
  tags: event.data.tags,
@@ -206,19 +227,14 @@ const SignatureErrorSchema = z.instanceof(Error).refine((error) => error.name ==
206
227
  /** Verify Svix signatures and map Resend email events. Ported from Typist's Resend route. */
207
228
  var ResendWebhook = class ResendWebhook {
208
229
  webhookSecret;
209
- verifier;
210
230
  /** Load the SDK on the first verification. Only the webhook secret is required. */
211
231
  static create(webhookSecret) {
212
- return new ResendWebhook(webhookSecret, async () => {
213
- const { Resend } = await import("resend");
214
- return new Resend("re_webhook_verification_only").webhooks;
215
- });
232
+ return new ResendWebhook(webhookSecret);
216
233
  }
217
234
  client;
218
- /** Inject the local SDK verifier when testing. */
219
- constructor(webhookSecret, verifier) {
235
+ /** Store the webhook secret. Load the SDK on the first verification. */
236
+ constructor(webhookSecret) {
220
237
  this.webhookSecret = webhookSecret;
221
- this.verifier = verifier;
222
238
  }
223
239
  /** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */
224
240
  async verify(body, headers) {
@@ -226,7 +242,7 @@ var ResendWebhook = class ResendWebhook {
226
242
  const timestamp = headers.get("svix-timestamp");
227
243
  const signature = headers.get("svix-signature");
228
244
  if (!id || !timestamp || !signature) throw new WebhookVerificationError("missing_headers");
229
- const client = await (this.client ??= this.verifier());
245
+ const client = await this.getClient();
230
246
  let payload;
231
247
  try {
232
248
  payload = client.verify({
@@ -250,6 +266,13 @@ var ResendWebhook = class ResendWebhook {
250
266
  throw cause;
251
267
  }
252
268
  }
269
+ async getClient() {
270
+ if (this.client === void 0) {
271
+ const { Resend } = await import("resend");
272
+ this.client ??= new Resend("re_webhook_verification_only").webhooks;
273
+ }
274
+ return this.client;
275
+ }
253
276
  };
254
277
 
255
278
  //#endregion
@@ -1 +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(\n payload,\n email.idempotencyKey === undefined ? undefined : { idempotencyKey: email.idempotencyKey },\n )\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(payload: WebhookEventPayload, id: string): EmailEvent | null {\n const header = EventHeaderSchema.parse(payload)\n if (!Object.hasOwn(EMAIL_TYPES, header.type)) return null\n const event = EmailPayloadSchema.parse(payload)\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.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\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 case 'email.failed':\n return { ...base, type: event.type, reason: event.data.failed.reason }\n case 'email.clicked':\n return { ...base, type: event.type, link: event.data.click.link }\n default:\n return { ...base, type: event.type }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport { WebhookVerificationError, type WebhookVerifier } 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 throw typed errors; other failures propagate. */\n async verify(body: string, headers: Headers): Promise<EmailEvent | null> {\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) throw 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) throw new WebhookVerificationError('invalid_payload', cause)\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n throw new WebhookVerificationError('invalid_signature', cause)\n }\n try {\n return parseEmailEvent(payload, id)\n } catch (cause) {\n if (cause instanceof z.ZodError) throw new WebhookVerificationError('invalid_payload', cause)\n throw cause\n }\n }\n}\n"],"mappings":"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"}
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 {\n EmailAccepted,\n EmailAttachment,\n IEmailService,\n OutboundEmail,\n ReceivedEmail,\n} 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/**\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 email operation. Constructing the service does not load it. */\n static create(apiKey: string, fromAddress: string): ResendEmailService {\n return new ResendEmailService(apiKey, fromAddress)\n }\n\n private client: Resend | undefined\n\n /** Store configuration. Load the SDK and create its client on the first email operation. */\n constructor(\n private readonly apiKey: string,\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 const client = await this.getClient()\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(\n payload,\n email.idempotencyKey === undefined ? undefined : { idempotencyKey: email.idempotencyKey },\n )\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 /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */\n async getReceivedEmail(emailId: string): Promise<ReceivedEmail> {\n const client = await this.getClient()\n const result = await client.emails.receiving.get(emailId)\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 { headers: result.data.headers ?? {} }\n }\n\n private async getClient(): Promise<Resend> {\n if (this.client === undefined) {\n const { Resend } = await import('resend')\n // Recheck after the import so concurrent calls share one client.\n this.client ??= new Resend(this.apiKey)\n }\n return this.client\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 message_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 messageId: 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(payload: WebhookEventPayload, id: string): EmailEvent | null {\n const header = EventHeaderSchema.parse(payload)\n if (!Object.hasOwn(EMAIL_TYPES, header.type)) return null\n const event = EmailPayloadSchema.parse(payload)\n const base: EmailEventBase = {\n id,\n emailId: event.data.email_id,\n messageId: event.data.message_id,\n recipients: event.data.to,\n subject: event.data.subject,\n tags: event.data.tags,\n occurredAt: header.created_at,\n }\n switch (event.type) {\n case 'email.bounced':\n return {\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 case 'email.failed':\n return { ...base, type: event.type, reason: event.data.failed.reason }\n case 'email.clicked':\n return { ...base, type: event.type, link: event.data.click.link }\n default:\n return { ...base, type: event.type }\n }\n}\n","import type { Resend } from 'resend'\nimport { z } from 'zod'\n\nimport { WebhookVerificationError, type WebhookVerifier } 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/** 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)\n }\n\n private client: Resend['webhooks'] | undefined\n\n /** Store the webhook secret. Load the SDK on the first verification. */\n constructor(private readonly webhookSecret: string) {}\n\n /** Verify the raw body and Svix headers. Invalid requests throw typed errors; other failures propagate. */\n async verify(body: string, headers: Headers): Promise<EmailEvent | null> {\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) throw new WebhookVerificationError('missing_headers')\n const client = await this.getClient()\n let payload: ReturnType<Resend['webhooks']['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) throw new WebhookVerificationError('invalid_payload', cause)\n if (!SignatureErrorSchema.safeParse(cause).success) throw cause\n throw new WebhookVerificationError('invalid_signature', cause)\n }\n try {\n return parseEmailEvent(payload, id)\n } catch (cause) {\n if (cause instanceof z.ZodError) throw new WebhookVerificationError('invalid_payload', cause)\n throw cause\n }\n }\n\n private async getClient(): Promise<Resend['webhooks']> {\n if (this.client === undefined) {\n const { Resend } = await import('resend')\n // The SDK requires a nonempty key even though its local verifier never uses that key.\n this.client ??= new Resend('re_webhook_verification_only').webhooks\n }\n return this.client\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { a as setLoggerErrorHook } from "../core-BT1dSEQI.js";
1
+ import { a as setLoggerErrorHook } from "../core-BIas38R0.js";
2
2
  import * as Sentry from "@sentry/tanstackstart-react";
3
3
  import { isNotFound, isRedirect } from "@tanstack/react-router";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { u as LoggerErrorHook } from "../index-DUFS0SJ_.js";
2
- import { H as ObservabilityScope, U as ObservabilityService, W as ObservabilityUser } from "../index-CYHXOH08.js";
2
+ import { G as ObservabilityUser, U as ObservabilityScope, W as ObservabilityService } from "../index-tNZHSCpJ.js";
3
3
  import * as Sentry from "@sentry/cloudflare";
4
4
  //#region src/infrastructure/sentry/server.d.ts
5
5
  /** Application-owned deployment settings with the full Cloudflare SDK option contract. */
@@ -1,4 +1,4 @@
1
- import { nt as AppError } from "../core-BT1dSEQI.js";
1
+ import { nt as AppError } from "../core-BIas38R0.js";
2
2
  import Stripe from "stripe";
3
3
 
4
4
  //#region src/infrastructure/stripe/payment-service.error.ts
@@ -1,4 +1,4 @@
1
- import { t as WebhookVerificationError } from "../webhook-verification-qSFXv5H3.js";
1
+ import { t as WebhookVerificationError } from "../webhook-verification-CEUARY9X.js";
2
2
  import Stripe from "stripe";
3
3
 
4
4
  //#region src/infrastructure/stripe/payment-webhook.ts
package/dist/tanstack.js CHANGED
@@ -1,4 +1,4 @@
1
- import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-BT1dSEQI.js";
1
+ import { H as StaleSessionError, R as ValidationError, W as AuthenticationError, nt as AppError, r as createLogger } from "./core-BIas38R0.js";
2
2
  import { z } from "zod";
3
3
  import { getReasonPhrase } from "http-status-codes";
4
4
  import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
@@ -1,5 +1,5 @@
1
1
  import { W as DomainEvent } from "../index-DUFS0SJ_.js";
2
- import { A as IOutboxRepository } from "../index-CYHXOH08.js";
2
+ import { A as IOutboxRepository } from "../index-tNZHSCpJ.js";
3
3
  //#region src/infrastructure/testing/outbox-repository-contract.d.ts
4
4
  /** Stored fields observed by the adapter contract, independent of a database library. */
5
5
  export type OutboxContractRow = {
@@ -1,4 +1,4 @@
1
- import { nt as AppError } from "./core-BT1dSEQI.js";
1
+ import { nt as AppError } from "./core-BIas38R0.js";
2
2
 
3
3
  //#region src/infrastructure/http/webhook-verification.ts
4
4
  /** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */
@@ -15,4 +15,4 @@ var WebhookVerificationError = class extends AppError {
15
15
 
16
16
  //#endregion
17
17
  export { WebhookVerificationError as t };
18
- //# sourceMappingURL=webhook-verification-qSFXv5H3.js.map
18
+ //# sourceMappingURL=webhook-verification-CEUARY9X.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhook-verification-qSFXv5H3.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Verified> {\n /**\n * Verify the exact body before parsing it. Return null for an ignored webhook.\n * @throws WebhookVerificationError when the request cannot be trusted or parsed.\n */\n verify(body: string, headers: Headers): Promise<Verified | null>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
1
+ {"version":3,"file":"webhook-verification-CEUARY9X.js","names":[],"sources":["../src/infrastructure/http/webhook-verification.ts"],"sourcesContent":["import { AppError } from '@/core'\n\n/** A webhook cannot be trusted or parsed. The HTTP handler logs the rejection once. */\nexport class WebhookVerificationError extends AppError {\n readonly _tag = 'WebhookVerificationError'\n readonly classification = 'terminal'\n\n /** Keep secrets and payload details out of the public message. */\n constructor(\n readonly reason: 'missing_headers' | 'invalid_signature' | 'invalid_payload',\n cause?: unknown,\n ) {\n super(`Webhook rejected: ${reason}`, { cause })\n }\n}\n\n/** The provider owns signature verification, timestamp tolerance, and payload parsing. */\nexport interface WebhookVerifier<Verified> {\n /**\n * Verify the exact body before parsing it. Return null for an ignored webhook.\n * @throws WebhookVerificationError when the request cannot be trusted or parsed.\n */\n verify(body: string, headers: Headers): Promise<Verified | null>\n}\n"],"mappings":";;;;AAGA,IAAa,2BAAb,cAA8C,SAAS;CAM1C;CALX,AAAS,OAAO;CAChB,AAAS,iBAAiB;;CAG1B,YACE,AAAS,QACT,OACA;EACA,MAAM,qBAAqB,UAAU,EAAE,MAAM,CAAC;EAHrC;CAIX;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.19.0",
4
+ "version": "0.21.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -32,6 +32,9 @@ export type OutboundEmail = {
32
32
  readonly attachments?: readonly EmailAttachment[]
33
33
  }
34
34
 
35
+ /** Received email headers, without the body or attachments. */
36
+ export type ReceivedEmail = { readonly headers: Readonly<Record<string, string>> }
37
+
35
38
  /** The provider accepted the email and assigned its id. */
36
39
  export type EmailAccepted = { readonly providerId: string }
37
40
 
@@ -19,6 +19,7 @@ export {
19
19
  type EmailAttachment,
20
20
  type MimeType,
21
21
  type EmailAccepted,
22
+ type ReceivedEmail,
22
23
  } from './email-service.ts'
23
24
  export {
24
25
  recordEmailEvent,
@@ -40,7 +40,7 @@ export class EmailServiceError extends AppError {
40
40
  readonly retryAfter: number | null
41
41
  readonly cause: unknown
42
42
  }) {
43
- super(`Email send failed: ${args.provider} ${args.providerCode}`, { cause: args.cause })
43
+ super(`Email request failed: ${args.provider} ${args.providerCode}`, { cause: args.cause })
44
44
  this.reason = args.reason
45
45
  this.classification = CLASSIFICATION_BY_REASON[args.reason]
46
46
  this.provider = args.provider
@@ -1,19 +1,23 @@
1
1
  import posthog, { type PostHog, type PostHogConfig } from 'posthog-js'
2
2
 
3
+ /** Application properties and native SDK options for browser initialization. */
4
+ export interface PostHogClientOptionsConfig {
5
+ readonly environment: string
6
+ readonly source: string
7
+ readonly options?: Partial<PostHogConfig>
8
+ }
9
+
3
10
  /** The application controls enablement; disabled initialization needs no project key. */
4
11
  export type PostHogClientConfig =
5
12
  | { readonly enabled: false }
6
- | {
13
+ | (PostHogClientOptionsConfig & {
7
14
  readonly enabled: true
8
15
  readonly apiKey: string
9
- readonly environment: string
10
- readonly source: string
11
- readonly options?: Partial<PostHogConfig>
12
- }
16
+ })
13
17
 
14
18
  /** Browser options from Typist and Porte; SDK option names and application overrides stay intact. */
15
19
  export function createPostHogClientOptions(
16
- config: Extract<PostHogClientConfig, { enabled: true }>,
20
+ config: PostHogClientOptionsConfig,
17
21
  ): Partial<PostHogConfig> {
18
22
  return {
19
23
  api_host: '/relay',
@@ -37,6 +37,7 @@ const EMAIL_TYPES = {
37
37
 
38
38
  const BaseDataSchema = z.object({
39
39
  email_id: z.string().min(1),
40
+ message_id: z.string().min(1),
40
41
  to: z.array(EmailAddressSchema).min(1),
41
42
  subject: z.string(),
42
43
  tags: z.record(z.string(), z.string()).default({}),
@@ -67,6 +68,7 @@ const EmailPayloadSchema = z.discriminatedUnion('type', [
67
68
  type EmailEventBase = {
68
69
  readonly id: string
69
70
  readonly emailId: string
71
+ readonly messageId: string
70
72
  readonly recipients: readonly string[]
71
73
  readonly subject: string
72
74
  readonly tags: Readonly<Record<string, string>>
@@ -100,6 +102,7 @@ export function parseEmailEvent(payload: WebhookEventPayload, id: string): Email
100
102
  const base: EmailEventBase = {
101
103
  id,
102
104
  emailId: event.data.email_id,
105
+ messageId: event.data.message_id,
103
106
  recipients: event.data.to,
104
107
  subject: event.data.subject,
105
108
  tags: event.data.tags,
@@ -1,3 +1,3 @@
1
- export { ResendEmailService, type ResendEmailClient } from './resend-email-service.ts'
2
- export { ResendWebhook, type ResendWebhookClient } from './resend-webhook.ts'
1
+ export { ResendEmailService } from './resend-email-service.ts'
2
+ export { ResendWebhook } from './resend-webhook.ts'
3
3
  export type { EmailEvent, EmailEventType } from './email-event.ts'
@@ -2,7 +2,13 @@ import { Buffer } from 'node:buffer'
2
2
 
3
3
  import type { Attachment, CreateEmailOptions, ErrorResponse, Resend } from 'resend'
4
4
 
5
- import type { EmailAccepted, EmailAttachment, IEmailService, OutboundEmail } from '@/application'
5
+ import type {
6
+ EmailAccepted,
7
+ EmailAttachment,
8
+ IEmailService,
9
+ OutboundEmail,
10
+ ReceivedEmail,
11
+ } from '@/application'
6
12
  import { EmailServiceError, type EmailSendFailureReason } from '@/core'
7
13
 
8
14
  // SDK additions must receive an explicit classification before the package builds.
@@ -67,37 +73,27 @@ function toAttachment(attachment: EmailAttachment): Attachment {
67
73
  return result
68
74
  }
69
75
 
70
- /** The SDK operations required by the adapter; a test factory can return a plain object. */
71
- export type ResendEmailClient = { readonly emails: Pick<Resend['emails'], 'send'> }
72
-
73
76
  /**
74
77
  * Send without package retries or logs. Ported from Suparelay's resend-email-service.ts.
75
78
  * The Resend SDK itself logs API errors outside production.
76
79
  */
77
80
  export class ResendEmailService implements IEmailService {
78
- /** Load the SDK on the first send. Constructing the service does not load it. */
81
+ /** Load the SDK on the first email operation. Constructing the service does not load it. */
79
82
  static create(apiKey: string, fromAddress: string): ResendEmailService {
80
- return new ResendEmailService(
81
- () => import('resend').then(({ Resend }) => new Resend(apiKey)),
82
- fromAddress,
83
- )
83
+ return new ResendEmailService(apiKey, fromAddress)
84
84
  }
85
85
 
86
- private client: Promise<ResendEmailClient> | undefined
86
+ private client: Resend | undefined
87
87
 
88
- /** Supply an async factory. The service calls it once, on the first send, and keeps its promise. */
88
+ /** Store configuration. Load the SDK and create its client on the first email operation. */
89
89
  constructor(
90
- private readonly createClient: () => Promise<ResendEmailClient>,
90
+ private readonly apiKey: string,
91
91
  private readonly fromAddress: string,
92
92
  ) {}
93
93
 
94
94
  /** Resolve after acceptance. Provider failures throw EmailServiceError; SDK defects retain their identity. */
95
95
  async sendEmail(email: OutboundEmail): Promise<EmailAccepted> {
96
- if (this.client === undefined) {
97
- // Cache synchronous factory throws as rejections too, so later sends cannot initialize again.
98
- this.client = Promise.resolve().then(this.createClient)
99
- }
100
- const client = await this.client
96
+ const client = await this.getClient()
101
97
  const payload: CreateEmailOptions = {
102
98
  from: email.from ?? this.fromAddress,
103
99
  to: [email.to].flat(),
@@ -125,4 +121,30 @@ export class ResendEmailService implements IEmailService {
125
121
  }
126
122
  return { providerId: result.data.id }
127
123
  }
124
+
125
+ /** Retrieve headers by the received email's provider ID. Provider failures throw EmailServiceError. */
126
+ async getReceivedEmail(emailId: string): Promise<ReceivedEmail> {
127
+ const client = await this.getClient()
128
+ const result = await client.emails.receiving.get(emailId)
129
+ if (result.error !== null) {
130
+ throw new EmailServiceError({
131
+ reason: reasonFromResend(result.error),
132
+ provider: 'resend',
133
+ providerCode: result.error.name,
134
+ statusCode: result.error.statusCode,
135
+ retryAfter: retryAfterSeconds(result.headers?.['retry-after']),
136
+ cause: result.error,
137
+ })
138
+ }
139
+ return { headers: result.data.headers ?? {} }
140
+ }
141
+
142
+ private async getClient(): Promise<Resend> {
143
+ if (this.client === undefined) {
144
+ const { Resend } = await import('resend')
145
+ // Recheck after the import so concurrent calls share one client.
146
+ this.client ??= new Resend(this.apiKey)
147
+ }
148
+ return this.client
149
+ }
128
150
  }