@jxsuite/server 4.3.0 → 4.4.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.
Files changed (3) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +5 -4
  3. package/src/ai-api.ts +44 -14
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Kevin Shenk
3
+ Copyright (c) 2026 Avunu LLC and Jx Suite contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "4.3.0",
3
+ "version": "4.4.1",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,19 +41,20 @@
41
41
  "upgrade": "bun update --latest && bun install"
42
42
  },
43
43
  "dependencies": {
44
- "@jxsuite/ai": "^0.38.0",
44
+ "@jxsuite/ai": "^0.39.1",
45
45
  "@jxsuite/catalog": "^0.1.0",
46
- "@jxsuite/collab": "^0.10.0",
46
+ "@jxsuite/collab": "^0.11.0",
47
47
  "@jxsuite/compiler": "^4.0.2",
48
48
  "@jxsuite/create": "^1.3.12",
49
49
  "@jxsuite/import": "^0.40.4",
50
- "@jxsuite/protocol": "^2.4.0",
50
+ "@jxsuite/protocol": "^2.6.0",
51
51
  "@jxsuite/runtime": "^4.0.2",
52
52
  "@jxsuite/schema": "^2.3.0",
53
53
  "@jxsuite/site": "^2.0.2",
54
54
  "@jxsuite/starters": "^1.8.2",
55
55
  "chokidar": "^5.0.0",
56
56
  "kysely": "^0.29.6",
57
+ "oxfmt": "^0.70.0",
57
58
  "zod": "^4.6.5"
58
59
  },
59
60
  "devDependencies": {
package/src/ai-api.ts CHANGED
@@ -1,25 +1,34 @@
1
1
  /**
2
2
  * Ai-api.js — AI proxy endpoints for Jx Studio
3
3
  *
4
- * Handles /__studio/ai/chat (SSE streaming proxy to OpenAI) and /__studio/ai/models.
5
- * The server acts as a thin proxy: validates the request shape, forwards to OpenAI,
6
- * normalizes the SSE stream into StreamEvent-compatible format, and pipes back.
4
+ * Handles /__studio/ai/chat (SSE streaming proxy to OpenAI) and /__studio/ai/models. The server
5
+ * acts as a thin proxy: validates the request shape, forwards to OpenAI, normalizes the SSE stream
6
+ * into StreamEvent-compatible format, and pipes back.
7
7
  *
8
8
  * The wire half of that (reading the body, the upstream request, the stream normalizer, the SSE
9
9
  * framing, the problem responses) is `@jxsuite/ai/gateway`, which every backend serving these
10
10
  * routes can share. What stays here is this server's POLICY, which no other host shares:
11
11
  *
12
12
  * API key flow:
13
- * 1. Request header X-Api-Key or Authorization: Bearer <key>
14
- * 2. Fallback: OPENAI_API_KEY env var — attached ONLY to the env/default base URL, never to a
15
- * caller-supplied X-Api-Base-URL (prevents exfiltrating the server key to a chosen endpoint)
16
- * 3. If neither → 401 with error message
13
+ *
14
+ * 1. Request header X-Api-Key or Authorization: Bearer <key>
15
+ * 2. Fallback: OPENAI_API_KEY env var — attached ONLY to the env/default base URL, never to a
16
+ * caller-supplied X-Api-Base-URL (prevents exfiltrating the server key to a chosen endpoint)
17
+ * 3. If neither → 401 with error message
17
18
  *
18
19
  * Base URL flow:
19
- * 1. Request header X-Api-Base-URL — allowed only alongside a header API key
20
- * 2. Fallback: OPENAI_BASE_URL env var
21
- * 3. Default: https://api.openai.com/v1
22
- * A base URL resolving to a cloud metadata / link-local host is refused (SSRF defense).
20
+ *
21
+ * 1. Request header X-Api-Base-URL — allowed only alongside a header API key
22
+ * 2. Fallback: OPENAI_BASE_URL env var
23
+ * 3. Default: https://api.openai.com/v1
24
+ *
25
+ * A base URL resolving to a cloud metadata / link-local host is refused (SSRF defense).
26
+ *
27
+ * Prompt-cache affinity: a request carrying a valid X-Jx-Ai-Session header (the client's
28
+ * conversation id) is forwarded with `sessionAffinity` set to a hash of it, scoped to this server
29
+ * ("local"). The gateway sends that hash only to an upstream that routes on it (OpenAI, Workers
30
+ * AI); the raw id never leaves this process. An absent or malformed header just means no hint: the
31
+ * request is still served.
23
32
  *
24
33
  * The model catalogue for /models is this server's too: its defaults, and how it reads the
25
34
  * upstream's own listing.
@@ -27,8 +36,11 @@
27
36
  * @license MIT
28
37
  */
29
38
  import {
39
+ AI_SESSION_HEADER,
40
+ affinityKey,
30
41
  createChatHandler,
31
42
  extractUpstreamErrorMessage,
43
+ isAiSessionId,
32
44
  modelsResponse,
33
45
  problemResponse,
34
46
  } from "@jxsuite/ai/gateway";
@@ -43,6 +55,12 @@ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
43
55
  /** The model a chat request that names none is forwarded with. */
44
56
  const DEFAULT_MODEL = "gpt-4o";
45
57
 
58
+ /**
59
+ * This server's namespace for affinity keys. Every dev/desktop server shares it: their keys never
60
+ * meet a hosted backend's, because that one hashes under its own scope.
61
+ */
62
+ const AFFINITY_SCOPE = "local";
63
+
46
64
  /** A model entry from the upstream `/models` listing. */
47
65
  interface ModelEntry {
48
66
  id: string;
@@ -151,9 +169,10 @@ function refuse(status: number, message: string): GatewayRefusal {
151
169
 
152
170
  /**
153
171
  * This server's upstream for a request, or its refusal: the key-provenance and SSRF rules above,
154
- * applied per request (the environment is read each time, so a key set later is honoured).
172
+ * applied per request (the environment is read each time, so a key set later is honoured). A valid
173
+ * session header adds the hashed affinity key; an invalid one is ignored rather than refused.
155
174
  */
156
- function resolveUpstream(req: Request): Upstream | GatewayRefusal {
175
+ async function resolveUpstream(req: Request): Promise<Upstream | GatewayRefusal> {
157
176
  const { apiKey, baseUrl, reject } = getConfig(req);
158
177
  if (reject) {
159
178
  return refuse(reject.status, reject.message);
@@ -164,7 +183,18 @@ function resolveUpstream(req: Request): Upstream | GatewayRefusal {
164
183
  "No API key configured. Set OPENAI_API_KEY env var or send X-Api-Key header.",
165
184
  );
166
185
  }
167
- return { apiKey, baseUrl, defaultModel: DEFAULT_MODEL, family: "openai-compat", managed: false };
186
+ const upstream: Upstream = {
187
+ apiKey,
188
+ baseUrl,
189
+ defaultModel: DEFAULT_MODEL,
190
+ family: "openai-compat",
191
+ managed: false,
192
+ };
193
+ const sessionId = req.headers.get(AI_SESSION_HEADER);
194
+ if (isAiSessionId(sessionId)) {
195
+ return { ...upstream, sessionAffinity: await affinityKey(AFFINITY_SCOPE, sessionId) };
196
+ }
197
+ return upstream;
168
198
  }
169
199
 
170
200
  // ─── /__studio/ai/chat — SSE streaming proxy ───────────────────────────────