Files
stuffbox-sdk/docs/protocol.md
T
cyph3rasi 9e1ed2c9cb Publish the unified MIT-licensed Stuffbox SDK source
Hop-State: A_06FPAF0M83E7T0JJ7EP2WQ0
Hop-Proposal: R_06FPAF04S0D4G8TA8TNK3C0
Hop-Task: T_06FPADTV2YD4PD5GMTK3150
Hop-Attempt: AT_06FPADTV2ZYKEY8GH3JTK98
2026-07-15 03:04:24 -07:00

174 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Stuffbox protocol v1
All control endpoints return JSON. Successful response fields use `snake_case`; SDK objects use idiomatic TypeScript names. Errors have a stable envelope:
```json
{
"error": {
"code": "quota_exceeded",
"message": "Upload exceeds remaining storage quota",
"details": {}
}
}
```
Clients must not parse the human message. Branch on `error.code`. A `429` response includes `Retry-After` when available. Token responses and canonical redirects use `Cache-Control: no-store`.
## Scopes
- `assets:read` — list the users active media library and retrieve usable canonical file URLs.
- `assets:write` — create and complete upload sessions.
- `assets:delete` — tombstone assets and delete their stored objects.
## Register and connect an application
Stuffbox supports two application identities:
- A conventional hosted application is registered once in the Stuffbox developer dashboard. Stuffbox assigns a public `client_id` and stores one or more exact callback URLs.
- A self-hosted application registers itself during its first connection request. Stuffbox creates or reuses an identity for that exact callback URL and returns its public `client_id`; the person running the installation does not visit Stuffbox to register it manually.
A client ID identifies routing and policy state; it is not a secret. Automatic self-hosted registration does not assert ownership of the callback domain. Exact callback binding, PKCE, state verification, user consent, and scoped tokens protect the flow.
The application creates and retains a random PKCE verifier (43128 RFC 7636 unreserved characters), its S256 challenge, and a random state value.
### `POST /api/v1/connection-requests`
No bearer credential. Rate limited.
A conventional hosted application, or a returning self-hosted installation, sends its persisted client ID:
```json
{
"client_id": "app_public-client-id",
"callback_url": "https://node.example/settings/stuffbox/callback",
"code_challenge": "base64url-sha256-challenge",
"code_challenge_method": "S256",
"scopes": ["assets:read"],
"state": "node-generated-state"
}
```
A self-hosted installation without a persisted client ID sends:
```json
{
"registration_mode": "self_hosted",
"callback_url": "https://node.example/settings/stuffbox/callback",
"code_challenge": "base64url-sha256-challenge",
"code_challenge_method": "S256",
"scopes": ["assets:read"],
"state": "node-generated-state"
}
```
These examples request read-only gallery access. Add `assets:write` or `assets:delete` only when the application has a feature that needs that permission.
Both forms return:
```json
{
"request_id": "cr_...",
"client_id": "app_public-client-id",
"callback_url": "https://node.example/settings/stuffbox/callback",
"authorization_url": "https://stuffbox.example/connect/cr_...",
"expires_at": "2026-07-15T00:10:00.000Z"
}
```
Persist the returned `client_id` and canonical `callback_url` before redirecting the user's browser to `authorization_url`. Use that pair for the authorization-code exchange and all later connection requests. If a self-hosted installation moves to a different callback URL, begin a new self-hosted registration for that exact URL.
For a managed application, the callback must exactly match one registered for its client ID. For a self-hosted application, Stuffbox creates or reuses the identity for the submitted exact callback. HTTPS callbacks are mandatory except loopback `http://localhost`, `127.0.0.1`, and `[::1]` URLs used for development. URLs cannot contain credentials or fragments.
The consent page requires a Stuffbox login and identifies the application by the exact callback domain, alongside every requested permission. Approval redirects only to the stored callback with `code` and the original `state`. The application must compare state before exchanging the code.
### `POST /api/v1/token`
Authorization-code exchange:
```json
{
"grant_type": "authorization_code",
"client_id": "app_public-client-id",
"code": "sbc_opaque-code",
"code_verifier": "original-pkce-verifier",
"redirect_uri": "https://node.example/settings/stuffbox/callback"
}
```
Refresh rotation:
```json
{
"grant_type": "refresh_token",
"refresh_token": "sbr_opaque-refresh-token"
}
```
The `client_id` and `redirect_uri` must be the values persisted from the connection-request response. The response contains `access_token`, `refresh_token`, `token_type: "Bearer"`, `expires_in`, and a space-delimited `scope`. Replace the old refresh token atomically with the returned one. Reuse of a consumed token revokes the entire grant and returns `refresh_token_reuse`; the application must reconnect.
### `POST /api/v1/revoke`
```json
{ "token": "sbr_or_sba_value", "token_type_hint": "refresh_token" }
```
Revokes the token's whole grant. Unknown values return success to avoid becoming a token oracle.
## Direct upload
All asset and upload endpoints use `Authorization: Bearer {access_token}`. The browser should normally call the node's same-origin upload-session proxy; the node calls Stuffbox and returns only the signed upload details.
### `POST /api/v1/uploads`
Requires `assets:write` and is rate limited.
```json
{
"filename": "photo.png",
"mime_type": "image/png",
"size": 12345,
"sha256": "optional-64-character-lowercase-hex"
}
```
Stuffbox validates policy and atomically reserves quota, then returns:
```json
{
"upload_id": "upl_...",
"upload_url": "https://object-store.example/signed-put",
"method": "PUT",
"required_headers": {
"content-type": "image/png",
"if-none-match": "*"
},
"expires_at": "2026-07-15T00:10:00.000Z"
}
```
The browser sends the file body directly to `upload_url` with exactly the returned method and headers. It must not send the signed URL to logs or analytics.
### `POST /api/v1/uploads/{uploadId}/complete`
Requires `assets:write` on the grant that created the pending upload. Stuffbox loads the server-selected provider/key, performs `HEAD`, requires exact size/type, compares a provider checksum when available, and atomically turns reserved bytes into used bytes. A successful response is the asset described below. Completion is idempotent.
## Assets
### `GET /api/v1/assets`
Requires `assets:read`. The library is user-owned rather than grant-owned, so the response includes active media uploaded through any app the same Stuffbox user connected. Query parameters are `limit` (1100, default 100) and an opaque `cursor` returned by the previous page. Results use a stable newest-first order and return `{ "items": [asset], "next_cursor": "..." }` when another page is available. Treat cursors as opaque and omit `cursor` for the first page.
### `GET /api/v1/assets/{assetId}`
Requires `assets:read` and predicates the query on the grant owner's user ID. A cross-owner ID is indistinguishable from a missing ID.
### `DELETE /api/v1/assets/{assetId}`
Requires `assets:delete`. Tombstones the asset and releases used quota in a transaction before deleting the provider object. Canonical delivery stops immediately. Provider deletion is idempotent and retryable.
Asset fields are `id`, `public_id`, `canonical_url`, `original_filename`, `mime_type`, `byte_size`, optional `sha256`, `status`, `created_at`, and optional `deleted_at`. Internal object keys and provider credentials are absent.
### `GET` or `HEAD /f/{publicAssetId}`
No bearer credential. Active assets return a temporary `307` redirect to a CDN/provider URL; Stuffbox never proxies the file body. Deleted assets return `410` and unknown IDs return `404`.