run402 1.17.0 → 1.18.1

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 CHANGED
@@ -44,7 +44,7 @@ Examples:
44
44
  run402 deploy --manifest app.json
45
45
  run402 projects list
46
46
  run402 projects sql <project_id> "SELECT * FROM users LIMIT 5"
47
- run402 functions deploy <project_id> my-fn --code handler.ts
47
+ run402 functions deploy <project_id> my-fn --file handler.ts
48
48
  run402 secrets set <project_id> API_KEY sk-1234
49
49
  run402 image generate "a startup mascot, pixel art" --output logo.png
50
50
 
package/lib/functions.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { readFileSync, existsSync } from "fs";
2
- import { findProject, readAllowance, API, ALLOWANCE_FILE } from "./config.mjs";
1
+ import { readFileSync } from "fs";
2
+ import { findProject, API } from "./config.mjs";
3
+ import { setupPaidFetch } from "./paid-fetch.mjs";
3
4
 
4
5
  const HELP = `run402 functions — Manage serverless functions
5
6
 
@@ -7,7 +8,7 @@ Usage:
7
8
  run402 functions <subcommand> [args...]
8
9
 
9
10
  Subcommands:
10
- deploy <id> <name> --code <file> [--timeout <s>] [--memory <mb>] [--deps <pkg,...>]
11
+ deploy <id> <name> --file <file> [--timeout <s>] [--memory <mb>] [--deps <pkg,...>]
11
12
  Deploy a function to a project
12
13
  invoke <id> <name> [--method <M>] [--body <json>]
13
14
  Invoke a deployed function
@@ -16,7 +17,7 @@ Subcommands:
16
17
  delete <id> <name> Delete a function
17
18
 
18
19
  Examples:
19
- run402 functions deploy abc123 stripe-webhook --code handler.ts
20
+ run402 functions deploy abc123 stripe-webhook --file handler.ts
20
21
  run402 functions invoke abc123 stripe-webhook --body '{"event":"test"}'
21
22
  run402 functions logs abc123 stripe-webhook --tail 100
22
23
  run402 functions list abc123
@@ -27,37 +28,17 @@ Notes:
27
28
  - Deploy may require payment if the project lease has expired
28
29
  `;
29
30
 
30
- async function setupPaidFetch() {
31
- if (!existsSync(ALLOWANCE_FILE)) {
32
- console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create && run402 allowance fund" }));
33
- process.exit(1);
34
- }
35
- const allowance = readAllowance();
36
- const { privateKeyToAccount } = await import("viem/accounts");
37
- const { createPublicClient, http } = await import("viem");
38
- const { baseSepolia } = await import("viem/chains");
39
- const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
40
- const { ExactEvmScheme } = await import("@x402/evm/exact/client");
41
- const { toClientEvmSigner } = await import("@x402/evm");
42
- const account = privateKeyToAccount(allowance.privateKey);
43
- const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
44
- const signer = toClientEvmSigner(account, publicClient);
45
- const client = new x402Client();
46
- client.register("eip155:84532", new ExactEvmScheme(signer));
47
- return wrapFetchWithPayment(fetch, client);
48
- }
49
-
50
31
  async function deploy(projectId, name, args) {
51
32
  const p = findProject(projectId);
52
- const opts = { code: null, timeout: undefined, memory: undefined, deps: undefined };
33
+ const opts = { file: null, timeout: undefined, memory: undefined, deps: undefined };
53
34
  for (let i = 0; i < args.length; i++) {
54
- if (args[i] === "--code" && args[i + 1]) opts.code = args[++i];
35
+ if (args[i] === "--file" && args[i + 1]) opts.file = args[++i];
55
36
  if (args[i] === "--timeout" && args[i + 1]) opts.timeout = parseInt(args[++i]);
56
37
  if (args[i] === "--memory" && args[i + 1]) opts.memory = parseInt(args[++i]);
57
38
  if (args[i] === "--deps" && args[i + 1]) opts.deps = args[++i].split(",");
58
39
  }
59
- if (!opts.code) { console.error(JSON.stringify({ status: "error", message: "Missing --code <file>" })); process.exit(1); }
60
- const code = readFileSync(opts.code, "utf-8");
40
+ if (!opts.file) { console.error(JSON.stringify({ status: "error", message: "Missing --file <file>" })); process.exit(1); }
41
+ const code = readFileSync(opts.file, "utf-8");
61
42
  const body = { name, code };
62
43
  if (opts.timeout || opts.memory) body.config = {};
63
44
  if (opts.timeout) body.config.timeout = opts.timeout;
@@ -3,11 +3,33 @@
3
3
  * Branches on allowance rail:
4
4
  * - "mpp": uses mppx.fetch (Tempo pathUSD)
5
5
  * - "x402" (default): uses @x402/fetch (Base USDC)
6
+ *
7
+ * Checks on-chain balances at setup time and selects funded networks.
6
8
  */
7
9
 
8
10
  import { readAllowance, ALLOWANCE_FILE } from "./config.mjs";
9
11
  import { existsSync } from "fs";
10
12
 
13
+ const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
14
+ const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
15
+ const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
16
+ const PATH_USD = "0x20c0000000000000000000000000000000000000";
17
+ const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
18
+
19
+ async function checkBalance(publicClient, tokenAddress, walletAddress) {
20
+ try {
21
+ const raw = await publicClient.readContract({
22
+ address: tokenAddress,
23
+ abi: USDC_ABI,
24
+ functionName: "balanceOf",
25
+ args: [walletAddress],
26
+ });
27
+ return Number(raw);
28
+ } catch {
29
+ return 0;
30
+ }
31
+ }
32
+
11
33
  export async function setupPaidFetch() {
12
34
  if (!existsSync(ALLOWANCE_FILE)) {
13
35
  console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create && run402 allowance fund" }));
@@ -18,6 +40,23 @@ export async function setupPaidFetch() {
18
40
  const account = privateKeyToAccount(allowance.privateKey);
19
41
 
20
42
  if (allowance.rail === "mpp") {
43
+ const { createPublicClient, http, defineChain } = await import("viem");
44
+ const tempoModerato = defineChain({
45
+ id: 42431,
46
+ name: "Tempo Moderato",
47
+ nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
48
+ rpcUrls: { default: { http: [TEMPO_RPC] } },
49
+ });
50
+ const tempoClient = createPublicClient({ chain: tempoModerato, transport: http() });
51
+ const balance = await checkBalance(tempoClient, PATH_USD, allowance.address);
52
+ if (balance === 0) {
53
+ console.error(JSON.stringify({
54
+ status: "error",
55
+ message: `No pathUSD balance on Tempo Moderato (0). Fund your wallet: run402 allowance fund`,
56
+ }));
57
+ process.exit(1);
58
+ }
59
+
21
60
  const { Mppx, tempo } = await import("mppx/client");
22
61
  const mppx = Mppx.create({
23
62
  polyfill: false,
@@ -26,7 +65,7 @@ export async function setupPaidFetch() {
26
65
  return mppx.fetch;
27
66
  }
28
67
 
29
- // Default: x402 (existing behavior)
68
+ // Default: x402
30
69
  const { createPublicClient, http } = await import("viem");
31
70
  const { base, baseSepolia } = await import("viem/chains");
32
71
  const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
@@ -36,8 +75,33 @@ export async function setupPaidFetch() {
36
75
  const mainnetClient = createPublicClient({ chain: base, transport: http() });
37
76
  const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
38
77
 
78
+ // Check balances in parallel
79
+ const [mainnetBalance, sepoliaBalance] = await Promise.all([
80
+ checkBalance(mainnetClient, USDC_MAINNET, allowance.address),
81
+ checkBalance(sepoliaClient, USDC_SEPOLIA, allowance.address),
82
+ ]);
83
+
84
+ if (mainnetBalance === 0 && sepoliaBalance === 0) {
85
+ console.error(JSON.stringify({
86
+ status: "error",
87
+ message: `No USDC balance on any supported network (Base: $${(mainnetBalance / 1e6).toFixed(2)}, Base Sepolia: $${(sepoliaBalance / 1e6).toFixed(2)}). Fund your wallet or run: run402 allowance fund`,
88
+ }));
89
+ process.exit(1);
90
+ }
91
+
39
92
  const client = new x402Client();
40
93
  client.register("eip155:8453", new ExactEvmScheme(toClientEvmSigner(account, mainnetClient)));
41
94
  client.register("eip155:84532", new ExactEvmScheme(toClientEvmSigner(account, sepoliaClient)));
95
+
96
+ // Policy: only allow networks where the wallet has funds
97
+ client.registerPolicy((_version, reqs) => {
98
+ const funded = reqs.filter((r) => {
99
+ if (r.network === "eip155:8453") return mainnetBalance > 0;
100
+ if (r.network === "eip155:84532") return sepoliaBalance > 0;
101
+ return false;
102
+ });
103
+ return funded.length > 0 ? funded : reqs;
104
+ });
105
+
42
106
  return wrapFetchWithPayment(fetch, client);
43
107
  }
package/lib/projects.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "fs";
1
2
  import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders, setActiveProjectId, getActiveProjectId } from "./config.mjs";
2
3
 
3
4
  const HELP = `run402 projects — Manage your deployed Run402 projects
@@ -12,7 +13,7 @@ Subcommands:
12
13
  list List all your projects (IDs, URLs, active marker)
13
14
  info <id> Show project details: REST URL, keys
14
15
  keys <id> Print anon_key and service_key as JSON
15
- sql <id> "<query>" Run a SQL query against a project's Postgres DB
16
+ sql <id> "<query>" [--file <path>] Run a SQL query against a project's Postgres DB
16
17
  rest <id> <table> [params] Query a table via the REST API (PostgREST)
17
18
  usage <id> Show compute/storage usage for a project
18
19
  schema <id> Inspect the database schema
@@ -28,6 +29,7 @@ Examples:
28
29
  run402 projects list
29
30
  run402 projects info abc123
30
31
  run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
32
+ run402 projects sql abc123 --file setup.sql
31
33
  run402 projects rest abc123 users "limit=10&select=id,name"
32
34
  run402 projects usage abc123
33
35
  run402 projects schema abc123
@@ -115,9 +117,17 @@ async function keys(projectId) {
115
117
  console.log(JSON.stringify({ project_id: projectId, anon_key: p.anon_key, service_key: p.service_key }, null, 2));
116
118
  }
117
119
 
118
- async function sqlCmd(projectId, query) {
120
+ async function sqlCmd(projectId, args = []) {
119
121
  const p = findProject(projectId);
120
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: query });
122
+ let file = null;
123
+ let query = null;
124
+ for (let i = 0; i < args.length; i++) {
125
+ if (args[i] === "--file" && args[i + 1]) { file = args[++i]; }
126
+ else if (!query && !args[i].startsWith("--")) { query = args[i]; }
127
+ }
128
+ const sql = file ? readFileSync(file, "utf-8") : query;
129
+ if (!sql) { console.error(JSON.stringify({ status: "error", message: "Missing SQL query. Provide inline or use --file <path>" })); process.exit(1); }
130
+ const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: sql });
121
131
  console.log(JSON.stringify(await res.json(), null, 2));
122
132
  }
123
133
 
@@ -193,7 +203,7 @@ export async function run(sub, args) {
193
203
  case "list": await list(); break;
194
204
  case "info": await info(args[0]); break;
195
205
  case "keys": await keys(args[0]); break;
196
- case "sql": await sqlCmd(args[0], args[1]); break;
206
+ case "sql": await sqlCmd(args[0], args.slice(1)); break;
197
207
  case "rest": await rest(args[0], args[1], args[2]); break;
198
208
  case "usage": await usage(args[0]); break;
199
209
  case "schema": await schema(args[0]); break;
package/lib/secrets.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "fs";
1
2
  import { findProject, API } from "./config.mjs";
2
3
 
3
4
  const HELP = `run402 secrets — Manage project secrets
@@ -6,12 +7,13 @@ Usage:
6
7
  run402 secrets <subcommand> [args...]
7
8
 
8
9
  Subcommands:
9
- set <id> <key> <value> Set a secret on a project
10
+ set <id> <key> <value> [--file <path>] Set a secret on a project
10
11
  list <id> List all secrets for a project
11
12
  delete <id> <key> Delete a secret from a project
12
13
 
13
14
  Examples:
14
15
  run402 secrets set abc123 STRIPE_KEY sk-1234
16
+ run402 secrets set abc123 TLS_CERT --file cert.pem
15
17
  run402 secrets list abc123
16
18
  run402 secrets delete abc123 STRIPE_KEY
17
19
 
@@ -20,12 +22,20 @@ Notes:
20
22
  - Values are never shown after being set
21
23
  `;
22
24
 
23
- async function set(projectId, key, value) {
25
+ async function set(projectId, key, args = []) {
24
26
  const p = findProject(projectId);
27
+ let file = null;
28
+ let value = null;
29
+ for (let i = 0; i < args.length; i++) {
30
+ if (args[i] === "--file" && args[i + 1]) { file = args[++i]; }
31
+ else if (!value && !args[i].startsWith("--")) { value = args[i]; }
32
+ }
33
+ const val = file ? readFileSync(file, "utf-8") : value;
34
+ if (!val) { console.error(JSON.stringify({ status: "error", message: "Missing secret value. Provide inline or use --file <path>" })); process.exit(1); }
25
35
  const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets`, {
26
36
  method: "POST",
27
37
  headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
28
- body: JSON.stringify({ key, value }),
38
+ body: JSON.stringify({ key, value: val }),
29
39
  });
30
40
  const data = await res.json();
31
41
  if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
@@ -59,7 +69,7 @@ async function deleteSecret(projectId, key) {
59
69
  export async function run(sub, args) {
60
70
  if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
61
71
  switch (sub) {
62
- case "set": await set(args[0], args[1], args[2]); break;
72
+ case "set": await set(args[0], args[1], args.slice(2)); break;
63
73
  case "list": await list(args[0]); break;
64
74
  case "delete": await deleteSecret(args[0], args[1]); break;
65
75
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.17.0",
3
+ "version": "1.18.1",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {