@proma-dev/sdk 0.1.2 → 0.1.3
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 +28 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +28 -5
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +28 -5
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +28 -5
- package/dist/react/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -131,12 +131,20 @@ var PromaClient = class {
|
|
|
131
131
|
* Useful if you want to control the redirect yourself.
|
|
132
132
|
*/
|
|
133
133
|
async buildAuthorizeUrl(scopes = this.defaultScopes) {
|
|
134
|
+
var _a;
|
|
134
135
|
const verifier = generateCodeVerifier();
|
|
135
136
|
const challenge = await generateCodeChallenge(verifier);
|
|
136
137
|
saveCodeVerifier(verifier);
|
|
137
138
|
const state = crypto.randomUUID();
|
|
138
139
|
if (typeof localStorage !== "undefined") {
|
|
139
|
-
|
|
140
|
+
const stored = JSON.parse(
|
|
141
|
+
(_a = localStorage.getItem("proma_oauth_states")) != null ? _a : "[]"
|
|
142
|
+
);
|
|
143
|
+
stored.push(state);
|
|
144
|
+
localStorage.setItem(
|
|
145
|
+
"proma_oauth_states",
|
|
146
|
+
JSON.stringify(stored.slice(-10))
|
|
147
|
+
);
|
|
140
148
|
}
|
|
141
149
|
const url = new URL("/api/oauth/authorize", this.baseUrl);
|
|
142
150
|
url.searchParams.set("client_id", this.config.clientId);
|
|
@@ -164,7 +172,7 @@ var PromaClient = class {
|
|
|
164
172
|
* }, [])
|
|
165
173
|
*/
|
|
166
174
|
async handleCallback(url) {
|
|
167
|
-
var _a;
|
|
175
|
+
var _a, _b;
|
|
168
176
|
const href = url != null ? url : typeof window !== "undefined" ? window.location.href : "";
|
|
169
177
|
const params = new URL(href).searchParams;
|
|
170
178
|
const code = params.get("code");
|
|
@@ -177,11 +185,26 @@ var PromaClient = class {
|
|
|
177
185
|
}
|
|
178
186
|
const returnedState = params.get("state");
|
|
179
187
|
if (typeof localStorage !== "undefined") {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
const stored = JSON.parse(
|
|
189
|
+
(_b = localStorage.getItem("proma_oauth_states")) != null ? _b : "[]"
|
|
190
|
+
);
|
|
191
|
+
if (stored.length === 0) {
|
|
192
|
+
const legacy = localStorage.getItem("proma_oauth_state");
|
|
193
|
+
if (legacy) stored.push(legacy);
|
|
194
|
+
}
|
|
195
|
+
if (!returnedState || !stored.includes(returnedState)) {
|
|
183
196
|
throw new Error("Invalid state parameter \u2014 possible CSRF attack");
|
|
184
197
|
}
|
|
198
|
+
const remaining = stored.filter((s) => s !== returnedState);
|
|
199
|
+
if (remaining.length === 0) {
|
|
200
|
+
localStorage.removeItem("proma_oauth_states");
|
|
201
|
+
} else {
|
|
202
|
+
localStorage.setItem(
|
|
203
|
+
"proma_oauth_states",
|
|
204
|
+
JSON.stringify(remaining)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
localStorage.removeItem("proma_oauth_state");
|
|
185
208
|
}
|
|
186
209
|
const verifier = consumeCodeVerifier();
|
|
187
210
|
const body = new URLSearchParams({
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { PromaClient } from './client';\nexport { MemoryStorage } from './storage';\nexport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n TokenStorage,\n UserInfo,\n} from './types';\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem('proma_oauth_state', state);\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 // Validate state parameter to prevent CSRF attacks\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const expectedState = localStorage.getItem('proma_oauth_state');\n localStorage.removeItem('proma_oauth_state');\n if (!returnedState || returnedState !== expectedState) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { PromaClient } from './client';\nexport { MemoryStorage } from './storage';\nexport type {\n BalanceResponse,\n ChatMessage,\n ChatOptions,\n OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n TokenStorage,\n UserInfo,\n} from './types';\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 // 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(\n 'proma_oauth_states',\n JSON.stringify(remaining),\n );\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,IAAM,mBAAmB;AAKlB,SAAS,uBAA+B;AAC7C,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,UAAU,KAAK;AACxB;AAKA,eAAsB,sBAAsB,UAAmC;AAC7E,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,QAAQ;AACpC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AACvD,SAAO,UAAU,IAAI,WAAW,IAAI,CAAC;AACvC;AAKO,SAAS,iBAAiB,UAAwB;AACvD,MAAI,OAAO,iBAAiB,aAAa;AACvC,iBAAa,QAAQ,kBAAkB,QAAQ;AAAA,EACjD;AACF;AAKO,SAAS,sBAAqC;AACnD,MAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,QAAM,WAAW,aAAa,QAAQ,gBAAgB;AACtD,eAAa,WAAW,gBAAgB;AACxC,SAAO;AACT;AAEA,SAAS,UAAU,OAA2B;AAC5C,QAAM,SAAS,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC;AACjD,SAAO,OAAO,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACzE;;;AC7CA,IAAM,cAAc;AAEb,IAAM,aAAN,MAAiB;AAAA,EACtB,YAA6B,SAAuB;AAAvB;AAAA,EAAwB;AAAA,EAErD,MAAsB;AACpB,QAAI;AACF,YAAM,MAAM,KAAK,QAAQ,QAAQ,WAAW;AAC5C,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,SAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,IAAI,SAAwB;AAC1B,SAAK,QAAQ,QAAQ,aAAa,KAAK,UAAU,OAAO,CAAC;AAAA,EAC3D;AAAA,EAEA,QAAc;AACZ,SAAK,QAAQ,WAAW,WAAW;AAEnC,SAAK,QAAQ,WAAW,qBAAqB;AAAA,EAC/C;AAAA,EAEA,UAAU,SAA2B;AAEnC,WAAO,KAAK,IAAI,KAAK,QAAQ,YAAY;AAAA,EAC3C;AACF;AAGO,IAAM,gBAAN,MAA4C;AAAA,EAA5C;AACL,SAAQ,MAAM,oBAAI,IAAoB;AAAA;AAAA,EACtC,QAAQ,KAAa;AApCvB;AAqCI,YAAO,UAAK,IAAI,IAAI,GAAG,MAAhB,YAAqB;AAAA,EAC9B;AAAA,EACA,QAAQ,KAAa,OAAe;AAClC,SAAK,IAAI,IAAI,KAAK,KAAK;AAAA,EACzB;AAAA,EACA,WAAW,KAAa;AACtB,SAAK,IAAI,OAAO,GAAG;AAAA,EACrB;AACF;AAEO,SAAS,oBAAkC;AAChD,MAAI,OAAO,iBAAiB,YAAa,QAAO;AAChD,SAAO,IAAI,cAAc;AAC3B;;;AC/BA,IAAM,mBAAmB;AAElB,IAAM,cAAN,MAAkB;AAAA,EAWvB,YAA6B,QAA2B;AAA3B;AAhC/B;AAiCI,SAAK,WAAU,YAAO,YAAP,YAAkB;AACjC,SAAK,QAAQ,IAAI,YAAW,YAAO,YAAP,YAAkB,kBAAkB,CAAC;AACjE,SAAK,iBAAgB,YAAO,WAAP,YAAiB,CAAC,SAAS;AAChD,SAAK,UAAU,IAAI,WAAW,IAAI;AAClC,SAAK,KAAK,IAAI,MAAM,IAAI;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,MAAM,QAAsC;AAChD,UAAM,MAAM,MAAM,KAAK,kBAAkB,0BAAU,KAAK,aAAa;AACrE,WAAO,SAAS,OAAO;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,kBACJ,SAAuB,KAAK,eACX;AA9DrB;AA+DI,UAAM,WAAW,qBAAqB;AACtC,UAAM,YAAY,MAAM,sBAAsB,QAAQ;AACtD,qBAAiB,QAAQ;AAKzB,UAAM,QAAQ,OAAO,WAAW;AAChC,QAAI,OAAO,iBAAiB,aAAa;AACvC,YAAM,SAAS,KAAK;AAAA,SAClB,kBAAa,QAAQ,oBAAoB,MAAzC,YAA8C;AAAA,MAChD;AACA,aAAO,KAAK,KAAK;AACjB,mBAAa;AAAA,QACX;AAAA,QACA,KAAK,UAAU,OAAO,MAAM,GAAG,CAAC;AAAA,MAClC;AAAA,IACF;AAEA,UAAM,MAAM,IAAI,IAAI,wBAAwB,KAAK,OAAO;AACxD,QAAI,aAAa,IAAI,aAAa,KAAK,OAAO,QAAQ;AACtD,QAAI,aAAa,IAAI,gBAAgB,KAAK,OAAO,WAAW;AAC5D,QAAI,aAAa,IAAI,iBAAiB,MAAM;AAC5C,QAAI,aAAa,IAAI,SAAS,OAAO,KAAK,GAAG,CAAC;AAC9C,QAAI,aAAa,IAAI,SAAS,KAAK;AACnC,QAAI,aAAa,IAAI,kBAAkB,SAAS;AAChD,QAAI,aAAa,IAAI,yBAAyB,MAAM;AAEpD,WAAO,IAAI,SAAS;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,MAAM,eAAe,KAAgC;AA7GvD;AA8GI,UAAM,OACJ,oBAAQ,OAAO,WAAW,cAAc,OAAO,SAAS,OAAO;AACjE,UAAM,SAAS,IAAI,IAAI,IAAI,EAAE;AAC7B,UAAM,OAAO,OAAO,IAAI,MAAM;AAC9B,UAAM,QAAQ,OAAO,IAAI,OAAO;AAEhC,QAAI,OAAO;AACT,YAAM,IAAI,OAAM,YAAO,IAAI,mBAAmB,MAA9B,YAAmC,KAAK;AAAA,IAC1D;AAEA,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAIA,UAAM,gBAAgB,OAAO,IAAI,OAAO;AACxC,QAAI,OAAO,iBAAiB,aAAa;AACvC,YAAM,SAAS,KAAK;AAAA,SAClB,kBAAa,QAAQ,oBAAoB,MAAzC,YAA8C;AAAA,MAChD;AAGA,UAAI,OAAO,WAAW,GAAG;AACvB,cAAM,SAAS,aAAa,QAAQ,mBAAmB;AACvD,YAAI,OAAQ,QAAO,KAAK,MAAM;AAAA,MAChC;AAEA,UAAI,CAAC,iBAAiB,CAAC,OAAO,SAAS,aAAa,GAAG;AACrD,cAAM,IAAI,MAAM,qDAAgD;AAAA,MAClE;AAGA,YAAM,YAAY,OAAO,OAAO,CAAC,MAAM,MAAM,aAAa;AAC1D,UAAI,UAAU,WAAW,GAAG;AAC1B,qBAAa,WAAW,oBAAoB;AAAA,MAC9C,OAAO;AACL,qBAAa;AAAA,UACX;AAAA,UACA,KAAK,UAAU,SAAS;AAAA,QAC1B;AAAA,MACF;AACA,mBAAa,WAAW,mBAAmB;AAAA,IAC7C;AAEA,UAAM,WAAW,oBAAoB;AAErC,UAAM,OAAO,IAAI,gBAAgB;AAAA,MAC/B,YAAY;AAAA,MACZ;AAAA,MACA,cAAc,KAAK,OAAO;AAAA,MAC1B,WAAW,KAAK,OAAO;AAAA,IACzB,CAAC;AAED,QAAI,SAAU,MAAK,IAAI,iBAAiB,QAAQ;AAEhD,UAAM,SAAS,MAAM,KAAK,YAAY,IAAI;AAC1C,UAAM,UAAU,KAAK,gBAAgB,MAAM;AAC3C,SAAK,MAAM,IAAI,OAAO;AACtB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAsC;AAC1C,UAAM,UAAU,KAAK,MAAM,IAAI;AAC/B,QAAI,CAAC,QAAS,QAAO;AAErB,QAAI,KAAK,MAAM,UAAU,OAAO,GAAG;AACjC,UAAI;AACF,eAAO,MAAM,KAAK,QAAQ,QAAQ,YAAY;AAAA,MAChD,SAAQ;AACN,aAAK,MAAM,MAAM;AACjB,eAAO;AAAA,MACT;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,kBAAoC;AACxC,WAAQ,MAAM,KAAK,WAAW,MAAO;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,UAA6B;AACjC,UAAM,QAAQ,MAAM,KAAK,mBAAmB;AAC5C,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,uBAAuB;AAAA,MAC5D,SAAS,EAAE,eAAe,UAAU,KAAK,GAAG;AAAA,IAC9C,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,2BAA2B;AACxD,WAAO,IAAI,KAAK;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,SAAe;AACb,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,qBAAsC;AAC1C,UAAM,UAAU,MAAM,KAAK,WAAW;AACtC,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,mDAA8C;AAChE,WAAO,QAAQ;AAAA,EACjB;AAAA,EAEA,MAAc,QAAQ,cAAwC;AAC5D,UAAM,OAAO,IAAI,gBAAgB;AAAA,MAC/B,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,WAAW,KAAK,OAAO;AAAA,IACzB,CAAC;AACD,UAAM,SAAS,MAAM,KAAK,YAAY,IAAI;AAC1C,UAAM,UAAU,KAAK,gBAAgB,MAAM;AAC3C,SAAK,MAAM,IAAI,OAAO;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,MAAc,YAAY,MAA+C;AApP3E;AAqPI,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,oBAAoB;AAAA,MACzD,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D,MAAM,KAAK,SAAS;AAAA,IACtB,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,MAAO,MAAM,IAChB,KAAK,EACL,MAAM,OAAO,EAAE,OAAO,gBAAgB,EAAE;AAI3C,YAAM,IAAI,OAAM,SAAI,sBAAJ,YAAyB,IAAI,KAAK;AAAA,IACpD;AACA,WAAO,IAAI,KAAK;AAAA,EAClB;AAAA,EAEQ,gBAAgB,QAAgC;AACtD,WAAO;AAAA,MACL,aAAa,OAAO;AAAA,MACpB,cAAc,OAAO;AAAA,MACrB,WAAW,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,MAC5C,OAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;AAMA,IAAM,aAAN,MAAiB;AAAA,EACf,YAA6B,QAAqB;AAArB;AAAA,EAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUnD,MAAM,aAAuC;AAC3C,UAAM,QAAQ,MAAM,KAAK,OAAO,mBAAmB;AACnD,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,OAAO,4BAA4B;AAAA,MACxE,SAAS,EAAE,eAAe,UAAU,KAAK,GAAG;AAAA,IAC9C,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,gCAAgC;AAC7D,WAAO,IAAI,KAAK;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,MACJ,QACA,aAC+B;AAC/B,UAAM,QAAQ,MAAM,KAAK,OAAO,mBAAmB;AACnD,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,OAAO,0BAA0B;AAAA,MACtE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,QAAQ,YAAY,CAAC;AAAA,IAC9C,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,MAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,EAAE,OAAO,UAAU,EAAE;AAGhE,YAAM,IAAI,MAAM,IAAI,KAAK;AAAA,IAC3B;AACA,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;AAMA,IAAM,QAAN,MAAY;AAAA,EACV,YAA6B,QAAqB;AAArB;AAAA,EAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAenD,MAAM,KAAK,SAAyD;AA7VtE;AA8VI,UAAM,QAAQ,MAAM,KAAK,OAAO,mBAAmB;AACnD,UAAM,SAAsB,MAAM,QAAQ,OAAO,IAC7C,EAAE,UAAU,QAAQ,IACpB;AAEJ,WAAO,MAAM,GAAG,KAAK,OAAO,OAAO,qBAAqB;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,UAAU,OAAO;AAAA,QACjB,QAAO,YAAO,UAAP,YAAgB;AAAA,MACzB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,SAAS,SAAuD;AA1XxE;AA2XI,UAAM,MAAM,MAAM,KAAK,KAAK,OAAO;AACnC,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,MAAO,MAAM,IAChB,KAAK,EACL,MAAM,OAAO,EAAE,OAAO,iBAAiB,EAAE;AAC5C,YAAM,IAAI,MAAM,IAAI,KAAK;AAAA,IAC3B;AAEA,UAAM,UAAS,SAAI,SAAJ,mBAAU;AACzB,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,UAAU,IAAI,YAAY;AAChC,QAAI,WAAW;AAEf,WAAO,MAAM;AACX,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,UAAI,KAAM;AAEV,YAAM,QAAQ,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAEpD,iBAAW,QAAQ,MAAM,MAAM,IAAI,GAAG;AACpC,YAAI,CAAC,KAAK,WAAW,QAAQ,EAAG;AAChC,cAAM,OAAO,KAAK,MAAM,CAAC,EAAE,KAAK;AAChC,YAAI,SAAS,SAAU;AACvB,YAAI;AACF,gBAAM,SAAS,KAAK,MAAM,IAAI;AAK9B,gBAAM,QAAO,0CAAO,eAAP,mBAAoB,OAApB,mBAAwB,YAAxB,mBAAiC,UAAjC,mBAAyC,OAAzC,mBAA6C,SAA7C,YAAqD;AAClE,sBAAY;AAAA,QACd,SAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -104,12 +104,20 @@ var PromaClient = class {
|
|
|
104
104
|
* Useful if you want to control the redirect yourself.
|
|
105
105
|
*/
|
|
106
106
|
async buildAuthorizeUrl(scopes = this.defaultScopes) {
|
|
107
|
+
var _a;
|
|
107
108
|
const verifier = generateCodeVerifier();
|
|
108
109
|
const challenge = await generateCodeChallenge(verifier);
|
|
109
110
|
saveCodeVerifier(verifier);
|
|
110
111
|
const state = crypto.randomUUID();
|
|
111
112
|
if (typeof localStorage !== "undefined") {
|
|
112
|
-
|
|
113
|
+
const stored = JSON.parse(
|
|
114
|
+
(_a = localStorage.getItem("proma_oauth_states")) != null ? _a : "[]"
|
|
115
|
+
);
|
|
116
|
+
stored.push(state);
|
|
117
|
+
localStorage.setItem(
|
|
118
|
+
"proma_oauth_states",
|
|
119
|
+
JSON.stringify(stored.slice(-10))
|
|
120
|
+
);
|
|
113
121
|
}
|
|
114
122
|
const url = new URL("/api/oauth/authorize", this.baseUrl);
|
|
115
123
|
url.searchParams.set("client_id", this.config.clientId);
|
|
@@ -137,7 +145,7 @@ var PromaClient = class {
|
|
|
137
145
|
* }, [])
|
|
138
146
|
*/
|
|
139
147
|
async handleCallback(url) {
|
|
140
|
-
var _a;
|
|
148
|
+
var _a, _b;
|
|
141
149
|
const href = url != null ? url : typeof window !== "undefined" ? window.location.href : "";
|
|
142
150
|
const params = new URL(href).searchParams;
|
|
143
151
|
const code = params.get("code");
|
|
@@ -150,11 +158,26 @@ var PromaClient = class {
|
|
|
150
158
|
}
|
|
151
159
|
const returnedState = params.get("state");
|
|
152
160
|
if (typeof localStorage !== "undefined") {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
const stored = JSON.parse(
|
|
162
|
+
(_b = localStorage.getItem("proma_oauth_states")) != null ? _b : "[]"
|
|
163
|
+
);
|
|
164
|
+
if (stored.length === 0) {
|
|
165
|
+
const legacy = localStorage.getItem("proma_oauth_state");
|
|
166
|
+
if (legacy) stored.push(legacy);
|
|
167
|
+
}
|
|
168
|
+
if (!returnedState || !stored.includes(returnedState)) {
|
|
156
169
|
throw new Error("Invalid state parameter \u2014 possible CSRF attack");
|
|
157
170
|
}
|
|
171
|
+
const remaining = stored.filter((s) => s !== returnedState);
|
|
172
|
+
if (remaining.length === 0) {
|
|
173
|
+
localStorage.removeItem("proma_oauth_states");
|
|
174
|
+
} else {
|
|
175
|
+
localStorage.setItem(
|
|
176
|
+
"proma_oauth_states",
|
|
177
|
+
JSON.stringify(remaining)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
localStorage.removeItem("proma_oauth_state");
|
|
158
181
|
}
|
|
159
182
|
const verifier = consumeCodeVerifier();
|
|
160
183
|
const body = new URLSearchParams({
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["/**\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem('proma_oauth_state', state);\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 // Validate state parameter to prevent CSRF attacks\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const expectedState = localStorage.getItem('proma_oauth_state');\n localStorage.removeItem('proma_oauth_state');\n if (!returnedState || returnedState !== expectedState) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["/**\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 // 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(\n 'proma_oauth_states',\n JSON.stringify(remaining),\n );\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/react/index.cjs
CHANGED
|
@@ -149,12 +149,20 @@ var PromaClient = class {
|
|
|
149
149
|
* Useful if you want to control the redirect yourself.
|
|
150
150
|
*/
|
|
151
151
|
async buildAuthorizeUrl(scopes = this.defaultScopes) {
|
|
152
|
+
var _a;
|
|
152
153
|
const verifier = generateCodeVerifier();
|
|
153
154
|
const challenge = await generateCodeChallenge(verifier);
|
|
154
155
|
saveCodeVerifier(verifier);
|
|
155
156
|
const state = crypto.randomUUID();
|
|
156
157
|
if (typeof localStorage !== "undefined") {
|
|
157
|
-
|
|
158
|
+
const stored = JSON.parse(
|
|
159
|
+
(_a = localStorage.getItem("proma_oauth_states")) != null ? _a : "[]"
|
|
160
|
+
);
|
|
161
|
+
stored.push(state);
|
|
162
|
+
localStorage.setItem(
|
|
163
|
+
"proma_oauth_states",
|
|
164
|
+
JSON.stringify(stored.slice(-10))
|
|
165
|
+
);
|
|
158
166
|
}
|
|
159
167
|
const url = new URL("/api/oauth/authorize", this.baseUrl);
|
|
160
168
|
url.searchParams.set("client_id", this.config.clientId);
|
|
@@ -182,7 +190,7 @@ var PromaClient = class {
|
|
|
182
190
|
* }, [])
|
|
183
191
|
*/
|
|
184
192
|
async handleCallback(url) {
|
|
185
|
-
var _a;
|
|
193
|
+
var _a, _b;
|
|
186
194
|
const href = url != null ? url : typeof window !== "undefined" ? window.location.href : "";
|
|
187
195
|
const params = new URL(href).searchParams;
|
|
188
196
|
const code = params.get("code");
|
|
@@ -195,11 +203,26 @@ var PromaClient = class {
|
|
|
195
203
|
}
|
|
196
204
|
const returnedState = params.get("state");
|
|
197
205
|
if (typeof localStorage !== "undefined") {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
const stored = JSON.parse(
|
|
207
|
+
(_b = localStorage.getItem("proma_oauth_states")) != null ? _b : "[]"
|
|
208
|
+
);
|
|
209
|
+
if (stored.length === 0) {
|
|
210
|
+
const legacy = localStorage.getItem("proma_oauth_state");
|
|
211
|
+
if (legacy) stored.push(legacy);
|
|
212
|
+
}
|
|
213
|
+
if (!returnedState || !stored.includes(returnedState)) {
|
|
201
214
|
throw new Error("Invalid state parameter \u2014 possible CSRF attack");
|
|
202
215
|
}
|
|
216
|
+
const remaining = stored.filter((s) => s !== returnedState);
|
|
217
|
+
if (remaining.length === 0) {
|
|
218
|
+
localStorage.removeItem("proma_oauth_states");
|
|
219
|
+
} else {
|
|
220
|
+
localStorage.setItem(
|
|
221
|
+
"proma_oauth_states",
|
|
222
|
+
JSON.stringify(remaining)
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
localStorage.removeItem("proma_oauth_state");
|
|
203
226
|
}
|
|
204
227
|
const verifier = consumeCodeVerifier();
|
|
205
228
|
const body = new URLSearchParams({
|
package/dist/react/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/index.ts","../../src/react/login-button.tsx","../../src/pkce.ts","../../src/storage.ts","../../src/client.ts","../../src/react/proma-provider.tsx"],"sourcesContent":["export { LoginWithProma } from './login-button';\nexport { PromaProvider, usePromaAuth } from './proma-provider';\n","import { type ReactNode, useState } from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope } from '../types';\n\ninterface LoginWithPromaProps {\n clientId: string;\n redirectUri: string;\n scopes?: OAuthScope[];\n baseUrl?: string;\n onError?: (error: Error) => void;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * A ready-to-use \"Login with Proma\" button.\n *\n * @example\n * <LoginWithProma\n * clientId=\"proma_app_abc123\"\n * redirectUri=\"https://myapp.com/callback\"\n * scopes={['profile', 'credits']}\n * />\n */\nexport function LoginWithProma({\n clientId,\n redirectUri,\n scopes = ['profile'],\n baseUrl,\n onError,\n children,\n className,\n}: LoginWithPromaProps) {\n const [isLoading, setIsLoading] = useState(false);\n\n async function handleClick() {\n setIsLoading(true);\n\n try {\n const client = new PromaClient({ clientId, redirectUri, baseUrl });\n await client.login(scopes);\n } catch (err) {\n setIsLoading(false);\n onError?.(err instanceof Error ? err : new Error('Authorization failed'));\n }\n }\n\n return (\n <button\n type={'button'}\n onClick={handleClick}\n disabled={isLoading}\n className={className ?? 'proma-login-button'}\n aria-label={'Login with Proma'}\n >\n {isLoading\n ? 'Redirecting…'\n : (children ?? (\n <>\n <PromaLogo />\n Login with Proma\n </>\n ))}\n </button>\n );\n}\n\nfunction PromaLogo() {\n return (\n <svg\n width={16}\n height={16}\n viewBox={'0 0 24 24'}\n fill={'currentColor'}\n aria-hidden={'true'}\n >\n <path\n d={\n 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z'\n }\n />\n </svg>\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem('proma_oauth_state', state);\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 // Validate state parameter to prevent CSRF attacks\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const expectedState = localStorage.getItem('proma_oauth_state');\n localStorage.removeItem('proma_oauth_state');\n if (!returnedState || returnedState !== expectedState) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope, PromaClientConfig, UserInfo } from '../types';\n\ninterface PromaAuthState {\n user: UserInfo | null;\n isLoading: boolean;\n isAuthenticated: boolean;\n /** Redirects the user to Proma's login page. */\n login: (scopes?: OAuthScope[]) => Promise<void>;\n /** Clears the session. */\n logout: () => void;\n /** The underlying PromaClient instance. */\n client: PromaClient;\n}\n\nconst PromaContext = createContext<PromaAuthState | null>(null);\n\ntype PromaProviderProps = PromaClientConfig & {\n children: React.ReactNode;\n};\n\n/**\n * Wraps your app with Proma auth context.\n * Call `usePromaAuth()` in any child component to access auth state.\n *\n * @example\n * <PromaProvider clientId=\"proma_app_xxx\" redirectUri=\"https://myapp.com/callback\">\n * <App />\n * </PromaProvider>\n */\nexport function PromaProvider({ children, ...config }: PromaProviderProps) {\n const [client] = useState(() => new PromaClient(config));\n const [user, setUser] = useState<UserInfo | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n // Restore session on mount\n useEffect(() => {\n let cancelled = false;\n\n async function restore() {\n try {\n const session = await client.getSession();\n if (!session || cancelled) {\n setIsLoading(false);\n return;\n }\n const userInfo = await client.getUser();\n if (!cancelled) setUser(userInfo);\n } catch {\n // no valid session\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n }\n\n void restore();\n return () => {\n cancelled = true;\n };\n }, [client]);\n\n const login = useCallback(\n (scopes?: OAuthScope[]) => client.login(scopes),\n [client],\n );\n\n const logout = useCallback(() => {\n client.logout();\n setUser(null);\n }, [client]);\n\n return (\n <PromaContext.Provider\n value={{\n user,\n isLoading,\n isAuthenticated: !!user,\n login,\n logout,\n client,\n }}\n >\n {children}\n </PromaContext.Provider>\n );\n}\n\n/**\n * Returns the current Proma auth state.\n * Must be used inside a `<PromaProvider>`.\n *\n * @example\n * const { user, isLoading, login, logout } = usePromaAuth()\n */\nexport function usePromaAuth(): PromaAuthState {\n const ctx = useContext(PromaContext);\n if (!ctx) {\n throw new Error('usePromaAuth must be used inside <PromaProvider>');\n }\n return ctx;\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/index.ts","../../src/react/login-button.tsx","../../src/pkce.ts","../../src/storage.ts","../../src/client.ts","../../src/react/proma-provider.tsx"],"sourcesContent":["export { LoginWithProma } from './login-button';\nexport { PromaProvider, usePromaAuth } from './proma-provider';\n","import { type ReactNode, useState } from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope } from '../types';\n\ninterface LoginWithPromaProps {\n clientId: string;\n redirectUri: string;\n scopes?: OAuthScope[];\n baseUrl?: string;\n onError?: (error: Error) => void;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * A ready-to-use \"Login with Proma\" button.\n *\n * @example\n * <LoginWithProma\n * clientId=\"proma_app_abc123\"\n * redirectUri=\"https://myapp.com/callback\"\n * scopes={['profile', 'credits']}\n * />\n */\nexport function LoginWithProma({\n clientId,\n redirectUri,\n scopes = ['profile'],\n baseUrl,\n onError,\n children,\n className,\n}: LoginWithPromaProps) {\n const [isLoading, setIsLoading] = useState(false);\n\n async function handleClick() {\n setIsLoading(true);\n\n try {\n const client = new PromaClient({ clientId, redirectUri, baseUrl });\n await client.login(scopes);\n } catch (err) {\n setIsLoading(false);\n onError?.(err instanceof Error ? err : new Error('Authorization failed'));\n }\n }\n\n return (\n <button\n type={'button'}\n onClick={handleClick}\n disabled={isLoading}\n className={className ?? 'proma-login-button'}\n aria-label={'Login with Proma'}\n >\n {isLoading\n ? 'Redirecting…'\n : (children ?? (\n <>\n <PromaLogo />\n Login with Proma\n </>\n ))}\n </button>\n );\n}\n\nfunction PromaLogo() {\n return (\n <svg\n width={16}\n height={16}\n viewBox={'0 0 24 24'}\n fill={'currentColor'}\n aria-hidden={'true'}\n >\n <path\n d={\n 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z'\n }\n />\n </svg>\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 // 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(\n 'proma_oauth_states',\n JSON.stringify(remaining),\n );\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope, PromaClientConfig, UserInfo } from '../types';\n\ninterface PromaAuthState {\n user: UserInfo | null;\n isLoading: boolean;\n isAuthenticated: boolean;\n /** Redirects the user to Proma's login page. */\n login: (scopes?: OAuthScope[]) => Promise<void>;\n /** Clears the session. */\n logout: () => void;\n /** The underlying PromaClient instance. */\n client: PromaClient;\n}\n\nconst PromaContext = createContext<PromaAuthState | null>(null);\n\ntype PromaProviderProps = PromaClientConfig & {\n children: React.ReactNode;\n};\n\n/**\n * Wraps your app with Proma auth context.\n * Call `usePromaAuth()` in any child component to access auth state.\n *\n * @example\n * <PromaProvider clientId=\"proma_app_xxx\" redirectUri=\"https://myapp.com/callback\">\n * <App />\n * </PromaProvider>\n */\nexport function PromaProvider({ children, ...config }: PromaProviderProps) {\n const [client] = useState(() => new PromaClient(config));\n const [user, setUser] = useState<UserInfo | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n // Restore session on mount\n useEffect(() => {\n let cancelled = false;\n\n async function restore() {\n try {\n const session = await client.getSession();\n if (!session || cancelled) {\n setIsLoading(false);\n return;\n }\n const userInfo = await client.getUser();\n if (!cancelled) setUser(userInfo);\n } catch {\n // no valid session\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n }\n\n void restore();\n return () => {\n cancelled = true;\n };\n }, [client]);\n\n const login = useCallback(\n (scopes?: OAuthScope[]) => client.login(scopes),\n [client],\n );\n\n const logout = useCallback(() => {\n client.logout();\n setUser(null);\n }, [client]);\n\n return (\n <PromaContext.Provider\n value={{\n user,\n isLoading,\n isAuthenticated: !!user,\n login,\n logout,\n client,\n }}\n >\n {children}\n </PromaContext.Provider>\n );\n}\n\n/**\n * Returns the current Proma auth state.\n * Must be used inside a `<PromaProvider>`.\n *\n * @example\n * const { user, isLoading, login, logout } = usePromaAuth()\n */\nexport function usePromaAuth(): PromaAuthState {\n const ctx = useContext(PromaContext);\n if (!ctx) {\n throw new Error('usePromaAuth must be used inside <PromaProvider>');\n }\n return ctx;\n}\n"],"mappings":"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","names":["import_react","import_jsx_runtime"]}
|
package/dist/react/index.js
CHANGED
|
@@ -123,12 +123,20 @@ var PromaClient = class {
|
|
|
123
123
|
* Useful if you want to control the redirect yourself.
|
|
124
124
|
*/
|
|
125
125
|
async buildAuthorizeUrl(scopes = this.defaultScopes) {
|
|
126
|
+
var _a;
|
|
126
127
|
const verifier = generateCodeVerifier();
|
|
127
128
|
const challenge = await generateCodeChallenge(verifier);
|
|
128
129
|
saveCodeVerifier(verifier);
|
|
129
130
|
const state = crypto.randomUUID();
|
|
130
131
|
if (typeof localStorage !== "undefined") {
|
|
131
|
-
|
|
132
|
+
const stored = JSON.parse(
|
|
133
|
+
(_a = localStorage.getItem("proma_oauth_states")) != null ? _a : "[]"
|
|
134
|
+
);
|
|
135
|
+
stored.push(state);
|
|
136
|
+
localStorage.setItem(
|
|
137
|
+
"proma_oauth_states",
|
|
138
|
+
JSON.stringify(stored.slice(-10))
|
|
139
|
+
);
|
|
132
140
|
}
|
|
133
141
|
const url = new URL("/api/oauth/authorize", this.baseUrl);
|
|
134
142
|
url.searchParams.set("client_id", this.config.clientId);
|
|
@@ -156,7 +164,7 @@ var PromaClient = class {
|
|
|
156
164
|
* }, [])
|
|
157
165
|
*/
|
|
158
166
|
async handleCallback(url) {
|
|
159
|
-
var _a;
|
|
167
|
+
var _a, _b;
|
|
160
168
|
const href = url != null ? url : typeof window !== "undefined" ? window.location.href : "";
|
|
161
169
|
const params = new URL(href).searchParams;
|
|
162
170
|
const code = params.get("code");
|
|
@@ -169,11 +177,26 @@ var PromaClient = class {
|
|
|
169
177
|
}
|
|
170
178
|
const returnedState = params.get("state");
|
|
171
179
|
if (typeof localStorage !== "undefined") {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
const stored = JSON.parse(
|
|
181
|
+
(_b = localStorage.getItem("proma_oauth_states")) != null ? _b : "[]"
|
|
182
|
+
);
|
|
183
|
+
if (stored.length === 0) {
|
|
184
|
+
const legacy = localStorage.getItem("proma_oauth_state");
|
|
185
|
+
if (legacy) stored.push(legacy);
|
|
186
|
+
}
|
|
187
|
+
if (!returnedState || !stored.includes(returnedState)) {
|
|
175
188
|
throw new Error("Invalid state parameter \u2014 possible CSRF attack");
|
|
176
189
|
}
|
|
190
|
+
const remaining = stored.filter((s) => s !== returnedState);
|
|
191
|
+
if (remaining.length === 0) {
|
|
192
|
+
localStorage.removeItem("proma_oauth_states");
|
|
193
|
+
} else {
|
|
194
|
+
localStorage.setItem(
|
|
195
|
+
"proma_oauth_states",
|
|
196
|
+
JSON.stringify(remaining)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
localStorage.removeItem("proma_oauth_state");
|
|
177
200
|
}
|
|
178
201
|
const verifier = consumeCodeVerifier();
|
|
179
202
|
const body = new URLSearchParams({
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/login-button.tsx","../../src/pkce.ts","../../src/storage.ts","../../src/client.ts","../../src/react/proma-provider.tsx"],"sourcesContent":["import { type ReactNode, useState } from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope } from '../types';\n\ninterface LoginWithPromaProps {\n clientId: string;\n redirectUri: string;\n scopes?: OAuthScope[];\n baseUrl?: string;\n onError?: (error: Error) => void;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * A ready-to-use \"Login with Proma\" button.\n *\n * @example\n * <LoginWithProma\n * clientId=\"proma_app_abc123\"\n * redirectUri=\"https://myapp.com/callback\"\n * scopes={['profile', 'credits']}\n * />\n */\nexport function LoginWithProma({\n clientId,\n redirectUri,\n scopes = ['profile'],\n baseUrl,\n onError,\n children,\n className,\n}: LoginWithPromaProps) {\n const [isLoading, setIsLoading] = useState(false);\n\n async function handleClick() {\n setIsLoading(true);\n\n try {\n const client = new PromaClient({ clientId, redirectUri, baseUrl });\n await client.login(scopes);\n } catch (err) {\n setIsLoading(false);\n onError?.(err instanceof Error ? err : new Error('Authorization failed'));\n }\n }\n\n return (\n <button\n type={'button'}\n onClick={handleClick}\n disabled={isLoading}\n className={className ?? 'proma-login-button'}\n aria-label={'Login with Proma'}\n >\n {isLoading\n ? 'Redirecting…'\n : (children ?? (\n <>\n <PromaLogo />\n Login with Proma\n </>\n ))}\n </button>\n );\n}\n\nfunction PromaLogo() {\n return (\n <svg\n width={16}\n height={16}\n viewBox={'0 0 24 24'}\n fill={'currentColor'}\n aria-hidden={'true'}\n >\n <path\n d={\n 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z'\n }\n />\n </svg>\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 const state = crypto.randomUUID();\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem('proma_oauth_state', state);\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 // Validate state parameter to prevent CSRF attacks\n const returnedState = params.get('state');\n if (typeof localStorage !== 'undefined') {\n const expectedState = localStorage.getItem('proma_oauth_state');\n localStorage.removeItem('proma_oauth_state');\n if (!returnedState || returnedState !== expectedState) {\n throw new Error('Invalid state parameter — possible CSRF attack');\n }\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope, PromaClientConfig, UserInfo } from '../types';\n\ninterface PromaAuthState {\n user: UserInfo | null;\n isLoading: boolean;\n isAuthenticated: boolean;\n /** Redirects the user to Proma's login page. */\n login: (scopes?: OAuthScope[]) => Promise<void>;\n /** Clears the session. */\n logout: () => void;\n /** The underlying PromaClient instance. */\n client: PromaClient;\n}\n\nconst PromaContext = createContext<PromaAuthState | null>(null);\n\ntype PromaProviderProps = PromaClientConfig & {\n children: React.ReactNode;\n};\n\n/**\n * Wraps your app with Proma auth context.\n * Call `usePromaAuth()` in any child component to access auth state.\n *\n * @example\n * <PromaProvider clientId=\"proma_app_xxx\" redirectUri=\"https://myapp.com/callback\">\n * <App />\n * </PromaProvider>\n */\nexport function PromaProvider({ children, ...config }: PromaProviderProps) {\n const [client] = useState(() => new PromaClient(config));\n const [user, setUser] = useState<UserInfo | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n // Restore session on mount\n useEffect(() => {\n let cancelled = false;\n\n async function restore() {\n try {\n const session = await client.getSession();\n if (!session || cancelled) {\n setIsLoading(false);\n return;\n }\n const userInfo = await client.getUser();\n if (!cancelled) setUser(userInfo);\n } catch {\n // no valid session\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n }\n\n void restore();\n return () => {\n cancelled = true;\n };\n }, [client]);\n\n const login = useCallback(\n (scopes?: OAuthScope[]) => client.login(scopes),\n [client],\n );\n\n const logout = useCallback(() => {\n client.logout();\n setUser(null);\n }, [client]);\n\n return (\n <PromaContext.Provider\n value={{\n user,\n isLoading,\n isAuthenticated: !!user,\n login,\n logout,\n client,\n }}\n >\n {children}\n </PromaContext.Provider>\n );\n}\n\n/**\n * Returns the current Proma auth state.\n * Must be used inside a `<PromaProvider>`.\n *\n * @example\n * const { user, isLoading, login, logout } = usePromaAuth()\n */\nexport function usePromaAuth(): PromaAuthState {\n const ctx = useContext(PromaContext);\n if (!ctx) {\n throw new Error('usePromaAuth must be used inside <PromaProvider>');\n }\n return ctx;\n}\n"],"mappings":"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","names":["useState","jsx","useState"]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/login-button.tsx","../../src/pkce.ts","../../src/storage.ts","../../src/client.ts","../../src/react/proma-provider.tsx"],"sourcesContent":["import { type ReactNode, useState } from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope } from '../types';\n\ninterface LoginWithPromaProps {\n clientId: string;\n redirectUri: string;\n scopes?: OAuthScope[];\n baseUrl?: string;\n onError?: (error: Error) => void;\n children?: ReactNode;\n className?: string;\n}\n\n/**\n * A ready-to-use \"Login with Proma\" button.\n *\n * @example\n * <LoginWithProma\n * clientId=\"proma_app_abc123\"\n * redirectUri=\"https://myapp.com/callback\"\n * scopes={['profile', 'credits']}\n * />\n */\nexport function LoginWithProma({\n clientId,\n redirectUri,\n scopes = ['profile'],\n baseUrl,\n onError,\n children,\n className,\n}: LoginWithPromaProps) {\n const [isLoading, setIsLoading] = useState(false);\n\n async function handleClick() {\n setIsLoading(true);\n\n try {\n const client = new PromaClient({ clientId, redirectUri, baseUrl });\n await client.login(scopes);\n } catch (err) {\n setIsLoading(false);\n onError?.(err instanceof Error ? err : new Error('Authorization failed'));\n }\n }\n\n return (\n <button\n type={'button'}\n onClick={handleClick}\n disabled={isLoading}\n className={className ?? 'proma-login-button'}\n aria-label={'Login with Proma'}\n >\n {isLoading\n ? 'Redirecting…'\n : (children ?? (\n <>\n <PromaLogo />\n Login with Proma\n </>\n ))}\n </button>\n );\n}\n\nfunction PromaLogo() {\n return (\n <svg\n width={16}\n height={16}\n viewBox={'0 0 24 24'}\n fill={'currentColor'}\n aria-hidden={'true'}\n >\n <path\n d={\n 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z'\n }\n />\n </svg>\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 {\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 OAuthScope,\n PromaClientConfig,\n Session,\n SpendCreditsResponse,\n TokenResponse,\n UserInfo,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://proma.dev';\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 // 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(\n 'proma_oauth_states',\n JSON.stringify(remaining),\n );\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 return 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 messages: params.messages,\n model: params.model ?? 'gemini-2.0-flash',\n }),\n });\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 const res = await this.chat(options);\n if (!res.ok) {\n const err = (await res\n .json()\n .catch(() => ({ error: 'upstream_error' }))) as { error: string };\n throw new Error(err.error);\n }\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\n const chunk = decoder.decode(value, { stream: true });\n // Parse SSE lines: \"data: {...}\"\n for (const line of chunk.split('\\n')) {\n if (!line.startsWith('data: ')) continue;\n const json = line.slice(6).trim();\n if (json === '[DONE]') continue;\n try {\n const parsed = JSON.parse(json) as {\n candidates?: Array<{\n content?: { parts?: Array<{ text?: string }> };\n }>;\n };\n const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';\n fullText += text;\n } catch {\n // skip malformed chunks\n }\n }\n }\n\n return fullText;\n }\n}\n","import {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from 'react';\n\nimport { PromaClient } from '../client';\nimport type { OAuthScope, PromaClientConfig, UserInfo } from '../types';\n\ninterface PromaAuthState {\n user: UserInfo | null;\n isLoading: boolean;\n isAuthenticated: boolean;\n /** Redirects the user to Proma's login page. */\n login: (scopes?: OAuthScope[]) => Promise<void>;\n /** Clears the session. */\n logout: () => void;\n /** The underlying PromaClient instance. */\n client: PromaClient;\n}\n\nconst PromaContext = createContext<PromaAuthState | null>(null);\n\ntype PromaProviderProps = PromaClientConfig & {\n children: React.ReactNode;\n};\n\n/**\n * Wraps your app with Proma auth context.\n * Call `usePromaAuth()` in any child component to access auth state.\n *\n * @example\n * <PromaProvider clientId=\"proma_app_xxx\" redirectUri=\"https://myapp.com/callback\">\n * <App />\n * </PromaProvider>\n */\nexport function PromaProvider({ children, ...config }: PromaProviderProps) {\n const [client] = useState(() => new PromaClient(config));\n const [user, setUser] = useState<UserInfo | null>(null);\n const [isLoading, setIsLoading] = useState(true);\n\n // Restore session on mount\n useEffect(() => {\n let cancelled = false;\n\n async function restore() {\n try {\n const session = await client.getSession();\n if (!session || cancelled) {\n setIsLoading(false);\n return;\n }\n const userInfo = await client.getUser();\n if (!cancelled) setUser(userInfo);\n } catch {\n // no valid session\n } finally {\n if (!cancelled) setIsLoading(false);\n }\n }\n\n void restore();\n return () => {\n cancelled = true;\n };\n }, [client]);\n\n const login = useCallback(\n (scopes?: OAuthScope[]) => client.login(scopes),\n [client],\n );\n\n const logout = useCallback(() => {\n client.logout();\n setUser(null);\n }, [client]);\n\n return (\n <PromaContext.Provider\n value={{\n user,\n isLoading,\n isAuthenticated: !!user,\n login,\n logout,\n client,\n }}\n >\n {children}\n </PromaContext.Provider>\n );\n}\n\n/**\n * Returns the current Proma auth state.\n * Must be used inside a `<PromaProvider>`.\n *\n * @example\n * const { user, isLoading, login, logout } = usePromaAuth()\n */\nexport function usePromaAuth(): PromaAuthState {\n const ctx = useContext(PromaContext);\n if (!ctx) {\n throw new Error('usePromaAuth must be used inside <PromaProvider>');\n }\n return ctx;\n}\n"],"mappings":"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","names":["useState","jsx","useState"]}
|