@m2msentinel/sdk 1.1.1
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 +57 -0
- package/agent_adapter.js +248 -0
- package/eliza_plugin.js +84 -0
- package/index.d.ts +94 -0
- package/index.js +187 -0
- package/m2m_sentinel_sdk.js +1 -0
- package/mcp_server.js +223 -0
- package/package.json +53 -0
- package/smithery.yaml +27 -0
- package/typescript/index.ts +204 -0
- package/typescript/package.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# M2M Sentinel SDKs
|
|
2
|
+
|
|
3
|
+
Production API Base URL: `https://api.m2msentinel.com` (with fallback `https://m2msentinel.com`).
|
|
4
|
+
|
|
5
|
+
M2M Sentinel reports selected static EVM bytecode capabilities, common proxy structures, evidence quality, and sourced Base market observations. Factual static capability observation — not a formal reachability audit, safety guarantee, or transaction advice. Transaction middleware therefore requires a caller-defined policy and has no built-in allow threshold.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### JavaScript / TypeScript (npm)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install m2m-sentinel-sdk@1.1.1
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Python (PyPI)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install m2m-sentinel==1.1.0
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### MCP Server (Model Context Protocol)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx -y m2m-sentinel-sdk
|
|
25
|
+
# or
|
|
26
|
+
npx -y m2m-sentinel-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Authentication and x402
|
|
30
|
+
|
|
31
|
+
Send API keys only in `x-api-key` (or `Authorization: Bearer`). Query-string credentials are rejected with HTTP 401. Payable routes also support x402 v2 on Base USDC. An unpaid call returns a base64 `PAYMENT-REQUIRED` challenge when the facilitator is ready; a successful settlement returns `PAYMENT-RESPONSE`.
|
|
32
|
+
|
|
33
|
+
## Public Routes
|
|
34
|
+
|
|
35
|
+
- `GET /v1/status`
|
|
36
|
+
- `GET /v1/stats` (public privacy envelope; exact aggregate commercial counters are operator-only)
|
|
37
|
+
- `GET /v1/plans`
|
|
38
|
+
- `GET /v1/demo/audit/:address` for the published sample allowlist
|
|
39
|
+
- `GET /v1/audit/:address` (requires API key or x402 payment)
|
|
40
|
+
|
|
41
|
+
The JavaScript, TypeScript, and Python clients expose multi-period purchase/renewal and wallet recovery without requiring callers to hand-build requests:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
await client.createSubscriptionIntent('GROWTH', wallet, {
|
|
45
|
+
durationDays: 90,
|
|
46
|
+
renewExistingKey: true,
|
|
47
|
+
apiKey: existingPaidKey
|
|
48
|
+
});
|
|
49
|
+
const challenge = await client.createRecoveryChallenge(wallet, { txHash });
|
|
50
|
+
await client.claimRecoveredKey(challenge.intent.id, walletSignature);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`getPublicStats()` returns the counter-free privacy envelope. Operator-only detail has a deliberately separate `getOperatorAggregateStats(days, token)` method and sends the operator token only as `Authorization: Bearer`. This is for private server/operator tooling only: never embed or bundle the operator token in browser, mobile, SDK-distribution, or customer code.
|
|
54
|
+
|
|
55
|
+
## Disclaimer & Limitations
|
|
56
|
+
|
|
57
|
+
M2M Sentinel extracts static EVM opcode capabilities and EIP-1967/UUPS proxy implementation slots on Base Mainnet. Factual static capability observation — not a formal reachability audit, safety guarantee, or transaction advice.
|
package/agent_adapter.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Coinbase AgentKit Action Provider for M2M Sentinel.
|
|
5
|
+
*
|
|
6
|
+
* Exposes pre-transaction contract bytecode capability inspection, proxy resolution,
|
|
7
|
+
* and market observations to autonomous Base agents using @coinbase/agentkit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const https = require('https');
|
|
11
|
+
const http = require('http');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BASE_URL = process.env.M2M_SENTINEL_BASE_URL || 'https://api.m2msentinel.com';
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = Number(process.env.M2M_SENTINEL_TIMEOUT_MS || 30000);
|
|
15
|
+
|
|
16
|
+
class M2MSentinelActionProvider {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.name = 'm2m_sentinel';
|
|
19
|
+
this.actionProviderName = 'm2m_sentinel';
|
|
20
|
+
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL;
|
|
21
|
+
this.apiKey = options.apiKey || process.env.M2M_SENTINEL_API_KEY || '';
|
|
22
|
+
this.timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
supportsNetwork(network) {
|
|
26
|
+
if (!network) return true;
|
|
27
|
+
const chainId = String(network.chainId || network.networkId || '');
|
|
28
|
+
const protocolFamily = String(network.protocolFamily || 'evm').toLowerCase();
|
|
29
|
+
return protocolFamily === 'evm' && (
|
|
30
|
+
chainId === '8453' ||
|
|
31
|
+
chainId === 'base' ||
|
|
32
|
+
chainId === 'base-mainnet' ||
|
|
33
|
+
chainId === 'base-sepolia' ||
|
|
34
|
+
chainId === '84532'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async _queryApi(endpointPath, options = {}) {
|
|
39
|
+
const url = new URL(endpointPath, this.baseUrl);
|
|
40
|
+
const isHttps = url.protocol === 'https:';
|
|
41
|
+
const transport = isHttps ? https : http;
|
|
42
|
+
|
|
43
|
+
const headers = {
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
'User-Agent': 'M2MSentinel-AgentKit/1.1.1',
|
|
46
|
+
...options.headers
|
|
47
|
+
};
|
|
48
|
+
if (this.apiKey && !headers['x-api-key']) {
|
|
49
|
+
headers['x-api-key'] = this.apiKey;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const req = transport.request(url, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
headers,
|
|
56
|
+
timeout: this.timeoutMs
|
|
57
|
+
}, (res) => {
|
|
58
|
+
let data = '';
|
|
59
|
+
res.setEncoding('utf8');
|
|
60
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
61
|
+
res.on('end', () => {
|
|
62
|
+
let body = null;
|
|
63
|
+
if (data) {
|
|
64
|
+
try { body = JSON.parse(data); } catch (_) { body = { raw: data }; }
|
|
65
|
+
}
|
|
66
|
+
resolve({
|
|
67
|
+
statusCode: res.statusCode,
|
|
68
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
69
|
+
body
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
req.on('timeout', () => req.destroy(new Error('M2M Sentinel request timed out')));
|
|
75
|
+
req.on('error', reject);
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async auditContract(args) {
|
|
81
|
+
const address = String(args.address || args.contractAddress || '').trim();
|
|
82
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
status: 'ERROR',
|
|
85
|
+
error: 'INVALID_ADDRESS',
|
|
86
|
+
message: 'A valid 40-hex 0x-prefixed Base contract address is required.'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const res = await this._queryApi(`/v1/audit/${encodeURIComponent(address)}`);
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
status: 'ERROR',
|
|
94
|
+
statusCode: res.statusCode,
|
|
95
|
+
error: res.body && res.body.error ? res.body.error : 'API_ERROR',
|
|
96
|
+
message: res.body && res.body.message ? res.body.message : 'Contract audit query failed',
|
|
97
|
+
notASafetyGuarantee: true
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
status: 'SUCCESS',
|
|
103
|
+
data: res.body,
|
|
104
|
+
notASafetyGuarantee: true,
|
|
105
|
+
observationSummary: `Contract ${address}: Type=${res.body.bytecodeAnalysis?.contractType || 'UNKNOWN'}, isProxy=${Boolean(res.body.proxyDetection?.isProxy)}`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getGasMetrics() {
|
|
110
|
+
const res = await this._queryApi('/v1/gas/fees');
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
return JSON.stringify({ status: 'ERROR', statusCode: res.statusCode, message: 'Failed to retrieve gas fees' });
|
|
113
|
+
}
|
|
114
|
+
return JSON.stringify(res.body);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getTokenPrice(args) {
|
|
118
|
+
const symbol = String(args.symbol || args.tokenSymbol || '').trim().toUpperCase();
|
|
119
|
+
if (!symbol) {
|
|
120
|
+
return JSON.stringify({ status: 'ERROR', message: 'Token symbol is required (e.g. USDC, WETH)' });
|
|
121
|
+
}
|
|
122
|
+
const res = await this._queryApi(`/v1/token/price/${encodeURIComponent(symbol)}`);
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
return JSON.stringify({ status: 'ERROR', statusCode: res.statusCode, message: `Failed to retrieve price for ${symbol}` });
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(res.body);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getServiceStatus() {
|
|
130
|
+
const res = await this._queryApi('/v1/status');
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
return JSON.stringify({ status: 'UNAVAILABLE', statusCode: res.statusCode });
|
|
133
|
+
}
|
|
134
|
+
return JSON.stringify(res.body);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getDexLiquidity(args = {}) {
|
|
138
|
+
const pair = String(args.pair || 'WETH-USDC').trim();
|
|
139
|
+
const res = await this._queryApi(`/v1/dex/metrics?pair=${encodeURIComponent(pair)}`);
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
return JSON.stringify({ status: 'ERROR', statusCode: res.statusCode, message: 'Failed to retrieve DEX liquidity' });
|
|
142
|
+
}
|
|
143
|
+
return JSON.stringify(res.body);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getWhaleSignals(args = {}) {
|
|
147
|
+
const limit = Number(args.limit || 10);
|
|
148
|
+
const res = await this._queryApi(`/v1/whales/signals?limit=${encodeURIComponent(limit)}`);
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
return JSON.stringify({ status: 'ERROR', statusCode: res.statusCode, message: 'Failed to retrieve whale signals' });
|
|
151
|
+
}
|
|
152
|
+
return JSON.stringify(res.body);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getActions(_walletProvider) {
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
name: 'm2m_audit_contract',
|
|
159
|
+
description: 'Inspect Base target contract bytecode capability observations, proxy implementation slots, and limitations before executing transactions. Returns factual evidence, not a safety guarantee.',
|
|
160
|
+
schema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties: {
|
|
163
|
+
address: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
description: 'Target Base contract address (0x-prefixed 40-hex)'
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
required: ['address']
|
|
169
|
+
},
|
|
170
|
+
invoke: (args) => this.auditContract(args)
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'm2m_get_gas_metrics',
|
|
174
|
+
description: 'Get real-time Base network gas execution metrics and recommendations before submitting on-chain transactions.',
|
|
175
|
+
schema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: {}
|
|
178
|
+
},
|
|
179
|
+
invoke: () => this.getGasMetrics()
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'm2m_get_token_price',
|
|
183
|
+
description: 'Observe real-time Base DEX token price for slippage check and valuation.',
|
|
184
|
+
schema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
symbol: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Token symbol on Base (e.g. USDC, WETH)'
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
required: ['symbol']
|
|
193
|
+
},
|
|
194
|
+
invoke: (args) => this.getTokenPrice(args)
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'm2m_get_dex_liquidity',
|
|
198
|
+
description: 'Get tracked Base DEX pool reserve and liquidity metrics.',
|
|
199
|
+
schema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
pair: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'DEX pair identifier (e.g. WETH-USDC)'
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
invoke: (args) => this.getDexLiquidity(args)
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'm2m_get_whale_signals',
|
|
212
|
+
description: 'Get tracked Base whale transfer and concentration signals.',
|
|
213
|
+
schema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
limit: {
|
|
217
|
+
type: 'number',
|
|
218
|
+
description: 'Maximum signals to retrieve (1-50)'
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
invoke: (args) => this.getWhaleSignals(args)
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'm2m_get_service_status',
|
|
226
|
+
description: 'Check operational status of M2M Sentinel upstream verification rails.',
|
|
227
|
+
schema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {}
|
|
230
|
+
},
|
|
231
|
+
invoke: () => this.getServiceStatus()
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function m2mSentinelActionProvider(options = {}) {
|
|
238
|
+
return new M2MSentinelActionProvider(options);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Backward compatibility alias
|
|
242
|
+
class M2MSentinelAgentTool extends M2MSentinelActionProvider {}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
M2MSentinelActionProvider,
|
|
246
|
+
m2mSentinelActionProvider,
|
|
247
|
+
M2MSentinelAgentTool
|
|
248
|
+
};
|
package/eliza_plugin.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// UNSUPPORTED / COMMUNITY PREVIEW — not covered by the supported-SDK guarantee; verify against /openapi.json before production use.
|
|
2
|
+
|
|
3
|
+
const { M2MSentinelClient } = require('./index.js');
|
|
4
|
+
|
|
5
|
+
function clientFor(runtime) {
|
|
6
|
+
const apiKey = runtime?.getSetting?.('M2M_SENTINEL_API_KEY') || process.env.M2M_SENTINEL_API_KEY;
|
|
7
|
+
return new M2MSentinelClient({ apiKey });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function errorText(err) {
|
|
11
|
+
if (err.status === 402) return 'M2M Sentinel payment required. Configure M2M_SENTINEL_API_KEY or settle the x402 challenge.';
|
|
12
|
+
if (err.status === 503 && err.body?.error === 'DATA_SOURCE_UNAVAILABLE') return 'M2M Sentinel data source unavailable; no estimated fallback value is returned.';
|
|
13
|
+
if (err.status === 429) return 'M2M Sentinel rate limited. Retry-After: ' + (err.retryAfter || 'not provided');
|
|
14
|
+
return 'M2M Sentinel API request failed: ' + err.message;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const m2mSentinelPlugin = {
|
|
18
|
+
name: '@elizaos/plugin-m2m-sentinel',
|
|
19
|
+
description: 'Preview ElizaOS actions for M2M Sentinel protected Web3 API routes.',
|
|
20
|
+
actions: [
|
|
21
|
+
{
|
|
22
|
+
name: 'AUDIT_CONTRACT',
|
|
23
|
+
description: 'Reports selected static capability and proxy observations for a Base contract.',
|
|
24
|
+
handler: async (runtime, message) => {
|
|
25
|
+
const address = message.content.text.match(/0x[a-fA-F0-9]{40}/)?.[0];
|
|
26
|
+
if (!address) return { text: 'Please provide a valid 42-character EVM contract address.' };
|
|
27
|
+
try {
|
|
28
|
+
const data = await clientFor(runtime).auditContract(address);
|
|
29
|
+
const audit = data.audit || {};
|
|
30
|
+
const proxy = audit.proxyResolution || {};
|
|
31
|
+
return { text: `M2M Sentinel capability analysis for ${address}: ${audit.capabilityRating || 'UNVERIFIED'}; proxy=${proxy.isProxy ? proxy.proxyType : 'NO'}; notASafetyGuarantee=true; provenance=${JSON.stringify(audit.provenance || data.provenance || null)}` };
|
|
32
|
+
} catch (err) { return { text: errorText(err) }; }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'GET_CAPABILITY_SCORE',
|
|
37
|
+
description: 'Returns the static capability coverage index for a Base contract. The index is not a safety score.',
|
|
38
|
+
handler: async (runtime, message) => {
|
|
39
|
+
const address = message.content.text.match(/0x[a-fA-F0-9]{40}/)?.[0];
|
|
40
|
+
if (!address) return { text: 'Please provide a valid EVM contract address.' };
|
|
41
|
+
try {
|
|
42
|
+
const data = await clientFor(runtime).getCapabilityScore(address);
|
|
43
|
+
const score = data.capabilityScore === null ? 'unverified' : `${data.capabilityScore}/100 capability coverage`;
|
|
44
|
+
return { text: `M2M Sentinel static index for ${address}: ${score}; not a safety score. Provenance: ${JSON.stringify(data.provenance || null)}` };
|
|
45
|
+
} catch (err) { return { text: errorText(err) }; }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'GET_DEX_METRICS',
|
|
50
|
+
description: 'Fetches Base DEX metrics.',
|
|
51
|
+
handler: async (runtime) => {
|
|
52
|
+
try { return { text: JSON.stringify(await clientFor(runtime).getDexMetrics()) }; }
|
|
53
|
+
catch (err) { return { text: errorText(err) }; }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'GET_TOKEN_PRICE',
|
|
58
|
+
description: 'Queries a tracked Base token price.',
|
|
59
|
+
handler: async (runtime, message) => {
|
|
60
|
+
const symbol = (message.content.text.match(/price of ([a-zA-Z0-9]+)/i)?.[1] || 'USDC').toUpperCase();
|
|
61
|
+
try { return { text: JSON.stringify(await clientFor(runtime).getTokenPrice(symbol)) }; }
|
|
62
|
+
catch (err) { return { text: errorText(err) }; }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'GET_GAS_FEES',
|
|
67
|
+
description: 'Fetches live Base gas fees.',
|
|
68
|
+
handler: async (runtime) => {
|
|
69
|
+
try { return { text: JSON.stringify(await clientFor(runtime).getGasFees()) }; }
|
|
70
|
+
catch (err) { return { text: errorText(err) }; }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'GET_WHALE_SIGNALS',
|
|
75
|
+
description: 'Fetches whale signals.',
|
|
76
|
+
handler: async (runtime) => {
|
|
77
|
+
try { return { text: JSON.stringify(await clientFor(runtime).getWhaleSignals()) }; }
|
|
78
|
+
catch (err) { return { text: errorText(err) }; }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
module.exports = { m2mSentinelPlugin };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface M2MSentinelClientOptions {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
paymentSignature?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CreateSubscriptionIntentOptions extends M2MSentinelClientOptions {
|
|
9
|
+
durationDays?: 31 | 90 | 365;
|
|
10
|
+
renewExistingKey?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RecoveryChallengeOptions {
|
|
14
|
+
txHash?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface M2MSentinelErrorOptions {
|
|
18
|
+
status?: number;
|
|
19
|
+
body?: unknown;
|
|
20
|
+
retryAfter?: string | null;
|
|
21
|
+
paymentRequired?: unknown;
|
|
22
|
+
paymentResponse?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class M2MSentinelError extends Error {
|
|
26
|
+
readonly status?: number;
|
|
27
|
+
readonly body?: unknown;
|
|
28
|
+
readonly retryAfter?: string | null;
|
|
29
|
+
readonly paymentRequired?: unknown;
|
|
30
|
+
readonly paymentResponse?: unknown;
|
|
31
|
+
constructor(message: string, options?: M2MSentinelErrorOptions);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class PaymentRequiredError extends M2MSentinelError {}
|
|
35
|
+
export class RateLimitedError extends M2MSentinelError {}
|
|
36
|
+
export class DataSourceUnavailableError extends M2MSentinelError {}
|
|
37
|
+
|
|
38
|
+
export class M2MSentinelClient {
|
|
39
|
+
constructor(options?: M2MSentinelClientOptions);
|
|
40
|
+
constructor(apiKey?: string, baseUrl?: string);
|
|
41
|
+
request(method: string, path: string, body?: unknown, options?: M2MSentinelClientOptions): Promise<any>;
|
|
42
|
+
getStatus(): Promise<any>;
|
|
43
|
+
getPublicStats(days?: number): Promise<any>;
|
|
44
|
+
/** Backward-compatible alias for getPublicStats; customer keys never reveal operator counters. */
|
|
45
|
+
getAggregateStats(days?: number): Promise<any>;
|
|
46
|
+
getOperatorAggregateStats(days: number, operatorToken: string): Promise<any>;
|
|
47
|
+
getPlans(): Promise<any>;
|
|
48
|
+
demoAudit(address: string): Promise<any>;
|
|
49
|
+
createFreeChallenge(userWallet: string): Promise<any>;
|
|
50
|
+
claimFreeTier(intentId: string, signature: string): Promise<any>;
|
|
51
|
+
createSubscriptionIntent(tier: string, userWallet: string, options?: CreateSubscriptionIntentOptions): Promise<any>;
|
|
52
|
+
claimSubscription(intentId: string, signature: string, txHash: string): Promise<any>;
|
|
53
|
+
createRecoveryChallenge(userWallet: string, options?: RecoveryChallengeOptions): Promise<any>;
|
|
54
|
+
claimRecoveredKey(intentId: string, signature: string): Promise<any>;
|
|
55
|
+
auditContract(address: string, options?: M2MSentinelClientOptions): Promise<any>;
|
|
56
|
+
getCapabilityScore(address: string, options?: M2MSentinelClientOptions): Promise<any>;
|
|
57
|
+
/** Legacy alias. The response is a capability coverage index, not a safety score. */
|
|
58
|
+
getSecurityScore(address: string, options?: M2MSentinelClientOptions): Promise<any>;
|
|
59
|
+
getGasFees(options?: M2MSentinelClientOptions): Promise<any>;
|
|
60
|
+
getDexMetrics(options?: M2MSentinelClientOptions): Promise<any>;
|
|
61
|
+
getTokenPrice(symbol: string, options?: M2MSentinelClientOptions): Promise<any>;
|
|
62
|
+
getWhaleSignals(options?: M2MSentinelClientOptions): Promise<any>;
|
|
63
|
+
getKeySelf(): Promise<any>;
|
|
64
|
+
revokeKey(confirm?: boolean): Promise<any>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type SentinelPolicy = (
|
|
68
|
+
analysis: any,
|
|
69
|
+
context: { targetAddress: string; integration: string }
|
|
70
|
+
) => boolean | { allow: boolean; reason?: string } | Promise<boolean | { allow: boolean; reason?: string }>;
|
|
71
|
+
|
|
72
|
+
export function enforceCallerPolicy(
|
|
73
|
+
policy: SentinelPolicy | undefined,
|
|
74
|
+
analysis: any,
|
|
75
|
+
context: { targetAddress: string; integration: string }
|
|
76
|
+
): Promise<void>;
|
|
77
|
+
|
|
78
|
+
export function createEthersSentinelMiddleware(
|
|
79
|
+
apiKey?: string,
|
|
80
|
+
baseUrl?: string,
|
|
81
|
+
policy?: SentinelPolicy
|
|
82
|
+
): {
|
|
83
|
+
client: M2MSentinelClient;
|
|
84
|
+
verifyContractBeforeTx(targetAddress: string, policyOverride?: SentinelPolicy): Promise<any>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function createViemSentinelInterceptor(
|
|
88
|
+
apiKey?: string,
|
|
89
|
+
baseUrl?: string,
|
|
90
|
+
policy?: SentinelPolicy
|
|
91
|
+
): {
|
|
92
|
+
client: M2MSentinelClient;
|
|
93
|
+
inspectSwapTarget(address: string, policyOverride?: SentinelPolicy): Promise<any>;
|
|
94
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://api.m2msentinel.com';
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
3
|
+
|
|
4
|
+
class M2MSentinelError extends Error {
|
|
5
|
+
constructor(message, options = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
this.status = options.status;
|
|
9
|
+
this.body = options.body;
|
|
10
|
+
this.retryAfter = options.retryAfter || null;
|
|
11
|
+
this.paymentRequired = options.paymentRequired || null;
|
|
12
|
+
this.paymentResponse = options.paymentResponse || null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class PaymentRequiredError extends M2MSentinelError {}
|
|
17
|
+
class RateLimitedError extends M2MSentinelError {}
|
|
18
|
+
class DataSourceUnavailableError extends M2MSentinelError {}
|
|
19
|
+
|
|
20
|
+
function parseX402Header(value) {
|
|
21
|
+
if (!value) return null;
|
|
22
|
+
// x402 v2 transports protocol objects as base64 JSON. Retain raw-JSON
|
|
23
|
+
// compatibility for older M2M Sentinel deployments during upgrades.
|
|
24
|
+
try { return JSON.parse(value); } catch (_) { /* try v2 encoding */ }
|
|
25
|
+
try {
|
|
26
|
+
const normalized = String(value).replace(/-/g, '+').replace(/_/g, '/');
|
|
27
|
+
const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
|
|
28
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
29
|
+
} catch (_) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeOptions(optionsOrApiKey, baseUrl) {
|
|
35
|
+
if (optionsOrApiKey && typeof optionsOrApiKey === 'object') return { ...optionsOrApiKey };
|
|
36
|
+
return { apiKey: optionsOrApiKey || undefined, baseUrl: baseUrl || DEFAULT_BASE_URL };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class M2MSentinelClient {
|
|
40
|
+
constructor(optionsOrApiKey, baseUrl) {
|
|
41
|
+
const options = normalizeOptions(optionsOrApiKey, baseUrl);
|
|
42
|
+
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
43
|
+
this.apiKey = options.apiKey || undefined;
|
|
44
|
+
this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
45
|
+
this.paymentSignature = options.paymentSignature || undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async request(method, path, body, options = {}) {
|
|
49
|
+
const headers = {
|
|
50
|
+
'Accept': 'application/json'
|
|
51
|
+
};
|
|
52
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
53
|
+
const apiKey = options.apiKey || this.apiKey;
|
|
54
|
+
if (apiKey) headers['x-api-key'] = apiKey;
|
|
55
|
+
if (options.operatorToken) headers.Authorization = 'Bearer ' + options.operatorToken;
|
|
56
|
+
const paymentSignature = options.paymentSignature || this.paymentSignature;
|
|
57
|
+
if (paymentSignature) headers['PAYMENT-SIGNATURE'] = paymentSignature;
|
|
58
|
+
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs || this.timeoutMs);
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetch(this.baseUrl + path, {
|
|
64
|
+
method,
|
|
65
|
+
headers,
|
|
66
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
67
|
+
signal: controller.signal
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err && err.name === 'AbortError') throw new M2MSentinelError('M2M Sentinel request timed out', { status: 0 });
|
|
71
|
+
throw err;
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
let data = null;
|
|
78
|
+
if (text) {
|
|
79
|
+
try { data = JSON.parse(text); } catch (_) { data = { raw: text }; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const paymentRequired = parseX402Header(response.headers.get('PAYMENT-REQUIRED'));
|
|
83
|
+
const paymentResponse = parseX402Header(response.headers.get('PAYMENT-RESPONSE')) || response.headers.get('PAYMENT-RESPONSE');
|
|
84
|
+
if (response.ok) return data;
|
|
85
|
+
|
|
86
|
+
const details = {
|
|
87
|
+
status: response.status,
|
|
88
|
+
body: data,
|
|
89
|
+
retryAfter: response.headers.get('Retry-After'),
|
|
90
|
+
paymentRequired,
|
|
91
|
+
paymentResponse
|
|
92
|
+
};
|
|
93
|
+
const message = data && data.message ? data.message : 'M2M Sentinel HTTP ' + response.status;
|
|
94
|
+
if (response.status === 402) throw new PaymentRequiredError(message, details);
|
|
95
|
+
if (response.status === 429) throw new RateLimitedError(message, details);
|
|
96
|
+
if (response.status === 503 && data && data.error === 'DATA_SOURCE_UNAVAILABLE') throw new DataSourceUnavailableError(message, details);
|
|
97
|
+
throw new M2MSentinelError(message, details);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getStatus() { return this.request('GET', '/v1/status'); }
|
|
101
|
+
getPublicStats(days = 30) { return this.request('GET', '/v1/stats?days=' + encodeURIComponent(days)); }
|
|
102
|
+
// Backward-compatible alias. Ordinary/customer credentials still receive
|
|
103
|
+
// only the counter-free public privacy envelope.
|
|
104
|
+
getAggregateStats(days = 30) { return this.getPublicStats(days); }
|
|
105
|
+
getOperatorAggregateStats(days, operatorToken) {
|
|
106
|
+
return this.request('GET', '/v1/stats?days=' + encodeURIComponent(days || 30), undefined, { operatorToken });
|
|
107
|
+
}
|
|
108
|
+
getPlans() { return this.request('GET', '/v1/plans'); }
|
|
109
|
+
demoAudit(address) { return this.request('GET', '/v1/demo/audit/' + encodeURIComponent(address)); }
|
|
110
|
+
createFreeChallenge(userWallet) { return this.request('POST', '/v1/subscribe/free/challenge', { userWallet }); }
|
|
111
|
+
claimFreeTier(intentId, signature) { return this.request('POST', '/v1/subscribe/free/claim', { intentId, signature }); }
|
|
112
|
+
createSubscriptionIntent(tier, userWallet, options = {}) {
|
|
113
|
+
const body = { tier, userWallet };
|
|
114
|
+
if (options.durationDays !== undefined) body.durationDays = options.durationDays;
|
|
115
|
+
if (options.renewExistingKey !== undefined) body.renewExistingKey = options.renewExistingKey;
|
|
116
|
+
return this.request('POST', '/v1/subscribe/intents', body, options);
|
|
117
|
+
}
|
|
118
|
+
claimSubscription(intentId, signature, txHash) { return this.request('POST', '/v1/subscribe/crypto', { intentId, signature, txHash }); }
|
|
119
|
+
createRecoveryChallenge(userWallet, options = {}) {
|
|
120
|
+
const body = { userWallet };
|
|
121
|
+
if (options.txHash !== undefined) body.txHash = options.txHash;
|
|
122
|
+
return this.request('POST', '/v1/keys/recovery/challenge', body);
|
|
123
|
+
}
|
|
124
|
+
claimRecoveredKey(intentId, signature) {
|
|
125
|
+
return this.request('POST', '/v1/keys/recovery/claim', { intentId, signature });
|
|
126
|
+
}
|
|
127
|
+
auditContract(address, options) { return this.request('GET', '/v1/audit/' + encodeURIComponent(address), undefined, options); }
|
|
128
|
+
getCapabilityScore(address, options) { return this.request('GET', '/v1/security/score/' + encodeURIComponent(address), undefined, options); }
|
|
129
|
+
// Legacy method name; the response is a capability coverage index, not a safety score.
|
|
130
|
+
getSecurityScore(address, options) { return this.request('GET', '/v1/security/score/' + encodeURIComponent(address), undefined, options); }
|
|
131
|
+
getGasFees(options) { return this.request('GET', '/v1/gas/fees', undefined, options); }
|
|
132
|
+
getDexMetrics(options) { return this.request('GET', '/v1/dex/metrics', undefined, options); }
|
|
133
|
+
getTokenPrice(symbol, options) { return this.request('GET', '/v1/token/price/' + encodeURIComponent(symbol), undefined, options); }
|
|
134
|
+
getWhaleSignals(options) { return this.request('GET', '/v1/whales/signals', undefined, options); }
|
|
135
|
+
getKeySelf() { return this.request('GET', '/v1/keys/self'); }
|
|
136
|
+
revokeKey(confirm = true) { return this.request('POST', '/v1/keys/revoke', { confirm }); }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function enforceCallerPolicy(policy, analysis, context) {
|
|
140
|
+
if (typeof policy !== 'function') {
|
|
141
|
+
throw new Error('[M2M Sentinel] A caller-defined policy function is required. Static capability observations are not a safety decision.');
|
|
142
|
+
}
|
|
143
|
+
const result = await policy(analysis, context);
|
|
144
|
+
if (result === false || (result && result.allow === false)) {
|
|
145
|
+
const reason = result && result.reason ? result.reason : 'caller-defined policy rejected the transaction';
|
|
146
|
+
throw new Error('[M2M Sentinel] Transaction blocked: ' + reason + '.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createEthersSentinelMiddleware(apiKey, baseUrl, policy) {
|
|
151
|
+
const client = new M2MSentinelClient({ apiKey, baseUrl });
|
|
152
|
+
return {
|
|
153
|
+
client,
|
|
154
|
+
verifyContractBeforeTx: async (targetAddress, policyOverride) => {
|
|
155
|
+
const audit = await client.auditContract(targetAddress);
|
|
156
|
+
await enforceCallerPolicy(policyOverride || policy, audit, { targetAddress, integration: 'ethers' });
|
|
157
|
+
return audit;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function createViemSentinelInterceptor(apiKey, baseUrl, policy) {
|
|
163
|
+
const client = new M2MSentinelClient({ apiKey, baseUrl });
|
|
164
|
+
return {
|
|
165
|
+
client,
|
|
166
|
+
inspectSwapTarget: async (address, policyOverride) => {
|
|
167
|
+
const analysis = await client.getCapabilityScore(address);
|
|
168
|
+
await enforceCallerPolicy(policyOverride || policy, analysis, { targetAddress: address, integration: 'viem' });
|
|
169
|
+
return analysis;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { M2MSentinelActionProvider, m2mSentinelActionProvider } = require('./agent_adapter.js');
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
M2MSentinelClient,
|
|
178
|
+
M2MSentinelError,
|
|
179
|
+
PaymentRequiredError,
|
|
180
|
+
RateLimitedError,
|
|
181
|
+
DataSourceUnavailableError,
|
|
182
|
+
enforceCallerPolicy,
|
|
183
|
+
createEthersSentinelMiddleware,
|
|
184
|
+
createViemSentinelInterceptor,
|
|
185
|
+
M2MSentinelActionProvider,
|
|
186
|
+
m2mSentinelActionProvider
|
|
187
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./index.js');
|
package/mcp_server.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
const BASE_URL = process.env.M2M_SENTINEL_BASE_URL || 'https://api.m2msentinel.com';
|
|
9
|
+
const API_KEY = process.env.M2M_SENTINEL_API_KEY || '';
|
|
10
|
+
const TIMEOUT_MS = Number(process.env.M2M_SENTINEL_TIMEOUT_MS || 30000);
|
|
11
|
+
|
|
12
|
+
const TOOLS = [
|
|
13
|
+
{
|
|
14
|
+
name: 'm2m_audit_contract',
|
|
15
|
+
description: 'Return selected static bytecode capability observations, common proxy resolution, limitations, and provenance for a Base contract. This is factual capability observation, not a safety or exploitability guarantee.',
|
|
16
|
+
inputSchema: { type: 'object', properties: { address: { type: 'string', description: 'Base contract address (0x...)' } }, required: ['address'] }
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'm2m_get_gas_metrics',
|
|
20
|
+
description: 'Return sourced Base gas fee metrics and execution recommendations.',
|
|
21
|
+
inputSchema: { type: 'object', properties: {} }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'm2m_get_token_price',
|
|
25
|
+
description: 'Return sourced Base DEX token price observation for allowlisted assets (e.g. USDC, WETH).',
|
|
26
|
+
inputSchema: { type: 'object', properties: { symbol: { type: 'string', description: 'Token symbol (USDC, WETH)' } }, required: ['symbol'] }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'm2m_get_dex_liquidity',
|
|
30
|
+
description: 'Return tracked Base DEX pool reserve and liquidity metrics.',
|
|
31
|
+
inputSchema: { type: 'object', properties: { pair: { type: 'string' } } }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'm2m_get_whale_signals',
|
|
35
|
+
description: 'Return tracked Base whale transfer signals.',
|
|
36
|
+
inputSchema: { type: 'object', properties: { limit: { type: 'number' } } }
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'm2m_get_service_status',
|
|
40
|
+
description: 'Return real-time operational status of M2M Sentinel upstream RPC and persistence rails.',
|
|
41
|
+
inputSchema: { type: 'object', properties: {} }
|
|
42
|
+
},
|
|
43
|
+
// Backwards compatibility aliases
|
|
44
|
+
{
|
|
45
|
+
name: 'audit_contract',
|
|
46
|
+
description: 'Alias for m2m_audit_contract.',
|
|
47
|
+
inputSchema: { type: 'object', properties: { address: { type: 'string' } }, required: ['address'] }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'get_capability_score',
|
|
51
|
+
description: 'Return the static capability coverage index and provenance for a Base contract. This is not a safety score.',
|
|
52
|
+
inputSchema: { type: 'object', properties: { address: { type: 'string' } }, required: ['address'] }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'get_gas_fees',
|
|
56
|
+
description: 'Alias for m2m_get_gas_metrics.',
|
|
57
|
+
inputSchema: { type: 'object', properties: {} }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'get_dex_metrics',
|
|
61
|
+
description: 'Alias for m2m_get_dex_liquidity.',
|
|
62
|
+
inputSchema: { type: 'object', properties: {} }
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'get_token_price',
|
|
66
|
+
description: 'Alias for m2m_get_token_price.',
|
|
67
|
+
inputSchema: { type: 'object', properties: { symbol: { type: 'string' } }, required: ['symbol'] }
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'get_whale_signals',
|
|
71
|
+
description: 'Alias for m2m_get_whale_signals.',
|
|
72
|
+
inputSchema: { type: 'object', properties: {} }
|
|
73
|
+
}
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function parseX402Header(value) {
|
|
77
|
+
if (!value) return null;
|
|
78
|
+
try { return JSON.parse(value); } catch (_) {}
|
|
79
|
+
try {
|
|
80
|
+
const normalized = String(value).replace(/-/g, '+').replace(/_/g, '/');
|
|
81
|
+
const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
|
|
82
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
83
|
+
} catch (_) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function queryApi(path) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const url = new URL(path, BASE_URL);
|
|
91
|
+
const isHttps = url.protocol === 'https:';
|
|
92
|
+
const transport = isHttps ? https : http;
|
|
93
|
+
const headers = { Accept: 'application/json', 'User-Agent': 'M2MSentinel-MCP/1.1.0' };
|
|
94
|
+
if (API_KEY) headers['x-api-key'] = API_KEY;
|
|
95
|
+
|
|
96
|
+
const req = transport.request(url, { method: 'GET', headers, timeout: TIMEOUT_MS }, (res) => {
|
|
97
|
+
let data = '';
|
|
98
|
+
res.setEncoding('utf8');
|
|
99
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
100
|
+
res.on('end', () => {
|
|
101
|
+
let body = null;
|
|
102
|
+
if (data) {
|
|
103
|
+
try { body = JSON.parse(data); } catch (_) { body = { raw: data }; }
|
|
104
|
+
}
|
|
105
|
+
resolve({
|
|
106
|
+
status: res.statusCode,
|
|
107
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
108
|
+
body,
|
|
109
|
+
paymentRequired: parseX402Header(res.headers['payment-required'] || res.headers['x-payment-required']),
|
|
110
|
+
paymentResponse: parseX402Header(res.headers['payment-response'] || res.headers['x-payment-response']),
|
|
111
|
+
retryAfter: res.headers['retry-after'] || null
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
req.on('timeout', () => req.destroy(new Error('M2M Sentinel request timed out')));
|
|
116
|
+
req.on('error', reject);
|
|
117
|
+
req.end();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pathForTool(name, args) {
|
|
122
|
+
const input = args || {};
|
|
123
|
+
switch (name) {
|
|
124
|
+
case 'm2m_audit_contract':
|
|
125
|
+
case 'audit_contract':
|
|
126
|
+
if (!input.address) throw new Error('address is required');
|
|
127
|
+
return '/v1/audit/' + encodeURIComponent(input.address);
|
|
128
|
+
case 'get_capability_score':
|
|
129
|
+
if (!input.address) throw new Error('address is required');
|
|
130
|
+
return '/v1/security/score/' + encodeURIComponent(input.address);
|
|
131
|
+
case 'm2m_get_gas_metrics':
|
|
132
|
+
case 'get_gas_fees':
|
|
133
|
+
return '/v1/gas/fees';
|
|
134
|
+
case 'm2m_get_dex_liquidity':
|
|
135
|
+
case 'get_dex_metrics':
|
|
136
|
+
return '/v1/dex/metrics';
|
|
137
|
+
case 'm2m_get_token_price':
|
|
138
|
+
case 'get_token_price':
|
|
139
|
+
if (!input.symbol) throw new Error('symbol is required');
|
|
140
|
+
return '/v1/token/price/' + encodeURIComponent(input.symbol);
|
|
141
|
+
case 'm2m_get_whale_signals':
|
|
142
|
+
case 'get_whale_signals':
|
|
143
|
+
return '/v1/whales/signals';
|
|
144
|
+
case 'm2m_get_service_status':
|
|
145
|
+
return '/v1/status';
|
|
146
|
+
default:
|
|
147
|
+
throw new Error('Unknown tool: ' + name);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function send(message) {
|
|
152
|
+
process.stdout.write(JSON.stringify(message) + '\n');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function success(id, result) {
|
|
156
|
+
send({ jsonrpc: '2.0', id, result });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function failure(id, code, message, data) {
|
|
160
|
+
const error = { code, message };
|
|
161
|
+
if (data !== undefined) error.data = data;
|
|
162
|
+
send({ jsonrpc: '2.0', id, error });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function handle(request) {
|
|
166
|
+
if (!request || request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
|
|
167
|
+
failure(request && request.id !== undefined ? request.id : null, -32600, 'Invalid Request');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (request.id === undefined || request.id === null) return;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (request.method === 'initialize') {
|
|
174
|
+
success(request.id, {
|
|
175
|
+
protocolVersion: request.params && request.params.protocolVersion ? request.params.protocolVersion : '2024-11-05',
|
|
176
|
+
capabilities: { tools: {} },
|
|
177
|
+
serverInfo: { name: 'm2m-sentinel-mcp', version: '1.1.0' }
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (request.method === 'tools/list') {
|
|
182
|
+
success(request.id, { tools: TOOLS });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (request.method === 'tools/call') {
|
|
186
|
+
const params = request.params || {};
|
|
187
|
+
const path = pathForTool(params.name, params.arguments || {});
|
|
188
|
+
const apiResult = await queryApi(path);
|
|
189
|
+
const isError = !apiResult.ok;
|
|
190
|
+
success(request.id, {
|
|
191
|
+
content: [{ type: 'text', text: typeof apiResult.body === 'object' ? JSON.stringify(apiResult.body, null, 2) : String(apiResult.body) }],
|
|
192
|
+
isError,
|
|
193
|
+
structuredContent: apiResult.body,
|
|
194
|
+
_meta: {
|
|
195
|
+
httpStatus: apiResult.status,
|
|
196
|
+
paymentRequired: apiResult.paymentRequired,
|
|
197
|
+
paymentResponse: apiResult.paymentResponse,
|
|
198
|
+
retryAfter: apiResult.retryAfter,
|
|
199
|
+
notASafetyGuarantee: true
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
failure(request.id, -32601, 'Method not found');
|
|
205
|
+
} catch (err) {
|
|
206
|
+
failure(request.id, -32603, err && err.message ? err.message : 'Internal error');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
211
|
+
rl.on('line', (line) => {
|
|
212
|
+
if (!line.trim()) return;
|
|
213
|
+
let request;
|
|
214
|
+
try {
|
|
215
|
+
request = JSON.parse(line);
|
|
216
|
+
} catch (_) {
|
|
217
|
+
failure(null, -32700, 'Parse error');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
handle(request);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
module.exports = { TOOLS, pathForTool, parseX402Header };
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@m2msentinel/sdk",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "JavaScript client for M2M Sentinel Base bytecode capability, proxy and market observations",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"types": "index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"m2m-sentinel-mcp": "./mcp_server.js",
|
|
12
|
+
"m2m-sentinel": "./mcp_server.js"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://m2msentinel.com",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/M2M-Sentinel/m2m-sentinel-sdk.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://m2msentinel.com/docs.html"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"web3",
|
|
25
|
+
"x402",
|
|
26
|
+
"m2m-sentinel",
|
|
27
|
+
"base",
|
|
28
|
+
"bytecode-capabilities",
|
|
29
|
+
"proxy-detection",
|
|
30
|
+
"agents",
|
|
31
|
+
"mcp",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"mcp-server",
|
|
34
|
+
"smithery",
|
|
35
|
+
"glama",
|
|
36
|
+
"agentkit",
|
|
37
|
+
"coinbase",
|
|
38
|
+
"smart-contract-audit"
|
|
39
|
+
],
|
|
40
|
+
"author": "M2M Sentinel",
|
|
41
|
+
"files": [
|
|
42
|
+
"index.js",
|
|
43
|
+
"index.d.ts",
|
|
44
|
+
"m2m_sentinel_sdk.js",
|
|
45
|
+
"agent_adapter.js",
|
|
46
|
+
"eliza_plugin.js",
|
|
47
|
+
"mcp_server.js",
|
|
48
|
+
"smithery.yaml",
|
|
49
|
+
"README.md",
|
|
50
|
+
"typescript/"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Smithery MCP Server Configuration
|
|
2
|
+
# https://smithery.ai/docs/config
|
|
3
|
+
|
|
4
|
+
startCommand:
|
|
5
|
+
type: stdio
|
|
6
|
+
command: npx
|
|
7
|
+
args:
|
|
8
|
+
- -y
|
|
9
|
+
- m2m-sentinel-sdk
|
|
10
|
+
configSchema:
|
|
11
|
+
type: object
|
|
12
|
+
properties:
|
|
13
|
+
M2M_SENTINEL_API_KEY:
|
|
14
|
+
type: string
|
|
15
|
+
title: M2M Sentinel API Key
|
|
16
|
+
description: Optional API key for higher rate limits. Public free tier works without key.
|
|
17
|
+
default: ""
|
|
18
|
+
|
|
19
|
+
configSchema:
|
|
20
|
+
type: object
|
|
21
|
+
properties:
|
|
22
|
+
M2M_SENTINEL_API_KEY:
|
|
23
|
+
type: string
|
|
24
|
+
title: M2M Sentinel API Key
|
|
25
|
+
description: Optional API key for higher rate limits. Public free tier works without key.
|
|
26
|
+
default: ""
|
|
27
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
export interface M2MSentinelClientOptions {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
paymentSignature?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface M2MSentinelRequestOptions extends M2MSentinelClientOptions {
|
|
9
|
+
operatorToken?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CreateSubscriptionIntentOptions extends M2MSentinelClientOptions {
|
|
13
|
+
durationDays?: 31 | 90 | 365;
|
|
14
|
+
renewExistingKey?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RecoveryChallengeOptions {
|
|
18
|
+
txHash?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface M2MSentinelErrorOptions {
|
|
22
|
+
status?: number;
|
|
23
|
+
body?: any;
|
|
24
|
+
retryAfter?: string | null;
|
|
25
|
+
paymentRequired?: any;
|
|
26
|
+
paymentResponse?: any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class M2MSentinelError extends Error {
|
|
30
|
+
public readonly status?: number;
|
|
31
|
+
public readonly body?: any;
|
|
32
|
+
public readonly retryAfter?: string | null;
|
|
33
|
+
public readonly paymentRequired?: any;
|
|
34
|
+
public readonly paymentResponse?: any;
|
|
35
|
+
|
|
36
|
+
constructor(message: string, options: M2MSentinelErrorOptions = {}) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = new.target.name;
|
|
39
|
+
this.status = options.status;
|
|
40
|
+
this.body = options.body;
|
|
41
|
+
this.retryAfter = options.retryAfter || null;
|
|
42
|
+
this.paymentRequired = options.paymentRequired || null;
|
|
43
|
+
this.paymentResponse = options.paymentResponse || null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class PaymentRequiredError extends M2MSentinelError {}
|
|
48
|
+
export class RateLimitedError extends M2MSentinelError {}
|
|
49
|
+
export class DataSourceUnavailableError extends M2MSentinelError {}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_BASE_URL = 'https://api.m2msentinel.com';
|
|
52
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
53
|
+
|
|
54
|
+
function parseX402Header(value: string | null): any {
|
|
55
|
+
if (!value) return null;
|
|
56
|
+
try { return JSON.parse(value); } catch { /* try v2 encoding */ }
|
|
57
|
+
try {
|
|
58
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
59
|
+
const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
|
|
60
|
+
const bytes = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
61
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class M2MSentinelClient {
|
|
68
|
+
private baseUrl: string;
|
|
69
|
+
private apiKey?: string;
|
|
70
|
+
private timeoutMs: number;
|
|
71
|
+
private paymentSignature?: string;
|
|
72
|
+
|
|
73
|
+
constructor(optionsOrBaseUrl: M2MSentinelClientOptions | string = {}, apiKey?: string) {
|
|
74
|
+
const options = typeof optionsOrBaseUrl === 'string'
|
|
75
|
+
? { baseUrl: optionsOrBaseUrl, apiKey }
|
|
76
|
+
: optionsOrBaseUrl;
|
|
77
|
+
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
78
|
+
this.apiKey = options.apiKey;
|
|
79
|
+
this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
80
|
+
this.paymentSignature = options.paymentSignature;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async request<T>(method: 'GET' | 'POST', path: string, body?: any, options: M2MSentinelRequestOptions = {}): Promise<T> {
|
|
84
|
+
const headers: Record<string, string> = {
|
|
85
|
+
Accept: 'application/json'
|
|
86
|
+
};
|
|
87
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
88
|
+
const key = options.apiKey || this.apiKey;
|
|
89
|
+
if (key) headers['x-api-key'] = key;
|
|
90
|
+
if (options.operatorToken) headers.Authorization = `Bearer ${options.operatorToken}`;
|
|
91
|
+
const paymentSignature = options.paymentSignature || this.paymentSignature;
|
|
92
|
+
if (paymentSignature) headers['PAYMENT-SIGNATURE'] = paymentSignature;
|
|
93
|
+
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs || this.timeoutMs);
|
|
96
|
+
let response: Response;
|
|
97
|
+
try {
|
|
98
|
+
response = await fetch(this.baseUrl + path, {
|
|
99
|
+
method,
|
|
100
|
+
headers,
|
|
101
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
102
|
+
signal: controller.signal
|
|
103
|
+
});
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
let data: any = null;
|
|
110
|
+
if (text) {
|
|
111
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
112
|
+
}
|
|
113
|
+
if (response.ok) return data as T;
|
|
114
|
+
|
|
115
|
+
const errorOptions = {
|
|
116
|
+
status: response.status,
|
|
117
|
+
body: data,
|
|
118
|
+
retryAfter: response.headers.get('Retry-After'),
|
|
119
|
+
paymentRequired: parseX402Header(response.headers.get('PAYMENT-REQUIRED')),
|
|
120
|
+
paymentResponse: parseX402Header(response.headers.get('PAYMENT-RESPONSE')) || response.headers.get('PAYMENT-RESPONSE')
|
|
121
|
+
};
|
|
122
|
+
const message = data?.message || `M2M Sentinel HTTP ${response.status}`;
|
|
123
|
+
if (response.status === 402) throw new PaymentRequiredError(message, errorOptions);
|
|
124
|
+
if (response.status === 429) throw new RateLimitedError(message, errorOptions);
|
|
125
|
+
if (response.status === 503 && data?.error === 'DATA_SOURCE_UNAVAILABLE') throw new DataSourceUnavailableError(message, errorOptions);
|
|
126
|
+
throw new M2MSentinelError(message, errorOptions);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public getStatus() { return this.request<any>('GET', '/v1/status'); }
|
|
130
|
+
public getPublicStats(days = 30) { return this.request<any>('GET', `/v1/stats?days=${encodeURIComponent(days)}`); }
|
|
131
|
+
public getAggregateStats(days = 30) { return this.getPublicStats(days); }
|
|
132
|
+
public getOperatorAggregateStats(days: number, operatorToken: string) {
|
|
133
|
+
return this.request<any>('GET', `/v1/stats?days=${encodeURIComponent(days || 30)}`, undefined, { operatorToken });
|
|
134
|
+
}
|
|
135
|
+
public getPlans() { return this.request<any>('GET', '/v1/plans'); }
|
|
136
|
+
public demoAudit(address: string) { return this.request<any>('GET', `/v1/demo/audit/${encodeURIComponent(address)}`); }
|
|
137
|
+
public createFreeChallenge(userWallet: string) { return this.request<any>('POST', '/v1/subscribe/free/challenge', { userWallet }); }
|
|
138
|
+
public claimFreeTier(intentId: string, signature: string) { return this.request<any>('POST', '/v1/subscribe/free/claim', { intentId, signature }); }
|
|
139
|
+
public createSubscriptionIntent(tier: string, userWallet: string, options: CreateSubscriptionIntentOptions = {}) {
|
|
140
|
+
const body: Record<string, any> = { tier, userWallet };
|
|
141
|
+
if (options.durationDays !== undefined) body.durationDays = options.durationDays;
|
|
142
|
+
if (options.renewExistingKey !== undefined) body.renewExistingKey = options.renewExistingKey;
|
|
143
|
+
return this.request<any>('POST', '/v1/subscribe/intents', body, options);
|
|
144
|
+
}
|
|
145
|
+
public claimSubscription(intentId: string, signature: string, txHash: string) { return this.request<any>('POST', '/v1/subscribe/crypto', { intentId, signature, txHash }); }
|
|
146
|
+
public createRecoveryChallenge(userWallet: string, options: RecoveryChallengeOptions = {}) {
|
|
147
|
+
const body: Record<string, string> = { userWallet };
|
|
148
|
+
if (options.txHash !== undefined) body.txHash = options.txHash;
|
|
149
|
+
return this.request<any>('POST', '/v1/keys/recovery/challenge', body);
|
|
150
|
+
}
|
|
151
|
+
public claimRecoveredKey(intentId: string, signature: string) {
|
|
152
|
+
return this.request<any>('POST', '/v1/keys/recovery/claim', { intentId, signature });
|
|
153
|
+
}
|
|
154
|
+
public auditContract(address: string, options?: M2MSentinelClientOptions) { return this.request<any>('GET', `/v1/audit/${encodeURIComponent(address)}`, undefined, options); }
|
|
155
|
+
public getCapabilityScore(address: string, options?: M2MSentinelClientOptions) { return this.request<any>('GET', `/v1/security/score/${encodeURIComponent(address)}`, undefined, options); }
|
|
156
|
+
/** Legacy name. The response is a capability coverage index, not a safety score. */
|
|
157
|
+
public getSecurityScore(address: string, options?: M2MSentinelClientOptions) { return this.request<any>('GET', `/v1/security/score/${encodeURIComponent(address)}`, undefined, options); }
|
|
158
|
+
public getGasFees(options?: M2MSentinelClientOptions) { return this.request<any>('GET', '/v1/gas/fees', undefined, options); }
|
|
159
|
+
public getDexMetrics(options?: M2MSentinelClientOptions) { return this.request<any>('GET', '/v1/dex/metrics', undefined, options); }
|
|
160
|
+
public getTokenPrice(symbol: string, options?: M2MSentinelClientOptions) { return this.request<any>('GET', `/v1/token/price/${encodeURIComponent(symbol)}`, undefined, options); }
|
|
161
|
+
public getWhaleSignals(options?: M2MSentinelClientOptions) { return this.request<any>('GET', '/v1/whales/signals', undefined, options); }
|
|
162
|
+
public getKeySelf() { return this.request<any>('GET', '/v1/keys/self'); }
|
|
163
|
+
public revokeKey(confirm = true) { return this.request<any>('POST', '/v1/keys/revoke', { confirm }); }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type SentinelPolicy = (analysis: any, context: { targetAddress: string; integration: string }) =>
|
|
167
|
+
boolean | { allow: boolean; reason?: string } | Promise<boolean | { allow: boolean; reason?: string }>;
|
|
168
|
+
|
|
169
|
+
export async function enforceCallerPolicy(policy: SentinelPolicy | undefined, analysis: any, context: { targetAddress: string; integration: string }) {
|
|
170
|
+
if (typeof policy !== 'function') {
|
|
171
|
+
throw new Error('[M2M Sentinel] A caller-defined policy function is required. Static capability observations are not a safety decision.');
|
|
172
|
+
}
|
|
173
|
+
const result = await policy(analysis, context);
|
|
174
|
+
if (result === false || (typeof result === 'object' && result !== null && result.allow === false)) {
|
|
175
|
+
const reason = typeof result === 'object' && result !== null && result.reason
|
|
176
|
+
? result.reason
|
|
177
|
+
: 'caller-defined policy rejected the transaction';
|
|
178
|
+
throw new Error(`[M2M Sentinel] Transaction blocked: ${reason}.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function createEthersSentinelMiddleware(apiKey?: string, baseUrl?: string, policy?: SentinelPolicy) {
|
|
183
|
+
const client = new M2MSentinelClient({ apiKey, baseUrl });
|
|
184
|
+
return {
|
|
185
|
+
client,
|
|
186
|
+
verifyContractBeforeTx: async (targetAddress: string, policyOverride?: SentinelPolicy) => {
|
|
187
|
+
const audit = await client.auditContract(targetAddress);
|
|
188
|
+
await enforceCallerPolicy(policyOverride || policy, audit, { targetAddress, integration: 'ethers' });
|
|
189
|
+
return audit;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createViemSentinelInterceptor(apiKey?: string, baseUrl?: string, policy?: SentinelPolicy) {
|
|
195
|
+
const client = new M2MSentinelClient({ apiKey, baseUrl });
|
|
196
|
+
return {
|
|
197
|
+
client,
|
|
198
|
+
inspectSwapTarget: async (address: string, policyOverride?: SentinelPolicy) => {
|
|
199
|
+
const analysis = await client.getCapabilityScore(address);
|
|
200
|
+
await enforceCallerPolicy(policyOverride || policy, analysis, { targetAddress: address, integration: 'viem' });
|
|
201
|
+
return analysis;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "m2m-sentinel-sdk",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Source-distributed TypeScript client for M2M Sentinel Base capability intelligence API",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"types": "index.ts",
|
|
7
|
+
"private": true,
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"typescript": "^5.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|