9e1ed2c9cb
Hop-State: A_06FPAF0M83E7T0JJ7EP2WQ0 Hop-Proposal: R_06FPAF04S0D4G8TA8TNK3C0 Hop-Task: T_06FPADTV2YD4PD5GMTK3150 Hop-Attempt: AT_06FPADTV2ZYKEY8GH3JTK98
174 lines
7.5 KiB
Markdown
174 lines
7.5 KiB
Markdown
# 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 user’s 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 (43–128 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` (1–100, 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`.
|