run402 2.27.0 → 2.29.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 +15 -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/doctor.mjs +47 -2
- package/lib/functions.mjs +128 -0
- 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/admin.d.ts +15 -0
- package/sdk/dist/namespaces/admin.d.ts.map +1 -1
- package/sdk/dist/namespaces/admin.js.map +1 -1
- package/sdk/dist/namespaces/functions.d.ts +42 -2
- package/sdk/dist/namespaces/functions.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.js +52 -1
- package/sdk/dist/namespaces/functions.js.map +1 -1
- package/sdk/dist/namespaces/functions.types.d.ts +54 -0
- package/sdk/dist/namespaces/functions.types.d.ts.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
- package/sdk/dist/scoped.d.ts +3 -1
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +6 -0
- package/sdk/dist/scoped.js.map +1 -1
package/README.md
CHANGED
|
@@ -118,8 +118,12 @@ run402 functions deploy <id> my-fn --file fn.ts \
|
|
|
118
118
|
--deps "stripe,zod@^3"
|
|
119
119
|
run402 functions logs <id> my-fn --tail 100 --request-id req_abc123 --follow
|
|
120
120
|
run402 functions invoke <id> my-fn --body '{"hello":"world"}'
|
|
121
|
+
run402 functions rebuild <id> my-fn # refresh ONE function onto the current runtime
|
|
122
|
+
run402 functions rebuild <id> --all # refresh every function in the project
|
|
121
123
|
```
|
|
122
124
|
|
|
125
|
+
`functions rebuild` is opt-in and never changes your source: it re-bundles the stored source against the platform's current runtime/entry-wrapper (deps pinned to the exact versions recorded at deploy), so a gateway-side wrapper fix (e.g. an SSR `auth.*` fix) reaches an already-deployed function — a plain redeploy with unchanged source does not. The source `code_hash` is unchanged and no new release is created. Functions deployed before dependency locking return `CANNOT_REBUILD_UNLOCKED_DEPS`; redeploy those from source instead. `run402 doctor` flags functions on a stale runtime.
|
|
126
|
+
|
|
123
127
|
Functions run on Node 22 with `@run402/functions` auto-bundled. Inside the handler:
|
|
124
128
|
|
|
125
129
|
```ts
|
|
@@ -193,8 +197,18 @@ Local state lives at:
|
|
|
193
197
|
|
|
194
198
|
- `~/.config/run402/projects.json` (`0600`) — project credentials (`anon_key`, `service_key`, `tier`, `lease_expires_at`)
|
|
195
199
|
- `~/.config/run402/allowance.json` (`0600`) — wallet for x402 signing
|
|
200
|
+
- `~/.config/run402/config.json` (`0600`) — global default wallet pointer (`active_wallet`)
|
|
201
|
+
- `~/.config/run402/profiles/<name>/` (`0700`) — named wallets, each with its own `allowance.json` + `projects.json` + non-secret `meta.json`
|
|
202
|
+
|
|
203
|
+
Override the base directory with `RUN402_CONFIG_DIR` or the allowance file with `RUN402_ALLOWANCE_PATH`. Override the API base with `RUN402_API_BASE`.
|
|
204
|
+
|
|
205
|
+
### Named wallets (profiles)
|
|
206
|
+
|
|
207
|
+
Hold several wallets on one machine and select between them:
|
|
196
208
|
|
|
197
|
-
|
|
209
|
+
- `run402 wallets list | new <name> | use <name> | rename <old> <new> | bind [<name>] | unbind | import <name> --key <path|-> | rm <name> --yes`
|
|
210
|
+
- Select per-command with `--wallet <name>` (alias `--profile`), the `RUN402_WALLET` env var, or a per-directory `.run402.json` (commit-safe — holds only a name) resolved by walking up the tree. Precedence: flag > env > binding > `wallets use` default > `default`. A conflicting env + binding is a hard error.
|
|
211
|
+
- The active wallet name shows in `run402 status` and `run402 wallets current`.
|
|
198
212
|
|
|
199
213
|
The CLI handles all x402 payment signing automatically — never ask the human for a private key or set up payment libraries by hand.
|
|
200
214
|
|
package/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const rawArgv = process.argv.slice(2);
|
|
10
10
|
|
|
11
11
|
const { version } = JSON.parse(
|
|
12
12
|
readFileSync(new URL("./package.json", import.meta.url), "utf8")
|
|
@@ -22,6 +22,7 @@ Commands:
|
|
|
22
22
|
init Set up allowance, funding, and check tier status (x402 default)
|
|
23
23
|
init mpp Set up with MPP payment rail (Tempo Moderato testnet)
|
|
24
24
|
status Show full account state (allowance, balance, tier, projects)
|
|
25
|
+
wallets Manage multiple named wallets (list, new, use, rename, bind, import)
|
|
25
26
|
allowance Manage your agent allowance (create, fund, balance, status)
|
|
26
27
|
tier Manage tier subscription (status, set)
|
|
27
28
|
projects Manage projects (provision, list, query, inspect, delete)
|
|
@@ -53,6 +54,10 @@ Commands:
|
|
|
53
54
|
dev Run Astro dev with Run402 env + credentials in scope
|
|
54
55
|
logs Fetch function logs by request id (--request-id req_...)
|
|
55
56
|
|
|
57
|
+
Global options (any command):
|
|
58
|
+
--wallet <name> Select a named wallet for this command (see 'run402 wallets')
|
|
59
|
+
Also: RUN402_WALLET env, or a ./.run402.json directory binding.
|
|
60
|
+
|
|
56
61
|
Run 'run402 <command> --help' for detailed usage of each command.
|
|
57
62
|
|
|
58
63
|
Examples:
|
|
@@ -74,22 +79,39 @@ Getting started:
|
|
|
74
79
|
run402 ci link github --project prj_... --manifest run402.deploy.json
|
|
75
80
|
`;
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
const first = rawArgv[0];
|
|
83
|
+
|
|
84
|
+
if (first === '--version' || first === '-v') {
|
|
78
85
|
console.log(version);
|
|
79
86
|
process.exit(0);
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
if (
|
|
89
|
+
if (first === undefined || first === '--help' || first === '-h') {
|
|
83
90
|
console.log(HELP);
|
|
84
91
|
process.exit(0);
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
// Resolve the active wallet/profile from the global --wallet/--profile flag,
|
|
95
|
+
// env, and any per-directory .run402.json binding BEFORE dispatch loads a
|
|
96
|
+
// subcommand (whose config.mjs snapshots credential paths). splitWalletFlag
|
|
97
|
+
// also strips the global flag so subcommands never see it.
|
|
98
|
+
const { splitWalletFlag, applyWalletSelection } = await import("./lib/wallet-context.mjs");
|
|
99
|
+
const { argv, walletFlag } = splitWalletFlag(rawArgv);
|
|
100
|
+
const [cmd, sub, ...rest] = argv;
|
|
101
|
+
|
|
87
102
|
try {
|
|
103
|
+
applyWalletSelection({
|
|
104
|
+
walletFlag,
|
|
105
|
+
cmd,
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
env: process.env,
|
|
108
|
+
quiet: rawArgv.includes("--quiet"),
|
|
109
|
+
});
|
|
88
110
|
await dispatch();
|
|
89
111
|
} catch (err) {
|
|
90
|
-
// Surface env/config errors (e.g. invalid RUN402_API_BASE
|
|
91
|
-
// JSON envelope on stderr instead of a raw stack trace. We import
|
|
92
|
-
// helper lazily so a broken env doesn't fail this catch handler too.
|
|
112
|
+
// Surface env/config errors (e.g. invalid RUN402_API_BASE, bad RUN402_WALLET)
|
|
113
|
+
// as a clean JSON envelope on stderr instead of a raw stack trace. We import
|
|
114
|
+
// the helper lazily so a broken env doesn't fail this catch handler too.
|
|
93
115
|
const { fail } = await import("./lib/sdk-errors.mjs");
|
|
94
116
|
fail({
|
|
95
117
|
code: "BAD_ENV",
|
|
@@ -112,6 +134,11 @@ switch (cmd) {
|
|
|
112
134
|
await run([sub, ...rest].filter(Boolean));
|
|
113
135
|
break;
|
|
114
136
|
}
|
|
137
|
+
case "wallets": {
|
|
138
|
+
const { run } = await import("./lib/wallets.mjs");
|
|
139
|
+
await run(sub, rest);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
115
142
|
case "allowance": {
|
|
116
143
|
const { run } = await import("./lib/allowance.mjs");
|
|
117
144
|
await run(sub, rest);
|
package/core-dist/allowance.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { getAllowancePath } from "./config.js";
|
|
@@ -26,10 +26,33 @@ const PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
|
26
26
|
* `src/tools/{status,init}.ts` callers translate the throw into their own
|
|
27
27
|
* structured envelopes (`code: BAD_ALLOWANCE_FILE`).
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* If an allowance file is readable by group or other (any of the low 0o077
|
|
31
|
+
* bits set), tighten it to 0600 and warn once on stderr. This self-heals the
|
|
32
|
+
* historical case where a legacy world-readable `wallet.json` (mode 0644) was
|
|
33
|
+
* migrated to `allowance.json` via a mode-preserving rename, leaving the
|
|
34
|
+
* private key exposed on a shared machine. Best-effort: POSIX-only and silent
|
|
35
|
+
* on platforms without meaningful Unix modes (e.g. Windows).
|
|
36
|
+
*/
|
|
37
|
+
function selfHealPermissions(p) {
|
|
38
|
+
if (process.platform === "win32")
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const mode = statSync(p).mode & 0o777;
|
|
42
|
+
if ((mode & 0o077) !== 0) {
|
|
43
|
+
chmodSync(p, 0o600);
|
|
44
|
+
process.stderr.write(`warning: tightened permissions on ${p} from ${mode.toString(8)} to 600 (was readable by other users).\n`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Best-effort; never block a read on a chmod/stat failure.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
29
51
|
export function readAllowance(path) {
|
|
30
52
|
const p = path ?? getAllowancePath();
|
|
31
53
|
if (!existsSync(p))
|
|
32
54
|
return null;
|
|
55
|
+
selfHealPermissions(p);
|
|
33
56
|
let raw;
|
|
34
57
|
try {
|
|
35
58
|
raw = readFileSync(p, "utf-8");
|
package/core-dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { existsSync, renameSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { existsSync, renameSync, mkdirSync, chmodSync } from "node:fs";
|
|
4
4
|
const DEFAULT_API_BASE = "https://api.run402.com";
|
|
5
5
|
/**
|
|
6
6
|
* Validate a user-supplied API base URL. Throws a clear error message that
|
|
@@ -47,9 +47,71 @@ export function getDeployApiBase() {
|
|
|
47
47
|
const validated = validateApiBase("RUN402_DEPLOY_API_BASE", process.env.RUN402_DEPLOY_API_BASE, fallback);
|
|
48
48
|
return validated ?? fallback;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The base credential directory — the root under which the `default` wallet
|
|
52
|
+
* lives directly and named wallets live under `profiles/<name>/`. This is the
|
|
53
|
+
* value `RUN402_CONFIG_DIR` overrides; profiles nest *within* it.
|
|
54
|
+
*/
|
|
55
|
+
export function getConfigBaseDir() {
|
|
51
56
|
return process.env.RUN402_CONFIG_DIR || join(homedir(), ".config", "run402");
|
|
52
57
|
}
|
|
58
|
+
export const DEFAULT_PROFILE = "default";
|
|
59
|
+
// Filesystem-safe wallet/profile name. Lowercase only (avoids collisions on
|
|
60
|
+
// case-insensitive filesystems like macOS), starts alphanumeric, then
|
|
61
|
+
// alphanumeric/underscore/hyphen, max 64 chars. The CLI edge enforces this for
|
|
62
|
+
// nice UX; core re-checks it as a defense-in-depth guard so a hostile
|
|
63
|
+
// `RUN402_WALLET`/`RUN402_PROFILE` cannot traverse outside the profiles dir.
|
|
64
|
+
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
65
|
+
export function isValidProfileName(name) {
|
|
66
|
+
return PROFILE_NAME_RE.test(name);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Guard against path traversal from the profile env vars. The reserved
|
|
70
|
+
* `default` profile is always allowed (it maps to the base dir). Any other
|
|
71
|
+
* name must pass the filesystem-safe pattern; otherwise we throw rather than
|
|
72
|
+
* silently resolve a surprising path.
|
|
73
|
+
*/
|
|
74
|
+
function assertSafeProfileName(name) {
|
|
75
|
+
if (name === DEFAULT_PROFILE)
|
|
76
|
+
return;
|
|
77
|
+
if (!isValidProfileName(name)) {
|
|
78
|
+
throw new Error(`Invalid wallet/profile name ${JSON.stringify(name)}. ` +
|
|
79
|
+
"Names must match /^[a-z0-9][a-z0-9_-]{0,63}$/ (lowercase letters, digits, '_' and '-'). " +
|
|
80
|
+
"Check the RUN402_WALLET / RUN402_PROFILE env var.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The active wallet/profile name from the environment. `RUN402_WALLET` is
|
|
85
|
+
* canonical; `RUN402_PROFILE` is accepted as an alias. Empty/unset → the
|
|
86
|
+
* reserved `default`. The CLI edge resolves the `--wallet` flag and any
|
|
87
|
+
* per-directory `.run402.json` binding into `RUN402_WALLET` *before* dispatch,
|
|
88
|
+
* so core stays env-only and never reads argv or cwd.
|
|
89
|
+
*/
|
|
90
|
+
export function getActiveProfile() {
|
|
91
|
+
const raw = process.env.RUN402_WALLET ?? process.env.RUN402_PROFILE;
|
|
92
|
+
const name = raw == null ? "" : raw.trim();
|
|
93
|
+
if (!name)
|
|
94
|
+
return DEFAULT_PROFILE;
|
|
95
|
+
assertSafeProfileName(name);
|
|
96
|
+
return name;
|
|
97
|
+
}
|
|
98
|
+
/** Directory holding all named wallets: `{base}/profiles`. */
|
|
99
|
+
export function getProfilesDir() {
|
|
100
|
+
return join(getConfigBaseDir(), "profiles");
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The effective config directory for the *active* wallet. The `default` wallet
|
|
104
|
+
* resolves to the base dir (zero migration for existing single-wallet
|
|
105
|
+
* installs); any named wallet resolves to `{base}/profiles/<name>`. Because
|
|
106
|
+
* keystore, allowance, and meta paths all derive from this, switching the
|
|
107
|
+
* profile env var moves the whole wallet bundle atomically — and the SDK/MCP
|
|
108
|
+
* inherit profile selection for free.
|
|
109
|
+
*/
|
|
110
|
+
export function getConfigDir() {
|
|
111
|
+
const base = getConfigBaseDir();
|
|
112
|
+
const profile = getActiveProfile();
|
|
113
|
+
return profile === DEFAULT_PROFILE ? base : join(base, "profiles", profile);
|
|
114
|
+
}
|
|
53
115
|
export function getKeystorePath() {
|
|
54
116
|
return join(getConfigDir(), "projects.json");
|
|
55
117
|
}
|
|
@@ -59,10 +121,20 @@ export function getAllowancePath() {
|
|
|
59
121
|
const dir = getConfigDir();
|
|
60
122
|
const newPath = join(dir, "allowance.json");
|
|
61
123
|
const oldPath = join(dir, "wallet.json");
|
|
62
|
-
// Auto-migrate from wallet.json → allowance.json
|
|
124
|
+
// Auto-migrate from wallet.json → allowance.json. renameSync preserves the
|
|
125
|
+
// source file's mode, so a legacy world-readable wallet.json (mode 0644)
|
|
126
|
+
// would otherwise carry that mode forward and leave the private key
|
|
127
|
+
// world-readable on a shared machine. Tighten to 0600 after the rename.
|
|
63
128
|
if (!existsSync(newPath) && existsSync(oldPath)) {
|
|
64
129
|
mkdirSync(dir, { recursive: true });
|
|
65
130
|
renameSync(oldPath, newPath);
|
|
131
|
+
try {
|
|
132
|
+
chmodSync(newPath, 0o600);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Best-effort (e.g. Windows / exotic filesystems). Read-time self-heal
|
|
136
|
+
// in readAllowance() is the backstop.
|
|
137
|
+
}
|
|
66
138
|
}
|
|
67
139
|
return newPath;
|
|
68
140
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named-wallet profile storage.
|
|
3
|
+
*
|
|
4
|
+
* A "wallet" is a whole config directory. The reserved `default` wallet lives
|
|
5
|
+
* at the base config dir (zero migration for existing installs); every named
|
|
6
|
+
* wallet lives under `{base}/profiles/<name>/` with its own `allowance.json`,
|
|
7
|
+
* `projects.json`, and this module's non-secret `meta.json`.
|
|
8
|
+
*
|
|
9
|
+
* Two levels of active state, mirroring the wallet → project model:
|
|
10
|
+
* - base `{base}/config.json` `active_wallet` — which wallet is the default
|
|
11
|
+
* - per-wallet `projects.json` `active_project_id` — which project within it
|
|
12
|
+
*
|
|
13
|
+
* All writes are atomic (temp-file + rename) and owner-only (0600 files,
|
|
14
|
+
* 0700 dirs). `meta.json` holds only public/display data (no private key), so
|
|
15
|
+
* listing and name display never need to load key material.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, existsSync, readdirSync, rmSync, } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
20
|
+
import { getConfigBaseDir, getProfilesDir, DEFAULT_PROFILE, isValidProfileName, } from "./config.js";
|
|
21
|
+
function atomicWrite(p, content, mode) {
|
|
22
|
+
const dir = dirname(p);
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
const tmp = join(dir, `.${randomBytes(4).toString("hex")}.tmp`);
|
|
25
|
+
writeFileSync(tmp, content, { mode });
|
|
26
|
+
renameSync(tmp, p);
|
|
27
|
+
try {
|
|
28
|
+
chmodSync(p, mode);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* best-effort on non-POSIX */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// --- base config.json (global default wallet pointer) ---
|
|
35
|
+
function baseConfigPath() {
|
|
36
|
+
return join(getConfigBaseDir(), "config.json");
|
|
37
|
+
}
|
|
38
|
+
export function readBaseConfig() {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(baseConfigPath(), "utf-8"));
|
|
41
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* missing / unreadable / malformed → empty */
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
export function writeBaseConfig(cfg) {
|
|
51
|
+
atomicWrite(baseConfigPath(), JSON.stringify(cfg, null, 2), 0o600);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The globally-selected default wallet (set via `wallets use`), or the
|
|
55
|
+
* reserved `default` when unset/invalid. This is precedence rung 4 — below the
|
|
56
|
+
* flag, env var, and per-directory binding.
|
|
57
|
+
*/
|
|
58
|
+
export function getDefaultWallet() {
|
|
59
|
+
const w = readBaseConfig().active_wallet;
|
|
60
|
+
return w && (w === DEFAULT_PROFILE || isValidProfileName(w)) ? w : DEFAULT_PROFILE;
|
|
61
|
+
}
|
|
62
|
+
export function setDefaultWallet(name) {
|
|
63
|
+
const cfg = readBaseConfig();
|
|
64
|
+
cfg.active_wallet = name;
|
|
65
|
+
writeBaseConfig(cfg);
|
|
66
|
+
}
|
|
67
|
+
// --- per-profile directory + meta.json ---
|
|
68
|
+
/** Absolute directory for a wallet. `default` → base dir; named → profiles/<name>. */
|
|
69
|
+
export function profileDir(name) {
|
|
70
|
+
return name === DEFAULT_PROFILE ? getConfigBaseDir() : join(getProfilesDir(), name);
|
|
71
|
+
}
|
|
72
|
+
/** Create (if needed) a wallet's directory with owner-only (0700) perms. */
|
|
73
|
+
export function ensureProfileDir(name) {
|
|
74
|
+
const dir = profileDir(name);
|
|
75
|
+
if (name === DEFAULT_PROFILE) {
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
return dir;
|
|
78
|
+
}
|
|
79
|
+
const profilesRoot = getProfilesDir();
|
|
80
|
+
mkdirSync(profilesRoot, { recursive: true });
|
|
81
|
+
try {
|
|
82
|
+
chmodSync(profilesRoot, 0o700);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* best-effort */
|
|
86
|
+
}
|
|
87
|
+
mkdirSync(dir, { recursive: true });
|
|
88
|
+
try {
|
|
89
|
+
chmodSync(dir, 0o700);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* best-effort */
|
|
93
|
+
}
|
|
94
|
+
return dir;
|
|
95
|
+
}
|
|
96
|
+
function metaPath(name) {
|
|
97
|
+
return join(profileDir(name), "meta.json");
|
|
98
|
+
}
|
|
99
|
+
export function readMeta(name) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(readFileSync(metaPath(name), "utf-8"));
|
|
102
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* missing / unreadable / malformed → null */
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
export function writeMeta(name, meta) {
|
|
112
|
+
ensureProfileDir(name);
|
|
113
|
+
atomicWrite(metaPath(name), JSON.stringify(meta, null, 2), 0o600);
|
|
114
|
+
}
|
|
115
|
+
// --- enumeration + lifecycle ---
|
|
116
|
+
/** True when a wallet's `allowance.json` exists on disk. */
|
|
117
|
+
export function profileExists(name) {
|
|
118
|
+
return existsSync(join(profileDir(name), "allowance.json"));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* All wallet names on disk: `default` (only if a root allowance.json exists)
|
|
122
|
+
* plus every valid directory under `profiles/`.
|
|
123
|
+
*/
|
|
124
|
+
export function listProfileNames() {
|
|
125
|
+
const names = [];
|
|
126
|
+
if (existsSync(join(getConfigBaseDir(), "allowance.json"))) {
|
|
127
|
+
names.push(DEFAULT_PROFILE);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
for (const entry of readdirSync(getProfilesDir(), { withFileTypes: true })) {
|
|
131
|
+
if (entry.isDirectory() && isValidProfileName(entry.name))
|
|
132
|
+
names.push(entry.name);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* no profiles dir yet */
|
|
137
|
+
}
|
|
138
|
+
return names;
|
|
139
|
+
}
|
|
140
|
+
/** Delete a named wallet's directory. Refuses to remove the reserved default. */
|
|
141
|
+
export function removeProfile(name) {
|
|
142
|
+
if (name === DEFAULT_PROFILE) {
|
|
143
|
+
throw new Error("Refusing to remove the reserved 'default' wallet");
|
|
144
|
+
}
|
|
145
|
+
rmSync(profileDir(name), { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Move a wallet to a new name. Renaming `default` migrates the root-level
|
|
149
|
+
* credential files into `profiles/<newName>/` (so a named wallet is always a
|
|
150
|
+
* directory). Does NOT update the `active_wallet` pointer — the caller owns
|
|
151
|
+
* that orchestration. Throws if the destination already exists or the source
|
|
152
|
+
* is missing.
|
|
153
|
+
*/
|
|
154
|
+
export function renameProfile(oldName, newName) {
|
|
155
|
+
if (!isValidProfileName(newName)) {
|
|
156
|
+
throw new Error(`Invalid wallet name ${JSON.stringify(newName)}.`);
|
|
157
|
+
}
|
|
158
|
+
if (newName === oldName)
|
|
159
|
+
return;
|
|
160
|
+
const dest = join(getProfilesDir(), newName);
|
|
161
|
+
if (existsSync(dest)) {
|
|
162
|
+
throw new Error(`A wallet named '${newName}' already exists.`);
|
|
163
|
+
}
|
|
164
|
+
mkdirSync(getProfilesDir(), { recursive: true });
|
|
165
|
+
try {
|
|
166
|
+
chmodSync(getProfilesDir(), 0o700);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
/* best-effort */
|
|
170
|
+
}
|
|
171
|
+
if (oldName === DEFAULT_PROFILE) {
|
|
172
|
+
if (!profileExists(DEFAULT_PROFILE)) {
|
|
173
|
+
throw new Error("No 'default' wallet to rename.");
|
|
174
|
+
}
|
|
175
|
+
mkdirSync(dest, { recursive: true });
|
|
176
|
+
try {
|
|
177
|
+
chmodSync(dest, 0o700);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* best-effort */
|
|
181
|
+
}
|
|
182
|
+
const base = getConfigBaseDir();
|
|
183
|
+
for (const f of ["allowance.json", "projects.json", "meta.json"]) {
|
|
184
|
+
const src = join(base, f);
|
|
185
|
+
if (existsSync(src))
|
|
186
|
+
renameSync(src, join(dest, f));
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const src = join(getProfilesDir(), oldName);
|
|
191
|
+
if (!existsSync(src)) {
|
|
192
|
+
throw new Error(`Wallet '${oldName}' not found.`);
|
|
193
|
+
}
|
|
194
|
+
renameSync(src, dest);
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=profiles.js.map
|
package/lib/allowance.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readAllowance, saveAllowance,
|
|
1
|
+
import { readAllowance, saveAllowance, allowanceFile } from "./config.mjs";
|
|
2
2
|
import { getSdk } from "./sdk.mjs";
|
|
3
3
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
4
4
|
import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs } from "./argparse.mjs";
|
|
@@ -98,7 +98,7 @@ async function status() {
|
|
|
98
98
|
// now" check, use `run402 allowance balance`.
|
|
99
99
|
faucet_used: !!data.faucet_used,
|
|
100
100
|
rail: w?.rail || "x402",
|
|
101
|
-
path: data.path ??
|
|
101
|
+
path: data.path ?? allowanceFile(),
|
|
102
102
|
},
|
|
103
103
|
}));
|
|
104
104
|
} catch (err) {
|
|
@@ -111,7 +111,7 @@ async function create() {
|
|
|
111
111
|
const result = await getSdk().allowance.create();
|
|
112
112
|
console.log(JSON.stringify({
|
|
113
113
|
address: result.address,
|
|
114
|
-
path: result.path ??
|
|
114
|
+
path: result.path ?? allowanceFile(),
|
|
115
115
|
created: true,
|
|
116
116
|
}));
|
|
117
117
|
} catch (err) {
|
package/lib/config.mjs
CHANGED
|
@@ -9,9 +9,24 @@ import { loadKeyStore, getProject, saveProject, updateProject, removeProject, sa
|
|
|
9
9
|
import { getAllowanceAuthHeaders as coreGetAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
|
|
10
10
|
import { fail } from "./sdk-errors.mjs";
|
|
11
11
|
|
|
12
|
+
// Wallet-dependent paths are exposed as getters (preferred — they always
|
|
13
|
+
// reflect the active profile, even if some future code path imports this module
|
|
14
|
+
// before wallet resolution). Production code (init/doctor/allowance) uses these.
|
|
15
|
+
export function configDir() { return getConfigDir(); }
|
|
16
|
+
export function allowanceFile() { return getAllowancePath(); }
|
|
17
|
+
export function projectsFile() { return getKeystorePath(); }
|
|
18
|
+
|
|
19
|
+
// Snapshot constants, retained for backward compatibility (tests, the OpenClaw
|
|
20
|
+
// config re-export). These are evaluated when this module is first imported.
|
|
21
|
+
// That is safe in every real flow because the CLI resolves the active wallet
|
|
22
|
+
// (publishing RUN402_WALLET) in cli.mjs BEFORE any subcommand imports this
|
|
23
|
+
// module, and tests set RUN402_CONFIG_DIR before importing. New code should
|
|
24
|
+
// prefer the getters above.
|
|
12
25
|
export const CONFIG_DIR = getConfigDir();
|
|
13
26
|
export const ALLOWANCE_FILE = getAllowancePath();
|
|
14
27
|
export const PROJECTS_FILE = getKeystorePath();
|
|
28
|
+
|
|
29
|
+
// API base is independent of the active wallet, so a module-load snapshot is safe.
|
|
15
30
|
export const API = getApiBase();
|
|
16
31
|
|
|
17
32
|
/**
|
|
@@ -32,7 +47,7 @@ export function readAllowance() {
|
|
|
32
47
|
code: "BAD_ALLOWANCE_FILE",
|
|
33
48
|
message: err?.message ?? "allowance.json is malformed",
|
|
34
49
|
hint: "Back up ~/.config/run402/allowance.json and run 'run402 init' to recreate it.",
|
|
35
|
-
details: { path:
|
|
50
|
+
details: { path: allowanceFile() },
|
|
36
51
|
});
|
|
37
52
|
}
|
|
38
53
|
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, statSync } from "node:fs";
|
|
15
|
-
import {
|
|
15
|
+
import { configDir, readAllowance, loadKeyStore } from "./config.mjs";
|
|
16
16
|
import { getSdk } from "./sdk.mjs";
|
|
17
17
|
import {
|
|
18
18
|
resolveScanRoot,
|
|
@@ -39,6 +39,9 @@ Checks performed:
|
|
|
39
39
|
- Keystore has at least one wallet
|
|
40
40
|
- API_BASE is reachable (network check via /health)
|
|
41
41
|
- Active tier resolves and is not 'past_due' / 'frozen'
|
|
42
|
+
- Function runtime staleness: deployed functions running an older platform
|
|
43
|
+
runtime than the current gateway build (refresh with 'run402 functions
|
|
44
|
+
rebuild --all'; re-bundles from your stored source, no source change)
|
|
42
45
|
- Source scan: hallucinated SDK auth names (R402_AUTH_UNKNOWN_EXPORT),
|
|
43
46
|
state-changing GET handlers (R402_AUTH_STATE_CHANGING_GET),
|
|
44
47
|
auth.* calls in prerendered pages (R402_AUTH_PRERENDERED),
|
|
@@ -62,6 +65,7 @@ export async function run(sub, args = []) {
|
|
|
62
65
|
const scanDirOverride = scanDirArgIdx >= 0 ? all[scanDirArgIdx + 1] : null;
|
|
63
66
|
|
|
64
67
|
const checks = [];
|
|
68
|
+
const CONFIG_DIR = configDir();
|
|
65
69
|
|
|
66
70
|
// 1. Config directory.
|
|
67
71
|
try {
|
|
@@ -226,16 +230,57 @@ export async function run(sub, args = []) {
|
|
|
226
230
|
} else {
|
|
227
231
|
checks.push({ name: "operator_health", status: "ok" });
|
|
228
232
|
}
|
|
233
|
+
|
|
234
|
+
// 6b. Function runtime staleness (v1.69, capability
|
|
235
|
+
// function-runtime-rebuild). A deployed function is stale when its Lambda
|
|
236
|
+
// zip carries an older platform entry wrapper / bundled runtime than the
|
|
237
|
+
// gateway's current build — a plain redeploy with unchanged source does
|
|
238
|
+
// NOT refresh it (apply's release diff keys on the source code_hash, not
|
|
239
|
+
// the wrapper). Read-only signal; refreshing is strictly opt-in. Reuses
|
|
240
|
+
// the operator status fetched above to avoid a second round-trip.
|
|
241
|
+
const runtime = status.runtime;
|
|
242
|
+
if (runtime && typeof runtime.stale_function_count === "number") {
|
|
243
|
+
if (runtime.stale_function_count > 0) {
|
|
244
|
+
checks.push({
|
|
245
|
+
name: "runtime_staleness",
|
|
246
|
+
status: "warning",
|
|
247
|
+
value: {
|
|
248
|
+
stale_function_count: runtime.stale_function_count,
|
|
249
|
+
stale_functions: runtime.stale_functions ?? [],
|
|
250
|
+
},
|
|
251
|
+
hint: `${runtime.stale_function_count} function(s) are running an older platform runtime. Run 'run402 functions rebuild --all' to refresh (re-bundles from your stored source; no source change).`,
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
checks.push({
|
|
255
|
+
name: "runtime_staleness",
|
|
256
|
+
status: "ok",
|
|
257
|
+
value: { stale_function_count: 0 },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
// Gateway older than v1.69 doesn't surface the runtime block.
|
|
262
|
+
checks.push({
|
|
263
|
+
name: "runtime_staleness",
|
|
264
|
+
status: "skipped",
|
|
265
|
+
...(verbose && { hint: "operator status has no 'runtime' block; requires v1.69+ gateway." }),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
229
268
|
} catch (err) {
|
|
230
269
|
// Operator status endpoint may not be reachable if the operator-binding
|
|
231
270
|
// substrate isn't deployed yet on the target API. Don't fail the whole
|
|
232
|
-
// doctor over it — emit as a soft warning.
|
|
271
|
+
// doctor over it — emit as a soft warning. The runtime-staleness check
|
|
272
|
+
// rides on the same fetch, so skip it for the same reason.
|
|
233
273
|
checks.push({
|
|
234
274
|
name: "operator_health",
|
|
235
275
|
status: "skipped",
|
|
236
276
|
message: err instanceof Error ? err.message : String(err),
|
|
237
277
|
...(verbose && { hint: "GET /agent/v1/operator/status not reachable; requires v1.55+ gateway." }),
|
|
238
278
|
});
|
|
279
|
+
checks.push({
|
|
280
|
+
name: "runtime_staleness",
|
|
281
|
+
status: "skipped",
|
|
282
|
+
message: err instanceof Error ? err.message : String(err),
|
|
283
|
+
});
|
|
239
284
|
}
|
|
240
285
|
|
|
241
286
|
// 7. Source-tree scan (auth-aware-ssr Section 9). Detects hallucinated
|