@llmnative/react 1.25.3 → 1.26.0

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/dist/server.js ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./serverCredentials-lxpxgMsj.js");async function t(t,n){let r=[t.url,...Object.values(t.headers)].flatMap(e.i);if(r.length===0)return{ok:!0,url:t.url,headers:t.headers};let i=new URL(t.url),{url:a}=t,o={...t.headers},s=new Set;for(let{placeholder:t,scope:c,provider:l}of r){if(s.has(t))continue;s.add(t);let r=await n(c,l);if(!r?.apiKey)return{ok:!1,status:404,error:`unknown-credential`,provider:l};if(!e.a({...r,id:l},i))return{ok:!1,status:403,error:`forbidden-target`,provider:l};a=a.split(t).join(encodeURIComponent(r.apiKey)),o=Object.fromEntries(Object.entries(o).map(([e,n])=>[e,n.split(t).join(r.apiKey)]))}return{ok:!0,url:a,headers:o}}exports.AI_PROVIDER_HOSTS=e.t,exports.SERVER_CREDENTIAL_PREFIX=e.n,exports.isAIProviderTarget=e.a,exports.resolveServerCredentials=t,exports.serverCredential=e.o;
2
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":["findServerCredentials","isAIProviderTarget","require_serverCredentials","resolveServerCredentials","found","request","target","headers","done","placeholder","secret","lookup","scope","provider","url","name","value"],"sources":["../src/providers/ai/server.ts"],"sourcesContent":["// `@llmnative/react/server`: what a Node proxy needs for server-held AI credentials (CR-102).\n// No React and no browser API: it runs in the app's own server or in the Vite dev proxy.\n\nimport { findServerCredentials, isAIProviderTarget, type ServerAIConnection } from './serverCredentials';\n\nexport { AI_PROVIDER_HOSTS, isAIProviderTarget, serverCredential, SERVER_CREDENTIAL_PREFIX } from './serverCredentials';\nexport type { ServerAIConnection } from './serverCredentials';\n\n/** The connection of a provider as the server keeps it, key included. */\nexport type ServerAISecret = ServerAIConnection & { apiKey: string };\n\n/** The key of `provider` for `scope` (e.g. a company), or nothing when it is not connected. */\nexport type ServerCredentialLookup = (scope: string, provider: string) => ServerAISecret | undefined | Promise<ServerAISecret | undefined>;\n\nexport type ServerCredentialRequest = { url: string; headers: Record<string, string> };\n\nexport type ServerCredentialResolution =\n | { ok: true; url: string; headers: Record<string, string> }\n /** 404: the provider is not connected for that scope; 403: the target is not a host of that provider. */\n | { ok: false; status: 403 | 404; error: 'unknown-credential' | 'forbidden-target'; provider: string };\n\n/**\n * Swaps every server-credential placeholder of a proxied request (headers and URL) for the key it\n * stands for. The key only goes to a host of its provider: any other target is refused, so a\n * placeholder can never carry a key elsewhere. A request without placeholders passes unchanged.\n */\nexport async function resolveServerCredentials(request: ServerCredentialRequest, lookup: ServerCredentialLookup): Promise<ServerCredentialResolution> {\n const found = [request.url, ...Object.values(request.headers)].flatMap(findServerCredentials);\n if (found.length === 0) return { ok: true, url: request.url, headers: request.headers };\n\n const target = new URL(request.url);\n let { url } = request;\n let headers = { ...request.headers };\n const done = new Set<string>();\n for (const { placeholder, scope, provider } of found) {\n if (done.has(placeholder)) continue;\n done.add(placeholder);\n const secret = await lookup(scope, provider);\n if (!secret?.apiKey) return { ok: false, status: 404, error: 'unknown-credential', provider };\n if (!isAIProviderTarget({ ...secret, id: provider }, target)) return { ok: false, status: 403, error: 'forbidden-target', provider };\n url = url.split(placeholder).join(encodeURIComponent(secret.apiKey));\n headers = Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, value.split(placeholder).join(secret.apiKey)]));\n }\n return { ok: true, url, headers };\n}\n"],"mappings":"sHA0BA,eAAsBG,EAAyB,EAAkC,EAAqE,CAClJ,IAAMC,EAAQ,CAACC,EAAQ,IAAK,GAAG,OAAO,OAAOA,EAAQ,OAAO,CAAC,CAAC,CAAC,QAAQL,EAAAA,CAAqB,EAC5F,GAAII,EAAM,SAAW,EAAG,MAAO,CAAE,GAAI,GAAM,IAAKC,EAAQ,IAAK,QAASA,EAAQ,OAAQ,EAEtF,IAAMC,EAAS,IAAI,IAAID,EAAQ,GAAG,EAC9B,CAAE,OAAQA,EACVE,EAAU,CAAE,GAAGF,EAAQ,OAAQ,EAC7BG,EAAO,IAAI,IACjB,IAAK,GAAM,CAAE,cAAa,QAAO,cAAcJ,EAAO,CAClD,GAAII,EAAK,IAAIC,CAAW,EAAG,SAC3BD,EAAK,IAAIC,CAAW,EACpB,IAAMC,EAAS,MAAMC,EAAOC,EAAOC,CAAQ,EAC3C,GAAI,CAACH,GAAQ,OAAQ,MAAO,CAAE,GAAI,GAAO,OAAQ,IAAK,MAAO,qBAAsB,UAAS,EAC5F,GAAI,CAACT,EAAAA,EAAmB,CAAE,GAAGS,EAAQ,GAAIG,CAAS,EAAGP,CAAM,EAAG,MAAO,CAAE,GAAI,GAAO,OAAQ,IAAK,MAAO,mBAAoB,UAAS,EACnI,EAAMQ,EAAI,MAAML,CAAW,CAAC,CAAC,KAAK,mBAAmBC,EAAO,MAAM,CAAC,EACnE,EAAU,OAAO,YAAY,OAAO,QAAQH,CAAO,CAAC,CAAC,KAAK,CAACQ,EAAMC,KAAW,CAACD,EAAMC,EAAM,MAAMP,CAAW,CAAC,CAAC,KAAKC,EAAO,MAAM,CAAC,CAAC,CAAC,CACrI,CACA,MAAO,CAAE,GAAI,GAAM,MAAK,SAAQ,CACpC"}
@@ -0,0 +1,41 @@
1
+ import { a as e, i as t, n, o as r, t as i } from "./serverCredentials-PJbll0eu.mjs";
2
+ //#region src/providers/ai/server.ts
3
+ async function a(n, r) {
4
+ let i = [n.url, ...Object.values(n.headers)].flatMap(t);
5
+ if (i.length === 0) return {
6
+ ok: !0,
7
+ url: n.url,
8
+ headers: n.headers
9
+ };
10
+ let a = new URL(n.url), { url: o } = n, s = { ...n.headers }, c = /* @__PURE__ */ new Set();
11
+ for (let { placeholder: t, scope: n, provider: l } of i) {
12
+ if (c.has(t)) continue;
13
+ c.add(t);
14
+ let i = await r(n, l);
15
+ if (!i?.apiKey) return {
16
+ ok: !1,
17
+ status: 404,
18
+ error: "unknown-credential",
19
+ provider: l
20
+ };
21
+ if (!e({
22
+ ...i,
23
+ id: l
24
+ }, a)) return {
25
+ ok: !1,
26
+ status: 403,
27
+ error: "forbidden-target",
28
+ provider: l
29
+ };
30
+ o = o.split(t).join(encodeURIComponent(i.apiKey)), s = Object.fromEntries(Object.entries(s).map(([e, n]) => [e, n.split(t).join(i.apiKey)]));
31
+ }
32
+ return {
33
+ ok: !0,
34
+ url: o,
35
+ headers: s
36
+ };
37
+ }
38
+ //#endregion
39
+ export { i as AI_PROVIDER_HOSTS, n as SERVER_CREDENTIAL_PREFIX, e as isAIProviderTarget, a as resolveServerCredentials, r as serverCredential };
40
+
41
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.mjs","names":["isAIProviderTarget","findServerCredentials","serverCredential","AI_PROVIDER_HOSTS","resolveServerCredentials","found","request","target","headers","done","placeholder","secret","lookup","scope","provider","url","name","value","SERVER_CREDENTIAL_PREFIX"],"sources":["../src/providers/ai/server.ts"],"sourcesContent":["// `@llmnative/react/server`: what a Node proxy needs for server-held AI credentials (CR-102).\n// No React and no browser API: it runs in the app's own server or in the Vite dev proxy.\n\nimport { findServerCredentials, isAIProviderTarget, type ServerAIConnection } from './serverCredentials';\n\nexport { AI_PROVIDER_HOSTS, isAIProviderTarget, serverCredential, SERVER_CREDENTIAL_PREFIX } from './serverCredentials';\nexport type { ServerAIConnection } from './serverCredentials';\n\n/** The connection of a provider as the server keeps it, key included. */\nexport type ServerAISecret = ServerAIConnection & { apiKey: string };\n\n/** The key of `provider` for `scope` (e.g. a company), or nothing when it is not connected. */\nexport type ServerCredentialLookup = (scope: string, provider: string) => ServerAISecret | undefined | Promise<ServerAISecret | undefined>;\n\nexport type ServerCredentialRequest = { url: string; headers: Record<string, string> };\n\nexport type ServerCredentialResolution =\n | { ok: true; url: string; headers: Record<string, string> }\n /** 404: the provider is not connected for that scope; 403: the target is not a host of that provider. */\n | { ok: false; status: 403 | 404; error: 'unknown-credential' | 'forbidden-target'; provider: string };\n\n/**\n * Swaps every server-credential placeholder of a proxied request (headers and URL) for the key it\n * stands for. The key only goes to a host of its provider: any other target is refused, so a\n * placeholder can never carry a key elsewhere. A request without placeholders passes unchanged.\n */\nexport async function resolveServerCredentials(request: ServerCredentialRequest, lookup: ServerCredentialLookup): Promise<ServerCredentialResolution> {\n const found = [request.url, ...Object.values(request.headers)].flatMap(findServerCredentials);\n if (found.length === 0) return { ok: true, url: request.url, headers: request.headers };\n\n const target = new URL(request.url);\n let { url } = request;\n let headers = { ...request.headers };\n const done = new Set<string>();\n for (const { placeholder, scope, provider } of found) {\n if (done.has(placeholder)) continue;\n done.add(placeholder);\n const secret = await lookup(scope, provider);\n if (!secret?.apiKey) return { ok: false, status: 404, error: 'unknown-credential', provider };\n if (!isAIProviderTarget({ ...secret, id: provider }, target)) return { ok: false, status: 403, error: 'forbidden-target', provider };\n url = url.split(placeholder).join(encodeURIComponent(secret.apiKey));\n headers = Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, value.split(placeholder).join(secret.apiKey)]));\n }\n return { ok: true, url, headers };\n}\n"],"mappings":";;AA0BA,eAAsBI,EAAyB,GAAkC,GAAqE;CAClJ,IAAMC,IAAQ,CAACC,EAAQ,KAAK,GAAG,OAAO,OAAOA,EAAQ,OAAO,CAAC,CAAC,CAAC,QAAQL,CAAqB;CAC5F,IAAII,EAAM,WAAW,GAAG,OAAO;EAAE,IAAI;EAAM,KAAKC,EAAQ;EAAK,SAASA,EAAQ;CAAQ;CAEtF,IAAMC,IAAS,IAAI,IAAID,EAAQ,GAAG,GAC9B,EAAE,WAAQA,GACVE,IAAU,EAAE,GAAGF,EAAQ,QAAQ,GAC7BG,oBAAO,IAAI,IAAY;CAC7B,KAAK,IAAM,EAAE,gBAAa,UAAO,iBAAcJ,GAAO;EAClD,IAAII,EAAK,IAAIC,CAAW,GAAG;EAC3B,EAAK,IAAIA,CAAW;EACpB,IAAMC,IAAS,MAAMC,EAAOC,GAAOC,CAAQ;EAC3C,IAAI,CAACH,GAAQ,QAAQ,OAAO;GAAE,IAAI;GAAO,QAAQ;GAAK,OAAO;GAAsB;EAAS;EAC5F,IAAI,CAACX,EAAmB;GAAE,GAAGW;GAAQ,IAAIG;EAAS,GAAGP,CAAM,GAAG,OAAO;GAAE,IAAI;GAAO,QAAQ;GAAK,OAAO;GAAoB;EAAS;EAEnI,AADA,IAAMQ,EAAI,MAAML,CAAW,CAAC,CAAC,KAAK,mBAAmBC,EAAO,MAAM,CAAC,GACnE,IAAU,OAAO,YAAY,OAAO,QAAQH,CAAO,CAAC,CAAC,KAAK,CAACQ,GAAMC,OAAW,CAACD,GAAMC,EAAM,MAAMP,CAAW,CAAC,CAAC,KAAKC,EAAO,MAAM,CAAC,CAAC,CAAC;CACrI;CACA,OAAO;EAAE,IAAI;EAAM;EAAK;CAAQ;AACpC"}
@@ -0,0 +1,31 @@
1
+ //#region src/providers/ai/serverCredentials.ts
2
+ var e = "llmnative-server-credential", t = /^[A-Za-z0-9_-]+$/, n = /llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g, r = (n, r) => {
3
+ if (!t.test(n)) throw Error(`Server credential scope "${n}" may only contain letters, digits, "-" and "_".`);
4
+ return `${e}:${n}:${r}`;
5
+ }, i = (e) => [...e.matchAll(n)].map(([e, t, n]) => ({
6
+ placeholder: e,
7
+ scope: t,
8
+ provider: n
9
+ })), a = (t) => t.includes(`${e}:`), o = {
10
+ openai: "api.openai.com",
11
+ openrouter: "openrouter.ai",
12
+ opencode: "opencode.ai",
13
+ deepseek: "api.deepseek.com",
14
+ gemini: "generativelanguage.googleapis.com",
15
+ anthropic: "api.anthropic.com",
16
+ mistral: "api.mistral.ai",
17
+ glm: "open.bigmodel.cn",
18
+ groq: "api.groq.com",
19
+ cloudflare: "api.cloudflare.com"
20
+ }, s = (e) => e.replace(/\/+$/, ""), c = (e, t) => {
21
+ if (e.id === "openai-compatible") {
22
+ if (!e.baseUrl) return !1;
23
+ let n = new URL(e.baseUrl), r = s(n.pathname);
24
+ return t.origin === n.origin && (t.pathname === r || t.pathname.startsWith(`${r}/`));
25
+ }
26
+ return t.protocol !== "https:" || t.host !== o[e.id] ? !1 : e.id !== "cloudflare" || !!e.accountId && t.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(e.accountId.trim())}/`);
27
+ };
28
+ //#endregion
29
+ export { c as a, i, e as n, r as o, a as r, o as t };
30
+
31
+ //# sourceMappingURL=serverCredentials-PJbll0eu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverCredentials-PJbll0eu.mjs","names":["SERVER_CREDENTIAL_PREFIX","SCOPE_PATTERN","PLACEHOLDER_PATTERN","serverCredential","scope","provider","findServerCredentials","text","placeholder","containsServerCredential","AI_PROVIDER_HOSTS","trimSlashes","path","isAIProviderTarget","connection","base","basePath","target"],"sources":["../src/providers/ai/serverCredentials.ts"],"sourcesContent":["// Server-held AI credentials (CR-102). An app that cannot hold provider keys in the browser builds\n// its adapters with a placeholder where the key goes; its proxy swaps the placeholder for the real\n// key, only towards a host of that provider. Pure module: no React, no config — the browser\n// registry, proxyFetch and the Node entry (`@llmnative/react/server`) all share it.\n\nexport const SERVER_CREDENTIAL_PREFIX = 'llmnative-server-credential';\n\nconst SCOPE_PATTERN = /^[A-Za-z0-9_-]+$/;\nconst PLACEHOLDER_PATTERN = /llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g;\n\n/** A provider connected on the server: its id, and what its URLs are built from. Never the key. */\nexport type ServerAIConnection = {\n id: string;\n /** Cloudflare: the account in every Workers AI URL. */\n accountId?: string;\n /** openai-compatible: the endpoint. */\n baseUrl?: string;\n};\n\n/** The placeholder an adapter writes where the key goes: `llmnative-server-credential:<scope>:<provider>`. */\nexport const serverCredential = (scope: string, provider: string): string => {\n if (!SCOPE_PATTERN.test(scope)) throw new Error(`Server credential scope \"${scope}\" may only contain letters, digits, \"-\" and \"_\".`);\n return `${SERVER_CREDENTIAL_PREFIX}:${scope}:${provider}`;\n};\n\n/** Every placeholder in `text`, as scope and provider. */\nexport const findServerCredentials = (text: string): Array<{ placeholder: string; scope: string; provider: string }> =>\n [...text.matchAll(PLACEHOLDER_PATTERN)].map(([placeholder, scope, provider]) => ({ placeholder, scope, provider }));\n\nexport const containsServerCredential = (text: string): boolean => text.includes(`${SERVER_CREDENTIAL_PREFIX}:`);\n\n/** The fixed API host of each built-in provider (openai-compatible and Cloudflare depend on the connection). */\nexport const AI_PROVIDER_HOSTS: Readonly<Record<string, string>> = {\n openai: 'api.openai.com',\n openrouter: 'openrouter.ai',\n opencode: 'opencode.ai',\n deepseek: 'api.deepseek.com',\n gemini: 'generativelanguage.googleapis.com',\n anthropic: 'api.anthropic.com',\n mistral: 'api.mistral.ai',\n glm: 'open.bigmodel.cn',\n groq: 'api.groq.com',\n cloudflare: 'api.cloudflare.com',\n};\n\nconst trimSlashes = (path: string) => path.replace(/\\/+$/, '');\n\n/** Whether `target` is an API URL of the provider of `connection`: where its key may go. */\nexport const isAIProviderTarget = (connection: ServerAIConnection, target: URL): boolean => {\n if (connection.id === 'openai-compatible') {\n if (!connection.baseUrl) return false;\n const base = new URL(connection.baseUrl);\n const basePath = trimSlashes(base.pathname);\n return target.origin === base.origin && (target.pathname === basePath || target.pathname.startsWith(`${basePath}/`));\n }\n if (target.protocol !== 'https:' || target.host !== AI_PROVIDER_HOSTS[connection.id]) return false;\n // Cloudflare: one host for every account, so the key only goes to the account it belongs to.\n if (connection.id === 'cloudflare') {\n return Boolean(connection.accountId) && target.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(connection.accountId!.trim())}/`);\n }\n return true;\n};\n"],"mappings":";AAKA,IAAaA,IAA2B,+BAElCC,IAAgB,oBAChBC,IAAsB,2DAYfC,KAAoB,GAAe,MAA6B;CACzE,IAAI,CAACF,EAAc,KAAKG,CAAK,GAAG,MAAU,MAAM,4BAA4BA,EAAM,iDAAiD;CACnI,OAAO,GAAGJ,EAAyB,GAAGI,EAAM,GAAGC;AACnD,GAGaC,KAAyB,MAClC,CAAC,GAAGC,EAAK,SAASL,CAAmB,CAAC,CAAC,CAAC,KAAK,CAACM,GAAaJ,GAAOC,QAAe;CAAE;CAAa;CAAO;AAAS,EAAE,GAEzGI,KAA4B,MAA0BF,EAAK,SAAS,GAAGP,EAAyB,EAAE,GAGlGU,IAAsD;CAC/D,QAAQ;CACR,YAAY;CACZ,UAAU;CACV,UAAU;CACV,QAAQ;CACR,WAAW;CACX,SAAS;CACT,KAAK;CACL,MAAM;CACN,YAAY;AAChB,GAEMC,KAAe,MAAiBC,EAAK,QAAQ,QAAQ,EAAE,GAGhDC,KAAsB,GAAgC,MAAyB;CACxF,IAAIC,EAAW,OAAO,qBAAqB;EACvC,IAAI,CAACA,EAAW,SAAS,OAAO;EAChC,IAAMC,IAAO,IAAI,IAAID,EAAW,OAAO,GACjCE,IAAWL,EAAYI,EAAK,QAAQ;EAC1C,OAAOE,EAAO,WAAWF,EAAK,WAAWE,EAAO,aAAaD,KAAYC,EAAO,SAAS,WAAW,GAAGD,EAAS,EAAE;CACtH;CAMA,OALIC,EAAO,aAAa,YAAYA,EAAO,SAASP,EAAkBI,EAAW,MAAY,KAEzFA,EAAW,OAAO,gBACX,EAAQA,EAAW,aAAcG,EAAO,SAAS,WAAW,uBAAuB,mBAAmBH,EAAW,UAAW,KAAK,CAAC,EAAE,EAAE;AAGrJ"}
@@ -0,0 +1,2 @@
1
+ var e=`llmnative-server-credential`,t=/^[A-Za-z0-9_-]+$/,n=/llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g,r=(n,r)=>{if(!t.test(n))throw Error(`Server credential scope "${n}" may only contain letters, digits, "-" and "_".`);return`${e}:${n}:${r}`},i=e=>[...e.matchAll(n)].map(([e,t,n])=>({placeholder:e,scope:t,provider:n})),a=t=>t.includes(`${e}:`),o={openai:`api.openai.com`,openrouter:`openrouter.ai`,opencode:`opencode.ai`,deepseek:`api.deepseek.com`,gemini:`generativelanguage.googleapis.com`,anthropic:`api.anthropic.com`,mistral:`api.mistral.ai`,glm:`open.bigmodel.cn`,groq:`api.groq.com`,cloudflare:`api.cloudflare.com`},s=e=>e.replace(/\/+$/,``),c=(e,t)=>{if(e.id===`openai-compatible`){if(!e.baseUrl)return!1;let n=new URL(e.baseUrl),r=s(n.pathname);return t.origin===n.origin&&(t.pathname===r||t.pathname.startsWith(`${r}/`))}return t.protocol!==`https:`||t.host!==o[e.id]?!1:e.id!==`cloudflare`||!!e.accountId&&t.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(e.accountId.trim())}/`)};Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return e}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return o}});
2
+ //# sourceMappingURL=serverCredentials-lxpxgMsj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverCredentials-lxpxgMsj.js","names":["SERVER_CREDENTIAL_PREFIX","SCOPE_PATTERN","PLACEHOLDER_PATTERN","serverCredential","scope","provider","findServerCredentials","text","placeholder","containsServerCredential","AI_PROVIDER_HOSTS","trimSlashes","path","isAIProviderTarget","connection","base","basePath","target"],"sources":["../src/providers/ai/serverCredentials.ts"],"sourcesContent":["// Server-held AI credentials (CR-102). An app that cannot hold provider keys in the browser builds\n// its adapters with a placeholder where the key goes; its proxy swaps the placeholder for the real\n// key, only towards a host of that provider. Pure module: no React, no config — the browser\n// registry, proxyFetch and the Node entry (`@llmnative/react/server`) all share it.\n\nexport const SERVER_CREDENTIAL_PREFIX = 'llmnative-server-credential';\n\nconst SCOPE_PATTERN = /^[A-Za-z0-9_-]+$/;\nconst PLACEHOLDER_PATTERN = /llmnative-server-credential:([A-Za-z0-9_-]+):([a-z-]+)/g;\n\n/** A provider connected on the server: its id, and what its URLs are built from. Never the key. */\nexport type ServerAIConnection = {\n id: string;\n /** Cloudflare: the account in every Workers AI URL. */\n accountId?: string;\n /** openai-compatible: the endpoint. */\n baseUrl?: string;\n};\n\n/** The placeholder an adapter writes where the key goes: `llmnative-server-credential:<scope>:<provider>`. */\nexport const serverCredential = (scope: string, provider: string): string => {\n if (!SCOPE_PATTERN.test(scope)) throw new Error(`Server credential scope \"${scope}\" may only contain letters, digits, \"-\" and \"_\".`);\n return `${SERVER_CREDENTIAL_PREFIX}:${scope}:${provider}`;\n};\n\n/** Every placeholder in `text`, as scope and provider. */\nexport const findServerCredentials = (text: string): Array<{ placeholder: string; scope: string; provider: string }> =>\n [...text.matchAll(PLACEHOLDER_PATTERN)].map(([placeholder, scope, provider]) => ({ placeholder, scope, provider }));\n\nexport const containsServerCredential = (text: string): boolean => text.includes(`${SERVER_CREDENTIAL_PREFIX}:`);\n\n/** The fixed API host of each built-in provider (openai-compatible and Cloudflare depend on the connection). */\nexport const AI_PROVIDER_HOSTS: Readonly<Record<string, string>> = {\n openai: 'api.openai.com',\n openrouter: 'openrouter.ai',\n opencode: 'opencode.ai',\n deepseek: 'api.deepseek.com',\n gemini: 'generativelanguage.googleapis.com',\n anthropic: 'api.anthropic.com',\n mistral: 'api.mistral.ai',\n glm: 'open.bigmodel.cn',\n groq: 'api.groq.com',\n cloudflare: 'api.cloudflare.com',\n};\n\nconst trimSlashes = (path: string) => path.replace(/\\/+$/, '');\n\n/** Whether `target` is an API URL of the provider of `connection`: where its key may go. */\nexport const isAIProviderTarget = (connection: ServerAIConnection, target: URL): boolean => {\n if (connection.id === 'openai-compatible') {\n if (!connection.baseUrl) return false;\n const base = new URL(connection.baseUrl);\n const basePath = trimSlashes(base.pathname);\n return target.origin === base.origin && (target.pathname === basePath || target.pathname.startsWith(`${basePath}/`));\n }\n if (target.protocol !== 'https:' || target.host !== AI_PROVIDER_HOSTS[connection.id]) return false;\n // Cloudflare: one host for every account, so the key only goes to the account it belongs to.\n if (connection.id === 'cloudflare') {\n return Boolean(connection.accountId) && target.pathname.startsWith(`/client/v4/accounts/${encodeURIComponent(connection.accountId!.trim())}/`);\n }\n return true;\n};\n"],"mappings":"AAKA,IAAaA,EAA2B,8BAElCC,EAAgB,mBAChBC,EAAsB,0DAYfC,GAAoB,EAAe,IAA6B,CACzE,GAAI,CAACF,EAAc,KAAKG,CAAK,EAAG,MAAU,MAAM,4BAA4BA,EAAM,iDAAiD,EACnI,MAAO,GAAGJ,EAAyB,GAAGI,EAAM,GAAGC,GACnD,EAGaC,EAAyB,GAClC,CAAC,GAAGC,EAAK,SAASL,CAAmB,CAAC,CAAC,CAAC,KAAK,CAACM,EAAaJ,EAAOC,MAAe,CAAE,cAAa,QAAO,UAAS,EAAE,EAEzGI,EAA4B,GAA0BF,EAAK,SAAS,GAAGP,EAAyB,EAAE,EAGlGU,EAAsD,CAC/D,OAAQ,iBACR,WAAY,gBACZ,SAAU,cACV,SAAU,mBACV,OAAQ,oCACR,UAAW,oBACX,QAAS,iBACT,IAAK,mBACL,KAAM,eACN,WAAY,oBAChB,EAEMC,EAAe,GAAiBC,EAAK,QAAQ,OAAQ,EAAE,EAGhDC,GAAsB,EAAgC,IAAyB,CACxF,GAAIC,EAAW,KAAO,oBAAqB,CACvC,GAAI,CAACA,EAAW,QAAS,MAAO,GAChC,IAAMC,EAAO,IAAI,IAAID,EAAW,OAAO,EACjCE,EAAWL,EAAYI,EAAK,QAAQ,EAC1C,OAAOE,EAAO,SAAWF,EAAK,SAAWE,EAAO,WAAaD,GAAYC,EAAO,SAAS,WAAW,GAAGD,EAAS,EAAE,EACtH,CAMA,OALIC,EAAO,WAAa,UAAYA,EAAO,OAASP,EAAkBI,EAAW,IAAY,GAEzFA,EAAW,KAAO,cACX,EAAQA,EAAW,WAAcG,EAAO,SAAS,WAAW,uBAAuB,mBAAmBH,EAAW,UAAW,KAAK,CAAC,EAAE,EAAE,CAGrJ"}
@@ -78,8 +78,8 @@ export { loadGoogleMaps } from './providers/googleMaps/loadGoogleMaps';
78
78
  export type { GoogleMapsConfig } from './providers/googleMaps/loadGoogleMaps';
79
79
  export { useEmailProvider, EmailProvider } from './providers/email/EmailProviderContext';
80
80
  export { GmailEmailProvider } from './providers/email/google/GmailEmailProvider';
81
- export type { AIRequestOptions, AIAttachment, AIProviderAdapter, AIKeyValidationResult, AIModelDescriptor, AIModelPricing, DiscoveredAIModel, AIProviderCapabilities, AIModelCatalog, AIProviderDefinition, AICompleteRequest, AICompleteResult, AIConversationTurn, AIToolDefinition, AIToolCall, AIToolResult, CloudflareProviderOptions, } from './providers/ai';
82
- export { createAIProviderRegistry, getAIModelCatalog, formatAIModelRef, parseAIModelRef, isFreeAIModel, AI_PROVIDER_DEFINITIONS, AI_PROVIDER_DESCRIPTORS, OPENAI_COMPATIBLE_PROVIDER_DESCRIPTOR, CLOUDFLARE_PROVIDER_DESCRIPTOR, createCloudflareProviderDefinition, toProviderDescriptor } from './providers/ai';
81
+ export type { AIRequestOptions, AIAttachment, AIProviderAdapter, AIKeyValidationResult, AIModelDescriptor, AIModelPricing, DiscoveredAIModel, AIProviderCapabilities, AIModelCatalog, AIProviderDefinition, AICompleteRequest, AICompleteResult, AIConversationTurn, AIToolDefinition, AIToolCall, AIToolResult, CloudflareProviderOptions, ServerAIProviderConnection, ServerAIConnection, } from './providers/ai';
82
+ export { createAIProviderRegistry, getAIModelCatalog, formatAIModelRef, parseAIModelRef, isFreeAIModel, AI_PROVIDER_DEFINITIONS, AI_PROVIDER_DESCRIPTORS, OPENAI_COMPATIBLE_PROVIDER_DESCRIPTOR, CLOUDFLARE_PROVIDER_DESCRIPTOR, createCloudflareProviderDefinition, toProviderDescriptor, createServerCredentialAIRegistry, serverCredential, SERVER_CREDENTIAL_PREFIX, AI_PROVIDER_HOSTS, isAIProviderTarget } from './providers/ai';
83
83
  export type { ProviderDescriptor, ProviderCredentialField } from './providers/ProviderDescriptor';
84
84
  export { useAIProvider, useAIProviderRegistry, AIProvider } from './providers/ai/AIProviderContext';
85
85
  export { proxyFetch, useProxy, configureProxy, isProxyEnabled } from './providers/proxy';
@@ -1,7 +1,7 @@
1
1
  import type { AIConfig } from '../../Config';
2
2
  import type { ProviderDescriptor } from '../ProviderDescriptor';
3
3
  import type { AIProviderAdapter } from './AIProvider';
4
- import { type AIProviderDefinition } from './shared';
4
+ import { type AIProviderDefinition, type BuiltInAIProviderId } from './shared';
5
5
  export type { AIProviderAdapter, AIKeyValidationResult, AIModelDescriptor, AIModelPricing, AIProviderCapabilities, AIRequestOptions, AIAttachment, AICompleteRequest, AICompleteResult, AIConversationTurn, AIToolDefinition, AIToolCall, AIToolResult, } from './AIProvider';
6
6
  export { formatAIModelRef, parseAIModelRef, isFreeAIModel } from './AIProvider';
7
7
  export type { DiscoveredAIModel } from './shared';
@@ -9,6 +9,8 @@ export type { AIModelCatalog, AIProviderDefinition } from './shared';
9
9
  export { getAIModelCatalog } from './shared';
10
10
  export { CLOUDFLARE_PROVIDER_DESCRIPTOR, createCloudflareProviderDefinition } from './cloudflare';
11
11
  export type { CloudflareProviderOptions } from './cloudflare';
12
+ export { AI_PROVIDER_HOSTS, isAIProviderTarget, serverCredential, SERVER_CREDENTIAL_PREFIX } from './serverCredentials';
13
+ export type { ServerAIConnection } from './serverCredentials';
12
14
  export declare const AI_PROVIDER_DEFINITIONS: AIProviderDefinition[];
13
15
  /** Generic "connect this provider" view of an AIProviderDefinition — the shape a CMS/admin
14
16
  * UI needs (label, description, credential fields, where to find them), independent of the
@@ -24,3 +26,24 @@ export declare const AI_PROVIDER_DESCRIPTORS: ProviderDescriptor[];
24
26
  * from until the user supplies that URL), but a "connect" UI still needs to offer it. */
25
27
  export declare const OPENAI_COMPATIBLE_PROVIDER_DESCRIPTOR: ProviderDescriptor;
26
28
  export declare const createAIProviderRegistry: (aiConfig?: AIConfig) => Record<string, AIProviderAdapter>;
29
+ /** A provider connected on the app's server, as the browser may know it: never its key. */
30
+ export type ServerAIProviderConnection = {
31
+ id: BuiltInAIProviderId;
32
+ /** Cloudflare: the account in every Workers AI URL. */
33
+ accountId?: string;
34
+ /** openai-compatible: the endpoint. */
35
+ baseUrl?: string;
36
+ /** openai-compatible: its name and default model. */
37
+ label?: string;
38
+ defaultModel?: string;
39
+ };
40
+ /**
41
+ * The adapters of the providers connected on the app's server (CR-102): the same ones
42
+ * createAIProviderRegistry() builds, with a placeholder where the key goes. Each adapter writes it
43
+ * exactly where it would write the key; the app's proxy swaps it for the key of `scope` with
44
+ * resolveServerCredentials() from `@llmnative/react/server`. Needs the proxy: proxyFetch never
45
+ * sends a placeholder straight to a provider.
46
+ */
47
+ export declare const createServerCredentialAIRegistry: (connections: ServerAIProviderConnection[], { scope }: {
48
+ scope: string;
49
+ }) => Record<string, AIProviderAdapter>;
@@ -0,0 +1,31 @@
1
+ import { type ServerAIConnection } from './serverCredentials';
2
+ export { AI_PROVIDER_HOSTS, isAIProviderTarget, serverCredential, SERVER_CREDENTIAL_PREFIX } from './serverCredentials';
3
+ export type { ServerAIConnection } from './serverCredentials';
4
+ /** The connection of a provider as the server keeps it, key included. */
5
+ export type ServerAISecret = ServerAIConnection & {
6
+ apiKey: string;
7
+ };
8
+ /** The key of `provider` for `scope` (e.g. a company), or nothing when it is not connected. */
9
+ export type ServerCredentialLookup = (scope: string, provider: string) => ServerAISecret | undefined | Promise<ServerAISecret | undefined>;
10
+ export type ServerCredentialRequest = {
11
+ url: string;
12
+ headers: Record<string, string>;
13
+ };
14
+ export type ServerCredentialResolution = {
15
+ ok: true;
16
+ url: string;
17
+ headers: Record<string, string>;
18
+ }
19
+ /** 404: the provider is not connected for that scope; 403: the target is not a host of that provider. */
20
+ | {
21
+ ok: false;
22
+ status: 403 | 404;
23
+ error: 'unknown-credential' | 'forbidden-target';
24
+ provider: string;
25
+ };
26
+ /**
27
+ * Swaps every server-credential placeholder of a proxied request (headers and URL) for the key it
28
+ * stands for. The key only goes to a host of its provider: any other target is refused, so a
29
+ * placeholder can never carry a key elsewhere. A request without placeholders passes unchanged.
30
+ */
31
+ export declare function resolveServerCredentials(request: ServerCredentialRequest, lookup: ServerCredentialLookup): Promise<ServerCredentialResolution>;
@@ -0,0 +1,22 @@
1
+ export declare const SERVER_CREDENTIAL_PREFIX = "llmnative-server-credential";
2
+ /** A provider connected on the server: its id, and what its URLs are built from. Never the key. */
3
+ export type ServerAIConnection = {
4
+ id: string;
5
+ /** Cloudflare: the account in every Workers AI URL. */
6
+ accountId?: string;
7
+ /** openai-compatible: the endpoint. */
8
+ baseUrl?: string;
9
+ };
10
+ /** The placeholder an adapter writes where the key goes: `llmnative-server-credential:<scope>:<provider>`. */
11
+ export declare const serverCredential: (scope: string, provider: string) => string;
12
+ /** Every placeholder in `text`, as scope and provider. */
13
+ export declare const findServerCredentials: (text: string) => Array<{
14
+ placeholder: string;
15
+ scope: string;
16
+ provider: string;
17
+ }>;
18
+ export declare const containsServerCredential: (text: string) => boolean;
19
+ /** The fixed API host of each built-in provider (openai-compatible and Cloudflare depend on the connection). */
20
+ export declare const AI_PROVIDER_HOSTS: Readonly<Record<string, string>>;
21
+ /** Whether `target` is an API URL of the provider of `connection`: where its key may go. */
22
+ export declare const isAIProviderTarget: (connection: ServerAIConnection, target: URL) => boolean;
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from 'vite';
2
+ import { type ServerCredentialLookup } from '../ai/server';
2
3
  export declare const PROXY_PATH = "/api/proxy";
3
4
  /** Logging file di richiesta/risposta LLM — SOLO dev, mai attivo di default. Un file per
4
5
  * conversazione: il client (useAgent, CMS) genera un `logId` stabile per l'intera sessione e
@@ -10,4 +11,6 @@ export interface ProxyLogOptions {
10
11
  * il CMS) o assoluta. Creata se assente. */
11
12
  dir: string;
12
13
  }
13
- export declare const createProxyPlugin: (route?: string, logOptions?: ProxyLogOptions) => Plugin;
14
+ /** `credentials`: the keys of the providers held by the server (CR-102), for the adapters built
15
+ * with createServerCredentialAIRegistry(). */
16
+ export declare const createProxyPlugin: (route?: string, logOptions?: ProxyLogOptions, credentials?: ServerCredentialLookup) => Plugin;
package/dist/vite.js CHANGED
@@ -1,3 +1,3 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./logHeader-CUsdmGAo.js");let t=require("node:fs/promises"),n=require("node:path");n=e.r(n);var r=`x-llmnative-proxy`,i=`/api/proxy`,a=async e=>{let t=[];for await(let n of e)t.push(Buffer.isBuffer(n)?n:Buffer.from(n));return t.length===0?void 0:Buffer.concat(t)};function o(e){return e.replace(/[^a-zA-Z0-9_-]/g,``).slice(0,128)||`unknown`}function s(e){if(!e.trim())return`(empty body)`;try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}async function c(e,r,i,a,c,l){try{await(0,t.mkdir)(e.dir,{recursive:!0});let u=[``,`=`.repeat(80),`Turn @ ${new Date().toISOString()}`,`Target: ${i}`,`--- REQUEST BODY ---`,s(a),`--- RESPONSE BODY (status ${c}) ---`,s(l),`=`.repeat(80),``].join(`
2
- `);await(0,t.appendFile)(n.default.join(e.dir,`${o(r)}.txt`),u,`utf-8`)}catch(e){console.warn(`[llmnative-dev-proxy] Failed to write LLM log:`,e)}}var l=async(t,n,i,o)=>{let s=new URL(t.url||`/`,`http://localhost`).searchParams.get(`url`);if(t.headers[r]!==`1`){n.statusCode=403,n.setHeader(`Content-Type`,`application/json`),n.end(JSON.stringify({error:`Proxy access denied.`}));return}if(!s){n.statusCode=400,n.setHeader(`Content-Type`,`application/json`),n.end(JSON.stringify({error:`Missing "url" query parameter.`}));return}try{let l=new URL(s);if(l.pathname.startsWith(i)&&[`localhost`,`127.0.0.1`].includes(l.hostname)){n.statusCode=508,n.setHeader(`Content-Type`,`application/json`),n.end(JSON.stringify({error:`Recursive proxy target detected.`,target:s}));return}let u=await a(t),d=t.headers[e.t],f=new Headers,p=new Set([r,e.t,`host`,`origin`,`referer`,`content-length`,`transfer-encoding`,`connection`]);Object.entries(t.headers).forEach(([e,t])=>{t&&!p.has(e.toLowerCase())&&f.set(e,Array.isArray(t)?t.join(`, `):t)});let m=await fetch(s,{method:t.method||`GET`,headers:f,body:u&&![`GET`,`HEAD`].includes((t.method||`GET`).toUpperCase())?Uint8Array.from(u):void 0,redirect:`follow`}),h=Buffer.from(await m.arrayBuffer());o?.enabled&&typeof d==`string`&&d&&c(o,d,s,u?.toString(`utf-8`)??``,m.status,h.toString(`utf-8`)),n.statusCode=m.status,m.headers.forEach((e,t)=>{t.toLowerCase()!==`content-encoding`&&n.setHeader(t,e)}),n.end(h)}catch(e){n.statusCode=502,n.setHeader(`Content-Type`,`application/json`),n.end(JSON.stringify({error:e instanceof Error?e.message:`Proxy request failed.`}))}},u=(e=i,t)=>({name:`llmnative-dev-proxy`,configureServer(n){n.middlewares.use(e,(n,r)=>{l(n,r,e,t)})},configurePreviewServer(n){n.middlewares.use(e,(n,r)=>{l(n,r,e,t)})}});exports.PROXY_PATH=i,exports.createProxyPlugin=u;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./logHeader-CUsdmGAo.js"),t=require("./server.js");let n=require("node:fs/promises"),r=require("node:path");r=e.r(r);var i=`x-llmnative-proxy`,a=`/api/proxy`,o=async e=>{let t=[];for await(let n of e)t.push(Buffer.isBuffer(n)?n:Buffer.from(n));return t.length===0?void 0:Buffer.concat(t)};function s(e){return e.replace(/[^a-zA-Z0-9_-]/g,``).slice(0,128)||`unknown`}function c(e){if(!e.trim())return`(empty body)`;try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}async function l(e,t,i,a,o,l){try{await(0,n.mkdir)(e.dir,{recursive:!0});let u=[``,`=`.repeat(80),`Turn @ ${new Date().toISOString()}`,`Target: ${i}`,`--- REQUEST BODY ---`,c(a),`--- RESPONSE BODY (status ${o}) ---`,c(l),`=`.repeat(80),``].join(`
2
+ `);await(0,n.appendFile)(r.default.join(e.dir,`${s(t)}.txt`),u,`utf-8`)}catch(e){console.warn(`[llmnative-dev-proxy] Failed to write LLM log:`,e)}}var u=async(n,r,a,s,c)=>{let u=new URL(n.url||`/`,`http://localhost`).searchParams.get(`url`);if(n.headers[i]!==`1`){r.statusCode=403,r.setHeader(`Content-Type`,`application/json`),r.end(JSON.stringify({error:`Proxy access denied.`}));return}if(!u){r.statusCode=400,r.setHeader(`Content-Type`,`application/json`),r.end(JSON.stringify({error:`Missing "url" query parameter.`}));return}try{let d=new URL(u);if(d.pathname.startsWith(a)&&[`localhost`,`127.0.0.1`].includes(d.hostname)){r.statusCode=508,r.setHeader(`Content-Type`,`application/json`),r.end(JSON.stringify({error:`Recursive proxy target detected.`,target:u}));return}let f=await o(n),p=n.headers[e.t],m=new Headers,h=new Set([i,e.t,`host`,`origin`,`referer`,`content-length`,`transfer-encoding`,`connection`]);Object.entries(n.headers).forEach(([e,t])=>{t&&!h.has(e.toLowerCase())&&m.set(e,Array.isArray(t)?t.join(`, `):t)});let g=await t.resolveServerCredentials({url:u,headers:Object.fromEntries(m)},c??(()=>void 0));if(!g.ok){r.statusCode=g.status,r.setHeader(`Content-Type`,`application/json`),r.end(JSON.stringify({error:g.error,provider:g.provider}));return}let _=await fetch(g.url,{method:n.method||`GET`,headers:new Headers(g.headers),body:f&&![`GET`,`HEAD`].includes((n.method||`GET`).toUpperCase())?Uint8Array.from(f):void 0,redirect:`follow`}),v=Buffer.from(await _.arrayBuffer());s?.enabled&&typeof p==`string`&&p&&l(s,p,u,f?.toString(`utf-8`)??``,_.status,v.toString(`utf-8`)),r.statusCode=_.status,_.headers.forEach((e,t)=>{t.toLowerCase()!==`content-encoding`&&r.setHeader(t,e)}),r.end(v)}catch(e){r.statusCode=502,r.setHeader(`Content-Type`,`application/json`),r.end(JSON.stringify({error:e instanceof Error?e.message:`Proxy request failed.`}))}},d=(e=a,t,n)=>({name:`llmnative-dev-proxy`,configureServer(r){r.middlewares.use(e,(r,i)=>{u(r,i,e,t,n)})},configurePreviewServer(r){r.middlewares.use(e,(r,i)=>{u(r,i,e,t,n)})}});exports.PROXY_PATH=a,exports.createProxyPlugin=d;
3
3
  //# sourceMappingURL=vite.js.map
package/dist/vite.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.js","names":["mkdir","appendFile","path","LLM_LOG_ID_HEADER","require_logHeader","node_fs_promises","node_path","PROXY_CALLER_HEADER","PROXY_PATH","readRequestBody","chunks","chunk","req","sanitizeLogId","logId","prettyJsonOrRaw","text","appendLogBlock","logOptions","block","target","requestBody","status","responseBody","error","writeProxyResponse","res","targetUrl","route","body","headers","skipHeaders","key","value","upstream","responseBuffer","createProxyPlugin","server"],"sources":["../src/providers/proxy/vite.ts"],"sourcesContent":["import { appendFile, mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Plugin } from 'vite';\nimport { LLM_LOG_ID_HEADER } from './logHeader';\n\nconst PROXY_CALLER_HEADER = 'x-llmnative-proxy';\nexport const PROXY_PATH = '/api/proxy';\n\n/** Logging file di richiesta/risposta LLM — SOLO dev, mai attivo di default. Un file per\n * conversazione: il client (useAgent, CMS) genera un `logId` stabile per l'intera sessione e\n * lo manda su ogni chiamata come header `x-llmnative-log-id`; il proxy accoda ogni turno allo\n * stesso file, non lo sovrascrive. Vedi AICompleteRequest.logId per il lato client. */\nexport interface ProxyLogOptions {\n enabled: boolean;\n /** Cartella di destinazione, relativa alla cwd del dev server (il progetto consumer, es.\n * il CMS) o assoluta. Creata se assente. */\n dir: string;\n}\n\nconst readRequestBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return chunks.length === 0 ? undefined : Buffer.concat(chunks);\n};\n\n/** Solo caratteri sicuri per un nome file — l'id è generato da noi (crypto.randomUUID() lato\n * client), ma niente path traversal per costruzione anche se un client si comportasse male. */\nfunction sanitizeLogId(logId: string): string {\n return logId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'unknown';\n}\n\nfunction prettyJsonOrRaw(text: string): string {\n if (!text.trim()) return '(empty body)';\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch {\n return text;\n }\n}\n\nasync function appendLogBlock(logOptions: ProxyLogOptions, logId: string, target: string, requestBody: string, status: number, responseBody: string) {\n try {\n await mkdir(logOptions.dir, { recursive: true });\n const block = [\n '',\n '='.repeat(80),\n `Turn @ ${new Date().toISOString()}`,\n `Target: ${target}`,\n '--- REQUEST BODY ---',\n prettyJsonOrRaw(requestBody),\n `--- RESPONSE BODY (status ${status}) ---`,\n prettyJsonOrRaw(responseBody),\n '='.repeat(80),\n '',\n ].join('\\n');\n await appendFile(path.join(logOptions.dir, `${sanitizeLogId(logId)}.txt`), block, 'utf-8');\n } catch (error) {\n // Un log fallito non deve mai far fallire la vera richiesta — solo un avviso in console.\n console.warn('[llmnative-dev-proxy] Failed to write LLM log:', error);\n }\n}\n\nconst writeProxyResponse = async (req: IncomingMessage, res: ServerResponse, route: string, logOptions?: ProxyLogOptions) => {\n const requestUrl = new URL(req.url || '/', 'http://localhost');\n const target = requestUrl.searchParams.get('url');\n\n if (req.headers[PROXY_CALLER_HEADER] !== '1') {\n res.statusCode = 403;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'Proxy access denied.' }));\n return;\n }\n\n if (!target) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'Missing \"url\" query parameter.' }));\n return;\n }\n\n try {\n const targetUrl = new URL(target);\n if (targetUrl.pathname.startsWith(route) && ['localhost', '127.0.0.1'].includes(targetUrl.hostname)) {\n res.statusCode = 508;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'Recursive proxy target detected.', target }));\n return;\n }\n\n const body = await readRequestBody(req);\n const logId = req.headers[LLM_LOG_ID_HEADER];\n const headers = new Headers();\n // L'header di log è consumato QUI, mai inoltrato al provider upstream (stesso\n // trattamento del PROXY_CALLER_HEADER) — è un dettaglio nostro, non dell'API esterna.\n const skipHeaders = new Set([PROXY_CALLER_HEADER, LLM_LOG_ID_HEADER, 'host', 'origin', 'referer', 'content-length', 'transfer-encoding', 'connection']);\n\n Object.entries(req.headers).forEach(([key, value]) => {\n if (!value || skipHeaders.has(key.toLowerCase())) return;\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n });\n\n const upstream = await fetch(target, {\n method: req.method || 'GET',\n headers,\n body: body && !['GET', 'HEAD'].includes((req.method || 'GET').toUpperCase())\n ? Uint8Array.from(body)\n : undefined,\n redirect: 'follow',\n });\n\n const responseBuffer = Buffer.from(await upstream.arrayBuffer());\n\n if (logOptions?.enabled && typeof logId === 'string' && logId) {\n void appendLogBlock(logOptions, logId, target, body?.toString('utf-8') ?? '', upstream.status, responseBuffer.toString('utf-8'));\n }\n\n res.statusCode = upstream.status;\n upstream.headers.forEach((value, key) => {\n if (key.toLowerCase() === 'content-encoding') return;\n res.setHeader(key, value);\n });\n res.end(responseBuffer);\n } catch (error) {\n res.statusCode = 502;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Proxy request failed.' }));\n }\n};\n\nexport const createProxyPlugin = (route = PROXY_PATH, logOptions?: ProxyLogOptions): Plugin => ({\n name: 'llmnative-dev-proxy',\n configureServer(server) {\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions); });\n },\n configurePreviewServer(server) {\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions); });\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"vite.js","names":["mkdir","appendFile","path","LLM_LOG_ID_HEADER","resolveServerCredentials","require_logHeader","require_server","node_fs_promises","node_path","PROXY_CALLER_HEADER","PROXY_PATH","readRequestBody","chunks","chunk","req","sanitizeLogId","logId","prettyJsonOrRaw","text","appendLogBlock","logOptions","block","target","requestBody","status","responseBody","error","writeProxyResponse","res","targetUrl","route","body","headers","skipHeaders","key","value","resolved","credentials","upstream","responseBuffer","createProxyPlugin","server"],"sources":["../src/providers/proxy/vite.ts"],"sourcesContent":["import { appendFile, mkdir } from 'node:fs/promises';\r\nimport path from 'node:path';\r\nimport type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport type { Plugin } from 'vite';\r\nimport { LLM_LOG_ID_HEADER } from './logHeader';\r\nimport { resolveServerCredentials, type ServerCredentialLookup } from '../ai/server';\r\n\r\nconst PROXY_CALLER_HEADER = 'x-llmnative-proxy';\r\nexport const PROXY_PATH = '/api/proxy';\r\n\r\n/** Logging file di richiesta/risposta LLM — SOLO dev, mai attivo di default. Un file per\r\n * conversazione: il client (useAgent, CMS) genera un `logId` stabile per l'intera sessione e\r\n * lo manda su ogni chiamata come header `x-llmnative-log-id`; il proxy accoda ogni turno allo\r\n * stesso file, non lo sovrascrive. Vedi AICompleteRequest.logId per il lato client. */\r\nexport interface ProxyLogOptions {\r\n enabled: boolean;\r\n /** Cartella di destinazione, relativa alla cwd del dev server (il progetto consumer, es.\r\n * il CMS) o assoluta. Creata se assente. */\r\n dir: string;\r\n}\r\n\r\nconst readRequestBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {\r\n const chunks: Buffer[] = [];\r\n for await (const chunk of req) {\r\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\r\n }\r\n return chunks.length === 0 ? undefined : Buffer.concat(chunks);\r\n};\r\n\r\n/** Solo caratteri sicuri per un nome file — l'id è generato da noi (crypto.randomUUID() lato\r\n * client), ma niente path traversal per costruzione anche se un client si comportasse male. */\r\nfunction sanitizeLogId(logId: string): string {\r\n return logId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'unknown';\r\n}\r\n\r\nfunction prettyJsonOrRaw(text: string): string {\r\n if (!text.trim()) return '(empty body)';\r\n try {\r\n return JSON.stringify(JSON.parse(text), null, 2);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n\r\nasync function appendLogBlock(logOptions: ProxyLogOptions, logId: string, target: string, requestBody: string, status: number, responseBody: string) {\r\n try {\r\n await mkdir(logOptions.dir, { recursive: true });\r\n const block = [\r\n '',\r\n '='.repeat(80),\r\n `Turn @ ${new Date().toISOString()}`,\r\n `Target: ${target}`,\r\n '--- REQUEST BODY ---',\r\n prettyJsonOrRaw(requestBody),\r\n `--- RESPONSE BODY (status ${status}) ---`,\r\n prettyJsonOrRaw(responseBody),\r\n '='.repeat(80),\r\n '',\r\n ].join('\\n');\r\n await appendFile(path.join(logOptions.dir, `${sanitizeLogId(logId)}.txt`), block, 'utf-8');\r\n } catch (error) {\r\n // Un log fallito non deve mai far fallire la vera richiesta — solo un avviso in console.\r\n console.warn('[llmnative-dev-proxy] Failed to write LLM log:', error);\r\n }\r\n}\r\n\r\nconst writeProxyResponse = async (req: IncomingMessage, res: ServerResponse, route: string, logOptions?: ProxyLogOptions, credentials?: ServerCredentialLookup) => {\r\n const requestUrl = new URL(req.url || '/', 'http://localhost');\r\n const target = requestUrl.searchParams.get('url');\r\n\r\n if (req.headers[PROXY_CALLER_HEADER] !== '1') {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Proxy access denied.' }));\r\n return;\r\n }\r\n\r\n if (!target) {\r\n res.statusCode = 400;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Missing \"url\" query parameter.' }));\r\n return;\r\n }\r\n\r\n try {\r\n const targetUrl = new URL(target);\r\n if (targetUrl.pathname.startsWith(route) && ['localhost', '127.0.0.1'].includes(targetUrl.hostname)) {\r\n res.statusCode = 508;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Recursive proxy target detected.', target }));\r\n return;\r\n }\r\n\r\n const body = await readRequestBody(req);\r\n const logId = req.headers[LLM_LOG_ID_HEADER];\r\n const headers = new Headers();\r\n // L'header di log è consumato QUI, mai inoltrato al provider upstream (stesso\r\n // trattamento del PROXY_CALLER_HEADER) — è un dettaglio nostro, non dell'API esterna.\r\n const skipHeaders = new Set([PROXY_CALLER_HEADER, LLM_LOG_ID_HEADER, 'host', 'origin', 'referer', 'content-length', 'transfer-encoding', 'connection']);\r\n\r\n Object.entries(req.headers).forEach(([key, value]) => {\r\n if (!value || skipHeaders.has(key.toLowerCase())) return;\r\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\r\n });\r\n\r\n // Server-held AI credentials (CR-102): the placeholder becomes the key, only towards its\r\n // provider. Without a lookup a placeholder finds no key and the request is refused.\r\n const resolved = await resolveServerCredentials({ url: target, headers: Object.fromEntries(headers) }, credentials ?? (() => undefined));\r\n if (!resolved.ok) {\r\n res.statusCode = resolved.status;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: resolved.error, provider: resolved.provider }));\r\n return;\r\n }\r\n\r\n const upstream = await fetch(resolved.url, {\r\n method: req.method || 'GET',\r\n headers: new Headers(resolved.headers),\r\n body: body && !['GET', 'HEAD'].includes((req.method || 'GET').toUpperCase())\r\n ? Uint8Array.from(body)\r\n : undefined,\r\n redirect: 'follow',\r\n });\r\n\r\n const responseBuffer = Buffer.from(await upstream.arrayBuffer());\r\n\r\n if (logOptions?.enabled && typeof logId === 'string' && logId) {\r\n void appendLogBlock(logOptions, logId, target, body?.toString('utf-8') ?? '', upstream.status, responseBuffer.toString('utf-8'));\r\n }\r\n\r\n res.statusCode = upstream.status;\r\n upstream.headers.forEach((value, key) => {\r\n if (key.toLowerCase() === 'content-encoding') return;\r\n res.setHeader(key, value);\r\n });\r\n res.end(responseBuffer);\r\n } catch (error) {\r\n res.statusCode = 502;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Proxy request failed.' }));\r\n }\r\n};\r\n\r\n/** `credentials`: the keys of the providers held by the server (CR-102), for the adapters built\r\n * with createServerCredentialAIRegistry(). */\r\nexport const createProxyPlugin = (route = PROXY_PATH, logOptions?: ProxyLogOptions, credentials?: ServerCredentialLookup): Plugin => ({\r\n name: 'llmnative-dev-proxy',\r\n configureServer(server) {\r\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions, credentials); });\r\n },\r\n configurePreviewServer(server) {\r\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions, credentials); });\r\n },\r\n});\r\n"],"mappings":"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"}
package/dist/vite.mjs CHANGED
@@ -1,16 +1,17 @@
1
1
  import { t as e } from "./logHeader-Busbc8OO.mjs";
2
- import { appendFile as t, mkdir as n } from "node:fs/promises";
3
- import r from "node:path";
2
+ import { resolveServerCredentials as t } from "./server.mjs";
3
+ import { appendFile as n, mkdir as r } from "node:fs/promises";
4
+ import i from "node:path";
4
5
  //#region src/providers/proxy/vite.ts
5
- var i = "x-llmnative-proxy", a = "/api/proxy", o = async (e) => {
6
+ var a = "x-llmnative-proxy", o = "/api/proxy", s = async (e) => {
6
7
  let t = [];
7
8
  for await (let n of e) t.push(Buffer.isBuffer(n) ? n : Buffer.from(n));
8
9
  return t.length === 0 ? void 0 : Buffer.concat(t);
9
10
  };
10
- function s(e) {
11
+ function c(e) {
11
12
  return e.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 128) || "unknown";
12
13
  }
13
- function c(e) {
14
+ function l(e) {
14
15
  if (!e.trim()) return "(empty body)";
15
16
  try {
16
17
  return JSON.stringify(JSON.parse(e), null, 2);
@@ -18,47 +19,47 @@ function c(e) {
18
19
  return e;
19
20
  }
20
21
  }
21
- async function l(e, i, a, o, l, u) {
22
+ async function u(e, t, a, o, s, u) {
22
23
  try {
23
- await n(e.dir, { recursive: !0 });
24
+ await r(e.dir, { recursive: !0 });
24
25
  let d = [
25
26
  "",
26
27
  "=".repeat(80),
27
28
  `Turn @ ${(/* @__PURE__ */ new Date()).toISOString()}`,
28
29
  `Target: ${a}`,
29
30
  "--- REQUEST BODY ---",
30
- c(o),
31
- `--- RESPONSE BODY (status ${l}) ---`,
32
- c(u),
31
+ l(o),
32
+ `--- RESPONSE BODY (status ${s}) ---`,
33
+ l(u),
33
34
  "=".repeat(80),
34
35
  ""
35
36
  ].join("\n");
36
- await t(r.join(e.dir, `${s(i)}.txt`), d, "utf-8");
37
+ await n(i.join(e.dir, `${c(t)}.txt`), d, "utf-8");
37
38
  } catch (e) {
38
39
  console.warn("[llmnative-dev-proxy] Failed to write LLM log:", e);
39
40
  }
40
41
  }
41
- var u = async (t, n, r, a) => {
42
- let s = new URL(t.url || "/", "http://localhost").searchParams.get("url");
43
- if (t.headers[i] !== "1") {
44
- n.statusCode = 403, n.setHeader("Content-Type", "application/json"), n.end(JSON.stringify({ error: "Proxy access denied." }));
42
+ var d = async (n, r, i, o, c) => {
43
+ let l = new URL(n.url || "/", "http://localhost").searchParams.get("url");
44
+ if (n.headers[a] !== "1") {
45
+ r.statusCode = 403, r.setHeader("Content-Type", "application/json"), r.end(JSON.stringify({ error: "Proxy access denied." }));
45
46
  return;
46
47
  }
47
- if (!s) {
48
- n.statusCode = 400, n.setHeader("Content-Type", "application/json"), n.end(JSON.stringify({ error: "Missing \"url\" query parameter." }));
48
+ if (!l) {
49
+ r.statusCode = 400, r.setHeader("Content-Type", "application/json"), r.end(JSON.stringify({ error: "Missing \"url\" query parameter." }));
49
50
  return;
50
51
  }
51
52
  try {
52
- let c = new URL(s);
53
- if (c.pathname.startsWith(r) && ["localhost", "127.0.0.1"].includes(c.hostname)) {
54
- n.statusCode = 508, n.setHeader("Content-Type", "application/json"), n.end(JSON.stringify({
53
+ let d = new URL(l);
54
+ if (d.pathname.startsWith(i) && ["localhost", "127.0.0.1"].includes(d.hostname)) {
55
+ r.statusCode = 508, r.setHeader("Content-Type", "application/json"), r.end(JSON.stringify({
55
56
  error: "Recursive proxy target detected.",
56
- target: s
57
+ target: l
57
58
  }));
58
59
  return;
59
60
  }
60
- let u = await o(t), d = t.headers[e], f = new Headers(), p = /* @__PURE__ */ new Set([
61
- i,
61
+ let f = await s(n), p = n.headers[e], m = new Headers(), h = /* @__PURE__ */ new Set([
62
+ a,
62
63
  e,
63
64
  "host",
64
65
  "origin",
@@ -67,35 +68,46 @@ var u = async (t, n, r, a) => {
67
68
  "transfer-encoding",
68
69
  "connection"
69
70
  ]);
70
- Object.entries(t.headers).forEach(([e, t]) => {
71
- t && !p.has(e.toLowerCase()) && f.set(e, Array.isArray(t) ? t.join(", ") : t);
71
+ Object.entries(n.headers).forEach(([e, t]) => {
72
+ t && !h.has(e.toLowerCase()) && m.set(e, Array.isArray(t) ? t.join(", ") : t);
72
73
  });
73
- let m = await fetch(s, {
74
- method: t.method || "GET",
75
- headers: f,
76
- body: u && !["GET", "HEAD"].includes((t.method || "GET").toUpperCase()) ? Uint8Array.from(u) : void 0,
74
+ let g = await t({
75
+ url: l,
76
+ headers: Object.fromEntries(m)
77
+ }, c ?? (() => void 0));
78
+ if (!g.ok) {
79
+ r.statusCode = g.status, r.setHeader("Content-Type", "application/json"), r.end(JSON.stringify({
80
+ error: g.error,
81
+ provider: g.provider
82
+ }));
83
+ return;
84
+ }
85
+ let _ = await fetch(g.url, {
86
+ method: n.method || "GET",
87
+ headers: new Headers(g.headers),
88
+ body: f && !["GET", "HEAD"].includes((n.method || "GET").toUpperCase()) ? Uint8Array.from(f) : void 0,
77
89
  redirect: "follow"
78
- }), h = Buffer.from(await m.arrayBuffer());
79
- a?.enabled && typeof d == "string" && d && l(a, d, s, u?.toString("utf-8") ?? "", m.status, h.toString("utf-8")), n.statusCode = m.status, m.headers.forEach((e, t) => {
80
- t.toLowerCase() !== "content-encoding" && n.setHeader(t, e);
81
- }), n.end(h);
90
+ }), v = Buffer.from(await _.arrayBuffer());
91
+ o?.enabled && typeof p == "string" && p && u(o, p, l, f?.toString("utf-8") ?? "", _.status, v.toString("utf-8")), r.statusCode = _.status, _.headers.forEach((e, t) => {
92
+ t.toLowerCase() !== "content-encoding" && r.setHeader(t, e);
93
+ }), r.end(v);
82
94
  } catch (e) {
83
- n.statusCode = 502, n.setHeader("Content-Type", "application/json"), n.end(JSON.stringify({ error: e instanceof Error ? e.message : "Proxy request failed." }));
95
+ r.statusCode = 502, r.setHeader("Content-Type", "application/json"), r.end(JSON.stringify({ error: e instanceof Error ? e.message : "Proxy request failed." }));
84
96
  }
85
- }, d = (e = a, t) => ({
97
+ }, f = (e = o, t, n) => ({
86
98
  name: "llmnative-dev-proxy",
87
- configureServer(n) {
88
- n.middlewares.use(e, (n, r) => {
89
- u(n, r, e, t);
99
+ configureServer(r) {
100
+ r.middlewares.use(e, (r, i) => {
101
+ d(r, i, e, t, n);
90
102
  });
91
103
  },
92
- configurePreviewServer(n) {
93
- n.middlewares.use(e, (n, r) => {
94
- u(n, r, e, t);
104
+ configurePreviewServer(r) {
105
+ r.middlewares.use(e, (r, i) => {
106
+ d(r, i, e, t, n);
95
107
  });
96
108
  }
97
109
  });
98
110
  //#endregion
99
- export { a as PROXY_PATH, d as createProxyPlugin };
111
+ export { o as PROXY_PATH, f as createProxyPlugin };
100
112
 
101
113
  //# sourceMappingURL=vite.mjs.map
package/dist/vite.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"vite.mjs","names":["LLM_LOG_ID_HEADER","path","PROXY_CALLER_HEADER","PROXY_PATH","readRequestBody","chunks","chunk","req","sanitizeLogId","logId","prettyJsonOrRaw","text","appendLogBlock","mkdir","logOptions","block","target","requestBody","status","responseBody","appendFile","error","writeProxyResponse","res","targetUrl","route","body","headers","skipHeaders","key","value","upstream","responseBuffer","createProxyPlugin"],"sources":["../src/providers/proxy/vite.ts"],"sourcesContent":["import { appendFile, mkdir } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Plugin } from 'vite';\nimport { LLM_LOG_ID_HEADER } from './logHeader';\n\nconst PROXY_CALLER_HEADER = 'x-llmnative-proxy';\nexport const PROXY_PATH = '/api/proxy';\n\n/** Logging file di richiesta/risposta LLM — SOLO dev, mai attivo di default. Un file per\n * conversazione: il client (useAgent, CMS) genera un `logId` stabile per l'intera sessione e\n * lo manda su ogni chiamata come header `x-llmnative-log-id`; il proxy accoda ogni turno allo\n * stesso file, non lo sovrascrive. Vedi AICompleteRequest.logId per il lato client. */\nexport interface ProxyLogOptions {\n enabled: boolean;\n /** Cartella di destinazione, relativa alla cwd del dev server (il progetto consumer, es.\n * il CMS) o assoluta. Creata se assente. */\n dir: string;\n}\n\nconst readRequestBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n }\n return chunks.length === 0 ? undefined : Buffer.concat(chunks);\n};\n\n/** Solo caratteri sicuri per un nome file — l'id è generato da noi (crypto.randomUUID() lato\n * client), ma niente path traversal per costruzione anche se un client si comportasse male. */\nfunction sanitizeLogId(logId: string): string {\n return logId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'unknown';\n}\n\nfunction prettyJsonOrRaw(text: string): string {\n if (!text.trim()) return '(empty body)';\n try {\n return JSON.stringify(JSON.parse(text), null, 2);\n } catch {\n return text;\n }\n}\n\nasync function appendLogBlock(logOptions: ProxyLogOptions, logId: string, target: string, requestBody: string, status: number, responseBody: string) {\n try {\n await mkdir(logOptions.dir, { recursive: true });\n const block = [\n '',\n '='.repeat(80),\n `Turn @ ${new Date().toISOString()}`,\n `Target: ${target}`,\n '--- REQUEST BODY ---',\n prettyJsonOrRaw(requestBody),\n `--- RESPONSE BODY (status ${status}) ---`,\n prettyJsonOrRaw(responseBody),\n '='.repeat(80),\n '',\n ].join('\\n');\n await appendFile(path.join(logOptions.dir, `${sanitizeLogId(logId)}.txt`), block, 'utf-8');\n } catch (error) {\n // Un log fallito non deve mai far fallire la vera richiesta — solo un avviso in console.\n console.warn('[llmnative-dev-proxy] Failed to write LLM log:', error);\n }\n}\n\nconst writeProxyResponse = async (req: IncomingMessage, res: ServerResponse, route: string, logOptions?: ProxyLogOptions) => {\n const requestUrl = new URL(req.url || '/', 'http://localhost');\n const target = requestUrl.searchParams.get('url');\n\n if (req.headers[PROXY_CALLER_HEADER] !== '1') {\n res.statusCode = 403;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'Proxy access denied.' }));\n return;\n }\n\n if (!target) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'Missing \"url\" query parameter.' }));\n return;\n }\n\n try {\n const targetUrl = new URL(target);\n if (targetUrl.pathname.startsWith(route) && ['localhost', '127.0.0.1'].includes(targetUrl.hostname)) {\n res.statusCode = 508;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: 'Recursive proxy target detected.', target }));\n return;\n }\n\n const body = await readRequestBody(req);\n const logId = req.headers[LLM_LOG_ID_HEADER];\n const headers = new Headers();\n // L'header di log è consumato QUI, mai inoltrato al provider upstream (stesso\n // trattamento del PROXY_CALLER_HEADER) — è un dettaglio nostro, non dell'API esterna.\n const skipHeaders = new Set([PROXY_CALLER_HEADER, LLM_LOG_ID_HEADER, 'host', 'origin', 'referer', 'content-length', 'transfer-encoding', 'connection']);\n\n Object.entries(req.headers).forEach(([key, value]) => {\n if (!value || skipHeaders.has(key.toLowerCase())) return;\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n });\n\n const upstream = await fetch(target, {\n method: req.method || 'GET',\n headers,\n body: body && !['GET', 'HEAD'].includes((req.method || 'GET').toUpperCase())\n ? Uint8Array.from(body)\n : undefined,\n redirect: 'follow',\n });\n\n const responseBuffer = Buffer.from(await upstream.arrayBuffer());\n\n if (logOptions?.enabled && typeof logId === 'string' && logId) {\n void appendLogBlock(logOptions, logId, target, body?.toString('utf-8') ?? '', upstream.status, responseBuffer.toString('utf-8'));\n }\n\n res.statusCode = upstream.status;\n upstream.headers.forEach((value, key) => {\n if (key.toLowerCase() === 'content-encoding') return;\n res.setHeader(key, value);\n });\n res.end(responseBuffer);\n } catch (error) {\n res.statusCode = 502;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Proxy request failed.' }));\n }\n};\n\nexport const createProxyPlugin = (route = PROXY_PATH, logOptions?: ProxyLogOptions): Plugin => ({\n name: 'llmnative-dev-proxy',\n configureServer(server) {\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions); });\n },\n configurePreviewServer(server) {\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions); });\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"vite.mjs","names":["LLM_LOG_ID_HEADER","path","PROXY_CALLER_HEADER","PROXY_PATH","readRequestBody","chunks","chunk","req","sanitizeLogId","logId","prettyJsonOrRaw","text","appendLogBlock","mkdir","logOptions","block","target","requestBody","status","responseBody","appendFile","error","writeProxyResponse","res","targetUrl","route","body","headers","skipHeaders","key","value","resolved","resolveServerCredentials","credentials","upstream","responseBuffer","createProxyPlugin"],"sources":["../src/providers/proxy/vite.ts"],"sourcesContent":["import { appendFile, mkdir } from 'node:fs/promises';\r\nimport path from 'node:path';\r\nimport type { IncomingMessage, ServerResponse } from 'node:http';\r\nimport type { Plugin } from 'vite';\r\nimport { LLM_LOG_ID_HEADER } from './logHeader';\r\nimport { resolveServerCredentials, type ServerCredentialLookup } from '../ai/server';\r\n\r\nconst PROXY_CALLER_HEADER = 'x-llmnative-proxy';\r\nexport const PROXY_PATH = '/api/proxy';\r\n\r\n/** Logging file di richiesta/risposta LLM — SOLO dev, mai attivo di default. Un file per\r\n * conversazione: il client (useAgent, CMS) genera un `logId` stabile per l'intera sessione e\r\n * lo manda su ogni chiamata come header `x-llmnative-log-id`; il proxy accoda ogni turno allo\r\n * stesso file, non lo sovrascrive. Vedi AICompleteRequest.logId per il lato client. */\r\nexport interface ProxyLogOptions {\r\n enabled: boolean;\r\n /** Cartella di destinazione, relativa alla cwd del dev server (il progetto consumer, es.\r\n * il CMS) o assoluta. Creata se assente. */\r\n dir: string;\r\n}\r\n\r\nconst readRequestBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {\r\n const chunks: Buffer[] = [];\r\n for await (const chunk of req) {\r\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\r\n }\r\n return chunks.length === 0 ? undefined : Buffer.concat(chunks);\r\n};\r\n\r\n/** Solo caratteri sicuri per un nome file — l'id è generato da noi (crypto.randomUUID() lato\r\n * client), ma niente path traversal per costruzione anche se un client si comportasse male. */\r\nfunction sanitizeLogId(logId: string): string {\r\n return logId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 128) || 'unknown';\r\n}\r\n\r\nfunction prettyJsonOrRaw(text: string): string {\r\n if (!text.trim()) return '(empty body)';\r\n try {\r\n return JSON.stringify(JSON.parse(text), null, 2);\r\n } catch {\r\n return text;\r\n }\r\n}\r\n\r\nasync function appendLogBlock(logOptions: ProxyLogOptions, logId: string, target: string, requestBody: string, status: number, responseBody: string) {\r\n try {\r\n await mkdir(logOptions.dir, { recursive: true });\r\n const block = [\r\n '',\r\n '='.repeat(80),\r\n `Turn @ ${new Date().toISOString()}`,\r\n `Target: ${target}`,\r\n '--- REQUEST BODY ---',\r\n prettyJsonOrRaw(requestBody),\r\n `--- RESPONSE BODY (status ${status}) ---`,\r\n prettyJsonOrRaw(responseBody),\r\n '='.repeat(80),\r\n '',\r\n ].join('\\n');\r\n await appendFile(path.join(logOptions.dir, `${sanitizeLogId(logId)}.txt`), block, 'utf-8');\r\n } catch (error) {\r\n // Un log fallito non deve mai far fallire la vera richiesta — solo un avviso in console.\r\n console.warn('[llmnative-dev-proxy] Failed to write LLM log:', error);\r\n }\r\n}\r\n\r\nconst writeProxyResponse = async (req: IncomingMessage, res: ServerResponse, route: string, logOptions?: ProxyLogOptions, credentials?: ServerCredentialLookup) => {\r\n const requestUrl = new URL(req.url || '/', 'http://localhost');\r\n const target = requestUrl.searchParams.get('url');\r\n\r\n if (req.headers[PROXY_CALLER_HEADER] !== '1') {\r\n res.statusCode = 403;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Proxy access denied.' }));\r\n return;\r\n }\r\n\r\n if (!target) {\r\n res.statusCode = 400;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Missing \"url\" query parameter.' }));\r\n return;\r\n }\r\n\r\n try {\r\n const targetUrl = new URL(target);\r\n if (targetUrl.pathname.startsWith(route) && ['localhost', '127.0.0.1'].includes(targetUrl.hostname)) {\r\n res.statusCode = 508;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Recursive proxy target detected.', target }));\r\n return;\r\n }\r\n\r\n const body = await readRequestBody(req);\r\n const logId = req.headers[LLM_LOG_ID_HEADER];\r\n const headers = new Headers();\r\n // L'header di log è consumato QUI, mai inoltrato al provider upstream (stesso\r\n // trattamento del PROXY_CALLER_HEADER) — è un dettaglio nostro, non dell'API esterna.\r\n const skipHeaders = new Set([PROXY_CALLER_HEADER, LLM_LOG_ID_HEADER, 'host', 'origin', 'referer', 'content-length', 'transfer-encoding', 'connection']);\r\n\r\n Object.entries(req.headers).forEach(([key, value]) => {\r\n if (!value || skipHeaders.has(key.toLowerCase())) return;\r\n headers.set(key, Array.isArray(value) ? value.join(', ') : value);\r\n });\r\n\r\n // Server-held AI credentials (CR-102): the placeholder becomes the key, only towards its\r\n // provider. Without a lookup a placeholder finds no key and the request is refused.\r\n const resolved = await resolveServerCredentials({ url: target, headers: Object.fromEntries(headers) }, credentials ?? (() => undefined));\r\n if (!resolved.ok) {\r\n res.statusCode = resolved.status;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: resolved.error, provider: resolved.provider }));\r\n return;\r\n }\r\n\r\n const upstream = await fetch(resolved.url, {\r\n method: req.method || 'GET',\r\n headers: new Headers(resolved.headers),\r\n body: body && !['GET', 'HEAD'].includes((req.method || 'GET').toUpperCase())\r\n ? Uint8Array.from(body)\r\n : undefined,\r\n redirect: 'follow',\r\n });\r\n\r\n const responseBuffer = Buffer.from(await upstream.arrayBuffer());\r\n\r\n if (logOptions?.enabled && typeof logId === 'string' && logId) {\r\n void appendLogBlock(logOptions, logId, target, body?.toString('utf-8') ?? '', upstream.status, responseBuffer.toString('utf-8'));\r\n }\r\n\r\n res.statusCode = upstream.status;\r\n upstream.headers.forEach((value, key) => {\r\n if (key.toLowerCase() === 'content-encoding') return;\r\n res.setHeader(key, value);\r\n });\r\n res.end(responseBuffer);\r\n } catch (error) {\r\n res.statusCode = 502;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'Proxy request failed.' }));\r\n }\r\n};\r\n\r\n/** `credentials`: the keys of the providers held by the server (CR-102), for the adapters built\r\n * with createServerCredentialAIRegistry(). */\r\nexport const createProxyPlugin = (route = PROXY_PATH, logOptions?: ProxyLogOptions, credentials?: ServerCredentialLookup): Plugin => ({\r\n name: 'llmnative-dev-proxy',\r\n configureServer(server) {\r\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions, credentials); });\r\n },\r\n configurePreviewServer(server) {\r\n server.middlewares.use(route, (req, res) => { void writeProxyResponse(req, res, route, logOptions, credentials); });\r\n },\r\n});\r\n"],"mappings":"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"}