x402 paywall docs and implementation plan#1
Merged
Merged
Conversation
- agent-payment-happy-path.puml - developer-onboarding.puml - payment-error-flows.puml - contract-architecture.puml Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ic architecture - patterns.md: fee configurable (owner-only, default 50bps, cap 1000bps) - architecture.md: EVM chain-agnostic, Arc as first chain, Base post-MVP - tech-spec: deviations marked resolved Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wholesale rewrite addressing 22+ critical findings beyond review.md: Architecture pivot: - Adopt standard x402 v1 wire format (JSON body in 402, base64 X-PAYMENT) - Replace EIP-2612 permit with EIP-3009 transferWithAuthorization - Self-hosted facilitator inline in middleware (~150 LoC TS, no external service) - Custom PaymentSplitter wraps transferWithAuthorization for on-chain non-custodial split - Arc Testnet only for MVP (Arc Mainnet not launched by Circle) - Two-layer replay protection (USDC authorizationState on-chain + middleware NonceStore) Side effects: - Update CLAUDE.md (Base -> Arc Network), architecture.md (Payment Flows, data model, integrations rewritten for x402 + Arc), patterns.md (EIP-3009 instead of permit, dual-layer replay, Pausable, registry semantics) - Rewrite user-spec.md ACs to match new contract signature, x402 wire format, framework adapters (Node http + Fastify), price parsing - Rewrite 4 PlantUML diagrams (happy path, contract architecture, onboarding, error flows) for facilitator model - Add code-research.md with verified Arc Testnet constants and x402 spec refs - Remove test.txt stray artifact, add .fastembed_cache to .gitignore 20 tasks across 7 waves (5 above the soft limit; user notified for splitting decision in Phase 5).
…ory + vault pivot) Round-1 validators consolidated: 7 critical, ~20 major, ~25 minor. User-approved Path 2 architecture (per-developer vault factory) + all fixes. Key architectural change: - Replace shared PaymentSplitter (+ payWithAuthorization wrapper) with PaymentSplitterFactory + PaymentVaultImpl (EIP-1167 minimal proxy via Clones.cloneDeterministic). Each developer registers their own vault. - Middleware calls USDC.transferWithAuthorization directly (no wrapper). - developerId field gone — recipient determined cryptographically by EIP-3009 `to` field bound to the per-dev vault address. Eliminates 3 critical findings: - Cross-developer payment-attribution race attack (signed `to` is unique). - Open-registration griefing (vault owner = msg.sender, immutable). - Non-x402-standard payload field (`developerId`) removed from X-PAYMENT. Additional security + correctness fixes: - OZ 5.x Ownable2Step (two-step owner transfer) - Storage-based ReentrancyGuard (Arc 1153 support unverified) - OpaqueRelayerKey wrapper (non-enumerable, redacted toString/toJSON) - Startup chainId pin + rpcUrl trust check (D14) - X-PAYMENT 4 KB size cap; malformed → HTTP 400 (not 402) - Settlement failure taxonomy: 7 classified reasons - waitForTransactionReceipt 30s timeout - authorization_not_yet_valid error code added - NonceStore: synchronous has+insert, 100k cap, TTL eviction - Fee math: only full withdraw() (no partial), gross-fee is exact - SafeERC20 for non-standard token return values - CAIP-2 canonical network IDs (eip155:5042002) + alias Test plan tightened: - ajv validation of 402 body against vendored x402 v1 schema - EIP-712 tamper tests across chainId, verifyingContract, name, version - Forked-integration test runs in CI by default (mock USDC + factory + vault) - Live Arc Testnet test gated on ARC_TESTNET_E2E=1 (nightly) - Both adapters (Node http + Fastify) covered by integration Task count: 20 → 18 (consolidated Wave 3+4). Project knowledge updates: - project.md: free-tier vs paid-tier vault model clarified - deployment.md: env vars (ARC_RPC_URL, PAYWALL_RELAYER_KEY, PLATFORM_TREASURY_ADDRESS, PAYMENT_SPLITTER_FACTORY_ADDRESS) - patterns.md: factory + vault, EIP-3009, dual-layer replay, off-chain pause - architecture.md: project structure + Tech Stack + Payment Flows updated Diagrams: all 4 redrawn for vault-as-payTo and direct USDC.transferWithAuthorization.
All 5 round-1 critical findings stayed resolved. Round 2 surfaced 6 new critical issues across 5 validators; this commit applies all 6 + 14 key major findings. Critical fixes: - Wave 2 was labelled sequential but parallel; split into Wave 2 (T4 contracts) and Wave 3 (T5 contract tests) so the orchestrator respects the dependency. Renumbered all downstream waves. - Task 16 (now T15) Pre-deploy QA: added Verify-smoke (all suites exit 0 + checklist report committed). Task 17 (now T16) Deploy: added the catalog-default reviewers (code-reviewer, security-auditor, deploy-reviewer) — the prior empty list violated the skills catalog. Task 18 (now T17) Post-deploy: added Verify-smoke. - Fixed function selector for transferWithAuthorization: was 0xef55bec6 (which is receiveWithAuthorization); correct selector is 0xe3ee160e. - D15 added: PaymentVaultImpl constructor calls _disableInitializers() so the impl cannot be hijacked via direct initialize. New test row in PaymentVaultImpl.test.ts. - D16 added: vault has no receive() / fallback() payable — surfaces the user-spec constraint as a Decision and Data Models comment so reviewers reading only the tech-spec see it. New test row. - D17 added: developer / factory are write-once in initialize, no setters exist; ABI test asserts neither setDeveloper(address) nor setFactory(address) selector is exposed. Other significant changes: - D18 added: SecurityLogger interface (10 named events, default no-op) for OWASP A09 coverage; wired through core.ts. - D13 expanded: OpaqueRelayerKey now also redacts via [util.inspect.custom], handles pino/winston serialization, and is applied to REGISTER_KEY too. - Risks expanded: * per-payment economics with Wave-1 gas spike (from external-analysis.md) * owner-key compromise (no timelock; multisig recommendation) * pause race condition (5s cache window, intentional) * setFeeBps / setPlatformTreasury front-running vs vault.withdraw * untrusted RPC (D14 chainId pin covers wrong-chain, not adversarial) * rogue clone deployment (canonical factory address in NETWORKS is the trust root) - Risks: relayer_no_balance detection logic specified (proactive balance read + reactive classifier). - User-Spec Deviations: no-partial-withdraw (D4 rationale) added. - Testing Strategy: CREATE2 cross-component invariant test, register reentrancy test, ABI-shape assertions for no-receive / no-setter, fee-snapshot semantics, adapter unit tests for both Node http + Fastify, factory-state cache TTL tests, D14 chainId pin test, OpaqueRelayerKey util.inspect / pino / winston / structuredClone redaction tests. - Task 3 (USDC spike) reviewers fixed (catalog defaults for code-writing); also estimates real gas cost on Arc Testnet to validate per-payment economics from external-analysis.md. - Task 8 (now T7) description compressed (deferred internals to D5/D14). - Tasks consolidated: T6+T7 (round 2) merged into T6 (pure modules). Total task count 18 → 17 (still over the 15 soft cap, but compression beyond this loses logical task boundaries). - user-spec.md: SafeERC20.balanceOf removed (function doesn't exist in OZ 5.x SafeERC20 — uses plain IERC20.balanceOf). - Acceptance Criteria heading clarified as "technical complement". - Diagrams: cosmetic fix for bytes32(msg.sender) shorthand → explicit bytes32(uint256(uint160(msg.sender))) Solidity cast. Round 2 validator reports (in logs/techspec/) re-deleted ahead of round 3.
Round 3 results:
- tech-spec-validator: PASS (approved, 3 minors)
- completeness-validator: PASS (41/41 covered)
- skeptic: 1 critical
- test-reviewer: 3 major
- security-auditor: 1 critical + 5 major
Iteration 3 fixes:
Skeptic critical — leftover bytes20(developer) in Solution paragraph
(was an earlier-draft remnant; D3 already used the canonical
bytes32(uint256(uint160(...))) form). Replaced the inline formula in
Solution with a back-reference to D3 so the canonical salt formula is
defined exactly once.
Security critical — redactor regex too narrow:
- D13 redefined as defense-in-depth (primary: non-enumerable +
custom serialization hooks; secondary: scrubSecrets helper with
broadened regex set covering 0x{64}, raw {64}, 0x{130} signatures,
and brand-typed values matching the OpaqueRelayerKey symbol).
Security majors:
- Rogue-clone Risks entry rewritten: defense is CREATE2 address
derivation off-chain against canonical factory baked into NETWORKS,
NOT a self-reported vault.factory field. Wording aligned with
reality of the middleware's settle path.
- D17 expanded with 4 defense-in-depth enforcement layers: source
review, ABI test, NatSpec @Custom:security-invariant, and D15's
delegatecall ban.
- D15 expanded with @Custom:security-invariant
(no_selfdestruct no_delegatecall single_initializer) and a static-
analysis lint step in contracts/package.json + CI; SELFDESTRUCT and
DELEGATECALL bans rationalized against EIP-1167 clone-bricking.
- D16 documents carve-outs: SELFDESTRUCT-deposit (de minimis on Arc
since USDC IS native gas), pre-deploy CREATE2 funding (the intended
prepay UX), Arc dual-decimal foot-gun (Wave 1 spike confirms 6dec).
- D18 SecurityLogger payload schema fully typed:
10 named events, each with a fixed payload shape (no free-form
payloads → no PII leak; payerHash/developerEoaHash instead of raw
addresses; no raw signatures; no relayer info). Logger calls wrapped
in try/catch + fire-and-forget to prevent self-DoS.
Test majors:
- Settlement failure → replay-store retention: test row added
asserting nonce entry NOT deleted on failure → same-nonce retry
returns nonce_already_used.
- Cross-adapter replay protection in forked-e2e: test row updated
to pay via Node http then retry SAME X-PAYMENT on Fastify in the
same process (verifies process-singleton NonceStore claim).
- register CLI tests: new test row in scripts/__tests__/register.test.ts
asserts the CLI never prints the developer key (stdout/stderr scrub),
asserts incorrect REGISTER_KEY exits non-zero without input echo.
Remaining minors (non-blocking): documented_deviation cosmetic
([PENDING USER APPROVAL] resolves at status flip), test-reviewer
minors about validBefore boundary and concurrent-cache assertions
(can be addressed at task-decomposition time).
Status: draft -> approved on both user-spec.md and tech-spec.md frontmatter. User-Spec Deviations marker flipped: [PENDING USER APPROVAL] -> [APPROVED 2026-06-16]. Three iterations of validation completed (5 validators per round, 15 reports total). Final state: - tech-spec-validator: PASS (approved) - completeness-validator: PASS (41/41 ACs covered) - skeptic: all blockers/criticals resolved - test-reviewer: all criticals + major test rows added - security-auditor: all criticals resolved; majors either fixed or documented as accepted trade-offs in Risks Next step: run /decompose-tech-spec to generate task files.
…anteen thesis) Captures the validation I previously did only in conversation. Documents: - Per-thesis alignment (9 article theses → our position) - Authors' own admitted caveats (compliance glossed, donor privacy, single-vendor risk, distribution-not-rails framing) and how each lands for our design - The one technical critique that survives all three review surfaces (per-payment vs batched settlement) and our deferred-to-post-MVP plan - Decisions deliberately not taken in response (creator-stack pivot, batched settlement, removing platform fee, Circle Gateway dependency) - Pointers into the spec for each item
Decomposition of approved tech-spec across 8 waves: - Wave 1: T1 (monorepo), T2 (hardhat), T3 (USDC spike + gas measurement) - Wave 2: T4 (factory + vault contracts) - Wave 3: T5 (contract tests) - Wave 4: T6 (middleware pure modules — types, networks, x402 codec, errors, relayer-key, replay-store) - Wave 5: T7 (verify + settle), T8 (core orchestrator + adapters + index) - Wave 6: T9 (middleware unit tests), T10 (forked + arc-testnet e2e), T11 (deploy + register CLI + README) - Audit Wave: T12 (code audit), T13 (security audit), T14 (test audit) - Final Wave: T15 (pre-deploy QA), T16 (deploy + npm publish), T17 (post-deploy verification) T3 flagged a wave-sequencing concern: it modifies networks.ts which T6 creates (Wave 4); task-creator emitted a JSON-artifact fallback at contracts/scripts/arc-testnet-usdc-domain.json for T6 to consume. Flagged for validation pass.
Iteration 2 of task-decomposition validation cycle. 18 critical findings
across 10 validators (4 task-validator batches + 6 reality-checker batches).
Systemic fixes (logged at logs/tasks/iteration-2-systemic-fixes.md):
1. Wave renumbering — strict invariant: depends_on tasks must be in
strictly earlier wave. Renumbered 17 tasks across 13 waves (was 8).
2. Test directory canonicalization — all vitest tests under
packages/middleware/src/__tests__/ (was split src/__tests__ vs
__tests__ root). Single tree.
3. Canonical env var names per deployment.md (ARC_RPC_URL,
PAYWALL_RELAYER_KEY, PLATFORM_TREASURY_ADDRESS,
PAYMENT_SPLITTER_FACTORY_ADDRESS, DEPLOYER_KEY, REGISTER_KEY).
4. Canonical error reason strings — to_mismatch (not recipient_mismatch),
insufficient_amount (not value_too_low), mine_timeout vs rpc_timeout
per settle.ts classifier.
5. Shared resource ownership clarified per D13: WalletClient owned by
settle.ts (only OpaqueRelayerKey extractor); PublicClient + NonceStore
+ factory-state cache owned by core.ts. T7 also owns chainId pin.
6. parseUsdPrice() owner: x402.ts (T6 module), tested in x402.test.ts.
7. withdraw transfer order: developer FIRST, platform SECOND
(matches code-research.md; T5 reentrancy test attacks developer).
8. arc-mainnet placeholder: chainId 0 + enabled:false (NOT eip155:42161
which is Arbitrum One's real ID).
9. npm run test:e2e script added to T1 (root scaffolding).
10. T17 read-factory.mjs: replaced file ref with inline node -e
one-liner (no separate file).
11. T15 forked-e2e command: runs via hardhat test, not npm test.
12. T13/T14 decisions.md race: removed from Context Files (orchestrator
composes feature-level summary after audit wave).
13. networks.ts sentinel comments — T6 owns, T11 deploy script patches
via sed.
Per-task fixes:
- T2/T3 env var conflict resolved
- T3 wave 1→2, drops cross-wave networks.ts patch (JSON-artifact only)
- T4 withdraw order; grep guard refined to exclude NatSpec
- T5 wave 3→4; reentrancy test uses MaliciousDeveloper; mocks under test/mocks
- T6 wave 4→5; phantom test files removed; parseUsdPrice added
- T7 wave 5→6; WalletClient owned here; viem WaitForTransactionReceiptTimeoutError
- T8 wave 5→7; core.ts holds OpaqueRelayerKey opaque, doesn't extract
- T9 wave 6→8; test dir canonicalized; phantom tests removed
- T10 wave 6→9; depends_on adds T9; mocha timeout for cache-TTL wait
- T11 wave 6→8; sed-style networks.ts patch (no TS cross-import); register
CLI tests live in packages/middleware to share vitest config
- T12 wave 7→10; cross-reference language removed
- T13 wave 7→10; D1-D18 coverage matrix explicit; slither hard requirement
- T14 wave 7→10; network normalization test row added; coverage gate owned
- T15 wave 8→11; correct forked-e2e command; PaymentVaultImpl AC count
- T16 wave 8→12; npm publish via package name; --provenance pre-flight;
release tag after publish success
- T17 wave 8→13; inline node -e for read-factory; readiness loop for HTTP
smoke; scratch dir is type:module
15 critical findings + ~20 major remain after iteration-2 fixes.
Mostly cross-task drift: constructor signatures (T4/T11/T16), function
signatures (T7/T8 verifyEip3009Authorization), SecurityLogger ownership
(T9 tests forbidden emissions in settle/verify), parseUsdPrice('0')
behavior (T6 accepts, T9 throws), infrastructure feasibility
(T6 needs vitest not installed by T1, T10 Hardhat in-process has no
HTTP URL, T11 __dirname doesn't exist in ESM).
…ixes
Iteration 3 fixes (15 round-2 criticals + ~20 majors):
Canonical decisions added in logs/tasks/iteration-3-addendum.md:
- §1: factory constructor is 3-arg (usdc, treasury, feeBps:uint16);
vaultImpl deployed inside; initialOwner via Ownable2Step default.
- §2: SecurityLogger emission owner = core.ts ONLY. settle.ts and
verify.ts never import the logger; they return classified results.
- §3: verifyEip3009Authorization is 2-arg (payload, opts);
nonceStore lives inside opts.
- §4: parseUsdPrice('0') throws InvalidPriceError (zero is not a payment).
- §5: payerHash = '0x' + keccak256(from).slice(2, 10) — 10-char total.
- §6: authorization_already_used_onchain classifier uses STRING revert
matching ('authorization is used' substring); no 4-byte selector.
- §7: relayer_no_balance threshold = constant 1_000_000n (1 USDC).
- §8: validBefore safety margin = 5000 ms (both prose and code in ms).
- §9: vitest+ajv+pino+winston installed by T1 (root devDeps).
- §10: T10 spawns hardhat node (not in-process); Mocha function() syntax.
- §11: ESM __dirname replacement via fileURLToPath(import.meta.url).
- §12: OpaqueRelayerKey extract imported from INTERNAL relayer-key.ts,
never from public index.
- §13: viem installed in contracts/ via T2.
- §14: paths.sources single string; mocks under contracts/test/mocks/.
- §15: T5 register reentrancy test replaced with source-pattern check
(initialize has no developer callback).
- §16-18: T13/T14 stale path/workspace fixes.
- §19: T2 reviewer paths repo-relative.
- §20: T16 sentinel attribution to T6/T11, not T13.
Other:
- work/x402-agent-payment/decisions.md created as empty stub so tasks
can reference it as Context File without crashing (post-completion
log appended by feature-execution).
- user-spec.md canonicalized test path src/__tests__/ and workspace
name @universal-paywall/middleware.
16 tasks updated (T12 was already approved in round 2).
10 round-3 validators launched after iteration-3 fixes. 9 stalled at the 600s watchdog before writing their JSON reports (each was on the final 'now writing JSON' step). 1 (template-batch1, T1-T5) completed. Skill iteration budget exhausted (3 rounds of fix+validate done). Findings extracted from batch-1 JSON + transcript fragments of stalled batches: Critical (1): - T5 retains a Fallback option for mock placement that contradicts iteration-3 addendum §14 (paths.sources single string; mocks under contracts/test/mocks/). Major (2): - T3 stdout JSON shape vs artifact JSON shape inconsistency (notes? field) - T8 uses --workspace=packages/middleware (path form) instead of @universal-paywall/middleware (package name form) — same class as T13 round-2 finding. Minor: - T5 loadFixture incorrectly called 'sync wrapper' (it's async) - T5 __dirname workaround scope note missing for CJS contracts/ context - T1 smoke step doesn't pin --passWithNoTests requirement Per skill protocol: present remaining issues to user for resolution. Current task files are ready for /do-task execution with the documented minor caveats.
Per user decision: switch contract toolchain to Foundry (forge + anvil +
slither). Foundry's invariant + fuzz support is materially better for the
security surface here (factory/vault + malicious actor scenarios + fee
math overflow). Tests written in Solidity, mocks in same compile unit
(no parallel TS test runner). OpenZeppelin via forge install submodule.
Tech-spec D9 flipped: Hardhat -> Foundry. Dependencies updated.
iteration-4-addendum.md sets canonical paths, commands, and per-task
adjustments. Tasks updated:
T1: Pin --passWithNoTests requirement (must stay until T6 lands first
vitest test).
T2: MAJOR REWRITE — Foundry setup. foundry.toml, forge install
OpenZeppelin v5.0.2, remappings.txt, slither.config.json. Drops
Hardhat. Adds ABI export hook (copies contracts/out/*.json to
packages/middleware/src/abi/ for T7/T10 viem consumption).
T3: artifact JSON now includes notes?: string[] field; T6 surfaces.
T4: Source paths contracts/src/ (was contracts/contracts/). Mocks
under contracts/test/mocks/. Compile commands forge build (was
hardhat compile).
T5: MAJOR REWRITE — Foundry tests in Solidity (.t.sol). Includes
invariant tests (VaultInvariants.t.sol), fuzz tests (testFuzz_*),
MockMaliciousTreasury for dynamic reentrancy. CREATE2 cross-component
via OZ Clones.predictDeterministicAddress (no off-chain TS compute).
Coverage via forge coverage --report lcov >=95% branch.
T8: --workspace=packages/middleware -> @universal-paywall/middleware
(package-name form) — final round-3 finding.
T10: anvil spawn replaces hardhat node spawn. Vendored ABI from T2's
post-build copy. Programmatic contract deploy via viem in beforeAll.
T11: MAJOR REWRITE — two-step deploy: forge script Solidity for
contract deploy + tsx post-deploy.ts for networks.ts sentinel patch.
No Hardhat anywhere. --force flag on post-deploy idempotency check.
T13: forge test + forge coverage + slither replace hardhat commands.
T14: forge coverage --report lcov; AC requires invariant + fuzz test
presence verification.
T15: Test command set updated to forge test + forge coverage --report
summary.
T16: Deploy via forge script ... --broadcast --verify + tsx post-deploy
+ npm publish (unchanged).
Not touched (no Foundry impact): T6, T7, T9, T12, T17.
Round 3 validator stalls (9 of 10) prevent final verdict; per skill
budget exhausted, user approved 'apply known fixes and go'.
Bootstrap npm workspace root and packages/middleware as ESM-only TypeScript package per tech-spec Task 1. Establishes infra (TypeScript strict, tsup, vitest, ESLint, Prettier, Husky + gitleaks pre-commit) so subsequent waves can land code, tests, and publish without further devDep installs. - Root package.json declares workspaces packages/*, apps/*, contracts; devDeps include vitest@^1.5 (root-level runner for CI per §9 addendum). - packages/middleware: type=module, exports map without require branch, publishConfig.access=public/provenance=true, scripts test/test:e2e use --passWithNoTests (pinned via "//test" sibling comment until T6). - tsconfig: strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes + verbatimModuleSyntax; excludes src/__tests__ per systemic-fix §2. - tsup.config.ts: ESM-only (format ['esm']), dts, target node20, no minify. - .husky/pre-commit runs gitleaks protect --staged --redact then npm run lint (middleware-only scope). - .gitleaks.toml extends default rules. - .gitignore adds .husky/_/ (husky v9 auto-generated shim dir).
Initialize the `contracts/` Foundry workspace per tech-spec D8/D9 and
iteration-4-addendum §1, §2, §4 T2. Tooling only — no Solidity sources,
no tests, no deploy scripts. Sets up the canonical layout so subsequent
contract tasks can `cd contracts && forge build` immediately.
- foundry.toml: solc 0.8.20, optimizer 200 runs, [profile.ci] with
fuzz=1000 / invariant runs=256 depth=64, arc_testnet rpc endpoint
bound to ${ARC_RPC_URL}. Leading comment documents the foundryup
bootstrap and canonical env-var names.
- OpenZeppelin contracts pinned to v5.0.2 via `forge install` as git
submodule under contracts/lib/openzeppelin-contracts. forge-std also
installed (so Task 5 tests inherit forge-std/Test.sol with zero
friction). .gitmodules records both, with branch = v5.0.2 on the OZ
entry for documentation.
- foundry.lock pins the exact submodule SHAs (forge 1.6 generates this
automatically for reproducible builds).
- remappings.txt maps @openzeppelin/contracts/ to lib/openzeppelin-contracts/contracts/.
- slither.config.json filters test/,lib/ and keeps exclude_low=false so
Task 13's audit ignores mocks + OZ library but still surfaces lows in
production sources.
- scripts/export-abi.ts is the post-build ABI export hook: copies forge
artifacts to packages/middleware/src/abi/ for viem consumption in
Tasks 7 and 10. Safe no-op when contracts/out/ is missing (T2 state).
- contracts/package.json declares the TS-only dev surface (viem, tsx,
typescript, @types/node). No hardhat, no @nomicfoundation/*, no chai,
no mocha, no ts-node — the legacy Hardhat stack is explicitly dropped
per iteration-4-addendum §4 T2.
- tsconfig.json is ESM-only, strict, scoped to scripts/**/*.ts.
- .env.example lists the canonical names ARC_RPC_URL, DEPLOYER_KEY,
ARC_VERIFY_API_KEY. Forbidden legacy names ARC_TESTNET_RPC_URL and
ARC_TESTNET_PRIVATE_KEY are absent across the workspace.
Deviation from spec: `forge install --no-commit` flag does not exist in
forge 1.6 (no-commit is now the default; --commit is opt-in). Omitted
the flag; behavior matches what the spec intended.
Smoke: `cd contracts && forge build` exits 0 (Nothing to compile);
`cd contracts && npm run build` exits 0 (forge no-op + export-abi
skip); `npx tsc --noEmit -p tsconfig.json` exits 0.
Apply round-1 findings from code-reviewer-t1, security-auditor-t1, and infrastructure-reviewer-t1: - T1-M1 (code-reviewer): align tsup.config.ts quotes with Prettier singleQuote rule (was double-quoted, would diff on next format run). - SA-T1-02 (security-auditor): add overrides.esbuild ^0.28.1 in root package.json to resolve GHSA-gv7w-rqvm-qjhr (HIGH — esbuild binary integrity check). All consumers (tsup, vitest/vite, tsx) now dedupe to esbuild@0.28.1. - SA-T1-03 (security-auditor): add .npmrc to .gitignore — prevents an accidental commit of project-level npm publish tokens before Task 16. - SA-T1-04 (security-auditor): add prepublishOnly guard in middleware package.json — accidental npm publish during development now fails with an informative error. Task 16 removes the guard. Non-blocking notes: - SA-T1-01 (vitest 1.6.1 CRITICAL via GHSA-5xrq-8626-4rwp): not exploitable with current scripts (vulnerability requires vitest --ui, which no script uses). Task spec pins vitest ^1.5.0; upgrade to ^3.2.6 is a major migration tracked in decisions.md, not addressed in T1. Added sibling "//audit-vitest" comment in scripts to document the ban on vitest --ui until upgrade. - T1-M2 (ajv production-vs-dev placement): informational only; T6 implementer must promote ajv to dependencies if used in runtime src/ paths. Smoke verification re-run: lint, typecheck, build, test, npx vitest all exit 0. esbuild deduplicates to 0.28.1 across the dependency tree.
Capture round 1 + round 2 review JSON reports for Task 1 from code-reviewer-t1, security-auditor-t1, and infrastructure-reviewer-t1, and add Task 1 entry to decisions.md. Round 1 verdicts: approved_with_minors / conditional_pass / APPROVED. Round 2 verdicts: approved / pass / APPROVED stands.
- contracts/.env.example: replace all-zeros DEPLOYER_KEY placeholder with a non-functional sentinel `0x<your-0x-prefixed-64-hex-char-private-key>` (the all-zeros key is a publicly-known monitored address; using it as a copy-paste placeholder risks a developer silently transacting with it). Addresses T2-C1 (code-reviewer-t2) and T2-SEC-01 (security-auditor-t2). - contracts/foundry.toml: document `foundry.lock` in the leading comment block as a forge 1.6+ secondary lock that complements submodule SHA pinning. Addresses T2-C2 (code-reviewer-t2). - .gitmodules: add `branch = v1.16.1` to the forge-std submodule entry for parity with the OZ entry. The cryptographic pin remains the submodule SHA + foundry.lock SHA. Addresses T2-SEC-02 (security-auditor-t2, low) and INF-T2-001 (infrastructure-reviewer-t2, informational). Round 1 verdicts: - code-reviewer-t2: approve_with_minor - security-auditor-t2: PASS (2 low advisories) - infrastructure-reviewer-t2: APPROVED (1 informational)
Add six review reports (3 reviewers × 2 rounds) and append decisions.md entry for Task 2 (Foundry contracts workspace scaffolding). Round 1 verdicts: - code-reviewer-t2: approve_with_minor (2 minor findings) - security-auditor-t2: PASS (2 low advisories) - infrastructure-reviewer-t2: APPROVED (1 informational) Round 2 verdicts (after fixes in d5c5dc7): - code-reviewer-t2: approved (no remaining findings) - security-auditor-t2: PASS / APPROVE (no remaining findings) - infrastructure-reviewer-t2: approved (no remaining findings)
Standalone tsx script + JSON artifact for the Wave 2 spike. Reads name/version/decimals/supportsEip3009 from canonical Arc Testnet USDC (0x36...00), self-checks the transferWithAuthorization selector (0xe3ee160e), probes method existence for transferWithAuthorization + authorizationState, estimates per-payment gas cost in micro-USDC with a documented fallback when the node refuses to estimate a reverting call, and writes arc-testnet-usdc-domain.json for Task 6 to consume. Spike outcome: supportsEip3009=true, decimals=6, gas cost 1290 micro-USDC (exceeds the 500 micro-USDC / 5%-of-0.01 threshold — flagged for post-MVP batched-settlement follow-up per Risks row 4, not blocking). Dual-decimal note emitted for T6 module-load surfacing.
- CF-01: align nativeCurrency.name with empirical chain value ("USDC",
not "USD Coin"); add comment explaining the divergence.
- EH-01: document intent of the two empty catch blocks in readGasPriceWei
(any throw is intentionally treated as "this pricing path unusable";
next branch / notes-flag is the fall-through).
- DOC-01: add Task 3 entry to decisions.md per post-completion checklist:
spike outcome values, literal notes[] array, gas-threshold breach note,
open items for T6 (consume artifact, surface notes at module load).
Artifact regenerated by smoke run — sampleGasCost now 1212 micro-USDC
(varies with live Arc gasPrice; still exceeds threshold, still warning-only).
- SEC-T3-01 / SEC-T3-02: introduce scrubUrls() and errMessage() helpers, apply to every err.message / err.shortMessage push into notes[] and every console.error call site. Prevents an authenticated RPC URL (e.g. Infura-style endpoint with a path-embedded API key) from leaking into stderr or the committed artifact via viem's HttpRequestError. - SEC-T3-INFO-01: add resolveRpcUrl() guard that requires ARC_RPC_URL to start with http:// or https://; rejects empty/bogus values up-front without echoing the offending value (which could be a secret). Stdout / artifact shape unchanged; smoke still exit 0; lint clean.
- T3-M1 (medium): hoist hard-blocker checks (supportsEip3009===false, decimals!==6) above artifact write and stdout emission, so a failed spike no longer leaves stale or wrong-valued JSON on disk for T6 to pick up. Restructure main() to fail fast. - T3-M2 (medium): introduce GAS_COST_UNAVAILABLE sentinel string used when gas pricing is unavailable, so consumers can distinguish "we couldn't measure" from a real "0 micro-USDC". Field stays string-typed. - T3-L1 (low): tighten ambiguous-probe note text in both probeAuthorizationState and probeTransferWithAuthorization — explicitly state "treating as absent to avoid false positive" so reviewers see the conservative classification choice. - T3-L3 (low): expand the sanity-band comment to spell out what the wildlyLow / wildlyHigh predicates actually catch (off-by-12-orders or inverted conversion) so a future reader doesn't have to reverse-engineer the intent. Smoke still exit 0; output shape unchanged (sampleGasCost varies with live Arc gasPrice — 1260 micro-USDC this run; still exceeds threshold). Lint exit 0. gitleaks 0 leaks.
- Vendor x402 v1 JSON Schema at src/__tests__/fixtures/x402-v1.schema.json (covers PaymentRequirements, PaymentPayload, ChallengeBody, XPaymentResponse). - Add ajv-based schema validation to x402.test.ts (build402Body, decodeXPayment round-trip, encodeXPaymentResponse) and errors.test.ts (every reason → ChallengeBody validates). - Add shared helpers under src/__tests__/helpers/ (sign.ts, mock-clients.ts, timers.ts, encode-header.ts, recording-logger.ts) for downstream T10 e2e reuse and tamper-matrix tests. - Wire @vitest/coverage-v8 + vitest.config.ts with v8 provider and 85% line / statement / function thresholds (75% branch). - Expand unit tests for x402.ts (validAfter/validBefore decimal int guard, empty/non-string network), verify.ts (unknown network, sig-recovery throw, default nowMs), settle.ts (balance-read TimeoutError → rpc_timeout, unknown network), relayer-key.ts (forged-brand throws on getRelayerKeySecret). - Coverage: 96.87% lines, 89.15% branches across src/ (errors, replay-store, relayer-key, verify, adapters at 100%). - Add test:coverage npm script; add coverage/ to .gitignore; allowlist vitest.config.ts in ESLint ignorePatterns (build tooling, outside tsconfig). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- contracts/script/Deploy.s.sol: forge script for PaymentSplitterFactory (3-arg canonical constructor, env-driven, USDC_ADDRESS optional override with T3-verified Arc Testnet default, console2.log of both deployed addresses). - contracts/scripts/post-deploy.ts: reads forge broadcast/run-latest.json, extracts factoryAddress + vaultImplAddress (inner CREATE in additionalContracts[0]), patches networks.ts via sentinel-anchored substitution. Idempotent. Refuses to overwrite already-populated arc-testnet without --force (exit 4). - scripts/register.ts: tsx CLI that calls factory.register() via viem. REGISTER_KEY wrapped in OpaqueRelayerKey immediately; getRelayerKeySecret imported from internal relayer-key.js per iter-3 §12. Pre-flight vaults() check makes re-runs print "Already registered" and exit 0. Typed error reasons mirror settle.ts taxonomy. Never echoes the key. - packages/middleware/src/__tests__/register-cli.test.ts: 7 spawn-based tests against a local anvil. Programmatic factory deploy via viem reading the forge artifact. Skips with a clear message if anvil or the artifact is missing. ESM __dirname via fileURLToPath per iter-3 §11. - README.md: developer onboarding (faucet → register → install → run) plus a maintainer-only deploy callout. No Hardhat references. Canonical env-var names only (DEPLOYER_KEY, PLATFORM_TREASURY_ADDRESS, ARC_RPC_URL, REGISTER_KEY, PAYWALL_RELAYER_KEY). Smoke verified: - anvil --chain-id 31337 + forge script ... --broadcast → broadcast file with factory at additionalContracts[0]=PaymentVaultImpl. - npx tsx contracts/scripts/post-deploy.ts --chain-id 31337 → patches networks.ts with only the two sentinel-anchored address literals. - Re-run: zero additional diff. - post-deploy.ts --chain-id 5042002 without --force on already-patched networks.ts → exit 4 with actionable message. - Register CLI happy path, idempotent re-register, malformed/missing key (exit 2), arc-mainnet disabled (exit 3), --help (exit 0). All 244 middleware tests pass (7 new). forge build, tsc, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T9-R1-01 (code-reviewer, REQUIRED): networks.test.ts now uses `dirname(fileURLToPath(import.meta.url))` instead of bare `__dirname` for canonical ESM module path resolution. vitest tolerates `__dirname` via its CJS-compat shim, but every other test file in the suite uses the explicit ESM pattern — this aligns networks.test.ts with that convention and removes the implicit dependency on the shim. - T9-R1-03 (code-reviewer, suggested): raised `vitest.config.ts` `thresholds.branches` from 75 to 85 — aligns with lines/statements/functions thresholds. Current branch coverage is 89.15% so there is headroom; a future drop below 85% will fail the build. - T9-R1-05 (code-reviewer, suggested): fixed misleading comment on `oversizedHeader()` in `helpers/encode-header.ts`. 'A' is a valid base64 char; the function works because the size guard fires on raw byte length pre-decode, so base64 validity is irrelevant. Comment now reflects that. T9-R1-02 and T9-R1-04 are spec-vs-impl drift in the D18 event catalog (`relayer_low_balance` payload shape and `nonce_replay` vs `nonce_replay_attempt` event name). The current tests correctly track the `SecurityEventCatalog` type in `types.ts`, which is authoritative at the type level. Reconciling those names against tech-spec D18 belongs to T8's catalog-owner, not T9. Flagged in T9 decisions.md follow-up. 244/244 tests still pass. Coverage: 96.87% lines, 89.15% branches, 100% functions across `src/`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SA-T11-01 (medium): register.ts classifyError() now scrubs err.message
via scrubSecrets() before pattern matching. Defense-in-depth so a
future maintainer adding stderr forwarding cannot leak a hex key.
- SA-T11-02 (medium): drop --allow-test-network flag entirely.
test-anvil network branch in register.ts is now gated exclusively on
NODE_ENV=test. The existing test suite already sets NODE_ENV=test in
spawn env, so no test changes were needed for this fix.
- SA-T11-03 (low): post-deploy.ts JSON.parse failure now throws a
static message ("malformed JSON; re-run forge script") instead of
interpolating the inner error.
- SA-T11-04 (low): README maintainer callout now notes that
$PLATFORM_TREASURY_ADDRESS / $DEPLOYER_KEY must be masked in CI logs.
- SA-T11-05 (info): added 8th test, register_never_prints_key_on_rpc_failure
— spawns with a valid REGISTER_KEY but an unreachable RPC (port 1),
asserts non-zero exit, "register_failed:" reason, and zero hex-key
pattern matches in combined stdout+stderr.
All 245 middleware tests pass (1 new). tsc + eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SEC-T9-01 (Medium): `ChallengeBody.additionalProperties` is now `false` in `fixtures/x402-v1.schema.json`, restoring strict drift detection on the 402 challenge body. Added `txHash` to the explicit property allowlist (core.ts emits it on settlement_failed). The previous `true` setting silently passed any extra field, defeating the purpose of vendoring the schema. - SEC-T9-02 (Medium): renamed `nonce_replay` → `nonce_replay_attempt` to match the tech-spec D18 canonical event catalog. Affects `types.ts` (SecurityEventCatalog key + docblock), `core.ts:539` emit site, and `core.test.ts:580` parameterized event-name map. Security monitoring configured against the canonical D18 name now actually receives events. - SEC-T9-03 (Low): added `core.test.ts::stale paused-cache + zero vault on refresh still short-circuits BEFORE verify`. Twin of the non-stale guard test at line 272. Models the path where the first request finds `vaults()=0x0` (no permanent pin per D3 — only non-zero addresses are cached forever), the TTL expires, the refresh succeeds, vaults still returns 0x0, and the vault_not_deployed short-circuit must fire before verify is called with expectedVaultAddress=ZERO_ADDRESS. 246/246 tests pass. Coverage holds at 96.87% lines / 89.15% branches / 100% functions across `src/`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records test review round-1 attribution for task 11. Implementation changes were absorbed into 1d2be23 by the parallel T9 agent due to shared workspace timing. Findings addressed for task 11 register-cli.test.ts: - M1 (shared_state): register_handles_already_registered self-sufficient. - M2 (flakiness): waitForPort dead createServer removed. - M3 (assertion_quality): broad 0x{64} regex moved to already-registered path; happy-path uses literal-substring checks only. - L1 (malformed coverage): sweep three malformed key shapes. - L2 (disabled-network leak check): assert no key in stdout/stderr on arc-mainnet rejected path. - L3 (USDC name): declined — README "USDC" matches T3-verified on-chain value per decisions.md Task 3; user-spec example is the outlier. All 246 middleware tests pass. tsc + ESLint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T11-01 (minor): add `timeout: 30_000` to waitForTransactionReceipt in register.ts so a lost tx surfaces as a typed register_failed result instead of hanging the CLI indefinitely. - T11-02 (minor): already addressed in test-review round 1 fixes (commits 1d2be23 + 869478b — dead createServer in waitForPort removed). - T11-03 (minor): README arcscan URL fixed to the tech-spec canonical https://testnet.arcscan.app (was .network). - T11-04 (minor): declined — README "USDC" matches the T3-verified on-chain value (decisions.md Task 3: "chain returns the literal string 'USDC' rather than the marketing-form 'USD Coin'"). The user-spec example is the outlier and is already tracked as a deferred user-spec amendment (SA-T6-INFO-02 in decisions.md Task 6 open items). All 246 middleware tests pass. tsc + ESLint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T9-F1 (medium): added Fastify adapter test `handler exception propagates via Fastify error path (not swallowed by plugin)`. Mirrors the node-http adapter's exception-propagation test. Registers fastifyPaywall, mocks paywall to return passthrough, then has the downstream route handler throw — expects res.statusCode === 500 (Fastify's default error handler ran). A regression that swallowed exceptions in the preHandler would now fail loudly. - T9-F2 (medium): added cold-checkout guard in networks.test.ts. The T3-artefact-comparison test now uses `it.skip` if the `arc-testnet-usdc-domain.json` file is absent (existsSync gate), with the rest of the suite still running. The dependence on T3 is structurally unchanged (networks.ts itself reads the artefact at module-load time), but the guard prevents a raw ENOENT from masking a useful skip message. T9-F3 (nonce_replay → nonce_replay_attempt) is already resolved in commit 1d2be23 (same finding overlapped with SEC-T9-02). 247/247 tests pass. Coverage holds at 96.87% lines / 89.15% branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 1 + round 2 reports for code-reviewer, security-auditor, and test-reviewer. Verdicts after round 2: - code-reviewer-t11: approved - security-auditor-t11: PASS (cleared to ship) - test-reviewer-t11: passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Status: Done (pending user verification of README walkthrough) - Commits: ef9752a + e14734f + 1d2be23 + 869478b + 5d8c2bd + 0c215d0 - Reviews after round 2: code-reviewer approved, security-auditor PASS, test-reviewer passed - Documented deviations (--allow-test-network removed; classifyError scrubs message; sentinel regex tolerates both quote styles; USDC domain name decline rationale) and open items (post-ship classifier fix for 4xx, user-spec amendment carry-forward, T16/T17 handoff). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-1 + round-2 JSON review reports from code-reviewer-t9, security-auditor-t9, and test-reviewer-t9. All three reviewers approved T9 by round 2: - code-reviewer-t9: APPROVED (1 required + 4 suggested in R1 → 0 in R2) - security-auditor-t9: PASS (2 medium + 1 low in R1 → 0 in R2) - test-reviewer-t9: PASSED (3 medium + 3 low in R1 → 0 in R2; lows optional) Also includes the T9 decisions.md entry documenting the implementation, deviations (nonce_replay → nonce_replay_attempt D18 alignment, schema fixture additionalProperties:false fix, SEC-T9-03 test variant), and open follow-ups (relayer_low_balance threshold field, vitest upgrade). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three reviewers approved by round 2. T9 unit-test expansion is complete: 247/247 tests pass, 96.87% line coverage / 89.15% branch coverage on src/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- forked-e2e.test.ts spawns real anvil, deploys MockUsdcEip3009 + PaymentSplitterFactory + PaymentVaultImpl via viem (reading Foundry artifacts from contracts/out/), exercises Node http and Fastify adapters in the same process. Pins the D5 cross-adapter NonceStore replay-rejection invariant: pay via Node http, replay identical X-PAYMENT on Fastify → 402 nonce_already_used. Also covers vault_not_deployed and paused branches. - arc-testnet-e2e.test.ts gated on ARC_TESTNET_E2E=1 via describe.skipIf. Reads canonical env (ARC_RPC_URL, PAYWALL_RELAYER_KEY, ARC_TESTNET_PAYER_PK, ARC_TESTNET_DEVELOPER_EOA, PAYMENT_SPLITTER_FACTORY_ADDRESS). Ajv-validates the 402 body against the vendored x402 v1 schema; asserts X-PAYMENT-RESPONSE + on-chain vault balance delta. - vitest.config.ts: add testTimeout: 60_000 and hookTimeout: 30_000 (required by anvil spawn + 5 s factory-state cache TTL wait). 253 passing + 2 skipped on default invocation; lint and typecheck clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T10-R1-M2: fastify happy path now decodes X-PAYMENT-RESPONSE and
asserts {success, transaction, network, payer} just like node-http
(catches any future Fastify adapter regression that would write a
malformed XPR header).
- T10-R1-M1: drop the unused `developerBEoa` extraction + `void` lint
escape in the vault_not_deployed test.
- T10-R1-M3: document that the Arc Testnet afterAll intentionally
does not close the viem PublicClient (http transport keeps no
persistent sockets).
253 passing + 2 skipped; typecheck and lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- T10-R1-F1: paused-branch cache-TTL wait grows 5_500 → 8_000 ms so a slow CI runner / GC pause can't land the request while the cache is still warm. Comfortable 3 s margin above the 5 s middleware TTL, still well inside testTimeout: 60_000. - T10-R1-F2: vault_not_deployed test now signs against factory.computeVaultAddress(devB) (the CREATE2-predicted address devB WOULD have on register()) rather than 0x0. The middleware reaches vault_not_deployed on its own merits — independent of any future re-order of the to_mismatch / verify check. - T10-R1-F3: vitest.config.ts now sets isolate: true explicitly so the forked-e2e NETWORKS mutation can't bleed into other test files even if vitest defaults shift; companion comment in forked-e2e explains the guarantee. - T10-R1-F5: forked-e2e node-http happy path now ajv-validates the 402 body against the vendored x402 v1 ChallengeBody schema (loaded once in beforeAll). A missing required field surfaces here instead of silently drifting on the wire format. T10-R1-F4 (Fastify XPR shape) was already addressed in commit 13f25d4 via code-reviewer T10-R1-M2. 253 passing + 2 skipped; lint and typecheck clean. Forked suite runs in ~10.2 s (vs ~7.6 s before — the extra 2.5 s is the longer paused wait + the ajv compile + validate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SEC-T10-01 (medium): add a path allowlist in .gitleaks.toml for forked-e2e.test.ts so the well-known public Foundry anvil test private keys don't trigger false-positive blocks in contributor pre-commit hooks. The keys derive from the public "test test … junk" mnemonic and carry no real value — they only ever control accounts on a local anvil node. Comment explains the rationale. - SEC-T10-02 + SEC-T10-03 (low): in arc-testnet-e2e.test.ts, wrap PAYWALL_RELAYER_KEY directly into new OpaqueRelayerKey(...) at the env read, and pass ARC_TESTNET_PAYER_PK directly into privateKeyToAccount(...) without a plain-hex intermediate variable. Eliminates the ~50-line window during which the raw hex key would have lived on the call stack. 253 passing + 2 skipped; typecheck and lint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three reviewers approved at round 2: - code-reviewer-t10: APPROVED (covers 13f25d4 + cee0c18) - security-auditor-t10: APPROVED (SEC-T10-01/02/03 resolved at 9edc669) - test-reviewer-t10: PASSED (F1/F2/F3/F4/F5 all verified) decisions.md entry summarizes the implementation, the four deviations from the initial spec (vitest timeout add, NETWORKS in-place mutation, computeVaultAddress hardening, 8 s paused sleep), and three round-2 review reports per reviewer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 9 minor/medium findings from the Wave 10 holistic audits.
Code (T12):
- T12-01: delete dead `errors.ts` and `errors.test.ts` — `core.ts:build402`
is the production response builder. Removes the parallel `buildErrorResponse`
whose `settlementReason` body field diverged from the wire `reason`.
- T12-02: with `errors.ts` deleted, `SettleReason` (settle.ts) is the single
source of truth. Added documented `SettlementFailedReason` union that
represents the full wire-observable set (8 reasons including the
core-emitted `chain_id_mismatch`).
- T12-03: replace `reason: 'internal_error'` (not in any taxonomy) with
`reason: 'chain_id_mismatch'` at both emit sites in `core.ts`. Matches
the D18 event name.
- T12-04: rewrite `relayer-key.ts` header docstring to describe the actual
`WeakMap<OpaqueRelayerKey, string>` implementation (per T6 round-1
hardening). The class has no `#key` field — the docstring lied.
- T12-05: rename `usdc`/`feeBps` locals to `usdcToken`/`currentFeeBps` in
`PaymentVaultImpl.withdraw()` so they no longer shadow interface methods.
Security (T13):
- T13-M-MW-01: tighten `scrubSecrets` bare-hex regex. Replace
`\b[0-9a-fA-F]{64}\b` with `(?<![0-9a-fA-F])[0-9a-fA-F]{64,}(?![0-9a-fA-F])`
to redact any bare-hex run of >=64 chars. The previous `\b` word boundaries
did not fire between adjacent hex digits, so concatenated runs (two spliced
keys, a 65-byte signature without `0x`) passed through unredacted. Adds 4
fuzz tests covering embedded windows, mixed-case 130-bare-hex, and
concatenated 64-hex runs.
Tests (T14):
- T14-L1: document why the 8s real-clock sleep in `forked-e2e` paused-request
test cannot use fake timers (would freeze in-flight anvil + Fastify timers).
- T14-L2: extract `MOCK_USDC_NAME` constant in `forked-e2e.test.ts` so the
mock-vs-live USDC domain name divergence has a single source of truth.
- T14-L3: comment the forge LCOV instrumentation quirk on
`_pause()`/`_unpause()` body lines (branch metric is the real CI gate).
Verification:
- npm test --workspace=@universal-paywall/middleware: 208 passed + 2 skipped
- cd contracts && forge test: 52 passed / 0 failed
- npm run lint / typecheck / build: all clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-1 re-audit reports from code-auditor, security-auditor, and
test-auditor on the Wave 10 audit-fix commit (58a1aae).
Verdicts:
- code-auditor: approved (all 5 T12 minors resolved)
- security-auditor: APPROVED / CLEAR FOR FINAL WAVE
(T13-M-MW-01 resolved; consolidated {64,} quantifier preferred)
- test-auditor: PASS_WITH_NOTES (T14-L1/L2/L3 resolved; 1 low + 1 info
raised, both non-blocking and deferred to post-MVP)
decisions.md updated with final verdicts and post-MVP open items
(restore per-reason schema iteration on build402; test the
unknown-network chain_id_mismatch branch; optional {64,4096} cap on
HEX_BARE_RUN_RE).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mnemonik-dev
pushed a commit
that referenced
this pull request
Jun 21, 2026
…rget
Navidrome scrobbles natively to any ListenBrainz-compatible server set via
ND_LISTENBRAINZ_BASEURL. Implement the two endpoints its client calls so the
sidecar attaches per-listen royalties with zero changes to Navidrome.
Verified against navidrome/adapters/listenbrainz/client.go + auth_router.go:
- GET /1/validate-token (Authorization: Token <t>) -> {valid:true,user_name,code:200}
- POST /1/submit-listens -> {status:'ok'}; recording_mbid (fallback first
artist_mbids) -> resolveCreator, the Token -> resolvePayer; playing_now skipped.
- src/listenbrainz.ts: adapter (parseListenToken, listenCreatorKey, handleListenSubmit).
- src/serve.ts: thread request headers into RouteHandler ctx; add listenBrainzRoutes.
- src/cli.ts: PLATFORM=navidrome (multi-route); src/index.ts exports.
- +7 unit tests (24 total in the package; typecheck + build clean).
- deploy/navidrome + deployment-plan/STATUS updated; corrected the stale Mastodon
campaign schema noted for gap #2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUqG1L5TrBNeScLxTXgzyM
mnemonik-dev
pushed a commit
that referenced
this pull request
Jun 21, 2026
Full vertical against a live ghcr.io/navidrome/navidrome container - zero Navidrome changes, it scrobbles natively to our ListenBrainz target: Subsonic scrobble -> Navidrome forwards to ND_LISTENBRAINZ_BASEURL (our sidecar) -> POST /1/submit-listens -> recording_mbid -> live MusicBrainz WS/2 resolver -> artist -> wallet -> facilitator -> anvil settle -> artist paid 100 micro-USDC The real Navidrome scrobble payload matched the parser exactly: listen_type=single, recording_mbid=f1aa509e..., artist_mbids=[4d5447d7...]. This field-verifies gap #1 (ListenBrainz target) and gap #4 (MusicBrainz resolver) together with a real instance + the real MusicBrainz API. - scripts/e2e-navidrome-live-docker.mjs: documented real-L3 harness (Docker + WS/2 egress prerequisites, including the ffmpeg MBID-tagging + container recipe). - testing-plan / STATUS / deploy/navidrome updated with the result + real payload. - HANDOFF gotcha: prefer GHCR over Docker Hub (anon pull rate limit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YUqG1L5TrBNeScLxTXgzyM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.