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