import { type OriginQuery, type OriginRequestResult } from './types.ts'
const SAFE_HTTP_METHODS = new Set(['GET', 'HEAD', 'OPTIONS'])
export function isMutatingMethod(method: string): boolean {
return !SAFE_HTTP_METHODS.has(method.toUpperCase())
}
export function assertMutationGuard(
mutating: boolean,
dryRun: boolean | undefined,
confirm: boolean | undefined,
action: string,
) {
if (!mutating) return
if (dryRun === true) return
if (confirm === true) return
throw new Error(
`Origin mutation "${action}" requires dryRun: true (preview, no Origin write) or confirm: true (live write).`,
)
}
export async function originFetch(input: {
path: string
method?: string
query?: OriginQuery
body?: unknown
bearerToken: string
}): Promise<OriginRequestResult> {
const url = new URL(resolveOriginPath(input.path), 'https://api.cursor.com')
if (input.query) {
for (const [key, value] of Object.entries(input.query)) {
if (value === undefined || value === null) continue
url.searchParams.set(key, String(value))
}
}
const method = (input.method ?? 'GET').toUpperCase()
const headers: Record<string, string> = {
Authorization: `Bearer ${input.bearerToken}`,
Accept: 'application/json',
}
let body: string | undefined
if (input.body !== undefined && method !== 'GET' && method !== 'HEAD') {
headers['Content-Type'] = 'application/json'
body = JSON.stringify(input.body)
}
const response = await fetch(url.toString(), { method, headers, body })
const text = await response.text()
let parsed: unknown = text
if (text) {
try {
parsed = JSON.parse(text)
} catch {
parsed = text
}
} else {
parsed = null
}
return {
ok: response.ok,
status: response.status,
body: parsed,
rateLimit: {
limit: response.headers.get('x-ratelimit-limit'),
remaining: response.headers.get('x-ratelimit-remaining'),
used: response.headers.get('x-ratelimit-used'),
reset: response.headers.get('x-ratelimit-reset'),
},
}
}
export function resolveOriginPath(path: string): string {
const trimmed = path.trim()
if (!trimmed) {
throw new Error('Origin request path is required.')
}
if (/^https?:\/\//i.test(trimmed)) {
throw new Error('Pass an Origin API path, not an absolute URL.')
}
if (trimmed.startsWith('/v1/origin/')) return trimmed
const suffix = trimmed.startsWith('/') ? trimmed : `/${trimmed}`
return `/v1/origin${suffix}`
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
export function asRecord(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {}
}
export function stringField(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null
}
export function numberField(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null
}
export function booleanField(value: unknown): boolean | null {
return typeof value === 'boolean' ? value : null
}
export function assertOk(status: number, body: unknown, action: string) {
if (status >= 200 && status < 300) return
const message =
isRecord(body) && typeof body.message === 'string'
? body.message
: `HTTP ${String(status)}`
throw new Error(`Failed to ${action}: ${message}`)
}
export function originErrorMessage(body: unknown, status: number): string {
return isRecord(body) && typeof body.message === 'string'
? body.message
: `HTTP ${String(status)}`
}