Nostr NIPS 96

 

Page content

NIP-96

HTTP File Storage Integration

draft optional

Introduction

This NIP defines a REST API for HTTP file storage servers intended to be used in conjunction with the nostr network. The API will enable nostr users to upload files and later reference them by url on nostr notes.

The spec DOES NOT use regular nostr events through websockets for storing, requesting nor retrieving data because, for simplicity, the server will not have to learn anything about nostr relays.

Server Adaptation

File 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:

{
  // Required
  // File upload and deletion are served from this url
  // Also downloads if "download_url" field is absent or empty string
  "api_url": "https://your-file-server.example/custom-api-path",
  // Optional
  // If absent, downloads are served from the api_url
  "download_url": "https://a-cdn.example/a-path",
  // Optional
  // Note: This field is not meant to be set by HTTP Servers.
  // Use this if you are a nostr relay using your /.well-known/nostr/nip96.json
  // just to redirect to someone else's http file storage server's /.well-known/nostr/nip96.json
  // In this case, "api_url" field must be an empty string
  "delegated_to_url": "https://your-file-server.example",
  // Optional
  "supported_nips": [60],
  // Optional
  "tos_url": "https://your-file-server.example/terms-of-service",
  // Optional
  "content_types": ["image/jpeg", "video/webm", "audio/*"],
  // Optional
  "plans": {
    // "free" is the only standardized plan key and
    // clients may use its presence to learn if server offers free storage
    "free": {
      "name": "Free Tier",
      // Default is true
      // All plans MUST support NIP-98 uploads
      // but some plans may also allow uploads without it
      "is_nip98_required": true,
      "url": "https://...", // plan's landing page if there is one
      "max_byte_size": 10485760,
      // Range in days / 0 for no expiration
      // [7, 0] means it may vary from 7 days to unlimited persistence,
      // [0, 0] means it has no expiration
      // early expiration may be due to low traffic or any other factor
      "file_expiration": [14, 90],
      "media_transformations": {
        "image": [
          'resizing'
        ]
      }
    }
  }
}

Relay Hints

Note: This section is not meant to be used by HTTP Servers.

A nostr relay MAY redirect to someone else’s HTTP file storage server by adding a /.well-known/nostr/nip96.json with “delegated_to_url” field pointing to the url where the server hosts its own /.well-known/nostr/nip96.json. In this case, the “api_url” field must be an empty string and all other fields must be absent.

If the nostr relay is also an HTTP file storage server, it must use the “api_url” field instead.

List of Supporting File Storage Servers

NameDomain
nostrcheck.mehttps://nostrcheck.me
nostragehttps://nostrage.com
sovehttps://sove.rent
nostr.buildhttps://nostr.build
sovbithttps://files.sovbit.host
void.cathttps://void.cat

Upload

A 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.

Clients must add an NIP-98 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). If using an html form, use an Authorization form data field instead.

These following optional form data fields MAY be used by servers and SHOULD be sent by clients:

  • expiration: string of the UNIX timestamp in seconds. Empty string if file should be stored forever. The server isn’t required to honor this;
  • 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;
  • alt: (recommended) strict description text for visibility-impaired users;
  • caption: loose description;
  • 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;
  • 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.

Others custom form data fields may be used depending on specific server support. The server isn’t required to store any metadata sent by clients.

Note 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.

The 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. The hash is enough to uniquely identify a file, that’s why it will be used on the “download” and “delete” routes.

The 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. Note 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. It 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).

The 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.

The server MUST reject with 413 Payload Too Large if file size exceeds limits.

The server MUST reject with 400 Bad Request status if some fields are invalid.

The 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.

The 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.).

On successful uploads the server MUST reply with 201 Created HTTP status code or 202 Accepted if a processing_url field is added to the response so that the client can follow the processing status (see Delayed Processing section).

The upload response is a json object as follows:

{
  // "success" if successful or "error" if not
  status: "success",
  // Free text success, failure or info message
  message: "Upload successful.",
  // Optional. See "Delayed Processing" section
  processing_url: "...",
  // This uses the NIP-94 event format but DO NOT need
  // to fill some fields like "id", "pubkey", "created_at" and "sig"
  //
  // This holds the download url ("url"),
  // the ORIGINAL file hash before server transformations ("ox")
  // and, optionally, all file metadata the server wants to make available
  //
  // nip94_event field is absent if unsuccessful upload
  nip94_event: {
    // Required tags: "url" and "ox"
    tags: [
      // Can be same from /.well-known/nostr/nip96.json's "download_url" field
      // (or "api_url" field if "download_url" is absent or empty) with appended
      // original file hash.
      //
      // Note we appended .png file extension to the `ox` value
      // (it is optional but extremely recommended to add the extension as it will help nostr clients
      // with detecting the file type by using regular expression)
      //
      // Could also be any url to download the file
      // (using or not using the /.well-known/nostr/nip96.json's "download_url" prefix),
      // for load balancing purposes for example.
      ["url", "https://your-file-server.example/custom-api-path/719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b.png"],
      // SHA-256 hash of the ORIGINAL file, before transformations.
      // The server MUST store it even though it represents the ORIGINAL file because
      // users may try to download/delete the transformed file using this value
      ["ox", "719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b"],
      // Optional. SHA-256 hash of the saved file after any server transformations.
      // The server can but does not need to store this value.
      ["x", "543244319525d9d08dd69cb716a18158a249b7b3b3ec4bbde5435543acb34443"],
      // Optional. Recommended for helping clients to easily know file type before downloading it.
      ["m", "image/png"]
      // Optional. Recommended for helping clients to reserve an adequate UI space to show the file before downloading it.
      ["dim", "800x600"]
      // ... other optional NIP-94 tags
    ],
    content: ""
  },
  // ... other custom fields (please consider adding them to this NIP or to NIP-94 tags)
}

Note 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.

Clients may upload the same file to one or many servers. After successful upload, the client may optionally generate and send to any set of nostr relays a NIP-94 event by including the missing fields.

Alternatively, instead of using NIP-94, the client can share or embed on a nostr note just the above url.

Delayed Processing

Sometimes the server may want to place the uploaded file in a processing queue for deferred file processing.

In 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.

The expected resulting metadata that is known in advance should be returned on the response. For example, if the file processing would change a file from “jpg” to “webp”, use “.webp” extension on the nip94_event.tags.*.url field value and set “image/webp” to the nip94_event.tags.*.m field. If some metadata are unknown before processing ends, omit them from the response.

The upload response MAY include a processing_url field informing a temporary url that may be used by clients to check if the file processing is done.

If the processing isn’t done, the server should reply at the processing_url url with 200 OK and the following JSON:

{
  // It should be "processing". If "error" it would mean the processing failed.
  status: "processing",
  message: "Processing. Please check again later for updated status.",
  percentage: 15 // Processing percentage. An integer between 0 and 100.
}

When 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 that weren’t available before processing.

File compression

File compression and other transformations like metadata stripping can be applied by the server. However, for all file actions, such as download and deletion, the original file SHA-256 hash is what identifies the file in the url string.

Download

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.

The primary file download url informed at the upload’s response field nip94_event.tags.*.url can be that or not (it can be any non-standard url the server wants). If not, the server still MUST also respond to downloads at the standard url mentioned on the previous paragraph, to make it possible for a client to try downloading a file on any NIP-96 compatible server by knowing just the SHA-256 file hash.

Note 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.

Supporting “.ext”, meaning “file extension”, is required for servers. It is optional, although recommended, for clients to append it to the path. When present it may be used by servers to know which Content-Type header to send (e.g.: “Content-Type”: “image/png” for “.png” extension). The file extension may be absent because the hash is the only needed string to uniquely identify a file.

Example: https://your-file-server.example/custom-api-path/719171db19525d9d08dd69cb716a18158a249b7b3b3ec4bbdec5698dca104b7b.png

Media Transformations

Servers may respond to some media transformation query parameters and ignore those they don’t support by serving the original media file without transformations.

Image Transformations
Resizing

Upon upload, servers may create resized image variants, such as thumbnails, respecting the original aspect ratio. Clients may use the w query parameter to request an image version with the desired pixel width. Servers can then serve the variant with the closest width to the parameter value or an image variant generated on the fly.

Example: https://your-file-server.example/custom-api-path/<sha256-file-hash>.png?w=32

Deletion

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.

Note 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.

The extension is optional as the file hash is the only needed file identification.

Clients should send a DELETE request to the server deletion route in the above format. It must include a NIP-98 Authorization header.

The server should reject deletes from users other than the original uploader. The pubkey encoded on the header value identifies the user.

It 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 in the same file hash).

The successful response is a 200 OK one with just basic JSON fields:

{
  status: "success",
  message: "File deleted."
}

Selecting a Server

Note: HTTP File Storage Server developers may skip this section. This is meant for client developers.

A File Server Preference event is a kind 10096 replaceable event meant to select one or more servers the user wants to upload files to. Servers are listed as server tags:

{
  // ...
  "kind": 10096,
  "content": "",
  "tags": [
    ["server", "https://file.server.one"],
    ["server", "https://file.server.two"]
  ]
}

Source: nostr-protocol/nips/96.md version: 9b39fd5 2024-01-17T16:09:50+08:00