← Public packages
@charlesmatte/granola
Search and read Granola meeting notes, summaries and transcripts from any agent; manage webhooks and legal holds safely.
src/verify.ts
86 lines · 4.3 KB · TypeScriptimport { 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-<account>`). 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<string> }>(input, { probes: { type: 'stringArray', maxItems: 5 } }, 'verify')
const wanted = p.probes ?? [...all]
const bad = wanted.filter((w) => !(all as ReadonlyArray<string>).includes(w))
if (bad.length) throw new Error(`verify: unknown probe(s) ${bad.join(', ')}; allowed: ${all.join(', ')}.`)
const paths: Record<string, string> = { notes: '/v1/notes', folders: '/v1/folders', webhooks: '/v1/webhook-endpoints', audit: '/v1/audit', legalHolds: '/v1/legal-holds' }
const probes: Record<string, Probe & Record<string, unknown>> = {}
for (const name of wanted) {
const res = await granolaRequest<Record<string, any>>(p.acct, {
path: paths[name] as string,
query: name === 'webhooks' ? undefined : { page_size: 1 },
throwOnError: false,
})
const probe: Probe & Record<string, unknown> = { status: res.status, ok: res.ok }
if (!res.ok) {
probe.errorCode = res.errorCode
const d = res.data as Record<string, any> | 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<string, string> = {
'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).`,
}
}