hazo_auth 10.4.8 → 10.4.10

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,113 @@ 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
+ function read_oauth_origin(): { origin: string; resolved: boolean } {
314
+ const nextauth_url = process.env.NEXTAUTH_URL;
315
+ if (nextauth_url && nextauth_url.trim().length > 0) {
316
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
317
+ }
318
+ const port = process.env.PORT ?? "3000";
319
+ return { origin: `http://localhost:${port}`, resolved: false };
320
+ }
321
+
322
+ function check_oauth(project_root: string): CheckResult[] {
323
+ const results: CheckResult[] = [];
324
+
325
+ const hazo_config_path = path.join(project_root, "config", "hazo_auth_config.ini");
326
+ const hazo_config = read_ini_file(hazo_config_path);
327
+ const oauth_section = hazo_config?.["hazo_auth__oauth"] ?? {};
328
+ const enable_google = (oauth_section["enable_google"] ?? "").toLowerCase() === "true";
329
+ const enable_facebook = (oauth_section["enable_facebook_oauth"] ?? "").toLowerCase() === "true";
330
+
331
+ if (!enable_google && !enable_facebook) {
332
+ results.push({
333
+ name: "OAuth providers",
334
+ status: "pass",
335
+ message: "No OAuth providers enabled (email/password only)",
336
+ });
337
+ return results;
338
+ }
339
+
340
+ const { origin, resolved } = read_oauth_origin();
341
+
342
+ // NEXTAUTH_URL — source of the redirect URI.
343
+ results.push(
344
+ resolved
345
+ ? { name: "NEXTAUTH_URL", status: "pass", message: origin }
346
+ : {
347
+ name: "NEXTAUTH_URL",
348
+ status: "fail",
349
+ message: `Not set. The redirect URI below is a guess from PORT. Set NEXTAUTH_URL=${origin}`,
350
+ }
351
+ );
352
+
353
+ // NEXTAUTH_SECRET — required for OAuth state/session signing.
354
+ const secret = process.env.NEXTAUTH_SECRET;
355
+ if (!secret || secret.length === 0) {
356
+ results.push({
357
+ name: "NEXTAUTH_SECRET",
358
+ status: "fail",
359
+ message: "Not set. Generate: openssl rand -base64 32",
360
+ });
361
+ } else if (secret.length < 32) {
362
+ results.push({
363
+ name: "NEXTAUTH_SECRET",
364
+ status: "warn",
365
+ message: `Only ${secret.length} chars; use at least 32. Generate: openssl rand -base64 32`,
366
+ });
367
+ } else {
368
+ results.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
369
+ }
370
+
371
+ if (enable_google) {
372
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
373
+ const cs = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
374
+ const missing = [
375
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
376
+ !cs ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
377
+ ].filter(Boolean);
378
+ results.push(
379
+ missing.length === 0
380
+ ? { name: "Google OAuth credentials", status: "pass", message: "Set" }
381
+ : {
382
+ name: "Google OAuth credentials",
383
+ status: "fail",
384
+ message: `enable_google=true but missing: ${missing.join(", ")}`,
385
+ }
386
+ );
387
+ }
388
+
389
+ return results;
390
+ }
391
+
392
+ /**
393
+ * Returns the exact redirect URIs to register, per enabled provider. Printed as
394
+ * a copy-pasteable box so the dev never guesses the port/path — the single thing
395
+ * that prevents redirect_uri_mismatch.
396
+ */
397
+ function get_oauth_redirect_lines(project_root: string): string[] {
398
+ const hazo_config = read_ini_file(path.join(project_root, "config", "hazo_auth_config.ini"));
399
+ const oauth_section = hazo_config?.["hazo_auth__oauth"] ?? {};
400
+ const enable_google = (oauth_section["enable_google"] ?? "").toLowerCase() === "true";
401
+ const enable_facebook = (oauth_section["enable_facebook_oauth"] ?? "").toLowerCase() === "true";
402
+ if (!enable_google && !enable_facebook) return [];
403
+
404
+ const { origin, resolved } = read_oauth_origin();
405
+ const lines: string[] = [];
406
+ if (enable_google) lines.push(` google → ${origin}${GOOGLE_NEXTAUTH_CALLBACK_PATH}`);
407
+ if (enable_facebook) lines.push(` facebook → ${origin}${FACEBOOK_NEXTAUTH_CALLBACK_PATH}`);
408
+ if (!resolved) {
409
+ lines.push("");
410
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT. Set it and re-register.");
411
+ }
412
+ return lines;
413
+ }
414
+
308
415
  function check_api_routes(project_root: string): CheckResult[] {
309
416
  const results: CheckResult[] = [];
310
417
 
@@ -743,6 +850,17 @@ export function run_validation(): ValidationSummary {
743
850
  all_results.push(...env_results);
744
851
  console.log();
745
852
 
853
+ console.log("\x1b[1m🔑 OAuth Providers\x1b[0m");
854
+ const oauth_results = check_oauth(project_root);
855
+ oauth_results.forEach(print_result);
856
+ all_results.push(...oauth_results);
857
+ const redirect_lines = get_oauth_redirect_lines(project_root);
858
+ if (redirect_lines.length > 0) {
859
+ console.log("\n Register these redirect URIs in the provider console (must match exactly):");
860
+ redirect_lines.forEach(line => console.log(line));
861
+ }
862
+ console.log();
863
+
746
864
  console.log("\x1b[1m🗄️ Database\x1b[0m");
747
865
  const db_results = check_database(project_root);
748
866
  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
 
@@ -175,9 +180,22 @@ export function get_nextauth_config(): AuthOptions {
175
180
  // Store user_id in account for the JWT callback to pick up
176
181
  (account as Record<string, unknown>).hazo_user_id = result.user_id;
177
182
 
178
- // Capture refresh token when extra scopes were granted
183
+ // Capture refresh token when extra scopes were granted.
184
+ // Google echoes the base scopes back in full-URL form
185
+ // (https://www.googleapis.com/auth/userinfo.email|profile), so we
186
+ // normalise to short names before comparing — otherwise every plain
187
+ // login looks like it requested extra scopes and triggers offline
188
+ // token storage that should never run.
179
189
  const BASE_SCOPES = ["openid", "email", "profile"];
180
- const granted_scopes = (account.scope ?? "").split(" ").filter(Boolean);
190
+ const normalise_scope = (s: string): string => {
191
+ if (s === "https://www.googleapis.com/auth/userinfo.email") return "email";
192
+ if (s === "https://www.googleapis.com/auth/userinfo.profile") return "profile";
193
+ return s;
194
+ };
195
+ const granted_scopes = (account.scope ?? "")
196
+ .split(" ")
197
+ .filter(Boolean)
198
+ .map(normalise_scope);
181
199
  const has_extra_scopes = granted_scopes.some(s => !BASE_SCOPES.includes(s));
182
200
 
183
201
  if (account.refresh_token && has_extra_scopes) {
@@ -197,17 +215,23 @@ export function get_nextauth_config(): AuthOptions {
197
215
  });
198
216
  } catch (tokenError) {
199
217
  if (tokenError instanceof GoogleTokenStorageUnconfigured) {
200
- logger.error("nextauth_google_token_storage_unconfigured", {
218
+ // Non-fatal: offline Google API access (refresh-token storage)
219
+ // requires HAZO_AUTH_OAUTH_KEY_* encryption keys. Login itself
220
+ // does not — the user already authenticated. Warn so the admin
221
+ // can configure keys if they want offline access, but let the
222
+ // sign-in complete instead of dead-ending on an error page.
223
+ logger.warn("nextauth_google_token_storage_unconfigured", {
201
224
  user_id: result.user_id,
202
225
  error: tokenError.message,
226
+ impact: "Login succeeded; offline Google API access disabled until HAZO_AUTH_OAUTH_KEY_CURRENT and HAZO_AUTH_OAUTH_KEY_<ID> are set.",
227
+ });
228
+ } else {
229
+ const errorMsg = tokenError instanceof Error ? tokenError.message : String(tokenError);
230
+ logger.error("nextauth_google_token_store_failed", {
231
+ user_id: result.user_id,
232
+ error: errorMsg,
203
233
  });
204
- return "/api/auth/error?error=GoogleTokenStorageUnconfigured";
205
234
  }
206
- const errorMsg = tokenError instanceof Error ? tokenError.message : String(tokenError);
207
- logger.error("nextauth_google_token_store_failed", {
208
- user_id: result.user_id,
209
- error: errorMsg,
210
- });
211
235
  // Non-fatal: continue sign-in even if token storage fails
212
236
  }
213
237
  }
@@ -0,0 +1,188 @@
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 { googleRedirectUri, facebookRedirectUri } from "./oauth_routes.js";
7
+ import { create_app_logger } from "./app_logger.js";
8
+
9
+ // section: types
10
+ export type PreflightStatus = "pass" | "warn" | "fail";
11
+
12
+ export type PreflightCheck = {
13
+ name: string;
14
+ status: PreflightStatus;
15
+ message: string;
16
+ };
17
+
18
+ export type OAuthPreflightReport = {
19
+ /** Origin used to derive redirect URIs (NEXTAUTH_URL, or a fallback). */
20
+ origin: string;
21
+ /** True when NEXTAUTH_URL was actually set (vs. a guessed fallback). */
22
+ originResolved: boolean;
23
+ /** Absolute redirect URIs to paste into the provider console, per enabled provider. */
24
+ redirectUris: { provider: "google" | "facebook"; uri: string }[];
25
+ checks: PreflightCheck[];
26
+ };
27
+
28
+ // section: helpers
29
+ const MIN_SECRET_LEN = 32;
30
+
31
+ function resolve_origin(): { origin: string; resolved: boolean } {
32
+ const nextauth_url = process.env.NEXTAUTH_URL;
33
+ if (nextauth_url && nextauth_url.trim().length > 0) {
34
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
35
+ }
36
+ // Fallback so we can still print a best-guess URI to register.
37
+ const port = process.env.PORT ?? "3000";
38
+ return { origin: `http://localhost:${port}`, resolved: false };
39
+ }
40
+
41
+ /**
42
+ * Builds a structured OAuth config report without printing. Safe to call
43
+ * anywhere server-side (CLI, route, instrumentation). Pure config inspection —
44
+ * no DB or network access.
45
+ */
46
+ export function getOAuthPreflightReport(): OAuthPreflightReport {
47
+ const checks: PreflightCheck[] = [];
48
+ const { origin, resolved } = resolve_origin();
49
+ const oauth = get_oauth_config();
50
+ const redirectUris: OAuthPreflightReport["redirectUris"] = [];
51
+
52
+ // NEXTAUTH_URL — the source of the redirect URI.
53
+ if (resolved) {
54
+ checks.push({ name: "NEXTAUTH_URL", status: "pass", message: origin });
55
+ } else {
56
+ checks.push({
57
+ name: "NEXTAUTH_URL",
58
+ status: "fail",
59
+ message: `Not set. NextAuth derives the OAuth redirect URI from it. Set NEXTAUTH_URL=${origin} (matching the port you serve on).`,
60
+ });
61
+ }
62
+
63
+ // NEXTAUTH_URL port vs. the port the app is told to listen on.
64
+ const declared_port = process.env.PORT;
65
+ if (resolved && declared_port) {
66
+ let url_port: string | null = null;
67
+ try {
68
+ const u = new URL(origin);
69
+ url_port = u.port || (u.protocol === "https:" ? "443" : "80");
70
+ } catch {
71
+ url_port = null;
72
+ }
73
+ if (url_port && url_port !== declared_port) {
74
+ checks.push({
75
+ name: "Port match",
76
+ status: "warn",
77
+ 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.`,
78
+ });
79
+ }
80
+ }
81
+
82
+ // NEXTAUTH_SECRET — required for OAuth session/state signing.
83
+ const secret = process.env.NEXTAUTH_SECRET;
84
+ if (!secret || secret.length === 0) {
85
+ checks.push({
86
+ name: "NEXTAUTH_SECRET",
87
+ status: "fail",
88
+ message: "Not set. Generate one: openssl rand -base64 32",
89
+ });
90
+ } else if (secret.length < MIN_SECRET_LEN) {
91
+ checks.push({
92
+ name: "NEXTAUTH_SECRET",
93
+ status: "warn",
94
+ message: `Only ${secret.length} chars; use at least ${MIN_SECRET_LEN}. Generate: openssl rand -base64 32`,
95
+ });
96
+ } else {
97
+ checks.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
98
+ }
99
+
100
+ // Google provider.
101
+ if (oauth.enable_google) {
102
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
103
+ const secretG = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
104
+ if (!id || !secretG) {
105
+ const missing = [
106
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
107
+ !secretG ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
108
+ ].filter(Boolean);
109
+ checks.push({
110
+ name: "Google OAuth credentials",
111
+ status: "fail",
112
+ message: `enable_google=true but missing: ${missing.join(", ")} (from Google Cloud Console).`,
113
+ });
114
+ } else {
115
+ checks.push({ name: "Google OAuth credentials", status: "pass", message: "Set" });
116
+ }
117
+ redirectUris.push({ provider: "google", uri: googleRedirectUri(origin) });
118
+ }
119
+
120
+ // Facebook provider.
121
+ if (oauth.enable_facebook_oauth) {
122
+ const appId = oauth.facebook_app_id || process.env.HAZO_AUTH_FACEBOOK_APP_ID;
123
+ const appSecret = oauth.facebook_app_secret || process.env.HAZO_AUTH_FACEBOOK_APP_SECRET;
124
+ if (!appId || !appSecret) {
125
+ checks.push({
126
+ name: "Facebook OAuth credentials",
127
+ status: "fail",
128
+ message: "enable_facebook_oauth=true but app_id/app_secret missing.",
129
+ });
130
+ } else {
131
+ checks.push({ name: "Facebook OAuth credentials", status: "pass", message: "Set" });
132
+ }
133
+ redirectUris.push({ provider: "facebook", uri: facebookRedirectUri(origin) });
134
+ }
135
+
136
+ return { origin, originResolved: resolved, redirectUris, checks };
137
+ }
138
+
139
+ // section: boot logger
140
+ let _logged_once = false;
141
+
142
+ /**
143
+ * Logs the OAuth preflight report once per process. Call from a Next.js
144
+ * `instrumentation.ts` `register()` hook to surface config problems — and the
145
+ * exact redirect URI to register — the moment the server boots, before anyone
146
+ * hits a broken sign-in.
147
+ *
148
+ * Idempotent: subsequent calls in the same process are no-ops.
149
+ */
150
+ export function logOAuthPreflight(): OAuthPreflightReport | null {
151
+ if (_logged_once) return null;
152
+ _logged_once = true;
153
+
154
+ const report = getOAuthPreflightReport();
155
+ if (report.redirectUris.length === 0) return report; // No OAuth providers enabled.
156
+
157
+ const logger = create_app_logger();
158
+
159
+ for (const check of report.checks) {
160
+ if (check.status === "fail") {
161
+ logger.error("oauth_preflight_check_failed", { check: check.name, detail: check.message });
162
+ } else if (check.status === "warn") {
163
+ logger.warn("oauth_preflight_check_warning", { check: check.name, detail: check.message });
164
+ }
165
+ }
166
+
167
+ // Highlighted, copy-pasteable block — the single thing that prevents
168
+ // redirect_uri_mismatch. console (not logger) so it renders verbatim.
169
+ const lines: string[] = [
170
+ "",
171
+ "──────────────────────────────────────────────────────────────",
172
+ " hazo_auth · OAuth redirect URIs to register in the provider console",
173
+ "──────────────────────────────────────────────────────────────",
174
+ ];
175
+ for (const { provider, uri } of report.redirectUris) {
176
+ lines.push(` ${provider.padEnd(9)} → ${uri}`);
177
+ }
178
+ if (!report.originResolved) {
179
+ lines.push("");
180
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT.");
181
+ lines.push(" Set NEXTAUTH_URL to your real origin and re-register.");
182
+ }
183
+ lines.push("──────────────────────────────────────────────────────────────");
184
+ lines.push("");
185
+ console.log(lines.join("\n"));
186
+
187
+ return report;
188
+ }
@@ -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;AAsyBF,wBAAgB,cAAc,IAAI,iBAAiB,CAsFlD"}
@@ -268,6 +268,105 @@ 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
+ function read_oauth_origin() {
276
+ var _a;
277
+ const nextauth_url = process.env.NEXTAUTH_URL;
278
+ if (nextauth_url && nextauth_url.trim().length > 0) {
279
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
280
+ }
281
+ const port = (_a = process.env.PORT) !== null && _a !== void 0 ? _a : "3000";
282
+ return { origin: `http://localhost:${port}`, resolved: false };
283
+ }
284
+ function check_oauth(project_root) {
285
+ var _a, _b, _c;
286
+ const results = [];
287
+ const hazo_config_path = path.join(project_root, "config", "hazo_auth_config.ini");
288
+ const hazo_config = read_ini_file(hazo_config_path);
289
+ const oauth_section = (_a = hazo_config === null || hazo_config === void 0 ? void 0 : hazo_config["hazo_auth__oauth"]) !== null && _a !== void 0 ? _a : {};
290
+ const enable_google = ((_b = oauth_section["enable_google"]) !== null && _b !== void 0 ? _b : "").toLowerCase() === "true";
291
+ const enable_facebook = ((_c = oauth_section["enable_facebook_oauth"]) !== null && _c !== void 0 ? _c : "").toLowerCase() === "true";
292
+ if (!enable_google && !enable_facebook) {
293
+ results.push({
294
+ name: "OAuth providers",
295
+ status: "pass",
296
+ message: "No OAuth providers enabled (email/password only)",
297
+ });
298
+ return results;
299
+ }
300
+ const { origin, resolved } = read_oauth_origin();
301
+ // NEXTAUTH_URL — source of the redirect URI.
302
+ results.push(resolved
303
+ ? { name: "NEXTAUTH_URL", status: "pass", message: origin }
304
+ : {
305
+ name: "NEXTAUTH_URL",
306
+ status: "fail",
307
+ message: `Not set. The redirect URI below is a guess from PORT. Set NEXTAUTH_URL=${origin}`,
308
+ });
309
+ // NEXTAUTH_SECRET — required for OAuth state/session signing.
310
+ const secret = process.env.NEXTAUTH_SECRET;
311
+ if (!secret || secret.length === 0) {
312
+ results.push({
313
+ name: "NEXTAUTH_SECRET",
314
+ status: "fail",
315
+ message: "Not set. Generate: openssl rand -base64 32",
316
+ });
317
+ }
318
+ else if (secret.length < 32) {
319
+ results.push({
320
+ name: "NEXTAUTH_SECRET",
321
+ status: "warn",
322
+ message: `Only ${secret.length} chars; use at least 32. Generate: openssl rand -base64 32`,
323
+ });
324
+ }
325
+ else {
326
+ results.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
327
+ }
328
+ if (enable_google) {
329
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
330
+ const cs = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
331
+ const missing = [
332
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
333
+ !cs ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
334
+ ].filter(Boolean);
335
+ results.push(missing.length === 0
336
+ ? { name: "Google OAuth credentials", status: "pass", message: "Set" }
337
+ : {
338
+ name: "Google OAuth credentials",
339
+ status: "fail",
340
+ message: `enable_google=true but missing: ${missing.join(", ")}`,
341
+ });
342
+ }
343
+ return results;
344
+ }
345
+ /**
346
+ * Returns the exact redirect URIs to register, per enabled provider. Printed as
347
+ * a copy-pasteable box so the dev never guesses the port/path — the single thing
348
+ * that prevents redirect_uri_mismatch.
349
+ */
350
+ function get_oauth_redirect_lines(project_root) {
351
+ var _a, _b, _c;
352
+ const hazo_config = read_ini_file(path.join(project_root, "config", "hazo_auth_config.ini"));
353
+ const oauth_section = (_a = hazo_config === null || hazo_config === void 0 ? void 0 : hazo_config["hazo_auth__oauth"]) !== null && _a !== void 0 ? _a : {};
354
+ const enable_google = ((_b = oauth_section["enable_google"]) !== null && _b !== void 0 ? _b : "").toLowerCase() === "true";
355
+ const enable_facebook = ((_c = oauth_section["enable_facebook_oauth"]) !== null && _c !== void 0 ? _c : "").toLowerCase() === "true";
356
+ if (!enable_google && !enable_facebook)
357
+ return [];
358
+ const { origin, resolved } = read_oauth_origin();
359
+ const lines = [];
360
+ if (enable_google)
361
+ lines.push(` google → ${origin}${GOOGLE_NEXTAUTH_CALLBACK_PATH}`);
362
+ if (enable_facebook)
363
+ lines.push(` facebook → ${origin}${FACEBOOK_NEXTAUTH_CALLBACK_PATH}`);
364
+ if (!resolved) {
365
+ lines.push("");
366
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT. Set it and re-register.");
367
+ }
368
+ return lines;
369
+ }
271
370
  function check_api_routes(project_root) {
272
371
  const results = [];
273
372
  const possible_app_dirs = [
@@ -682,6 +781,16 @@ export function run_validation() {
682
781
  env_results.forEach(print_result);
683
782
  all_results.push(...env_results);
684
783
  console.log();
784
+ console.log("\x1b[1m🔑 OAuth Providers\x1b[0m");
785
+ const oauth_results = check_oauth(project_root);
786
+ oauth_results.forEach(print_result);
787
+ all_results.push(...oauth_results);
788
+ const redirect_lines = get_oauth_redirect_lines(project_root);
789
+ if (redirect_lines.length > 0) {
790
+ console.log("\n Register these redirect URIs in the provider console (must match exactly):");
791
+ redirect_lines.forEach(line => console.log(line));
792
+ }
793
+ console.log();
685
794
  console.log("\x1b[1m🗄️ Database\x1b[0m");
686
795
  const db_results = check_database(project_root);
687
796
  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,CAwQjD;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
@@ -116,9 +120,24 @@ export function get_nextauth_config() {
116
120
  });
117
121
  // Store user_id in account for the JWT callback to pick up
118
122
  account.hazo_user_id = result.user_id;
119
- // Capture refresh token when extra scopes were granted
123
+ // Capture refresh token when extra scopes were granted.
124
+ // Google echoes the base scopes back in full-URL form
125
+ // (https://www.googleapis.com/auth/userinfo.email|profile), so we
126
+ // normalise to short names before comparing — otherwise every plain
127
+ // login looks like it requested extra scopes and triggers offline
128
+ // token storage that should never run.
120
129
  const BASE_SCOPES = ["openid", "email", "profile"];
121
- const granted_scopes = ((_b = account.scope) !== null && _b !== void 0 ? _b : "").split(" ").filter(Boolean);
130
+ const normalise_scope = (s) => {
131
+ if (s === "https://www.googleapis.com/auth/userinfo.email")
132
+ return "email";
133
+ if (s === "https://www.googleapis.com/auth/userinfo.profile")
134
+ return "profile";
135
+ return s;
136
+ };
137
+ const granted_scopes = ((_b = account.scope) !== null && _b !== void 0 ? _b : "")
138
+ .split(" ")
139
+ .filter(Boolean)
140
+ .map(normalise_scope);
122
141
  const has_extra_scopes = granted_scopes.some(s => !BASE_SCOPES.includes(s));
123
142
  if (account.refresh_token && has_extra_scopes) {
124
143
  try {
@@ -138,17 +157,24 @@ export function get_nextauth_config() {
138
157
  }
139
158
  catch (tokenError) {
140
159
  if (tokenError instanceof GoogleTokenStorageUnconfigured) {
141
- logger.error("nextauth_google_token_storage_unconfigured", {
160
+ // Non-fatal: offline Google API access (refresh-token storage)
161
+ // requires HAZO_AUTH_OAUTH_KEY_* encryption keys. Login itself
162
+ // does not — the user already authenticated. Warn so the admin
163
+ // can configure keys if they want offline access, but let the
164
+ // sign-in complete instead of dead-ending on an error page.
165
+ logger.warn("nextauth_google_token_storage_unconfigured", {
142
166
  user_id: result.user_id,
143
167
  error: tokenError.message,
168
+ impact: "Login succeeded; offline Google API access disabled until HAZO_AUTH_OAUTH_KEY_CURRENT and HAZO_AUTH_OAUTH_KEY_<ID> are set.",
169
+ });
170
+ }
171
+ else {
172
+ const errorMsg = tokenError instanceof Error ? tokenError.message : String(tokenError);
173
+ logger.error("nextauth_google_token_store_failed", {
174
+ user_id: result.user_id,
175
+ error: errorMsg,
144
176
  });
145
- return "/api/auth/error?error=GoogleTokenStorageUnconfigured";
146
177
  }
147
- const errorMsg = tokenError instanceof Error ? tokenError.message : String(tokenError);
148
- logger.error("nextauth_google_token_store_failed", {
149
- user_id: result.user_id,
150
- error: errorMsg,
151
- });
152
178
  // Non-fatal: continue sign-in even if token storage fails
153
179
  }
154
180
  }
@@ -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;AAOrB,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,CA2F9D;AAKD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,IAAI,oBAAoB,GAAG,IAAI,CAsC/D"}
@@ -0,0 +1,162 @@
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 { googleRedirectUri, facebookRedirectUri } from "./oauth_routes.js";
6
+ import { create_app_logger } from "./app_logger.js";
7
+ // section: helpers
8
+ const MIN_SECRET_LEN = 32;
9
+ function resolve_origin() {
10
+ var _a;
11
+ const nextauth_url = process.env.NEXTAUTH_URL;
12
+ if (nextauth_url && nextauth_url.trim().length > 0) {
13
+ return { origin: nextauth_url.trim().replace(/\/+$/, ""), resolved: true };
14
+ }
15
+ // Fallback so we can still print a best-guess URI to register.
16
+ const port = (_a = process.env.PORT) !== null && _a !== void 0 ? _a : "3000";
17
+ return { origin: `http://localhost:${port}`, resolved: false };
18
+ }
19
+ /**
20
+ * Builds a structured OAuth config report without printing. Safe to call
21
+ * anywhere server-side (CLI, route, instrumentation). Pure config inspection —
22
+ * no DB or network access.
23
+ */
24
+ export function getOAuthPreflightReport() {
25
+ const checks = [];
26
+ const { origin, resolved } = resolve_origin();
27
+ const oauth = get_oauth_config();
28
+ const redirectUris = [];
29
+ // NEXTAUTH_URL — the source of the redirect URI.
30
+ if (resolved) {
31
+ checks.push({ name: "NEXTAUTH_URL", status: "pass", message: origin });
32
+ }
33
+ else {
34
+ checks.push({
35
+ name: "NEXTAUTH_URL",
36
+ status: "fail",
37
+ message: `Not set. NextAuth derives the OAuth redirect URI from it. Set NEXTAUTH_URL=${origin} (matching the port you serve on).`,
38
+ });
39
+ }
40
+ // NEXTAUTH_URL port vs. the port the app is told to listen on.
41
+ const declared_port = process.env.PORT;
42
+ if (resolved && declared_port) {
43
+ let url_port = null;
44
+ try {
45
+ const u = new URL(origin);
46
+ url_port = u.port || (u.protocol === "https:" ? "443" : "80");
47
+ }
48
+ catch (_a) {
49
+ url_port = null;
50
+ }
51
+ if (url_port && url_port !== declared_port) {
52
+ checks.push({
53
+ name: "Port match",
54
+ status: "warn",
55
+ 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.`,
56
+ });
57
+ }
58
+ }
59
+ // NEXTAUTH_SECRET — required for OAuth session/state signing.
60
+ const secret = process.env.NEXTAUTH_SECRET;
61
+ if (!secret || secret.length === 0) {
62
+ checks.push({
63
+ name: "NEXTAUTH_SECRET",
64
+ status: "fail",
65
+ message: "Not set. Generate one: openssl rand -base64 32",
66
+ });
67
+ }
68
+ else if (secret.length < MIN_SECRET_LEN) {
69
+ checks.push({
70
+ name: "NEXTAUTH_SECRET",
71
+ status: "warn",
72
+ message: `Only ${secret.length} chars; use at least ${MIN_SECRET_LEN}. Generate: openssl rand -base64 32`,
73
+ });
74
+ }
75
+ else {
76
+ checks.push({ name: "NEXTAUTH_SECRET", status: "pass", message: "Set" });
77
+ }
78
+ // Google provider.
79
+ if (oauth.enable_google) {
80
+ const id = process.env.HAZO_AUTH_GOOGLE_CLIENT_ID;
81
+ const secretG = process.env.HAZO_AUTH_GOOGLE_CLIENT_SECRET;
82
+ if (!id || !secretG) {
83
+ const missing = [
84
+ !id ? "HAZO_AUTH_GOOGLE_CLIENT_ID" : null,
85
+ !secretG ? "HAZO_AUTH_GOOGLE_CLIENT_SECRET" : null,
86
+ ].filter(Boolean);
87
+ checks.push({
88
+ name: "Google OAuth credentials",
89
+ status: "fail",
90
+ message: `enable_google=true but missing: ${missing.join(", ")} (from Google Cloud Console).`,
91
+ });
92
+ }
93
+ else {
94
+ checks.push({ name: "Google OAuth credentials", status: "pass", message: "Set" });
95
+ }
96
+ redirectUris.push({ provider: "google", uri: googleRedirectUri(origin) });
97
+ }
98
+ // Facebook provider.
99
+ if (oauth.enable_facebook_oauth) {
100
+ const appId = oauth.facebook_app_id || process.env.HAZO_AUTH_FACEBOOK_APP_ID;
101
+ const appSecret = oauth.facebook_app_secret || process.env.HAZO_AUTH_FACEBOOK_APP_SECRET;
102
+ if (!appId || !appSecret) {
103
+ checks.push({
104
+ name: "Facebook OAuth credentials",
105
+ status: "fail",
106
+ message: "enable_facebook_oauth=true but app_id/app_secret missing.",
107
+ });
108
+ }
109
+ else {
110
+ checks.push({ name: "Facebook OAuth credentials", status: "pass", message: "Set" });
111
+ }
112
+ redirectUris.push({ provider: "facebook", uri: facebookRedirectUri(origin) });
113
+ }
114
+ return { origin, originResolved: resolved, redirectUris, checks };
115
+ }
116
+ // section: boot logger
117
+ let _logged_once = false;
118
+ /**
119
+ * Logs the OAuth preflight report once per process. Call from a Next.js
120
+ * `instrumentation.ts` `register()` hook to surface config problems — and the
121
+ * exact redirect URI to register — the moment the server boots, before anyone
122
+ * hits a broken sign-in.
123
+ *
124
+ * Idempotent: subsequent calls in the same process are no-ops.
125
+ */
126
+ export function logOAuthPreflight() {
127
+ if (_logged_once)
128
+ return null;
129
+ _logged_once = true;
130
+ const report = getOAuthPreflightReport();
131
+ if (report.redirectUris.length === 0)
132
+ return report; // No OAuth providers enabled.
133
+ const logger = create_app_logger();
134
+ for (const check of report.checks) {
135
+ if (check.status === "fail") {
136
+ logger.error("oauth_preflight_check_failed", { check: check.name, detail: check.message });
137
+ }
138
+ else if (check.status === "warn") {
139
+ logger.warn("oauth_preflight_check_warning", { check: check.name, detail: check.message });
140
+ }
141
+ }
142
+ // Highlighted, copy-pasteable block — the single thing that prevents
143
+ // redirect_uri_mismatch. console (not logger) so it renders verbatim.
144
+ const lines = [
145
+ "",
146
+ "──────────────────────────────────────────────────────────────",
147
+ " hazo_auth · OAuth redirect URIs to register in the provider console",
148
+ "──────────────────────────────────────────────────────────────",
149
+ ];
150
+ for (const { provider, uri } of report.redirectUris) {
151
+ lines.push(` ${provider.padEnd(9)} → ${uri}`);
152
+ }
153
+ if (!report.originResolved) {
154
+ lines.push("");
155
+ lines.push(" ⚠ NEXTAUTH_URL not set — above is a guess from PORT.");
156
+ lines.push(" Set NEXTAUTH_URL to your real origin and re-register.");
157
+ }
158
+ lines.push("──────────────────────────────────────────────────────────────");
159
+ lines.push("");
160
+ console.log(lines.join("\n"));
161
+ return report;
162
+ }
@@ -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.8",
3
+ "version": "10.4.10",
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",