import { createAuthenticatedFetch, kody } from 'kody:runtime' import type { LinearAuthInput, LinearAuthMode } from './types.ts' import { optionalString, requireString } from './types.ts' import { DEFAULT_API_KEY_SECRET, DEFAULT_INTEGRATION_NAME, LINEAR_GRAPHQL_URL, missingCredentialsMessage, } from './setup.ts' const authenticatedFetches = new Map() export type ResolvedLinearAuth = { mode: LinearAuthMode integrationName: string secretName: string authorization: string } function secretEntries( result: unknown, ): Array<{ name?: string; scope?: string }> { if ( result && typeof result === 'object' && Array.isArray((result as { secrets?: unknown }).secrets) ) { return (result as { secrets: Array<{ name?: string; scope?: string }> }).secrets } return Array.isArray(result) ? result : [] } export async function listUserSecretNames(): Promise> { const listed = await kody.secretList({ scope: 'user' }) const names = new Set() for (const entry of secretEntries(listed)) { if (entry?.name && (entry.scope === 'user' || !entry.scope)) { names.add(entry.name) } } return names } export async function listLinearIntegrationNames(): Promise> { const listed = await kody.integrationList({}) const integrations = listed && typeof listed === 'object' && Array.isArray((listed as { integrations?: unknown }).integrations) ? (listed as { integrations: Array<{ name?: string }> }).integrations : Array.isArray(listed) ? listed : [] return integrations .map((item) => (typeof item?.name === 'string' ? item.name : '')) .filter((name) => name === 'linear' || name.startsWith('linear-')) } export function resolveIntegrationName(input: LinearAuthInput = {}): string { const explicit = optionalString(input.integrationName, 'integrationName') ?? optionalString(input.integration, 'integration') if (explicit) return explicit const account = optionalString(input.account, 'account') if (!account || account === 'default' || account === 'linear') { return DEFAULT_INTEGRATION_NAME } if (account.startsWith('linear-') || account === 'linear') return account return `linear-${account}` } export function resolveSecretName(input: LinearAuthInput = {}): string { const explicit = optionalString(input.secretName, 'secretName') if (explicit) return explicit const integrationName = resolveIntegrationName(input) if (integrationName === DEFAULT_INTEGRATION_NAME) return DEFAULT_API_KEY_SECRET const suffix = integrationName.startsWith('linear-') ? integrationName.slice('linear-'.length) : integrationName return `linearApiKey-${suffix}` } export async function resolveLinearAuth( input: LinearAuthInput = {}, ): Promise { const integrationName = resolveIntegrationName(input) const secretName = resolveSecretName(input) const forced = input.auth if (forced !== undefined && forced !== 'oauth' && forced !== 'api-key') { throw new Error('auth must be "oauth" or "api-key".') } const [secrets, integrations] = await Promise.all([ listUserSecretNames(), listLinearIntegrationNames(), ]) const hasOauth = integrations.includes(integrationName) const hasApiKey = secrets.has(secretName) let mode: LinearAuthMode if (forced === 'oauth') { if (!hasOauth) { throw new Error(missingCredentialsMessage({ integrationName, secretName })) } mode = 'oauth' } else if (forced === 'api-key') { if (!hasApiKey) { throw new Error(missingCredentialsMessage({ integrationName, secretName })) } mode = 'api-key' } else if (hasOauth) { mode = 'oauth' } else if (hasApiKey) { mode = 'api-key' } else { throw new Error(missingCredentialsMessage({ integrationName, secretName })) } switch (mode) { case 'oauth': return { mode, integrationName, secretName, authorization: `[managed ${integrationName} OAuth integration]`, } case 'api-key': return { mode, integrationName, secretName, authorization: `{{secret:${secretName}}}`, } default: { const exhaustive: never = mode throw new Error(`Unsupported Linear auth mode: ${String(exhaustive)}`) } } } async function getOauthFetch(integrationName: string): Promise { let authedFetch = authenticatedFetches.get(integrationName) if (!authedFetch) { try { authedFetch = await createAuthenticatedFetch(integrationName) } catch (error) { const message = error instanceof Error ? error.message : String(error) throw new Error( [ `Linear OAuth integration "${integrationName}" is not usable: ${message}`, missingCredentialsMessage({ integrationName, secretName: DEFAULT_API_KEY_SECRET, }), ].join(' '), ) } authenticatedFetches.set(integrationName, authedFetch) } return authedFetch } export async function linearFetch( auth: ResolvedLinearAuth, init: RequestInit, ): Promise { switch (auth.mode) { case 'oauth': { const authedFetch = await getOauthFetch(auth.integrationName) return authedFetch(LINEAR_GRAPHQL_URL, init) } case 'api-key': { const headers = new Headers(init.headers) headers.set('Authorization', `{{secret:${auth.secretName}}}`) return fetch(LINEAR_GRAPHQL_URL, { ...init, headers }) } default: { const exhaustive: never = auth.mode throw new Error(`Unsupported Linear auth mode: ${String(exhaustive)}`) } } } export function requireAuthMode(value: unknown): LinearAuthMode | undefined { if (value === undefined) return undefined const mode = requireString(value, 'auth') if (mode !== 'oauth' && mode !== 'api-key') { throw new Error('auth must be "oauth" or "api-key".') } return mode }