- Remove every box-shadow; panel uses the native sidebar fill surface (--dsw-specific-sidebar-fill) with a hairline border, the collapse control becomes the native 36px circular rail button, the active tab uses the native interactive-bg-active fill, and rows/typography follow the native 34px/14px session-row recipe. - New 'Side card' settings section in the DSH Settings shell: open-by- default for new conversations (boolean) and default width share as a percent of the window (20-60). New sessions seed from these prefs; existing per-session layouts stay untouched. - The settings namespace is read/written through the plugin's own fenced /sidebar routes calling the settings seam in-process: the DSH settings RPC domain only serves allowlisted namespaces to configuration clients, so third-party namespaces cannot use connection.api.settings (reads are silently filtered, writes refused with 'not exposed'). The seam still persists to the user settings document with revision-guarded writes.
94 lines
3.1 KiB
TypeScript
94 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'
|
|
| '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
|
|
}
|