@openparachute/hub 0.5.2 → 0.5.9-rc.6
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/__tests__/admin-clients.test.ts +275 -0
- package/src/__tests__/admin-handlers.test.ts +159 -320
- package/src/__tests__/admin-host-admin-token.test.ts +52 -4
- package/src/__tests__/api-me.test.ts +149 -0
- package/src/__tests__/api-mint-token.test.ts +381 -0
- package/src/__tests__/api-revocation-list.test.ts +198 -0
- package/src/__tests__/api-revoke-token.test.ts +320 -0
- package/src/__tests__/api-tokens.test.ts +629 -0
- package/src/__tests__/auth.test.ts +680 -16
- package/src/__tests__/expose-2fa-warning.test.ts +123 -0
- package/src/__tests__/expose-cloudflare.test.ts +101 -0
- package/src/__tests__/expose.test.ts +199 -340
- package/src/__tests__/hub-server.test.ts +986 -66
- package/src/__tests__/hub.test.ts +108 -55
- package/src/__tests__/install-source.test.ts +249 -0
- package/src/__tests__/install.test.ts +50 -31
- package/src/__tests__/jwt-sign.test.ts +205 -0
- package/src/__tests__/lifecycle.test.ts +97 -2
- package/src/__tests__/module-manifest.test.ts +48 -0
- package/src/__tests__/notes-serve.test.ts +154 -2
- package/src/__tests__/oauth-handlers.test.ts +1000 -3
- package/src/__tests__/operator-token.test.ts +379 -3
- package/src/__tests__/origin-check.test.ts +220 -0
- package/src/__tests__/port-assign.test.ts +41 -52
- package/src/__tests__/rate-limit.test.ts +190 -0
- package/src/__tests__/services-manifest.test.ts +341 -0
- package/src/__tests__/setup.test.ts +12 -9
- package/src/__tests__/status.test.ts +372 -0
- package/src/__tests__/well-known.test.ts +69 -0
- package/src/admin-clients.ts +139 -0
- package/src/admin-handlers.ts +63 -260
- package/src/admin-host-admin-token.ts +25 -10
- package/src/admin-login-ui.ts +256 -0
- package/src/admin-vault-admin-token.ts +1 -1
- package/src/api-me.ts +124 -0
- package/src/api-mint-token.ts +239 -0
- package/src/api-revocation-list.ts +59 -0
- package/src/api-revoke-token.ts +153 -0
- package/src/api-tokens.ts +224 -0
- package/src/commands/auth.ts +408 -51
- package/src/commands/expose-2fa-warning.ts +82 -0
- package/src/commands/expose-cloudflare.ts +27 -0
- package/src/commands/expose-public-auto.ts +3 -7
- package/src/commands/expose.ts +88 -173
- package/src/commands/install.ts +11 -13
- package/src/commands/lifecycle.ts +53 -4
- package/src/commands/status.ts +99 -8
- package/src/csrf.ts +6 -3
- package/src/help.ts +13 -7
- package/src/hub-db.ts +63 -0
- package/src/hub-server.ts +572 -106
- package/src/hub.ts +272 -149
- package/src/install-source.ts +291 -0
- package/src/jwt-sign.ts +265 -5
- package/src/module-manifest.ts +48 -10
- package/src/notes-serve.ts +70 -9
- package/src/oauth-handlers.ts +395 -29
- package/src/oauth-ui.ts +188 -0
- package/src/operator-token.ts +272 -18
- package/src/origin-check.ts +127 -0
- package/src/port-assign.ts +28 -35
- package/src/rate-limit.ts +166 -0
- package/src/scope-explanations.ts +33 -2
- package/src/service-spec.ts +58 -13
- package/src/services-manifest.ts +62 -3
- package/src/sessions.ts +19 -0
- package/src/well-known.ts +54 -1
- package/web/ui/dist/assets/index-Bv6Bq_wx.js +60 -0
- package/web/ui/dist/assets/index-D54otIhv.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/admin-config.test.ts +0 -281
- package/src/admin-config-ui.ts +0 -534
- package/src/admin-config.ts +0 -226
- package/web/ui/dist/assets/index-BKzPDdB0.js +0 -60
- package/web/ui/dist/assets/index-Dyk6g7vT.css +0 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { is2FAEnrolled, printPublic2FAWarning } from "../commands/expose-2fa-warning.ts";
|
|
6
|
+
import type { VaultAuthStatus } from "../vault/auth-status.ts";
|
|
7
|
+
|
|
8
|
+
function status(partial: Partial<VaultAuthStatus> = {}): VaultAuthStatus {
|
|
9
|
+
return {
|
|
10
|
+
hasOwnerPassword: false,
|
|
11
|
+
hasTotp: false,
|
|
12
|
+
tokenCount: 0,
|
|
13
|
+
vaultNames: [],
|
|
14
|
+
...partial,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("is2FAEnrolled", () => {
|
|
19
|
+
test("returns true when status carries hasTotp: true", () => {
|
|
20
|
+
expect(is2FAEnrolled({ status: status({ hasTotp: true }) })).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns false when status carries hasTotp: false", () => {
|
|
24
|
+
expect(is2FAEnrolled({ status: status({ hasTotp: false }) })).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("reads totp_secret from vaultHome's config.yaml when status not supplied", () => {
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-2fa-warn-"));
|
|
29
|
+
try {
|
|
30
|
+
writeFileSync(join(dir, "config.yaml"), 'totp_secret: "JBSWY3DPEHPK3PXP"\n');
|
|
31
|
+
expect(is2FAEnrolled({ vaultHome: dir })).toBe(true);
|
|
32
|
+
} finally {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("missing config.yaml → not enrolled (false)", () => {
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-2fa-warn-"));
|
|
39
|
+
try {
|
|
40
|
+
// No config.yaml written.
|
|
41
|
+
expect(is2FAEnrolled({ vaultHome: dir })).toBe(false);
|
|
42
|
+
} finally {
|
|
43
|
+
rmSync(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("empty totp_secret value → not enrolled (matches vault's readGlobalConfig)", () => {
|
|
48
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-2fa-warn-"));
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(join(dir, "config.yaml"), 'totp_secret: ""\n');
|
|
51
|
+
expect(is2FAEnrolled({ vaultHome: dir })).toBe(false);
|
|
52
|
+
} finally {
|
|
53
|
+
rmSync(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("malformed config.yaml → not enrolled (safer fail mode, fires warning)", () => {
|
|
58
|
+
// The probe parses config.yaml with a line-anchored regex (no YAML
|
|
59
|
+
// dependency), so junk content simply doesn't match `totp_secret: "..."`
|
|
60
|
+
// and resolves to `hasTotp: false` — which fires the public-exposure
|
|
61
|
+
// warning rather than silently suppressing it. Pin that contract so a
|
|
62
|
+
// future refactor of auth-status.ts can't quietly invert it.
|
|
63
|
+
const dir = mkdtempSync(join(tmpdir(), "pcli-2fa-warn-"));
|
|
64
|
+
try {
|
|
65
|
+
writeFileSync(join(dir, "config.yaml"), "totp_secret: [unbalanced\n ::: not yaml\n");
|
|
66
|
+
expect(is2FAEnrolled({ vaultHome: dir })).toBe(false);
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("printPublic2FAWarning", () => {
|
|
74
|
+
test("not enrolled → fires warning, returns true", () => {
|
|
75
|
+
const logs: string[] = [];
|
|
76
|
+
const fired = printPublic2FAWarning({
|
|
77
|
+
status: status({ hasTotp: false }),
|
|
78
|
+
log: (l) => logs.push(l),
|
|
79
|
+
publicUrl: "https://vault.example.com",
|
|
80
|
+
});
|
|
81
|
+
expect(fired).toBe(true);
|
|
82
|
+
const joined = logs.join("\n");
|
|
83
|
+
expect(joined).toContain("2FA is not enrolled");
|
|
84
|
+
expect(joined).toContain("https://vault.example.com/login");
|
|
85
|
+
expect(joined).toContain("parachute auth 2fa enroll");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("enrolled → suppressed, returns false, logs nothing", () => {
|
|
89
|
+
const logs: string[] = [];
|
|
90
|
+
const fired = printPublic2FAWarning({
|
|
91
|
+
status: status({ hasTotp: true }),
|
|
92
|
+
log: (l) => logs.push(l),
|
|
93
|
+
publicUrl: "https://vault.example.com",
|
|
94
|
+
});
|
|
95
|
+
expect(fired).toBe(false);
|
|
96
|
+
expect(logs).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("password-also-missing case still fires (warning is layer-independent of password state)", () => {
|
|
100
|
+
// The wide-open state (no password, no 2FA) hits this branch too — the
|
|
101
|
+
// hub's own `printAuthGuidance` (cloudflare) and `runAuthPreflight`
|
|
102
|
+
// (interactive wizard) cover the password remediation; this warning is
|
|
103
|
+
// strictly about 2FA.
|
|
104
|
+
const logs: string[] = [];
|
|
105
|
+
const fired = printPublic2FAWarning({
|
|
106
|
+
status: status({ hasOwnerPassword: false, hasTotp: false }),
|
|
107
|
+
log: (l) => logs.push(l),
|
|
108
|
+
publicUrl: "https://vault.example.com",
|
|
109
|
+
});
|
|
110
|
+
expect(fired).toBe(true);
|
|
111
|
+
expect(logs.some((l) => l.includes("2FA is not enrolled"))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("embeds the supplied publicUrl into the /login pointer", () => {
|
|
115
|
+
const logs: string[] = [];
|
|
116
|
+
printPublic2FAWarning({
|
|
117
|
+
status: status({ hasTotp: false }),
|
|
118
|
+
log: (l) => logs.push(l),
|
|
119
|
+
publicUrl: "https://parachute.taildf9ce2.ts.net",
|
|
120
|
+
});
|
|
121
|
+
expect(logs.some((l) => l.includes("https://parachute.taildf9ce2.ts.net/login"))).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -511,6 +511,107 @@ describe("exposeCloudflareUp", () => {
|
|
|
511
511
|
env.cleanup();
|
|
512
512
|
}
|
|
513
513
|
});
|
|
514
|
+
|
|
515
|
+
// 2FA-enrollment warning (#186). The cloudflare path is always public —
|
|
516
|
+
// every successful bringup makes /admin/login reachable on the open
|
|
517
|
+
// internet, where 2FA is the primary defense beyond #188's rate-limit floor.
|
|
518
|
+
describe("2FA-enrollment warning", () => {
|
|
519
|
+
test("not enrolled → warning fires after the success block", async () => {
|
|
520
|
+
const env = makeEnv();
|
|
521
|
+
try {
|
|
522
|
+
const uuid = "cccccccc-0000-0000-0000-000000000003";
|
|
523
|
+
const { runner } = queueRunner([
|
|
524
|
+
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" },
|
|
525
|
+
{ code: 0, stdout: "[]", stderr: "" },
|
|
526
|
+
{
|
|
527
|
+
code: 0,
|
|
528
|
+
stdout: `Tunnel credentials written to ${env.cloudflaredHome}/${uuid}.json.\nCreated tunnel parachute with id ${uuid}\n`,
|
|
529
|
+
stderr: "",
|
|
530
|
+
},
|
|
531
|
+
{ code: 0, stdout: "", stderr: "" },
|
|
532
|
+
]);
|
|
533
|
+
const { spawner } = fakeSpawner(42100);
|
|
534
|
+
const logs: string[] = [];
|
|
535
|
+
|
|
536
|
+
const code = await exposeCloudflareUp("vault.example.com", {
|
|
537
|
+
runner,
|
|
538
|
+
spawner,
|
|
539
|
+
alive: () => false,
|
|
540
|
+
kill: () => {},
|
|
541
|
+
log: (l) => logs.push(l),
|
|
542
|
+
manifestPath: env.manifestPath,
|
|
543
|
+
statePath: env.statePath,
|
|
544
|
+
configPath: env.configPath,
|
|
545
|
+
logPath: env.logPath,
|
|
546
|
+
cloudflaredHome: env.cloudflaredHome,
|
|
547
|
+
// No password, no 2FA — fully wide open. The warning should still
|
|
548
|
+
// fire; password-recovery copy already lives in `printAuthGuidance`.
|
|
549
|
+
vaultAuthStatus: {
|
|
550
|
+
hasOwnerPassword: false,
|
|
551
|
+
hasTotp: false,
|
|
552
|
+
tokenCount: 0,
|
|
553
|
+
vaultNames: [],
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
expect(code).toBe(0);
|
|
558
|
+
const joined = logs.join("\n");
|
|
559
|
+
expect(joined).toContain("2FA is not enrolled");
|
|
560
|
+
expect(joined).toContain("https://vault.example.com/login");
|
|
561
|
+
expect(joined).toContain("parachute auth 2fa enroll");
|
|
562
|
+
} finally {
|
|
563
|
+
env.cleanup();
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("enrolled → warning suppressed (no '2FA is not enrolled' line)", async () => {
|
|
568
|
+
const env = makeEnv();
|
|
569
|
+
try {
|
|
570
|
+
const uuid = "dddddddd-0000-0000-0000-000000000004";
|
|
571
|
+
const { runner } = queueRunner([
|
|
572
|
+
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" },
|
|
573
|
+
{ code: 0, stdout: "[]", stderr: "" },
|
|
574
|
+
{
|
|
575
|
+
code: 0,
|
|
576
|
+
stdout: `Tunnel credentials written to ${env.cloudflaredHome}/${uuid}.json.\nCreated tunnel parachute with id ${uuid}\n`,
|
|
577
|
+
stderr: "",
|
|
578
|
+
},
|
|
579
|
+
{ code: 0, stdout: "", stderr: "" },
|
|
580
|
+
]);
|
|
581
|
+
const { spawner } = fakeSpawner(42101);
|
|
582
|
+
const logs: string[] = [];
|
|
583
|
+
|
|
584
|
+
const code = await exposeCloudflareUp("vault.example.com", {
|
|
585
|
+
runner,
|
|
586
|
+
spawner,
|
|
587
|
+
alive: () => false,
|
|
588
|
+
kill: () => {},
|
|
589
|
+
log: (l) => logs.push(l),
|
|
590
|
+
manifestPath: env.manifestPath,
|
|
591
|
+
statePath: env.statePath,
|
|
592
|
+
configPath: env.configPath,
|
|
593
|
+
logPath: env.logPath,
|
|
594
|
+
cloudflaredHome: env.cloudflaredHome,
|
|
595
|
+
vaultAuthStatus: {
|
|
596
|
+
hasOwnerPassword: true,
|
|
597
|
+
hasTotp: true,
|
|
598
|
+
tokenCount: 1,
|
|
599
|
+
vaultNames: ["default"],
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
expect(code).toBe(0);
|
|
604
|
+
const joined = logs.join("\n");
|
|
605
|
+
expect(joined).not.toContain("2FA is not enrolled");
|
|
606
|
+
// The existing `printAuthGuidance` 2FA-recommend bullet is unrelated
|
|
607
|
+
// to the new contextual warning and stays in place — assert it on a
|
|
608
|
+
// shape that doesn't collide with the warning text.
|
|
609
|
+
expect(joined).toContain("(recommended) TOTP + backup codes");
|
|
610
|
+
} finally {
|
|
611
|
+
env.cleanup();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
});
|
|
514
615
|
});
|
|
515
616
|
|
|
516
617
|
describe("exposeCloudflareOff", () => {
|