run402 1.48.0 → 1.50.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/config.js +10 -0
- package/core-dist/wallet-auth.js +62 -0
- package/core-dist/wallet.js +25 -0
- package/lib/deploy-v2.mjs +359 -0
- package/lib/deploy.mjs +14 -0
- package/lib/functions.mjs +16 -1
- package/package.json +1 -1
- package/sdk/core-dist/config.js +10 -0
- package/sdk/core-dist/wallet-auth.js +62 -0
- package/sdk/core-dist/wallet.js +25 -0
- package/sdk/dist/errors.d.ts +41 -0
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js +23 -0
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +24 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +24 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/apps.d.ts +14 -0
- package/sdk/dist/namespaces/apps.d.ts.map +1 -1
- package/sdk/dist/namespaces/apps.js +162 -19
- package/sdk/dist/namespaces/apps.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts +116 -0
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -0
- package/sdk/dist/namespaces/deploy.js +1127 -0
- package/sdk/dist/namespaces/deploy.js.map +1 -0
- package/sdk/dist/namespaces/deploy.types.d.ts +430 -0
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -0
- package/sdk/dist/namespaces/deploy.types.js +11 -0
- package/sdk/dist/namespaces/deploy.types.js.map +1 -0
- package/sdk/dist/namespaces/functions.d.ts +11 -3
- package/sdk/dist/namespaces/functions.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.js +11 -3
- package/sdk/dist/namespaces/functions.js.map +1 -1
- package/sdk/dist/namespaces/functions.types.d.ts +21 -4
- package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
- package/sdk/dist/node/canonicalize.d.ts +12 -5
- package/sdk/dist/node/canonicalize.d.ts.map +1 -1
- package/sdk/dist/node/canonicalize.js +12 -5
- package/sdk/dist/node/canonicalize.js.map +1 -1
- package/sdk/dist/node/files.d.ts +38 -0
- package/sdk/dist/node/files.d.ts.map +1 -0
- package/sdk/dist/node/files.js +88 -0
- package/sdk/dist/node/files.js.map +1 -0
- package/sdk/dist/node/index.d.ts +5 -3
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +2 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/sites-node.d.ts +34 -107
- package/sdk/dist/node/sites-node.d.ts.map +1 -1
- package/sdk/dist/node/sites-node.js +84 -353
- package/sdk/dist/node/sites-node.js.map +1 -1
package/core-dist/config.js
CHANGED
|
@@ -4,6 +4,16 @@ import { existsSync, renameSync, mkdirSync } from "node:fs";
|
|
|
4
4
|
export function getApiBase() {
|
|
5
5
|
return process.env.RUN402_API_BASE || "https://api.run402.com";
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* API base for the deploy-v2 routes. Defaults to the same value as
|
|
9
|
+
* `getApiBase()`. Set `RUN402_DEPLOY_API_BASE` to point only deploy traffic
|
|
10
|
+
* elsewhere — useful when running deploy-v2 against a staging gateway while
|
|
11
|
+
* the rest of the SDK still talks to production. In normal use callers
|
|
12
|
+
* should not need this override.
|
|
13
|
+
*/
|
|
14
|
+
export function getDeployApiBase() {
|
|
15
|
+
return process.env.RUN402_DEPLOY_API_BASE || getApiBase();
|
|
16
|
+
}
|
|
7
17
|
export function getConfigDir() {
|
|
8
18
|
return process.env.RUN402_CONFIG_DIR || join(homedir(), ".config", "run402");
|
|
9
19
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run402 deploy apply` and `run402 deploy resume` — CLI wrappers over the
|
|
3
|
+
* unified deploy primitive (`r.deploy.apply` / `r.deploy.resume`).
|
|
4
|
+
*
|
|
5
|
+
* The legacy `run402 deploy --manifest …` command is preserved in
|
|
6
|
+
* `cli/lib/deploy.mjs` and continues to work; this file adds the new
|
|
7
|
+
* subcommand surface.
|
|
8
|
+
*
|
|
9
|
+
* Manifest format mirrors the MCP `deploy` tool's input schema:
|
|
10
|
+
* {
|
|
11
|
+
* "project_id": "...",
|
|
12
|
+
* "base": { "release": "current" } | { "release": "empty" } | { "release_id": "..." },
|
|
13
|
+
* "database": { "migrations": [...], "expose": {...}, "zero_downtime": false },
|
|
14
|
+
* "secrets": { "set": {...}, "delete": [...], "replace_all": {...} },
|
|
15
|
+
* "functions": { "replace": {...}, "patch": { "set": {...}, "delete": [...] } },
|
|
16
|
+
* "site": { "replace": {...} } | { "patch": { "put": {...}, "delete": [...] } },
|
|
17
|
+
* "subdomains": { "set": ["..."], "add": [...], "remove": [...] },
|
|
18
|
+
* "idempotency_key": "..."
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* File entries: `{ "data": "...", "encoding": "utf-8" | "base64", "contentType": "..." }`
|
|
22
|
+
* — same shape used by `bundle_deploy`. UTF-8 is the default; binary files
|
|
23
|
+
* pass `"encoding": "base64"`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync } from "node:fs";
|
|
27
|
+
import { resolve, dirname, isAbsolute, join } from "node:path";
|
|
28
|
+
import { getSdk } from "./sdk.mjs";
|
|
29
|
+
import { reportSdkError } from "./sdk-errors.mjs";
|
|
30
|
+
import { allowanceAuthHeaders, resolveProjectId } from "./config.mjs";
|
|
31
|
+
|
|
32
|
+
const APPLY_HELP = `run402 deploy apply — Unified deploy primitive (v1.34+)
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
run402 deploy apply --manifest <path> [--project <id>] [--quiet]
|
|
36
|
+
run402 deploy apply --spec '<json>' [--project <id>] [--quiet]
|
|
37
|
+
cat spec.json | run402 deploy apply [--project <id>]
|
|
38
|
+
|
|
39
|
+
Manifest format mirrors the MCP \`deploy\` tool's ReleaseSpec:
|
|
40
|
+
{
|
|
41
|
+
"project_id": "prj_...",
|
|
42
|
+
"base": { "release": "current" },
|
|
43
|
+
"database": { "migrations": [{ "id": "001_init", "sql": "CREATE TABLE ..." }], "expose": {...} },
|
|
44
|
+
"secrets": { "set": { "OPENAI_API_KEY": { "value": "sk-..." } } },
|
|
45
|
+
"functions": { "replace": { "api": { "source": { "data": "export default ..." } } } },
|
|
46
|
+
"site": { "replace": { "index.html": { "data": "<html>..." } } },
|
|
47
|
+
"subdomains": { "set": ["my-app"] }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--manifest <path> Read the spec from this JSON file
|
|
52
|
+
--spec '<json>' Inline JSON spec (single-quote in shell)
|
|
53
|
+
--project <id> Override project_id from the manifest
|
|
54
|
+
--quiet Suppress per-event JSON-line stderr (final result still on stdout)
|
|
55
|
+
|
|
56
|
+
Output:
|
|
57
|
+
stdout: { "status": "ok", "release_id": "rel_...", "operation_id": "op_...", "urls": {...} }
|
|
58
|
+
stderr: one JSON event per line (suppressed with --quiet)
|
|
59
|
+
|
|
60
|
+
Patch examples (only the listed file changes):
|
|
61
|
+
{ "project_id": "prj_...", "site": { "patch": { "put": { "index.html": { "data": "..." } } } } }
|
|
62
|
+
{ "project_id": "prj_...", "site": { "patch": { "delete": ["old.html"] } } }
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const RESUME_HELP = `run402 deploy resume — Resume a stuck deploy operation
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
run402 deploy resume <operation_id> [--quiet]
|
|
69
|
+
|
|
70
|
+
Used when a previous \`deploy apply\` ended in \`activation_pending\` or
|
|
71
|
+
\`schema_settling\` (e.g. transient gateway failure between SQL commit and
|
|
72
|
+
the pointer-swap activation). The gateway re-runs only the failed phase
|
|
73
|
+
forward — SQL is never replayed.
|
|
74
|
+
|
|
75
|
+
Output:
|
|
76
|
+
stdout: { "status": "ok", "release_id": "...", "operation_id": "...", "urls": {...} }
|
|
77
|
+
stderr: one JSON event per line (suppressed with --quiet)
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
export async function runDeployV2(sub, args) {
|
|
81
|
+
if (sub === "apply") return await applyCmd(args);
|
|
82
|
+
if (sub === "resume") return await resumeCmd(args);
|
|
83
|
+
console.error(JSON.stringify({ status: "error", message: `Unknown deploy subcommand: ${sub}` }));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readStdin() {
|
|
88
|
+
const chunks = [];
|
|
89
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
90
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeStderrEventWriter(quiet) {
|
|
94
|
+
if (quiet) return undefined;
|
|
95
|
+
return (event) => {
|
|
96
|
+
console.error(JSON.stringify(event));
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function applyCmd(args) {
|
|
101
|
+
const opts = { manifest: null, spec: null, project: null, quiet: false };
|
|
102
|
+
for (let i = 0; i < args.length; i++) {
|
|
103
|
+
if (args[i] === "--help" || args[i] === "-h") { console.log(APPLY_HELP); process.exit(0); }
|
|
104
|
+
if (args[i] === "--manifest" && args[i + 1]) { opts.manifest = args[++i]; continue; }
|
|
105
|
+
if (args[i] === "--spec" && args[i + 1]) { opts.spec = args[++i]; continue; }
|
|
106
|
+
if (args[i] === "--project" && args[i + 1]) { opts.project = args[++i]; continue; }
|
|
107
|
+
if (args[i] === "--quiet") { opts.quiet = true; continue; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let raw;
|
|
111
|
+
if (opts.spec) {
|
|
112
|
+
raw = opts.spec;
|
|
113
|
+
} else if (opts.manifest) {
|
|
114
|
+
try {
|
|
115
|
+
const manifestPath = isAbsolute(opts.manifest) ? opts.manifest : resolve(process.cwd(), opts.manifest);
|
|
116
|
+
raw = readFileSync(manifestPath, "utf-8");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error(JSON.stringify({ status: "error", message: `Failed to read manifest: ${err.message}` }));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
raw = await readStdin();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let spec;
|
|
126
|
+
try {
|
|
127
|
+
spec = JSON.parse(raw);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error(JSON.stringify({ status: "error", message: `Manifest is not valid JSON: ${err.message}` }));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opts.manifest) resolveFileDataPaths(spec, dirname(resolve(opts.manifest)));
|
|
134
|
+
|
|
135
|
+
if (opts.project && spec.project_id && spec.project_id !== opts.project) {
|
|
136
|
+
console.error(JSON.stringify({
|
|
137
|
+
status: "error",
|
|
138
|
+
message: `project_id conflict: spec.project_id=${spec.project_id} but --project=${opts.project}`,
|
|
139
|
+
}));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
if (opts.project) spec.project_id = opts.project;
|
|
143
|
+
if (!spec.project_id) spec.project_id = resolveProjectId(null);
|
|
144
|
+
|
|
145
|
+
// Translate { project_id, ... } envelope → ReleaseSpec ({ project, ... })
|
|
146
|
+
// The SDK ReleaseSpec uses `project` rather than `project_id`; both shapes
|
|
147
|
+
// are accepted at the manifest layer (project_id is friendlier for agents
|
|
148
|
+
// sharing JSON manifests with the MCP tool).
|
|
149
|
+
const releaseSpec = mapManifestToReleaseSpec(spec);
|
|
150
|
+
const idempotencyKey = spec.idempotency_key;
|
|
151
|
+
|
|
152
|
+
// Preserve the aggressive early exit when no allowance is configured.
|
|
153
|
+
allowanceAuthHeaders("/deploy/v2/plans");
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = await getSdk().deploy.apply(releaseSpec, {
|
|
157
|
+
onEvent: makeStderrEventWriter(opts.quiet),
|
|
158
|
+
idempotencyKey,
|
|
159
|
+
});
|
|
160
|
+
console.log(JSON.stringify({ status: "ok", ...result }, null, 2));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
reportSdkError(err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function resumeCmd(args) {
|
|
167
|
+
const opts = { operationId: null, quiet: false };
|
|
168
|
+
for (let i = 0; i < args.length; i++) {
|
|
169
|
+
if (args[i] === "--help" || args[i] === "-h") { console.log(RESUME_HELP); process.exit(0); }
|
|
170
|
+
if (args[i] === "--quiet") { opts.quiet = true; continue; }
|
|
171
|
+
if (!args[i].startsWith("-") && !opts.operationId) opts.operationId = args[i];
|
|
172
|
+
}
|
|
173
|
+
if (!opts.operationId) {
|
|
174
|
+
console.error(JSON.stringify({ status: "error", message: "Usage: run402 deploy resume <operation_id>" }));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
allowanceAuthHeaders("/deploy/v2/operations");
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const result = await getSdk().deploy.resume(opts.operationId, {
|
|
182
|
+
onEvent: makeStderrEventWriter(opts.quiet),
|
|
183
|
+
});
|
|
184
|
+
console.log(JSON.stringify({ status: "ok", ...result }, null, 2));
|
|
185
|
+
} catch (err) {
|
|
186
|
+
reportSdkError(err);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Manifest → ReleaseSpec ──────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
function mapManifestToReleaseSpec(spec) {
|
|
193
|
+
const out = { project: spec.project_id };
|
|
194
|
+
if (spec.base !== undefined) out.base = spec.base;
|
|
195
|
+
if (spec.subdomains !== undefined) out.subdomains = spec.subdomains;
|
|
196
|
+
if (spec.secrets !== undefined) out.secrets = spec.secrets;
|
|
197
|
+
if (spec.routes !== undefined) out.routes = spec.routes;
|
|
198
|
+
if (spec.checks !== undefined) out.checks = spec.checks;
|
|
199
|
+
|
|
200
|
+
if (spec.database) {
|
|
201
|
+
out.database = {};
|
|
202
|
+
if (spec.database.expose !== undefined) out.database.expose = spec.database.expose;
|
|
203
|
+
if (spec.database.zero_downtime !== undefined) out.database.zero_downtime = spec.database.zero_downtime;
|
|
204
|
+
if (spec.database.migrations) {
|
|
205
|
+
out.database.migrations = spec.database.migrations.map((m) => {
|
|
206
|
+
const mm = { id: m.id };
|
|
207
|
+
if (m.sql !== undefined) mm.sql = m.sql;
|
|
208
|
+
if (m.sql_ref !== undefined) mm.sql_ref = m.sql_ref;
|
|
209
|
+
if (m.checksum !== undefined) mm.checksum = m.checksum;
|
|
210
|
+
if (m.transaction !== undefined) mm.transaction = m.transaction;
|
|
211
|
+
return mm;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (spec.functions) {
|
|
217
|
+
out.functions = {};
|
|
218
|
+
if (spec.functions.replace) out.functions.replace = mapFunctionMap(spec.functions.replace);
|
|
219
|
+
if (spec.functions.patch) {
|
|
220
|
+
out.functions.patch = {};
|
|
221
|
+
if (spec.functions.patch.set) out.functions.patch.set = mapFunctionMap(spec.functions.patch.set);
|
|
222
|
+
if (spec.functions.patch.delete) out.functions.patch.delete = spec.functions.patch.delete;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (spec.site) {
|
|
227
|
+
if (spec.site.replace) {
|
|
228
|
+
out.site = { replace: mapFileMap(spec.site.replace) };
|
|
229
|
+
} else if (spec.site.patch) {
|
|
230
|
+
const patch = {};
|
|
231
|
+
if (spec.site.patch.put) patch.put = mapFileMap(spec.site.patch.put);
|
|
232
|
+
if (spec.site.patch.delete) patch.delete = spec.site.patch.delete;
|
|
233
|
+
out.site = { patch };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function mapFunctionMap(map) {
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const [name, fn] of Object.entries(map)) {
|
|
243
|
+
const f = {};
|
|
244
|
+
if (fn.runtime) f.runtime = fn.runtime;
|
|
245
|
+
if (fn.source !== undefined) f.source = fileEntryToContentSource(fn.source);
|
|
246
|
+
if (fn.files) f.files = mapFileMap(fn.files);
|
|
247
|
+
if (fn.entrypoint !== undefined) f.entrypoint = fn.entrypoint;
|
|
248
|
+
if (fn.config !== undefined) f.config = fn.config;
|
|
249
|
+
if (fn.schedule !== undefined) f.schedule = fn.schedule;
|
|
250
|
+
out[name] = f;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function mapFileMap(map) {
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const [path, entry] of Object.entries(map)) {
|
|
258
|
+
out[path] = fileEntryToContentSource(entry);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function fileEntryToContentSource(entry) {
|
|
264
|
+
if (entry === null || entry === undefined) return entry;
|
|
265
|
+
if (typeof entry === "string") return entry;
|
|
266
|
+
if (entry instanceof Uint8Array) return entry;
|
|
267
|
+
if (typeof entry === "object") {
|
|
268
|
+
if (entry.encoding === "base64" && typeof entry.data === "string") {
|
|
269
|
+
const bytes = Buffer.from(entry.data, "base64");
|
|
270
|
+
const u8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
271
|
+
return entry.contentType ? { data: u8, contentType: entry.contentType } : u8;
|
|
272
|
+
}
|
|
273
|
+
if (typeof entry.data === "string") {
|
|
274
|
+
return entry.contentType ? { data: entry.data, contentType: entry.contentType } : entry.data;
|
|
275
|
+
}
|
|
276
|
+
// Pre-resolved ContentRef shape — pass through.
|
|
277
|
+
if (typeof entry.sha256 === "string" && typeof entry.size === "number") {
|
|
278
|
+
return entry;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return entry;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Resolve any `{ "path": "..." }` entries in the manifest to inline data.
|
|
286
|
+
* Mirrors the legacy deploy.mjs behavior so `run402 deploy apply` accepts
|
|
287
|
+
* the same files-with-paths shape that `run402 deploy` does today.
|
|
288
|
+
*/
|
|
289
|
+
function resolveFileDataPaths(spec, baseDir) {
|
|
290
|
+
// Site files
|
|
291
|
+
if (spec.site?.replace) resolveMap(spec.site.replace, baseDir);
|
|
292
|
+
if (spec.site?.patch?.put) resolveMap(spec.site.patch.put, baseDir);
|
|
293
|
+
// Function files
|
|
294
|
+
const visitFns = (fnMap) => {
|
|
295
|
+
if (!fnMap) return;
|
|
296
|
+
for (const fn of Object.values(fnMap)) {
|
|
297
|
+
if (fn.source && typeof fn.source === "object" && fn.source.path) {
|
|
298
|
+
const resolved = readFileEntry(fn.source, baseDir);
|
|
299
|
+
if (resolved) fn.source = resolved;
|
|
300
|
+
}
|
|
301
|
+
if (fn.files) resolveMap(fn.files, baseDir);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
visitFns(spec.functions?.replace);
|
|
305
|
+
visitFns(spec.functions?.patch?.set);
|
|
306
|
+
// Migration sql_path / sql_file
|
|
307
|
+
if (spec.database?.migrations) {
|
|
308
|
+
for (const m of spec.database.migrations) {
|
|
309
|
+
if (!m.sql && m.sql_path) {
|
|
310
|
+
try {
|
|
311
|
+
const p = isAbsolute(m.sql_path) ? m.sql_path : join(baseDir, m.sql_path);
|
|
312
|
+
m.sql = readFileSync(p, "utf-8");
|
|
313
|
+
delete m.sql_path;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(JSON.stringify({
|
|
316
|
+
status: "error",
|
|
317
|
+
message: `Failed to read migration sql_path '${m.sql_path}': ${err.message}`,
|
|
318
|
+
}));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function resolveMap(map, baseDir) {
|
|
327
|
+
for (const [key, entry] of Object.entries(map)) {
|
|
328
|
+
if (entry && typeof entry === "object" && typeof entry.path === "string" && entry.data === undefined) {
|
|
329
|
+
const resolved = readFileEntry(entry, baseDir);
|
|
330
|
+
if (resolved) map[key] = resolved;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function readFileEntry(entry, baseDir) {
|
|
336
|
+
try {
|
|
337
|
+
const p = isAbsolute(entry.path) ? entry.path : join(baseDir, entry.path);
|
|
338
|
+
const buf = readFileSync(p);
|
|
339
|
+
const out = {};
|
|
340
|
+
// Detect text vs binary via simple UTF-8 round-trip; mirrors the bundle
|
|
341
|
+
// deploy behavior. Image/font types get base64; HTML/CSS/JS stay UTF-8.
|
|
342
|
+
const looksTextual = !entry.contentType?.match(/^(image|font|application\/(pdf|wasm|octet-stream|zip))/);
|
|
343
|
+
if (looksTextual) {
|
|
344
|
+
out.data = buf.toString("utf-8");
|
|
345
|
+
out.encoding = "utf-8";
|
|
346
|
+
} else {
|
|
347
|
+
out.data = buf.toString("base64");
|
|
348
|
+
out.encoding = "base64";
|
|
349
|
+
}
|
|
350
|
+
if (entry.contentType) out.contentType = entry.contentType;
|
|
351
|
+
return out;
|
|
352
|
+
} catch (err) {
|
|
353
|
+
console.error(JSON.stringify({
|
|
354
|
+
status: "error",
|
|
355
|
+
message: `Failed to read file '${entry.path}': ${err.message}`,
|
|
356
|
+
}));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
}
|
package/lib/deploy.mjs
CHANGED
|
@@ -255,6 +255,20 @@ async function loadManifest(opts) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
export async function run(args) {
|
|
258
|
+
// Subcommand dispatch (v1.34+):
|
|
259
|
+
// run402 deploy apply ... → unified deploy primitive (deploy.apply)
|
|
260
|
+
// run402 deploy resume <op> → resume an activation_pending operation
|
|
261
|
+
// run402 deploy --manifest … → legacy bundle deploy (still works)
|
|
262
|
+
const sub = args[0];
|
|
263
|
+
switch (sub) {
|
|
264
|
+
case "apply":
|
|
265
|
+
case "resume": {
|
|
266
|
+
const { runDeployV2 } = await import("./deploy-v2.mjs");
|
|
267
|
+
await runDeployV2(sub, args.slice(1));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
258
272
|
const opts = { manifest: null, project: null };
|
|
259
273
|
for (let i = 0; i < args.length; i++) {
|
|
260
274
|
if (args[i] === "--help" || args[i] === "-h") { console.log(HELP); process.exit(0); }
|
package/lib/functions.mjs
CHANGED
|
@@ -53,7 +53,15 @@ Options:
|
|
|
53
53
|
--file <file> Required: path to the function source file
|
|
54
54
|
--timeout <s> Runtime timeout in seconds
|
|
55
55
|
--memory <mb> Memory in MB
|
|
56
|
-
--deps <
|
|
56
|
+
--deps <spec,...> Comma-separated npm specs to install and bundle.
|
|
57
|
+
Bare names (e.g. 'lodash') resolve to latest at
|
|
58
|
+
deploy time; pinned ('lodash@4.17.21') or range
|
|
59
|
+
('date-fns@^3.0.0') specs are honored verbatim.
|
|
60
|
+
'@run402/functions' is auto-bundled and is rejected
|
|
61
|
+
here; the legacy 'run402-functions' name is also
|
|
62
|
+
rejected. Max 30 entries, max 200 chars per spec.
|
|
63
|
+
Native binary modules (sharp, canvas, native bcrypt)
|
|
64
|
+
are rejected.
|
|
57
65
|
--schedule <cron> Cron schedule; pass '' to clear an existing schedule
|
|
58
66
|
|
|
59
67
|
Notes:
|
|
@@ -61,6 +69,13 @@ Notes:
|
|
|
61
69
|
export default async (req: Request) => Response
|
|
62
70
|
Deploy may require payment if the project lease has expired.
|
|
63
71
|
|
|
72
|
+
The deploy response includes:
|
|
73
|
+
- runtime_version: the bundled @run402/functions version (e.g. "1.48.0")
|
|
74
|
+
- deps_resolved: map of each --deps name to the actually-installed
|
|
75
|
+
concrete version (e.g. {"lodash":"4.17.21"})
|
|
76
|
+
- warnings (optional, top-level, sibling to the record): non-fatal
|
|
77
|
+
notes such as bundle-size advisories
|
|
78
|
+
|
|
64
79
|
Examples:
|
|
65
80
|
run402 functions deploy abc123 stripe-webhook --file handler.ts
|
|
66
81
|
run402 functions deploy abc123 send-reminders --file remind.ts \\
|
package/package.json
CHANGED
package/sdk/core-dist/config.js
CHANGED
|
@@ -4,6 +4,16 @@ import { existsSync, renameSync, mkdirSync } from "node:fs";
|
|
|
4
4
|
export function getApiBase() {
|
|
5
5
|
return process.env.RUN402_API_BASE || "https://api.run402.com";
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* API base for the deploy-v2 routes. Defaults to the same value as
|
|
9
|
+
* `getApiBase()`. Set `RUN402_DEPLOY_API_BASE` to point only deploy traffic
|
|
10
|
+
* elsewhere — useful when running deploy-v2 against a staging gateway while
|
|
11
|
+
* the rest of the SDK still talks to production. In normal use callers
|
|
12
|
+
* should not need this override.
|
|
13
|
+
*/
|
|
14
|
+
export function getDeployApiBase() {
|
|
15
|
+
return process.env.RUN402_DEPLOY_API_BASE || getApiBase();
|
|
16
|
+
}
|
|
7
17
|
export function getConfigDir() {
|
|
8
18
|
return process.env.RUN402_CONFIG_DIR || join(homedir(), ".config", "run402");
|
|
9
19
|
}
|
|
@@ -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/sdk/dist/errors.d.ts
CHANGED
|
@@ -36,4 +36,45 @@ export declare class LocalError extends Run402Error {
|
|
|
36
36
|
readonly cause?: unknown;
|
|
37
37
|
constructor(message: string, context: string, cause?: unknown);
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Deploy-state-machine failure surfaced from the v2 deploy flow. Carries the
|
|
41
|
+
* structured error envelope the gateway returns alongside the operation
|
|
42
|
+
* snapshot — phase, resource, retryability, and an optional remediation hint.
|
|
43
|
+
*
|
|
44
|
+
* The `code` enumerates the gateway's deploy error codes; consumers may
|
|
45
|
+
* switch on it to decide whether to retry, prompt the user for payment, ask
|
|
46
|
+
* for a fix, or escalate. Unknown codes from a newer gateway pass through
|
|
47
|
+
* verbatim — callers should treat unrecognized values as opaque.
|
|
48
|
+
*/
|
|
49
|
+
export type Run402DeployErrorCode = "MIGRATION_FAILED" | "MIGRATION_CHECKSUM_MISMATCH" | "MIGRATION_SQL_NOT_FOUND" | "BASE_RELEASE_CONFLICT" | "PAYMENT_REQUIRED" | "SUBDOMAIN_MULTI_NOT_SUPPORTED" | "SCHEMA_SETTLE_TIMEOUT" | "ACTIVATION_FAILED" | "STORAGE_UNAVAILABLE" | "SITE_STAGE_FAILED" | "FUNCTION_BUILD_FAILED" | "CONTENT_UPLOAD_FAILED" | "INVALID_SPEC" | "OPERATION_NOT_FOUND" | "PLAN_NOT_FOUND" | "NOT_RESUMABLE" | "INVALID_STATE" | "RESUME_FAILED" | "INTERNAL_ERROR" | "PROJECT_NOT_FOUND" | (string & {});
|
|
50
|
+
export interface Run402DeployErrorFix {
|
|
51
|
+
action: string;
|
|
52
|
+
path?: string;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
export declare class Run402DeployError extends Run402Error {
|
|
56
|
+
readonly code: Run402DeployErrorCode;
|
|
57
|
+
readonly phase: string | null;
|
|
58
|
+
readonly resource: string | null;
|
|
59
|
+
readonly retryable: boolean;
|
|
60
|
+
readonly operationId: string | null;
|
|
61
|
+
readonly planId: string | null;
|
|
62
|
+
readonly fix: Run402DeployErrorFix | null;
|
|
63
|
+
readonly logs: string[] | null;
|
|
64
|
+
readonly rolledBack: boolean;
|
|
65
|
+
constructor(message: string, init: {
|
|
66
|
+
code: Run402DeployErrorCode;
|
|
67
|
+
phase?: string | null;
|
|
68
|
+
resource?: string | null;
|
|
69
|
+
retryable?: boolean;
|
|
70
|
+
operationId?: string | null;
|
|
71
|
+
planId?: string | null;
|
|
72
|
+
fix?: Run402DeployErrorFix | null;
|
|
73
|
+
logs?: string[] | null;
|
|
74
|
+
rolledBack?: boolean;
|
|
75
|
+
status?: number | null;
|
|
76
|
+
body?: unknown;
|
|
77
|
+
context: string;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
39
80
|
//# sourceMappingURL=errors.d.ts.map
|