run402 1.33.1 → 1.34.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/cli.mjs +6 -0
- package/core-dist/wallet-auth.js +62 -0
- package/core-dist/wallet.js +25 -0
- package/lib/deploy.mjs +30 -2
- package/lib/service.mjs +47 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -42,6 +42,7 @@ Commands:
|
|
|
42
42
|
billing Email billing accounts, Stripe tier checkout, email packs
|
|
43
43
|
contracts KMS contract wallets ($0.04/day rental + $0.000005/sign)
|
|
44
44
|
agent Manage agent identity (contact info)
|
|
45
|
+
service Run402 service health and availability (status, health)
|
|
45
46
|
|
|
46
47
|
Run 'run402 <command> --help' for detailed usage of each command.
|
|
47
48
|
|
|
@@ -183,6 +184,11 @@ switch (cmd) {
|
|
|
183
184
|
await run(sub, rest);
|
|
184
185
|
break;
|
|
185
186
|
}
|
|
187
|
+
case "service": {
|
|
188
|
+
const { run } = await import("./lib/service.mjs");
|
|
189
|
+
await run(sub, rest);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
186
192
|
default:
|
|
187
193
|
console.error(`Unknown command: ${cmd}\n`);
|
|
188
194
|
console.log(HELP);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet auth helper — generates EIP-191 signature headers for Run402 API.
|
|
3
|
+
* Uses @noble/curves (lighter than viem) for signing.
|
|
4
|
+
*/
|
|
5
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
6
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
7
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
8
|
+
import { readWallet } from "./wallet.js";
|
|
9
|
+
/**
|
|
10
|
+
* EIP-191 personal_sign: sign a message with the wallet's private key.
|
|
11
|
+
*/
|
|
12
|
+
function personalSign(privateKeyHex, address, message) {
|
|
13
|
+
const msgBytes = new TextEncoder().encode(message);
|
|
14
|
+
const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
|
|
15
|
+
const prefixed = new Uint8Array(prefix.length + msgBytes.length);
|
|
16
|
+
prefixed.set(prefix);
|
|
17
|
+
prefixed.set(msgBytes, prefix.length);
|
|
18
|
+
const hash = keccak_256(prefixed);
|
|
19
|
+
const pkHex = privateKeyHex.startsWith("0x")
|
|
20
|
+
? privateKeyHex.slice(2)
|
|
21
|
+
: privateKeyHex;
|
|
22
|
+
const pkBytes = Uint8Array.from(Buffer.from(pkHex, "hex"));
|
|
23
|
+
const rawSig = secp256k1.sign(hash, pkBytes);
|
|
24
|
+
const sig = secp256k1.Signature.fromBytes(rawSig);
|
|
25
|
+
// Determine recovery bit by trying both and matching the address
|
|
26
|
+
let recovery = 0;
|
|
27
|
+
for (const v of [0, 1]) {
|
|
28
|
+
try {
|
|
29
|
+
const recovered = sig.addRecoveryBit(v).recoverPublicKey(hash);
|
|
30
|
+
const pubBytes = recovered.toBytes(false).slice(1); // uncompressed, drop 04 prefix
|
|
31
|
+
const addrBytes = keccak_256(pubBytes).slice(-20);
|
|
32
|
+
if ("0x" + bytesToHex(addrBytes) === address.toLowerCase()) {
|
|
33
|
+
recovery = v;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const r = sig.r.toString(16).padStart(64, "0");
|
|
42
|
+
const s = sig.s.toString(16).padStart(64, "0");
|
|
43
|
+
const vHex = (recovery + 27).toString(16).padStart(2, "0");
|
|
44
|
+
return "0x" + r + s + vHex;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get wallet auth headers for the Run402 API.
|
|
48
|
+
* Returns null if no wallet is configured.
|
|
49
|
+
*/
|
|
50
|
+
export function getWalletAuthHeaders(walletPath) {
|
|
51
|
+
const wallet = readWallet(walletPath);
|
|
52
|
+
if (!wallet || !wallet.address || !wallet.privateKey)
|
|
53
|
+
return null;
|
|
54
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
55
|
+
const signature = personalSign(wallet.privateKey, wallet.address, `run402:${timestamp}`);
|
|
56
|
+
return {
|
|
57
|
+
"X-Run402-Wallet": wallet.address,
|
|
58
|
+
"X-Run402-Signature": signature,
|
|
59
|
+
"X-Run402-Timestamp": timestamp,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=wallet-auth.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { getWalletPath } from "./config.js";
|
|
5
|
+
export function readWallet(path) {
|
|
6
|
+
const p = path ?? getWalletPath();
|
|
7
|
+
if (!existsSync(p))
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function saveWallet(data, path) {
|
|
17
|
+
const p = path ?? getWalletPath();
|
|
18
|
+
const dir = dirname(p);
|
|
19
|
+
mkdirSync(dir, { recursive: true });
|
|
20
|
+
const tmp = join(dir, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
|
|
21
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
22
|
+
renameSync(tmp, p);
|
|
23
|
+
chmodSync(p, 0o600);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=wallet.js.map
|
package/lib/deploy.mjs
CHANGED
|
@@ -114,7 +114,35 @@ export async function run(args) {
|
|
|
114
114
|
|
|
115
115
|
const authHeaders = allowanceAuthHeaders("/deploy/v1");
|
|
116
116
|
const res = await fetch(`${API}/deploy/v1`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders }, body: JSON.stringify(manifest) });
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
|
|
118
|
+
// Content-type aware parsing: gateways (ALB, CloudFront, etc.) return HTML on
|
|
119
|
+
// 504/413/etc., which would otherwise crash res.json() with SyntaxError.
|
|
120
|
+
const contentType = res.headers.get("content-type") || "";
|
|
121
|
+
let result = null;
|
|
122
|
+
let parseError = null;
|
|
123
|
+
let bodyText = null;
|
|
124
|
+
if (contentType.includes("application/json")) {
|
|
125
|
+
try {
|
|
126
|
+
result = await res.json();
|
|
127
|
+
} catch (e) {
|
|
128
|
+
parseError = e;
|
|
129
|
+
try { bodyText = await res.text(); } catch { bodyText = ""; }
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
try { bodyText = await res.text(); } catch { bodyText = ""; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!res.ok || parseError || result === null) {
|
|
136
|
+
const err = { status: "error", http: res.status, content_type: contentType || null };
|
|
137
|
+
if (result && typeof result === "object") {
|
|
138
|
+
Object.assign(err, result);
|
|
139
|
+
} else {
|
|
140
|
+
const preview = typeof bodyText === "string" ? bodyText.slice(0, 500) : "";
|
|
141
|
+
err.body_preview = preview;
|
|
142
|
+
if (parseError) err.parse_error = "response body was not valid JSON";
|
|
143
|
+
}
|
|
144
|
+
console.error(JSON.stringify(err));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
119
147
|
console.log(JSON.stringify(result, null, 2));
|
|
120
148
|
}
|
package/lib/service.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { API } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 service — Run402 service health and availability
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
run402 service status Public availability report (uptime, capabilities, operator, deployment)
|
|
7
|
+
run402 service health Liveness check (per-dependency status + version)
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- Both endpoints are unauthenticated and free. No allowance required.
|
|
11
|
+
- This is the Run402 SERVICE status. For your ACCOUNT status (allowance,
|
|
12
|
+
balance, tier, projects), use 'run402 status'.
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
async function fetchAndEmit(path) {
|
|
16
|
+
let res;
|
|
17
|
+
try {
|
|
18
|
+
res = await fetch(`${API}${path}`);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.log(JSON.stringify({ error: "fetch_failed", message: err?.message || String(err) }));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const text = await res.text();
|
|
24
|
+
let body;
|
|
25
|
+
try { body = JSON.parse(text); } catch { body = text; }
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
console.log(JSON.stringify({ error: "non_2xx", status: res.status, body }, null, 2));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(JSON.stringify(body, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function run(sub, args) {
|
|
34
|
+
if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
|
|
35
|
+
switch (sub) {
|
|
36
|
+
case "status":
|
|
37
|
+
await fetchAndEmit("/status");
|
|
38
|
+
return;
|
|
39
|
+
case "health":
|
|
40
|
+
await fetchAndEmit("/health");
|
|
41
|
+
return;
|
|
42
|
+
default:
|
|
43
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
44
|
+
console.log(HELP);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
package/package.json
CHANGED