run402 1.9.1 → 1.10.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.
@@ -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/lib/init.mjs CHANGED
@@ -71,6 +71,7 @@ export async function run() {
71
71
  }
72
72
 
73
73
  // 4. Tier status
74
+ const store = loadKeyStore();
74
75
  let tierInfo = null;
75
76
  try {
76
77
  const { privateKeyToAccount } = await import("viem/accounts");
@@ -83,6 +84,13 @@ export async function run() {
83
84
  if (res.ok) tierInfo = await res.json();
84
85
  } catch {}
85
86
 
87
+ // Fall back to keystore if the API call failed or returned no tier
88
+ if (!tierInfo || !tierInfo.tier) {
89
+ const projects = Object.values(store.projects);
90
+ const active = projects.find(p => p.tier && p.lease_expires_at && new Date(p.lease_expires_at) > new Date());
91
+ if (active) tierInfo = { tier: active.tier, status: "active", lease_expires_at: active.lease_expires_at };
92
+ }
93
+
86
94
  if (tierInfo && tierInfo.tier && tierInfo.status === "active") {
87
95
  const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
88
96
  line("Tier", `${tierInfo.tier} (expires ${expiry})`);
@@ -91,7 +99,6 @@ export async function run() {
91
99
  }
92
100
 
93
101
  // 5. Projects
94
- const store = loadKeyStore();
95
102
  line("Projects", `${Object.keys(store.projects).length} active`);
96
103
 
97
104
  // 6. Next step
package/lib/projects.mjs CHANGED
@@ -9,6 +9,7 @@ Subcommands:
9
9
  quote Show pricing tiers
10
10
  provision [--tier <tier>] [--name <n>] Provision a new Postgres project (pays via x402)
11
11
  list List all your projects (IDs, tiers, URLs, expiry)
12
+ info <id> Show project details: REST URL, keys, expiry
12
13
  sql <id> "<query>" Run a SQL query against a project's Postgres DB
13
14
  rest <id> <table> [params] Query a table via the REST API (PostgREST)
14
15
  usage <id> Show compute/storage usage for a project
@@ -21,6 +22,7 @@ Examples:
21
22
  run402 projects provision --tier prototype
22
23
  run402 projects provision --tier hobby --name my-app
23
24
  run402 projects list
25
+ run402 projects info abc123
24
26
  run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
25
27
  run402 projects rest abc123 users "limit=10&select=id,name"
26
28
  run402 projects usage abc123
@@ -89,6 +91,22 @@ async function list() {
89
91
  console.log(JSON.stringify(entries.map(([id, p]) => ({ project_id: id, tier: p.tier, site_url: p.site_url, lease_expires_at: p.lease_expires_at, deployed_at: p.deployed_at })), null, 2));
90
92
  }
91
93
 
94
+ async function info(projectId) {
95
+ const p = findProject(projectId);
96
+ const active = p.lease_expires_at ? new Date(p.lease_expires_at) > new Date() : null;
97
+ console.log(JSON.stringify({
98
+ project_id: projectId,
99
+ tier: p.tier,
100
+ active,
101
+ lease_expires_at: p.lease_expires_at,
102
+ rest_url: `${API}/rest/v1`,
103
+ anon_key: p.anon_key,
104
+ service_key: p.service_key,
105
+ site_url: p.site_url || null,
106
+ deployed_at: p.deployed_at || null,
107
+ }, null, 2));
108
+ }
109
+
92
110
  async function sqlCmd(projectId, query) {
93
111
  const p = findProject(projectId);
94
112
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: query });
@@ -138,6 +156,7 @@ export async function run(sub, args) {
138
156
  case "quote": await quote(); break;
139
157
  case "provision": await provision(args); break;
140
158
  case "list": await list(); break;
159
+ case "info": await info(args[0]); break;
141
160
  case "sql": await sqlCmd(args[0], args[1]); break;
142
161
  case "rest": await rest(args[0], args[1], args[2]); break;
143
162
  case "usage": await usage(args[0]); break;
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
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",