mrmainspring 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 +47 -0
- package/dist/audit/service.d.ts +7 -0
- package/dist/audit/service.js +98 -0
- package/dist/audit/store.d.ts +7 -0
- package/dist/audit/store.js +37 -0
- package/dist/audit/supabase-store.d.ts +9 -0
- package/dist/audit/supabase-store.js +22 -0
- package/dist/audit/types.d.ts +31 -0
- package/dist/audit/types.js +1 -0
- package/dist/casper/anchorClient.d.ts +99 -0
- package/dist/casper/anchorClient.js +412 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +215 -0
- package/dist/env-file.d.ts +1 -0
- package/dist/env-file.js +51 -0
- package/dist/grimoire/service.d.ts +13 -0
- package/dist/grimoire/service.js +199 -0
- package/dist/grimoire/store.d.ts +10 -0
- package/dist/grimoire/store.js +64 -0
- package/dist/grimoire/supabase-store.d.ts +13 -0
- package/dist/grimoire/supabase-store.js +50 -0
- package/dist/grimoire/types.d.ts +60 -0
- package/dist/grimoire/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/mcp/auditTools.d.ts +3 -0
- package/dist/mcp/auditTools.js +13 -0
- package/dist/mcp/grimoireTools.d.ts +3 -0
- package/dist/mcp/grimoireTools.js +91 -0
- package/dist/mcp/jsonResult.d.ts +2 -0
- package/dist/mcp/jsonResult.js +10 -0
- package/dist/mcp/memoryTools.d.ts +3 -0
- package/dist/mcp/memoryTools.js +73 -0
- package/dist/mcp/paymentTools.d.ts +3 -0
- package/dist/mcp/paymentTools.js +33 -0
- package/dist/memory/canonical.d.ts +4 -0
- package/dist/memory/canonical.js +49 -0
- package/dist/memory/hash.d.ts +1 -0
- package/dist/memory/hash.js +4 -0
- package/dist/memory/service.d.ts +37 -0
- package/dist/memory/service.js +175 -0
- package/dist/memory/store.d.ts +8 -0
- package/dist/memory/store.js +49 -0
- package/dist/memory/supabase-store.d.ts +10 -0
- package/dist/memory/supabase-store.js +30 -0
- package/dist/memory/types.d.ts +56 -0
- package/dist/memory/types.js +7 -0
- package/dist/payments/service.d.ts +26 -0
- package/dist/payments/service.js +613 -0
- package/dist/payments/store.d.ts +10 -0
- package/dist/payments/store.js +64 -0
- package/dist/payments/supabase-store.d.ts +13 -0
- package/dist/payments/supabase-store.js +51 -0
- package/dist/payments/types.d.ts +101 -0
- package/dist/payments/types.js +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +68 -0
- package/dist/storage/json-file-store.d.ts +17 -0
- package/dist/storage/json-file-store.js +87 -0
- package/dist/storage/store-factory.d.ts +12 -0
- package/dist/storage/store-factory.js +26 -0
- package/dist/storage/supabase-rest.d.ts +26 -0
- package/dist/storage/supabase-rest.js +85 -0
- package/dist/x402/client.d.ts +44 -0
- package/dist/x402/client.js +95 -0
- package/dist/x402/facilitator.d.ts +84 -0
- package/dist/x402/facilitator.js +800 -0
- package/dist/x402/readiness.d.ts +55 -0
- package/dist/x402/readiness.js +433 -0
- package/dist/x402/redaction.d.ts +1 -0
- package/dist/x402/redaction.js +30 -0
- package/dist/x402/resource.d.ts +69 -0
- package/dist/x402/resource.js +325 -0
- package/dist/x402/settlement.d.ts +176 -0
- package/dist/x402/settlement.js +1210 -0
- package/dist/x402/signer.d.ts +71 -0
- package/dist/x402/signer.js +616 -0
- package/package.json +61 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JsonObject } from "../memory/types.js";
|
|
2
|
+
import type { PolicyRecord } from "../grimoire/types.js";
|
|
3
|
+
export type X402RequirementApprovalInput = {
|
|
4
|
+
requirements: unknown;
|
|
5
|
+
policy: PolicyRecord;
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
expectedAmount: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type X402RequirementCandidateRejection = {
|
|
11
|
+
index: number;
|
|
12
|
+
reason: X402RequirementCandidateRejectionReason;
|
|
13
|
+
};
|
|
14
|
+
export type X402RequirementCandidateRejectionReason = "requirement_not_object" | "amount_missing" | "amount_invalid" | "expected_amount_mismatch" | "amount_over_limit" | "resource_missing" | "resource_mismatch" | "method_missing" | "method_mismatch" | "network_missing" | "network_mismatch" | "asset_missing" | "asset_mismatch" | "payee_missing" | "payee_mismatch" | "scheme_missing" | "scheme_mismatch" | "timeout_missing" | "timeout_invalid";
|
|
15
|
+
export type X402RequirementRejectionReason = "requirements_not_object" | "requirements_accepts_missing" | "requirements_accepts_empty" | "invalid_policy_amount" | "invalid_expected_amount" | "no_acceptable_requirement";
|
|
16
|
+
export type X402RequirementApproval = {
|
|
17
|
+
approved: true;
|
|
18
|
+
selected_index: number;
|
|
19
|
+
selected_requirement_hash: string;
|
|
20
|
+
selected_requirement: JsonObject;
|
|
21
|
+
} | {
|
|
22
|
+
approved: false;
|
|
23
|
+
reason: X402RequirementRejectionReason;
|
|
24
|
+
rejected_candidates: X402RequirementCandidateRejection[];
|
|
25
|
+
};
|
|
26
|
+
export type X402SettlementResponseVerification = {
|
|
27
|
+
settled: true;
|
|
28
|
+
settlement_status: "settled";
|
|
29
|
+
transaction_hash: string;
|
|
30
|
+
receipt_json: string;
|
|
31
|
+
} | {
|
|
32
|
+
settled: false;
|
|
33
|
+
settlement_status: "not_settled";
|
|
34
|
+
reason: "settlement_response_not_object" | "settlement_response_not_successful" | "settlement_transaction_hash_missing" | "settlement_network_missing" | "settlement_network_mismatch" | "settlement_asset_missing" | "settlement_asset_mismatch" | "settlement_amount_missing" | "settlement_amount_invalid" | "settlement_amount_mismatch" | "settlement_payer_missing" | "settlement_payer_mismatch" | "settlement_payee_missing" | "settlement_payee_mismatch";
|
|
35
|
+
receipt_json: string | null;
|
|
36
|
+
};
|
|
37
|
+
export type X402PaymentPayloadRejectionReason = "payment_payload_not_object" | "payer_missing" | "authorization_missing" | "nonce_missing" | "valid_after_missing" | "valid_until_missing" | "validity_window_invalid" | "payment_not_yet_valid" | "payment_expired" | "validity_window_too_long" | "selected_requirement_hash_missing" | "selected_requirement_hash_mismatch";
|
|
38
|
+
export type X402PaymentPayloadApproval = {
|
|
39
|
+
approved: true;
|
|
40
|
+
} | {
|
|
41
|
+
approved: false;
|
|
42
|
+
reason: X402PaymentPayloadRejectionReason;
|
|
43
|
+
};
|
|
44
|
+
export type X402SettlementResponseExpectation = {
|
|
45
|
+
selectedRequirement?: JsonObject;
|
|
46
|
+
signedPayload?: JsonObject;
|
|
47
|
+
};
|
|
48
|
+
export type X402PaymentPayloadValidationOptions = {
|
|
49
|
+
now?: Date;
|
|
50
|
+
clockSkewSeconds?: number;
|
|
51
|
+
maxValiditySeconds?: number | null;
|
|
52
|
+
};
|
|
53
|
+
export declare function approveX402Requirements(input: X402RequirementApprovalInput): X402RequirementApproval;
|
|
54
|
+
export declare function validateX402PaymentPayload(paymentPayload: unknown, selectedRequirementHash: string, options?: X402PaymentPayloadValidationOptions): X402PaymentPayloadApproval;
|
|
55
|
+
export declare function verifyX402SettlementResponse(settlementResponse: unknown, expectation?: X402SettlementResponseExpectation): X402SettlementResponseVerification;
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { canonicalizeJson, toJsonObject } from "../memory/canonical.js";
|
|
2
|
+
import { sha256Hex } from "../memory/hash.js";
|
|
3
|
+
import { redactX402Value } from "./redaction.js";
|
|
4
|
+
export function approveX402Requirements(input) {
|
|
5
|
+
const requirements = asRecord(input.requirements);
|
|
6
|
+
if (!requirements) {
|
|
7
|
+
return rejected("requirements_not_object");
|
|
8
|
+
}
|
|
9
|
+
const accepts = requirements.accepts;
|
|
10
|
+
if (!Array.isArray(accepts)) {
|
|
11
|
+
return rejected("requirements_accepts_missing");
|
|
12
|
+
}
|
|
13
|
+
if (accepts.length === 0) {
|
|
14
|
+
return rejected("requirements_accepts_empty");
|
|
15
|
+
}
|
|
16
|
+
const policyMax = parseDecimalAmount(input.policy.max_amount_per_call);
|
|
17
|
+
if (!policyMax) {
|
|
18
|
+
return rejected("invalid_policy_amount");
|
|
19
|
+
}
|
|
20
|
+
const expectedAmount = input.expectedAmount === null ? null : parseDecimalAmount(input.expectedAmount);
|
|
21
|
+
if (input.expectedAmount !== null && !expectedAmount) {
|
|
22
|
+
return rejected("invalid_expected_amount");
|
|
23
|
+
}
|
|
24
|
+
const rejectedCandidates = [];
|
|
25
|
+
for (const [index, item] of accepts.entries()) {
|
|
26
|
+
const candidate = asRecord(item);
|
|
27
|
+
if (!candidate) {
|
|
28
|
+
rejectedCandidates.push({ index, reason: "requirement_not_object" });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const reason = validateRequirementCandidate({
|
|
32
|
+
candidate,
|
|
33
|
+
index,
|
|
34
|
+
policy: input.policy,
|
|
35
|
+
policyMax,
|
|
36
|
+
expectedAmount,
|
|
37
|
+
method: input.method.toUpperCase(),
|
|
38
|
+
url: input.url
|
|
39
|
+
});
|
|
40
|
+
if (reason) {
|
|
41
|
+
rejectedCandidates.push({ index, reason });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const selectedRequirement = toJsonObject(candidate, "payment_requirement");
|
|
45
|
+
return {
|
|
46
|
+
approved: true,
|
|
47
|
+
selected_index: index,
|
|
48
|
+
selected_requirement: selectedRequirement,
|
|
49
|
+
selected_requirement_hash: sha256Hex(canonicalizeJson(selectedRequirement))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
approved: false,
|
|
54
|
+
reason: "no_acceptable_requirement",
|
|
55
|
+
rejected_candidates: rejectedCandidates
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function validateX402PaymentPayload(paymentPayload, selectedRequirementHash, options = {}) {
|
|
59
|
+
const payload = asRecord(paymentPayload);
|
|
60
|
+
if (!payload) {
|
|
61
|
+
return { approved: false, reason: "payment_payload_not_object" };
|
|
62
|
+
}
|
|
63
|
+
if (!firstString(payload, ["payer", "payerAccount", "payer_account"])) {
|
|
64
|
+
return { approved: false, reason: "payer_missing" };
|
|
65
|
+
}
|
|
66
|
+
if (!asRecord(payload.authorization)) {
|
|
67
|
+
return { approved: false, reason: "authorization_missing" };
|
|
68
|
+
}
|
|
69
|
+
if (!firstString(payload, ["nonce"])) {
|
|
70
|
+
return { approved: false, reason: "nonce_missing" };
|
|
71
|
+
}
|
|
72
|
+
const validAfter = firstString(payload, ["validAfter", "valid_after", "validFrom", "valid_from"]);
|
|
73
|
+
if (!validAfter) {
|
|
74
|
+
return { approved: false, reason: "valid_after_missing" };
|
|
75
|
+
}
|
|
76
|
+
const validUntil = firstString(payload, [
|
|
77
|
+
"validUntil",
|
|
78
|
+
"valid_until",
|
|
79
|
+
"validBefore",
|
|
80
|
+
"valid_before",
|
|
81
|
+
"expiresAt",
|
|
82
|
+
"expires_at"
|
|
83
|
+
]);
|
|
84
|
+
if (!validUntil) {
|
|
85
|
+
return { approved: false, reason: "valid_until_missing" };
|
|
86
|
+
}
|
|
87
|
+
const validAfterMs = Date.parse(validAfter);
|
|
88
|
+
const validUntilMs = Date.parse(validUntil);
|
|
89
|
+
if (!Number.isFinite(validAfterMs) ||
|
|
90
|
+
!Number.isFinite(validUntilMs) ||
|
|
91
|
+
validUntilMs <= validAfterMs) {
|
|
92
|
+
return { approved: false, reason: "validity_window_invalid" };
|
|
93
|
+
}
|
|
94
|
+
if (options.now) {
|
|
95
|
+
const skewMs = Math.max(0, options.clockSkewSeconds ?? 0) * 1000;
|
|
96
|
+
const nowMs = options.now.getTime();
|
|
97
|
+
if (validAfterMs > nowMs + skewMs) {
|
|
98
|
+
return { approved: false, reason: "payment_not_yet_valid" };
|
|
99
|
+
}
|
|
100
|
+
if (validUntilMs <= nowMs - skewMs) {
|
|
101
|
+
return { approved: false, reason: "payment_expired" };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (options.maxValiditySeconds !== null &&
|
|
105
|
+
options.maxValiditySeconds !== undefined &&
|
|
106
|
+
validUntilMs - validAfterMs > options.maxValiditySeconds * 1000) {
|
|
107
|
+
return { approved: false, reason: "validity_window_too_long" };
|
|
108
|
+
}
|
|
109
|
+
const payloadHash = firstString(payload, [
|
|
110
|
+
"selectedRequirementHash",
|
|
111
|
+
"selected_requirement_hash"
|
|
112
|
+
]);
|
|
113
|
+
if (!payloadHash) {
|
|
114
|
+
return { approved: false, reason: "selected_requirement_hash_missing" };
|
|
115
|
+
}
|
|
116
|
+
if (payloadHash !== selectedRequirementHash) {
|
|
117
|
+
return { approved: false, reason: "selected_requirement_hash_mismatch" };
|
|
118
|
+
}
|
|
119
|
+
return { approved: true };
|
|
120
|
+
}
|
|
121
|
+
export function verifyX402SettlementResponse(settlementResponse, expectation = {}) {
|
|
122
|
+
const response = asRecord(settlementResponse);
|
|
123
|
+
if (!response) {
|
|
124
|
+
return {
|
|
125
|
+
settled: false,
|
|
126
|
+
settlement_status: "not_settled",
|
|
127
|
+
reason: "settlement_response_not_object",
|
|
128
|
+
receipt_json: null
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const receipt = toJsonObject(redactX402Value(response), "settlement_response");
|
|
132
|
+
const receiptJson = JSON.stringify(receipt);
|
|
133
|
+
const successful = response.success === true || response.settled === true;
|
|
134
|
+
if (!successful) {
|
|
135
|
+
return {
|
|
136
|
+
settled: false,
|
|
137
|
+
settlement_status: "not_settled",
|
|
138
|
+
reason: "settlement_response_not_successful",
|
|
139
|
+
receipt_json: receiptJson
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const transactionHash = firstString(response, [
|
|
143
|
+
"transactionHash",
|
|
144
|
+
"transaction_hash",
|
|
145
|
+
"txHash",
|
|
146
|
+
"tx_hash",
|
|
147
|
+
"transaction"
|
|
148
|
+
]);
|
|
149
|
+
if (!transactionHash) {
|
|
150
|
+
return {
|
|
151
|
+
settled: false,
|
|
152
|
+
settlement_status: "not_settled",
|
|
153
|
+
reason: "settlement_transaction_hash_missing",
|
|
154
|
+
receipt_json: receiptJson
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const expectedNetwork = expectation.selectedRequirement
|
|
158
|
+
? firstString(expectation.selectedRequirement, ["network", "networkId", "network_id"])
|
|
159
|
+
: null;
|
|
160
|
+
const network = firstString(response, ["network", "networkId", "network_id"]);
|
|
161
|
+
if (!network) {
|
|
162
|
+
return notSettled("settlement_network_missing", receiptJson);
|
|
163
|
+
}
|
|
164
|
+
if (expectedNetwork && !matchesAny(network, [expectedNetwork])) {
|
|
165
|
+
return notSettled("settlement_network_mismatch", receiptJson);
|
|
166
|
+
}
|
|
167
|
+
const expectedAsset = expectation.selectedRequirement
|
|
168
|
+
? firstString(expectation.selectedRequirement, ["asset", "assetPackage", "asset_package"])
|
|
169
|
+
: null;
|
|
170
|
+
const asset = firstString(response, ["asset", "assetPackage", "asset_package"]);
|
|
171
|
+
if (!asset) {
|
|
172
|
+
return notSettled("settlement_asset_missing", receiptJson);
|
|
173
|
+
}
|
|
174
|
+
if (expectedAsset && !matchesAny(asset, [expectedAsset])) {
|
|
175
|
+
return notSettled("settlement_asset_mismatch", receiptJson);
|
|
176
|
+
}
|
|
177
|
+
const expectedAmount = expectation.selectedRequirement
|
|
178
|
+
? firstString(expectation.selectedRequirement, [
|
|
179
|
+
"amount",
|
|
180
|
+
"maxAmountRequired",
|
|
181
|
+
"max_amount_required"
|
|
182
|
+
])
|
|
183
|
+
: null;
|
|
184
|
+
const amount = firstString(response, ["amount", "maxAmountRequired", "max_amount_required"]);
|
|
185
|
+
if (!amount) {
|
|
186
|
+
return notSettled("settlement_amount_missing", receiptJson);
|
|
187
|
+
}
|
|
188
|
+
const responseAmount = parseDecimalAmount(amount);
|
|
189
|
+
if (!responseAmount) {
|
|
190
|
+
return notSettled("settlement_amount_invalid", receiptJson);
|
|
191
|
+
}
|
|
192
|
+
if (expectedAmount) {
|
|
193
|
+
const expected = parseDecimalAmount(expectedAmount);
|
|
194
|
+
if (!expected || compareDecimal(responseAmount, expected) !== 0) {
|
|
195
|
+
return notSettled("settlement_amount_mismatch", receiptJson);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const expectedPayer = expectation.signedPayload
|
|
199
|
+
? firstString(expectation.signedPayload, ["payer", "payerAccount", "payer_account"])
|
|
200
|
+
: null;
|
|
201
|
+
const payer = firstString(response, ["payer", "payerAccount", "payer_account"]);
|
|
202
|
+
if (!payer) {
|
|
203
|
+
return notSettled("settlement_payer_missing", receiptJson);
|
|
204
|
+
}
|
|
205
|
+
if (expectedPayer && !matchesAny(payer, [expectedPayer])) {
|
|
206
|
+
return notSettled("settlement_payer_mismatch", receiptJson);
|
|
207
|
+
}
|
|
208
|
+
const expectedPayee = expectation.selectedRequirement
|
|
209
|
+
? firstString(expectation.selectedRequirement, ["payTo", "pay_to", "payee"])
|
|
210
|
+
: null;
|
|
211
|
+
const payee = firstString(response, ["payTo", "pay_to", "payee"]);
|
|
212
|
+
if (!payee) {
|
|
213
|
+
return notSettled("settlement_payee_missing", receiptJson);
|
|
214
|
+
}
|
|
215
|
+
if (expectedPayee && !matchesAny(payee, [expectedPayee])) {
|
|
216
|
+
return notSettled("settlement_payee_mismatch", receiptJson);
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
settled: true,
|
|
220
|
+
settlement_status: "settled",
|
|
221
|
+
transaction_hash: transactionHash,
|
|
222
|
+
receipt_json: receiptJson
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function notSettled(reason, receiptJson) {
|
|
226
|
+
return {
|
|
227
|
+
settled: false,
|
|
228
|
+
settlement_status: "not_settled",
|
|
229
|
+
reason,
|
|
230
|
+
receipt_json: receiptJson
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function validateRequirementCandidate(input) {
|
|
234
|
+
const amount = firstString(input.candidate, [
|
|
235
|
+
"maxAmountRequired",
|
|
236
|
+
"amount",
|
|
237
|
+
"max_amount_required"
|
|
238
|
+
]);
|
|
239
|
+
if (!amount) {
|
|
240
|
+
return "amount_missing";
|
|
241
|
+
}
|
|
242
|
+
const candidateAmount = parseDecimalAmount(amount);
|
|
243
|
+
if (!candidateAmount) {
|
|
244
|
+
return "amount_invalid";
|
|
245
|
+
}
|
|
246
|
+
if (input.expectedAmount && compareDecimal(candidateAmount, input.expectedAmount) !== 0) {
|
|
247
|
+
return "expected_amount_mismatch";
|
|
248
|
+
}
|
|
249
|
+
if (!input.expectedAmount && compareDecimal(candidateAmount, input.policyMax) > 0) {
|
|
250
|
+
return "amount_over_limit";
|
|
251
|
+
}
|
|
252
|
+
const resource = firstString(input.candidate, ["resource", "resourceUrl", "resource_url"]);
|
|
253
|
+
if (!resource) {
|
|
254
|
+
return "resource_missing";
|
|
255
|
+
}
|
|
256
|
+
if (resource !== input.url) {
|
|
257
|
+
return "resource_mismatch";
|
|
258
|
+
}
|
|
259
|
+
const requirementMethod = firstString(input.candidate, ["method", "httpMethod", "http_method"]);
|
|
260
|
+
if (!requirementMethod) {
|
|
261
|
+
return "method_missing";
|
|
262
|
+
}
|
|
263
|
+
if (requirementMethod.toUpperCase() !== input.method) {
|
|
264
|
+
return "method_mismatch";
|
|
265
|
+
}
|
|
266
|
+
const policySchemes = policyStrings(input.policy.allowed_asset, [
|
|
267
|
+
"scheme",
|
|
268
|
+
"schemes",
|
|
269
|
+
"payment_scheme",
|
|
270
|
+
"payment_schemes"
|
|
271
|
+
]);
|
|
272
|
+
const requirementScheme = firstString(input.candidate, ["scheme"]);
|
|
273
|
+
if (!requirementScheme) {
|
|
274
|
+
return "scheme_missing";
|
|
275
|
+
}
|
|
276
|
+
if (policySchemes.length > 0 && !matchesAny(requirementScheme ?? "", policySchemes)) {
|
|
277
|
+
return "scheme_mismatch";
|
|
278
|
+
}
|
|
279
|
+
const policyNetworks = policyStrings(input.policy.allowed_asset, [
|
|
280
|
+
"network",
|
|
281
|
+
"networkId",
|
|
282
|
+
"network_id",
|
|
283
|
+
"caip2_chain_id",
|
|
284
|
+
"caip2ChainId",
|
|
285
|
+
"chain_id",
|
|
286
|
+
"chainId"
|
|
287
|
+
]);
|
|
288
|
+
const requirementNetwork = firstString(input.candidate, [
|
|
289
|
+
"network",
|
|
290
|
+
"networkId",
|
|
291
|
+
"network_id",
|
|
292
|
+
"caip2_chain_id",
|
|
293
|
+
"caip2ChainId"
|
|
294
|
+
]);
|
|
295
|
+
if (!requirementNetwork) {
|
|
296
|
+
return "network_missing";
|
|
297
|
+
}
|
|
298
|
+
if (policyNetworks.length > 0 &&
|
|
299
|
+
!policyNetworks.some((policyNetwork) => matchesAny(requirementNetwork ?? "", [policyNetwork]))) {
|
|
300
|
+
return "network_mismatch";
|
|
301
|
+
}
|
|
302
|
+
const policyAssets = policyStrings(input.policy.allowed_asset, [
|
|
303
|
+
"asset",
|
|
304
|
+
"assetPackage",
|
|
305
|
+
"asset_package",
|
|
306
|
+
"assetPackageHash",
|
|
307
|
+
"asset_package_hash",
|
|
308
|
+
"token",
|
|
309
|
+
"tokenAddress",
|
|
310
|
+
"token_address"
|
|
311
|
+
]);
|
|
312
|
+
const requirementAsset = firstString(input.candidate, ["asset", "assetPackage", "asset_package"]);
|
|
313
|
+
if (!requirementAsset) {
|
|
314
|
+
return "asset_missing";
|
|
315
|
+
}
|
|
316
|
+
if (policyAssets.length > 0 && !matchesAny(requirementAsset ?? "", policyAssets)) {
|
|
317
|
+
return "asset_mismatch";
|
|
318
|
+
}
|
|
319
|
+
const policyPayees = policyStrings(input.policy.allowed_asset, [
|
|
320
|
+
"payTo",
|
|
321
|
+
"pay_to",
|
|
322
|
+
"payee",
|
|
323
|
+
"recipient",
|
|
324
|
+
"recipientAddress",
|
|
325
|
+
"recipient_address"
|
|
326
|
+
]);
|
|
327
|
+
const requirementPayee = firstString(input.candidate, [
|
|
328
|
+
"payTo",
|
|
329
|
+
"pay_to",
|
|
330
|
+
"payee",
|
|
331
|
+
"recipient",
|
|
332
|
+
"recipientAddress",
|
|
333
|
+
"recipient_address"
|
|
334
|
+
]);
|
|
335
|
+
if (!requirementPayee) {
|
|
336
|
+
return "payee_missing";
|
|
337
|
+
}
|
|
338
|
+
if (policyPayees.length > 0 && !matchesAny(requirementPayee ?? "", policyPayees)) {
|
|
339
|
+
return "payee_mismatch";
|
|
340
|
+
}
|
|
341
|
+
const timeout = firstValue(input.candidate, [
|
|
342
|
+
"timeout",
|
|
343
|
+
"timeoutSeconds",
|
|
344
|
+
"timeout_seconds",
|
|
345
|
+
"maxTimeoutSeconds",
|
|
346
|
+
"max_timeout_seconds"
|
|
347
|
+
]);
|
|
348
|
+
if (timeout === null) {
|
|
349
|
+
return "timeout_missing";
|
|
350
|
+
}
|
|
351
|
+
if (!validTimeoutSeconds(timeout)) {
|
|
352
|
+
return "timeout_invalid";
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
function rejected(reason) {
|
|
357
|
+
return {
|
|
358
|
+
approved: false,
|
|
359
|
+
reason,
|
|
360
|
+
rejected_candidates: []
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function asRecord(value) {
|
|
364
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
365
|
+
? value
|
|
366
|
+
: null;
|
|
367
|
+
}
|
|
368
|
+
function firstString(record, keys) {
|
|
369
|
+
for (const key of keys) {
|
|
370
|
+
const value = record[key];
|
|
371
|
+
if (typeof value === "string" && value.trim()) {
|
|
372
|
+
return value.trim();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
function firstValue(record, keys) {
|
|
378
|
+
for (const key of keys) {
|
|
379
|
+
if (record[key] !== undefined && record[key] !== null) {
|
|
380
|
+
return record[key];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function policyStrings(value, keys) {
|
|
386
|
+
const strings = new Set();
|
|
387
|
+
for (const key of keys) {
|
|
388
|
+
const nested = value[key];
|
|
389
|
+
if (typeof nested === "string" && nested.trim()) {
|
|
390
|
+
strings.add(nested.trim());
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(nested)) {
|
|
393
|
+
for (const item of nested) {
|
|
394
|
+
if (typeof item === "string" && item.trim()) {
|
|
395
|
+
strings.add(item.trim());
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return [...strings];
|
|
401
|
+
}
|
|
402
|
+
function matchesAny(value, accepted) {
|
|
403
|
+
const normalized = value.toLowerCase();
|
|
404
|
+
return accepted.some((item) => item.toLowerCase() === normalized);
|
|
405
|
+
}
|
|
406
|
+
function compareDecimal(leftParts, rightParts) {
|
|
407
|
+
const scale = Math.max(leftParts.scale, rightParts.scale);
|
|
408
|
+
const leftValue = leftParts.value * 10n ** BigInt(scale - leftParts.scale);
|
|
409
|
+
const rightValue = rightParts.value * 10n ** BigInt(scale - rightParts.scale);
|
|
410
|
+
if (leftValue === rightValue) {
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
return leftValue > rightValue ? 1 : -1;
|
|
414
|
+
}
|
|
415
|
+
function parseDecimalAmount(value) {
|
|
416
|
+
if (!/^\d+(\.\d+)?$/.test(value)) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const [whole, fraction = ""] = value.split(".");
|
|
420
|
+
return {
|
|
421
|
+
value: BigInt(`${whole}${fraction}`),
|
|
422
|
+
scale: fraction.length
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function validTimeoutSeconds(value) {
|
|
426
|
+
if (typeof value === "number") {
|
|
427
|
+
return Number.isInteger(value) && value > 0;
|
|
428
|
+
}
|
|
429
|
+
if (typeof value === "string") {
|
|
430
|
+
return /^\d+$/.test(value.trim()) && BigInt(value.trim()) > 0n;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function redactX402Value(value: unknown, depth?: number): unknown;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const REDACTED = "[redacted]";
|
|
2
|
+
const MAX_DEPTH = 8;
|
|
3
|
+
const MAX_ARRAY_ITEMS = 100;
|
|
4
|
+
const MAX_STRING_LENGTH = 4096;
|
|
5
|
+
export function redactX402Value(value, depth = 0) {
|
|
6
|
+
if (depth >= MAX_DEPTH) {
|
|
7
|
+
return "[truncated]";
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.slice(0, MAX_ARRAY_ITEMS).map((item) => redactX402Value(item, depth + 1));
|
|
11
|
+
}
|
|
12
|
+
if (value && typeof value === "object") {
|
|
13
|
+
const record = value;
|
|
14
|
+
const redacted = {};
|
|
15
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
16
|
+
redacted[key] = isSensitiveKey(key) ? REDACTED : redactX402Value(nested, depth + 1);
|
|
17
|
+
}
|
|
18
|
+
return redacted;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === "string" && value.length > MAX_STRING_LENGTH) {
|
|
21
|
+
return `${value.slice(0, MAX_STRING_LENGTH)}[truncated]`;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function isSensitiveKey(key) {
|
|
26
|
+
if (/(_hash|Hash|hash)$/.test(key)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return /secret|token|key|password|private|payload|credential|authorization|signature|value/i.test(key);
|
|
30
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import type { JsonObject } from "../memory/types.js";
|
|
3
|
+
export type X402PaidResourceLogger = {
|
|
4
|
+
info?(message: string): void;
|
|
5
|
+
warn?(message: string): void;
|
|
6
|
+
error?(message: string): void;
|
|
7
|
+
};
|
|
8
|
+
export type X402PaidResourceReplayState = "settling" | "settled";
|
|
9
|
+
export type X402PaidResourceReplayRecord = {
|
|
10
|
+
replayKey: string;
|
|
11
|
+
payloadHash: string;
|
|
12
|
+
state: X402PaidResourceReplayState;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
};
|
|
15
|
+
export type X402PaidResourceReplayReservation = {
|
|
16
|
+
ok: true;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: "payment_payload_replayed" | "nonce_replayed" | "payment_settlement_in_progress";
|
|
20
|
+
};
|
|
21
|
+
export type X402PaidResourceReplayStore = {
|
|
22
|
+
reserve(input: {
|
|
23
|
+
replayKey: string;
|
|
24
|
+
payloadHash: string;
|
|
25
|
+
}): X402PaidResourceReplayReservation;
|
|
26
|
+
complete(input: {
|
|
27
|
+
replayKey: string;
|
|
28
|
+
payloadHash: string;
|
|
29
|
+
}): void;
|
|
30
|
+
release(input: {
|
|
31
|
+
replayKey: string;
|
|
32
|
+
payloadHash: string;
|
|
33
|
+
}): void;
|
|
34
|
+
};
|
|
35
|
+
export type X402PaidResourceBodyFactory = (input: {
|
|
36
|
+
paymentPayload: JsonObject;
|
|
37
|
+
settlementResponse: JsonObject;
|
|
38
|
+
}) => JsonObject | Promise<JsonObject>;
|
|
39
|
+
export type X402PaidResourceFacilitatorPostResult = {
|
|
40
|
+
status: number;
|
|
41
|
+
body: unknown;
|
|
42
|
+
};
|
|
43
|
+
export type X402PaidResourceFacilitatorPoster = (url: string, body: JsonObject) => Promise<X402PaidResourceFacilitatorPostResult>;
|
|
44
|
+
export type X402PaidResourceHttpServerConfig = {
|
|
45
|
+
resourcePath: string;
|
|
46
|
+
facilitatorUrl: string;
|
|
47
|
+
paymentRequirements: JsonObject;
|
|
48
|
+
paymentHeaderName?: string;
|
|
49
|
+
resourceBody?: JsonObject | X402PaidResourceBodyFactory;
|
|
50
|
+
postJson?: X402PaidResourceFacilitatorPoster;
|
|
51
|
+
replayStore?: X402PaidResourceReplayStore;
|
|
52
|
+
logger?: X402PaidResourceLogger;
|
|
53
|
+
};
|
|
54
|
+
export declare class InMemoryX402PaidResourceReplayStore implements X402PaidResourceReplayStore {
|
|
55
|
+
private readonly records;
|
|
56
|
+
reserve(input: {
|
|
57
|
+
replayKey: string;
|
|
58
|
+
payloadHash: string;
|
|
59
|
+
}): X402PaidResourceReplayReservation;
|
|
60
|
+
complete(input: {
|
|
61
|
+
replayKey: string;
|
|
62
|
+
payloadHash: string;
|
|
63
|
+
}): void;
|
|
64
|
+
release(input: {
|
|
65
|
+
replayKey: string;
|
|
66
|
+
payloadHash: string;
|
|
67
|
+
}): void;
|
|
68
|
+
}
|
|
69
|
+
export declare function createX402PaidResourceHttpServer(config: X402PaidResourceHttpServerConfig): Server;
|