import { API_V1, CREDENTIALS, UNAUTHORIZED_EVENT, signal, } from "../apiContract"; import type { SurfaceScope } from "./apiBridge"; /** * ⭐ WAVE 19 (owner item 12) — EVERY VERB CARRIES ITS SURFACE. * * The three URLs were hardcoded `/customers/{pid}/comments`, so opening a PRODUCT record and * typing a comment asked the customer endpoint about a CRC32 hash of a SKU code. The honest * failures read "that customer is not in your book" on a screen showing a product — the bug the * owner reported. The dishonest failure is the one that made this worth a wave: a product pid * that happens to collide with a real partner id answers 200, and the comment lands on somebody * else's customer. * * `?scope=` rather than a path segment, because that is what the surrounding API already speaks * for reads (`/workspace?scope=`) and what `scopeKey` does for writes — one vocabulary, not two. * * ⚠ THE PATH STAYS `/customers/…` and that is deliberate, not an oversight. It is the shipped * URL, pinned by `verify_api.py`'s E1a section, and the scope is now carried explicitly beside * it — so renaming the segment would buy a nicer noun in exchange for churning another session's * gate mid-wave. Booked as a cosmetic debt in the wave doc; the WALL is `?scope=`, not the noun. */ function commentsUrl(scope: SurfaceScope, pid: number, suffix = ""): string { return `${API_V1}/customers/${encodeURIComponent(String(pid))}/comments${suffix}` + `?scope=${encodeURIComponent(scope)}`; } export interface RecordComment { id: string; body: string; authorKey: string; author: string; createdAt: string; } export class RecordCommentsError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = "RecordCommentsError"; this.status = status; } } async function result(response: Response): Promise { let body: unknown = null; try { body = await response.json(); } catch { // The status still gives the user an honest fallback below. } if (response.status === 401) signal(UNAUTHORIZED_EVENT); if (!response.ok) { const shaped = body as { error?: { message?: string } } | null; throw new RecordCommentsError( shaped?.error?.message || `The server answered ${response.status}.`, response.status ); } return body as T; } export async function fetchRecordComments( scope: SurfaceScope, pid: number, abortSignal?: AbortSignal ): Promise { const response = await fetch(commentsUrl(scope, pid), { credentials: CREDENTIALS, signal: abortSignal, }); const body = await result<{ comments?: RecordComment[] }>(response); return Array.isArray(body.comments) ? body.comments : []; } export async function postRecordComment( scope: SurfaceScope, pid: number, body: string ): Promise { const response = await fetch(commentsUrl(scope, pid), { method: "POST", credentials: CREDENTIALS, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ body }), }); const payload = await result<{ comment: RecordComment }>(response); return payload.comment; } export async function deleteRecordComment( scope: SurfaceScope, pid: number, commentId: string ): Promise { const response = await fetch( commentsUrl(scope, pid, `/${encodeURIComponent(commentId)}`), { method: "DELETE", credentials: CREDENTIALS, } ); await result<{ ok: boolean }>(response); }