sync: mirror open-core subset from private repo - #23
sync: mirror open-core subset from private repo#23wave-av-release-bot[bot] wants to merge 1 commit into
Conversation
|
|
| Filename | Overview |
|---|---|
| sdk/js/index.js | Adds context-attestation verifier (canonical, _attSigValid, verifyAttestation) with Ed25519 + HMAC tri-state; re-exports payment-receipt + registry from verify.js. Contains a duplicate _asciiEscape with an invisible U+0080 character in the regex (already flagged). |
| sdk/js/verify.js | New self-contained payment-receipt verifier + trusted-key registry for JS. Contains a second copy of _asciiEscape with the same invisible U+0080 regex issue as index.js; both copies must be patched together. |
| sdk/python/wave_dispatch/verify.py | New offline verifier for both receipt types in Python; stdlib-only HMAC path, lazy Ed25519 via cryptography; make_registry returns frozenset; consistent tri-state logic mirrors JS byte-for-byte. |
| sdk/rust/src/verify.rs | Adds canonical/verify/trusted-key registry in pure Rust (ed25519-dalek, hmac, sha2, hex); ascii_escape correctly handles astral chars via UTF-16 surrogate pairs; inline #[cfg(test)] vectors are consistent with JS/Python pins. |
| sdk/ruby/lib/wave_dispatch/verify.rb | New Ruby verifier using JSON.generate(ascii_only:true) for canonical form and OpenSSL for Ed25519/HMAC; explicit delegators avoid Ruby 2.6/2.7 keyword splat pitfalls; correctly gates OpenSSL::PKey.new_raw_public_key on >= 1.1.1. |
| sdk/js/attest.test.js | Cross-language test pinning canonical bytes, Ed25519 verification, HMAC tri-state, and truncation detection against the Python-signer fixed vector. |
| sdk/js/trusted.test.js | Comprehensive registry + trustedSigner + fold tests including key_id lie detection and WebCrypto-absent simulation; shared fixed vectors with the other SDK test suites. |
| sdk/ruby/test/verify_test.rb | Minitest suite with cross-language byte-parity assertions; Ed25519 cases skip gracefully when OpenSSL < 1.1.1; registry/trusted-signer fold tested end-to-end. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant SDK as SDK (JS/Python/Rust/Ruby)
participant Registry as TrustedRegistry
Note over SDK: Offline, no network required
Client->>SDK: "verify*(record, {hmacKey?, registry?})"
SDK->>SDK: "canonical*(record)"
Note over SDK: Sort fields, JSON.stringify,<br/>ASCII-escape non-ASCII
alt "alg = ed25519"
SDK->>SDK: importKey(pubkey)
SDK->>SDK: subtle.verify(sig, canonicalBytes)
SDK-->>Client: true / false
else "alg = hmac-sha256"
alt hmacKey provided
SDK->>SDK: HMAC-SHA256(key, canonicalBytes)
SDK->>SDK: timingSafeEqual(computed, sig)
SDK-->>Client: true / false
else no hmacKey
SDK-->>Client: null (uncheckable)
end
else no alg or none
SDK-->>Client: null (unsigned)
end
opt "registry provided AND sig=true"
SDK->>Registry: trustedSigner(record, registry)
Registry->>Registry: "sha256(pubkey_bytes)[:16] == key_id?"
Registry->>Registry: registry.has(pubkey)?
Registry-->>SDK: true / false / null
alt "trustedSigner = false"
SDK-->>Client: false (valid sig, untrusted key)
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant SDK as SDK (JS/Python/Rust/Ruby)
participant Registry as TrustedRegistry
Note over SDK: Offline, no network required
Client->>SDK: "verify*(record, {hmacKey?, registry?})"
SDK->>SDK: "canonical*(record)"
Note over SDK: Sort fields, JSON.stringify,<br/>ASCII-escape non-ASCII
alt "alg = ed25519"
SDK->>SDK: importKey(pubkey)
SDK->>SDK: subtle.verify(sig, canonicalBytes)
SDK-->>Client: true / false
else "alg = hmac-sha256"
alt hmacKey provided
SDK->>SDK: HMAC-SHA256(key, canonicalBytes)
SDK->>SDK: timingSafeEqual(computed, sig)
SDK-->>Client: true / false
else no hmacKey
SDK-->>Client: null (uncheckable)
end
else no alg or none
SDK-->>Client: null (unsigned)
end
opt "registry provided AND sig=true"
SDK->>Registry: trustedSigner(record, registry)
Registry->>Registry: "sha256(pubkey_bytes)[:16] == key_id?"
Registry->>Registry: registry.has(pubkey)?
Registry-->>SDK: true / false / null
alt "trustedSigner = false"
SDK-->>Client: false (valid sig, untrusted key)
end
end
Reviews (9): Last reviewed commit: "sync: mirror open-core subset from priva..." | Re-trigger Greptile
🌊 WAVE BugBot — 29 finding(s)🔴 22 · 🟠 3 · 🟡 4
severity: critical · major · minor · info — local review · $0 inference · wave-dispatch · react 👍/👎 to tune |
c3df6cb to
d79b7f6
Compare
d79b7f6 to
1ffc31c
Compare
Automated by scripts/sync-public.sh — README, SDK READMEs, threat-model, LICENSE, wrangler example, and the JS/Python/Rust/Ruby thin clients. worker.ts + sdk/go are curated separately (boundary).
1ffc31c to
1563829
Compare
|
PR author is in the excluded authors list. |
ApprovabilityVerdict: Needs human review Unable to check for correctness in 1563829. This PR changes the repository license (Apache 2.0 → MIT) and introduces new cryptographic verification functionality across JS, Python, Ruby, and Rust SDKs. All modified files are owned by streaming-team, not the release bot author. License changes and new crypto capabilities warrant review by the designated owners. You can customize Macroscope's approvability policy. Learn more. |
| // ── Payment-Receipt + trusted-key registry: both live in ./verify.js (self-contained, zero-dep) so this | ||
| // entrypoint stays under the 300-line gate; re-exported so the public API is unchanged. ──────────────── | ||
| import { trustedSigner } from "./verify.js"; // local use by the {registry} fold above (imports are hoisted) | ||
| export { canonicalPaymentReceipt, verifyPaymentReceipt, makeRegistry, trustedSigner } from "./verify.js"; |
There was a problem hiding this comment.
🟡 Changelog not updated for the newly added public verification APIs
New user-facing APIs are added to all four client libraries (export { canonicalPaymentReceipt, verifyPaymentReceipt, makeRegistry, trustedSigner } at sdk/js/index.js:283) without any matching entry in the project's change log, so users upgrading have no record of the new capabilities.
Impact: Consumers of the published packages cannot discover the new offline verification features from the release notes.
Repository rule: CHANGELOG "Unreleased" must be updated for user-facing changes
AGENTS.md states: "Conventional Commit titles; update CHANGELOG.md (Unreleased) for user-facing changes." This PR adds new exported APIs in sdk/js/index.js, sdk/js/verify.js, sdk/python/wave_dispatch/verify.py, sdk/ruby/lib/wave_dispatch/verify.rb, and sdk/rust/src/verify.rs, plus a license change in LICENSE — all user-facing. CHANGELOG.md still contains only an empty ## [Unreleased] section and is not touched by the commit.
Prompt for agents
AGENTS.md requires updating CHANGELOG.md's Unreleased section for user-facing changes. This PR adds new public APIs across four SDKs (canonicalAttestation/verifyAttestation/attestationTruncated/canonicalPaymentReceipt/verifyPaymentReceipt/makeRegistry/trustedSigner in JS, the equivalents in sdk/python/wave_dispatch/verify.py, sdk/ruby/lib/wave_dispatch/verify.rb, sdk/rust/src/verify.rs) and relicenses the project from Apache-2.0 to MIT, but CHANGELOG.md still has an empty Unreleased section. Add entries under Unreleased describing the new verification APIs per SDK and the license change. Note the repo's sync script (scripts/sync-public.sh) may need to be taught to carry changelog updates across from the private repo.
Was this helpful? React with 👍 or 👎 to provide feedback.
| fn verify_record(record: &Value, canonical_str: &str, hmac_key: Option<&str>) -> Option<bool> { | ||
| let alg = record.get("alg").and_then(Value::as_str)?; | ||
| let sig = record.get("sig").and_then(Value::as_str)?; | ||
| if alg.is_empty() || alg == "none" { | ||
| return None; | ||
| } | ||
| match alg { | ||
| "ed25519" => { | ||
| let pubkey = record.get("pubkey").and_then(Value::as_str)?; // None if no pubkey | ||
| Some(verify_ed25519(pubkey, sig, canonical_str.as_bytes())) | ||
| } | ||
| "hmac-sha256" => { | ||
| let key = hmac_key?; // None if the key was not supplied | ||
| Some(verify_hmac(key, sig, canonical_str.as_bytes())) | ||
| } | ||
| _ => None, | ||
| } | ||
| } |
There was a problem hiding this comment.
🔍 Tri-state contract diverges across SDKs for empty-string sig / hash fields
The four verifiers agree on the documented tri-state for missing fields, but not for present-but-empty strings, which weakens the "identical behaviour in every runtime" claim:
- Empty
sig(sig: ""): JS treats it as falsy and returnsnull(sdk/js/verify.js:38,sdk/js/index.js:250); Python likewise returnsNone(sdk/python/wave_dispatch/verify.py:59). Rust only bails whensigis absent or not a string, so""reachesverify_ed25519/verify_hmacand yieldsSome(false)(sdk/rust/src/verify.rs:91-106). Ruby only checkssig.nil?, so""also yieldsfalse(sdk/ruby/lib/wave_dispatch/verify.rb:56). - Empty hashes in
attestation_truncated: JS compares them (returnsfalsefor two empty strings) (sdk/js/index.js:236), Ruby likewise (sdk/ruby/lib/wave_dispatch/verify.rb:159-165), while Python (sdk/python/wave_dispatch/verify.py:137) and Rust (sdk/rust/src/verify.rs:166) returnNone/nullbecause they additionally test for emptiness.
None of these are exercised by the shared vectors, so the cross-language pins pass regardless. Worth aligning if the tri-state is meant to be a contract.
Was this helpful? React with 👍 or 👎 to provide feedback.
| MIT License | ||
|
|
||
| Copyright (c) 2026 WAVE Online, LLC |
There was a problem hiding this comment.
🔍 NOTICE file still asserts Apache-2.0 terms after the relicense to MIT
LICENSE is replaced with MIT ("Copyright (c) 2026 WAVE Online, LLC"), but the repo-root NOTICE is untouched and still reads "are NOT licensed under the Apache License, Version 2.0. The Apache License grants rights to the software in this repository only..." and is copyrighted to "WAVE, Inc." rather than "WAVE Online, LLC". NOTICE is an Apache-2.0 construct (§4d) with no meaning under MIT; leaving it creates a contradictory licensing statement for downstream consumers. The sync script that generated this PR should either drop NOTICE or rewrite it for MIT + trademark reservation.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (att.alg === "ed25519") { | ||
| if (!att.pubkey) return null; | ||
| try { | ||
| const pub = await subtle.importKey("raw", _fromHex(att.pubkey), "Ed25519", false, ["verify"]); | ||
| return await subtle.verify("Ed25519", pub, _fromHex(att.sig), msg); // self-describing: anyone verifies | ||
| } catch { return false; } | ||
| } |
There was a problem hiding this comment.
🔍 Ed25519 in WebCrypto is not universally available on the declared Node >= 18 floor
The JS verifier documents "needs Node >=18 or a modern browser" and package.json sets engines.node >= 18, but WebCrypto's "Ed25519" algorithm only landed in Node's crypto.subtle in 18.4.0 (and is still gated/absent in several browsers). On an 18.0–18.3 runtime, subtle.importKey("raw", ..., "Ed25519", ...) throws and is swallowed by the catch { return false; }, so a perfectly valid attestation/receipt is reported as a cryptographic failure rather than an environment error — the opposite of the "fail loud" posture applied to the missing-WebCrypto case at sdk/js/verify.js:40. Consider distinguishing "unsupported algorithm" from "bad signature".
Was this helpful? React with 👍 or 👎 to provide feedback.
| // Zero-dep tests (node:test) for the SDK's offline Payment-Receipt verifier — the MONEY half of "the two | ||
| // receipts". The decisive test is CROSS-IMPLEMENTATION: EXPECTED_CANONICAL is the SAME constant pinned in | ||
| // edge-router/payment-receipt.test.ts, so a green here proves the SDK and the edge signer produce byte-for- | ||
| // byte identical canonical strings — "sign on the edge, verify in any SDK, trust no one" for payments. | ||
| import { test } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { canonicalPaymentReceipt, verifyPaymentReceipt } from "./index.js"; | ||
|
|
||
| // Pinned identically in edge-router/payment-receipt.test.ts (the byte-parity vector). | ||
| const FIXED = { | ||
| v: "wave.payment-receipt/v0", ts: 1700000000, protocol: "x402", mode: "wave-x402", | ||
| resource: "/extract", network: "base", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", | ||
| amount_atomic: "1000", pay_to: "0x0000000000000000000000000000000000000001", | ||
| tx_hash: "0xdeadbeef", verified: true, | ||
| }; | ||
| const EXPECTED_CANONICAL = | ||
| '{"amount_atomic":"1000","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",' + | ||
| '"mode":"wave-x402","network":"base","pay_to":"0x0000000000000000000000000000000000000001",' + | ||
| '"protocol":"x402","resource":"/extract","ts":1700000000,"tx_hash":"0xdeadbeef",' + | ||
| '"v":"wave.payment-receipt/v0","verified":true}'; |
There was a problem hiding this comment.
🔍 Cross-language byte-parity vectors are pinned against signers not present in this repo
The tests assert byte-for-byte parity with edge-router/context-attest.ts, edge-router/payment-receipt.ts, and edge-router/*.test.ts, none of which exist in this repository (edge-router/ contains only worker.ts and wrangler.example.toml). Only the private repo can actually run the parity check between SDK and edge signer; here the vectors are hard-coded constants that four independent SDKs are compared against. That still catches SDK-vs-SDK drift, but a change to the edge signer will not be caught by this repo's CI — worth confirming the private repo gates the same constants.
Was this helpful? React with 👍 or 👎 to provide feedback.
| export function makeRegistry(entries) { | ||
| const list = Array.isArray(entries) ? entries : (entries && Array.isArray(entries.keys) ? entries.keys : []); | ||
| const set = new Set(); | ||
| for (const e of list) { | ||
| const pub = typeof e === "string" ? e : (e && e.pubkey); | ||
| if (_isHex(pub) && pub.length) set.add(pub.toLowerCase()); | ||
| } | ||
| return { has: (p) => typeof p === "string" && set.has(p.toLowerCase()), size: set.size }; | ||
| } |
There was a problem hiding this comment.
🟨 Trusted-key registry accepts any even-length hex string, including keys that cannot be valid Ed25519 pubkeys
makeRegistry (and the Python/Ruby/Rust equivalents) only checks that an entry is a non-empty, even-length hex string before adding it to the trusted-signer set (sdk/js/verify.js:79-87). It never enforces the 32-byte (64 hex char) Ed25519 public-key length. A malformed or truncated /.well-known/wave-keys.json payload (e.g. a value clipped to "8a88") is silently accepted as a trusted key rather than being rejected, and makeRegistry reports a non-zero size, which flips trustedSigner/the {registry} fold from the safe "cannot decide" (null) state into an active allow/deny decision built from garbage data.
Was this helpful? React with 👍 or 👎 to provide feedback.
Automated by
scripts/sync-public.sh. Mirrors README, SDK READMEs, threat-model, LICENSE, the wrangler example, and the JS/Python/Rust/Ruby thin clients from the private repo.worker.ts+sdk/goare curated separately (open-core boundary). Review + CI gate this before merge.Summary by cubic
Syncs the open‑core subset and ships offline, trustless verification for context‑attestations and payment receipts (Ed25519 or HMAC) across
sdk/js,sdk/python,sdk/rust, andsdk/ruby, with a trusted‑key registry and canonical byte parity with the edge/Python signers. No worker or route changes; no billing or metering impact.Verification
sdk/js:attest.test.js,receipt.test.js,trusted.test.js(node:test).sdk/ruby:test/verify_test.rb(minitest);sdk/rust: inline tests inverify.rs.Migration
LICENSEchanged to MIT; review compliance as needed.sdk/js:canonicalAttestation,attestationTruncated,verifyAttestation(att, { hmacKey?, registry? }),canonicalPaymentReceipt,verifyPaymentReceipt(r, { hmacKey?, registry? }),makeRegistry(entries),trustedSigner(obj, registry). Requires WebCrypto at runtime (e.g., Node ≥18) for Ed25519 andkey_idchecks. Internal moduleverify.jsadded; public API unchanged.sdk/python:canonical_attestation,verify_attestation(att, hmac_key=None, registry=None),attestation_truncated,canonical_payment_receipt,verify_payment_receipt(r, hmac_key=None, registry=None),make_registry(entries),trusted_signer(obj, registry)(Ed25519 needs optionalcryptography; HMAC works without it).sdk/rust:canonical_attestation,verify_attestation,verify_attestation_trusted,attestation_truncated,canonical_payment_receipt,verify_payment_receipt,verify_payment_receipt_trusted,make_registry,trusted_signer, typeRegistry(pure Rust viaed25519-dalek,hmac,sha2,hex).sdk/ruby:canonical_attestation,verify_attestation(att, hmac_key: nil, registry: nil),attestation_truncated,canonical_payment_receipt,verify_payment_receipt(r, hmac_key: nil, registry: nil),make_registry,trusted_signer(Ed25519 verify needs OpenSSL ≥1.1.1; HMAC works universally).Written for commit 1563829. Summary will update on new commits.
Note
Add offline verification of payment receipts and context attestations to JS, Python, Ruby, and Rust SDKs
verify_payment_receipt,verify_attestation,canonical_payment_receipt,canonical_attestation,attestation_truncated,make_registry, andtrusted_signerto all four SDKs (JS, Python, Ruby, Rust), each returning a tri-state result (true/false/null or Some(true/false)/None).ensure_ascii=Truebehavior with lexicographically sorted keys.makeRegistry/make_registry) that downgrades a valid signature to false when the signer's pubkey is not in the registry, with optionalkey_idhonesty enforcement.Macroscope summarized 1563829.