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.
- package/README.md +6 -1
- package/lib/allowance.mjs +20 -25
- package/lib/auth.mjs +7 -16
- package/lib/billing.mjs +12 -10
- package/lib/blob.mjs +30 -54
- package/lib/contracts.mjs +11 -19
- package/lib/deploy-v2.mjs +4 -1
- package/lib/functions.mjs +65 -17
- package/lib/init.mjs +12 -19
- package/lib/projects.mjs +157 -48
- package/lib/status.mjs +8 -15
- package/package.json +1 -1
- package/sdk/dist/kernel.d.ts +6 -0
- package/sdk/dist/kernel.d.ts.map +1 -1
- package/sdk/dist/kernel.js +5 -1
- package/sdk/dist/kernel.js.map +1 -1
- package/sdk/dist/namespaces/billing.d.ts +10 -5
- package/sdk/dist/namespaces/billing.d.ts.map +1 -1
- package/sdk/dist/namespaces/billing.js +23 -9
- package/sdk/dist/namespaces/billing.js.map +1 -1
- package/sdk/dist/namespaces/blobs.d.ts +16 -1
- package/sdk/dist/namespaces/blobs.d.ts.map +1 -1
- package/sdk/dist/namespaces/blobs.js +61 -25
- package/sdk/dist/namespaces/blobs.js.map +1 -1
- package/sdk/dist/namespaces/blobs.types.d.ts +41 -0
- package/sdk/dist/namespaces/blobs.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.d.ts +2 -1
- package/sdk/dist/namespaces/functions.d.ts.map +1 -1
- package/sdk/dist/namespaces/functions.js +19 -7
- package/sdk/dist/namespaces/functions.js.map +1 -1
- package/sdk/dist/namespaces/functions.types.d.ts +12 -2
- package/sdk/dist/namespaces/functions.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.d.ts +14 -3
- package/sdk/dist/namespaces/projects.d.ts.map +1 -1
- package/sdk/dist/namespaces/projects.js +95 -5
- package/sdk/dist/namespaces/projects.js.map +1 -1
- package/sdk/dist/namespaces/projects.types.d.ts +39 -0
- package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
- package/sdk/dist/scoped.d.ts +10 -3
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +20 -2
- 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
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 {
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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
|
|
326
|
-
if (poll.status ===
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
initRes = init.body;
|
|
346
|
-
saveState({
|
|
337
|
+
state = {
|
|
347
338
|
upload_id: initRes.upload_id,
|
|
348
|
-
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
|
-
|
|
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
|
|
390
|
-
|
|
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", ...
|
|
394
|
-
return
|
|
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
|
|
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
|
-
|
|
457
|
-
|
|
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.
|
|
151
|
-
|
|
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
|
|
155
|
-
|
|
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": "
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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 (
|
|
297
|
-
|
|
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
|
|
2
|
-
import {
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
}
|
|
204
|
-
|
|
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
|
-
|
|
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) {
|