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
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.
Stream encryption
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.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
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()?;
AttachmentEncryptionStream(key).use { stream ->
val nonce = stream.nonce
input.forEachChunk { output.write(stream.update(it)) }
val finalValue = stream.finish()
}
Read and write in bounded chunks on an IO dispatcher. Use a private cache or app-files path; persist transfer state separately for WorkManager restart.
let stream = try AttachmentEncryptionStream(key: key)
let nonce = stream.nonce
for chunk in chunks {
try encryptedHandle.write(contentsOf: stream.update(chunk))
}
let finalValue = try stream.finish()
const stream = new AttachmentEncryptionStream(key);
const nonce = stream.nonce;
for (const chunk of chunks) output.write(stream.update(chunk));
const finalValue = stream.finish();
Transfer chunk ArrayBuffers to a Worker rather than copying them through
multiple application layers.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
Authenticate before exposing plaintext
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.
NIP-98 request plans
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.
- Rust
- Kotlin
- Android
- Swift
- TypeScript
let plan = create_nip98_authorization(
&identity,
"PUT",
url,
Some(body.as_slice()),
now,
)?;
http.execute(plan.method, plan.url, plan.authorization_header, body)?;
val plan = identity.createNip98Authorization("PUT", url, body, now)
httpClient.execute(plan.method, plan.url, plan.authorizationHeader, body)
Execute the plan with the application’s maintained HTTP client. If the server redirects, strip the header and request a fresh plan for the exact new URL.
let plan = try identity.createNip98Authorization(
method: "PUT", url: url, payload: body, createdAt: now
)
Build a URLRequest from the immutable plan. Keep URLSession delegate and
redirect decisions in the application.
const plan = identity.createNip98Authorization("PUT", url, body, now);
await fetch(plan.url, {
method: plan.method,
headers: { Authorization: plan.authorizationHeader },
body,
redirect: "manual",
});
Do not reuse a plan for another method, URL, payload, redirect target, or timestamp.