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

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 (33) hide show
  1. package/dist/{chunk-DCW7NQXH.js → chunk-4IZ2F732.js} +52 -19
  2. package/dist/chunk-4IZ2F732.js.map +1 -0
  3. package/dist/{chunk-MOE5S6CE.js → chunk-NBO5E2RO.js} +152 -6
  4. package/dist/chunk-NBO5E2RO.js.map +1 -0
  5. package/dist/{chunk-VH34YC3Q.js → chunk-UJYSXXJF.js} +168 -3
  6. package/dist/chunk-UJYSXXJF.js.map +1 -0
  7. package/dist/client/index.cjs +469 -145
  8. package/dist/client/index.cjs.map +1 -1
  9. package/dist/client/index.d.cts +114 -35
  10. package/dist/client/index.d.ts +114 -35
  11. package/dist/client/index.js +297 -114
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/server/index.cjs +507 -116
  14. package/dist/server/index.cjs.map +1 -1
  15. package/dist/server/index.d.cts +65 -41
  16. package/dist/server/index.d.ts +65 -41
  17. package/dist/server/index.js +321 -65
  18. package/dist/server/index.js.map +1 -1
  19. package/dist/time-CFocgYyl.d.cts +50 -0
  20. package/dist/time-DvgN98Q9.d.ts +50 -0
  21. package/dist/{token-registry-snapshot-BNBNuDu1.d.ts → token-registry-snapshot-BsAfBL_b.d.ts} +9 -7
  22. package/dist/{token-registry-snapshot-CqI6dXM_.d.cts → token-registry-snapshot-Cc_eA5zM.d.cts} +9 -7
  23. package/dist/{tokens-namespace-CIWBtgt2.d.cts → tokens-namespace-D_XSpiS-.d.cts} +154 -19
  24. package/dist/{tokens-namespace-CIWBtgt2.d.ts → tokens-namespace-D_XSpiS-.d.ts} +154 -19
  25. package/dist/universal/index.cjs +333 -40
  26. package/dist/universal/index.cjs.map +1 -1
  27. package/dist/universal/index.d.cts +358 -5
  28. package/dist/universal/index.d.ts +358 -5
  29. package/dist/universal/index.js +28 -2
  30. package/package.json +1 -1
  31. package/dist/chunk-DCW7NQXH.js.map +0 -1
  32. package/dist/chunk-MOE5S6CE.js.map +0 -1
  33. package/dist/chunk-VH34YC3Q.js.map +0 -1
@@ -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-4IZ2F732.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-NBO5E2RO.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() {
@@ -172,28 +352,102 @@ var ServerAuthNamespaceImpl = class {
172
352
  }
173
353
  });
174
354
  } catch (err) {
175
- if (!(err instanceof HttpError) || this.config.strict) {
355
+ if (this.config.strict) {
176
356
  throw err;
177
357
  }
178
- this.log.child({ op: "logout" }).warn(() => ({
179
- msg: "the token was not revoked upstream; the local session is cleared regardless"
180
- }));
358
+ this.log.child({ op: "logout" }).warning(
359
+ () => ({ msg: "revoke failed; local session cleared regardless" }),
360
+ err
361
+ );
181
362
  }
182
363
  await setSession(null);
183
364
  }
184
365
  /**
185
- * Returns the store's `setSession`, or throws — the guard every
366
+ * Returns the store's `setSession`, or throws - the guard every
186
367
  * session-writing operation shares.
187
368
  */
188
369
  writableSessionStore() {
189
- const sessionStore = this.config.sessionStore;
190
- const setSession = sessionStore.setSession?.bind(sessionStore);
191
- if (!setSession) {
192
- throw new WritableSessionStoreRequiredError();
370
+ const store = sessionStoreAccess(this.config.sessionStore);
371
+ switch (store.type) {
372
+ case "writable":
373
+ return store.setSession;
374
+ case "read-only":
375
+ throw new WritableSessionStoreRequiredError();
193
376
  }
194
- return setSession;
195
377
  }
196
378
  };
379
+ function classifyAccessToken({
380
+ session,
381
+ store,
382
+ trigger,
383
+ now,
384
+ buffer
385
+ }) {
386
+ if (session === null) {
387
+ return { type: "no-session" };
388
+ }
389
+ const token = session.accessToken;
390
+ const stale = isExpired({
391
+ expiresAt: EpochMillis.fromDate(token.expiresAt),
392
+ now,
393
+ buffer
394
+ });
395
+ switch (store.type) {
396
+ case "read-only":
397
+ return classifyReadOnly(token, trigger, stale);
398
+ case "writable": {
399
+ if (!needsRefresh(token, trigger, stale)) {
400
+ return { type: "serve", token };
401
+ }
402
+ const fallback = fallbackToken(token, trigger, now);
403
+ return session.refreshToken === void 0 && fallback !== null ? { type: "serve", token: fallback } : {
404
+ type: "refresh",
405
+ session,
406
+ setSession: store.setSession,
407
+ fallback
408
+ };
409
+ }
410
+ }
411
+ }
412
+ function classifyReadOnly(token, trigger, stale) {
413
+ switch (trigger.type) {
414
+ case "after-401":
415
+ return isRejectedToken(token, trigger.rejected) ? { type: "refuse" } : { type: "serve", token };
416
+ case "before-request":
417
+ return stale ? { type: "serve-stale", token } : { type: "serve", token };
418
+ }
419
+ }
420
+ function sessionStoreAccess(sessionStore) {
421
+ const setSession = sessionStore.setSession?.bind(sessionStore);
422
+ return setSession === void 0 ? { type: "read-only" } : { type: "writable", setSession };
423
+ }
424
+ function needsRefresh(token, trigger, stale) {
425
+ switch (trigger.type) {
426
+ case "before-request":
427
+ return stale;
428
+ case "after-401":
429
+ return trigger.rejected.type === "no-user-token" || isRejectedToken(token, trigger.rejected) || stale;
430
+ }
431
+ }
432
+ function fallbackToken(token, trigger, now) {
433
+ const unexpired = !isExpired({
434
+ expiresAt: EpochMillis.fromDate(token.expiresAt),
435
+ now
436
+ });
437
+ switch (trigger.type) {
438
+ case "after-401":
439
+ return unexpired && !isRejectedToken(token, trigger.rejected) ? token : null;
440
+ case "before-request":
441
+ return unexpired ? token : null;
442
+ }
443
+ }
444
+ function isInvalidGrant(error) {
445
+ if (!(error instanceof HttpError) || error.response.status !== 400) {
446
+ return false;
447
+ }
448
+ const body = error.response.body;
449
+ return typeof body === "object" && body !== null && "error" in body && body.error === "invalid_grant";
450
+ }
197
451
 
198
452
  // src/server/core/server-offers-namespace.ts
199
453
  var ServerOffersNamespaceImpl = class {
@@ -240,28 +494,26 @@ var ServerRequirementsNamespaceImpl = class extends RequirementsNamespaceImpl {
240
494
  };
241
495
 
242
496
  // src/server/core/coinlist-server.ts
243
- var ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS = 60;
497
+ var ACCESS_TOKEN_EXPIRY_BUFFER = Seconds.of(60);
244
498
  var CoinListServerImpl = class {
245
499
  constructor(_config) {
246
- this._config = _config;
247
500
  const logger = _config.logger ?? null;
248
501
  this.api = new ApiClient(
249
502
  {
250
503
  baseUrl: _config.baseUrl ?? PUBLIC_API_BASE_URL,
251
504
  xApiVersion: API_VERSION
252
505
  },
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(),
506
+ (trigger) => auth.tokenFor(trigger),
258
507
  logger
259
508
  );
260
- this.auth = new ServerAuthNamespaceImpl(this.api, {
509
+ const auth = new ServerAuthNamespaceImpl(this.api, {
261
510
  ..._config,
262
- accessTokenExpiryBufferSeconds: _config.accessTokenExpiryBufferSeconds ?? ACCESS_TOKEN_EXPIRY_BUFFER_SECONDS,
263
- strict: _config.strict ?? false
511
+ accessTokenExpiryBufferSeconds: _config.accessTokenExpiryBufferSeconds ?? ACCESS_TOKEN_EXPIRY_BUFFER,
512
+ strict: _config.strict ?? false,
513
+ refreshes: processRefreshCoordinator,
514
+ ...systemTime
264
515
  });
516
+ this.auth = auth;
265
517
  const ctx = {
266
518
  api: this.api,
267
519
  rpc: publicClients(this.api, _config.rpc, _config.logger ?? null),
@@ -311,8 +563,12 @@ function pinoServerLogger(options) {
311
563
  options.level
312
564
  );
313
565
  }
566
+
567
+ // src/server/core/server-auth-namespace.ts
568
+ function emptySessionStore() {
569
+ return { getSession: async () => null };
570
+ }
314
571
  export {
315
- ServerAuthNamespaceImpl,
316
572
  ServerOffersNamespaceImpl,
317
573
  ServerRequirementsNamespaceImpl,
318
574
  WritableSessionStoreRequiredError,