Skip to content
← Public packages

@kentcdodds/raycast

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

src/webhook.ts

98 lines · 2.9 KB · TypeScript
import listCommands from './list-commands.ts'
import run, { type RunInput } from './run.ts'

const ROUTER_ACTIONS = new Set(['list-commands', 'run'] as const)
type RouterAction = 'list-commands' | 'run'

function isPlainObject(value: unknown): value is Record<string, unknown> {
	return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}

/**
 * Ignore GitHub deliveries that may still hit this URL from an old
 * webhookUrlApply → kentcdodds/kody-raycast-hooks registration.
 */
function isGithubWebhookNoise(input: Record<string, unknown>): boolean {
	const action = typeof input.action === 'string' ? input.action.trim() : ''
	if (action === 'list-commands' || action === 'run') return false
	if (typeof input.commandId === 'string' && input.commandId.trim()) return false
	if (typeof input.code === 'string' && input.code.trim()) return false

	if (typeof input.zen === 'string') return true
	if ('hook_id' in input || isPlainObject(input.hook)) return true
	if (isPlainObject(input.installation)) return true
	if (
		'repositories_added' in input ||
		'repositories_removed' in input ||
		'repository_selection' in input
	) {
		return true
	}
	if (isPlainObject(input.organization) && isPlainObject(input.sender)) return true
	if (
		isPlainObject(input.security_advisory) ||
		isPlainObject(input.marketplace_purchase) ||
		isPlainObject(input.sponsorship)
	) {
		return true
	}
	if (isPlainObject(input.repository)) return true

	const sender = input.sender
	if (
		isPlainObject(sender) &&
		typeof sender.login === 'string' &&
		typeof input.action === 'string'
	) {
		return true
	}
	return false
}

export type WebhookInput = {
	/** Router action. Required for Raycast / HTTP callers. */
	action?: RouterAction | string
	includeCode?: boolean
	commandId?: string
	code?: string
	params?: Record<string, unknown>
	idempotencyKey?: string
}

/**
 * Single inbound webhook router for the Raycast launcher.
 * POST params: `{ action: "list-commands" | "run", ... }`.
 * Agents should prefer `kody:@kentcdodds/raycast/...` imports over this webhook.
 *
 * @example
 * import webhook from 'kody:@kentcdodds/raycast/webhook'
 * await webhook({ action: 'list-commands' })
 * await webhook({ action: 'run', commandId: 'ping-discord-general' })
 */
export default async function webhook(input: WebhookInput = {}) {
	const record = input as Record<string, unknown>
	if (isGithubWebhookNoise(record)) {
		return {
			ok: true,
			ignored: true,
			reason: 'github-webhook-event',
		}
	}

	const action = typeof input.action === 'string' ? input.action.trim() : ''
	if (!ROUTER_ACTIONS.has(action as RouterAction)) {
		throw new Error('Provide action: "list-commands" | "run"')
	}

	if (action === 'list-commands') {
		return await listCommands({ includeCode: Boolean(input.includeCode) })
	}

	const runInput: RunInput = {
		commandId: input.commandId,
		code: input.code,
		params: input.params,
		idempotencyKey: input.idempotencyKey,
	}
	return await run(runInput)
}