run402 2.26.0 → 2.28.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 +11 -1
- package/cli.mjs +33 -6
- package/core-dist/allowance.js +24 -1
- package/core-dist/config.js +75 -3
- package/core-dist/profiles.js +196 -0
- package/lib/allowance.mjs +3 -3
- package/lib/config.mjs +16 -1
- package/lib/deploy-v2.mjs +9 -7
- package/lib/doctor.mjs +2 -1
- package/lib/init.mjs +5 -1
- package/lib/status.mjs +13 -0
- package/lib/wallet-context.mjs +223 -0
- package/lib/wallet-context.test.mjs +228 -0
- package/lib/wallets.mjs +344 -0
- package/package.json +1 -1
- package/sdk/core-dist/allowance.js +24 -1
- package/sdk/core-dist/config.js +75 -3
- package/sdk/core-dist/profiles.js +196 -0
- package/sdk/dist/credentials.d.ts +16 -0
- package/sdk/dist/credentials.d.ts.map +1 -1
- package/sdk/dist/index.d.ts +24 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +30 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/deploy.js +73 -1
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/wallets.d.ts +35 -0
- package/sdk/dist/namespaces/wallets.d.ts.map +1 -0
- package/sdk/dist/namespaces/wallets.js +50 -0
- package/sdk/dist/namespaces/wallets.js.map +1 -0
- package/sdk/dist/node/credentials.d.ts +2 -1
- package/sdk/dist/node/credentials.d.ts.map +1 -1
- package/sdk/dist/node/credentials.js +11 -1
- package/sdk/dist/node/credentials.js.map +1 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active-wallet (profile) resolution for the CLI edge.
|
|
3
|
+
*
|
|
4
|
+
* Runs at the top of cli.mjs BEFORE any subcommand module (and therefore
|
|
5
|
+
* before cli/lib/config.mjs snapshots its paths) is loaded. Resolves which
|
|
6
|
+
* named wallet a command operates on, sets `process.env.RUN402_WALLET` so all
|
|
7
|
+
* core path functions resolve under it, and emits a provenance line for
|
|
8
|
+
* non-default selections. Core itself stays env-only — the `--wallet` flag and
|
|
9
|
+
* the per-directory `.run402.json` binding are translated into the env var
|
|
10
|
+
* here, at the edge.
|
|
11
|
+
*
|
|
12
|
+
* Precedence (highest first):
|
|
13
|
+
* 1. --wallet <name> / --profile <name> (flag)
|
|
14
|
+
* 2. RUN402_WALLET / RUN402_PROFILE (env)
|
|
15
|
+
* 3. nearest .run402.local.json/.run402.json (directory binding, walk up)
|
|
16
|
+
* 4. config.json active_wallet (global `wallets use`)
|
|
17
|
+
* 5. "default" (root wallet)
|
|
18
|
+
*
|
|
19
|
+
* The flag is also the conflict resolver: when env and binding name different
|
|
20
|
+
* wallets and no flag is given, that is a hard error (not a silent pick).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { join, dirname, resolve } from "node:path";
|
|
25
|
+
import { fail } from "./sdk-errors.mjs";
|
|
26
|
+
import { isValidProfileName } from "../core-dist/config.js";
|
|
27
|
+
import { getDefaultWallet, profileExists, readMeta, profileDir } from "../core-dist/profiles.js";
|
|
28
|
+
import { readAllowance } from "../core-dist/allowance.js";
|
|
29
|
+
|
|
30
|
+
const DEFAULT = "default";
|
|
31
|
+
const GLOBAL_FLAGS = new Set(["--wallet", "--profile"]);
|
|
32
|
+
// The `wallets` group is the management + escape surface — it must work even
|
|
33
|
+
// when selection is ambiguous (so you can `wallets unbind`), and it validates
|
|
34
|
+
// its own positional targets. `init` creates wallets, so it must not fail
|
|
35
|
+
// closed on a not-yet-existing name.
|
|
36
|
+
const CONFLICT_EXEMPT = new Set(["wallets"]);
|
|
37
|
+
const EXISTENCE_EXEMPT = new Set(["wallets", "init"]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Split the global --wallet/--profile flag (and its value) out of argv so the
|
|
41
|
+
* subcommand never sees it. Pure: no core imports, no side effects. Returns
|
|
42
|
+
* the cleaned argv and the selected flag (`{ flag, value }` or null). Last
|
|
43
|
+
* occurrence wins. A missing value is left as `value: undefined` for
|
|
44
|
+
* resolveWallet to reject with a precise error.
|
|
45
|
+
*/
|
|
46
|
+
export function splitWalletFlag(rawArgv = []) {
|
|
47
|
+
const argv = [];
|
|
48
|
+
let flag = null;
|
|
49
|
+
for (let i = 0; i < rawArgv.length; i++) {
|
|
50
|
+
const a = rawArgv[i];
|
|
51
|
+
if (typeof a === "string" && a.startsWith("--") && a.includes("=")) {
|
|
52
|
+
const name = a.slice(0, a.indexOf("="));
|
|
53
|
+
if (GLOBAL_FLAGS.has(name)) {
|
|
54
|
+
flag = { flag: name, value: a.slice(a.indexOf("=") + 1) };
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (typeof a === "string" && GLOBAL_FLAGS.has(a)) {
|
|
59
|
+
const next = rawArgv[i + 1];
|
|
60
|
+
if (next === undefined || (typeof next === "string" && next.startsWith("-"))) {
|
|
61
|
+
flag = { flag: a, value: undefined };
|
|
62
|
+
} else {
|
|
63
|
+
flag = { flag: a, value: next };
|
|
64
|
+
i += 1;
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
argv.push(a);
|
|
69
|
+
}
|
|
70
|
+
return { argv, walletFlag: flag };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readBindingFrom(dir) {
|
|
74
|
+
// .run402.local.json (gitignored personal override) beats .run402.json.
|
|
75
|
+
for (const fname of [".run402.local.json", ".run402.json"]) {
|
|
76
|
+
const p = join(dir, fname);
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
|
79
|
+
const w = parsed?.wallet;
|
|
80
|
+
if (typeof w === "string" && w.trim()) return { wallet: w.trim(), file: p };
|
|
81
|
+
} catch {
|
|
82
|
+
/* missing / unreadable / malformed → skip */
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Nearest binding walking up from `startDir` to the filesystem root. */
|
|
89
|
+
export function findBinding(startDir) {
|
|
90
|
+
let dir = resolve(startDir);
|
|
91
|
+
for (;;) {
|
|
92
|
+
const b = readBindingFrom(dir);
|
|
93
|
+
if (b) return b;
|
|
94
|
+
const parent = dirname(dir);
|
|
95
|
+
if (parent === dir) return null;
|
|
96
|
+
dir = parent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function assertValidName(name, origin) {
|
|
101
|
+
if (name === DEFAULT || isValidProfileName(name)) return;
|
|
102
|
+
fail({
|
|
103
|
+
code: "BAD_WALLET_NAME",
|
|
104
|
+
message: `Invalid wallet name ${JSON.stringify(name)} (from ${origin}).`,
|
|
105
|
+
hint: "Wallet names must match /^[a-z0-9][a-z0-9_-]{0,63}$/ (lowercase letters, digits, '_' and '-').",
|
|
106
|
+
details: { name, origin },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Pure precedence resolution + conflict detection. Returns { name, source, sourceDetail }. */
|
|
111
|
+
export function resolveWallet({ walletFlag, env = {}, cwd = process.cwd(), cmd } = {}) {
|
|
112
|
+
if (walletFlag) {
|
|
113
|
+
if (walletFlag.value === undefined || walletFlag.value === "") {
|
|
114
|
+
fail({ code: "BAD_FLAG", message: `${walletFlag.flag} requires a value`, details: { flag: walletFlag.flag } });
|
|
115
|
+
}
|
|
116
|
+
assertValidName(walletFlag.value, walletFlag.flag);
|
|
117
|
+
return { name: walletFlag.value, source: "flag", sourceDetail: walletFlag.flag };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const envRaw = env.RUN402_WALLET ?? env.RUN402_PROFILE;
|
|
121
|
+
const envName = typeof envRaw === "string" && envRaw.trim() ? envRaw.trim() : null;
|
|
122
|
+
const binding = findBinding(cwd);
|
|
123
|
+
|
|
124
|
+
if (envName && binding && envName !== binding.wallet && !CONFLICT_EXEMPT.has(cmd)) {
|
|
125
|
+
fail({
|
|
126
|
+
code: "WALLET_SELECTION_CONFLICT",
|
|
127
|
+
message: `Ambiguous wallet: RUN402_WALLET=${envName} but ${binding.file} selects '${binding.wallet}'.`,
|
|
128
|
+
hint: "Resolve with one of: pass --wallet <name>, unset RUN402_WALLET, or run402 wallets unbind.",
|
|
129
|
+
details: { env_wallet: envName, binding_wallet: binding.wallet, binding_file: binding.file },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (envName) {
|
|
134
|
+
assertValidName(envName, "RUN402_WALLET");
|
|
135
|
+
return { name: envName, source: "env", sourceDetail: "RUN402_WALLET" };
|
|
136
|
+
}
|
|
137
|
+
if (binding) {
|
|
138
|
+
assertValidName(binding.wallet, binding.file);
|
|
139
|
+
return { name: binding.wallet, source: "binding", sourceDetail: binding.file };
|
|
140
|
+
}
|
|
141
|
+
const def = getDefaultWallet();
|
|
142
|
+
if (def && def !== DEFAULT) return { name: def, source: "config", sourceDetail: "wallets use" };
|
|
143
|
+
return { name: DEFAULT, source: "default", sourceDetail: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function looksLikeAddress(s) {
|
|
147
|
+
return typeof s === "string" && /^0x[a-fA-F0-9]{40}$/.test(s);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Fail closed when a non-default selection names a wallet that does not exist locally. */
|
|
151
|
+
export function enforceWalletExists({ name, source }, cmd) {
|
|
152
|
+
if (name === DEFAULT) return;
|
|
153
|
+
if (EXISTENCE_EXEMPT.has(cmd)) return;
|
|
154
|
+
if (profileExists(name)) return;
|
|
155
|
+
const hint = looksLikeAddress(name)
|
|
156
|
+
? `'${name}' looks like an address. For billing use: run402 billing ... --wallet-address ${name}`
|
|
157
|
+
: `Run 'run402 wallets list' to see wallets, or 'run402 wallets new ${name}' to create it.`;
|
|
158
|
+
fail({
|
|
159
|
+
code: "WALLET_NOT_FOUND",
|
|
160
|
+
message: `No local wallet named '${name}'.`,
|
|
161
|
+
hint,
|
|
162
|
+
details: { wallet: name, source },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function shortAddr(a) {
|
|
167
|
+
return typeof a === "string" && a.length >= 12 ? `${a.slice(0, 6)}…${a.slice(-4)}` : a;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Best-effort address for display: meta.json (no key) first, allowance second. */
|
|
171
|
+
function walletAddress(name) {
|
|
172
|
+
const meta = readMeta(name);
|
|
173
|
+
if (meta?.address) return meta.address;
|
|
174
|
+
try {
|
|
175
|
+
return readAllowance(join(profileDir(name), "allowance.json"))?.address ?? null;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Emit the stderr provenance line for non-default selections (silent for default). */
|
|
182
|
+
export function emitProvenance({ name, source, sourceDetail }, { cmd, quiet } = {}) {
|
|
183
|
+
if (quiet) return;
|
|
184
|
+
if (name === DEFAULT) return;
|
|
185
|
+
if (cmd === "wallets") return; // the wallets group reports its own context
|
|
186
|
+
const where =
|
|
187
|
+
source === "env" ? "RUN402_WALLET" :
|
|
188
|
+
source === "config" ? "wallets use" :
|
|
189
|
+
sourceDetail || source;
|
|
190
|
+
const addr = walletAddress(name);
|
|
191
|
+
const addrPart = addr ? ` (${shortAddr(addr)})` : "";
|
|
192
|
+
process.stderr.write(` ↪ wallet: ${name}${addrPart} ← ${where}\n`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Orchestrate edge resolution: resolve → fail-closed → publish to the env so
|
|
197
|
+
* core paths resolve → provenance. Returns the resolved selection.
|
|
198
|
+
*/
|
|
199
|
+
export function applyWalletSelection({ walletFlag, cmd, cwd = process.cwd(), env = process.env, quiet = false } = {}) {
|
|
200
|
+
// Capture the pre-resolution signals so `wallets current` can report
|
|
201
|
+
// provenance and any env-vs-binding divergence (it can't recompute them once
|
|
202
|
+
// we overwrite RUN402_WALLET below).
|
|
203
|
+
const envRaw = env.RUN402_WALLET ?? env.RUN402_PROFILE;
|
|
204
|
+
const envName = typeof envRaw === "string" && envRaw.trim() ? envRaw.trim() : null;
|
|
205
|
+
const binding = findBinding(cwd);
|
|
206
|
+
|
|
207
|
+
const resolved = resolveWallet({ walletFlag, env, cwd, cmd });
|
|
208
|
+
enforceWalletExists(resolved, cmd);
|
|
209
|
+
|
|
210
|
+
// Publish to the env so all core path functions resolve under this wallet.
|
|
211
|
+
env.RUN402_WALLET = resolved.name;
|
|
212
|
+
env.RUN402_ACTIVE_WALLET_JSON = JSON.stringify({
|
|
213
|
+
name: resolved.name,
|
|
214
|
+
source: resolved.source,
|
|
215
|
+
sourceDetail: resolved.sourceDetail,
|
|
216
|
+
binding: binding ? { wallet: binding.wallet, file: binding.file } : null,
|
|
217
|
+
envName,
|
|
218
|
+
diverged: !!(envName && binding && envName !== binding.wallet),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
emitProvenance(resolved, { cmd, quiet });
|
|
222
|
+
return resolved;
|
|
223
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import {
|
|
7
|
+
splitWalletFlag,
|
|
8
|
+
findBinding,
|
|
9
|
+
resolveWallet,
|
|
10
|
+
enforceWalletExists,
|
|
11
|
+
emitProvenance,
|
|
12
|
+
} from "./wallet-context.mjs";
|
|
13
|
+
import { ensureProfileDir, setDefaultWallet } from "../core-dist/profiles.js";
|
|
14
|
+
|
|
15
|
+
const origConfigDir = process.env.RUN402_CONFIG_DIR;
|
|
16
|
+
let tmp;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tmp = mkdtempSync(join(tmpdir(), "wallet-ctx-"));
|
|
20
|
+
process.env.RUN402_CONFIG_DIR = tmp;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
25
|
+
if (origConfigDir !== undefined) process.env.RUN402_CONFIG_DIR = origConfigDir;
|
|
26
|
+
else delete process.env.RUN402_CONFIG_DIR;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Capture a fail() invocation: fail() does console.error(envelope) + process.exit.
|
|
30
|
+
function captureFail(fn) {
|
|
31
|
+
const origExit = process.exit;
|
|
32
|
+
const origErr = console.error;
|
|
33
|
+
let envelope = null;
|
|
34
|
+
let exited = false;
|
|
35
|
+
console.error = (s) => { try { envelope = JSON.parse(s); } catch { envelope = s; } };
|
|
36
|
+
process.exit = () => { exited = true; throw new Error("__EXIT__"); };
|
|
37
|
+
try {
|
|
38
|
+
fn();
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (!String(e?.message).startsWith("__EXIT__")) throw e;
|
|
41
|
+
} finally {
|
|
42
|
+
process.exit = origExit;
|
|
43
|
+
console.error = origErr;
|
|
44
|
+
}
|
|
45
|
+
return { envelope, exited };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bindingDir(wallet, localWallet) {
|
|
49
|
+
const dir = mkdtempSync(join(tmpdir(), "binding-"));
|
|
50
|
+
if (wallet) writeFileSync(join(dir, ".run402.json"), JSON.stringify({ wallet }));
|
|
51
|
+
if (localWallet) writeFileSync(join(dir, ".run402.local.json"), JSON.stringify({ wallet: localWallet }));
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("splitWalletFlag", () => {
|
|
56
|
+
it("passes through argv with no global flag", () => {
|
|
57
|
+
assert.deepEqual(splitWalletFlag(["status"]), { argv: ["status"], walletFlag: null });
|
|
58
|
+
});
|
|
59
|
+
it("strips --wallet <value>", () => {
|
|
60
|
+
const r = splitWalletFlag(["--wallet", "kychon", "status"]);
|
|
61
|
+
assert.deepEqual(r.argv, ["status"]);
|
|
62
|
+
assert.deepEqual(r.walletFlag, { flag: "--wallet", value: "kychon" });
|
|
63
|
+
});
|
|
64
|
+
it("strips --wallet=<value> mid-args", () => {
|
|
65
|
+
const r = splitWalletFlag(["deploy", "apply", "--wallet=foo", "--manifest", "x"]);
|
|
66
|
+
assert.deepEqual(r.argv, ["deploy", "apply", "--manifest", "x"]);
|
|
67
|
+
assert.equal(r.walletFlag.value, "foo");
|
|
68
|
+
});
|
|
69
|
+
it("accepts --profile as an alias", () => {
|
|
70
|
+
const r = splitWalletFlag(["--profile", "p", "status"]);
|
|
71
|
+
assert.equal(r.walletFlag.flag, "--profile");
|
|
72
|
+
assert.equal(r.walletFlag.value, "p");
|
|
73
|
+
});
|
|
74
|
+
it("last occurrence wins", () => {
|
|
75
|
+
const r = splitWalletFlag(["--wallet", "a", "--wallet", "b", "status"]);
|
|
76
|
+
assert.equal(r.walletFlag.value, "b");
|
|
77
|
+
assert.deepEqual(r.argv, ["status"]);
|
|
78
|
+
});
|
|
79
|
+
it("records a missing value as undefined", () => {
|
|
80
|
+
const r = splitWalletFlag(["status", "--wallet"]);
|
|
81
|
+
assert.deepEqual(r.argv, ["status"]);
|
|
82
|
+
assert.equal(r.walletFlag.value, undefined);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("findBinding", () => {
|
|
87
|
+
it("finds the nearest .run402.json walking up", () => {
|
|
88
|
+
const root = bindingDir("client-a");
|
|
89
|
+
const sub = join(root, "api");
|
|
90
|
+
mkdirSync(sub);
|
|
91
|
+
const b = findBinding(sub);
|
|
92
|
+
assert.equal(b.wallet, "client-a");
|
|
93
|
+
rmSync(root, { recursive: true, force: true });
|
|
94
|
+
});
|
|
95
|
+
it(".run402.local.json overrides .run402.json in the same dir", () => {
|
|
96
|
+
const dir = bindingDir("client-a", "client-a-staging");
|
|
97
|
+
assert.equal(findBinding(dir).wallet, "client-a-staging");
|
|
98
|
+
rmSync(dir, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
it("returns null when no binding exists in the tree", () => {
|
|
101
|
+
const dir = mkdtempSync(join(tmpdir(), "nobind-"));
|
|
102
|
+
assert.equal(findBinding(dir), null);
|
|
103
|
+
rmSync(dir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("resolveWallet — precedence", () => {
|
|
108
|
+
it("flag beats everything", () => {
|
|
109
|
+
const dir = bindingDir("client-a");
|
|
110
|
+
const r = resolveWallet({ walletFlag: { flag: "--wallet", value: "kychon" }, env: { RUN402_WALLET: "personal" }, cwd: dir, cmd: "status" });
|
|
111
|
+
assert.equal(r.name, "kychon");
|
|
112
|
+
assert.equal(r.source, "flag");
|
|
113
|
+
rmSync(dir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
it("env beats binding when they agree is moot; env returned when no binding", () => {
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), "nobind-"));
|
|
117
|
+
const r = resolveWallet({ env: { RUN402_WALLET: "kychon" }, cwd: dir, cmd: "status" });
|
|
118
|
+
assert.equal(r.name, "kychon");
|
|
119
|
+
assert.equal(r.source, "env");
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
it("binding used when no flag/env", () => {
|
|
123
|
+
const dir = bindingDir("client-a");
|
|
124
|
+
const r = resolveWallet({ env: {}, cwd: dir, cmd: "status" });
|
|
125
|
+
assert.equal(r.name, "client-a");
|
|
126
|
+
assert.equal(r.source, "binding");
|
|
127
|
+
rmSync(dir, { recursive: true, force: true });
|
|
128
|
+
});
|
|
129
|
+
it("global default (wallets use) applies when no flag/env/binding", () => {
|
|
130
|
+
setDefaultWallet("kychon");
|
|
131
|
+
const dir = mkdtempSync(join(tmpdir(), "nobind-"));
|
|
132
|
+
const r = resolveWallet({ env: {}, cwd: dir, cmd: "status" });
|
|
133
|
+
assert.equal(r.name, "kychon");
|
|
134
|
+
assert.equal(r.source, "config");
|
|
135
|
+
rmSync(dir, { recursive: true, force: true });
|
|
136
|
+
});
|
|
137
|
+
it("falls back to default when nothing selects", () => {
|
|
138
|
+
const dir = mkdtempSync(join(tmpdir(), "nobind-"));
|
|
139
|
+
const r = resolveWallet({ env: {}, cwd: dir, cmd: "status" });
|
|
140
|
+
assert.equal(r.name, "default");
|
|
141
|
+
assert.equal(r.source, "default");
|
|
142
|
+
rmSync(dir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("resolveWallet — conflict + validation", () => {
|
|
147
|
+
it("env vs binding mismatch is a hard error (non-wallets command)", () => {
|
|
148
|
+
const dir = bindingDir("client-a");
|
|
149
|
+
const { envelope, exited } = captureFail(() =>
|
|
150
|
+
resolveWallet({ env: { RUN402_WALLET: "personal" }, cwd: dir, cmd: "deploy" }));
|
|
151
|
+
assert.ok(exited);
|
|
152
|
+
assert.equal(envelope.code, "WALLET_SELECTION_CONFLICT");
|
|
153
|
+
assert.match(envelope.message, /personal/);
|
|
154
|
+
assert.match(envelope.message, /client-a/);
|
|
155
|
+
rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
});
|
|
157
|
+
it("matching env and binding proceed without error", () => {
|
|
158
|
+
const dir = bindingDir("client-a");
|
|
159
|
+
const r = resolveWallet({ env: { RUN402_WALLET: "client-a" }, cwd: dir, cmd: "deploy" });
|
|
160
|
+
assert.equal(r.name, "client-a");
|
|
161
|
+
rmSync(dir, { recursive: true, force: true });
|
|
162
|
+
});
|
|
163
|
+
it("the wallets group is exempt from the conflict error", () => {
|
|
164
|
+
const dir = bindingDir("client-a");
|
|
165
|
+
const r = resolveWallet({ env: { RUN402_WALLET: "personal" }, cwd: dir, cmd: "wallets" });
|
|
166
|
+
assert.equal(r.name, "personal"); // env still wins; no error
|
|
167
|
+
rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
});
|
|
169
|
+
it("rejects an invalid wallet name", () => {
|
|
170
|
+
const dir = mkdtempSync(join(tmpdir(), "nobind-"));
|
|
171
|
+
const { envelope } = captureFail(() =>
|
|
172
|
+
resolveWallet({ env: { RUN402_WALLET: "../evil" }, cwd: dir, cmd: "status" }));
|
|
173
|
+
assert.equal(envelope.code, "BAD_WALLET_NAME");
|
|
174
|
+
rmSync(dir, { recursive: true, force: true });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("enforceWalletExists — fail closed", () => {
|
|
179
|
+
it("no-op for default", () => {
|
|
180
|
+
assert.doesNotThrow(() => enforceWalletExists({ name: "default", source: "default" }, "deploy"));
|
|
181
|
+
});
|
|
182
|
+
it("no-op for an existing wallet", () => {
|
|
183
|
+
ensureProfileDir("client-a");
|
|
184
|
+
writeFileSync(join(tmp, "profiles", "client-a", "allowance.json"), "{}");
|
|
185
|
+
assert.doesNotThrow(() => enforceWalletExists({ name: "client-a", source: "binding" }, "deploy"));
|
|
186
|
+
});
|
|
187
|
+
it("fails closed for a missing wallet on a normal command", () => {
|
|
188
|
+
const { envelope, exited } = captureFail(() =>
|
|
189
|
+
enforceWalletExists({ name: "ghost", source: "binding" }, "deploy"));
|
|
190
|
+
assert.ok(exited);
|
|
191
|
+
assert.equal(envelope.code, "WALLET_NOT_FOUND");
|
|
192
|
+
});
|
|
193
|
+
it("wallets + init are exempt (create paths)", () => {
|
|
194
|
+
assert.doesNotThrow(() => enforceWalletExists({ name: "ghost", source: "flag" }, "wallets"));
|
|
195
|
+
assert.doesNotThrow(() => enforceWalletExists({ name: "ghost", source: "flag" }, "init"));
|
|
196
|
+
});
|
|
197
|
+
it("address-looking name hints at billing --wallet-address", () => {
|
|
198
|
+
const { envelope } = captureFail(() =>
|
|
199
|
+
enforceWalletExists({ name: "0x" + "a".repeat(40), source: "flag" }, "deploy"));
|
|
200
|
+
assert.match(envelope.hint, /--wallet-address/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("emitProvenance", () => {
|
|
205
|
+
function captureStderr(fn) {
|
|
206
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
207
|
+
let out = "";
|
|
208
|
+
process.stderr.write = (c) => { out += typeof c === "string" ? c : Buffer.from(c).toString("utf8"); return true; };
|
|
209
|
+
try { fn(); } finally { process.stderr.write = orig; }
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
it("stays silent for the default wallet", () => {
|
|
213
|
+
assert.equal(captureStderr(() => emitProvenance({ name: "default", source: "default" }, { cmd: "status" })), "");
|
|
214
|
+
});
|
|
215
|
+
it("emits a provenance line for a named wallet", () => {
|
|
216
|
+
ensureProfileDir("kychon");
|
|
217
|
+
writeFileSync(join(tmp, "profiles", "kychon", "meta.json"), JSON.stringify({ name: "kychon", address: "0x1234567890abcdef" }));
|
|
218
|
+
const out = captureStderr(() => emitProvenance({ name: "kychon", source: "env", sourceDetail: "RUN402_WALLET" }, { cmd: "status" }));
|
|
219
|
+
assert.match(out, /wallet: kychon/);
|
|
220
|
+
assert.match(out, /RUN402_WALLET/);
|
|
221
|
+
});
|
|
222
|
+
it("honors --quiet", () => {
|
|
223
|
+
assert.equal(captureStderr(() => emitProvenance({ name: "kychon", source: "env" }, { cmd: "status", quiet: true })), "");
|
|
224
|
+
});
|
|
225
|
+
it("stays silent for the wallets group", () => {
|
|
226
|
+
assert.equal(captureStderr(() => emitProvenance({ name: "kychon", source: "flag" }, { cmd: "wallets" })), "");
|
|
227
|
+
});
|
|
228
|
+
});
|