run402 1.63.0 → 1.65.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 (42) hide show
  1. package/README.md +6 -1
  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/functions.mjs +65 -17
  9. package/lib/init.mjs +12 -19
  10. package/lib/projects.mjs +157 -48
  11. package/lib/status.mjs +8 -15
  12. package/package.json +1 -1
  13. package/sdk/dist/kernel.d.ts +6 -0
  14. package/sdk/dist/kernel.d.ts.map +1 -1
  15. package/sdk/dist/kernel.js +5 -1
  16. package/sdk/dist/kernel.js.map +1 -1
  17. package/sdk/dist/namespaces/billing.d.ts +10 -5
  18. package/sdk/dist/namespaces/billing.d.ts.map +1 -1
  19. package/sdk/dist/namespaces/billing.js +23 -9
  20. package/sdk/dist/namespaces/billing.js.map +1 -1
  21. package/sdk/dist/namespaces/blobs.d.ts +16 -1
  22. package/sdk/dist/namespaces/blobs.d.ts.map +1 -1
  23. package/sdk/dist/namespaces/blobs.js +61 -25
  24. package/sdk/dist/namespaces/blobs.js.map +1 -1
  25. package/sdk/dist/namespaces/blobs.types.d.ts +41 -0
  26. package/sdk/dist/namespaces/blobs.types.d.ts.map +1 -1
  27. package/sdk/dist/namespaces/functions.d.ts +2 -1
  28. package/sdk/dist/namespaces/functions.d.ts.map +1 -1
  29. package/sdk/dist/namespaces/functions.js +19 -7
  30. package/sdk/dist/namespaces/functions.js.map +1 -1
  31. package/sdk/dist/namespaces/functions.types.d.ts +12 -2
  32. package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
  33. package/sdk/dist/namespaces/projects.d.ts +14 -3
  34. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  35. package/sdk/dist/namespaces/projects.js +95 -5
  36. package/sdk/dist/namespaces/projects.js.map +1 -1
  37. package/sdk/dist/namespaces/projects.types.d.ts +39 -0
  38. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  39. package/sdk/dist/scoped.d.ts +10 -3
  40. package/sdk/dist/scoped.d.ts.map +1 -1
  41. package/sdk/dist/scoped.js +20 -2
  42. package/sdk/dist/scoped.js.map +1 -1
package/README.md CHANGED
@@ -47,6 +47,7 @@ run402 allowance export # print address (for funding)
47
47
 
48
48
  ```bash
49
49
  run402 projects sql <id> "CREATE TABLE items (id serial PRIMARY KEY, …)"
50
+ run402 projects validate-expose <id> --file manifest.json # check auth manifest, no mutation
50
51
  run402 projects apply-expose <id> --file manifest.json # declare what's reachable
51
52
  run402 projects rest <id> items "select=*&order=id.desc&limit=10"
52
53
  run402 projects schema <id> # introspect tables + RLS
@@ -113,7 +114,7 @@ run402 functions deploy <id> my-fn --file fn.ts \
113
114
  --timeout 30 --memory 256 \
114
115
  --schedule "*/15 * * * *" \
115
116
  --deps "stripe,zod@^3"
116
- run402 functions logs <id> my-fn --tail 100 --follow
117
+ run402 functions logs <id> my-fn --tail 100 --request-id req_abc123 --follow
117
118
  run402 functions invoke <id> my-fn --body '{"hello":"world"}'
118
119
  ```
119
120
 
@@ -125,6 +126,10 @@ import { db, adminDb, getUser, email, ai } from "@run402/functions";
125
126
 
126
127
  `db(req)` is the caller-context client (RLS applies); `adminDb()` bypasses RLS for platform-authored writes.
127
128
 
129
+ ### Same-origin web routes
130
+
131
+ `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).
132
+
128
133
  ### Secrets
129
134
 
130
135
  ```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/functions.mjs CHANGED
@@ -4,6 +4,8 @@ import { getSdk } from "./sdk.mjs";
4
4
  import { reportSdkError, fail } from "./sdk-errors.mjs";
5
5
  import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, validateRegularFile } from "./argparse.mjs";
6
6
 
7
+ const FUNCTION_LOG_REQUEST_ID_RE = /^req_[A-Za-z0-9_-]{4,128}$/;
8
+
7
9
  const HELP = `run402 functions — Manage serverless functions
8
10
 
9
11
  Usage:
@@ -14,7 +16,7 @@ Subcommands:
14
16
  Deploy a function to a project
15
17
  invoke <id> <name> [--method <M>] [--body <json>]
16
18
  Invoke a deployed function
17
- logs <id> <name> [--tail <n>] [--since <ts>] [--follow]
19
+ logs <id> <name> [--tail <n>] [--since <ts>] [--request-id <req_...>] [--follow]
18
20
  Get function logs
19
21
  update <id> <name> [--schedule <cron>] [--schedule-remove] [--timeout <s>] [--memory <mb>]
20
22
  Update function schedule or config without re-deploying
@@ -28,6 +30,7 @@ Examples:
28
30
  run402 functions invoke prj_abc123 stripe-webhook --body '{"event":"test"}'
29
31
  run402 functions logs prj_abc123 stripe-webhook --tail 100
30
32
  run402 functions logs prj_abc123 stripe-webhook --since 2026-03-29T14:00:00Z
33
+ run402 functions logs prj_abc123 stripe-webhook --request-id req_abc123
31
34
  run402 functions logs prj_abc123 stripe-webhook --follow
32
35
  run402 functions update prj_abc123 send-reminders --schedule '0 */4 * * *'
33
36
  run402 functions update prj_abc123 send-reminders --schedule-remove
@@ -110,13 +113,15 @@ Arguments:
110
113
  <name> Function name
111
114
 
112
115
  Options:
113
- --tail <n> Number of most-recent entries (default 50)
116
+ --tail <n> Number of most-recent entries (default 50, max 1000)
114
117
  --since <ts> ISO timestamp or epoch ms; only entries after this
118
+ --request-id <id> Only entries correlated to this req_... request id
115
119
  --follow Poll every 3s and stream new entries (Ctrl-C to stop)
116
120
 
117
121
  Examples:
118
122
  run402 functions logs prj_abc123 stripe-webhook --tail 100
119
123
  run402 functions logs prj_abc123 stripe-webhook --since 2026-03-29T14:00:00Z
124
+ run402 functions logs prj_abc123 stripe-webhook --request-id req_abc123
120
125
  run402 functions logs prj_abc123 stripe-webhook --follow
121
126
  `,
122
127
  update: `run402 functions update — Update function config without re-deploying
@@ -227,14 +232,16 @@ async function invoke(projectId, name, args) {
227
232
  }
228
233
 
229
234
  async function logs(projectId, name, args) {
230
- assertRequiredProjectAndName(projectId, name, "run402 functions logs <project_id> <name> [--tail <n>]");
231
- assertKnownFlags(args, ["--tail", "--since", "--follow", "--help", "-h"], ["--tail", "--since"]);
235
+ assertRequiredProjectAndName(projectId, name, "run402 functions logs <project_id> <name> [--tail <n>] [--request-id <req_...>]");
236
+ assertKnownFlags(args, ["--tail", "--since", "--request-id", "--follow", "--help", "-h"], ["--tail", "--since", "--request-id"]);
232
237
  let tail = 50;
233
238
  let since = undefined;
239
+ let requestId = undefined;
234
240
  let follow = false;
235
241
  for (let i = 0; i < args.length; i++) {
236
242
  if (args[i] === "--tail") tail = parseIntegerFlag("--tail", args[++i], { min: 1 });
237
243
  if (args[i] === "--since" && args[i + 1]) since = args[++i];
244
+ if (args[i] === "--request-id" && args[i + 1]) requestId = args[++i];
238
245
  if (args[i] === "--follow") follow = true;
239
246
  }
240
247
 
@@ -254,12 +261,20 @@ async function logs(projectId, name, args) {
254
261
  }
255
262
  sinceIso = new Date(ms).toISOString();
256
263
  }
264
+ if (requestId !== undefined && !FUNCTION_LOG_REQUEST_ID_RE.test(requestId)) {
265
+ fail({
266
+ code: "BAD_USAGE",
267
+ message: `Invalid --request-id value: ${requestId}`,
268
+ details: { flag: "--request-id", value: requestId, expected: "req_<4-128 url-safe chars>" },
269
+ });
270
+ }
257
271
 
258
272
  const fetchLogs = async () => {
259
273
  try {
260
274
  const data = await getSdk().functions.logs(projectId, name, {
261
275
  tail,
262
276
  since: sinceIso,
277
+ requestId,
263
278
  });
264
279
  return data.logs || [];
265
280
  } catch (err) {
@@ -278,27 +293,60 @@ async function logs(projectId, name, args) {
278
293
  let running = true;
279
294
  process.on("SIGINT", () => { running = false; });
280
295
 
281
- const initial = await fetchLogs();
282
- for (const entry of initial) {
283
- console.log(`[${entry.timestamp}] ${entry.message}`);
284
- }
285
- if (initial.length > 0) {
286
- sinceIso = new Date(new Date(initial[initial.length - 1].timestamp).getTime() + 1).toISOString();
287
- }
296
+ let highWaterMs = sinceIso === undefined ? Number.NEGATIVE_INFINITY : new Date(sinceIso).getTime();
297
+ let seenAtHighWater = new Set();
288
298
 
289
- while (running) {
290
- await new Promise(r => setTimeout(r, 3000));
291
- if (!running) break;
292
- const entries = await fetchLogs();
299
+ const printFreshEntries = (entries) => {
300
+ let nextHighWaterMs = highWaterMs;
301
+ const fresh = [];
293
302
  for (const entry of entries) {
303
+ const entryMs = logTimestampMs(entry);
304
+ const identity = logEntryIdentity(entry);
305
+ if (entryMs < highWaterMs) continue;
306
+ if (entryMs === highWaterMs && seenAtHighWater.has(identity)) continue;
307
+ fresh.push({ entry, entryMs, identity });
308
+ if (entryMs > nextHighWaterMs) nextHighWaterMs = entryMs;
309
+ }
310
+
311
+ for (const { entry } of fresh) {
294
312
  console.log(`[${entry.timestamp}] ${entry.message}`);
295
313
  }
296
- if (entries.length > 0) {
297
- sinceIso = new Date(new Date(entries[entries.length - 1].timestamp).getTime() + 1).toISOString();
314
+ if (fresh.length === 0 || !Number.isFinite(nextHighWaterMs)) return;
315
+
316
+ const nextSeenAtHighWater = new Set();
317
+ for (const entry of entries) {
318
+ if (logTimestampMs(entry) === nextHighWaterMs) {
319
+ nextSeenAtHighWater.add(logEntryIdentity(entry));
320
+ }
298
321
  }
322
+ for (const { entry, entryMs, identity } of fresh) {
323
+ if (entryMs === nextHighWaterMs) {
324
+ nextSeenAtHighWater.add(identity);
325
+ }
326
+ }
327
+ highWaterMs = nextHighWaterMs;
328
+ seenAtHighWater = nextSeenAtHighWater;
329
+ sinceIso = new Date(highWaterMs).toISOString();
330
+ };
331
+
332
+ printFreshEntries(await fetchLogs());
333
+
334
+ while (running) {
335
+ await new Promise(r => setTimeout(r, 3000));
336
+ if (!running) break;
337
+ printFreshEntries(await fetchLogs());
299
338
  }
300
339
  }
301
340
 
341
+ function logTimestampMs(entry) {
342
+ const ms = new Date(entry.timestamp).getTime();
343
+ return Number.isNaN(ms) ? 0 : ms;
344
+ }
345
+
346
+ function logEntryIdentity(entry) {
347
+ return entry.event_id || `${entry.log_stream_name || ""}:${entry.timestamp || ""}:${entry.message || ""}`;
348
+ }
349
+
302
350
  async function update(projectId, name, args) {
303
351
  assertRequiredProjectAndName(projectId, name, "run402 functions update <project_id> <name> [options]");
304
352
  assertKnownFlags(args, ["--schedule", "--schedule-remove", "--timeout", "--memory", "--help", "-h"], ["--schedule", "--timeout", "--memory"]);
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) {