@elytracms/studio 0.0.55 → 0.0.57

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 (55) hide show
  1. package/convex/auth.ts +152 -2
  2. package/convex/users.ts +41 -14
  3. package/dist/components/ai/ai-chat-panel-lazy.js +3 -3
  4. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  5. package/dist/components/ai/ai-chat-panel.d.ts +0 -9
  6. package/dist/components/ai/ai-chat-panel.js +161 -60
  7. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  8. package/dist/components/ai/chat-ui.d.ts +62 -10
  9. package/dist/components/ai/chat-ui.js +130 -23
  10. package/dist/components/ai/chat-ui.js.map +1 -1
  11. package/dist/lib/ai/anthropic-provider.d.ts +0 -12
  12. package/dist/lib/ai/anthropic-provider.js +3 -11
  13. package/dist/lib/ai/anthropic-provider.js.map +1 -1
  14. package/dist/lib/ai/chat-context.d.ts +58 -0
  15. package/dist/lib/ai/chat-context.js +71 -0
  16. package/dist/lib/ai/chat-context.js.map +1 -0
  17. package/dist/lib/ai/chat-loop.d.ts +70 -0
  18. package/dist/lib/ai/chat-loop.js +46 -0
  19. package/dist/lib/ai/chat-loop.js.map +1 -0
  20. package/dist/lib/ai/chat-state.d.ts +70 -57
  21. package/dist/lib/ai/chat-state.js +49 -90
  22. package/dist/lib/ai/chat-state.js.map +1 -1
  23. package/dist/lib/ai/fake-provider.d.ts +84 -6
  24. package/dist/lib/ai/fake-provider.js +193 -8
  25. package/dist/lib/ai/fake-provider.js.map +1 -1
  26. package/dist/lib/ai/gateway-provider.d.ts +30 -0
  27. package/dist/lib/ai/gateway-provider.js +73 -0
  28. package/dist/lib/ai/gateway-provider.js.map +1 -0
  29. package/dist/lib/ai/index.d.ts +3 -1
  30. package/dist/lib/ai/index.js +3 -1
  31. package/dist/lib/ai/index.js.map +1 -1
  32. package/dist/lib/ai/node-handler.d.ts +13 -4
  33. package/dist/lib/ai/node-handler.js +33 -3
  34. package/dist/lib/ai/node-handler.js.map +1 -1
  35. package/dist/lib/ai/route-provider.d.ts +35 -10
  36. package/dist/lib/ai/route-provider.js +13 -6
  37. package/dist/lib/ai/route-provider.js.map +1 -1
  38. package/dist/lib/ai/route.d.ts +50 -21
  39. package/dist/lib/ai/route.js +69 -40
  40. package/dist/lib/ai/route.js.map +1 -1
  41. package/dist/lib/ai/tools.d.ts +169 -39
  42. package/dist/lib/ai/tools.js +308 -62
  43. package/dist/lib/ai/tools.js.map +1 -1
  44. package/dist/lib/ai/types.d.ts +11 -16
  45. package/dist/lib/ai/types.js.map +1 -1
  46. package/dist/lib/auth/convex-auth-adapter.d.ts +37 -14
  47. package/dist/lib/auth/convex-auth-adapter.js +72 -16
  48. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  49. package/dist/lib/i18n/messages.en.js +9 -0
  50. package/dist/lib/i18n/messages.en.js.map +1 -1
  51. package/dist/ui/badge.d.ts +1 -1
  52. package/package.json +4 -3
  53. package/dist/lib/ai/create-flow.d.ts +0 -132
  54. package/dist/lib/ai/create-flow.js +0 -280
  55. package/dist/lib/ai/create-flow.js.map +0 -1
@@ -33,8 +33,11 @@ async function toWebRequest(req) {
33
33
  });
34
34
  }
35
35
  /**
36
- * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the body,
37
- * bridge headers both directions, copy status + payload back.
36
+ * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the request
37
+ * body, bridge headers both directions, copy the status back, and STREAM the
38
+ * response body — each chunk is written as it arrives (EC-334), so SSE token
39
+ * streaming survives the bridge. A client disconnect cancels the reader so the
40
+ * handler's stream stops being pulled.
38
41
  */
39
42
  export function toNodeHandler(handler) {
40
43
  return async (req, res) => {
@@ -42,7 +45,34 @@ export function toNodeHandler(handler) {
42
45
  const response = await handler(request);
43
46
  res.statusCode = response.status;
44
47
  response.headers.forEach((value, key) => res.setHeader(key, value));
45
- res.end(new Uint8Array(await response.arrayBuffer()));
48
+ const body = response.body;
49
+ if (!body) {
50
+ res.end();
51
+ return;
52
+ }
53
+ const reader = body.getReader();
54
+ let finished = false;
55
+ res.on('close', () => {
56
+ // Client gone before the stream ended — stop pulling from the handler.
57
+ if (!finished)
58
+ void reader.cancel().catch(() => undefined);
59
+ });
60
+ try {
61
+ for (;;) {
62
+ const { done, value } = await reader.read();
63
+ if (done)
64
+ break;
65
+ if (value !== undefined)
66
+ res.write(value);
67
+ }
68
+ }
69
+ catch {
70
+ // Stream error mid-flight (or cancelled by disconnect): terminate below.
71
+ }
72
+ finally {
73
+ finished = true;
74
+ res.end();
75
+ }
46
76
  };
47
77
  }
48
78
  //# sourceMappingURL=node-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-handler.js","sourceRoot":"","sources":["../../../src/lib/ai/node-handler.ts"],"names":[],"mappings":"AA0BA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,SAAS,WAAW,CAAC,MAAoB;IACvC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,IAAI,KAAK,CAAC,UAAU,CAAA;IACrD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACtB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAA;IAC5B,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,wFAAwF;AACxF,KAAK,UAAU,YAAY,CAAC,GAAoB;IAC9C,MAAM,MAAM,GAAiB,EAAE,CAAA;IAC/B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAoB,CAAC,CAAA;IACxF,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;aACjD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvD,MAAM,KAAK,GACT,CAAC,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,MAAM,CAAA;IACxF,OAAO,IAAI,OAAO,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,EAAE;QACnF,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO;QACP,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC;KACtF,CAAC,CAAA;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAuB;IACnD,OAAO,KAAK,EAAE,GAAoB,EAAE,GAAmB,EAAiB,EAAE;QACxE,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACvC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;QACvC,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAA;QAChC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;QACnE,GAAG,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IACvD,CAAC,CAAA;AACH,CAAC","sourcesContent":["/**\n * `toNodeHandler` — bridge the framework-agnostic Web `Request → Response`\n * {@link AiRouteHandler} to Node's `(req, res)` signature (EC-371; closes EC-369\n * finding #2, where the identical ~25-line bridge was hand-copied four times across\n * consumers). This is the PROD half of the mounting kit: a Vercel Node function (or\n * any Node server) mounts the route as\n *\n * ```ts\n * // api/ai.ts (Vercel filesystem routing beats the SPA catch-all rewrite)\n * import { toNodeHandler } from '@elytracms/studio/lib/ai'\n * import { aiRouteHandler } from '../src/server/ai-handler.js'\n * export default toNodeHandler(aiRouteHandler)\n * ```\n *\n * The DEV half (`createAiDevPlugin`, Vite middleware + `ssrLoadModule`) reuses this\n * same bridge — one implementation, both mounts. No platform dependency: the classic\n * `(IncomingMessage, ServerResponse)` signature works on every @vercel/node version\n * and on plain `node:http`. Note it buffers the response body (including the chat\n * stream) — the proven Konditorei behavior; native streaming is EC-334's scope.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { AiRouteHandler } from './types'\n\n/** A Node-mountable request handler (Vercel Node function / `node:http` compatible). */\nexport type NodeAiRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>\n\nconst encoder = new TextEncoder()\n\nfunction concatBytes(chunks: Uint8Array[]): Uint8Array<ArrayBuffer> {\n let total = 0\n for (const chunk of chunks) total += chunk.byteLength\n const out = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n out.set(chunk, offset)\n offset += chunk.byteLength\n }\n return out\n}\n\n/** Reconstruct a Web `Request` from a Node request (headers, method, buffered body). */\nasync function toWebRequest(req: IncomingMessage): Promise<Request> {\n const chunks: Uint8Array[] = []\n for await (const chunk of req) {\n chunks.push(typeof chunk === 'string' ? encoder.encode(chunk) : (chunk as Uint8Array))\n }\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(key, value)\n else if (Array.isArray(value)) headers.set(key, value.join(', '))\n }\n const forwardedProto = req.headers['x-forwarded-proto']\n const proto =\n (typeof forwardedProto === 'string' && forwardedProto.split(',')[0]?.trim()) || 'http'\n return new Request(`${proto}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {\n method: req.method,\n headers,\n body: req.method === 'GET' || req.method === 'HEAD' ? undefined : concatBytes(chunks),\n })\n}\n\n/**\n * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the body,\n * bridge headers both directions, copy status + payload back.\n */\nexport function toNodeHandler(handler: AiRouteHandler): NodeAiRouteHandler {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const request = await toWebRequest(req)\n const response = await handler(request)\n res.statusCode = response.status\n response.headers.forEach((value, key) => res.setHeader(key, value))\n res.end(new Uint8Array(await response.arrayBuffer()))\n }\n}\n"]}
1
+ {"version":3,"file":"node-handler.js","sourceRoot":"","sources":["../../../src/lib/ai/node-handler.ts"],"names":[],"mappings":"AAgCA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,SAAS,WAAW,CAAC,MAAoB;IACvC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,IAAI,KAAK,CAAC,UAAU,CAAA;IACrD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACtB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAA;IAC5B,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,wFAAwF;AACxF,KAAK,UAAU,YAAY,CAAC,GAAoB;IAC9C,MAAM,MAAM,GAAiB,EAAE,CAAA;IAC/B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAoB,CAAC,CAAA;IACxF,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;aACjD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvD,MAAM,KAAK,GACT,CAAC,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,MAAM,CAAA;IACxF,OAAO,IAAI,OAAO,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,EAAE;QACnF,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO;QACP,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC;KACtF,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,OAAuB;IACnD,OAAO,KAAK,EAAE,GAAoB,EAAE,GAAmB,EAAiB,EAAE;QACxE,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACvC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;QACvC,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAA;QAChC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;QAEnE,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAA;QAC1B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,GAAG,CAAC,GAAG,EAAE,CAAA;YACT,OAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAA;QAC/B,IAAI,QAAQ,GAAG,KAAK,CAAA;QACpB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACnB,uEAAuE;YACvE,IAAI,CAAC,QAAQ;gBAAE,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QAC5D,CAAC,CAAC,CAAA;QACF,IAAI,CAAC;YACH,SAAS,CAAC;gBACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;gBAC3C,IAAI,IAAI;oBAAE,MAAK;gBACf,IAAI,KAAK,KAAK,SAAS;oBAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAC3C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;QAC3E,CAAC;gBAAS,CAAC;YACT,QAAQ,GAAG,IAAI,CAAA;YACf,GAAG,CAAC,GAAG,EAAE,CAAA;QACX,CAAC;IACH,CAAC,CAAA;AACH,CAAC","sourcesContent":["/**\n * `toNodeHandler` — bridge the framework-agnostic Web `Request → Response`\n * {@link AiRouteHandler} to Node's `(req, res)` signature (EC-371; closes EC-369\n * finding #2, where the identical ~25-line bridge was hand-copied four times across\n * consumers). This is the PROD half of the mounting kit: a Vercel Node function (or\n * any Node server) mounts the route as\n *\n * ```ts\n * // api/ai.ts (Vercel filesystem routing beats the SPA catch-all rewrite)\n * import { toNodeHandler } from '@elytracms/studio/lib/ai'\n * import { aiRouteHandler } from '../src/server/ai-handler.js'\n * export default toNodeHandler(aiRouteHandler)\n * ```\n *\n * The DEV half (`createAiDevPlugin`, Vite middleware + `ssrLoadModule`) reuses this\n * same bridge — one implementation, both mounts. No platform dependency: the classic\n * `(IncomingMessage, ServerResponse)` signature works on every @vercel/node version\n * and on plain `node:http`.\n *\n * EC-334: the RESPONSE side STREAMS — `response.body` reader chunks are piped to\n * `res.write` as they arrive, so the route's native SSE chat protocol delivers\n * token-by-token through every mount (the previous `arrayBuffer()` buffering would\n * have erased token streaming). The REQUEST side stays buffered (the route only\n * ever reads JSON bodies). Ships to every consumer mount — streaming through\n * Vercel Node functions goes on the post-release live-verify checklist.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { AiRouteHandler } from './types'\n\n/** A Node-mountable request handler (Vercel Node function / `node:http` compatible). */\nexport type NodeAiRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>\n\nconst encoder = new TextEncoder()\n\nfunction concatBytes(chunks: Uint8Array[]): Uint8Array<ArrayBuffer> {\n let total = 0\n for (const chunk of chunks) total += chunk.byteLength\n const out = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n out.set(chunk, offset)\n offset += chunk.byteLength\n }\n return out\n}\n\n/** Reconstruct a Web `Request` from a Node request (headers, method, buffered body). */\nasync function toWebRequest(req: IncomingMessage): Promise<Request> {\n const chunks: Uint8Array[] = []\n for await (const chunk of req) {\n chunks.push(typeof chunk === 'string' ? encoder.encode(chunk) : (chunk as Uint8Array))\n }\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(key, value)\n else if (Array.isArray(value)) headers.set(key, value.join(', '))\n }\n const forwardedProto = req.headers['x-forwarded-proto']\n const proto =\n (typeof forwardedProto === 'string' && forwardedProto.split(',')[0]?.trim()) || 'http'\n return new Request(`${proto}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {\n method: req.method,\n headers,\n body: req.method === 'GET' || req.method === 'HEAD' ? undefined : concatBytes(chunks),\n })\n}\n\n/**\n * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the request\n * body, bridge headers both directions, copy the status back, and STREAM the\n * response body — each chunk is written as it arrives (EC-334), so SSE token\n * streaming survives the bridge. A client disconnect cancels the reader so the\n * handler's stream stops being pulled.\n */\nexport function toNodeHandler(handler: AiRouteHandler): NodeAiRouteHandler {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const request = await toWebRequest(req)\n const response = await handler(request)\n res.statusCode = response.status\n response.headers.forEach((value, key) => res.setHeader(key, value))\n\n const body = response.body\n if (!body) {\n res.end()\n return\n }\n\n const reader = body.getReader()\n let finished = false\n res.on('close', () => {\n // Client gone before the stream ended — stop pulling from the handler.\n if (!finished) void reader.cancel().catch(() => undefined)\n })\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value !== undefined) res.write(value)\n }\n } catch {\n // Stream error mid-flight (or cancelled by disconnect): terminate below.\n } finally {\n finished = true\n res.end()\n }\n }\n}\n"]}
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the
3
- * browser (EC-199). It is a thin proxy that forwards `generateStructured` /
4
- * `streamChat` to the host's mounted {@link createAiRouteHandler} endpoint over
5
- * `fetch`. The provider KEY never touches the browser: the key-holding
6
- * {@link createAnthropicProvider} lives only inside the server route; the studio's
7
- * `defineStudio({ ai: { provider } })` gets this route provider (or the FakeProvider
8
- * for a keyless playground), so the whole authoring loop —
9
- * `applyContentWithShape` included — proxies its model calls through the session-gated
10
- * route while the tools (search / list / apply) execute client-side with the user's
11
- * own session.
3
+ * browser (EC-199). It is a thin proxy that forwards `generateStructured` to the
4
+ * host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider
5
+ * KEY never touches the browser: the key-holding {@link createAnthropicProvider}
6
+ * lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`
7
+ * gets this route provider (or the FakeProvider for a keyless playground), so the
8
+ * whole authoring loop — `applyContentWithShape` included — proxies its model calls
9
+ * through the session-gated route while the tools (search / list / apply) execute
10
+ * client-side with the user's own session. It exposes NO `languageModel()` — the
11
+ * conversational loop talks to the route's native chat branch directly (EC-334,
12
+ * `useChat` + `DefaultChatTransport`), not through this provider; the provider
13
+ * only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are
14
+ * configured once.
12
15
  *
13
16
  * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`
14
17
  * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test
@@ -36,6 +39,28 @@ export interface RouteProviderOptions {
36
39
  }
37
40
  /** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */
38
41
  export declare const DEFAULT_AI_ENDPOINT = "/api/ai";
42
+ /**
43
+ * The chat transport target (EC-334): where the panel's `useChat` posts its
44
+ * native `{ messages, context }` requests, and which extra headers ride along
45
+ * (the session token thunk). `createRouteProvider` exposes its own endpoint +
46
+ * headers here, so the ONE configured provider is the single source of truth
47
+ * for both the structured proxy and the chat transport.
48
+ */
49
+ export interface AiChatTarget {
50
+ endpoint: string;
51
+ headers?: RouteHeaders;
52
+ }
53
+ /** An {@link AiProvider} that also names the chat transport target it fronts. */
54
+ export interface RouteAiProvider extends AiProvider {
55
+ readonly chatTarget: AiChatTarget;
56
+ }
57
+ /**
58
+ * The chat target behind a configured provider: the route provider's own
59
+ * endpoint + headers, or the default mount path for any other provider (the
60
+ * chat loop always talks to the mounted route — a missing mount fails loud as
61
+ * a visible route error in the panel, never silently).
62
+ */
63
+ export declare function resolveChatTarget(provider: AiProvider | null): AiChatTarget;
39
64
  /** A structured error thrown when the route rejects (surfaced by the chat UI). */
40
65
  export declare class AiRouteError extends Error {
41
66
  readonly status: number;
@@ -47,4 +72,4 @@ export declare class AiRouteError extends Error {
47
72
  * (so the FakeProvider mounted behind the route drives the whole client loop in the
48
73
  * playground). Never holds a key; never imports the AI SDK.
49
74
  */
50
- export declare function createRouteProvider(options?: RouteProviderOptions): AiProvider;
75
+ export declare function createRouteProvider(options?: RouteProviderOptions): RouteAiProvider;
@@ -1,5 +1,17 @@
1
1
  /** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */
2
2
  export const DEFAULT_AI_ENDPOINT = '/api/ai';
3
+ /**
4
+ * The chat target behind a configured provider: the route provider's own
5
+ * endpoint + headers, or the default mount path for any other provider (the
6
+ * chat loop always talks to the mounted route — a missing mount fails loud as
7
+ * a visible route error in the panel, never silently).
8
+ */
9
+ export function resolveChatTarget(provider) {
10
+ const candidate = provider?.chatTarget;
11
+ if (candidate && typeof candidate.endpoint === 'string')
12
+ return candidate;
13
+ return { endpoint: DEFAULT_AI_ENDPOINT };
14
+ }
3
15
  async function resolveHeaders(headers) {
4
16
  if (typeof headers === 'function')
5
17
  return headers();
@@ -42,6 +54,7 @@ export function createRouteProvider(options = {}) {
42
54
  return {
43
55
  id: options.id ?? 'route',
44
56
  ...(options.model !== undefined ? { model: options.model } : {}),
57
+ chatTarget: { endpoint, ...(options.headers !== undefined ? { headers: options.headers } : {}) },
45
58
  capabilities: () => ({ toolCalling: true, structuredOutput: true }),
46
59
  async generateStructured(request) {
47
60
  const response = await post({ kind: 'generate', request });
@@ -49,12 +62,6 @@ export function createRouteProvider(options = {}) {
49
62
  throw new AiRouteError(response.status, await errorCode(response));
50
63
  return (await response.json());
51
64
  },
52
- async streamChat(request) {
53
- const response = await post({ kind: 'chat', ...request });
54
- if (!response.ok)
55
- throw new AiRouteError(response.status, await errorCode(response));
56
- return response;
57
- },
58
65
  };
59
66
  }
60
67
  //# sourceMappingURL=route-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/route-provider.ts"],"names":[],"mappings":"AA+CA,0FAA0F;AAC1F,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAE5C,KAAK,UAAU,cAAc,CAAC,OAAiC;IAC7D,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,OAAO,OAAO,EAAE,CAAA;IACnD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,kFAAkF;AAClF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAE1B;IACA;IAFX,YACW,MAAc,EACd,IAAY;QAErB,KAAK,CAAC,4BAA4B,MAAM,KAAK,IAAI,GAAG,CAAC,CAAA;QAH5C,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAGrB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;IAC5B,CAAC;CACF;AAED,KAAK,UAAU,SAAS,CAAC,QAAkB;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAwB,CAAA;QAC3D,OAAO,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAA;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,gBAAgB,CAAA;IACzB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAgC,EAAE;IACpE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,mBAAmB,CAAA;IACxD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAA;IAE5C,KAAK,UAAU,IAAI,CAAC,IAAa;QAC/B,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACnD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,CAAA;QAClC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;QAC/C,OAAO,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACrF,CAAC;IAED,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,OAAO;QACzB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,YAAY,EAAE,GAA2B,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC;QAE3F,KAAK,CAAC,kBAAkB,CACtB,OAAkC;YAElC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAA;YAC1D,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpF,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAA;QAC5D,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,OAA0B;YACzC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;YACzD,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpF,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the\n * browser (EC-199). It is a thin proxy that forwards `generateStructured` /\n * `streamChat` to the host's mounted {@link createAiRouteHandler} endpoint over\n * `fetch`. The provider KEY never touches the browser: the key-holding\n * {@link createAnthropicProvider} lives only inside the server route; the studio's\n * `defineStudio({ ai: { provider } })` gets this route provider (or the FakeProvider\n * for a keyless playground), so the whole authoring loop —\n * `applyContentWithShape` included — proxies its model calls through the session-gated\n * route while the tools (search / list / apply) execute client-side with the user's\n * own session.\n *\n * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`\n * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test\n * pins — the route provider can sit on the studio critical path without pulling the\n * SDK. The capability gate passes: the route only ever fronts a gate-checked provider\n * (validated at the server mount), so the proxy declares both required capabilities.\n */\nimport type {\n AiProvider,\n AiProviderCapabilities,\n GenerateStructuredRequest,\n GenerateStructuredResult,\n StreamChatRequest,\n} from './types'\n\n/** How extra request headers are supplied — a static set or a (possibly async) thunk. */\nexport type RouteHeaders =\n | HeadersInit\n | (() => HeadersInit | undefined | Promise<HeadersInit | undefined>)\n\nexport interface RouteProviderOptions {\n /** The mounted route path/URL (default `/api/ai`). */\n endpoint?: string\n /** `fetch` implementation (default the global). Injected in tests. */\n fetchImpl?: typeof fetch\n /**\n * Extra headers per request — e.g. the studio session token the route's `getSession`\n * checks. A thunk is re-evaluated on every call so a refreshed token is picked up.\n */\n headers?: RouteHeaders\n /** Provider id recorded in provenance (default `route`). */\n id?: string\n /** Model id recorded in provenance (the downstream model, when the host fixes one). */\n model?: string\n}\n\n/** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */\nexport const DEFAULT_AI_ENDPOINT = '/api/ai'\n\nasync function resolveHeaders(headers: RouteHeaders | undefined): Promise<HeadersInit | undefined> {\n if (typeof headers === 'function') return headers()\n return headers\n}\n\n/** A structured error thrown when the route rejects (surfaced by the chat UI). */\nexport class AiRouteError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n ) {\n super(`AI route request failed (${status}: ${code})`)\n this.name = 'AiRouteError'\n }\n}\n\nasync function errorCode(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown }\n return typeof body.error === 'string' ? body.error : 'request-failed'\n } catch {\n return 'request-failed'\n }\n}\n\n/**\n * Build a route-backed {@link AiProvider}. Deterministic given a deterministic route\n * (so the FakeProvider mounted behind the route drives the whole client loop in the\n * playground). Never holds a key; never imports the AI SDK.\n */\nexport function createRouteProvider(options: RouteProviderOptions = {}): AiProvider {\n const endpoint = options.endpoint ?? DEFAULT_AI_ENDPOINT\n const fetchImpl = options.fetchImpl ?? fetch\n\n async function post(body: unknown): Promise<Response> {\n const extra = await resolveHeaders(options.headers)\n const headers = new Headers(extra)\n headers.set('content-type', 'application/json')\n return fetchImpl(endpoint, { method: 'POST', headers, body: JSON.stringify(body) })\n }\n\n return {\n id: options.id ?? 'route',\n ...(options.model !== undefined ? { model: options.model } : {}),\n capabilities: (): AiProviderCapabilities => ({ toolCalling: true, structuredOutput: true }),\n\n async generateStructured(\n request: GenerateStructuredRequest,\n ): Promise<GenerateStructuredResult> {\n const response = await post({ kind: 'generate', request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return (await response.json()) as GenerateStructuredResult\n },\n\n async streamChat(request: StreamChatRequest): Promise<Response> {\n const response = await post({ kind: 'chat', ...request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return response\n },\n }\n}\n"]}
1
+ {"version":3,"file":"route-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/route-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the\n * browser (EC-199). It is a thin proxy that forwards `generateStructured` to the\n * host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider\n * KEY never touches the browser: the key-holding {@link createAnthropicProvider}\n * lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`\n * gets this route provider (or the FakeProvider for a keyless playground), so the\n * whole authoring loop — `applyContentWithShape` included — proxies its model calls\n * through the session-gated route while the tools (search / list / apply) execute\n * client-side with the user's own session. It exposes NO `languageModel()` — the\n * conversational loop talks to the route's native chat branch directly (EC-334,\n * `useChat` + `DefaultChatTransport`), not through this provider; the provider\n * only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are\n * configured once.\n *\n * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`\n * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test\n * pins — the route provider can sit on the studio critical path without pulling the\n * SDK. The capability gate passes: the route only ever fronts a gate-checked provider\n * (validated at the server mount), so the proxy declares both required capabilities.\n */\nimport type {\n AiProvider,\n AiProviderCapabilities,\n GenerateStructuredRequest,\n GenerateStructuredResult,\n} from './types'\n\n/** How extra request headers are supplied — a static set or a (possibly async) thunk. */\nexport type RouteHeaders =\n | HeadersInit\n | (() => HeadersInit | undefined | Promise<HeadersInit | undefined>)\n\nexport interface RouteProviderOptions {\n /** The mounted route path/URL (default `/api/ai`). */\n endpoint?: string\n /** `fetch` implementation (default the global). Injected in tests. */\n fetchImpl?: typeof fetch\n /**\n * Extra headers per request — e.g. the studio session token the route's `getSession`\n * checks. A thunk is re-evaluated on every call so a refreshed token is picked up.\n */\n headers?: RouteHeaders\n /** Provider id recorded in provenance (default `route`). */\n id?: string\n /** Model id recorded in provenance (the downstream model, when the host fixes one). */\n model?: string\n}\n\n/** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */\nexport const DEFAULT_AI_ENDPOINT = '/api/ai'\n\n/**\n * The chat transport target (EC-334): where the panel's `useChat` posts its\n * native `{ messages, context }` requests, and which extra headers ride along\n * (the session token thunk). `createRouteProvider` exposes its own endpoint +\n * headers here, so the ONE configured provider is the single source of truth\n * for both the structured proxy and the chat transport.\n */\nexport interface AiChatTarget {\n endpoint: string\n headers?: RouteHeaders\n}\n\n/** An {@link AiProvider} that also names the chat transport target it fronts. */\nexport interface RouteAiProvider extends AiProvider {\n readonly chatTarget: AiChatTarget\n}\n\n/**\n * The chat target behind a configured provider: the route provider's own\n * endpoint + headers, or the default mount path for any other provider (the\n * chat loop always talks to the mounted route — a missing mount fails loud as\n * a visible route error in the panel, never silently).\n */\nexport function resolveChatTarget(provider: AiProvider | null): AiChatTarget {\n const candidate = (provider as Partial<RouteAiProvider> | null)?.chatTarget\n if (candidate && typeof candidate.endpoint === 'string') return candidate\n return { endpoint: DEFAULT_AI_ENDPOINT }\n}\n\nasync function resolveHeaders(headers: RouteHeaders | undefined): Promise<HeadersInit | undefined> {\n if (typeof headers === 'function') return headers()\n return headers\n}\n\n/** A structured error thrown when the route rejects (surfaced by the chat UI). */\nexport class AiRouteError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n ) {\n super(`AI route request failed (${status}: ${code})`)\n this.name = 'AiRouteError'\n }\n}\n\nasync function errorCode(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown }\n return typeof body.error === 'string' ? body.error : 'request-failed'\n } catch {\n return 'request-failed'\n }\n}\n\n/**\n * Build a route-backed {@link AiProvider}. Deterministic given a deterministic route\n * (so the FakeProvider mounted behind the route drives the whole client loop in the\n * playground). Never holds a key; never imports the AI SDK.\n */\nexport function createRouteProvider(options: RouteProviderOptions = {}): RouteAiProvider {\n const endpoint = options.endpoint ?? DEFAULT_AI_ENDPOINT\n const fetchImpl = options.fetchImpl ?? fetch\n\n async function post(body: unknown): Promise<Response> {\n const extra = await resolveHeaders(options.headers)\n const headers = new Headers(extra)\n headers.set('content-type', 'application/json')\n return fetchImpl(endpoint, { method: 'POST', headers, body: JSON.stringify(body) })\n }\n\n return {\n id: options.id ?? 'route',\n ...(options.model !== undefined ? { model: options.model } : {}),\n chatTarget: { endpoint, ...(options.headers !== undefined ? { headers: options.headers } : {}) },\n capabilities: (): AiProviderCapabilities => ({ toolCalling: true, structuredOutput: true }),\n\n async generateStructured(\n request: GenerateStructuredRequest,\n ): Promise<GenerateStructuredResult> {\n const response = await post({ kind: 'generate', request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return (await response.json()) as GenerateStructuredResult\n },\n }\n}\n"]}
@@ -1,36 +1,65 @@
1
1
  /**
2
- * Server-route factory (EC-331): a streaming chat/completion endpoint the consuming
3
- * studio app mounts. Key custody = host route (never the browser, never Convex): the
2
+ * Server-route factory (EC-331): the session-gated endpoint the consuming studio
3
+ * app mounts. Key custody = host route (never the browser, never Convex): the
4
4
  * `provider` holds the host-env key and this handler is a THIN model proxy — NO data
5
- * access, NO service credentials.
5
+ * access, NO service credentials, NO server-side tool execution.
6
6
  *
7
7
  * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the
8
- * host wires into whatever server entry it has (a TanStack Start server route, a
9
- * Convex `httpAction`, a plain fetch handler). The current demo studio builds as a
10
- * pure client SPA with server functions disabled (EC-234), so actually mounting this
11
- * is an EC-199 integration step; phase 1 ships the handler + its contract.
8
+ * host wires into whatever server entry it has (`createAiDevPlugin` in dev,
9
+ * `toNodeHandler` on a Vercel Node function, any fetch handler).
12
10
  *
13
11
  * Session-gated: `getSession` (host-wired, reading its own auth — this module never
14
12
  * touches `lib/auth`) must return a session for authenticated callers; anonymous
15
- * callers are rejected. Plus a soft, in-memory per-project rate counter (spend guard).
13
+ * callers are rejected with 401 BEFORE any stream byte. Plus a soft, in-memory
14
+ * per-project rate counter (spend guard) — also the backstop for the SDK's
15
+ * client-driven tool-call continuations, each of which is a fresh request here.
16
16
  *
17
- * EC-199: the route serves TWO model operations — both thin proxies, neither with any
18
- * data access — discriminated by the request body's `kind`:
19
- * - `{ kind: 'chat', messages, system? }` → `provider.streamChat` (text stream);
20
- * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON).
21
- * The structured mode is what lets `applyContentWithShape` run CLIENT-side against a
22
- * {@link createRouteProvider} whose key never touches the browser — the creation flow's
23
- * whole authoring loop proxies its model calls here while the tools (search/list/apply)
24
- * execute in the client with the user's own session. A body with `messages` and no
25
- * `kind` is still treated as a chat request (back-compat with the phase-1 contract).
17
+ * The route serves TWO model operations — both thin proxies, neither with any data
18
+ * access:
19
+ * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON) — the
20
+ * proxy `applyContentWithShape` / `translateDocument` run CLIENT-side against,
21
+ * so the key never touches the browser;
22
+ * - `{ messages: UIMessage[], context? }` (EC-334, kind-less — the shape the AI
23
+ * SDK's `DefaultChatTransport` posts) → the SDK's native streaming chat
24
+ * protocol: `validateUIMessages` → `streamText` over
25
+ * `provider.languageModel()` with {@link chatToolDefinitions} declared WITHOUT
26
+ * `execute` (every tool call forwards to the client, which executes it against
27
+ * the operations layer with the user's own session) →
28
+ * `createUIMessageStreamResponse` + `toUIMessageStream` (the non-deprecated
29
+ * pair — `result.toUIMessageStreamResponse()` dies in SDK v8). The `system`
30
+ * prompt is assembled from the request's `context` sections; conversation
31
+ * history rides natively in `messages`.
32
+ *
33
+ * BREAKING (EC-334, ratified): the legacy `{ kind: 'chat', messages:
34
+ * AiChatMessage[] }` wire is gone — route factory and client ship lockstep in
35
+ * `@elytracms/studio`, so consumers re-mount the same factory on upgrade.
26
36
  */
27
37
  import type { AiRouteHandler, AiRouteHandlerOptions } from './types.js';
28
- /** The wire discriminator selecting which model operation the route proxies. */
29
- export type AiRouteKind = 'chat' | 'generate';
38
+ /**
39
+ * The wire discriminator for the structured-generation branch. The native chat
40
+ * branch (EC-334) is deliberately kind-less: the SDK's `DefaultChatTransport`
41
+ * posts `{ messages, … }` with no `kind`.
42
+ */
43
+ export type AiRouteKind = 'generate';
44
+ /** The outcome of folding the request `context` into a system prompt. */
45
+ export type AssembledSystem = {
46
+ ok: true;
47
+ system?: string;
48
+ } | {
49
+ ok: false;
50
+ };
51
+ /**
52
+ * Fold the request-body `context` into the system prompt (EC-334). The client sends
53
+ * `context` as one string or ordered string sections (collections-catalog summary,
54
+ * client-resolved knowledge, EC-378's current-document block) — the route only
55
+ * ASSEMBLES them; it resolves no data itself (zero-data-access proxy). Malformed
56
+ * context is a structural 400, never silently dropped (validation-first).
57
+ */
58
+ export declare function assembleSystemPrompt(context: unknown): AssembledSystem;
30
59
  /**
31
60
  * Build the mounted route handler. The returned function is a plain
32
61
  * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin
33
- * proxy to `provider.streamChat`. Never throws to the caller: every failure is a
34
- * structured JSON response with an appropriate status.
62
+ * proxy to the provider. Never throws to the caller: every failure is a structured
63
+ * JSON response with an appropriate status.
35
64
  */
36
65
  export declare function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler;
@@ -1,29 +1,10 @@
1
+ import { chatToolDefinitions } from './tools.js';
1
2
  function jsonResponse(value, status) {
2
3
  return new Response(JSON.stringify(value), {
3
4
  status,
4
5
  headers: { 'content-type': 'application/json' },
5
6
  });
6
7
  }
7
- function isChatMessage(value) {
8
- if (typeof value !== 'object' || value === null)
9
- return false;
10
- const m = value;
11
- return ((m.role === 'system' || m.role === 'user' || m.role === 'assistant') &&
12
- typeof m.content === 'string');
13
- }
14
- /** Parse a request body into a {@link StreamChatRequest}, or `null` when malformed. */
15
- function parseChatBody(body) {
16
- if (typeof body !== 'object' || body === null)
17
- return null;
18
- const { messages, system } = body;
19
- if (!Array.isArray(messages) || messages.length === 0 || !messages.every(isChatMessage)) {
20
- return null;
21
- }
22
- return {
23
- messages: messages,
24
- ...(typeof system === 'string' ? { system } : {}),
25
- };
26
- }
27
8
  /** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */
28
9
  function parseGenerateBody(body) {
29
10
  if (typeof body !== 'object' || body === null)
@@ -36,22 +17,34 @@ function parseGenerateBody(body) {
36
17
  return null;
37
18
  return request;
38
19
  }
39
- /** The `kind` a body selects: explicit `kind`, else `messages` ⇒ chat (back-compat). */
40
- function bodyKind(body) {
41
- if (typeof body !== 'object' || body === null)
42
- return null;
43
- const kind = body.kind;
44
- if (kind === 'chat' || kind === 'generate')
45
- return kind;
46
- if (Array.isArray(body.messages))
47
- return 'chat';
48
- return null;
20
+ /**
21
+ * Fold the request-body `context` into the system prompt (EC-334). The client sends
22
+ * `context` as one string or ordered string sections (collections-catalog summary,
23
+ * client-resolved knowledge, EC-378's current-document block) — the route only
24
+ * ASSEMBLES them; it resolves no data itself (zero-data-access proxy). Malformed
25
+ * context is a structural 400, never silently dropped (validation-first).
26
+ */
27
+ export function assembleSystemPrompt(context) {
28
+ if (context === undefined || context === null)
29
+ return { ok: true };
30
+ const sections = typeof context === 'string'
31
+ ? [context]
32
+ : Array.isArray(context) && context.every((section) => typeof section === 'string')
33
+ ? context
34
+ : null;
35
+ if (sections === null)
36
+ return { ok: false };
37
+ const system = sections
38
+ .map((section) => section.trim())
39
+ .filter((section) => section.length > 0)
40
+ .join('\n\n');
41
+ return { ok: true, ...(system.length > 0 ? { system } : {}) };
49
42
  }
50
43
  /**
51
44
  * Build the mounted route handler. The returned function is a plain
52
45
  * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin
53
- * proxy to `provider.streamChat`. Never throws to the caller: every failure is a
54
- * structured JSON response with an appropriate status.
46
+ * proxy to the provider. Never throws to the caller: every failure is a structured
47
+ * JSON response with an appropriate status.
55
48
  */
56
49
  export function createAiRouteHandler(options) {
57
50
  const maxPerMinute = options.limits?.maxRequestsPerMinute;
@@ -71,6 +64,7 @@ export function createAiRouteHandler(options) {
71
64
  }
72
65
  return async (request) => {
73
66
  // 1. Session gate — reject anonymous callers (the route holds spend, not data).
67
+ // Position is FROZEN: the 401 must precede any stream byte (EC-371/EC-334).
74
68
  let session;
75
69
  try {
76
70
  session = await options.getSession(request);
@@ -94,10 +88,12 @@ export function createAiRouteHandler(options) {
94
88
  catch {
95
89
  return jsonResponse({ error: 'invalid-json' }, 400);
96
90
  }
97
- const kind = bodyKind(body);
91
+ if (typeof body !== 'object' || body === null) {
92
+ return jsonResponse({ error: 'invalid-json' }, 400);
93
+ }
98
94
  // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`
99
95
  // run client-side without the key ever reaching the browser.
100
- if (kind === 'generate') {
96
+ if (body.kind === 'generate') {
101
97
  const generate = parseGenerateBody(body);
102
98
  if (!generate) {
103
99
  return jsonResponse({ error: 'invalid-request', message: 'expected { kind: "generate", request }' }, 400);
@@ -110,16 +106,49 @@ export function createAiRouteHandler(options) {
110
106
  return jsonResponse({ error: 'provider-error' }, 502);
111
107
  }
112
108
  }
113
- // 5b. Chat stream (default / back-compat).
114
- const chat = kind === 'chat' ? parseChatBody(body) : null;
115
- if (!chat) {
116
- return jsonResponse({ error: 'invalid-request', message: 'expected { messages: AiChatMessage[] }' }, 400);
109
+ // 5b. Native chat branch (EC-334): `{ messages: UIMessage[], context? }`.
110
+ const rawMessages = body.messages;
111
+ if (!Array.isArray(rawMessages) || rawMessages.length === 0) {
112
+ return jsonResponse({
113
+ error: 'invalid-request',
114
+ message: 'expected { kind: "generate", request } or { messages: UIMessage[] }',
115
+ }, 400);
116
+ }
117
+ const assembled = assembleSystemPrompt(body.context);
118
+ if (!assembled.ok) {
119
+ return jsonResponse({ error: 'invalid-request', message: 'context must be a string or string[]' }, 400);
117
120
  }
118
- if (!options.provider.streamChat) {
121
+ if (!options.provider.languageModel) {
119
122
  return jsonResponse({ error: 'provider-cannot-stream' }, 501);
120
123
  }
124
+ // The SDK loads lazily INSIDE the handler (the anthropic-provider discipline):
125
+ // the client-imported barrel re-exports this module, and a static edge would
126
+ // put the AI SDK on the studio critical path (pinned in bundle.test.ts).
127
+ const { validateUIMessages, convertToModelMessages, streamText, createUIMessageStreamResponse, toUIMessageStream, } = await import('ai');
128
+ // Structural UIMessage validation (id/role/parts). Tool parts validate by
129
+ // shape — deep input validation belongs to the client executor (slice 2).
130
+ let messages;
131
+ try {
132
+ messages = await validateUIMessages({ messages: rawMessages });
133
+ }
134
+ catch {
135
+ return jsonResponse({ error: 'invalid-request', message: 'expected { messages: UIMessage[] }' }, 400);
136
+ }
121
137
  try {
122
- return await options.provider.streamChat(chat);
138
+ const model = await options.provider.languageModel();
139
+ const result = streamText({
140
+ model,
141
+ ...(assembled.system !== undefined ? { system: assembled.system } : {}),
142
+ messages: await convertToModelMessages(messages),
143
+ // No `execute` on any tool — the SDK forwards each call to the client
144
+ // (ratified trust boundary). Continuation is therefore client-driven: one
145
+ // tool round-trip per REQUEST, throttled by the rate limit above; the
146
+ // per-turn step cap lands client-side with slice 2.
147
+ tools: chatToolDefinitions,
148
+ });
149
+ return createUIMessageStreamResponse({
150
+ stream: toUIMessageStream({ stream: result.stream, originalMessages: messages }),
151
+ });
123
152
  }
124
153
  catch {
125
154
  return jsonResponse({ error: 'provider-error' }, 502);
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../src/lib/ai/route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server-route factory (EC-331): a streaming chat/completion endpoint the consuming\n * studio app mounts. Key custody = host route (never the browser, never Convex): the\n * `provider` holds the host-env key and this handler is a THIN model proxy — NO data\n * access, NO service credentials.\n *\n * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the\n * host wires into whatever server entry it has (a TanStack Start server route, a\n * Convex `httpAction`, a plain fetch handler). The current demo studio builds as a\n * pure client SPA with server functions disabled (EC-234), so actually mounting this\n * is an EC-199 integration step; phase 1 ships the handler + its contract.\n *\n * Session-gated: `getSession` (host-wired, reading its own auth — this module never\n * touches `lib/auth`) must return a session for authenticated callers; anonymous\n * callers are rejected. Plus a soft, in-memory per-project rate counter (spend guard).\n *\n * EC-199: the route serves TWO model operations — both thin proxies, neither with any\n * data access — discriminated by the request body's `kind`:\n * - `{ kind: 'chat', messages, system? }` → `provider.streamChat` (text stream);\n * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON).\n * The structured mode is what lets `applyContentWithShape` run CLIENT-side against a\n * {@link createRouteProvider} whose key never touches the browser — the creation flow's\n * whole authoring loop proxies its model calls here while the tools (search/list/apply)\n * execute in the client with the user's own session. A body with `messages` and no\n * `kind` is still treated as a chat request (back-compat with the phase-1 contract).\n */\nimport type {\n AiChatMessage,\n AiRouteHandler,\n AiRouteHandlerOptions,\n AiSession,\n GenerateStructuredRequest,\n StreamChatRequest,\n} from './types'\n\nfunction jsonResponse(value: unknown, status: number): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\ninterface RateBucket {\n windowStart: number\n count: number\n}\n\nfunction isChatMessage(value: unknown): value is AiChatMessage {\n if (typeof value !== 'object' || value === null) return false\n const m = value as { role?: unknown; content?: unknown }\n return (\n (m.role === 'system' || m.role === 'user' || m.role === 'assistant') &&\n typeof m.content === 'string'\n )\n}\n\n/** Parse a request body into a {@link StreamChatRequest}, or `null` when malformed. */\nfunction parseChatBody(body: unknown): StreamChatRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const { messages, system } = body as { messages?: unknown; system?: unknown }\n if (!Array.isArray(messages) || messages.length === 0 || !messages.every(isChatMessage)) {\n return null\n }\n return {\n messages: messages as AiChatMessage[],\n ...(typeof system === 'string' ? { system } : {}),\n }\n}\n\n/** The wire discriminator selecting which model operation the route proxies. */\nexport type AiRouteKind = 'chat' | 'generate'\n\n/** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */\nfunction parseGenerateBody(body: unknown): GenerateStructuredRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const request = (body as { request?: unknown }).request\n if (typeof request !== 'object' || request === null) return null\n const { prompt, schema } = request as { prompt?: unknown; schema?: unknown }\n if (typeof prompt !== 'string' || typeof schema !== 'object' || schema === null) return null\n return request as GenerateStructuredRequest\n}\n\n/** The `kind` a body selects: explicit `kind`, else `messages` ⇒ chat (back-compat). */\nfunction bodyKind(body: unknown): AiRouteKind | null {\n if (typeof body !== 'object' || body === null) return null\n const kind = (body as { kind?: unknown }).kind\n if (kind === 'chat' || kind === 'generate') return kind\n if (Array.isArray((body as { messages?: unknown }).messages)) return 'chat'\n return null\n}\n\n/**\n * Build the mounted route handler. The returned function is a plain\n * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin\n * proxy to `provider.streamChat`. Never throws to the caller: every failure is a\n * structured JSON response with an appropriate status.\n */\nexport function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler {\n const maxPerMinute = options.limits?.maxRequestsPerMinute\n const buckets = new Map<string, RateBucket>()\n const projectKey = options.projectKey ?? (() => 'global')\n\n function rateLimited(key: string): boolean {\n if (maxPerMinute === undefined || maxPerMinute <= 0) return false\n const now = Date.now()\n const bucket = buckets.get(key)\n if (!bucket || now - bucket.windowStart >= 60_000) {\n buckets.set(key, { windowStart: now, count: 1 })\n return false\n }\n bucket.count += 1\n return bucket.count > maxPerMinute\n }\n\n return async (request: Request): Promise<Response> => {\n // 1. Session gate — reject anonymous callers (the route holds spend, not data).\n let session: AiSession | null\n try {\n session = await options.getSession(request)\n } catch {\n return jsonResponse({ error: 'unauthorized' }, 401)\n }\n if (!session) return jsonResponse({ error: 'unauthorized' }, 401)\n\n // 2. Method.\n if (request.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405)\n\n // 3. Soft per-project rate / spend counter.\n if (rateLimited(projectKey(request))) return jsonResponse({ error: 'rate-limited' }, 429)\n\n // 4. Body → the requested model operation.\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n\n const kind = bodyKind(body)\n\n // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`\n // run client-side without the key ever reaching the browser.\n if (kind === 'generate') {\n const generate = parseGenerateBody(body)\n if (!generate) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { kind: \"generate\", request }' },\n 400,\n )\n }\n try {\n const result = await options.provider.generateStructured(generate)\n return jsonResponse(result, 200)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n\n // 5b. Chat stream (default / back-compat).\n const chat = kind === 'chat' ? parseChatBody(body) : null\n if (!chat) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { messages: AiChatMessage[] }' },\n 400,\n )\n }\n if (!options.provider.streamChat) {\n return jsonResponse({ error: 'provider-cannot-stream' }, 501)\n }\n try {\n return await options.provider.streamChat(chat)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n}\n"]}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../../../src/lib/ai/route.ts"],"names":[],"mappings":"AAqCA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAE7C,SAAS,YAAY,CAAC,KAAc,EAAE,MAAc;IAClD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;QACzC,MAAM;QACN,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAA;AACJ,CAAC;AAcD,2FAA2F;AAC3F,SAAS,iBAAiB,CAAC,IAAa;IACtC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC1D,MAAM,OAAO,GAAI,IAA8B,CAAC,OAAO,CAAA;IACvD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAChE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAiD,CAAA;IAC5E,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IAC5F,OAAO,OAAoC,CAAA;AAC7C,CAAC;AAKD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IAClE,MAAM,QAAQ,GACZ,OAAO,OAAO,KAAK,QAAQ;QACzB,CAAC,CAAC,CAAC,OAAO,CAAC;QACX,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC;YACjF,CAAC,CAAE,OAAoB;YACvB,CAAC,CAAC,IAAI,CAAA;IACZ,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,CAAA;IAC3C,MAAM,MAAM,GAAG,QAAQ;SACpB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;SACvC,IAAI,CAAC,MAAM,CAAC,CAAA;IACf,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AAC/D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA8B;IACjE,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,oBAAoB,CAAA;IACzD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAA;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAA;IAEzD,SAAS,WAAW,CAAC,GAAW;QAC9B,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QACjE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QACtB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,WAAW,IAAI,MAAM,EAAE,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;YAChD,OAAO,KAAK,CAAA;QACd,CAAC;QACD,MAAM,CAAC,KAAK,IAAI,CAAC,CAAA;QACjB,OAAO,MAAM,CAAC,KAAK,GAAG,YAAY,CAAA;IACpC,CAAC;IAED,OAAO,KAAK,EAAE,OAAgB,EAAqB,EAAE;QACnD,gFAAgF;QAChF,+EAA+E;QAC/E,IAAI,OAAyB,CAAA;QAC7B,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAA;QACrD,CAAC;QACD,IAAI,CAAC,OAAO;YAAE,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAA;QAEjE,aAAa;QACb,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,EAAE,GAAG,CAAC,CAAA;QAExF,4CAA4C;QAC5C,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAAE,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAA;QAEzF,2CAA2C;QAC3C,IAAI,IAAa,CAAA;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAA;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAA;QACrD,CAAC;QACD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAC9C,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,GAAG,CAAC,CAAA;QACrD,CAAC;QAED,+EAA+E;QAC/E,iEAAiE;QACjE,IAAK,IAA2B,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACrD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAA;YACxC,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,OAAO,YAAY,CACjB,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,wCAAwC,EAAE,EAC/E,GAAG,CACJ,CAAA;YACH,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;gBAClE,OAAO,YAAY,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YAClC,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAA;YACvD,CAAC;QACH,CAAC;QAED,0EAA0E;QAC1E,MAAM,WAAW,GAAI,IAA+B,CAAC,QAAQ,CAAA;QAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5D,OAAO,YAAY,CACjB;gBACE,KAAK,EAAE,iBAAiB;gBACxB,OAAO,EAAE,qEAAqE;aAC/E,EACD,GAAG,CACJ,CAAA;QACH,CAAC;QAED,MAAM,SAAS,GAAG,oBAAoB,CAAE,IAA8B,CAAC,OAAO,CAAC,CAAA;QAC/E,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YAClB,OAAO,YAAY,CACjB,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,sCAAsC,EAAE,EAC7E,GAAG,CACJ,CAAA;QACH,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;YACpC,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,wBAAwB,EAAE,EAAE,GAAG,CAAC,CAAA;QAC/D,CAAC;QAED,+EAA+E;QAC/E,6EAA6E;QAC7E,yEAAyE;QACzE,MAAM,EACJ,kBAAkB,EAClB,sBAAsB,EACtB,UAAU,EACV,6BAA6B,EAC7B,iBAAiB,GAClB,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QAEtB,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,QAAQ,CAAA;QACZ,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,kBAAkB,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAA;QAChE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,YAAY,CACjB,EAAE,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,oCAAoC,EAAE,EAC3E,GAAG,CACJ,CAAA;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;YACpD,MAAM,MAAM,GAAG,UAAU,CAAC;gBACxB,KAAK;gBACL,GAAG,CAAC,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACvE,QAAQ,EAAE,MAAM,sBAAsB,CAAC,QAAQ,CAAC;gBAChD,sEAAsE;gBACtE,0EAA0E;gBAC1E,sEAAsE;gBACtE,oDAAoD;gBACpD,KAAK,EAAE,mBAAmB;aAC3B,CAAC,CAAA;YACF,OAAO,6BAA6B,CAAC;gBACnC,MAAM,EAAE,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,CAAC;aACjF,CAAC,CAAA;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,YAAY,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAA;QACvD,CAAC;IACH,CAAC,CAAA;AACH,CAAC","sourcesContent":["/**\n * Server-route factory (EC-331): the session-gated endpoint the consuming studio\n * app mounts. Key custody = host route (never the browser, never Convex): the\n * `provider` holds the host-env key and this handler is a THIN model proxy — NO data\n * access, NO service credentials, NO server-side tool execution.\n *\n * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the\n * host wires into whatever server entry it has (`createAiDevPlugin` in dev,\n * `toNodeHandler` on a Vercel Node function, any fetch handler).\n *\n * Session-gated: `getSession` (host-wired, reading its own auth — this module never\n * touches `lib/auth`) must return a session for authenticated callers; anonymous\n * callers are rejected with 401 BEFORE any stream byte. Plus a soft, in-memory\n * per-project rate counter (spend guard) — also the backstop for the SDK's\n * client-driven tool-call continuations, each of which is a fresh request here.\n *\n * The route serves TWO model operations — both thin proxies, neither with any data\n * access:\n * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON) — the\n * proxy `applyContentWithShape` / `translateDocument` run CLIENT-side against,\n * so the key never touches the browser;\n * - `{ messages: UIMessage[], context? }` (EC-334, kind-less — the shape the AI\n * SDK's `DefaultChatTransport` posts) → the SDK's native streaming chat\n * protocol: `validateUIMessages` → `streamText` over\n * `provider.languageModel()` with {@link chatToolDefinitions} declared WITHOUT\n * `execute` (every tool call forwards to the client, which executes it against\n * the operations layer with the user's own session) →\n * `createUIMessageStreamResponse` + `toUIMessageStream` (the non-deprecated\n * pair — `result.toUIMessageStreamResponse()` dies in SDK v8). The `system`\n * prompt is assembled from the request's `context` sections; conversation\n * history rides natively in `messages`.\n *\n * BREAKING (EC-334, ratified): the legacy `{ kind: 'chat', messages:\n * AiChatMessage[] }` wire is gone — route factory and client ship lockstep in\n * `@elytracms/studio`, so consumers re-mount the same factory on upgrade.\n */\nimport type { AiRouteHandler, AiRouteHandlerOptions, AiSession, GenerateStructuredRequest } from './types'\nimport { chatToolDefinitions } from './tools'\n\nfunction jsonResponse(value: unknown, status: number): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { 'content-type': 'application/json' },\n })\n}\n\ninterface RateBucket {\n windowStart: number\n count: number\n}\n\n/**\n * The wire discriminator for the structured-generation branch. The native chat\n * branch (EC-334) is deliberately kind-less: the SDK's `DefaultChatTransport`\n * posts `{ messages, … }` with no `kind`.\n */\nexport type AiRouteKind = 'generate'\n\n/** Structurally validate a {@link GenerateStructuredRequest}, or `null` when malformed. */\nfunction parseGenerateBody(body: unknown): GenerateStructuredRequest | null {\n if (typeof body !== 'object' || body === null) return null\n const request = (body as { request?: unknown }).request\n if (typeof request !== 'object' || request === null) return null\n const { prompt, schema } = request as { prompt?: unknown; schema?: unknown }\n if (typeof prompt !== 'string' || typeof schema !== 'object' || schema === null) return null\n return request as GenerateStructuredRequest\n}\n\n/** The outcome of folding the request `context` into a system prompt. */\nexport type AssembledSystem = { ok: true; system?: string } | { ok: false }\n\n/**\n * Fold the request-body `context` into the system prompt (EC-334). The client sends\n * `context` as one string or ordered string sections (collections-catalog summary,\n * client-resolved knowledge, EC-378's current-document block) — the route only\n * ASSEMBLES them; it resolves no data itself (zero-data-access proxy). Malformed\n * context is a structural 400, never silently dropped (validation-first).\n */\nexport function assembleSystemPrompt(context: unknown): AssembledSystem {\n if (context === undefined || context === null) return { ok: true }\n const sections =\n typeof context === 'string'\n ? [context]\n : Array.isArray(context) && context.every((section) => typeof section === 'string')\n ? (context as string[])\n : null\n if (sections === null) return { ok: false }\n const system = sections\n .map((section) => section.trim())\n .filter((section) => section.length > 0)\n .join('\\n\\n')\n return { ok: true, ...(system.length > 0 ? { system } : {}) }\n}\n\n/**\n * Build the mounted route handler. The returned function is a plain\n * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin\n * proxy to the provider. Never throws to the caller: every failure is a structured\n * JSON response with an appropriate status.\n */\nexport function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler {\n const maxPerMinute = options.limits?.maxRequestsPerMinute\n const buckets = new Map<string, RateBucket>()\n const projectKey = options.projectKey ?? (() => 'global')\n\n function rateLimited(key: string): boolean {\n if (maxPerMinute === undefined || maxPerMinute <= 0) return false\n const now = Date.now()\n const bucket = buckets.get(key)\n if (!bucket || now - bucket.windowStart >= 60_000) {\n buckets.set(key, { windowStart: now, count: 1 })\n return false\n }\n bucket.count += 1\n return bucket.count > maxPerMinute\n }\n\n return async (request: Request): Promise<Response> => {\n // 1. Session gate — reject anonymous callers (the route holds spend, not data).\n // Position is FROZEN: the 401 must precede any stream byte (EC-371/EC-334).\n let session: AiSession | null\n try {\n session = await options.getSession(request)\n } catch {\n return jsonResponse({ error: 'unauthorized' }, 401)\n }\n if (!session) return jsonResponse({ error: 'unauthorized' }, 401)\n\n // 2. Method.\n if (request.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405)\n\n // 3. Soft per-project rate / spend counter.\n if (rateLimited(projectKey(request))) return jsonResponse({ error: 'rate-limited' }, 429)\n\n // 4. Body → the requested model operation.\n let body: unknown\n try {\n body = await request.json()\n } catch {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n if (typeof body !== 'object' || body === null) {\n return jsonResponse({ error: 'invalid-json' }, 400)\n }\n\n // 5a. Structured generation — the thin proxy that lets `applyContentWithShape`\n // run client-side without the key ever reaching the browser.\n if ((body as { kind?: unknown }).kind === 'generate') {\n const generate = parseGenerateBody(body)\n if (!generate) {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { kind: \"generate\", request }' },\n 400,\n )\n }\n try {\n const result = await options.provider.generateStructured(generate)\n return jsonResponse(result, 200)\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n\n // 5b. Native chat branch (EC-334): `{ messages: UIMessage[], context? }`.\n const rawMessages = (body as { messages?: unknown }).messages\n if (!Array.isArray(rawMessages) || rawMessages.length === 0) {\n return jsonResponse(\n {\n error: 'invalid-request',\n message: 'expected { kind: \"generate\", request } or { messages: UIMessage[] }',\n },\n 400,\n )\n }\n\n const assembled = assembleSystemPrompt((body as { context?: unknown }).context)\n if (!assembled.ok) {\n return jsonResponse(\n { error: 'invalid-request', message: 'context must be a string or string[]' },\n 400,\n )\n }\n\n if (!options.provider.languageModel) {\n return jsonResponse({ error: 'provider-cannot-stream' }, 501)\n }\n\n // The SDK loads lazily INSIDE the handler (the anthropic-provider discipline):\n // the client-imported barrel re-exports this module, and a static edge would\n // put the AI SDK on the studio critical path (pinned in bundle.test.ts).\n const {\n validateUIMessages,\n convertToModelMessages,\n streamText,\n createUIMessageStreamResponse,\n toUIMessageStream,\n } = await import('ai')\n\n // Structural UIMessage validation (id/role/parts). Tool parts validate by\n // shape — deep input validation belongs to the client executor (slice 2).\n let messages\n try {\n messages = await validateUIMessages({ messages: rawMessages })\n } catch {\n return jsonResponse(\n { error: 'invalid-request', message: 'expected { messages: UIMessage[] }' },\n 400,\n )\n }\n\n try {\n const model = await options.provider.languageModel()\n const result = streamText({\n model,\n ...(assembled.system !== undefined ? { system: assembled.system } : {}),\n messages: await convertToModelMessages(messages),\n // No `execute` on any tool — the SDK forwards each call to the client\n // (ratified trust boundary). Continuation is therefore client-driven: one\n // tool round-trip per REQUEST, throttled by the rate limit above; the\n // per-turn step cap lands client-side with slice 2.\n tools: chatToolDefinitions,\n })\n return createUIMessageStreamResponse({\n stream: toUIMessageStream({ stream: result.stream, originalMessages: messages }),\n })\n } catch {\n return jsonResponse({ error: 'provider-error' }, 502)\n }\n }\n}\n"]}