Implement the RPC client and error decoding in src/client.ts and src/errors.ts with unit tests #188
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills#188
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implement the Penpot RPC transport in
extensions/penpot/src/client.tsand error decoding inextensions/penpot/src/errors.ts: authenticated command calls against<PENPOT_URL>/api/rpc/command/<command>, with readable errors for both JSON validation failures and opaque Transit-encoded bodies. Unit-tested withnode --testagainst a stubbedfetch.Background
Depends on: #187
Penpot's RPC API is a command dispatch endpoint:
POST <base>/api/rpc/command/<command>with headerAuthorization: Token <token>(notBearer), andGETfor read-only commands. The API is internal and unversioned, so the client must be tolerant: it should surface the server's own error payload rather than inventing messages.Confirmed behaviour on Penpot 2.17 (validated against a live instance):
Accept: application/json, a validation failure returns readable JSON:{"type":"validation","code":"params-validation","explain":"…"}whereexplainis a precise Malli validation path. Surfaceexplainverbatim rather than paraphrasing it — it names the exact field that was wrong.binfileimport/export endpoints speak Transit for both payloads and errors even when the request andAcceptare JSON. This produces an opaque string that must never be shown raw. A full minimal Transit reader is built in the binfile milestone; in this step it is enough to (a) detect a Transit-looking body and (b) decode the small subset needed to make errors readable, or otherwise wrap it in a clear "unreadable error payload" message that includes the raw bytes.This client is the single transport used by every read and write tool in later milestones.
Documentation Required
A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting.
docs/reference/penpot-api/Authorization: Token <token>header, and the/api/rpc/command/<command>URL shape (the canonicalget-profilecurl example).<PENPOT_URL>/api/main/doc/openapi.json— the instance's OpenAPI 3.0 spec: authoritative for which commands exist, whether they areGETorPOST, and their parameter shapes.docs/reference/transit-format/{"~#uri": …},"~u<uuid>","~:keyword") is produced, and what a decoder must handle. Needed to recognise and (partially) decode Transit error bodies here and fully in the binfile milestone.docs/reference/nodejs/fetchused for requests, andResponse.ok/status/text().AbortSignal/timeout semantics for socket-level failures.Implementation Details
extensions/penpot/src/errors.tsPenpotError(orResult-based) shape with a small, closed set of categories, e.g.config | auth | validation | not-found | transport | server | unexpected— mirroring the categorization style ofextensions/forgejo/src/errors.tsandextensions/mongodb/src/errors.ts(same idea, no shared import).formatPenpotError(...): category prefix plus a message that always includes the command name, the HTTP status when there was one, and — forvalidation— the server'sexplainverbatim.decodeErrorBody(status, contentType, rawText): { category, message }:type,code,explain,messagefields when present.["^,~#,~:, or a~-tagged map) and either decodes the tags needed to extract a readable message or returns an "unreadable Transit error payload" message that includes the raw body — never render raw Transit as if it were prose.401/403→ auth/permission guidance (mention that the token needs edit rights for writes),404→ hint that the command or file id is wrong and thatPENPOT_URLmust not carry an/apisuffix,400withvalidation→ the server'sexplain.extensions/penpot/src/client.tspenpotRequest(command, options)whereoptionscarriesmethod(GETdefault for reads,POSTfor writes), an optional JSONbody, and optionalqueryparams.resolveConfig()(import from./env.ts), appending/api/rpc/command/<command>; never double-appends/api.Authorization: Token <token>,Accept: application/json, andContent-Type: application/jsonwhen there is a body.AbortSignaland on a bounded timeout; maps network failures to thetransportcategory with an actionable message (unreachable host, TLS error, timeout).undefined), and a categorised error result on failure. Do not throw for expected API failures — callers decide how to present them.Response/status available where a caller needs it (the commit path inspectsrevn-conflictspecifically).Write co-located tests next to the module (keep the same test location convention as
src/env.ts), stubbingglobalThis.fetch— the repo'sextensions/postgres/extensions/mongodbtests show the fake-client style. Tests must not require network access or credentials.Acceptance Criteria
penpotRequesttargets<base>/api/rpc/command/<command>and sendsAuthorization: Token <token>(asserted on the captured request)./apiis not double-appended.explainfield is surfaced with theexplaintext verbatim and avalidationcategory./api-suffix and id/command causes.transporterror naming the host, not an unhandled exception.Test Plan
Manual smoke check against a real instance (requires
PENPOT_URL/PENPOT_TOKEN):Then repeat with a deliberately invalid token and confirm the printed error is human-readable and contains no raw Transit dump.
pi-loop opened and merged a pull request for this issue: #221