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.

code
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

code
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 string

The 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 | Uint8Arrayrequired
Raw bytes of the file.
filenamestringrequired

Original filename used for display and to derive a safe path segment.

mediaTypestringrequired

MIME type the client claimed. The router validates it against the UploadPolicy.allowedMediaTypes allow-list before calling upload; the adapter may re-check defensively.

sizenumberrequired

Size in bytes. The router enforces the size cap before calling upload.

conversationIdstring

The 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

storagePathstring

Durable, opaque pointer into the storage backend. Persisted on the message part and used for all future resign and remove calls. Stable across reads.

urlstring

Freshly-signed, expiring URL for immediate use (model fetch and attachment preview). Ephemeral — do not store it as a permanent reference.

filenamestring
Echoed back for convenience.
mediaTypestring
Echoed back for convenience.
sizenumber
Echoed back for convenience.

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