otto-execute 0.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/cdp-signer.d.ts +2 -0
- package/dist/cdp-signer.js +3 -0
- package/dist/cdp.d.ts +7 -0
- package/dist/cdp.js +8 -0
- package/dist/chain.d.ts +2 -0
- package/dist/chain.js +3 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +400 -0
- package/dist/delegate.d.ts +117 -0
- package/dist/delegate.js +394 -0
- package/dist/eoa-signer.d.ts +94 -0
- package/dist/eoa-signer.js +289 -0
- package/dist/erc20.d.ts +2 -0
- package/dist/erc20.js +3 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +19 -0
- package/dist/lifi-decode.d.ts +2 -0
- package/dist/lifi-decode.js +3 -0
- package/dist/mutations.d.ts +55 -0
- package/dist/mutations.js +363 -0
- package/dist/refusal.d.ts +2 -0
- package/dist/refusal.js +3 -0
- package/dist/vendor/otto-intel-mcp/VENDORED.json +52 -0
- package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.d.ts +133 -0
- package/dist/vendor/otto-intel-mcp/adapter/cdp-signer.js +356 -0
- package/dist/vendor/otto-intel-mcp/adapter/chain.d.ts +36 -0
- package/dist/vendor/otto-intel-mcp/adapter/chain.js +65 -0
- package/dist/vendor/otto-intel-mcp/adapter/erc20.d.ts +39 -0
- package/dist/vendor/otto-intel-mcp/adapter/erc20.js +17 -0
- package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.d.ts +52 -0
- package/dist/vendor/otto-intel-mcp/adapter/lifi-decode.js +149 -0
- package/dist/vendor/otto-intel-mcp/adapter/refusal.d.ts +21 -0
- package/dist/vendor/otto-intel-mcp/adapter/refusal.js +55 -0
- package/dist/vendor/otto-intel-mcp/adapter/sent-step.d.ts +15 -0
- package/dist/vendor/otto-intel-mcp/adapter/sent-step.js +6 -0
- package/dist/vendor/otto-intel-mcp/adapter/verify.d.ts +149 -0
- package/dist/vendor/otto-intel-mcp/adapter/verify.js +432 -0
- package/dist/vendor/otto-intel-mcp/adapter-cdp-index.d.ts +6 -0
- package/dist/vendor/otto-intel-mcp/adapter-cdp-index.js +7 -0
- package/dist/vendor/otto-intel-mcp/adapter-index.d.ts +15 -0
- package/dist/vendor/otto-intel-mcp/adapter-index.js +15 -0
- package/dist/vendor/otto-intel-mcp/artifact-id.d.ts +16 -0
- package/dist/vendor/otto-intel-mcp/artifact-id.js +60 -0
- package/dist/vendor/otto-intel-mcp/execution-config.d.ts +232 -0
- package/dist/vendor/otto-intel-mcp/execution-config.js +443 -0
- package/dist/vendor/otto-intel-mcp/execution-delegated-definition.d.ts +165 -0
- package/dist/vendor/otto-intel-mcp/execution-delegated-definition.js +116 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.d.ts +208 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-admin-definition.js +170 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-policy.d.ts +257 -0
- package/dist/vendor/otto-intel-mcp/execution-delegation-policy.js +279 -0
- package/dist/vendor/otto-intel-mcp/execution-errors.d.ts +9 -0
- package/dist/vendor/otto-intel-mcp/execution-errors.js +134 -0
- package/dist/vendor/otto-intel-mcp/execution-index.d.ts +17 -0
- package/dist/vendor/otto-intel-mcp/execution-index.js +16 -0
- package/dist/vendor/otto-intel-mcp/execution-intent.d.ts +14 -0
- package/dist/vendor/otto-intel-mcp/execution-intent.js +36 -0
- package/dist/vendor/otto-intel-mcp/execution-tool-definitions.d.ts +1103 -0
- package/dist/vendor/otto-intel-mcp/execution-tool-definitions.js +1051 -0
- package/dist/vendor/otto-intel-mcp/execution-types.d.ts +274 -0
- package/dist/vendor/otto-intel-mcp/execution-types.js +157 -0
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +3 -0
- package/dist/x402-table.d.ts +99 -0
- package/dist/x402-table.js +221 -0
- package/package.json +97 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otto-execute/delegate — the AGENT path to a Model-B delegation, headless from the agent's own process
|
|
3
|
+
* (PR-2a; Founder word `agent delegate cli`, 2026-08-29). Sign in as the end user by email OTP, mint a
|
|
4
|
+
* time-boxed CDP delegation to Otto's project, read it back, revoke it — all END-USER-SIDE: no Otto
|
|
5
|
+
* credential is ever in this process, and the delegation can only be minted by the signed-in user (the API
|
|
6
|
+
* requires the user's Bearer token plus an `X-Wallet-Auth` JWT signed by a Temporary Wallet Secret that
|
|
7
|
+
* lives inside `@coinbase/cdp-core`).
|
|
8
|
+
*
|
|
9
|
+
* THIS PATH IS NOT DOCUMENTED OR SUPPORTED BY COINBASE AND CAN BREAK ON AN SDK UPDATE; the browser page is
|
|
10
|
+
* the supported path. `@coinbase/cdp-core` ships browser + react-native bundles only, so Node resolves the
|
|
11
|
+
* WEB bundle and runs it with exactly two shims installed here before the SDK loads: an in-memory
|
|
12
|
+
* `localStorage` (sign-in needs it) and a `window = { crypto }` alias (the delegation leg spells WebCrypto as
|
|
13
|
+
* `window.crypto`; Node 22's `globalThis.crypto` is the same WebCrypto the SDK's native build uses). Nothing
|
|
14
|
+
* else is aliased — no `document`, no `location`. The dependency is PINNED (`@coinbase/cdp-core` 0.0.120 —
|
|
15
|
+
* the version proven live at Sitting B, 2026-08-29); a canary test drives the real web build on Node against
|
|
16
|
+
* a loopback fake of the CDP API, and the alias breaking is a NAMED refusal (`CDP_CORE_NODE_PATH_BROKEN`),
|
|
17
|
+
* never a silent failure. A Node client sends no Origin, so Coinbase's domain allowlist does not apply to
|
|
18
|
+
* it — a policy/support risk with Coinbase, not a fund or user risk: the secret stays on the user's own
|
|
19
|
+
* device and the human still consents via the OTP.
|
|
20
|
+
*
|
|
21
|
+
* FENCE BEFORE AUTHORITY holds on this path too: `readPublicFenceStatus` asks the hosted MCP's PUBLIC
|
|
22
|
+
* `otto_delegation_fence_status` (no header — the fence's public promise, never user data) and the CLI
|
|
23
|
+
* refuses to mint when the fence is not present, or when the MCP cannot be reached (fail-closed). The server
|
|
24
|
+
* enforces the same on every delegated send regardless.
|
|
25
|
+
*
|
|
26
|
+
* Kept on its OWN entry (`otto-execute/delegate`): the root export never loads cdp-core; the SDK is loaded
|
|
27
|
+
* lazily inside `initEndUserSdk` and a consumer without the optional peers (`@coinbase/cdp-core`, pinned, and
|
|
28
|
+
* `react` — the web build imports `zustand/react`) gets a named refusal (`CDP_CORE_NOT_INSTALLED`).
|
|
29
|
+
*/
|
|
30
|
+
export declare const CDP_CORE_PINNED_VERSION: "0.0.120";
|
|
31
|
+
export declare const DEFAULT_OTTO_MCP_URL: "https://mcp.ottoai.services/mcp";
|
|
32
|
+
/**
|
|
33
|
+
* The endpoints the delegate path talks to. In production they are FIXED: the fence is Otto's default MCP and
|
|
34
|
+
* the SDK is Coinbase's own endpoint — neither is a caller's to redirect (no flag, no environment variable).
|
|
35
|
+
* The ONLY override is an in-process global (`__ottoDelegateTestEndpoints__`) that the test harness installs
|
|
36
|
+
* through a `--import` preload a production invocation never loads; forging it needs code execution inside this
|
|
37
|
+
* process, which already subsumes it. So a caller who controls only the environment or argv can point neither
|
|
38
|
+
* the fence read nor the OTP's SDK endpoint anywhere.
|
|
39
|
+
*/
|
|
40
|
+
export interface DelegateEndpoints {
|
|
41
|
+
readonly mcpUrl: string;
|
|
42
|
+
readonly apiBasePath?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare const DELEGATE_TEST_ENDPOINTS_KEY: "__ottoDelegateTestEndpoints__";
|
|
45
|
+
export declare function resolveDelegateEndpoints(): DelegateEndpoints;
|
|
46
|
+
export declare const FENCE_STATUS_TOOL_NAME: "otto_delegation_fence_status";
|
|
47
|
+
export declare const UNSUPPORTED_PATH_NOTE: "this path is not documented or supported by Coinbase and can break on an SDK update; the browser page is the supported path";
|
|
48
|
+
export declare const DELEGATE_REFUSAL_CODES: readonly ["CDP_CORE_NOT_INSTALLED", "CDP_CORE_NODE_PATH_BROKEN", "PROJECT_ID_REQUIRED", "EMAIL_INVALID", "OTP_INVALID", "NO_EVM_ACCOUNT", "EXPIRY_INVALID", "DELEGATION_FENCE_ABSENT", "FENCE_MISMATCH", "FENCE_UNREACHABLE", "STORAGE_NOT_EPHEMERAL", "WINDOW_NOT_OURS", "CREDENTIAL_IN_ENVIRONMENT", "UNKNOWN_OPTION", "FENCE_SOURCE_NOT_OTTO", "SDK_ENDPOINT_NOT_COINBASE", "REVOKE_NOT_CONFIRMED", "NOT_SIGNED_IN"];
|
|
49
|
+
export type DelegateRefusalCode = (typeof DELEGATE_REFUSAL_CODES)[number];
|
|
50
|
+
export declare class DelegateRefusal extends Error {
|
|
51
|
+
readonly code: DelegateRefusalCode;
|
|
52
|
+
constructor(code: DelegateRefusalCode, message: string);
|
|
53
|
+
}
|
|
54
|
+
export declare function isDelegateRefusal(error: unknown): error is DelegateRefusal;
|
|
55
|
+
/**
|
|
56
|
+
* Mask a message before it is printed: URLs → `<url>` and any environment VALUE ≥ 8 chars → `<NAME>` (longest
|
|
57
|
+
* first, so a value that contains another is masked whole). Applied to every error the CLI prints, so a
|
|
58
|
+
* verbatim SDK error can never carry a secret or an endpoint to stdout/stderr.
|
|
59
|
+
*/
|
|
60
|
+
export declare function redactSecrets(text: string, env?: NodeJS.ProcessEnv): string;
|
|
61
|
+
/**
|
|
62
|
+
* The SDK's web build reaching a browser global it expected (`window`, `document`, `navigator`, …) is the
|
|
63
|
+
* signature of the unsupported path breaking under an SDK change: a NAMED refusal, never a raw
|
|
64
|
+
* ReferenceError. Anything else is rethrown as itself.
|
|
65
|
+
*/
|
|
66
|
+
export declare function nodePathGuard<T>(run: () => Promise<T>): Promise<T>;
|
|
67
|
+
export interface EndUserSdkOptions {
|
|
68
|
+
/** TESTS ONLY: point the SDK at a loopback fake of the CDP API (its `/platform`-shaped base). */
|
|
69
|
+
readonly apiBasePath?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Boot cdp-core for Otto's CDP project (a PUBLIC project id). Idempotent per process. Returns nothing on
|
|
73
|
+
* purpose: the SDK module type must not appear in this package's declarations, or a consumer WITHOUT the
|
|
74
|
+
* optional `@coinbase/cdp-core` peer fails `tsc` on `otto-execute/delegate` (the isolated-install smoke
|
|
75
|
+
* compiles a consumer with `skipLibCheck: false` to keep it that way).
|
|
76
|
+
*/
|
|
77
|
+
export declare function initEndUserSdk(projectId: string | undefined, options?: EndUserSdkOptions): Promise<void>;
|
|
78
|
+
export interface SignedInEndUser {
|
|
79
|
+
readonly userId: string;
|
|
80
|
+
readonly address: `0x${string}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Email OTP sign-in. `readOtp` is the caller's prompt (the CLI reads it muted from the terminal); the code
|
|
84
|
+
* is consulted once, never echoed, never written.
|
|
85
|
+
*/
|
|
86
|
+
export declare function signInWithEmailOtp(email: string, readOtp: () => Promise<string>): Promise<SignedInEndUser>;
|
|
87
|
+
export interface MintedDelegation {
|
|
88
|
+
readonly expiresAt: string;
|
|
89
|
+
readonly days: number;
|
|
90
|
+
}
|
|
91
|
+
/** Mint the delegation for the signed-in user, with the ruled expiry bounds (default 90 days, max 180). */
|
|
92
|
+
export declare function mintDelegation(requestedDays: unknown, nowMs?: number): Promise<MintedDelegation>;
|
|
93
|
+
/** The signed-in user's own delegation as CDP reports it, or `undefined` when none is active. */
|
|
94
|
+
export declare function readDelegationAsUser(): Promise<{
|
|
95
|
+
readonly expiresAt: string;
|
|
96
|
+
} | undefined>;
|
|
97
|
+
/** End-user-side revoke ("the user pulls the plug"), confirmed by read-back. */
|
|
98
|
+
export declare function revokeDelegationAsUser(): Promise<{
|
|
99
|
+
readonly readsBack: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
export interface PublicFenceStatus {
|
|
102
|
+
readonly present: boolean;
|
|
103
|
+
readonly policy_name: string;
|
|
104
|
+
readonly per_swap_cap_usd: number;
|
|
105
|
+
/** Otto's PUBLIC CDP project id (a UUID) — the authority the CLI binds its mint to. Always required. */
|
|
106
|
+
readonly project_id: string;
|
|
107
|
+
readonly rules_digest?: string;
|
|
108
|
+
readonly reason?: string;
|
|
109
|
+
}
|
|
110
|
+
export interface FenceReadOptions {
|
|
111
|
+
readonly fetchImpl?: typeof fetch;
|
|
112
|
+
readonly timeoutMs?: number;
|
|
113
|
+
}
|
|
114
|
+
/** Ask the hosted MCP's PUBLIC `otto_delegation_fence_status`. Any failure to get an answer is FENCE_UNREACHABLE (fail-closed). */
|
|
115
|
+
export declare function readPublicFenceStatus(mcpUrl?: string, options?: FenceReadOptions): Promise<PublicFenceStatus>;
|
|
116
|
+
/** Refuse to proceed unless the fence reads back present. */
|
|
117
|
+
export declare function assertFencePresent(status: PublicFenceStatus): void;
|
package/dist/delegate.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* otto-execute/delegate — the AGENT path to a Model-B delegation, headless from the agent's own process
|
|
3
|
+
* (PR-2a; Founder word `agent delegate cli`, 2026-08-29). Sign in as the end user by email OTP, mint a
|
|
4
|
+
* time-boxed CDP delegation to Otto's project, read it back, revoke it — all END-USER-SIDE: no Otto
|
|
5
|
+
* credential is ever in this process, and the delegation can only be minted by the signed-in user (the API
|
|
6
|
+
* requires the user's Bearer token plus an `X-Wallet-Auth` JWT signed by a Temporary Wallet Secret that
|
|
7
|
+
* lives inside `@coinbase/cdp-core`).
|
|
8
|
+
*
|
|
9
|
+
* THIS PATH IS NOT DOCUMENTED OR SUPPORTED BY COINBASE AND CAN BREAK ON AN SDK UPDATE; the browser page is
|
|
10
|
+
* the supported path. `@coinbase/cdp-core` ships browser + react-native bundles only, so Node resolves the
|
|
11
|
+
* WEB bundle and runs it with exactly two shims installed here before the SDK loads: an in-memory
|
|
12
|
+
* `localStorage` (sign-in needs it) and a `window = { crypto }` alias (the delegation leg spells WebCrypto as
|
|
13
|
+
* `window.crypto`; Node 22's `globalThis.crypto` is the same WebCrypto the SDK's native build uses). Nothing
|
|
14
|
+
* else is aliased — no `document`, no `location`. The dependency is PINNED (`@coinbase/cdp-core` 0.0.120 —
|
|
15
|
+
* the version proven live at Sitting B, 2026-08-29); a canary test drives the real web build on Node against
|
|
16
|
+
* a loopback fake of the CDP API, and the alias breaking is a NAMED refusal (`CDP_CORE_NODE_PATH_BROKEN`),
|
|
17
|
+
* never a silent failure. A Node client sends no Origin, so Coinbase's domain allowlist does not apply to
|
|
18
|
+
* it — a policy/support risk with Coinbase, not a fund or user risk: the secret stays on the user's own
|
|
19
|
+
* device and the human still consents via the OTP.
|
|
20
|
+
*
|
|
21
|
+
* FENCE BEFORE AUTHORITY holds on this path too: `readPublicFenceStatus` asks the hosted MCP's PUBLIC
|
|
22
|
+
* `otto_delegation_fence_status` (no header — the fence's public promise, never user data) and the CLI
|
|
23
|
+
* refuses to mint when the fence is not present, or when the MCP cannot be reached (fail-closed). The server
|
|
24
|
+
* enforces the same on every delegated send regardless.
|
|
25
|
+
*
|
|
26
|
+
* Kept on its OWN entry (`otto-execute/delegate`): the root export never loads cdp-core; the SDK is loaded
|
|
27
|
+
* lazily inside `initEndUserSdk` and a consumer without the optional peers (`@coinbase/cdp-core`, pinned, and
|
|
28
|
+
* `react` — the web build imports `zustand/react`) gets a named refusal (`CDP_CORE_NOT_INSTALLED`).
|
|
29
|
+
*/
|
|
30
|
+
import { boundedDelegationExpiry, MODEL_B_PER_SWAP_CAP_USD, MODEL_B_POLICY_DIGEST_V1, MODEL_B_POLICY_NAME_V1, } from './vendor/otto-intel-mcp/execution-index.js';
|
|
31
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
32
|
+
export const CDP_CORE_PINNED_VERSION = '0.0.120';
|
|
33
|
+
export const DEFAULT_OTTO_MCP_URL = 'https://mcp.ottoai.services/mcp';
|
|
34
|
+
export const DELEGATE_TEST_ENDPOINTS_KEY = '__ottoDelegateTestEndpoints__';
|
|
35
|
+
export function resolveDelegateEndpoints() {
|
|
36
|
+
const injected = globalThis[DELEGATE_TEST_ENDPOINTS_KEY];
|
|
37
|
+
const mcpUrl = typeof injected?.mcpUrl === 'string' ? injected.mcpUrl : DEFAULT_OTTO_MCP_URL;
|
|
38
|
+
const apiBasePath = typeof injected?.apiBasePath === 'string' ? injected.apiBasePath : undefined;
|
|
39
|
+
return apiBasePath ? { mcpUrl, apiBasePath } : { mcpUrl };
|
|
40
|
+
}
|
|
41
|
+
export const FENCE_STATUS_TOOL_NAME = 'otto_delegation_fence_status';
|
|
42
|
+
export const UNSUPPORTED_PATH_NOTE = 'this path is not documented or supported by Coinbase and can break on an SDK update; the browser page is the supported path';
|
|
43
|
+
export const DELEGATE_REFUSAL_CODES = [
|
|
44
|
+
/** `@coinbase/cdp-core` (the optional peer, pinned) is not installed. */
|
|
45
|
+
'CDP_CORE_NOT_INSTALLED',
|
|
46
|
+
/** The pinned web build no longer runs on Node with the two shims — the SDK reached a browser global. */
|
|
47
|
+
'CDP_CORE_NODE_PATH_BROKEN',
|
|
48
|
+
'PROJECT_ID_REQUIRED',
|
|
49
|
+
'EMAIL_INVALID',
|
|
50
|
+
'OTP_INVALID',
|
|
51
|
+
/** Sign-in succeeded but the end user has no EVM account (the project must create an EOA on login). */
|
|
52
|
+
'NO_EVM_ACCOUNT',
|
|
53
|
+
'EXPIRY_INVALID',
|
|
54
|
+
/** The hosted MCP reports the Model-B fence is not present — nothing is minted. */
|
|
55
|
+
'DELEGATION_FENCE_ABSENT',
|
|
56
|
+
/** The public fence read answered `present` but the name/digest/cap/project_id are not Otto's v1 — never coerced. */
|
|
57
|
+
'FENCE_MISMATCH',
|
|
58
|
+
/** The hosted MCP could not be reached or did not answer the public fence read — nothing is minted (fail-closed). */
|
|
59
|
+
'FENCE_UNREACHABLE',
|
|
60
|
+
/** A pre-existing localStorage that this process did not install (persistent web storage) — key material could hit disk. */
|
|
61
|
+
'STORAGE_NOT_EPHEMERAL',
|
|
62
|
+
/** A pre-existing `window` that is not exactly this process's `{ crypto }` alias — an unexpected browser global. */
|
|
63
|
+
'WINDOW_NOT_OURS',
|
|
64
|
+
/** An Otto credential or the server secret (or any 64-hex value) is present in this process's environment. */
|
|
65
|
+
'CREDENTIAL_IN_ENVIRONMENT',
|
|
66
|
+
/** An unrecognized CLI flag (including the OTP families `--otp/--code/--token`) — the OTP must never enter argv. */
|
|
67
|
+
'UNKNOWN_OPTION',
|
|
68
|
+
/** `--mcp`/`OTTO_MCP_URL` points at a non-Otto, non-loopback host — the fence source is not trusted. */
|
|
69
|
+
'FENCE_SOURCE_NOT_OTTO',
|
|
70
|
+
/** `OTTO_DELEGATE_CDP_BASE_PATH` redirects the SDK endpoint to a non-loopback host — refused (tests use loopback only). */
|
|
71
|
+
'SDK_ENDPOINT_NOT_COINBASE',
|
|
72
|
+
/** A revoke that did not take: the grant still reads back — a non-zero refusal, never a success. */
|
|
73
|
+
'REVOKE_NOT_CONFIRMED',
|
|
74
|
+
'NOT_SIGNED_IN',
|
|
75
|
+
];
|
|
76
|
+
export class DelegateRefusal extends Error {
|
|
77
|
+
code;
|
|
78
|
+
constructor(code, message) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.name = 'DelegateRefusal';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function isDelegateRefusal(error) {
|
|
85
|
+
return error instanceof DelegateRefusal;
|
|
86
|
+
}
|
|
87
|
+
function refuse(code, message) {
|
|
88
|
+
throw new DelegateRefusal(code, message);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Mask a message before it is printed: URLs → `<url>` and any environment VALUE ≥ 8 chars → `<NAME>` (longest
|
|
92
|
+
* first, so a value that contains another is masked whole). Applied to every error the CLI prints, so a
|
|
93
|
+
* verbatim SDK error can never carry a secret or an endpoint to stdout/stderr.
|
|
94
|
+
*/
|
|
95
|
+
export function redactSecrets(text, env = process.env) {
|
|
96
|
+
let out = text.replace(/https?:\/\/[^\s'"]+/gi, '<url>');
|
|
97
|
+
const values = Object.entries(env)
|
|
98
|
+
.map(([name, value]) => ({ name, value: (value ?? '').trim() }))
|
|
99
|
+
.filter((e) => e.value.length >= 8)
|
|
100
|
+
.sort((a, b) => b.value.length - a.value.length);
|
|
101
|
+
for (const { name, value } of values) {
|
|
102
|
+
if (out.includes(value))
|
|
103
|
+
out = out.split(value).join(`<${name}>`);
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/* ------------------------------------------------------------------ *
|
|
108
|
+
* The two shims — installed before the SDK loads, idempotent, nothing else touched.
|
|
109
|
+
* ------------------------------------------------------------------ */
|
|
110
|
+
/** The in-memory shim THIS process installed — identity, so a foreign (persistent) localStorage is refused. */
|
|
111
|
+
let ourLocalStorage;
|
|
112
|
+
function installLocalStorageShim() {
|
|
113
|
+
const existing = globalThis.localStorage;
|
|
114
|
+
if (existing !== undefined) {
|
|
115
|
+
// Idempotent for our own shim; anything else (e.g. Node `--experimental-webstorage
|
|
116
|
+
// --localstorage-file`, which writes to disk) is refused so no session/key material can persist.
|
|
117
|
+
if (existing === ourLocalStorage)
|
|
118
|
+
return;
|
|
119
|
+
refuse('STORAGE_NOT_EPHEMERAL', 'a localStorage this process did not install is present (persistent web storage can write SDK session/key material to disk); run the delegate path without --experimental-webstorage/--localstorage-file');
|
|
120
|
+
}
|
|
121
|
+
const store = new Map();
|
|
122
|
+
const shim = {
|
|
123
|
+
get length() {
|
|
124
|
+
return store.size;
|
|
125
|
+
},
|
|
126
|
+
clear: () => store.clear(),
|
|
127
|
+
getItem: (key) => (store.has(key) ? store.get(key) : null),
|
|
128
|
+
key: (index) => Array.from(store.keys())[index] ?? null,
|
|
129
|
+
removeItem: (key) => void store.delete(key),
|
|
130
|
+
setItem: (key, value) => void store.set(key, String(value)),
|
|
131
|
+
};
|
|
132
|
+
ourLocalStorage = shim;
|
|
133
|
+
Object.defineProperty(globalThis, 'localStorage', { value: shim, configurable: true });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A pre-existing `window` is trusted ONLY when its sole own key is `crypto`, held as a DATA property whose
|
|
137
|
+
* value is IDENTICALLY Node's own `globalThis.crypto`. Requiring a data property (not an accessor) closes the
|
|
138
|
+
* getter TOCTOU where `window.crypto` returns the genuine object on the validating read and an interceptor on
|
|
139
|
+
* the SDK's later read. (A Proxy that lies on getOwnPropertyDescriptor requires in-process code execution to
|
|
140
|
+
* install — which already subsumes this process — so it is out of scope.)
|
|
141
|
+
*/
|
|
142
|
+
function windowIsOurs(win) {
|
|
143
|
+
const keys = Reflect.ownKeys(win);
|
|
144
|
+
if (keys.length !== 1 || keys[0] !== 'crypto')
|
|
145
|
+
return false;
|
|
146
|
+
const descriptor = Object.getOwnPropertyDescriptor(win, 'crypto');
|
|
147
|
+
return !!descriptor && 'value' in descriptor && descriptor.value === globalThis.crypto;
|
|
148
|
+
}
|
|
149
|
+
function installWindowCryptoAlias() {
|
|
150
|
+
const existing = globalThis.window;
|
|
151
|
+
if (existing !== undefined) {
|
|
152
|
+
if (windowIsOurs(existing))
|
|
153
|
+
return;
|
|
154
|
+
refuse('WINDOW_NOT_OURS', 'a pre-existing `window` that is not exactly this process’s { crypto } alias is present; refusing so the SDK cannot reach an unexpected browser global');
|
|
155
|
+
}
|
|
156
|
+
if (!globalThis.crypto?.subtle) {
|
|
157
|
+
refuse('CDP_CORE_NODE_PATH_BROKEN', 'Node WebCrypto (globalThis.crypto.subtle) is required for the headless end-user leg');
|
|
158
|
+
}
|
|
159
|
+
Object.defineProperty(globalThis, 'window', {
|
|
160
|
+
value: Object.freeze({ crypto: globalThis.crypto }),
|
|
161
|
+
configurable: true,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The SDK's web build reaching a browser global it expected (`window`, `document`, `navigator`, …) is the
|
|
166
|
+
* signature of the unsupported path breaking under an SDK change: a NAMED refusal, never a raw
|
|
167
|
+
* ReferenceError. Anything else is rethrown as itself.
|
|
168
|
+
*/
|
|
169
|
+
export async function nodePathGuard(run) {
|
|
170
|
+
try {
|
|
171
|
+
return await run();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (isNodePathBreak(error)) {
|
|
175
|
+
refuse('CDP_CORE_NODE_PATH_BROKEN', `the pinned @coinbase/cdp-core ${CDP_CORE_PINNED_VERSION} web build reached a browser global on Node (${error.message}); ${UNSUPPORTED_PATH_NOTE}`);
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function isNodePathBreak(error, depth = 0) {
|
|
181
|
+
if (!(error instanceof Error) || depth > 8)
|
|
182
|
+
return false;
|
|
183
|
+
const browserGlobal = /\b(window|document|navigator|location|self|indexedDB|sessionStorage)\b/;
|
|
184
|
+
if (error instanceof ReferenceError && browserGlobal.test(error.message))
|
|
185
|
+
return true;
|
|
186
|
+
if (error instanceof TypeError &&
|
|
187
|
+
/Cannot read propert(y|ies) of undefined/.test(error.message) &&
|
|
188
|
+
(browserGlobal.test(error.message) ||
|
|
189
|
+
/reading '(crypto|subtle|getRandomValues|createElement|addEventListener|querySelector|cookie|userAgent)'/.test(error.message)))
|
|
190
|
+
return true;
|
|
191
|
+
// The SDK wraps failures ("Something went wrong.") — the break, if any, is in the cause chain.
|
|
192
|
+
return isNodePathBreak(error.cause, depth + 1);
|
|
193
|
+
}
|
|
194
|
+
let core;
|
|
195
|
+
async function loadCdpCore() {
|
|
196
|
+
try {
|
|
197
|
+
return (await import('@coinbase/cdp-core'));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
// An import-time failure that reaches a browser global is the unsupported path breaking, NOT an
|
|
201
|
+
// absent peer — classify it first so it is not mislabeled `CDP_CORE_NOT_INSTALLED`.
|
|
202
|
+
if (isNodePathBreak(error)) {
|
|
203
|
+
refuse('CDP_CORE_NODE_PATH_BROKEN', `the pinned @coinbase/cdp-core ${CDP_CORE_PINNED_VERSION} web build reached a browser global at import on Node (${error.message}); ${UNSUPPORTED_PATH_NOTE}`);
|
|
204
|
+
}
|
|
205
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
206
|
+
return refuse('CDP_CORE_NOT_INSTALLED', `the delegate entry needs its optional peers — @coinbase/cdp-core@${CDP_CORE_PINNED_VERSION} and react (cdp-core's web build imports zustand/react) — and one is not installed (${reason})`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Boot cdp-core for Otto's CDP project (a PUBLIC project id). Idempotent per process. Returns nothing on
|
|
211
|
+
* purpose: the SDK module type must not appear in this package's declarations, or a consumer WITHOUT the
|
|
212
|
+
* optional `@coinbase/cdp-core` peer fails `tsc` on `otto-execute/delegate` (the isolated-install smoke
|
|
213
|
+
* compiles a consumer with `skipLibCheck: false` to keep it that way).
|
|
214
|
+
*/
|
|
215
|
+
export async function initEndUserSdk(projectId, options = {}) {
|
|
216
|
+
if (core)
|
|
217
|
+
return;
|
|
218
|
+
const id = projectId?.trim() ?? '';
|
|
219
|
+
if (!/^[0-9a-f-]{36}$/i.test(id)) {
|
|
220
|
+
refuse('PROJECT_ID_REQUIRED', 'a CDP project id (UUID) is required: --project-id or OTTO_CDP_PROJECT_ID (a public identifier)');
|
|
221
|
+
}
|
|
222
|
+
installLocalStorageShim();
|
|
223
|
+
installWindowCryptoAlias();
|
|
224
|
+
const mod = await loadCdpCore();
|
|
225
|
+
await nodePathGuard(() => mod.initialize({
|
|
226
|
+
projectId: id,
|
|
227
|
+
ethereum: { createOnLogin: 'eoa' },
|
|
228
|
+
disableAnalytics: true,
|
|
229
|
+
...(options.apiBasePath ? { basePath: options.apiBasePath } : {}),
|
|
230
|
+
}));
|
|
231
|
+
core = mod;
|
|
232
|
+
}
|
|
233
|
+
function sdk() {
|
|
234
|
+
if (!core)
|
|
235
|
+
refuse('NOT_SIGNED_IN', 'initEndUserSdk has not run in this process');
|
|
236
|
+
return core;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Email OTP sign-in. `readOtp` is the caller's prompt (the CLI reads it muted from the terminal); the code
|
|
240
|
+
* is consulted once, never echoed, never written.
|
|
241
|
+
*/
|
|
242
|
+
export async function signInWithEmailOtp(email, readOtp) {
|
|
243
|
+
const address = email.trim();
|
|
244
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address))
|
|
245
|
+
refuse('EMAIL_INVALID', 'a valid email is required');
|
|
246
|
+
const mod = sdk();
|
|
247
|
+
// A fresh sign-in each time: the SDK refuses a second sign-in while a session is live in the process.
|
|
248
|
+
if (await nodePathGuard(() => mod.isSignedIn()))
|
|
249
|
+
await nodePathGuard(() => mod.signOut());
|
|
250
|
+
const { flowId } = await nodePathGuard(() => mod.signInWithEmail({ email: address }));
|
|
251
|
+
const otp = (await readOtp()).trim();
|
|
252
|
+
if (!/^[0-9]{4,10}$/.test(otp))
|
|
253
|
+
refuse('OTP_INVALID', 'that does not look like an OTP code; request a fresh one');
|
|
254
|
+
const { user } = await nodePathGuard(() => mod.verifyEmailOTP({ flowId, otp }));
|
|
255
|
+
const account = user.evmAccounts?.[0];
|
|
256
|
+
if (!account) {
|
|
257
|
+
refuse('NO_EVM_ACCOUNT', 'sign-in succeeded but the end user has no EVM account; the project must create an EVM EOA on login');
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze({ userId: user.userId, address: account });
|
|
260
|
+
}
|
|
261
|
+
/** Mint the delegation for the signed-in user, with the ruled expiry bounds (default 90 days, max 180). */
|
|
262
|
+
export async function mintDelegation(requestedDays, nowMs = Date.now()) {
|
|
263
|
+
let bounded;
|
|
264
|
+
try {
|
|
265
|
+
bounded = boundedDelegationExpiry(requestedDays, nowMs);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return refuse('EXPIRY_INVALID', error instanceof Error ? error.message : String(error));
|
|
269
|
+
}
|
|
270
|
+
const mod = sdk();
|
|
271
|
+
const minted = await nodePathGuard(() => mod.createDelegation({ expiresAt: bounded.expiresAt }));
|
|
272
|
+
return Object.freeze({ expiresAt: minted.expiresAt, days: bounded.days });
|
|
273
|
+
}
|
|
274
|
+
/** The signed-in user's own delegation as CDP reports it, or `undefined` when none is active. */
|
|
275
|
+
export async function readDelegationAsUser() {
|
|
276
|
+
const mod = sdk();
|
|
277
|
+
try {
|
|
278
|
+
const grant = await nodePathGuard(() => mod.getDelegation());
|
|
279
|
+
return Object.freeze({ expiresAt: String(grant.expiresAt ?? '') });
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const e = error;
|
|
283
|
+
if (e.statusCode === 404 ||
|
|
284
|
+
['delegation_not_found', 'delegation_revoked', 'delegation_expired'].includes(e.errorType ?? ''))
|
|
285
|
+
return undefined;
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** End-user-side revoke ("the user pulls the plug"), confirmed by read-back. */
|
|
290
|
+
export async function revokeDelegationAsUser() {
|
|
291
|
+
const mod = sdk();
|
|
292
|
+
await nodePathGuard(() => mod.revokeDelegation());
|
|
293
|
+
const after = await readDelegationAsUser();
|
|
294
|
+
return Object.freeze({ readsBack: after !== undefined });
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The hosted MCP may answer JSON or, per its `Accept`, a `text/event-stream` frame. Read the JSON either way:
|
|
298
|
+
* for SSE, concatenate the `data:` lines of the (single) event and parse that. Never throws — an unparseable
|
|
299
|
+
* body returns `undefined`, which the caller turns into FENCE_UNREACHABLE (fail-closed).
|
|
300
|
+
*/
|
|
301
|
+
function parseRpcBody(text) {
|
|
302
|
+
try {
|
|
303
|
+
return JSON.parse(text);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
const data = text
|
|
307
|
+
.split(/\r?\n/)
|
|
308
|
+
.filter((line) => line.startsWith('data:'))
|
|
309
|
+
.map((line) => line.slice(5).trimStart())
|
|
310
|
+
.join('');
|
|
311
|
+
if (data === '')
|
|
312
|
+
return undefined;
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(data);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** Ask the hosted MCP's PUBLIC `otto_delegation_fence_status`. Any failure to get an answer is FENCE_UNREACHABLE (fail-closed). */
|
|
322
|
+
export async function readPublicFenceStatus(mcpUrl = DEFAULT_OTTO_MCP_URL, options = {}) {
|
|
323
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
|
|
326
|
+
let reply;
|
|
327
|
+
try {
|
|
328
|
+
const response = await fetchImpl(mcpUrl, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: {
|
|
331
|
+
Accept: 'application/json, text/event-stream',
|
|
332
|
+
'Content-Type': 'application/json',
|
|
333
|
+
},
|
|
334
|
+
body: JSON.stringify({
|
|
335
|
+
jsonrpc: '2.0',
|
|
336
|
+
id: 1,
|
|
337
|
+
method: 'tools/call',
|
|
338
|
+
params: { name: FENCE_STATUS_TOOL_NAME, arguments: {} },
|
|
339
|
+
}),
|
|
340
|
+
// A 3xx would take the read to a host the caller's guard never vetted; refuse rather than follow it,
|
|
341
|
+
// so the host the CLI trusted is the host that actually answers (fail-closed → FENCE_UNREACHABLE).
|
|
342
|
+
redirect: 'error',
|
|
343
|
+
signal: controller.signal,
|
|
344
|
+
});
|
|
345
|
+
if (response.status !== 200) {
|
|
346
|
+
refuse('FENCE_UNREACHABLE', `the hosted MCP answered HTTP ${response.status} to the public fence read; nothing is minted`);
|
|
347
|
+
}
|
|
348
|
+
reply = parseRpcBody(await response.text());
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (isDelegateRefusal(error))
|
|
352
|
+
throw error;
|
|
353
|
+
refuse('FENCE_UNREACHABLE', `the hosted MCP could not be reached for the public fence read (${error instanceof Error ? error.name : 'error'}); nothing is minted`);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
}
|
|
358
|
+
const result = reply
|
|
359
|
+
?.result;
|
|
360
|
+
const status = result?.structuredContent;
|
|
361
|
+
if (!result || result.isError || !status || typeof status.present !== 'boolean') {
|
|
362
|
+
refuse('FENCE_UNREACHABLE', 'the hosted MCP did not answer the public fence read with a fence status; nothing is minted');
|
|
363
|
+
}
|
|
364
|
+
// The authority binding: a UUID project id must be published (present OR absent) or there is nothing to
|
|
365
|
+
// mint to — fail-closed. Never coerced.
|
|
366
|
+
const projectId = typeof status.project_id === 'string' ? status.project_id : '';
|
|
367
|
+
if (!UUID_PATTERN.test(projectId)) {
|
|
368
|
+
refuse('FENCE_UNREACHABLE', 'the public fence read carried no CDP project id (a UUID) to bind the mint to; nothing is minted');
|
|
369
|
+
}
|
|
370
|
+
// When the fence reads back PRESENT, its promise must be Otto's v1 EXACTLY — name, rules digest and cap.
|
|
371
|
+
// Anything else is a fence that is not the one this build knows: FENCE_MISMATCH, never coerced to authority.
|
|
372
|
+
if (status.present === true) {
|
|
373
|
+
if (status.policy_name !== MODEL_B_POLICY_NAME_V1 ||
|
|
374
|
+
status.rules_digest !== MODEL_B_POLICY_DIGEST_V1 ||
|
|
375
|
+
status.per_swap_cap_usd !== MODEL_B_PER_SWAP_CAP_USD) {
|
|
376
|
+
refuse('FENCE_MISMATCH', `the public fence read answered present but not as "${MODEL_B_POLICY_NAME_V1}" with the v1 rules digest and the ${MODEL_B_PER_SWAP_CAP_USD} USD cap; nothing is minted`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return Object.freeze({
|
|
380
|
+
present: status.present,
|
|
381
|
+
policy_name: String(status.policy_name ?? ''),
|
|
382
|
+
per_swap_cap_usd: Number(status.per_swap_cap_usd ?? 0),
|
|
383
|
+
project_id: projectId,
|
|
384
|
+
...(typeof status.rules_digest === 'string' ? { rules_digest: status.rules_digest } : {}),
|
|
385
|
+
...(typeof status.reason === 'string' ? { reason: status.reason } : {}),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/** Refuse to proceed unless the fence reads back present. */
|
|
389
|
+
export function assertFencePresent(status) {
|
|
390
|
+
if (!status.present) {
|
|
391
|
+
refuse('DELEGATION_FENCE_ABSENT', `the Model-B fence is not present on the hosted MCP (${status.reason ?? 'absent'}); nothing is minted — the operator installs it first`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
//# sourceMappingURL=delegate.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eoa-signer.ts — the bring-your-own-EOA capability provider (spec §2 SigningAdapter, §9 handshakes).
|
|
3
|
+
*
|
|
4
|
+
* Provides `send_evm_transaction` (one signed tx per plan step, in order; nonce/gas/serialization are
|
|
5
|
+
* adapter-owned per spec §6) and `sign_x402_payment` (challenge → signed `x_payment`, exactly the
|
|
6
|
+
* hosted MCP's existing retry protocol). The key never leaves this process; it is read from the
|
|
7
|
+
* environment once (env.ts registers it with the output guard) or generated in-process for a
|
|
8
|
+
* dry run and discarded. On this path the USER's key both signs and submits.
|
|
9
|
+
*
|
|
10
|
+
* Dry run = sign, never broadcast: every step is signed with placeholder nonce/gas, parsed back, and
|
|
11
|
+
* the parsed {to, value, data} must equal the verified plan step byte-for-byte, and the signature
|
|
12
|
+
* must recover to this account. That proves "what the signer signs is what the adapter verified"
|
|
13
|
+
* without touching a chain.
|
|
14
|
+
*/
|
|
15
|
+
import { type Address, type Hex, type PublicClient } from 'viem';
|
|
16
|
+
import { type SealedWaitOptions } from './chain.js';
|
|
17
|
+
import { type ValidatedX402Challenge, type X402Table } from './x402-table.js';
|
|
18
|
+
import { type SigningCapability, type VerifiedStep, type VerifiedSwap } from './verify.js';
|
|
19
|
+
import type { SentStep } from './vendor/otto-intel-mcp/adapter-index.js';
|
|
20
|
+
export declare const EOA_CAPABILITIES: readonly SigningCapability[];
|
|
21
|
+
export declare const CHAIN_ID: 8453;
|
|
22
|
+
/**
|
|
23
|
+
* The INDEPENDENT per-payment ceiling applied to every x402 signature, stated here rather than
|
|
24
|
+
* inherited from `@x402/core`'s default (which is the same `$1`, but is the SDK's number to change,
|
|
25
|
+
* not ours). Every live table row is $0.001–$0.002, so this has ~500x of headroom over real traffic;
|
|
26
|
+
* it exists to bound a table that is wrong, not to bound normal use. Raising it is a deliberate edit.
|
|
27
|
+
*/
|
|
28
|
+
export declare const PER_PAYMENT_SPEND_CAP: "$1";
|
|
29
|
+
export interface DryRunSignedStep {
|
|
30
|
+
readonly index: number;
|
|
31
|
+
readonly kind: VerifiedStep['kind'];
|
|
32
|
+
readonly to: Address;
|
|
33
|
+
readonly signed_bytes: number;
|
|
34
|
+
readonly recovered_signer: Address;
|
|
35
|
+
readonly decoded_matches_plan: true;
|
|
36
|
+
readonly broadcast: false;
|
|
37
|
+
}
|
|
38
|
+
export type { SentStep };
|
|
39
|
+
export interface SignedX402Payment {
|
|
40
|
+
/** The `x_payment` value: base64 x402 v2 PaymentPayload (the hosted MCP forwards it as `X-Payment`). */
|
|
41
|
+
readonly header: string;
|
|
42
|
+
readonly payload: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
/** A plan step failed after an approval was live; the adapter tried to clear the allowance. */
|
|
45
|
+
export declare class PlanHaltedError extends Error {
|
|
46
|
+
readonly failedStep: number;
|
|
47
|
+
readonly allowanceCleared: boolean;
|
|
48
|
+
readonly clearTransactionHash?: Hex | undefined;
|
|
49
|
+
constructor(message: string, failedStep: number, allowanceCleared: boolean, clearTransactionHash?: Hex | undefined);
|
|
50
|
+
}
|
|
51
|
+
export declare class EoaSigner {
|
|
52
|
+
private readonly account;
|
|
53
|
+
private readonly rpcUrl;
|
|
54
|
+
readonly address: Address;
|
|
55
|
+
readonly capabilities: readonly SigningCapability[];
|
|
56
|
+
readonly chainId: 8453;
|
|
57
|
+
private constructor();
|
|
58
|
+
static fromPrivateKey(privateKey: Hex, rpcUrl?: string): EoaSigner;
|
|
59
|
+
/** A throwaway in-process key for dry runs. Never printed, never persisted, gone with the process. */
|
|
60
|
+
static ephemeral(): EoaSigner;
|
|
61
|
+
publicClient(): PublicClient;
|
|
62
|
+
/**
|
|
63
|
+
* `send_evm_transaction`, dry: sign every step, decode back, recover — broadcast nothing. `clock` is
|
|
64
|
+
* either a fixed instant (deterministic tests) or a clock FUNCTION read afresh before every signature
|
|
65
|
+
* (the default, `Date.now`), so an artifact that expires between two signatures is caught.
|
|
66
|
+
*/
|
|
67
|
+
signPlanDryRun(plan: VerifiedSwap, clock?: number | (() => number)): Promise<DryRunSignedStep[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Nonce / fee / gas round trips first, then `beforeSign` (the plan's freshness + binding re-check,
|
|
70
|
+
* AFTER those round trips and immediately before the signature), then sign and broadcast. Resolves
|
|
71
|
+
* only with the SEALED, CANONICAL receipt (`waitForSealedReceipt`): the next step's gas estimate and
|
|
72
|
+
* any read-back must see this transaction, and a Flashblocks preconfirmation alone guarantees neither
|
|
73
|
+
* that they do nor that the transaction survived sealing.
|
|
74
|
+
*/
|
|
75
|
+
private submit;
|
|
76
|
+
/**
|
|
77
|
+
* `send_evm_transaction`, live: one tx per step, in order, the plan re-checked before every step AND
|
|
78
|
+
* again immediately before each signature (after the nonce / fee / gas round trips, so an artifact
|
|
79
|
+
* that expires during preparation is never signed). The plan halts at the first step that does not
|
|
80
|
+
* succeed; after ANY failure a bounded `approve(spender, 0)` is attempted and its real outcome —
|
|
81
|
+
* confirmed by an allowance read-back — is reported, so no allowance outlives the halted plan unnoticed.
|
|
82
|
+
*/
|
|
83
|
+
sendPlan(plan: VerifiedSwap, pub?: PublicClient, sealedWait?: SealedWaitOptions): Promise<SentStep[]>;
|
|
84
|
+
private halt;
|
|
85
|
+
/**
|
|
86
|
+
* `sign_x402_payment`: turn a table-validated challenge into the signed `x_payment` value.
|
|
87
|
+
* Validation and hand-off are two moments. Immediately before signing, the document is validated
|
|
88
|
+
* AGAIN against the static table and must resolve to the same row and the same bound fields as the
|
|
89
|
+
* earlier validation; the requirement selector accepts only a candidate matching that ROW; and the
|
|
90
|
+
* produced payload's `accepted` requirement and authorization are checked against the ROW (not
|
|
91
|
+
* against anything the challenge supplied) before the header is returned.
|
|
92
|
+
*/
|
|
93
|
+
signX402Payment(paymentRequired: unknown, validated: ValidatedX402Challenge, table?: X402Table, nowSeconds?: number): Promise<SignedX402Payment>;
|
|
94
|
+
}
|