clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / source-nips / 96.md
96.md 14.9K rendered
1> __Warning__ `unrecommended`: deprecated in favor of [NIP-B7](B7.md)23NIP-964======56HTTP File Storage Integration7-----------------------------89`draft` `unrecommended` `optional`1011## Introduction1213This NIP defines a REST API for HTTP file storage servers intended to be used in conjunction with the nostr network.14The API will enable nostr users to upload files and later reference them by url on nostr notes.1516The spec DOES NOT use regular nostr events through websockets for17storing, requesting nor retrieving data because, for simplicity, the server18will not have to learn anything about nostr relays.1920## Server Adaptation2122File storage servers wishing to be accessible by nostr users should opt-in by making available an https route at `/.well-known/nostr/nip96.json` with `api_url`:2324```yaml25{26 // Required27 // File upload and deletion are served from this url28 // Also downloads if "download_url" field is absent or empty string29 "api_url": "https://your-file-server.example/custom-api-path",30 // Optional31 // If absent, downloads are served from the api_url32 "download_url": "https://a-cdn.example/a-path",33 // Optional34 // Note: This field is not meant to be set by HTTP Servers.35 // Use this if you are a nostr relay using your /.well-known/nostr/nip96.json36 // just to redirect to someone else's http file storage server's /.well-known/nostr/nip96.json37 // In this case, "api_url" field must be an empty string38 "delegated_to_url": "https://your-file-server.example",39 // Optional40 "supported_nips": [60],41 // Optional42 "tos_url": "https://your-file-server.example/terms-of-service",43 // Optional44 "content_types": ["image/jpeg", "video/webm", "audio/*"],45 // Optional46 "plans": {47 // "free" is the only standardized plan key and48 // clients may use its presence to learn if server offers free storage49 "free": {50 "name": "Free Tier",51 // Default is true52 // All plans MUST support NIP-98 uploads53 // but some plans may also allow uploads without it54 "is_nip98_required": true,55 "url": "https://...", // plan's landing page if there is one56 "max_byte_size": 10485760,57 // Range in days / 0 for no expiration58 // [7, 0] means it may vary from 7 days to unlimited persistence,59 // [0, 0] means it has no expiration60 // early expiration may be due to low traffic or any other factor61 "file_expiration": [14, 90],62 "media_transformations": {63 "image": [64 "resizing"65 ]66 }67 }68 }69}70```7172### Relay Hints7374Note: This section is not meant to be used by HTTP Servers.7576A nostr relay MAY redirect to someone else's HTTP file storage server by77adding a `/.well-known/nostr/nip96.json` with "delegated_to_url" field78pointing to the url where the server hosts its own79`/.well-known/nostr/nip96.json`. In this case, the "api_url" field must80be an empty string and all other fields must be absent.8182If the nostr relay is also an HTTP file storage server,83it must use the "api_url" field instead.8485### List of Supporting File Storage Servers8687See https://github.com/aljazceru/awesome-nostr#nip-96-file-storage-servers.8889## Auth9091When indicated, `clients` must add an [NIP-98](98.md) `Authorization` header (**optionally** with the encoded `payload` tag set to the base64-encoded 256-bit SHA-256 hash of the file - not the hash of the whole request body).9293## Upload9495`POST $api_url` as `multipart/form-data`.9697**AUTH required**9899List of form fields:100101- `file`: **REQUIRED** the file to upload102- `caption`: **RECOMMENDED** loose description;103- `expiration`: UNIX timestamp in seconds. Empty string if file should be stored forever. The server isn't required to honor this.104- `size`: File byte size. This is just a value the server can use to reject early if the file size exceeds the server limits.105- `alt`: **RECOMMENDED** strict description text for visibility-impaired users.106- `media_type`: "avatar" or "banner". Informs the server if the file will be used as an avatar or banner. If absent, the server will interpret it as a normal upload, without special treatment.107- `content_type`: mime type such as "image/jpeg". This is just a value the server can use to reject early if the mime type isn't supported.108- `no_transform`: "true" asks server not to transform the file and serve the uploaded file as is, may be rejected.109110Others custom form data fields may be used depending on specific `server` support.111The `server` isn't required to store any metadata sent by `clients`.112113The `filename` embedded in the file may not be honored by the `server`, which could internally store just the SHA-256 hash value as the file name, ignoring extra metadata.114The hash is enough to uniquely identify a file, that's why it will be used on the `download` and `delete` routes.115116The `server` MUST link the user's `pubkey` string as the owner of the file so to later allow them to delete the file.117118`no_transform` can be used to replicate a file to multiple servers for redundancy, clients can use the [server list](#selecting-a-server) to find alternative servers which might contain the same file. When uploading a file and requesting `no_transform` clients should check that the hash matches in the response in order to detect if the file was modified.119120### Response codes121122- `200 OK`: File upload exists, but is successful (Existing hash)123- `201 Created`: File upload successful (New hash)124- `202 Accepted`: File upload is awaiting processing, see [Delayed Processing](#delayed-processing) section125- `413 Payload Too Large`: File size exceeds limit126- `400 Bad Request`: Form data is invalid or not supported.127- `403 Forbidden`: User is not allowed to upload or the uploaded file hash didnt match the hash included in the `Authorization` header `payload` tag.128- `402 Payment Required`: Payment is required by the server, **this flow is undefined**.129130The upload response is a json object as follows:131132```yaml133{134 // "success" if successful or "error" if not135 "status": "success",136 // Free text success, failure or info message137 "message": "Upload successful.",138 // Optional. See "Delayed Processing" section139 "processing_url": "...",140 // This uses the NIP-94 event format but DO NOT need141 // to fill some fields like "id", "pubkey", "created_at" and "sig"142 //143 // This holds the download url ("url"),144 // the ORIGINAL file hash before server transformations ("ox")145 // and, optionally, all file metadata the server wants to make available146 //147 // nip94_event field is absent if unsuccessful upload148 "nip94_event": {149 // Required tags: "url" and "ox"150 "tags": [151 // Can be same from /.well-known/nostr/nip96.json's "download_url" field152 // (or "api_url" field if "download_url" is absent or empty) with appended153 // original file hash.154 //155 // Note we appended .png file extension to the `ox` value156 // (it is optional but extremely recommended to add the extension as it will help nostr clients157 // with detecting the file type by using regular expression)158 //159 // Could also be any url to download the file160 // (using or not using the /.well-known/nostr/nip96.json's "download_url" prefix),161 // for load balancing purposes for example.162 ["url", "https://your-file-server.example/custom-api-path/719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b.png"],163 // SHA-256 hash of the ORIGINAL file, before transformations.164 // The server MUST store it even though it represents the ORIGINAL file because165 // users may try to download/delete the transformed file using this value166 ["ox", "719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b"],167 // Optional. SHA-256 hash of the saved file after any server transformations.168 // The server can but does not need to store this value.169 ["x", "543244319525d9d08dd69cb716a18158a249b7b3b3ec4bbde5435543acb34443"],170 // Optional. Recommended for helping clients to easily know file type before downloading it.171 ["m", "image/png"],172 // Optional. Recommended for helping clients to reserve an adequate UI space to show the file before downloading it.173 ["dim", "800x600"]174 // ... other optional NIP-94 tags175 ],176 "content": ""177 },178 // ... other custom fields (please consider adding them to this NIP or to NIP-94 tags)179}180```181182Note that if the server didn't apply any transformation to the received file, both `nip94_event.tags.*.ox` and `nip94_event.tags.*.x` fields will have the same value. The server MUST link the saved file to the SHA-256 hash of the **original** file before any server transformations (the `nip94_event.tags.*.ox` tag value). The **original** file's SHA-256 hash will be used to identify the saved file when downloading or deleting it.183184`clients` may upload the same file to one or many `servers`.185After successful upload, the `client` may optionally generate and send to any set of nostr `relays` a [NIP-94](94.md) event by including the missing fields.186187Alternatively, instead of using NIP-94, the `client` can share or embed on a nostr note just the above url.188189`clients` may also use the tags from the `nip94_event` to construct an `imeta` tag190191### Delayed Processing192193Sometimes the server may want to place the uploaded file in a processing queue for deferred file processing.194195In that case, the server MUST serve the original file while the processing isn't done, then swap the original file for the processed one when the processing is over. The upload response is the same as usual but some optional metadata like `nip94_event.tags.*.x` and `nip94_event.tags.*.size` won't be available.196197The expected resulting metadata that is known in advance should be returned on the response.198For example, if the file processing would change a file from "jpg" to "webp",199use ".webp" extension on the `nip94_event.tags.*.url` field value and set "image/webp" to the `nip94_event.tags.*.m` field.200If some metadata are unknown before processing ends, omit them from the response.201202The upload response MAY include a `processing_url` field informing a temporary url that may be used by clients to check if203the file processing is done.204205If the processing isn't done, the server should reply at the `processing_url` url with **200 OK** and the following JSON:206207```yaml208{209 // It should be "processing". If "error" it would mean the processing failed.210 "status": "processing",211 "message": "Processing. Please check again later for updated status.",212 "percentage": 15 // Processing percentage. An integer between 0 and 100.213}214```215216When the processing is over, the server replies at the `processing_url` url with **201 Created** status and a regular successful JSON response already mentioned before (now **without** a `processing_url` field), possibly including optional metadata at `nip94_event.tags.*` fields217that weren't available before processing.218219### File compression220221File compression and other transformations like metadata stripping can be applied by the server.222However, for all file actions, such as download and deletion, the **original** file SHA-256 hash is what identifies the file in the url string.223224## Download225226`GET $api_url/<sha256-hash>(.ext)`227228The primary file download url informed at the upload's response field `nip94_event.tags.*.url`229can be that or not (it can be any non-standard url the server wants).230If not, the server still MUST also respond to downloads at the standard url231mentioned on the previous paragraph, to make it possible for a client232to try downloading a file on any NIP-96 compatible server by knowing just the SHA-256 file hash.233234Note that the "\<sha256-hash\>" part is from the **original** file, **not** from the **transformed** file if the uploaded file went through any server transformation.235236Supporting ".ext", meaning "file extension", is required for `servers`. It is optional, although recommended, for `clients` to append it to the path.237When present it may be used by `servers` to know which `Content-Type` header to send (e.g.: "Content-Type": "image/png" for ".png" extension).238The file extension may be absent because the hash is the only needed string to uniquely identify a file.239240Example: `$api_url/719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b.png`241242### Media Transformations243244`servers` may respond to some media transformation query parameters and ignore those they don't support by serving245the original media file without transformations.246247#### Image Transformations248249##### Resizing250251Upon upload, `servers` may create resized image variants, such as thumbnails, respecting the original aspect ratio.252`clients` may use the `w` query parameter to request an image version with the desired pixel width.253`servers` can then serve the variant with the closest width to the parameter value254or an image variant generated on the fly.255256Example: `$api_url/<sha256-hash>.png?w=32`257258## Deletion259260`DELETE $api_url/<sha256-hash>(.ext)`261262**AUTH required**263264Note that the `/<sha256-hash>` part is from the **original** file, **not** from the **transformed** file if the uploaded file went through any server transformation.265266The extension is optional as the file hash is the only needed file identification.267268The `server` should reject deletes from users other than the original uploader with the appropriate http response code (403 Forbidden).269270It should be noted that more than one user may have uploaded the same file (with the same hash). In this case, a delete must not really delete the file but just remove the user's `pubkey` from the file owners list (considering the server keeps just one copy of the same file, because multiple uploads of the same file results271in the same file hash).272273The successful response is a 200 OK one with just basic JSON fields:274275```json276{277 "status": "success",278 "message": "File deleted."279}280```281282## Listing files283284`GET $api_url?page=x&count=y`285286**AUTH required**287288Returns a list of files linked to the authenticated users pubkey.289290Example Response:291292```yaml293{294 "count": 1, // server page size, eg. max(1, min(server_max_page_size, arg_count))295 "total": 1, // total number of files296 "page": 0, // the current page number297 "files": [298 {299 "tags": [300 ["ox", "719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b"],301 ["x", "5d2899290e0e69bcd809949ee516a4a1597205390878f780c098707a7f18e3df"],302 ["size", "123456"],303 ["alt", "a meme that makes you laugh"],304 ["expiration", "1715691139"],305 // ...other metadata306 ],307 "content": "haha funny meme", // caption308 "created_at": 1715691130 // upload timestamp309 }310 ]311}312```313314`files` contains an array of NIP-94 events315316### Query args317318- `page` page number (`offset=page*count`)319- `count` number of items per page320321## Selecting a Server322323Note: HTTP File Storage Server developers may skip this section. This is meant for client developers.324325A File Server Preference event is a kind 10096 replaceable event meant to select one or more servers the user wants326to upload files to. Servers are listed as `server` tags:327328```yaml329{330 "kind": 10096,331 "content": "",332 "tags": [333 ["server", "https://file.server.one"],334 ["server", "https://file.server.two"]335 ],336 // other fields...337}338```