@alleyboss/micropay-solana-x402-paywall 2.1.2 → 2.3.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/README.md CHANGED
@@ -19,7 +19,7 @@ npm install @alleyboss/micropay-solana-x402-paywall @solana/web3.js
19
19
  | Feature | Description |
20
20
  |---------|-------------|
21
21
  | 💰 **SOL & USDC Payments** | Native SOL and SPL tokens (USDC, USDT) |
22
- | 🔐 **x402 Protocol** | HTTP 402 Payment Required standard |
22
+ | 🔐 **x402 Protocol** | Full HTTP 402 compliance with `X-Payment-Required` headers |
23
23
  | 🔑 **JWT Sessions** | Secure unlock tracking with anti-replay |
24
24
  | 🛡️ **Signature Store** | Prevent double-spend at app layer |
25
25
  | 🔌 **Express & Next.js** | Zero-boilerplate middleware |
@@ -82,27 +82,42 @@ import { getSolPrice, formatPriceDisplay, configurePricing } from '@alleyboss/mi
82
82
  import { withRetry } from '@alleyboss/micropay-solana-x402-paywall/utils';
83
83
  ```
84
84
 
85
- ## 🔥 New in v2.1.0
85
+ ## 🔥 New in v2.2.0
86
86
 
87
- - **RPC Fallback Support** — Automatic failover on primary RPC failure (configurable, default: off)
88
- - **Priority Fees** — Compute budget instructions for landing transactions faster (configurable, default: off)
89
- - **Versioned Transactions** — Full v0 transaction support with lookup tables
90
- - **TDD Test Suite** — Comprehensive tests with vitest (must pass before npm publish)
87
+ ### x402 Protocol Compliance
88
+
89
+ Full compliance with the [x402.org](https://x402.org) specification:
91
90
 
92
91
  ```typescript
93
- // RPC Fallback configuration
94
- const config = {
95
- network: 'mainnet-beta',
96
- rpcUrl: 'https://primary-rpc.com',
97
- enableFallback: true, // default: false
98
- fallbackRpcUrls: [
99
- 'https://fallback1.com',
100
- 'https://fallback2.com',
101
- ],
102
- };
92
+ import { create402Response, parsePaymentHeader, encodePaymentRequirement } from '@alleyboss/micropay-solana-x402-paywall/x402';
93
+
94
+ // Create 402 response with X-Payment-Required header
95
+ const response = create402Response({
96
+ scheme: 'exact',
97
+ network: 'solana-mainnet',
98
+ maxAmountRequired: '10000000',
99
+ payTo: 'CreatorWallet...',
100
+ resource: '/api/premium',
101
+ description: 'Premium content access',
102
+ maxTimeoutSeconds: 300,
103
+ asset: 'native',
104
+ });
105
+ // Response includes: X-Payment-Required: <base64-encoded-requirement>
103
106
 
107
+ // Parse X-Payment header from client
108
+ const payload = parsePaymentHeader(request.headers.get('x-payment'));
109
+ ```
110
+
111
+ ### Previous Features (v2.1.x)
112
+
113
+ - **RPC Fallback Support** — Automatic failover on primary RPC failure
114
+ - **Priority Fees** — Compute budget instructions for landing transactions faster
115
+ - **Versioned Transactions** — Full v0 transaction support with lookup tables
116
+ - **TDD Test Suite** — Comprehensive tests with vitest
117
+
118
+ ```typescript
104
119
  // Priority fees
105
- import { createPriorityFeeInstructions, estimatePriorityFee } from '@alleyboss/micropay-solana-x402-paywall/solana';
120
+ import { createPriorityFeeInstructions } from '@alleyboss/micropay-solana-x402-paywall/solana';
106
121
 
107
122
  const instructions = createPriorityFeeInstructions({
108
123
  enabled: true,
package/dist/index.cjs CHANGED
@@ -148,7 +148,8 @@ async function verifyPayment(params) {
148
148
  expectedRecipient,
149
149
  expectedAmount,
150
150
  maxAgeSeconds = 300,
151
- clientConfig
151
+ clientConfig,
152
+ signatureStore
152
153
  } = params;
153
154
  if (!isValidSignature(signature)) {
154
155
  return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
@@ -159,6 +160,12 @@ async function verifyPayment(params) {
159
160
  if (expectedAmount <= 0n) {
160
161
  return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
161
162
  }
163
+ if (signatureStore) {
164
+ const isUsed = await signatureStore.hasBeenUsed(signature);
165
+ if (isUsed) {
166
+ return { valid: false, confirmed: true, signature, error: "Signature already used" };
167
+ }
168
+ }
162
169
  const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
163
170
  const connection = getConnection(clientConfig);
164
171
  try {
@@ -267,8 +274,6 @@ function solToLamports(sol) {
267
274
  }
268
275
  return BigInt(Math.floor(sol * web3_js.LAMPORTS_PER_SOL));
269
276
  }
270
-
271
- // src/solana/spl.ts
272
277
  var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
273
278
  var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
274
279
  function resolveMintAddress(asset, network) {
@@ -362,8 +367,15 @@ async function verifySPLPayment(params) {
362
367
  expectedAmount,
363
368
  asset,
364
369
  clientConfig,
365
- maxAgeSeconds = 300
370
+ maxAgeSeconds = 300,
371
+ signatureStore
366
372
  } = params;
373
+ if (signatureStore) {
374
+ const isUsed = await signatureStore.hasBeenUsed(signature);
375
+ if (isUsed) {
376
+ return { valid: false, confirmed: true, signature, error: "Signature already used" };
377
+ }
378
+ }
367
379
  if (!SIGNATURE_REGEX2.test(signature)) {
368
380
  return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
369
381
  }
@@ -408,6 +420,27 @@ async function verifySPLPayment(params) {
408
420
  error: "No valid token transfer to recipient found"
409
421
  };
410
422
  }
423
+ if (transfer.to) {
424
+ try {
425
+ const destinationInfo = await connection.getParsedAccountInfo(new web3_js.PublicKey(transfer.to));
426
+ const owner = destinationInfo.value?.data?.parsed?.info?.owner;
427
+ if (owner && owner !== expectedRecipient) {
428
+ return {
429
+ valid: false,
430
+ confirmed: true,
431
+ signature,
432
+ error: "Recipient mismatch: Token account not owned by merchant"
433
+ };
434
+ }
435
+ } catch (e) {
436
+ return {
437
+ valid: false,
438
+ confirmed: true,
439
+ signature,
440
+ error: "Could not verify token account owner"
441
+ };
442
+ }
443
+ }
411
444
  if (transfer.mint !== mintAddress) {
412
445
  return {
413
446
  valid: false,
@@ -829,9 +862,28 @@ function parsePaymentHeader(header) {
829
862
  return null;
830
863
  }
831
864
  }
865
+ function encodePaymentRequirement(requirement) {
866
+ return Buffer.from(JSON.stringify(requirement)).toString("base64");
867
+ }
832
868
  function encodePaymentResponse(response) {
833
869
  return Buffer.from(JSON.stringify(response)).toString("base64");
834
870
  }
871
+ function create402Response(requirement, body) {
872
+ const headers = new Headers({
873
+ "Content-Type": "application/json",
874
+ "X-Payment-Required": encodePaymentRequirement(requirement)
875
+ });
876
+ const responseBody = body || {
877
+ error: "Payment Required",
878
+ message: "This resource requires payment to access",
879
+ x402Version: 1,
880
+ accepts: [requirement]
881
+ };
882
+ return new Response(JSON.stringify(responseBody), {
883
+ status: 402,
884
+ headers
885
+ });
886
+ }
835
887
 
836
888
  // src/store/memory.ts
837
889
  function createMemoryStore(options = {}) {
@@ -976,16 +1028,22 @@ function createPaywallMiddleware(config2) {
976
1028
  const sessionToken = cookies[cookieName];
977
1029
  const result = await checkPaywallAccess(path, sessionToken, config2);
978
1030
  if (!result.allowed && result.requiresPayment) {
1031
+ const headers = {
1032
+ "Content-Type": "application/json"
1033
+ };
1034
+ if (config2.paymentRequirement) {
1035
+ const requirement = typeof config2.paymentRequirement === "function" ? config2.paymentRequirement(path) : config2.paymentRequirement;
1036
+ headers["X-Payment-Required"] = encodePaymentRequirement(requirement);
1037
+ }
979
1038
  const body = config2.custom402Response ? config2.custom402Response(path) : {
980
1039
  error: "Payment Required",
981
1040
  message: "This resource requires payment to access",
1041
+ x402Version: 1,
982
1042
  path
983
1043
  };
984
1044
  return new Response(JSON.stringify(body), {
985
1045
  status: 402,
986
- headers: {
987
- "Content-Type": "application/json"
988
- }
1046
+ headers
989
1047
  });
990
1048
  }
991
1049
  return null;
@@ -1261,14 +1319,14 @@ async function getSolPrice() {
1261
1319
  }
1262
1320
  }
1263
1321
  if (cachedPrice) {
1264
- return cachedPrice;
1322
+ return {
1323
+ ...cachedPrice,
1324
+ source: `${cachedPrice.source} (stale)`
1325
+ };
1265
1326
  }
1266
- return {
1267
- solPrice: 150,
1268
- // Reasonable fallback
1269
- fetchedAt: /* @__PURE__ */ new Date(),
1270
- source: "fallback"
1271
- };
1327
+ throw new Error(
1328
+ "Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
1329
+ );
1272
1330
  }
1273
1331
  async function lamportsToUsd(lamports) {
1274
1332
  const { solPrice } = await getSolPrice();
@@ -1314,6 +1372,7 @@ exports.checkPaywallAccess = checkPaywallAccess;
1314
1372
  exports.clearPriceCache = clearPriceCache;
1315
1373
  exports.configurePricing = configurePricing;
1316
1374
  exports.create402Headers = create402Headers;
1375
+ exports.create402Response = create402Response;
1317
1376
  exports.create402ResponseBody = create402ResponseBody;
1318
1377
  exports.createMemoryStore = createMemoryStore;
1319
1378
  exports.createPaymentFlow = createPaymentFlow;
@@ -1324,6 +1383,7 @@ exports.createRedisStore = createRedisStore;
1324
1383
  exports.createSession = createSession;
1325
1384
  exports.decodePaymentRequired = decodePaymentRequired;
1326
1385
  exports.encodePaymentRequired = encodePaymentRequired;
1386
+ exports.encodePaymentRequirement = encodePaymentRequirement;
1327
1387
  exports.encodePaymentResponse = encodePaymentResponse;
1328
1388
  exports.estimatePriorityFee = estimatePriorityFee;
1329
1389
  exports.fetchLookupTables = fetchLookupTables;