Receipts
Opt into a provenance Receipt for every mutating call - op, provider, key, bytes, etag, timing, optional SHA-256 - delivered on the onAction hook.
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. A successful conditional mutation uses the ordinary upload, delete, or copy receipt and adds its redacted condition label; predicate ETags are never copied into the receipt.
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.
Receipt delivery remains observational. If onAction throws, the committed operation still succeeds. If an awaited plugin throws after the native operation has committed, the public call rejects and no success receipt is emitted; reconcile that applied-but-unacknowledged outcome with an exact read.
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
opReceiptOp
The mutating verb that produced this receipt.
ReceiptOpcondition?ConditionalActionType
The native conditional primitive, when this was a conditional mutation.
ConditionalActionTypeproviderstring
The storage provider, from the adapter's `name` (e.g. `"s3"`, `"r2"`).
stringkeystring
Caller-facing key the content landed at — the upload/delete key, or a `copy` / `move` destination. Always the un-prefixed key the caller passed.
stringbytes?number
Stored byte size, when the settled result reports one (`upload`).
numbersha256?string
Lowercase-hex SHA-256 of the body as passed to `upload()`, before any plugin transform — see {@link Receipt}.
stringetag?string
Entity tag the provider returned, when present (`upload`).
stringdurationMsnumber
Wall-clock duration of the public call, in milliseconds.
numbertsnumber
When the call settled, in ms since the epoch.
number