@coinlist-co/react 0.0.1 → 0.0.2

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createCoinListClient
3
- } from "./chunk-OETRTIQ4.js";
3
+ } from "./chunk-S56KGI3U.js";
4
4
 
5
5
  // src/client/CoinListProvider.tsx
6
6
  import {
@@ -60,4 +60,4 @@ export {
60
60
  CoinListProvider,
61
61
  useCoinList
62
62
  };
63
- //# sourceMappingURL=chunk-F56RBKNS.js.map
63
+ //# sourceMappingURL=chunk-CGIPLHD3.js.map
@@ -2,16 +2,16 @@ import {
2
2
  API_VERSION,
3
3
  Attributes,
4
4
  AuthorizationCode,
5
- BASE_URL,
6
5
  COINLIST_OAUTH_URL,
7
6
  CodeVerifier,
8
7
  HEADER_IDEMPOTENCY_KEY,
9
8
  HEADER_USER_AGENT,
10
9
  HttpClient,
11
10
  HttpError,
11
+ PUBLIC_API_BASE_URL,
12
12
  Request,
13
13
  requestRetryMiddleware
14
- } from "./chunk-NLJMUHOG.js";
14
+ } from "./chunk-W6ZBUBLA.js";
15
15
 
16
16
  // src/client/core/api/middleware/attach-session-middleware.ts
17
17
  function attachSessionMiddleware(fetchAccessToken) {
@@ -191,7 +191,7 @@ var CoinListClientImpl = class {
191
191
  this._accessToken = void 0;
192
192
  this._api = new ApiClient(
193
193
  {
194
- baseUrl: _config.baseUrl || BASE_URL,
194
+ baseUrl: _config.baseUrl || PUBLIC_API_BASE_URL,
195
195
  xApiVersion: API_VERSION
196
196
  },
197
197
  this.fetchAccessToken.bind(this)
@@ -271,4 +271,4 @@ function createCoinListClient(config) {
271
271
  export {
272
272
  createCoinListClient
273
273
  };
274
- //# sourceMappingURL=chunk-OETRTIQ4.js.map
274
+ //# sourceMappingURL=chunk-S56KGI3U.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/core/api/middleware/attach-session-middleware.ts","../src/client/core/api/middleware/session-renewal.ts","../src/shared/utils.ts","../src/client/core/api/middleware/device-id.ts","../src/client/core/api/middleware/user-agent.ts","../src/shared/api/middleware/idempotency-key.ts","../src/client/core/api/api-client.ts","../src/client/core/coinlist-client.ts"],"sourcesContent":["import type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function attachSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): BeforeRequestMiddleware {\n return async (request: HttpRequest): Promise<HttpRequest> => {\n if (!Attributes.isProtected(request.attributes)) {\n return request;\n }\n\n const accessToken = await fetchAccessToken(false);\n if (accessToken === null) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n Authorization: `Bearer ${accessToken.value}`,\n },\n };\n };\n}\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function renewSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): AfterRequestMiddleware {\n return async ({ request, response, retry }) => {\n if (response.status !== 401) {\n return response;\n }\n if (!Attributes.isProtected(request.attributes)) {\n return response;\n }\n if (Attributes.wasRenewAttempted(request.attributes)) {\n return response;\n }\n\n const newToken = await fetchAccessToken(true);\n if (newToken) {\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.renewAttempted(true)\n );\n return retry(nextRequest);\n } else {\n return response;\n }\n };\n}\n","/**\n * Computes SHA-256 digest of the input (UTF-8 string or raw bytes).\n * Uses the Web Crypto API.\n */\nexport async function sha256(data: string | Uint8Array): Promise<ArrayBuffer> {\n const bytes =\n typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const buffer = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n ) as ArrayBuffer;\n return crypto.subtle.digest('SHA-256', buffer);\n}\n\n/**\n * Converts an ArrayBuffer to Base64-URL encoding (RFC 4648).\n * @param padding - If false, omits trailing '=' padding (e.g. for PKCE).\n */\nexport function arrayBufferToBase64Url(\n buffer: ArrayBuffer,\n padding: boolean = true\n): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n let base64 = btoa(binary);\n base64 = base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n if (!padding) {\n base64 = base64.replace(/=+$/, '');\n }\n return base64;\n}\n\n/**\n * Securely generates random bytes and returns them as Base64-URL (no padding).\n * Suitable for PKCE code_verifier and OAuth state.\n * @param byteLength - Number of random bytes (e.g. 32 for PKCE).\n */\nexport function generateSecureRandomBase64Url(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n const buffer = bytes.buffer;\n return arrayBufferToBase64Url(buffer, false);\n}\n\n/**\n * Generate a UUID v4 using the standard Web Crypto API (crypto.randomUUID).\n * Falls back to RFC 4122–compliant generation via crypto.getRandomValues in old browsers.\n */\nexport function getUUIDv4(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function'\n ) {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n }\n\n return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\n}\n","'use client';\n\nimport { getUUIDv4 } from '@/shared/utils';\n\nconst DEVICE_ID_KEY = 'coinlist.device_id';\n\n/**\n * Get or create a device ID stored in localStorage.\n * Uses a crypto UUID that persists across sessions.\n */\nexport function getDeviceId(): string {\n let deviceId = localStorage.getItem(DEVICE_ID_KEY);\n\n if (!deviceId) {\n deviceId = getUUIDv4();\n localStorage.setItem(DEVICE_ID_KEY, deviceId);\n }\n\n return deviceId;\n}\n\n/**\n * Clear the stored device ID (useful for testing or logout).\n */\nexport function clearDeviceId(): void {\n localStorage.removeItem(DEVICE_ID_KEY);\n}\n","'use client';\n\nimport { getDeviceId } from '@/client/core/api/middleware/device-id';\nimport { HEADER_USER_AGENT } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\n\nexport const userAgentMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.needUserAgent(request.attributes)) {\n return request;\n }\n\n const userAgent = generateUserAgent();\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n [HEADER_USER_AGENT]: userAgent,\n },\n };\n};\n\n// Generate User-Agent in the required format for Frontline API\n// Format: <app-info> (<device-info>; DevicePlatform <device-platform>; DeviceAppID <device-app-id>; DeviceID <device-id>)\nfunction generateUserAgent(): string {\n const appInfo = `CoinList/1.0`;\n const deviceInfo = 'Web Browser';\n const devicePlatform = 'DevicePlatform web'; // Must be 'web' for validation\n\n const deviceAppId = 'DeviceAppID co.coinlist.sdk'; // SDK app ID\n\n const deviceId = getDeviceId();\n const finalDeviceId = `DeviceID ${deviceId}`;\n\n return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;\n}\n","import { HEADER_IDEMPOTENCY_KEY } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport { getUUIDv4 } from '@/shared/utils';\n\nexport const idempotencyKeyMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.isIdempotent(request.attributes)) {\n return request;\n }\n\n const existingHeaders = request.headers ?? {};\n if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...existingHeaders,\n [HEADER_IDEMPOTENCY_KEY]: getUUIDv4(),\n },\n };\n};\n","'use client';\n\nimport { attachSessionMiddleware } from '@/client/core/api/middleware/attach-session-middleware';\nimport { renewSessionMiddleware } from '@/client/core/api/middleware/session-renewal';\nimport { userAgentMiddleware } from '@/client/core/api/middleware/user-agent';\nimport {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { idempotencyKeyMiddleware } from '@/shared/api/middleware/idempotency-key';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport class ApiClient {\n private readonly httpClient: HttpClient;\n\n constructor(\n readonly config: HttpClientConfig,\n readonly fetchAccessToken: (\n refresh: boolean\n ) => Promise<OAuthAccessToken | null>\n ) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [\n attachSessionMiddleware(this.fetchAccessToken),\n userAgentMiddleware,\n idempotencyKeyMiddleware,\n ],\n afterRequest: [\n renewSessionMiddleware(this.fetchAccessToken),\n requestRetryMiddleware,\n ],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","'use client';\n\nimport { ApiClient } from '@/client/core/api/api-client';\nimport type {\n AuthState,\n OauthClientResult,\n} from '@/client/core/types/client-types';\nimport {\n API_VERSION,\n COINLIST_OAUTH_URL,\n PUBLIC_API_BASE_URL,\n} from '@/shared/api/frontline';\nimport type { Config } from '@/shared/types/config';\nimport { AuthorizationCode, CodeVerifier } from '@/shared/types/oauth';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\nimport {\n arrayBufferToBase64Url,\n generateSecureRandomBase64Url,\n sha256,\n} from '@/shared/utils';\n\n/** Session storage keys for OAuth PKCE (used by startOauth and completeOauth). */\nexport const OAUTH_STATE_KEY = 'coinlist.oauth_state';\nexport const OAUTH_CODE_VERIFIER_KEY = 'coinlist.oauth_code_verifier';\n\nexport interface ClientConfig extends Config {\n /**\n * The SDK user must provide this function. It is responsible for fetching a\n * valid {@link OAuthAccessToken}, or **null** for logged-out users. If your\n * backend is in TypeScript, it is recommended to use\n * {@link CoinListServer#accessToken} from `@/server/coinlist.server`.\n */\n getAccessToken: () => Promise<OAuthAccessToken | null>;\n}\n\nexport interface CoinListClient {\n /**\n * Calls the provided {@link ClientConfig.getAccessToken} lambda to determine\n * the user's {@link AuthState}. This is called automatically by {@link CoinListProvider};\n * you typically do not need to call it yourself unless you are using the client\n * outside of the provider.\n */\n init(): Promise<AuthState>;\n /**\n * Returns the current auth state: `'logged-in'`, `'logged-out'`, or `'unknown'`.\n * Returns **unknown** until {@link CoinListClient.init} has completed. It is\n * recommended to use the `const { isReady, coinlist } = useCoinList()` hook to know when\n * the SDK has initialized, or to manually await {@link CoinListClient.init}\n * before relying on this value.\n */\n getAuthState(): AuthState;\n /**\n * Starts the OAuth flow with PKCE. Generates a random `state` and `code_verifier`,\n * stores them in sessionStorage (for validation in {@link CoinListClient.completeOAuth}\n * on redirect), then redirects the user to the CoinList authorization page. After\n * the user authorizes, they are sent back to your application's redirect URI; call\n * {@link CoinListClient.completeOAuth} on that page to finish the flow.\n */\n startOAuth(): Promise<void>;\n /**\n * Completes the OAuth flow on the FE with PKCE and state validation. Parses the\n * redirect URL (state, code) and validates them against sessionStorage\n * (state, code_verifier). The state check protects against CSRF (cross-site\n * request forgery). Call this on the frontend page the user is redirected\n * to after authorizing.\n *\n * On `ok`: Call your backend to exchange the short-lived `authorizationCode`\n * (and the returned `codeVerifier`) for a long-lived session. If your backend\n * is in TypeScript, you can use {@link CoinListServer#completeOAuth} from\n * `@/server/coinlist.server` (CoinListServer) to perform that exchange.\n *\n * On `error`: `reason` is an {@link OauthClientErrorReason} (e.g. `'missing_state'`, `'invalid_state'`) so you can switch on it and surface a message to the user.\n */\n completeOAuth(): OauthClientResult;\n /**\n * Clears the in-memory access token. Note: you need to manually\n * also logout on the backend via CoinListServer#logout to clear the session cookie.\n */\n logout(): void;\n}\n\nclass CoinListClientImpl implements CoinListClient {\n /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */\n private _accessToken: OAuthAccessToken | null | undefined = undefined;\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: we'll need it\n private readonly _api: ApiClient;\n\n constructor(readonly _config: ClientConfig) {\n this._api = new ApiClient(\n {\n baseUrl: _config.baseUrl || PUBLIC_API_BASE_URL,\n xApiVersion: API_VERSION,\n },\n this.fetchAccessToken.bind(this)\n );\n }\n\n async init(): Promise<AuthState> {\n await this.fetchAccessToken(true);\n return this.getAuthState();\n }\n\n async fetchAccessToken(refresh: boolean): Promise<OAuthAccessToken | null> {\n if (!refresh && this._accessToken !== undefined) return this._accessToken;\n\n try {\n const accessToken = await this._config.getAccessToken();\n this._accessToken = accessToken;\n return accessToken;\n } catch (_) {\n this._accessToken = null;\n return null;\n }\n }\n\n getAuthState(): AuthState {\n if (this._accessToken === undefined) {\n return 'unknown';\n }\n return this._accessToken !== null ? 'logged-in' : 'logged-out';\n }\n\n async startOAuth(): Promise<void> {\n const state = generateSecureRandomBase64Url(32);\n const codeVerifier = generateSecureRandomBase64Url(32);\n\n const codeChallengeRaw = await sha256(codeVerifier);\n const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);\n\n sessionStorage.setItem(OAUTH_STATE_KEY, state);\n sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: this._config.clientId,\n response_type: 'code',\n redirect_uri: this._config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;\n window.location.href = url;\n }\n\n completeOAuth(): OauthClientResult {\n const params = new URLSearchParams(window.location.search);\n\n const stateParam = params.get('state');\n if (!stateParam) {\n return { type: 'error', reason: 'missing_state' };\n }\n const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);\n if (storedState === null || storedState !== stateParam) {\n return { type: 'error', reason: 'invalid_state' };\n }\n\n const codeParam = params.get('code');\n if (!codeParam) {\n return { type: 'error', reason: 'missing_code' };\n }\n\n const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);\n if (!codeVerifierValue) {\n return { type: 'error', reason: 'missing_code_verifier' };\n }\n\n return {\n type: 'ok',\n code: AuthorizationCode(codeParam),\n codeVerifier: CodeVerifier(codeVerifierValue),\n };\n }\n\n logout(): void {\n this._accessToken = null;\n }\n}\n\nexport function createCoinListClient(config: ClientConfig): CoinListClient {\n return new CoinListClientImpl(config);\n}\n"],"mappings":"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","names":[]}
@@ -121,7 +121,7 @@ var Request = {
121
121
  };
122
122
 
123
123
  // src/shared/api/frontline.ts
124
- var BASE_URL = "https://mobile-api.coinlist.co";
124
+ var PUBLIC_API_BASE_URL = "https://api.coinlist.co";
125
125
  var HEADER_API_VERSION = "X-API-Version";
126
126
  var HEADER_USER_AGENT = "User-Agent";
127
127
  var HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
@@ -236,7 +236,7 @@ export {
236
236
  Attributes,
237
237
  HttpError,
238
238
  Request,
239
- BASE_URL,
239
+ PUBLIC_API_BASE_URL,
240
240
  HEADER_USER_AGENT,
241
241
  HEADER_IDEMPOTENCY_KEY,
242
242
  API_VERSION,
@@ -250,4 +250,4 @@ export {
250
250
  ClientId,
251
251
  ClientSecret
252
252
  };
253
- //# sourceMappingURL=chunk-NLJMUHOG.js.map
253
+ //# sourceMappingURL=chunk-W6ZBUBLA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/types/oauth.ts","../src/shared/api/http-attributes.ts","../src/shared/api/http.ts","../src/shared/api/frontline.ts","../src/shared/api/http-client.ts","../src/shared/api/middleware/request-retry.ts"],"sourcesContent":["import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","export const PUBLIC_API_BASE_URL = 'https://api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n"],"mappings":"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","names":[]}
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  useCoinList
3
- } from "../../chunk-F56RBKNS.js";
4
- import "../../chunk-OETRTIQ4.js";
5
- import "../../chunk-NLJMUHOG.js";
3
+ } from "../../chunk-CGIPLHD3.js";
4
+ import "../../chunk-S56KGI3U.js";
5
+ import "../../chunk-W6ZBUBLA.js";
6
6
 
7
7
  // src/client/components/index.ts
8
8
  import "../styles.css";
@@ -249,7 +249,7 @@ function getDeviceId() {
249
249
  }
250
250
 
251
251
  // src/shared/api/frontline.ts
252
- var BASE_URL = "https://mobile-api.coinlist.co";
252
+ var PUBLIC_API_BASE_URL = "https://api.coinlist.co";
253
253
  var HEADER_API_VERSION = "X-API-Version";
254
254
  var HEADER_USER_AGENT = "User-Agent";
255
255
  var HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
@@ -447,7 +447,7 @@ var CoinListClientImpl = class {
447
447
  this._accessToken = void 0;
448
448
  this._api = new ApiClient(
449
449
  {
450
- baseUrl: _config.baseUrl || BASE_URL,
450
+ baseUrl: _config.baseUrl || PUBLIC_API_BASE_URL,
451
451
  xApiVersion: API_VERSION
452
452
  },
453
453
  this.fetchAccessToken.bind(this)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/client/core/index.ts","../../../src/shared/api/http-attributes.ts","../../../src/client/core/api/middleware/attach-session-middleware.ts","../../../src/shared/api/http.ts","../../../src/client/core/api/middleware/session-renewal.ts","../../../src/shared/utils.ts","../../../src/client/core/api/middleware/device-id.ts","../../../src/shared/api/frontline.ts","../../../src/client/core/api/middleware/user-agent.ts","../../../src/shared/api/http-client.ts","../../../src/shared/api/middleware/idempotency-key.ts","../../../src/shared/api/middleware/request-retry.ts","../../../src/client/core/api/api-client.ts","../../../src/shared/types/oauth.ts","../../../src/client/core/coinlist-client.ts","../../../src/shared/types/oauth-session.ts","../../../src/shared/types/user.ts"],"sourcesContent":["export {\n type ClientConfig,\n type CoinListClient,\n createCoinListClient,\n} from '@/client/core/coinlist-client';\nexport * from '@/client/core/types/client-types';\nexport * from '@/shared/types/oauth';\nexport * from '@/shared/types/oauth-session';\nexport * from '@/shared/types/user';\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function attachSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): BeforeRequestMiddleware {\n return async (request: HttpRequest): Promise<HttpRequest> => {\n if (!Attributes.isProtected(request.attributes)) {\n return request;\n }\n\n const accessToken = await fetchAccessToken(false);\n if (accessToken === null) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n Authorization: `Bearer ${accessToken.value}`,\n },\n };\n };\n}\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function renewSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): AfterRequestMiddleware {\n return async ({ request, response, retry }) => {\n if (response.status !== 401) {\n return response;\n }\n if (!Attributes.isProtected(request.attributes)) {\n return response;\n }\n if (Attributes.wasRenewAttempted(request.attributes)) {\n return response;\n }\n\n const newToken = await fetchAccessToken(true);\n if (newToken) {\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.renewAttempted(true)\n );\n return retry(nextRequest);\n } else {\n return response;\n }\n };\n}\n","/**\n * Computes SHA-256 digest of the input (UTF-8 string or raw bytes).\n * Uses the Web Crypto API.\n */\nexport async function sha256(data: string | Uint8Array): Promise<ArrayBuffer> {\n const bytes =\n typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const buffer = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n ) as ArrayBuffer;\n return crypto.subtle.digest('SHA-256', buffer);\n}\n\n/**\n * Converts an ArrayBuffer to Base64-URL encoding (RFC 4648).\n * @param padding - If false, omits trailing '=' padding (e.g. for PKCE).\n */\nexport function arrayBufferToBase64Url(\n buffer: ArrayBuffer,\n padding: boolean = true\n): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n let base64 = btoa(binary);\n base64 = base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n if (!padding) {\n base64 = base64.replace(/=+$/, '');\n }\n return base64;\n}\n\n/**\n * Securely generates random bytes and returns them as Base64-URL (no padding).\n * Suitable for PKCE code_verifier and OAuth state.\n * @param byteLength - Number of random bytes (e.g. 32 for PKCE).\n */\nexport function generateSecureRandomBase64Url(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n const buffer = bytes.buffer;\n return arrayBufferToBase64Url(buffer, false);\n}\n\n/**\n * Generate a UUID v4 using the standard Web Crypto API (crypto.randomUUID).\n * Falls back to RFC 4122–compliant generation via crypto.getRandomValues in old browsers.\n */\nexport function getUUIDv4(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function'\n ) {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n }\n\n return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\n}\n","'use client';\n\nimport { getUUIDv4 } from '@/shared/utils';\n\nconst DEVICE_ID_KEY = 'coinlist.device_id';\n\n/**\n * Get or create a device ID stored in localStorage.\n * Uses a crypto UUID that persists across sessions.\n */\nexport function getDeviceId(): string {\n let deviceId = localStorage.getItem(DEVICE_ID_KEY);\n\n if (!deviceId) {\n deviceId = getUUIDv4();\n localStorage.setItem(DEVICE_ID_KEY, deviceId);\n }\n\n return deviceId;\n}\n\n/**\n * Clear the stored device ID (useful for testing or logout).\n */\nexport function clearDeviceId(): void {\n localStorage.removeItem(DEVICE_ID_KEY);\n}\n","export const BASE_URL = 'https://mobile-api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","'use client';\n\nimport { getDeviceId } from '@/client/core/api/middleware/device-id';\nimport { HEADER_USER_AGENT } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\n\nexport const userAgentMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.needUserAgent(request.attributes)) {\n return request;\n }\n\n const userAgent = generateUserAgent();\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n [HEADER_USER_AGENT]: userAgent,\n },\n };\n};\n\n// Generate User-Agent in the required format for Frontline API\n// Format: <app-info> (<device-info>; DevicePlatform <device-platform>; DeviceAppID <device-app-id>; DeviceID <device-id>)\nfunction generateUserAgent(): string {\n const appInfo = `CoinList/1.0`;\n const deviceInfo = 'Web Browser';\n const devicePlatform = 'DevicePlatform web'; // Must be 'web' for validation\n\n const deviceAppId = 'DeviceAppID co.coinlist.sdk'; // SDK app ID\n\n const deviceId = getDeviceId();\n const finalDeviceId = `DeviceID ${deviceId}`;\n\n return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;\n}\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { HEADER_IDEMPOTENCY_KEY } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport { getUUIDv4 } from '@/shared/utils';\n\nexport const idempotencyKeyMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.isIdempotent(request.attributes)) {\n return request;\n }\n\n const existingHeaders = request.headers ?? {};\n if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...existingHeaders,\n [HEADER_IDEMPOTENCY_KEY]: getUUIDv4(),\n },\n };\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n","'use client';\n\nimport { attachSessionMiddleware } from '@/client/core/api/middleware/attach-session-middleware';\nimport { renewSessionMiddleware } from '@/client/core/api/middleware/session-renewal';\nimport { userAgentMiddleware } from '@/client/core/api/middleware/user-agent';\nimport {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { idempotencyKeyMiddleware } from '@/shared/api/middleware/idempotency-key';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport class ApiClient {\n private readonly httpClient: HttpClient;\n\n constructor(\n readonly config: HttpClientConfig,\n readonly fetchAccessToken: (\n refresh: boolean\n ) => Promise<OAuthAccessToken | null>\n ) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [\n attachSessionMiddleware(this.fetchAccessToken),\n userAgentMiddleware,\n idempotencyKeyMiddleware,\n ],\n afterRequest: [\n renewSessionMiddleware(this.fetchAccessToken),\n requestRetryMiddleware,\n ],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n","'use client';\n\nimport { ApiClient } from '@/client/core/api/api-client';\nimport type {\n AuthState,\n OauthClientResult,\n} from '@/client/core/types/client-types';\nimport {\n API_VERSION,\n BASE_URL,\n COINLIST_OAUTH_URL,\n} from '@/shared/api/frontline';\nimport type { Config } from '@/shared/types/config';\nimport { AuthorizationCode, CodeVerifier } from '@/shared/types/oauth';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\nimport {\n arrayBufferToBase64Url,\n generateSecureRandomBase64Url,\n sha256,\n} from '@/shared/utils';\n\n/** Session storage keys for OAuth PKCE (used by startOauth and completeOauth). */\nexport const OAUTH_STATE_KEY = 'coinlist.oauth_state';\nexport const OAUTH_CODE_VERIFIER_KEY = 'coinlist.oauth_code_verifier';\n\nexport interface ClientConfig extends Config {\n /**\n * The SDK user must provide this function. It is responsible for fetching a\n * valid {@link OAuthAccessToken}, or **null** for logged-out users. If your\n * backend is in TypeScript, it is recommended to use\n * {@link CoinListServer#accessToken} from `@/server/coinlist.server`.\n */\n getAccessToken: () => Promise<OAuthAccessToken | null>;\n}\n\nexport interface CoinListClient {\n /**\n * Calls the provided {@link ClientConfig.getAccessToken} lambda to determine\n * the user's {@link AuthState}. This is called automatically by {@link CoinListProvider};\n * you typically do not need to call it yourself unless you are using the client\n * outside of the provider.\n */\n init(): Promise<AuthState>;\n /**\n * Returns the current auth state: `'logged-in'`, `'logged-out'`, or `'unknown'`.\n * Returns **unknown** until {@link CoinListClient.init} has completed. It is\n * recommended to use the `const { isReady, coinlist } = useCoinList()` hook to know when\n * the SDK has initialized, or to manually await {@link CoinListClient.init}\n * before relying on this value.\n */\n getAuthState(): AuthState;\n /**\n * Starts the OAuth flow with PKCE. Generates a random `state` and `code_verifier`,\n * stores them in sessionStorage (for validation in {@link CoinListClient.completeOAuth}\n * on redirect), then redirects the user to the CoinList authorization page. After\n * the user authorizes, they are sent back to your application's redirect URI; call\n * {@link CoinListClient.completeOAuth} on that page to finish the flow.\n */\n startOAuth(): Promise<void>;\n /**\n * Completes the OAuth flow on the FE with PKCE and state validation. Parses the\n * redirect URL (state, code) and validates them against sessionStorage\n * (state, code_verifier). The state check protects against CSRF (cross-site\n * request forgery). Call this on the frontend page the user is redirected\n * to after authorizing.\n *\n * On `ok`: Call your backend to exchange the short-lived `authorizationCode`\n * (and the returned `codeVerifier`) for a long-lived session. If your backend\n * is in TypeScript, you can use {@link CoinListServer#completeOAuth} from\n * `@/server/coinlist.server` (CoinListServer) to perform that exchange.\n *\n * On `error`: `reason` is an {@link OauthClientErrorReason} (e.g. `'missing_state'`, `'invalid_state'`) so you can switch on it and surface a message to the user.\n */\n completeOAuth(): OauthClientResult;\n /**\n * Clears the in-memory access token. Note: you need to manually\n * also logout on the backend via CoinListServer#logout to clear the session cookie.\n */\n logout(): void;\n}\n\nclass CoinListClientImpl implements CoinListClient {\n /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */\n private _accessToken: OAuthAccessToken | null | undefined = undefined;\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: we'll need it\n private readonly _api: ApiClient;\n\n constructor(readonly _config: ClientConfig) {\n this._api = new ApiClient(\n {\n baseUrl: _config.baseUrl || BASE_URL,\n xApiVersion: API_VERSION,\n },\n this.fetchAccessToken.bind(this)\n );\n }\n\n async init(): Promise<AuthState> {\n await this.fetchAccessToken(true);\n return this.getAuthState();\n }\n\n async fetchAccessToken(refresh: boolean): Promise<OAuthAccessToken | null> {\n if (!refresh && this._accessToken !== undefined) return this._accessToken;\n\n try {\n const accessToken = await this._config.getAccessToken();\n this._accessToken = accessToken;\n return accessToken;\n } catch (_) {\n this._accessToken = null;\n return null;\n }\n }\n\n getAuthState(): AuthState {\n if (this._accessToken === undefined) {\n return 'unknown';\n }\n return this._accessToken !== null ? 'logged-in' : 'logged-out';\n }\n\n async startOAuth(): Promise<void> {\n const state = generateSecureRandomBase64Url(32);\n const codeVerifier = generateSecureRandomBase64Url(32);\n\n const codeChallengeRaw = await sha256(codeVerifier);\n const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);\n\n sessionStorage.setItem(OAUTH_STATE_KEY, state);\n sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: this._config.clientId,\n response_type: 'code',\n redirect_uri: this._config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;\n window.location.href = url;\n }\n\n completeOAuth(): OauthClientResult {\n const params = new URLSearchParams(window.location.search);\n\n const stateParam = params.get('state');\n if (!stateParam) {\n return { type: 'error', reason: 'missing_state' };\n }\n const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);\n if (storedState === null || storedState !== stateParam) {\n return { type: 'error', reason: 'invalid_state' };\n }\n\n const codeParam = params.get('code');\n if (!codeParam) {\n return { type: 'error', reason: 'missing_code' };\n }\n\n const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);\n if (!codeVerifierValue) {\n return { type: 'error', reason: 'missing_code_verifier' };\n }\n\n return {\n type: 'ok',\n code: AuthorizationCode(codeParam),\n codeVerifier: CodeVerifier(codeVerifierValue),\n };\n }\n\n logout(): void {\n this._accessToken = null;\n }\n}\n\nexport function createCoinListClient(config: ClientConfig): CoinListClient {\n return new CoinListClientImpl(config);\n}\n","import type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type OAuthAccessToken = {\n value: string;\n expiresAt: Date;\n};\n\nexport type OAuthRefreshToken = Newtype<string, 'OAuthRefreshToken'>;\nexport const OAuthRefreshToken = (value: string) => value as OAuthRefreshToken;\n\nexport type OAuthSession = {\n accessToken: OAuthAccessToken;\n refreshToken?: OAuthRefreshToken;\n};\n\nexport const OAuthSession = {\n fromDto: (dto: OAuthSessionDto): OAuthSession => {\n const expiresAt = new Date(Date.now() + dto.expires_in * 1000);\n return {\n accessToken: {\n value: dto.access_token,\n expiresAt,\n },\n ...(dto.refresh_token != null && dto.refresh_token !== ''\n ? { refreshToken: OAuthRefreshToken(dto.refresh_token) }\n : undefined),\n };\n },\n};\n","import type { UserDto } from '@/shared/types/dto/user';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type UserId = Newtype<string, 'UserId'>;\nexport const UserId = (value: string) => value as UserId;\nexport type UserEmail = Newtype<string, 'UserEmail'>;\nexport const UserEmail = (value: string) => value as UserEmail;\n\nexport type User = {\n id: UserId;\n email: UserEmail;\n};\n\nexport const User = {\n fromDto: (dto: UserDto): User => ({\n id: UserId(dto.id),\n email: UserEmail(dto.email),\n }),\n};\n"],"mappings":"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","names":["userAgent"]}
1
+ {"version":3,"sources":["../../../src/client/core/index.ts","../../../src/shared/api/http-attributes.ts","../../../src/client/core/api/middleware/attach-session-middleware.ts","../../../src/shared/api/http.ts","../../../src/client/core/api/middleware/session-renewal.ts","../../../src/shared/utils.ts","../../../src/client/core/api/middleware/device-id.ts","../../../src/shared/api/frontline.ts","../../../src/client/core/api/middleware/user-agent.ts","../../../src/shared/api/http-client.ts","../../../src/shared/api/middleware/idempotency-key.ts","../../../src/shared/api/middleware/request-retry.ts","../../../src/client/core/api/api-client.ts","../../../src/shared/types/oauth.ts","../../../src/client/core/coinlist-client.ts","../../../src/shared/types/oauth-session.ts","../../../src/shared/types/user.ts"],"sourcesContent":["export {\n type ClientConfig,\n type CoinListClient,\n createCoinListClient,\n} from '@/client/core/coinlist-client';\nexport * from '@/client/core/types/client-types';\nexport * from '@/shared/types/oauth';\nexport * from '@/shared/types/oauth-session';\nexport * from '@/shared/types/user';\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function attachSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): BeforeRequestMiddleware {\n return async (request: HttpRequest): Promise<HttpRequest> => {\n if (!Attributes.isProtected(request.attributes)) {\n return request;\n }\n\n const accessToken = await fetchAccessToken(false);\n if (accessToken === null) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n Authorization: `Bearer ${accessToken.value}`,\n },\n };\n };\n}\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function renewSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): AfterRequestMiddleware {\n return async ({ request, response, retry }) => {\n if (response.status !== 401) {\n return response;\n }\n if (!Attributes.isProtected(request.attributes)) {\n return response;\n }\n if (Attributes.wasRenewAttempted(request.attributes)) {\n return response;\n }\n\n const newToken = await fetchAccessToken(true);\n if (newToken) {\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.renewAttempted(true)\n );\n return retry(nextRequest);\n } else {\n return response;\n }\n };\n}\n","/**\n * Computes SHA-256 digest of the input (UTF-8 string or raw bytes).\n * Uses the Web Crypto API.\n */\nexport async function sha256(data: string | Uint8Array): Promise<ArrayBuffer> {\n const bytes =\n typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const buffer = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n ) as ArrayBuffer;\n return crypto.subtle.digest('SHA-256', buffer);\n}\n\n/**\n * Converts an ArrayBuffer to Base64-URL encoding (RFC 4648).\n * @param padding - If false, omits trailing '=' padding (e.g. for PKCE).\n */\nexport function arrayBufferToBase64Url(\n buffer: ArrayBuffer,\n padding: boolean = true\n): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n let base64 = btoa(binary);\n base64 = base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n if (!padding) {\n base64 = base64.replace(/=+$/, '');\n }\n return base64;\n}\n\n/**\n * Securely generates random bytes and returns them as Base64-URL (no padding).\n * Suitable for PKCE code_verifier and OAuth state.\n * @param byteLength - Number of random bytes (e.g. 32 for PKCE).\n */\nexport function generateSecureRandomBase64Url(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n const buffer = bytes.buffer;\n return arrayBufferToBase64Url(buffer, false);\n}\n\n/**\n * Generate a UUID v4 using the standard Web Crypto API (crypto.randomUUID).\n * Falls back to RFC 4122–compliant generation via crypto.getRandomValues in old browsers.\n */\nexport function getUUIDv4(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function'\n ) {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n }\n\n return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\n}\n","'use client';\n\nimport { getUUIDv4 } from '@/shared/utils';\n\nconst DEVICE_ID_KEY = 'coinlist.device_id';\n\n/**\n * Get or create a device ID stored in localStorage.\n * Uses a crypto UUID that persists across sessions.\n */\nexport function getDeviceId(): string {\n let deviceId = localStorage.getItem(DEVICE_ID_KEY);\n\n if (!deviceId) {\n deviceId = getUUIDv4();\n localStorage.setItem(DEVICE_ID_KEY, deviceId);\n }\n\n return deviceId;\n}\n\n/**\n * Clear the stored device ID (useful for testing or logout).\n */\nexport function clearDeviceId(): void {\n localStorage.removeItem(DEVICE_ID_KEY);\n}\n","export const PUBLIC_API_BASE_URL = 'https://api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","'use client';\n\nimport { getDeviceId } from '@/client/core/api/middleware/device-id';\nimport { HEADER_USER_AGENT } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\n\nexport const userAgentMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.needUserAgent(request.attributes)) {\n return request;\n }\n\n const userAgent = generateUserAgent();\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n [HEADER_USER_AGENT]: userAgent,\n },\n };\n};\n\n// Generate User-Agent in the required format for Frontline API\n// Format: <app-info> (<device-info>; DevicePlatform <device-platform>; DeviceAppID <device-app-id>; DeviceID <device-id>)\nfunction generateUserAgent(): string {\n const appInfo = `CoinList/1.0`;\n const deviceInfo = 'Web Browser';\n const devicePlatform = 'DevicePlatform web'; // Must be 'web' for validation\n\n const deviceAppId = 'DeviceAppID co.coinlist.sdk'; // SDK app ID\n\n const deviceId = getDeviceId();\n const finalDeviceId = `DeviceID ${deviceId}`;\n\n return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;\n}\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { HEADER_IDEMPOTENCY_KEY } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport { getUUIDv4 } from '@/shared/utils';\n\nexport const idempotencyKeyMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.isIdempotent(request.attributes)) {\n return request;\n }\n\n const existingHeaders = request.headers ?? {};\n if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...existingHeaders,\n [HEADER_IDEMPOTENCY_KEY]: getUUIDv4(),\n },\n };\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n","'use client';\n\nimport { attachSessionMiddleware } from '@/client/core/api/middleware/attach-session-middleware';\nimport { renewSessionMiddleware } from '@/client/core/api/middleware/session-renewal';\nimport { userAgentMiddleware } from '@/client/core/api/middleware/user-agent';\nimport {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { idempotencyKeyMiddleware } from '@/shared/api/middleware/idempotency-key';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport class ApiClient {\n private readonly httpClient: HttpClient;\n\n constructor(\n readonly config: HttpClientConfig,\n readonly fetchAccessToken: (\n refresh: boolean\n ) => Promise<OAuthAccessToken | null>\n ) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [\n attachSessionMiddleware(this.fetchAccessToken),\n userAgentMiddleware,\n idempotencyKeyMiddleware,\n ],\n afterRequest: [\n renewSessionMiddleware(this.fetchAccessToken),\n requestRetryMiddleware,\n ],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n","'use client';\n\nimport { ApiClient } from '@/client/core/api/api-client';\nimport type {\n AuthState,\n OauthClientResult,\n} from '@/client/core/types/client-types';\nimport {\n API_VERSION,\n COINLIST_OAUTH_URL,\n PUBLIC_API_BASE_URL,\n} from '@/shared/api/frontline';\nimport type { Config } from '@/shared/types/config';\nimport { AuthorizationCode, CodeVerifier } from '@/shared/types/oauth';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\nimport {\n arrayBufferToBase64Url,\n generateSecureRandomBase64Url,\n sha256,\n} from '@/shared/utils';\n\n/** Session storage keys for OAuth PKCE (used by startOauth and completeOauth). */\nexport const OAUTH_STATE_KEY = 'coinlist.oauth_state';\nexport const OAUTH_CODE_VERIFIER_KEY = 'coinlist.oauth_code_verifier';\n\nexport interface ClientConfig extends Config {\n /**\n * The SDK user must provide this function. It is responsible for fetching a\n * valid {@link OAuthAccessToken}, or **null** for logged-out users. If your\n * backend is in TypeScript, it is recommended to use\n * {@link CoinListServer#accessToken} from `@/server/coinlist.server`.\n */\n getAccessToken: () => Promise<OAuthAccessToken | null>;\n}\n\nexport interface CoinListClient {\n /**\n * Calls the provided {@link ClientConfig.getAccessToken} lambda to determine\n * the user's {@link AuthState}. This is called automatically by {@link CoinListProvider};\n * you typically do not need to call it yourself unless you are using the client\n * outside of the provider.\n */\n init(): Promise<AuthState>;\n /**\n * Returns the current auth state: `'logged-in'`, `'logged-out'`, or `'unknown'`.\n * Returns **unknown** until {@link CoinListClient.init} has completed. It is\n * recommended to use the `const { isReady, coinlist } = useCoinList()` hook to know when\n * the SDK has initialized, or to manually await {@link CoinListClient.init}\n * before relying on this value.\n */\n getAuthState(): AuthState;\n /**\n * Starts the OAuth flow with PKCE. Generates a random `state` and `code_verifier`,\n * stores them in sessionStorage (for validation in {@link CoinListClient.completeOAuth}\n * on redirect), then redirects the user to the CoinList authorization page. After\n * the user authorizes, they are sent back to your application's redirect URI; call\n * {@link CoinListClient.completeOAuth} on that page to finish the flow.\n */\n startOAuth(): Promise<void>;\n /**\n * Completes the OAuth flow on the FE with PKCE and state validation. Parses the\n * redirect URL (state, code) and validates them against sessionStorage\n * (state, code_verifier). The state check protects against CSRF (cross-site\n * request forgery). Call this on the frontend page the user is redirected\n * to after authorizing.\n *\n * On `ok`: Call your backend to exchange the short-lived `authorizationCode`\n * (and the returned `codeVerifier`) for a long-lived session. If your backend\n * is in TypeScript, you can use {@link CoinListServer#completeOAuth} from\n * `@/server/coinlist.server` (CoinListServer) to perform that exchange.\n *\n * On `error`: `reason` is an {@link OauthClientErrorReason} (e.g. `'missing_state'`, `'invalid_state'`) so you can switch on it and surface a message to the user.\n */\n completeOAuth(): OauthClientResult;\n /**\n * Clears the in-memory access token. Note: you need to manually\n * also logout on the backend via CoinListServer#logout to clear the session cookie.\n */\n logout(): void;\n}\n\nclass CoinListClientImpl implements CoinListClient {\n /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */\n private _accessToken: OAuthAccessToken | null | undefined = undefined;\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: we'll need it\n private readonly _api: ApiClient;\n\n constructor(readonly _config: ClientConfig) {\n this._api = new ApiClient(\n {\n baseUrl: _config.baseUrl || PUBLIC_API_BASE_URL,\n xApiVersion: API_VERSION,\n },\n this.fetchAccessToken.bind(this)\n );\n }\n\n async init(): Promise<AuthState> {\n await this.fetchAccessToken(true);\n return this.getAuthState();\n }\n\n async fetchAccessToken(refresh: boolean): Promise<OAuthAccessToken | null> {\n if (!refresh && this._accessToken !== undefined) return this._accessToken;\n\n try {\n const accessToken = await this._config.getAccessToken();\n this._accessToken = accessToken;\n return accessToken;\n } catch (_) {\n this._accessToken = null;\n return null;\n }\n }\n\n getAuthState(): AuthState {\n if (this._accessToken === undefined) {\n return 'unknown';\n }\n return this._accessToken !== null ? 'logged-in' : 'logged-out';\n }\n\n async startOAuth(): Promise<void> {\n const state = generateSecureRandomBase64Url(32);\n const codeVerifier = generateSecureRandomBase64Url(32);\n\n const codeChallengeRaw = await sha256(codeVerifier);\n const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);\n\n sessionStorage.setItem(OAUTH_STATE_KEY, state);\n sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: this._config.clientId,\n response_type: 'code',\n redirect_uri: this._config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;\n window.location.href = url;\n }\n\n completeOAuth(): OauthClientResult {\n const params = new URLSearchParams(window.location.search);\n\n const stateParam = params.get('state');\n if (!stateParam) {\n return { type: 'error', reason: 'missing_state' };\n }\n const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);\n if (storedState === null || storedState !== stateParam) {\n return { type: 'error', reason: 'invalid_state' };\n }\n\n const codeParam = params.get('code');\n if (!codeParam) {\n return { type: 'error', reason: 'missing_code' };\n }\n\n const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);\n if (!codeVerifierValue) {\n return { type: 'error', reason: 'missing_code_verifier' };\n }\n\n return {\n type: 'ok',\n code: AuthorizationCode(codeParam),\n codeVerifier: CodeVerifier(codeVerifierValue),\n };\n }\n\n logout(): void {\n this._accessToken = null;\n }\n}\n\nexport function createCoinListClient(config: ClientConfig): CoinListClient {\n return new CoinListClientImpl(config);\n}\n","import type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type OAuthAccessToken = {\n value: string;\n expiresAt: Date;\n};\n\nexport type OAuthRefreshToken = Newtype<string, 'OAuthRefreshToken'>;\nexport const OAuthRefreshToken = (value: string) => value as OAuthRefreshToken;\n\nexport type OAuthSession = {\n accessToken: OAuthAccessToken;\n refreshToken?: OAuthRefreshToken;\n};\n\nexport const OAuthSession = {\n fromDto: (dto: OAuthSessionDto): OAuthSession => {\n const expiresAt = new Date(Date.now() + dto.expires_in * 1000);\n return {\n accessToken: {\n value: dto.access_token,\n expiresAt,\n },\n ...(dto.refresh_token != null && dto.refresh_token !== ''\n ? { refreshToken: OAuthRefreshToken(dto.refresh_token) }\n : undefined),\n };\n },\n};\n","import type { UserDto } from '@/shared/types/dto/user';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type UserId = Newtype<string, 'UserId'>;\nexport const UserId = (value: string) => value as UserId;\nexport type UserEmail = Newtype<string, 'UserEmail'>;\nexport const UserEmail = (value: string) => value as UserEmail;\n\nexport type User = {\n id: UserId;\n email: UserEmail;\n};\n\nexport const User = {\n fromDto: (dto: UserDto): User => ({\n id: UserId(dto.id),\n email: UserEmail(dto.email),\n }),\n};\n"],"mappings":"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","names":["userAgent"]}
@@ -5,7 +5,7 @@ import {
5
5
  } from "../../chunk-YJQMJFA7.js";
6
6
  import {
7
7
  createCoinListClient
8
- } from "../../chunk-OETRTIQ4.js";
8
+ } from "../../chunk-S56KGI3U.js";
9
9
  import {
10
10
  OAuthRefreshToken,
11
11
  OAuthSession
@@ -17,7 +17,7 @@ import {
17
17
  CodeChallenge,
18
18
  CodeVerifier,
19
19
  RedirectUri
20
- } from "../../chunk-NLJMUHOG.js";
20
+ } from "../../chunk-W6ZBUBLA.js";
21
21
  export {
22
22
  AuthorizationCode,
23
23
  ClientId,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  useCoinList
3
- } from "../../chunk-F56RBKNS.js";
4
- import "../../chunk-OETRTIQ4.js";
5
- import "../../chunk-NLJMUHOG.js";
3
+ } from "../../chunk-CGIPLHD3.js";
4
+ import "../../chunk-S56KGI3U.js";
5
+ import "../../chunk-W6ZBUBLA.js";
6
6
  export {
7
7
  useCoinList
8
8
  };
@@ -254,7 +254,7 @@ function getDeviceId() {
254
254
  }
255
255
 
256
256
  // src/shared/api/frontline.ts
257
- var BASE_URL = "https://mobile-api.coinlist.co";
257
+ var PUBLIC_API_BASE_URL = "https://api.coinlist.co";
258
258
  var HEADER_API_VERSION = "X-API-Version";
259
259
  var HEADER_USER_AGENT = "User-Agent";
260
260
  var HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
@@ -452,7 +452,7 @@ var CoinListClientImpl = class {
452
452
  this._accessToken = void 0;
453
453
  this._api = new ApiClient(
454
454
  {
455
- baseUrl: _config.baseUrl || BASE_URL,
455
+ baseUrl: _config.baseUrl || PUBLIC_API_BASE_URL,
456
456
  xApiVersion: API_VERSION
457
457
  },
458
458
  this.fetchAccessToken.bind(this)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/index.ts","../../src/client/CoinListProvider.tsx","../../src/shared/api/http-attributes.ts","../../src/client/core/api/middleware/attach-session-middleware.ts","../../src/shared/api/http.ts","../../src/client/core/api/middleware/session-renewal.ts","../../src/shared/utils.ts","../../src/client/core/api/middleware/device-id.ts","../../src/shared/api/frontline.ts","../../src/client/core/api/middleware/user-agent.ts","../../src/shared/api/http-client.ts","../../src/shared/api/middleware/idempotency-key.ts","../../src/shared/api/middleware/request-retry.ts","../../src/client/core/api/api-client.ts","../../src/shared/types/oauth.ts","../../src/client/core/coinlist-client.ts","../../src/client/hooks/useCoinList.ts","../../src/shared/types/oauth-session.ts","../../src/shared/types/user.ts"],"sourcesContent":["export {\n CoinListProvider,\n type CoinListProviderProps,\n} from '@/client/CoinListProvider';\nexport {\n type ClientConfig,\n type CoinListClient,\n createCoinListClient,\n} from '@/client/core/coinlist-client';\nexport * from '@/client/core/types/client-types';\nexport { useCoinList } from '@/client/hooks/useCoinList';\nexport * from '@/shared/types/oauth';\nexport * from '@/shared/types/oauth-session';\nexport * from '@/shared/types/user';\n","/**\n * Global CoinList initialization for React apps.\n * Wrap your app once at the root; use useCoinList() anywhere below.\n */\n\nimport {\n createContext,\n type ReactNode,\n useEffect,\n useMemo,\n useState,\n} from 'react';\nimport {\n type ClientConfig,\n type CoinListClient,\n createCoinListClient,\n} from '@/client/core/coinlist-client';\n\nexport type CoinListContextValue = {\n coinlist: CoinListClient;\n isReady: boolean;\n};\n\nexport const CoinListContext = createContext<CoinListContextValue | null>(null);\n\nexport type CoinListProviderProps = {\n config: ClientConfig;\n children: ReactNode;\n};\n\n/**\n * Provide CoinList (api + config) to the tree. Initialize once at app root with your config.\n */\nexport function CoinListProvider({ config, children }: CoinListProviderProps) {\n const [isReady, setIsReady] = useState(false);\n const coinlist = useMemo(() => createCoinListClient(config), [config]);\n\n useEffect(() => {\n let isCancelled = false;\n if (coinlist.getAuthState() === 'unknown') {\n coinlist\n .init()\n .then(() => {\n if (!isCancelled) {\n setIsReady(true);\n }\n })\n .catch(() => {\n if (!isCancelled) {\n setIsReady(true);\n }\n });\n } else {\n setIsReady(true);\n }\n\n return () => {\n isCancelled = true;\n };\n }, [coinlist]);\n\n const value = useMemo(() => ({ coinlist, isReady }), [coinlist, isReady]);\n\n return (\n <CoinListContext.Provider value={value}>\n {children}\n </CoinListContext.Provider>\n );\n}\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function attachSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): BeforeRequestMiddleware {\n return async (request: HttpRequest): Promise<HttpRequest> => {\n if (!Attributes.isProtected(request.attributes)) {\n return request;\n }\n\n const accessToken = await fetchAccessToken(false);\n if (accessToken === null) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n Authorization: `Bearer ${accessToken.value}`,\n },\n };\n };\n}\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function renewSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): AfterRequestMiddleware {\n return async ({ request, response, retry }) => {\n if (response.status !== 401) {\n return response;\n }\n if (!Attributes.isProtected(request.attributes)) {\n return response;\n }\n if (Attributes.wasRenewAttempted(request.attributes)) {\n return response;\n }\n\n const newToken = await fetchAccessToken(true);\n if (newToken) {\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.renewAttempted(true)\n );\n return retry(nextRequest);\n } else {\n return response;\n }\n };\n}\n","/**\n * Computes SHA-256 digest of the input (UTF-8 string or raw bytes).\n * Uses the Web Crypto API.\n */\nexport async function sha256(data: string | Uint8Array): Promise<ArrayBuffer> {\n const bytes =\n typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const buffer = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n ) as ArrayBuffer;\n return crypto.subtle.digest('SHA-256', buffer);\n}\n\n/**\n * Converts an ArrayBuffer to Base64-URL encoding (RFC 4648).\n * @param padding - If false, omits trailing '=' padding (e.g. for PKCE).\n */\nexport function arrayBufferToBase64Url(\n buffer: ArrayBuffer,\n padding: boolean = true\n): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n let base64 = btoa(binary);\n base64 = base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n if (!padding) {\n base64 = base64.replace(/=+$/, '');\n }\n return base64;\n}\n\n/**\n * Securely generates random bytes and returns them as Base64-URL (no padding).\n * Suitable for PKCE code_verifier and OAuth state.\n * @param byteLength - Number of random bytes (e.g. 32 for PKCE).\n */\nexport function generateSecureRandomBase64Url(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n const buffer = bytes.buffer;\n return arrayBufferToBase64Url(buffer, false);\n}\n\n/**\n * Generate a UUID v4 using the standard Web Crypto API (crypto.randomUUID).\n * Falls back to RFC 4122–compliant generation via crypto.getRandomValues in old browsers.\n */\nexport function getUUIDv4(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function'\n ) {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n }\n\n return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\n}\n","'use client';\n\nimport { getUUIDv4 } from '@/shared/utils';\n\nconst DEVICE_ID_KEY = 'coinlist.device_id';\n\n/**\n * Get or create a device ID stored in localStorage.\n * Uses a crypto UUID that persists across sessions.\n */\nexport function getDeviceId(): string {\n let deviceId = localStorage.getItem(DEVICE_ID_KEY);\n\n if (!deviceId) {\n deviceId = getUUIDv4();\n localStorage.setItem(DEVICE_ID_KEY, deviceId);\n }\n\n return deviceId;\n}\n\n/**\n * Clear the stored device ID (useful for testing or logout).\n */\nexport function clearDeviceId(): void {\n localStorage.removeItem(DEVICE_ID_KEY);\n}\n","export const BASE_URL = 'https://mobile-api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","'use client';\n\nimport { getDeviceId } from '@/client/core/api/middleware/device-id';\nimport { HEADER_USER_AGENT } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\n\nexport const userAgentMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.needUserAgent(request.attributes)) {\n return request;\n }\n\n const userAgent = generateUserAgent();\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n [HEADER_USER_AGENT]: userAgent,\n },\n };\n};\n\n// Generate User-Agent in the required format for Frontline API\n// Format: <app-info> (<device-info>; DevicePlatform <device-platform>; DeviceAppID <device-app-id>; DeviceID <device-id>)\nfunction generateUserAgent(): string {\n const appInfo = `CoinList/1.0`;\n const deviceInfo = 'Web Browser';\n const devicePlatform = 'DevicePlatform web'; // Must be 'web' for validation\n\n const deviceAppId = 'DeviceAppID co.coinlist.sdk'; // SDK app ID\n\n const deviceId = getDeviceId();\n const finalDeviceId = `DeviceID ${deviceId}`;\n\n return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;\n}\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { HEADER_IDEMPOTENCY_KEY } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport { getUUIDv4 } from '@/shared/utils';\n\nexport const idempotencyKeyMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.isIdempotent(request.attributes)) {\n return request;\n }\n\n const existingHeaders = request.headers ?? {};\n if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...existingHeaders,\n [HEADER_IDEMPOTENCY_KEY]: getUUIDv4(),\n },\n };\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n","'use client';\n\nimport { attachSessionMiddleware } from '@/client/core/api/middleware/attach-session-middleware';\nimport { renewSessionMiddleware } from '@/client/core/api/middleware/session-renewal';\nimport { userAgentMiddleware } from '@/client/core/api/middleware/user-agent';\nimport {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { idempotencyKeyMiddleware } from '@/shared/api/middleware/idempotency-key';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport class ApiClient {\n private readonly httpClient: HttpClient;\n\n constructor(\n readonly config: HttpClientConfig,\n readonly fetchAccessToken: (\n refresh: boolean\n ) => Promise<OAuthAccessToken | null>\n ) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [\n attachSessionMiddleware(this.fetchAccessToken),\n userAgentMiddleware,\n idempotencyKeyMiddleware,\n ],\n afterRequest: [\n renewSessionMiddleware(this.fetchAccessToken),\n requestRetryMiddleware,\n ],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n","'use client';\n\nimport { ApiClient } from '@/client/core/api/api-client';\nimport type {\n AuthState,\n OauthClientResult,\n} from '@/client/core/types/client-types';\nimport {\n API_VERSION,\n BASE_URL,\n COINLIST_OAUTH_URL,\n} from '@/shared/api/frontline';\nimport type { Config } from '@/shared/types/config';\nimport { AuthorizationCode, CodeVerifier } from '@/shared/types/oauth';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\nimport {\n arrayBufferToBase64Url,\n generateSecureRandomBase64Url,\n sha256,\n} from '@/shared/utils';\n\n/** Session storage keys for OAuth PKCE (used by startOauth and completeOauth). */\nexport const OAUTH_STATE_KEY = 'coinlist.oauth_state';\nexport const OAUTH_CODE_VERIFIER_KEY = 'coinlist.oauth_code_verifier';\n\nexport interface ClientConfig extends Config {\n /**\n * The SDK user must provide this function. It is responsible for fetching a\n * valid {@link OAuthAccessToken}, or **null** for logged-out users. If your\n * backend is in TypeScript, it is recommended to use\n * {@link CoinListServer#accessToken} from `@/server/coinlist.server`.\n */\n getAccessToken: () => Promise<OAuthAccessToken | null>;\n}\n\nexport interface CoinListClient {\n /**\n * Calls the provided {@link ClientConfig.getAccessToken} lambda to determine\n * the user's {@link AuthState}. This is called automatically by {@link CoinListProvider};\n * you typically do not need to call it yourself unless you are using the client\n * outside of the provider.\n */\n init(): Promise<AuthState>;\n /**\n * Returns the current auth state: `'logged-in'`, `'logged-out'`, or `'unknown'`.\n * Returns **unknown** until {@link CoinListClient.init} has completed. It is\n * recommended to use the `const { isReady, coinlist } = useCoinList()` hook to know when\n * the SDK has initialized, or to manually await {@link CoinListClient.init}\n * before relying on this value.\n */\n getAuthState(): AuthState;\n /**\n * Starts the OAuth flow with PKCE. Generates a random `state` and `code_verifier`,\n * stores them in sessionStorage (for validation in {@link CoinListClient.completeOAuth}\n * on redirect), then redirects the user to the CoinList authorization page. After\n * the user authorizes, they are sent back to your application's redirect URI; call\n * {@link CoinListClient.completeOAuth} on that page to finish the flow.\n */\n startOAuth(): Promise<void>;\n /**\n * Completes the OAuth flow on the FE with PKCE and state validation. Parses the\n * redirect URL (state, code) and validates them against sessionStorage\n * (state, code_verifier). The state check protects against CSRF (cross-site\n * request forgery). Call this on the frontend page the user is redirected\n * to after authorizing.\n *\n * On `ok`: Call your backend to exchange the short-lived `authorizationCode`\n * (and the returned `codeVerifier`) for a long-lived session. If your backend\n * is in TypeScript, you can use {@link CoinListServer#completeOAuth} from\n * `@/server/coinlist.server` (CoinListServer) to perform that exchange.\n *\n * On `error`: `reason` is an {@link OauthClientErrorReason} (e.g. `'missing_state'`, `'invalid_state'`) so you can switch on it and surface a message to the user.\n */\n completeOAuth(): OauthClientResult;\n /**\n * Clears the in-memory access token. Note: you need to manually\n * also logout on the backend via CoinListServer#logout to clear the session cookie.\n */\n logout(): void;\n}\n\nclass CoinListClientImpl implements CoinListClient {\n /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */\n private _accessToken: OAuthAccessToken | null | undefined = undefined;\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: we'll need it\n private readonly _api: ApiClient;\n\n constructor(readonly _config: ClientConfig) {\n this._api = new ApiClient(\n {\n baseUrl: _config.baseUrl || BASE_URL,\n xApiVersion: API_VERSION,\n },\n this.fetchAccessToken.bind(this)\n );\n }\n\n async init(): Promise<AuthState> {\n await this.fetchAccessToken(true);\n return this.getAuthState();\n }\n\n async fetchAccessToken(refresh: boolean): Promise<OAuthAccessToken | null> {\n if (!refresh && this._accessToken !== undefined) return this._accessToken;\n\n try {\n const accessToken = await this._config.getAccessToken();\n this._accessToken = accessToken;\n return accessToken;\n } catch (_) {\n this._accessToken = null;\n return null;\n }\n }\n\n getAuthState(): AuthState {\n if (this._accessToken === undefined) {\n return 'unknown';\n }\n return this._accessToken !== null ? 'logged-in' : 'logged-out';\n }\n\n async startOAuth(): Promise<void> {\n const state = generateSecureRandomBase64Url(32);\n const codeVerifier = generateSecureRandomBase64Url(32);\n\n const codeChallengeRaw = await sha256(codeVerifier);\n const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);\n\n sessionStorage.setItem(OAUTH_STATE_KEY, state);\n sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: this._config.clientId,\n response_type: 'code',\n redirect_uri: this._config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;\n window.location.href = url;\n }\n\n completeOAuth(): OauthClientResult {\n const params = new URLSearchParams(window.location.search);\n\n const stateParam = params.get('state');\n if (!stateParam) {\n return { type: 'error', reason: 'missing_state' };\n }\n const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);\n if (storedState === null || storedState !== stateParam) {\n return { type: 'error', reason: 'invalid_state' };\n }\n\n const codeParam = params.get('code');\n if (!codeParam) {\n return { type: 'error', reason: 'missing_code' };\n }\n\n const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);\n if (!codeVerifierValue) {\n return { type: 'error', reason: 'missing_code_verifier' };\n }\n\n return {\n type: 'ok',\n code: AuthorizationCode(codeParam),\n codeVerifier: CodeVerifier(codeVerifierValue),\n };\n }\n\n logout(): void {\n this._accessToken = null;\n }\n}\n\nexport function createCoinListClient(config: ClientConfig): CoinListClient {\n return new CoinListClientImpl(config);\n}\n","import { useContext } from 'react';\nimport { CoinListContext } from '@/client/CoinListProvider';\nimport type { CoinListClient } from '@/client/core/coinlist-client';\n\nexport interface UseCoinListResult {\n /**\n * Indicates whether the CoinList client has been initialized and ready to use.\n * While isReady is false, then the current user state will be `unknown`.\n */\n isReady: boolean;\n coinlist: CoinListClient;\n}\n\nexport function useCoinList(): UseCoinListResult {\n const context = useContext(CoinListContext);\n if (context === null) {\n throw new CoinListClientInitializationError();\n }\n\n return {\n isReady: context.isReady,\n coinlist: context.coinlist,\n };\n}\n\nexport class CoinListClientInitializationError extends Error {\n constructor(\n message = 'useCoinList() must be used within CoinListProvider. Wrap your app with <CoinListProvider config={...}>.'\n ) {\n super(message);\n this.name = 'CoinListClientInitializationError';\n }\n}\n","import type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type OAuthAccessToken = {\n value: string;\n expiresAt: Date;\n};\n\nexport type OAuthRefreshToken = Newtype<string, 'OAuthRefreshToken'>;\nexport const OAuthRefreshToken = (value: string) => value as OAuthRefreshToken;\n\nexport type OAuthSession = {\n accessToken: OAuthAccessToken;\n refreshToken?: OAuthRefreshToken;\n};\n\nexport const OAuthSession = {\n fromDto: (dto: OAuthSessionDto): OAuthSession => {\n const expiresAt = new Date(Date.now() + dto.expires_in * 1000);\n return {\n accessToken: {\n value: dto.access_token,\n expiresAt,\n },\n ...(dto.refresh_token != null && dto.refresh_token !== ''\n ? { refreshToken: OAuthRefreshToken(dto.refresh_token) }\n : undefined),\n };\n },\n};\n","import type { UserDto } from '@/shared/types/dto/user';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type UserId = Newtype<string, 'UserId'>;\nexport const UserId = (value: string) => value as UserId;\nexport type UserEmail = Newtype<string, 'UserEmail'>;\nexport const UserEmail = (value: string) => value as UserEmail;\n\nexport type User = {\n id: UserId;\n email: UserEmail;\n};\n\nexport const User = {\n fromDto: (dto: UserDto): User => ({\n id: UserId(dto.id),\n email: UserEmail(dto.email),\n }),\n};\n"],"mappings":"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","names":["userAgent","import_react"]}
1
+ {"version":3,"sources":["../../src/client/index.ts","../../src/client/CoinListProvider.tsx","../../src/shared/api/http-attributes.ts","../../src/client/core/api/middleware/attach-session-middleware.ts","../../src/shared/api/http.ts","../../src/client/core/api/middleware/session-renewal.ts","../../src/shared/utils.ts","../../src/client/core/api/middleware/device-id.ts","../../src/shared/api/frontline.ts","../../src/client/core/api/middleware/user-agent.ts","../../src/shared/api/http-client.ts","../../src/shared/api/middleware/idempotency-key.ts","../../src/shared/api/middleware/request-retry.ts","../../src/client/core/api/api-client.ts","../../src/shared/types/oauth.ts","../../src/client/core/coinlist-client.ts","../../src/client/hooks/useCoinList.ts","../../src/shared/types/oauth-session.ts","../../src/shared/types/user.ts"],"sourcesContent":["export {\n CoinListProvider,\n type CoinListProviderProps,\n} from '@/client/CoinListProvider';\nexport {\n type ClientConfig,\n type CoinListClient,\n createCoinListClient,\n} from '@/client/core/coinlist-client';\nexport * from '@/client/core/types/client-types';\nexport { useCoinList } from '@/client/hooks/useCoinList';\nexport * from '@/shared/types/oauth';\nexport * from '@/shared/types/oauth-session';\nexport * from '@/shared/types/user';\n","/**\n * Global CoinList initialization for React apps.\n * Wrap your app once at the root; use useCoinList() anywhere below.\n */\n\nimport {\n createContext,\n type ReactNode,\n useEffect,\n useMemo,\n useState,\n} from 'react';\nimport {\n type ClientConfig,\n type CoinListClient,\n createCoinListClient,\n} from '@/client/core/coinlist-client';\n\nexport type CoinListContextValue = {\n coinlist: CoinListClient;\n isReady: boolean;\n};\n\nexport const CoinListContext = createContext<CoinListContextValue | null>(null);\n\nexport type CoinListProviderProps = {\n config: ClientConfig;\n children: ReactNode;\n};\n\n/**\n * Provide CoinList (api + config) to the tree. Initialize once at app root with your config.\n */\nexport function CoinListProvider({ config, children }: CoinListProviderProps) {\n const [isReady, setIsReady] = useState(false);\n const coinlist = useMemo(() => createCoinListClient(config), [config]);\n\n useEffect(() => {\n let isCancelled = false;\n if (coinlist.getAuthState() === 'unknown') {\n coinlist\n .init()\n .then(() => {\n if (!isCancelled) {\n setIsReady(true);\n }\n })\n .catch(() => {\n if (!isCancelled) {\n setIsReady(true);\n }\n });\n } else {\n setIsReady(true);\n }\n\n return () => {\n isCancelled = true;\n };\n }, [coinlist]);\n\n const value = useMemo(() => ({ coinlist, isReady }), [coinlist, isReady]);\n\n return (\n <CoinListContext.Provider value={value}>\n {children}\n </CoinListContext.Provider>\n );\n}\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function attachSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): BeforeRequestMiddleware {\n return async (request: HttpRequest): Promise<HttpRequest> => {\n if (!Attributes.isProtected(request.attributes)) {\n return request;\n }\n\n const accessToken = await fetchAccessToken(false);\n if (accessToken === null) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n Authorization: `Bearer ${accessToken.value}`,\n },\n };\n };\n}\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function renewSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): AfterRequestMiddleware {\n return async ({ request, response, retry }) => {\n if (response.status !== 401) {\n return response;\n }\n if (!Attributes.isProtected(request.attributes)) {\n return response;\n }\n if (Attributes.wasRenewAttempted(request.attributes)) {\n return response;\n }\n\n const newToken = await fetchAccessToken(true);\n if (newToken) {\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.renewAttempted(true)\n );\n return retry(nextRequest);\n } else {\n return response;\n }\n };\n}\n","/**\n * Computes SHA-256 digest of the input (UTF-8 string or raw bytes).\n * Uses the Web Crypto API.\n */\nexport async function sha256(data: string | Uint8Array): Promise<ArrayBuffer> {\n const bytes =\n typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const buffer = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n ) as ArrayBuffer;\n return crypto.subtle.digest('SHA-256', buffer);\n}\n\n/**\n * Converts an ArrayBuffer to Base64-URL encoding (RFC 4648).\n * @param padding - If false, omits trailing '=' padding (e.g. for PKCE).\n */\nexport function arrayBufferToBase64Url(\n buffer: ArrayBuffer,\n padding: boolean = true\n): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n let base64 = btoa(binary);\n base64 = base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n if (!padding) {\n base64 = base64.replace(/=+$/, '');\n }\n return base64;\n}\n\n/**\n * Securely generates random bytes and returns them as Base64-URL (no padding).\n * Suitable for PKCE code_verifier and OAuth state.\n * @param byteLength - Number of random bytes (e.g. 32 for PKCE).\n */\nexport function generateSecureRandomBase64Url(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n const buffer = bytes.buffer;\n return arrayBufferToBase64Url(buffer, false);\n}\n\n/**\n * Generate a UUID v4 using the standard Web Crypto API (crypto.randomUUID).\n * Falls back to RFC 4122–compliant generation via crypto.getRandomValues in old browsers.\n */\nexport function getUUIDv4(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function'\n ) {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n }\n\n return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\n}\n","'use client';\n\nimport { getUUIDv4 } from '@/shared/utils';\n\nconst DEVICE_ID_KEY = 'coinlist.device_id';\n\n/**\n * Get or create a device ID stored in localStorage.\n * Uses a crypto UUID that persists across sessions.\n */\nexport function getDeviceId(): string {\n let deviceId = localStorage.getItem(DEVICE_ID_KEY);\n\n if (!deviceId) {\n deviceId = getUUIDv4();\n localStorage.setItem(DEVICE_ID_KEY, deviceId);\n }\n\n return deviceId;\n}\n\n/**\n * Clear the stored device ID (useful for testing or logout).\n */\nexport function clearDeviceId(): void {\n localStorage.removeItem(DEVICE_ID_KEY);\n}\n","export const PUBLIC_API_BASE_URL = 'https://api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","'use client';\n\nimport { getDeviceId } from '@/client/core/api/middleware/device-id';\nimport { HEADER_USER_AGENT } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\n\nexport const userAgentMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.needUserAgent(request.attributes)) {\n return request;\n }\n\n const userAgent = generateUserAgent();\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n [HEADER_USER_AGENT]: userAgent,\n },\n };\n};\n\n// Generate User-Agent in the required format for Frontline API\n// Format: <app-info> (<device-info>; DevicePlatform <device-platform>; DeviceAppID <device-app-id>; DeviceID <device-id>)\nfunction generateUserAgent(): string {\n const appInfo = `CoinList/1.0`;\n const deviceInfo = 'Web Browser';\n const devicePlatform = 'DevicePlatform web'; // Must be 'web' for validation\n\n const deviceAppId = 'DeviceAppID co.coinlist.sdk'; // SDK app ID\n\n const deviceId = getDeviceId();\n const finalDeviceId = `DeviceID ${deviceId}`;\n\n return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;\n}\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { HEADER_IDEMPOTENCY_KEY } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport { getUUIDv4 } from '@/shared/utils';\n\nexport const idempotencyKeyMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.isIdempotent(request.attributes)) {\n return request;\n }\n\n const existingHeaders = request.headers ?? {};\n if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...existingHeaders,\n [HEADER_IDEMPOTENCY_KEY]: getUUIDv4(),\n },\n };\n};\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n","'use client';\n\nimport { attachSessionMiddleware } from '@/client/core/api/middleware/attach-session-middleware';\nimport { renewSessionMiddleware } from '@/client/core/api/middleware/session-renewal';\nimport { userAgentMiddleware } from '@/client/core/api/middleware/user-agent';\nimport {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { idempotencyKeyMiddleware } from '@/shared/api/middleware/idempotency-key';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport class ApiClient {\n private readonly httpClient: HttpClient;\n\n constructor(\n readonly config: HttpClientConfig,\n readonly fetchAccessToken: (\n refresh: boolean\n ) => Promise<OAuthAccessToken | null>\n ) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [\n attachSessionMiddleware(this.fetchAccessToken),\n userAgentMiddleware,\n idempotencyKeyMiddleware,\n ],\n afterRequest: [\n renewSessionMiddleware(this.fetchAccessToken),\n requestRetryMiddleware,\n ],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n","'use client';\n\nimport { ApiClient } from '@/client/core/api/api-client';\nimport type {\n AuthState,\n OauthClientResult,\n} from '@/client/core/types/client-types';\nimport {\n API_VERSION,\n COINLIST_OAUTH_URL,\n PUBLIC_API_BASE_URL,\n} from '@/shared/api/frontline';\nimport type { Config } from '@/shared/types/config';\nimport { AuthorizationCode, CodeVerifier } from '@/shared/types/oauth';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\nimport {\n arrayBufferToBase64Url,\n generateSecureRandomBase64Url,\n sha256,\n} from '@/shared/utils';\n\n/** Session storage keys for OAuth PKCE (used by startOauth and completeOauth). */\nexport const OAUTH_STATE_KEY = 'coinlist.oauth_state';\nexport const OAUTH_CODE_VERIFIER_KEY = 'coinlist.oauth_code_verifier';\n\nexport interface ClientConfig extends Config {\n /**\n * The SDK user must provide this function. It is responsible for fetching a\n * valid {@link OAuthAccessToken}, or **null** for logged-out users. If your\n * backend is in TypeScript, it is recommended to use\n * {@link CoinListServer#accessToken} from `@/server/coinlist.server`.\n */\n getAccessToken: () => Promise<OAuthAccessToken | null>;\n}\n\nexport interface CoinListClient {\n /**\n * Calls the provided {@link ClientConfig.getAccessToken} lambda to determine\n * the user's {@link AuthState}. This is called automatically by {@link CoinListProvider};\n * you typically do not need to call it yourself unless you are using the client\n * outside of the provider.\n */\n init(): Promise<AuthState>;\n /**\n * Returns the current auth state: `'logged-in'`, `'logged-out'`, or `'unknown'`.\n * Returns **unknown** until {@link CoinListClient.init} has completed. It is\n * recommended to use the `const { isReady, coinlist } = useCoinList()` hook to know when\n * the SDK has initialized, or to manually await {@link CoinListClient.init}\n * before relying on this value.\n */\n getAuthState(): AuthState;\n /**\n * Starts the OAuth flow with PKCE. Generates a random `state` and `code_verifier`,\n * stores them in sessionStorage (for validation in {@link CoinListClient.completeOAuth}\n * on redirect), then redirects the user to the CoinList authorization page. After\n * the user authorizes, they are sent back to your application's redirect URI; call\n * {@link CoinListClient.completeOAuth} on that page to finish the flow.\n */\n startOAuth(): Promise<void>;\n /**\n * Completes the OAuth flow on the FE with PKCE and state validation. Parses the\n * redirect URL (state, code) and validates them against sessionStorage\n * (state, code_verifier). The state check protects against CSRF (cross-site\n * request forgery). Call this on the frontend page the user is redirected\n * to after authorizing.\n *\n * On `ok`: Call your backend to exchange the short-lived `authorizationCode`\n * (and the returned `codeVerifier`) for a long-lived session. If your backend\n * is in TypeScript, you can use {@link CoinListServer#completeOAuth} from\n * `@/server/coinlist.server` (CoinListServer) to perform that exchange.\n *\n * On `error`: `reason` is an {@link OauthClientErrorReason} (e.g. `'missing_state'`, `'invalid_state'`) so you can switch on it and surface a message to the user.\n */\n completeOAuth(): OauthClientResult;\n /**\n * Clears the in-memory access token. Note: you need to manually\n * also logout on the backend via CoinListServer#logout to clear the session cookie.\n */\n logout(): void;\n}\n\nclass CoinListClientImpl implements CoinListClient {\n /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */\n private _accessToken: OAuthAccessToken | null | undefined = undefined;\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: we'll need it\n private readonly _api: ApiClient;\n\n constructor(readonly _config: ClientConfig) {\n this._api = new ApiClient(\n {\n baseUrl: _config.baseUrl || PUBLIC_API_BASE_URL,\n xApiVersion: API_VERSION,\n },\n this.fetchAccessToken.bind(this)\n );\n }\n\n async init(): Promise<AuthState> {\n await this.fetchAccessToken(true);\n return this.getAuthState();\n }\n\n async fetchAccessToken(refresh: boolean): Promise<OAuthAccessToken | null> {\n if (!refresh && this._accessToken !== undefined) return this._accessToken;\n\n try {\n const accessToken = await this._config.getAccessToken();\n this._accessToken = accessToken;\n return accessToken;\n } catch (_) {\n this._accessToken = null;\n return null;\n }\n }\n\n getAuthState(): AuthState {\n if (this._accessToken === undefined) {\n return 'unknown';\n }\n return this._accessToken !== null ? 'logged-in' : 'logged-out';\n }\n\n async startOAuth(): Promise<void> {\n const state = generateSecureRandomBase64Url(32);\n const codeVerifier = generateSecureRandomBase64Url(32);\n\n const codeChallengeRaw = await sha256(codeVerifier);\n const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);\n\n sessionStorage.setItem(OAUTH_STATE_KEY, state);\n sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: this._config.clientId,\n response_type: 'code',\n redirect_uri: this._config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;\n window.location.href = url;\n }\n\n completeOAuth(): OauthClientResult {\n const params = new URLSearchParams(window.location.search);\n\n const stateParam = params.get('state');\n if (!stateParam) {\n return { type: 'error', reason: 'missing_state' };\n }\n const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);\n if (storedState === null || storedState !== stateParam) {\n return { type: 'error', reason: 'invalid_state' };\n }\n\n const codeParam = params.get('code');\n if (!codeParam) {\n return { type: 'error', reason: 'missing_code' };\n }\n\n const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);\n if (!codeVerifierValue) {\n return { type: 'error', reason: 'missing_code_verifier' };\n }\n\n return {\n type: 'ok',\n code: AuthorizationCode(codeParam),\n codeVerifier: CodeVerifier(codeVerifierValue),\n };\n }\n\n logout(): void {\n this._accessToken = null;\n }\n}\n\nexport function createCoinListClient(config: ClientConfig): CoinListClient {\n return new CoinListClientImpl(config);\n}\n","import { useContext } from 'react';\nimport { CoinListContext } from '@/client/CoinListProvider';\nimport type { CoinListClient } from '@/client/core/coinlist-client';\n\nexport interface UseCoinListResult {\n /**\n * Indicates whether the CoinList client has been initialized and ready to use.\n * While isReady is false, then the current user state will be `unknown`.\n */\n isReady: boolean;\n coinlist: CoinListClient;\n}\n\nexport function useCoinList(): UseCoinListResult {\n const context = useContext(CoinListContext);\n if (context === null) {\n throw new CoinListClientInitializationError();\n }\n\n return {\n isReady: context.isReady,\n coinlist: context.coinlist,\n };\n}\n\nexport class CoinListClientInitializationError extends Error {\n constructor(\n message = 'useCoinList() must be used within CoinListProvider. Wrap your app with <CoinListProvider config={...}>.'\n ) {\n super(message);\n this.name = 'CoinListClientInitializationError';\n }\n}\n","import type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type OAuthAccessToken = {\n value: string;\n expiresAt: Date;\n};\n\nexport type OAuthRefreshToken = Newtype<string, 'OAuthRefreshToken'>;\nexport const OAuthRefreshToken = (value: string) => value as OAuthRefreshToken;\n\nexport type OAuthSession = {\n accessToken: OAuthAccessToken;\n refreshToken?: OAuthRefreshToken;\n};\n\nexport const OAuthSession = {\n fromDto: (dto: OAuthSessionDto): OAuthSession => {\n const expiresAt = new Date(Date.now() + dto.expires_in * 1000);\n return {\n accessToken: {\n value: dto.access_token,\n expiresAt,\n },\n ...(dto.refresh_token != null && dto.refresh_token !== ''\n ? { refreshToken: OAuthRefreshToken(dto.refresh_token) }\n : undefined),\n };\n },\n};\n","import type { UserDto } from '@/shared/types/dto/user';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type UserId = Newtype<string, 'UserId'>;\nexport const UserId = (value: string) => value as UserId;\nexport type UserEmail = Newtype<string, 'UserEmail'>;\nexport const UserEmail = (value: string) => value as UserEmail;\n\nexport type User = {\n id: UserId;\n email: UserEmail;\n};\n\nexport const User = {\n fromDto: (dto: UserDto): User => ({\n id: UserId(dto.id),\n email: UserEmail(dto.email),\n }),\n};\n"],"mappings":"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","names":["userAgent","import_react"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CoinListProvider,
3
3
  useCoinList
4
- } from "../chunk-F56RBKNS.js";
4
+ } from "../chunk-CGIPLHD3.js";
5
5
  import {
6
6
  User,
7
7
  UserEmail,
@@ -9,7 +9,7 @@ import {
9
9
  } from "../chunk-YJQMJFA7.js";
10
10
  import {
11
11
  createCoinListClient
12
- } from "../chunk-OETRTIQ4.js";
12
+ } from "../chunk-S56KGI3U.js";
13
13
  import {
14
14
  OAuthRefreshToken,
15
15
  OAuthSession
@@ -21,7 +21,7 @@ import {
21
21
  CodeChallenge,
22
22
  CodeVerifier,
23
23
  RedirectUri
24
- } from "../chunk-NLJMUHOG.js";
24
+ } from "../chunk-W6ZBUBLA.js";
25
25
  export {
26
26
  AuthorizationCode,
27
27
  ClientId,
@@ -147,7 +147,7 @@ var Request = {
147
147
  };
148
148
 
149
149
  // src/shared/api/frontline.ts
150
- var BASE_URL = "https://mobile-api.coinlist.co";
150
+ var PUBLIC_API_BASE_URL = "https://api.coinlist.co";
151
151
  var HEADER_API_VERSION = "X-API-Version";
152
152
  var API_VERSION = "2025-10-17";
153
153
 
@@ -294,7 +294,7 @@ var ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;
294
294
  var CoinListServerImpl = class {
295
295
  constructor(_config) {
296
296
  this._config = _config;
297
- this.baseUrl = _config.baseUrl ?? BASE_URL;
297
+ this.baseUrl = _config.baseUrl ?? PUBLIC_API_BASE_URL;
298
298
  this.accessTokenExpiryBufferSeconds = _config.accessTokenExpiryBufferSeconds ?? ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS;
299
299
  this.strict = _config.strict ?? false;
300
300
  this.api = new Api({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/index.ts","../../src/shared/api/http-attributes.ts","../../src/shared/api/http.ts","../../src/shared/api/frontline.ts","../../src/shared/api/http-client.ts","../../src/shared/api/middleware/request-retry.ts","../../src/server/api/api.server.ts","../../src/shared/types/oauth-session.ts","../../src/server/coinlist.server.ts","../../src/shared/types/oauth.ts"],"sourcesContent":["export {\n type CoinListServer,\n createCoinListServer,\n type ServerConfig,\n type SessionStore,\n} from '@/server/coinlist.server';\nexport * from '@/shared/types/config';\nexport * from '@/shared/types/oauth';\nexport * from '@/shared/types/oauth-session';\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","export const BASE_URL = 'https://mobile-api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n","import {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\n\nexport class Api {\n private readonly httpClient: HttpClient;\n\n constructor(private readonly config: HttpClientConfig) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [],\n afterRequest: [requestRetryMiddleware],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type OAuthAccessToken = {\n value: string;\n expiresAt: Date;\n};\n\nexport type OAuthRefreshToken = Newtype<string, 'OAuthRefreshToken'>;\nexport const OAuthRefreshToken = (value: string) => value as OAuthRefreshToken;\n\nexport type OAuthSession = {\n accessToken: OAuthAccessToken;\n refreshToken?: OAuthRefreshToken;\n};\n\nexport const OAuthSession = {\n fromDto: (dto: OAuthSessionDto): OAuthSession => {\n const expiresAt = new Date(Date.now() + dto.expires_in * 1000);\n return {\n accessToken: {\n value: dto.access_token,\n expiresAt,\n },\n ...(dto.refresh_token != null && dto.refresh_token !== ''\n ? { refreshToken: OAuthRefreshToken(dto.refresh_token) }\n : undefined),\n };\n },\n};\n","import { Api } from '@/server/api/api.server';\nimport { API_VERSION, BASE_URL } from '@/shared/api/frontline';\nimport { HttpError } from '@/shared/api/http';\nimport type { Config } from '@/shared/types/config';\nimport type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type {\n AuthorizationCode,\n ClientSecret,\n CodeVerifier,\n} from '@/shared/types/oauth';\nimport {\n type OAuthAccessToken,\n OAuthSession,\n} from '@/shared/types/oauth-session';\n\n/** Buffer in seconds before expiry to consider token expired for refresh. */\nconst ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;\n\nexport interface SessionStore {\n getSession(): Promise<OAuthSession | null>;\n setSession(session: OAuthSession | null): Promise<void>;\n}\n\nexport interface ServerConfig extends Config {\n readonly clientSecret: ClientSecret;\n readonly sessionStore: SessionStore;\n /** Buffer in seconds before expiry to consider token expired for refresh. */\n readonly accessTokenExpiryBufferSeconds?: number;\n /**\n * Whether SDK will throw an exception in cases it can be silent.\n * For example, if token revokation on logout fails.\n */\n readonly strict?: boolean;\n}\n\nexport interface CoinListServer {\n completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession>;\n\n /**\n * Responsible for:\n * 1. Returning a securely stored access token from Cookies\n * 2. Renewing the session if it's expired (or about to expire)\n *\n * @returns a valid access token or null if the user is not authentication.\n * On error rejects the Promise and throws.\n */\n accessToken(): Promise<OAuthAccessToken | null>;\n\n /**\n * Revokes the current token via POST /oauth/revoke and clears the session.\n */\n logout(): Promise<void>;\n}\n\nclass CoinListServerImpl implements CoinListServer {\n private readonly api: Api;\n private readonly baseUrl: string;\n\n private readonly accessTokenExpiryBufferSeconds: number;\n private readonly strict: boolean;\n\n constructor(private readonly _config: ServerConfig) {\n this.baseUrl = _config.baseUrl ?? BASE_URL;\n this.accessTokenExpiryBufferSeconds =\n _config.accessTokenExpiryBufferSeconds ??\n ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS;\n this.strict = _config.strict ?? false;\n this.api = new Api({\n baseUrl: this.baseUrl,\n xApiVersion: API_VERSION,\n });\n }\n\n async completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession> {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'authorization_code',\n code,\n redirect_uri: this._config.redirectUri,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n code_verifier: codeVerifier,\n },\n });\n const session = OAuthSession.fromDto(sessionDto);\n this._config.sessionStore.setSession(session);\n return session;\n }\n\n async accessToken(): Promise<OAuthAccessToken | null> {\n const session = await this._config.sessionStore.getSession();\n if (session == null) return null;\n\n const now = Date.now();\n const expiresAt = session.accessToken.expiresAt.getTime();\n const bufferMs = this.accessTokenExpiryBufferSeconds * 1000;\n if (expiresAt > now + bufferMs) {\n return session.accessToken;\n }\n\n const refreshToken = session?.refreshToken;\n if (!refreshToken) {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n\n try {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n const newSession = OAuthSession.fromDto(sessionDto);\n await this._config.sessionStore.setSession(newSession);\n return newSession.accessToken;\n } catch {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n }\n\n async logout(): Promise<void> {\n const session = await this._config.sessionStore.getSession();\n if (session != null) {\n const tokenToRevoke = session.accessToken.value;\n try {\n await this.api.send({\n method: 'POST',\n url: `/oauth/revoke`,\n body: {\n token: tokenToRevoke,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n } catch (err) {\n if (err instanceof HttpError) {\n if (this.strict) {\n throw err;\n }\n } else {\n throw err;\n }\n }\n await this._config.sessionStore.setSession(null);\n }\n }\n}\n\nexport function createCoinListServer(config: ServerConfig): CoinListServer {\n return new CoinListServerImpl(config);\n}\n","import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/index.ts","../../src/shared/api/http-attributes.ts","../../src/shared/api/http.ts","../../src/shared/api/frontline.ts","../../src/shared/api/http-client.ts","../../src/shared/api/middleware/request-retry.ts","../../src/server/api/api.server.ts","../../src/shared/types/oauth-session.ts","../../src/server/coinlist.server.ts","../../src/shared/types/oauth.ts"],"sourcesContent":["export {\n type CoinListServer,\n createCoinListServer,\n type ServerConfig,\n type SessionStore,\n} from '@/server/coinlist.server';\nexport * from '@/shared/types/config';\nexport * from '@/shared/types/oauth';\nexport * from '@/shared/types/oauth-session';\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","export const PUBLIC_API_BASE_URL = 'https://api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n","import {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\n\nexport class Api {\n private readonly httpClient: HttpClient;\n\n constructor(private readonly config: HttpClientConfig) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [],\n afterRequest: [requestRetryMiddleware],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type { Newtype } from '@/shared/types/newtype';\n\nexport type OAuthAccessToken = {\n value: string;\n expiresAt: Date;\n};\n\nexport type OAuthRefreshToken = Newtype<string, 'OAuthRefreshToken'>;\nexport const OAuthRefreshToken = (value: string) => value as OAuthRefreshToken;\n\nexport type OAuthSession = {\n accessToken: OAuthAccessToken;\n refreshToken?: OAuthRefreshToken;\n};\n\nexport const OAuthSession = {\n fromDto: (dto: OAuthSessionDto): OAuthSession => {\n const expiresAt = new Date(Date.now() + dto.expires_in * 1000);\n return {\n accessToken: {\n value: dto.access_token,\n expiresAt,\n },\n ...(dto.refresh_token != null && dto.refresh_token !== ''\n ? { refreshToken: OAuthRefreshToken(dto.refresh_token) }\n : undefined),\n };\n },\n};\n","import { Api } from '@/server/api/api.server';\nimport { API_VERSION, PUBLIC_API_BASE_URL } from '@/shared/api/frontline';\nimport { HttpError } from '@/shared/api/http';\nimport type { Config } from '@/shared/types/config';\nimport type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type {\n AuthorizationCode,\n ClientSecret,\n CodeVerifier,\n} from '@/shared/types/oauth';\nimport {\n type OAuthAccessToken,\n OAuthSession,\n} from '@/shared/types/oauth-session';\n\n/** Buffer in seconds before expiry to consider token expired for refresh. */\nconst ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;\n\nexport interface SessionStore {\n getSession(): Promise<OAuthSession | null>;\n setSession(session: OAuthSession | null): Promise<void>;\n}\n\nexport interface ServerConfig extends Config {\n readonly clientSecret: ClientSecret;\n readonly sessionStore: SessionStore;\n /** Buffer in seconds before expiry to consider token expired for refresh. */\n readonly accessTokenExpiryBufferSeconds?: number;\n /**\n * Whether SDK will throw an exception in cases it can be silent.\n * For example, if token revokation on logout fails.\n */\n readonly strict?: boolean;\n}\n\nexport interface CoinListServer {\n completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession>;\n\n /**\n * Responsible for:\n * 1. Returning a securely stored access token from Cookies\n * 2. Renewing the session if it's expired (or about to expire)\n *\n * @returns a valid access token or null if the user is not authentication.\n * On error rejects the Promise and throws.\n */\n accessToken(): Promise<OAuthAccessToken | null>;\n\n /**\n * Revokes the current token via POST /oauth/revoke and clears the session.\n */\n logout(): Promise<void>;\n}\n\nclass CoinListServerImpl implements CoinListServer {\n private readonly api: Api;\n private readonly baseUrl: string;\n\n private readonly accessTokenExpiryBufferSeconds: number;\n private readonly strict: boolean;\n\n constructor(private readonly _config: ServerConfig) {\n this.baseUrl = _config.baseUrl ?? PUBLIC_API_BASE_URL;\n this.accessTokenExpiryBufferSeconds =\n _config.accessTokenExpiryBufferSeconds ??\n ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS;\n this.strict = _config.strict ?? false;\n this.api = new Api({\n baseUrl: this.baseUrl,\n xApiVersion: API_VERSION,\n });\n }\n\n async completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession> {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'authorization_code',\n code,\n redirect_uri: this._config.redirectUri,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n code_verifier: codeVerifier,\n },\n });\n const session = OAuthSession.fromDto(sessionDto);\n this._config.sessionStore.setSession(session);\n return session;\n }\n\n async accessToken(): Promise<OAuthAccessToken | null> {\n const session = await this._config.sessionStore.getSession();\n if (session == null) return null;\n\n const now = Date.now();\n const expiresAt = session.accessToken.expiresAt.getTime();\n const bufferMs = this.accessTokenExpiryBufferSeconds * 1000;\n if (expiresAt > now + bufferMs) {\n return session.accessToken;\n }\n\n const refreshToken = session?.refreshToken;\n if (!refreshToken) {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n\n try {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n const newSession = OAuthSession.fromDto(sessionDto);\n await this._config.sessionStore.setSession(newSession);\n return newSession.accessToken;\n } catch {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n }\n\n async logout(): Promise<void> {\n const session = await this._config.sessionStore.getSession();\n if (session != null) {\n const tokenToRevoke = session.accessToken.value;\n try {\n await this.api.send({\n method: 'POST',\n url: `/oauth/revoke`,\n body: {\n token: tokenToRevoke,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n } catch (err) {\n if (err instanceof HttpError) {\n if (this.strict) {\n throw err;\n }\n } else {\n throw err;\n }\n }\n await this._config.sessionStore.setSession(null);\n }\n }\n}\n\nexport function createCoinListServer(config: ServerConfig): CoinListServer {\n return new CoinListServerImpl(config);\n}\n","import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n"],"mappings":"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","names":[]}
@@ -5,16 +5,16 @@ import {
5
5
  import {
6
6
  API_VERSION,
7
7
  AuthorizationCode,
8
- BASE_URL,
9
8
  ClientId,
10
9
  ClientSecret,
11
10
  CodeChallenge,
12
11
  CodeVerifier,
13
12
  HttpClient,
14
13
  HttpError,
14
+ PUBLIC_API_BASE_URL,
15
15
  RedirectUri,
16
16
  requestRetryMiddleware
17
- } from "../chunk-NLJMUHOG.js";
17
+ } from "../chunk-W6ZBUBLA.js";
18
18
 
19
19
  // src/server/api/api.server.ts
20
20
  var Api = class {
@@ -40,7 +40,7 @@ var ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;
40
40
  var CoinListServerImpl = class {
41
41
  constructor(_config) {
42
42
  this._config = _config;
43
- this.baseUrl = _config.baseUrl ?? BASE_URL;
43
+ this.baseUrl = _config.baseUrl ?? PUBLIC_API_BASE_URL;
44
44
  this.accessTokenExpiryBufferSeconds = _config.accessTokenExpiryBufferSeconds ?? ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS;
45
45
  this.strict = _config.strict ?? false;
46
46
  this.api = new Api({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/api/api.server.ts","../../src/server/coinlist.server.ts"],"sourcesContent":["import {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\n\nexport class Api {\n private readonly httpClient: HttpClient;\n\n constructor(private readonly config: HttpClientConfig) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [],\n afterRequest: [requestRetryMiddleware],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import { Api } from '@/server/api/api.server';\nimport { API_VERSION, BASE_URL } from '@/shared/api/frontline';\nimport { HttpError } from '@/shared/api/http';\nimport type { Config } from '@/shared/types/config';\nimport type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type {\n AuthorizationCode,\n ClientSecret,\n CodeVerifier,\n} from '@/shared/types/oauth';\nimport {\n type OAuthAccessToken,\n OAuthSession,\n} from '@/shared/types/oauth-session';\n\n/** Buffer in seconds before expiry to consider token expired for refresh. */\nconst ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;\n\nexport interface SessionStore {\n getSession(): Promise<OAuthSession | null>;\n setSession(session: OAuthSession | null): Promise<void>;\n}\n\nexport interface ServerConfig extends Config {\n readonly clientSecret: ClientSecret;\n readonly sessionStore: SessionStore;\n /** Buffer in seconds before expiry to consider token expired for refresh. */\n readonly accessTokenExpiryBufferSeconds?: number;\n /**\n * Whether SDK will throw an exception in cases it can be silent.\n * For example, if token revokation on logout fails.\n */\n readonly strict?: boolean;\n}\n\nexport interface CoinListServer {\n completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession>;\n\n /**\n * Responsible for:\n * 1. Returning a securely stored access token from Cookies\n * 2. Renewing the session if it's expired (or about to expire)\n *\n * @returns a valid access token or null if the user is not authentication.\n * On error rejects the Promise and throws.\n */\n accessToken(): Promise<OAuthAccessToken | null>;\n\n /**\n * Revokes the current token via POST /oauth/revoke and clears the session.\n */\n logout(): Promise<void>;\n}\n\nclass CoinListServerImpl implements CoinListServer {\n private readonly api: Api;\n private readonly baseUrl: string;\n\n private readonly accessTokenExpiryBufferSeconds: number;\n private readonly strict: boolean;\n\n constructor(private readonly _config: ServerConfig) {\n this.baseUrl = _config.baseUrl ?? BASE_URL;\n this.accessTokenExpiryBufferSeconds =\n _config.accessTokenExpiryBufferSeconds ??\n ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS;\n this.strict = _config.strict ?? false;\n this.api = new Api({\n baseUrl: this.baseUrl,\n xApiVersion: API_VERSION,\n });\n }\n\n async completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession> {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'authorization_code',\n code,\n redirect_uri: this._config.redirectUri,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n code_verifier: codeVerifier,\n },\n });\n const session = OAuthSession.fromDto(sessionDto);\n this._config.sessionStore.setSession(session);\n return session;\n }\n\n async accessToken(): Promise<OAuthAccessToken | null> {\n const session = await this._config.sessionStore.getSession();\n if (session == null) return null;\n\n const now = Date.now();\n const expiresAt = session.accessToken.expiresAt.getTime();\n const bufferMs = this.accessTokenExpiryBufferSeconds * 1000;\n if (expiresAt > now + bufferMs) {\n return session.accessToken;\n }\n\n const refreshToken = session?.refreshToken;\n if (!refreshToken) {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n\n try {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n const newSession = OAuthSession.fromDto(sessionDto);\n await this._config.sessionStore.setSession(newSession);\n return newSession.accessToken;\n } catch {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n }\n\n async logout(): Promise<void> {\n const session = await this._config.sessionStore.getSession();\n if (session != null) {\n const tokenToRevoke = session.accessToken.value;\n try {\n await this.api.send({\n method: 'POST',\n url: `/oauth/revoke`,\n body: {\n token: tokenToRevoke,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n } catch (err) {\n if (err instanceof HttpError) {\n if (this.strict) {\n throw err;\n }\n } else {\n throw err;\n }\n }\n await this._config.sessionStore.setSession(null);\n }\n }\n}\n\nexport function createCoinListServer(config: ServerConfig): CoinListServer {\n return new CoinListServerImpl(config);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/server/api/api.server.ts","../../src/server/coinlist.server.ts"],"sourcesContent":["import {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\n\nexport class Api {\n private readonly httpClient: HttpClient;\n\n constructor(private readonly config: HttpClientConfig) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [],\n afterRequest: [requestRetryMiddleware],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","import { Api } from '@/server/api/api.server';\nimport { API_VERSION, PUBLIC_API_BASE_URL } from '@/shared/api/frontline';\nimport { HttpError } from '@/shared/api/http';\nimport type { Config } from '@/shared/types/config';\nimport type { OAuthSessionDto } from '@/shared/types/dto/oauth-session';\nimport type {\n AuthorizationCode,\n ClientSecret,\n CodeVerifier,\n} from '@/shared/types/oauth';\nimport {\n type OAuthAccessToken,\n OAuthSession,\n} from '@/shared/types/oauth-session';\n\n/** Buffer in seconds before expiry to consider token expired for refresh. */\nconst ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;\n\nexport interface SessionStore {\n getSession(): Promise<OAuthSession | null>;\n setSession(session: OAuthSession | null): Promise<void>;\n}\n\nexport interface ServerConfig extends Config {\n readonly clientSecret: ClientSecret;\n readonly sessionStore: SessionStore;\n /** Buffer in seconds before expiry to consider token expired for refresh. */\n readonly accessTokenExpiryBufferSeconds?: number;\n /**\n * Whether SDK will throw an exception in cases it can be silent.\n * For example, if token revokation on logout fails.\n */\n readonly strict?: boolean;\n}\n\nexport interface CoinListServer {\n completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession>;\n\n /**\n * Responsible for:\n * 1. Returning a securely stored access token from Cookies\n * 2. Renewing the session if it's expired (or about to expire)\n *\n * @returns a valid access token or null if the user is not authentication.\n * On error rejects the Promise and throws.\n */\n accessToken(): Promise<OAuthAccessToken | null>;\n\n /**\n * Revokes the current token via POST /oauth/revoke and clears the session.\n */\n logout(): Promise<void>;\n}\n\nclass CoinListServerImpl implements CoinListServer {\n private readonly api: Api;\n private readonly baseUrl: string;\n\n private readonly accessTokenExpiryBufferSeconds: number;\n private readonly strict: boolean;\n\n constructor(private readonly _config: ServerConfig) {\n this.baseUrl = _config.baseUrl ?? PUBLIC_API_BASE_URL;\n this.accessTokenExpiryBufferSeconds =\n _config.accessTokenExpiryBufferSeconds ??\n ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS;\n this.strict = _config.strict ?? false;\n this.api = new Api({\n baseUrl: this.baseUrl,\n xApiVersion: API_VERSION,\n });\n }\n\n async completeOAuth(\n code: AuthorizationCode,\n codeVerifier: CodeVerifier\n ): Promise<OAuthSession> {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'authorization_code',\n code,\n redirect_uri: this._config.redirectUri,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n code_verifier: codeVerifier,\n },\n });\n const session = OAuthSession.fromDto(sessionDto);\n this._config.sessionStore.setSession(session);\n return session;\n }\n\n async accessToken(): Promise<OAuthAccessToken | null> {\n const session = await this._config.sessionStore.getSession();\n if (session == null) return null;\n\n const now = Date.now();\n const expiresAt = session.accessToken.expiresAt.getTime();\n const bufferMs = this.accessTokenExpiryBufferSeconds * 1000;\n if (expiresAt > now + bufferMs) {\n return session.accessToken;\n }\n\n const refreshToken = session?.refreshToken;\n if (!refreshToken) {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n\n try {\n const sessionDto = await this.api.send<OAuthSessionDto>({\n method: 'POST',\n url: `/oauth/token`,\n body: {\n grant_type: 'refresh_token',\n refresh_token: refreshToken,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n const newSession = OAuthSession.fromDto(sessionDto);\n await this._config.sessionStore.setSession(newSession);\n return newSession.accessToken;\n } catch {\n await this._config.sessionStore.setSession(null);\n return null;\n }\n }\n\n async logout(): Promise<void> {\n const session = await this._config.sessionStore.getSession();\n if (session != null) {\n const tokenToRevoke = session.accessToken.value;\n try {\n await this.api.send({\n method: 'POST',\n url: `/oauth/revoke`,\n body: {\n token: tokenToRevoke,\n client_id: this._config.clientId,\n client_secret: this._config.clientSecret,\n },\n });\n } catch (err) {\n if (err instanceof HttpError) {\n if (this.strict) {\n throw err;\n }\n } else {\n throw err;\n }\n }\n await this._config.sessionStore.setSession(null);\n }\n }\n}\n\nexport function createCoinListServer(config: ServerConfig): CoinListServer {\n return new CoinListServerImpl(config);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinlist-co/react",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "CoinList Partner SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/client/index.cjs",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/shared/types/oauth.ts","../src/shared/api/http-attributes.ts","../src/shared/api/http.ts","../src/shared/api/frontline.ts","../src/shared/api/http-client.ts","../src/shared/api/middleware/request-retry.ts"],"sourcesContent":["import type { Newtype } from '@/shared/types/newtype';\n\nexport type AuthorizationCode = Newtype<string, 'AuthorizationCode'>;\nexport const AuthorizationCode = (value: string): AuthorizationCode =>\n value as AuthorizationCode;\n\nexport type CodeVerifier = Newtype<string, 'CodeVerifier'>;\nexport const CodeVerifier = (value: string): CodeVerifier =>\n value as CodeVerifier;\n\nexport type CodeChallenge = Newtype<string, 'CodeChallenge'>;\nexport const CodeChallenge = (value: string): CodeChallenge =>\n value as CodeChallenge;\n\nexport type RedirectUri = Newtype<string, 'RedirectUri'>;\nexport const RedirectUri = (value: string): RedirectUri => value as RedirectUri;\n\nexport type ClientId = Newtype<string, 'ClientId'>;\nexport const ClientId = (value: string): ClientId => value as ClientId;\n\nexport type ClientSecret = Newtype<string, 'ClientSecret'>;\nexport const ClientSecret = (value: string): ClientSecret =>\n value as ClientSecret;\n","export type HttpRequestAttributes = {\n protected?: boolean;\n userAgent?: boolean;\n idempotencyKey?: boolean;\n /** Zero-based attempt index: 0 = first request, 1 = first retry, etc. */\n retryAttempt?: number;\n renewAttempted?: boolean;\n};\n\nexport type Attributes = Readonly<HttpRequestAttributes>;\n\nconst empty: Attributes = {};\n\nconst concat = (left: Attributes, right: Attributes): Attributes => ({\n ...left,\n ...right,\n});\n\nconst concatAll = (...items: Attributes[]): Attributes => {\n let result = empty;\n for (const item of items) {\n result = concat(result, item);\n }\n return result;\n};\n\nconst protectedRequest = (): Attributes => ({ protected: true });\nconst userAgent = (): Attributes => ({ userAgent: true });\nconst idempotencyKey = (): Attributes => ({\n idempotencyKey: true,\n});\n\n/** Returns attributes with the given retry attempt (zero-based). */\nconst retryAttempt = (attempt: number): Attributes => ({\n retryAttempt: attempt,\n});\nconst renewAttempted = (value: boolean): Attributes => ({\n renewAttempted: value,\n});\n\nconst isProtected = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.protected === true;\nconst needUserAgent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.userAgent === true;\nconst isIdempotent = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.idempotencyKey === true;\nconst getRetryAttempt = (attrs?: HttpRequestAttributes): number =>\n attrs?.retryAttempt ?? 0;\nconst wasRenewAttempted = (attrs?: HttpRequestAttributes): boolean =>\n attrs?.renewAttempted === true;\n\nexport const Attributes = {\n empty,\n concat,\n concatAll,\n protected: protectedRequest,\n isProtected,\n userAgent,\n needUserAgent,\n idempotencyKey,\n isIdempotent,\n retryAttempt,\n getRetryAttempt,\n renewAttempted,\n wasRenewAttempted,\n} as const;\n","import {\n Attributes,\n type HttpRequestAttributes,\n} from '@/shared/api/http-attributes';\n\nexport type HttpClientConfig = {\n baseUrl: string;\n xApiVersion: string;\n};\n\nexport type HttpRequest<TBody = unknown> =\n | {\n method: 'GET';\n url: string;\n headers?: Record<string, string>;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n }\n | {\n method: 'POST';\n url: string;\n headers?: Record<string, string>;\n body: TBody;\n attributes?: HttpRequestAttributes;\n redirect?: RequestRedirect;\n };\nexport type HttpResponse<TBody = unknown> = {\n status: number;\n headers?: Record<string, string>;\n body: TBody | null;\n};\n\nexport class HttpError<TBody = unknown> extends Error {\n readonly response: HttpResponse<TBody>;\n\n constructor(response: HttpResponse<TBody>) {\n super(`Request failed with ${response.status} status`);\n this.name = 'HttpError';\n this.response = response;\n }\n}\n\nexport async function makeRequest<TResponse = unknown>(\n request: HttpRequest\n): Promise<HttpResponse<TResponse>> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(request.method === 'POST' && request.body !== undefined\n ? { 'Content-Type': 'application/json' }\n : {}),\n ...(request.headers ?? {}),\n };\n\n const init: RequestInit = {\n method: request.method,\n headers,\n ...(request.redirect !== undefined ? { redirect: request.redirect } : {}),\n };\n\n if (request.method === 'POST' && request.body !== undefined) {\n init.body = JSON.stringify(request.body);\n }\n\n const response = await fetch(request.url, init);\n const responseHeaders = headersToRecord(response.headers);\n\n if (response.status === 204 || response.status === 205) {\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n if (response.status >= 300 && response.status < 400) {\n await response.text();\n return {\n status: response.status,\n body: null,\n headers: responseHeaders,\n };\n }\n\n const text = await response.text();\n const body = text ? (JSON.parse(text) as TResponse) : null;\n\n return {\n status: response.status,\n body,\n headers: responseHeaders,\n };\n}\n\nfunction headersToRecord(headers: Headers): Record<string, string> {\n const record: Record<string, string> = {};\n headers.forEach((value, key) => {\n record[key.toLowerCase()] = value;\n });\n return record;\n}\n\nfunction concatAttributes(\n request: HttpRequest,\n attrs: Attributes\n): HttpRequest {\n const nextAttributes = Attributes.concat(\n request.attributes ?? Attributes.empty,\n attrs\n );\n const nextRequest: typeof request = {\n ...request,\n attributes: nextAttributes,\n };\n return nextRequest;\n}\n\nexport const Request = {\n concatAttributes,\n};\n","export const BASE_URL = 'https://mobile-api.coinlist.co';\nexport const HEADER_API_VERSION = 'X-API-Version';\nexport const HEADER_USER_AGENT = 'User-Agent';\nexport const HEADER_IDEMPOTENCY_KEY = 'Idempotency-Key';\nexport const API_VERSION = '2025-10-17';\n\nexport const COINLIST_OAUTH_URL = 'https://coinlist.co/oauth/authorize';\n","import { HEADER_API_VERSION } from '@/shared/api/frontline';\nimport type {\n HttpClientConfig,\n HttpRequest,\n HttpResponse,\n} from '@/shared/api/http';\nimport { makeRequest } from '@/shared/api/http';\n\nexport type BeforeRequestMiddleware = (\n request: HttpRequest\n) => Promise<HttpRequest>;\n\nexport type AfterRequestMiddleware = (args: {\n request: HttpRequest;\n response: HttpResponse<unknown>;\n retry: (request?: HttpRequest) => Promise<HttpResponse<unknown>>;\n}) => Promise<HttpResponse<unknown>>;\n\nexport type HttpClientMiddleware = {\n beforeRequest?: BeforeRequestMiddleware[];\n afterRequest?: AfterRequestMiddleware[];\n};\n\nexport class HttpClient {\n constructor(\n readonly config: HttpClientConfig,\n readonly middleware: HttpClientMiddleware = {}\n ) {}\n\n async send<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return this.runRequestWithAfterMiddleware<TResponse>(request);\n }\n\n /**\n * Runs beforeRequest, executeRequest, then the full afterRequest middleware\n * chain. Used by send() and by the retry() callback so that when a\n * middleware calls retry(), the retried response also goes through all\n * afterRequest middleware (e.g. session renewal, retry). Middleware\n * must use request.attributes.retryAttempt (or similar) to avoid infinite\n * recursion when they trigger retries.\n */\n private async runRequestWithAfterMiddleware<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n const preparedRequest = await this.runBeforeRequestMiddleware(\n this.withClientDefaults(request)\n );\n let response = await this.executeRequest<TResponse>(preparedRequest);\n\n for (const middleware of this.middleware.afterRequest ?? []) {\n response = (await middleware({\n request: preparedRequest,\n response,\n retry: (nextRequest = preparedRequest) =>\n this.runRequestWithAfterMiddleware(nextRequest) as Promise<\n HttpResponse<TResponse>\n >,\n })) as HttpResponse<TResponse>;\n }\n\n return response;\n }\n\n private withClientDefaults(request: HttpRequest): HttpRequest {\n const url = this.resolveUrl(request.url);\n const headers = {\n ...(request.headers ?? {}),\n [HEADER_API_VERSION]: this.config.xApiVersion,\n };\n\n return {\n ...request,\n url,\n headers,\n };\n }\n\n /**\n * Resolves a request URL against the client's baseUrl.\n *\n * @internal Public only for testing. Do not use in application code; use\n * {@link HttpClient.send} with a path and the client will resolve the URL.\n */\n resolveUrl(url: string): string {\n // If already absolute, use as-is.\n if (url.startsWith('http://') || url.startsWith('https://')) {\n return url;\n }\n // Otherwise append path to baseUrl so base path (e.g. /api) is preserved.\n const base = this.config.baseUrl.replace(/\\/$/, '');\n const path = url.startsWith('/') ? url : `/${url}`;\n return base + path;\n }\n\n private async runBeforeRequestMiddleware(\n initialRequest: HttpRequest\n ): Promise<HttpRequest> {\n let request = initialRequest;\n for (const middleware of this.middleware.beforeRequest ?? []) {\n request = await middleware(request);\n }\n\n return request;\n }\n\n private executeRequest<TResponse = unknown>(\n request: HttpRequest\n ): Promise<HttpResponse<TResponse>> {\n return makeRequest<TResponse>(request);\n }\n}\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\n\nconst MAX_ATTEMPTS = 3;\nconst INITIAL_DELAY_MS = 300;\nconst MAX_DELAY_MS = 2000;\n\nconst RETRYABLE_4XX = new Set([408, 409, 429]);\n\nexport function isRetryableStatus(status: number): boolean {\n return (status >= 500 && status < 600) || RETRYABLE_4XX.has(status);\n}\n\nfunction getRetryDelayMs(attempt: number): number {\n return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);\n}\n\nexport type RequestRetryMiddlewareOptions = {\n /**\n * Optional delay function. Defaults to real setTimeout-based delay.\n * Use a no-op (e.g. () => Promise.resolve()) in tests to avoid slow tests.\n */\n delayFn?: (ms: number) => Promise<void>;\n};\n\n/**\n * Creates the request retry after-request middleware. Inject a no-op delayFn\n * in tests to avoid real delays (unit testing best practice).\n */\nexport function createRequestRetryMiddleware(\n options: RequestRetryMiddlewareOptions = {}\n): AfterRequestMiddleware {\n const delayFn = options.delayFn ?? defaultDelay;\n\n return async ({ request, response, retry }) => {\n if (!isRetryableStatus(response.status)) {\n return response;\n }\n\n const currentAttempt = Attributes.getRetryAttempt(request.attributes);\n if (currentAttempt >= MAX_ATTEMPTS - 1) {\n return response;\n }\n\n const nextAttempt = currentAttempt + 1;\n await delayFn(getRetryDelayMs(nextAttempt));\n\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.retryAttempt(nextAttempt)\n );\n return retry(nextRequest);\n };\n}\n\nfunction defaultDelay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Retries the request up to 3 times with exponential backoff when the response\n * has a retryable status (5xx server errors). Uses request.attributes.retryAttempt\n * to decide delay and whether to retry, so the middleware does not loop when\n * retry() runs the full HTTP middleware chain again.\n */\nexport const requestRetryMiddleware: AfterRequestMiddleware =\n createRequestRetryMiddleware();\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/client/core/api/middleware/attach-session-middleware.ts","../src/client/core/api/middleware/session-renewal.ts","../src/shared/utils.ts","../src/client/core/api/middleware/device-id.ts","../src/client/core/api/middleware/user-agent.ts","../src/shared/api/middleware/idempotency-key.ts","../src/client/core/api/api-client.ts","../src/client/core/coinlist-client.ts"],"sourcesContent":["import type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function attachSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): BeforeRequestMiddleware {\n return async (request: HttpRequest): Promise<HttpRequest> => {\n if (!Attributes.isProtected(request.attributes)) {\n return request;\n }\n\n const accessToken = await fetchAccessToken(false);\n if (accessToken === null) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n Authorization: `Bearer ${accessToken.value}`,\n },\n };\n };\n}\n","import { Request } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { AfterRequestMiddleware } from '@/shared/api/http-client';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport function renewSessionMiddleware(\n fetchAccessToken: (refresh: boolean) => Promise<OAuthAccessToken | null>\n): AfterRequestMiddleware {\n return async ({ request, response, retry }) => {\n if (response.status !== 401) {\n return response;\n }\n if (!Attributes.isProtected(request.attributes)) {\n return response;\n }\n if (Attributes.wasRenewAttempted(request.attributes)) {\n return response;\n }\n\n const newToken = await fetchAccessToken(true);\n if (newToken) {\n const nextRequest = Request.concatAttributes(\n request,\n Attributes.renewAttempted(true)\n );\n return retry(nextRequest);\n } else {\n return response;\n }\n };\n}\n","/**\n * Computes SHA-256 digest of the input (UTF-8 string or raw bytes).\n * Uses the Web Crypto API.\n */\nexport async function sha256(data: string | Uint8Array): Promise<ArrayBuffer> {\n const bytes =\n typeof data === 'string' ? new TextEncoder().encode(data) : data;\n const buffer = bytes.buffer.slice(\n bytes.byteOffset,\n bytes.byteOffset + bytes.byteLength\n ) as ArrayBuffer;\n return crypto.subtle.digest('SHA-256', buffer);\n}\n\n/**\n * Converts an ArrayBuffer to Base64-URL encoding (RFC 4648).\n * @param padding - If false, omits trailing '=' padding (e.g. for PKCE).\n */\nexport function arrayBufferToBase64Url(\n buffer: ArrayBuffer,\n padding: boolean = true\n): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n let base64 = btoa(binary);\n base64 = base64.replace(/\\+/g, '-').replace(/\\//g, '_');\n if (!padding) {\n base64 = base64.replace(/=+$/, '');\n }\n return base64;\n}\n\n/**\n * Securely generates random bytes and returns them as Base64-URL (no padding).\n * Suitable for PKCE code_verifier and OAuth state.\n * @param byteLength - Number of random bytes (e.g. 32 for PKCE).\n */\nexport function generateSecureRandomBase64Url(byteLength: number): string {\n const bytes = new Uint8Array(byteLength);\n crypto.getRandomValues(bytes);\n const buffer = bytes.buffer;\n return arrayBufferToBase64Url(buffer, false);\n}\n\n/**\n * Generate a UUID v4 using the standard Web Crypto API (crypto.randomUUID).\n * Falls back to RFC 4122–compliant generation via crypto.getRandomValues in old browsers.\n */\nexport function getUUIDv4(): string {\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.randomUUID === 'function'\n ) {\n return crypto.randomUUID();\n }\n\n if (\n typeof crypto !== 'undefined' &&\n typeof crypto.getRandomValues === 'function'\n ) {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[6] = (bytes[6] & 0x0f) | 0x40;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'));\n return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;\n }\n\n return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;\n}\n","'use client';\n\nimport { getUUIDv4 } from '@/shared/utils';\n\nconst DEVICE_ID_KEY = 'coinlist.device_id';\n\n/**\n * Get or create a device ID stored in localStorage.\n * Uses a crypto UUID that persists across sessions.\n */\nexport function getDeviceId(): string {\n let deviceId = localStorage.getItem(DEVICE_ID_KEY);\n\n if (!deviceId) {\n deviceId = getUUIDv4();\n localStorage.setItem(DEVICE_ID_KEY, deviceId);\n }\n\n return deviceId;\n}\n\n/**\n * Clear the stored device ID (useful for testing or logout).\n */\nexport function clearDeviceId(): void {\n localStorage.removeItem(DEVICE_ID_KEY);\n}\n","'use client';\n\nimport { getDeviceId } from '@/client/core/api/middleware/device-id';\nimport { HEADER_USER_AGENT } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\n\nexport const userAgentMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.needUserAgent(request.attributes)) {\n return request;\n }\n\n const userAgent = generateUserAgent();\n return {\n ...request,\n headers: {\n ...(request.headers ?? {}),\n [HEADER_USER_AGENT]: userAgent,\n },\n };\n};\n\n// Generate User-Agent in the required format for Frontline API\n// Format: <app-info> (<device-info>; DevicePlatform <device-platform>; DeviceAppID <device-app-id>; DeviceID <device-id>)\nfunction generateUserAgent(): string {\n const appInfo = `CoinList/1.0`;\n const deviceInfo = 'Web Browser';\n const devicePlatform = 'DevicePlatform web'; // Must be 'web' for validation\n\n const deviceAppId = 'DeviceAppID co.coinlist.sdk'; // SDK app ID\n\n const deviceId = getDeviceId();\n const finalDeviceId = `DeviceID ${deviceId}`;\n\n return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;\n}\n","import { HEADER_IDEMPOTENCY_KEY } from '@/shared/api/frontline';\nimport type { HttpRequest } from '@/shared/api/http';\nimport { Attributes } from '@/shared/api/http-attributes';\nimport type { BeforeRequestMiddleware } from '@/shared/api/http-client';\nimport { getUUIDv4 } from '@/shared/utils';\n\nexport const idempotencyKeyMiddleware: BeforeRequestMiddleware = async (\n request: HttpRequest\n): Promise<HttpRequest> => {\n if (!Attributes.isIdempotent(request.attributes)) {\n return request;\n }\n\n const existingHeaders = request.headers ?? {};\n if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {\n return request;\n }\n\n return {\n ...request,\n headers: {\n ...existingHeaders,\n [HEADER_IDEMPOTENCY_KEY]: getUUIDv4(),\n },\n };\n};\n","'use client';\n\nimport { attachSessionMiddleware } from '@/client/core/api/middleware/attach-session-middleware';\nimport { renewSessionMiddleware } from '@/client/core/api/middleware/session-renewal';\nimport { userAgentMiddleware } from '@/client/core/api/middleware/user-agent';\nimport {\n type HttpClientConfig,\n HttpError,\n type HttpRequest,\n} from '@/shared/api/http';\nimport { HttpClient } from '@/shared/api/http-client';\nimport { idempotencyKeyMiddleware } from '@/shared/api/middleware/idempotency-key';\nimport { requestRetryMiddleware } from '@/shared/api/middleware/request-retry';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\n\nexport class ApiClient {\n private readonly httpClient: HttpClient;\n\n constructor(\n readonly config: HttpClientConfig,\n readonly fetchAccessToken: (\n refresh: boolean\n ) => Promise<OAuthAccessToken | null>\n ) {\n this.httpClient = new HttpClient(this.config, {\n beforeRequest: [\n attachSessionMiddleware(this.fetchAccessToken),\n userAgentMiddleware,\n idempotencyKeyMiddleware,\n ],\n afterRequest: [\n renewSessionMiddleware(this.fetchAccessToken),\n requestRetryMiddleware,\n ],\n });\n }\n\n async send<TResponse>(request: HttpRequest): Promise<TResponse> {\n const response = await this.httpClient.send<TResponse>(request);\n if (response.status >= 200 && response.status < 300) {\n return response.body as TResponse;\n } else {\n throw new HttpError(response);\n }\n }\n}\n","'use client';\n\nimport { ApiClient } from '@/client/core/api/api-client';\nimport type {\n AuthState,\n OauthClientResult,\n} from '@/client/core/types/client-types';\nimport {\n API_VERSION,\n BASE_URL,\n COINLIST_OAUTH_URL,\n} from '@/shared/api/frontline';\nimport type { Config } from '@/shared/types/config';\nimport { AuthorizationCode, CodeVerifier } from '@/shared/types/oauth';\nimport type { OAuthAccessToken } from '@/shared/types/oauth-session';\nimport {\n arrayBufferToBase64Url,\n generateSecureRandomBase64Url,\n sha256,\n} from '@/shared/utils';\n\n/** Session storage keys for OAuth PKCE (used by startOauth and completeOauth). */\nexport const OAUTH_STATE_KEY = 'coinlist.oauth_state';\nexport const OAUTH_CODE_VERIFIER_KEY = 'coinlist.oauth_code_verifier';\n\nexport interface ClientConfig extends Config {\n /**\n * The SDK user must provide this function. It is responsible for fetching a\n * valid {@link OAuthAccessToken}, or **null** for logged-out users. If your\n * backend is in TypeScript, it is recommended to use\n * {@link CoinListServer#accessToken} from `@/server/coinlist.server`.\n */\n getAccessToken: () => Promise<OAuthAccessToken | null>;\n}\n\nexport interface CoinListClient {\n /**\n * Calls the provided {@link ClientConfig.getAccessToken} lambda to determine\n * the user's {@link AuthState}. This is called automatically by {@link CoinListProvider};\n * you typically do not need to call it yourself unless you are using the client\n * outside of the provider.\n */\n init(): Promise<AuthState>;\n /**\n * Returns the current auth state: `'logged-in'`, `'logged-out'`, or `'unknown'`.\n * Returns **unknown** until {@link CoinListClient.init} has completed. It is\n * recommended to use the `const { isReady, coinlist } = useCoinList()` hook to know when\n * the SDK has initialized, or to manually await {@link CoinListClient.init}\n * before relying on this value.\n */\n getAuthState(): AuthState;\n /**\n * Starts the OAuth flow with PKCE. Generates a random `state` and `code_verifier`,\n * stores them in sessionStorage (for validation in {@link CoinListClient.completeOAuth}\n * on redirect), then redirects the user to the CoinList authorization page. After\n * the user authorizes, they are sent back to your application's redirect URI; call\n * {@link CoinListClient.completeOAuth} on that page to finish the flow.\n */\n startOAuth(): Promise<void>;\n /**\n * Completes the OAuth flow on the FE with PKCE and state validation. Parses the\n * redirect URL (state, code) and validates them against sessionStorage\n * (state, code_verifier). The state check protects against CSRF (cross-site\n * request forgery). Call this on the frontend page the user is redirected\n * to after authorizing.\n *\n * On `ok`: Call your backend to exchange the short-lived `authorizationCode`\n * (and the returned `codeVerifier`) for a long-lived session. If your backend\n * is in TypeScript, you can use {@link CoinListServer#completeOAuth} from\n * `@/server/coinlist.server` (CoinListServer) to perform that exchange.\n *\n * On `error`: `reason` is an {@link OauthClientErrorReason} (e.g. `'missing_state'`, `'invalid_state'`) so you can switch on it and surface a message to the user.\n */\n completeOAuth(): OauthClientResult;\n /**\n * Clears the in-memory access token. Note: you need to manually\n * also logout on the backend via CoinListServer#logout to clear the session cookie.\n */\n logout(): void;\n}\n\nclass CoinListClientImpl implements CoinListClient {\n /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */\n private _accessToken: OAuthAccessToken | null | undefined = undefined;\n // biome-ignore lint/correctness/noUnusedPrivateClassMembers: we'll need it\n private readonly _api: ApiClient;\n\n constructor(readonly _config: ClientConfig) {\n this._api = new ApiClient(\n {\n baseUrl: _config.baseUrl || BASE_URL,\n xApiVersion: API_VERSION,\n },\n this.fetchAccessToken.bind(this)\n );\n }\n\n async init(): Promise<AuthState> {\n await this.fetchAccessToken(true);\n return this.getAuthState();\n }\n\n async fetchAccessToken(refresh: boolean): Promise<OAuthAccessToken | null> {\n if (!refresh && this._accessToken !== undefined) return this._accessToken;\n\n try {\n const accessToken = await this._config.getAccessToken();\n this._accessToken = accessToken;\n return accessToken;\n } catch (_) {\n this._accessToken = null;\n return null;\n }\n }\n\n getAuthState(): AuthState {\n if (this._accessToken === undefined) {\n return 'unknown';\n }\n return this._accessToken !== null ? 'logged-in' : 'logged-out';\n }\n\n async startOAuth(): Promise<void> {\n const state = generateSecureRandomBase64Url(32);\n const codeVerifier = generateSecureRandomBase64Url(32);\n\n const codeChallengeRaw = await sha256(codeVerifier);\n const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);\n\n sessionStorage.setItem(OAUTH_STATE_KEY, state);\n sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);\n\n const params = new URLSearchParams({\n client_id: this._config.clientId,\n response_type: 'code',\n redirect_uri: this._config.redirectUri,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n });\n const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;\n window.location.href = url;\n }\n\n completeOAuth(): OauthClientResult {\n const params = new URLSearchParams(window.location.search);\n\n const stateParam = params.get('state');\n if (!stateParam) {\n return { type: 'error', reason: 'missing_state' };\n }\n const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);\n if (storedState === null || storedState !== stateParam) {\n return { type: 'error', reason: 'invalid_state' };\n }\n\n const codeParam = params.get('code');\n if (!codeParam) {\n return { type: 'error', reason: 'missing_code' };\n }\n\n const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);\n if (!codeVerifierValue) {\n return { type: 'error', reason: 'missing_code_verifier' };\n }\n\n return {\n type: 'ok',\n code: AuthorizationCode(codeParam),\n codeVerifier: CodeVerifier(codeVerifierValue),\n };\n }\n\n logout(): void {\n this._accessToken = null;\n }\n}\n\nexport function createCoinListClient(config: ClientConfig): CoinListClient {\n return new CoinListClientImpl(config);\n}\n"],"mappings":"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","names":[]}