hazo_auth 10.4.9 → 10.4.11

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.
@@ -305,6 +305,150 @@ function check_env_vars(): CheckResult[] {
305
305
  return results;
306
306
  }
307
307
 
308
+ // Canonical values live in src/lib/oauth_routes.ts; inlined here because the CLI
309
+ // compiles as an isolated tree and must not import the server-only lib.
310
+ const GOOGLE_NEXTAUTH_CALLBACK_PATH = "/api/auth/callback/google";
311
+ const FACEBOOK_NEXTAUTH_CALLBACK_PATH = "/api/auth/callback/facebook";
312
+
313
+ /**
314
+ * The edge proxy (Edge Runtime, no filesystem) reads the cookie prefix from the
315
+ * HAZO_AUTH_COOKIE_PREFIX env var, while the server reads cookie_prefix from the
316
+ * ini. If they differ — or the env var is unset — the edge gate can't find the
317
+ * session cookie and silently bounces authenticated users back to /login. This
318
+ * check catches that split before it wastes an afternoon.
319
+ */
320
+ function check_cookie_prefix(project_root: string): CheckResult[] {
321
+ const hazo_config = read_ini_file(path.join(project_root, "config", "hazo_auth_config.ini"));
322
+ const ini_prefix = (hazo_config?.["hazo_auth__cookies"]?.["cookie_prefix"] ?? "").trim();
323
+ const env_prefix = (process.env.HAZO_AUTH_COOKIE_PREFIX ?? "").trim();
324
+
325
+ if (!ini_prefix) {
326
+ // Already reported as a fail by check_config_values; nothing to compare.
327
+ return [];
328
+ }
329
+ if (!env_prefix) {
330
+ return [
331
+ {
332
+ name: "Cookie prefix (env ↔ ini)",
333
+ status: "fail",
334
+ message: `HAZO_AUTH_COOKIE_PREFIX env var not set. The edge proxy throws without it and bounces authenticated users to /login. Set HAZO_AUTH_COOKIE_PREFIX=${ini_prefix} to match cookie_prefix in hazo_auth_config.ini.`,
335
+ },
336
+ ];
337
+ }
338
+ if (env_prefix !== ini_prefix) {
339
+ return [
340
+ {
341
+ name: "Cookie prefix (env ↔ ini)",
342
+ status: "fail",
343
+ message: `Mismatch: HAZO_AUTH_COOKIE_PREFIX="${env_prefix}" but ini cookie_prefix="${ini_prefix}". They must be identical or the edge gate can't find the session cookie. Make them equal.`,
344
+ },
345
+ ];
346
+ }
347
+ return [{ name: "Cookie prefix (env ↔ ini)", status: "pass", message: `"${env_prefix}" (matches)` }];
348
+ }
349
+
350
+ function read_oauth_origin(): { origin: string; resolved: boolean } {
351
+ const nextauth_url = process.env.NEXTAUTH_URL;
352
+ if (nextauth_url && nextauth_url.trim().length > 0) {
353
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
354
+ }
355
+ const port = process.env.PORT ?? "3000";
356
+ return { origin: `http://localhost:${port}`, resolved: false };
357
+ }
358
+
359
+ function check_oauth(project_root: string): CheckResult[] {
360
+ const results: CheckResult[] = [];
361
+
362
+ const hazo_config_path = path.join(project_root, "config", "hazo_auth_config.ini");
363
+ const hazo_config = read_ini_file(hazo_config_path);
364
+ const oauth_section = hazo_config?.["hazo_auth__oauth"] ?? {};
365
+ const enable_google = (oauth_section["enable_google"] ?? "").toLowerCase() === "true";
366
+ const enable_facebook = (oauth_section["enable_facebook_oauth"] ?? "").toLowerCase() === "true";
367
+
368
+ if (!enable_google && !enable_facebook) {
369
+ results.push({
370
+ name: "OAuth providers",
371
+ status: "pass",
372
+ message: "No OAuth providers enabled (email/password only)",
373
+ });
374
+ return results;
375
+ }
376
+
377
+ const { origin, resolved } = read_oauth_origin();
378
+
379
+ // NEXTAUTH_URL — source of the redirect URI.
380
+ results.push(
381
+ resolved
382
+ ? { name: "NEXTAUTH_URL", status: "pass", message: origin }
383
+ : {
384
+ name: "NEXTAUTH_URL",
385
+ status: "fail",
386
+ message: `Not set. The redirect URI below is a guess from PORT. Set NEXTAUTH_URL=${origin}`,
387
+ }
388
+ );
389
+
390
+ // NEXTAUTH_SECRET — required for OAuth state/session signing.
391
+ const secret = process.env.NEXTAUTH_SECRET;
392
+ if (!secret || secret.length === 0) {
393
+ results.push({
394
+ name: "NEXTAUTH_SECRET",
395
+ status: "fail",
396
+ message: "Not set. Generate: openssl rand -base64 32",
397
+ });
398
+ } else if (secret.length < 32) {
399
+ results.push({
400
+ name: "NEXTAUTH_SECRET",
401
+ status: "warn",
402
+ message: `Only ${secret.length} chars; use at least 32. Generate: openssl rand -base64 32`,
403
+ });
404
+ } else {
405
+ results.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
406
+ }
407
+
408
+ if (enable_google) {
409
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
410
+ const cs = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
411
+ const missing = [
412
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
413
+ !cs ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
414
+ ].filter(Boolean);
415
+ results.push(
416
+ missing.length === 0
417
+ ? { name: "Google OAuth credentials", status: "pass", message: "Set" }
418
+ : {
419
+ name: "Google OAuth credentials",
420
+ status: "fail",
421
+ message: `enable_google=true but missing: ${missing.join(", ")}`,
422
+ }
423
+ );
424
+ }
425
+
426
+ return results;
427
+ }
428
+
429
+ /**
430
+ * Returns the exact redirect URIs to register, per enabled provider. Printed as
431
+ * a copy-pasteable box so the dev never guesses the port/path — the single thing
432
+ * that prevents redirect_uri_mismatch.
433
+ */
434
+ function get_oauth_redirect_lines(project_root: string): string[] {
435
+ const hazo_config = read_ini_file(path.join(project_root, "config", "hazo_auth_config.ini"));
436
+ const oauth_section = hazo_config?.["hazo_auth__oauth"] ?? {};
437
+ const enable_google = (oauth_section["enable_google"] ?? "").toLowerCase() === "true";
438
+ const enable_facebook = (oauth_section["enable_facebook_oauth"] ?? "").toLowerCase() === "true";
439
+ if (!enable_google && !enable_facebook) return [];
440
+
441
+ const { origin, resolved } = read_oauth_origin();
442
+ const lines: string[] = [];
443
+ if (enable_google) lines.push(` google → ${origin}${GOOGLE_NEXTAUTH_CALLBACK_PATH}`);
444
+ if (enable_facebook) lines.push(` facebook → ${origin}${FACEBOOK_NEXTAUTH_CALLBACK_PATH}`);
445
+ if (!resolved) {
446
+ lines.push("");
447
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT. Set it and re-register.");
448
+ }
449
+ return lines;
450
+ }
451
+
308
452
  function check_api_routes(project_root: string): CheckResult[] {
309
453
  const results: CheckResult[] = [];
310
454
 
@@ -743,6 +887,25 @@ export function run_validation(): ValidationSummary {
743
887
  all_results.push(...env_results);
744
888
  console.log();
745
889
 
890
+ const cookie_prefix_results = check_cookie_prefix(project_root);
891
+ if (cookie_prefix_results.length > 0) {
892
+ console.log("\x1b[1m🍪 Cookie Prefix\x1b[0m");
893
+ cookie_prefix_results.forEach(print_result);
894
+ all_results.push(...cookie_prefix_results);
895
+ console.log();
896
+ }
897
+
898
+ console.log("\x1b[1m🔑 OAuth Providers\x1b[0m");
899
+ const oauth_results = check_oauth(project_root);
900
+ oauth_results.forEach(print_result);
901
+ all_results.push(...oauth_results);
902
+ const redirect_lines = get_oauth_redirect_lines(project_root);
903
+ if (redirect_lines.length > 0) {
904
+ console.log("\n Register these redirect URIs in the provider console (must match exactly):");
905
+ redirect_lines.forEach(line => console.log(line));
906
+ }
907
+ console.log();
908
+
746
909
  console.log("\x1b[1m🗄️ Database\x1b[0m");
747
910
  const db_results = check_database(project_root);
748
911
  db_results.forEach(print_result);
@@ -0,0 +1,67 @@
1
+ // file_description: client-safe decoder mapping NextAuth/hazo_auth `?error=` codes
2
+ // to human-readable guidance. Use on a login/error page to replace opaque codes.
3
+
4
+ export type AuthErrorHelp = {
5
+ /** The raw error code from the `?error=` query param. */
6
+ code: string;
7
+ /** Short, user-facing summary. */
8
+ title: string;
9
+ /** Developer-facing detail and remediation. */
10
+ detail: string;
11
+ };
12
+
13
+ const KNOWN: Record<string, Omit<AuthErrorHelp, "code">> = {
14
+ Configuration: {
15
+ title: "Sign-in is misconfigured.",
16
+ detail:
17
+ "The server's OAuth/NextAuth config is incomplete. Check NEXTAUTH_SECRET, NEXTAUTH_URL, and provider credentials. Run `npx hazo_auth validate`.",
18
+ },
19
+ AccessDenied: {
20
+ title: "Access denied.",
21
+ detail: "You don't have permission to sign in, or you cancelled the consent screen.",
22
+ },
23
+ Verification: {
24
+ title: "This sign-in link has expired.",
25
+ detail: "The link was already used or timed out. Request a new one.",
26
+ },
27
+ OAuthSignin: {
28
+ title: "Couldn't start sign-in.",
29
+ detail: "Error constructing the provider authorization URL. Check provider credentials.",
30
+ },
31
+ OAuthCallback: {
32
+ title: "Sign-in callback failed.",
33
+ detail:
34
+ "The provider rejected the callback. Most often the registered redirect URI doesn't match. Confirm the provider console lists the exact URI hazo_auth prints at boot.",
35
+ },
36
+ OAuthAccountNotLinked: {
37
+ title: "This email is already linked to a different sign-in method.",
38
+ detail: "Sign in with the original method, then link the new provider from account settings.",
39
+ },
40
+ GoogleTokenStorageUnconfigured: {
41
+ title: "Signed in, but offline Google access is unavailable.",
42
+ detail:
43
+ "Refresh-token storage needs HAZO_AUTH_OAUTH_KEY_CURRENT and HAZO_AUTH_OAUTH_KEY_<ID>. Login still works; set these only if you need offline Google API calls.",
44
+ },
45
+ redirect_uri_mismatch: {
46
+ title: "Sign-in blocked: redirect URI mismatch.",
47
+ detail:
48
+ "The redirect URI sent to the provider isn't registered. Register the exact URI hazo_auth prints at boot (origin = NEXTAUTH_URL + /api/auth/callback/<provider>). Note: providers usually show this on their own page, before returning to the app.",
49
+ },
50
+ };
51
+
52
+ /**
53
+ * Resolves a NextAuth/hazo_auth error code to friendly guidance. Unknown codes
54
+ * get a generic fallback so callers can always render something useful.
55
+ */
56
+ export function describeAuthError(code: string | null | undefined): AuthErrorHelp {
57
+ const key = (code ?? "").trim();
58
+ const hit = key ? KNOWN[key] : undefined;
59
+ if (hit) return { code: key, ...hit };
60
+ return {
61
+ code: key || "Unknown",
62
+ title: "Sign-in failed.",
63
+ detail: key
64
+ ? `Unrecognised error code "${key}". Check the server logs for details.`
65
+ : "No error code provided. Check the server logs for details.",
66
+ };
67
+ }
@@ -13,6 +13,7 @@ import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
13
13
  import { create_app_logger } from "../app_logger.js";
14
14
  import { store_google_oauth_token, GoogleTokenStorageUnconfigured } from "../services/google_token_service.js";
15
15
  import { GOOGLE_OAUTH_CALLBACK_PATH, FACEBOOK_OAUTH_CALLBACK_PATH } from "../oauth_routes.js";
16
+ import { logOAuthPreflight } from "../oauth_preflight.server.js";
16
17
 
17
18
  // section: types
18
19
  export type NextAuthCallbackUser = {
@@ -48,6 +49,10 @@ export type NextAuthCallbackProfile = {
48
49
  * @returns NextAuth configuration object
49
50
  */
50
51
  export function get_nextauth_config(): AuthOptions {
52
+ // Print enabled-provider redirect URIs + flag missing env once per process.
53
+ // No-op after the first call; safe to run on every config build.
54
+ logOAuthPreflight();
55
+
51
56
  const oauth_config = get_oauth_config();
52
57
  const providers = [];
53
58
 
@@ -0,0 +1,215 @@
1
+ // file_description: server-only OAuth preflight — validates env and prints the exact
2
+ // redirect URIs to register, killing the recurring `redirect_uri_mismatch` for new apps.
3
+ import "server-only";
4
+
5
+ import { get_oauth_config } from "./oauth_config.server.js";
6
+ import { get_cookies_config } from "./cookies_config.server.js";
7
+ import { googleRedirectUri, facebookRedirectUri } from "./oauth_routes.js";
8
+ import { create_app_logger } from "./app_logger.js";
9
+
10
+ // section: types
11
+ export type PreflightStatus = "pass" | "warn" | "fail";
12
+
13
+ export type PreflightCheck = {
14
+ name: string;
15
+ status: PreflightStatus;
16
+ message: string;
17
+ };
18
+
19
+ export type OAuthPreflightReport = {
20
+ /** Origin used to derive redirect URIs (NEXTAUTH_URL, or a fallback). */
21
+ origin: string;
22
+ /** True when NEXTAUTH_URL was actually set (vs. a guessed fallback). */
23
+ originResolved: boolean;
24
+ /** Absolute redirect URIs to paste into the provider console, per enabled provider. */
25
+ redirectUris: { provider: "google" | "facebook"; uri: string }[];
26
+ checks: PreflightCheck[];
27
+ };
28
+
29
+ // section: helpers
30
+ const MIN_SECRET_LEN = 32;
31
+
32
+ function resolve_origin(): { origin: string; resolved: boolean } {
33
+ const nextauth_url = process.env.NEXTAUTH_URL;
34
+ if (nextauth_url && nextauth_url.trim().length > 0) {
35
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
36
+ }
37
+ // Fallback so we can still print a best-guess URI to register.
38
+ const port = process.env.PORT ?? "3000";
39
+ return { origin: `http://localhost:${port}`, resolved: false };
40
+ }
41
+
42
+ /**
43
+ * Builds a structured OAuth config report without printing. Safe to call
44
+ * anywhere server-side (CLI, route, instrumentation). Pure config inspection —
45
+ * no DB or network access.
46
+ */
47
+ export function getOAuthPreflightReport(): OAuthPreflightReport {
48
+ const checks: PreflightCheck[] = [];
49
+ const { origin, resolved } = resolve_origin();
50
+ const oauth = get_oauth_config();
51
+ const redirectUris: OAuthPreflightReport["redirectUris"] = [];
52
+
53
+ // Cookie prefix: env (read by the edge proxy) must match the ini (read by the
54
+ // server). A mismatch — or an unset env var — makes the edge gate throw and
55
+ // silently bounce authenticated users to /login.
56
+ try {
57
+ const ini_prefix = (get_cookies_config().cookie_prefix ?? "").trim();
58
+ const env_prefix = (process.env.HAZO_AUTH_COOKIE_PREFIX ?? "").trim();
59
+ if (ini_prefix && !env_prefix) {
60
+ checks.push({
61
+ name: "Cookie prefix",
62
+ status: "fail",
63
+ message: `HAZO_AUTH_COOKIE_PREFIX env var not set. The edge proxy throws without it and bounces authenticated users to /login. Set HAZO_AUTH_COOKIE_PREFIX=${ini_prefix} (matching cookie_prefix in hazo_auth_config.ini).`,
64
+ });
65
+ } else if (ini_prefix && env_prefix && env_prefix !== ini_prefix) {
66
+ checks.push({
67
+ name: "Cookie prefix",
68
+ status: "fail",
69
+ message: `Mismatch: HAZO_AUTH_COOKIE_PREFIX="${env_prefix}" vs ini cookie_prefix="${ini_prefix}". They must be identical or the edge gate can't find the session cookie.`,
70
+ });
71
+ }
72
+ } catch {
73
+ // get_cookies_config throws when cookie_prefix is absent from the ini; that
74
+ // missing-config case is reported elsewhere (validate CLI / server boot).
75
+ }
76
+
77
+ // NEXTAUTH_URL — the source of the redirect URI.
78
+ if (resolved) {
79
+ checks.push({ name: "NEXTAUTH_URL", status: "pass", message: origin });
80
+ } else {
81
+ checks.push({
82
+ name: "NEXTAUTH_URL",
83
+ status: "fail",
84
+ message: `Not set. NextAuth derives the OAuth redirect URI from it. Set NEXTAUTH_URL=${origin} (matching the port you serve on).`,
85
+ });
86
+ }
87
+
88
+ // NEXTAUTH_URL port vs. the port the app is told to listen on.
89
+ const declared_port = process.env.PORT;
90
+ if (resolved && declared_port) {
91
+ let url_port: string | null = null;
92
+ try {
93
+ const u = new URL(origin);
94
+ url_port = u.port || (u.protocol === "https:" ? "443" : "80");
95
+ } catch {
96
+ url_port = null;
97
+ }
98
+ if (url_port && url_port !== declared_port) {
99
+ checks.push({
100
+ name: "Port match",
101
+ status: "warn",
102
+ message: `NEXTAUTH_URL port (${url_port}) differs from PORT (${declared_port}). The redirect URI uses ${url_port}; if you serve on ${declared_port}, OAuth will fail with redirect_uri_mismatch.`,
103
+ });
104
+ }
105
+ }
106
+
107
+ // NEXTAUTH_SECRET — required for OAuth session/state signing.
108
+ const secret = process.env.NEXTAUTH_SECRET;
109
+ if (!secret || secret.length === 0) {
110
+ checks.push({
111
+ name: "NEXTAUTH_SECRET",
112
+ status: "fail",
113
+ message: "Not set. Generate one: openssl rand -base64 32",
114
+ });
115
+ } else if (secret.length < MIN_SECRET_LEN) {
116
+ checks.push({
117
+ name: "NEXTAUTH_SECRET",
118
+ status: "warn",
119
+ message: `Only ${secret.length} chars; use at least ${MIN_SECRET_LEN}. Generate: openssl rand -base64 32`,
120
+ });
121
+ } else {
122
+ checks.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
123
+ }
124
+
125
+ // Google provider.
126
+ if (oauth.enable_google) {
127
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
128
+ const secretG = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
129
+ if (!id || !secretG) {
130
+ const missing = [
131
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
132
+ !secretG ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
133
+ ].filter(Boolean);
134
+ checks.push({
135
+ name: "Google OAuth credentials",
136
+ status: "fail",
137
+ message: `enable_google=true but missing: ${missing.join(", ")} (from Google Cloud Console).`,
138
+ });
139
+ } else {
140
+ checks.push({ name: "Google OAuth credentials", status: "pass", message: "Set" });
141
+ }
142
+ redirectUris.push({ provider: "google", uri: googleRedirectUri(origin) });
143
+ }
144
+
145
+ // Facebook provider.
146
+ if (oauth.enable_facebook_oauth) {
147
+ const appId = oauth.facebook_app_id || process.env.HAZO_AUTH_FACEBOOK_APP_ID;
148
+ const appSecret = oauth.facebook_app_secret || process.env.HAZO_AUTH_FACEBOOK_APP_SECRET;
149
+ if (!appId || !appSecret) {
150
+ checks.push({
151
+ name: "Facebook OAuth credentials",
152
+ status: "fail",
153
+ message: "enable_facebook_oauth=true but app_id/app_secret missing.",
154
+ });
155
+ } else {
156
+ checks.push({ name: "Facebook OAuth credentials", status: "pass", message: "Set" });
157
+ }
158
+ redirectUris.push({ provider: "facebook", uri: facebookRedirectUri(origin) });
159
+ }
160
+
161
+ return { origin, originResolved: resolved, redirectUris, checks };
162
+ }
163
+
164
+ // section: boot logger
165
+ let _logged_once = false;
166
+
167
+ /**
168
+ * Logs the OAuth preflight report once per process. Call from a Next.js
169
+ * `instrumentation.ts` `register()` hook to surface config problems — and the
170
+ * exact redirect URI to register — the moment the server boots, before anyone
171
+ * hits a broken sign-in.
172
+ *
173
+ * Idempotent: subsequent calls in the same process are no-ops.
174
+ */
175
+ export function logOAuthPreflight(): OAuthPreflightReport | null {
176
+ if (_logged_once) return null;
177
+ _logged_once = true;
178
+
179
+ const report = getOAuthPreflightReport();
180
+ const logger = create_app_logger();
181
+
182
+ // Always surface failing/warning checks (e.g. cookie-prefix mismatch), even
183
+ // when no OAuth provider is enabled and there are no redirect URIs to print.
184
+ for (const check of report.checks) {
185
+ if (check.status === "fail") {
186
+ logger.error("oauth_preflight_check_failed", { check: check.name, detail: check.message });
187
+ } else if (check.status === "warn") {
188
+ logger.warn("oauth_preflight_check_warning", { check: check.name, detail: check.message });
189
+ }
190
+ }
191
+
192
+ if (report.redirectUris.length === 0) return report; // No OAuth providers — nothing to register.
193
+
194
+ // Highlighted, copy-pasteable block — the single thing that prevents
195
+ // redirect_uri_mismatch. console (not logger) so it renders verbatim.
196
+ const lines: string[] = [
197
+ "",
198
+ "──────────────────────────────────────────────────────────────",
199
+ " hazo_auth · OAuth redirect URIs to register in the provider console",
200
+ "──────────────────────────────────────────────────────────────",
201
+ ];
202
+ for (const { provider, uri } of report.redirectUris) {
203
+ lines.push(` ${provider.padEnd(9)} → ${uri}`);
204
+ }
205
+ if (!report.originResolved) {
206
+ lines.push("");
207
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT.");
208
+ lines.push(" Set NEXTAUTH_URL to your real origin and re-register.");
209
+ }
210
+ lines.push("──────────────────────────────────────────────────────────────");
211
+ lines.push("");
212
+ console.log(lines.join("\n"));
213
+
214
+ return report;
215
+ }
@@ -11,3 +11,24 @@ export const FACEBOOK_NEXTAUTH_CALLBACK_PATH = "/api/auth/callback/facebook" as
11
11
 
12
12
  /** hazo_auth's custom post-auth handler — creates the hazo_auth session after Facebook sign-in. */
13
13
  export const FACEBOOK_OAUTH_CALLBACK_PATH = "/api/hazo_auth/oauth/facebook/callback" as const;
14
+
15
+ /**
16
+ * Builds the absolute Google redirect URI to register in Google Cloud Console.
17
+ * This is exactly the value NextAuth sends as `redirect_uri`; registering
18
+ * anything else (wrong port, https vs http, trailing slash) causes the
19
+ * `redirect_uri_mismatch` 400. Derive it from your origin (= NEXTAUTH_URL) so
20
+ * the two can never drift.
21
+ */
22
+ export function googleRedirectUri(origin: string): string {
23
+ return joinOrigin(origin, GOOGLE_NEXTAUTH_CALLBACK_PATH);
24
+ }
25
+
26
+ /** Builds the absolute Facebook redirect URI to register in the Facebook Developer Console. */
27
+ export function facebookRedirectUri(origin: string): string {
28
+ return joinOrigin(origin, FACEBOOK_NEXTAUTH_CALLBACK_PATH);
29
+ }
30
+
31
+ function joinOrigin(origin: string, pathname: string): string {
32
+ // Tolerate a trailing slash on the origin; never emit a double slash.
33
+ return `${origin.replace(/\/+$/, "")}${pathname}`;
34
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/cli/validate.ts"],"names":[],"mappings":"AAOA,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB,CAAC;AA2rBF,wBAAgB,cAAc,IAAI,iBAAiB,CA2ElD"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/cli/validate.ts"],"names":[],"mappings":"AAOA,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB,CAAC;AA20BF,wBAAgB,cAAc,IAAI,iBAAiB,CA8FlD"}
@@ -268,6 +268,141 @@ function check_env_vars() {
268
268
  }
269
269
  return results;
270
270
  }
271
+ // Canonical values live in src/lib/oauth_routes.ts; inlined here because the CLI
272
+ // compiles as an isolated tree and must not import the server-only lib.
273
+ const GOOGLE_NEXTAUTH_CALLBACK_PATH = "/api/auth/callback/google";
274
+ const FACEBOOK_NEXTAUTH_CALLBACK_PATH = "/api/auth/callback/facebook";
275
+ /**
276
+ * The edge proxy (Edge Runtime, no filesystem) reads the cookie prefix from the
277
+ * HAZO_AUTH_COOKIE_PREFIX env var, while the server reads cookie_prefix from the
278
+ * ini. If they differ — or the env var is unset — the edge gate can't find the
279
+ * session cookie and silently bounces authenticated users back to /login. This
280
+ * check catches that split before it wastes an afternoon.
281
+ */
282
+ function check_cookie_prefix(project_root) {
283
+ var _a, _b, _c;
284
+ const hazo_config = read_ini_file(path.join(project_root, "config", "hazo_auth_config.ini"));
285
+ const ini_prefix = ((_b = (_a = hazo_config === null || hazo_config === void 0 ? void 0 : hazo_config["hazo_auth__cookies"]) === null || _a === void 0 ? void 0 : _a["cookie_prefix"]) !== null && _b !== void 0 ? _b : "").trim();
286
+ const env_prefix = ((_c = process.env.HAZO_AUTH_COOKIE_PREFIX) !== null && _c !== void 0 ? _c : "").trim();
287
+ if (!ini_prefix) {
288
+ // Already reported as a fail by check_config_values; nothing to compare.
289
+ return [];
290
+ }
291
+ if (!env_prefix) {
292
+ return [
293
+ {
294
+ name: "Cookie prefix (env ↔ ini)",
295
+ status: "fail",
296
+ message: `HAZO_AUTH_COOKIE_PREFIX env var not set. The edge proxy throws without it and bounces authenticated users to /login. Set HAZO_AUTH_COOKIE_PREFIX=${ini_prefix} to match cookie_prefix in hazo_auth_config.ini.`,
297
+ },
298
+ ];
299
+ }
300
+ if (env_prefix !== ini_prefix) {
301
+ return [
302
+ {
303
+ name: "Cookie prefix (env ↔ ini)",
304
+ status: "fail",
305
+ message: `Mismatch: HAZO_AUTH_COOKIE_PREFIX="${env_prefix}" but ini cookie_prefix="${ini_prefix}". They must be identical or the edge gate can't find the session cookie. Make them equal.`,
306
+ },
307
+ ];
308
+ }
309
+ return [{ name: "Cookie prefix (env ↔ ini)", status: "pass", message: `"${env_prefix}" (matches)` }];
310
+ }
311
+ function read_oauth_origin() {
312
+ var _a;
313
+ const nextauth_url = process.env.NEXTAUTH_URL;
314
+ if (nextauth_url && nextauth_url.trim().length > 0) {
315
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
316
+ }
317
+ const port = (_a = process.env.PORT) !== null && _a !== void 0 ? _a : "3000";
318
+ return { origin: `http://localhost:${port}`, resolved: false };
319
+ }
320
+ function check_oauth(project_root) {
321
+ var _a, _b, _c;
322
+ const results = [];
323
+ const hazo_config_path = path.join(project_root, "config", "hazo_auth_config.ini");
324
+ const hazo_config = read_ini_file(hazo_config_path);
325
+ const oauth_section = (_a = hazo_config === null || hazo_config === void 0 ? void 0 : hazo_config["hazo_auth__oauth"]) !== null && _a !== void 0 ? _a : {};
326
+ const enable_google = ((_b = oauth_section["enable_google"]) !== null && _b !== void 0 ? _b : "").toLowerCase() === "true";
327
+ const enable_facebook = ((_c = oauth_section["enable_facebook_oauth"]) !== null && _c !== void 0 ? _c : "").toLowerCase() === "true";
328
+ if (!enable_google && !enable_facebook) {
329
+ results.push({
330
+ name: "OAuth providers",
331
+ status: "pass",
332
+ message: "No OAuth providers enabled (email/password only)",
333
+ });
334
+ return results;
335
+ }
336
+ const { origin, resolved } = read_oauth_origin();
337
+ // NEXTAUTH_URL — source of the redirect URI.
338
+ results.push(resolved
339
+ ? { name: "NEXTAUTH_URL", status: "pass", message: origin }
340
+ : {
341
+ name: "NEXTAUTH_URL",
342
+ status: "fail",
343
+ message: `Not set. The redirect URI below is a guess from PORT. Set NEXTAUTH_URL=${origin}`,
344
+ });
345
+ // NEXTAUTH_SECRET — required for OAuth state/session signing.
346
+ const secret = process.env.NEXTAUTH_SECRET;
347
+ if (!secret || secret.length === 0) {
348
+ results.push({
349
+ name: "NEXTAUTH_SECRET",
350
+ status: "fail",
351
+ message: "Not set. Generate: openssl rand -base64 32",
352
+ });
353
+ }
354
+ else if (secret.length < 32) {
355
+ results.push({
356
+ name: "NEXTAUTH_SECRET",
357
+ status: "warn",
358
+ message: `Only ${secret.length} chars; use at least 32. Generate: openssl rand -base64 32`,
359
+ });
360
+ }
361
+ else {
362
+ results.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
363
+ }
364
+ if (enable_google) {
365
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
366
+ const cs = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
367
+ const missing = [
368
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
369
+ !cs ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
370
+ ].filter(Boolean);
371
+ results.push(missing.length === 0
372
+ ? { name: "Google OAuth credentials", status: "pass", message: "Set" }
373
+ : {
374
+ name: "Google OAuth credentials",
375
+ status: "fail",
376
+ message: `enable_google=true but missing: ${missing.join(", ")}`,
377
+ });
378
+ }
379
+ return results;
380
+ }
381
+ /**
382
+ * Returns the exact redirect URIs to register, per enabled provider. Printed as
383
+ * a copy-pasteable box so the dev never guesses the port/path — the single thing
384
+ * that prevents redirect_uri_mismatch.
385
+ */
386
+ function get_oauth_redirect_lines(project_root) {
387
+ var _a, _b, _c;
388
+ const hazo_config = read_ini_file(path.join(project_root, "config", "hazo_auth_config.ini"));
389
+ const oauth_section = (_a = hazo_config === null || hazo_config === void 0 ? void 0 : hazo_config["hazo_auth__oauth"]) !== null && _a !== void 0 ? _a : {};
390
+ const enable_google = ((_b = oauth_section["enable_google"]) !== null && _b !== void 0 ? _b : "").toLowerCase() === "true";
391
+ const enable_facebook = ((_c = oauth_section["enable_facebook_oauth"]) !== null && _c !== void 0 ? _c : "").toLowerCase() === "true";
392
+ if (!enable_google && !enable_facebook)
393
+ return [];
394
+ const { origin, resolved } = read_oauth_origin();
395
+ const lines = [];
396
+ if (enable_google)
397
+ lines.push(` google → ${origin}${GOOGLE_NEXTAUTH_CALLBACK_PATH}`);
398
+ if (enable_facebook)
399
+ lines.push(` facebook → ${origin}${FACEBOOK_NEXTAUTH_CALLBACK_PATH}`);
400
+ if (!resolved) {
401
+ lines.push("");
402
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT. Set it and re-register.");
403
+ }
404
+ return lines;
405
+ }
271
406
  function check_api_routes(project_root) {
272
407
  const results = [];
273
408
  const possible_app_dirs = [
@@ -682,6 +817,23 @@ export function run_validation() {
682
817
  env_results.forEach(print_result);
683
818
  all_results.push(...env_results);
684
819
  console.log();
820
+ const cookie_prefix_results = check_cookie_prefix(project_root);
821
+ if (cookie_prefix_results.length > 0) {
822
+ console.log("\x1b[1m🍪 Cookie Prefix\x1b[0m");
823
+ cookie_prefix_results.forEach(print_result);
824
+ all_results.push(...cookie_prefix_results);
825
+ console.log();
826
+ }
827
+ console.log("\x1b[1m🔑 OAuth Providers\x1b[0m");
828
+ const oauth_results = check_oauth(project_root);
829
+ oauth_results.forEach(print_result);
830
+ all_results.push(...oauth_results);
831
+ const redirect_lines = get_oauth_redirect_lines(project_root);
832
+ if (redirect_lines.length > 0) {
833
+ console.log("\n Register these redirect URIs in the provider console (must match exactly):");
834
+ redirect_lines.forEach(line => console.log(line));
835
+ }
836
+ console.log();
685
837
  console.log("\x1b[1m🗄️ Database\x1b[0m");
686
838
  const db_results = check_database(project_root);
687
839
  db_results.forEach(print_result);
package/dist/index.d.ts CHANGED
@@ -8,4 +8,7 @@ export { cn, merge_class_names } from "./lib/utils.js";
8
8
  export { HAZO_AUTH_PERMISSIONS, ALL_ADMIN_PERMISSIONS, GLOBAL_ADMIN_PERMISSION } from "./lib/constants.js";
9
9
  export { GOOGLE_NEXTAUTH_CALLBACK_PATH, GOOGLE_OAUTH_CALLBACK_PATH, FACEBOOK_NEXTAUTH_CALLBACK_PATH, FACEBOOK_OAUTH_CALLBACK_PATH, } from "./lib/oauth_routes.js";
10
10
  export { requestGoogleScopes } from "./lib/auth/request_google_scopes.js";
11
+ export { describeAuthError } from "./lib/auth/auth_error_help.js";
12
+ export type { AuthErrorHelp } from "./lib/auth/auth_error_help";
13
+ export { googleRedirectUri, facebookRedirectUri } from "./lib/oauth_routes.js";
11
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAG5C,cAAc,oBAAoB,CAAC;AAGnC,YAAY,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAG/B,YAAY,EAAE,QAAQ,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGxG,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AACxG,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAG5C,cAAc,oBAAoB,CAAC;AAGnC,YAAY,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EACb,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAG/B,YAAY,EAAE,QAAQ,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAGxG,OAAO,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AACxG,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGhE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -17,3 +17,7 @@ export { HAZO_AUTH_PERMISSIONS, ALL_ADMIN_PERMISSIONS, GLOBAL_ADMIN_PERMISSION }
17
17
  export { GOOGLE_NEXTAUTH_CALLBACK_PATH, GOOGLE_OAUTH_CALLBACK_PATH, FACEBOOK_NEXTAUTH_CALLBACK_PATH, FACEBOOK_OAUTH_CALLBACK_PATH, } from "./lib/oauth_routes.js";
18
18
  // section: google_oauth_exports
19
19
  export { requestGoogleScopes } from "./lib/auth/request_google_scopes.js";
20
+ // section: auth_error_help_exports (client-safe)
21
+ export { describeAuthError } from "./lib/auth/auth_error_help.js";
22
+ // section: oauth_route_helper_exports (client-safe — pure string builders)
23
+ export { googleRedirectUri, facebookRedirectUri } from "./lib/oauth_routes.js";
@@ -0,0 +1,14 @@
1
+ export type AuthErrorHelp = {
2
+ /** The raw error code from the `?error=` query param. */
3
+ code: string;
4
+ /** Short, user-facing summary. */
5
+ title: string;
6
+ /** Developer-facing detail and remediation. */
7
+ detail: string;
8
+ };
9
+ /**
10
+ * Resolves a NextAuth/hazo_auth error code to friendly guidance. Unknown codes
11
+ * get a generic fallback so callers can always render something useful.
12
+ */
13
+ export declare function describeAuthError(code: string | null | undefined): AuthErrorHelp;
14
+ //# sourceMappingURL=auth_error_help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth_error_help.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_error_help.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG;IAC1B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAyCF;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,CAWhF"}
@@ -0,0 +1,53 @@
1
+ // file_description: client-safe decoder mapping NextAuth/hazo_auth `?error=` codes
2
+ // to human-readable guidance. Use on a login/error page to replace opaque codes.
3
+ const KNOWN = {
4
+ Configuration: {
5
+ title: "Sign-in is misconfigured.",
6
+ detail: "The server's OAuth/NextAuth config is incomplete. Check NEXTAUTH_SECRET, NEXTAUTH_URL, and provider credentials. Run `npx hazo_auth validate`.",
7
+ },
8
+ AccessDenied: {
9
+ title: "Access denied.",
10
+ detail: "You don't have permission to sign in, or you cancelled the consent screen.",
11
+ },
12
+ Verification: {
13
+ title: "This sign-in link has expired.",
14
+ detail: "The link was already used or timed out. Request a new one.",
15
+ },
16
+ OAuthSignin: {
17
+ title: "Couldn't start sign-in.",
18
+ detail: "Error constructing the provider authorization URL. Check provider credentials.",
19
+ },
20
+ OAuthCallback: {
21
+ title: "Sign-in callback failed.",
22
+ detail: "The provider rejected the callback. Most often the registered redirect URI doesn't match. Confirm the provider console lists the exact URI hazo_auth prints at boot.",
23
+ },
24
+ OAuthAccountNotLinked: {
25
+ title: "This email is already linked to a different sign-in method.",
26
+ detail: "Sign in with the original method, then link the new provider from account settings.",
27
+ },
28
+ GoogleTokenStorageUnconfigured: {
29
+ title: "Signed in, but offline Google access is unavailable.",
30
+ detail: "Refresh-token storage needs HAZO_AUTH_OAUTH_KEY_CURRENT and HAZO_AUTH_OAUTH_KEY_<ID>. Login still works; set these only if you need offline Google API calls.",
31
+ },
32
+ redirect_uri_mismatch: {
33
+ title: "Sign-in blocked: redirect URI mismatch.",
34
+ detail: "The redirect URI sent to the provider isn't registered. Register the exact URI hazo_auth prints at boot (origin = NEXTAUTH_URL + /api/auth/callback/<provider>). Note: providers usually show this on their own page, before returning to the app.",
35
+ },
36
+ };
37
+ /**
38
+ * Resolves a NextAuth/hazo_auth error code to friendly guidance. Unknown codes
39
+ * get a generic fallback so callers can always render something useful.
40
+ */
41
+ export function describeAuthError(code) {
42
+ const key = (code !== null && code !== void 0 ? code : "").trim();
43
+ const hit = key ? KNOWN[key] : undefined;
44
+ if (hit)
45
+ return Object.assign({ code: key }, hit);
46
+ return {
47
+ code: key || "Unknown",
48
+ title: "Sign-in failed.",
49
+ detail: key
50
+ ? `Unrecognised error code "${key}". Check the server logs for details.`
51
+ : "No error code provided. Check the server logs for details.",
52
+ };
53
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"nextauth_config.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/nextauth_config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,WAAW,CAAC;AAetD,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,CA2RjD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAW7C"}
1
+ {"version":3,"file":"nextauth_config.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/nextauth_config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,WAAW,CAAC;AAgBtD,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAGF;;;;GAIG;AACH,wBAAgB,mBAAmB,IAAI,WAAW,CA+RjD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,OAAO,CAW7C"}
@@ -9,6 +9,7 @@ import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
9
9
  import { create_app_logger } from "../app_logger.js";
10
10
  import { store_google_oauth_token, GoogleTokenStorageUnconfigured } from "../services/google_token_service.js";
11
11
  import { GOOGLE_OAUTH_CALLBACK_PATH, FACEBOOK_OAUTH_CALLBACK_PATH } from "../oauth_routes.js";
12
+ import { logOAuthPreflight } from "../oauth_preflight.server.js";
12
13
  // section: config
13
14
  /**
14
15
  * Gets NextAuth.js configuration with enabled OAuth providers
@@ -16,6 +17,9 @@ import { GOOGLE_OAUTH_CALLBACK_PATH, FACEBOOK_OAUTH_CALLBACK_PATH } from "../oau
16
17
  * @returns NextAuth configuration object
17
18
  */
18
19
  export function get_nextauth_config() {
20
+ // Print enabled-provider redirect URIs + flag missing env once per process.
21
+ // No-op after the first call; safe to run on every config build.
22
+ logOAuthPreflight();
19
23
  const oauth_config = get_oauth_config();
20
24
  const providers = [];
21
25
  // Add Google provider if enabled
@@ -0,0 +1,35 @@
1
+ import "server-only";
2
+ export type PreflightStatus = "pass" | "warn" | "fail";
3
+ export type PreflightCheck = {
4
+ name: string;
5
+ status: PreflightStatus;
6
+ message: string;
7
+ };
8
+ export type OAuthPreflightReport = {
9
+ /** Origin used to derive redirect URIs (NEXTAUTH_URL, or a fallback). */
10
+ origin: string;
11
+ /** True when NEXTAUTH_URL was actually set (vs. a guessed fallback). */
12
+ originResolved: boolean;
13
+ /** Absolute redirect URIs to paste into the provider console, per enabled provider. */
14
+ redirectUris: {
15
+ provider: "google" | "facebook";
16
+ uri: string;
17
+ }[];
18
+ checks: PreflightCheck[];
19
+ };
20
+ /**
21
+ * Builds a structured OAuth config report without printing. Safe to call
22
+ * anywhere server-side (CLI, route, instrumentation). Pure config inspection —
23
+ * no DB or network access.
24
+ */
25
+ export declare function getOAuthPreflightReport(): OAuthPreflightReport;
26
+ /**
27
+ * Logs the OAuth preflight report once per process. Call from a Next.js
28
+ * `instrumentation.ts` `register()` hook to surface config problems — and the
29
+ * exact redirect URI to register — the moment the server boots, before anyone
30
+ * hits a broken sign-in.
31
+ *
32
+ * Idempotent: subsequent calls in the same process are no-ops.
33
+ */
34
+ export declare function logOAuthPreflight(): OAuthPreflightReport | null;
35
+ //# sourceMappingURL=oauth_preflight.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth_preflight.server.d.ts","sourceRoot":"","sources":["../../src/lib/oauth_preflight.server.ts"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAQrB,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,cAAc,EAAE,OAAO,CAAC;IACxB,uFAAuF;IACvF,YAAY,EAAE;QAAE,QAAQ,EAAE,QAAQ,GAAG,UAAU,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B,CAAC;AAeF;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,oBAAoB,CAmH9D;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,oBAAoB,GAAG,IAAI,CAwC/D"}
@@ -0,0 +1,191 @@
1
+ // file_description: server-only OAuth preflight — validates env and prints the exact
2
+ // redirect URIs to register, killing the recurring `redirect_uri_mismatch` for new apps.
3
+ import "server-only";
4
+ import { get_oauth_config } from "./oauth_config.server.js";
5
+ import { get_cookies_config } from "./cookies_config.server.js";
6
+ import { googleRedirectUri, facebookRedirectUri } from "./oauth_routes.js";
7
+ import { create_app_logger } from "./app_logger.js";
8
+ // section: helpers
9
+ const MIN_SECRET_LEN = 32;
10
+ function resolve_origin() {
11
+ var _a;
12
+ const nextauth_url = process.env.NEXTAUTH_URL;
13
+ if (nextauth_url && nextauth_url.trim().length > 0) {
14
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
15
+ }
16
+ // Fallback so we can still print a best-guess URI to register.
17
+ const port = (_a = process.env.PORT) !== null && _a !== void 0 ? _a : "3000";
18
+ return { origin: `http://localhost:${port}`, resolved: false };
19
+ }
20
+ /**
21
+ * Builds a structured OAuth config report without printing. Safe to call
22
+ * anywhere server-side (CLI, route, instrumentation). Pure config inspection —
23
+ * no DB or network access.
24
+ */
25
+ export function getOAuthPreflightReport() {
26
+ var _a, _b;
27
+ const checks = [];
28
+ const { origin, resolved } = resolve_origin();
29
+ const oauth = get_oauth_config();
30
+ const redirectUris = [];
31
+ // Cookie prefix: env (read by the edge proxy) must match the ini (read by the
32
+ // server). A mismatch — or an unset env var — makes the edge gate throw and
33
+ // silently bounce authenticated users to /login.
34
+ try {
35
+ const ini_prefix = ((_a = get_cookies_config().cookie_prefix) !== null && _a !== void 0 ? _a : "").trim();
36
+ const env_prefix = ((_b = process.env.HAZO_AUTH_COOKIE_PREFIX) !== null && _b !== void 0 ? _b : "").trim();
37
+ if (ini_prefix && !env_prefix) {
38
+ checks.push({
39
+ name: "Cookie prefix",
40
+ status: "fail",
41
+ message: `HAZO_AUTH_COOKIE_PREFIX env var not set. The edge proxy throws without it and bounces authenticated users to /login. Set HAZO_AUTH_COOKIE_PREFIX=${ini_prefix} (matching cookie_prefix in hazo_auth_config.ini).`,
42
+ });
43
+ }
44
+ else if (ini_prefix && env_prefix && env_prefix !== ini_prefix) {
45
+ checks.push({
46
+ name: "Cookie prefix",
47
+ status: "fail",
48
+ message: `Mismatch: HAZO_AUTH_COOKIE_PREFIX="${env_prefix}" vs ini cookie_prefix="${ini_prefix}". They must be identical or the edge gate can't find the session cookie.`,
49
+ });
50
+ }
51
+ }
52
+ catch (_c) {
53
+ // get_cookies_config throws when cookie_prefix is absent from the ini; that
54
+ // missing-config case is reported elsewhere (validate CLI / server boot).
55
+ }
56
+ // NEXTAUTH_URL — the source of the redirect URI.
57
+ if (resolved) {
58
+ checks.push({ name: "NEXTAUTH_URL", status: "pass", message: origin });
59
+ }
60
+ else {
61
+ checks.push({
62
+ name: "NEXTAUTH_URL",
63
+ status: "fail",
64
+ message: `Not set. NextAuth derives the OAuth redirect URI from it. Set NEXTAUTH_URL=${origin} (matching the port you serve on).`,
65
+ });
66
+ }
67
+ // NEXTAUTH_URL port vs. the port the app is told to listen on.
68
+ const declared_port = process.env.PORT;
69
+ if (resolved && declared_port) {
70
+ let url_port = null;
71
+ try {
72
+ const u = new URL(origin);
73
+ url_port = u.port || (u.protocol === "https:" ? "443" : "80");
74
+ }
75
+ catch (_d) {
76
+ url_port = null;
77
+ }
78
+ if (url_port && url_port !== declared_port) {
79
+ checks.push({
80
+ name: "Port match",
81
+ status: "warn",
82
+ message: `NEXTAUTH_URL port (${url_port}) differs from PORT (${declared_port}). The redirect URI uses ${url_port}; if you serve on ${declared_port}, OAuth will fail with redirect_uri_mismatch.`,
83
+ });
84
+ }
85
+ }
86
+ // NEXTAUTH_SECRET — required for OAuth session/state signing.
87
+ const secret = process.env.NEXTAUTH_SECRET;
88
+ if (!secret || secret.length === 0) {
89
+ checks.push({
90
+ name: "NEXTAUTH_SECRET",
91
+ status: "fail",
92
+ message: "Not set. Generate one: openssl rand -base64 32",
93
+ });
94
+ }
95
+ else if (secret.length < MIN_SECRET_LEN) {
96
+ checks.push({
97
+ name: "NEXTAUTH_SECRET",
98
+ status: "warn",
99
+ message: `Only ${secret.length} chars; use at least ${MIN_SECRET_LEN}. Generate: openssl rand -base64 32`,
100
+ });
101
+ }
102
+ else {
103
+ checks.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
104
+ }
105
+ // Google provider.
106
+ if (oauth.enable_google) {
107
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
108
+ const secretG = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
109
+ if (!id || !secretG) {
110
+ const missing = [
111
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
112
+ !secretG ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
113
+ ].filter(Boolean);
114
+ checks.push({
115
+ name: "Google OAuth credentials",
116
+ status: "fail",
117
+ message: `enable_google=true but missing: ${missing.join(", ")} (from Google Cloud Console).`,
118
+ });
119
+ }
120
+ else {
121
+ checks.push({ name: "Google OAuth credentials", status: "pass", message: "Set" });
122
+ }
123
+ redirectUris.push({ provider: "google", uri: googleRedirectUri(origin) });
124
+ }
125
+ // Facebook provider.
126
+ if (oauth.enable_facebook_oauth) {
127
+ const appId = oauth.facebook_app_id || process.env.HAZO_AUTH_FACEBOOK_APP_ID;
128
+ const appSecret = oauth.facebook_app_secret || process.env.HAZO_AUTH_FACEBOOK_APP_SECRET;
129
+ if (!appId || !appSecret) {
130
+ checks.push({
131
+ name: "Facebook OAuth credentials",
132
+ status: "fail",
133
+ message: "enable_facebook_oauth=true but app_id/app_secret missing.",
134
+ });
135
+ }
136
+ else {
137
+ checks.push({ name: "Facebook OAuth credentials", status: "pass", message: "Set" });
138
+ }
139
+ redirectUris.push({ provider: "facebook", uri: facebookRedirectUri(origin) });
140
+ }
141
+ return { origin, originResolved: resolved, redirectUris, checks };
142
+ }
143
+ // section: boot logger
144
+ let _logged_once = false;
145
+ /**
146
+ * Logs the OAuth preflight report once per process. Call from a Next.js
147
+ * `instrumentation.ts` `register()` hook to surface config problems — and the
148
+ * exact redirect URI to register — the moment the server boots, before anyone
149
+ * hits a broken sign-in.
150
+ *
151
+ * Idempotent: subsequent calls in the same process are no-ops.
152
+ */
153
+ export function logOAuthPreflight() {
154
+ if (_logged_once)
155
+ return null;
156
+ _logged_once = true;
157
+ const report = getOAuthPreflightReport();
158
+ const logger = create_app_logger();
159
+ // Always surface failing/warning checks (e.g. cookie-prefix mismatch), even
160
+ // when no OAuth provider is enabled and there are no redirect URIs to print.
161
+ for (const check of report.checks) {
162
+ if (check.status === "fail") {
163
+ logger.error("oauth_preflight_check_failed", { check: check.name, detail: check.message });
164
+ }
165
+ else if (check.status === "warn") {
166
+ logger.warn("oauth_preflight_check_warning", { check: check.name, detail: check.message });
167
+ }
168
+ }
169
+ if (report.redirectUris.length === 0)
170
+ return report; // No OAuth providers — nothing to register.
171
+ // Highlighted, copy-pasteable block — the single thing that prevents
172
+ // redirect_uri_mismatch. console (not logger) so it renders verbatim.
173
+ const lines = [
174
+ "",
175
+ "──────────────────────────────────────────────────────────────",
176
+ " hazo_auth · OAuth redirect URIs to register in the provider console",
177
+ "──────────────────────────────────────────────────────────────",
178
+ ];
179
+ for (const { provider, uri } of report.redirectUris) {
180
+ lines.push(` ${provider.padEnd(9)} → ${uri}`);
181
+ }
182
+ if (!report.originResolved) {
183
+ lines.push("");
184
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT.");
185
+ lines.push(" Set NEXTAUTH_URL to your real origin and re-register.");
186
+ }
187
+ lines.push("──────────────────────────────────────────────────────────────");
188
+ lines.push("");
189
+ console.log(lines.join("\n"));
190
+ return report;
191
+ }
@@ -6,4 +6,14 @@ export declare const GOOGLE_OAUTH_CALLBACK_PATH: "/api/hazo_auth/oauth/google/ca
6
6
  export declare const FACEBOOK_NEXTAUTH_CALLBACK_PATH: "/api/auth/callback/facebook";
7
7
  /** hazo_auth's custom post-auth handler — creates the hazo_auth session after Facebook sign-in. */
8
8
  export declare const FACEBOOK_OAUTH_CALLBACK_PATH: "/api/hazo_auth/oauth/facebook/callback";
9
+ /**
10
+ * Builds the absolute Google redirect URI to register in Google Cloud Console.
11
+ * This is exactly the value NextAuth sends as `redirect_uri`; registering
12
+ * anything else (wrong port, https vs http, trailing slash) causes the
13
+ * `redirect_uri_mismatch` 400. Derive it from your origin (= NEXTAUTH_URL) so
14
+ * the two can never drift.
15
+ */
16
+ export declare function googleRedirectUri(origin: string): string;
17
+ /** Builds the absolute Facebook redirect URI to register in the Facebook Developer Console. */
18
+ export declare function facebookRedirectUri(origin: string): string;
9
19
  //# sourceMappingURL=oauth_routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauth_routes.d.ts","sourceRoot":"","sources":["../../src/lib/oauth_routes.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,eAAO,MAAM,6BAA6B,EAAG,2BAAoC,CAAC;AAElF,iGAAiG;AACjG,eAAO,MAAM,0BAA0B,EAAG,sCAA+C,CAAC;AAE1F,4FAA4F;AAC5F,eAAO,MAAM,+BAA+B,EAAG,6BAAsC,CAAC;AAEtF,mGAAmG;AACnG,eAAO,MAAM,4BAA4B,EAAG,wCAAiD,CAAC"}
1
+ {"version":3,"file":"oauth_routes.d.ts","sourceRoot":"","sources":["../../src/lib/oauth_routes.ts"],"names":[],"mappings":"AAEA,sFAAsF;AACtF,eAAO,MAAM,6BAA6B,EAAG,2BAAoC,CAAC;AAElF,iGAAiG;AACjG,eAAO,MAAM,0BAA0B,EAAG,sCAA+C,CAAC;AAE1F,4FAA4F;AAC5F,eAAO,MAAM,+BAA+B,EAAG,6BAAsC,CAAC;AAEtF,mGAAmG;AACnG,eAAO,MAAM,4BAA4B,EAAG,wCAAiD,CAAC;AAE9F;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D"}
@@ -7,3 +7,21 @@ export const GOOGLE_OAUTH_CALLBACK_PATH = "/api/hazo_auth/oauth/google/callback"
7
7
  export const FACEBOOK_NEXTAUTH_CALLBACK_PATH = "/api/auth/callback/facebook";
8
8
  /** hazo_auth's custom post-auth handler — creates the hazo_auth session after Facebook sign-in. */
9
9
  export const FACEBOOK_OAUTH_CALLBACK_PATH = "/api/hazo_auth/oauth/facebook/callback";
10
+ /**
11
+ * Builds the absolute Google redirect URI to register in Google Cloud Console.
12
+ * This is exactly the value NextAuth sends as `redirect_uri`; registering
13
+ * anything else (wrong port, https vs http, trailing slash) causes the
14
+ * `redirect_uri_mismatch` 400. Derive it from your origin (= NEXTAUTH_URL) so
15
+ * the two can never drift.
16
+ */
17
+ export function googleRedirectUri(origin) {
18
+ return joinOrigin(origin, GOOGLE_NEXTAUTH_CALLBACK_PATH);
19
+ }
20
+ /** Builds the absolute Facebook redirect URI to register in the Facebook Developer Console. */
21
+ export function facebookRedirectUri(origin) {
22
+ return joinOrigin(origin, FACEBOOK_NEXTAUTH_CALLBACK_PATH);
23
+ }
24
+ function joinOrigin(origin, pathname) {
25
+ // Tolerate a trailing slash on the origin; never emit a double slash.
26
+ return `${origin.replace(/\/+$/, "")}${pathname}`;
27
+ }
@@ -38,4 +38,6 @@ export { wireAdminIssueCapture } from "./admin-issues/wire.server.js";
38
38
  export type { WireAdminIssueCaptureOptions } from "./admin-issues/wire.server";
39
39
  export { denyPermission } from "./lib/auth/deny_permission.server.js";
40
40
  export type { DenyPermissionInput } from "./lib/auth/deny_permission.server";
41
+ export { logOAuthPreflight, getOAuthPreflightReport } from "./lib/oauth_preflight.server.js";
42
+ export type { OAuthPreflightReport, PreflightCheck, PreflightStatus, } from "./lib/oauth_preflight.server";
41
43
  //# sourceMappingURL=server-lib.d.ts.map
@@ -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,4BAA4B,EAAE,MAAM,2CAA2C,CAAC;AACzF,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,oCAAoC,CAAC;AAG5C,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;AAG9C,OAAO,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AACvF,YAAY,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAGpF,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,YAAY,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,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;AAGvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,4BAA4B,EAAE,MAAM,2CAA2C,CAAC;AACzF,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,oCAAoC,CAAC;AAG5C,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;AAG9C,OAAO,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AACvF,YAAY,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAGpF,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,YAAY,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAG/E,OAAO,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACnE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAK7E,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAC1F,YAAY,EACV,oBAAoB,EACpB,cAAc,EACd,eAAe,GAChB,MAAM,8BAA8B,CAAC"}
@@ -53,3 +53,7 @@ export { authIssueTypeDef, registerAuthIssuePlugin } from "./admin-issues/plugin
53
53
  export { wireAdminIssueCapture } from "./admin-issues/wire.server.js";
54
54
  // deny_permission helper
55
55
  export { denyPermission } from "./lib/auth/deny_permission.server.js";
56
+ // section: oauth_preflight_exports
57
+ // Call logOAuthPreflight() from a Next.js instrumentation.ts register() hook to
58
+ // print the exact redirect URIs to register and flag missing OAuth env at boot.
59
+ export { logOAuthPreflight, getOAuthPreflightReport } from "./lib/oauth_preflight.server.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "10.4.9",
3
+ "version": "10.4.11",
4
4
  "description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
5
5
  "keywords": [
6
6
  "authentication",