@selvajs/cli 4.8.0-beta.7 → 4.8.0-beta.9
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 +2 -2
- package/package.json +6 -3
- package/src/checks/boot.js +149 -0
- package/src/checks/config.js +97 -0
- package/src/checks/result.js +27 -0
- package/src/commands/create.js +20 -56
- package/src/commands/doctor.js +47 -263
- package/src/commands/init.js +2 -20
- package/src/commands/keys.js +3 -17
- package/src/commands/migrate.js +128 -91
- package/src/commands/pm2.js +10 -4
- package/src/deployment-package.js +102 -0
- package/src/node-range.js +66 -0
- package/src/paths.js +44 -0
- package/src/prompts.js +0 -2
- package/bin/create.js +0 -7
- package/src/__tests__/doctor-node-engine.test.js +0 -97
- package/src/__tests__/env-rename.test.js +0 -65
- package/src/__tests__/pm2-skew.test.js +0 -60
package/src/commands/doctor.js
CHANGED
|
@@ -5,19 +5,33 @@
|
|
|
5
5
|
// checks, each behind its own confirmation — and only those needing no root and
|
|
6
6
|
// no runtime restart. See `applyFixes`.
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
accessSync,
|
|
13
|
+
constants,
|
|
14
|
+
statSync,
|
|
15
|
+
rmSync
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { join, resolve, dirname } from 'node:path';
|
|
11
18
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { homedir } from 'node:os';
|
|
13
19
|
import * as p from '@clack/prompts';
|
|
14
20
|
import pc from 'picocolors';
|
|
15
|
-
import { readEnvFile
|
|
21
|
+
import { readEnvFile } from '../env.js';
|
|
16
22
|
import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
import { satisfiesNodeRange } from '../node-range.js';
|
|
24
|
+
import { green, yellow, red, fixable } from '../checks/result.js';
|
|
25
|
+
import { checkBootPersistence } from '../checks/boot.js';
|
|
26
|
+
import {
|
|
27
|
+
checkDeprecatedEnv,
|
|
28
|
+
checkOrigin,
|
|
29
|
+
checkProviders,
|
|
30
|
+
checkSecret,
|
|
31
|
+
checkTenancy,
|
|
32
|
+
resolveProviders
|
|
33
|
+
} from '../checks/config.js';
|
|
34
|
+
import { detectDrift, NODE_MODULES_STASH } from './migrate.js';
|
|
21
35
|
|
|
22
36
|
export async function runDoctor(argv = []) {
|
|
23
37
|
const fix = argv.includes('--fix');
|
|
@@ -32,28 +46,12 @@ export async function runDoctor(argv = []) {
|
|
|
32
46
|
checks.push(checkFile(join(dir, '.env'), '.env present'));
|
|
33
47
|
checks.push(checkFile(join(dir, 'ecosystem.config.cjs'), 'ecosystem.config.cjs present'));
|
|
34
48
|
checks.push(checkLayoutDrift(dir));
|
|
49
|
+
checks.push(checkMigrationLeftovers(dir));
|
|
35
50
|
checks.push(checkSecret(env.SELVA_HMAC_KEY, 'SELVA_HMAC_KEY is a 32-byte hex string'));
|
|
36
51
|
checks.push(checkSecret(env.SELVA_AT_REST_KEY, 'SELVA_AT_REST_KEY is a 32-byte hex string'));
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
auth: (env.SELVA_AUTH_PROVIDER ?? 'local').toLowerCase(),
|
|
41
|
-
data: (env.SELVA_DATA_PROVIDER ?? 'local').toLowerCase(),
|
|
42
|
-
storage: (env.SELVA_STORAGE_PROVIDER ?? 'local').toLowerCase()
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const validForAuth = new Set(['local', 'supabase', 'header']);
|
|
46
|
-
const validForData = new Set(['local', 'supabase']);
|
|
47
|
-
|
|
48
|
-
if (!validForAuth.has(providers.auth)) {
|
|
49
|
-
checks.push(red(`SELVA_AUTH_PROVIDER="${providers.auth}" — expected local|supabase|header`));
|
|
50
|
-
}
|
|
51
|
-
if (!validForData.has(providers.data)) {
|
|
52
|
-
checks.push(red(`SELVA_DATA_PROVIDER="${providers.data}" — expected local|supabase`));
|
|
53
|
-
}
|
|
54
|
-
if (!validForData.has(providers.storage)) {
|
|
55
|
-
checks.push(red(`SELVA_STORAGE_PROVIDER="${providers.storage}" — expected local|supabase`));
|
|
56
|
-
}
|
|
53
|
+
const providers = resolveProviders(env);
|
|
54
|
+
checks.push(...checkProviders(providers));
|
|
57
55
|
|
|
58
56
|
const used = new Set(Object.values(providers));
|
|
59
57
|
|
|
@@ -70,42 +68,14 @@ export async function runDoctor(argv = []) {
|
|
|
70
68
|
checks.push(...checkHeaderAuth(dir, env, providers.data));
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
if (tenancy !== 'single' && tenancy !== 'multi') {
|
|
75
|
-
checks.push(red(`SELVA_TENANCY="${tenancy}" — expected single|multi`));
|
|
76
|
-
} else {
|
|
77
|
-
checks.push(green(`SELVA_TENANCY=${tenancy}`));
|
|
78
|
-
}
|
|
71
|
+
checks.push(checkTenancy(env));
|
|
79
72
|
|
|
80
73
|
checks.push(checkPackage(dir, '@selvajs/selva'));
|
|
81
74
|
checks.push(checkNodeEngine(dir));
|
|
82
75
|
checks.push(checkCliRuntimeAlignment(dir));
|
|
83
76
|
checks.push(...checkBootPersistence(dir));
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
new URL(env.ORIGIN);
|
|
87
|
-
checks.push(green(`ORIGIN=${env.ORIGIN}`));
|
|
88
|
-
} catch {
|
|
89
|
-
checks.push(red(`ORIGIN="${env.ORIGIN}" is not a valid URL`));
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
checks.push(yellow('ORIGIN unset — required behind a reverse proxy'));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
for (const [oldName, newName] of Object.entries(RENAMED_ENV_VARS)) {
|
|
96
|
-
if (env[oldName] === undefined) continue;
|
|
97
|
-
checks.push(
|
|
98
|
-
env[newName] === undefined
|
|
99
|
-
? yellow(`${oldName} is deprecated — \`selva migrate\` renames it to ${newName}`)
|
|
100
|
-
: yellow(`${oldName} is deprecated and ignored — ${newName} is set and wins`)
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Not auto-fixable: the replacement encodes a value, so migrate won't guess.
|
|
105
|
-
for (const [oldName, replacement] of Object.entries(REPLACED_ENV_VARS)) {
|
|
106
|
-
if (env[oldName] === undefined) continue;
|
|
107
|
-
checks.push(yellow(`${oldName} is deprecated — replace it with ${replacement}`));
|
|
108
|
-
}
|
|
77
|
+
checks.push(checkOrigin(env));
|
|
78
|
+
checks.push(...checkDeprecatedEnv(env));
|
|
109
79
|
|
|
110
80
|
// ── Render ─────────────────────────────────────────────────────────
|
|
111
81
|
const resolved = await Promise.all(checks);
|
|
@@ -175,39 +145,10 @@ async function applyFixes(resolved, failures) {
|
|
|
175
145
|
return failures;
|
|
176
146
|
}
|
|
177
147
|
|
|
178
|
-
function green(text) {
|
|
179
|
-
return { severity: 'green', line: `${pc.green('✓')} ${text}` };
|
|
180
|
-
}
|
|
181
|
-
function yellow(text, fix) {
|
|
182
|
-
return { severity: 'yellow', line: `${pc.yellow('!')} ${text}`, fix };
|
|
183
|
-
}
|
|
184
|
-
function red(text, fix) {
|
|
185
|
-
return { severity: 'red', line: `${pc.red('✗')} ${text}`, fix };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* A repair `--fix` may run. `label` is what the operator is asked to approve;
|
|
190
|
-
* `run()` performs it and returns a result line.
|
|
191
|
-
*
|
|
192
|
-
* Only attach one where the repair is unambiguous and reversible-ish. Anything
|
|
193
|
-
* needing root, or that restarts the runtime running this process, stays a
|
|
194
|
-
* printed instruction — a half-applied privileged fix is worse than none.
|
|
195
|
-
*/
|
|
196
|
-
function fixable(label, run) {
|
|
197
|
-
return { label, run };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
148
|
function checkFile(path, label) {
|
|
201
149
|
return existsSync(path) ? green(label) : red(`${label} (missing: ${path})`);
|
|
202
150
|
}
|
|
203
151
|
|
|
204
|
-
function checkSecret(value, label) {
|
|
205
|
-
if (!value) return red(`${label} — unset`);
|
|
206
|
-
if (value === PLACEHOLDER) return red(`${label} — still the placeholder`);
|
|
207
|
-
if (!HEX_64.test(value)) return red(`${label} — not 64 hex chars`);
|
|
208
|
-
return green(label);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
152
|
function checkDataPath(dir, dataPath) {
|
|
212
153
|
const absolute = resolve(dir, dataPath);
|
|
213
154
|
try {
|
|
@@ -423,183 +364,26 @@ function checkNodeEngine(dir) {
|
|
|
423
364
|
}
|
|
424
365
|
|
|
425
366
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* dependency-free by design (it scaffolds the deployment that installs the
|
|
431
|
-
* runtime, so it cannot import from it).
|
|
367
|
+
* `selva migrate` parks the old node_modules aside so a failed install can be
|
|
368
|
+
* rolled back, and removes it on either outcome. One surviving here means the
|
|
369
|
+
* migration was killed mid-flight — worth saying, because it is a full copy of
|
|
370
|
+
* the dependency tree and the deployment may be running on the wrong one.
|
|
432
371
|
*/
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
if (!
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
for (const clause of clauses) {
|
|
446
|
-
const m = /^(>=|<=|>|<|\^|~|=)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?/.exec(clause);
|
|
447
|
-
if (!m) {
|
|
448
|
-
parsed = false;
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
const op = m[1] ?? '=';
|
|
452
|
-
const num = (s) => (s == null || s === 'x' || s === '*' ? 0 : Number(s));
|
|
453
|
-
const t = [Number(m[2]), num(m[3]), num(m[4])];
|
|
454
|
-
let cmp = 0;
|
|
455
|
-
for (let i = 0; i < 3; i++) {
|
|
456
|
-
if (v[i] > t[i]) {
|
|
457
|
-
cmp = 1;
|
|
458
|
-
break;
|
|
459
|
-
}
|
|
460
|
-
if (v[i] < t[i]) {
|
|
461
|
-
cmp = -1;
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
let ok;
|
|
466
|
-
if (op === '>=') ok = cmp >= 0;
|
|
467
|
-
else if (op === '>') ok = cmp > 0;
|
|
468
|
-
else if (op === '<=') ok = cmp <= 0;
|
|
469
|
-
else if (op === '<') ok = cmp < 0;
|
|
470
|
-
else if (op === '^') ok = cmp >= 0 && v[0] === t[0];
|
|
471
|
-
else if (op === '~') ok = cmp >= 0 && v[0] === t[0] && v[1] === t[1];
|
|
472
|
-
else
|
|
473
|
-
ok =
|
|
474
|
-
v[0] === t[0] &&
|
|
475
|
-
(m[3] == null || m[3] === 'x' || m[3] === '*' || v[1] === t[1]) &&
|
|
476
|
-
(m[4] == null || m[4] === 'x' || m[4] === '*' || v[2] === t[2]);
|
|
477
|
-
if (!ok) {
|
|
478
|
-
all = false;
|
|
479
|
-
break;
|
|
372
|
+
function checkMigrationLeftovers(dir) {
|
|
373
|
+
const stash = join(dir, NODE_MODULES_STASH);
|
|
374
|
+
if (!existsSync(stash)) return green('no interrupted migration left behind');
|
|
375
|
+
return yellow(
|
|
376
|
+
`${NODE_MODULES_STASH} exists — a \`selva migrate\` was interrupted. If the app ` +
|
|
377
|
+
`is healthy, delete it: rm -rf ${stash}`,
|
|
378
|
+
fixable(`delete the leftover ${NODE_MODULES_STASH}`, () => {
|
|
379
|
+
try {
|
|
380
|
+
rmSync(stash, { recursive: true, force: true });
|
|
381
|
+
return green(`removed ${NODE_MODULES_STASH}`);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
return red(`could not remove ${stash}: ${err instanceof Error ? err.message : err}`);
|
|
480
384
|
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
anyParsed = true;
|
|
484
|
-
if (all) return true;
|
|
485
|
-
}
|
|
486
|
-
return anyParsed ? false : null;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function checkBootPersistence(dir) {
|
|
490
|
-
// pm2's boot integration is Linux/systemd-specific. On macOS it's launchd
|
|
491
|
-
// (different unit path) and on Windows pm2 boot persistence isn't a thing —
|
|
492
|
-
// stay silent rather than emit misleading checks.
|
|
493
|
-
if (process.platform !== 'linux') return [];
|
|
494
|
-
|
|
495
|
-
const out = [];
|
|
496
|
-
|
|
497
|
-
// dump.pm2 (what pm2 startup resurrects).
|
|
498
|
-
const pm2Home = process.env.PM2_HOME ?? join(homedir(), '.pm2');
|
|
499
|
-
const dumpPath = join(pm2Home, 'dump.pm2');
|
|
500
|
-
if (existsSync(dumpPath)) {
|
|
501
|
-
out.push(green('pm2 process list saved (dump.pm2 present)'));
|
|
502
|
-
} else {
|
|
503
|
-
out.push(
|
|
504
|
-
yellow(
|
|
505
|
-
'pm2 process list not saved — run `npx pm2 save` so a reboot can ' +
|
|
506
|
-
'resurrect the app (nothing to restore without it)',
|
|
507
|
-
fixable('run `pm2 save` to persist the current process list', () => {
|
|
508
|
-
const bin = join(dir, 'node_modules', '.bin', 'pm2');
|
|
509
|
-
if (!existsSync(bin)) return red('pm2 not installed in this deployment');
|
|
510
|
-
const r = spawnSync(bin, ['save'], { cwd: dir, encoding: 'utf8' });
|
|
511
|
-
return (r.status ?? 1) === 0
|
|
512
|
-
? green('pm2 process list saved')
|
|
513
|
-
: red(`pm2 save failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
514
|
-
})
|
|
515
|
-
)
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// systemd unit: present and pointing at deployment-local pm2.
|
|
520
|
-
const user = process.env.USER ?? process.env.LOGNAME;
|
|
521
|
-
const unitPath = user ? `/etc/systemd/system/pm2-${user}.service` : null;
|
|
522
|
-
|
|
523
|
-
if (unitPath && existsSync(unitPath)) {
|
|
524
|
-
const localPm2 = join(dir, 'node_modules', 'pm2', 'bin', 'pm2');
|
|
525
|
-
let unit = '';
|
|
526
|
-
try {
|
|
527
|
-
unit = readFileSync(unitPath, 'utf8');
|
|
528
|
-
} catch {
|
|
529
|
-
out.push(yellow(`pm2 systemd unit present but unreadable (${unitPath})`));
|
|
530
|
-
return out;
|
|
531
|
-
}
|
|
532
|
-
const execStart = /^ExecStart=(.+)$/m.exec(unit)?.[1] ?? '';
|
|
533
|
-
if (execStart.includes(localPm2)) {
|
|
534
|
-
out.push(green('pm2 systemd boot unit installed (uses deployment-local pm2)'));
|
|
535
|
-
} else {
|
|
536
|
-
out.push(
|
|
537
|
-
red(
|
|
538
|
-
`pm2 systemd boot unit points at a different pm2 than this deployment's.\n ` +
|
|
539
|
-
`ExecStart: ${execStart || '(not found)'}\n ` +
|
|
540
|
-
`expected: ${localPm2} resurrect\n ` +
|
|
541
|
-
`Reboots will resurrect via the wrong pm2 (version skew). Re-run startup ` +
|
|
542
|
-
`with the local binary:\n ` +
|
|
543
|
-
`sudo env PATH=$PATH:${join(dir, 'node_modules', '.bin')} ${localPm2} ` +
|
|
544
|
-
`startup systemd -u $USER --hp $HOME`
|
|
545
|
-
)
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
} else {
|
|
549
|
-
out.push(
|
|
550
|
-
yellow(
|
|
551
|
-
'pm2 systemd boot unit not installed — the app will NOT restart after a ' +
|
|
552
|
-
'reboot. Run `npx pm2 startup systemd -u $USER --hp $HOME` and paste the ' +
|
|
553
|
-
'printed command (point it at this deployment’s pm2).'
|
|
554
|
-
)
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Warn if global pm2 is on PATH (root cause of version skew).
|
|
559
|
-
const globalPm2 = findGlobalPm2(dir);
|
|
560
|
-
if (globalPm2) {
|
|
561
|
-
// Only offer the removal when we could actually perform it. A global
|
|
562
|
-
// install under /usr is root-owned; attempting it would half-fail and
|
|
563
|
-
// leave the operator worse informed than a printed instruction.
|
|
564
|
-
let writable = false;
|
|
565
|
-
try {
|
|
566
|
-
accessSync(dirname(globalPm2), constants.W_OK);
|
|
567
|
-
writable = true;
|
|
568
|
-
} catch {
|
|
569
|
-
writable = false;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
out.push(
|
|
573
|
-
yellow(
|
|
574
|
-
`a pm2 outside this deployment is on PATH (${globalPm2}) — it can fork a ` +
|
|
575
|
-
`mismatched daemon and trigger skew. Prefer \`npm run\` wrappers / \`npx pm2\` ` +
|
|
576
|
-
`from this directory; consider \`npm uninstall -g pm2\`.` +
|
|
577
|
-
(writable ? '' : `\n (${dirname(globalPm2)} is not writable — needs sudo)`),
|
|
578
|
-
writable
|
|
579
|
-
? fixable(`uninstall the global pm2 at ${globalPm2}`, () => {
|
|
580
|
-
const r = spawnSync('npm', ['uninstall', '-g', 'pm2'], { encoding: 'utf8' });
|
|
581
|
-
return (r.status ?? 1) === 0
|
|
582
|
-
? green('global pm2 uninstalled')
|
|
583
|
-
: red(`npm uninstall -g pm2 failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
584
|
-
})
|
|
585
|
-
: undefined
|
|
586
|
-
)
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return out;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Scan PATH for stray pm2 binary (read-only).
|
|
594
|
-
function findGlobalPm2(dir) {
|
|
595
|
-
const localBin = resolve(dir, 'node_modules', '.bin');
|
|
596
|
-
const dirs = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
|
|
597
|
-
for (const d of dirs) {
|
|
598
|
-
if (resolve(d) === localBin) continue;
|
|
599
|
-
const candidate = join(d, 'pm2');
|
|
600
|
-
if (existsSync(candidate)) return candidate;
|
|
601
|
-
}
|
|
602
|
-
return null;
|
|
385
|
+
})
|
|
386
|
+
);
|
|
603
387
|
}
|
|
604
388
|
|
|
605
389
|
function checkLayoutDrift(dir) {
|
package/src/commands/init.js
CHANGED
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
// `selva keys rotate`'s job, not init's.
|
|
8
8
|
// • Doesn't touch package.json or run npm install.
|
|
9
9
|
|
|
10
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
11
|
import * as p from '@clack/prompts';
|
|
13
12
|
import pc from 'picocolors';
|
|
14
13
|
import { collectConfig } from '../prompts.js';
|
|
15
14
|
import { readEnvFile, writeEnvFile } from '../env.js';
|
|
16
15
|
import { generateKey } from '../secrets.js';
|
|
17
|
-
import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
16
|
+
import { readEnvTemplate, requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
18
17
|
|
|
19
18
|
export async function runInit() {
|
|
20
19
|
const dir = resolveDeploymentDir();
|
|
@@ -48,24 +47,7 @@ export async function runInit() {
|
|
|
48
47
|
p.log.warn('SELVA_AT_REST_KEY was missing — generated a fresh one.');
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
// canonical template. Falls back to the current .env if the runtime
|
|
53
|
-
// templates aren't present (shouldn't happen post-install).
|
|
54
|
-
const templatePath = join(
|
|
55
|
-
dir,
|
|
56
|
-
'node_modules',
|
|
57
|
-
'@selvajs',
|
|
58
|
-
'selva',
|
|
59
|
-
'templates',
|
|
60
|
-
'.env.example'
|
|
61
|
-
);
|
|
62
|
-
const template = existsSync(templatePath)
|
|
63
|
-
? readFileSync(templatePath, 'utf8')
|
|
64
|
-
: existsSync(envPath)
|
|
65
|
-
? readFileSync(envPath, 'utf8')
|
|
66
|
-
: '';
|
|
67
|
-
|
|
68
|
-
writeEnvFile(envPath, template, values);
|
|
50
|
+
writeEnvFile(envPath, readEnvTemplate(dir), values);
|
|
69
51
|
|
|
70
52
|
p.outro(
|
|
71
53
|
[
|
package/src/commands/keys.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Rotate SELVA_HMAC_KEY or SELVA_AT_REST_KEY; requires explicit confirm (blast radius in TARGETS).
|
|
2
2
|
|
|
3
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import * as p from '@clack/prompts';
|
|
6
5
|
import pc from 'picocolors';
|
|
7
6
|
import { readEnvFile, writeEnvFile } from '../env.js';
|
|
8
7
|
import { generateKey } from '../secrets.js';
|
|
9
|
-
import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
8
|
+
import { readEnvTemplate, requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
10
9
|
|
|
11
10
|
const TARGETS = {
|
|
12
11
|
hmac: {
|
|
@@ -14,8 +13,7 @@ const TARGETS = {
|
|
|
14
13
|
warning: [
|
|
15
14
|
'This will:',
|
|
16
15
|
pc.red(' • log every signed-in user out (existing session cookies stop verifying)'),
|
|
17
|
-
pc.red(' • invalidate share
|
|
18
|
-
pc.dim(' (only relevant when SHARE_LINK_SECRET / INVITE_TOKEN_SECRET are unset)')
|
|
16
|
+
pc.red(' • invalidate every share link and pending invite')
|
|
19
17
|
].join('\n')
|
|
20
18
|
},
|
|
21
19
|
'at-rest': {
|
|
@@ -58,19 +56,7 @@ export async function runKeysRotate(argv) {
|
|
|
58
56
|
const fresh = generateKey();
|
|
59
57
|
current[envVar] = fresh;
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
dir,
|
|
63
|
-
'node_modules',
|
|
64
|
-
'@selvajs',
|
|
65
|
-
'selva',
|
|
66
|
-
'templates',
|
|
67
|
-
'.env.example'
|
|
68
|
-
);
|
|
69
|
-
const template = existsSync(templatePath)
|
|
70
|
-
? readFileSync(templatePath, 'utf8')
|
|
71
|
-
: readFileSync(envPath, 'utf8');
|
|
72
|
-
|
|
73
|
-
writeEnvFile(envPath, template, current);
|
|
59
|
+
writeEnvFile(envPath, readEnvTemplate(dir), current);
|
|
74
60
|
|
|
75
61
|
p.outro(
|
|
76
62
|
[
|