The gate
One command for everyday work, seven gates before a release, and what each one catches.
Everyday: npm run check
cd extension && npm run checkFour things behind one exit code:
tsc --noEmit the source
tsc --noEmit -p tsconfig.tests.json the tests, which are a separate project
eslint src
vitest run src/**/*.test.ts
vitest run --config vitest.suites.config.ts tests/{auth,failure,edge,qa}/**/*.test.tsThe tests are typechecked separately because they are a separate TypeScript project, and the source config does not reach them. Without their own pass, a test file can construct an error class with arguments it does not take and nothing objects, in a file whose whole job is asserting what that class says.
The second vitest config exists for a related reason: vitest.config.ts covers src/** only, so a suite added under tests/ would be run by nothing, and a run reporting hundreds of passes would be silent about every test in that directory.
A test not named in a config is a file, not a test. A new suite directory needs adding to vitest.suites.config.ts or it will never run while the gate stays green.
Before a release: seven gates
./scripts/release-gate.shEach gate exists because getting the thing wrong produces a failure that looks like a protocol bug rather than a build mistake.
Toolchain pin
nargo 1.0.0-beta.11 and bb 0.87.0, read from the binaries.
bb is the binding constraint, not nargo: it determines the proof and verification-key byte layout the on-chain verifier hardcodes.
Verification keys reproduce from circuit source
All six keys are regenerated with bb write_vk and hashed, then compared against the pinned hashes and against the vendored keys the extension ships.
Reproduce, then hash. A size check is not enough: a key from a different revision of the same circuit is the same 1,760 bytes. The measured sizes are 1,760 for a keccak transcript and 1,764 for poseidon2, so size catches a wrong transcript only by accident and the hash is what actually pins it.
The gate also proves a register circuit and asserts the proof is 14,592 bytes. An accidental zero-knowledge flag produces 16,224, which the key hash cannot catch, so both assertions are needed.
Deployment addresses resolve on chain
Every contract id the deployment record declares, derived from the record rather than a hardcoded list of three.
Testnet is wiped on resets and confidential identities are per deployment, so a stale address means every user silently re-registers.
The pinned verifier fork carries no Rust changes
The fork exists only to bump the Soroban SDK from 26 to 27. The gate diffs it against upstream main and fails on any .rs difference, because a Rust change would mean running verifier logic upstream has not reviewed.
Public-input slot counts match the circuits
Counted from the pub Field parameters of each circuit's main, against the recorded 6, 15, 24, 24, 24, 19.
The gate is named for what it asserts. Counting is not ordering, and ordering is the sharper risk, since a permutation of two same-typed inputs verifies a different statement. That is caught by the parity test, not here.
The shipping package is the one you think you are shipping
Gates 1 to 5 inspect sources. This one builds the package and reads it.
| Checked | Fails on |
|---|---|
| Version | a manifest version that disagrees with package.json, is not a version Chrome accepts, or is not newer than the last release tag |
| Source maps | any .map file or sourceMappingURL reference |
| Endpoints | any loopback address, or any plaintext http:// endpoint |
| Content security policy | a missing policy, unsafe-eval, or a remote origin |
| Icons | a missing icons key, a file it names that is absent, or a missing 16, 48 or 128 px size |
| Host permissions | a declared host the shipped code never uses |
| Deployment ids | any recorded contract id absent from the built bundle |
The version check earns its place: Chrome only replaces an installed extension when the published version is newer, so a fix that never reaches an existing install is the worst kind of release, because the release looks successful.
The deployment-id check earns its place too: confidential openings are stored under a key containing the token contract address, so a build pointing at a different deployment silently orphans every existing user's openings.
The browser suite runs against the package gate 6 built
POCKET_EXT_PATH points Playwright at that exact artifact rather than building a second one, which is also the stronger check: the browser tests then run against the thing the other gates inspected.
It is the only gate that exercises the interface. Everything before it is tsc, eslint and two vitest configs, none of which opens a browser, so a shared locator that goes stale against a rebuilt interface turns green all the way to here.
Skipped tests fail the gate
vitest exits successfully with any number of skipped tests, so the gate reads the machine-readable report and fails when anything skipped itself.
This is not pedantry. The parity test, which is the only thing checking public-input ordering, gates on a nargo binary and on circuit sources under an ignored directory. On a machine without the toolchain it reports "24 skipped" and exits zero.
Gate 5 says in its own comment that ordering is caught by that test. So without this rule, a machine without the toolchain would assert a property nothing had checked, and print PASS.
Failures print
A gate that says only FAIL is a gate people learn to skip, so each step prints an excerpt of what went wrong.
The three checks behind npm run check are also run separately inside the gate, rather than through the single command, because one exit code cannot tell a real pass from a run that quietly disabled part of itself.
The vitest excerpt is taken from the tail of the output, because vitest prints the run first and the failures last, and printing the head would show forty green ticks and hide the reason.