[WIP]feat(spore): Spore Protocol — @aastar/messaging + @aastar/message-relay (M1-M13)#14
[WIP]feat(spore): Spore Protocol — @aastar/messaging + @aastar/message-relay (M1-M13)#14jhfnetboy wants to merge 23 commits into
Conversation
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.
Review findings addressed (commit 1f6732b)CRIT-1 — UserOpBridge authorizationSig ambiguity ✅Fixed misleading JSDoc. The CRIT-2 — In-memory nonce store loses state on restart ✅Added CRIT-3 — ChannelBridge verifyVoucherSig optional ✅
H1 — maxValidBeforeWindowSeconds default too large ✅Default reduced from 7 days → 24 hours (86400s). H2 — callData layout assumption undocumented ✅
All 212 tests passing, build clean. |
M10 + M11 completed (commits bf5c94d, 4af2af9)M10: SporeHttpGateway — HTTP/SSE REST GatewayA XMTP-compatible REST API that lets external clients (Python, Go, mobile, etc.) use Spore Protocol without a Nostr SDK. API surface:
Security measures (from security review):
M11: Mainnet HardeningRateLimiter (token bucket):
MainnetChecklist (pre-deploy audit):
Total tests: 258 passing (10 test files) |
Review + Simplify + Security Fix (commit 444bb1f)Code review and security review findings applied: CRIT: timingSafeEqual — constant-time comparison via zero-paddingRemoved the dummy-work pattern for unequal-length strings. Both inputs are now zero-padded to equal length and compared with a single HIGH: RateLimiter O(n²) → O(1)Replaced MED: Remove internal error string leakage in ChannelBridge
MED: FileNonceStore — handle corrupted JSON gracefullyConstructor now catches Simplification:
258 tests passing, build clean. |
…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
444bb1f to
fc0b4a6
Compare
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.
Summary
Spore Protocol SDK — @aastar/messaging + @aastar/message-relay
Milestones delivered
Total: 305 unit tests, 12 test files, all passing
Security fixes (code review + per-milestone review)
M1–M9 (original):
verifyEvent) before trusting sender pubkeyverifyVoucherSigcallback in ChannelBridge (prevents gas-waste spam)verifyEventon all relay-returned kind:0 / kind:10001 events (relay spoofing prevention)extrafieldverifyEventon all relay-returned kind:443 KeyPackages before useverifyEventbefore decrypting kind:445 group messagesM10–M11:
crypto.timingSafeEqualwith zero-padding for constant-time Bearer token comparisonmaxBodyBytes(65536) with drain-then-respond before 413 — prevents memory DoSmaxSseClients(100) +requestTimeoutMs(30s) — prevents slow-client DoSSet(replaced O(n²) array pattern)FileNonceStorehandles corrupted JSON gracefully (warn + empty reset)M12–M13:
SeenSetusesMapfor O(1) deduplication across transportsArchitecture
Test plan
pnpm --filter @aastar/messaging test— 305 tests passpnpm --filter @aastar/messaging build— clean TypeScript compilationpnpm --filter @aastar/message-relay build— clean TypeScript compilationpnpm -r lint— no lint errors