run402 1.9.1 → 1.9.2

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.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Allowance 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 { readAllowance } from "./allowance.js";
9
+ /**
10
+ * EIP-191 personal_sign: sign a message with the allowance'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 allowance auth headers for the Run402 API.
48
+ * Returns null if no allowance is configured.
49
+ */
50
+ export function getAllowanceAuthHeaders(allowancePath) {
51
+ const allowance = readAllowance(allowancePath);
52
+ if (!allowance || !allowance.address || !allowance.privateKey)
53
+ return null;
54
+ const timestamp = Math.floor(Date.now() / 1000).toString();
55
+ const signature = personalSign(allowance.privateKey, allowance.address, `run402:${timestamp}`);
56
+ return {
57
+ "X-Run402-Wallet": allowance.address,
58
+ "X-Run402-Signature": signature,
59
+ "X-Run402-Timestamp": timestamp,
60
+ };
61
+ }
62
+ //# sourceMappingURL=allowance-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 { getAllowancePath } from "./config.js";
5
+ export function readAllowance(path) {
6
+ const p = path ?? getAllowancePath();
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 saveAllowance(data, path) {
17
+ const p = path ?? getAllowancePath();
18
+ const dir = dirname(p);
19
+ mkdirSync(dir, { recursive: true });
20
+ const tmp = join(dir, `.allowance.${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=allowance.js.map
@@ -0,0 +1,42 @@
1
+ import { getApiBase } from "./config.js";
2
+ export async function apiRequest(path, opts = {}) {
3
+ const { method = "GET", headers = {}, body, rawBody } = opts;
4
+ const url = `${getApiBase()}${path}`;
5
+ const fetchHeaders = { ...headers };
6
+ let fetchBody;
7
+ if (rawBody !== undefined) {
8
+ fetchBody = rawBody;
9
+ }
10
+ else if (body !== undefined) {
11
+ fetchHeaders["Content-Type"] = fetchHeaders["Content-Type"] || "application/json";
12
+ fetchBody = JSON.stringify(body);
13
+ }
14
+ let res;
15
+ try {
16
+ res = await fetch(url, {
17
+ method,
18
+ headers: fetchHeaders,
19
+ body: fetchBody,
20
+ });
21
+ }
22
+ catch (err) {
23
+ return {
24
+ ok: false,
25
+ status: 0,
26
+ body: { error: `Network error: ${err.message}` },
27
+ };
28
+ }
29
+ let resBody;
30
+ const contentType = res.headers.get("content-type") || "";
31
+ if (contentType.includes("application/json")) {
32
+ resBody = await res.json();
33
+ }
34
+ else {
35
+ resBody = await res.text();
36
+ }
37
+ if (res.status === 402) {
38
+ return { ok: false, is402: true, status: 402, body: resBody };
39
+ }
40
+ return { ok: res.ok, status: res.status, body: resBody };
41
+ }
42
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,24 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { existsSync, renameSync, mkdirSync } from "node:fs";
4
+ export function getApiBase() {
5
+ return process.env.RUN402_API_BASE || "https://api.run402.com";
6
+ }
7
+ export function getConfigDir() {
8
+ return process.env.RUN402_CONFIG_DIR || join(homedir(), ".config", "run402");
9
+ }
10
+ export function getKeystorePath() {
11
+ return join(getConfigDir(), "projects.json");
12
+ }
13
+ export function getAllowancePath() {
14
+ const dir = getConfigDir();
15
+ const newPath = join(dir, "allowance.json");
16
+ const oldPath = join(dir, "wallet.json");
17
+ // Auto-migrate from wallet.json → allowance.json
18
+ if (!existsSync(newPath) && existsSync(oldPath)) {
19
+ mkdirSync(dir, { recursive: true });
20
+ renameSync(oldPath, newPath);
21
+ }
22
+ return newPath;
23
+ }
24
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,75 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { getKeystorePath } from "./config.js";
5
+ /**
6
+ * Load the keystore from disk.
7
+ * Auto-migrates legacy formats:
8
+ * - Array format (CLI legacy): [{project_id, ...}] → {projects: {id: {...}}}
9
+ * - Old field name: expires_at → lease_expires_at
10
+ */
11
+ export function loadKeyStore(path) {
12
+ const p = path ?? getKeystorePath();
13
+ try {
14
+ const data = readFileSync(p, "utf-8");
15
+ const parsed = JSON.parse(data);
16
+ // Auto-migrate array format (CLI legacy) to object format
17
+ if (Array.isArray(parsed)) {
18
+ const projects = {};
19
+ for (const item of parsed) {
20
+ if (item.project_id) {
21
+ projects[item.project_id] = {
22
+ anon_key: item.anon_key,
23
+ service_key: item.service_key,
24
+ tier: item.tier,
25
+ lease_expires_at: item.lease_expires_at || item.expires_at || "",
26
+ ...(item.site_url && { site_url: item.site_url }),
27
+ ...(item.deployed_at && { deployed_at: item.deployed_at }),
28
+ };
29
+ }
30
+ }
31
+ return { projects };
32
+ }
33
+ if (parsed && typeof parsed === "object" && parsed.projects) {
34
+ // Auto-normalize expires_at → lease_expires_at
35
+ for (const proj of Object.values(parsed.projects)) {
36
+ const rec = proj;
37
+ if (rec.expires_at && !rec.lease_expires_at) {
38
+ rec.lease_expires_at = rec.expires_at;
39
+ delete rec.expires_at;
40
+ }
41
+ }
42
+ return parsed;
43
+ }
44
+ return { projects: {} };
45
+ }
46
+ catch {
47
+ return { projects: {} };
48
+ }
49
+ }
50
+ export function saveKeyStore(store, path) {
51
+ const p = path ?? getKeystorePath();
52
+ const dir = dirname(p);
53
+ mkdirSync(dir, { recursive: true });
54
+ const tmp = join(dir, `.projects.${randomBytes(4).toString("hex")}.tmp`);
55
+ writeFileSync(tmp, JSON.stringify(store, null, 2), { mode: 0o600 });
56
+ renameSync(tmp, p);
57
+ chmodSync(p, 0o600);
58
+ }
59
+ export function getProject(projectId, path) {
60
+ const store = loadKeyStore(path);
61
+ return store.projects[projectId];
62
+ }
63
+ export function saveProject(projectId, project, path) {
64
+ const p = path ?? getKeystorePath();
65
+ const store = loadKeyStore(p);
66
+ store.projects[projectId] = project;
67
+ saveKeyStore(store, p);
68
+ }
69
+ export function removeProject(projectId, path) {
70
+ const p = path ?? getKeystorePath();
71
+ const store = loadKeyStore(p);
72
+ delete store.projects[projectId];
73
+ saveKeyStore(store, p);
74
+ }
75
+ //# sourceMappingURL=keystore.js.map
@@ -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/config.mjs CHANGED
@@ -3,10 +3,9 @@
3
3
  * Adds CLI-specific behavior: process.exit() on errors.
4
4
  */
5
5
 
6
- import { getApiBase, getConfigDir, getKeystorePath, getAllowancePath } from "../../core/dist/config.js";
7
- import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance } from "../../core/dist/allowance.js";
8
- import { getAllowanceAuthHeaders } from "../../core/dist/allowance-auth.js";
9
- import { loadKeyStore, getProject, saveProject, removeProject, saveKeyStore } from "../../core/dist/keystore.js";
6
+ import { getApiBase, getConfigDir, getKeystorePath, getAllowancePath } from "../core-dist/config.js";
7
+ import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance } from "../core-dist/allowance.js";
8
+ import { loadKeyStore, getProject, saveProject, removeProject, saveKeyStore } from "../core-dist/keystore.js";
10
9
 
11
10
  export const CONFIG_DIR = getConfigDir();
12
11
  export const ALLOWANCE_FILE = getAllowancePath();
@@ -22,9 +21,13 @@ export function saveAllowance(data) {
22
21
  }
23
22
 
24
23
  export async function allowanceAuthHeaders() {
25
- const headers = getAllowanceAuthHeaders();
26
- if (!headers) { console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create" })); process.exit(1); }
27
- return headers;
24
+ const w = readAllowance();
25
+ if (!w) { console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create" })); process.exit(1); }
26
+ const { privateKeyToAccount } = await import("viem/accounts");
27
+ const account = privateKeyToAccount(w.privateKey);
28
+ const timestamp = Math.floor(Date.now() / 1000).toString();
29
+ const signature = await account.signMessage({ message: `run402:${timestamp}` });
30
+ return { "X-Run402-Wallet": account.address, "X-Run402-Signature": signature, "X-Run402-Timestamp": timestamp };
28
31
  }
29
32
 
30
33
  export function findProject(id) {
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "run402": "cli.mjs"
8
8
  },
9
+ "scripts": {
10
+ "prepack": "mkdir -p core-dist && cp ../core/dist/*.js core-dist/"
11
+ },
9
12
  "files": [
10
13
  "cli.mjs",
11
- "lib/"
14
+ "lib/",
15
+ "core-dist/"
12
16
  ],
13
17
  "dependencies": {
14
18
  "@x402/evm": "^2.6.0",