Implement penpot_import_library (multipart upload plus SSE result) #203

Closed
opened 2026-09-14 23:12:18 +00:00 by david · 1 comment
Owner

Summary

Implement penpot_import_library: upload a committed .penpot library artifact into a nominated project as a new file, using a multipart request with kebab-case field names and reading the created file ids from the SSE end frame. This is how the design system's component library gets into a project without hand-authoring components over the API.

Background

Depends on: #202

Component authoring over the API (add-component with a main-instance marking) is the riskiest part of the API surface, and it is deliberately out of scope. Instead the component library is authored once in the Penpot UI, exported, and committed to the repo as an artifact — the extension imports it per project, repeatedly and reproducibly.

Confirmed contract on Penpot 2.17:

  • import-binfile is a multipart request. Field names are kebab-case: name, project-id, version, file (the upload itself). An optional file-id parameter also exists on 2.17.
  • version defaults to 3.
  • The command requires project edit permission on the target project.
  • The response streams event: progress frames and terminates with event: end whose data: is a Transit vector of the created file ids (["~u<uuid>"]).

Two safety properties matter here:

  • The import creates a file, so it must register that file with the designated-target guard (registerCreatedFile(fileId, projectId)) — a file the extension created in a nominated project is exactly the case the guard is designed to permit writes to.
  • The upload happens immediately (it is not a staged change), so the project scope must be authorised before the request is sent.

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/

docs/reference/transit-format/

docs/reference/nodejs/

docs/reference/pi-coding-agent/ and docs/reference/typebox/

Implementation Details

Create extensions/penpot/src/tools/importLibrary.ts (reusing src/binfile.ts and src/media.ts-style multipart construction) and register the tool.

Parameters:

  • project_id (string, required) — the project to import into.
  • path (string, required) — path to the .penpot artifact (default it to the repo artifact under skills/penpot/assets/, but allow an explicit path).
  • name (string, optional) — the imported file's name; default the artifact's basename.
  • file_id (string, optional) — only pass through if the instance's spec lists it; omit otherwise.

Behaviour:

  1. Authorise first: require the project (or the file, once known) to be within the designated-target allow-list; refuse with the guard's hint if not. Never upload before that check.
  2. Read the artifact; refuse a missing/empty/oversized file with a clear message before sending anything.
  3. Build the multipart body with kebab-case field names (name, project-id, version, file), no JSON Content-Type header.
  4. Consume the SSE stream, reporting progress frames through the tool's update callback where available, and decode the end frame via the Transit decoder.
  5. Normalise the end payload to a list of created file ids. If it decodes to something unexpected, fail loudly with the raw payload (do not guess).
  6. Register each created file with the guard as createdFiles in project_id.
  7. Return the created file id(s) in details plus a text summary; when more than one file is created, say so explicitly rather than assuming one.
  8. Map failures precisely: 403 → the project needs edit permission for the token's user; a Transit error body → decoded readable message via decodeTransitError.

Acceptance Criteria

  • The tool refuses to upload when the target project is not in the designated-target allow-list, and no request is sent.
  • The multipart body uses kebab-case fields (name, project-id, version, file) and sends no JSON Content-Type header.
  • The end frame's Transit payload is decoded into the created file id(s) and returned in details.
  • An unexpected or undecodable end payload fails loudly with the raw payload attached.
  • A stream with no end frame fails with the accumulated raw frames attached.
  • A 403 is reported as a project-permission problem naming the token user's needed role.
  • Every created file is registered with the guard, so a subsequent penpot_commit to it is permitted without a separate authorisation call.
  • A missing, empty or oversized artifact is refused locally with no request sent.
  • Unit tests cover multipart construction, end-payload normalisation, the guard refusal path and the permission error mapping, with a stubbed client.

Test Plan

node --test extensions/penpot/src/tools/importLibrary.test.ts

Live validation (requires PENPOT_URL/PENPOT_TOKEN, scratch project):

  1. Authorise the scratch project, then import the repo's .penpot artifact (or any known-good .penpot export) into it.
  2. Confirm the tool returns a created file id and that the file appears in the project in the Penpot UI.
  3. Open the imported file and confirm its contents (frames/rects/text and any colour + typography assets) arrived.
  4. Re-run without authorising the project and confirm the guard refuses with no upload.
  5. Call penpot_commit against the newly created file after staging a colour, and confirm the guard permits it because the file was registered as created.
## Summary Implement `penpot_import_library`: upload a committed `.penpot` library artifact into a nominated project as a new file, using a multipart request with kebab-case field names and reading the created file ids from the SSE `end` frame. This is how the design system's component library gets into a project without hand-authoring components over the API. ## Background **Depends on:** #202 Component authoring over the API (`add-component` with a main-instance marking) is the riskiest part of the API surface, and it is deliberately out of scope. Instead the component library is authored once in the Penpot UI, exported, and committed to the repo as an artifact — the extension imports it per project, repeatedly and reproducibly. Confirmed contract on Penpot 2.17: - `import-binfile` is a **multipart** request. Field names are **kebab-case**: `name`, `project-id`, `version`, `file` (the upload itself). An optional `file-id` parameter also exists on 2.17. - `version` defaults to `3`. - The command requires **project edit permission** on the target project. - The response streams `event: progress` frames and terminates with `event: end` whose `data:` is a **Transit vector of the created file ids** (`["~u<uuid>"]`). Two safety properties matter here: - The import **creates a file**, so it must register that file with the designated-target guard (`registerCreatedFile(fileId, projectId)`) — a file the extension created in a nominated project is exactly the case the guard is designed to permit writes to. - The upload happens immediately (it is not a staged change), so the project scope must be authorised **before** the request is sent. ## 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/`** - `<PENPOT_URL>/api/main/doc/openapi.json` — `import-binfile`'s exact parameters (including whether `file-id` exists on this instance) and its declared response type. - https://help.penpot.app/user-guide/import-export/ — the `.penpot` import flow from the user's perspective: what the file contains and what appears after import. - https://help.penpot.app/user-guide/export-import/export-import-files/ — what "Export shared libraries" and "Include shared library assets in file libraries" mean, i.e. what the artifact carries. - https://help.penpot.app/user-guide/design-systems/libraries/ — the library concept the imported file becomes, and how a consuming file connects to it. - https://help.penpot.app/technical-guide/integration/ — auth and RPC URL shape. **`docs/reference/transit-format/`** - https://github.com/cognitect/transit-format — how the `end` payload's Transit vector and UUID tags are encoded (implemented in the previous step). **`docs/reference/nodejs/`** - https://nodejs.org/api/globals.html#class-formdata and https://nodejs.org/api/globals.html#class-blob — building the multipart body with no third-party dependency. - https://nodejs.org/api/fs.html — reading the `.penpot` artifact. - https://nodejs.org/api/webstreams.html — consuming the SSE body. **`docs/reference/pi-coding-agent/`** and **`docs/reference/typebox/`** - https://pi.dev/docs/latest/extensions — tool registration and structured `details`. - https://github.com/sinclairzx81/typebox — string parameter schemas. ## Implementation Details Create `extensions/penpot/src/tools/importLibrary.ts` (reusing `src/binfile.ts` and `src/media.ts`-style multipart construction) and register the tool. Parameters: - `project_id` (string, **required**) — the project to import into. - `path` (string, **required**) — path to the `.penpot` artifact (default it to the repo artifact under `skills/penpot/assets/`, but allow an explicit path). - `name` (string, optional) — the imported file's name; default the artifact's basename. - `file_id` (string, optional) — only pass through if the instance's spec lists it; omit otherwise. Behaviour: 1. **Authorise first**: require the project (or the file, once known) to be within the designated-target allow-list; refuse with the guard's hint if not. Never upload before that check. 2. Read the artifact; refuse a missing/empty/oversized file with a clear message before sending anything. 3. Build the multipart body with **kebab-case** field names (`name`, `project-id`, `version`, `file`), no JSON `Content-Type` header. 4. Consume the SSE stream, reporting progress frames through the tool's update callback where available, and decode the `end` frame via the Transit decoder. 5. Normalise the `end` payload to a list of created file ids. If it decodes to something unexpected, fail loudly with the raw payload (do not guess). 6. Register each created file with the guard as `createdFiles` in `project_id`. 7. Return the created file id(s) in `details` plus a text summary; when more than one file is created, say so explicitly rather than assuming one. 8. Map failures precisely: 403 → the project needs **edit** permission for the token's user; a Transit error body → decoded readable message via `decodeTransitError`. ## Acceptance Criteria - [ ] The tool refuses to upload when the target project is not in the designated-target allow-list, and no request is sent. - [ ] The multipart body uses kebab-case fields (`name`, `project-id`, `version`, `file`) and sends no JSON `Content-Type` header. - [ ] The `end` frame's Transit payload is decoded into the created file id(s) and returned in `details`. - [ ] An unexpected or undecodable `end` payload fails loudly with the raw payload attached. - [ ] A stream with no `end` frame fails with the accumulated raw frames attached. - [ ] A 403 is reported as a project-permission problem naming the token user's needed role. - [ ] Every created file is registered with the guard, so a subsequent `penpot_commit` to it is permitted without a separate authorisation call. - [ ] A missing, empty or oversized artifact is refused locally with no request sent. - [ ] Unit tests cover multipart construction, `end`-payload normalisation, the guard refusal path and the permission error mapping, with a stubbed client. ## Test Plan ```bash node --test extensions/penpot/src/tools/importLibrary.test.ts ``` Live validation (requires `PENPOT_URL`/`PENPOT_TOKEN`, scratch project): 1. Authorise the scratch project, then import the repo's `.penpot` artifact (or any known-good `.penpot` export) into it. 2. Confirm the tool returns a created file id and that the file appears in the project in the Penpot UI. 3. Open the imported file and confirm its contents (frames/rects/text and any colour + typography assets) arrived. 4. Re-run without authorising the project and confirm the guard refuses with no upload. 5. Call `penpot_commit` against the newly created file after staging a colour, and confirm the guard permits it because the file was registered as created.
david closed this issue 2026-09-15 04:24:19 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #236

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/236
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills#203
No description provided.