Skip to main content

Media and HTTP

Softchat validates the bounded imeta union and performs compatibility or streaming attachment transforms. The host owns file descriptors, private staging, HTTP execution, redirects, background work, and final publication.

Attachment metadata

Operation contractmedia.metadata

Parse and preserve bounded Softchat imeta metadata.

Inputs and exact bounds
  • at most 128 fields
  • at most 16 fallback URLs
Output
Validated metadata retaining unknown fields.
Trust and authentication
Metadata does not authenticate downloaded bytes.
Stable errors
invalid_attachment_metadata · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
parse_attachment_metadataAttachmentMetadata

Parse untrusted imeta tag values before using a URL, media type, size, hash, dimensions, duration, blurhash, or encryption fields. Unknown or malformed fields fail under the bounded Softchat profile. Waveform samples stay local and are not emitted in the interoperable tag.

let metadata = parse_attachment_metadata(vec![
"url https://cdn.example/file".into(),
"m image/jpeg".into(),
"dim 1200x800".into(),
])?;

Stream encryption

Operation contractmedia.encrypt

Stream attachment encryption into private staging.

Inputs and exact bounds
  • chunks at most 1 MiB
  • stream at most 16 GiB
Output
Nonce, ciphertext chunks, final tag, and hashes.
Trust and authentication
Output is private staging until finish succeeds.
Stable errors
attachment_too_large, attachment_encryption_failed · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AttachmentEncryptionStream

Create a stream with a 32-byte attachment key, persist its nonce with the encrypted object, feed bounded chunks, and store the final authentication tag and hashes returned by finish.

let stream = AttachmentEncryptionStream::new(key)?;
let nonce = stream.nonce();
for chunk in plaintext_chunks {
encrypted_file.write_all(&stream.update(chunk)?)?;
}
let final_value = stream.finish()?;

Authenticate before exposing plaintext

Operation contractmedia.decrypt

Stream attachment decryption with final authentication.

Inputs and exact bounds
  • chunks at most 1 MiB
  • stream at most 16 GiB
Output
Unauthenticated staged chunks then authenticated hashes.
Trust and authentication
All update output is untrusted and must be deleted unless finish succeeds.
Stable errors
attachment_too_large, attachment_decryption_failed · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
AttachmentDecryptionStream

Decryption update returns staged bytes, not trusted media. Write them only to a private temporary destination. Call finish(authenticationTag) after the last ciphertext chunk. Only a successful finish permits an atomic move or UI exposure. On error or cancellation, delete the staging object.

Never stream unauthenticated plaintext directly to a decoder, gallery, media scanner, shared URL, or browser object URL.

let stream = AttachmentDecryptionStream::new(key, nonce)?;
for chunk in ciphertext_chunks {
staging.write_all(&stream.update(chunk)?)?;
}
stream.finish(authentication_tag)?;
atomic_publish(staging)?;

NIP-98 request plans

Operation contractmedia.nip98

Plan exact-request NIP-98 authorization without executing HTTP.

Inputs and exact bounds
  • absolute normalized HTTP(S) URL from 1 through 8,192 UTF-8 bytes
  • ASCII-letter HTTP method from 1 through 32 bytes
  • optional request payload at most 16 MiB
  • explicit timestamp from 0 through 9,007,199,254,740,991
Output
An immutable authorization header and verified event.
Trust and authentication
Redirects require a new exact-URL plan; headers must not be forwarded.
Stable errors
invalid_http_authorization · handling and retry rules
Ownership, lifecycle, and execution rules
Ownership
Rust owns protocol semantics, Android account SQLite/use-case state, and optional native relay I/O; the host owns account lifecycle, HTTP/media/background effects, files, and UI.
Retry
Retry only host transport or explicitly retryable session work; validation and authentication failures are permanent for the same input.
Lifecycle
Language ownership is automatic; explicitly cancel or erase only at the operation-specific authority boundary.
Concurrency
Serialize mutable session capabilities; immutable values follow the language type system.
Cancellation
The host owns async cancellation; use cancel, shutdown, close, or erase only where exposed.
Exact public symbols
create_nip98_authorizationHttpAuthorizationPlan

createNip98Authorization binds a signed event to one normalized method, exact URL, optional payload hash, and caller timestamp. The result contains the Authorization header and explicitly says that authorization must not be forwarded across a redirect. Methods are 1–32 ASCII letters, URLs are bounded to 8 KiB, and this whole-buffer helper hashes at most 16 MiB of request body. The Android account's media.authorizeUpload(method, url, payloadSha256) accepts an already streamed lowercase hexadecimal SHA-256, so larger uploads need no whole-body allocation. Hash the bytes required by the HTTP service's profile: the released Softrelay /api/v2/files endpoint authenticates the multipart file part, including the nonce and authentication tag for encrypted files. It excludes multipart boundaries and headers. Passing a hash of the full multipart body to that endpoint produces an authentication rejection even when the SDK signature is valid. A separate full-body digest can still protect the host's replay/stream integrity. Generic NIP-98 body hashing remains distinct from this file-service profile.

let plan = create_nip98_authorization(
&identity,
"PUT",
url,
Some(body.as_slice()),
now,
)?;
http.execute(plan.method, plan.url, plan.authorization_header, body)?;

Do not reuse a plan for another method, URL, payload, redirect target, or timestamp.