Runn Token Broker

A middleware API that turns Runn's account-level API token into safe, per-user timesheet access.

1. The Problem

Runn's API v1 (https://api.runn.io) only issues account-level tokens, created by admins under Settings → API. A token is tied to the account, not to a user — so any write-scoped token can modify anyone's timesheets. There is no personal access token, and none is on Runn's public roadmap.

We still want individual users (starting with: us) to automate their own timesheet submission without ever holding the privileged token. The answer is a small token broker: a service that holds the one privileged Runn token server-side and issues its own scoped, per-user tokens on top.

2. Architecture

CLIENTS (untrusted) CLI script cron / manual Web UI / Shortcut per-user Automation agent n8n / Zapier / bot holds: user token only (JWT, scoped, expiring) TOKEN BROKER (trusted) Auth: validate user JWT resolve → Runn personId Policy: endpoint allowlist force personId, filter reads Upstream client: rate-limit, 100ms spacing, retries Audit log who did what, when Secrets: LIVE_ Runn token server-side only, never leaves RUNN api.runn.io API v1 /actuals/* timesheets Bearer: user JWT Bearer: LIVE_… narrow, rewritten calls

Fig 1 — Three trust zones. The privileged Runn token exists only inside the broker.

The broker is not a transparent proxy. It exposes its own small API (/my/…) and translates each call into a specific, constrained Runn API call. Clients cannot reach arbitrary Runn endpoints through it.

3. Request Flow

Client Broker Runn API POST /my/timesheet {date, projectId, minutes} Authorization: Bearer <user JWT> 1. verify JWT sig + expiry + scope 2. map sub → personId (server-side) 3. validate payload vs allowlist POST /actuals/timeentry (personId forced by broker) Authorization: Bearer LIVE_… · Accept-Version: 1.0.0 200 OK · actual created/updated 4. audit log: user, action, payload hash, upstream result 201 {status: "logged", date, minutes} Any personId in the client payload is ignored/rejected — identity comes only from the validated token.

Fig 2 — Happy path for a timesheet submission.

4. Broker API Surface

Keep it deliberately tiny. Every endpoint maps to one constrained upstream operation.

Broker endpointUpstream (Runn v1)Constraint applied
POST /my/timesheet POST /actuals/timeentry personId forced from token; date range limited (e.g. current ± 1 period)
POST /my/timesheet/bulk POST /actuals/bulk/ All entries rewritten to caller's personId; ≤ 100 entries; broker paces same-day entries
GET /my/actuals?from&to GET /actuals Response filtered to caller's personId before returning
GET /my/assignments GET /assignments Filtered to caller; used to know what projects/roles are valid targets
GET /my/projects GET /projects (+ cache) Only projects the caller is assigned to; names/ids only, no financials

Anti-pattern: a generic /proxy/* route that forwards paths to api.runn.io. That hands every client the full power of the admin token, including other people's data and financials. Never build this.

5. Token Model & Lifecycle

Two token types, two worlds

Runn account tokenBroker user token
Issued byRunn admin (Settings → API)Broker admin endpoint / CLI
Lives inSecrets manager on the broker host onlyClient machines (CLI config, keychain)
ScopeWhole account (write)One personId, allowlisted endpoints
FormatLIVE_… / TEST_…Signed JWT: sub, personId, scope, exp, jti
RotationSet expiry at creation; rotate manuallyShort-lived (e.g. 30–90 days) + revocation list by jti

Issuing a user token

# admin-only, on the broker host
$ broker-admin issue --email jan@company.com --person-id 4711 \
    --scope timesheet:write,timesheet:read --ttl 60d
→ eyJhbGciOiJIUzI1NiIs…   (hand to the user once, store hash of jti)

The mapping email → personId is resolved once at issue time (via GET /people) and baked into the token + a server-side record. The client never supplies it.

6. Security Rules (Non-Negotiable)

7. Runn API Quirks to Handle Centrally

These live in the broker's upstream client so no consumer ever has to know about them:

8. Suggested Stack

Bottom line: Runn gives you one big key. The broker turns it into many small keys — each opening exactly one person's timesheet drawer, with a camera pointed at the lock.