Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .claude/skills/build-plugin/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)).

---

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/build-plugin/references/data-streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
70 changes: 70 additions & 0 deletions .claude/skills/build-plugin/references/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -193,3 +194,72 @@ 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)

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.<key>`. 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}`,
},
});
Comment on lines +245 to +251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and inspect relevant surrounding lines.
wc -l .claude/skills/build-plugin/references/metadata.md
sed -n '220,280p' .claude/skills/build-plugin/references/metadata.md

# Search for related mentions of auth fetch, timeout, cancellation, and WebAPI 1.8.0.
rg -n "authUrl|signedJwt|fetch\\(|timeout|cancell|WebAPI 1\\.8\\.0|plugin request|before every plugin request" .claude/skills/build-plugin -S

Repository: squaredup/plugins

Length of output: 6217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any adjacent docs that define the auth flow or runtime constraints.
fd -a "metadata.md" .claude/skills/build-plugin
fd -a "README.md" .claude/skills/build-plugin
fd -a "*.md" .claude/skills/build-plugin | sed -n '1,120p'

Repository: squaredup/plugins

Length of output: 509


Bound the auth fetch. This pre-request hook runs before every plugin request, so a hung auth endpoint can stall streams and imports. Add a timeout/cancellation path, or document the platform timeout for fetch in WebAPI 1.8.0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/build-plugin/references/metadata.md around lines 245 - 251,
Add timeout or cancellation handling to the auth fetch in the pre-request hook,
using an AbortSignal or the platform’s documented fetch timeout mechanism.
Ensure hung auth requests terminate within a defined limit so plugin streams and
imports cannot stall indefinitely.

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}`;
```
Loading