@dashclaw/cli 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/up/index.js +45 -4
- package/package.json +1 -1
package/lib/up/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// without touching the network, Docker, or a real Next server.
|
|
14
14
|
|
|
15
15
|
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
18
|
import { homedir } from 'node:os';
|
|
18
19
|
import { join } from 'node:path';
|
|
@@ -111,6 +112,32 @@ export function rewriteEnvDatabaseUrl(appDir, databaseUrl, logger = console) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Mint a one-time browser sign-in token into the app's .env.local, BEFORE the
|
|
117
|
+
* server starts (Next reads .env.local at boot). The server consumes it on
|
|
118
|
+
* first use (app/api/auth/local); the 15-minute expiry bounds replay. This is
|
|
119
|
+
* what lets `dashclaw up` open a browser that lands already signed in instead
|
|
120
|
+
* of a login form asking for a password the user never saw.
|
|
121
|
+
* Best-effort: returns null (→ fall back to /setup) when .env.local is absent.
|
|
122
|
+
*/
|
|
123
|
+
export function mintLoginToken(appDir, logger = console) {
|
|
124
|
+
try {
|
|
125
|
+
const envPath = join(appDir, '.env.local');
|
|
126
|
+
if (!existsSync(envPath)) return null;
|
|
127
|
+
const token = randomBytes(24).toString('base64url');
|
|
128
|
+
const line = `DASHCLAW_LOGIN_OTT=${token}.${Date.now() + 15 * 60_000}`;
|
|
129
|
+
const src = readFileSync(envPath, 'utf8');
|
|
130
|
+
const out = /^DASHCLAW_LOGIN_OTT=.*$/m.test(src)
|
|
131
|
+
? src.replace(/^DASHCLAW_LOGIN_OTT=.*$/m, line)
|
|
132
|
+
: `${src}${src.endsWith('\n') || src === '' ? '' : '\n'}${line}\n`;
|
|
133
|
+
writeFileSync(envPath, out);
|
|
134
|
+
return token;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
logger.error(`[warn] Could not mint a browser sign-in link: ${e.message} — sign in with the admin password instead.`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
/** Default process-liveness probe: signal 0 succeeds iff the pid exists. */
|
|
115
142
|
export function defaultProcessAlive(pid) {
|
|
116
143
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
@@ -125,6 +152,7 @@ export function realDeps() {
|
|
|
125
152
|
chooseDbMode,
|
|
126
153
|
provisionDatabase,
|
|
127
154
|
runSetupScript: runSetupScriptReal,
|
|
155
|
+
mintLoginToken,
|
|
128
156
|
buildApp,
|
|
129
157
|
startServer,
|
|
130
158
|
waitForHealth,
|
|
@@ -210,10 +238,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
210
238
|
if (out.ok === false) throw new Error(out.error || 'Setup failed.');
|
|
211
239
|
apiKey = out.apiKey;
|
|
212
240
|
inst = saveInstance(baseDir, { apiKey });
|
|
213
|
-
// setup.mjs
|
|
214
|
-
//
|
|
241
|
+
// setup.mjs prints the password to ITS stderr, but runSetupScript pipes
|
|
242
|
+
// (and on success discards) that stream — so this line is the one place
|
|
243
|
+
// the operator ever sees it. It is also saved to <appDir>/.env.local.
|
|
215
244
|
if (out.adminPassword) {
|
|
216
|
-
logger.
|
|
245
|
+
logger.log(`[ok] Dashboard admin password: ${out.adminPassword} (also saved to ${join(appDir, '.env.local')})`);
|
|
217
246
|
}
|
|
218
247
|
inst = checkpoint(baseDir, 'setup_done');
|
|
219
248
|
}
|
|
@@ -242,7 +271,11 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
242
271
|
// Pid alive but health check failed — fall through to a fresh start.
|
|
243
272
|
}
|
|
244
273
|
}
|
|
274
|
+
// One-time browser sign-in: only mintable for a server WE are about to start
|
|
275
|
+
// (Next reads .env.local at boot; a reused server would never see the token).
|
|
276
|
+
let loginToken = null;
|
|
245
277
|
if (!reusedServer) {
|
|
278
|
+
loginToken = deps.mintLoginToken?.(appDir, logger) ?? null;
|
|
246
279
|
child = deps.startServer({ appDir, port, logger });
|
|
247
280
|
inst = saveInstance(baseDir, { pid: child.pid });
|
|
248
281
|
try {
|
|
@@ -271,8 +304,16 @@ export async function runUp({ args, baseDir = join(homedir(), '.dashclaw'), deps
|
|
|
271
304
|
}
|
|
272
305
|
|
|
273
306
|
// 8. open -----------------------------------------------------------------
|
|
307
|
+
// With a fresh token the browser opens /login?ott=... and lands signed in
|
|
308
|
+
// (redirecting on to /setup); otherwise it opens /setup directly and any
|
|
309
|
+
// protected page will ask for the admin password.
|
|
310
|
+
const signInUrl = loginToken
|
|
311
|
+
? `${baseUrl}/login?ott=${loginToken}&next=${encodeURIComponent('/setup')}`
|
|
312
|
+
: `${baseUrl}/setup`;
|
|
274
313
|
if (!args.noBrowser) {
|
|
275
|
-
deps.openBrowser(
|
|
314
|
+
deps.openBrowser(signInUrl, logger);
|
|
315
|
+
} else if (loginToken) {
|
|
316
|
+
logger.log(`Sign in (link valid ~15 min, single use): ${signInUrl}`);
|
|
276
317
|
}
|
|
277
318
|
logger.log(`Done. First steps: ${baseUrl}/connect`);
|
|
278
319
|
|