run402 1.10.2 → 1.12.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.
- package/core-dist/allowance-auth.js +74 -8
- package/core-dist/keystore.js +30 -8
- package/lib/agent.mjs +1 -1
- package/lib/apps.mjs +1 -1
- package/lib/config.mjs +19 -10
- package/lib/deploy.mjs +23 -4
- package/lib/init.mjs +8 -15
- package/lib/message.mjs +1 -1
- package/lib/projects.mjs +19 -11
- package/lib/sites.mjs +14 -14
- package/lib/subdomains.mjs +40 -22
- package/lib/tier.mjs +3 -8
- package/package.json +1 -1
|
@@ -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/core-dist/keystore.js
CHANGED
|
@@ -21,8 +21,6 @@ export function loadKeyStore(path) {
|
|
|
21
21
|
projects[item.project_id] = {
|
|
22
22
|
anon_key: item.anon_key,
|
|
23
23
|
service_key: item.service_key,
|
|
24
|
-
tier: item.tier,
|
|
25
|
-
lease_expires_at: item.lease_expires_at || item.expires_at || "",
|
|
26
24
|
...(item.site_url && { site_url: item.site_url }),
|
|
27
25
|
...(item.deployed_at && { deployed_at: item.deployed_at }),
|
|
28
26
|
};
|
|
@@ -31,15 +29,17 @@ export function loadKeyStore(path) {
|
|
|
31
29
|
return { projects };
|
|
32
30
|
}
|
|
33
31
|
if (parsed && typeof parsed === "object" && parsed.projects) {
|
|
34
|
-
//
|
|
32
|
+
// Strip legacy fields (tier, lease_expires_at, expires_at) from projects
|
|
35
33
|
for (const proj of Object.values(parsed.projects)) {
|
|
36
34
|
const rec = proj;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
35
|
+
delete rec.tier;
|
|
36
|
+
delete rec.lease_expires_at;
|
|
37
|
+
delete rec.expires_at;
|
|
41
38
|
}
|
|
42
|
-
return
|
|
39
|
+
return {
|
|
40
|
+
...(parsed.active_project_id && { active_project_id: parsed.active_project_id }),
|
|
41
|
+
projects: parsed.projects,
|
|
42
|
+
};
|
|
43
43
|
}
|
|
44
44
|
return { projects: {} };
|
|
45
45
|
}
|
|
@@ -66,10 +66,32 @@ export function saveProject(projectId, project, path) {
|
|
|
66
66
|
store.projects[projectId] = project;
|
|
67
67
|
saveKeyStore(store, p);
|
|
68
68
|
}
|
|
69
|
+
export function updateProject(projectId, update, path) {
|
|
70
|
+
const p = path ?? getKeystorePath();
|
|
71
|
+
const store = loadKeyStore(p);
|
|
72
|
+
const existing = store.projects[projectId];
|
|
73
|
+
if (existing) {
|
|
74
|
+
store.projects[projectId] = { ...existing, ...update };
|
|
75
|
+
saveKeyStore(store, p);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
69
78
|
export function removeProject(projectId, path) {
|
|
70
79
|
const p = path ?? getKeystorePath();
|
|
71
80
|
const store = loadKeyStore(p);
|
|
72
81
|
delete store.projects[projectId];
|
|
82
|
+
if (store.active_project_id === projectId) {
|
|
83
|
+
delete store.active_project_id;
|
|
84
|
+
}
|
|
85
|
+
saveKeyStore(store, p);
|
|
86
|
+
}
|
|
87
|
+
export function getActiveProjectId(path) {
|
|
88
|
+
const store = loadKeyStore(path);
|
|
89
|
+
return store.active_project_id;
|
|
90
|
+
}
|
|
91
|
+
export function setActiveProjectId(projectId, path) {
|
|
92
|
+
const p = path ?? getKeystorePath();
|
|
93
|
+
const store = loadKeyStore(p);
|
|
94
|
+
store.active_project_id = projectId;
|
|
73
95
|
saveKeyStore(store, p);
|
|
74
96
|
}
|
|
75
97
|
//# sourceMappingURL=keystore.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
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
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
|
-
import { loadKeyStore, getProject, saveProject, removeProject, saveKeyStore } from "../core-dist/keystore.js";
|
|
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) {
|
|
@@ -36,5 +33,17 @@ export function findProject(id) {
|
|
|
36
33
|
return p;
|
|
37
34
|
}
|
|
38
35
|
|
|
36
|
+
export function resolveProject(id) {
|
|
37
|
+
const projectId = id || getActiveProjectId();
|
|
38
|
+
if (!projectId) { console.error("Error: no project specified and no active project set. Run: run402 projects provision"); process.exit(1); }
|
|
39
|
+
return findProject(projectId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveProjectId(id) {
|
|
43
|
+
const projectId = id || getActiveProjectId();
|
|
44
|
+
if (!projectId) { console.error("Error: no project specified and no active project set. Run: run402 projects provision"); process.exit(1); }
|
|
45
|
+
return projectId;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
// Re-export core keystore functions for direct use
|
|
40
|
-
export { loadKeyStore, saveProject, removeProject, saveKeyStore };
|
|
49
|
+
export { loadKeyStore, saveProject, updateProject, removeProject, saveKeyStore, getActiveProjectId, setActiveProjectId };
|
package/lib/deploy.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
-
import { API, allowanceAuthHeaders, saveProject } from "./config.mjs";
|
|
2
|
+
import { API, allowanceAuthHeaders, saveProject, setActiveProjectId } from "./config.mjs";
|
|
3
3
|
|
|
4
4
|
const HELP = `run402 deploy — Deploy a full-stack app or static site on Run402
|
|
5
5
|
|
|
@@ -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,17 +67,17 @@ 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); }
|
|
55
74
|
if (result.project_id) {
|
|
56
75
|
saveProject(result.project_id, {
|
|
57
76
|
anon_key: result.anon_key, service_key: result.service_key,
|
|
58
|
-
tier: result.tier, lease_expires_at: result.lease_expires_at,
|
|
59
77
|
site_url: result.site_url || result.subdomain_url,
|
|
60
78
|
deployed_at: new Date().toISOString(),
|
|
61
79
|
});
|
|
80
|
+
setActiveProjectId(result.project_id);
|
|
62
81
|
}
|
|
63
82
|
console.log(JSON.stringify(result, null, 2));
|
|
64
83
|
}
|
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,23 +75,15 @@ 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
|
-
// 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
|
-
|
|
94
87
|
if (tierInfo && tierInfo.tier && tierInfo.status === "active") {
|
|
95
88
|
const expiry = tierInfo.lease_expires_at ? tierInfo.lease_expires_at.split("T")[0] : "unknown";
|
|
96
89
|
line("Tier", `${tierInfo.tier} (expires ${expiry})`);
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders } from "./config.mjs";
|
|
1
|
+
import { findProject, loadKeyStore, saveProject, removeProject, API, allowanceAuthHeaders, setActiveProjectId, getActiveProjectId } from "./config.mjs";
|
|
2
2
|
|
|
3
3
|
const HELP = `run402 projects — Manage your deployed Run402 projects
|
|
4
4
|
|
|
@@ -8,8 +8,9 @@ Usage:
|
|
|
8
8
|
Subcommands:
|
|
9
9
|
quote Show pricing tiers
|
|
10
10
|
provision [--tier <tier>] [--name <n>] Provision a new Postgres project (pays via x402)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
use <id> Set the active project (used as default for other commands)
|
|
12
|
+
list List all your projects (IDs, URLs, active marker)
|
|
13
|
+
info <id> Show project details: REST URL, keys
|
|
13
14
|
sql <id> "<query>" Run a SQL query against a project's Postgres DB
|
|
14
15
|
rest <id> <table> [params] Query a table via the REST API (PostgREST)
|
|
15
16
|
usage <id> Show compute/storage usage for a project
|
|
@@ -21,6 +22,7 @@ Examples:
|
|
|
21
22
|
run402 projects quote
|
|
22
23
|
run402 projects provision --tier prototype
|
|
23
24
|
run402 projects provision --tier hobby --name my-app
|
|
25
|
+
run402 projects use prj_abc123
|
|
24
26
|
run402 projects list
|
|
25
27
|
run402 projects info abc123
|
|
26
28
|
run402 projects sql abc123 "SELECT * FROM users LIMIT 5"
|
|
@@ -32,6 +34,7 @@ Examples:
|
|
|
32
34
|
|
|
33
35
|
Notes:
|
|
34
36
|
- <id> is the project_id shown in 'run402 projects list'
|
|
37
|
+
- Most commands that take <id> default to the active project if omitted
|
|
35
38
|
- 'rest' uses PostgREST query syntax (table name + optional query string)
|
|
36
39
|
- 'provision' requires a funded allowance — payment is automatic via x402
|
|
37
40
|
- RLS templates: user_owns_rows, public_read, public_read_write
|
|
@@ -50,7 +53,7 @@ async function provision(args) {
|
|
|
50
53
|
if (args[i] === "--tier" && args[i + 1]) opts.tier = args[++i];
|
|
51
54
|
if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
|
|
52
55
|
}
|
|
53
|
-
const authHeaders =
|
|
56
|
+
const authHeaders = allowanceAuthHeaders("/projects/v1");
|
|
54
57
|
const body = { tier: opts.tier };
|
|
55
58
|
if (opts.name) body.name = opts.name;
|
|
56
59
|
const res = await fetch(`${API}/projects/v1`, {
|
|
@@ -60,13 +63,13 @@ async function provision(args) {
|
|
|
60
63
|
});
|
|
61
64
|
const data = await res.json();
|
|
62
65
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
63
|
-
// Save project credentials locally
|
|
66
|
+
// Save project credentials locally and set as active
|
|
64
67
|
if (data.project_id) {
|
|
65
68
|
saveProject(data.project_id, {
|
|
66
69
|
anon_key: data.anon_key, service_key: data.service_key,
|
|
67
|
-
tier: data.tier, lease_expires_at: data.lease_expires_at,
|
|
68
70
|
deployed_at: new Date().toISOString(),
|
|
69
71
|
});
|
|
72
|
+
setActiveProjectId(data.project_id);
|
|
70
73
|
}
|
|
71
74
|
console.log(JSON.stringify(data, null, 2));
|
|
72
75
|
}
|
|
@@ -88,17 +91,14 @@ async function list() {
|
|
|
88
91
|
const store = loadKeyStore();
|
|
89
92
|
const entries = Object.entries(store.projects);
|
|
90
93
|
if (entries.length === 0) { console.log(JSON.stringify({ status: "ok", projects: [], message: "No projects yet." })); return; }
|
|
91
|
-
|
|
94
|
+
const activeId = store.active_project_id;
|
|
95
|
+
console.log(JSON.stringify(entries.map(([id, p]) => ({ project_id: id, active: id === activeId, site_url: p.site_url, deployed_at: p.deployed_at })), null, 2));
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
async function info(projectId) {
|
|
95
99
|
const p = findProject(projectId);
|
|
96
|
-
const active = p.lease_expires_at ? new Date(p.lease_expires_at) > new Date() : null;
|
|
97
100
|
console.log(JSON.stringify({
|
|
98
101
|
project_id: projectId,
|
|
99
|
-
tier: p.tier,
|
|
100
|
-
active,
|
|
101
|
-
lease_expires_at: p.lease_expires_at,
|
|
102
102
|
rest_url: `${API}/rest/v1`,
|
|
103
103
|
anon_key: p.anon_key,
|
|
104
104
|
service_key: p.service_key,
|
|
@@ -135,6 +135,13 @@ async function schema(projectId) {
|
|
|
135
135
|
console.log(JSON.stringify(data, null, 2));
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
async function use(projectId) {
|
|
139
|
+
if (!projectId) { console.error("Usage: run402 projects use <project_id>"); process.exit(1); }
|
|
140
|
+
findProject(projectId); // verify it exists
|
|
141
|
+
setActiveProjectId(projectId);
|
|
142
|
+
console.log(JSON.stringify({ status: "ok", active_project_id: projectId }));
|
|
143
|
+
}
|
|
144
|
+
|
|
138
145
|
async function deleteProject(projectId) {
|
|
139
146
|
const p = findProject(projectId);
|
|
140
147
|
const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
@@ -155,6 +162,7 @@ export async function run(sub, args) {
|
|
|
155
162
|
switch (sub) {
|
|
156
163
|
case "quote": await quote(); break;
|
|
157
164
|
case "provision": await provision(args); break;
|
|
165
|
+
case "use": await use(args[0]); break;
|
|
158
166
|
case "list": await list(); break;
|
|
159
167
|
case "info": await info(args[0]); break;
|
|
160
168
|
case "sql": await sqlCmd(args[0], args[1]); break;
|
package/lib/sites.mjs
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
|
-
import { API, allowanceAuthHeaders } from "./config.mjs";
|
|
2
|
+
import { API, allowanceAuthHeaders, resolveProjectId, updateProject } from "./config.mjs";
|
|
3
3
|
|
|
4
4
|
const HELP = `run402 sites — Deploy and manage static sites
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
|
-
run402 sites deploy --
|
|
7
|
+
run402 sites deploy --manifest <file> [--project <id>] [--target <target>]
|
|
8
8
|
run402 sites status <deployment_id>
|
|
9
|
-
cat manifest.json | run402 sites deploy
|
|
9
|
+
cat manifest.json | run402 sites deploy
|
|
10
10
|
|
|
11
11
|
Subcommands:
|
|
12
12
|
deploy Deploy a static site
|
|
13
13
|
status Check the status of a deployment
|
|
14
14
|
|
|
15
15
|
Options (deploy):
|
|
16
|
-
--name <name> Site name (e.g. 'portfolio', 'family-todo')
|
|
17
16
|
--manifest <file> Path to manifest JSON file (or read from stdin)
|
|
18
|
-
--project <id>
|
|
17
|
+
--project <id> Project ID (defaults to active project)
|
|
19
18
|
--target <target> Deployment target (e.g. 'production')
|
|
20
19
|
--help, -h Show this help message
|
|
21
20
|
|
|
@@ -28,9 +27,9 @@ Manifest format (JSON):
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
Examples:
|
|
31
|
-
run402 sites deploy --
|
|
32
|
-
run402 sites status
|
|
33
|
-
cat site.json | run402 sites deploy
|
|
30
|
+
run402 sites deploy --manifest site.json
|
|
31
|
+
run402 sites status dpl_abc123
|
|
32
|
+
cat site.json | run402 sites deploy
|
|
34
33
|
|
|
35
34
|
Notes:
|
|
36
35
|
- Must include at least index.html in the files array
|
|
@@ -44,21 +43,19 @@ async function readStdin() {
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
async function deploy(args) {
|
|
47
|
-
const opts = {
|
|
46
|
+
const opts = { manifest: null, project: undefined, target: undefined };
|
|
48
47
|
for (let i = 0; i < args.length; i++) {
|
|
49
48
|
if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
|
|
50
|
-
if (args[i] === "--name" && args[i + 1]) opts.name = args[++i];
|
|
51
49
|
if (args[i] === "--manifest" && args[i + 1]) opts.manifest = args[++i];
|
|
52
50
|
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
53
51
|
if (args[i] === "--target" && args[i + 1]) opts.target = args[++i];
|
|
54
52
|
}
|
|
55
|
-
|
|
53
|
+
const projectId = resolveProjectId(opts.project);
|
|
56
54
|
const manifest = opts.manifest ? JSON.parse(readFileSync(opts.manifest, "utf-8")) : JSON.parse(await readStdin());
|
|
57
|
-
const body = {
|
|
58
|
-
if (opts.project) body.project = opts.project;
|
|
55
|
+
const body = { files: manifest.files, project: projectId };
|
|
59
56
|
if (opts.target) body.target = opts.target;
|
|
60
57
|
|
|
61
|
-
const authHeaders =
|
|
58
|
+
const authHeaders = allowanceAuthHeaders("/deployments/v1");
|
|
62
59
|
const res = await fetch(`${API}/deployments/v1`, {
|
|
63
60
|
method: "POST",
|
|
64
61
|
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
@@ -66,6 +63,9 @@ async function deploy(args) {
|
|
|
66
63
|
});
|
|
67
64
|
const data = await res.json();
|
|
68
65
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
66
|
+
if (data.deployment_id) {
|
|
67
|
+
updateProject(projectId, { last_deployment_id: data.deployment_id });
|
|
68
|
+
}
|
|
69
69
|
console.log(JSON.stringify(data, null, 2));
|
|
70
70
|
}
|
|
71
71
|
|
package/lib/subdomains.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveProject, resolveProjectId, API } from "./config.mjs";
|
|
2
2
|
|
|
3
3
|
const HELP = `run402 subdomains — Manage custom subdomains
|
|
4
4
|
|
|
@@ -6,31 +6,43 @@ Usage:
|
|
|
6
6
|
run402 subdomains <subcommand> [args...]
|
|
7
7
|
|
|
8
8
|
Subcommands:
|
|
9
|
-
claim <
|
|
10
|
-
delete <name> --project <id>
|
|
11
|
-
list <id>
|
|
9
|
+
claim <name> [--project <id>] [--deployment <id>] Claim a subdomain
|
|
10
|
+
delete <name> [--project <id>] Release a subdomain
|
|
11
|
+
list [<id>] List subdomains for a project
|
|
12
|
+
|
|
13
|
+
Options default to the active project and its last deployment when omitted.
|
|
14
|
+
Legacy syntax 'claim <deployment_id> <name>' is still supported.
|
|
12
15
|
|
|
13
16
|
Examples:
|
|
14
|
-
run402 subdomains claim
|
|
15
|
-
run402 subdomains claim
|
|
17
|
+
run402 subdomains claim myapp
|
|
18
|
+
run402 subdomains claim myapp --deployment dpl_abc123 --project proj123
|
|
16
19
|
run402 subdomains delete myapp
|
|
17
|
-
run402 subdomains list
|
|
20
|
+
run402 subdomains list
|
|
18
21
|
|
|
19
22
|
Notes:
|
|
20
23
|
- Subdomain names: 3-63 chars, lowercase alphanumeric + hyphens
|
|
21
24
|
- Creates <name>.run402.com pointing to the deployment
|
|
22
25
|
`;
|
|
23
26
|
|
|
24
|
-
async function claim(
|
|
25
|
-
const opts = { project: null };
|
|
26
|
-
for (let i = 0; i <
|
|
27
|
-
if (
|
|
27
|
+
async function claim(positionalArgs, flagArgs) {
|
|
28
|
+
const opts = { project: null, deployment: null };
|
|
29
|
+
for (let i = 0; i < flagArgs.length; i++) {
|
|
30
|
+
if (flagArgs[i] === "--project" && flagArgs[i + 1]) opts.project = flagArgs[++i];
|
|
31
|
+
if (flagArgs[i] === "--deployment" && flagArgs[i + 1]) opts.deployment = flagArgs[++i];
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
// positional: [name] or [deployment_id, name]
|
|
34
|
+
let name, deploymentId;
|
|
35
|
+
if (positionalArgs.length >= 2) {
|
|
36
|
+
deploymentId = positionalArgs[0];
|
|
37
|
+
name = positionalArgs[1];
|
|
38
|
+
} else if (positionalArgs.length === 1) {
|
|
39
|
+
name = positionalArgs[0];
|
|
32
40
|
}
|
|
33
|
-
|
|
41
|
+
if (!name) { console.error("Usage: run402 subdomains claim <name> [--project <id>] [--deployment <id>]"); process.exit(1); }
|
|
42
|
+
const projectId = resolveProjectId(opts.project);
|
|
43
|
+
const p = resolveProject(opts.project);
|
|
44
|
+
deploymentId = opts.deployment || deploymentId || p.last_deployment_id;
|
|
45
|
+
if (!deploymentId) { console.error("Error: no deployment_id specified and no recent deployment found. Deploy a site first or pass --deployment <id>."); process.exit(1); }
|
|
34
46
|
const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${p.service_key}` };
|
|
35
47
|
const res = await fetch(`${API}/subdomains/v1`, {
|
|
36
48
|
method: "POST",
|
|
@@ -47,11 +59,7 @@ async function deleteSubdomain(name, args) {
|
|
|
47
59
|
for (let i = 0; i < args.length; i++) {
|
|
48
60
|
if (args[i] === "--project" && args[i + 1]) opts.project = args[++i];
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
console.error("Error: --project <id> is required for subdomain delete.");
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
const p = findProject(opts.project);
|
|
62
|
+
const p = resolveProject(opts.project);
|
|
55
63
|
const headers = { "Authorization": `Bearer ${p.service_key}` };
|
|
56
64
|
const res = await fetch(`${API}/subdomains/v1/${encodeURIComponent(name)}`, {
|
|
57
65
|
method: "DELETE",
|
|
@@ -66,7 +74,7 @@ async function deleteSubdomain(name, args) {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
async function list(projectId) {
|
|
69
|
-
const p =
|
|
77
|
+
const p = resolveProject(projectId);
|
|
70
78
|
const res = await fetch(`${API}/subdomains/v1`, {
|
|
71
79
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
72
80
|
});
|
|
@@ -78,7 +86,17 @@ async function list(projectId) {
|
|
|
78
86
|
export async function run(sub, args) {
|
|
79
87
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
80
88
|
switch (sub) {
|
|
81
|
-
case "claim":
|
|
89
|
+
case "claim": {
|
|
90
|
+
const positional = [];
|
|
91
|
+
const flags = [];
|
|
92
|
+
let i = 0;
|
|
93
|
+
while (i < args.length) {
|
|
94
|
+
if (args[i].startsWith("--")) { flags.push(args[i], args[i + 1]); i += 2; }
|
|
95
|
+
else { positional.push(args[i]); i++; }
|
|
96
|
+
}
|
|
97
|
+
await claim(positional, flags);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
82
100
|
case "delete": await deleteSubdomain(args[0], args.slice(1)); break;
|
|
83
101
|
case "list": await list(args[0]); break;
|
|
84
102
|
default:
|
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