@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.
Files changed (87) hide show
  1. package/README.md +109 -15
  2. package/package.json +7 -3
  3. package/src/__tests__/account-home-ui.test.ts +251 -15
  4. package/src/__tests__/account-vault-token.test.ts +355 -0
  5. package/src/__tests__/admin-vaults.test.ts +70 -4
  6. package/src/__tests__/api-mint-token.test.ts +693 -5
  7. package/src/__tests__/api-modules-ops.test.ts +45 -0
  8. package/src/__tests__/api-revoke-token.test.ts +384 -0
  9. package/src/__tests__/api-users.test.ts +7 -2
  10. package/src/__tests__/auth.test.ts +157 -30
  11. package/src/__tests__/cli.test.ts +44 -5
  12. package/src/__tests__/expose-2fa-warning.test.ts +31 -17
  13. package/src/__tests__/expose-auth-preflight.test.ts +71 -72
  14. package/src/__tests__/expose-cloudflare.test.ts +482 -14
  15. package/src/__tests__/expose.test.ts +52 -2
  16. package/src/__tests__/hub-server.test.ts +97 -0
  17. package/src/__tests__/hub.test.ts +85 -6
  18. package/src/__tests__/init.test.ts +102 -1
  19. package/src/__tests__/lifecycle.test.ts +464 -2
  20. package/src/__tests__/oauth-handlers.test.ts +1252 -83
  21. package/src/__tests__/oauth-ui.test.ts +12 -1
  22. package/src/__tests__/operator-token-issuer-self-heal.test.ts +412 -0
  23. package/src/__tests__/resource-binding.test.ts +97 -0
  24. package/src/__tests__/scope-explanations.test.ts +77 -12
  25. package/src/__tests__/services-manifest.test.ts +122 -4
  26. package/src/__tests__/setup-wizard.test.ts +335 -15
  27. package/src/__tests__/status.test.ts +36 -0
  28. package/src/__tests__/two-factor-flow.test.ts +602 -0
  29. package/src/__tests__/two-factor.test.ts +183 -0
  30. package/src/__tests__/upgrade.test.ts +78 -1
  31. package/src/__tests__/users.test.ts +68 -0
  32. package/src/__tests__/vault-auth-status.test.ts +47 -6
  33. package/src/__tests__/vault-hub-origin-env.test.ts +263 -0
  34. package/src/account-home-ui.ts +488 -38
  35. package/src/account-vault-token.ts +282 -0
  36. package/src/admin-handlers.ts +159 -4
  37. package/src/admin-login-ui.ts +49 -5
  38. package/src/admin-vaults.ts +48 -15
  39. package/src/api-account.ts +14 -0
  40. package/src/api-mint-token.ts +132 -24
  41. package/src/api-modules-ops.ts +49 -11
  42. package/src/api-revoke-token.ts +107 -21
  43. package/src/api-users.ts +29 -3
  44. package/src/cli.ts +26 -21
  45. package/src/clients.ts +18 -6
  46. package/src/cloudflare/config.ts +10 -4
  47. package/src/cloudflare/detect.ts +39 -44
  48. package/src/commands/auth.ts +165 -24
  49. package/src/commands/expose-2fa-warning.ts +34 -32
  50. package/src/commands/expose-auth-preflight.ts +89 -78
  51. package/src/commands/expose-cloudflare.ts +370 -12
  52. package/src/commands/expose.ts +8 -0
  53. package/src/commands/init.ts +33 -2
  54. package/src/commands/lifecycle.ts +386 -17
  55. package/src/commands/status.ts +22 -0
  56. package/src/commands/upgrade.ts +55 -11
  57. package/src/commands/wizard.ts +8 -4
  58. package/src/env-file.ts +10 -0
  59. package/src/help.ts +3 -1
  60. package/src/hub-db.ts +39 -1
  61. package/src/hub-server.ts +52 -0
  62. package/src/hub.ts +82 -14
  63. package/src/oauth-handlers.ts +298 -21
  64. package/src/oauth-ui.ts +10 -0
  65. package/src/operator-token.ts +151 -0
  66. package/src/pending-login.ts +116 -0
  67. package/src/rate-limit.ts +51 -0
  68. package/src/resource-binding.ts +134 -0
  69. package/src/scope-attenuation.ts +85 -0
  70. package/src/scope-explanations.ts +131 -14
  71. package/src/services-manifest.ts +112 -0
  72. package/src/setup-wizard.ts +77 -7
  73. package/src/tailscale/run.ts +28 -11
  74. package/src/totp.ts +201 -0
  75. package/src/two-factor-handlers.ts +287 -0
  76. package/src/two-factor-store.ts +181 -0
  77. package/src/two-factor-ui.ts +462 -0
  78. package/src/users.ts +58 -0
  79. package/src/vault/auth-status.ts +71 -19
  80. package/src/vault-hub-origin-env.ts +163 -0
  81. package/web/ui/dist/assets/index-BiBlvEaj.css +1 -0
  82. package/web/ui/dist/assets/index-CIN3mnmf.js +61 -0
  83. package/web/ui/dist/index.html +2 -2
  84. package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
  85. package/src/commands/vault-tokens-create-interactive.ts +0 -143
  86. package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
  87. 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
- * password or tokens set" trap before someone else finds it first.
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
- * Four states we branch on, based on {@link VaultAuthStatus}:
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
- * - neither password nor tokens: loud warning + offer to set up each.
11
- * - password, no 2FA: shorter "recommend 2FA" nudge.
12
- * - tokens but no password: OAuth isn't set up; offer to add a password.
13
- * - `tokenCount === null`: couldn't read the DB; advisory only, no prompts
14
- * that depend on token state.
15
- * - all set: one-line "looks good" (the quiet path).
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 …` or `parachute vault tokens create` later.
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
- if (await yesNo(r, "Enable TOTP 2FA now?")) {
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
- async function offerTokenCreate(r: Resolved): Promise<void> {
110
- if (await yesNo(r, "Create an API token now?")) {
111
- await runCmd(r, ["parachute", "vault", "tokens", "create"], "parachute vault tokens create");
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
- * `neither password nor tokens`: the exposure is wide open — anyone who
122
- * finds the URL can talk to the vault. The loudest warning we draw.
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 and no API tokens are configured.");
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 (enables the browser sign-in flow)");
131
- r.log("and/or create an API token (for programmatic clients).");
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
- // Offer 2FA regardless of the password step outcome: we can't observe it
135
- // from outside the subprocess, and vault itself will reject a 2fa enroll
136
- // if there's no password yet, surfacing the real error to the user.
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 common case where the user did the obvious
144
- * thing but hasn't opted into the stronger factor yet. Short nudge.
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 handlePasswordNoTotp(r: Resolved): Promise<void> {
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
- * `tokens exist, no password`: vault is authenticated for API clients but
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
- async function handleTokensNoPassword(r: Resolved): Promise<void> {
201
+ function handleFullyConfigured(r: Resolved): void {
159
202
  r.log("");
160
- r.log(" API tokens exist, but no owner password is set.");
161
- r.log(" Browser sign-in (OAuth) won't work until you add one.");
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: VaultAuthStatus,
194
- ): "wide-open" | "password-no-totp" | "tokens-no-password" | "unknown-tokens" | "all-good" {
195
- if (s.tokenCount === null) return "unknown-tokens";
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-totp":
210
- await handlePasswordNoTotp(r);
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 "all-good":
219
- handleAllGood(r);
229
+ case "fully-configured":
230
+ handleFullyConfigured(r);
220
231
  return;
221
232
  }
222
233
  }