@openparachute/hub 0.5.14-rc.8 → 0.6.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/README.md +109 -15
- package/package.json +7 -3
- package/src/__tests__/account-home-ui.test.ts +251 -15
- package/src/__tests__/account-vault-token.test.ts +355 -0
- package/src/__tests__/admin-vaults.test.ts +70 -4
- package/src/__tests__/api-mint-token.test.ts +693 -5
- package/src/__tests__/api-modules-ops.test.ts +45 -0
- package/src/__tests__/api-revoke-token.test.ts +384 -0
- package/src/__tests__/api-users.test.ts +7 -2
- package/src/__tests__/auth.test.ts +157 -30
- package/src/__tests__/cli.test.ts +44 -5
- package/src/__tests__/expose-2fa-warning.test.ts +31 -17
- package/src/__tests__/expose-auth-preflight.test.ts +71 -72
- package/src/__tests__/expose-cloudflare.test.ts +482 -14
- package/src/__tests__/expose.test.ts +52 -2
- package/src/__tests__/hub-server.test.ts +97 -0
- package/src/__tests__/hub.test.ts +85 -6
- package/src/__tests__/init.test.ts +102 -1
- package/src/__tests__/lifecycle.test.ts +464 -2
- package/src/__tests__/oauth-handlers.test.ts +1252 -83
- package/src/__tests__/oauth-ui.test.ts +12 -1
- package/src/__tests__/operator-token-issuer-self-heal.test.ts +412 -0
- package/src/__tests__/resource-binding.test.ts +97 -0
- package/src/__tests__/scope-explanations.test.ts +77 -12
- package/src/__tests__/services-manifest.test.ts +122 -4
- package/src/__tests__/setup-wizard.test.ts +335 -15
- package/src/__tests__/status.test.ts +36 -0
- package/src/__tests__/two-factor-flow.test.ts +602 -0
- package/src/__tests__/two-factor.test.ts +183 -0
- package/src/__tests__/upgrade.test.ts +78 -1
- package/src/__tests__/users.test.ts +68 -0
- package/src/__tests__/vault-auth-status.test.ts +47 -6
- package/src/__tests__/vault-hub-origin-env.test.ts +263 -0
- package/src/account-home-ui.ts +488 -38
- package/src/account-vault-token.ts +282 -0
- package/src/admin-handlers.ts +159 -4
- package/src/admin-login-ui.ts +49 -5
- package/src/admin-vaults.ts +48 -15
- package/src/api-account.ts +14 -0
- package/src/api-mint-token.ts +132 -24
- package/src/api-modules-ops.ts +49 -11
- package/src/api-revoke-token.ts +107 -21
- package/src/api-users.ts +29 -3
- package/src/cli.ts +26 -21
- package/src/clients.ts +18 -6
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +39 -44
- package/src/commands/auth.ts +165 -24
- package/src/commands/expose-2fa-warning.ts +34 -32
- package/src/commands/expose-auth-preflight.ts +89 -78
- package/src/commands/expose-cloudflare.ts +370 -12
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +33 -2
- package/src/commands/lifecycle.ts +386 -17
- package/src/commands/status.ts +22 -0
- package/src/commands/upgrade.ts +55 -11
- package/src/commands/wizard.ts +8 -4
- package/src/env-file.ts +10 -0
- package/src/help.ts +3 -1
- package/src/hub-db.ts +39 -1
- package/src/hub-server.ts +52 -0
- package/src/hub.ts +82 -14
- package/src/oauth-handlers.ts +298 -21
- package/src/oauth-ui.ts +10 -0
- package/src/operator-token.ts +151 -0
- package/src/pending-login.ts +116 -0
- package/src/rate-limit.ts +51 -0
- package/src/resource-binding.ts +134 -0
- package/src/scope-attenuation.ts +85 -0
- package/src/scope-explanations.ts +131 -14
- package/src/services-manifest.ts +112 -0
- package/src/setup-wizard.ts +77 -7
- package/src/tailscale/run.ts +28 -11
- package/src/totp.ts +201 -0
- package/src/two-factor-handlers.ts +287 -0
- package/src/two-factor-store.ts +181 -0
- package/src/two-factor-ui.ts +462 -0
- package/src/users.ts +58 -0
- package/src/vault/auth-status.ts +71 -19
- package/src/vault-hub-origin-env.ts +163 -0
- package/web/ui/dist/assets/index-BiBlvEaj.css +1 -0
- package/web/ui/dist/assets/index-CIN3mnmf.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
- package/src/commands/vault-tokens-create-interactive.ts +0 -143
- package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
- package/web/ui/dist/assets/index-tRmPbbC7.js +0 -61
|
@@ -2,20 +2,34 @@
|
|
|
2
2
|
* Post-exposure auth nudge. Runs after `parachute expose public` successfully
|
|
3
3
|
* brings a tunnel up (TTY only). The tunnel is already live; this is purely
|
|
4
4
|
* advisory — we never error the exposure flow regardless of what the user
|
|
5
|
-
* chooses. The goal is to catch the "fresh vault, just went public, no
|
|
6
|
-
*
|
|
5
|
+
* chooses. The goal is to catch the "fresh vault, just went public, no auth
|
|
6
|
+
* configured" trap before someone else finds it first.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* The load-bearing signal is the **owner password**. Post-pvt_*-DROP (vault
|
|
9
|
+
* #412 / hub#466), the vault `tokens` table holds only vestigial pvt_* rows;
|
|
10
|
+
* a non-zero count no longer means "API auth is configured." Access is now
|
|
11
|
+
* hub-issued JWTs, minted against the operator's identity — and minting that
|
|
12
|
+
* identity requires the owner password (browser OAuth) or the operator token
|
|
13
|
+
* that `set-password` seeds. So "has an owner password" is the single gate
|
|
14
|
+
* that tells us whether *any* authenticated access is reachable. We branch
|
|
15
|
+
* purely on password + 2FA; we no longer count vault-DB rows for the auth
|
|
16
|
+
* decision.
|
|
9
17
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
18
|
+
* Two states we branch on, based on {@link VaultAuthStatus}:
|
|
19
|
+
*
|
|
20
|
+
* - no owner password: loud warning — the exposure is wide open. Offer to
|
|
21
|
+
* set a password, and point at the hub-JWT mint path for clients.
|
|
22
|
+
* - password set, no 2FA: one-line "looks good" + offer to enroll hub-login
|
|
23
|
+
* TOTP (real as of hub#473) since the box is now public.
|
|
24
|
+
* - password + 2FA set: one-line "looks good, 2FA on."
|
|
25
|
+
*
|
|
26
|
+
* `parachute auth 2fa enroll` is the real hub-login TOTP path now (hub#473) —
|
|
27
|
+
* it gates `/login` for real, so the preflight offers it when the operator has
|
|
28
|
+
* a password but no second factor.
|
|
16
29
|
*
|
|
17
30
|
* Defaults are always "skip" — Enter declines every prompt. User can always
|
|
18
|
-
* run `parachute auth
|
|
31
|
+
* run `parachute auth set-password` / `parachute auth 2fa enroll` /
|
|
32
|
+
* `parachute auth mint-token …` later.
|
|
19
33
|
*/
|
|
20
34
|
|
|
21
35
|
import { createInterface } from "node:readline/promises";
|
|
@@ -100,16 +114,47 @@ async function offerOwnerPassword(r: Resolved): Promise<void> {
|
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Offer to enroll hub-login TOTP 2FA (real as of hub#473). Interactive enroll
|
|
119
|
+
* needs to print a secret + prompt for a confirm code, so we run the real CLI
|
|
120
|
+
* command inheriting stdio. Declining is fine — the operator can run it later.
|
|
121
|
+
*/
|
|
103
122
|
async function offerTotp(r: Resolved): Promise<void> {
|
|
104
|
-
|
|
123
|
+
r.log("");
|
|
124
|
+
r.log("Add two-factor authentication? It puts a one-time code (from your");
|
|
125
|
+
r.log("authenticator app) in front of /login on top of your password.");
|
|
126
|
+
if (await yesNo(r, "Set up two-factor authentication now?")) {
|
|
105
127
|
await runCmd(r, ["parachute", "auth", "2fa", "enroll"], "parachute auth 2fa enroll");
|
|
128
|
+
} else {
|
|
129
|
+
r.log("");
|
|
130
|
+
r.log("You can enroll later: `parachute auth 2fa enroll` (or /account/2fa in a browser).");
|
|
106
131
|
}
|
|
107
132
|
}
|
|
108
133
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
/** One-line confirmation that 2FA is already on. */
|
|
135
|
+
function note2faOn(r: Resolved): void {
|
|
136
|
+
r.log("✓ Two-factor authentication is on.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Programmatic / headless clients don't use a password — they carry a
|
|
141
|
+
* hub-issued JWT. We don't auto-mint one here (it needs a scope, and the
|
|
142
|
+
* operator should choose read vs write per client), so this is guidance,
|
|
143
|
+
* not a prompt. Mint paths, in order of how most operators reach them:
|
|
144
|
+
*
|
|
145
|
+
* - Admin SPA → Vaults → "Connect" card (mints + shows the header command).
|
|
146
|
+
* - `parachute auth mint-token --scope vault:<name>:<verb>` (pipeable JWT).
|
|
147
|
+
*
|
|
148
|
+
* The old affordance ran `parachute vault tokens create`, which exits 1
|
|
149
|
+
* post-DROP (vault no longer mints pvt_* tokens) — we never offer it.
|
|
150
|
+
*/
|
|
151
|
+
function printTokenGuidance(r: Resolved): void {
|
|
152
|
+
const name = r.status.vaultNames[0] ?? "<name>";
|
|
153
|
+
r.log("");
|
|
154
|
+
r.log("For programmatic / headless clients (scripts, CI), mint a hub token:");
|
|
155
|
+
r.log(" • Admin → Vaults → Connect (mints a scope-narrow token + copy-paste header)");
|
|
156
|
+
r.log(` • parachute auth mint-token --scope vault:${name}:read # or :write`);
|
|
157
|
+
r.log(" → attach the printed JWT as Authorization: Bearer <hub-jwt>");
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
function printDivider(r: Resolved): void {
|
|
@@ -118,86 +163,58 @@ function printDivider(r: Resolved): void {
|
|
|
118
163
|
}
|
|
119
164
|
|
|
120
165
|
/**
|
|
121
|
-
* `
|
|
122
|
-
*
|
|
166
|
+
* `no owner password`: the exposure is wide open — without a password,
|
|
167
|
+
* nobody can sign in and no hub JWT can be minted, so there's no auth gate
|
|
168
|
+
* at all. The loudest warning we draw.
|
|
123
169
|
*/
|
|
124
170
|
async function handleWideOpen(r: Resolved): Promise<void> {
|
|
125
171
|
printDivider(r);
|
|
126
|
-
r.log("⚠ No owner password
|
|
172
|
+
r.log("⚠ No owner password is configured.");
|
|
127
173
|
r.log(" The tunnel is reachable from the public internet RIGHT NOW.");
|
|
128
174
|
r.log(" Anyone with the URL can make requests until you set auth up.");
|
|
129
175
|
r.log("");
|
|
130
|
-
r.log("Recommended: set an owner password
|
|
131
|
-
r.log("and
|
|
176
|
+
r.log("Recommended: set an owner password — it's the gate for both browser");
|
|
177
|
+
r.log("sign-in (OAuth) and minting hub tokens for programmatic clients.");
|
|
132
178
|
r.log("");
|
|
133
179
|
await offerOwnerPassword(r);
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
180
|
+
// Programmatic-client guidance is informational (no auto-mint) — print it
|
|
181
|
+
// so the operator knows the headless path exists, not the dead pvt_* one.
|
|
182
|
+
printTokenGuidance(r);
|
|
183
|
+
// Offer real hub-login 2FA (hub#473) — the box is public now.
|
|
137
184
|
await offerTotp(r);
|
|
138
|
-
await offerTokenCreate(r);
|
|
139
185
|
printDivider(r);
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
/**
|
|
143
|
-
* `password set, no 2FA`: the
|
|
144
|
-
*
|
|
189
|
+
* `password set, no 2FA`: the operator has a password but no second factor.
|
|
190
|
+
* One-line confirmation, then offer to enroll TOTP since the box is public.
|
|
145
191
|
*/
|
|
146
|
-
async function
|
|
192
|
+
async function handlePasswordSetNo2fa(r: Resolved): Promise<void> {
|
|
147
193
|
r.log("");
|
|
148
194
|
r.log("✓ Owner password is set.");
|
|
149
|
-
r.log(" Consider also enabling 2FA for defense-in-depth.");
|
|
150
195
|
await offerTotp(r);
|
|
151
196
|
}
|
|
152
197
|
|
|
153
198
|
/**
|
|
154
|
-
* `
|
|
155
|
-
* nobody can sign in through a browser — the hub's OAuth flow is dead in
|
|
156
|
-
* the water. Offer to fix.
|
|
199
|
+
* `password + 2FA set`: the operator did everything. Two-line confirmation.
|
|
157
200
|
*/
|
|
158
|
-
|
|
201
|
+
function handleFullyConfigured(r: Resolved): void {
|
|
159
202
|
r.log("");
|
|
160
|
-
r.log("
|
|
161
|
-
r
|
|
162
|
-
await offerOwnerPassword(r);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* `tokenCount === null`: SQLite probe failed (DB missing, locked, schema
|
|
167
|
-
* drift, whatever). Don't guess; don't prompt on token state. Nudge 2FA
|
|
168
|
-
* if we know the password is set, otherwise stay quiet.
|
|
169
|
-
*/
|
|
170
|
-
async function handleUnknownTokens(r: Resolved): Promise<void> {
|
|
171
|
-
r.log("");
|
|
172
|
-
r.log("ℹ Couldn't read vault token state (vault may be locked or offline).");
|
|
173
|
-
r.log(" Run `parachute vault tokens list` to check token config yourself.");
|
|
174
|
-
if (r.status.hasOwnerPassword && !r.status.hasTotp) {
|
|
175
|
-
r.log("");
|
|
176
|
-
r.log(" (While you're here: owner password is set, 2FA is not.)");
|
|
177
|
-
await offerTotp(r);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* `all set`: password + 2FA + at least one token. Keep it tight.
|
|
183
|
-
*/
|
|
184
|
-
function handleAllGood(r: Resolved): void {
|
|
185
|
-
r.log("");
|
|
186
|
-
r.log("✓ Auth config looks good (password + 2FA + API tokens).");
|
|
203
|
+
r.log("✓ Owner password is set.");
|
|
204
|
+
note2faOn(r);
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
/**
|
|
190
208
|
* Pick the branch. Pure function of the status — keeps test coverage trivial.
|
|
209
|
+
*
|
|
210
|
+
* Owner-password-centric since the pvt_* DROP (hub#466): `tokenCount` is no
|
|
211
|
+
* longer consulted. Real hub-login 2FA (hub#473) re-introduces the 2FA branch:
|
|
212
|
+
* three states — wide-open, password-but-no-2FA, fully-configured.
|
|
191
213
|
*/
|
|
192
|
-
function classify(
|
|
193
|
-
s
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
const hasTokens = s.tokenCount > 0;
|
|
197
|
-
if (!s.hasOwnerPassword && !hasTokens) return "wide-open";
|
|
198
|
-
if (!s.hasOwnerPassword && hasTokens) return "tokens-no-password";
|
|
199
|
-
if (s.hasOwnerPassword && !s.hasTotp) return "password-no-totp";
|
|
200
|
-
return "all-good";
|
|
214
|
+
function classify(s: VaultAuthStatus): "wide-open" | "password-no-2fa" | "fully-configured" {
|
|
215
|
+
if (!s.hasOwnerPassword) return "wide-open";
|
|
216
|
+
if (!s.hasTotp) return "password-no-2fa";
|
|
217
|
+
return "fully-configured";
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
export async function runAuthPreflight(opts: AuthPreflightOpts = {}): Promise<void> {
|
|
@@ -206,17 +223,11 @@ export async function runAuthPreflight(opts: AuthPreflightOpts = {}): Promise<vo
|
|
|
206
223
|
case "wide-open":
|
|
207
224
|
await handleWideOpen(r);
|
|
208
225
|
return;
|
|
209
|
-
case "password-no-
|
|
210
|
-
await
|
|
211
|
-
return;
|
|
212
|
-
case "tokens-no-password":
|
|
213
|
-
await handleTokensNoPassword(r);
|
|
214
|
-
return;
|
|
215
|
-
case "unknown-tokens":
|
|
216
|
-
await handleUnknownTokens(r);
|
|
226
|
+
case "password-no-2fa":
|
|
227
|
+
await handlePasswordSetNo2fa(r);
|
|
217
228
|
return;
|
|
218
|
-
case "
|
|
219
|
-
|
|
229
|
+
case "fully-configured":
|
|
230
|
+
handleFullyConfigured(r);
|
|
220
231
|
return;
|
|
221
232
|
}
|
|
222
233
|
}
|