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.
- package/lib/assets.mjs +18 -6
- package/lib/cache.mjs +11 -47
- package/lib/deploy-v2.mjs +38 -0
- package/lib/doctor-source-scan.mjs +424 -0
- package/lib/doctor-source-scan.test.mjs +318 -0
- package/lib/doctor.mjs +59 -26
- package/lib/email.mjs +30 -12
- package/lib/functions.mjs +37 -12
- package/lib/init-astro.mjs +127 -35
- package/lib/init.mjs +10 -10
- package/lib/logs.mjs +32 -37
- package/package.json +1 -1
package/lib/init-astro.mjs
CHANGED
|
@@ -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]
|
|
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
|
-
|
|
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
|
|
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,
|
|
222
|
+
import { db, auth, cache } from "@run402/functions";
|
|
219
223
|
|
|
220
224
|
export const POST: APIRoute = async ({ request }) => {
|
|
221
|
-
const user = await
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
79
|
-
// agents.
|
|
80
|
-
const write =
|
|
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
|
-
|
|
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>] [--
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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