@runtab/mcp 0.0.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/LICENSE +21 -0
- package/dist/bootstrap.d.ts +18 -0
- package/dist/bootstrap.js +125 -0
- package/dist/cli-config.d.ts +11 -0
- package/dist/cli-config.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/control-plane-origin.d.ts +4 -0
- package/dist/control-plane-origin.js +27 -0
- package/dist/detect.d.ts +10 -0
- package/dist/detect.js +74 -0
- package/dist/durable-mcp-payment.d.ts +40 -0
- package/dist/durable-mcp-payment.js +105 -0
- package/dist/durable-payment-fetch.d.ts +19 -0
- package/dist/durable-payment-fetch.js +117 -0
- package/dist/eip3009-authorization.d.ts +59 -0
- package/dist/eip3009-authorization.js +142 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/fetch-wire.d.ts +46 -0
- package/dist/fetch-wire.js +143 -0
- package/dist/fetch-wrapper.d.ts +24 -0
- package/dist/fetch-wrapper.js +70 -0
- package/dist/fetch.d.ts +3 -0
- package/dist/fetch.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/origin-context.d.ts +5 -0
- package/dist/origin-context.js +15 -0
- package/dist/paid-fetch-server.d.ts +49 -0
- package/dist/paid-fetch-server.js +104 -0
- package/dist/payment-authorization-state.d.ts +48 -0
- package/dist/payment-authorization-state.js +150 -0
- package/dist/payment-client.d.ts +4 -0
- package/dist/payment-client.js +14 -0
- package/dist/payment-correlation.d.ts +9 -0
- package/dist/payment-correlation.js +36 -0
- package/dist/payment-envelope-capacity.d.ts +2 -0
- package/dist/payment-envelope-capacity.js +28 -0
- package/dist/payment-envelope-disk.d.ts +5 -0
- package/dist/payment-envelope-disk.js +97 -0
- package/dist/payment-envelope-journal.d.ts +54 -0
- package/dist/payment-envelope-journal.js +116 -0
- package/dist/payment-envelope-lock.d.ts +5 -0
- package/dist/payment-envelope-lock.js +88 -0
- package/dist/payment-envelope-model.d.ts +32 -0
- package/dist/payment-envelope-model.js +123 -0
- package/dist/payment-envelope-store.d.ts +31 -0
- package/dist/payment-envelope-store.js +210 -0
- package/dist/payment-envelope.d.ts +17 -0
- package/dist/payment-envelope.js +145 -0
- package/dist/payment-idempotency.d.ts +2 -0
- package/dist/payment-idempotency.js +8 -0
- package/dist/payment-observation-reporter.d.ts +31 -0
- package/dist/payment-observation-reporter.js +151 -0
- package/dist/payment-profile.d.ts +3 -0
- package/dist/payment-profile.js +4 -0
- package/dist/payment-request-fingerprint.d.ts +5 -0
- package/dist/payment-request-fingerprint.js +54 -0
- package/dist/payment-settlement-observation.d.ts +9 -0
- package/dist/payment-settlement-observation.js +74 -0
- package/dist/payment-signal.d.ts +2 -0
- package/dist/payment-signal.js +8 -0
- package/dist/payment-target-network.d.ts +16 -0
- package/dist/payment-target-network.js +178 -0
- package/dist/payment-target-policy.d.ts +10 -0
- package/dist/payment-target-policy.js +75 -0
- package/dist/proxy.d.ts +47 -0
- package/dist/proxy.js +72 -0
- package/dist/remote-signer-http.d.ts +16 -0
- package/dist/remote-signer-http.js +182 -0
- package/dist/remote-signer.d.ts +34 -0
- package/dist/remote-signer.js +157 -0
- package/dist/resource-url.d.ts +5 -0
- package/dist/resource-url.js +36 -0
- package/dist/routing.d.ts +13 -0
- package/dist/routing.js +42 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +92 -0
- package/dist/upstream.d.ts +43 -0
- package/dist/upstream.js +38 -0
- package/package.json +65 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { decodePaymentSignatureHeader } from "@x402/core/http";
|
|
2
|
+
import { getAddress, isAddressEqual, recoverTypedDataAddress } from "viem";
|
|
3
|
+
import { ARBITRUM_NETWORK, ARBITRUM_USDC, BASE_NETWORK, BASE_SEPOLIA_NETWORK, BASE_SEPOLIA_USDC, BASE_USDC, } from "./routing.js";
|
|
4
|
+
const AUTHORIZATION_TYPES = [
|
|
5
|
+
{ name: "from", type: "address" },
|
|
6
|
+
{ name: "to", type: "address" },
|
|
7
|
+
{ name: "value", type: "uint256" },
|
|
8
|
+
{ name: "validAfter", type: "uint256" },
|
|
9
|
+
{ name: "validBefore", type: "uint256" },
|
|
10
|
+
{ name: "nonce", type: "bytes32" },
|
|
11
|
+
];
|
|
12
|
+
const NETWORKS = {
|
|
13
|
+
[ARBITRUM_NETWORK]: { asset: ARBITRUM_USDC, chainId: 42_161, name: "USD Coin" },
|
|
14
|
+
[BASE_NETWORK]: { asset: BASE_USDC, chainId: 8_453, name: "USD Coin" },
|
|
15
|
+
[BASE_SEPOLIA_NETWORK]: { asset: BASE_SEPOLIA_USDC, chainId: 84_532, name: "USDC" },
|
|
16
|
+
};
|
|
17
|
+
export class PaymentEnvelopeValidationError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("The persisted payment envelope is invalid.");
|
|
20
|
+
this.name = "PaymentEnvelopeValidationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function record(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function decimal(value) {
|
|
27
|
+
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) {
|
|
28
|
+
throw new PaymentEnvelopeValidationError();
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function address(value) {
|
|
33
|
+
if (typeof value !== "string")
|
|
34
|
+
throw new PaymentEnvelopeValidationError();
|
|
35
|
+
try {
|
|
36
|
+
return getAddress(value);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new PaymentEnvelopeValidationError();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function newPaymentEnvelope(payload, paymentSignature, signer) {
|
|
43
|
+
const authorization = record(payload.payload.authorization)
|
|
44
|
+
? payload.payload.authorization
|
|
45
|
+
: undefined;
|
|
46
|
+
const signature = payload.payload.signature;
|
|
47
|
+
const validBefore = authorization?.validBefore;
|
|
48
|
+
if (typeof signature !== "string" ||
|
|
49
|
+
typeof validBefore !== "string" ||
|
|
50
|
+
!/^(0|[1-9][0-9]*)$/.test(validBefore)) {
|
|
51
|
+
throw new PaymentEnvelopeValidationError();
|
|
52
|
+
}
|
|
53
|
+
const validBeforeNumber = Number(validBefore);
|
|
54
|
+
const receiptId = signer.receiptIdForSignature(signature);
|
|
55
|
+
if (!paymentSignature ||
|
|
56
|
+
!Number.isSafeInteger(validBeforeNumber) ||
|
|
57
|
+
validBeforeNumber < 1 ||
|
|
58
|
+
!receiptId) {
|
|
59
|
+
throw new PaymentEnvelopeValidationError();
|
|
60
|
+
}
|
|
61
|
+
return { paymentSignature, receiptId, validBefore: validBeforeNumber };
|
|
62
|
+
}
|
|
63
|
+
function networkFor(payload, profile) {
|
|
64
|
+
const network = payload.accepted.network;
|
|
65
|
+
const supported = NETWORKS[network];
|
|
66
|
+
const profileMatch = profile === "base_sepolia_integration"
|
|
67
|
+
? network === BASE_SEPOLIA_NETWORK
|
|
68
|
+
: network === BASE_NETWORK || network === ARBITRUM_NETWORK;
|
|
69
|
+
if (!supported || !profileMatch)
|
|
70
|
+
throw new PaymentEnvelopeValidationError();
|
|
71
|
+
return { ...supported, network: network };
|
|
72
|
+
}
|
|
73
|
+
export async function parsePaymentEnvelope(paymentSignature, expectedAddress, profile) {
|
|
74
|
+
try {
|
|
75
|
+
const payload = decodePaymentSignatureHeader(paymentSignature);
|
|
76
|
+
if (payload.x402Version !== 2 || payload.accepted.scheme !== "exact") {
|
|
77
|
+
throw new PaymentEnvelopeValidationError();
|
|
78
|
+
}
|
|
79
|
+
const selected = networkFor(payload, profile);
|
|
80
|
+
const authorization = record(payload.payload.authorization)
|
|
81
|
+
? payload.payload.authorization
|
|
82
|
+
: undefined;
|
|
83
|
+
const signature = payload.payload.signature;
|
|
84
|
+
if (!authorization ||
|
|
85
|
+
typeof signature !== "string" ||
|
|
86
|
+
!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
87
|
+
throw new PaymentEnvelopeValidationError();
|
|
88
|
+
}
|
|
89
|
+
const from = address(authorization.from);
|
|
90
|
+
const to = address(authorization.to);
|
|
91
|
+
const value = decimal(authorization.value);
|
|
92
|
+
const validAfter = decimal(authorization.validAfter);
|
|
93
|
+
const validBeforeRaw = decimal(authorization.validBefore);
|
|
94
|
+
const nonce = authorization.nonce;
|
|
95
|
+
if (!isAddressEqual(from, expectedAddress) ||
|
|
96
|
+
!isAddressEqual(to, address(payload.accepted.payTo)) ||
|
|
97
|
+
!isAddressEqual(address(payload.accepted.asset), getAddress(selected.asset)) ||
|
|
98
|
+
value !== payload.accepted.amount ||
|
|
99
|
+
validAfter !== "0" ||
|
|
100
|
+
typeof nonce !== "string" ||
|
|
101
|
+
!/^0x[0-9a-fA-F]{64}$/.test(nonce) ||
|
|
102
|
+
payload.accepted.extra.name !== selected.name ||
|
|
103
|
+
payload.accepted.extra.version !== "2") {
|
|
104
|
+
throw new PaymentEnvelopeValidationError();
|
|
105
|
+
}
|
|
106
|
+
const recovered = await recoverTypedDataAddress({
|
|
107
|
+
domain: {
|
|
108
|
+
chainId: selected.chainId,
|
|
109
|
+
name: selected.name,
|
|
110
|
+
verifyingContract: getAddress(selected.asset),
|
|
111
|
+
version: "2",
|
|
112
|
+
},
|
|
113
|
+
message: {
|
|
114
|
+
from,
|
|
115
|
+
nonce: nonce,
|
|
116
|
+
to,
|
|
117
|
+
validAfter: BigInt(validAfter),
|
|
118
|
+
validBefore: BigInt(validBeforeRaw),
|
|
119
|
+
value: BigInt(value),
|
|
120
|
+
},
|
|
121
|
+
primaryType: "TransferWithAuthorization",
|
|
122
|
+
signature: signature,
|
|
123
|
+
types: { TransferWithAuthorization: AUTHORIZATION_TYPES },
|
|
124
|
+
});
|
|
125
|
+
if (!isAddressEqual(recovered, expectedAddress))
|
|
126
|
+
throw new PaymentEnvelopeValidationError();
|
|
127
|
+
const validBefore = Number(validBeforeRaw);
|
|
128
|
+
if (!Number.isSafeInteger(validBefore))
|
|
129
|
+
throw new PaymentEnvelopeValidationError();
|
|
130
|
+
return {
|
|
131
|
+
asset: getAddress(selected.asset),
|
|
132
|
+
from,
|
|
133
|
+
network: selected.network,
|
|
134
|
+
nonce: nonce,
|
|
135
|
+
payload,
|
|
136
|
+
signature: signature,
|
|
137
|
+
validBefore,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof PaymentEnvelopeValidationError)
|
|
142
|
+
throw error;
|
|
143
|
+
throw new PaymentEnvelopeValidationError();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const storage = new AsyncLocalStorage();
|
|
3
|
+
export function currentPaymentIdempotencyKey() {
|
|
4
|
+
return storage.getStore();
|
|
5
|
+
}
|
|
6
|
+
export function withPaymentIdempotencyKey(key, task) {
|
|
7
|
+
return storage.run(key, task);
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PaymentResponseContext } from "@x402/core/client";
|
|
2
|
+
import type { PaymentCorrelations } from "./payment-correlation.js";
|
|
3
|
+
interface PaymentObservationReporterOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
attempts: number;
|
|
6
|
+
correlations: PaymentCorrelations;
|
|
7
|
+
endpoint: URL;
|
|
8
|
+
fetch: typeof globalThis.fetch;
|
|
9
|
+
retryDelayMs: number;
|
|
10
|
+
timeoutMs: number;
|
|
11
|
+
}
|
|
12
|
+
export type PaymentObservationResult = {
|
|
13
|
+
status: "failed";
|
|
14
|
+
verified: true;
|
|
15
|
+
} | {
|
|
16
|
+
status: "ignored";
|
|
17
|
+
verified: false;
|
|
18
|
+
} | {
|
|
19
|
+
status: "pending";
|
|
20
|
+
verified: false;
|
|
21
|
+
} | {
|
|
22
|
+
status: "settled";
|
|
23
|
+
verified: true;
|
|
24
|
+
};
|
|
25
|
+
export declare class PaymentObservationReporter {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(options: PaymentObservationReporterOptions);
|
|
28
|
+
report(context: PaymentResponseContext, address: `0x${string}`): Promise<PaymentObservationResult>;
|
|
29
|
+
flush(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { parsePaymentSettlementObservation } from "./payment-settlement-observation.js";
|
|
2
|
+
import { readRemoteSignerJson } from "./remote-signer-http.js";
|
|
3
|
+
function record(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function acknowledgement(value, status) {
|
|
7
|
+
if (!record(value) || Object.keys(value).sort().join(",") !== "receiptId,status,verified") {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (typeof value.receiptId !== "string")
|
|
11
|
+
return;
|
|
12
|
+
if (status === 202 && value.status === "pending" && value.verified === false) {
|
|
13
|
+
return { receiptId: value.receiptId, status: "pending", verified: false };
|
|
14
|
+
}
|
|
15
|
+
if (status === 200 &&
|
|
16
|
+
(value.status === "settled" || value.status === "failed") &&
|
|
17
|
+
value.verified === true) {
|
|
18
|
+
return { receiptId: value.receiptId, status: value.status, verified: true };
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export class PaymentObservationReporter {
|
|
23
|
+
#apiKey;
|
|
24
|
+
#attempts;
|
|
25
|
+
#correlations;
|
|
26
|
+
#endpoint;
|
|
27
|
+
#fetch;
|
|
28
|
+
#inFlight = new Set();
|
|
29
|
+
#retryDelayMs;
|
|
30
|
+
#timeoutMs;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.#apiKey = options.apiKey;
|
|
33
|
+
this.#attempts = options.attempts;
|
|
34
|
+
this.#correlations = options.correlations;
|
|
35
|
+
this.#endpoint = options.endpoint;
|
|
36
|
+
this.#fetch = options.fetch;
|
|
37
|
+
this.#retryDelayMs = options.retryDelayMs;
|
|
38
|
+
this.#timeoutMs = options.timeoutMs;
|
|
39
|
+
}
|
|
40
|
+
async #attempt(body) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
let timeout;
|
|
43
|
+
const deadline = new Promise((resolve) => {
|
|
44
|
+
timeout = setTimeout(() => {
|
|
45
|
+
controller.abort();
|
|
46
|
+
resolve({ retry: true });
|
|
47
|
+
}, this.#timeoutMs);
|
|
48
|
+
});
|
|
49
|
+
const request = async () => {
|
|
50
|
+
try {
|
|
51
|
+
const response = await this.#fetch(this.#endpoint, {
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
headers: {
|
|
54
|
+
accept: "application/json",
|
|
55
|
+
authorization: `Bearer ${this.#apiKey}`,
|
|
56
|
+
"content-type": "application/json",
|
|
57
|
+
},
|
|
58
|
+
method: "POST",
|
|
59
|
+
redirect: "error",
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
return { retry: response.status === 429 || response.status >= 500 };
|
|
64
|
+
}
|
|
65
|
+
if (response.status === 204)
|
|
66
|
+
return { retry: false };
|
|
67
|
+
const parsed = acknowledgement(await readRemoteSignerJson(response, controller.signal), response.status);
|
|
68
|
+
return {
|
|
69
|
+
...(parsed ? { acknowledgement: parsed } : {}),
|
|
70
|
+
retry: parsed?.status === "pending",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { retry: true };
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
try {
|
|
78
|
+
return await Promise.race([request(), deadline]);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (timeout)
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async #send(body, receiptId, signature) {
|
|
86
|
+
let result = { status: "ignored", verified: false };
|
|
87
|
+
for (let attempt = 0; attempt < this.#attempts; attempt += 1) {
|
|
88
|
+
const attempted = await this.#attempt(body);
|
|
89
|
+
if (attempted.acknowledgement?.receiptId === receiptId) {
|
|
90
|
+
const verified = attempted.acknowledgement;
|
|
91
|
+
result =
|
|
92
|
+
verified.status === "pending"
|
|
93
|
+
? { status: "pending", verified: false }
|
|
94
|
+
: verified.status === "settled"
|
|
95
|
+
? { status: "settled", verified: true }
|
|
96
|
+
: { status: "failed", verified: true };
|
|
97
|
+
if (verified.status === "settled") {
|
|
98
|
+
this.#correlations.deleteIf(signature, receiptId);
|
|
99
|
+
}
|
|
100
|
+
if (verified.status !== "pending")
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
if (!attempted.retry || attempt === this.#attempts - 1)
|
|
104
|
+
return result;
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, this.#retryDelayMs * (attempt + 1)));
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
async report(context, address) {
|
|
110
|
+
const settlement = parsePaymentSettlementObservation(context.settleResponse, {
|
|
111
|
+
...(context.paymentPayload.x402Version === 2
|
|
112
|
+
? { amount: context.paymentPayload.accepted.amount }
|
|
113
|
+
: {}),
|
|
114
|
+
network: context.requirements.network,
|
|
115
|
+
payer: address,
|
|
116
|
+
});
|
|
117
|
+
const signature = context.paymentPayload.payload.signature;
|
|
118
|
+
if (!settlement || typeof signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
119
|
+
return { status: "ignored", verified: false };
|
|
120
|
+
}
|
|
121
|
+
const receiptId = this.#correlations.get(signature);
|
|
122
|
+
if (!receiptId)
|
|
123
|
+
return { status: "ignored", verified: false };
|
|
124
|
+
const task = this.#send({
|
|
125
|
+
outcome: "observed",
|
|
126
|
+
paymentResponse: {
|
|
127
|
+
...(settlement.success
|
|
128
|
+
? {}
|
|
129
|
+
: {
|
|
130
|
+
...(settlement.errorMessage ? { errorMessage: settlement.errorMessage } : {}),
|
|
131
|
+
errorReason: settlement.errorReason,
|
|
132
|
+
}),
|
|
133
|
+
network: settlement.network,
|
|
134
|
+
payer: settlement.payer,
|
|
135
|
+
success: settlement.success,
|
|
136
|
+
transaction: settlement.transaction,
|
|
137
|
+
},
|
|
138
|
+
receiptId,
|
|
139
|
+
}, receiptId, signature);
|
|
140
|
+
this.#inFlight.add(task);
|
|
141
|
+
try {
|
|
142
|
+
return await task;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
this.#inFlight.delete(task);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async flush() {
|
|
149
|
+
await Promise.allSettled([...this.#inFlight]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const MAX_PAYMENT_REQUEST_BODY_BYTES = 1_048_576;
|
|
3
|
+
const IGNORED_HEADERS = new Set([
|
|
4
|
+
"access-control-expose-headers",
|
|
5
|
+
"payment-signature",
|
|
6
|
+
"x-payment",
|
|
7
|
+
]);
|
|
8
|
+
export class PaymentRequestFingerprintError extends Error {
|
|
9
|
+
code = "INVALID_FETCH_REQUEST";
|
|
10
|
+
constructor() {
|
|
11
|
+
super("The fetch request is invalid.");
|
|
12
|
+
this.name = "PaymentRequestFingerprintError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function part(hash, value) {
|
|
16
|
+
hash.update(String(Buffer.byteLength(value)));
|
|
17
|
+
hash.update(":");
|
|
18
|
+
hash.update(value);
|
|
19
|
+
hash.update(";");
|
|
20
|
+
}
|
|
21
|
+
export async function fingerprintPaymentRequest(request) {
|
|
22
|
+
const hash = createHash("sha256");
|
|
23
|
+
part(hash, request.method.toUpperCase());
|
|
24
|
+
part(hash, request.url);
|
|
25
|
+
const headers = [...request.headers]
|
|
26
|
+
.filter(([name]) => !IGNORED_HEADERS.has(name.toLowerCase()))
|
|
27
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
28
|
+
for (const [name, value] of headers) {
|
|
29
|
+
part(hash, name.toLowerCase());
|
|
30
|
+
part(hash, value);
|
|
31
|
+
}
|
|
32
|
+
const body = request.body;
|
|
33
|
+
if (body) {
|
|
34
|
+
const reader = body.getReader();
|
|
35
|
+
let length = 0;
|
|
36
|
+
try {
|
|
37
|
+
while (true) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
length += value.byteLength;
|
|
42
|
+
if (length > MAX_PAYMENT_REQUEST_BODY_BYTES) {
|
|
43
|
+
void reader.cancel().catch(() => undefined);
|
|
44
|
+
throw new PaymentRequestFingerprintError();
|
|
45
|
+
}
|
|
46
|
+
hash.update(value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
reader.releaseLock();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return hash.digest("hex");
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SettleResponse } from "@x402/core/types";
|
|
2
|
+
export type PaymentSettlementObservation = SettleResponse & {
|
|
3
|
+
payer: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function parsePaymentSettlementObservation(value: unknown, expected?: {
|
|
6
|
+
amount?: string;
|
|
7
|
+
network?: string;
|
|
8
|
+
payer?: string;
|
|
9
|
+
}): PaymentSettlementObservation | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getAddress, isAddress } from "viem";
|
|
2
|
+
function record(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function exactAllowedKeys(value, required, allowed) {
|
|
6
|
+
return (required.every((key) => Object.hasOwn(value, key)) &&
|
|
7
|
+
Object.keys(value).every((key) => allowed.includes(key)));
|
|
8
|
+
}
|
|
9
|
+
function boundedJsonRecord(value) {
|
|
10
|
+
if (!record(value))
|
|
11
|
+
return false;
|
|
12
|
+
try {
|
|
13
|
+
const serialized = JSON.stringify(value);
|
|
14
|
+
return serialized !== undefined && new TextEncoder().encode(serialized).byteLength <= 4_096;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function parsePaymentSettlementObservation(value, expected = {}) {
|
|
21
|
+
if (!record(value) || typeof value.success !== "boolean")
|
|
22
|
+
return null;
|
|
23
|
+
const required = value.success
|
|
24
|
+
? ["network", "payer", "success", "transaction"]
|
|
25
|
+
: ["errorReason", "network", "payer", "success", "transaction"];
|
|
26
|
+
const allowed = value.success
|
|
27
|
+
? [...required, "amount", "extensions", "extra"]
|
|
28
|
+
: [...required, "amount", "errorMessage", "extensions", "extra"];
|
|
29
|
+
if (!exactAllowedKeys(value, required, allowed))
|
|
30
|
+
return null;
|
|
31
|
+
if (typeof value.network !== "string" ||
|
|
32
|
+
value.network.length < 3 ||
|
|
33
|
+
value.network.length > 128 ||
|
|
34
|
+
!value.network.includes(":") ||
|
|
35
|
+
(expected.network !== undefined && value.network !== expected.network) ||
|
|
36
|
+
typeof value.payer !== "string" ||
|
|
37
|
+
!isAddress(value.payer) ||
|
|
38
|
+
(expected.payer !== undefined &&
|
|
39
|
+
(!isAddress(expected.payer) || getAddress(value.payer) !== getAddress(expected.payer))) ||
|
|
40
|
+
typeof value.transaction !== "string" ||
|
|
41
|
+
!/^0x[0-9a-fA-F]{64}$/.test(value.transaction) ||
|
|
42
|
+
(value.amount !== undefined &&
|
|
43
|
+
(typeof value.amount !== "string" ||
|
|
44
|
+
!/^(0|[1-9][0-9]*)$/.test(value.amount) ||
|
|
45
|
+
value.amount.length > 78 ||
|
|
46
|
+
expected.amount === undefined ||
|
|
47
|
+
value.amount !== expected.amount)) ||
|
|
48
|
+
(value.extensions !== undefined && !boundedJsonRecord(value.extensions)) ||
|
|
49
|
+
(value.extra !== undefined && !boundedJsonRecord(value.extra))) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const common = {
|
|
53
|
+
network: value.network,
|
|
54
|
+
payer: getAddress(value.payer),
|
|
55
|
+
transaction: value.transaction,
|
|
56
|
+
};
|
|
57
|
+
if (value.success)
|
|
58
|
+
return { ...common, success: true };
|
|
59
|
+
if (typeof value.errorReason !== "string" ||
|
|
60
|
+
!/\S/.test(value.errorReason) ||
|
|
61
|
+
value.errorReason.length > 256 ||
|
|
62
|
+
(value.errorMessage !== undefined &&
|
|
63
|
+
(typeof value.errorMessage !== "string" ||
|
|
64
|
+
!/\S/.test(value.errorMessage) ||
|
|
65
|
+
value.errorMessage.length > 2_048))) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...common,
|
|
70
|
+
...(value.errorMessage === undefined ? {} : { errorMessage: value.errorMessage }),
|
|
71
|
+
errorReason: value.errorReason,
|
|
72
|
+
success: false,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface PaymentTargetAddress {
|
|
2
|
+
address: string;
|
|
3
|
+
family: 4 | 6;
|
|
4
|
+
}
|
|
5
|
+
export type PaymentTargetLookup = (hostname: string) => Promise<readonly PaymentTargetAddress[]>;
|
|
6
|
+
interface PinnedPaymentFetchOptions {
|
|
7
|
+
allowDevelopmentLoopback?: boolean;
|
|
8
|
+
fetch: typeof globalThis.fetch;
|
|
9
|
+
lookup?: PaymentTargetLookup;
|
|
10
|
+
maxPinnedHosts?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function createPinnedPaymentFetch(options: PinnedPaymentFetchOptions): {
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
fetch: typeof globalThis.fetch;
|
|
15
|
+
};
|
|
16
|
+
export {};
|