@odingard/guardian-x402 0.0.0-stage → 0.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/LICENSE +201 -0
- package/README.md +269 -2
- package/dist/axios.js +85 -0
- package/dist/chunk-C2QTXTL6.js +1443 -0
- package/dist/chunk-DJIAG2IA.js +58 -0
- package/dist/fetch.js +62 -0
- package/dist/index.js +57 -0
- package/dist/mcp.js +60 -0
- package/dist-dts/axios.d.ts +9 -0
- package/dist-dts/constants.d.ts +58 -0
- package/dist-dts/context.d.ts +56 -0
- package/dist-dts/fetch.d.ts +9 -0
- package/dist-dts/guard.d.ts +209 -0
- package/dist-dts/index.d.ts +32 -0
- package/dist-dts/ledger.d.ts +84 -0
- package/dist-dts/mcp.d.ts +30 -0
- package/dist-dts/terms.d.ts +79 -0
- package/dist-dts/util.d.ts +51 -0
- package/dist-dts/version.d.ts +14 -0
- package/package.json +89 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { SupportedNetwork } from "./constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* A durable key-value store with an atomic compare-and-set, supplied by the
|
|
4
|
+
* developer (a database row with a version column, for example). The guard
|
|
5
|
+
* keeps budgets and live authorizations in it, so a restart forgets neither.
|
|
6
|
+
*/
|
|
7
|
+
export interface X402GuardStore {
|
|
8
|
+
/** The current value and its version, or null when the key is absent. */
|
|
9
|
+
get(key: string): Promise<{
|
|
10
|
+
value: string;
|
|
11
|
+
version: string;
|
|
12
|
+
} | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Write `value` only if the key's version is still `expectedVersion`
|
|
15
|
+
* (null: only if the key is absent). Returns false on a conflict. Must be
|
|
16
|
+
* atomic.
|
|
17
|
+
*/
|
|
18
|
+
compareAndSet(key: string, expectedVersion: string | null, value: string): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
/** Reads `authorizationState(from, nonce)` on the token contract. */
|
|
21
|
+
export type AuthorizationStateReader = (query: {
|
|
22
|
+
network: SupportedNetwork;
|
|
23
|
+
asset: string;
|
|
24
|
+
from: string;
|
|
25
|
+
nonce: string;
|
|
26
|
+
}) => Promise<boolean>;
|
|
27
|
+
export type BudgetConfig = Readonly<{
|
|
28
|
+
network: SupportedNetwork;
|
|
29
|
+
asset: string;
|
|
30
|
+
period: "day" | "month";
|
|
31
|
+
/** Atomic units of the asset. */
|
|
32
|
+
limit: string;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* One payment's local entry.
|
|
36
|
+
* - reserved: amount held before Guardian is asked, nothing signed yet
|
|
37
|
+
* - signing: about to sign; nonce and validBefore are known
|
|
38
|
+
* - signed: an authorization exists and may be settled until validBefore
|
|
39
|
+
* - spent: settled, or the chain shows the nonce used; never released
|
|
40
|
+
* - released: no signature was made, or validBefore passed and the chain
|
|
41
|
+
* shows the nonce unused
|
|
42
|
+
*/
|
|
43
|
+
export type LedgerEntry = {
|
|
44
|
+
id: string;
|
|
45
|
+
requestKey: string;
|
|
46
|
+
network: SupportedNetwork;
|
|
47
|
+
asset: string;
|
|
48
|
+
amount: string;
|
|
49
|
+
createdAtMs: number;
|
|
50
|
+
/** A "reserved" entry is not released by reconcile before this time. */
|
|
51
|
+
holdUntilMs?: number;
|
|
52
|
+
state: "reserved" | "signing" | "signed" | "spent" | "released";
|
|
53
|
+
from?: string;
|
|
54
|
+
nonce?: string;
|
|
55
|
+
/** Unix seconds. */
|
|
56
|
+
validBefore?: number;
|
|
57
|
+
requestId?: string;
|
|
58
|
+
sent?: "unknown" | "not-sent";
|
|
59
|
+
};
|
|
60
|
+
/** In-process store used for live authorizations when none is given. */
|
|
61
|
+
export declare function processStore(): X402GuardStore;
|
|
62
|
+
/** Live: may still be settled (or may still be signed). */
|
|
63
|
+
export declare function isLive(e: LedgerEntry, nowMs: number): boolean;
|
|
64
|
+
export declare class Ledger {
|
|
65
|
+
private readonly store;
|
|
66
|
+
private readonly budgets;
|
|
67
|
+
constructor(store: X402GuardStore, budgets: readonly BudgetConfig[]);
|
|
68
|
+
private read;
|
|
69
|
+
/** Read-modify-write with compare-and-set; `change` may throw to abort. */
|
|
70
|
+
private update;
|
|
71
|
+
entries(): Promise<LedgerEntry[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Atomically checks budgets and records a reservation. Returns the live
|
|
74
|
+
* entries for the same request that existed before this one: same
|
|
75
|
+
* request key, or one of `sameRequestIds`.
|
|
76
|
+
*/
|
|
77
|
+
reserve(entry: LedgerEntry, nowMs: number, sameRequestIds?: readonly string[]): Promise<{
|
|
78
|
+
liveForRequest: LedgerEntry[];
|
|
79
|
+
}>;
|
|
80
|
+
patch(id: string, change: (entry: LedgerEntry) => void): Promise<LedgerEntry | undefined>;
|
|
81
|
+
patchByNonce(nonce: string, change: (entry: LedgerEntry) => void): Promise<LedgerEntry | undefined>;
|
|
82
|
+
/** Release an entry only if no signature was made for it. */
|
|
83
|
+
releaseUnsigned(id: string): Promise<void>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { x402MCPClient, type PaymentRequiredHook } from "@x402/mcp";
|
|
2
|
+
import { type X402Guard } from "./guard.js";
|
|
3
|
+
type McpClient = ConstructorParameters<typeof x402MCPClient>[0];
|
|
4
|
+
export type GuardedMcpOptions = Readonly<{
|
|
5
|
+
/**
|
|
6
|
+
* The MCP server's URL. When given, a payment whose `resource.url` is
|
|
7
|
+
* exactly `<serverUrl origin>/tools/<called tool>` is accepted as well as
|
|
8
|
+
* `mcp://tool/<called tool>`. Any other path or tool on the host is refused.
|
|
9
|
+
*/
|
|
10
|
+
serverUrl?: string;
|
|
11
|
+
maxRequestTimeoutSeconds?: number;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* The official `x402MCPClient`, built on the guard's client. Each MCP
|
|
15
|
+
* payment, including the corrective-402 retry, gets exactly one Guardian
|
|
16
|
+
* decision before the wallet signs.
|
|
17
|
+
*/
|
|
18
|
+
export declare class GuardedX402MCPClient extends x402MCPClient {
|
|
19
|
+
private readonly serverUrl;
|
|
20
|
+
constructor(guard: X402Guard, mcpClient: McpClient, options?: GuardedMcpOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Refused: an `onPaymentRequired` hook can hand x402 a ready-made payment
|
|
23
|
+
* that skips the guard.
|
|
24
|
+
*/
|
|
25
|
+
onPaymentRequired(_hook: PaymentRequiredHook): this;
|
|
26
|
+
callTool(name: string, args?: Record<string, unknown>, options?: Parameters<x402MCPClient["callTool"]>[2]): ReturnType<x402MCPClient["callTool"]>;
|
|
27
|
+
}
|
|
28
|
+
/** Builds a guarded `x402MCPClient` around an MCP SDK `Client`. */
|
|
29
|
+
export declare function guardedMcpClient(guard: X402Guard, mcpClient: McpClient, options?: GuardedMcpOptions): GuardedX402MCPClient;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type PinnedAsset, type SupportedNetwork } from "./constants.js";
|
|
2
|
+
import { type Refusal } from "./util.js";
|
|
3
|
+
/** The request a payment belongs to, as the guard saw it. */
|
|
4
|
+
export type PaymentRequestInfo = Readonly<{
|
|
5
|
+
transport: "http";
|
|
6
|
+
method: string;
|
|
7
|
+
/** The URL the agent asked for, before any redirect. */
|
|
8
|
+
originalUrl: string;
|
|
9
|
+
/** The URL that answered 402, after redirects (as x402 reports it). */
|
|
10
|
+
finalUrl: string | null;
|
|
11
|
+
} | {
|
|
12
|
+
transport: "mcp";
|
|
13
|
+
method: "tools/call";
|
|
14
|
+
/** `mcp://tool/<name>` for the tool the agent called. */
|
|
15
|
+
originalUrl: string;
|
|
16
|
+
finalUrl: null;
|
|
17
|
+
toolName: string;
|
|
18
|
+
/** The MCP server's URL, when the developer gave it. */
|
|
19
|
+
serverUrl: string | null;
|
|
20
|
+
}>;
|
|
21
|
+
/** What Guardian decides on. Plain JSON, deep-copied and frozen. */
|
|
22
|
+
export type CanonicalTerms = Readonly<{
|
|
23
|
+
schema: "odingard.guardian.x402.pay.v1";
|
|
24
|
+
x402Version: 1 | 2;
|
|
25
|
+
scheme: "exact";
|
|
26
|
+
network: SupportedNetwork;
|
|
27
|
+
networkName: string;
|
|
28
|
+
/** Lowercase token address. */
|
|
29
|
+
asset: string;
|
|
30
|
+
assetSymbol: string;
|
|
31
|
+
/** Integer string in the asset's atomic units. */
|
|
32
|
+
amount: string;
|
|
33
|
+
/** For people, from the guard's pinned decimals, e.g. "0.01 USDC". */
|
|
34
|
+
amountDisplay: string;
|
|
35
|
+
/** Lowercase recipient address. */
|
|
36
|
+
payTo: string;
|
|
37
|
+
maxTimeoutSeconds: number;
|
|
38
|
+
extra: Readonly<{
|
|
39
|
+
name: string;
|
|
40
|
+
version: string;
|
|
41
|
+
assetTransferMethod: "eip3009";
|
|
42
|
+
paymentFlow: "authorization";
|
|
43
|
+
}>;
|
|
44
|
+
resourceUrl: string;
|
|
45
|
+
request: Readonly<{
|
|
46
|
+
transport: "http" | "mcp";
|
|
47
|
+
method: string;
|
|
48
|
+
originalUrl: string;
|
|
49
|
+
finalUrl: string | null;
|
|
50
|
+
}>;
|
|
51
|
+
}>;
|
|
52
|
+
export type TermsResult = {
|
|
53
|
+
ok: true;
|
|
54
|
+
terms: CanonicalTerms;
|
|
55
|
+
pinned: PinnedAsset;
|
|
56
|
+
} | Refusal;
|
|
57
|
+
export type TermsOptions = Readonly<{
|
|
58
|
+
enabledNetworks: readonly SupportedNetwork[];
|
|
59
|
+
maxTimeoutSecondsLimit: number;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* The resource URLs that name the called MCP tool, and nothing else:
|
|
63
|
+
* - `mcp://tool/<tool>`, what `@x402/mcp` names by default
|
|
64
|
+
* (`createToolResourceUrl`), compared exactly;
|
|
65
|
+
* - only when `serverUrl` is given, `<serverUrl origin>/tools/<tool>` with no
|
|
66
|
+
* query or fragment (the tool name percent-encoded as a path segment).
|
|
67
|
+
* Any other path, query or tool on the server host is refused.
|
|
68
|
+
*/
|
|
69
|
+
export declare function mcpResourceMatches(resourceUrl: string, toolName: string, serverUrl: string | null): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Reads one payment entry into canonical terms, or refuses. Everything it
|
|
72
|
+
* cannot read exactly is refused.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readTerms(x402Version: unknown, requirement: unknown, paymentRequired: unknown, request: PaymentRequestInfo | null, options: TermsOptions): TermsResult;
|
|
75
|
+
/**
|
|
76
|
+
* Same request = same method, original URL and resource URL. On MCP the
|
|
77
|
+
* resource is named by the server, so the key is the called tool alone.
|
|
78
|
+
*/
|
|
79
|
+
export declare function requestKeyOf(terms: CanonicalTerms): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Refusal codes the guard uses. */
|
|
2
|
+
export type RefusalCode = "MALFORMED_PAYMENT_REQUIRED" | "UNSUPPORTED_VERSION" | "UNSUPPORTED_SCHEME" | "UNSUPPORTED_NETWORK" | "NETWORK_NOT_ENABLED" | "UNSUPPORTED_ASSET" | "UNSUPPORTED_TRANSFER_METHOD" | "UNSUPPORTED_PAYMENT_FLOW" | "SPLITS_REFUSED" | "UNKNOWN_EXTRA_FIELD" | "DOMAIN_MISMATCH" | "INVALID_AMOUNT" | "INVALID_PAY_TO" | "INVALID_TIMEOUT" | "TIMEOUT_ABOVE_LIMIT" | "MISSING_RESOURCE" | "RESOURCE_HOST_MISMATCH" | "ORIGINAL_REQUEST_MISSING" | "V1_NOT_SUPPORTED_ON_MCP" | "ABOVE_MAX" | "PAY_TO_NOT_ALLOWED" | "BUDGET_EXCEEDED" | "STORE_UNAVAILABLE" | "GUARDIAN_UNREACHABLE" | "GUARDIAN_TIMEOUT" | "GUARDIAN_DENIED" | "APPROVAL_TIMEOUT" | "PERSON_APPROVAL_REQUIRED" | "EXECUTE_FAILED" | "RECEIPT_CHECK_FAILED" | "NO_PAYMENT_FLOW" | "NO_SIGNING_TOKEN" | "SIGNING_TOKEN_USED" | "SIGNING_TOKEN_EXPIRED" | "TERMS_CHANGED" | "TYPED_DATA_MISMATCH" | "SIGNING_REFUSED" | "GUARD_SEALED";
|
|
3
|
+
/** Thrown (or used as the abort reason) whenever the guard refuses. */
|
|
4
|
+
export declare class X402GuardRefused extends Error {
|
|
5
|
+
readonly code: RefusalCode;
|
|
6
|
+
/** Guardian's own reason code, when Guardian refused. */
|
|
7
|
+
readonly guardianReasonCode?: string;
|
|
8
|
+
/**
|
|
9
|
+
* EXECUTE_FAILED only: `execute` was sent but its outcome is unknown, so
|
|
10
|
+
* Guardian may have redeemed the decision. Nothing was signed. Look the
|
|
11
|
+
* request up by `requestId` before retrying; a retry needs a new decision.
|
|
12
|
+
*/
|
|
13
|
+
readonly outcomeUnknown?: true;
|
|
14
|
+
/** The Guardian request this refusal is about, when there is one. */
|
|
15
|
+
readonly requestId?: string;
|
|
16
|
+
constructor(code: RefusalCode, detail: string, guardianReasonCode?: string, extra?: {
|
|
17
|
+
outcomeUnknown?: boolean;
|
|
18
|
+
requestId?: string;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export type Refusal = Readonly<{
|
|
22
|
+
ok: false;
|
|
23
|
+
code: RefusalCode;
|
|
24
|
+
detail: string;
|
|
25
|
+
}>;
|
|
26
|
+
export declare const refusal: (code: RefusalCode, detail: string) => Refusal;
|
|
27
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Stable JSON for comparisons. Throws on anything that is not plain JSON
|
|
30
|
+
* data (functions, symbols, cycles, class instances); bigint is written as
|
|
31
|
+
* a tagged string so typed-data values compare exactly.
|
|
32
|
+
*/
|
|
33
|
+
export declare function canonical(value: unknown, depth?: number): string;
|
|
34
|
+
export declare function sameData(a: unknown, b: unknown): boolean;
|
|
35
|
+
export declare function deepFreeze<T>(value: T): T;
|
|
36
|
+
/** A detached, frozen copy of plain JSON data. */
|
|
37
|
+
export declare function frozenCopy<T>(value: T): T;
|
|
38
|
+
export declare const ADDRESS_RE: RegExp;
|
|
39
|
+
export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
40
|
+
/** A positive integer with no leading zero, at most 78 digits (uint256). */
|
|
41
|
+
export declare const ATOMIC_RE: RegExp;
|
|
42
|
+
export declare function formatAtomic(amount: string, decimals: number): string;
|
|
43
|
+
export declare function withTimeout<T>(promise: Promise<T>, ms: number): Promise<{
|
|
44
|
+
ok: true;
|
|
45
|
+
value: T;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
timedOut: boolean;
|
|
49
|
+
error?: unknown;
|
|
50
|
+
}>;
|
|
51
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The x402 releases this guard was built and tested against: 2.30.x, the
|
|
3
|
+
* same range as the peer dependencies (`~2.30.0`).
|
|
4
|
+
*/
|
|
5
|
+
export declare const TESTED_X402_MAJOR = 2;
|
|
6
|
+
export declare const TESTED_X402_MINOR = 30;
|
|
7
|
+
export declare const TESTED_X402_RANGE = "~2.30.0";
|
|
8
|
+
/** True only for a plain release in the tested range (no prerelease). */
|
|
9
|
+
export declare function isSupportedX402Version(version: string | null | undefined): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Refuses an x402 package outside the tested range (another major or minor
|
|
12
|
+
* line, a prerelease, or none installed). Throws; the guard is not built.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSupportedX402(name: string, readVersion?: (name: string) => string | null): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odingard/guardian-x402",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Guardian for x402: Guardian decides x402 payments made through the guarded official x402 clients (ALLOW/ASK/DENY on the exact terms), before the wallet signs.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Odingard Security LLC",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist-dts/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./fetch": {
|
|
14
|
+
"types": "./dist-dts/fetch.d.ts",
|
|
15
|
+
"default": "./dist/fetch.js"
|
|
16
|
+
},
|
|
17
|
+
"./axios": {
|
|
18
|
+
"types": "./dist-dts/axios.d.ts",
|
|
19
|
+
"default": "./dist/axios.js"
|
|
20
|
+
},
|
|
21
|
+
"./mcp": {
|
|
22
|
+
"types": "./dist-dts/mcp.d.ts",
|
|
23
|
+
"default": "./dist/mcp.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"dist-dts",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "rm -rf dist && esbuild src/index.ts src/fetch.ts src/axios.ts src/mcp.ts --bundle --splitting --platform=node --format=esm --target=node22 --external:@odingard/guardian-sdk --external:@x402/* --external:axios --external:@modelcontextprotocol/* --outdir=dist && tsc -p tsconfig.build.json",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@odingard/guardian-sdk": "^0.4.1 || ^0.5.0 || ^0.6.0",
|
|
43
|
+
"@x402/axios": "~2.30.0",
|
|
44
|
+
"@x402/core": "~2.30.0",
|
|
45
|
+
"@x402/evm": "~2.30.0",
|
|
46
|
+
"@x402/fetch": "~2.30.0",
|
|
47
|
+
"@x402/mcp": "~2.30.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@x402/axios": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@x402/fetch": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@x402/mcp": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@odingard/guardian-sdk": "^0.4.1 || ^0.5.0 || ^0.6.0",
|
|
62
|
+
"@types/node": "^22.20.5",
|
|
63
|
+
"@x402/axios": "2.30.0",
|
|
64
|
+
"@x402/core": "2.30.0",
|
|
65
|
+
"@x402/evm": "2.30.0",
|
|
66
|
+
"@x402/fetch": "2.30.0",
|
|
67
|
+
"@x402/mcp": "2.30.0",
|
|
68
|
+
"axios": "^1.21.1",
|
|
69
|
+
"esbuild": "^0.28.2",
|
|
70
|
+
"typescript": "^5.9.3",
|
|
71
|
+
"viem": "^2.57.17",
|
|
72
|
+
"vitest": "^5.1.0"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=22"
|
|
76
|
+
},
|
|
77
|
+
"keywords": [
|
|
78
|
+
"guardian",
|
|
79
|
+
"x402",
|
|
80
|
+
"payments",
|
|
81
|
+
"ai-agents",
|
|
82
|
+
"authorization",
|
|
83
|
+
"human-in-the-loop"
|
|
84
|
+
],
|
|
85
|
+
"repository": {
|
|
86
|
+
"type": "git",
|
|
87
|
+
"url": "https://github.com/Odingard/guardian-sdk.git",
|
|
88
|
+
"directory": "packages/x402"
|
|
89
|
+
},
|
|
90
|
+
"homepage": "https://odingard.com/guardian"
|
|
91
|
+
}
|