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 CHANGED
@@ -1,59 +1,71 @@
1
- # M2M Sentinel SDKs
2
-
3
- Base URL: `https://m2msentinel.vercel.app`.
4
-
5
- ## Support status
6
-
7
- Supported in this repository:
8
- - JavaScript/Node: `public/sdk/index.js` (`m2m_sentinel_sdk.js` re-exports it)
9
- - TypeScript source: `public/sdk/typescript/index.ts`
10
- - Python source: `public/sdk/m2m_sentinel/` and `public/sdk/python/m2m_sentinel/`
11
- - MCP stdio server: `public/sdk/mcp_server.js`
12
-
13
- Community preview only: Go, Rust, Java, Kotlin, Swift, C#, PHP, Ruby, CLI, Eliza plugin, and agent adapter. They include current route/header examples but are not covered by the supported-SDK guarantee; verify against `/openapi.json` before production use.
14
-
15
- ## Installation
16
-
17
- Install from source (recommended for current `v1.0.4` build):
18
-
19
- ```bash
20
- # JavaScript / Node.js (local path)
21
- node -e "const { M2MSentinelClient } = require('./public/sdk')"
22
-
23
- # Python (local path)
24
- python -m pip install ./public/sdk/python
25
- ```
26
-
27
- *Note on Registry Packages (`v1.0.4`)*:
28
- Once published to npm and PyPI (via `npm publish` and `twine upload`), install directly via the package manager:
29
- - **npm registry**: `m2m-sentinel-sdk`
30
- - **PyPI registry**: `m2m-sentinel`
31
-
32
- ## Authentication
33
-
34
- API keys must be sent only in headers:
35
-
36
- ```text
37
- x-api-key: <key>
38
- ```
39
-
40
- `Authorization: Bearer <key>` is accepted by the API, but the supported SDKs use `x-api-key`. Never place credentials in URLs or query strings; the server rejects them with HTTP 401.
41
-
42
- ## x402 / 402 behavior
43
-
44
- Paid endpoints accept either `x-api-key` or a settled x402 v2 payment. Without credentials they return HTTP 402 and a `PAYMENT-REQUIRED` header containing a JSON challenge. Supported SDKs raise/return a payment-required error that exposes the parsed challenge. After settlement, retry with `PAYMENT-SIGNATURE`; successful paid responses may include `PAYMENT-RESPONSE` and include `provenance` in the JSON body.
45
-
46
- ## Free-tier signup
47
-
48
- 1. `POST /v1/subscribe/free/challenge` with `{ "userWallet": "0x..." }`.
49
- 2. Sign `intent.messageToSign` with that wallet.
50
- 3. `POST /v1/subscribe/free/claim` with `{ "intentId": "...", "signature": "..." }`.
51
- 4. Store the returned `apiKey`; it cannot be recovered later.
52
-
53
- ## Current routes
54
-
55
- Public: `GET /v1/status`, `GET /v1/plans`, free signup, and subscription intent/claim routes.
56
-
57
- Protected/paid: `GET /v1/audit/:address`, `GET /v1/security/score/:address`, `GET /v1/gas/fees`, `GET /v1/dex/metrics`, `GET /v1/token/price/:symbol`, `GET /v1/whales/signals`, `GET /v1/keys/self`, `POST /v1/keys/revoke`.
58
-
59
- SDKs preserve response bodies, including `provenance`. They surface 401, 402, 405, 429 (`Retry-After`), and 503 `DATA_SOURCE_UNAVAILABLE` without inventing fallback values.
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 = 'm2m_sentinel_security_audit';
8
- this.description = 'Audits EVM smart contracts on Base for security risks, proxies, and bytecode findings.';
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: 'Audits an EVM contract on Base Mainnet.',
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 audit for ${address}: ${audit.securityRating || 'UNKNOWN'}; proxy=${proxy.isProxy ? proxy.proxyType : 'NO'}; provenance=${JSON.stringify(audit.provenance || data.provenance || null)}` };
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: 'GET_SECURITY_SCORE',
37
- description: 'Returns the security score for a Base contract address.',
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).getSecurityScore(address);
43
- const score = data.securityScore === null ? 'not decision-grade' : `${data.securityScore}/100`;
44
- return { text: `M2M Sentinel security score for ${address}: ${score}. Provenance: ${JSON.stringify(data.provenance || null)}` };
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 parseHeaderJson(value) {
21
- if (!value) return null;
22
- try { return JSON.parse(value); } catch (_) { return null; }
23
- }
24
-
25
- function normalizeOptions(optionsOrApiKey, baseUrl) {
26
- if (optionsOrApiKey && typeof optionsOrApiKey === 'object') return { ...optionsOrApiKey };
27
- return { apiKey: optionsOrApiKey || undefined, baseUrl: baseUrl || DEFAULT_BASE_URL };
28
- }
29
-
30
- class M2MSentinelClient {
31
- constructor(optionsOrApiKey, baseUrl) {
32
- const options = normalizeOptions(optionsOrApiKey, baseUrl);
33
- this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
34
- this.apiKey = options.apiKey || undefined;
35
- this.timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
36
- this.paymentSignature = options.paymentSignature || undefined;
37
- }
38
-
39
- async request(method, path, body, options = {}) {
40
- const headers = {
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
- const paymentSignature = options.paymentSignature || this.paymentSignature;
47
- if (paymentSignature) headers['PAYMENT-SIGNATURE'] = paymentSignature;
48
-
49
- const controller = new AbortController();
50
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs || this.timeoutMs);
51
- let response;
52
- try {
53
- response = await fetch(this.baseUrl + path, {
54
- method,
55
- headers,
56
- body: body === undefined ? undefined : JSON.stringify(body),
57
- signal: controller.signal
58
- });
59
- } catch (err) {
60
- if (err && err.name === 'AbortError') throw new M2MSentinelError('M2M Sentinel request timed out', { status: 0 });
61
- throw err;
62
- } finally {
63
- clearTimeout(timeout);
64
- }
65
-
66
- const text = await response.text();
67
- let data = null;
68
- if (text) {
69
- try { data = JSON.parse(text); } catch (_) { data = { raw: text }; }
70
- }
71
-
72
- const paymentRequired = parseHeaderJson(response.headers.get('PAYMENT-REQUIRED'));
73
- const paymentResponse = parseHeaderJson(response.headers.get('PAYMENT-RESPONSE')) || response.headers.get('PAYMENT-RESPONSE');
74
- if (response.ok) return data;
75
-
76
- const details = {
77
- status: response.status,
78
- body: data,
79
- retryAfter: response.headers.get('Retry-After'),
80
- paymentRequired,
81
- paymentResponse
82
- };
83
- const message = data && data.message ? data.message : 'M2M Sentinel HTTP ' + response.status;
84
- if (response.status === 402) throw new PaymentRequiredError(message, details);
85
- if (response.status === 429) throw new RateLimitedError(message, details);
86
- if (response.status === 503 && data && data.error === 'DATA_SOURCE_UNAVAILABLE') throw new DataSourceUnavailableError(message, details);
87
- throw new M2MSentinelError(message, details);
88
- }
89
-
90
- getStatus() { return this.request('GET', '/v1/status'); }
91
- getPlans() { return this.request('GET', '/v1/plans'); }
92
- createFreeChallenge(userWallet) { return this.request('POST', '/v1/subscribe/free/challenge', { userWallet }); }
93
- claimFreeTier(intentId, signature) { return this.request('POST', '/v1/subscribe/free/claim', { intentId, signature }); }
94
- createSubscriptionIntent(tier, userWallet) { return this.request('POST', '/v1/subscribe/intents', { tier, userWallet }); }
95
- claimSubscription(intentId, signature, txHash) { return this.request('POST', '/v1/subscribe/crypto', { intentId, signature, txHash }); }
96
- auditContract(address, options) { return this.request('GET', '/v1/audit/' + encodeURIComponent(address), undefined, options); }
97
- getSecurityScore(address, options) { return this.request('GET', '/v1/security/score/' + encodeURIComponent(address), undefined, options); }
98
- getGasFees(options) { return this.request('GET', '/v1/gas/fees', undefined, options); }
99
- getDexMetrics(options) { return this.request('GET', '/v1/dex/metrics', undefined, options); }
100
- getTokenPrice(symbol, options) { return this.request('GET', '/v1/token/price/' + encodeURIComponent(symbol), undefined, options); }
101
- getWhaleSignals(options) { return this.request('GET', '/v1/whales/signals', undefined, options); }
102
- getKeySelf() { return this.request('GET', '/v1/keys/self'); }
103
- revokeKey(confirm = true) { return this.request('POST', '/v1/keys/revoke', { confirm }); }
104
- }
105
-
106
- function createEthersSentinelMiddleware(apiKey, baseUrl) {
107
- const client = new M2MSentinelClient({ apiKey, baseUrl });
108
- return {
109
- client,
110
- verifyContractBeforeTx: async (targetAddress) => {
111
- const audit = await client.auditContract(targetAddress);
112
- const verdict = audit && audit.audit && audit.audit.verdict;
113
- if (verdict && verdict.securityRating === 'HIGH_RISK') {
114
- throw new Error('[M2M Sentinel] Transaction blocked: ' + targetAddress + ' flagged HIGH_RISK.');
115
- }
116
- return audit;
117
- }
118
- };
119
- }
120
-
121
- function createViemSentinelInterceptor(apiKey, baseUrl) {
122
- const client = new M2MSentinelClient({ apiKey, baseUrl });
123
- return {
124
- client,
125
- inspectSwapTarget: async (address) => {
126
- const score = await client.getSecurityScore(address);
127
- if (score && score.decisionGrade && typeof score.securityScore === 'number' && score.securityScore < 50) {
128
- throw new Error('[M2M Sentinel] Security score too low (' + score.securityScore + '/100) for ' + address);
129
- }
130
- return score;
131
- }
132
- };
133
- }
134
-
135
- module.exports = {
136
- M2MSentinelClient,
137
- M2MSentinelError,
138
- PaymentRequiredError,
139
- RateLimitedError,
140
- DataSourceUnavailableError,
141
- createEthersSentinelMiddleware,
142
- createViemSentinelInterceptor
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
  };