wolverine-ai 5.2.1 → 5.2.3

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.1",
3
+ "version": "5.2.3",
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,78 +74,122 @@ 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
+ };
105
+ const paymentRequired = {
106
+ x402Version: 2,
107
+ error: "Payment Required",
108
+ resource: {
109
+ url: `${request.method} ${request.url}`,
110
+ description: routeConfig.description || `Payment for ${request.method} ${request.url}`,
111
+ mimeType: "application/json",
112
+ },
113
+ accepts: [paymentRequirements],
133
114
  };
115
+
116
+ const encoded = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
134
117
  reply
135
118
  .code(402)
136
- .header("Payment-Required", JSON.stringify(instructions))
119
+ .header("Payment-Required", encoded)
137
120
  .send({
138
121
  error: "Payment Required",
139
122
  price,
140
123
  network: _network,
141
124
  payTo: _payTo,
142
125
  protocol: "x402",
126
+ x402Version: 2,
143
127
  ...(routeConfig.variable ? { variable: true, min: routeConfig.min, max: routeConfig.max, priceField: routeConfig.priceField || "dollars" } : {}),
144
128
  });
145
129
  return;
146
130
  }
147
131
 
148
- // Payment present — send to facilitator for verification + on-chain settlement
149
- const routeAccepts = [{
132
+ // Payment present — decode, verify via facilitator, then settle
133
+ const paymentRequirements = {
150
134
  scheme: "exact",
151
- price,
152
135
  network: _network,
136
+ amount: usdcAmount,
137
+ asset: USDC_BASE,
153
138
  payTo: _payTo,
154
- }];
139
+ maxTimeoutSeconds: 300,
140
+ };
155
141
 
156
- const verified = await _settleViaFacilitator(paymentSig, routeAccepts);
157
- if (verified.valid) {
158
- reply.header("Payment-Response", JSON.stringify(verified.receipt || {}));
159
- request.x402 = { paid: true, amount: price, receipt: verified.receipt, txHash: verified.txHash, from: verified.from };
160
- // Log payment for dashboard analytics
161
- _logPayment({ route: request.url, method: request.method, amount: price, from: verified.from, txHash: verified.txHash, verified: true, timestamp: Date.now() });
162
- return; // continue to route handler
142
+ // Decode the payment payload
143
+ let paymentPayload;
144
+ try {
145
+ const decoded = Buffer.from(paymentHeader, "base64").toString();
146
+ paymentPayload = JSON.parse(decoded);
147
+ } catch {
148
+ reply.code(400).send({ error: "Invalid payment encoding" });
149
+ return;
163
150
  }
164
151
 
165
- // Facilitator rejected try self-settlement as fallback
166
- if (verified.reason === "facilitator_unavailable") {
167
- const selfResult = await _selfSettle(paymentSig, price);
168
- if (selfResult.valid) {
169
- reply.header("Payment-Response", JSON.stringify(selfResult.receipt || {}));
170
- request.x402 = { paid: true, amount: price, receipt: selfResult.receipt, txHash: selfResult.txHash, from: selfResult.from };
171
- _logPayment({ route: request.url, method: request.method, amount: price, from: selfResult.from, txHash: selfResult.txHash, verified: true, settled: "self", timestamp: Date.now() });
172
- return;
173
- }
174
- reply.code(402).send({ error: "Payment settlement failed", reason: selfResult.reason, price, payTo: _payTo });
152
+ // Ensure x402Version is set
153
+ if (!paymentPayload.x402Version) {
154
+ paymentPayload.x402Version = 2;
155
+ }
156
+
157
+ // Ensure the accepted requirements are included (x402 v2 spec)
158
+ if (!paymentPayload.accepted) {
159
+ paymentPayload.accepted = paymentRequirements;
160
+ }
161
+
162
+ // Step 1: Verify via facilitator
163
+ const verifyResult = await _facilitatorCall("/verify", paymentPayload, paymentRequirements);
164
+ if (!verifyResult.ok) {
165
+ reply.code(402).send({
166
+ error: "Payment verification failed",
167
+ reason: verifyResult.reason,
168
+ price,
169
+ payTo: _payTo,
170
+ });
175
171
  return;
176
172
  }
177
173
 
178
- reply.code(402).send({ error: "Payment verification failed", reason: verified.reason, price, payTo: _payTo });
174
+ // Step 2: Settle via facilitator (executes on-chain transfer)
175
+ const settleResult = await _facilitatorCall("/settle", paymentPayload, paymentRequirements);
176
+ if (!settleResult.ok) {
177
+ reply.code(402).send({
178
+ error: "Payment settlement failed",
179
+ reason: settleResult.reason,
180
+ price,
181
+ payTo: _payTo,
182
+ });
183
+ return;
184
+ }
185
+
186
+ // Payment verified AND settled on-chain
187
+ const txHash = settleResult.data?.transaction || settleResult.data?.txHash || null;
188
+ const payer = settleResult.data?.payer || verifyResult.data?.payer || paymentPayload.payload?.authorization?.from || null;
189
+
190
+ reply.header("Payment-Response", JSON.stringify(settleResult.data || {}));
191
+ request.x402 = { paid: true, amount: price, receipt: settleResult.data, txHash, from: payer };
192
+ _logPayment({ route: request.url, method: request.method, amount: price, from: payer, txHash, verified: true, settled: "facilitator", timestamp: Date.now() });
179
193
  });
180
194
 
181
195
  // ── Public pricing endpoint ──
@@ -184,28 +198,28 @@ async function x402Plugin(fastify, opts) {
184
198
  network: _network,
185
199
  facilitator: _facilitatorUrl,
186
200
  protocol: "x402",
201
+ x402Version: 2,
187
202
  docs: "https://docs.cdp.coinbase.com/x402/welcome",
188
203
  }));
189
204
  }
190
205
 
191
206
  /**
192
- * Send payment to the x402 facilitator for verification AND on-chain settlement.
193
- * The facilitator:
194
- * 1. Verifies the EIP-3009 signature
195
- * 2. Calls transferWithAuthorization() on the USDC contract
196
- * 3. Returns the transaction hash
207
+ * Call the x402 facilitator matches the exact format from @x402/core HTTPFacilitatorClient.
197
208
  *
198
- * This is the standard x402 flow — compatible with Bazaar discovery.
209
+ * POST {facilitatorUrl}/verify or /settle
210
+ * Body: { x402Version, paymentPayload, paymentRequirements }
199
211
  */
200
- async function _settleViaFacilitator(paymentSig, routeAccepts) {
212
+ async function _facilitatorCall(endpoint, paymentPayload, paymentRequirements) {
201
213
  try {
202
214
  const https = require("https");
203
215
  const http = require("http");
204
- const url = new (require("url").URL)(_facilitatorUrl + "/verify");
216
+ const url = new (require("url").URL)(_facilitatorUrl + endpoint);
205
217
  const body = JSON.stringify({
206
- paymentSignature: paymentSig,
207
- routeConfig: { accepts: routeAccepts },
218
+ x402Version: paymentPayload.x402Version || 2,
219
+ paymentPayload,
220
+ paymentRequirements,
208
221
  });
222
+
209
223
  return new Promise((resolve) => {
210
224
  const client = url.protocol === "https:" ? https : http;
211
225
  const req = client.request({
@@ -214,161 +228,43 @@ async function _settleViaFacilitator(paymentSig, routeAccepts) {
214
228
  path: url.pathname,
215
229
  method: "POST",
216
230
  headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
217
- timeout: 30000, // settlement can take time (on-chain tx)
231
+ timeout: 30000,
218
232
  }, (res) => {
219
233
  let data = "";
220
234
  res.on("data", (c) => data += c);
221
235
  res.on("end", () => {
222
236
  try {
223
- const p = JSON.parse(data);
224
- if (p.valid || p.success) {
225
- resolve({
226
- valid: true,
227
- receipt: p,
228
- txHash: p.txHash || p.transactionHash || null,
229
- from: p.from || p.payer || null,
230
- });
237
+ const parsed = JSON.parse(data);
238
+ if (res.statusCode >= 200 && res.statusCode < 300) {
239
+ // Verify: check isValid. Settle: check success.
240
+ if (endpoint === "/verify" && parsed.isValid === false) {
241
+ resolve({ ok: false, reason: parsed.invalidReason || "verification_rejected", data: parsed });
242
+ } else if (endpoint === "/settle" && parsed.success === false) {
243
+ resolve({ ok: false, reason: parsed.errorReason || "settlement_rejected", data: parsed });
244
+ } else {
245
+ resolve({ ok: true, data: parsed });
246
+ }
231
247
  } else {
232
- resolve({ valid: false, reason: p.error || p.reason || "facilitator_rejected" });
248
+ // Error response
249
+ const reason = parsed.invalidReason || parsed.errorReason || parsed.error || `facilitator_${res.statusCode}`;
250
+ console.log(` ⚠️ x402 facilitator ${endpoint} ${res.statusCode}: ${reason}`);
251
+ resolve({ ok: false, reason, data: parsed });
233
252
  }
234
253
  } catch {
235
- resolve({ valid: false, reason: "facilitator_invalid_response" });
254
+ resolve({ ok: false, reason: `facilitator_parse_error_${res.statusCode}` });
236
255
  }
237
256
  });
238
257
  });
239
- req.on("error", () => resolve({ valid: false, reason: "facilitator_unavailable" }));
240
- req.on("timeout", () => { req.destroy(); resolve({ valid: false, reason: "facilitator_timeout" }); });
258
+ req.on("error", (err) => {
259
+ console.log(` ⚠️ x402 facilitator ${endpoint} error: ${err.message}`);
260
+ resolve({ ok: false, reason: "facilitator_unavailable: " + err.message });
261
+ });
262
+ req.on("timeout", () => { req.destroy(); resolve({ ok: false, reason: "facilitator_timeout" }); });
241
263
  req.write(body);
242
264
  req.end();
243
265
  });
244
- } catch {
245
- return { valid: false, reason: "facilitator_unavailable" };
246
- }
247
- }
248
-
249
- /**
250
- * Self-settlement fallback — verify the signature AND execute the
251
- * on-chain transferWithAuthorization() using the server's vault wallet.
252
- *
253
- * Only used when the facilitator is unavailable. Requires ethers + RPC.
254
- * The server pays gas to execute the transfer.
255
- */
256
- async function _selfSettle(paymentSig, price) {
257
- let payload;
258
- try {
259
- payload = JSON.parse(Buffer.from(paymentSig, "base64").toString());
260
- } catch {
261
- return { valid: false, reason: "invalid_payment_format" };
262
- }
263
-
264
- if (!payload.payload?.authorization || !payload.payload?.signature) {
265
- return { valid: false, reason: "missing_authorization" };
266
- }
267
-
268
- const auth = payload.payload.authorization;
269
- const sig = payload.payload.signature;
270
-
271
- // Verify amount
272
- const expectedUsdc = Math.round(parseFloat(price.replace("$", "")) * 1e6);
273
- // Value comes as decimal string (e.g. "1000000" for $1) or hex (legacy "0xf4240")
274
- const valStr = String(auth.value);
275
- const actualUsdc = valStr.startsWith("0x") ? parseInt(valStr, 16) : parseInt(valStr, 10) || 0;
276
- if (actualUsdc < expectedUsdc * 0.99) {
277
- return { valid: false, reason: "amount_mismatch" };
278
- }
279
-
280
- // Verify payTo matches
281
- if (auth.to?.toLowerCase() !== _payTo?.toLowerCase()) {
282
- return { valid: false, reason: "wrong_recipient" };
283
- }
284
-
285
- // Verify not expired
286
- if (auth.validBefore && auth.validBefore < Math.floor(Date.now() / 1000)) {
287
- return { valid: false, reason: "payment_expired" };
288
- }
289
-
290
- try {
291
- const { ethers } = require("ethers");
292
- const fromAddr = ethers.getAddress(auth.from.toLowerCase());
293
- const toAddr = ethers.getAddress(auth.to.toLowerCase());
294
-
295
- // Step 1: Verify the signature
296
- const domain = {
297
- name: "USD Coin",
298
- version: "2",
299
- chainId: 8453,
300
- verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
301
- };
302
- const types = {
303
- TransferWithAuthorization: [
304
- { name: "from", type: "address" },
305
- { name: "to", type: "address" },
306
- { name: "value", type: "uint256" },
307
- { name: "validAfter", type: "uint256" },
308
- { name: "validBefore", type: "uint256" },
309
- { name: "nonce", type: "bytes32" },
310
- ],
311
- };
312
- const message = {
313
- from: fromAddr,
314
- to: toAddr,
315
- value: auth.value,
316
- validAfter: auth.validAfter,
317
- validBefore: auth.validBefore,
318
- nonce: auth.nonce,
319
- };
320
-
321
- const recoveredAddress = ethers.verifyTypedData(domain, types, message, sig);
322
- if (recoveredAddress.toLowerCase() !== fromAddr.toLowerCase()) {
323
- return { valid: false, reason: "signature_mismatch" };
324
- }
325
-
326
- // Step 2: Execute transferWithAuthorization on-chain
327
- // Use the vault wallet to submit the transaction (server pays gas)
328
- const { decryptPrivateKey } = require("../vault/vault-manager");
329
- let keyBuf = null;
330
- try {
331
- keyBuf = decryptPrivateKey();
332
- const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
333
- const wallet = new ethers.Wallet(keyBuf, provider);
334
-
335
- const usdcContract = new ethers.Contract(
336
- "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
337
- [
338
- "function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external",
339
- ],
340
- wallet
341
- );
342
-
343
- // Parse the signature into v, r, s
344
- const sigBytes = ethers.getBytes(sig);
345
- const r = ethers.hexlify(sigBytes.slice(0, 32));
346
- const s = ethers.hexlify(sigBytes.slice(32, 64));
347
- const v = sigBytes[64];
348
-
349
- const tx = await usdcContract.transferWithAuthorization(
350
- fromAddr, toAddr, auth.value,
351
- auth.validAfter, auth.validBefore, auth.nonce,
352
- v, r, s,
353
- { gasLimit: 100000 }
354
- );
355
-
356
- // Wait for confirmation (1 block)
357
- const receipt = await tx.wait(1);
358
- console.log(` 💰 x402 self-settled: ${receipt.hash} (${price} USDC)`);
359
-
360
- return {
361
- valid: true,
362
- from: fromAddr,
363
- receipt: { authorization: auth, settled: "self", blockNumber: receipt.blockNumber },
364
- txHash: receipt.hash,
365
- };
366
- } finally {
367
- if (keyBuf) keyBuf.fill(0);
368
- }
369
266
  } catch (err) {
370
- console.log(` ⚠️ x402 self-settlement failed: ${err.message}`);
371
- return { valid: false, reason: "settlement_failed: " + err.message };
267
+ return { ok: false, reason: "facilitator_error: " + err.message };
372
268
  }
373
269
  }
374
270
 
@@ -378,7 +274,6 @@ function _logPayment(entry) {
378
274
  let payments = [];
379
275
  try { payments = JSON.parse(fs.readFileSync(logPath, "utf-8")); } catch {}
380
276
  payments.push(entry);
381
- // Keep last 1000 payments
382
277
  if (payments.length > 1000) payments = payments.slice(-1000);
383
278
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
384
279
  fs.writeFileSync(logPath, JSON.stringify(payments, null, 2));