@coinlist-co/react 0.12.2-rc.7b430dd → 0.12.2-rc.9fb6364

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