run402 2.22.0 → 2.24.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.
@@ -21,14 +21,19 @@ import { fail } from "./sdk-errors.mjs";
21
21
  const HELP = `run402 init astro — Scaffold a deployable Astro project
22
22
 
23
23
  Usage:
24
- run402 init astro [<dir>] [--force] [--json]
24
+ run402 init astro [<dir>] [--force]
25
25
 
26
26
  Arguments:
27
27
  <dir> Target directory (default: current directory)
28
28
 
29
29
  Options:
30
30
  --force Overwrite a non-empty directory
31
- --json Emit a structured JSON summary on stdout
31
+
32
+ Output:
33
+ Stdout is a JSON summary { dir, files_created, created, next_steps }.
34
+ Progress lines ("Scaffolded ...", "Files created:", "Next steps:") go to
35
+ stderr so a human re-running interactively sees what's happening while
36
+ a script piping stdout to jq stays clean.
32
37
 
33
38
  The scaffolded project includes:
34
39
  - package.json (with 'dev' / 'deploy' scripts)
@@ -53,7 +58,6 @@ export async function runInitAstro(args = []) {
53
58
  return;
54
59
  }
55
60
  const force = args.includes("--force");
56
- const json = args.includes("--json");
57
61
  const positionals = args.filter((a) => !a.startsWith("--"));
58
62
  const targetDir = resolve(positionals[0] ?? ".");
59
63
 
@@ -155,7 +159,7 @@ import Layout from "../layouts/Layout.astro";
155
159
  // SSR time. The first request renders + caches; subsequent requests
156
160
  // HIT the cache. When an admin edits the row, call cache.invalidate()
157
161
  // from your save handler for sub-second freshness.
158
- import { db, getUser, cache } from "@run402/functions";
162
+ import { db } from "@run402/functions";
159
163
  import Layout from "../layouts/Layout.astro";
160
164
 
161
165
  const { slug } = Astro.params;
@@ -215,11 +219,10 @@ const { title, ogImage, canonical } = Astro.props;
215
219
  // 2. DB update.
216
220
  // 3. cache.invalidate() so the public URL re-renders fresh on next visit.
217
221
  import type { APIRoute } from "astro";
218
- import { db, getUser, cache } from "@run402/functions";
222
+ import { db, auth, cache } from "@run402/functions";
219
223
 
220
224
  export const POST: APIRoute = async ({ request }) => {
221
- const user = await getUser();
222
- if (!user) return new Response("Unauthorized", { status: 401 });
225
+ const user = await auth.requireUser();
223
226
 
224
227
  const body = (await request.json()) as { slug: string; title: string; html: string };
225
228
  if (!body?.slug) return new Response("Missing slug", { status: 400 });
@@ -236,6 +239,96 @@ export const POST: APIRoute = async ({ request }) => {
236
239
  headers: { "content-type": "application/json" },
237
240
  });
238
241
  };
242
+ `,
243
+ },
244
+ {
245
+ path: "AGENTS.md",
246
+ content: `# AGENTS.md
247
+
248
+ This file documents the brutally-small Run402 surface this Astro project
249
+ uses. Coding agents: read this first. The platform is intentionally small —
250
+ there are no other auth helpers, no other client surfaces, and no other
251
+ hidden APIs.
252
+
253
+ ## The auth surface
254
+
255
+ \`auth\` is the entire user-auth surface. Import from \`@run402/functions\`:
256
+
257
+ \`\`\`ts
258
+ import { auth } from "@run402/functions";
259
+
260
+ // In SSR pages and API routes:
261
+ const user = await auth.user(); // Actor | null
262
+ const user = await auth.requireUser(); // Actor; throws R402_AUTH_REQUIRED
263
+ const { user, role } = await auth.requireRole("admin");
264
+ const { user, membership } = await auth.requireMembership("member");
265
+ await auth.requireFresh({ maxAge: "10m", amr: ["passkey"] });
266
+
267
+ // CSRF for hosted forms (server-side, in <form> rendering):
268
+ const field = auth.csrfField();
269
+ // → <input type="hidden" name="_csrf" value="..." />
270
+
271
+ // Cross-origin-safe fetch (auto-forwards actor context to same-origin):
272
+ const res = await auth.fetch("/api/internal"); // relative URLs only
273
+ \`\`\`
274
+
275
+ ## The four Never rules
276
+
277
+ 1. **Never \`try\`/\`catch\` auth errors.** Let them bubble. The platform turns
278
+ \`R402_AUTH_REQUIRED\` into a 303 to \`/auth/sign-in?return_to=…\` and
279
+ \`R402_AUTH_INSUFFICIENT_ROLE\` into 403 with a fix-it response. Catching
280
+ them creates silent-null bugs.
281
+
282
+ 2. **Never \`.eq("user_id", user.id)\`.** \`db()\` propagates the actor to
283
+ PostgREST so RLS enforces ownership server-side. The redundant filter is
284
+ a code smell that \`run402 doctor\` flags as
285
+ \`R402_AUTH_REDUNDANT_USER_FILTER\`.
286
+
287
+ 3. **Never set client-supplied actor headers.** \`x-run402-actor-*\`,
288
+ \`run402.actor.*\`, \`x-r402-actor-*\` are platform-owned channel headers.
289
+ The gateway strips inbound spoofing attempts and emits
290
+ \`R402_AUTH_ACTOR_HEADER_SPOOF\` in strict mode.
291
+
292
+ 4. **Never mint a session from a raw \`userId\`.** Use
293
+ \`auth.sessions.createResponseFromIdentity({ provider, subject, proof, amr })\`
294
+ with a verified identity proof. No \`createSessionForUserId(uuid)\` API exists.
295
+
296
+ ## Hosted UI components
297
+
298
+ For sign-in, sign-up, and sign-out chrome, use the platform's
299
+ \`@run402/astro\` components — they emit forms posting to platform hosted
300
+ routes (\`/auth/v1/sign-in\` etc.) with the CSRF token already wired:
301
+
302
+ \`\`\`astro
303
+ ---
304
+ import { SignIn, SignUp, UserButton, SignedIn, SignedOut } from "@run402/astro";
305
+ ---
306
+
307
+ <SignedIn>
308
+ <UserButton />
309
+ </SignedIn>
310
+ <SignedOut>
311
+ <SignIn returnTo="/dashboard" />
312
+ </SignedOut>
313
+ \`\`\`
314
+
315
+ Do NOT roll your own sign-in form. The hosted routes handle CSRF, returnTo
316
+ validation, OAuth provider bridges, and passkey ceremonies.
317
+
318
+ ## Rendering-mode quick map
319
+
320
+ \`auth.*\` calls run at request time, so the page must be SSR or a
321
+ server-island. Calling \`auth.user()\` from a prerendered page throws
322
+ \`R402_AUTH_PRERENDERED\`.
323
+
324
+ | Mode | When | Auth-aware |
325
+ | ----------------------------- | ----------------------------------- | ----------------------- |
326
+ | SSR (default) | Personalized pages | \`auth.user()\` works |
327
+ | Prerendered | Marketing pages, never sees actor | \`auth.*\` throws |
328
+ | Server island | Prerendered page + personalized slot| \`auth.*\` in the island|
329
+ | Client hydrate | Visibility-only, no SSR pass | Component hits session |
330
+
331
+ For error-code reference: https://run402.com/errors/#R402_AUTH_REQUIRED
239
332
  `,
240
333
  },
241
334
  ];
@@ -246,32 +339,31 @@ export const POST: APIRoute = async ({ request }) => {
246
339
  writeFileSync(fullPath, file.content, "utf-8");
247
340
  }
248
341
 
249
- if (json) {
250
- console.log(
251
- JSON.stringify(
252
- {
253
- dir: targetDir,
254
- files_created: files.map((f) => f.path),
255
- created: true,
256
- next_steps: [
257
- `cd ${positionals[0] ?? "."}`,
258
- "npm install",
259
- "run402 deploy",
260
- ],
261
- },
262
- null,
263
- 2,
264
- ),
265
- );
266
- } else {
267
- console.log(`Scaffolded Astro project at ${targetDir}`);
268
- console.log("");
269
- console.log("Files created:");
270
- for (const f of files) console.log(` - ${f.path}`);
271
- console.log("");
272
- console.log("Next steps:");
273
- if (positionals[0]) console.log(` cd ${positionals[0]}`);
274
- console.log(" npm install");
275
- console.log(" run402 deploy");
276
- }
342
+ // Human-readable progress goes to stderr; stdout stays JSON-clean.
343
+ console.error(`Scaffolded Astro project at ${targetDir}`);
344
+ console.error("");
345
+ console.error("Files created:");
346
+ for (const f of files) console.error(` - ${f.path}`);
347
+ console.error("");
348
+ console.error("Next steps:");
349
+ if (positionals[0]) console.error(` cd ${positionals[0]}`);
350
+ console.error(" npm install");
351
+ console.error(" run402 deploy");
352
+
353
+ console.log(
354
+ JSON.stringify(
355
+ {
356
+ dir: targetDir,
357
+ files_created: files.map((f) => f.path),
358
+ created: true,
359
+ next_steps: [
360
+ `cd ${positionals[0] ?? "."}`,
361
+ "npm install",
362
+ "run402 deploy",
363
+ ],
364
+ },
365
+ null,
366
+ 2,
367
+ ),
368
+ );
277
369
  }
package/lib/init.mjs CHANGED
@@ -18,14 +18,17 @@ Usage:
18
18
  Required when an allowance already exists on
19
19
  the other rail; protects scripted re-runs from
20
20
  silently flipping billing networks.
21
- run402 init --json Same as init, but emit a JSON summary on stdout
22
- (human lines go to stderr — for agent automation)
23
21
 
24
22
  Options:
25
23
  --switch-rail Confirm switching the persisted payment rail. Re-running
26
24
  init with the SAME rail as the existing allowance is always
27
25
  idempotent and does not need this flag.
28
- --json Emit a structured JSON summary on stdout.
26
+
27
+ Output:
28
+ Stdout is a JSON summary { config_dir, allowance, rail, network, balance,
29
+ tier, projects_saved, next_step }. Progress lines (Config / Allowance /
30
+ Balance / Tier / Next) go to stderr so a human re-running interactively
31
+ sees what's happening while a script piping stdout to jq stays clean.
29
32
 
30
33
  Steps (idempotent when re-run with the same rail; pass --switch-rail to change rails):
31
34
  1. Creates config directory (~/.config/run402)
@@ -61,7 +64,6 @@ export async function run(args = []) {
61
64
 
62
65
  if (args.includes("--help") || args.includes("-h")) { console.log(HELP); process.exit(0); }
63
66
 
64
- const jsonMode = args.includes("--json");
65
67
  const isMpp = args[0] === "mpp";
66
68
  const requestedRail = isMpp ? "mpp" : "x402";
67
69
  const switchRailConfirmed = args.includes("--switch-rail");
@@ -75,9 +77,9 @@ export async function run(args = []) {
75
77
  });
76
78
  }
77
79
 
78
- // In --json mode, human-readable lines go to stderr so stdout stays clean for
79
- // agents. We also collect structured data for the final JSON emit.
80
- const write = jsonMode ? (s) => console.error(s) : (s) => console.log(s);
80
+ // Human-readable progress lines go to stderr so stdout stays JSON-clean for
81
+ // agents. Final structured summary emits to stdout at the end.
82
+ const write = (s) => console.error(s);
81
83
  const line = (label, value) => write(` ${label.padEnd(10)} ${value}`);
82
84
  const summary = {
83
85
  config_dir: CONFIG_DIR,
@@ -265,7 +267,5 @@ export async function run(args = []) {
265
267
  write("");
266
268
  summary.next_step = nextStep;
267
269
 
268
- if (jsonMode) {
269
- console.log(JSON.stringify(summary, null, 2));
270
- }
270
+ console.log(JSON.stringify(summary, null, 2));
271
271
  }
package/lib/logs.mjs CHANGED
@@ -21,7 +21,7 @@ import { reportSdkError, fail } from "./sdk-errors.mjs";
21
21
  const HELP = `run402 logs — Fetch function logs by request id
22
22
 
23
23
  Usage:
24
- run402 logs --request-id <req_id> [--function <name>] [--project <id>] [--json] [--tail <n>]
24
+ run402 logs --request-id <req_id> [--function <name>] [--project <id>] [--tail <n>]
25
25
 
26
26
  Required:
27
27
  --request-id <req_id> The req_... id (from x-run402-request-id header)
@@ -30,12 +30,14 @@ Optional:
30
30
  --function <name> Limit to one function (default: scan all functions in the project)
31
31
  --project <id> Project id (default: \$RUN402_PROJECT_ID)
32
32
  --tail <n> Max entries per function (default 100)
33
- --json Machine-readable output
33
+
34
+ Output:
35
+ Stdout is JSON { ok, request_id, project_id, scanned, entries, errors? }.
34
36
 
35
37
  Examples:
36
38
  run402 logs --request-id req_abc123
37
39
  run402 logs --request-id req_abc123 --function ssr
38
- run402 logs --request-id req_abc123 --project prj_xyz --json
40
+ run402 logs --request-id req_abc123 --project prj_xyz
39
41
 
40
42
  Tip: the request id appears in:
41
43
  - The 'x-run402-request-id' response header on every SSR response
@@ -50,7 +52,6 @@ export async function run(sub, args = []) {
50
52
  return;
51
53
  }
52
54
 
53
- const json = all.includes("--json");
54
55
  const requestId = pickFlagValue(all, "--request-id");
55
56
  const fnName = pickFlagValue(all, "--function");
56
57
  const projectIdArg = pickFlagValue(all, "--project");
@@ -98,18 +99,19 @@ export async function run(sub, args = []) {
98
99
  const list = await sdk.functions.list(projectId);
99
100
  fnNames = (list?.functions ?? []).map((f) => f.name);
100
101
  if (fnNames.length === 0) {
101
- if (json) console.log(JSON.stringify({ ok: true, entries: [], scanned: [] }));
102
- else console.log("No functions in project " + projectId);
102
+ console.log(JSON.stringify({ ok: true, request_id: requestId, project_id: projectId, entries: [], scanned: [] }, null, 2));
103
103
  return;
104
104
  }
105
105
  }
106
106
 
107
- // Query each function in parallel; aggregate entries.
107
+ // Query each function in parallel; aggregate entries. SDK returns
108
+ // FunctionLogsResult = { logs: FunctionLogEntry[] }; unwrap to the array
109
+ // so the aggregated JSON has a flat entries[] field.
108
110
  const results = await Promise.allSettled(
109
111
  fnNames.map((name) =>
110
112
  sdk.functions
111
113
  .logs(projectId, name, { requestId, tail })
112
- .then((entries) => ({ name, entries: entries ?? [] })),
114
+ .then((result) => ({ name, entries: result?.logs ?? [] })),
113
115
  ),
114
116
  );
115
117
 
@@ -127,35 +129,28 @@ export async function run(sub, args = []) {
127
129
  }
128
130
  }
129
131
 
130
- // Sort by timestamp ascending.
131
- allEntries.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
132
-
133
- if (json) {
134
- console.log(
135
- JSON.stringify(
136
- {
137
- ok: errors.length === 0,
138
- request_id: requestId,
139
- project_id: projectId,
140
- scanned,
141
- entries: allEntries,
142
- ...(errors.length > 0 && { errors }),
143
- },
144
- null,
145
- 2,
146
- ),
147
- );
148
- } else {
149
- if (allEntries.length === 0) {
150
- console.log(`No log entries found for ${requestId} across ${scanned.length} function(s).`);
151
- } else {
152
- for (const e of allEntries) {
153
- const t = e.ts ? new Date(e.ts).toISOString() : "";
154
- console.log(`[${t}] [${e.function}] ${e.message ?? ""}`);
155
- }
156
- console.log(`\n${allEntries.length} entries across ${scanned.length} function(s) for ${requestId}.`);
157
- }
158
- }
132
+ // Sort by timestamp ascending. FunctionLogEntry.timestamp is an ISO 8601
133
+ // string; convert to epoch ms for comparison.
134
+ allEntries.sort((a, b) => {
135
+ const ta = a.timestamp ? Date.parse(a.timestamp) : 0;
136
+ const tb = b.timestamp ? Date.parse(b.timestamp) : 0;
137
+ return ta - tb;
138
+ });
139
+
140
+ console.log(
141
+ JSON.stringify(
142
+ {
143
+ ok: errors.length === 0,
144
+ request_id: requestId,
145
+ project_id: projectId,
146
+ scanned,
147
+ entries: allEntries,
148
+ ...(errors.length > 0 && { errors }),
149
+ },
150
+ null,
151
+ 2,
152
+ ),
153
+ );
159
154
  } catch (err) {
160
155
  reportSdkError(err);
161
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {