From 2865a0ac65fa8d3826b06f0d4919249ee14a54b3 Mon Sep 17 00:00:00 2001 From: Andrew Harris Date: Thu, 16 Jul 2026 07:52:13 +0100 Subject: [PATCH 1/2] Guide build-plugin on pre-request auth scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebAPI base plugin 1.8.0 (squaredup-plugin-repository PR #1966, SAAS-8337) adds datasource-level pre-request scripting so plugins can handle custom auth flows no authMode expresses — token-exchange steps and per-request HMAC signing. The matching saas change lets a declarative plugin's base.config reference the script as a file under dataStreams/scripts/preRequest/, inlined at deploy time. The build-plugin skill had no coverage of this, so an agent building a plugin for such an API had no supported path. Add a reference section beside the auth patterns in metadata.md covering when to reach for a script (and to prefer built-in authModes first), the wiring including scriptingVariables and the reserved scriptState property, the script's variable scope, and a token-exchange example with state-cached expiry drawn from the PR's real N-Able script. Phase 1's auth step and the post-request-scripts section point at it, keeping the full material in one place per the skill-writing guidance. Co-Authored-By: Claude Fable 5 --- .claude/skills/build-plugin/SKILL.md | 6 +- .../build-plugin/references/data-streams.md | 2 +- .../build-plugin/references/metadata.md | 67 +++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/.claude/skills/build-plugin/SKILL.md b/.claude/skills/build-plugin/SKILL.md index d508a634..0988c978 100644 --- a/.claude/skills/build-plugin/SKILL.md +++ b/.claude/skills/build-plugin/SKILL.md @@ -3,7 +3,7 @@ name: build-plugin description: Guides building a SquaredUp low-code plugin for HTTP/REST APIs, from API exploration through deployment. Use when the user wants to integrate a service with SquaredUp, add a new data source, connect to a third-party tool, "pull data from", or "monitor" any service in SquaredUp. metadata: author: SquaredUp - version: "0.0.11" + version: "0.0.12" --- # Building a SquaredUp Low-Code Plugin @@ -68,7 +68,7 @@ Before writing a single file, understand and explore the API. **Use `AskUserQues - **Time-range control** — does the endpoint accept a queryable time range at all (a `from`/`to`, `start`/`end`, or `period` parameter), or only return a fixed snapshot / current values? - **Data granularity** — when the endpoint _does_ accept a range, the finest interval it aggregates at: **per-event/raw**, **hourly**, **daily**, or **monthly**. Read it off the API docs (aggregation windows, `granularity`/`interval` params, the minimum queryable range). 5. **Understand pagination** — Cursor/next-token, or offset/limit? Separate concern from response transformation. -6. **Note the auth pattern** — API key in header, Bearer token, OAuth2, Basic auth, JWT Bearer (signed-JWT auth)? Determine from the docs. +6. **Note the auth pattern** — API key in header, Bearer token, OAuth2, Basic auth, JWT Bearer (signed-JWT auth)? Determine from the docs. If the flow is custom — a token-exchange step or per-request signing no standard mode covers — plan a **pre-request script** ([metadata.md](references/metadata.md#pre-request-scripts-custom-auth-flows)). --- @@ -188,6 +188,8 @@ my-plugin/ myStream.json scripts/ myScript.js + preRequest/ + my-plugin.js # only when using a pre-request script (custom auth) defaultContent/ manifest.json scopes.json diff --git a/.claude/skills/build-plugin/references/data-streams.md b/.claude/skills/build-plugin/references/data-streams.md index 8c220cd1..dc0ca0d3 100644 --- a/.claude/skills/build-plugin/references/data-streams.md +++ b/.claude/skills/build-plugin/references/data-streams.md @@ -732,7 +732,7 @@ Replace a raw ID column with a human-readable property from a related indexed ob ## Post-request scripts -Scripts run after the HTTP response is received. Input is `data` (parsed JSON body). Set `result` to an array of row objects. +Scripts run after the HTTP response is received. Input is `data` (parsed JSON body). Set `result` to an array of row objects. (For a datasource-level script that runs _before_ every request — custom auth flows — see [Pre-request scripts](metadata.md#pre-request-scripts-custom-auth-flows) in metadata.md.) ### Wiring a script to a stream diff --git a/.claude/skills/build-plugin/references/metadata.md b/.claude/skills/build-plugin/references/metadata.md index a13502fb..ce02553b 100644 --- a/.claude/skills/build-plugin/references/metadata.md +++ b/.claude/skills/build-plugin/references/metadata.md @@ -4,6 +4,7 @@ - [metadata.json template and field notes](#metadatajson) - [Auth patterns](#auth-patterns) +- [Pre-request scripts (custom auth flows)](#pre-request-scripts-custom-auth-flows) --- @@ -193,3 +194,69 @@ Set exactly one of `jwtSecret` (HMAC algorithms) or `jwtPrivateKey` — a PEM-en ``` `jwtPayload` and `jwtHeaders` are plain JSON objects; any string value can itself be a `{{ ... }}` expression, evaluated fresh on every request — this is how `iat`/`exp` stay current without any extra logic. + +--- + +## Pre-request scripts (custom auth flows) + +> Requires WebAPI base plugin **1.8.0+**. + +Some APIs have an auth flow no `authMode` above can express — exchanging a long-lived credential for a short-lived access token via an auth endpoint, or signing each request (HMAC canonical-request signatures). For these, set a **pre-request script** in `base.config`: JavaScript that runs immediately before **every** HTTP request the plugin makes (all data streams, including import streams) and can rewrite the request's `url`, `headers`, and `body`. + +Exhaust the auth patterns above first — a pre-request script that only sets a static header is just a `headers` entry, and token refresh for OAuth2/JWT Bearer is already automatic. + +### Wiring + +In `base.config`: + +```json +"scriptingVariables": [ + { "key": "signedJwt", "value": "{{signedJwt}}" } +], +"preRequestScript": "preRequest/my-plugin.js" +``` + +- `scriptingVariables` — key/value secrets the script reads as `secrets.`. Values support `{{fieldName}}` expressions referencing `ui.json` fields, same as `headers`. Values are encrypted at rest. +- `preRequestScript` — a file reference relative to `dataStreams/scripts/`: put the script at `dataStreams/scripts/preRequest/my-plugin.js` (named after the plugin, `.js` extension required); it's inlined into the config at deploy time. +- `scriptState` — never set this: it's a reserved config property where the platform persists the script's `state`, encrypted. + +### Script scope + +The script body runs inside an async function — top-level `await` works. `fetch` and `crypto.subtle` are available (for auth calls and HMAC/SHA signing). + +| Variable | Mutable | Notes | +| --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `url` | Yes | [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instance of the full endpoint address about to be called | +| `method` | No | `"get"` or `"post"` | +| `headers` | Yes | POJO of request headers — the usual thing a script mutates | +| `body` | Yes | Request body (POST only) | +| `state` | Yes | POJO persisted (encrypted) between requests — cache tokens here with an expiry. It can disappear at any time, so always re-derive when missing or expired, never assume it's populated | +| `secrets` | No | Values from `scriptingVariables`, by key | +| `api` | No | `api.report.warning(text)` / `api.report.error(text)` | +| `context` | No | `dataSources[0]` (the plugin config, incl. `baseUrl`), `objects`, `timeframe`, `config` (the calling stream's config), `pagingContext` | + +### Example: token exchange with cached state + +The user supplies a pre-signed JWT (a `ui.json` field mapped via `scriptingVariables`); the script exchanges it for a short-lived access token and caches it in `state` until expiry: + +```javascript +// dataStreams/scripts/preRequest/my-plugin.js +const now = Date.now(); +if (typeof state?.token !== "string" || (state?.expiryTime ?? 0) <= now) { + const authUrl = `${context.dataSources[0].baseUrl.replace(/\/*$/, "")}/api/auth/authenticate`; + const resp = await fetch(authUrl, { + method: "post", + headers: { Accept: "application/json", Authorization: `Bearer ${secrets.signedJwt}` }, + }); + if (!resp.ok) { + api.report.error(`Failed to get token: ${resp.status} - ${resp.statusText}`); + } + const payload = await resp.json(); + state = { + token: payload.tokens.access.token, + // refresh 1 minute early to be safe + expiryTime: now + payload.tokens.access.expirySeconds * 1000 - 60000, + }; +} +headers["Authorization"] = `Bearer ${state.token}`; +``` From e9ed69829e620078cc8f521301d0c34a2bf46d8b Mon Sep 17 00:00:00 2001 From: Andrew Harris Date: Thu, 16 Jul 2026 08:21:30 +0100 Subject: [PATCH 2/2] remove version reference --- .../build-plugin/references/metadata.md | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/.claude/skills/build-plugin/references/metadata.md b/.claude/skills/build-plugin/references/metadata.md index ce02553b..2592e809 100644 --- a/.claude/skills/build-plugin/references/metadata.md +++ b/.claude/skills/build-plugin/references/metadata.md @@ -199,8 +199,6 @@ Set exactly one of `jwtSecret` (HMAC algorithms) or `jwtPrivateKey` — a PEM-en ## Pre-request scripts (custom auth flows) -> Requires WebAPI base plugin **1.8.0+**. - Some APIs have an auth flow no `authMode` above can express — exchanging a long-lived credential for a short-lived access token via an auth endpoint, or signing each request (HMAC canonical-request signatures). For these, set a **pre-request script** in `base.config`: JavaScript that runs immediately before **every** HTTP request the plugin makes (all data streams, including import streams) and can rewrite the request's `url`, `headers`, and `body`. Exhaust the auth patterns above first — a pre-request script that only sets a static header is just a `headers` entry, and token refresh for OAuth2/JWT Bearer is already automatic. @@ -224,16 +222,16 @@ In `base.config`: The script body runs inside an async function — top-level `await` works. `fetch` and `crypto.subtle` are available (for auth calls and HMAC/SHA signing). -| Variable | Mutable | Notes | +| Variable | Mutable | Notes | | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `url` | Yes | [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instance of the full endpoint address about to be called | -| `method` | No | `"get"` or `"post"` | -| `headers` | Yes | POJO of request headers — the usual thing a script mutates | -| `body` | Yes | Request body (POST only) | +| `url` | Yes | [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) instance of the full endpoint address about to be called | +| `method` | No | `"get"` or `"post"` | +| `headers` | Yes | POJO of request headers — the usual thing a script mutates | +| `body` | Yes | Request body (POST only) | | `state` | Yes | POJO persisted (encrypted) between requests — cache tokens here with an expiry. It can disappear at any time, so always re-derive when missing or expired, never assume it's populated | -| `secrets` | No | Values from `scriptingVariables`, by key | -| `api` | No | `api.report.warning(text)` / `api.report.error(text)` | -| `context` | No | `dataSources[0]` (the plugin config, incl. `baseUrl`), `objects`, `timeframe`, `config` (the calling stream's config), `pagingContext` | +| `secrets` | No | Values from `scriptingVariables`, by key | +| `api` | No | `api.report.warning(text)` / `api.report.error(text)` | +| `context` | No | `dataSources[0]` (the plugin config, incl. `baseUrl`), `objects`, `timeframe`, `config` (the calling stream's config), `pagingContext` | ### Example: token exchange with cached state @@ -246,10 +244,15 @@ if (typeof state?.token !== "string" || (state?.expiryTime ?? 0) <= now) { const authUrl = `${context.dataSources[0].baseUrl.replace(/\/*$/, "")}/api/auth/authenticate`; const resp = await fetch(authUrl, { method: "post", - headers: { Accept: "application/json", Authorization: `Bearer ${secrets.signedJwt}` }, + headers: { + Accept: "application/json", + Authorization: `Bearer ${secrets.signedJwt}`, + }, }); if (!resp.ok) { - api.report.error(`Failed to get token: ${resp.status} - ${resp.statusText}`); + api.report.error( + `Failed to get token: ${resp.status} - ${resp.statusText}`, + ); } const payload = await resp.json(); state = {