@selvajs/cli 4.7.4-beta.1 → 4.8.0-beta.10
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 +7 -4
- package/src/checks/boot.js +149 -0
- package/src/checks/config.js +97 -0
- package/src/checks/result.js +27 -0
- package/src/cli.js +1 -1
- package/src/commands/create.js +38 -55
- package/src/commands/doctor.js +143 -145
- package/src/commands/init.js +2 -20
- package/src/commands/keys.js +3 -17
- package/src/commands/migrate.js +169 -98
- package/src/commands/pm2.js +50 -5
- package/src/deployment-package.js +102 -0
- package/src/env.js +65 -0
- package/src/node-range.js +66 -0
- package/src/paths.js +66 -2
- package/src/prompts.js +0 -2
- package/bin/create.js +0 -7
package/src/commands/doctor.js
CHANGED
|
@@ -1,24 +1,44 @@
|
|
|
1
1
|
// Validate deployment without starting it: files, secrets, layout, providers,
|
|
2
|
-
// boot persistence.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// Node engine, boot persistence. Yellow warnings don't fail, red failures exit 1.
|
|
3
|
+
//
|
|
4
|
+
// Read-only by default. `--fix` applies the repairs attached to individual
|
|
5
|
+
// checks, each behind its own confirmation — and only those needing no root and
|
|
6
|
+
// no runtime restart. See `applyFixes`.
|
|
7
|
+
|
|
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';
|
|
6
18
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
19
|
import * as p from '@clack/prompts';
|
|
9
20
|
import pc from 'picocolors';
|
|
10
21
|
import { readEnvFile } from '../env.js';
|
|
11
22
|
import { requireDeploymentDir, resolveDeploymentDir } from '../paths.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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';
|
|
35
|
+
|
|
36
|
+
export async function runDoctor(argv = []) {
|
|
37
|
+
const fix = argv.includes('--fix');
|
|
18
38
|
const dir = resolveDeploymentDir();
|
|
19
39
|
requireDeploymentDir(dir);
|
|
20
40
|
|
|
21
|
-
p.intro(pc.bgCyan(pc.black(' selva doctor ')));
|
|
41
|
+
p.intro(pc.bgCyan(pc.black(fix ? ' selva doctor --fix ' : ' selva doctor ')));
|
|
22
42
|
|
|
23
43
|
const checks = [];
|
|
24
44
|
const env = readEnvFile(join(dir, '.env'));
|
|
@@ -26,28 +46,12 @@ export async function runDoctor() {
|
|
|
26
46
|
checks.push(checkFile(join(dir, '.env'), '.env present'));
|
|
27
47
|
checks.push(checkFile(join(dir, 'ecosystem.config.cjs'), 'ecosystem.config.cjs present'));
|
|
28
48
|
checks.push(checkLayoutDrift(dir));
|
|
49
|
+
checks.push(checkMigrationLeftovers(dir));
|
|
29
50
|
checks.push(checkSecret(env.SELVA_HMAC_KEY, 'SELVA_HMAC_KEY is a 32-byte hex string'));
|
|
30
51
|
checks.push(checkSecret(env.SELVA_AT_REST_KEY, 'SELVA_AT_REST_KEY is a 32-byte hex string'));
|
|
31
52
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
auth: (env.SELVA_AUTH_PROVIDER ?? 'local').toLowerCase(),
|
|
35
|
-
data: (env.SELVA_DATA_PROVIDER ?? 'local').toLowerCase(),
|
|
36
|
-
storage: (env.SELVA_STORAGE_PROVIDER ?? 'local').toLowerCase()
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const validForAuth = new Set(['local', 'supabase', 'header']);
|
|
40
|
-
const validForData = new Set(['local', 'supabase']);
|
|
41
|
-
|
|
42
|
-
if (!validForAuth.has(providers.auth)) {
|
|
43
|
-
checks.push(red(`SELVA_AUTH_PROVIDER="${providers.auth}" — expected local|supabase|header`));
|
|
44
|
-
}
|
|
45
|
-
if (!validForData.has(providers.data)) {
|
|
46
|
-
checks.push(red(`SELVA_DATA_PROVIDER="${providers.data}" — expected local|supabase`));
|
|
47
|
-
}
|
|
48
|
-
if (!validForData.has(providers.storage)) {
|
|
49
|
-
checks.push(red(`SELVA_STORAGE_PROVIDER="${providers.storage}" — expected local|supabase`));
|
|
50
|
-
}
|
|
53
|
+
const providers = resolveProviders(env);
|
|
54
|
+
checks.push(...checkProviders(providers));
|
|
51
55
|
|
|
52
56
|
const used = new Set(Object.values(providers));
|
|
53
57
|
|
|
@@ -64,34 +68,35 @@ export async function runDoctor() {
|
|
|
64
68
|
checks.push(...checkHeaderAuth(dir, env, providers.data));
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
if (tenancy !== 'single' && tenancy !== 'multi') {
|
|
69
|
-
checks.push(red(`SELVA_TENANCY="${tenancy}" — expected single|multi`));
|
|
70
|
-
} else {
|
|
71
|
-
checks.push(green(`SELVA_TENANCY=${tenancy}`));
|
|
72
|
-
}
|
|
71
|
+
checks.push(checkTenancy(env));
|
|
73
72
|
|
|
74
73
|
checks.push(checkPackage(dir, '@selvajs/selva'));
|
|
74
|
+
checks.push(checkNodeEngine(dir));
|
|
75
75
|
checks.push(checkCliRuntimeAlignment(dir));
|
|
76
76
|
checks.push(...checkBootPersistence(dir));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
new URL(env.ORIGIN);
|
|
80
|
-
checks.push(green(`ORIGIN=${env.ORIGIN}`));
|
|
81
|
-
} catch {
|
|
82
|
-
checks.push(red(`ORIGIN="${env.ORIGIN}" is not a valid URL`));
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
checks.push(yellow('ORIGIN unset — required behind a reverse proxy'));
|
|
86
|
-
}
|
|
77
|
+
checks.push(checkOrigin(env));
|
|
78
|
+
checks.push(...checkDeprecatedEnv(env));
|
|
87
79
|
|
|
88
80
|
// ── Render ─────────────────────────────────────────────────────────
|
|
81
|
+
const resolved = await Promise.all(checks);
|
|
89
82
|
let failures = 0;
|
|
90
|
-
for (const c of
|
|
83
|
+
for (const c of resolved) {
|
|
91
84
|
console.log(' ' + c.line);
|
|
92
85
|
if (c.severity === 'red') failures += 1;
|
|
93
86
|
}
|
|
94
87
|
|
|
88
|
+
if (fix) {
|
|
89
|
+
failures = await applyFixes(resolved, failures);
|
|
90
|
+
} else {
|
|
91
|
+
const repairable = resolved.filter((c) => c.fix).length;
|
|
92
|
+
if (repairable > 0) {
|
|
93
|
+
p.log.info(
|
|
94
|
+
`${repairable} issue${repairable === 1 ? '' : 's'} can be repaired automatically — ` +
|
|
95
|
+
`re-run with \`selva doctor --fix\`.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
if (failures === 0) {
|
|
96
101
|
p.outro(pc.green('All checks passed.'));
|
|
97
102
|
} else {
|
|
@@ -100,27 +105,50 @@ export async function runDoctor() {
|
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Apply the repairs attached to failing checks, each behind its own
|
|
110
|
+
* confirmation. Returns the updated failure count.
|
|
111
|
+
*
|
|
112
|
+
* Nothing here needs root: anything privileged (the systemd unit) or that would
|
|
113
|
+
* restart this process (a Node upgrade) stays a printed instruction, because a
|
|
114
|
+
* fixer that dies halfway through its own runtime leaves the operator with no
|
|
115
|
+
* obvious path back.
|
|
116
|
+
*/
|
|
117
|
+
async function applyFixes(resolved, failures) {
|
|
118
|
+
const repairs = resolved.filter((c) => c.fix && c.severity !== 'green');
|
|
119
|
+
if (repairs.length === 0) {
|
|
120
|
+
p.log.info('Nothing to repair automatically.');
|
|
121
|
+
return failures;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
p.log.step(`${repairs.length} repair${repairs.length === 1 ? '' : 's'} available`);
|
|
126
|
+
|
|
127
|
+
for (const check of repairs) {
|
|
128
|
+
const approved = await p.confirm({
|
|
129
|
+
message: check.fix.label,
|
|
130
|
+
initialValue: false
|
|
131
|
+
});
|
|
132
|
+
if (p.isCancel(approved) || !approved) {
|
|
133
|
+
p.log.info('Skipped.');
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
let result;
|
|
137
|
+
try {
|
|
138
|
+
result = await check.fix.run();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
result = red(`repair threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
console.log(' ' + result.line);
|
|
143
|
+
if (result.severity === 'green' && check.severity === 'red') failures -= 1;
|
|
144
|
+
}
|
|
145
|
+
return failures;
|
|
111
146
|
}
|
|
112
147
|
|
|
113
148
|
function checkFile(path, label) {
|
|
114
149
|
return existsSync(path) ? green(label) : red(`${label} (missing: ${path})`);
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
function checkSecret(value, label) {
|
|
118
|
-
if (!value) return red(`${label} — unset`);
|
|
119
|
-
if (value === PLACEHOLDER) return red(`${label} — still the placeholder`);
|
|
120
|
-
if (!HEX_64.test(value)) return red(`${label} — not 64 hex chars`);
|
|
121
|
-
return green(label);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
152
|
function checkDataPath(dir, dataPath) {
|
|
125
153
|
const absolute = resolve(dir, dataPath);
|
|
126
154
|
try {
|
|
@@ -297,94 +325,65 @@ function checkCliRuntimeAlignment(dir) {
|
|
|
297
325
|
}
|
|
298
326
|
|
|
299
327
|
// Verify VM reboot will resurrect app: dump.pm2 saved, systemd unit installed, no stray global pm2.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
out.push(
|
|
315
|
-
yellow(
|
|
316
|
-
'pm2 process list not saved — run `npx pm2 save` so a reboot can ' +
|
|
317
|
-
'resurrect the app (nothing to restore without it)'
|
|
318
|
-
)
|
|
328
|
+
/**
|
|
329
|
+
* Does this host's Node satisfy the installed runtime's `engines.node`?
|
|
330
|
+
*
|
|
331
|
+
* npm only enforces engines under `engine-strict=true`, which no deployment
|
|
332
|
+
* sets, so a mismatched install succeeds silently and `/api/health` still
|
|
333
|
+
* returns 200 — routes using newer APIs throw only under real traffic
|
|
334
|
+
* (issue #176). Deliberately NOT auto-fixable: upgrading Node is
|
|
335
|
+
* distro-specific and would restart the process running this check.
|
|
336
|
+
*/
|
|
337
|
+
function checkNodeEngine(dir) {
|
|
338
|
+
let required;
|
|
339
|
+
try {
|
|
340
|
+
const pkg = JSON.parse(
|
|
341
|
+
readFileSync(join(dir, 'node_modules', '@selvajs', 'selva', 'package.json'), 'utf8')
|
|
319
342
|
);
|
|
343
|
+
required = pkg.engines?.node;
|
|
344
|
+
} catch {
|
|
345
|
+
return yellow('@selvajs/selva engines.node — package.json unreadable, skipped');
|
|
320
346
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const user = process.env.USER ?? process.env.LOGNAME;
|
|
324
|
-
const unitPath = user
|
|
325
|
-
? `/etc/systemd/system/pm2-${user}.service`
|
|
326
|
-
: null;
|
|
327
|
-
|
|
328
|
-
if (unitPath && existsSync(unitPath)) {
|
|
329
|
-
const localPm2 = join(dir, 'node_modules', 'pm2', 'bin', 'pm2');
|
|
330
|
-
let unit = '';
|
|
331
|
-
try {
|
|
332
|
-
unit = readFileSync(unitPath, 'utf8');
|
|
333
|
-
} catch {
|
|
334
|
-
out.push(yellow(`pm2 systemd unit present but unreadable (${unitPath})`));
|
|
335
|
-
return out;
|
|
336
|
-
}
|
|
337
|
-
const execStart = /^ExecStart=(.+)$/m.exec(unit)?.[1] ?? '';
|
|
338
|
-
if (execStart.includes(localPm2)) {
|
|
339
|
-
out.push(green('pm2 systemd boot unit installed (uses deployment-local pm2)'));
|
|
340
|
-
} else {
|
|
341
|
-
out.push(
|
|
342
|
-
red(
|
|
343
|
-
`pm2 systemd boot unit points at a different pm2 than this deployment's.\n ` +
|
|
344
|
-
`ExecStart: ${execStart || '(not found)'}\n ` +
|
|
345
|
-
`expected: ${localPm2} resurrect\n ` +
|
|
346
|
-
`Reboots will resurrect via the wrong pm2 (version skew). Re-run startup ` +
|
|
347
|
-
`with the local binary:\n ` +
|
|
348
|
-
`sudo env PATH=$PATH:${join(dir, 'node_modules', '.bin')} ${localPm2} ` +
|
|
349
|
-
`startup systemd -u $USER --hp $HOME`
|
|
350
|
-
)
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
out.push(
|
|
355
|
-
yellow(
|
|
356
|
-
'pm2 systemd boot unit not installed — the app will NOT restart after a ' +
|
|
357
|
-
'reboot. Run `npx pm2 startup systemd -u $USER --hp $HOME` and paste the ' +
|
|
358
|
-
'printed command (point it at this deployment’s pm2).'
|
|
359
|
-
)
|
|
360
|
-
);
|
|
347
|
+
if (typeof required !== 'string') {
|
|
348
|
+
return yellow('@selvajs/selva declares no engines.node — cannot verify this host');
|
|
361
349
|
}
|
|
362
350
|
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
yellow(
|
|
368
|
-
`a pm2 outside this deployment is on PATH (${globalPm2}) — it can fork a ` +
|
|
369
|
-
`mismatched daemon and trigger skew. Prefer \`npm run\` wrappers / \`npx pm2\` ` +
|
|
370
|
-
`from this directory; consider \`npm uninstall -g pm2\`.`
|
|
371
|
-
)
|
|
372
|
-
);
|
|
351
|
+
const running = process.versions.node;
|
|
352
|
+
const ok = satisfiesNodeRange(running, required);
|
|
353
|
+
if (ok === null) {
|
|
354
|
+
return yellow(`engines.node "${required}" not understood — verify Node v${running} manually`);
|
|
373
355
|
}
|
|
356
|
+
if (ok) return green(`Node v${running} satisfies engines.node ${required}`);
|
|
374
357
|
|
|
375
|
-
return
|
|
358
|
+
return red(
|
|
359
|
+
`Node v${running} does NOT satisfy @selvajs/selva's engines.node ${required}.\n ` +
|
|
360
|
+
`npm installs it anyway and the health check still passes, so this fails only\n ` +
|
|
361
|
+
`under real traffic. Upgrade Node on this host (nvm/fnm or your package manager),\n ` +
|
|
362
|
+
`then: npm rebuild && npm run restart`
|
|
363
|
+
);
|
|
376
364
|
}
|
|
377
365
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return
|
|
366
|
+
/**
|
|
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.
|
|
371
|
+
*/
|
|
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}`);
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
);
|
|
388
387
|
}
|
|
389
388
|
|
|
390
389
|
function checkLayoutDrift(dir) {
|
|
@@ -494,8 +493,7 @@ function checkHeaderAuth(dir, env, dataProvider) {
|
|
|
494
493
|
Boolean(env.HEADER_AUTH_EMAIL_HEADER),
|
|
495
494
|
Boolean(env.HEADER_AUTH_DISPLAY_NAME_HEADER)
|
|
496
495
|
].filter(Boolean).length;
|
|
497
|
-
const headerList =
|
|
498
|
-
`UPN=${resolved.upn}, Email=${resolved.email}, DisplayName=${resolved.displayName}`;
|
|
496
|
+
const headerList = `UPN=${resolved.upn}, Email=${resolved.email}, DisplayName=${resolved.displayName}`;
|
|
499
497
|
if (overrides === 0) {
|
|
500
498
|
out.push(green(`header names (bundled defaults): ${headerList}`));
|
|
501
499
|
} else if (overrides === 3) {
|
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
|
[
|