@vellumai/vellum-gateway 0.7.3 → 0.8.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/AGENTS.md +10 -0
- package/Dockerfile +4 -2
- package/bun.lock +8 -1
- package/knip.json +1 -0
- package/package.json +2 -1
- package/src/__tests__/contact-prompt-submit.test.ts +5 -5
- package/src/__tests__/contact-store-mark-channel-verified.test.ts +177 -0
- package/src/__tests__/contacts-control-plane-proxy.test.ts +297 -6
- package/src/__tests__/contacts-control-plane-route-match.test.ts +16 -0
- package/src/__tests__/edge-auth.test.ts +253 -0
- package/src/__tests__/edge-guardian-auth.test.ts +198 -0
- package/src/__tests__/ipc-route-policy-coverage.test.ts +297 -0
- package/src/__tests__/ipc-route-policy.test.ts +43 -0
- package/src/__tests__/ipc-server-watchdog.test.ts +189 -0
- package/src/__tests__/live-voice-websocket.test.ts +1 -1
- package/src/__tests__/slack-normalize.test.ts +132 -0
- package/src/auth/guardian-bootstrap.ts +3 -1
- package/src/auth/ipc-route-policy.ts +34 -0
- package/src/db/assistant-db-proxy.ts +76 -7
- package/src/db/contact-store.ts +767 -1
- package/src/db/schema.ts +29 -0
- package/src/feature-flag-registry.json +17 -17
- package/src/handlers/handle-inbound.ts +9 -23
- package/src/http/middleware/auth.ts +193 -40
- package/src/http/router.ts +26 -4
- package/src/http/routes/channel-verification-session-proxy.ts +53 -6
- package/src/http/routes/contact-prompt.ts +44 -15
- package/src/http/routes/contacts-control-plane-proxy.ts +329 -2
- package/src/http/routes/contacts-control-plane-route-match.ts +12 -0
- package/src/http/routes/ipc-runtime-proxy.test.ts +38 -43
- package/src/http/routes/ipc-runtime-proxy.ts +2 -2
- package/src/http/routes/log-export.test.ts +1 -0
- package/src/http/routes/log-export.ts +9 -2
- package/src/http/routes/log-tail.test.ts +10 -9
- package/src/http/routes/log-tail.ts +5 -2
- package/src/http/routes/pair.ts +8 -0
- package/src/http/routes/twilio-voice-webhook.ts +11 -2
- package/src/index.ts +98 -13
- package/src/ipc/assistant-client.test.ts +67 -15
- package/src/ipc/assistant-client.ts +12 -118
- package/src/ipc/risk-classification-handlers.test.ts +76 -0
- package/src/ipc/risk-classification-handlers.ts +20 -9
- package/src/ipc/server.ts +113 -46
- package/src/logger.ts +71 -17
- package/src/post-assistant-ready.ts +9 -3
- package/src/risk/bash-risk-classifier.test.ts +106 -0
- package/src/risk/bash-risk-classifier.ts +19 -15
- package/src/risk/command-registry/commands/assistant.ts +75 -26
- package/src/risk/command-registry.test.ts +3 -1
- package/src/risk/shell-parser.test.ts +159 -0
- package/src/risk/shell-parser.ts +150 -19
- package/src/runtime/client.ts +0 -11
- package/src/schema.ts +32 -0
- package/src/slack/normalize.test.ts +5 -0
- package/src/slack/normalize.ts +7 -0
- package/src/velay/bridge-utils.ts +10 -0
- package/src/velay/client.test.ts +156 -0
- package/src/velay/client.ts +83 -0
- package/src/velay/http-bridge.test.ts +29 -0
- package/src/velay/http-bridge.ts +7 -0
- package/src/velay/protocol.ts +12 -1
- package/src/verification/outbound-voice-verification-sync.ts +171 -0
- package/src/verification/voice-approval-sync.ts +107 -0
|
@@ -15,6 +15,15 @@ describe("matchContactsControlPlaneRoute", () => {
|
|
|
15
15
|
expect(
|
|
16
16
|
matchContactsControlPlaneRoute("/v1/contact-channels/ch_1", "PATCH"),
|
|
17
17
|
).toEqual({ kind: "updateContactChannel", contactChannelId: "ch_1" });
|
|
18
|
+
expect(
|
|
19
|
+
matchContactsControlPlaneRoute(
|
|
20
|
+
"/v1/contact-channels/ch_1/verify",
|
|
21
|
+
"POST",
|
|
22
|
+
),
|
|
23
|
+
).toEqual({
|
|
24
|
+
kind: "verifyContactChannel",
|
|
25
|
+
contactChannelId: "ch_1",
|
|
26
|
+
});
|
|
18
27
|
expect(matchContactsControlPlaneRoute("/v1/contacts/ct_1", "GET")).toEqual({
|
|
19
28
|
kind: "getContact",
|
|
20
29
|
contactId: "ct_1",
|
|
@@ -27,6 +36,13 @@ describe("matchContactsControlPlaneRoute", () => {
|
|
|
27
36
|
expect(
|
|
28
37
|
matchContactsControlPlaneRoute("/v1/contact-channels/ch_1", "GET"),
|
|
29
38
|
).toBeNull();
|
|
39
|
+
// PATCH on verify subpath does not match (POST only)
|
|
40
|
+
expect(
|
|
41
|
+
matchContactsControlPlaneRoute(
|
|
42
|
+
"/v1/contact-channels/ch_1/verify",
|
|
43
|
+
"PATCH",
|
|
44
|
+
),
|
|
45
|
+
).toBeNull();
|
|
30
46
|
});
|
|
31
47
|
|
|
32
48
|
test("GET /v1/contacts/merge falls through to getContact", () => {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `requireEdgeAuth` and `requireEdgeAuthWithScope` — the
|
|
3
|
+
* client-facing edge guards.
|
|
4
|
+
*
|
|
5
|
+
* Two auth modes (mirrors `requireEdgeGuardianAuth`):
|
|
6
|
+
*
|
|
7
|
+
* 1. Platform-managed (DISABLE_HTTP_AUTH=true + IS_PLATFORM=true): identity
|
|
8
|
+
* asserted via `X-Vellum-User-Id` header cross-referenced against the
|
|
9
|
+
* stored `vellum:platform_user_id` credential. Scope authorization is
|
|
10
|
+
* delegated to the upstream platform proxy.
|
|
11
|
+
* 2. Default: edge JWT validated; scoped guard additionally checks the
|
|
12
|
+
* scope_profile claim.
|
|
13
|
+
*
|
|
14
|
+
* Importantly, DISABLE_HTTP_AUTH alone (without IS_PLATFORM) does NOT
|
|
15
|
+
* bypass JWT validation — protects against accidental misconfig.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
import "./test-preload.js";
|
|
21
|
+
|
|
22
|
+
// --- Mocks (set BEFORE importing the module under test) -------------------
|
|
23
|
+
|
|
24
|
+
let mockReadCredential = mock(
|
|
25
|
+
async (_key: string): Promise<string | undefined> => undefined,
|
|
26
|
+
);
|
|
27
|
+
mock.module("../credential-reader.js", () => ({
|
|
28
|
+
readCredential: (key: string) => mockReadCredential(key),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
let mockValidateEdgeToken = mock(
|
|
32
|
+
(
|
|
33
|
+
_token: string,
|
|
34
|
+
):
|
|
35
|
+
| { ok: true; claims: { sub: string; scope_profile: string } }
|
|
36
|
+
| { ok: false; reason: string } => ({
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: "noop",
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
mock.module("../auth/token-exchange.js", () => ({
|
|
42
|
+
validateEdgeToken: (token: string) => mockValidateEdgeToken(token),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const { AuthRateLimiter } = await import("../auth-rate-limiter.js");
|
|
46
|
+
const { createAuthMiddleware } = await import("../http/middleware/auth.js");
|
|
47
|
+
|
|
48
|
+
const PLATFORM_USER_ID = "user-abc-123";
|
|
49
|
+
|
|
50
|
+
function makeMiddleware() {
|
|
51
|
+
const rl = new AuthRateLimiter();
|
|
52
|
+
return createAuthMiddleware(rl, () => "1.2.3.4");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeReq(headers: Record<string, string> = {}): Request {
|
|
56
|
+
return new Request("http://gateway.local/v1/something", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
mockReadCredential = mock(async () => undefined);
|
|
64
|
+
mockValidateEdgeToken = mock(() => ({ ok: false, reason: "noop" }));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
delete process.env.DISABLE_HTTP_AUTH;
|
|
69
|
+
delete process.env.IS_PLATFORM;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// =========================================================================
|
|
73
|
+
// requireEdgeAuth — platform bypass active
|
|
74
|
+
// =========================================================================
|
|
75
|
+
|
|
76
|
+
describe("requireEdgeAuth — DISABLE_HTTP_AUTH + IS_PLATFORM", () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
process.env.DISABLE_HTTP_AUTH = "true";
|
|
79
|
+
process.env.IS_PLATFORM = "true";
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("401 when X-Vellum-User-Id header is missing", async () => {
|
|
83
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
84
|
+
const res = await requireEdgeAuth(makeReq());
|
|
85
|
+
expect(res?.status).toBe(401);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("403 when no platform_user_id is stored", async () => {
|
|
89
|
+
mockReadCredential = mock(async () => undefined);
|
|
90
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
91
|
+
const res = await requireEdgeAuth(
|
|
92
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
93
|
+
);
|
|
94
|
+
expect(res?.status).toBe(403);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("403 when X-Vellum-User-Id does not match stored credential", async () => {
|
|
98
|
+
mockReadCredential = mock(async () => PLATFORM_USER_ID);
|
|
99
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
100
|
+
const res = await requireEdgeAuth(
|
|
101
|
+
makeReq({ "x-vellum-user-id": "different-user" }),
|
|
102
|
+
);
|
|
103
|
+
expect(res?.status).toBe(403);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("503 when readCredential throws", async () => {
|
|
107
|
+
mockReadCredential = mock(async () => {
|
|
108
|
+
throw new Error("cred store unavailable");
|
|
109
|
+
});
|
|
110
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
111
|
+
const res = await requireEdgeAuth(
|
|
112
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
113
|
+
);
|
|
114
|
+
expect(res?.status).toBe(503);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("null (auth ok) when X-Vellum-User-Id matches stored credential", async () => {
|
|
118
|
+
mockReadCredential = mock(async () => PLATFORM_USER_ID);
|
|
119
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
120
|
+
const res = await requireEdgeAuth(
|
|
121
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
122
|
+
);
|
|
123
|
+
expect(res).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// =========================================================================
|
|
128
|
+
// requireEdgeAuth — accidental misconfig (only one flag set)
|
|
129
|
+
// =========================================================================
|
|
130
|
+
|
|
131
|
+
describe("requireEdgeAuth — DISABLE_HTTP_AUTH alone is insufficient", () => {
|
|
132
|
+
test("DISABLE_HTTP_AUTH=true without IS_PLATFORM still runs JWT validation", async () => {
|
|
133
|
+
process.env.DISABLE_HTTP_AUTH = "true";
|
|
134
|
+
// IS_PLATFORM intentionally NOT set
|
|
135
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
136
|
+
const res = await requireEdgeAuth(makeReq());
|
|
137
|
+
// No bearer token + no bypass → 401, NOT a free pass
|
|
138
|
+
expect(res?.status).toBe(401);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("IS_PLATFORM=true without DISABLE_HTTP_AUTH still runs JWT validation", async () => {
|
|
142
|
+
process.env.IS_PLATFORM = "true";
|
|
143
|
+
// DISABLE_HTTP_AUTH intentionally NOT set
|
|
144
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
145
|
+
const res = await requireEdgeAuth(makeReq());
|
|
146
|
+
expect(res?.status).toBe(401);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("both flags unset → JWT validation runs", async () => {
|
|
150
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
151
|
+
const res = await requireEdgeAuth(makeReq());
|
|
152
|
+
expect(res?.status).toBe(401);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// =========================================================================
|
|
157
|
+
// requireEdgeAuth — default (JWT) mode
|
|
158
|
+
// =========================================================================
|
|
159
|
+
|
|
160
|
+
describe("requireEdgeAuth — JWT mode", () => {
|
|
161
|
+
test("null on valid bearer token", async () => {
|
|
162
|
+
mockValidateEdgeToken = mock(() => ({
|
|
163
|
+
ok: true,
|
|
164
|
+
claims: { sub: "actor:asst:123", scope_profile: "actor_client_v1" },
|
|
165
|
+
}));
|
|
166
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
167
|
+
const res = await requireEdgeAuth(
|
|
168
|
+
makeReq({ authorization: "Bearer good.jwt.here" }),
|
|
169
|
+
);
|
|
170
|
+
expect(res).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("401 on invalid bearer token", async () => {
|
|
174
|
+
mockValidateEdgeToken = mock(() => ({ ok: false, reason: "expired" }));
|
|
175
|
+
const { requireEdgeAuth } = makeMiddleware();
|
|
176
|
+
const res = await requireEdgeAuth(
|
|
177
|
+
makeReq({ authorization: "Bearer bad.jwt.here" }),
|
|
178
|
+
);
|
|
179
|
+
expect(res?.status).toBe(401);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// =========================================================================
|
|
184
|
+
// requireEdgeAuthWithScope — same bypass model + scope check on JWT path
|
|
185
|
+
// =========================================================================
|
|
186
|
+
|
|
187
|
+
describe("requireEdgeAuthWithScope — DISABLE_HTTP_AUTH + IS_PLATFORM", () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
process.env.DISABLE_HTTP_AUTH = "true";
|
|
190
|
+
process.env.IS_PLATFORM = "true";
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("uses platform header check; no scope check on bypass path", async () => {
|
|
194
|
+
// Even with a scope profile that wouldn't grant the required scope under
|
|
195
|
+
// JWT mode, the bypass path only cross-checks the user header. Scope is
|
|
196
|
+
// enforced upstream by the platform proxy.
|
|
197
|
+
mockReadCredential = mock(async () => PLATFORM_USER_ID);
|
|
198
|
+
const { requireEdgeAuthWithScope } = makeMiddleware();
|
|
199
|
+
const res = await requireEdgeAuthWithScope(
|
|
200
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
201
|
+
// any scope — bypass path does not look at it
|
|
202
|
+
"ingress.write",
|
|
203
|
+
);
|
|
204
|
+
expect(res).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("401 when X-Vellum-User-Id missing under bypass", async () => {
|
|
208
|
+
const { requireEdgeAuthWithScope } = makeMiddleware();
|
|
209
|
+
const res = await requireEdgeAuthWithScope(makeReq(), "ingress.write");
|
|
210
|
+
expect(res?.status).toBe(401);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("requireEdgeAuthWithScope — JWT mode", () => {
|
|
215
|
+
test("403 when token's scope_profile lacks the required scope", async () => {
|
|
216
|
+
// actor_client_v1 grants chat.* and settings.*, but NOT ingress.write
|
|
217
|
+
mockValidateEdgeToken = mock(() => ({
|
|
218
|
+
ok: true,
|
|
219
|
+
claims: {
|
|
220
|
+
sub: "actor:asst:123",
|
|
221
|
+
scope_profile: "actor_client_v1",
|
|
222
|
+
},
|
|
223
|
+
}));
|
|
224
|
+
const { requireEdgeAuthWithScope } = makeMiddleware();
|
|
225
|
+
const res = await requireEdgeAuthWithScope(
|
|
226
|
+
makeReq({ authorization: "Bearer good.jwt.here" }),
|
|
227
|
+
"ingress.write",
|
|
228
|
+
);
|
|
229
|
+
expect(res?.status).toBe(403);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("null when token's scope_profile contains the required scope", async () => {
|
|
233
|
+
mockValidateEdgeToken = mock(() => ({
|
|
234
|
+
ok: true,
|
|
235
|
+
claims: {
|
|
236
|
+
sub: "actor:asst:123",
|
|
237
|
+
scope_profile: "actor_client_v1",
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
const { requireEdgeAuthWithScope } = makeMiddleware();
|
|
241
|
+
const res = await requireEdgeAuthWithScope(
|
|
242
|
+
makeReq({ authorization: "Bearer good.jwt.here" }),
|
|
243
|
+
"chat.write",
|
|
244
|
+
);
|
|
245
|
+
expect(res).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("401 when bearer token missing", async () => {
|
|
249
|
+
const { requireEdgeAuthWithScope } = makeMiddleware();
|
|
250
|
+
const res = await requireEdgeAuthWithScope(makeReq(), "chat.write");
|
|
251
|
+
expect(res?.status).toBe(401);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `requireEdgeGuardianAuth` middleware — both modes:
|
|
3
|
+
*
|
|
4
|
+
* 1. Platform-managed (DISABLE_HTTP_AUTH=true + IS_PLATFORM=true): identity
|
|
5
|
+
* asserted via `X-Vellum-User-Id` header cross-referenced against the
|
|
6
|
+
* stored `vellum:platform_user_id` credential.
|
|
7
|
+
* 2. Default (laptop / docker / bare-metal): edge JWT validated, then
|
|
8
|
+
* actor principal is matched against the bound vellum guardian.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import "./test-preload.js";
|
|
14
|
+
|
|
15
|
+
// --- Mocks (set BEFORE importing the module under test) -------------------
|
|
16
|
+
|
|
17
|
+
let mockReadCredential = mock(
|
|
18
|
+
async (_key: string): Promise<string | undefined> => undefined,
|
|
19
|
+
);
|
|
20
|
+
mock.module("../credential-reader.js", () => ({
|
|
21
|
+
readCredential: (key: string) => mockReadCredential(key),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
let mockFindVellumGuardian = mock(
|
|
25
|
+
async (): Promise<{ principalId: string } | null> => null,
|
|
26
|
+
);
|
|
27
|
+
mock.module("../auth/guardian-bootstrap.js", () => ({
|
|
28
|
+
findVellumGuardian: () => mockFindVellumGuardian(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
let mockValidateEdgeToken = mock(
|
|
32
|
+
(
|
|
33
|
+
_token: string,
|
|
34
|
+
):
|
|
35
|
+
| { ok: true; claims: { sub: string; scope_profile: string } }
|
|
36
|
+
| { ok: false; reason: string } => ({
|
|
37
|
+
ok: false,
|
|
38
|
+
reason: "noop",
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
mock.module("../auth/token-exchange.js", () => ({
|
|
42
|
+
validateEdgeToken: (token: string) => mockValidateEdgeToken(token),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const { AuthRateLimiter } = await import("../auth-rate-limiter.js");
|
|
46
|
+
const { createAuthMiddleware } = await import("../http/middleware/auth.js");
|
|
47
|
+
|
|
48
|
+
const PLATFORM_USER_ID = "user-abc-123";
|
|
49
|
+
const GUARDIAN_PRINCIPAL = "actor-guardian-xyz";
|
|
50
|
+
|
|
51
|
+
function makeMiddleware() {
|
|
52
|
+
const rl = new AuthRateLimiter();
|
|
53
|
+
return createAuthMiddleware(rl, () => "1.2.3.4");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeReq(headers: Record<string, string> = {}): Request {
|
|
57
|
+
return new Request("http://gateway.local/v1/contact-channels/abc/verify", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
mockReadCredential = mock(async () => undefined);
|
|
65
|
+
mockFindVellumGuardian = mock(async () => null);
|
|
66
|
+
mockValidateEdgeToken = mock(() => ({ ok: false, reason: "noop" }));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
delete process.env.DISABLE_HTTP_AUTH;
|
|
71
|
+
delete process.env.IS_PLATFORM;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// =========================================================================
|
|
75
|
+
// Platform-managed mode (DISABLE_HTTP_AUTH=true + IS_PLATFORM=true)
|
|
76
|
+
// =========================================================================
|
|
77
|
+
|
|
78
|
+
describe("requireEdgeGuardianAuth — platform header mode", () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
process.env.DISABLE_HTTP_AUTH = "true";
|
|
81
|
+
process.env.IS_PLATFORM = "true";
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("returns 401 when X-Vellum-User-Id header is missing", async () => {
|
|
85
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
86
|
+
const res = await requireEdgeGuardianAuth(makeReq());
|
|
87
|
+
expect(res?.status).toBe(401);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("returns 503 when readCredential throws (transient cred-store outage)", async () => {
|
|
91
|
+
mockReadCredential = mock(async () => {
|
|
92
|
+
throw new Error("simulated lookup failure");
|
|
93
|
+
});
|
|
94
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
95
|
+
const res = await requireEdgeGuardianAuth(
|
|
96
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
97
|
+
);
|
|
98
|
+
expect(res?.status).toBe(503);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("returns 403 when no platform_user_id is stored", async () => {
|
|
102
|
+
mockReadCredential = mock(async () => undefined);
|
|
103
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
104
|
+
const res = await requireEdgeGuardianAuth(
|
|
105
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
106
|
+
);
|
|
107
|
+
expect(res?.status).toBe(403);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns 403 when header does not match stored platform_user_id", async () => {
|
|
111
|
+
mockReadCredential = mock(async () => PLATFORM_USER_ID);
|
|
112
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
113
|
+
const res = await requireEdgeGuardianAuth(
|
|
114
|
+
makeReq({ "x-vellum-user-id": "different-user" }),
|
|
115
|
+
);
|
|
116
|
+
expect(res?.status).toBe(403);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("returns null (auth ok) when header matches stored platform_user_id", async () => {
|
|
120
|
+
mockReadCredential = mock(async () => PLATFORM_USER_ID);
|
|
121
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
122
|
+
const res = await requireEdgeGuardianAuth(
|
|
123
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
124
|
+
);
|
|
125
|
+
expect(res).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("falls through to JWT mode when IS_PLATFORM is false (rejects missing bearer token)", async () => {
|
|
129
|
+
// DISABLE_HTTP_AUTH=true but IS_PLATFORM=false → should use JWT path, not
|
|
130
|
+
// platform header path. No JWT provided, so expect 401.
|
|
131
|
+
process.env.IS_PLATFORM = "false";
|
|
132
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
133
|
+
const res = await requireEdgeGuardianAuth(
|
|
134
|
+
makeReq({ "x-vellum-user-id": PLATFORM_USER_ID }),
|
|
135
|
+
);
|
|
136
|
+
expect(res?.status).toBe(401);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// =========================================================================
|
|
141
|
+
// Default mode (JWT actor-principal == bound guardian)
|
|
142
|
+
// =========================================================================
|
|
143
|
+
|
|
144
|
+
describe("requireEdgeGuardianAuth — actor principal mode", () => {
|
|
145
|
+
test("returns 503 when findVellumGuardian throws (transient assistant DB outage)", async () => {
|
|
146
|
+
mockValidateEdgeToken = mock(() => ({
|
|
147
|
+
ok: true,
|
|
148
|
+
claims: {
|
|
149
|
+
sub: `actor:test-assistant:${GUARDIAN_PRINCIPAL}`,
|
|
150
|
+
scope_profile: "actor_client_v1",
|
|
151
|
+
},
|
|
152
|
+
}));
|
|
153
|
+
mockFindVellumGuardian = mock(async () => {
|
|
154
|
+
throw new Error("assistant DB IPC failed");
|
|
155
|
+
});
|
|
156
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
157
|
+
const res = await requireEdgeGuardianAuth(
|
|
158
|
+
makeReq({ authorization: "Bearer fake-jwt" }),
|
|
159
|
+
);
|
|
160
|
+
expect(res?.status).toBe(503);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns 403 when actor principal does not match the bound guardian", async () => {
|
|
164
|
+
mockValidateEdgeToken = mock(() => ({
|
|
165
|
+
ok: true,
|
|
166
|
+
claims: {
|
|
167
|
+
sub: `actor:test-assistant:some-other-actor`,
|
|
168
|
+
scope_profile: "actor_client_v1",
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
mockFindVellumGuardian = mock(async () => ({
|
|
172
|
+
principalId: GUARDIAN_PRINCIPAL,
|
|
173
|
+
}));
|
|
174
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
175
|
+
const res = await requireEdgeGuardianAuth(
|
|
176
|
+
makeReq({ authorization: "Bearer fake-jwt" }),
|
|
177
|
+
);
|
|
178
|
+
expect(res?.status).toBe(403);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("returns null (auth ok) when actor principal matches the bound guardian", async () => {
|
|
182
|
+
mockValidateEdgeToken = mock(() => ({
|
|
183
|
+
ok: true,
|
|
184
|
+
claims: {
|
|
185
|
+
sub: `actor:test-assistant:${GUARDIAN_PRINCIPAL}`,
|
|
186
|
+
scope_profile: "actor_client_v1",
|
|
187
|
+
},
|
|
188
|
+
}));
|
|
189
|
+
mockFindVellumGuardian = mock(async () => ({
|
|
190
|
+
principalId: GUARDIAN_PRINCIPAL,
|
|
191
|
+
}));
|
|
192
|
+
const { requireEdgeGuardianAuth } = makeMiddleware();
|
|
193
|
+
const res = await requireEdgeGuardianAuth(
|
|
194
|
+
makeReq({ authorization: "Bearer fake-jwt" }),
|
|
195
|
+
);
|
|
196
|
+
expect(res).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
});
|