@coinlist-co/react 0.0.1

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.
Files changed (47) hide show
  1. package/README.md +7 -0
  2. package/dist/chunk-5E3P7AMH.js +20 -0
  3. package/dist/chunk-5E3P7AMH.js.map +1 -0
  4. package/dist/chunk-F56RBKNS.js +63 -0
  5. package/dist/chunk-F56RBKNS.js.map +1 -0
  6. package/dist/chunk-NLJMUHOG.js +253 -0
  7. package/dist/chunk-NLJMUHOG.js.map +1 -0
  8. package/dist/chunk-OETRTIQ4.js +274 -0
  9. package/dist/chunk-OETRTIQ4.js.map +1 -0
  10. package/dist/chunk-YJQMJFA7.js +16 -0
  11. package/dist/chunk-YJQMJFA7.js.map +1 -0
  12. package/dist/client/components/index.cjs +445 -0
  13. package/dist/client/components/index.cjs.map +1 -0
  14. package/dist/client/components/index.d.cts +19 -0
  15. package/dist/client/components/index.d.ts +19 -0
  16. package/dist/client/components/index.js +399 -0
  17. package/dist/client/components/index.js.map +1 -0
  18. package/dist/client/core/index.cjs +566 -0
  19. package/dist/client/core/index.cjs.map +1 -0
  20. package/dist/client/core/index.d.cts +22 -0
  21. package/dist/client/core/index.d.ts +22 -0
  22. package/dist/client/core/index.js +35 -0
  23. package/dist/client/core/index.js.map +1 -0
  24. package/dist/client/hooks/index.cjs +56 -0
  25. package/dist/client/hooks/index.cjs.map +1 -0
  26. package/dist/client/hooks/index.d.cts +14 -0
  27. package/dist/client/hooks/index.d.ts +14 -0
  28. package/dist/client/hooks/index.js +9 -0
  29. package/dist/client/hooks/index.js.map +1 -0
  30. package/dist/client/index.cjs +621 -0
  31. package/dist/client/index.cjs.map +1 -0
  32. package/dist/client/index.d.cts +18 -0
  33. package/dist/client/index.d.ts +18 -0
  34. package/dist/client/index.js +41 -0
  35. package/dist/client/index.js.map +1 -0
  36. package/dist/client/styles.css +2 -0
  37. package/dist/coinlist-client-BH_Cn_7u.d.cts +71 -0
  38. package/dist/coinlist-client-fkB3J-2a.d.ts +71 -0
  39. package/dist/oauth-session-g0OPJNKn.d.cts +59 -0
  40. package/dist/oauth-session-g0OPJNKn.d.ts +59 -0
  41. package/dist/server/index.cjs +405 -0
  42. package/dist/server/index.cjs.map +1 -0
  43. package/dist/server/index.d.cts +37 -0
  44. package/dist/server/index.d.ts +37 -0
  45. package/dist/server/index.js +142 -0
  46. package/dist/server/index.js.map +1 -0
  47. package/package.json +122 -0
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # CoinList React SDK
2
+
3
+ This package is documented at:
4
+
5
+ https://docs.coinlist.co/
6
+
7
+ Please refer to the docs site for installation, configuration, and integration guides.
@@ -0,0 +1,20 @@
1
+ // src/shared/types/oauth-session.ts
2
+ var OAuthRefreshToken = (value) => value;
3
+ var OAuthSession = {
4
+ fromDto: (dto) => {
5
+ const expiresAt = new Date(Date.now() + dto.expires_in * 1e3);
6
+ return {
7
+ accessToken: {
8
+ value: dto.access_token,
9
+ expiresAt
10
+ },
11
+ ...dto.refresh_token != null && dto.refresh_token !== "" ? { refreshToken: OAuthRefreshToken(dto.refresh_token) } : void 0
12
+ };
13
+ }
14
+ };
15
+
16
+ export {
17
+ OAuthRefreshToken,
18
+ OAuthSession
19
+ };
20
+ //# sourceMappingURL=chunk-5E3P7AMH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/types/oauth-session.ts"],"sourcesContent":["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"],"mappings":";AASO,IAAM,oBAAoB,CAAC,UAAkB;AAO7C,IAAM,eAAe;AAAA,EAC1B,SAAS,CAAC,QAAuC;AAC/C,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,aAAa,GAAI;AAC7D,WAAO;AAAA,MACL,aAAa;AAAA,QACX,OAAO,IAAI;AAAA,QACX;AAAA,MACF;AAAA,MACA,GAAI,IAAI,iBAAiB,QAAQ,IAAI,kBAAkB,KACnD,EAAE,cAAc,kBAAkB,IAAI,aAAa,EAAE,IACrD;AAAA,IACN;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,63 @@
1
+ import {
2
+ createCoinListClient
3
+ } from "./chunk-OETRTIQ4.js";
4
+
5
+ // src/client/CoinListProvider.tsx
6
+ import {
7
+ createContext,
8
+ useEffect,
9
+ useMemo,
10
+ useState
11
+ } from "react";
12
+ import { jsx } from "react/jsx-runtime";
13
+ var CoinListContext = createContext(null);
14
+ function CoinListProvider({ config, children }) {
15
+ const [isReady, setIsReady] = useState(false);
16
+ const coinlist = useMemo(() => createCoinListClient(config), [config]);
17
+ useEffect(() => {
18
+ let isCancelled = false;
19
+ if (coinlist.getAuthState() === "unknown") {
20
+ coinlist.init().then(() => {
21
+ if (!isCancelled) {
22
+ setIsReady(true);
23
+ }
24
+ }).catch(() => {
25
+ if (!isCancelled) {
26
+ setIsReady(true);
27
+ }
28
+ });
29
+ } else {
30
+ setIsReady(true);
31
+ }
32
+ return () => {
33
+ isCancelled = true;
34
+ };
35
+ }, [coinlist]);
36
+ const value = useMemo(() => ({ coinlist, isReady }), [coinlist, isReady]);
37
+ return /* @__PURE__ */ jsx(CoinListContext.Provider, { value, children });
38
+ }
39
+
40
+ // src/client/hooks/useCoinList.ts
41
+ import { useContext } from "react";
42
+ function useCoinList() {
43
+ const context = useContext(CoinListContext);
44
+ if (context === null) {
45
+ throw new CoinListClientInitializationError();
46
+ }
47
+ return {
48
+ isReady: context.isReady,
49
+ coinlist: context.coinlist
50
+ };
51
+ }
52
+ var CoinListClientInitializationError = class extends Error {
53
+ constructor(message = "useCoinList() must be used within CoinListProvider. Wrap your app with <CoinListProvider config={...}>.") {
54
+ super(message);
55
+ this.name = "CoinListClientInitializationError";
56
+ }
57
+ };
58
+
59
+ export {
60
+ CoinListProvider,
61
+ useCoinList
62
+ };
63
+ //# sourceMappingURL=chunk-F56RBKNS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/CoinListProvider.tsx","../src/client/hooks/useCoinList.ts"],"sourcesContent":["/**\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","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"],"mappings":";;;;;AAKA;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAqDH;AAzCG,IAAM,kBAAkB,cAA2C,IAAI;AAUvE,SAAS,iBAAiB,EAAE,QAAQ,SAAS,GAA0B;AAC5E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,WAAW,QAAQ,MAAM,qBAAqB,MAAM,GAAG,CAAC,MAAM,CAAC;AAErE,YAAU,MAAM;AACd,QAAI,cAAc;AAClB,QAAI,SAAS,aAAa,MAAM,WAAW;AACzC,eACG,KAAK,EACL,KAAK,MAAM;AACV,YAAI,CAAC,aAAa;AAChB,qBAAW,IAAI;AAAA,QACjB;AAAA,MACF,CAAC,EACA,MAAM,MAAM;AACX,YAAI,CAAC,aAAa;AAChB,qBAAW,IAAI;AAAA,QACjB;AAAA,MACF,CAAC;AAAA,IACL,OAAO;AACL,iBAAW,IAAI;AAAA,IACjB;AAEA,WAAO,MAAM;AACX,oBAAc;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,QAAQ,QAAQ,OAAO,EAAE,UAAU,QAAQ,IAAI,CAAC,UAAU,OAAO,CAAC;AAExE,SACE,oBAAC,gBAAgB,UAAhB,EAAyB,OACvB,UACH;AAEJ;;;ACpEA,SAAS,kBAAkB;AAapB,SAAS,cAAiC;AAC/C,QAAM,UAAU,WAAW,eAAe;AAC1C,MAAI,YAAY,MAAM;AACpB,UAAM,IAAI,kCAAkC;AAAA,EAC9C;AAEA,SAAO;AAAA,IACL,SAAS,QAAQ;AAAA,IACjB,UAAU,QAAQ;AAAA,EACpB;AACF;AAEO,IAAM,oCAAN,cAAgD,MAAM;AAAA,EAC3D,YACE,UAAU,2GACV;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;","names":[]}
@@ -0,0 +1,253 @@
1
+ // src/shared/types/oauth.ts
2
+ var AuthorizationCode = (value) => value;
3
+ var CodeVerifier = (value) => value;
4
+ var CodeChallenge = (value) => value;
5
+ var RedirectUri = (value) => value;
6
+ var ClientId = (value) => value;
7
+ var ClientSecret = (value) => value;
8
+
9
+ // src/shared/api/http-attributes.ts
10
+ var empty = {};
11
+ var concat = (left, right) => ({
12
+ ...left,
13
+ ...right
14
+ });
15
+ var concatAll = (...items) => {
16
+ let result = empty;
17
+ for (const item of items) {
18
+ result = concat(result, item);
19
+ }
20
+ return result;
21
+ };
22
+ var protectedRequest = () => ({ protected: true });
23
+ var userAgent = () => ({ userAgent: true });
24
+ var idempotencyKey = () => ({
25
+ idempotencyKey: true
26
+ });
27
+ var retryAttempt = (attempt) => ({
28
+ retryAttempt: attempt
29
+ });
30
+ var renewAttempted = (value) => ({
31
+ renewAttempted: value
32
+ });
33
+ var isProtected = (attrs) => attrs?.protected === true;
34
+ var needUserAgent = (attrs) => attrs?.userAgent === true;
35
+ var isIdempotent = (attrs) => attrs?.idempotencyKey === true;
36
+ var getRetryAttempt = (attrs) => attrs?.retryAttempt ?? 0;
37
+ var wasRenewAttempted = (attrs) => attrs?.renewAttempted === true;
38
+ var Attributes = {
39
+ empty,
40
+ concat,
41
+ concatAll,
42
+ protected: protectedRequest,
43
+ isProtected,
44
+ userAgent,
45
+ needUserAgent,
46
+ idempotencyKey,
47
+ isIdempotent,
48
+ retryAttempt,
49
+ getRetryAttempt,
50
+ renewAttempted,
51
+ wasRenewAttempted
52
+ };
53
+
54
+ // src/shared/api/http.ts
55
+ var HttpError = class extends Error {
56
+ constructor(response) {
57
+ super(`Request failed with ${response.status} status`);
58
+ this.name = "HttpError";
59
+ this.response = response;
60
+ }
61
+ };
62
+ async function makeRequest(request) {
63
+ const headers = {
64
+ Accept: "application/json",
65
+ ...request.method === "POST" && request.body !== void 0 ? { "Content-Type": "application/json" } : {},
66
+ ...request.headers ?? {}
67
+ };
68
+ const init = {
69
+ method: request.method,
70
+ headers,
71
+ ...request.redirect !== void 0 ? { redirect: request.redirect } : {}
72
+ };
73
+ if (request.method === "POST" && request.body !== void 0) {
74
+ init.body = JSON.stringify(request.body);
75
+ }
76
+ const response = await fetch(request.url, init);
77
+ const responseHeaders = headersToRecord(response.headers);
78
+ if (response.status === 204 || response.status === 205) {
79
+ return {
80
+ status: response.status,
81
+ body: null,
82
+ headers: responseHeaders
83
+ };
84
+ }
85
+ if (response.status >= 300 && response.status < 400) {
86
+ await response.text();
87
+ return {
88
+ status: response.status,
89
+ body: null,
90
+ headers: responseHeaders
91
+ };
92
+ }
93
+ const text = await response.text();
94
+ const body = text ? JSON.parse(text) : null;
95
+ return {
96
+ status: response.status,
97
+ body,
98
+ headers: responseHeaders
99
+ };
100
+ }
101
+ function headersToRecord(headers) {
102
+ const record = {};
103
+ headers.forEach((value, key) => {
104
+ record[key.toLowerCase()] = value;
105
+ });
106
+ return record;
107
+ }
108
+ function concatAttributes(request, attrs) {
109
+ const nextAttributes = Attributes.concat(
110
+ request.attributes ?? Attributes.empty,
111
+ attrs
112
+ );
113
+ const nextRequest = {
114
+ ...request,
115
+ attributes: nextAttributes
116
+ };
117
+ return nextRequest;
118
+ }
119
+ var Request = {
120
+ concatAttributes
121
+ };
122
+
123
+ // src/shared/api/frontline.ts
124
+ var BASE_URL = "https://mobile-api.coinlist.co";
125
+ var HEADER_API_VERSION = "X-API-Version";
126
+ var HEADER_USER_AGENT = "User-Agent";
127
+ var HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";
128
+ var API_VERSION = "2025-10-17";
129
+ var COINLIST_OAUTH_URL = "https://coinlist.co/oauth/authorize";
130
+
131
+ // src/shared/api/http-client.ts
132
+ var HttpClient = class {
133
+ constructor(config, middleware = {}) {
134
+ this.config = config;
135
+ this.middleware = middleware;
136
+ }
137
+ async send(request) {
138
+ return this.runRequestWithAfterMiddleware(request);
139
+ }
140
+ /**
141
+ * Runs beforeRequest, executeRequest, then the full afterRequest middleware
142
+ * chain. Used by send() and by the retry() callback so that when a
143
+ * middleware calls retry(), the retried response also goes through all
144
+ * afterRequest middleware (e.g. session renewal, retry). Middleware
145
+ * must use request.attributes.retryAttempt (or similar) to avoid infinite
146
+ * recursion when they trigger retries.
147
+ */
148
+ async runRequestWithAfterMiddleware(request) {
149
+ const preparedRequest = await this.runBeforeRequestMiddleware(
150
+ this.withClientDefaults(request)
151
+ );
152
+ let response = await this.executeRequest(preparedRequest);
153
+ for (const middleware of this.middleware.afterRequest ?? []) {
154
+ response = await middleware({
155
+ request: preparedRequest,
156
+ response,
157
+ retry: (nextRequest = preparedRequest) => this.runRequestWithAfterMiddleware(nextRequest)
158
+ });
159
+ }
160
+ return response;
161
+ }
162
+ withClientDefaults(request) {
163
+ const url = this.resolveUrl(request.url);
164
+ const headers = {
165
+ ...request.headers ?? {},
166
+ [HEADER_API_VERSION]: this.config.xApiVersion
167
+ };
168
+ return {
169
+ ...request,
170
+ url,
171
+ headers
172
+ };
173
+ }
174
+ /**
175
+ * Resolves a request URL against the client's baseUrl.
176
+ *
177
+ * @internal Public only for testing. Do not use in application code; use
178
+ * {@link HttpClient.send} with a path and the client will resolve the URL.
179
+ */
180
+ resolveUrl(url) {
181
+ if (url.startsWith("http://") || url.startsWith("https://")) {
182
+ return url;
183
+ }
184
+ const base = this.config.baseUrl.replace(/\/$/, "");
185
+ const path = url.startsWith("/") ? url : `/${url}`;
186
+ return base + path;
187
+ }
188
+ async runBeforeRequestMiddleware(initialRequest) {
189
+ let request = initialRequest;
190
+ for (const middleware of this.middleware.beforeRequest ?? []) {
191
+ request = await middleware(request);
192
+ }
193
+ return request;
194
+ }
195
+ executeRequest(request) {
196
+ return makeRequest(request);
197
+ }
198
+ };
199
+
200
+ // src/shared/api/middleware/request-retry.ts
201
+ var MAX_ATTEMPTS = 3;
202
+ var INITIAL_DELAY_MS = 300;
203
+ var MAX_DELAY_MS = 2e3;
204
+ var RETRYABLE_4XX = /* @__PURE__ */ new Set([408, 409, 429]);
205
+ function isRetryableStatus(status) {
206
+ return status >= 500 && status < 600 || RETRYABLE_4XX.has(status);
207
+ }
208
+ function getRetryDelayMs(attempt) {
209
+ return Math.min(INITIAL_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
210
+ }
211
+ function createRequestRetryMiddleware(options = {}) {
212
+ const delayFn = options.delayFn ?? defaultDelay;
213
+ return async ({ request, response, retry }) => {
214
+ if (!isRetryableStatus(response.status)) {
215
+ return response;
216
+ }
217
+ const currentAttempt = Attributes.getRetryAttempt(request.attributes);
218
+ if (currentAttempt >= MAX_ATTEMPTS - 1) {
219
+ return response;
220
+ }
221
+ const nextAttempt = currentAttempt + 1;
222
+ await delayFn(getRetryDelayMs(nextAttempt));
223
+ const nextRequest = Request.concatAttributes(
224
+ request,
225
+ Attributes.retryAttempt(nextAttempt)
226
+ );
227
+ return retry(nextRequest);
228
+ };
229
+ }
230
+ function defaultDelay(ms) {
231
+ return new Promise((resolve) => setTimeout(resolve, ms));
232
+ }
233
+ var requestRetryMiddleware = createRequestRetryMiddleware();
234
+
235
+ export {
236
+ Attributes,
237
+ HttpError,
238
+ Request,
239
+ BASE_URL,
240
+ HEADER_USER_AGENT,
241
+ HEADER_IDEMPOTENCY_KEY,
242
+ API_VERSION,
243
+ COINLIST_OAUTH_URL,
244
+ HttpClient,
245
+ requestRetryMiddleware,
246
+ AuthorizationCode,
247
+ CodeVerifier,
248
+ CodeChallenge,
249
+ RedirectUri,
250
+ ClientId,
251
+ ClientSecret
252
+ };
253
+ //# sourceMappingURL=chunk-NLJMUHOG.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 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":[]}
@@ -0,0 +1,274 @@
1
+ import {
2
+ API_VERSION,
3
+ Attributes,
4
+ AuthorizationCode,
5
+ BASE_URL,
6
+ COINLIST_OAUTH_URL,
7
+ CodeVerifier,
8
+ HEADER_IDEMPOTENCY_KEY,
9
+ HEADER_USER_AGENT,
10
+ HttpClient,
11
+ HttpError,
12
+ Request,
13
+ requestRetryMiddleware
14
+ } from "./chunk-NLJMUHOG.js";
15
+
16
+ // src/client/core/api/middleware/attach-session-middleware.ts
17
+ function attachSessionMiddleware(fetchAccessToken) {
18
+ return async (request) => {
19
+ if (!Attributes.isProtected(request.attributes)) {
20
+ return request;
21
+ }
22
+ const accessToken = await fetchAccessToken(false);
23
+ if (accessToken === null) {
24
+ return request;
25
+ }
26
+ return {
27
+ ...request,
28
+ headers: {
29
+ ...request.headers ?? {},
30
+ Authorization: `Bearer ${accessToken.value}`
31
+ }
32
+ };
33
+ };
34
+ }
35
+
36
+ // src/client/core/api/middleware/session-renewal.ts
37
+ function renewSessionMiddleware(fetchAccessToken) {
38
+ return async ({ request, response, retry }) => {
39
+ if (response.status !== 401) {
40
+ return response;
41
+ }
42
+ if (!Attributes.isProtected(request.attributes)) {
43
+ return response;
44
+ }
45
+ if (Attributes.wasRenewAttempted(request.attributes)) {
46
+ return response;
47
+ }
48
+ const newToken = await fetchAccessToken(true);
49
+ if (newToken) {
50
+ const nextRequest = Request.concatAttributes(
51
+ request,
52
+ Attributes.renewAttempted(true)
53
+ );
54
+ return retry(nextRequest);
55
+ } else {
56
+ return response;
57
+ }
58
+ };
59
+ }
60
+
61
+ // src/shared/utils.ts
62
+ async function sha256(data) {
63
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
64
+ const buffer = bytes.buffer.slice(
65
+ bytes.byteOffset,
66
+ bytes.byteOffset + bytes.byteLength
67
+ );
68
+ return crypto.subtle.digest("SHA-256", buffer);
69
+ }
70
+ function arrayBufferToBase64Url(buffer, padding = true) {
71
+ const bytes = new Uint8Array(buffer);
72
+ let binary = "";
73
+ for (let i = 0; i < bytes.length; i++) {
74
+ binary += String.fromCharCode(bytes[i]);
75
+ }
76
+ let base64 = btoa(binary);
77
+ base64 = base64.replace(/\+/g, "-").replace(/\//g, "_");
78
+ if (!padding) {
79
+ base64 = base64.replace(/=+$/, "");
80
+ }
81
+ return base64;
82
+ }
83
+ function generateSecureRandomBase64Url(byteLength) {
84
+ const bytes = new Uint8Array(byteLength);
85
+ crypto.getRandomValues(bytes);
86
+ const buffer = bytes.buffer;
87
+ return arrayBufferToBase64Url(buffer, false);
88
+ }
89
+ function getUUIDv4() {
90
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
91
+ return crypto.randomUUID();
92
+ }
93
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
94
+ const bytes = new Uint8Array(16);
95
+ crypto.getRandomValues(bytes);
96
+ bytes[6] = bytes[6] & 15 | 64;
97
+ bytes[8] = bytes[8] & 63 | 128;
98
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
99
+ 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("")}`;
100
+ }
101
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
102
+ }
103
+
104
+ // src/client/core/api/middleware/device-id.ts
105
+ var DEVICE_ID_KEY = "coinlist.device_id";
106
+ function getDeviceId() {
107
+ let deviceId = localStorage.getItem(DEVICE_ID_KEY);
108
+ if (!deviceId) {
109
+ deviceId = getUUIDv4();
110
+ localStorage.setItem(DEVICE_ID_KEY, deviceId);
111
+ }
112
+ return deviceId;
113
+ }
114
+
115
+ // src/client/core/api/middleware/user-agent.ts
116
+ var userAgentMiddleware = async (request) => {
117
+ if (!Attributes.needUserAgent(request.attributes)) {
118
+ return request;
119
+ }
120
+ const userAgent = generateUserAgent();
121
+ return {
122
+ ...request,
123
+ headers: {
124
+ ...request.headers ?? {},
125
+ [HEADER_USER_AGENT]: userAgent
126
+ }
127
+ };
128
+ };
129
+ function generateUserAgent() {
130
+ const appInfo = `CoinList/1.0`;
131
+ const deviceInfo = "Web Browser";
132
+ const devicePlatform = "DevicePlatform web";
133
+ const deviceAppId = "DeviceAppID co.coinlist.sdk";
134
+ const deviceId = getDeviceId();
135
+ const finalDeviceId = `DeviceID ${deviceId}`;
136
+ return `${appInfo} (${deviceInfo}; ${devicePlatform}; ${deviceAppId}; ${finalDeviceId})`;
137
+ }
138
+
139
+ // src/shared/api/middleware/idempotency-key.ts
140
+ var idempotencyKeyMiddleware = async (request) => {
141
+ if (!Attributes.isIdempotent(request.attributes)) {
142
+ return request;
143
+ }
144
+ const existingHeaders = request.headers ?? {};
145
+ if (existingHeaders[HEADER_IDEMPOTENCY_KEY]) {
146
+ return request;
147
+ }
148
+ return {
149
+ ...request,
150
+ headers: {
151
+ ...existingHeaders,
152
+ [HEADER_IDEMPOTENCY_KEY]: getUUIDv4()
153
+ }
154
+ };
155
+ };
156
+
157
+ // src/client/core/api/api-client.ts
158
+ var ApiClient = class {
159
+ constructor(config, fetchAccessToken) {
160
+ this.config = config;
161
+ this.fetchAccessToken = fetchAccessToken;
162
+ this.httpClient = new HttpClient(this.config, {
163
+ beforeRequest: [
164
+ attachSessionMiddleware(this.fetchAccessToken),
165
+ userAgentMiddleware,
166
+ idempotencyKeyMiddleware
167
+ ],
168
+ afterRequest: [
169
+ renewSessionMiddleware(this.fetchAccessToken),
170
+ requestRetryMiddleware
171
+ ]
172
+ });
173
+ }
174
+ async send(request) {
175
+ const response = await this.httpClient.send(request);
176
+ if (response.status >= 200 && response.status < 300) {
177
+ return response.body;
178
+ } else {
179
+ throw new HttpError(response);
180
+ }
181
+ }
182
+ };
183
+
184
+ // src/client/core/coinlist-client.ts
185
+ var OAUTH_STATE_KEY = "coinlist.oauth_state";
186
+ var OAUTH_CODE_VERIFIER_KEY = "coinlist.oauth_code_verifier";
187
+ var CoinListClientImpl = class {
188
+ constructor(_config) {
189
+ this._config = _config;
190
+ /** Cached access token: undefined = loading, null = no token, AccessToken = logged in */
191
+ this._accessToken = void 0;
192
+ this._api = new ApiClient(
193
+ {
194
+ baseUrl: _config.baseUrl || BASE_URL,
195
+ xApiVersion: API_VERSION
196
+ },
197
+ this.fetchAccessToken.bind(this)
198
+ );
199
+ }
200
+ async init() {
201
+ await this.fetchAccessToken(true);
202
+ return this.getAuthState();
203
+ }
204
+ async fetchAccessToken(refresh) {
205
+ if (!refresh && this._accessToken !== void 0) return this._accessToken;
206
+ try {
207
+ const accessToken = await this._config.getAccessToken();
208
+ this._accessToken = accessToken;
209
+ return accessToken;
210
+ } catch (_) {
211
+ this._accessToken = null;
212
+ return null;
213
+ }
214
+ }
215
+ getAuthState() {
216
+ if (this._accessToken === void 0) {
217
+ return "unknown";
218
+ }
219
+ return this._accessToken !== null ? "logged-in" : "logged-out";
220
+ }
221
+ async startOAuth() {
222
+ const state = generateSecureRandomBase64Url(32);
223
+ const codeVerifier = generateSecureRandomBase64Url(32);
224
+ const codeChallengeRaw = await sha256(codeVerifier);
225
+ const codeChallenge = arrayBufferToBase64Url(codeChallengeRaw, false);
226
+ sessionStorage.setItem(OAUTH_STATE_KEY, state);
227
+ sessionStorage.setItem(OAUTH_CODE_VERIFIER_KEY, codeVerifier);
228
+ const params = new URLSearchParams({
229
+ client_id: this._config.clientId,
230
+ response_type: "code",
231
+ redirect_uri: this._config.redirectUri,
232
+ code_challenge: codeChallenge,
233
+ code_challenge_method: "S256",
234
+ state
235
+ });
236
+ const url = `${COINLIST_OAUTH_URL}?${params.toString()}`;
237
+ window.location.href = url;
238
+ }
239
+ completeOAuth() {
240
+ const params = new URLSearchParams(window.location.search);
241
+ const stateParam = params.get("state");
242
+ if (!stateParam) {
243
+ return { type: "error", reason: "missing_state" };
244
+ }
245
+ const storedState = sessionStorage.getItem(OAUTH_STATE_KEY);
246
+ if (storedState === null || storedState !== stateParam) {
247
+ return { type: "error", reason: "invalid_state" };
248
+ }
249
+ const codeParam = params.get("code");
250
+ if (!codeParam) {
251
+ return { type: "error", reason: "missing_code" };
252
+ }
253
+ const codeVerifierValue = sessionStorage.getItem(OAUTH_CODE_VERIFIER_KEY);
254
+ if (!codeVerifierValue) {
255
+ return { type: "error", reason: "missing_code_verifier" };
256
+ }
257
+ return {
258
+ type: "ok",
259
+ code: AuthorizationCode(codeParam),
260
+ codeVerifier: CodeVerifier(codeVerifierValue)
261
+ };
262
+ }
263
+ logout() {
264
+ this._accessToken = null;
265
+ }
266
+ };
267
+ function createCoinListClient(config) {
268
+ return new CoinListClientImpl(config);
269
+ }
270
+
271
+ export {
272
+ createCoinListClient
273
+ };
274
+ //# sourceMappingURL=chunk-OETRTIQ4.js.map