@wopr-network/platform-core 1.54.0 → 1.56.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.
@@ -61,6 +61,8 @@ export interface BetterAuthConfig {
61
61
  rateLimitRules?: Record<string, AuthRateLimitRule>;
62
62
  /** Trusted origins for CORS. Falls back to UI_ORIGIN env var. */
63
63
  trustedOrigins?: string[];
64
+ /** Brand name used in email templates. Default: "WOPR" */
65
+ brandName?: string;
64
66
  /** Called after a new user signs up (e.g., create personal tenant). */
65
67
  onUserCreated?: (userId: string, userName: string, email: string) => Promise<void>;
66
68
  }
@@ -160,7 +160,7 @@ function authOptions(cfg) {
160
160
  sendResetPassword: async ({ user, url }) => {
161
161
  try {
162
162
  const emailClient = getEmailClient();
163
- const template = passwordResetEmailTemplate(url, user.email);
163
+ const template = passwordResetEmailTemplate(url, user.email, cfg.brandName);
164
164
  await emailClient.send({
165
165
  to: user.email,
166
166
  ...template,
@@ -199,7 +199,7 @@ function authOptions(cfg) {
199
199
  const { token } = await generateVerificationToken(pool, user.id);
200
200
  const verifyUrl = `${baseURL}${basePath}/verify?token=${token}`;
201
201
  const emailClient = getEmailClient();
202
- const template = verifyEmailTemplate(verifyUrl, user.email);
202
+ const template = verifyEmailTemplate(verifyUrl, user.email, cfg.brandName);
203
203
  await emailClient.send({
204
204
  to: user.email,
205
205
  ...template,
@@ -23,7 +23,7 @@ function layoutOpen(title) {
23
23
  <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);">`;
24
24
  }
25
25
  const LAYOUT_CLOSE = ` </table>
26
- <p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">&copy; ${YEAR} WOPR Network. All rights reserved.</p>
26
+ <p style="margin-top: 20px; color: #a0aec0; font-size: 12px;">&copy; ${YEAR} {{brandName}}. All rights reserved.</p>
27
27
  </td>
28
28
  </tr>
29
29
  </table>
@@ -60,17 +60,17 @@ function ft(text) {
60
60
  function html(title, ...rows) {
61
61
  return `${layoutOpen(title)}\n${rows.join("\n")}\n${LAYOUT_CLOSE}`;
62
62
  }
63
- const CR = `\n\n(c) ${YEAR} WOPR Network. All rights reserved.`;
63
+ const CR = `\n\n(c) ${YEAR} {{brandName}}. All rights reserved.`;
64
64
  export const DEFAULT_TEMPLATES = [
65
65
  // -- Credits & Billing ---------------------------------------------------
66
66
  {
67
67
  name: "credits-depleted",
68
68
  description: "Sent when tenant credit balance reaches zero",
69
- subject: "Your WOPR credits are depleted \u2014 capabilities paused",
70
- htmlBody: html("Credits Depleted", hd("Your WOPR Credits Are Depleted"), p("<p>Your WOPR credit balance has reached $0. All agent capabilities have been paused.</p><p>Add credits now to resume service immediately.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("Your data is preserved. Add credits to reactivate.")),
71
- textBody: `Your WOPR Credits Are Depleted
69
+ subject: "Your {{brandName}} credits are depleted \u2014 capabilities paused",
70
+ htmlBody: html("Credits Depleted", hd("Your {{brandName}} Credits Are Depleted"), p("<p>Your {{brandName}} credit balance has reached $0. All agent capabilities have been paused.</p><p>Add credits now to resume service immediately.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("Your data is preserved. Add credits to reactivate.")),
71
+ textBody: `Your {{brandName}} Credits Are Depleted
72
72
 
73
- Your WOPR credit balance has reached $0. All agent capabilities have been paused.
73
+ Your {{brandName}} credit balance has reached $0. All agent capabilities have been paused.
74
74
 
75
75
  Add credits now to resume service immediately.
76
76
  {{#if creditsUrl}}
@@ -80,9 +80,9 @@ Add credits: {{creditsUrl}}
80
80
  {
81
81
  name: "grace-period-start",
82
82
  description: "Sent when a tenant enters the grace period after failed billing",
83
- subject: "Action needed: top up to keep your WOPRs running",
84
- htmlBody: html("Grace Period Started", hd("Action Needed: Top Up to Keep Your WOPRs Running"), p("<p>Your current balance is <strong>{{balanceDollars}}</strong> and the monthly deduction could not be processed.</p><p>You have a <strong>{{graceDays}}-day grace period</strong> to add credits before your account is suspended.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now")}{{/if}}`, ft("This is a critical notification about your account status.")),
85
- textBody: `Action Needed: Top Up to Keep Your WOPRs Running
83
+ subject: "Action needed: top up to keep your {{brandName}} agents running",
84
+ htmlBody: html("Grace Period Started", hd("Action Needed: Top Up to Keep Your {{brandName}} Agents Running"), p("<p>Your current balance is <strong>{{balanceDollars}}</strong> and the monthly deduction could not be processed.</p><p>You have a <strong>{{graceDays}}-day grace period</strong> to add credits before your account is suspended.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now")}{{/if}}`, ft("This is a critical notification about your account status.")),
85
+ textBody: `Action Needed: Top Up to Keep Your {{brandName}} Agents Running
86
86
 
87
87
  Your current balance is {{balanceDollars}} and the monthly deduction could not be processed.
88
88
 
@@ -94,9 +94,9 @@ Add credits: {{creditsUrl}}
94
94
  {
95
95
  name: "grace-period-warning",
96
96
  description: "Sent one day before grace period expires",
97
- subject: "Last chance: your WOPRs will be suspended tomorrow",
98
- htmlBody: html("Grace Period Warning", hd("Last Chance: Your WOPRs Will Be Suspended Tomorrow"), p("<p>Your grace period expires tomorrow. If you do not add credits, your account will be suspended.</p><p>Add credits now to keep your agents running.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now", "#dc2626")}{{/if}}`, ft("This is a critical notification about your account status.")),
99
- textBody: `Last Chance: Your WOPRs Will Be Suspended Tomorrow
97
+ subject: "Last chance: your agents will be suspended tomorrow",
98
+ htmlBody: html("Grace Period Warning", hd("Last Chance: Your Agents Will Be Suspended Tomorrow"), p("<p>Your grace period expires tomorrow. If you do not add credits, your account will be suspended.</p><p>Add credits now to keep your agents running.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits Now", "#dc2626")}{{/if}}`, ft("This is a critical notification about your account status.")),
99
+ textBody: `Last Chance: Your Agents Will Be Suspended Tomorrow
100
100
 
101
101
  Your grace period expires tomorrow. If you do not add credits, your account will be suspended.
102
102
  {{#if creditsUrl}}
@@ -107,7 +107,7 @@ Add credits: {{creditsUrl}}
107
107
  name: "auto-suspended",
108
108
  description: "Sent when account is automatically suspended",
109
109
  subject: "Your account has been suspended",
110
- htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your WOPR account has been automatically suspended.</p><p><strong>Reason:</strong> {{reason}}</p><p>Add credits to reactivate your account immediately.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits to Reactivate")}{{/if}}`, ft("Your data is preserved for 30 days.")),
110
+ htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your {{brandName}} account has been automatically suspended.</p><p><strong>Reason:</strong> {{reason}}</p><p>Add credits to reactivate your account immediately.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits to Reactivate")}{{/if}}`, ft("Your data is preserved for 30 days.")),
111
111
  textBody: `Your Account Has Been Suspended
112
112
 
113
113
  Reason: {{reason}}
@@ -135,7 +135,7 @@ View credits: {{creditsUrl}}
135
135
  name: "auto-topup-failed",
136
136
  description: "Sent when auto top-up charge fails",
137
137
  subject: "Auto top-up failed \u2014 update your payment method",
138
- htmlBody: html("Auto Top-Up Failed", hd("Auto Top-Up Failed"), p("<p>Your auto top-up failed. We were unable to charge your payment method.</p><p>Please update your payment method or add credits manually to avoid service interruption.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("If you need help, contact support@wopr.bot.")),
138
+ htmlBody: html("Auto Top-Up Failed", hd("Auto Top-Up Failed"), p("<p>Your auto top-up failed. We were unable to charge your payment method.</p><p>Please update your payment method or add credits manually to avoid service interruption.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Add Credits")}{{/if}}`, ft("If you need help, contact {{supportEmail}}.")),
139
139
  textBody: `Auto Top-Up Failed
140
140
 
141
141
  Your auto top-up failed. We were unable to charge your payment method.
@@ -149,7 +149,7 @@ Add credits: {{creditsUrl}}
149
149
  name: "crypto-payment-confirmed",
150
150
  description: "Sent when a crypto payment is confirmed on-chain",
151
151
  subject: "Crypto payment confirmed: {{amountDollars}} credits added",
152
- htmlBody: html("Crypto Payment Confirmed", hd("Crypto Payment Confirmed: {{amountDollars}} Credits Added"), p("<p>Your crypto payment has been confirmed. <strong>{{amountDollars}}</strong> in credits has been added to your account.</p><p>Your new balance is <strong>{{newBalanceDollars}}</strong>.</p>"), ft("Thank you for supporting WOPR!")),
152
+ htmlBody: html("Crypto Payment Confirmed", hd("Crypto Payment Confirmed: {{amountDollars}} Credits Added"), p("<p>Your crypto payment has been confirmed. <strong>{{amountDollars}}</strong> in credits has been added to your account.</p><p>Your new balance is <strong>{{newBalanceDollars}}</strong>.</p>"), ft("Thank you for supporting {{brandName}}!")),
153
153
  textBody: `Crypto Payment Confirmed: {{amountDollars}} Credits Added
154
154
 
155
155
  Your crypto payment has been confirmed. {{amountDollars}} in credits has been added.
@@ -161,27 +161,27 @@ Your new balance is {{newBalanceDollars}}.${CR}`,
161
161
  name: "admin-suspended",
162
162
  description: "Sent when an admin manually suspends an account",
163
163
  subject: "Your account has been suspended",
164
- htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your WOPR account has been suspended by an administrator.</p><p><strong>Reason:</strong> {{reason}}</p><p>If you believe this is an error, please contact support@wopr.bot.</p>"), ft("Contact support@wopr.bot if you have questions.")),
164
+ htmlBody: html("Account Suspended", hd("Your Account Has Been Suspended"), p("<p>Your {{brandName}} account has been suspended by an administrator.</p><p><strong>Reason:</strong> {{reason}}</p><p>If you believe this is an error, please contact {{supportEmail}}.</p>"), ft("Contact {{supportEmail}} if you have questions.")),
165
165
  textBody: `Your Account Has Been Suspended
166
166
 
167
167
  Reason: {{reason}}
168
168
 
169
- If you believe this is an error, please contact support@wopr.bot.${CR}`,
169
+ If you believe this is an error, please contact {{supportEmail}}.${CR}`,
170
170
  },
171
171
  {
172
172
  name: "admin-reactivated",
173
173
  description: "Sent when an admin reactivates a suspended account",
174
174
  subject: "Your account has been reactivated",
175
- htmlBody: html("Account Reactivated", hd("Your Account Has Been Reactivated"), p("<p>Your WOPR account has been reactivated. You now have full access to all services.</p><p>Your agents and channels are ready to use.</p>"), ft("Welcome back!")),
175
+ htmlBody: html("Account Reactivated", hd("Your Account Has Been Reactivated"), p("<p>Your {{brandName}} account has been reactivated. You now have full access to all services.</p><p>Your agents and channels are ready to use.</p>"), ft("Welcome back!")),
176
176
  textBody: `Your Account Has Been Reactivated
177
177
 
178
- Your WOPR account has been reactivated. You now have full access to all services.${CR}`,
178
+ Your {{brandName}} account has been reactivated. You now have full access to all services.${CR}`,
179
179
  },
180
180
  {
181
181
  name: "credits-granted",
182
182
  description: "Sent when credits are manually granted to a tenant",
183
183
  subject: "You received {{amountDollars}} in credits",
184
- htmlBody: html("Credits Granted", hd("You Received {{amountDollars}} in Credits"), p("<p><strong>{{amountDollars}}</strong> in credits has been added to your WOPR account.</p>{{#if reason}}<p><strong>Note:</strong> {{reason}}</p>{{/if}}"), ft("Thank you for using WOPR!")),
184
+ htmlBody: html("Credits Granted", hd("You Received {{amountDollars}} in Credits"), p("<p><strong>{{amountDollars}}</strong> in credits has been added to your {{brandName}} account.</p>{{#if reason}}<p><strong>Note:</strong> {{reason}}</p>{{/if}}"), ft("Thank you for using {{brandName}}!")),
185
185
  textBody: `You Received {{amountDollars}} in Credits
186
186
 
187
187
  {{amountDollars}} has been added to your account.{{#if reason}}
@@ -192,7 +192,7 @@ Note: {{reason}}{{/if}}${CR}`,
192
192
  name: "role-changed",
193
193
  description: "Sent when a user role is changed",
194
194
  subject: "Your role has been updated",
195
- htmlBody: html("Role Changed", hd("Your Role Has Been Updated"), p("<p>Your role on the WOPR platform has been updated to <strong>{{newRole}}</strong>.</p><p>Your new permissions are now active.</p>"), ft("If you did not expect this change, contact support@wopr.bot.")),
195
+ htmlBody: html("Role Changed", hd("Your Role Has Been Updated"), p("<p>Your role on the {{brandName}} platform has been updated to <strong>{{newRole}}</strong>.</p><p>Your new permissions are now active.</p>"), ft("If you did not expect this change, contact {{supportEmail}}.")),
196
196
  textBody: `Your Role Has Been Updated
197
197
 
198
198
  Your role has been updated to {{newRole}}.${CR}`,
@@ -201,7 +201,7 @@ Your role has been updated to {{newRole}}.${CR}`,
201
201
  name: "team-invite",
202
202
  description: "Sent when a user is invited to join a tenant",
203
203
  subject: "You've been invited to join {{tenantName}}",
204
- htmlBody: html("Team Invite", hd("You've Been Invited to Join {{tenantName}}"), p("<p>You've been invited to join <strong>{{tenantName}}</strong> on the WOPR platform.</p><p>Click below to accept the invitation.</p>"), `{{#if inviteUrl}}${btn("{{inviteUrl}}", "Accept Invitation")}{{/if}}`, ft("If you did not expect this invitation, you can ignore this email.")),
204
+ htmlBody: html("Team Invite", hd("You've Been Invited to Join {{tenantName}}"), p("<p>You've been invited to join <strong>{{tenantName}}</strong> on the {{brandName}} platform.</p><p>Click below to accept the invitation.</p>"), `{{#if inviteUrl}}${btn("{{inviteUrl}}", "Accept Invitation")}{{/if}}`, ft("If you did not expect this invitation, you can ignore this email.")),
205
205
  textBody: `You've Been Invited to Join {{tenantName}}
206
206
  {{#if inviteUrl}}
207
207
  Accept: {{inviteUrl}}
@@ -211,9 +211,9 @@ Accept: {{inviteUrl}}
211
211
  {
212
212
  name: "agent-created",
213
213
  description: "Sent when a new agent is created",
214
- subject: "Your WOPR {{agentName}} is ready",
215
- htmlBody: html("Agent Created", hd("Your WOPR {{agentName}} Is Ready"), p("<p>Your new agent <strong>{{agentName}}</strong> has been created and is ready to use.</p><p>Connect it to a channel to start receiving and sending messages.</p>"), ft("Happy building!")),
216
- textBody: `Your WOPR {{agentName}} Is Ready
214
+ subject: "Your {{brandName}} {{agentName}} is ready",
215
+ htmlBody: html("Agent Created", hd("Your {{brandName}} {{agentName}} Is Ready"), p("<p>Your new agent <strong>{{agentName}}</strong> has been created and is ready to use.</p><p>Connect it to a channel to start receiving and sending messages.</p>"), ft("Happy building!")),
216
+ textBody: `Your {{brandName}} {{agentName}} Is Ready
217
217
 
218
218
  Your new agent has been created and is ready to use.${CR}`,
219
219
  },
@@ -241,7 +241,7 @@ Reason: {{reason}}
241
241
  name: "agent-suspended",
242
242
  description: "Sent when an agent is paused/suspended",
243
243
  subject: "{{agentName}} has been paused",
244
- htmlBody: html("Agent Paused", hd("{{agentName}} Has Been Paused"), p("<p>Your agent <strong>{{agentName}}</strong> has been paused.</p>{{#if reason}}<p><strong>Reason:</strong> {{reason}}</p>{{/if}}"), ft("Contact support@wopr.bot if you have questions.")),
244
+ htmlBody: html("Agent Paused", hd("{{agentName}} Has Been Paused"), p("<p>Your agent <strong>{{agentName}}</strong> has been paused.</p>{{#if reason}}<p><strong>Reason:</strong> {{reason}}</p>{{/if}}"), ft("Contact {{supportEmail}} if you have questions.")),
245
245
  textBody: `{{agentName}} Has Been Paused
246
246
  {{#if reason}}
247
247
  Reason: {{reason}}
@@ -251,13 +251,13 @@ Reason: {{reason}}
251
251
  {
252
252
  name: "account-deletion-requested",
253
253
  description: "Sent when a user requests account deletion",
254
- subject: "Your WOPR account deletion request",
255
- htmlBody: html("Account Deletion Requested", hd("Account Deletion Requested"), p("<p>Hi <strong>{{email}}</strong>,</p><p>We've received your request to delete your WOPR account and all associated data.</p><p>Your account will be permanently deleted on <strong>{{deleteAfterDate}}</strong>. Until then, you can cancel this request and keep your account.</p><p>After that date, all your data will be permanently and irreversibly removed, including bots, conversation history, credit records, and plugin configurations.</p>"), `{{#if cancelUrl}}${btn("{{cancelUrl}}", "Cancel Deletion", "#22c55e")}{{/if}}`, ft("If you did not request this, please contact support@wopr.bot immediately.")),
254
+ subject: "Your {{brandName}} account deletion request",
255
+ htmlBody: html("Account Deletion Requested", hd("Account Deletion Requested"), p("<p>Hi <strong>{{email}}</strong>,</p><p>We've received your request to delete your {{brandName}} account and all associated data.</p><p>Your account will be permanently deleted on <strong>{{deleteAfterDate}}</strong>. Until then, you can cancel this request and keep your account.</p><p>After that date, all your data will be permanently and irreversibly removed, including bots, conversation history, credit records, and plugin configurations.</p>"), `{{#if cancelUrl}}${btn("{{cancelUrl}}", "Cancel Deletion", "#22c55e")}{{/if}}`, ft("If you did not request this, please contact {{supportEmail}} immediately.")),
256
256
  textBody: `Account Deletion Requested
257
257
 
258
258
  Hi {{email}},
259
259
 
260
- We've received your request to delete your WOPR account and all associated data.
260
+ We've received your request to delete your {{brandName}} account and all associated data.
261
261
 
262
262
  Your account will be permanently deleted on {{deleteAfterDate}}. Until then, you can cancel this request.
263
263
 
@@ -265,13 +265,13 @@ After that date, all your data will be permanently and irreversibly removed.
265
265
  {{#if cancelUrl}}
266
266
  Cancel deletion: {{cancelUrl}}
267
267
  {{/if}}
268
- If you did not request this, please contact support@wopr.bot immediately.${CR}`,
268
+ If you did not request this, please contact {{supportEmail}} immediately.${CR}`,
269
269
  },
270
270
  {
271
271
  name: "account-deletion-cancelled",
272
272
  description: "Sent when account deletion is cancelled",
273
- subject: "Your WOPR account deletion has been cancelled",
274
- htmlBody: html("Account Deletion Cancelled", hd("Account Deletion Cancelled"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your account deletion request has been cancelled. Your account and all data remain intact.</p><p>No further action is needed.</p>"), ft("If you didn't cancel this, please contact support@wopr.bot.")),
273
+ subject: "Your {{brandName}} account deletion has been cancelled",
274
+ htmlBody: html("Account Deletion Cancelled", hd("Account Deletion Cancelled"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your account deletion request has been cancelled. Your account and all data remain intact.</p><p>No further action is needed.</p>"), ft("If you didn't cancel this, please contact {{supportEmail}}.")),
275
275
  textBody: `Account Deletion Cancelled
276
276
 
277
277
  Hi {{email}},
@@ -283,24 +283,24 @@ No further action is needed.${CR}`,
283
283
  {
284
284
  name: "account-deletion-completed",
285
285
  description: "Sent after account is permanently deleted",
286
- subject: "Your WOPR account has been deleted",
287
- htmlBody: html("Account Deleted", hd("Your Account Has Been Deleted"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your WOPR account and all associated data have been permanently deleted as requested.</p><p>This includes all bots, conversation history, credit records, billing data, and plugin configurations.</p><p>If you'd like to use WOPR again in the future, you're welcome to create a new account.</p>"), ft("Thank you for using WOPR. We're sorry to see you go.")),
286
+ subject: "Your {{brandName}} account has been deleted",
287
+ htmlBody: html("Account Deleted", hd("Your Account Has Been Deleted"), p("<p>Hi <strong>{{email}}</strong>,</p><p>Your {{brandName}} account and all associated data have been permanently deleted as requested.</p><p>This includes all bots, conversation history, credit records, billing data, and plugin configurations.</p><p>If you'd like to use {{brandName}} again in the future, you're welcome to create a new account.</p>"), ft("Thank you for using {{brandName}}. We're sorry to see you go.")),
288
288
  textBody: `Your Account Has Been Deleted
289
289
 
290
290
  Hi {{email}},
291
291
 
292
- Your WOPR account and all associated data have been permanently deleted as requested.
292
+ Your {{brandName}} account and all associated data have been permanently deleted as requested.
293
293
 
294
294
  This includes all bots, conversation history, credit records, billing data, and plugin configurations.
295
295
 
296
- If you'd like to use WOPR again in the future, you're welcome to create a new account.${CR}`,
296
+ If you'd like to use {{brandName}} again in the future, you're welcome to create a new account.${CR}`,
297
297
  },
298
298
  // -- Dividend & Affiliate --------------------------------------------------
299
299
  {
300
300
  name: "dividend-weekly-digest",
301
301
  description: "Weekly summary of dividend payouts",
302
- subject: "WOPR paid you {{weeklyTotalDollars}} this week",
303
- htmlBody: html("Weekly Dividend Digest", hd("WOPR Paid You {{weeklyTotalDollars}} This Week"), p(`<p>Here's your weekly dividend summary for <strong>{{weekStartDate}} \u2013 {{weekEndDate}}</strong>.</p>` +
302
+ subject: "{{brandName}} paid you {{weeklyTotalDollars}} this week",
303
+ htmlBody: html("Weekly Dividend Digest", hd("{{brandName}} Paid You {{weeklyTotalDollars}} This Week"), p(`<p>Here's your weekly dividend summary for <strong>{{weekStartDate}} \u2013 {{weekEndDate}}</strong>.</p>` +
304
304
  `<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">` +
305
305
  `<tr><td style="padding: 8px 0; color: #4a5568;">This week's dividends</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #1a1a1a;">{{weeklyTotalDollars}}</td></tr>` +
306
306
  `<tr><td style="padding: 8px 0; color: #4a5568;">Days with distributions</td><td style="padding: 8px 0; text-align: right; font-weight: 600; color: #1a1a1a;">{{distributionCount}} of 7</td></tr>` +
@@ -309,7 +309,7 @@ If you'd like to use WOPR again in the future, you're welcome to create a new ac
309
309
  `<tr style="border-top: 2px solid #e2e8f0;"><td style="padding: 12px 0; color: #4a5568; font-weight: 600;">Lifetime total</td><td style="padding: 12px 0; text-align: right; font-weight: 700; color: #1a1a1a; font-size: 18px;">{{lifetimeTotalDollars}}</td></tr>` +
310
310
  `</table>` +
311
311
  `{{#if nextDividendDate}}<p style="color: #718096; font-size: 14px;">Next dividend: <strong>{{nextDividendDate}}</strong></p>{{/if}}`), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Your Credits")}{{/if}}`, ft("Community dividends are distributed daily from platform revenue. Keep your credits active to stay eligible."), '{{#if unsubscribeUrl}}<tr><td style="padding: 0 40px 20px 40px; text-align: center; color: #a0aec0; font-size: 12px;"><a href="{{unsubscribeUrl}}" style="color: #a0aec0; text-decoration: underline;">Unsubscribe from dividend digests</a></td></tr>{{/if}}'),
312
- textBody: `WOPR Paid You {{weeklyTotalDollars}} This Week
312
+ textBody: `{{brandName}} Paid You {{weeklyTotalDollars}} This Week
313
313
 
314
314
  Weekly summary for {{weekStartDate}} \u2013 {{weekEndDate}}:
315
315
 
@@ -329,7 +329,7 @@ Unsubscribe: {{unsubscribeUrl}}{{/if}}${CR}`,
329
329
  name: "affiliate-credit-match",
330
330
  description: "Sent when affiliate earns credits from a referral purchase",
331
331
  subject: "You earned {{amountDollars}} in affiliate credits!",
332
- htmlBody: html("Affiliate Credits Earned", hd("You Earned Affiliate Credits!"), p("<p>Great news! A user you referred just made their first purchase, and you've been credited <strong>{{amountDollars}}</strong>.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Credit Balance")}{{/if}}`, ft("Thank you for spreading the word about WOPR!")),
332
+ htmlBody: html("Affiliate Credits Earned", hd("You Earned Affiliate Credits!"), p("<p>Great news! A user you referred just made their first purchase, and you've been credited <strong>{{amountDollars}}</strong>.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "View Credit Balance")}{{/if}}`, ft("Thank you for spreading the word about {{brandName}}!")),
333
333
  textBody: `You Earned Affiliate Credits!
334
334
 
335
335
  A user you referred just made their first purchase, and you've been credited {{amountDollars}}.
@@ -353,16 +353,16 @@ Review spending: {{creditsUrl}}
353
353
  name: "custom",
354
354
  description: "Admin custom email with arbitrary body text",
355
355
  subject: "{{subject}}",
356
- htmlBody: html("{{subject}}", hd("Message from WOPR"), p("<p>{{{bodyTextHtml}}}</p>"), ft("This is an administrative message from WOPR Network.")),
356
+ htmlBody: html("{{subject}}", hd("Message from {{brandName}}"), p("<p>{{{bodyTextHtml}}}</p>"), ft("This is an administrative message from {{brandName}}.")),
357
357
  textBody: `{{bodyText}}${CR}`,
358
358
  },
359
359
  // -- Passthrough templates (billing/auth) ----------------------------------
360
360
  {
361
361
  name: "low-balance",
362
362
  description: "Sent when credit balance drops below threshold",
363
- subject: "Your WOPR credits are running low",
364
- htmlBody: html("Low Balance", hd("Your WOPR Credits Are Running Low"), p("<p>Your balance is <strong>{{balanceDollars}}</strong>. Top up to keep your agents running.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Buy Credits")}{{/if}}`, ft("This is an automated billing notification.")),
365
- textBody: `Your WOPR Credits Are Running Low
363
+ subject: "Your {{brandName}} credits are running low",
364
+ htmlBody: html("Low Balance", hd("Your {{brandName}} Credits Are Running Low"), p("<p>Your balance is <strong>{{balanceDollars}}</strong>. Top up to keep your agents running.</p>"), `{{#if creditsUrl}}${btn("{{creditsUrl}}", "Buy Credits")}{{/if}}`, ft("This is an automated billing notification.")),
365
+ textBody: `Your {{brandName}} Credits Are Running Low
366
366
 
367
367
  Balance: {{balanceDollars}}
368
368
  {{#if creditsUrl}}
@@ -373,7 +373,7 @@ Buy credits: {{creditsUrl}}
373
373
  name: "credit-purchase-receipt",
374
374
  description: "Sent after a credit purchase is completed",
375
375
  subject: "Credits added to your account",
376
- htmlBody: html("Credits Added", hd("Credits Added to Your Account"), p("<p><strong>{{amountDollars}}</strong> in credits has been added.</p>{{#if newBalanceDollars}}<p>New balance: <strong>{{newBalanceDollars}}</strong></p>{{/if}}"), ft("Thank you for supporting WOPR!")),
376
+ htmlBody: html("Credits Added", hd("Credits Added to Your Account"), p("<p><strong>{{amountDollars}}</strong> in credits has been added.</p>{{#if newBalanceDollars}}<p>New balance: <strong>{{newBalanceDollars}}</strong></p>{{/if}}"), ft("Thank you for supporting {{brandName}}!")),
377
377
  textBody: `Credits Added
378
378
 
379
379
  {{amountDollars}} added.${CR}`,
@@ -381,16 +381,16 @@ Buy credits: {{creditsUrl}}
381
381
  {
382
382
  name: "welcome",
383
383
  description: "Sent to new users after registration",
384
- subject: "Welcome to WOPR",
385
- htmlBody: html("Welcome", hd("Welcome to WOPR!"), p("<p>Your account is now active. Start building!</p>"), ft("Happy building!")),
386
- textBody: `Welcome to WOPR!
384
+ subject: "Welcome to {{brandName}}",
385
+ htmlBody: html("Welcome", hd("Welcome to {{brandName}}!"), p("<p>Your account is now active. Start building!</p>"), ft("Happy building!")),
386
+ textBody: `Welcome to {{brandName}}!
387
387
 
388
388
  Your account is now active.${CR}`,
389
389
  },
390
390
  {
391
391
  name: "password-reset",
392
392
  description: "Sent when a user requests a password reset",
393
- subject: "Reset your WOPR password",
393
+ subject: "Reset your {{brandName}} password",
394
394
  htmlBody: html("Reset Password", hd("Reset Your Password"), p("<p>Click below to reset your password.</p>"), `{{#if resetUrl}}${btn("{{resetUrl}}", "Reset Password")}{{/if}}`, ft("If you did not request this, ignore this email.")),
395
395
  textBody: `Reset Your Password
396
396
  {{#if resetUrl}}
@@ -17,5 +17,16 @@ export declare class DrizzleNotificationTemplateRepository implements INotificat
17
17
  htmlBody: string;
18
18
  textBody: string;
19
19
  }>): Promise<number>;
20
+ /**
21
+ * Re-seed all templates using upsert — overwrites existing content
22
+ * while preserving row IDs and createdAt timestamps.
23
+ */
24
+ reseedAll(templates: Array<{
25
+ name: string;
26
+ description: string;
27
+ subject: string;
28
+ htmlBody: string;
29
+ textBody: string;
30
+ }>): Promise<number>;
20
31
  private toRow;
21
32
  }
@@ -74,6 +74,24 @@ export class DrizzleNotificationTemplateRepository {
74
74
  .returning({ id: notificationTemplates.id });
75
75
  return result.length;
76
76
  }
77
+ /**
78
+ * Re-seed all templates using upsert — overwrites existing content
79
+ * while preserving row IDs and createdAt timestamps.
80
+ */
81
+ async reseedAll(templates) {
82
+ let count = 0;
83
+ for (const t of templates) {
84
+ await this.upsert(t.name, {
85
+ description: t.description,
86
+ subject: t.subject,
87
+ htmlBody: t.htmlBody,
88
+ textBody: t.textBody,
89
+ active: true,
90
+ });
91
+ count++;
92
+ }
93
+ return count;
94
+ }
77
95
  toRow(r) {
78
96
  return {
79
97
  id: r.id,
@@ -8,7 +8,10 @@ import type { INotificationQueueRepository } from "./notification-repository-typ
8
8
  export declare class NotificationService {
9
9
  private readonly queue;
10
10
  private readonly appBaseUrl;
11
- constructor(queue: INotificationQueueRepository, appBaseUrl: string);
11
+ private readonly brandName;
12
+ constructor(queue: INotificationQueueRepository, appBaseUrl: string, brandName?: string);
13
+ /** Enqueue with brandName injected into every data bag. */
14
+ private enqueue;
12
15
  private creditsUrl;
13
16
  notifyLowBalance(tenantId: string, email: string, balanceDollars: string, estimatedDays: number): void;
14
17
  notifyCreditsDepeleted(tenantId: string, email: string): void;
@@ -7,9 +7,15 @@
7
7
  export class NotificationService {
8
8
  queue;
9
9
  appBaseUrl;
10
- constructor(queue, appBaseUrl) {
10
+ brandName;
11
+ constructor(queue, appBaseUrl, brandName = "WOPR") {
11
12
  this.queue = queue;
12
13
  this.appBaseUrl = appBaseUrl;
14
+ this.brandName = brandName;
15
+ }
16
+ /** Enqueue with brandName injected into every data bag. */
17
+ enqueue(tenantId, template, data) {
18
+ this.queue.enqueue(tenantId, template, { ...data, brandName: this.brandName });
13
19
  }
14
20
  creditsUrl() {
15
21
  return `${this.appBaseUrl}/billing/credits`;
@@ -18,7 +24,7 @@ export class NotificationService {
18
24
  // Credit & Billing
19
25
  // ---------------------------------------------------------------------------
20
26
  notifyLowBalance(tenantId, email, balanceDollars, estimatedDays) {
21
- this.queue.enqueue(tenantId, "low-balance", {
27
+ this.enqueue(tenantId, "low-balance", {
22
28
  email,
23
29
  balanceDollars,
24
30
  estimatedDaysRemaining: estimatedDays,
@@ -26,13 +32,13 @@ export class NotificationService {
26
32
  });
27
33
  }
28
34
  notifyCreditsDepeleted(tenantId, email) {
29
- this.queue.enqueue(tenantId, "credits-depleted", {
35
+ this.enqueue(tenantId, "credits-depleted", {
30
36
  email,
31
37
  creditsUrl: this.creditsUrl(),
32
38
  });
33
39
  }
34
40
  notifyGracePeriodStart(tenantId, email, balanceDollars, graceDays) {
35
- this.queue.enqueue(tenantId, "grace-period-start", {
41
+ this.enqueue(tenantId, "grace-period-start", {
36
42
  email,
37
43
  balanceDollars,
38
44
  graceDays,
@@ -40,20 +46,20 @@ export class NotificationService {
40
46
  });
41
47
  }
42
48
  notifyGracePeriodWarning(tenantId, email) {
43
- this.queue.enqueue(tenantId, "grace-period-warning", {
49
+ this.enqueue(tenantId, "grace-period-warning", {
44
50
  email,
45
51
  creditsUrl: this.creditsUrl(),
46
52
  });
47
53
  }
48
54
  notifyAutoSuspended(tenantId, email, reason) {
49
- this.queue.enqueue(tenantId, "auto-suspended", {
55
+ this.enqueue(tenantId, "auto-suspended", {
50
56
  email,
51
57
  reason,
52
58
  creditsUrl: this.creditsUrl(),
53
59
  });
54
60
  }
55
61
  notifyAutoTopUpSuccess(tenantId, email, amountDollars, newBalanceDollars) {
56
- this.queue.enqueue(tenantId, "auto-topup-success", {
62
+ this.enqueue(tenantId, "auto-topup-success", {
57
63
  email,
58
64
  amountDollars,
59
65
  newBalanceDollars,
@@ -61,20 +67,20 @@ export class NotificationService {
61
67
  });
62
68
  }
63
69
  notifyAutoTopUpFailed(tenantId, email) {
64
- this.queue.enqueue(tenantId, "auto-topup-failed", {
70
+ this.enqueue(tenantId, "auto-topup-failed", {
65
71
  email,
66
72
  creditsUrl: this.creditsUrl(),
67
73
  });
68
74
  }
69
75
  notifyAffiliateCreditMatch(tenantId, email, amountDollars) {
70
- this.queue.enqueue(tenantId, "affiliate-credit-match", {
76
+ this.enqueue(tenantId, "affiliate-credit-match", {
71
77
  email,
72
78
  amountDollars,
73
79
  creditsUrl: this.creditsUrl(),
74
80
  });
75
81
  }
76
82
  notifyDisputeCreated(tenantId, email, disputeId, amountDollars, reason) {
77
- this.queue.enqueue(tenantId, "dispute-created", {
83
+ this.enqueue(tenantId, "dispute-created", {
78
84
  email,
79
85
  disputeId,
80
86
  amountDollars,
@@ -83,7 +89,7 @@ export class NotificationService {
83
89
  });
84
90
  }
85
91
  notifyDisputeWon(tenantId, email, disputeId, amountDollars) {
86
- this.queue.enqueue(tenantId, "dispute-won", {
92
+ this.enqueue(tenantId, "dispute-won", {
87
93
  email,
88
94
  disputeId,
89
95
  amountDollars,
@@ -91,7 +97,7 @@ export class NotificationService {
91
97
  });
92
98
  }
93
99
  notifyDisputeLost(tenantId, email, disputeId, amountDollars) {
94
- this.queue.enqueue(tenantId, "dispute-lost", {
100
+ this.enqueue(tenantId, "dispute-lost", {
95
101
  email,
96
102
  disputeId,
97
103
  amountDollars,
@@ -99,7 +105,7 @@ export class NotificationService {
99
105
  });
100
106
  }
101
107
  notifySpendThresholdAlert(tenantId, email, currentSpendDollars, alertAtDollars) {
102
- this.queue.enqueue(tenantId, "spend-alert", {
108
+ this.enqueue(tenantId, "spend-alert", {
103
109
  email,
104
110
  currentSpendDollars,
105
111
  alertAtDollars,
@@ -107,7 +113,7 @@ export class NotificationService {
107
113
  });
108
114
  }
109
115
  notifyCreditPurchaseReceipt(tenantId, email, amountDollars, newBalanceDollars) {
110
- this.queue.enqueue(tenantId, "credit-purchase-receipt", {
116
+ this.enqueue(tenantId, "credit-purchase-receipt", {
111
117
  email,
112
118
  amountDollars,
113
119
  newBalanceDollars,
@@ -116,7 +122,7 @@ export class NotificationService {
116
122
  }
117
123
  notifyDividendWeeklyDigest(tenantId, email, weeklyTotalDollars, weeklyTotalCents, lifetimeTotalDollars, distributionCount, poolAvgCents, activeUsersAvg, nextDividendDate, weekStartDate, weekEndDate) {
118
124
  const unsubscribeUrl = `${this.appBaseUrl}/settings/notifications`;
119
- this.queue.enqueue(tenantId, "dividend-weekly-digest", {
125
+ this.enqueue(tenantId, "dividend-weekly-digest", {
120
126
  email,
121
127
  weeklyTotalDollars,
122
128
  weeklyTotalCents,
@@ -132,7 +138,7 @@ export class NotificationService {
132
138
  });
133
139
  }
134
140
  notifyCryptoPaymentConfirmed(tenantId, email, amountDollars, newBalanceDollars) {
135
- this.queue.enqueue(tenantId, "crypto-payment-confirmed", {
141
+ this.enqueue(tenantId, "crypto-payment-confirmed", {
136
142
  email,
137
143
  amountDollars,
138
144
  newBalanceDollars,
@@ -142,56 +148,56 @@ export class NotificationService {
142
148
  // Account
143
149
  // ---------------------------------------------------------------------------
144
150
  notifyAdminSuspended(tenantId, email, reason) {
145
- this.queue.enqueue(tenantId, "admin-suspended", { email, reason });
151
+ this.enqueue(tenantId, "admin-suspended", { email, reason });
146
152
  }
147
153
  notifyAdminReactivated(tenantId, email) {
148
- this.queue.enqueue(tenantId, "admin-reactivated", { email });
154
+ this.enqueue(tenantId, "admin-reactivated", { email });
149
155
  }
150
156
  notifyCreditsGranted(tenantId, email, amountDollars, reason) {
151
- this.queue.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
157
+ this.enqueue(tenantId, "credits-granted", { email, amountDollars, reason });
152
158
  }
153
159
  notifyRoleChanged(tenantId, email, newRole) {
154
- this.queue.enqueue(tenantId, "role-changed", { email, newRole });
160
+ this.enqueue(tenantId, "role-changed", { email, newRole });
155
161
  }
156
162
  notifyTeamInvite(tenantId, email, tenantName, inviteUrl) {
157
- this.queue.enqueue(tenantId, "team-invite", { email, tenantName, inviteUrl });
163
+ this.enqueue(tenantId, "team-invite", { email, tenantName, inviteUrl });
158
164
  }
159
165
  // ---------------------------------------------------------------------------
160
166
  // Agent & Channel
161
167
  // ---------------------------------------------------------------------------
162
168
  notifyAgentCreated(tenantId, email, agentName) {
163
- this.queue.enqueue(tenantId, "agent-created", { email, agentName });
169
+ this.enqueue(tenantId, "agent-created", { email, agentName });
164
170
  }
165
171
  notifyChannelConnected(tenantId, email, channelName, agentName) {
166
- this.queue.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
172
+ this.enqueue(tenantId, "channel-connected", { email, channelName, agentName });
167
173
  }
168
174
  notifyChannelDisconnected(tenantId, email, channelName, agentName, reason) {
169
- this.queue.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
175
+ this.enqueue(tenantId, "channel-disconnected", { email, channelName, agentName, reason });
170
176
  }
171
177
  notifyAgentSuspended(tenantId, email, agentName, reason) {
172
- this.queue.enqueue(tenantId, "agent-suspended", { email, agentName, reason });
178
+ this.enqueue(tenantId, "agent-suspended", { email, agentName, reason });
173
179
  }
174
180
  // ---------------------------------------------------------------------------
175
181
  // Account Deletion
176
182
  // ---------------------------------------------------------------------------
177
183
  notifyAccountDeletionRequested(tenantId, email, deleteAfterDate) {
178
- this.queue.enqueue(tenantId, "account-deletion-requested", {
184
+ this.enqueue(tenantId, "account-deletion-requested", {
179
185
  email,
180
186
  deleteAfterDate,
181
187
  cancelUrl: `${this.appBaseUrl}/settings/account`,
182
188
  });
183
189
  }
184
190
  notifyAccountDeletionCancelled(tenantId, email) {
185
- this.queue.enqueue(tenantId, "account-deletion-cancelled", { email });
191
+ this.enqueue(tenantId, "account-deletion-cancelled", { email });
186
192
  }
187
193
  notifyAccountDeletionCompleted(tenantId, email) {
188
- this.queue.enqueue(tenantId, "account-deletion-completed", { email });
194
+ this.enqueue(tenantId, "account-deletion-completed", { email });
189
195
  }
190
196
  // ---------------------------------------------------------------------------
191
197
  // Fleet Updates
192
198
  // ---------------------------------------------------------------------------
193
199
  notifyFleetUpdateAvailable(tenantId, email, version, changelogDate, changelogSummary) {
194
- this.queue.enqueue(tenantId, "fleet-update-available", {
200
+ this.enqueue(tenantId, "fleet-update-available", {
195
201
  email,
196
202
  version,
197
203
  changelogDate,
@@ -200,7 +206,7 @@ export class NotificationService {
200
206
  });
201
207
  }
202
208
  notifyFleetUpdateComplete(tenantId, email, version, succeeded, failed) {
203
- this.queue.enqueue(tenantId, "fleet-update-complete", {
209
+ this.enqueue(tenantId, "fleet-update-complete", {
204
210
  email,
205
211
  version,
206
212
  succeeded,
@@ -213,6 +219,6 @@ export class NotificationService {
213
219
  // Admin custom email
214
220
  // ---------------------------------------------------------------------------
215
221
  sendCustomEmail(tenantId, email, subject, bodyText) {
216
- this.queue.enqueue(tenantId, "custom", { email, subject, bodyText });
222
+ this.enqueue(tenantId, "custom", { email, subject, bodyText });
217
223
  }
218
224
  }