@geekapps/auth-nextjs 0.24.0 → 0.26.0
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.
- package/README.md +58 -4
- package/dist/actions.d.ts +18 -2
- package/dist/actions.js +44 -4
- package/dist/actions.js.map +1 -1
- package/dist/client.d.ts +75 -4
- package/dist/client.js +48 -10
- package/dist/client.js.map +1 -1
- package/dist/connections.d.ts +2 -1
- package/dist/connections.js +12 -21
- package/dist/connections.js.map +1 -1
- package/dist/middleware.js.map +1 -1
- package/dist/server.d.ts +23 -9
- package/dist/server.js +22 -8
- package/dist/server.js.map +1 -1
- package/dist/typegen.d.ts +8 -1
- package/dist/typegen.js +10 -4
- package/dist/typegen.js.map +1 -1
- package/package.json +5 -9
package/dist/connections.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/typegen.ts","../src/connections.ts","../src/server.ts"],"sourcesContent":["// Gerador de tipos das App Connections: manifesto da app alvo\n// (/.well-known/geekapps-app.json) → TypeScript. O OpenAPI, quando a app\n// declara um, ainda gera os `paths` do cliente antigo (createConnectionClient).\nimport { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nexport const DEFAULT_ISSUER = \"https://accounts-api.geekapps.com.br\";\nexport const DEFAULT_OUT_DIR = \"geekapps-connections\";\nconst DEFAULT_AUGMENT = \"@geekapps/auth-nextjs/connections\";\n\n/** Uma app conectável — o que o catálogo do auth-server devolve. */\nexport type CatalogApp = {\n targetAppId: string;\n name: string;\n /** Chave estável no tipo `GeekappsConnections` (ex: \"post\"). */\n slug: string;\n apiBaseUrl: string | null;\n /** `${apiBaseUrl}/.well-known/geekapps-app.json`, quando a app publica um manifesto. */\n manifestUrl?: string | null;\n /** OpenAPI declarado (opcional) — só para o cliente antigo. */\n openApiUrl?: string | null;\n};\n\nexport type Credentials = { issuer: string; clientId: string; clientSecret: string };\n\ntype JsonSchema = Record<string, unknown>;\n\n/** O manifesto publicado pelo plugin do @geekapps/auth-fastify. */\nexport type ConnectionManifest = {\n version: 1;\n appId: string | null;\n scopes: Record<string, { description: string }>;\n routes: Array<{\n method: string;\n path: string;\n scope: string;\n route: string;\n params: JsonSchema | null;\n query: JsonSchema | null;\n body: JsonSchema | null;\n response: JsonSchema | null;\n }>;\n};\n\n/** Apps conectáveis vistas por esta app (credenciais dela). */\nexport async function fetchCatalog({ issuer, clientId, clientSecret }: Credentials): Promise<CatalogApp[]> {\n const response = await fetch(`${issuer.replace(/\\/$/, \"\")}/connections/catalog`, {\n headers: { authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\")}` },\n cache: \"no-store\",\n });\n if (!response.ok) {\n throw new Error(`catálogo indisponível (${response.status}) — confira GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET`);\n }\n return (await response.json()) as CatalogApp[];\n}\n\nexport async function fetchManifest(manifestUrl: string): Promise<ConnectionManifest> {\n const response = await fetch(manifestUrl, { headers: { accept: \"application/json\" }, cache: \"no-store\" });\n if (!response.ok) throw new Error(`manifesto indisponível (${response.status})`);\n const manifest = (await response.json()) as ConnectionManifest;\n if (manifest?.version !== 1 || !manifest.scopes || !Array.isArray(manifest.routes)) {\n throw new Error(\"manifesto em formato inválido\");\n }\n return manifest;\n}\n\nconst HEADER = \"// Gerado pelo SDK do Geekapps Auth. Não edite à mão.\\n\\n\";\n\n/** \"messages.send\" → \"MessagesSend\". */\nfunction pascal(name: string) {\n return name\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => part[0]!.toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\nfunction properties(schema: JsonSchema | null) {\n return (schema?.properties as Record<string, JsonSchema> | undefined) ?? {};\n}\n\nfunction required(schema: JsonSchema | null) {\n return Array.isArray(schema?.required) ? (schema!.required as string[]) : [];\n}\n\n/**\n * O cliente recebe params, query e body num objeto só (o manifesto garante\n * que não há campo repetido): junta os três num schema de objeto.\n */\nfunction mergeInputSchema(route: ConnectionManifest[\"routes\"][number]): JsonSchema {\n const parts = [route.params, route.query, route.body];\n // Path params são sempre obrigatórios, mesmo sem `required` no schema.\n const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]!);\n return {\n type: \"object\",\n properties: Object.assign({}, ...parts.map(properties)),\n required: [...new Set([...pathParams, ...parts.flatMap(required)])],\n additionalProperties: false,\n };\n}\n\n/**\n * Tipos de um manifesto. Usa `json-schema-to-typescript` (import dinâmico:\n * dependência opcional, só de desenvolvimento — sem ela, a geração não roda).\n */\nexport async function generateManifestTypes(app: Pick<CatalogApp, \"slug\" | \"targetAppId\">, manifest: ConnectionManifest) {\n const { compile } = await import(\"json-schema-to-typescript\");\n const options = { bannerComment: \"\", additionalProperties: false, format: false, declareExternallyReferenced: true } as const;\n\n const blocks: string[] = [];\n const routeEntries: string[] = [];\n for (const route of manifest.routes) {\n const base = pascal(route.route);\n const input = `${base}Input`;\n const output = `${base}Output`;\n blocks.push(await compile(mergeInputSchema(route) as never, input, options));\n blocks.push(\n route.response\n ? await compile({ ...route.response, title: undefined } as never, output, options)\n : `export type ${output} = unknown;\\n`,\n );\n routeEntries.push(\n ` ${JSON.stringify(route.route)}: {\\n` +\n ` scope: ${JSON.stringify(route.scope)};\\n` +\n ` method: ${JSON.stringify(route.method)};\\n` +\n ` path: ${JSON.stringify(route.path)};\\n` +\n ` input: ${input};\\n` +\n ` output: ${output};\\n` +\n ` };`,\n );\n }\n\n const scopeIds = Object.keys(manifest.scopes);\n const scopeDocs = scopeIds\n .map((id) => ` ${JSON.stringify(id)}: ${JSON.stringify(manifest.scopes[id]!.description)};`)\n .join(\"\\n\");\n\n return `${HEADER}/** Escopos que esta app aceita em conexões. */\nexport type Scope = ${scopeIds.length ? scopeIds.map((id) => JSON.stringify(id)).join(\" | \") : \"never\"};\n\n/** Descrição de cada escopo, como aparece no consent. */\nexport interface ScopeDescriptions {\n${scopeDocs}\n}\n\n/** Rotas liberadas por escopo, pelo nome usado no cliente (\\`post.messages.send(...)\\`). */\nexport interface Routes {\n${routeEntries.join(\"\\n\")}\n}\n\nexport interface App {\n slug: ${JSON.stringify(app.slug)};\n targetAppId: ${JSON.stringify(app.targetAppId)};\n scopes: Scope;\n routes: Routes;\n}\n\n${blocks.join(\"\\n\")}`;\n}\n\n/** Tipos de um OpenAPI, no formato do openapi-typescript (cliente antigo). */\nasync function generateOpenApiTypes(openApiUrl: string) {\n const { default: openapiTS, astToString } = await import(\"openapi-typescript\");\n return HEADER + astToString(await openapiTS(new URL(openApiUrl)));\n}\n\ntype Generated = { app: CatalogApp; manifest: boolean; openApi: boolean };\n\n/** `index.d.ts`: liga cada app ao `GeekappsConnections` do módulo do SDK. */\nfunction renderIndex(apps: Generated[], augment: string) {\n const imports = apps\n .flatMap(({ app, manifest, openApi }, i) => [\n manifest ? `import type { App as App${i} } from \"./${app.slug}\";` : null,\n openApi ? `import type { paths as paths${i} } from \"./${app.slug}.openapi\";` : null,\n ])\n .filter(Boolean)\n .join(\"\\n\");\n const members = apps\n .map(({ app, manifest, openApi }, i) => {\n const fields = [manifest ? `app: App${i};` : null, openApi ? `paths: paths${i};` : null].filter(Boolean).join(\" \");\n return ` ${JSON.stringify(app.slug)}: { ${fields} };`;\n })\n .join(\"\\n\");\n return `${HEADER}${imports}\n\ndeclare module ${JSON.stringify(augment)} {\n interface GeekappsConnections {\n${members}\n }\n}\n\nexport {};\n`;\n}\n\nexport type GenerateResult = {\n apps: string[];\n /** Apps do catálogo cujo manifesto/OpenAPI não pôde ser baixado/convertido. */\n failed: Array<{ name: string; reason: string }>;\n /** Com `check`: arquivos que estariam diferentes. */\n stale: string[];\n};\n\n/**\n * Gerador de tipos das App Connections, embutido no SDK (sem pacote à parte).\n * Roda sozinho em desenvolvimento; para o CI, chame com `check: true`.\n *\n * Para cada app conectável escreve em `outDir`:\n * • `<slug>.d.ts` — a partir do manifesto (escopos, rotas e tipos de\n * entrada/saída de cada rota), requer `json-schema-to-typescript`;\n * • `<slug>.openapi.d.ts` — a partir do OpenAPI, se a app declarar um\n * (só para o `createConnectionClient` antigo), requer `openapi-typescript`;\n * • `index.d.ts`, que estende `GeekappsConnections` do módulo `augment`.\n * Com `check`, não escreve nada: só diz o que estaria diferente. Uma app cuja\n * busca falha mantém os arquivos anteriores.\n */\nexport async function generateConnectionTypes(params: {\n credentials: Credentials;\n outDir?: string;\n /** Módulo que recebe o `GeekappsConnections`. Padrão: \"@geekapps/auth-nextjs/connections\". */\n augment?: string;\n check?: boolean;\n}): Promise<GenerateResult> {\n const outDir = resolve(process.cwd(), params.outDir ?? DEFAULT_OUT_DIR);\n const catalog = (await fetchCatalog(params.credentials)).sort((a, b) => a.slug.localeCompare(b.slug));\n\n const files = new Map<string, string>();\n const result: GenerateResult = { apps: [], failed: [], stale: [] };\n const generated: Generated[] = [];\n\n // Mantém o que já existia, para a app não perder os tipos por uma falha de rede.\n const keepExisting = (file: string) => {\n const existing = join(outDir, file);\n if (!existsSync(existing)) return false;\n files.set(file, readFileSync(existing, \"utf-8\"));\n return true;\n };\n\n for (const app of catalog) {\n const entry: Generated = { app, manifest: false, openApi: false };\n\n if (app.manifestUrl) {\n const file = `${app.slug}.d.ts`;\n try {\n files.set(file, await generateManifestTypes(app, await fetchManifest(app.manifestUrl)));\n entry.manifest = true;\n } catch (error) {\n result.failed.push({ name: app.name, reason: `manifesto: ${(error as Error).message}` });\n entry.manifest = keepExisting(file);\n }\n }\n\n if (app.openApiUrl) {\n const file = `${app.slug}.openapi.d.ts`;\n try {\n files.set(file, await generateOpenApiTypes(app.openApiUrl));\n entry.openApi = true;\n } catch (error) {\n result.failed.push({ name: app.name, reason: `openapi: ${(error as Error).message}` });\n entry.openApi = keepExisting(file);\n }\n }\n\n if (entry.manifest || entry.openApi) generated.push(entry);\n }\n result.apps = generated.map(({ app }) => app.name);\n files.set(\"index.d.ts\", renderIndex(generated, params.augment ?? DEFAULT_AUGMENT));\n\n const current = existsSync(outDir) ? new Set(readdirSync(outDir).filter((f) => f.endsWith(\".d.ts\"))) : new Set<string>();\n\n if (params.check) {\n for (const [file, content] of files) {\n const path = join(outDir, file);\n if (!existsSync(path) || readFileSync(path, \"utf-8\") !== content) result.stale.push(file);\n }\n for (const file of current) if (!files.has(file)) result.stale.push(file);\n return result;\n }\n\n mkdirSync(outDir, { recursive: true });\n for (const [file, content] of files) {\n const path = join(outDir, file);\n if (!existsSync(path) || readFileSync(path, \"utf-8\") !== content) writeFileSync(path, content, \"utf-8\");\n }\n // Apps que deixaram o catálogo não devem deixar tipos órfãos.\n for (const file of current) if (!files.has(file)) rmSync(join(outDir, file));\n\n return result;\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { cookies } from \"next/headers\";\nimport type { Client, ClientOptions } from \"openapi-fetch\";\nimport { redirect } from \"next/navigation\";\nimport { GEEKAPPS_AUTH_SERVER_URL } from \"./server\";\n\n/**\n * App Connections — agir como um usuário numa organização de OUTRA Application.\n *\n * Uma conexão liga um USUÁRIO desta app (X) a uma organização da app alvo (Z)\n * de que ele já é membro. Ele a aprova uma vez, no consent; o auth-server\n * devolve a esta app um API token (`gka_…`). Daí em diante esta app guarda o\n * token e o usa quando quiser — com ou sem o usuário logado, num cron, num\n * workflow — trocando-o por um JWT curto que age COMO o usuário naquela\n * organização (`getConnectionAccessToken`). As permissions são as do usuário\n * na organização, hoje: mudou o role dele lá, a conexão muda junto; saiu da\n * organização, ela para.\n *\n * ⚠️ ATENÇÃO — o API token é uma credencial de longa duração.\n * • Guarde-o cifrado, associado ao usuário que aprovou a conexão.\n * • Use-o só para agir em nome DESSE usuário. Usar o token de A para atender\n * um pedido de B dá a B o acesso de A à organização alvo.\n * • Restringir quem pode disparar uma chamada, e o que ela pode pedir, é\n * responsabilidade desta app: o auth-server não tem como saber.\n * • Vazou? Revogue no dashboard do Geekapps Auth ou gire com `rotateConnectionToken`.\n */\n\nexport type AppConnectionsConfig = {\n issuer?: string;\n /** client_id/secret desta app (a consumidora, X). */\n clientId?: string;\n clientSecret?: string;\n};\n\n/**\n * Preenchida por declaration merging no `geekapps-connections/index.d.ts` que\n * o SDK gera (em desenvolvimento) ou `generateConnectionTypes()` escreve: uma\n * chave por app conectável (o slug do catálogo), com `app` (escopos, rotas e\n * tipos, do manifesto) e, se a app declarar um OpenAPI, `paths`. Vazia aqui\n * de propósito — quem sabe quais apps existem é o auth-server.\n */\nexport interface GeekappsConnections {}\n\n/**\n * O que deu errado numa chamada de conexão:\n * • `missing_scope` — a conexão não tem o escopo da rota (peça de novo com `connectApp`);\n * • `connection_revoked` — revogada, token inválido ou o usuário saiu da organização;\n * • `connection_expired` — passou da validade escolhida no consent;\n * • `validation_error` — a app alvo recusou a entrada (400/422);\n * • `network_error` — não deu para falar com o auth-server ou a app alvo;\n * • `unknown` — o resto (veja `status`, `reason` e `body`).\n */\nexport type AppConnectionErrorCode =\n | \"missing_scope\"\n | \"connection_revoked\"\n | \"connection_expired\"\n | \"validation_error\"\n | \"network_error\"\n | \"unknown\";\n\n/** Erro de App Connection — `code` dá pra tratar; `reason` é o código cru do servidor. */\nexport class AppConnectionError extends Error {\n readonly status: number;\n readonly code: AppConnectionErrorCode;\n /** Código de erro como o servidor mandou (ex: \"connection_inactive\", \"invalid_state\"). */\n readonly reason: string | undefined;\n /** Escopo que faltou, quando `code` é \"missing_scope\". */\n readonly scope: string | undefined;\n readonly body: unknown;\n\n constructor(message: string, status: number, reason: string | undefined, body: unknown) {\n super(message);\n this.name = \"AppConnectionError\";\n this.status = status;\n this.reason = reason;\n this.code = toErrorCode(status, reason);\n this.body = body;\n this.scope =\n body && typeof body === \"object\" && typeof (body as { scope?: unknown }).scope === \"string\"\n ? (body as { scope: string }).scope\n : undefined;\n }\n}\n\nfunction toErrorCode(status: number, reason: string | undefined): AppConnectionErrorCode {\n switch (reason) {\n case \"missing_scope\":\n case \"app_connection_not_allowed\":\n return \"missing_scope\";\n case \"connection_expired\":\n return \"connection_expired\";\n case \"connection_inactive\":\n case \"connection_revoked\":\n case \"invalid_token\":\n case \"membership_required\":\n return \"connection_revoked\";\n case \"network_error\":\n return \"network_error\";\n }\n if (status === 400 || status === 422) return \"validation_error\";\n return \"unknown\";\n}\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction resolveConfig(config: AppConnectionsConfig) {\n const issuer = (\n config.issuer ??\n env(\"GEEKAPPS_AUTH_SERVER_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_SERVER_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL\n ).replace(/\\/$/, \"\");\n\n return {\n issuer,\n clientId: config.clientId ?? env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\",\n clientSecret: config.clientSecret ?? env(\"GEEKAPPS_AUTH_CLIENT_SECRET\") ?? \"\",\n };\n}\n\nfunction requireCredentials(config: AppConnectionsConfig) {\n const resolved = resolveConfig(config);\n if (!resolved.clientId || !resolved.clientSecret) {\n throw new Error(\"App Connections: defina GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET.\");\n }\n return resolved;\n}\n\nfunction errorCode(body: unknown) {\n return body && typeof body === \"object\" && typeof (body as { error?: unknown }).error === \"string\"\n ? (body as { error: string }).error\n : undefined;\n}\n\n// ─── Conectar esta app a outra (consent) ────────────────────────────────────\n\nconst CONNECT_COOKIES = {\n verifier: \"gk_conn_pkce\",\n state: \"gk_conn_state\",\n returnTo: \"gk_conn_return\",\n redirectUri: \"gk_conn_redirect\",\n} as const;\n\n// ─── Tipos derivados do que o typegen gerou ─────────────────────────────────\n\n/** Slugs das apps conectáveis (do catálogo); sem tipos gerados, qualquer string. */\nexport type ConnectionAppSlug = [keyof GeekappsConnections] extends [never] ? string : keyof GeekappsConnections & string;\n\n/** Manifesto tipado de uma app (escopos e rotas), quando o typegen gerou. */\nexport type ConnectionAppOf<K> = K extends keyof GeekappsConnections\n ? GeekappsConnections[K] extends { app: infer A } ? A : never\n : never;\n\n/** Escopos aceitos por uma app; sem tipos gerados, string. */\nexport type ConnectionScopeOf<K> = [ConnectionAppOf<K>] extends [never]\n ? string\n : ConnectionAppOf<K> extends { scopes: infer S extends string } ? S : string;\n\ntype ConnectAppTarget<K extends ConnectionAppSlug> =\n | {\n /** Slug da app ALVO no catálogo (ex: \"post\"), com autocomplete pelos tipos gerados. */\n app: K;\n targetAppId?: undefined;\n /** Escopos da app alvo que esta conexão vai pedir — o usuário vê a descrição de cada um no consent. */\n scopes?: ReadonlyArray<ConnectionScopeOf<K>>;\n }\n | {\n /** client_id da app ALVO (a que vai ser acessada). */\n targetAppId: string;\n app?: undefined;\n scopes?: readonly string[];\n };\n\nexport type ConnectAppParams<K extends ConnectionAppSlug = ConnectionAppSlug> = ConnectAppTarget<K> & {\n /** URL desta app que recebe o retorno do consent (rota que chama `completeAppConnection`). Precisa estar nos redirect URIs desta app. */\n redirectUri: string;\n /** Para onde `completeAppConnection` manda o usuário depois (padrão \"/\"). */\n returnTo?: string;\n /** Nome desta app exibido no consent. */\n displayName?: string;\n config?: AppConnectionsConfig;\n};\n\n/**\n * Inicia a conexão desta app com `targetAppId`: guarda PKCE/state em cookies\n * httpOnly e redireciona para o consent, onde o usuário escolhe a organização\n * e a validade. Chame de um Server Action ou Route Handler — não retorna (o\n * `redirect` do Next interrompe a execução).\n */\nexport async function connectApp<K extends ConnectionAppSlug>(params: ConnectAppParams<K>): Promise<never> {\n const { issuer, clientId } = resolveConfig(params.config ?? {});\n if (!clientId) {\n throw new Error(\"App Connections: defina GEEKAPPS_AUTH_CLIENT_ID — é o client_id desta app, a que pede a conexão.\");\n }\n const targetAppId = params.targetAppId ?? (await resolveAppSlug(params.app as string, params.config ?? {}));\n\n const codeVerifier = randomBytes(64).toString(\"base64url\");\n const state = randomBytes(24).toString(\"base64url\");\n\n const store = await cookies();\n const options = { httpOnly: true, secure: true, sameSite: \"lax\" as const, path: \"/\", maxAge: 600 };\n store.set(CONNECT_COOKIES.verifier, codeVerifier, options);\n store.set(CONNECT_COOKIES.state, state, options);\n store.set(CONNECT_COOKIES.returnTo, params.returnTo ?? \"/\", options);\n store.set(CONNECT_COOKIES.redirectUri, params.redirectUri, options);\n\n const url = new URL(`${issuer}/authorize`);\n url.searchParams.set(\"client_id\", clientId);\n url.searchParams.set(\"redirect_uri\", params.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", \"openid\");\n url.searchParams.set(\"state\", state);\n url.searchParams.set(\"code_challenge\", createHash(\"sha256\").update(codeVerifier).digest(\"base64url\"));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"target_app_id\", targetAppId);\n if (params.scopes && params.scopes.length > 0) {\n url.searchParams.set(\"connection_scopes\", [...new Set(params.scopes)].join(\" \"));\n }\n // O servidor já exige isso para conexões; pedir explícito deixa a intenção clara.\n url.searchParams.set(\"prompt\", \"select_account consent\");\n if (params.displayName) url.searchParams.set(\"connection_display_name\", params.displayName);\n\n redirect(url.toString());\n}\n\n/** A conexão recém-criada. O `token` só aparece aqui, uma única vez. */\nexport type AppConnectionToken = {\n /** API token (`gka_…`). Guarde-o cifrado, associado ao usuário que aprovou. */\n token: string;\n connectionId: string;\n /** null = sem expiração. */\n expiresAt: string | null;\n /** Organização da app alvo em que a conexão age. */\n organizationId: string;\n targetAppId: string;\n};\n\n/**\n * Fecha o fluxo de `connectApp`. Chame no callback com o `code` e o `state`\n * da query; devolve o `returnTo` e a conexão, com o API token — a ÚNICA vez em\n * que ele aparece, então persista-o antes de redirecionar. Lança\n * AppConnectionError se o state não bater ou o auth-server recusar.\n */\nexport async function completeAppConnection(params: {\n code: string;\n state: string;\n config?: AppConnectionsConfig;\n}): Promise<{ returnTo: string; connection: AppConnectionToken }> {\n const { issuer, clientId, clientSecret } = requireCredentials(params.config ?? {});\n\n const store = await cookies();\n const read = (name: string) => store.get(name)?.value;\n const expectedState = read(CONNECT_COOKIES.state);\n const codeVerifier = read(CONNECT_COOKIES.verifier);\n const redirectUri = read(CONNECT_COOKIES.redirectUri);\n const returnTo = read(CONNECT_COOKIES.returnTo) ?? \"/\";\n\n for (const name of Object.values(CONNECT_COOKIES)) store.set(name, \"\", { maxAge: 0, path: \"/\" });\n\n if (!expectedState || !codeVerifier || !redirectUri || params.state !== expectedState) {\n throw new AppConnectionError(\"App Connections: estado inválido — inicie a conexão novamente.\", 400, \"invalid_state\", null);\n }\n\n const response = await fetch(`${issuer}/oauth/token`, {\n method: \"POST\",\n headers: {\n authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\")}`,\n \"content-type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code: params.code,\n redirect_uri: redirectUri,\n code_verifier: codeVerifier,\n }),\n cache: \"no-store\",\n });\n\n const body = (await response.json().catch(() => null)) as {\n connection_token?: string;\n connection_id?: string;\n connection_expires_at?: string | null;\n connection_organization_id?: string;\n connection_target_app_id?: string;\n } | null;\n\n if (!response.ok || !body?.connection_token) {\n const code = errorCode(body);\n throw new AppConnectionError(\n `App Connections: não foi possível concluir a conexão (${response.status}${code ? ` ${code}` : \"\"})`,\n response.status,\n code,\n body,\n );\n }\n\n return {\n returnTo,\n connection: {\n token: body.connection_token,\n connectionId: body.connection_id!,\n expiresAt: body.connection_expires_at ?? null,\n organizationId: body.connection_organization_id!,\n targetAppId: body.connection_target_app_id!,\n },\n };\n}\n\n// ─── Usar a conexão ─────────────────────────────────────────────────────────\n\nconst _catalogCache: { value: Array<{ targetAppId: string; slug: string }> | null; fetchedAt: number } = {\n value: null,\n fetchedAt: 0,\n};\n\n/** slug do catálogo → client_id da app alvo (cache de 5 min). */\nasync function resolveAppSlug(slug: string, config: AppConnectionsConfig): Promise<string> {\n if (!_catalogCache.value || Date.now() - _catalogCache.fetchedAt > 5 * 60_000) {\n const { issuer, clientId, clientSecret } = requireCredentials(config);\n const { fetchCatalog } = await import(\"./typegen\");\n _catalogCache.value = await fetchCatalog({ issuer, clientId, clientSecret });\n _catalogCache.fetchedAt = Date.now();\n }\n const app = _catalogCache.value.find((item) => item.slug === slug);\n if (!app) throw new Error(`App Connections: a app \"${slug}\" não está no catálogo do Geekapps Auth.`);\n return app.targetAppId;\n}\n\n/** Resultado de `getConnectionAccessToken`. */\nexport type AppConnectionAccessToken = {\n /** JWT curto: use em `Authorization: Bearer` contra a API da app alvo. */\n accessToken: string;\n /** Segundos até o JWT vencer. */\n expiresIn: number;\n organizationId: string;\n targetAppId: string;\n /** apiBaseUrl da app alvo, quando ela declarou uma. */\n targetApiBaseUrl: string | null;\n /** URL do OpenAPI da app alvo, quando ela declarou uma — fonte dos tipos. */\n targetOpenApiUrl: string | null;\n};\n\nconst _jwtCache = new Map<string, { value: AppConnectionAccessToken; expiresAt: number }>();\n\nasync function postToAuthServer<T>(path: string, connectionToken: string, config: AppConnectionsConfig): Promise<T> {\n const { issuer, clientId, clientSecret } = requireCredentials(config);\n\n const response = await fetch(`${issuer}${path}`, {\n method: \"POST\",\n headers: {\n authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\")}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({ connection_token: connectionToken }),\n cache: \"no-store\",\n });\n\n const body = await response.json().catch(() => null);\n if (!response.ok) {\n const code = errorCode(body);\n throw new AppConnectionError(\n `App Connections: ${path} falhou (${response.status}${code ? ` ${code}` : \"\"})`,\n response.status,\n code,\n body,\n );\n }\n return body as T;\n}\n\n/**\n * Troca o API token por um JWT curto que age como o usuário na organização da\n * conexão. Não exige ninguém logado. O resultado fica em cache até perto de\n * vencer, então pode ser chamado a cada requisição.\n *\n * Erros possíveis (`AppConnectionError.code`): `invalid_token`,\n * `connection_inactive` (revogada), `connection_expired`,\n * `membership_required` (o usuário saiu da organização).\n */\nexport async function getConnectionAccessToken(params: {\n /** O API token guardado por esta app. */\n connectionToken: string;\n config?: AppConnectionsConfig;\n}): Promise<AppConnectionAccessToken> {\n const cached = _jwtCache.get(params.connectionToken);\n if (cached && cached.expiresAt > Date.now() + 30_000) return cached.value;\n\n const data = await postToAuthServer<{\n access_token: string;\n expires_in: number;\n organization_id: string;\n target_app_id: string;\n target_api_base_url: string | null;\n target_openapi_url: string | null;\n }>(\"/connections/token\", params.connectionToken, params.config ?? {});\n\n const value: AppConnectionAccessToken = {\n accessToken: data.access_token,\n expiresIn: data.expires_in,\n organizationId: data.organization_id,\n targetAppId: data.target_app_id,\n targetApiBaseUrl: data.target_api_base_url,\n targetOpenApiUrl: data.target_openapi_url,\n };\n _jwtCache.set(params.connectionToken, { value, expiresAt: Date.now() + data.expires_in * 1000 });\n return value;\n}\n\n/**\n * @deprecated Use `connections.<app>(token).<rota>(...)`, tipado pelo manifesto.\n *\n * Atalho: um `fetch` contra a API da app alvo já com o JWT da conexão. `path`\n * é relativo ao `apiBaseUrl` da app alvo (ou uma URL absoluta).\n */\nexport async function connectionFetch(params: {\n connectionToken: string;\n path: string;\n init?: RequestInit;\n config?: AppConnectionsConfig;\n}): Promise<Response> {\n const { accessToken, targetApiBaseUrl } = await getConnectionAccessToken({\n connectionToken: params.connectionToken,\n config: params.config,\n });\n\n const isAbsolute = /^https?:\\/\\//i.test(params.path);\n if (!isAbsolute && !targetApiBaseUrl) {\n throw new Error(\"App Connections: a app alvo não declarou apiBaseUrl — passe `path` como URL absoluta.\");\n }\n\n const headers = new Headers(params.init?.headers);\n headers.set(\"authorization\", `Bearer ${accessToken}`);\n\n return fetch(isAbsolute ? params.path : new URL(params.path, targetApiBaseUrl!), { ...params.init, headers });\n}\n\n/**\n * @deprecated Use `connections.<app>(token).<rota>(...)`, tipado pelo manifesto\n * da app alvo (escopos e rotas), sem depender de OpenAPI nem de `openapi-fetch`.\n *\n * Cliente tipado para a API da app alvo, a partir do OpenAPI dela: o JWT da\n * conexão é obtido, renovado e injetado a cada chamada.\n *\n * Os tipos de TODAS as apps conectáveis vêm de uma vez: em desenvolvimento o\n * SDK baixa o OpenAPI de cada uma ao subir e escreve `geekapps-connections/`\n * (requer `openapi-typescript` como devDependency; para o CI,\n * `generateConnectionTypes({ check: true })` de `@geekapps/auth-nextjs/typegen`). Daí basta nomear a app:\n *\n * const billing = await createConnectionClient({ app: \"billing\", connectionToken });\n * const { data } = await billing.GET(\"/charges\", { params: { query: { limit: 10 } } });\n *\n * `app` só escolhe os tipos; a app alvo de verdade é a do token. Sem tipos\n * gerados, passe os `paths` você mesmo: `createConnectionClient<paths>({ connectionToken })`.\n *\n * Requer o pacote `openapi-fetch` instalado na sua app.\n */\nexport async function createConnectionClient<App extends keyof GeekappsConnections>(\n params: ConnectionClientParams & { app: App },\n): Promise<Client<(GeekappsConnections[App] extends { paths: infer P } ? P : {}) & {}>>;\nexport async function createConnectionClient<Paths extends {}>(\n params: ConnectionClientParams & { app?: undefined },\n): Promise<Client<Paths>>;\nexport async function createConnectionClient(params: ConnectionClientParams & { app?: string }): Promise<Client<{}>> {\n const { default: createClient } = await import(\"openapi-fetch\").catch(() => {\n throw new Error(\"App Connections: instale `openapi-fetch` para usar createConnectionClient.\");\n });\n\n const { targetApiBaseUrl } = await getConnectionAccessToken({\n connectionToken: params.connectionToken,\n config: params.config,\n });\n const baseUrl = params.baseUrl ?? targetApiBaseUrl;\n if (!baseUrl) {\n throw new Error(\"App Connections: a app alvo não declarou apiBaseUrl — passe `baseUrl`.\");\n }\n\n return createClient<{}>({\n ...params.options,\n baseUrl,\n // O JWT vence em minutos: pega do cache (ou renova) a cada requisição.\n fetch: async (request) => {\n const { accessToken } = await getConnectionAccessToken({\n connectionToken: params.connectionToken,\n config: params.config,\n });\n request.headers.set(\"authorization\", `Bearer ${accessToken}`);\n return fetch(request);\n },\n });\n}\n\ntype ConnectionClientParams = {\n /** O API token guardado por esta app. */\n connectionToken: string;\n /** Sobrescreve o apiBaseUrl declarado pela app alvo. */\n baseUrl?: string;\n /** Demais opções do openapi-fetch (headers, querySerializer…). */\n options?: Omit<ClientOptions, \"baseUrl\" | \"fetch\">;\n config?: AppConnectionsConfig;\n};\n\n/**\n * Gira o API token: o antigo deixa de valer na hora e o novo é devolvido (a\n * validade da conexão não muda). Persista o novo no lugar do antigo.\n */\nexport async function rotateConnectionToken(params: {\n connectionToken: string;\n config?: AppConnectionsConfig;\n}): Promise<{ token: string; expiresAt: string | null }> {\n const data = await postToAuthServer<{ connection_token: string; expires_at: string | null }>(\n \"/connections/token/rotate\",\n params.connectionToken,\n params.config ?? {},\n );\n _jwtCache.delete(params.connectionToken);\n return { token: data.connection_token, expiresAt: data.expires_at };\n}\n\n// ─── Cliente tipado pelo manifesto da app alvo ──────────────────────────────\n\ntype ManifestRoute = {\n method: string;\n path: string;\n scope: string;\n route: string;\n params: Record<string, unknown> | null;\n query: Record<string, unknown> | null;\n body: Record<string, unknown> | null;\n};\n\ntype RuntimeManifest = { version: 1; routes: ManifestRoute[] };\n\nconst MANIFEST_PATH = \"/.well-known/geekapps-app.json\";\nconst MANIFEST_TTL_MS = 5 * 60_000;\nconst _manifestCache = new Map<string, { value: RuntimeManifest; fetchedAt: number }>();\n\n/** Manifesto da app alvo (cache de 5 min; se a busca falhar, usa a última cópia). */\nasync function getRuntimeManifest(baseUrl: string, force = false): Promise<RuntimeManifest> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}${MANIFEST_PATH}`;\n const cached = _manifestCache.get(url);\n if (cached && !force && Date.now() - cached.fetchedAt < MANIFEST_TTL_MS) return cached.value;\n\n try {\n const response = await fetch(url, { headers: { accept: \"application/json\" }, cache: \"no-store\" });\n if (!response.ok) throw new Error(`status ${response.status}`);\n const value = (await response.json()) as RuntimeManifest;\n if (value?.version !== 1 || !Array.isArray(value.routes)) throw new Error(\"formato inválido\");\n _manifestCache.set(url, { value, fetchedAt: Date.now() });\n return value;\n } catch (error) {\n if (cached) return cached.value;\n throw new AppConnectionError(\n `App Connections: manifesto da app alvo indisponível em ${url} (${(error as Error).message})`,\n 0,\n \"network_error\",\n null,\n );\n }\n}\n\nfunction propertyNames(schema: Record<string, unknown> | null) {\n const properties = schema?.properties;\n return properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n}\n\nfunction appendQuery(url: URL, key: string, value: unknown) {\n if (value === undefined) return;\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(url, key, item);\n return;\n }\n url.searchParams.append(key, value !== null && typeof value === \"object\" ? JSON.stringify(value) : String(value));\n}\n\n/**\n * Separa o objeto único da chamada em path, query e body, pelo manifesto:\n * params vão no path; em GET/DELETE o resto vai na query; nos demais métodos,\n * os campos da query declarada vão na query e o resto no body.\n */\nfunction buildRequest(baseUrl: string, route: ManifestRoute, input: Record<string, unknown>) {\n const rest: Record<string, unknown> = { ...input };\n const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_, name: string) => {\n const value = rest[name];\n delete rest[name];\n if (value === undefined || value === null || value === \"\") {\n throw new AppConnectionError(`App Connections: falta o parâmetro \"${name}\" em ${route.route}.`, 0, \"validation_error\", null);\n }\n return encodeURIComponent(String(value));\n });\n\n const url = new URL(path.replace(/^\\//, \"\"), `${baseUrl.replace(/\\/+$/, \"\")}/`);\n const method = route.method.toUpperCase();\n const bodyless = method === \"GET\" || method === \"DELETE\" || method === \"HEAD\";\n const queryFields = new Set(propertyNames(route.query));\n\n const body: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(rest)) {\n if (bodyless || queryFields.has(key)) appendQuery(url, key, value);\n else if (value !== undefined) body[key] = value;\n }\n\n const hasBody = !bodyless && (route.body !== null || Object.keys(body).length > 0);\n return { url, method, body: hasBody ? JSON.stringify(body) : undefined };\n}\n\n/** Troca o token, mapeando falhas de rede para `network_error`. */\nasync function accessTokenFor(connectionToken: string, config: AppConnectionsConfig) {\n try {\n return await getConnectionAccessToken({ connectionToken, config });\n } catch (error) {\n if (error instanceof AppConnectionError) throw error;\n throw new AppConnectionError(`App Connections: auth-server inacessível (${(error as Error).message})`, 0, \"network_error\", null);\n }\n}\n\ntype ClientOptions_ = {\n config?: AppConnectionsConfig;\n /** Sobrescreve o apiBaseUrl declarado pela app alvo. */\n baseUrl?: string;\n};\n\nasync function callRoute(\n connectionToken: string,\n options: ClientOptions_,\n routeName: string,\n input: Record<string, unknown> = {},\n): Promise<unknown> {\n const config = options.config ?? {};\n let token = await accessTokenFor(connectionToken, config);\n const baseUrl = options.baseUrl ?? token.targetApiBaseUrl;\n if (!baseUrl) {\n throw new AppConnectionError(\"App Connections: a app alvo não declarou apiBaseUrl — passe `baseUrl`.\", 0, \"unknown\", null);\n }\n\n let manifest = await getRuntimeManifest(baseUrl);\n let route = manifest.routes.find((item) => item.route === routeName);\n if (!route) {\n // A app alvo pode ter acabado de publicar a rota: confere o manifesto atual.\n manifest = await getRuntimeManifest(baseUrl, true);\n route = manifest.routes.find((item) => item.route === routeName);\n }\n if (!route) {\n throw new AppConnectionError(`App Connections: a app alvo não tem a rota \"${routeName}\".`, 404, \"route_not_found\", null);\n }\n\n const request = buildRequest(baseUrl, route, input);\n\n for (let attempt = 0; ; attempt++) {\n let response: Response;\n try {\n response = await fetch(request.url, {\n method: request.method,\n headers: {\n authorization: `Bearer ${token.accessToken}`,\n accept: \"application/json\",\n ...(request.body !== undefined && { \"content-type\": \"application/json\" }),\n },\n body: request.body,\n cache: \"no-store\",\n });\n } catch (error) {\n throw new AppConnectionError(`App Connections: app alvo inacessível (${(error as Error).message})`, 0, \"network_error\", null);\n }\n\n // 401 com JWT em cache: pode ter vencido ou a conexão mudou. Troca de novo\n // uma vez — se a conexão foi revogada/expirou, a troca já diz qual.\n if (response.status === 401 && attempt === 0) {\n _jwtCache.delete(connectionToken);\n token = await accessTokenFor(connectionToken, config);\n continue;\n }\n\n const text = await response.text();\n let body: unknown = null;\n try {\n body = text ? JSON.parse(text) : null;\n } catch {\n body = text;\n }\n\n if (!response.ok) {\n const reason = errorCode(body) ?? (response.status === 401 ? \"connection_revoked\" : undefined);\n throw new AppConnectionError(\n `App Connections: ${route.route} falhou (${response.status}${reason ? ` ${reason}` : \"\"})`,\n response.status,\n reason,\n body,\n );\n }\n return body ?? undefined;\n }\n}\n\ntype UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;\n\ntype RouteFn<R> = R extends { input: infer I; output: infer O }\n ? {} extends I\n ? (input?: I) => Promise<O>\n : (input: I) => Promise<O>\n : never;\n\ntype PathToObject<K extends string, V> = K extends `${infer Head}.${infer Tail}`\n ? { [P in Head]: PathToObject<Tail, V> }\n : { [P in K]: V };\n\n/**\n * Cliente de uma app: `post.messages.send({ ... })`. Com `S`, só as rotas dos\n * escopos pedidos aparecem.\n */\nexport type ConnectionClient<K, S extends string = ConnectionScopeOf<K>> = [ConnectionAppOf<K>] extends [never]\n ? // Sem tipos gerados: qualquer rota, sem checagem.\n any\n : ConnectionAppOf<K> extends { routes: infer R }\n ? UnionToIntersection<\n { [N in keyof R & string]: R[N] extends { scope: S } ? PathToObject<N, RouteFn<R[N]>> : never }[keyof R & string]\n >\n : never;\n\n/** `connections.post(token)` — um cliente por app, pelo slug do catálogo. */\nexport type Connections = {\n [K in ConnectionAppSlug]: <const S extends ConnectionScopeOf<K> = ConnectionScopeOf<K>>(\n connectionToken: string,\n options?: ClientOptions_ & {\n /** Restringe os tipos às rotas destes escopos (os que a conexão pediu). Só tipos: quem decide é o token. */\n scopes?: readonly S[];\n },\n ) => ConnectionClient<K, S>;\n};\n\nfunction routeProxy(call: (route: string, input?: Record<string, unknown>) => Promise<unknown>, path: string[]): unknown {\n return new Proxy(function () {}, {\n get(_target, property) {\n // Não é uma Promise: `await connections.post(token)` não pode virar chamada.\n if (typeof property !== \"string\" || property === \"then\") return undefined;\n return routeProxy(call, [...path, property]);\n },\n apply(_target, _this, args: unknown[]) {\n if (path.length === 0) throw new Error(\"App Connections: chame uma rota, ex: post.messages.send({ ... }).\");\n return call(path.join(\".\"), args[0] as Record<string, unknown> | undefined);\n },\n });\n}\n\n/**\n * Clientes tipados das apps conectáveis, a partir do manifesto de cada uma\n * (`/.well-known/geekapps-app.json`, publicado pelo @geekapps/auth-fastify):\n *\n * const post = connections.post(connectionToken);\n * const message = await post.messages.send({ channel, to, text });\n *\n * Params, query e body vão num objeto só — o SDK separa pelo manifesto. O JWT\n * da conexão é obtido, guardado em cache e renovado sozinho. Erros viram\n * `AppConnectionError` com `code` tipado (\"missing_scope\", \"connection_revoked\"…).\n *\n * O slug só escolhe os tipos (gerados em `geekapps-connections/`, ver\n * `generateConnectionTypes`); a app alvo de verdade é a do token.\n */\nexport function createConnections(config: AppConnectionsConfig = {}): Connections {\n return new Proxy({} as Connections, {\n get(_target, slug) {\n if (typeof slug !== \"string\" || slug === \"then\") return undefined;\n return (connectionToken: string, options: ClientOptions_ = {}) =>\n routeProxy(\n (route, input) => callRoute(connectionToken, { config, ...options }, route, input),\n [],\n );\n },\n });\n}\n\n/** `createConnections()` com a configuração do .env. */\nexport const connections: Connections = createConnections();\n\n// ─── Tipos automáticos em desenvolvimento ───────────────────────────────────\n\nconst TYPES_AUGMENT_MODULE = \"@geekapps/auth-nextjs/connections\";\n\nasync function syncTypes() {\n const { issuer, clientId, clientSecret } = resolveConfig({});\n if (!clientId || !clientSecret) return;\n try {\n const { generateConnectionTypes } = await import(\"./typegen\");\n await generateConnectionTypes({\n credentials: { issuer, clientId, clientSecret },\n augment: TYPES_AUGMENT_MODULE,\n });\n } catch {\n // Os tipos são conveniência de desenvolvimento: nunca derrubam a app.\n }\n}\n\n// Ao subir em desenvolvimento e a cada minuto: o dev não roda nada, e uma app\n// alvo que passa a declarar (ou muda) o OpenAPI aparece nos tipos sozinha.\nif (process.env.NODE_ENV === \"development\" && process.env.GEEKAPPS_CONNECTIONS_TYPES !== \"off\") {\n void syncTypes();\n setInterval(() => void syncTypes(), 60_000).unref?.();\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { cookies, headers } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\";\n\n/**\n * A sessão não vale mais: expirou, foi revogada no painel ou o refresh token\n * foi invalidado. Os cookies já foram limpos quando este erro é lançado, então\n * o tratamento normal é redirecionar para o login.\n */\nexport class GeekappsSessionExpiredError extends Error {\n readonly code = \"session_expired\" as const;\n\n constructor(message = \"A sessão expirou ou foi revogada. Entre novamente.\") {\n super(message);\n this.name = \"GeekappsSessionExpiredError\";\n }\n}\n\n/**\n * Fixed URL of the Geekapps Auth Server (OIDC issuer + API). This never\n * changes per-environment, so it is hardcoded rather than read from env.\n */\nexport const GEEKAPPS_AUTH_SERVER_URL = \"https://accounts-api.geekapps.com.br\";\n\nexport type GeekappsAuthConfig = {\n /**\n * Auth Server (OIDC) issuer URL. Defaults to GEEKAPPS_AUTH_SERVER_URL and\n * should not need overriding in normal use.\n *\n * Deprecated (still supported as a fallback): GEEKAPPS_AUTH_SERVER_ISSUER,\n * GEEKAPPS_AUTH_ISSUER, GEEKAPPS_AUTH_SERVER_URL env, GEEKAPPS_AUTH_API_URL\n */\n issuer?: string;\n\n /**\n * OAuth client id. Optional when GEEKAPPS_AUTH_CLIENT_ID is defined.\n */\n clientId?: string;\n\n /**\n * OAuth client secret for confidential clients. Optional when\n * GEEKAPPS_AUTH_CLIENT_SECRET is defined.\n */\n clientSecret?: string;\n\n /**\n * Callback URL. Optional when GEEKAPPS_AUTH_REDIRECT_URI is defined.\n * If omitted, it is inferred from the current request origin + basePath/callback.\n */\n redirectUri?: string;\n\n scopes?: string[];\n basePath?: string;\n /**\n * @deprecated cookiePrefix is now auto-derived from clientId.\n * This field is kept for backwards compatibility but is ignored.\n */\n cookiePrefix?: string;\n secureCookies?: boolean;\n\n /**\n * Friendly alias used by apps. Same as afterSignInRedirectTo.\n */\n redirectTo?: string;\n\n afterSignInRedirectTo?: string;\n afterSignOutRedirectTo?: string;\n};\n\ntype ResolvedGeekappsAuthConfig = GeekappsAuthConfig & {\n issuer: string;\n clientId: string;\n basePath: string;\n cookiePrefix: string;\n};\n\nexport type SessionTokens = {\n accessToken: string | null;\n refreshToken: string | null;\n idToken: string | null;\n sessionId: string | null;\n};\n\nexport type StoredAccount = {\n /** Stable identifier — `sub` from id_token or fallback to sessionId. */\n id: string;\n accessToken: string;\n refreshToken: string | null;\n idToken: string | null;\n sessionId: string | null;\n user: {\n sub: string;\n name?: string | null;\n email?: string | null;\n picture?: string | null;\n phone?: string | null;\n };\n};\n\nexport type ServerAuthApiClient = {\n get: <T>(path: string) => Promise<T>;\n post: <T>(path: string, body?: unknown) => Promise<T>;\n patch: <T>(path: string, body?: unknown) => Promise<T>;\n put: <T>(path: string, body?: unknown) => Promise<T>;\n delete: <T>(path: string) => Promise<T>;\n};\n\ntype JsonObject = Record<string, unknown>;\n\ntype TokenLikeResponse = JsonObject & {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n session_id?: string;\n sessionId?: string;\n tokens?: {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n session_id?: string;\n sessionId?: string;\n };\n};\n\nconst MAX_ACCOUNTS = 8;\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction normalizeBasePath(path = \"/api/auth\") {\n if (!path.startsWith(\"/\")) return `/${path}`;\n return path.replace(/\\/$/, \"\");\n}\n\nfunction createUrl(baseUrl: string, path: string) {\n if (/^https?:\\/\\//.test(path)) return path;\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n return `${normalizeBaseUrl(baseUrl)}${normalizedPath}`;\n}\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction resolveIssuer(input: GeekappsAuthConfig) {\n return normalizeBaseUrl(\n input.issuer ??\n env(\"GEEKAPPS_AUTH_SERVER_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_SERVER_URL\") ??\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL,\n );\n}\n\nfunction resolveClientId(input: GeekappsAuthConfig) {\n return input.clientId ?? env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n}\n\nfunction resolveClientSecret(input: GeekappsAuthConfig) {\n return input.clientSecret ?? env(\"GEEKAPPS_AUTH_CLIENT_SECRET\");\n}\n\n\nfunction resolveConfiguredRedirectUri(input: GeekappsAuthConfig) {\n return input.redirectUri ?? env(\"GEEKAPPS_AUTH_REDIRECT_URI\");\n}\n\n/**\n * Derives a short stable 8-char hex hash from a clientId string.\n * Uses a simple djb2-style hash — no crypto dependency needed.\n */\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0; // keep unsigned 32-bit\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\n/**\n * Auto-derives the cookie prefix from clientId.\n * Format: `gk_{8-char-hash}` — guaranteed unique per OAuth client.\n * Falls back to `gk` when clientId is empty (dev/test environments).\n */\nfunction deriveCookiePrefix(clientId: string): string {\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\n// GEEKAPPS_APP_URL é o override de produção: em prod o app roda atrás de\n// Docker sem proxy setando x-forwarded-host/x-forwarded-proto, então nem o\n// header Host nem request.nextUrl.origin são confiáveis (refletem o host\n// INTERNO do container, não o domínio público real) — por isso, quando\n// configurada, essa env var sempre vence. Sem ela (dev local, onde não está\n// setada no .env), deriva do próprio request — em localhost sem proxy o\n// Host já é localhost:PORTA mesmo, então funciona sem precisar de override.\nfunction requestOrigin(request: NextRequest) {\n const configured = env(\"GEEKAPPS_APP_URL\");\n if (configured) return configured.replace(/\\/$/, \"\");\n\n if (request.nextUrl?.origin) return request.nextUrl.origin;\n\n throw new Error(\n \"Cannot determine app origin. Set GEEKAPPS_APP_URL or ensure request.nextUrl is available.\",\n );\n}\n\nfunction resolveRedirectUri(config: ResolvedGeekappsAuthConfig, request: NextRequest) {\n return config.redirectUri ?? `${requestOrigin(request)}${config.basePath}/callback`;\n}\n\nfunction tokenFromResponse(data: TokenLikeResponse): SessionTokens {\n return {\n accessToken: data.accessToken ?? data.access_token ?? data.tokens?.accessToken ?? data.tokens?.access_token ?? null,\n refreshToken:\n data.refreshToken ?? data.refresh_token ?? data.tokens?.refreshToken ?? data.tokens?.refresh_token ?? null,\n idToken: data.idToken ?? data.id_token ?? data.tokens?.idToken ?? data.tokens?.id_token ?? null,\n sessionId: data.sessionId ?? data.session_id ?? data.tokens?.sessionId ?? data.tokens?.session_id ?? null,\n };\n}\n\n/** Decode a JWT payload without verifying signature (safe for client-visible claims). */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length < 2) return null;\n const payload = parts[1];\n if (!payload) return null;\n return JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction parseAccountFromJwt(tokens: SessionTokens): StoredAccount | null {\n if (!tokens.accessToken) return null;\n\n const jwtToParse = tokens.idToken ?? tokens.accessToken;\n const payload = decodeJwtPayload(jwtToParse);\n if (!payload) return null;\n\n const sub = typeof payload.sub === \"string\" ? payload.sub : null;\n const id = sub ?? tokens.sessionId;\n if (!id) return null;\n\n return {\n id,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n idToken: tokens.idToken ?? null,\n sessionId: tokens.sessionId,\n user: {\n sub: id,\n name: typeof payload.name === \"string\" ? payload.name : null,\n email: typeof payload.email === \"string\" ? payload.email : null,\n picture: typeof payload.picture === \"string\" ? payload.picture : null,\n phone: typeof payload.phone_number === \"string\" ? payload.phone_number : null,\n },\n };\n}\n\n/**\n * Cookie name scheme — 3 cookies per slot, each well under 4096 bytes:\n *\n * {prefix}_m{n} — metadata blob (base64url JSON): { id, sid?, n?, e?, p? }\n * Claims extracted from id_token at login. Never stores tokens.\n * {prefix}_at{n} — access token (httpOnly). ~1-2 KB.\n * {prefix}_rt{n} — refresh token (httpOnly). ~1 KB.\n * {prefix}_active — active slot index (\"1\", \"2\", …)\n * {prefix}_count — total stored slots\n * {prefix}_ao — active org id for this app\n * {prefix}_tp — non-httpOnly copy of active access token for client-side axios\n *\n * id_token is NOT stored in cookies — claims are extracted at login and cached in\n * the metadata cookie. This is the key size saving.\n */\nfunction getCookieNames(prefix: string) {\n return {\n active: `${prefix}_active`,\n count: `${prefix}_count`,\n activeOrganizationId: `${prefix}_ao`,\n tokenPub: `${prefix}_tp`,\n pkceVerifier: `${prefix}_pkce`,\n slotMeta: (n: number) => `${prefix}_m${n}`,\n slotAt: (n: number) => `${prefix}_at${n}`,\n slotRt: (n: number) => `${prefix}_rt${n}`,\n };\n}\n\nfunction generateCodeVerifier(): string {\n return randomBytes(64).toString(\"base64url\");\n}\n\nfunction createCodeChallenge(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n}\n\ntype SlotMeta = {\n id: string;\n sid?: string;\n n?: string;\n e?: string;\n p?: string;\n};\n\nfunction encodeMeta(account: StoredAccount): string {\n const m: SlotMeta = { id: account.id };\n if (account.sessionId) m.sid = account.sessionId;\n if (account.user.name) m.n = account.user.name;\n if (account.user.email) m.e = account.user.email;\n if (account.user.picture) m.p = account.user.picture;\n return Buffer.from(JSON.stringify(m), \"utf8\").toString(\"base64url\");\n}\n\nfunction decodeMeta(value: string): SlotMeta | null {\n try {\n const m = JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")) as SlotMeta;\n return m.id ? m : null;\n } catch {\n return null;\n }\n}\n\nfunction getCookieOptions(config: ResolvedGeekappsAuthConfig) {\n return {\n httpOnly: true,\n secure: config.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n };\n}\n\nasync function readBody(request: NextRequest) {\n const text = await request.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction appendSearchParams(url: string, request: NextRequest) {\n const target = new URL(url);\n request.nextUrl.searchParams.forEach((value, key) => {\n target.searchParams.set(key, value);\n });\n return target.toString();\n}\n\nfunction routeFromRequest(request: NextRequest, basePath: string) {\n const pathname = request.nextUrl.pathname;\n if (!pathname.startsWith(basePath)) return pathname;\n const route = pathname.slice(basePath.length);\n return route || \"/\";\n}\n\nfunction isJsonResponse(response: Response) {\n return response.headers.get(\"content-type\")?.includes(\"application/json\");\n}\n\nasync function responseFromApi(response: Response) {\n if (response.status === 204) return new NextResponse(null, { status: 204 });\n const body = isJsonResponse(response) ? await response.json().catch(() => null) : await response.text();\n return NextResponse.json(body, { status: response.status });\n}\n\nconst _remoteJWKSCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\ntype ServiceTokenCache = { token: string; expiresAt: number };\nconst _serviceTokenCache = new Map<string, ServiceTokenCache>();\n\nasync function fetchServiceToken(issuer: string, clientId: string, clientSecret: string): Promise<string | null> {\n const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\");\n try {\n const res = await fetch(createUrl(issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${credentials}`,\n },\n body: \"grant_type=service_account\",\n cache: \"no-store\",\n });\n if (!res.ok) return null;\n const data = (await res.json()) as { access_token?: string };\n return data.access_token ?? null;\n } catch {\n return null;\n }\n}\n\nasync function getServiceToken(issuer: string, clientId: string, clientSecret: string): Promise<string | null> {\n const key = `${issuer}:${clientId}`;\n const now = Date.now();\n const cached = _serviceTokenCache.get(key);\n if (cached && cached.expiresAt - 30_000 > now) return cached.token;\n const token = await fetchServiceToken(issuer, clientId, clientSecret);\n if (!token) return null;\n _serviceTokenCache.set(key, { token, expiresAt: now + 420_000 });\n return token;\n}\n\ntype ServiceAccountConfig = {\n issuer?: string;\n clientId?: string;\n clientSecret?: string;\n};\n\n/**\n * Returns a service account access token.\n * The issuer is always the Geekapps Auth Server (GEEKAPPS_AUTH_SERVER_URL) —\n * it does not need to be configured. Reads GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID,\n * GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET by default.\n * Any field can be overridden via the optional config argument.\n * Tokens are cached per client and auto-renewed before expiry (~7 min TTL).\n */\nexport function getServiceAccountToken(override: ServiceAccountConfig = {}): Promise<string | null> {\n const issuer = normalizeBaseUrl(\n override.issuer ??\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL,\n );\n const clientId = override.clientId ?? env(\"GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID\") ?? \"\";\n const clientSecret = override.clientSecret ?? env(\"GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET\") ?? \"\";\n if (!clientId || !clientSecret) return Promise.resolve(null);\n return getServiceToken(issuer, clientId, clientSecret);\n}\n\nfunction getRemoteJWKS(issuer: string) {\n if (!_remoteJWKSCache.has(issuer)) {\n _remoteJWKSCache.set(issuer, createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)));\n }\n return _remoteJWKSCache.get(issuer)!;\n}\n\nexport type VerifyTokenOptions = {\n /** client_id desta app. Quando informado, o token precisa ter sido emitido para ela (`aud`). */\n audience?: string;\n /** Credenciais desta app, para conferir se uma App Connection ainda está ativa. */\n clientId?: string;\n clientSecret?: string;\n};\n\n/** Informações de App Connection lidas de um token já verificado. */\nexport type TokenConnectionInfo = {\n /** true quando outra app está agindo em nome do usuário, sem ele presente. */\n isAppConnection: boolean;\n /** Application que está agindo (claim `act.sub`). */\n actorAppId: string | null;\n /** Id da App Connection. */\n connectionId: string | null;\n};\n\nexport function getTokenConnectionInfo(payload: JWTPayload): TokenConnectionInfo {\n const isAppConnection = payload.token_type === \"app_connection\";\n const act = payload.act as { sub?: unknown } | undefined;\n return {\n isAppConnection,\n actorAppId: isAppConnection && typeof act?.sub === \"string\" ? act.sub : null,\n connectionId: isAppConnection && typeof payload.connection_id === \"string\" ? payload.connection_id : null,\n };\n}\n\n/**\n * Pergunta ao Auth se a App Connection segue ativa. Sem cache: revogar\n * corta o acesso na próxima requisição.\n */\nasync function isAppConnectionActive(\n issuer: string,\n connectionId: string,\n clientId: string,\n clientSecret: string,\n): Promise<boolean> {\n try {\n const response = await fetch(`${issuer}/connections/introspect`, {\n method: \"POST\",\n headers: {\n authorization: `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString(\"base64\")}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({ connection_id: connectionId }),\n cache: \"no-store\",\n });\n const body = (await response.json().catch(() => null)) as { active?: boolean } | null;\n return response.ok && body?.active === true;\n } catch {\n return false;\n }\n}\n\nexport async function verifyToken(\n token: string,\n issuer: string,\n options: VerifyTokenOptions = {},\n): Promise<JWTPayload | null> {\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, getRemoteJWKS(issuer), {\n algorithms: [\"RS256\"],\n issuer,\n // aud = issuer também passa: é o que os tokens do painel do Auth usam.\n ...(options.audience ? { audience: [options.audience, issuer] } : {}),\n }));\n } catch {\n return null;\n }\n\n // Token de App Connection: só vale se a conexão continua ativa agora.\n const info = getTokenConnectionInfo(payload);\n if (info.isAppConnection) {\n if (!info.connectionId || !options.clientId || !options.clientSecret) return null;\n const active = await isAppConnectionActive(issuer, info.connectionId, options.clientId, options.clientSecret);\n if (!active) return null;\n }\n\n return payload;\n}\n\n// Renovações em andamento por refresh token (por processo) — ver tryRefreshTokens.\nconst inflightServerRefreshes = new Map<string, Promise<SessionTokens | null>>();\n\nexport function configureGeekappsAuth(input: GeekappsAuthConfig = {}) {\n const clientId = resolveClientId(input);\n const config: ResolvedGeekappsAuthConfig = {\n ...input,\n issuer: resolveIssuer(input),\n clientId,\n clientSecret: resolveClientSecret(input),\n redirectUri: resolveConfiguredRedirectUri(input),\n afterSignInRedirectTo:\n input.afterSignInRedirectTo ?? input.redirectTo ?? \"/\",\n basePath: normalizeBasePath(input.basePath),\n cookiePrefix: deriveCookiePrefix(clientId),\n };\n\n const cn = getCookieNames(config.cookiePrefix);\n\n // ── Slot helpers ──────────────────────────────────────────────────────────\n\n async function getCount(): Promise<number> {\n const cookieStore = await cookies();\n const raw = cookieStore.get(cn.count)?.value;\n const n = raw ? Number.parseInt(raw, 10) : 0;\n return Number.isNaN(n) ? 0 : n;\n }\n\n async function getActiveIndex(): Promise<number> {\n const cookieStore = await cookies();\n const raw = cookieStore.get(cn.active)?.value;\n if (!raw) return 0;\n const n = Number.parseInt(raw, 10);\n return Number.isNaN(n) ? 0 : n;\n }\n\n function readSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n ): StoredAccount | null {\n const at = cookieStore.get(cn.slotAt(n))?.value;\n if (!at) return null;\n const meta = decodeMeta(cookieStore.get(cn.slotMeta(n))?.value ?? \"\");\n if (!meta) return null;\n return {\n id: meta.id,\n accessToken: at,\n refreshToken: cookieStore.get(cn.slotRt(n))?.value ?? null,\n idToken: null, // not stored in cookies; claims are in meta\n sessionId: meta.sid ?? null,\n user: {\n sub: meta.id,\n name: meta.n ?? null,\n email: meta.e ?? null,\n picture: meta.p ?? null,\n },\n };\n }\n\n function writeSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n account: StoredAccount,\n ) {\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n cookieStore.set(cn.slotMeta(n), encodeMeta(account), opts);\n cookieStore.set(cn.slotAt(n), account.accessToken, opts);\n if (account.refreshToken) cookieStore.set(cn.slotRt(n), account.refreshToken, opts);\n }\n\n function clearSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n ) {\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n cookieStore.set(cn.slotMeta(n), \"\", clear);\n cookieStore.set(cn.slotAt(n), \"\", clear);\n cookieStore.set(cn.slotRt(n), \"\", clear);\n }\n\n // ── Public account helpers ─────────────────────────────────────────────────\n\n async function getAccountsList(): Promise<StoredAccount[]> {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const accounts: StoredAccount[] = [];\n for (let i = 1; i <= count; i++) {\n const slot = readSlot(cookieStore, i);\n if (slot) accounts.push(slot);\n }\n return accounts;\n }\n\n async function getActiveAccount(): Promise<StoredAccount | null> {\n const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);\n if (count === 0) return null;\n if (activeIdx >= 1 && activeIdx <= count) {\n const slot = readSlot(cookieStore, activeIdx);\n if (slot) return slot;\n }\n // Fall back to slot 1\n return readSlot(cookieStore, 1);\n }\n\n async function getAccessToken() {\n // Prefer the token injected by the middleware via request header (same-request refresh)\n try {\n const hdrs = await headers();\n const auth = hdrs.get(\"authorization\");\n if (auth?.startsWith(\"Bearer \")) return auth.slice(7);\n } catch {}\n const active = await getActiveAccount();\n return active?.accessToken ?? null;\n }\n\n async function getRefreshToken() {\n const active = await getActiveAccount();\n return active?.refreshToken ?? null;\n }\n\n async function getSession(): Promise<SessionTokens | null> {\n const active = await getActiveAccount();\n if (!active) return null;\n return {\n accessToken: active.accessToken,\n refreshToken: active.refreshToken,\n idToken: active.idToken,\n sessionId: active.sessionId,\n };\n }\n\n async function getUser(): Promise<StoredAccount[\"user\"] | null> {\n const active = await getActiveAccount();\n return active?.user ?? null;\n }\n\n async function getUserById(userId: string, applicationId?: string): Promise<unknown | null> {\n const qs = new URLSearchParams({ ...(applicationId ? { applicationId } : {}) });\n try {\n return await apiRequest<unknown>(`/users/${userId}?${qs}`);\n } catch {\n return null;\n }\n }\n\n async function getOrganizations(): Promise<{ organizations: unknown[]; currentOrgId: string | null }> {\n const cookieStore = await cookies();\n const currentOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n try {\n const data = await apiRequest<unknown[] | { organizations: unknown[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: unknown[] }).organizations;\n return { organizations, currentOrgId };\n } catch {\n return { organizations: [], currentOrgId };\n }\n }\n\n function clearPublicTokenCookie(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n ) {\n // Clear legacy _tp cookie if present\n if (cookieStore.get(cn.tokenPub)?.value) {\n cookieStore.set(cn.tokenPub, \"\", { httpOnly: false, maxAge: 0, path: \"/\" });\n }\n }\n\n async function saveAccountToList(tokens: SessionTokens) {\n const account = parseAccountFromJwt(tokens);\n if (!account) return;\n\n const cookieStore = await cookies();\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n const count = await getCount();\n\n // Find existing slot for this account id\n let targetSlot = 0;\n for (let i = 1; i <= count; i++) {\n const existing = readSlot(cookieStore, i);\n if (existing?.id === account.id) {\n targetSlot = i;\n break;\n }\n }\n\n if (targetSlot === 0) {\n if (count >= MAX_ACCOUNTS) return;\n targetSlot = count + 1;\n cookieStore.set(cn.count, String(targetSlot), opts);\n }\n\n writeSlot(cookieStore, targetSlot, account);\n cookieStore.set(cn.active, String(targetSlot), opts);\n clearPublicTokenCookie(cookieStore);\n }\n\n // Alias for backwards compat\n const setSessionCookies = saveAccountToList;\n\n async function switchAccount(accountId: string): Promise<boolean> {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n\n for (let i = 1; i <= count; i++) {\n const slot = readSlot(cookieStore, i);\n if (slot && (slot.id === accountId || slot.sessionId === accountId)) {\n cookieStore.set(cn.active, String(i), opts);\n clearPublicTokenCookie(cookieStore);\n return true;\n }\n }\n return false;\n }\n\n async function removeActiveAccount() {\n const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n\n if (count === 0) return;\n\n const remaining: StoredAccount[] = [];\n for (let i = 1; i <= count; i++) {\n if (i === activeIdx) continue;\n const slot = readSlot(cookieStore, i);\n if (slot) remaining.push(slot);\n }\n\n for (let i = 1; i <= count; i++) {\n clearSlot(cookieStore, i);\n }\n\n if (remaining.length === 0) {\n cookieStore.set(cn.count, \"0\", clear);\n cookieStore.set(cn.active, \"0\", clear);\n await clearPublicTokenCookie(cookieStore);\n return;\n }\n\n // Re-compact remaining accounts into slots 1..n\n for (let i = 0; i < remaining.length; i++) {\n await writeSlot(cookieStore, i + 1, remaining[i]!);\n }\n cookieStore.set(cn.count, String(remaining.length), opts);\n cookieStore.set(cn.active, \"1\", opts);\n clearPublicTokenCookie(cookieStore);\n }\n\n async function clearAllSessionCookies() {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n for (let i = 1; i <= count; i++) {\n clearSlot(cookieStore, i);\n }\n cookieStore.set(cn.count, \"0\", clear);\n cookieStore.set(cn.active, \"0\", clear);\n clearPublicTokenCookie(cookieStore);\n }\n\n async function tryRefreshTokens(): Promise<string | null> {\n const refreshToken = await getRefreshToken();\n if (!refreshToken || !config.clientId) return null;\n\n // Chamadas simultâneas com o mesmo refresh token dividem uma renovação só\n // (ver middleware): renovar duas vezes com o mesmo token parece reuso.\n let pending = inflightServerRefreshes.get(refreshToken);\n if (!pending) {\n pending = refreshWithToken(refreshToken).finally(() => {\n setTimeout(() => inflightServerRefreshes.delete(refreshToken), 10_000);\n });\n inflightServerRefreshes.set(refreshToken, pending);\n }\n const tokens = await pending;\n if (!tokens?.accessToken) return null;\n\n // Em Server Component o Next não deixa gravar cookie: a renovação vale\n // para esta requisição, e o middleware grava o par novo na próxima. O\n // auth-server aceita a reapresentação do token antigo nesse caso.\n try {\n await saveAccountToList(tokens);\n } catch {\n // cookies() somente leitura neste contexto\n }\n return tokens.accessToken;\n }\n\n async function refreshWithToken(refreshToken: string) {\n\n const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret ?? \"\"}`).toString(\"base64\");\n\n const response = await fetch(createUrl(config.issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n }),\n cache: \"no-store\",\n });\n\n if (!response.ok) return null;\n\n const data = (await response.json().catch(() => null)) as TokenLikeResponse | null;\n if (!data) return null;\n\n const tokens = tokenFromResponse(data);\n return tokens.accessToken ? tokens : null;\n }\n\n async function apiRequest<T>(path: string, init: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n const requestHeaders = new Headers(init.headers);\n\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (init.body !== undefined && !requestHeaders.has(\"content-type\")) {\n requestHeaders.set(\"content-type\", \"application/json\");\n }\n\n const url = new URL(createUrl(config.issuer, path));\n\n const response = await fetch(url, {\n ...init,\n headers: requestHeaders,\n cache: \"no-store\",\n });\n\n if (response.status === 401) {\n const newAccessToken = await tryRefreshTokens();\n\n if (!newAccessToken) {\n // Sem renovar depois de um 401, a sessão acabou: expirou, foi revogada\n // no painel ou o refresh token caiu. Os cookies apontam para algo que\n // não existe mais, então saem — senão toda requisição seguinte repete\n // o mesmo ciclo. Quem chamou recebe um erro que dá para tratar, em vez\n // de \"Auth API error 401\" vindo do fundo da pilha.\n await clearAllSessionCookies().catch(() => {});\n throw new GeekappsSessionExpiredError();\n }\n\n if (newAccessToken) {\n const retryHeaders = new Headers(init.headers);\n retryHeaders.set(\"authorization\", `Bearer ${newAccessToken}`);\n if (init.body !== undefined && !retryHeaders.has(\"content-type\")) {\n retryHeaders.set(\"content-type\", \"application/json\");\n }\n const retryUrl = new URL(createUrl(config.issuer, path));\n const retryResponse = await fetch(retryUrl, {\n ...init,\n headers: retryHeaders,\n cache: \"no-store\",\n });\n if (!retryResponse.ok) {\n const error = await retryResponse.json().catch(() => null);\n throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${retryResponse.status}`);\n }\n if (retryResponse.status === 204) return null as T;\n return (await retryResponse.json()) as T;\n }\n }\n\n if (!response.ok) {\n const error = await response.json().catch(() => null);\n throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${response.status}`);\n }\n\n if (response.status === 204) return null as T;\n return (await response.json()) as T;\n }\n\n function createApiClient(): ServerAuthApiClient {\n return {\n get: (path) => apiRequest(path),\n post: (path, body) => apiRequest(path, { method: \"POST\", body: body === undefined ? undefined : JSON.stringify(body) }),\n patch: (path, body) => apiRequest(path, { method: \"PATCH\", body: body === undefined ? undefined : JSON.stringify(body) }),\n put: (path, body) => apiRequest(path, { method: \"PUT\", body: body === undefined ? undefined : JSON.stringify(body) }),\n delete: (path) => apiRequest(path, { method: \"DELETE\" }),\n };\n }\n\n async function requireUser(options: { redirectTo?: string } = {}) {\n const accessToken = await getAccessToken();\n if (!accessToken) {\n redirect(options.redirectTo ?? \"/\");\n }\n return accessToken;\n }\n\n function escapeHtml(str: string): string {\n return str.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n }\n\n function sanitizeRedirectTo(value: string | null | undefined): string {\n const raw = value ?? \"/\";\n // Never redirect back into the auth basePath — that would loop or replay a stale code.\n if (raw.startsWith(config.basePath)) return \"/\";\n // Only allow relative paths to prevent open-redirect.\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n return \"/\";\n }\n\n async function createAuthorizeRedirect(request: NextRequest, mode: \"login\" | \"register\" | \"add-account\" | \"google\") {\n const redirectTo = sanitizeRedirectTo(request.nextUrl.searchParams.get(\"redirectTo\") ?? config.afterSignInRedirectTo);\n const flowId = request.nextUrl.searchParams.get(\"flow_id\");\n const url = new URL(createUrl(config.issuer, mode === \"google\" ? \"/auth/google\" : \"/authorize\"));\n\n if (!config.clientId) {\n return NextResponse.json(\n { error: \"missing_client_id\", message: \"Missing GEEKAPPS_AUTH_CLIENT_ID or clientId.\" },\n { status: 500 },\n );\n }\n\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", resolveRedirectUri(config, request));\n url.searchParams.set(\"response_type\", \"code\");\n const scopes = config.scopes ?? [\"openid\", \"profile\", \"email\"];\n if (!scopes.includes(\"offline_access\")) scopes.push(\"offline_access\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", Buffer.from(JSON.stringify({ redirectTo, mode })).toString(\"base64url\"));\n if (mode === \"register\") url.searchParams.set(\"screen_hint\", \"signup\");\n if (mode === \"add-account\") {\n // Adding an account always needs a fresh login, regardless of any existing session.\n url.searchParams.set(\"prompt\", \"login\");\n } else {\n // `prompt` (login | select_account | none | consent) is forwarded as-is to\n // the IDP — see OidcPrompt in client.tsx for what each value does.\n const prompt = request.nextUrl.searchParams.get(\"prompt\");\n // Legacy fallback: silentLogin=false is equivalent to prompt=login.\n const silentLogin = request.nextUrl.searchParams.get(\"silentLogin\");\n const resolvedPrompt = prompt ?? (silentLogin === \"false\" ? \"login\" : null);\n if (resolvedPrompt) url.searchParams.set(\"prompt\", resolvedPrompt);\n }\n if (flowId) url.searchParams.set(\"flow_id\", flowId);\n\n const codeVerifier = generateCodeVerifier();\n url.searchParams.set(\"code_challenge\", createCodeChallenge(codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n const response = NextResponse.redirect(url);\n const cn = getCookieNames(config.cookiePrefix);\n response.cookies.set(cn.pkceVerifier, codeVerifier, {\n ...getCookieOptions(config),\n maxAge: 60 * 10,\n });\n return response;\n }\n\n async function exchangeCode(request: NextRequest) {\n const code = request.nextUrl.searchParams.get(\"code\");\n const state = request.nextUrl.searchParams.get(\"state\");\n\n const origin = requestOrigin(request);\n\n if (!code) {\n const err = request.nextUrl.searchParams.get(\"error\") ?? \"missing_code\";\n const desc = request.nextUrl.searchParams.get(\"error_description\") ?? request.nextUrl.searchParams.get(\"error_message\") ?? \"No authorization code received\";\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${encodeURIComponent(err)}&error_description=${encodeURIComponent(desc)}`, origin));\n }\n\n if (!config.clientId) {\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=missing_client_id&error_description=Auth+client+is+not+configured`, origin));\n }\n\n const basicAuth = Buffer.from(\n `${config.clientId}:${config.clientSecret ?? \"\"}`,\n ).toString(\"base64\");\n\n const cn = getCookieNames(config.cookiePrefix);\n const cookieStore = await cookies();\n const codeVerifier = cookieStore.get(cn.pkceVerifier)?.value;\n cookieStore.set(cn.pkceVerifier, \"\", { ...getCookieOptions(config), maxAge: 0 });\n\n const tokenBody = new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: resolveRedirectUri(config, request),\n });\n if (codeVerifier) tokenBody.set(\"code_verifier\", codeVerifier);\n\n const response = await fetch(createUrl(config.issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: tokenBody,\n cache: \"no-store\",\n });\n\n const data = (await response.json().catch(() => ({}))) as TokenLikeResponse & { error?: string; error_description?: string };\n if (!response.ok) {\n const error = encodeURIComponent((data.error as string | undefined) ?? \"token_exchange_failed\");\n const desc = encodeURIComponent((data.error_description as string | undefined) ?? `Token exchange failed with status ${response.status}`);\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${error}&error_description=${desc}`, origin));\n }\n\n await saveAccountToList(tokenFromResponse(data));\n\n let redirectTo = sanitizeRedirectTo(config.afterSignInRedirectTo);\n if (state) {\n try {\n const parsed = JSON.parse(Buffer.from(state, \"base64url\").toString(\"utf8\")) as { redirectTo?: string };\n redirectTo = sanitizeRedirectTo(parsed.redirectTo);\n } catch {\n // ignore malformed state\n }\n }\n\n return NextResponse.redirect(new URL(redirectTo, origin));\n }\n\n function mapLocalPathToApiPath(route: string) {\n if (route === \"/me\") return \"/auth/me\";\n if (route === \"/accounts/switch\") return \"/auth/accounts/switch\";\n if (route === \"/organizations/active\") return \"/auth/organizations/active\";\n if (route === \"/permissions/check\") return \"/auth/permissions/check\";\n if (route === \"/login/password\") return \"/auth/login/password\";\n if (route === \"/register/password\") return \"/auth/register/password\";\n if (route === \"/passwordless/link\") return \"/auth/passwordless/link\";\n return route;\n }\n\n async function handleAccountsLocal(): Promise<NextResponse> {\n const [accounts, activeIdx] = await Promise.all([getAccountsList(), getActiveIndex()]);\n\n return NextResponse.json({\n accounts: accounts.map((a, i) => ({\n id: a.id,\n sessionId: a.sessionId ?? a.id,\n isActive: i + 1 === activeIdx,\n user: {\n id: a.user.sub,\n name: a.user.name ?? null,\n email: a.user.email ?? null,\n imageUrl: a.user.picture ?? null,\n phone: a.user.phone ?? null,\n },\n })),\n });\n }\n\n async function handleOrganizationsLocal(): Promise<NextResponse> {\n const cookieStore = await cookies();\n const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n try {\n const data = await apiRequest<unknown[]|{ organizations: unknown[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: unknown[] }).organizations;\n return NextResponse.json({ organizations, currentOrgId: activeOrgId });\n } catch {\n return NextResponse.json({ organizations: [], currentOrgId: activeOrgId });\n }\n }\n\n async function handleSwitchAccountLocal(request: NextRequest): Promise<NextResponse> {\n const body = await readBody(request);\n const targetId =\n body && typeof body === \"object\"\n ? String(\n (body as Record<string, unknown>).accountId ??\n (body as Record<string, unknown>).sessionId ??\n \"\",\n )\n : \"\";\n\n if (!targetId) {\n return NextResponse.json({ error: \"accountId required\" }, { status: 400 });\n }\n\n const switched = await switchAccount(targetId);\n if (!switched) {\n return NextResponse.json({ error: \"Account not found\" }, { status: 404 });\n }\n\n return NextResponse.json({ ok: true });\n }\n\n async function proxy(request: NextRequest, route: string) {\n const accessToken = await getAccessToken();\n const body = [\"GET\", \"HEAD\"].includes(request.method) ? undefined : await readBody(request);\n const url = new URL(appendSearchParams(createUrl(config.issuer, mapLocalPathToApiPath(route)), request));\n const requestHeaders = new Headers();\n\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (body !== undefined) requestHeaders.set(\"content-type\", \"application/json\");\n\n let response = await fetch(url, {\n method: request.method,\n headers: requestHeaders,\n body: body === undefined ? undefined : typeof body === \"string\" ? body : JSON.stringify(body),\n cache: \"no-store\",\n });\n\n if (response.status === 401) {\n // Mesmo tratamento do apiRequest: tenta renovar uma vez e, se a sessão\n // já não existe (expirada ou revogada no painel), limpa os cookies. Sem\n // isso o cliente repetiria o mesmo token morto a cada chamada.\n const refreshed = await tryRefreshTokens();\n\n if (refreshed) {\n requestHeaders.set(\"authorization\", `Bearer ${refreshed}`);\n response = await fetch(url, {\n method: request.method,\n headers: requestHeaders,\n body:\n body === undefined\n ? undefined\n : typeof body === \"string\"\n ? body\n : JSON.stringify(body),\n cache: \"no-store\",\n });\n } else {\n await clearAllSessionCookies().catch(() => {});\n return NextResponse.json(\n { error: \"session_expired\" },\n { status: 401 },\n );\n }\n }\n\n const data = isJsonResponse(response) ? ((await response.json().catch(() => null)) as TokenLikeResponse | null) : null;\n if (data) {\n const tokens = tokenFromResponse(data);\n if (tokens.accessToken || tokens.refreshToken || tokens.idToken || tokens.sessionId) {\n await saveAccountToList(tokens);\n }\n return NextResponse.json(data, { status: response.status });\n }\n\n return responseFromApi(response);\n }\n\n async function handle(request: NextRequest) {\n const route = routeFromRequest(request, config.basePath);\n\n if (request.method === \"GET\" && route === \"/sign-in\") return createAuthorizeRedirect(request, \"login\");\n if (request.method === \"GET\" && route === \"/sign-up\") return createAuthorizeRedirect(request, \"register\");\n if (request.method === \"GET\" && route === \"/add-account\") return createAuthorizeRedirect(request, \"add-account\");\n if (request.method === \"GET\" && route === \"/login/google\") return createAuthorizeRedirect(request, \"google\");\n if (request.method === \"GET\" && route === \"/callback\") {\n const error = request.nextUrl.searchParams.get(\"error\");\n if (error) {\n const description = request.nextUrl.searchParams.get(\"error_description\") ?? request.nextUrl.searchParams.get(\"error_message\") ?? \"An authentication error occurred.\";\n return new NextResponse(\n `<!doctype html><html><head><meta charset=\"utf-8\"><title>Authentication Error</title></head><body style=\"font-family:sans-serif;max-width:480px;margin:80px auto;padding:0 16px\"><h2>Authentication Error</h2><p><strong>${escapeHtml(error)}</strong></p><p>${escapeHtml(description)}</p><a href=\"/\">Go back</a></body></html>`,\n { status: 400, headers: { \"content-type\": \"text/html; charset=utf-8\" } },\n );\n }\n return exchangeCode(request);\n }\n if (request.method === \"GET\" && route === \"/token\") {\n const accessToken = await getAccessToken();\n if (!accessToken) return NextResponse.json({ error: \"missing_token\" }, { status: 401 });\n return NextResponse.json({ accessToken });\n }\n\n\n if (request.method === \"GET\" && route === \"/accounts\") return handleAccountsLocal();\n if (request.method === \"POST\" && route === \"/accounts/switch\") return handleSwitchAccountLocal(request);\n if (request.method === \"GET\" && route === \"/organizations\") return handleOrganizationsLocal();\n\n if (request.method === \"POST\" && route === \"/organizations\") {\n const accessToken = await getAccessToken();\n const body = (await readBody(request)) as Record<string, unknown> | undefined;\n if (body && accessToken && !body.applicationId) {\n const claims = decodeJwtPayload(accessToken);\n const applicationId =\n (typeof claims?.application_id === \"string\" ? claims.application_id : null) ??\n (typeof claims?.client_id === \"string\" ? claims.client_id : null) ??\n (typeof claims?.applicationId === \"string\" ? claims.applicationId : null);\n if (applicationId) body.applicationId = applicationId;\n }\n const orgUrl = new URL(createUrl(config.issuer, \"/organizations\"));\n const requestHeaders = new Headers();\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (body !== undefined) requestHeaders.set(\"content-type\", \"application/json\");\n const response = await fetch(orgUrl, {\n method: \"POST\",\n headers: requestHeaders,\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n return responseFromApi(response);\n }\n\n if (request.method === \"POST\" && route === \"/sign-out\") {\n await apiRequest(\"/auth/sign-out\", { method: \"POST\" }).catch(() => null);\n await removeActiveAccount();\n return NextResponse.json({ ok: true });\n }\n\n if (request.method === \"POST\" && route === \"/sign-out-all\") {\n await apiRequest(\"/auth/sign-out-all\", { method: \"POST\" }).catch(() => null);\n await clearAllSessionCookies();\n return NextResponse.json({ ok: true });\n }\n\n if (request.method === \"POST\" && route === \"/organizations/active\") {\n const body = await readBody(request);\n const organizationId =\n body && typeof body === \"object\" && \"organizationId\" in body\n ? (body as Record<string, unknown>).organizationId\n : null;\n const response = NextResponse.json({ ok: true });\n if (typeof organizationId === \"string\") {\n const cookieOpts = {\n httpOnly: true,\n sameSite: \"lax\" as const,\n path: \"/\",\n secure: process.env.NODE_ENV === \"production\",\n maxAge: 60 * 60 * 24 * 7,\n };\n response.cookies.set(cn.activeOrganizationId, organizationId, cookieOpts);\n }\n return response;\n }\n\n return proxy(request, route);\n }\n\n return {\n handlers: {\n GET: handle,\n POST: handle,\n PATCH: handle,\n DELETE: handle,\n },\n GetSession: getSession,\n GetUser: getUser,\n GetUserById: getUserById,\n GetOrganizations: getOrganizations,\n SaveSession: saveAccountToList,\n GetAccessToken: getAccessToken,\n GetRefreshToken: getRefreshToken,\n IsAuthenticated: async () => (await getAccessToken()) !== null,\n TryRefreshTokens: tryRefreshTokens,\n RequireUser: requireUser,\n CreateAuthApiClient: createApiClient,\n cookiePrefix: config.cookiePrefix,\n VerifyToken: (token: string) =>\n verifyToken(token, config.issuer, {\n audience: config.clientId || undefined,\n clientId: config.clientId || undefined,\n clientSecret: config.clientSecret,\n }),\n GetTokenConnectionInfo: getTokenConnectionInfo,\n SignInUrl: async (redirectTo?: string) => {\n const configured = env(\"GEEKAPPS_APP_URL\");\n if (!configured) throw new Error(\"Cannot determine app origin. Set GEEKAPPS_APP_URL.\");\n const origin = configured.replace(/\\/$/, \"\");\n const url = new URL(`${origin}${config.basePath}/sign-in`);\n if (redirectTo) url.searchParams.set(\"redirectTo\", redirectTo);\n return url.toString();\n },\n };\n}\n\nexport function handlers(options?: GeekappsAuthConfig) {\n return configureGeekappsAuth(options).handlers;\n}\n\n/**\n * Returns true if the current request has a valid access token in cookies.\n * Safe to call in Server Components, Server Actions, and Route Handlers.\n */\nexport async function isAuthenticated(options?: GeekappsAuthConfig): Promise<boolean> {\n const { GetAccessToken } = configureGeekappsAuth(options);\n return (await GetAccessToken()) !== null;\n}\n"],"mappings":"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","names":["createHash","randomBytes","cookies","redirect","randomBytes","cookies","createHash","redirect","fetchCatalog","headers","properties","generateConnectionTypes"]}
|
|
1
|
+
{"version":3,"sources":["../src/typegen.ts","../src/connections.ts","../src/server.ts"],"sourcesContent":["// Gerador de tipos das App Connections: manifesto da app alvo\n// (/.well-known/geekapps-app.json) → TypeScript. O OpenAPI, quando a app\n// declara um, ainda gera os `paths` do cliente antigo (createConnectionClient).\nimport { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\n\nexport const DEFAULT_ISSUER = \"https://accounts-api.geekapps.com.br\";\nexport const DEFAULT_OUT_DIR = \"geekapps-connections\";\nconst DEFAULT_AUGMENT = \"@geekapps/auth-nextjs/connections\";\n\n/** Uma app conectável — o que o catálogo do auth-server devolve. */\nexport type CatalogApp = {\n targetAppId: string;\n name: string;\n /** Chave estável no tipo `GeekappsConnections` (ex: \"post\"). */\n slug: string;\n apiBaseUrl: string | null;\n /** `${apiBaseUrl}/.well-known/geekapps-app.json`, quando a app publica um manifesto. */\n manifestUrl?: string | null;\n /** OpenAPI declarado (opcional) — só para o cliente antigo. */\n openApiUrl?: string | null;\n};\n\n/**\n * Quem pede o catálogo: a app consumidora (client id + secret) ou o usuário\n * logado pelo CLI (`geekapps login`), que vê as apps das suas organizações.\n */\nexport type Credentials =\n | { issuer: string; clientId: string; clientSecret: string }\n | { issuer: string; accessToken: string };\n\ntype JsonSchema = Record<string, unknown>;\n\n/** O manifesto publicado pelo plugin do @geekapps/auth-fastify. */\nexport type ConnectionManifest = {\n version: 1;\n appId: string | null;\n scopes: Record<string, { description: string }>;\n routes: Array<{\n method: string;\n path: string;\n scope: string;\n route: string;\n params: JsonSchema | null;\n query: JsonSchema | null;\n body: JsonSchema | null;\n response: JsonSchema | null;\n }>;\n};\n\n/** Apps conectáveis vistas por esta app (credenciais dela). */\nexport async function fetchCatalog(credentials: Credentials): Promise<CatalogApp[]> {\n const issuer = credentials.issuer.replace(/\\/$/, \"\");\n const asUser = \"accessToken\" in credentials;\n const response = await fetch(`${issuer}${asUser ? \"/me/connections/catalog\" : \"/connections/catalog\"}`, {\n headers: {\n authorization: asUser\n ? `Bearer ${credentials.accessToken}`\n : `Basic ${Buffer.from(`${credentials.clientId}:${credentials.clientSecret}`).toString(\"base64\")}`,\n },\n cache: \"no-store\",\n });\n if (!response.ok) {\n throw new Error(\n asUser\n ? `catálogo indisponível (${response.status}) — rode \\`geekapps login\\` de novo`\n : `catálogo indisponível (${response.status}) — confira GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET`,\n );\n }\n return (await response.json()) as CatalogApp[];\n}\n\nexport async function fetchManifest(manifestUrl: string): Promise<ConnectionManifest> {\n const response = await fetch(manifestUrl, { headers: { accept: \"application/json\" }, cache: \"no-store\" });\n if (!response.ok) throw new Error(`manifesto indisponível (${response.status})`);\n const manifest = (await response.json()) as ConnectionManifest;\n if (manifest?.version !== 1 || !manifest.scopes || !Array.isArray(manifest.routes)) {\n throw new Error(\"manifesto em formato inválido\");\n }\n return manifest;\n}\n\nconst HEADER = \"// Gerado pelo SDK do Geekapps Auth. Não edite à mão.\\n\\n\";\n\n/** \"messages.send\" → \"MessagesSend\". */\nfunction pascal(name: string) {\n return name\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => part[0]!.toUpperCase() + part.slice(1))\n .join(\"\");\n}\n\nfunction properties(schema: JsonSchema | null) {\n return (schema?.properties as Record<string, JsonSchema> | undefined) ?? {};\n}\n\nfunction required(schema: JsonSchema | null) {\n return Array.isArray(schema?.required) ? (schema!.required as string[]) : [];\n}\n\n/**\n * O cliente recebe params, query e body num objeto só (o manifesto garante\n * que não há campo repetido): junta os três num schema de objeto.\n */\nfunction mergeInputSchema(route: ConnectionManifest[\"routes\"][number]): JsonSchema {\n const parts = [route.params, route.query, route.body];\n // Path params são sempre obrigatórios, mesmo sem `required` no schema.\n const pathParams = [...route.path.matchAll(/:([A-Za-z0-9_]+)/g)].map((m) => m[1]!);\n return {\n type: \"object\",\n properties: Object.assign({}, ...parts.map(properties)),\n required: [...new Set([...pathParams, ...parts.flatMap(required)])],\n additionalProperties: false,\n };\n}\n\n/**\n * Tipos de um manifesto. Usa `json-schema-to-typescript` (import dinâmico:\n * dependência opcional, só de desenvolvimento — sem ela, a geração não roda).\n */\nexport async function generateManifestTypes(app: Pick<CatalogApp, \"slug\" | \"targetAppId\">, manifest: ConnectionManifest) {\n const { compile } = await import(\"json-schema-to-typescript\");\n const options = { bannerComment: \"\", additionalProperties: false, format: false, declareExternallyReferenced: true } as const;\n\n const blocks: string[] = [];\n const routeEntries: string[] = [];\n for (const route of manifest.routes) {\n const base = pascal(route.route);\n const input = `${base}Input`;\n const output = `${base}Output`;\n blocks.push(await compile(mergeInputSchema(route) as never, input, options));\n blocks.push(\n route.response\n ? await compile({ ...route.response, title: undefined } as never, output, options)\n : `export type ${output} = unknown;\\n`,\n );\n routeEntries.push(\n ` ${JSON.stringify(route.route)}: {\\n` +\n ` scope: ${JSON.stringify(route.scope)};\\n` +\n ` method: ${JSON.stringify(route.method)};\\n` +\n ` path: ${JSON.stringify(route.path)};\\n` +\n ` input: ${input};\\n` +\n ` output: ${output};\\n` +\n ` };`,\n );\n }\n\n const scopeIds = Object.keys(manifest.scopes);\n const scopeDocs = scopeIds\n .map((id) => ` ${JSON.stringify(id)}: ${JSON.stringify(manifest.scopes[id]!.description)};`)\n .join(\"\\n\");\n\n return `${HEADER}/** Escopos que esta app aceita em conexões. */\nexport type Scope = ${scopeIds.length ? scopeIds.map((id) => JSON.stringify(id)).join(\" | \") : \"never\"};\n\n/** Descrição de cada escopo, como aparece no consent. */\nexport interface ScopeDescriptions {\n${scopeDocs}\n}\n\n/** Rotas liberadas por escopo, pelo nome usado no cliente (\\`post.messages.send(...)\\`). */\nexport interface Routes {\n${routeEntries.join(\"\\n\")}\n}\n\nexport interface App {\n slug: ${JSON.stringify(app.slug)};\n targetAppId: ${JSON.stringify(app.targetAppId)};\n scopes: Scope;\n routes: Routes;\n}\n\n${blocks.join(\"\\n\")}`;\n}\n\n/** Tipos de um OpenAPI, no formato do openapi-typescript (cliente antigo). */\nasync function generateOpenApiTypes(openApiUrl: string) {\n const { default: openapiTS, astToString } = await import(\"openapi-typescript\");\n return HEADER + astToString(await openapiTS(new URL(openApiUrl)));\n}\n\ntype Generated = { app: CatalogApp; manifest: boolean; openApi: boolean };\n\n/** `index.d.ts`: liga cada app ao `GeekappsConnections` do módulo do SDK. */\nfunction renderIndex(apps: Generated[], augment: string) {\n const imports = apps\n .flatMap(({ app, manifest, openApi }, i) => [\n manifest ? `import type { App as App${i} } from \"./${app.slug}\";` : null,\n openApi ? `import type { paths as paths${i} } from \"./${app.slug}.openapi\";` : null,\n ])\n .filter(Boolean)\n .join(\"\\n\");\n const members = apps\n .map(({ app, manifest, openApi }, i) => {\n const fields = [manifest ? `app: App${i};` : null, openApi ? `paths: paths${i};` : null].filter(Boolean).join(\" \");\n return ` ${JSON.stringify(app.slug)}: { ${fields} };`;\n })\n .join(\"\\n\");\n return `${HEADER}${imports}\n\ndeclare module ${JSON.stringify(augment)} {\n interface GeekappsConnections {\n${members}\n }\n}\n\nexport {};\n`;\n}\n\nexport type GenerateResult = {\n apps: string[];\n /** Apps do catálogo cujo manifesto/OpenAPI não pôde ser baixado/convertido. */\n failed: Array<{ name: string; reason: string }>;\n /** Com `check`: arquivos que estariam diferentes. */\n stale: string[];\n};\n\n/**\n * Gerador de tipos das App Connections, embutido no SDK (sem pacote à parte).\n * Roda sozinho em desenvolvimento; para o CI, chame com `check: true`.\n *\n * Para cada app conectável escreve em `outDir`:\n * • `<slug>.d.ts` — a partir do manifesto (escopos, rotas e tipos de\n * entrada/saída de cada rota), requer `json-schema-to-typescript`;\n * • `<slug>.openapi.d.ts` — a partir do OpenAPI, se a app declarar um\n * (só para o `createConnectionClient` antigo), requer `openapi-typescript`;\n * • `index.d.ts`, que estende `GeekappsConnections` do módulo `augment`.\n * Com `check`, não escreve nada: só diz o que estaria diferente. Uma app cuja\n * busca falha mantém os arquivos anteriores.\n */\nexport async function generateConnectionTypes(params: {\n credentials: Credentials;\n outDir?: string;\n /** Módulo que recebe o `GeekappsConnections`. Padrão: \"@geekapps/auth-nextjs/connections\". */\n augment?: string;\n check?: boolean;\n}): Promise<GenerateResult> {\n const outDir = resolve(process.cwd(), params.outDir ?? DEFAULT_OUT_DIR);\n const catalog = (await fetchCatalog(params.credentials)).sort((a, b) => a.slug.localeCompare(b.slug));\n\n const files = new Map<string, string>();\n const result: GenerateResult = { apps: [], failed: [], stale: [] };\n const generated: Generated[] = [];\n\n // Mantém o que já existia, para a app não perder os tipos por uma falha de rede.\n const keepExisting = (file: string) => {\n const existing = join(outDir, file);\n if (!existsSync(existing)) return false;\n files.set(file, readFileSync(existing, \"utf-8\"));\n return true;\n };\n\n for (const app of catalog) {\n const entry: Generated = { app, manifest: false, openApi: false };\n\n if (app.manifestUrl) {\n const file = `${app.slug}.d.ts`;\n try {\n files.set(file, await generateManifestTypes(app, await fetchManifest(app.manifestUrl)));\n entry.manifest = true;\n } catch (error) {\n result.failed.push({ name: app.name, reason: `manifesto: ${(error as Error).message}` });\n entry.manifest = keepExisting(file);\n }\n }\n\n if (app.openApiUrl) {\n const file = `${app.slug}.openapi.d.ts`;\n try {\n files.set(file, await generateOpenApiTypes(app.openApiUrl));\n entry.openApi = true;\n } catch (error) {\n result.failed.push({ name: app.name, reason: `openapi: ${(error as Error).message}` });\n entry.openApi = keepExisting(file);\n }\n }\n\n if (entry.manifest || entry.openApi) generated.push(entry);\n }\n result.apps = generated.map(({ app }) => app.name);\n files.set(\"index.d.ts\", renderIndex(generated, params.augment ?? DEFAULT_AUGMENT));\n\n const current = existsSync(outDir) ? new Set(readdirSync(outDir).filter((f) => f.endsWith(\".d.ts\"))) : new Set<string>();\n\n if (params.check) {\n for (const [file, content] of files) {\n const path = join(outDir, file);\n if (!existsSync(path) || readFileSync(path, \"utf-8\") !== content) result.stale.push(file);\n }\n for (const file of current) if (!files.has(file)) result.stale.push(file);\n return result;\n }\n\n mkdirSync(outDir, { recursive: true });\n for (const [file, content] of files) {\n const path = join(outDir, file);\n if (!existsSync(path) || readFileSync(path, \"utf-8\") !== content) writeFileSync(path, content, \"utf-8\");\n }\n // Apps que deixaram o catálogo não devem deixar tipos órfãos.\n for (const file of current) if (!files.has(file)) rmSync(join(outDir, file));\n\n return result;\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { cookies } from \"next/headers\";\nimport type { Client, ClientOptions } from \"openapi-fetch\";\nimport { redirect } from \"next/navigation\";\nimport { GEEKAPPS_AUTH_SERVER_URL } from \"./server\";\n\n/**\n * App Connections — agir como um usuário numa organização de OUTRA Application.\n *\n * Uma conexão liga um USUÁRIO desta app (X) a uma organização da app alvo (Z)\n * de que ele já é membro. Ele a aprova uma vez, no consent; o auth-server\n * devolve a esta app um API token (`gka_…`). Daí em diante esta app guarda o\n * token e o usa quando quiser — com ou sem o usuário logado, num cron, num\n * workflow — trocando-o por um JWT curto que age COMO o usuário naquela\n * organização (`getConnectionAccessToken`). As permissions são as do usuário\n * na organização, hoje: mudou o role dele lá, a conexão muda junto; saiu da\n * organização, ela para.\n *\n * ⚠️ ATENÇÃO — o API token é uma credencial de longa duração.\n * • Guarde-o cifrado, associado ao usuário que aprovou a conexão.\n * • Use-o só para agir em nome DESSE usuário. Usar o token de A para atender\n * um pedido de B dá a B o acesso de A à organização alvo.\n * • Restringir quem pode disparar uma chamada, e o que ela pode pedir, é\n * responsabilidade desta app: o auth-server não tem como saber.\n * • Vazou? Revogue no dashboard do Geekapps Auth ou gire com `rotateConnectionToken`.\n */\n\nexport type AppConnectionsConfig = {\n issuer?: string;\n /** client_id/secret desta app (a consumidora, X). */\n clientId?: string;\n clientSecret?: string;\n};\n\n/**\n * Preenchida por declaration merging no `geekapps-connections/index.d.ts` que\n * o SDK gera (em desenvolvimento) ou `generateConnectionTypes()` escreve: uma\n * chave por app conectável (o slug do catálogo), com `app` (escopos, rotas e\n * tipos, do manifesto) e, se a app declarar um OpenAPI, `paths`. Vazia aqui\n * de propósito — quem sabe quais apps existem é o auth-server.\n */\nexport interface GeekappsConnections {}\n\n/**\n * O que deu errado numa chamada de conexão:\n * • `missing_scope` — a conexão não tem o escopo da rota (peça de novo com `connectApp`);\n * • `connection_revoked` — revogada, token inválido ou o usuário saiu da organização;\n * • `connection_expired` — passou da validade escolhida no consent;\n * • `app_not_allowed` — a app consumidora e a alvo são de organizações diferentes (não se conectam);\n * • `validation_error` — a app alvo recusou a entrada (400/422);\n * • `network_error` — não deu para falar com o auth-server ou a app alvo;\n * • `unknown` — o resto (veja `status`, `reason` e `body`).\n */\nexport type AppConnectionErrorCode =\n | \"missing_scope\"\n | \"connection_revoked\"\n | \"connection_expired\"\n | \"app_not_allowed\"\n | \"validation_error\"\n | \"network_error\"\n | \"unknown\";\n\n/** Erro de App Connection — `code` dá pra tratar; `reason` é o código cru do servidor. */\nexport class AppConnectionError extends Error {\n readonly status: number;\n readonly code: AppConnectionErrorCode;\n /** Código de erro como o servidor mandou (ex: \"connection_inactive\", \"invalid_state\"). */\n readonly reason: string | undefined;\n /** Escopo que faltou, quando `code` é \"missing_scope\". */\n readonly scope: string | undefined;\n readonly body: unknown;\n\n constructor(message: string, status: number, reason: string | undefined, body: unknown) {\n super(message);\n this.name = \"AppConnectionError\";\n this.status = status;\n this.reason = reason;\n this.code = toErrorCode(status, reason);\n this.body = body;\n this.scope =\n body && typeof body === \"object\" && typeof (body as { scope?: unknown }).scope === \"string\"\n ? (body as { scope: string }).scope\n : undefined;\n }\n}\n\nfunction toErrorCode(status: number, reason: string | undefined): AppConnectionErrorCode {\n switch (reason) {\n case \"missing_scope\":\n case \"app_connection_not_allowed\":\n return \"missing_scope\";\n case \"connection_expired\":\n return \"connection_expired\";\n case \"app_not_allowed\":\n return \"app_not_allowed\";\n case \"connection_inactive\":\n case \"connection_revoked\":\n case \"invalid_token\":\n case \"membership_required\":\n return \"connection_revoked\";\n case \"network_error\":\n return \"network_error\";\n }\n if (status === 400 || status === 422) return \"validation_error\";\n return \"unknown\";\n}\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction resolveConfig(config: AppConnectionsConfig) {\n const issuer = (\n config.issuer ??\n env(\"GEEKAPPS_AUTH_SERVER_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_SERVER_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL\n ).replace(/\\/$/, \"\");\n\n return {\n issuer,\n clientId: config.clientId ?? env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\",\n clientSecret: config.clientSecret ?? env(\"GEEKAPPS_AUTH_CLIENT_SECRET\") ?? \"\",\n };\n}\n\nfunction requireCredentials(config: AppConnectionsConfig) {\n const resolved = resolveConfig(config);\n if (!resolved.clientId || !resolved.clientSecret) {\n throw new Error(\"App Connections: defina GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET.\");\n }\n return resolved;\n}\n\nfunction errorCode(body: unknown) {\n return body && typeof body === \"object\" && typeof (body as { error?: unknown }).error === \"string\"\n ? (body as { error: string }).error\n : undefined;\n}\n\n// ─── Conectar esta app a outra (consent) ────────────────────────────────────\n\nconst CONNECT_COOKIES = {\n verifier: \"gk_conn_pkce\",\n state: \"gk_conn_state\",\n returnTo: \"gk_conn_return\",\n redirectUri: \"gk_conn_redirect\",\n} as const;\n\n// ─── Tipos derivados do que o typegen gerou ─────────────────────────────────\n\n/** Slugs das apps conectáveis (do catálogo); sem tipos gerados, qualquer string. */\nexport type ConnectionAppSlug = [keyof GeekappsConnections] extends [never] ? string : keyof GeekappsConnections & string;\n\n/** Manifesto tipado de uma app (escopos e rotas), quando o typegen gerou. */\nexport type ConnectionAppOf<K> = K extends keyof GeekappsConnections\n ? GeekappsConnections[K] extends { app: infer A } ? A : never\n : never;\n\n/** Escopos aceitos por uma app; sem tipos gerados, string. */\nexport type ConnectionScopeOf<K> = [ConnectionAppOf<K>] extends [never]\n ? string\n : ConnectionAppOf<K> extends { scopes: infer S extends string } ? S : string;\n\ntype ConnectAppTarget<K extends ConnectionAppSlug> =\n | {\n /** Slug da app ALVO no catálogo (ex: \"post\"), com autocomplete pelos tipos gerados. */\n app: K;\n targetAppId?: undefined;\n /** Escopos da app alvo que esta conexão vai pedir — o usuário vê a descrição de cada um no consent. */\n scopes?: ReadonlyArray<ConnectionScopeOf<K>>;\n }\n | {\n /** client_id da app ALVO (a que vai ser acessada). */\n targetAppId: string;\n app?: undefined;\n scopes?: readonly string[];\n };\n\nexport type ConnectAppParams<K extends ConnectionAppSlug = ConnectionAppSlug> = ConnectAppTarget<K> & {\n /** URL desta app que recebe o retorno do consent (rota que chama `completeAppConnection`). Precisa estar nos redirect URIs desta app. */\n redirectUri: string;\n /** Para onde `completeAppConnection` manda o usuário depois (padrão \"/\"). */\n returnTo?: string;\n /** Nome desta app exibido no consent. */\n displayName?: string;\n config?: AppConnectionsConfig;\n};\n\n/**\n * Inicia a conexão desta app com `targetAppId`: guarda PKCE/state em cookies\n * httpOnly e redireciona para o consent, onde o usuário escolhe a organização\n * e a validade. Chame de um Server Action ou Route Handler — não retorna (o\n * `redirect` do Next interrompe a execução).\n */\nexport async function connectApp<K extends ConnectionAppSlug>(params: ConnectAppParams<K>): Promise<never> {\n const { issuer, clientId } = resolveConfig(params.config ?? {});\n if (!clientId) {\n throw new Error(\"App Connections: defina GEEKAPPS_AUTH_CLIENT_ID — é o client_id desta app, a que pede a conexão.\");\n }\n const targetAppId = params.targetAppId ?? (await resolveAppSlug(params.app as string, params.config ?? {}));\n\n const codeVerifier = randomBytes(64).toString(\"base64url\");\n const state = randomBytes(24).toString(\"base64url\");\n\n const store = await cookies();\n const options = { httpOnly: true, secure: true, sameSite: \"lax\" as const, path: \"/\", maxAge: 600 };\n store.set(CONNECT_COOKIES.verifier, codeVerifier, options);\n store.set(CONNECT_COOKIES.state, state, options);\n store.set(CONNECT_COOKIES.returnTo, params.returnTo ?? \"/\", options);\n store.set(CONNECT_COOKIES.redirectUri, params.redirectUri, options);\n\n const url = new URL(`${issuer}/authorize`);\n url.searchParams.set(\"client_id\", clientId);\n url.searchParams.set(\"redirect_uri\", params.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", \"openid\");\n url.searchParams.set(\"state\", state);\n url.searchParams.set(\"code_challenge\", createHash(\"sha256\").update(codeVerifier).digest(\"base64url\"));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"target_app_id\", targetAppId);\n if (params.scopes && params.scopes.length > 0) {\n url.searchParams.set(\"connection_scopes\", [...new Set(params.scopes)].join(\" \"));\n }\n // O servidor já exige isso para conexões; pedir explícito deixa a intenção clara.\n url.searchParams.set(\"prompt\", \"select_account consent\");\n if (params.displayName) url.searchParams.set(\"connection_display_name\", params.displayName);\n\n redirect(url.toString());\n}\n\n/** A conexão recém-criada. O `token` só aparece aqui, uma única vez. */\nexport type AppConnectionToken = {\n /** API token (`gka_…`). Guarde-o cifrado, associado ao usuário que aprovou. */\n token: string;\n connectionId: string;\n /** null = sem expiração. */\n expiresAt: string | null;\n /** Organização da app alvo em que a conexão age. */\n organizationId: string;\n targetAppId: string;\n};\n\n/**\n * Fecha o fluxo de `connectApp`. Chame no callback com o `code` e o `state`\n * da query; devolve o `returnTo` e a conexão, com o API token — a ÚNICA vez em\n * que ele aparece, então persista-o antes de redirecionar. Lança\n * AppConnectionError se o state não bater ou o auth-server recusar.\n */\nexport async function completeAppConnection(params: {\n code: string;\n state: string;\n config?: AppConnectionsConfig;\n}): Promise<{ returnTo: string; connection: AppConnectionToken }> {\n const { issuer, clientId, clientSecret } = requireCredentials(params.config ?? {});\n\n const store = await cookies();\n const read = (name: string) => store.get(name)?.value;\n const expectedState = read(CONNECT_COOKIES.state);\n const codeVerifier = read(CONNECT_COOKIES.verifier);\n const redirectUri = read(CONNECT_COOKIES.redirectUri);\n const returnTo = read(CONNECT_COOKIES.returnTo) ?? \"/\";\n\n for (const name of Object.values(CONNECT_COOKIES)) store.set(name, \"\", { maxAge: 0, path: \"/\" });\n\n if (!expectedState || !codeVerifier || !redirectUri || params.state !== expectedState) {\n throw new AppConnectionError(\"App Connections: estado inválido — inicie a conexão novamente.\", 400, \"invalid_state\", null);\n }\n\n const response = await fetch(`${issuer}/oauth/token`, {\n method: \"POST\",\n headers: {\n authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\")}`,\n \"content-type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code: params.code,\n redirect_uri: redirectUri,\n code_verifier: codeVerifier,\n }),\n cache: \"no-store\",\n });\n\n const body = (await response.json().catch(() => null)) as {\n connection_token?: string;\n connection_id?: string;\n connection_expires_at?: string | null;\n connection_organization_id?: string;\n connection_target_app_id?: string;\n } | null;\n\n if (!response.ok || !body?.connection_token) {\n const code = errorCode(body);\n throw new AppConnectionError(\n `App Connections: não foi possível concluir a conexão (${response.status}${code ? ` ${code}` : \"\"})`,\n response.status,\n code,\n body,\n );\n }\n\n return {\n returnTo,\n connection: {\n token: body.connection_token,\n connectionId: body.connection_id!,\n expiresAt: body.connection_expires_at ?? null,\n organizationId: body.connection_organization_id!,\n targetAppId: body.connection_target_app_id!,\n },\n };\n}\n\n// ─── Usar a conexão ─────────────────────────────────────────────────────────\n\nconst _catalogCache: { value: Array<{ targetAppId: string; slug: string }> | null; fetchedAt: number } = {\n value: null,\n fetchedAt: 0,\n};\n\n/** slug do catálogo → client_id da app alvo (cache de 5 min). */\nasync function resolveAppSlug(slug: string, config: AppConnectionsConfig): Promise<string> {\n if (!_catalogCache.value || Date.now() - _catalogCache.fetchedAt > 5 * 60_000) {\n const { issuer, clientId, clientSecret } = requireCredentials(config);\n const { fetchCatalog } = await import(\"./typegen\");\n _catalogCache.value = await fetchCatalog({ issuer, clientId, clientSecret });\n _catalogCache.fetchedAt = Date.now();\n }\n const app = _catalogCache.value.find((item) => item.slug === slug);\n if (!app) throw new Error(`App Connections: a app \"${slug}\" não está no catálogo do Geekapps Auth.`);\n return app.targetAppId;\n}\n\n/** Resultado de `getConnectionAccessToken`. */\nexport type AppConnectionAccessToken = {\n /** JWT curto: use em `Authorization: Bearer` contra a API da app alvo. */\n accessToken: string;\n /** Segundos até o JWT vencer. */\n expiresIn: number;\n organizationId: string;\n targetAppId: string;\n /** apiBaseUrl da app alvo, quando ela declarou uma. */\n targetApiBaseUrl: string | null;\n /** URL do OpenAPI da app alvo, quando ela declarou uma — fonte dos tipos. */\n targetOpenApiUrl: string | null;\n};\n\nconst _jwtCache = new Map<string, { value: AppConnectionAccessToken; expiresAt: number }>();\n\nasync function postToAuthServer<T>(path: string, connectionToken: string, config: AppConnectionsConfig): Promise<T> {\n const { issuer, clientId, clientSecret } = requireCredentials(config);\n\n const response = await fetch(`${issuer}${path}`, {\n method: \"POST\",\n headers: {\n authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\")}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({ connection_token: connectionToken }),\n cache: \"no-store\",\n });\n\n const body = await response.json().catch(() => null);\n if (!response.ok) {\n const code = errorCode(body);\n throw new AppConnectionError(\n `App Connections: ${path} falhou (${response.status}${code ? ` ${code}` : \"\"})`,\n response.status,\n code,\n body,\n );\n }\n return body as T;\n}\n\n/**\n * Troca o API token por um JWT curto que age como o usuário na organização da\n * conexão. Não exige ninguém logado. O resultado fica em cache até perto de\n * vencer, então pode ser chamado a cada requisição.\n *\n * Erros possíveis (`AppConnectionError.code`): `invalid_token`,\n * `connection_inactive` (revogada), `connection_expired`,\n * `membership_required` (o usuário saiu da organização).\n */\nexport async function getConnectionAccessToken(params: {\n /** O API token guardado por esta app. */\n connectionToken: string;\n config?: AppConnectionsConfig;\n}): Promise<AppConnectionAccessToken> {\n const cached = _jwtCache.get(params.connectionToken);\n if (cached && cached.expiresAt > Date.now() + 30_000) return cached.value;\n\n const data = await postToAuthServer<{\n access_token: string;\n expires_in: number;\n organization_id: string;\n target_app_id: string;\n target_api_base_url: string | null;\n target_openapi_url: string | null;\n }>(\"/connections/token\", params.connectionToken, params.config ?? {});\n\n const value: AppConnectionAccessToken = {\n accessToken: data.access_token,\n expiresIn: data.expires_in,\n organizationId: data.organization_id,\n targetAppId: data.target_app_id,\n targetApiBaseUrl: data.target_api_base_url,\n targetOpenApiUrl: data.target_openapi_url,\n };\n _jwtCache.set(params.connectionToken, { value, expiresAt: Date.now() + data.expires_in * 1000 });\n return value;\n}\n\n/**\n * @deprecated Use `connections.<app>(token).<rota>(...)`, tipado pelo manifesto.\n *\n * Atalho: um `fetch` contra a API da app alvo já com o JWT da conexão. `path`\n * é relativo ao `apiBaseUrl` da app alvo (ou uma URL absoluta).\n */\nexport async function connectionFetch(params: {\n connectionToken: string;\n path: string;\n init?: RequestInit;\n config?: AppConnectionsConfig;\n}): Promise<Response> {\n const { accessToken, targetApiBaseUrl } = await getConnectionAccessToken({\n connectionToken: params.connectionToken,\n config: params.config,\n });\n\n const isAbsolute = /^https?:\\/\\//i.test(params.path);\n if (!isAbsolute && !targetApiBaseUrl) {\n throw new Error(\"App Connections: a app alvo não declarou apiBaseUrl — passe `path` como URL absoluta.\");\n }\n\n const headers = new Headers(params.init?.headers);\n headers.set(\"authorization\", `Bearer ${accessToken}`);\n\n return fetch(isAbsolute ? params.path : new URL(params.path, targetApiBaseUrl!), { ...params.init, headers });\n}\n\n/**\n * @deprecated Use `connections.<app>(token).<rota>(...)`, tipado pelo manifesto\n * da app alvo (escopos e rotas), sem depender de OpenAPI nem de `openapi-fetch`.\n *\n * Cliente tipado para a API da app alvo, a partir do OpenAPI dela: o JWT da\n * conexão é obtido, renovado e injetado a cada chamada.\n *\n * Os tipos de TODAS as apps conectáveis vêm de uma vez: em desenvolvimento o\n * SDK baixa o OpenAPI de cada uma ao subir e escreve `geekapps-connections/`\n * (requer `openapi-typescript` como devDependency; para o CI,\n * `generateConnectionTypes({ check: true })` de `@geekapps/auth-nextjs/typegen`). Daí basta nomear a app:\n *\n * const billing = await createConnectionClient({ app: \"billing\", connectionToken });\n * const { data } = await billing.GET(\"/charges\", { params: { query: { limit: 10 } } });\n *\n * `app` só escolhe os tipos; a app alvo de verdade é a do token. Sem tipos\n * gerados, passe os `paths` você mesmo: `createConnectionClient<paths>({ connectionToken })`.\n *\n * Requer o pacote `openapi-fetch` instalado na sua app.\n */\nexport async function createConnectionClient<App extends keyof GeekappsConnections>(\n params: ConnectionClientParams & { app: App },\n): Promise<Client<(GeekappsConnections[App] extends { paths: infer P } ? P : {}) & {}>>;\nexport async function createConnectionClient<Paths extends {}>(\n params: ConnectionClientParams & { app?: undefined },\n): Promise<Client<Paths>>;\nexport async function createConnectionClient(params: ConnectionClientParams & { app?: string }): Promise<Client<{}>> {\n const { default: createClient } = await import(\"openapi-fetch\").catch(() => {\n throw new Error(\"App Connections: instale `openapi-fetch` para usar createConnectionClient.\");\n });\n\n const { targetApiBaseUrl } = await getConnectionAccessToken({\n connectionToken: params.connectionToken,\n config: params.config,\n });\n const baseUrl = params.baseUrl ?? targetApiBaseUrl;\n if (!baseUrl) {\n throw new Error(\"App Connections: a app alvo não declarou apiBaseUrl — passe `baseUrl`.\");\n }\n\n return createClient<{}>({\n ...params.options,\n baseUrl,\n // O JWT vence em minutos: pega do cache (ou renova) a cada requisição.\n fetch: async (request) => {\n const { accessToken } = await getConnectionAccessToken({\n connectionToken: params.connectionToken,\n config: params.config,\n });\n request.headers.set(\"authorization\", `Bearer ${accessToken}`);\n return fetch(request);\n },\n });\n}\n\ntype ConnectionClientParams = {\n /** O API token guardado por esta app. */\n connectionToken: string;\n /** Sobrescreve o apiBaseUrl declarado pela app alvo. */\n baseUrl?: string;\n /** Demais opções do openapi-fetch (headers, querySerializer…). */\n options?: Omit<ClientOptions, \"baseUrl\" | \"fetch\">;\n config?: AppConnectionsConfig;\n};\n\n/**\n * Gira o API token: o antigo deixa de valer na hora e o novo é devolvido (a\n * validade da conexão não muda). Persista o novo no lugar do antigo.\n */\nexport async function rotateConnectionToken(params: {\n connectionToken: string;\n config?: AppConnectionsConfig;\n}): Promise<{ token: string; expiresAt: string | null }> {\n const data = await postToAuthServer<{ connection_token: string; expires_at: string | null }>(\n \"/connections/token/rotate\",\n params.connectionToken,\n params.config ?? {},\n );\n _jwtCache.delete(params.connectionToken);\n return { token: data.connection_token, expiresAt: data.expires_at };\n}\n\n// ─── Cliente tipado pelo manifesto da app alvo ──────────────────────────────\n\ntype ManifestRoute = {\n method: string;\n path: string;\n scope: string;\n route: string;\n params: Record<string, unknown> | null;\n query: Record<string, unknown> | null;\n body: Record<string, unknown> | null;\n};\n\ntype RuntimeManifest = { version: 1; routes: ManifestRoute[] };\n\nconst MANIFEST_PATH = \"/.well-known/geekapps-app.json\";\nconst MANIFEST_TTL_MS = 5 * 60_000;\nconst _manifestCache = new Map<string, { value: RuntimeManifest; fetchedAt: number }>();\n\n/** Manifesto da app alvo (cache de 5 min; se a busca falhar, usa a última cópia). */\nasync function getRuntimeManifest(baseUrl: string, force = false): Promise<RuntimeManifest> {\n const url = `${baseUrl.replace(/\\/+$/, \"\")}${MANIFEST_PATH}`;\n const cached = _manifestCache.get(url);\n if (cached && !force && Date.now() - cached.fetchedAt < MANIFEST_TTL_MS) return cached.value;\n\n try {\n const response = await fetch(url, { headers: { accept: \"application/json\" }, cache: \"no-store\" });\n if (!response.ok) throw new Error(`status ${response.status}`);\n const value = (await response.json()) as RuntimeManifest;\n if (value?.version !== 1 || !Array.isArray(value.routes)) throw new Error(\"formato inválido\");\n _manifestCache.set(url, { value, fetchedAt: Date.now() });\n return value;\n } catch (error) {\n if (cached) return cached.value;\n throw new AppConnectionError(\n `App Connections: manifesto da app alvo indisponível em ${url} (${(error as Error).message})`,\n 0,\n \"network_error\",\n null,\n );\n }\n}\n\nfunction propertyNames(schema: Record<string, unknown> | null) {\n const properties = schema?.properties;\n return properties && typeof properties === \"object\" ? Object.keys(properties) : [];\n}\n\nfunction appendQuery(url: URL, key: string, value: unknown) {\n if (value === undefined) return;\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(url, key, item);\n return;\n }\n url.searchParams.append(key, value !== null && typeof value === \"object\" ? JSON.stringify(value) : String(value));\n}\n\n/**\n * Separa o objeto único da chamada em path, query e body, pelo manifesto:\n * params vão no path; em GET/DELETE o resto vai na query; nos demais métodos,\n * os campos da query declarada vão na query e o resto no body.\n */\nfunction buildRequest(baseUrl: string, route: ManifestRoute, input: Record<string, unknown>) {\n const rest: Record<string, unknown> = { ...input };\n const path = route.path.replace(/:([A-Za-z0-9_]+)/g, (_, name: string) => {\n const value = rest[name];\n delete rest[name];\n if (value === undefined || value === null || value === \"\") {\n throw new AppConnectionError(`App Connections: falta o parâmetro \"${name}\" em ${route.route}.`, 0, \"validation_error\", null);\n }\n return encodeURIComponent(String(value));\n });\n\n const url = new URL(path.replace(/^\\//, \"\"), `${baseUrl.replace(/\\/+$/, \"\")}/`);\n const method = route.method.toUpperCase();\n const bodyless = method === \"GET\" || method === \"DELETE\" || method === \"HEAD\";\n const queryFields = new Set(propertyNames(route.query));\n\n const body: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(rest)) {\n if (bodyless || queryFields.has(key)) appendQuery(url, key, value);\n else if (value !== undefined) body[key] = value;\n }\n\n const hasBody = !bodyless && (route.body !== null || Object.keys(body).length > 0);\n return { url, method, body: hasBody ? JSON.stringify(body) : undefined };\n}\n\n/** Troca o token, mapeando falhas de rede para `network_error`. */\nasync function accessTokenFor(connectionToken: string, config: AppConnectionsConfig) {\n try {\n return await getConnectionAccessToken({ connectionToken, config });\n } catch (error) {\n if (error instanceof AppConnectionError) throw error;\n throw new AppConnectionError(`App Connections: auth-server inacessível (${(error as Error).message})`, 0, \"network_error\", null);\n }\n}\n\ntype ClientOptions_ = {\n config?: AppConnectionsConfig;\n /** Sobrescreve o apiBaseUrl declarado pela app alvo. */\n baseUrl?: string;\n};\n\nasync function callRoute(\n connectionToken: string,\n options: ClientOptions_,\n routeName: string,\n input: Record<string, unknown> = {},\n): Promise<unknown> {\n const config = options.config ?? {};\n let token = await accessTokenFor(connectionToken, config);\n const baseUrl = options.baseUrl ?? token.targetApiBaseUrl;\n if (!baseUrl) {\n throw new AppConnectionError(\"App Connections: a app alvo não declarou apiBaseUrl — passe `baseUrl`.\", 0, \"unknown\", null);\n }\n\n let manifest = await getRuntimeManifest(baseUrl);\n let route = manifest.routes.find((item) => item.route === routeName);\n if (!route) {\n // A app alvo pode ter acabado de publicar a rota: confere o manifesto atual.\n manifest = await getRuntimeManifest(baseUrl, true);\n route = manifest.routes.find((item) => item.route === routeName);\n }\n if (!route) {\n throw new AppConnectionError(`App Connections: a app alvo não tem a rota \"${routeName}\".`, 404, \"route_not_found\", null);\n }\n\n const request = buildRequest(baseUrl, route, input);\n\n for (let attempt = 0; ; attempt++) {\n let response: Response;\n try {\n response = await fetch(request.url, {\n method: request.method,\n headers: {\n authorization: `Bearer ${token.accessToken}`,\n accept: \"application/json\",\n ...(request.body !== undefined && { \"content-type\": \"application/json\" }),\n },\n body: request.body,\n cache: \"no-store\",\n });\n } catch (error) {\n throw new AppConnectionError(`App Connections: app alvo inacessível (${(error as Error).message})`, 0, \"network_error\", null);\n }\n\n // 401 com JWT em cache: pode ter vencido ou a conexão mudou. Troca de novo\n // uma vez — se a conexão foi revogada/expirou, a troca já diz qual.\n if (response.status === 401 && attempt === 0) {\n _jwtCache.delete(connectionToken);\n token = await accessTokenFor(connectionToken, config);\n continue;\n }\n\n const text = await response.text();\n let body: unknown = null;\n try {\n body = text ? JSON.parse(text) : null;\n } catch {\n body = text;\n }\n\n if (!response.ok) {\n const reason = errorCode(body) ?? (response.status === 401 ? \"connection_revoked\" : undefined);\n throw new AppConnectionError(\n `App Connections: ${route.route} falhou (${response.status}${reason ? ` ${reason}` : \"\"})`,\n response.status,\n reason,\n body,\n );\n }\n return body ?? undefined;\n }\n}\n\ntype UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;\n\ntype RouteFn<R> = R extends { input: infer I; output: infer O }\n ? {} extends I\n ? (input?: I) => Promise<O>\n : (input: I) => Promise<O>\n : never;\n\ntype PathToObject<K extends string, V> = K extends `${infer Head}.${infer Tail}`\n ? { [P in Head]: PathToObject<Tail, V> }\n : { [P in K]: V };\n\n/**\n * Cliente de uma app: `post.messages.send({ ... })`. Com `S`, só as rotas dos\n * escopos pedidos aparecem.\n */\nexport type ConnectionClient<K, S extends string = ConnectionScopeOf<K>> = [ConnectionAppOf<K>] extends [never]\n ? // Sem tipos gerados: qualquer rota, sem checagem.\n any\n : ConnectionAppOf<K> extends { routes: infer R }\n ? UnionToIntersection<\n { [N in keyof R & string]: R[N] extends { scope: S } ? PathToObject<N, RouteFn<R[N]>> : never }[keyof R & string]\n >\n : never;\n\n/** `connections.post(token)` — um cliente por app, pelo slug do catálogo. */\nexport type Connections = {\n [K in ConnectionAppSlug]: <const S extends ConnectionScopeOf<K> = ConnectionScopeOf<K>>(\n connectionToken: string,\n options?: ClientOptions_ & {\n /** Restringe os tipos às rotas destes escopos (os que a conexão pediu). Só tipos: quem decide é o token. */\n scopes?: readonly S[];\n },\n ) => ConnectionClient<K, S>;\n};\n\nfunction routeProxy(call: (route: string, input?: Record<string, unknown>) => Promise<unknown>, path: string[]): unknown {\n return new Proxy(function () {}, {\n get(_target, property) {\n // Não é uma Promise: `await connections.post(token)` não pode virar chamada.\n if (typeof property !== \"string\" || property === \"then\") return undefined;\n return routeProxy(call, [...path, property]);\n },\n apply(_target, _this, args: unknown[]) {\n if (path.length === 0) throw new Error(\"App Connections: chame uma rota, ex: post.messages.send({ ... }).\");\n return call(path.join(\".\"), args[0] as Record<string, unknown> | undefined);\n },\n });\n}\n\n/**\n * Clientes tipados das apps conectáveis, a partir do manifesto de cada uma\n * (`/.well-known/geekapps-app.json`, publicado pelo @geekapps/auth-fastify):\n *\n * const post = connections.post(connectionToken);\n * const message = await post.messages.send({ channel, to, text });\n *\n * Params, query e body vão num objeto só — o SDK separa pelo manifesto. O JWT\n * da conexão é obtido, guardado em cache e renovado sozinho. Erros viram\n * `AppConnectionError` com `code` tipado (\"missing_scope\", \"connection_revoked\"…).\n *\n * O slug só escolhe os tipos (gerados em `geekapps-connections/`, ver\n * `generateConnectionTypes`); a app alvo de verdade é a do token.\n */\nexport function createConnections(config: AppConnectionsConfig = {}): Connections {\n return new Proxy({} as Connections, {\n get(_target, slug) {\n if (typeof slug !== \"string\" || slug === \"then\") return undefined;\n return (connectionToken: string, options: ClientOptions_ = {}) =>\n routeProxy(\n (route, input) => callRoute(connectionToken, { config, ...options }, route, input),\n [],\n );\n },\n });\n}\n\n/** `createConnections()` com a configuração do .env. */\nexport const connections: Connections = createConnections();\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { cookies, headers } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\";\n\n/**\n * A sessão não vale mais: expirou, foi revogada no painel ou o refresh token\n * foi invalidado. Os cookies já foram limpos quando este erro é lançado, então\n * o tratamento normal é redirecionar para o login.\n */\nexport class GeekappsSessionExpiredError extends Error {\n readonly code = \"session_expired\" as const;\n\n constructor(message = \"A sessão expirou ou foi revogada. Entre novamente.\") {\n super(message);\n this.name = \"GeekappsSessionExpiredError\";\n }\n}\n\n/**\n * Fixed URL of the Geekapps Auth Server (OIDC issuer + API). This never\n * changes per-environment, so it is hardcoded rather than read from env.\n */\nexport const GEEKAPPS_AUTH_SERVER_URL = \"https://accounts-api.geekapps.com.br\";\n\n/** Erro padrão OAuth devolvido ao redirect_uri (`?error=&error_description=&state=`). */\nexport type AuthCallbackError = {\n error: \"invalid_request\" | \"interaction_required\" | \"access_denied\" | \"login_required\" | \"missing_code\" | \"missing_client_id\" | \"token_exchange_failed\" | (string & {});\n description: string;\n state: string | null;\n};\n\n/** Lê o erro OAuth de uma URL de callback; null quando não há `error`. */\nexport function parseAuthCallbackError(\n input: URL | URLSearchParams | string,\n): AuthCallbackError | null {\n const params =\n input instanceof URLSearchParams\n ? input\n : input instanceof URL\n ? input.searchParams\n : new URL(input, \"http://localhost\").searchParams;\n const error = params.get(\"error\");\n if (!error) return null;\n return {\n error,\n description: params.get(\"error_description\") ?? params.get(\"error_message\") ?? \"An authentication error occurred.\",\n state: params.get(\"state\"),\n };\n}\n\nexport type GeekappsAuthConfig = {\n /**\n * Chamado quando o callback recebe um erro OAuth (fluxo expirado, 2FA pendente\n * com prompt=none, acesso negado...). Devolva uma Response (ex.: redirect para\n * uma página sua) para assumir; devolva undefined para a página de erro padrão.\n */\n onCallbackError?: (\n error: AuthCallbackError,\n request: NextRequest,\n ) => Response | NextResponse | undefined | Promise<Response | NextResponse | undefined>;\n /**\n * Auth Server (OIDC) issuer URL. Defaults to GEEKAPPS_AUTH_SERVER_URL and\n * should not need overriding in normal use.\n *\n * Deprecated (still supported as a fallback): GEEKAPPS_AUTH_SERVER_ISSUER,\n * GEEKAPPS_AUTH_ISSUER, GEEKAPPS_AUTH_SERVER_URL env, GEEKAPPS_AUTH_API_URL\n */\n issuer?: string;\n\n /**\n * OAuth client id. Optional when GEEKAPPS_AUTH_CLIENT_ID is defined.\n */\n clientId?: string;\n\n /**\n * OAuth client secret for confidential clients. Optional when\n * GEEKAPPS_AUTH_CLIENT_SECRET is defined.\n */\n clientSecret?: string;\n\n /**\n * Callback URL. Optional when GEEKAPPS_AUTH_REDIRECT_URI is defined.\n * If omitted, it is inferred from the current request origin + basePath/callback.\n */\n redirectUri?: string;\n\n scopes?: string[];\n basePath?: string;\n /**\n * @deprecated cookiePrefix is now auto-derived from clientId.\n * This field is kept for backwards compatibility but is ignored.\n */\n cookiePrefix?: string;\n secureCookies?: boolean;\n\n /**\n * Friendly alias used by apps. Same as afterSignInRedirectTo.\n */\n redirectTo?: string;\n\n afterSignInRedirectTo?: string;\n afterSignOutRedirectTo?: string;\n};\n\ntype ResolvedGeekappsAuthConfig = GeekappsAuthConfig & {\n issuer: string;\n clientId: string;\n basePath: string;\n cookiePrefix: string;\n};\n\nexport type SessionTokens = {\n accessToken: string | null;\n refreshToken: string | null;\n idToken: string | null;\n sessionId: string | null;\n};\n\nexport type StoredAccount = {\n /** Stable identifier — `sub` from id_token or fallback to sessionId. */\n id: string;\n accessToken: string;\n refreshToken: string | null;\n idToken: string | null;\n sessionId: string | null;\n user: {\n sub: string;\n name?: string | null;\n email?: string | null;\n picture?: string | null;\n phone?: string | null;\n };\n};\n\nexport type ServerAuthApiClient = {\n get: <T>(path: string) => Promise<T>;\n post: <T>(path: string, body?: unknown) => Promise<T>;\n patch: <T>(path: string, body?: unknown) => Promise<T>;\n put: <T>(path: string, body?: unknown) => Promise<T>;\n delete: <T>(path: string) => Promise<T>;\n};\n\ntype JsonObject = Record<string, unknown>;\n\ntype TokenLikeResponse = JsonObject & {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n session_id?: string;\n sessionId?: string;\n tokens?: {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n session_id?: string;\n sessionId?: string;\n };\n};\n\nconst MAX_ACCOUNTS = 8;\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction normalizeBasePath(path = \"/api/auth\") {\n if (!path.startsWith(\"/\")) return `/${path}`;\n return path.replace(/\\/$/, \"\");\n}\n\nfunction createUrl(baseUrl: string, path: string) {\n if (/^https?:\\/\\//.test(path)) return path;\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n return `${normalizeBaseUrl(baseUrl)}${normalizedPath}`;\n}\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction resolveIssuer(input: GeekappsAuthConfig) {\n return normalizeBaseUrl(\n input.issuer ??\n env(\"GEEKAPPS_AUTH_SERVER_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_SERVER_URL\") ??\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL,\n );\n}\n\nfunction resolveClientId(input: GeekappsAuthConfig) {\n return input.clientId ?? env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n}\n\nfunction resolveClientSecret(input: GeekappsAuthConfig) {\n return input.clientSecret ?? env(\"GEEKAPPS_AUTH_CLIENT_SECRET\");\n}\n\n\nfunction resolveConfiguredRedirectUri(input: GeekappsAuthConfig) {\n return input.redirectUri ?? env(\"GEEKAPPS_AUTH_REDIRECT_URI\");\n}\n\n/**\n * Derives a short stable 8-char hex hash from a clientId string.\n * Uses a simple djb2-style hash — no crypto dependency needed.\n */\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0; // keep unsigned 32-bit\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\n/**\n * Auto-derives the cookie prefix from clientId.\n * Format: `gk_{8-char-hash}` — guaranteed unique per OAuth client.\n * Falls back to `gk` when clientId is empty (dev/test environments).\n */\nfunction deriveCookiePrefix(clientId: string): string {\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\n// GEEKAPPS_APP_URL é o override de produção: em prod o app roda atrás de\n// Docker sem proxy setando x-forwarded-host/x-forwarded-proto, então nem o\n// header Host nem request.nextUrl.origin são confiáveis (refletem o host\n// INTERNO do container, não o domínio público real) — por isso, quando\n// configurada, essa env var sempre vence. Sem ela (dev local, onde não está\n// setada no .env), deriva do próprio request — em localhost sem proxy o\n// Host já é localhost:PORTA mesmo, então funciona sem precisar de override.\nfunction requestOrigin(request: NextRequest) {\n const configured = env(\"GEEKAPPS_APP_URL\");\n if (configured) return configured.replace(/\\/$/, \"\");\n\n if (request.nextUrl?.origin) return request.nextUrl.origin;\n\n throw new Error(\n \"Cannot determine app origin. Set GEEKAPPS_APP_URL or ensure request.nextUrl is available.\",\n );\n}\n\nfunction resolveRedirectUri(config: ResolvedGeekappsAuthConfig, request: NextRequest) {\n return config.redirectUri ?? `${requestOrigin(request)}${config.basePath}/callback`;\n}\n\nfunction tokenFromResponse(data: TokenLikeResponse): SessionTokens {\n return {\n accessToken: data.accessToken ?? data.access_token ?? data.tokens?.accessToken ?? data.tokens?.access_token ?? null,\n refreshToken:\n data.refreshToken ?? data.refresh_token ?? data.tokens?.refreshToken ?? data.tokens?.refresh_token ?? null,\n idToken: data.idToken ?? data.id_token ?? data.tokens?.idToken ?? data.tokens?.id_token ?? null,\n sessionId: data.sessionId ?? data.session_id ?? data.tokens?.sessionId ?? data.tokens?.session_id ?? null,\n };\n}\n\n/** Decode a JWT payload without verifying signature (safe for client-visible claims). */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length < 2) return null;\n const payload = parts[1];\n if (!payload) return null;\n return JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction parseAccountFromJwt(tokens: SessionTokens): StoredAccount | null {\n if (!tokens.accessToken) return null;\n\n const jwtToParse = tokens.idToken ?? tokens.accessToken;\n const payload = decodeJwtPayload(jwtToParse);\n if (!payload) return null;\n\n const sub = typeof payload.sub === \"string\" ? payload.sub : null;\n const id = sub ?? tokens.sessionId;\n if (!id) return null;\n\n return {\n id,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n idToken: tokens.idToken ?? null,\n sessionId: tokens.sessionId,\n user: {\n sub: id,\n name: typeof payload.name === \"string\" ? payload.name : null,\n email: typeof payload.email === \"string\" ? payload.email : null,\n picture: typeof payload.picture === \"string\" ? payload.picture : null,\n phone: typeof payload.phone_number === \"string\" ? payload.phone_number : null,\n },\n };\n}\n\n/**\n * Cookie name scheme — 3 cookies per slot, each well under 4096 bytes:\n *\n * {prefix}_m{n} — metadata blob (base64url JSON): { id, sid?, n?, e?, p? }\n * Claims extracted from id_token at login. Never stores tokens.\n * {prefix}_at{n} — access token (httpOnly). ~1-2 KB.\n * {prefix}_rt{n} — refresh token (httpOnly). ~1 KB.\n * {prefix}_active — active slot index (\"1\", \"2\", …)\n * {prefix}_count — total stored slots\n * {prefix}_ao — active org id for this app\n * {prefix}_tp — non-httpOnly copy of active access token for client-side axios\n *\n * id_token is NOT stored in cookies — claims are extracted at login and cached in\n * the metadata cookie. This is the key size saving.\n */\nfunction getCookieNames(prefix: string) {\n return {\n active: `${prefix}_active`,\n count: `${prefix}_count`,\n activeOrganizationId: `${prefix}_ao`,\n tokenPub: `${prefix}_tp`,\n pkceVerifier: `${prefix}_pkce`,\n slotMeta: (n: number) => `${prefix}_m${n}`,\n slotAt: (n: number) => `${prefix}_at${n}`,\n slotRt: (n: number) => `${prefix}_rt${n}`,\n };\n}\n\nfunction generateCodeVerifier(): string {\n return randomBytes(64).toString(\"base64url\");\n}\n\nfunction createCodeChallenge(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n}\n\ntype SlotMeta = {\n id: string;\n sid?: string;\n n?: string;\n e?: string;\n p?: string;\n};\n\nfunction encodeMeta(account: StoredAccount): string {\n const m: SlotMeta = { id: account.id };\n if (account.sessionId) m.sid = account.sessionId;\n if (account.user.name) m.n = account.user.name;\n if (account.user.email) m.e = account.user.email;\n if (account.user.picture) m.p = account.user.picture;\n return Buffer.from(JSON.stringify(m), \"utf8\").toString(\"base64url\");\n}\n\nfunction decodeMeta(value: string): SlotMeta | null {\n try {\n const m = JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")) as SlotMeta;\n return m.id ? m : null;\n } catch {\n return null;\n }\n}\n\nfunction getCookieOptions(config: ResolvedGeekappsAuthConfig) {\n return {\n httpOnly: true,\n secure: config.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n };\n}\n\nasync function readBody(request: NextRequest) {\n const text = await request.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction appendSearchParams(url: string, request: NextRequest) {\n const target = new URL(url);\n request.nextUrl.searchParams.forEach((value, key) => {\n target.searchParams.set(key, value);\n });\n return target.toString();\n}\n\nfunction routeFromRequest(request: NextRequest, basePath: string) {\n const pathname = request.nextUrl.pathname;\n if (!pathname.startsWith(basePath)) return pathname;\n const route = pathname.slice(basePath.length);\n return route || \"/\";\n}\n\nfunction isJsonResponse(response: Response) {\n return response.headers.get(\"content-type\")?.includes(\"application/json\");\n}\n\nasync function responseFromApi(response: Response) {\n if (response.status === 204) return new NextResponse(null, { status: 204 });\n const body = isJsonResponse(response) ? await response.json().catch(() => null) : await response.text();\n return NextResponse.json(body, { status: response.status });\n}\n\nconst _remoteJWKSCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\ntype ServiceTokenCache = { token: string; expiresAt: number };\nconst _serviceTokenCache = new Map<string, ServiceTokenCache>();\n\nasync function fetchServiceToken(issuer: string, clientId: string, clientSecret: string): Promise<string | null> {\n const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\");\n try {\n const res = await fetch(createUrl(issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${credentials}`,\n },\n body: \"grant_type=service_account\",\n cache: \"no-store\",\n });\n if (!res.ok) return null;\n const data = (await res.json()) as { access_token?: string };\n return data.access_token ?? null;\n } catch {\n return null;\n }\n}\n\nasync function getServiceToken(issuer: string, clientId: string, clientSecret: string): Promise<string | null> {\n const key = `${issuer}:${clientId}`;\n const now = Date.now();\n const cached = _serviceTokenCache.get(key);\n if (cached && cached.expiresAt - 30_000 > now) return cached.token;\n const token = await fetchServiceToken(issuer, clientId, clientSecret);\n if (!token) return null;\n _serviceTokenCache.set(key, { token, expiresAt: now + 420_000 });\n return token;\n}\n\ntype ServiceAccountConfig = {\n issuer?: string;\n clientId?: string;\n clientSecret?: string;\n};\n\n/**\n * Returns a service account access token.\n * The issuer is always the Geekapps Auth Server (GEEKAPPS_AUTH_SERVER_URL) —\n * it does not need to be configured. Reads GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID,\n * GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET by default.\n * Any field can be overridden via the optional config argument.\n * Tokens are cached per client and auto-renewed before expiry (~7 min TTL).\n */\nexport function getServiceAccountToken(override: ServiceAccountConfig = {}): Promise<string | null> {\n const issuer = normalizeBaseUrl(\n override.issuer ??\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL,\n );\n const clientId = override.clientId ?? env(\"GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID\") ?? \"\";\n const clientSecret = override.clientSecret ?? env(\"GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET\") ?? \"\";\n if (!clientId || !clientSecret) return Promise.resolve(null);\n return getServiceToken(issuer, clientId, clientSecret);\n}\n\nfunction getRemoteJWKS(issuer: string) {\n if (!_remoteJWKSCache.has(issuer)) {\n _remoteJWKSCache.set(issuer, createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)));\n }\n return _remoteJWKSCache.get(issuer)!;\n}\n\nexport type VerifyTokenOptions = {\n /** client_id desta app. Quando informado, o token precisa ter sido emitido para ela (`aud`). */\n audience?: string;\n /** Credenciais desta app, para conferir se uma App Connection ainda está ativa. */\n clientId?: string;\n clientSecret?: string;\n};\n\n/** Informações de App Connection lidas de um token já verificado. */\nexport type TokenConnectionInfo = {\n /** true quando outra app está agindo em nome do usuário, sem ele presente. */\n isAppConnection: boolean;\n /** Application que está agindo (claim `act.sub`). */\n actorAppId: string | null;\n /** Id da App Connection. */\n connectionId: string | null;\n};\n\nexport function getTokenConnectionInfo(payload: JWTPayload): TokenConnectionInfo {\n const isAppConnection = payload.token_type === \"app_connection\";\n const act = payload.act as { sub?: unknown } | undefined;\n return {\n isAppConnection,\n actorAppId: isAppConnection && typeof act?.sub === \"string\" ? act.sub : null,\n connectionId: isAppConnection && typeof payload.connection_id === \"string\" ? payload.connection_id : null,\n };\n}\n\n/**\n * Pergunta ao Auth se a App Connection segue ativa. Sem cache: revogar\n * corta o acesso na próxima requisição.\n */\nasync function isAppConnectionActive(\n issuer: string,\n connectionId: string,\n clientId: string,\n clientSecret: string,\n): Promise<boolean> {\n try {\n const response = await fetch(`${issuer}/connections/introspect`, {\n method: \"POST\",\n headers: {\n authorization: `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString(\"base64\")}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify({ connection_id: connectionId }),\n cache: \"no-store\",\n });\n const body = (await response.json().catch(() => null)) as { active?: boolean } | null;\n return response.ok && body?.active === true;\n } catch {\n return false;\n }\n}\n\nexport async function verifyToken(\n token: string,\n issuer: string,\n options: VerifyTokenOptions = {},\n): Promise<JWTPayload | null> {\n let payload: JWTPayload;\n try {\n ({ payload } = await jwtVerify(token, getRemoteJWKS(issuer), {\n algorithms: [\"RS256\"],\n issuer,\n // aud = issuer também passa: é o que os tokens do painel do Auth usam.\n ...(options.audience ? { audience: [options.audience, issuer] } : {}),\n }));\n } catch {\n return null;\n }\n\n // Token de App Connection: só vale se a conexão continua ativa agora.\n const info = getTokenConnectionInfo(payload);\n if (info.isAppConnection) {\n if (!info.connectionId || !options.clientId || !options.clientSecret) return null;\n const active = await isAppConnectionActive(issuer, info.connectionId, options.clientId, options.clientSecret);\n if (!active) return null;\n }\n\n return payload;\n}\n\n// Renovações em andamento por refresh token (por processo) — ver tryRefreshTokens.\nconst inflightServerRefreshes = new Map<string, Promise<SessionTokens | null>>();\n\nexport function configureGeekappsAuth(input: GeekappsAuthConfig = {}) {\n const clientId = resolveClientId(input);\n const config: ResolvedGeekappsAuthConfig = {\n ...input,\n issuer: resolveIssuer(input),\n clientId,\n clientSecret: resolveClientSecret(input),\n redirectUri: resolveConfiguredRedirectUri(input),\n afterSignInRedirectTo:\n input.afterSignInRedirectTo ?? input.redirectTo ?? \"/\",\n basePath: normalizeBasePath(input.basePath),\n cookiePrefix: deriveCookiePrefix(clientId),\n };\n\n const cn = getCookieNames(config.cookiePrefix);\n\n // ── Slot helpers ──────────────────────────────────────────────────────────\n\n async function getCount(): Promise<number> {\n const cookieStore = await cookies();\n const raw = cookieStore.get(cn.count)?.value;\n const n = raw ? Number.parseInt(raw, 10) : 0;\n return Number.isNaN(n) ? 0 : n;\n }\n\n async function getActiveIndex(): Promise<number> {\n const cookieStore = await cookies();\n const raw = cookieStore.get(cn.active)?.value;\n if (!raw) return 0;\n const n = Number.parseInt(raw, 10);\n return Number.isNaN(n) ? 0 : n;\n }\n\n function readSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n ): StoredAccount | null {\n const at = cookieStore.get(cn.slotAt(n))?.value;\n if (!at) return null;\n const meta = decodeMeta(cookieStore.get(cn.slotMeta(n))?.value ?? \"\");\n if (!meta) return null;\n return {\n id: meta.id,\n accessToken: at,\n refreshToken: cookieStore.get(cn.slotRt(n))?.value ?? null,\n idToken: null, // not stored in cookies; claims are in meta\n sessionId: meta.sid ?? null,\n user: {\n sub: meta.id,\n name: meta.n ?? null,\n email: meta.e ?? null,\n picture: meta.p ?? null,\n },\n };\n }\n\n function writeSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n account: StoredAccount,\n ) {\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n cookieStore.set(cn.slotMeta(n), encodeMeta(account), opts);\n cookieStore.set(cn.slotAt(n), account.accessToken, opts);\n if (account.refreshToken) cookieStore.set(cn.slotRt(n), account.refreshToken, opts);\n }\n\n function clearSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n ) {\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n cookieStore.set(cn.slotMeta(n), \"\", clear);\n cookieStore.set(cn.slotAt(n), \"\", clear);\n cookieStore.set(cn.slotRt(n), \"\", clear);\n }\n\n // ── Public account helpers ─────────────────────────────────────────────────\n\n async function getAccountsList(): Promise<StoredAccount[]> {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const accounts: StoredAccount[] = [];\n for (let i = 1; i <= count; i++) {\n const slot = readSlot(cookieStore, i);\n if (slot) accounts.push(slot);\n }\n return accounts;\n }\n\n async function getActiveAccount(): Promise<StoredAccount | null> {\n const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);\n if (count === 0) return null;\n if (activeIdx >= 1 && activeIdx <= count) {\n const slot = readSlot(cookieStore, activeIdx);\n if (slot) return slot;\n }\n // Fall back to slot 1\n return readSlot(cookieStore, 1);\n }\n\n async function getAccessToken() {\n // Prefer the token injected by the middleware via request header (same-request refresh)\n try {\n const hdrs = await headers();\n const auth = hdrs.get(\"authorization\");\n if (auth?.startsWith(\"Bearer \")) return auth.slice(7);\n } catch {}\n const active = await getActiveAccount();\n return active?.accessToken ?? null;\n }\n\n async function getRefreshToken() {\n const active = await getActiveAccount();\n return active?.refreshToken ?? null;\n }\n\n async function getSession(): Promise<SessionTokens | null> {\n const active = await getActiveAccount();\n if (!active) return null;\n return {\n accessToken: active.accessToken,\n refreshToken: active.refreshToken,\n idToken: active.idToken,\n sessionId: active.sessionId,\n };\n }\n\n async function getUser(): Promise<StoredAccount[\"user\"] | null> {\n const active = await getActiveAccount();\n return active?.user ?? null;\n }\n\n async function getUserById(userId: string, applicationId?: string): Promise<unknown | null> {\n const qs = new URLSearchParams({ ...(applicationId ? { applicationId } : {}) });\n try {\n return await apiRequest<unknown>(`/users/${userId}?${qs}`);\n } catch {\n return null;\n }\n }\n\n async function getOrganizations(): Promise<{ organizations: unknown[]; currentOrgId: string | null }> {\n const cookieStore = await cookies();\n const currentOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n try {\n const data = await apiRequest<unknown[] | { organizations: unknown[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: unknown[] }).organizations;\n return { organizations, currentOrgId };\n } catch {\n return { organizations: [], currentOrgId };\n }\n }\n\n function clearPublicTokenCookie(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n ) {\n // Clear legacy _tp cookie if present\n if (cookieStore.get(cn.tokenPub)?.value) {\n cookieStore.set(cn.tokenPub, \"\", { httpOnly: false, maxAge: 0, path: \"/\" });\n }\n }\n\n async function saveAccountToList(tokens: SessionTokens) {\n const account = parseAccountFromJwt(tokens);\n if (!account) return;\n\n const cookieStore = await cookies();\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n const count = await getCount();\n\n // Find existing slot for this account id\n let targetSlot = 0;\n for (let i = 1; i <= count; i++) {\n const existing = readSlot(cookieStore, i);\n if (existing?.id === account.id) {\n targetSlot = i;\n break;\n }\n }\n\n if (targetSlot === 0) {\n if (count >= MAX_ACCOUNTS) return;\n targetSlot = count + 1;\n cookieStore.set(cn.count, String(targetSlot), opts);\n }\n\n writeSlot(cookieStore, targetSlot, account);\n cookieStore.set(cn.active, String(targetSlot), opts);\n clearPublicTokenCookie(cookieStore);\n }\n\n // Alias for backwards compat\n const setSessionCookies = saveAccountToList;\n\n async function switchAccount(accountId: string): Promise<boolean> {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n\n for (let i = 1; i <= count; i++) {\n const slot = readSlot(cookieStore, i);\n if (slot && (slot.id === accountId || slot.sessionId === accountId)) {\n cookieStore.set(cn.active, String(i), opts);\n clearPublicTokenCookie(cookieStore);\n return true;\n }\n }\n return false;\n }\n\n async function removeActiveAccount() {\n const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n\n if (count === 0) return;\n\n const remaining: StoredAccount[] = [];\n for (let i = 1; i <= count; i++) {\n if (i === activeIdx) continue;\n const slot = readSlot(cookieStore, i);\n if (slot) remaining.push(slot);\n }\n\n for (let i = 1; i <= count; i++) {\n clearSlot(cookieStore, i);\n }\n\n if (remaining.length === 0) {\n cookieStore.set(cn.count, \"0\", clear);\n cookieStore.set(cn.active, \"0\", clear);\n await clearPublicTokenCookie(cookieStore);\n return;\n }\n\n // Re-compact remaining accounts into slots 1..n\n for (let i = 0; i < remaining.length; i++) {\n await writeSlot(cookieStore, i + 1, remaining[i]!);\n }\n cookieStore.set(cn.count, String(remaining.length), opts);\n cookieStore.set(cn.active, \"1\", opts);\n clearPublicTokenCookie(cookieStore);\n }\n\n async function clearAllSessionCookies() {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n for (let i = 1; i <= count; i++) {\n clearSlot(cookieStore, i);\n }\n cookieStore.set(cn.count, \"0\", clear);\n cookieStore.set(cn.active, \"0\", clear);\n clearPublicTokenCookie(cookieStore);\n }\n\n async function tryRefreshTokens(): Promise<string | null> {\n const refreshToken = await getRefreshToken();\n if (!refreshToken || !config.clientId) return null;\n\n // Chamadas simultâneas com o mesmo refresh token dividem uma renovação só\n // (ver middleware): renovar duas vezes com o mesmo token parece reuso.\n let pending = inflightServerRefreshes.get(refreshToken);\n if (!pending) {\n pending = refreshWithToken(refreshToken).finally(() => {\n setTimeout(() => inflightServerRefreshes.delete(refreshToken), 10_000);\n });\n inflightServerRefreshes.set(refreshToken, pending);\n }\n const tokens = await pending;\n if (!tokens?.accessToken) return null;\n\n // Em Server Component o Next não deixa gravar cookie: a renovação vale\n // para esta requisição, e o middleware grava o par novo na próxima. O\n // auth-server aceita a reapresentação do token antigo nesse caso.\n try {\n await saveAccountToList(tokens);\n } catch {\n // cookies() somente leitura neste contexto\n }\n return tokens.accessToken;\n }\n\n async function refreshWithToken(refreshToken: string) {\n\n const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret ?? \"\"}`).toString(\"base64\");\n\n const response = await fetch(createUrl(config.issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n }),\n cache: \"no-store\",\n });\n\n if (!response.ok) return null;\n\n const data = (await response.json().catch(() => null)) as TokenLikeResponse | null;\n if (!data) return null;\n\n const tokens = tokenFromResponse(data);\n return tokens.accessToken ? tokens : null;\n }\n\n async function apiRequest<T>(path: string, init: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n const requestHeaders = new Headers(init.headers);\n\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (init.body !== undefined && !requestHeaders.has(\"content-type\")) {\n requestHeaders.set(\"content-type\", \"application/json\");\n }\n\n const url = new URL(createUrl(config.issuer, path));\n\n const response = await fetch(url, {\n ...init,\n headers: requestHeaders,\n cache: \"no-store\",\n });\n\n if (response.status === 401) {\n const newAccessToken = await tryRefreshTokens();\n\n if (!newAccessToken) {\n // Sem renovar depois de um 401, a sessão acabou: expirou, foi revogada\n // no painel ou o refresh token caiu. Os cookies apontam para algo que\n // não existe mais, então saem — senão toda requisição seguinte repete\n // o mesmo ciclo. Quem chamou recebe um erro que dá para tratar, em vez\n // de \"Auth API error 401\" vindo do fundo da pilha.\n await clearAllSessionCookies().catch(() => {});\n throw new GeekappsSessionExpiredError();\n }\n\n if (newAccessToken) {\n const retryHeaders = new Headers(init.headers);\n retryHeaders.set(\"authorization\", `Bearer ${newAccessToken}`);\n if (init.body !== undefined && !retryHeaders.has(\"content-type\")) {\n retryHeaders.set(\"content-type\", \"application/json\");\n }\n const retryUrl = new URL(createUrl(config.issuer, path));\n const retryResponse = await fetch(retryUrl, {\n ...init,\n headers: retryHeaders,\n cache: \"no-store\",\n });\n if (!retryResponse.ok) {\n const error = await retryResponse.json().catch(() => null);\n throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${retryResponse.status}`);\n }\n if (retryResponse.status === 204) return null as T;\n return (await retryResponse.json()) as T;\n }\n }\n\n if (!response.ok) {\n const error = await response.json().catch(() => null);\n throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${response.status}`);\n }\n\n if (response.status === 204) return null as T;\n return (await response.json()) as T;\n }\n\n function createApiClient(): ServerAuthApiClient {\n return {\n get: (path) => apiRequest(path),\n post: (path, body) => apiRequest(path, { method: \"POST\", body: body === undefined ? undefined : JSON.stringify(body) }),\n patch: (path, body) => apiRequest(path, { method: \"PATCH\", body: body === undefined ? undefined : JSON.stringify(body) }),\n put: (path, body) => apiRequest(path, { method: \"PUT\", body: body === undefined ? undefined : JSON.stringify(body) }),\n delete: (path) => apiRequest(path, { method: \"DELETE\" }),\n };\n }\n\n async function requireUser(options: { redirectTo?: string } = {}) {\n const accessToken = await getAccessToken();\n if (!accessToken) {\n redirect(options.redirectTo ?? \"/\");\n }\n return accessToken;\n }\n\n function escapeHtml(str: string): string {\n return str.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n }\n\n function sanitizeRedirectTo(value: string | null | undefined): string {\n const raw = value ?? \"/\";\n // Never redirect back into the auth basePath — that would loop or replay a stale code.\n if (raw.startsWith(config.basePath)) return \"/\";\n // Only allow relative paths to prevent open-redirect.\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n return \"/\";\n }\n\n async function createAuthorizeRedirect(request: NextRequest, mode: \"login\" | \"register\" | \"add-account\" | \"google\") {\n const redirectTo = sanitizeRedirectTo(request.nextUrl.searchParams.get(\"redirectTo\") ?? config.afterSignInRedirectTo);\n const flowId = request.nextUrl.searchParams.get(\"flow_id\");\n const url = new URL(createUrl(config.issuer, mode === \"google\" ? \"/auth/google\" : \"/authorize\"));\n\n if (!config.clientId) {\n return NextResponse.json(\n { error: \"missing_client_id\", message: \"Missing GEEKAPPS_AUTH_CLIENT_ID or clientId.\" },\n { status: 500 },\n );\n }\n\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", resolveRedirectUri(config, request));\n url.searchParams.set(\"response_type\", \"code\");\n const scopes = config.scopes ?? [\"openid\", \"profile\", \"email\"];\n if (!scopes.includes(\"offline_access\")) scopes.push(\"offline_access\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", Buffer.from(JSON.stringify({ redirectTo, mode })).toString(\"base64url\"));\n if (mode === \"register\") url.searchParams.set(\"screen_hint\", \"signup\");\n if (mode === \"add-account\") {\n // Adding an account always needs a fresh login, regardless of any existing session.\n url.searchParams.set(\"prompt\", \"login\");\n } else {\n // `prompt` (login | select_account | none | consent) is forwarded as-is to\n // the IDP — see OidcPrompt in client.tsx for what each value does.\n const prompt = request.nextUrl.searchParams.get(\"prompt\");\n // Legacy fallback: silentLogin=false is equivalent to prompt=login.\n const silentLogin = request.nextUrl.searchParams.get(\"silentLogin\");\n const resolvedPrompt = prompt ?? (silentLogin === \"false\" ? \"login\" : null);\n if (resolvedPrompt) url.searchParams.set(\"prompt\", resolvedPrompt);\n }\n if (flowId) url.searchParams.set(\"flow_id\", flowId);\n\n const codeVerifier = generateCodeVerifier();\n url.searchParams.set(\"code_challenge\", createCodeChallenge(codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n const response = NextResponse.redirect(url);\n const cn = getCookieNames(config.cookiePrefix);\n response.cookies.set(cn.pkceVerifier, codeVerifier, {\n ...getCookieOptions(config),\n maxAge: 60 * 10,\n });\n return response;\n }\n\n async function exchangeCode(request: NextRequest) {\n const code = request.nextUrl.searchParams.get(\"code\");\n const state = request.nextUrl.searchParams.get(\"state\");\n\n const origin = requestOrigin(request);\n\n if (!code) {\n const err = request.nextUrl.searchParams.get(\"error\") ?? \"missing_code\";\n const desc = request.nextUrl.searchParams.get(\"error_description\") ?? request.nextUrl.searchParams.get(\"error_message\") ?? \"No authorization code received\";\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${encodeURIComponent(err)}&error_description=${encodeURIComponent(desc)}${state ? `&state=${encodeURIComponent(state)}` : \"\"}`, origin));\n }\n\n if (!config.clientId) {\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=missing_client_id&error_description=Auth+client+is+not+configured`, origin));\n }\n\n const basicAuth = Buffer.from(\n `${config.clientId}:${config.clientSecret ?? \"\"}`,\n ).toString(\"base64\");\n\n const cn = getCookieNames(config.cookiePrefix);\n const cookieStore = await cookies();\n const codeVerifier = cookieStore.get(cn.pkceVerifier)?.value;\n cookieStore.set(cn.pkceVerifier, \"\", { ...getCookieOptions(config), maxAge: 0 });\n\n const tokenBody = new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: resolveRedirectUri(config, request),\n });\n if (codeVerifier) tokenBody.set(\"code_verifier\", codeVerifier);\n\n const response = await fetch(createUrl(config.issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: tokenBody,\n cache: \"no-store\",\n });\n\n const data = (await response.json().catch(() => ({}))) as TokenLikeResponse & { error?: string; error_description?: string };\n if (!response.ok) {\n const error = encodeURIComponent((data.error as string | undefined) ?? \"token_exchange_failed\");\n const desc = encodeURIComponent((data.error_description as string | undefined) ?? `Token exchange failed with status ${response.status}`);\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${error}&error_description=${desc}`, origin));\n }\n\n await saveAccountToList(tokenFromResponse(data));\n\n let redirectTo = sanitizeRedirectTo(config.afterSignInRedirectTo);\n if (state) {\n try {\n const parsed = JSON.parse(Buffer.from(state, \"base64url\").toString(\"utf8\")) as { redirectTo?: string };\n redirectTo = sanitizeRedirectTo(parsed.redirectTo);\n } catch {\n // ignore malformed state\n }\n }\n\n return NextResponse.redirect(new URL(redirectTo, origin));\n }\n\n function mapLocalPathToApiPath(route: string) {\n if (route === \"/me\") return \"/auth/me\";\n if (route === \"/accounts/switch\") return \"/auth/accounts/switch\";\n if (route === \"/organizations/active\") return \"/auth/organizations/active\";\n if (route === \"/permissions/check\") return \"/permissions/check\";\n if (route === \"/login/password\") return \"/auth/login\";\n if (route === \"/register/password\") return \"/auth/register\";\n if (route === \"/passwordless/link\") return \"/auth/passwordless/request\";\n return route;\n }\n\n async function handleAccountsLocal(): Promise<NextResponse> {\n const [accounts, activeIdx] = await Promise.all([getAccountsList(), getActiveIndex()]);\n\n return NextResponse.json({\n accounts: accounts.map((a, i) => ({\n id: a.id,\n sessionId: a.sessionId ?? a.id,\n isActive: i + 1 === activeIdx,\n user: {\n id: a.user.sub,\n name: a.user.name ?? null,\n email: a.user.email ?? null,\n imageUrl: a.user.picture ?? null,\n phone: a.user.phone ?? null,\n },\n })),\n });\n }\n\n async function handleOrganizationsLocal(): Promise<NextResponse> {\n const cookieStore = await cookies();\n const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n try {\n const data = await apiRequest<unknown[]|{ organizations: unknown[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: unknown[] }).organizations;\n return NextResponse.json({ organizations, currentOrgId: activeOrgId });\n } catch {\n return NextResponse.json({ organizations: [], currentOrgId: activeOrgId });\n }\n }\n\n async function handleSwitchAccountLocal(request: NextRequest): Promise<NextResponse> {\n const body = await readBody(request);\n const targetId =\n body && typeof body === \"object\"\n ? String(\n (body as Record<string, unknown>).accountId ??\n (body as Record<string, unknown>).sessionId ??\n \"\",\n )\n : \"\";\n\n if (!targetId) {\n return NextResponse.json({ error: \"accountId required\" }, { status: 400 });\n }\n\n const switched = await switchAccount(targetId);\n if (!switched) {\n return NextResponse.json({ error: \"Account not found\" }, { status: 404 });\n }\n\n return NextResponse.json({ ok: true });\n }\n\n async function proxy(request: NextRequest, route: string) {\n const accessToken = await getAccessToken();\n const body = [\"GET\", \"HEAD\"].includes(request.method) ? undefined : await readBody(request);\n const url = new URL(appendSearchParams(createUrl(config.issuer, mapLocalPathToApiPath(route)), request));\n const requestHeaders = new Headers();\n\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (body !== undefined) requestHeaders.set(\"content-type\", \"application/json\");\n\n let response = await fetch(url, {\n method: request.method,\n headers: requestHeaders,\n body: body === undefined ? undefined : typeof body === \"string\" ? body : JSON.stringify(body),\n cache: \"no-store\",\n });\n\n if (response.status === 401) {\n // Mesmo tratamento do apiRequest: tenta renovar uma vez e, se a sessão\n // já não existe (expirada ou revogada no painel), limpa os cookies. Sem\n // isso o cliente repetiria o mesmo token morto a cada chamada.\n const refreshed = await tryRefreshTokens();\n\n if (refreshed) {\n requestHeaders.set(\"authorization\", `Bearer ${refreshed}`);\n response = await fetch(url, {\n method: request.method,\n headers: requestHeaders,\n body:\n body === undefined\n ? undefined\n : typeof body === \"string\"\n ? body\n : JSON.stringify(body),\n cache: \"no-store\",\n });\n } else {\n await clearAllSessionCookies().catch(() => {});\n return NextResponse.json(\n { error: \"session_expired\" },\n { status: 401 },\n );\n }\n }\n\n const data = isJsonResponse(response) ? ((await response.json().catch(() => null)) as TokenLikeResponse | null) : null;\n if (data) {\n const tokens = tokenFromResponse(data);\n if (tokens.accessToken || tokens.refreshToken || tokens.idToken || tokens.sessionId) {\n await saveAccountToList(tokens);\n }\n return NextResponse.json(data, { status: response.status });\n }\n\n return responseFromApi(response);\n }\n\n async function handle(request: NextRequest) {\n const route = routeFromRequest(request, config.basePath);\n\n if (request.method === \"GET\" && route === \"/sign-in\") return createAuthorizeRedirect(request, \"login\");\n if (request.method === \"GET\" && route === \"/sign-up\") return createAuthorizeRedirect(request, \"register\");\n if (request.method === \"GET\" && route === \"/add-account\") return createAuthorizeRedirect(request, \"add-account\");\n if (request.method === \"GET\" && route === \"/login/google\") return createAuthorizeRedirect(request, \"google\");\n if (request.method === \"GET\" && route === \"/callback\") {\n const callbackError = parseAuthCallbackError(request.nextUrl.searchParams);\n if (callbackError) {\n const custom = await config.onCallbackError?.(callbackError, request);\n if (custom) return custom;\n const error = callbackError.error;\n const description = callbackError.description;\n return new NextResponse(\n `<!doctype html><html><head><meta charset=\"utf-8\"><title>Authentication Error</title></head><body style=\"font-family:sans-serif;max-width:480px;margin:80px auto;padding:0 16px\"><h2>Authentication Error</h2><p><strong>${escapeHtml(error)}</strong></p><p>${escapeHtml(description)}</p><a href=\"/\">Go back</a></body></html>`,\n { status: 400, headers: { \"content-type\": \"text/html; charset=utf-8\" } },\n );\n }\n return exchangeCode(request);\n }\n if (request.method === \"GET\" && route === \"/token\") {\n const accessToken = await getAccessToken();\n if (!accessToken) return NextResponse.json({ error: \"missing_token\" }, { status: 401 });\n return NextResponse.json({ accessToken });\n }\n\n\n if (request.method === \"GET\" && route === \"/accounts\") return handleAccountsLocal();\n if (request.method === \"POST\" && route === \"/accounts/switch\") return handleSwitchAccountLocal(request);\n if (request.method === \"GET\" && route === \"/organizations\") return handleOrganizationsLocal();\n\n if (request.method === \"POST\" && route === \"/organizations\") {\n const accessToken = await getAccessToken();\n const body = (await readBody(request)) as Record<string, unknown> | undefined;\n if (body && accessToken && !body.applicationId) {\n const claims = decodeJwtPayload(accessToken);\n const applicationId =\n (typeof claims?.application_id === \"string\" ? claims.application_id : null) ??\n (typeof claims?.client_id === \"string\" ? claims.client_id : null) ??\n (typeof claims?.applicationId === \"string\" ? claims.applicationId : null);\n if (applicationId) body.applicationId = applicationId;\n }\n const orgUrl = new URL(createUrl(config.issuer, \"/organizations\"));\n const requestHeaders = new Headers();\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (body !== undefined) requestHeaders.set(\"content-type\", \"application/json\");\n const response = await fetch(orgUrl, {\n method: \"POST\",\n headers: requestHeaders,\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n return responseFromApi(response);\n }\n\n if (request.method === \"POST\" && route === \"/sign-out\") {\n await apiRequest(\"/auth/sign-out\", { method: \"POST\" }).catch(() => null);\n await removeActiveAccount();\n return NextResponse.json({ ok: true });\n }\n\n if (request.method === \"POST\" && route === \"/sign-out-all\") {\n await apiRequest(\"/auth/sign-out-all\", { method: \"POST\" }).catch(() => null);\n await clearAllSessionCookies();\n return NextResponse.json({ ok: true });\n }\n\n if (request.method === \"POST\" && route === \"/organizations/active\") {\n const body = await readBody(request);\n const organizationId =\n body && typeof body === \"object\" && \"organizationId\" in body\n ? (body as Record<string, unknown>).organizationId\n : null;\n const response = NextResponse.json({ ok: true });\n if (typeof organizationId === \"string\") {\n const cookieOpts = {\n httpOnly: true,\n sameSite: \"lax\" as const,\n path: \"/\",\n secure: process.env.NODE_ENV === \"production\",\n maxAge: 60 * 60 * 24 * 7,\n };\n response.cookies.set(cn.activeOrganizationId, organizationId, cookieOpts);\n }\n return response;\n }\n\n return proxy(request, route);\n }\n\n return {\n handlers: {\n GET: handle,\n POST: handle,\n PATCH: handle,\n DELETE: handle,\n },\n GetSession: getSession,\n GetUser: getUser,\n GetUserById: getUserById,\n GetOrganizations: getOrganizations,\n SaveSession: saveAccountToList,\n GetAccessToken: getAccessToken,\n GetRefreshToken: getRefreshToken,\n IsAuthenticated: async () => (await getAccessToken()) !== null,\n TryRefreshTokens: tryRefreshTokens,\n RequireUser: requireUser,\n CreateAuthApiClient: createApiClient,\n cookiePrefix: config.cookiePrefix,\n VerifyToken: (token: string) =>\n verifyToken(token, config.issuer, {\n audience: config.clientId || undefined,\n clientId: config.clientId || undefined,\n clientSecret: config.clientSecret,\n }),\n GetTokenConnectionInfo: getTokenConnectionInfo,\n SignInUrl: async (redirectTo?: string) => {\n const configured = env(\"GEEKAPPS_APP_URL\");\n if (!configured) throw new Error(\"Cannot determine app origin. Set GEEKAPPS_APP_URL.\");\n const origin = configured.replace(/\\/$/, \"\");\n const url = new URL(`${origin}${config.basePath}/sign-in`);\n if (redirectTo) url.searchParams.set(\"redirectTo\", redirectTo);\n return url.toString();\n },\n };\n}\n\nexport function handlers(options?: GeekappsAuthConfig) {\n return configureGeekappsAuth(options).handlers;\n}\n\n/**\n * Returns true if the current request has a valid access token in cookies.\n * Safe to call in Server Components, Server Actions, and Route Handlers.\n */\nexport async function isAuthenticated(options?: GeekappsAuthConfig): Promise<boolean> {\n const { GetAccessToken } = configureGeekappsAuth(options);\n return (await GetAccessToken()) !== null;\n}\n"],"mappings":"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","names":["createHash","randomBytes","cookies","redirect","randomBytes","cookies","createHash","redirect","fetchCatalog","headers","properties"]}
|