upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/96.md
diff options
context:
space:
mode:
Diffstat (limited to '96.md')
-rw-r--r--96.md295
1 files changed, 295 insertions, 0 deletions
diff --git a/96.md b/96.md
new file mode 100644
index 0000000..f7d901f
--- /dev/null
+++ b/96.md
@@ -0,0 +1,295 @@
1NIP-96
2======
3
4HTTP File Storage Integration
5-----------------------------
6
7`draft` `optional`
8
9## Introduction
10
11This NIP defines a REST API for HTTP file storage servers intended to be used in conjunction with the nostr network.
12The API will enable nostr users to upload files and later reference them by url on nostr notes.
13
14The spec DOES NOT use regular nostr events through websockets for
15storing, requesting nor retrieving data because, for simplicity, the server
16will not have to learn anything about nostr relays.
17
18## Server Adaptation
19
20File 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`:
21
22```js
23{
24 // Required
25 // File upload and deletion are served from this url
26 // Also downloads if "download_url" field is absent or empty string
27 "api_url": "https://your-file-server.example/custom-api-path",
28 // Optional
29 // If absent, downloads are served from the api_url
30 "download_url": "https://a-cdn.example/a-path",
31 // Optional
32 // Note: This field is not meant to be set by HTTP Servers.
33 // Use this if you are a nostr relay using your /.well-known/nostr/nip96.json
34 // just to redirect to someone else's http file storage server's /.well-known/nostr/nip96.json
35 // In this case, "api_url" field must be an empty string
36 "delegated_to_url": "https://your-file-server.example",
37 // Optional
38 "supported_nips": [60],
39 // Optional
40 "tos_url": "https://your-file-server.example/terms-of-service",
41 // Optional
42 "content_types": ["image/jpeg", "video/webm", "audio/*"],
43 // Optional
44 "plans": {
45 // "free" is the only standardized plan key and
46 // clients may use its presence to learn if server offers free storage
47 "free": {
48 "name": "Free Tier",
49 // Default is true
50 // All plans MUST support NIP-98 uploads
51 // but some plans may also allow uploads without it
52 "is_nip98_required": true,
53 "url": "https://...", // plan's landing page if there is one
54 "max_byte_size": 10485760,
55 // Range in days / 0 for no expiration
56 // [7, 0] means it may vary from 7 days to unlimited persistence,
57 // [0, 0] means it has no expiration
58 // early expiration may be due to low traffic or any other factor
59 "file_expiration": [14, 90],
60 "media_transformations": {
61 "image": [
62 'resizing'
63 ]
64 }
65 }
66 }
67}
68```
69
70### Relay Hints
71
72Note: This section is not meant to be used by HTTP Servers.
73
74A nostr relay MAY redirect to someone else's HTTP file storage server by
75adding a `/.well-known/nostr/nip96.json` with "delegated_to_url" field
76pointing to the url where the server hosts its own
77`/.well-known/nostr/nip96.json`. In this case, the "api_url" field must
78be an empty string and all other fields must be absent.
79
80If the nostr relay is also an HTTP file storage server,
81it must use the "api_url" field instead.
82
83### List of Supporting File Storage Servers
84
85See https://github.com/aljazceru/awesome-nostr#nip-96-file-storage-servers.
86
87## Upload
88
89A file can be uploaded one at a time to `https://your-file-server.example/custom-api-path` (route from `https://your-file-server.example/.well-known/nostr/nip96.json` "api_url" field) as `multipart/form-data` content type using `POST` method with the file object set to the `file` form data field.
90
91`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).
92If using an html form, use an `Authorization` form data field instead.
93
94These following **optional** form data fields MAY be used by `servers` and SHOULD be sent by `clients`:
95- `expiration`: string of the UNIX timestamp in seconds. Empty string if file should be stored forever. The server isn't required to honor this;
96- `size`: string of the file byte size. This is just a value the server can use to reject early if the file size exceeds the server limits;
97- `alt`: (recommended) strict description text for visibility-impaired users;
98- `caption`: loose description;
99- `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;
100- `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.
101
102
103Others custom form data fields may be used depending on specific `server` support.
104The `server` isn't required to store any metadata sent by `clients`.
105
106Note for `clients`: if using an HTML form, it is important for the `file` form field to be the **last** one, or be re-ordered right before sending or be appended as the last field of XHR2's FormData object.
107
108The `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.
109The hash is enough to uniquely identify a file, that's why it will be used on the "download" and "delete" routes.
110
111The `server` MUST link the user's `pubkey` string (which is embedded in the decoded header value) as the owner of the file so to later allow them to delete the file.
112Note that if a file with the same hash of a previously received file (so the same file) is uploaded by another user, the server doesn't need to store the new file.
113It should just add the new user's `pubkey` to the list of the owners of the already stored file with said hash (if it wants to save space by keeping just one copy of the same file, because multiple uploads of the same file results in the same file hash).
114
115The `server` MAY also store the `Authorization` header/field value (decoded or not) for accountability purpose as this proves that the user with the unique pubkey did ask for the upload of the file with a specific hash. However, storing the pubkey is sufficient to establish ownership.
116
117The `server` MUST reject with 413 Payload Too Large if file size exceeds limits.
118
119The `server` MUST reject with 400 Bad Request status if some fields are invalid.
120
121The `server` MUST reply to the upload with 200 OK status if the `payload` tag value contains an already used SHA-256 hash (if file is already owned by the same pubkey) or reject the upload with 403 Forbidden status if it isn't the same of the received file.
122
123The `server` MAY reject the upload with 402 Payment Required status if the user has a pending payment (Payment flow is not strictly required. Server owners decide if the storage is free or not. Monetization schemes may be added later to correlated NIPs.).
124
125On successful uploads the `server` MUST reply with **201 Created** HTTP status code or **202 Accepted** if a `processing_url` field is added
126to the response so that the `client` can follow the processing status (see [Delayed Processing](#delayed-processing) section).
127
128The upload response is a json object as follows:
129
130```js
131{
132 // "success" if successful or "error" if not
133 status: "success",
134 // Free text success, failure or info message
135 message: "Upload successful.",
136 // Optional. See "Delayed Processing" section
137 processing_url: "...",
138 // This uses the NIP-94 event format but DO NOT need
139 // to fill some fields like "id", "pubkey", "created_at" and "sig"
140 //
141 // This holds the download url ("url"),
142 // the ORIGINAL file hash before server transformations ("ox")
143 // and, optionally, all file metadata the server wants to make available
144 //
145 // nip94_event field is absent if unsuccessful upload
146 nip94_event: {
147 // Required tags: "url" and "ox"
148 tags: [
149 // Can be same from /.well-known/nostr/nip96.json's "download_url" field
150 // (or "api_url" field if "download_url" is absent or empty) with appended
151 // original file hash.
152 //
153 // Note we appended .png file extension to the `ox` value
154 // (it is optional but extremely recommended to add the extension as it will help nostr clients
155 // with detecting the file type by using regular expression)
156 //
157 // Could also be any url to download the file
158 // (using or not using the /.well-known/nostr/nip96.json's "download_url" prefix),
159 // for load balancing purposes for example.
160 ["url", "https://your-file-server.example/custom-api-path/719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b.png"],
161 // SHA-256 hash of the ORIGINAL file, before transformations.
162 // The server MUST store it even though it represents the ORIGINAL file because
163 // users may try to download/delete the transformed file using this value
164 ["ox", "719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b"],
165 // Optional. SHA-256 hash of the saved file after any server transformations.
166 // The server can but does not need to store this value.
167 ["x", "543244319525d9d08dd69cb716a18158a249b7b3b3ec4bbde5435543acb34443"],
168 // Optional. Recommended for helping clients to easily know file type before downloading it.
169 ["m", "image/png"]
170 // Optional. Recommended for helping clients to reserve an adequate UI space to show the file before downloading it.
171 ["dim", "800x600"]
172 // ... other optional NIP-94 tags
173 ],
174 content: ""
175 },
176 // ... other custom fields (please consider adding them to this NIP or to NIP-94 tags)
177}
178```
179
180Note 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.
181
182`Clients` may upload the same file to one or many `servers`.
183After 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.
184
185Alternatively, instead of using NIP-94, the `client` can share or embed on a nostr note just the above url.
186
187### Delayed Processing
188
189Sometimes the server may want to place the uploaded file in a processing queue for deferred file processing.
190
191In 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.
192
193The expected resulting metadata that is known in advance should be returned on the response.
194For example, if the file processing would change a file from "jpg" to "webp",
195use ".webp" extension on the `nip94_event.tags.*.url` field value and set "image/webp" to the `nip94_event.tags.*.m` field.
196If some metadata are unknown before processing ends, omit them from the response.
197
198The upload response MAY include a `processing_url` field informing a temporary url that may be used by clients to check if
199the file processing is done.
200
201If the processing isn't done, the server should reply at the `processing_url` url with **200 OK** and the following JSON:
202
203```
204{
205 // It should be "processing". If "error" it would mean the processing failed.
206 status: "processing",
207 message: "Processing. Please check again later for updated status.",
208 percentage: 15 // Processing percentage. An integer between 0 and 100.
209}
210```
211
212When 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.*` fields
213that weren't available before processing.
214
215### File compression
216
217File compression and other transformations like metadata stripping can be applied by the server.
218However, for all file actions, such as download and deletion, the **original** file SHA-256 hash is what identifies the file in the url string.
219
220## Download
221
222`Servers` must make available the route `https://your-file-server.example/custom-api-path/<sha256-file-hash>(.ext)` (route taken from `https://your-file-server.example/.well-known/nostr/nip96.json` "api_url" or "download_url" field) with `GET` method for file download.
223
224The primary file download url informed at the upload's response field `nip94_event.tags.*.url`
225can be that or not (it can be any non-standard url the server wants).
226If not, the server still MUST also respond to downloads at the standard url
227mentioned on the previous paragraph, to make it possible for a client
228to try downloading a file on any NIP-96 compatible server by knowing just the SHA-256 file hash.
229
230Note that the "\<sha256-file-hash\>" part is from the **original** file, **not** from the **transformed** file if the uploaded file went through any server transformation.
231
232Supporting ".ext", meaning "file extension", is required for `servers`. It is optional, although recommended, for `clients` to append it to the path.
233When present it may be used by `servers` to know which `Content-Type` header to send (e.g.: "Content-Type": "image/png" for ".png" extension).
234The file extension may be absent because the hash is the only needed string to uniquely identify a file.
235
236Example: `https://your-file-server.example/custom-api-path/719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b.png`
237
238### Media Transformations
239
240`Servers` may respond to some media transformation query parameters and ignore those they don't support by serving
241the original media file without transformations.
242
243#### Image Transformations
244
245##### Resizing
246
247Upon upload, `servers` may create resized image variants, such as thumbnails, respecting the original aspect ratio.
248`Clients` may use the `w` query parameter to request an image version with the desired pixel width.
249`Servers` can then serve the variant with the closest width to the parameter value
250or an image variant generated on the fly.
251
252Example: `https://your-file-server.example/custom-api-path/<sha256-file-hash>.png?w=32`
253
254## Deletion
255
256`Servers` must make available the route `https://deletion.domain/deletion-path/<sha256-file-hash>(.ext)` (route taken from `https://your-file-server.example/.well-known/nostr/nip96.json` "api_url" field) with `DELETE` method for file deletion.
257
258Note that the "\<sha256-file-hash\>" part is from the **original** file, **not** from the **transformed** file if the uploaded file went through any server transformation.
259
260The extension is optional as the file hash is the only needed file identification.
261
262`Clients` should send a `DELETE` request to the server deletion route in the above format. It must include a NIP-98 `Authorization` header.
263
264The `server` should reject deletes from users other than the original uploader. The `pubkey` encoded on the header value identifies the user.
265
266It 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 results
267in the same file hash).
268
269The successful response is a 200 OK one with just basic JSON fields:
270
271```
272{
273 status: "success",
274 message: "File deleted."
275}
276```
277
278## Selecting a Server
279
280Note: HTTP File Storage Server developers may skip this section. This is meant for client developers.
281
282A File Server Preference event is a kind 10096 replaceable event meant to select one or more servers the user wants
283to upload files to. Servers are listed as `server` tags:
284
285```js
286{
287 // ...
288 "kind": 10096,
289 "content": "",
290 "tags": [
291 ["server", "https://file.server.one"],
292 ["server", "https://file.server.two"]
293 ]
294}
295```