@selvajs/cli 4.20.0-beta.21 → 4.20.0-beta.23
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/migrations.js +156 -0
- package/src/commands/doctor.js +8 -78
- package/src/commands/pm2.js +80 -6
- package/src/generated/config.js +211 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/cli",
|
|
3
|
-
"version": "4.20.0-beta.
|
|
3
|
+
"version": "4.20.0-beta.23",
|
|
4
4
|
"description": "Scaffold and operate a Selva white-label deployment. `npx @selvajs/cli <dir>` to bootstrap, `selva <cmd>` to manage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "VektorNode",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Supabase schema head: what the installed runtime expects, what the installed
|
|
2
|
+
// provider can migrate to, and what the database is at. These are three
|
|
3
|
+
// separate numbers. The runtime bundles the provider's code but not its SQL,
|
|
4
|
+
// so `selva update` can move the runtime past a provider install that stays
|
|
5
|
+
// pinned. Comparing the database against the provider then reports a match
|
|
6
|
+
// while the app degrades /api/health to 503.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { green, yellow, red } from './result.js';
|
|
11
|
+
|
|
12
|
+
const HEAD_RE = /^\d{14}$/;
|
|
13
|
+
|
|
14
|
+
/** Head the installed @selvajs/selva was built against, or null for a runtime that predates runtime-meta.json. */
|
|
15
|
+
export function readRuntimeMigrationHead(dir) {
|
|
16
|
+
try {
|
|
17
|
+
const meta = JSON.parse(
|
|
18
|
+
readFileSync(
|
|
19
|
+
join(dir, 'node_modules', '@selvajs', 'selva', 'build', 'runtime-meta.json'),
|
|
20
|
+
'utf8'
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
return HEAD_RE.test(meta?.supabaseMigrationHead) ? meta.supabaseMigrationHead : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Newest migration the installed @selvajs/supabase-provider ships, or null when it isn't installed. */
|
|
30
|
+
export function readProviderMigrationHead(dir) {
|
|
31
|
+
const migrationsDir = join(
|
|
32
|
+
dir,
|
|
33
|
+
'node_modules',
|
|
34
|
+
'@selvajs',
|
|
35
|
+
'supabase-provider',
|
|
36
|
+
'supabase',
|
|
37
|
+
'migrations'
|
|
38
|
+
);
|
|
39
|
+
if (!existsSync(migrationsDir)) return null;
|
|
40
|
+
return (
|
|
41
|
+
readdirSync(migrationsDir)
|
|
42
|
+
.filter((f) => /^\d{14}_.+\.sql$/.test(f))
|
|
43
|
+
.map((f) => f.slice(0, 14))
|
|
44
|
+
.sort()
|
|
45
|
+
.pop() ?? null
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The npm dist-tag matching the installed runtime: a pre-release runtime needs the provider's beta migrations. */
|
|
50
|
+
export function providerTagFor(runtimeVersion) {
|
|
51
|
+
return runtimeVersion?.includes('-') ? 'beta' : 'latest';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `{ head }` on success, `{ missing: true }` when selva.migration_head() doesn't
|
|
56
|
+
* exist yet, `{ error }` when the database couldn't be asked.
|
|
57
|
+
*/
|
|
58
|
+
export async function fetchDatabaseMigrationHead(env) {
|
|
59
|
+
try {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), 4000);
|
|
62
|
+
const res = await fetch(env.SUPABASE_URL + '/rest/v1/rpc/migration_head', {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
apikey: env.SUPABASE_SERVICE_ROLE_KEY,
|
|
66
|
+
Authorization: `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
// The function lives in the selva schema the app is pinned to.
|
|
69
|
+
'Content-Profile': 'selva'
|
|
70
|
+
},
|
|
71
|
+
body: '{}',
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
if (res.status === 404) return { missing: true };
|
|
76
|
+
if (!res.ok) return { error: `migration_head RPC responded ${res.status}` };
|
|
77
|
+
return { head: String((await res.json()) ?? '') };
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return { error: err.message ?? String(err) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function migrateSteps(tag, providerBehind) {
|
|
84
|
+
return [
|
|
85
|
+
...(providerBehind ? [`npm install --prefer-online @selvajs/supabase-provider@${tag}`] : []),
|
|
86
|
+
'npx selva-supabase',
|
|
87
|
+
'npx supabase db push'
|
|
88
|
+
].join(' && ');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pure verdict, so the three-way comparison is testable without disk or network.
|
|
93
|
+
* `runtime` is null for an old runtime; the provider head is then the best guess.
|
|
94
|
+
* String comparison is safe: every head is a fixed-width timestamp.
|
|
95
|
+
*/
|
|
96
|
+
export function judgeMigrationHeads({ runtime, provider, database, tag }) {
|
|
97
|
+
const expected = runtime ?? provider;
|
|
98
|
+
if (!expected) {
|
|
99
|
+
return red(
|
|
100
|
+
'@selvajs/supabase-provider is not installed, so the migration SQL is missing. ' +
|
|
101
|
+
'Run `selva migrate` to add it, then `npm install`.'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const providerBehind = !provider || provider < expected;
|
|
105
|
+
const steps = migrateSteps(tag, providerBehind);
|
|
106
|
+
const providerNote =
|
|
107
|
+
runtime && providerBehind
|
|
108
|
+
? ` The installed @selvajs/supabase-provider only reaches ${provider ?? '(none)'}, so syncing it copies nothing new.`
|
|
109
|
+
: '';
|
|
110
|
+
|
|
111
|
+
if (database.error) return yellow(`Migration-head check skipped (${database.error})`);
|
|
112
|
+
if (database.missing) {
|
|
113
|
+
return red(
|
|
114
|
+
`Database is missing selva.migration_head(): migrations are pending (expected head ${expected}).${providerNote} Run: ${steps}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!database.head) {
|
|
118
|
+
return red(
|
|
119
|
+
`Database reports no applied migrations (expected head ${expected}).${providerNote} Run: ${steps}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (database.head < expected) {
|
|
123
|
+
const against = runtime ? "the app's expected" : 'the installed provider';
|
|
124
|
+
return red(
|
|
125
|
+
`Database migration head ${database.head} is behind ${against} ${expected}. ` +
|
|
126
|
+
`/api/health returns 503 and writes that need the newer schema fail.${providerNote} Run: ${steps}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (runtime && providerBehind) {
|
|
130
|
+
// The database is fine today; the next upgrade's sync would silently copy nothing.
|
|
131
|
+
return yellow(
|
|
132
|
+
`Database migration head ${database.head} matches the app, but @selvajs/supabase-provider ` +
|
|
133
|
+
`is at ${provider ?? '(none)'}. Run: npm install --prefer-online @selvajs/supabase-provider@${tag}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return green(
|
|
137
|
+
`Database migration head ${database.head} matches the ${runtime ? 'app' : 'installed provider'}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function checkSupabaseMigrations(dir, env, runtimeVersion) {
|
|
142
|
+
if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
143
|
+
return yellow('Migration-head check skipped: Supabase env incomplete');
|
|
144
|
+
}
|
|
145
|
+
const runtime = readRuntimeMigrationHead(dir);
|
|
146
|
+
const provider = readProviderMigrationHead(dir);
|
|
147
|
+
if (!runtime && !provider) {
|
|
148
|
+
return judgeMigrationHeads({ runtime, provider, database: {}, tag: 'latest' });
|
|
149
|
+
}
|
|
150
|
+
return judgeMigrationHeads({
|
|
151
|
+
runtime,
|
|
152
|
+
provider,
|
|
153
|
+
database: await fetchDatabaseMigrationHead(env),
|
|
154
|
+
tag: providerTagFor(runtimeVersion)
|
|
155
|
+
});
|
|
156
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -31,6 +31,7 @@ import { satisfiesNodeRange } from '../node-range.js';
|
|
|
31
31
|
import { green, yellow, red, fixable } from '../checks/result.js';
|
|
32
32
|
import { checkBootPersistence } from '../checks/boot.js';
|
|
33
33
|
import { checkRuntimeEnvironment } from '../checks/runtime.js';
|
|
34
|
+
import { checkSupabaseMigrations } from '../checks/migrations.js';
|
|
34
35
|
import {
|
|
35
36
|
checkBodySizeLimit,
|
|
36
37
|
checkClientAddress,
|
|
@@ -77,7 +78,13 @@ export async function runDoctor(argv = []) {
|
|
|
77
78
|
|
|
78
79
|
if (used.has('supabase')) {
|
|
79
80
|
checks.push(checkSupabase(env));
|
|
80
|
-
checks.push(
|
|
81
|
+
checks.push(
|
|
82
|
+
checkSupabaseMigrations(
|
|
83
|
+
dir,
|
|
84
|
+
env,
|
|
85
|
+
readPackageVersion(join(dir, 'node_modules', '@selvajs', 'selva', 'package.json'))
|
|
86
|
+
)
|
|
87
|
+
);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
if (providers.auth === 'header') {
|
|
@@ -222,83 +229,6 @@ export async function checkSupabase(env) {
|
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
// Compares the newest migration shipped by @selvajs/supabase-provider against
|
|
226
|
-
// what the database reports via selva.migration_head(). Red on skew — the app
|
|
227
|
-
// degrades /api/health to 503 at boot in the same state, so catching it here
|
|
228
|
-
// saves a confusing deploy.
|
|
229
|
-
async function checkSupabaseMigrations(dir, env) {
|
|
230
|
-
if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_ROLE_KEY) {
|
|
231
|
-
return yellow('Migration-head check skipped — Supabase env incomplete');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const migrationsDir = join(
|
|
235
|
-
dir,
|
|
236
|
-
'node_modules',
|
|
237
|
-
'@selvajs',
|
|
238
|
-
'supabase-provider',
|
|
239
|
-
'supabase',
|
|
240
|
-
'migrations'
|
|
241
|
-
);
|
|
242
|
-
let expected = null;
|
|
243
|
-
try {
|
|
244
|
-
expected =
|
|
245
|
-
readdirSync(migrationsDir)
|
|
246
|
-
.filter((f) => /^\d{14}_.+\.sql$/.test(f))
|
|
247
|
-
.map((f) => f.slice(0, 14))
|
|
248
|
-
.sort()
|
|
249
|
-
.pop() ?? null;
|
|
250
|
-
} catch {
|
|
251
|
-
// Provider package not installed in this deployment.
|
|
252
|
-
}
|
|
253
|
-
if (!expected) {
|
|
254
|
-
return red(
|
|
255
|
-
'@selvajs/supabase-provider is not installed, so the migration SQL is missing. ' +
|
|
256
|
-
'Run `selva migrate` to add it, then `npm install`.'
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const controller = new AbortController();
|
|
262
|
-
const timer = setTimeout(() => controller.abort(), 4000);
|
|
263
|
-
const res = await fetch(env.SUPABASE_URL + '/rest/v1/rpc/migration_head', {
|
|
264
|
-
method: 'POST',
|
|
265
|
-
headers: {
|
|
266
|
-
apikey: env.SUPABASE_SERVICE_ROLE_KEY,
|
|
267
|
-
Authorization: `Bearer ${env.SUPABASE_SERVICE_ROLE_KEY}`,
|
|
268
|
-
'Content-Type': 'application/json',
|
|
269
|
-
// The function lives in the selva schema the app is pinned to.
|
|
270
|
-
'Content-Profile': 'selva'
|
|
271
|
-
},
|
|
272
|
-
body: '{}',
|
|
273
|
-
signal: controller.signal
|
|
274
|
-
});
|
|
275
|
-
clearTimeout(timer);
|
|
276
|
-
if (res.status === 404) {
|
|
277
|
-
return red(
|
|
278
|
-
`Database is missing selva.migration_head() — migrations are pending (expected head ${expected}). Run: npx selva-supabase && npx supabase db push`
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
if (!res.ok) {
|
|
282
|
-
return yellow(`migration_head RPC responded ${res.status} — cannot verify schema head`);
|
|
283
|
-
}
|
|
284
|
-
const actual = String((await res.json()) ?? '');
|
|
285
|
-
if (!actual) {
|
|
286
|
-
return red(
|
|
287
|
-
`Database reports no applied migrations (expected head ${expected}). Run: npx selva-supabase && npx supabase db push`
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
// String comparison — relies on both heads being fixed-width, zero-padded timestamps.
|
|
291
|
-
if (actual < expected) {
|
|
292
|
-
return red(
|
|
293
|
-
`Database migration head ${actual} is behind the installed provider (${expected}). Run: npx selva-supabase && npx supabase db push`
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
return green(`Database migration head ${actual} matches the installed provider`);
|
|
297
|
-
} catch (err) {
|
|
298
|
-
return yellow(`Migration-head check skipped (${err.message ?? err})`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
232
|
function checkPackage(dir, name) {
|
|
303
233
|
const path = join(dir, 'node_modules', ...name.split('/'), 'package.json');
|
|
304
234
|
if (!existsSync(path)) return red(`${name} not installed (run npm install)`);
|
package/src/commands/pm2.js
CHANGED
|
@@ -8,6 +8,9 @@ import { spawnSync, execSync } from 'node:child_process';
|
|
|
8
8
|
import * as p from '@clack/prompts';
|
|
9
9
|
import pc from 'picocolors';
|
|
10
10
|
import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
11
|
+
import { readEnvFile } from '../env.js';
|
|
12
|
+
import { needsSupabaseProvider } from '../deployment-package.js';
|
|
13
|
+
import { checkSupabaseMigrations, providerTagFor } from '../checks/migrations.js';
|
|
11
14
|
|
|
12
15
|
export const APP_NAME = 'selva-compute';
|
|
13
16
|
|
|
@@ -147,10 +150,10 @@ export async function runUpdate() {
|
|
|
147
150
|
const before = readRuntimeVersion(dir);
|
|
148
151
|
p.log.info(`Current @selvajs/selva: ${before ?? 'unknown'}`);
|
|
149
152
|
|
|
150
|
-
//
|
|
151
|
-
// an operator install carries are the runtime and the CLI. The admin-center
|
|
152
|
-
// "Run update" button runs the same list — keep them in sync if you edit.
|
|
153
|
+
// The admin-center "Run update" button runs the same list: keep them in sync.
|
|
153
154
|
const packages = ['@selvajs/cli', '@selvajs/selva'];
|
|
155
|
+
const env = readEnvFile(join(dir, '.env'));
|
|
156
|
+
const supabase = needsSupabaseProvider(env);
|
|
154
157
|
|
|
155
158
|
const confirmed = await p.confirm({
|
|
156
159
|
message: 'Refresh all @selvajs/* packages and restart the app?',
|
|
@@ -185,6 +188,21 @@ export async function runUpdate() {
|
|
|
185
188
|
const after = readRuntimeVersion(dir);
|
|
186
189
|
p.log.info(`New @selvajs/selva: ${after ?? 'unknown'}`);
|
|
187
190
|
|
|
191
|
+
if (supabase) {
|
|
192
|
+
// Provider code is bundled into @selvajs/selva, but the migration SQL ships
|
|
193
|
+
// here. Left behind, `selva-supabase` copies nothing new. Installed by tag,
|
|
194
|
+
// not `npm update`d: its pin can sit on another channel than the runtime's.
|
|
195
|
+
const spec = `@selvajs/supabase-provider@${providerTagFor(after)}`;
|
|
196
|
+
s.start(`npm install ${spec}`);
|
|
197
|
+
try {
|
|
198
|
+
execSync(`npm install --save --prefer-online ${spec}`, { cwd: dir, stdio: 'pipe' });
|
|
199
|
+
s.stop(`${spec} installed`);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
s.stop(`npm install ${spec} failed`);
|
|
202
|
+
p.log.warn(String(err.stderr ?? err.message ?? err).trim());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
if (before && after && before === after) {
|
|
189
207
|
p.log.warn(
|
|
190
208
|
[
|
|
@@ -199,12 +217,68 @@ export async function runUpdate() {
|
|
|
199
217
|
);
|
|
200
218
|
}
|
|
201
219
|
|
|
220
|
+
// Migrations are not applied here: they aren't reversible, and the old build
|
|
221
|
+
// is already gone. The app serves on an old schema (degraded), so start it and
|
|
222
|
+
// say what is left to do.
|
|
223
|
+
const schema = supabase ? await checkSupabaseMigrations(dir, env, after) : null;
|
|
224
|
+
|
|
202
225
|
const status = runPm2(dir, ['start', APP_NAME, '--update-env'], { inherit: false });
|
|
203
|
-
if (status
|
|
204
|
-
p.outro(pc.green('Started ' + APP_NAME));
|
|
205
|
-
} else {
|
|
226
|
+
if (status !== 0) {
|
|
206
227
|
p.outro(pc.yellow(`Start failed — investigate with \`pm2 logs ${APP_NAME}\`.`));
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const health = await waitForHealth(env);
|
|
233
|
+
if (schema && schema.severity !== 'green') p.log.message(schema.line);
|
|
234
|
+
if (health.ok) {
|
|
235
|
+
p.outro(pc.green(`Started ${APP_NAME}`));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
p.outro(
|
|
239
|
+
pc.red(
|
|
240
|
+
health.status
|
|
241
|
+
? `Started ${APP_NAME}, but /api/health returned ${health.status}: ${health.reason}`
|
|
242
|
+
: `Started ${APP_NAME}, but /api/health did not answer within 30s. Check \`pm2 logs ${APP_NAME}\`.`
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The app was stopped before the update, so the first answer is the new process.
|
|
249
|
+
async function waitForHealth(env, timeoutMs = 30_000) {
|
|
250
|
+
const host = !env.HOST || env.HOST === '0.0.0.0' || env.HOST === '::' ? '127.0.0.1' : env.HOST;
|
|
251
|
+
const url = `http://${host.includes(':') ? `[${host}]` : host}:${env.PORT || 3000}/api/health`;
|
|
252
|
+
const deadline = Date.now() + timeoutMs;
|
|
253
|
+
while (Date.now() < deadline) {
|
|
254
|
+
try {
|
|
255
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
256
|
+
if (res.ok) return { ok: true };
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
status: res.status,
|
|
260
|
+
reason: degradedReason(await res.json().catch(() => null))
|
|
261
|
+
};
|
|
262
|
+
} catch {
|
|
263
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { ok: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Names the cause a 503 from /api/health reports in `boot`. */
|
|
270
|
+
export function degradedReason(body) {
|
|
271
|
+
const boot = body?.boot;
|
|
272
|
+
if (boot?.schemaVersion?.ok === false) {
|
|
273
|
+
return (
|
|
274
|
+
boot.schemaVersion.message ??
|
|
275
|
+
`database migration head ${boot.schemaVersion.actual} is behind ${boot.schemaVersion.expected}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (boot?.atRestSecrets?.ok === false) {
|
|
279
|
+
return 'stored compute API keys cannot be decrypted under the current SELVA_AT_REST_KEY';
|
|
207
280
|
}
|
|
281
|
+
return 'see `pm2 logs selva-compute`';
|
|
208
282
|
}
|
|
209
283
|
|
|
210
284
|
function readRuntimeVersion(dir) {
|
package/src/generated/config.js
CHANGED
|
@@ -260,6 +260,11 @@ function buildConfigReport(manifest, env, options = {}) {
|
|
|
260
260
|
const raw = k.source === 'alias' ? env[k.alias] : env[spec.key];
|
|
261
261
|
return {
|
|
262
262
|
key: spec.key,
|
|
263
|
+
...(spec.label ? { label: spec.label } : {}),
|
|
264
|
+
type: spec.type,
|
|
265
|
+
required: spec.required === true,
|
|
266
|
+
...(spec.type === 'int' && spec.zero ? { zero: spec.zero } : {}),
|
|
267
|
+
...(spec.type === 'int' && spec.unit ? { unit: spec.unit } : {}),
|
|
263
268
|
class: spec.class,
|
|
264
269
|
summary: spec.summary === true,
|
|
265
270
|
description: spec.description,
|
|
@@ -407,9 +412,10 @@ Object.freeze({
|
|
|
407
412
|
maxPerWindow: 20
|
|
408
413
|
});
|
|
409
414
|
// ../server/src/config/settings.ts
|
|
410
|
-
var flag = (key, description, impact) =>
|
|
415
|
+
var flag = (key, label, description, impact) =>
|
|
411
416
|
defineSetting({
|
|
412
417
|
key: `SELVA_FLAG_${key}`,
|
|
418
|
+
label,
|
|
413
419
|
type: 'bool',
|
|
414
420
|
default: false,
|
|
415
421
|
onInvalid: 'fail',
|
|
@@ -417,9 +423,10 @@ var flag = (key, description, impact) =>
|
|
|
417
423
|
description,
|
|
418
424
|
...(impact ? { impact } : {})
|
|
419
425
|
});
|
|
420
|
-
var tunableInt = (key, fallback, description, extra = {}) =>
|
|
426
|
+
var tunableInt = (key, fallback, label, description, extra = {}) =>
|
|
421
427
|
defineSetting({
|
|
422
428
|
key,
|
|
429
|
+
label,
|
|
423
430
|
type: 'int',
|
|
424
431
|
min: 1,
|
|
425
432
|
default: fallback,
|
|
@@ -430,31 +437,50 @@ var tunableInt = (key, fallback, description, extra = {}) =>
|
|
|
430
437
|
});
|
|
431
438
|
var MB = 1048576;
|
|
432
439
|
var ENGINE_SETTINGS = composeManifest([
|
|
433
|
-
...[
|
|
440
|
+
...[
|
|
441
|
+
[
|
|
442
|
+
'AUTH',
|
|
443
|
+
'Sign-in',
|
|
444
|
+
'Who checks passwords: local (this server), supabase, or header (your SSO proxy).'
|
|
445
|
+
],
|
|
446
|
+
[
|
|
447
|
+
'DATA',
|
|
448
|
+
'Database',
|
|
449
|
+
'Where users, orgs and projects live: local (files on this server) or supabase.'
|
|
450
|
+
],
|
|
451
|
+
[
|
|
452
|
+
'STORAGE',
|
|
453
|
+
'File storage',
|
|
454
|
+
'Where uploaded .gh files and images live: local (this server) or supabase.'
|
|
455
|
+
]
|
|
456
|
+
].map(([slot, label, description]) =>
|
|
434
457
|
defineSetting({
|
|
435
458
|
key: `SELVA_${slot}_PROVIDER`,
|
|
459
|
+
label,
|
|
436
460
|
type: 'string',
|
|
437
461
|
default: 'local',
|
|
438
462
|
onInvalid: 'fail',
|
|
439
463
|
class: 'bootstrap',
|
|
440
464
|
summary: true,
|
|
441
|
-
description
|
|
465
|
+
description,
|
|
442
466
|
impact:
|
|
443
467
|
'Switching it does not move any data: the instance starts on what the new provider holds.'
|
|
444
468
|
})
|
|
445
469
|
),
|
|
446
470
|
defineSetting({
|
|
447
471
|
key: 'SELVA_TENANCY',
|
|
472
|
+
label: 'Organizations',
|
|
448
473
|
type: 'enum',
|
|
449
474
|
values: ['single', 'multi'],
|
|
450
475
|
default: 'single',
|
|
451
476
|
onInvalid: 'fail',
|
|
452
477
|
class: 'bootstrap',
|
|
453
478
|
summary: true,
|
|
454
|
-
description: 'single:
|
|
479
|
+
description: 'single: everyone shares one organization. multi: users can create their own.'
|
|
455
480
|
}),
|
|
456
481
|
defineSetting({
|
|
457
482
|
key: 'SELVA_CONFIG_PATH',
|
|
483
|
+
label: 'Custom config file',
|
|
458
484
|
type: 'string',
|
|
459
485
|
onInvalid: 'fail',
|
|
460
486
|
class: 'bootstrap',
|
|
@@ -462,6 +488,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
462
488
|
}),
|
|
463
489
|
defineSetting({
|
|
464
490
|
key: 'SELVA_HMAC_KEY',
|
|
491
|
+
label: 'Signing key',
|
|
465
492
|
type: 'string',
|
|
466
493
|
minLength: 32,
|
|
467
494
|
required: true,
|
|
@@ -470,12 +497,14 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
470
497
|
sensitive: true,
|
|
471
498
|
generated: true,
|
|
472
499
|
summary: true,
|
|
473
|
-
description:
|
|
500
|
+
description:
|
|
501
|
+
'Stops sign-ins, links and tokens from being forged. Generated at install; never share it.',
|
|
474
502
|
impact:
|
|
475
503
|
'Changing it without SELVA_HMAC_KEY_PREVIOUS breaks what it signed: run `selva keys rotate hmac`.'
|
|
476
504
|
}),
|
|
477
505
|
defineSetting({
|
|
478
506
|
key: 'SELVA_AT_REST_KEY',
|
|
507
|
+
label: 'Encryption key',
|
|
479
508
|
type: 'key',
|
|
480
509
|
required: true,
|
|
481
510
|
onInvalid: 'fail',
|
|
@@ -483,12 +512,13 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
483
512
|
sensitive: true,
|
|
484
513
|
generated: true,
|
|
485
514
|
summary: true,
|
|
486
|
-
description: 'Encrypts
|
|
515
|
+
description: 'Encrypts the Rhino.Compute API keys stored here. Generated at install.',
|
|
487
516
|
impact:
|
|
488
517
|
'Changing it without SELVA_AT_REST_KEY_PREVIOUS makes stored API keys unreadable: run `selva keys rotate at-rest`.'
|
|
489
518
|
}),
|
|
490
519
|
defineSetting({
|
|
491
520
|
key: 'SELVA_HMAC_KEY_PREVIOUS',
|
|
521
|
+
label: 'Previous signing key',
|
|
492
522
|
type: 'string',
|
|
493
523
|
minLength: 32,
|
|
494
524
|
onInvalid: 'fail',
|
|
@@ -496,11 +526,12 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
496
526
|
sensitive: true,
|
|
497
527
|
generated: true,
|
|
498
528
|
summary: true,
|
|
499
|
-
description: '
|
|
529
|
+
description: 'Kept after a rotation so what the old signing key signed works until it expires.',
|
|
500
530
|
impact: 'Removing it ends sessions, MCP clients and links signed before the rotation.'
|
|
501
531
|
}),
|
|
502
532
|
defineSetting({
|
|
503
533
|
key: 'SELVA_AT_REST_KEY_PREVIOUS',
|
|
534
|
+
label: 'Previous encryption key',
|
|
504
535
|
type: 'key',
|
|
505
536
|
onInvalid: 'fail',
|
|
506
537
|
class: 'secret',
|
|
@@ -508,11 +539,12 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
508
539
|
generated: true,
|
|
509
540
|
summary: true,
|
|
510
541
|
description:
|
|
511
|
-
'
|
|
542
|
+
'Kept after a rotation until stored API keys are re-encrypted, which happens at the next start.',
|
|
512
543
|
impact: 'Remove it only once /admin/system shows no API key on the previous key.'
|
|
513
544
|
}),
|
|
514
545
|
defineSetting({
|
|
515
546
|
key: 'SELVA_HMAC_KEY_PREVIOUS_EXPIRES',
|
|
547
|
+
label: 'Previous signing key expiry',
|
|
516
548
|
type: 'string',
|
|
517
549
|
onInvalid: 'fail',
|
|
518
550
|
class: 'secret',
|
|
@@ -523,6 +555,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
523
555
|
}),
|
|
524
556
|
defineSetting({
|
|
525
557
|
key: 'SELVA_AT_REST_KEY_PREVIOUS_EXPIRES',
|
|
558
|
+
label: 'Previous encryption key expiry',
|
|
526
559
|
type: 'string',
|
|
527
560
|
onInvalid: 'fail',
|
|
528
561
|
class: 'secret',
|
|
@@ -533,49 +566,61 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
533
566
|
}),
|
|
534
567
|
defineSetting({
|
|
535
568
|
key: 'BOOTSTRAP_INSTANCE_ADMIN_EMAIL',
|
|
569
|
+
label: 'Recovery admin email',
|
|
536
570
|
type: 'string',
|
|
537
571
|
onInvalid: 'fail',
|
|
538
572
|
class: 'bootstrap',
|
|
539
573
|
sensitive: true,
|
|
540
574
|
summary: true,
|
|
541
575
|
description:
|
|
542
|
-
'
|
|
576
|
+
'While no instance admin exists, only this address may become one. For first setup and lockouts.'
|
|
543
577
|
}),
|
|
544
578
|
defineSetting({
|
|
545
579
|
key: 'SIGN_IN_MAX_AGE_DAYS',
|
|
580
|
+
label: 'Force sign-in again after',
|
|
546
581
|
type: 'int',
|
|
547
582
|
min: 0,
|
|
548
583
|
default: 0,
|
|
549
584
|
onInvalid: 'fail',
|
|
550
585
|
class: 'setting',
|
|
551
586
|
summary: true,
|
|
552
|
-
|
|
587
|
+
zero: 'Never',
|
|
588
|
+
unit: 'days',
|
|
589
|
+
description: 'Days before users must sign in at the identity provider again. 0 is never.',
|
|
553
590
|
impact: 'API tokens of anyone who has not signed in within this many days are revoked.'
|
|
554
591
|
}),
|
|
555
|
-
flag('ALLOW_ORG_CREATION', 'Self-service org creation (read by host apps).'),
|
|
592
|
+
flag('ALLOW_ORG_CREATION', 'Users create orgs', 'Self-service org creation (read by host apps).'),
|
|
556
593
|
flag(
|
|
557
594
|
'ALLOW_CROSS_ORG_PUBLIC',
|
|
595
|
+
'Public across orgs',
|
|
558
596
|
'Public projects are visible to every signed-in user, not just the publishing org.',
|
|
559
597
|
'Turning it on widens who sees existing public projects.'
|
|
560
598
|
),
|
|
561
|
-
flag(
|
|
599
|
+
flag(
|
|
600
|
+
'ALLOW_ORG_COMPUTE_OVERRIDE',
|
|
601
|
+
'Orgs bring their own compute',
|
|
602
|
+
'Orgs may configure their own Rhino.Compute server.'
|
|
603
|
+
),
|
|
562
604
|
flag(
|
|
563
605
|
'ENABLE_SHARING',
|
|
606
|
+
'Share links',
|
|
564
607
|
'Per-definition share links for anonymous access.',
|
|
565
608
|
'Turning it off stops every existing share link from resolving.'
|
|
566
609
|
),
|
|
567
610
|
flag(
|
|
568
611
|
'ENABLE_PLATFORM_PROJECTS',
|
|
612
|
+
'Platform projects',
|
|
569
613
|
'Projects owned by instance admins and granted to orgs or users.'
|
|
570
614
|
),
|
|
571
615
|
...[
|
|
572
|
-
['NAME', 'Product name in the header and titles.'],
|
|
573
|
-
['COPYRIGHT_NAME', 'Footer copyright owner. Defaults to the brand name.'],
|
|
574
|
-
['TAGLINE', 'Subtitle on the landing page.'],
|
|
575
|
-
['DESCRIPTION', 'Meta description for search and link previews.']
|
|
576
|
-
].map(([key, description]) =>
|
|
616
|
+
['NAME', 'Product name', 'Product name in the header and titles.'],
|
|
617
|
+
['COPYRIGHT_NAME', 'Copyright holder', 'Footer copyright owner. Defaults to the brand name.'],
|
|
618
|
+
['TAGLINE', 'Tagline', 'Subtitle on the landing page.'],
|
|
619
|
+
['DESCRIPTION', 'Site description', 'Meta description for search and link previews.']
|
|
620
|
+
].map(([key, label, description]) =>
|
|
577
621
|
defineSetting({
|
|
578
622
|
key: `SELVA_BRAND_${key}`,
|
|
623
|
+
label,
|
|
579
624
|
type: 'string',
|
|
580
625
|
onInvalid: 'fail',
|
|
581
626
|
class: 'setting',
|
|
@@ -584,6 +629,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
584
629
|
),
|
|
585
630
|
defineSetting({
|
|
586
631
|
key: 'SMTP_HOST',
|
|
632
|
+
label: 'Mail server',
|
|
587
633
|
type: 'string',
|
|
588
634
|
onInvalid: 'fail',
|
|
589
635
|
class: 'setting',
|
|
@@ -591,6 +637,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
591
637
|
}),
|
|
592
638
|
defineSetting({
|
|
593
639
|
key: 'SMTP_PORT',
|
|
640
|
+
label: 'Mail server port',
|
|
594
641
|
type: 'int',
|
|
595
642
|
min: 1,
|
|
596
643
|
max: 65535,
|
|
@@ -601,6 +648,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
601
648
|
}),
|
|
602
649
|
defineSetting({
|
|
603
650
|
key: 'SMTP_USER',
|
|
651
|
+
label: 'Mail username',
|
|
604
652
|
type: 'string',
|
|
605
653
|
onInvalid: 'fail',
|
|
606
654
|
class: 'setting',
|
|
@@ -609,6 +657,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
609
657
|
}),
|
|
610
658
|
defineSetting({
|
|
611
659
|
key: 'SMTP_PASS',
|
|
660
|
+
label: 'Mail password',
|
|
612
661
|
type: 'string',
|
|
613
662
|
onInvalid: 'fail',
|
|
614
663
|
class: 'secret',
|
|
@@ -617,6 +666,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
617
666
|
}),
|
|
618
667
|
defineSetting({
|
|
619
668
|
key: 'SMTP_FROM',
|
|
669
|
+
label: 'Sender address',
|
|
620
670
|
type: 'string',
|
|
621
671
|
onInvalid: 'fail',
|
|
622
672
|
class: 'setting',
|
|
@@ -625,52 +675,123 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
625
675
|
}),
|
|
626
676
|
defineSetting({
|
|
627
677
|
key: 'SMTP_SECURE',
|
|
678
|
+
label: 'Mail TLS mode',
|
|
628
679
|
type: 'bool',
|
|
629
680
|
onInvalid: 'fail',
|
|
630
681
|
class: 'tunable',
|
|
631
682
|
description: 'Override the TLS mode inferred from the port.',
|
|
632
683
|
legacy: { bool: 'exact-true' }
|
|
633
684
|
}),
|
|
634
|
-
tunableInt('COMPUTE_SOLVE_DEADLINE_MS', 1e5, 'Longest one solve may run.', {
|
|
685
|
+
tunableInt('COMPUTE_SOLVE_DEADLINE_MS', 1e5, 'Solve time limit', 'Longest one solve may run.', {
|
|
686
|
+
unit: 'ms',
|
|
635
687
|
aliases: ['MAX_SOLVE_DURATION_MS'],
|
|
636
688
|
impact: 'Past the reverse proxy read timeout, solves fail there with a 502 instead.'
|
|
637
689
|
}),
|
|
638
|
-
tunableInt(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
tunableInt(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
690
|
+
tunableInt(
|
|
691
|
+
'COMPUTE_RATE_LIMIT_WINDOW_MS',
|
|
692
|
+
1e5,
|
|
693
|
+
'Solve rate limit window',
|
|
694
|
+
'Compute rate-limit window.',
|
|
695
|
+
{ unit: 'ms' }
|
|
696
|
+
),
|
|
697
|
+
tunableInt(
|
|
698
|
+
'COMPUTE_RATE_LIMIT_MAX',
|
|
699
|
+
120,
|
|
700
|
+
'Solves per window',
|
|
701
|
+
'Solves per user or share link per window.'
|
|
702
|
+
),
|
|
703
|
+
tunableInt('MAX_DEFINITION_FILE_SIZE_BYTES', 50 * MB, 'Max .gh upload', 'Largest .gh upload.', {
|
|
704
|
+
unit: 'bytes'
|
|
649
705
|
}),
|
|
706
|
+
tunableInt(
|
|
707
|
+
'MAX_IMAGE_FILE_SIZE_BYTES',
|
|
708
|
+
10 * MB,
|
|
709
|
+
'Max image upload',
|
|
710
|
+
'Largest cover image upload.',
|
|
711
|
+
{ unit: 'bytes' }
|
|
712
|
+
),
|
|
713
|
+
tunableInt(
|
|
714
|
+
'COMPUTE_REQUEST_MAX_BYTES',
|
|
715
|
+
256 * MB,
|
|
716
|
+
'Max solve request',
|
|
717
|
+
'Largest solve request body.',
|
|
718
|
+
{
|
|
719
|
+
unit: 'bytes',
|
|
720
|
+
impact: 'Must stay at or below BODY_SIZE_LIMIT, or that limit rejects first.'
|
|
721
|
+
}
|
|
722
|
+
),
|
|
723
|
+
tunableInt(
|
|
724
|
+
'COMPUTE_RESPONSE_MAX_BYTES',
|
|
725
|
+
300 * MB,
|
|
726
|
+
'Max solve response',
|
|
727
|
+
'Largest solve response body.',
|
|
728
|
+
{ unit: 'bytes' }
|
|
729
|
+
),
|
|
730
|
+
tunableInt(
|
|
731
|
+
'REMOTE_DEFINITION_FETCH_TIMEOUT_MS',
|
|
732
|
+
3e4,
|
|
733
|
+
'Remote .gh fetch timeout',
|
|
734
|
+
'Deadline for fetching a remote .gh.',
|
|
735
|
+
{ unit: 'ms' }
|
|
736
|
+
),
|
|
737
|
+
tunableInt(
|
|
738
|
+
'REMOTE_DEFINITION_CACHE_TTL_MS',
|
|
739
|
+
3e5,
|
|
740
|
+
'Remote .gh cache time',
|
|
741
|
+
'How long remote .gh bytes stay cached.',
|
|
742
|
+
{
|
|
743
|
+
unit: 'ms',
|
|
744
|
+
aliases: ['DEFINITION_CACHE_TTL_MS']
|
|
745
|
+
}
|
|
746
|
+
),
|
|
650
747
|
tunableInt(
|
|
651
748
|
'COMPUTE_MAX_QUEUE_DEPTH',
|
|
652
749
|
0,
|
|
750
|
+
'Solve queue length',
|
|
653
751
|
'Solves allowed to wait per compute server. 0 is unbounded.',
|
|
654
|
-
{
|
|
752
|
+
{
|
|
753
|
+
min: 0,
|
|
754
|
+
zero: 'No limit'
|
|
755
|
+
}
|
|
756
|
+
),
|
|
757
|
+
tunableInt(
|
|
758
|
+
'COMPUTE_QUEUE_WAIT_MS',
|
|
759
|
+
0,
|
|
760
|
+
'Max queue wait',
|
|
761
|
+
'Longest a solve may wait queued. 0 is no deadline.',
|
|
762
|
+
{
|
|
763
|
+
min: 0,
|
|
764
|
+
unit: 'ms',
|
|
765
|
+
zero: 'No limit'
|
|
766
|
+
}
|
|
767
|
+
),
|
|
768
|
+
tunableInt(
|
|
769
|
+
'COMPUTE_DEFINITION_CACHE_MB',
|
|
770
|
+
256,
|
|
771
|
+
'Definition cache size',
|
|
772
|
+
'Definition cache budget. 0 turns it off.',
|
|
773
|
+
{
|
|
774
|
+
min: 0,
|
|
775
|
+
unit: 'mb',
|
|
776
|
+
zero: 'Off',
|
|
777
|
+
aliases: ['COMPUTE_DEFINITION_BYTE_CACHE_MB']
|
|
778
|
+
}
|
|
655
779
|
),
|
|
656
|
-
tunableInt('COMPUTE_QUEUE_WAIT_MS', 0, 'Longest a solve may wait queued. 0 is no deadline.', {
|
|
657
|
-
min: 0
|
|
658
|
-
}),
|
|
659
|
-
tunableInt('COMPUTE_DEFINITION_CACHE_MB', 256, 'Definition cache budget. 0 turns it off.', {
|
|
660
|
-
min: 0,
|
|
661
|
-
aliases: ['COMPUTE_DEFINITION_BYTE_CACHE_MB']
|
|
662
|
-
}),
|
|
663
780
|
tunableInt(
|
|
664
781
|
'COMPUTE_SOLVE_CACHE_MB',
|
|
665
782
|
256,
|
|
783
|
+
'Solve cache size',
|
|
666
784
|
'Solve cache budget per warm compute server. 0 turns it off.',
|
|
667
785
|
{
|
|
668
786
|
min: 0,
|
|
787
|
+
unit: 'mb',
|
|
788
|
+
zero: 'Off',
|
|
669
789
|
aliases: ['COMPUTE_RESPONSE_CACHE_MB']
|
|
670
790
|
}
|
|
671
791
|
),
|
|
672
792
|
defineSetting({
|
|
673
793
|
key: 'COMPUTE_REUSE_DEFINITION_CACHE',
|
|
794
|
+
label: 'Reuse compute server definition cache',
|
|
674
795
|
type: 'bool',
|
|
675
796
|
default: true,
|
|
676
797
|
onInvalid: 'default',
|
|
@@ -681,6 +802,7 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
681
802
|
}),
|
|
682
803
|
defineSetting({
|
|
683
804
|
key: 'COMPUTE_SERVER_CACHESOLVE',
|
|
805
|
+
label: 'Compute server solve cache',
|
|
684
806
|
type: 'bool',
|
|
685
807
|
default: true,
|
|
686
808
|
onInvalid: 'default',
|
|
@@ -690,19 +812,26 @@ var ENGINE_SETTINGS = composeManifest([
|
|
|
690
812
|
tunableInt(
|
|
691
813
|
'API_TOKEN_RATE_LIMIT_WINDOW_MS',
|
|
692
814
|
DEFAULT_API_RATE_LIMIT.windowMs,
|
|
693
|
-
'API
|
|
815
|
+
'API rate limit window',
|
|
816
|
+
'API token rate-limit window.',
|
|
817
|
+
{ unit: 'ms' }
|
|
694
818
|
),
|
|
695
819
|
tunableInt(
|
|
696
820
|
'API_TOKEN_RATE_LIMIT_MAX',
|
|
697
821
|
DEFAULT_API_RATE_LIMIT.maxPerWindow,
|
|
822
|
+
'API requests per window',
|
|
698
823
|
'Requests per API token per window. 0 is off.',
|
|
699
|
-
{
|
|
824
|
+
{
|
|
825
|
+
min: 0,
|
|
826
|
+
zero: 'Off'
|
|
827
|
+
}
|
|
700
828
|
)
|
|
701
829
|
]);
|
|
702
830
|
// ../providers/header-auth/src/config.ts
|
|
703
831
|
var header = (key, fallback, slot) =>
|
|
704
832
|
defineSetting({
|
|
705
833
|
key,
|
|
834
|
+
label: `${slot[0].toUpperCase()}${slot.slice(1)} header`,
|
|
706
835
|
type: 'string',
|
|
707
836
|
default: fallback,
|
|
708
837
|
onInvalid: 'fail',
|
|
@@ -713,6 +842,7 @@ var header = (key, fallback, slot) =>
|
|
|
713
842
|
var HEADER_AUTH_SETTINGS = composeManifest([
|
|
714
843
|
defineSetting({
|
|
715
844
|
key: 'HEADER_AUTH_DATA_DIR',
|
|
845
|
+
label: 'Allowlist folder',
|
|
716
846
|
type: 'string',
|
|
717
847
|
onInvalid: 'fail',
|
|
718
848
|
class: 'bootstrap',
|
|
@@ -726,12 +856,13 @@ var HEADER_AUTH_SETTINGS = composeManifest([
|
|
|
726
856
|
var LOCAL_SETTINGS = composeManifest([
|
|
727
857
|
defineSetting({
|
|
728
858
|
key: 'DATA_PATH',
|
|
859
|
+
label: 'Data folder',
|
|
729
860
|
type: 'string',
|
|
730
861
|
required: true,
|
|
731
862
|
onInvalid: 'fail',
|
|
732
863
|
class: 'bootstrap',
|
|
733
864
|
summary: true,
|
|
734
|
-
description: '
|
|
865
|
+
description: 'Folder on this server holding users, orgs, projects and uploads. Back it up.',
|
|
735
866
|
impact: 'Pointing it elsewhere starts an empty instance; the old data stays where it was.'
|
|
736
867
|
})
|
|
737
868
|
]);
|
|
@@ -739,6 +870,7 @@ var LOCAL_SETTINGS = composeManifest([
|
|
|
739
870
|
var SUPABASE_SETTINGS = composeManifest([
|
|
740
871
|
defineSetting({
|
|
741
872
|
key: 'SUPABASE_URL',
|
|
873
|
+
label: 'Supabase project',
|
|
742
874
|
type: 'url',
|
|
743
875
|
required: true,
|
|
744
876
|
onInvalid: 'fail',
|
|
@@ -749,6 +881,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
749
881
|
}),
|
|
750
882
|
defineSetting({
|
|
751
883
|
key: 'SUPABASE_ANON_KEY',
|
|
884
|
+
label: 'Supabase publishable key',
|
|
752
885
|
type: 'string',
|
|
753
886
|
required: true,
|
|
754
887
|
onInvalid: 'fail',
|
|
@@ -757,6 +890,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
757
890
|
}),
|
|
758
891
|
defineSetting({
|
|
759
892
|
key: 'SUPABASE_SERVICE_ROLE_KEY',
|
|
893
|
+
label: 'Supabase secret key',
|
|
760
894
|
type: 'string',
|
|
761
895
|
required: true,
|
|
762
896
|
onInvalid: 'fail',
|
|
@@ -766,6 +900,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
766
900
|
}),
|
|
767
901
|
defineSetting({
|
|
768
902
|
key: 'SUPABASE_JWT_SIGNING_KEY',
|
|
903
|
+
label: 'Supabase JWT signing key',
|
|
769
904
|
type: 'string',
|
|
770
905
|
onInvalid: 'fail',
|
|
771
906
|
class: 'secret',
|
|
@@ -775,6 +910,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
775
910
|
}),
|
|
776
911
|
defineSetting({
|
|
777
912
|
key: 'SUPABASE_JWT_SECRET',
|
|
913
|
+
label: 'Supabase JWT secret (legacy)',
|
|
778
914
|
type: 'string',
|
|
779
915
|
onInvalid: 'fail',
|
|
780
916
|
class: 'secret',
|
|
@@ -783,6 +919,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
783
919
|
}),
|
|
784
920
|
defineSetting({
|
|
785
921
|
key: 'SUPABASE_ENABLE_SELF_SIGNUP',
|
|
922
|
+
label: 'Self sign-up',
|
|
786
923
|
type: 'bool',
|
|
787
924
|
default: false,
|
|
788
925
|
onInvalid: 'fail',
|
|
@@ -792,6 +929,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
792
929
|
}),
|
|
793
930
|
defineSetting({
|
|
794
931
|
key: 'SUPABASE_ALLOW_EMAIL_LINK_SIGNUP',
|
|
932
|
+
label: 'Sign-up by email link',
|
|
795
933
|
type: 'bool',
|
|
796
934
|
default: true,
|
|
797
935
|
onInvalid: 'fail',
|
|
@@ -801,6 +939,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
801
939
|
}),
|
|
802
940
|
defineSetting({
|
|
803
941
|
key: 'SUPABASE_OAUTH_PROVIDERS',
|
|
942
|
+
label: 'OAuth sign-in buttons',
|
|
804
943
|
type: 'list',
|
|
805
944
|
default: [],
|
|
806
945
|
onInvalid: 'fail',
|
|
@@ -809,6 +948,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
809
948
|
}),
|
|
810
949
|
defineSetting({
|
|
811
950
|
key: 'SUPABASE_TOKEN_VERIFICATION',
|
|
951
|
+
label: 'Session check mode',
|
|
812
952
|
type: 'enum',
|
|
813
953
|
values: ['hybrid', 'strict'],
|
|
814
954
|
default: 'hybrid',
|
|
@@ -820,15 +960,18 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
820
960
|
}),
|
|
821
961
|
defineSetting({
|
|
822
962
|
key: 'SUPABASE_REVALIDATE_MS',
|
|
963
|
+
label: 'Session recheck interval',
|
|
823
964
|
type: 'int',
|
|
824
965
|
min: 1,
|
|
825
966
|
default: 6e4,
|
|
967
|
+
unit: 'ms',
|
|
826
968
|
onInvalid: 'default',
|
|
827
969
|
class: 'tunable',
|
|
828
970
|
description: 'Hybrid recheck window: the longest a revoked session keeps working.'
|
|
829
971
|
}),
|
|
830
972
|
defineSetting({
|
|
831
973
|
key: 'SUPABASE_PUBLIC_BUCKET',
|
|
974
|
+
label: 'Public files bucket',
|
|
832
975
|
type: 'string',
|
|
833
976
|
default: 'selva-public',
|
|
834
977
|
onInvalid: 'fail',
|
|
@@ -837,6 +980,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
837
980
|
}),
|
|
838
981
|
defineSetting({
|
|
839
982
|
key: 'SUPABASE_PRIVATE_BUCKET',
|
|
983
|
+
label: 'Private files bucket',
|
|
840
984
|
type: 'string',
|
|
841
985
|
default: 'selva-private',
|
|
842
986
|
onInvalid: 'fail',
|
|
@@ -845,6 +989,7 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
845
989
|
}),
|
|
846
990
|
defineSetting({
|
|
847
991
|
key: 'SUPABASE_PRIVATE_URL_PREFIX',
|
|
992
|
+
label: 'Private file URL prefix',
|
|
848
993
|
type: 'string',
|
|
849
994
|
default: '/api/files',
|
|
850
995
|
onInvalid: 'fail',
|
|
@@ -857,16 +1002,18 @@ var SUPABASE_SETTINGS = composeManifest([
|
|
|
857
1002
|
var APP_SETTINGS = composeManifest([
|
|
858
1003
|
defineSetting({
|
|
859
1004
|
key: 'ORIGIN',
|
|
1005
|
+
label: 'Public address',
|
|
860
1006
|
type: 'url',
|
|
861
1007
|
onInvalid: 'fail',
|
|
862
1008
|
class: 'bootstrap',
|
|
863
1009
|
summary: true,
|
|
864
|
-
description: '
|
|
1010
|
+
description: 'The URL people use to reach Selva, such as https://selva.example.com.',
|
|
865
1011
|
impact:
|
|
866
1012
|
'Wrong or unset behind a proxy: form posts fail the CSRF check and invite links carry an internal host.'
|
|
867
1013
|
}),
|
|
868
1014
|
defineSetting({
|
|
869
1015
|
key: 'PORT',
|
|
1016
|
+
label: 'Port',
|
|
870
1017
|
type: 'int',
|
|
871
1018
|
min: 1,
|
|
872
1019
|
max: 65535,
|
|
@@ -877,44 +1024,51 @@ var APP_SETTINGS = composeManifest([
|
|
|
877
1024
|
}),
|
|
878
1025
|
defineSetting({
|
|
879
1026
|
key: 'HOST',
|
|
1027
|
+
label: 'Listen address',
|
|
880
1028
|
type: 'string',
|
|
881
1029
|
default: '0.0.0.0',
|
|
882
1030
|
onInvalid: 'fail',
|
|
883
1031
|
class: 'bootstrap',
|
|
884
1032
|
summary: true,
|
|
885
|
-
description:
|
|
1033
|
+
description:
|
|
1034
|
+
'Which network address the app accepts connections on. 127.0.0.1 when a proxy sits in front.',
|
|
886
1035
|
impact: 'Under header auth, anything that reaches the port directly can claim to be any user.'
|
|
887
1036
|
}),
|
|
888
1037
|
defineSetting({
|
|
889
1038
|
key: 'BODY_SIZE_LIMIT',
|
|
1039
|
+
label: 'Max request size',
|
|
890
1040
|
type: 'string',
|
|
891
1041
|
default: '512K',
|
|
892
1042
|
onInvalid: 'fail',
|
|
893
1043
|
class: 'bootstrap',
|
|
894
1044
|
summary: true,
|
|
895
|
-
description: 'Largest request
|
|
1045
|
+
description: 'Largest request the server accepts at all, uploads included (K, M or G suffix).',
|
|
896
1046
|
impact: 'Below COMPUTE_REQUEST_MAX_BYTES, uploads and file inputs fail with 413.'
|
|
897
1047
|
}),
|
|
898
1048
|
defineSetting({
|
|
899
1049
|
key: 'ADDRESS_HEADER',
|
|
1050
|
+
label: 'Visitor IP header',
|
|
900
1051
|
type: 'string',
|
|
901
1052
|
onInvalid: 'fail',
|
|
902
1053
|
class: 'bootstrap',
|
|
903
1054
|
summary: true,
|
|
904
|
-
description:
|
|
1055
|
+
description:
|
|
1056
|
+
'Header your proxy uses to pass on the visitor’s IP address. Usually X-Forwarded-For.',
|
|
905
1057
|
impact: 'Unset behind a proxy: every client shares one rate-limit bucket.'
|
|
906
1058
|
}),
|
|
907
1059
|
defineSetting({
|
|
908
1060
|
key: 'XFF_DEPTH',
|
|
1061
|
+
label: 'Proxies in front',
|
|
909
1062
|
type: 'int',
|
|
910
1063
|
min: 1,
|
|
911
1064
|
onInvalid: 'fail',
|
|
912
1065
|
class: 'bootstrap',
|
|
913
1066
|
summary: true,
|
|
914
|
-
description: '
|
|
1067
|
+
description: 'How many proxies sit between visitors and the app. Usually 1.'
|
|
915
1068
|
}),
|
|
916
1069
|
defineSetting({
|
|
917
1070
|
key: 'PROTOCOL_HEADER',
|
|
1071
|
+
label: 'Protocol header',
|
|
918
1072
|
type: 'string',
|
|
919
1073
|
onInvalid: 'fail',
|
|
920
1074
|
class: 'bootstrap',
|
|
@@ -922,6 +1076,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
922
1076
|
}),
|
|
923
1077
|
defineSetting({
|
|
924
1078
|
key: 'HOST_HEADER',
|
|
1079
|
+
label: 'Host header',
|
|
925
1080
|
type: 'string',
|
|
926
1081
|
onInvalid: 'fail',
|
|
927
1082
|
class: 'bootstrap',
|
|
@@ -929,6 +1084,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
929
1084
|
}),
|
|
930
1085
|
defineSetting({
|
|
931
1086
|
key: 'NODE_ENV',
|
|
1087
|
+
label: 'Run mode',
|
|
932
1088
|
type: 'string',
|
|
933
1089
|
onInvalid: 'fail',
|
|
934
1090
|
class: 'bootstrap',
|
|
@@ -936,6 +1092,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
936
1092
|
}),
|
|
937
1093
|
defineSetting({
|
|
938
1094
|
key: 'ALLOW_INSECURE_COOKIES',
|
|
1095
|
+
label: 'Allow sign-in over http',
|
|
939
1096
|
type: 'bool',
|
|
940
1097
|
default: false,
|
|
941
1098
|
onInvalid: 'fail',
|
|
@@ -945,6 +1102,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
945
1102
|
}),
|
|
946
1103
|
defineSetting({
|
|
947
1104
|
key: 'INSTALL_DIR',
|
|
1105
|
+
label: 'Install folder',
|
|
948
1106
|
type: 'string',
|
|
949
1107
|
onInvalid: 'fail',
|
|
950
1108
|
class: 'bootstrap',
|
|
@@ -953,6 +1111,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
953
1111
|
}),
|
|
954
1112
|
defineSetting({
|
|
955
1113
|
key: 'SELVA_EVENT_CALLBACK_ORIGIN',
|
|
1114
|
+
label: 'Solve event callback address',
|
|
956
1115
|
type: 'url',
|
|
957
1116
|
onInvalid: 'fail',
|
|
958
1117
|
class: 'bootstrap',
|
|
@@ -961,6 +1120,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
961
1120
|
}),
|
|
962
1121
|
defineSetting({
|
|
963
1122
|
key: 'MCP_ALLOWED_ORIGINS',
|
|
1123
|
+
label: 'Extra MCP origins',
|
|
964
1124
|
type: 'list',
|
|
965
1125
|
default: [],
|
|
966
1126
|
onInvalid: 'fail',
|
|
@@ -969,6 +1129,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
969
1129
|
}),
|
|
970
1130
|
defineSetting({
|
|
971
1131
|
key: 'LOG_LEVEL',
|
|
1132
|
+
label: 'Log level',
|
|
972
1133
|
type: 'enum',
|
|
973
1134
|
values: ['debug', 'info', 'warn', 'error'],
|
|
974
1135
|
onInvalid: 'default',
|
|
@@ -977,6 +1138,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
977
1138
|
}),
|
|
978
1139
|
defineSetting({
|
|
979
1140
|
key: 'SELVA_FLAG_COMPUTE_DEBUG',
|
|
1141
|
+
label: 'Compute debug logging',
|
|
980
1142
|
type: 'string',
|
|
981
1143
|
default: 'off',
|
|
982
1144
|
onInvalid: 'default',
|
|
@@ -987,6 +1149,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
987
1149
|
}),
|
|
988
1150
|
defineSetting({
|
|
989
1151
|
key: 'SELVA_FLAG_COMPUTE_DEBUG_VERBOSE',
|
|
1152
|
+
label: 'Compute debug logging (old name)',
|
|
990
1153
|
type: 'bool',
|
|
991
1154
|
onInvalid: 'default',
|
|
992
1155
|
class: 'setting',
|
|
@@ -994,6 +1157,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
994
1157
|
}),
|
|
995
1158
|
defineSetting({
|
|
996
1159
|
key: 'SENTRY_DSN',
|
|
1160
|
+
label: 'Sentry error reporting',
|
|
997
1161
|
type: 'url',
|
|
998
1162
|
onInvalid: 'fail',
|
|
999
1163
|
class: 'secret',
|
|
@@ -1002,6 +1166,7 @@ var APP_SETTINGS = composeManifest([
|
|
|
1002
1166
|
}),
|
|
1003
1167
|
defineSetting({
|
|
1004
1168
|
key: 'SELVA_RELEASE',
|
|
1169
|
+
label: 'Release tag',
|
|
1005
1170
|
type: 'string',
|
|
1006
1171
|
onInvalid: 'fail',
|
|
1007
1172
|
class: 'tunable',
|