@openparachute/hub 0.7.1 → 0.7.2
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 +13 -14
- package/package.json +1 -1
- package/src/__tests__/admin-agent-grants.test.ts +1547 -0
- package/src/__tests__/{admin-channel-token.test.ts → admin-agent-token.test.ts} +32 -32
- package/src/__tests__/admin-connections-credentials.test.ts +8 -4
- package/src/__tests__/admin-connections.test.ts +211 -57
- package/src/__tests__/admin-csrf-belt.test.ts +7 -7
- package/src/__tests__/admin-lock.test.ts +600 -0
- package/src/__tests__/admin-module-token.test.ts +36 -8
- package/src/__tests__/admin-vaults.test.ts +8 -8
- package/src/__tests__/api-modules-ops.test.ts +17 -16
- package/src/__tests__/api-modules.test.ts +35 -36
- package/src/__tests__/api-ready.test.ts +2 -2
- package/src/__tests__/clients.test.ts +91 -0
- package/src/__tests__/grants-store.test.ts +219 -0
- package/src/__tests__/hub-server.test.ts +9 -5
- package/src/__tests__/migrate.test.ts +1 -1
- package/src/__tests__/module-manifest.test.ts +11 -11
- package/src/__tests__/oauth-client.test.ts +446 -0
- package/src/__tests__/oauth-flows-store.test.ts +141 -0
- package/src/__tests__/oauth-handlers.test.ts +124 -26
- package/src/__tests__/operator-token.test.ts +2 -2
- package/src/__tests__/scope-explanations.test.ts +3 -3
- package/src/__tests__/serve-boot.test.ts +14 -14
- package/src/__tests__/serve.test.ts +26 -0
- package/src/__tests__/service-spec-discovery.test.ts +26 -18
- package/src/__tests__/services-manifest.test.ts +60 -48
- package/src/__tests__/setup-gate.test.ts +52 -3
- package/src/__tests__/setup-wizard.test.ts +86 -280
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/upgrade.test.ts +276 -0
- package/src/__tests__/vault-remove.test.ts +393 -0
- package/src/admin-agent-grants.ts +1365 -0
- package/src/admin-agent-token.ts +147 -0
- package/src/admin-connections.ts +67 -50
- package/src/admin-host-admin-token.ts +14 -1
- package/src/admin-lock.ts +281 -0
- package/src/admin-module-token.ts +15 -7
- package/src/admin-vault-admin-token.ts +8 -1
- package/src/admin-vaults.ts +12 -12
- package/src/api-admin-lock.ts +335 -0
- package/src/api-modules-ops.ts +3 -2
- package/src/api-modules.ts +9 -9
- package/src/cli.ts +13 -1
- package/src/clients.ts +88 -0
- package/src/commands/install.ts +7 -0
- package/src/commands/serve-boot.ts +5 -4
- package/src/commands/serve.ts +45 -19
- package/src/commands/setup.ts +4 -3
- package/src/commands/upgrade.ts +118 -2
- package/src/commands/vault-remove.ts +361 -0
- package/src/commands/wizard.ts +4 -4
- package/src/connections-store.ts +3 -3
- package/src/grants-store.ts +272 -0
- package/src/help.ts +4 -1
- package/src/hub-server.ts +209 -27
- package/src/hub-settings.ts +23 -8
- package/src/jwt-sign.ts +5 -1
- package/src/module-manifest.ts +2 -2
- package/src/oauth-client.ts +497 -0
- package/src/oauth-flows-store.ts +163 -0
- package/src/oauth-handlers.ts +40 -13
- package/src/operator-token.ts +1 -1
- package/src/origin-check.ts +7 -2
- package/src/resource-binding.ts +4 -4
- package/src/scope-explanations.ts +3 -3
- package/src/service-spec.ts +56 -43
- package/src/setup-wizard.ts +56 -240
- package/web/ui/dist/assets/index-B5AUE359.js +61 -0
- package/web/ui/dist/assets/{index-E_9wqjEm.css → index-DR6R8EFf.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/src/admin-channel-token.ts +0 -135
- package/web/ui/dist/assets/index-Cxtod68O.js +0 -61
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the
|
|
3
|
-
* `admin-host-admin-token.test.ts` shape (
|
|
2
|
+
* Tests for the agent UI session→bearer mint endpoint. Mirrors
|
|
3
|
+
* `admin-host-admin-token.test.ts` shape (agent has a single bare audience,
|
|
4
4
|
* no per-vault name). Covers:
|
|
5
5
|
* - 401 when no admin session cookie is present.
|
|
6
6
|
* - 401 when the cookie names a deleted session.
|
|
7
7
|
* - 405 on POST.
|
|
8
|
-
* - 200 + JWT carrying `aud: "
|
|
8
|
+
* - 200 + JWT carrying `aud: "agent"` and `agent:read agent:send agent:admin`.
|
|
9
9
|
* - First-admin gate: 403 for a signed-in non-first-admin (friend); the
|
|
10
10
|
* admin's happy path still mints when a friend exists alongside.
|
|
11
11
|
*/
|
|
@@ -14,7 +14,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
14
14
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import {
|
|
17
|
+
import { AGENT_TOKEN_TTL_SECONDS, handleAgentToken } from "../admin-agent-token.ts";
|
|
18
18
|
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
19
19
|
import { validateAccessToken } from "../jwt-sign.ts";
|
|
20
20
|
import { SESSION_TTL_MS, buildSessionCookie, createSession, deleteSession } from "../sessions.ts";
|
|
@@ -29,7 +29,7 @@ interface Harness {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function makeHarness(): Harness {
|
|
32
|
-
const dir = mkdtempSync(join(tmpdir(), "phub-
|
|
32
|
+
const dir = mkdtempSync(join(tmpdir(), "phub-agent-token-"));
|
|
33
33
|
const db = openHubDb(hubDbPath(dir));
|
|
34
34
|
return {
|
|
35
35
|
db,
|
|
@@ -79,10 +79,10 @@ async function withAdminAndFriend(): Promise<{
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
describe("
|
|
82
|
+
describe("handleAgentToken", () => {
|
|
83
83
|
test("401 when no session cookie is present", async () => {
|
|
84
|
-
const req = new Request(`${ISSUER}/admin/
|
|
85
|
-
const res = await
|
|
84
|
+
const req = new Request(`${ISSUER}/admin/agent-token`);
|
|
85
|
+
const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
|
|
86
86
|
expect(res.status).toBe(401);
|
|
87
87
|
const body = (await res.json()) as { error: string };
|
|
88
88
|
expect(body.error).toBe("unauthenticated");
|
|
@@ -92,54 +92,54 @@ describe("handleChannelToken", () => {
|
|
|
92
92
|
const { cookie } = await withSession();
|
|
93
93
|
const sid = cookie.match(/parachute_hub_session=([^;]+)/)?.[1] ?? "";
|
|
94
94
|
deleteSession(harness.db, sid);
|
|
95
|
-
const req = new Request(`${ISSUER}/admin/
|
|
96
|
-
const res = await
|
|
95
|
+
const req = new Request(`${ISSUER}/admin/agent-token`, { headers: { cookie } });
|
|
96
|
+
const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
|
|
97
97
|
expect(res.status).toBe(401);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
test("405 on POST", async () => {
|
|
101
101
|
const { cookie } = await withSession();
|
|
102
|
-
const req = new Request(`${ISSUER}/admin/
|
|
102
|
+
const req = new Request(`${ISSUER}/admin/agent-token`, {
|
|
103
103
|
method: "POST",
|
|
104
104
|
headers: { cookie },
|
|
105
105
|
});
|
|
106
|
-
const res = await
|
|
106
|
+
const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
|
|
107
107
|
expect(res.status).toBe(405);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test("200 mints a JWT carrying aud:
|
|
110
|
+
test("200 mints a JWT carrying aud:agent + agent:read agent:send agent:admin", async () => {
|
|
111
111
|
const { cookie, userId } = await withSession();
|
|
112
112
|
rotateSigningKey(harness.db);
|
|
113
|
-
const req = new Request(`${ISSUER}/admin/
|
|
114
|
-
const res = await
|
|
113
|
+
const req = new Request(`${ISSUER}/admin/agent-token`, { headers: { cookie } });
|
|
114
|
+
const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
|
|
115
115
|
expect(res.status).toBe(200);
|
|
116
116
|
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
117
117
|
|
|
118
118
|
const body = (await res.json()) as { token: string; expires_at: string; scopes: string[] };
|
|
119
|
-
// `
|
|
119
|
+
// `agent:send` (post) + `agent:read` (SSE replies) + `agent:admin`
|
|
120
120
|
// (config UI list/edit — 2026-06-09 modular-UI architecture P3). Deliberately
|
|
121
|
-
// NOT `
|
|
122
|
-
expect(body.scopes).toEqual(["
|
|
123
|
-
expect(body.scopes).not.toContain("
|
|
121
|
+
// NOT `agent:write` — that's the session-reply scope a UI token must not hold.
|
|
122
|
+
expect(body.scopes).toEqual(["agent:read", "agent:send", "agent:admin"]);
|
|
123
|
+
expect(body.scopes).not.toContain("agent:write");
|
|
124
124
|
expect(body.token.length).toBeGreaterThan(20);
|
|
125
125
|
|
|
126
126
|
const expMs = new Date(body.expires_at).getTime();
|
|
127
127
|
const skew = expMs - Date.now();
|
|
128
|
-
expect(skew).toBeGreaterThan((
|
|
129
|
-
expect(skew).toBeLessThan((
|
|
128
|
+
expect(skew).toBeGreaterThan((AGENT_TOKEN_TTL_SECONDS - 30) * 1000);
|
|
129
|
+
expect(skew).toBeLessThan((AGENT_TOKEN_TTL_SECONDS + 30) * 1000);
|
|
130
130
|
|
|
131
131
|
const validated = await validateAccessToken(harness.db, body.token, ISSUER);
|
|
132
132
|
expect(validated.payload.sub).toBe(userId);
|
|
133
133
|
expect(validated.payload.iss).toBe(ISSUER);
|
|
134
|
-
// Bare service audience —
|
|
135
|
-
// (parachute-
|
|
136
|
-
expect(validated.payload.aud).toBe("
|
|
134
|
+
// Bare service audience — agent validates `aud === "agent"`
|
|
135
|
+
// (parachute-agent src/hub-jwt.ts).
|
|
136
|
+
expect(validated.payload.aud).toBe("agent");
|
|
137
137
|
const scopeClaim = (validated.payload as { scope?: string }).scope ?? "";
|
|
138
138
|
const scopes = scopeClaim.split(/\s+/);
|
|
139
|
-
expect(scopes).toContain("
|
|
140
|
-
expect(scopes).toContain("
|
|
141
|
-
expect(scopes).toContain("
|
|
142
|
-
expect(scopes).not.toContain("
|
|
139
|
+
expect(scopes).toContain("agent:read");
|
|
140
|
+
expect(scopes).toContain("agent:send");
|
|
141
|
+
expect(scopes).toContain("agent:admin");
|
|
142
|
+
expect(scopes).not.toContain("agent:write");
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
test("403 not_admin when a signed-in non-first-admin (friend) hits the endpoint", async () => {
|
|
@@ -148,10 +148,10 @@ describe("handleChannelToken", () => {
|
|
|
148
148
|
// first-admin row.
|
|
149
149
|
const { friendCookie } = await withAdminAndFriend();
|
|
150
150
|
rotateSigningKey(harness.db);
|
|
151
|
-
const req = new Request(`${ISSUER}/admin/
|
|
151
|
+
const req = new Request(`${ISSUER}/admin/agent-token`, {
|
|
152
152
|
headers: { cookie: friendCookie },
|
|
153
153
|
});
|
|
154
|
-
const res = await
|
|
154
|
+
const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
|
|
155
155
|
expect(res.status).toBe(403);
|
|
156
156
|
const body = (await res.json()) as { error: string; error_description: string };
|
|
157
157
|
expect(body.error).toBe("not_admin");
|
|
@@ -161,10 +161,10 @@ describe("handleChannelToken", () => {
|
|
|
161
161
|
test("first-admin path still succeeds when a friend exists alongside", async () => {
|
|
162
162
|
const { adminCookie, adminId } = await withAdminAndFriend();
|
|
163
163
|
rotateSigningKey(harness.db);
|
|
164
|
-
const req = new Request(`${ISSUER}/admin/
|
|
164
|
+
const req = new Request(`${ISSUER}/admin/agent-token`, {
|
|
165
165
|
headers: { cookie: adminCookie },
|
|
166
166
|
});
|
|
167
|
-
const res = await
|
|
167
|
+
const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
|
|
168
168
|
expect(res.status).toBe(200);
|
|
169
169
|
const body = (await res.json()) as { token: string };
|
|
170
170
|
const validated = await validateAccessToken(harness.db, body.token, ISSUER);
|
|
@@ -168,7 +168,7 @@ function credDeps(fetchImpl: typeof fetch, modules: InstalledModuleInfo[]): Conn
|
|
|
168
168
|
modules,
|
|
169
169
|
resolveVaultOrigin: (v) => (v === "default" ? VAULT_ORIGIN : null),
|
|
170
170
|
resolveModuleOrigin: (short) => (short === "surface" ? SURFACE_ORIGIN : null),
|
|
171
|
-
|
|
171
|
+
agentOrigin: null,
|
|
172
172
|
storePath: harness.storePath,
|
|
173
173
|
fetchImpl,
|
|
174
174
|
};
|
|
@@ -1121,7 +1121,11 @@ describe("credential connection — claim/reconcile (surface#113)", () => {
|
|
|
1121
1121
|
test("CLI-shape claim: vault_scope [] accepted when scope+aud pin the vault (the live surface#113 credentials)", async () => {
|
|
1122
1122
|
const { fetchImpl } = mockFetch({});
|
|
1123
1123
|
const deps = credDeps(fetchImpl, modulesOf(SURFACE_MANIFEST));
|
|
1124
|
-
const cli = await mintDirectDelivered({
|
|
1124
|
+
const cli = await mintDirectDelivered({
|
|
1125
|
+
vault: "default",
|
|
1126
|
+
verb: "read",
|
|
1127
|
+
emptyVaultScope: true,
|
|
1128
|
+
});
|
|
1125
1129
|
const res = await handleConnections(
|
|
1126
1130
|
claimReq(CLAIM_ID, SURFACE_CLAIM, cli.token),
|
|
1127
1131
|
`/${CLAIM_ID}/claim`,
|
|
@@ -1233,7 +1237,7 @@ describe("credential connection — claim/reconcile (surface#113)", () => {
|
|
|
1233
1237
|
putConnection(harness.storePath, {
|
|
1234
1238
|
id: CLAIM_ID,
|
|
1235
1239
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
1236
|
-
sink: { module: "
|
|
1240
|
+
sink: { module: "agent", action: "message.deliver" },
|
|
1237
1241
|
provisioned: { type: "vault-trigger", vault: "default", triggerName: "t", mintedJtis: [] },
|
|
1238
1242
|
createdAt: new Date().toISOString(),
|
|
1239
1243
|
});
|
|
@@ -1260,7 +1264,7 @@ describe("credential connection — claim/reconcile (surface#113)", () => {
|
|
|
1260
1264
|
putConnection(harness.storePath, {
|
|
1261
1265
|
id: "ev-conn",
|
|
1262
1266
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
1263
|
-
sink: { module: "
|
|
1267
|
+
sink: { module: "agent", action: "message.deliver" },
|
|
1264
1268
|
provisioned: { type: "vault-trigger", vault: "default", triggerName: "t", mintedJtis: [] },
|
|
1265
1269
|
createdAt: new Date().toISOString(),
|
|
1266
1270
|
});
|