[WIP] feat(m14): @aastar/react hooks + @aastar/xiaoheishu component library#15
[WIP] feat(m14): @aastar/react hooks + @aastar/xiaoheishu component library#15jhfnetboy wants to merge 36 commits into
Conversation
…alized Agent messaging - XMTP integration evaluation (permissioned risk, fork rights, MIT license) - XMTP deep analysis (node permissioning, 7-node centralization, fork cost) - Spore Protocol design doc: Nostr transport + AirAccount identity + Pay-per-Store incentive - M1: @aastar/messaging TS SDK MVP (XMTP-compatible interface, NIP-44, nostr-tools) - M2: on-chain integration (x402/Channel/UserOp event kinds) - M3: incentivized Relay network (Pay-per-Store via SuperPaymaster) - M4: MLS upgrade + Rust SDK (rust-nostr + libxmtp)
- Spore_M2_Engineering_Spec.md: complete M2 on-chain bridge design (SporeEventTypes, X402Bridge, ChannelBridge, UserOpBridge, RelayPool upgrade) - Spore_NewFeatures_Integration.md: SuperPaymaster/airaccount-contract/AirAccount new features mapped to Spore Protocol milestones P0: AgentSessionKeyValidator, x402 Facilitator, dual settlement P1: ERC-8004 identity binding, SKILL.md discovery, reputation-tiered relay incentive P2: KMS/TEE key management, BLS aggregate, hierarchical sub-delegation
… MVP) - SporeAgent: XMTP-compatible EventEmitter API (createFromEnv, on/text/dm/group, sendDm, sendGroupMessage) - AirAccountIdentity: EOA private key → Nostr pubkey (same secp256k1 curve, zero conversion) - RelayPool: multi-relay WebSocket management with LRU dedup and backoff reconnect - NostrTransport: NIP-17 gift-wrap DMs + NIP-44 ChaCha20-Poly1305 E2E encryption - Nip44Crypto: full NIP-44 v2 implementation (HKDF conversation key, XChaCha20-Poly1305) - 31 unit tests, all passing - xmtp-compat shim for drop-in XMTP agent-sdk migration - docs/Spore_M3_Incentive_Design.md: Pay-per-Store economic model, strfry plugin, RelayRegistry.sol - scripts/spore_poc.ts: end-to-end demo script
- SporeEventTypes: full kind:23402–23405 type system with tag parsers - X402Bridge (kind:23402): EIP-3009 tag validation → policy checks → nonce idempotency → settlePayment(); reject reasons: expired/nonce_already_used/ amount_exceeds_limit/payer_not_allowed/missing_tags - ChannelBridge (kind:23403): monotonic cumulative check + lazy settle (threshold-triggered); forceSettleAll() for graceful shutdown - UserOpBridge (kind:23404): three auth modes (self_only/whitelist/open) + prompt injection defense via selector/contract allowlists; trigger nonce consumed only on successful bundler submit - SporeAgent: registerBridge(), enableX402(), enableChannel(), enableUserOp(); bridge:error event; #p-filtered subscription for payment kinds - Zero blockchain dependencies in @aastar/messaging — all on-chain calls via injected *Like interfaces (X402ClientLike, ChannelClientLike, BundlerClientLike) - 59 tests passing (28 bridge + 31 existing)
- SporeRelayNode: full NIP-01 WebSocket relay (EVENT/REQ/CLOSE), event ID and Schnorr sig validation, fan-out to matching subscribers, EOSE support - PaymentValidator: off-chain EIP-3009 commitment validation (<5ms, no RPC); checks amount >= minFee, expiry, 'to' address match, chain ID, signature - SqliteEventStore: persistent event storage with kind/pubkey/tag indexes, configurable limit (default 100, max 500 per query) - SporeRelayOperator: accumulates kind:23405 payment vouchers, batch-settles via ChannelClient when threshold reached; forceSettleAll() for shutdown - StrfryPlugin: stdin/stdout JSON write plugin for strfry (Rust relay) - RelayRegistryClient: on-chain read/write for SporeRelayRegistry.sol (Optimism) - bin/spore-relay: CLI entry point (PORT, SPORE_OPERATOR_ADDRESS, SPORE_MIN_FEE_USDC) - docker/docker-compose.yml: one-command deployment - 23 unit tests passing - Renamed from @aastar/relay to @aastar/message-relay to avoid ambiguity with Nostr relay terminology
- Start SporeRelayNode on localhost:17777 (in-memory SQLite) - Agent A + Agent B connect to local relay exclusively - Test: A→B DM delivery via NIP-17 GiftWrap + NIP-44 decrypt ✓ - Test: B echoes A; A receives round-trip reply ✓ - Test: relay rejects tampered event signatures ✓ - Test: relay sends EOSE on subscription ✓ - Test: distinct secp256k1 identities derived per agent ✓ - No mocks, no external relays, real crypto end-to-end
Security fixes (CRITICAL/HIGH): - validateEventId/Sig → private in SporeRelayNode (prevent monkey-patching) - verifyAuthorizationSig: hash bytes not hex strings (case-sensitive bug) - StrfryPlugin: replace process.exit(0) with injectable onClose callback Correctness fixes: - unwrapDm: guard sealEvent.pubkey/content field types before decrypt - randomTimestampInLastDay: use crypto.getRandomValues, not Math.random - AirAccountIdentity: remove XOR keccak fallback, use @noble/hashes directly - PaymentValidator: compute real EIP-712 domain separator (was placeholder) - RelayPool: enforce wss:// in production (allowInsecure opt-in for dev/test) Resilience: - NonceStore interface: injectable persistence for X402Bridge + UserOpBridge - VoucherStore interface: injectable persistence for ChannelBridge - BoundedSet: cap seenIds at 10k entries to prevent OOM on long-running nodes Tests: - validateEventId/Sig tests: access private methods via unknown cast - bridges.test.ts: restore self_only/whitelist modes; fix authSig expectations
CRITICAL — TOCTOU replay prevention: - NonceStore: add claim() atomic method (check+set in one operation) - X402Bridge/UserOpBridge: replace has()+add() two-step with claim() - Async store implementations (Redis/DB) must implement claim() as atomic SET-NX or INSERT-OR-IGNORE to prevent concurrent replay attacks HIGH — validBefore upper bound (X402Bridge): - Add maxValidBeforeWindowSeconds config (default: 7 days) - Commitments with validBefore > now + window are now rejected - Prevents indefinitely-valid payment commitments LOW — defensive robustness: - X402Bridge: wrap BigInt() in try/catch; malformed amount/valid_before strings now return missing_tags instead of throwing uncaught - PaymentValidator: remove 'as unknown as' type cast; pass Uint8Array directly to viem keccak256 (ByteArray overload, no runtime change) False positives from review (not fixed): - CRITICAL #2: keccak256 address derivation is correct (slice(-40) works) - CRITICAL #3: BoundedSet never exceeds maxSize (evict-then-add) - HIGH #7: Timing attack on string compare not exploitable in this context
- SporeRelayOperator: add SettlementClientLike injectable interface; settleNow() calls batchSettle() on-chain when client is provided; retains pending vouchers on failure for retry (H4/I5) - SporeRelayNode: pass settlementClient to operator; add maxEventsPerSecond rate limiting (default 20/s) with 1-second sliding window per client (I1) - SporeAgent: add allowedSenders/blockedSenders consent API to config; allowlist takes precedence over blocklist; silently drops non-consented messages before event emission (F1) - Add 9 new tests covering settlement client injection, rate limiting, and consent allowlist/blocklist behaviour (91 total)
Unit tests added: - NostrTransport: 15 tests — sendDm gift-wrap structure, sendGroupMessage tags, subscribeToDms/Groups filter setup, decodeGroupEvent parsing - StrfryPlugin: 14 tests — accept/reject decisions, strictMode, kind:23405 fee/expiry/recipient/sig pipeline, onClose injectable callback verified (no process.exit called when custom onClose is provided) E2E tests added (tests/e2e/two-agent-dm.test.ts): - Real crypto (no mocks), real WebSocket relay, real SQLite - Alice→Bob DM round-trip with NIP-17 gift wrap verified - Bob→Alice reply verified - Relay deduplication: same event not delivered twice - Consent blocklist: blocked sender messages silently dropped - Allowed sender (Carol→Dave) not affected by Eve being blocked Total: 120 unit tests + 6 E2E tests (all passing) Also added docs/Spore_XMTP_Parity_Roadmap.md: - Full XMTP v3 capability gap table - M5-M11 milestone plan (~5 months to full parity) - Remaining test TODOs per milestone
CRIT-1: Verify seal Schnorr signature in NostrTransport.unwrapDm() before trusting seal.pubkey as the true sender. Without this, any relay could inject a spoofed seal to impersonate an arbitrary Nostr identity. CRIT-3: Add injectable verifyVoucherSig callback to ChannelBridgeConfig. Without offline sig verification, an attacker can spam fake high-amount vouchers to force expensive on-chain submitVoucher() calls that waste gas. HIGH-2: Derive DM conversation IDs with SHA256(sorted pubkeys) instead of a colon-joined string — trivially collidable via crafted pubkeys. HIGH-3: Eliminate modulo bias in randomTimestampInLastDay() using rejection sampling. The 172800s window does not evenly divide 2^32. HIGH-8: Verify Nostr event id + Schnorr sig in SporeAgent.handleBridgeEvent() before routing to a bridge. Events may arrive from third-party relays that do not enforce NIP-01 signature validation. MED-1: Set this.running = true only after all subscriptions are established. MED-2: Wrap async subscription callbacks with .catch() to prevent unhandled promise rejections bubbling to Node.js process-level uncaught rejection. MED-3: Add 'valid_before_too_far' to X402RejectReason union type (was returned at runtime but missing from the TypeScript union). TS-2: Widen unhandledError event signature to accept MessageContext | ConversationContext | null. All 120 unit tests pass (78 messaging + 42 message-relay).
…llMessages
Add M5 Conversations API to SporeAgent, providing full history + streaming access:
- agent.listConversations(opts?)
Returns known conversations sorted by createdAt desc.
Supports type filter ('dm' | 'group' | 'all') and limit.
- agent.getMessages(convId, opts?)
Fetches history from relay for a known conversation.
DMs: queries kind:1059 gift-wraps (#p filter), decrypts via NostrTransport.
Groups: queries kind:11 events (#h filter).
Supports limit/since/until pagination. Results sorted by sentAt asc.
- agent.streamAllMessages(opts?)
Returns AsyncGenerator<MessageContext> backed by 'message' event queue.
Stops on AbortSignal or pre-aborted signal (returns immediately).
NostrTransport: expose decryptDm() and decodeGroup() as public wrappers
around the private unwrapDm() and decodeGroupEvent() methods.
types.ts: add ListConversationsOptions, GetMessagesOptions, StreamAllMessagesOptions.
Tests: 15 new unit tests covering all M5 API surface (93 total, all passing).
…Member, getGroupInfo M6 adds a stateful group management layer on top of NIP-29 group events. NostrTransport: - KIND_GROUP_META (9000), KIND_GROUP_ADD (9001), KIND_GROUP_REMOVE (9002) constants - sendGroupMeta() — publish kind:9000 metadata (name, picture, description) - sendGroupMembership() — publish kind:9001/9002 add/remove member events SporeAgent: - createGroup(opts?) — generate groupId, store conversation, publish meta + initial members - addGroupMember(groupId, pubkey) — idempotent; updates local state + publishes kind:9001 - removeGroupMember(groupId, pubkey) — idempotent; updates local state + publishes kind:9002 - getGroupInfo(groupId) — returns GroupInfo snapshot (members copy, not reference) Security properties: - Idempotency: duplicate add/remove calls are silently no-ops (no extra relay events) - getGroupInfo() returns a copy of members array; mutations don't affect stored state - createGroup() uses Node.js crypto.randomBytes(32) for group ID generation types.ts: add CreateGroupOptions, GroupInfo. index.ts: export new kinds + types. Tests: 22 new unit tests (115 total across 5 test files, all passing).
…ctionCodec, ReplyCodec, RemoteAttachmentCodec
M7 adds a pluggable content type codec system (mirrors XMTP ContentTypeCodec API).
Architecture:
- SporeCodec<T>: encode(T)→string, decode(string)→T, fallback?(string)→string
- SporeContentTypeId: { authority, type, version } → serializes to "a/t/v"
- CodecRegistry: Map-backed lookup, pre-loaded with TextCodec
- Wire format: event content = encoded payload; ['ct', 'a/t/v'] tag = type id
Built-in codecs:
- TextCodec ('spore/text/1.0'): passthrough for plain text (default)
- ReactionCodec ('spore/reaction/1.0'): {emoji, action, referencedMessageId}
NIP-25 compat: fallback() returns just the emoji
- ReplyCodec ('spore/reply/1.0'): {text, referencedMessageId}
NIP-10 compat: transport still sets the 'e reply' tag
- RemoteAttachmentCodec ('spore/remote-attachment/1.0'):
{url, filename, mimeType, sizeBytes?, contentDigest?, paymentRequired?}
contentDigest allows receiver integrity verification after download
NostrTransport:
- SendDmOptions + SendGroupMessageOptions gain optional contentTypeId?
- When set, ['ct', contentTypeId] tag added to rumor/group event
- Decoders (unwrapDm, decodeGroupEvent) now extract ct tag → SporeMessage.contentTypeId
SporeAgent:
- codecRegistry: CodecRegistry (pre-loaded with TextCodec)
- registerCodec(codec): this — for chaining
- sendTypedMessage(conv, typeId, content): encodes + sends with ct tag
- handleIncomingMessage: calls decodeMessageContent() to populate decodedContent
MessageContext:
- getDecodedContent<T>(): T | null — typed access to decoded payload
- isContentType(ContentTypeId): boolean — content type check
- sendReaction(emoji, action?): uses ReactionCodec when registered, fallback to plain text
- sendReply(text): uses ReplyCodec when registered, fallback to NIP-10 e-tag plain text
- sendAttachment(attachment): uses RemoteAttachmentCodec, fallback to URL string
Tests: 37 new unit tests (152 total across 6 test files, all passing).
Add SporeIdentityRegistry for NIP-01 profile publishing/fetching and NIP-29-style kind:10001 device list management. SporeAgent exposes publishProfile, fetchProfile, linkDevice, unlinkDevice, getLinkedDevices, and isLinkedDevice. Security fixes applied during review: - Verify Schnorr signatures on all relay-returned events (fetchProfile, fetchProfiles, fetchDeviceList) to defend against relay spoofing - Apply extra fields before reserved keys in publishProfile so that caller-supplied eth_address/name/etc. cannot be overridden by extra 26 unit tests covering all registry and SporeAgent M8 methods.
Add SporeKeyAgreement module implementing a NIP-104 compatible group key agreement protocol with three new event kinds: kind:443 KeyPackage — device broadcasts secp256k1 ECDH public key kind:444 Welcome — epoch key delivered via NIP-17 gift-wrap DM kind:445 Group msg — ChaCha20-Poly1305 with shared epoch key Epoch key lifecycle: epoch-0 = randomBytes(32) epoch-n = HKDF-SHA256(epoch-n-1, "", "spore-mls-next-epoch") SporeAgent M9 API: publishKeyPackage, fetchKeyPackages, createMlsGroup, sendMlsMessage (with optional epoch ratchet), decryptMlsMessage, processWelcome. Security: relay-returned events are Schnorr-verified before use; decryptMlsMessage verifies event sig before decrypting; Welcome parser validates all required fields strictly. 33 unit tests.
Simplification:
- Extract hexToBytes/bytesToHex into shared src/utils/hex.ts; remove 5
duplicate inline implementations across crypto, identity, transport,
payment, and keyagreement modules
- Remove duplicate MlsGroupState/MlsWelcomePayload from SporeKeyAgreement
(canonical definitions live in types.ts)
- Standardize ChannelBridge and UserOpBridge tag parsing to use shared
parseTagsToObject() instead of duplicated new Map() inline pattern
Security fixes:
- hexToBytes now validates hex characters and rejects odd-length strings
- UserOpBridge: reject malformed callData (<72 bytes) instead of silently
falling back to potentially wrong target/selector values
- SporeIdentityRegistry: validate eth_address format with /^0x[0-9a-fA-F]{40}$/
before casting to `0x${string}`
- X402Bridge: validate amount/valid_before are numeric strings before
BigInt() conversion; return distinct 'invalid_tag_format' error
Test updates:
- Replace non-hex fixture strings ('priv'.padEnd(64,'0')) with valid hex
('abcd'.padEnd(64,'0')) in all test files
- Update UserOpBridge allowlist tests to use proper 72-byte callData
- Add test for malformed_calldata rejection
- Replace Buffer.from().equals() comparisons with a bytesEqual() helper
to avoid TypeScript strict-typing issues with Buffer vs Uint8Array
CRIT-1: Fix misleading security model comment in UserOpBridge — authorizationSig is fully verified via secp256k1 ecrecover (not deferred to consumer). CRIT-2: Add FileNonceStore to NonceStore.ts — JSON-file backed store that survives process restarts, eliminating nonce replay vulnerability on restart. Exported as part of the public API alongside InMemoryNonceStore. Warn clearly in docs that InMemoryNonceStore must not be used in production. CRIT-3: ChannelBridgeConfig is now a discriminated union requiring either verifyVoucherSig (EIP-712 offline verifier) or skipVoucherSigVerification: true (testing only). This prevents gas-wasting spam submissions at the type level. H1: X402Bridge maxValidBeforeWindowSeconds default reduced from 7 days to 24 hours, preventing long-lived pre-signed authorizations from being replayed. H2: Expanded callData layout comments in UserOpBridge to name the SimpleAccount execute(address,uint256,bytes) ABI assumption explicitly, with selector byte layout.
Exposes an XMTP-compatible REST API over SporeAgent so external clients (Python, Go, mobile apps) can use the Spore Protocol without the Nostr SDK. API surface: POST /api/v1/messages/send — send NIP-17 DM GET /api/v1/conversations — list known conversations GET /api/v1/messages?peer=<hex> — fetch messages with a peer GET /api/v1/stream — Server-Sent Events stream GET /api/v1/health — liveness probe Security hardening (all findings from security review addressed): - Bearer token auth via crypto.timingSafeEqual (prevents timing oracle) - maxBodyBytes (default 65536) with proper drain-before-413 logic - maxSseClients (default 100) — 429 when exceeded (prevents memory DoS) - requestTimeoutMs (default 30s) socket timeout (prevents slow-client DoS) - Internal errors logged server-side only; 500 returns 'internal_server_error' - contentType field validated against MIME pattern /^[\w-]+\/[\w\-+.]+$/ - limit param clamped to [1, 200] (prevents negative values) - X-Content-Type-Options: nosniff on all JSON responses 23 unit tests covering all routes, auth enforcement, SSE, body size limit.
RateLimiter (token-bucket, injectable store):
- RateLimiter.allow(key) — consume 1 token, return false when bucket empty
- Configurable ratePerSecond, burstLimit, maxKeys (LRU eviction)
- InMemoryRateLimitStore default; RateLimitStore interface for Redis adapters
- RateLimiter.reset(key) — manual bucket reset for post-review allow-listing
MainnetChecklist (automated pre-deployment audit):
Checks SEC-1 through SEC-9 covering:
CRITICAL: wss-only relays, persistent nonce store, relay count, UserOp auth mode
HIGH: rate limiter, voucher sig verifier, gateway auth token, X402 window
MEDIUM: gateway request timeout
runMainnetChecklist(input) returns ChecklistReport with .passed, per-check
results, failure counts, and .summary() for human-readable output.
23 RateLimiter + 15 MainnetChecklist unit tests. Total: 258 passing.
SporeHttpGateway:
- timingSafeEqual: pad both inputs to equal length before crypto.timingSafeEqual,
eliminating the length-oracle dummy-work pattern (cleaner, provably safe)
- Add inline comment clarifying http:// in URL parsing is scheme-only
- Document convId derivation convention matches SporeAgent internals
GatewayTypes:
- Replace loose StreamEvent interface with discriminated union
(StreamMessageEvent | StreamConnectedEvent | StreamErrorEvent) for type safety
RateLimiter:
- Replace O(n) array-based keyOrder with Set<string> for O(1) has/add/delete
- Eviction now uses Set.values().next() (O(1)) instead of shift() (O(n))
- Eliminates quadratic worst-case in maxKeys eviction path
MainnetChecklist:
- Remove unused pass() and fail() helpers — inline into check() directly
ChannelBridge:
- Sanitize error messages: channel_fetch_failed and settlement_failed
(raw exception strings no longer leak to callers; logged server-side)
- Remove redundant 'verifyVoucherSig' in this.config type guard —
TypeScript's discriminated union already narrows correctly
NonceStore (FileNonceStore):
- Wrap JSON.parse in try/catch; gracefully degrade on corrupted files
with a console.warn instead of throwing in constructor
Implements SporeTransport interface over Waku v2 (GossipSub/libp2p):
- SporeTransport pluggable interface (sendDm, sendGroupMessage, subscribeToDms, subscribeToGroups, queryMessages)
- WakuTransport: content topic routing /spore/1/dm-{pubkey}/proto and /spore/1/group-{id}/proto
- NIP-44 encryption preserved; WakuNodeLike interface keeps @aastar/messaging free of @waku/sdk dependency
- Group fan-out: per-member NIP-17-style individually encrypted envelopes
- AbortSignal support for graceful DM subscription cleanup
- Waku Store protocol query with empty-array fallback when unavailable
- 22 unit tests: interface compliance, topic routing, fan-out, subscribe/unsubscribe, history, malformed payloads
Total: 280 tests passing
- Add runtime envelope validation (isValidEnvelope guard) — LOW-1 - Clamp incoming timestamps to ±5min/24h window — MED-1 - Fix fragile convId substring check: use explicit isGroup option — MED-2 - Add AbortSignal support to subscribeToGroups — MED-4 - Fix group fan-out comment: clarify this is shared-topic (not NIP-17 per-DM) — HIGH-1 - Add security caveats in JSDoc: from field unverified, no replay protection — CRIT-1/CRIT-2 - Add SporeTransport.subscribeToGroups opts parameter - Add 4 new tests: isGroup topic routing, missing envelope fields, group AbortSignal 284 tests passing
… and redundant variable
…ansport instances Composes N transports into one SporeTransport for redundancy (e.g. Nostr + Waku): - sendDm / sendGroupMessage: concurrent fan-out, returns first successful hash, re-throws only if all fail - subscribeToDms / subscribeToGroups: merged subscriptions with TTL-bounded deduplication by message ID - queryMessages: merges + deduplicates + sorts by sentAt across all transports; respects limit after merge - SeenSet: lazily evicts expired entries to keep memory bounded without a background timer - AbortSignal forwarded to all child transports 21 unit tests: fan-out sends, fallback on failure, deduplication, TTL eviction, AbortSignal, query merge/sort/limit Total: 305 tests passing
…extract fanOutSubscribe, trim comments
usage-guide.md — role-based guide covering:
- Agent Developer: echo bot, codecs (M7), groups (M6), MLS (M9), payment bridges (M2),
multi-device identity (M8), Waku transport (M12), multi-transport (M13)
- End User: HTTP gateway API, SSE streaming (M10)
- Relay Operator: relay node (M4), rate limiting + mainnet checklist (M11)
- dApp Integrator: gateway setup, React integration (M10)
- Incentive mechanics: X402/Channel/UserOp earning models, relay operator rewards
- Feature acceptance checklist for every milestone (M1–M13)
deployment-guide.md — deployment guide covering:
- Local dev (Anvil + test relay)
- Running an agent, gateway, relay node, Waku node
- Testnet (Sepolia, Optimism Sepolia)
- Production hardening with MainnetChecklist
- Key management, Nginx TLS, Docker Compose
- Monitoring, health checks, upgrade path M1→M13
…ance guides ecosystem-overview.md: - 5-role ecosystem map (Agent Developer, End User, Relay Operator, Waku Operator, DAO) - Token model: xPNTs/aPNTs/GToken/SBT/OpenPNTs with value flow diagram - HyperCapital reputation scoring formula and SBT tier table (Seed→Spore) - Fee distribution table (X402/Channel/relay/storage) - Phase 1–4 growth roadmap (M1–M13 done, M14–v1.0 planned) node-operations-guide.md: - Nostr relay: installation, systemd, Nginx TLS, fee collection, monitoring metrics - Waku full node: nwaku Docker config, topic subscriptions, future Store fees - SuperPaymaster: registration, keeper oracle, collateral monitoring, slashing table - Monthly revenue estimation per node type - Staking APY estimates and slashing/penalty table - Operator upgrade path and deprecation policy community-governance.md: - MushroomDAO structure: Council + Community Assembly, voting mechanics - 4-step proposal process with code examples (RFC → formal → vote → execute) - Treasury management: sources, allocation policy, grant tiers - Contribution paths: developers (bug bounty, PRs), researchers, community builders - aPNTs → xPNTs conversion (100:1, 6-month vesting) - Communication channels and node registry discovery API - Security/emergency procedures, circuit breakers, bug bounty program - Roadmap to full decentralization (Phase 1→3)
…ibrary M14-A — @aastar/react: - SporeProvider context: initialises SporeAgent, manages lifecycle (stop on unmount) - useDm: NIP-17 E2E DM thread with history load + live stream - useGroup: group chat with sendText/addMember/removeMember - useConversations: inbox listing via listConversations - useIdentity: profile publish, linkDevice/unlinkDevice (uses public agent.getLinkedDevices()) - usePayment: X402 USDC tip via agent.sendDm encoded payload - useGatewayStream: SSE EventSource hook for gateway push events - 39 unit tests (vitest + @testing-library/react@16, jsdom) M14-B — @aastar/xiaoheishu: - XiaoHeiNote / XiaoHeiAuthor types (AT Protocol app.xiaohei.note lexicon) - Components: NoteCard, FeedList, CreateNoteForm, DmThread, PaymentModal, ProfileCard - Pages: FeedPage, DmPage, ProfilePage - 46 unit tests Total monorepo tests: 390 passing
Security: - useGatewayStream: validate URL protocol (http/https only) before EventSource - usePayment: cap message at 280 chars, guard amount <= 0n - PaymentModal: parseAtomicUsdc() guards NaN/Infinity/negative before BigInt() Simplification: - useIdentity: remove no-op setLoading(true/false) effect, add TODO for relay fetch - FeedList: replace nested ternary with renderFeedContent() early-return function - PaymentModal: remove unused sender prop from interface and call sites
8e6937f to
37fa5d5
Compare
clestons
left a comment
There was a problem hiding this comment.
Code Review — #15
[WIP] feat(m14): messaging + payment protocol (Spore Protocol, NIP-44, EIP-3009)
Verdict: REQUEST_CHANGES | 4-round PK (DeepSeek R1 → Sonnet R2 → Codex R3 → Opus)
Early security review — WIP, fix before merge.
🔴 Blocking Issues
[Medium] packages/messaging/src/payment/X402Bridge.ts — nonceKey missing payer address (cross-payer nonce burning)
// CURRENT — vulnerable:
const nonceKey = `${chainId}:${nonce}`;
// FIX:
const nonceKey = `${chainId}:${from.toLowerCase()}:${nonce}`;EIP-3009 nonces are scoped per payer address on-chain — from=Alice:nonce=X and from=Bob:nonce=X are different slots on the USDC contract. But the relay nonce store uses only ${chainId}:${nonce}, collapsing the payer dimension. An attacker can:
- Observe Alice's pending payment event (chainId=1, nonce=X)
- Send their own event with the same nonce (invalid signature, different
from) to the relay - X402Bridge claims
1:Xin the nonce store, attempts on-chain settlement (fails — invalid sig) - Alice's real event arrives →
nonce_already_used— relay never processes her payment
Fix: include from in the nonce key. This also means the offline sig check (see below) should come BEFORE the nonce claim.
[Medium] packages/messaging/src/payment/X402Bridge.ts — no offline EIP-3009 signature verification before settlement
// Step 3: Atomic nonce claim
if (!(await this.nonceStore.claim(nonceKey))) { return { error: 'nonce_already_used' }; }
// Step 4: Settle on-chain — sig passed WITHOUT offline verification
const { txHash } = await this.config.x402Client.settlePayment({ ..., sig });Invalid signatures are passed directly to the on-chain settlement client. The on-chain contract will reject them, but:
- The relay operator's EOA pays gas for each rejected call
- An attacker can spam invalid-sig events (consuming nonce slots) to drain the operator's gas balance
PaymentValidator in message-relay correctly does offline EIP-3009 verification before accepting payment events. X402Bridge should do the same.
Fix: add offline sig verification before nonceStore.claim():
// Verify offline before claiming nonce or calling chain
const sigValid = this.verifyEip3009Sig({ from, to, amount, nonce, validBefore, tokenAddress, chainId, sig });
if (!sigValid) return { success: false, error: 'invalid_signature' };
if (!(await this.nonceStore.claim(nonceKey))) { return { success: false, error: 'nonce_already_used' }; }Consider extracting PaymentValidator.verifyEip3009Sig into a shared utility so both code paths stay in sync.
[Medium] packages/messaging/src/payment/ChannelBridge.ts — skipVoucherSigVerification is a config flag, not a runtime guard
The ChannelBridgeConfig type requires either verifyVoucherSig or skipVoucherSigVerification: true. The MainnetChecklist (SEC-4) catches missing verification at audit time, but it's not enforced at runtime — a production deployment using skipVoucherSigVerification: true would silently bypass voucher authentication.
Without voucher sig verification, any attacker who knows a channelId can force costly on-chain submitVoucher() calls that burn gas.
Fix: enforce voucher sig verification as a runtime guard: if !config.skipVoucherSigVerification && !config.verifyVoucherSig, throw at construction time. Additionally, fail-hard if skipVoucherSigVerification: true is used on mainnet chain IDs.
🟡 Confirmed (Fix Before Merge)
[Low] PaymentValidator.ts — validAfter hardcoded to 0n without assertion
The struct hash encodes validAfter=0n regardless of the incoming commitment. If the signer used validAfter != 0, the signature check would fail (correct behavior), but there's no explicit guard rejecting commitments with non-zero validAfter. Make the protocol invariant explicit:
if (c.validAfter !== 0n) return false; // Spore commitments always use validAfter=0[Low] SqliteEventStore.ts / event parsing — JSON.parse on untrusted tag data without try-catch
Malformed tags cause an uncaught exception, crashing the event processing goroutine. Wrap in try/catch and reject the event cleanly.
✅ Security Controls Verified
EIP-712 digest construction (PaymentValidator) — CORRECT:
// Raw concat is mandatory — encodeAbiParameters would right-pad \x19\x01 to 32 bytes
const combined = new Uint8Array(66);
combined[0] = 0x19; combined[1] = 0x01;
combined.set(hexToBytes(domainSeparator), 2);
combined.set(hexToBytes(structHash), 34);
const digest = keccak256(combined);The code even has a comment explaining why encodeAbiParameters cannot be used here. ✓
PaymentValidator offline sig verification — ecrecover matches commitment .from. ✓
Nonce replay protection — nonceStore.claim() atomic operation. ✓ (but nonceKey scope — see above)
MainnetChecklist.ts — runMainnetChecklist covers 8 security checks including ChannelBridge sig verifier (SEC-4) and X402Bridge window (SEC-7). ✓
❌ Rejected
- F1 (EIP-712 raw concat wrong) — Rejected. Raw
\x19\x01 || domainSeparator || structHashIS the correct EIP-712 spec.encodeAbiParameterswould right-pad to 32 bytes and produce the wrong digest. The code has an explicit comment documenting this.
💡 Suggestions
- Centralize EIP-3009 sig verification in a shared utility so X402Bridge and PaymentValidator can't drift.
- Add unit tests for the nonce collision case (two payers, same chainId+nonce) and invalid-sig rejection.
- Gate payment bridges behind a testnet-only chainId allowlist until the runtime sig guards land.
Reviewed by: 4-round PK — DeepSeek R1 (10 findings, F1 false positive) → Sonnet R2 (rejected F1 with EIP-712 reasoning, confirmed F3+F4 Medium) → Codex R3 (confirmed F3/F4/F5, caught MISSED F9 — cross-payer nonce burning) → Opus REQUEST_CHANGES (3 BLOCKING: F9 payer-scoped nonce key, F3 offline sig verify, F4 runtime guard; EIP-712 false positive correctly rejected).
) Address 3 Medium findings from the PR #15 security review plus follow-up hardening from an adversarial Codex pass. X402Bridge (EIP-3009 settlement): - F1: scope the nonce dedup key per (chainId, tokenAddress, from, nonce) to match on-chain authorizationState[authorizer][nonce] semantics. The old chainId:nonce key allowed cross-account / cross-token nonce burning. - F2: require an injected offline `verifyAuthorization` (discriminated-union config XOR `skipSignatureVerification`) and run it BEFORE claiming a nonce or settling on-chain, so invalid-signature spam can no longer burn nonces or force reverting, gas-wasting settlePayment() calls. Verification is delegated (not inlined) because @aastar/messaging has no chain deps / EIP-712 domain. Verifier failures fail closed. ChannelBridge (voucher settlement): - F3: add a constructor runtime guard so a config built dynamically cannot silently bypass voucher verification (compile-time union is not enough). - Reject vouchers whose cumulativeAmount exceeds the on-chain deposit before any sig/store/settle (prevents guaranteed-revert gas waste + channel lock). - verifyVoucherSig now fails closed on throw (consistent with X402Bridge). Docs/types: export X402AuthorizationParams; document the scoped NonceStore key format. Added unit tests for every change (bridges.test.ts: 314 passing). Note: claim-before-settle (no nonce release on transient settle failure) is a pre-existing anti-double-spend tradeoff, left as-is; a retry/pending state machine should be tracked separately.
🔒 Security fixes pushed (
|
clestons
left a comment
There was a problem hiding this comment.
Review — aastar-sdk#15 (security commits) · messaging bridge 安全加固
4-round PK verdict: APPROVE ✅
本 review 仅针对两个安全修复提交(37fa5d55 + 30b9f8c),不包含 WIP react hooks 部分。
(R2 Sonnet → R3 Codex → Opus;R1 跳过;Codex 零 findings,零 misses)
4 个安全修复全部实现正确,314 tests 全覆盖,Opus 无 blocking 项。
commit 37fa5d5 — react/xiaoheishu 安全修复
SSRF prevention — useGatewayStream.ts
function isAllowedGatewayUrl(raw: string): boolean {
try {
const parsed = new URL(raw);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch { return false; }
}
// 在构建 EventSource URL 前调用,阻断任意协议注入正确:new URL() 解析失败返回 false(fail-closed),仅放行 http/https ✓
输入验证 — usePayment.ts + PaymentModal.tsx
amount <= 0n前置 guard — 在任何状态变更前抛出 ✓message.slice(0, 280)截断(非拒绝) ✓parseAtomicUsdc: guards NaN/Infinity/负值/超 1B → 0n;Math.round(num*1e6)因 cap 保证在 safe integer 范围内,BigInt()安全 ✓
commit 30b9f8c — messaging bridge 安全加固(认真 review)
F1 — Nonce key 范围修复(跨账户 nonce burning 漏洞)
// OLD: const nonceKey = `${chainId}:${nonce}`;
// NEW:
const nonceKey = `${chainId}:${tokenAddress.toLowerCase()}:${from.toLowerCase()}:${nonce}`;- 与链上 EIP-3009
authorizationState[authorizer][nonce]语义完全对齐 ✓ - 测试:同 nonce 不同 payer → 两者均 settle(cross-account burning 已修复)✓
- 测试:同 nonce 不同 token → 两者均 settle(cross-token burning 已修复)✓
F2 — EIP-3009 离线签名验证注入(nonce claim 前执行)
// Step 2.5: BEFORE nonce claim
if (this.config.verifyAuthorization) {
let sigValid = false;
try { sigValid = await this.config.verifyAuthorization({...}); }
catch { sigValid = false; } // fail-closed: verifier 异常 = 无效签名
if (!sigValid) return { success: false, error: 'invalid_signature' };
}
// Step 3: nonce claim (只有合法签名才烧 nonce)- 验证在所有状态变更(nonce claim、链上 settle)之前 ✓
- 测试:invalid sig → nonce 未烧,后续合法重试成功 ✓
- TypeScript discriminated union + constructor runtime guard 双层强制 ✓
F3 — ChannelBridge constructor guard + 超额存款早拒
// 构造时强制配置验证器
if (!config.skipVoucherSigVerification && !config.verifyVoucherSig) {
throw new Error('ChannelBridge: verifyVoucherSig is required...');
}
// 在 sig/store/settle 全部之前检查超额
if (cumulativeAmount > state.depositedAmount) {
return { success: false, error: 'exceeds_deposit' };
}- 早拒防止 guaranteed-revert gas 浪费 + channel lock ✓
verifyVoucherSig同样 fail-closed(throws → valid=false)✓
Opus 建议(非阻断):
- claim-before-settle 防双花权衡(nonce 被烧后 RPC 瞬断 settle 失败 → 该 nonce 永久 stranded)— 已知设计取舍,建议后续独立 issue 跟踪 retry/pending 状态机
- ChannelBridge over-deposit 在签名前 check — 信息无泄露(depositedAmount 链上公开),建议加一行注释说明是有意的 pre-auth 早拒
- NonceStore key 格式应在测试中有 lint/assertion guard(opaque key 不可随意缩窄)
Verdict: APPROVE.
Summary
@aastar/react: React hooks wrapping@aastar/messagingSporeAgent@aastar/xiaoheishu: Component library for 小黑书 (decentralized lifestyle community)M14-A —
@aastar/reactSporeProviderSporeAgent, manages lifecycle (stopon unmount)useDmstreamAllMessagesuseGroupsendText/addMember/removeMemberuseConversationslistConversationsuseIdentitylinkDevice/unlinkDeviceusePaymentagent.sendDmencoded payloaduseGatewayStreamEventSourcehook for M10 HTTP gateway push eventsM14-B —
@aastar/xiaoheishuNoteCardapp.xiaohei.notecard with tip buttonFeedListonLoadMoreCreateNoteFormDmThreaduseDm)PaymentModalProfileCardSecurity fixes (post-review)
useGatewayStream: validate URL protocol (http/httpsonly) beforenew EventSource()usePayment: capmessageat 280 chars, guardamount <= 0nPaymentModal:parseAtomicUsdc()guardsNaN/Infinity/negative beforeBigInt()Tests
@aastar/react: 39 tests (vitest +@testing-library/react@16, jsdom)@aastar/xiaoheishu: 46 testsTest plan
pnpm --filter @aastar/react testpnpm --filter @aastar/xiaoheishu test<SporeProvider>+<DmThread>in a dev app, verify E2E DM round-trip<PaymentModal>, enter custom amount, verifytip()encodes correctly