@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.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -2
- package/dist/browser/index.mjs +100 -53
- package/dist/browser/index.mjs.map +1 -1
- package/dist/index.mjs +100 -53
- package/dist/index.mjs.map +1 -1
- package/dist/node/index.cjs +100 -53
- package/dist/node/index.cjs.map +1 -1
- package/dist/node/index.mjs +100 -53
- package/dist/node/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/node/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1512
|
+
let outcome;
|
|
1476
1513
|
try {
|
|
1477
|
-
|
|
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);
|