hazo_auth 5.3.1 → 6.1.1

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 (126) hide show
  1. package/README.md +167 -17
  2. package/SETUP_CHECKLIST.md +99 -7
  3. package/cli-src/cli/generate.ts +10 -1
  4. package/cli-src/cli/validate.ts +4 -0
  5. package/cli-src/lib/auth/auth_types.ts +21 -12
  6. package/cli-src/lib/auth/hazo_get_tenant_auth.server.ts +25 -24
  7. package/cli-src/lib/auth/index.ts +2 -2
  8. package/cli-src/lib/auth/with_auth.server.ts +15 -15
  9. package/cli-src/lib/cookies_config.server.ts +1 -0
  10. package/cli-src/lib/login_config.server.ts +14 -0
  11. package/cli-src/lib/otp_config.server.ts +91 -0
  12. package/cli-src/lib/services/email_service.ts +3 -1
  13. package/cli-src/lib/services/email_template_manifest.ts +17 -0
  14. package/cli-src/lib/services/email_templates/otp_signin_code.html +13 -0
  15. package/cli-src/lib/services/email_templates/otp_signin_code.txt +5 -0
  16. package/cli-src/lib/services/index.ts +8 -2
  17. package/cli-src/lib/services/otp_service.ts +295 -0
  18. package/cli-src/lib/services/session_token_service.ts +4 -1
  19. package/config/hazo_auth_config.example.ini +38 -0
  20. package/dist/cli/generate.d.ts.map +1 -1
  21. package/dist/cli/generate.js +10 -1
  22. package/dist/cli/validate.d.ts.map +1 -1
  23. package/dist/cli/validate.js +4 -0
  24. package/dist/client.d.ts +2 -0
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +1 -0
  27. package/dist/components/layouts/login/index.d.ts +7 -1
  28. package/dist/components/layouts/login/index.d.ts.map +1 -1
  29. package/dist/components/layouts/login/index.js +2 -2
  30. package/dist/components/layouts/otp/index.d.ts +10 -0
  31. package/dist/components/layouts/otp/index.d.ts.map +1 -0
  32. package/dist/components/layouts/otp/index.js +14 -0
  33. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  34. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +8 -3
  35. package/dist/components/otp/OTPRequestForm.d.ts +11 -0
  36. package/dist/components/otp/OTPRequestForm.d.ts.map +1 -0
  37. package/dist/components/otp/OTPRequestForm.js +42 -0
  38. package/dist/components/otp/OTPVerifyForm.d.ts +16 -0
  39. package/dist/components/otp/OTPVerifyForm.d.ts.map +1 -0
  40. package/dist/components/otp/OTPVerifyForm.js +75 -0
  41. package/dist/components/otp/index.d.ts +5 -0
  42. package/dist/components/otp/index.d.ts.map +1 -0
  43. package/dist/components/otp/index.js +2 -0
  44. package/dist/components/ui/input-otp.d.ts +35 -0
  45. package/dist/components/ui/input-otp.d.ts.map +1 -0
  46. package/dist/components/ui/input-otp.js +44 -0
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/lib/auth/auth_types.d.ts +13 -12
  50. package/dist/lib/auth/auth_types.d.ts.map +1 -1
  51. package/dist/lib/auth/auth_types.js +8 -0
  52. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts +8 -7
  53. package/dist/lib/auth/hazo_get_tenant_auth.server.d.ts.map +1 -1
  54. package/dist/lib/auth/hazo_get_tenant_auth.server.js +23 -22
  55. package/dist/lib/auth/index.d.ts +2 -2
  56. package/dist/lib/auth/index.d.ts.map +1 -1
  57. package/dist/lib/auth/with_auth.server.d.ts +13 -13
  58. package/dist/lib/auth/with_auth.server.d.ts.map +1 -1
  59. package/dist/lib/auth/with_auth.server.js +2 -2
  60. package/dist/lib/cookies_config.server.d.ts +1 -0
  61. package/dist/lib/cookies_config.server.d.ts.map +1 -1
  62. package/dist/lib/cookies_config.server.js +1 -0
  63. package/dist/lib/login_config.server.d.ts +6 -0
  64. package/dist/lib/login_config.server.d.ts.map +1 -1
  65. package/dist/lib/login_config.server.js +7 -0
  66. package/dist/lib/otp_config.server.d.ts +49 -0
  67. package/dist/lib/otp_config.server.d.ts.map +1 -0
  68. package/dist/lib/otp_config.server.js +48 -0
  69. package/dist/lib/services/email_service.d.ts +1 -1
  70. package/dist/lib/services/email_service.d.ts.map +1 -1
  71. package/dist/lib/services/email_service.js +2 -0
  72. package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
  73. package/dist/lib/services/email_template_manifest.js +17 -0
  74. package/dist/lib/services/email_templates/otp_signin_code.html +13 -0
  75. package/dist/lib/services/email_templates/otp_signin_code.txt +5 -0
  76. package/dist/lib/services/index.d.ts +2 -0
  77. package/dist/lib/services/index.d.ts.map +1 -1
  78. package/dist/lib/services/index.js +1 -0
  79. package/dist/lib/services/otp_service.d.ts +46 -0
  80. package/dist/lib/services/otp_service.d.ts.map +1 -0
  81. package/dist/lib/services/otp_service.js +238 -0
  82. package/dist/lib/services/session_token_service.d.ts +3 -1
  83. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  84. package/dist/lib/services/session_token_service.js +4 -2
  85. package/dist/page_components/otp.d.ts +4 -0
  86. package/dist/page_components/otp.d.ts.map +1 -0
  87. package/dist/page_components/otp.js +5 -0
  88. package/dist/server/routes/index.d.ts +2 -0
  89. package/dist/server/routes/index.d.ts.map +1 -1
  90. package/dist/server/routes/index.js +3 -0
  91. package/dist/server/routes/me.d.ts.map +1 -1
  92. package/dist/server/routes/me.js +43 -1
  93. package/dist/server/routes/otp/request.d.ts +3 -0
  94. package/dist/server/routes/otp/request.d.ts.map +1 -0
  95. package/dist/server/routes/otp/request.js +33 -0
  96. package/dist/server/routes/otp/verify.d.ts +3 -0
  97. package/dist/server/routes/otp/verify.d.ts.map +1 -0
  98. package/dist/server/routes/otp/verify.js +58 -0
  99. package/dist/server-lib.d.ts +3 -0
  100. package/dist/server-lib.d.ts.map +1 -1
  101. package/dist/server-lib.js +2 -0
  102. package/dist/server_pages/forgot_password.d.ts +1 -1
  103. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  104. package/dist/server_pages/forgot_password.js +2 -1
  105. package/dist/server_pages/login.d.ts +1 -1
  106. package/dist/server_pages/login.d.ts.map +1 -1
  107. package/dist/server_pages/login.js +3 -2
  108. package/dist/server_pages/login_client_wrapper.d.ts +1 -1
  109. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  110. package/dist/server_pages/login_client_wrapper.js +2 -2
  111. package/dist/server_pages/my_settings.d.ts +1 -1
  112. package/dist/server_pages/my_settings.d.ts.map +1 -1
  113. package/dist/server_pages/my_settings.js +2 -1
  114. package/dist/server_pages/otp.d.ts +42 -0
  115. package/dist/server_pages/otp.d.ts.map +1 -0
  116. package/dist/server_pages/otp.js +38 -0
  117. package/dist/server_pages/register.d.ts +1 -1
  118. package/dist/server_pages/register.d.ts.map +1 -1
  119. package/dist/server_pages/register.js +2 -1
  120. package/dist/server_pages/reset_password.d.ts +1 -1
  121. package/dist/server_pages/reset_password.d.ts.map +1 -1
  122. package/dist/server_pages/reset_password.js +2 -1
  123. package/dist/server_pages/verify_email.d.ts +1 -1
  124. package/dist/server_pages/verify_email.d.ts.map +1 -1
  125. package/dist/server_pages/verify_email.js +2 -1
  126. package/package.json +20 -3
@@ -0,0 +1,46 @@
1
+ import "server-only";
2
+ /**
3
+ * Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
4
+ * Uses crypto.randomInt for uniform distribution.
5
+ */
6
+ export declare function generate_otp_code(): string;
7
+ export declare function hash_otp_code(code: string): Promise<string>;
8
+ export declare function verify_otp_code(otp_hash: string, code: string): Promise<boolean>;
9
+ export type RequestEmailOTPResult = {
10
+ ok: true;
11
+ } | {
12
+ ok: false;
13
+ error: "rate_limited";
14
+ retry_after_seconds: number;
15
+ };
16
+ /**
17
+ * Initiates an OTP sign-in flow for the given email address.
18
+ *
19
+ * Behaviour:
20
+ * 1. Per-email rate limit — rejects if too many requests in the sliding window.
21
+ * 2. Per-IP rate limit — rejects if too many requests from this IP.
22
+ * 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
23
+ * 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
24
+ * 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
25
+ *
26
+ * Never reveals whether an email address is registered (always returns ok:true on success).
27
+ */
28
+ export declare function request_email_otp(args: {
29
+ email: string;
30
+ ip: string;
31
+ }): Promise<RequestEmailOTPResult>;
32
+ export type VerifyEmailOTPResult = {
33
+ ok: true;
34
+ user_id: string;
35
+ email: string;
36
+ session_token: string;
37
+ } | {
38
+ ok: false;
39
+ error: "invalid_or_expired";
40
+ };
41
+ export declare function verify_email_otp(args: {
42
+ email: string;
43
+ code: string;
44
+ ip: string;
45
+ }): Promise<VerifyEmailOTPResult>;
46
+ //# sourceMappingURL=otp_service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/otp_service.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAUrB;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAEjE;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMtF;AAID,MAAM,MAAM,qBAAqB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,cAAc,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,CAAC;AAItE;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA8GjC;AAID,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,oBAAoB,CAAA;CAAE,CAAC;AAE/C,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAsHhC"}
@@ -0,0 +1,238 @@
1
+ import "server-only";
2
+ import crypto from "node:crypto";
3
+ import argon2 from "argon2";
4
+ import { createCrudService } from "hazo_connect/server";
5
+ import { get_otp_config, hazo_auth_otp_session_ttl_seconds } from "../otp_config.server.js";
6
+ import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
7
+ import { send_template_email } from "./email_service.js";
8
+ import { create_app_logger } from "../app_logger.js";
9
+ import { create_session_token } from "./session_token_service.js";
10
+ /**
11
+ * Generates a cryptographically random 6-digit numeric OTP code (000000–999999).
12
+ * Uses crypto.randomInt for uniform distribution.
13
+ */
14
+ export function generate_otp_code() {
15
+ const n = crypto.randomInt(0, 1000000);
16
+ return String(n).padStart(6, "0");
17
+ }
18
+ export async function hash_otp_code(code) {
19
+ return argon2.hash(code);
20
+ }
21
+ export async function verify_otp_code(otp_hash, code) {
22
+ try {
23
+ return await argon2.verify(otp_hash, code);
24
+ }
25
+ catch (_a) {
26
+ return false;
27
+ }
28
+ }
29
+ // section: request_email_otp
30
+ /**
31
+ * Initiates an OTP sign-in flow for the given email address.
32
+ *
33
+ * Behaviour:
34
+ * 1. Per-email rate limit — rejects if too many requests in the sliding window.
35
+ * 2. Per-IP rate limit — rejects if too many requests from this IP.
36
+ * 3. Unknown email + auto_register=false — silent no-op (constant-time padding).
37
+ * 4. Unknown email + auto_register=true — inserts OTP row with user_id=null, dispatches email.
38
+ * 5. Known email — marks prior unconsumed rows consumed, inserts fresh OTP row, dispatches email.
39
+ *
40
+ * Never reveals whether an email address is registered (always returns ok:true on success).
41
+ */
42
+ export async function request_email_otp(args) {
43
+ const logger = create_app_logger();
44
+ const cfg = get_otp_config();
45
+ const email = args.email.trim().toLowerCase();
46
+ const ip = args.ip;
47
+ const adapter = get_hazo_connect_instance();
48
+ const otp_table = createCrudService(adapter, "hazo_email_otps");
49
+ const users_table = createCrudService(adapter, "hazo_users");
50
+ // 1. Per-email rate limit
51
+ const email_window_ms = cfg.email_rate_limit_window_seconds * 1000;
52
+ const email_threshold = new Date(Date.now() - email_window_ms).toISOString();
53
+ const recent_for_email = await otp_table.list((qb) => qb
54
+ .select(["created_at"])
55
+ .where("email", "eq", email)
56
+ .where("created_at", "gte", email_threshold));
57
+ if (recent_for_email.length >= cfg.email_rate_limit_max) {
58
+ const oldest = recent_for_email
59
+ .map((r) => Date.parse(String(r.created_at)))
60
+ .sort((a, b) => a - b)[0];
61
+ const retry_after_seconds = Math.max(1, Math.ceil((oldest + email_window_ms - Date.now()) / 1000));
62
+ logger.warn("otp_request_email_rate_limited", { email, ip, retry_after_seconds });
63
+ return { ok: false, error: "rate_limited", retry_after_seconds };
64
+ }
65
+ // 2. Per-IP rate limit
66
+ const ip_window_ms = cfg.ip_rate_limit_window_seconds * 1000;
67
+ const ip_threshold = new Date(Date.now() - ip_window_ms).toISOString();
68
+ const recent_for_ip = await otp_table.list((qb) => qb
69
+ .select(["created_at"])
70
+ .where("requester_ip", "eq", ip)
71
+ .where("created_at", "gte", ip_threshold));
72
+ if (recent_for_ip.length >= cfg.ip_rate_limit_max) {
73
+ const oldest = recent_for_ip
74
+ .map((r) => Date.parse(String(r.created_at)))
75
+ .sort((a, b) => a - b)[0];
76
+ const retry_after_seconds = Math.max(1, Math.ceil((oldest + ip_window_ms - Date.now()) / 1000));
77
+ logger.warn("otp_request_ip_rate_limited", { email, ip, retry_after_seconds });
78
+ return { ok: false, error: "rate_limited", retry_after_seconds };
79
+ }
80
+ // 3. Lookup user
81
+ const existing_users = await users_table.findBy({ email_address: email });
82
+ const existing_user = existing_users.length > 0 ? existing_users[0] : null;
83
+ // 4. Unknown email + auto_register=false → silent no-op with constant-time padding
84
+ if (!existing_user && !cfg.auto_register) {
85
+ await argon2.hash("000000"); // constant-time padding — never stored
86
+ return { ok: true };
87
+ }
88
+ // 5. Mark any unconsumed rows for this email as superseded
89
+ try {
90
+ const unconsumed = await otp_table.list((qb) => qb
91
+ .select(["id"])
92
+ .where("email", "eq", email)
93
+ .where("consumed_at", "is", null));
94
+ for (const row of unconsumed) {
95
+ await otp_table.updateById(String(row.id), { consumed_at: new Date().toISOString() });
96
+ }
97
+ }
98
+ catch (_a) {
99
+ // IS NULL filter may not be supported in all adapter versions — not critical
100
+ }
101
+ // 6. Generate code + hash + insert row
102
+ const code = generate_otp_code();
103
+ const otp_hash = await hash_otp_code(code);
104
+ const expires_at = new Date(Date.now() + cfg.code_ttl_seconds * 1000).toISOString();
105
+ const row_id = crypto.randomUUID();
106
+ await otp_table.insert({
107
+ id: row_id,
108
+ user_id: existing_user ? String(existing_user.id) : null,
109
+ email,
110
+ otp_hash,
111
+ expires_at,
112
+ attempt_count: 0,
113
+ requester_ip: ip,
114
+ });
115
+ // 7. Dispatch email — fire-and-forget; errors are logged but do not surface to caller
116
+ try {
117
+ await send_template_email("otp_signin_code", email, {
118
+ otp_code: code,
119
+ expires_in_minutes: String(Math.round(cfg.code_ttl_seconds / 60)),
120
+ });
121
+ }
122
+ catch (err) {
123
+ logger.error("otp_request_email_dispatch_failed", {
124
+ email,
125
+ ip,
126
+ error: err instanceof Error ? err.message : String(err),
127
+ });
128
+ // Return ok:true to preserve no-enumeration property — caller cannot distinguish
129
+ // a missing user from a delivery failure.
130
+ }
131
+ return { ok: true };
132
+ }
133
+ export async function verify_email_otp(args) {
134
+ const logger = create_app_logger();
135
+ const cfg = get_otp_config();
136
+ const email = args.email.trim().toLowerCase();
137
+ const code = args.code.trim();
138
+ const adapter = get_hazo_connect_instance();
139
+ const otp_table = createCrudService(adapter, "hazo_email_otps");
140
+ const users_table = createCrudService(adapter, "hazo_users");
141
+ const user_scopes_table = createCrudService(adapter, "hazo_user_scopes");
142
+ const roles_table = createCrudService(adapter, "hazo_roles");
143
+ // 1. Find most-recent unconsumed row for this email
144
+ const now_iso = new Date().toISOString();
145
+ const candidates = await otp_table.list((qb) => qb
146
+ .select(["id", "user_id", "otp_hash", "expires_at", "attempt_count"])
147
+ .where("email", "eq", email)
148
+ .where("consumed_at", "is", null)
149
+ .order("created_at", "desc")
150
+ .limit(1));
151
+ const row = candidates.length > 0 ? candidates[0] : null;
152
+ if (!row) {
153
+ return { ok: false, error: "invalid_or_expired" };
154
+ }
155
+ // 2. Check expiry
156
+ const expires_at_ms = Date.parse(String(row.expires_at));
157
+ if (Number.isNaN(expires_at_ms) || expires_at_ms < Date.now()) {
158
+ return { ok: false, error: "invalid_or_expired" };
159
+ }
160
+ // 3. argon2 verify
161
+ const is_valid = await verify_otp_code(String(row.otp_hash), code);
162
+ if (!is_valid) {
163
+ const new_attempt_count = Number(row.attempt_count) + 1;
164
+ const updates = { attempt_count: new_attempt_count };
165
+ if (new_attempt_count >= cfg.max_verify_attempts) {
166
+ updates.consumed_at = now_iso; // poison
167
+ }
168
+ await otp_table.updateById(String(row.id), updates);
169
+ logger.info("otp_verify_invalid_code", {
170
+ email,
171
+ attempt_count: new_attempt_count,
172
+ poisoned: new_attempt_count >= cfg.max_verify_attempts,
173
+ });
174
+ return { ok: false, error: "invalid_or_expired" };
175
+ }
176
+ // 4. Mark consumed
177
+ await otp_table.updateById(String(row.id), { consumed_at: now_iso });
178
+ // 5. Resolve / create user
179
+ let user_id = row.user_id ? String(row.user_id) : null;
180
+ if (user_id) {
181
+ // Ensure email_verified=true
182
+ const user = await users_table.findById(user_id);
183
+ if (user && !user.email_verified) {
184
+ await users_table.updateById(user_id, { email_verified: true });
185
+ }
186
+ }
187
+ else if (cfg.auto_register) {
188
+ // Create user + bind scope/role
189
+ const new_user_id = crypto.randomUUID();
190
+ await users_table.insert({
191
+ id: new_user_id,
192
+ email_address: email,
193
+ email_verified: true,
194
+ password_hash: null,
195
+ name: null,
196
+ });
197
+ // Resolve role_id from name — try scope-specific first, then any
198
+ const scoped_roles = await roles_table.findBy({
199
+ name: cfg.auto_assign_role_name,
200
+ scope_id: cfg.auto_assign_scope_id,
201
+ });
202
+ let role_id = null;
203
+ if (scoped_roles.length > 0) {
204
+ role_id = String(scoped_roles[0].id);
205
+ }
206
+ else {
207
+ const any_roles = await roles_table.findBy({ name: cfg.auto_assign_role_name });
208
+ if (any_roles.length > 0) {
209
+ role_id = String(any_roles[0].id);
210
+ }
211
+ }
212
+ if (!role_id) {
213
+ logger.error("otp_verify_auto_register_role_not_found", {
214
+ email,
215
+ scope_id: cfg.auto_assign_scope_id,
216
+ role_name: cfg.auto_assign_role_name,
217
+ });
218
+ return { ok: false, error: "invalid_or_expired" };
219
+ }
220
+ await user_scopes_table.insert({
221
+ user_id: new_user_id,
222
+ scope_id: cfg.auto_assign_scope_id,
223
+ root_scope_id: cfg.auto_assign_scope_id,
224
+ role_id,
225
+ });
226
+ user_id = new_user_id;
227
+ }
228
+ else {
229
+ // Row exists but no user and auto_register=false — stale edge case
230
+ logger.warn("otp_verify_no_user_resolvable", { email });
231
+ return { ok: false, error: "invalid_or_expired" };
232
+ }
233
+ // 6. Issue session JWT with OTP TTL
234
+ const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
235
+ const session_token = await create_session_token(user_id, email, undefined, ttl_seconds);
236
+ logger.info("otp_verify_ok", { email, user_id, ttl_seconds });
237
+ return { ok: true, user_id, email, session_token };
238
+ }
@@ -16,9 +16,11 @@ export type ValidateSessionTokenResult = {
16
16
  * Token includes user_id, email, issued at time, and expiration
17
17
  * @param user_id - User ID
18
18
  * @param email - User email address
19
+ * @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
20
+ * @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
19
21
  * @returns JWT token string
20
22
  */
21
- export declare function create_session_token(user_id: string, email: string, managed_by_user_id?: string): Promise<string>;
23
+ export declare function create_session_token(user_id: string, email: string, managed_by_user_id?: string, ttl_seconds?: number): Promise<string>;
22
24
  /**
23
25
  * Validates a JWT session token
24
26
  * Checks signature and expiration
@@ -1 +1 @@
1
- {"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuCF;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,kBAAkB,CAAC,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,CAAC,CA4CjB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,0BAA0B,CAAC,CAkDrC"}
1
+ {"version":3,"file":"session_token_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/session_token_service.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAuCF;;;;;;;;GAQG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,kBAAkB,CAAC,EAAE,MAAM,EAC3B,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA4CjB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,0BAA0B,CAAC,CAkDrC"}
@@ -41,14 +41,16 @@ function get_session_token_expiry_seconds() {
41
41
  * Token includes user_id, email, issued at time, and expiration
42
42
  * @param user_id - User ID
43
43
  * @param email - User email address
44
+ * @param managed_by_user_id - Optional: ID of the managing user (for impersonation)
45
+ * @param ttl_seconds - Optional: token lifetime in seconds (default: 30 days). Use 604800 for 7-day OTP sessions.
44
46
  * @returns JWT token string
45
47
  */
46
- export async function create_session_token(user_id, email, managed_by_user_id) {
48
+ export async function create_session_token(user_id, email, managed_by_user_id, ttl_seconds) {
47
49
  const logger = create_app_logger();
48
50
  try {
49
51
  const secret = get_jwt_secret();
50
52
  const now = Math.floor(Date.now() / 1000); // Current time in seconds
51
- const expiry_seconds = get_session_token_expiry_seconds();
53
+ const expiry_seconds = ttl_seconds !== null && ttl_seconds !== void 0 ? ttl_seconds : get_session_token_expiry_seconds();
52
54
  const exp = now + expiry_seconds;
53
55
  const payload = { user_id, email };
54
56
  if (managed_by_user_id) {
@@ -0,0 +1,4 @@
1
+ import "server-only";
2
+ export { default, OTPPage } from "../server_pages/otp.js";
3
+ export type { OTPPageProps } from "../server_pages/otp";
4
+ //# sourceMappingURL=otp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp.d.ts","sourceRoot":"","sources":["../../src/page_components/otp.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAGrB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACvD,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,5 @@
1
+ // file_description: zero-config OTP page component for hazo_auth
2
+ // section: server-only-guard
3
+ import "server-only";
4
+ // section: imports
5
+ export { default, OTPPage } from "../server_pages/otp.js";
@@ -8,6 +8,8 @@ export { POST as changePasswordPOST } from "./change_password.js";
8
8
  export { GET as validateResetTokenGET } from "./validate_reset_token.js";
9
9
  export { GET as verifyEmailGET } from "./verify_email.js";
10
10
  export { POST as resendVerificationPOST } from "./resend_verification.js";
11
+ export { otpRequestPOST } from "./otp/request.js";
12
+ export { otpVerifyPOST } from "./otp/verify.js";
11
13
  export { PATCH as updateUserPATCH } from "./update_user.js";
12
14
  export { POST as uploadProfilePicturePOST } from "./upload_profile_picture.js";
13
15
  export { DELETE as removeProfilePictureDELETE } from "./remove_profile_picture.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/routes/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/routes/index.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,9 @@ export { GET as validateResetTokenGET } from "./validate_reset_token.js";
13
13
  // Email verification routes
14
14
  export { GET as verifyEmailGET } from "./verify_email.js";
15
15
  export { POST as resendVerificationPOST } from "./resend_verification.js";
16
+ // OTP routes (one-time password via email)
17
+ export { otpRequestPOST } from "./otp/request.js";
18
+ export { otpVerifyPOST } from "./otp/verify.js";
16
19
  // User profile routes
17
20
  export { PATCH as updateUserPATCH } from "./update_user.js";
18
21
  export { POST as uploadProfilePicturePOST } from "./upload_profile_picture.js";
@@ -1 +1 @@
1
- {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/server/routes/me.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAoBxD;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;IA+G7C"}
1
+ {"version":3,"file":"me.d.ts","sourceRoot":"","sources":["../../../src/server/routes/me.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA+BxD;;;;;GAKG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;IAqJ7C"}
@@ -1,6 +1,7 @@
1
1
  // file_description: API route handler to get current authenticated user information with permissions
2
2
  // section: imports
3
3
  import { NextResponse } from "next/server";
4
+ import { jwtVerify } from "jose";
4
5
  import { hazo_get_auth } from "../../lib/auth/hazo_get_auth.server.js";
5
6
  import { get_hazo_connect_instance } from "../../lib/hazo_connect_instance.server.js";
6
7
  import { createCrudService } from "hazo_connect/server";
@@ -8,6 +9,9 @@ import { map_db_source_to_ui } from "../../lib/services/profile_picture_source_m
8
9
  import { create_app_logger } from "../../lib/app_logger.js";
9
10
  import { get_filename, get_line_number } from "../../lib/utils/api_route_helpers.js";
10
11
  import { is_user_types_enabled, get_user_type_by_key, } from "../../lib/user_types_config.server.js";
12
+ import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES, } from "../../lib/cookies_config.server.js";
13
+ import { create_session_token } from "../../lib/services/session_token_service.js";
14
+ import { get_otp_config, hazo_auth_otp_session_ttl_seconds, } from "../../lib/otp_config.server.js";
11
15
  // section: helpers
12
16
  function strip_sentinel_email(email) {
13
17
  if (!email)
@@ -24,6 +28,7 @@ function strip_sentinel_email(email) {
24
28
  * Always returns the same format to prevent downstream variations.
25
29
  */
26
30
  export async function GET(request) {
31
+ var _a, _b, _c, _d, _e, _f;
27
32
  const logger = create_app_logger();
28
33
  try {
29
34
  // Use hazo_get_auth to get user with permissions
@@ -70,7 +75,7 @@ export async function GET(request) {
70
75
  }
71
76
  // Return unified format with all fields
72
77
  const profile_pic = auth_result.user.profile_picture_url;
73
- return NextResponse.json({
78
+ const response = NextResponse.json({
74
79
  authenticated: true,
75
80
  // Top-level fields for backward compatibility
76
81
  user_id: auth_result.user.id,
@@ -100,6 +105,43 @@ export async function GET(request) {
100
105
  permission_ok: auth_result.permission_ok,
101
106
  missing_permissions: auth_result.missing_permissions,
102
107
  }, { status: 200 });
108
+ // --- OTP sliding-session hook ---
109
+ const session_kind = (_a = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.SESSION_KIND))) === null || _a === void 0 ? void 0 : _a.value;
110
+ if (session_kind === "otp") {
111
+ try {
112
+ const session_cookie = (_b = request.cookies.get(get_cookie_name(BASE_COOKIE_NAMES.SESSION))) === null || _b === void 0 ? void 0 : _b.value;
113
+ if (session_cookie) {
114
+ const secret = new TextEncoder().encode((_c = process.env.JWT_SECRET) !== null && _c !== void 0 ? _c : "");
115
+ const { payload } = await jwtVerify(session_cookie, secret);
116
+ const exp = Number((_d = payload.exp) !== null && _d !== void 0 ? _d : 0);
117
+ const now_seconds = Math.floor(Date.now() / 1000);
118
+ const otp_cfg = get_otp_config();
119
+ const seconds_until_exp = exp - now_seconds;
120
+ if (seconds_until_exp > 0 && seconds_until_exp < otp_cfg.slide_when_within_seconds) {
121
+ const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
122
+ const user_id = String((_e = payload.user_id) !== null && _e !== void 0 ? _e : "");
123
+ const user_email = String((_f = payload.email) !== null && _f !== void 0 ? _f : "");
124
+ const new_token = await create_session_token(user_id, user_email, undefined, ttl_seconds);
125
+ const cookie_options = get_cookie_options({
126
+ httpOnly: true,
127
+ secure: process.env.NODE_ENV === "production",
128
+ sameSite: "lax",
129
+ path: "/",
130
+ maxAge: ttl_seconds,
131
+ });
132
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION), new_token, cookie_options);
133
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_ID), user_id, cookie_options);
134
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL), user_email, cookie_options);
135
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION_KIND), "otp", cookie_options);
136
+ }
137
+ }
138
+ }
139
+ catch (slide_err) {
140
+ // Slide is best-effort — never break /me for this
141
+ }
142
+ }
143
+ // --- end OTP sliding-session hook ---
144
+ return response;
103
145
  }
104
146
  catch (error) {
105
147
  const error_message = error instanceof Error ? error.message : "Unknown error";
@@ -0,0 +1,3 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ export declare function otpRequestPOST(request: NextRequest): Promise<NextResponse>;
3
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../../../src/server/routes/otp/request.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAYxD,wBAAsB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAsBhF"}
@@ -0,0 +1,33 @@
1
+ // file_description: API route handler for OTP request (sends OTP email to user)
2
+ // section: imports
3
+ import { NextResponse } from "next/server";
4
+ import { z } from "zod";
5
+ import { request_email_otp } from "../../../lib/services/otp_service.js";
6
+ import { get_client_ip } from "../../../lib/auth/hazo_get_auth.server.js";
7
+ import { create_app_logger } from "../../../lib/app_logger.js";
8
+ // section: validation
9
+ const RequestSchema = z.object({
10
+ email: z.string().email().max(254),
11
+ });
12
+ // section: api_handler
13
+ export async function otpRequestPOST(request) {
14
+ const logger = create_app_logger();
15
+ try {
16
+ const body_raw = await request.json().catch(() => ({}));
17
+ const parsed = RequestSchema.safeParse(body_raw);
18
+ if (!parsed.success) {
19
+ return NextResponse.json({ ok: false, error: "invalid_email" }, { status: 400 });
20
+ }
21
+ const ip = get_client_ip(request);
22
+ const result = await request_email_otp({ email: parsed.data.email, ip });
23
+ if (result.ok === false && result.error === "rate_limited") {
24
+ return NextResponse.json(result, { status: 429 });
25
+ }
26
+ return NextResponse.json({ ok: true }, { status: 200 });
27
+ }
28
+ catch (err) {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ logger.error("otp_request_route_error", { error: msg });
31
+ return NextResponse.json({ ok: false, error: "server_error" }, { status: 500 });
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ export declare function otpVerifyPOST(request: NextRequest): Promise<NextResponse>;
3
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../../src/server/routes/otp/verify.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmBxD,wBAAsB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAmD/E"}
@@ -0,0 +1,58 @@
1
+ // file_description: API route handler for OTP verify (validates code and issues session cookies)
2
+ // section: imports
3
+ import { NextResponse } from "next/server";
4
+ import { z } from "zod";
5
+ import { verify_email_otp } from "../../../lib/services/otp_service.js";
6
+ import { hazo_auth_otp_session_ttl_seconds } from "../../../lib/otp_config.server.js";
7
+ import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES, } from "../../../lib/cookies_config.server.js";
8
+ import { get_client_ip } from "../../../lib/auth/hazo_get_auth.server.js";
9
+ import { create_app_logger } from "../../../lib/app_logger.js";
10
+ // section: validation
11
+ const VerifySchema = z.object({
12
+ email: z.string().email().max(254),
13
+ code: z.string().regex(/^\d{6}$/),
14
+ });
15
+ // section: api_handler
16
+ export async function otpVerifyPOST(request) {
17
+ const logger = create_app_logger();
18
+ try {
19
+ const body_raw = await request.json().catch(() => ({}));
20
+ const parsed = VerifySchema.safeParse(body_raw);
21
+ if (!parsed.success) {
22
+ return NextResponse.json({ ok: false, error: "invalid_or_expired" }, { status: 400 });
23
+ }
24
+ const ip = get_client_ip(request);
25
+ const result = await verify_email_otp({
26
+ email: parsed.data.email,
27
+ code: parsed.data.code,
28
+ ip,
29
+ });
30
+ if (result.ok === false) {
31
+ return NextResponse.json({ ok: false, error: "invalid_or_expired" }, { status: 400 });
32
+ }
33
+ const ttl_seconds = hazo_auth_otp_session_ttl_seconds();
34
+ const base_cookie_options = {
35
+ httpOnly: true,
36
+ secure: process.env.NODE_ENV === "production",
37
+ sameSite: "lax",
38
+ path: "/",
39
+ maxAge: ttl_seconds,
40
+ };
41
+ const cookie_options = get_cookie_options(base_cookie_options);
42
+ const response = NextResponse.json({
43
+ ok: true,
44
+ user_id: result.user_id,
45
+ email: result.email,
46
+ });
47
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION), result.session_token, cookie_options);
48
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_ID), result.user_id, cookie_options);
49
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.USER_EMAIL), result.email, cookie_options);
50
+ response.cookies.set(get_cookie_name(BASE_COOKIE_NAMES.SESSION_KIND), "otp", cookie_options);
51
+ return response;
52
+ }
53
+ catch (err) {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ logger.error("otp_verify_route_error", { error: msg });
56
+ return NextResponse.json({ ok: false, error: "invalid_or_expired" }, { status: 400 });
57
+ }
58
+ }
@@ -24,6 +24,9 @@ export { get_oauth_config, is_google_oauth_enabled, is_email_password_enabled, }
24
24
  export type { OAuthConfig } from "./lib/oauth_config.server";
25
25
  export { get_branding_config, is_branding_enabled, is_allowed_logo_format, get_max_logo_size_bytes, } from "./lib/branding_config.server.js";
26
26
  export type { FirmBrandingConfig } from "./lib/branding_config.server";
27
+ export { get_otp_config, hazo_auth_otp_session_ttl_seconds, OTP_CONFIG_DEFAULTS } from "./lib/otp_config.server.js";
28
+ export type { OtpConfig } from "./lib/otp_config.server";
29
+ export { request_email_otp, verify_email_otp } from "./lib/services/otp_service.js";
27
30
  export { create_sqlite_hazo_connect } from "./lib/hazo_connect_setup.js";
28
31
  export { get_hazo_connect_instance } from "./lib/hazo_connect_instance.server.js";
29
32
  export { create_app_logger } from "./lib/app_logger.js";
@@ -1 +1 @@
1
- {"version":3,"file":"server-lib.d.ts","sourceRoot":"","sources":["../src/server-lib.ts"],"names":[],"mappings":"AAYA,OAAO,aAAa,CAAC;AAGrB,cAAc,kBAAkB,CAAC;AAGjC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAGrF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,2CAA2C,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAG/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,cAAc,+BAA+B,CAAC"}
1
+ {"version":3,"file":"server-lib.d.ts","sourceRoot":"","sources":["../src/server-lib.ts"],"names":[],"mappings":"AAYA,OAAO,aAAa,CAAC;AAGrB,cAAc,kBAAkB,CAAC;AAGjC,cAAc,sBAAsB,CAAC;AACrC,OAAO,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAGrF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uCAAuC,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,2CAA2C,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,8BAA8B,CAAC;AACtC,YAAY,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,iCAAiC,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACjH,YAAY,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAGjF,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAG/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,cAAc,+BAA+B,CAAC"}
@@ -37,6 +37,8 @@ export { get_user_fields_config } from "./lib/user_fields_config.server.js";
37
37
  export { get_file_types_config } from "./lib/file_types_config.server.js";
38
38
  export { get_oauth_config, is_google_oauth_enabled, is_email_password_enabled, } from "./lib/oauth_config.server.js";
39
39
  export { get_branding_config, is_branding_enabled, is_allowed_logo_format, get_max_logo_size_bytes, } from "./lib/branding_config.server.js";
40
+ export { get_otp_config, hazo_auth_otp_session_ttl_seconds, OTP_CONFIG_DEFAULTS } from "./lib/otp_config.server.js";
41
+ export { request_email_otp, verify_email_otp } from "./lib/services/otp_service.js";
40
42
  // section: hazo_connect_exports
41
43
  export { create_sqlite_hazo_connect } from "./lib/hazo_connect_setup.js";
42
44
  export { get_hazo_connect_instance } from "./lib/hazo_connect_instance.server.js";
@@ -50,6 +50,6 @@ export type ForgotPasswordPageProps = {
50
50
  * @param props - Optional visual and navigation customization props
51
51
  * @returns Server-rendered forgot password page
52
52
  */
53
- export default function ForgotPasswordPage({ image_src, image_alt, image_background_color, sign_in_path, sign_in_label, }?: ForgotPasswordPageProps): import("react/jsx-runtime").JSX.Element;
53
+ export default function ForgotPasswordPage(props: ForgotPasswordPageProps): import("react/jsx-runtime").JSX.Element;
54
54
  export { ForgotPasswordPage };
55
55
  //# sourceMappingURL=forgot_password.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"forgot_password.d.ts","sourceRoot":"","sources":["../../src/server_pages/forgot_password.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IAErC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,SAAS,EACT,SAAS,EACT,sBAAsB,EACtB,YAAgD,EAChD,aAAkD,GACnD,GAAE,uBAA4B,2CA0B9B;AAGD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
1
+ {"version":3,"file":"forgot_password.d.ts","sourceRoot":"","sources":["../../src/server_pages/forgot_password.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,CAAC;IAErC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,2CAiCxE;AAGD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -31,7 +31,8 @@ import { DEFAULT_FORGOT_PASSWORD } from "../lib/config/default_config.js";
31
31
  * @param props - Optional visual and navigation customization props
32
32
  * @returns Server-rendered forgot password page
33
33
  */
34
- export default function ForgotPasswordPage({ image_src, image_alt, image_background_color, sign_in_path = DEFAULT_FORGOT_PASSWORD.loginPath, sign_in_label = DEFAULT_FORGOT_PASSWORD.loginLabel, } = {}) {
34
+ export default function ForgotPasswordPage(props) {
35
+ const { image_src, image_alt, image_background_color, sign_in_path = DEFAULT_FORGOT_PASSWORD.loginPath, sign_in_label = DEFAULT_FORGOT_PASSWORD.loginLabel, } = props !== null && props !== void 0 ? props : {};
35
36
  // Load configuration from INI file (with defaults including asset images)
36
37
  const config = get_forgot_password_config();
37
38
  // Use props if provided, otherwise fall back to config (which includes default asset image)