m2m-sentinel-sdk 1.0.4 → 1.1.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 +71 -59
- package/agent_adapter.js +2 -2
- package/eliza_plugin.js +7 -7
- package/index.d.ts +94 -0
- package/index.js +181 -141
- package/mcp_server.js +222 -171
- package/package.json +6 -9
- package/typescript/index.ts +202 -147
- package/typescript/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,59 +1,71 @@
|
|
|
1
|
-
# M2M Sentinel SDKs
|
|
2
|
-
|
|
3
|
-
Base URL: `https://m2msentinel.vercel.app`.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
# M2M Sentinel SDKs
|
|
2
|
+
|
|
3
|
+
Base URL: `https://m2msentinel.vercel.app`.
|
|
4
|
+
|
|
5
|
+
M2M Sentinel reports selected static bytecode capabilities, common proxy
|
|
6
|
+
structures, evidence quality, and sourced Base market observations. It does not
|
|
7
|
+
classify a contract as safe, malicious, or exploitable. Transaction middleware
|
|
8
|
+
therefore requires a caller-defined policy and has no built-in allow threshold.
|
|
9
|
+
|
|
10
|
+
## Support status
|
|
11
|
+
|
|
12
|
+
Supported here: JavaScript/Node (`index.js`), TypeScript source, Python source,
|
|
13
|
+
and the MCP stdio server. Other language folders are community previews and
|
|
14
|
+
must be checked against `/openapi.json` before production use.
|
|
15
|
+
|
|
16
|
+
## Install this source build
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node -e "const { M2MSentinelClient } = require('./public/sdk')"
|
|
20
|
+
python -m pip install ./public/sdk/python
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Registry publishing is intentionally separate from the production API.
|
|
24
|
+
The repository does not claim that these source-installable packages are available from every language registry.
|
|
25
|
+
|
|
26
|
+
## Authentication and x402
|
|
27
|
+
|
|
28
|
+
Send API keys only in `x-api-key` (or `Authorization: Bearer`). Query-string
|
|
29
|
+
credentials are rejected. Payable routes also support x402 v2. An unpaid call
|
|
30
|
+
returns a base64 `PAYMENT-REQUIRED` challenge when the facilitator is ready; a
|
|
31
|
+
successful settlement returns `PAYMENT-RESPONSE`.
|
|
32
|
+
|
|
33
|
+
## Public routes
|
|
34
|
+
|
|
35
|
+
- `GET /v1/status`
|
|
36
|
+
- `GET /v1/stats` (public counter-free privacy envelope; exact aggregate
|
|
37
|
+
commercial counters are operator-only)
|
|
38
|
+
- `GET /v1/plans`
|
|
39
|
+
- `GET /v1/demo/audit/:address` for the published sample allowlist
|
|
40
|
+
- free-key and subscription onboarding routes
|
|
41
|
+
|
|
42
|
+
The JavaScript, TypeScript, and Python clients expose multi-period
|
|
43
|
+
purchase/renewal and wallet recovery without requiring callers to hand-build
|
|
44
|
+
requests:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
await client.createSubscriptionIntent('GROWTH', wallet, {
|
|
48
|
+
durationDays: 90,
|
|
49
|
+
renewExistingKey: true,
|
|
50
|
+
apiKey: existingPaidKey
|
|
51
|
+
});
|
|
52
|
+
const challenge = await client.createRecoveryChallenge(wallet, { txHash });
|
|
53
|
+
await client.claimRecoveredKey(challenge.intent.id, walletSignature);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`getPublicStats()` returns the counter-free privacy envelope. Operator-only
|
|
57
|
+
detail has a deliberately separate `getOperatorAggregateStats(days, token)`
|
|
58
|
+
method and sends the operator token only as `Authorization: Bearer`. This is
|
|
59
|
+
for private server/operator tooling only: never embed or bundle the operator
|
|
60
|
+
token in browser, mobile, SDK-distribution, or customer code.
|
|
61
|
+
|
|
62
|
+
## Protected routes
|
|
63
|
+
|
|
64
|
+
- `GET /v1/audit/:address`
|
|
65
|
+
- `GET /v1/security/score/:address` (legacy URL; returns a capability coverage index)
|
|
66
|
+
- Base gas, DEX, token-price, and large-transfer observation routes
|
|
67
|
+
- key self-service routes
|
|
68
|
+
|
|
69
|
+
All live-data responses preserve provenance. Capability responses include
|
|
70
|
+
`notASafetyGuarantee: true`, a limitations array, `capabilityRating`, and an
|
|
71
|
+
evidence-grade flag. Do not replace `UNVERIFIED` or `null` with a default.
|
package/agent_adapter.js
CHANGED
|
@@ -4,8 +4,8 @@ const { M2MSentinelClient } = require('./index.js');
|
|
|
4
4
|
|
|
5
5
|
class M2MSentinelAgentTool {
|
|
6
6
|
constructor(options = {}) {
|
|
7
|
-
this.name = '
|
|
8
|
-
this.description = '
|
|
7
|
+
this.name = 'm2m_sentinel_capability_analysis';
|
|
8
|
+
this.description = 'Reports selected static bytecode capabilities, common proxy structures, provenance, and limitations for Base contracts.';
|
|
9
9
|
this.client = new M2MSentinelClient({
|
|
10
10
|
apiKey: options.apiKey || process.env.M2M_SENTINEL_API_KEY,
|
|
11
11
|
baseUrl: options.baseUrl
|
package/eliza_plugin.js
CHANGED
|
@@ -20,7 +20,7 @@ const m2mSentinelPlugin = {
|
|
|
20
20
|
actions: [
|
|
21
21
|
{
|
|
22
22
|
name: 'AUDIT_CONTRACT',
|
|
23
|
-
description: '
|
|
23
|
+
description: 'Reports selected static capability and proxy observations for a Base contract.',
|
|
24
24
|
handler: async (runtime, message) => {
|
|
25
25
|
const address = message.content.text.match(/0x[a-fA-F0-9]{40}/)?.[0];
|
|
26
26
|
if (!address) return { text: 'Please provide a valid 42-character EVM contract address.' };
|
|
@@ -28,20 +28,20 @@ const m2mSentinelPlugin = {
|
|
|
28
28
|
const data = await clientFor(runtime).auditContract(address);
|
|
29
29
|
const audit = data.audit || {};
|
|
30
30
|
const proxy = audit.proxyResolution || {};
|
|
31
|
-
return { text: `M2M Sentinel
|
|
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
32
|
} catch (err) { return { text: errorText(err) }; }
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
name: '
|
|
37
|
-
description: 'Returns the
|
|
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
38
|
handler: async (runtime, message) => {
|
|
39
39
|
const address = message.content.text.match(/0x[a-fA-F0-9]{40}/)?.[0];
|
|
40
40
|
if (!address) return { text: 'Please provide a valid EVM contract address.' };
|
|
41
41
|
try {
|
|
42
|
-
const data = await clientFor(runtime).
|
|
43
|
-
const score = data.
|
|
44
|
-
return { text: `M2M Sentinel
|
|
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
45
|
} catch (err) { return { text: errorText(err) }; }
|
|
46
46
|
}
|
|
47
47
|
},
|
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
CHANGED
|
@@ -1,143 +1,183 @@
|
|
|
1
|
-
const DEFAULT_BASE_URL = 'https://m2msentinel.vercel.app';
|
|
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
|
|
21
|
-
if (!value) return null;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://m2msentinel.vercel.app';
|
|
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 = {
|
|
41
50
|
'Accept': 'application/json'
|
|
42
|
-
};
|
|
43
|
-
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
44
|
-
const apiKey = options.apiKey || this.apiKey;
|
|
45
|
-
if (apiKey) headers['x-api-key'] = apiKey;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
throw
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (response.status ===
|
|
86
|
-
if (response.status ===
|
|
87
|
-
throw new
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
module.exports = {
|
|
175
|
+
M2MSentinelClient,
|
|
176
|
+
M2MSentinelError,
|
|
177
|
+
PaymentRequiredError,
|
|
178
|
+
RateLimitedError,
|
|
179
|
+
DataSourceUnavailableError,
|
|
180
|
+
enforceCallerPolicy,
|
|
181
|
+
createEthersSentinelMiddleware,
|
|
182
|
+
createViemSentinelInterceptor
|
|
143
183
|
};
|