Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 receipts option 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

PropType
opReceiptOp

The mutating verb that produced this receipt.

TypeReceiptOp
condition?ConditionalActionType

The native conditional primitive, when this was a conditional mutation.

TypeConditionalActionType
providerstring

The storage provider, from the adapter's `name` (e.g. `"s3"`, `"r2"`).

Typestring
keystring

Caller-facing key the content landed at — the upload/delete key, or a `copy` / `move` destination. Always the un-prefixed key the caller passed.

Typestring
bytes?number

Stored byte size, when the settled result reports one (`upload`).

Typenumber
sha256?string

Lowercase-hex SHA-256 of the body as passed to `upload()`, before any plugin transform — see {@link Receipt}.

Typestring
etag?string

Entity tag the provider returned, when present (`upload`).

Typestring
durationMsnumber

Wall-clock duration of the public call, in milliseconds.

Typenumber
tsnumber

When the call settled, in ms since the epoch.

Typenumber

Was this page helpful?