Remove cans feature; harden pw_/tok_ cookie flags (pre-release fixes) (#307)
CI / test (push) Successful in 21s
CI / docker (push) Successful in 42s

This commit was merged in pull request #307.
This commit is contained in:
fen committed 2026-10-16 21:59:34 +00:00
1 parent 651bc0cb86
commit c02550050f
20 files changed
+176 -1367

No files matched your search

+5 -64
View File
@@ -18,11 +18,11 @@ The examples below use `http://localhost:8080` as a placeholder.
There is no user account system. Access control works through three mechanisms:
- Viewer cookie. Every request gets an anonymous, random viewer id in an HttpOnly `vwr` cookie. This cookie identifies the browser that created a paste or can, and authorizes browser-scoped actions such as `GET /api/mine` and creator-only deletion.
- Viewer cookie. Every request gets an anonymous, random viewer id in an HttpOnly `vwr` cookie. This cookie identifies the browser that created a paste, and authorizes browser-scoped actions such as `GET /api/mine` and creator-only deletion.
- Deletion token. Creating a paste returns a one-time `deletion_token`. Sending it in the `Authorization` header (Bearer or Token prefix, or the bare value) authorizes deletion of that paste. The token is deliberately not accepted as a URL query parameter, because query strings leak into access logs and browser history.
- Admin key. The admin settings endpoints require an install-time key sent in the `X-Admin-Key` header. The key comes from `PALETTE_ADMIN_KEY` or is generated and persisted server-side at first start.
Password-protected pastes and cans are unlocked with the `X-Paste-Password` request header (or the browser unlock flow on the web pages). Password checks are rate limited per IP and per paste.
Password-protected pastes are unlocked with the `X-Paste-Password` request header (or the browser unlock flow on the web pages). Password checks are rate limited per IP and per paste.
### Rate limiting
@@ -41,7 +41,6 @@ Exceeding a limit returns `429` with a `Retry-After` header and the error code `
- Paste content: `PALETTE_MAX_TEXT` (default 5 MB), admin tunable. Larger content is rejected with `413`.
- Attached files: 25 MB per file, one file per paste.
- Can items: `PALETTE_MAX_ITEM` (default 25 MB) per item.
- The request body itself is capped server side; oversized bodies fail with `413` before handlers run.
### Error format
@@ -130,13 +129,13 @@ Errors: `400` token required, `403` invalid token, `404` not found.
#### GET /api/mine
List pastes (and cans) created from this browser, via the `vwr` cookie. Query parameters: `limit` (default 50, max 100), `offset`.
List pastes created from this browser, via the `vwr` cookie. Query parameters: `limit` (default 50, max 100), `offset`.
Response `200`: `{"total": n, "limit": l, "offset": o, "items": [...]}` with each item carrying `id`, `title`, `language`, `created_at`, `view_count`, `size`, `custom_slug`, `visibility`, `is_can`.
Response `200`: `{"total": n, "limit": l, "offset": o, "items": [...]}` with each item carrying `id`, `title`, `language`, `created_at`, `view_count`, `size`, `custom_slug`, `visibility`.
#### GET /api/public
List public pastes. Query parameters: `limit` (default 25, max 100), `offset`. Same shape as `/api/mine`; items include `is_can` and `custom_slug`.
List public pastes. Query parameters: `limit` (default 25, max 100), `offset`. Same shape as `/api/mine`; items include `custom_slug`.
### Files and raw content
@@ -162,53 +161,6 @@ Response `200`: `{"language": "go"}`. The result is a best-effort guess and may
Errors: `400` invalid JSON body, `429` rate limited.
### Cans
A can bundles multiple text items and files into one shareable page at `/can/{id}`. Cans appear in `/api/mine` and `/api/public` with `is_can: true`.
#### POST /api/pastes/can
Create a can. Multipart form only. Fields:
| Field | Type | Notes |
|---|---|---|
| `title` | string | optional |
| `description` | string | optional |
| `visibility` | string | `public` or `unlisted`; default `public` |
| `expires_in` | string | Go duration, 1 minute to 1 year |
| `password` | string | optional |
| `custom_slug` | string | optional; becomes the can id |
| `json_items` | string | JSON array of `{"title": "...", "content": "...", "language": "..."}` text items |
| files | parts | one or more uploaded file parts |
A can needs at least one item (file or `json_items`); otherwise the can is deleted and `400` (`content_empty`) is returned.
Response `201`: `{"id": "...", "url": "/can/{id}", "items": n}`.
Errors: `400` (`invalid_visibility`, `expiry_invalid`, `invalid json_items`, `content_empty`), `409` slug errors (codes `slug_taken`, `slug_invalid`, `slug_reserved`), `413` item over the size cap.
#### GET /api/cans/{id}
Can metadata with an item list. Password-protected cans require `X-Paste-Password`.
Response `200`: `id`, `title`, `visibility`, `created_at`, and `items`, where each item has `id`, `title`, `content_type`, `size`, and `url` (the item's fetch URL).
Errors: `404` not found or expired, `401` password required.
#### GET /api/cans/{id}/items/{item}
Fetch one can item's content. Items inherit the can's password: send `X-Paste-Password`, or the browser's `pw_{can}` unlock cookie. Served with the same safe content-type guard as `/raw`.
Errors: `404` item not found or not a can item, `401` password required.
#### DELETE /api/cans/{id}
Soft delete a can. Creator browser only: the `vwr` cookie must match the can's viewer. Cans carry no deletion token.
Response `200`: `{"status": "soft-deleted"}`.
Errors: `403` deletion not authorized, `404` not found.
### Admin
#### GET /admin/api/settings and POST /admin/api/settings
@@ -296,17 +248,6 @@ Hard delete immediately (row removed):
curl -X DELETE -H "Authorization: Bearer TOKEN" http://localhost:8080/api/pastes/ab12cd34/redeem
```
Create a can:
```bash
curl -X POST http://localhost:8080/api/pastes/can \
-F "title=My bundle" \
-F "expires_in=48h" \
-F "custom_slug=my-bundle" \
-F 'json_items=[{"title":"notes.txt","content":"some notes"}]' \
-F "files=@screenshot.png"
```
## Deletion and expiry behavior
- Expired pastes are soft-deleted by a background sweeper that runs every minute.