codepiper 0.1.4 → 0.2.0
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/.env.example +8 -2
- package/CHANGELOG.md +16 -0
- package/README.md +18 -4
- package/package.json +1 -1
- package/packages/cli/src/commands/auth.ts +38 -4
- package/packages/cli/src/main.ts +2 -1
- package/packages/daemon/src/api/authRoutes.ts +45 -7
- package/packages/daemon/src/auth/authService.ts +101 -2
- package/packages/daemon/src/main.ts +25 -3
- package/packages/web/dist/assets/{AnalyticsPage-tt9VQaeG.js → AnalyticsPage-B2Jo5KhD.js} +1 -1
- package/packages/web/dist/assets/{PoliciesPage-GcKOy8iE.js → PoliciesPage-Co0wDxU6.js} +1 -1
- package/packages/web/dist/assets/{SessionDetailPage-CXhaRigf.js → SessionDetailPage-B02hN2vV.js} +1 -1
- package/packages/web/dist/assets/{SettingsPage-XpiwINnF.js → SettingsPage-CsblfgAu.js} +3 -3
- package/packages/web/dist/assets/{WorkflowsPage-ByyUhe7P.js → WorkflowsPage-D6nmynEV.js} +1 -1
- package/packages/web/dist/assets/{index-DcbqZfqt.js → index-CpYkYxsw.js} +45 -45
- package/packages/web/dist/assets/{pencil-CSMVe0ds.js → pencil-BQdMfs6R.js} +1 -1
- package/packages/web/dist/assets/{refresh-cw-CdfhRXCd.js → refresh-cw-Bk2hIBMy.js} +1 -1
- package/packages/web/dist/assets/{tabs-Ckbv8J0d.js → tabs-8Wa2H5Vg.js} +1 -1
- package/packages/web/dist/assets/{trash-2-DCNTb20S.js → trash-2-kiez5gqY.js} +1 -1
- package/packages/web/dist/index.html +1 -1
package/.env.example
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
# CODEPIPER_WS_PORT=9999 # WebSocket port (standalone)
|
|
8
8
|
|
|
9
9
|
# ── Remote Access ───────────────────────────────────────────────────
|
|
10
|
-
# Comma-separated
|
|
11
|
-
# Required when accessing from a non-localhost domain
|
|
10
|
+
# Comma-separated list of allowed origin hostnames for WebSocket and CSRF checks.
|
|
11
|
+
# Required when accessing the web dashboard from a non-localhost domain
|
|
12
|
+
# (e.g., via reverse proxy, Cloudflare Tunnel, etc.).
|
|
13
|
+
# Accepts bare hostnames or full origins (scheme is stripped).
|
|
14
|
+
# Localhost (127.0.0.1, ::1, localhost) is always allowed implicitly.
|
|
12
15
|
# CODEPIPER_ALLOWED_ORIGINS=myapp.example.com,other.dev
|
|
13
16
|
|
|
14
17
|
# ── Database ────────────────────────────────────────────────────────
|
|
@@ -16,6 +19,9 @@
|
|
|
16
19
|
|
|
17
20
|
# ── Security ───────────────────────────────────────────────────────
|
|
18
21
|
# CODEPIPER_SECRET= # Hook authentication secret (auto-generated if unset)
|
|
22
|
+
# CODEPIPER_FORCE_SECURE_COOKIES=0 # Set to 1 to force Secure auth cookies behind TLS-terminating proxies
|
|
23
|
+
# CODEPIPER_TRUST_PROXY_HEADERS=0 # Set to 1 to trust X-Forwarded-For/X-Real-IP/X-Forwarded-Proto headers
|
|
24
|
+
# CODEPIPER_MFA_QR_TIMEOUT_MS=8000 # Optional QR generation timeout before fallback to manual MFA key
|
|
19
25
|
|
|
20
26
|
# ── Push Delivery (Optional) ───────────────────────────────────────
|
|
21
27
|
# CODEPIPER_PUSH_ENABLED=0 # Set to 1 to enable daemon web-push delivery
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-02-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- First-run bootstrap password flow for web onboarding when no password exists.
|
|
13
|
+
- CLI password rotation generator (`codepiper auth reset-password --generate`).
|
|
14
|
+
- Onboarding-aware auth status signals (`onboardingPending`) across daemon and web auth state.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Enforced MFA-first onboarding flow for initial web sign-in.
|
|
18
|
+
- Improved onboarding and production docs for bootstrap password, remote origin allowlisting, and CSRF/WS origin controls.
|
|
19
|
+
- Updated setup/login UI copy and recovery flow for clearer first-run guidance.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Added QR generation timeout with manual-key fallback to prevent MFA setup hangs on constrained VPS environments.
|
|
23
|
+
- Hardened MFA setup handling in API/UI paths with explicit failure and retry behavior.
|
|
24
|
+
|
|
9
25
|
## [0.1.4] - 2026-02-20
|
|
10
26
|
|
|
11
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -224,7 +224,7 @@ codepiper daemon --web --port 3456
|
|
|
224
224
|
Then:
|
|
225
225
|
1. Open `http://127.0.0.1:3000` (or your custom port).
|
|
226
226
|
2. Complete onboarding:
|
|
227
|
-
1.
|
|
227
|
+
1. Sign in with the bootstrap password printed by the daemon on first web start.
|
|
228
228
|
2. Set up and verify MFA (required before first sign-in).
|
|
229
229
|
3. Create your first session in **Sessions**.
|
|
230
230
|
4. Send a prompt from terminal/conversation view.
|
|
@@ -232,8 +232,10 @@ Then:
|
|
|
232
232
|
### First-Run Onboarding (Mandatory MFA)
|
|
233
233
|
|
|
234
234
|
Web onboarding is a strict two-step flow:
|
|
235
|
-
1.
|
|
236
|
-
|
|
235
|
+
1. Daemon generates a secure bootstrap password once (printed on first `codepiper daemon --web` run).
|
|
236
|
+
- Password is printed only on TTY startup (not in non-interactive service logs).
|
|
237
|
+
- For headless/service deployments, rotate/generate from host CLI: `codepiper auth reset-password --generate`
|
|
238
|
+
2. After signing in with that password, complete TOTP MFA setup and verification.
|
|
237
239
|
|
|
238
240
|
Until MFA verification succeeds, the dashboard remains in onboarding mode and does not issue a normal authenticated session.
|
|
239
241
|
|
|
@@ -242,6 +244,12 @@ MFA disable/reset is CLI-only:
|
|
|
242
244
|
codepiper auth reset-mfa
|
|
243
245
|
```
|
|
244
246
|
|
|
247
|
+
Rotate password with a generated secure value:
|
|
248
|
+
```bash
|
|
249
|
+
codepiper auth reset-password --generate
|
|
250
|
+
```
|
|
251
|
+
If MFA is not enabled, this command forces MFA onboarding on the next sign-in.
|
|
252
|
+
|
|
245
253
|
### Direct Tmux Attach
|
|
246
254
|
|
|
247
255
|
Use this when you want to attach from your local terminal without going through CLI streaming:
|
|
@@ -375,8 +383,13 @@ Environment Variables:
|
|
|
375
383
|
CODEPIPER_DB_PATH SQLite database path (default: ~/.codepiper/codepiper.db)
|
|
376
384
|
CODEPIPER_WS_PORT WebSocket port (default: 9999)
|
|
377
385
|
CODEPIPER_HTTP_PORT HTTP port (default: 3000, overridden by --port)
|
|
386
|
+
CODEPIPER_ALLOWED_ORIGINS Comma-separated allowed origin hostnames for WebSocket and CSRF checks.
|
|
387
|
+
Required when accessing the dashboard from a non-localhost domain
|
|
388
|
+
(e.g., via reverse proxy, Cloudflare Tunnel, etc.).
|
|
389
|
+
Localhost is always allowed implicitly
|
|
378
390
|
CODEPIPER_FORCE_SECURE_COOKIES Optional (`1`) to force `Secure` auth cookies behind TLS-terminating proxies
|
|
379
391
|
CODEPIPER_TRUST_PROXY_HEADERS Optional (`1`) to trust proxy headers (`X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto`) for client IP and secure-cookie inference
|
|
392
|
+
CODEPIPER_MFA_QR_TIMEOUT_MS Optional QR generation timeout in ms before manual-key fallback (default: 8000)
|
|
380
393
|
CODEPIPER_PUSH_ENABLED Optional daemon push delivery toggle (`1` to enable)
|
|
381
394
|
CODEPIPER_PUSH_PUBLIC_KEY Optional Base64URL VAPID public key for daemon push delivery
|
|
382
395
|
CODEPIPER_PUSH_PRIVATE_KEY Optional Base64URL VAPID private key for daemon push delivery
|
|
@@ -476,7 +489,8 @@ Sessions support two billing modes via `--billing` flag or `billingMode` API par
|
|
|
476
489
|
- Unix socket API is trusted local access (no session token required)
|
|
477
490
|
- HTTP/WebSocket dashboard routes enforce session auth when auth is configured
|
|
478
491
|
- MFA disable/reset is CLI-only (`codepiper auth reset-mfa`) to reduce browser downgrade risk
|
|
479
|
-
- Browser-originated mutating HTTP API requests require same-origin (CSRF mitigation)
|
|
492
|
+
- Browser-originated mutating HTTP API requests require same-origin or `CODEPIPER_ALLOWED_ORIGINS` match (CSRF mitigation)
|
|
493
|
+
- WebSocket upgrades gated by `Origin` header (CSWSH prevention) — only localhost and `CODEPIPER_ALLOWED_ORIGINS` accepted
|
|
480
494
|
- Input validation prevents injection attacks
|
|
481
495
|
- Audit logs for all permission decisions
|
|
482
496
|
- Remote access recommended via SSH port forwarding
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ interface AuthOptions {
|
|
|
7
7
|
args: string[];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
function parseAuthOptions(args: string[]): AuthOptions {
|
|
10
|
+
export function parseAuthOptions(args: string[]): AuthOptions {
|
|
11
11
|
let socket = "/tmp/codepiper.sock";
|
|
12
12
|
const positional: string[] = [];
|
|
13
13
|
|
|
@@ -69,7 +69,41 @@ async function handleStatus(socket: string): Promise<void> {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
interface ResetPasswordFlags {
|
|
73
|
+
generate: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseResetPasswordFlags(args: string[]): ResetPasswordFlags {
|
|
77
|
+
const flags: ResetPasswordFlags = { generate: false };
|
|
78
|
+
for (const arg of args) {
|
|
79
|
+
if (arg === "--generate" || arg === "-g") {
|
|
80
|
+
flags.generate = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Unknown reset-password option: ${arg}`);
|
|
84
|
+
}
|
|
85
|
+
return flags;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleResetPassword(socket: string, args: string[]): Promise<void> {
|
|
89
|
+
const flags = parseResetPasswordFlags(args);
|
|
90
|
+
if (flags.generate) {
|
|
91
|
+
const result = await daemonRequest<{ ok: boolean; generated: boolean; password: string }>(
|
|
92
|
+
socket,
|
|
93
|
+
"/auth/cli/reset-password",
|
|
94
|
+
{
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: JSON.stringify({ generate: true }),
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log("\x1b[32m✓\x1b[0m Password rotated with a generated secure value");
|
|
101
|
+
console.log(` New password: ${result.password}`);
|
|
102
|
+
console.log(" All existing sessions have been invalidated.");
|
|
103
|
+
console.log(" Keep this password secure and complete MFA onboarding if prompted.");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
73
107
|
process.stdout.write("New password: ");
|
|
74
108
|
const password = await readPasswordFromStdin();
|
|
75
109
|
|
|
@@ -240,7 +274,7 @@ export async function runAuthCommand(args: string[]): Promise<void> {
|
|
|
240
274
|
await handleStatus(options.socket);
|
|
241
275
|
break;
|
|
242
276
|
case "reset-password":
|
|
243
|
-
await handleResetPassword(options.socket);
|
|
277
|
+
await handleResetPassword(options.socket, options.args);
|
|
244
278
|
break;
|
|
245
279
|
case "reset-mfa":
|
|
246
280
|
await handleResetMfa(options.socket);
|
|
@@ -254,7 +288,7 @@ export async function runAuthCommand(args: string[]): Promise<void> {
|
|
|
254
288
|
default:
|
|
255
289
|
console.error(`Unknown auth subcommand: ${options.subcommand}`);
|
|
256
290
|
console.error(
|
|
257
|
-
"Available subcommands: status, reset-password, reset-mfa, sessions, revoke-all"
|
|
291
|
+
"Available subcommands: status, reset-password [--generate], reset-mfa, sessions, revoke-all"
|
|
258
292
|
);
|
|
259
293
|
process.exit(1);
|
|
260
294
|
}
|
package/packages/cli/src/main.ts
CHANGED
|
@@ -114,7 +114,7 @@ Manage authentication and security settings.
|
|
|
114
114
|
|
|
115
115
|
Subcommands:
|
|
116
116
|
status Show auth configuration status
|
|
117
|
-
reset-password Reset the dashboard password
|
|
117
|
+
reset-password Reset the dashboard password (or generate one)
|
|
118
118
|
reset-mfa Disable two-factor authentication
|
|
119
119
|
sessions List active login sessions
|
|
120
120
|
revoke-all Revoke all active sessions
|
|
@@ -125,6 +125,7 @@ Options:
|
|
|
125
125
|
Examples:
|
|
126
126
|
codepiper auth status
|
|
127
127
|
codepiper auth reset-password
|
|
128
|
+
codepiper auth reset-password --generate
|
|
128
129
|
codepiper auth reset-mfa
|
|
129
130
|
codepiper auth sessions
|
|
130
131
|
codepiper auth revoke-all`,
|
|
@@ -46,13 +46,20 @@ export async function handleAuthStatus(req: Request, ctx: RouteContext): Promise
|
|
|
46
46
|
return Response.json({
|
|
47
47
|
setupRequired: false,
|
|
48
48
|
mfaEnabled: false,
|
|
49
|
+
onboardingPending: false,
|
|
49
50
|
authenticated: false,
|
|
50
51
|
authEnabled: false,
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const setupRequired = authService.isSetupRequired();
|
|
55
|
-
const
|
|
56
|
+
const onboardingPending = !setupRequired && authService.isMfaSetupPending();
|
|
57
|
+
let mfaSetupRequired = false;
|
|
58
|
+
if (onboardingPending) {
|
|
59
|
+
const onboardingTokenHash = extractAndHashOnboardingToken(req);
|
|
60
|
+
mfaSetupRequired =
|
|
61
|
+
onboardingTokenHash !== null && authService.validateOnboardingToken(onboardingTokenHash);
|
|
62
|
+
}
|
|
56
63
|
const tokenHash = extractAndHashToken(req);
|
|
57
64
|
const authenticated = tokenHash ? authService.validateSession(tokenHash) : false;
|
|
58
65
|
|
|
@@ -62,7 +69,13 @@ export async function handleAuthStatus(req: Request, ctx: RouteContext): Promise
|
|
|
62
69
|
mfaEnabled = config?.totpEnabled ?? false;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
return Response.json({
|
|
72
|
+
return Response.json({
|
|
73
|
+
setupRequired,
|
|
74
|
+
mfaEnabled,
|
|
75
|
+
mfaSetupRequired,
|
|
76
|
+
onboardingPending,
|
|
77
|
+
authenticated,
|
|
78
|
+
});
|
|
66
79
|
}
|
|
67
80
|
|
|
68
81
|
export async function handleAuthSetup(req: Request, ctx: RouteContext): Promise<Response> {
|
|
@@ -233,8 +246,15 @@ export async function handleMfaSetup(_req: Request, ctx: RouteContext): Promise<
|
|
|
233
246
|
return Response.json({ error: "Auth not configured" }, { status: 500 });
|
|
234
247
|
}
|
|
235
248
|
|
|
236
|
-
|
|
237
|
-
|
|
249
|
+
try {
|
|
250
|
+
const result = await authService.generateTotpSetup();
|
|
251
|
+
return Response.json(result);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof AuthError) {
|
|
254
|
+
return Response.json({ error: error.message, code: error.code }, { status: 400 });
|
|
255
|
+
}
|
|
256
|
+
return Response.json({ error: "Failed to generate MFA setup data" }, { status: 500 });
|
|
257
|
+
}
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
export async function handleMfaVerify(req: Request, ctx: RouteContext): Promise<Response> {
|
|
@@ -318,12 +338,30 @@ export async function handleCliResetPassword(req: Request, ctx: RouteContext): P
|
|
|
318
338
|
|
|
319
339
|
try {
|
|
320
340
|
const body = (await req.json()) as Record<string, unknown>;
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
341
|
+
const shouldGenerate = body.generate === true;
|
|
342
|
+
|
|
343
|
+
let password: string;
|
|
344
|
+
if (shouldGenerate) {
|
|
345
|
+
password = authService.generateSecurePassword();
|
|
346
|
+
} else {
|
|
347
|
+
const provided = getStringField(body, "password");
|
|
348
|
+
if (!provided) {
|
|
349
|
+
return Response.json({ error: "Password is required" }, { status: 400 });
|
|
350
|
+
}
|
|
351
|
+
password = provided;
|
|
324
352
|
}
|
|
325
353
|
|
|
326
354
|
await authService.resetPassword(password);
|
|
355
|
+
|
|
356
|
+
// Re-enter MFA onboarding when MFA is not currently enabled
|
|
357
|
+
const config = ctx.db.getAuthConfig();
|
|
358
|
+
if (config && !config.totpEnabled) {
|
|
359
|
+
ctx.db.updateAuthOnboardingState(true, null, null);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (shouldGenerate) {
|
|
363
|
+
return Response.json({ ok: true, generated: true, password });
|
|
364
|
+
}
|
|
327
365
|
return Response.json({ ok: true });
|
|
328
366
|
} catch (error) {
|
|
329
367
|
if (error instanceof AuthError) {
|
|
@@ -18,6 +18,8 @@ const ONBOARDING_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
18
18
|
const TOTP_WINDOW = 1; // ±1 step (90 seconds total)
|
|
19
19
|
const RECOVERY_CODE_COUNT = 8;
|
|
20
20
|
const MIN_PASSWORD_LENGTH = 8;
|
|
21
|
+
const GENERATED_PASSWORD_LENGTH = 24;
|
|
22
|
+
const DEFAULT_MFA_QR_TIMEOUT_MS = 8000;
|
|
21
23
|
|
|
22
24
|
export interface LoginResult {
|
|
23
25
|
token: string;
|
|
@@ -35,7 +37,7 @@ export interface MfaSetupRequiredResult {
|
|
|
35
37
|
export interface TotpSetupResult {
|
|
36
38
|
secret: string;
|
|
37
39
|
otpauthUri: string;
|
|
38
|
-
qrDataUrl: string;
|
|
40
|
+
qrDataUrl: string | null;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export interface TotpVerifyResult {
|
|
@@ -46,6 +48,10 @@ export interface OnboardingTotpVerifyResult extends TotpVerifyResult {
|
|
|
46
48
|
token: string;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export interface BootstrapPasswordResult {
|
|
52
|
+
password: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
export class AuthService {
|
|
50
56
|
private pendingTotpSecret: string | null = null;
|
|
51
57
|
|
|
@@ -97,6 +103,27 @@ export class AuthService {
|
|
|
97
103
|
};
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
async initializeBootstrapPassword(): Promise<BootstrapPasswordResult> {
|
|
107
|
+
if (!this.isSetupRequired()) {
|
|
108
|
+
throw new AuthError("Password is already configured", "PASSWORD_ALREADY_CONFIGURED");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const password = this.generateSecurePassword();
|
|
112
|
+
const hash = await Bun.password.hash(password, {
|
|
113
|
+
algorithm: "argon2id",
|
|
114
|
+
memoryCost: 65536,
|
|
115
|
+
timeCost: 3,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.db.createAuthConfig(hash, {
|
|
119
|
+
mfaSetupPending: true,
|
|
120
|
+
onboardingTokenHash: null,
|
|
121
|
+
onboardingTokenExpiresAt: null,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return { password };
|
|
125
|
+
}
|
|
126
|
+
|
|
100
127
|
// ─── Login ────────────────────────────────────────────────────────
|
|
101
128
|
|
|
102
129
|
async login(
|
|
@@ -269,7 +296,15 @@ export class AuthService {
|
|
|
269
296
|
});
|
|
270
297
|
|
|
271
298
|
const otpauthUri = totp.toString();
|
|
272
|
-
|
|
299
|
+
let qrDataUrl: string | null = null;
|
|
300
|
+
try {
|
|
301
|
+
qrDataUrl = await this.withTimeout(QRCode.toDataURL(otpauthUri), this.getQrTimeoutMs());
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.warn(
|
|
304
|
+
"[auth] QR generation failed or timed out; falling back to manual setup key only:",
|
|
305
|
+
error
|
|
306
|
+
);
|
|
307
|
+
}
|
|
273
308
|
|
|
274
309
|
// Store pending secret (not persisted until verified)
|
|
275
310
|
this.pendingTotpSecret = secret.base32;
|
|
@@ -457,6 +492,70 @@ export class AuthService {
|
|
|
457
492
|
return codes;
|
|
458
493
|
}
|
|
459
494
|
|
|
495
|
+
generateSecurePassword(length = GENERATED_PASSWORD_LENGTH): string {
|
|
496
|
+
const safeLength = Math.max(length, MIN_PASSWORD_LENGTH);
|
|
497
|
+
const charSets = {
|
|
498
|
+
upper: "ABCDEFGHJKLMNPQRSTUVWXYZ",
|
|
499
|
+
lower: "abcdefghijkmnopqrstuvwxyz",
|
|
500
|
+
digits: "23456789",
|
|
501
|
+
symbols: "!@#$%^&*_-+=",
|
|
502
|
+
};
|
|
503
|
+
const allChars = Object.values(charSets).join("");
|
|
504
|
+
|
|
505
|
+
const pickFrom = (set: string): string => set.charAt(crypto.randomInt(0, set.length));
|
|
506
|
+
|
|
507
|
+
// Guarantee at least one character from each class
|
|
508
|
+
const chars: string[] = [
|
|
509
|
+
pickFrom(charSets.upper),
|
|
510
|
+
pickFrom(charSets.lower),
|
|
511
|
+
pickFrom(charSets.digits),
|
|
512
|
+
pickFrom(charSets.symbols),
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
for (let i = chars.length; i < safeLength; i++) {
|
|
516
|
+
chars.push(pickFrom(allChars));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Fisher-Yates shuffle
|
|
520
|
+
for (let i = chars.length - 1; i > 0; i--) {
|
|
521
|
+
const j = crypto.randomInt(0, i + 1);
|
|
522
|
+
const tmp = chars[i] ?? "";
|
|
523
|
+
chars[i] = chars[j] ?? "";
|
|
524
|
+
chars[j] = tmp;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return chars.join("");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private getQrTimeoutMs(): number {
|
|
531
|
+
const raw = process.env.CODEPIPER_MFA_QR_TIMEOUT_MS;
|
|
532
|
+
if (!raw) {
|
|
533
|
+
return DEFAULT_MFA_QR_TIMEOUT_MS;
|
|
534
|
+
}
|
|
535
|
+
const parsed = Number.parseInt(raw, 10);
|
|
536
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
537
|
+
return DEFAULT_MFA_QR_TIMEOUT_MS;
|
|
538
|
+
}
|
|
539
|
+
return parsed;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private async withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
543
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
544
|
+
try {
|
|
545
|
+
return await Promise.race([
|
|
546
|
+
promise,
|
|
547
|
+
new Promise<never>((_resolve, reject) => {
|
|
548
|
+
timeoutHandle = setTimeout(
|
|
549
|
+
() => reject(new Error("QR code generation timed out")),
|
|
550
|
+
timeoutMs
|
|
551
|
+
);
|
|
552
|
+
}),
|
|
553
|
+
]);
|
|
554
|
+
} finally {
|
|
555
|
+
clearTimeout(timeoutHandle);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
460
559
|
private hashRecoveryCode(code: string): string {
|
|
461
560
|
return crypto.createHash("sha256").update(code).digest("hex");
|
|
462
561
|
}
|
|
@@ -372,7 +372,7 @@ async function main() {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
// Initialize authentication
|
|
375
|
-
const { AuthService } = await import("./auth/authService");
|
|
375
|
+
const { AuthError, AuthService } = await import("./auth/authService");
|
|
376
376
|
const { RateLimiter } = await import("./auth/rateLimiter");
|
|
377
377
|
const { getOrCreateEncryptionKey, getOrCreateHookSecret } = await import("./crypto/encryption");
|
|
378
378
|
|
|
@@ -388,8 +388,30 @@ async function main() {
|
|
|
388
388
|
console.log(`Cleaned up ${expiredSessions} expired auth session(s)`);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
if (authService.isSetupRequired()) {
|
|
392
|
-
|
|
391
|
+
if (authService.isSetupRequired() && cliArgs.web) {
|
|
392
|
+
try {
|
|
393
|
+
const bootstrap = await authService.initializeBootstrapPassword();
|
|
394
|
+
console.log("Auth: Generated first-run bootstrap password for web onboarding.");
|
|
395
|
+
if (process.stdout.isTTY) {
|
|
396
|
+
console.log(`Auth: Bootstrap password: ${bootstrap.password}`);
|
|
397
|
+
} else {
|
|
398
|
+
console.log(
|
|
399
|
+
"Auth: Bootstrap password was generated but is not printed because stdout is not a TTY."
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
console.log("Auth: Sign in once with this password, then complete mandatory MFA setup.");
|
|
403
|
+
console.log("Auth: Rotate anytime with `codepiper auth reset-password --generate`.");
|
|
404
|
+
} catch (error) {
|
|
405
|
+
// Race guard: another process may have configured the password between the check and now
|
|
406
|
+
if (!(error instanceof AuthError && error.code === "PASSWORD_ALREADY_CONFIGURED")) {
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
console.log("Auth: Password configured. Web dashboard requires login.");
|
|
410
|
+
}
|
|
411
|
+
} else if (authService.isSetupRequired()) {
|
|
412
|
+
console.log(
|
|
413
|
+
"Auth: No password configured yet. Start with `codepiper daemon --web` to auto-generate a bootstrap password."
|
|
414
|
+
);
|
|
393
415
|
} else {
|
|
394
416
|
console.log("Auth: Password configured. Web dashboard requires login.");
|
|
395
417
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as i,j as e}from"./react-vendor-B5MgMUHH.js";import{c as A,a as f,f as h,u as U,S as ne,b as le,d as ce,e as de,g as N,B as xe,M as me,A as he,Z as pe}from"./index-
|
|
1
|
+
import{r as i,j as e}from"./react-vendor-B5MgMUHH.js";import{c as A,a as f,f as h,u as U,S as ne,b as le,d as ce,e as de,g as N,B as xe,M as me,A as he,Z as pe}from"./index-CpYkYxsw.js";import{R as g,A as B,C as E,X as w,Y as S,T as v,a as u,B as K,b as F,P as ue,d as fe,e as je}from"./chart-vendor-DlOHLaCG.js";import"./monaco-core-DQ5Mk8AK.js";/**
|
|
2
2
|
* @license lucide-react v0.564.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as c,j as e}from"./react-vendor-B5MgMUHH.js";import{c as z,S as V,b as O,d as U,e as q,g as y,h as v,i as B,j as f,u,B as p,P as D,I as j,D as S,k as $,l as F,m as I,n as E,o as R,p as T,C as X,q as J,X as K,r as L}from"./index-
|
|
1
|
+
import{r as c,j as e}from"./react-vendor-B5MgMUHH.js";import{c as z,S as V,b as O,d as U,e as q,g as y,h as v,i as B,j as f,u,B as p,P as D,I as j,D as S,k as $,l as F,m as I,n as E,o as R,p as T,C as X,q as J,X as K,r as L}from"./index-CpYkYxsw.js";import{T as _}from"./trash-2-kiez5gqY.js";import{P as Q,L as k}from"./pencil-BQdMfs6R.js";import{T as Y,a as Z,b as C,c as P}from"./tabs-8Wa2H5Vg.js";import"./monaco-core-DQ5Mk8AK.js";import"./chart-vendor-DlOHLaCG.js";/**
|
|
2
2
|
* @license lucide-react v0.564.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
package/packages/web/dist/assets/{SessionDetailPage-CXhaRigf.js → SessionDetailPage-B02hN2vV.js}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/monacoSetup-DvBj52bT.js","assets/monaco-react-DfZNWvtW.js","assets/react-vendor-B5MgMUHH.js","assets/monaco-core-DQ5Mk8AK.js","assets/monaco-core-B_19GPAS.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{r as u,j as t,k as ur,l as hs,o as gt,m as pt,n as wn,p as At,q as Vt,t as ps,w as dr,E as xs,x as kn,y as Rt,z as ke,A as gs,C as bs,D as We,F as Zt,G as ys,H as St,I as ft,J as vs,M as ws,K as ks,u as js}from"./react-vendor-B5MgMUHH.js";import{c as K,j as A,q as xt,i as Gt,S as mr,b as fr,G as jn,d as hr,e as pr,g as pn,s as Ns,a as Qe,t as xr,C as gr,u as q,T as Cs,v as Ss,B as Ke,P as Es,X as et,r as Rs,h as Et,L as br,w as Ae,x as yr,M as vr,y as en,z as wr,E as Ts,F as As,H as Ms,J as Ds,K as Ps,N as Ls,O as _s,Q as Fs,R as zs,U as $s,I as Is,V as Os,W as Bs,Y as Hs,_ as Us,$ as qs}from"./index-
|
|
2
|
+
import{r as u,j as t,k as ur,l as hs,o as gt,m as pt,n as wn,p as At,q as Vt,t as ps,w as dr,E as xs,x as kn,y as Rt,z as ke,A as gs,C as bs,D as We,F as Zt,G as ys,H as St,I as ft,J as vs,M as ws,K as ks,u as js}from"./react-vendor-B5MgMUHH.js";import{c as K,j as A,q as xt,i as Gt,S as mr,b as fr,G as jn,d as hr,e as pr,g as pn,s as Ns,a as Qe,t as xr,C as gr,u as q,T as Cs,v as Ss,B as Ke,P as Es,X as et,r as Rs,h as Et,L as br,w as Ae,x as yr,M as vr,y as en,z as wr,E as Ts,F as As,H as Ms,J as Ds,K as Ps,N as Ls,O as _s,Q as Fs,R as zs,U as $s,I as Is,V as Os,W as Bs,Y as Hs,_ as Us,$ as qs}from"./index-CpYkYxsw.js";import{_ as In}from"./monaco-core-DQ5Mk8AK.js";import{R as Vs}from"./refresh-cw-Bk2hIBMy.js";import{L as On,P as Ks}from"./pencil-BQdMfs6R.js";import{x as Gs,a as Ws}from"./terminal-vendor-Cs8KPbV3.js";import{T as Qs,a as Ys,b as Xs,c as kt}from"./tabs-8Wa2H5Vg.js";import"./chart-vendor-DlOHLaCG.js";/**
|
|
3
3
|
* @license lucide-react v0.564.0 - ISC
|
|
4
4
|
*
|
|
5
5
|
* This source code is licensed under the ISC license.
|