@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 +2 -2
- package/agent_adapter.js +1 -1
- package/mcp_server.js +1 -1
- package/package.json +1 -1
- package/typescript/package.json +1 -1
- package/typescript/x402.ts +105 -16
- package/x402_signer.js +77 -14
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.
|
|
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.
|
|
49
|
+
pip install m2m-sentinel==1.2.0
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
### MCP Server (Model Context Protocol)
|
package/agent_adapter.js
CHANGED
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.
|
|
11
|
+
const VERSION = '1.2.0';
|
|
12
12
|
|
|
13
13
|
const TOOLS = [
|
|
14
14
|
{
|
package/package.json
CHANGED
package/typescript/package.json
CHANGED
package/typescript/x402.ts
CHANGED
|
@@ -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<
|
|
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 (
|
|
106
|
-
throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${
|
|
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 (
|
|
109
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${
|
|
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 (
|
|
112
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unexpected 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 (
|
|
115
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${
|
|
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 (
|
|
118
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
66
|
-
throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${
|
|
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 (
|
|
69
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${
|
|
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 (
|
|
72
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unexpected 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 (
|
|
75
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${
|
|
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 (
|
|
78
|
-
throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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.
|
|
268
|
+
'User-Agent': 'M2M-Sentinel-X402Signer/1.2.0',
|
|
206
269
|
'Accept': 'application/json',
|
|
207
270
|
...(options.headers || {})
|
|
208
271
|
},
|