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(' ')
}