@odatano/x402 0.1.0 → 0.3.0

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.
Files changed (64) hide show
  1. package/.github/workflows/test.yaml +49 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +29 -281
  4. package/cds-plugin.js +2 -0
  5. package/db/x402-grants.cds +49 -0
  6. package/db/x402-receipts.cds +44 -0
  7. package/package.json +11 -4
  8. package/srv/bridge.d.ts +9 -12
  9. package/srv/bridge.js +10 -13
  10. package/srv/cds-augment.d.ts +17 -0
  11. package/srv/client/axios.d.ts +38 -0
  12. package/srv/client/axios.js +107 -0
  13. package/srv/client/envelope.d.ts +33 -0
  14. package/srv/client/envelope.js +52 -0
  15. package/srv/client/errors.d.ts +107 -0
  16. package/srv/client/errors.js +144 -0
  17. package/srv/client/fetch.d.ts +30 -0
  18. package/srv/client/fetch.js +141 -0
  19. package/srv/client/pay-handlers.d.ts +41 -0
  20. package/srv/client/pay-handlers.js +47 -0
  21. package/srv/client/types.d.ts +56 -0
  22. package/srv/client/types.js +10 -0
  23. package/srv/core/asset.d.ts +1 -1
  24. package/srv/core/decode.d.ts +2 -2
  25. package/srv/core/decode.js +5 -5
  26. package/srv/core/errors.js +3 -3
  27. package/srv/core/network.d.ts +1 -1
  28. package/srv/core/network.js +1 -1
  29. package/srv/core/requirements.d.ts +37 -5
  30. package/srv/core/requirements.js +43 -4
  31. package/srv/core/types.d.ts +68 -6
  32. package/srv/core/types.js +3 -3
  33. package/srv/core/validate.d.ts +31 -7
  34. package/srv/core/validate.js +84 -9
  35. package/srv/facilitator/adapter.d.ts +69 -0
  36. package/srv/facilitator/adapter.js +52 -0
  37. package/srv/facilitator/http.d.ts +43 -0
  38. package/srv/facilitator/http.js +99 -0
  39. package/srv/facilitator/nonce.d.ts +4 -4
  40. package/srv/facilitator/nonce.js +4 -4
  41. package/srv/facilitator/server.d.ts +68 -0
  42. package/srv/facilitator/server.js +167 -0
  43. package/srv/facilitator/settle.d.ts +2 -2
  44. package/srv/facilitator/settle.js +4 -4
  45. package/srv/facilitator/verify.d.ts +5 -5
  46. package/srv/facilitator/verify.js +19 -5
  47. package/srv/helpers/build-unsigned-tx.d.ts +5 -5
  48. package/srv/helpers/build-unsigned-tx.js +3 -3
  49. package/srv/helpers/verify-confirmed.d.ts +1 -1
  50. package/srv/helpers/verify-confirmed.js +1 -1
  51. package/srv/index.d.ts +11 -2
  52. package/srv/index.js +23 -3
  53. package/srv/middleware/cap.d.ts +53 -8
  54. package/srv/middleware/cap.js +87 -43
  55. package/srv/middleware/express.d.ts +22 -9
  56. package/srv/middleware/express.js +21 -21
  57. package/srv/middleware/grants.d.ts +64 -0
  58. package/srv/middleware/grants.js +113 -0
  59. package/srv/middleware/pricing.d.ts +41 -0
  60. package/srv/middleware/pricing.js +78 -0
  61. package/srv/middleware/receipts.d.ts +38 -0
  62. package/srv/middleware/receipts.js +68 -0
  63. package/srv/plugin.d.ts +2 -2
  64. package/srv/plugin.js +2 -2
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * Reference HTTP facilitator, the server side of `httpFacilitator()`.
4
+ *
5
+ * `httpFacilitator()` (in `./http.ts`) is the client wrapper that resource
6
+ * servers wire into their middleware via the `facilitator` option. This
7
+ * module is the matching server: a thin Express `Router` exposing the
8
+ * three endpoints documented in `docs/facilitator-protocol.md`:
9
+ *
10
+ * POST /verify-settle , runs the full pipeline through a `Facilitator`
11
+ * GET /supported , discovery
12
+ * GET /healthz , orchestrator health check
13
+ *
14
+ * The router is composable, consumers mount it under whatever path /
15
+ * port / TLS / CORS / rate-limiter they prefer, no opinions baked in.
16
+ * Auth is a single `auth(req)` hook, callers can implement bearer-token,
17
+ * mTLS, signed-request, OAuth, etc. There is NO default auth, an
18
+ * unconfigured router is open; document this loudly in deployment.
19
+ *
20
+ * The `onAccepted` callback CANNOT cross the HTTP boundary, the matching
21
+ * client (`httpFacilitator()`) invokes it locally after the remote returns
22
+ * `accepted`. To cover the facilitator-side audit need, this router
23
+ * exposes `onRejected` and `onPending` hooks, fired exactly once per
24
+ * non-accepted outcome, with the request available for context.
25
+ */
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.createFacilitatorRouter = createFacilitatorRouter;
31
+ const express_1 = __importDefault(require("express"));
32
+ const cds_1 = __importDefault(require("@sap/cds"));
33
+ const defaultLog = cds_1.default.log('x402');
34
+ async function runAudit(result, req, cb, log, label) {
35
+ if (!cb)
36
+ return;
37
+ try {
38
+ await cb(result, req);
39
+ }
40
+ catch (err) {
41
+ log.warn(`facilitator-server: ${label} hook failed (non-fatal):`, err);
42
+ }
43
+ }
44
+ /**
45
+ * Build the facilitator HTTP router. Mount on whatever path you like:
46
+ *
47
+ * const app = express();
48
+ * app.use('/v1', createFacilitatorRouter({ auth: bearer(token) }));
49
+ * app.listen(4040);
50
+ *
51
+ * The router parses its own JSON body, do not mount `express.json()`
52
+ * upstream with a smaller limit, the inner parser will see an already-
53
+ * parsed body and skip.
54
+ */
55
+ function resolveDefaultFacilitator() {
56
+ // Lazy require so the @odatano/core dependency is pulled in only when
57
+ // a consumer actually uses the default. Tests and consumers that pass
58
+ // a custom facilitator (e.g. a chained or mock one) never load core.
59
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
60
+ const { localFacilitator } = require('./adapter');
61
+ return localFacilitator();
62
+ }
63
+ function createFacilitatorRouter(opts = {}) {
64
+ const facilitator = opts.facilitator ?? resolveDefaultFacilitator();
65
+ const log = opts.logger ?? defaultLog;
66
+ const jsonLimit = opts.jsonLimit ?? '256kb';
67
+ const router = express_1.default.Router();
68
+ // Body parser scoped to this router. We DO NOT install a global JSON
69
+ // parser, callers may already have one with a different limit they
70
+ // don't want overridden for the rest of their app.
71
+ const json = express_1.default.json({ limit: jsonLimit });
72
+ // Auth runs BEFORE body parsing so we don't waste CPU parsing
73
+ // payloads for unauthenticated callers.
74
+ router.use(async (req, res, next) => {
75
+ if (!opts.auth)
76
+ return next();
77
+ try {
78
+ const ok = await opts.auth(req);
79
+ if (!ok) {
80
+ res.status(401).json({ error: 'unauthorized' });
81
+ return;
82
+ }
83
+ next();
84
+ }
85
+ catch (err) {
86
+ log.error('facilitator-server: auth hook threw', err);
87
+ res.status(500).json({ error: 'auth check failed' });
88
+ }
89
+ });
90
+ // ─── POST /verify-settle ─────────────────────────────────────────────
91
+ router.post('/verify-settle', json, async (req, res) => {
92
+ const body = req.body;
93
+ if (!body || typeof body !== 'object') {
94
+ res.status(400).json({ error: 'request body must be a JSON object' });
95
+ return;
96
+ }
97
+ if (!body.paymentHeader) {
98
+ // Don't 400 here, the facilitator returns a structured `rejected`
99
+ // with `missing_payment_header` so clients get a uniform shape.
100
+ }
101
+ if (!body.requirementsBody || typeof body.requirementsBody !== 'object') {
102
+ res.status(400).json({ error: 'requirementsBody is required' });
103
+ return;
104
+ }
105
+ // Strip onAccepted defensively, the wire schema doesn't carry it
106
+ // and we'd refuse to invoke a foreign callback anyway.
107
+ const args = {
108
+ paymentHeader: body.paymentHeader,
109
+ requirementsBody: body.requirementsBody,
110
+ ...(body.settlePollBudgetMs !== undefined ? { settlePollBudgetMs: body.settlePollBudgetMs } : {}),
111
+ ...(body.allowNoTtl ? { allowNoTtl: true } : {}),
112
+ };
113
+ let result;
114
+ try {
115
+ result = await facilitator.verifyAndSettle(args);
116
+ }
117
+ catch (err) {
118
+ log.error('facilitator-server: verifyAndSettle threw', err);
119
+ res.status(500).json({ error: err?.message ?? 'internal error' });
120
+ return;
121
+ }
122
+ res.json(result);
123
+ // Audit hooks fire AFTER the response so a slow audit DB never
124
+ // delays the buyer's settle round-trip.
125
+ if (result.kind === 'rejected') {
126
+ void runAudit(result, req, opts.onRejected, log, 'onRejected');
127
+ }
128
+ else if (result.kind === 'pending') {
129
+ void runAudit(result, req, opts.onPending, log, 'onPending');
130
+ }
131
+ });
132
+ // ─── GET /supported ──────────────────────────────────────────────────
133
+ router.get('/supported', async (_req, res) => {
134
+ if (!facilitator.supported) {
135
+ // Spec allows minimal facilitators to omit /supported. We surface
136
+ // 501 so the caller can distinguish "endpoint missing" from
137
+ // "facilitator unavailable".
138
+ res.status(501).json({ error: 'facilitator does not implement supported()' });
139
+ return;
140
+ }
141
+ try {
142
+ const s = await facilitator.supported();
143
+ res.json(s);
144
+ }
145
+ catch (err) {
146
+ log.error('facilitator-server: supported() threw', err);
147
+ res.status(500).json({ error: err?.message ?? 'internal error' });
148
+ }
149
+ });
150
+ // ─── GET /healthz ────────────────────────────────────────────────────
151
+ // Orchestrator-friendly liveness probe. NOT auth-gated, on purpose:
152
+ // k8s / Cloud Run probes don't carry bearer tokens. If the auth hook
153
+ // ran first and rejected the request, this route would be unreachable
154
+ // to probes. We register healthz on a side-router that bypasses auth.
155
+ // Implementation note: Express runs middlewares in registration order
156
+ // for a router, so we install healthz on a SEPARATE router and mount
157
+ // it before the auth middleware on the returned router. The cleanest
158
+ // way: build healthz on a sub-router and mount FIRST.
159
+ //
160
+ // (Restructure: rebuild the router with healthz prepended.)
161
+ const outer = express_1.default.Router();
162
+ outer.get('/healthz', (_req, res) => {
163
+ res.json({ ok: true });
164
+ });
165
+ outer.use(router);
166
+ return outer;
167
+ }
@@ -2,7 +2,7 @@
2
2
  * Submit a signed payment tx to Cardano and confirm settlement.
3
3
  *
4
4
  * Confirmation policy (v2 spec): accept after first chain sighting.
5
- * `mempool` status is explicitly discouraged in v2 Cardano's
5
+ * `mempool` status is explicitly discouraged in v2, Cardano's
6
6
  * Ouroboros Praos has probabilistic finality, so "in mempool" gives
7
7
  * no economic guarantee. We poll for first-chain-sighting via
8
8
  * `getTransactionByHash` (resolves to non-null when Blockfrost / Koios
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Confirmation budget: middleware paths use ~60s (covers preprod's
12
12
  * worst-case block time of ~20s plus indexer lag). On timeout we
13
- * return `{ confirmed: false, pending: true }` the spec contract is
13
+ * return `{ confirmed: false, pending: true }`, the spec contract is
14
14
  * that the buyer retries with the same `PAYMENT-SIGNATURE`. Replay
15
15
  * defense (on-chain via UTxO nonce) ensures only one retry actually
16
16
  * gets served.
@@ -3,7 +3,7 @@
3
3
  * Submit a signed payment tx to Cardano and confirm settlement.
4
4
  *
5
5
  * Confirmation policy (v2 spec): accept after first chain sighting.
6
- * `mempool` status is explicitly discouraged in v2 Cardano's
6
+ * `mempool` status is explicitly discouraged in v2, Cardano's
7
7
  * Ouroboros Praos has probabilistic finality, so "in mempool" gives
8
8
  * no economic guarantee. We poll for first-chain-sighting via
9
9
  * `getTransactionByHash` (resolves to non-null when Blockfrost / Koios
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Confirmation budget: middleware paths use ~60s (covers preprod's
13
13
  * worst-case block time of ~20s plus indexer lag). On timeout we
14
- * return `{ confirmed: false, pending: true }` the spec contract is
14
+ * return `{ confirmed: false, pending: true }`, the spec contract is
15
15
  * that the buyer retries with the same `PAYMENT-SIGNATURE`. Replay
16
16
  * defense (on-chain via UTxO nonce) ensures only one retry actually
17
17
  * gets served.
@@ -55,7 +55,7 @@ const bridge = __importStar(require("../bridge"));
55
55
  const errors_1 = require("../core/errors");
56
56
  /**
57
57
  * Patterns surfaced by the submit step that mean "the tx is already
58
- * known to the network" either in mempool or already mined. In
58
+ * known to the network", either in mempool or already mined. In
59
59
  * both cases we should NOT treat as failure; we should fall through
60
60
  * to polling.
61
61
  *
@@ -101,7 +101,7 @@ async function settle({ signedTxCborHex, expectedTxHash, pollBudgetMs = 60_000,
101
101
  }
102
102
  }
103
103
  // Cross-check: backend's hash must match our locally-computed one.
104
- // If it doesn't, something is structurally off bail loudly.
104
+ // If it doesn't, something is structurally off, bail loudly.
105
105
  if (submittedHash && submittedHash.toLowerCase() !== expectedTxHash.toLowerCase()) {
106
106
  return {
107
107
  confirmed: false,
@@ -5,7 +5,7 @@
5
5
  * Pipeline (v2):
6
6
  * 1. decode (PAYMENT-SIGNATURE → DecodedPayment)
7
7
  * 2. validate (6 mandatory checks, pure)
8
- * 3. checkNonceUnspent (chain UTxO still spendable)
8
+ * 3. checkNonceUnspent (chain, UTxO still spendable)
9
9
  * 4. settle (submit + poll-until-confirmed)
10
10
  * 5. onAccepted callback (consumer-side audit, best-effort)
11
11
  *
@@ -17,7 +17,7 @@
17
17
  * CBOR whose nonce was already spent will fail at the network
18
18
  * level anyway, and we want to return a precise REPLAY code
19
19
  * instead of a generic SUBMIT_FAILED.
20
- * - `onAccepted` runs ONLY after settle confirms we never call it
20
+ * - `onAccepted` runs ONLY after settle confirms, we never call it
21
21
  * for pending/rejected outcomes.
22
22
  */
23
23
  import { type X402Code } from '../core/errors';
@@ -26,19 +26,19 @@ export type ProcessKind = 'accepted' | 'rejected' | 'pending';
26
26
  export interface ProcessArgs {
27
27
  /** Raw header value (undefined if missing). */
28
28
  paymentHeader: string | string[] | undefined;
29
- /** Full 402 body the validator inspects `accepts[0]`. */
29
+ /** Full 402 body, the validator inspects `accepts[0]`. */
30
30
  requirementsBody: PaymentRequirementsBody;
31
31
  /** Optional override of the settle poll budget (ms). Default 60_000. */
32
32
  settlePollBudgetMs?: number;
33
33
  /**
34
34
  * Optional: callback invoked on successful payment. Use for consumer-
35
35
  * side audit (e.g. CHAINFEED writing to FeedReads, ODATAPAY writing to
36
- * Receipts). Throws here are swallowed and logged the canonical
36
+ * Receipts). Throws here are swallowed and logged, the canonical
37
37
  * record is on chain.
38
38
  */
39
39
  onAccepted?: (claim: PaymentClaim) => void | Promise<void>;
40
40
  /**
41
- * Optional: TTL check tolerance. Default false txs without a
41
+ * Optional: TTL check tolerance. Default false, txs without a
42
42
  * validity-range upper bound are rejected.
43
43
  */
44
44
  allowNoTtl?: boolean;
@@ -6,7 +6,7 @@
6
6
  * Pipeline (v2):
7
7
  * 1. decode (PAYMENT-SIGNATURE → DecodedPayment)
8
8
  * 2. validate (6 mandatory checks, pure)
9
- * 3. checkNonceUnspent (chain UTxO still spendable)
9
+ * 3. checkNonceUnspent (chain, UTxO still spendable)
10
10
  * 4. settle (submit + poll-until-confirmed)
11
11
  * 5. onAccepted callback (consumer-side audit, best-effort)
12
12
  *
@@ -18,7 +18,7 @@
18
18
  * CBOR whose nonce was already spent will fail at the network
19
19
  * level anyway, and we want to return a precise REPLAY code
20
20
  * instead of a generic SUBMIT_FAILED.
21
- * - `onAccepted` runs ONLY after settle confirms we never call it
21
+ * - `onAccepted` runs ONLY after settle confirms, we never call it
22
22
  * for pending/rejected outcomes.
23
23
  */
24
24
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -62,7 +62,6 @@ exports.process = process;
62
62
  const cds_1 = __importDefault(require("@sap/cds"));
63
63
  const decode_1 = require("../core/decode");
64
64
  const validate_1 = require("../core/validate");
65
- const requirements_1 = require("../core/requirements");
66
65
  const errors_1 = require("../core/errors");
67
66
  const nonce_1 = require("./nonce");
68
67
  const settle_1 = require("./settle");
@@ -95,8 +94,10 @@ async function process(args) {
95
94
  requirementsBody: args.requirementsBody,
96
95
  };
97
96
  }
98
- const requirements = (0, requirements_1.flatRequirements)(args.requirementsBody);
99
97
  // ─── 1. Decode ──────────────────────────────────────────────────────
98
+ // Decode happens BEFORE we pick a requirements entry, the picker needs
99
+ // to know which (payTo, asset) the tx actually credits to choose
100
+ // among multi-accept options.
100
101
  let decoded;
101
102
  try {
102
103
  decoded = (0, decode_1.decode)(headerStr);
@@ -112,6 +113,19 @@ async function process(args) {
112
113
  }
113
114
  throw err;
114
115
  }
116
+ // Pick the accepts[] entry the buyer paid against. For single-entry
117
+ // bodies this is the same as the old `flatRequirements`; for
118
+ // multi-accept it routes the tx to the matching seller option.
119
+ const picked = (0, validate_1.pickRequirement)(decoded, args.requirementsBody);
120
+ if (!picked.ok) {
121
+ return {
122
+ kind: 'rejected',
123
+ code: picked.code,
124
+ reason: picked.reason,
125
+ requirementsBody: args.requirementsBody,
126
+ };
127
+ }
128
+ const requirements = picked.entry;
115
129
  // ─── 2. Validate (6 checks, pure) ───────────────────────────────────
116
130
  let currentSlot;
117
131
  try {
@@ -137,7 +151,7 @@ async function process(args) {
137
151
  requirementsBody: args.requirementsBody,
138
152
  };
139
153
  }
140
- // ─── 3. Nonce UTxO still unspent (chain) ──────────────────────────
154
+ // ─── 3. Nonce, UTxO still unspent (chain) ──────────────────────────
141
155
  const nonceResult = await (0, nonce_1.checkNonceUnspent)({
142
156
  txHash: decoded.nonce.txHash,
143
157
  outputIndex: decoded.nonce.index,
@@ -9,9 +9,9 @@
9
9
  * `payload.transaction` in the PAYMENT-SIGNATURE envelope.
10
10
  *
11
11
  * Diff vs v1 of CHAINFEED's same-named helper:
12
- * - Asset-agnostic parses requirements.asset as a v2 string
12
+ * - Asset-agnostic, parses requirements.asset as a v2 string
13
13
  * (`'lovelace'` or `'<policy>.<nameHex>'`).
14
- * - Returns `nonceRef` alongside the unsigned CBOR the server
14
+ * - Returns `nonceRef` alongside the unsigned CBOR, the server
15
15
  * picks one of the buyer's chosen inputs as the v2 nonce UTxO,
16
16
  * so the browser doesn't have to reason about it.
17
17
  *
@@ -26,7 +26,7 @@ import type { PaymentRequirementEntry } from '../core/types';
26
26
  export interface BuildUnsignedTxArgs {
27
27
  /** Buyer's bech32 address (must be Base or Enterprise with VKey-hash payment cred). */
28
28
  buyerBech32: string;
29
- /** A single accepts[] entry call `flatRequirements(body)` to extract. */
29
+ /** A single accepts[] entry, call `flatRequirements(body)` to extract. */
30
30
  requirements: PaymentRequirementEntry;
31
31
  /**
32
32
  * Optional TTL in slots from "now" (= current chain tip slot).
@@ -37,9 +37,9 @@ export interface BuildUnsignedTxArgs {
37
37
  export interface UnsignedTxResult {
38
38
  /** CBOR hex of the unsigned tx (empty witness set). Ready for CIP-30 signTx. */
39
39
  unsignedTxCborHex: string;
40
- /** Hex tx hash what the buyer's wallet will display. */
40
+ /** Hex tx hash, what the buyer's wallet will display. */
41
41
  txHashHex: string;
42
- /** Buyer's payment-cred VKey hash wallet must sign for this. */
42
+ /** Buyer's payment-cred VKey hash, wallet must sign for this. */
43
43
  requiredSignerHex: string;
44
44
  /** v2 nonce reference `<txHash>#<index>`, picked from the buyer's chosen inputs. */
45
45
  nonceRef: string;
@@ -10,9 +10,9 @@
10
10
  * `payload.transaction` in the PAYMENT-SIGNATURE envelope.
11
11
  *
12
12
  * Diff vs v1 of CHAINFEED's same-named helper:
13
- * - Asset-agnostic parses requirements.asset as a v2 string
13
+ * - Asset-agnostic, parses requirements.asset as a v2 string
14
14
  * (`'lovelace'` or `'<policy>.<nameHex>'`).
15
- * - Returns `nonceRef` alongside the unsigned CBOR the server
15
+ * - Returns `nonceRef` alongside the unsigned CBOR, the server
16
16
  * picks one of the buyer's chosen inputs as the v2 nonce UTxO,
17
17
  * so the browser doesn't have to reason about it.
18
18
  *
@@ -189,7 +189,7 @@ async function buildUnsignedPaymentTx(args) {
189
189
  const unsigned = CSL.Transaction.new(txBody, emptyWits);
190
190
  // Pick the first input as the v2 nonce UTxO.
191
191
  // It MUST appear in tx.inputs (which it does by construction) and be
192
- // unspent (which it is we just queried it from the buyer's UTxO set).
192
+ // unspent (which it is, we just queried it from the buyer's UTxO set).
193
193
  const nonceInput = inputs[0];
194
194
  const nonceRef = `${nonceInput.txHash}#${nonceInput.outputIndex}`;
195
195
  return {
@@ -7,7 +7,7 @@
7
7
  * the right asset to the right address on the right network.
8
8
  *
9
9
  * Differences from the middleware path (`facilitator/verify.ts`):
10
- * - No envelope, no PAYMENT-SIGNATURE header just a tx hash.
10
+ * - No envelope, no PAYMENT-SIGNATURE header, just a tx hash.
11
11
  * - The tx is presumed already on-chain (the network already accepted
12
12
  * witnesses), so we skip the witness-presence check.
13
13
  * - No on-chain nonce check: replay defense for confirmed-payment
@@ -8,7 +8,7 @@
8
8
  * the right asset to the right address on the right network.
9
9
  *
10
10
  * Differences from the middleware path (`facilitator/verify.ts`):
11
- * - No envelope, no PAYMENT-SIGNATURE header just a tx hash.
11
+ * - No envelope, no PAYMENT-SIGNATURE header, just a tx hash.
12
12
  * - The tx is presumed already on-chain (the network already accepted
13
13
  * witnesses), so we skip the witness-presence check.
14
14
  * - No on-chain nonce check: replay defense for confirmed-payment
package/srv/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @odatano/x402 public API barrel.
2
+ * @odatano/x402, public API barrel.
3
3
  *
4
4
  * Cardano-x402-v2 payment library for SAP CAP applications.
5
5
  *
@@ -27,7 +27,7 @@
27
27
  * }
28
28
  * }
29
29
  *
30
- * // 3. Programmatic verify a confirmed payment by tx hash
30
+ * // 3. Programmatic, verify a confirmed payment by tx hash
31
31
  * import { verifyConfirmedPayment } from '@odatano/x402';
32
32
  * const r = await verifyConfirmedPayment({
33
33
  * txHash, requiredAmount, asset, payTo, network,
@@ -43,9 +43,18 @@ export type { AssetTransferMethod, ResourceDescriptor, PaymentRequirementEntry,
43
43
  export { process as verifyPayment, type ProcessArgs, type ProcessResult, type ProcessKind, } from './facilitator/verify';
44
44
  export { settle, type SettleArgs, type SettleResult } from './facilitator/settle';
45
45
  export { checkNonceUnspent, type NonceCheckArgs, type NonceResult } from './facilitator/nonce';
46
+ export { localFacilitator, type Facilitator, type FacilitatorVerifyAndSettleArgs, type FacilitatorResult, type FacilitatorSupportedResult, } from './facilitator/adapter';
47
+ export { httpFacilitator, type HttpFacilitatorConfig, } from './facilitator/http';
48
+ export { createFacilitatorRouter, type CreateFacilitatorRouterOptions, type FacilitatorServerLogger, } from './facilitator/server';
46
49
  export { verifyConfirmedPayment, type VerifyConfirmedArgs, type VerifyConfirmedResult, } from './helpers/verify-confirmed';
47
50
  export { buildUnsignedPaymentTx, type BuildUnsignedTxArgs, type UnsignedTxResult, } from './helpers/build-unsigned-tx';
48
51
  export { x402Middleware, type X402MiddlewareOptions } from './middleware/express';
49
52
  export { gateService, type X402CapOptions } from './middleware/cap';
53
+ export { x402Fetch, type X402FetchOptions } from './client/fetch';
54
+ export { x402Axios } from './client/axios';
55
+ export { encodePaymentEnvelope, type EncodeEnvelopeArgs, } from './client/envelope';
56
+ export { createBridgePayHandler, type BridgePayHandlerOptions, } from './client/pay-handlers';
57
+ export type { PayHandler, PayHandlerResult, AcceptsSelector, X402ClientOptions, } from './client/types';
58
+ export { X402PaymentError, parseErrorCode, paymentErrorFromBody, type X402PaymentErrorKind, type X402PaymentErrorInit, } from './client/errors';
50
59
  export * as bridge from './bridge';
51
60
  //# sourceMappingURL=index.d.ts.map
package/srv/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @odatano/x402 public API barrel.
3
+ * @odatano/x402, public API barrel.
4
4
  *
5
5
  * Cardano-x402-v2 payment library for SAP CAP applications.
6
6
  *
@@ -28,7 +28,7 @@
28
28
  * }
29
29
  * }
30
30
  *
31
- * // 3. Programmatic verify a confirmed payment by tx hash
31
+ * // 3. Programmatic, verify a confirmed payment by tx hash
32
32
  * import { verifyConfirmedPayment } from '@odatano/x402';
33
33
  * const r = await verifyConfirmedPayment({
34
34
  * txHash, requiredAmount, asset, payTo, network,
@@ -68,7 +68,7 @@ var __importStar = (this && this.__importStar) || (function () {
68
68
  };
69
69
  })();
70
70
  Object.defineProperty(exports, "__esModule", { value: true });
71
- exports.bridge = exports.gateService = exports.x402Middleware = exports.buildUnsignedPaymentTx = exports.verifyConfirmedPayment = exports.checkNonceUnspent = exports.settle = exports.verifyPayment = exports.Codes = exports.X402Error = exports.networksMatch = exports.isNetwork = exports.parseNetwork = exports.buildAssetString = exports.parseAsset = exports.validatePayment = exports.decode = exports.flatRequirements = exports.buildEntry = exports.buildPaymentRequirements = void 0;
71
+ exports.bridge = exports.paymentErrorFromBody = exports.parseErrorCode = exports.X402PaymentError = exports.createBridgePayHandler = exports.encodePaymentEnvelope = exports.x402Axios = exports.x402Fetch = exports.gateService = exports.x402Middleware = exports.buildUnsignedPaymentTx = exports.verifyConfirmedPayment = exports.createFacilitatorRouter = exports.httpFacilitator = exports.localFacilitator = exports.checkNonceUnspent = exports.settle = exports.verifyPayment = exports.Codes = exports.X402Error = exports.networksMatch = exports.isNetwork = exports.parseNetwork = exports.buildAssetString = exports.parseAsset = exports.validatePayment = exports.decode = exports.flatRequirements = exports.buildEntry = exports.buildPaymentRequirements = void 0;
72
72
  // ─── Core builders / validators (pure) ────────────────────────────────
73
73
  var requirements_1 = require("./core/requirements");
74
74
  Object.defineProperty(exports, "buildPaymentRequirements", { enumerable: true, get: function () { return requirements_1.buildPaymentRequirements; } });
@@ -97,6 +97,13 @@ var settle_1 = require("./facilitator/settle");
97
97
  Object.defineProperty(exports, "settle", { enumerable: true, get: function () { return settle_1.settle; } });
98
98
  var nonce_1 = require("./facilitator/nonce");
99
99
  Object.defineProperty(exports, "checkNonceUnspent", { enumerable: true, get: function () { return nonce_1.checkNonceUnspent; } });
100
+ // ─── Facilitator adapter (pluggable local vs hosted) ──────────────────
101
+ var adapter_1 = require("./facilitator/adapter");
102
+ Object.defineProperty(exports, "localFacilitator", { enumerable: true, get: function () { return adapter_1.localFacilitator; } });
103
+ var http_1 = require("./facilitator/http");
104
+ Object.defineProperty(exports, "httpFacilitator", { enumerable: true, get: function () { return http_1.httpFacilitator; } });
105
+ var server_1 = require("./facilitator/server");
106
+ Object.defineProperty(exports, "createFacilitatorRouter", { enumerable: true, get: function () { return server_1.createFacilitatorRouter; } });
100
107
  // ─── Helpers ──────────────────────────────────────────────────────────
101
108
  var verify_confirmed_1 = require("./helpers/verify-confirmed");
102
109
  Object.defineProperty(exports, "verifyConfirmedPayment", { enumerable: true, get: function () { return verify_confirmed_1.verifyConfirmedPayment; } });
@@ -107,5 +114,18 @@ var express_1 = require("./middleware/express");
107
114
  Object.defineProperty(exports, "x402Middleware", { enumerable: true, get: function () { return express_1.x402Middleware; } });
108
115
  var cap_1 = require("./middleware/cap");
109
116
  Object.defineProperty(exports, "gateService", { enumerable: true, get: function () { return cap_1.gateService; } });
117
+ // ─── Client (HTTP wrappers that auto-handle 402) ──────────────────────
118
+ var fetch_1 = require("./client/fetch");
119
+ Object.defineProperty(exports, "x402Fetch", { enumerable: true, get: function () { return fetch_1.x402Fetch; } });
120
+ var axios_1 = require("./client/axios");
121
+ Object.defineProperty(exports, "x402Axios", { enumerable: true, get: function () { return axios_1.x402Axios; } });
122
+ var envelope_1 = require("./client/envelope");
123
+ Object.defineProperty(exports, "encodePaymentEnvelope", { enumerable: true, get: function () { return envelope_1.encodePaymentEnvelope; } });
124
+ var pay_handlers_1 = require("./client/pay-handlers");
125
+ Object.defineProperty(exports, "createBridgePayHandler", { enumerable: true, get: function () { return pay_handlers_1.createBridgePayHandler; } });
126
+ var errors_2 = require("./client/errors");
127
+ Object.defineProperty(exports, "X402PaymentError", { enumerable: true, get: function () { return errors_2.X402PaymentError; } });
128
+ Object.defineProperty(exports, "parseErrorCode", { enumerable: true, get: function () { return errors_2.parseErrorCode; } });
129
+ Object.defineProperty(exports, "paymentErrorFromBody", { enumerable: true, get: function () { return errors_2.paymentErrorFromBody; } });
110
130
  // ─── Bridge (lower-level: exposed for advanced consumers) ─────────────
111
131
  exports.bridge = __importStar(require("./bridge"));
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * CAP services receive requests through `srv.before(...)` / `srv.on(...)`
5
5
  * handlers, not Express middleware. For OData-served entities, the 402
6
- * needs to come from `req.reject(402, body)` the response is built by
6
+ * needs to come from `req.reject(402, body)`, the response is built by
7
7
  * CAP, not by `res.status(...)`.
8
8
  *
9
9
  * Usage:
@@ -25,18 +25,24 @@
25
25
  * the request passes through unmodified.
26
26
  */
27
27
  import cds from '@sap/cds';
28
- import type { AssetTransferMethod, Network, PaymentClaim } from '../core/types';
28
+ import { type Facilitator } from '../facilitator/adapter';
29
+ import type { AssetTransferMethod, Network, PaymentClaim, PriceSpec, PriceResolver } from '../core/types';
29
30
  export interface X402CapOptions {
30
31
  payTo: string;
31
32
  network: Network | string;
32
33
  asset: string;
33
- /** Single price (applies to all gated events). */
34
- priceUnits?: string | number | bigint;
35
34
  /**
36
- * Per-event prices. Keys are CAP event names (entity name for CRUD,
37
- * action name for actions). Events absent here pass through.
35
+ * Single price (applies to all gated events). May be a scalar, a
36
+ * `RouteOption`, or a `RouteOption[]` (multi-accept).
38
37
  */
39
- routePricing?: Record<string, string | number | bigint>;
38
+ priceUnits?: PriceSpec;
39
+ /**
40
+ * Per-event prices. Either a static map (keys are CAP entity or
41
+ * action names; events absent here pass through) or a `PriceResolver`
42
+ * function for dynamic pricing. Resolver returning `null` skips the
43
+ * gate; otherwise returns a scalar / `RouteOption` / `RouteOption[]`.
44
+ */
45
+ routePricing?: Record<string, PriceSpec> | PriceResolver;
40
46
  description?: string;
41
47
  mimeType?: string;
42
48
  assetTransferMethod?: AssetTransferMethod;
@@ -51,6 +57,45 @@ export interface X402CapOptions {
51
57
  * builder to embed pair / entity id in the resource string.
52
58
  */
53
59
  resourceUrl?: (req: cds.Request) => string;
60
+ /**
61
+ * Persist accepted payments to a CDS entity. Set to `true` to use the
62
+ * default entity `odatano.x402.X402Receipts` (shipped in the plugin's
63
+ * `db/x402-receipts.cds`), or pass `{ entity: 'my.namespace.MyTable' }`
64
+ * to write to a consumer-defined table with the same shape.
65
+ *
66
+ * INSERT runs AFTER settle confirms, BEFORE the 200 response. INSERT
67
+ * failures are logged and never block the response, the canonical
68
+ * record is on chain. Pair with `onAccepted` if you need side-effects
69
+ * beyond persistence.
70
+ */
71
+ receipts?: boolean | {
72
+ entity?: string;
73
+ };
74
+ /**
75
+ * Time-limited grants: pay once, get `ttlSeconds` of free access to
76
+ * the same route. On accepted payment, the gate issues an opaque
77
+ * token and returns it via the `X-PAYMENT-GRANT` response header.
78
+ * The buyer presents that token via the `X-PAYMENT-GRANT` request
79
+ * header on subsequent calls; while it's valid the gate bypasses the
80
+ * 402 + verify+settle pipeline.
81
+ *
82
+ * Default `ttlSeconds`: 3600. Default entity:
83
+ * `odatano.x402.X402Grants` (shipped in `db/x402-grants.cds`).
84
+ *
85
+ * Grant errors (issue or lookup) are SWALLOWED. A failing DB never
86
+ * denies a paying buyer their response; it just means no
87
+ * subscription short-circuit until the DB recovers.
88
+ */
89
+ grants?: boolean | {
90
+ ttlSeconds?: number;
91
+ entity?: string;
92
+ };
93
+ /**
94
+ * Facilitator implementation handling verify+settle. Default
95
+ * `localFacilitator()`, in-process via `@odatano/core`. Use
96
+ * `httpFacilitator({ url, apiKey })` to delegate to a hosted service.
97
+ */
98
+ facilitator?: Facilitator;
54
99
  }
55
100
  /**
56
101
  * Attach the x402 gate to a CAP ApplicationService. Returns the service
@@ -58,7 +103,7 @@ export interface X402CapOptions {
58
103
  *
59
104
  * The gate registers as `srv.before('*', ...)` which fires for every
60
105
  * event on the service. We filter inside the handler based on
61
- * `routePricing` registering per-entity would lose actions, and
106
+ * `routePricing`, registering per-entity would lose actions, and
62
107
  * per-event arrays don't support the `'*'` fallback we want.
63
108
  */
64
109
  export declare function gateService<S extends cds.Service>(srv: S, opts: X402CapOptions): S;