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 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.

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

On this page