docs/BEST-PRACTICES.md declared eight criteria unmet for want of static and dynamic analysis, and the letter to an auditor points at that document. This closes them.
scripts/check-static.py is six rules aimed at what this engine can actually have. The one worth having alone is a WebCrypto Promise used as a value: if (SUBTLE.verify(...)) without await is truthy for a forged signature as readily as for a good one. The others are Math.random anywhere in src/, eval or new Function, loose equality on a secret-shaped name, an empty catch outside browser storage, and JSON.parse returned with no shape check.
The first version flagged nine things and seven were its own fault, which is how a rule teaches people to ignore it:
src/e2e.js:123: [await] importKey() without await <- return SUBTLE.importKey(...)
src/e2e-client.js:503: [catch] empty catch block <- around localStorage.setItem
Six were return SUBTLE.importKey(...) from an async function, which is correct and idiomatic. Two were empty catches around localStorage.setItem, which throws on a quota or in a private window and should be swallowed. Both rules are narrowed, and both narrowings are explained where someone will read them.
The ninth was real.openBackup returned JSON.parse of the decrypted plaintext with no shape check, so a backup whose payload was null or a bare string came back to a caller that reads fields off it. AES-GCM has authenticated those bytes under a passphrase-derived key, so it is a smaller hazard than the payload parser test/fuzz.mjs found, and it is the same bug. It throws backupPayloadNotAnObject now.
Every rule was checked by planting the bug it looks for:
src/e2e.js:46: [random] Math.random is not a CSPRNG
src/e2e.js:47: [await] verify() used as a value without await
src/e2e.js:48: [equality] loose equality near 'tag'
src/e2e.js:49: [catch] empty catch block
Then removing the plants and confirming the tree is clean.
test/fuzz.mjs joins the same analysis job, because a check nobody runs stops finding things. The CI comment states the measured runtime, 45 seconds, rather than the 11 I guessed first.
Gate. Static analysis clean across 10 files, 8,001 fuzz cases with 0 problems, conformance 304 passed with 2 skipped, client 289, ratchet 35, vault 26, all 0 failures. Secret sweep clean across 91 files, doc-references resolves 153 sections, and check-exports-documented passes 106 exports.
`docs/BEST-PRACTICES.md` declared eight criteria unmet for want of static and dynamic analysis, and the letter to an auditor points at that document. This closes them.
`scripts/check-static.py` is six rules aimed at what this engine can actually have. The one worth having alone is a WebCrypto Promise used as a value: `if (SUBTLE.verify(...))` without `await` is truthy for a forged signature as readily as for a good one. The others are `Math.random` anywhere in `src/`, `eval` or `new Function`, loose equality on a secret-shaped name, an empty catch outside browser storage, and `JSON.parse` returned with no shape check.
**The first version flagged nine things and seven were its own fault**, which is how a rule teaches people to ignore it:
```
src/e2e.js:123: [await] importKey() without await <- return SUBTLE.importKey(...)
src/e2e-client.js:503: [catch] empty catch block <- around localStorage.setItem
```
Six were `return SUBTLE.importKey(...)` from an async function, which is correct and idiomatic. Two were empty catches around `localStorage.setItem`, which throws on a quota or in a private window and should be swallowed. Both rules are narrowed, and both narrowings are explained where someone will read them.
**The ninth was real.** `openBackup` returned `JSON.parse` of the decrypted plaintext with no shape check, so a backup whose payload was `null` or a bare string came back to a caller that reads fields off it. AES-GCM has authenticated those bytes under a passphrase-derived key, so it is a smaller hazard than the payload parser `test/fuzz.mjs` found, and it is the same bug. It throws `backupPayloadNotAnObject` now.
**Every rule was checked by planting the bug it looks for:**
```
src/e2e.js:46: [random] Math.random is not a CSPRNG
src/e2e.js:47: [await] verify() used as a value without await
src/e2e.js:48: [equality] loose equality near 'tag'
src/e2e.js:49: [catch] empty catch block
```
Then removing the plants and confirming the tree is clean.
`test/fuzz.mjs` joins the same `analysis` job, because a check nobody runs stops finding things. The CI comment states the measured runtime, 45 seconds, rather than the 11 I guessed first.
**Gate.** Static analysis clean across 10 files, 8,001 fuzz cases with 0 problems, conformance 304 passed with 2 skipped, client 289, ratchet 35, vault 26, all 0 failures. Secret sweep clean across 91 files, `doc-references` resolves 153 sections, and `check-exports-documented` passes 106 exports.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs/BEST-PRACTICES.md declared eight criteria unmet for want of static
and dynamic analysis, and the letter to an auditor points at that
document. This closes them.
scripts/check-static.py is six rules aimed at what this engine can
actually have. The one worth having alone is a WebCrypto Promise used as
a value: `if (SUBTLE.verify(...))` without `await` is truthy for a forged
signature as readily as for a good one. The others are Math.random
anywhere in src/, eval or new Function, loose equality on a
secret-shaped name, an empty catch outside browser storage, and
JSON.parse returned with no shape check.
🔴 The first version flagged nine things and seven were its own fault,
which is how a rule teaches people to ignore it. Six were
`return SUBTLE.importKey(...)` from an async function, which is correct
and idiomatic. Two were empty catches around `localStorage.setItem`,
which throws on a quota or in a private window and should be swallowed.
Both rules are narrowed and both narrowings are explained where someone
will read them.
The ninth was real. `openBackup` returned `JSON.parse` of the decrypted
plaintext with no shape check, so a backup whose payload was `null` or a
bare string came back to a caller that reads fields off it. AES-GCM has
authenticated those bytes under a passphrase-derived key, so it is a
smaller hazard than the payload parser that test/fuzz.mjs found, and it
is the same bug. It now throws `backupPayloadNotAnObject`.
Every rule was checked by planting the bug it looks for and confirming
it fires, then removing it and confirming the tree is clean.
test/fuzz.mjs joins the same `analysis` job. A check nobody runs stops
finding things, and this one found a real defect on the day it was
written. The CI comment states the measured runtime, 45 seconds, rather
than the 11 I guessed first.
Gate: static analysis clean across 10 files, 8,001 fuzz cases with 0
problems, conformance 304 passed with 2 skipped, client 289, ratchet 35,
vault 26, all 0 failures. Secret sweep clean across 91 files and
doc-references resolves all 153 sections.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
docs/BEST-PRACTICES.mddeclared eight criteria unmet for want of static and dynamic analysis, and the letter to an auditor points at that document. This closes them.scripts/check-static.pyis six rules aimed at what this engine can actually have. The one worth having alone is a WebCrypto Promise used as a value:if (SUBTLE.verify(...))withoutawaitis truthy for a forged signature as readily as for a good one. The others areMath.randomanywhere insrc/,evalornew Function, loose equality on a secret-shaped name, an empty catch outside browser storage, andJSON.parsereturned with no shape check.The first version flagged nine things and seven were its own fault, which is how a rule teaches people to ignore it:
Six were
return SUBTLE.importKey(...)from an async function, which is correct and idiomatic. Two were empty catches aroundlocalStorage.setItem, which throws on a quota or in a private window and should be swallowed. Both rules are narrowed, and both narrowings are explained where someone will read them.The ninth was real.
openBackupreturnedJSON.parseof the decrypted plaintext with no shape check, so a backup whose payload wasnullor a bare string came back to a caller that reads fields off it. AES-GCM has authenticated those bytes under a passphrase-derived key, so it is a smaller hazard than the payload parsertest/fuzz.mjsfound, and it is the same bug. It throwsbackupPayloadNotAnObjectnow.Every rule was checked by planting the bug it looks for:
Then removing the plants and confirming the tree is clean.
test/fuzz.mjsjoins the sameanalysisjob, because a check nobody runs stops finding things. The CI comment states the measured runtime, 45 seconds, rather than the 11 I guessed first.Gate. Static analysis clean across 10 files, 8,001 fuzz cases with 0 problems, conformance 304 passed with 2 skipped, client 289, ratchet 35, vault 26, all 0 failures. Secret sweep clean across 91 files,
doc-referencesresolves 153 sections, andcheck-exports-documentedpasses 106 exports.🤖 Generated with Claude Code