hazo_auth 5.2.0 → 5.3.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 (31) hide show
  1. package/SETUP_CHECKLIST.md +46 -0
  2. package/cli-src/lib/services/email_service.ts +136 -289
  3. package/cli-src/lib/services/email_template_manifest.ts +104 -0
  4. package/cli-src/lib/services/email_templates/email_verification.html +18 -0
  5. package/cli-src/lib/services/email_templates/email_verification.txt +9 -0
  6. package/cli-src/lib/services/email_templates/forgot_password.html +18 -0
  7. package/cli-src/lib/services/email_templates/forgot_password.txt +9 -0
  8. package/cli-src/lib/services/email_templates/password_changed.html +14 -0
  9. package/cli-src/lib/services/email_templates/password_changed.txt +9 -0
  10. package/dist/components/ui/button.d.ts +2 -2
  11. package/dist/lib/services/email_service.d.ts +17 -4
  12. package/dist/lib/services/email_service.d.ts.map +1 -1
  13. package/dist/lib/services/email_service.js +93 -221
  14. package/dist/lib/services/email_template_manifest.d.ts +11 -0
  15. package/dist/lib/services/email_template_manifest.d.ts.map +1 -0
  16. package/dist/lib/services/email_template_manifest.js +95 -0
  17. package/dist/lib/services/email_templates/email_verification.html +18 -0
  18. package/dist/lib/services/email_templates/email_verification.txt +9 -0
  19. package/dist/lib/services/email_templates/forgot_password.html +18 -0
  20. package/dist/lib/services/email_templates/forgot_password.txt +9 -0
  21. package/dist/lib/services/email_templates/password_changed.html +14 -0
  22. package/dist/lib/services/email_templates/password_changed.txt +9 -0
  23. package/dist/server-lib.d.ts +1 -0
  24. package/dist/server-lib.d.ts.map +1 -1
  25. package/dist/server-lib.js +1 -0
  26. package/package.json +4 -3
  27. package/cli-src/assets/images/new_firm_default.jpg +0 -0
  28. package/cli-src/lib/auth/org_cache.ts +0 -148
  29. package/cli-src/lib/index.ts +0 -48
  30. package/cli-src/lib/services/org_service.ts +0 -965
  31. package/cli-src/lib/services/scope_labels_service.ts +0 -348
@@ -2,6 +2,52 @@
2
2
 
3
3
  This checklist provides step-by-step instructions for setting up the `hazo_auth` package in your Next.js project. AI assistants can follow this guide to ensure complete and correct setup.
4
4
 
5
+ ## v5.3.0 Migration (from v5.2.x)
6
+
7
+ If you are already on hazo_auth `5.2.x`, the only mandatory work is wiring up hazo_notify's template manager at boot. Apps that don't use the templated email path (only the plain `send_email`) keep working unchanged after bumping the peer dep.
8
+
9
+ **1. Bump the peer dep.** Update your app's `package.json`:
10
+
11
+ ```diff
12
+ - "hazo_notify": "^1.1.0",
13
+ + "hazo_notify": "^3.0.0",
14
+ ```
15
+
16
+ Then `npm install`.
17
+
18
+ **2. Apply the hazo_notify template-manager migration.** hazo_notify 3.0.0 introduces two new tables (`hazo_notify_template_cat`, `hazo_notify_templates`). If you are using SQLite via the existing schema seed, the tables are created on first connect when `initial_sql` is provided to the adapter (see step 4 below). If you are on PostgREST or you manage migrations explicitly, run `migrations/002_template_manager.sql` from hazo_notify.
19
+
20
+ **3. Register the `notify_templates_admin` permission (optional).** Users with this permission can edit auth email templates via the hazo_notify admin UI. Grant `notify_templates_super_admin` to also allow deleting system templates. These are hazo_notify permissions, declared in your app's permission set; hazo_auth does not grant them itself.
21
+
22
+ **4. Wire up `init_template_manager` and `set_hazo_notify_connect` at boot.** In your Next.js `instrumentation.ts` (or equivalent), feed hazo_auth's manifest into hazo_notify and register the connect instance with hazo_auth:
23
+
24
+ ```ts
25
+ // instrumentation.ts
26
+ import { init_template_manager } from "hazo_notify/template_manager";
27
+ import { hazo_auth_template_manifest } from "hazo_auth/server-lib";
28
+ import { set_hazo_notify_connect } from "hazo_auth/server-lib"; // re-exported from email_service
29
+ import { build_my_hazo_notify_connect } from "./lib/hazo_notify_connect";
30
+
31
+ export async function register() {
32
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
33
+
34
+ const notify_connect = build_my_hazo_notify_connect();
35
+
36
+ await init_template_manager({
37
+ hazo_connect_factory: () => notify_connect,
38
+ manifests: [...hazo_auth_template_manifest],
39
+ });
40
+
41
+ set_hazo_notify_connect(notify_connect);
42
+ }
43
+ ```
44
+
45
+ `build_my_hazo_notify_connect()` is consuming-app code that wraps your `hazo_connect` adapter into the `HazoConnectInstance` shape hazo_notify expects (`list`/`findById`/`findBy`/`insert`/`updateById`/`deleteById`/`query`). hazo_auth's own test-app ships an example wrapper at `instrumentation_hazo_notify_connect.ts` you can copy.
46
+
47
+ **5. Migrate any custom templates.** The `[hazo_auth__email] email_template_main_directory` config key is deprecated and no longer overrides anything — a one-time `warn` log will fire on first send if it is set. Move any custom HTML/text into the admin UI as scope-specific (or super-admin-edited global) templates before `hazo_auth@6.0.0` removes the key.
48
+
49
+ **6. (Optional) Mount the template admin UI.** Use `<TemplateManagerAdmin />` from `hazo_notify/template_manager_admin` to give your admins a UI for managing the auth templates. See hazo_notify's docs for routing and permission-gating.
50
+
5
51
  ## Quick Start (Recommended)
6
52
 
7
53
  The fastest way to set up hazo_auth:
@@ -1,10 +1,9 @@
1
1
  // file_description: service for sending emails with template support
2
2
  // section: imports
3
- import fs from "fs";
4
- import path from "path";
5
3
  import { create_app_logger } from "../app_logger.js";
6
4
  import { read_config_section } from "../config/config_loader.server.js";
7
5
  import type { EmailerConfig, SendEmailOptions } from "hazo_notify";
6
+ import type { HazoConnectInstance } from "hazo_notify/template_manager";
8
7
 
9
8
  // section: types
10
9
  export type EmailOptions = {
@@ -26,16 +25,6 @@ export type EmailTemplateData = {
26
25
  [key: string]: string | undefined;
27
26
  };
28
27
 
29
- // section: constants
30
- const DEFAULT_EMAIL_FROM = "noreply@hazo_auth.local";
31
-
32
- /**
33
- * Gets the default email template directory (lazy-evaluated to avoid Edge Runtime issues)
34
- */
35
- function get_default_email_template_dir(): string {
36
- return path.resolve(process.cwd(), "email_templates");
37
- }
38
-
39
28
  // section: singleton
40
29
  /**
41
30
  * Singleton instance for hazo_notify emailer configuration
@@ -43,6 +32,14 @@ function get_default_email_template_dir(): string {
43
32
  */
44
33
  let hazo_notify_config: EmailerConfig | null = null;
45
34
 
35
+ /**
36
+ * Singleton hazo_notify-compatible hazo_connect instance used by the template
37
+ * manager. The consuming app builds this once (typically the same instance fed
38
+ * into `init_template_manager({ hazo_connect_factory })`) and registers it via
39
+ * {@link set_hazo_notify_connect}. Required for `send_template_email`.
40
+ */
41
+ let hazo_notify_connect: HazoConnectInstance | null = null;
42
+
46
43
  /**
47
44
  * Sets the hazo_notify emailer configuration instance
48
45
  * This is called from instrumentation.ts during initialization
@@ -52,6 +49,17 @@ export function set_hazo_notify_instance(config: EmailerConfig): void {
52
49
  hazo_notify_config = config;
53
50
  }
54
51
 
52
+ /**
53
+ * Registers the hazo_notify-compatible hazo_connect instance used by
54
+ * `send_template_email`. Call this from `instrumentation.ts` with the same
55
+ * instance you pass to `init_template_manager({ hazo_connect_factory })`.
56
+ *
57
+ * @param instance - hazo_notify-shaped database adapter
58
+ */
59
+ export function set_hazo_notify_connect(instance: HazoConnectInstance): void {
60
+ hazo_notify_connect = instance;
61
+ }
62
+
55
63
  /**
56
64
  * Gets the hazo_notify emailer configuration instance
57
65
  * If not set, loads it from config file as fallback
@@ -84,24 +92,36 @@ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
84
92
  return hazo_notify_config;
85
93
  }
86
94
 
87
- // section: helpers
95
+ // section: deprecated_config_helpers
88
96
  /**
89
- * Gets email template directory from config
90
- * @returns Email template directory path
97
+ * One-time warning state for the deprecated email_template_main_directory key.
91
98
  */
92
- function get_email_template_directory(): string {
99
+ let warned_about_template_dir = false;
100
+
101
+ /**
102
+ * Reads the deprecated [hazo_auth__email] email_template_main_directory key
103
+ * solely to emit a one-time deprecation warning. The value is no longer used —
104
+ * template overrides now live in hazo_notify's database and are managed via
105
+ * the template admin UI. The key will be removed in hazo_auth@6.0.0.
106
+ */
107
+ function warn_if_template_directory_configured(): void {
108
+ if (warned_about_template_dir) return;
93
109
  const email_section = read_config_section("hazo_auth__email");
94
110
  const template_dir = email_section?.email_template_main_directory;
95
-
96
- if (template_dir) {
97
- return path.isAbsolute(template_dir)
98
- ? template_dir
99
- : path.resolve(process.cwd(), template_dir);
100
- }
101
-
102
- return get_default_email_template_dir();
111
+ if (!template_dir) return;
112
+ warned_about_template_dir = true;
113
+ create_app_logger().warn("hazo_auth_email_template_main_directory_deprecated", {
114
+ filename: "email_service.ts",
115
+ line_number: 0,
116
+ note:
117
+ "Filesystem template overrides are deprecated in hazo_auth@5.3.0. " +
118
+ "Use the hazo_notify template admin UI (mounted via <TemplateManagerAdmin />) " +
119
+ "to override the email_verification / forgot_password / password_changed templates per scope. " +
120
+ "This config key will be removed in hazo_auth@6.0.0.",
121
+ });
103
122
  }
104
123
 
124
+ // section: helpers
105
125
  /**
106
126
  * Gets email from address from config
107
127
  * Priority: 1. hazo_auth__email.from_email, 2. hazo_notify_config.from_email
@@ -111,12 +131,12 @@ function get_email_template_directory(): string {
111
131
  async function get_email_from(notify_config: EmailerConfig): Promise<string> {
112
132
  const email_section = read_config_section("hazo_auth__email");
113
133
  const hazo_auth_from_email = email_section?.from_email;
114
-
134
+
115
135
  // If set in hazo_auth_config.ini, use it (overrides hazo_notify config)
116
136
  if (hazo_auth_from_email) {
117
137
  return hazo_auth_from_email;
118
138
  }
119
-
139
+
120
140
  // Fall back to hazo_notify config
121
141
  return notify_config.from_email;
122
142
  }
@@ -130,12 +150,12 @@ async function get_email_from(notify_config: EmailerConfig): Promise<string> {
130
150
  async function get_email_from_name(notify_config: EmailerConfig): Promise<string> {
131
151
  const email_section = read_config_section("hazo_auth__email");
132
152
  const hazo_auth_from_name = email_section?.from_name;
133
-
153
+
134
154
  // If set in hazo_auth_config.ini, use it (overrides hazo_notify config)
135
155
  if (hazo_auth_from_name) {
136
156
  return hazo_auth_from_name;
137
157
  }
138
-
158
+
139
159
  // Fall back to hazo_notify config
140
160
  return notify_config.from_name;
141
161
  }
@@ -148,11 +168,11 @@ async function get_email_from_name(notify_config: EmailerConfig): Promise<string
148
168
  function get_base_url(): string {
149
169
  const email_section = read_config_section("hazo_auth__email");
150
170
  const base_url = email_section?.base_url;
151
-
171
+
152
172
  if (base_url) {
153
173
  return base_url.endsWith("/") ? base_url.slice(0, -1) : base_url;
154
174
  }
155
-
175
+
156
176
  // Try to get from APP_DOMAIN_NAME environment variable (adds protocol if needed)
157
177
  const app_domain_name = process.env.APP_DOMAIN_NAME;
158
178
  if (app_domain_name) {
@@ -164,13 +184,13 @@ function get_base_url(): string {
164
184
  // If no protocol, default to https
165
185
  return `https://${domain}`;
166
186
  }
167
-
187
+
168
188
  // Try to get from other environment variables (fallback)
169
189
  const env_base_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
170
190
  if (env_base_url) {
171
191
  return env_base_url.endsWith("/") ? env_base_url.slice(0, -1) : env_base_url;
172
192
  }
173
-
193
+
174
194
  // Default to empty string (will use relative URLs)
175
195
  return "";
176
196
  }
@@ -199,195 +219,6 @@ function get_reset_password_url(token: string): string {
199
219
  return url;
200
220
  }
201
221
 
202
- /**
203
- * Gets default HTML template for a given template type
204
- * @param template_type - Type of email template
205
- * @param data - Template data for variable substitution
206
- * @returns Default HTML template content
207
- */
208
- function get_default_html_template(
209
- template_type: EmailTemplateType,
210
- data: EmailTemplateData
211
- ): string {
212
- switch (template_type) {
213
- case "email_verification":
214
- return `
215
- <!DOCTYPE html>
216
- <html>
217
- <head>
218
- <meta charset="UTF-8">
219
- <title>Verify Your Email</title>
220
- </head>
221
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
222
- <h1 style="color: #0f172a;">Verify Your Email Address</h1>
223
- <p>Thank you for registering! Please click the link below to verify your email address:</p>
224
- <p style="margin: 20px 0;">
225
- <a href="${data.verification_url || "#"}" style="display: inline-block; padding: 12px 24px; background-color: #0f172a; color: #ffffff; text-decoration: none; border-radius: 4px;">Verify Email Address</a>
226
- </p>
227
- <p>Or copy and paste this link into your browser:</p>
228
- <p style="word-break: break-all; color: #666;">${data.verification_url || data.token || ""}</p>
229
- <p>This link will expire in 48 hours.</p>
230
- <p style="margin-top: 30px; color: #666; font-size: 12px;">If you didn't create an account, you can safely ignore this email.</p>
231
- </body>
232
- </html>
233
- `.trim();
234
-
235
- case "forgot_password":
236
- return `
237
- <!DOCTYPE html>
238
- <html>
239
- <head>
240
- <meta charset="UTF-8">
241
- <title>Reset Your Password</title>
242
- </head>
243
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
244
- <h1 style="color: #0f172a;">Reset Your Password</h1>
245
- <p>We received a request to reset your password. Click the link below to reset it:</p>
246
- <p style="margin: 20px 0;">
247
- <a href="${data.reset_url || "#"}" style="display: inline-block; padding: 12px 24px; background-color: #0f172a; color: #ffffff; text-decoration: none; border-radius: 4px;">Reset Password</a>
248
- </p>
249
- <p>Or copy and paste this link into your browser:</p>
250
- <p style="word-break: break-all; color: #666;">${data.reset_url || data.token || ""}</p>
251
- <p>This link will expire in 10 minutes.</p>
252
- <p style="margin-top: 30px; color: #666; font-size: 12px;">If you didn't request a password reset, you can safely ignore this email.</p>
253
- </body>
254
- </html>
255
- `.trim();
256
-
257
- case "password_changed":
258
- return `
259
- <!DOCTYPE html>
260
- <html>
261
- <head>
262
- <meta charset="UTF-8">
263
- <title>Password Changed</title>
264
- </head>
265
- <body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
266
- <h1 style="color: #0f172a;">Password Changed Successfully</h1>
267
- <p>Hello${data.user_name ? ` ${data.user_name}` : ""},</p>
268
- <p>This email confirms that your password has been changed successfully.</p>
269
- <p>If you did not make this change, please contact support immediately to secure your account.</p>
270
- <p style="margin-top: 30px; color: #666; font-size: 12px;">This is an automated notification. Please do not reply to this email.</p>
271
- </body>
272
- </html>
273
- `.trim();
274
-
275
- default:
276
- return "<p>Email content</p>";
277
- }
278
- }
279
-
280
- /**
281
- * Gets default text template for a given template type
282
- * @param template_type - Type of email template
283
- * @param data - Template data for variable substitution
284
- * @returns Default text template content
285
- */
286
- function get_default_text_template(
287
- template_type: EmailTemplateType,
288
- data: EmailTemplateData
289
- ): string {
290
- switch (template_type) {
291
- case "email_verification":
292
- return `
293
- Verify Your Email Address
294
-
295
- Thank you for registering! Please click the link below to verify your email address:
296
-
297
- ${data.verification_url || data.token || ""}
298
-
299
- This link will expire in 48 hours.
300
-
301
- If you didn't create an account, you can safely ignore this email.
302
- `.trim();
303
-
304
- case "forgot_password":
305
- return `
306
- Reset Your Password
307
-
308
- We received a request to reset your password. Click the link below to reset it:
309
-
310
- ${data.reset_url || data.token || ""}
311
-
312
- This link will expire in 10 minutes.
313
-
314
- If you didn't request a password reset, you can safely ignore this email.
315
- `.trim();
316
-
317
- case "password_changed":
318
- return `
319
- Password Changed Successfully
320
-
321
- Hello${data.user_name ? ` ${data.user_name}` : ""},
322
-
323
- This email confirms that your password has been changed successfully.
324
-
325
- If you did not make this change, please contact support immediately to secure your account.
326
-
327
- This is an automated notification. Please do not reply to this email.
328
- `.trim();
329
-
330
- default:
331
- return "Email content";
332
- }
333
- }
334
-
335
- /**
336
- * Loads email template from file system
337
- * @param template_type - Type of email template
338
- * @param extension - File extension (html or txt)
339
- * @returns Template content or undefined if not found
340
- */
341
- function load_template_file(
342
- template_type: EmailTemplateType,
343
- extension: "html" | "txt"
344
- ): string | undefined {
345
- const template_dir = get_email_template_directory();
346
- const template_filename = `${template_type}.${extension}`;
347
- const template_path = path.join(template_dir, template_filename);
348
-
349
- if (!fs.existsSync(template_path)) {
350
- return undefined;
351
- }
352
-
353
- try {
354
- return fs.readFileSync(template_path, "utf-8");
355
- } catch (error) {
356
- const logger = create_app_logger();
357
- logger.error("email_service_template_load_failed", {
358
- filename: "email_service.ts",
359
- line_number: 0,
360
- template_path,
361
- error: error instanceof Error ? error.message : "Unknown error",
362
- });
363
- return undefined;
364
- }
365
- }
366
-
367
- /**
368
- * Simple template variable substitution
369
- * Replaces {{variable_name}} with values from data object
370
- * @param template - Template string with variables
371
- * @param data - Data object for variable substitution
372
- * @returns Template with variables substituted
373
- */
374
- function substitute_template_variables(
375
- template: string,
376
- data: EmailTemplateData
377
- ): string {
378
- let result = template;
379
-
380
- // Replace {{variable}} with values from data
381
- Object.entries(data).forEach(([key, value]) => {
382
- if (value !== undefined) {
383
- const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
384
- result = result.replace(regex, value);
385
- }
386
- });
387
-
388
- return result;
389
- }
390
-
391
222
  /**
392
223
  * Gets email subject for a given template type
393
224
  * @param template_type - Type of email template
@@ -397,13 +228,13 @@ function get_email_subject(template_type: EmailTemplateType): string {
397
228
  const email_section = read_config_section("hazo_auth__email");
398
229
  const template_config_key = `email_template__${template_type}`;
399
230
  const subject_key = `${template_config_key}__subject`;
400
-
231
+
401
232
  // Try to get subject from config
402
233
  const subject = email_section?.[subject_key];
403
234
  if (subject) {
404
235
  return subject;
405
236
  }
406
-
237
+
407
238
  // Default subjects
408
239
  switch (template_type) {
409
240
  case "email_verification":
@@ -417,33 +248,6 @@ function get_email_subject(template_type: EmailTemplateType): string {
417
248
  }
418
249
  }
419
250
 
420
- /**
421
- * Gets email templates (HTML and text) for a given template type
422
- * Falls back to default templates if custom templates are not found
423
- * @param template_type - Type of email template
424
- * @param data - Template data for variable substitution
425
- * @returns Object with html_body and text_body
426
- */
427
- function get_email_templates(
428
- template_type: EmailTemplateType,
429
- data: EmailTemplateData
430
- ): { html_body: string; text_body: string } {
431
- // Try to load custom templates
432
- const html_template = load_template_file(template_type, "html");
433
- const text_template = load_template_file(template_type, "txt");
434
-
435
- // Use custom templates if found, otherwise use defaults
436
- const html_body = html_template
437
- ? substitute_template_variables(html_template, data)
438
- : get_default_html_template(template_type, data);
439
-
440
- const text_body = text_template
441
- ? substitute_template_variables(text_template, data)
442
- : get_default_text_template(template_type, data);
443
-
444
- return { html_body, text_body };
445
- }
446
-
447
251
  /**
448
252
  * Sends an email using hazo_notify
449
253
  * @param options - Email options (to, from, subject, html_body, text_body)
@@ -451,20 +255,20 @@ function get_email_templates(
451
255
  */
452
256
  export async function send_email(options: EmailOptions): Promise<{ success: boolean; error?: string }> {
453
257
  const logger = create_app_logger();
454
-
258
+
455
259
  try {
456
260
  // Get hazo_notify configuration instance
457
261
  const notify_config = await get_hazo_notify_instance();
458
-
262
+
459
263
  // Dynamic import to avoid build-time issues with hazo_notify
460
264
  const hazo_notify_module = await import("hazo_notify");
461
265
  const { send_email: hazo_notify_send_email } = hazo_notify_module;
462
-
266
+
463
267
  // Get from email and from name (hazo_auth_config overrides hazo_notify_config)
464
268
  // Priority: 1. options.from (explicit parameter), 2. hazo_auth_config.from_email, 3. hazo_notify_config.from_email
465
269
  const from_email = options.from || await get_email_from(notify_config);
466
270
  const from_name = await get_email_from_name(notify_config);
467
-
271
+
468
272
  // Prepare hazo_notify email options
469
273
  const hazo_notify_options: SendEmailOptions = {
470
274
  to: options.to,
@@ -477,10 +281,10 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
477
281
  from: from_email,
478
282
  from_name: from_name,
479
283
  };
480
-
284
+
481
285
  // Send email using hazo_notify
482
286
  const result = await hazo_notify_send_email(hazo_notify_options, notify_config);
483
-
287
+
484
288
  if (result.success) {
485
289
  logger.info("email_sent", {
486
290
  filename: "email_service.ts",
@@ -490,11 +294,11 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
490
294
  subject: options.subject,
491
295
  message_id: result.message_id,
492
296
  });
493
-
297
+
494
298
  return { success: true };
495
299
  } else {
496
300
  const error_message = result.error || result.message || "Unknown error";
497
-
301
+
498
302
  logger.error("email_send_failed", {
499
303
  filename: "email_service.ts",
500
304
  line_number: 0,
@@ -504,12 +308,12 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
504
308
  error: error_message,
505
309
  raw_response: result.raw_response,
506
310
  });
507
-
311
+
508
312
  return { success: false, error: error_message };
509
313
  }
510
314
  } catch (error) {
511
315
  const error_message = error instanceof Error ? error.message : "Unknown error";
512
-
316
+
513
317
  logger.error("email_send_failed", {
514
318
  filename: "email_service.ts",
515
319
  line_number: 0,
@@ -518,17 +322,21 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
518
322
  subject: options.subject,
519
323
  error: error_message,
520
324
  });
521
-
325
+
522
326
  return { success: false, error: error_message };
523
327
  }
524
328
  }
525
329
 
526
330
  /**
527
- * Sends an email using a template
528
- * @param template_type - Type of email template
331
+ * Sends an email using a template, delegating rendering to hazo_notify's
332
+ * scope-aware template manager. Templates and per-scope overrides live in
333
+ * hazo_notify's database; hazo_auth ships the global defaults via
334
+ * `hazo_auth_template_manifest`.
335
+ *
336
+ * @param template_type - System template name (email_verification | forgot_password | password_changed)
529
337
  * @param to - Recipient email address
530
- * @param data - Template data for variable substitution
531
- * @returns Promise that resolves when email is sent
338
+ * @param data - Template data; `token` is auto-expanded into verification_url/reset_url
339
+ * @returns Promise that resolves with success status
532
340
  */
533
341
  export async function send_template_email(
534
342
  template_type: EmailTemplateType,
@@ -536,11 +344,13 @@ export async function send_template_email(
536
344
  data: EmailTemplateData
537
345
  ): Promise<{ success: boolean; error?: string }> {
538
346
  const logger = create_app_logger();
539
-
347
+
348
+ // Emit a one-time deprecation warning if the old filesystem override key is set.
349
+ warn_if_template_directory_configured();
350
+
540
351
  try {
541
- // Enhance data with URLs if token is provided
352
+ // URL construction stays here hazo_notify just renders variables.
542
353
  const enhanced_data: EmailTemplateData = { ...data };
543
-
544
354
  if (data.token) {
545
355
  if (template_type === "email_verification") {
546
356
  enhanced_data.verification_url = get_verification_url(data.token);
@@ -548,31 +358,69 @@ export async function send_template_email(
548
358
  enhanced_data.reset_url = get_reset_password_url(data.token);
549
359
  }
550
360
  }
551
-
552
- // Get email templates
553
- const { html_body, text_body } = get_email_templates(template_type, enhanced_data);
554
-
555
- // Get email subject
556
- const subject = get_email_subject(template_type);
557
-
558
- // Get hazo_notify config instance
361
+
362
+ // Coerce `string | undefined` data into `Record<string, string>` for Handlebars.
363
+ const variables: Record<string, string> = {};
364
+ for (const [key, value] of Object.entries(enhanced_data)) {
365
+ if (typeof value === "string") {
366
+ variables[key] = value;
367
+ }
368
+ }
369
+
370
+ if (!hazo_notify_connect) {
371
+ throw new Error(
372
+ "hazo_notify connect not initialized. Call set_hazo_notify_connect() " +
373
+ "from instrumentation.ts with the same HazoConnectInstance you pass " +
374
+ "to init_template_manager({ hazo_connect_factory })."
375
+ );
376
+ }
377
+
559
378
  const notify_config = await get_hazo_notify_instance();
560
-
561
- // Get email from address and from name
562
- // Priority: 1. hazo_auth_config.from_email/from_name, 2. hazo_notify_config.from_email/from_name
563
379
  const from = await get_email_from(notify_config);
564
-
565
- // Send email (from_name is handled inside send_email function)
566
- return await send_email({
380
+ const from_name = await get_email_from_name(notify_config);
381
+ const subject = get_email_subject(template_type);
382
+
383
+ // Dynamic import keeps hazo_notify optional at build time.
384
+ const { send_template_email: notify_send_template_email } = await import(
385
+ "hazo_notify/template_manager"
386
+ );
387
+
388
+ const result = await notify_send_template_email(
389
+ {
390
+ template_name: template_type,
391
+ to,
392
+ variables,
393
+ scope_id: null,
394
+ subject,
395
+ from,
396
+ from_name,
397
+ },
398
+ hazo_notify_connect
399
+ );
400
+
401
+ if (result.success) {
402
+ logger.info("email_sent_via_template", {
403
+ filename: "email_service.ts",
404
+ line_number: 0,
405
+ template_type,
406
+ to,
407
+ message_id: result.message_id,
408
+ });
409
+ return { success: true };
410
+ }
411
+
412
+ const error_message = result.error || result.message || "Unknown error";
413
+ logger.error("email_template_send_failed", {
414
+ filename: "email_service.ts",
415
+ line_number: 0,
416
+ template_type,
567
417
  to,
568
- from,
569
- subject,
570
- html_body,
571
- text_body,
418
+ error: error_message,
572
419
  });
420
+ return { success: false, error: error_message };
573
421
  } catch (error) {
574
422
  const error_message = error instanceof Error ? error.message : "Unknown error";
575
-
423
+
576
424
  logger.error("email_template_send_failed", {
577
425
  filename: "email_service.ts",
578
426
  line_number: 0,
@@ -580,8 +428,7 @@ export async function send_template_email(
580
428
  to,
581
429
  error: error_message,
582
430
  });
583
-
431
+
584
432
  return { success: false, error: error_message };
585
433
  }
586
434
  }
587
-