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,71 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
import { type Server } from "node:http";
|
|
3
|
+
import type { JsonObject } from "../memory/types.js";
|
|
4
|
+
export type CasperSigningKey = {
|
|
5
|
+
algorithm: "ed25519";
|
|
6
|
+
privateKey: KeyObject;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
} | {
|
|
9
|
+
algorithm: "secp256k1";
|
|
10
|
+
privateKey: KeyObject;
|
|
11
|
+
privateScalar: bigint;
|
|
12
|
+
publicKey: string;
|
|
13
|
+
};
|
|
14
|
+
export type X402SignerLogger = {
|
|
15
|
+
info?(message: string): void;
|
|
16
|
+
warn?(message: string): void;
|
|
17
|
+
error?(message: string): void;
|
|
18
|
+
};
|
|
19
|
+
export type X402SignerConfig = {
|
|
20
|
+
signingKey: CasperSigningKey;
|
|
21
|
+
buyerAccountHash: string;
|
|
22
|
+
now?: () => Date;
|
|
23
|
+
maxValiditySeconds?: number;
|
|
24
|
+
signBytes?: (bytes: Buffer, signingKey: CasperSigningKey) => string;
|
|
25
|
+
};
|
|
26
|
+
export type X402SignerHttpServerConfig = X402SignerConfig & {
|
|
27
|
+
authToken?: string | null;
|
|
28
|
+
logger?: X402SignerLogger;
|
|
29
|
+
maxBodyBytes?: number;
|
|
30
|
+
};
|
|
31
|
+
export type X402SignerRequest = {
|
|
32
|
+
payment_id: string;
|
|
33
|
+
facilitator_url: string | null;
|
|
34
|
+
method: string;
|
|
35
|
+
url: string;
|
|
36
|
+
selected_requirement: JsonObject;
|
|
37
|
+
selected_requirement_hash: string;
|
|
38
|
+
policy_hash: string;
|
|
39
|
+
};
|
|
40
|
+
export type X402SignerRejectionReason = "request_not_object" | "unexpected_field" | "payment_id_invalid" | "facilitator_url_invalid" | "method_invalid" | "url_invalid" | "selected_requirement_missing" | "selected_requirement_sensitive_field" | "selected_requirement_hash_invalid" | "selected_requirement_hash_mismatch" | "policy_hash_invalid" | "scheme_missing" | "scheme_unsupported" | "network_missing" | "amount_missing" | "amount_invalid" | "resource_missing" | "resource_mismatch" | "method_missing" | "method_mismatch" | "asset_missing" | "payee_missing" | "timeout_missing" | "timeout_invalid" | "buyer_account_invalid";
|
|
41
|
+
export type X402SignerValidation = {
|
|
42
|
+
ok: true;
|
|
43
|
+
request: X402SignerRequest;
|
|
44
|
+
requirement: NormalizedRequirement;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
reason: X402SignerRejectionReason;
|
|
48
|
+
};
|
|
49
|
+
export type X402SignerResult = {
|
|
50
|
+
signed: true;
|
|
51
|
+
signed_payload: JsonObject;
|
|
52
|
+
} | {
|
|
53
|
+
signed: false;
|
|
54
|
+
reason: X402SignerRejectionReason;
|
|
55
|
+
};
|
|
56
|
+
type NormalizedRequirement = {
|
|
57
|
+
scheme: "exact";
|
|
58
|
+
network: string;
|
|
59
|
+
amount: string;
|
|
60
|
+
resource: string;
|
|
61
|
+
method: string;
|
|
62
|
+
asset: string;
|
|
63
|
+
payTo: string;
|
|
64
|
+
timeoutSeconds: number;
|
|
65
|
+
};
|
|
66
|
+
export declare function loadCasperSigningKeyFromFile(path: string): CasperSigningKey;
|
|
67
|
+
export declare function loadCasperSigningKey(pem: string): CasperSigningKey;
|
|
68
|
+
export declare function validateX402SignerRequest(input: unknown): X402SignerValidation;
|
|
69
|
+
export declare function signX402PaymentPayload(input: unknown, config: X402SignerConfig): X402SignerResult;
|
|
70
|
+
export declare function createX402SignerHttpServer(config: X402SignerHttpServerConfig): Server;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { createHash, createHmac, createPrivateKey, createPublicKey, sign as nodeSign } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { canonicalizeJson, toJsonObject } from "../memory/canonical.js";
|
|
5
|
+
import { sha256Hex } from "../memory/hash.js";
|
|
6
|
+
const HASH_HEX_PATTERN = /^[a-f0-9]{64}$/i;
|
|
7
|
+
const PAYMENT_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
8
|
+
const HTTP_METHOD_PATTERN = /^[A-Za-z]+$/;
|
|
9
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
10
|
+
const DEFAULT_MAX_VALIDITY_SECONDS = 900;
|
|
11
|
+
const CASPER_ACCOUNT_PATTERN = /^(account-hash-[a-f0-9]{64}|0[01][a-f0-9]{64})$/i;
|
|
12
|
+
const SENSITIVE_REQUIREMENT_KEY_PATTERN = /private|secret|token|password|credential|authorization|seed|mnemonic/i;
|
|
13
|
+
const SECP256K1_P = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f");
|
|
14
|
+
const SECP256K1_N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
|
|
15
|
+
const SECP256K1_GX = BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
|
|
16
|
+
const SECP256K1_GY = BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8");
|
|
17
|
+
export function loadCasperSigningKeyFromFile(path) {
|
|
18
|
+
return loadCasperSigningKey(readFileSync(path, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
export function loadCasperSigningKey(pem) {
|
|
21
|
+
const privateKey = createPrivateKey(pem);
|
|
22
|
+
const publicKey = createPublicKey(privateKey);
|
|
23
|
+
if (privateKey.asymmetricKeyType === "ed25519") {
|
|
24
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
25
|
+
if (typeof jwk.x !== "string") {
|
|
26
|
+
throw new Error("Unable to read Ed25519 public key");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
algorithm: "ed25519",
|
|
30
|
+
privateKey,
|
|
31
|
+
publicKey: `01${base64UrlToBuffer(jwk.x).toString("hex")}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (privateKey.asymmetricKeyType === "ec") {
|
|
35
|
+
const jwk = privateKey.export({ format: "jwk" });
|
|
36
|
+
if (jwk.crv !== "secp256k1" || typeof jwk.d !== "string") {
|
|
37
|
+
throw new Error("Only Ed25519 and secp256k1 Casper keys are supported");
|
|
38
|
+
}
|
|
39
|
+
if (typeof jwk.x !== "string" || typeof jwk.y !== "string") {
|
|
40
|
+
throw new Error("Unable to read secp256k1 public key");
|
|
41
|
+
}
|
|
42
|
+
const x = leftPad(base64UrlToBuffer(jwk.x), 32);
|
|
43
|
+
const y = leftPad(base64UrlToBuffer(jwk.y), 32);
|
|
44
|
+
const compressedPrefix = y[y.length - 1] % 2 === 0 ? "02" : "03";
|
|
45
|
+
return {
|
|
46
|
+
algorithm: "secp256k1",
|
|
47
|
+
privateKey,
|
|
48
|
+
privateScalar: bytesToBigInt(leftPad(base64UrlToBuffer(jwk.d), 32)),
|
|
49
|
+
publicKey: `02${compressedPrefix}${x.toString("hex")}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
throw new Error("Only Ed25519 and secp256k1 Casper keys are supported");
|
|
53
|
+
}
|
|
54
|
+
export function validateX402SignerRequest(input) {
|
|
55
|
+
const body = asRecord(input);
|
|
56
|
+
if (!body) {
|
|
57
|
+
return rejected("request_not_object");
|
|
58
|
+
}
|
|
59
|
+
const allowedFields = new Set([
|
|
60
|
+
"payment_id",
|
|
61
|
+
"facilitator_url",
|
|
62
|
+
"method",
|
|
63
|
+
"url",
|
|
64
|
+
"selected_requirement",
|
|
65
|
+
"approved_requirement",
|
|
66
|
+
"selected_requirement_hash",
|
|
67
|
+
"policy_hash"
|
|
68
|
+
]);
|
|
69
|
+
for (const key of Object.keys(body)) {
|
|
70
|
+
if (!allowedFields.has(key)) {
|
|
71
|
+
return rejected("unexpected_field");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const paymentId = stringValue(body.payment_id);
|
|
75
|
+
if (!paymentId || !PAYMENT_ID_PATTERN.test(paymentId)) {
|
|
76
|
+
return rejected("payment_id_invalid");
|
|
77
|
+
}
|
|
78
|
+
const method = stringValue(body.method)?.toUpperCase() ?? null;
|
|
79
|
+
if (!method || !HTTP_METHOD_PATTERN.test(method)) {
|
|
80
|
+
return rejected("method_invalid");
|
|
81
|
+
}
|
|
82
|
+
const url = stringValue(body.url);
|
|
83
|
+
if (!url || !isHttpUrl(url)) {
|
|
84
|
+
return rejected("url_invalid");
|
|
85
|
+
}
|
|
86
|
+
const facilitatorUrl = nullableStringValue(body.facilitator_url);
|
|
87
|
+
if (facilitatorUrl !== null && !isHttpUrl(facilitatorUrl)) {
|
|
88
|
+
return rejected("facilitator_url_invalid");
|
|
89
|
+
}
|
|
90
|
+
const selectedRequirementValue = body.selected_requirement ?? body.approved_requirement;
|
|
91
|
+
let selectedRequirement;
|
|
92
|
+
try {
|
|
93
|
+
selectedRequirement = toJsonObject(selectedRequirementValue, "selected_requirement");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return rejected("selected_requirement_missing");
|
|
97
|
+
}
|
|
98
|
+
if (hasSensitiveRequirementKey(selectedRequirement)) {
|
|
99
|
+
return rejected("selected_requirement_sensitive_field");
|
|
100
|
+
}
|
|
101
|
+
const selectedRequirementHash = stringValue(body.selected_requirement_hash);
|
|
102
|
+
if (!selectedRequirementHash || !HASH_HEX_PATTERN.test(selectedRequirementHash)) {
|
|
103
|
+
return rejected("selected_requirement_hash_invalid");
|
|
104
|
+
}
|
|
105
|
+
const actualRequirementHash = sha256Hex(canonicalizeJson(selectedRequirement));
|
|
106
|
+
if (selectedRequirementHash.toLowerCase() !== actualRequirementHash) {
|
|
107
|
+
return rejected("selected_requirement_hash_mismatch");
|
|
108
|
+
}
|
|
109
|
+
const policyHash = stringValue(body.policy_hash);
|
|
110
|
+
if (!policyHash || !HASH_HEX_PATTERN.test(policyHash)) {
|
|
111
|
+
return rejected("policy_hash_invalid");
|
|
112
|
+
}
|
|
113
|
+
const requirement = normalizeRequirement(selectedRequirement);
|
|
114
|
+
if (!requirement.ok) {
|
|
115
|
+
return rejected(requirement.reason);
|
|
116
|
+
}
|
|
117
|
+
if (requirement.value.resource !== url) {
|
|
118
|
+
return rejected("resource_mismatch");
|
|
119
|
+
}
|
|
120
|
+
if (requirement.value.method !== method) {
|
|
121
|
+
return rejected("method_mismatch");
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
request: {
|
|
126
|
+
payment_id: paymentId,
|
|
127
|
+
facilitator_url: facilitatorUrl,
|
|
128
|
+
method,
|
|
129
|
+
url,
|
|
130
|
+
selected_requirement: selectedRequirement,
|
|
131
|
+
selected_requirement_hash: selectedRequirementHash.toLowerCase(),
|
|
132
|
+
policy_hash: policyHash.toLowerCase()
|
|
133
|
+
},
|
|
134
|
+
requirement: requirement.value
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function signX402PaymentPayload(input, config) {
|
|
138
|
+
const validation = validateX402SignerRequest(input);
|
|
139
|
+
if (!validation.ok) {
|
|
140
|
+
return {
|
|
141
|
+
signed: false,
|
|
142
|
+
reason: validation.reason
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const buyerAccountHash = normalizeBuyerAccount(config.buyerAccountHash);
|
|
146
|
+
if (!buyerAccountHash) {
|
|
147
|
+
return {
|
|
148
|
+
signed: false,
|
|
149
|
+
reason: "buyer_account_invalid"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const validity = createValidityWindow(config.now?.() ?? new Date(), validation.requirement.timeoutSeconds, config.maxValiditySeconds ?? DEFAULT_MAX_VALIDITY_SECONDS);
|
|
153
|
+
const authorizationBase = {
|
|
154
|
+
domain: "casper-x402-authorization",
|
|
155
|
+
version: 1,
|
|
156
|
+
paymentId: validation.request.payment_id,
|
|
157
|
+
policyHash: validation.request.policy_hash,
|
|
158
|
+
selectedRequirementHash: validation.request.selected_requirement_hash,
|
|
159
|
+
method: validation.request.method,
|
|
160
|
+
resource: validation.request.url,
|
|
161
|
+
scheme: validation.requirement.scheme,
|
|
162
|
+
network: validation.requirement.network,
|
|
163
|
+
asset: validation.requirement.asset,
|
|
164
|
+
payTo: validation.requirement.payTo,
|
|
165
|
+
amount: validation.requirement.amount,
|
|
166
|
+
payer: buyerAccountHash,
|
|
167
|
+
publicKey: config.signingKey.publicKey,
|
|
168
|
+
validAfter: validity.validAfter,
|
|
169
|
+
validBefore: validity.validBefore
|
|
170
|
+
};
|
|
171
|
+
const nonce = sha256Hex(canonicalizeJson(authorizationBase));
|
|
172
|
+
const authorizationToSign = {
|
|
173
|
+
...authorizationBase,
|
|
174
|
+
nonce
|
|
175
|
+
};
|
|
176
|
+
const canonicalAuthorization = canonicalizeJson(authorizationToSign);
|
|
177
|
+
const canonicalBytes = Buffer.from(canonicalAuthorization, "utf8");
|
|
178
|
+
const signature = (config.signBytes ?? signCasperBytes)(canonicalBytes, config.signingKey);
|
|
179
|
+
const authorizationHash = sha256Hex(canonicalAuthorization);
|
|
180
|
+
return {
|
|
181
|
+
signed: true,
|
|
182
|
+
signed_payload: {
|
|
183
|
+
x402Version: 2,
|
|
184
|
+
scheme: validation.requirement.scheme,
|
|
185
|
+
network: validation.requirement.network,
|
|
186
|
+
accepted: validation.request.selected_requirement,
|
|
187
|
+
paymentId: validation.request.payment_id,
|
|
188
|
+
policyHash: validation.request.policy_hash,
|
|
189
|
+
method: validation.request.method,
|
|
190
|
+
resource: validation.request.url,
|
|
191
|
+
asset: validation.requirement.asset,
|
|
192
|
+
payTo: validation.requirement.payTo,
|
|
193
|
+
amount: validation.requirement.amount,
|
|
194
|
+
payer: buyerAccountHash,
|
|
195
|
+
nonce,
|
|
196
|
+
validAfter: validity.validAfter,
|
|
197
|
+
validUntil: validity.validBefore,
|
|
198
|
+
selectedRequirementHash: validation.request.selected_requirement_hash,
|
|
199
|
+
authorization: {
|
|
200
|
+
type: validation.requirement.asset.toLowerCase() === "casper-native-cspr"
|
|
201
|
+
? "casper-native-transfer"
|
|
202
|
+
: "casper-cep18-transfer-with-authorization",
|
|
203
|
+
paymentId: validation.request.payment_id,
|
|
204
|
+
policyHash: validation.request.policy_hash,
|
|
205
|
+
method: validation.request.method,
|
|
206
|
+
resource: validation.request.url,
|
|
207
|
+
scheme: validation.requirement.scheme,
|
|
208
|
+
network: validation.requirement.network,
|
|
209
|
+
asset: validation.requirement.asset,
|
|
210
|
+
payTo: validation.requirement.payTo,
|
|
211
|
+
payer: buyerAccountHash,
|
|
212
|
+
from: buyerAccountHash,
|
|
213
|
+
to: validation.requirement.payTo,
|
|
214
|
+
amount: validation.requirement.amount,
|
|
215
|
+
value: validation.requirement.amount,
|
|
216
|
+
validAfter: validity.validAfter,
|
|
217
|
+
validBefore: validity.validBefore,
|
|
218
|
+
nonce,
|
|
219
|
+
publicKey: config.signingKey.publicKey,
|
|
220
|
+
signature,
|
|
221
|
+
authorizationHash
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
export function createX402SignerHttpServer(config) {
|
|
227
|
+
return createServer(async (request, response) => {
|
|
228
|
+
try {
|
|
229
|
+
await handleX402SignerHttpRequest(request, response, config);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
config.logger?.error?.("x402 signer request failed");
|
|
233
|
+
sendJson(response, 500, { error: "signer_failed" });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function handleX402SignerHttpRequest(request, response, config) {
|
|
238
|
+
if (request.method !== "POST" || request.url !== "/sign") {
|
|
239
|
+
sendJson(response, 404, { error: "not_found" });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (config.authToken) {
|
|
243
|
+
const authorization = headerValue(request, "authorization");
|
|
244
|
+
if (authorization !== `Bearer ${config.authToken}`) {
|
|
245
|
+
sendJson(response, 401, { error: "unauthorized" });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const body = await readJsonBody(request, config.maxBodyBytes ?? MAX_BODY_BYTES);
|
|
250
|
+
if (!body.ok) {
|
|
251
|
+
config.logger?.warn?.(`x402 signer rejected reason=${body.reason}`);
|
|
252
|
+
sendJson(response, 400, { error: "invalid_json", reason: body.reason });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const result = signX402PaymentPayload(body.value, config);
|
|
256
|
+
if (!result.signed) {
|
|
257
|
+
config.logger?.warn?.(`x402 signer rejected reason=${result.reason}`);
|
|
258
|
+
sendJson(response, 400, { error: "sign_request_invalid", reason: result.reason });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const signedPayloadHash = sha256Hex(canonicalizeJson(result.signed_payload));
|
|
262
|
+
const paymentId = stringValue(asRecord(body.value)?.payment_id) ?? "<unknown>";
|
|
263
|
+
const selectedRequirementHash = stringValue(asRecord(body.value)?.selected_requirement_hash) ?? "<unknown>";
|
|
264
|
+
config.logger?.info?.(`x402 signer signed payment_id=${paymentId} selected_requirement_hash=${selectedRequirementHash} signed_payload_hash=${signedPayloadHash}`);
|
|
265
|
+
sendJson(response, 200, { signed_payload: result.signed_payload });
|
|
266
|
+
}
|
|
267
|
+
function normalizeRequirement(requirement) {
|
|
268
|
+
const scheme = firstString(requirement, ["scheme"]);
|
|
269
|
+
if (!scheme) {
|
|
270
|
+
return { ok: false, reason: "scheme_missing" };
|
|
271
|
+
}
|
|
272
|
+
if (scheme !== "exact") {
|
|
273
|
+
return { ok: false, reason: "scheme_unsupported" };
|
|
274
|
+
}
|
|
275
|
+
const network = firstString(requirement, [
|
|
276
|
+
"network",
|
|
277
|
+
"networkId",
|
|
278
|
+
"network_id",
|
|
279
|
+
"caip2_chain_id",
|
|
280
|
+
"caip2ChainId"
|
|
281
|
+
]);
|
|
282
|
+
if (!network) {
|
|
283
|
+
return { ok: false, reason: "network_missing" };
|
|
284
|
+
}
|
|
285
|
+
const amount = firstString(requirement, [
|
|
286
|
+
"maxAmountRequired",
|
|
287
|
+
"amount",
|
|
288
|
+
"max_amount_required"
|
|
289
|
+
]);
|
|
290
|
+
if (!amount) {
|
|
291
|
+
return { ok: false, reason: "amount_missing" };
|
|
292
|
+
}
|
|
293
|
+
if (!/^(0|[1-9]\d*)$/.test(amount)) {
|
|
294
|
+
return { ok: false, reason: "amount_invalid" };
|
|
295
|
+
}
|
|
296
|
+
const resource = firstString(requirement, ["resource", "resourceUrl", "resource_url"]);
|
|
297
|
+
if (!resource) {
|
|
298
|
+
return { ok: false, reason: "resource_missing" };
|
|
299
|
+
}
|
|
300
|
+
const method = firstString(requirement, ["method", "httpMethod", "http_method"]);
|
|
301
|
+
if (!method) {
|
|
302
|
+
return { ok: false, reason: "method_missing" };
|
|
303
|
+
}
|
|
304
|
+
const normalizedMethod = method.toUpperCase();
|
|
305
|
+
if (!HTTP_METHOD_PATTERN.test(normalizedMethod)) {
|
|
306
|
+
return { ok: false, reason: "method_invalid" };
|
|
307
|
+
}
|
|
308
|
+
const asset = firstString(requirement, [
|
|
309
|
+
"asset",
|
|
310
|
+
"assetId",
|
|
311
|
+
"asset_id",
|
|
312
|
+
"assetPackage",
|
|
313
|
+
"asset_package",
|
|
314
|
+
"assetPackageHash",
|
|
315
|
+
"asset_package_hash"
|
|
316
|
+
]);
|
|
317
|
+
if (!asset) {
|
|
318
|
+
return { ok: false, reason: "asset_missing" };
|
|
319
|
+
}
|
|
320
|
+
const payTo = firstString(requirement, [
|
|
321
|
+
"payTo",
|
|
322
|
+
"pay_to",
|
|
323
|
+
"payee",
|
|
324
|
+
"recipient",
|
|
325
|
+
"recipientAddress",
|
|
326
|
+
"recipient_address"
|
|
327
|
+
]);
|
|
328
|
+
if (!payTo) {
|
|
329
|
+
return { ok: false, reason: "payee_missing" };
|
|
330
|
+
}
|
|
331
|
+
const timeout = firstValue(requirement, [
|
|
332
|
+
"timeout",
|
|
333
|
+
"timeoutSeconds",
|
|
334
|
+
"timeout_seconds",
|
|
335
|
+
"maxTimeoutSeconds",
|
|
336
|
+
"max_timeout_seconds"
|
|
337
|
+
]);
|
|
338
|
+
if (timeout === null) {
|
|
339
|
+
return { ok: false, reason: "timeout_missing" };
|
|
340
|
+
}
|
|
341
|
+
const timeoutSeconds = timeoutValue(timeout);
|
|
342
|
+
if (timeoutSeconds === null) {
|
|
343
|
+
return { ok: false, reason: "timeout_invalid" };
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
value: {
|
|
348
|
+
scheme: "exact",
|
|
349
|
+
network,
|
|
350
|
+
amount,
|
|
351
|
+
resource,
|
|
352
|
+
method: normalizedMethod,
|
|
353
|
+
asset,
|
|
354
|
+
payTo,
|
|
355
|
+
timeoutSeconds
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function signCasperBytes(bytes, signingKey) {
|
|
360
|
+
if (signingKey.algorithm === "ed25519") {
|
|
361
|
+
return `01${nodeSign(null, bytes, signingKey.privateKey).toString("hex")}`;
|
|
362
|
+
}
|
|
363
|
+
const digest = createHash("sha256").update(bytes).digest();
|
|
364
|
+
const signature = deterministicSecp256k1Signature(digest, signingKey.privateScalar);
|
|
365
|
+
return `02${signature}`;
|
|
366
|
+
}
|
|
367
|
+
function deterministicSecp256k1Signature(digest, privateScalar) {
|
|
368
|
+
const z = bytesToBigInt(digest) % SECP256K1_N;
|
|
369
|
+
for (const k of deterministicK(digest, privateScalar)) {
|
|
370
|
+
const point = scalarMultiply(k, {
|
|
371
|
+
x: SECP256K1_GX,
|
|
372
|
+
y: SECP256K1_GY
|
|
373
|
+
});
|
|
374
|
+
if (!point) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const r = mod(point.x, SECP256K1_N);
|
|
378
|
+
if (r === 0n) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
let s = mod(modInverse(k, SECP256K1_N) * (z + r * privateScalar), SECP256K1_N);
|
|
382
|
+
if (s === 0n) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (s > SECP256K1_N / 2n) {
|
|
386
|
+
s = SECP256K1_N - s;
|
|
387
|
+
}
|
|
388
|
+
return `${bigIntToFixedHex(r, 32)}${bigIntToFixedHex(s, 32)}`;
|
|
389
|
+
}
|
|
390
|
+
throw new Error("Unable to produce deterministic secp256k1 signature");
|
|
391
|
+
}
|
|
392
|
+
function* deterministicK(digest, privateScalar) {
|
|
393
|
+
const x = bigIntToBuffer(privateScalar, 32);
|
|
394
|
+
const h1 = bigIntToBuffer(bytesToBigInt(digest) % SECP256K1_N, 32);
|
|
395
|
+
let v = Buffer.alloc(32, 0x01);
|
|
396
|
+
let k = Buffer.alloc(32, 0x00);
|
|
397
|
+
k = hmacSha256(k, Buffer.concat([v, Buffer.from([0x00]), x, h1]));
|
|
398
|
+
v = hmacSha256(k, v);
|
|
399
|
+
k = hmacSha256(k, Buffer.concat([v, Buffer.from([0x01]), x, h1]));
|
|
400
|
+
v = hmacSha256(k, v);
|
|
401
|
+
for (;;) {
|
|
402
|
+
v = hmacSha256(k, v);
|
|
403
|
+
const candidate = bytesToBigInt(v);
|
|
404
|
+
if (candidate > 0n && candidate < SECP256K1_N) {
|
|
405
|
+
yield candidate;
|
|
406
|
+
}
|
|
407
|
+
k = hmacSha256(k, Buffer.concat([v, Buffer.from([0x00])]));
|
|
408
|
+
v = hmacSha256(k, v);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function scalarMultiply(scalar, point) {
|
|
412
|
+
let addend = point;
|
|
413
|
+
let result = null;
|
|
414
|
+
let remaining = scalar;
|
|
415
|
+
while (remaining > 0n) {
|
|
416
|
+
if (remaining & 1n) {
|
|
417
|
+
result = pointAdd(result, addend);
|
|
418
|
+
}
|
|
419
|
+
addend = pointAdd(addend, addend);
|
|
420
|
+
remaining >>= 1n;
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
function pointAdd(left, right) {
|
|
425
|
+
if (!left) {
|
|
426
|
+
return right;
|
|
427
|
+
}
|
|
428
|
+
if (!right) {
|
|
429
|
+
return left;
|
|
430
|
+
}
|
|
431
|
+
if (left.x === right.x && mod(left.y + right.y, SECP256K1_P) === 0n) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const slope = left.x === right.x && left.y === right.y
|
|
435
|
+
? mod(3n * left.x * left.x * modInverse(2n * left.y, SECP256K1_P), SECP256K1_P)
|
|
436
|
+
: mod((right.y - left.y) * modInverse(right.x - left.x, SECP256K1_P), SECP256K1_P);
|
|
437
|
+
const x = mod(slope * slope - left.x - right.x, SECP256K1_P);
|
|
438
|
+
const y = mod(slope * (left.x - x) - left.y, SECP256K1_P);
|
|
439
|
+
return { x, y };
|
|
440
|
+
}
|
|
441
|
+
function createValidityWindow(now, requirementTimeoutSeconds, maxValiditySeconds) {
|
|
442
|
+
const validitySeconds = Math.max(1, Math.min(requirementTimeoutSeconds, maxValiditySeconds));
|
|
443
|
+
const validAfterMs = Math.floor(now.getTime() / 1000) * 1000;
|
|
444
|
+
const validBeforeMs = validAfterMs + validitySeconds * 1000;
|
|
445
|
+
return {
|
|
446
|
+
validAfter: new Date(validAfterMs).toISOString(),
|
|
447
|
+
validBefore: new Date(validBeforeMs).toISOString()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function readJsonBody(request, maxBodyBytes) {
|
|
451
|
+
return new Promise((resolve) => {
|
|
452
|
+
const chunks = [];
|
|
453
|
+
let total = 0;
|
|
454
|
+
let resolved = false;
|
|
455
|
+
request.on("data", (chunk) => {
|
|
456
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
457
|
+
total += buffer.byteLength;
|
|
458
|
+
if (total > maxBodyBytes && !resolved) {
|
|
459
|
+
resolved = true;
|
|
460
|
+
request.destroy();
|
|
461
|
+
resolve({ ok: false, reason: "body_too_large" });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
chunks.push(buffer);
|
|
465
|
+
});
|
|
466
|
+
request.on("end", () => {
|
|
467
|
+
if (resolved) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
471
|
+
try {
|
|
472
|
+
resolve({ ok: true, value: raw.trim() ? JSON.parse(raw) : {} });
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
resolve({ ok: false, reason: "body_invalid_json" });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
request.on("error", () => {
|
|
479
|
+
if (!resolved) {
|
|
480
|
+
resolved = true;
|
|
481
|
+
resolve({ ok: false, reason: "body_invalid_json" });
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
function sendJson(response, statusCode, value) {
|
|
487
|
+
if (response.writableEnded) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
response.statusCode = statusCode;
|
|
491
|
+
response.setHeader("content-type", "application/json");
|
|
492
|
+
response.end(`${JSON.stringify(value)}\n`);
|
|
493
|
+
}
|
|
494
|
+
function headerValue(request, name) {
|
|
495
|
+
const value = request.headers[name.toLowerCase()];
|
|
496
|
+
if (Array.isArray(value)) {
|
|
497
|
+
return value[0] ?? null;
|
|
498
|
+
}
|
|
499
|
+
return value ?? null;
|
|
500
|
+
}
|
|
501
|
+
function normalizeBuyerAccount(value) {
|
|
502
|
+
const normalized = value.trim().toLowerCase();
|
|
503
|
+
return CASPER_ACCOUNT_PATTERN.test(normalized) ? normalized : null;
|
|
504
|
+
}
|
|
505
|
+
function hasSensitiveRequirementKey(value) {
|
|
506
|
+
if (Array.isArray(value)) {
|
|
507
|
+
return value.some(hasSensitiveRequirementKey);
|
|
508
|
+
}
|
|
509
|
+
if (!value || typeof value !== "object") {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return Object.entries(value).some(([key, nested]) => SENSITIVE_REQUIREMENT_KEY_PATTERN.test(key) || hasSensitiveRequirementKey(nested));
|
|
513
|
+
}
|
|
514
|
+
function timeoutValue(value) {
|
|
515
|
+
if (typeof value === "number") {
|
|
516
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
517
|
+
}
|
|
518
|
+
if (typeof value === "string" && /^[1-9]\d*$/.test(value.trim())) {
|
|
519
|
+
const parsed = Number(value.trim());
|
|
520
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
function isHttpUrl(value) {
|
|
525
|
+
try {
|
|
526
|
+
const parsed = new URL(value);
|
|
527
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function nullableStringValue(value) {
|
|
534
|
+
if (value === null || value === undefined) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return stringValue(value);
|
|
538
|
+
}
|
|
539
|
+
function stringValue(value) {
|
|
540
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
541
|
+
}
|
|
542
|
+
function firstString(record, keys) {
|
|
543
|
+
for (const key of keys) {
|
|
544
|
+
const value = record[key];
|
|
545
|
+
if (typeof value === "string" && value.trim()) {
|
|
546
|
+
return value.trim();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
function firstValue(record, keys) {
|
|
552
|
+
for (const key of keys) {
|
|
553
|
+
if (record[key] !== undefined && record[key] !== null) {
|
|
554
|
+
return record[key];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
function asRecord(value) {
|
|
560
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
561
|
+
? value
|
|
562
|
+
: null;
|
|
563
|
+
}
|
|
564
|
+
function rejected(reason) {
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
reason
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function hmacSha256(key, value) {
|
|
571
|
+
return createHmac("sha256", key).update(value).digest();
|
|
572
|
+
}
|
|
573
|
+
function mod(value, by) {
|
|
574
|
+
const result = value % by;
|
|
575
|
+
return result >= 0n ? result : result + by;
|
|
576
|
+
}
|
|
577
|
+
function modInverse(value, by) {
|
|
578
|
+
let low = mod(value, by);
|
|
579
|
+
let high = by;
|
|
580
|
+
let lowCoefficient = 1n;
|
|
581
|
+
let highCoefficient = 0n;
|
|
582
|
+
while (low > 1n) {
|
|
583
|
+
const ratio = high / low;
|
|
584
|
+
[low, high] = [high - low * ratio, low];
|
|
585
|
+
[lowCoefficient, highCoefficient] = [
|
|
586
|
+
highCoefficient - lowCoefficient * ratio,
|
|
587
|
+
lowCoefficient
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
return mod(lowCoefficient, by);
|
|
591
|
+
}
|
|
592
|
+
function bytesToBigInt(value) {
|
|
593
|
+
return BigInt(`0x${value.toString("hex") || "0"}`);
|
|
594
|
+
}
|
|
595
|
+
function bigIntToBuffer(value, length) {
|
|
596
|
+
return Buffer.from(bigIntToFixedHex(value, length), "hex");
|
|
597
|
+
}
|
|
598
|
+
function bigIntToFixedHex(value, length) {
|
|
599
|
+
const hex = value.toString(16);
|
|
600
|
+
if (hex.length > length * 2) {
|
|
601
|
+
throw new Error("Integer does not fit fixed buffer");
|
|
602
|
+
}
|
|
603
|
+
return hex.padStart(length * 2, "0");
|
|
604
|
+
}
|
|
605
|
+
function leftPad(value, length) {
|
|
606
|
+
if (value.length > length) {
|
|
607
|
+
throw new Error("Value does not fit fixed buffer");
|
|
608
|
+
}
|
|
609
|
+
if (value.length === length) {
|
|
610
|
+
return value;
|
|
611
|
+
}
|
|
612
|
+
return Buffer.concat([Buffer.alloc(length - value.length), value]);
|
|
613
|
+
}
|
|
614
|
+
function base64UrlToBuffer(value) {
|
|
615
|
+
return Buffer.from(value, "base64url");
|
|
616
|
+
}
|