← All @molecule/* packages · App templates
@molecule/api-uploads-s3Provider bond · storage · API (Node) · v1.0.3 · Apache-2.0
AWS S3 upload provider for molecule.dev.
npm install @molecule/api-uploads-s3npm · Source on GitHub · Implements @molecule/api-uploads
@molecule/api-uploads-s3 is a provider bond on the API (Node) side: it implements the storage core interface (@molecule/api-uploads) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
Works with: @molecule/api-bond, @molecule/api-i18n, @molecule/api-secrets, @molecule/api-uploads
Secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET, AWS_S3_REGION (optional), AWS_S3_ENDPOINT (optional), AWS_S3_FORCE_PATH_STYLE (optional)
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
AWS S3 upload provider for molecule.dev.
Handles file uploads to AWS S3.
provider
npm install @molecule/api-uploads-s3 @aws-sdk/client-s3 @aws-sdk/lib-storage @molecule/api-bond @molecule/api-i18n @molecule/api-proxy-agent @molecule/api-secrets @molecule/api-uploads uuid
FileS3-uploaded file extending the core UploadedFile with S3-specific abort capabilities.
interface File extends UploadedFile {
/**
* Aborts the in-progress S3 upload.
*/
abort?: () => Promise<void>
}
FileInfoInformation about a file being uploaded.
This interface is provider-agnostic. Implementations using busboy or other multipart parsers should adapt to this interface.
interface FileInfo {
/**
* The original filename - e.g., `some-image.jpg`.
*/
filename: string
/**
* The file's encoding - e.g., `7bit`, `binary`.
*/
encoding: string
/**
* The file's MIME type - e.g., `image/jpeg`.
*/
mimeType: string
}
UploadedFileProperties describing an uploading/uploaded file.
interface UploadedFile {
/**
* The unique file identifier.
*/
id: string
/**
* The file's fieldname from the form.
* Used as the key for the file within `req.files` - e.g., `req.files[fieldname] = file`.
*/
fieldname: string
/**
* The original filename - e.g., `some-image.jpg`.
*/
filename: string
/**
* The file's encoding - e.g., `binary`.
*/
encoding: string
/**
* The file's mimetype - e.g., `image/jpeg`.
*/
mimetype: string
/**
* The file's size in bytes.
*/
size: number
/**
* The source stream (available during upload).
*/
stream?: NodeJS.ReadableStream
/**
* A promise that resolves when the upload completes.
*/
uploadPromise?: Promise<void>
/**
* Whether the upload has completed.
*/
uploaded: boolean
/**
* The URL/location of the uploaded file (if available).
*/
location?: string
}
abortUpload(file)Aborts an in-progress S3 upload. Removes stream listeners and calls the S3 multipart abort.
Rejects the file's uploadPromise with UploadAbortedError — never as a success, and
never routed through the upload() call's onError (parity with the filesystem bond).
function abortUpload(file: File): Promise<void>
file — The File object returned by upload().deleteFile(id)Deletes a file from S3 by its UUID key using DeleteObjectCommand.
function deleteFile(id: string): Promise<void>
id — The UUID file identifier (S3 object key).getFile(id)Downloads a file from S3 by its UUID key using GetObjectCommand.
function getFile(id: string): Promise<NodeJS.ReadableStream | null>
id — The UUID file identifier (S3 object key).Returns: A readable stream of the file contents, or null if the file does not exist.
upload(fieldname, stream, info, onError)Streams a file upload to AWS S3 using the @aws-sdk/lib-storage multipart Upload utility.
Creates a UUID key in the configured S3 bucket and pipes the readable stream into it.
function upload(
fieldname: string,
stream: NodeJS.ReadableStream,
info: FileInfo,
onError: (error: Error) => void,
): File
fieldname — The form field name this file was submitted under.stream — The readable stream of the uploaded file data.info — File metadata (filename, encoding, mimeType) from the multipart parser.onError — Callback invoked if the S3 upload fails or the stream exceeds its size limit.Returns: A File object with the upload's ID, metadata, and a uploadPromise that resolves on completion.
providerS3 upload provider implementing UploadProvider. Stores files in the
AWS S3 bucket specified by AWS_S3_BUCKET.
const provider: UploadProvider
s3ClientLazily-initialized S3 client proxy. Property access is forwarded to the real client on first use.
const s3Client: S3Client
uploadsS3SecretDefinitionsSecret definitions required by the S3 uploads bond.
const uploadsS3SecretDefinitions: SecretDefinition[]
Implements @molecule/api-uploads interface.
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-uploads'
import { provider } from '@molecule/api-uploads-s3'
export function setupUploadsS3(): void {
setProvider(provider)
}
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-secrets ^1.0.1@molecule/api-uploads ^1.0.1AWS_ACCESS_KEY_ID (required) — AWS access key ID
AKIA...AWS_SECRET_ACCESS_KEY (required) — AWS secret access key
AWS_S3_BUCKET (required) — S3 bucket name
my-app-uploadsAWS_S3_REGION (optional) — S3 bucket region — default: us-east-1
us-east-1AWS_S3_ENDPOINT (optional) — S3 endpoint override
http://localhost:9000AWS_S3_FORCE_PATH_STYLE (optional) — S3 path-style addressing
true@aws-sdk/client-s3@aws-sdk/lib-storage@molecule/api-bond@molecule/api-i18n@molecule/api-proxy-agent@molecule/api-secrets@molecule/api-uploadsuuidBond this as the uploads provider (see @molecule/api-uploads for the handler pattern and
the own-every-file / validate rules). Config is all ENV, server-side: AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET, AWS_S3_REGION. For an S3-COMPATIBLE store
(Cloudflare R2, MinIO, DigitalOcean Spaces) set AWS_S3_ENDPOINT (plus
AWS_S3_FORCE_PATH_STYLE=true for MinIO) — no code change.
getFile, scoped to the owner) or hand out a short-lived presigned URL; never make an
object public just to "make it load".text/html, application/xhtml+xml, JavaScript
types, image/svg+xml, and XML are REJECTED at upload() (reported via
onError; same list as the filesystem bond) — SVG uploads must be
rasterized/re-typed client-side. Content-Disposition: attachment
(next bullet) is the second layer for everything that IS accepted.Content-Disposition: attachment — a deliberate
stored-XSS safety default (S3 has no server-side rendering, so this stops a browser from
ever executing an uploaded HTML/SVG file inline). This means a browser hitting the object
directly (a raw S3 URL, or an <img src>/<iframe> pointing at a presigned URL) always
DOWNLOADS it instead of rendering it inline — including otherwise-safe images. If you need
inline rendering, serve the file THROUGH your API's getFile route, which lets you set
your own Content-Disposition/Content-Type after your own validation — do not rely on
this bond's default for that. There is no override for this default in the current
revision.uploadPromise with UploadAbortedError (from
@molecule/api-uploads) — it never resolves as success and never calls the upload()
call's onError. Identical behavior to the @molecule/api-uploads-filesystem bond; see
that core package's AbortHandler remarks for the full cross-provider contract.HTTPS_PROXY is set. The AWS SDK v3
builds its own agent and reads no proxy variable, so on a host whose only
egress path is a proxy every upload used to fail with a bare connection
error. The client now gets a CONNECT-capable agent through its own
requestHandler hook (@molecule/api-proxy-agent, resolved against
AWS_S3_ENDPOINT/AWS_ENDPOINT_URL_S3 when set and the regional endpoint
otherwise). An internal S3-compatible endpoint listed in NO_PROXY keeps
connecting directly, and with no proxy configured nothing is passed at all.
Allowlist *.amazonaws.com (or your store's host) on the proxy.Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip: