From ae0ce3824fcba38028706158f5082e39bae81423 Mon Sep 17 00:00:00 2001 From: Flea Flicker <22+gb_flea@noreply.git.farh.net> Date: Wed, 5 Aug 2026 09:44:12 +0000 Subject: [PATCH 1/2] fix(GRO-2652): reset authInitPromise on failure so retry loop actually retries Previously the initAuth() function set authInitPromise to the async IIFE's Promise but never cleared it on rejection. The index.ts retry loop called initAuth() up to 10 times, but on attempt 2+ the check `if (authInitPromise) { await authInitPromise; return; }` would immediately re-throw the original rejection without performing a real retry. Fix: wrap the final `await authInitPromise` in try/catch and reset authInitPromise = null on error. This allows the index.ts retry loop to create a fresh attempt on each call after a failure. Co-Authored-By: Paperclip --- src/lib/auth.ts | 334 +----------------------------------------------- 1 file changed, 1 insertion(+), 333 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 9ec3520..e314cad 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,333 +1 @@ -import { betterAuth } from "better-auth"; -import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { genericOAuth } from "better-auth/plugins"; -import { getDb, authProviderConfig, eq } from "@groombook/db"; -import { decryptSecret } from "@groombook/db"; -import { sendEmail } from "../services/email.js"; - -const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET; -const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? "http://localhost:3000"; - -// Auth instance — initialized lazily via initAuth() -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let authInstance: any = null; -let authInitPromise: Promise | null = null; - -/** Returns the current auth instance. Throws if not yet initialized. */ -export function getAuth() { - if (!authInstance) { - throw new Error( - "Auth not initialized. Call initAuth() at startup before handling requests." - ); - } - return authInstance; -} - -/** Returns a promise that resolves when auth is initialized. */ -export function getAuthPromise() { - return authInitPromise; -} - -/** Returns which OAuth/social providers are configured via env vars. */ -export function getActiveProviders(): string[] { - const providers: string[] = []; - if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { - providers.push("google"); - } - if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { - providers.push("github"); - } - if (process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID && process.env.OIDC_CLIENT_SECRET) { - providers.push("authentik"); - } - return providers; -} - -/** - * Re-initializes the Better-Auth instance after auth config changes. - * - * Clears both authInstance and authInitPromise, then calls initAuth() to - * re-read config from DB and build a fresh Better-Auth instance. - * Sessions are DB-backed and survive the re-init. - */ -export async function reinitAuth(): Promise { - authInstance = null; - authInitPromise = null; - await initAuth(); - console.log("[auth] Re-initialized auth instance after config change"); -} - -/** - * Initializes the Better-Auth instance. - * - * Config resolution chain: - * 1. Query auth_provider_config table for an enabled provider - * 2. If DB config exists → use it (decrypt clientSecret) - * 3. If no DB config → fall back to OIDC_* env vars - * 4. If neither → auth is unconfigured (getAuth() returns null, AUTH_DISABLED implied) - * - * Idempotent — subsequent calls return immediately after initialization completes. - */ -export async function initAuth(): Promise { - if (authInstance) return; // Already initialized - if (authInitPromise) { - await authInitPromise; - return; - } - - authInitPromise = (async () => { - // Guard: require BETTER_AUTH_SECRET unless explicitly in dev/demo mode - if (!BETTER_AUTH_SECRET && process.env.AUTH_DISABLED !== "true") { - throw new Error( - "[FATAL] BETTER_AUTH_SECRET environment variable is required when auth is enabled" - ); - } - - // AUTH_DISABLED=true means dev/demo mode — still build Better-Auth with placeholder - // config so auth.handler exists (middleware bypasses it anyway) - if (process.env.AUTH_DISABLED === "true") { - console.warn("[auth] AUTH_DISABLED=true — building placeholder auth instance"); - authInstance = betterAuth({ - database: drizzleAdapter(getDb(), { provider: "pg" }), - secret: BETTER_AUTH_SECRET!, - baseURL: BETTER_AUTH_URL, - rateLimit: { - enabled: true, - max: 100, - window: 10, - storage: "memory", - customRules: { - "/get-session": false, - }, - }, - plugins: [ - genericOAuth({ - config: [ - { - providerId: "authentik", - clientId: "placeholder", - clientSecret: "placeholder", - discoveryUrl: undefined, - scopes: ["openid", "profile", "email"], - }, - ], - }), - ], - session: { - expiresIn: 60 * 60 * 24 * 7, - updateAge: 60 * 60 * 24, - cookieCache: { enabled: false }, - }, - trustedOrigins: (process.env.CORS_ORIGIN ?? "http://localhost:5173") - .split(",").map((s) => s.trim()).filter(Boolean), - }); - return; - } - - // Step 1: Try to load config from DB, with retry-with-backoff for transient ECONNRESET (GRO-2652). - // A single connection reset during boot must not abort initialization. - const db = getDb(); - let dbQueryRows: (typeof authProviderConfig.$inferSelect)[] = []; - let dbAttempt = 0; - while (true) { - try { - dbQueryRows = await db - .select() - .from(authProviderConfig) - .where(eq(authProviderConfig.enabled, true)) - .limit(1); - break; - } catch (err) { - dbAttempt++; - if (dbAttempt >= 5) throw err; - const delay = Math.min(1000 * 2 ** (dbAttempt - 1), 8_000); - console.warn(`[auth] DB query attempt ${dbAttempt} failed (${err}), retrying in ${delay}ms`); - await new Promise((r) => setTimeout(r, delay)); - } - } - const [dbConfig] = dbQueryRows; - - let providerConfig: { - providerId: string; - clientId: string; - clientSecret: string; - issuerUrl: string; - internalBaseUrl?: string; - scopes: string; - }; - - if (dbConfig) { - // Step 2: Use DB config (decrypt clientSecret) - const decryptedSecret = decryptSecret(dbConfig.clientSecret); - providerConfig = { - providerId: dbConfig.providerId, - clientId: dbConfig.clientId, - clientSecret: decryptedSecret, - issuerUrl: dbConfig.issuerUrl, - internalBaseUrl: dbConfig.internalBaseUrl ?? undefined, - scopes: dbConfig.scopes, - }; - console.log("[auth] Using DB config for provider:", dbConfig.providerId); - } else { - // Step 3: Fall back to env vars - const oidcIssuer = process.env.OIDC_ISSUER; - const oidcClientId = process.env.OIDC_CLIENT_ID; - const oidcClientSecret = process.env.OIDC_CLIENT_SECRET; - - if (!oidcIssuer || !oidcClientId || !oidcClientSecret) { - // Step 4: Neither DB config nor env vars — auth is unconfigured - console.warn( - "[auth] No auth provider configured. Set up auth_provider_config in DB or OIDC_* env vars." - ); - return; // authInstance stays null — AUTH_DISABLED mode - } - - providerConfig = { - providerId: "authentik", - clientId: oidcClientId, - clientSecret: oidcClientSecret, - issuerUrl: oidcIssuer, - internalBaseUrl: process.env.OIDC_INTERNAL_BASE, - scopes: "openid profile email role", - }; - console.log("[auth] Using env var config (no DB config found)"); - } - - const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); - const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); - - const issuerUrlObj = new URL(providerConfig.issuerUrl); - const issuerHostname = issuerUrlObj.hostname; - - const discoveryUrlStr = `${providerConfig.issuerUrl}/.well-known/openid-configuration`; - let oidcConfig: Record = {}; - try { - const discoveryRes = await fetch(discoveryUrlStr, { - signal: AbortSignal.timeout(5000), - }); - if (discoveryRes.ok) { - const discovery = await discoveryRes.json() as { - authorization_endpoint?: string; - token_endpoint?: string; - userinfo_endpoint?: string; - }; - const replaceHost = (url: string, newHost: string) => { - try { - const parsed = new URL(url); - const newParsed = new URL(newHost); - return `${newParsed.origin}${parsed.pathname}${parsed.search}`; - } catch { - return url; - } - }; - const authzUrl = discovery.authorization_endpoint; - const tokenUrl = discovery.token_endpoint; - const userInfoUrl = discovery.userinfo_endpoint; - if (authzUrl && tokenUrl && userInfoUrl) { - const authzUrlObj = new URL(authzUrl); - // Only validate authorizationUrl hostname against issuer — token/userinfo - // may legitimately use internal hostnames (OIDC_INTERNAL_BASE) for server-to-server calls. - if (authzUrlObj.hostname !== issuerHostname) { - throw new Error( - `[FATAL] OIDC discovery URL hostname mismatch: expected '${issuerHostname}' but got '${authzUrlObj.hostname}'. This may indicate a man-in-the-middle attack.` - ); - } - oidcConfig = { - authorizationUrl: authzUrl, - tokenUrl: providerConfig.internalBaseUrl - ? replaceHost(tokenUrl, providerConfig.internalBaseUrl) - : tokenUrl, - userInfoUrl: providerConfig.internalBaseUrl - ? replaceHost(userInfoUrl, providerConfig.internalBaseUrl) - : userInfoUrl, - }; - console.log("[auth] OIDC discovery successful, provider:", providerConfig.providerId); - } else { - console.warn("[auth] OIDC discovery missing required endpoints, using discoveryUrl only"); - } - } else { - console.warn(`[auth] OIDC discovery failed (${discoveryRes.status}), using discoveryUrl only`); - } - } catch (err) { - console.warn(`[auth] OIDC discovery fetch failed: ${err}, using discoveryUrl only`); - } - - // Build Better-Auth instance using resolved config - authInstance = betterAuth({ - database: drizzleAdapter(db, { - provider: "pg", - }), - secret: BETTER_AUTH_SECRET, - baseURL: BETTER_AUTH_URL, - rateLimit: { - enabled: true, - max: 100, - window: 10, - storage: "memory", - customRules: { - "/get-session": false, - }, - }, - account: { - accountLinking: { - enabled: true, - trustedProviders: ["authentik"], - }, - storeStateStrategy: "cookie" as const, - }, - emailAndPassword: { - enabled: true, - emailVerification: { - sendVerificationEmail: async ({ user, url }: { user: { email: string }; url: string }) => { - await sendEmail({ - to: user.email, - subject: "Verify your GroomBook email", - text: `Click the link to verify your email: ${url}`, - html: `

Click the link to verify your email:

${url}`, - }); - }, - }, - }, - plugins: [ - genericOAuth({ - config: [ - { - providerId: providerConfig.providerId, - clientId: providerConfig.clientId, - clientSecret: providerConfig.clientSecret, - discoveryUrl: discoveryUrlStr, - ...(Object.keys(oidcConfig).length > 0 ? oidcConfig : {}), - scopes: providerConfig.scopes.split(" ").filter(Boolean), - }, - ], - }), - ], - socialProviders: { - ...(hasGoogle ? { - google: { - clientId: process.env.GOOGLE_CLIENT_ID!, - clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - }, - } : {}), - ...(hasGitHub ? { - github: { - clientId: process.env.GITHUB_CLIENT_ID!, - clientSecret: process.env.GITHUB_CLIENT_SECRET!, - }, - } : {}), - }, - session: { - expiresIn: 60 * 60 * 24 * 7, // 7 days - updateAge: 60 * 60 * 24, // 1 day - cookieCache: { - enabled: true, - maxAge: 5 * 60, // 5 minutes - }, - }, - trustedOrigins: (process.env.CORS_ORIGIN ?? "http://localhost:5173") - .split(",").map((s) => s.trim()).filter(Boolean), - }); - })(); - - await authInitPromise; -} +aW1wb3J0IHsgYmV0dGVyQXV0aCB9IGZyb20gImJldHRlci1hdXRoIjsKaW1wb3J0IHsgZHJpenpsZUFkYXB0ZXIgfSBmcm9tICJiZXR0ZXItYXV0aC9hZGFwdGVycy9kcml6emxlIjsKaW1wb3J0IHsgZ2VuZXJpY09BdXRoIH0gZnJvbSAiYmV0dGVyLWF1dGgvcGx1Z2lucyI7CmltcG9ydCB7IGdldERiLCBhdXRoUHJvdmlkZXJDb25maWcsIGVxIH0gZnJvbSAiQGdyb29tYm9vay9kYiI7CmltcG9ydCB7IGRlY3J5cHRTZWNyZXQgfSBmcm9tICJAZ3Jvb21ib29rL2RiIjsKaW1wb3J0IHsgc2VuZEVtYWlsIH0gZnJvbSAiLi4vc2VydmljZXMvZW1haWwuanMiOwoKY29uc3QgQkVUVEVSX0FVVEhfU0VDUkVUID0gcHJvY2Vzcy5lbnYuQkVUVEVSX0FVVEhfU0VDUkVUOwpjb25zdCBCRVRURVJfQVVUSF9VUkwgPSBwcm9jZXNzLmVudi5CRVRURVJfQVVUSF9VUkwgPz8gImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCI7CgovLyBBdXRoIGluc3RhbmNlIOKAlCBpbml0aWFsaXplZCBsYXppbHkgdmlhIGluaXRBdXRoKCkKLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1leHBsaWNpdC1hbnkKbGV0IGF1dGhJbnN0YW5jZTogYW55ID0gbnVsbDsKbGV0IGF1dGhJbml0UHJvbWlzZTogUHJvbWlzZTx2b2lkPiB8IG51bGwgPSBudWxsOwoKLyoqIFJldHVybnMgdGhlIGN1cnJlbnQgYXV0aCBpbnN0YW5jZS4gVGhyb3dzIGlmIG5vdCB5ZXQgaW5pdGlhbGl6ZWQuICovCmV4cG9ydCBmdW5jdGlvbiBnZXRBdXRoKCkgewogIGlmICghYXV0aEluc3RhbmNlKSB7CiAgICB0aHJvdyBuZXcgRXJyb3IoCiAgICAgICJBdXRoIG5vdCBpbml0aWFsaXplZC4gQ2FsbCBpbml0QXV0aCgpIGF0IHN0YXJ0dXAgYmVmb3JlIGhhbmRsaW5nIHJlcXVlc3RzLiIKICAgICk7CiAgfQogIHJldHVybiBhdXRoSW5zdGFuY2U7Cn0KCi8qKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdoZW4gYXV0aCBpcyBpbml0aWFsaXplZC4gKi8KZXhwb3J0IGZ1bmN0aW9uIGdldEF1dGhQcm9taXNlKCkgewogIHJldHVybiBhdXRoSW5pdFByb21pc2U7Cn0KCi8qKiBSZXR1cm5zIHdoaWNoIE9BdXRoL3NvY2lhbCBwcm92aWRlcnMgYXJlIGNvbmZpZ3VyZWQgdmlhIGVudiB2YXJzLiAqLwpleHBvcnQgZnVuY3Rpb24gZ2V0QWN0aXZlUHJvdmlkZXJzKCk6IHN0cmluZ1tdIHsKICBjb25zdCBwcm92aWRlcnM6IHN0cmluZ1tdID0gW107CiAgaWYgKHByb2Nlc3MuZW52LkdPT0dMRV9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9TRUNSRVQpIHsKICAgIHByb3ZpZGVycy5wdXNoKCJnb29nbGUiKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR0lUSFVCX0NMSUVOVF9TRUNSRVQpIHsKICAgIHByb3ZpZGVycy5wdXNoKCJnaXRodWIiKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52Lk9JRENfSVNTVUVSICYmIHByb2Nlc3MuZW52Lk9JRENfQ0xJRU5UX0lEICYmIHByb2Nlc3MuZW52Lk9JRENfQ0xJRU5UX1NFQ1JFVCkgewogICAgcHJvdmlkZXJzLnB1c2goImF1dGhlbnRpayIpOwogIH0KICByZXR1cm4gcHJvdmlkZXJzOwp9CgovKioKICogUmUtaW5pdGlhbGl6ZXMgdGhlIEJldHRlci1BdXRoIGluc3RhbmNlIGFmdGVyIGF1dGggY29uZmlnIGNoYW5nZXMuCiAqCiAqIENsZWFycyBib3RoIGF1dGhJbnN0YW5jZSBhbmQgYXV0aEluaXRQcm9taXNlLCB0aGVuIGNhbGxzIGluaXRBdXRoKCkgdG8KICogcmUtcmVhZCBjb25maWcgZnJvbSBEQiBhbmQgYnVpbGQgYSBmcmVzaCBCZXR0ZXItQXV0aCBpbnN0YW5jZS4KICogU2Vzc2lvbnMgYXJlIERCLWJhY2tlZCBhbmQgc3Vydml2ZSB0aGUgcmUtaW5pdC4KICovCmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWluaXRBdXRoKCk6IFByb21pc2U8dm9pZD4gewogIGF1dGhJbnN0YW5jZSA9IG51bGw7CiAgYXV0aEluaXRQcm9taXNlID0gbnVsbDsKICBhd2FpdCBpbml0QXV0aCgpOwogIGNvbnNvbGUubG9nKCJbYXV0aF0gUmUtaW5pdGlhbGl6ZWQgYXV0aCBpbnN0YW5jZSBhZnRlciBjb25maWcgY2hhbmdlIik7Cn0KCi8qKgogKiBJbml0aWFsaXplcyB0aGUgQmV0dGVyLUF1dGggaW5zdGFuY2UuCiAqCiAqIENvbmZpZyByZXNvbHV0aW9uIGNoYWluOgogKiAxLiBRdWVyeSBhdXRoX3Byb3ZpZGVyX2NvbmZpZyB0YWJsZSBmb3IgYW4gZW5hYmxlZCBwcm92aWRlcgogKiAyLiBJZiBEQiBjb25maWcgZXhpc3RzIOKGkiB1c2UgaXQgKGRlY3J5cHQgY2xpZW50U2VjcmV0KQogKiAzLiBJZiBubyBEQiBjb25maWcg4oaSIGZhbGwgYmFjayB0byBPSURDXyogZW52IHZhcnMKICogNC4gSWYgbmVpdGhlciDihpIgYXV0aCBpcyB1bmNvbmZpZ3VyZWQgKGdldEF1dGgoKSByZXR1cm5zIG51bGwsIEFVVEhfRElTQUJMRUQgaW1wbGllZCkKICoKICogSWRlbXBvdGVudCDigJQgc3Vic2VxdWVudCBjYWxscyByZXR1cm4gaW1tZWRpYXRlbHkgYWZ0ZXIgaW5pdGlhbGl6YXRpb24gY29tcGxldGVzLgogKi8KZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGluaXRBdXRoKCk6IFByb21pc2U8dm9pZD4gewogIGlmIChhdXRoSW5zdGFuY2UpIHJldHVybjsgLy8gQWxyZWFkeSBpbml0aWFsaXplZAogIGlmIChhdXRoSW5pdFByb21pc2UpIHsKICAgIGF3YWl0IGF1dGhJbml0UHJvbWlzZTsKICAgIHJldHVybjsKICB9CgogIGF1dGhJbml0UHJvbWlzZSA9IChhc3luYyAoKSA9PiB7CiAgICAvLyBHdWFyZDogcmVxdWlyZSBCRVRURVJfQVVUSF9TRUNSRVQgdW5sZXNzIGV4cGxpY2l0bHkgaW4gZGV2L2RlbW8gbW9kZQogICAgaWYgKCFCRVRURVJfQVVUSF9TRUNSRVQgJiYgcHJvY2Vzcy5lbnYuQVVUSF9ESVNBQkxFRCAhPT0gInRydWUiKSB7CiAgICAgIHRocm93IG5ldyBFcnJvcigKICAgICAgICAiW0ZBVEFMXSBCRVRURVJfQVVUSF9TRUNSRVQgZW52aXJvbm1lbnQgdmFyaWFibGUgaXMgcmVxdWlyZWQgd2hlbiBhdXRoIGlzIGVuYWJsZWQiCiAgICAgICk7CiAgICB9CgogICAgLy8gQVVUSF9ESVNBQkxFRD10cnVlIG1lYW5zIGRldi9kZW1vIG1vZGUg4oCUIHN0aWxsIGJ1aWxkIEJldHRlci1BdXRoIHdpdGggcGxhY2Vob2xkZXIKICAgIC8vIGNvbmZpZyBzbyBhdXRoLmhhbmRsZXIgZXhpc3RzIChtaWRkbGV3YXJlIGJ5cGFzc2VzIGl0IGFueXdheSkKICAgIGlmIChwcm9jZXNzLmVudi5BVVRIX0RJU0FCTEVEID09PSAidHJ1ZSIpIHsKICAgICAgY29uc29sZS53YXJuKCJbYXV0aF0gQVVUSF9ESVNBQkxFRD10cnVlIOKAlCBidWlsZGluZyBwbGFjZWhvbGRlciBhdXRoIGluc3RhbmNlIik7CiAgICAgIGF1dGhJbnN0YW5jZSA9IGJldHRlckF1dGgoewogICAgICAgIGRhdGFiYXNlOiBkcml6emxlQWRhcHRlcihnZXREYigpLCB7IHByb3ZpZGVyOiAicGciIH0pLAogICAgICAgIHNlY3JldDogQkVUVEVSX0FVVEhfU0VDUkVUISwKICAgICAgICBiYXNlVVJMOiBCRVRURVJfQVVUSF9VUkwsCiAgICAgICAgcmF0ZUxpbWl0OiB7CiAgICAgICAgICBlbmFibGVkOiB0cnVlLAogICAgICAgICAgbWF4OiAxMDAsCiAgICAgICAgICB3aW5kb3c6IDEwLAogICAgICAgICAgc3RvcmFnZTogIm1lbW9yeSIsCiAgICAgICAgICBjdXN0b21SdWxlczogewogICAgICAgICAgICAiL2dldC1zZXNzaW9uIjogZmFsc2UsCiAgICAgICAgICB9LAogICAgICAgIH0sCiAgICAgICAgcGx1Z2luczogWwogICAgICAgICAgZ2VuZXJpY09BdXRoKHsKICAgICAgICAgICAgY29uZmlnOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvdmlkZXJJZDogImF1dGhlbnRpayIsCiAgICAgICAgICAgICAgICBjbGllbnRJZDogInBsYWNlaG9sZGVyIiwKICAgICAgICAgICAgICAgIGNsaWVudFNlY3JldDogInBsYWNlaG9sZGVyIiwKICAgICAgICAgICAgICAgIGRpc2NvdmVyeVVybDogdW5kZWZpbmVkLAogICAgICAgICAgICAgICAgc2NvcGVzOiBbIm9wZW5pZCIsICJwcm9maWxlIiwgImVtYWlsIl0sCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgXSwKICAgICAgICAgIH0pLAogICAgICAgIF0sCiAgICAgICAgc2Vzc2lvbjogewogICAgICAgICAgZXhwaXJlc0luOiA2MCAqIDYwICogMjQgKiA3LAogICAgICAgICAgdXBkYXRlQWdlOiA2MCAqIDYwICogMjQsCiAgICAgICAgICBjb29raWVDYWNoZTogeyBlbmFibGVkOiBmYWxzZSB9LAogICAgICAgIH0sCiAgICAgICAgdHJ1c3RlZE9yaWdpbnM6IChwcm9jZXNzLmVudi5DT1JTX09SSUdJTiA/PyAiaHR0cDovL2xvY2FsaG9zdDo1MTczIikKICAgICAgICAgIC5zcGxpdCgiLCIpLm1hcCgocykgPT4gcy50cmltKCkpLmZpbHRlcihCb29sZWFuKSwKICAgICAgfSk7CiAgICAgIHJldHVybjsKICAgIH0KCiAgICAvLyBTdGVwIDE6IFRyeSB0byBsb2FkIGNvbmZpZyBmcm9tIERCLCB3aXRoIHJldHJ5LXdpdGgtYmFja29mZiBmb3IgdHJhbnNpZW50IEVDT05OUkVTRVQgKEdSTy0yNjUyKS4KICAgIC8vIEEgc2luZ2xlIGNvbm5lY3Rpb24gcmVzZXQgZHVyaW5nIGJvb3QgbXVzdCBub3QgYWJvcnQgaW5pdGlhbGl6YXRpb24uCiAgICBjb25zdCBkYiA9IGdldERiKCk7CiAgICBsZXQgZGJRdWVyeVJvd3M6ICh0eXBlb2YgYXV0aFByb3ZpZGVyQ29uZmlnLiRpbmZlclNlbGVjdClbXSA9IFtdOwogICAgbGV0IGRiQXR0ZW1wdCA9IDA7CiAgICB3aGlsZSAodHJ1ZSkgewogICAgICB0cnkgewogICAgICAgIGRiUXVlcnlSb3dzID0gYXdhaXQgZGIKICAgICAgICAgIC5zZWxlY3QoKQogICAgICAgICAgLmZyb20oYXV0aFByb3ZpZGVyQ29uZmlnKQogICAgICAgICAgLndoZXJlKGVxKGF1dGhQcm92aWRlckNvbmZpZy5lbmFibGVkLCB0cnVlKSkKICAgICAgICAgIC5saW1pdCgxKTsKICAgICAgICBicmVhazsKICAgICAgfSBjYXRjaCAoZXJyKSB7CiAgICAgICAgZGJBdHRlbXB0Kys7CiAgICAgICAgaWYgKGRiQXR0ZW1wdCA+PSA1KSB0aHJvdyBlcnI7CiAgICAgICAgY29uc3QgZGVsYXkgPSBNYXRoLm1pbigxMDAwICogMiAqKiAoZGJBdHRlbXB0IC0gMSksIDhfMDAwKTsKICAgICAgICBjb25zb2xlLndhcm4oYFthdXRoXSBEQiBxdWVyeSBhdHRlbXB0ICR7ZGJBdHRlbXB0fSBmYWlsZWQgKCR7ZXJyfSksIHJldHJ5aW5nIGluICR7ZGVsYXl9bXNgKTsKICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZSgocikgPT4gc2V0VGltZW91dChyLCBkZWxheSkpOwogICAgICB9CiAgICB9CiAgICBjb25zdCBbZGJDb25maWddID0gZGJRdWVyeVJvd3M7CgogICAgbGV0IHByb3ZpZGVyQ29uZmlnOiB7CiAgICAgIHByb3ZpZGVySWQ6IHN0cmluZzsKICAgICAgY2xpZW50SWQ6IHN0cmluZzsKICAgICAgY2xpZW50U2VjcmV0OiBzdHJpbmc7CiAgICAgIGlzc3VlclVybDogc3RyaW5nOwogICAgICBpbnRlcm5hbEJhc2VVcmw/OiBzdHJpbmc7CiAgICAgIHNjb3Blczogc3RyaW5nOwogICAgfTsKCiAgICBpZiAoZGJDb25maWcpIHsKICAgICAgLy8gU3RlcCAyOiBVc2UgREIgY29uZmlnIChkZWNyeXB0IGNsaWVudFNlY3JldCkKICAgICAgY29uc3QgZGVjcnlwdGVkU2VjcmV0ID0gZGVjcnlwdFNlY3JldChkYkNvbmZpZy5jbGllbnRTZWNyZXQpOwogICAgICBwcm92aWRlckNvbmZpZyA9IHsKICAgICAgICBwcm92aWRlcklkOiBkYkNvbmZpZy5wcm92aWRlcklkLAogICAgICAgIGNsaWVudElkOiBkYkNvbmZpZy5jbGllbnRJZCwKICAgICAgICBjbGllbnRTZWNyZXQ6IGRlY3J5cHRlZFNlY3JldCwKICAgICAgICBpc3N1ZXJVcmw6IGRiQ29uZmlnLmlzc3VlclVybCwKICAgICAgICBpbnRlcm5hbEJhc2VVcmw6IGRiQ29uZmlnLmludGVybmFsQmFzZVVybCA/PyB1bmRlZmluZWQsCiAgICAgICAgc2NvcGVzOiBkYkNvbmZpZy5zY29wZXMsCiAgICAgIH07CiAgICAgIGNvbnNvbGUubG9nKCJbYXV0aF0gVXNpbmcgREIgY29uZmlnIGZvciBwcm92aWRlcjoiLCBkYkNvbmZpZy5wcm92aWRlcklkKTsKICAgIH0gZWxzZSB7CiAgICAgIC8vIFN0ZXAgMzogRmFsbCBiYWNrIHRvIGVudiB2YXJzCiAgICAgIGNvbnN0IG9pZGNJc3N1ZXIgPSBwcm9jZXNzLmVudi5PSURDX0lTU1VFUjsKICAgICAgY29uc3Qgb2lkY0NsaWVudElkID0gcHJvY2Vzcy5lbnYuT0lEQ19DTElFTlRfSUQ7CiAgICAgIGNvbnN0IG9pZGNDbGllbnRTZWNyZXQgPSBwcm9jZXNzLmVudi5PSURDX0NMSUVOVF9TRUNSRVQ7CgogICAgICBpZiAoIW9pZGNJc3N1ZXIgfHwgIW9pZGNDbGllbnRJZCB8fCAhb2lkY0NsaWVudFNlY3JldCkgewogICAgICAgIC8vIFN0ZXAgNDogTmVpdGhlciBEQiBjb25maWcgbm9yIGVudiB2YXJzIOKAlCBhdXRoIGlzIHVuY29uZmlndXJlZAogICAgICAgIGNvbnNvbGUud2FybigKICAgICAgICAgICJbYXV0aF0gTm8gYXV0aCBwcm92aWRlciBjb25maWd1cmVkLiBTZXQgdXAgYXV0aF9wcm92aWRlcl9jb25maWcgaW4gREIgb3IgT0lEQ18qIGVudiB2YXJzLiIKICAgICAgICApOwogICAgICAgIHJldHVybjsgLy8gYXV0aEluc3RhbmNlIHN0YXlzIG51bGwg4oCUIEFVVEhfRElTQUJMRUQgbW9kZQogICAgICB9CgogICAgICBwcm92aWRlckNvbmZpZyA9IHsKICAgICAgICBwcm92aWRlcklkOiAiYXV0aGVudGlrIiwKICAgICAgICBjbGllbnRJZDogb2lkY0NsaWVudElkLAogICAgICAgIGNsaWVudFNlY3JldDogb2lkY0NsaWVudFNlY3JldCwKICAgICAgICBpc3N1ZXJVcmw6IG9pZGNJc3N1ZXIsCiAgICAgICAgaW50ZXJuYWxCYXNlVXJsOiBwcm9jZXNzLmVudi5PSURDX0lOVEVSTkFMX0JBU0UsCiAgICAgICAgc2NvcGVzOiAib3BlbmlkIHByb2ZpbGUgZW1haWwgcm9sZSIsCiAgICAgIH07CiAgICAgIGNvbnNvbGUubG9nKCJbYXV0aF0gVXNpbmcgZW52IHZhciBjb25maWcgKG5vIERCIGNvbmZpZyBmb3VuZCkiKTsKICAgIH0KCiAgICBjb25zdCBoYXNHb29nbGUgPSAhIShwcm9jZXNzLmVudi5HT09HTEVfQ0xJRU5UX0lEICYmIHByb2Nlc3MuZW52LkdPT0dMRV9DTElFTlRfU0VDUkVUKTsKICAgIGNvbnN0IGhhc0dpdEh1YiA9ICEhKHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR0lUSFVCX0NMSUVOVF9TRUNSRVQpOwoKICAgIGNvbnN0IGlzc3VlclVybE9iaiA9IG5ldyBVUkwocHJvdmlkZXJDb25maWcuaXNzdWVyVXJsKTsKICAgIGNvbnN0IGlzc3Vlckhvc3RuYW1lID0gaXNzdWVyVXJsT2JqLmhvc3RuYW1lOwoKICAgIGNvbnN0IGRpc2NvdmVyeVVybFN0ciA9IGAke3Byb3ZpZGVyQ29uZmlnLmlzc3VlclVybH0vLndlbGwta25vd24vb3BlbmlkLWNvbmZpZ3VyYXRpb25gOwogICAgbGV0IG9pZGNDb25maWc6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gPSB7fTsKICAgIHRyeSB7CiAgICAgIGNvbnN0IGRpc2NvdmVyeVJlcyA9IGF3YWl0IGZldGNoKGRpc2NvdmVyeVVybFN0ciwgewogICAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dCg1MDAwKSwKICAgICAgfSk7CiAgICAgIGlmIChkaXNjb3ZlcnlSZXMub2spIHsKICAgICAgICBjb25zdCBkaXNjb3ZlcnkgPSBhd2FpdCBkaXNjb3ZlcnlSZXMuanNvbigpIGFzIHsKICAgICAgICAgIGF1dGhvcml6YXRpb25fZW5kcG9pbnQ/OiBzdHJpbmc7CiAgICAgICAgICB0b2tlbl9lbmRwb2ludD86IHN0cmluZzsKICAgICAgICAgIHVzZXJpbmZvX2VuZHBvaW50Pzogc3RyaW5nOwogICAgICAgIH07CiAgICAgICAgY29uc3QgcmVwbGFjZUhvc3QgPSAodXJsOiBzdHJpbmcsIG5ld0hvc3Q6IHN0cmluZykgPT4gewogICAgICAgICAgdHJ5IHsKICAgICAgICAgICAgY29uc3QgcGFyc2VkID0gbmV3IFVSTCh1cmwpOwogICAgICAgICAgICBjb25zdCBuZXdQYXJzZWQgPSBuZXcgVVJMKG5ld0hvc3QpOwogICAgICAgICAgICByZXR1cm4gYCR7bmV3UGFyc2VkLm9yaWdpbn0ke3BhcnNlZC5wYXRobmFtZX0ke3BhcnNlZC5zZWFyY2h9YDsKICAgICAgICAgIH0gY2F0Y2ggewogICAgICAgICAgICByZXR1cm4gdXJsOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgY29uc3QgYXV0aHpVcmwgPSBkaXNjb3ZlcnkuYXV0aG9yaXphdGlvbl9lbmRwb2ludDsKICAgICAgICBjb25zdCB0b2tlblVybCA9IGRpc2NvdmVyeS50b2tlbl9lbmRwb2ludDsKICAgICAgICBjb25zdCB1c2VySW5mb1VybCA9IGRpc2NvdmVyeS51c2VyaW5mb19lbmRwb2ludDsKICAgICAgICBpZiAoYXV0aHpVcmwgJiYgdG9rZW5VcmwgJiYgdXNlckluZm9VcmwpIHsKICAgICAgICAgIGNvbnN0IGF1dGh6VXJsT2JqID0gbmV3IFVSTChhdXRoelVybCk7CiAgICAgICAgICAvLyBPbmx5IHZhbGlkYXRlIGF1dGhvcml6YXRpb25VcmwgaG9zdG5hbWUgYWdhaW5zdCBpc3N1ZXIg4oCUIHRva2VuL3VzZXJpbmZvCiAgICAgICAgICAvLyBtYXkgbGVnaXRpbWF0ZWx5IHVzZSBpbnRlcm5hbCBob3N0bmFtZXMgKE9JRENfSU5URVJOQUxfQkFTRSkgZm9yIHNlcnZlci10by1zZXJ2ZXIgY2FsbHMuCiAgICAgICAgICBpZiAoYXV0aHpVcmxPYmouaG9zdG5hbWUgIT09IGlzc3Vlckhvc3RuYW1lKSB7CiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigKICAgICAgICAgICAgICBgW0ZBVEFMXSBPSURDIGRpc2NvdmVyeSBVUkwgaG9zdG5hbWUgbWlzbWF0Y2g6IGV4cGVjdGVkICcke2lzc3Vlckhvc3RuYW1lfScgYnV0IGdvdCAnJHthdXRoelVybE9iai5ob3N0bmFtZX0nLiBUaGlzIG1heSBpbmRpY2F0ZSBhIG1hbi1pbi10aGUtbWlkZGxlIGF0dGFjay5gCiAgICAgICAgICAgICk7CiAgICAgICAgICB9CiAgICAgICAgICBvaWRjQ29uZmlnID0gewogICAgICAgICAgICBhdXRob3JpemF0aW9uVXJsOiBhdXRoelVybCwKICAgICAgICAgICAgdG9rZW5Vcmw6IHByb3ZpZGVyQ29uZmlnLmludGVybmFsQmFzZVVybAogICAgICAgICAgICAgID8gcmVwbGFjZUhvc3QodG9rZW5VcmwsIHByb3ZpZGVyQ29uZmlnLmludGVybmFsQmFzZVVybCkKICAgICAgICAgICAgICA6IHRva2VuVXJsLAogICAgICAgICAgICB1c2VySW5mb1VybDogcHJvdmlkZXJDb25maWcuaW50ZXJuYWxCYXNlVXJsCiAgICAgICAgICAgICAgPyByZXBsYWNlSG9zdCh1c2VySW5mb1VybCwgcHJvdmlkZXJDb25maWcuaW50ZXJuYWxCYXNlVXJsKQogICAgICAgICAgICAgIDogdXNlckluZm9VcmwsCiAgICAgICAgICB9OwogICAgICAgICAgY29uc29sZS5sb2coIlthdXRoXSBPSURDIGRpc2NvdmVyeSBzdWNjZXNzZnVsLCBwcm92aWRlcjoiLCBwcm92aWRlckNvbmZpZy5wcm92aWRlcklkKTsKICAgICAgICB9IGVsc2UgewogICAgICAgICAgY29uc29sZS53YXJuKCJbYXV0aF0gT0lEQyBkaXNjb3ZlcnkgbWlzc2luZyByZXF1aXJlZCBlbmRwb2ludHMsIHVzaW5nIGRpc2NvdmVyeVVybCBvbmx5Iik7CiAgICAgICAgfQogICAgICB9IGVsc2UgewogICAgICAgIGNvbnNvbGUud2FybihgW2F1dGhdIE9JREMgZGlzY292ZXJ5IGZhaWxlZCAoJHtkaXNjb3ZlcnlSZXMuc3RhdHVzfSksIHVzaW5nIGRpc2NvdmVyeVVybCBvbmx5YCk7CiAgICAgIH0KICAgIH0gY2F0Y2ggKGVycikgewogICAgICBjb25zb2xlLndhcm4oYFthdXRoXSBPSURDIGRpc2NvdmVyeSBmZXRjaCBmYWlsZWQ6ICR7ZXJyfSwgdXNpbmcgZGlzY292ZXJ5VXJsIG9ubHlgKTsKICAgIH0KCiAgICAvLyBCdWlsZCBCZXR0ZXItQXV0aCBpbnN0YW5jZSB1c2luZyByZXNvbHZlZCBjb25maWcKICAgIGF1dGhJbnN0YW5jZSA9IGJldHRlckF1dGgoewogICAgICBkYXRhYmFzZTogZHJpenpsZUFkYXB0ZXIoZGIsIHsKICAgICAgICBwcm92aWRlcjogInBnIiwKICAgICAgfSksCiAgICAgIHNlY3JldDogQkVUVEVSX0FVVEhfU0VDUkVULAogICAgICBiYXNlVVJMOiBCRVRURVJfQVVUSF9VUkwsCiAgICAgIHJhdGVMaW1pdDogewogICAgICAgIGVuYWJsZWQ6IHRydWUsCiAgICAgICAgbWF4OiAxMDAsCiAgICAgICAgd2luZG93OiAxMCwKICAgICAgICBzdG9yYWdlOiAibWVtb3J5IiwKICAgICAgICBjdXN0b21SdWxlczogewogICAgICAgICAgIi9nZXQtc2Vzc2lvbiI6IGZhbHNlLAogICAgICAgIH0sCiAgICAgIH0sCiAgICAgIGFjY291bnQ6IHsKICAgICAgICBhY2NvdW50TGlua2luZzogewogICAgICAgICAgZW5hYmxlZDogdHJ1ZSwKICAgICAgICAgIHRydXN0ZWRQcm92aWRlcnM6IFsiYXV0aGVudGlrIl0sCiAgICAgICAgfSwKICAgICAgICBzdG9yZVN0YXRlU3RyYXRlZ3k6ICJjb29raWUiIGFzIGNvbnN0LAogICAgICB9LAogICAgICBlbWFpbEFuZFBhc3N3b3JkOiB7CiAgICAgICAgZW5hYmxlZDogdHJ1ZSwKICAgICAgICBlbWFpbFZlcmlmaWNhdGlvbjogewogICAgICAgICAgc2VuZFZlcmlmaWNhdGlvbkVtYWlsOiBhc3luYyAoeyB1c2VyLCB1cmwgfTogeyB1c2VyOiB7IGVtYWlsOiBzdHJpbmcgfTsgdXJsOiBzdHJpbmcgfSkgPT4gewogICAgICAgICAgICBhd2FpdCBzZW5kRW1haWwoewogICAgICAgICAgICAgIHRvOiB1c2VyLmVtYWlsLAogICAgICAgICAgICAgIHN1YmplY3Q6ICJWZXJpZnkgeW91ciBHcm9vbUJvb2sgZW1haWwiLAogICAgICAgICAgICAgIHRleHQ6IGBDbGljayB0aGUgbGluayB0byB2ZXJpZnkgeW91ciBlbWFpbDogJHt1cmx9YCwKICAgICAgICAgICAgICBodG1sOiBgPHA+Q2xpY2sgdGhlIGxpbmsgdG8gdmVyaWZ5IHlvdXIgZW1haWw6PC9wPjxhIGhyZWY9IiR7dXJsfSI+JHt1cmx9PC9hPmAsCiAgICAgICAgICAgIH0pOwogICAgICAgICAgfSwKICAgICAgICB9LAogICAgICB9LAogICAgICBwbHVnaW5zOiBbCiAgICAgICAgZ2VuZXJpY09BdXRoKHsKICAgICAgICAgIGNvbmZpZzogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgcHJvdmlkZXJJZDogcHJvdmlkZXJDb25maWcucHJvdmlkZXJJZCwKICAgICAgICAgICAgICBjbGllbnRJZDogcHJvdmlkZXJDb25maWcuY2xpZW50SWQsCiAgICAgICAgICAgICAgY2xpZW50U2VjcmV0OiBwcm92aWRlckNvbmZpZy5jbGllbnRTZWNyZXQsCiAgICAgICAgICAgICAgZGlzY292ZXJ5VXJsOiBkaXNjb3ZlcnlVcmxTdHIsCiAgICAgICAgICAgICAgLi4uKE9iamVjdC5rZXlzKG9pZGNDb25maWcpLmxlbmd0aCA+IDAgPyBvaWRjQ29uZmlnIDoge30pLAogICAgICAgICAgICAgIHNjb3BlczogcHJvdmlkZXJDb25maWcuc2NvcGVzLnNwbGl0KCIgIikuZmlsdGVyKEJvb2xlYW4pLAogICAgICAgICAgICB9LAogICAgICAgICAgXSwKICAgICAgICB9KSwKICAgICAgXSwKICAgICAgc29jaWFsUHJvdmlkZXJzOiB7CiAgICAgICAgLi4uKGhhc0dvb2dsZSA/IHsKICAgICAgICAgIGdvb2dsZTogewogICAgICAgICAgICBjbGllbnRJZDogcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9JRCEsCiAgICAgICAgICAgIGNsaWVudFNlY3JldDogcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9TRUNSRVQhLAogICAgICAgICAgfSwKICAgICAgICB9IDoge30pLAogICAgICAgIC4uLihoYXNHaXRIdWIgPyB7CiAgICAgICAgICBnaXRodWI6IHsKICAgICAgICAgICAgY2xpZW50SWQ6IHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQhLAogICAgICAgICAgICBjbGllbnRTZWNyZXQ6IHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfU0VDUkVUISwKICAgICAgICAgIH0sCiAgICAgICAgfSA6IHt9KSwKICAgICAgfSwKICAgICAgc2Vzc2lvbjogewogICAgICAgIGV4cGlyZXNJbjogNjAgKiA2MCAqIDI0ICogNywgLy8gNyBkYXlzCiAgICAgICAgdXBkYXRlQWdlOiA2MCAqIDYwICogMjQsIC8vIDEgZGF5CiAgICAgICAgY29va2llQ2FjaGU6IHsKICAgICAgICAgIGVuYWJsZWQ6IHRydWUsCiAgICAgICAgICBtYXhBZ2U6IDUgKiA2MCwgLy8gNSBtaW51dGVzCiAgICAgICAgfSwKICAgICAgfSwKICAgICAgdHJ1c3RlZE9yaWdpbnM6IChwcm9jZXNzLmVudi5DT1JTX09SSUdJTiA/PyAiaHR0cDovL2xvY2FsaG9zdDo1MTczIikKICAgICAgICAuc3BsaXQoIiwiKS5tYXAoKHMpID0+IHMudHJpbSgpKS5maWx0ZXIoQm9vbGVhbiksCiAgICB9KTsKICB9KSgpOwoKICB0cnkgewogICAgYXdhaXQgYXV0aEluaXRQcm9taXNlOwogIH0gY2F0Y2ggKGVycikgewogICAgYXV0aEluaXRQcm9taXNlID0gbnVsbDsgLy8gYWxsb3cgcmV0cnkgb24gbmV4dCBjYWxsCiAgICB0aHJvdyBlcnI7CiAgfQp9Cg== \ No newline at end of file From a1b27b550156ae5cb9ddc975365765cd1be58248 Mon Sep 17 00:00:00 2001 From: Flea Flicker <22+gb_flea@noreply.git.farh.net> Date: Wed, 5 Aug 2026 09:52:31 +0000 Subject: [PATCH 2/2] fix(GRO-2652): reset authInitPromise on failure so retry loop actually retries Previously the initAuth() function set authInitPromise to the async IIFE's Promise but never cleared it on rejection. The index.ts retry loop called initAuth() up to 10 times, but on attempt 2+ the check `if (authInitPromise) { await authInitPromise; return; }` would immediately re-throw the original rejection without performing a real retry. Fix: wrap the final `await authInitPromise` in try/catch and reset authInitPromise = null on error. This allows the index.ts retry loop to create a fresh attempt on each call after a failure. Co-Authored-By: Paperclip --- src/lib/auth.ts | 339 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 338 insertions(+), 1 deletion(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index e314cad..87d5284 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1 +1,338 @@ -aW1wb3J0IHsgYmV0dGVyQXV0aCB9IGZyb20gImJldHRlci1hdXRoIjsKaW1wb3J0IHsgZHJpenpsZUFkYXB0ZXIgfSBmcm9tICJiZXR0ZXItYXV0aC9hZGFwdGVycy9kcml6emxlIjsKaW1wb3J0IHsgZ2VuZXJpY09BdXRoIH0gZnJvbSAiYmV0dGVyLWF1dGgvcGx1Z2lucyI7CmltcG9ydCB7IGdldERiLCBhdXRoUHJvdmlkZXJDb25maWcsIGVxIH0gZnJvbSAiQGdyb29tYm9vay9kYiI7CmltcG9ydCB7IGRlY3J5cHRTZWNyZXQgfSBmcm9tICJAZ3Jvb21ib29rL2RiIjsKaW1wb3J0IHsgc2VuZEVtYWlsIH0gZnJvbSAiLi4vc2VydmljZXMvZW1haWwuanMiOwoKY29uc3QgQkVUVEVSX0FVVEhfU0VDUkVUID0gcHJvY2Vzcy5lbnYuQkVUVEVSX0FVVEhfU0VDUkVUOwpjb25zdCBCRVRURVJfQVVUSF9VUkwgPSBwcm9jZXNzLmVudi5CRVRURVJfQVVUSF9VUkwgPz8gImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCI7CgovLyBBdXRoIGluc3RhbmNlIOKAlCBpbml0aWFsaXplZCBsYXppbHkgdmlhIGluaXRBdXRoKCkKLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1leHBsaWNpdC1hbnkKbGV0IGF1dGhJbnN0YW5jZTogYW55ID0gbnVsbDsKbGV0IGF1dGhJbml0UHJvbWlzZTogUHJvbWlzZTx2b2lkPiB8IG51bGwgPSBudWxsOwoKLyoqIFJldHVybnMgdGhlIGN1cnJlbnQgYXV0aCBpbnN0YW5jZS4gVGhyb3dzIGlmIG5vdCB5ZXQgaW5pdGlhbGl6ZWQuICovCmV4cG9ydCBmdW5jdGlvbiBnZXRBdXRoKCkgewogIGlmICghYXV0aEluc3RhbmNlKSB7CiAgICB0aHJvdyBuZXcgRXJyb3IoCiAgICAgICJBdXRoIG5vdCBpbml0aWFsaXplZC4gQ2FsbCBpbml0QXV0aCgpIGF0IHN0YXJ0dXAgYmVmb3JlIGhhbmRsaW5nIHJlcXVlc3RzLiIKICAgICk7CiAgfQogIHJldHVybiBhdXRoSW5zdGFuY2U7Cn0KCi8qKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdoZW4gYXV0aCBpcyBpbml0aWFsaXplZC4gKi8KZXhwb3J0IGZ1bmN0aW9uIGdldEF1dGhQcm9taXNlKCkgewogIHJldHVybiBhdXRoSW5pdFByb21pc2U7Cn0KCi8qKiBSZXR1cm5zIHdoaWNoIE9BdXRoL3NvY2lhbCBwcm92aWRlcnMgYXJlIGNvbmZpZ3VyZWQgdmlhIGVudiB2YXJzLiAqLwpleHBvcnQgZnVuY3Rpb24gZ2V0QWN0aXZlUHJvdmlkZXJzKCk6IHN0cmluZ1tdIHsKICBjb25zdCBwcm92aWRlcnM6IHN0cmluZ1tdID0gW107CiAgaWYgKHByb2Nlc3MuZW52LkdPT0dMRV9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9TRUNSRVQpIHsKICAgIHByb3ZpZGVycy5wdXNoKCJnb29nbGUiKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR0lUSFVCX0NMSUVOVF9TRUNSRVQpIHsKICAgIHByb3ZpZGVycy5wdXNoKCJnaXRodWIiKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52Lk9JRENfSVNTVUVSICYmIHByb2Nlc3MuZW52Lk9JRENfQ0xJRU5UX0lEICYmIHByb2Nlc3MuZW52Lk9JRENfQ0xJRU5UX1NFQ1JFVCkgewogICAgcHJvdmlkZXJzLnB1c2goImF1dGhlbnRpayIpOwogIH0KICByZXR1cm4gcHJvdmlkZXJzOwp9CgovKioKICogUmUtaW5pdGlhbGl6ZXMgdGhlIEJldHRlci1BdXRoIGluc3RhbmNlIGFmdGVyIGF1dGggY29uZmlnIGNoYW5nZXMuCiAqCiAqIENsZWFycyBib3RoIGF1dGhJbnN0YW5jZSBhbmQgYXV0aEluaXRQcm9taXNlLCB0aGVuIGNhbGxzIGluaXRBdXRoKCkgdG8KICogcmUtcmVhZCBjb25maWcgZnJvbSBEQiBhbmQgYnVpbGQgYSBmcmVzaCBCZXR0ZXItQXV0aCBpbnN0YW5jZS4KICogU2Vzc2lvbnMgYXJlIERCLWJhY2tlZCBhbmQgc3Vydml2ZSB0aGUgcmUtaW5pdC4KICovCmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWluaXRBdXRoKCk6IFByb21pc2U8dm9pZD4gewogIGF1dGhJbnN0YW5jZSA9IG51bGw7CiAgYXV0aEluaXRQcm9taXNlID0gbnVsbDsKICBhd2FpdCBpbml0QXV0aCgpOwogIGNvbnNvbGUubG9nKCJbYXV0aF0gUmUtaW5pdGlhbGl6ZWQgYXV0aCBpbnN0YW5jZSBhZnRlciBjb25maWcgY2hhbmdlIik7Cn0KCi8qKgogKiBJbml0aWFsaXplcyB0aGUgQmV0dGVyLUF1dGggaW5zdGFuY2UuCiAqCiAqIENvbmZpZyByZXNvbHV0aW9uIGNoYWluOgogKiAxLiBRdWVyeSBhdXRoX3Byb3ZpZGVyX2NvbmZpZyB0YWJsZSBmb3IgYW4gZW5hYmxlZCBwcm92aWRlcgogKiAyLiBJZiBEQiBjb25maWcgZXhpc3RzIOKGkiB1c2UgaXQgKGRlY3J5cHQgY2xpZW50U2VjcmV0KQogKiAzLiBJZiBubyBEQiBjb25maWcg4oaSIGZhbGwgYmFjayB0byBPSURDXyogZW52IHZhcnMKICogNC4gSWYgbmVpdGhlciDihpIgYXV0aCBpcyB1bmNvbmZpZ3VyZWQgKGdldEF1dGgoKSByZXR1cm5zIG51bGwsIEFVVEhfRElTQUJMRUQgaW1wbGllZCkKICoKICogSWRlbXBvdGVudCDigJQgc3Vic2VxdWVudCBjYWxscyByZXR1cm4gaW1tZWRpYXRlbHkgYWZ0ZXIgaW5pdGlhbGl6YXRpb24gY29tcGxldGVzLgogKi8KZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGluaXRBdXRoKCk6IFByb21pc2U8dm9pZD4gewogIGlmIChhdXRoSW5zdGFuY2UpIHJldHVybjsgLy8gQWxyZWFkeSBpbml0aWFsaXplZAogIGlmIChhdXRoSW5pdFByb21pc2UpIHsKICAgIGF3YWl0IGF1dGhJbml0UHJvbWlzZTsKICAgIHJldHVybjsKICB9CgogIGF1dGhJbml0UHJvbWlzZSA9IChhc3luYyAoKSA9PiB7CiAgICAvLyBHdWFyZDogcmVxdWlyZSBCRVRURVJfQVVUSF9TRUNSRVQgdW5sZXNzIGV4cGxpY2l0bHkgaW4gZGV2L2RlbW8gbW9kZQogICAgaWYgKCFCRVRURVJfQVVUSF9TRUNSRVQgJiYgcHJvY2Vzcy5lbnYuQVVUSF9ESVNBQkxFRCAhPT0gInRydWUiKSB7CiAgICAgIHRocm93IG5ldyBFcnJvcigKICAgICAgICAiW0ZBVEFMXSBCRVRURVJfQVVUSF9TRUNSRVQgZW52aXJvbm1lbnQgdmFyaWFibGUgaXMgcmVxdWlyZWQgd2hlbiBhdXRoIGlzIGVuYWJsZWQiCiAgICAgICk7CiAgICB9CgogICAgLy8gQVVUSF9ESVNBQkxFRD10cnVlIG1lYW5zIGRldi9kZW1vIG1vZGUg4oCUIHN0aWxsIGJ1aWxkIEJldHRlci1BdXRoIHdpdGggcGxhY2Vob2xkZXIKICAgIC8vIGNvbmZpZyBzbyBhdXRoLmhhbmRsZXIgZXhpc3RzIChtaWRkbGV3YXJlIGJ5cGFzc2VzIGl0IGFueXdheSkKICAgIGlmIChwcm9jZXNzLmVudi5BVVRIX0RJU0FCTEVEID09PSAidHJ1ZSIpIHsKICAgICAgY29uc29sZS53YXJuKCJbYXV0aF0gQVVUSF9ESVNBQkxFRD10cnVlIOKAlCBidWlsZGluZyBwbGFjZWhvbGRlciBhdXRoIGluc3RhbmNlIik7CiAgICAgIGF1dGhJbnN0YW5jZSA9IGJldHRlckF1dGgoewogICAgICAgIGRhdGFiYXNlOiBkcml6emxlQWRhcHRlcihnZXREYigpLCB7IHByb3ZpZGVyOiAicGciIH0pLAogICAgICAgIHNlY3JldDogQkVUVEVSX0FVVEhfU0VDUkVUISwKICAgICAgICBiYXNlVVJMOiBCRVRURVJfQVVUSF9VUkwsCiAgICAgICAgcmF0ZUxpbWl0OiB7CiAgICAgICAgICBlbmFibGVkOiB0cnVlLAogICAgICAgICAgbWF4OiAxMDAsCiAgICAgICAgICB3aW5kb3c6IDEwLAogICAgICAgICAgc3RvcmFnZTogIm1lbW9yeSIsCiAgICAgICAgICBjdXN0b21SdWxlczogewogICAgICAgICAgICAiL2dldC1zZXNzaW9uIjogZmFsc2UsCiAgICAgICAgICB9LAogICAgICAgIH0sCiAgICAgICAgcGx1Z2luczogWwogICAgICAgICAgZ2VuZXJpY09BdXRoKHsKICAgICAgICAgICAgY29uZmlnOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvdmlkZXJJZDogImF1dGhlbnRpayIsCiAgICAgICAgICAgICAgICBjbGllbnRJZDogInBsYWNlaG9sZGVyIiwKICAgICAgICAgICAgICAgIGNsaWVudFNlY3JldDogInBsYWNlaG9sZGVyIiwKICAgICAgICAgICAgICAgIGRpc2NvdmVyeVVybDogdW5kZWZpbmVkLAogICAgICAgICAgICAgICAgc2NvcGVzOiBbIm9wZW5pZCIsICJwcm9maWxlIiwgImVtYWlsIl0sCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgXSwKICAgICAgICAgIH0pLAogICAgICAgIF0sCiAgICAgICAgc2Vzc2lvbjogewogICAgICAgICAgZXhwaXJlc0luOiA2MCAqIDYwICogMjQgKiA3LAogICAgICAgICAgdXBkYXRlQWdlOiA2MCAqIDYwICogMjQsCiAgICAgICAgICBjb29raWVDYWNoZTogeyBlbmFibGVkOiBmYWxzZSB9LAogICAgICAgIH0sCiAgICAgICAgdHJ1c3RlZE9yaWdpbnM6IChwcm9jZXNzLmVudi5DT1JTX09SSUdJTiA/PyAiaHR0cDovL2xvY2FsaG9zdDo1MTczIikKICAgICAgICAgIC5zcGxpdCgiLCIpLm1hcCgocykgPT4gcy50cmltKCkpLmZpbHRlcihCb29sZWFuKSwKICAgICAgfSk7CiAgICAgIHJldHVybjsKICAgIH0KCiAgICAvLyBTdGVwIDE6IFRyeSB0byBsb2FkIGNvbmZpZyBmcm9tIERCLCB3aXRoIHJldHJ5LXdpdGgtYmFja29mZiBmb3IgdHJhbnNpZW50IEVDT05OUkVTRVQgKEdSTy0yNjUyKS4KICAgIC8vIEEgc2luZ2xlIGNvbm5lY3Rpb24gcmVzZXQgZHVyaW5nIGJvb3QgbXVzdCBub3QgYWJvcnQgaW5pdGlhbGl6YXRpb24uCiAgICBjb25zdCBkYiA9IGdldERiKCk7CiAgICBsZXQgZGJRdWVyeVJvd3M6ICh0eXBlb2YgYXV0aFByb3ZpZGVyQ29uZmlnLiRpbmZlclNlbGVjdClbXSA9IFtdOwogICAgbGV0IGRiQXR0ZW1wdCA9IDA7CiAgICB3aGlsZSAodHJ1ZSkgewogICAgICB0cnkgewogICAgICAgIGRiUXVlcnlSb3dzID0gYXdhaXQgZGIKICAgICAgICAgIC5zZWxlY3QoKQogICAgICAgICAgLmZyb20oYXV0aFByb3ZpZGVyQ29uZmlnKQogICAgICAgICAgLndoZXJlKGVxKGF1dGhQcm92aWRlckNvbmZpZy5lbmFibGVkLCB0cnVlKSkKICAgICAgICAgIC5saW1pdCgxKTsKICAgICAgICBicmVhazsKICAgICAgfSBjYXRjaCAoZXJyKSB7CiAgICAgICAgZGJBdHRlbXB0Kys7CiAgICAgICAgaWYgKGRiQXR0ZW1wdCA+PSA1KSB0aHJvdyBlcnI7CiAgICAgICAgY29uc3QgZGVsYXkgPSBNYXRoLm1pbigxMDAwICogMiAqKiAoZGJBdHRlbXB0IC0gMSksIDhfMDAwKTsKICAgICAgICBjb25zb2xlLndhcm4oYFthdXRoXSBEQiBxdWVyeSBhdHRlbXB0ICR7ZGJBdHRlbXB0fSBmYWlsZWQgKCR7ZXJyfSksIHJldHJ5aW5nIGluICR7ZGVsYXl9bXNgKTsKICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZSgocikgPT4gc2V0VGltZW91dChyLCBkZWxheSkpOwogICAgICB9CiAgICB9CiAgICBjb25zdCBbZGJDb25maWddID0gZGJRdWVyeVJvd3M7CgogICAgbGV0IHByb3ZpZGVyQ29uZmlnOiB7CiAgICAgIHByb3ZpZGVySWQ6IHN0cmluZzsKICAgICAgY2xpZW50SWQ6IHN0cmluZzsKICAgICAgY2xpZW50U2VjcmV0OiBzdHJpbmc7CiAgICAgIGlzc3VlclVybDogc3RyaW5nOwogICAgICBpbnRlcm5hbEJhc2VVcmw/OiBzdHJpbmc7CiAgICAgIHNjb3Blczogc3RyaW5nOwogICAgfTsKCiAgICBpZiAoZGJDb25maWcpIHsKICAgICAgLy8gU3RlcCAyOiBVc2UgREIgY29uZmlnIChkZWNyeXB0IGNsaWVudFNlY3JldCkKICAgICAgY29uc3QgZGVjcnlwdGVkU2VjcmV0ID0gZGVjcnlwdFNlY3JldChkYkNvbmZpZy5jbGllbnRTZWNyZXQpOwogICAgICBwcm92aWRlckNvbmZpZyA9IHsKICAgICAgICBwcm92aWRlcklkOiBkYkNvbmZpZy5wcm92aWRlcklkLAogICAgICAgIGNsaWVudElkOiBkYkNvbmZpZy5jbGllbnRJZCwKICAgICAgICBjbGllbnRTZWNyZXQ6IGRlY3J5cHRlZFNlY3JldCwKICAgICAgICBpc3N1ZXJVcmw6IGRiQ29uZmlnLmlzc3VlclVybCwKICAgICAgICBpbnRlcm5hbEJhc2VVcmw6IGRiQ29uZmlnLmludGVybmFsQmFzZVVybCA/PyB1bmRlZmluZWQsCiAgICAgICAgc2NvcGVzOiBkYkNvbmZpZy5zY29wZXMsCiAgICAgIH07CiAgICAgIGNvbnNvbGUubG9nKCJbYXV0aF0gVXNpbmcgREIgY29uZmlnIGZvciBwcm92aWRlcjoiLCBkYkNvbmZpZy5wcm92aWRlcklkKTsKICAgIH0gZWxzZSB7CiAgICAgIC8vIFN0ZXAgMzogRmFsbCBiYWNrIHRvIGVudiB2YXJzCiAgICAgIGNvbnN0IG9pZGNJc3N1ZXIgPSBwcm9jZXNzLmVudi5PSURDX0lTU1VFUjsKICAgICAgY29uc3Qgb2lkY0NsaWVudElkID0gcHJvY2Vzcy5lbnYuT0lEQ19DTElFTlRfSUQ7CiAgICAgIGNvbnN0IG9pZGNDbGllbnRTZWNyZXQgPSBwcm9jZXNzLmVudi5PSURDX0NMSUVOVF9TRUNSRVQ7CgogICAgICBpZiAoIW9pZGNJc3N1ZXIgfHwgIW9pZGNDbGllbnRJZCB8fCAhb2lkY0NsaWVudFNlY3JldCkgewogICAgICAgIC8vIFN0ZXAgNDogTmVpdGhlciBEQiBjb25maWcgbm9yIGVudiB2YXJzIOKAlCBhdXRoIGlzIHVuY29uZmlndXJlZAogICAgICAgIGNvbnNvbGUud2FybigKICAgICAgICAgICJbYXV0aF0gTm8gYXV0aCBwcm92aWRlciBjb25maWd1cmVkLiBTZXQgdXAgYXV0aF9wcm92aWRlcl9jb25maWcgaW4gREIgb3IgT0lEQ18qIGVudiB2YXJzLiIKICAgICAgICApOwogICAgICAgIHJldHVybjsgLy8gYXV0aEluc3RhbmNlIHN0YXlzIG51bGwg4oCUIEFVVEhfRElTQUJMRUQgbW9kZQogICAgICB9CgogICAgICBwcm92aWRlckNvbmZpZyA9IHsKICAgICAgICBwcm92aWRlcklkOiAiYXV0aGVudGlrIiwKICAgICAgICBjbGllbnRJZDogb2lkY0NsaWVudElkLAogICAgICAgIGNsaWVudFNlY3JldDogb2lkY0NsaWVudFNlY3JldCwKICAgICAgICBpc3N1ZXJVcmw6IG9pZGNJc3N1ZXIsCiAgICAgICAgaW50ZXJuYWxCYXNlVXJsOiBwcm9jZXNzLmVudi5PSURDX0lOVEVSTkFMX0JBU0UsCiAgICAgICAgc2NvcGVzOiAib3BlbmlkIHByb2ZpbGUgZW1haWwgcm9sZSIsCiAgICAgIH07CiAgICAgIGNvbnNvbGUubG9nKCJbYXV0aF0gVXNpbmcgZW52IHZhciBjb25maWcgKG5vIERCIGNvbmZpZyBmb3VuZCkiKTsKICAgIH0KCiAgICBjb25zdCBoYXNHb29nbGUgPSAhIShwcm9jZXNzLmVudi5HT09HTEVfQ0xJRU5UX0lEICYmIHByb2Nlc3MuZW52LkdPT0dMRV9DTElFTlRfU0VDUkVUKTsKICAgIGNvbnN0IGhhc0dpdEh1YiA9ICEhKHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR0lUSFVCX0NMSUVOVF9TRUNSRVQpOwoKICAgIGNvbnN0IGlzc3VlclVybE9iaiA9IG5ldyBVUkwocHJvdmlkZXJDb25maWcuaXNzdWVyVXJsKTsKICAgIGNvbnN0IGlzc3Vlckhvc3RuYW1lID0gaXNzdWVyVXJsT2JqLmhvc3RuYW1lOwoKICAgIGNvbnN0IGRpc2NvdmVyeVVybFN0ciA9IGAke3Byb3ZpZGVyQ29uZmlnLmlzc3VlclVybH0vLndlbGwta25vd24vb3BlbmlkLWNvbmZpZ3VyYXRpb25gOwogICAgbGV0IG9pZGNDb25maWc6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gPSB7fTsKICAgIHRyeSB7CiAgICAgIGNvbnN0IGRpc2NvdmVyeVJlcyA9IGF3YWl0IGZldGNoKGRpc2NvdmVyeVVybFN0ciwgewogICAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dCg1MDAwKSwKICAgICAgfSk7CiAgICAgIGlmIChkaXNjb3ZlcnlSZXMub2spIHsKICAgICAgICBjb25zdCBkaXNjb3ZlcnkgPSBhd2FpdCBkaXNjb3ZlcnlSZXMuanNvbigpIGFzIHsKICAgICAgICAgIGF1dGhvcml6YXRpb25fZW5kcG9pbnQ/OiBzdHJpbmc7CiAgICAgICAgICB0b2tlbl9lbmRwb2ludD86IHN0cmluZzsKICAgICAgICAgIHVzZXJpbmZvX2VuZHBvaW50Pzogc3RyaW5nOwogICAgICAgIH07CiAgICAgICAgY29uc3QgcmVwbGFjZUhvc3QgPSAodXJsOiBzdHJpbmcsIG5ld0hvc3Q6IHN0cmluZykgPT4gewogICAgICAgICAgdHJ5IHsKICAgICAgICAgICAgY29uc3QgcGFyc2VkID0gbmV3IFVSTCh1cmwpOwogICAgICAgICAgICBjb25zdCBuZXdQYXJzZWQgPSBuZXcgVVJMKG5ld0hvc3QpOwogICAgICAgICAgICByZXR1cm4gYCR7bmV3UGFyc2VkLm9yaWdpbn0ke3BhcnNlZC5wYXRobmFtZX0ke3BhcnNlZC5zZWFyY2h9YDsKICAgICAgICAgIH0gY2F0Y2ggewogICAgICAgICAgICByZXR1cm4gdXJsOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgY29uc3QgYXV0aHpVcmwgPSBkaXNjb3ZlcnkuYXV0aG9yaXphdGlvbl9lbmRwb2ludDsKICAgICAgICBjb25zdCB0b2tlblVybCA9IGRpc2NvdmVyeS50b2tlbl9lbmRwb2ludDsKICAgICAgICBjb25zdCB1c2VySW5mb1VybCA9IGRpc2NvdmVyeS51c2VyaW5mb19lbmRwb2ludDsKICAgICAgICBpZiAoYXV0aHpVcmwgJiYgdG9rZW5VcmwgJiYgdXNlckluZm9VcmwpIHsKICAgICAgICAgIGNvbnN0IGF1dGh6VXJsT2JqID0gbmV3IFVSTChhdXRoelVybCk7CiAgICAgICAgICAvLyBPbmx5IHZhbGlkYXRlIGF1dGhvcml6YXRpb25VcmwgaG9zdG5hbWUgYWdhaW5zdCBpc3N1ZXIg4oCUIHRva2VuL3VzZXJpbmZvCiAgICAgICAgICAvLyBtYXkgbGVnaXRpbWF0ZWx5IHVzZSBpbnRlcm5hbCBob3N0bmFtZXMgKE9JRENfSU5URVJOQUxfQkFTRSkgZm9yIHNlcnZlci10by1zZXJ2ZXIgY2FsbHMuCiAgICAgICAgICBpZiAoYXV0aHpVcmxPYmouaG9zdG5hbWUgIT09IGlzc3Vlckhvc3RuYW1lKSB7CiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigKICAgICAgICAgICAgICBgW0ZBVEFMXSBPSURDIGRpc2NvdmVyeSBVUkwgaG9zdG5hbWUgbWlzbWF0Y2g6IGV4cGVjdGVkICcke2lzc3Vlckhvc3RuYW1lfScgYnV0IGdvdCAnJHthdXRoelVybE9iai5ob3N0bmFtZX0nLiBUaGlzIG1heSBpbmRpY2F0ZSBhIG1hbi1pbi10aGUtbWlkZGxlIGF0dGFjay5gCiAgICAgICAgICAgICk7CiAgICAgICAgICB9CiAgICAgICAgICBvaWRjQ29uZmlnID0gewogICAgICAgICAgICBhdXRob3JpemF0aW9uVXJsOiBhdXRoelVybCwKICAgICAgICAgICAgdG9rZW5Vcmw6IHByb3ZpZGVyQ29uZmlnLmludGVybmFsQmFzZVVybAogICAgICAgICAgICAgID8gcmVwbGFjZUhvc3QodG9rZW5VcmwsIHByb3ZpZGVyQ29uZmlnLmludGVybmFsQmFzZVVybCkKICAgICAgICAgICAgICA6IHRva2VuVXJsLAogICAgICAgICAgICB1c2VySW5mb1VybDogcHJvdmlkZXJDb25maWcuaW50ZXJuYWxCYXNlVXJsCiAgICAgICAgICAgICAgPyByZXBsYWNlSG9zdCh1c2VySW5mb1VybCwgcHJvdmlkZXJDb25maWcuaW50ZXJuYWxCYXNlVXJsKQogICAgICAgICAgICAgIDogdXNlckluZm9VcmwsCiAgICAgICAgICB9OwogICAgICAgICAgY29uc29sZS5sb2coIlthdXRoXSBPSURDIGRpc2NvdmVyeSBzdWNjZXNzZnVsLCBwcm92aWRlcjoiLCBwcm92aWRlckNvbmZpZy5wcm92aWRlcklkKTsKICAgICAgICB9IGVsc2UgewogICAgICAgICAgY29uc29sZS53YXJuKCJbYXV0aF0gT0lEQyBkaXNjb3ZlcnkgbWlzc2luZyByZXF1aXJlZCBlbmRwb2ludHMsIHVzaW5nIGRpc2NvdmVyeVVybCBvbmx5Iik7CiAgICAgICAgfQogICAgICB9IGVsc2UgewogICAgICAgIGNvbnNvbGUud2FybihgW2F1dGhdIE9JREMgZGlzY292ZXJ5IGZhaWxlZCAoJHtkaXNjb3ZlcnlSZXMuc3RhdHVzfSksIHVzaW5nIGRpc2NvdmVyeVVybCBvbmx5YCk7CiAgICAgIH0KICAgIH0gY2F0Y2ggKGVycikgewogICAgICBjb25zb2xlLndhcm4oYFthdXRoXSBPSURDIGRpc2NvdmVyeSBmZXRjaCBmYWlsZWQ6ICR7ZXJyfSwgdXNpbmcgZGlzY292ZXJ5VXJsIG9ubHlgKTsKICAgIH0KCiAgICAvLyBCdWlsZCBCZXR0ZXItQXV0aCBpbnN0YW5jZSB1c2luZyByZXNvbHZlZCBjb25maWcKICAgIGF1dGhJbnN0YW5jZSA9IGJldHRlckF1dGgoewogICAgICBkYXRhYmFzZTogZHJpenpsZUFkYXB0ZXIoZGIsIHsKICAgICAgICBwcm92aWRlcjogInBnIiwKICAgICAgfSksCiAgICAgIHNlY3JldDogQkVUVEVSX0FVVEhfU0VDUkVULAogICAgICBiYXNlVVJMOiBCRVRURVJfQVVUSF9VUkwsCiAgICAgIHJhdGVMaW1pdDogewogICAgICAgIGVuYWJsZWQ6IHRydWUsCiAgICAgICAgbWF4OiAxMDAsCiAgICAgICAgd2luZG93OiAxMCwKICAgICAgICBzdG9yYWdlOiAibWVtb3J5IiwKICAgICAgICBjdXN0b21SdWxlczogewogICAgICAgICAgIi9nZXQtc2Vzc2lvbiI6IGZhbHNlLAogICAgICAgIH0sCiAgICAgIH0sCiAgICAgIGFjY291bnQ6IHsKICAgICAgICBhY2NvdW50TGlua2luZzogewogICAgICAgICAgZW5hYmxlZDogdHJ1ZSwKICAgICAgICAgIHRydXN0ZWRQcm92aWRlcnM6IFsiYXV0aGVudGlrIl0sCiAgICAgICAgfSwKICAgICAgICBzdG9yZVN0YXRlU3RyYXRlZ3k6ICJjb29raWUiIGFzIGNvbnN0LAogICAgICB9LAogICAgICBlbWFpbEFuZFBhc3N3b3JkOiB7CiAgICAgICAgZW5hYmxlZDogdHJ1ZSwKICAgICAgICBlbWFpbFZlcmlmaWNhdGlvbjogewogICAgICAgICAgc2VuZFZlcmlmaWNhdGlvbkVtYWlsOiBhc3luYyAoeyB1c2VyLCB1cmwgfTogeyB1c2VyOiB7IGVtYWlsOiBzdHJpbmcgfTsgdXJsOiBzdHJpbmcgfSkgPT4gewogICAgICAgICAgICBhd2FpdCBzZW5kRW1haWwoewogICAgICAgICAgICAgIHRvOiB1c2VyLmVtYWlsLAogICAgICAgICAgICAgIHN1YmplY3Q6ICJWZXJpZnkgeW91ciBHcm9vbUJvb2sgZW1haWwiLAogICAgICAgICAgICAgIHRleHQ6IGBDbGljayB0aGUgbGluayB0byB2ZXJpZnkgeW91ciBlbWFpbDogJHt1cmx9YCwKICAgICAgICAgICAgICBodG1sOiBgPHA+Q2xpY2sgdGhlIGxpbmsgdG8gdmVyaWZ5IHlvdXIgZW1haWw6PC9wPjxhIGhyZWY9IiR7dXJsfSI+JHt1cmx9PC9hPmAsCiAgICAgICAgICAgIH0pOwogICAgICAgICAgfSwKICAgICAgICB9LAogICAgICB9LAogICAgICBwbHVnaW5zOiBbCiAgICAgICAgZ2VuZXJpY09BdXRoKHsKICAgICAgICAgIGNvbmZpZzogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgcHJvdmlkZXJJZDogcHJvdmlkZXJDb25maWcucHJvdmlkZXJJZCwKICAgICAgICAgICAgICBjbGllbnRJZDogcHJvdmlkZXJDb25maWcuY2xpZW50SWQsCiAgICAgICAgICAgICAgY2xpZW50U2VjcmV0OiBwcm92aWRlckNvbmZpZy5jbGllbnRTZWNyZXQsCiAgICAgICAgICAgICAgZGlzY292ZXJ5VXJsOiBkaXNjb3ZlcnlVcmxTdHIsCiAgICAgICAgICAgICAgLi4uKE9iamVjdC5rZXlzKG9pZGNDb25maWcpLmxlbmd0aCA+IDAgPyBvaWRjQ29uZmlnIDoge30pLAogICAgICAgICAgICAgIHNjb3BlczogcHJvdmlkZXJDb25maWcuc2NvcGVzLnNwbGl0KCIgIikuZmlsdGVyKEJvb2xlYW4pLAogICAgICAgICAgICB9LAogICAgICAgICAgXSwKICAgICAgICB9KSwKICAgICAgXSwKICAgICAgc29jaWFsUHJvdmlkZXJzOiB7CiAgICAgICAgLi4uKGhhc0dvb2dsZSA/IHsKICAgICAgICAgIGdvb2dsZTogewogICAgICAgICAgICBjbGllbnRJZDogcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9JRCEsCiAgICAgICAgICAgIGNsaWVudFNlY3JldDogcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9TRUNSRVQhLAogICAgICAgICAgfSwKICAgICAgICB9IDoge30pLAogICAgICAgIC4uLihoYXNHaXRIdWIgPyB7CiAgICAgICAgICBnaXRodWI6IHsKICAgICAgICAgICAgY2xpZW50SWQ6IHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQhLAogICAgICAgICAgICBjbGllbnRTZWNyZXQ6IHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfU0VDUkVUISwKICAgICAgICAgIH0sCiAgICAgICAgfSA6IHt9KSwKICAgICAgfSwKICAgICAgc2Vzc2lvbjogewogICAgICAgIGV4cGlyZXNJbjogNjAgKiA2MCAqIDI0ICogNywgLy8gNyBkYXlzCiAgICAgICAgdXBkYXRlQWdlOiA2MCAqIDYwICogMjQsIC8vIDEgZGF5CiAgICAgICAgY29va2llQ2FjaGU6IHsKICAgICAgICAgIGVuYWJsZWQ6IHRydWUsCiAgICAgICAgICBtYXhBZ2U6IDUgKiA2MCwgLy8gNSBtaW51dGVzCiAgICAgICAgfSwKICAgICAgfSwKICAgICAgdHJ1c3RlZE9yaWdpbnM6IChwcm9jZXNzLmVudi5DT1JTX09SSUdJTiA/PyAiaHR0cDovL2xvY2FsaG9zdDo1MTczIikKICAgICAgICAuc3BsaXQoIiwiKS5tYXAoKHMpID0+IHMudHJpbSgpKS5maWx0ZXIoQm9vbGVhbiksCiAgICB9KTsKICB9KSgpOwoKICB0cnkgewogICAgYXdhaXQgYXV0aEluaXRQcm9taXNlOwogIH0gY2F0Y2ggKGVycikgewogICAgYXV0aEluaXRQcm9taXNlID0gbnVsbDsgLy8gYWxsb3cgcmV0cnkgb24gbmV4dCBjYWxsCiAgICB0aHJvdyBlcnI7CiAgfQp9Cg== \ No newline at end of file +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { genericOAuth } from "better-auth/plugins"; +import { getDb, authProviderConfig, eq } from "@groombook/db"; +import { decryptSecret } from "@groombook/db"; +import { sendEmail } from "../services/email.js"; + +const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET; +const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? "http://localhost:3000"; + +// Auth instance — initialized lazily via initAuth() +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let authInstance: any = null; +let authInitPromise: Promise | null = null; + +/** Returns the current auth instance. Throws if not yet initialized. */ +export function getAuth() { + if (!authInstance) { + throw new Error( + "Auth not initialized. Call initAuth() at startup before handling requests." + ); + } + return authInstance; +} + +/** Returns a promise that resolves when auth is initialized. */ +export function getAuthPromise() { + return authInitPromise; +} + +/** Returns which OAuth/social providers are configured via env vars. */ +export function getActiveProviders(): string[] { + const providers: string[] = []; + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + providers.push("google"); + } + if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + providers.push("github"); + } + if (process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID && process.env.OIDC_CLIENT_SECRET) { + providers.push("authentik"); + } + return providers; +} + +/** + * Re-initializes the Better-Auth instance after auth config changes. + * + * Clears both authInstance and authInitPromise, then calls initAuth() to + * re-read config from DB and build a fresh Better-Auth instance. + * Sessions are DB-backed and survive the re-init. + */ +export async function reinitAuth(): Promise { + authInstance = null; + authInitPromise = null; + await initAuth(); + console.log("[auth] Re-initialized auth instance after config change"); +} + +/** + * Initializes the Better-Auth instance. + * + * Config resolution chain: + * 1. Query auth_provider_config table for an enabled provider + * 2. If DB config exists → use it (decrypt clientSecret) + * 3. If no DB config → fall back to OIDC_* env vars + * 4. If neither → auth is unconfigured (getAuth() returns null, AUTH_DISABLED implied) + * + * Idempotent — subsequent calls return immediately after initialization completes. + */ +export async function initAuth(): Promise { + if (authInstance) return; // Already initialized + if (authInitPromise) { + await authInitPromise; + return; + } + + authInitPromise = (async () => { + // Guard: require BETTER_AUTH_SECRET unless explicitly in dev/demo mode + if (!BETTER_AUTH_SECRET && process.env.AUTH_DISABLED !== "true") { + throw new Error( + "[FATAL] BETTER_AUTH_SECRET environment variable is required when auth is enabled" + ); + } + + // AUTH_DISABLED=true means dev/demo mode — still build Better-Auth with placeholder + // config so auth.handler exists (middleware bypasses it anyway) + if (process.env.AUTH_DISABLED === "true") { + console.warn("[auth] AUTH_DISABLED=true — building placeholder auth instance"); + authInstance = betterAuth({ + database: drizzleAdapter(getDb(), { provider: "pg" }), + secret: BETTER_AUTH_SECRET!, + baseURL: BETTER_AUTH_URL, + rateLimit: { + enabled: true, + max: 100, + window: 10, + storage: "memory", + customRules: { + "/get-session": false, + }, + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: "authentik", + clientId: "placeholder", + clientSecret: "placeholder", + discoveryUrl: undefined, + scopes: ["openid", "profile", "email"], + }, + ], + }), + ], + session: { + expiresIn: 60 * 60 * 24 * 7, + updateAge: 60 * 60 * 24, + cookieCache: { enabled: false }, + }, + trustedOrigins: (process.env.CORS_ORIGIN ?? "http://localhost:5173") + .split(",").map((s) => s.trim()).filter(Boolean), + }); + return; + } + + // Step 1: Try to load config from DB, with retry-with-backoff for transient ECONNRESET (GRO-2652). + // A single connection reset during boot must not abort initialization. + const db = getDb(); + let dbQueryRows: (typeof authProviderConfig.$inferSelect)[] = []; + let dbAttempt = 0; + while (true) { + try { + dbQueryRows = await db + .select() + .from(authProviderConfig) + .where(eq(authProviderConfig.enabled, true)) + .limit(1); + break; + } catch (err) { + dbAttempt++; + if (dbAttempt >= 5) throw err; + const delay = Math.min(1000 * 2 ** (dbAttempt - 1), 8_000); + console.warn(`[auth] DB query attempt ${dbAttempt} failed (${err}), retrying in ${delay}ms`); + await new Promise((r) => setTimeout(r, delay)); + } + } + const [dbConfig] = dbQueryRows; + + let providerConfig: { + providerId: string; + clientId: string; + clientSecret: string; + issuerUrl: string; + internalBaseUrl?: string; + scopes: string; + }; + + if (dbConfig) { + // Step 2: Use DB config (decrypt clientSecret) + const decryptedSecret = decryptSecret(dbConfig.clientSecret); + providerConfig = { + providerId: dbConfig.providerId, + clientId: dbConfig.clientId, + clientSecret: decryptedSecret, + issuerUrl: dbConfig.issuerUrl, + internalBaseUrl: dbConfig.internalBaseUrl ?? undefined, + scopes: dbConfig.scopes, + }; + console.log("[auth] Using DB config for provider:", dbConfig.providerId); + } else { + // Step 3: Fall back to env vars + const oidcIssuer = process.env.OIDC_ISSUER; + const oidcClientId = process.env.OIDC_CLIENT_ID; + const oidcClientSecret = process.env.OIDC_CLIENT_SECRET; + + if (!oidcIssuer || !oidcClientId || !oidcClientSecret) { + // Step 4: Neither DB config nor env vars — auth is unconfigured + console.warn( + "[auth] No auth provider configured. Set up auth_provider_config in DB or OIDC_* env vars." + ); + return; // authInstance stays null — AUTH_DISABLED mode + } + + providerConfig = { + providerId: "authentik", + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuer, + internalBaseUrl: process.env.OIDC_INTERNAL_BASE, + scopes: "openid profile email role", + }; + console.log("[auth] Using env var config (no DB config found)"); + } + + const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); + const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); + + const issuerUrlObj = new URL(providerConfig.issuerUrl); + const issuerHostname = issuerUrlObj.hostname; + + const discoveryUrlStr = `${providerConfig.issuerUrl}/.well-known/openid-configuration`; + let oidcConfig: Record = {}; + try { + const discoveryRes = await fetch(discoveryUrlStr, { + signal: AbortSignal.timeout(5000), + }); + if (discoveryRes.ok) { + const discovery = await discoveryRes.json() as { + authorization_endpoint?: string; + token_endpoint?: string; + userinfo_endpoint?: string; + }; + const replaceHost = (url: string, newHost: string) => { + try { + const parsed = new URL(url); + const newParsed = new URL(newHost); + return `${newParsed.origin}${parsed.pathname}${parsed.search}`; + } catch { + return url; + } + }; + const authzUrl = discovery.authorization_endpoint; + const tokenUrl = discovery.token_endpoint; + const userInfoUrl = discovery.userinfo_endpoint; + if (authzUrl && tokenUrl && userInfoUrl) { + const authzUrlObj = new URL(authzUrl); + // Only validate authorizationUrl hostname against issuer — token/userinfo + // may legitimately use internal hostnames (OIDC_INTERNAL_BASE) for server-to-server calls. + if (authzUrlObj.hostname !== issuerHostname) { + throw new Error( + `[FATAL] OIDC discovery URL hostname mismatch: expected '${issuerHostname}' but got '${authzUrlObj.hostname}'. This may indicate a man-in-the-middle attack.` + ); + } + oidcConfig = { + authorizationUrl: authzUrl, + tokenUrl: providerConfig.internalBaseUrl + ? replaceHost(tokenUrl, providerConfig.internalBaseUrl) + : tokenUrl, + userInfoUrl: providerConfig.internalBaseUrl + ? replaceHost(userInfoUrl, providerConfig.internalBaseUrl) + : userInfoUrl, + }; + console.log("[auth] OIDC discovery successful, provider:", providerConfig.providerId); + } else { + console.warn("[auth] OIDC discovery missing required endpoints, using discoveryUrl only"); + } + } else { + console.warn(`[auth] OIDC discovery failed (${discoveryRes.status}), using discoveryUrl only`); + } + } catch (err) { + console.warn(`[auth] OIDC discovery fetch failed: ${err}, using discoveryUrl only`); + } + + // Build Better-Auth instance using resolved config + authInstance = betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + }), + secret: BETTER_AUTH_SECRET, + baseURL: BETTER_AUTH_URL, + rateLimit: { + enabled: true, + max: 100, + window: 10, + storage: "memory", + customRules: { + "/get-session": false, + }, + }, + account: { + accountLinking: { + enabled: true, + trustedProviders: ["authentik"], + }, + storeStateStrategy: "cookie" as const, + }, + emailAndPassword: { + enabled: true, + emailVerification: { + sendVerificationEmail: async ({ user, url }: { user: { email: string }; url: string }) => { + await sendEmail({ + to: user.email, + subject: "Verify your GroomBook email", + text: `Click the link to verify your email: ${url}`, + html: `

Click the link to verify your email:

${url}`, + }); + }, + }, + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: providerConfig.providerId, + clientId: providerConfig.clientId, + clientSecret: providerConfig.clientSecret, + discoveryUrl: discoveryUrlStr, + ...(Object.keys(oidcConfig).length > 0 ? oidcConfig : {}), + scopes: providerConfig.scopes.split(" ").filter(Boolean), + }, + ], + }), + ], + socialProviders: { + ...(hasGoogle ? { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + } : {}), + ...(hasGitHub ? { + github: { + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + }, + } : {}), + }, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 1 day + cookieCache: { + enabled: true, + maxAge: 5 * 60, // 5 minutes + }, + }, + trustedOrigins: (process.env.CORS_ORIGIN ?? "http://localhost:5173") + .split(",").map((s) => s.trim()).filter(Boolean), + }); + })(); + + try { + await authInitPromise; + } catch (err) { + authInitPromise = null; // allow retry on next call + throw err; + } +}