run402 1.11.0 → 1.12.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/core-dist/allowance-auth.js +74 -8
- package/lib/agent.mjs +1 -1
- package/lib/apps.mjs +1 -1
- package/lib/config.mjs +5 -8
- package/lib/deploy.mjs +21 -2
- package/lib/init.mjs +8 -8
- package/lib/message.mjs +1 -1
- package/lib/projects.mjs +1 -1
- package/lib/sites.mjs +1 -1
- package/lib/tier.mjs +3 -8
- package/package.json +4 -2
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Allowance auth helper — generates EIP-
|
|
2
|
+
* Allowance auth helper — generates SIWX (Sign-In With X / EIP-4361) headers for Run402 API.
|
|
3
3
|
* Uses @noble/curves (lighter than viem) for signing.
|
|
4
4
|
*/
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
6
7
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
7
8
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
8
9
|
import { readAllowance } from "./allowance.js";
|
|
10
|
+
import { getApiBase } from "./config.js";
|
|
11
|
+
/**
|
|
12
|
+
* EIP-55 mixed-case checksum encoding.
|
|
13
|
+
*/
|
|
14
|
+
export function toChecksumAddress(address) {
|
|
15
|
+
const lower = address.toLowerCase().replace("0x", "");
|
|
16
|
+
const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));
|
|
17
|
+
let checksummed = "0x";
|
|
18
|
+
for (let i = 0; i < lower.length; i++) {
|
|
19
|
+
checksummed += parseInt(hash[i], 16) >= 8 ? lower[i].toUpperCase() : lower[i];
|
|
20
|
+
}
|
|
21
|
+
return checksummed;
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* EIP-191 personal_sign: sign a message with the allowance's private key.
|
|
11
25
|
*/
|
|
@@ -44,19 +58,71 @@ function personalSign(privateKeyHex, address, message) {
|
|
|
44
58
|
return "0x" + r + s + vHex;
|
|
45
59
|
}
|
|
46
60
|
/**
|
|
47
|
-
*
|
|
61
|
+
* Format an EIP-4361 (SIWE) message. Must be byte-for-byte compatible
|
|
62
|
+
* with the `siwe` library's message format used server-side for verification.
|
|
63
|
+
*/
|
|
64
|
+
export function formatSIWEMessage(opts, address) {
|
|
65
|
+
const checksummed = toChecksumAddress(address);
|
|
66
|
+
const lines = [
|
|
67
|
+
`${opts.domain} wants you to sign in with your Ethereum account:`,
|
|
68
|
+
checksummed,
|
|
69
|
+
"",
|
|
70
|
+
opts.statement,
|
|
71
|
+
"",
|
|
72
|
+
`URI: ${opts.uri}`,
|
|
73
|
+
`Version: ${opts.version}`,
|
|
74
|
+
`Chain ID: ${opts.chainId}`,
|
|
75
|
+
`Nonce: ${opts.nonce}`,
|
|
76
|
+
`Issued At: ${opts.issuedAt}`,
|
|
77
|
+
];
|
|
78
|
+
if (opts.expirationTime) {
|
|
79
|
+
lines.push(`Expiration Time: ${opts.expirationTime}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get SIWX auth headers for the Run402 API.
|
|
48
85
|
* Returns null if no allowance is configured.
|
|
86
|
+
*
|
|
87
|
+
* @param path - API path (e.g. "/projects/v1") used to build the SIWE uri field.
|
|
49
88
|
*/
|
|
50
|
-
export function getAllowanceAuthHeaders(allowancePath) {
|
|
89
|
+
export function getAllowanceAuthHeaders(path, allowancePath) {
|
|
51
90
|
const allowance = readAllowance(allowancePath);
|
|
52
91
|
if (!allowance || !allowance.address || !allowance.privateKey)
|
|
53
92
|
return null;
|
|
54
|
-
const
|
|
55
|
-
const
|
|
93
|
+
const apiBase = getApiBase();
|
|
94
|
+
const url = new URL(apiBase);
|
|
95
|
+
const domain = url.hostname;
|
|
96
|
+
const uri = `${apiBase}${path}`;
|
|
97
|
+
const nonce = randomBytes(16).toString("hex");
|
|
98
|
+
const now = new Date();
|
|
99
|
+
const issuedAt = now.toISOString();
|
|
100
|
+
const expirationTime = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
|
|
101
|
+
const message = formatSIWEMessage({
|
|
102
|
+
domain,
|
|
103
|
+
uri,
|
|
104
|
+
statement: "Sign in to Run402",
|
|
105
|
+
version: "1",
|
|
106
|
+
chainId: 84532, // Base Sepolia
|
|
107
|
+
nonce,
|
|
108
|
+
issuedAt,
|
|
109
|
+
expirationTime,
|
|
110
|
+
}, allowance.address);
|
|
111
|
+
const signature = personalSign(allowance.privateKey, allowance.address, message);
|
|
112
|
+
const payload = {
|
|
113
|
+
domain,
|
|
114
|
+
address: toChecksumAddress(allowance.address),
|
|
115
|
+
uri,
|
|
116
|
+
version: "1",
|
|
117
|
+
chainId: 84532,
|
|
118
|
+
type: "eip4361",
|
|
119
|
+
nonce,
|
|
120
|
+
issuedAt,
|
|
121
|
+
expirationTime,
|
|
122
|
+
signature,
|
|
123
|
+
};
|
|
56
124
|
return {
|
|
57
|
-
"
|
|
58
|
-
"X-Run402-Signature": signature,
|
|
59
|
-
"X-Run402-Timestamp": timestamp,
|
|
125
|
+
"SIGN-IN-WITH-X": Buffer.from(JSON.stringify(payload)).toString("base64"),
|
|
60
126
|
};
|
|
61
127
|
}
|
|
62
128
|
//# sourceMappingURL=allowance-auth.js.map
|
package/lib/agent.mjs
CHANGED
|
@@ -23,7 +23,7 @@ async function contact(args) {
|
|
|
23
23
|
if (args[i] === "--webhook" && args[i + 1]) webhook = args[++i];
|
|
24
24
|
}
|
|
25
25
|
if (!name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
|
|
26
|
-
const authHeaders =
|
|
26
|
+
const authHeaders = allowanceAuthHeaders("/agent/v1/contact");
|
|
27
27
|
|
|
28
28
|
const body = { name };
|
|
29
29
|
if (email) body.email = email;
|
package/lib/apps.mjs
CHANGED
|
@@ -47,7 +47,7 @@ async function fork(versionId, name, args) {
|
|
|
47
47
|
if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
|
|
48
48
|
if (args[i] === "--subdomain" && args[i + 1]) opts.subdomain = args[++i];
|
|
49
49
|
}
|
|
50
|
-
const authHeaders =
|
|
50
|
+
const authHeaders = allowanceAuthHeaders("/fork/v1");
|
|
51
51
|
|
|
52
52
|
const body = { version_id: versionId, name };
|
|
53
53
|
if (opts.subdomain) body.subdomain = opts.subdomain;
|
package/lib/config.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getApiBase, getConfigDir, getKeystorePath, getAllowancePath } from "../core-dist/config.js";
|
|
7
7
|
import { readAllowance as coreReadAllowance, saveAllowance as coreSaveAllowance } from "../core-dist/allowance.js";
|
|
8
8
|
import { loadKeyStore, getProject, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId } from "../core-dist/keystore.js";
|
|
9
|
+
import { getAllowanceAuthHeaders as coreGetAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
|
|
9
10
|
|
|
10
11
|
export const CONFIG_DIR = getConfigDir();
|
|
11
12
|
export const ALLOWANCE_FILE = getAllowancePath();
|
|
@@ -20,14 +21,10 @@ export function saveAllowance(data) {
|
|
|
20
21
|
coreSaveAllowance(data);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
26
|
-
|
|
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 };
|
|
24
|
+
export function allowanceAuthHeaders(path) {
|
|
25
|
+
const headers = coreGetAllowanceAuthHeaders(path);
|
|
26
|
+
if (!headers) { console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create" })); process.exit(1); }
|
|
27
|
+
return headers;
|
|
31
28
|
}
|
|
32
29
|
|
|
33
30
|
export function findProject(id) {
|
package/lib/deploy.mjs
CHANGED
|
@@ -14,11 +14,30 @@ Options:
|
|
|
14
14
|
Manifest format (JSON):
|
|
15
15
|
{
|
|
16
16
|
"name": "my-app",
|
|
17
|
-
"migrations": "CREATE TABLE items
|
|
17
|
+
"migrations": "CREATE TABLE items (id serial PRIMARY KEY, title text NOT NULL, done boolean DEFAULT false)",
|
|
18
|
+
"rls": {
|
|
19
|
+
"template": "public_read_write",
|
|
20
|
+
"tables": [{ "table": "items" }]
|
|
21
|
+
},
|
|
22
|
+
"secrets": [{ "key": "OPENAI_API_KEY", "value": "sk-..." }],
|
|
23
|
+
"functions": [{
|
|
24
|
+
"name": "my-fn",
|
|
25
|
+
"code": "export default async (req) => new Response('ok')"
|
|
26
|
+
}],
|
|
18
27
|
"files": [{ "file": "index.html", "data": "<html>...</html>" }],
|
|
19
28
|
"subdomain": "my-app"
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
All fields except "name" are optional.
|
|
32
|
+
|
|
33
|
+
RLS templates:
|
|
34
|
+
user_owns_rows — users see only their rows (requires owner_column per table)
|
|
35
|
+
public_read — anyone reads, authenticated users write
|
|
36
|
+
public_read_write — anyone reads and writes
|
|
37
|
+
|
|
38
|
+
⚠️ Without RLS, tables are read-only via anon_key. If your app writes
|
|
39
|
+
data from the browser, you almost certainly need an rls block.
|
|
40
|
+
|
|
22
41
|
Examples:
|
|
23
42
|
run402 deploy --manifest app.json
|
|
24
43
|
cat app.json | run402 deploy
|
|
@@ -48,7 +67,7 @@ export async function run(args) {
|
|
|
48
67
|
|
|
49
68
|
const manifest = opts.manifest ? JSON.parse(readFileSync(opts.manifest, "utf-8")) : JSON.parse(await readStdin());
|
|
50
69
|
|
|
51
|
-
const authHeaders =
|
|
70
|
+
const authHeaders = allowanceAuthHeaders("/deploy/v1");
|
|
52
71
|
const res = await fetch(`${API}/deploy/v1`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders }, body: JSON.stringify(manifest) });
|
|
53
72
|
const result = await res.json();
|
|
54
73
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...result })); process.exit(1); }
|
package/lib/init.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readAllowance, saveAllowance, loadKeyStore, CONFIG_DIR, ALLOWANCE_FILE, API } from "./config.mjs";
|
|
2
|
+
import { getAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
|
|
2
3
|
import { mkdirSync } from "fs";
|
|
3
4
|
|
|
4
5
|
const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
|
|
@@ -74,14 +75,13 @@ export async function run() {
|
|
|
74
75
|
const store = loadKeyStore();
|
|
75
76
|
let tierInfo = null;
|
|
76
77
|
try {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
if (res.ok) tierInfo = await res.json();
|
|
78
|
+
const authHeaders = getAllowanceAuthHeaders("/tiers/v1/status");
|
|
79
|
+
if (authHeaders) {
|
|
80
|
+
const res = await fetch(`${API}/tiers/v1/status`, {
|
|
81
|
+
headers: { ...authHeaders },
|
|
82
|
+
});
|
|
83
|
+
if (res.ok) tierInfo = await res.json();
|
|
84
|
+
}
|
|
85
85
|
} catch {}
|
|
86
86
|
|
|
87
87
|
if (tierInfo && tierInfo.tier && tierInfo.status === "active") {
|
package/lib/message.mjs
CHANGED
|
@@ -15,7 +15,7 @@ Examples:
|
|
|
15
15
|
|
|
16
16
|
async function send(text) {
|
|
17
17
|
if (!text) { console.error(JSON.stringify({ status: "error", message: "Missing message text" })); process.exit(1); }
|
|
18
|
-
const authHeaders =
|
|
18
|
+
const authHeaders = allowanceAuthHeaders("/message/v1");
|
|
19
19
|
|
|
20
20
|
const res = await fetch(`${API}/message/v1`, {
|
|
21
21
|
method: "POST",
|
package/lib/projects.mjs
CHANGED
|
@@ -53,7 +53,7 @@ async function provision(args) {
|
|
|
53
53
|
if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
|
|
54
54
|
if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
|
|
55
55
|
}
|
|
56
|
-
const authHeaders =
|
|
56
|
+
const authHeaders = allowanceAuthHeaders("/projects/v1");
|
|
57
57
|
const body = { tier: opts.tier };
|
|
58
58
|
if (opts.name) body.name = opts.name;
|
|
59
59
|
const res = await fetch(`${API}/projects/v1`, {
|
package/lib/sites.mjs
CHANGED
|
@@ -55,7 +55,7 @@ async function deploy(args) {
|
|
|
55
55
|
const body = { files: manifest.files, project: projectId };
|
|
56
56
|
if (opts.target) body.target = opts.target;
|
|
57
57
|
|
|
58
|
-
const authHeaders =
|
|
58
|
+
const authHeaders = allowanceAuthHeaders("/deployments/v1");
|
|
59
59
|
const res = await fetch(`${API}/deployments/v1`, {
|
|
60
60
|
method: "POST",
|
|
61
61
|
headers: { "Content-Type": "application/json", ...authHeaders },
|
package/lib/tier.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readAllowance, ALLOWANCE_FILE, API } from "./config.mjs";
|
|
1
|
+
import { readAllowance, ALLOWANCE_FILE, API, allowanceAuthHeaders } from "./config.mjs";
|
|
2
2
|
import { setupPaidFetch } from "./paid-fetch.mjs";
|
|
3
3
|
|
|
4
4
|
const HELP = `run402 tier — Manage your Run402 tier subscription
|
|
@@ -25,14 +25,9 @@ Examples:
|
|
|
25
25
|
`;
|
|
26
26
|
|
|
27
27
|
async function status() {
|
|
28
|
-
const
|
|
29
|
-
if (!w) { console.log(JSON.stringify({ status: "error", message: "No agent allowance. Run: run402 allowance create" })); process.exit(1); }
|
|
30
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
31
|
-
const account = privateKeyToAccount(w.privateKey);
|
|
32
|
-
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
33
|
-
const signature = await account.signMessage({ message: `run402:${timestamp}` });
|
|
28
|
+
const authHeaders = allowanceAuthHeaders("/tiers/v1/status");
|
|
34
29
|
const res = await fetch(`${API}/tiers/v1/status`, {
|
|
35
|
-
headers: {
|
|
30
|
+
headers: { ...authHeaders },
|
|
36
31
|
});
|
|
37
32
|
const data = await res.json();
|
|
38
33
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
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": {
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
"core-dist/"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
+
"@noble/curves": "^2.0.1",
|
|
19
|
+
"@noble/hashes": "^2.0.1",
|
|
18
20
|
"@x402/evm": "^2.6.0",
|
|
19
21
|
"@x402/fetch": "^2.6.0",
|
|
20
22
|
"viem": "^2.47.1"
|
|
21
23
|
},
|
|
22
24
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
25
|
+
"node": ">=22"
|
|
24
26
|
},
|
|
25
27
|
"license": "MIT",
|
|
26
28
|
"homepage": "https://run402.com",
|