run402 1.33.0 → 1.33.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
+ * 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/billing.mjs CHANGED
@@ -9,7 +9,7 @@ Subcommands:
9
9
  create-email <email> Create an email billing account
10
10
  link-wallet <account_id> <wallet> Link a wallet to an email account
11
11
  tier-checkout <tier> [--email <e> | --wallet <w>] Stripe tier checkout
12
- buy-pack [--email <e> | --wallet <w>] Buy \$5 email pack (10,000 emails)
12
+ buy-email-pack [--email <e> | --wallet <w>] Buy \$5 email pack (10,000 emails)
13
13
  auto-recharge <account_id> <on|off> [--threshold <n>]
14
14
  balance <identifier> Balance by email or wallet (0x...)
15
15
  history <identifier> [--limit <n>] Ledger history by email or wallet
@@ -17,7 +17,7 @@ Subcommands:
17
17
  Examples:
18
18
  run402 billing create-email user@example.com
19
19
  run402 billing tier-checkout hobby --email user@example.com
20
- run402 billing buy-pack --wallet 0x1234...
20
+ run402 billing buy-email-pack --wallet 0x1234...
21
21
  run402 billing auto-recharge acct_abc on --threshold 2000
22
22
  run402 billing balance user@example.com
23
23
  `;
@@ -157,7 +157,7 @@ export async function run(sub, args) {
157
157
  case "create-email": await createEmail(args); break;
158
158
  case "link-wallet": await linkWallet(args); break;
159
159
  case "tier-checkout": await tierCheckout(args); break;
160
- case "buy-pack": await buyPack(args); break;
160
+ case "buy-email-pack": await buyPack(args); break;
161
161
  case "auto-recharge": await autoRecharge(args); break;
162
162
  case "balance": await balance(args); break;
163
163
  case "history": await history(args); break;
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
- const result = await res.json();
118
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...result })); process.exit(1); }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.33.0",
3
+ "version": "1.33.2",
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": {