wolverine-ai 5.2.2 → 5.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "5.2.2",
3
+ "version": "5.2.4",
4
4
  "description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -4,49 +4,21 @@ const path = require("path");
4
4
  /**
5
5
  * x402 Fastify Plugin — add crypto payments to any route with one flag.
6
6
  *
7
- * Makes it dead simple to accept USDC payments on Base network.
8
- * The developer marks a route with x402 config, and the middleware
9
- * handles the 402 → payment → verification → settlement flow.
10
- *
11
- * Settlement is handled by the x402 facilitator (Coinbase CDP),
12
- * which verifies the payment signature AND executes the on-chain
13
- * USDC transfer. Compatible with x402 Bazaar for API discovery.
7
+ * Implements the x402 v2 protocol with CDP facilitator for on-chain settlement.
8
+ * Compatible with @x402/fetch, @x402/evm client SDKs and x402 Bazaar.
14
9
  *
15
10
  * Two modes:
16
11
  * Fixed price: { x402: { price: "$0.01" } }
17
12
  * Variable price: { x402: { variable: true, min: "$1", max: "$10000" } }
18
- *
19
- * Example — fixed price route:
20
- * fastify.get("/premium-data", { config: { x402: { price: "$0.10" } } }, handler)
21
- *
22
- * Example — variable price (credit purchase):
23
- * fastify.post("/buy-credits", {
24
- * config: {
25
- * x402: {
26
- * variable: true,
27
- * min: "$1",
28
- * max: "$10000",
29
- * priceField: "dollars", // reads amount from request body
30
- * }
31
- * }
32
- * }, async (req, reply) => {
33
- * // req.x402.paid === true, req.x402.amount === "$5.00"
34
- * addCredits(req.x402.amount);
35
- * return { credits: newBalance };
36
- * })
37
- *
38
- * The payment receipt is in req.x402 after verification:
39
- * { paid: true, amount: "$5.00", receipt: {...}, txHash: "0x..." }
40
13
  */
41
14
 
15
+ const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
16
+
42
17
  let _payTo = null;
43
18
  let _network = "eip155:8453";
44
- // Production facilitator (Base mainnet) — handles verify + on-chain settlement
45
- let _facilitatorUrl = "https://api.cdp.coinbase.com/platform/v2/x402";
46
- // Testnet facilitator: "https://x402.org/facilitator"
19
+ let _facilitatorUrl = "https://x402.org/facilitator";
47
20
 
48
21
  async function x402Plugin(fastify, opts) {
49
- // Config
50
22
  _network = opts.network || _network;
51
23
  _facilitatorUrl = opts.facilitator || _facilitatorUrl;
52
24
  _payTo = opts.payTo || null;
@@ -73,7 +45,7 @@ async function x402Plugin(fastify, opts) {
73
45
  const isTestnet = _network.includes("84532") || _network.includes("11155");
74
46
  _facilitatorUrl = isTestnet
75
47
  ? "https://x402.org/facilitator"
76
- : "https://api.cdp.coinbase.com/platform/v2/x402";
48
+ : "https://x402.org/facilitator"; // CDP production requires auth — use x402.org for both
77
49
  }
78
50
 
79
51
  if (_payTo) {
@@ -81,20 +53,18 @@ async function x402Plugin(fastify, opts) {
81
53
  console.log(` 💰 x402: facilitator ${_facilitatorUrl}`);
82
54
  }
83
55
 
84
- // ── Route-level x402 hook (preHandler so body is parsed for variable pricing) ──
56
+ // ── Route-level x402 hook ──
85
57
  fastify.addHook("preHandler", async (request, reply) => {
86
- // Check if this route has x402 config (Fastify v4: context.config, v5: routeOptions.config)
87
58
  const routeConfig = request.routeOptions?.config?.x402 || request.routeConfig?.x402 || request.context?.config?.x402;
88
- if (!routeConfig) return; // Not an x402 route
59
+ if (!routeConfig) return;
89
60
  if (!_payTo) {
90
61
  reply.code(500).send({ error: "x402 not configured — no wallet address" });
91
62
  return;
92
63
  }
93
64
 
94
65
  // Determine the price
95
- let price;
66
+ let dollarAmount;
96
67
  if (routeConfig.variable) {
97
- // Variable pricing — read amount from request body or query
98
68
  const field = routeConfig.priceField || "dollars";
99
69
  const raw = request.body?.[field] || request.query?.[field];
100
70
  if (!raw) {
@@ -104,76 +74,124 @@ async function x402Plugin(fastify, opts) {
104
74
  });
105
75
  return;
106
76
  }
107
- const amount = parseFloat(String(raw).replace("$", ""));
77
+ dollarAmount = parseFloat(String(raw).replace("$", ""));
108
78
  const min = parseFloat((routeConfig.min || "$1").replace("$", ""));
109
79
  const max = parseFloat((routeConfig.max || "$10000").replace("$", ""));
110
- if (isNaN(amount) || amount < min || amount > max) {
80
+ if (isNaN(dollarAmount) || dollarAmount < min || dollarAmount > max) {
111
81
  reply.code(400).send({ error: `Amount must be $${min}-$${max}` });
112
82
  return;
113
83
  }
114
- price = "$" + amount.toFixed(2);
115
84
  } else {
116
- price = routeConfig.price;
117
- if (!price) { reply.code(500).send({ error: "x402 route missing price config" }); return; }
85
+ dollarAmount = parseFloat((routeConfig.price || "0").replace("$", ""));
86
+ if (!dollarAmount) { reply.code(500).send({ error: "x402 route missing price config" }); return; }
118
87
  }
119
88
 
120
- const paymentSig = request.headers["payment-signature"];
121
-
122
- // No payment return 402 with payment instructions
123
- if (!paymentSig) {
124
- const instructions = {
125
- accepts: [{
126
- scheme: "exact",
127
- price,
128
- network: _network,
129
- payTo: _payTo,
130
- }],
131
- description: routeConfig.description || `Payment for ${request.method} ${request.url}`,
132
- mimeType: "application/json",
89
+ // USDC amount in atomic units (6 decimals)
90
+ const usdcAmount = String(Math.round(dollarAmount * 1e6));
91
+ const price = "$" + dollarAmount.toFixed(2);
92
+
93
+ const paymentHeader = request.headers["payment-signature"] || request.headers["x-payment"];
94
+
95
+ // No payment — return 402 with x402 v2 payment requirements
96
+ if (!paymentHeader) {
97
+ const paymentRequirements = {
98
+ scheme: "exact",
99
+ network: _network,
100
+ amount: usdcAmount,
101
+ asset: USDC_BASE,
102
+ payTo: _payTo,
103
+ maxTimeoutSeconds: 300,
104
+ extra: { name: "USD Coin", version: "2" }, // EIP-712 domain params for USDC
105
+ };
106
+ const paymentRequired = {
107
+ x402Version: 2,
108
+ error: "Payment Required",
109
+ resource: {
110
+ url: `${request.method} ${request.url}`,
111
+ description: routeConfig.description || `Payment for ${request.method} ${request.url}`,
112
+ mimeType: "application/json",
113
+ },
114
+ accepts: [paymentRequirements],
133
115
  };
116
+
117
+ const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
134
118
  reply
135
119
  .code(402)
136
- .header("Payment-Required", JSON.stringify(instructions))
120
+ .header("Payment-Required", encoded)
137
121
  .send({
138
122
  error: "Payment Required",
139
123
  price,
140
124
  network: _network,
141
125
  payTo: _payTo,
142
126
  protocol: "x402",
127
+ x402Version: 2,
143
128
  ...(routeConfig.variable ? { variable: true, min: routeConfig.min, max: routeConfig.max, priceField: routeConfig.priceField || "dollars" } : {}),
144
129
  });
145
130
  return;
146
131
  }
147
132
 
148
- // Payment present — verify and settle on-chain
149
- // Try facilitator first (standard x402 SDK clients), then self-settle (wallet-signed payments)
150
- const routeAccepts = [{
133
+ // Payment present — decode, verify via facilitator, then settle
134
+ const paymentRequirements = {
151
135
  scheme: "exact",
152
- price,
153
136
  network: _network,
137
+ amount: usdcAmount,
138
+ asset: USDC_BASE,
154
139
  payTo: _payTo,
155
- }];
140
+ maxTimeoutSeconds: 300,
141
+ extra: { name: "USD Coin", version: "2" },
142
+ };
143
+
144
+ // Decode the payment payload
145
+ let paymentPayload;
146
+ try {
147
+ const decoded = Buffer.from(paymentHeader, "base64").toString();
148
+ paymentPayload = JSON.parse(decoded);
149
+ } catch {
150
+ reply.code(400).send({ error: "Invalid payment encoding" });
151
+ return;
152
+ }
153
+
154
+ // Ensure x402Version is set
155
+ if (!paymentPayload.x402Version) {
156
+ paymentPayload.x402Version = 2;
157
+ }
158
+
159
+ // Ensure the accepted requirements are included (x402 v2 spec)
160
+ if (!paymentPayload.accepted) {
161
+ paymentPayload.accepted = paymentRequirements;
162
+ }
156
163
 
157
- // Step 1: Try facilitator (works with @x402/fetch, @x402/evm SDK clients)
158
- const verified = await _settleViaFacilitator(paymentSig, routeAccepts);
159
- if (verified.valid) {
160
- reply.header("Payment-Response", JSON.stringify(verified.receipt || {}));
161
- request.x402 = { paid: true, amount: price, receipt: verified.receipt, txHash: verified.txHash, from: verified.from };
162
- _logPayment({ route: request.url, method: request.method, amount: price, from: verified.from, txHash: verified.txHash, verified: true, settled: "facilitator", timestamp: Date.now() });
163
- return; // continue to route handler
164
+ // Step 1: Verify via facilitator
165
+ const verifyResult = await _facilitatorCall("/verify", paymentPayload, paymentRequirements);
166
+ if (!verifyResult.ok) {
167
+ reply.code(402).send({
168
+ error: "Payment verification failed",
169
+ reason: verifyResult.reason,
170
+ price,
171
+ payTo: _payTo,
172
+ });
173
+ return;
164
174
  }
165
175
 
166
- // Step 2: Facilitator failed or rejected — self-settle via vault wallet
167
- // This handles wallet-signed EIP-3009 payments (our own frontend, custom clients)
168
- const selfResult = await _selfSettle(paymentSig, price);
169
- if (selfResult.valid) {
170
- reply.header("Payment-Response", JSON.stringify(selfResult.receipt || {}));
171
- request.x402 = { paid: true, amount: price, receipt: selfResult.receipt, txHash: selfResult.txHash, from: selfResult.from };
172
- _logPayment({ route: request.url, method: request.method, amount: price, from: selfResult.from, txHash: selfResult.txHash, verified: true, settled: "self", timestamp: Date.now() });
176
+ // Step 2: Settle via facilitator (executes on-chain transfer)
177
+ const settleResult = await _facilitatorCall("/settle", paymentPayload, paymentRequirements);
178
+ if (!settleResult.ok) {
179
+ reply.code(402).send({
180
+ error: "Payment settlement failed",
181
+ reason: settleResult.reason,
182
+ price,
183
+ payTo: _payTo,
184
+ });
173
185
  return;
174
186
  }
175
187
 
176
- reply.code(402).send({ error: "Payment settlement failed", reason: selfResult.reason, facilitatorReason: verified.reason, price, payTo: _payTo });
188
+ // Payment verified AND settled on-chain
189
+ const txHash = settleResult.data?.transaction || settleResult.data?.txHash || null;
190
+ const payer = settleResult.data?.payer || verifyResult.data?.payer || paymentPayload.payload?.authorization?.from || null;
191
+
192
+ reply.header("Payment-Response", JSON.stringify(settleResult.data || {}));
193
+ request.x402 = { paid: true, amount: price, receipt: settleResult.data, txHash, from: payer };
194
+ _logPayment({ route: request.url, method: request.method, amount: price, from: payer, txHash, verified: true, settled: "facilitator", timestamp: Date.now() });
177
195
  });
178
196
 
179
197
  // ── Public pricing endpoint ──
@@ -182,28 +200,28 @@ async function x402Plugin(fastify, opts) {
182
200
  network: _network,
183
201
  facilitator: _facilitatorUrl,
184
202
  protocol: "x402",
203
+ x402Version: 2,
185
204
  docs: "https://docs.cdp.coinbase.com/x402/welcome",
186
205
  }));
187
206
  }
188
207
 
189
208
  /**
190
- * Send payment to the x402 facilitator for verification AND on-chain settlement.
191
- * The facilitator:
192
- * 1. Verifies the EIP-3009 signature
193
- * 2. Calls transferWithAuthorization() on the USDC contract
194
- * 3. Returns the transaction hash
209
+ * Call the x402 facilitator matches the exact format from @x402/core HTTPFacilitatorClient.
195
210
  *
196
- * This is the standard x402 flow — compatible with Bazaar discovery.
211
+ * POST {facilitatorUrl}/verify or /settle
212
+ * Body: { x402Version, paymentPayload, paymentRequirements }
197
213
  */
198
- async function _settleViaFacilitator(paymentSig, routeAccepts) {
214
+ async function _facilitatorCall(endpoint, paymentPayload, paymentRequirements) {
199
215
  try {
200
216
  const https = require("https");
201
217
  const http = require("http");
202
- const url = new (require("url").URL)(_facilitatorUrl + "/verify");
218
+ const url = new (require("url").URL)(_facilitatorUrl + endpoint);
203
219
  const body = JSON.stringify({
204
- paymentSignature: paymentSig,
205
- routeConfig: { accepts: routeAccepts },
220
+ x402Version: paymentPayload.x402Version || 2,
221
+ paymentPayload,
222
+ paymentRequirements,
206
223
  });
224
+
207
225
  return new Promise((resolve) => {
208
226
  const client = url.protocol === "https:" ? https : http;
209
227
  const req = client.request({
@@ -212,161 +230,43 @@ async function _settleViaFacilitator(paymentSig, routeAccepts) {
212
230
  path: url.pathname,
213
231
  method: "POST",
214
232
  headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
215
- timeout: 30000, // settlement can take time (on-chain tx)
233
+ timeout: 30000,
216
234
  }, (res) => {
217
235
  let data = "";
218
236
  res.on("data", (c) => data += c);
219
237
  res.on("end", () => {
220
238
  try {
221
- const p = JSON.parse(data);
222
- if (p.valid || p.success) {
223
- resolve({
224
- valid: true,
225
- receipt: p,
226
- txHash: p.txHash || p.transactionHash || null,
227
- from: p.from || p.payer || null,
228
- });
239
+ const parsed = JSON.parse(data);
240
+ if (res.statusCode >= 200 && res.statusCode < 300) {
241
+ // Verify: check isValid. Settle: check success.
242
+ if (endpoint === "/verify" && parsed.isValid === false) {
243
+ resolve({ ok: false, reason: parsed.invalidReason || "verification_rejected", data: parsed });
244
+ } else if (endpoint === "/settle" && parsed.success === false) {
245
+ resolve({ ok: false, reason: parsed.errorReason || "settlement_rejected", data: parsed });
246
+ } else {
247
+ resolve({ ok: true, data: parsed });
248
+ }
229
249
  } else {
230
- resolve({ valid: false, reason: p.error || p.reason || "facilitator_rejected" });
250
+ // Error response
251
+ const reason = parsed.invalidReason || parsed.errorReason || parsed.error || `facilitator_${res.statusCode}`;
252
+ console.log(` ⚠️ x402 facilitator ${endpoint} ${res.statusCode}: ${reason}`);
253
+ resolve({ ok: false, reason, data: parsed });
231
254
  }
232
255
  } catch {
233
- resolve({ valid: false, reason: "facilitator_invalid_response" });
256
+ resolve({ ok: false, reason: `facilitator_parse_error_${res.statusCode}` });
234
257
  }
235
258
  });
236
259
  });
237
- req.on("error", () => resolve({ valid: false, reason: "facilitator_unavailable" }));
238
- req.on("timeout", () => { req.destroy(); resolve({ valid: false, reason: "facilitator_timeout" }); });
260
+ req.on("error", (err) => {
261
+ console.log(` ⚠️ x402 facilitator ${endpoint} error: ${err.message}`);
262
+ resolve({ ok: false, reason: "facilitator_unavailable: " + err.message });
263
+ });
264
+ req.on("timeout", () => { req.destroy(); resolve({ ok: false, reason: "facilitator_timeout" }); });
239
265
  req.write(body);
240
266
  req.end();
241
267
  });
242
- } catch {
243
- return { valid: false, reason: "facilitator_unavailable" };
244
- }
245
- }
246
-
247
- /**
248
- * Self-settlement fallback — verify the signature AND execute the
249
- * on-chain transferWithAuthorization() using the server's vault wallet.
250
- *
251
- * Only used when the facilitator is unavailable. Requires ethers + RPC.
252
- * The server pays gas to execute the transfer.
253
- */
254
- async function _selfSettle(paymentSig, price) {
255
- let payload;
256
- try {
257
- payload = JSON.parse(Buffer.from(paymentSig, "base64").toString());
258
- } catch {
259
- return { valid: false, reason: "invalid_payment_format" };
260
- }
261
-
262
- if (!payload.payload?.authorization || !payload.payload?.signature) {
263
- return { valid: false, reason: "missing_authorization" };
264
- }
265
-
266
- const auth = payload.payload.authorization;
267
- const sig = payload.payload.signature;
268
-
269
- // Verify amount
270
- const expectedUsdc = Math.round(parseFloat(price.replace("$", "")) * 1e6);
271
- // Value comes as decimal string (e.g. "1000000" for $1) or hex (legacy "0xf4240")
272
- const valStr = String(auth.value);
273
- const actualUsdc = valStr.startsWith("0x") ? parseInt(valStr, 16) : parseInt(valStr, 10) || 0;
274
- if (actualUsdc < expectedUsdc * 0.99) {
275
- return { valid: false, reason: "amount_mismatch" };
276
- }
277
-
278
- // Verify payTo matches
279
- if (auth.to?.toLowerCase() !== _payTo?.toLowerCase()) {
280
- return { valid: false, reason: "wrong_recipient" };
281
- }
282
-
283
- // Verify not expired
284
- if (auth.validBefore && auth.validBefore < Math.floor(Date.now() / 1000)) {
285
- return { valid: false, reason: "payment_expired" };
286
- }
287
-
288
- try {
289
- const { ethers } = require("ethers");
290
- const fromAddr = ethers.getAddress(auth.from.toLowerCase());
291
- const toAddr = ethers.getAddress(auth.to.toLowerCase());
292
-
293
- // Step 1: Verify the signature
294
- const domain = {
295
- name: "USD Coin",
296
- version: "2",
297
- chainId: 8453,
298
- verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
299
- };
300
- const types = {
301
- TransferWithAuthorization: [
302
- { name: "from", type: "address" },
303
- { name: "to", type: "address" },
304
- { name: "value", type: "uint256" },
305
- { name: "validAfter", type: "uint256" },
306
- { name: "validBefore", type: "uint256" },
307
- { name: "nonce", type: "bytes32" },
308
- ],
309
- };
310
- const message = {
311
- from: fromAddr,
312
- to: toAddr,
313
- value: auth.value,
314
- validAfter: auth.validAfter,
315
- validBefore: auth.validBefore,
316
- nonce: auth.nonce,
317
- };
318
-
319
- const recoveredAddress = ethers.verifyTypedData(domain, types, message, sig);
320
- if (recoveredAddress.toLowerCase() !== fromAddr.toLowerCase()) {
321
- return { valid: false, reason: "signature_mismatch" };
322
- }
323
-
324
- // Step 2: Execute transferWithAuthorization on-chain
325
- // Use the vault wallet to submit the transaction (server pays gas)
326
- const { decryptPrivateKey } = require("../vault/vault-manager");
327
- let keyBuf = null;
328
- try {
329
- keyBuf = decryptPrivateKey();
330
- const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
331
- const wallet = new ethers.Wallet(keyBuf, provider);
332
-
333
- const usdcContract = new ethers.Contract(
334
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
335
- [
336
- "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external",
337
- ],
338
- wallet
339
- );
340
-
341
- // Parse the signature into v, r, s
342
- const sigBytes = ethers.getBytes(sig);
343
- const r = ethers.hexlify(sigBytes.slice(0, 32));
344
- const s = ethers.hexlify(sigBytes.slice(32, 64));
345
- const v = sigBytes[64];
346
-
347
- const tx = await usdcContract.transferWithAuthorization(
348
- fromAddr, toAddr, auth.value,
349
- auth.validAfter, auth.validBefore, auth.nonce,
350
- v, r, s,
351
- { gasLimit: 100000 }
352
- );
353
-
354
- // Wait for confirmation (1 block)
355
- const receipt = await tx.wait(1);
356
- console.log(` 💰 x402 self-settled: ${receipt.hash} (${price} USDC)`);
357
-
358
- return {
359
- valid: true,
360
- from: fromAddr,
361
- receipt: { authorization: auth, settled: "self", blockNumber: receipt.blockNumber },
362
- txHash: receipt.hash,
363
- };
364
- } finally {
365
- if (keyBuf) keyBuf.fill(0);
366
- }
367
268
  } catch (err) {
368
- console.log(` ⚠️ x402 self-settlement failed: ${err.message}`);
369
- return { valid: false, reason: "settlement_failed: " + err.message };
269
+ return { ok: false, reason: "facilitator_error: " + err.message };
370
270
  }
371
271
  }
372
272
 
@@ -376,7 +276,6 @@ function _logPayment(entry) {
376
276
  let payments = [];
377
277
  try { payments = JSON.parse(fs.readFileSync(logPath, "utf-8")); } catch {}
378
278
  payments.push(entry);
379
- // Keep last 1000 payments
380
279
  if (payments.length > 1000) payments = payments.slice(-1000);
381
280
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
382
281
  fs.writeFileSync(logPath, JSON.stringify(payments, null, 2));