@acosmi/sdk-ts 2.20.0 → 2.20.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.
@@ -1097,6 +1097,18 @@ var abort = (signal) => {
1097
1097
  if (signal?.aborted) throw new Error("aborted");
1098
1098
  };
1099
1099
  var sameRequestOwner = (snapshot, expected) => snapshot.storeInstanceId === expected.storeInstanceId && snapshot.authSessionId === expected.authSessionId && snapshot.principal?.issuer === expected.principal?.issuer && snapshot.principal?.subject === expected.principal?.subject && snapshot.principal?.organizationId === expected.principal?.organizationId;
1100
+ var REFRESH_TEMPORARILY_UNAVAILABLE = "refresh_temporarily_unavailable";
1101
+ var REFRESH_RETRY_BASE_MS = 5e3;
1102
+ var REFRESH_RETRY_MAX_MS = 6e4;
1103
+ var REFRESH_RETRY_AFTER_CAP_MS = 3e5;
1104
+ function retryAfterMs(response) {
1105
+ const raw = response.headers.get("retry-after");
1106
+ if (!raw) return null;
1107
+ const seconds = Number(raw);
1108
+ const ms = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - Date.now();
1109
+ if (!Number.isFinite(ms)) return null;
1110
+ return Math.min(Math.max(ms, 0), REFRESH_RETRY_AFTER_CAP_MS);
1111
+ }
1100
1112
  var CredentialLifecycle = class {
1101
1113
  constructor(store, protocol) {
1102
1114
  this.store = store;
@@ -1107,6 +1119,8 @@ var CredentialLifecycle = class {
1107
1119
  listeners = /* @__PURE__ */ new Set();
1108
1120
  notificationKey = "";
1109
1121
  observations = /* @__PURE__ */ new Map();
1122
+ /** Last refusal of a refresh token that was not rotated; see `REFRESH_RETRY_BASE_MS`. */
1123
+ refreshDeferral = null;
1110
1124
  read(signal) {
1111
1125
  return this.store.readSnapshot(signal);
1112
1126
  }
@@ -1296,6 +1310,19 @@ var CredentialLifecycle = class {
1296
1310
  usable(s) {
1297
1311
  return !!s.tokenSet && Date.parse(s.tokenSet.expires_at) > Date.now() + 3e5;
1298
1312
  }
1313
+ refreshDeferred(s) {
1314
+ const d = this.refreshDeferral;
1315
+ return !!d && d.refreshToken === s.tokenSet?.refresh_token && Date.now() < d.until;
1316
+ }
1317
+ deferRefresh(refreshToken2, retryAfter) {
1318
+ const failures = this.refreshDeferral?.refreshToken === refreshToken2 ? this.refreshDeferral.failures + 1 : 1;
1319
+ const backoff = Math.min(REFRESH_RETRY_MAX_MS, REFRESH_RETRY_BASE_MS * 2 ** (failures - 1));
1320
+ this.refreshDeferral = {
1321
+ refreshToken: refreshToken2,
1322
+ failures,
1323
+ until: Date.now() + Math.max(backoff, retryAfter ?? 0)
1324
+ };
1325
+ }
1299
1326
  async bindIdentity(sessionId, signal) {
1300
1327
  const s = await this.ensureSnapshot(signal, void 0, true);
1301
1328
  if (s.authSessionId !== sessionId) throw new Error("superseded");
@@ -1409,6 +1436,10 @@ var CredentialLifecycle = class {
1409
1436
  if (live && (refreshed || s.revision !== (rejected?.revision ?? initialRevision) && (rejected !== void 0 || s.tokenSet.access_token !== initialAccess)))
1410
1437
  return s;
1411
1438
  if (this.usable(s) && !rejected) return s;
1439
+ if (this.refreshDeferred(s)) {
1440
+ if (live && !rejected) return s;
1441
+ throw new Error(REFRESH_TEMPORARILY_UNAVAILABLE);
1442
+ }
1412
1443
  const metadata = await this.metadata(s, signal);
1413
1444
  abort(signal);
1414
1445
  const operationId = uuid();
@@ -1431,7 +1462,13 @@ var CredentialLifecycle = class {
1431
1462
  );
1432
1463
  if (r.status !== "committed") continue;
1433
1464
  const owner = this.refresh(r.snapshot, metadata);
1434
- await this.wait(owner, signal);
1465
+ try {
1466
+ await this.wait(owner, signal);
1467
+ } catch (error) {
1468
+ const deferred = error instanceof Error && error.message === REFRESH_TEMPORARILY_UNAVAILABLE;
1469
+ if (rejected || !deferred) throw error;
1470
+ continue;
1471
+ }
1435
1472
  refreshed = true;
1436
1473
  rejected = void 0;
1437
1474
  }
@@ -1472,59 +1509,9 @@ var CredentialLifecycle = class {
1472
1509
  const s = r.snapshot;
1473
1510
  const ctl = new AbortController();
1474
1511
  const timeout = setTimeout(() => ctl.abort(), 3e4);
1475
- let tokens;
1512
+ let outcome;
1476
1513
  try {
1477
- const result = await this.wait(
1478
- (async () => {
1479
- const response = await this.protocol.fetch(metadata.token_endpoint, {
1480
- method: "POST",
1481
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1482
- body: new URLSearchParams({
1483
- grant_type: "refresh_token",
1484
- client_id: s.tokenSet.client_id,
1485
- refresh_token: s.tokenSet.refresh_token
1486
- }),
1487
- signal: ctl.signal
1488
- });
1489
- const body2 = await response.json();
1490
- return { response, body: body2 };
1491
- })(),
1492
- ctl.signal
1493
- );
1494
- if (!result.response.ok) {
1495
- const code = result.body.error;
1496
- let reason = "refresh_outcome_unknown";
1497
- let state = "reauth_required";
1498
- if (code === "invalid_grant") reason = code;
1499
- else if (["invalid_client", "invalid_scope", "unsupported_grant_type"].includes(code)) {
1500
- reason = code;
1501
- state = "configuration_error";
1502
- } else if (result.body.rotationOutcome === "not_committed" && ["temporarily_unavailable", "server_error"].includes(code)) {
1503
- await this.commit(s, {
1504
- credentialState: s.refreshOperation.returnState,
1505
- refreshOperation: null
1506
- });
1507
- throw new Error("refresh_temporarily_unavailable");
1508
- }
1509
- await this.commit(s, {
1510
- credentialState: state,
1511
- tokenSet: null,
1512
- refreshOperation: null,
1513
- reason
1514
- });
1515
- throw new Error(reason);
1516
- }
1517
- const body = result.body;
1518
- if (!Number.isFinite(body.expires_in) || body.expires_in <= 0)
1519
- throw new Error("invalid_response");
1520
- tokens = {
1521
- ...s.tokenSet,
1522
- access_token: body.access_token,
1523
- refresh_token: body.refresh_token,
1524
- expires_at: new Date(Date.now() + body.expires_in * 1e3).toISOString(),
1525
- scope: body.scope ?? s.tokenSet.scope
1526
- };
1527
- this.validateTokens(tokens);
1514
+ outcome = await this.wait(this.exchange(metadata, s.tokenSet, ctl.signal), ctl.signal);
1528
1515
  } catch (error) {
1529
1516
  await this.commit(s, {
1530
1517
  credentialState: "reauth_required",
@@ -1536,6 +1523,24 @@ var CredentialLifecycle = class {
1536
1523
  } finally {
1537
1524
  clearTimeout(timeout);
1538
1525
  }
1526
+ if (outcome.kind === "not_committed") {
1527
+ this.deferRefresh(s.tokenSet.refresh_token, outcome.retryAfterMs);
1528
+ await this.commit(s, {
1529
+ credentialState: s.refreshOperation.returnState,
1530
+ refreshOperation: null
1531
+ });
1532
+ throw new Error(REFRESH_TEMPORARILY_UNAVAILABLE);
1533
+ }
1534
+ if (outcome.kind === "rejected") {
1535
+ await this.commit(s, {
1536
+ credentialState: outcome.state,
1537
+ tokenSet: null,
1538
+ refreshOperation: null,
1539
+ reason: outcome.reason
1540
+ });
1541
+ throw new Error(outcome.reason);
1542
+ }
1543
+ const tokens = outcome.tokens;
1539
1544
  const started = performance.now(), mutationId = uuid();
1540
1545
  for (; ; ) {
1541
1546
  if (Date.now() >= Date.parse(s.refreshOperation.deadlineAt)) {
@@ -1558,6 +1563,7 @@ var CredentialLifecycle = class {
1558
1563
  mutationId
1559
1564
  );
1560
1565
  if (result.status === "committed") {
1566
+ this.refreshDeferral = null;
1561
1567
  await this.notify(result.snapshot);
1562
1568
  return;
1563
1569
  }
@@ -1569,6 +1575,47 @@ var CredentialLifecycle = class {
1569
1575
  await pause();
1570
1576
  }
1571
1577
  }
1578
+ /**
1579
+ * One refresh-token exchange. Settles with what the authority said; rejects only when there is
1580
+ * no answer or the answer cannot be read, which leaves the rotation outcome unknown.
1581
+ */
1582
+ async exchange(metadata, current, signal) {
1583
+ const response = await this.protocol.fetch(metadata.token_endpoint, {
1584
+ method: "POST",
1585
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1586
+ body: new URLSearchParams({
1587
+ grant_type: "refresh_token",
1588
+ client_id: current.client_id,
1589
+ refresh_token: current.refresh_token
1590
+ }),
1591
+ signal
1592
+ });
1593
+ if (response.status === 429)
1594
+ return { kind: "not_committed", retryAfterMs: retryAfterMs(response) };
1595
+ const body = await response.json();
1596
+ if (!response.ok) {
1597
+ const code = body.error;
1598
+ if (code === "invalid_grant")
1599
+ return { kind: "rejected", state: "reauth_required", reason: code };
1600
+ if (["invalid_client", "invalid_scope", "unsupported_grant_type"].includes(code))
1601
+ return { kind: "rejected", state: "configuration_error", reason: code };
1602
+ if (body.rotationOutcome === "not_committed" && ["temporarily_unavailable", "server_error"].includes(code))
1603
+ return { kind: "not_committed", retryAfterMs: retryAfterMs(response) };
1604
+ return { kind: "rejected", state: "reauth_required", reason: "refresh_outcome_unknown" };
1605
+ }
1606
+ const tokenResponse = body;
1607
+ if (!Number.isFinite(tokenResponse.expires_in) || tokenResponse.expires_in <= 0)
1608
+ throw new Error("invalid_response");
1609
+ const tokens = {
1610
+ ...current,
1611
+ access_token: tokenResponse.access_token,
1612
+ refresh_token: tokenResponse.refresh_token,
1613
+ expires_at: new Date(Date.now() + tokenResponse.expires_in * 1e3).toISOString(),
1614
+ scope: tokenResponse.scope ?? current.scope
1615
+ };
1616
+ this.validateTokens(tokens);
1617
+ return { kind: "tokens", tokens };
1618
+ }
1572
1619
  async revoke(tokens, metadata, authority) {
1573
1620
  const ctl = new AbortController();
1574
1621
  const timer = setTimeout(() => ctl.abort(), 3e4);