Reading Files
Visibility is set on the app, not on individual files. An app is public or private, and every file in it inherits that. There is no per-file visibility. New apps default to public.
Public apps
Every file gets a stable, permanently valid, auth-free URL on cdn.sonuslab.dev. It comes back as file.url on any StorageFile.
const file = await storage.upload({
name: 'avatar.png',
contentType: 'image/png',
data: bytes,
})
file.url // https://cdn.sonuslab.dev/<app-slug>/<random>-avatar.png
StorageFile is { id, key, name, size, contentType, url?, metadata, createdAt }.
The URL never expires and needs no signing round-trip, so store it directly:
await db.users.update(userId, { avatarUrl: file.url })
What the CDN serves
The URL path is the object key verbatim. Only GET and HEAD are answered (plus OPTIONS preflight); anything else is a 405. Range requests are honoured and return 206, so <video> seeking works.
access-control-allow-origin: *
accept-ranges: bytes
cache-control: public, max-age=3600, s-maxage=3600
etag: "..."
x-content-type-options: nosniff
Inline vs. download
Content-type comes from whoever uploaded the bytes, so the CDN does not replay it verbatim for renderable types. Images (png, jpeg, gif, webp, avif), video (mp4, webm), and audio (mpeg, wav, ogg, flac, m4a, aac) render inline. PDFs keep their content-type but are forced to content-disposition: attachment. Everything else — including text/*, image/svg+xml, and any XML — is rewritten to application/octet-stream and downloaded. Those types can execute script on a domain shared by every tenant.
Private apps
The CDN returns 404 for every object belonging to a private app. file.url is undefined; mint a short-lived presigned GET instead.
storage.getDownloadUrl(id, opts?)
Resolve a readable URL. Presigned GET on private apps, the stable CDN URL on public ones.
const url = await storage.getDownloadUrl(file.id, { expiresIn: 600 })
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | File id. |
expiresIn | number | no | Lifetime in seconds, 1–604800 (7 days). Default 300. Ignored on public apps — the CDN URL never expires. |
storage.download(id)
Fetch the stored bytes plus the file record in one call.
const { data, file } = await storage.download(file.id)
// data : Uint8Array — ciphertext if the file was uploaded encrypted
// file : StorageFile
Over REST
Both methods are thin wrappers over two API-key endpoints. expiresAt is only present when the url actually expires.
GET /files/:id/url?expiresIn=600
Authorization: Bearer sl_...
200 { "url": "https://...", "expiresAt": "2026-08-05T15:10:00.000Z" }
GET /files/:id/download
Authorization: Bearer sl_...
302 Location: https://...
Encrypted files
<img> or play in an <audio> — the browser sees an opaque blob. Fetch and decrypt client-side instead.import { decryptDownload } from 'sonuslab-storage/client'
const url = await storage.getDownloadUrl(fileId)
const plaintext = await decryptDownload({ url, key })
// Uint8Array — the original bytes. Only now is it renderable.
Need the sealed filename and content-type as well? Use decryptContainer — see Encryption.
What a public URL actually guarantees
max-age=3600), on top of a 300-second visibility cache in the CDN worker. Anything already fetched stays readable through that window. If you need the file gone now, delete it.The public key also contains the original filename verbatim (sanitised to [A-Za-z0-9._-]), so the filename is visible to anyone who sees the URL. The encryption path avoids this by storing under an opaque random name.
