---
title: "A package release needs a trust chain."
description: "Our release chain broke twice in its first 48 hours: an unnotarized DMG, then an overwritten updater tarball that emptied latest.json. What each break taught us about publish identity, notarization, and update manifests."
publishDate: 2026-06-07T00:00:00.000Z
author: Sarvesh Chidambaram
tags: ["npm", "release", "trusted-publishing", "studio", "homebrew"]
canonical: https://memoire.cv/blog/public-package-trust-chain
---
An agent tool touches real project files, so the install path has to be boring. Nobody should think about provenance while double-clicking a DMG. Getting to boring took a chain of verified links, and we found the weak ones the empirical way: they broke, in production, twice in the first 48 hours of having releases at all.

## Break one: signed is not trusted

The v0.18.0 DMGs went out signed with a valid Developer ID. macOS rejected them anyway. spctl reported "Unnotarized Developer ID", and the fix landed the next day (commit 9dcf306, 2026-05-10).

I had quietly assumed that signing implied trust. It does not. Signing answers "who built this". Notarization is the separate Apple-side check that Gatekeeper actually asks about, and stapling attaches the proof to the artifact so the check works offline. Our release workflow now runs an explicit `xcrun notarytool submit --wait` followed by `stapler staple`, and a release without that step is a release that fails at the user's first double-click no matter how green CI is.

## Break two: the manifest with a hole in it

The same day, the v1.0.0 release died at "missing updater signature(s)" (commit ccdded8, 2026-05-10).

The cause was almost embarrassingly mechanical. Tauri emitted the update bundle for both architectures under the identical filename, "Memoire Studio.app.tar.gz". The release workflow moved one, then moved the other onto it. The second `mv` silently destroyed the first, and the step that generates latest.json found a single tarball where it needed two.

This matters because latest.json is the auto-updater's entire view of reality: which version is current, where each arch's bundle lives, and the signature for each. Lose a tarball and the chain has a hole at exactly the point where every installed copy of the app checks it. The fix was per-arch renames so both bundles survive and the manifest lists both. Both this fix and the notarization fix live in release.yml now, which is the correct home: a trust chain encoded in a workflow file instead of in someone's memory.

Seventeen days later the signing workflow needed repairing again, and v1.0.3 (commit 80ae5a2, 2026-05-27) exists mostly as that fix. Release engineering is not a phase you complete. It is a subsystem that regresses.

## Upstream of the artifact: publish identity

The npm side of the chain starts earlier, at the question of who is allowed to publish. The answer should never be "whoever holds a long-lived automation token on a laptop". The publish path is npm trusted publishing:

- Provider: GitHub Actions
- Owner: `sarveshsea`
- Repository: `memi`
- Workflow: `publish.yml`
- Environment: blank unless the workflow uses one

npm trusts a specific workflow identity. There is no token to leak from a shell history, a secret manager, or a half-forgotten CI variable.

The engine's history shows this instinct predates Studio. Release trust gates were hardened as their own security patch for v0.14.4 (commit d81f4716, 2026-04-30), publishing was gated on npm propagation lag (commit f9fd3843, 2026-04-25), and the public conversion surfaces got verified inside release CI (commit f219936d). The chain was being built link by link weeks before there was a desktop app to sign.

## The human links expire too

Every cryptographic link in the chain hangs off a credential a human manages. docs/CREDENTIALS.md maps all nine secrets to the release step each one breaks, with a rotation calendar attached. It records, among other things, that changing the Apple ID password silently breaks notarization, which is the kind of dependency you only discover at 11pm on a release night.

A trust chain that is not written down is a trust chain with a bus factor of one.

## The last link is a working product

Verification closes the chain. The package gate is small and repeatable: `npm run typecheck`, `npm test`, `npm run check:release`, `npm run growth:status -- --json`, `npm pack --dry-run --json`, then a temp install proving `memi --version`, `memi status --json`, the MCP metadata, and agent kit install planning.

The app gate is heavier: macOS build, signing, notarization, updater assets for both arches, Homebrew checksums, runtime status, and live Codex plus Claude Code sessions from the installed build. And the chain extends further than I expected: the day these trust posts shipped, the launch page itself got its e2e gates restored (web commit 64eb587, 2026-06-06). If the marketing page can drift, it is a link too.

## The operating rule

Do not say "download it" until npm `latest`, the GitHub release, the Homebrew cask, the website button, and the app's own runtime all point at the same release. Two of those links failed us inside one weekend, and a third repair became an entire version number. The chain holds now because every link got broken once, fixed in the workflow, and left with a check standing guard.