Implement PENPOT_URL/PENPOT_TOKEN resolution in src/env.ts with unit tests #187

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

Summary

Implement extensions/penpot/src/env.ts: resolve and normalise the PENPOT_URL base URL and the PENPOT_TOKEN personal access token from the environment, with actionable failure messages when either is missing or malformed. Pure functions, unit-tested with node --test.

Background

Depends on: #186

Every penpot tool call needs two environment values:

  • PENPOT_URL — the instance base URL, with no /api suffix (e.g. https://penpot.example.com). Users routinely paste the UI URL or a URL ending in /api, and a stray suffix makes every RPC call 404, so this module normalises it rather than propagating the mistake.
  • PENPOT_TOKEN — a Penpot personal access token. It is presented as Authorization: Token <token> (not Bearer). Tokens live in the per-machine environment (or .env); .env must never be committed.

Configuration is resolved at call time, not at extension load time, so a machine without Penpot configured still loads the extension cleanly. This mirrors the lazy-resolution pattern in extensions/victorialogs/src/env.ts.

This module is a prerequisite for the RPC client and every later read/write tool. It must stay dependency-free apart from node:test in the co-located test file.

Documentation Required

A separate process downloads these into the listed folder before this issue is implemented. Check the folder for the actual reference material before starting.

docs/reference/penpot-api/

  • https://help.penpot.app/technical-guide/integration/ — official Penpot integration guide: the access-token model, the exact base URL shape (no /api suffix), and the canonical curl -H "Authorization: Token <token>" https://design.penpot.app/api/rpc/command/get-profile example. This is the authoritative source for what a valid PENPOT_URL looks like and which header scheme is used.
  • <PENPOT_URL>/api/main/doc/openapi.json — the instance's own OpenAPI 3.0 spec (generated from Penpot source). Useful for confirming command paths are rooted at /api/rpc/command/; the full vendored copy is produced in the skill-content milestone.

docs/reference/nodejs/

Implementation Details

Create extensions/penpot/src/env.ts with (at minimum) these pure, independently-testable pieces:

  1. normalizeBaseUrl(raw: string): string

    • Trims surrounding whitespace.
    • Strips a single trailing /.
    • Strips a trailing /api (case-insensitive) that would otherwise be duplicated when the client appends /api/rpc/command/....
    • Strips a trailing / again after removing /api.
    • Returns the URL unchanged otherwise; must not touch path segments beyond the tail (a self-hosted instance under a sub-path, e.g. https://host/penpot, must survive intact).
  2. resolveConfig(env: Record<string, string | undefined> = process.env) returning a discriminated result — for example { ok: true, value: { baseUrl, token } } | { ok: false, error: string } — rather than throwing, so callers (and tests) can branch. Follow the Result-style shape already used in extensions/forgejo/src/errors.ts if that is convenient, but do not import forgejo code.

  3. Actionable error messages. Each failure names the variable and says what to do:

    • Missing PENPOT_URL → tell the user to set it to the instance base URL without the /api suffix, and give a concrete example.
    • Missing PENPOT_TOKEN → tell the user to create a personal access token in Penpot (account settings → access tokens) and export it as PENPOT_TOKEN.
    • Present-but-not-a-URL PENPOT_URL (fails new URL(...)) → quote the offending value and restate the expected shape.
    • Whitespace-only values count as missing.
  4. Malformed-token guard is intentionally minimal — do not attempt to validate token format (Penpot tokens are opaque). Only reject empty/whitespace-only values.

Keep the module importable with zero third-party dependencies so tests run without node_modules.

Write co-located tests in extensions/penpot/src/env.test.ts (or extensions/penpot/env.test.ts — pick the location used by the rest of this extension and stay consistent; the root npm test glob for this extension is registered in a later milestone).

Acceptance Criteria

  • normalizeBaseUrl strips a trailing /, a trailing /api and a trailing / after that, and is idempotent.
  • A base URL with a real sub-path (e.g. https://host/penpot) is preserved unchanged.
  • resolveConfig returns the normalised base URL and the token when both variables are set.
  • Missing/whitespace-only PENPOT_URL produces an actionable error naming the variable and the expected shape (no /api suffix).
  • Missing/whitespace-only PENPOT_TOKEN produces an actionable error naming the variable and how to obtain a token.
  • A non-URL PENPOT_URL produces an error quoting the offending value rather than throwing a raw TypeError.
  • The module imports no third-party package.
  • node --test extensions/penpot/src/env.test.ts (or the chosen test path) is green.

Test Plan

# unit tests only, no network and no credentials required
node --test extensions/penpot/src/env.test.ts

Manual sanity check that the module sees real environment values:

PENPOT_URL="https://penpot.example.com/api" PENPOT_TOKEN="tok" node --input-type=module -e "
import { resolveConfig } from './extensions/penpot/src/env.ts';
console.log(resolveConfig());
// expect: { ok: true, value: { baseUrl: 'https://penpot.example.com', token: 'tok' } }
"
## Summary Implement `extensions/penpot/src/env.ts`: resolve and normalise the `PENPOT_URL` base URL and the `PENPOT_TOKEN` personal access token from the environment, with actionable failure messages when either is missing or malformed. Pure functions, unit-tested with `node --test`. ## Background **Depends on:** #186 Every penpot tool call needs two environment values: - `PENPOT_URL` — the instance base URL, with **no** `/api` suffix (e.g. `https://penpot.example.com`). Users routinely paste the UI URL or a URL ending in `/api`, and a stray suffix makes every RPC call 404, so this module normalises it rather than propagating the mistake. - `PENPOT_TOKEN` — a Penpot personal access token. It is presented as `Authorization: Token <token>` (not `Bearer`). Tokens live in the per-machine environment (or `.env`); `.env` must never be committed. Configuration is resolved **at call time**, not at extension load time, so a machine without Penpot configured still loads the extension cleanly. This mirrors the lazy-resolution pattern in `extensions/victorialogs/src/env.ts`. This module is a prerequisite for the RPC client and every later read/write tool. It must stay dependency-free apart from `node:test` in the co-located test file. ## Documentation Required A separate process downloads these into the listed folder before this issue is implemented. Check the folder for the actual reference material before starting. **`docs/reference/penpot-api/`** - https://help.penpot.app/technical-guide/integration/ — official Penpot integration guide: the access-token model, the exact base URL shape (no `/api` suffix), and the canonical `curl -H "Authorization: Token <token>" https://design.penpot.app/api/rpc/command/get-profile` example. This is the authoritative source for what a valid `PENPOT_URL` looks like and which header scheme is used. - `<PENPOT_URL>/api/main/doc/openapi.json` — the instance's own OpenAPI 3.0 spec (generated from Penpot source). Useful for confirming command paths are rooted at `/api/rpc/command/`; the full vendored copy is produced in the skill-content milestone. **`docs/reference/nodejs/`** - https://nodejs.org/api/process.html#processenv — `process.env` access and the fact that missing variables are `undefined`, not empty strings. ## Implementation Details Create `extensions/penpot/src/env.ts` with (at minimum) these pure, independently-testable pieces: 1. **`normalizeBaseUrl(raw: string): string`** - Trims surrounding whitespace. - Strips a single trailing `/`. - Strips a trailing `/api` (case-insensitive) that would otherwise be duplicated when the client appends `/api/rpc/command/...`. - Strips a trailing `/` again after removing `/api`. - Returns the URL unchanged otherwise; must not touch path segments beyond the tail (a self-hosted instance under a sub-path, e.g. `https://host/penpot`, must survive intact). 2. **`resolveConfig(env: Record<string, string | undefined> = process.env)`** returning a discriminated result — for example `{ ok: true, value: { baseUrl, token } } | { ok: false, error: string }` — rather than throwing, so callers (and tests) can branch. Follow the `Result`-style shape already used in `extensions/forgejo/src/errors.ts` if that is convenient, but do not import forgejo code. 3. **Actionable error messages.** Each failure names the variable and says what to do: - Missing `PENPOT_URL` → tell the user to set it to the instance base URL *without* the `/api` suffix, and give a concrete example. - Missing `PENPOT_TOKEN` → tell the user to create a personal access token in Penpot (account settings → access tokens) and export it as `PENPOT_TOKEN`. - Present-but-not-a-URL `PENPOT_URL` (fails `new URL(...)`) → quote the offending value and restate the expected shape. - Whitespace-only values count as missing. 4. **Malformed-token guard is intentionally minimal** — do not attempt to validate token format (Penpot tokens are opaque). Only reject empty/whitespace-only values. Keep the module importable with zero third-party dependencies so tests run without `node_modules`. Write co-located tests in `extensions/penpot/src/env.test.ts` (or `extensions/penpot/env.test.ts` — pick the location used by the rest of this extension and stay consistent; the root `npm test` glob for this extension is registered in a later milestone). ## Acceptance Criteria - [ ] `normalizeBaseUrl` strips a trailing `/`, a trailing `/api` and a trailing `/` after that, and is idempotent. - [ ] A base URL with a real sub-path (e.g. `https://host/penpot`) is preserved unchanged. - [ ] `resolveConfig` returns the normalised base URL and the token when both variables are set. - [ ] Missing/whitespace-only `PENPOT_URL` produces an actionable error naming the variable and the expected shape (no `/api` suffix). - [ ] Missing/whitespace-only `PENPOT_TOKEN` produces an actionable error naming the variable and how to obtain a token. - [ ] A non-URL `PENPOT_URL` produces an error quoting the offending value rather than throwing a raw `TypeError`. - [ ] The module imports no third-party package. - [ ] `node --test extensions/penpot/src/env.test.ts` (or the chosen test path) is green. ## Test Plan ```bash # unit tests only, no network and no credentials required node --test extensions/penpot/src/env.test.ts ``` Manual sanity check that the module sees real environment values: ```bash PENPOT_URL="https://penpot.example.com/api" PENPOT_TOKEN="tok" node --input-type=module -e " import { resolveConfig } from './extensions/penpot/src/env.ts'; console.log(resolveConfig()); // expect: { ok: true, value: { baseUrl: 'https://penpot.example.com', token: 'tok' } } " ```
david closed this issue 2026-09-14 23:30:01 +00:00
Author
Owner

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

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/220
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#187
No description provided.