@odatano/x402 0.2.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 +35 -0
- package/README.md +5 -0
- package/cds-plugin.js +2 -0
- package/db/x402-grants.cds +49 -0
- package/db/x402-receipts.cds +44 -0
- package/package.json +4 -3
- package/srv/bridge.d.ts +9 -12
- package/srv/bridge.js +10 -13
- package/srv/client/axios.d.ts +1 -1
- package/srv/client/axios.js +45 -8
- package/srv/client/envelope.d.ts +1 -1
- package/srv/client/envelope.js +1 -1
- package/srv/client/errors.d.ts +86 -0
- package/srv/client/errors.js +107 -0
- package/srv/client/fetch.d.ts +2 -2
- package/srv/client/fetch.js +71 -11
- package/srv/client/pay-handlers.d.ts +4 -4
- package/srv/client/pay-handlers.js +3 -3
- package/srv/client/types.d.ts +19 -7
- package/srv/client/types.js +3 -3
- package/srv/core/asset.d.ts +1 -1
- package/srv/core/decode.d.ts +2 -2
- package/srv/core/decode.js +5 -5
- package/srv/core/errors.js +3 -3
- package/srv/core/network.d.ts +1 -1
- package/srv/core/network.js +1 -1
- package/srv/core/requirements.d.ts +37 -5
- package/srv/core/requirements.js +43 -4
- package/srv/core/types.d.ts +68 -6
- package/srv/core/types.js +3 -3
- package/srv/core/validate.d.ts +31 -7
- package/srv/core/validate.js +84 -9
- package/srv/facilitator/adapter.d.ts +8 -8
- package/srv/facilitator/adapter.js +5 -5
- package/srv/facilitator/http.d.ts +4 -4
- package/srv/facilitator/http.js +5 -5
- package/srv/facilitator/nonce.d.ts +4 -4
- package/srv/facilitator/nonce.js +4 -4
- package/srv/facilitator/server.d.ts +68 -0
- package/srv/facilitator/server.js +167 -0
- package/srv/facilitator/settle.d.ts +2 -2
- package/srv/facilitator/settle.js +4 -4
- package/srv/facilitator/verify.d.ts +5 -5
- package/srv/facilitator/verify.js +19 -5
- package/srv/helpers/build-unsigned-tx.d.ts +5 -5
- package/srv/helpers/build-unsigned-tx.js +3 -3
- package/srv/helpers/verify-confirmed.d.ts +1 -1
- package/srv/helpers/verify-confirmed.js +1 -1
- package/srv/index.d.ts +4 -2
- package/srv/index.js +9 -3
- package/srv/middleware/cap.d.ts +47 -9
- package/srv/middleware/cap.js +111 -43
- package/srv/middleware/express.d.ts +15 -10
- package/srv/middleware/express.js +18 -19
- package/srv/middleware/grants.d.ts +64 -0
- package/srv/middleware/grants.js +113 -0
- package/srv/middleware/pricing.d.ts +41 -0
- package/srv/middleware/pricing.js +78 -0
- package/srv/middleware/receipts.d.ts +38 -0
- package/srv/middleware/receipts.js +68 -0
- package/srv/plugin.d.ts +2 -2
- 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
|
|
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 }
|
|
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
|
|
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 }
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12
|
+
* - Asset-agnostic, parses requirements.asset as a v2 string
|
|
13
13
|
* (`'lovelace'` or `'<policy>.<nameHex>'`).
|
|
14
|
-
* - Returns `nonceRef` alongside the unsigned CBOR
|
|
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
|
|
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
|
|
40
|
+
/** Hex tx hash, what the buyer's wallet will display. */
|
|
41
41
|
txHashHex: string;
|
|
42
|
-
/** Buyer's payment-cred VKey hash
|
|
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
|
|
13
|
+
* - Asset-agnostic, parses requirements.asset as a v2 string
|
|
14
14
|
* (`'lovelace'` or `'<policy>.<nameHex>'`).
|
|
15
|
-
* - Returns `nonceRef` alongside the unsigned CBOR
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
@@ -45,6 +45,7 @@ export { settle, type SettleArgs, type SettleResult } from './facilitator/settle
|
|
|
45
45
|
export { checkNonceUnspent, type NonceCheckArgs, type NonceResult } from './facilitator/nonce';
|
|
46
46
|
export { localFacilitator, type Facilitator, type FacilitatorVerifyAndSettleArgs, type FacilitatorResult, type FacilitatorSupportedResult, } from './facilitator/adapter';
|
|
47
47
|
export { httpFacilitator, type HttpFacilitatorConfig, } from './facilitator/http';
|
|
48
|
+
export { createFacilitatorRouter, type CreateFacilitatorRouterOptions, type FacilitatorServerLogger, } from './facilitator/server';
|
|
48
49
|
export { verifyConfirmedPayment, type VerifyConfirmedArgs, type VerifyConfirmedResult, } from './helpers/verify-confirmed';
|
|
49
50
|
export { buildUnsignedPaymentTx, type BuildUnsignedTxArgs, type UnsignedTxResult, } from './helpers/build-unsigned-tx';
|
|
50
51
|
export { x402Middleware, type X402MiddlewareOptions } from './middleware/express';
|
|
@@ -54,5 +55,6 @@ export { x402Axios } from './client/axios';
|
|
|
54
55
|
export { encodePaymentEnvelope, type EncodeEnvelopeArgs, } from './client/envelope';
|
|
55
56
|
export { createBridgePayHandler, type BridgePayHandlerOptions, } from './client/pay-handlers';
|
|
56
57
|
export type { PayHandler, PayHandlerResult, AcceptsSelector, X402ClientOptions, } from './client/types';
|
|
58
|
+
export { X402PaymentError, parseErrorCode, paymentErrorFromBody, type X402PaymentErrorKind, type X402PaymentErrorInit, } from './client/errors';
|
|
57
59
|
export * as bridge from './bridge';
|
|
58
60
|
//# sourceMappingURL=index.d.ts.map
|
package/srv/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* @odatano/x402
|
|
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
|
|
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.createBridgePayHandler = exports.encodePaymentEnvelope = exports.x402Axios = exports.x402Fetch = exports.gateService = exports.x402Middleware = exports.buildUnsignedPaymentTx = exports.verifyConfirmedPayment = 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;
|
|
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; } });
|
|
@@ -102,6 +102,8 @@ var adapter_1 = require("./facilitator/adapter");
|
|
|
102
102
|
Object.defineProperty(exports, "localFacilitator", { enumerable: true, get: function () { return adapter_1.localFacilitator; } });
|
|
103
103
|
var http_1 = require("./facilitator/http");
|
|
104
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; } });
|
|
105
107
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
106
108
|
var verify_confirmed_1 = require("./helpers/verify-confirmed");
|
|
107
109
|
Object.defineProperty(exports, "verifyConfirmedPayment", { enumerable: true, get: function () { return verify_confirmed_1.verifyConfirmedPayment; } });
|
|
@@ -121,5 +123,9 @@ var envelope_1 = require("./client/envelope");
|
|
|
121
123
|
Object.defineProperty(exports, "encodePaymentEnvelope", { enumerable: true, get: function () { return envelope_1.encodePaymentEnvelope; } });
|
|
122
124
|
var pay_handlers_1 = require("./client/pay-handlers");
|
|
123
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; } });
|
|
124
130
|
// ─── Bridge (lower-level: exposed for advanced consumers) ─────────────
|
|
125
131
|
exports.bridge = __importStar(require("./bridge"));
|
package/srv/middleware/cap.d.ts
CHANGED
|
@@ -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)
|
|
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:
|
|
@@ -26,18 +26,23 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import cds from '@sap/cds';
|
|
28
28
|
import { type Facilitator } from '../facilitator/adapter';
|
|
29
|
-
import type { AssetTransferMethod, Network, PaymentClaim } from '../core/types';
|
|
29
|
+
import type { AssetTransferMethod, Network, PaymentClaim, PriceSpec, PriceResolver } from '../core/types';
|
|
30
30
|
export interface X402CapOptions {
|
|
31
31
|
payTo: string;
|
|
32
32
|
network: Network | string;
|
|
33
33
|
asset: string;
|
|
34
|
-
/** Single price (applies to all gated events). */
|
|
35
|
-
priceUnits?: string | number | bigint;
|
|
36
34
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* Single price (applies to all gated events). May be a scalar, a
|
|
36
|
+
* `RouteOption`, or a `RouteOption[]` (multi-accept).
|
|
39
37
|
*/
|
|
40
|
-
|
|
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;
|
|
41
46
|
description?: string;
|
|
42
47
|
mimeType?: string;
|
|
43
48
|
assetTransferMethod?: AssetTransferMethod;
|
|
@@ -52,9 +57,42 @@ export interface X402CapOptions {
|
|
|
52
57
|
* builder to embed pair / entity id in the resource string.
|
|
53
58
|
*/
|
|
54
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
|
+
};
|
|
55
93
|
/**
|
|
56
94
|
* Facilitator implementation handling verify+settle. Default
|
|
57
|
-
* `localFacilitator()
|
|
95
|
+
* `localFacilitator()`, in-process via `@odatano/core`. Use
|
|
58
96
|
* `httpFacilitator({ url, apiKey })` to delegate to a hosted service.
|
|
59
97
|
*/
|
|
60
98
|
facilitator?: Facilitator;
|
|
@@ -65,7 +103,7 @@ export interface X402CapOptions {
|
|
|
65
103
|
*
|
|
66
104
|
* The gate registers as `srv.before('*', ...)` which fires for every
|
|
67
105
|
* event on the service. We filter inside the handler based on
|
|
68
|
-
* `routePricing
|
|
106
|
+
* `routePricing`, registering per-entity would lose actions, and
|
|
69
107
|
* per-event arrays don't support the `'*'` fallback we want.
|
|
70
108
|
*/
|
|
71
109
|
export declare function gateService<S extends cds.Service>(srv: S, opts: X402CapOptions): S;
|