@m2msentinel/sdk 1.1.2 → 1.2.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
@@ -40,13 +40,13 @@ Autonomous agents handling value must never rely on a single oracle or heuristic
40
40
  npm install @m2msentinel/sdk
41
41
 
42
42
  # Or unscoped package
43
- npm install m2m-sentinel-sdk@1.1.2
43
+ npm install m2m-sentinel-sdk@1.2.0
44
44
  ```
45
45
 
46
46
  ### Python (PyPI)
47
47
 
48
48
  ```bash
49
- pip install m2m-sentinel==1.1.2
49
+ pip install m2m-sentinel==1.2.0
50
50
  ```
51
51
 
52
52
  ### MCP Server (Model Context Protocol)
package/agent_adapter.js CHANGED
@@ -67,7 +67,7 @@ class M2MSentinelActionProvider {
67
67
 
68
68
  const headers = {
69
69
  Accept: 'application/json',
70
- 'User-Agent': 'M2MSentinel-AgentKit/1.1.2',
70
+ 'User-Agent': 'M2MSentinel-AgentKit/1.2.0',
71
71
  ...options.headers
72
72
  };
73
73
  if (this.apiKey && !headers['x-api-key']) {
package/mcp_server.js CHANGED
@@ -8,7 +8,7 @@ const readline = require('readline');
8
8
  let BASE_URL = process.env.M2M_SENTINEL_BASE_URL || 'https://api.m2msentinel.com';
9
9
  let API_KEY = process.env.M2M_SENTINEL_API_KEY || '';
10
10
  const TIMEOUT_MS = Number(process.env.M2M_SENTINEL_TIMEOUT_MS || 30000);
11
- const VERSION = '1.1.2';
11
+ const VERSION = '1.2.0';
12
12
 
13
13
  const TOOLS = [
14
14
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m2msentinel/sdk",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "mcpName": "io.github.m2m-sentinel/m2m-sentinel-sdk",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m2m-sentinel-sdk",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Source-distributed TypeScript client for M2M Sentinel Base capability intelligence API",
5
5
  "main": "index.ts",
6
6
  "types": "index.ts",
@@ -23,11 +23,40 @@ export interface X402PaymentChallenge {
23
23
  recipient?: string;
24
24
  amountUnits?: string;
25
25
  maxAmountRequired?: string;
26
+ /** x402 v2 nests the offer here. Its presence selects the v2 envelope. */
27
+ accepts?: any[];
28
+ resource?: any;
29
+ asset?: string;
30
+ network?: string;
31
+ extra?: { name?: string; version?: string };
32
+ maxTimeoutSeconds?: number;
26
33
  amount?: string;
27
34
  price?: string;
28
35
  [key: string]: any;
29
36
  }
30
37
 
38
+ /** The canonical x402 v2 payment payload. */
39
+ export interface X402SignedAuthorizationV2 {
40
+ x402Version: 2;
41
+ resource?: any;
42
+ /** The chosen offer echoed back verbatim. The server deep-equality matches
43
+ * this against its own requirements to learn which offer is being paid. */
44
+ accepted: any;
45
+ payload: {
46
+ authorization: {
47
+ from: string;
48
+ to: string;
49
+ value: string;
50
+ validAfter: string;
51
+ validBefore: string;
52
+ nonce: string;
53
+ };
54
+ signature: string;
55
+ };
56
+ }
57
+
58
+ export type X402SignedPayload = X402SignedAuthorization | X402SignedAuthorizationV2;
59
+
31
60
  export interface X402SignedAuthorization {
32
61
  x402Version: number;
33
62
  scheme: string;
@@ -91,7 +120,7 @@ export class X402SignerClient {
91
120
  this.maxAmountUnits = parsePriceToUnits(this.maxPriceUsd, 6);
92
121
  }
93
122
 
94
- async signPaymentAuthorization(challenge: X402PaymentChallenge = {}): Promise<X402SignedAuthorization> {
123
+ async signPaymentAuthorization(challenge: X402PaymentChallenge = {}): Promise<X402SignedPayload> {
95
124
  if (!this.wallet) {
96
125
  throw new Error('Signer wallet is required to sign x402 payment authorization');
97
126
  }
@@ -101,33 +130,67 @@ export class X402SignerClient {
101
130
  const tokenContract = BASE_USDC_CONTRACT; // Base USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
102
131
  const payTo = EXPECTED_PAYOUT_RECIPIENT; // M2M Sentinel Payout (0x6d6c398390cfb88f1cd42715b84906a0bd6652aa)
103
132
 
133
+ // x402 v2 nests the offer under `accepts` and is identified by its presence.
134
+ // Reading only the flat shape made every guard below miss silently: they
135
+ // compared undefined and passed, and the amount fell back to a hardcoded
136
+ // default, signing for less than the server demanded.
137
+ const isV2 = Array.isArray(challenge.accepts) && challenge.accepts.length > 0;
138
+ const offer: any = isV2 ? challenge.accepts![0] : challenge;
139
+
140
+ const offeredChainId = typeof offer.network === 'string' && offer.network.startsWith('eip155:')
141
+ ? Number(offer.network.slice('eip155:'.length))
142
+ : (offer.chainId || challenge.chainId);
143
+ // v2 carries the address in `asset`; the flat shape carries a symbol there
144
+ // and the address in `assetContract`. Compare addresses to addresses only.
145
+ const offeredAsset = [offer.assetContract, challenge.assetContract, offer.asset, challenge.asset]
146
+ .find((value: any) => typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) || null;
147
+ const offeredPayTo = offer.payTo || offer.recipient || challenge.payTo;
148
+ const offeredTokenName = (offer.extra && offer.extra.name) || challenge.tokenName;
149
+ const offeredTokenVersion = (offer.extra && offer.extra.version) || challenge.tokenVersion;
150
+
104
151
  // 2. STRICT CHALLENGE INTEGRITY CHECKS (Refuse if challenge alters network, asset, or recipient)
105
- if (challenge.chainId && Number(challenge.chainId) !== BASE_CHAIN_ID) {
106
- throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${challenge.chainId}. Autonomous signer strictly requires Base Mainnet (8453).`);
152
+ if (offeredChainId && Number(offeredChainId) !== BASE_CHAIN_ID) {
153
+ throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${offeredChainId}. Autonomous signer strictly requires Base Mainnet (8453).`);
107
154
  }
108
- if (challenge.assetContract && challenge.assetContract.toLowerCase() !== BASE_USDC_CONTRACT.toLowerCase()) {
109
- throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${challenge.assetContract}. Autonomous signer strictly requires Base USDC (${BASE_USDC_CONTRACT}).`);
155
+ if (offeredAsset && offeredAsset.toLowerCase() !== BASE_USDC_CONTRACT.toLowerCase()) {
156
+ throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${offeredAsset}. Autonomous signer strictly requires Base USDC (${BASE_USDC_CONTRACT}).`);
110
157
  }
111
- if (challenge.payTo && challenge.payTo.toLowerCase() !== EXPECTED_PAYOUT_RECIPIENT.toLowerCase()) {
112
- throw new Error(`[x402 Security Policy] Refusing to sign for unexpected recipient: ${challenge.payTo}. Autonomous signer strictly requires ${EXPECTED_PAYOUT_RECIPIENT}.`);
158
+ if (offeredPayTo && offeredPayTo.toLowerCase() !== EXPECTED_PAYOUT_RECIPIENT.toLowerCase()) {
159
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected recipient: ${offeredPayTo}. Autonomous signer strictly requires ${EXPECTED_PAYOUT_RECIPIENT}.`);
113
160
  }
114
- if (challenge.tokenName && challenge.tokenName !== 'USD Coin') {
115
- throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${challenge.tokenName}. Expected USD Coin.`);
161
+ if (offeredTokenName && offeredTokenName !== 'USD Coin') {
162
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${offeredTokenName}. Expected USD Coin.`);
116
163
  }
117
- if (challenge.tokenVersion && challenge.tokenVersion !== '2') {
118
- throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${challenge.tokenVersion}. Expected 2.`);
164
+ if (offeredTokenVersion && offeredTokenVersion !== '2') {
165
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${offeredTokenVersion}. Expected 2.`);
119
166
  }
120
167
 
121
168
  // 3. STRICT LOCAL PRICE CEILING CHECK
122
- const requestedAmountUnits = challenge.maxAmountRequired || challenge.amountUnits || parsePriceToUnits(challenge.amount || challenge.price || '$0.005', 6).toString();
169
+ // Never invent a price. A guessed amount produces an authorization the
170
+ // server refuses, which is indistinguishable from a rejected payment.
171
+ const rawAmount = offer.amount ?? offer.maxAmountRequired ??
172
+ challenge.maxAmountRequired ?? challenge.amountUnits ?? challenge.amount ?? challenge.price;
173
+ let requestedAmountUnits: string | null = null;
174
+ if (typeof rawAmount === 'number' && Number.isInteger(rawAmount) && rawAmount > 0) {
175
+ requestedAmountUnits = String(rawAmount);
176
+ } else if (typeof rawAmount === 'string' && /^[0-9]+$/.test(rawAmount.trim())) {
177
+ requestedAmountUnits = rawAmount.trim();
178
+ } else if (typeof rawAmount === 'string' && /[$.]/.test(rawAmount)) {
179
+ requestedAmountUnits = parsePriceToUnits(rawAmount, 6).toString();
180
+ }
181
+ if (!requestedAmountUnits || !/^[0-9]+$/.test(requestedAmountUnits) || BigInt(requestedAmountUnits) <= BigInt(0)) {
182
+ throw new Error('[x402] The challenge carried no readable amount. Refusing to guess a price.');
183
+ }
123
184
  if (BigInt(requestedAmountUnits) > this.maxAmountUnits) {
124
185
  throw new Error(`[x402 Security Policy] Requested amount (${requestedAmountUnits} units) exceeds local client authorized price ceiling (${this.maxAmountUnits.toString()} units / $${this.maxPriceUsd}).`);
125
186
  }
126
187
  const amountUnits = requestedAmountUnits;
127
188
 
128
189
  const now = Math.floor(Date.now() / 1000);
129
- const validAfter = now - 60;
130
- const validBefore = now + 3600;
190
+ // v2 servers bound the window with `maxTimeoutSeconds`; a longer window
191
+ // than advertised is not more permissive, just outside what settles.
192
+ const validAfter: any = isV2 ? '0' : now - 60;
193
+ const validBefore: any = isV2 ? String(now + (Number(offer.maxTimeoutSeconds) || 120)) : now + 3600;
131
194
 
132
195
  // Generate 32-byte hex nonce
133
196
  let nonce = '0x';
@@ -199,6 +262,30 @@ export class X402SignerClient {
199
262
  ? signature
200
263
  : '0x' + r.slice(2) + s.slice(2) + v.toString(16).padStart(2, '0');
201
264
 
265
+ if (isV2) {
266
+ // The canonical v2 envelope. Omitting `accepted` does not read as "no
267
+ // payment": it crashes the server's offer matcher, which then answers 402
268
+ // with an opaque body. There is deliberately no top-level scheme/network
269
+ // in v2 -- both live inside `accepted`.
270
+ return {
271
+ x402Version: 2,
272
+ resource: challenge.resource,
273
+ accepted: offer,
274
+ payload: {
275
+ authorization: {
276
+ from: fromAddress,
277
+ to: payTo,
278
+ value: amountUnits.toString(),
279
+ validAfter: String(validAfter),
280
+ validBefore: String(validBefore),
281
+ nonce
282
+ },
283
+ signature: flatSignature
284
+ }
285
+ };
286
+ }
287
+
288
+ // Legacy flat envelope, for servers that do not advertise `accepts`.
202
289
  return {
203
290
  x402Version: 2,
204
291
  scheme: 'eip3009',
@@ -222,7 +309,7 @@ export class X402SignerClient {
222
309
 
223
310
  async fetchWithAutoPayment(path: string, options: any = {}): Promise<any> {
224
311
  const url = path.startsWith('http') ? path : `${this.baseUrl}/${path.replace(/^\/+/, '')}`;
225
- const headers = { 'Accept': 'application/json', 'User-Agent': '@m2msentinel/sdk-ts/1.1.2', ...(options.headers || {}) };
312
+ const headers = { 'Accept': 'application/json', 'User-Agent': '@m2msentinel/sdk-ts/1.2.0', ...(options.headers || {}) };
226
313
 
227
314
  let res = await fetch(url, { method: options.method || 'GET', headers, body: options.body });
228
315
  if (res.status !== 402) {
@@ -235,7 +322,9 @@ export class X402SignerClient {
235
322
  if (!challenge) {
236
323
  try {
237
324
  const bodyJson = await res.clone().json();
238
- challenge = bodyJson.accepts?.[0] || bodyJson.paymentRequired;
325
+ // Pass the whole PaymentRequired, not just accepts[0]: the signer
326
+ // needs `resource` and must echo the offer back as `accepted`.
327
+ challenge = bodyJson.accepts ? bodyJson : bodyJson.paymentRequired;
239
328
  } catch {
240
329
  // Body was not JSON
241
330
  }
package/x402_signer.js CHANGED
@@ -61,33 +61,69 @@ class X402SignerClient {
61
61
  const tokenContract = BASE_USDC_CONTRACT; // Base USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
62
62
  const payTo = EXPECTED_PAYOUT_RECIPIENT; // M2M Sentinel Payout (0x6d6c398390cfb88f1cd42715b84906a0bd6652aa)
63
63
 
64
+ // x402 v2 nests the offer under `accepts` and is identified by its presence.
65
+ // Reading only the flat shape made every check below miss silently: the
66
+ // guards passed vacuously because the fields they read were undefined, and
67
+ // the amount fell back to a hardcoded default, signing for less than the
68
+ // server demanded. Read the offer first, keep the flat shape as fallback.
69
+ const isV2 = Array.isArray(challenge.accepts) && challenge.accepts.length > 0;
70
+ const offer = isV2 ? challenge.accepts[0] : challenge;
71
+
72
+ const offeredChainId = typeof offer.network === 'string' && offer.network.startsWith('eip155:')
73
+ ? Number(offer.network.slice('eip155:'.length))
74
+ : (offer.chainId || challenge.chainId);
75
+ // v2 carries the address in `asset`; the flat shape carries a symbol there
76
+ // and the address in `assetContract`. Compare addresses to addresses only.
77
+ const offeredAsset = [offer.assetContract, challenge.assetContract, offer.asset, challenge.asset]
78
+ .find((value) => typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value)) || null;
79
+ const offeredPayTo = offer.payTo || offer.recipient || challenge.payTo;
80
+ const offeredTokenName = (offer.extra && offer.extra.name) || challenge.tokenName;
81
+ const offeredTokenVersion = (offer.extra && offer.extra.version) || challenge.tokenVersion;
82
+
64
83
  // 2. STRICT CHALLENGE INTEGRITY CHECKS (Refuse if challenge alters network, asset, or recipient)
65
- if (challenge.chainId && Number(challenge.chainId) !== BASE_CHAIN_ID) {
66
- throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${challenge.chainId}. Autonomous signer strictly requires Base Mainnet (8453).`);
84
+ if (offeredChainId && Number(offeredChainId) !== BASE_CHAIN_ID) {
85
+ throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${offeredChainId}. Autonomous signer strictly requires Base Mainnet (8453).`);
67
86
  }
68
- if (challenge.assetContract && challenge.assetContract.toLowerCase() !== BASE_USDC_CONTRACT.toLowerCase()) {
69
- throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${challenge.assetContract}. Autonomous signer strictly requires Base USDC (${BASE_USDC_CONTRACT}).`);
87
+ if (offeredAsset && offeredAsset.toLowerCase() !== BASE_USDC_CONTRACT.toLowerCase()) {
88
+ throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${offeredAsset}. Autonomous signer strictly requires Base USDC (${BASE_USDC_CONTRACT}).`);
70
89
  }
71
- if (challenge.payTo && challenge.payTo.toLowerCase() !== EXPECTED_PAYOUT_RECIPIENT.toLowerCase()) {
72
- throw new Error(`[x402 Security Policy] Refusing to sign for unexpected recipient: ${challenge.payTo}. Autonomous signer strictly requires ${EXPECTED_PAYOUT_RECIPIENT}.`);
90
+ if (offeredPayTo && offeredPayTo.toLowerCase() !== EXPECTED_PAYOUT_RECIPIENT.toLowerCase()) {
91
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected recipient: ${offeredPayTo}. Autonomous signer strictly requires ${EXPECTED_PAYOUT_RECIPIENT}.`);
73
92
  }
74
- if (challenge.tokenName && challenge.tokenName !== EIP712_TOKEN_NAME) {
75
- throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${challenge.tokenName}. Expected ${EIP712_TOKEN_NAME}.`);
93
+ if (offeredTokenName && offeredTokenName !== EIP712_TOKEN_NAME) {
94
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${offeredTokenName}. Expected ${EIP712_TOKEN_NAME}.`);
76
95
  }
77
- if (challenge.tokenVersion && challenge.tokenVersion !== EIP712_TOKEN_VERSION) {
78
- throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${challenge.tokenVersion}. Expected ${EIP712_TOKEN_VERSION}.`);
96
+ if (offeredTokenVersion && offeredTokenVersion !== EIP712_TOKEN_VERSION) {
97
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${offeredTokenVersion}. Expected ${EIP712_TOKEN_VERSION}.`);
79
98
  }
80
99
 
81
100
  // 3. STRICT LOCAL PRICE CEILING CHECK
82
- const requestedAmountUnits = challenge.maxAmountRequired || challenge.amountUnits || parsePriceToUnits(challenge.amount || challenge.price || '$0.005', 6).toString();
101
+ // Never invent a price. A guessed amount produces an authorization the
102
+ // server refuses, which is indistinguishable from a rejected payment.
103
+ // v2 states base units ("20000"); the flat shape states a price ("$0.005").
104
+ const rawAmount = offer.amount ?? offer.maxAmountRequired ??
105
+ challenge.maxAmountRequired ?? challenge.amountUnits ?? challenge.amount ?? challenge.price;
106
+ let requestedAmountUnits = null;
107
+ if (typeof rawAmount === 'number' && Number.isInteger(rawAmount) && rawAmount > 0) {
108
+ requestedAmountUnits = String(rawAmount);
109
+ } else if (typeof rawAmount === 'string' && /^\d+$/.test(rawAmount.trim())) {
110
+ requestedAmountUnits = rawAmount.trim();
111
+ } else if (typeof rawAmount === 'string' && /[$.]/.test(rawAmount)) {
112
+ requestedAmountUnits = parsePriceToUnits(rawAmount, 6).toString();
113
+ }
114
+ if (!requestedAmountUnits || !/^\d+$/.test(requestedAmountUnits) || BigInt(requestedAmountUnits) <= 0n) {
115
+ throw new Error('[x402] The challenge carried no readable amount. Refusing to guess a price.');
116
+ }
83
117
  if (BigInt(requestedAmountUnits) > this.maxAmountUnits) {
84
118
  throw new Error(`[x402 Security Policy] Requested amount (${requestedAmountUnits} units) exceeds local client authorized price ceiling (${this.maxAmountUnits.toString()} units / $${this.maxPriceUsd}).`);
85
119
  }
86
120
  const amountUnits = requestedAmountUnits;
87
121
 
88
122
  const now = Math.floor(Date.now() / 1000);
89
- const validAfter = now - 60;
90
- const validBefore = now + 3600;
123
+ // v2 servers bound the window with `maxTimeoutSeconds`; a longer window
124
+ // than advertised is not more permissive, just outside what settles.
125
+ const validAfter = isV2 ? '0' : now - 60;
126
+ const validBefore = isV2 ? String(now + (Number(offer.maxTimeoutSeconds) || 120)) : now + 3600;
91
127
  const nonce = '0x' + crypto.randomBytes(32).toString('hex');
92
128
 
93
129
  const domain = {
@@ -141,6 +177,33 @@ class X402SignerClient {
141
177
  v = signature.v;
142
178
  }
143
179
 
180
+ if (isV2) {
181
+ // The canonical v2 envelope. `accepted` is the offer echoed back verbatim:
182
+ // the server deep-equality matches it against its own requirements to learn
183
+ // which offer is being paid. Omitting it does not read as "no payment" --
184
+ // it crashes the server's matcher, which then answers 402 with an opaque
185
+ // body. Reconstructing the offer fails the same way, so it is passed
186
+ // through untouched. Note there is no top-level scheme/network in v2:
187
+ // both live inside `accepted`.
188
+ return {
189
+ x402Version: 2,
190
+ resource: challenge.resource,
191
+ accepted: offer,
192
+ payload: {
193
+ authorization: {
194
+ from: message.from,
195
+ to: message.to,
196
+ value: message.value,
197
+ validAfter: message.validAfter,
198
+ validBefore: message.validBefore,
199
+ nonce: message.nonce
200
+ },
201
+ signature: typeof signature === 'string' ? signature : `0x${r.slice(2)}${s.slice(2)}${v.toString(16)}`
202
+ }
203
+ };
204
+ }
205
+
206
+ // Legacy flat envelope, for servers that do not advertise `accepts`.
144
207
  return {
145
208
  x402Version: 2,
146
209
  scheme: 'eip3009',
@@ -202,7 +265,7 @@ class X402SignerClient {
202
265
  path: url.pathname + url.search,
203
266
  method: options.method || 'GET',
204
267
  headers: {
205
- 'User-Agent': 'M2M-Sentinel-X402Signer/1.1.2',
268
+ 'User-Agent': 'M2M-Sentinel-X402Signer/1.2.0',
206
269
  'Accept': 'application/json',
207
270
  ...(options.headers || {})
208
271
  },