@odatano/x402 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `@odatano/x402` are documented here. The format follows [
4
4
 
5
5
  **Pre-1.0 caveat:** minor versions may include breaking changes until `1.0.0`.
6
6
 
7
+ ## [0.3.1] - 2026-05-15
8
+
9
+ ### Fixed
10
+ - **`gateService` now emits the canonical x402 v2 body on the wire** instead of CAP's OData-wrapped shape. The gate writes `httpRes.status(402).json(body)` directly when an Express response is reachable, then calls `req.reject(402, ...)` as the chain-terminator: the synchronous throw stops CAP's handler pipeline so the gated `on` handler never runs, and CAP's render attempt no-ops on `headersSent`. Non-HTTP transports (event invocations, `$batch` reuse) fall back to plain `req.reject`. Validated against `@sap/cds ^9`. Third-party x402 clients now interop with CAP-gated services without any unwrap shim. Closes the "Known issues" item from 0.3.0.
11
+
12
+ ### Removed (breaking)
13
+ - **`unwrapCapEnvelope`** helper and its calls from `x402Fetch` / `x402Axios`. With the server fix above, the v2 body lands at the top level on the wire and the defensive unwrap is dead code. The export is gone; consumers who pulled it in (e.g. for wrapping the wrappers) should remove the import. **Pair the upgrade**: if you upgrade the `@odatano/x402` client to 0.3.1, upgrade the server in the same step, since 0.3.1 clients no longer unwrap a 0.3.0-style wrapped body.
14
+
15
+ ### Changed
16
+ - `srv/middleware/cap.ts` , new `send402` helper and `getHttpRes` accessor; the one 402 emit site routes through `send402`. The two 500 `req.reject` paths (pricing-resolver throw, facilitator throw) are unchanged.
17
+ - Test suite: 232 tests across 21 suites. CAP middleware tests gained 3 cases (canonical-wire-shape regression, `headersSent` defensive fallback, no-`http.res` transport fallback). Client tests dropped 7 cases tied to `unwrapCapEnvelope`.
18
+
7
19
  ## [0.3.0] - 2026-05-15
8
20
 
9
21
  ### Added
@@ -16,7 +28,7 @@ All notable changes to `@odatano/x402` are documented here. The format follows [
16
28
  - **Browser-buyer example** , `examples/browser-buyer/` Vite scaffold showing CIP-30 wallet + `x402Fetch` wiring. Documents the typical "unsigned-from-server, signed-by-wallet" architecture (server exposes `POST /pay/intent` via `buildUnsignedPaymentTx`; browser signs via CIP-30). Includes CORS notes for cross-origin deployments.
17
29
 
18
30
  ### Fixed
19
- - **`x402Fetch` / `x402Axios` now interop with `gateService`** out of the box. CAP's `req.reject(402, body)` wraps the canonical v2 body inside its standard OData error envelope (`{ error: { message: "<json>", code: "402", ... } }`), so previous client wrappers saw `body.x402Version === undefined` and bailed without retrying. Both clients now defensively unwrap the OData envelope before validating shape. Reported by the CHAINFEED team; matches the workaround they were shipping in `scripts/buyer-pay-and-fetch.ts`. New helper `unwrapCapEnvelope` is exported for consumers wrapping the wrappers.
31
+ - **`x402Fetch` / `x402Axios` now interop with `gateService`** out of the box. CAP's `req.reject(402, body)` wraps the canonical v2 body inside its standard OData error envelope (`{ error: { message: "<json>", code: "402", ... } }`), so previous client wrappers saw `body.x402Version === undefined` and bailed without retrying. Both clients now defensively unwrap the OData envelope before validating shape.
20
32
 
21
33
  ### Known issues
22
34
  - The CAP `gateService` still emits 402 responses wrapped in CAP's OData error envelope on the wire (because `req.reject` is the only documented abort path). Third-party x402 clients hitting a CAP-gated server will see the wrapped shape; only `@odatano/x402`'s own clients unwrap it. Direct-write-to-`req.http.res` is the planned symmetric fix but needs validation against `@sap/cds` ^9 internals; tracked for v0.3.1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odatano/x402",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "x402 Cardano-v2 payment library for SAP CAP applications",
5
5
  "license": "Apache-2.0",
6
6
  "main": "srv/index.js",
@@ -54,9 +54,7 @@ function x402Axios(instance, opts) {
54
54
  }
55
55
  const cfg = error.config;
56
56
  const retries = Number(cfg[RETRY_KEY] ?? 0);
57
- // CAP-style servers wrap the v2 body inside an OData error envelope.
58
- // Defensively unwrap before validating shape.
59
- const body = (0, errors_1.unwrapCapEnvelope)(error.response.data);
57
+ const body = error.response.data;
60
58
  const status = error.response.status;
61
59
  if (retries >= maxRetries) {
62
60
  if (opts.errorOnFailure) {
@@ -60,27 +60,6 @@ export declare class X402PaymentError extends Error {
60
60
  readonly cause?: unknown;
61
61
  constructor(init: X402PaymentErrorInit);
62
62
  }
63
- /**
64
- * Defensively unwrap a CAP / OData error envelope around a v2 body.
65
- *
66
- * Background: `gateService` in this package historically (≤ v0.2)
67
- * reaches a 402 via `req.reject(402, JSON.stringify(body))`, which
68
- * CAP wraps into its standard OData error shape, putting the canonical
69
- * v2 body inside `error.message` as a JSON string:
70
- *
71
- * { "error": { "message": "{\"x402Version\":2, ... }", "code": "402", ... } }
72
- *
73
- * The Express middleware emits the v2 body at the top level directly.
74
- * This helper detects the CAP wrap and returns the unwrapped candidate
75
- * so client wrappers can validate v2 shape uniformly. Non-CAP bodies
76
- * pass through untouched.
77
- *
78
- * Symmetric server-side fix (emit canonical body even through CAP) is
79
- * a separate concern, deferred until we can validate the CAP-version
80
- * specific behaviour. The unwrap below keeps `x402Fetch` /
81
- * `x402Axios` working against both shapes.
82
- */
83
- export declare function unwrapCapEnvelope(parsed: unknown): unknown;
84
63
  /**
85
64
  * Parse the (server, canonical) error string from a `PaymentRequirementsBody`.
86
65
  *
@@ -31,7 +31,6 @@
31
31
  */
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
33
  exports.X402PaymentError = void 0;
34
- exports.unwrapCapEnvelope = unwrapCapEnvelope;
35
34
  exports.parseErrorCode = parseErrorCode;
36
35
  exports.paymentErrorFromBody = paymentErrorFromBody;
37
36
  /**
@@ -71,42 +70,6 @@ class X402PaymentError extends Error {
71
70
  }
72
71
  }
73
72
  exports.X402PaymentError = X402PaymentError;
74
- /**
75
- * Defensively unwrap a CAP / OData error envelope around a v2 body.
76
- *
77
- * Background: `gateService` in this package historically (≤ v0.2)
78
- * reaches a 402 via `req.reject(402, JSON.stringify(body))`, which
79
- * CAP wraps into its standard OData error shape, putting the canonical
80
- * v2 body inside `error.message` as a JSON string:
81
- *
82
- * { "error": { "message": "{\"x402Version\":2, ... }", "code": "402", ... } }
83
- *
84
- * The Express middleware emits the v2 body at the top level directly.
85
- * This helper detects the CAP wrap and returns the unwrapped candidate
86
- * so client wrappers can validate v2 shape uniformly. Non-CAP bodies
87
- * pass through untouched.
88
- *
89
- * Symmetric server-side fix (emit canonical body even through CAP) is
90
- * a separate concern, deferred until we can validate the CAP-version
91
- * specific behaviour. The unwrap below keeps `x402Fetch` /
92
- * `x402Axios` working against both shapes.
93
- */
94
- function unwrapCapEnvelope(parsed) {
95
- if (!parsed || typeof parsed !== 'object')
96
- return parsed;
97
- const maybeError = parsed.error;
98
- if (!maybeError || typeof maybeError !== 'object')
99
- return parsed;
100
- const msg = maybeError.message;
101
- if (typeof msg !== 'string')
102
- return parsed;
103
- try {
104
- return JSON.parse(msg);
105
- }
106
- catch {
107
- return parsed;
108
- }
109
- }
110
73
  /**
111
74
  * Parse the (server, canonical) error string from a `PaymentRequirementsBody`.
112
75
  *
@@ -57,8 +57,7 @@ function x402Fetch(opts) {
57
57
  let body = lastBody;
58
58
  if (!body) {
59
59
  try {
60
- const raw = await res.clone().json();
61
- body = (0, errors_1.unwrapCapEnvelope)(raw);
60
+ body = await res.clone().json();
62
61
  }
63
62
  catch { /* fall through */ }
64
63
  }
@@ -75,13 +74,9 @@ function x402Fetch(opts) {
75
74
  }
76
75
  // Parse 402 body. If it's not a v2 PaymentRequirementsBody we
77
76
  // bail with the original response (or throw under errorOnFailure).
78
- // Some servers (CAP-gated ones using req.reject) wrap the v2 body
79
- // inside an OData error envelope, unwrap defensively before the
80
- // x402Version check.
81
77
  let body;
82
78
  try {
83
- const raw = await res.clone().json();
84
- body = (0, errors_1.unwrapCapEnvelope)(raw);
79
+ body = await res.clone().json();
85
80
  }
86
81
  catch {
87
82
  if (opts.errorOnFailure) {
@@ -72,6 +72,32 @@ function getResourceUrl(req, opts) {
72
72
  const httpReq = req;
73
73
  return httpReq.http?.req?.originalUrl ?? httpReq.http?.req?.url ?? `cap://${req.event}`;
74
74
  }
75
+ function getHttpRes(req) {
76
+ return req.http?.res;
77
+ }
78
+ /**
79
+ * Emit a 402 with the canonical x402 v2 body on the wire.
80
+ *
81
+ * When an Express response is reachable we write the body ourselves so
82
+ * third-party x402 clients see `{ x402Version: 2, accepts: [...] }` at
83
+ * the top level rather than CAP's OData-wrapped `{ error: { message:
84
+ * "<v2-json-string>", ... } }`. The `req.reject` call is always made:
85
+ * its synchronous throw is what terminates CAP's handler pipeline (so
86
+ * the gated `on` handler never runs); CAP's own render attempt no-ops
87
+ * because `headersSent` is already true. Validated against `@sap/cds ^9`.
88
+ *
89
+ * For non-HTTP transports (event invocations, `$batch` reuse, tests
90
+ * without `http.res`) we skip the direct write and let `req.reject` do
91
+ * the whole job — the wrap is irrelevant when there's no HTTP buyer.
92
+ */
93
+ function send402(req, body) {
94
+ const httpRes = getHttpRes(req);
95
+ if (httpRes && !httpRes.headersSent) {
96
+ httpRes.status(402).json(body);
97
+ }
98
+ req
99
+ .reject(402, JSON.stringify(body));
100
+ }
75
101
  /**
76
102
  * Attach the x402 gate to a CAP ApplicationService. Returns the service
77
103
  * (chainable) so callers can fluently wire multiple middlewares.
@@ -207,8 +233,7 @@ function gateService(srv, opts) {
207
233
  if (result.txHash)
208
234
  body.transaction = result.txHash;
209
235
  }
210
- req
211
- .reject(402, JSON.stringify(body));
236
+ send402(req, body);
212
237
  });
213
238
  return srv;
214
239
  }