@namera-ai/cli 0.1.4 → 1.0.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 +91 -221
- package/dist/index.js +1660 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -50
- package/LICENSE +0 -201
- package/dist/commands/index.mjs +0 -21
- package/dist/commands/index.mjs.map +0 -1
- package/dist/commands/keystore/create.mjs +0 -74
- package/dist/commands/keystore/create.mjs.map +0 -1
- package/dist/commands/keystore/decrypt.mjs +0 -71
- package/dist/commands/keystore/decrypt.mjs.map +0 -1
- package/dist/commands/keystore/import.mjs +0 -84
- package/dist/commands/keystore/import.mjs.map +0 -1
- package/dist/commands/keystore/index.mjs +0 -28
- package/dist/commands/keystore/index.mjs.map +0 -1
- package/dist/commands/keystore/info.mjs +0 -62
- package/dist/commands/keystore/info.mjs.map +0 -1
- package/dist/commands/keystore/list.mjs +0 -46
- package/dist/commands/keystore/list.mjs.map +0 -1
- package/dist/commands/keystore/remove.mjs +0 -51
- package/dist/commands/keystore/remove.mjs.map +0 -1
- package/dist/commands/mcp/index.mjs +0 -13
- package/dist/commands/mcp/index.mjs.map +0 -1
- package/dist/commands/mcp/start.mjs +0 -82
- package/dist/commands/mcp/start.mjs.map +0 -1
- package/dist/commands/schema/index.mjs +0 -53
- package/dist/commands/schema/index.mjs.map +0 -1
- package/dist/commands/session-key/create/index.mjs +0 -87
- package/dist/commands/session-key/create/index.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/base.mjs +0 -52
- package/dist/commands/session-key/create/prompts/base.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/call.mjs +0 -85
- package/dist/commands/session-key/create/prompts/call.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/common.mjs +0 -27
- package/dist/commands/session-key/create/prompts/common.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/gas.mjs +0 -19
- package/dist/commands/session-key/create/prompts/gas.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/index.mjs +0 -44
- package/dist/commands/session-key/create/prompts/index.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/rate-limit.mjs +0 -40
- package/dist/commands/session-key/create/prompts/rate-limit.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/signature-caller.mjs +0 -26
- package/dist/commands/session-key/create/prompts/signature-caller.mjs.map +0 -1
- package/dist/commands/session-key/create/prompts/timestamp.mjs +0 -32
- package/dist/commands/session-key/create/prompts/timestamp.mjs.map +0 -1
- package/dist/commands/session-key/index.mjs +0 -26
- package/dist/commands/session-key/index.mjs.map +0 -1
- package/dist/commands/session-key/info.mjs +0 -64
- package/dist/commands/session-key/info.mjs.map +0 -1
- package/dist/commands/session-key/list.mjs +0 -54
- package/dist/commands/session-key/list.mjs.map +0 -1
- package/dist/commands/session-key/remove.mjs +0 -51
- package/dist/commands/session-key/remove.mjs.map +0 -1
- package/dist/commands/session-key/status.mjs +0 -64
- package/dist/commands/session-key/status.mjs.map +0 -1
- package/dist/commands/smart-account/create.mjs +0 -94
- package/dist/commands/smart-account/create.mjs.map +0 -1
- package/dist/commands/smart-account/import.mjs +0 -45
- package/dist/commands/smart-account/import.mjs.map +0 -1
- package/dist/commands/smart-account/index.mjs +0 -30
- package/dist/commands/smart-account/index.mjs.map +0 -1
- package/dist/commands/smart-account/info.mjs +0 -66
- package/dist/commands/smart-account/info.mjs.map +0 -1
- package/dist/commands/smart-account/list.mjs +0 -48
- package/dist/commands/smart-account/list.mjs.map +0 -1
- package/dist/commands/smart-account/remove.mjs +0 -54
- package/dist/commands/smart-account/remove.mjs.map +0 -1
- package/dist/commands/smart-account/status.mjs +0 -63
- package/dist/commands/smart-account/status.mjs.map +0 -1
- package/dist/dto/index.mjs +0 -6
- package/dist/dto/keystore.mjs +0 -44
- package/dist/dto/keystore.mjs.map +0 -1
- package/dist/dto/mcp.mjs +0 -13
- package/dist/dto/mcp.mjs.map +0 -1
- package/dist/dto/session-key.mjs +0 -54
- package/dist/dto/session-key.mjs.map +0 -1
- package/dist/dto/smart-account.mjs +0 -45
- package/dist/dto/smart-account.mjs.map +0 -1
- package/dist/flags/global.mjs +0 -29
- package/dist/flags/global.mjs.map +0 -1
- package/dist/flags/shared.mjs +0 -11
- package/dist/flags/shared.mjs.map +0 -1
- package/dist/helpers/humanize.mjs +0 -153
- package/dist/helpers/humanize.mjs.map +0 -1
- package/dist/helpers/paths.mjs +0 -31
- package/dist/helpers/paths.mjs.map +0 -1
- package/dist/helpers/policy.mjs +0 -89
- package/dist/helpers/policy.mjs.map +0 -1
- package/dist/helpers/pretty.mjs +0 -71
- package/dist/helpers/pretty.mjs.map +0 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -36
- package/dist/index.mjs.map +0 -1
- package/dist/layers/config.mjs +0 -124
- package/dist/layers/config.mjs.map +0 -1
- package/dist/layers/index.mjs +0 -11
- package/dist/layers/keystore.mjs +0 -225
- package/dist/layers/keystore.mjs.map +0 -1
- package/dist/layers/mcp-context.mjs +0 -8
- package/dist/layers/mcp-context.mjs.map +0 -1
- package/dist/layers/mcp.mjs +0 -31
- package/dist/layers/mcp.mjs.map +0 -1
- package/dist/layers/output.mjs +0 -21
- package/dist/layers/output.mjs.map +0 -1
- package/dist/layers/prompt.mjs +0 -78
- package/dist/layers/prompt.mjs.map +0 -1
- package/dist/layers/session-key.mjs +0 -218
- package/dist/layers/session-key.mjs.map +0 -1
- package/dist/layers/smart-account.mjs +0 -175
- package/dist/layers/smart-account.mjs.map +0 -1
- package/dist/layers/web3.mjs +0 -83
- package/dist/layers/web3.mjs.map +0 -1
- package/dist/mcp/helpers/common.mjs +0 -9
- package/dist/mcp/helpers/common.mjs.map +0 -1
- package/dist/mcp/helpers/index.mjs +0 -4
- package/dist/mcp/helpers/session-key.mjs +0 -129
- package/dist/mcp/helpers/session-key.mjs.map +0 -1
- package/dist/mcp/index.mjs +0 -43
- package/dist/mcp/index.mjs.map +0 -1
- package/dist/mcp/mcp.mjs +0 -15
- package/dist/mcp/mcp.mjs.map +0 -1
- package/dist/mcp/tools/account/address.mjs +0 -20
- package/dist/mcp/tools/account/address.mjs.map +0 -1
- package/dist/mcp/tools/account/get-balance.mjs +0 -54
- package/dist/mcp/tools/account/get-balance.mjs.map +0 -1
- package/dist/mcp/tools/account/index.mjs +0 -15
- package/dist/mcp/tools/account/index.mjs.map +0 -1
- package/dist/mcp/tools/read/index.mjs +0 -11
- package/dist/mcp/tools/read/index.mjs.map +0 -1
- package/dist/mcp/tools/read/read-contract.mjs +0 -49
- package/dist/mcp/tools/read/read-contract.mjs.map +0 -1
- package/dist/mcp/tools/transaction/execute-transaction.mjs +0 -38
- package/dist/mcp/tools/transaction/execute-transaction.mjs.map +0 -1
- package/dist/mcp/tools/transaction/index.mjs +0 -15
- package/dist/mcp/tools/transaction/index.mjs.map +0 -1
- package/dist/mcp/tools/transaction/native-transfer.mjs +0 -59
- package/dist/mcp/tools/transaction/native-transfer.mjs.map +0 -1
- package/dist/schema/chain.mjs +0 -124
- package/dist/schema/chain.mjs.map +0 -1
- package/dist/schema/common.mjs +0 -29
- package/dist/schema/common.mjs.map +0 -1
- package/dist/schema/index.mjs +0 -5
- package/dist/schema/policy.mjs +0 -114
- package/dist/schema/policy.mjs.map +0 -1
- package/dist/schema/tx.mjs +0 -19
- package/dist/schema/tx.mjs.map +0 -1
- package/dist/types/index.mjs +0 -11
- package/dist/types/index.mjs.map +0 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,1660 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
3
|
+
import { Clock, Console, Context, DateTime, Deferred, Effect, Fiber, FileSystem, Layer, Logger, Option, Predicate, Redacted, Schema, Stdio, Stream } from "effect";
|
|
4
|
+
import { Argument, Command, Flag, Prompt } from "effect/unstable/cli";
|
|
5
|
+
import { NAMERA_API_ORIGIN, NameraClient, openLocalSessionKey, sealLocalSessionKey } from "@namera-ai/sdk";
|
|
6
|
+
import { chmod, link, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { homedir, platform } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { Entry } from "@napi-rs/keyring";
|
|
10
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
11
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
12
|
+
import { constants } from "node:fs";
|
|
13
|
+
import { EthereumAddress, EvmTypedData, ExecutionId, ExecutionSubmissionId, Hex, SessionKeyId, SupportedEvmChainId, WalletId } from "@namera-ai/protocol";
|
|
14
|
+
import { EncryptedLocalSessionKey } from "@namera-ai/protocol/local";
|
|
15
|
+
import { stripVTControlCharacters } from "node:util";
|
|
16
|
+
import { arbitrum, arbitrumSepolia, base, baseSepolia, mainnet, optimism, optimismSepolia, sepolia } from "viem/chains";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { CompleteExecutionResponse, ExecuteRequest, GetExecutionSubmissionResponse, GetSessionKeyResponse, GetWalletResponse, ListExecutionsResponse, ListSessionKeysForOrganizationResponse, ListWalletsResponse, McpExecuteTransactionRequest, McpGetExecutionsRequest, McpGetSessionKeyRequest, McpGetTransactionStatusRequest, McpGetWalletRequest, McpListSessionKeysRequest, McpSignRequest, McpSimulateTransactionRequest, McpToolError, McpToolErrorCode, McpVerifySignatureRequest, OAuthDynamicClientRegistrationResponse, OAuthTokenResponse, PrepareSignatureRequest, SignResponse, SimulateExecutionRequest, SimulateExecutionResponse, VerifySignatureRequest, VerifySignatureResponse } from "@namera-ai/protocol/dto";
|
|
19
|
+
import { DatabaseSync } from "node:sqlite";
|
|
20
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpIncomingMessage } from "effect/unstable/http";
|
|
21
|
+
import { createServer } from "node:http";
|
|
22
|
+
import { McpProtocol, McpSchema, McpServer, Tool } from "effect/unstable/ai";
|
|
23
|
+
|
|
24
|
+
//#region src/commands/common.ts
|
|
25
|
+
const profileFlag = Flag.string("profile").pipe(Flag.withDescription("CLI profile name"), Flag.withDefault("personal"));
|
|
26
|
+
const paramsFlag = Flag.string("params").pipe(Flag.withDescription("Inline JSON command payload"), Flag.optional);
|
|
27
|
+
const resolveParams = (params, schema, prompt) => Option.match(params, {
|
|
28
|
+
onNone: () => prompt,
|
|
29
|
+
onSome: (value) => Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(value)
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/services/config.ts
|
|
34
|
+
const configDirectory = () => {
|
|
35
|
+
if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, "namera");
|
|
36
|
+
if (process.platform === "darwin") return join(homedir(), "Library", "Application Support", "namera");
|
|
37
|
+
if (process.platform === "win32") return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "Namera");
|
|
38
|
+
return join(homedir(), ".config", "namera");
|
|
39
|
+
};
|
|
40
|
+
const cliConfigPath = join(configDirectory(), "config.json");
|
|
41
|
+
const cliLockPath = (profile) => join(configDirectory(), `${profile}.refresh.lock`);
|
|
42
|
+
const readCliConfig = async () => {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(await readFile(cliConfigPath, "utf8"));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== "ENOENT") throw error;
|
|
47
|
+
return {
|
|
48
|
+
activeProfile: "personal",
|
|
49
|
+
profiles: {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const writeCliConfig = async (config) => {
|
|
54
|
+
await mkdir(dirname(cliConfigPath), {
|
|
55
|
+
recursive: true,
|
|
56
|
+
mode: 448
|
|
57
|
+
});
|
|
58
|
+
const temporaryPath = `${cliConfigPath}.${process.pid}.tmp`;
|
|
59
|
+
await writeFile(temporaryPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 384 });
|
|
60
|
+
await rename(temporaryPath, cliConfigPath);
|
|
61
|
+
};
|
|
62
|
+
const getProfile = async (name) => {
|
|
63
|
+
const config = await readCliConfig();
|
|
64
|
+
const profileName = name ?? config.activeProfile;
|
|
65
|
+
const profile = config.profiles[profileName];
|
|
66
|
+
if (profile === void 0) throw new Error(`Profile "${profileName}" is not logged in.`);
|
|
67
|
+
return {
|
|
68
|
+
config,
|
|
69
|
+
profile,
|
|
70
|
+
profileName
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const saveProfile = async (name, profile) => {
|
|
74
|
+
const config = await readCliConfig();
|
|
75
|
+
await writeCliConfig({
|
|
76
|
+
activeProfile: name,
|
|
77
|
+
profiles: {
|
|
78
|
+
...config.profiles,
|
|
79
|
+
[name]: profile
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
const removeProfile = async (name) => {
|
|
84
|
+
const config = await readCliConfig();
|
|
85
|
+
const profiles = { ...config.profiles };
|
|
86
|
+
delete profiles[name];
|
|
87
|
+
await writeCliConfig({
|
|
88
|
+
activeProfile: config.activeProfile === name ? Object.keys(profiles)[0] ?? "personal" : config.activeProfile,
|
|
89
|
+
profiles
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/services/credentials.ts
|
|
95
|
+
const entry$1 = (profile) => new Entry("namera-cli", profile);
|
|
96
|
+
const readCredentials = (profile) => {
|
|
97
|
+
const value = entry$1(profile).getPassword();
|
|
98
|
+
return value === null ? void 0 : JSON.parse(value);
|
|
99
|
+
};
|
|
100
|
+
const writeCredentials = (profile, credentials) => {
|
|
101
|
+
entry$1(profile).setPassword(JSON.stringify(credentials));
|
|
102
|
+
};
|
|
103
|
+
const deleteCredentials = (profile) => {
|
|
104
|
+
try {
|
|
105
|
+
entry$1(profile).deletePassword();
|
|
106
|
+
} catch {}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/services/oauth.ts
|
|
111
|
+
const clientId = "namera-cli";
|
|
112
|
+
const resourceScopes = [
|
|
113
|
+
"wallet:read",
|
|
114
|
+
"session-key:read",
|
|
115
|
+
"execution:read",
|
|
116
|
+
"execution:execute",
|
|
117
|
+
"signature:create",
|
|
118
|
+
"offline_access"
|
|
119
|
+
];
|
|
120
|
+
var OAuthRequestError = class extends Error {
|
|
121
|
+
code;
|
|
122
|
+
constructor(code, message) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.code = code;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const postForm = async (url, body) => {
|
|
128
|
+
const response = await fetch(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
131
|
+
body
|
|
132
|
+
});
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
if (!response.ok) throw new OAuthRequestError(typeof data.error === "string" ? data.error : "oauth_error", typeof data.error_description === "string" ? data.error_description : "OAuth request failed");
|
|
135
|
+
return data;
|
|
136
|
+
};
|
|
137
|
+
const startDeviceAuthorization = (baseUrl, metadata) => postForm(`${baseUrl}/oauth/device/authorize`, new URLSearchParams({
|
|
138
|
+
client_id: clientId,
|
|
139
|
+
scope: resourceScopes.join(" "),
|
|
140
|
+
resource: new URL(baseUrl).origin,
|
|
141
|
+
device_name: metadata.deviceName,
|
|
142
|
+
cli_version: metadata.cliVersion,
|
|
143
|
+
platform: metadata.platform
|
|
144
|
+
}));
|
|
145
|
+
const toCredentials = (token) => ({
|
|
146
|
+
accessToken: token.access_token,
|
|
147
|
+
refreshToken: token.refresh_token ?? null,
|
|
148
|
+
expiresAt: Date.now() + token.expires_in * 1e3,
|
|
149
|
+
scopes: token.scope.split(/\s+/).filter(Boolean)
|
|
150
|
+
});
|
|
151
|
+
const pollDeviceToken = async (baseUrl, deviceCode) => {
|
|
152
|
+
const token = await postForm(`${baseUrl}/oauth/token`, new URLSearchParams({
|
|
153
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
154
|
+
device_code: deviceCode,
|
|
155
|
+
client_id: clientId,
|
|
156
|
+
resource: new URL(baseUrl).origin
|
|
157
|
+
}));
|
|
158
|
+
return toCredentials(token);
|
|
159
|
+
};
|
|
160
|
+
const acquireRefreshLock = async (profile) => {
|
|
161
|
+
const path = cliLockPath(profile);
|
|
162
|
+
for (let attempt = 0; attempt < 40; attempt += 1) try {
|
|
163
|
+
const handle = await open(path, "wx", 384);
|
|
164
|
+
return async () => {
|
|
165
|
+
await handle.close();
|
|
166
|
+
await unlink(path).catch(() => void 0);
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error.code !== "EEXIST") throw error;
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
171
|
+
}
|
|
172
|
+
throw new Error("Timed out waiting for another Namera command to refresh credentials.");
|
|
173
|
+
};
|
|
174
|
+
const getValidAccessToken = async (profile, baseUrl) => {
|
|
175
|
+
const release = await acquireRefreshLock(profile);
|
|
176
|
+
try {
|
|
177
|
+
const credentials = readCredentials(profile);
|
|
178
|
+
if (credentials === void 0) throw new Error(`Profile "${profile}" is not logged in.`);
|
|
179
|
+
if (credentials.expiresAt > Date.now() + 3e4) return credentials.accessToken;
|
|
180
|
+
if (credentials.refreshToken === null) throw new Error("The CLI session expired. Run namera login.");
|
|
181
|
+
const token = await postForm(`${baseUrl}/oauth/token`, new URLSearchParams({
|
|
182
|
+
grant_type: "refresh_token",
|
|
183
|
+
refresh_token: credentials.refreshToken,
|
|
184
|
+
client_id: clientId,
|
|
185
|
+
resource: new URL(baseUrl).origin
|
|
186
|
+
}));
|
|
187
|
+
const refreshed = toCredentials(token);
|
|
188
|
+
writeCredentials(profile, refreshed);
|
|
189
|
+
return refreshed.accessToken;
|
|
190
|
+
} finally {
|
|
191
|
+
await release();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/services/session-keystore/resolver.ts
|
|
197
|
+
const makeSessionSignerResolver = (store, apiOrigin, maxGasCostWei) => async (request) => {
|
|
198
|
+
const material = await store.readKey(apiOrigin, request.sessionKeyId);
|
|
199
|
+
const binding = material.bindings.find((installation) => installation.chainId === request.chainId && installation.walletId === request.walletId);
|
|
200
|
+
if (binding === void 0) throw new Error("No local installation for the selected wallet and chain.");
|
|
201
|
+
const account = privateKeyToAccount(Redacted.value(material.privateKey));
|
|
202
|
+
return {
|
|
203
|
+
binding,
|
|
204
|
+
...maxGasCostWei === void 0 ? {} : { maxGasCostWei },
|
|
205
|
+
signMessage: (message) => account.signMessage({ message }),
|
|
206
|
+
signTypedData: (typedData) => account.signTypedData(typedData)
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/services/session-keystore/storage.ts
|
|
212
|
+
const StoredSession = Schema.Struct({
|
|
213
|
+
version: Schema.Literal(1),
|
|
214
|
+
keyringId: Schema.String.check(Schema.isPattern(/^[0-9a-f-]{36}$/)),
|
|
215
|
+
encrypted: EncryptedLocalSessionKey
|
|
216
|
+
});
|
|
217
|
+
var SessionKeystoreError = class extends Schema.TaggedError()("SessionKeystoreError", { code: Schema.Literals([
|
|
218
|
+
"IMPORT_FAILED",
|
|
219
|
+
"ORIGIN_MISMATCH",
|
|
220
|
+
"READ_FAILED",
|
|
221
|
+
"UNLOCK_UNAVAILABLE"
|
|
222
|
+
]) }) {};
|
|
223
|
+
const createSessionKeystore = (directory, keyring) => {
|
|
224
|
+
const pathFor = (apiOrigin, sessionKeyId) => {
|
|
225
|
+
const id = Schema.decodeUnknownSync(SessionKeyId)(sessionKeyId);
|
|
226
|
+
const host = createHash("sha256").update(apiOrigin).digest("hex");
|
|
227
|
+
return join(directory, `${host}-${id}.json`);
|
|
228
|
+
};
|
|
229
|
+
const importKey = async (input, password, apiOrigin) => {
|
|
230
|
+
const material = await openLocalSessionKey(input, password);
|
|
231
|
+
if (material.apiOrigin !== apiOrigin) throw new SessionKeystoreError({ code: "ORIGIN_MISMATCH" });
|
|
232
|
+
const binding = material.bindings[0];
|
|
233
|
+
if (binding === void 0) throw new SessionKeystoreError({ code: "IMPORT_FAILED" });
|
|
234
|
+
const keyringId = randomUUID();
|
|
235
|
+
const unlockSecret = randomBytes(32).toString("base64url");
|
|
236
|
+
const encrypted = await sealLocalSessionKey(material, Redacted.make(unlockSecret));
|
|
237
|
+
const temporaryPath = join(directory, `.${randomUUID()}.tmp`);
|
|
238
|
+
let savedSecret = false;
|
|
239
|
+
let temporaryCreated = false;
|
|
240
|
+
let installed = false;
|
|
241
|
+
let failed = false;
|
|
242
|
+
try {
|
|
243
|
+
await mkdir(directory, {
|
|
244
|
+
recursive: true,
|
|
245
|
+
mode: 448
|
|
246
|
+
});
|
|
247
|
+
const directoryStat = await lstat(directory);
|
|
248
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new Error("Unsafe directory");
|
|
249
|
+
if (process.platform !== "win32") await chmod(directory, 448);
|
|
250
|
+
keyring.set(keyringId, unlockSecret);
|
|
251
|
+
savedSecret = true;
|
|
252
|
+
const handle = await open(temporaryPath, "wx", 384);
|
|
253
|
+
temporaryCreated = true;
|
|
254
|
+
try {
|
|
255
|
+
await handle.writeFile(JSON.stringify({
|
|
256
|
+
version: 1,
|
|
257
|
+
keyringId,
|
|
258
|
+
encrypted
|
|
259
|
+
}));
|
|
260
|
+
await handle.sync();
|
|
261
|
+
} finally {
|
|
262
|
+
await handle.close();
|
|
263
|
+
}
|
|
264
|
+
await link(temporaryPath, pathFor(apiOrigin, binding.sessionKeyId));
|
|
265
|
+
installed = true;
|
|
266
|
+
} catch {
|
|
267
|
+
failed = true;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
if (temporaryCreated) await unlink(temporaryPath);
|
|
271
|
+
} catch {
|
|
272
|
+
failed = true;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
if (!installed && savedSecret) keyring.delete(keyringId);
|
|
276
|
+
} catch {
|
|
277
|
+
failed = true;
|
|
278
|
+
}
|
|
279
|
+
if (failed) throw new SessionKeystoreError({ code: "IMPORT_FAILED" });
|
|
280
|
+
return {
|
|
281
|
+
sessionKeyId: binding.sessionKeyId,
|
|
282
|
+
walletId: binding.walletId,
|
|
283
|
+
chains: material.bindings.map((entry) => entry.chainId)
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
const readKey = async (apiOrigin, sessionKeyId) => {
|
|
287
|
+
let record;
|
|
288
|
+
try {
|
|
289
|
+
const handle = await open(pathFor(apiOrigin, sessionKeyId), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
290
|
+
try {
|
|
291
|
+
const stat = await handle.stat();
|
|
292
|
+
if (!stat.isFile() || stat.size > 131072 || process.platform !== "win32" && (stat.mode & 63) !== 0) throw new Error("Unsafe keystore file");
|
|
293
|
+
record = Schema.decodeUnknownSync(StoredSession)(JSON.parse(await handle.readFile("utf8")));
|
|
294
|
+
} finally {
|
|
295
|
+
await handle.close();
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
throw new SessionKeystoreError({ code: "READ_FAILED" });
|
|
299
|
+
}
|
|
300
|
+
let password;
|
|
301
|
+
try {
|
|
302
|
+
password = keyring.get(record.keyringId);
|
|
303
|
+
} catch {
|
|
304
|
+
throw new SessionKeystoreError({ code: "UNLOCK_UNAVAILABLE" });
|
|
305
|
+
}
|
|
306
|
+
if (password === null) throw new SessionKeystoreError({ code: "UNLOCK_UNAVAILABLE" });
|
|
307
|
+
const material = await openLocalSessionKey(record.encrypted, Redacted.make(password));
|
|
308
|
+
if (material.apiOrigin !== apiOrigin || material.bindings.some((binding) => binding.sessionKeyId !== sessionKeyId)) throw new SessionKeystoreError({ code: "ORIGIN_MISMATCH" });
|
|
309
|
+
return material;
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
importKey,
|
|
313
|
+
readKey
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/services/session-keystore/index.ts
|
|
319
|
+
const entry = (id) => new Entry("namera-session-keystore", id);
|
|
320
|
+
const sessionKeystore = createSessionKeystore(join(dirname(cliConfigPath), "session-keys"), {
|
|
321
|
+
get: (id) => entry(id).getPassword(),
|
|
322
|
+
set: (id, secret) => entry(id).setPassword(secret),
|
|
323
|
+
delete: (id) => entry(id).deletePassword()
|
|
324
|
+
});
|
|
325
|
+
const resolveCliSessionSigner = (apiOrigin, maxGasCostWei) => makeSessionSignerResolver(sessionKeystore, apiOrigin, maxGasCostWei);
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/services/client.ts
|
|
329
|
+
const makeCliClient = async (profile, maxGasCostWei) => {
|
|
330
|
+
const apiKey = process.env.NAMERA_API_KEY;
|
|
331
|
+
if (apiKey !== void 0) {
|
|
332
|
+
const baseUrl = process.env.NAMERA_API_URL ?? NAMERA_API_ORIGIN;
|
|
333
|
+
return {
|
|
334
|
+
config: {
|
|
335
|
+
activeProfile: "automation",
|
|
336
|
+
profiles: {}
|
|
337
|
+
},
|
|
338
|
+
profile: { baseUrl },
|
|
339
|
+
profileName: "automation",
|
|
340
|
+
client: new NameraClient({
|
|
341
|
+
apiKey,
|
|
342
|
+
baseUrl,
|
|
343
|
+
resolveSessionSigner: resolveCliSessionSigner(new URL(baseUrl).origin, maxGasCostWei)
|
|
344
|
+
})
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const resolved = await getProfile(profile);
|
|
348
|
+
return {
|
|
349
|
+
...resolved,
|
|
350
|
+
client: new NameraClient({
|
|
351
|
+
baseUrl: resolved.profile.baseUrl,
|
|
352
|
+
getAccessToken: () => getValidAccessToken(resolved.profileName, resolved.profile.baseUrl),
|
|
353
|
+
resolveSessionSigner: resolveCliSessionSigner(new URL(resolved.profile.baseUrl).origin, maxGasCostWei)
|
|
354
|
+
})
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/commands/root.ts
|
|
360
|
+
const outputFlag = Flag.choice("output", [
|
|
361
|
+
"pretty",
|
|
362
|
+
"json",
|
|
363
|
+
"ndjson"
|
|
364
|
+
]).pipe(Flag.withAlias("o"), Flag.withDescription("Output format"), Flag.withDefault("pretty"));
|
|
365
|
+
const quietFlag = Flag.boolean("quiet").pipe(Flag.withAlias("q"), Flag.withDescription("Suppress command output"));
|
|
366
|
+
const nameraCommand = Command.make("namera").pipe(Command.withSharedFlags({
|
|
367
|
+
output: outputFlag,
|
|
368
|
+
quiet: quietFlag
|
|
369
|
+
}), Command.withDescription("Manage Namera wallets through delegated session-key grants"));
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/services/output.ts
|
|
373
|
+
const replacer = (_, item) => typeof item === "bigint" ? item.toString() : item;
|
|
374
|
+
const color = (code, value, enabled) => enabled ? `\u001B[${code}m${value}\u001B[0m` : value;
|
|
375
|
+
const prettyScalar = (value, colors) => {
|
|
376
|
+
if (value === null) return color(90, "null", colors);
|
|
377
|
+
if (value === void 0) return color(90, "undefined", colors);
|
|
378
|
+
if (Predicate.isString(value)) return color(32, value, colors);
|
|
379
|
+
if (Predicate.isNumber(value) || Predicate.isBigInt(value)) return color(33, String(value), colors);
|
|
380
|
+
if (Predicate.isBoolean(value)) return color(35, String(value), colors);
|
|
381
|
+
if (Predicate.isDate(value)) return color(32, value.toISOString(), colors);
|
|
382
|
+
if (DateTime.isDateTime(value)) return color(32, DateTime.formatIso(value), colors);
|
|
383
|
+
return color(32, String(value), colors);
|
|
384
|
+
};
|
|
385
|
+
const prettyValue = (value, colors, depth = 0) => {
|
|
386
|
+
const indentation = " ".repeat(depth);
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
if (value.length === 0) return `${indentation}${color(90, "(none)", colors)}`;
|
|
389
|
+
return value.map((item, index) => `${indentation}${color(1, `${index + 1}.`, colors)}\n${prettyValue(item, colors, depth + 1)}`).join("\n");
|
|
390
|
+
}
|
|
391
|
+
if (Predicate.isObject(value) && !Predicate.isDate(value) && !DateTime.isDateTime(value)) {
|
|
392
|
+
const entries = Object.entries(value);
|
|
393
|
+
if (entries.length === 0) return `${indentation}${color(90, "(empty)", colors)}`;
|
|
394
|
+
return entries.map(([key, item]) => {
|
|
395
|
+
const label = `${indentation}${color(36, key, colors)}:`;
|
|
396
|
+
return Array.isArray(item) || Predicate.isObject(item) ? `${label}\n${prettyValue(item, colors, depth + 1)}` : `${label} ${prettyScalar(item, colors)}`;
|
|
397
|
+
}).join("\n");
|
|
398
|
+
}
|
|
399
|
+
return `${indentation}${prettyScalar(value, colors)}`;
|
|
400
|
+
};
|
|
401
|
+
const formatValue = (value, output, options = {}) => {
|
|
402
|
+
if (output === "pretty") return [options.pretty ? options.pretty(value, options.colors ?? false) : prettyValue(value, options.colors ?? false)];
|
|
403
|
+
if (output === "json") return [JSON.stringify(value, replacer)];
|
|
404
|
+
if (Array.isArray(value)) return value.map((item) => JSON.stringify(item, replacer));
|
|
405
|
+
return [JSON.stringify(value, replacer)];
|
|
406
|
+
};
|
|
407
|
+
const printValue = Effect.fn("cli.output.printValue")(function* (value, pretty) {
|
|
408
|
+
const { output, quiet } = yield* nameraCommand;
|
|
409
|
+
if (quiet) return;
|
|
410
|
+
const colors = output === "pretty" && process.stdout.isTTY && process.env.NO_COLOR === void 0;
|
|
411
|
+
for (const line of formatValue(value, output, {
|
|
412
|
+
colors,
|
|
413
|
+
...pretty ? { pretty } : {}
|
|
414
|
+
})) yield* Console.log(line);
|
|
415
|
+
});
|
|
416
|
+
const printLine = Effect.fn("cli.output.printLine")(function* (value) {
|
|
417
|
+
const { quiet } = yield* nameraCommand;
|
|
418
|
+
if (!quiet) yield* Console.log(value);
|
|
419
|
+
});
|
|
420
|
+
const unwrapResult = (result) => result.success ? Effect.succeed(result.data) : Effect.fail(new Error(result.error.kind === "api" ? result.error.code === void 0 ? result.error.message : `${result.error.code}: ${result.error.message}` : result.error.message));
|
|
421
|
+
const runPromise = (promise) => Effect.tryPromise(() => promise).pipe(Effect.flatMap(unwrapResult));
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/services/output/document.ts
|
|
425
|
+
const networks = [
|
|
426
|
+
arbitrum,
|
|
427
|
+
arbitrumSepolia,
|
|
428
|
+
base,
|
|
429
|
+
baseSepolia,
|
|
430
|
+
mainnet,
|
|
431
|
+
optimism,
|
|
432
|
+
optimismSepolia,
|
|
433
|
+
sepolia
|
|
434
|
+
];
|
|
435
|
+
const network = (chainId) => {
|
|
436
|
+
const chain = networks.find((entry) => `eip155:${entry.id}` === chainId);
|
|
437
|
+
return chain ? `${chain.name} (${chainId})` : chainId;
|
|
438
|
+
};
|
|
439
|
+
const terminalText = (value) => stripVTControlCharacters(value).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ");
|
|
440
|
+
const heading = (value, colors) => colors ? `\u001b[1m${terminalText(value)}\u001b[0m` : terminalText(value);
|
|
441
|
+
const humanize = (value) => {
|
|
442
|
+
const words = value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ");
|
|
443
|
+
return (words.charAt(0).toUpperCase() + words.slice(1)).replace(/\bId\b/g, "ID").replace(/\bApi\b/g, "API");
|
|
444
|
+
};
|
|
445
|
+
const date = (value) => `${DateTime.format(value, {
|
|
446
|
+
dateStyle: "medium",
|
|
447
|
+
timeStyle: "medium",
|
|
448
|
+
timeZone: "UTC",
|
|
449
|
+
locale: "en-GB"
|
|
450
|
+
})} UTC`;
|
|
451
|
+
const timestamp = (seconds) => date(DateTime.makeUnsafe(seconds * 1e3));
|
|
452
|
+
const scalar = (value) => {
|
|
453
|
+
if (value === null || value === void 0) return "None";
|
|
454
|
+
if (DateTime.isDateTime(value)) return date(value);
|
|
455
|
+
if (Predicate.isDate(value)) return date(DateTime.makeUnsafe(value));
|
|
456
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
457
|
+
return terminalText(String(value));
|
|
458
|
+
};
|
|
459
|
+
const nested = (value) => Predicate.isObject(value) && !DateTime.isDateTime(value) && !Predicate.isDate(value);
|
|
460
|
+
const fields = (entries, colors, indent = 2) => entries.filter(([, value]) => value !== void 0).map(([label, value]) => {
|
|
461
|
+
const prefix = " ".repeat(indent) + heading(label + ":", colors);
|
|
462
|
+
if (Array.isArray(value)) {
|
|
463
|
+
if (!value.length) return `${prefix} None`;
|
|
464
|
+
if (value.every((entry) => !nested(entry))) return `${prefix} ${value.map(scalar).join(", ")}`;
|
|
465
|
+
return `${prefix}\n${value.map((entry, index) => fields([[String(index + 1), entry]], colors, indent + 2)).join("\n")}`;
|
|
466
|
+
}
|
|
467
|
+
if (nested(value)) return `${prefix}\n${fields(Object.entries(value).map(([key, entry]) => [humanize(key), entry]), colors, indent + 2)}`;
|
|
468
|
+
return `${prefix} ${scalar(value)}`;
|
|
469
|
+
}).join("\n");
|
|
470
|
+
const section = (title, entries, colors) => `${heading(title, colors)}\n${fields(entries, colors)}`;
|
|
471
|
+
const collection = (values, singular, plural, render, colors) => values.length === 0 ? `No ${plural} found.` : `${heading(`Found ${values.length} ${values.length === 1 ? singular : plural}:`, colors)}\n\n${values.map((value) => render(value, colors)).join("\n\n")}`;
|
|
472
|
+
const named = (metadata) => metadata.logo?.type === "emoji" ? `${metadata.logo.value} ${metadata.name}` : metadata.name;
|
|
473
|
+
const recordView = (title) => (value, colors) => section(title, Object.entries(value).map(([key, entry]) => [humanize(key), entry]), colors);
|
|
474
|
+
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region src/services/output/auth.ts
|
|
477
|
+
const authView = ({ profile, actor }, colors) => section("CLI authorization", [
|
|
478
|
+
["Profile", profile],
|
|
479
|
+
["Authenticated via", actor.type === "api-key" ? "API key" : actor.type.toUpperCase()],
|
|
480
|
+
["Organization ID", actor.type === "user" ? actor.data.organization.id : actor.data.organizationId],
|
|
481
|
+
..."authorization" in actor.data ? [
|
|
482
|
+
["Authorization ID", actor.data.authorization.id],
|
|
483
|
+
["Scopes", actor.data.authorization.scopes],
|
|
484
|
+
["Expires", actor.data.authorization.expiresAt]
|
|
485
|
+
] : [],
|
|
486
|
+
..."grants" in actor.data ? [["Granted session keys", actor.data.grants.map(({ sessionKey }) => ({
|
|
487
|
+
name: sessionKey.metadata.name,
|
|
488
|
+
sessionKeyId: sessionKey.id,
|
|
489
|
+
walletId: sessionKey.walletId,
|
|
490
|
+
status: humanize(sessionKey.status)
|
|
491
|
+
}))]] : []
|
|
492
|
+
], colors);
|
|
493
|
+
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/commands/auth/status.ts
|
|
496
|
+
const authStatusCommand = Command.make("status", { profile: profileFlag }, Effect.fn(function* ({ profile }) {
|
|
497
|
+
const { client, profileName } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
498
|
+
const actor = yield* runPromise(client.auth.currentActor());
|
|
499
|
+
yield* printValue({
|
|
500
|
+
profile: profileName,
|
|
501
|
+
actor
|
|
502
|
+
}, authView);
|
|
503
|
+
})).pipe(Command.withDescription("Show the current delegated CLI actor"));
|
|
504
|
+
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region package.json
|
|
507
|
+
var version$1 = "1.0.0";
|
|
508
|
+
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/version.ts
|
|
511
|
+
const version = version$1;
|
|
512
|
+
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/commands/auth/login.ts
|
|
515
|
+
const openBrowser = (url) => {
|
|
516
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
517
|
+
const args = process.platform === "win32" ? [
|
|
518
|
+
"/c",
|
|
519
|
+
"start",
|
|
520
|
+
"",
|
|
521
|
+
url
|
|
522
|
+
] : [url];
|
|
523
|
+
spawn(command, args, {
|
|
524
|
+
detached: true,
|
|
525
|
+
stdio: "ignore"
|
|
526
|
+
}).unref();
|
|
527
|
+
};
|
|
528
|
+
const waitForToken = async (baseUrl, deviceCode, expiresIn, initialInterval) => {
|
|
529
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
530
|
+
let interval = initialInterval;
|
|
531
|
+
while (Date.now() < deadline) {
|
|
532
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
|
|
533
|
+
try {
|
|
534
|
+
return await pollDeviceToken(baseUrl, deviceCode);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
if (!(error instanceof OAuthRequestError)) throw error;
|
|
537
|
+
if (error.code === "authorization_pending") continue;
|
|
538
|
+
if (error.code === "slow_down") {
|
|
539
|
+
interval += 5;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
throw new Error("The device authorization expired. Run namera login again.");
|
|
546
|
+
};
|
|
547
|
+
const loginCommand = Command.make("login", {
|
|
548
|
+
profile: profileFlag,
|
|
549
|
+
host: Flag.string("host").pipe(Flag.withDescription("Namera API origin"), Flag.withDefault(NAMERA_API_ORIGIN)),
|
|
550
|
+
deviceName: Flag.string("device-name").pipe(Flag.withDescription("Friendly name shown on the consent screen"), Flag.withDefault(`Namera CLI on ${platform()}`))
|
|
551
|
+
}, Effect.fn(function* ({ profile, host, deviceName }) {
|
|
552
|
+
const baseUrl = new URL(host).origin;
|
|
553
|
+
const request = yield* Effect.tryPromise(() => startDeviceAuthorization(baseUrl, {
|
|
554
|
+
deviceName,
|
|
555
|
+
cliVersion: version,
|
|
556
|
+
platform: platform()
|
|
557
|
+
}));
|
|
558
|
+
yield* printLine(`Open ${request.verification_uri}`);
|
|
559
|
+
yield* printLine(`Confirm code: ${request.user_code}`);
|
|
560
|
+
yield* Effect.sync(() => openBrowser(request.verification_uri_complete));
|
|
561
|
+
const credentials = yield* Effect.tryPromise(() => waitForToken(baseUrl, request.device_code, request.expires_in, request.interval));
|
|
562
|
+
writeCredentials(profile, credentials);
|
|
563
|
+
const actor = yield* runPromise(new NameraClient({
|
|
564
|
+
accessToken: credentials.accessToken,
|
|
565
|
+
baseUrl
|
|
566
|
+
}).auth.currentActor());
|
|
567
|
+
if (actor.type !== "cli") return yield* Effect.fail(/* @__PURE__ */ new Error("Expected a CLI actor token."));
|
|
568
|
+
yield* Effect.tryPromise(() => saveProfile(profile, {
|
|
569
|
+
baseUrl,
|
|
570
|
+
authorizationId: actor.data.authorization.id,
|
|
571
|
+
organizationId: actor.data.organizationId
|
|
572
|
+
}));
|
|
573
|
+
yield* printLine(`Logged in as profile "${profile}".`);
|
|
574
|
+
})).pipe(Command.withDescription("Authorize this CLI using the browser device flow"));
|
|
575
|
+
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/commands/auth/logout.ts
|
|
578
|
+
const logoutCommand = Command.make("logout", { profile: profileFlag }, Effect.fn(function* ({ profile }) {
|
|
579
|
+
yield* Effect.sync(() => deleteCredentials(profile));
|
|
580
|
+
yield* Effect.tryPromise(() => removeProfile(profile));
|
|
581
|
+
yield* printLine(`Logged out profile "${profile}".`);
|
|
582
|
+
})).pipe(Command.withDescription("Remove a CLI profile and its stored credentials"));
|
|
583
|
+
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region src/commands/auth/index.ts
|
|
586
|
+
const authCommand = Command.make("auth").pipe(Command.withDescription("Inspect CLI authentication"), Command.withSubcommands([authStatusCommand]));
|
|
587
|
+
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/services/output/execution.ts
|
|
590
|
+
const executionStatusView = (result, colors) => [section(`Execution ${humanize(result.status).toLowerCase()}`, [
|
|
591
|
+
["Status", humanize(result.status)],
|
|
592
|
+
["Submission ID", result.submissionId],
|
|
593
|
+
["User operation", "userOperationHash" in result ? result.userOperationHash ?? void 0 : void 0],
|
|
594
|
+
...result.status === "confirmed" && "execution" in result ? [
|
|
595
|
+
["Execution ID", result.execution.id],
|
|
596
|
+
["Network", network(result.execution.data.chainId)],
|
|
597
|
+
["Transaction", result.execution.data.transactionHash],
|
|
598
|
+
["Created", result.execution.createdAt]
|
|
599
|
+
] : []
|
|
600
|
+
], colors), ...[
|
|
601
|
+
"reserved",
|
|
602
|
+
"prepared",
|
|
603
|
+
"submitted"
|
|
604
|
+
].includes(result.status) ? [`Not confirmed yet. Check again with:\n namera execution status ${result.submissionId}`] : []].join("\n\n");
|
|
605
|
+
const executionsView = (result, colors) => [collection(result.items, "confirmed execution", "confirmed executions", (item, useColors) => section(item.wallet.metadata.name, [
|
|
606
|
+
["Execution ID", item.details.id],
|
|
607
|
+
["Network", network(item.details.chainId)],
|
|
608
|
+
["Transaction", item.details.transactionHash],
|
|
609
|
+
["Session key", item.sessionKey.metadata.name],
|
|
610
|
+
["Created", item.details.createdAt]
|
|
611
|
+
], useColors), colors), ...result.nextCursor ? [`Next page:\n namera execution list --cursor ${result.nextCursor}`] : []].join("\n\n");
|
|
612
|
+
const simulationView = (result, colors) => [
|
|
613
|
+
section("Execution simulation", [
|
|
614
|
+
["Policy decision", result.allowed ? "Allowed" : "Denied"],
|
|
615
|
+
["Calls", result.callsSucceeded ? "Succeeded" : "Failed"],
|
|
616
|
+
["Wallet ID", result.walletId],
|
|
617
|
+
["Account", result.account],
|
|
618
|
+
["Network", network(result.chainId)],
|
|
619
|
+
["Session key ID", result.allowed ? result.sessionKeyId : void 0],
|
|
620
|
+
["Denials", result.allowed ? void 0 : result.denials]
|
|
621
|
+
], colors),
|
|
622
|
+
section("Simulation details", [["Gas estimate", result.simulation.userOperation], ["Calls", result.simulation.calls]], colors),
|
|
623
|
+
"Simulation only. No transaction was submitted."
|
|
624
|
+
].join("\n\n");
|
|
625
|
+
const signatureView = (result, colors) => section("Signature created", [
|
|
626
|
+
["Type", result.type === "typed-data" ? "Typed data (EIP-712)" : "Message"],
|
|
627
|
+
["Account", result.account],
|
|
628
|
+
["Wallet ID", result.walletId],
|
|
629
|
+
["Network", network(result.chainId)],
|
|
630
|
+
["Signature", result.signature]
|
|
631
|
+
], colors);
|
|
632
|
+
const verificationView = (result, colors) => section(result.valid ? "Signature valid" : "Signature invalid", [
|
|
633
|
+
["Valid", result.valid],
|
|
634
|
+
["Account", result.account],
|
|
635
|
+
["Wallet ID", result.walletId],
|
|
636
|
+
["Network", network(result.chainId)],
|
|
637
|
+
["Type", humanize(result.type)]
|
|
638
|
+
], colors);
|
|
639
|
+
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/services/prompts.ts
|
|
642
|
+
const schemaText = (message, schema, defaultValue) => Prompt.text({
|
|
643
|
+
message,
|
|
644
|
+
...defaultValue === void 0 ? {} : { default: defaultValue },
|
|
645
|
+
validate: (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.as(value), Effect.mapError((error) => error.message))
|
|
646
|
+
});
|
|
647
|
+
const makePrompts = () => ({
|
|
648
|
+
namespace: Prompt.run(Prompt.select({
|
|
649
|
+
message: "Namespace",
|
|
650
|
+
choices: [{
|
|
651
|
+
title: "EVM",
|
|
652
|
+
description: "Ethereum and EVM-compatible networks",
|
|
653
|
+
value: "eip155"
|
|
654
|
+
}]
|
|
655
|
+
})),
|
|
656
|
+
walletId: (message = "Wallet ID") => Prompt.run(schemaText(message, WalletId)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(WalletId))),
|
|
657
|
+
chainId: (message = "Chain") => Prompt.run(schemaText(message, SupportedEvmChainId, "eip155:11155111")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(SupportedEvmChainId))),
|
|
658
|
+
sessionKeyId: (message = "Session key ID") => Prompt.run(schemaText(message, SessionKeyId)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(SessionKeyId))),
|
|
659
|
+
ethereumAddress: (message) => Prompt.run(schemaText(message, EthereumAddress)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(EthereumAddress))),
|
|
660
|
+
ethereumValue: (message = "Native value (wei)") => Prompt.run(schemaText(message, Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n)), "0")).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.BigIntFromString))),
|
|
661
|
+
hex: (message, defaultValue = "0x") => Prompt.run(schemaText(message, Hex, defaultValue)).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Hex))),
|
|
662
|
+
integer: (message, options) => Prompt.run(Prompt.integer({
|
|
663
|
+
message,
|
|
664
|
+
...options
|
|
665
|
+
})),
|
|
666
|
+
confirm: (message, initial = false) => Prompt.run(Prompt.confirm({
|
|
667
|
+
message,
|
|
668
|
+
initial
|
|
669
|
+
})),
|
|
670
|
+
signatureType: Prompt.run(Prompt.select({
|
|
671
|
+
message: "Signature type",
|
|
672
|
+
choices: [{
|
|
673
|
+
title: "Message",
|
|
674
|
+
description: "Sign or verify a UTF-8 message",
|
|
675
|
+
value: "message"
|
|
676
|
+
}, {
|
|
677
|
+
title: "Typed data",
|
|
678
|
+
description: "Sign or verify an EIP-712 typed-data object",
|
|
679
|
+
value: "typed-data"
|
|
680
|
+
}]
|
|
681
|
+
})),
|
|
682
|
+
message: (message = "Message") => Prompt.run(Prompt.text({ message })),
|
|
683
|
+
typedData: Prompt.run(Prompt.text({
|
|
684
|
+
message: "EIP-712 typed data (JSON)",
|
|
685
|
+
validate: (value) => Schema.decodeUnknownEffect(Schema.fromJsonString(EvmTypedData))(value).pipe(Effect.as(value), Effect.mapError((error) => error.message))
|
|
686
|
+
})).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(EvmTypedData)))),
|
|
687
|
+
date: (message) => Prompt.run(Prompt.date({ message }))
|
|
688
|
+
});
|
|
689
|
+
var CliPrompts = class CliPrompts extends Context.Service()("@namera-ai/cli/CliPrompts") {
|
|
690
|
+
static layer = Layer.succeed(CliPrompts, CliPrompts.of(makePrompts()));
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/commands/execution/index.ts
|
|
695
|
+
const promptTransactionRequest = Effect.fn("cli.execution.promptTransactionRequest")(function* () {
|
|
696
|
+
const prompts = yield* CliPrompts;
|
|
697
|
+
const namespace = yield* prompts.namespace;
|
|
698
|
+
const walletId = yield* prompts.walletId();
|
|
699
|
+
const sessionKeyId = yield* prompts.sessionKeyId();
|
|
700
|
+
const chainId = yield* prompts.chainId();
|
|
701
|
+
const callCount = yield* prompts.integer("Number of calls", {
|
|
702
|
+
min: 1,
|
|
703
|
+
default: 1
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
namespace,
|
|
707
|
+
walletId,
|
|
708
|
+
sessionKeyId,
|
|
709
|
+
chainId,
|
|
710
|
+
calls: yield* Effect.forEach(Array.from({ length: callCount }, (_, index) => index), (index) => Effect.gen(function* () {
|
|
711
|
+
const number = callCount === 1 ? "" : ` ${index + 1}`;
|
|
712
|
+
return {
|
|
713
|
+
to: yield* prompts.ethereumAddress(`Recipient${number}`),
|
|
714
|
+
value: yield* prompts.ethereumValue(`Native value${number} (wei)`),
|
|
715
|
+
data: yield* prompts.hex(`Calldata${number}`)
|
|
716
|
+
};
|
|
717
|
+
}))
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
const promptExecutionRequest = Effect.fn("cli.execution.promptExecutionRequest")(function* () {
|
|
721
|
+
const prompts = yield* CliPrompts;
|
|
722
|
+
const request = yield* promptTransactionRequest();
|
|
723
|
+
const sponsor = yield* prompts.confirm("Sponsor gas with Namera?", true);
|
|
724
|
+
return {
|
|
725
|
+
...request,
|
|
726
|
+
sponsor
|
|
727
|
+
};
|
|
728
|
+
});
|
|
729
|
+
const execute = Command.make("execute", {
|
|
730
|
+
params: paramsFlag,
|
|
731
|
+
profile: profileFlag,
|
|
732
|
+
maxGasCost: Flag.string("max-gas-cost-wei").pipe(Flag.withDescription("Local fee ceiling for self-funded operations, in wei"), Flag.optional)
|
|
733
|
+
}, Effect.fn(function* ({ params, profile, maxGasCost }) {
|
|
734
|
+
const request = yield* resolveParams(params, ExecuteRequest, promptExecutionRequest());
|
|
735
|
+
let maxGasCostWei;
|
|
736
|
+
if (Option.isSome(maxGasCost)) maxGasCostWei = yield* Schema.decodeUnknownEffect(Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(maxGasCost.value);
|
|
737
|
+
else if (request.sponsor === false && Option.isNone(params)) maxGasCostWei = yield* (yield* CliPrompts).ethereumValue("Maximum total gas cost (wei)");
|
|
738
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile, maxGasCostWei));
|
|
739
|
+
yield* printValue(yield* runPromise(client.executions.execute(request)), executionStatusView);
|
|
740
|
+
})).pipe(Command.withDescription("Execute EVM calls after validating an inline or prompted request"));
|
|
741
|
+
const simulate = Command.make("simulate", {
|
|
742
|
+
params: paramsFlag,
|
|
743
|
+
profile: profileFlag
|
|
744
|
+
}, Effect.fn(function* ({ params, profile }) {
|
|
745
|
+
const request = yield* resolveParams(params, SimulateExecutionRequest, promptTransactionRequest());
|
|
746
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
747
|
+
yield* printValue(yield* runPromise(client.executions.simulate(request)), simulationView);
|
|
748
|
+
})).pipe(Command.withDescription("Simulate calls and evaluate session-key policies without signing or submitting"));
|
|
749
|
+
const status$1 = Command.make("status", {
|
|
750
|
+
submissionId: Argument.string("submission-id"),
|
|
751
|
+
profile: profileFlag
|
|
752
|
+
}, Effect.fn(function* ({ submissionId, profile }) {
|
|
753
|
+
const id = yield* Schema.decodeUnknownEffect(ExecutionSubmissionId)(submissionId);
|
|
754
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
755
|
+
yield* printValue(yield* runPromise(client.executions.getStatus(id)), executionStatusView);
|
|
756
|
+
}));
|
|
757
|
+
const list$2 = Command.make("list", {
|
|
758
|
+
cursor: Flag.string("cursor").pipe(Flag.optional),
|
|
759
|
+
profile: profileFlag
|
|
760
|
+
}, Effect.fn(function* ({ cursor, profile }) {
|
|
761
|
+
const decodedCursor = Option.isSome(cursor) ? yield* Schema.decodeUnknownEffect(ExecutionId)(cursor.value) : void 0;
|
|
762
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
763
|
+
yield* printValue(yield* runPromise(client.executions.list(decodedCursor === void 0 ? {} : { cursor: decodedCursor })), executionsView);
|
|
764
|
+
}));
|
|
765
|
+
const executionCommand = Command.make("execution").pipe(Command.withDescription("Execute and inspect onchain operations"), Command.withSubcommands([
|
|
766
|
+
execute,
|
|
767
|
+
simulate,
|
|
768
|
+
status$1,
|
|
769
|
+
list$2
|
|
770
|
+
]));
|
|
771
|
+
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/services/mcp/oauth-contracts.ts
|
|
774
|
+
var LocalOAuthError = class extends Schema.TaggedError()("LocalOAuthError", { code: Schema.Literals([
|
|
775
|
+
"invalid_grant",
|
|
776
|
+
"invalid_token",
|
|
777
|
+
"temporarily_unavailable"
|
|
778
|
+
]) }) {};
|
|
779
|
+
var McpOAuthUpstream = class extends Context.Service()("@namera-ai/cli/McpOAuthUpstream") {};
|
|
780
|
+
|
|
781
|
+
//#endregion
|
|
782
|
+
//#region src/services/mcp/api-client.ts
|
|
783
|
+
var McpAuthentication = class extends Context.Service()("@namera-ai/cli/McpAuthentication") {};
|
|
784
|
+
const makeMcpApiClient = Effect.fn("LocalMcpApi.authenticate")(function* (config, authorization) {
|
|
785
|
+
const resolveSigner = config.resolveSessionSigner;
|
|
786
|
+
const client = new NameraClient({
|
|
787
|
+
baseUrl: config.apiOrigin,
|
|
788
|
+
accessToken: Redacted.value(authorization.accessToken),
|
|
789
|
+
fetch: (input, init) => {
|
|
790
|
+
const timeout = AbortSignal.timeout(3e4);
|
|
791
|
+
return (config.fetch ?? globalThis.fetch)(input, {
|
|
792
|
+
...init,
|
|
793
|
+
redirect: "error",
|
|
794
|
+
signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout
|
|
795
|
+
});
|
|
796
|
+
},
|
|
797
|
+
...resolveSigner === void 0 ? {} : { resolveSessionSigner: async (request) => {
|
|
798
|
+
if (!actor.data.grants.some(({ sessionKey }) => sessionKey.id === request.sessionKeyId && sessionKey.walletId === request.walletId && sessionKey.namespace === request.namespace && sessionKey.status === "active")) throw new Error("The selected session is not authorized.");
|
|
799
|
+
return resolveSigner(request);
|
|
800
|
+
} }
|
|
801
|
+
});
|
|
802
|
+
const result = yield* Effect.tryPromise({
|
|
803
|
+
try: () => client.auth.currentActor(),
|
|
804
|
+
catch: () => new LocalOAuthError({ code: "temporarily_unavailable" })
|
|
805
|
+
});
|
|
806
|
+
if (!result.success) return yield* new LocalOAuthError({ code: result.error.status === 401 || result.error.status === 403 || result.error.kind === "api" && (result.error.tag === "Unauthorized" || result.error.tag === "Forbidden") ? "invalid_token" : "temporarily_unavailable" });
|
|
807
|
+
if (result.data.type !== "mcp") return yield* new LocalOAuthError({ code: "invalid_token" });
|
|
808
|
+
const actor = result.data;
|
|
809
|
+
const scopes = authorization.scopes.filter((scope) => actor.data.authorization.scopes.some((allowed) => allowed === scope));
|
|
810
|
+
if (!scopes.includes("mcp:read")) return yield* new LocalOAuthError({ code: "invalid_token" });
|
|
811
|
+
return {
|
|
812
|
+
client,
|
|
813
|
+
actor,
|
|
814
|
+
scopes
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/services/mcp/credential-store.ts
|
|
820
|
+
const McpProfile = Schema.String.check(Schema.isPattern(/^[a-zA-Z0-9_-]{1,64}$/));
|
|
821
|
+
const McpCredential = Schema.Struct({
|
|
822
|
+
version: Schema.Literal(1),
|
|
823
|
+
generation: Schema.String,
|
|
824
|
+
phase: Schema.Literals([
|
|
825
|
+
"ready",
|
|
826
|
+
"refreshing",
|
|
827
|
+
"authorizing",
|
|
828
|
+
"signed-out"
|
|
829
|
+
]),
|
|
830
|
+
clientId: Schema.String,
|
|
831
|
+
accessToken: Schema.String,
|
|
832
|
+
refreshToken: Schema.NullOr(Schema.String),
|
|
833
|
+
expiresAt: Schema.Number,
|
|
834
|
+
scopes: Schema.Array(Schema.Literals([
|
|
835
|
+
"mcp:read",
|
|
836
|
+
"mcp:execute",
|
|
837
|
+
"offline_access"
|
|
838
|
+
]))
|
|
839
|
+
});
|
|
840
|
+
const withCredentialLock = async (path, run) => {
|
|
841
|
+
await mkdir(dirname(path), {
|
|
842
|
+
recursive: true,
|
|
843
|
+
mode: 448
|
|
844
|
+
});
|
|
845
|
+
const database = new DatabaseSync(path);
|
|
846
|
+
try {
|
|
847
|
+
if (process.platform !== "win32") await chmod(path, 384);
|
|
848
|
+
database.exec("PRAGMA busy_timeout = 0");
|
|
849
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
850
|
+
try {
|
|
851
|
+
database.exec("BEGIN IMMEDIATE");
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (error.errcode !== 5) throw error;
|
|
854
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
return await run();
|
|
859
|
+
} finally {
|
|
860
|
+
database.exec("ROLLBACK");
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
throw new Error("Another Namera process is updating MCP credentials. Try again.");
|
|
864
|
+
} finally {
|
|
865
|
+
database.close();
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
const mcpCredentialStore = (apiOrigin, profile) => {
|
|
869
|
+
Schema.decodeUnknownSync(McpProfile)(profile);
|
|
870
|
+
const key = createHash("sha256").update(`${apiOrigin}\n${profile}`).digest("hex");
|
|
871
|
+
const entry = () => new Entry("namera-mcp", key);
|
|
872
|
+
return {
|
|
873
|
+
read: () => {
|
|
874
|
+
try {
|
|
875
|
+
const raw = entry().getPassword();
|
|
876
|
+
return raw === null ? void 0 : Schema.decodeUnknownSync(Schema.fromJsonString(McpCredential))(raw);
|
|
877
|
+
} catch {
|
|
878
|
+
throw new Error("MCP credentials could not be read from the OS keyring.");
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
write: (credentials) => {
|
|
882
|
+
try {
|
|
883
|
+
entry().setPassword(JSON.stringify(credentials));
|
|
884
|
+
} catch {
|
|
885
|
+
throw new Error("MCP credentials could not be saved to the OS keyring.");
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
lock: (run) => withCredentialLock(join(dirname(cliConfigPath), "mcp", `${key}.lock.sqlite`), run)
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/services/mcp/callback.ts
|
|
894
|
+
const openAuthorizationBrowser = (url) => new Promise((resolve, reject) => {
|
|
895
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
|
|
896
|
+
const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
897
|
+
const child = spawn(command, args, {
|
|
898
|
+
stdio: "ignore",
|
|
899
|
+
timeout: 1e4
|
|
900
|
+
});
|
|
901
|
+
child.once("error", () => reject(/* @__PURE__ */ new Error("Open the authorization URL in your browser.")));
|
|
902
|
+
child.once("exit", (code) => {
|
|
903
|
+
if (code === 0) resolve();
|
|
904
|
+
else reject(/* @__PURE__ */ new Error("Open the authorization URL in your browser."));
|
|
905
|
+
});
|
|
906
|
+
});
|
|
907
|
+
const listenForAuthorization = async (state, signal) => {
|
|
908
|
+
let accept;
|
|
909
|
+
let deny;
|
|
910
|
+
let consumed = false;
|
|
911
|
+
const code = new Promise((resolve, reject) => {
|
|
912
|
+
accept = resolve;
|
|
913
|
+
deny = reject;
|
|
914
|
+
});
|
|
915
|
+
code.catch(() => void 0);
|
|
916
|
+
let authority = "";
|
|
917
|
+
const server = createServer({
|
|
918
|
+
headersTimeout: 1e4,
|
|
919
|
+
requestTimeout: 1e4
|
|
920
|
+
}, (request, response) => {
|
|
921
|
+
response.setHeader("Cache-Control", "no-store");
|
|
922
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
923
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
924
|
+
response.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
|
|
925
|
+
const url = URL.parse(request.url ?? "/", `http://${authority}`);
|
|
926
|
+
if (!url) {
|
|
927
|
+
response.writeHead(400).end("Invalid authorization callback.");
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const received = url.searchParams.get("state") ?? "";
|
|
931
|
+
const params = [...url.searchParams.keys()];
|
|
932
|
+
const receivedBytes = Buffer.from(received);
|
|
933
|
+
const expectedBytes = Buffer.from(state);
|
|
934
|
+
const validState = receivedBytes.length === expectedBytes.length && timingSafeEqual(receivedBytes, expectedBytes);
|
|
935
|
+
if (request.method !== "GET" || request.headers.host !== authority || url.origin !== `http://${authority}` || request.headers.origin !== void 0 || (request.url?.length ?? 0) > 8192 || url.pathname !== "/oauth/callback" || consumed || params.length !== new Set(params).size || !validState || url.searchParams.has("code") === url.searchParams.has("error")) {
|
|
936
|
+
response.writeHead(400).end("Invalid authorization callback.");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
consumed = true;
|
|
940
|
+
const value = url.searchParams.get("code");
|
|
941
|
+
if (!value || value.length > 2048) {
|
|
942
|
+
deny(/* @__PURE__ */ new Error("Authorization was denied. Run namera mcp login to try again."));
|
|
943
|
+
response.writeHead(400).end("Authorization was not completed. Return to Namera.");
|
|
944
|
+
} else {
|
|
945
|
+
accept(value);
|
|
946
|
+
response.end("Authorization received. You can close this tab and return to your agent.");
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
await new Promise((resolve, reject) => {
|
|
950
|
+
server.once("error", reject);
|
|
951
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
952
|
+
});
|
|
953
|
+
const address = server.address();
|
|
954
|
+
if (!address || typeof address === "string") throw new Error("Could not open OAuth callback.");
|
|
955
|
+
authority = `127.0.0.1:${address.port}`;
|
|
956
|
+
const abort = () => {
|
|
957
|
+
deny(/* @__PURE__ */ new Error("Authorization cancelled."));
|
|
958
|
+
server.closeAllConnections();
|
|
959
|
+
server.close();
|
|
960
|
+
};
|
|
961
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
962
|
+
if (signal.aborted) abort();
|
|
963
|
+
return {
|
|
964
|
+
callback: `http://${authority}/oauth/callback`,
|
|
965
|
+
code,
|
|
966
|
+
close: () => {
|
|
967
|
+
signal.removeEventListener("abort", abort);
|
|
968
|
+
server.closeAllConnections();
|
|
969
|
+
server.close();
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/services/mcp/oauth-upstream.ts
|
|
976
|
+
const mcpOAuthUpstreamLayer = (config) => Layer.effect(McpOAuthUpstream, Effect.gen(function* () {
|
|
977
|
+
const client = yield* HttpClient.HttpClient;
|
|
978
|
+
const requestJson = Effect.fn("McpOAuthUpstream.request")(function* (request) {
|
|
979
|
+
const response = yield* client.execute(request).pipe(Effect.mapError(() => new LocalOAuthError({ code: "temporarily_unavailable" })));
|
|
980
|
+
if (response.status < 200 || response.status >= 300) return yield* new LocalOAuthError({ code: response.status >= 500 || response.status === 429 ? "temporarily_unavailable" : "invalid_grant" });
|
|
981
|
+
return yield* response.json.pipe(Effect.mapError(() => new LocalOAuthError({ code: "temporarily_unavailable" })));
|
|
982
|
+
}, Effect.provideService(FetchHttpClient.RequestInit, { redirect: "error" }), Effect.provideService(HttpIncomingMessage.MaxBodySize, FileSystem.KiB(32)), Effect.timeout("15 seconds"), Effect.catchTag("TimeoutError", () => Effect.fail(new LocalOAuthError({ code: "temporarily_unavailable" }))), Effect.scoped);
|
|
983
|
+
const tokenRequest = Effect.fn("McpOAuthUpstream.token")(function* (body) {
|
|
984
|
+
const json = yield* requestJson(HttpClientRequest.post(`${config.apiOrigin}/oauth/token`).pipe(HttpClientRequest.bodyUrlParams({
|
|
985
|
+
...body,
|
|
986
|
+
resource: config.apiOrigin
|
|
987
|
+
})));
|
|
988
|
+
const token = yield* Schema.decodeUnknownEffect(OAuthTokenResponse)(json).pipe(Effect.mapError(() => new LocalOAuthError({ code: "temporarily_unavailable" })));
|
|
989
|
+
const now = yield* Clock.currentTimeMillis;
|
|
990
|
+
return {
|
|
991
|
+
accessToken: Redacted.make(token.access_token),
|
|
992
|
+
...token.refresh_token === void 0 ? {} : { refreshToken: Redacted.make(token.refresh_token) },
|
|
993
|
+
expiresAt: now + token.expires_in * 1e3,
|
|
994
|
+
scopes: token.scope.split(/\s+/).filter(Boolean)
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
return McpOAuthUpstream.of({
|
|
998
|
+
register: Effect.fn("McpOAuthUpstream.register")(function* (name) {
|
|
999
|
+
const json = yield* requestJson(HttpClientRequest.post(`${config.apiOrigin}/oauth/register`).pipe(HttpClientRequest.bodyJsonUnsafe({
|
|
1000
|
+
client_name: `Namera local MCP: ${name}`.slice(0, 128),
|
|
1001
|
+
redirect_uris: [config.callback],
|
|
1002
|
+
token_endpoint_auth_method: "none",
|
|
1003
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1004
|
+
response_types: ["code"],
|
|
1005
|
+
application_type: "native"
|
|
1006
|
+
})));
|
|
1007
|
+
const registered = yield* Schema.decodeUnknownEffect(OAuthDynamicClientRegistrationResponse)(json).pipe(Effect.mapError(() => new LocalOAuthError({ code: "temporarily_unavailable" })));
|
|
1008
|
+
if (registered.redirect_uris.length !== 1 || registered.redirect_uris[0] !== config.callback || registered.token_endpoint_auth_method !== "none") return yield* new LocalOAuthError({ code: "temporarily_unavailable" });
|
|
1009
|
+
return registered.client_id;
|
|
1010
|
+
}),
|
|
1011
|
+
exchange: ({ clientId, code, verifier }) => tokenRequest({
|
|
1012
|
+
grant_type: "authorization_code",
|
|
1013
|
+
client_id: clientId,
|
|
1014
|
+
redirect_uri: config.callback,
|
|
1015
|
+
code,
|
|
1016
|
+
code_verifier: Redacted.value(verifier)
|
|
1017
|
+
}),
|
|
1018
|
+
refresh: ({ clientId, refreshToken, scopes }) => tokenRequest({
|
|
1019
|
+
grant_type: "refresh_token",
|
|
1020
|
+
client_id: clientId,
|
|
1021
|
+
refresh_token: Redacted.value(refreshToken),
|
|
1022
|
+
scope: scopes.join(" ")
|
|
1023
|
+
}),
|
|
1024
|
+
revoke: Effect.fn("McpOAuthUpstream.revoke")(function* (clientId, token) {
|
|
1025
|
+
const response = yield* client.execute(HttpClientRequest.post(`${config.apiOrigin}/oauth/revoke`).pipe(HttpClientRequest.bodyUrlParams({
|
|
1026
|
+
client_id: clientId,
|
|
1027
|
+
token: Redacted.value(token)
|
|
1028
|
+
}))).pipe(Effect.mapError(() => new LocalOAuthError({ code: "temporarily_unavailable" })));
|
|
1029
|
+
if (response.status < 200 || response.status >= 300) return yield* new LocalOAuthError({ code: "temporarily_unavailable" });
|
|
1030
|
+
}, Effect.provideService(FetchHttpClient.RequestInit, { redirect: "error" }), Effect.timeout("15 seconds"), Effect.catchTag("TimeoutError", () => Effect.fail(new LocalOAuthError({ code: "temporarily_unavailable" }))), Effect.scoped)
|
|
1031
|
+
});
|
|
1032
|
+
}));
|
|
1033
|
+
|
|
1034
|
+
//#endregion
|
|
1035
|
+
//#region src/services/mcp/session.ts
|
|
1036
|
+
const mcpOAuthProvider = (apiOrigin, callback) => {
|
|
1037
|
+
const layer = mcpOAuthUpstreamLayer({
|
|
1038
|
+
apiOrigin,
|
|
1039
|
+
callback
|
|
1040
|
+
}).pipe(Layer.provide(FetchHttpClient.layer));
|
|
1041
|
+
const run = (call) => Effect.runPromise(Effect.flatMap(McpOAuthUpstream, call).pipe(Effect.provide(layer)));
|
|
1042
|
+
return {
|
|
1043
|
+
register: (name) => run((provider) => provider.register(name)),
|
|
1044
|
+
exchange: (clientId, code, verifier) => run((provider) => provider.exchange({
|
|
1045
|
+
clientId,
|
|
1046
|
+
code,
|
|
1047
|
+
verifier: Redacted.make(verifier)
|
|
1048
|
+
})),
|
|
1049
|
+
refresh: (saved) => run((provider) => provider.refresh({
|
|
1050
|
+
clientId: saved.clientId,
|
|
1051
|
+
refreshToken: Redacted.make(saved.refreshToken ?? ""),
|
|
1052
|
+
scopes: saved.scopes
|
|
1053
|
+
})),
|
|
1054
|
+
revoke: (saved) => run((provider) => provider.revoke(saved.clientId, Redacted.make(saved.refreshToken ?? saved.accessToken)))
|
|
1055
|
+
};
|
|
1056
|
+
};
|
|
1057
|
+
const scopeSchema = Schema.Array(Schema.Literals([
|
|
1058
|
+
"mcp:read",
|
|
1059
|
+
"mcp:execute",
|
|
1060
|
+
"offline_access"
|
|
1061
|
+
]));
|
|
1062
|
+
const ready = (generation, clientId, token) => {
|
|
1063
|
+
const scopes = Schema.decodeUnknownSync(scopeSchema)(token.scopes);
|
|
1064
|
+
if (!scopes.includes("mcp:read")) throw new Error("MCP read permission was not granted.");
|
|
1065
|
+
return {
|
|
1066
|
+
version: 1,
|
|
1067
|
+
generation,
|
|
1068
|
+
phase: "ready",
|
|
1069
|
+
clientId,
|
|
1070
|
+
accessToken: Redacted.value(token.accessToken),
|
|
1071
|
+
refreshToken: token.refreshToken ? Redacted.value(token.refreshToken) : null,
|
|
1072
|
+
expiresAt: token.expiresAt,
|
|
1073
|
+
scopes
|
|
1074
|
+
};
|
|
1075
|
+
};
|
|
1076
|
+
const signedOut = () => ({
|
|
1077
|
+
version: 1,
|
|
1078
|
+
generation: randomUUID(),
|
|
1079
|
+
phase: "signed-out",
|
|
1080
|
+
clientId: "",
|
|
1081
|
+
accessToken: "",
|
|
1082
|
+
refreshToken: null,
|
|
1083
|
+
expiresAt: 0,
|
|
1084
|
+
scopes: []
|
|
1085
|
+
});
|
|
1086
|
+
const createMcpSession = (config) => {
|
|
1087
|
+
const { store } = config;
|
|
1088
|
+
const provider = config.provider ?? ((callback) => mcpOAuthProvider(config.apiOrigin, callback));
|
|
1089
|
+
const now = config.now ?? Date.now;
|
|
1090
|
+
const notify = config.notify ?? ((message) => process.stderr.write(`${message}\n`));
|
|
1091
|
+
let attempted = false;
|
|
1092
|
+
let pending;
|
|
1093
|
+
const lifetime = new AbortController();
|
|
1094
|
+
const login = async () => {
|
|
1095
|
+
const signal = AbortSignal.any([lifetime.signal, AbortSignal.timeout(3e5)]);
|
|
1096
|
+
const state = randomBytes(32).toString("base64url");
|
|
1097
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
1098
|
+
const generation = randomUUID();
|
|
1099
|
+
await store.lock(async () => {
|
|
1100
|
+
const previous = store.read();
|
|
1101
|
+
if (previous?.accessToken) await provider("").revoke(previous);
|
|
1102
|
+
store.write({
|
|
1103
|
+
...signedOut(),
|
|
1104
|
+
generation,
|
|
1105
|
+
phase: "authorizing"
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
const callback = await listenForAuthorization(state, signal);
|
|
1109
|
+
try {
|
|
1110
|
+
const oauth = provider(callback.callback);
|
|
1111
|
+
const clientId = await oauth.register(config.profile);
|
|
1112
|
+
const url = new URL("/oauth/authorize", config.apiOrigin);
|
|
1113
|
+
url.search = new URLSearchParams({
|
|
1114
|
+
client_id: clientId,
|
|
1115
|
+
redirect_uri: callback.callback,
|
|
1116
|
+
resource: config.apiOrigin,
|
|
1117
|
+
response_type: "code",
|
|
1118
|
+
code_challenge_method: "S256",
|
|
1119
|
+
code_challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
1120
|
+
scope: "mcp:read mcp:execute offline_access",
|
|
1121
|
+
state
|
|
1122
|
+
}).toString();
|
|
1123
|
+
notify(`Authorize Namera (${config.profile}): ${url}`);
|
|
1124
|
+
try {
|
|
1125
|
+
await (config.openBrowser ?? openAuthorizationBrowser)(url.toString());
|
|
1126
|
+
} catch {
|
|
1127
|
+
notify("Could not open your browser. Open the URL above manually.");
|
|
1128
|
+
}
|
|
1129
|
+
const code = await callback.code;
|
|
1130
|
+
signal.throwIfAborted();
|
|
1131
|
+
const token = await oauth.exchange(clientId, code, verifier);
|
|
1132
|
+
const saved = ready(generation, clientId, token);
|
|
1133
|
+
try {
|
|
1134
|
+
await store.lock(async () => {
|
|
1135
|
+
if (signal.aborted || store.read()?.generation !== generation) throw new Error("Authorization was cancelled or replaced.");
|
|
1136
|
+
store.write(saved);
|
|
1137
|
+
});
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
await oauth.revoke(saved);
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
notify(`Namera MCP profile "${config.profile}" is connected.`);
|
|
1143
|
+
} finally {
|
|
1144
|
+
callback.close();
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
const startLogin = () => {
|
|
1148
|
+
if (!pending) {
|
|
1149
|
+
attempted = true;
|
|
1150
|
+
pending = login().finally(() => {
|
|
1151
|
+
pending = void 0;
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
return pending;
|
|
1155
|
+
};
|
|
1156
|
+
const credentials = () => store.lock(async () => {
|
|
1157
|
+
const saved = store.read();
|
|
1158
|
+
if (saved?.phase !== "ready") return void 0;
|
|
1159
|
+
if (saved.expiresAt > now() + 3e4) return saved;
|
|
1160
|
+
if (!saved.refreshToken) return void 0;
|
|
1161
|
+
store.write({
|
|
1162
|
+
...saved,
|
|
1163
|
+
phase: "refreshing"
|
|
1164
|
+
});
|
|
1165
|
+
const token = await provider("").refresh(saved);
|
|
1166
|
+
if (token.scopes.some((scope) => !saved.scopes.includes(scope))) throw new Error("Refreshed MCP permissions expanded unexpectedly.");
|
|
1167
|
+
const refreshed = ready(saved.generation, saved.clientId, token);
|
|
1168
|
+
store.write(refreshed);
|
|
1169
|
+
return refreshed;
|
|
1170
|
+
});
|
|
1171
|
+
return {
|
|
1172
|
+
login: startLogin,
|
|
1173
|
+
credentials,
|
|
1174
|
+
requestLogin: () => {
|
|
1175
|
+
if (!attempted) startLogin().catch(() => notify(`MCP login did not finish. Run namera mcp login --profile ${config.profile} --host ${config.apiOrigin}`));
|
|
1176
|
+
},
|
|
1177
|
+
status: () => {
|
|
1178
|
+
const saved = store.read();
|
|
1179
|
+
return {
|
|
1180
|
+
profile: config.profile,
|
|
1181
|
+
apiOrigin: config.apiOrigin,
|
|
1182
|
+
status: saved?.phase === "ready" ? saved.expiresAt > now() ? "connected" : saved.refreshToken ? "refresh-required" : "login-required" : "login-required",
|
|
1183
|
+
scopes: saved?.phase === "ready" ? saved.scopes : []
|
|
1184
|
+
};
|
|
1185
|
+
},
|
|
1186
|
+
logout: () => store.lock(async () => {
|
|
1187
|
+
const saved = store.read();
|
|
1188
|
+
store.write(signedOut());
|
|
1189
|
+
if (saved?.accessToken) await provider("").revoke(saved);
|
|
1190
|
+
}),
|
|
1191
|
+
close: () => lifetime.abort()
|
|
1192
|
+
};
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
//#endregion
|
|
1196
|
+
//#region src/services/mcp/tool-errors.ts
|
|
1197
|
+
const messages = {
|
|
1198
|
+
NETWORK_PAUSED: "New operations on this network are paused. Do not retry until it is re-enabled.",
|
|
1199
|
+
INVALID_ARGUMENT: "Invalid tool input. Use returned IDs and the documented field formats.",
|
|
1200
|
+
UNAUTHORIZED: "The authorization expired or was revoked. Reconnect Namera.",
|
|
1201
|
+
INSUFFICIENT_SCOPE: "Reauthorize Namera with permission to execute and sign.",
|
|
1202
|
+
WALLET_NOT_FOUND: "Wallet not delegated or not found. Use a walletId from list_wallets.",
|
|
1203
|
+
SESSION_KEY_NOT_FOUND: "Session not delegated or not found. Use list_session_keys.",
|
|
1204
|
+
EXECUTION_SUBMISSION_NOT_FOUND: "Use the submissionId returned by execute_transaction.",
|
|
1205
|
+
NO_AUTHORIZED_SESSION_KEY: "The selected session does not authorize this wallet operation.",
|
|
1206
|
+
POLICY_DENIED: "The selected session's policy denied the operation. Inspect policyCode and simulate again.",
|
|
1207
|
+
IDEMPOTENCY_CONFLICT: "The operation conflicts with an earlier request.",
|
|
1208
|
+
EXECUTION_FAILED: "The execution failed. Inspect its status before submitting another operation.",
|
|
1209
|
+
EXECUTION_UNAVAILABLE: "Execution is unavailable. Check submission status before retrying a transfer.",
|
|
1210
|
+
SIGNING_FAILED: "The signature could not be created.",
|
|
1211
|
+
SIGNATURE_UNAVAILABLE: "Signature creation is unavailable.",
|
|
1212
|
+
VERIFICATION_FAILED: "Signature verification could not be completed on this chain.",
|
|
1213
|
+
LIMIT_EXCEEDED: "The workspace has reached its plan limit.",
|
|
1214
|
+
RATE_LIMITED: "Too many requests. Wait before retrying.",
|
|
1215
|
+
INTERNAL_ERROR: "Namera could not complete the operation.",
|
|
1216
|
+
UPSTREAM_UNAVAILABLE: "The API is unavailable. Check transaction status before repeating a transfer.",
|
|
1217
|
+
LOCAL_SIGNER_REQUIRED: "Configure a local signer before executing or signing.",
|
|
1218
|
+
LOCAL_SIGNER_UNAVAILABLE: "The authorized session's local key is unavailable. Import it with namera session-key import.",
|
|
1219
|
+
PREPARED_EXECUTION_INVALID: "The preparation does not match local session authorization or fee consent. Do not sign it.",
|
|
1220
|
+
PREPARED_SIGNATURE_INVALID: "The challenge does not match the original payload or local signature consent. Do not sign it.",
|
|
1221
|
+
LOCAL_SIGNATURE_INVALID: "Local signature verification failed. Check the imported session key."
|
|
1222
|
+
};
|
|
1223
|
+
const localToolError = (code) => ({
|
|
1224
|
+
code,
|
|
1225
|
+
message: messages[code],
|
|
1226
|
+
retryable: false
|
|
1227
|
+
});
|
|
1228
|
+
const unwrapMcpSdk = Effect.fn("LocalMcp.unwrapSdk")(function* (call) {
|
|
1229
|
+
const result = yield* Effect.tryPromise({
|
|
1230
|
+
try: call,
|
|
1231
|
+
catch: () => localToolError("INTERNAL_ERROR")
|
|
1232
|
+
});
|
|
1233
|
+
if (result.success) return result.data;
|
|
1234
|
+
const error = result.error;
|
|
1235
|
+
const code = error.kind === "contract" ? "INVALID_ARGUMENT" : error.kind === "network" ? "UPSTREAM_UNAVAILABLE" : error.status === 401 || error.kind === "api" && error.tag === "Unauthorized" ? "UNAUTHORIZED" : error.status === 403 || error.kind === "api" && error.tag === "Forbidden" ? "INSUFFICIENT_SCOPE" : "code" in error && Schema.is(McpToolErrorCode)(error.code) ? error.code : error.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR";
|
|
1236
|
+
const cause = error.cause;
|
|
1237
|
+
const base = localToolError(code);
|
|
1238
|
+
const diagnostic = Predicate.isObject(cause) ? {
|
|
1239
|
+
...base,
|
|
1240
|
+
...Reflect.get(cause, "policyId") === void 0 ? {} : { policyId: Reflect.get(cause, "policyId") },
|
|
1241
|
+
...Reflect.get(cause, "policyCode") === void 0 ? {} : { policyCode: Reflect.get(cause, "policyCode") }
|
|
1242
|
+
} : base;
|
|
1243
|
+
return yield* Effect.fail(Schema.is(McpToolError)(diagnostic) ? diagnostic : base);
|
|
1244
|
+
});
|
|
1245
|
+
const toolErrorResult = (failure) => {
|
|
1246
|
+
const error = Schema.is(McpToolError)(failure) ? failure : localToolError(Schema.isSchemaError(failure) ? "INVALID_ARGUMENT" : "INTERNAL_ERROR");
|
|
1247
|
+
return new McpSchema.CallToolResult({
|
|
1248
|
+
isError: true,
|
|
1249
|
+
structuredContent: { error },
|
|
1250
|
+
content: [{
|
|
1251
|
+
type: "text",
|
|
1252
|
+
text: JSON.stringify({ error })
|
|
1253
|
+
}]
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/services/mcp/tools.ts
|
|
1259
|
+
const rootObject = (schema) => {
|
|
1260
|
+
const ref = Reflect.get(schema, "$ref");
|
|
1261
|
+
const defs = Reflect.get(schema, "$defs");
|
|
1262
|
+
if (typeof ref !== "string" || !ref.startsWith("#/$defs/") || typeof defs !== "object" || defs === null) return schema;
|
|
1263
|
+
const root = Reflect.get(defs, ref.slice(8));
|
|
1264
|
+
return typeof root === "object" && root !== null ? {
|
|
1265
|
+
...root,
|
|
1266
|
+
$defs: defs
|
|
1267
|
+
} : schema;
|
|
1268
|
+
};
|
|
1269
|
+
const register = Effect.fn("LocalMcp.registerTool")(function* (tool, scope, destructive, handle) {
|
|
1270
|
+
const authentication = yield* McpAuthentication;
|
|
1271
|
+
yield* (yield* McpServer.McpServer).addTool({
|
|
1272
|
+
tool: new McpSchema.Tool({
|
|
1273
|
+
name: tool.name,
|
|
1274
|
+
description: tool.description,
|
|
1275
|
+
inputSchema: rootObject(Tool.getJsonSchema(tool)),
|
|
1276
|
+
outputSchema: rootObject(Tool.getJsonSchemaFromSchema(tool.successSchema)),
|
|
1277
|
+
annotations: {
|
|
1278
|
+
readOnlyHint: scope === "mcp:read" || tool.name === "simulate_transaction",
|
|
1279
|
+
destructiveHint: destructive,
|
|
1280
|
+
idempotentHint: tool.name !== "execute_transaction" && tool.name !== "sign",
|
|
1281
|
+
openWorldHint: true
|
|
1282
|
+
}
|
|
1283
|
+
}),
|
|
1284
|
+
annotations: Context.empty(),
|
|
1285
|
+
handle: (payload) => Effect.gen(function* () {
|
|
1286
|
+
const principal = yield* authentication.principal;
|
|
1287
|
+
if (!principal.scopes.includes(scope)) return toolErrorResult(localToolError("INSUFFICIENT_SCOPE"));
|
|
1288
|
+
const value = yield* handle(yield* Schema.decodeUnknownEffect(tool.parametersSchema)(payload), principal.client);
|
|
1289
|
+
const encoded = yield* Schema.encodeUnknownEffect(tool.successSchema)(value);
|
|
1290
|
+
return new McpSchema.CallToolResult({
|
|
1291
|
+
structuredContent: encoded,
|
|
1292
|
+
content: [{
|
|
1293
|
+
type: "text",
|
|
1294
|
+
text: JSON.stringify(encoded)
|
|
1295
|
+
}]
|
|
1296
|
+
});
|
|
1297
|
+
}).pipe(Effect.catch((error) => Effect.succeed(toolErrorResult(error))), Effect.withSpan("cli.mcp.tool", { attributes: { tool: tool.name } }))
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
const localMcpTools = Effect.gen(function* () {
|
|
1301
|
+
yield* register(Tool.make("list_wallets", {
|
|
1302
|
+
description: "List delegated wallets. Use a returned wallet id as walletId, never an address or session-key ID.",
|
|
1303
|
+
success: Schema.Struct({ wallets: ListWalletsResponse })
|
|
1304
|
+
}), "mcp:read", false, (_, client) => unwrapMcpSdk(() => client.wallets.list()).pipe(Effect.map((wallets) => ({ wallets }))));
|
|
1305
|
+
yield* register(Tool.make("get_wallet", {
|
|
1306
|
+
description: "Get one delegated wallet by walletId from list_wallets.",
|
|
1307
|
+
parameters: McpGetWalletRequest,
|
|
1308
|
+
success: Schema.Struct({ wallet: GetWalletResponse })
|
|
1309
|
+
}), "mcp:read", false, ({ walletId }, client) => unwrapMcpSdk(() => client.wallets.get(walletId)).pipe(Effect.map((wallet) => ({ wallet }))));
|
|
1310
|
+
yield* register(Tool.make("list_session_keys", {
|
|
1311
|
+
description: "List delegated session keys and their wallet IDs. Execute and sign require both walletId and sessionKeyId; do not interchange them. The local key must also be imported on this machine.",
|
|
1312
|
+
parameters: McpListSessionKeysRequest,
|
|
1313
|
+
success: Schema.Struct({ sessionKeys: ListSessionKeysForOrganizationResponse })
|
|
1314
|
+
}), "mcp:read", false, (input, client) => unwrapMcpSdk(() => client.sessionKeys.list(input)).pipe(Effect.map((sessionKeys) => ({ sessionKeys }))));
|
|
1315
|
+
yield* register(Tool.make("get_session_key", {
|
|
1316
|
+
description: "Get one delegated session key and its policies by sessionKeyId.",
|
|
1317
|
+
parameters: McpGetSessionKeyRequest,
|
|
1318
|
+
success: Schema.Struct({ sessionKey: GetSessionKeyResponse })
|
|
1319
|
+
}), "mcp:read", false, ({ sessionKeyId }, client) => unwrapMcpSdk(() => client.sessionKeys.get(sessionKeyId)).pipe(Effect.map((sessionKey) => ({ sessionKey }))));
|
|
1320
|
+
yield* register(Tool.make("simulate_transaction", {
|
|
1321
|
+
description: "Simulate the exact session and calls without signing or consuming operation usage. Execute only when allowed and callsSucceeded are true.",
|
|
1322
|
+
parameters: McpSimulateTransactionRequest,
|
|
1323
|
+
success: Schema.Struct({ simulation: SimulateExecutionResponse })
|
|
1324
|
+
}), "mcp:execute", false, (input, client) => unwrapMcpSdk(() => client.executions.simulate(input)).pipe(Effect.map((simulation) => ({ simulation }))));
|
|
1325
|
+
yield* register(Tool.make("execute_transaction", {
|
|
1326
|
+
description: "Execute exact calls using walletId and sessionKeyId from discovery tools. Simulate first. Signs locally using an imported session key. Gas is sponsored by default; sponsor:false requires a fee cap configured by the user when starting local MCP. Prepared means queued, not confirmed: poll get_transaction_status. Repeating this tool can transfer assets twice.",
|
|
1327
|
+
parameters: McpExecuteTransactionRequest,
|
|
1328
|
+
success: Schema.Struct({ execution: CompleteExecutionResponse })
|
|
1329
|
+
}), "mcp:execute", true, (input, client) => unwrapMcpSdk(() => client.executions.execute(input)).pipe(Effect.map((execution) => ({ execution }))));
|
|
1330
|
+
yield* register(Tool.make("get_transaction_status", {
|
|
1331
|
+
description: "Poll the submissionId returned by execute_transaction until confirmed or failed. Prepared and submitted are still pending.",
|
|
1332
|
+
parameters: McpGetTransactionStatusRequest,
|
|
1333
|
+
success: Schema.Struct({ submission: GetExecutionSubmissionResponse })
|
|
1334
|
+
}), "mcp:read", false, ({ submissionId }, client) => unwrapMcpSdk(() => client.executions.getStatus(submissionId)).pipe(Effect.map((submission) => ({ submission }))));
|
|
1335
|
+
yield* register(Tool.make("get_executions", {
|
|
1336
|
+
description: "Read confirmed execution history for this authorization. Pass nextCursor to fetch the next page. Pending submissions use get_transaction_status.",
|
|
1337
|
+
parameters: McpGetExecutionsRequest,
|
|
1338
|
+
success: ListExecutionsResponse
|
|
1339
|
+
}), "mcp:read", false, (input, client) => unwrapMcpSdk(() => client.executions.list(input)));
|
|
1340
|
+
yield* register(Tool.make("sign", {
|
|
1341
|
+
description: "Sign an exact UTF-8 message or EIP-712 object locally, using an explicit walletId and sessionKeyId. Requires imported signature consent and installed onchain signature permission plus passing API policies. Does not submit a transaction.",
|
|
1342
|
+
parameters: McpSignRequest,
|
|
1343
|
+
success: Schema.Struct({ signature: SignResponse })
|
|
1344
|
+
}), "mcp:execute", false, ({ request }, client) => unwrapMcpSdk(() => client.sign(request)).pipe(Effect.map((signature) => ({ signature }))));
|
|
1345
|
+
yield* register(Tool.make("verify_signature", {
|
|
1346
|
+
description: "Verify a smart-account signature against the original unmodified payload and walletId. A false validity result is not a tool failure.",
|
|
1347
|
+
parameters: McpVerifySignatureRequest,
|
|
1348
|
+
success: Schema.Struct({ verification: VerifySignatureResponse })
|
|
1349
|
+
}), "mcp:read", false, ({ request }, client) => unwrapMcpSdk(() => client.verifySignature(request)).pipe(Effect.map((verification) => ({ verification }))));
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
//#endregion
|
|
1353
|
+
//#region src/services/mcp/stdio.ts
|
|
1354
|
+
const mcpStdio = Layer.effectDiscard(localMcpTools).pipe(Layer.provide(McpServer.layerStdio({
|
|
1355
|
+
name: "Namera",
|
|
1356
|
+
version,
|
|
1357
|
+
protocols: [McpProtocol.v2025_06_18]
|
|
1358
|
+
})));
|
|
1359
|
+
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/services/mcp/transport-security.ts
|
|
1362
|
+
const LocalMcpApiOrigin = Schema.NonEmptyString.check(Schema.makeFilter((value) => {
|
|
1363
|
+
const url = URL.parse(value);
|
|
1364
|
+
return url !== null && url.username === "" && url.password === "" && url.pathname === "/" && url.search === "" && url.hash === "" && (url.protocol === "https:" || url.protocol === "http:" && [
|
|
1365
|
+
"localhost",
|
|
1366
|
+
"127.0.0.1",
|
|
1367
|
+
"[::1]"
|
|
1368
|
+
].includes(url.hostname));
|
|
1369
|
+
}, { message: "Use an HTTPS API origin, or HTTP localhost for development, without credentials, path, query or fragment" }));
|
|
1370
|
+
|
|
1371
|
+
//#endregion
|
|
1372
|
+
//#region src/commands/mcp/index.ts
|
|
1373
|
+
const flags = {
|
|
1374
|
+
profile: Flag.string("profile").pipe(Flag.withDefault("default")),
|
|
1375
|
+
host: Flag.string("host").pipe(Flag.withDefault(NAMERA_API_ORIGIN), Flag.withDescription("Namera API origin"))
|
|
1376
|
+
};
|
|
1377
|
+
const session = Effect.fn("Mcp.session")(function* (options) {
|
|
1378
|
+
const apiOrigin = new URL(yield* Schema.decodeUnknownEffect(LocalMcpApiOrigin)(options.host)).origin;
|
|
1379
|
+
const profile = yield* Schema.decodeUnknownEffect(McpProfile)(options.profile);
|
|
1380
|
+
return yield* Effect.acquireRelease(Effect.try(() => createMcpSession({
|
|
1381
|
+
apiOrigin,
|
|
1382
|
+
profile,
|
|
1383
|
+
store: mcpCredentialStore(apiOrigin, profile)
|
|
1384
|
+
})), (value) => Effect.sync(() => value.close()));
|
|
1385
|
+
});
|
|
1386
|
+
const serve = Command.make("serve", {
|
|
1387
|
+
...flags,
|
|
1388
|
+
maxGasCost: Flag.string("max-gas-cost-wei").pipe(Flag.optional, Flag.withDescription("Fee ceiling for self-funded operations; default is sponsored only"))
|
|
1389
|
+
}, Effect.fn("Mcp.serve")(function* (options) {
|
|
1390
|
+
const connection = yield* session(options);
|
|
1391
|
+
const apiOrigin = new URL(options.host).origin;
|
|
1392
|
+
const maxGasCostWei = Option.isSome(options.maxGasCost) ? yield* Schema.decodeUnknownEffect(Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n)))(options.maxGasCost.value) : void 0;
|
|
1393
|
+
const principal = Effect.gen(function* () {
|
|
1394
|
+
const credentials = yield* Effect.tryPromise({
|
|
1395
|
+
try: connection.credentials,
|
|
1396
|
+
catch: () => localToolError("UNAUTHORIZED")
|
|
1397
|
+
});
|
|
1398
|
+
if (!credentials) {
|
|
1399
|
+
connection.requestLogin();
|
|
1400
|
+
return yield* Effect.fail({
|
|
1401
|
+
...localToolError("UNAUTHORIZED"),
|
|
1402
|
+
message: `Authorize Namera in your browser, or run namera mcp login --profile ${options.profile} --host ${apiOrigin}. Then retry.`
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
return yield* makeMcpApiClient({
|
|
1406
|
+
apiOrigin,
|
|
1407
|
+
resolveSessionSigner: resolveCliSessionSigner(apiOrigin, maxGasCostWei)
|
|
1408
|
+
}, {
|
|
1409
|
+
accessToken: Redacted.make(credentials.accessToken),
|
|
1410
|
+
clientId: credentials.clientId,
|
|
1411
|
+
scopes: credentials.scopes
|
|
1412
|
+
}).pipe(Effect.mapError((error) => localToolError(error.code === "temporarily_unavailable" ? "UPSTREAM_UNAVAILABLE" : "UNAUTHORIZED")));
|
|
1413
|
+
});
|
|
1414
|
+
const io = yield* Stdio.Stdio;
|
|
1415
|
+
const ended = yield* Deferred.make();
|
|
1416
|
+
const transport = Stdio.make({
|
|
1417
|
+
...io,
|
|
1418
|
+
stdin: io.stdin.pipe(Stream.ensuring(Deferred.succeed(ended, void 0)))
|
|
1419
|
+
});
|
|
1420
|
+
yield* Layer.build(mcpStdio.pipe(Layer.provide(Layer.succeed(McpAuthentication, { principal })), Layer.provide(Layer.succeed(Stdio.Stdio, transport)))).pipe(Effect.forkScoped, Effect.flatMap(Fiber.join));
|
|
1421
|
+
yield* Deferred.await(ended);
|
|
1422
|
+
}, Effect.scoped, Effect.provide(Logger.layer([Logger.withConsoleError(Logger.formatSimple)]))));
|
|
1423
|
+
const login = Command.make("login", flags, Effect.fn("Mcp.login")(function* (options) {
|
|
1424
|
+
const connection = yield* session(options);
|
|
1425
|
+
yield* Effect.tryPromise({
|
|
1426
|
+
try: connection.login,
|
|
1427
|
+
catch: () => /* @__PURE__ */ new Error("MCP login failed. Check the browser consent and API connection, then try again.")
|
|
1428
|
+
});
|
|
1429
|
+
yield* printValue(connection.status(), recordView("MCP connection"));
|
|
1430
|
+
}, Effect.scoped));
|
|
1431
|
+
const status = Command.make("status", flags, Effect.fn("Mcp.status")(function* (options) {
|
|
1432
|
+
const connection = yield* session(options);
|
|
1433
|
+
yield* printValue(yield* Effect.try(connection.status), recordView("MCP connection"));
|
|
1434
|
+
}, Effect.scoped));
|
|
1435
|
+
const logout = Command.make("logout", flags, Effect.fn("Mcp.logout")(function* (options) {
|
|
1436
|
+
const connection = yield* session(options);
|
|
1437
|
+
yield* Effect.tryPromise({
|
|
1438
|
+
try: connection.logout,
|
|
1439
|
+
catch: () => /* @__PURE__ */ new Error("MCP logout could not be completed. Check your OS keyring and revoke the connection in Namera settings.")
|
|
1440
|
+
});
|
|
1441
|
+
yield* printValue({
|
|
1442
|
+
profile: options.profile,
|
|
1443
|
+
status: "signed-out"
|
|
1444
|
+
}, recordView("MCP signed out"));
|
|
1445
|
+
}, Effect.scoped));
|
|
1446
|
+
const mcpCommand = Command.make("mcp").pipe(Command.withDescription("Local stdio MCP with persistent OAuth authorization"), Command.withSubcommands([
|
|
1447
|
+
serve,
|
|
1448
|
+
login,
|
|
1449
|
+
status,
|
|
1450
|
+
logout
|
|
1451
|
+
]));
|
|
1452
|
+
|
|
1453
|
+
//#endregion
|
|
1454
|
+
//#region src/services/output/session-key.ts
|
|
1455
|
+
const sessionKeyView = (key, colors) => [
|
|
1456
|
+
section(named(key.metadata), [
|
|
1457
|
+
["Status", humanize(key.status)],
|
|
1458
|
+
["Session key ID", key.id],
|
|
1459
|
+
["Wallet", key.wallet.metadata.name],
|
|
1460
|
+
["Wallet ID", key.walletId],
|
|
1461
|
+
["Description", key.metadata.description ?? void 0],
|
|
1462
|
+
["Created", key.createdAt],
|
|
1463
|
+
["Revoked", key.revokedAt ?? void 0]
|
|
1464
|
+
], colors),
|
|
1465
|
+
...key.installations.map((installation) => section(network(installation.chainId), [
|
|
1466
|
+
["Status", humanize(installation.status)],
|
|
1467
|
+
["Signer", installation.authorization.signerAddress],
|
|
1468
|
+
["Starts", installation.authorization.validAfter === 0 ? "Immediately after installation" : timestamp(installation.authorization.validAfter)],
|
|
1469
|
+
["Expires", timestamp(installation.authorization.validUntil)],
|
|
1470
|
+
["Signatures", installation.authorization.allowSignatures === true ? "Enabled" : "Disabled"],
|
|
1471
|
+
["Onchain permissions", installation.authorization.permissions.map((permission) => {
|
|
1472
|
+
const { type, ...parameters } = permission;
|
|
1473
|
+
return {
|
|
1474
|
+
policy: type === "root" ? "Unrestricted account access" : humanize(type),
|
|
1475
|
+
...parameters,
|
|
1476
|
+
...type === "native-token-transfer" ? { unit: "wei" } : {},
|
|
1477
|
+
...type === "erc20-token-transfer" ? { unit: "token base units" } : {},
|
|
1478
|
+
...type === "gas-limit" ? { unit: "gas units" } : {}
|
|
1479
|
+
};
|
|
1480
|
+
})]
|
|
1481
|
+
], colors)),
|
|
1482
|
+
...key.installations.length === 0 ? [" No network installations."] : [],
|
|
1483
|
+
section("Offchain policies", [["Policies", key.policies.map(({ type, ...parameters }) => ({
|
|
1484
|
+
policy: humanize(type.replace(/^evm\./, "")),
|
|
1485
|
+
...parameters
|
|
1486
|
+
}))]], colors)
|
|
1487
|
+
].join("\n\n");
|
|
1488
|
+
const sessionKeysView = (keys, colors) => collection(keys, "session key", "session keys", (key, useColors) => section(named(key.metadata), [
|
|
1489
|
+
["Status", humanize(key.status)],
|
|
1490
|
+
["Session key ID", key.id],
|
|
1491
|
+
["Wallet", key.wallet.metadata.name],
|
|
1492
|
+
["Wallet ID", key.walletId],
|
|
1493
|
+
["Networks", key.installations.map((installation) => `${network(installation.chainId)}: ${humanize(installation.status)}`)],
|
|
1494
|
+
["Created", key.createdAt]
|
|
1495
|
+
], useColors), colors);
|
|
1496
|
+
|
|
1497
|
+
//#endregion
|
|
1498
|
+
//#region src/commands/session-key/import.ts
|
|
1499
|
+
const importSessionKeyCommand = Command.make("import", {
|
|
1500
|
+
encryptedExport: Argument.string("encrypted-export"),
|
|
1501
|
+
profile: profileFlag
|
|
1502
|
+
}, Effect.fn("cli.sessionKey.import")(function* ({ encryptedExport, profile }) {
|
|
1503
|
+
const envelope = yield* Effect.try({
|
|
1504
|
+
try: () => {
|
|
1505
|
+
if (encryptedExport.length > 131072 || !/^[A-Za-z0-9_-]+$/.test(encryptedExport)) throw new Error("Invalid export");
|
|
1506
|
+
return Schema.decodeUnknownSync(Schema.fromJsonString(EncryptedLocalSessionKey))(Buffer.from(encryptedExport, "base64url").toString("utf8"));
|
|
1507
|
+
},
|
|
1508
|
+
catch: () => new SessionKeystoreError({ code: "IMPORT_FAILED" })
|
|
1509
|
+
});
|
|
1510
|
+
const { profile: activeProfile } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1511
|
+
const password = yield* Prompt.password({ message: "Export passphrase" });
|
|
1512
|
+
const result = yield* Effect.tryPromise({
|
|
1513
|
+
try: () => sessionKeystore.importKey(envelope, password, new URL(activeProfile.baseUrl).origin),
|
|
1514
|
+
catch: () => new SessionKeystoreError({ code: "IMPORT_FAILED" })
|
|
1515
|
+
});
|
|
1516
|
+
yield* printValue({
|
|
1517
|
+
status: "imported",
|
|
1518
|
+
...result
|
|
1519
|
+
}, recordView("Session key imported"));
|
|
1520
|
+
})).pipe(Command.withDescription("Import an encrypted dashboard session export; passphrase is entered securely"));
|
|
1521
|
+
|
|
1522
|
+
//#endregion
|
|
1523
|
+
//#region src/commands/session-key/index.ts
|
|
1524
|
+
const list$1 = Command.make("list", {
|
|
1525
|
+
wallet: Flag.string("wallet").pipe(Flag.withDescription("Only list keys for one wallet"), Flag.optional),
|
|
1526
|
+
profile: profileFlag
|
|
1527
|
+
}, Effect.fn(function* ({ wallet, profile }) {
|
|
1528
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1529
|
+
const result = yield* runPromise(client.sessionKeys.list(Option.isSome(wallet) ? { walletId: yield* Schema.decodeUnknownEffect(WalletId)(wallet.value) } : {}));
|
|
1530
|
+
yield* printValue(result, sessionKeysView);
|
|
1531
|
+
}));
|
|
1532
|
+
const get$1 = Command.make("get", {
|
|
1533
|
+
sessionKeyId: Argument.string("session-key-id"),
|
|
1534
|
+
profile: profileFlag
|
|
1535
|
+
}, Effect.fn(function* ({ sessionKeyId, profile }) {
|
|
1536
|
+
const id = yield* Schema.decodeUnknownEffect(SessionKeyId)(sessionKeyId);
|
|
1537
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1538
|
+
yield* printValue(yield* runPromise(client.sessionKeys.get(id)), sessionKeyView);
|
|
1539
|
+
}));
|
|
1540
|
+
const sessionKeyCommand = Command.make("session-key").pipe(Command.withDescription("Read granted session keys and import local signing material"), Command.withSubcommands([
|
|
1541
|
+
list$1,
|
|
1542
|
+
get$1,
|
|
1543
|
+
importSessionKeyCommand
|
|
1544
|
+
]));
|
|
1545
|
+
|
|
1546
|
+
//#endregion
|
|
1547
|
+
//#region src/commands/sign.ts
|
|
1548
|
+
const promptSignRequest = Effect.fn("cli.sign.promptRequest")(function* () {
|
|
1549
|
+
const prompts = yield* CliPrompts;
|
|
1550
|
+
const namespace = yield* prompts.namespace;
|
|
1551
|
+
const walletId = yield* prompts.walletId();
|
|
1552
|
+
const sessionKeyId = yield* prompts.sessionKeyId();
|
|
1553
|
+
const chainId = yield* prompts.chainId();
|
|
1554
|
+
const type = yield* prompts.signatureType;
|
|
1555
|
+
return type === "message" ? {
|
|
1556
|
+
namespace,
|
|
1557
|
+
walletId,
|
|
1558
|
+
sessionKeyId,
|
|
1559
|
+
chainId,
|
|
1560
|
+
type,
|
|
1561
|
+
message: yield* prompts.message()
|
|
1562
|
+
} : {
|
|
1563
|
+
namespace,
|
|
1564
|
+
walletId,
|
|
1565
|
+
sessionKeyId,
|
|
1566
|
+
chainId,
|
|
1567
|
+
type,
|
|
1568
|
+
typedData: yield* prompts.typedData
|
|
1569
|
+
};
|
|
1570
|
+
});
|
|
1571
|
+
const signCommand = Command.make("sign", {
|
|
1572
|
+
params: paramsFlag,
|
|
1573
|
+
profile: profileFlag
|
|
1574
|
+
}, Effect.fn(function* ({ params, profile }) {
|
|
1575
|
+
const request = yield* resolveParams(params, PrepareSignatureRequest, promptSignRequest());
|
|
1576
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1577
|
+
yield* printValue(yield* runPromise(client.sign(request)), signatureView);
|
|
1578
|
+
})).pipe(Command.withDescription("Sign an EVM message or typed-data request"));
|
|
1579
|
+
|
|
1580
|
+
//#endregion
|
|
1581
|
+
//#region src/commands/verify-signature.ts
|
|
1582
|
+
const promptVerifySignatureRequest = Effect.fn("cli.verifySignature.promptRequest")(function* () {
|
|
1583
|
+
const prompts = yield* CliPrompts;
|
|
1584
|
+
const namespace = yield* prompts.namespace;
|
|
1585
|
+
const walletId = yield* prompts.walletId();
|
|
1586
|
+
const chainId = yield* prompts.chainId();
|
|
1587
|
+
const type = yield* prompts.signatureType;
|
|
1588
|
+
const signature = yield* prompts.hex("Signature");
|
|
1589
|
+
return type === "message" ? {
|
|
1590
|
+
namespace,
|
|
1591
|
+
walletId,
|
|
1592
|
+
chainId,
|
|
1593
|
+
type,
|
|
1594
|
+
signature,
|
|
1595
|
+
message: yield* prompts.message()
|
|
1596
|
+
} : {
|
|
1597
|
+
namespace,
|
|
1598
|
+
walletId,
|
|
1599
|
+
chainId,
|
|
1600
|
+
type,
|
|
1601
|
+
signature,
|
|
1602
|
+
typedData: yield* prompts.typedData
|
|
1603
|
+
};
|
|
1604
|
+
});
|
|
1605
|
+
const verifySignatureCommand = Command.make("verify-signature", {
|
|
1606
|
+
params: paramsFlag,
|
|
1607
|
+
profile: profileFlag
|
|
1608
|
+
}, Effect.fn(function* ({ params, profile }) {
|
|
1609
|
+
const request = yield* resolveParams(params, VerifySignatureRequest, promptVerifySignatureRequest());
|
|
1610
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1611
|
+
yield* printValue(yield* runPromise(client.verifySignature(request)), verificationView);
|
|
1612
|
+
})).pipe(Command.withDescription("Verify an EVM smart-account signature"));
|
|
1613
|
+
|
|
1614
|
+
//#endregion
|
|
1615
|
+
//#region src/services/output/wallet.ts
|
|
1616
|
+
const walletView = (wallet, colors) => section(named(wallet.metadata), [
|
|
1617
|
+
["Status", humanize(wallet.status)],
|
|
1618
|
+
["Address", wallet.address],
|
|
1619
|
+
["Wallet ID", wallet.id],
|
|
1620
|
+
["Network type", "EVM"],
|
|
1621
|
+
["Key custody", wallet.owner.custody === "local" ? "Local (user-owned)" : "Namera-managed"],
|
|
1622
|
+
["Implementation", "Alchemy Modular V2"],
|
|
1623
|
+
["Description", wallet.metadata.description ?? void 0],
|
|
1624
|
+
["Created", wallet.createdAt]
|
|
1625
|
+
], colors);
|
|
1626
|
+
const walletsView = (wallets, colors) => collection(wallets, "delegated wallet", "delegated wallets", walletView, colors);
|
|
1627
|
+
|
|
1628
|
+
//#endregion
|
|
1629
|
+
//#region src/commands/wallet/index.ts
|
|
1630
|
+
const list = Command.make("list", { profile: profileFlag }, Effect.fn(function* ({ profile }) {
|
|
1631
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1632
|
+
yield* printValue(yield* runPromise(client.wallets.list()), walletsView);
|
|
1633
|
+
}));
|
|
1634
|
+
const get = Command.make("get", {
|
|
1635
|
+
walletId: Argument.string("wallet-id"),
|
|
1636
|
+
profile: profileFlag
|
|
1637
|
+
}, Effect.fn(function* ({ walletId, profile }) {
|
|
1638
|
+
const id = yield* Schema.decodeUnknownEffect(WalletId)(walletId);
|
|
1639
|
+
const { client } = yield* Effect.tryPromise(() => makeCliClient(profile));
|
|
1640
|
+
yield* printValue(yield* runPromise(client.wallets.get(id)), walletView);
|
|
1641
|
+
}));
|
|
1642
|
+
const walletCommand = Command.make("wallet").pipe(Command.withDescription("Read wallets granted to this CLI"), Command.withSubcommands([list, get]));
|
|
1643
|
+
|
|
1644
|
+
//#endregion
|
|
1645
|
+
//#region src/index.ts
|
|
1646
|
+
nameraCommand.pipe(Command.withSubcommands([
|
|
1647
|
+
loginCommand,
|
|
1648
|
+
logoutCommand,
|
|
1649
|
+
authCommand,
|
|
1650
|
+
walletCommand,
|
|
1651
|
+
sessionKeyCommand,
|
|
1652
|
+
executionCommand,
|
|
1653
|
+
mcpCommand,
|
|
1654
|
+
signCommand,
|
|
1655
|
+
verifySignatureCommand
|
|
1656
|
+
])).pipe(Command.run({ version }), Effect.provide(CliPrompts.layer), Effect.provide(NodeServices.layer), NodeRuntime.runMain);
|
|
1657
|
+
|
|
1658
|
+
//#endregion
|
|
1659
|
+
export { };
|
|
1660
|
+
//# sourceMappingURL=index.js.map
|