m2m-sentinel-sdk 1.0.2 → 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 -43
- package/agent_adapter.js +30 -0
- package/eliza_plugin.js +84 -0
- package/index.d.ts +94 -0
- package/index.js +183 -26
- package/m2m_sentinel_sdk.js +1 -12
- package/mcp_server.js +223 -0
- package/package.json +24 -10
- package/typescript/index.ts +204 -49
- package/typescript/package.json +6 -5
- package/cli/m2m_cli.js +0 -25
- package/csharp/M2MSentinel.csproj +0 -9
- package/csharp/M2MSentinelClient.cs +0 -67
- package/go/client.go +0 -94
- package/go/go.mod +0 -3
- package/java/M2MSentinelClient.java +0 -67
- package/java/pom.xml +0 -20
- package/java/src/main/java/com/m2msentinel/M2MSentinelClient.java +0 -69
- package/kotlin/M2MSentinelClient.kt +0 -59
- package/kotlin/build.gradle.kts +0 -24
- package/m2m_sentinel/__init__.py +0 -3
- package/m2m_sentinel/client.py +0 -16
- package/m2m_sentinel_sdk.py +0 -9
- package/php/M2MSentinelClient.php +0 -52
- package/php/composer.json +0 -20
- package/python/dist/m2m_sentinel-0.1.0-py3-none-any.whl +0 -0
- package/python/dist/m2m_sentinel-0.1.0.tar.gz +0 -0
- package/python/dist/m2m_sentinel-0.1.1-py3-none-any.whl +0 -0
- package/python/dist/m2m_sentinel-0.1.1.tar.gz +0 -0
- package/python/m2m_sentinel/__pycache__/client.cpython-314.pyc +0 -0
- package/python/m2m_sentinel/client.py +0 -40
- package/python/m2m_sentinel.egg-info/PKG-INFO +0 -35
- package/python/m2m_sentinel.egg-info/SOURCES.txt +0 -5
- package/python/m2m_sentinel.egg-info/dependency_links.txt +0 -1
- package/python/m2m_sentinel.egg-info/top_level.txt +0 -1
- package/python/setup.py +0 -35
- package/ruby/m2m_sentinel.gemspec +0 -17
- package/ruby/m2m_sentinel.rb +0 -55
- package/rust/Cargo.toml +0 -11
- package/rust/src/lib.rs +0 -61
- package/swift/M2MSentinelClient.swift +0 -65
- package/swift/Package.swift +0 -23
package/README.md
CHANGED
|
@@ -1,43 +1,71 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
class M2MSentinelAgentTool {
|
|
6
|
+
constructor(options = {}) {
|
|
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
|
+
this.client = new M2MSentinelClient({
|
|
10
|
+
apiKey: options.apiKey || process.env.M2M_SENTINEL_API_KEY,
|
|
11
|
+
baseUrl: options.baseUrl
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async _call(address) {
|
|
16
|
+
try {
|
|
17
|
+
return await this.client.auditContract(address);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return {
|
|
20
|
+
error: err.name || 'M2MSentinelError',
|
|
21
|
+
message: err.message,
|
|
22
|
+
status: err.status,
|
|
23
|
+
paymentRequired: err.paymentRequired || null,
|
|
24
|
+
retryAfter: err.retryAfter || null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { M2MSentinelAgentTool };
|
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
CHANGED
|
@@ -1,26 +1,183 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
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 = {
|
|
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
|
+
module.exports = {
|
|
175
|
+
M2MSentinelClient,
|
|
176
|
+
M2MSentinelError,
|
|
177
|
+
PaymentRequiredError,
|
|
178
|
+
RateLimitedError,
|
|
179
|
+
DataSourceUnavailableError,
|
|
180
|
+
enforceCallerPolicy,
|
|
181
|
+
createEthersSentinelMiddleware,
|
|
182
|
+
createViemSentinelInterceptor
|
|
183
|
+
};
|
package/m2m_sentinel_sdk.js
CHANGED
|
@@ -1,12 +1 @@
|
|
|
1
|
-
|
|
2
|
-
constructor(apiKey) {
|
|
3
|
-
this.apiKey = apiKey;
|
|
4
|
-
this.baseUrl = 'https://m2msentinel.vercel.app/v1';
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
async getTokenPrice() {
|
|
8
|
-
const response = await fetch(`${this.baseUrl}/token/price`);
|
|
9
|
-
return response.json();
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
module.exports = M2MSentinelSDK;
|
|
1
|
+
module.exports = require('./index.js');
|