wolverine-ai 5.2.2 → 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.2",
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,76 +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 — verify and settle on-chain
149
- // Try facilitator first (standard x402 SDK clients), then self-settle (wallet-signed payments)
150
- const routeAccepts = [{
132
+ // Payment present — decode, verify via facilitator, then settle
133
+ const paymentRequirements = {
151
134
  scheme: "exact",
152
- price,
153
135
  network: _network,
136
+ amount: usdcAmount,
137
+ asset: USDC_BASE,
154
138
  payTo: _payTo,
155
- }];
139
+ maxTimeoutSeconds: 300,
140
+ };
141
+
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;
150
+ }
151
+
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
+ }
156
161
 
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
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
+ });
171
+ return;
164
172
  }
165
173
 
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() });
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
+ });
173
183
  return;
174
184
  }
175
185
 
176
- reply.code(402).send({ error: "Payment settlement failed", reason: selfResult.reason, facilitatorReason: verified.reason, price, payTo: _payTo });
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() });
177
193
  });
178
194
 
179
195
  // ── Public pricing endpoint ──
@@ -182,28 +198,28 @@ async function x402Plugin(fastify, opts) {
182
198
  network: _network,
183
199
  facilitator: _facilitatorUrl,
184
200
  protocol: "x402",
201
+ x402Version: 2,
185
202
  docs: "https://docs.cdp.coinbase.com/x402/welcome",
186
203
  }));
187
204
  }
188
205
 
189
206
  /**
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
207
+ * Call the x402 facilitator matches the exact format from @x402/core HTTPFacilitatorClient.
195
208
  *
196
- * This is the standard x402 flow — compatible with Bazaar discovery.
209
+ * POST {facilitatorUrl}/verify or /settle
210
+ * Body: { x402Version, paymentPayload, paymentRequirements }
197
211
  */
198
- async function _settleViaFacilitator(paymentSig, routeAccepts) {
212
+ async function _facilitatorCall(endpoint, paymentPayload, paymentRequirements) {
199
213
  try {
200
214
  const https = require("https");
201
215
  const http = require("http");
202
- const url = new (require("url").URL)(_facilitatorUrl + "/verify");
216
+ const url = new (require("url").URL)(_facilitatorUrl + endpoint);
203
217
  const body = JSON.stringify({
204
- paymentSignature: paymentSig,
205
- routeConfig: { accepts: routeAccepts },
218
+ x402Version: paymentPayload.x402Version || 2,
219
+ paymentPayload,
220
+ paymentRequirements,
206
221
  });
222
+
207
223
  return new Promise((resolve) => {
208
224
  const client = url.protocol === "https:" ? https : http;
209
225
  const req = client.request({
@@ -212,161 +228,43 @@ async function _settleViaFacilitator(paymentSig, routeAccepts) {
212
228
  path: url.pathname,
213
229
  method: "POST",
214
230
  headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
215
- timeout: 30000, // settlement can take time (on-chain tx)
231
+ timeout: 30000,
216
232
  }, (res) => {
217
233
  let data = "";
218
234
  res.on("data", (c) => data += c);
219
235
  res.on("end", () => {
220
236
  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
- });
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
+ }
229
247
  } else {
230
- 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 });
231
252
  }
232
253
  } catch {
233
- resolve({ valid: false, reason: "facilitator_invalid_response" });
254
+ resolve({ ok: false, reason: `facilitator_parse_error_${res.statusCode}` });
234
255
  }
235
256
  });
236
257
  });
237
- req.on("error", () => resolve({ valid: false, reason: "facilitator_unavailable" }));
238
- 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" }); });
239
263
  req.write(body);
240
264
  req.end();
241
265
  });
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
266
  } catch (err) {
368
- console.log(` ⚠️ x402 self-settlement failed: ${err.message}`);
369
- return { valid: false, reason: "settlement_failed: " + err.message };
267
+ return { ok: false, reason: "facilitator_error: " + err.message };
370
268
  }
371
269
  }
372
270
 
@@ -376,7 +274,6 @@ function _logPayment(entry) {
376
274
  let payments = [];
377
275
  try { payments = JSON.parse(fs.readFileSync(logPath, "utf-8")); } catch {}
378
276
  payments.push(entry);
379
- // Keep last 1000 payments
380
277
  if (payments.length > 1000) payments = payments.slice(-1000);
381
278
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
382
279
  fs.writeFileSync(logPath, JSON.stringify(payments, null, 2));