hazo_auth 8.0.1 → 9.0.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 (71) hide show
  1. package/cli-src/lib/AGENTS.md +26 -0
  2. package/cli-src/lib/app_logger.ts +3 -7
  3. package/cli-src/lib/auth/auth_utils.server.ts +2 -1
  4. package/cli-src/lib/auth/ensure_anon_id.server.ts +2 -1
  5. package/cli-src/lib/auth/hazo_get_auth.server.ts +11 -4
  6. package/cli-src/lib/config/hazo_auth_core_config.ts +44 -0
  7. package/cli-src/lib/cookies_config.server.ts +13 -10
  8. package/cli-src/lib/hazo_connect_setup.server.ts +19 -11
  9. package/cli-src/lib/legal/legal_docs_service.ts +2 -1
  10. package/cli-src/lib/services/email_service.ts +22 -11
  11. package/cli-src/lib/services/firm_service.ts +2 -1
  12. package/cli-src/lib/services/otp_service.ts +3 -2
  13. package/cli-src/lib/services/profile_picture_service.ts +2 -1
  14. package/cli-src/lib/services/relationship_service.ts +5 -4
  15. package/cli-src/lib/services/session_token_service.ts +3 -2
  16. package/cli-src/lib/utils/api_route_helpers.ts +4 -59
  17. package/cli-src/lib/utils/get_origin_url.ts +5 -61
  18. package/cli-src/lib/utils.ts +4 -10
  19. package/config/hazo_auth_config.example.ini +6 -0
  20. package/dist/components/ui/button.d.ts +1 -1
  21. package/dist/components/ui/sheet.d.ts +1 -1
  22. package/dist/lib/app_logger.d.ts +2 -3
  23. package/dist/lib/app_logger.d.ts.map +1 -1
  24. package/dist/lib/app_logger.js +3 -5
  25. package/dist/lib/auth/auth_utils.server.d.ts.map +1 -1
  26. package/dist/lib/auth/auth_utils.server.js +2 -1
  27. package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -1
  28. package/dist/lib/auth/ensure_anon_id.server.js +2 -1
  29. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  30. package/dist/lib/auth/hazo_get_auth.server.js +11 -4
  31. package/dist/lib/config/hazo_auth_core_config.d.ts +44 -0
  32. package/dist/lib/config/hazo_auth_core_config.d.ts.map +1 -0
  33. package/dist/lib/config/hazo_auth_core_config.js +40 -0
  34. package/dist/lib/cookies_config.server.d.ts.map +1 -1
  35. package/dist/lib/cookies_config.server.js +12 -7
  36. package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
  37. package/dist/lib/hazo_connect_setup.server.js +18 -5
  38. package/dist/lib/legal/legal_docs_service.d.ts.map +1 -1
  39. package/dist/lib/legal/legal_docs_service.js +2 -1
  40. package/dist/lib/services/email_service.d.ts +1 -1
  41. package/dist/lib/services/email_service.d.ts.map +1 -1
  42. package/dist/lib/services/email_service.js +21 -9
  43. package/dist/lib/services/firm_service.d.ts.map +1 -1
  44. package/dist/lib/services/firm_service.js +2 -1
  45. package/dist/lib/services/otp_service.d.ts.map +1 -1
  46. package/dist/lib/services/otp_service.js +3 -2
  47. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  48. package/dist/lib/services/profile_picture_service.js +2 -1
  49. package/dist/lib/services/relationship_service.d.ts.map +1 -1
  50. package/dist/lib/services/relationship_service.js +5 -4
  51. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  52. package/dist/lib/services/session_token_service.js +3 -2
  53. package/dist/lib/utils/api_route_helpers.d.ts +1 -12
  54. package/dist/lib/utils/api_route_helpers.d.ts.map +1 -1
  55. package/dist/lib/utils/api_route_helpers.js +4 -57
  56. package/dist/lib/utils/get_origin_url.d.ts +1 -22
  57. package/dist/lib/utils/get_origin_url.d.ts.map +1 -1
  58. package/dist/lib/utils/get_origin_url.js +5 -57
  59. package/dist/lib/utils.d.ts +2 -3
  60. package/dist/lib/utils.d.ts.map +1 -1
  61. package/dist/lib/utils.js +4 -9
  62. package/dist/server/config/config_loader.js +2 -2
  63. package/dist/server/index.js +1 -1
  64. package/dist/server/routes/remove_profile_picture.d.ts.map +1 -1
  65. package/dist/server/routes/remove_profile_picture.js +6 -1
  66. package/dist/server/routes/upload_profile_picture.d.ts.map +1 -1
  67. package/dist/server/routes/upload_profile_picture.js +6 -1
  68. package/dist/server/routes/user_management_users.d.ts +1 -1
  69. package/dist/server/server.d.ts.map +1 -1
  70. package/dist/server/server.js +7 -0
  71. package/package.json +29 -14
@@ -0,0 +1,26 @@
1
+ # src/lib/ — AGENTS.md
2
+
3
+ Server-side business logic, configuration loaders, and utilities.
4
+
5
+ ## Key subdirectories
6
+
7
+ - `auth/` — Core auth logic: `hazo_get_auth.server.ts` (the main auth utility), session token validation, OAuth helpers, auth cache, scope cache, rate limiter.
8
+ - `config/` — Config loaders: `config_loader.server.ts` (hazo_config-based, handles all 18+ INI sections), `hazo_auth_core_config.ts` (hazo_core loadConfig overlay for critical sections with Zod validation).
9
+ - `services/` — Business logic: `login_service.ts`, `registration_service.ts`, `password_reset_service.ts`, `session_token_service.ts`, `scope_service.ts`, `invitation_service.ts`, `email_service.ts`, etc.
10
+ - `auto_test/` — hazo_auth's internal test runner (server-side). This is the original test infrastructure that inspired `hazo_ui/test-harness`. Executes scenario definitions via API calls; the runner is invoked through `/api/hazo_auth/auto_test`.
11
+ - `utils/` — Utilities: `api_route_helpers.ts` (re-exports `get_filename`/`get_line_number` from hazo_logs), `sanitize_error_for_user.ts`.
12
+ - `legal/` — Legal docs management (acceptance tracking, versioned docs).
13
+ - `schema/` — Zod schemas for request validation.
14
+
15
+ ## Error handling (Wave 2)
16
+
17
+ Server-side `throw new Error(...)` replaced with `HazoError` subclasses from `hazo_core`:
18
+ - `HazoAuthError(HAZO_AUTH_FORBIDDEN)` — authentication required or permission denied
19
+ - `HazoAuthError(HAZO_AUTH_INVALID_TOKEN)` — invalid session token
20
+ - `HazoNotFoundError(HAZO_AUTH_USER_NOT_FOUND)` — user not found
21
+ - `HazoRateLimitError(HAZO_AUTH_RATE_LIMITED)` — rate limit exceeded
22
+ - `HazoConfigError(HAZO_AUTH_CONFIG)` — missing/invalid config or env vars
23
+
24
+ ## Logging
25
+
26
+ All logging via `create_app_logger()` from `app_logger.ts`, which returns `createLogger('hazo_auth')` from `hazo_core`. Log calls use structured format: `logger.info('event.name', { fields })`.
@@ -1,9 +1,6 @@
1
- // file_description: server-only wrapper for the main app logging service using hazo_logs
2
- // section: server-only-guard
3
- import "server-only";
4
-
1
+ // file_description: server-only wrapper for the main app logging service using hazo_core
5
2
  // section: imports
6
- import { createLogger } from "hazo_logs";
3
+ import { createLogger } from "hazo_core";
7
4
 
8
5
  // section: logger_instance
9
6
  // Create a singleton logger for the hazo_auth package
@@ -11,7 +8,6 @@ const logger = createLogger("hazo_auth");
11
8
 
12
9
  /**
13
10
  * Returns the hazo_auth logger instance
14
- * Uses hazo_logs for consistent logging across hazo packages
11
+ * Uses hazo_core for consistent logging across hazo packages
15
12
  */
16
13
  export const create_app_logger = () => logger;
17
-
@@ -4,6 +4,7 @@ import "server-only";
4
4
 
5
5
  // section: imports
6
6
  import { NextRequest, NextResponse } from "next/server";
7
+ import { HazoAuthError } from "hazo_core";
7
8
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
8
9
  import { createCrudService } from "hazo_connect/server";
9
10
  import { map_db_source_to_ui } from "../services/profile_picture_source_mapper.js";
@@ -118,7 +119,7 @@ export async function require_auth(request: NextRequest): Promise<AuthUser> {
118
119
  const result = await get_authenticated_user(request);
119
120
 
120
121
  if (!result.authenticated) {
121
- throw new Error("Authentication required");
122
+ throw new HazoAuthError({ code: 'HAZO_AUTH_FORBIDDEN', pkg: 'hazo_auth', message: 'Authentication required' });
122
123
  }
123
124
 
124
125
  return result;
@@ -24,6 +24,7 @@ import "server-only";
24
24
  // section: imports
25
25
  import { NextRequest } from "next/server";
26
26
  import { cookies } from "next/headers";
27
+ import { generateRequestId } from "hazo_core";
27
28
  import {
28
29
  BASE_COOKIE_NAMES,
29
30
  get_cookie_name,
@@ -71,7 +72,7 @@ export async function ensure_anon_id(request: NextRequest): Promise<string> {
71
72
  }
72
73
 
73
74
  // Issue a new id and queue the Set-Cookie via the next/headers cookie store.
74
- const new_id = crypto.randomUUID();
75
+ const new_id = generateRequestId().slice(4);
75
76
  const cookie_options = get_cookie_options({
76
77
  httpOnly: true,
77
78
  secure: process.env.NODE_ENV === "production",
@@ -4,6 +4,7 @@ import "server-only";
4
4
 
5
5
  // section: imports
6
6
  import { NextRequest } from "next/server";
7
+ import { HazoNotFoundError, HazoAuthError, HazoRateLimitError, getCorrelationId } from "hazo_core";
7
8
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
8
9
  import { createCrudService } from "hazo_connect/server";
9
10
  import { create_app_logger } from "../app_logger.js";
@@ -183,14 +184,14 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
183
184
  // Fetch user
184
185
  const users = await users_service.findBy({ id: user_id });
185
186
  if (!Array.isArray(users) || users.length === 0) {
186
- throw new Error("User not found");
187
+ throw new HazoNotFoundError({ code: 'HAZO_AUTH_USER_NOT_FOUND', pkg: 'hazo_auth', message: 'User not found' });
187
188
  }
188
189
 
189
190
  const user_db = users[0];
190
191
 
191
192
  // Check if user is active (status must be 'ACTIVE')
192
193
  if (user_db.status !== "ACTIVE") {
193
- throw new Error("User is inactive");
194
+ throw new HazoAuthError({ code: 'HAZO_AUTH_FORBIDDEN', pkg: 'hazo_auth', message: 'User account is inactive' });
194
195
  }
195
196
 
196
197
  // Build user object
@@ -441,6 +442,7 @@ export async function hazo_get_auth(
441
442
  line_number: get_line_number(),
442
443
  error: token_error_message,
443
444
  note: "Falling back to simple cookie check",
445
+ correlation_id: getCorrelationId(),
444
446
  });
445
447
  }
446
448
  }
@@ -464,8 +466,9 @@ export async function hazo_get_auth(
464
466
  filename: get_filename(),
465
467
  line_number: get_line_number(),
466
468
  ip: client_ip,
469
+ correlation_id: getCorrelationId(),
467
470
  });
468
- throw new Error("Rate limit exceeded. Please try again later.");
471
+ throw new HazoRateLimitError({ code: 'HAZO_AUTH_RATE_LIMITED', pkg: 'hazo_auth', message: 'Rate limit exceeded. Please try again later.' });
469
472
  }
470
473
 
471
474
  return {
@@ -483,8 +486,9 @@ export async function hazo_get_auth(
483
486
  filename: get_filename(),
484
487
  line_number: get_line_number(),
485
488
  user_id,
489
+ correlation_id: getCorrelationId(),
486
490
  });
487
- throw new Error("Rate limit exceeded. Please try again later.");
491
+ throw new HazoRateLimitError({ code: 'HAZO_AUTH_RATE_LIMITED', pkg: 'hazo_auth', message: 'Rate limit exceeded. Please try again later.' });
488
492
  }
489
493
 
490
494
  // Check cache
@@ -516,6 +520,7 @@ export async function hazo_get_auth(
516
520
  line_number: get_line_number(),
517
521
  user_id,
518
522
  error: error_message,
523
+ correlation_id: getCorrelationId(),
519
524
  });
520
525
 
521
526
  return {
@@ -550,6 +555,7 @@ export async function hazo_get_auth(
550
555
  missing_permissions,
551
556
  user_permissions: permissions,
552
557
  ip: client_ip,
558
+ correlation_id: getCorrelationId(),
553
559
  });
554
560
  }
555
561
 
@@ -599,6 +605,7 @@ export async function hazo_get_auth(
599
605
  scope_id: options.scope_id,
600
606
  user_scopes: scope_result.user_scopes,
601
607
  ip: client_ip,
608
+ correlation_id: getCorrelationId(),
602
609
  });
603
610
  }
604
611
 
@@ -0,0 +1,44 @@
1
+ // file_description: Zod-validated config loader for hazo_auth core settings.
2
+ // Covers server-critical sections ([hazo_auth__tokens], [hazo_auth__cookies], [hazo_auth__rate_limit], [log.overrides]).
3
+ // UI sections (login_layout, register_layout, etc.) are still handled by config_loader.server.ts.
4
+ import { z } from 'zod';
5
+ import { loadConfig } from 'hazo_core';
6
+
7
+ const HazoAuthCoreConfigSchema = z.object({
8
+ hazo_auth__tokens: z
9
+ .object({
10
+ access_token_ttl_seconds: z.string().optional().transform(v => v ? parseInt(v, 10) : 900),
11
+ refresh_token_ttl_seconds: z.string().optional().transform(v => v ? parseInt(v, 10) : 2592000),
12
+ })
13
+ .optional()
14
+ .transform(v => v ?? { access_token_ttl_seconds: 900, refresh_token_ttl_seconds: 2592000 }),
15
+ hazo_auth__cookies: z
16
+ .object({
17
+ cookie_prefix: z.string().optional().default(''),
18
+ cookie_domain: z.string().optional().default(''),
19
+ })
20
+ .optional()
21
+ .transform(v => v ?? { cookie_prefix: '', cookie_domain: '' }),
22
+ hazo_auth__rate_limit: z
23
+ .object({
24
+ max_attempts: z.string().optional().transform(v => v ? parseInt(v, 10) : 5),
25
+ window_minutes: z.string().optional().transform(v => v ? parseInt(v, 10) : 5),
26
+ })
27
+ .optional()
28
+ .transform(v => v ?? { max_attempts: 5, window_minutes: 5 }),
29
+ log: z
30
+ .object({
31
+ overrides: z.record(z.string(), z.string()).optional().default({}),
32
+ })
33
+ .optional()
34
+ .transform(v => v ?? { overrides: {} }),
35
+ });
36
+
37
+ export type HazoAuthCoreConfig = z.infer<typeof HazoAuthCoreConfigSchema>;
38
+
39
+ export function getHazoAuthCoreConfig(): HazoAuthCoreConfig {
40
+ return loadConfig<HazoAuthCoreConfig>({
41
+ pkg: 'hazo_auth',
42
+ schema: HazoAuthCoreConfigSchema as never,
43
+ });
44
+ }
@@ -2,7 +2,7 @@
2
2
  // section: server-only-guard
3
3
  import "server-only";
4
4
 
5
-
5
+ import { HazoConfigError } from "hazo_core";
6
6
  import { read_config_section } from "./config/config_loader.server.js";
7
7
 
8
8
  // section: types
@@ -44,15 +44,18 @@ export function get_cookies_config(): CookiesConfig {
44
44
  const cookie_prefix = section?.cookie_prefix || "";
45
45
 
46
46
  if (!cookie_prefix) {
47
- throw new Error(
48
- "[hazo_auth] cookie_prefix is required but not configured.\n" +
49
- "Set cookie_prefix in [hazo_auth__cookies] section of config/hazo_auth_config.ini:\n\n" +
50
- " [hazo_auth__cookies]\n" +
51
- " cookie_prefix = myapp_\n\n" +
52
- "Also set the matching environment variable for Edge runtime (middleware):\n" +
53
- " HAZO_AUTH_COOKIE_PREFIX=myapp_\n\n" +
54
- "This prevents cookie conflicts between apps using hazo_auth."
55
- );
47
+ throw new HazoConfigError({
48
+ code: 'HAZO_AUTH_CONFIG',
49
+ pkg: 'hazo_auth',
50
+ message:
51
+ "[hazo_auth] cookie_prefix is required but not configured.\n" +
52
+ "Set cookie_prefix in [hazo_auth__cookies] section of config/hazo_auth_config.ini:\n\n" +
53
+ " [hazo_auth__cookies]\n" +
54
+ " cookie_prefix = myapp_\n\n" +
55
+ "Also set the matching environment variable for Edge runtime (middleware):\n" +
56
+ " HAZO_AUTH_COOKIE_PREFIX=myapp_\n\n" +
57
+ "This prevents cookie conflicts between apps using hazo_auth.",
58
+ });
56
59
  }
57
60
 
58
61
  return {
@@ -7,6 +7,7 @@ import "server-only";
7
7
  // section: imports
8
8
  import { createHazoConnect } from "hazo_connect/server";
9
9
  import { HazoConfig } from "hazo_config/server";
10
+ import { HazoConfigError } from "hazo_core";
10
11
  import path from "path";
11
12
  import fs from "fs";
12
13
  import { create_app_logger } from "./app_logger.js";
@@ -79,10 +80,13 @@ function get_hazo_connect_config(): {
79
80
  );
80
81
  sqlite_path = path.normalize(fallback_sqlite_path);
81
82
  } else {
82
- throw new Error(
83
- "[hazo_auth] sqlite_path not configured. Set sqlite_path in [hazo_connect] section of config/hazo_auth_config.ini, " +
84
- "or set HAZO_CONNECT_SQLITE_PATH environment variable."
85
- );
83
+ throw new HazoConfigError({
84
+ code: 'HAZO_AUTH_CONFIG',
85
+ pkg: 'hazo_auth',
86
+ message:
87
+ "[hazo_auth] sqlite_path not configured. Set sqlite_path in [hazo_connect] section of config/hazo_auth_config.ini, " +
88
+ "or set HAZO_CONNECT_SQLITE_PATH environment variable.",
89
+ });
86
90
  }
87
91
 
88
92
  // Validate config keys for typos
@@ -132,9 +136,11 @@ function get_hazo_connect_config(): {
132
136
  process.env.POSTGREST_API_KEY;
133
137
 
134
138
  if (!postgrest_url) {
135
- throw new Error(
136
- "PostgREST URL is required. Set postgrest_url in [hazo_connect] section of hazo_auth_config.ini or HAZO_CONNECT_POSTGREST_URL environment variable."
137
- );
139
+ throw new HazoConfigError({
140
+ code: 'HAZO_AUTH_CONFIG',
141
+ pkg: 'hazo_auth',
142
+ message: 'PostgREST URL is required. Set postgrest_url in [hazo_connect] section of hazo_auth_config.ini or HAZO_CONNECT_POSTGREST_URL environment variable.',
143
+ });
138
144
  }
139
145
 
140
146
  return {
@@ -145,9 +151,11 @@ function get_hazo_connect_config(): {
145
151
  };
146
152
  }
147
153
 
148
- throw new Error(
149
- `Unsupported HAZO_CONNECT_TYPE: ${type}. Supported types: sqlite, postgrest`
150
- );
154
+ throw new HazoConfigError({
155
+ code: 'HAZO_AUTH_CONFIG',
156
+ pkg: 'hazo_auth',
157
+ message: `Unsupported HAZO_CONNECT_TYPE: ${type}. Supported types: sqlite, postgrest`,
158
+ });
151
159
  }
152
160
 
153
161
  /**
@@ -177,7 +185,7 @@ export function create_sqlite_hazo_connect_server() {
177
185
  });
178
186
  }
179
187
 
180
- throw new Error(`Unsupported database type: ${config.type}`);
188
+ throw new HazoConfigError({ code: 'HAZO_AUTH_CONFIG', pkg: 'hazo_auth', message: `Unsupported database type: ${config.type}` });
181
189
  }
182
190
 
183
191
  /**
@@ -1,12 +1,13 @@
1
1
  // file_description: service functions for the legal document acceptance system
2
2
  // section: imports
3
3
  import { createCrudService } from 'hazo_connect/server';
4
+ import { generateRequestId } from 'hazo_core';
4
5
  import type { LegalAcceptanceMap } from './legal_docs_types';
5
6
 
6
7
  // section: helpers
7
8
 
8
9
  function generate_id(): string {
9
- return crypto.randomUUID();
10
+ return generateRequestId().slice(4);
10
11
  }
11
12
 
12
13
  // section: exports
@@ -1,8 +1,9 @@
1
1
  // file_description: service for sending emails with template support
2
2
  // section: imports
3
3
  import { create_app_logger } from "../app_logger.js";
4
+ import { HazoConfigError, optional_import } from "hazo_core";
4
5
  import { read_config_section } from "../config/config_loader.server.js";
5
- import type { EmailerConfig, SendEmailOptions } from "hazo_notify";
6
+ import type { EmailerConfig, SendEmailOptions } from "hazo_notify/adapters/email";
6
7
  import type { HazoConnectInstance } from "hazo_notify/template_manager";
7
8
 
8
9
  // section: types
@@ -76,8 +77,11 @@ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
76
77
  });
77
78
  try {
78
79
  // Dynamic import to avoid build-time issues with hazo_notify
79
- const hazo_notify_module = await import("hazo_notify");
80
- const { load_emailer_config } = hazo_notify_module;
80
+ const hazo_notify_email_module = await optional_import<typeof import("hazo_notify/adapters/email")>('hazo_notify/adapters/email');
81
+ if (!hazo_notify_email_module) {
82
+ throw new Error("hazo_notify is not installed");
83
+ }
84
+ const { load_emailer_config } = hazo_notify_email_module;
81
85
  hazo_notify_config = load_emailer_config();
82
86
  } catch (error) {
83
87
  const error_message = error instanceof Error ? error.message : "Unknown error";
@@ -86,7 +90,7 @@ async function get_hazo_notify_instance(): Promise<EmailerConfig> {
86
90
  line_number: 0,
87
91
  error: error_message,
88
92
  });
89
- throw new Error(`Failed to load hazo_notify config: ${error_message}`);
93
+ throw new HazoConfigError({ code: 'HAZO_AUTH_CONFIG', pkg: 'hazo_auth', message: `Failed to load hazo_notify config: ${error_message}` });
90
94
  }
91
95
  }
92
96
  return hazo_notify_config;
@@ -263,8 +267,12 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
263
267
  const notify_config = await get_hazo_notify_instance();
264
268
 
265
269
  // Dynamic import to avoid build-time issues with hazo_notify
266
- const hazo_notify_module = await import("hazo_notify");
267
- const { send_email: hazo_notify_send_email } = hazo_notify_module;
270
+ const hazo_notify_email_module = await optional_import<typeof import("hazo_notify/adapters/email")>('hazo_notify/adapters/email');
271
+ if (!hazo_notify_email_module) {
272
+ throw new Error("hazo_notify is not installed");
273
+ }
274
+ const { get_email_provider } = hazo_notify_email_module;
275
+ const provider = get_email_provider(notify_config);
268
276
 
269
277
  // Get from email and from name (hazo_auth_config overrides hazo_notify_config)
270
278
  // Priority: 1. options.from (explicit parameter), 2. hazo_auth_config.from_email, 3. hazo_notify_config.from_email
@@ -285,7 +293,7 @@ export async function send_email(options: EmailOptions): Promise<{ success: bool
285
293
  };
286
294
 
287
295
  // Send email using hazo_notify
288
- const result = await hazo_notify_send_email(hazo_notify_options, notify_config);
296
+ const result = await provider.send_email(hazo_notify_options, notify_config);
289
297
 
290
298
  if (result.success) {
291
299
  logger.info("email_sent", {
@@ -370,11 +378,14 @@ export async function send_template_email(
370
378
  }
371
379
 
372
380
  if (!hazo_notify_connect) {
373
- throw new Error(
374
- "hazo_notify connect not initialized. Call set_hazo_notify_connect() " +
381
+ throw new HazoConfigError({
382
+ code: 'HAZO_AUTH_CONFIG',
383
+ pkg: 'hazo_auth',
384
+ message:
385
+ "hazo_notify connect not initialized. Call set_hazo_notify_connect() " +
375
386
  "from instrumentation.ts with the same HazoConnectInstance you pass " +
376
- "to init_template_manager({ hazo_connect_factory })."
377
- );
387
+ "to init_template_manager({ hazo_connect_factory }).",
388
+ });
378
389
  }
379
390
 
380
391
  const notify_config = await get_hazo_notify_instance();
@@ -2,6 +2,7 @@
2
2
  // section: imports
3
3
  import type { HazoConnectAdapter } from "hazo_connect";
4
4
  import { createCrudService } from "hazo_connect/server";
5
+ import { generateRequestId } from "hazo_core";
5
6
  import { create_app_logger } from "../app_logger.js";
6
7
  import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
7
8
  import { create_scope, type ScopeRecord } from "./scope_service.js";
@@ -146,7 +147,7 @@ async function assign_owner_permissions(
146
147
 
147
148
  if (!Array.isArray(permissions) || permissions.length === 0) {
148
149
  // Create permission with generated UUID
149
- const perm_id = crypto.randomUUID();
150
+ const perm_id = generateRequestId().slice(4);
150
151
  const inserted = await permission_service.insert({
151
152
  id: perm_id,
152
153
  permission_name: perm_name,
@@ -1,5 +1,6 @@
1
1
  import "server-only";
2
2
  import crypto from "node:crypto";
3
+ import { generateRequestId } from "hazo_core";
3
4
  import argon2 from "argon2";
4
5
  import { createCrudService } from "hazo_connect/server";
5
6
  import { get_otp_config, hazo_auth_otp_session_ttl_seconds } from "../otp_config.server.js";
@@ -133,7 +134,7 @@ export async function request_email_otp(args: {
133
134
  const code = generate_otp_code();
134
135
  const otp_hash = await hash_otp_code(code);
135
136
  const expires_at = new Date(Date.now() + cfg.code_ttl_seconds * 1000).toISOString();
136
- const row_id = crypto.randomUUID();
137
+ const row_id = generateRequestId().slice(4);
137
138
 
138
139
  await otp_table.insert({
139
140
  id: row_id,
@@ -244,7 +245,7 @@ export async function verify_email_otp(args: {
244
245
  }
245
246
  } else if (cfg.auto_register) {
246
247
  // Create user + bind scope/role
247
- const new_user_id = crypto.randomUUID();
248
+ const new_user_id = generateRequestId().slice(4);
248
249
  await users_table.insert({
249
250
  id: new_user_id,
250
251
  email_address: email,
@@ -3,6 +3,7 @@
3
3
  import type { HazoConnectAdapter } from "hazo_connect";
4
4
  import { createCrudService } from "hazo_connect/server";
5
5
  import gravatarUrl from "gravatar-url";
6
+ import { fetchWithRequestId } from "hazo_core";
6
7
  import { get_profile_picture_config } from "../profile_picture_config.server.js";
7
8
  import { get_ui_sizes_config } from "../ui_sizes_config.server.js";
8
9
  import { get_file_types_config } from "../file_types_config.server.js";
@@ -304,7 +305,7 @@ async function check_gravatar_exists(email: string): Promise<boolean> {
304
305
  const gravatar_url = get_gravatar_url(email, uiSizes.gravatar_size);
305
306
 
306
307
  // Make HEAD request to check if image exists without downloading it
307
- const response = await fetch(gravatar_url, {
308
+ const response = await fetchWithRequestId(gravatar_url, {
308
309
  method: 'HEAD',
309
310
  // Add timeout to prevent hanging
310
311
  signal: AbortSignal.timeout(5000) // 5 second timeout
@@ -2,6 +2,7 @@
2
2
  // section: imports
3
3
  import type { HazoConnectAdapter } from "hazo_connect";
4
4
  import { createCrudService } from "hazo_connect/server";
5
+ import { generateRequestId } from "hazo_core";
5
6
  import argon2 from "argon2";
6
7
  import { create_app_logger } from "../app_logger.js";
7
8
  import { get_relationships_config, get_allowed_relationship_types } from "../relationships_config.server.js";
@@ -66,7 +67,7 @@ export function get_display_email(email: string | null | undefined): string | nu
66
67
  }
67
68
 
68
69
  function generate_sentinel_email(): string {
69
- return `${SENTINEL_PREFIX}${crypto.randomUUID()}${SENTINEL_DOMAIN}`;
70
+ return `${SENTINEL_PREFIX}${generateRequestId().slice(4)}${SENTINEL_DOMAIN}`;
70
71
  }
71
72
 
72
73
  // section: helpers
@@ -129,8 +130,8 @@ export async function create_managed_child(
129
130
  }
130
131
 
131
132
  // Generate IDs
132
- const child_user_id = crypto.randomUUID();
133
- const relationship_id = crypto.randomUUID();
133
+ const child_user_id = generateRequestId().slice(4);
134
+ const relationship_id = generateRequestId().slice(4);
134
135
  const now = new Date().toISOString();
135
136
 
136
137
  // Insert managed child user
@@ -375,7 +376,7 @@ export async function create_self_relationship(
375
376
  }
376
377
 
377
378
  const config = get_relationships_config();
378
- const relationship_id = crypto.randomUUID();
379
+ const relationship_id = generateRequestId().slice(4);
379
380
  const now = new Date().toISOString();
380
381
 
381
382
  await relationships_service.insert({
@@ -2,6 +2,7 @@
2
2
  // Uses jose library for Edge-compatible JWT operations
3
3
  // section: imports
4
4
  import { SignJWT, jwtVerify } from "jose";
5
+ import { HazoConfigError, HazoAuthError } from "hazo_core";
5
6
  import { create_app_logger } from "../app_logger.js";
6
7
  import { get_filename, get_line_number } from "../utils/api_route_helpers.js";
7
8
 
@@ -37,7 +38,7 @@ function get_jwt_secret(): Uint8Array {
37
38
  line_number: get_line_number(),
38
39
  error: "JWT_SECRET environment variable is required",
39
40
  });
40
- throw new Error("JWT_SECRET environment variable is required");
41
+ throw new HazoConfigError({ code: 'HAZO_AUTH_CONFIG', pkg: 'hazo_auth', message: 'JWT_SECRET environment variable is required' });
41
42
  }
42
43
 
43
44
  // Convert string secret to Uint8Array for jose
@@ -112,7 +113,7 @@ export async function create_session_token(
112
113
  error_stack,
113
114
  });
114
115
 
115
- throw new Error("Failed to create session token");
116
+ throw new HazoAuthError({ code: 'HAZO_AUTH_INVALID_TOKEN', pkg: 'hazo_auth', message: 'Failed to create session token' });
116
117
  }
117
118
  }
118
119
 
@@ -1,60 +1,5 @@
1
1
  // file_description: shared helper functions for API routes to get filename and line number
2
- // section: helpers
3
- /**
4
- * Gets the filename from the call stack
5
- * This is a simplified version that extracts the filename from the error stack
6
- * @returns Filename or "route.ts" as default
7
- */
8
- export function get_filename(): string {
9
- try {
10
- const stack = new Error().stack;
11
- if (!stack) {
12
- return "route.ts";
13
- }
14
-
15
- // Parse stack trace to find the caller's file
16
- const lines = stack.split("\n");
17
- // Skip Error line and get_filename line, get the actual caller
18
- for (let i = 2; i < lines.length; i++) {
19
- const line = lines[i];
20
- // Match file paths in stack trace (e.g., "at /path/to/file.ts:123:45")
21
- const match = line.match(/([^/\\]+\.tsx?):(\d+):(\d+)/);
22
- if (match) {
23
- return match[1];
24
- }
25
- }
26
- return "route.ts";
27
- } catch {
28
- return "route.ts";
29
- }
30
- }
31
-
32
- /**
33
- * Gets the line number from the call stack
34
- * This is a simplified version that extracts the line number from the error stack
35
- * @returns Line number or 0
36
- */
37
- export function get_line_number(): number {
38
- try {
39
- const stack = new Error().stack;
40
- if (!stack) {
41
- return 0;
42
- }
43
-
44
- // Parse stack trace to find the caller's line number
45
- const lines = stack.split("\n");
46
- // Skip Error line and get_line_number line, get the actual caller
47
- for (let i = 2; i < lines.length; i++) {
48
- const line = lines[i];
49
- // Match line numbers in stack trace (e.g., "at /path/to/file.ts:123:45")
50
- const match = line.match(/([^/\\]+\.tsx?):(\d+):(\d+)/);
51
- if (match) {
52
- return parseInt(match[2], 10) || 0;
53
- }
54
- }
55
- return 0;
56
- } catch {
57
- return 0;
58
- }
59
- }
60
-
2
+ // Canonical location moved to hazo_logs/src/lib/utils/caller_info.ts.
3
+ // This re-export maintains backward compatibility for hazo_auth consumers.
4
+ // Will be removed in hazo_auth v9 — import from 'hazo_logs' directly.
5
+ export { get_filename, get_line_number } from 'hazo_logs';
@@ -1,61 +1,5 @@
1
- // file_description: Resolves the public-facing origin URL for constructing redirects
2
- // When running behind a reverse proxy (Cloudflare, nginx), request.url resolves to
3
- // the internal address (e.g. http://localhost:3000). This utility returns the correct
4
- // public origin using NEXTAUTH_URL, falling back to request.url.
5
-
6
- /**
7
- * Gets the public-facing origin URL for redirect construction.
8
- *
9
- * Behind reverse proxies (Cloudflare, nginx, etc.), `request.url` contains the
10
- * internal address (e.g. `http://localhost:3000`), not the public domain.
11
- * This function returns the correct origin from environment variables.
12
- *
13
- * Priority: NEXTAUTH_URL > APP_DOMAIN_NAME > NEXT_PUBLIC_APP_URL > APP_URL > request.url
14
- *
15
- * @param request_url - The request.url to use as fallback
16
- * @returns The origin URL (e.g. "https://gotimer.org")
17
- */
18
- export function get_origin_url(request_url: string): string {
19
- // NEXTAUTH_URL is the standard for NextAuth.js apps
20
- const nextauth_url = process.env.NEXTAUTH_URL;
21
- if (nextauth_url) {
22
- return nextauth_url.replace(/\/$/, "");
23
- }
24
-
25
- // APP_DOMAIN_NAME (with protocol handling)
26
- const app_domain = process.env.APP_DOMAIN_NAME;
27
- if (app_domain) {
28
- const domain = app_domain.trim();
29
- if (domain.startsWith("http://") || domain.startsWith("https://")) {
30
- return domain.replace(/\/$/, "");
31
- }
32
- return `https://${domain}`;
33
- }
34
-
35
- // Other common env vars
36
- const env_url = process.env.NEXT_PUBLIC_APP_URL || process.env.APP_URL;
37
- if (env_url) {
38
- return env_url.replace(/\/$/, "");
39
- }
40
-
41
- // Fallback to request.url (works in development without a proxy)
42
- try {
43
- const url = new URL(request_url);
44
- return url.origin;
45
- } catch {
46
- return request_url;
47
- }
48
- }
49
-
50
- /**
51
- * Creates a URL using the public-facing origin instead of request.url.
52
- * Drop-in replacement for `new URL(path, request.url)` in route handlers.
53
- *
54
- * @param path - The path or relative URL (e.g. "/hazo_auth/login")
55
- * @param request_url - The request.url (used as fallback only)
56
- * @returns A URL object with the correct public origin
57
- */
58
- export function create_redirect_url(path: string, request_url: string): URL {
59
- const origin = get_origin_url(request_url);
60
- return new URL(path, origin);
61
- }
1
+ // file_description: Re-exports get_origin_url and create_redirect_url from hazo_core.
2
+ // Canonical location moved to hazo_core/http in hazo_core v1.
3
+ // This re-export maintains backward compatibility for hazo_auth consumers.
4
+ // Will be removed in hazo_auth v9 import from 'hazo_core' directly.
5
+ export { get_origin_url, create_redirect_url } from 'hazo_core';