@openparachute/hub 0.5.7 → 0.5.10-rc.10

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 (85) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/admin-clients.test.ts +275 -0
  3. package/src/__tests__/admin-handlers.test.ts +70 -323
  4. package/src/__tests__/admin-host-admin-token.test.ts +52 -4
  5. package/src/__tests__/api-me.test.ts +149 -0
  6. package/src/__tests__/api-mint-token.test.ts +381 -0
  7. package/src/__tests__/api-modules-ops.test.ts +658 -0
  8. package/src/__tests__/api-modules.test.ts +426 -0
  9. package/src/__tests__/api-revocation-list.test.ts +198 -0
  10. package/src/__tests__/api-revoke-token.test.ts +320 -0
  11. package/src/__tests__/api-tokens.test.ts +629 -0
  12. package/src/__tests__/auth.test.ts +680 -16
  13. package/src/__tests__/csrf.test.ts +40 -1
  14. package/src/__tests__/expose-2fa-warning.test.ts +3 -5
  15. package/src/__tests__/expose-cloudflare.test.ts +1 -1
  16. package/src/__tests__/expose.test.ts +2 -2
  17. package/src/__tests__/hub-server.test.ts +584 -67
  18. package/src/__tests__/hub-settings.test.ts +377 -0
  19. package/src/__tests__/hub.test.ts +123 -53
  20. package/src/__tests__/install-source.test.ts +249 -0
  21. package/src/__tests__/jwt-sign.test.ts +205 -0
  22. package/src/__tests__/module-manifest.test.ts +48 -0
  23. package/src/__tests__/oauth-handlers.test.ts +522 -5
  24. package/src/__tests__/operator-token.test.ts +427 -3
  25. package/src/__tests__/origin-check.test.ts +220 -0
  26. package/src/__tests__/request-protocol.test.ts +54 -0
  27. package/src/__tests__/serve-boot.test.ts +193 -0
  28. package/src/__tests__/serve.test.ts +100 -0
  29. package/src/__tests__/sessions.test.ts +25 -2
  30. package/src/__tests__/setup-gate.test.ts +222 -0
  31. package/src/__tests__/setup-wizard.test.ts +2089 -0
  32. package/src/__tests__/status.test.ts +199 -0
  33. package/src/__tests__/supervisor.test.ts +482 -0
  34. package/src/__tests__/upgrade.test.ts +247 -4
  35. package/src/__tests__/vault-name.test.ts +79 -0
  36. package/src/__tests__/well-known.test.ts +69 -0
  37. package/src/admin-clients.ts +139 -0
  38. package/src/admin-handlers.ts +37 -254
  39. package/src/admin-host-admin-token.ts +25 -10
  40. package/src/admin-login-ui.ts +256 -0
  41. package/src/admin-vault-admin-token.ts +1 -1
  42. package/src/api-me.ts +124 -0
  43. package/src/api-mint-token.ts +239 -0
  44. package/src/api-modules-ops.ts +585 -0
  45. package/src/api-modules.ts +367 -0
  46. package/src/api-revocation-list.ts +59 -0
  47. package/src/api-revoke-token.ts +153 -0
  48. package/src/api-tokens.ts +224 -0
  49. package/src/cli.ts +28 -0
  50. package/src/commands/auth.ts +408 -51
  51. package/src/commands/expose-2fa-warning.ts +6 -6
  52. package/src/commands/serve-boot.ts +133 -0
  53. package/src/commands/serve.ts +214 -0
  54. package/src/commands/status.ts +74 -10
  55. package/src/commands/upgrade.ts +33 -6
  56. package/src/csrf.ts +34 -13
  57. package/src/help.ts +55 -5
  58. package/src/hub-control.ts +1 -0
  59. package/src/hub-db.ts +87 -0
  60. package/src/hub-server.ts +767 -136
  61. package/src/hub-settings.ts +259 -0
  62. package/src/hub.ts +298 -150
  63. package/src/install-source.ts +291 -0
  64. package/src/jwt-sign.ts +265 -5
  65. package/src/module-manifest.ts +48 -10
  66. package/src/oauth-handlers.ts +262 -56
  67. package/src/oauth-ui.ts +23 -2
  68. package/src/operator-token.ts +349 -18
  69. package/src/origin-check.ts +127 -0
  70. package/src/rate-limit.ts +5 -2
  71. package/src/request-protocol.ts +48 -0
  72. package/src/scope-explanations.ts +33 -2
  73. package/src/sessions.ts +30 -18
  74. package/src/setup-wizard.ts +2009 -0
  75. package/src/supervisor.ts +411 -0
  76. package/src/vault-name.ts +71 -0
  77. package/src/well-known.ts +54 -1
  78. package/web/ui/dist/assets/index-BDSEsaBY.css +1 -0
  79. package/web/ui/dist/assets/index-CP07NbdF.js +61 -0
  80. package/web/ui/dist/index.html +2 -2
  81. package/src/__tests__/admin-config.test.ts +0 -281
  82. package/src/admin-config-ui.ts +0 -534
  83. package/src/admin-config.ts +0 -226
  84. package/web/ui/dist/assets/index-BKzPDdB0.js +0 -60
  85. package/web/ui/dist/assets/index-Dyk6g7vT.css +0 -1
@@ -66,6 +66,30 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
66
66
  "Provision and manage vaults across this host (create new vaults, configure cross-vault settings).",
67
67
  level: "admin",
68
68
  },
69
+ // Fine-grained host scopes (#213) — the `parachute auth rotate-operator
70
+ // --scope-set <set>` vocabulary. Each is a narrowing of `parachute:host:admin`:
71
+ // an operator who wants a tighter token mints with one of these and uses
72
+ // it in place of the broad operator.token. Operator-only (non-requestable).
73
+ "parachute:host:install": {
74
+ label: "Install or upgrade Parachute modules on this host.",
75
+ level: "admin",
76
+ },
77
+ "parachute:host:start": {
78
+ label: "Lifecycle Parachute modules on this host (start, stop, restart, status).",
79
+ level: "admin",
80
+ },
81
+ "parachute:host:expose": {
82
+ label: "Bring tailnet or public exposure layers up and down on this host.",
83
+ level: "admin",
84
+ },
85
+ "parachute:host:auth": {
86
+ label: "Mint hub-issued tokens and manage user accounts on this host.",
87
+ level: "admin",
88
+ },
89
+ "parachute:host:vault": {
90
+ label: "Administer vaults on this host (create, configure, delete).",
91
+ level: "admin",
92
+ },
69
93
  };
70
94
 
71
95
  /**
@@ -83,7 +107,7 @@ export const FIRST_PARTY_SCOPES = Object.keys(SCOPE_EXPLANATIONS).sort();
83
107
  * - `parachute auth rotate-operator` writes the long-lived operator token
84
108
  * (`~/.parachute/operator.token`, mode 0600) for service accounts.
85
109
  * - `GET /admin/host-admin-token` exchanges a valid `parachute_hub_session`
86
- * cookie (set by `/admin/login` after a password check) for a
110
+ * cookie (set by `/login` after a password check) for a
87
111
  * short-lived JWT consumed by the in-tree vault-management SPA.
88
112
  *
89
113
  * Both surfaces predicate on local-operator identity that the public OAuth
@@ -104,7 +128,14 @@ export const FIRST_PARTY_SCOPES = Object.keys(SCOPE_EXPLANATIONS).sort();
104
128
  * intentional: the blast radius of compromised cross-vault admin doesn't
105
129
  * justify third-party requestability.
106
130
  */
107
- export const NON_REQUESTABLE_SCOPES: ReadonlySet<string> = new Set(["parachute:host:admin"]);
131
+ export const NON_REQUESTABLE_SCOPES: ReadonlySet<string> = new Set([
132
+ "parachute:host:admin",
133
+ "parachute:host:install",
134
+ "parachute:host:start",
135
+ "parachute:host:expose",
136
+ "parachute:host:auth",
137
+ "parachute:host:vault",
138
+ ]);
108
139
 
109
140
  /**
110
141
  * Per-vault `vault:<name>:admin` scopes are also non-requestable: they let
package/src/sessions.ts CHANGED
@@ -83,26 +83,38 @@ export function deleteSession(db: Database, id: string): void {
83
83
 
84
84
  /**
85
85
  * Build a `Set-Cookie` header value for the given session id. HttpOnly +
86
- * SameSite=Lax + Secure (we always assume a TLS terminator; localhost dev
87
- * still sets Secure because Tailscale serves with HTTPS even on the tailnet
88
- * mount). Path=/ covers the whole hub origin: the operator's session is "logged
89
- * into this hub", and admin pages outside /oauth/ (config portal, etc.) ride
90
- * the same session. State-changing admin POSTs require a CSRF token (see
91
- * src/csrf.ts) since SameSite=Lax alone doesn't prevent same-site CSRF.
86
+ * SameSite=Lax + Secure (conditional) + Path=/.
87
+ *
88
+ * Path=/ covers the whole hub origin: the operator's session is "logged
89
+ * into this hub", and admin pages outside /oauth/ (config portal, etc.)
90
+ * ride the same session. State-changing admin POSTs require a CSRF token
91
+ * (see src/csrf.ts) since SameSite=Lax alone doesn't prevent same-site
92
+ * CSRF.
93
+ *
94
+ * `Secure` defaults to true (production behind a TLS terminator).
95
+ * Callers minting the cookie for a known-HTTP request — `/login` POST
96
+ * over `http://localhost:1939`, the wizard's account POST same — pass
97
+ * `secure: false` (computed from `isHttpsRequest(req)`) so the
98
+ * browser actually keeps the cookie. Setting `Secure` unconditionally
99
+ * over plain HTTP silently drops the cookie and breaks the very next
100
+ * authenticated request.
92
101
  */
93
- export function buildSessionCookie(sessionId: string, maxAgeSeconds: number): string {
94
- return [
95
- `${SESSION_COOKIE_NAME}=${sessionId}`,
96
- "HttpOnly",
97
- "Secure",
98
- "SameSite=Lax",
99
- "Path=/",
100
- `Max-Age=${maxAgeSeconds}`,
101
- ].join("; ");
102
+ export function buildSessionCookie(
103
+ sessionId: string,
104
+ maxAgeSeconds: number,
105
+ opts: { secure?: boolean } = {},
106
+ ): string {
107
+ const parts = [`${SESSION_COOKIE_NAME}=${sessionId}`, "HttpOnly"];
108
+ if (opts.secure !== false) parts.push("Secure");
109
+ parts.push("SameSite=Lax", "Path=/", `Max-Age=${maxAgeSeconds}`);
110
+ return parts.join("; ");
102
111
  }
103
112
 
104
- export function buildSessionClearCookie(): string {
105
- return `${SESSION_COOKIE_NAME}=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0`;
113
+ export function buildSessionClearCookie(opts: { secure?: boolean } = {}): string {
114
+ const parts = [`${SESSION_COOKIE_NAME}=`, "HttpOnly"];
115
+ if (opts.secure !== false) parts.push("Secure");
116
+ parts.push("SameSite=Lax", "Path=/", "Max-Age=0");
117
+ return parts.join("; ");
106
118
  }
107
119
 
108
120
  export function parseSessionCookie(cookieHeader: string | null): string | null {
@@ -121,7 +133,7 @@ export function parseSessionCookie(cookieHeader: string | null): string | null {
121
133
  * callers don't repeat the parse+find+null-check dance.
122
134
  *
123
135
  * Caller decides what to do on null — admin pages redirect to
124
- * `/admin/login?next=<path>`, OAuth's DCR endpoint falls through to
136
+ * `/login?next=<path>`, OAuth's DCR endpoint falls through to
125
137
  * status=`pending` (closes #199).
126
138
  */
127
139
  export function findActiveSession(