@openparachute/hub 0.7.0 → 0.7.1
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__/account-setup.test.ts +276 -6
- package/src/__tests__/admin-connections-credentials.test.ts +1320 -0
- package/src/__tests__/api-invites.test.ts +166 -6
- package/src/__tests__/audience-gate.test.ts +752 -0
- package/src/__tests__/hub-db.test.ts +36 -0
- package/src/__tests__/hub-server.test.ts +266 -0
- package/src/__tests__/invites.test.ts +64 -1
- package/src/__tests__/lifecycle.test.ts +238 -3
- package/src/__tests__/ws-bridge.test.ts +573 -0
- package/src/__tests__/ws-connection-caps.test.ts +456 -0
- package/src/account-setup.ts +94 -23
- package/src/admin-connections.ts +916 -14
- package/src/admin-login-ui.ts +64 -15
- package/src/admin-vaults.ts +9 -0
- package/src/api-invites.ts +92 -12
- package/src/audience-gate.ts +268 -0
- package/src/chrome-strip.ts +8 -1
- package/src/commands/lifecycle.ts +187 -47
- package/src/connections-store.ts +32 -2
- package/src/help.ts +13 -6
- package/src/host-admin-token-validation.ts +6 -2
- package/src/hub-db.ts +26 -1
- package/src/hub-server.ts +501 -12
- package/src/invites.ts +69 -2
- package/src/jwt-sign.ts +7 -1
- package/src/module-manifest.ts +107 -0
- package/src/origin-check.ts +7 -4
- package/src/services-manifest.ts +97 -0
- package/src/ws-bridge.ts +256 -0
- package/src/ws-connection-caps.ts +170 -0
- package/web/ui/dist/assets/index-Cxtod68O.js +61 -0
- package/web/ui/dist/index.html +1 -1
- package/web/ui/dist/assets/index-C-XzMVqN.js +0 -61
|
@@ -68,6 +68,24 @@ function deps() {
|
|
|
68
68
|
return { db: harness.db, issuer: ISSUER, manifestPath: harness.manifestPath };
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Register an existing vault in the harness manifest (for shared-vault invites). */
|
|
72
|
+
function seedVaultInManifest(name: string) {
|
|
73
|
+
writeFileSync(
|
|
74
|
+
harness.manifestPath,
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
services: [
|
|
77
|
+
{
|
|
78
|
+
name: "parachute-vault",
|
|
79
|
+
port: 4101,
|
|
80
|
+
paths: [`/vault/${name}`],
|
|
81
|
+
health: "/health",
|
|
82
|
+
version: "0.0.0-test",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
function withBearer(path: string, bearer: string, init: RequestInit = {}): Request {
|
|
72
90
|
const headers = new Headers(init.headers ?? {});
|
|
73
91
|
headers.set("authorization", `Bearer ${bearer}`);
|
|
@@ -121,23 +139,64 @@ describe("POST /api/invites", () => {
|
|
|
121
139
|
expect(res.status).toBe(400);
|
|
122
140
|
});
|
|
123
141
|
|
|
124
|
-
test("
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
142
|
+
test("shared-vault invite (provision_vault=false + EXISTING vault_name) → 201 at the given role", async () => {
|
|
143
|
+
// The Adam/Jonathan shape: read-only access to a vault the admin already
|
|
144
|
+
// built. Issuing is host:admin-gated — the same authority that can assign
|
|
145
|
+
// any user to any vault via POST /api/users.
|
|
146
|
+
seedVaultInManifest("jonathan-vault");
|
|
128
147
|
const bearer = await makeAdminBearer();
|
|
129
148
|
const res = await handleCreateInvite(
|
|
130
149
|
withBearer("/api/invites", bearer, {
|
|
131
150
|
method: "POST",
|
|
132
151
|
headers: { "content-type": "application/json" },
|
|
133
|
-
body: JSON.stringify({
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
provision_vault: false,
|
|
154
|
+
vault_name: "jonathan-vault",
|
|
155
|
+
role: "read",
|
|
156
|
+
}),
|
|
157
|
+
}),
|
|
158
|
+
deps(),
|
|
159
|
+
);
|
|
160
|
+
expect(res.status).toBe(201);
|
|
161
|
+
const body = (await res.json()) as {
|
|
162
|
+
invite: { vault_name: string | null; role: string; provision_vault: boolean };
|
|
163
|
+
};
|
|
164
|
+
expect(body.invite.vault_name).toBe("jonathan-vault");
|
|
165
|
+
expect(body.invite.role).toBe("read");
|
|
166
|
+
expect(body.invite.provision_vault).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("400 vault_not_found when the shared vault doesn't exist on this hub", async () => {
|
|
170
|
+
// A pinned-but-nonexistent name is a typo, not a future reservation —
|
|
171
|
+
// fail at mint, not at the invitee's redeem.
|
|
172
|
+
const bearer = await makeAdminBearer();
|
|
173
|
+
const res = await handleCreateInvite(
|
|
174
|
+
withBearer("/api/invites", bearer, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: { "content-type": "application/json" },
|
|
177
|
+
body: JSON.stringify({ provision_vault: false, vault_name: "nosuchvault", role: "read" }),
|
|
178
|
+
}),
|
|
179
|
+
deps(),
|
|
180
|
+
);
|
|
181
|
+
expect(res.status).toBe(400);
|
|
182
|
+
const body = (await res.json()) as { error: string };
|
|
183
|
+
expect(body.error).toBe("vault_not_found");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("400 rejects role=read with provision_vault=true (sole user of a new vault must hold write)", async () => {
|
|
187
|
+
const bearer = await makeAdminBearer();
|
|
188
|
+
const res = await handleCreateInvite(
|
|
189
|
+
withBearer("/api/invites", bearer, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "content-type": "application/json" },
|
|
192
|
+
body: JSON.stringify({ provision_vault: true, role: "read" }),
|
|
134
193
|
}),
|
|
135
194
|
deps(),
|
|
136
195
|
);
|
|
137
196
|
expect(res.status).toBe(400);
|
|
138
197
|
const body = (await res.json()) as { error: string; error_description: string };
|
|
139
198
|
expect(body.error).toBe("invalid_request");
|
|
140
|
-
expect(body.error_description).toContain("
|
|
199
|
+
expect(body.error_description).toContain("write");
|
|
141
200
|
});
|
|
142
201
|
|
|
143
202
|
test("account-only invite (provision_vault=false, NO vault_name) is still allowed", async () => {
|
|
@@ -159,6 +218,107 @@ describe("POST /api/invites", () => {
|
|
|
159
218
|
});
|
|
160
219
|
});
|
|
161
220
|
|
|
221
|
+
describe("POST /api/invites — pre-named username", () => {
|
|
222
|
+
test("201 carries the username in the wire shape", async () => {
|
|
223
|
+
seedVaultInManifest("jonathan-vault");
|
|
224
|
+
const bearer = await makeAdminBearer();
|
|
225
|
+
const res = await handleCreateInvite(
|
|
226
|
+
withBearer("/api/invites", bearer, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "content-type": "application/json" },
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
username: "jonathan",
|
|
231
|
+
provision_vault: false,
|
|
232
|
+
vault_name: "jonathan-vault",
|
|
233
|
+
role: "read",
|
|
234
|
+
}),
|
|
235
|
+
}),
|
|
236
|
+
deps(),
|
|
237
|
+
);
|
|
238
|
+
expect(res.status).toBe(201);
|
|
239
|
+
const body = (await res.json()) as { invite: { username: string | null } };
|
|
240
|
+
expect(body.invite.username).toBe("jonathan");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("400 invalid_username on a name outside the /api/users vocabulary", async () => {
|
|
244
|
+
const bearer = await makeAdminBearer();
|
|
245
|
+
for (const bad of ["A", "Has Spaces", "admin"]) {
|
|
246
|
+
const res = await handleCreateInvite(
|
|
247
|
+
withBearer("/api/invites", bearer, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "content-type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ username: bad }),
|
|
251
|
+
}),
|
|
252
|
+
deps(),
|
|
253
|
+
);
|
|
254
|
+
expect(res.status).toBe(400);
|
|
255
|
+
const body = (await res.json()) as { error: string };
|
|
256
|
+
expect(body.error).toBe("invalid_username");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("409 username_taken when an existing user holds the name (case-insensitive)", async () => {
|
|
261
|
+
const bearer = await makeAdminBearer(); // creates user "operator"
|
|
262
|
+
const res = await handleCreateInvite(
|
|
263
|
+
withBearer("/api/invites", bearer, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: { "content-type": "application/json" },
|
|
266
|
+
body: JSON.stringify({ username: "operator" }),
|
|
267
|
+
}),
|
|
268
|
+
deps(),
|
|
269
|
+
);
|
|
270
|
+
expect(res.status).toBe(409);
|
|
271
|
+
const body = (await res.json()) as { error: string };
|
|
272
|
+
expect(body.error).toBe("username_taken");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("409 username_reserved when another PENDING invite pre-names the same username", async () => {
|
|
276
|
+
const bearer = await makeAdminBearer();
|
|
277
|
+
const make = () =>
|
|
278
|
+
handleCreateInvite(
|
|
279
|
+
withBearer("/api/invites", bearer, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { "content-type": "application/json" },
|
|
282
|
+
body: JSON.stringify({ username: "jonathan" }),
|
|
283
|
+
}),
|
|
284
|
+
deps(),
|
|
285
|
+
);
|
|
286
|
+
const first = await make();
|
|
287
|
+
expect(first.status).toBe(201);
|
|
288
|
+
const second = await make();
|
|
289
|
+
expect(second.status).toBe(409);
|
|
290
|
+
const body = (await second.json()) as { error: string };
|
|
291
|
+
expect(body.error).toBe("username_reserved");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("revoking the pending invite frees the reserved username", async () => {
|
|
295
|
+
const bearer = await makeAdminBearer();
|
|
296
|
+
const first = await handleCreateInvite(
|
|
297
|
+
withBearer("/api/invites", bearer, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: { "content-type": "application/json" },
|
|
300
|
+
body: JSON.stringify({ username: "jonathan" }),
|
|
301
|
+
}),
|
|
302
|
+
deps(),
|
|
303
|
+
);
|
|
304
|
+
const { invite } = (await first.json()) as { invite: { id: string } };
|
|
305
|
+
await handleRevokeInvite(
|
|
306
|
+
withBearer(`/api/invites/${invite.id}`, bearer, { method: "DELETE" }),
|
|
307
|
+
invite.id,
|
|
308
|
+
deps(),
|
|
309
|
+
);
|
|
310
|
+
const second = await handleCreateInvite(
|
|
311
|
+
withBearer("/api/invites", bearer, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { "content-type": "application/json" },
|
|
314
|
+
body: JSON.stringify({ username: "jonathan" }),
|
|
315
|
+
}),
|
|
316
|
+
deps(),
|
|
317
|
+
);
|
|
318
|
+
expect(second.status).toBe(201);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
162
322
|
describe("GET /api/invites", () => {
|
|
163
323
|
test("lists invites; raw token NEVER present in the wire shape", async () => {
|
|
164
324
|
const bearer = await makeAdminBearer();
|