hazo_auth 6.1.1 → 7.0.2

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 (210) hide show
  1. package/README.md +65 -167
  2. package/SETUP_CHECKLIST.md +28 -100
  3. package/cli-src/cli/generate.ts +1 -10
  4. package/cli-src/cli/validate.ts +0 -4
  5. package/cli-src/lib/auth/auth_types.ts +12 -21
  6. package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +24 -25
  7. package/cli-src/lib/auth/index.ts +2 -2
  8. package/cli-src/lib/auth/nextauth_config.ts +61 -1
  9. package/cli-src/lib/auth/with_auth.server.ts +15 -15
  10. package/cli-src/lib/config/default_config.ts +8 -0
  11. package/cli-src/lib/cookies_config.server.ts +1 -1
  12. package/cli-src/lib/login_config.server.ts +2 -18
  13. package/cli-src/lib/oauth_config.server.ts +32 -0
  14. package/cli-src/lib/register_config.server.ts +4 -0
  15. package/cli-src/lib/services/email_template_manifest.ts +0 -17
  16. package/cli-src/lib/services/index.ts +2 -8
  17. package/cli-src/lib/services/oauth_service.ts +143 -0
  18. package/cli-src/lib/services/otp_service.ts +7 -2
  19. package/cli-src/lib/services/session_token_service.ts +0 -2
  20. package/config/hazo_auth_config.example.ini +0 -38
  21. package/dist/cli/generate.d.ts.map +1 -1
  22. package/dist/cli/generate.js +1 -10
  23. package/dist/cli/validate.d.ts.map +1 -1
  24. package/dist/cli/validate.js +0 -4
  25. package/dist/client.d.ts +0 -2
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +0 -1
  28. package/dist/components/layouts/login/index.d.ts +5 -7
  29. package/dist/components/layouts/login/index.d.ts.map +1 -1
  30. package/dist/components/layouts/login/index.js +5 -2
  31. package/dist/components/layouts/otp/index.d.ts +12 -1
  32. package/dist/components/layouts/otp/index.d.ts.map +1 -1
  33. package/dist/components/layouts/otp/index.js +4 -2
  34. package/dist/components/layouts/register/index.d.ts +4 -0
  35. package/dist/components/layouts/register/index.d.ts.map +1 -1
  36. package/dist/components/layouts/register/index.js +4 -1
  37. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts +21 -0
  38. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts.map +1 -0
  39. package/dist/components/layouts/shared/components/facebook_sign_in_button.js +47 -0
  40. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  41. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +3 -8
  42. package/dist/components/layouts/shared/index.d.ts +2 -0
  43. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  44. package/dist/components/layouts/shared/index.js +1 -0
  45. package/dist/components/layouts/user_management/index.d.ts.map +1 -1
  46. package/dist/components/layouts/user_management/index.js +39 -2
  47. package/dist/consent/consent_state.d.ts +18 -0
  48. package/dist/consent/consent_state.d.ts.map +1 -0
  49. package/dist/consent/consent_state.js +29 -0
  50. package/dist/consent/cookie_consent_banner.d.ts +11 -0
  51. package/dist/consent/cookie_consent_banner.d.ts.map +1 -0
  52. package/dist/consent/cookie_consent_banner.js +40 -0
  53. package/dist/consent/gtm_mapping.d.ts +13 -0
  54. package/dist/consent/gtm_mapping.d.ts.map +1 -0
  55. package/dist/consent/gtm_mapping.js +30 -0
  56. package/dist/consent/index.d.ts +7 -0
  57. package/dist/consent/index.d.ts.map +1 -0
  58. package/dist/consent/index.js +7 -0
  59. package/dist/consent/manage_modal.d.ts +2 -0
  60. package/dist/consent/manage_modal.d.ts.map +1 -0
  61. package/dist/consent/manage_modal.js +33 -0
  62. package/dist/consent/read_consent.d.ts +15 -0
  63. package/dist/consent/read_consent.d.ts.map +1 -0
  64. package/dist/consent/read_consent.js +23 -0
  65. package/dist/consent/use_consent.d.ts +7 -0
  66. package/dist/consent/use_consent.d.ts.map +1 -0
  67. package/dist/consent/use_consent.js +55 -0
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/lib/auth/auth_types.d.ts +12 -13
  71. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  72. package/dist/lib/auth/auth_types.js +0 -8
  73. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts +7 -8
  74. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
  75. package/dist/lib/auth/hazo_get_tenant_auth.server.js +22 -23
  76. package/dist/lib/auth/index.d.ts +2 -2
  77. package/dist/lib/auth/index.d.ts.map +1 -1
  78. package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
  79. package/dist/lib/auth/nextauth_config.js +50 -1
  80. package/dist/lib/auth/with_auth.server.d.ts +13 -13
  81. package/dist/lib/auth/with_auth.server.d.ts.map +1 -1
  82. package/dist/lib/auth/with_auth.server.js +2 -2
  83. package/dist/lib/config/default_config.d.ts +16 -0
  84. package/dist/lib/config/default_config.d.ts.map +1 -1
  85. package/dist/lib/config/default_config.js +8 -0
  86. package/dist/lib/cookies_config.server.d.ts +1 -1
  87. package/dist/lib/cookies_config.server.js +1 -1
  88. package/dist/lib/login_config.server.d.ts +0 -6
  89. package/dist/lib/login_config.server.d.ts.map +1 -1
  90. package/dist/lib/login_config.server.js +2 -11
  91. package/dist/lib/oauth_config.server.d.ts +8 -0
  92. package/dist/lib/oauth_config.server.d.ts.map +1 -1
  93. package/dist/lib/oauth_config.server.js +10 -0
  94. package/dist/lib/register_config.server.d.ts +2 -0
  95. package/dist/lib/register_config.server.d.ts.map +1 -1
  96. package/dist/lib/register_config.server.js +2 -0
  97. package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
  98. package/dist/lib/services/email_template_manifest.js +0 -17
  99. package/dist/lib/services/index.d.ts +0 -2
  100. package/dist/lib/services/index.d.ts.map +1 -1
  101. package/dist/lib/services/index.js +0 -1
  102. package/dist/lib/services/oauth_service.d.ts +13 -0
  103. package/dist/lib/services/oauth_service.d.ts.map +1 -1
  104. package/dist/lib/services/oauth_service.js +122 -0
  105. package/dist/lib/services/otp_service.d.ts +1 -1
  106. package/dist/lib/services/otp_service.d.ts.map +1 -1
  107. package/dist/lib/services/otp_service.js +6 -1
  108. package/dist/lib/services/session_token_service.d.ts +0 -2
  109. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  110. package/dist/lib/services/session_token_service.js +0 -2
  111. package/dist/server/routes/assets.d.ts +8 -0
  112. package/dist/server/routes/assets.d.ts.map +1 -0
  113. package/dist/server/routes/assets.js +38 -0
  114. package/dist/server/routes/consent_me.d.ts +4 -0
  115. package/dist/server/routes/consent_me.d.ts.map +1 -0
  116. package/dist/server/routes/consent_me.js +15 -0
  117. package/dist/server/routes/index.d.ts +6 -3
  118. package/dist/server/routes/index.d.ts.map +1 -1
  119. package/dist/server/routes/index.js +9 -4
  120. package/dist/server/routes/me.d.ts.map +1 -1
  121. package/dist/server/routes/me.js +1 -43
  122. package/dist/server/routes/oauth_facebook_callback.d.ts +8 -0
  123. package/dist/server/routes/oauth_facebook_callback.d.ts.map +1 -0
  124. package/dist/server/routes/oauth_facebook_callback.js +164 -0
  125. package/dist/server/routes/otp/verify.js +2 -2
  126. package/dist/server/routes/strings_defaults.d.ts +4 -0
  127. package/dist/server/routes/strings_defaults.d.ts.map +1 -0
  128. package/dist/server/routes/strings_defaults.js +7 -0
  129. package/dist/server/routes/user_management_users.d.ts +11 -0
  130. package/dist/server/routes/user_management_users.d.ts.map +1 -1
  131. package/dist/server/routes/user_management_users.js +50 -0
  132. package/dist/server-lib.d.ts +0 -3
  133. package/dist/server-lib.d.ts.map +1 -1
  134. package/dist/server-lib.js +0 -2
  135. package/dist/server_pages/forgot_password.d.ts +1 -1
  136. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  137. package/dist/server_pages/forgot_password.js +9 -3
  138. package/dist/server_pages/forgot_password_client_wrapper.d.ts +3 -1
  139. package/dist/server_pages/forgot_password_client_wrapper.d.ts.map +1 -1
  140. package/dist/server_pages/forgot_password_client_wrapper.js +2 -2
  141. package/dist/server_pages/index.d.ts +2 -0
  142. package/dist/server_pages/index.d.ts.map +1 -1
  143. package/dist/server_pages/index.js +1 -0
  144. package/dist/server_pages/login.d.ts +1 -1
  145. package/dist/server_pages/login.d.ts.map +1 -1
  146. package/dist/server_pages/login.js +12 -3
  147. package/dist/server_pages/login_client_wrapper.d.ts +4 -1
  148. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  149. package/dist/server_pages/login_client_wrapper.js +2 -2
  150. package/dist/server_pages/my_settings.d.ts +1 -1
  151. package/dist/server_pages/my_settings.d.ts.map +1 -1
  152. package/dist/server_pages/my_settings.js +1 -2
  153. package/dist/server_pages/otp.d.ts +16 -2
  154. package/dist/server_pages/otp.d.ts.map +1 -1
  155. package/dist/server_pages/otp.js +10 -3
  156. package/dist/server_pages/register.d.ts +1 -1
  157. package/dist/server_pages/register.d.ts.map +1 -1
  158. package/dist/server_pages/register.js +11 -3
  159. package/dist/server_pages/register_client_wrapper.d.ts +3 -1
  160. package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
  161. package/dist/server_pages/register_client_wrapper.js +2 -2
  162. package/dist/server_pages/reset_password.d.ts +1 -1
  163. package/dist/server_pages/reset_password.d.ts.map +1 -1
  164. package/dist/server_pages/reset_password.js +9 -3
  165. package/dist/server_pages/reset_password_client_wrapper.d.ts +3 -1
  166. package/dist/server_pages/reset_password_client_wrapper.d.ts.map +1 -1
  167. package/dist/server_pages/reset_password_client_wrapper.js +2 -2
  168. package/dist/server_pages/verify_email.d.ts +1 -1
  169. package/dist/server_pages/verify_email.d.ts.map +1 -1
  170. package/dist/server_pages/verify_email.js +8 -3
  171. package/dist/server_pages/verify_email_client_wrapper.d.ts +3 -1
  172. package/dist/server_pages/verify_email_client_wrapper.d.ts.map +1 -1
  173. package/dist/server_pages/verify_email_client_wrapper.js +2 -2
  174. package/dist/strings/default_strings.d.ts +47 -0
  175. package/dist/strings/default_strings.d.ts.map +1 -0
  176. package/dist/strings/default_strings.js +18 -0
  177. package/dist/strings/index.d.ts +4 -0
  178. package/dist/strings/index.d.ts.map +1 -0
  179. package/dist/strings/index.js +3 -0
  180. package/dist/strings/strings_context.d.ts +12 -0
  181. package/dist/strings/strings_context.d.ts.map +1 -0
  182. package/dist/strings/strings_context.js +23 -0
  183. package/dist/strings/strings_provider.d.ts +26 -0
  184. package/dist/strings/strings_provider.d.ts.map +1 -0
  185. package/dist/strings/strings_provider.js +45 -0
  186. package/dist/theme/create_theme.d.ts +7 -0
  187. package/dist/theme/create_theme.d.ts.map +1 -0
  188. package/dist/theme/create_theme.js +97 -0
  189. package/dist/theme/hex_to_hsl.d.ts +16 -0
  190. package/dist/theme/hex_to_hsl.d.ts.map +1 -0
  191. package/dist/theme/hex_to_hsl.js +110 -0
  192. package/dist/theme/index.d.ts +4 -0
  193. package/dist/theme/index.d.ts.map +1 -0
  194. package/dist/theme/index.js +3 -0
  195. package/dist/theme/luminance.d.ts +11 -0
  196. package/dist/theme/luminance.d.ts.map +1 -0
  197. package/dist/theme/luminance.js +45 -0
  198. package/dist/theme/theme_provider.d.ts +14 -0
  199. package/dist/theme/theme_provider.d.ts.map +1 -0
  200. package/dist/theme/theme_provider.js +23 -0
  201. package/dist/theme/theme_types.d.ts +36 -0
  202. package/dist/theme/theme_types.d.ts.map +1 -0
  203. package/dist/theme/theme_types.js +1 -0
  204. package/dist/themes/index.d.ts +2 -0
  205. package/dist/themes/index.d.ts.map +1 -0
  206. package/dist/themes/index.js +2 -0
  207. package/dist/themes/preset_neutral.d.ts +3 -0
  208. package/dist/themes/preset_neutral.d.ts.map +1 -0
  209. package/dist/themes/preset_neutral.js +14 -0
  210. package/package.json +25 -22
package/README.md CHANGED
@@ -2,154 +2,6 @@
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 v6.1.0
6
-
7
- **Email-OTP sign-in** — a passwordless, OAuth-free way to sign in via a 6-digit code emailed to the user. Targeted at single-operator deployments that don't want to wire Google OAuth or manage passwords.
8
-
9
- ### Highlights
10
-
11
- - **New routes** — `POST /api/hazo_auth/otp/request` and `POST /api/hazo_auth/otp/verify`
12
- - **New components** — `<OTPRequestForm/>` and `<OTPVerifyForm/>` exported from `hazo_auth/client`
13
- - **New page** — `/hazo_auth/otp` (zero-config) via `hazo_auth/pages/otp`
14
- - **Sliding 7-day session** — OTP sessions auto-extend on `/me` when within 24h of expiry. OAuth/password sessions keep their existing 30-day fixed behaviour.
15
- - **Auto-register (opt-in)** — set `otp_auto_register = true` to let unknown emails sign in on first successful /verify
16
- - **Rate limited** — 3 requests/email/15min, 20 requests/IP/hour; per-code attempts capped at 5
17
-
18
- ### Consumer example
19
-
20
- Mount the routes:
21
-
22
- ```ts
23
- // app/api/hazo_auth/otp/request/route.ts
24
- export { otpRequestPOST as POST } from "hazo_auth/server/routes";
25
-
26
- // app/api/hazo_auth/otp/verify/route.ts
27
- export { otpVerifyPOST as POST } from "hazo_auth/server/routes";
28
- ```
29
-
30
- Render the zero-config page:
31
-
32
- ```tsx
33
- // app/hazo_auth/otp/page.tsx
34
- export { default } from "hazo_auth/pages/otp";
35
- ```
36
-
37
- Or compose components yourself:
38
-
39
- ```tsx
40
- "use client";
41
- import { OTPRequestForm, OTPVerifyForm } from "hazo_auth/client";
42
- import { useState } from "react";
43
-
44
- export default function CustomOtp() {
45
- const [sent_to, set_sent_to] = useState<string | null>(null);
46
- return sent_to
47
- ? <OTPVerifyForm email={sent_to} redirect_url="/" />
48
- : <OTPRequestForm on_sent={set_sent_to} />;
49
- }
50
- ```
51
-
52
- ### Required setup
53
-
54
- 1. Apply migration: `npm run migrate migrations/015_email_otp.sql`
55
- 2. Add `[hazo_auth__otp]` section to `config/hazo_auth_config.ini` (template in `hazo_auth_config.example.ini`)
56
- 3. `hazo_notify ^3.0.0` (existing peer dep) must be configured to deliver email
57
- 4. New peer dep: `input-otp` (optional; required only if you render `<OTPVerifyForm/>`)
58
- 5. **Add OTP routes to your middleware/proxy `public_routes`** — unauthenticated users land on the OTP page, so the page route and its API routes must bypass auth guards:
59
-
60
- ```typescript
61
- // middleware.ts / proxy.ts (in your consuming app)
62
- const public_routes = [
63
- // ... existing entries ...
64
- "/hazo_auth/otp", // OTP sign-in page (public — users arrive unauthenticated)
65
- "/api/hazo_auth/otp", // OTP request + verify API routes
66
- ];
67
- ```
68
-
69
- Without this your middleware redirects unauthenticated users away from the sign-in page before they can authenticate.
70
-
71
- See `MIGRATION.md` "v6.0.x → v6.1" for the full upgrade walkthrough.
72
-
73
- ---
74
-
75
- ### What's New in v6.0.0 🚨 BREAKING CHANGE
76
-
77
- **`TenantAuthResult.organization` / `.organization_id` renamed to `.selected_scope` / `.selected_scope_id`.**
78
-
79
- The multi-tenancy model has been scope-based for several releases — `auth.user_scopes`, `auth.scope_access_via`, `auth.scope_ok`, the `hazo_auth_scope_id` cookie, the `X-Hazo-Scope-Id` header. The only holdouts using the legacy "organization" name were two fields on `TenantAuthResult` that were always just *"the currently selected scope"* under the hood. This release eliminates the inconsistency.
80
-
81
- > 📖 **Full migration guide:** [MIGRATION.md → v5.x → v6.0](./MIGRATION.md#v5x--v60-organization--selected_scope)
82
-
83
- **No behavioral change.** `selected_scope_id` is derived from the same scope-selection cookie/header that `organization_id` was. Wire-format auth responses change field names, but the underlying scope lookup is identical.
84
-
85
- **No deprecation shim.** A clean find-replace is the upgrade path. If you absolutely need a transitional period, pin to `hazo_auth@^5.3` until you can do the rename in one shot.
86
-
87
- **What to replace (find ↔ replace, all in your app code):**
88
-
89
- | Find | Replace with |
90
- | ------------------------------------------------- | -------------------------------------------------- |
91
- | `auth.organization` | `auth.selected_scope` |
92
- | `auth.organization_id` | `auth.selected_scope_id` |
93
- | `import type { TenantOrganization }` | `import type { SelectedScope }` |
94
- | `: TenantOrganization` | `: SelectedScope` |
95
- | `AuthenticatedTenantAuthWithOrg` | `AuthenticatedTenantAuthWithSelectedScope` |
96
-
97
- **Before / after example:**
98
-
99
- ```typescript
100
- // BEFORE (v5.x)
101
- import { hazo_get_tenant_auth } from "hazo_auth/server-lib";
102
- import type { TenantOrganization } from "hazo_auth";
103
-
104
- export async function GET(request: NextRequest) {
105
- const auth = await hazo_get_tenant_auth(request);
106
- if (!auth.authenticated || !auth.organization) {
107
- return NextResponse.json({ error: "no tenant" }, { status: 403 });
108
- }
109
- const data = await getData(auth.organization_id); // or auth.organization.id
110
- return NextResponse.json({ org: auth.organization, data });
111
- }
112
-
113
- // AFTER (v6.0)
114
- import { hazo_get_tenant_auth } from "hazo_auth/server-lib";
115
- import type { SelectedScope } from "hazo_auth";
116
-
117
- export async function GET(request: NextRequest) {
118
- const auth = await hazo_get_tenant_auth(request);
119
- if (!auth.authenticated || !auth.selected_scope) {
120
- return NextResponse.json({ error: "no tenant scope" }, { status: 403 });
121
- }
122
- const data = await getData(auth.selected_scope_id); // or auth.selected_scope.id
123
- return NextResponse.json({ selected_scope: auth.selected_scope, data });
124
- }
125
- ```
126
-
127
- **What did NOT change** (same names, same behavior, same wire format):
128
- - `auth.user_scopes` — array of all scopes the user has access to
129
- - `auth.scope_ok`, `auth.scope_access_via` — scope-access fields
130
- - `hazo_auth_scope_id` cookie name and `X-Hazo-Scope-Id` request header
131
- - All `Tenant*` type and class names: `TenantAuthResult`, `TenantAuthOptions`, `RequiredTenantAuthResult`, `TenantRequiredError`, `TenantAccessDeniedError`, `hazo_get_tenant_auth`, `require_tenant_auth`, `withAuth`'s `require_tenant` option
132
- - The error response `{ code: "TENANT_REQUIRED" }` shape — only the human-readable `error` string was rephrased ("Organization context required" → "Tenant scope context required")
133
-
134
- **One-liner upgrade for typical consumers:**
135
-
136
- ```bash
137
- # Run from your app repo (NOT inside hazo_auth itself).
138
- # Update the path glob to match your code layout.
139
- git grep -l "organization\b\|TenantOrganization\|AuthenticatedTenantAuthWithOrg" -- 'src/**' 'app/**' 'lib/**' \
140
- | xargs sed -i.bak '
141
- s/auth\.organization_id\b/auth.selected_scope_id/g;
142
- s/auth\.organization\b/auth.selected_scope/g;
143
- s/\bTenantOrganization\b/SelectedScope/g;
144
- s/\bAuthenticatedTenantAuthWithOrg\b/AuthenticatedTenantAuthWithSelectedScope/g;
145
- '
146
- # Then: review the diff carefully — sed is blunt. Remove .bak files when satisfied.
147
- ```
148
-
149
- > ⚠️ The sed line is a starting point, not a blanket "trust me." It only catches `auth.organization*` and the type names. If you destructure (`const { organization } = auth`), aliased imports (`TenantOrganization as Org`), or reference `organization` for unrelated reasons (HTML autocomplete attribute, your own variables), the script either misses or wrongly rewrites them. **Always diff before committing.**
150
-
151
- ---
152
-
153
5
  ### What's New in v5.3.1 🔧
154
6
 
155
7
  **`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).
@@ -1792,7 +1644,7 @@ export async function proxy(request: NextRequest) {
1792
1644
 
1793
1645
  #### `hazo_get_tenant_auth` (Recommended for Multi-Tenant Apps)
1794
1646
 
1795
- **New:** Tenant-aware authentication function that extracts scope context from request headers or cookies and returns enriched result including the currently selected scope.
1647
+ **New:** Tenant-aware authentication function that extracts scope context from request headers or cookies and returns enriched result with organization information.
1796
1648
 
1797
1649
  **Location:** `src/lib/auth/hazo_get_tenant_auth.server.ts`
1798
1650
 
@@ -1826,9 +1678,8 @@ type TenantAuthResult =
1826
1678
  permissions: string[];
1827
1679
  permission_ok: boolean;
1828
1680
  missing_permissions?: string[];
1829
- selected_scope: SelectedScope | null; // Currently selected tenant/scope
1830
- selected_scope_id: string | null; // Shorthand for selected_scope?.id
1831
- user_scopes: ScopeDetails[]; // All user's scopes for switching
1681
+ organization: TenantOrganization | null; // NEW: Tenant context
1682
+ user_scopes: ScopeDetails[]; // NEW: All user's scopes for switching
1832
1683
  scope_ok: boolean;
1833
1684
  }
1834
1685
  | {
@@ -1836,13 +1687,12 @@ type TenantAuthResult =
1836
1687
  user: null;
1837
1688
  permissions: [];
1838
1689
  permission_ok: false;
1839
- selected_scope: null;
1840
- selected_scope_id: null;
1690
+ organization: null;
1841
1691
  user_scopes: [];
1842
1692
  scope_ok: false;
1843
1693
  };
1844
1694
 
1845
- type SelectedScope = {
1695
+ type TenantOrganization = {
1846
1696
  id: string;
1847
1697
  name: string;
1848
1698
  slug: string | null; // URL-friendly identifier
@@ -1884,10 +1734,10 @@ export async function GET(request: NextRequest) {
1884
1734
  return NextResponse.json({ error: "Authentication required" }, { status: 401 });
1885
1735
  }
1886
1736
 
1887
- if (!auth.selected_scope) {
1737
+ if (!auth.organization) {
1888
1738
  return NextResponse.json(
1889
1739
  {
1890
- error: "No tenant scope context",
1740
+ error: "No organization context",
1891
1741
  available_scopes: auth.user_scopes.map(s => ({ id: s.id, name: s.name }))
1892
1742
  },
1893
1743
  { status: 403 }
@@ -1895,10 +1745,10 @@ export async function GET(request: NextRequest) {
1895
1745
  }
1896
1746
 
1897
1747
  // Access tenant-specific data
1898
- const data = await getTenantData(auth.selected_scope.id);
1748
+ const data = await getTenantData(auth.organization.id);
1899
1749
 
1900
1750
  return NextResponse.json({
1901
- selected_scope: auth.selected_scope,
1751
+ organization: auth.organization,
1902
1752
  data,
1903
1753
  // Include available scopes for UI scope switcher
1904
1754
  available_scopes: auth.user_scopes,
@@ -1916,8 +1766,8 @@ export async function GET(request: NextRequest) {
1916
1766
  required_permissions: ["view_reports"],
1917
1767
  });
1918
1768
 
1919
- // auth.selected_scope is guaranteed non-null here
1920
- const reports = await getReports(auth.selected_scope.id);
1769
+ // auth.organization is guaranteed non-null here
1770
+ const reports = await getReports(auth.organization.id);
1921
1771
  return NextResponse.json({ reports });
1922
1772
  } catch (error) {
1923
1773
  if (error instanceof HazoAuthError) {
@@ -1967,16 +1817,16 @@ Helper function that wraps `hazo_get_tenant_auth` and throws typed errors for co
1967
1817
  - `TenantRequiredError` (403) - No tenant context in request
1968
1818
  - `TenantAccessDeniedError` (403) - User lacks access to requested tenant
1969
1819
 
1970
- **Returns:** `RequiredTenantAuthResult` with guaranteed non-null `selected_scope`
1820
+ **Returns:** `RequiredTenantAuthResult` with guaranteed non-null `organization`
1971
1821
 
1972
1822
  **Example:**
1973
1823
  ```typescript
1974
1824
  export async function GET(request: NextRequest) {
1975
1825
  try {
1976
- // selected_scope is guaranteed to exist
1977
- const { selected_scope, user, permissions } = await require_tenant_auth(request);
1826
+ // organization is guaranteed to exist
1827
+ const { organization, user, permissions } = await require_tenant_auth(request);
1978
1828
 
1979
- const data = await getData(selected_scope.id);
1829
+ const data = await getData(organization.id);
1980
1830
  return NextResponse.json(data);
1981
1831
  } catch (error) {
1982
1832
  if (error instanceof HazoAuthError) {
@@ -2029,10 +1879,10 @@ export const DELETE = withAuth<{ id: string }>(
2029
1879
  { required_permissions: ["admin_system"] }
2030
1880
  );
2031
1881
 
2032
- // With tenant requirement (auth.selected_scope guaranteed non-null)
1882
+ // With tenant requirement (auth.organization guaranteed non-null)
2033
1883
  export const GET = withAuth<{ id: string }>(
2034
1884
  async (request, auth, { id }) => {
2035
- const data = await getData(auth.selected_scope.id, id);
1885
+ const data = await getData(auth.organization.id, id);
2036
1886
  return NextResponse.json(data);
2037
1887
  },
2038
1888
  { require_tenant: true }
@@ -3128,3 +2978,51 @@ The `package.json` exports field defines the public API:
3128
2978
  - Use `npx shadcn@latest add <component>` to scaffold new UI primitives.
3129
2979
  - Centralize configurable values through `hazo_config`.
3130
2980
  - Access backend resources exclusively via `hazo_connect`.
2981
+
2982
+ ## Email OTP sign-in
2983
+
2984
+ ```tsx
2985
+ // app/(auth)/otp/page.tsx
2986
+ import { OTPPage } from "hazo_auth/pages/otp";
2987
+ export default function Page(props) { return <OTPPage {...props} />; }
2988
+ ```
2989
+
2990
+ Enable in config: `[hazo_auth__otp] enable_email_otp = true`. Run `npx hazo_auth migrate migrations/015_email_otp.sql`.
2991
+
2992
+ ## Theming
2993
+
2994
+ ```tsx
2995
+ import { HazoAuthThemeProvider, createTheme } from "hazo_auth/theme";
2996
+
2997
+ const my_theme = createTheme({
2998
+ colors: { primary: "#1D4ED8" },
2999
+ layout: "split",
3000
+ brandPanel: { logoSrc: "/logo.svg", tagline: "Welcome." },
3001
+ });
3002
+
3003
+ <HazoAuthThemeProvider theme={my_theme}>
3004
+ <LoginPage />
3005
+ </HazoAuthThemeProvider>
3006
+ ```
3007
+
3008
+ Theme is auth-page scoped. Does not affect `:root`.
3009
+
3010
+ ## Cookie Consent
3011
+
3012
+ ```tsx
3013
+ import { CookieConsentBanner } from "hazo_auth/consent";
3014
+ // Place in your root layout:
3015
+ <CookieConsentBanner />
3016
+ ```
3017
+
3018
+ Server-side consent check: `GET /api/hazo_auth/consent/me` returns `ConsentState`.
3019
+
3020
+ ## String overrides
3021
+
3022
+ ```tsx
3023
+ import { HazoAuthStringsProvider } from "hazo_auth/strings";
3024
+
3025
+ <HazoAuthStringsProvider strings={{ login: { title: "Welcome back" } }}>
3026
+ <LoginPage />
3027
+ </HazoAuthStringsProvider>
3028
+ ```
@@ -958,7 +958,27 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_users_google_id_unique ON hazo_users(
958
958
  CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
959
959
  ```
960
960
 
961
- ### Step 3.2.4: Configure OAuth in hazo_auth_config.ini
961
+ ### Step 3.2.4 (Optional): Run Email OTP Migration
962
+
963
+ Required only if you plan to enable `enable_email_otp = true` in `[hazo_auth__otp]`.
964
+
965
+ ```bash
966
+ npx hazo_auth migrate migrations/015_email_otp.sql
967
+ ```
968
+
969
+ Creates `hazo_email_otps` table for OTP codes and rate limiting.
970
+
971
+ ### Step 3.2.4b (Optional): Run Facebook OAuth Migration
972
+
973
+ Required only if `enable_facebook_oauth = true` in `[hazo_auth__oauth]`.
974
+
975
+ ```bash
976
+ npx hazo_auth migrate migrations/016_add_facebook_id_to_hazo_users.sql
977
+ ```
978
+
979
+ Adds `facebook_id` column to `hazo_users`.
980
+
981
+ ### Step 3.2.5: Configure OAuth in hazo_auth_config.ini
962
982
 
963
983
  Add (or modify) the OAuth section:
964
984
 
@@ -1443,98 +1463,6 @@ export { POST } from "hazo_auth/server/routes/pin_login";
1443
1463
 
1444
1464
  ---
1445
1465
 
1446
- ## Phase 5.3: Email-OTP Sign-in Setup (Optional)
1447
-
1448
- Email-OTP sign-in lets users authenticate with a 6-digit code sent to their email — no password or Google OAuth required. Skip this phase if your app uses only password or OAuth authentication.
1449
-
1450
- ### Step 5.3.1: Apply the OTP migration
1451
-
1452
- ```bash
1453
- npm run migrate migrations/015_email_otp.sql
1454
- ```
1455
-
1456
- This creates the `hazo_email_otps` table (stores hashed codes with expiry).
1457
-
1458
- **Verify the table exists:**
1459
- ```bash
1460
- sqlite3 data/hazo_auth.sqlite ".tables" | tr ' ' '\n' | grep hazo_email_otps
1461
- # Expected: hazo_email_otps
1462
- ```
1463
-
1464
- ### Step 5.3.2: Add OTP config section
1465
-
1466
- Add to `config/hazo_auth_config.ini`:
1467
-
1468
- ```ini
1469
- [hazo_auth__otp]
1470
- # Whether OTP sign-in is enabled
1471
- enabled = true
1472
-
1473
- # Auto-register unknown emails on first successful verify (default: false)
1474
- otp_auto_register = false
1475
-
1476
- # Code expiry in minutes (default: 10)
1477
- code_expiry_minutes = 10
1478
- ```
1479
-
1480
- ### Step 5.3.3: Create OTP API routes
1481
-
1482
- ```bash
1483
- npx hazo_auth generate-routes
1484
- ```
1485
-
1486
- Or manually:
1487
-
1488
- `app/api/hazo_auth/otp/request/route.ts`:
1489
- ```typescript
1490
- export { otpRequestPOST as POST } from "hazo_auth/server/routes";
1491
- ```
1492
-
1493
- `app/api/hazo_auth/otp/verify/route.ts`:
1494
- ```typescript
1495
- export { otpVerifyPOST as POST } from "hazo_auth/server/routes";
1496
- ```
1497
-
1498
- ### Step 5.3.4: Create the OTP sign-in page
1499
-
1500
- `app/hazo_auth/otp/page.tsx`:
1501
- ```typescript
1502
- export { default } from "hazo_auth/pages/otp";
1503
- ```
1504
-
1505
- ### Step 5.3.5: Add OTP routes to middleware/proxy public_routes (REQUIRED)
1506
-
1507
- **This step is critical.** Unauthenticated users arrive at the OTP sign-in page before they have auth cookies. If the OTP routes are not in `public_routes`, your middleware will redirect them to `/login` in a loop.
1508
-
1509
- Edit your `middleware.ts` or `proxy.ts`:
1510
-
1511
- ```typescript
1512
- const public_routes = [
1513
- // ... existing entries ...
1514
- "/hazo_auth/otp", // OTP sign-in page (public — users arrive unauthenticated)
1515
- "/api/hazo_auth/otp", // OTP request + verify API routes
1516
- ];
1517
- ```
1518
-
1519
- ### Step 5.3.6: Install optional peer dep (if using OTPVerifyForm)
1520
-
1521
- If you render `<OTPVerifyForm/>` directly (rather than using the zero-config page), install:
1522
-
1523
- ```bash
1524
- npm install input-otp
1525
- ```
1526
-
1527
- **OTP Setup Checklist:**
1528
- - [ ] `hazo_email_otps` table created (`npm run migrate migrations/015_email_otp.sql`)
1529
- - [ ] `[hazo_auth__otp]` section added to `hazo_auth_config.ini`
1530
- - [ ] OTP API routes created (`/api/hazo_auth/otp/request` and `/api/hazo_auth/otp/verify`)
1531
- - [ ] OTP page created (`/hazo_auth/otp`)
1532
- - [ ] `/hazo_auth/otp` and `/api/hazo_auth/otp` added to middleware `public_routes`
1533
- - [ ] `input-otp` installed (only if using `<OTPVerifyForm/>` directly)
1534
- - [ ] Email delivery configured (`hazo_notify` + valid `ZEPTOMAIL_API_KEY`)
1535
-
1536
- ---
1537
-
1538
1466
  ## Phase 6: Verification Tests
1539
1467
 
1540
1468
  Run these tests to verify your setup is working correctly.
@@ -2016,10 +1944,10 @@ import { hazo_get_tenant_auth } from "hazo_auth/server-lib";
2016
1944
  export async function GET(request: NextRequest) {
2017
1945
  const auth = await hazo_get_tenant_auth(request);
2018
1946
 
2019
- if (auth.authenticated && auth.selected_scope) {
2020
- // auth.selected_scope contains the currently selected tenant/scope
1947
+ if (auth.authenticated && auth.organization) {
1948
+ // auth.organization contains tenant details
2021
1949
  // auth.user_scopes contains all scopes user can access (for UI switcher)
2022
- const data = await getTenantData(auth.selected_scope.id);
1950
+ const data = await getTenantData(auth.organization.id);
2023
1951
  }
2024
1952
  }
2025
1953
  ```
@@ -2031,8 +1959,8 @@ import { require_tenant_auth, HazoAuthError } from "hazo_auth/server-lib";
2031
1959
  export async function GET(request: NextRequest) {
2032
1960
  try {
2033
1961
  const auth = await require_tenant_auth(request);
2034
- // auth.selected_scope is guaranteed non-null
2035
- return NextResponse.json(await getData(auth.selected_scope.id));
1962
+ // auth.organization is guaranteed non-null
1963
+ return NextResponse.json(await getData(auth.organization.id));
2036
1964
  } catch (error) {
2037
1965
  if (error instanceof HazoAuthError) {
2038
1966
  return NextResponse.json(
@@ -2052,7 +1980,7 @@ import { withAuth } from "hazo_auth/server-lib";
2052
1980
  // Auth + params + error handling all automatic
2053
1981
  export const GET = withAuth<{ id: string }>(
2054
1982
  async (request, auth, { id }) => {
2055
- const data = await getData(auth.selected_scope.id, id);
1983
+ const data = await getData(auth.organization.id, id);
2056
1984
  return NextResponse.json(data);
2057
1985
  },
2058
1986
  { require_tenant: true }
@@ -2101,7 +2029,7 @@ const auth = await hazo_get_tenant_auth(request);
2101
2029
 
2102
2030
  // Return available scopes to frontend
2103
2031
  return NextResponse.json({
2104
- current_scope: auth.selected_scope,
2032
+ current_scope: auth.organization,
2105
2033
  available_scopes: auth.user_scopes.map(s => ({
2106
2034
  id: s.id,
2107
2035
  name: s.name,
@@ -127,20 +127,11 @@ ${exports}
127
127
  }
128
128
 
129
129
  function generate_page_content(page: PageDefinition): string {
130
- // Next 16 requires page default exports to satisfy `PageProps` (i.e. accept
131
- // `{ params?, searchParams? }` and nothing else). Re-exporting the named
132
- // component directly fails that check because the component carries custom
133
- // props (image_src, layout, …). The wrapper below is a parameter-less
134
- // function that returns the component — Next sees a `() => JSX` signature
135
- // which trivially satisfies PageProps. Direct JSX consumers still use the
136
- // named export from `hazo_auth/pages` and get the full custom-prop API.
137
130
  return `// Generated by hazo_auth - do not edit manually
138
131
  // Page: /${page.path}
139
132
  import { ${page.component_name} } from "hazo_auth/pages";
140
133
 
141
- export default function Page() {
142
- return <${page.component_name} />;
143
- }
134
+ export default ${page.component_name};
144
135
  `;
145
136
  }
146
137
 
@@ -64,9 +64,6 @@ const REQUIRED_API_ROUTES = [
64
64
  { path: "api/hazo_auth/user_management/users/roles", method: "GET" },
65
65
  { path: "api/hazo_auth/user_management/users/roles", method: "POST" },
66
66
  { path: "api/hazo_auth/user_management/users/roles", method: "PUT" },
67
- // OTP routes
68
- { path: "api/hazo_auth/otp/request", method: "POST" },
69
- { path: "api/hazo_auth/otp/verify", method: "POST" },
70
67
  ];
71
68
 
72
69
  // section: helpers
@@ -537,7 +534,6 @@ const REQUIRED_TABLES = [
537
534
  "hazo_role_permissions",
538
535
  "hazo_invitations",
539
536
  "hazo_refresh_tokens",
540
- "hazo_email_otps",
541
537
  ];
542
538
 
543
539
  const TEXT_ID_TABLES = [
@@ -1,12 +1,4 @@
1
1
  // file_description: Type definitions and error classes for hazo_get_auth utility
2
- //
3
- // Naming note (v6.0.0): the field previously called `organization` (and
4
- // `organization_id`) on `TenantAuthResult` was renamed to `selected_scope`
5
- // (and `selected_scope_id`), and the type `TenantOrganization` was renamed
6
- // to `SelectedScope`. The multi-tenancy model is scopes throughout; the
7
- // old name was a legacy synonym for "the currently selected scope" derived
8
- // from the scope-selection cookie/header. No deprecation shim is provided.
9
- //
10
2
  // section: types
11
3
 
12
4
  /**
@@ -131,11 +123,10 @@ export type ScopeDetails = {
131
123
  };
132
124
 
133
125
  /**
134
- * Currently selected scope information returned in tenant auth results.
135
- * Simplified view of the scope chosen via the scope-selection cookie or
136
- * `X-Hazo-Scope-Id` header.
126
+ * Tenant/organization information returned in tenant auth results
127
+ * Simplified view of scope for API responses
137
128
  */
138
- export type SelectedScope = {
129
+ export type TenantOrganization = {
139
130
  id: string;
140
131
  name: string;
141
132
  slug: string | null;
@@ -176,9 +167,9 @@ export type TenantAuthResult =
176
167
  permissions: string[];
177
168
  permission_ok: boolean;
178
169
  missing_permissions?: string[];
179
- selected_scope: SelectedScope | null;
180
- /** Shorthand for selected_scope?.id - commonly used for DB query filters. */
181
- selected_scope_id: string | null;
170
+ organization: TenantOrganization | null;
171
+ /** Shorthand for organization?.id - commonly used for DB query filters */
172
+ organization_id: string | null;
182
173
  user_scopes: ScopeDetails[];
183
174
  scope_ok?: boolean;
184
175
  scope_access_via?: ScopeAccessInfo;
@@ -188,20 +179,20 @@ export type TenantAuthResult =
188
179
  user: null;
189
180
  permissions: [];
190
181
  permission_ok: false;
191
- selected_scope: null;
192
- /** Shorthand for selected_scope?.id - commonly used for DB query filters. */
193
- selected_scope_id: null;
182
+ organization: null;
183
+ /** Shorthand for organization?.id - commonly used for DB query filters */
184
+ organization_id: null;
194
185
  user_scopes: [];
195
186
  scope_ok?: false;
196
187
  };
197
188
 
198
189
  /**
199
- * Guaranteed authenticated result with non-null selected_scope.
200
- * Returned by require_tenant_auth when validation passes.
190
+ * Guaranteed authenticated result with non-null organization
191
+ * Returned by require_tenant_auth when validation passes
201
192
  */
202
193
  export type RequiredTenantAuthResult = TenantAuthResult & {
203
194
  authenticated: true;
204
- selected_scope: SelectedScope;
195
+ organization: TenantOrganization;
205
196
  };
206
197
 
207
198
  // section: tenant_error_classes