run402 1.57.1 → 1.57.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,11 +97,13 @@ CI deploys can ship `site`, `functions`, and `database` changes. Keep secrets, d
97
97
 
98
98
  ```bash
99
99
  run402 blob put ./logo.png # → AssetRef with cdn_url, sri, etag
100
+ run402 blob put ./asset --key assets/logo --content-type image/svg+xml
100
101
  run402 blob get <key> --output /tmp/logo.png
101
102
  run402 blob diagnose <url> # exit 0 if fresh, 1 if stale
102
103
  ```
103
104
 
104
105
  The returned `cdn_url` is content-addressed (`pr-<public_id>.run402.com/_blob/<key>-<8hex>.<ext>`) — paste it straight into HTML. SRI is bundled in `sri`.
106
+ `blob put` infers MIME type from the destination key; use `--content-type <mime>` when the key has no useful extension or needs an explicit override.
105
107
 
106
108
  ### Functions
107
109
 
package/lib/blob.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  * run402 blob — direct-to-S3 storage CLI.
3
3
  *
4
4
  * Usage:
5
- * run402 blob put <file> [files...] [--project <id>] [--key <dest>] [--private] [--immutable] [--concurrency N] [--no-resume]
5
+ * run402 blob put <file> [files...] [--project <id>] [--key <dest>] [--content-type <mime>] [--private] [--immutable] [--concurrency N] [--no-resume]
6
6
  * run402 blob get <key> --output <file> [--project <id>]
7
7
  * run402 blob ls [--project <id>] [--prefix <p>] [--limit <n>]
8
8
  * run402 blob rm <key> [--project <id>]
@@ -52,6 +52,7 @@ Usage:
52
52
  Options:
53
53
  --project <id> Project ID (defaults to active project from 'run402 projects use')
54
54
  --key <dest> Destination key (put only; defaults to file basename)
55
+ --content-type <mime> MIME override for blob put (defaults to extension inference)
55
56
  --private Upload as private (not served by CDN; apikey required to read)
56
57
  --immutable Adds a content-hash suffix to the URL so overwrites produce distinct URLs.
57
58
  Requires computing SHA-256 over the file (CLI does this automatically).
@@ -84,6 +85,7 @@ Arguments:
84
85
  Options:
85
86
  --project <id> Project ID (defaults to active project from 'run402 projects use')
86
87
  --key <dest> Destination key; defaults to file basename. Use trailing '/' as prefix.
88
+ --content-type <mime> MIME override; defaults to inferring from the destination key extension
87
89
  --private Upload as private (not served by CDN; apikey required to read)
88
90
  --immutable Append content-hash suffix so overwrites produce distinct URLs
89
91
  --concurrency N Concurrent part PUTs for multipart uploads (default 4)
@@ -93,6 +95,7 @@ Options:
93
95
  Examples:
94
96
  run402 blob put ./artifact.tgz --project prj_abc123
95
97
  run402 blob put ./dist/**/*.png --project prj_abc123 --key assets/
98
+ run402 blob put ./asset --project prj_abc123 --key assets/logo --content-type image/svg+xml
96
99
  run402 blob put huge.bin --project prj_abc123 --immutable --concurrency 8
97
100
  `,
98
101
  get: `run402 blob get — Download a blob by key
@@ -202,10 +205,11 @@ function dieApiFailure(prefix, http, body) {
202
205
 
203
206
  function parseArgs(rawArgs) {
204
207
  const args = normalizeArgv(rawArgs);
205
- const valueFlags = ["--project", "--key", "--concurrency", "--prefix", "--limit", "--output", "-o", "--ttl"];
208
+ const valueFlags = ["--project", "--key", "--content-type", "--concurrency", "--prefix", "--limit", "--output", "-o", "--ttl"];
206
209
  assertKnownFlags(args, [
207
210
  "--project",
208
211
  "--key",
212
+ "--content-type",
209
213
  "--private",
210
214
  "--immutable",
211
215
  "--concurrency",
@@ -221,11 +225,12 @@ function parseArgs(rawArgs) {
221
225
  ], valueFlags);
222
226
  const out = { positional: [], project: null, key: null, private: false, immutable: false,
223
227
  concurrency: 4, resume: true, json: false, prefix: null, limit: null,
224
- output: null, ttl: null };
228
+ output: null, ttl: null, contentType: null };
225
229
  for (let i = 0; i < args.length; i++) {
226
230
  const a = args[i];
227
231
  if (a === "--project") out.project = args[++i];
228
232
  else if (a === "--key") out.key = args[++i];
233
+ else if (a === "--content-type") out.contentType = parseContentTypeFlag("--content-type", args[++i]);
229
234
  else if (a === "--private") out.private = true;
230
235
  else if (a === "--immutable") out.immutable = true;
231
236
  else if (a === "--concurrency") out.concurrency = parseIntegerFlag("--concurrency", args[++i], { min: 1 });
@@ -240,6 +245,26 @@ function parseArgs(rawArgs) {
240
245
  return out;
241
246
  }
242
247
 
248
+ function parseContentTypeFlag(name, value) {
249
+ if (value === undefined || value === null) {
250
+ fail({
251
+ code: "BAD_FLAG",
252
+ message: `${name} requires a MIME type value`,
253
+ details: { flag: name },
254
+ });
255
+ }
256
+ const raw = String(value).trim();
257
+ const base = raw.split(";", 1)[0].trim();
258
+ if (!/^[^\s/]+\/[^\s/]+$/.test(base)) {
259
+ fail({
260
+ code: "BAD_FLAG",
261
+ message: `${name} must be a non-empty type/subtype MIME value, got: ${String(value)}`,
262
+ details: { flag: name, value: String(value) },
263
+ });
264
+ }
265
+ return raw;
266
+ }
267
+
243
268
  async function sha256File(filePath) {
244
269
  const h = createHash("sha256");
245
270
  const stream = createReadStream(filePath);
@@ -311,7 +336,7 @@ async function putOne(project, filePath, opts) {
311
336
  const init = await apiFetch(`${API}/storage/v1/uploads`, "POST", project, {
312
337
  key: destKey,
313
338
  size_bytes: size,
314
- content_type: guessContentType(destKey),
339
+ content_type: opts.contentType ?? guessContentType(destKey),
315
340
  visibility: opts.private ? "private" : "public",
316
341
  immutable: opts.immutable,
317
342
  sha256,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.57.1",
3
+ "version": "1.57.2",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,62 +0,0 @@
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
@@ -1,25 +0,0 @@
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
@@ -1,62 +0,0 @@
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
@@ -1,25 +0,0 @@
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