@vellumai/assistant 0.12.2-dev.202609212215.5512e77 → 0.12.2-dev.202609212314.ad5e307

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 CHANGED
@@ -183,7 +183,7 @@ Internal forwarding routes (`/v1/internal/twilio/*`) are unaffected — these ac
183
183
  The `/channels/inbound` endpoint requires a JWT with the `svc_gateway` principal type and `ingress.write` scope to prove the request originated from the gateway. This ensures channel messages can only arrive via the gateway (which performs webhook-level verification) and not via direct HTTP calls that bypass signature checks.
184
184
 
185
185
  - **JWT-based enforcement:** The route policy in `route-policy.ts` restricts `/channels/inbound` to the `svc_gateway` principal type with `ingress.write` scope. Actor and local principals are rejected with 403.
186
- - **Auth bypass:** When `DISABLE_HTTP_AUTH=true` is set (platform-managed deployments), JWT verification is skipped and a synthetic context is used.
186
+ - **Auth bypass:** When `DISABLE_HTTP_AUTH=true` is set (platform-managed deployments), a request that supplies no `Authorization` header gets a synthetic context. A request that supplies one is verified normally, and a token that fails verification is rejected.
187
187
 
188
188
  ## Twilio Setup Primitive
189
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.2-dev.202609212215.5512e77",
3
+ "version": "0.12.2-dev.202609212314.ad5e307",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -241,8 +241,10 @@ function makeConfirmationEmittingSession(opts?: {
241
241
  // Tests
242
242
  // ---------------------------------------------------------------------------
243
243
 
244
- const TEST_TOKEN = "test-bearer-token-approvals";
245
- const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
244
+ // This suite mocks isHttpAuthDisabled() to true, so a request carrying no
245
+ // bearer authenticates through the dev bypass. A bearer that is present is
246
+ // verified against the signing key, which these tests do not set up.
247
+ const AUTH_HEADERS: Record<string, string> = {};
246
248
 
247
249
  describe("standalone approval endpoints — HTTP layer", () => {
248
250
  let server: RuntimeHttpServer;
@@ -82,8 +82,10 @@ await initializeDb();
82
82
  // Helpers
83
83
  // ---------------------------------------------------------------------------
84
84
 
85
- const TEST_TOKEN = "test-bearer-token-calls";
86
- const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
85
+ // This suite mocks isHttpAuthDisabled() to true, so a request carrying no
86
+ // bearer authenticates through the dev bypass. A bearer that is present is
87
+ // verified against the signing key, which these tests do not set up.
88
+ const AUTH_HEADERS: Record<string, string> = {};
87
89
 
88
90
  let ensuredConvIds = new Set<string>();
89
91
 
@@ -55,8 +55,10 @@ afterAll(() => {
55
55
  resetDbForTesting();
56
56
  });
57
57
 
58
- const TEST_TOKEN = "test-bearer-token-attach";
59
- const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
58
+ // This suite mocks isHttpAuthDisabled() to true, so a request carrying no
59
+ // bearer authenticates through the dev bypass. A bearer that is present is
60
+ // verified against the signing key, which these tests do not set up.
61
+ const AUTH_HEADERS: Record<string, string> = {};
60
62
 
61
63
  describe("Runtime attachment metadata", () => {
62
64
  let server: RuntimeHttpServer;
@@ -302,8 +302,10 @@ function makePendingApprovalConversation(
302
302
  // Tests
303
303
  // ---------------------------------------------------------------------------
304
304
 
305
- const TEST_TOKEN = "test-bearer-token-send";
306
- const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };
305
+ // This suite mocks isHttpAuthDisabled() to true, so a request carrying no
306
+ // bearer authenticates through the dev bypass. A bearer that is present is
307
+ // verified against the signing key, which these tests do not set up.
308
+ const AUTH_HEADERS: Record<string, string> = {};
307
309
 
308
310
  describe("POST /v1/messages — queue-if-busy and hub publishing", () => {
309
311
  let server: RuntimeHttpServer;
@@ -439,6 +439,23 @@ describe("GeminiEmbeddingBackend: batched text inputs", () => {
439
439
  expect(vectors.map((v) => v[0])).toEqual(texts(250).map(textIndex));
440
440
  });
441
441
 
442
+ test("managed text inputs use single calls from the first request and preserve vector order", async () => {
443
+ const fetchMock = routedFetch();
444
+ globalThis.fetch = fetchMock as unknown as typeof fetch;
445
+ const backend = new GeminiEmbeddingBackend("test-key", "test-model", {
446
+ managedBaseUrl: "https://proxy.example.com/v1/runtime-proxy/gemini",
447
+ interCallDelayMs: 0,
448
+ });
449
+
450
+ expect(await backend.embed(texts(3))).toEqual([[0], [1], [2]]);
451
+ expect(await backend.embed(["t4", "t5"])).toEqual([[4], [5]]);
452
+ expect(calledUrls(fetchMock)).toEqual(
453
+ Array(5).fill(
454
+ "https://proxy.example.com/v1/runtime-proxy/gemini/v1beta/models/test-model:embedContent",
455
+ ),
456
+ );
457
+ });
458
+
442
459
  test("each batched request names the model with the models/ prefix and carries taskType and outputDimensionality", async () => {
443
460
  const fetchMock = routedFetch();
444
461
  globalThis.fetch = fetchMock as unknown as typeof fetch;
@@ -71,7 +71,8 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
71
71
  }
72
72
 
73
73
  /**
74
- * Embed `inputs` in order. Runs of text inputs go through
74
+ * Embed `inputs` in order. Managed requests use single `embedContent`
75
+ * calls because Vertex does not expose the batch route. Direct text runs use
75
76
  * `batchEmbedContents`, {@link GEMINI_EMBED_BATCH_SIZE} texts per round
76
77
  * trip, so a corpus re-embed costs one request per hundred sections rather
77
78
  * than one per section; a lone text and every multimodal input take the
@@ -99,6 +100,7 @@ export class GeminiEmbeddingBackend implements EmbeddingBackend {
99
100
  // Gather the run of text inputs starting here, up to one batch.
100
101
  let end = i;
101
102
  while (
103
+ !this.managed &&
102
104
  !this.batchRouteUnavailable &&
103
105
  end < normalized.length &&
104
106
  end - i < GEMINI_EMBED_BATCH_SIZE &&
@@ -325,6 +325,144 @@ describe("authenticateRequest for /v1/host-browser-result", () => {
325
325
  });
326
326
  });
327
327
 
328
+ // ---------------------------------------------------------------------------
329
+ // Dev bypass covers only a request that supplied no Authorization header. A
330
+ // bearer sent to a platform pod (the gateway's exchange token, or a trusted
331
+ // contact's) is verified on its own terms.
332
+ // ---------------------------------------------------------------------------
333
+
334
+ describe("authenticateRequest with auth disabled and a bearer present", () => {
335
+ beforeEach(() => {
336
+ authDisabled = true;
337
+ });
338
+
339
+ test("uses the token's claims rather than the dev-bypass context", () => {
340
+ const token = mintValidToken({ sub: "actor:self:contact-principal" });
341
+
342
+ const req = new Request("http://localhost/v1/messages", {
343
+ method: "POST",
344
+ headers: { Authorization: `Bearer ${token}` },
345
+ });
346
+
347
+ const result = authenticateRequest(req);
348
+ expect(result.ok).toBe(true);
349
+ if (result.ok) {
350
+ expect(result.context.actorPrincipalId).toBe("contact-principal");
351
+ expect(result.context.policyEpoch).toBe(CURRENT_POLICY_EPOCH);
352
+ }
353
+ });
354
+
355
+ test("accepts the gateway's exchange token", () => {
356
+ const token = mintValidToken({
357
+ sub: "svc:gateway:self",
358
+ scope_profile: "gateway_service_v1",
359
+ });
360
+
361
+ const req = new Request("http://localhost/v1/messages", {
362
+ method: "POST",
363
+ headers: { Authorization: `Bearer ${token}` },
364
+ });
365
+
366
+ const result = authenticateRequest(req);
367
+ expect(result.ok).toBe(true);
368
+ if (result.ok) {
369
+ expect(result.context.principalType).toBe("svc_gateway");
370
+ }
371
+ });
372
+
373
+ test("accepts a gateway-audience token through the fallback", () => {
374
+ const token = mintValidToken({
375
+ aud: "vellum-gateway",
376
+ sub: "actor:self:local-client",
377
+ });
378
+
379
+ const req = new Request("http://localhost/v1/messages", {
380
+ method: "POST",
381
+ headers: { Authorization: `Bearer ${token}` },
382
+ });
383
+
384
+ const result = authenticateRequest(req);
385
+ expect(result.ok).toBe(true);
386
+ if (result.ok) {
387
+ expect(result.context.actorPrincipalId).toBe("local-client");
388
+ expect(result.context.assistantId).toBe(DAEMON_INTERNAL_ASSISTANT_ID);
389
+ }
390
+ });
391
+
392
+ test.each([
393
+ ["malformed", () => "not-a-token.xxxxxxxxxxxxx"],
394
+ [
395
+ "expired",
396
+ () => mintValidToken({ exp: Math.floor(Date.now() / 1000) - 100 }),
397
+ ],
398
+ [
399
+ "wrong-audience",
400
+ () => mintValidToken({ aud: "vellum-other" as TokenAudience }),
401
+ ],
402
+ ["unparseable-sub", () => mintValidToken({ sub: "garbage" })],
403
+ ])("refuses a %s token rather than falling back", (_label, build) => {
404
+ const req = new Request("http://localhost/v1/messages", {
405
+ method: "POST",
406
+ headers: { Authorization: `Bearer ${build()}` },
407
+ });
408
+
409
+ const result = authenticateRequest(req);
410
+ expect(result.ok).toBe(false);
411
+ if (!result.ok) {
412
+ expect(result.response.status).toBe(401);
413
+ }
414
+ });
415
+
416
+ test("still returns the dev-bypass context when no bearer is sent", () => {
417
+ const req = new Request("http://localhost/v1/messages", {
418
+ method: "POST",
419
+ });
420
+
421
+ const result = authenticateRequest(req);
422
+ expect(result.ok).toBe(true);
423
+ if (result.ok) {
424
+ expect(result.context.actorPrincipalId).toBe("dev-bypass");
425
+ }
426
+ });
427
+
428
+ test("verifies a lowercase bearer scheme rather than bypassing it", () => {
429
+ const token = mintValidToken({ sub: "actor:self:contact-principal" });
430
+
431
+ const req = new Request("http://localhost/v1/messages", {
432
+ method: "POST",
433
+ headers: { Authorization: `bearer ${token}` },
434
+ });
435
+
436
+ const result = authenticateRequest(req);
437
+ expect(result.ok).toBe(true);
438
+ if (result.ok) {
439
+ expect(result.context.actorPrincipalId).toBe("contact-principal");
440
+ }
441
+ });
442
+
443
+ test.each([
444
+ ["lowercase scheme with a bad token", "bearer not-a-jwt.xxxxxxxx"],
445
+ ["empty credential", "Bearer "],
446
+ ["non-bearer scheme", "Basic dXNlcjpwYXNz"],
447
+ ["an empty header value", ""],
448
+ ["a whitespace-only header value", " "],
449
+ ])(
450
+ "refuses %s instead of granting the dev-bypass context",
451
+ (_label, header) => {
452
+ const req = new Request("http://localhost/v1/messages", {
453
+ method: "POST",
454
+ headers: { Authorization: header },
455
+ });
456
+
457
+ const result = authenticateRequest(req);
458
+ expect(result.ok).toBe(false);
459
+ if (!result.ok) {
460
+ expect(result.response.status).toBe(401);
461
+ }
462
+ },
463
+ );
464
+ });
465
+
328
466
  // ---------------------------------------------------------------------------
329
467
  // oauth_proxy_v1 grants: the narrow bearer a third-party CLI presents to the
330
468
  // OAuth passthrough route. Accepted on either audience, and carrying nothing
@@ -17,9 +17,10 @@
17
17
  * Replaces both the legacy bearer shared-secret check and the
18
18
  * actor-token HMAC middleware with a single JWT verification path.
19
19
  *
20
- * When DISABLE_HTTP_AUTH is set (platform-managed deployments), JWT
21
- * verification is skipped and a synthetic AuthContext is constructed
22
- * so downstream code always has a typed context to consume.
20
+ * When DISABLE_HTTP_AUTH is set (platform-managed deployments), a request
21
+ * carrying no bearer gets a synthetic AuthContext so downstream code always
22
+ * has a typed context to consume. A request that carries one is verified
23
+ * normally, and a bad token is refused rather than falling back.
23
24
  */
24
25
 
25
26
  import { isHttpAuthDisabled } from "../../config/env.js";
@@ -73,17 +74,18 @@ function buildDevBypassContext(): AuthContext {
73
74
  * The caller should return the error Response directly to the client.
74
75
  */
75
76
  export function authenticateRequest(req: Request): AuthenticateResult {
76
- // Dev bypass: skip JWT verification entirely
77
- if (isHttpAuthDisabled()) {
78
- return { ok: true, context: buildDevBypassContext() };
79
- }
80
-
81
- const path = new URL(req.url).pathname;
82
-
83
77
  const rawToken = extractBearerToken(req);
84
78
  if (!rawToken) {
79
+ // Dev bypass covers only a request that supplied no Authorization header
80
+ // at all. A header that is present but unusable (empty or whitespace-only
81
+ // value, a non-bearer scheme, a malformed credential) is refused rather
82
+ // than handed the guardian context. `has` rather than `get`: an empty
83
+ // value reads back as "" and would otherwise pass as absent.
84
+ if (isHttpAuthDisabled() && !req.headers.has("authorization")) {
85
+ return { ok: true, context: buildDevBypassContext() };
86
+ }
85
87
  log.warn(
86
- { reason: "missing_token", path },
88
+ { reason: "missing_token", path: new URL(req.url).pathname },
87
89
  "Auth denied: missing Authorization header",
88
90
  );
89
91
  return {
@@ -100,6 +102,8 @@ export function authenticateRequest(req: Request): AuthenticateResult {
100
102
  };
101
103
  }
102
104
 
105
+ const path = new URL(req.url).pathname;
106
+
103
107
  // Verify the JWT — prefer vellum-daemon audience (gateway-proxied requests
104
108
  // and daemon-minted tokens), but also accept vellum-gateway audience for
105
109
  // direct local clients (macOS SettingsStore) that hold a guardian-issued JWT
@@ -156,8 +156,14 @@ export function isPrivateNetworkOrigin(req: Request): boolean {
156
156
  /**
157
157
  * Extract and validate a bearer token from the Authorization header.
158
158
  * Returns the token string if present, or null.
159
+ *
160
+ * The scheme is matched case-insensitively, per RFC 7235 and to match the
161
+ * gateway's own parsing. An empty credential returns null.
159
162
  */
160
163
  export function extractBearerToken(req: Request): string | null {
161
164
  const authHeader = req.headers.get("authorization");
162
- return authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
165
+ if (!authHeader?.toLowerCase().startsWith("bearer ")) {
166
+ return null;
167
+ }
168
+ return authHeader.slice(7) || null;
163
169
  }