@selvajs/cli 4.14.1 → 4.14.2
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/package.json +1 -1
- package/src/checks/config.js +54 -0
- package/src/commands/create.js +6 -1
- package/src/commands/doctor.js +5 -3
- package/src/commands/proxy.js +16 -0
- package/src/deployment-package.js +1 -1
package/package.json
CHANGED
package/src/checks/config.js
CHANGED
|
@@ -232,6 +232,60 @@ export function checkBodySizeLimit(env) {
|
|
|
232
232
|
return green(`BODY_SIZE_LIMIT=${raw}`);
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
/**
|
|
236
|
+
* `ADDRESS_HEADER` / `XFF_DEPTH` are read by adapter-node, not by Selva, and
|
|
237
|
+
* unset they are invisible: nothing fails, nothing logs, and the deployment
|
|
238
|
+
* looks healthy.
|
|
239
|
+
*
|
|
240
|
+
* What actually happens is that `getClientAddress()` returns the socket peer,
|
|
241
|
+
* which behind a reverse proxy is the proxy — `127.0.0.1` for every request
|
|
242
|
+
* from every user. Login rate limiting keys on that, so the whole instance
|
|
243
|
+
* shares one bucket: five failed logins from anywhere return 429 to everyone,
|
|
244
|
+
* and only a *successful* login clears a bucket, which nobody can then reach.
|
|
245
|
+
*
|
|
246
|
+
* `ORIGIN` is the proxy tell. It is only needed when the app is served under a
|
|
247
|
+
* public URL it can't derive from its own socket, so its presence means a proxy
|
|
248
|
+
* is terminating requests. Without it we say nothing: on a directly-reachable
|
|
249
|
+
* app these settings are a footgun, since `X-Forwarded-For` is client-supplied
|
|
250
|
+
* and anyone could then pick their own rate-limit bucket.
|
|
251
|
+
*/
|
|
252
|
+
export function checkClientAddress(env) {
|
|
253
|
+
const header = env.ADDRESS_HEADER;
|
|
254
|
+
const depth = env.XFF_DEPTH;
|
|
255
|
+
|
|
256
|
+
if (!env.ORIGIN) {
|
|
257
|
+
// No proxy signal — nothing to advise either way.
|
|
258
|
+
return green('ADDRESS_HEADER not needed (no ORIGIN, so no reverse proxy)');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!header) {
|
|
262
|
+
return red(
|
|
263
|
+
`ADDRESS_HEADER unset while ORIGIN is set — every request looks like it came from ` +
|
|
264
|
+
`the proxy, so all users share ONE login rate-limit bucket. Five failed logins ` +
|
|
265
|
+
`from anyone locks out the whole instance for 15 minutes. ` +
|
|
266
|
+
`Set ADDRESS_HEADER=X-Forwarded-For and XFF_DEPTH=<number of proxies>.`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (header.toLowerCase() === 'x-forwarded-for' && !depth) {
|
|
271
|
+
return yellow(
|
|
272
|
+
`ADDRESS_HEADER=${header} but XFF_DEPTH is unset. X-Forwarded-For is a ` +
|
|
273
|
+
`client-appendable list, so a caller can prepend fake entries — adapter-node ` +
|
|
274
|
+
`needs the proxy count to know which entry is real. Set XFF_DEPTH=1 for a ` +
|
|
275
|
+
`single reverse proxy, 2 if a CDN or load balancer sits in front of it.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (depth !== undefined && !/^[1-9][0-9]*$/.test(depth)) {
|
|
280
|
+
return red(
|
|
281
|
+
`XFF_DEPTH="${depth}" is not a positive integer — it counts proxies from the ` +
|
|
282
|
+
`outside in. Use 1 for a single reverse proxy.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return green(`ADDRESS_HEADER=${header}${depth ? ` (XFF_DEPTH=${depth})` : ''}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
235
289
|
// A rename is auto-fixable (`selva migrate` rewrites the key). A replacement
|
|
236
290
|
// encodes a value in the new name, so migrate leaves it alone rather than
|
|
237
291
|
// guessing — it's reported here and nowhere else.
|
package/src/commands/create.js
CHANGED
|
@@ -127,9 +127,14 @@ function nextSteps({ rawDir, values, supabase }) {
|
|
|
127
127
|
if (supabase) {
|
|
128
128
|
lines.push(
|
|
129
129
|
` ${n++}. Apply the database schema (once per Supabase project):`,
|
|
130
|
+
pc.dim(' npx supabase login # or: export SUPABASE_ACCESS_TOKEN=sbp_...'),
|
|
130
131
|
pc.dim(' npx selva-supabase # copy migrations into ./supabase'),
|
|
131
132
|
pc.dim(' npx supabase link --project-ref <ref>'),
|
|
132
|
-
pc.dim(' npx supabase db push')
|
|
133
|
+
pc.dim(' npx supabase db push'),
|
|
134
|
+
// `link` fails without an ACCOUNT credential, which is none of the three
|
|
135
|
+
// project keys the prompts just collected — the distinction is the most
|
|
136
|
+
// common first-run stall, so name it here rather than only in the docs.
|
|
137
|
+
pc.dim(' (login is a Supabase account credential — not the keys in .env)')
|
|
133
138
|
);
|
|
134
139
|
}
|
|
135
140
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -28,6 +28,7 @@ import { checkBootPersistence } from '../checks/boot.js';
|
|
|
28
28
|
import { checkRuntimeEnvironment } from '../checks/runtime.js';
|
|
29
29
|
import {
|
|
30
30
|
checkBodySizeLimit,
|
|
31
|
+
checkClientAddress,
|
|
31
32
|
checkDeprecatedEnv,
|
|
32
33
|
checkEnvDocumentation,
|
|
33
34
|
checkHeaderNames,
|
|
@@ -84,6 +85,7 @@ export async function runDoctor(argv = []) {
|
|
|
84
85
|
checks.push(...checkBootPersistence(dir));
|
|
85
86
|
checks.push(checkOrigin(env));
|
|
86
87
|
checks.push(checkBodySizeLimit(env));
|
|
88
|
+
checks.push(checkClientAddress(env));
|
|
87
89
|
checks.push(...checkDeprecatedEnv(env));
|
|
88
90
|
checks.push(checkEnvDocs(dir));
|
|
89
91
|
|
|
@@ -265,7 +267,7 @@ async function checkSupabaseMigrations(dir, env) {
|
|
|
265
267
|
clearTimeout(timer);
|
|
266
268
|
if (res.status === 404) {
|
|
267
269
|
return red(
|
|
268
|
-
`Database is missing selva.migration_head() — migrations are pending (expected head ${expected}).
|
|
270
|
+
`Database is missing selva.migration_head() — migrations are pending (expected head ${expected}). Run: npx selva-supabase && npx supabase db push`
|
|
269
271
|
);
|
|
270
272
|
}
|
|
271
273
|
if (!res.ok) {
|
|
@@ -274,13 +276,13 @@ async function checkSupabaseMigrations(dir, env) {
|
|
|
274
276
|
const actual = String((await res.json()) ?? '');
|
|
275
277
|
if (!actual) {
|
|
276
278
|
return red(
|
|
277
|
-
`Database reports no applied migrations (expected head ${expected}).
|
|
279
|
+
`Database reports no applied migrations (expected head ${expected}). Run: npx selva-supabase && npx supabase db push`
|
|
278
280
|
);
|
|
279
281
|
}
|
|
280
282
|
// String comparison — relies on both heads being fixed-width, zero-padded timestamps.
|
|
281
283
|
if (actual < expected) {
|
|
282
284
|
return red(
|
|
283
|
-
`Database migration head ${actual} is behind the installed provider (${expected}).
|
|
285
|
+
`Database migration head ${actual} is behind the installed provider (${expected}). Run: npx selva-supabase && npx supabase db push`
|
|
284
286
|
);
|
|
285
287
|
}
|
|
286
288
|
return green(`Database migration head ${actual} matches the installed provider`);
|
package/src/commands/proxy.js
CHANGED
|
@@ -128,6 +128,22 @@ export async function runSetupProxy(argv = []) {
|
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Caddy sets X-Forwarded-For on its own, but the app ignores it unless told
|
|
132
|
+
// to trust it — and unset, every request looks like it came from Caddy, so
|
|
133
|
+
// all users share one login rate-limit bucket. Nothing fails visibly, which
|
|
134
|
+
// is why it is said here rather than left to the docs.
|
|
135
|
+
if (!env.ADDRESS_HEADER) {
|
|
136
|
+
p.log.warn(
|
|
137
|
+
`Add these to .env, then \`selva restart\`:\n` +
|
|
138
|
+
pc.cyan(' ADDRESS_HEADER=X-Forwarded-For\n XFF_DEPTH=1\n') +
|
|
139
|
+
pc.dim(
|
|
140
|
+
'Without them the app sees every request as coming from Caddy, so one\n' +
|
|
141
|
+
'user failing five logins rate-limits everyone. Use XFF_DEPTH=2 if a CDN\n' +
|
|
142
|
+
'or load balancer sits in front of Caddy.'
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
p.outro(
|
|
132
148
|
[
|
|
133
149
|
pc.green(`Caddy is serving ${domain}.`),
|
|
@@ -71,7 +71,7 @@ const DEPENDENCIES = {
|
|
|
71
71
|
* without this. Its migration SQL is not: `@selvajs/selva` publishes only
|
|
72
72
|
* `build` + `templates`, while the SQL and the `selva-supabase` bin ship in
|
|
73
73
|
* this package's own tarball. Without it, a deployment has no way to reach the
|
|
74
|
-
* schema it needs — `npx selva-supabase
|
|
74
|
+
* schema it needs — `npx selva-supabase` resolves nothing and
|
|
75
75
|
* `doctor`'s migration-head check finds no directory to compare against.
|
|
76
76
|
*/
|
|
77
77
|
export const SUPABASE_DEPENDENCIES = {
|