@wopr-network/platform-core 1.54.0 → 1.55.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.
@@ -177,7 +177,7 @@ describe("all templates", () => {
177
177
  expect(t.html).toContain("<!DOCTYPE html>");
178
178
  expect(t.html).toContain("<html>");
179
179
  expect(t.html).toContain("</html>");
180
- expect(t.html).toContain("WOPR Network");
180
+ expect(t.html).toContain("WOPR");
181
181
  expect(t.subject.length).toBeGreaterThan(0);
182
182
  expect(t.text.length).toBeGreaterThan(0);
183
183
  }
@@ -12,7 +12,7 @@ import { escapeHtml } from "./resend-adapter.js";
12
12
  // Shared layout helpers
13
13
  // ---------------------------------------------------------------------------
14
14
 
15
- function wrapHtml(title: string, bodyContent: string): string {
15
+ function wrapHtml(title: string, bodyContent: string, brandName = "WOPR"): string {
16
16
  return `<!DOCTYPE html>
17
17
  <html>
18
18
  <head>
@@ -27,7 +27,7 @@ function wrapHtml(title: string, bodyContent: string): string {
27
27
  <table role="presentation" style="width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
28
28
  ${bodyContent}
29
29
  </table>
30
- <p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">&copy; ${new Date().getFullYear()} WOPR Network. All rights reserved.</p>
30
+ <p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">&copy; ${new Date().getFullYear()} ${escapeHtml(brandName)}. All rights reserved.</p>
31
31
  </td>
32
32
  </tr>
33
33
  </table>
@@ -116,57 +116,61 @@ export interface TemplateResult {
116
116
  // 1. Verify Email
117
117
  // ---------------------------------------------------------------------------
118
118
 
119
- export function verifyEmailTemplate(verifyUrl: string, email: string): TemplateResult {
119
+ export function verifyEmailTemplate(verifyUrl: string, email: string, brandName = "WOPR"): TemplateResult {
120
120
  const escapedEmail = escapeHtml(email);
121
121
  const escapedUrl = escapeHtml(verifyUrl);
122
+ const b = escapeHtml(brandName);
122
123
 
123
124
  const html = wrapHtml(
124
125
  "Verify Your Email",
125
126
  [
126
127
  heading("Verify Your Email"),
127
- paragraph(`<p>Thanks for signing up for WOPR! Please verify your email address (<strong>${escapedEmail}</strong>) to activate your account.</p>
128
+ paragraph(`<p>Thanks for signing up for ${b}! Please verify your email address (<strong>${escapedEmail}</strong>) to activate your account.</p>
128
129
  <p>Click the button below to verify. This link will expire in 24 hours.</p>`),
129
130
  button(verifyUrl, "Verify Email"),
130
131
  paragraph(`<p style="color: #718096; font-size: 14px;">Or copy and paste this URL into your browser:</p>
131
132
  <p style="word-break: break-all; color: #2563eb; font-size: 14px;">${escapedUrl}</p>`),
132
- footer("If you didn't create a WOPR account, you can safely ignore this email."),
133
+ footer(`If you didn't create a ${b} account, you can safely ignore this email.`),
133
134
  ].join("\n"),
135
+ brandName,
134
136
  );
135
137
 
136
138
  const text = `Verify Your Email
137
139
 
138
- Thanks for signing up for WOPR! Please verify your email address (${email}) to activate your account.
140
+ Thanks for signing up for ${brandName}! Please verify your email address (${email}) to activate your account.
139
141
 
140
142
  Click the link below to verify. This link will expire in 24 hours.
141
143
 
142
144
  ${verifyUrl}
143
145
 
144
- If you didn't create a WOPR account, you can safely ignore this email.
146
+ If you didn't create a ${brandName} account, you can safely ignore this email.
145
147
 
146
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
148
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
147
149
 
148
- return { subject: "Verify your WOPR account", html, text };
150
+ return { subject: `Verify your ${brandName} account`, html, text };
149
151
  }
150
152
 
151
153
  // ---------------------------------------------------------------------------
152
154
  // 2. Welcome
153
155
  // ---------------------------------------------------------------------------
154
156
 
155
- export function welcomeTemplate(email: string): TemplateResult {
157
+ export function welcomeTemplate(email: string, brandName = "WOPR"): TemplateResult {
156
158
  const escapedEmail = escapeHtml(email);
159
+ const b = escapeHtml(brandName);
157
160
 
158
161
  const html = wrapHtml(
159
- "Welcome to WOPR",
162
+ `Welcome to ${b}`,
160
163
  [
161
- heading("Welcome to WOPR!"),
164
+ heading(`Welcome to ${b}!`),
162
165
  paragraph(`<p>Hi <strong>${escapedEmail}</strong>,</p>
163
166
  <p>Your email has been verified and your account is now active. You've been granted <strong>$5.00 in free credits</strong> to get started.</p>
164
167
  <p>You can now create bots, connect them to Discord, Slack, and more.</p>`),
165
168
  footer("Happy building!"),
166
169
  ].join("\n"),
170
+ brandName,
167
171
  );
168
172
 
169
- const text = `Welcome to WOPR!
173
+ const text = `Welcome to ${brandName}!
170
174
 
171
175
  Hi ${email},
172
176
 
@@ -176,38 +180,40 @@ You can now create bots, connect them to Discord, Slack, and more.
176
180
 
177
181
  Happy building!
178
182
 
179
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
183
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
180
184
 
181
- return { subject: "Welcome to WOPR", html, text };
185
+ return { subject: `Welcome to ${brandName}`, html, text };
182
186
  }
183
187
 
184
188
  // ---------------------------------------------------------------------------
185
189
  // 3. Password Reset (supersedes WOP-346 inline template)
186
190
  // ---------------------------------------------------------------------------
187
191
 
188
- export function passwordResetEmailTemplate(resetUrl: string, email: string): TemplateResult {
192
+ export function passwordResetEmailTemplate(resetUrl: string, email: string, brandName = "WOPR"): TemplateResult {
189
193
  const escapedEmail = escapeHtml(email);
190
194
  const escapedUrl = escapeHtml(resetUrl);
195
+ const b = escapeHtml(brandName);
191
196
 
192
197
  const html = wrapHtml(
193
198
  "Reset Your Password",
194
199
  [
195
200
  heading("Reset Your Password"),
196
201
  paragraph(`<p>Hi there,</p>
197
- <p>You requested a password reset for your WOPR account (<strong>${escapedEmail}</strong>).</p>
202
+ <p>You requested a password reset for your ${b} account (<strong>${escapedEmail}</strong>).</p>
198
203
  <p>Click the button below to create a new password. This link will expire in 1 hour.</p>`),
199
204
  button(resetUrl, "Reset Password"),
200
205
  paragraph(`<p style="color: #718096; font-size: 14px;">Or copy and paste this URL into your browser:</p>
201
206
  <p style="word-break: break-all; color: #2563eb; font-size: 14px;">${escapedUrl}</p>`),
202
207
  footer("If you didn't request this password reset, you can safely ignore this email."),
203
208
  ].join("\n"),
209
+ brandName,
204
210
  );
205
211
 
206
212
  const text = `Reset Your Password
207
213
 
208
214
  Hi there,
209
215
 
210
- You requested a password reset for your WOPR account (${email}).
216
+ You requested a password reset for your ${brandName} account (${email}).
211
217
 
212
218
  Click the link below to create a new password. This link will expire in 1 hour.
213
219
 
@@ -215,9 +221,9 @@ ${resetUrl}
215
221
 
216
222
  If you didn't request this password reset, you can safely ignore this email.
217
223
 
218
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
224
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
219
225
 
220
- return { subject: "Reset your WOPR password", html, text };
226
+ return { subject: `Reset your ${brandName} password`, html, text };
221
227
  }
222
228
 
223
229
  // ---------------------------------------------------------------------------
@@ -229,9 +235,11 @@ export function creditPurchaseTemplate(
229
235
  amountDollars: string,
230
236
  newBalanceDollars?: string,
231
237
  creditsUrl?: string,
238
+ brandName = "WOPR",
232
239
  ): TemplateResult {
233
240
  const escapedEmail = escapeHtml(email);
234
241
  const escapedAmount = escapeHtml(amountDollars);
242
+ const b = escapeHtml(brandName);
235
243
  const balanceLine = newBalanceDollars
236
244
  ? `<p>Your new balance is <strong>${escapeHtml(newBalanceDollars)}</strong>.</p>`
237
245
  : "<p>Your updated balance is now available in your dashboard.</p>";
@@ -242,25 +250,25 @@ export function creditPurchaseTemplate(
242
250
  const parts = [
243
251
  heading("Credits Added to Your Account"),
244
252
  paragraph(`<p>Hi <strong>${escapedEmail}</strong>,</p>
245
- <p><strong>${escapedAmount}</strong> in credits has been added to your WOPR account.</p>
253
+ <p><strong>${escapedAmount}</strong> in credits has been added to your ${b} account.</p>
246
254
  ${balanceLine}`),
247
255
  ];
248
256
  if (creditsUrl) parts.push(button(creditsUrl, "View Credits"));
249
- parts.push(footer("Thank you for supporting WOPR!"));
257
+ parts.push(footer(`Thank you for supporting ${b}!`));
250
258
  parts.push(unsubscribeFooter(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined));
251
259
 
252
- const html = wrapHtml("Credits Added", parts.join("\n"));
260
+ const html = wrapHtml("Credits Added", parts.join("\n"), brandName);
253
261
 
254
262
  const text = `Credits Added to Your Account
255
263
 
256
264
  Hi ${email},
257
265
 
258
- ${amountDollars} in credits has been added to your WOPR account.
266
+ ${amountDollars} in credits has been added to your ${brandName} account.
259
267
  ${balanceTextLine}
260
268
  ${creditsUrl ? `\nView your credits: ${creditsUrl}` : ""}
261
- Thank you for supporting WOPR!${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
269
+ Thank you for supporting ${brandName}!${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
262
270
 
263
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
271
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
264
272
 
265
273
  return { subject: "Credits added to your account", html, text };
266
274
  }
@@ -274,9 +282,11 @@ export function lowBalanceTemplate(
274
282
  balanceDollars: string,
275
283
  estimatedDaysRemaining?: number,
276
284
  creditsUrl?: string,
285
+ brandName = "WOPR",
277
286
  ): TemplateResult {
278
287
  const escapedEmail = escapeHtml(email);
279
288
  const escapedBalance = escapeHtml(balanceDollars);
289
+ const b = escapeHtml(brandName);
280
290
  const daysLine =
281
291
  estimatedDaysRemaining != null
282
292
  ? `<p>At your current usage, your credits will run out in approximately <strong>${estimatedDaysRemaining} day${estimatedDaysRemaining === 1 ? "" : "s"}</strong>.</p>`
@@ -287,9 +297,9 @@ export function lowBalanceTemplate(
287
297
  : "";
288
298
 
289
299
  const parts = [
290
- heading("Your WOPR Credits Are Running Low"),
300
+ heading(`Your ${b} Credits Are Running Low`),
291
301
  paragraph(`<p>Hi <strong>${escapedEmail}</strong>,</p>
292
- <p>Your WOPR credit balance is now <strong>${escapedBalance}</strong>. When your balance reaches $0, your bots will be paused.</p>
302
+ <p>Your ${b} credit balance is now <strong>${escapedBalance}</strong>. When your balance reaches $0, your bots will be paused.</p>
293
303
  ${daysLine}
294
304
  <p>Top up your credits to keep your bots running.</p>`),
295
305
  ];
@@ -297,20 +307,20 @@ export function lowBalanceTemplate(
297
307
  parts.push(footer("This is an automated notification based on your account balance."));
298
308
  parts.push(unsubscribeFooter(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined));
299
309
 
300
- const html = wrapHtml("Low Balance", parts.join("\n"));
310
+ const html = wrapHtml("Low Balance", parts.join("\n"), brandName);
301
311
 
302
- const text = `Your WOPR Credits Are Running Low
312
+ const text = `Your ${brandName} Credits Are Running Low
303
313
 
304
314
  Hi ${email},
305
315
 
306
- Your WOPR credit balance is now ${balanceDollars}. When your balance reaches $0, your bots will be paused.
316
+ Your ${brandName} credit balance is now ${balanceDollars}. When your balance reaches $0, your bots will be paused.
307
317
  ${daysTextLine ? `${daysTextLine}\n` : ""}
308
318
  Top up your credits to keep your bots running.
309
319
  ${creditsUrl ? `\nBuy credits: ${creditsUrl}` : ""}${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
310
320
 
311
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
321
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
312
322
 
313
- return { subject: "Your WOPR credits are running low", html, text };
323
+ return { subject: `Your ${brandName} credits are running low`, html, text };
314
324
  }
315
325
 
316
326
  // ---------------------------------------------------------------------------
@@ -322,6 +332,8 @@ export function botSuspendedTemplate(
322
332
  botName: string,
323
333
  reason: string,
324
334
  creditsUrl?: string,
335
+ brandName = "WOPR",
336
+ supportEmail = "support@wopr.bot",
325
337
  ): TemplateResult {
326
338
  const escapedEmail = escapeHtml(email);
327
339
  const escapedBotName = escapeHtml(botName);
@@ -335,10 +347,10 @@ export function botSuspendedTemplate(
335
347
  <p>Buy credits to reactivate instantly. Your data is preserved for 30 days.</p>`),
336
348
  ];
337
349
  if (creditsUrl) parts.push(button(creditsUrl, "Buy Credits to Reactivate"));
338
- parts.push(footer("If you need help, reply to this email or contact support@wopr.bot."));
350
+ parts.push(footer(`If you need help, reply to this email or contact ${escapeHtml(supportEmail)}.`));
339
351
  parts.push(unsubscribeFooter(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined));
340
352
 
341
- const html = wrapHtml("Bot Suspended", parts.join("\n"));
353
+ const html = wrapHtml("Bot Suspended", parts.join("\n"), brandName);
342
354
 
343
355
  const text = `Your Bot Has Been Suspended
344
356
 
@@ -350,9 +362,9 @@ Reason: ${reason}
350
362
 
351
363
  Buy credits to reactivate instantly. Your data is preserved for 30 days.
352
364
  ${creditsUrl ? `\nBuy credits: ${creditsUrl}` : ""}
353
- If you need help, reply to this email or contact support@wopr.bot.${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
365
+ If you need help, reply to this email or contact ${supportEmail}.${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
354
366
 
355
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
367
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
356
368
 
357
369
  return { subject: "Your bot has been suspended", html, text };
358
370
  }
@@ -366,6 +378,7 @@ export function botDestructionTemplate(
366
378
  botName: string,
367
379
  daysRemaining: number,
368
380
  creditsUrl?: string,
381
+ brandName = "WOPR",
369
382
  ): TemplateResult {
370
383
  const escapedEmail = escapeHtml(email);
371
384
  const escapedBotName = escapeHtml(botName);
@@ -388,7 +401,7 @@ export function botDestructionTemplate(
388
401
  );
389
402
  parts.push(unsubscribeFooter(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined));
390
403
 
391
- const html = wrapHtml("Bot Data Deletion", parts.join("\n"));
404
+ const html = wrapHtml("Bot Data Deletion", parts.join("\n"), brandName);
392
405
 
393
406
  const text = `URGENT: Bot Data Will Be Deleted
394
407
 
@@ -400,7 +413,7 @@ Buy credits before the deadline to save your data.
400
413
  ${creditsUrl ? `\nBuy credits now: ${creditsUrl}` : ""}
401
414
  This action is irreversible. All bot configuration, history, and connected services will be removed.${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
402
415
 
403
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
416
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
404
417
 
405
418
  return { subject: `URGENT: Your bot data will be deleted in ${daysRemaining} days`, html, text };
406
419
  }
@@ -409,7 +422,12 @@ This action is irreversible. All bot configuration, history, and connected servi
409
422
  // 8. Data Deleted Confirmation
410
423
  // ---------------------------------------------------------------------------
411
424
 
412
- export function dataDeletedTemplate(email: string, creditsUrl?: string): TemplateResult {
425
+ export function dataDeletedTemplate(
426
+ email: string,
427
+ creditsUrl?: string,
428
+ brandName = "WOPR",
429
+ supportEmail = "support@wopr.bot",
430
+ ): TemplateResult {
413
431
  const escapedEmail = escapeHtml(email);
414
432
 
415
433
  const parts = [
@@ -419,10 +437,10 @@ export function dataDeletedTemplate(email: string, creditsUrl?: string): Templat
419
437
  <p>You can create a new bot anytime by adding credits to your account.</p>`),
420
438
  ];
421
439
  if (creditsUrl) parts.push(button(creditsUrl, "Add Credits"));
422
- parts.push(footer("If you have questions, contact support@wopr.bot."));
440
+ parts.push(footer(`If you have questions, contact ${escapeHtml(supportEmail)}.`));
423
441
  parts.push(unsubscribeFooter(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined));
424
442
 
425
- const html = wrapHtml("Data Deleted", parts.join("\n"));
443
+ const html = wrapHtml("Data Deleted", parts.join("\n"), brandName);
426
444
 
427
445
  const text = `Your Bot Data Has Been Deleted
428
446
 
@@ -432,9 +450,9 @@ Your suspended bot data has been permanently deleted after 30 days of inactivity
432
450
 
433
451
  You can create a new bot anytime by adding credits to your account.
434
452
  ${creditsUrl ? `\nAdd credits: ${creditsUrl}` : ""}
435
- If you have questions, contact support@wopr.bot.${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
453
+ If you have questions, contact ${supportEmail}.${unsubscribeText(creditsUrl ? buildUnsubscribeUrl(creditsUrl) : undefined)}
436
454
 
437
- (c) ${new Date().getFullYear()} WOPR Network. All rights reserved.`;
455
+ (c) ${new Date().getFullYear()} ${brandName}. All rights reserved.`;
438
456
 
439
457
  return { subject: "Your bot data has been deleted", html, text };
440
458
  }
@@ -443,23 +461,25 @@ If you have questions, contact support@wopr.bot.${unsubscribeText(creditsUrl ? b
443
461
  // Org Invite
444
462
  // ---------------------------------------------------------------------------
445
463
 
446
- export function orgInviteEmailTemplate(inviteUrl: string, orgName: string): TemplateResult {
464
+ export function orgInviteEmailTemplate(inviteUrl: string, orgName: string, brandName = "WOPR"): TemplateResult {
447
465
  const safeOrg = escapeHtml(orgName);
448
466
  const safeUrl = escapeHtml(inviteUrl);
467
+ const b = escapeHtml(brandName);
449
468
 
450
469
  const html = wrapHtml(
451
470
  `You're invited to join ${safeOrg}`,
452
471
  [
453
472
  heading(`Join ${safeOrg}`),
454
473
  paragraph(
455
- `You've been invited to join <strong>${safeOrg}</strong> on WOPR Network. Click the button below to accept the invitation.`,
474
+ `You've been invited to join <strong>${safeOrg}</strong> on ${b}. Click the button below to accept the invitation.`,
456
475
  ),
457
476
  button(safeUrl, "Accept Invitation"),
458
477
  footer("This invitation expires in 7 days. If you didn't expect this email, you can safely ignore it."),
459
478
  ].join("\n"),
479
+ brandName,
460
480
  );
461
481
 
462
- const text = `You're invited to join ${orgName} on WOPR Network.
482
+ const text = `You're invited to join ${orgName} on ${brandName}.
463
483
 
464
484
  Accept the invitation: ${inviteUrl}
465
485