@pix3/agent-bridge 0.2.0 → 0.2.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/src/proxy.ts CHANGED
@@ -1,68 +1,68 @@
1
- /**
2
- * Credential-injecting reverse proxy for the metered LLM providers. The editor talks to the bridge
3
- * with the pairing token only; this module strips that token, builds a fresh upstream request with
4
- * the provider's stored key, and forwards it to the fixed upstream base for that provider.
5
- *
6
- * Security properties:
7
- * - the upstream host is NEVER taken from the client request — it comes from the provider's stored
8
- * `baseUrl`, so this cannot be turned into an open relay / SSRF against arbitrary hosts;
9
- * - the outbound request carries ONLY `content-type` + the injected auth header — the pairing
10
- * token, cookies, Origin/Host and any other inbound headers are dropped, so the bridge token
11
- * never leaks upstream;
12
- * - the wire body is passed through verbatim, so the bridge stays agnostic to each provider's
13
- * request/response format (OpenAI Chat Completions vs Anthropic Messages).
14
- */
15
-
16
- import type { ProviderConfig } from './config.ts';
17
-
18
- const ANTHROPIC_VERSION = '2023-06-01';
19
-
20
- export interface ProxyRequest {
21
- readonly method: string;
22
- /** Path AFTER `/providers/:id`, e.g. `/chat/completions`, `/messages`, `/models`. */
23
- readonly restPath: string;
24
- /** Raw query string including the leading `?`, or `''`. */
25
- readonly query: string;
26
- /** Request body bytes (POST), or null (GET). */
27
- readonly body: Buffer | null;
28
- }
29
-
30
- export interface ProxyResult {
31
- readonly status: number;
32
- readonly contentType: string;
33
- readonly body: Buffer;
34
- }
35
-
36
- const buildUpstreamHeaders = (provider: ProviderConfig): Record<string, string> => {
37
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
38
- if (provider.kind === 'anthropic') {
39
- headers['x-api-key'] = provider.apiKey;
40
- headers['anthropic-version'] = ANTHROPIC_VERSION;
41
- } else {
42
- headers['Authorization'] = `Bearer ${provider.apiKey}`;
43
- }
44
- return headers;
45
- };
46
-
47
- /** Forward one editor request to its provider's upstream, injecting the stored key. */
48
- export const forwardToProvider = async (
49
- provider: ProviderConfig,
50
- request: ProxyRequest
51
- ): Promise<ProxyResult> => {
52
- const url = `${provider.baseUrl}${request.restPath}${request.query}`;
53
- const init: RequestInit = {
54
- method: request.method,
55
- headers: buildUpstreamHeaders(provider),
56
- };
57
- if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {
58
- init.body = request.body;
59
- }
60
-
61
- const response = await fetch(url, init);
62
- const body = Buffer.from(await response.arrayBuffer());
63
- return {
64
- status: response.status,
65
- contentType: response.headers.get('content-type') ?? 'application/json',
66
- body,
67
- };
68
- };
1
+ /**
2
+ * Credential-injecting reverse proxy for the metered LLM providers. The editor talks to the bridge
3
+ * with the pairing token only; this module strips that token, builds a fresh upstream request with
4
+ * the provider's stored key, and forwards it to the fixed upstream base for that provider.
5
+ *
6
+ * Security properties:
7
+ * - the upstream host is NEVER taken from the client request — it comes from the provider's stored
8
+ * `baseUrl`, so this cannot be turned into an open relay / SSRF against arbitrary hosts;
9
+ * - the outbound request carries ONLY `content-type` + the injected auth header — the pairing
10
+ * token, cookies, Origin/Host and any other inbound headers are dropped, so the bridge token
11
+ * never leaks upstream;
12
+ * - the wire body is passed through verbatim, so the bridge stays agnostic to each provider's
13
+ * request/response format (OpenAI Chat Completions vs Anthropic Messages).
14
+ */
15
+
16
+ import type { ProviderConfig } from './config.ts';
17
+
18
+ const ANTHROPIC_VERSION = '2023-06-01';
19
+
20
+ export interface ProxyRequest {
21
+ readonly method: string;
22
+ /** Path AFTER `/providers/:id`, e.g. `/chat/completions`, `/messages`, `/models`. */
23
+ readonly restPath: string;
24
+ /** Raw query string including the leading `?`, or `''`. */
25
+ readonly query: string;
26
+ /** Request body bytes (POST), or null (GET). */
27
+ readonly body: Buffer | null;
28
+ }
29
+
30
+ export interface ProxyResult {
31
+ readonly status: number;
32
+ readonly contentType: string;
33
+ readonly body: Buffer;
34
+ }
35
+
36
+ const buildUpstreamHeaders = (provider: ProviderConfig): Record<string, string> => {
37
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
38
+ if (provider.kind === 'anthropic') {
39
+ headers['x-api-key'] = provider.apiKey;
40
+ headers['anthropic-version'] = ANTHROPIC_VERSION;
41
+ } else {
42
+ headers['Authorization'] = `Bearer ${provider.apiKey}`;
43
+ }
44
+ return headers;
45
+ };
46
+
47
+ /** Forward one editor request to its provider's upstream, injecting the stored key. */
48
+ export const forwardToProvider = async (
49
+ provider: ProviderConfig,
50
+ request: ProxyRequest
51
+ ): Promise<ProxyResult> => {
52
+ const url = `${provider.baseUrl}${request.restPath}${request.query}`;
53
+ const init: RequestInit = {
54
+ method: request.method,
55
+ headers: buildUpstreamHeaders(provider),
56
+ };
57
+ if (request.body && request.method !== 'GET' && request.method !== 'HEAD') {
58
+ init.body = request.body;
59
+ }
60
+
61
+ const response = await fetch(url, init);
62
+ const body = Buffer.from(await response.arrayBuffer());
63
+ return {
64
+ status: response.status,
65
+ contentType: response.headers.get('content-type') ?? 'application/json',
66
+ body,
67
+ };
68
+ };