@vellumai/vellum-gateway 0.6.5 → 0.6.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,190 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { initGatewayDb, resetGatewayDb } from "../db/connection.js";
3
+ import {
4
+ createConversationThresholdGetHandler,
5
+ createConversationThresholdPutHandler,
6
+ createConversationThresholdDeleteHandler,
7
+ } from "../http/routes/auto-approve-thresholds.js";
8
+
9
+ beforeEach(async () => {
10
+ resetGatewayDb();
11
+ await initGatewayDb();
12
+ });
13
+
14
+ afterEach(() => {
15
+ resetGatewayDb();
16
+ });
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const BASE_URL = "http://gateway.test";
23
+
24
+ function makeGet(conversationId: string): [Request, string[]] {
25
+ return [
26
+ new Request(
27
+ `${BASE_URL}/v1/permissions/thresholds/conversations/${conversationId}`,
28
+ { method: "GET" },
29
+ ),
30
+ [conversationId],
31
+ ];
32
+ }
33
+
34
+ function makePut(conversationId: string, body: unknown): [Request, string[]] {
35
+ return [
36
+ new Request(
37
+ `${BASE_URL}/v1/permissions/thresholds/conversations/${conversationId}`,
38
+ {
39
+ method: "PUT",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify(body),
42
+ },
43
+ ),
44
+ [conversationId],
45
+ ];
46
+ }
47
+
48
+ function makeDelete(conversationId: string): [Request, string[]] {
49
+ return [
50
+ new Request(
51
+ `${BASE_URL}/v1/permissions/thresholds/conversations/${conversationId}`,
52
+ { method: "DELETE" },
53
+ ),
54
+ [conversationId],
55
+ ];
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Tests
60
+ // ---------------------------------------------------------------------------
61
+
62
+ describe("GET /v1/permissions/thresholds/conversations/:conversationId", () => {
63
+ test("returns 404 for nonexistent conversation", async () => {
64
+ const handler = createConversationThresholdGetHandler();
65
+ const [req, params] = makeGet("conv-xyz");
66
+
67
+ const res = await handler(req, params);
68
+ expect(res.status).toBe(404);
69
+
70
+ const body = await res.json();
71
+ expect(body.error).toBe("No override for this conversation");
72
+ });
73
+
74
+ test("returns threshold after PUT creates it", async () => {
75
+ const putHandler = createConversationThresholdPutHandler();
76
+ const getHandler = createConversationThresholdGetHandler();
77
+
78
+ // Create the override
79
+ const [putReq, putParams] = makePut("conv-xyz", { threshold: "medium" });
80
+ const putRes = await putHandler(putReq, putParams);
81
+ expect(putRes.status).toBe(200);
82
+
83
+ // Read it back
84
+ const [getReq, getParams] = makeGet("conv-xyz");
85
+ const getRes = await getHandler(getReq, getParams);
86
+ expect(getRes.status).toBe(200);
87
+
88
+ const body = await getRes.json();
89
+ expect(body.threshold).toBe("medium");
90
+ });
91
+ });
92
+
93
+ describe("PUT /v1/permissions/thresholds/conversations/:conversationId", () => {
94
+ test("creates override and returns conversationId + threshold", async () => {
95
+ const handler = createConversationThresholdPutHandler();
96
+ const [req, params] = makePut("conv-abc", { threshold: "low" });
97
+
98
+ const res = await handler(req, params);
99
+ expect(res.status).toBe(200);
100
+
101
+ const body = await res.json();
102
+ expect(body).toEqual({ conversationId: "conv-abc", threshold: "low" });
103
+ });
104
+
105
+ test("updates existing override with new value", async () => {
106
+ const handler = createConversationThresholdPutHandler();
107
+ const getHandler = createConversationThresholdGetHandler();
108
+
109
+ // Create initial
110
+ const [req1, params1] = makePut("conv-abc", { threshold: "low" });
111
+ await handler(req1, params1);
112
+
113
+ // Update
114
+ const [req2, params2] = makePut("conv-abc", { threshold: "none" });
115
+ const res = await handler(req2, params2);
116
+ expect(res.status).toBe(200);
117
+
118
+ const body = await res.json();
119
+ expect(body).toEqual({ conversationId: "conv-abc", threshold: "none" });
120
+
121
+ // Verify via GET
122
+ const [getReq, getParams] = makeGet("conv-abc");
123
+ const getRes = await getHandler(getReq, getParams);
124
+ const getBody = await getRes.json();
125
+ expect(getBody.threshold).toBe("none");
126
+ });
127
+
128
+ test("returns 400 for invalid threshold", async () => {
129
+ const handler = createConversationThresholdPutHandler();
130
+ const [req, params] = makePut("conv-abc", { threshold: "invalid" });
131
+
132
+ const res = await handler(req, params);
133
+ expect(res.status).toBe(400);
134
+
135
+ const body = await res.json();
136
+ expect(body.error).toContain("threshold");
137
+ });
138
+
139
+ test("returns 400 for missing threshold", async () => {
140
+ const handler = createConversationThresholdPutHandler();
141
+ const [req, params] = makePut("conv-abc", {});
142
+
143
+ const res = await handler(req, params);
144
+ expect(res.status).toBe(400);
145
+ });
146
+
147
+ test("returns 400 for non-JSON body", async () => {
148
+ const handler = createConversationThresholdPutHandler();
149
+ const req = new Request(
150
+ `${BASE_URL}/v1/permissions/thresholds/conversations/conv-abc`,
151
+ {
152
+ method: "PUT",
153
+ body: "not json",
154
+ },
155
+ );
156
+
157
+ const res = await handler(req, ["conv-abc"]);
158
+ expect(res.status).toBe(400);
159
+ });
160
+ });
161
+
162
+ describe("DELETE /v1/permissions/thresholds/conversations/:conversationId", () => {
163
+ test("removes existing override, subsequent GET returns 404", async () => {
164
+ const putHandler = createConversationThresholdPutHandler();
165
+ const getHandler = createConversationThresholdGetHandler();
166
+ const deleteHandler = createConversationThresholdDeleteHandler();
167
+
168
+ // Create
169
+ const [putReq, putParams] = makePut("conv-del", { threshold: "medium" });
170
+ await putHandler(putReq, putParams);
171
+
172
+ // Delete
173
+ const [delReq, delParams] = makeDelete("conv-del");
174
+ const delRes = await deleteHandler(delReq, delParams);
175
+ expect(delRes.status).toBe(204);
176
+
177
+ // Verify gone
178
+ const [getReq, getParams] = makeGet("conv-del");
179
+ const getRes = await getHandler(getReq, getParams);
180
+ expect(getRes.status).toBe(404);
181
+ });
182
+
183
+ test("returns 204 on nonexistent conversation (idempotent)", async () => {
184
+ const handler = createConversationThresholdDeleteHandler();
185
+ const [req, params] = makeDelete("conv-nonexistent");
186
+
187
+ const res = await handler(req, params);
188
+ expect(res.status).toBe(204);
189
+ });
190
+ });
@@ -0,0 +1,238 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { initGatewayDb, resetGatewayDb } from "../db/connection.js";
3
+ import "./test-preload.js";
4
+
5
+ import {
6
+ createGlobalThresholdGetHandler,
7
+ createGlobalThresholdPutHandler,
8
+ } from "../http/routes/auto-approve-thresholds.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Setup / teardown
12
+ // ---------------------------------------------------------------------------
13
+
14
+ beforeEach(async () => {
15
+ resetGatewayDb();
16
+ await initGatewayDb();
17
+ });
18
+
19
+ afterEach(() => {
20
+ resetGatewayDb();
21
+ });
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function makeRequest(body?: unknown, method = "PUT"): Request {
28
+ if (body !== undefined) {
29
+ return new Request("http://localhost/v1/permissions/thresholds", {
30
+ method,
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify(body),
33
+ });
34
+ }
35
+ return new Request("http://localhost/v1/permissions/thresholds", {
36
+ method,
37
+ });
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Tests
42
+ // ---------------------------------------------------------------------------
43
+
44
+ describe("auto-approve thresholds", () => {
45
+ describe("GET handler", () => {
46
+ test("returns defaults when no row exists", async () => {
47
+ const handler = createGlobalThresholdGetHandler();
48
+ const res = await handler(makeRequest(undefined, "GET"));
49
+
50
+ expect(res.status).toBe(200);
51
+ const data = await res.json();
52
+ expect(data).toEqual({
53
+ interactive: "low",
54
+ background: "medium",
55
+ headless: "none",
56
+ });
57
+ });
58
+
59
+ test("returns updated values after PUT", async () => {
60
+ const putHandler = createGlobalThresholdPutHandler();
61
+ const getHandler = createGlobalThresholdGetHandler();
62
+
63
+ // First PUT to set values
64
+ await putHandler(makeRequest({ interactive: "medium" }));
65
+
66
+ // GET should reflect the update
67
+ const res = await getHandler(makeRequest(undefined, "GET"));
68
+ expect(res.status).toBe(200);
69
+ const data = await res.json();
70
+ expect(data).toEqual({
71
+ interactive: "medium",
72
+ background: "medium",
73
+ headless: "none",
74
+ });
75
+ });
76
+ });
77
+
78
+ describe("PUT handler", () => {
79
+ test("partial update only changes provided fields", async () => {
80
+ const handler = createGlobalThresholdPutHandler();
81
+
82
+ const res = await handler(makeRequest({ interactive: "medium" }));
83
+ expect(res.status).toBe(200);
84
+ const data = await res.json();
85
+ expect(data).toEqual({
86
+ interactive: "medium",
87
+ background: "medium",
88
+ headless: "none",
89
+ });
90
+ });
91
+
92
+ test("returns 400 for invalid threshold value", async () => {
93
+ const handler = createGlobalThresholdPutHandler();
94
+
95
+ const res = await handler(makeRequest({ interactive: "extreme" }));
96
+ expect(res.status).toBe(400);
97
+ const data = await res.json();
98
+ expect(data.error).toContain("interactive");
99
+ expect(data.error).toContain("none, low, medium, high");
100
+ });
101
+
102
+ test("accepts high as a valid threshold value", async () => {
103
+ const handler = createGlobalThresholdPutHandler();
104
+
105
+ const res = await handler(makeRequest({ interactive: "high" }));
106
+ expect(res.status).toBe(200);
107
+ const data = await res.json();
108
+ expect(data).toEqual({
109
+ interactive: "high",
110
+ background: "medium",
111
+ headless: "none",
112
+ });
113
+ });
114
+
115
+ test("returns 400 for invalid body (non-JSON)", async () => {
116
+ const handler = createGlobalThresholdPutHandler();
117
+
118
+ const req = new Request("http://localhost/v1/permissions/thresholds", {
119
+ method: "PUT",
120
+ body: "not json",
121
+ });
122
+ const res = await handler(req);
123
+ expect(res.status).toBe(400);
124
+ const data = await res.json();
125
+ expect(data.error).toContain("valid JSON");
126
+ });
127
+
128
+ test("returns 400 for non-object body", async () => {
129
+ const handler = createGlobalThresholdPutHandler();
130
+
131
+ const res = await handler(makeRequest("just a string"));
132
+ expect(res.status).toBe(400);
133
+ const data = await res.json();
134
+ expect(data.error).toContain("JSON object");
135
+ });
136
+
137
+ test("returns 400 for array body", async () => {
138
+ const handler = createGlobalThresholdPutHandler();
139
+
140
+ const res = await handler(makeRequest([1, 2, 3]));
141
+ expect(res.status).toBe(400);
142
+ const data = await res.json();
143
+ expect(data.error).toContain("JSON object");
144
+ });
145
+
146
+ test("returns 400 for invalid background value", async () => {
147
+ const handler = createGlobalThresholdPutHandler();
148
+
149
+ const res = await handler(makeRequest({ background: "invalid" }));
150
+ expect(res.status).toBe(400);
151
+ const data = await res.json();
152
+ expect(data.error).toContain("background");
153
+ });
154
+
155
+ test("returns 400 for invalid headless value", async () => {
156
+ const handler = createGlobalThresholdPutHandler();
157
+
158
+ const res = await handler(makeRequest({ headless: 42 }));
159
+ expect(res.status).toBe(400);
160
+ const data = await res.json();
161
+ expect(data.error).toContain("headless");
162
+ });
163
+
164
+ test("upserts correctly — first write creates, second write updates", async () => {
165
+ const handler = createGlobalThresholdPutHandler();
166
+
167
+ // First write — creates the row
168
+ const res1 = await handler(
169
+ makeRequest({ interactive: "none", background: "low" }),
170
+ );
171
+ expect(res1.status).toBe(200);
172
+ const data1 = await res1.json();
173
+ expect(data1).toEqual({
174
+ interactive: "none",
175
+ background: "low",
176
+ headless: "none",
177
+ });
178
+
179
+ // Second write — updates the existing row
180
+ const res2 = await handler(
181
+ makeRequest({ background: "medium", headless: "low" }),
182
+ );
183
+ expect(res2.status).toBe(200);
184
+ const data2 = await res2.json();
185
+ expect(data2).toEqual({
186
+ interactive: "none",
187
+ background: "medium",
188
+ headless: "low",
189
+ });
190
+ });
191
+
192
+ test("updates all fields at once", async () => {
193
+ const handler = createGlobalThresholdPutHandler();
194
+
195
+ const res = await handler(
196
+ makeRequest({
197
+ interactive: "medium",
198
+ background: "none",
199
+ headless: "low",
200
+ }),
201
+ );
202
+ expect(res.status).toBe(200);
203
+ const data = await res.json();
204
+ expect(data).toEqual({
205
+ interactive: "medium",
206
+ background: "none",
207
+ headless: "low",
208
+ });
209
+ });
210
+
211
+ test("empty object preserves existing values when row exists", async () => {
212
+ const putHandler = createGlobalThresholdPutHandler();
213
+
214
+ // First: set non-default values
215
+ await putHandler(
216
+ makeRequest({
217
+ interactive: "medium",
218
+ background: "none",
219
+ headless: "low",
220
+ }),
221
+ );
222
+
223
+ // Then PUT empty object — existing values should be preserved
224
+ const res = await putHandler(makeRequest({}));
225
+ expect(res.status).toBe(200);
226
+ const data = await res.json();
227
+ expect(data).toEqual({
228
+ interactive: "medium",
229
+ background: "none",
230
+ headless: "low",
231
+ });
232
+ });
233
+
234
+ // Note: "empty PUT inserts schema defaults when no row" is covered by
235
+ // the GET handler test suite. The PUT tests run after prior PUTs leave
236
+ // a row in the DB (bun test reuse), so we test preserve-existing above.
237
+ });
238
+ });
@@ -1,41 +1,56 @@
1
- import { describe, test, expect, beforeAll } from "bun:test";
1
+ import {
2
+ describe,
3
+ test,
4
+ expect,
5
+ beforeAll,
6
+ beforeEach,
7
+ afterEach,
8
+ } from "bun:test";
2
9
  import "./test-preload.js";
3
10
 
4
11
  import {
5
12
  initSigningKey,
6
13
  loadOrCreateSigningKey,
7
- mintToken,
8
14
  verifyToken,
9
15
  } from "../auth/token-service.js";
10
- import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
11
16
  import { createCloudOAuthTokenHandler } from "../http/routes/cloud-oauth-token.js";
12
17
 
13
- let serviceToken: string;
14
-
15
18
  beforeAll(() => {
16
19
  initSigningKey(loadOrCreateSigningKey());
17
- // Mint a service token (svc:gateway:self) that the handler accepts.
18
- serviceToken = mintToken({
19
- aud: "vellum-gateway",
20
- sub: "svc:gateway:self",
21
- scope_profile: "gateway_service_v1",
22
- policy_epoch: CURRENT_POLICY_EPOCH,
23
- ttlSeconds: 60,
24
- });
25
20
  });
26
21
 
27
22
  const handler = createCloudOAuthTokenHandler();
23
+ const ASSISTANT_ID = "asst-123";
24
+ const ACTOR_PRINCIPAL_ID = "user-456";
25
+ const PLATFORM_INTERNAL_API_KEY = "platform-internal-key";
26
+ const ORIGINAL_PLATFORM_INTERNAL_API_KEY =
27
+ process.env.PLATFORM_INTERNAL_API_KEY;
28
+
29
+ beforeEach(() => {
30
+ process.env.PLATFORM_INTERNAL_API_KEY = PLATFORM_INTERNAL_API_KEY;
31
+ });
28
32
 
29
- /** Build a POST request with the service-token Authorization header. */
30
- function makeRequest(body: unknown): Request {
33
+ afterEach(() => {
34
+ if (ORIGINAL_PLATFORM_INTERNAL_API_KEY === undefined) {
35
+ delete process.env.PLATFORM_INTERNAL_API_KEY;
36
+ return;
37
+ }
38
+ process.env.PLATFORM_INTERNAL_API_KEY = ORIGINAL_PLATFORM_INTERNAL_API_KEY;
39
+ });
40
+
41
+ /** Build a POST request to the cloud OAuth token endpoint. */
42
+ function makeRequest(body: unknown, authorization?: string): Request {
43
+ const headers: Record<string, string> = {
44
+ "content-type": "application/json",
45
+ };
46
+ if (authorization) {
47
+ headers.authorization = authorization;
48
+ }
31
49
  return new Request(
32
50
  "http://gateway.test/v1/internal/oauth/chrome-extension/token",
33
51
  {
34
52
  method: "POST",
35
- headers: {
36
- "content-type": "application/json",
37
- authorization: `Bearer ${serviceToken}`,
38
- },
53
+ headers,
39
54
  body: typeof body === "string" ? body : JSON.stringify(body),
40
55
  },
41
56
  );
@@ -44,7 +59,10 @@ function makeRequest(body: unknown): Request {
44
59
  describe("POST /v1/internal/oauth/chrome-extension/token", () => {
45
60
  test("happy path: valid body returns 200 with token, expiresIn, and guardianId", async () => {
46
61
  const res = await handler.handleMintToken(
47
- makeRequest({ assistantId: "asst-123", actorPrincipalId: "user-456" }),
62
+ makeRequest(
63
+ { assistantId: ASSISTANT_ID, actorPrincipalId: ACTOR_PRINCIPAL_ID },
64
+ `Bearer ${PLATFORM_INTERNAL_API_KEY}`,
65
+ ),
48
66
  );
49
67
 
50
68
  expect(res.status).toBe(200);
@@ -57,13 +75,15 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
57
75
  expect(typeof body.token).toBe("string");
58
76
  expect(body.token.split(".").length).toBe(3); // JWT format
59
77
  expect(body.expiresIn).toBe(3600);
60
- expect(body.guardianId).toBe("user-456");
78
+ expect(body.guardianId).toBe(ACTOR_PRINCIPAL_ID);
61
79
 
62
80
  // Verify the minted token has the correct claims
63
81
  const result = verifyToken(body.token, "vellum-gateway");
64
82
  expect(result.ok).toBe(true);
65
83
  if (result.ok) {
66
- expect(result.claims.sub).toBe("actor:asst-123:user-456");
84
+ expect(result.claims.sub).toBe(
85
+ `actor:${ASSISTANT_ID}:${ACTOR_PRINCIPAL_ID}`,
86
+ );
67
87
  expect(result.claims.aud).toBe("vellum-gateway");
68
88
  expect(result.claims.scope_profile).toBe("actor_client_v1");
69
89
  }
@@ -80,7 +100,7 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
80
100
 
81
101
  test("missing actorPrincipalId returns 400", async () => {
82
102
  const res = await handler.handleMintToken(
83
- makeRequest({ assistantId: "asst-123" }),
103
+ makeRequest({ assistantId: ASSISTANT_ID }),
84
104
  );
85
105
  expect(res.status).toBe(400);
86
106
  const body = (await res.json()) as { error: string };
@@ -98,7 +118,7 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
98
118
 
99
119
  test("empty actorPrincipalId string returns 400", async () => {
100
120
  const res = await handler.handleMintToken(
101
- makeRequest({ assistantId: "asst-123", actorPrincipalId: "" }),
121
+ makeRequest({ assistantId: ASSISTANT_ID, actorPrincipalId: "" }),
102
122
  );
103
123
  expect(res.status).toBe(400);
104
124
  const body = (await res.json()) as { error: string };
@@ -107,7 +127,7 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
107
127
 
108
128
  test("whitespace-only strings return 400", async () => {
109
129
  const res = await handler.handleMintToken(
110
- makeRequest({ assistantId: " ", actorPrincipalId: "user-456" }),
130
+ makeRequest({ assistantId: " ", actorPrincipalId: ACTOR_PRINCIPAL_ID }),
111
131
  );
112
132
  expect(res.status).toBe(400);
113
133
  const body = (await res.json()) as { error: string };
@@ -120,10 +140,7 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
120
140
  "http://gateway.test/v1/internal/oauth/chrome-extension/token",
121
141
  {
122
142
  method: "POST",
123
- headers: {
124
- "content-type": "application/json",
125
- authorization: `Bearer ${serviceToken}`,
126
- },
143
+ headers: { "content-type": "application/json" },
127
144
  body: "not-json",
128
145
  },
129
146
  ),
@@ -135,7 +152,7 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
135
152
 
136
153
  test("non-string assistantId returns 400", async () => {
137
154
  const res = await handler.handleMintToken(
138
- makeRequest({ assistantId: 123, actorPrincipalId: "user-456" }),
155
+ makeRequest({ assistantId: 123, actorPrincipalId: ACTOR_PRINCIPAL_ID }),
139
156
  );
140
157
  expect(res.status).toBe(400);
141
158
  const body = (await res.json()) as { error: string };
@@ -144,7 +161,10 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
144
161
 
145
162
  test("colon in assistantId returns 400", async () => {
146
163
  const res = await handler.handleMintToken(
147
- makeRequest({ assistantId: "asst:123", actorPrincipalId: "user-456" }),
164
+ makeRequest({
165
+ assistantId: "asst:123",
166
+ actorPrincipalId: ACTOR_PRINCIPAL_ID,
167
+ }),
148
168
  );
149
169
  expect(res.status).toBe(400);
150
170
  const body = (await res.json()) as { error: string };
@@ -153,7 +173,7 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
153
173
 
154
174
  test("colon in actorPrincipalId returns 400", async () => {
155
175
  const res = await handler.handleMintToken(
156
- makeRequest({ assistantId: "asst-123", actorPrincipalId: "user:456" }),
176
+ makeRequest({ assistantId: ASSISTANT_ID, actorPrincipalId: "user:456" }),
157
177
  );
158
178
  expect(res.status).toBe(400);
159
179
  const body = (await res.json()) as { error: string };
@@ -162,45 +182,43 @@ describe("POST /v1/internal/oauth/chrome-extension/token", () => {
162
182
 
163
183
  test("request without authorization header returns 403", async () => {
164
184
  const res = await handler.handleMintToken(
165
- new Request(
166
- "http://gateway.test/v1/internal/oauth/chrome-extension/token",
167
- {
168
- method: "POST",
169
- headers: { "content-type": "application/json" },
170
- body: JSON.stringify({
171
- assistantId: "asst-123",
172
- actorPrincipalId: "user-456",
173
- }),
174
- },
185
+ makeRequest({
186
+ assistantId: ASSISTANT_ID,
187
+ actorPrincipalId: ACTOR_PRINCIPAL_ID,
188
+ }),
189
+ );
190
+ expect(res.status).toBe(403);
191
+ });
192
+
193
+ test("request with invalid bearer token returns 403", async () => {
194
+ const res = await handler.handleMintToken(
195
+ makeRequest(
196
+ { assistantId: ASSISTANT_ID, actorPrincipalId: ACTOR_PRINCIPAL_ID },
197
+ "Bearer wrong-internal-key",
175
198
  ),
176
199
  );
177
200
  expect(res.status).toBe(403);
178
201
  });
179
202
 
180
- test("request with actor token (not service) returns 403", async () => {
181
- const actorToken = mintToken({
182
- aud: "vellum-gateway",
183
- sub: "actor:asst-123:some-user",
184
- scope_profile: "actor_client_v1",
185
- policy_epoch: CURRENT_POLICY_EPOCH,
186
- ttlSeconds: 60,
187
- });
203
+ test("request with non-bearer authorization header returns 403", async () => {
188
204
  const res = await handler.handleMintToken(
189
- new Request(
190
- "http://gateway.test/v1/internal/oauth/chrome-extension/token",
191
- {
192
- method: "POST",
193
- headers: {
194
- "content-type": "application/json",
195
- authorization: `Bearer ${actorToken}`,
196
- },
197
- body: JSON.stringify({
198
- assistantId: "asst-123",
199
- actorPrincipalId: "user-456",
200
- }),
201
- },
205
+ makeRequest(
206
+ { assistantId: ASSISTANT_ID, actorPrincipalId: ACTOR_PRINCIPAL_ID },
207
+ "Api-Key some-key",
202
208
  ),
203
209
  );
204
210
  expect(res.status).toBe(403);
205
211
  });
212
+
213
+ test("request returns 503 when PLATFORM_INTERNAL_API_KEY is not configured", async () => {
214
+ delete process.env.PLATFORM_INTERNAL_API_KEY;
215
+
216
+ const res = await handler.handleMintToken(
217
+ makeRequest(
218
+ { assistantId: ASSISTANT_ID, actorPrincipalId: ACTOR_PRINCIPAL_ID },
219
+ `Bearer ${PLATFORM_INTERNAL_API_KEY}`,
220
+ ),
221
+ );
222
+ expect(res.status).toBe(503);
223
+ });
206
224
  });