@openparachute/hub 0.5.14-rc.1 → 0.5.14-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.
- package/README.md +109 -15
- package/package.json +1 -1
- package/src/__tests__/account-home-ui.test.ts +83 -18
- package/src/__tests__/admin-vault-admin-token.test.ts +2 -2
- package/src/__tests__/admin-vaults.test.ts +70 -4
- package/src/__tests__/api-account.test.ts +2 -2
- package/src/__tests__/api-mint-token.test.ts +682 -3
- package/src/__tests__/api-modules-config.test.ts +16 -10
- package/src/__tests__/api-modules-ops.test.ts +97 -0
- package/src/__tests__/api-modules.test.ts +92 -75
- package/src/__tests__/api-ready.test.ts +135 -0
- package/src/__tests__/api-revoke-token.test.ts +384 -0
- package/src/__tests__/api-users.test.ts +198 -11
- package/src/__tests__/cli.test.ts +7 -5
- package/src/__tests__/cloudflare-detect.test.ts +60 -5
- package/src/__tests__/expose-auth-preflight.test.ts +58 -50
- package/src/__tests__/expose-cloudflare.test.ts +114 -3
- package/src/__tests__/expose-interactive.test.ts +10 -4
- package/src/__tests__/expose-public-auto.test.ts +5 -1
- package/src/__tests__/expose.test.ts +49 -1
- package/src/__tests__/hub-db.test.ts +194 -29
- package/src/__tests__/hub-server.test.ts +299 -10
- package/src/__tests__/hub.test.ts +11 -0
- package/src/__tests__/init.test.ts +827 -0
- package/src/__tests__/lifecycle.test.ts +33 -1
- package/src/__tests__/migrate.test.ts +433 -51
- package/src/__tests__/oauth-handlers.test.ts +1060 -29
- package/src/__tests__/oauth-ui.test.ts +12 -1
- package/src/__tests__/proxy-error-ui.test.ts +212 -0
- package/src/__tests__/proxy-state.test.ts +192 -0
- package/src/__tests__/resource-binding.test.ts +97 -0
- package/src/__tests__/scope-explanations.test.ts +36 -0
- package/src/__tests__/serve.test.ts +9 -9
- package/src/__tests__/setup-wizard.test.ts +969 -59
- package/src/__tests__/users.test.ts +135 -9
- package/src/__tests__/vault-auth-status.test.ts +271 -11
- package/src/__tests__/vault-hub-origin-env.test.ts +126 -0
- package/src/__tests__/wizard.test.ts +372 -0
- package/src/account-home-ui.ts +196 -53
- package/src/admin-login-ui.ts +4 -4
- package/src/admin-vaults.ts +48 -15
- package/src/api-account.ts +1 -1
- package/src/api-mint-token.ts +132 -24
- package/src/api-modules-ops.ts +52 -16
- package/src/api-modules.ts +29 -12
- package/src/api-ready.ts +102 -0
- package/src/api-revoke-token.ts +107 -21
- package/src/api-users.ts +332 -54
- package/src/bun-link.ts +55 -0
- package/src/cli.ts +93 -24
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +73 -6
- package/src/commands/expose-auth-preflight.ts +55 -63
- package/src/commands/expose-cloudflare.ts +114 -10
- package/src/commands/expose-interactive.ts +10 -11
- package/src/commands/expose-public-auto.ts +6 -4
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +563 -0
- package/src/commands/install.ts +41 -23
- package/src/commands/lifecycle.ts +12 -0
- package/src/commands/migrate.ts +293 -41
- package/src/commands/wizard.ts +843 -0
- package/src/env-file.ts +10 -0
- package/src/help.ts +155 -15
- package/src/hub-db.ts +42 -0
- package/src/hub-server.ts +86 -13
- package/src/hub-settings.ts +11 -0
- package/src/hub.ts +10 -3
- package/src/oauth-handlers.ts +342 -173
- package/src/oauth-ui.ts +28 -2
- package/src/proxy-error-ui.ts +506 -0
- package/src/proxy-state.ts +131 -0
- package/src/resource-binding.ts +134 -0
- package/src/scope-attenuation.ts +85 -0
- package/src/scope-explanations.ts +94 -5
- package/src/setup-wizard.ts +928 -179
- package/src/users.ts +195 -29
- package/src/vault/auth-status.ts +152 -25
- package/src/vault-hub-origin-env.ts +100 -0
- package/web/ui/dist/assets/index-2SSK7JbM.js +61 -0
- package/web/ui/dist/assets/index-B28SdMSz.css +1 -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-Qf56GsGm.js +0 -61
package/README.md
CHANGED
|
@@ -61,33 +61,122 @@ Operators who want env-var-driven seeding (CI, scripted deploys) can still set `
|
|
|
61
61
|
|
|
62
62
|
## First 5 minutes
|
|
63
63
|
|
|
64
|
+
One command gets you from a fresh install to the setup wizard:
|
|
65
|
+
|
|
64
66
|
```sh
|
|
65
67
|
# 1. Install the hub (one line — installs the `parachute` binary)
|
|
66
68
|
bun add -g @openparachute/hub
|
|
67
69
|
|
|
68
|
-
# 2.
|
|
69
|
-
|
|
70
|
+
# 2. parachute init — the unified front door (laptop, EC2, any VPS).
|
|
71
|
+
# It starts the hub, offers to expose it, always installs the vault
|
|
72
|
+
# module, then drops you into the setup wizard.
|
|
73
|
+
parachute init
|
|
74
|
+
```
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
`parachute init` is idempotent — every re-run is safe. End to end it:
|
|
77
|
+
|
|
78
|
+
1. **Starts the hub** if it isn't already running (port `1939`).
|
|
79
|
+
2. **Offers to expose it** so you can reach the wizard from other devices. In a
|
|
80
|
+
terminal you pick: stay loopback-only, your **tailnet** (`tailscale serve` —
|
|
81
|
+
private to your own Tailscale devices), or a **Cloudflare Tunnel** (public
|
|
82
|
+
HTTPS on your own domain). The default highlights "no thanks — loopback" on a
|
|
83
|
+
laptop and pre-selects Cloudflare on an SSH'd server. Skip with
|
|
84
|
+
`--no-expose-prompt`, or pin non-interactively with
|
|
85
|
+
`--expose none|tailnet|cloudflare`.
|
|
86
|
+
3. **Installs the vault module** — always — so the wizard can offer
|
|
87
|
+
create / import / skip. No vault *instance* is created yet; that's the
|
|
88
|
+
wizard's call.
|
|
89
|
+
4. **Drops you into the setup wizard.** Browser by default (opens
|
|
90
|
+
`/admin/setup`); pick the in-terminal walk-through with `--cli-wizard`, or
|
|
91
|
+
force the browser with `--browser-wizard`. It prints the canonical admin URL
|
|
92
|
+
either way — loopback when you're not exposed, the tailnet / Cloudflare FQDN
|
|
93
|
+
when you are.
|
|
94
|
+
|
|
95
|
+
The wizard walks the same three steps in the browser and the CLI:
|
|
96
|
+
|
|
97
|
+
- **Account** — create the admin operator for this hub (username + password).
|
|
98
|
+
- **Vault** — *create* a fresh vault (default name `default`), *import* one from
|
|
99
|
+
a git repo (a previously-exported Parachute vault on any HTTPS / SSH remote;
|
|
100
|
+
PAT optional for private repos), or *skip* and create one later. The vault
|
|
101
|
+
module is installed regardless of which you pick.
|
|
102
|
+
- **Expose** — record how this hub is reached (localhost / tailnet / public) so
|
|
103
|
+
the done screen surfaces the right URLs.
|
|
104
|
+
|
|
105
|
+
The done screen hands you a copy-pasteable `claude mcp add` command (with a
|
|
106
|
+
freshly-minted operator token), a link to start using your vault, and the admin
|
|
107
|
+
UI. Verify the stack any time:
|
|
73
108
|
|
|
74
|
-
|
|
109
|
+
```sh
|
|
75
110
|
parachute status
|
|
76
111
|
# SERVICE PORT VERSION PROCESS PID UPTIME HEALTH LATENCY
|
|
77
|
-
# parachute-
|
|
112
|
+
# parachute-hub 1939 0.5.14 running 12344 20s ok 1ms
|
|
113
|
+
# parachute-vault 1940 0.4.5 running 12345 12s ok 2ms
|
|
114
|
+
```
|
|
78
115
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# http://127.0.0.1:1940/vault/default/mcp
|
|
116
|
+
Vault is up on `127.0.0.1:1940`; Claude Code picks up the MCP on your next
|
|
117
|
+
session. Point any other local MCP client (Codex, Goose, OpenCode, Cursor, Zed,
|
|
118
|
+
Cline, your own agent) at `http://127.0.0.1:1940/vault/<name>/mcp`.
|
|
83
119
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
parachute
|
|
120
|
+
### Want the wizard in the terminal instead of the browser?
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
parachute init --cli-wizard
|
|
88
124
|
```
|
|
89
125
|
|
|
90
|
-
|
|
126
|
+
…or drive the wizard directly against an already-running hub:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
parachute setup-wizard --hub-url http://127.0.0.1:1939
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`setup-wizard` is the in-terminal mirror of `/admin/setup` — same handlers, same
|
|
133
|
+
Account → Vault → Expose walk. Every prompt has a paired flag for scripted /
|
|
134
|
+
non-interactive setup (`--account-username`, `--account-password`,
|
|
135
|
+
`--vault-mode create|import|skip`, `--vault-name`, `--vault-import-url`,
|
|
136
|
+
`--expose-mode localhost|tailnet|public`, …); run `parachute setup-wizard --help`
|
|
137
|
+
for the full list.
|
|
138
|
+
|
|
139
|
+
### Prefer to drive installs by hand?
|
|
140
|
+
|
|
141
|
+
`parachute init` → wizard is the recommended path, but the per-module commands
|
|
142
|
+
still work and are additive:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
parachute install vault # install + register + create first vault + start one module
|
|
146
|
+
parachute setup # older interactive multi-pick: survey + install vault/notes/scribe
|
|
147
|
+
parachute start vault # PID + logs tracked under ~/.parachute/vault/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Expose across your tailnet
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
parachute expose tailnet # HTTPS, MagicDNS, only your devices (the supported shape today)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Tear down with `parachute expose tailnet off`. The public layer (`expose public off`) tears down independently — `off` only affects the layer you name. Public-internet exposure is exploratory (see "Public exposure" below).
|
|
157
|
+
|
|
158
|
+
### Onboarding a team member
|
|
159
|
+
|
|
160
|
+
Want to give a friend or teammate their own account on your hub? The flow is
|
|
161
|
+
self-service end to end:
|
|
162
|
+
|
|
163
|
+
1. **Create the user.** In the admin UI, open **Users → Create User**: pick a
|
|
164
|
+
username, set a temporary password, and (optionally) assign one or more
|
|
165
|
+
vaults. The success banner echoes the exact sign-in URL.
|
|
166
|
+
2. **Hand them three things:** your hub's sign-in URL (`<hub-origin>/login`),
|
|
167
|
+
their username, and the temporary password you set.
|
|
168
|
+
3. **They sign in and set their own password.** On first sign-in they're
|
|
169
|
+
prompted to change it — they can't reach the rest of the hub until they do.
|
|
170
|
+
4. **Later changes are self-service.** A signed-in user changes their password
|
|
171
|
+
anytime from their account home at `/account/` (reachable via the **Account**
|
|
172
|
+
link in the top-right once signed in).
|
|
173
|
+
5. **You can reset it for them.** If they're locked out, the **Reset password**
|
|
174
|
+
button on the Users row sets a new temporary password and re-arms the
|
|
175
|
+
force-change-on-next-sign-in prompt.
|
|
176
|
+
|
|
177
|
+
The first admin (the wizard / env-seeded account) is unrestricted and can't be
|
|
178
|
+
deleted or reset from the Users page — it changes its own password at
|
|
179
|
+
`/account/change-password` directly.
|
|
91
180
|
|
|
92
181
|
## Service lifecycle
|
|
93
182
|
|
|
@@ -313,6 +402,11 @@ Public-internet exposure (`parachute expose public`) is exploratory — see "Pub
|
|
|
313
402
|
Run `parachute --help` for the top-level list, and `parachute <subcommand> --help` for details on any individual command.
|
|
314
403
|
|
|
315
404
|
```
|
|
405
|
+
parachute init fresh-install front door: start hub, offer expose,
|
|
406
|
+
install vault module, open the setup wizard
|
|
407
|
+
parachute setup-wizard --hub-url <url>
|
|
408
|
+
in-terminal mirror of /admin/setup (Account/Vault/Expose)
|
|
409
|
+
parachute setup older interactive multi-pick service installer
|
|
316
410
|
parachute install <service> install and register a service
|
|
317
411
|
parachute status show installed services, process state, health
|
|
318
412
|
parachute start [service] start services in the background
|
package/package.json
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* tests pin the load-bearing shape:
|
|
5
5
|
*
|
|
6
6
|
* - Assigned-vault branch: Notes CTA href encodes the hub+vault URL,
|
|
7
|
-
* vault name shows in the body,
|
|
8
|
-
*
|
|
7
|
+
* vault name shows in the body, AND a per-tile MCP connect block
|
|
8
|
+
* surfaces the endpoint + `claude mcp add` command (OAuth, no token)
|
|
9
|
+
* with copy buttons — the multi-user friend-connect surface.
|
|
9
10
|
* - Admin (no assigned vault) branch: link to /admin/ visible.
|
|
10
11
|
* - Defensive third branch (non-admin + no vault): "ask the operator"
|
|
11
12
|
* copy renders.
|
|
@@ -13,7 +14,11 @@
|
|
|
13
14
|
* change-password link.
|
|
14
15
|
*/
|
|
15
16
|
import { describe, expect, test } from "bun:test";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
accountClaudeMcpAddCommand,
|
|
19
|
+
accountMcpEndpoint,
|
|
20
|
+
renderAccountHome,
|
|
21
|
+
} from "../account-home-ui.ts";
|
|
17
22
|
|
|
18
23
|
const HUB_ORIGIN = "https://hub.example";
|
|
19
24
|
const CSRF = "test-csrf-token";
|
|
@@ -22,7 +27,7 @@ describe("renderAccountHome", () => {
|
|
|
22
27
|
test("assigned-vault branch — Notes CTA carries the encoded hub+vault URL", () => {
|
|
23
28
|
const html = renderAccountHome({
|
|
24
29
|
username: "alice",
|
|
25
|
-
|
|
30
|
+
assignedVaults: ["alice"],
|
|
26
31
|
passwordChanged: true,
|
|
27
32
|
hubOrigin: HUB_ORIGIN,
|
|
28
33
|
isFirstAdmin: false,
|
|
@@ -38,9 +43,19 @@ describe("renderAccountHome", () => {
|
|
|
38
43
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${encodedVaultUrl}`);
|
|
39
44
|
expect(html).toContain('target="_blank"');
|
|
40
45
|
expect(html).toContain('rel="noopener"');
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
expect(html).toContain(
|
|
46
|
+
// The friend-connect surface: MCP endpoint + `claude mcp add` command,
|
|
47
|
+
// each with a copy button. OAuth path (no token in the command).
|
|
48
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
49
|
+
expect(html).toContain(
|
|
50
|
+
`claude mcp add --transport http parachute-alice ${HUB_ORIGIN}/vault/alice/mcp`,
|
|
51
|
+
);
|
|
52
|
+
expect(html).toContain('data-testid="copy-mcp-endpoint"');
|
|
53
|
+
expect(html).toContain('data-testid="copy-mcp-add-command"');
|
|
54
|
+
// The connect command must NOT embed a token — the OAuth path needs none.
|
|
55
|
+
expect(html).not.toContain("--header");
|
|
56
|
+
expect(html).not.toContain("Authorization: Bearer");
|
|
57
|
+
// Copy-button progressive-enhancement script is present.
|
|
58
|
+
expect(html).toContain("navigator.clipboard");
|
|
44
59
|
});
|
|
45
60
|
|
|
46
61
|
test("assigned-vault branch — trailing slash on hubOrigin is normalized", () => {
|
|
@@ -49,7 +64,7 @@ describe("renderAccountHome", () => {
|
|
|
49
64
|
// renderer must produce a clean `/vault/<name>` join either way.
|
|
50
65
|
const html = renderAccountHome({
|
|
51
66
|
username: "alice",
|
|
52
|
-
|
|
67
|
+
assignedVaults: ["alice"],
|
|
53
68
|
passwordChanged: true,
|
|
54
69
|
hubOrigin: `${HUB_ORIGIN}/`,
|
|
55
70
|
isFirstAdmin: false,
|
|
@@ -57,15 +72,16 @@ describe("renderAccountHome", () => {
|
|
|
57
72
|
});
|
|
58
73
|
const cleanEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/alice`);
|
|
59
74
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${cleanEncoded}`);
|
|
60
|
-
// The
|
|
61
|
-
|
|
62
|
-
expect(html).
|
|
75
|
+
// The MCP endpoint + connect command also drop the trailing slash — no
|
|
76
|
+
// `//vault` double-slash sneaks in.
|
|
77
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/alice/mcp`);
|
|
78
|
+
expect(html).not.toContain(`${HUB_ORIGIN}//vault`);
|
|
63
79
|
});
|
|
64
80
|
|
|
65
81
|
test("admin branch — null assignedVault + isFirstAdmin renders an /admin/ link", () => {
|
|
66
82
|
const html = renderAccountHome({
|
|
67
83
|
username: "admin",
|
|
68
|
-
|
|
84
|
+
assignedVaults: [],
|
|
69
85
|
passwordChanged: true,
|
|
70
86
|
hubOrigin: HUB_ORIGIN,
|
|
71
87
|
isFirstAdmin: true,
|
|
@@ -85,7 +101,7 @@ describe("renderAccountHome", () => {
|
|
|
85
101
|
// state via hand-edit or migration race.
|
|
86
102
|
const html = renderAccountHome({
|
|
87
103
|
username: "ghost",
|
|
88
|
-
|
|
104
|
+
assignedVaults: [],
|
|
89
105
|
passwordChanged: true,
|
|
90
106
|
hubOrigin: HUB_ORIGIN,
|
|
91
107
|
isFirstAdmin: false,
|
|
@@ -102,7 +118,7 @@ describe("renderAccountHome", () => {
|
|
|
102
118
|
test("account card — change-password link and sign-out form are present", () => {
|
|
103
119
|
const html = renderAccountHome({
|
|
104
120
|
username: "alice",
|
|
105
|
-
|
|
121
|
+
assignedVaults: ["alice"],
|
|
106
122
|
passwordChanged: true,
|
|
107
123
|
hubOrigin: HUB_ORIGIN,
|
|
108
124
|
isFirstAdmin: false,
|
|
@@ -120,21 +136,70 @@ describe("renderAccountHome", () => {
|
|
|
120
136
|
expect(html).toContain("<code>alice</code>");
|
|
121
137
|
});
|
|
122
138
|
|
|
139
|
+
test("multi-vault branch — renders one tile per assigned vault (Phase 2 PR 2)", () => {
|
|
140
|
+
const html = renderAccountHome({
|
|
141
|
+
username: "alice",
|
|
142
|
+
assignedVaults: ["personal", "family"],
|
|
143
|
+
passwordChanged: true,
|
|
144
|
+
hubOrigin: HUB_ORIGIN,
|
|
145
|
+
isFirstAdmin: false,
|
|
146
|
+
csrfToken: CSRF,
|
|
147
|
+
});
|
|
148
|
+
// Plural heading.
|
|
149
|
+
expect(html).toContain("Your vaults");
|
|
150
|
+
// Each vault name appears.
|
|
151
|
+
expect(html).toContain("<strong>personal</strong>");
|
|
152
|
+
expect(html).toContain("<strong>family</strong>");
|
|
153
|
+
// One CTA per vault with the right encoded URL.
|
|
154
|
+
const personalEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/personal`);
|
|
155
|
+
const familyEncoded = encodeURIComponent(`${HUB_ORIGIN}/vault/family`);
|
|
156
|
+
expect(html).toContain(`https://notes.parachute.computer/add?url=${personalEncoded}`);
|
|
157
|
+
expect(html).toContain(`https://notes.parachute.computer/add?url=${familyEncoded}`);
|
|
158
|
+
// One per-vault MCP connect block per tile (endpoint + command).
|
|
159
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/personal/mcp`);
|
|
160
|
+
expect(html).toContain(`${HUB_ORIGIN}/vault/family/mcp`);
|
|
161
|
+
expect(html).toContain(
|
|
162
|
+
`claude mcp add --transport http parachute-personal ${HUB_ORIGIN}/vault/personal/mcp`,
|
|
163
|
+
);
|
|
164
|
+
expect(html).toContain(
|
|
165
|
+
`claude mcp add --transport http parachute-family ${HUB_ORIGIN}/vault/family/mcp`,
|
|
166
|
+
);
|
|
167
|
+
// Two tiles → two copy-endpoint buttons.
|
|
168
|
+
expect(html.split('data-testid="copy-mcp-endpoint"').length - 1).toBe(2);
|
|
169
|
+
// The copy script is emitted once at the section level, not per-tile.
|
|
170
|
+
expect(html.split("<script>").length - 1).toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("accountMcpEndpoint / accountClaudeMcpAddCommand build the canonical shapes", () => {
|
|
174
|
+
expect(accountMcpEndpoint("https://hub.example", "work")).toBe(
|
|
175
|
+
"https://hub.example/vault/work/mcp",
|
|
176
|
+
);
|
|
177
|
+
expect(accountClaudeMcpAddCommand("https://hub.example", "work")).toBe(
|
|
178
|
+
"claude mcp add --transport http parachute-work https://hub.example/vault/work/mcp",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
123
182
|
test("escapes hostile content in username and vault name", () => {
|
|
124
183
|
// Defense-in-depth: usernames pass validateUsername (lowercase alnum
|
|
125
184
|
// + `_-`), so HTML metacharacters won't normally make it through. But
|
|
126
185
|
// the renderer is a pure function over arbitrary string input and the
|
|
127
186
|
// escape is load-bearing if the validator ever loosens.
|
|
128
187
|
const html = renderAccountHome({
|
|
129
|
-
username: "<script>",
|
|
130
|
-
|
|
188
|
+
username: "<script>alert(1)</script>",
|
|
189
|
+
assignedVaults: ["<vault>"],
|
|
131
190
|
passwordChanged: true,
|
|
132
191
|
hubOrigin: HUB_ORIGIN,
|
|
133
192
|
isFirstAdmin: false,
|
|
134
193
|
csrfToken: CSRF,
|
|
135
194
|
});
|
|
136
|
-
|
|
137
|
-
|
|
195
|
+
// The injected username/vault metacharacters are escaped — the only
|
|
196
|
+
// `<script>` tag in the output is the page's own copy-button helper, so
|
|
197
|
+
// we assert on the injected payload specifically rather than a blanket
|
|
198
|
+
// "no <script>" (the connect block legitimately emits one).
|
|
199
|
+
expect(html).not.toContain("<script>alert(1)</script>");
|
|
200
|
+
expect(html).toContain("<script>alert(1)</script>");
|
|
138
201
|
expect(html).toContain("<vault>");
|
|
202
|
+
// The escaped vault name also flows into the connect command + endpoint.
|
|
203
|
+
expect(html).toContain("parachute-<vault>");
|
|
139
204
|
});
|
|
140
205
|
});
|
|
@@ -160,7 +160,7 @@ describe("handleVaultAdminToken", () => {
|
|
|
160
160
|
// whole reason this gate exists.
|
|
161
161
|
await createUser(harness.db, "operator", "hunter2-admin");
|
|
162
162
|
const friend = await createUser(harness.db, "friend", "hunter2-friend", {
|
|
163
|
-
|
|
163
|
+
assignedVaults: ["work"],
|
|
164
164
|
allowMulti: true,
|
|
165
165
|
});
|
|
166
166
|
const session = createSession(harness.db, { userId: friend.id });
|
|
@@ -182,7 +182,7 @@ describe("handleVaultAdminToken", () => {
|
|
|
182
182
|
// happy path when there are friends in the DB.
|
|
183
183
|
const admin = await createUser(harness.db, "operator", "hunter2-admin");
|
|
184
184
|
await createUser(harness.db, "friend", "hunter2-friend", {
|
|
185
|
-
|
|
185
|
+
assignedVaults: ["work"],
|
|
186
186
|
allowMulti: true,
|
|
187
187
|
});
|
|
188
188
|
const session = createSession(harness.db, { userId: admin.id });
|
|
@@ -8,11 +8,21 @@ import { signAccessToken } from "../jwt-sign.ts";
|
|
|
8
8
|
import { upsertService, writeManifest } from "../services-manifest.ts";
|
|
9
9
|
import { rotateSigningKey } from "../signing-keys.ts";
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Build the JSON shape `parachute-vault create --json` emits (PR #184).
|
|
13
|
+
* Post the pvt_* DROP the `token` is a hub-issued access JWT (scoped
|
|
14
|
+
* `vault:<name>:admin`), and may be the empty string when the vault
|
|
15
|
+
* couldn't mint — in which case `token_guidance` carries the reason.
|
|
16
|
+
*/
|
|
17
|
+
function vaultCreateJson(
|
|
18
|
+
name: string,
|
|
19
|
+
token = `hubjwt.${name}.access`,
|
|
20
|
+
tokenGuidance?: string,
|
|
21
|
+
): string {
|
|
13
22
|
return JSON.stringify({
|
|
14
23
|
name,
|
|
15
24
|
token,
|
|
25
|
+
...(tokenGuidance ? { token_guidance: tokenGuidance } : {}),
|
|
16
26
|
paths: {
|
|
17
27
|
vault_dir: `/home/test/.parachute/vault/${name}`,
|
|
18
28
|
vault_db: `/home/test/.parachute/vault/${name}/vault.db`,
|
|
@@ -404,7 +414,7 @@ describe("POST /vaults — orchestration", () => {
|
|
|
404
414
|
);
|
|
405
415
|
return {
|
|
406
416
|
exitCode: 0,
|
|
407
|
-
stdout: vaultCreateJson("work", "
|
|
417
|
+
stdout: vaultCreateJson("work", "hubjwt.work.access"),
|
|
408
418
|
stderr: "",
|
|
409
419
|
};
|
|
410
420
|
};
|
|
@@ -420,7 +430,7 @@ describe("POST /vaults — orchestration", () => {
|
|
|
420
430
|
token?: string;
|
|
421
431
|
paths?: { vault_dir: string; vault_db: string; vault_config: string };
|
|
422
432
|
};
|
|
423
|
-
expect(body.token).toBe("
|
|
433
|
+
expect(body.token).toBe("hubjwt.work.access");
|
|
424
434
|
expect(body.paths).toEqual({
|
|
425
435
|
vault_dir: "/home/test/.parachute/vault/work",
|
|
426
436
|
vault_db: "/home/test/.parachute/vault/work/vault.db",
|
|
@@ -434,6 +444,62 @@ describe("POST /vaults — orchestration", () => {
|
|
|
434
444
|
}
|
|
435
445
|
});
|
|
436
446
|
|
|
447
|
+
test("201 forwards an empty token + token_guidance when the vault couldn't mint (post-DROP)", async () => {
|
|
448
|
+
// The vault emits `token: ""` + a `token_guidance` reason when no hub
|
|
449
|
+
// origin was reachable to mint against (e.g. loopback create). The hub
|
|
450
|
+
// must forward both verbatim so the SPA can render the
|
|
451
|
+
// created-but-no-token state instead of confusing it with a re-POST.
|
|
452
|
+
const h = makeHarness();
|
|
453
|
+
try {
|
|
454
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
455
|
+
try {
|
|
456
|
+
rotateSigningKey(db);
|
|
457
|
+
upsertService(
|
|
458
|
+
{
|
|
459
|
+
name: "parachute-vault",
|
|
460
|
+
port: 1940,
|
|
461
|
+
paths: ["/vault/default"],
|
|
462
|
+
health: "/health",
|
|
463
|
+
version: "0.3.5",
|
|
464
|
+
},
|
|
465
|
+
h.manifestPath,
|
|
466
|
+
);
|
|
467
|
+
const runCommand = async (_cmd: readonly string[]): Promise<RunResult> => {
|
|
468
|
+
upsertService(
|
|
469
|
+
{
|
|
470
|
+
name: "parachute-vault",
|
|
471
|
+
port: 1940,
|
|
472
|
+
paths: ["/vault/default", "/vault/work"],
|
|
473
|
+
health: "/health",
|
|
474
|
+
version: "0.3.5",
|
|
475
|
+
},
|
|
476
|
+
h.manifestPath,
|
|
477
|
+
);
|
|
478
|
+
return {
|
|
479
|
+
exitCode: 0,
|
|
480
|
+
stdout: vaultCreateJson("work", "", "no hub origin reachable to mint against"),
|
|
481
|
+
stderr: "",
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
const res = await call({
|
|
485
|
+
db,
|
|
486
|
+
manifestPath: h.manifestPath,
|
|
487
|
+
body: { name: "work" },
|
|
488
|
+
runCommand,
|
|
489
|
+
});
|
|
490
|
+
// Still a fresh create — HTTP 201, NOT 200.
|
|
491
|
+
expect(res.status).toBe(201);
|
|
492
|
+
const body = (await res.json()) as { token?: string; token_guidance?: string };
|
|
493
|
+
expect(body.token).toBe("");
|
|
494
|
+
expect(body.token_guidance).toBe("no hub origin reachable to mint against");
|
|
495
|
+
} finally {
|
|
496
|
+
db.close();
|
|
497
|
+
}
|
|
498
|
+
} finally {
|
|
499
|
+
h.cleanup();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
437
503
|
test("500 when `parachute-vault create --json` exits 0 but stdout is unparseable", async () => {
|
|
438
504
|
const h = makeHarness();
|
|
439
505
|
try {
|
|
@@ -757,7 +757,7 @@ describe("handleAccountHomeGet", () => {
|
|
|
757
757
|
const friend = await createUser(harness.db, "alice", "alice-passphrase", {
|
|
758
758
|
allowMulti: true,
|
|
759
759
|
passwordChanged: true,
|
|
760
|
-
|
|
760
|
+
assignedVaults: ["alice"],
|
|
761
761
|
});
|
|
762
762
|
const session = createSession(harness.db, { userId: friend.id });
|
|
763
763
|
const cookie = buildSessionCookie(session.id, Math.floor(SESSION_TTL_MS / 1000));
|
|
@@ -774,7 +774,7 @@ describe("handleAccountHomeGet", () => {
|
|
|
774
774
|
expect(html).toContain(`https://notes.parachute.computer/add?url=${encoded}`);
|
|
775
775
|
});
|
|
776
776
|
|
|
777
|
-
test("200 + admin branch when the first-admin signs in (
|
|
777
|
+
test("200 + admin branch when the first-admin signs in (no vault assignments)", async () => {
|
|
778
778
|
// The first-created user with no vault pin is the admin posture.
|
|
779
779
|
const admin = await createUser(harness.db, "admin", "admin-passphrase", {
|
|
780
780
|
passwordChanged: true,
|