Files
apes-sidebar/src/wire.ts
T
Menghuan1918 1bfabbbfc2 feat: 适配 DSH 命名契约重命名(jobs/terminal/webServer)并处理 Session log 按钮位置
- tasks→jobs:ctx.jobs / job_output / job_id / session/jobs / jobsBySession / Job* 词汇
- httpServer→webServer(inject + augmentation + register/registerUpgrade)
- autoOpenTasks→autoOpenJobs 偏好键
- 右上角开关簇回到角落,注入 CSS 挤开 header 的「Session log」胶囊
2026-08-13 09:06:55 +08:00

95 lines
3.1 KiB
TypeScript

/**
* Wire helpers for the /sidebar JSON API: bounded body reading, response
* writing, and the shared error envelope. Every API method returns
* `{ok: true, value}` on success and `{ok: false, error: {code, message}}`
* (HTTP 4xx/5xx matching the code) on failure.
*/
import type { IncomingMessage, ServerResponse } from 'node:http'
/** Machine-readable error codes of the sidebar API. */
export type SidebarErrorCode =
| 'bad-request'
| 'not-found'
| 'forbidden'
| 'method-error'
| 'fs-error'
| 'git-error'
| 'pty-error'
| 'job-error'
| 'settings-rejected'
| 'settings-conflict'
| 'internal'
/** One API failure with its wire code and HTTP status. */
export class SidebarError extends Error {
constructor(
readonly code: SidebarErrorCode,
message: string,
readonly status = 400,
) {
super(message)
}
}
/** Body size bound of one JSON request (defense against unbounded reads). */
const MAX_BODY_BYTES = 1 << 20
/** Success envelope of one API method. */
export interface SidebarOk<T> { ok: true; value: T }
/** Failure envelope of one API method. */
export interface SidebarErr { ok: false; error: { code: SidebarErrorCode; message: string } }
/** Read and parse the JSON request body (bounded; malformed → bad-request). */
export async function readJsonBody(req: IncomingMessage): Promise<unknown> {
const chunks: Buffer[] = []
let total = 0
for await (const chunk of req) {
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
total += buffer.length
if (total > MAX_BODY_BYTES) {
throw new SidebarError('bad-request', 'request body too large')
}
chunks.push(buffer)
}
const text = Buffer.concat(chunks).toString('utf8')
if (text.trim() === '') return {}
try {
return JSON.parse(text) as unknown
} catch {
throw new SidebarError('bad-request', 'request body is not valid JSON')
}
}
/** Write a JSON response with the given status. */
export function writeJson(res: ServerResponse, status: number, body: unknown): void {
const payload = JSON.stringify(body)
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
res.end(payload)
}
/** Write the success envelope. */
export function writeOk(res: ServerResponse, value: unknown): void {
writeJson(res, 200, { ok: true, value })
}
/** Write the failure envelope for any thrown value (unknown → internal 500). */
export function writeError(res: ServerResponse, error: unknown): void {
if (error instanceof SidebarError) {
writeJson(res, error.status, { ok: false, error: { code: error.code, message: error.message } })
return
}
const message = error instanceof Error ? error.message : String(error)
writeJson(res, 500, { ok: false, error: { code: 'internal', message } })
}
/** Narrow an unknown payload value to a string, else throw bad-request. */
export function requireString(payload: unknown, key: string): string {
const record = payload as Record<string, unknown> | null
const value = record?.[key]
if (typeof value !== 'string' || value === '') {
throw new SidebarError('bad-request', `missing or invalid "${key}"`)
}
return value
}