import { parseWithAccount } from './lib/api.ts' import { granolaRequest, KEY_TYPES_HINT } from './lib/client.ts' type Probe = { status: number; ok: boolean; errorCode?: string | null; detail?: string } /** * Check that an account's API key works and what it can reach. Makes up to five read-only GETs * (notes, folders, webhook endpoints, audit log, legal holds; one item each) and reports each * status — the Granola API has no "who am I" endpoint. Never returns note content beyond counts * and one sample id. * * @param input.account - Account name (secret `granola-`). Required. @param input.secretName - Secret override. * @param input.probes - Subset of `notes`, `folders`, `webhooks`, `audit`, `legalHolds` (default all). * @returns `{ account, secretName, keyWorks, keyKind, probes: { notes: { status, ok, hasMore, sampleNoteId }, … }, summary }` * * @example * import verify from 'kody:@charlesmatte/granola/verify' * * const r = await verify({ account: 'work' }) */ export default async function verifyGranolaAccount(input: unknown) { const all = ['notes', 'folders', 'webhooks', 'audit', 'legalHolds'] as const const p = parseWithAccount<{ probes?: Array }>(input, { probes: { type: 'stringArray', maxItems: 5 } }, 'verify') const wanted = p.probes ?? [...all] const bad = wanted.filter((w) => !(all as ReadonlyArray).includes(w)) if (bad.length) throw new Error(`verify: unknown probe(s) ${bad.join(', ')}; allowed: ${all.join(', ')}.`) const paths: Record = { notes: '/v1/notes', folders: '/v1/folders', webhooks: '/v1/webhook-endpoints', audit: '/v1/audit', legalHolds: '/v1/legal-holds' } const probes: Record> = {} for (const name of wanted) { const res = await granolaRequest>(p.acct, { path: paths[name] as string, query: name === 'webhooks' ? undefined : { page_size: 1 }, throwOnError: false, }) const probe: Probe & Record = { status: res.status, ok: res.ok } if (!res.ok) { probe.errorCode = res.errorCode const d = res.data as Record | string | null const msg = typeof d === 'string' ? d : (d?.error?.message ?? d?.message) if (msg) probe.detail = String(msg).slice(0, 200) } else if (name === 'notes') { probe.hasMore = Boolean(res.data?.hasMore) probe.sampleNoteId = res.data?.notes?.[0]?.id ?? null probe.visibleNotes = (res.data?.notes?.length ?? 0) > 0 } else if (name === 'folders') { probe.anyFolders = (res.data?.folders?.length ?? 0) > 0 } else if (name === 'webhooks') { probe.count = res.data?.webhook_endpoints?.length ?? 0 } else if (name === 'legalHolds') { probe.count = res.data?.legal_holds?.length ?? 0 } probes[name] = probe } const okNames = Object.entries(probes).filter(([, v]) => v.ok).map(([k]) => k) const all401 = Object.values(probes).every((v) => v.status === 401) const keyKind = all401 ? 'invalid' : probes.legalHolds?.ok && !probes.notes?.ok ? 'legal-hold key' : probes.audit?.ok && !probes.notes?.ok ? 'audit key' : probes.notes?.ok ? probes.audit?.ok ? 'notes key with audit access' : 'personal or workspace API key' : 'unknown' const KEY_KIND_NOTES: Record = { 'personal or workspace API key': 'Reads notes, transcripts and folders (and manages its own webhooks). The audit log needs a separate Audit API key; legal holds need a Legal hold API key.', 'audit key': 'Reads the audit log only (./audit/list). Notes, folders and webhooks need a personal or workspace API key.', 'legal-hold key': 'Manages legal holds only (./legal-holds/*). Notes need a personal or workspace API key.', 'notes key with audit access': 'Reads notes and the audit log.', invalid: 'Create a new key and update the Kody secret.', unknown: KEY_TYPES_HINT, } return { account: p.acct.account, secretName: p.acct.secretName, keyWorks: okNames.length > 0, keyKind, keyKindNote: KEY_KIND_NOTES[keyKind], probes, summary: all401 ? 'Every probe returned 401: the key is invalid, revoked or expired. Create a new one (Settings → Connectors → API keys) and update the Kody secret.' : `Reachable: ${okNames.join(', ') || 'nothing'}. Others returned the statuses shown; 403/404 there is expected when the key type cannot use that endpoint (see keyKindNote).`, } }