@vellumai/vellum-gateway 0.10.9-staging.1 → 0.10.9
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,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect, mock } from "bun:test";
|
|
1
|
+
import { afterAll, beforeAll, describe, test, expect, mock } from "bun:test";
|
|
2
2
|
import type { GatewayConfig } from "../config.js";
|
|
3
3
|
import { initSigningKey, mintToken } from "../auth/token-service.js";
|
|
4
4
|
import { CURRENT_POLICY_EPOCH } from "../auth/policy.js";
|
|
@@ -6,6 +6,7 @@ import type { CredentialCache } from "../credential-cache.js";
|
|
|
6
6
|
import {
|
|
7
7
|
createSpeechRelayUpgradeHandler,
|
|
8
8
|
getSpeechRelayWebsocketHandlers,
|
|
9
|
+
velayOriginForPlatformHost,
|
|
9
10
|
type SpeechRelaySocketData,
|
|
10
11
|
} from "../http/routes/speech-relay-websocket.js";
|
|
11
12
|
import { VELAY_FORWARDED_HEADER } from "../velay/bridge-utils.js";
|
|
@@ -81,6 +82,126 @@ async function bodyOf(
|
|
|
81
82
|
|
|
82
83
|
const TOKEN = mintDaemonServiceToken();
|
|
83
84
|
|
|
85
|
+
function makeKeyedCredentials(
|
|
86
|
+
values: Record<string, string | undefined>,
|
|
87
|
+
): CredentialCache {
|
|
88
|
+
return {
|
|
89
|
+
get: mock(async (key: string) => values[key]),
|
|
90
|
+
} as unknown as CredentialCache;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("velay origin derivation", () => {
|
|
94
|
+
// getPlatformBaseUrl falls back to this env var; pin it absent so the
|
|
95
|
+
// missing-credential cases below are deterministic on dev machines.
|
|
96
|
+
const savedPlatformUrl = process.env.VELLUM_PLATFORM_URL;
|
|
97
|
+
beforeAll(() => {
|
|
98
|
+
delete process.env.VELLUM_PLATFORM_URL;
|
|
99
|
+
});
|
|
100
|
+
afterAll(() => {
|
|
101
|
+
if (savedPlatformUrl !== undefined) {
|
|
102
|
+
process.env.VELLUM_PLATFORM_URL = savedPlatformUrl;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("velayOriginForPlatformHost follows the deployment naming convention", () => {
|
|
107
|
+
expect(velayOriginForPlatformHost("platform.vellum.ai")).toBe(
|
|
108
|
+
"https://velay.vellum.ai",
|
|
109
|
+
);
|
|
110
|
+
expect(velayOriginForPlatformHost("staging-platform.vellum.ai")).toBe(
|
|
111
|
+
"https://velay-staging.vellum.ai",
|
|
112
|
+
);
|
|
113
|
+
expect(velayOriginForPlatformHost("dev-platform.vellum.ai")).toBe(
|
|
114
|
+
"https://velay-dev.vellum.ai",
|
|
115
|
+
);
|
|
116
|
+
expect(velayOriginForPlatformHost("localhost")).toBeNull();
|
|
117
|
+
expect(velayOriginForPlatformHost("example.com")).toBeNull();
|
|
118
|
+
expect(
|
|
119
|
+
velayOriginForPlatformHost("staging-platform.vellum.ai.evil.com"),
|
|
120
|
+
).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("derives the velay origin from the stored platform base URL when VELAY_BASE_URL is unset", async () => {
|
|
124
|
+
const server = makeFakeServer();
|
|
125
|
+
const handler = createSpeechRelayUpgradeHandler(
|
|
126
|
+
makeConfig({ velayBaseUrl: undefined }),
|
|
127
|
+
"stt",
|
|
128
|
+
{
|
|
129
|
+
credentials: makeKeyedCredentials({
|
|
130
|
+
"credential/vellum/assistant_api_key": "vk-1",
|
|
131
|
+
"credential/vellum/platform_base_url":
|
|
132
|
+
"https://staging-platform.vellum.ai",
|
|
133
|
+
}),
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
const req = new Request(
|
|
137
|
+
`http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`,
|
|
138
|
+
{ headers: { upgrade: "websocket" } },
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(await handler(req, server)).toBeUndefined();
|
|
142
|
+
const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } })
|
|
143
|
+
.mock.calls[0]![1] as { data: SpeechRelaySocketData };
|
|
144
|
+
expect(new URL(data.data.upstreamWsUrl).origin).toBe(
|
|
145
|
+
"wss://velay-staging.vellum.ai",
|
|
146
|
+
);
|
|
147
|
+
expect(new URL(data.data.upstreamHttpUrl).origin).toBe(
|
|
148
|
+
"https://velay-staging.vellum.ai",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("falls back to prod velay for an off-convention or missing platform base URL", async () => {
|
|
153
|
+
for (const platformBaseUrl of [
|
|
154
|
+
"http://localhost:8000",
|
|
155
|
+
"not a url",
|
|
156
|
+
undefined,
|
|
157
|
+
]) {
|
|
158
|
+
const server = makeFakeServer();
|
|
159
|
+
const handler = createSpeechRelayUpgradeHandler(
|
|
160
|
+
makeConfig({ velayBaseUrl: undefined }),
|
|
161
|
+
"stt",
|
|
162
|
+
{
|
|
163
|
+
credentials: makeKeyedCredentials({
|
|
164
|
+
"credential/vellum/assistant_api_key": "vk-1",
|
|
165
|
+
"credential/vellum/platform_base_url": platformBaseUrl,
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
const req = new Request(
|
|
170
|
+
`http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`,
|
|
171
|
+
{ headers: { upgrade: "websocket" } },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(await handler(req, server)).toBeUndefined();
|
|
175
|
+
const data = (
|
|
176
|
+
server.upgrade as unknown as { mock: { calls: unknown[][] } }
|
|
177
|
+
).mock.calls[0]![1] as { data: SpeechRelaySocketData };
|
|
178
|
+
expect(new URL(data.data.upstreamWsUrl).origin).toBe(
|
|
179
|
+
"wss://velay.vellum.ai",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("an explicit VELAY_BASE_URL wins over the stored platform base URL", async () => {
|
|
185
|
+
const server = makeFakeServer();
|
|
186
|
+
const handler = createSpeechRelayUpgradeHandler(makeConfig(), "stt", {
|
|
187
|
+
credentials: makeKeyedCredentials({
|
|
188
|
+
"credential/vellum/assistant_api_key": "vk-1",
|
|
189
|
+
"credential/vellum/platform_base_url":
|
|
190
|
+
"https://staging-platform.vellum.ai",
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
const req = new Request(
|
|
194
|
+
`http://127.0.0.1:7830/v1/speech/stt/stream?key=${TOKEN}`,
|
|
195
|
+
{ headers: { upgrade: "websocket" } },
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(await handler(req, server)).toBeUndefined();
|
|
199
|
+
const data = (server.upgrade as unknown as { mock: { calls: unknown[][] } })
|
|
200
|
+
.mock.calls[0]![1] as { data: SpeechRelaySocketData };
|
|
201
|
+
expect(new URL(data.data.upstreamWsUrl).origin).toBe("wss://velay.test");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
84
205
|
describe("createSpeechRelayUpgradeHandler — gate", () => {
|
|
85
206
|
test("upgrades a daemon service connection and strips the key param", async () => {
|
|
86
207
|
const server = makeFakeServer();
|
|
@@ -178,14 +178,6 @@
|
|
|
178
178
|
"description": "Show a live HUD in the top-right of the conversation with scroll geometry, velocity, update rate, and anchor-preserver activity. Developer diagnostic for investigating scroll jank.",
|
|
179
179
|
"defaultEnabled": false
|
|
180
180
|
},
|
|
181
|
-
{
|
|
182
|
-
"id": "coding-agents-panel",
|
|
183
|
-
"scope": "client",
|
|
184
|
-
"key": "coding-agents-panel",
|
|
185
|
-
"label": "Coding Agents Panel",
|
|
186
|
-
"description": "Gate native macOS Coding Agents panel entry points and inline ACP session deep links.",
|
|
187
|
-
"defaultEnabled": false
|
|
188
|
-
},
|
|
189
181
|
{
|
|
190
182
|
"id": "compaction-playground",
|
|
191
183
|
"scope": "assistant",
|
|
@@ -311,7 +303,7 @@
|
|
|
311
303
|
"scope": "assistant",
|
|
312
304
|
"key": "memory-concept-graph",
|
|
313
305
|
"label": "Memory concept graph",
|
|
314
|
-
"description": "Gates the Obsidian-style memory concept graph on
|
|
306
|
+
"description": "Gates the Obsidian-style memory concept graph on its own Memory tab (/assistant/memory) and the backend-agnostic /memory-graph + /memory-graph-node routes that feed it. When off, the Memory tab is hidden; the identity page keeps the skills constellation. Only produces a graph on memory-v3 backends. Default off.",
|
|
315
307
|
"defaultEnabled": false
|
|
316
308
|
},
|
|
317
309
|
{
|
|
@@ -27,6 +27,7 @@ import type { GatewayConfig } from "../../config.js";
|
|
|
27
27
|
import type { CredentialCache } from "../../credential-cache.js";
|
|
28
28
|
import { credentialKey } from "../../credential-key.js";
|
|
29
29
|
import { getLogger } from "../../logger.js";
|
|
30
|
+
import { getPlatformBaseUrl } from "../../platform-url.js";
|
|
30
31
|
import { VELAY_FORWARDED_HEADER } from "../../velay/bridge-utils.js";
|
|
31
32
|
import { requestHasVelayBridgeAuth } from "../../velay/bridge-auth.js";
|
|
32
33
|
|
|
@@ -108,17 +109,58 @@ function jsonError(status: number, code: string, detail: string): Response {
|
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Map a Vellum platform host onto its environment's velay origin, following
|
|
114
|
+
* the deployment naming convention (`platform.vellum.ai` → `velay.vellum.ai`,
|
|
115
|
+
* `{env}-platform.vellum.ai` → `velay-{env}.vellum.ai`). Returns null for
|
|
116
|
+
* hosts outside that convention (localhost, custom domains).
|
|
117
|
+
*/
|
|
118
|
+
export function velayOriginForPlatformHost(host: string): string | null {
|
|
119
|
+
if (host === "platform.vellum.ai") {
|
|
120
|
+
return "https://velay.vellum.ai";
|
|
121
|
+
}
|
|
122
|
+
const match = /^([a-z0-9-]+)-platform\.vellum\.ai$/.exec(host);
|
|
123
|
+
return match ? `https://velay-${match[1]}.vellum.ai` : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the velay origin for this dial. An explicit VELAY_BASE_URL always
|
|
128
|
+
* wins; otherwise derive it from the stored platform base URL so a non-prod
|
|
129
|
+
* assistant's key is presented to its own environment's velay rather than
|
|
130
|
+
* prod (which would reject it with `invalid_key`).
|
|
131
|
+
*/
|
|
132
|
+
async function resolveVelayBaseUrl(
|
|
112
133
|
config: GatewayConfig,
|
|
134
|
+
deps: SpeechRelayDeps,
|
|
135
|
+
): Promise<string> {
|
|
136
|
+
if (config.velayBaseUrl) {
|
|
137
|
+
return config.velayBaseUrl;
|
|
138
|
+
}
|
|
139
|
+
const platformBaseUrl = await getPlatformBaseUrl(deps.credentials);
|
|
140
|
+
if (platformBaseUrl) {
|
|
141
|
+
try {
|
|
142
|
+
const derived = velayOriginForPlatformHost(
|
|
143
|
+
new URL(platformBaseUrl).hostname,
|
|
144
|
+
);
|
|
145
|
+
if (derived) {
|
|
146
|
+
return derived;
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// Unparseable platform URL — fall through to the prod default.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return DEFAULT_VELAY_BASE_URL;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function velaySpeechUrls(
|
|
156
|
+
velayBaseUrl: string,
|
|
113
157
|
operation: SpeechRelayOperation,
|
|
114
158
|
params: URLSearchParams,
|
|
115
159
|
): { wsUrl: string; httpUrl: string } {
|
|
116
160
|
// The velay tunnel client accepts ws(s):// bases too — normalize to the
|
|
117
161
|
// http(s) twin first so the probe URL is always fetchable, then derive
|
|
118
162
|
// the ws(s) dial URL from that.
|
|
119
|
-
const httpBase = (
|
|
120
|
-
.replace(/\/+$/, "")
|
|
121
|
-
.replace(/^ws/, "http");
|
|
163
|
+
const httpBase = velayBaseUrl.replace(/\/+$/, "").replace(/^ws/, "http");
|
|
122
164
|
const query = params.toString();
|
|
123
165
|
const path = `/v1/speech/${operation}/stream${query ? `?${query}` : ""}`;
|
|
124
166
|
return {
|
|
@@ -221,7 +263,11 @@ export function createSpeechRelayUpgradeHandler(
|
|
|
221
263
|
);
|
|
222
264
|
}
|
|
223
265
|
}
|
|
224
|
-
const { wsUrl, httpUrl } = velaySpeechUrls(
|
|
266
|
+
const { wsUrl, httpUrl } = velaySpeechUrls(
|
|
267
|
+
await resolveVelayBaseUrl(config, deps),
|
|
268
|
+
operation,
|
|
269
|
+
params,
|
|
270
|
+
);
|
|
225
271
|
|
|
226
272
|
// Non-upgrade request: this is the daemon's dial-failure probe.
|
|
227
273
|
// Replay the gate and hand back velay's own rejection.
|