import listCommands from './list-commands.ts' import run from './run.ts' import { PACKAGE_NAME } from './helpers.ts' import { bearerFromRequest, verifyLauncherToken } from './launcher-auth.ts' function json(data: unknown, status = 200) { return new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', }, }) } async function readBody(request: Request): Promise> { if (request.method === 'GET' || request.method === 'HEAD') return {} const text = await request.text() if (!text.trim()) return {} const parsed = JSON.parse(text) as unknown if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('JSON body must be an object') } const record = parsed as Record if (record.params && typeof record.params === 'object' && !Array.isArray(record.params)) { return record.params as Record } return record } function apiPathOf(pathname: string) { const idx = pathname.indexOf('/api/') if (idx >= 0) return pathname.slice(idx).replace(/\/+$/, '') || '/api' if (pathname.startsWith('/api/')) return pathname.replace(/\/+$/, '') return null } /** * Optional hosted Raycast API: bearer-token list/run without minted webhook URLs. * Prefer minted webhooks for external clients — package apps on `*.kody.run` * need a Kody browser handoff. The package app runtime calls this default export * as `fetch`. * * @example * import handleApp from 'kody:@kody/raycast/app' * await handleApp(new Request('https://example.com/api/list-commands', { method: 'POST' })) */ export default async function handleApp(request: Request): Promise { const url = new URL(request.url) const apiPath = apiPathOf(url.pathname) if (request.method === 'GET' && !apiPath) { return json({ ok: true, name: PACKAGE_NAME, routes: ['POST /api/list-commands', 'POST /api/run'], auth: 'Authorization: Bearer ', preferred: 'minted webhooks + ~/.config/kody/raycast.json', }) } if (!apiPath) return json({ error: 'not_found' }, 404) const token = bearerFromRequest(request) if (!(await verifyLauncherToken(token))) { return json({ error: 'unauthorized' }, 401) } try { if (apiPath === '/api/list-commands' && (request.method === 'POST' || request.method === 'GET')) { const params = await readBody(request) return json(await listCommands({ includeCode: Boolean(params.includeCode) })) } if (apiPath === '/api/run' && request.method === 'POST') { const params = await readBody(request) const nested = params.params && typeof params.params === 'object' && !Array.isArray(params.params) ? (params.params as Record) : {} return json( await run({ commandId: typeof params.commandId === 'string' ? params.commandId : undefined, code: typeof params.code === 'string' ? params.code : undefined, params: nested, idempotencyKey: request.headers.get('idempotency-key') || (typeof params.idempotencyKey === 'string' ? params.idempotencyKey : undefined), }), ) } return json({ error: 'not_found' }, 404) } catch (error) { const message = error instanceof Error ? error.message : String(error) return json({ error: message }, 400) } }