StorageAdapter
StorageAdapter is the attachment-storage contract. The package ships a Supabase
implementation (createSupabaseStorage) and a hosted implementation; any S3, R2,
or other backend that satisfies this interface is a valid drop-in.
import type {
StorageAdapter,
StorageAdapterFactory,
UploadInput,
UploadResult,
} from '@mordn/chat-widget/server';The three rules
These three rules are the security model. Every implementation must uphold all three — they are load-bearing, not advisory.
1. Private at rest
Stored objects must not be publicly readable. No public bucket, no permanent public URL. The only way to read an object is through a signed, expiring URL this adapter mints. Making the bucket public reopens a data-leak hole the package otherwise closes.
2. Signed, short-lived reads
upload and resign return URLs that expire. A leaked URL stops working. Expiry
is the adapter's choice but should be short (minutes to a few hours) — long enough
for the model to fetch the file mid-turn and for the user to see the thumbnail.
History rehydration re-signs on demand via resign, so short expiry costs nothing
in UX.
3. User-namespaced, unguessable paths
The adapter is bound to one verified user (like ChatStore). It derives every
storage path from its bound userId plus a random segment — callers never supply
the full path, so an upload cannot be aimed into another user's namespace. Paths
include a random segment so they are unguessable even if a bucket were accidentally
made listable.
Factory
type StorageAdapterFactory = (userId: string) => StorageAdapter;The router calls the factory with the server-verified userId after
authentication — never from anything client-supplied. The userId must come from
the server session; passing a client-controlled value would reintroduce the IDOR
the bound-adapter design prevents.
Properties
userIdreadonly stringThe user this adapter is bound to. Set at construction; the adapter uses it to namespace every path it writes and to validate paths before signing or deleting.
Methods
upload(input: UploadInput) => Promise<UploadResult>Store a file and return a signed URL plus its durable storagePath.
The adapter derives the path from its bound userId plus a random segment and a
sanitised filename — callers cannot inject an absolute or foreign path. It writes
the bytes to private storage and mints a short-lived signed URL. Throws on storage
failure so the router returns a clean 5xx rather than handing the client a
half-uploaded attachment.
resign(storagePath: string) => Promise<string | null>Mint a fresh signed URL for an already-stored object given the storagePath
returned by a prior upload. Called by the router when loading old conversations
so the user always gets a live URL.
The adapter must verify the path belongs to its bound user's namespace before
signing. Returns null if it does not, or if the object is missing. A null
result means render a broken/expired thumbnail — it must never cause the
whole conversation history to be thrown away. Do not throw.
remove(storagePath: string) => Promise<void>Permanently delete a stored object by storagePath. Called when a conversation
is deleted. The adapter must verify the path is in the bound user's namespace
before deleting. Idempotent — no-op (does not throw) if the object is already
gone.
Supporting types
UploadInput
dataArrayBuffer | Uint8ArrayrequiredfilenamestringrequiredOriginal filename used for display and to derive a safe path segment.
mediaTypestringrequiredMIME type the client claimed. The router validates it against the
UploadPolicy.allowedMediaTypes allow-list before calling upload; the adapter
may re-check defensively.
sizenumberrequiredSize in bytes. The router enforces the size cap before calling upload.
conversationIdstringThe conversation this attachment belongs to. Used as a path segment for organisation only — not as an ownership signal (ownership comes from the adapter's bound user). Falls back to an "unfiled" segment when omitted.
UploadResult
storagePathstringDurable, opaque pointer into the storage backend. Persisted on the message part
and used for all future resign and remove calls. Stable across reads.
urlstringFreshly-signed, expiring URL for immediate use (model fetch and attachment preview). Ephemeral — do not store it as a permanent reference.
filenamestringmediaTypestringsizenumberBuilding your own adapter
Implement StorageAdapter and return it from a StorageAdapterFactory. The three
rules above are the security contract — uphold all of them or the attachment
isolation guarantee breaks. See the
Supabase backend guide for a reference implementation
and the attachments guide for the full upload flow.