@wopr-network/platform-core 1.61.2 → 1.63.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.
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Email Client — Template-based transactional email sender.
3
3
  *
4
- * Supports two backends:
5
- * - **Resend**: Set RESEND_API_KEY env var
6
- * - **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
7
- *
8
- * SES takes priority when both are configured.
4
+ * Supports three backends (first match wins):
5
+ * 1. **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
6
+ * 2. **Postmark**: Set POSTMARK_API_KEY env var
7
+ * 3. **Resend**: Set RESEND_API_KEY env var
9
8
  */
10
9
  export interface EmailClientConfig {
11
10
  apiKey: string;
@@ -49,7 +48,20 @@ export declare class EmailClient {
49
48
  /** Send a transactional email. */
50
49
  send(opts: SendTemplateEmailOpts): Promise<EmailSendResult>;
51
50
  }
52
- export declare function getEmailClient(): EmailClient;
51
+ export interface EmailClientOverrides {
52
+ /** Sender address — overrides EMAIL_FROM env var. */
53
+ from?: string;
54
+ /** Reply-to address — overrides EMAIL_REPLY_TO env var. */
55
+ replyTo?: string;
56
+ }
57
+ /**
58
+ * Create a lazily-initialized singleton EmailClient.
59
+ *
60
+ * Optional overrides (from DB-driven product config) take precedence
61
+ * over env vars. Pass them on first call; subsequent calls return the
62
+ * cached singleton.
63
+ */
64
+ export declare function getEmailClient(overrides?: EmailClientOverrides): EmailClient;
53
65
  /** Reset the singleton (for testing). */
54
66
  export declare function resetEmailClient(): void;
55
67
  /** Replace the singleton (for testing). */
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Email Client — Template-based transactional email sender.
3
3
  *
4
- * Supports two backends:
5
- * - **Resend**: Set RESEND_API_KEY env var
6
- * - **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
7
- *
8
- * SES takes priority when both are configured.
4
+ * Supports three backends (first match wins):
5
+ * 1. **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
6
+ * 2. **Postmark**: Set POSTMARK_API_KEY env var
7
+ * 3. **Resend**: Set RESEND_API_KEY env var
9
8
  */
10
9
  import { Resend } from "resend";
11
10
  import { logger } from "../config/logger.js";
11
+ import { PostmarkTransport } from "./postmark-transport.js";
12
12
  import { SesTransport } from "./ses-transport.js";
13
13
  /**
14
14
  * Transactional email client with pluggable transport.
@@ -94,7 +94,8 @@ class ResendTransport {
94
94
  *
95
95
  * Backend selection (first match wins):
96
96
  * 1. AWS SES — AWS_SES_REGION is set
97
- * 2. ResendRESEND_API_KEY is set
97
+ * 2. PostmarkPOSTMARK_API_KEY is set
98
+ * 3. Resend — RESEND_API_KEY is set
98
99
  *
99
100
  * Common env vars:
100
101
  * - EMAIL_FROM (default: "noreply@wopr.bot") — sender address
@@ -105,6 +106,9 @@ class ResendTransport {
105
106
  * - AWS_ACCESS_KEY_ID
106
107
  * - AWS_SECRET_ACCESS_KEY
107
108
  *
109
+ * Postmark env vars:
110
+ * - POSTMARK_API_KEY (server token from Postmark dashboard)
111
+ *
108
112
  * Resend env vars:
109
113
  * - RESEND_API_KEY
110
114
  *
@@ -113,10 +117,17 @@ class ResendTransport {
113
117
  * - RESEND_REPLY_TO → falls back if EMAIL_REPLY_TO is not set
114
118
  */
115
119
  let _client = null;
116
- export function getEmailClient() {
120
+ /**
121
+ * Create a lazily-initialized singleton EmailClient.
122
+ *
123
+ * Optional overrides (from DB-driven product config) take precedence
124
+ * over env vars. Pass them on first call; subsequent calls return the
125
+ * cached singleton.
126
+ */
127
+ export function getEmailClient(overrides) {
117
128
  if (!_client) {
118
- const from = process.env.EMAIL_FROM || process.env.RESEND_FROM || "noreply@wopr.bot";
119
- const replyTo = process.env.EMAIL_REPLY_TO || process.env.RESEND_REPLY_TO || "support@wopr.bot";
129
+ const from = overrides?.from || process.env.EMAIL_FROM || process.env.RESEND_FROM || "noreply@wopr.bot";
130
+ const replyTo = overrides?.replyTo || process.env.EMAIL_REPLY_TO || process.env.RESEND_REPLY_TO || "support@wopr.bot";
120
131
  const sesRegion = process.env.AWS_SES_REGION;
121
132
  if (sesRegion) {
122
133
  const transport = new SesTransport({
@@ -129,10 +140,19 @@ export function getEmailClient() {
129
140
  _client = new EmailClient(transport);
130
141
  logger.info("Email client initialized with AWS SES", { region: sesRegion, from });
131
142
  }
143
+ else if (process.env.POSTMARK_API_KEY) {
144
+ const transport = new PostmarkTransport({
145
+ apiKey: process.env.POSTMARK_API_KEY,
146
+ from,
147
+ replyTo,
148
+ });
149
+ _client = new EmailClient(transport);
150
+ logger.info("Email client initialized with Postmark", { from });
151
+ }
132
152
  else {
133
153
  const apiKey = process.env.RESEND_API_KEY;
134
154
  if (!apiKey) {
135
- throw new Error("Either AWS_SES_REGION or RESEND_API_KEY environment variable is required");
155
+ throw new Error("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY environment variable");
136
156
  }
137
157
  _client = new EmailClient({ apiKey, from, replyTo });
138
158
  logger.info("Email client initialized with Resend", { from });
@@ -91,8 +91,8 @@ describe("getEmailClient / setEmailClient / resetEmailClient", () => {
91
91
  delete process.env.RESEND_FROM;
92
92
  delete process.env.RESEND_REPLY_TO;
93
93
  });
94
- it("should throw if RESEND_API_KEY is not set", () => {
95
- expect(() => getEmailClient()).toThrow("RESEND_API_KEY environment variable is required");
94
+ it("should throw if no email provider is configured", () => {
95
+ expect(() => getEmailClient()).toThrow("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY");
96
96
  });
97
97
  it("should create client from env vars", () => {
98
98
  process.env.RESEND_API_KEY = "re_test123";
@@ -10,7 +10,7 @@
10
10
  */
11
11
  export type { BillingEmailServiceConfig, BillingEmailType } from "./billing-emails.js";
12
12
  export { BillingEmailService } from "./billing-emails.js";
13
- export type { EmailClientConfig, EmailSendResult, SendTemplateEmailOpts } from "./client.js";
13
+ export type { EmailClientConfig, EmailClientOverrides, EmailSendResult, SendTemplateEmailOpts } from "./client.js";
14
14
  export { EmailClient, getEmailClient, resetEmailClient, setEmailClient } from "./client.js";
15
15
  export { DEFAULT_TEMPLATES } from "./default-templates.js";
16
16
  export type { IBillingEmailRepository } from "./drizzle-billing-email-repository.js";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Postmark email transport.
3
+ *
4
+ * Env vars:
5
+ * POSTMARK_API_KEY — server API token from Postmark
6
+ */
7
+ import type { EmailSendResult, EmailTransport, SendTemplateEmailOpts } from "./client.js";
8
+ export interface PostmarkTransportConfig {
9
+ apiKey: string;
10
+ from: string;
11
+ replyTo?: string;
12
+ }
13
+ export declare class PostmarkTransport implements EmailTransport {
14
+ private client;
15
+ private from;
16
+ private replyTo;
17
+ constructor(config: PostmarkTransportConfig);
18
+ send(opts: SendTemplateEmailOpts): Promise<EmailSendResult>;
19
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Postmark email transport.
3
+ *
4
+ * Env vars:
5
+ * POSTMARK_API_KEY — server API token from Postmark
6
+ */
7
+ import { ServerClient } from "postmark";
8
+ import { logger } from "../config/logger.js";
9
+ export class PostmarkTransport {
10
+ client;
11
+ from;
12
+ replyTo;
13
+ constructor(config) {
14
+ this.client = new ServerClient(config.apiKey);
15
+ this.from = config.from;
16
+ this.replyTo = config.replyTo;
17
+ }
18
+ async send(opts) {
19
+ const result = await this.client.sendEmail({
20
+ From: this.from,
21
+ To: opts.to,
22
+ Subject: opts.subject,
23
+ HtmlBody: opts.html,
24
+ TextBody: opts.text,
25
+ ReplyTo: this.replyTo,
26
+ MessageStream: "outbound",
27
+ });
28
+ if (result.ErrorCode !== 0) {
29
+ logger.error("Failed to send email via Postmark", {
30
+ to: opts.to,
31
+ template: opts.templateName,
32
+ error: result.Message,
33
+ code: result.ErrorCode,
34
+ });
35
+ throw new Error(`Postmark error ${result.ErrorCode}: ${result.Message}`);
36
+ }
37
+ logger.info("Email sent via Postmark", {
38
+ emailId: result.MessageID,
39
+ to: opts.to,
40
+ template: opts.templateName,
41
+ userId: opts.userId,
42
+ });
43
+ return { id: result.MessageID, success: true };
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.61.2",
3
+ "version": "1.63.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -138,6 +138,7 @@
138
138
  "@scure/bip39": "^2.0.1",
139
139
  "handlebars": "^4.7.8",
140
140
  "js-yaml": "^4.1.1",
141
+ "postmark": "^4.0.7",
141
142
  "viem": "^2.47.4",
142
143
  "yaml": "^2.8.2"
143
144
  }
@@ -116,8 +116,8 @@ describe("getEmailClient / setEmailClient / resetEmailClient", () => {
116
116
  delete process.env.RESEND_REPLY_TO;
117
117
  });
118
118
 
119
- it("should throw if RESEND_API_KEY is not set", () => {
120
- expect(() => getEmailClient()).toThrow("RESEND_API_KEY environment variable is required");
119
+ it("should throw if no email provider is configured", () => {
120
+ expect(() => getEmailClient()).toThrow("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY");
121
121
  });
122
122
 
123
123
  it("should create client from env vars", () => {
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Email Client — Template-based transactional email sender.
3
3
  *
4
- * Supports two backends:
5
- * - **Resend**: Set RESEND_API_KEY env var
6
- * - **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
7
- *
8
- * SES takes priority when both are configured.
4
+ * Supports three backends (first match wins):
5
+ * 1. **AWS SES**: Set AWS_SES_REGION env var (+ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
6
+ * 2. **Postmark**: Set POSTMARK_API_KEY env var
7
+ * 3. **Resend**: Set RESEND_API_KEY env var
9
8
  */
10
9
 
11
10
  import { Resend } from "resend";
12
11
  import { logger } from "../config/logger.js";
12
+ import { PostmarkTransport } from "./postmark-transport.js";
13
13
  import { SesTransport } from "./ses-transport.js";
14
14
 
15
15
  export interface EmailClientConfig {
@@ -134,7 +134,8 @@ class ResendTransport implements EmailTransport {
134
134
  *
135
135
  * Backend selection (first match wins):
136
136
  * 1. AWS SES — AWS_SES_REGION is set
137
- * 2. ResendRESEND_API_KEY is set
137
+ * 2. PostmarkPOSTMARK_API_KEY is set
138
+ * 3. Resend — RESEND_API_KEY is set
138
139
  *
139
140
  * Common env vars:
140
141
  * - EMAIL_FROM (default: "noreply@wopr.bot") — sender address
@@ -145,6 +146,9 @@ class ResendTransport implements EmailTransport {
145
146
  * - AWS_ACCESS_KEY_ID
146
147
  * - AWS_SECRET_ACCESS_KEY
147
148
  *
149
+ * Postmark env vars:
150
+ * - POSTMARK_API_KEY (server token from Postmark dashboard)
151
+ *
148
152
  * Resend env vars:
149
153
  * - RESEND_API_KEY
150
154
  *
@@ -154,10 +158,25 @@ class ResendTransport implements EmailTransport {
154
158
  */
155
159
  let _client: EmailClient | null = null;
156
160
 
157
- export function getEmailClient(): EmailClient {
161
+ export interface EmailClientOverrides {
162
+ /** Sender address — overrides EMAIL_FROM env var. */
163
+ from?: string;
164
+ /** Reply-to address — overrides EMAIL_REPLY_TO env var. */
165
+ replyTo?: string;
166
+ }
167
+
168
+ /**
169
+ * Create a lazily-initialized singleton EmailClient.
170
+ *
171
+ * Optional overrides (from DB-driven product config) take precedence
172
+ * over env vars. Pass them on first call; subsequent calls return the
173
+ * cached singleton.
174
+ */
175
+ export function getEmailClient(overrides?: EmailClientOverrides): EmailClient {
158
176
  if (!_client) {
159
- const from = process.env.EMAIL_FROM || process.env.RESEND_FROM || "noreply@wopr.bot";
160
- const replyTo = process.env.EMAIL_REPLY_TO || process.env.RESEND_REPLY_TO || "support@wopr.bot";
177
+ const from = overrides?.from || process.env.EMAIL_FROM || process.env.RESEND_FROM || "noreply@wopr.bot";
178
+ const replyTo =
179
+ overrides?.replyTo || process.env.EMAIL_REPLY_TO || process.env.RESEND_REPLY_TO || "support@wopr.bot";
161
180
 
162
181
  const sesRegion = process.env.AWS_SES_REGION;
163
182
  if (sesRegion) {
@@ -170,10 +189,18 @@ export function getEmailClient(): EmailClient {
170
189
  });
171
190
  _client = new EmailClient(transport);
172
191
  logger.info("Email client initialized with AWS SES", { region: sesRegion, from });
192
+ } else if (process.env.POSTMARK_API_KEY) {
193
+ const transport = new PostmarkTransport({
194
+ apiKey: process.env.POSTMARK_API_KEY,
195
+ from,
196
+ replyTo,
197
+ });
198
+ _client = new EmailClient(transport);
199
+ logger.info("Email client initialized with Postmark", { from });
173
200
  } else {
174
201
  const apiKey = process.env.RESEND_API_KEY;
175
202
  if (!apiKey) {
176
- throw new Error("Either AWS_SES_REGION or RESEND_API_KEY environment variable is required");
203
+ throw new Error("Set AWS_SES_REGION, POSTMARK_API_KEY, or RESEND_API_KEY environment variable");
177
204
  }
178
205
  _client = new EmailClient({ apiKey, from, replyTo });
179
206
  logger.info("Email client initialized with Resend", { from });
@@ -11,7 +11,7 @@
11
11
 
12
12
  export type { BillingEmailServiceConfig, BillingEmailType } from "./billing-emails.js";
13
13
  export { BillingEmailService } from "./billing-emails.js";
14
- export type { EmailClientConfig, EmailSendResult, SendTemplateEmailOpts } from "./client.js";
14
+ export type { EmailClientConfig, EmailClientOverrides, EmailSendResult, SendTemplateEmailOpts } from "./client.js";
15
15
  export { EmailClient, getEmailClient, resetEmailClient, setEmailClient } from "./client.js";
16
16
  export { DEFAULT_TEMPLATES } from "./default-templates.js";
17
17
  export type { IBillingEmailRepository } from "./drizzle-billing-email-repository.js";
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Postmark email transport.
3
+ *
4
+ * Env vars:
5
+ * POSTMARK_API_KEY — server API token from Postmark
6
+ */
7
+
8
+ import { ServerClient } from "postmark";
9
+ import { logger } from "../config/logger.js";
10
+ import type { EmailSendResult, EmailTransport, SendTemplateEmailOpts } from "./client.js";
11
+
12
+ export interface PostmarkTransportConfig {
13
+ apiKey: string;
14
+ from: string;
15
+ replyTo?: string;
16
+ }
17
+
18
+ export class PostmarkTransport implements EmailTransport {
19
+ private client: ServerClient;
20
+ private from: string;
21
+ private replyTo: string | undefined;
22
+
23
+ constructor(config: PostmarkTransportConfig) {
24
+ this.client = new ServerClient(config.apiKey);
25
+ this.from = config.from;
26
+ this.replyTo = config.replyTo;
27
+ }
28
+
29
+ async send(opts: SendTemplateEmailOpts): Promise<EmailSendResult> {
30
+ const result = await this.client.sendEmail({
31
+ From: this.from,
32
+ To: opts.to,
33
+ Subject: opts.subject,
34
+ HtmlBody: opts.html,
35
+ TextBody: opts.text,
36
+ ReplyTo: this.replyTo,
37
+ MessageStream: "outbound",
38
+ });
39
+
40
+ if (result.ErrorCode !== 0) {
41
+ logger.error("Failed to send email via Postmark", {
42
+ to: opts.to,
43
+ template: opts.templateName,
44
+ error: result.Message,
45
+ code: result.ErrorCode,
46
+ });
47
+ throw new Error(`Postmark error ${result.ErrorCode}: ${result.Message}`);
48
+ }
49
+
50
+ logger.info("Email sent via Postmark", {
51
+ emailId: result.MessageID,
52
+ to: opts.to,
53
+ template: opts.templateName,
54
+ userId: opts.userId,
55
+ });
56
+
57
+ return { id: result.MessageID, success: true };
58
+ }
59
+ }