import type { LinearAuthMode } from './types.ts' import { requireString } from './types.ts' export const LINEAR_GRAPHQL_URL = 'https://api.linear.app/graphql' export const LINEAR_AUTHORIZE_URL = 'https://linear.app/oauth/authorize' export const LINEAR_TOKEN_URL = 'https://api.linear.app/oauth/token' export const LINEAR_API_BASE_URL = 'https://api.linear.app' export const LINEAR_REQUIRED_HOST = 'api.linear.app' export const LINEAR_OAUTH_APP_URL = 'https://linear.app/settings/api/applications/new' export const LINEAR_API_KEY_URL = 'https://linear.app/settings/account/security' export const LINEAR_CALLBACK_URL = 'https://kody.codes/connect/oauth' export const DEFAULT_INTEGRATION_NAME = 'linear' export const DEFAULT_API_KEY_SECRET = 'linearApiKey' export const DEFAULT_OAUTH_SCOPES = ['read', 'write'] as const export const LINEAR_SCOPES = { read: 'Read access for the authorizing user. Always present on OAuth tokens.', write: 'Write access. Needed to update issues, projects, and most mutations.', 'issues:create': 'Create issues and attachments without full write.', 'comments:create': 'Create comments without full write.', 'timeSchedule:write': 'Create and modify time schedules.', admin: 'Admin endpoints. Do not request unless the workflow truly needs it.', } as const export type LinearScope = keyof typeof LINEAR_SCOPES export type LinearOperation = | 'query' | 'issueCreate' | 'issueUpdate' | 'commentCreate' | 'projectCreate' | 'unknownMutation' export function oauthConnectUrl(provider: string = DEFAULT_INTEGRATION_NAME): string { const name = requireString(provider, 'provider') const params = new URLSearchParams({ provider: name, authorizeUrl: LINEAR_AUTHORIZE_URL, tokenUrl: LINEAR_TOKEN_URL, apiBaseUrl: LINEAR_API_BASE_URL, scopes: DEFAULT_OAUTH_SCOPES.join(','), scopeSeparator: ',', flow: 'confidential', allowedHosts: LINEAR_REQUIRED_HOST, dashboardUrl: LINEAR_OAUTH_APP_URL, }) return `https://kody.codes/connect/oauth?${params.toString()}` } export function apiKeySetupUrl(secretName: string = DEFAULT_API_KEY_SECRET): string { const name = requireString(secretName, 'secretName') const params = new URLSearchParams({ name, description: 'Linear personal API key', allowedHosts: LINEAR_REQUIRED_HOST, scope: 'user', }) return `https://kody.codes/account/secrets/new?${params.toString()}` } export function reconnectUrl(provider: string = DEFAULT_INTEGRATION_NAME): string { return `https://kody.codes/connect/oauth?provider=${encodeURIComponent(provider)}` } export function scopeForOperation(operation: LinearOperation): LinearScope { switch (operation) { case 'query': return 'read' case 'issueCreate': return 'issues:create' case 'commentCreate': return 'comments:create' case 'issueUpdate': case 'projectCreate': return 'write' case 'unknownMutation': return 'write' default: { const exhaustive: never = operation throw new Error(`Unsupported Linear operation: ${String(exhaustive)}`) } } } export function nextStepForScope( scope: LinearScope, options: { authMode: LinearAuthMode integrationName: string secretName: string }, ): string { switch (options.authMode) { case 'oauth': return [ `Reconnect the "${options.integrationName}" OAuth integration and include the "${scope}" scope.`, reconnectUrl(options.integrationName), `Full BYO connect URL (scopes ${DEFAULT_OAUTH_SCOPES.join(',')}): ${oauthConnectUrl(options.integrationName)}`, ].join(' ') case 'api-key': return [ `Create a Linear personal API key that includes the "${scope}" permission (or Write), then save it as ${options.secretName}.`, LINEAR_API_KEY_URL, apiKeySetupUrl(options.secretName), ].join(' ') default: { const exhaustive: never = options.authMode throw new Error(`Unsupported Linear auth mode: ${String(exhaustive)}`) } } } export function missingCredentialsMessage(options: { integrationName: string secretName: string }): string { return [ 'Linear credentials are missing.', 'OAuth (recommended for shared/multi-account use): create an OAuth app at', LINEAR_OAUTH_APP_URL, `with redirect URI ${LINEAR_CALLBACK_URL}, then connect:`, oauthConnectUrl(options.integrationName), `Personal API key (fastest for one workspace): create a key at ${LINEAR_API_KEY_URL}`, 'with Read plus the write permissions you need, then save it (do not paste the value in chat):', apiKeySetupUrl(options.secretName), `Required API host: ${LINEAR_REQUIRED_HOST}.`, ].join(' ') }