# Native packaging verifies published digests

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0241-native-packaging-verifies-published-digests.md

---

# Native packaging verifies published digests

Amends ADR 0233: the source SHA256 of an asset is the digest the package published, not one
computed from the installed file. Invariants I8, I13, I14.

## Context

`createNativePackaging` returned a `sha256` per asset that it computed from the file currently in
`node_modules`. The package published no digest to compare it with, so the value described
whatever was installed: an addon substituted after installation received its own digest, and a
consumer that compared the bytes it copied with that digest compared the file with itself. The
asset also exposed `sourcePath`, so a consumer read the addon a second time to copy it, outside
any check, and a build that called `createNativePackaging` from several places read and hashed
both addons on every call. The only real integrity was the tarball hash in the consumer's
lockfile.

## Decision

1. **The package publishes the digest.** The tracked source layout is `native-layout.json`
   (loader and addon paths, build input only). `build` generates the published
   `native-assets.json` from it at `formatVersion: 2`, with `{ path, size, sha256 }` for every
   Darwin addon present at build time. A release is packed after both addons are in place and
   refuses a manifest without both digests. An architecture without an entry is not published.
2. **The reader refuses any other format.** `formatVersion` is a literal `2`; an older or newer
   manifest throws before any asset is read, like other damaged metadata.
3. **One read, verified, and a refusal that can be read.** Each selected addon is read once, and
   the size and SHA256 of the bytes read are compared with the manifest. A difference is the typed
   refusal `{ state: 'mismatch', code: 'NATIVE_ASSET_DIGEST_MISMATCH', architecture, expected,
   actual }`, beside `unsupported` and `missing`: `expected` is the published `{ size, sha256 }`
   and `actual` the installed file's, so a log tells a truncated copy from a substituted one
   without a second read. The bytes are hashed even when the size already differs; the read has
   been paid, and the hash is what identifies the file that was found. A universal request
   refuses as a whole, at the first differing addon.
4. **The platform is a closed set.** `platform` is `z.enum(['darwin'])`, the platforms the
   package publishes addons for. Another name is a type error and a schema refusal that throws;
   it is not an `unsupported` result, which names only an architecture without an addon. A free
   string promised platforms that do not exist; adding one is a deliberate widening of the enum.
5. **The verified bytes are the result.** A ready asset is `{ outputPath, size, sha256, bytes }`
   (plus `architecture` in the array form); `size` and `sha256` are the published values and
   `bytes` match them. `sourcePath` is not exposed. Embedded delivery gives Bun the same bytes
   through the plugin's `onLoad`, so the compiled executable carries what was checked even if the
   file changes afterwards.
6. **One call per build.** Every call reads and hashes the selected addons; the result (assets
   and plugin) is the value to reuse. There is no hidden cache: a second call is a second check.

## Consequences

A consumer writes `asset.bytes` and records `asset.sha256` in its own archive manifest; it no
longer re-hashes the installed addon. The digest binds the addon to the package Stitchkit built.
It does not authenticate the package: the tarball's integrity in the consumer's lockfile and any
signature policy stay with the application. The shape change is a recorded break of an evolving
entrypoint.

Verification: `packages/core/tests/native-packaging.test.ts` builds the packaging module into a
fixture package with synthetic addons and a published manifest; one changed byte and one extra
byte both refuse with the published and the installed size and SHA256, a platform other than
`darwin` throws (and is a `@ts-expect-error`), an unlisted architecture is missing, a `formatVersion: 1` manifest throws, and
an embedded build after the file was replaced still carries the verified bytes. On packed
packages, `scripts/universal-native-build.ts` refuses a manifest without both digests and proves
the tamper refusal with the real addons; the Darwin consumer lane
(`darwin-artifacts.mjs`) does the same before executing the artifacts.
