@proma-dev/sdk 0.2.0 → 0.3.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.cjs +18 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/index.js +18 -17
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +18 -17
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +8 -4
- package/dist/react/index.d.ts +8 -4
- package/dist/react/index.js +18 -17
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -447,10 +447,13 @@ var AiApi = class {
|
|
|
447
447
|
this.client = client;
|
|
448
448
|
}
|
|
449
449
|
/**
|
|
450
|
-
* Sends a chat request through the Proma AI gateway
|
|
450
|
+
* Sends a chat request through the Proma AI gateway.
|
|
451
451
|
* Credits are deducted automatically per token used.
|
|
452
452
|
* Requires scope: `ai:chat`
|
|
453
453
|
*
|
|
454
|
+
* Defaults to `anthropic` + `claude-3-5-haiku-latest`. Pass `provider` and
|
|
455
|
+
* `model` to route elsewhere (gemini / openrouter).
|
|
456
|
+
*
|
|
454
457
|
* Returns a streaming `Response` — iterate SSE chunks or use a helper library.
|
|
455
458
|
*
|
|
456
459
|
* @example
|
|
@@ -460,16 +463,9 @@ var AiApi = class {
|
|
|
460
463
|
* const reader = stream.body.getReader()
|
|
461
464
|
*/
|
|
462
465
|
async chat(options) {
|
|
463
|
-
var _a, _b;
|
|
464
466
|
const token = await this.client.requireAccessToken();
|
|
465
467
|
const params = Array.isArray(options) ? { messages: options } : options;
|
|
466
|
-
const provider
|
|
467
|
-
const model = (_b = params.model) != null ? _b : provider === "gemini" ? "gemini-2.0-flash" : "";
|
|
468
|
-
if (!model) {
|
|
469
|
-
throw new Error(
|
|
470
|
-
`model is required when provider is "${provider}" \u2014 pass e.g. { provider: "${provider}", model: "..." }`
|
|
471
|
-
);
|
|
472
|
-
}
|
|
468
|
+
const { provider, model } = resolveProviderAndModel(params);
|
|
473
469
|
const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {
|
|
474
470
|
method: "POST",
|
|
475
471
|
headers: {
|
|
@@ -569,15 +565,8 @@ var AiApi = class {
|
|
|
569
565
|
* })
|
|
570
566
|
*/
|
|
571
567
|
async chatStructured(options) {
|
|
572
|
-
var _a, _b;
|
|
573
568
|
const token = await this.client.requireAccessToken();
|
|
574
|
-
const provider
|
|
575
|
-
const model = (_b = options.model) != null ? _b : provider === "gemini" ? "gemini-2.0-flash" : "";
|
|
576
|
-
if (!model) {
|
|
577
|
-
throw new Error(
|
|
578
|
-
`model is required when provider is "${provider}" \u2014 pass e.g. { provider: "${provider}", model: "..." }`
|
|
579
|
-
);
|
|
580
|
-
}
|
|
569
|
+
const { provider, model } = resolveProviderAndModel(options);
|
|
581
570
|
const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {
|
|
582
571
|
method: "POST",
|
|
583
572
|
headers: {
|
|
@@ -604,6 +593,18 @@ var AiApi = class {
|
|
|
604
593
|
return payload.object;
|
|
605
594
|
}
|
|
606
595
|
};
|
|
596
|
+
function resolveProviderAndModel(options) {
|
|
597
|
+
var _a, _b;
|
|
598
|
+
const provider = (_a = options.provider) != null ? _a : "anthropic";
|
|
599
|
+
const defaultModel = provider === "anthropic" ? "claude-3-5-haiku-latest" : provider === "gemini" ? "gemini-2.0-flash" : "";
|
|
600
|
+
const model = (_b = options.model) != null ? _b : defaultModel;
|
|
601
|
+
if (!model) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`model is required when provider is "${provider}" \u2014 pass e.g. { provider: "${provider}", model: "..." }`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return { provider, model };
|
|
607
|
+
}
|
|
607
608
|
function stripJsonFences(text) {
|
|
608
609
|
const trimmed = text.trim();
|
|
609
610
|
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { PromaClient } from './client';\nexport { MemoryStorage } from './storage';\nexport {\n PromaError,\n InsufficientCreditsError,\n AppSpendingLimitExceededError,\n AppLimitNotSetError,\n} from './errors';\nexport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n ChatProvider,\n ChatStructuredOptions,\n JsonSchema,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n TokenStorage,\n UserInfo,\n} from './types';\n","// Structured errors thrown by SDK methods. Apps can `instanceof`-check these\n// to react gracefully (e.g. prompt the user to top up or raise a per-app cap).\n\nexport class PromaError extends Error {\n readonly status: number;\n readonly code: string;\n\n constructor(message: string, code: string, status: number) {\n super(message);\n this.name = 'PromaError';\n this.code = code;\n this.status = status;\n }\n}\n\nexport class InsufficientCreditsError extends PromaError {\n constructor() {\n super(\n 'The user has insufficient credits — prompt them to top up.',\n 'insufficient_credits',\n 402,\n );\n this.name = 'InsufficientCreditsError';\n }\n}\n\nexport class AppSpendingLimitExceededError extends PromaError {\n readonly monthlyLimitMicroCredits: number | null;\n readonly monthToDateSpendMicroCredits: number | null;\n\n constructor(\n monthlyLimitMicroCredits: number | null,\n monthToDateSpendMicroCredits: number | null,\n ) {\n super(\n 'This app has reached its monthly spending limit set by the user.',\n 'app_limit_exceeded',\n 403,\n );\n this.name = 'AppSpendingLimitExceededError';\n this.monthlyLimitMicroCredits = monthlyLimitMicroCredits;\n this.monthToDateSpendMicroCredits = monthToDateSpendMicroCredits;\n }\n}\n\nexport class AppLimitNotSetError extends PromaError {\n constructor() {\n super(\n 'The user has not set a spending limit for this app yet — direct them to /home/my-apps.',\n 'app_limit_not_set',\n 403,\n );\n this.name = 'AppLimitNotSetError';\n }\n}\n\ninterface GatewayErrorBody {\n error?: string;\n reason?: string;\n limit?: number;\n spent?: number;\n}\n\nexport function errorFromGatewayResponse(\n status: number,\n body: GatewayErrorBody,\n): PromaError {\n const code = body.error ?? body.reason ?? 'unknown_error';\n switch (code) {\n case 'insufficient_credits':\n return new InsufficientCreditsError();\n case 'app_limit_exceeded':\n return new AppSpendingLimitExceededError(\n body.limit ?? null,\n body.spent ?? null,\n );\n case 'app_limit_not_set':\n return new AppLimitNotSetError();\n default:\n return new PromaError(\n body.error ?? `Gateway error (${status})`,\n code,\n status,\n );\n }\n}\n","/**\n * PKCE helpers — browser + Node 18+ compatible via SubtleCrypto.\n */\n\nconst PKCE_STORAGE_KEY = 'proma_code_verifier';\n\n/**\n * Generates a cryptographically random code_verifier (43–128 chars from unreserved character set).\n */\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\n/**\n * Derives the code_challenge from a code_verifier using SHA-256 (S256 method).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64url(new Uint8Array(hash));\n}\n\n/**\n * Saves the code_verifier to localStorage for retrieval after the redirect.\n */\nexport function saveCodeVerifier(verifier: string): void {\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem(PKCE_STORAGE_KEY, verifier);\n }\n}\n\n/**\n * Reads and removes the code_verifier from localStorage.\n */\nexport function consumeCodeVerifier(): string | null {\n if (typeof localStorage === 'undefined') return null;\n const verifier = localStorage.getItem(PKCE_STORAGE_KEY);\n localStorage.removeItem(PKCE_STORAGE_KEY);\n return verifier;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...bytes));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n","import type { Session, TokenStorage } from './types';\n\nconst SESSION_KEY = 'proma_session';\n\nexport class TokenStore {\n constructor(private readonly storage: TokenStorage) {}\n\n get(): Session | null {\n try {\n const raw = this.storage.getItem(SESSION_KEY);\n if (!raw) return null;\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n set(session: Session): void {\n this.storage.setItem(SESSION_KEY, JSON.stringify(session));\n }\n\n clear(): void {\n this.storage.removeItem(SESSION_KEY);\n // Also clear the PKCE verifier if present\n this.storage.removeItem('proma_code_verifier');\n }\n\n isExpired(session: Session): boolean {\n // Consider expired 30 seconds before actual expiry\n return Date.now() >= session.expiresAt - 30_000;\n }\n}\n\n/** Default in-memory storage for environments without localStorage (SSR, Node). */\nexport class MemoryStorage implements TokenStorage {\n private map = new Map<string, string>();\n getItem(key: string) {\n return this.map.get(key) ?? null;\n }\n setItem(key: string, value: string) {\n this.map.set(key, value);\n }\n removeItem(key: string) {\n this.map.delete(key);\n }\n}\n\nexport function getDefaultStorage(): TokenStorage {\n if (typeof localStorage !== 'undefined') return localStorage;\n return new MemoryStorage();\n}\n","import { errorFromGatewayResponse } from './errors';\nimport {\n consumeCodeVerifier,\n generateCodeChallenge,\n generateCodeVerifier,\n saveCodeVerifier,\n} from './pkce';\nimport { TokenStore, getDefaultStorage } from './storage';\nimport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n ChatStructuredOptions,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://www.proma.dev';\n\n/**\n * Deduplicates concurrent handleCallback() calls with the same code.\n * This handles React Strict Mode's double-effect invocation, which would\n * otherwise consume the PKCE verifier and state on the first call, leaving\n * nothing for the second call.\n */\nconst pendingCallbacks = new Map<string, Promise<Session>>();\n\nexport class PromaClient {\n readonly baseUrl: string;\n private readonly store: TokenStore;\n private readonly defaultScopes: OAuthScope[];\n\n /** Credits API — requires the `credits` scope. */\n readonly credits: CreditsApi;\n\n /** AI gateway API — requires the `ai:chat` scope. */\n readonly ai: AiApi;\n\n constructor(private readonly config: PromaClientConfig) {\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.store = new TokenStore(config.storage ?? getDefaultStorage());\n this.defaultScopes = config.scopes ?? ['profile'];\n this.credits = new CreditsApi(this);\n this.ai = new AiApi(this);\n }\n\n // ---------------------------------------------------------------------------\n // Auth\n // ---------------------------------------------------------------------------\n\n /**\n * Redirects the user to Proma's login page.\n * Call this on a button click — it will navigate away from the current page.\n *\n * @example\n * button.onclick = () => proma.login()\n */\n async login(scopes?: OAuthScope[]): Promise<void> {\n const url = await this.buildAuthorizeUrl(scopes ?? this.defaultScopes);\n window.location.href = url;\n }\n\n /**\n * Builds the authorization URL without navigating.\n * Useful if you want to control the redirect yourself.\n */\n async buildAuthorizeUrl(\n scopes: OAuthScope[] = this.defaultScopes,\n ): Promise<string> {\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n saveCodeVerifier(verifier);\n\n // Generate and persist state for CSRF protection.\n // Use a set so multiple concurrent login() calls don't clobber each other\n // (e.g. auth guards that call login() again on the callback page).\n const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n stored.push(state);\n localStorage.setItem(\n 'proma_oauth_states',\n JSON.stringify(stored.slice(-10)),\n );\n }\n\n const url = new URL('/api/oauth/authorize', this.baseUrl);\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', this.config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n\n return url.toString();\n }\n\n /**\n * Handles the OAuth callback. Call this on your redirect page.\n * Reads the `code` from the URL, exchanges it for tokens, and stores the session.\n *\n * @param url - Defaults to `window.location.href`\n * @returns The new session\n *\n * @example\n * // pages/callback.tsx\n * useEffect(() => {\n * proma.handleCallback().then(session => {\n * router.push('/dashboard')\n * })\n * }, [])\n */\n async handleCallback(url?: string): Promise<Session> {\n const href =\n url ?? (typeof window !== 'undefined' ? window.location.href : '');\n const params = new URL(href).searchParams;\n const code = params.get('code');\n const error = params.get('error');\n\n if (error) {\n throw new Error(params.get('error_description') ?? error);\n }\n\n if (!code) {\n throw new Error('No authorization code found in URL');\n }\n\n // Deduplicate: React Strict Mode fires effects twice with the same code.\n // Return the in-flight promise so the state/verifier are only consumed once.\n const pending = pendingCallbacks.get(code);\n if (pending) return pending;\n\n const promise = this.exchangeCode(code, params);\n pendingCallbacks.set(code, promise);\n promise.finally(() => pendingCallbacks.delete(code));\n return promise;\n }\n\n private async exchangeCode(\n code: string,\n params: URLSearchParams,\n ): Promise<Session> {\n // Validate state parameter to prevent CSRF attacks.\n // Accepts any state from the stored set (handles concurrent/repeated login calls).\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n\n // Fall back to legacy single-value key for backward compatibility\n if (stored.length === 0) {\n const legacy = localStorage.getItem('proma_oauth_state');\n if (legacy) stored.push(legacy);\n }\n\n if (!returnedState || !stored.includes(returnedState)) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\n\n // Remove the consumed state and persist the remainder\n const remaining = stored.filter((s) => s !== returnedState);\n if (remaining.length === 0) {\n localStorage.removeItem('proma_oauth_states');\n } else {\n localStorage.setItem('proma_oauth_states', JSON.stringify(remaining));\n }\n localStorage.removeItem('proma_oauth_state'); // clean up legacy key\n }\n\n const verifier = consumeCodeVerifier();\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.config.redirectUri,\n client_id: this.config.clientId,\n });\n\n if (verifier) body.set('code_verifier', verifier);\n\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n /**\n * Returns the current session (access token, refresh token, expiry).\n * Automatically refreshes the access token if it is expired.\n * Returns `null` if the user is not logged in.\n */\n async getSession(): Promise<Session | null> {\n const session = this.store.get();\n if (!session) return null;\n\n if (this.store.isExpired(session)) {\n try {\n return await this.refresh(session.refreshToken);\n } catch {\n this.store.clear();\n return null;\n }\n }\n\n return session;\n }\n\n /**\n * Returns `true` if the user has a valid (or refreshable) session.\n */\n async isAuthenticated(): Promise<boolean> {\n return (await this.getSession()) !== null;\n }\n\n /**\n * Fetches the logged-in user's profile.\n * Requires the `profile` scope.\n */\n async getUser(): Promise<UserInfo> {\n const token = await this.requireAccessToken();\n const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch user info');\n return res.json() as Promise<UserInfo>;\n }\n\n /**\n * Clears the stored session and logs the user out.\n * Does not revoke the token server-side.\n */\n logout(): void {\n this.store.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Internal helpers (used by sub-APIs)\n // ---------------------------------------------------------------------------\n\n async requireAccessToken(): Promise<string> {\n const session = await this.getSession();\n if (!session)\n throw new Error('Not authenticated — call proma.login() first');\n return session.accessToken;\n }\n\n private async refresh(refreshToken: string): Promise<Session> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this.config.clientId,\n });\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n private async fetchTokens(body: URLSearchParams): Promise<TokenResponse> {\n const res = await fetch(`${this.baseUrl}/api/oauth/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as {\n error: string;\n error_description?: string;\n };\n throw new Error(err.error_description ?? err.error);\n }\n return res.json() as Promise<TokenResponse>;\n }\n\n private tokensToSession(tokens: TokenResponse): Session {\n return {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: Date.now() + tokens.expires_in * 1000,\n scope: tokens.scope,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Credits API\n// ---------------------------------------------------------------------------\n\nclass CreditsApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Returns the user's current credit balance.\n * Requires scope: `credits`\n *\n * @example\n * const { balance, formatted } = await proma.credits.getBalance()\n * console.log(`You have ${formatted}`) // \"You have $1.23\"\n */\n async getBalance(): Promise<BalanceResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/balance`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch credit balance');\n return res.json() as Promise<BalanceResponse>;\n }\n\n /**\n * Deducts credits from the user's account.\n * Requires scope: `credits`\n *\n * @param amount - Micro-credits to spend. 1,000,000 = $1.00\n * @param description - Optional description for the transaction ledger.\n *\n * @example\n * await proma.credits.spend(500_000, 'Generated a report')\n */\n async spend(\n amount: number,\n description?: string,\n ): Promise<SpendCreditsResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/spend`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ amount, description }),\n });\n if (!res.ok) {\n const err = (await res.json().catch(() => ({ error: 'unknown' }))) as {\n error: string;\n };\n throw new Error(err.error);\n }\n return res.json() as Promise<SpendCreditsResponse>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// AI API\n// ---------------------------------------------------------------------------\n\nclass AiApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Sends a chat request through the Proma AI gateway (Gemini).\n * Credits are deducted automatically per token used.\n * Requires scope: `ai:chat`\n *\n * Returns a streaming `Response` — iterate SSE chunks or use a helper library.\n *\n * @example\n * const stream = await proma.ai.chat({\n * messages: [{ role: 'user', content: 'Explain quantum entanglement simply.' }]\n * })\n * const reader = stream.body.getReader()\n */\n async chat(options: ChatOptions | ChatMessage[]): Promise<Response> {\n const token = await this.client.requireAccessToken();\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const provider = params.provider ?? 'gemini';\n const model =\n params.model ?? (provider === 'gemini' ? 'gemini-2.0-flash' : '');\n\n if (!model) {\n throw new Error(\n `model is required when provider is \"${provider}\" — pass e.g. { provider: \"${provider}\", model: \"...\" }`,\n );\n }\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: params.messages,\n maxOutputTokens: params.maxOutputTokens,\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n return response;\n }\n\n /**\n * Convenience wrapper around `chat` that collects the full streamed text.\n * Use this when you don't need streaming and just want the final string.\n *\n * @example\n * const text = await proma.ai.chatText({\n * messages: [{ role: 'user', content: 'Hello!' }]\n * })\n * console.log(text)\n */\n async chatText(options: ChatOptions | ChatMessage[]): Promise<string> {\n // chat() throws a structured PromaError on non-OK responses, so by the\n // time we get here the response is a successful plain-text stream\n // (Vercel AI SDK's toTextStreamResponse — no JSON envelope).\n const res = await this.chat(options);\n\n const reader = res.body?.getReader();\n if (!reader) return '';\n\n const decoder = new TextDecoder();\n let fullText = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n fullText += decoder.decode(value, { stream: true });\n }\n fullText += decoder.decode();\n\n return fullText;\n }\n\n /**\n * Sends a chat request and parses the response as JSON. The model is nudged\n * with a system hint to emit JSON, and common wrappers (```json fences,\n * leading/trailing prose) are stripped before parsing.\n *\n * Best-effort: if the model returns malformed JSON, `fallback` is returned\n * when provided; otherwise an error is thrown. For guaranteed schema\n * compliance use `chatStructured()` instead.\n *\n * @example\n * const { title, tags } = await proma.ai.chatJSON<{ title: string; tags: string[] }>({\n * messages: [{ role: 'user', content: 'Suggest a title and 3 tags for this post' }],\n * })\n */\n async chatJSON<T = unknown>(\n options: ChatOptions | ChatMessage[],\n fallback?: T,\n ): Promise<T> {\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const hasSystem = params.messages.some((m) => m.role === 'system');\n const messages: ChatMessage[] = hasSystem\n ? params.messages\n : [\n {\n role: 'system',\n content:\n 'You must reply with a single valid JSON value and nothing else. No prose, no markdown fences.',\n },\n ...params.messages,\n ];\n\n const text = await this.chatText({ ...params, messages });\n const cleaned = stripJsonFences(text);\n\n try {\n return JSON.parse(cleaned) as T;\n } catch (err) {\n if (fallback !== undefined) return fallback;\n throw new Error(\n `chatJSON: model returned invalid JSON (${(err as Error).message}). Pass a fallback to tolerate parse failures, or use chatStructured() for guaranteed schema compliance.`,\n );\n }\n }\n\n /**\n * Sends a chat request with a JSON Schema and returns a typed object that is\n * guaranteed to match the schema (enforced provider-side via structured\n * output / function calling).\n *\n * @example\n * interface Summary { score: number; tags: string[] }\n * const result = await proma.ai.chatStructured<Summary>({\n * messages: [{ role: 'user', content: 'Rate and tag this article' }],\n * schema: {\n * type: 'object',\n * properties: {\n * score: { type: 'number' },\n * tags: { type: 'array', items: { type: 'string' } },\n * },\n * required: ['score', 'tags'],\n * },\n * })\n */\n async chatStructured<T = unknown>(\n options: ChatStructuredOptions,\n ): Promise<T> {\n const token = await this.client.requireAccessToken();\n const provider = options.provider ?? 'gemini';\n const model =\n options.model ?? (provider === 'gemini' ? 'gemini-2.0-flash' : '');\n\n if (!model) {\n throw new Error(\n `model is required when provider is \"${provider}\" — pass e.g. { provider: \"${provider}\", model: \"...\" }`,\n );\n }\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: options.messages,\n maxOutputTokens: options.maxOutputTokens,\n responseFormat: {\n type: 'json_schema',\n ...(options.schemaName && { name: options.schemaName }),\n schema: options.schema,\n },\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n const payload = (await response.json()) as { object: T };\n return payload.object;\n }\n}\n\nfunction stripJsonFences(text: string): string {\n const trimmed = text.trim();\n const fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n if (fenced) return fenced[1]!.trim();\n\n // Fall back to grabbing the outermost {...} or [...] block if the model wrapped\n // the JSON in prose.\n const firstBrace = trimmed.search(/[{[]/);\n const lastBrace = Math.max(trimmed.lastIndexOf('}'), trimmed.lastIndexOf(']'));\n if (firstBrace !== -1 && lastBrace > firstBrace) {\n return trimmed.slice(firstBrace, lastBrace + 1);\n }\n\n return trimmed;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { PromaClient } from './client';\nexport { MemoryStorage } from './storage';\nexport {\n PromaError,\n InsufficientCreditsError,\n AppSpendingLimitExceededError,\n AppLimitNotSetError,\n} from './errors';\nexport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n ChatProvider,\n ChatStructuredOptions,\n JsonSchema,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n TokenStorage,\n UserInfo,\n} from './types';\n","// Structured errors thrown by SDK methods. Apps can `instanceof`-check these\n// to react gracefully (e.g. prompt the user to top up or raise a per-app cap).\n\nexport class PromaError extends Error {\n readonly status: number;\n readonly code: string;\n\n constructor(message: string, code: string, status: number) {\n super(message);\n this.name = 'PromaError';\n this.code = code;\n this.status = status;\n }\n}\n\nexport class InsufficientCreditsError extends PromaError {\n constructor() {\n super(\n 'The user has insufficient credits — prompt them to top up.',\n 'insufficient_credits',\n 402,\n );\n this.name = 'InsufficientCreditsError';\n }\n}\n\nexport class AppSpendingLimitExceededError extends PromaError {\n readonly monthlyLimitMicroCredits: number | null;\n readonly monthToDateSpendMicroCredits: number | null;\n\n constructor(\n monthlyLimitMicroCredits: number | null,\n monthToDateSpendMicroCredits: number | null,\n ) {\n super(\n 'This app has reached its monthly spending limit set by the user.',\n 'app_limit_exceeded',\n 403,\n );\n this.name = 'AppSpendingLimitExceededError';\n this.monthlyLimitMicroCredits = monthlyLimitMicroCredits;\n this.monthToDateSpendMicroCredits = monthToDateSpendMicroCredits;\n }\n}\n\nexport class AppLimitNotSetError extends PromaError {\n constructor() {\n super(\n 'The user has not set a spending limit for this app yet — direct them to /home/my-apps.',\n 'app_limit_not_set',\n 403,\n );\n this.name = 'AppLimitNotSetError';\n }\n}\n\ninterface GatewayErrorBody {\n error?: string;\n reason?: string;\n limit?: number;\n spent?: number;\n}\n\nexport function errorFromGatewayResponse(\n status: number,\n body: GatewayErrorBody,\n): PromaError {\n const code = body.error ?? body.reason ?? 'unknown_error';\n switch (code) {\n case 'insufficient_credits':\n return new InsufficientCreditsError();\n case 'app_limit_exceeded':\n return new AppSpendingLimitExceededError(\n body.limit ?? null,\n body.spent ?? null,\n );\n case 'app_limit_not_set':\n return new AppLimitNotSetError();\n default:\n return new PromaError(\n body.error ?? `Gateway error (${status})`,\n code,\n status,\n );\n }\n}\n","/**\n * PKCE helpers — browser + Node 18+ compatible via SubtleCrypto.\n */\n\nconst PKCE_STORAGE_KEY = 'proma_code_verifier';\n\n/**\n * Generates a cryptographically random code_verifier (43–128 chars from unreserved character set).\n */\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\n/**\n * Derives the code_challenge from a code_verifier using SHA-256 (S256 method).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64url(new Uint8Array(hash));\n}\n\n/**\n * Saves the code_verifier to localStorage for retrieval after the redirect.\n */\nexport function saveCodeVerifier(verifier: string): void {\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem(PKCE_STORAGE_KEY, verifier);\n }\n}\n\n/**\n * Reads and removes the code_verifier from localStorage.\n */\nexport function consumeCodeVerifier(): string | null {\n if (typeof localStorage === 'undefined') return null;\n const verifier = localStorage.getItem(PKCE_STORAGE_KEY);\n localStorage.removeItem(PKCE_STORAGE_KEY);\n return verifier;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...bytes));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n","import type { Session, TokenStorage } from './types';\n\nconst SESSION_KEY = 'proma_session';\n\nexport class TokenStore {\n constructor(private readonly storage: TokenStorage) {}\n\n get(): Session | null {\n try {\n const raw = this.storage.getItem(SESSION_KEY);\n if (!raw) return null;\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n set(session: Session): void {\n this.storage.setItem(SESSION_KEY, JSON.stringify(session));\n }\n\n clear(): void {\n this.storage.removeItem(SESSION_KEY);\n // Also clear the PKCE verifier if present\n this.storage.removeItem('proma_code_verifier');\n }\n\n isExpired(session: Session): boolean {\n // Consider expired 30 seconds before actual expiry\n return Date.now() >= session.expiresAt - 30_000;\n }\n}\n\n/** Default in-memory storage for environments without localStorage (SSR, Node). */\nexport class MemoryStorage implements TokenStorage {\n private map = new Map<string, string>();\n getItem(key: string) {\n return this.map.get(key) ?? null;\n }\n setItem(key: string, value: string) {\n this.map.set(key, value);\n }\n removeItem(key: string) {\n this.map.delete(key);\n }\n}\n\nexport function getDefaultStorage(): TokenStorage {\n if (typeof localStorage !== 'undefined') return localStorage;\n return new MemoryStorage();\n}\n","import { errorFromGatewayResponse } from './errors';\nimport {\n consumeCodeVerifier,\n generateCodeChallenge,\n generateCodeVerifier,\n saveCodeVerifier,\n} from './pkce';\nimport { TokenStore, getDefaultStorage } from './storage';\nimport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n ChatStructuredOptions,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://www.proma.dev';\n\n/**\n * Deduplicates concurrent handleCallback() calls with the same code.\n * This handles React Strict Mode's double-effect invocation, which would\n * otherwise consume the PKCE verifier and state on the first call, leaving\n * nothing for the second call.\n */\nconst pendingCallbacks = new Map<string, Promise<Session>>();\n\nexport class PromaClient {\n readonly baseUrl: string;\n private readonly store: TokenStore;\n private readonly defaultScopes: OAuthScope[];\n\n /** Credits API — requires the `credits` scope. */\n readonly credits: CreditsApi;\n\n /** AI gateway API — requires the `ai:chat` scope. */\n readonly ai: AiApi;\n\n constructor(private readonly config: PromaClientConfig) {\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.store = new TokenStore(config.storage ?? getDefaultStorage());\n this.defaultScopes = config.scopes ?? ['profile'];\n this.credits = new CreditsApi(this);\n this.ai = new AiApi(this);\n }\n\n // ---------------------------------------------------------------------------\n // Auth\n // ---------------------------------------------------------------------------\n\n /**\n * Redirects the user to Proma's login page.\n * Call this on a button click — it will navigate away from the current page.\n *\n * @example\n * button.onclick = () => proma.login()\n */\n async login(scopes?: OAuthScope[]): Promise<void> {\n const url = await this.buildAuthorizeUrl(scopes ?? this.defaultScopes);\n window.location.href = url;\n }\n\n /**\n * Builds the authorization URL without navigating.\n * Useful if you want to control the redirect yourself.\n */\n async buildAuthorizeUrl(\n scopes: OAuthScope[] = this.defaultScopes,\n ): Promise<string> {\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n saveCodeVerifier(verifier);\n\n // Generate and persist state for CSRF protection.\n // Use a set so multiple concurrent login() calls don't clobber each other\n // (e.g. auth guards that call login() again on the callback page).\n const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n stored.push(state);\n localStorage.setItem(\n 'proma_oauth_states',\n JSON.stringify(stored.slice(-10)),\n );\n }\n\n const url = new URL('/api/oauth/authorize', this.baseUrl);\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', this.config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n\n return url.toString();\n }\n\n /**\n * Handles the OAuth callback. Call this on your redirect page.\n * Reads the `code` from the URL, exchanges it for tokens, and stores the session.\n *\n * @param url - Defaults to `window.location.href`\n * @returns The new session\n *\n * @example\n * // pages/callback.tsx\n * useEffect(() => {\n * proma.handleCallback().then(session => {\n * router.push('/dashboard')\n * })\n * }, [])\n */\n async handleCallback(url?: string): Promise<Session> {\n const href =\n url ?? (typeof window !== 'undefined' ? window.location.href : '');\n const params = new URL(href).searchParams;\n const code = params.get('code');\n const error = params.get('error');\n\n if (error) {\n throw new Error(params.get('error_description') ?? error);\n }\n\n if (!code) {\n throw new Error('No authorization code found in URL');\n }\n\n // Deduplicate: React Strict Mode fires effects twice with the same code.\n // Return the in-flight promise so the state/verifier are only consumed once.\n const pending = pendingCallbacks.get(code);\n if (pending) return pending;\n\n const promise = this.exchangeCode(code, params);\n pendingCallbacks.set(code, promise);\n promise.finally(() => pendingCallbacks.delete(code));\n return promise;\n }\n\n private async exchangeCode(\n code: string,\n params: URLSearchParams,\n ): Promise<Session> {\n // Validate state parameter to prevent CSRF attacks.\n // Accepts any state from the stored set (handles concurrent/repeated login calls).\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n\n // Fall back to legacy single-value key for backward compatibility\n if (stored.length === 0) {\n const legacy = localStorage.getItem('proma_oauth_state');\n if (legacy) stored.push(legacy);\n }\n\n if (!returnedState || !stored.includes(returnedState)) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\n\n // Remove the consumed state and persist the remainder\n const remaining = stored.filter((s) => s !== returnedState);\n if (remaining.length === 0) {\n localStorage.removeItem('proma_oauth_states');\n } else {\n localStorage.setItem('proma_oauth_states', JSON.stringify(remaining));\n }\n localStorage.removeItem('proma_oauth_state'); // clean up legacy key\n }\n\n const verifier = consumeCodeVerifier();\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.config.redirectUri,\n client_id: this.config.clientId,\n });\n\n if (verifier) body.set('code_verifier', verifier);\n\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n /**\n * Returns the current session (access token, refresh token, expiry).\n * Automatically refreshes the access token if it is expired.\n * Returns `null` if the user is not logged in.\n */\n async getSession(): Promise<Session | null> {\n const session = this.store.get();\n if (!session) return null;\n\n if (this.store.isExpired(session)) {\n try {\n return await this.refresh(session.refreshToken);\n } catch {\n this.store.clear();\n return null;\n }\n }\n\n return session;\n }\n\n /**\n * Returns `true` if the user has a valid (or refreshable) session.\n */\n async isAuthenticated(): Promise<boolean> {\n return (await this.getSession()) !== null;\n }\n\n /**\n * Fetches the logged-in user's profile.\n * Requires the `profile` scope.\n */\n async getUser(): Promise<UserInfo> {\n const token = await this.requireAccessToken();\n const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch user info');\n return res.json() as Promise<UserInfo>;\n }\n\n /**\n * Clears the stored session and logs the user out.\n * Does not revoke the token server-side.\n */\n logout(): void {\n this.store.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Internal helpers (used by sub-APIs)\n // ---------------------------------------------------------------------------\n\n async requireAccessToken(): Promise<string> {\n const session = await this.getSession();\n if (!session)\n throw new Error('Not authenticated — call proma.login() first');\n return session.accessToken;\n }\n\n private async refresh(refreshToken: string): Promise<Session> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this.config.clientId,\n });\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n private async fetchTokens(body: URLSearchParams): Promise<TokenResponse> {\n const res = await fetch(`${this.baseUrl}/api/oauth/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as {\n error: string;\n error_description?: string;\n };\n throw new Error(err.error_description ?? err.error);\n }\n return res.json() as Promise<TokenResponse>;\n }\n\n private tokensToSession(tokens: TokenResponse): Session {\n return {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: Date.now() + tokens.expires_in * 1000,\n scope: tokens.scope,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Credits API\n// ---------------------------------------------------------------------------\n\nclass CreditsApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Returns the user's current credit balance.\n * Requires scope: `credits`\n *\n * @example\n * const { balance, formatted } = await proma.credits.getBalance()\n * console.log(`You have ${formatted}`) // \"You have $1.23\"\n */\n async getBalance(): Promise<BalanceResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/balance`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch credit balance');\n return res.json() as Promise<BalanceResponse>;\n }\n\n /**\n * Deducts credits from the user's account.\n * Requires scope: `credits`\n *\n * @param amount - Micro-credits to spend. 1,000,000 = $1.00\n * @param description - Optional description for the transaction ledger.\n *\n * @example\n * await proma.credits.spend(500_000, 'Generated a report')\n */\n async spend(\n amount: number,\n description?: string,\n ): Promise<SpendCreditsResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/spend`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ amount, description }),\n });\n if (!res.ok) {\n const err = (await res.json().catch(() => ({ error: 'unknown' }))) as {\n error: string;\n };\n throw new Error(err.error);\n }\n return res.json() as Promise<SpendCreditsResponse>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// AI API\n// ---------------------------------------------------------------------------\n\nclass AiApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Sends a chat request through the Proma AI gateway.\n * Credits are deducted automatically per token used.\n * Requires scope: `ai:chat`\n *\n * Defaults to `anthropic` + `claude-3-5-haiku-latest`. Pass `provider` and\n * `model` to route elsewhere (gemini / openrouter).\n *\n * Returns a streaming `Response` — iterate SSE chunks or use a helper library.\n *\n * @example\n * const stream = await proma.ai.chat({\n * messages: [{ role: 'user', content: 'Explain quantum entanglement simply.' }]\n * })\n * const reader = stream.body.getReader()\n */\n async chat(options: ChatOptions | ChatMessage[]): Promise<Response> {\n const token = await this.client.requireAccessToken();\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const { provider, model } = resolveProviderAndModel(params);\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: params.messages,\n maxOutputTokens: params.maxOutputTokens,\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n return response;\n }\n\n /**\n * Convenience wrapper around `chat` that collects the full streamed text.\n * Use this when you don't need streaming and just want the final string.\n *\n * @example\n * const text = await proma.ai.chatText({\n * messages: [{ role: 'user', content: 'Hello!' }]\n * })\n * console.log(text)\n */\n async chatText(options: ChatOptions | ChatMessage[]): Promise<string> {\n // chat() throws a structured PromaError on non-OK responses, so by the\n // time we get here the response is a successful plain-text stream\n // (Vercel AI SDK's toTextStreamResponse — no JSON envelope).\n const res = await this.chat(options);\n\n const reader = res.body?.getReader();\n if (!reader) return '';\n\n const decoder = new TextDecoder();\n let fullText = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n fullText += decoder.decode(value, { stream: true });\n }\n fullText += decoder.decode();\n\n return fullText;\n }\n\n /**\n * Sends a chat request and parses the response as JSON. The model is nudged\n * with a system hint to emit JSON, and common wrappers (```json fences,\n * leading/trailing prose) are stripped before parsing.\n *\n * Best-effort: if the model returns malformed JSON, `fallback` is returned\n * when provided; otherwise an error is thrown. For guaranteed schema\n * compliance use `chatStructured()` instead.\n *\n * @example\n * const { title, tags } = await proma.ai.chatJSON<{ title: string; tags: string[] }>({\n * messages: [{ role: 'user', content: 'Suggest a title and 3 tags for this post' }],\n * })\n */\n async chatJSON<T = unknown>(\n options: ChatOptions | ChatMessage[],\n fallback?: T,\n ): Promise<T> {\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const hasSystem = params.messages.some((m) => m.role === 'system');\n const messages: ChatMessage[] = hasSystem\n ? params.messages\n : [\n {\n role: 'system',\n content:\n 'You must reply with a single valid JSON value and nothing else. No prose, no markdown fences.',\n },\n ...params.messages,\n ];\n\n const text = await this.chatText({ ...params, messages });\n const cleaned = stripJsonFences(text);\n\n try {\n return JSON.parse(cleaned) as T;\n } catch (err) {\n if (fallback !== undefined) return fallback;\n throw new Error(\n `chatJSON: model returned invalid JSON (${(err as Error).message}). Pass a fallback to tolerate parse failures, or use chatStructured() for guaranteed schema compliance.`,\n );\n }\n }\n\n /**\n * Sends a chat request with a JSON Schema and returns a typed object that is\n * guaranteed to match the schema (enforced provider-side via structured\n * output / function calling).\n *\n * @example\n * interface Summary { score: number; tags: string[] }\n * const result = await proma.ai.chatStructured<Summary>({\n * messages: [{ role: 'user', content: 'Rate and tag this article' }],\n * schema: {\n * type: 'object',\n * properties: {\n * score: { type: 'number' },\n * tags: { type: 'array', items: { type: 'string' } },\n * },\n * required: ['score', 'tags'],\n * },\n * })\n */\n async chatStructured<T = unknown>(\n options: ChatStructuredOptions,\n ): Promise<T> {\n const token = await this.client.requireAccessToken();\n const { provider, model } = resolveProviderAndModel(options);\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: options.messages,\n maxOutputTokens: options.maxOutputTokens,\n responseFormat: {\n type: 'json_schema',\n ...(options.schemaName && { name: options.schemaName }),\n schema: options.schema,\n },\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n const payload = (await response.json()) as { object: T };\n return payload.object;\n }\n}\n\nfunction resolveProviderAndModel(options: ChatOptions): {\n provider: 'gemini' | 'anthropic' | 'openrouter';\n model: string;\n} {\n const provider = options.provider ?? 'anthropic';\n\n const defaultModel =\n provider === 'anthropic'\n ? 'claude-3-5-haiku-latest'\n : provider === 'gemini'\n ? 'gemini-2.0-flash'\n : '';\n\n const model = options.model ?? defaultModel;\n\n if (!model) {\n throw new Error(\n `model is required when provider is \"${provider}\" — pass e.g. { provider: \"${provider}\", model: \"...\" }`,\n );\n }\n\n return { provider, model };\n}\n\nfunction stripJsonFences(text: string): string {\n const trimmed = text.trim();\n const fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n if (fenced) return fenced[1]!.trim();\n\n // Fall back to grabbing the outermost {...} or [...] block if the model wrapped\n // the JSON in prose.\n const firstBrace = trimmed.search(/[{[]/);\n const lastBrace = Math.max(trimmed.lastIndexOf('}'), trimmed.lastIndexOf(']'));\n if (firstBrace !== -1 && lastBrace > firstBrace) {\n return trimmed.slice(firstBrace, lastBrace + 1);\n }\n\n return trimmed;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -55,17 +55,18 @@ type ChatProvider = 'gemini' | 'anthropic' | 'openrouter';
|
|
|
55
55
|
interface ChatOptions {
|
|
56
56
|
messages: ChatMessage[];
|
|
57
57
|
/**
|
|
58
|
-
* Which upstream provider to route through. Defaults to `
|
|
58
|
+
* Which upstream provider to route through. Defaults to `anthropic`.
|
|
59
59
|
* `openrouter` exposes 100+ models including xAI, Mistral, Llama, etc.
|
|
60
60
|
*/
|
|
61
61
|
provider?: ChatProvider;
|
|
62
62
|
/**
|
|
63
63
|
* Model identifier within the chosen provider. Examples:
|
|
64
|
+
* - anthropic: `claude-3-5-haiku-latest`, `claude-3-5-sonnet-latest`, `claude-opus-4-0`
|
|
64
65
|
* - gemini: `gemini-2.0-flash`, `gemini-1.5-pro`
|
|
65
|
-
* - anthropic: `claude-3-5-sonnet-latest`, `claude-opus-4-0`
|
|
66
66
|
* - openrouter: `meta-llama/llama-3.3-70b-instruct`, `x-ai/grok-2-1212`
|
|
67
67
|
*
|
|
68
|
-
* Defaults to `
|
|
68
|
+
* Defaults to `claude-3-5-haiku-latest` when provider is unset/`anthropic`,
|
|
69
|
+
* and to `gemini-2.0-flash` when provider is `gemini`.
|
|
69
70
|
*/
|
|
70
71
|
model?: string;
|
|
71
72
|
/** Maximum tokens to generate. Caps the per-call cost estimate. */
|
|
@@ -176,10 +177,13 @@ declare class AiApi {
|
|
|
176
177
|
private readonly client;
|
|
177
178
|
constructor(client: PromaClient);
|
|
178
179
|
/**
|
|
179
|
-
* Sends a chat request through the Proma AI gateway
|
|
180
|
+
* Sends a chat request through the Proma AI gateway.
|
|
180
181
|
* Credits are deducted automatically per token used.
|
|
181
182
|
* Requires scope: `ai:chat`
|
|
182
183
|
*
|
|
184
|
+
* Defaults to `anthropic` + `claude-3-5-haiku-latest`. Pass `provider` and
|
|
185
|
+
* `model` to route elsewhere (gemini / openrouter).
|
|
186
|
+
*
|
|
183
187
|
* Returns a streaming `Response` — iterate SSE chunks or use a helper library.
|
|
184
188
|
*
|
|
185
189
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -55,17 +55,18 @@ type ChatProvider = 'gemini' | 'anthropic' | 'openrouter';
|
|
|
55
55
|
interface ChatOptions {
|
|
56
56
|
messages: ChatMessage[];
|
|
57
57
|
/**
|
|
58
|
-
* Which upstream provider to route through. Defaults to `
|
|
58
|
+
* Which upstream provider to route through. Defaults to `anthropic`.
|
|
59
59
|
* `openrouter` exposes 100+ models including xAI, Mistral, Llama, etc.
|
|
60
60
|
*/
|
|
61
61
|
provider?: ChatProvider;
|
|
62
62
|
/**
|
|
63
63
|
* Model identifier within the chosen provider. Examples:
|
|
64
|
+
* - anthropic: `claude-3-5-haiku-latest`, `claude-3-5-sonnet-latest`, `claude-opus-4-0`
|
|
64
65
|
* - gemini: `gemini-2.0-flash`, `gemini-1.5-pro`
|
|
65
|
-
* - anthropic: `claude-3-5-sonnet-latest`, `claude-opus-4-0`
|
|
66
66
|
* - openrouter: `meta-llama/llama-3.3-70b-instruct`, `x-ai/grok-2-1212`
|
|
67
67
|
*
|
|
68
|
-
* Defaults to `
|
|
68
|
+
* Defaults to `claude-3-5-haiku-latest` when provider is unset/`anthropic`,
|
|
69
|
+
* and to `gemini-2.0-flash` when provider is `gemini`.
|
|
69
70
|
*/
|
|
70
71
|
model?: string;
|
|
71
72
|
/** Maximum tokens to generate. Caps the per-call cost estimate. */
|
|
@@ -176,10 +177,13 @@ declare class AiApi {
|
|
|
176
177
|
private readonly client;
|
|
177
178
|
constructor(client: PromaClient);
|
|
178
179
|
/**
|
|
179
|
-
* Sends a chat request through the Proma AI gateway
|
|
180
|
+
* Sends a chat request through the Proma AI gateway.
|
|
180
181
|
* Credits are deducted automatically per token used.
|
|
181
182
|
* Requires scope: `ai:chat`
|
|
182
183
|
*
|
|
184
|
+
* Defaults to `anthropic` + `claude-3-5-haiku-latest`. Pass `provider` and
|
|
185
|
+
* `model` to route elsewhere (gemini / openrouter).
|
|
186
|
+
*
|
|
183
187
|
* Returns a streaming `Response` — iterate SSE chunks or use a helper library.
|
|
184
188
|
*
|
|
185
189
|
* @example
|
package/dist/index.js
CHANGED
|
@@ -419,10 +419,13 @@ var AiApi = class {
|
|
|
419
419
|
this.client = client;
|
|
420
420
|
}
|
|
421
421
|
/**
|
|
422
|
-
* Sends a chat request through the Proma AI gateway
|
|
422
|
+
* Sends a chat request through the Proma AI gateway.
|
|
423
423
|
* Credits are deducted automatically per token used.
|
|
424
424
|
* Requires scope: `ai:chat`
|
|
425
425
|
*
|
|
426
|
+
* Defaults to `anthropic` + `claude-3-5-haiku-latest`. Pass `provider` and
|
|
427
|
+
* `model` to route elsewhere (gemini / openrouter).
|
|
428
|
+
*
|
|
426
429
|
* Returns a streaming `Response` — iterate SSE chunks or use a helper library.
|
|
427
430
|
*
|
|
428
431
|
* @example
|
|
@@ -432,16 +435,9 @@ var AiApi = class {
|
|
|
432
435
|
* const reader = stream.body.getReader()
|
|
433
436
|
*/
|
|
434
437
|
async chat(options) {
|
|
435
|
-
var _a, _b;
|
|
436
438
|
const token = await this.client.requireAccessToken();
|
|
437
439
|
const params = Array.isArray(options) ? { messages: options } : options;
|
|
438
|
-
const provider
|
|
439
|
-
const model = (_b = params.model) != null ? _b : provider === "gemini" ? "gemini-2.0-flash" : "";
|
|
440
|
-
if (!model) {
|
|
441
|
-
throw new Error(
|
|
442
|
-
`model is required when provider is "${provider}" \u2014 pass e.g. { provider: "${provider}", model: "..." }`
|
|
443
|
-
);
|
|
444
|
-
}
|
|
440
|
+
const { provider, model } = resolveProviderAndModel(params);
|
|
445
441
|
const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {
|
|
446
442
|
method: "POST",
|
|
447
443
|
headers: {
|
|
@@ -541,15 +537,8 @@ var AiApi = class {
|
|
|
541
537
|
* })
|
|
542
538
|
*/
|
|
543
539
|
async chatStructured(options) {
|
|
544
|
-
var _a, _b;
|
|
545
540
|
const token = await this.client.requireAccessToken();
|
|
546
|
-
const provider
|
|
547
|
-
const model = (_b = options.model) != null ? _b : provider === "gemini" ? "gemini-2.0-flash" : "";
|
|
548
|
-
if (!model) {
|
|
549
|
-
throw new Error(
|
|
550
|
-
`model is required when provider is "${provider}" \u2014 pass e.g. { provider: "${provider}", model: "..." }`
|
|
551
|
-
);
|
|
552
|
-
}
|
|
541
|
+
const { provider, model } = resolveProviderAndModel(options);
|
|
553
542
|
const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {
|
|
554
543
|
method: "POST",
|
|
555
544
|
headers: {
|
|
@@ -576,6 +565,18 @@ var AiApi = class {
|
|
|
576
565
|
return payload.object;
|
|
577
566
|
}
|
|
578
567
|
};
|
|
568
|
+
function resolveProviderAndModel(options) {
|
|
569
|
+
var _a, _b;
|
|
570
|
+
const provider = (_a = options.provider) != null ? _a : "anthropic";
|
|
571
|
+
const defaultModel = provider === "anthropic" ? "claude-3-5-haiku-latest" : provider === "gemini" ? "gemini-2.0-flash" : "";
|
|
572
|
+
const model = (_b = options.model) != null ? _b : defaultModel;
|
|
573
|
+
if (!model) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`model is required when provider is "${provider}" \u2014 pass e.g. { provider: "${provider}", model: "..." }`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
return { provider, model };
|
|
579
|
+
}
|
|
579
580
|
function stripJsonFences(text) {
|
|
580
581
|
const trimmed = text.trim();
|
|
581
582
|
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["// Structured errors thrown by SDK methods. Apps can `instanceof`-check these\n// to react gracefully (e.g. prompt the user to top up or raise a per-app cap).\n\nexport class PromaError extends Error {\n readonly status: number;\n readonly code: string;\n\n constructor(message: string, code: string, status: number) {\n super(message);\n this.name = 'PromaError';\n this.code = code;\n this.status = status;\n }\n}\n\nexport class InsufficientCreditsError extends PromaError {\n constructor() {\n super(\n 'The user has insufficient credits — prompt them to top up.',\n 'insufficient_credits',\n 402,\n );\n this.name = 'InsufficientCreditsError';\n }\n}\n\nexport class AppSpendingLimitExceededError extends PromaError {\n readonly monthlyLimitMicroCredits: number | null;\n readonly monthToDateSpendMicroCredits: number | null;\n\n constructor(\n monthlyLimitMicroCredits: number | null,\n monthToDateSpendMicroCredits: number | null,\n ) {\n super(\n 'This app has reached its monthly spending limit set by the user.',\n 'app_limit_exceeded',\n 403,\n );\n this.name = 'AppSpendingLimitExceededError';\n this.monthlyLimitMicroCredits = monthlyLimitMicroCredits;\n this.monthToDateSpendMicroCredits = monthToDateSpendMicroCredits;\n }\n}\n\nexport class AppLimitNotSetError extends PromaError {\n constructor() {\n super(\n 'The user has not set a spending limit for this app yet — direct them to /home/my-apps.',\n 'app_limit_not_set',\n 403,\n );\n this.name = 'AppLimitNotSetError';\n }\n}\n\ninterface GatewayErrorBody {\n error?: string;\n reason?: string;\n limit?: number;\n spent?: number;\n}\n\nexport function errorFromGatewayResponse(\n status: number,\n body: GatewayErrorBody,\n): PromaError {\n const code = body.error ?? body.reason ?? 'unknown_error';\n switch (code) {\n case 'insufficient_credits':\n return new InsufficientCreditsError();\n case 'app_limit_exceeded':\n return new AppSpendingLimitExceededError(\n body.limit ?? null,\n body.spent ?? null,\n );\n case 'app_limit_not_set':\n return new AppLimitNotSetError();\n default:\n return new PromaError(\n body.error ?? `Gateway error (${status})`,\n code,\n status,\n );\n }\n}\n","/**\n * PKCE helpers — browser + Node 18+ compatible via SubtleCrypto.\n */\n\nconst PKCE_STORAGE_KEY = 'proma_code_verifier';\n\n/**\n * Generates a cryptographically random code_verifier (43–128 chars from unreserved character set).\n */\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\n/**\n * Derives the code_challenge from a code_verifier using SHA-256 (S256 method).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64url(new Uint8Array(hash));\n}\n\n/**\n * Saves the code_verifier to localStorage for retrieval after the redirect.\n */\nexport function saveCodeVerifier(verifier: string): void {\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem(PKCE_STORAGE_KEY, verifier);\n }\n}\n\n/**\n * Reads and removes the code_verifier from localStorage.\n */\nexport function consumeCodeVerifier(): string | null {\n if (typeof localStorage === 'undefined') return null;\n const verifier = localStorage.getItem(PKCE_STORAGE_KEY);\n localStorage.removeItem(PKCE_STORAGE_KEY);\n return verifier;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...bytes));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n","import type { Session, TokenStorage } from './types';\n\nconst SESSION_KEY = 'proma_session';\n\nexport class TokenStore {\n constructor(private readonly storage: TokenStorage) {}\n\n get(): Session | null {\n try {\n const raw = this.storage.getItem(SESSION_KEY);\n if (!raw) return null;\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n set(session: Session): void {\n this.storage.setItem(SESSION_KEY, JSON.stringify(session));\n }\n\n clear(): void {\n this.storage.removeItem(SESSION_KEY);\n // Also clear the PKCE verifier if present\n this.storage.removeItem('proma_code_verifier');\n }\n\n isExpired(session: Session): boolean {\n // Consider expired 30 seconds before actual expiry\n return Date.now() >= session.expiresAt - 30_000;\n }\n}\n\n/** Default in-memory storage for environments without localStorage (SSR, Node). */\nexport class MemoryStorage implements TokenStorage {\n private map = new Map<string, string>();\n getItem(key: string) {\n return this.map.get(key) ?? null;\n }\n setItem(key: string, value: string) {\n this.map.set(key, value);\n }\n removeItem(key: string) {\n this.map.delete(key);\n }\n}\n\nexport function getDefaultStorage(): TokenStorage {\n if (typeof localStorage !== 'undefined') return localStorage;\n return new MemoryStorage();\n}\n","import { errorFromGatewayResponse } from './errors';\nimport {\n consumeCodeVerifier,\n generateCodeChallenge,\n generateCodeVerifier,\n saveCodeVerifier,\n} from './pkce';\nimport { TokenStore, getDefaultStorage } from './storage';\nimport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n ChatStructuredOptions,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://www.proma.dev';\n\n/**\n * Deduplicates concurrent handleCallback() calls with the same code.\n * This handles React Strict Mode's double-effect invocation, which would\n * otherwise consume the PKCE verifier and state on the first call, leaving\n * nothing for the second call.\n */\nconst pendingCallbacks = new Map<string, Promise<Session>>();\n\nexport class PromaClient {\n readonly baseUrl: string;\n private readonly store: TokenStore;\n private readonly defaultScopes: OAuthScope[];\n\n /** Credits API — requires the `credits` scope. */\n readonly credits: CreditsApi;\n\n /** AI gateway API — requires the `ai:chat` scope. */\n readonly ai: AiApi;\n\n constructor(private readonly config: PromaClientConfig) {\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.store = new TokenStore(config.storage ?? getDefaultStorage());\n this.defaultScopes = config.scopes ?? ['profile'];\n this.credits = new CreditsApi(this);\n this.ai = new AiApi(this);\n }\n\n // ---------------------------------------------------------------------------\n // Auth\n // ---------------------------------------------------------------------------\n\n /**\n * Redirects the user to Proma's login page.\n * Call this on a button click — it will navigate away from the current page.\n *\n * @example\n * button.onclick = () => proma.login()\n */\n async login(scopes?: OAuthScope[]): Promise<void> {\n const url = await this.buildAuthorizeUrl(scopes ?? this.defaultScopes);\n window.location.href = url;\n }\n\n /**\n * Builds the authorization URL without navigating.\n * Useful if you want to control the redirect yourself.\n */\n async buildAuthorizeUrl(\n scopes: OAuthScope[] = this.defaultScopes,\n ): Promise<string> {\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n saveCodeVerifier(verifier);\n\n // Generate and persist state for CSRF protection.\n // Use a set so multiple concurrent login() calls don't clobber each other\n // (e.g. auth guards that call login() again on the callback page).\n const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n stored.push(state);\n localStorage.setItem(\n 'proma_oauth_states',\n JSON.stringify(stored.slice(-10)),\n );\n }\n\n const url = new URL('/api/oauth/authorize', this.baseUrl);\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', this.config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n\n return url.toString();\n }\n\n /**\n * Handles the OAuth callback. Call this on your redirect page.\n * Reads the `code` from the URL, exchanges it for tokens, and stores the session.\n *\n * @param url - Defaults to `window.location.href`\n * @returns The new session\n *\n * @example\n * // pages/callback.tsx\n * useEffect(() => {\n * proma.handleCallback().then(session => {\n * router.push('/dashboard')\n * })\n * }, [])\n */\n async handleCallback(url?: string): Promise<Session> {\n const href =\n url ?? (typeof window !== 'undefined' ? window.location.href : '');\n const params = new URL(href).searchParams;\n const code = params.get('code');\n const error = params.get('error');\n\n if (error) {\n throw new Error(params.get('error_description') ?? error);\n }\n\n if (!code) {\n throw new Error('No authorization code found in URL');\n }\n\n // Deduplicate: React Strict Mode fires effects twice with the same code.\n // Return the in-flight promise so the state/verifier are only consumed once.\n const pending = pendingCallbacks.get(code);\n if (pending) return pending;\n\n const promise = this.exchangeCode(code, params);\n pendingCallbacks.set(code, promise);\n promise.finally(() => pendingCallbacks.delete(code));\n return promise;\n }\n\n private async exchangeCode(\n code: string,\n params: URLSearchParams,\n ): Promise<Session> {\n // Validate state parameter to prevent CSRF attacks.\n // Accepts any state from the stored set (handles concurrent/repeated login calls).\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n\n // Fall back to legacy single-value key for backward compatibility\n if (stored.length === 0) {\n const legacy = localStorage.getItem('proma_oauth_state');\n if (legacy) stored.push(legacy);\n }\n\n if (!returnedState || !stored.includes(returnedState)) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\n\n // Remove the consumed state and persist the remainder\n const remaining = stored.filter((s) => s !== returnedState);\n if (remaining.length === 0) {\n localStorage.removeItem('proma_oauth_states');\n } else {\n localStorage.setItem('proma_oauth_states', JSON.stringify(remaining));\n }\n localStorage.removeItem('proma_oauth_state'); // clean up legacy key\n }\n\n const verifier = consumeCodeVerifier();\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.config.redirectUri,\n client_id: this.config.clientId,\n });\n\n if (verifier) body.set('code_verifier', verifier);\n\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n /**\n * Returns the current session (access token, refresh token, expiry).\n * Automatically refreshes the access token if it is expired.\n * Returns `null` if the user is not logged in.\n */\n async getSession(): Promise<Session | null> {\n const session = this.store.get();\n if (!session) return null;\n\n if (this.store.isExpired(session)) {\n try {\n return await this.refresh(session.refreshToken);\n } catch {\n this.store.clear();\n return null;\n }\n }\n\n return session;\n }\n\n /**\n * Returns `true` if the user has a valid (or refreshable) session.\n */\n async isAuthenticated(): Promise<boolean> {\n return (await this.getSession()) !== null;\n }\n\n /**\n * Fetches the logged-in user's profile.\n * Requires the `profile` scope.\n */\n async getUser(): Promise<UserInfo> {\n const token = await this.requireAccessToken();\n const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch user info');\n return res.json() as Promise<UserInfo>;\n }\n\n /**\n * Clears the stored session and logs the user out.\n * Does not revoke the token server-side.\n */\n logout(): void {\n this.store.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Internal helpers (used by sub-APIs)\n // ---------------------------------------------------------------------------\n\n async requireAccessToken(): Promise<string> {\n const session = await this.getSession();\n if (!session)\n throw new Error('Not authenticated — call proma.login() first');\n return session.accessToken;\n }\n\n private async refresh(refreshToken: string): Promise<Session> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this.config.clientId,\n });\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n private async fetchTokens(body: URLSearchParams): Promise<TokenResponse> {\n const res = await fetch(`${this.baseUrl}/api/oauth/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as {\n error: string;\n error_description?: string;\n };\n throw new Error(err.error_description ?? err.error);\n }\n return res.json() as Promise<TokenResponse>;\n }\n\n private tokensToSession(tokens: TokenResponse): Session {\n return {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: Date.now() + tokens.expires_in * 1000,\n scope: tokens.scope,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Credits API\n// ---------------------------------------------------------------------------\n\nclass CreditsApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Returns the user's current credit balance.\n * Requires scope: `credits`\n *\n * @example\n * const { balance, formatted } = await proma.credits.getBalance()\n * console.log(`You have ${formatted}`) // \"You have $1.23\"\n */\n async getBalance(): Promise<BalanceResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/balance`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch credit balance');\n return res.json() as Promise<BalanceResponse>;\n }\n\n /**\n * Deducts credits from the user's account.\n * Requires scope: `credits`\n *\n * @param amount - Micro-credits to spend. 1,000,000 = $1.00\n * @param description - Optional description for the transaction ledger.\n *\n * @example\n * await proma.credits.spend(500_000, 'Generated a report')\n */\n async spend(\n amount: number,\n description?: string,\n ): Promise<SpendCreditsResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/spend`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ amount, description }),\n });\n if (!res.ok) {\n const err = (await res.json().catch(() => ({ error: 'unknown' }))) as {\n error: string;\n };\n throw new Error(err.error);\n }\n return res.json() as Promise<SpendCreditsResponse>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// AI API\n// ---------------------------------------------------------------------------\n\nclass AiApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Sends a chat request through the Proma AI gateway (Gemini).\n * Credits are deducted automatically per token used.\n * Requires scope: `ai:chat`\n *\n * Returns a streaming `Response` — iterate SSE chunks or use a helper library.\n *\n * @example\n * const stream = await proma.ai.chat({\n * messages: [{ role: 'user', content: 'Explain quantum entanglement simply.' }]\n * })\n * const reader = stream.body.getReader()\n */\n async chat(options: ChatOptions | ChatMessage[]): Promise<Response> {\n const token = await this.client.requireAccessToken();\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const provider = params.provider ?? 'gemini';\n const model =\n params.model ?? (provider === 'gemini' ? 'gemini-2.0-flash' : '');\n\n if (!model) {\n throw new Error(\n `model is required when provider is \"${provider}\" — pass e.g. { provider: \"${provider}\", model: \"...\" }`,\n );\n }\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: params.messages,\n maxOutputTokens: params.maxOutputTokens,\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n return response;\n }\n\n /**\n * Convenience wrapper around `chat` that collects the full streamed text.\n * Use this when you don't need streaming and just want the final string.\n *\n * @example\n * const text = await proma.ai.chatText({\n * messages: [{ role: 'user', content: 'Hello!' }]\n * })\n * console.log(text)\n */\n async chatText(options: ChatOptions | ChatMessage[]): Promise<string> {\n // chat() throws a structured PromaError on non-OK responses, so by the\n // time we get here the response is a successful plain-text stream\n // (Vercel AI SDK's toTextStreamResponse — no JSON envelope).\n const res = await this.chat(options);\n\n const reader = res.body?.getReader();\n if (!reader) return '';\n\n const decoder = new TextDecoder();\n let fullText = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n fullText += decoder.decode(value, { stream: true });\n }\n fullText += decoder.decode();\n\n return fullText;\n }\n\n /**\n * Sends a chat request and parses the response as JSON. The model is nudged\n * with a system hint to emit JSON, and common wrappers (```json fences,\n * leading/trailing prose) are stripped before parsing.\n *\n * Best-effort: if the model returns malformed JSON, `fallback` is returned\n * when provided; otherwise an error is thrown. For guaranteed schema\n * compliance use `chatStructured()` instead.\n *\n * @example\n * const { title, tags } = await proma.ai.chatJSON<{ title: string; tags: string[] }>({\n * messages: [{ role: 'user', content: 'Suggest a title and 3 tags for this post' }],\n * })\n */\n async chatJSON<T = unknown>(\n options: ChatOptions | ChatMessage[],\n fallback?: T,\n ): Promise<T> {\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const hasSystem = params.messages.some((m) => m.role === 'system');\n const messages: ChatMessage[] = hasSystem\n ? params.messages\n : [\n {\n role: 'system',\n content:\n 'You must reply with a single valid JSON value and nothing else. No prose, no markdown fences.',\n },\n ...params.messages,\n ];\n\n const text = await this.chatText({ ...params, messages });\n const cleaned = stripJsonFences(text);\n\n try {\n return JSON.parse(cleaned) as T;\n } catch (err) {\n if (fallback !== undefined) return fallback;\n throw new Error(\n `chatJSON: model returned invalid JSON (${(err as Error).message}). Pass a fallback to tolerate parse failures, or use chatStructured() for guaranteed schema compliance.`,\n );\n }\n }\n\n /**\n * Sends a chat request with a JSON Schema and returns a typed object that is\n * guaranteed to match the schema (enforced provider-side via structured\n * output / function calling).\n *\n * @example\n * interface Summary { score: number; tags: string[] }\n * const result = await proma.ai.chatStructured<Summary>({\n * messages: [{ role: 'user', content: 'Rate and tag this article' }],\n * schema: {\n * type: 'object',\n * properties: {\n * score: { type: 'number' },\n * tags: { type: 'array', items: { type: 'string' } },\n * },\n * required: ['score', 'tags'],\n * },\n * })\n */\n async chatStructured<T = unknown>(\n options: ChatStructuredOptions,\n ): Promise<T> {\n const token = await this.client.requireAccessToken();\n const provider = options.provider ?? 'gemini';\n const model =\n options.model ?? (provider === 'gemini' ? 'gemini-2.0-flash' : '');\n\n if (!model) {\n throw new Error(\n `model is required when provider is \"${provider}\" — pass e.g. { provider: \"${provider}\", model: \"...\" }`,\n );\n }\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: options.messages,\n maxOutputTokens: options.maxOutputTokens,\n responseFormat: {\n type: 'json_schema',\n ...(options.schemaName && { name: options.schemaName }),\n schema: options.schema,\n },\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n const payload = (await response.json()) as { object: T };\n return payload.object;\n }\n}\n\nfunction stripJsonFences(text: string): string {\n const trimmed = text.trim();\n const fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n if (fenced) return fenced[1]!.trim();\n\n // Fall back to grabbing the outermost {...} or [...] block if the model wrapped\n // the JSON in prose.\n const firstBrace = trimmed.search(/[{[]/);\n const lastBrace = Math.max(trimmed.lastIndexOf('}'), trimmed.lastIndexOf(']'));\n if (firstBrace !== -1 && lastBrace > firstBrace) {\n return trimmed.slice(firstBrace, lastBrace + 1);\n }\n\n return trimmed;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["// Structured errors thrown by SDK methods. Apps can `instanceof`-check these\n// to react gracefully (e.g. prompt the user to top up or raise a per-app cap).\n\nexport class PromaError extends Error {\n readonly status: number;\n readonly code: string;\n\n constructor(message: string, code: string, status: number) {\n super(message);\n this.name = 'PromaError';\n this.code = code;\n this.status = status;\n }\n}\n\nexport class InsufficientCreditsError extends PromaError {\n constructor() {\n super(\n 'The user has insufficient credits — prompt them to top up.',\n 'insufficient_credits',\n 402,\n );\n this.name = 'InsufficientCreditsError';\n }\n}\n\nexport class AppSpendingLimitExceededError extends PromaError {\n readonly monthlyLimitMicroCredits: number | null;\n readonly monthToDateSpendMicroCredits: number | null;\n\n constructor(\n monthlyLimitMicroCredits: number | null,\n monthToDateSpendMicroCredits: number | null,\n ) {\n super(\n 'This app has reached its monthly spending limit set by the user.',\n 'app_limit_exceeded',\n 403,\n );\n this.name = 'AppSpendingLimitExceededError';\n this.monthlyLimitMicroCredits = monthlyLimitMicroCredits;\n this.monthToDateSpendMicroCredits = monthToDateSpendMicroCredits;\n }\n}\n\nexport class AppLimitNotSetError extends PromaError {\n constructor() {\n super(\n 'The user has not set a spending limit for this app yet — direct them to /home/my-apps.',\n 'app_limit_not_set',\n 403,\n );\n this.name = 'AppLimitNotSetError';\n }\n}\n\ninterface GatewayErrorBody {\n error?: string;\n reason?: string;\n limit?: number;\n spent?: number;\n}\n\nexport function errorFromGatewayResponse(\n status: number,\n body: GatewayErrorBody,\n): PromaError {\n const code = body.error ?? body.reason ?? 'unknown_error';\n switch (code) {\n case 'insufficient_credits':\n return new InsufficientCreditsError();\n case 'app_limit_exceeded':\n return new AppSpendingLimitExceededError(\n body.limit ?? null,\n body.spent ?? null,\n );\n case 'app_limit_not_set':\n return new AppLimitNotSetError();\n default:\n return new PromaError(\n body.error ?? `Gateway error (${status})`,\n code,\n status,\n );\n }\n}\n","/**\n * PKCE helpers — browser + Node 18+ compatible via SubtleCrypto.\n */\n\nconst PKCE_STORAGE_KEY = 'proma_code_verifier';\n\n/**\n * Generates a cryptographically random code_verifier (43–128 chars from unreserved character set).\n */\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return base64url(bytes);\n}\n\n/**\n * Derives the code_challenge from a code_verifier using SHA-256 (S256 method).\n */\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n return base64url(new Uint8Array(hash));\n}\n\n/**\n * Saves the code_verifier to localStorage for retrieval after the redirect.\n */\nexport function saveCodeVerifier(verifier: string): void {\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem(PKCE_STORAGE_KEY, verifier);\n }\n}\n\n/**\n * Reads and removes the code_verifier from localStorage.\n */\nexport function consumeCodeVerifier(): string | null {\n if (typeof localStorage === 'undefined') return null;\n const verifier = localStorage.getItem(PKCE_STORAGE_KEY);\n localStorage.removeItem(PKCE_STORAGE_KEY);\n return verifier;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...bytes));\n return base64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n","import type { Session, TokenStorage } from './types';\n\nconst SESSION_KEY = 'proma_session';\n\nexport class TokenStore {\n constructor(private readonly storage: TokenStorage) {}\n\n get(): Session | null {\n try {\n const raw = this.storage.getItem(SESSION_KEY);\n if (!raw) return null;\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n set(session: Session): void {\n this.storage.setItem(SESSION_KEY, JSON.stringify(session));\n }\n\n clear(): void {\n this.storage.removeItem(SESSION_KEY);\n // Also clear the PKCE verifier if present\n this.storage.removeItem('proma_code_verifier');\n }\n\n isExpired(session: Session): boolean {\n // Consider expired 30 seconds before actual expiry\n return Date.now() >= session.expiresAt - 30_000;\n }\n}\n\n/** Default in-memory storage for environments without localStorage (SSR, Node). */\nexport class MemoryStorage implements TokenStorage {\n private map = new Map<string, string>();\n getItem(key: string) {\n return this.map.get(key) ?? null;\n }\n setItem(key: string, value: string) {\n this.map.set(key, value);\n }\n removeItem(key: string) {\n this.map.delete(key);\n }\n}\n\nexport function getDefaultStorage(): TokenStorage {\n if (typeof localStorage !== 'undefined') return localStorage;\n return new MemoryStorage();\n}\n","import { errorFromGatewayResponse } from './errors';\nimport {\n consumeCodeVerifier,\n generateCodeChallenge,\n generateCodeVerifier,\n saveCodeVerifier,\n} from './pkce';\nimport { TokenStore, getDefaultStorage } from './storage';\nimport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n ChatStructuredOptions,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://www.proma.dev';\n\n/**\n * Deduplicates concurrent handleCallback() calls with the same code.\n * This handles React Strict Mode's double-effect invocation, which would\n * otherwise consume the PKCE verifier and state on the first call, leaving\n * nothing for the second call.\n */\nconst pendingCallbacks = new Map<string, Promise<Session>>();\n\nexport class PromaClient {\n readonly baseUrl: string;\n private readonly store: TokenStore;\n private readonly defaultScopes: OAuthScope[];\n\n /** Credits API — requires the `credits` scope. */\n readonly credits: CreditsApi;\n\n /** AI gateway API — requires the `ai:chat` scope. */\n readonly ai: AiApi;\n\n constructor(private readonly config: PromaClientConfig) {\n this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n this.store = new TokenStore(config.storage ?? getDefaultStorage());\n this.defaultScopes = config.scopes ?? ['profile'];\n this.credits = new CreditsApi(this);\n this.ai = new AiApi(this);\n }\n\n // ---------------------------------------------------------------------------\n // Auth\n // ---------------------------------------------------------------------------\n\n /**\n * Redirects the user to Proma's login page.\n * Call this on a button click — it will navigate away from the current page.\n *\n * @example\n * button.onclick = () => proma.login()\n */\n async login(scopes?: OAuthScope[]): Promise<void> {\n const url = await this.buildAuthorizeUrl(scopes ?? this.defaultScopes);\n window.location.href = url;\n }\n\n /**\n * Builds the authorization URL without navigating.\n * Useful if you want to control the redirect yourself.\n */\n async buildAuthorizeUrl(\n scopes: OAuthScope[] = this.defaultScopes,\n ): Promise<string> {\n const verifier = generateCodeVerifier();\n const challenge = await generateCodeChallenge(verifier);\n saveCodeVerifier(verifier);\n\n // Generate and persist state for CSRF protection.\n // Use a set so multiple concurrent login() calls don't clobber each other\n // (e.g. auth guards that call login() again on the callback page).\n const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n stored.push(state);\n localStorage.setItem(\n 'proma_oauth_states',\n JSON.stringify(stored.slice(-10)),\n );\n }\n\n const url = new URL('/api/oauth/authorize', this.baseUrl);\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', this.config.redirectUri);\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('state', state);\n url.searchParams.set('code_challenge', challenge);\n url.searchParams.set('code_challenge_method', 'S256');\n\n return url.toString();\n }\n\n /**\n * Handles the OAuth callback. Call this on your redirect page.\n * Reads the `code` from the URL, exchanges it for tokens, and stores the session.\n *\n * @param url - Defaults to `window.location.href`\n * @returns The new session\n *\n * @example\n * // pages/callback.tsx\n * useEffect(() => {\n * proma.handleCallback().then(session => {\n * router.push('/dashboard')\n * })\n * }, [])\n */\n async handleCallback(url?: string): Promise<Session> {\n const href =\n url ?? (typeof window !== 'undefined' ? window.location.href : '');\n const params = new URL(href).searchParams;\n const code = params.get('code');\n const error = params.get('error');\n\n if (error) {\n throw new Error(params.get('error_description') ?? error);\n }\n\n if (!code) {\n throw new Error('No authorization code found in URL');\n }\n\n // Deduplicate: React Strict Mode fires effects twice with the same code.\n // Return the in-flight promise so the state/verifier are only consumed once.\n const pending = pendingCallbacks.get(code);\n if (pending) return pending;\n\n const promise = this.exchangeCode(code, params);\n pendingCallbacks.set(code, promise);\n promise.finally(() => pendingCallbacks.delete(code));\n return promise;\n }\n\n private async exchangeCode(\n code: string,\n params: URLSearchParams,\n ): Promise<Session> {\n // Validate state parameter to prevent CSRF attacks.\n // Accepts any state from the stored set (handles concurrent/repeated login calls).\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const stored = JSON.parse(\n localStorage.getItem('proma_oauth_states') ?? '[]',\n ) as string[];\n\n // Fall back to legacy single-value key for backward compatibility\n if (stored.length === 0) {\n const legacy = localStorage.getItem('proma_oauth_state');\n if (legacy) stored.push(legacy);\n }\n\n if (!returnedState || !stored.includes(returnedState)) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\n\n // Remove the consumed state and persist the remainder\n const remaining = stored.filter((s) => s !== returnedState);\n if (remaining.length === 0) {\n localStorage.removeItem('proma_oauth_states');\n } else {\n localStorage.setItem('proma_oauth_states', JSON.stringify(remaining));\n }\n localStorage.removeItem('proma_oauth_state'); // clean up legacy key\n }\n\n const verifier = consumeCodeVerifier();\n\n const body = new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: this.config.redirectUri,\n client_id: this.config.clientId,\n });\n\n if (verifier) body.set('code_verifier', verifier);\n\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n /**\n * Returns the current session (access token, refresh token, expiry).\n * Automatically refreshes the access token if it is expired.\n * Returns `null` if the user is not logged in.\n */\n async getSession(): Promise<Session | null> {\n const session = this.store.get();\n if (!session) return null;\n\n if (this.store.isExpired(session)) {\n try {\n return await this.refresh(session.refreshToken);\n } catch {\n this.store.clear();\n return null;\n }\n }\n\n return session;\n }\n\n /**\n * Returns `true` if the user has a valid (or refreshable) session.\n */\n async isAuthenticated(): Promise<boolean> {\n return (await this.getSession()) !== null;\n }\n\n /**\n * Fetches the logged-in user's profile.\n * Requires the `profile` scope.\n */\n async getUser(): Promise<UserInfo> {\n const token = await this.requireAccessToken();\n const res = await fetch(`${this.baseUrl}/api/oauth/userinfo`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch user info');\n return res.json() as Promise<UserInfo>;\n }\n\n /**\n * Clears the stored session and logs the user out.\n * Does not revoke the token server-side.\n */\n logout(): void {\n this.store.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Internal helpers (used by sub-APIs)\n // ---------------------------------------------------------------------------\n\n async requireAccessToken(): Promise<string> {\n const session = await this.getSession();\n if (!session)\n throw new Error('Not authenticated — call proma.login() first');\n return session.accessToken;\n }\n\n private async refresh(refreshToken: string): Promise<Session> {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this.config.clientId,\n });\n const tokens = await this.fetchTokens(body);\n const session = this.tokensToSession(tokens);\n this.store.set(session);\n return session;\n }\n\n private async fetchTokens(body: URLSearchParams): Promise<TokenResponse> {\n const res = await fetch(`${this.baseUrl}/api/oauth/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: body.toString(),\n });\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as {\n error: string;\n error_description?: string;\n };\n throw new Error(err.error_description ?? err.error);\n }\n return res.json() as Promise<TokenResponse>;\n }\n\n private tokensToSession(tokens: TokenResponse): Session {\n return {\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresAt: Date.now() + tokens.expires_in * 1000,\n scope: tokens.scope,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Credits API\n// ---------------------------------------------------------------------------\n\nclass CreditsApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Returns the user's current credit balance.\n * Requires scope: `credits`\n *\n * @example\n * const { balance, formatted } = await proma.credits.getBalance()\n * console.log(`You have ${formatted}`) // \"You have $1.23\"\n */\n async getBalance(): Promise<BalanceResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/balance`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n if (!res.ok) throw new Error('Failed to fetch credit balance');\n return res.json() as Promise<BalanceResponse>;\n }\n\n /**\n * Deducts credits from the user's account.\n * Requires scope: `credits`\n *\n * @param amount - Micro-credits to spend. 1,000,000 = $1.00\n * @param description - Optional description for the transaction ledger.\n *\n * @example\n * await proma.credits.spend(500_000, 'Generated a report')\n */\n async spend(\n amount: number,\n description?: string,\n ): Promise<SpendCreditsResponse> {\n const token = await this.client.requireAccessToken();\n const res = await fetch(`${this.client.baseUrl}/api/sdk/credits/spend`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ amount, description }),\n });\n if (!res.ok) {\n const err = (await res.json().catch(() => ({ error: 'unknown' }))) as {\n error: string;\n };\n throw new Error(err.error);\n }\n return res.json() as Promise<SpendCreditsResponse>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// AI API\n// ---------------------------------------------------------------------------\n\nclass AiApi {\n constructor(private readonly client: PromaClient) {}\n\n /**\n * Sends a chat request through the Proma AI gateway.\n * Credits are deducted automatically per token used.\n * Requires scope: `ai:chat`\n *\n * Defaults to `anthropic` + `claude-3-5-haiku-latest`. Pass `provider` and\n * `model` to route elsewhere (gemini / openrouter).\n *\n * Returns a streaming `Response` — iterate SSE chunks or use a helper library.\n *\n * @example\n * const stream = await proma.ai.chat({\n * messages: [{ role: 'user', content: 'Explain quantum entanglement simply.' }]\n * })\n * const reader = stream.body.getReader()\n */\n async chat(options: ChatOptions | ChatMessage[]): Promise<Response> {\n const token = await this.client.requireAccessToken();\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const { provider, model } = resolveProviderAndModel(params);\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: params.messages,\n maxOutputTokens: params.maxOutputTokens,\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n return response;\n }\n\n /**\n * Convenience wrapper around `chat` that collects the full streamed text.\n * Use this when you don't need streaming and just want the final string.\n *\n * @example\n * const text = await proma.ai.chatText({\n * messages: [{ role: 'user', content: 'Hello!' }]\n * })\n * console.log(text)\n */\n async chatText(options: ChatOptions | ChatMessage[]): Promise<string> {\n // chat() throws a structured PromaError on non-OK responses, so by the\n // time we get here the response is a successful plain-text stream\n // (Vercel AI SDK's toTextStreamResponse — no JSON envelope).\n const res = await this.chat(options);\n\n const reader = res.body?.getReader();\n if (!reader) return '';\n\n const decoder = new TextDecoder();\n let fullText = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n fullText += decoder.decode(value, { stream: true });\n }\n fullText += decoder.decode();\n\n return fullText;\n }\n\n /**\n * Sends a chat request and parses the response as JSON. The model is nudged\n * with a system hint to emit JSON, and common wrappers (```json fences,\n * leading/trailing prose) are stripped before parsing.\n *\n * Best-effort: if the model returns malformed JSON, `fallback` is returned\n * when provided; otherwise an error is thrown. For guaranteed schema\n * compliance use `chatStructured()` instead.\n *\n * @example\n * const { title, tags } = await proma.ai.chatJSON<{ title: string; tags: string[] }>({\n * messages: [{ role: 'user', content: 'Suggest a title and 3 tags for this post' }],\n * })\n */\n async chatJSON<T = unknown>(\n options: ChatOptions | ChatMessage[],\n fallback?: T,\n ): Promise<T> {\n const params: ChatOptions = Array.isArray(options)\n ? { messages: options }\n : options;\n\n const hasSystem = params.messages.some((m) => m.role === 'system');\n const messages: ChatMessage[] = hasSystem\n ? params.messages\n : [\n {\n role: 'system',\n content:\n 'You must reply with a single valid JSON value and nothing else. No prose, no markdown fences.',\n },\n ...params.messages,\n ];\n\n const text = await this.chatText({ ...params, messages });\n const cleaned = stripJsonFences(text);\n\n try {\n return JSON.parse(cleaned) as T;\n } catch (err) {\n if (fallback !== undefined) return fallback;\n throw new Error(\n `chatJSON: model returned invalid JSON (${(err as Error).message}). Pass a fallback to tolerate parse failures, or use chatStructured() for guaranteed schema compliance.`,\n );\n }\n }\n\n /**\n * Sends a chat request with a JSON Schema and returns a typed object that is\n * guaranteed to match the schema (enforced provider-side via structured\n * output / function calling).\n *\n * @example\n * interface Summary { score: number; tags: string[] }\n * const result = await proma.ai.chatStructured<Summary>({\n * messages: [{ role: 'user', content: 'Rate and tag this article' }],\n * schema: {\n * type: 'object',\n * properties: {\n * score: { type: 'number' },\n * tags: { type: 'array', items: { type: 'string' } },\n * },\n * required: ['score', 'tags'],\n * },\n * })\n */\n async chatStructured<T = unknown>(\n options: ChatStructuredOptions,\n ): Promise<T> {\n const token = await this.client.requireAccessToken();\n const { provider, model } = resolveProviderAndModel(options);\n\n const response = await fetch(`${this.client.baseUrl}/api/gateway/chat`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n provider,\n model,\n messages: options.messages,\n maxOutputTokens: options.maxOutputTokens,\n responseFormat: {\n type: 'json_schema',\n ...(options.schemaName && { name: options.schemaName }),\n schema: options.schema,\n },\n }),\n });\n\n if (!response.ok) {\n const body = (await response\n .json()\n .catch(() => ({ error: 'unknown_error' }))) as Record<string, unknown>;\n throw errorFromGatewayResponse(response.status, body);\n }\n\n const payload = (await response.json()) as { object: T };\n return payload.object;\n }\n}\n\nfunction resolveProviderAndModel(options: ChatOptions): {\n provider: 'gemini' | 'anthropic' | 'openrouter';\n model: string;\n} {\n const provider = options.provider ?? 'anthropic';\n\n const defaultModel =\n provider === 'anthropic'\n ? 'claude-3-5-haiku-latest'\n : provider === 'gemini'\n ? 'gemini-2.0-flash'\n : '';\n\n const model = options.model ?? defaultModel;\n\n if (!model) {\n throw new Error(\n `model is required when provider is \"${provider}\" — pass e.g. { provider: \"${provider}\", model: \"...\" }`,\n );\n }\n\n return { provider, model };\n}\n\nfunction stripJsonFences(text: string): string {\n const trimmed = text.trim();\n const fenced = trimmed.match(/^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/i);\n if (fenced) return fenced[1]!.trim();\n\n // Fall back to grabbing the outermost {...} or [...] block if the model wrapped\n // the JSON in prose.\n const firstBrace = trimmed.search(/[{[]/);\n const lastBrace = Math.max(trimmed.lastIndexOf('}'), trimmed.lastIndexOf(']'));\n if (firstBrace !== -1 && lastBrace > firstBrace) {\n return trimmed.slice(firstBrace, lastBrace + 1);\n }\n\n return trimmed;\n}\n"],"mappings":"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","names":[]}
|