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.
Files changed (52) hide show
  1. package/core-dist/config.js +10 -0
  2. package/core-dist/wallet-auth.js +62 -0
  3. package/core-dist/wallet.js +25 -0
  4. package/lib/deploy-v2.mjs +359 -0
  5. package/lib/deploy.mjs +14 -0
  6. package/lib/functions.mjs +16 -1
  7. package/package.json +1 -1
  8. package/sdk/core-dist/config.js +10 -0
  9. package/sdk/core-dist/wallet-auth.js +62 -0
  10. package/sdk/core-dist/wallet.js +25 -0
  11. package/sdk/dist/errors.d.ts +41 -0
  12. package/sdk/dist/errors.d.ts.map +1 -1
  13. package/sdk/dist/errors.js +23 -0
  14. package/sdk/dist/errors.js.map +1 -1
  15. package/sdk/dist/index.d.ts +24 -1
  16. package/sdk/dist/index.d.ts.map +1 -1
  17. package/sdk/dist/index.js +24 -1
  18. package/sdk/dist/index.js.map +1 -1
  19. package/sdk/dist/namespaces/apps.d.ts +14 -0
  20. package/sdk/dist/namespaces/apps.d.ts.map +1 -1
  21. package/sdk/dist/namespaces/apps.js +162 -19
  22. package/sdk/dist/namespaces/apps.js.map +1 -1
  23. package/sdk/dist/namespaces/deploy.d.ts +116 -0
  24. package/sdk/dist/namespaces/deploy.d.ts.map +1 -0
  25. package/sdk/dist/namespaces/deploy.js +1127 -0
  26. package/sdk/dist/namespaces/deploy.js.map +1 -0
  27. package/sdk/dist/namespaces/deploy.types.d.ts +430 -0
  28. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -0
  29. package/sdk/dist/namespaces/deploy.types.js +11 -0
  30. package/sdk/dist/namespaces/deploy.types.js.map +1 -0
  31. package/sdk/dist/namespaces/functions.d.ts +11 -3
  32. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  33. package/sdk/dist/namespaces/functions.js +11 -3
  34. package/sdk/dist/namespaces/functions.js.map +1 -1
  35. package/sdk/dist/namespaces/functions.types.d.ts +21 -4
  36. package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
  37. package/sdk/dist/node/canonicalize.d.ts +12 -5
  38. package/sdk/dist/node/canonicalize.d.ts.map +1 -1
  39. package/sdk/dist/node/canonicalize.js +12 -5
  40. package/sdk/dist/node/canonicalize.js.map +1 -1
  41. package/sdk/dist/node/files.d.ts +38 -0
  42. package/sdk/dist/node/files.d.ts.map +1 -0
  43. package/sdk/dist/node/files.js +88 -0
  44. package/sdk/dist/node/files.js.map +1 -0
  45. package/sdk/dist/node/index.d.ts +5 -3
  46. package/sdk/dist/node/index.d.ts.map +1 -1
  47. package/sdk/dist/node/index.js +2 -1
  48. package/sdk/dist/node/index.js.map +1 -1
  49. package/sdk/dist/node/sites-node.d.ts +34 -107
  50. package/sdk/dist/node/sites-node.d.ts.map +1 -1
  51. package/sdk/dist/node/sites-node.js +84 -353
  52. package/sdk/dist/node/sites-node.js.map +1 -1
@@ -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 <pkg,...> Comma-separated npm deps to bundle
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
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": {
@@ -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
@@ -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