Receipts
Opt into a provenance Receipt for every mutating call - op, provider, key, bytes, etag, timing, and an optional SHA-256 - delivered on the onAction hook. Off by default, and never hashes unless you ask.
A receipt is a provenance record for a single mutating call (upload, delete, copy, move): what landed where, how big it was, how long it took, and - when you ask - a SHA-256 fingerprint of the content you upload. It's built for tool wrappers and agents that need to attest "this exact content was written to this key", without bolting on a separate operation or a middleware layer.
Receipts are off by default. An instance without the option behaves exactly as before: nothing is recorded, and nothing is hashed.
const files = new Files({
adapter: s3({ bucket: "uploads" }),
receipts: true,
hooks: {
onAction(event) {
if (event.receipt) {
provenance.record(event.receipt);
}
},
},
});
await files.upload("reports/q3.pdf", pdf);
// event.receipt -> { op: "upload", provider: "s3",
// key: "reports/q3.pdf", bytes: 48213, etag: "\"a1b2…\"",
// durationMs: 31, ts: 1733788800123 }How it's delivered
Receipts ride on the existing onAction hook as an additive receipt field - there's no new method, callback, or changed return type. The field is present only when:
- the
receiptsoption is on, - the call is a mutating verb (
upload,delete,copy,move), and - the call succeeded.
Reads, signedUploadUrl, failures, bulk array calls (which aggregate many objects into one event), and every instance with receipts off leave event.receipt unset - so an existing onAction consumer that never opted in sees the exact payload it always has.
Every field except sha256 is derived from the work the SDK already does for the hook - the timing, the adapter name, the caller-facing key, and bytes / etag read straight off the UploadResult. Turning receipts on with receipts: true therefore adds no per-call cost.
SHA-256 is opt-in
The fingerprint is the one field with a real per-call cost, so it stays off until you ask for it by name:
const files = new Files({
adapter: s3({ bucket: "uploads" }),
receipts: { sha256: true },
hooks: {
onAction(event) {
if (event.receipt?.sha256) {
attest(event.receipt.key, event.receipt.sha256);
}
},
},
});sha256 is the lowercase-hex SHA-256 of the body as you pass it to upload(). It is computed only when you pass { sha256: true }, and present only on an upload of a buffered body (a string, Uint8Array, ArrayBuffer, typed-array view, or Blob). A streaming upload is handed to the adapter without ever being buffered, so it carries no fingerprint - the SDK won't silently buffer a stream to hash it. delete, copy, and move transfer no content of their own, so they never carry one either.
With receipts: true (or { sha256: false }), the body is never read and no hash is taken.
Plugins that transform the body
The fingerprint is taken before any plugin runs. If you compose the instance with a body-transforming plugin - encryption writes ciphertext, compression writes compressed bytes - the bytes on disk differ from sha256. That's deliberate: it's the hash of the content you handed in, and it matches what a download gives back, since reads reverse the same transforms. So it's the value a round-trip check can verify - and the only stable one, since encryption uses a fresh key per object and would otherwise hash differently on every upload of identical content.
The shape
Prop
Type
Read-only
Lock a Files instance to reads only with the readonly option or files.readonly(), so every write surface fails consistently with a ReadOnly FilesError.
Escape hatch
Drop down to the native, per-adapter client for any feature outside the unified surface — versioning, lifecycle rules, ACLs, object tags, and more.