@guestcraft/storage
GuestCraft’s object storage adapters: the ObjectStore port of @guestcraft/core/files over an
S3-compatible bucket or a private Vercel Blob store (ADR 0046).
It is the backend integration package of the repository rules:
it imports only core, reads no environment, and server composes it from its configuration.
The file store of db uses the port: it writes an object before the row that names it, and the
worker’s orphan collection deletes objects that no row names.
Exported factories
Section titled “Exported factories”| Factory | Store |
|---|---|
s3ObjectStore({ endpoint, region, bucket, accessKeyId, secretAccessKey, forcePathStyle, fetch? }) |
An S3-compatible bucket: AWS S3, Cloudflare R2, MinIO, Supabase Storage’s S3 endpoint |
vercelBlobObjectStore({ token?, storeId?, api? }) |
A private Vercel Blob store, through @vercel/blob |
Both implement put, get (undefined for a missing object), delete (which succeeds for a missing
object) and list, which lists the keys under a prefix (db names objects
<instance>/<workspace>/<key>, and the orphan collection lists only its instance’s prefix) and
pages with an opaque cursor that a caller may store: S3 continues after
the last key listed (start-after of ListObjectsV2), Vercel Blob with its own cursor.
- Requests are signed with AWS Signature Version 4 by aws4fetch,
with the SHA-256 of the body in
x-amz-content-sha256, so the service checks the bytes too. endpointmay carry a path (Supabase’s/storage/v1/s3);forcePathStyleputs the bucket in the path rather than in the host name, as MinIO and Supabase need. Keys are percent-encoded per segment as SigV4 canonicalizes them; a key with a.or..segment, which the URL would resolve to another object, is refused as a bug.- The listing’s XML is read minimally: the
IsTruncatedflag and each object’sKeyandLastModified, with the five predefined XML entities and numeric character references decoded. An answer without them, or with a reference to no Unicode character, is refused. - Each request is sent once, with a 10-second limit: the file store calls object storage inside its
database transaction. A failure is an
InfrastructureError(ADR 0008): HTTP 408, 429 and 5xx and an unreachable service are retryable; other statuses are not; a write whose answer was lost has an unknown outcome. The message names the method and the status only: the answer’s body, which may name the key, is discarded.
The settings of each provider are in the installation guide.
Vercel Blob
Section titled “Vercel Blob”Objects are written with access: "private", addRandomSuffix: false and allowOverwrite: true,
so each has exactly its key as its name and is read only with the store’s credentials. The
store authenticates with the read-write token, or, without one, the SDK uses the deployment’s
Vercel OIDC token for the store of storeId. The SDK is loaded by a store’s first call, so an
instance without Vercel Blob never loads it. Every call, with the SDK’s own retries, ends after 10 seconds. A failure that remains is an
InfrastructureError, retryable for an unavailable or rate-limited service (with its
Retry-After), an unknown server error, a call that timed out, and the plain BlobError that
get throws for any failed answer; not retryable for any other refusal of the SDK.
Not exercised against a real store in CI. The four SDK calls sit behind the small BlobApi
interface, and the unit tests replace it: they check the options sent and the conversion of the
answers and errors, not Vercel’s service. Verify uploads and downloads on a deployment after
enabling it.
pnpm --filter @guestcraft/storage test # unit tests, no Dockerpnpm --filter @guestcraft/storage test:integration # the S3 adapter against MinIO (Docker)The unit tests check the signature of a request against an independent implementation of SigV4,
itself checked against AWS’s published example. The integration tests start MinIO with
@guestcraft/testing/minio and run a round trip, overwrites, missing keys, paging with names that
need escaping, and a refused secret. The file store, the orphan collection and the move command
are tested on PostgreSQL and MinIO in packages/server/test/integration/object-storage.integration.test.ts.