run402 1.62.1 → 1.64.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 (45) hide show
  1. package/README.md +4 -0
  2. package/lib/allowance.mjs +20 -25
  3. package/lib/auth.mjs +7 -16
  4. package/lib/billing.mjs +12 -10
  5. package/lib/blob.mjs +30 -54
  6. package/lib/contracts.mjs +11 -19
  7. package/lib/deploy-v2.mjs +4 -1
  8. package/lib/init.mjs +12 -19
  9. package/lib/projects.mjs +37 -48
  10. package/lib/sdk-errors.mjs +3 -0
  11. package/lib/status.mjs +8 -15
  12. package/package.json +1 -1
  13. package/sdk/dist/errors.d.ts +6 -0
  14. package/sdk/dist/errors.d.ts.map +1 -1
  15. package/sdk/dist/errors.js +12 -0
  16. package/sdk/dist/errors.js.map +1 -1
  17. package/sdk/dist/kernel.d.ts +6 -0
  18. package/sdk/dist/kernel.d.ts.map +1 -1
  19. package/sdk/dist/kernel.js +5 -1
  20. package/sdk/dist/kernel.js.map +1 -1
  21. package/sdk/dist/namespaces/billing.d.ts +10 -5
  22. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/billing.js +23 -9
  24. package/sdk/dist/namespaces/billing.js.map +1 -1
  25. package/sdk/dist/namespaces/blobs.d.ts +16 -1
  26. package/sdk/dist/namespaces/blobs.d.ts.map +1 -1
  27. package/sdk/dist/namespaces/blobs.js +61 -25
  28. package/sdk/dist/namespaces/blobs.js.map +1 -1
  29. package/sdk/dist/namespaces/blobs.types.d.ts +41 -0
  30. package/sdk/dist/namespaces/blobs.types.d.ts.map +1 -1
  31. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  32. package/sdk/dist/namespaces/deploy.js +131 -22
  33. package/sdk/dist/namespaces/deploy.js.map +1 -1
  34. package/sdk/dist/namespaces/deploy.types.d.ts +25 -1
  35. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  36. package/sdk/dist/namespaces/projects.d.ts +5 -3
  37. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/projects.js +31 -5
  39. package/sdk/dist/namespaces/projects.js.map +1 -1
  40. package/sdk/dist/namespaces/projects.types.d.ts +16 -0
  41. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  42. package/sdk/dist/scoped.d.ts +9 -3
  43. package/sdk/dist/scoped.d.ts.map +1 -1
  44. package/sdk/dist/scoped.js +14 -2
  45. package/sdk/dist/scoped.js.map +1 -1
package/README.md CHANGED
@@ -125,6 +125,10 @@ import { db, adminDb, getUser, email, ai } from "@run402/functions";
125
125
 
126
126
  `db(req)` is the caller-context client (RLS applies); `adminDb()` bypasses RLS for platform-authored writes.
127
127
 
128
+ ### Same-origin web routes
129
+
130
+ `run402 deploy apply` accepts `routes.replace` as an array of route entries, not a path-keyed map. Use exact `/admin` plus final-wildcard `/admin/*` for a routed section root, and target a function deployed in the same release. Routed functions use Node 22 Fetch Request -> Response; `req.url` is the full public URL on managed subdomains, deployment hosts, and verified custom domains, so OAuth redirect URIs can be derived from `new URL(req.url).origin`. Direct `/functions/v1/:name` remains API-key protected. Runtime route failure codes to branch on: `ROUTE_MANIFEST_LOAD_FAILED` (manifest/propagation), `ROUTED_INVOKE_WORKER_SECRET_MISSING` (custom-domain Worker secret), `ROUTED_INVOKE_AUTH_FAILED` (internal invoke signature), `ROUTED_ROUTE_STALE` (selected route failed release revalidation), `ROUTE_METHOD_NOT_ALLOWED` (method mismatch), and `ROUTED_RESPONSE_TOO_LARGE` (body over 6 MiB).
131
+
128
132
  ### Secrets
129
133
 
130
134
  ```bash
package/lib/allowance.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { readAllowance, saveAllowance, ALLOWANCE_FILE, API } from "./config.mjs";
1
+ import { readAllowance, saveAllowance, ALLOWANCE_FILE } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
 
@@ -173,14 +173,11 @@ async function fund() {
173
173
  const client = createPublicClient({ chain: baseSepolia, transport: http() });
174
174
  const before = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => 0);
175
175
 
176
- const res = await fetch(`${API}/faucet/v1`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: w.address }) });
177
- const data = await res.json();
178
- if (!res.ok) {
179
- fail({
180
- code: data?.code || "FAUCET_FAILED",
181
- message: data?.message || "Faucet request failed",
182
- details: { http: res.status, ...data },
183
- });
176
+ let data;
177
+ try {
178
+ data = await getSdk().allowance.faucet(w.address);
179
+ } catch (err) {
180
+ reportSdkError(err);
184
181
  }
185
182
 
186
183
  const MAX_WAIT = 30;
@@ -228,11 +225,9 @@ async function balance() {
228
225
  readUsdcBalance(mainnetClient, USDC_MAINNET, w.address).catch(() => null),
229
226
  readUsdcBalance(sepoliaClient, USDC_SEPOLIA, w.address).catch(() => null),
230
227
  readUsdcBalance(tempoClient, PATH_USD, w.address).catch(() => null),
231
- fetch(`${API}/billing/v1/accounts/${w.address.toLowerCase()}`),
228
+ getSdk().billing.checkBalance(w.address).catch(() => null),
232
229
  ]);
233
230
 
234
- const billing = billingRes.ok ? await billingRes.json() : null;
235
-
236
231
  console.log(JSON.stringify({
237
232
  address: w.address,
238
233
  rail: w.rail || "x402",
@@ -241,7 +236,7 @@ async function balance() {
241
236
  "base-sepolia_usd_micros": sepoliaUsdc,
242
237
  "tempo-moderato_pathusd_micros": tempoPathUsd,
243
238
  },
244
- run402: billing ? { balance_usd_micros: billing.available_usd_micros } : "no billing account",
239
+ run402: billingRes ? { balance_usd_micros: billingRes.available_usd_micros } : "no billing account",
245
240
  }, null, 2));
246
241
  }
247
242
 
@@ -274,14 +269,12 @@ async function checkout(args) {
274
269
  hint: "e.g. --amount 5000000 for $5",
275
270
  });
276
271
  }
277
- const res = await fetch(`${API}/billing/v1/checkouts`, {
278
- method: "POST",
279
- headers: { "Content-Type": "application/json" },
280
- body: JSON.stringify({ wallet: w.address.toLowerCase(), amount_usd_micros: amount }),
281
- });
282
- const data = await res.json();
283
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
284
- console.log(JSON.stringify(data, null, 2));
272
+ try {
273
+ const data = await getSdk().billing.createCheckout(w.address, amount);
274
+ console.log(JSON.stringify(data, null, 2));
275
+ } catch (err) {
276
+ reportSdkError(err);
277
+ }
285
278
  }
286
279
 
287
280
  async function history(args) {
@@ -297,10 +290,12 @@ async function history(args) {
297
290
  for (let i = 0; i < args.length; i++) {
298
291
  if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i], 10);
299
292
  }
300
- const res = await fetch(`${API}/billing/v1/accounts/${w.address.toLowerCase()}/history?limit=${limit}`);
301
- const data = await res.json();
302
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
303
- console.log(JSON.stringify(data, null, 2));
293
+ try {
294
+ const data = await getSdk().billing.history(w.address, limit);
295
+ console.log(JSON.stringify(data, null, 2));
296
+ } catch (err) {
297
+ reportSdkError(err);
298
+ }
304
299
  }
305
300
 
306
301
  export async function run(sub, args) {
package/lib/auth.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { findProject, resolveProjectId, API } from "./config.mjs";
1
+ import { resolveProjectId } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
4
 
@@ -546,22 +546,13 @@ async function deletePasskey(args) {
546
546
  }
547
547
 
548
548
  async function providers(args) {
549
- // `providers` isn't in the pilot SDK surface — keep the direct fetch.
550
549
  const projectId = resolveProjectId(parseFlag(args, "--project"));
551
- const p = findProject(projectId);
552
-
553
- const res = await fetch(`${API}/auth/v1/providers`, {
554
- headers: {
555
- "apikey": p.anon_key,
556
- "Authorization": `Bearer ${p.anon_key}`,
557
- },
558
- });
559
- const data = await res.json();
560
- if (!res.ok) {
561
- console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
562
- process.exit(1);
563
- }
564
- console.log(JSON.stringify(data, null, 2));
550
+ try {
551
+ const data = await getSdk().auth.providers(projectId);
552
+ console.log(JSON.stringify(data, null, 2));
553
+ } catch (err) {
554
+ reportSdkError(err);
555
+ }
565
556
  }
566
557
 
567
558
  export async function run(sub, args) {
package/lib/billing.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { API } from "./config.mjs";
2
1
  import { getSdk } from "./sdk.mjs";
3
2
  import { reportSdkError, fail } from "./sdk-errors.mjs";
4
3
 
@@ -224,7 +223,6 @@ async function autoRecharge(args) {
224
223
  }
225
224
 
226
225
  async function balance(args) {
227
- // Accepts email OR wallet — SDK only models wallet, so keep direct fetch.
228
226
  const id = args[0];
229
227
  if (!id) {
230
228
  fail({
@@ -233,10 +231,12 @@ async function balance(args) {
233
231
  hint: "run402 billing balance <email-or-wallet>",
234
232
  });
235
233
  }
236
- const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(id)}`);
237
- const data = await res.json();
238
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
239
- console.log(JSON.stringify(data, null, 2));
234
+ try {
235
+ const data = await getSdk().billing.getAccount(id);
236
+ console.log(JSON.stringify(data, null, 2));
237
+ } catch (err) {
238
+ reportSdkError(err);
239
+ }
240
240
  }
241
241
 
242
242
  async function history(args) {
@@ -249,10 +249,12 @@ async function history(args) {
249
249
  });
250
250
  }
251
251
  const limit = parseFlag(args, "--limit") || "50";
252
- const res = await fetch(`${API}/billing/v1/accounts/${encodeURIComponent(id)}/history?limit=${encodeURIComponent(limit)}`);
253
- const data = await res.json();
254
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
255
- console.log(JSON.stringify(data, null, 2));
252
+ try {
253
+ const data = await getSdk().billing.getHistory(id, Number(limit));
254
+ console.log(JSON.stringify(data, null, 2));
255
+ } catch (err) {
256
+ reportSdkError(err);
257
+ }
256
258
  }
257
259
 
258
260
  export async function run(sub, args) {
package/lib/blob.mjs CHANGED
@@ -34,7 +34,7 @@ import { basename, dirname, join, resolve as resolvePath } from "node:path";
34
34
  import { homedir } from "node:os";
35
35
  import { pipeline } from "node:stream/promises";
36
36
 
37
- import { resolveProject, resolveProjectId, API } from "./config.mjs";
37
+ import { resolveProjectId } from "./config.mjs";
38
38
  import { getSdk } from "./sdk.mjs";
39
39
  import { reportSdkError, fail } from "./sdk-errors.mjs";
40
40
  import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag } from "./argparse.mjs";
@@ -190,19 +190,6 @@ function die(msg, exit_code = 1) {
190
190
  fail({ code: "BAD_USAGE", message: msg, exit_code });
191
191
  }
192
192
 
193
- function dieApiFailure(prefix, http, body) {
194
- if (body && typeof body === "object" && !Array.isArray(body)) {
195
- const envelope = { status: "error", http, ...body };
196
- if (!envelope.message && envelope.error) envelope.message = envelope.error;
197
- console.error(JSON.stringify(envelope));
198
- process.exit(1);
199
- }
200
- fail({
201
- message: `${prefix}: HTTP ${http}${typeof body === "string" && body ? `: ${body.slice(0, 500)}` : ""}`,
202
- details: { http },
203
- });
204
- }
205
-
206
193
  function parseArgs(rawArgs) {
207
194
  const args = normalizeArgv(rawArgs);
208
195
  const valueFlags = ["--project", "--key", "--content-type", "--concurrency", "--prefix", "--limit", "--output", "-o", "--ttl"];
@@ -305,7 +292,7 @@ function findResumableStateForFile(projectId, localPath, key) {
305
292
  // put
306
293
  // ---------------------------------------------------------------------------
307
294
 
308
- async function putOne(project, filePath, opts) {
295
+ async function putOne(projectId, filePath, opts) {
309
296
  const stat = statSync(filePath);
310
297
  const size = stat.size;
311
298
  const destKey = computeDestKey(filePath, opts.key);
@@ -317,15 +304,21 @@ async function putOne(project, filePath, opts) {
317
304
 
318
305
  // Attempt to resume
319
306
  let state = opts.resume
320
- ? findResumableStateForFile(project.id, absLocal, destKey)
307
+ ? findResumableStateForFile(projectId, absLocal, destKey)
321
308
  : null;
322
309
  let initRes;
323
310
  if (state) {
324
311
  // Re-poll the session; if it's still active, resume. Otherwise start fresh.
325
- const poll = await apiFetch(`${API}/storage/v1/uploads/${state.upload_id}`, "GET", project, null);
326
- if (poll.status === 200 && poll.body.status === "active") {
312
+ const poll = await getSdk().blobs.getUploadSession(projectId, state.upload_id);
313
+ if (poll.status === "active") {
327
314
  log(opts, { event: "resume", upload_id: state.upload_id, key: destKey });
328
- initRes = { upload_id: state.upload_id, mode: state.mode, parts: state.parts, part_count: state.part_count, part_size_bytes: state.part_size_bytes };
315
+ initRes = {
316
+ upload_id: state.upload_id,
317
+ mode: poll.mode ?? state.mode,
318
+ parts: poll.parts ?? state.parts,
319
+ part_count: poll.part_count ?? state.part_count,
320
+ part_size_bytes: poll.part_size_bytes ?? state.part_size_bytes,
321
+ };
329
322
  } else {
330
323
  removeState(state.upload_id);
331
324
  state = null;
@@ -333,7 +326,7 @@ async function putOne(project, filePath, opts) {
333
326
  }
334
327
 
335
328
  if (!state) {
336
- const init = await apiFetch(`${API}/storage/v1/uploads`, "POST", project, {
329
+ initRes = await getSdk().blobs.initUploadSession(projectId, {
337
330
  key: destKey,
338
331
  size_bytes: size,
339
332
  content_type: opts.contentType ?? guessContentType(destKey),
@@ -341,11 +334,9 @@ async function putOne(project, filePath, opts) {
341
334
  immutable: opts.immutable,
342
335
  sha256,
343
336
  });
344
- if (init.status !== 201) dieApiFailure("Init failed", init.status, init.body);
345
- initRes = init.body;
346
- saveState({
337
+ state = {
347
338
  upload_id: initRes.upload_id,
348
- project_id: project.id,
339
+ project_id: projectId,
349
340
  local_path: absLocal,
350
341
  key: destKey,
351
342
  mode: initRes.mode,
@@ -355,8 +346,8 @@ async function putOne(project, filePath, opts) {
355
346
  parts_done: {},
356
347
  sha256,
357
348
  started_at: new Date().toISOString(),
358
- });
359
- state = loadState(initRes.upload_id);
349
+ };
350
+ if (opts.resume) saveState(state);
360
351
  }
361
352
 
362
353
  // Upload parts with concurrency limit. For single-PUT mode part_count=1 and
@@ -378,7 +369,7 @@ async function putOne(project, filePath, opts) {
378
369
  const { etag } = await putPart(filePath, part);
379
370
  etags[part.part_number - 1] = { etag };
380
371
  state.parts_done[String(part.part_number)] = { etag };
381
- saveState(state);
372
+ if (opts.resume) saveState(state);
382
373
  log(opts, { event: "part", upload_id: state.upload_id, part_number: part.part_number, etag });
383
374
  });
384
375
 
@@ -386,12 +377,13 @@ async function putOne(project, filePath, opts) {
386
377
  const body = initRes.mode === "multipart"
387
378
  ? { parts: etags.map((e, i) => ({ part_number: i + 1, etag: e.etag })) }
388
379
  : {};
389
- const complete = await apiFetch(`${API}/storage/v1/uploads/${state.upload_id}/complete`, "POST", project, body);
390
- if (complete.status !== 200) dieApiFailure("Complete failed", complete.status, complete.body);
380
+ const result = await getSdk().blobs.completeUploadSession(projectId, state.upload_id, body, {
381
+ contentType: opts.contentType ?? guessContentType(destKey),
382
+ });
391
383
 
392
384
  removeState(state.upload_id);
393
- log(opts, { event: "done", ...complete.body });
394
- return complete.body;
385
+ log(opts, { event: "done", ...result });
386
+ return result;
395
387
  }
396
388
 
397
389
  function computeDestKey(filePath, keyOpt) {
@@ -443,7 +435,7 @@ function isSettled(p) {
443
435
  async function put(projectId, argv) {
444
436
  const opts = parseArgs(argv);
445
437
  opts.project = opts.project || projectId;
446
- const project = resolveProject(opts.project);
438
+ const resolvedId = resolveProjectId(opts.project);
447
439
 
448
440
  if (opts.positional.length === 0) die("At least one file path is required");
449
441
  if (opts.immutable && opts.positional.length > 1 && opts.key && !opts.key.endsWith("/")) {
@@ -453,8 +445,12 @@ async function put(projectId, argv) {
453
445
  const results = [];
454
446
  for (const filePath of opts.positional) {
455
447
  if (!existsSync(filePath)) die(`File not found: ${filePath}`);
456
- const r = await putOne(project, filePath, opts);
457
- results.push({ file: filePath, ...r });
448
+ try {
449
+ const r = await putOne(resolvedId, filePath, opts);
450
+ results.push({ file: filePath, ...r });
451
+ } catch (err) {
452
+ reportSdkError(err);
453
+ }
458
454
  }
459
455
  if (!opts.json) console.log(JSON.stringify(results, null, 2));
460
456
  }
@@ -576,11 +572,6 @@ async function sign(projectId, argv) {
576
572
  // Shared helpers
577
573
  // ---------------------------------------------------------------------------
578
574
 
579
- function encodeKey(key) {
580
- // Encode each path segment; preserve `/` as separator.
581
- return key.split("/").map(encodeURIComponent).join("/");
582
- }
583
-
584
575
  function guessContentType(key) {
585
576
  const ext = key.slice(key.lastIndexOf(".") + 1).toLowerCase();
586
577
  const map = {
@@ -594,21 +585,6 @@ function guessContentType(key) {
594
585
  return map[ext] ?? "application/octet-stream";
595
586
  }
596
587
 
597
- async function apiFetch(url, method, project, body) {
598
- const res = await fetch(url, {
599
- method,
600
- headers: {
601
- "content-type": "application/json",
602
- apikey: project.anon_key,
603
- Authorization: `Bearer ${project.anon_key}`,
604
- },
605
- body: body === null ? undefined : JSON.stringify(body ?? {}),
606
- });
607
- const txt = await res.text();
608
- let parsed; try { parsed = txt ? JSON.parse(txt) : null; } catch { parsed = txt; }
609
- return { status: res.status, body: parsed };
610
- }
611
-
612
588
  function log(opts, event) {
613
589
  if (opts.json) console.log(JSON.stringify(event));
614
590
  }
package/lib/contracts.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { findProject, API } from "./config.mjs";
2
1
  import { getSdk } from "./sdk.mjs";
3
2
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
4
3
 
@@ -138,7 +137,6 @@ function hasFlag(args, flag) {
138
137
  }
139
138
 
140
139
  async function provisionWallet(projectId, args) {
141
- const p = findProject(projectId);
142
140
  const chain = parseFlag(args, "--chain");
143
141
  if (!chain) {
144
142
  fail({
@@ -147,25 +145,19 @@ async function provisionWallet(projectId, args) {
147
145
  });
148
146
  }
149
147
  const recovery = parseFlag(args, "--recovery");
150
- // Soft default of one wallet — confirm if project already has one. This
151
- // pre-check stays on raw fetch because it's a discovery best-effort, not
152
- // a primary API call.
148
+ // Soft default of one wallet — confirm if project already has one.
149
+ let activeWallets = null;
153
150
  try {
154
- const listRes = await fetch(`${API}/contracts/v1/wallets`, {
155
- headers: { Authorization: `Bearer ${p.service_key}` },
156
- });
157
- if (listRes.ok) {
158
- const list = await listRes.json();
159
- const active = (list.wallets || []).filter((w) => w.status === "active");
160
- if (active.length >= 1 && !hasFlag(args, "--yes")) {
161
- fail({
162
- code: "CONFIRMATION_REQUIRED",
163
- message: `This project already has ${active.length} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`,
164
- details: { active_wallets: active.length },
165
- });
166
- }
167
- }
151
+ const list = await getSdk().contracts.listWallets(projectId);
152
+ activeWallets = (list.wallets || []).filter((w) => w.status === "active").length;
168
153
  } catch { /* best-effort */ }
154
+ if (activeWallets !== null && activeWallets >= 1 && !hasFlag(args, "--yes")) {
155
+ fail({
156
+ code: "CONFIRMATION_REQUIRED",
157
+ message: `This project already has ${activeWallets} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`,
158
+ details: { active_wallets: activeWallets },
159
+ });
160
+ }
169
161
 
170
162
  try {
171
163
  const data = await getSdk().contracts.provisionWallet(projectId, {
package/lib/deploy-v2.mjs CHANGED
@@ -62,7 +62,7 @@ Complete static site + function + route manifest:
62
62
  "replace": {
63
63
  "api": {
64
64
  "runtime": "node22",
65
- "source": { "data": "import { routedHttp } from '@run402/functions'; export default async (event) => routedHttp.json({ ok: true, path: event.path });" }
65
+ "source": { "data": "export default async function handler(req) { const url = new URL(req.url); return Response.json({ ok: true, path: url.pathname }); }" }
66
66
  }
67
67
  }
68
68
  },
@@ -93,7 +93,10 @@ Patch examples (only the listed file changes):
93
93
  Routes:
94
94
  Omit routes or pass "routes": null to carry forward base routes.
95
95
  Use "routes": { "replace": [] } to clear dynamic routes.
96
+ Route entries are array-based, not path-keyed maps. Use exact /admin plus final-wildcard /admin/* for a routed section root.
97
+ Routed functions use Node 22 Fetch Request -> Response. req.url is the full public URL on managed domains, deployment hosts, and verified custom domains.
96
98
  Routes activate atomically with the release. Direct /functions/v1/:name remains API-key protected.
99
+ Runtime route failure codes: ROUTE_MANIFEST_LOAD_FAILED, ROUTED_INVOKE_WORKER_SECRET_MISSING, ROUTED_INVOKE_AUTH_FAILED, ROUTED_ROUTE_STALE, ROUTE_METHOD_NOT_ALLOWED, ROUTED_RESPONSE_TOO_LARGE.
97
100
  `;
98
101
 
99
102
  const RESUME_HELP = `run402 deploy resume — Resume a stuck deploy operation
package/lib/init.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { readAllowance, saveAllowance, loadKeyStore, CONFIG_DIR, ALLOWANCE_FILE, API } from "./config.mjs";
2
- import { getAllowanceAuthHeaders } from "../core-dist/allowance-auth.js";
1
+ import { readAllowance, saveAllowance, loadKeyStore, CONFIG_DIR } from "./config.mjs";
2
+ import { getSdk } from "./sdk.mjs";
3
3
  import { mkdirSync } from "fs";
4
4
 
5
5
  const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
@@ -39,6 +39,11 @@ Run this once to get started, or again to check your setup.
39
39
 
40
40
  function short(addr) { return addr.slice(0, 6) + "..." + addr.slice(-4); }
41
41
 
42
+ function errorMessage(err) {
43
+ if (err?.body && typeof err.body === "object") return err.body.message || err.body.error || err.message;
44
+ return err?.message || String(err);
45
+ }
46
+
42
47
  export async function run(args = []) {
43
48
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
44
49
  const jsonMode = args.includes("--json");
@@ -178,12 +183,8 @@ export async function run(args = []) {
178
183
 
179
184
  if (balance === 0) {
180
185
  line("Balance", "0 USDC — requesting faucet...");
181
- const res = await fetch(`${API}/faucet/v1`, {
182
- method: "POST",
183
- headers: { "Content-Type": "application/json" },
184
- body: JSON.stringify({ address: allowance.address }),
185
- });
186
- if (res.ok) {
186
+ try {
187
+ await getSdk().allowance.faucet(allowance.address);
187
188
  // Poll for up to 30s
188
189
  for (let i = 0; i < 30; i++) {
189
190
  await new Promise(r => setTimeout(r, 1000));
@@ -200,10 +201,8 @@ export async function run(args = []) {
200
201
  } else {
201
202
  line("Balance", "faucet sent — not yet confirmed on-chain");
202
203
  }
203
- } else {
204
- const data = await res.json().catch(() => ({}));
205
- const msg = data.error || data.message || `HTTP ${res.status}`;
206
- line("Balance", `faucet failed: ${msg}`);
204
+ } catch (err) {
205
+ line("Balance", `faucet failed: ${errorMessage(err)}`);
207
206
  }
208
207
  } else {
209
208
  line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
@@ -222,13 +221,7 @@ export async function run(args = []) {
222
221
  const store = loadKeyStore();
223
222
  let tierInfo = null;
224
223
  try {
225
- const authHeaders = getAllowanceAuthHeaders("/tiers/v1/status");
226
- if (authHeaders) {
227
- const res = await fetch(`${API}/tiers/v1/status`, {
228
- headers: { ...authHeaders },
229
- });
230
- if (res.ok) tierInfo = await res.json();
231
- }
224
+ tierInfo = await getSdk().tier.status();
232
225
  } catch {}
233
226
 
234
227
  if (tierInfo && tierInfo.tier && tierInfo.active) {
package/lib/projects.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { findProject, loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
2
+ import { loadKeyStore, API, allowanceAuthHeaders, resolveProjectId, getActiveProjectId } from "./config.mjs";
3
3
  import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail, parseFlagJson } from "./sdk-errors.mjs";
5
5
  import { assertKnownFlags, failBadProjectId, flagValue, hasHelp, normalizeArgv, positionalArgs, resolvePositionalProject, validateRegularFile } from "./argparse.mjs";
@@ -212,7 +212,6 @@ async function applyExpose(projectId, args = []) {
212
212
  else if (!inline && !args[i].startsWith("--")) { inline = args[i]; }
213
213
  }
214
214
  if (file) validateRegularFile(file, "--file");
215
- const p = findProject(projectId);
216
215
  const raw = file ? readFileSync(file, "utf-8") : inline;
217
216
  if (!raw) {
218
217
  fail({
@@ -230,24 +229,21 @@ async function applyExpose(projectId, args = []) {
230
229
  details: { parse_error: err.message },
231
230
  });
232
231
  }
233
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/expose`, {
234
- method: "POST",
235
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
236
- body: JSON.stringify(manifest),
237
- });
238
- const data = await res.json();
239
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
240
- console.log(JSON.stringify(data, null, 2));
232
+ try {
233
+ const data = await getSdk().projects.applyExpose(projectId, manifest);
234
+ console.log(JSON.stringify(data, null, 2));
235
+ } catch (err) {
236
+ reportSdkError(err);
237
+ }
241
238
  }
242
239
 
243
240
  async function getExpose(projectId) {
244
- const p = findProject(projectId);
245
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/expose`, {
246
- headers: { "Authorization": `Bearer ${p.service_key}` },
247
- });
248
- const data = await res.json();
249
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
250
- console.log(JSON.stringify(data, null, 2));
241
+ try {
242
+ const data = await getSdk().projects.getExpose(projectId);
243
+ console.log(JSON.stringify(data, null, 2));
244
+ } catch (err) {
245
+ reportSdkError(err);
246
+ }
251
247
  }
252
248
 
253
249
  async function list() {
@@ -292,7 +288,6 @@ async function sqlCmd(projectId, args = []) {
292
288
  else if (!query && !args[i].startsWith("--")) { query = args[i]; }
293
289
  }
294
290
  if (file) validateRegularFile(file, "--file");
295
- const p = findProject(projectId);
296
291
  const sql = file ? readFileSync(file, "utf-8") : query;
297
292
  if (!sql) {
298
293
  fail({
@@ -311,13 +306,12 @@ async function sqlCmd(projectId, args = []) {
311
306
  });
312
307
  }
313
308
  }
314
- const useParams = params && params.length > 0;
315
- const headers = { "Authorization": `Bearer ${p.service_key}`, "Content-Type": useParams ? "application/json" : "text/plain" };
316
- const body = useParams ? JSON.stringify({ sql, params }) : sql;
317
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers, body });
318
- const data = await res.json();
319
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
320
- console.log(JSON.stringify(data, null, 2));
309
+ try {
310
+ const data = await getSdk().projects.sql(projectId, sql, params);
311
+ console.log(JSON.stringify(data, null, 2));
312
+ } catch (err) {
313
+ reportSdkError(err);
314
+ }
321
315
  }
322
316
 
323
317
  async function rest(projectId, table, queryParams) {
@@ -328,11 +322,12 @@ async function rest(projectId, table, queryParams) {
328
322
  hint: "Run 'run402 projects schema <id>' to list tables.",
329
323
  });
330
324
  }
331
- const p = findProject(projectId);
332
- const res = await fetch(`${API}/rest/v1/${table}${queryParams ? '?' + queryParams : ''}`, { headers: { "apikey": p.anon_key } });
333
- const data = await res.json();
334
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
335
- console.log(JSON.stringify(data, null, 2));
325
+ try {
326
+ const data = await getSdk().projects.rest(projectId, table, queryParams);
327
+ console.log(JSON.stringify(data, null, 2));
328
+ } catch (err) {
329
+ reportSdkError(err);
330
+ }
336
331
  }
337
332
 
338
333
  async function usage(projectId) {
@@ -420,15 +415,12 @@ async function promoteUser(projectId, email) {
420
415
  hint: "run402 projects promote-user <project_id> <email>",
421
416
  });
422
417
  }
423
- const p = findProject(projectId);
424
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/promote-user`, {
425
- method: "POST",
426
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
427
- body: JSON.stringify({ email }),
428
- });
429
- const data = await res.json();
430
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
431
- console.log(JSON.stringify(data, null, 2));
418
+ try {
419
+ await getSdk().projects.promoteUser(projectId, email);
420
+ console.log(JSON.stringify({ status: "ok", email }));
421
+ } catch (err) {
422
+ reportSdkError(err);
423
+ }
432
424
  }
433
425
 
434
426
  async function demoteUser(projectId, email) {
@@ -439,15 +431,12 @@ async function demoteUser(projectId, email) {
439
431
  hint: "run402 projects demote-user <project_id> <email>",
440
432
  });
441
433
  }
442
- const p = findProject(projectId);
443
- const res = await fetch(`${API}/projects/v1/admin/${projectId}/demote-user`, {
444
- method: "POST",
445
- headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
446
- body: JSON.stringify({ email }),
447
- });
448
- const data = await res.json();
449
- if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
450
- console.log(JSON.stringify(data, null, 2));
434
+ try {
435
+ await getSdk().projects.demoteUser(projectId, email);
436
+ console.log(JSON.stringify({ status: "ok", email }));
437
+ } catch (err) {
438
+ reportSdkError(err);
439
+ }
451
440
  }
452
441
 
453
442
  async function deleteProject(projectId, args = []) {
@@ -133,6 +133,9 @@ function mergeStructuredErrorFields(payload, err) {
133
133
  setIfAbsent(payload, "fix", err.fix);
134
134
  setIfAbsent(payload, "logs", err.logs);
135
135
  setIfAbsent(payload, "rolled_back", err.rolledBack);
136
+ setIfAbsent(payload, "attempts", err.attempts);
137
+ setIfAbsent(payload, "max_retries", err.maxRetries);
138
+ setIfAbsent(payload, "last_retry_code", err.lastRetryCode);
136
139
  }
137
140
 
138
141
  function setIfAbsent(payload, key, value) {