@llmnative/react 1.26.0 → 1.28.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/index.js +15 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4503 -4518
- package/dist/index.mjs.map +1 -1
- package/dist/modelIdentity-BEUaBIlb.js +2 -0
- package/dist/modelIdentity-BEUaBIlb.js.map +1 -0
- package/dist/modelIdentity-D93fYZOo.mjs +163 -0
- package/dist/modelIdentity-D93fYZOo.mjs.map +1 -0
- package/dist/server-BFQ0FDEk.js +2 -0
- package/dist/server-BFQ0FDEk.js.map +1 -0
- package/dist/server-BYI2zjyG.mjs +107 -0
- package/dist/server-BYI2zjyG.mjs.map +1 -0
- package/dist/server.js +1 -2
- package/dist/server.mjs +3 -41
- package/dist/types/src/Config.d.ts +5 -0
- package/dist/types/src/index.d.ts +5 -3
- package/dist/types/src/providers/ai/index.d.ts +2 -0
- package/dist/types/src/providers/ai/modelIdentity.d.ts +49 -0
- package/dist/types/src/providers/ai/server.d.ts +4 -0
- package/dist/types/src/providers/auth/github/GitHubAuthProvider.d.ts +38 -0
- package/dist/types/src/providers/auth/github/githubOAuth.d.ts +43 -0
- package/dist/types/src/providers/manifest.d.ts +3 -2
- package/dist/vite.js +2 -2
- package/dist/vite.js.map +1 -1
- package/dist/vite.mjs +1 -1
- package/llms-full.txt +39 -1
- package/package.json +1 -1
- package/dist/server.js.map +0 -1
- package/dist/server.mjs.map +0 -1
- package/dist/serverCredentials-PJbll0eu.mjs +0 -31
- package/dist/serverCredentials-PJbll0eu.mjs.map +0 -1
- package/dist/serverCredentials-lxpxgMsj.js +0 -2
- package/dist/serverCredentials-lxpxgMsj.js.map +0 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type ServerAIConnection } from './serverCredentials';
|
|
2
2
|
export { AI_PROVIDER_HOSTS, isAIProviderTarget, serverCredential, SERVER_CREDENTIAL_PREFIX } from './serverCredentials';
|
|
3
3
|
export type { ServerAIConnection } from './serverCredentials';
|
|
4
|
+
export { createGitHubOAuth, GitHubOAuthError } from '../auth/github/githubOAuth';
|
|
5
|
+
export type { GitHubOAuth, GitHubOAuthConfig, GitHubOAuthOrganization, GitHubOAuthUser } from '../auth/github/githubOAuth';
|
|
6
|
+
export { canonicalModelName, buildModelIdentityIndex, resolveModel, loadModelIdentityIndex } from './modelIdentity';
|
|
7
|
+
export type { ModelIdentity, ModelIdentityEntry, ModelIdentityIndex, ResolvedModel } from './modelIdentity';
|
|
4
8
|
/** The connection of a provider as the server keeps it, key included. */
|
|
5
9
|
export type ServerAISecret = ServerAIConnection & {
|
|
6
10
|
apiKey: string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { GitHubAuthConfig } from '../../../Config';
|
|
2
|
+
import { type ProviderConfigurationState } from '../../ProviderConfiguration';
|
|
3
|
+
import type { AuthProviderAdapter, AuthSignInOptions, UserProfile } from '../AuthProvider';
|
|
4
|
+
/** The profile the session endpoint of the app answers with (`<sessionUrl>/me`). */
|
|
5
|
+
export type GitHubSessionProfile = {
|
|
6
|
+
login: string;
|
|
7
|
+
name?: string | null;
|
|
8
|
+
avatarUrl?: string | null;
|
|
9
|
+
email?: string | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Sign-in with GitHub through a session held by the app's server (CR-104). GitHub needs the client
|
|
13
|
+
* secret to turn the authorization code into a token, so the exchange, the token and the session
|
|
14
|
+
* stay on the server (`createGitHubOAuth` from `@llmnative/react/server` does the GitHub part); the
|
|
15
|
+
* browser only ever sees the profile. Endpoints of the app, under `sessionUrl`:
|
|
16
|
+
* GET /login?returnTo=<path> → redirects to GitHub and back, sets the session cookie
|
|
17
|
+
* GET /me → the profile, 401 without a session
|
|
18
|
+
* POST /logout → ends the session
|
|
19
|
+
*/
|
|
20
|
+
export declare class GitHubAuthProvider implements AuthProviderAdapter {
|
|
21
|
+
private readonly config;
|
|
22
|
+
private user;
|
|
23
|
+
private readonly listeners;
|
|
24
|
+
private loading;
|
|
25
|
+
constructor(config: GitHubAuthConfig);
|
|
26
|
+
getConfigurationState(): ProviderConfigurationState;
|
|
27
|
+
isConfigured(): boolean;
|
|
28
|
+
private get base();
|
|
29
|
+
getUser(): UserProfile | null;
|
|
30
|
+
isAuthenticated(): boolean;
|
|
31
|
+
/** Asks the server who is signed in; the listeners hear of it when it changed. */
|
|
32
|
+
refresh(): Promise<UserProfile | null>;
|
|
33
|
+
/** Leaves for GitHub through the server, and comes back to the page it left from. */
|
|
34
|
+
signIn(options?: AuthSignInOptions): Promise<UserProfile | null>;
|
|
35
|
+
signOut(): Promise<void>;
|
|
36
|
+
onAuthChange(callback: (user: UserProfile | null) => void): () => void;
|
|
37
|
+
private setUser;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type GitHubOAuthConfig = {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
/** Default scopes, e.g. `['read:org']` to list the organizations of the person. */
|
|
5
|
+
scopes?: string[];
|
|
6
|
+
};
|
|
7
|
+
export type GitHubOAuthUser = {
|
|
8
|
+
login: string;
|
|
9
|
+
name: string | null;
|
|
10
|
+
avatarUrl: string | null;
|
|
11
|
+
email: string | null;
|
|
12
|
+
};
|
|
13
|
+
export type GitHubOAuthOrganization = {
|
|
14
|
+
login: string;
|
|
15
|
+
avatarUrl: string | null;
|
|
16
|
+
role: 'admin' | 'member';
|
|
17
|
+
};
|
|
18
|
+
export declare class GitHubOAuthError extends Error {
|
|
19
|
+
readonly status?: number | undefined;
|
|
20
|
+
constructor(message: string, status?: number | undefined);
|
|
21
|
+
}
|
|
22
|
+
export declare function createGitHubOAuth(config: GitHubOAuthConfig, fetchFn?: typeof fetch): {
|
|
23
|
+
/** Where the person authorizes the app. `state` ties the callback to this request; `codeChallenge` (S256) adds PKCE. */
|
|
24
|
+
authorizeUrl({ state, redirectUri, scopes, codeChallenge }: {
|
|
25
|
+
state: string;
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
scopes?: string[];
|
|
28
|
+
codeChallenge?: string;
|
|
29
|
+
}): string;
|
|
30
|
+
/** The access token for the code GitHub sent back. */
|
|
31
|
+
exchangeCode({ code, redirectUri, codeVerifier }: {
|
|
32
|
+
code: string;
|
|
33
|
+
redirectUri: string;
|
|
34
|
+
codeVerifier?: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
accessToken: string;
|
|
37
|
+
scope: string;
|
|
38
|
+
}>;
|
|
39
|
+
getUser(token: string): Promise<GitHubOAuthUser>;
|
|
40
|
+
/** The organizations the person belongs to, with their role (needs `read:org`). */
|
|
41
|
+
getOrganizations(token: string): Promise<GitHubOAuthOrganization[]>;
|
|
42
|
+
};
|
|
43
|
+
export type GitHubOAuth = ReturnType<typeof createGitHubOAuth>;
|
|
@@ -5,7 +5,7 @@ import type { DataProviderAdapter } from './data/DataProvider';
|
|
|
5
5
|
import type { StorageProviderAdapter } from './storage/StorageProvider';
|
|
6
6
|
import type { AuthProviderAdapter } from './auth/AuthProvider';
|
|
7
7
|
import type { EmailProviderAdapter } from './email/EmailProvider';
|
|
8
|
-
import type { AIConfig, DropboxConfig, FirebaseConfig, GoogleOAuth2, GoogleServiceAccount, SupabaseConfig } from '../Config';
|
|
8
|
+
import type { AIConfig, DropboxConfig, FirebaseConfig, GitHubAuthConfig, GoogleOAuth2, GoogleServiceAccount, SupabaseConfig } from '../Config';
|
|
9
9
|
import type { ProviderService } from './ProviderRegistryContext';
|
|
10
10
|
/** The 6 built-in categories — alias of ProviderService (ProviderRegistryContext.tsx), derived from ProviderAdapterMap so the two can't drift. */
|
|
11
11
|
export type ServiceCategory = ProviderService;
|
|
@@ -27,13 +27,14 @@ export type MockProviderConfig = {
|
|
|
27
27
|
export declare const FIREBASE_MANIFEST: DriverManifest<FirebaseConfig>;
|
|
28
28
|
export declare const GOOGLE_MANIFEST: DriverManifest<GoogleProviderConfig>;
|
|
29
29
|
export declare const DROPBOX_MANIFEST: DriverManifest<DropboxConfig>;
|
|
30
|
+
export declare const GITHUB_MANIFEST: DriverManifest<GitHubAuthConfig>;
|
|
30
31
|
export declare const SUPABASE_MANIFEST: DriverManifest<SupabaseProviderConfig>;
|
|
31
32
|
export declare const MOCK_MANIFEST: DriverManifest<MockProviderConfig>;
|
|
32
33
|
export declare const AI_MANIFEST: DriverManifest<AIConfig>;
|
|
33
34
|
export declare const PROVIDER_MANIFESTS: Record<string, DriverManifest<any>>;
|
|
34
35
|
export type DataDriverName = 'dbRealtime' | 'firestoreDb' | 'supabaseDb' | 'mock';
|
|
35
36
|
export type StorageDriverName = 'firestorage' | 'supabaseStorage';
|
|
36
|
-
export type AuthDriverName = 'googleAuth' | 'firebaseAuth' | 'dropboxAuth' | 'supabaseAuth';
|
|
37
|
+
export type AuthDriverName = 'googleAuth' | 'firebaseAuth' | 'dropboxAuth' | 'supabaseAuth' | 'githubAuth';
|
|
37
38
|
export type EmailDriverName = 'gmail';
|
|
38
39
|
export type AIDriverName = 'openai' | 'openrouter' | 'opencode' | 'openai-compatible' | 'deepseek' | 'gemini' | 'anthropic' | 'mistral' | 'cloudflare' | 'groq';
|
|
39
40
|
export type CredentialsDriverName = 'googleServiceAccount';
|
package/dist/vite.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
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.
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./logHeader-CUsdmGAo.js"),t=require("./server-BFQ0FDEk.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.t({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","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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"}
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./logHeader-Busbc8OO.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { t } from "./server-BYI2zjyG.mjs";
|
|
3
3
|
import { appendFile as n, mkdir as r } from "node:fs/promises";
|
|
4
4
|
import i from "node:path";
|
|
5
5
|
//#region src/providers/proxy/vite.ts
|
package/llms-full.txt
CHANGED
|
@@ -89,7 +89,7 @@ Built-in themes: `default`, `flat`, `cyber`.
|
|
|
89
89
|
|---|---|---|---|---|
|
|
90
90
|
| data | `DataProviderAdapter` | `mock`, `dbRealtime`, `firestoreDb`, `supabaseDb` | `providers.mock/firebase/supabase` | sì (fallback: mock) |
|
|
91
91
|
| storage | `StorageProviderAdapter` | `firestorage`, `supabaseStorage` | `providers.firebase/supabase` | no |
|
|
92
|
-
| auth | `AuthProviderAdapter` | `googleAuth`, `firebaseAuth`, `supabaseAuth`, `dropboxAuth` | `providers.google/firebase/supabase/dropbox` | sì (fallback: googleAuth) |
|
|
92
|
+
| auth | `AuthProviderAdapter` | `googleAuth`, `firebaseAuth`, `supabaseAuth`, `dropboxAuth`, `githubAuth` | `providers.google/firebase/supabase/dropbox/github` | sì (fallback: googleAuth) |
|
|
93
93
|
| email | `EmailProviderAdapter` | `gmail` | `providers.google` | no |
|
|
94
94
|
| ai | `AIProviderAdapter` | `openai`, `gemini`, `anthropic`, `deepseek`, `mistral`, `openrouter`, `opencode`, `cloudflare`, `groq` | `Config.tsx AIConfig` | no |
|
|
95
95
|
| credentials | `CredentialsAdapter` | `googleServiceAccount` | `providers.google.serviceAccount` | no |
|
|
@@ -108,6 +108,29 @@ const creds = useCredentialsProvider() // CredentialsAdapter | null
|
|
|
108
108
|
const supabase = useDataProvider('supabase')
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### GitHub (`githubAuth`, sessione sul server dell'app)
|
|
112
|
+
|
|
113
|
+
GitHub richiede il client secret per scambiare il codice: scambio, token e sessione stanno sul server
|
|
114
|
+
dell'app, il browser vede solo il profilo. Config `providers.github = { sessionUrl: '/api/auth/github' }`,
|
|
115
|
+
driver `auth: 'githubAuth'`; funziona con `AuthButton` (pulsante e `aspect="avatar"`) e
|
|
116
|
+
`useAuthProvider('githubAuth')`. Il server espone sotto `sessionUrl`:
|
|
117
|
+
|
|
118
|
+
| Rotta | Risposta |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `GET /login?returnTo=<path>[&scope=…]` | redirect a GitHub, poi di nuovo a `<path>` |
|
|
121
|
+
| `GET /me` | `{ login, name, avatarUrl, email }`, 401 senza sessione |
|
|
122
|
+
| `POST /logout` | chiude la sessione |
|
|
123
|
+
|
|
124
|
+
Lato server, da `@llmnative/react/server`:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const oauth = createGitHubOAuth({ clientId, clientSecret, scopes: ['read:org'] })
|
|
128
|
+
oauth.authorizeUrl({ state, redirectUri, codeChallenge }) // PKCE S256
|
|
129
|
+
const { accessToken } = await oauth.exchangeCode({ code, redirectUri, codeVerifier }) // GitHubOAuthError se rifiutato
|
|
130
|
+
await oauth.getUser(accessToken) // { login, name, avatarUrl, email }
|
|
131
|
+
await oauth.getOrganizations(accessToken) // [{ login, avatarUrl, role: 'admin' | 'member' }]
|
|
132
|
+
```
|
|
133
|
+
|
|
111
134
|
### Interfacce principali
|
|
112
135
|
|
|
113
136
|
```ts
|
|
@@ -962,6 +985,21 @@ class MyAIProvider implements AIProviderAdapter {
|
|
|
962
985
|
const registry = createAIProviderRegistry(definitions)
|
|
963
986
|
```
|
|
964
987
|
|
|
988
|
+
### Identità dei modelli indipendente dal provider (CR-103)
|
|
989
|
+
|
|
990
|
+
```ts
|
|
991
|
+
const index = await loadModelIdentityIndex() // models.dev, un fetch per sessione
|
|
992
|
+
canonicalModelName('DeepSeek V4.1 Flash (Alibaba Cloud)') // 'deepseek v4.1 flash'
|
|
993
|
+
index.models['deepseek v4.1 flash'] // { key, name, family?, providers: { openrouter: [{ id, name }], … } }
|
|
994
|
+
resolveModel(index, 'DeepSeek V4.1 Flash', ['openrouter', 'deepseek'])
|
|
995
|
+
// → { provider: 'openrouter', model: 'deepseek/deepseek-v4.1-flash', ref: 'openrouter/deepseek/deepseek-v4.1-flash' }
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
Il nome canonico toglie data, Free, Latest e host o regione tra parentesi; una modalità
|
|
999
|
+
("(Thinking)", "(Reasoning)", "(Fast)") resta. Per ogni provider l'id preferito è quello stabile (non
|
|
1000
|
+
alias `~`, non free tier, non datato). I provider sono le chiavi di models.dev, le stesse di opencode.
|
|
1001
|
+
Puro: anche da `@llmnative/react/server`.
|
|
1002
|
+
|
|
965
1003
|
### Chiavi tenute dal server (CR-102)
|
|
966
1004
|
|
|
967
1005
|
Per un'app che non mette mai le chiavi dei provider nel browser. Il browser conosce solo quali
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmnative/react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "LLM-native React framework for deterministic UI generation. Data-driven by default, schema-driven optional. Ultra-low token consumption — built for AI agents, not just humans.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
package/dist/server.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/server.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|