@coinlist-co/react 0.12.1 → 0.12.2-rc.0ff2d03

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 (34) hide show
  1. package/README.md +10 -0
  2. package/dist/chunk-MJ6YXZWH.js +22 -0
  3. package/dist/chunk-MJ6YXZWH.js.map +1 -0
  4. package/dist/{chunk-VH34YC3Q.js → chunk-PWGUKS4Q.js} +199 -186
  5. package/dist/chunk-PWGUKS4Q.js.map +1 -0
  6. package/dist/{chunk-DCW7NQXH.js → chunk-QTBEDN7C.js} +52 -19
  7. package/dist/chunk-QTBEDN7C.js.map +1 -0
  8. package/dist/{chunk-MOE5S6CE.js → chunk-SVPFFLIT.js} +1314 -796
  9. package/dist/chunk-SVPFFLIT.js.map +1 -0
  10. package/dist/client/index.d.ts +3332 -4554
  11. package/dist/client/index.js +7374 -4387
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/server/index.d.ts +69 -71
  14. package/dist/server/index.js +327 -67
  15. package/dist/server/index.js.map +1 -1
  16. package/dist/{tokens-namespace-CIWBtgt2.d.cts → time-BDzqB806.d.ts} +1901 -2491
  17. package/dist/{token-registry-snapshot-CqI6dXM_.d.cts → token-registry-snapshot-cvLsnMo_.d.ts} +74 -42
  18. package/dist/universal/index.d.ts +100 -1001
  19. package/dist/universal/index.js +52 -36
  20. package/package.json +18 -14
  21. package/dist/chunk-DCW7NQXH.js.map +0 -1
  22. package/dist/chunk-MOE5S6CE.js.map +0 -1
  23. package/dist/chunk-VH34YC3Q.js.map +0 -1
  24. package/dist/client/index.cjs +0 -16840
  25. package/dist/client/index.cjs.map +0 -1
  26. package/dist/client/index.d.cts +0 -6592
  27. package/dist/server/index.cjs +0 -3878
  28. package/dist/server/index.cjs.map +0 -1
  29. package/dist/server/index.d.cts +0 -354
  30. package/dist/token-registry-snapshot-BNBNuDu1.d.ts +0 -179
  31. package/dist/tokens-namespace-CIWBtgt2.d.ts +0 -3301
  32. package/dist/universal/index.cjs +0 -4028
  33. package/dist/universal/index.cjs.map +0 -1
  34. package/dist/universal/index.d.cts +0 -1276
@@ -4,28 +4,40 @@ import {
4
4
  PINO_LEVEL,
5
5
  SDK_LOGGER_NAME,
6
6
  loggerOverPino,
7
- publicClients
8
- } from "../chunk-DCW7NQXH.js";
7
+ notifySessionExpired,
8
+ publicClients,
9
+ systemTime
10
+ } from "../chunk-QTBEDN7C.js";
9
11
  import {
10
- API_VERSION,
11
12
  ClientCredentialsOAuth,
13
+ OAuthSession
14
+ } from "../chunk-MJ6YXZWH.js";
15
+ import {
16
+ API_VERSION,
17
+ Attributes,
18
+ CentrifugeNamespaceImpl,
12
19
  CoinListTokenSaleNamespaceImpl,
20
+ EpochMillis,
13
21
  Erc20NamespaceImpl,
14
22
  HttpError,
23
+ Millis,
15
24
  NotAuthenticatedError,
16
- OAuthSession,
17
25
  OndoNamespaceImpl,
18
26
  PUBLIC_API_BASE_URL,
19
27
  RequirementsNamespaceImpl,
28
+ Seconds,
20
29
  SuperstateSwapNamespaceImpl,
30
+ TokenTrigger,
21
31
  TokensNamespaceImpl,
22
32
  WalletsNamespaceImpl,
23
33
  fetchOfferDetails,
24
34
  fetchOfferRequirements,
25
35
  fetchOffers,
26
36
  fetchOffersPage,
27
- internalLogger
28
- } from "../chunk-MOE5S6CE.js";
37
+ internalLogger,
38
+ isExpired,
39
+ isRejectedToken
40
+ } from "../chunk-SVPFFLIT.js";
29
41
 
30
42
  // src/server/core/api/api-client.ts
31
43
  var ApiClient = class {
@@ -42,6 +54,107 @@ var ApiClient = class {
42
54
  }
43
55
  };
44
56
 
57
+ // src/server/core/session-refresh.ts
58
+ var VERDICT_MIN_GAP = Millis.of(5e3);
59
+ var REJECTION_MEMORY = Millis.of(6e4);
60
+ var RefreshKey = {
61
+ of: ({
62
+ clientId,
63
+ session
64
+ }) => {
65
+ const identity = session.refreshToken === void 0 ? ["access", session.accessToken.value] : ["refresh", session.refreshToken];
66
+ return JSON.stringify([clientId, ...identity]);
67
+ }
68
+ };
69
+ var RefreshCoordinator = class {
70
+ constructor(env = systemTime) {
71
+ this.env = env;
72
+ this.inFlight = /* @__PURE__ */ new Map();
73
+ this.rejections = /* @__PURE__ */ new Map();
74
+ }
75
+ /**
76
+ * Runs `exchange` for `key` unless one is already in flight, in which case
77
+ * the caller joins it. `joined` tells the caller whether it led: only the
78
+ * leader reports the outcome, so a burst of N produces one verdict, one log
79
+ * line and one `onSessionExpired`.
80
+ */
81
+ refresh(key, exchange) {
82
+ const existing = this.inFlight.get(key);
83
+ if (existing !== void 0) {
84
+ return { outcome: existing, joined: true };
85
+ }
86
+ const outcome = this.run(key, exchange).finally(() => {
87
+ this.inFlight.delete(key);
88
+ });
89
+ this.inFlight.set(key, outcome);
90
+ return { outcome, joined: false };
91
+ }
92
+ async run(key, exchange) {
93
+ this.forgetStale(this.env.time.now());
94
+ if (this.rejections.get(key)?.type === "expired") {
95
+ return { type: "already-expired" };
96
+ }
97
+ const result = await exchange();
98
+ switch (result.type) {
99
+ case "renewed":
100
+ case "unknown":
101
+ return result;
102
+ case "rejected":
103
+ case "unrenewable": {
104
+ const judged = judge({
105
+ result: result.type,
106
+ prior: this.rejections.get(key),
107
+ now: this.env.time.now()
108
+ });
109
+ this.rejections.set(key, judged.memory);
110
+ return { type: judged.outcome };
111
+ }
112
+ }
113
+ }
114
+ forgetStale(now) {
115
+ for (const [key, rejection] of this.rejections) {
116
+ if (EpochMillis.elapsed(rejectedAt(rejection), now) > REJECTION_MEMORY) {
117
+ this.rejections.delete(key);
118
+ }
119
+ }
120
+ }
121
+ };
122
+ function judge({
123
+ result,
124
+ prior,
125
+ now
126
+ }) {
127
+ const verdict = {
128
+ outcome: "expired",
129
+ memory: { type: "expired", at: now }
130
+ };
131
+ switch (result) {
132
+ case "unrenewable":
133
+ return verdict;
134
+ case "rejected":
135
+ switch (prior?.type) {
136
+ case void 0:
137
+ return {
138
+ outcome: "suspected",
139
+ memory: { type: "suspected", since: now }
140
+ };
141
+ case "suspected":
142
+ return EpochMillis.elapsed(prior.since, now) >= VERDICT_MIN_GAP ? verdict : { outcome: "suspected", memory: prior };
143
+ case "expired":
144
+ return { outcome: "expired", memory: prior };
145
+ }
146
+ }
147
+ }
148
+ function rejectedAt(rejection) {
149
+ switch (rejection.type) {
150
+ case "suspected":
151
+ return rejection.since;
152
+ case "expired":
153
+ return rejection.at;
154
+ }
155
+ }
156
+ var processRefreshCoordinator = new RefreshCoordinator();
157
+
45
158
  // src/server/shared/errors.ts
46
159
  var WritableSessionStoreRequiredError = class extends Error {
47
160
  constructor(message = "This operation requires a writable SessionStore. Provide a `setSession` implementation in your SessionStore to persist or clear OAuth sessions.") {
@@ -50,10 +163,7 @@ var WritableSessionStoreRequiredError = class extends Error {
50
163
  }
51
164
  };
52
165
 
53
- // src/server/core/server-auth-namespace.ts
54
- function emptySessionStore() {
55
- return { getSession: async () => null };
56
- }
166
+ // src/server/core/server-auth-namespace-impl.ts
57
167
  var ServerAuthNamespaceImpl = class {
58
168
  constructor(api, config) {
59
169
  this.api = api;
@@ -73,7 +183,8 @@ var ServerAuthNamespaceImpl = class {
73
183
  client_id: this.config.clientId,
74
184
  client_secret: this.config.clientSecret,
75
185
  code_verifier: params.codeVerifier
76
- }
186
+ },
187
+ attributes: Attributes.notRetryable()
77
188
  });
78
189
  const session = OAuthSession.fromDto(sessionDto);
79
190
  await setSession(session);
@@ -81,39 +192,118 @@ var ServerAuthNamespaceImpl = class {
81
192
  });
82
193
  }
83
194
  async getAccessToken() {
195
+ return this.tokenFor(TokenTrigger.beforeRequest);
196
+ }
197
+ /**
198
+ * {@link getAccessToken} with the reason the SDK is asking, which is what
199
+ * the authenticated HTTP pipeline calls: after a 401 the token's own expiry
200
+ * no longer matters, because the API has answered for it.
201
+ */
202
+ async tokenFor(trigger) {
84
203
  return this.log.wrap(
85
204
  "getAccessToken",
86
205
  void 0,
87
- () => this.readAccessToken()
206
+ () => this.readAccessToken(trigger)
88
207
  );
89
208
  }
90
- async readAccessToken() {
209
+ async readAccessToken(trigger) {
91
210
  const sessionStore = this.config.sessionStore;
92
- const session = await sessionStore.getSession();
93
- if (session == null) return null;
94
- const now = Date.now();
95
- const expiresAt = session.accessToken.expiresAt.getTime();
96
- const bufferMs = this.config.accessTokenExpiryBufferSeconds * 1e3;
97
- if (expiresAt > now + bufferMs) {
98
- return session.accessToken;
99
- }
100
- const setSession = sessionStore.setSession?.bind(sessionStore);
101
- if (!setSession) {
102
- this.log.child({ op: "getAccessToken" }).warn(() => ({
103
- msg: "serving an expired token: the session store is read-only, so it cannot be refreshed"
104
- }));
105
- return session.accessToken;
211
+ const classification = classifyAccessToken({
212
+ session: await sessionStore.getSession(),
213
+ store: sessionStoreAccess(sessionStore),
214
+ trigger,
215
+ now: this.config.time.now(),
216
+ buffer: Seconds.toMillis(this.config.accessTokenExpiryBufferSeconds)
217
+ });
218
+ switch (classification.type) {
219
+ case "no-session":
220
+ case "refuse":
221
+ return null;
222
+ case "serve":
223
+ return classification.token;
224
+ case "serve-stale":
225
+ this.log.child({ op: "getAccessToken" }).warn(() => ({
226
+ msg: "serving an expired token: the session store is read-only, so it cannot be refreshed"
227
+ }));
228
+ return classification.token;
229
+ case "refresh":
230
+ return this.refresh(classification);
106
231
  }
107
- return this.refreshSession(session.refreshToken, setSession);
108
232
  }
109
- async refreshSession(refreshToken, setSession) {
233
+ /**
234
+ * Refreshes through the process-wide coordinator, then acts on the outcome
235
+ * for this caller's own store. The store is written before anything is
236
+ * logged or the host is notified, so `onSessionExpired` always finds it
237
+ * cleared.
238
+ */
239
+ async refresh({
240
+ session,
241
+ setSession,
242
+ fallback
243
+ }) {
110
244
  const log = this.log.child({ op: "refresh" });
111
- if (!refreshToken) {
112
- log.warn(() => ({
113
- msg: "clearing the session: it carries no refresh token"
114
- }));
115
- await setSession(null);
116
- return null;
245
+ const key = RefreshKey.of({ clientId: this.config.clientId, session });
246
+ const { outcome, joined } = this.config.refreshes.refresh(
247
+ key,
248
+ () => this.exchange(session.refreshToken)
249
+ );
250
+ if (joined) {
251
+ log.debug(() => ({ msg: "joined a refresh already in flight" }));
252
+ }
253
+ const result = await outcome;
254
+ switch (result.type) {
255
+ case "renewed":
256
+ await setSession(result.session);
257
+ if (!joined) {
258
+ log.info(() => ({ msg: "session renewed" }));
259
+ }
260
+ return result.session.accessToken;
261
+ case "suspected":
262
+ if (!joined) {
263
+ log.warn(() => ({
264
+ msg: "refresh rejected; not clearing until it is confirmed"
265
+ }));
266
+ }
267
+ return fallback;
268
+ case "expired":
269
+ await setSession(null);
270
+ if (!joined) {
271
+ log.info(() => ({
272
+ msg: "session expired",
273
+ fields: {
274
+ reason: session.refreshToken === void 0 ? "no-refresh-token" : "refresh-rejected"
275
+ }
276
+ }));
277
+ notifySessionExpired(this.config.onSessionExpired, log);
278
+ }
279
+ return null;
280
+ case "already-expired":
281
+ await setSession(null);
282
+ return null;
283
+ case "unknown":
284
+ if (fallback === null) {
285
+ throw result.error;
286
+ }
287
+ if (!joined) {
288
+ log.warning(
289
+ () => ({ msg: "refresh failed; serving the current token" }),
290
+ result.error
291
+ );
292
+ }
293
+ return fallback;
294
+ }
295
+ }
296
+ /**
297
+ * One `POST /oauth/token`, total: every failure comes back as a value, so
298
+ * the coordinator can judge it once for everyone who joined.
299
+ *
300
+ * Never retried. The refresh token is single-use, so a retry of an attempt
301
+ * that reached the server would present a token already rotated away, and
302
+ * its `invalid_grant` would be judged as a rejection of the session.
303
+ */
304
+ async exchange(refreshToken) {
305
+ if (refreshToken === void 0) {
306
+ return { type: "unrenewable" };
117
307
  }
118
308
  try {
119
309
  const sessionDto = await this.api.send({
@@ -124,19 +314,12 @@ var ServerAuthNamespaceImpl = class {
124
314
  refresh_token: refreshToken,
125
315
  client_id: this.config.clientId,
126
316
  client_secret: this.config.clientSecret
127
- }
317
+ },
318
+ attributes: Attributes.notRetryable()
128
319
  });
129
- const newSession = OAuthSession.fromDto(sessionDto);
130
- await setSession(newSession);
131
- log.info(() => ({ msg: "session renewed" }));
132
- return newSession.accessToken;
320
+ return { type: "renewed", session: OAuthSession.fromDto(sessionDto) };
133
321
  } catch (error) {
134
- log.failure(
135
- () => ({ msg: "refresh failed; clearing the session" }),
136
- error
137
- );
138
- await setSession(null);
139
- return null;
322
+ return isInvalidGrant(error) ? { type: "rejected" } : { type: "unknown", error };
140
323
  }
141
324
  }
142
325
  async clientCredentials() {
@@ -172,28 +355,102 @@ var ServerAuthNamespaceImpl = class {
172
355
  }
173
356
  });
174
357
  } catch (err) {
175
- if (!(err instanceof HttpError) || this.config.strict) {
358
+ if (this.config.strict) {
176
359
  throw err;
177
360
  }
178
- this.log.child({ op: "logout" }).warn(() => ({
179
- msg: "the token was not revoked upstream; the local session is cleared regardless"
180
- }));
361
+ this.log.child({ op: "logout" }).warning(
362
+ () => ({ msg: "revoke failed; local session cleared regardless" }),
363
+ err
364
+ );
181
365
  }
182
366
  await setSession(null);
183
367
  }
184
368
  /**
185
- * Returns the store's `setSession`, or throws — the guard every
369
+ * Returns the store's `setSession`, or throws - the guard every
186
370
  * session-writing operation shares.
187
371
  */
188
372
  writableSessionStore() {
189
- const sessionStore = this.config.sessionStore;
190
- const setSession = sessionStore.setSession?.bind(sessionStore);
191
- if (!setSession) {
192
- throw new WritableSessionStoreRequiredError();
373
+ const store = sessionStoreAccess(this.config.sessionStore);
374
+ switch (store.type) {
375
+ case "writable":
376
+ return store.setSession;
377
+ case "read-only":
378
+ throw new WritableSessionStoreRequiredError();
193
379
  }
194
- return setSession;
195
380
  }
196
381
  };
382
+ function classifyAccessToken({
383
+ session,
384
+ store,
385
+ trigger,
386
+ now,
387
+ buffer
388
+ }) {
389
+ if (session === null) {
390
+ return { type: "no-session" };
391
+ }
392
+ const token = session.accessToken;
393
+ const stale = isExpired({
394
+ expiresAt: EpochMillis.fromDate(token.expiresAt),
395
+ now,
396
+ buffer
397
+ });
398
+ switch (store.type) {
399
+ case "read-only":
400
+ return classifyReadOnly(token, trigger, stale);
401
+ case "writable": {
402
+ if (!needsRefresh(token, trigger, stale)) {
403
+ return { type: "serve", token };
404
+ }
405
+ const fallback = fallbackToken(token, trigger, now);
406
+ return session.refreshToken === void 0 && fallback !== null ? { type: "serve", token: fallback } : {
407
+ type: "refresh",
408
+ session,
409
+ setSession: store.setSession,
410
+ fallback
411
+ };
412
+ }
413
+ }
414
+ }
415
+ function classifyReadOnly(token, trigger, stale) {
416
+ switch (trigger.type) {
417
+ case "after-401":
418
+ return isRejectedToken(token, trigger.rejected) ? { type: "refuse" } : { type: "serve", token };
419
+ case "before-request":
420
+ return stale ? { type: "serve-stale", token } : { type: "serve", token };
421
+ }
422
+ }
423
+ function sessionStoreAccess(sessionStore) {
424
+ const setSession = sessionStore.setSession?.bind(sessionStore);
425
+ return setSession === void 0 ? { type: "read-only" } : { type: "writable", setSession };
426
+ }
427
+ function needsRefresh(token, trigger, stale) {
428
+ switch (trigger.type) {
429
+ case "before-request":
430
+ return stale;
431
+ case "after-401":
432
+ return trigger.rejected.type === "no-user-token" || isRejectedToken(token, trigger.rejected) || stale;
433
+ }
434
+ }
435
+ function fallbackToken(token, trigger, now) {
436
+ const unexpired = !isExpired({
437
+ expiresAt: EpochMillis.fromDate(token.expiresAt),
438
+ now
439
+ });
440
+ switch (trigger.type) {
441
+ case "after-401":
442
+ return unexpired && !isRejectedToken(token, trigger.rejected) ? token : null;
443
+ case "before-request":
444
+ return unexpired ? token : null;
445
+ }
446
+ }
447
+ function isInvalidGrant(error) {
448
+ if (!(error instanceof HttpError) || error.response.status !== 400) {
449
+ return false;
450
+ }
451
+ const body = error.response.body;
452
+ return typeof body === "object" && body !== null && "error" in body && body.error === "invalid_grant";
453
+ }
197
454
 
198
455
  // src/server/core/server-offers-namespace.ts
199
456
  var ServerOffersNamespaceImpl = class {
@@ -240,28 +497,26 @@ var ServerRequirementsNamespaceImpl = class extends RequirementsNamespaceImpl {
240
497
  };
241
498
 
242
499
  // src/server/core/coinlist-server.ts
243
- var ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;
500
+ var ACCESS_TOKEN_EXPIRY_BUFFER = Seconds.of(60);
244
501
  var CoinListServerImpl = class {
245
502
  constructor(_config) {
246
- this._config = _config;
247
503
  const logger = _config.logger ?? null;
248
504
  this.api = new ApiClient(
249
505
  {
250
506
  baseUrl: _config.baseUrl ?? PUBLIC_API_BASE_URL,
251
507
  xApiVersion: API_VERSION
252
508
  },
253
- // When refresh=true the renewal middleware has received a 401 and wants a
254
- // fresh token. A read-only store cannot persist a new session, so return
255
- // null immediately — this tells the middleware to skip the retry rather
256
- // than re-sending with the same expired token and wasting a round-trip.
257
- (refresh) => refresh && !this._config.sessionStore.setSession ? Promise.resolve(null) : this.auth.getAccessToken(),
509
+ (trigger) => auth.tokenFor(trigger),
258
510
  logger
259
511
  );
260
- this.auth = new ServerAuthNamespaceImpl(this.api, {
512
+ const auth = new ServerAuthNamespaceImpl(this.api, {
261
513
  ..._config,
262
- accessTokenExpiryBufferSeconds: _config.accessTokenExpiryBufferSeconds ?? ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS,
263
- strict: _config.strict ?? false
514
+ accessTokenExpiryBufferSeconds: _config.accessTokenExpiryBufferSeconds ?? ACCESS_TOKEN_EXPIRY_BUFFER,
515
+ strict: _config.strict ?? false,
516
+ refreshes: processRefreshCoordinator,
517
+ ...systemTime
264
518
  });
519
+ this.auth = auth;
265
520
  const ctx = {
266
521
  api: this.api,
267
522
  rpc: publicClients(this.api, _config.rpc, _config.logger ?? null),
@@ -276,6 +531,7 @@ var CoinListServerImpl = class {
276
531
  this.tokenSale = new CoinListTokenSaleNamespaceImpl(ctx);
277
532
  this.superstate = new SuperstateSwapNamespaceImpl(ctx);
278
533
  this.ondo = new OndoNamespaceImpl(ctx);
534
+ this.centrifuge = new CentrifugeNamespaceImpl(ctx);
279
535
  this.tokens = new TokensNamespaceImpl(
280
536
  _config.tokensBaseUrl ?? NABU_BASE_URL,
281
537
  logger
@@ -311,8 +567,12 @@ function pinoServerLogger(options) {
311
567
  options.level
312
568
  );
313
569
  }
570
+
571
+ // src/server/core/server-auth-namespace.ts
572
+ function emptySessionStore() {
573
+ return { getSession: async () => null };
574
+ }
314
575
  export {
315
- ServerAuthNamespaceImpl,
316
576
  ServerOffersNamespaceImpl,
317
577
  ServerRequirementsNamespaceImpl,
318
578
  WritableSessionStoreRequiredError,