Feat/cep8 explicit gating#75
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds CEP-8 “explicit_gating” payment interaction mode support across Nostr client/server transports, including negotiation tags, server-side authorization gating, and client-side auto-retry behavior for -32042/-32043.
Changes:
- Introduces explicit-gating server middleware with canonical invocation hashing + authorization store.
- Adds client negotiation/disclosure plumbing (
payment_interactiontags) and client auto-retry handling for-32042/-32043. - Refactors shared server payment utilities and adds unit + transport-level tests.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/transport/payments-flow.test.ts | Adds a transport-level test for explicit-gating behavior and retry flow. |
| src/transport/nostr-server/session-store.ts | Stores requested/effective payment interaction on server sessions. |
| src/transport/nostr-server/outbound-response-router.ts | Discloses effective payment_interaction on first response (CEP-8). |
| src/transport/nostr-server/inbound-coordinator.ts | Parses client payment_interaction request and sets per-request context. |
| src/transport/nostr-server-transport.ts | Exposes API to configure supported payment interaction mode. |
| src/transport/nostr-client/server-metadata-store.ts | Persists server-disclosed effective payment interaction mode. |
| src/transport/nostr-client/outbound-sender.ts | Stores raw JSON-RPC request into correlation metadata for retries. |
| src/transport/nostr-client/inbound-coordinator.ts | Captures server-disclosed payment_interaction tag and passes event id to response handler. |
| src/transport/nostr-client/correlation-store.ts | Extends pending request state to include the raw JSON-RPC request. |
| src/transport/nostr-client-transport.ts | Exposes get/set API for payment interaction negotiation and forwards response context. |
| src/transport/middleware.ts | Extends inbound middleware context with paymentInteraction. |
| src/transport/capability-negotiator.ts | Advertises requested payment_interaction tag from the client. |
| src/payments/types.ts | Adds explicit-gating types: interaction mode, error data shapes, canonical identity. |
| src/payments/server-transport-payments.ts | Wires explicit-gating middleware + discovery tags into server transport. |
| src/payments/server-payments.ts | Refactors shared helpers into a new utils module; adds paymentInteraction option. |
| src/payments/server-payments-utils.ts | New module for timeout/capability matching and resolvePrice type guards. |
| src/payments/server-explicit-gating.ts | Implements explicit-gating server middleware returning -32042/-32043. |
| src/payments/server-explicit-gating.test.ts | Unit tests for explicit-gating server middleware. |
| src/payments/constants.ts | Adds explicit-gating error codes and negotiation error code constant. |
| src/payments/client-payments.ts | Adds client-side explicit-gating handling, auto-retry, and pending backoff. |
| src/payments/client-payments.test.ts | Adds tests for -32042/-32043 handling and retry behavior. |
| src/payments/canonical-identity.ts | Computes canonical invocation hash/identity using JSON canonicalization + SHA-256. |
| src/payments/canonical-identity.test.ts | Unit tests for canonical hashing determinism and identity composition. |
| src/payments/authorization-store.ts | Adds LRU+TTL store for pending/granted paid authorizations. |
| src/payments/authorization-store.test.ts | Unit tests for authorization store behavior (TTL, pending, eviction). |
| src/core/constants.ts | Adds PAYMENT_INTERACTION to Nostr tag constants. |
| package.json | Adds json-canonicalize dependency. |
| bun.lock | Locks json-canonicalize dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@ContextVM-org Up for review! Lmk if the implementation need refining? |
|
Thanks for this — it covers a lot of spec ground and the tests are thorough. I found two blocking issues, several significant ones, and a pervasive code-style violation that needs addressing across the diff. 🔴 Blocking1. Server ignores per-session
Fix: Both middlewares need an early guard: if (ctx.paymentInteraction !== 'explicit_gating') { await forward(message); return; }Then 2.
🟠 Significant3. CEP-8 says 4.
5. Unbounded
6. Retry/backoff behavior doesn't match the PR description The description claims "exponential backoff" with a "precise mathematically computed
Either implement true exponential backoff or update the description. 7. Confusing handler logic in the explicit-gating path In 8. Long stream-of-consciousness comments
🟡 Inline
|
|
The fix commits correctly resolve the two original blockers and clean up the inline-import pattern. Here's what still needs attention before merge. 🔴 Missing Tests (blocking)No test covers either of these paths, and both touch claim of 100% backward compatibility:
🟠 Redundancy~55 lines of price-resolution pipeline duplicated between 🟡 Consistency Gaps
🟢 FormattingTwo regressions from commit
|
🔴 Blocker — Branch is 8 commits / 2 days behind master
Also: Action: rebase onto current 🟠 New regression — set-on-read
|
| Prior item | Status |
|---|---|
| Redundant ~55-line price-resolution pipeline | ✅ Extracted to resolveAndInitiatePayment() in server-payments-utils.ts, used by both middlewares |
| Explicit-gating middleware silently dropped duplicate PMI processors | ✅ Now warns (matches transparent middleware) |
-32602 unsupported payment_interaction untested |
✅ Integration test added in nostr-server-transport.test.ts |
Legacy transparent client vs explicit_gating server untested |
✅ Unit test added (server-explicit-gating.test.ts:109); full e2e not added but the guard is simple and unit-covered |
markDiscoveryTagsSent() managing two unrelated flags |
✅ Flag moved into getNegotiationTags() — but see the regression above |
| Oversized paths skip mark-as-sent/disclosed | ✅ Documented with explicit comments at outbound-sender.ts:101 and outbound-response-router.ts:182 |
Grant TTL Math.min(verifyTimeoutMs, paymentTtlMs) |
✅ Now uses paymentRequired.ttl * 1000 ?? paymentTtlMs |
Formatting regressions (outbound-sender.ts:4, correlation-store.ts:2) |
✅ Both fixed |
All 64 payment tests + 44 transport/payment-flow tests pass; tsc --noEmit is clean.
🟡 Minor / cleanup
-
Dead constant.
NOSTR_TAGS.PAYMENT_INTERACTION(core/constants.ts:126) is defined but never referenced — every call site uses the string literal'payment_interaction'directly (6 occurrences). Either use it or drop it. Prefeer use it to avoid magic strings -
Convoluted
retry_after.server-explicit-gating.ts:100-106—Math.min(2, Math.ceil(remaining/1000)) || 2always reduces to1or2(and the|| 2only fires when remaining is 0). If the intent is "suggest 2 s, or less if the pending window is about to expire", the current expression already does that, but the trailing|| 2is dead in practice. Simplify toMath.min(2, Math.max(1, Math.ceil(remaining/1000)))or just a constant with a comment. -
MAX_RETRIES = 5for-32043is hardcoded and tight. Withretry_after=2and1.5^nbackoff capped at 10 s, the client gives up after ~26 s of cumulative wait. Server-side verification can legitimately run up tomin(verifyTimeoutMs, paymentTtlMs)(default 300 s). A slow-but-successful verification that takes >26 s surfaces a-32043error to the caller even though a paid authorization lands moments later. Consider exposingmaxPendingRetries/pendingRetryBackoffonClientPaymentsOptions, or raising the default. -
No e2e for the
-32043race. The pending path is unit-tested on both sides, but there's no end-to-end test exercising: client pays → server verification slow → client retry hits-32043→ backoff → eventual success. The happy-path-32042e2e exists; the pending-race e2e would lock in the interaction betweenAuthorizationStore.trySetPending, the asyncverifyPayment, and the client's backoff loop. Nice-to-have, not blocking.
Recommended next steps
- Rebase onto
masterand resolve conflicts (the blocker). Re-runbun test— the master test files will start running and may surface issues against the new payment code. - Decide on the
getNegotiationTagsset-on-read issue — I'd revert to post-send flagging and solve the naming concern separately; add the stateless regression test. - Sweep the minor items (dead constant,
retry_aftersimplification, configurable retry budget). - After rebase, this should be ready to merge.
Resolves ContextVM#74 by adding full support for the explicit gating payment lifecycle in the ContextVM TypeScript SDK. Includes server middleware for tracking authorization states, client support for auto-retrying intercepted -32042/-32043 errors, and transport modifications to negotiate payment modes.
- Fix integration test failure: remove getPendingRequestForEventId() guard from -32042/-32043 handlers. resolveResponse() consumes the correlation entry before the payment wrapper reads it, so the lookup always returned undefined. Use rawRequestCache as the authoritative source for retry requests instead. - Fix TS2769/TS2339 typecheck errors in client-payments.test.ts - Fix TS6133 unused parameter errors in server-explicit-gating.test.ts - Remove as any cast in server-transport-payments.ts - Wire -32602 error in inbound-coordinator.ts for unsupported modes - Delete accidental package-lock.json (project uses bun.lock)
…eraction learning - Server inbound-coordinator: set effectivePaymentInteraction = 'transparent' before early return when rejecting unsupported explicit_gating. Prevents inconsistent session state (requestedPaymentInteraction set but effectivePaymentInteraction undefined). - Client inbound-coordinator: move payment_interaction tag parsing before the initialize-event early return. The server sends this tag once on its first response, which was previously unreachable because the first event triggers setInitializeEvent() + return before the tag parsing code.
785e86a to
eabfc69
Compare
…erage
Spec compliance:
- -32602 (unsupported payment_interaction) now carries data: { requested, supported }
- -32042/-32043 instructions emphasize retrying with the same method and params
- Client effective-mode guard: declines transparent payment_required when the
server did not accept explicit_gating for the session
Behavior-preserving refactor:
- AuthorizationStore simplified to single-use grants (dropped count/remaining/
hasPending); surface confirmed internal-only
- Extracted shared buildProcessorsByPmi so the duplicate-PMI warning fires once
and the registry is built once across both server middlewares
- Split maybeHandlePaymentRequired into explicit-gating + transparent handlers
behind a slim classifier
- Extracted synthesizePaymentError / dispatchAndForward; consolidated all
client-side decline-error synthesis through the single helper
Tests (+12 pass):
- Explicit-gating e2e: server disclosure, -32043 pending race, user-decline,
handler-error, verify-fail-fresh-invoice, -32602 negotiation
- Transparent e2e: resolvePrice rejection -> -32000
- Unit: middleware lifecycle, verify-failure fresh -32042, onPaymentRequired
reject contract, -32043 retry exhaustion
429 -> 441 pass / 0 fail / 5 skip; tsc clean.
docs/: cep-8-update.md (spec reference), pr-improvements.md (review + decisions)
|
Summary of changes since last review — closes the remaining CEP-8 spec gaps, simplifies internal state, and locks the explicit-gating flow with integration tests. All behavior-preserving refactors verified against the existing suite (429 → 441 pass / 0 fail / 5 skip, Spec compliance (correctness)
Behavior-preserving refactor
Test coverage (+12 passing)Explicit-gating e2e (
Transparent e2e: Unit: middleware lifecycle (verify→grant→claim→forward), verify-failure/timeout → fresh |
|
@ContextVM-org Reviewed and ran the newly added test along with corresponding checks, seems great! We can merge it. |
Feat: CEP-8 Explicit Gating Lifecycle
Resolves: #74
Reference Spec: ContextVM/contextvm-docs#44
Description
This PR introduces full support for the Explicit Gating payment lifecycle (
explicit_gatingmode) in the ContextVM TypeScript SDK, as mandated by the latest CEP-8 specification updates.Previously, the SDK only supported the default
transparentnotification-based payment flow. With this update, servers can now strictly gate priced capabilities by returning JSON-RPC error responses (-32042 Payment Requiredand-32043 Payment Pending), effectively blocking execution until a verifiable payment is made.Key Changes & Architecture
-32042and-32043.payment_interactionto negotiation tags.CanonicalInvocationIdentityutilizing RFC-8785 JSON canonicalization (JCS) and SHA-256 to ensure idempotent matching between paid authorizations and retried executions.createExplicitGatingMiddleware):AuthorizationStorewith strict check-and-set atomicity.-32043with a precise mathematically computedretry_afterif a request races against an active payment verification.ClientSessionto track bothrequestedPaymentInteractionandeffectivePaymentInteraction.payment_interactionmode and safely fallback totransparentmode to prevent injection of untyped interactions.withClientPayments):onPaymentRequiredhandler hook.-32043errors capped at 5MAX_RETRIES, alongside robust memory management (timer cancellation on transport termination).Testing
AuthorizationStoreconcurrency, JCS hashing, and edge cases. (Timings have been buffered to prevent CI flakiness).payments-flow.test.ts) that runs a client request → intercepts-32042→ delegates to user payment logic → auto-retries → consumes authorization → and successfully returns the result.Notes-
AuthorizationStore.trySetPendingrelies on in-memory Maps and is therefore strictly single-process. Extensive doc comments have been added noting that distributed environments should implement an external Redis Redlock using theCanonicalInvocationIdentity.transparentcapability.