hazo_auth 7.0.2 → 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 (145) hide show
  1. package/README.md +34 -0
  2. package/SETUP_CHECKLIST.md +31 -0
  3. package/cli-src/lib/AGENTS.md +26 -0
  4. package/cli-src/lib/app_logger.ts +3 -7
  5. package/cli-src/lib/auth/auth_types.ts +3 -0
  6. package/cli-src/lib/auth/auth_utils.server.ts +2 -1
  7. package/cli-src/lib/auth/ensure_anon_id.server.ts +2 -1
  8. package/cli-src/lib/auth/hazo_get_auth.server.ts +30 -4
  9. package/cli-src/lib/config/hazo_auth_core_config.ts +44 -0
  10. package/cli-src/lib/cookies_config.server.ts +13 -10
  11. package/cli-src/lib/hazo_connect_setup.server.ts +19 -11
  12. package/cli-src/lib/legal/legal_docs_config.server.ts +61 -0
  13. package/cli-src/lib/legal/legal_docs_reader.server.ts +36 -0
  14. package/cli-src/lib/legal/legal_docs_service.ts +197 -0
  15. package/cli-src/lib/legal/legal_docs_types.ts +31 -0
  16. package/cli-src/lib/services/email_service.ts +22 -11
  17. package/cli-src/lib/services/firm_service.ts +2 -1
  18. package/cli-src/lib/services/otp_service.ts +3 -2
  19. package/cli-src/lib/services/profile_picture_service.ts +2 -1
  20. package/cli-src/lib/services/registration_service.ts +16 -1
  21. package/cli-src/lib/services/relationship_service.ts +5 -4
  22. package/cli-src/lib/services/session_token_service.ts +3 -2
  23. package/cli-src/lib/utils/api_route_helpers.ts +4 -59
  24. package/cli-src/lib/utils/get_origin_url.ts +5 -61
  25. package/cli-src/lib/utils.ts +4 -10
  26. package/config/hazo_auth_config.example.ini +6 -0
  27. package/dist/client.d.ts +1 -0
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +3 -0
  30. package/dist/components/layouts/index.d.ts +1 -0
  31. package/dist/components/layouts/index.d.ts.map +1 -1
  32. package/dist/components/layouts/index.js +2 -0
  33. package/dist/components/layouts/legal/index.d.ts +5 -0
  34. package/dist/components/layouts/legal/index.d.ts.map +1 -0
  35. package/dist/components/layouts/legal/index.js +4 -0
  36. package/dist/components/layouts/legal/legal_acceptance_gate.d.ts +7 -0
  37. package/dist/components/layouts/legal/legal_acceptance_gate.d.ts.map +1 -0
  38. package/dist/components/layouts/legal/legal_acceptance_gate.js +84 -0
  39. package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts +9 -0
  40. package/dist/components/layouts/legal/legal_doc_checkbox_list.d.ts.map +1 -0
  41. package/dist/components/layouts/legal/legal_doc_checkbox_list.js +11 -0
  42. package/dist/components/layouts/legal/legal_doc_combined_view.d.ts +9 -0
  43. package/dist/components/layouts/legal/legal_doc_combined_view.d.ts.map +1 -0
  44. package/dist/components/layouts/legal/legal_doc_combined_view.js +11 -0
  45. package/dist/components/layouts/legal/legal_doc_drawer.d.ts +8 -0
  46. package/dist/components/layouts/legal/legal_doc_drawer.d.ts.map +1 -0
  47. package/dist/components/layouts/legal/legal_doc_drawer.js +55 -0
  48. package/dist/components/layouts/register/hooks/use_register_form.d.ts +5 -1
  49. package/dist/components/layouts/register/hooks/use_register_form.d.ts.map +1 -1
  50. package/dist/components/layouts/register/hooks/use_register_form.js +25 -10
  51. package/dist/components/layouts/register/index.d.ts.map +1 -1
  52. package/dist/components/layouts/register/index.js +21 -1
  53. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  54. package/dist/components/layouts/user_management/index.js +45 -7
  55. package/dist/components/ui/input-otp.d.ts +2 -2
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/lib/app_logger.d.ts +2 -3
  59. package/dist/lib/app_logger.d.ts.map +1 -1
  60. package/dist/lib/app_logger.js +3 -5
  61. package/dist/lib/auth/auth_types.d.ts +2 -0
  62. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  63. package/dist/lib/auth/auth_types.js +0 -2
  64. package/dist/lib/auth/auth_utils.server.d.ts.map +1 -1
  65. package/dist/lib/auth/auth_utils.server.js +2 -1
  66. package/dist/lib/auth/ensure_anon_id.server.d.ts.map +1 -1
  67. package/dist/lib/auth/ensure_anon_id.server.js +2 -1
  68. package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
  69. package/dist/lib/auth/hazo_get_auth.server.js +30 -4
  70. package/dist/lib/config/hazo_auth_core_config.d.ts +44 -0
  71. package/dist/lib/config/hazo_auth_core_config.d.ts.map +1 -0
  72. package/dist/lib/config/hazo_auth_core_config.js +40 -0
  73. package/dist/lib/cookies_config.server.d.ts.map +1 -1
  74. package/dist/lib/cookies_config.server.js +12 -7
  75. package/dist/lib/hazo_connect_setup.server.d.ts.map +1 -1
  76. package/dist/lib/hazo_connect_setup.server.js +18 -5
  77. package/dist/lib/legal/legal_docs_config.server.d.ts +22 -0
  78. package/dist/lib/legal/legal_docs_config.server.d.ts.map +1 -0
  79. package/dist/lib/legal/legal_docs_config.server.js +52 -0
  80. package/dist/lib/legal/legal_docs_reader.server.d.ts +15 -0
  81. package/dist/lib/legal/legal_docs_reader.server.d.ts.map +1 -0
  82. package/dist/lib/legal/legal_docs_reader.server.js +24 -0
  83. package/dist/lib/legal/legal_docs_service.d.ts +49 -0
  84. package/dist/lib/legal/legal_docs_service.d.ts.map +1 -0
  85. package/dist/lib/legal/legal_docs_service.js +141 -0
  86. package/dist/lib/legal/legal_docs_types.d.ts +25 -0
  87. package/dist/lib/legal/legal_docs_types.d.ts.map +1 -0
  88. package/dist/lib/legal/legal_docs_types.js +2 -0
  89. package/dist/lib/services/email_service.d.ts +1 -1
  90. package/dist/lib/services/email_service.d.ts.map +1 -1
  91. package/dist/lib/services/email_service.js +21 -9
  92. package/dist/lib/services/firm_service.d.ts.map +1 -1
  93. package/dist/lib/services/firm_service.js +2 -1
  94. package/dist/lib/services/otp_service.d.ts.map +1 -1
  95. package/dist/lib/services/otp_service.js +3 -2
  96. package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
  97. package/dist/lib/services/profile_picture_service.js +2 -1
  98. package/dist/lib/services/registration_service.d.ts +5 -0
  99. package/dist/lib/services/registration_service.d.ts.map +1 -1
  100. package/dist/lib/services/registration_service.js +6 -0
  101. package/dist/lib/services/relationship_service.d.ts.map +1 -1
  102. package/dist/lib/services/relationship_service.js +5 -4
  103. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  104. package/dist/lib/services/session_token_service.js +3 -2
  105. package/dist/lib/utils/api_route_helpers.d.ts +1 -12
  106. package/dist/lib/utils/api_route_helpers.d.ts.map +1 -1
  107. package/dist/lib/utils/api_route_helpers.js +4 -57
  108. package/dist/lib/utils/get_origin_url.d.ts +1 -22
  109. package/dist/lib/utils/get_origin_url.d.ts.map +1 -1
  110. package/dist/lib/utils/get_origin_url.js +5 -57
  111. package/dist/lib/utils.d.ts +2 -3
  112. package/dist/lib/utils.d.ts.map +1 -1
  113. package/dist/lib/utils.js +4 -9
  114. package/dist/page_components/index.d.ts +0 -5
  115. package/dist/page_components/index.d.ts.map +1 -1
  116. package/dist/page_components/index.js +0 -5
  117. package/dist/server/config/config_loader.js +2 -2
  118. package/dist/server/index.js +1 -1
  119. package/dist/server/routes/index.d.ts +3 -0
  120. package/dist/server/routes/index.d.ts.map +1 -1
  121. package/dist/server/routes/index.js +4 -0
  122. package/dist/server/routes/legal_docs_accept.d.ts +3 -0
  123. package/dist/server/routes/legal_docs_accept.d.ts.map +1 -0
  124. package/dist/server/routes/legal_docs_accept.js +43 -0
  125. package/dist/server/routes/legal_docs_get.d.ts +3 -0
  126. package/dist/server/routes/legal_docs_get.d.ts.map +1 -0
  127. package/dist/server/routes/legal_docs_get.js +49 -0
  128. package/dist/server/routes/legal_docs_publish.d.ts +3 -0
  129. package/dist/server/routes/legal_docs_publish.d.ts.map +1 -0
  130. package/dist/server/routes/legal_docs_publish.js +35 -0
  131. package/dist/server/routes/register.d.ts.map +1 -1
  132. package/dist/server/routes/register.js +26 -0
  133. package/dist/server/routes/remove_profile_picture.d.ts.map +1 -1
  134. package/dist/server/routes/remove_profile_picture.js +6 -1
  135. package/dist/server/routes/upload_profile_picture.d.ts.map +1 -1
  136. package/dist/server/routes/upload_profile_picture.js +6 -1
  137. package/dist/server/routes/user_management_users.d.ts +2 -2
  138. package/dist/server/routes/user_management_users.d.ts.map +1 -1
  139. package/dist/server/routes/user_management_users.js +46 -2
  140. package/dist/server/server.d.ts.map +1 -1
  141. package/dist/server/server.js +7 -0
  142. package/dist/strings.d.ts +2 -0
  143. package/dist/strings.d.ts.map +1 -0
  144. package/dist/strings.js +3 -0
  145. package/package.json +33 -35
package/README.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
4
4
 
5
+ ### What's New in v8.0.1 🔧
6
+
7
+ **Auto-test and middleware bug fixes**
8
+
9
+ - Fixed 307 redirects blocking OTP, strings, consent, and legal_docs API routes when hit without auth cookies — all four are now in `middleware.ts` `public_routes`.
10
+ - Auto-test runner register calls now include `legal_accepted` hashes when legal docs are configured, fixing 24 test failures that cascaded from the initial registration step.
11
+
12
+ ### What's New in v8.0.0 ⚠️ BREAKING CHANGE
13
+
14
+ **Legal Document Acceptance** — opt-in, INI-configured. Add `[hazo_auth__legal_docs]` to `hazo_auth_config.ini` to require users to accept terms before registering. Each doc is a markdown file; hazo_auth hashes it, tracks acceptance history, and blocks register until all current hashes are accepted.
15
+
16
+ **Breaking:** Deprecated page-component wrappers removed (`LoginPage`, `RegisterPage`, `ForgotPasswordPage`, `ResetPasswordPage`, `VerifyEmailPage` from `hazo_auth/page_components/*`). Use `*Layout` components directly in a server component instead.
17
+
18
+ Run these migrations (in order) to upgrade an existing database:
19
+ - `017_legal_acceptance_column.sql`
20
+ - `018_hazo_legal_acceptances.sql`
21
+ - `019_hazo_legal_doc_versions.sql`
22
+
23
+ **New exports** — `LegalAcceptanceGate`, `LegalDocDrawer`, `LegalDocCheckboxList`, `LegalDocCombinedView` from `hazo_auth/client`; `legalDocsGET`, `legalDocsAcceptPOST`, `legalDocsPublishPOST` from `hazo_auth/server/routes`; `LegalDoc`, `LegalAcceptanceRecord`, `LegalAcceptanceMap` types from `hazo_auth`.
24
+
25
+ **New dependency** — `react-markdown` (markdown rendering for legal doc drawers).
26
+
27
+ ```ini
28
+ # config/hazo_auth_config.ini
29
+ [hazo_auth__legal_docs]
30
+ display_mode = separate # separate | combined
31
+ doc_1_key = tos
32
+ doc_1_title = Terms of Service
33
+ doc_1_path = legal/tos.md
34
+ doc_2_key = privacy
35
+ doc_2_title = Privacy Policy
36
+ doc_2_path = legal/privacy.md
37
+ ```
38
+
5
39
  ### What's New in v5.3.1 🔧
6
40
 
7
41
  **`get_client_ip(request)` exported from `hazo_auth/server-lib`** — extracts the client IP from `x-forwarded-for` (first element), falling back to `x-real-ip`, then `"unknown"`. Previously private to `hazo_get_auth.server.ts`. Useful for consumers that need consistent IP extraction across handlers (e.g., `hazo_feedback` audit logging).
@@ -2,6 +2,37 @@
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
+ ## v8.0.0 Migration (from v7.x)
6
+
7
+ ### Breaking changes
8
+
9
+ Remove the deprecated page-component imports if you used them:
10
+
11
+ ```diff
12
+ - import LoginPage from 'hazo_auth/page_components/login';
13
+ - import RegisterPage from 'hazo_auth/page_components/register';
14
+ + // Use the Layout components directly in a server component instead
15
+ + import LoginLayout from 'hazo_auth/components/layouts/login';
16
+ + import RegisterLayout from 'hazo_auth/components/layouts/register';
17
+ ```
18
+
19
+ ### New migrations (run in order)
20
+
21
+ ```bash
22
+ npm run migrate migrations/017_legal_acceptance_column.sql
23
+ npm run migrate migrations/018_hazo_legal_acceptances.sql
24
+ npm run migrate migrations/019_hazo_legal_doc_versions.sql
25
+ ```
26
+
27
+ ### Legal docs setup (optional)
28
+
29
+ 1. Add `[hazo_auth__legal_docs]` to `config/hazo_auth_config.ini` (see commented sample at the bottom of that file)
30
+ 2. Create markdown files at the configured paths (e.g. `legal/tos.md`, `legal/privacy.md`)
31
+ 3. Wrap your app layout with `<LegalAcceptanceGate>` from `hazo_auth/client`
32
+ 4. Go to User Management → Legal Docs tab, click "Publish current version" per doc to activate gating for existing users
33
+
34
+ ---
35
+
5
36
  ## v5.3.0 Migration (from v5.2.x)
6
37
 
7
38
  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.
@@ -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
-
@@ -1,5 +1,6 @@
1
1
  // file_description: Type definitions and error classes for hazo_get_auth utility
2
2
  // section: types
3
+ import type { LegalAcceptanceMap } from '../legal/legal_docs_types';
3
4
 
4
5
  /**
5
6
  * User data structure returned by hazo_get_auth
@@ -13,6 +14,8 @@ export type HazoAuthUser = {
13
14
  managed_by_user_id?: string | null;
14
15
  // App-specific user data (JSON object stored as TEXT in database)
15
16
  app_user_data: Record<string, unknown> | null;
17
+ // Legal document acceptance map (JSON object stored in database)
18
+ legal_acceptance: LegalAcceptanceMap | null;
16
19
  };
17
20
 
18
21
  /**
@@ -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";
@@ -59,6 +60,24 @@ function parse_app_user_data(
59
60
  }
60
61
  }
61
62
 
63
+ /**
64
+ * Parse raw legal_acceptance field from DB to LegalAcceptanceMap
65
+ * @param raw - Raw value from database (string or object)
66
+ * @returns Parsed LegalAcceptanceMap or null
67
+ */
68
+ function parse_legal_acceptance(
69
+ raw: unknown,
70
+ ): import('../legal/legal_docs_types').LegalAcceptanceMap | null {
71
+ if (!raw) return null;
72
+ try {
73
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
74
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) return null;
75
+ return parsed as import('../legal/legal_docs_types').LegalAcceptanceMap;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
62
81
  /**
63
82
  * Gets client IP address from request
64
83
  * @param request - NextRequest object
@@ -165,14 +184,14 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
165
184
  // Fetch user
166
185
  const users = await users_service.findBy({ id: user_id });
167
186
  if (!Array.isArray(users) || users.length === 0) {
168
- throw new Error("User not found");
187
+ throw new HazoNotFoundError({ code: 'HAZO_AUTH_USER_NOT_FOUND', pkg: 'hazo_auth', message: 'User not found' });
169
188
  }
170
189
 
171
190
  const user_db = users[0];
172
191
 
173
192
  // Check if user is active (status must be 'ACTIVE')
174
193
  if (user_db.status !== "ACTIVE") {
175
- throw new Error("User is inactive");
194
+ throw new HazoAuthError({ code: 'HAZO_AUTH_FORBIDDEN', pkg: 'hazo_auth', message: 'User account is inactive' });
176
195
  }
177
196
 
178
197
  // Build user object
@@ -185,6 +204,7 @@ async function fetch_user_data_from_db(user_id: string): Promise<{
185
204
  (user_db.profile_picture_url as string | null) || null,
186
205
  managed_by_user_id: (user_db.managed_by_user_id as string | undefined) || null,
187
206
  app_user_data: parse_app_user_data(user_db.app_user_data),
207
+ legal_acceptance: parse_legal_acceptance(user_db.legal_acceptance),
188
208
  };
189
209
 
190
210
  // v5.x: Fetch user's roles from hazo_user_scopes (scope-based role assignments)
@@ -422,6 +442,7 @@ export async function hazo_get_auth(
422
442
  line_number: get_line_number(),
423
443
  error: token_error_message,
424
444
  note: "Falling back to simple cookie check",
445
+ correlation_id: getCorrelationId(),
425
446
  });
426
447
  }
427
448
  }
@@ -445,8 +466,9 @@ export async function hazo_get_auth(
445
466
  filename: get_filename(),
446
467
  line_number: get_line_number(),
447
468
  ip: client_ip,
469
+ correlation_id: getCorrelationId(),
448
470
  });
449
- 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.' });
450
472
  }
451
473
 
452
474
  return {
@@ -464,8 +486,9 @@ export async function hazo_get_auth(
464
486
  filename: get_filename(),
465
487
  line_number: get_line_number(),
466
488
  user_id,
489
+ correlation_id: getCorrelationId(),
467
490
  });
468
- 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.' });
469
492
  }
470
493
 
471
494
  // Check cache
@@ -497,6 +520,7 @@ export async function hazo_get_auth(
497
520
  line_number: get_line_number(),
498
521
  user_id,
499
522
  error: error_message,
523
+ correlation_id: getCorrelationId(),
500
524
  });
501
525
 
502
526
  return {
@@ -531,6 +555,7 @@ export async function hazo_get_auth(
531
555
  missing_permissions,
532
556
  user_permissions: permissions,
533
557
  ip: client_ip,
558
+ correlation_id: getCorrelationId(),
534
559
  });
535
560
  }
536
561
 
@@ -580,6 +605,7 @@ export async function hazo_get_auth(
580
605
  scope_id: options.scope_id,
581
606
  user_scopes: scope_result.user_scopes,
582
607
  ip: client_ip,
608
+ correlation_id: getCorrelationId(),
583
609
  });
584
610
  }
585
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
  /**
@@ -0,0 +1,61 @@
1
+ // file_description: server-only helper to read legal docs configuration from hazo_auth_config.ini
2
+ // section: server-only-guard
3
+ import 'server-only';
4
+
5
+ // section: imports
6
+ import { read_config_section } from '../config/config_loader.server.js';
7
+ import type { LegalDocConfig, LegalDocsConfig } from './legal_docs_types';
8
+
9
+ // section: constants
10
+ const SECTION_NAME = 'hazo_auth__legal_docs';
11
+
12
+ // section: cache
13
+ // Cached after first load — INI changes require server restart anyway
14
+ let _cached: LegalDocsConfig | null = null;
15
+
16
+ // section: exports
17
+
18
+ /**
19
+ * Reads legal docs configuration from hazo_auth_config.ini.
20
+ * Returns an empty docs array if the section is absent (legal docs disabled).
21
+ *
22
+ * Expected INI shape:
23
+ * [hazo_auth__legal_docs]
24
+ * display_mode = separate ; or: combined
25
+ * doc_1_key = terms
26
+ * doc_1_title = Terms of Service
27
+ * doc_1_path = legal/terms.md
28
+ * doc_2_key = privacy
29
+ * doc_2_title = Privacy Policy
30
+ * doc_2_path = legal/privacy.md
31
+ */
32
+ export function get_legal_docs_config(): LegalDocsConfig {
33
+ if (_cached) return _cached;
34
+
35
+ const section = read_config_section(SECTION_NAME) ?? {};
36
+
37
+ const docs: LegalDocConfig[] = [];
38
+ let i = 1;
39
+ while (section[`doc_${i}_key`]) {
40
+ docs.push({
41
+ key: section[`doc_${i}_key`],
42
+ title: section[`doc_${i}_title`] ?? section[`doc_${i}_key`],
43
+ path: section[`doc_${i}_path`],
44
+ });
45
+ i++;
46
+ }
47
+
48
+ _cached = {
49
+ docs,
50
+ display_mode: section['display_mode'] === 'combined' ? 'combined' : 'separate',
51
+ };
52
+
53
+ return _cached;
54
+ }
55
+
56
+ /**
57
+ * Call this in tests to clear the cache between runs.
58
+ */
59
+ export function _reset_legal_docs_config_cache(): void {
60
+ _cached = null;
61
+ }
@@ -0,0 +1,36 @@
1
+ // file_description: server-only utility that reads a legal document from disk and returns its content + SHA-256 hash
2
+ // section: server-only-guard
3
+ import 'server-only';
4
+
5
+ // section: imports
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { createHash } from 'crypto';
9
+
10
+ // section: types
11
+
12
+ export interface ReadDocResult {
13
+ content: string;
14
+ hash: string; // "sha256:<hex>"
15
+ }
16
+
17
+ // section: exports
18
+
19
+ /**
20
+ * Reads a legal document from the filesystem and returns its text content
21
+ * together with a deterministic SHA-256 hash of that content.
22
+ *
23
+ * @param doc_path - Absolute path, or a path relative to process.cwd().
24
+ * @returns { content, hash } where hash is formatted as "sha256:<hex>".
25
+ * @throws If the file cannot be read.
26
+ */
27
+ export function read_legal_doc(doc_path: string): ReadDocResult {
28
+ const abs_path = path.isAbsolute(doc_path)
29
+ ? doc_path
30
+ : path.join(process.cwd(), doc_path);
31
+
32
+ const content = fs.readFileSync(abs_path, 'utf-8');
33
+ const hex = createHash('sha256').update(content).digest('hex');
34
+
35
+ return { content, hash: `sha256:${hex}` };
36
+ }