@coinlist-co/react 0.12.2-rc.7b430dd → 0.12.2-rc.8a0d02a

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