@odatano/x402 0.1.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.
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/cds-plugin.js +10 -0
- package/package.json +52 -0
- package/srv/bridge.d.ts +68 -0
- package/srv/bridge.js +155 -0
- package/srv/core/asset.d.ts +33 -0
- package/srv/core/asset.js +57 -0
- package/srv/core/decode.d.ts +26 -0
- package/srv/core/decode.js +243 -0
- package/srv/core/errors.d.ts +41 -0
- package/srv/core/errors.js +52 -0
- package/srv/core/network.d.ts +19 -0
- package/srv/core/network.js +39 -0
- package/srv/core/requirements.d.ts +51 -0
- package/srv/core/requirements.js +86 -0
- package/srv/core/types.d.ts +116 -0
- package/srv/core/types.js +10 -0
- package/srv/core/validate.d.ts +45 -0
- package/srv/core/validate.js +152 -0
- package/srv/facilitator/nonce.d.ts +35 -0
- package/srv/facilitator/nonce.js +69 -0
- package/srv/facilitator/settle.d.ts +36 -0
- package/srv/facilitator/settle.js +128 -0
- package/srv/facilitator/verify.d.ts +65 -0
- package/srv/facilitator/verify.js +188 -0
- package/srv/helpers/build-unsigned-tx.d.ts +56 -0
- package/srv/helpers/build-unsigned-tx.js +203 -0
- package/srv/helpers/verify-confirmed.d.ts +43 -0
- package/srv/helpers/verify-confirmed.js +129 -0
- package/srv/index.d.ts +51 -0
- package/srv/index.js +111 -0
- package/srv/middleware/cap.d.ts +65 -0
- package/srv/middleware/cap.js +170 -0
- package/srv/middleware/express.d.ts +66 -0
- package/srv/middleware/express.js +116 -0
- package/srv/plugin.d.ts +16 -0
- package/srv/plugin.js +73 -0
package/srv/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @odatano/x402 — public API barrel.
|
|
4
|
+
*
|
|
5
|
+
* Cardano-x402-v2 payment library for SAP CAP applications.
|
|
6
|
+
*
|
|
7
|
+
* Three usage shapes:
|
|
8
|
+
*
|
|
9
|
+
* // 1. Express middleware (mount under a path)
|
|
10
|
+
* import { x402Middleware } from '@odatano/x402';
|
|
11
|
+
* app.use('/api/premium', x402Middleware({
|
|
12
|
+
* payTo: 'addr_test1...',
|
|
13
|
+
* network: 'cardano:preprod',
|
|
14
|
+
* asset: '16a55b...ddde.0014df105553444d',
|
|
15
|
+
* priceUnits: '1000000',
|
|
16
|
+
* onAccepted: async (claim) => { ... },
|
|
17
|
+
* }));
|
|
18
|
+
*
|
|
19
|
+
* // 2. CAP service gate (registers a before-* handler)
|
|
20
|
+
* import { gateService } from '@odatano/x402';
|
|
21
|
+
* class MyService extends cds.ApplicationService {
|
|
22
|
+
* async init() {
|
|
23
|
+
* gateService(this, {
|
|
24
|
+
* payTo, network, asset,
|
|
25
|
+
* routePricing: { Prices: '10000', getBestPrice: '10000' },
|
|
26
|
+
* });
|
|
27
|
+
* return super.init();
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* // 3. Programmatic — verify a confirmed payment by tx hash
|
|
32
|
+
* import { verifyConfirmedPayment } from '@odatano/x402';
|
|
33
|
+
* const r = await verifyConfirmedPayment({
|
|
34
|
+
* txHash, requiredAmount, asset, payTo, network,
|
|
35
|
+
* });
|
|
36
|
+
*/
|
|
37
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
38
|
+
if (k2 === undefined) k2 = k;
|
|
39
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
40
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
41
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
42
|
+
}
|
|
43
|
+
Object.defineProperty(o, k2, desc);
|
|
44
|
+
}) : (function(o, m, k, k2) {
|
|
45
|
+
if (k2 === undefined) k2 = k;
|
|
46
|
+
o[k2] = m[k];
|
|
47
|
+
}));
|
|
48
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
49
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
50
|
+
}) : function(o, v) {
|
|
51
|
+
o["default"] = v;
|
|
52
|
+
});
|
|
53
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
54
|
+
var ownKeys = function(o) {
|
|
55
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
56
|
+
var ar = [];
|
|
57
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
58
|
+
return ar;
|
|
59
|
+
};
|
|
60
|
+
return ownKeys(o);
|
|
61
|
+
};
|
|
62
|
+
return function (mod) {
|
|
63
|
+
if (mod && mod.__esModule) return mod;
|
|
64
|
+
var result = {};
|
|
65
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
66
|
+
__setModuleDefault(result, mod);
|
|
67
|
+
return result;
|
|
68
|
+
};
|
|
69
|
+
})();
|
|
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;
|
|
72
|
+
// ─── Core builders / validators (pure) ────────────────────────────────
|
|
73
|
+
var requirements_1 = require("./core/requirements");
|
|
74
|
+
Object.defineProperty(exports, "buildPaymentRequirements", { enumerable: true, get: function () { return requirements_1.buildPaymentRequirements; } });
|
|
75
|
+
Object.defineProperty(exports, "buildEntry", { enumerable: true, get: function () { return requirements_1.buildEntry; } });
|
|
76
|
+
Object.defineProperty(exports, "flatRequirements", { enumerable: true, get: function () { return requirements_1.flatRequirements; } });
|
|
77
|
+
var decode_1 = require("./core/decode");
|
|
78
|
+
Object.defineProperty(exports, "decode", { enumerable: true, get: function () { return decode_1.decode; } });
|
|
79
|
+
var validate_1 = require("./core/validate");
|
|
80
|
+
Object.defineProperty(exports, "validatePayment", { enumerable: true, get: function () { return validate_1.validatePayment; } });
|
|
81
|
+
// ─── Asset / network helpers ──────────────────────────────────────────
|
|
82
|
+
var asset_1 = require("./core/asset");
|
|
83
|
+
Object.defineProperty(exports, "parseAsset", { enumerable: true, get: function () { return asset_1.parseAsset; } });
|
|
84
|
+
Object.defineProperty(exports, "buildAssetString", { enumerable: true, get: function () { return asset_1.buildAssetString; } });
|
|
85
|
+
var network_1 = require("./core/network");
|
|
86
|
+
Object.defineProperty(exports, "parseNetwork", { enumerable: true, get: function () { return network_1.parseNetwork; } });
|
|
87
|
+
Object.defineProperty(exports, "isNetwork", { enumerable: true, get: function () { return network_1.isNetwork; } });
|
|
88
|
+
Object.defineProperty(exports, "networksMatch", { enumerable: true, get: function () { return network_1.networksMatch; } });
|
|
89
|
+
// ─── Errors / codes ───────────────────────────────────────────────────
|
|
90
|
+
var errors_1 = require("./core/errors");
|
|
91
|
+
Object.defineProperty(exports, "X402Error", { enumerable: true, get: function () { return errors_1.X402Error; } });
|
|
92
|
+
Object.defineProperty(exports, "Codes", { enumerable: true, get: function () { return errors_1.Codes; } });
|
|
93
|
+
// ─── Facilitator (chain-touching) ─────────────────────────────────────
|
|
94
|
+
var verify_1 = require("./facilitator/verify");
|
|
95
|
+
Object.defineProperty(exports, "verifyPayment", { enumerable: true, get: function () { return verify_1.process; } });
|
|
96
|
+
var settle_1 = require("./facilitator/settle");
|
|
97
|
+
Object.defineProperty(exports, "settle", { enumerable: true, get: function () { return settle_1.settle; } });
|
|
98
|
+
var nonce_1 = require("./facilitator/nonce");
|
|
99
|
+
Object.defineProperty(exports, "checkNonceUnspent", { enumerable: true, get: function () { return nonce_1.checkNonceUnspent; } });
|
|
100
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
101
|
+
var verify_confirmed_1 = require("./helpers/verify-confirmed");
|
|
102
|
+
Object.defineProperty(exports, "verifyConfirmedPayment", { enumerable: true, get: function () { return verify_confirmed_1.verifyConfirmedPayment; } });
|
|
103
|
+
var build_unsigned_tx_1 = require("./helpers/build-unsigned-tx");
|
|
104
|
+
Object.defineProperty(exports, "buildUnsignedPaymentTx", { enumerable: true, get: function () { return build_unsigned_tx_1.buildUnsignedPaymentTx; } });
|
|
105
|
+
// ─── Middleware ───────────────────────────────────────────────────────
|
|
106
|
+
var express_1 = require("./middleware/express");
|
|
107
|
+
Object.defineProperty(exports, "x402Middleware", { enumerable: true, get: function () { return express_1.x402Middleware; } });
|
|
108
|
+
var cap_1 = require("./middleware/cap");
|
|
109
|
+
Object.defineProperty(exports, "gateService", { enumerable: true, get: function () { return cap_1.gateService; } });
|
|
110
|
+
// ─── Bridge (lower-level: exposed for advanced consumers) ─────────────
|
|
111
|
+
exports.bridge = __importStar(require("./bridge"));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAP integration for x402 payment gating.
|
|
3
|
+
*
|
|
4
|
+
* CAP services receive requests through `srv.before(...)` / `srv.on(...)`
|
|
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
|
|
7
|
+
* CAP, not by `res.status(...)`.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
*
|
|
11
|
+
* import { gateService } from '@odatano/x402';
|
|
12
|
+
*
|
|
13
|
+
* class MyService extends cds.ApplicationService {
|
|
14
|
+
* async init() {
|
|
15
|
+
* gateService(this, {
|
|
16
|
+
* payTo: '...', network: 'cardano:preprod', asset: '<policy>.<name>',
|
|
17
|
+
* routePricing: { Prices: '10000', getBestPrice: '10000' },
|
|
18
|
+
* });
|
|
19
|
+
* return super.init();
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* The gate inspects each `req.event` (entity name OR action name) and
|
|
24
|
+
* matches it against `routePricing`. For unmapped events / entities,
|
|
25
|
+
* the request passes through unmodified.
|
|
26
|
+
*/
|
|
27
|
+
import cds from '@sap/cds';
|
|
28
|
+
import type { AssetTransferMethod, Network, PaymentClaim } from '../core/types';
|
|
29
|
+
export interface X402CapOptions {
|
|
30
|
+
payTo: string;
|
|
31
|
+
network: Network | string;
|
|
32
|
+
asset: string;
|
|
33
|
+
/** Single price (applies to all gated events). */
|
|
34
|
+
priceUnits?: string | number | bigint;
|
|
35
|
+
/**
|
|
36
|
+
* Per-event prices. Keys are CAP event names (entity name for CRUD,
|
|
37
|
+
* action name for actions). Events absent here pass through.
|
|
38
|
+
*/
|
|
39
|
+
routePricing?: Record<string, string | number | bigint>;
|
|
40
|
+
description?: string;
|
|
41
|
+
mimeType?: string;
|
|
42
|
+
assetTransferMethod?: AssetTransferMethod;
|
|
43
|
+
maxTimeoutSeconds?: number;
|
|
44
|
+
extra?: Record<string, unknown>;
|
|
45
|
+
settlePollBudgetMs?: number;
|
|
46
|
+
allowNoTtl?: boolean;
|
|
47
|
+
onAccepted?: (claim: PaymentClaim, req: cds.Request) => void | Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Optional resource URL builder. Defaults to the request's HTTP URL
|
|
50
|
+
* when available, falling back to `cap://<event>`. Pass a custom
|
|
51
|
+
* builder to embed pair / entity id in the resource string.
|
|
52
|
+
*/
|
|
53
|
+
resourceUrl?: (req: cds.Request) => string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Attach the x402 gate to a CAP ApplicationService. Returns the service
|
|
57
|
+
* (chainable) so callers can fluently wire multiple middlewares.
|
|
58
|
+
*
|
|
59
|
+
* The gate registers as `srv.before('*', ...)` which fires for every
|
|
60
|
+
* event on the service. We filter inside the handler based on
|
|
61
|
+
* `routePricing` — registering per-entity would lose actions, and
|
|
62
|
+
* per-event arrays don't support the `'*'` fallback we want.
|
|
63
|
+
*/
|
|
64
|
+
export declare function gateService<S extends cds.Service>(srv: S, opts: X402CapOptions): S;
|
|
65
|
+
//# sourceMappingURL=cap.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CAP integration for x402 payment gating.
|
|
4
|
+
*
|
|
5
|
+
* CAP services receive requests through `srv.before(...)` / `srv.on(...)`
|
|
6
|
+
* handlers, not Express middleware. For OData-served entities, the 402
|
|
7
|
+
* needs to come from `req.reject(402, body)` — the response is built by
|
|
8
|
+
* CAP, not by `res.status(...)`.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* import { gateService } from '@odatano/x402';
|
|
13
|
+
*
|
|
14
|
+
* class MyService extends cds.ApplicationService {
|
|
15
|
+
* async init() {
|
|
16
|
+
* gateService(this, {
|
|
17
|
+
* payTo: '...', network: 'cardano:preprod', asset: '<policy>.<name>',
|
|
18
|
+
* routePricing: { Prices: '10000', getBestPrice: '10000' },
|
|
19
|
+
* });
|
|
20
|
+
* return super.init();
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* The gate inspects each `req.event` (entity name OR action name) and
|
|
25
|
+
* matches it against `routePricing`. For unmapped events / entities,
|
|
26
|
+
* the request passes through unmodified.
|
|
27
|
+
*/
|
|
28
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
29
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
|
+
};
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.gateService = gateService;
|
|
33
|
+
const cds_1 = __importDefault(require("@sap/cds"));
|
|
34
|
+
const requirements_1 = require("../core/requirements");
|
|
35
|
+
const verify_1 = require("../facilitator/verify");
|
|
36
|
+
const errors_1 = require("../core/errors");
|
|
37
|
+
const log = cds_1.default.log('x402');
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the routePricing key for a CAP request.
|
|
40
|
+
*
|
|
41
|
+
* CAP fires two distinct shapes:
|
|
42
|
+
* - CRUD on an entity: req.event === 'READ' | 'CREATE' | 'UPDATE' | 'DELETE'
|
|
43
|
+
* req.target.name === '<Service>.<Entity>'
|
|
44
|
+
* - Action call: req.event === '<actionName>'
|
|
45
|
+
* req.target may be empty or the bound-entity target
|
|
46
|
+
*
|
|
47
|
+
* routePricing keys are meant to be human-readable identifiers (entity
|
|
48
|
+
* names or action names), so we try the entity name first (more specific)
|
|
49
|
+
* and fall back to the event verb. Either match wins; both miss falls
|
|
50
|
+
* through to opts.priceUnits or null.
|
|
51
|
+
*/
|
|
52
|
+
function pickPriceUnits(req, opts) {
|
|
53
|
+
if (opts.routePricing) {
|
|
54
|
+
const event = String(req.event ?? '');
|
|
55
|
+
const targetName = req.target?.name ?? '';
|
|
56
|
+
const entitySegment = targetName.split('.').pop() ?? '';
|
|
57
|
+
const price = opts.routePricing[entitySegment] ?? opts.routePricing[event];
|
|
58
|
+
if (price != null)
|
|
59
|
+
return String(price);
|
|
60
|
+
if (opts.priceUnits != null)
|
|
61
|
+
return String(opts.priceUnits);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return opts.priceUnits != null ? String(opts.priceUnits) : null;
|
|
65
|
+
}
|
|
66
|
+
function getHeader(req, name) {
|
|
67
|
+
// CAP's req.http?.req exposes the underlying express request. Fall
|
|
68
|
+
// back to req._.req for older shapes.
|
|
69
|
+
const httpReq = req;
|
|
70
|
+
const hdrs = httpReq.http?.req?.headers ?? httpReq._?.req?.headers;
|
|
71
|
+
const v = hdrs?.[name];
|
|
72
|
+
return Array.isArray(v) ? v[0] : v;
|
|
73
|
+
}
|
|
74
|
+
function getResourceUrl(req, opts) {
|
|
75
|
+
if (opts.resourceUrl)
|
|
76
|
+
return opts.resourceUrl(req);
|
|
77
|
+
const httpReq = req;
|
|
78
|
+
return httpReq.http?.req?.originalUrl ?? httpReq.http?.req?.url ?? `cap://${req.event}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Attach the x402 gate to a CAP ApplicationService. Returns the service
|
|
82
|
+
* (chainable) so callers can fluently wire multiple middlewares.
|
|
83
|
+
*
|
|
84
|
+
* The gate registers as `srv.before('*', ...)` which fires for every
|
|
85
|
+
* event on the service. We filter inside the handler based on
|
|
86
|
+
* `routePricing` — registering per-entity would lose actions, and
|
|
87
|
+
* per-event arrays don't support the `'*'` fallback we want.
|
|
88
|
+
*/
|
|
89
|
+
function gateService(srv, opts) {
|
|
90
|
+
if (!opts.payTo)
|
|
91
|
+
throw new Error('gateService: payTo is required');
|
|
92
|
+
if (!opts.network)
|
|
93
|
+
throw new Error('gateService: network is required');
|
|
94
|
+
if (!opts.asset)
|
|
95
|
+
throw new Error('gateService: asset is required');
|
|
96
|
+
if (opts.priceUnits == null && !opts.routePricing) {
|
|
97
|
+
throw new Error('gateService: priceUnits or routePricing is required');
|
|
98
|
+
}
|
|
99
|
+
srv.before('*', async function x402CapGate(req) {
|
|
100
|
+
const priceUnits = pickPriceUnits(req, opts);
|
|
101
|
+
if (priceUnits == null)
|
|
102
|
+
return; // unmapped → pass through
|
|
103
|
+
const requirementsBody = (0, requirements_1.buildPaymentRequirements)({
|
|
104
|
+
amount: priceUnits,
|
|
105
|
+
asset: opts.asset,
|
|
106
|
+
payTo: opts.payTo,
|
|
107
|
+
network: opts.network,
|
|
108
|
+
resource: {
|
|
109
|
+
url: getResourceUrl(req, opts),
|
|
110
|
+
description: opts.description ?? '',
|
|
111
|
+
mimeType: opts.mimeType ?? 'application/json',
|
|
112
|
+
},
|
|
113
|
+
...(opts.assetTransferMethod ? { assetTransferMethod: opts.assetTransferMethod } : {}),
|
|
114
|
+
...(opts.maxTimeoutSeconds !== undefined ? { maxTimeoutSeconds: opts.maxTimeoutSeconds } : {}),
|
|
115
|
+
...(opts.extra ? { extra: opts.extra } : {}),
|
|
116
|
+
withMissingHeaderError: true,
|
|
117
|
+
});
|
|
118
|
+
const headerVal = getHeader(req, 'payment-signature');
|
|
119
|
+
const processArgs = {
|
|
120
|
+
paymentHeader: headerVal,
|
|
121
|
+
requirementsBody,
|
|
122
|
+
};
|
|
123
|
+
if (opts.settlePollBudgetMs !== undefined) {
|
|
124
|
+
processArgs.settlePollBudgetMs = opts.settlePollBudgetMs;
|
|
125
|
+
}
|
|
126
|
+
if (opts.allowNoTtl)
|
|
127
|
+
processArgs.allowNoTtl = true;
|
|
128
|
+
if (opts.onAccepted) {
|
|
129
|
+
processArgs.onAccepted = (claim) => opts.onAccepted(claim, req);
|
|
130
|
+
}
|
|
131
|
+
// ─── Run the pipeline. Only the orchestrator's internal errors are
|
|
132
|
+
// trapped here — `req.reject` MUST be called outside this catch
|
|
133
|
+
// because it throws synchronously, and re-catching that throw
|
|
134
|
+
// would translate the 402 into a 500. ─────────────────────────
|
|
135
|
+
let result;
|
|
136
|
+
try {
|
|
137
|
+
result = await (0, verify_1.process)(processArgs);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
log.error('x402 CAP gate internal error', err);
|
|
141
|
+
// `reject` throws; we DO NOT wrap this in another try/catch.
|
|
142
|
+
req
|
|
143
|
+
.reject(500, 'x402 internal error');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// ─── Apply the result. From here on, `req.reject` is called once
|
|
147
|
+
// and we let its synchronous throw bubble — CAP's dispatcher
|
|
148
|
+
// wraps it into the OData error response.
|
|
149
|
+
if (result.kind === 'accepted') {
|
|
150
|
+
req.payment = result.payment;
|
|
151
|
+
const httpRes = req.http?.res;
|
|
152
|
+
httpRes?.setHeader('X-PAYMENT-RESPONSE', result.paymentResponseB64);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// rejected | pending → 402 with the requirements body
|
|
156
|
+
const body = { ...result.requirementsBody };
|
|
157
|
+
const baseError = (result.requirementsBody.error ?? 'payment required').toString();
|
|
158
|
+
if (result.code && result.code !== errors_1.Codes.MISSING_HEADER) {
|
|
159
|
+
body.error = `${baseError} (${result.code}): ${result.reason ?? ''}`.trim();
|
|
160
|
+
}
|
|
161
|
+
if (result.kind === 'pending') {
|
|
162
|
+
body.pending = true;
|
|
163
|
+
if (result.txHash)
|
|
164
|
+
body.transaction = result.txHash;
|
|
165
|
+
}
|
|
166
|
+
req
|
|
167
|
+
.reject(402, JSON.stringify(body));
|
|
168
|
+
});
|
|
169
|
+
return srv;
|
|
170
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express middleware factory for Cardano-x402 payment gating.
|
|
3
|
+
*
|
|
4
|
+
* Mount on a route or service path to gate every request beneath it.
|
|
5
|
+
* The `skipPaths` regex carves out paths buyers MUST be able to fetch
|
|
6
|
+
* without paying (e.g. OData `$metadata`, `$batch` previews).
|
|
7
|
+
*
|
|
8
|
+
* Two pricing modes:
|
|
9
|
+
* 1. `priceUnits` — single price for everything under this mount.
|
|
10
|
+
* 2. `routePricing` — { 'EntityOrActionName': 'priceUnits' }, keyed
|
|
11
|
+
* by the last URL segment (with OData function
|
|
12
|
+
* args stripped). Unmapped paths pass through.
|
|
13
|
+
*
|
|
14
|
+
* The 402 body is the canonical v2 shape (`x402Version: 2`, `accepts[]`,
|
|
15
|
+
* etc). When the rejection has a more specific cause than "missing
|
|
16
|
+
* header", we append `(code): reason` to the `error` string so clients
|
|
17
|
+
* can extract the code without breaking wire format.
|
|
18
|
+
*/
|
|
19
|
+
import type { Request, RequestHandler } from 'express';
|
|
20
|
+
import type { AssetTransferMethod, PaymentClaim, Network } from '../core/types';
|
|
21
|
+
declare module 'express-serve-static-core' {
|
|
22
|
+
interface Request {
|
|
23
|
+
payment?: PaymentClaim;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export interface X402MiddlewareOptions {
|
|
27
|
+
/** Bech32 recipient. */
|
|
28
|
+
payTo: string;
|
|
29
|
+
/** v2 network identifier. */
|
|
30
|
+
network: Network | string;
|
|
31
|
+
/** v2 asset string: 'lovelace' or '<policy>.<nameHex>'. */
|
|
32
|
+
asset: string;
|
|
33
|
+
/**
|
|
34
|
+
* Single price for everything under this mount. Mutually exclusive
|
|
35
|
+
* with `routePricing` (but coexistable: routePricing wins where
|
|
36
|
+
* defined, falls back to priceUnits otherwise).
|
|
37
|
+
*/
|
|
38
|
+
priceUnits?: string | number | bigint;
|
|
39
|
+
/** Per-route prices keyed by the URL's last segment. */
|
|
40
|
+
routePricing?: Record<string, string | number | bigint>;
|
|
41
|
+
/** Regex of paths that bypass payment (default: $metadata, $batch, root, /index). */
|
|
42
|
+
skipPaths?: RegExp;
|
|
43
|
+
/** Shown in `accepts[0].resource.description`. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** Override default `accepts[0].resource.mimeType` ('application/json'). */
|
|
46
|
+
mimeType?: string;
|
|
47
|
+
/** v2 `assetTransferMethod`. Default 'default'. */
|
|
48
|
+
assetTransferMethod?: AssetTransferMethod;
|
|
49
|
+
/** Buyer-side timeout hint. Default 600. */
|
|
50
|
+
maxTimeoutSeconds?: number;
|
|
51
|
+
/** Free-form extras (decimals, fingerprint, UI hints). */
|
|
52
|
+
extra?: Record<string, unknown>;
|
|
53
|
+
/** Settle poll budget (ms). Default 60_000. */
|
|
54
|
+
settlePollBudgetMs?: number;
|
|
55
|
+
/** If true, accept tx with no TTL set. Default false (spec-strict). */
|
|
56
|
+
allowNoTtl?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Audit / persistence callback. Invoked exactly once per accepted
|
|
59
|
+
* payment, after settle confirms. Errors here are logged but never
|
|
60
|
+
* block serving the response.
|
|
61
|
+
*/
|
|
62
|
+
onAccepted?: (claim: PaymentClaim, req: Request) => void | Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/** Build the Express middleware. */
|
|
65
|
+
export declare function x402Middleware(opts: X402MiddlewareOptions): RequestHandler;
|
|
66
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Express middleware factory for Cardano-x402 payment gating.
|
|
4
|
+
*
|
|
5
|
+
* Mount on a route or service path to gate every request beneath it.
|
|
6
|
+
* The `skipPaths` regex carves out paths buyers MUST be able to fetch
|
|
7
|
+
* without paying (e.g. OData `$metadata`, `$batch` previews).
|
|
8
|
+
*
|
|
9
|
+
* Two pricing modes:
|
|
10
|
+
* 1. `priceUnits` — single price for everything under this mount.
|
|
11
|
+
* 2. `routePricing` — { 'EntityOrActionName': 'priceUnits' }, keyed
|
|
12
|
+
* by the last URL segment (with OData function
|
|
13
|
+
* args stripped). Unmapped paths pass through.
|
|
14
|
+
*
|
|
15
|
+
* The 402 body is the canonical v2 shape (`x402Version: 2`, `accepts[]`,
|
|
16
|
+
* etc). When the rejection has a more specific cause than "missing
|
|
17
|
+
* header", we append `(code): reason` to the `error` string so clients
|
|
18
|
+
* can extract the code without breaking wire format.
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.x402Middleware = x402Middleware;
|
|
25
|
+
const cds_1 = __importDefault(require("@sap/cds"));
|
|
26
|
+
const requirements_1 = require("../core/requirements");
|
|
27
|
+
const verify_1 = require("../facilitator/verify");
|
|
28
|
+
const errors_1 = require("../core/errors");
|
|
29
|
+
const log = cds_1.default.log('x402');
|
|
30
|
+
function pickPriceUnits(req, opts) {
|
|
31
|
+
if (opts.routePricing) {
|
|
32
|
+
// Last URL segment, OData function-args stripped:
|
|
33
|
+
// /odata/v4/price/getBestPrice(pair='ADA-USD') → getBestPrice
|
|
34
|
+
const segment = (req.path.split('/').pop() ?? '').split('(')[0];
|
|
35
|
+
const price = opts.routePricing[segment];
|
|
36
|
+
if (price != null)
|
|
37
|
+
return String(price);
|
|
38
|
+
if (opts.priceUnits != null)
|
|
39
|
+
return String(opts.priceUnits);
|
|
40
|
+
return null; // unmapped under routePricing = pass through
|
|
41
|
+
}
|
|
42
|
+
return opts.priceUnits != null ? String(opts.priceUnits) : null;
|
|
43
|
+
}
|
|
44
|
+
/** Build the Express middleware. */
|
|
45
|
+
function x402Middleware(opts) {
|
|
46
|
+
if (!opts.payTo)
|
|
47
|
+
throw new Error('x402Middleware: payTo is required');
|
|
48
|
+
if (!opts.network)
|
|
49
|
+
throw new Error('x402Middleware: network is required');
|
|
50
|
+
if (!opts.asset)
|
|
51
|
+
throw new Error('x402Middleware: asset is required');
|
|
52
|
+
if (opts.priceUnits == null && !opts.routePricing) {
|
|
53
|
+
throw new Error('x402Middleware: priceUnits or routePricing is required');
|
|
54
|
+
}
|
|
55
|
+
const skipPaths = opts.skipPaths ?? /(^\/?$|\$metadata|\$batch|^\/?\?|^\/index)/i;
|
|
56
|
+
return async function x402Express(req, res, next) {
|
|
57
|
+
try {
|
|
58
|
+
if (skipPaths.test(req.path))
|
|
59
|
+
return next();
|
|
60
|
+
const priceUnits = pickPriceUnits(req, opts);
|
|
61
|
+
if (priceUnits == null)
|
|
62
|
+
return next(); // unmapped path = pass through
|
|
63
|
+
const requirementsBody = (0, requirements_1.buildPaymentRequirements)({
|
|
64
|
+
amount: priceUnits,
|
|
65
|
+
asset: opts.asset,
|
|
66
|
+
payTo: opts.payTo,
|
|
67
|
+
network: opts.network,
|
|
68
|
+
resource: {
|
|
69
|
+
url: req.originalUrl ?? req.url,
|
|
70
|
+
description: opts.description ?? '',
|
|
71
|
+
mimeType: opts.mimeType ?? 'application/json',
|
|
72
|
+
},
|
|
73
|
+
...(opts.assetTransferMethod ? { assetTransferMethod: opts.assetTransferMethod } : {}),
|
|
74
|
+
...(opts.maxTimeoutSeconds !== undefined ? { maxTimeoutSeconds: opts.maxTimeoutSeconds } : {}),
|
|
75
|
+
...(opts.extra ? { extra: opts.extra } : {}),
|
|
76
|
+
withMissingHeaderError: true,
|
|
77
|
+
});
|
|
78
|
+
const headerVal = req.headers['payment-signature'];
|
|
79
|
+
const processArgs = {
|
|
80
|
+
paymentHeader: headerVal,
|
|
81
|
+
requirementsBody,
|
|
82
|
+
};
|
|
83
|
+
if (opts.settlePollBudgetMs !== undefined) {
|
|
84
|
+
processArgs.settlePollBudgetMs = opts.settlePollBudgetMs;
|
|
85
|
+
}
|
|
86
|
+
if (opts.allowNoTtl)
|
|
87
|
+
processArgs.allowNoTtl = true;
|
|
88
|
+
if (opts.onAccepted) {
|
|
89
|
+
processArgs.onAccepted = (claim) => opts.onAccepted(claim, req);
|
|
90
|
+
}
|
|
91
|
+
const result = await (0, verify_1.process)(processArgs);
|
|
92
|
+
if (result.kind === 'accepted') {
|
|
93
|
+
res.setHeader('X-PAYMENT-RESPONSE', result.paymentResponseB64);
|
|
94
|
+
req.payment = result.payment;
|
|
95
|
+
return next();
|
|
96
|
+
}
|
|
97
|
+
// rejected | pending → 402
|
|
98
|
+
const body = { ...result.requirementsBody };
|
|
99
|
+
const baseError = (result.requirementsBody.error ?? 'payment required').toString();
|
|
100
|
+
if (result.code && result.code !== errors_1.Codes.MISSING_HEADER) {
|
|
101
|
+
body.error = `${baseError} (${result.code}): ${result.reason ?? ''}`.trim();
|
|
102
|
+
}
|
|
103
|
+
if (result.kind === 'pending') {
|
|
104
|
+
// Add the spec-defined "pending" markers so the buyer can poll.
|
|
105
|
+
body.pending = true;
|
|
106
|
+
if (result.txHash)
|
|
107
|
+
body.transaction = result.txHash;
|
|
108
|
+
}
|
|
109
|
+
res.status(402).json(body);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
log.error('x402 middleware failed', err);
|
|
113
|
+
next(err);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
package/srv/plugin.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAP plugin registration for `@odatano/x402`.
|
|
3
|
+
*
|
|
4
|
+
* Unlike `@odatano/core`, this plugin doesn't auto-serve any CDS
|
|
5
|
+
* entities — x402 is a library, not a service. We use the
|
|
6
|
+
* `cds.on('served')` hook only to initialise the bridge (warm up the
|
|
7
|
+
* @odatano/core connection) and the `cds.on('shutdown')` hook to clean
|
|
8
|
+
* up. Consumers wire middleware / gateService themselves inside their
|
|
9
|
+
* own service init().
|
|
10
|
+
*
|
|
11
|
+
* NEVER throws on init failure — the plugin must not crash the host
|
|
12
|
+
* CAP application. Errors are logged; calls into the bridge later
|
|
13
|
+
* will fail with `BRIDGE_UNAVAILABLE`.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=plugin.d.ts.map
|
package/srv/plugin.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CAP plugin registration for `@odatano/x402`.
|
|
4
|
+
*
|
|
5
|
+
* Unlike `@odatano/core`, this plugin doesn't auto-serve any CDS
|
|
6
|
+
* entities — x402 is a library, not a service. We use the
|
|
7
|
+
* `cds.on('served')` hook only to initialise the bridge (warm up the
|
|
8
|
+
* @odatano/core connection) and the `cds.on('shutdown')` hook to clean
|
|
9
|
+
* up. Consumers wire middleware / gateService themselves inside their
|
|
10
|
+
* own service init().
|
|
11
|
+
*
|
|
12
|
+
* NEVER throws on init failure — the plugin must not crash the host
|
|
13
|
+
* CAP application. Errors are logged; calls into the bridge later
|
|
14
|
+
* will fail with `BRIDGE_UNAVAILABLE`.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
const cds_1 = __importDefault(require("@sap/cds"));
|
|
54
|
+
const bridge = __importStar(require("./bridge"));
|
|
55
|
+
const log = cds_1.default.log('x402');
|
|
56
|
+
cds_1.default.on('served', async () => {
|
|
57
|
+
try {
|
|
58
|
+
await bridge.init();
|
|
59
|
+
log.info('@odatano/x402 bridge ready');
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
log.warn('@odatano/x402 bridge init failed (will retry on first request):', err?.message ?? err);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
cds_1.default.on('shutdown', async () => {
|
|
66
|
+
try {
|
|
67
|
+
await bridge.shutdown();
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
log.warn('@odatano/x402 bridge shutdown:', err?.message ?? err);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
module.exports = {};
|