import type { CalendlyAuthMode } from './types.ts'
import { requireString } from './types.ts'
export const CALENDLY_API_BASE_URL = 'https://api.calendly.com'
export const CALENDLY_AUTHORIZE_URL = 'https://auth.calendly.com/oauth/authorize'
export const CALENDLY_TOKEN_URL = 'https://auth.calendly.com/oauth/token'
export const CALENDLY_API_HOST = 'api.calendly.com'
export const CALENDLY_AUTH_HOST = 'auth.calendly.com'
export const CALENDLY_REQUIRED_HOSTS = [CALENDLY_API_HOST, CALENDLY_AUTH_HOST] as const
export const CALENDLY_OAUTH_APP_URL = 'https://developer.calendly.com/'
export const CALENDLY_PAT_URL = 'https://calendly.com/integrations/api_webhooks'
export const CALENDLY_CALLBACK_URL = 'https://kody.codes/connect/oauth'
export const DEFAULT_INTEGRATION_NAME = 'calendly'
export const DEFAULT_PAT_SECRET = 'calendlyPat'
export const DEFAULT_OAUTH_SCOPES = [
'users:read',
'event_types:read',
'scheduled_events:read',
'scheduled_events:write',
] as const
export const CALENDLY_SCOPES = {
'users:read': 'Read the authorizing user (`GET /users/me`).',
'event_types:read': 'List and read event types and available times.',
'event_types:write': 'Create or update event types (escape-hatch `./request` only).',
'scheduled_events:read': 'Read scheduled events and invitees.',
'scheduled_events:write': 'Create invitees, cancel events, or mark no-shows.',
} as const
export type CalendlyScope = keyof typeof CALENDLY_SCOPES
export type CalendlyOperation =
| 'users.read'
| 'eventTypes.read'
| 'eventTypes.write'
| 'scheduledEvents.read'
| 'scheduledEvents.write'
| 'unknownRead'
| 'unknownMutation'
export function oauthConnectUrl(provider: string = DEFAULT_INTEGRATION_NAME): string {
const name = requireString(provider, 'provider')
const params = new URLSearchParams({
provider: name,
authorizeUrl: CALENDLY_AUTHORIZE_URL,
tokenUrl: CALENDLY_TOKEN_URL,
apiBaseUrl: CALENDLY_API_BASE_URL,
scopes: DEFAULT_OAUTH_SCOPES.join(' '),
flow: 'confidential',
pkce: 'true',
allowedHosts: CALENDLY_REQUIRED_HOSTS.join(','),
dashboardUrl: CALENDLY_OAUTH_APP_URL,
providerSetupInstructions: [
'Create an OAuth app at https://developer.calendly.com/.',
`Register redirect URI exactly ${CALENDLY_CALLBACK_URL}.`,
'Use Production for live account data (Sandbox is for Calendly test data).',
'Calendly shows the client secret only once.',
'Paste the client id and client secret into this Kody form.',
`Request scopes ${DEFAULT_OAUTH_SCOPES.join(' ')}.`,
'Do not paste secrets into chat.',
].join(' '),
})
return `https://kody.codes/connect/oauth?${params.toString()}`
}
export function patSetupUrl(secretName: string = DEFAULT_PAT_SECRET): string {
const name = requireString(secretName, 'secretName')
const params = new URLSearchParams({
name,
description: 'Calendly personal access token (Bearer token for api.calendly.com)',
allowedHosts: CALENDLY_API_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: CalendlyOperation): CalendlyScope {
switch (operation) {
case 'users.read':
return 'users:read'
case 'eventTypes.read':
return 'event_types:read'
case 'eventTypes.write':
return 'event_types:write'
case 'scheduledEvents.read':
return 'scheduled_events:read'
case 'scheduledEvents.write':
return 'scheduled_events:write'
case 'unknownRead':
return 'users:read'
case 'unknownMutation':
return 'scheduled_events:write'
default: {
const exhaustive: never = operation
throw new Error(`Unsupported Calendly operation: ${String(exhaustive)}`)
}
}
}
export function nextStepForScope(
scope: CalendlyScope,
options: {
authMode: CalendlyAuthMode
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 'pat':
return [
`Create a Calendly personal access token with the "${scope}" scope, then save it as ${options.secretName}.`,
CALENDLY_PAT_URL,
patSetupUrl(options.secretName),
].join(' ')
default: {
const exhaustive: never = options.authMode
throw new Error(`Unsupported Calendly auth mode: ${String(exhaustive)}`)
}
}
}
export function missingCredentialsMessage(options: {
integrationName: string
secretName: string
}): string {
return [
'Calendly credentials are missing.',
'OAuth (recommended for shared/multi-account use): create an OAuth app at',
CALENDLY_OAUTH_APP_URL,
`with redirect URI ${CALENDLY_CALLBACK_URL}, then connect:`,
oauthConnectUrl(options.integrationName),
`Personal access token (fastest for one account): create a PAT at ${CALENDLY_PAT_URL}`,
'then save it (do not paste the value in chat):',
patSetupUrl(options.secretName),
`Required API host: ${CALENDLY_API_HOST}. OAuth also needs ${CALENDLY_AUTH_HOST}.`,
].join(' ')
}
export function isMutatingMethod(method: string): boolean {
const normalized = method.toUpperCase()
return (
normalized === 'POST' ||
normalized === 'PUT' ||
normalized === 'PATCH' ||
normalized === 'DELETE'
)
}
export function inferOperationFromPath(
method: string,
path: string,
explicit?: CalendlyOperation,
): CalendlyOperation {
if (explicit) return explicit
const mutating = isMutatingMethod(method)
if (/\/invitees(?:\/|$)/.test(path) || /\/invitee_no_shows(?:\/|$)/.test(path)) {
return mutating ? 'scheduledEvents.write' : 'scheduledEvents.read'
}
if (/\/scheduled_events(?:\/|$)/.test(path)) {
return mutating ? 'scheduledEvents.write' : 'scheduledEvents.read'
}
if (/\/event_types(?:\/|$)/.test(path) || /\/event_type_available_times(?:\/|$)/.test(path)) {
return mutating ? 'eventTypes.write' : 'eventTypes.read'
}
if (/\/users(?:\/|$)/.test(path)) {
return mutating ? 'unknownMutation' : 'users.read'
}
return mutating ? 'unknownMutation' : 'unknownRead'
}