# Integrity

> Verify a build's artifacts, and know what a green result did and did not compare.

Canonical: https://docs.extension.dev/builds/integrity
This page as Markdown: https://docs.extension.dev/builds/integrity.md
Section: Console: build and release
Siblings: [Console overview](https://docs.extension.dev/console/overview.md) · [Usage and limits](https://docs.extension.dev/console/usage.md) · [Builds](https://docs.extension.dev/builds/overview.md) · [Channels](https://docs.extension.dev/builds/channels.md) · [Releases](https://docs.extension.dev/builds/releases.md) · [Store releases](https://docs.extension.dev/builds/store-releases.md) · [Promote gates](https://docs.extension.dev/builds/promote-gates.md)
Map of every page: https://docs.extension.dev/llms.txt · Whole corpus: https://docs.extension.dev/llms-full.txt

---

# Integrity

The integrity verifier checks that a downloaded artifact holds the bytes the
build produced. Reach for it before you ship an archive you did not build
yourself, or as a release gate in CI.

## Quick start

```bash
npx @extension.dev/artifact-integrity \
  --owner <workspace-slug> \
  --repo <project-slug> \
  --sha <short-sha> \
  --browser chrome \
  --require-digest
```

The verifier downloads the archive, runs six checks, writes
`artifact-integrity.json` and exits `0` only when the bytes match a declared
digest.


## How it works

The verifier takes flags, not a URL. It covers Chrome, Firefox and Edge, and
refuses Safari with `Unsupported browser "safari"`.

### Private projects need a token

The registry serves a project only when the project is declared public. A
private project answers `401` to a caller with no token. Pass one with
`--token`, or set `EXTENSION_DEV_TOKEN` in the environment:

```bash
npx @extension.dev/artifact-integrity \
  --owner <workspace-slug> \
  --repo <project-slug> \
  --sha <short-sha> \
  --browser chrome \
  --require-digest \
  --token "$EXTENSION_DEV_TOKEN"
```

The token travels as a bearer header. The verifier refuses to send it over a
URL that is not HTTPS. See [Access tokens](/tools/access-tokens) for minting
one, and [Public project pages](/share/public-pages) for what "declared
public" means.

### The digest is the check that matters

`package-integrity` is the only check that compares bytes to a promise. The
digest comes from `--expected-sha256` first, then from the build's artifact
manifest, then from the build metadata.

When none of the three carries a digest, the run still exits `0`. It records
`package-integrity` at level `info` and reports the SHA-256 it computed. That
proves a downloadable, well-formed archive, not a matched one. Pass
`--require-digest` to turn a missing digest into a failure, or
`--expected-sha256 <hex>` to pin the bytes you expect. A digest you carry
yourself is the only form that does not read the promise from the same place
it reads the bytes.

### The stored report

Each browser of a successful build writes one report. The console lists it as
`<browser>.integrity.json` beside `<browser>-extension.zip`. The same record
returns from the API for a caller with access to the project:

```
GET /api/projects/{id}/builds/{buildId}/integrity/{browser}
```

The platform run is advisory. It records the result and never fails the build,
so a release gate is yours to add.

### As a CI release gate

The exit code is the gate. Run it with a digest requirement, or the gate
passes on an archive nobody compared:

```yaml
- run: |
    npx @extension.dev/artifact-integrity \
      --owner "$WORKSPACE_SLUG" \
      --repo "$PROJECT_SLUG" \
      --sha "$BUILD_SHA" \
      --browser chrome \
      --require-digest
```

The report holds no timestamps, so two runs over the same artifact write
byte-identical JSON. You can commit it and diff it across runs.

## Reference

### Flags

| Flag | Required | What it does |
| --- | --- | --- |
| `--owner` | yes | The workspace slug |
| `--repo` | yes | The project slug |
| `--sha` | yes | The build's short sha |
| `--browser` | yes | `chrome`, `firefox` or `edge` |
| `--require-digest` | no | Fail when no digest is declared |
| `--expected-sha256 <hex>` | no | Pin the digest the archive must match |
| `--token` | no | Bearer token for a private project. `EXTENSION_DEV_TOKEN` does the same |
| `--out` | no | Report path. Default `artifact-integrity.json` |

### Checks, in order

| Check | Level | Passes when |
| --- | --- | --- |
| `download-package` | fail | The archive is reachable and downloads |
| `zip-structure` | fail | The bytes parse as a zip |
| `manifest-present` | fail | `manifest.json` sits at the root and declares `manifest_version` 2 or 3 |
| `download-metadata` | fail | The build's `<browser>.json` metadata is reachable and valid JSON |
| `download-manifest` | warn | The build's `artifact-manifest/<browser>.json` is reachable and valid JSON |
| `package-integrity` | fail | The archive's SHA-256 matches a declared digest |

When `download-manifest` fails, the digest falls back to the build metadata
and the exit code does not change. Firefox packages are `.xpi`, the others
are `.zip`.

### Exit codes

| Run | Exit |
| --- | --- |
| Digest declared and matching | `0` |
| Digest declared and different | `1` |
| No digest, no `--require-digest` | `0`, with `package-integrity` at `info` |
| No digest, `--require-digest` set | `1` |
| Any of the first four checks failing | `1` |
| Only `download-manifest` failing | `0`, with the check at `warn` |

### The API route

| Answer | Meaning |
| --- | --- |
| `200` with the report | The registry holds a report for that browser |
| `200` with `null` | The build produced no report for that browser |
| `502 REGISTRY_READ_REFUSED` | The registry refused the read |
| `502 REGISTRY_READ_FAILED` | The read failed for any other reason |

## When it fails

- **`Missing required flag: --owner`.** The verifier takes `--owner`,
  `--repo`, `--sha` and `--browser`. It has no positional argument and does
  not accept an artifact URL.
- **`Unsupported browser "safari"`.** Chrome, Firefox and Edge only.
- **`download-package` fails on a project that used to verify.** The
  registry answers `401` for a project that is not declared public. Pass
  `--token`, or declare the project public in **Project settings,
  Visibility**.
- **Exit `1` on `package-integrity`.** The bytes do not match the declared
  digest. Download once more to rule out truncation. Treat a second failure
  as an artifact that does not ship.
- **Exit `0` and nothing feels checked.** Read the `package-integrity` level
  in the report. `info` means no digest was declared. Add `--require-digest`.
- **`integrity/{browser}` returns `null`.** The registry holds no report for
  that browser. A read the registry refused answers `502` instead. Check
  which browsers the build produced with
  `GET /api/projects/{id}/builds/{buildId}`. Each browser there carries an
  `integrityUrl` you can read with your token. The verifier reads the
  artifacts, not this route, so it answers either way.

## Next

- [Builds](/builds/overview)
- [Promote gates](/builds/promote-gates)
- [Access tokens](/tools/access-tokens)
