# Requirements Traceability Matrix **x402-spendguard `v0.2.1`** — 53 verified requirements · 58 tests · 58 requirement→test checks · **53/53 verified** (58 passed · 0 failed · 0 skipped · 0 missing). Each requirement is verified by the named test(s). Their existence is *enforced* by `test/traceability.test.ts` (the build fails if a named test is missing); their pass/fail status here is from the suite run. **Verified** = every named test exists and passed. A test may verify more than one requirement, so *checks* ≥ *tests*. | Requirement | Description | Threat | Verified by | Status | |-------------|-------------|--------|-------------|--------| | **HALT-01** | When `policy.halt` is true, `evaluate()` returns **deny** for every input, including otherwise-valid payments. | T13 | `halt-denies-valid` | ✅ passed | | **ALLOW-01** | If the allowlist is non-empty and the payment's `payTo` is not present, **deny**. | T1 | `allowlist-blocks-unlisted` | ✅ passed | | **ALLOW-02** | The allowlist is keyed on **(address, chain)**; the address is compared case-insensitively; the chain is not normalized away. | T1 | `allowlist-same-address-wrong-chain-denied` | ✅ passed | | **ALLOW-03** | An **empty** allowlist denies all payments (secure by default — the user opts destinations *in*, rather than opting danger *out*). | T1 | `empty-allowlist-denies-all` | ✅ passed | | **BIND-01** | For scheme `exact`: **deny** unless the signed `value` equals the challenge `amount` exactly (rejects both over- and under-payment; note v1… | T2, T5 | `bind-rejects-overpayment` | ✅ passed | | **BIND-02** | **Deny** unless the signed `to` equals the challenge `payTo` (after canonicalization). | T1 | `bind-rejects-recipient-swap` | ✅ passed | | **BIND-03** | **Deny** unless `validBefore ≤ now + min(server maxTimeoutSeconds, policy maxAuthLifetimeSeconds) + skew`. The challenge's `maxTimeoutSeconds` is… | T6 | `bind-rejects-long-lived-auth` | ✅ passed | | **BIND-04** | **Deny** unless the signed asset (`verifyingContract`) and `chainId` match the challenge's declared asset and network. | T7 | `bind-rejects-asset-mismatch` | ✅ passed | | **BIND-05** | A non-`exact` scheme (e.g. `upto`, `auth-capture`) is **denied** with a stable reason. (`upto` is deferred to v2; see note.) | T2 | `nonexact-scheme-denied` | ✅ passed | | **SCOPE-01** | A payment whose authorization form is not `eip3009-evm` (e.g. Solana/SVM) is **denied at parse** with a stable reason. v1 is EVM-only (D-017). | T2 | `nonevm-form-denied` | ✅ passed | | **CAP-01** | Per-request: if `amount > cap[(asset, chain)]`, **deny**. | T2 | `cap-per-request-boundary` | ✅ passed | | **CAP-02** | Per-domain cumulative: if `domainSpent + amount > perDomainCap`, **deny**. | T3 | `cap-per-domain` | ✅ passed | | **CAP-03** | Global cumulative: if `globalSpent + amount > globalCap`, **deny**. | T4 | `cap-global` | ✅ passed | | **CAP-04** | Amounts of differing `(asset, chain)` are **never** summed into one cap. | T7 | `cap-no-cross-asset-sum` | ✅ passed | | **CAP-05** | A payment in an `(asset, chain)` with **no configured cap** is **denied** (missing cap = deny, fail-closed). | T2, T7 | `cap-asset-unconfigured` | ✅ passed | | **MONEY-01** | All amounts are integer smallest-units (`bigint`); the parser rejects negative, non-integer, `NaN`/`Infinity`, and out-of-decimal-range values; **no… | T2, T7 | `money-rejects-malformed`, `money-precision-exact` | ✅ passed | | **FAIL-01** | Any thrown exception on the enforcement path results in **deny**. | T8 | `throwing-check-denies` | ✅ passed | | **FAIL-02** | A missing or malformed field in the challenge or payment results in **deny** — never a skipped check. | T8 | `malformed-challenge-denies` | ✅ passed | | **PARSE-01** | Malformed input is denied with a **specific parse reason code** (e.g. `parse.amount_negative`, `scheme.unsupported`), never the generic… | T8 | `parse-failure-specific-reason` | ✅ passed | | **FAIL-03** | An audit/log write failure does **not** flip an allow to a deny or a deny to an allow; but a failure to durably record a *spend* before the payment… | T8, T10 | `audit-failure-preserves-decision`, `spend-record-failure-denies` | ✅ passed | | **ACCT-01** | Spend is recorded **write-ahead**, before the payment is released; a crash between record and settlement must never *under-count*. | T10 | `crash-between-record-and-settle-does-not-undercount` | ✅ passed | | **ACCT-02** | **Within a single guard instance/process,** evaluation is serialized (single-writer): two concurrent `authorize()` calls cannot both pass a cap they… | T9 | `concurrent-payments-cannot-both-pass` | ✅ passed | | **ACCT-03** | Spend state is durable across a process restart. | T3, T4 | `state-survives-restart` | ✅ passed | | **CLOCK-01** | A **backward** clock jump must never reset a spend window or un-count spend — time is viewed monotonically (`max(now, lastSeen)`). | T9 | `clock-anomaly-fails-closed` | ✅ passed | | **ACCT-05** | A spend store shared across processes is serialized (cross-process single-writer): two processes cannot both pass a cap they jointly exceed. | T9 | `cross-process-cannot-both-pass` | ✅ passed | | **ACCT-06** | The durable spend ledger is **refused if world-writable** (fail-closed) — its permissions are checked **before** its bytes are trusted, so a… | T15 | `ledger-refuses-world-writable` | ✅ passed | | **ACCT-07** | The CAS is **immune to version-number reuse**: a `compareAndSave` whose `expected` is no longer current is rejected as a **conflict**, even when… | T9 | `cas-rejects-stale-writer-after-cleanup` | ✅ passed | | **ACCT-08** | The `FileSpendStore` refuses a **world-writable ledger directory** on **every trust-taking operation** — the check runs at the top of both `load()`… | T15 | `ledger-refuses-world-writable-dir` | ✅ passed | | **PRIV-01** | The core makes no network calls; a static check fails the build if the core can import a socket-capable module. | T12 | `core-has-no-egress` | ✅ passed | | **PRIV-02** | The decision log never contains the signed authorization or the payment header (both are bearer capabilities). | T11 | `log-never-contains-signature` | ✅ passed | | **PRIV-03** | No telemetry. Absent, not opt-out. | T12 | `no-telemetry-calls` | ✅ passed | | **PRIV-04** | The spend ledger is created **owner-private (`0o600`)** — spend amounts, origins, and the counterparty graph are private payment data (the same… | T12 | `ledger-created-owner-private` | ✅ passed | | **AUDIT-01** | The log is a **hash chain** — each record commits to its `seq`, the prior record's hash, and the entry contents; `verify()` detects a link-breaking… | (design) | `chain-detects-tamper`, `verify-is-anchor-relative` | ✅ passed | | **AUDIT-02** | The integrity primitive is an **injected seam** (`ChainHasher`): default unkeyed SHA-256 (corruption + naive-tamper + export-verify against a pinned… | (design) | `keyed-chain-detects-forgery` | ✅ passed | | **AUDIT-03** | An audit-integrity failure — a failed append **or** a torn head at startup — **surfaces to the operator** (injected `onAuditFailure`) and **never… | (design) | `audit-failure-surfaced-not-swallowed`, `torn-head-fails-loud`, `concurrent-appends-linear-chain` | ✅ passed | | **DOM-01** | The domain a budget is keyed on comes from the **host the client chose to request** (redirect-immune), **never** from a server-controlled field — not… | T14 | `domain-derivation-ignores-redirect` | ✅ passed | | **CONF-01** | The guard refuses to load a world-writable policy file (`policy.json`) — a deterministic startup gate, mechanism not judgment. Scoped to the… | T15 | `rejects-world-writable-policy` | ✅ passed | | **CONF-02** | `parsePolicy` parses untrusted JSON into a *trustworthy* `Policy`: every field is required (no code-side defaults), money/time are re-parsed through… | T15 | `parse-into-trustworthy-policy` | ✅ passed | | **CONF-03** | The guard refuses to load a policy file from a **world-writable directory** — directory-write governs rename/replace of its entries, so a… | T15 | `rejects-world-writable-policy-dir` | ✅ passed | | **PLAT-01** | The POSIX file-permission refusals (CONF-01 policy, ACCT-06 ledger) are **skipped on Windows** (`process.platform === "win32"`), where Node… | (portability) | `perm-gates-skipped-on-windows` | ✅ passed | | **CORE-01** | `evaluate()` is pure: identical `(payment, challenge, policy, state)` yields an identical decision; no I/O in the core. | (design) | `core-is-pure` | ✅ passed | | **DEP-01** | The core has **zero** runtime dependencies. | (supply chain) | `core-zero-deps` | ✅ passed | | **OBS-01** | Every decision carries a stable, machine-readable reason code. | (design) | `every-decision-has-reason` | ✅ passed | | **SNAP-01** | `snapshot()` is **read-only**: it reads state via the store's existing retried `load()` and **never** writes — no `compareAndSave`, no… | (design) | `snapshot-is-read-only` | ✅ passed | | **SNAP-02** | On an **unreadable** spend store, `snapshot()` **throws** (`snapshot.state_unreadable`), never a fabricated zeroed snapshot. The **same**… | (design) | `snapshot-unreadable-throws-not-zeros` | ✅ passed | | **SNAP-03** | The snapshot is an **honest view**: every configured cap appears (even at 0 spent), a denomination with spend but no configured cap is **shown**… | (design) | `snapshot-honest-view` | ✅ passed | | **PKG-01** | The public API surface is a **frozen, explicit allowlist**: the barrel's exports must equal a declared set, so a change to what is published fails… | (userspace contract) | `public-surface-is-frozen` | ✅ passed | | **PKG-02** | The package publishes a **single entry**: `exports` exposes only `.` (plus `./package.json`), `main`/`types` resolve into `dist`, `sideEffects` is… | (userspace contract) | `package-manifest-is-single-entry` | ✅ passed | | **PKG-03** | The build emits the artifact from `src` **only** (never `test`), rooted so output is `dist/index.js` — not `dist/src/…` — with declarations on and… | (supply chain) | `build-emits-from-src-only` | ✅ passed | | **PKG-04** | Deep imports are blocked through the **type** channel as well as the runtime channel: the `exports` map exposes no internal subpath and no wildcard,… | (userspace contract) | `exports-map-blocks-deep-imports` | ✅ passed | | **PKG-05** | The published tarball ships **only** `dist` (compiled JS + declarations) plus the manifest/README/LICENSE — **no `.ts` source, no `.map`, no… | (supply chain) | `publish-ships-only-dist` | ✅ passed | | **INJ-01** | The clock and the spend-state store are **injected dependencies** at every layer that uses them. No module outside the composition root reads a wall… | (enables CLOCK-01, ACCT-01/02/03) | `no-ambient-clock-or-store` | ✅ passed | | **POL-01** | The enforcement path contains **no security-deciding literal** — every threshold, list, and tolerance is read from `Policy`. Shipped defaults (clock… | (mechanism-not-policy) | `no-deciding-literals-in-core` | ✅ passed |