← Public packages
@cameronpak/box
Full Box Public API v1 client: lifecycle, prompts, files, commands, snapshots, environments, webhooks, desktop, hosting, and account.
src/client.ts
198 lines · 5.9 KB · TypeScriptconst BASE_URL = 'https://ascii.dev/api/box/v1'
export type BoxError = {
/** Box error code, for example provider_not_configured or http_500. */
code: string
message: string
status: number
requestId?: string
}
export type Result<T> = { ok: true; data: T } | { ok: false; error: BoxError }
/** Raw bytes from a binary Box endpoint, base64-encoded so the package stays JSON-safe. */
export type BinaryPayload = {
contentType: string
contentDisposition?: string | null
encoding: 'base64'
size: number
content: string
}
export function success<T>(data: T): Result<T> {
return { ok: true, data }
}
export function failure<T = never>(error: BoxError): Result<T> {
return { ok: false, error }
}
type QueryValue = string | number | boolean | null | undefined
type RequestOptions = {
path: string
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
query?: Record<string, QueryValue>
body?: Record<string, unknown>
headers?: Record<string, string>
/** When true, non-JSON success bodies become a base64 BinaryPayload. */
raw?: boolean
}
function invalid(code: string, message: string): BoxError {
return { code, message, status: 400 }
}
/** Assert a box id looks like a Box public id before it reaches a URL path. */
export function assertBoxId(boxId: string): BoxError | null {
if (typeof boxId === 'string' && /^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/.test(boxId)) {
return null
}
return invalid('invalid_box_id', 'boxId must look like bx_23456789.')
}
/** Assert a webhook id looks like the public webhook id. */
export function assertWebhookId(webhookId: string): BoxError | null {
if (typeof webhookId === 'string' && /^wh_[a-f0-9]{24}$/.test(webhookId)) {
return null
}
return invalid('invalid_webhook_id', 'webhookId must look like wh_ followed by 24 hex characters.')
}
/** Assert a deletion operation id looks like the public operation id. */
export function assertOperationId(operationId: string): BoxError | null {
if (typeof operationId === 'string' && /^bdop_[a-f0-9]{32}$/.test(operationId)) {
return null
}
return invalid('invalid_operation_id', 'operationId must look like bdop_ followed by 32 hex characters.')
}
/** Assert a snapshot id is a non-empty string before it reaches a URL path. */
export function assertSnapshotId(snapshotId: string): BoxError | null {
if (typeof snapshotId === 'string' && snapshotId.length > 0) {
return null
}
return invalid('invalid_snapshot_id', 'snapshotId is required.')
}
/** Assert an environment id is a non-empty string before it reaches a URL path. */
export function assertEnvironmentId(environmentId: string): BoxError | null {
if (typeof environmentId === 'string' && environmentId.length > 0) {
return null
}
return invalid('invalid_environment_id', 'environmentId is required.')
}
function bytesToBase64(bytes: Uint8Array): string {
let binary = ''
const chunk = 0x8000
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunk))
}
return btoa(binary)
}
function looksLikeJson(text: string): boolean {
const trimmed = text.trim()
return trimmed.startsWith('{') || trimmed.startsWith('[')
}
/**
* Call the Box Public API v1 with the boxApiKey user secret.
* Never throws on an API error; returns a failure Result instead.
*/
export async function boxRequest<T>(options: RequestOptions): Promise<Result<T>> {
const url = new URL(BASE_URL + options.path)
for (const [key, value] of Object.entries(options.query ?? {})) {
if (value === undefined || value === null) continue
url.searchParams.set(key, String(value))
}
const headers: Record<string, string> = {
Authorization: 'Bearer {{secret:boxApiKey}}',
Accept: options.raw ? '*/*' : 'application/json',
...options.headers,
}
if (options.body !== undefined) headers['Content-Type'] = 'application/json'
let response: Response
try {
response = await fetch(url.toString(), {
method: options.method ?? 'GET',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
})
} catch (cause) {
return failure<T>({
code: 'network_error',
message: cause instanceof Error ? cause.message : String(cause),
status: 0,
})
}
const contentType = response.headers.get('content-type') ?? ''
const isJsonContent = contentType.includes('json')
if (options.raw && !isJsonContent) {
const bytes = new Uint8Array(await response.arrayBuffer())
if (!response.ok) {
const text = new TextDecoder().decode(bytes)
if (looksLikeJson(text)) {
try {
const payload = JSON.parse(text)
return failure<T>({
code: payload?.code ?? payload?.error?.code ?? 'http_' + response.status,
message:
payload?.message ??
payload?.error?.message ??
'Box request failed with status ' + response.status + '.',
status: payload?.status ?? response.status,
requestId: payload?.requestId,
})
} catch {
// fall through to generic failure
}
}
return failure<T>({
code: 'http_' + response.status,
message: text.slice(0, 300) || 'Box request failed with status ' + response.status + '.',
status: response.status,
})
}
return success({
contentType,
contentDisposition: response.headers.get('content-disposition'),
encoding: 'base64',
size: bytes.byteLength,
content: bytesToBase64(bytes),
} as T)
}
const text = await response.text()
let payload: any = null
if (text.length > 0) {
try {
payload = JSON.parse(text)
} catch {
return failure<T>({
code: 'invalid_json_response',
message: 'Box returned a non-JSON body: ' + text.slice(0, 300),
status: response.status,
})
}
}
if (!response.ok || payload?.ok === false) {
return failure<T>({
code: payload?.code ?? payload?.error?.code ?? 'http_' + response.status,
message:
payload?.message ??
payload?.error?.message ??
'Box request failed with status ' + response.status + '.',
status: payload?.status ?? response.status,
requestId: payload?.requestId,
})
}
return success(payload as T)
}