Skip to content

Feat/cep8 explicit gating#75

Open
1amKhush wants to merge 10 commits into
ContextVM:masterfrom
1amKhush:feat/cep8-explicit-gating
Open

Feat/cep8 explicit gating#75
1amKhush wants to merge 10 commits into
ContextVM:masterfrom
1amKhush:feat/cep8-explicit-gating

Conversation

@1amKhush

Copy link
Copy Markdown
Contributor

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_gating mode) in the ContextVM TypeScript SDK, as mandated by the latest CEP-8 specification updates.

Previously, the SDK only supported the default transparent notification-based payment flow. With this update, servers can now strictly gate priced capabilities by returning JSON-RPC error responses (-32042 Payment Required and -32043 Payment Pending), effectively blocking execution until a verifiable payment is made.

Key Changes & Architecture

  • Types & Constants:
    • Introduced standard error codes -32042 and -32043.
    • Added payment_interaction to negotiation tags.
    • Implemented CanonicalInvocationIdentity utilizing RFC-8785 JSON canonicalization (JCS) and SHA-256 to ensure idempotent matching between paid authorizations and retried executions.
  • Server Middleware (createExplicitGatingMiddleware):
    • Tracks paid executions using a new TTL-bounded AuthorizationStore with strict check-and-set atomicity.
    • Emits -32043 with a precise mathematically computed retry_after if a request races against an active payment verification.
  • Transport Negotiation:
    • Added capabilities to ClientSession to track both requestedPaymentInteraction and effectivePaymentInteraction.
    • Transports automatically validate the payment_interaction mode and safely fallback to transparent mode to prevent injection of untyped interactions.
  • Client Handling (withClientPayments):
    • Intercepts explicit gating error responses upstream, completely shielding the main MCP caller.
    • Delegates resolution strictly to the user's onPaymentRequired handler hook.
    • Supports completely autonomous auto-retry of the exact original request upon successful payment.
    • Includes resilient exponential backoff for -32043 errors capped at 5 MAX_RETRIES, alongside robust memory management (timer cancellation on transport termination).

Testing

  • Unit Tests: Full coverage for AuthorizationStore concurrency, JCS hashing, and edge cases. (Timings have been buffered to prevent CI flakiness).
  • Integration/E2E Tests: A complete end-to-end flow (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-

  • The atomicity inside AuthorizationStore.trySetPending relies 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 the CanonicalInvocationIdentity.
  • Backward compatibility is 100% maintained. Legacy clients not advertising the new mode will continue running through the default transparent capability.

1amKhush added 2 commits June 11, 2026 16:43
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.
Copilot AI review requested due to automatic review settings June 11, 2026 11:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_interaction tags) 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.

Comment thread src/payments/client-payments.ts Outdated
Comment thread src/payments/client-payments.ts Outdated
Comment thread src/payments/client-payments.ts
Comment thread src/payments/server-explicit-gating.ts
Comment thread src/payments/server-explicit-gating.ts Outdated
Comment thread src/payments/server-transport-payments.ts Outdated
Comment thread src/payments/canonical-identity.ts Outdated
1amKhush added 5 commits June 11, 2026 17:13
- 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)
@1amKhush

Copy link
Copy Markdown
Contributor Author

@ContextVM-org Up for review! Lmk if the implementation need refining?

…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.
@ContextVM-org

Copy link
Copy Markdown
Contributor

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.


🔴 Blocking

1. Server ignores per-session payment_interaction negotiation

withServerPayments installs only one middleware based on the global config option. createExplicitGatingMiddleware gates every priced invocation unconditionally — it never reads ctx.paymentInteraction. This means a legacy client that never opted into explicit_gating will still receive -32042 errors it cannot handle. CEP-8 is explicit that explicit gating is opt-in, and the PR claims 100% backward compatibility.

Fix: Both middlewares need an early guard:

if (ctx.paymentInteraction !== 'explicit_gating') { await forward(message); return; }

Then withServerPayments should install both middlewares so the effective session mode selects the behavior.

2. canonical-identity.ts uses Node crypto.createHash

src/payments/canonical-identity.ts:4 imports createHash from crypto. This is a public-API file and must remain browser-safe per AGENTS.md. Use the existing dependency @noble/hashes (sha256) or crypto.subtle.digest('SHA-256', …).


🟠 Significant

3. -32042 response omits instructions

CEP-8 says error.data.instructions SHOULD be present. createExplicitGatingMiddleware builds data: { payment_options: [...] } without it. Add a standard instruction string.

4. payment_interaction tag sent on every request

ClientCapabilityNegotiator.getNegotiationTags emits the tag on every outbound request. CEP-8 says clients SHOULD send it at most once, on the first direct message. Track a hasSentPaymentInteraction flag.

5. Unbounded rawRequestCache

withClientPayments stores every outgoing request in a plain Map and only cleans entries on non-explicit-gating terminal responses. Over a long-lived transport this is an unbounded memory leak. Wrap it in an LRU.

6. Retry/backoff behavior doesn't match the PR description

The description claims "exponential backoff" with a "precise mathematically computed retry_after". In practice:

  • retry_after is hard-capped at Math.min(2, …) — effectively always ≤ 2 seconds.
  • The client just sleeps for retry_after * 1000 ms with no exponential multiplier.
  • MAX_RETRIES is hard-coded and not configurable.

Either implement true exponential backoff or update the description.

7. Confusing handler logic in the explicit-gating path

In maybeHandlePaymentRequired the explicit -32042 branch loops over payment_options and checks handler/canHandle, but never calls handler.handle. All decision-making is on onPaymentRequired. Extract the explicit-gating path into its own function and drop the dead handler loop.

8. Long stream-of-consciousness comments

client-payments.ts contains speculative design notes ("Wait, we need to create a new ID…", "But actually we are the transport…") that should not be committed. Per AGENTS.md comments should be brief and explain "why", not document the author's thought process.


🟡 Inline import('…').Type pattern (forbidden)

The diff uses inline type imports extensively — for example:

// Instead of a top-level import:
import('./types.js').PaymentInteractionMode
import('../../payments/types.js').PaymentInteractionMode
import('@contextvm/mcp-sdk/types.js').JSONRPCErrorResponse

This appears across 15+ files including:

AGENTS.md says: "All relative imports must include the .js extension to ensure ESM compatibility." These import('…') expressions are ESM-incompatible dynamic imports that also add visual noise and repetition. Replace all of them with top-level import type { … } statements. This also reduces the number of lines in the diff.


🟢 Minor / Code quality


✅ What's solid

  • Constants, types, and tag structures are well-aligned with CEP-8.
  • AuthorizationStore has clean single-process atomicity with good documentation about distributed limits.
  • The client wrapper correctly intercepts -32042/-32043 upstream and auto-retries transparently.
  • The E2E test in payments-flow.test.ts:1014 exercises the full round-trip.

Summary

The two blockers (per-session middleware selection + Node crypto usage) must be resolved before merge. The inline-import pattern should be replaced project-wide. Once those are fixed and the retry/backoff claims are reconciled with the implementation, this will be a clean PR.

@ContextVM-org

Copy link
Copy Markdown
Contributor

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:

  • Legacy transparent client vs server configured with explicit_gating — the ctx.paymentInteraction guard in both middlewares handles this now, but without a test it regresses silently
  • -32602 unsupported payment_interaction error — the server inbound coordinator returns this when a client requests explicit_gating but the server doesn't support it; the path is untested

🟠 Redundancy

~55 lines of price-resolution pipeline duplicated between server-payments.ts and server-explicit-gating.ts. Steps 1–7 (processor resolution → resolve price → rejection/waiver → payment creation → timeout computation) are identical; only the response format diverges at step 8. Extracting a shared resolveAndInitiatePayment() would eliminate the duplication and also fix the missing duplicate-PMI-processor warning in the explicit-gating middleware (the transparent middleware warns — the explicit one silently drops duplicates).


🟡 Consistency Gaps

  • markDiscoveryTagsSent() now manages two unrelated flagshasSentDiscoveryTags (conditional set) and hasSentPaymentInteraction (unconditional set). The method name doesn't describe its expanded responsibility. Moving hasSentPaymentInteraction = true into getNegotiationTags() (set-on-read) would keep each concern self-contained.
  • Oversized transfer paths skip mark-as-sent/disclosed callsoutbound-sender.ts:164 and outbound-response-router.ts:182 have early returns that bypass markDiscoveryTagsSent() and maybeAppendPaymentInteractionDisclosure(). Low risk in practice (oversized triggers only for large payloads, negotiation happens early with small messages), but worth a comment or a follow-up issue.
  • Grant TTL uses Math.min(verifyTimeoutMs, paymentTtlMs) — a slow verification can exhaust most of the authorization window. Consider using the original payment option TTL instead.

🟢 Formatting

Two regressions from commit 61173bf:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants