// In-memory fake of the Granola public API (shapes from https://docs.granola.ai/api-reference/openapi.json). // Synthetic data only. export type Call = { method: string; path: string; query: Record; body: unknown; auth: string | null } const id = (prefix: string, n: number) => `${prefix}_${String(n).padStart(14, 'A')}` export type NoteContent = { title?: string; summary_markdown?: string; private_notes_markdown?: string | null; attendees?: Array<{ name: string | null; email: string }>; transcript?: Array<{ text: string; attribution?: 'me' | 'them' }>; status?: number } export function makeFake(opts: { notes?: number; transcriptItems?: number; tooLarge?: boolean; status429Once?: boolean; content?: Record; badKey?: boolean } = {}) { const calls: Array = [] const notes = Array.from({ length: opts.notes ?? 65 }, (_, i) => ({ id: id('not', i + 1), object: 'note', title: opts.content?.[i]?.title ?? (i % 2 ? `Weekly sync ${i}` : `Customer call ${i}`), owner: { name: 'Sample Owner', email: i % 3 ? 'owner@example.com' : 'other@example.com' }, created_at: `2026-09-${String((i % 28) + 1).padStart(2, '0')}T15:00:00Z`, updated_at: '2026-09-30T15:00:00Z', })) const transcript = Array.from({ length: opts.transcriptItems ?? 250 }, (_, i) => ({ speaker: { source: i % 2 ? 'speaker' : 'microphone', ...(i % 2 ? { attribution: 'them' } : { attribution: 'me' }) }, text: `line ${i}`, start_time: new Date(Date.parse('2026-09-01T15:00:00Z') + i * 5000).toISOString(), end_time: new Date(Date.parse('2026-09-01T15:00:00Z') + i * 5000 + 4000).toISOString(), })) const folders = [ { id: id('fol', 1), object: 'folder', name: 'Clients', parent_folder_id: null }, { id: id('fol', 2), object: 'folder', name: 'Acme', parent_folder_id: id('fol', 1) }, { id: id('fol', 3), object: 'folder', name: 'Internal', parent_folder_id: null }, ] let throttled = !opts.status429Once const page = (items: Array, q: Record, key: string, max: number) => { const size = Math.min(Number(q.page_size ?? 10), max) const start = q.cursor ? Number(atob(q.cursor)) : 0 const slice = items.slice(start, start + size) const next = start + size return { [key]: slice, hasMore: next < items.length, cursor: next < items.length ? btoa(String(next)) : null } } const json = (status: number, data: unknown, headers: Record = {}) => new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json', ...headers } }) const fetchImpl = async (input: RequestInfo | URL, init?: RequestInit) => { const u = new URL(String(input)) const method = (init?.method ?? 'GET').toUpperCase() const q = Object.fromEntries(u.searchParams) const auth = new Headers(init?.headers).get('Authorization') calls.push({ method, path: u.pathname, query: q, body: init?.body ? JSON.parse(String(init.body)) : undefined, auth }) if (u.host !== 'public-api.granola.ai') throw new Error('fake: wrong host') if (opts.badKey) return json(401, { error: { code: 'INVALID_API_KEY', message: 'Invalid API key format' } }) if (!throttled) { throttled = true return json(429, { error: { code: 'RATE_LIMITED', message: 'slow down' } }, { 'retry-after': '0' }) } const p = u.pathname if (method === 'GET' && p === '/v1/notes') return json(200, page(notes, q, 'notes', 30)) const noteMatch = p.match(/^\/v1\/notes\/(not_\w+)(\/transcript)?$/) if (method === 'GET' && noteMatch) { const note = notes.find((n) => n.id === noteMatch[1]) if (!note) return json(404, { error: { code: 'NOT_FOUND', message: 'Note not found' } }) const c = opts.content?.[notes.indexOf(note)] if (c?.status) return json(c.status, { error: { code: 'BOOM', message: 'fake failure' } }) const noteTranscript = c?.transcript ? c.transcript.map((t, k) => ({ speaker: { source: t.attribution === 'them' ? 'speaker' : 'microphone', attribution: t.attribution ?? 'me' }, text: t.text, start_time: new Date(Date.parse('2026-09-01T15:00:00Z') + k * 5000).toISOString(), end_time: new Date(Date.parse('2026-09-01T15:00:00Z') + k * 5000 + 4000).toISOString() })) : transcript.slice(0, 5) if (noteMatch[2]) return json(200, page(transcript, q, 'transcript', 100)) if (q.include === 'transcript' && opts.tooLarge) return json(413, { error: { code: 'TRANSCRIPT_TOO_LARGE', message: 'too large' } }) return json(200, { ...note, web_url: `https://notes.granola.ai/d/${note.id}`, calendar_event: null, attendees: c?.attendees ?? [{ name: 'Sample Owner', email: 'owner@example.com' }], folder_membership: [folders[1]], summary_text: (c?.summary_markdown ?? '## Summary').replace(/[#*]/g, ''), summary_markdown: c?.summary_markdown ?? '## Summary', private_notes_text: c?.private_notes_markdown ?? null, private_notes_markdown: c?.private_notes_markdown ?? null, ...(q.include === 'transcript' ? { transcript: noteTranscript } : {}), }) } if (method === 'GET' && p === '/v1/folders') return json(200, page(folders, q, 'folders', 30)) if (method === 'GET' && p === '/v1/audit') return json(403, { error: { code: 'FORBIDDEN', message: 'admin only' } }) if (method === 'GET' && p === '/v1/webhook-endpoints') return json(200, { webhook_endpoints: [{ id: id('whe', 1), object: 'webhook_endpoint', url: 'https://example.com/hook', events: ['note.generated'], scopes: ['personal'], folder_ids: [], enabled: true, created_by: null, created_at: '2026-09-01T00:00:00Z' }] }) if (method === 'POST' && p === '/v1/webhook-endpoints') return json(201, { id: id('whe', 2), object: 'webhook_endpoint', ...(JSON.parse(String(init?.body))), enabled: true, signing_secret: 'whsec_FAKEFAKEFAKE' }) if (method === 'GET' && p.startsWith('/v1/legal-holds')) return json(404, { error: { code: 'NOT_FOUND', message: 'Not found' } }) return json(404, { error: { code: 'NOT_FOUND', message: `fake: no route ${method} ${p}` } }) } return { calls, fetchImpl, notes, folders, transcript } }