@ractive-ch/tab-mcp 0.5.1 → 0.6.0
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/README.md +3 -3
- package/index.js +747 -169
- package/index.js.map +11 -9
- package/jwt.d.ts +10 -2
- package/main.js +747 -169
- package/main.js.map +11 -9
- package/oauth/cimd.d.ts +49 -4
- package/oauth/client-auth.d.ts +86 -0
- package/oauth/domain-cache.d.ts +24 -0
- package/oauth/server.d.ts +1 -0
- package/package.json +2 -2
- package/store/memory.d.ts +13 -0
- package/store/sqlite.d.ts +2 -0
- package/store/types.d.ts +17 -0
- package/testing.js +34 -1
- package/testing.js.map +3 -3
- package/tokens.d.ts +4 -1
package/index.js
CHANGED
|
@@ -462,6 +462,7 @@ var MAX_JWT_BYTES = 8192;
|
|
|
462
462
|
var MAX_JWKS_BYTES = 65536;
|
|
463
463
|
var JWKS_TTL_MS = 10 * 60000;
|
|
464
464
|
var JWKS_REFETCH_MS = 30000;
|
|
465
|
+
var UNKNOWN_KID_REFETCH_MS = 5000;
|
|
465
466
|
var JWKS_GRACE_MS = 60 * 60000;
|
|
466
467
|
var FETCH_TIMEOUT_MS = 5000;
|
|
467
468
|
|
|
@@ -626,6 +627,7 @@ class HttpJsonError extends Error {
|
|
|
626
627
|
class JwksCache {
|
|
627
628
|
#url;
|
|
628
629
|
#now;
|
|
630
|
+
#load;
|
|
629
631
|
#keys = [];
|
|
630
632
|
#fetchedAt = 0;
|
|
631
633
|
#attemptedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -634,14 +636,17 @@ class JwksCache {
|
|
|
634
636
|
constructor(url, options = {}) {
|
|
635
637
|
this.#url = typeof url === "string" ? () => Promise.resolve(url) : url;
|
|
636
638
|
this.#now = options.now ?? Date.now;
|
|
639
|
+
this.#load = options.load ?? ((u) => fetchJson(u));
|
|
637
640
|
}
|
|
638
641
|
async keys(kid) {
|
|
642
|
+
const entered = this.#now();
|
|
639
643
|
if (this.#inflight !== undefined)
|
|
640
644
|
await this.#inflight.catch(() => {});
|
|
641
645
|
const now = this.#now();
|
|
642
646
|
const stale = now - this.#fetchedAt >= JWKS_TTL_MS;
|
|
643
|
-
const unknown = kid !== undefined && !this.#
|
|
644
|
-
|
|
647
|
+
const unknown = kid !== undefined && !this.#has(kid);
|
|
648
|
+
const floor = unknown && this.#lastError === undefined ? UNKNOWN_KID_REFETCH_MS : JWKS_REFETCH_MS;
|
|
649
|
+
if ((stale || unknown) && now - this.#attemptedAt >= floor) {
|
|
645
650
|
try {
|
|
646
651
|
await this.#refresh();
|
|
647
652
|
} catch (error) {
|
|
@@ -651,13 +656,21 @@ class JwksCache {
|
|
|
651
656
|
if (this.#now() - this.#fetchedAt >= JWKS_TTL_MS + JWKS_GRACE_MS) {
|
|
652
657
|
throw this.#lastError ?? new Error("the JWKS could not be loaded");
|
|
653
658
|
}
|
|
654
|
-
|
|
659
|
+
if (kid === undefined)
|
|
660
|
+
return this.#keys;
|
|
661
|
+
if (!this.#has(kid) && this.#fetchedAt < entered) {
|
|
662
|
+
throw this.#lastError ?? new Error("an unknown key id; the JWKS was fetched too recently");
|
|
663
|
+
}
|
|
664
|
+
return this.#keys.filter((k) => k.kid === kid);
|
|
665
|
+
}
|
|
666
|
+
#has(kid) {
|
|
667
|
+
return this.#keys.some((k) => k.kid === kid);
|
|
655
668
|
}
|
|
656
669
|
#refresh() {
|
|
657
670
|
this.#inflight ??= (async () => {
|
|
658
671
|
this.#attemptedAt = this.#now();
|
|
659
672
|
try {
|
|
660
|
-
const doc = await
|
|
673
|
+
const doc = await this.#load(await this.#url());
|
|
661
674
|
const keys = doc?.keys;
|
|
662
675
|
if (!Array.isArray(keys))
|
|
663
676
|
throw new Error("a JWKS needs keys");
|
|
@@ -1205,7 +1218,7 @@ function parseTabMessage(text) {
|
|
|
1205
1218
|
}
|
|
1206
1219
|
}
|
|
1207
1220
|
// package.json
|
|
1208
|
-
var version = "0.
|
|
1221
|
+
var version = "0.6.0";
|
|
1209
1222
|
|
|
1210
1223
|
// src/exchange.ts
|
|
1211
1224
|
var RelayErrorCode = {
|
|
@@ -1682,6 +1695,142 @@ class RateLimiter {
|
|
|
1682
1695
|
}
|
|
1683
1696
|
}
|
|
1684
1697
|
|
|
1698
|
+
// src/store/memory.ts
|
|
1699
|
+
function copy(record) {
|
|
1700
|
+
return { ...record, scopes: [...record.scopes] };
|
|
1701
|
+
}
|
|
1702
|
+
function matches(record, selector) {
|
|
1703
|
+
return "grantId" in selector ? record.grantId === selector.grantId : record.addressId === selector.addressId;
|
|
1704
|
+
}
|
|
1705
|
+
function isLive(record, now) {
|
|
1706
|
+
return record.expiresAt > now && (record.kind === "access" || record.usedAt === undefined);
|
|
1707
|
+
}
|
|
1708
|
+
function summarizeGrants(records, now) {
|
|
1709
|
+
const grants = new Map;
|
|
1710
|
+
for (const r of records) {
|
|
1711
|
+
if (!isLive(r, now) || grants.has(r.grantId))
|
|
1712
|
+
continue;
|
|
1713
|
+
const summary = {
|
|
1714
|
+
grantId: r.grantId,
|
|
1715
|
+
clientId: r.clientId,
|
|
1716
|
+
clientName: r.clientName,
|
|
1717
|
+
scopes: [...r.scopes],
|
|
1718
|
+
createdAt: r.grantCreatedAt
|
|
1719
|
+
};
|
|
1720
|
+
if (r.subject !== undefined)
|
|
1721
|
+
summary.subject = r.subject;
|
|
1722
|
+
grants.set(r.grantId, summary);
|
|
1723
|
+
}
|
|
1724
|
+
return [...grants.values()].sort((a, b) => a.createdAt - b.createdAt || a.grantId.localeCompare(b.grantId));
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
class NonceSet {
|
|
1728
|
+
#until = new Map;
|
|
1729
|
+
get size() {
|
|
1730
|
+
return this.#until.size;
|
|
1731
|
+
}
|
|
1732
|
+
claim(key, expiresAt, now) {
|
|
1733
|
+
const until = this.#until.get(key);
|
|
1734
|
+
if (until !== undefined && until > now)
|
|
1735
|
+
return false;
|
|
1736
|
+
this.#until.set(key, expiresAt);
|
|
1737
|
+
return true;
|
|
1738
|
+
}
|
|
1739
|
+
sweep(now) {
|
|
1740
|
+
let n = 0;
|
|
1741
|
+
for (const [key, until] of this.#until) {
|
|
1742
|
+
if (until <= now) {
|
|
1743
|
+
this.#until.delete(key);
|
|
1744
|
+
n++;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return n;
|
|
1748
|
+
}
|
|
1749
|
+
clear() {
|
|
1750
|
+
this.#until.clear();
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
class MemoryTokenStore {
|
|
1755
|
+
#records = new Map;
|
|
1756
|
+
#nonces = new NonceSet;
|
|
1757
|
+
#revokedGrants = new Map;
|
|
1758
|
+
#revokedAddresses = new Map;
|
|
1759
|
+
async put(record) {
|
|
1760
|
+
if (this.#revokedGrants.has(record.grantId))
|
|
1761
|
+
return false;
|
|
1762
|
+
const address = this.#revokedAddresses.get(record.addressId);
|
|
1763
|
+
if (address !== undefined && record.grantCreatedAt <= address.at) {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
this.#records.set(record.hash, copy(record));
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
async get(hash) {
|
|
1770
|
+
const r = this.#records.get(hash);
|
|
1771
|
+
return r === undefined ? undefined : copy(r);
|
|
1772
|
+
}
|
|
1773
|
+
async markUsed(hash, now, keepUntil) {
|
|
1774
|
+
const r = this.#records.get(hash);
|
|
1775
|
+
if (r === undefined || r.usedAt !== undefined)
|
|
1776
|
+
return false;
|
|
1777
|
+
r.usedAt = now;
|
|
1778
|
+
r.expiresAt = Math.min(r.expiresAt, keepUntil);
|
|
1779
|
+
return true;
|
|
1780
|
+
}
|
|
1781
|
+
async revoke(selector, options) {
|
|
1782
|
+
if ("grantId" in selector) {
|
|
1783
|
+
this.#revokedGrants.set(selector.grantId, options.keepUntil);
|
|
1784
|
+
} else {
|
|
1785
|
+
const old = this.#revokedAddresses.get(selector.addressId);
|
|
1786
|
+
this.#revokedAddresses.set(selector.addressId, {
|
|
1787
|
+
at: Math.max(old?.at ?? 0, options.now),
|
|
1788
|
+
until: Math.max(old?.until ?? 0, options.keepUntil)
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
let n = 0;
|
|
1792
|
+
for (const [hash, r] of this.#records) {
|
|
1793
|
+
if (matches(r, selector)) {
|
|
1794
|
+
this.#records.delete(hash);
|
|
1795
|
+
n++;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return n;
|
|
1799
|
+
}
|
|
1800
|
+
async listGrants(addressId, now) {
|
|
1801
|
+
const own = [...this.#records.values()].filter((r) => r.addressId === addressId);
|
|
1802
|
+
return summarizeGrants(own, now);
|
|
1803
|
+
}
|
|
1804
|
+
async deleteExpired(now) {
|
|
1805
|
+
for (const [id, until] of this.#revokedGrants) {
|
|
1806
|
+
if (until <= now)
|
|
1807
|
+
this.#revokedGrants.delete(id);
|
|
1808
|
+
}
|
|
1809
|
+
for (const [id, t] of this.#revokedAddresses) {
|
|
1810
|
+
if (t.until <= now)
|
|
1811
|
+
this.#revokedAddresses.delete(id);
|
|
1812
|
+
}
|
|
1813
|
+
this.#nonces.sweep(now);
|
|
1814
|
+
let n = 0;
|
|
1815
|
+
for (const [hash, r] of this.#records) {
|
|
1816
|
+
if (r.expiresAt <= now) {
|
|
1817
|
+
this.#records.delete(hash);
|
|
1818
|
+
n++;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return n;
|
|
1822
|
+
}
|
|
1823
|
+
async claimNonce(key, expiresAt, now) {
|
|
1824
|
+
return this.#nonces.claim(key, expiresAt, now);
|
|
1825
|
+
}
|
|
1826
|
+
async close() {
|
|
1827
|
+
this.#nonces.clear();
|
|
1828
|
+
this.#records.clear();
|
|
1829
|
+
this.#revokedGrants.clear();
|
|
1830
|
+
this.#revokedAddresses.clear();
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1685
1834
|
// src/tokens.ts
|
|
1686
1835
|
var PREFIX = {
|
|
1687
1836
|
access: "tmcp_at_",
|
|
@@ -1711,7 +1860,8 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1711
1860
|
scopes: grant.scopes,
|
|
1712
1861
|
grantCreatedAt: grant.grantCreatedAt,
|
|
1713
1862
|
issuedAt: now,
|
|
1714
|
-
...grant.subject === undefined ? {} : { subject: grant.subject }
|
|
1863
|
+
...grant.subject === undefined ? {} : { subject: grant.subject },
|
|
1864
|
+
...grant.clientAuth === undefined ? {} : { clientAuth: grant.clientAuth }
|
|
1715
1865
|
};
|
|
1716
1866
|
const accessExpires = Math.min(now + options.accessTtlMs, options.grantEndsAt);
|
|
1717
1867
|
const refreshStored = await store.put({
|
|
@@ -1737,6 +1887,75 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1737
1887
|
};
|
|
1738
1888
|
}
|
|
1739
1889
|
|
|
1890
|
+
// src/oauth/domain-cache.ts
|
|
1891
|
+
class DomainCache {
|
|
1892
|
+
#entries = new Map;
|
|
1893
|
+
#counts = new Map;
|
|
1894
|
+
#max;
|
|
1895
|
+
#perDomain;
|
|
1896
|
+
#isDead;
|
|
1897
|
+
constructor(max, perDomain, isDead = () => false) {
|
|
1898
|
+
this.#max = max;
|
|
1899
|
+
this.#perDomain = perDomain;
|
|
1900
|
+
this.#isDead = isDead;
|
|
1901
|
+
}
|
|
1902
|
+
get size() {
|
|
1903
|
+
return this.#entries.size;
|
|
1904
|
+
}
|
|
1905
|
+
get(key) {
|
|
1906
|
+
const entry = this.#entries.get(key);
|
|
1907
|
+
if (entry === undefined)
|
|
1908
|
+
return;
|
|
1909
|
+
this.#entries.delete(key);
|
|
1910
|
+
this.#entries.set(key, entry);
|
|
1911
|
+
return entry.value;
|
|
1912
|
+
}
|
|
1913
|
+
delete(key) {
|
|
1914
|
+
const entry = this.#entries.get(key);
|
|
1915
|
+
if (entry === undefined)
|
|
1916
|
+
return;
|
|
1917
|
+
this.#entries.delete(key);
|
|
1918
|
+
const left = (this.#counts.get(entry.domain) ?? 1) - 1;
|
|
1919
|
+
if (left > 0)
|
|
1920
|
+
this.#counts.set(entry.domain, left);
|
|
1921
|
+
else
|
|
1922
|
+
this.#counts.delete(entry.domain);
|
|
1923
|
+
}
|
|
1924
|
+
set(key, domain, value, now) {
|
|
1925
|
+
this.delete(key);
|
|
1926
|
+
if ((this.#counts.get(domain) ?? 0) >= this.#perDomain) {
|
|
1927
|
+
this.#evictOldestOf(domain);
|
|
1928
|
+
} else if (this.#entries.size >= this.#max) {
|
|
1929
|
+
this.#evict(now);
|
|
1930
|
+
}
|
|
1931
|
+
this.#entries.set(key, { domain, value });
|
|
1932
|
+
this.#counts.set(domain, (this.#counts.get(domain) ?? 0) + 1);
|
|
1933
|
+
}
|
|
1934
|
+
#evict(now) {
|
|
1935
|
+
for (const [key, entry] of this.#entries) {
|
|
1936
|
+
if (this.#isDead(entry.value, now)) {
|
|
1937
|
+
this.delete(key);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const most = Math.max(0, ...this.#counts.values());
|
|
1942
|
+
for (const [key, entry] of this.#entries) {
|
|
1943
|
+
if (this.#counts.get(entry.domain) === most) {
|
|
1944
|
+
this.delete(key);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
#evictOldestOf(domain) {
|
|
1950
|
+
for (const [key, entry] of this.#entries) {
|
|
1951
|
+
if (entry.domain === domain) {
|
|
1952
|
+
this.delete(key);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1740
1959
|
// src/oauth/cimd.ts
|
|
1741
1960
|
class ClientMetadataError extends Error {
|
|
1742
1961
|
name = "ClientMetadataError";
|
|
@@ -1746,6 +1965,16 @@ class ClientMetadataError extends Error {
|
|
|
1746
1965
|
this.reason = reason;
|
|
1747
1966
|
}
|
|
1748
1967
|
}
|
|
1968
|
+
var TRANSIENT_REASONS = new Set([
|
|
1969
|
+
"rate_limited",
|
|
1970
|
+
"fetch_failed",
|
|
1971
|
+
"unresolvable",
|
|
1972
|
+
"too_large"
|
|
1973
|
+
]);
|
|
1974
|
+
function isTransient(error) {
|
|
1975
|
+
return TRANSIENT_REASONS.has(error.reason);
|
|
1976
|
+
}
|
|
1977
|
+
var STALE_GRACE_MS = 60 * 60000;
|
|
1749
1978
|
var MAX_CLIENT_ID_LENGTH = 2048;
|
|
1750
1979
|
var MAX_NAME_LENGTH = 64;
|
|
1751
1980
|
var MAX_REDIRECT_URIS = 20;
|
|
@@ -1753,6 +1982,9 @@ var MIN_CACHE_MS = 5 * 60000;
|
|
|
1753
1982
|
var DEFAULT_CACHE_MS = 60 * 60000;
|
|
1754
1983
|
var MAX_CACHE_MS = 24 * 60 * 60000;
|
|
1755
1984
|
var MAX_CACHE_ENTRIES = 1000;
|
|
1985
|
+
var MAX_CACHE_PER_DOMAIN = 100;
|
|
1986
|
+
var FAILURE_CACHE_MS = 60000;
|
|
1987
|
+
var MAX_FAILURES = 1000;
|
|
1756
1988
|
async function defaultLookup(hostname) {
|
|
1757
1989
|
const records = await Bun.dns.lookup(hostname, {});
|
|
1758
1990
|
return records.map((r) => r.address);
|
|
@@ -1786,12 +2018,22 @@ function limitKey(hostname) {
|
|
|
1786
2018
|
return h;
|
|
1787
2019
|
return h.split(".").slice(-2).join(".");
|
|
1788
2020
|
}
|
|
2021
|
+
function knownBudget() {
|
|
2022
|
+
return {
|
|
2023
|
+
perHost: new RateLimiter({ capacity: 5, refillPerSecond: 0.2 }),
|
|
2024
|
+
perDomain: new RateLimiter({ capacity: 15, refillPerSecond: 0.6 }),
|
|
2025
|
+
global: new RateLimiter({ capacity: 100, refillPerSecond: 10 })
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
1789
2028
|
|
|
1790
2029
|
class ClientMetadataFetcher {
|
|
1791
|
-
#cache = new
|
|
2030
|
+
#cache = new DomainCache(MAX_CACHE_ENTRIES, MAX_CACHE_PER_DOMAIN, (entry, now) => entry.until + STALE_GRACE_MS <= now);
|
|
2031
|
+
#failures = new Map;
|
|
1792
2032
|
#perHost = new RateLimiter({ capacity: 5, refillPerSecond: 0.2 });
|
|
1793
2033
|
#perDomain = new RateLimiter({ capacity: 15, refillPerSecond: 0.6 });
|
|
1794
2034
|
#global = new RateLimiter({ capacity: 30, refillPerSecond: 1 });
|
|
2035
|
+
#refreshes = knownBudget();
|
|
2036
|
+
#keys = knownBudget();
|
|
1795
2037
|
#allowedHosts;
|
|
1796
2038
|
#logger;
|
|
1797
2039
|
#timeoutMs;
|
|
@@ -1810,34 +2052,106 @@ class ClientMetadataFetcher {
|
|
|
1810
2052
|
this.#testMode = options.testMode;
|
|
1811
2053
|
this.#now = options.now ?? Date.now;
|
|
1812
2054
|
}
|
|
1813
|
-
async get(clientId) {
|
|
2055
|
+
async get(clientId, options = {}) {
|
|
1814
2056
|
const url = this.#checkUrl(clientId);
|
|
1815
2057
|
const now = this.#now();
|
|
1816
2058
|
const cached = this.#cache.get(clientId);
|
|
1817
|
-
if (cached !== undefined && cached.until > now)
|
|
1818
|
-
this.#cache.delete(clientId);
|
|
1819
|
-
this.#cache.set(clientId, cached);
|
|
2059
|
+
if (cached !== undefined && cached.until > now)
|
|
1820
2060
|
return cached.meta;
|
|
2061
|
+
const known = cached !== undefined && now < cached.until + STALE_GRACE_MS;
|
|
2062
|
+
const stale = options.allowStale === true && known ? cached?.meta : undefined;
|
|
2063
|
+
try {
|
|
2064
|
+
const failed = this.#failures.get(clientId);
|
|
2065
|
+
if (failed !== undefined && failed.until > now)
|
|
2066
|
+
throw failed.error;
|
|
2067
|
+
return await this.#refresh(clientId, url, now, known);
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
const transient = error instanceof ClientMetadataError && isTransient(error);
|
|
2070
|
+
if (!transient)
|
|
2071
|
+
this.#cache.delete(clientId);
|
|
2072
|
+
if (stale === undefined || !transient)
|
|
2073
|
+
throw error;
|
|
2074
|
+
this.#logger?.info("oauth.cimd_stale", {
|
|
2075
|
+
host: url.host,
|
|
2076
|
+
reason: error.reason
|
|
2077
|
+
});
|
|
2078
|
+
return stale;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
async#refresh(clientId, url, now, known) {
|
|
2082
|
+
this.#admit(url, now, known ? "refresh" : "new");
|
|
2083
|
+
let meta;
|
|
2084
|
+
let ttlMs;
|
|
2085
|
+
try {
|
|
2086
|
+
const address = await this.#vettedAddress(url);
|
|
2087
|
+
const response = await this.#fetchJson(url, address, [
|
|
2088
|
+
"application/json"
|
|
2089
|
+
]);
|
|
2090
|
+
meta = validateDocument(response.doc, clientId, url, {
|
|
2091
|
+
test: this.#testMode !== undefined,
|
|
2092
|
+
onFallback: (reason) => this.#logger?.info("oauth.cimd_auth_fallback", {
|
|
2093
|
+
host: url.host,
|
|
2094
|
+
reason
|
|
2095
|
+
})
|
|
2096
|
+
});
|
|
2097
|
+
ttlMs = cacheMs(response.cacheControl);
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
if (error instanceof ClientMetadataError) {
|
|
2100
|
+
this.#rememberFailure(clientId, error, now);
|
|
2101
|
+
}
|
|
2102
|
+
throw error;
|
|
1821
2103
|
}
|
|
2104
|
+
this.#failures.delete(clientId);
|
|
2105
|
+
this.#cache.set(clientId, limitKey(url.hostname), { meta, until: now + ttlMs }, now);
|
|
2106
|
+
return meta;
|
|
2107
|
+
}
|
|
2108
|
+
#rememberFailure(clientId, error, now) {
|
|
2109
|
+
this.#failures.delete(clientId);
|
|
2110
|
+
if (this.#failures.size >= MAX_FAILURES) {
|
|
2111
|
+
const oldest = this.#failures.keys().next().value;
|
|
2112
|
+
if (oldest !== undefined)
|
|
2113
|
+
this.#failures.delete(oldest);
|
|
2114
|
+
}
|
|
2115
|
+
this.#failures.set(clientId, { error, until: now + FAILURE_CACHE_MS });
|
|
2116
|
+
}
|
|
2117
|
+
async fetchJwks(jwksUri) {
|
|
2118
|
+
const url = this.#checkJwksUri(jwksUri);
|
|
2119
|
+
this.#admit(url, this.#now(), "keys");
|
|
2120
|
+
const address = await this.#vettedAddress(url);
|
|
2121
|
+
const response = await this.#fetchJson(url, address, [
|
|
2122
|
+
"application/json",
|
|
2123
|
+
"application/jwk-set+json"
|
|
2124
|
+
]);
|
|
2125
|
+
return response.doc;
|
|
2126
|
+
}
|
|
2127
|
+
#admit(url, now, budget) {
|
|
2128
|
+
const { perHost, perDomain, global } = budget === "refresh" ? this.#refreshes : budget === "keys" ? this.#keys : {
|
|
2129
|
+
perHost: this.#perHost,
|
|
2130
|
+
perDomain: this.#perDomain,
|
|
2131
|
+
global: this.#global
|
|
2132
|
+
};
|
|
1822
2133
|
const key = limitKey(url.hostname);
|
|
1823
|
-
if (!
|
|
2134
|
+
if (!perHost.available(url.host, now) || !perDomain.available(key, now)) {
|
|
1824
2135
|
throw new ClientMetadataError("rate_limited", "Too many requests for this AI app's details. Try again in a minute.");
|
|
1825
2136
|
}
|
|
1826
|
-
if (!
|
|
2137
|
+
if (!global.take("all", now)) {
|
|
1827
2138
|
throw new ClientMetadataError("rate_limited", "The relay is busy. Try again in a minute.");
|
|
1828
2139
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
this.#
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
if (oldest !== undefined)
|
|
1837
|
-
this.#cache.delete(oldest);
|
|
2140
|
+
perHost.take(url.host, now);
|
|
2141
|
+
perDomain.take(key, now);
|
|
2142
|
+
}
|
|
2143
|
+
#checkJwksUri(jwksUri) {
|
|
2144
|
+
const url = parseJwksUri(jwksUri, this.#testMode !== undefined);
|
|
2145
|
+
if (url === undefined) {
|
|
2146
|
+
throw new ClientMetadataError("invalid_jwks_uri", "The AI app's jwks_uri is not accepted.");
|
|
1838
2147
|
}
|
|
1839
|
-
this.#
|
|
1840
|
-
|
|
2148
|
+
if (this.#allowedHosts.length > 0 && !this.#allowedHosts.includes(url.host)) {
|
|
2149
|
+
throw new ClientMetadataError("host_not_allowed", `This relay does not accept AI apps from ${url.host}.`);
|
|
2150
|
+
}
|
|
2151
|
+
if (isIpLiteral(url.hostname) && !this.#addressAllowed(url.hostname)) {
|
|
2152
|
+
throw this.#notLoaded("private_address", url);
|
|
2153
|
+
}
|
|
2154
|
+
return url;
|
|
1841
2155
|
}
|
|
1842
2156
|
#checkUrl(clientId) {
|
|
1843
2157
|
const refuse = (message) => new ClientMetadataError("invalid_client_id", message);
|
|
@@ -1899,9 +2213,9 @@ class ClientMetadataFetcher {
|
|
|
1899
2213
|
}
|
|
1900
2214
|
return addresses.find((a) => parseIpv4(a) !== undefined) ?? addresses[0] ?? "";
|
|
1901
2215
|
}
|
|
1902
|
-
async#
|
|
2216
|
+
async#fetchJson(url, address, types) {
|
|
1903
2217
|
const fail = (detail) => this.#notLoaded("fetch_failed", url, detail);
|
|
1904
|
-
const pinned = new URL(
|
|
2218
|
+
const pinned = new URL(url.href);
|
|
1905
2219
|
pinned.hostname = address.includes(":") ? `[${address}]` : address;
|
|
1906
2220
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
1907
2221
|
const tls = url.protocol === "https:" ? {
|
|
@@ -1926,8 +2240,9 @@ class ClientMetadataFetcher {
|
|
|
1926
2240
|
if (response.status !== 200)
|
|
1927
2241
|
throw fail(`status_${response.status}`);
|
|
1928
2242
|
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1929
|
-
if (type
|
|
2243
|
+
if (type === undefined || !types.includes(type)) {
|
|
1930
2244
|
throw fail("not_json");
|
|
2245
|
+
}
|
|
1931
2246
|
const text = await this.#readLimited(response, url);
|
|
1932
2247
|
let doc;
|
|
1933
2248
|
try {
|
|
@@ -1935,8 +2250,7 @@ class ClientMetadataFetcher {
|
|
|
1935
2250
|
} catch {
|
|
1936
2251
|
throw fail("bad_json");
|
|
1937
2252
|
}
|
|
1938
|
-
|
|
1939
|
-
return { meta, ttlMs: cacheMs(response.headers.get("cache-control")) };
|
|
2253
|
+
return { doc, cacheControl: response.headers.get("cache-control") };
|
|
1940
2254
|
} finally {
|
|
1941
2255
|
if (!response.bodyUsed)
|
|
1942
2256
|
response.body?.cancel().catch(() => {});
|
|
@@ -1991,7 +2305,7 @@ function foreignDomain(name, hostname) {
|
|
|
1991
2305
|
}
|
|
1992
2306
|
return false;
|
|
1993
2307
|
}
|
|
1994
|
-
function validateDocument(doc, clientId, url) {
|
|
2308
|
+
function validateDocument(doc, clientId, url, options = {}) {
|
|
1995
2309
|
const invalid = (message) => new ClientMetadataError("invalid_metadata", message);
|
|
1996
2310
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
1997
2311
|
throw invalid("The AI app's details are not a JSON object.");
|
|
@@ -2017,11 +2331,7 @@ function validateDocument(doc, clientId, url) {
|
|
|
2017
2331
|
if (!Array.isArray(uris) || uris.length === 0 || uris.length > MAX_REDIRECT_URIS || !uris.every((u) => typeof u === "string" && isRedirectUri(u))) {
|
|
2018
2332
|
throw invalid("The AI app's details have no valid redirect_uris.");
|
|
2019
2333
|
}
|
|
2020
|
-
const
|
|
2021
|
-
const supported = d["token_endpoint_auth_methods_supported"];
|
|
2022
|
-
if (method !== undefined && method !== "none" && !(Array.isArray(supported) && supported.includes("none"))) {
|
|
2023
|
-
throw invalid('The AI app must use token_endpoint_auth_method "none" with this relay, or list "none" in token_endpoint_auth_methods_supported.');
|
|
2024
|
-
}
|
|
2334
|
+
const auth = clientAuth(d, url, options, invalid);
|
|
2025
2335
|
const grants = d["grant_types"];
|
|
2026
2336
|
if (grants !== undefined && (!Array.isArray(grants) || !grants.includes("authorization_code"))) {
|
|
2027
2337
|
throw invalid("The AI app's details do not allow the authorization code grant.");
|
|
@@ -2034,9 +2344,81 @@ function validateDocument(doc, clientId, url) {
|
|
|
2034
2344
|
clientId,
|
|
2035
2345
|
name: displayName(name),
|
|
2036
2346
|
host: url.host,
|
|
2037
|
-
redirectUris: uris
|
|
2347
|
+
redirectUris: uris,
|
|
2348
|
+
auth
|
|
2038
2349
|
};
|
|
2039
2350
|
}
|
|
2351
|
+
function clientAuth(d, url, options, invalid) {
|
|
2352
|
+
const method = d["token_endpoint_auth_method"];
|
|
2353
|
+
const supported = d["token_endpoint_auth_methods_supported"];
|
|
2354
|
+
const listsNone = Array.isArray(supported) && supported.includes("none");
|
|
2355
|
+
if (method === undefined || method === "none")
|
|
2356
|
+
return { method: "none" };
|
|
2357
|
+
const checked = method === "private_key_jwt" ? privateKeyJwt(d, url, options.test === true) : {
|
|
2358
|
+
problem: "unsupported_method",
|
|
2359
|
+
message: 'The AI app must use token_endpoint_auth_method "none" or "private_key_jwt" with this relay, or list "none" in token_endpoint_auth_methods_supported.'
|
|
2360
|
+
};
|
|
2361
|
+
if ("jwksUri" in checked) {
|
|
2362
|
+
return { method: "private_key_jwt", ...checked, publicAllowed: listsNone };
|
|
2363
|
+
}
|
|
2364
|
+
if (listsNone) {
|
|
2365
|
+
options.onFallback?.(checked.problem);
|
|
2366
|
+
return { method: "none" };
|
|
2367
|
+
}
|
|
2368
|
+
throw invalid(checked.message);
|
|
2369
|
+
}
|
|
2370
|
+
function privateKeyJwt(d, url, test) {
|
|
2371
|
+
const jwksUri = d["jwks_uri"];
|
|
2372
|
+
if (jwksUri === undefined) {
|
|
2373
|
+
return {
|
|
2374
|
+
problem: "no_jwks_uri",
|
|
2375
|
+
message: "The AI app uses private_key_jwt but publishes no jwks_uri (an inline jwks is not accepted)."
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
if (d["jwks"] !== undefined) {
|
|
2379
|
+
return {
|
|
2380
|
+
problem: "jwks_and_jwks_uri",
|
|
2381
|
+
message: "The AI app's details have both jwks and jwks_uri."
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
const parsed = typeof jwksUri === "string" ? parseJwksUri(jwksUri, test) : undefined;
|
|
2385
|
+
if (parsed === undefined || parsed.protocol !== url.protocol || parsed.host !== url.host) {
|
|
2386
|
+
return {
|
|
2387
|
+
problem: "bad_jwks_uri",
|
|
2388
|
+
message: "The AI app's jwks_uri must be an https:// URL on the host of its client_id."
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
const alg = d["token_endpoint_auth_signing_alg"];
|
|
2392
|
+
if (alg !== undefined && !isAlgorithm(alg)) {
|
|
2393
|
+
return {
|
|
2394
|
+
problem: "signing_alg",
|
|
2395
|
+
message: "The AI app's token_endpoint_auth_signing_alg is not one this relay accepts."
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
return {
|
|
2399
|
+
jwksUri: parsed.href,
|
|
2400
|
+
algorithms: alg === undefined ? JWT_ALGORITHMS : [alg]
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function parseJwksUri(value, test) {
|
|
2404
|
+
if (value.length > MAX_CLIENT_ID_LENGTH)
|
|
2405
|
+
return;
|
|
2406
|
+
let url;
|
|
2407
|
+
try {
|
|
2408
|
+
url = new URL(value);
|
|
2409
|
+
} catch {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const scheme = url.protocol === "https:" || test && url.protocol === "http:";
|
|
2413
|
+
if (!scheme || url.href !== value)
|
|
2414
|
+
return;
|
|
2415
|
+
if (url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (url.port !== "" && !test)
|
|
2419
|
+
return;
|
|
2420
|
+
return url;
|
|
2421
|
+
}
|
|
2040
2422
|
var PRIVATE_USE_SCHEME = /^[a-z][a-z0-9+-]*(\.[a-z0-9+-]+)+:$/;
|
|
2041
2423
|
function isRedirectUri(value) {
|
|
2042
2424
|
if (value.length > 2048 || value.includes("#"))
|
|
@@ -2080,6 +2462,151 @@ function redirectUriMatches(requested, registered) {
|
|
|
2080
2462
|
return a.href === b.href;
|
|
2081
2463
|
}
|
|
2082
2464
|
|
|
2465
|
+
// src/oauth/client-auth.ts
|
|
2466
|
+
var CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
2467
|
+
var CLIENT_ASSERTION_MAX_LIFETIME_MS = 60 * 60000;
|
|
2468
|
+
var CLIENT_ASSERTION_MAX_AGE_MS = 5 * 60000;
|
|
2469
|
+
var CLIENT_ASSERTION_LEEWAY_MS = 30000;
|
|
2470
|
+
var MAX_JTI2 = 256;
|
|
2471
|
+
var MAX_KEY_SETS = 1000;
|
|
2472
|
+
var MAX_KEY_SETS_PER_DOMAIN = 100;
|
|
2473
|
+
var SWEEP_INTERVAL_MS = 1000;
|
|
2474
|
+
var ACCEPTED_TYPES = new Set(["jwt", "client-authentication+jwt"]);
|
|
2475
|
+
function assertionSubject(assertion) {
|
|
2476
|
+
try {
|
|
2477
|
+
const sub = decodeJwt(assertion).payload["sub"];
|
|
2478
|
+
return typeof sub === "string" && sub !== "" ? sub : undefined;
|
|
2479
|
+
} catch {
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
function nonceKey(clientId, jti) {
|
|
2484
|
+
return hashToken(JSON.stringify(["client_assertion", clientId, jti]));
|
|
2485
|
+
}
|
|
2486
|
+
async function verifyClientAssertion(assertion, check) {
|
|
2487
|
+
const { header, payload } = await verifyJwt(assertion, check.keys, {
|
|
2488
|
+
algorithms: check.auth.algorithms
|
|
2489
|
+
});
|
|
2490
|
+
const typ = header["typ"];
|
|
2491
|
+
if (typ !== undefined && (typeof typ !== "string" || !ACCEPTED_TYPES.has(typ.toLowerCase()))) {
|
|
2492
|
+
throw new JwtError("typ", "the assertion's type is not a client assertion");
|
|
2493
|
+
}
|
|
2494
|
+
if (payload["iss"] !== check.clientId) {
|
|
2495
|
+
throw new JwtError("iss", "the assertion's issuer is not the client");
|
|
2496
|
+
}
|
|
2497
|
+
if (payload["sub"] !== check.clientId) {
|
|
2498
|
+
throw new JwtError("sub", "the assertion's subject is not the client");
|
|
2499
|
+
}
|
|
2500
|
+
if (!audiences(payload).some((a) => check.audiences.includes(a))) {
|
|
2501
|
+
throw new JwtError("aud", "the assertion is not for this relay");
|
|
2502
|
+
}
|
|
2503
|
+
const usableUntil = checkTimes(payload, check.now);
|
|
2504
|
+
const jti = payload["jti"];
|
|
2505
|
+
if (typeof jti !== "string" || jti === "" || jti.length > MAX_JTI2) {
|
|
2506
|
+
throw new JwtError("jti", "the assertion needs a jti");
|
|
2507
|
+
}
|
|
2508
|
+
const key = nonceKey(check.clientId, jti);
|
|
2509
|
+
if (!await check.claimNonce(key, usableUntil, check.now)) {
|
|
2510
|
+
throw new JwtError("replayed", "the assertion was already used");
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function checkTimes(payload, now) {
|
|
2514
|
+
const leeway = CLIENT_ASSERTION_LEEWAY_MS;
|
|
2515
|
+
const exp = numericDate(payload, "exp");
|
|
2516
|
+
if (exp === undefined)
|
|
2517
|
+
throw new JwtError("exp", "the assertion has no exp");
|
|
2518
|
+
if (exp <= now - leeway) {
|
|
2519
|
+
throw new JwtError("expired", "the assertion has expired");
|
|
2520
|
+
}
|
|
2521
|
+
if (exp > now + CLIENT_ASSERTION_MAX_LIFETIME_MS + leeway) {
|
|
2522
|
+
throw new JwtError("lifetime", "the assertion lives longer than an hour");
|
|
2523
|
+
}
|
|
2524
|
+
const iat = numericDate(payload, "iat");
|
|
2525
|
+
let end = exp;
|
|
2526
|
+
if (iat === undefined) {
|
|
2527
|
+
if (exp > now + CLIENT_ASSERTION_MAX_AGE_MS + leeway) {
|
|
2528
|
+
throw new JwtError("lifetime", "an assertion without iat may live five minutes");
|
|
2529
|
+
}
|
|
2530
|
+
} else {
|
|
2531
|
+
if (iat > now + leeway) {
|
|
2532
|
+
throw new JwtError("iat", "the assertion was issued in the future");
|
|
2533
|
+
}
|
|
2534
|
+
if (iat + CLIENT_ASSERTION_MAX_AGE_MS <= now - leeway) {
|
|
2535
|
+
throw new JwtError("too_old", "the assertion was issued too long ago");
|
|
2536
|
+
}
|
|
2537
|
+
end = Math.min(exp, iat + CLIENT_ASSERTION_MAX_AGE_MS);
|
|
2538
|
+
}
|
|
2539
|
+
const nbf = numericDate(payload, "nbf");
|
|
2540
|
+
if (nbf !== undefined && nbf > now + leeway) {
|
|
2541
|
+
throw new JwtError("nbf", "the assertion is not valid yet");
|
|
2542
|
+
}
|
|
2543
|
+
return end + leeway;
|
|
2544
|
+
}
|
|
2545
|
+
function quotaKey(clientId) {
|
|
2546
|
+
return ipPrefixKey(new URL(clientId).hostname.replace(/\.$/, ""));
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
class NonceQuota {
|
|
2550
|
+
#until = new Map;
|
|
2551
|
+
#perDomain;
|
|
2552
|
+
#max;
|
|
2553
|
+
#total = 0;
|
|
2554
|
+
#sweptAt = Number.NEGATIVE_INFINITY;
|
|
2555
|
+
constructor(perDomain, max) {
|
|
2556
|
+
this.#perDomain = perDomain;
|
|
2557
|
+
this.#max = max;
|
|
2558
|
+
}
|
|
2559
|
+
refusal(domain, now) {
|
|
2560
|
+
this.sweep(now);
|
|
2561
|
+
if ((this.#until.get(domain)?.length ?? 0) >= this.#perDomain) {
|
|
2562
|
+
return "domain_full";
|
|
2563
|
+
}
|
|
2564
|
+
return this.#total >= this.#max ? "full" : undefined;
|
|
2565
|
+
}
|
|
2566
|
+
add(domain, until) {
|
|
2567
|
+
const list = this.#until.get(domain);
|
|
2568
|
+
if (list === undefined)
|
|
2569
|
+
this.#until.set(domain, [until]);
|
|
2570
|
+
else
|
|
2571
|
+
list.push(until);
|
|
2572
|
+
this.#total++;
|
|
2573
|
+
}
|
|
2574
|
+
sweep(now) {
|
|
2575
|
+
if (now - this.#sweptAt < SWEEP_INTERVAL_MS)
|
|
2576
|
+
return false;
|
|
2577
|
+
this.#sweptAt = now;
|
|
2578
|
+
this.#total = 0;
|
|
2579
|
+
for (const [domain, list] of this.#until) {
|
|
2580
|
+
const live = list.filter((until) => until > now);
|
|
2581
|
+
if (live.length === 0)
|
|
2582
|
+
this.#until.delete(domain);
|
|
2583
|
+
else
|
|
2584
|
+
this.#until.set(domain, live);
|
|
2585
|
+
this.#total += live.length;
|
|
2586
|
+
}
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
class ClientKeySets {
|
|
2592
|
+
#sets = new DomainCache(MAX_KEY_SETS, MAX_KEY_SETS_PER_DOMAIN);
|
|
2593
|
+
#load;
|
|
2594
|
+
#now;
|
|
2595
|
+
constructor(load, now = Date.now) {
|
|
2596
|
+
this.#load = load;
|
|
2597
|
+
this.#now = now;
|
|
2598
|
+
}
|
|
2599
|
+
for(jwksUri) {
|
|
2600
|
+
const known = this.#sets.get(jwksUri);
|
|
2601
|
+
if (known !== undefined)
|
|
2602
|
+
return known;
|
|
2603
|
+
const set = new JwksCache(jwksUri, { load: this.#load, now: this.#now });
|
|
2604
|
+
const domain = limitKey(new URL(jwksUri).hostname);
|
|
2605
|
+
this.#sets.set(jwksUri, domain, set, this.#now());
|
|
2606
|
+
return set;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2083
2610
|
// src/oauth/html.ts
|
|
2084
2611
|
var ESCAPES = {
|
|
2085
2612
|
"&": "&",
|
|
@@ -2649,6 +3176,12 @@ var REFRESH_REUSE_WINDOW_MS = 24 * 60 * 60000;
|
|
|
2649
3176
|
var MAX_CODES = 1e4;
|
|
2650
3177
|
var MAX_FORM_BYTES = 8192;
|
|
2651
3178
|
var MAX_STATE_LENGTH = 1024;
|
|
3179
|
+
var MAX_NONCES_PER_HOST = 1e4;
|
|
3180
|
+
var MAX_NONCES = 1e5;
|
|
3181
|
+
var CLIENT_AUTH_FAILED = "client authentication failed";
|
|
3182
|
+
function assertionRequired(auth) {
|
|
3183
|
+
return auth.method === "private_key_jwt" && !auth.publicAllowed;
|
|
3184
|
+
}
|
|
2652
3185
|
|
|
2653
3186
|
class OAuthError extends Error {
|
|
2654
3187
|
status;
|
|
@@ -2698,10 +3231,16 @@ function hostOf(url) {
|
|
|
2698
3231
|
return url;
|
|
2699
3232
|
}
|
|
2700
3233
|
}
|
|
3234
|
+
function logHost(clientId) {
|
|
3235
|
+
return clientId !== undefined && URL.canParse(clientId) ? hostOf(clientId).slice(0, 100) : "(not a URL)";
|
|
3236
|
+
}
|
|
2701
3237
|
|
|
2702
3238
|
class AuthorizationServer {
|
|
2703
3239
|
#host;
|
|
2704
3240
|
#cimd;
|
|
3241
|
+
#clientKeys;
|
|
3242
|
+
#nonces = new NonceSet;
|
|
3243
|
+
#nonceQuota = new NonceQuota(MAX_NONCES_PER_HOST, MAX_NONCES);
|
|
2705
3244
|
#pending = new Map;
|
|
2706
3245
|
#pendingPerIp = new Map;
|
|
2707
3246
|
#pendingPerAddress = new Map;
|
|
@@ -2752,6 +3291,8 @@ class AuthorizationServer {
|
|
|
2752
3291
|
logger: host.logger,
|
|
2753
3292
|
...testMode?.cimd === undefined ? {} : { testMode: testMode.cimd }
|
|
2754
3293
|
});
|
|
3294
|
+
const cimd = this.#cimd;
|
|
3295
|
+
this.#clientKeys = new ClientKeySets((url) => cimd.fetchJwks(url));
|
|
2755
3296
|
}
|
|
2756
3297
|
get #issuer() {
|
|
2757
3298
|
return this.#host.publicUrl();
|
|
@@ -2805,8 +3346,10 @@ class AuthorizationServer {
|
|
|
2805
3346
|
response_modes_supported: ["query"],
|
|
2806
3347
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2807
3348
|
code_challenge_methods_supported: ["S256"],
|
|
2808
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
2809
|
-
|
|
3349
|
+
token_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3350
|
+
token_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
3351
|
+
revocation_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3352
|
+
revocation_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
2810
3353
|
scopes_supported: [SCOPE],
|
|
2811
3354
|
client_id_metadata_document_supported: true,
|
|
2812
3355
|
authorization_response_iss_parameter_supported: true,
|
|
@@ -3305,20 +3848,21 @@ ${cancel}
|
|
|
3305
3848
|
"code_verifier",
|
|
3306
3849
|
"refresh_token",
|
|
3307
3850
|
"resource",
|
|
3308
|
-
"scope"
|
|
3851
|
+
"scope",
|
|
3852
|
+
"client_assertion",
|
|
3853
|
+
"client_assertion_type"
|
|
3309
3854
|
]);
|
|
3310
3855
|
if ("duplicate" in p) {
|
|
3311
3856
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3312
3857
|
}
|
|
3313
|
-
const
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
}
|
|
3858
|
+
const client = await this.#authenticateClient(p, [
|
|
3859
|
+
`${this.#issuer}/token`
|
|
3860
|
+
]);
|
|
3317
3861
|
let body;
|
|
3318
3862
|
if (p["grant_type"] === "authorization_code") {
|
|
3319
|
-
body = await this.#codeGrant(p,
|
|
3863
|
+
body = await this.#codeGrant(p, client);
|
|
3320
3864
|
} else if (p["grant_type"] === "refresh_token") {
|
|
3321
|
-
body = await this.#refreshGrant(p,
|
|
3865
|
+
body = await this.#refreshGrant(p, client);
|
|
3322
3866
|
} else if (p["grant_type"] === undefined) {
|
|
3323
3867
|
throw new OAuthError(400, "invalid_request", "grant_type is required");
|
|
3324
3868
|
} else {
|
|
@@ -3352,19 +3896,96 @@ ${cancel}
|
|
|
3352
3896
|
throw new OAuthError(400, "invalid_request", "the body must be application/x-www-form-urlencoded");
|
|
3353
3897
|
}
|
|
3354
3898
|
if (req.headers.get("authorization") !== null) {
|
|
3355
|
-
throw new OAuthError(401, "invalid_client", "
|
|
3899
|
+
throw new OAuthError(401, "invalid_client", "HTTP authentication is not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3356
3900
|
}
|
|
3357
3901
|
const text = await this.#readForm(req);
|
|
3358
3902
|
if (text === undefined) {
|
|
3359
3903
|
throw new OAuthError(413, "invalid_request", "the request is too large");
|
|
3360
3904
|
}
|
|
3361
3905
|
const params = new URLSearchParams(text);
|
|
3362
|
-
if (params.has("client_secret")
|
|
3363
|
-
throw new OAuthError(400, "invalid_client", "client
|
|
3906
|
+
if (params.has("client_secret")) {
|
|
3907
|
+
throw new OAuthError(400, "invalid_client", "client secrets are not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3364
3908
|
}
|
|
3365
3909
|
return params;
|
|
3366
3910
|
}
|
|
3367
|
-
async#
|
|
3911
|
+
async#authenticateClient(p, endpoints) {
|
|
3912
|
+
const assertion = p["client_assertion"];
|
|
3913
|
+
const type = p["client_assertion_type"];
|
|
3914
|
+
if (assertion === undefined && type === undefined) {
|
|
3915
|
+
const clientId = p["client_id"];
|
|
3916
|
+
if (clientId === undefined) {
|
|
3917
|
+
throw new OAuthError(400, "invalid_request", "client_id is required");
|
|
3918
|
+
}
|
|
3919
|
+
return { clientId, assertion: false };
|
|
3920
|
+
}
|
|
3921
|
+
if (assertion === undefined || type === undefined) {
|
|
3922
|
+
throw new OAuthError(400, "invalid_request", "client_assertion and client_assertion_type go together");
|
|
3923
|
+
}
|
|
3924
|
+
const clientId = p["client_id"] ?? assertionSubject(assertion);
|
|
3925
|
+
if (type !== CLIENT_ASSERTION_TYPE) {
|
|
3926
|
+
throw this.#clientAuthFailed(clientId, "assertion_type");
|
|
3927
|
+
}
|
|
3928
|
+
if (clientId === undefined) {
|
|
3929
|
+
throw this.#clientAuthFailed(clientId, "no_client_id");
|
|
3930
|
+
}
|
|
3931
|
+
let client;
|
|
3932
|
+
try {
|
|
3933
|
+
client = await this.#cimd.get(clientId, { allowStale: true });
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
if (!(error instanceof ClientMetadataError))
|
|
3936
|
+
throw error;
|
|
3937
|
+
const reason = `metadata_${error.reason}`;
|
|
3938
|
+
throw isTransient(error) || error.reason === "private_address" ? this.#clientAuthUnavailable(clientId, reason) : this.#clientAuthFailed(clientId, reason);
|
|
3939
|
+
}
|
|
3940
|
+
if (client.auth.method !== "private_key_jwt") {
|
|
3941
|
+
throw this.#clientAuthFailed(clientId, "not_private_key_jwt");
|
|
3942
|
+
}
|
|
3943
|
+
const domain = quotaKey(clientId);
|
|
3944
|
+
try {
|
|
3945
|
+
await verifyClientAssertion(assertion, {
|
|
3946
|
+
clientId,
|
|
3947
|
+
auth: client.auth,
|
|
3948
|
+
keys: this.#clientKeys.for(client.auth.jwksUri),
|
|
3949
|
+
audiences: [this.#issuer, ...endpoints],
|
|
3950
|
+
claimNonce: (key, until, now) => this.#claimNonce(clientId, domain, key, until, now),
|
|
3951
|
+
now: Date.now()
|
|
3952
|
+
});
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
if (!(error instanceof JwtError))
|
|
3955
|
+
throw error;
|
|
3956
|
+
throw error.reason === "jwks_unavailable" ? this.#clientAuthUnavailable(clientId, error.reason) : this.#clientAuthFailed(clientId, error.reason);
|
|
3957
|
+
}
|
|
3958
|
+
return { clientId, assertion: true };
|
|
3959
|
+
}
|
|
3960
|
+
#clientAuthFailed(clientId, reason) {
|
|
3961
|
+
this.#host.logger.info("oauth.client_auth_failed", {
|
|
3962
|
+
client: logHost(clientId),
|
|
3963
|
+
reason
|
|
3964
|
+
});
|
|
3965
|
+
return new OAuthError(401, "invalid_client", CLIENT_AUTH_FAILED);
|
|
3966
|
+
}
|
|
3967
|
+
#clientAuthUnavailable(clientId, reason) {
|
|
3968
|
+
this.#host.logger.warn("oauth.client_auth_unavailable", {
|
|
3969
|
+
client: logHost(clientId),
|
|
3970
|
+
reason
|
|
3971
|
+
});
|
|
3972
|
+
return new OAuthError(503, "temporarily_unavailable", "the client cannot be authenticated right now; try again shortly");
|
|
3973
|
+
}
|
|
3974
|
+
async#claimNonce(clientId, domain, key, until, now) {
|
|
3975
|
+
if (this.#nonceQuota.sweep(now))
|
|
3976
|
+
this.#nonces.sweep(now);
|
|
3977
|
+
const refusal = this.#nonceQuota.refusal(domain, now);
|
|
3978
|
+
if (refusal !== undefined) {
|
|
3979
|
+
throw this.#clientAuthUnavailable(clientId, `nonces_${refusal}`);
|
|
3980
|
+
}
|
|
3981
|
+
const store = this.#host.store;
|
|
3982
|
+
const claimed = store.claimNonce === undefined ? this.#nonces.claim(key, until, now) : await store.claimNonce(key, until, now);
|
|
3983
|
+
if (claimed)
|
|
3984
|
+
this.#nonceQuota.add(domain, until);
|
|
3985
|
+
return claimed;
|
|
3986
|
+
}
|
|
3987
|
+
async#codeGrant(p, client) {
|
|
3988
|
+
const clientId = client.clientId;
|
|
3368
3989
|
const code = p["code"];
|
|
3369
3990
|
const redirectUri = p["redirect_uri"];
|
|
3370
3991
|
const verifier = p["code_verifier"];
|
|
@@ -3376,6 +3997,12 @@ ${cancel}
|
|
|
3376
3997
|
const invalid = (why) => new OAuthError(400, "invalid_grant", why);
|
|
3377
3998
|
if (issued === undefined)
|
|
3378
3999
|
throw invalid("the code is not valid");
|
|
4000
|
+
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
4001
|
+
throw invalid("the code was issued to another client");
|
|
4002
|
+
}
|
|
4003
|
+
if (assertionRequired(issued.client.auth) && !client.assertion) {
|
|
4004
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4005
|
+
}
|
|
3379
4006
|
if (issued.used) {
|
|
3380
4007
|
if (issued.grantId !== undefined) {
|
|
3381
4008
|
await this.#revokeGrant(issued.addressId, issued.grantId, "reuse_detected");
|
|
@@ -3385,9 +4012,6 @@ ${cancel}
|
|
|
3385
4012
|
issued.used = true;
|
|
3386
4013
|
if (issued.expiresAt <= Date.now())
|
|
3387
4014
|
throw invalid("the code has expired");
|
|
3388
|
-
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
3389
|
-
throw invalid("the code was issued to another client");
|
|
3390
|
-
}
|
|
3391
4015
|
if (redirectUri !== issued.redirectUri) {
|
|
3392
4016
|
throw invalid("redirect_uri does not match the authorization request");
|
|
3393
4017
|
}
|
|
@@ -3411,7 +4035,8 @@ ${cancel}
|
|
|
3411
4035
|
clientName: issued.client.name,
|
|
3412
4036
|
scopes: issued.scopes,
|
|
3413
4037
|
grantCreatedAt: now,
|
|
3414
|
-
...issued.subject === undefined ? {} : { subject: issued.subject }
|
|
4038
|
+
...issued.subject === undefined ? {} : { subject: issued.subject },
|
|
4039
|
+
...client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3415
4040
|
}, {
|
|
3416
4041
|
accessTtlMs: config.accessTokenTtlMs,
|
|
3417
4042
|
refreshTtlMs: config.refreshTokenTtlMs,
|
|
@@ -3445,7 +4070,8 @@ ${cancel}
|
|
|
3445
4070
|
scope: scopes.join(" ")
|
|
3446
4071
|
};
|
|
3447
4072
|
}
|
|
3448
|
-
async#refreshGrant(p,
|
|
4073
|
+
async#refreshGrant(p, client) {
|
|
4074
|
+
const clientId = client.clientId;
|
|
3449
4075
|
const token = p["refresh_token"];
|
|
3450
4076
|
if (token === undefined) {
|
|
3451
4077
|
throw new OAuthError(400, "invalid_request", "refresh_token is required");
|
|
@@ -3459,6 +4085,9 @@ ${cancel}
|
|
|
3459
4085
|
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
3460
4086
|
throw invalid("the refresh token was issued to another client");
|
|
3461
4087
|
}
|
|
4088
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4089
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4090
|
+
}
|
|
3462
4091
|
const now = Date.now();
|
|
3463
4092
|
if (record.expiresAt <= now)
|
|
3464
4093
|
throw invalid("the refresh token has expired");
|
|
@@ -3490,7 +4119,8 @@ ${cancel}
|
|
|
3490
4119
|
clientName: record.clientName,
|
|
3491
4120
|
scopes: record.scopes,
|
|
3492
4121
|
grantCreatedAt: record.grantCreatedAt,
|
|
3493
|
-
...record.subject === undefined ? {} : { subject: record.subject }
|
|
4122
|
+
...record.subject === undefined ? {} : { subject: record.subject },
|
|
4123
|
+
...record.clientAuth === "private_key_jwt" || client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3494
4124
|
}, {
|
|
3495
4125
|
accessTtlMs: this.#host.config.accessTokenTtlMs,
|
|
3496
4126
|
refreshTtlMs: this.#host.config.refreshTokenTtlMs,
|
|
@@ -3526,24 +4156,36 @@ ${cancel}
|
|
|
3526
4156
|
throw new OAuthError(429, "temporarily_unavailable", "too many requests; try again shortly");
|
|
3527
4157
|
}
|
|
3528
4158
|
const params = await this.#formParams(req);
|
|
3529
|
-
const p = single(params, [
|
|
4159
|
+
const p = single(params, [
|
|
4160
|
+
"token",
|
|
4161
|
+
"client_id",
|
|
4162
|
+
"token_type_hint",
|
|
4163
|
+
"client_assertion",
|
|
4164
|
+
"client_assertion_type"
|
|
4165
|
+
]);
|
|
3530
4166
|
if ("duplicate" in p) {
|
|
3531
4167
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3532
4168
|
}
|
|
3533
4169
|
const token = p["token"];
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
throw new OAuthError(400, "invalid_request", "token and client_id are required");
|
|
4170
|
+
if (token === undefined) {
|
|
4171
|
+
throw new OAuthError(400, "invalid_request", "token is required");
|
|
3537
4172
|
}
|
|
4173
|
+
const client = await this.#authenticateClient(p, [
|
|
4174
|
+
`${this.#issuer}/revoke`,
|
|
4175
|
+
`${this.#issuer}/token`
|
|
4176
|
+
]);
|
|
3538
4177
|
const record = await this.#host.store.get(hashToken(token));
|
|
3539
4178
|
if (record === undefined)
|
|
3540
4179
|
return new Response(null, {
|
|
3541
4180
|
status: 200,
|
|
3542
4181
|
headers: { ...this.#cors(), ...noStore }
|
|
3543
4182
|
});
|
|
3544
|
-
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
4183
|
+
if (!constantTimeEqual(client.clientId, record.clientId)) {
|
|
3545
4184
|
throw new OAuthError(400, "invalid_client", "the token was issued to another client");
|
|
3546
4185
|
}
|
|
4186
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4187
|
+
throw this.#clientAuthFailed(client.clientId, "assertion_required");
|
|
4188
|
+
}
|
|
3547
4189
|
await this.#revokeGrant(record.addressId, record.grantId, "revoked");
|
|
3548
4190
|
this.#host.logger.info("oauth.revoked", {
|
|
3549
4191
|
address: shortAddress(record.addressId),
|
|
@@ -3589,6 +4231,7 @@ ${cancel}
|
|
|
3589
4231
|
this.#guessesPerAddress.sweep(now);
|
|
3590
4232
|
this.#tokenPerIp.sweep(now);
|
|
3591
4233
|
this.#refreshesPerGrant.sweep(now);
|
|
4234
|
+
this.#nonces.sweep(now);
|
|
3592
4235
|
}
|
|
3593
4236
|
}
|
|
3594
4237
|
|
|
@@ -3997,112 +4640,10 @@ class TabSession {
|
|
|
3997
4640
|
}
|
|
3998
4641
|
}
|
|
3999
4642
|
|
|
4000
|
-
// src/store/memory.ts
|
|
4001
|
-
function copy(record) {
|
|
4002
|
-
return { ...record, scopes: [...record.scopes] };
|
|
4003
|
-
}
|
|
4004
|
-
function matches(record, selector) {
|
|
4005
|
-
return "grantId" in selector ? record.grantId === selector.grantId : record.addressId === selector.addressId;
|
|
4006
|
-
}
|
|
4007
|
-
function isLive(record, now) {
|
|
4008
|
-
return record.expiresAt > now && (record.kind === "access" || record.usedAt === undefined);
|
|
4009
|
-
}
|
|
4010
|
-
function summarizeGrants(records, now) {
|
|
4011
|
-
const grants = new Map;
|
|
4012
|
-
for (const r of records) {
|
|
4013
|
-
if (!isLive(r, now) || grants.has(r.grantId))
|
|
4014
|
-
continue;
|
|
4015
|
-
const summary = {
|
|
4016
|
-
grantId: r.grantId,
|
|
4017
|
-
clientId: r.clientId,
|
|
4018
|
-
clientName: r.clientName,
|
|
4019
|
-
scopes: [...r.scopes],
|
|
4020
|
-
createdAt: r.grantCreatedAt
|
|
4021
|
-
};
|
|
4022
|
-
if (r.subject !== undefined)
|
|
4023
|
-
summary.subject = r.subject;
|
|
4024
|
-
grants.set(r.grantId, summary);
|
|
4025
|
-
}
|
|
4026
|
-
return [...grants.values()].sort((a, b) => a.createdAt - b.createdAt || a.grantId.localeCompare(b.grantId));
|
|
4027
|
-
}
|
|
4028
|
-
|
|
4029
|
-
class MemoryTokenStore {
|
|
4030
|
-
#records = new Map;
|
|
4031
|
-
#revokedGrants = new Map;
|
|
4032
|
-
#revokedAddresses = new Map;
|
|
4033
|
-
async put(record) {
|
|
4034
|
-
if (this.#revokedGrants.has(record.grantId))
|
|
4035
|
-
return false;
|
|
4036
|
-
const address = this.#revokedAddresses.get(record.addressId);
|
|
4037
|
-
if (address !== undefined && record.grantCreatedAt <= address.at) {
|
|
4038
|
-
return false;
|
|
4039
|
-
}
|
|
4040
|
-
this.#records.set(record.hash, copy(record));
|
|
4041
|
-
return true;
|
|
4042
|
-
}
|
|
4043
|
-
async get(hash) {
|
|
4044
|
-
const r = this.#records.get(hash);
|
|
4045
|
-
return r === undefined ? undefined : copy(r);
|
|
4046
|
-
}
|
|
4047
|
-
async markUsed(hash, now, keepUntil) {
|
|
4048
|
-
const r = this.#records.get(hash);
|
|
4049
|
-
if (r === undefined || r.usedAt !== undefined)
|
|
4050
|
-
return false;
|
|
4051
|
-
r.usedAt = now;
|
|
4052
|
-
r.expiresAt = Math.min(r.expiresAt, keepUntil);
|
|
4053
|
-
return true;
|
|
4054
|
-
}
|
|
4055
|
-
async revoke(selector, options) {
|
|
4056
|
-
if ("grantId" in selector) {
|
|
4057
|
-
this.#revokedGrants.set(selector.grantId, options.keepUntil);
|
|
4058
|
-
} else {
|
|
4059
|
-
const old = this.#revokedAddresses.get(selector.addressId);
|
|
4060
|
-
this.#revokedAddresses.set(selector.addressId, {
|
|
4061
|
-
at: Math.max(old?.at ?? 0, options.now),
|
|
4062
|
-
until: Math.max(old?.until ?? 0, options.keepUntil)
|
|
4063
|
-
});
|
|
4064
|
-
}
|
|
4065
|
-
let n = 0;
|
|
4066
|
-
for (const [hash, r] of this.#records) {
|
|
4067
|
-
if (matches(r, selector)) {
|
|
4068
|
-
this.#records.delete(hash);
|
|
4069
|
-
n++;
|
|
4070
|
-
}
|
|
4071
|
-
}
|
|
4072
|
-
return n;
|
|
4073
|
-
}
|
|
4074
|
-
async listGrants(addressId, now) {
|
|
4075
|
-
const own = [...this.#records.values()].filter((r) => r.addressId === addressId);
|
|
4076
|
-
return summarizeGrants(own, now);
|
|
4077
|
-
}
|
|
4078
|
-
async deleteExpired(now) {
|
|
4079
|
-
for (const [id, until] of this.#revokedGrants) {
|
|
4080
|
-
if (until <= now)
|
|
4081
|
-
this.#revokedGrants.delete(id);
|
|
4082
|
-
}
|
|
4083
|
-
for (const [id, t] of this.#revokedAddresses) {
|
|
4084
|
-
if (t.until <= now)
|
|
4085
|
-
this.#revokedAddresses.delete(id);
|
|
4086
|
-
}
|
|
4087
|
-
let n = 0;
|
|
4088
|
-
for (const [hash, r] of this.#records) {
|
|
4089
|
-
if (r.expiresAt <= now) {
|
|
4090
|
-
this.#records.delete(hash);
|
|
4091
|
-
n++;
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
return n;
|
|
4095
|
-
}
|
|
4096
|
-
async close() {
|
|
4097
|
-
this.#records.clear();
|
|
4098
|
-
this.#revokedGrants.clear();
|
|
4099
|
-
this.#revokedAddresses.clear();
|
|
4100
|
-
}
|
|
4101
|
-
}
|
|
4102
|
-
|
|
4103
4643
|
// src/store/sqlite.ts
|
|
4104
4644
|
import { Database } from "bun:sqlite";
|
|
4105
|
-
var SCHEMA_VERSION =
|
|
4645
|
+
var SCHEMA_VERSION = 5;
|
|
4646
|
+
var MIN_READER_VERSION = 5;
|
|
4106
4647
|
var REUSE_WINDOW_MS = 86400000;
|
|
4107
4648
|
function toRecord(row) {
|
|
4108
4649
|
const record = {
|
|
@@ -4121,6 +4662,8 @@ function toRecord(row) {
|
|
|
4121
4662
|
record.usedAt = row.used_at;
|
|
4122
4663
|
if (row.subject !== null)
|
|
4123
4664
|
record.subject = row.subject;
|
|
4665
|
+
if (row.client_auth === "private_key_jwt")
|
|
4666
|
+
record.clientAuth = "private_key_jwt";
|
|
4124
4667
|
return record;
|
|
4125
4668
|
}
|
|
4126
4669
|
|
|
@@ -4131,7 +4674,24 @@ class SqliteTokenStore {
|
|
|
4131
4674
|
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
4132
4675
|
if (path !== ":memory:")
|
|
4133
4676
|
this.#db.run("PRAGMA journal_mode = WAL");
|
|
4677
|
+
this.#checkReader();
|
|
4134
4678
|
this.#migrate();
|
|
4679
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS meta (
|
|
4680
|
+
key TEXT PRIMARY KEY,
|
|
4681
|
+
value INTEGER NOT NULL
|
|
4682
|
+
) STRICT`);
|
|
4683
|
+
this.#db.query(`INSERT INTO meta (key, value) VALUES ('min_reader_version', $v)
|
|
4684
|
+
ON CONFLICT (key) DO UPDATE SET value = max(value, excluded.value)`).run({ v: MIN_READER_VERSION });
|
|
4685
|
+
}
|
|
4686
|
+
#checkReader() {
|
|
4687
|
+
const table = this.#db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
|
4688
|
+
if (table === null)
|
|
4689
|
+
return;
|
|
4690
|
+
const row = this.#db.query("SELECT value FROM meta WHERE key = 'min_reader_version'").get();
|
|
4691
|
+
if (row !== null && row.value > SCHEMA_VERSION) {
|
|
4692
|
+
this.#db.close();
|
|
4693
|
+
throw new Error(`the token store file needs a newer relay (schema ${row.value}; this relay knows ${SCHEMA_VERSION})`);
|
|
4694
|
+
}
|
|
4135
4695
|
}
|
|
4136
4696
|
#migrate() {
|
|
4137
4697
|
const row = this.#db.query("PRAGMA user_version").get();
|
|
@@ -4157,12 +4717,20 @@ class SqliteTokenStore {
|
|
|
4157
4717
|
issued_at INTEGER NOT NULL,
|
|
4158
4718
|
expires_at INTEGER NOT NULL,
|
|
4159
4719
|
used_at INTEGER,
|
|
4160
|
-
subject TEXT
|
|
4720
|
+
subject TEXT,
|
|
4721
|
+
client_auth TEXT
|
|
4161
4722
|
) STRICT`);
|
|
4162
4723
|
const columns = this.#db.query("PRAGMA table_info(tokens)").all();
|
|
4163
4724
|
if (!columns.some((c) => c.name === "subject")) {
|
|
4164
4725
|
this.#db.run("ALTER TABLE tokens ADD COLUMN subject TEXT");
|
|
4165
4726
|
}
|
|
4727
|
+
if (!columns.some((c) => c.name === "client_auth")) {
|
|
4728
|
+
this.#db.run("ALTER TABLE tokens ADD COLUMN client_auth TEXT");
|
|
4729
|
+
}
|
|
4730
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS nonces (
|
|
4731
|
+
key TEXT PRIMARY KEY,
|
|
4732
|
+
expires_at INTEGER NOT NULL
|
|
4733
|
+
) STRICT`);
|
|
4166
4734
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_grant ON tokens (grant_id)");
|
|
4167
4735
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_address ON tokens (address_id)");
|
|
4168
4736
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_expires ON tokens (expires_at)");
|
|
@@ -4187,9 +4755,11 @@ class SqliteTokenStore {
|
|
|
4187
4755
|
}
|
|
4188
4756
|
#insert(r) {
|
|
4189
4757
|
this.#db.query(`INSERT OR REPLACE INTO tokens (hash, kind, grant_id, address_id, client_id,
|
|
4190
|
-
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject
|
|
4758
|
+
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject,
|
|
4759
|
+
client_auth)
|
|
4191
4760
|
VALUES ($hash, $kind, $grant_id, $address_id, $client_id, $client_name,
|
|
4192
|
-
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject
|
|
4761
|
+
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject,
|
|
4762
|
+
$client_auth)`).run({
|
|
4193
4763
|
hash: r.hash,
|
|
4194
4764
|
kind: r.kind,
|
|
4195
4765
|
grant_id: r.grantId,
|
|
@@ -4201,7 +4771,8 @@ class SqliteTokenStore {
|
|
|
4201
4771
|
issued_at: r.issuedAt,
|
|
4202
4772
|
expires_at: r.expiresAt,
|
|
4203
4773
|
used_at: r.usedAt ?? null,
|
|
4204
|
-
subject: r.subject ?? null
|
|
4774
|
+
subject: r.subject ?? null,
|
|
4775
|
+
client_auth: r.clientAuth ?? null
|
|
4205
4776
|
});
|
|
4206
4777
|
}
|
|
4207
4778
|
async get(hash) {
|
|
@@ -4237,8 +4808,15 @@ class SqliteTokenStore {
|
|
|
4237
4808
|
}
|
|
4238
4809
|
async deleteExpired(now) {
|
|
4239
4810
|
this.#db.query("DELETE FROM revocations WHERE keep_until <= $now").run({ now });
|
|
4811
|
+
this.#db.query("DELETE FROM nonces WHERE expires_at <= $now").run({ now });
|
|
4240
4812
|
return this.#db.query("DELETE FROM tokens WHERE expires_at <= $now").run({ now }).changes;
|
|
4241
4813
|
}
|
|
4814
|
+
async claimNonce(key, expiresAt, now) {
|
|
4815
|
+
const result = this.#db.query(`INSERT INTO nonces (key, expires_at) VALUES ($key, $expires_at)
|
|
4816
|
+
ON CONFLICT (key) DO UPDATE SET expires_at = excluded.expires_at
|
|
4817
|
+
WHERE nonces.expires_at <= $now`).run({ key, expires_at: expiresAt, now });
|
|
4818
|
+
return result.changes === 1;
|
|
4819
|
+
}
|
|
4242
4820
|
async close() {
|
|
4243
4821
|
this.#db.close();
|
|
4244
4822
|
}
|
|
@@ -5206,5 +5784,5 @@ export {
|
|
|
5206
5784
|
resolveConfig
|
|
5207
5785
|
};
|
|
5208
5786
|
|
|
5209
|
-
//# debugId=
|
|
5787
|
+
//# debugId=61513056506259C164756E2164756E21
|
|
5210
5788
|
//# sourceMappingURL=index.js.map
|