@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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cardano-x402-v2 asset identifier handling.
3
+ *
4
+ * v2 expresses an asset as a single string:
5
+ * - For native assets: `<policyIdHex>.<assetNameHex>` (DOT separator)
6
+ * where policyId is 28 bytes (56 hex chars) and assetNameHex is 0..64 hex chars.
7
+ * An empty name (NFT-style) is `<policyIdHex>.`
8
+ * - For ADA: the literal string `'lovelace'`
9
+ *
10
+ * v1 split policy and name across `asset` + `extra.assetNameHex`; we
11
+ * refuse that shape outright so consumers can't mix specs.
12
+ */
13
+ export interface ParsedAsset {
14
+ /** raw v2 string as emitted by buildPaymentRequirements */
15
+ raw: string;
16
+ /** true when the asset is ADA (lovelace) */
17
+ isLovelace: boolean;
18
+ /** lowercase 56-char hex; empty string when isLovelace */
19
+ policyId: string;
20
+ /** lowercase hex, 0..64 chars; empty string when isLovelace OR when name is empty */
21
+ assetNameHex: string;
22
+ /**
23
+ * Concatenation used as the canonical UTxO `unit` key by Blockfrost /
24
+ * Koios / ODATANO (`policyId + assetNameHex`). Empty for lovelace —
25
+ * comparisons against UTxO assets short-circuit via `isLovelace`.
26
+ */
27
+ unit: string;
28
+ }
29
+ /** Parse a v2 asset string. Throws X402Error on malformed input. */
30
+ export declare function parseAsset(s: string): ParsedAsset;
31
+ /** Build a v2 asset string from policy + assetName parts. */
32
+ export declare function buildAssetString(policyId: string, assetNameHex?: string): string;
33
+ //# sourceMappingURL=asset.d.ts.map
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /**
3
+ * Cardano-x402-v2 asset identifier handling.
4
+ *
5
+ * v2 expresses an asset as a single string:
6
+ * - For native assets: `<policyIdHex>.<assetNameHex>` (DOT separator)
7
+ * where policyId is 28 bytes (56 hex chars) and assetNameHex is 0..64 hex chars.
8
+ * An empty name (NFT-style) is `<policyIdHex>.`
9
+ * - For ADA: the literal string `'lovelace'`
10
+ *
11
+ * v1 split policy and name across `asset` + `extra.assetNameHex`; we
12
+ * refuse that shape outright so consumers can't mix specs.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.parseAsset = parseAsset;
16
+ exports.buildAssetString = buildAssetString;
17
+ const errors_1 = require("./errors");
18
+ const POLICY_RE = /^[0-9a-f]{56}$/i;
19
+ const NAME_RE = /^[0-9a-f]{0,64}$/i;
20
+ /** Parse a v2 asset string. Throws X402Error on malformed input. */
21
+ function parseAsset(s) {
22
+ if (typeof s !== 'string' || s.length === 0) {
23
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, 'asset must be a non-empty string');
24
+ }
25
+ if (s === 'lovelace') {
26
+ return { raw: s, isLovelace: true, policyId: '', assetNameHex: '', unit: '' };
27
+ }
28
+ const dot = s.indexOf('.');
29
+ if (dot < 0) {
30
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `asset '${s}' must be 'lovelace' or '<policyIdHex>.<assetNameHex>' (dot-separated)`);
31
+ }
32
+ const policyId = s.slice(0, dot).toLowerCase();
33
+ const assetNameHex = s.slice(dot + 1).toLowerCase();
34
+ if (!POLICY_RE.test(policyId)) {
35
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `asset policyId '${policyId}' must be 28-byte (56-char) hex`);
36
+ }
37
+ if (!NAME_RE.test(assetNameHex)) {
38
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `asset name '${assetNameHex}' must be 0..32 byte (0..64 char) hex`);
39
+ }
40
+ return {
41
+ raw: s,
42
+ isLovelace: false,
43
+ policyId,
44
+ assetNameHex,
45
+ unit: (policyId + assetNameHex).toLowerCase(),
46
+ };
47
+ }
48
+ /** Build a v2 asset string from policy + assetName parts. */
49
+ function buildAssetString(policyId, assetNameHex = '') {
50
+ if (!POLICY_RE.test(policyId)) {
51
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `buildAssetString: policyId '${policyId}' must be 56-char hex`);
52
+ }
53
+ if (assetNameHex && !NAME_RE.test(assetNameHex)) {
54
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `buildAssetString: assetNameHex '${assetNameHex}' must be 0..64-char hex`);
55
+ }
56
+ return `${policyId.toLowerCase()}.${assetNameHex.toLowerCase()}`;
57
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Decode the `PAYMENT-SIGNATURE` header (Cardano-x402-v2 wire format).
3
+ *
4
+ * Wire format:
5
+ * PAYMENT-SIGNATURE: base64(JSON.stringify({
6
+ * x402Version: 2,
7
+ * scheme: 'exact',
8
+ * network: 'cardano:preprod' | 'cardano:mainnet' | 'cardano:preview',
9
+ * payload: {
10
+ * transaction: '<base64 CBOR of signed tx>',
11
+ * nonce: '<txHash>#<outputIndex>'
12
+ * }
13
+ * }))
14
+ *
15
+ * The decoder is **pure** — no chain calls, no DB. It produces a
16
+ * `DecodedPayment` that downstream `validate.ts` checks against
17
+ * `PaymentRequirementEntry` (the 6 mandatory checks).
18
+ */
19
+ import type { DecodedPayment } from './types';
20
+ /**
21
+ * Decode a `PAYMENT-SIGNATURE` header value end-to-end. Throws X402Error
22
+ * with a precise `code` on any malformed input — the caller catches and
23
+ * surfaces the code in the 402 response body.
24
+ */
25
+ export declare function decode(paymentHeader: string | undefined | null): DecodedPayment;
26
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ /**
3
+ * Decode the `PAYMENT-SIGNATURE` header (Cardano-x402-v2 wire format).
4
+ *
5
+ * Wire format:
6
+ * PAYMENT-SIGNATURE: base64(JSON.stringify({
7
+ * x402Version: 2,
8
+ * scheme: 'exact',
9
+ * network: 'cardano:preprod' | 'cardano:mainnet' | 'cardano:preview',
10
+ * payload: {
11
+ * transaction: '<base64 CBOR of signed tx>',
12
+ * nonce: '<txHash>#<outputIndex>'
13
+ * }
14
+ * }))
15
+ *
16
+ * The decoder is **pure** — no chain calls, no DB. It produces a
17
+ * `DecodedPayment` that downstream `validate.ts` checks against
18
+ * `PaymentRequirementEntry` (the 6 mandatory checks).
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.decode = decode;
55
+ const CSL = __importStar(require("@emurgo/cardano-serialization-lib-nodejs"));
56
+ const errors_1 = require("./errors");
57
+ const SUPPORTED_VERSION = 2;
58
+ const SUPPORTED_SCHEME = 'exact';
59
+ const NONCE_RE = /^([0-9a-f]{64})#(\d+)$/i;
60
+ function decodeBase64ToBuffer(s, errCode) {
61
+ // Node's Buffer.from is lenient (silently drops bad chars). Re-encode
62
+ // and compare modulo padding to catch malformed input early — otherwise
63
+ // garbage in `transaction` would only fail at CBOR parse time with a
64
+ // confusing error.
65
+ const buf = Buffer.from(s, 'base64');
66
+ if (buf.toString('base64').replace(/=+$/, '') !== String(s).replace(/=+$/, '')) {
67
+ throw new errors_1.X402Error(errCode, 'malformed base64 payload');
68
+ }
69
+ return buf;
70
+ }
71
+ function extractOutputs(txBody) {
72
+ const out = txBody.outputs();
73
+ const result = [];
74
+ for (let i = 0; i < out.len(); i++) {
75
+ const o = out.get(i);
76
+ const addr = o.address().to_bech32();
77
+ const value = o.amount();
78
+ const lovelace = value.coin().to_str();
79
+ const assets = [];
80
+ const ma = value.multiasset();
81
+ if (ma) {
82
+ const policies = ma.keys();
83
+ for (let p = 0; p < policies.len(); p++) {
84
+ const policy = policies.get(p);
85
+ const policyHex = Buffer.from(policy.to_bytes()).toString('hex').toLowerCase();
86
+ const assetMap = ma.get(policy);
87
+ if (!assetMap)
88
+ continue;
89
+ const names = assetMap.keys();
90
+ for (let n = 0; n < names.len(); n++) {
91
+ const name = names.get(n);
92
+ const nameHex = Buffer.from(name.name()).toString('hex').toLowerCase();
93
+ const qty = assetMap.get(name);
94
+ if (!qty)
95
+ continue;
96
+ assets.push({
97
+ unit: (policyHex + nameHex),
98
+ policyId: policyHex,
99
+ assetNameHex: nameHex,
100
+ quantity: qty.to_str(),
101
+ });
102
+ }
103
+ }
104
+ }
105
+ result.push({ outputIndex: i, address: addr, lovelace, assets });
106
+ }
107
+ return result;
108
+ }
109
+ function extractInputs(txBody) {
110
+ const ins = txBody.inputs();
111
+ const result = [];
112
+ for (let i = 0; i < ins.len(); i++) {
113
+ const inp = ins.get(i);
114
+ result.push({
115
+ txHash: Buffer.from(inp.transaction_id().to_bytes()).toString('hex').toLowerCase(),
116
+ outputIndex: inp.index(),
117
+ });
118
+ }
119
+ return result;
120
+ }
121
+ /**
122
+ * Pull validity range bounds. CSL exposes `ttl()` (upper) since Shelley
123
+ * and `validity_start_interval_bignum()` (lower) since Allegra. Both can
124
+ * be absent — in which case we return null and the TTL check is skipped
125
+ * (per v2 spec: only validate TTL if buyer set one).
126
+ */
127
+ function extractValidityRange(txBody) {
128
+ let ttlSlot = null;
129
+ let validityStartSlot = null;
130
+ try {
131
+ const ttl = txBody.ttl_bignum();
132
+ if (ttl) {
133
+ // BigNum → string → number; slots fit comfortably in JS number
134
+ // (current preprod ~85M, max safe int 9e15).
135
+ ttlSlot = Number(ttl.to_str());
136
+ if (!Number.isFinite(ttlSlot))
137
+ ttlSlot = null;
138
+ }
139
+ }
140
+ catch {
141
+ ttlSlot = null;
142
+ }
143
+ try {
144
+ const start = txBody.validity_start_interval_bignum();
145
+ if (start) {
146
+ validityStartSlot = Number(start.to_str());
147
+ if (!Number.isFinite(validityStartSlot))
148
+ validityStartSlot = null;
149
+ }
150
+ }
151
+ catch {
152
+ validityStartSlot = null;
153
+ }
154
+ return { ttlSlot, validityStartSlot };
155
+ }
156
+ function parseNonceRef(nonce) {
157
+ const m = NONCE_RE.exec(nonce);
158
+ if (!m) {
159
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_NONCE_FORMAT, `nonce '${nonce}' must be '<txHash>#<outputIndex>' (64-hex#int)`);
160
+ }
161
+ const idx = Number(m[2]);
162
+ if (!Number.isFinite(idx) || idx < 0 || idx > 65535) {
163
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_NONCE_FORMAT, `nonce output index ${m[2]} out of range`);
164
+ }
165
+ return { txHash: m[1].toLowerCase(), index: idx };
166
+ }
167
+ /**
168
+ * Decode a `PAYMENT-SIGNATURE` header value end-to-end. Throws X402Error
169
+ * with a precise `code` on any malformed input — the caller catches and
170
+ * surfaces the code in the 402 response body.
171
+ */
172
+ function decode(paymentHeader) {
173
+ if (!paymentHeader || typeof paymentHeader !== 'string') {
174
+ throw new errors_1.X402Error(errors_1.Codes.MISSING_HEADER);
175
+ }
176
+ // 1. base64 → JSON
177
+ const outerBuf = decodeBase64ToBuffer(paymentHeader, errors_1.Codes.INVALID_BASE64);
178
+ let raw;
179
+ try {
180
+ raw = JSON.parse(outerBuf.toString('utf8'));
181
+ }
182
+ catch {
183
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_JSON, 'PAYMENT-SIGNATURE body is not valid JSON');
184
+ }
185
+ // 2. Field shape
186
+ for (const f of ['x402Version', 'scheme', 'network', 'payload']) {
187
+ if (!(f in raw))
188
+ throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, `missing field: ${f}`);
189
+ }
190
+ if (raw.x402Version !== SUPPORTED_VERSION) {
191
+ throw new errors_1.X402Error(errors_1.Codes.UNSUPPORTED_VERSION, `x402Version ${raw.x402Version} not supported (only ${SUPPORTED_VERSION})`);
192
+ }
193
+ if (raw.scheme !== SUPPORTED_SCHEME) {
194
+ throw new errors_1.X402Error(errors_1.Codes.UNSUPPORTED_SCHEME, `scheme '${raw.scheme}' not supported (only '${SUPPORTED_SCHEME}')`);
195
+ }
196
+ const payload = raw.payload;
197
+ if (!payload || typeof payload.transaction !== 'string') {
198
+ throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, 'payload.transaction is required');
199
+ }
200
+ if (typeof payload.nonce !== 'string' || payload.nonce.length === 0) {
201
+ throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, 'payload.nonce is required (v2 UTxO-ref)');
202
+ }
203
+ // 3. Tx CBOR → CSL Transaction (parses both `transaction` and `fixed`
204
+ // representation; we need both — Transaction for body access,
205
+ // FixedTransaction for byte-stable hash).
206
+ const txBuf = decodeBase64ToBuffer(payload.transaction, errors_1.Codes.INVALID_CBOR);
207
+ let tx;
208
+ try {
209
+ tx = CSL.Transaction.from_bytes(txBuf);
210
+ }
211
+ catch {
212
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_CBOR, 'transaction CBOR did not decode');
213
+ }
214
+ // 4. Diagnostics
215
+ const txBody = tx.body();
216
+ const wits = tx.witness_set();
217
+ const vkeys = wits.vkeys();
218
+ const vkeyWitnessCount = vkeys ? vkeys.len() : 0;
219
+ const txHashBytes = CSL.FixedTransaction
220
+ .from_bytes(txBuf)
221
+ .transaction_hash()
222
+ .to_bytes();
223
+ const txHash = Buffer.from(txHashBytes).toString('hex').toLowerCase();
224
+ const validity = extractValidityRange(txBody);
225
+ const nonce = parseNonceRef(payload.nonce);
226
+ const envelope = {
227
+ x402Version: SUPPORTED_VERSION,
228
+ scheme: SUPPORTED_SCHEME,
229
+ network: raw.network,
230
+ payload: { transaction: payload.transaction, nonce: payload.nonce },
231
+ };
232
+ return {
233
+ envelope,
234
+ txCborHex: Buffer.from(txBuf).toString('hex'),
235
+ txHash,
236
+ outputs: extractOutputs(txBody),
237
+ inputs: extractInputs(txBody),
238
+ vkeyWitnessCount,
239
+ ttlSlot: validity.ttlSlot,
240
+ validityStartSlot: validity.validityStartSlot,
241
+ nonce,
242
+ };
243
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Typed errors for x402 (Cardano-v2) verification.
3
+ *
4
+ * Codes stay in lower_snake to match the masumi-spec convention so they
5
+ * are interoperable with v1 callers grepping for known strings. Codes
6
+ * specific to v2 (UTxO-ref nonce, TTL window) are marked below.
7
+ *
8
+ * The `code` field is surfaced in the 402 response body's `error` field
9
+ * (parenthesised after the human message), so it doubles as the
10
+ * machine-readable diagnostic for clients.
11
+ */
12
+ export declare class X402Error extends Error {
13
+ readonly code: string;
14
+ constructor(code: string, message?: string);
15
+ }
16
+ export declare const Codes: Readonly<{
17
+ readonly MISSING_HEADER: "missing_payment_header";
18
+ readonly INVALID_BASE64: "invalid_base64";
19
+ readonly INVALID_JSON: "invalid_json";
20
+ readonly MISSING_FIELD: "missing_field";
21
+ readonly UNSUPPORTED_VERSION: "unsupported_version";
22
+ readonly UNSUPPORTED_SCHEME: "unsupported_scheme";
23
+ readonly UNSUPPORTED_METHOD: "unsupported_transfer_method";
24
+ readonly INVALID_CBOR: "invalid_cbor";
25
+ readonly INVALID_NETWORK_FORMAT: "invalid_network_format";
26
+ readonly INVALID_ASSET_FORMAT: "invalid_asset_format";
27
+ readonly INVALID_NONCE_FORMAT: "invalid_nonce_format";
28
+ readonly NETWORK_MISMATCH: "network_mismatch";
29
+ readonly WRONG_RECIPIENT: "wrong_recipient";
30
+ readonly INSUFFICIENT_AMOUNT: "insufficient_amount";
31
+ readonly WRONG_ASSET: "wrong_asset";
32
+ readonly REPLAY: "replay_detected";
33
+ readonly NONCE_NOT_REFERENCED: "nonce_not_referenced";
34
+ readonly EXPIRED_TTL: "expired_ttl";
35
+ readonly UNSIGNED_TRANSACTION: "unsigned_transaction";
36
+ readonly SUBMIT_FAILED: "submit_failed";
37
+ readonly PENDING: "invalid_transaction_state";
38
+ readonly BRIDGE_UNAVAILABLE: "bridge_unavailable";
39
+ }>;
40
+ export type X402Code = typeof Codes[keyof typeof Codes];
41
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * Typed errors for x402 (Cardano-v2) verification.
4
+ *
5
+ * Codes stay in lower_snake to match the masumi-spec convention so they
6
+ * are interoperable with v1 callers grepping for known strings. Codes
7
+ * specific to v2 (UTxO-ref nonce, TTL window) are marked below.
8
+ *
9
+ * The `code` field is surfaced in the 402 response body's `error` field
10
+ * (parenthesised after the human message), so it doubles as the
11
+ * machine-readable diagnostic for clients.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.Codes = exports.X402Error = void 0;
15
+ class X402Error extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message ?? code);
19
+ this.name = 'X402Error';
20
+ this.code = code;
21
+ }
22
+ }
23
+ exports.X402Error = X402Error;
24
+ exports.Codes = Object.freeze({
25
+ // ---- decode ----
26
+ MISSING_HEADER: 'missing_payment_header',
27
+ INVALID_BASE64: 'invalid_base64',
28
+ INVALID_JSON: 'invalid_json',
29
+ MISSING_FIELD: 'missing_field',
30
+ UNSUPPORTED_VERSION: 'unsupported_version',
31
+ UNSUPPORTED_SCHEME: 'unsupported_scheme',
32
+ UNSUPPORTED_METHOD: 'unsupported_transfer_method',
33
+ INVALID_CBOR: 'invalid_cbor',
34
+ INVALID_NETWORK_FORMAT: 'invalid_network_format', // v2 requires 'cardano:<net>'
35
+ INVALID_ASSET_FORMAT: 'invalid_asset_format', // v2 requires '<policy>.<nameHex>'
36
+ INVALID_NONCE_FORMAT: 'invalid_nonce_format', // v2 requires '<txHash>#<index>'
37
+ // ---- validate (6 mandatory facilitator checks) ----
38
+ NETWORK_MISMATCH: 'network_mismatch', // check 1
39
+ WRONG_RECIPIENT: 'wrong_recipient', // check 2
40
+ INSUFFICIENT_AMOUNT: 'insufficient_amount', // check 3
41
+ WRONG_ASSET: 'wrong_asset', // check 4
42
+ REPLAY: 'replay_detected', // check 5 — UTxO already spent
43
+ NONCE_NOT_REFERENCED: 'nonce_not_referenced', // check 5 — UTxO not in tx inputs
44
+ EXPIRED_TTL: 'expired_ttl', // check 6 — validity range upper bound passed
45
+ // ---- supporting ----
46
+ UNSIGNED_TRANSACTION: 'unsigned_transaction', // sanity: no vkey witnesses
47
+ // ---- settle ----
48
+ SUBMIT_FAILED: 'submit_failed',
49
+ PENDING: 'invalid_transaction_state', // matches masumi spec
50
+ // ---- bridge / infrastructure ----
51
+ BRIDGE_UNAVAILABLE: 'bridge_unavailable',
52
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Cardano-x402-v2 network identifiers.
3
+ *
4
+ * v2 uses **colon** as separator: `cardano:mainnet | cardano:preprod | cardano:preview`.
5
+ * v1 used hyphen (`cardano-mainnet`). We accept only the v2 form on input
6
+ * and refuse v1 strings so callers can't silently misroute funds across
7
+ * networks.
8
+ */
9
+ export type Network = 'cardano:mainnet' | 'cardano:preprod' | 'cardano:preview';
10
+ export declare function isNetwork(s: unknown): s is Network;
11
+ /**
12
+ * Validate a network string and return it typed. Throws X402Error on
13
+ * malformed input — including v1-style hyphen variants — so the caller's
14
+ * 402 body carries a precise diagnostic.
15
+ */
16
+ export declare function parseNetwork(s: string): Network;
17
+ /** True iff `payload.network` (the buyer's claim) matches the server's requirement. */
18
+ export declare function networksMatch(claimed: string, required: Network): boolean;
19
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /**
3
+ * Cardano-x402-v2 network identifiers.
4
+ *
5
+ * v2 uses **colon** as separator: `cardano:mainnet | cardano:preprod | cardano:preview`.
6
+ * v1 used hyphen (`cardano-mainnet`). We accept only the v2 form on input
7
+ * and refuse v1 strings so callers can't silently misroute funds across
8
+ * networks.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.isNetwork = isNetwork;
12
+ exports.parseNetwork = parseNetwork;
13
+ exports.networksMatch = networksMatch;
14
+ const errors_1 = require("./errors");
15
+ const VALID = new Set(['cardano:mainnet', 'cardano:preprod', 'cardano:preview']);
16
+ function isNetwork(s) {
17
+ return typeof s === 'string' && VALID.has(s);
18
+ }
19
+ /**
20
+ * Validate a network string and return it typed. Throws X402Error on
21
+ * malformed input — including v1-style hyphen variants — so the caller's
22
+ * 402 body carries a precise diagnostic.
23
+ */
24
+ function parseNetwork(s) {
25
+ if (typeof s !== 'string' || s.length === 0) {
26
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_NETWORK_FORMAT, 'network must be a non-empty string');
27
+ }
28
+ if (s.includes('-') && !s.includes(':')) {
29
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_NETWORK_FORMAT, `network '${s}' uses v1 hyphen format; v2 requires colon: 'cardano:mainnet|preprod|preview'`);
30
+ }
31
+ if (!isNetwork(s)) {
32
+ throw new errors_1.X402Error(errors_1.Codes.INVALID_NETWORK_FORMAT, `network '${s}' is not one of cardano:mainnet | cardano:preprod | cardano:preview`);
33
+ }
34
+ return s;
35
+ }
36
+ /** True iff `payload.network` (the buyer's claim) matches the server's requirement. */
37
+ function networksMatch(claimed, required) {
38
+ return claimed === required;
39
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Build the canonical Cardano-x402-v2 PaymentRequirements body.
3
+ *
4
+ * Asset-agnostic by design — the consumer passes a v2 asset string
5
+ * (`<policy>.<nameHex>` or `'lovelace'`), a payTo bech32, a network, and
6
+ * a resource descriptor. There is NO USDM default and no decimals
7
+ * assumption — both belong to the consumer's product config.
8
+ *
9
+ * The v2 shape diverges from v1 in five places (see `docs/spec-v2-summary.md`
10
+ * once written):
11
+ * 1. `x402Version: 2` (was 1)
12
+ * 2. `accepts[].amount` (was `maxAmountRequired`)
13
+ * 3. `accepts[].asset` is a single (was split: `asset` + `extra.assetNameHex`)
14
+ * string `<policy>.<nameHex>` or `'lovelace'`
15
+ * 4. `accepts[].resource` is an (was a string)
16
+ * object `{ url, description, mimeType }`
17
+ * 5. `accepts[].assetTransferMethod`(new field)
18
+ */
19
+ import { type Network } from './network';
20
+ import type { PaymentRequirementEntry, PaymentRequirementsBody, ResourceDescriptor, AssetTransferMethod } from './types';
21
+ export interface BuildPaymentRequirementsArgs {
22
+ /** Asset amount in raw units (BigInt-safe). */
23
+ amount: string | number | bigint;
24
+ asset: string;
25
+ payTo: string;
26
+ network: Network | string;
27
+ resource: ResourceDescriptor | string;
28
+ description?: string;
29
+ mimeType?: string;
30
+ outputSchema?: unknown;
31
+ assetTransferMethod?: AssetTransferMethod;
32
+ maxTimeoutSeconds?: number;
33
+ /** Free-form extras (decimals, fingerprint, UI hints). */
34
+ extra?: Record<string, unknown>;
35
+ /**
36
+ * If true, prepend the standard 'PAYMENT-SIGNATURE header is required'
37
+ * error string. Used for the missing-header path; omit for downstream
38
+ * rejection bodies where the caller sets a more specific `error`.
39
+ */
40
+ withMissingHeaderError?: boolean;
41
+ }
42
+ /**
43
+ * Construct a single `accepts[]` entry. Most callers want
44
+ * `buildPaymentRequirements()` which wraps this in the 402 envelope —
45
+ * use `buildEntry()` directly when composing multi-asset accept lists.
46
+ */
47
+ export declare function buildEntry(args: BuildPaymentRequirementsArgs): PaymentRequirementEntry;
48
+ export declare function buildPaymentRequirements(args: BuildPaymentRequirementsArgs): PaymentRequirementsBody;
49
+ /** Pick the first `accepts[]` entry — what the validator inspects. */
50
+ export declare function flatRequirements(body: PaymentRequirementsBody): PaymentRequirementEntry;
51
+ //# sourceMappingURL=requirements.d.ts.map
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Build the canonical Cardano-x402-v2 PaymentRequirements body.
4
+ *
5
+ * Asset-agnostic by design — the consumer passes a v2 asset string
6
+ * (`<policy>.<nameHex>` or `'lovelace'`), a payTo bech32, a network, and
7
+ * a resource descriptor. There is NO USDM default and no decimals
8
+ * assumption — both belong to the consumer's product config.
9
+ *
10
+ * The v2 shape diverges from v1 in five places (see `docs/spec-v2-summary.md`
11
+ * once written):
12
+ * 1. `x402Version: 2` (was 1)
13
+ * 2. `accepts[].amount` (was `maxAmountRequired`)
14
+ * 3. `accepts[].asset` is a single (was split: `asset` + `extra.assetNameHex`)
15
+ * string `<policy>.<nameHex>` or `'lovelace'`
16
+ * 4. `accepts[].resource` is an (was a string)
17
+ * object `{ url, description, mimeType }`
18
+ * 5. `accepts[].assetTransferMethod`(new field)
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.buildEntry = buildEntry;
22
+ exports.buildPaymentRequirements = buildPaymentRequirements;
23
+ exports.flatRequirements = flatRequirements;
24
+ const network_1 = require("./network");
25
+ const asset_1 = require("./asset");
26
+ function normalizeResource(resource, description, mimeType, outputSchema) {
27
+ if (typeof resource === 'string') {
28
+ return {
29
+ url: resource,
30
+ description: description ?? '',
31
+ mimeType: mimeType ?? 'application/json',
32
+ ...(outputSchema !== undefined ? { outputSchema } : {}),
33
+ };
34
+ }
35
+ // Object form: caller's fields win, but allow per-call overrides.
36
+ return {
37
+ url: resource.url,
38
+ description: description ?? resource.description ?? '',
39
+ mimeType: mimeType ?? resource.mimeType ?? 'application/json',
40
+ ...(outputSchema !== undefined
41
+ ? { outputSchema }
42
+ : (resource.outputSchema !== undefined ? { outputSchema: resource.outputSchema } : {})),
43
+ };
44
+ }
45
+ /**
46
+ * Construct a single `accepts[]` entry. Most callers want
47
+ * `buildPaymentRequirements()` which wraps this in the 402 envelope —
48
+ * use `buildEntry()` directly when composing multi-asset accept lists.
49
+ */
50
+ function buildEntry(args) {
51
+ if (!args.payTo)
52
+ throw new Error('buildEntry: payTo is required');
53
+ const network = (0, network_1.parseNetwork)(args.network);
54
+ const parsedAsset = (0, asset_1.parseAsset)(args.asset);
55
+ const amount = String(args.amount);
56
+ if (!/^\d+$/.test(amount) || amount === '0') {
57
+ throw new Error(`buildEntry: amount must be a positive integer (got '${amount}')`);
58
+ }
59
+ const resource = normalizeResource(args.resource, args.description, args.mimeType, args.outputSchema);
60
+ return {
61
+ scheme: 'exact',
62
+ network,
63
+ asset: parsedAsset.raw,
64
+ amount,
65
+ payTo: args.payTo,
66
+ resource,
67
+ assetTransferMethod: args.assetTransferMethod ?? 'default',
68
+ maxTimeoutSeconds: args.maxTimeoutSeconds ?? 600,
69
+ ...(args.extra ? { extra: args.extra } : {}),
70
+ };
71
+ }
72
+ function buildPaymentRequirements(args) {
73
+ const accepts = [buildEntry(args)];
74
+ return {
75
+ x402Version: 2,
76
+ ...(args.withMissingHeaderError ? { error: 'PAYMENT-SIGNATURE header is required' } : {}),
77
+ accepts,
78
+ };
79
+ }
80
+ /** Pick the first `accepts[]` entry — what the validator inspects. */
81
+ function flatRequirements(body) {
82
+ if (!body.accepts || body.accepts.length === 0) {
83
+ throw new Error('flatRequirements: PaymentRequirementsBody.accepts is empty');
84
+ }
85
+ return body.accepts[0];
86
+ }