Files
stuffbox-sdk/docs/protocol.md
T
cyph3rasi bb04d47a29 Release automatic callback setup in SDK 0.2.0
Hop-State: A_06FPARPGMNVEAN3XBVH0QD8
Hop-Proposal: R_06FPARNXMP3DGP5VQGGX4Z8
Hop-Task: T_06FPAR9KH2V5ED7V65JEN00
Hop-Attempt: AT_06FPAR9KH3NZ1CV8RGPJDSG
2026-07-15 03:46:42 -07:00

172 lines
7.3 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.
## Connect an application
There is no manual application-registration step. On the first connection request, Stuffbox creates or reuses a public identity bound to the submitted exact callback URL and returns its `client_id`. An app can send that client ID on later requests for the same callback.
A client ID identifies routing and policy state; it is not a secret, and the callback-derived identity 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.
On the first connection, send the exact callback with no identity field:
```json
{
"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 returning app may also send the public client ID Stuffbox returned for that callback:
```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"
}
```
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 later connection requests. If an app moves to a different callback URL, omit the old client ID so Stuffbox can create or reuse the identity for the new exact callback.
When a client ID is sent, the callback must exactly match the URL bound to that identity. HTTPS callbacks are mandatory except loopback `http://localhost`, `127.0.0.1`, and `[::1]` URLs used for development. URLs cannot contain credentials, query strings, 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.
For compatibility, Stuffbox still accepts the deprecated `registration_mode: "self_hosted"` field sent by SDK 0.1.0. New integrations should omit it.
### `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 app's same-origin upload-session proxy; the app 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`.