> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR 0018 entitled downloads and supply chain

# ADR-0018: Entitled downloads + software supply chain (release catalog, signed URLs, checksums, SBOM)

**Status:** Accepted · 2026-07-15

## Context

ADR-0017 built the Account portal through the evaluation → activate path. The
next step in the customer journey (web design §7) is **download**: the customer,
having an entitlement, obtains the installable software. The design requires that
Account be the authoritative entitlement/release-access decision point, that
download URLs be short-lived and issued only after an entitlement check, and that
release artifacts carry checksums, signatures/provenance, and an SBOM.

## Decisions

### 1. A release catalog in the vendor registry, managed by Control admin

`cp_releases` (version PK, channel, artifact name, **sha256**, size, `min_edition`,
`yanked`, notes URL) lives in the shared vendor DB. Releases are registered,
listed, and **yanked** through Bearer-admin Control endpoints
(`/control/v1/admin/releases[...]`), so the same operator credential that manages
accounts manages the catalog. Registration validates the sha256 is hex — the
checksum is a first-class field, not an afterthought.

### 2. Account is the authoritative entitlement decision point; URLs are short-lived + signed

The portal mints a download URL only after three checks pass for the requesting
user: **membership** in the org, the account is **active**, and the account's
edition **satisfies** the release's `min_edition`. The URL carries a stateless,
HMAC-signed token (`b64url(claims).b64url(mac)`, claims = `{version, accountId,
exp}`) — no session is needed at download time, and **expiry (default 15 min) is
the control**. The download endpoint re-checks the signature and expiry, and
re-reads the release so a **yank takes effect immediately even for
already-minted URLs**. Every mint and every served download is a control event.

### 3. The download endpoint never trusts a path

The served file is `join(downloadsDir, basename(release.artifactName))` — the
admin-registered artifact name is reduced to its basename, so a catalog entry can
never point the endpoint outside the downloads directory (path-traversal safe).
A missing artifact is an honest 404 (`artifact_unavailable`), not a 500. The
response sets `x-checksum-sha256` so the client can verify what it received.

### 4. Feature-gated, with a production secret guard

Downloads are off unless `FMD_DOWNLOADS_DIR` is set (endpoints return 503). The
URL-signing secret resolves from `FMD_DOWNLOADS_URL_SECRET_REF`; **production
refuses to enable downloads without an explicit secret** (an ephemeral per-process
secret would break multi-replica mint/verify and not survive restarts).
Development falls back to an ephemeral secret so the feature is runnable locally.

### 5. Supply chain: checksums + SBOM on every release

The release workflow now generates a **CycloneDX 1.5 SBOM** (`scripts/sbom.ts`,
walking the full Bun-isolated install tree incl. scoped/nested packages) and a
`SHA256SUMS` over every attached artifact, and publishes both with the GitHub
Release alongside the web-dist tarball. Combined with the immutable version tag
and the GHCR image, a customer can verify provenance and integrity end to end.

## Consequences

* The evaluation → **download** → activate loop is now runnable: a licensed
  account lists its edition-permitted releases and gets a short-lived, verifiable
  URL.
* The catalog stores metadata + checksum only; artifact bytes live on the
  configured origin (a directory here; object storage in a real deployment — the
  contract is the same).
* Out of scope (future): object-storage/CDN origin with its own signed URLs,
  Sigstore/cosign artifact signatures + attestations, delta/updates channel
  (`updates.findmydata.io`), and per-download rate limiting.

## Verification

`account/downloads.test.ts` (14 tests) + additions to `vendor` admin coverage —
token mint/verify roundtrip, tamper/expiry/forgery rejection, edition-gate matrix,
membership + account-active guards, CSRF on mint, yank-takes-effect-immediately,
a real signed-URL download whose bytes match the published sha256, path-safety
(basename), missing-artifact 404, and the admin register→list→yank lifecycle
(Bearer-gated, hex-sha256 validated). All 263 server tests pass; typecheck + lint
clean; the SBOM generator produces valid CycloneDX for the installed tree.
