Skip to content
← Public packages

@kentcdodds/raycast

Raycast launcher: stored execute commands via webhooks (canonical; do not use raycast-shortcuts).

src/app.ts

92 lines · 3.1 KB · TypeScript
import listCommands from './list-commands.ts'
import run from './run.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<Record<string, unknown>> {
	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<string, unknown>
	if (record.params && typeof record.params === 'object' && !Array.isArray(record.params)) {
		return record.params as Record<string, unknown>
	}
	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
}

/**
 * Hosted Raycast API: bearer-token list/run without minted webhook URLs.
 * The package app runtime calls this default export as `fetch`.
 *
 * @example
 * import handleApp from 'kody:@kentcdodds/raycast/app'
 * await handleApp(new Request('https://example.com/api/list-commands', { method: 'POST' }))
 */
export default async function handleApp(request: Request): Promise<Response> {
	const url = new URL(request.url)
	const apiPath = apiPathOf(url.pathname)

	if (request.method === 'GET' && !apiPath) {
		return json({
			ok: true,
			name: '@kentcdodds/raycast',
			routes: ['POST /api/list-commands', 'POST /api/run'],
			auth: 'Authorization: Bearer <launcher-token>',
		})
	}

	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<string, unknown>)
					: {}
			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)
	}
}