@ractive-ch/tab-mcp 0.5.1 → 0.6.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/README.md +22 -3
- package/index.js +775 -170
- package/index.js.map +12 -10
- package/jwt.d.ts +10 -2
- package/main.js +775 -170
- package/main.js.map +12 -10
- 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/relay.d.ts +6 -0
- package/store/memory.d.ts +13 -0
- package/store/sqlite.d.ts +2 -0
- package/store/types.d.ts +17 -0
- package/testing.d.ts +33 -2
- package/testing.js +5863 -1
- package/testing.js.map +28 -4
- 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.1";
|
|
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_",
|
|
@@ -1699,6 +1848,26 @@ function newId(prefix) {
|
|
|
1699
1848
|
function hashToken(token) {
|
|
1700
1849
|
return new Bun.CryptoHasher("sha256").update(token).digest().toBase64({ alphabet: "base64url", omitPadding: true });
|
|
1701
1850
|
}
|
|
1851
|
+
async function issueAccessToken(store, grant, options) {
|
|
1852
|
+
const now = options.now ?? Date.now();
|
|
1853
|
+
const token = newToken("access");
|
|
1854
|
+
const record = {
|
|
1855
|
+
hash: hashToken(token),
|
|
1856
|
+
kind: "access",
|
|
1857
|
+
grantId: grant.grantId ?? newId("g"),
|
|
1858
|
+
addressId: grant.addressId,
|
|
1859
|
+
clientId: grant.clientId,
|
|
1860
|
+
clientName: grant.clientName,
|
|
1861
|
+
scopes: grant.scopes ?? ["tools"],
|
|
1862
|
+
grantCreatedAt: grant.grantCreatedAt ?? now,
|
|
1863
|
+
issuedAt: now,
|
|
1864
|
+
expiresAt: now + options.ttlMs
|
|
1865
|
+
};
|
|
1866
|
+
if (grant.subject !== undefined)
|
|
1867
|
+
record.subject = grant.subject;
|
|
1868
|
+
await store.put(record);
|
|
1869
|
+
return { token, record };
|
|
1870
|
+
}
|
|
1702
1871
|
async function issueTokenPair(store, grant, options) {
|
|
1703
1872
|
const now = options.now ?? Date.now();
|
|
1704
1873
|
const accessToken = newToken("access");
|
|
@@ -1711,7 +1880,8 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1711
1880
|
scopes: grant.scopes,
|
|
1712
1881
|
grantCreatedAt: grant.grantCreatedAt,
|
|
1713
1882
|
issuedAt: now,
|
|
1714
|
-
...grant.subject === undefined ? {} : { subject: grant.subject }
|
|
1883
|
+
...grant.subject === undefined ? {} : { subject: grant.subject },
|
|
1884
|
+
...grant.clientAuth === undefined ? {} : { clientAuth: grant.clientAuth }
|
|
1715
1885
|
};
|
|
1716
1886
|
const accessExpires = Math.min(now + options.accessTtlMs, options.grantEndsAt);
|
|
1717
1887
|
const refreshStored = await store.put({
|
|
@@ -1737,6 +1907,75 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1737
1907
|
};
|
|
1738
1908
|
}
|
|
1739
1909
|
|
|
1910
|
+
// src/oauth/domain-cache.ts
|
|
1911
|
+
class DomainCache {
|
|
1912
|
+
#entries = new Map;
|
|
1913
|
+
#counts = new Map;
|
|
1914
|
+
#max;
|
|
1915
|
+
#perDomain;
|
|
1916
|
+
#isDead;
|
|
1917
|
+
constructor(max, perDomain, isDead = () => false) {
|
|
1918
|
+
this.#max = max;
|
|
1919
|
+
this.#perDomain = perDomain;
|
|
1920
|
+
this.#isDead = isDead;
|
|
1921
|
+
}
|
|
1922
|
+
get size() {
|
|
1923
|
+
return this.#entries.size;
|
|
1924
|
+
}
|
|
1925
|
+
get(key) {
|
|
1926
|
+
const entry = this.#entries.get(key);
|
|
1927
|
+
if (entry === undefined)
|
|
1928
|
+
return;
|
|
1929
|
+
this.#entries.delete(key);
|
|
1930
|
+
this.#entries.set(key, entry);
|
|
1931
|
+
return entry.value;
|
|
1932
|
+
}
|
|
1933
|
+
delete(key) {
|
|
1934
|
+
const entry = this.#entries.get(key);
|
|
1935
|
+
if (entry === undefined)
|
|
1936
|
+
return;
|
|
1937
|
+
this.#entries.delete(key);
|
|
1938
|
+
const left = (this.#counts.get(entry.domain) ?? 1) - 1;
|
|
1939
|
+
if (left > 0)
|
|
1940
|
+
this.#counts.set(entry.domain, left);
|
|
1941
|
+
else
|
|
1942
|
+
this.#counts.delete(entry.domain);
|
|
1943
|
+
}
|
|
1944
|
+
set(key, domain, value, now) {
|
|
1945
|
+
this.delete(key);
|
|
1946
|
+
if ((this.#counts.get(domain) ?? 0) >= this.#perDomain) {
|
|
1947
|
+
this.#evictOldestOf(domain);
|
|
1948
|
+
} else if (this.#entries.size >= this.#max) {
|
|
1949
|
+
this.#evict(now);
|
|
1950
|
+
}
|
|
1951
|
+
this.#entries.set(key, { domain, value });
|
|
1952
|
+
this.#counts.set(domain, (this.#counts.get(domain) ?? 0) + 1);
|
|
1953
|
+
}
|
|
1954
|
+
#evict(now) {
|
|
1955
|
+
for (const [key, entry] of this.#entries) {
|
|
1956
|
+
if (this.#isDead(entry.value, now)) {
|
|
1957
|
+
this.delete(key);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
const most = Math.max(0, ...this.#counts.values());
|
|
1962
|
+
for (const [key, entry] of this.#entries) {
|
|
1963
|
+
if (this.#counts.get(entry.domain) === most) {
|
|
1964
|
+
this.delete(key);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
#evictOldestOf(domain) {
|
|
1970
|
+
for (const [key, entry] of this.#entries) {
|
|
1971
|
+
if (entry.domain === domain) {
|
|
1972
|
+
this.delete(key);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1740
1979
|
// src/oauth/cimd.ts
|
|
1741
1980
|
class ClientMetadataError extends Error {
|
|
1742
1981
|
name = "ClientMetadataError";
|
|
@@ -1746,6 +1985,16 @@ class ClientMetadataError extends Error {
|
|
|
1746
1985
|
this.reason = reason;
|
|
1747
1986
|
}
|
|
1748
1987
|
}
|
|
1988
|
+
var TRANSIENT_REASONS = new Set([
|
|
1989
|
+
"rate_limited",
|
|
1990
|
+
"fetch_failed",
|
|
1991
|
+
"unresolvable",
|
|
1992
|
+
"too_large"
|
|
1993
|
+
]);
|
|
1994
|
+
function isTransient(error) {
|
|
1995
|
+
return TRANSIENT_REASONS.has(error.reason);
|
|
1996
|
+
}
|
|
1997
|
+
var STALE_GRACE_MS = 60 * 60000;
|
|
1749
1998
|
var MAX_CLIENT_ID_LENGTH = 2048;
|
|
1750
1999
|
var MAX_NAME_LENGTH = 64;
|
|
1751
2000
|
var MAX_REDIRECT_URIS = 20;
|
|
@@ -1753,6 +2002,9 @@ var MIN_CACHE_MS = 5 * 60000;
|
|
|
1753
2002
|
var DEFAULT_CACHE_MS = 60 * 60000;
|
|
1754
2003
|
var MAX_CACHE_MS = 24 * 60 * 60000;
|
|
1755
2004
|
var MAX_CACHE_ENTRIES = 1000;
|
|
2005
|
+
var MAX_CACHE_PER_DOMAIN = 100;
|
|
2006
|
+
var FAILURE_CACHE_MS = 60000;
|
|
2007
|
+
var MAX_FAILURES = 1000;
|
|
1756
2008
|
async function defaultLookup(hostname) {
|
|
1757
2009
|
const records = await Bun.dns.lookup(hostname, {});
|
|
1758
2010
|
return records.map((r) => r.address);
|
|
@@ -1786,12 +2038,22 @@ function limitKey(hostname) {
|
|
|
1786
2038
|
return h;
|
|
1787
2039
|
return h.split(".").slice(-2).join(".");
|
|
1788
2040
|
}
|
|
2041
|
+
function knownBudget() {
|
|
2042
|
+
return {
|
|
2043
|
+
perHost: new RateLimiter({ capacity: 5, refillPerSecond: 0.2 }),
|
|
2044
|
+
perDomain: new RateLimiter({ capacity: 15, refillPerSecond: 0.6 }),
|
|
2045
|
+
global: new RateLimiter({ capacity: 100, refillPerSecond: 10 })
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
1789
2048
|
|
|
1790
2049
|
class ClientMetadataFetcher {
|
|
1791
|
-
#cache = new
|
|
2050
|
+
#cache = new DomainCache(MAX_CACHE_ENTRIES, MAX_CACHE_PER_DOMAIN, (entry, now) => entry.until + STALE_GRACE_MS <= now);
|
|
2051
|
+
#failures = new Map;
|
|
1792
2052
|
#perHost = new RateLimiter({ capacity: 5, refillPerSecond: 0.2 });
|
|
1793
2053
|
#perDomain = new RateLimiter({ capacity: 15, refillPerSecond: 0.6 });
|
|
1794
2054
|
#global = new RateLimiter({ capacity: 30, refillPerSecond: 1 });
|
|
2055
|
+
#refreshes = knownBudget();
|
|
2056
|
+
#keys = knownBudget();
|
|
1795
2057
|
#allowedHosts;
|
|
1796
2058
|
#logger;
|
|
1797
2059
|
#timeoutMs;
|
|
@@ -1810,34 +2072,106 @@ class ClientMetadataFetcher {
|
|
|
1810
2072
|
this.#testMode = options.testMode;
|
|
1811
2073
|
this.#now = options.now ?? Date.now;
|
|
1812
2074
|
}
|
|
1813
|
-
async get(clientId) {
|
|
2075
|
+
async get(clientId, options = {}) {
|
|
1814
2076
|
const url = this.#checkUrl(clientId);
|
|
1815
2077
|
const now = this.#now();
|
|
1816
2078
|
const cached = this.#cache.get(clientId);
|
|
1817
|
-
if (cached !== undefined && cached.until > now)
|
|
1818
|
-
this.#cache.delete(clientId);
|
|
1819
|
-
this.#cache.set(clientId, cached);
|
|
2079
|
+
if (cached !== undefined && cached.until > now)
|
|
1820
2080
|
return cached.meta;
|
|
2081
|
+
const known = cached !== undefined && now < cached.until + STALE_GRACE_MS;
|
|
2082
|
+
const stale = options.allowStale === true && known ? cached?.meta : undefined;
|
|
2083
|
+
try {
|
|
2084
|
+
const failed = this.#failures.get(clientId);
|
|
2085
|
+
if (failed !== undefined && failed.until > now)
|
|
2086
|
+
throw failed.error;
|
|
2087
|
+
return await this.#refresh(clientId, url, now, known);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
const transient = error instanceof ClientMetadataError && isTransient(error);
|
|
2090
|
+
if (!transient)
|
|
2091
|
+
this.#cache.delete(clientId);
|
|
2092
|
+
if (stale === undefined || !transient)
|
|
2093
|
+
throw error;
|
|
2094
|
+
this.#logger?.info("oauth.cimd_stale", {
|
|
2095
|
+
host: url.host,
|
|
2096
|
+
reason: error.reason
|
|
2097
|
+
});
|
|
2098
|
+
return stale;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
async#refresh(clientId, url, now, known) {
|
|
2102
|
+
this.#admit(url, now, known ? "refresh" : "new");
|
|
2103
|
+
let meta;
|
|
2104
|
+
let ttlMs;
|
|
2105
|
+
try {
|
|
2106
|
+
const address = await this.#vettedAddress(url);
|
|
2107
|
+
const response = await this.#fetchJson(url, address, [
|
|
2108
|
+
"application/json"
|
|
2109
|
+
]);
|
|
2110
|
+
meta = validateDocument(response.doc, clientId, url, {
|
|
2111
|
+
test: this.#testMode !== undefined,
|
|
2112
|
+
onFallback: (reason) => this.#logger?.info("oauth.cimd_auth_fallback", {
|
|
2113
|
+
host: url.host,
|
|
2114
|
+
reason
|
|
2115
|
+
})
|
|
2116
|
+
});
|
|
2117
|
+
ttlMs = cacheMs(response.cacheControl);
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
if (error instanceof ClientMetadataError) {
|
|
2120
|
+
this.#rememberFailure(clientId, error, now);
|
|
2121
|
+
}
|
|
2122
|
+
throw error;
|
|
1821
2123
|
}
|
|
2124
|
+
this.#failures.delete(clientId);
|
|
2125
|
+
this.#cache.set(clientId, limitKey(url.hostname), { meta, until: now + ttlMs }, now);
|
|
2126
|
+
return meta;
|
|
2127
|
+
}
|
|
2128
|
+
#rememberFailure(clientId, error, now) {
|
|
2129
|
+
this.#failures.delete(clientId);
|
|
2130
|
+
if (this.#failures.size >= MAX_FAILURES) {
|
|
2131
|
+
const oldest = this.#failures.keys().next().value;
|
|
2132
|
+
if (oldest !== undefined)
|
|
2133
|
+
this.#failures.delete(oldest);
|
|
2134
|
+
}
|
|
2135
|
+
this.#failures.set(clientId, { error, until: now + FAILURE_CACHE_MS });
|
|
2136
|
+
}
|
|
2137
|
+
async fetchJwks(jwksUri) {
|
|
2138
|
+
const url = this.#checkJwksUri(jwksUri);
|
|
2139
|
+
this.#admit(url, this.#now(), "keys");
|
|
2140
|
+
const address = await this.#vettedAddress(url);
|
|
2141
|
+
const response = await this.#fetchJson(url, address, [
|
|
2142
|
+
"application/json",
|
|
2143
|
+
"application/jwk-set+json"
|
|
2144
|
+
]);
|
|
2145
|
+
return response.doc;
|
|
2146
|
+
}
|
|
2147
|
+
#admit(url, now, budget) {
|
|
2148
|
+
const { perHost, perDomain, global } = budget === "refresh" ? this.#refreshes : budget === "keys" ? this.#keys : {
|
|
2149
|
+
perHost: this.#perHost,
|
|
2150
|
+
perDomain: this.#perDomain,
|
|
2151
|
+
global: this.#global
|
|
2152
|
+
};
|
|
1822
2153
|
const key = limitKey(url.hostname);
|
|
1823
|
-
if (!
|
|
2154
|
+
if (!perHost.available(url.host, now) || !perDomain.available(key, now)) {
|
|
1824
2155
|
throw new ClientMetadataError("rate_limited", "Too many requests for this AI app's details. Try again in a minute.");
|
|
1825
2156
|
}
|
|
1826
|
-
if (!
|
|
2157
|
+
if (!global.take("all", now)) {
|
|
1827
2158
|
throw new ClientMetadataError("rate_limited", "The relay is busy. Try again in a minute.");
|
|
1828
2159
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
this.#
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
if (oldest !== undefined)
|
|
1837
|
-
this.#cache.delete(oldest);
|
|
2160
|
+
perHost.take(url.host, now);
|
|
2161
|
+
perDomain.take(key, now);
|
|
2162
|
+
}
|
|
2163
|
+
#checkJwksUri(jwksUri) {
|
|
2164
|
+
const url = parseJwksUri(jwksUri, this.#testMode !== undefined);
|
|
2165
|
+
if (url === undefined) {
|
|
2166
|
+
throw new ClientMetadataError("invalid_jwks_uri", "The AI app's jwks_uri is not accepted.");
|
|
1838
2167
|
}
|
|
1839
|
-
this.#
|
|
1840
|
-
|
|
2168
|
+
if (this.#allowedHosts.length > 0 && !this.#allowedHosts.includes(url.host)) {
|
|
2169
|
+
throw new ClientMetadataError("host_not_allowed", `This relay does not accept AI apps from ${url.host}.`);
|
|
2170
|
+
}
|
|
2171
|
+
if (isIpLiteral(url.hostname) && !this.#addressAllowed(url.hostname)) {
|
|
2172
|
+
throw this.#notLoaded("private_address", url);
|
|
2173
|
+
}
|
|
2174
|
+
return url;
|
|
1841
2175
|
}
|
|
1842
2176
|
#checkUrl(clientId) {
|
|
1843
2177
|
const refuse = (message) => new ClientMetadataError("invalid_client_id", message);
|
|
@@ -1899,9 +2233,9 @@ class ClientMetadataFetcher {
|
|
|
1899
2233
|
}
|
|
1900
2234
|
return addresses.find((a) => parseIpv4(a) !== undefined) ?? addresses[0] ?? "";
|
|
1901
2235
|
}
|
|
1902
|
-
async#
|
|
2236
|
+
async#fetchJson(url, address, types) {
|
|
1903
2237
|
const fail = (detail) => this.#notLoaded("fetch_failed", url, detail);
|
|
1904
|
-
const pinned = new URL(
|
|
2238
|
+
const pinned = new URL(url.href);
|
|
1905
2239
|
pinned.hostname = address.includes(":") ? `[${address}]` : address;
|
|
1906
2240
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
1907
2241
|
const tls = url.protocol === "https:" ? {
|
|
@@ -1926,8 +2260,9 @@ class ClientMetadataFetcher {
|
|
|
1926
2260
|
if (response.status !== 200)
|
|
1927
2261
|
throw fail(`status_${response.status}`);
|
|
1928
2262
|
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1929
|
-
if (type
|
|
2263
|
+
if (type === undefined || !types.includes(type)) {
|
|
1930
2264
|
throw fail("not_json");
|
|
2265
|
+
}
|
|
1931
2266
|
const text = await this.#readLimited(response, url);
|
|
1932
2267
|
let doc;
|
|
1933
2268
|
try {
|
|
@@ -1935,8 +2270,7 @@ class ClientMetadataFetcher {
|
|
|
1935
2270
|
} catch {
|
|
1936
2271
|
throw fail("bad_json");
|
|
1937
2272
|
}
|
|
1938
|
-
|
|
1939
|
-
return { meta, ttlMs: cacheMs(response.headers.get("cache-control")) };
|
|
2273
|
+
return { doc, cacheControl: response.headers.get("cache-control") };
|
|
1940
2274
|
} finally {
|
|
1941
2275
|
if (!response.bodyUsed)
|
|
1942
2276
|
response.body?.cancel().catch(() => {});
|
|
@@ -1991,7 +2325,7 @@ function foreignDomain(name, hostname) {
|
|
|
1991
2325
|
}
|
|
1992
2326
|
return false;
|
|
1993
2327
|
}
|
|
1994
|
-
function validateDocument(doc, clientId, url) {
|
|
2328
|
+
function validateDocument(doc, clientId, url, options = {}) {
|
|
1995
2329
|
const invalid = (message) => new ClientMetadataError("invalid_metadata", message);
|
|
1996
2330
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
1997
2331
|
throw invalid("The AI app's details are not a JSON object.");
|
|
@@ -2017,11 +2351,7 @@ function validateDocument(doc, clientId, url) {
|
|
|
2017
2351
|
if (!Array.isArray(uris) || uris.length === 0 || uris.length > MAX_REDIRECT_URIS || !uris.every((u) => typeof u === "string" && isRedirectUri(u))) {
|
|
2018
2352
|
throw invalid("The AI app's details have no valid redirect_uris.");
|
|
2019
2353
|
}
|
|
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
|
-
}
|
|
2354
|
+
const auth = clientAuth(d, url, options, invalid);
|
|
2025
2355
|
const grants = d["grant_types"];
|
|
2026
2356
|
if (grants !== undefined && (!Array.isArray(grants) || !grants.includes("authorization_code"))) {
|
|
2027
2357
|
throw invalid("The AI app's details do not allow the authorization code grant.");
|
|
@@ -2034,9 +2364,81 @@ function validateDocument(doc, clientId, url) {
|
|
|
2034
2364
|
clientId,
|
|
2035
2365
|
name: displayName(name),
|
|
2036
2366
|
host: url.host,
|
|
2037
|
-
redirectUris: uris
|
|
2367
|
+
redirectUris: uris,
|
|
2368
|
+
auth
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
function clientAuth(d, url, options, invalid) {
|
|
2372
|
+
const method = d["token_endpoint_auth_method"];
|
|
2373
|
+
const supported = d["token_endpoint_auth_methods_supported"];
|
|
2374
|
+
const listsNone = Array.isArray(supported) && supported.includes("none");
|
|
2375
|
+
if (method === undefined || method === "none")
|
|
2376
|
+
return { method: "none" };
|
|
2377
|
+
const checked = method === "private_key_jwt" ? privateKeyJwt(d, url, options.test === true) : {
|
|
2378
|
+
problem: "unsupported_method",
|
|
2379
|
+
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.'
|
|
2380
|
+
};
|
|
2381
|
+
if ("jwksUri" in checked) {
|
|
2382
|
+
return { method: "private_key_jwt", ...checked, publicAllowed: listsNone };
|
|
2383
|
+
}
|
|
2384
|
+
if (listsNone) {
|
|
2385
|
+
options.onFallback?.(checked.problem);
|
|
2386
|
+
return { method: "none" };
|
|
2387
|
+
}
|
|
2388
|
+
throw invalid(checked.message);
|
|
2389
|
+
}
|
|
2390
|
+
function privateKeyJwt(d, url, test) {
|
|
2391
|
+
const jwksUri = d["jwks_uri"];
|
|
2392
|
+
if (jwksUri === undefined) {
|
|
2393
|
+
return {
|
|
2394
|
+
problem: "no_jwks_uri",
|
|
2395
|
+
message: "The AI app uses private_key_jwt but publishes no jwks_uri (an inline jwks is not accepted)."
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
if (d["jwks"] !== undefined) {
|
|
2399
|
+
return {
|
|
2400
|
+
problem: "jwks_and_jwks_uri",
|
|
2401
|
+
message: "The AI app's details have both jwks and jwks_uri."
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
const parsed = typeof jwksUri === "string" ? parseJwksUri(jwksUri, test) : undefined;
|
|
2405
|
+
if (parsed === undefined || parsed.protocol !== url.protocol || parsed.host !== url.host) {
|
|
2406
|
+
return {
|
|
2407
|
+
problem: "bad_jwks_uri",
|
|
2408
|
+
message: "The AI app's jwks_uri must be an https:// URL on the host of its client_id."
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const alg = d["token_endpoint_auth_signing_alg"];
|
|
2412
|
+
if (alg !== undefined && !isAlgorithm(alg)) {
|
|
2413
|
+
return {
|
|
2414
|
+
problem: "signing_alg",
|
|
2415
|
+
message: "The AI app's token_endpoint_auth_signing_alg is not one this relay accepts."
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
return {
|
|
2419
|
+
jwksUri: parsed.href,
|
|
2420
|
+
algorithms: alg === undefined ? JWT_ALGORITHMS : [alg]
|
|
2038
2421
|
};
|
|
2039
2422
|
}
|
|
2423
|
+
function parseJwksUri(value, test) {
|
|
2424
|
+
if (value.length > MAX_CLIENT_ID_LENGTH)
|
|
2425
|
+
return;
|
|
2426
|
+
let url;
|
|
2427
|
+
try {
|
|
2428
|
+
url = new URL(value);
|
|
2429
|
+
} catch {
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const scheme = url.protocol === "https:" || test && url.protocol === "http:";
|
|
2433
|
+
if (!scheme || url.href !== value)
|
|
2434
|
+
return;
|
|
2435
|
+
if (url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (url.port !== "" && !test)
|
|
2439
|
+
return;
|
|
2440
|
+
return url;
|
|
2441
|
+
}
|
|
2040
2442
|
var PRIVATE_USE_SCHEME = /^[a-z][a-z0-9+-]*(\.[a-z0-9+-]+)+:$/;
|
|
2041
2443
|
function isRedirectUri(value) {
|
|
2042
2444
|
if (value.length > 2048 || value.includes("#"))
|
|
@@ -2080,6 +2482,151 @@ function redirectUriMatches(requested, registered) {
|
|
|
2080
2482
|
return a.href === b.href;
|
|
2081
2483
|
}
|
|
2082
2484
|
|
|
2485
|
+
// src/oauth/client-auth.ts
|
|
2486
|
+
var CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
2487
|
+
var CLIENT_ASSERTION_MAX_LIFETIME_MS = 60 * 60000;
|
|
2488
|
+
var CLIENT_ASSERTION_MAX_AGE_MS = 5 * 60000;
|
|
2489
|
+
var CLIENT_ASSERTION_LEEWAY_MS = 30000;
|
|
2490
|
+
var MAX_JTI2 = 256;
|
|
2491
|
+
var MAX_KEY_SETS = 1000;
|
|
2492
|
+
var MAX_KEY_SETS_PER_DOMAIN = 100;
|
|
2493
|
+
var SWEEP_INTERVAL_MS = 1000;
|
|
2494
|
+
var ACCEPTED_TYPES = new Set(["jwt", "client-authentication+jwt"]);
|
|
2495
|
+
function assertionSubject(assertion) {
|
|
2496
|
+
try {
|
|
2497
|
+
const sub = decodeJwt(assertion).payload["sub"];
|
|
2498
|
+
return typeof sub === "string" && sub !== "" ? sub : undefined;
|
|
2499
|
+
} catch {
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
function nonceKey(clientId, jti) {
|
|
2504
|
+
return hashToken(JSON.stringify(["client_assertion", clientId, jti]));
|
|
2505
|
+
}
|
|
2506
|
+
async function verifyClientAssertion(assertion, check) {
|
|
2507
|
+
const { header, payload } = await verifyJwt(assertion, check.keys, {
|
|
2508
|
+
algorithms: check.auth.algorithms
|
|
2509
|
+
});
|
|
2510
|
+
const typ = header["typ"];
|
|
2511
|
+
if (typ !== undefined && (typeof typ !== "string" || !ACCEPTED_TYPES.has(typ.toLowerCase()))) {
|
|
2512
|
+
throw new JwtError("typ", "the assertion's type is not a client assertion");
|
|
2513
|
+
}
|
|
2514
|
+
if (payload["iss"] !== check.clientId) {
|
|
2515
|
+
throw new JwtError("iss", "the assertion's issuer is not the client");
|
|
2516
|
+
}
|
|
2517
|
+
if (payload["sub"] !== check.clientId) {
|
|
2518
|
+
throw new JwtError("sub", "the assertion's subject is not the client");
|
|
2519
|
+
}
|
|
2520
|
+
if (!audiences(payload).some((a) => check.audiences.includes(a))) {
|
|
2521
|
+
throw new JwtError("aud", "the assertion is not for this relay");
|
|
2522
|
+
}
|
|
2523
|
+
const usableUntil = checkTimes(payload, check.now);
|
|
2524
|
+
const jti = payload["jti"];
|
|
2525
|
+
if (typeof jti !== "string" || jti === "" || jti.length > MAX_JTI2) {
|
|
2526
|
+
throw new JwtError("jti", "the assertion needs a jti");
|
|
2527
|
+
}
|
|
2528
|
+
const key = nonceKey(check.clientId, jti);
|
|
2529
|
+
if (!await check.claimNonce(key, usableUntil, check.now)) {
|
|
2530
|
+
throw new JwtError("replayed", "the assertion was already used");
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
function checkTimes(payload, now) {
|
|
2534
|
+
const leeway = CLIENT_ASSERTION_LEEWAY_MS;
|
|
2535
|
+
const exp = numericDate(payload, "exp");
|
|
2536
|
+
if (exp === undefined)
|
|
2537
|
+
throw new JwtError("exp", "the assertion has no exp");
|
|
2538
|
+
if (exp <= now - leeway) {
|
|
2539
|
+
throw new JwtError("expired", "the assertion has expired");
|
|
2540
|
+
}
|
|
2541
|
+
if (exp > now + CLIENT_ASSERTION_MAX_LIFETIME_MS + leeway) {
|
|
2542
|
+
throw new JwtError("lifetime", "the assertion lives longer than an hour");
|
|
2543
|
+
}
|
|
2544
|
+
const iat = numericDate(payload, "iat");
|
|
2545
|
+
let end = exp;
|
|
2546
|
+
if (iat === undefined) {
|
|
2547
|
+
if (exp > now + CLIENT_ASSERTION_MAX_AGE_MS + leeway) {
|
|
2548
|
+
throw new JwtError("lifetime", "an assertion without iat may live five minutes");
|
|
2549
|
+
}
|
|
2550
|
+
} else {
|
|
2551
|
+
if (iat > now + leeway) {
|
|
2552
|
+
throw new JwtError("iat", "the assertion was issued in the future");
|
|
2553
|
+
}
|
|
2554
|
+
if (iat + CLIENT_ASSERTION_MAX_AGE_MS <= now - leeway) {
|
|
2555
|
+
throw new JwtError("too_old", "the assertion was issued too long ago");
|
|
2556
|
+
}
|
|
2557
|
+
end = Math.min(exp, iat + CLIENT_ASSERTION_MAX_AGE_MS);
|
|
2558
|
+
}
|
|
2559
|
+
const nbf = numericDate(payload, "nbf");
|
|
2560
|
+
if (nbf !== undefined && nbf > now + leeway) {
|
|
2561
|
+
throw new JwtError("nbf", "the assertion is not valid yet");
|
|
2562
|
+
}
|
|
2563
|
+
return end + leeway;
|
|
2564
|
+
}
|
|
2565
|
+
function quotaKey(clientId) {
|
|
2566
|
+
return ipPrefixKey(new URL(clientId).hostname.replace(/\.$/, ""));
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
class NonceQuota {
|
|
2570
|
+
#until = new Map;
|
|
2571
|
+
#perDomain;
|
|
2572
|
+
#max;
|
|
2573
|
+
#total = 0;
|
|
2574
|
+
#sweptAt = Number.NEGATIVE_INFINITY;
|
|
2575
|
+
constructor(perDomain, max) {
|
|
2576
|
+
this.#perDomain = perDomain;
|
|
2577
|
+
this.#max = max;
|
|
2578
|
+
}
|
|
2579
|
+
refusal(domain, now) {
|
|
2580
|
+
this.sweep(now);
|
|
2581
|
+
if ((this.#until.get(domain)?.length ?? 0) >= this.#perDomain) {
|
|
2582
|
+
return "domain_full";
|
|
2583
|
+
}
|
|
2584
|
+
return this.#total >= this.#max ? "full" : undefined;
|
|
2585
|
+
}
|
|
2586
|
+
add(domain, until) {
|
|
2587
|
+
const list = this.#until.get(domain);
|
|
2588
|
+
if (list === undefined)
|
|
2589
|
+
this.#until.set(domain, [until]);
|
|
2590
|
+
else
|
|
2591
|
+
list.push(until);
|
|
2592
|
+
this.#total++;
|
|
2593
|
+
}
|
|
2594
|
+
sweep(now) {
|
|
2595
|
+
if (now - this.#sweptAt < SWEEP_INTERVAL_MS)
|
|
2596
|
+
return false;
|
|
2597
|
+
this.#sweptAt = now;
|
|
2598
|
+
this.#total = 0;
|
|
2599
|
+
for (const [domain, list] of this.#until) {
|
|
2600
|
+
const live = list.filter((until) => until > now);
|
|
2601
|
+
if (live.length === 0)
|
|
2602
|
+
this.#until.delete(domain);
|
|
2603
|
+
else
|
|
2604
|
+
this.#until.set(domain, live);
|
|
2605
|
+
this.#total += live.length;
|
|
2606
|
+
}
|
|
2607
|
+
return true;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
class ClientKeySets {
|
|
2612
|
+
#sets = new DomainCache(MAX_KEY_SETS, MAX_KEY_SETS_PER_DOMAIN);
|
|
2613
|
+
#load;
|
|
2614
|
+
#now;
|
|
2615
|
+
constructor(load, now = Date.now) {
|
|
2616
|
+
this.#load = load;
|
|
2617
|
+
this.#now = now;
|
|
2618
|
+
}
|
|
2619
|
+
for(jwksUri) {
|
|
2620
|
+
const known = this.#sets.get(jwksUri);
|
|
2621
|
+
if (known !== undefined)
|
|
2622
|
+
return known;
|
|
2623
|
+
const set = new JwksCache(jwksUri, { load: this.#load, now: this.#now });
|
|
2624
|
+
const domain = limitKey(new URL(jwksUri).hostname);
|
|
2625
|
+
this.#sets.set(jwksUri, domain, set, this.#now());
|
|
2626
|
+
return set;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2083
2630
|
// src/oauth/html.ts
|
|
2084
2631
|
var ESCAPES = {
|
|
2085
2632
|
"&": "&",
|
|
@@ -2649,6 +3196,12 @@ var REFRESH_REUSE_WINDOW_MS = 24 * 60 * 60000;
|
|
|
2649
3196
|
var MAX_CODES = 1e4;
|
|
2650
3197
|
var MAX_FORM_BYTES = 8192;
|
|
2651
3198
|
var MAX_STATE_LENGTH = 1024;
|
|
3199
|
+
var MAX_NONCES_PER_HOST = 1e4;
|
|
3200
|
+
var MAX_NONCES = 1e5;
|
|
3201
|
+
var CLIENT_AUTH_FAILED = "client authentication failed";
|
|
3202
|
+
function assertionRequired(auth) {
|
|
3203
|
+
return auth.method === "private_key_jwt" && !auth.publicAllowed;
|
|
3204
|
+
}
|
|
2652
3205
|
|
|
2653
3206
|
class OAuthError extends Error {
|
|
2654
3207
|
status;
|
|
@@ -2698,10 +3251,16 @@ function hostOf(url) {
|
|
|
2698
3251
|
return url;
|
|
2699
3252
|
}
|
|
2700
3253
|
}
|
|
3254
|
+
function logHost(clientId) {
|
|
3255
|
+
return clientId !== undefined && URL.canParse(clientId) ? hostOf(clientId).slice(0, 100) : "(not a URL)";
|
|
3256
|
+
}
|
|
2701
3257
|
|
|
2702
3258
|
class AuthorizationServer {
|
|
2703
3259
|
#host;
|
|
2704
3260
|
#cimd;
|
|
3261
|
+
#clientKeys;
|
|
3262
|
+
#nonces = new NonceSet;
|
|
3263
|
+
#nonceQuota = new NonceQuota(MAX_NONCES_PER_HOST, MAX_NONCES);
|
|
2705
3264
|
#pending = new Map;
|
|
2706
3265
|
#pendingPerIp = new Map;
|
|
2707
3266
|
#pendingPerAddress = new Map;
|
|
@@ -2752,6 +3311,8 @@ class AuthorizationServer {
|
|
|
2752
3311
|
logger: host.logger,
|
|
2753
3312
|
...testMode?.cimd === undefined ? {} : { testMode: testMode.cimd }
|
|
2754
3313
|
});
|
|
3314
|
+
const cimd = this.#cimd;
|
|
3315
|
+
this.#clientKeys = new ClientKeySets((url) => cimd.fetchJwks(url));
|
|
2755
3316
|
}
|
|
2756
3317
|
get #issuer() {
|
|
2757
3318
|
return this.#host.publicUrl();
|
|
@@ -2805,8 +3366,10 @@ class AuthorizationServer {
|
|
|
2805
3366
|
response_modes_supported: ["query"],
|
|
2806
3367
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2807
3368
|
code_challenge_methods_supported: ["S256"],
|
|
2808
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
2809
|
-
|
|
3369
|
+
token_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3370
|
+
token_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
3371
|
+
revocation_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3372
|
+
revocation_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
2810
3373
|
scopes_supported: [SCOPE],
|
|
2811
3374
|
client_id_metadata_document_supported: true,
|
|
2812
3375
|
authorization_response_iss_parameter_supported: true,
|
|
@@ -3305,20 +3868,21 @@ ${cancel}
|
|
|
3305
3868
|
"code_verifier",
|
|
3306
3869
|
"refresh_token",
|
|
3307
3870
|
"resource",
|
|
3308
|
-
"scope"
|
|
3871
|
+
"scope",
|
|
3872
|
+
"client_assertion",
|
|
3873
|
+
"client_assertion_type"
|
|
3309
3874
|
]);
|
|
3310
3875
|
if ("duplicate" in p) {
|
|
3311
3876
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3312
3877
|
}
|
|
3313
|
-
const
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
}
|
|
3878
|
+
const client = await this.#authenticateClient(p, [
|
|
3879
|
+
`${this.#issuer}/token`
|
|
3880
|
+
]);
|
|
3317
3881
|
let body;
|
|
3318
3882
|
if (p["grant_type"] === "authorization_code") {
|
|
3319
|
-
body = await this.#codeGrant(p,
|
|
3883
|
+
body = await this.#codeGrant(p, client);
|
|
3320
3884
|
} else if (p["grant_type"] === "refresh_token") {
|
|
3321
|
-
body = await this.#refreshGrant(p,
|
|
3885
|
+
body = await this.#refreshGrant(p, client);
|
|
3322
3886
|
} else if (p["grant_type"] === undefined) {
|
|
3323
3887
|
throw new OAuthError(400, "invalid_request", "grant_type is required");
|
|
3324
3888
|
} else {
|
|
@@ -3352,19 +3916,96 @@ ${cancel}
|
|
|
3352
3916
|
throw new OAuthError(400, "invalid_request", "the body must be application/x-www-form-urlencoded");
|
|
3353
3917
|
}
|
|
3354
3918
|
if (req.headers.get("authorization") !== null) {
|
|
3355
|
-
throw new OAuthError(401, "invalid_client", "
|
|
3919
|
+
throw new OAuthError(401, "invalid_client", "HTTP authentication is not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3356
3920
|
}
|
|
3357
3921
|
const text = await this.#readForm(req);
|
|
3358
3922
|
if (text === undefined) {
|
|
3359
3923
|
throw new OAuthError(413, "invalid_request", "the request is too large");
|
|
3360
3924
|
}
|
|
3361
3925
|
const params = new URLSearchParams(text);
|
|
3362
|
-
if (params.has("client_secret")
|
|
3363
|
-
throw new OAuthError(400, "invalid_client", "client
|
|
3926
|
+
if (params.has("client_secret")) {
|
|
3927
|
+
throw new OAuthError(400, "invalid_client", "client secrets are not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3364
3928
|
}
|
|
3365
3929
|
return params;
|
|
3366
3930
|
}
|
|
3367
|
-
async#
|
|
3931
|
+
async#authenticateClient(p, endpoints) {
|
|
3932
|
+
const assertion = p["client_assertion"];
|
|
3933
|
+
const type = p["client_assertion_type"];
|
|
3934
|
+
if (assertion === undefined && type === undefined) {
|
|
3935
|
+
const clientId = p["client_id"];
|
|
3936
|
+
if (clientId === undefined) {
|
|
3937
|
+
throw new OAuthError(400, "invalid_request", "client_id is required");
|
|
3938
|
+
}
|
|
3939
|
+
return { clientId, assertion: false };
|
|
3940
|
+
}
|
|
3941
|
+
if (assertion === undefined || type === undefined) {
|
|
3942
|
+
throw new OAuthError(400, "invalid_request", "client_assertion and client_assertion_type go together");
|
|
3943
|
+
}
|
|
3944
|
+
const clientId = p["client_id"] ?? assertionSubject(assertion);
|
|
3945
|
+
if (type !== CLIENT_ASSERTION_TYPE) {
|
|
3946
|
+
throw this.#clientAuthFailed(clientId, "assertion_type");
|
|
3947
|
+
}
|
|
3948
|
+
if (clientId === undefined) {
|
|
3949
|
+
throw this.#clientAuthFailed(clientId, "no_client_id");
|
|
3950
|
+
}
|
|
3951
|
+
let client;
|
|
3952
|
+
try {
|
|
3953
|
+
client = await this.#cimd.get(clientId, { allowStale: true });
|
|
3954
|
+
} catch (error) {
|
|
3955
|
+
if (!(error instanceof ClientMetadataError))
|
|
3956
|
+
throw error;
|
|
3957
|
+
const reason = `metadata_${error.reason}`;
|
|
3958
|
+
throw isTransient(error) || error.reason === "private_address" ? this.#clientAuthUnavailable(clientId, reason) : this.#clientAuthFailed(clientId, reason);
|
|
3959
|
+
}
|
|
3960
|
+
if (client.auth.method !== "private_key_jwt") {
|
|
3961
|
+
throw this.#clientAuthFailed(clientId, "not_private_key_jwt");
|
|
3962
|
+
}
|
|
3963
|
+
const domain = quotaKey(clientId);
|
|
3964
|
+
try {
|
|
3965
|
+
await verifyClientAssertion(assertion, {
|
|
3966
|
+
clientId,
|
|
3967
|
+
auth: client.auth,
|
|
3968
|
+
keys: this.#clientKeys.for(client.auth.jwksUri),
|
|
3969
|
+
audiences: [this.#issuer, ...endpoints],
|
|
3970
|
+
claimNonce: (key, until, now) => this.#claimNonce(clientId, domain, key, until, now),
|
|
3971
|
+
now: Date.now()
|
|
3972
|
+
});
|
|
3973
|
+
} catch (error) {
|
|
3974
|
+
if (!(error instanceof JwtError))
|
|
3975
|
+
throw error;
|
|
3976
|
+
throw error.reason === "jwks_unavailable" ? this.#clientAuthUnavailable(clientId, error.reason) : this.#clientAuthFailed(clientId, error.reason);
|
|
3977
|
+
}
|
|
3978
|
+
return { clientId, assertion: true };
|
|
3979
|
+
}
|
|
3980
|
+
#clientAuthFailed(clientId, reason) {
|
|
3981
|
+
this.#host.logger.info("oauth.client_auth_failed", {
|
|
3982
|
+
client: logHost(clientId),
|
|
3983
|
+
reason
|
|
3984
|
+
});
|
|
3985
|
+
return new OAuthError(401, "invalid_client", CLIENT_AUTH_FAILED);
|
|
3986
|
+
}
|
|
3987
|
+
#clientAuthUnavailable(clientId, reason) {
|
|
3988
|
+
this.#host.logger.warn("oauth.client_auth_unavailable", {
|
|
3989
|
+
client: logHost(clientId),
|
|
3990
|
+
reason
|
|
3991
|
+
});
|
|
3992
|
+
return new OAuthError(503, "temporarily_unavailable", "the client cannot be authenticated right now; try again shortly");
|
|
3993
|
+
}
|
|
3994
|
+
async#claimNonce(clientId, domain, key, until, now) {
|
|
3995
|
+
if (this.#nonceQuota.sweep(now))
|
|
3996
|
+
this.#nonces.sweep(now);
|
|
3997
|
+
const refusal = this.#nonceQuota.refusal(domain, now);
|
|
3998
|
+
if (refusal !== undefined) {
|
|
3999
|
+
throw this.#clientAuthUnavailable(clientId, `nonces_${refusal}`);
|
|
4000
|
+
}
|
|
4001
|
+
const store = this.#host.store;
|
|
4002
|
+
const claimed = store.claimNonce === undefined ? this.#nonces.claim(key, until, now) : await store.claimNonce(key, until, now);
|
|
4003
|
+
if (claimed)
|
|
4004
|
+
this.#nonceQuota.add(domain, until);
|
|
4005
|
+
return claimed;
|
|
4006
|
+
}
|
|
4007
|
+
async#codeGrant(p, client) {
|
|
4008
|
+
const clientId = client.clientId;
|
|
3368
4009
|
const code = p["code"];
|
|
3369
4010
|
const redirectUri = p["redirect_uri"];
|
|
3370
4011
|
const verifier = p["code_verifier"];
|
|
@@ -3376,6 +4017,12 @@ ${cancel}
|
|
|
3376
4017
|
const invalid = (why) => new OAuthError(400, "invalid_grant", why);
|
|
3377
4018
|
if (issued === undefined)
|
|
3378
4019
|
throw invalid("the code is not valid");
|
|
4020
|
+
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
4021
|
+
throw invalid("the code was issued to another client");
|
|
4022
|
+
}
|
|
4023
|
+
if (assertionRequired(issued.client.auth) && !client.assertion) {
|
|
4024
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4025
|
+
}
|
|
3379
4026
|
if (issued.used) {
|
|
3380
4027
|
if (issued.grantId !== undefined) {
|
|
3381
4028
|
await this.#revokeGrant(issued.addressId, issued.grantId, "reuse_detected");
|
|
@@ -3385,9 +4032,6 @@ ${cancel}
|
|
|
3385
4032
|
issued.used = true;
|
|
3386
4033
|
if (issued.expiresAt <= Date.now())
|
|
3387
4034
|
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
4035
|
if (redirectUri !== issued.redirectUri) {
|
|
3392
4036
|
throw invalid("redirect_uri does not match the authorization request");
|
|
3393
4037
|
}
|
|
@@ -3411,7 +4055,8 @@ ${cancel}
|
|
|
3411
4055
|
clientName: issued.client.name,
|
|
3412
4056
|
scopes: issued.scopes,
|
|
3413
4057
|
grantCreatedAt: now,
|
|
3414
|
-
...issued.subject === undefined ? {} : { subject: issued.subject }
|
|
4058
|
+
...issued.subject === undefined ? {} : { subject: issued.subject },
|
|
4059
|
+
...client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3415
4060
|
}, {
|
|
3416
4061
|
accessTtlMs: config.accessTokenTtlMs,
|
|
3417
4062
|
refreshTtlMs: config.refreshTokenTtlMs,
|
|
@@ -3445,7 +4090,8 @@ ${cancel}
|
|
|
3445
4090
|
scope: scopes.join(" ")
|
|
3446
4091
|
};
|
|
3447
4092
|
}
|
|
3448
|
-
async#refreshGrant(p,
|
|
4093
|
+
async#refreshGrant(p, client) {
|
|
4094
|
+
const clientId = client.clientId;
|
|
3449
4095
|
const token = p["refresh_token"];
|
|
3450
4096
|
if (token === undefined) {
|
|
3451
4097
|
throw new OAuthError(400, "invalid_request", "refresh_token is required");
|
|
@@ -3459,6 +4105,9 @@ ${cancel}
|
|
|
3459
4105
|
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
3460
4106
|
throw invalid("the refresh token was issued to another client");
|
|
3461
4107
|
}
|
|
4108
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4109
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4110
|
+
}
|
|
3462
4111
|
const now = Date.now();
|
|
3463
4112
|
if (record.expiresAt <= now)
|
|
3464
4113
|
throw invalid("the refresh token has expired");
|
|
@@ -3490,7 +4139,8 @@ ${cancel}
|
|
|
3490
4139
|
clientName: record.clientName,
|
|
3491
4140
|
scopes: record.scopes,
|
|
3492
4141
|
grantCreatedAt: record.grantCreatedAt,
|
|
3493
|
-
...record.subject === undefined ? {} : { subject: record.subject }
|
|
4142
|
+
...record.subject === undefined ? {} : { subject: record.subject },
|
|
4143
|
+
...record.clientAuth === "private_key_jwt" || client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3494
4144
|
}, {
|
|
3495
4145
|
accessTtlMs: this.#host.config.accessTokenTtlMs,
|
|
3496
4146
|
refreshTtlMs: this.#host.config.refreshTokenTtlMs,
|
|
@@ -3526,24 +4176,36 @@ ${cancel}
|
|
|
3526
4176
|
throw new OAuthError(429, "temporarily_unavailable", "too many requests; try again shortly");
|
|
3527
4177
|
}
|
|
3528
4178
|
const params = await this.#formParams(req);
|
|
3529
|
-
const p = single(params, [
|
|
4179
|
+
const p = single(params, [
|
|
4180
|
+
"token",
|
|
4181
|
+
"client_id",
|
|
4182
|
+
"token_type_hint",
|
|
4183
|
+
"client_assertion",
|
|
4184
|
+
"client_assertion_type"
|
|
4185
|
+
]);
|
|
3530
4186
|
if ("duplicate" in p) {
|
|
3531
4187
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3532
4188
|
}
|
|
3533
4189
|
const token = p["token"];
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
throw new OAuthError(400, "invalid_request", "token and client_id are required");
|
|
4190
|
+
if (token === undefined) {
|
|
4191
|
+
throw new OAuthError(400, "invalid_request", "token is required");
|
|
3537
4192
|
}
|
|
4193
|
+
const client = await this.#authenticateClient(p, [
|
|
4194
|
+
`${this.#issuer}/revoke`,
|
|
4195
|
+
`${this.#issuer}/token`
|
|
4196
|
+
]);
|
|
3538
4197
|
const record = await this.#host.store.get(hashToken(token));
|
|
3539
4198
|
if (record === undefined)
|
|
3540
4199
|
return new Response(null, {
|
|
3541
4200
|
status: 200,
|
|
3542
4201
|
headers: { ...this.#cors(), ...noStore }
|
|
3543
4202
|
});
|
|
3544
|
-
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
4203
|
+
if (!constantTimeEqual(client.clientId, record.clientId)) {
|
|
3545
4204
|
throw new OAuthError(400, "invalid_client", "the token was issued to another client");
|
|
3546
4205
|
}
|
|
4206
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4207
|
+
throw this.#clientAuthFailed(client.clientId, "assertion_required");
|
|
4208
|
+
}
|
|
3547
4209
|
await this.#revokeGrant(record.addressId, record.grantId, "revoked");
|
|
3548
4210
|
this.#host.logger.info("oauth.revoked", {
|
|
3549
4211
|
address: shortAddress(record.addressId),
|
|
@@ -3589,6 +4251,7 @@ ${cancel}
|
|
|
3589
4251
|
this.#guessesPerAddress.sweep(now);
|
|
3590
4252
|
this.#tokenPerIp.sweep(now);
|
|
3591
4253
|
this.#refreshesPerGrant.sweep(now);
|
|
4254
|
+
this.#nonces.sweep(now);
|
|
3592
4255
|
}
|
|
3593
4256
|
}
|
|
3594
4257
|
|
|
@@ -3997,112 +4660,10 @@ class TabSession {
|
|
|
3997
4660
|
}
|
|
3998
4661
|
}
|
|
3999
4662
|
|
|
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
4663
|
// src/store/sqlite.ts
|
|
4104
4664
|
import { Database } from "bun:sqlite";
|
|
4105
|
-
var SCHEMA_VERSION =
|
|
4665
|
+
var SCHEMA_VERSION = 5;
|
|
4666
|
+
var MIN_READER_VERSION = 5;
|
|
4106
4667
|
var REUSE_WINDOW_MS = 86400000;
|
|
4107
4668
|
function toRecord(row) {
|
|
4108
4669
|
const record = {
|
|
@@ -4121,6 +4682,8 @@ function toRecord(row) {
|
|
|
4121
4682
|
record.usedAt = row.used_at;
|
|
4122
4683
|
if (row.subject !== null)
|
|
4123
4684
|
record.subject = row.subject;
|
|
4685
|
+
if (row.client_auth === "private_key_jwt")
|
|
4686
|
+
record.clientAuth = "private_key_jwt";
|
|
4124
4687
|
return record;
|
|
4125
4688
|
}
|
|
4126
4689
|
|
|
@@ -4131,7 +4694,24 @@ class SqliteTokenStore {
|
|
|
4131
4694
|
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
4132
4695
|
if (path !== ":memory:")
|
|
4133
4696
|
this.#db.run("PRAGMA journal_mode = WAL");
|
|
4697
|
+
this.#checkReader();
|
|
4134
4698
|
this.#migrate();
|
|
4699
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS meta (
|
|
4700
|
+
key TEXT PRIMARY KEY,
|
|
4701
|
+
value INTEGER NOT NULL
|
|
4702
|
+
) STRICT`);
|
|
4703
|
+
this.#db.query(`INSERT INTO meta (key, value) VALUES ('min_reader_version', $v)
|
|
4704
|
+
ON CONFLICT (key) DO UPDATE SET value = max(value, excluded.value)`).run({ v: MIN_READER_VERSION });
|
|
4705
|
+
}
|
|
4706
|
+
#checkReader() {
|
|
4707
|
+
const table = this.#db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
|
4708
|
+
if (table === null)
|
|
4709
|
+
return;
|
|
4710
|
+
const row = this.#db.query("SELECT value FROM meta WHERE key = 'min_reader_version'").get();
|
|
4711
|
+
if (row !== null && row.value > SCHEMA_VERSION) {
|
|
4712
|
+
this.#db.close();
|
|
4713
|
+
throw new Error(`the token store file needs a newer relay (schema ${row.value}; this relay knows ${SCHEMA_VERSION})`);
|
|
4714
|
+
}
|
|
4135
4715
|
}
|
|
4136
4716
|
#migrate() {
|
|
4137
4717
|
const row = this.#db.query("PRAGMA user_version").get();
|
|
@@ -4157,12 +4737,20 @@ class SqliteTokenStore {
|
|
|
4157
4737
|
issued_at INTEGER NOT NULL,
|
|
4158
4738
|
expires_at INTEGER NOT NULL,
|
|
4159
4739
|
used_at INTEGER,
|
|
4160
|
-
subject TEXT
|
|
4740
|
+
subject TEXT,
|
|
4741
|
+
client_auth TEXT
|
|
4161
4742
|
) STRICT`);
|
|
4162
4743
|
const columns = this.#db.query("PRAGMA table_info(tokens)").all();
|
|
4163
4744
|
if (!columns.some((c) => c.name === "subject")) {
|
|
4164
4745
|
this.#db.run("ALTER TABLE tokens ADD COLUMN subject TEXT");
|
|
4165
4746
|
}
|
|
4747
|
+
if (!columns.some((c) => c.name === "client_auth")) {
|
|
4748
|
+
this.#db.run("ALTER TABLE tokens ADD COLUMN client_auth TEXT");
|
|
4749
|
+
}
|
|
4750
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS nonces (
|
|
4751
|
+
key TEXT PRIMARY KEY,
|
|
4752
|
+
expires_at INTEGER NOT NULL
|
|
4753
|
+
) STRICT`);
|
|
4166
4754
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_grant ON tokens (grant_id)");
|
|
4167
4755
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_address ON tokens (address_id)");
|
|
4168
4756
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_expires ON tokens (expires_at)");
|
|
@@ -4187,9 +4775,11 @@ class SqliteTokenStore {
|
|
|
4187
4775
|
}
|
|
4188
4776
|
#insert(r) {
|
|
4189
4777
|
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
|
|
4778
|
+
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject,
|
|
4779
|
+
client_auth)
|
|
4191
4780
|
VALUES ($hash, $kind, $grant_id, $address_id, $client_id, $client_name,
|
|
4192
|
-
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject
|
|
4781
|
+
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject,
|
|
4782
|
+
$client_auth)`).run({
|
|
4193
4783
|
hash: r.hash,
|
|
4194
4784
|
kind: r.kind,
|
|
4195
4785
|
grant_id: r.grantId,
|
|
@@ -4201,7 +4791,8 @@ class SqliteTokenStore {
|
|
|
4201
4791
|
issued_at: r.issuedAt,
|
|
4202
4792
|
expires_at: r.expiresAt,
|
|
4203
4793
|
used_at: r.usedAt ?? null,
|
|
4204
|
-
subject: r.subject ?? null
|
|
4794
|
+
subject: r.subject ?? null,
|
|
4795
|
+
client_auth: r.clientAuth ?? null
|
|
4205
4796
|
});
|
|
4206
4797
|
}
|
|
4207
4798
|
async get(hash) {
|
|
@@ -4237,8 +4828,15 @@ class SqliteTokenStore {
|
|
|
4237
4828
|
}
|
|
4238
4829
|
async deleteExpired(now) {
|
|
4239
4830
|
this.#db.query("DELETE FROM revocations WHERE keep_until <= $now").run({ now });
|
|
4831
|
+
this.#db.query("DELETE FROM nonces WHERE expires_at <= $now").run({ now });
|
|
4240
4832
|
return this.#db.query("DELETE FROM tokens WHERE expires_at <= $now").run({ now }).changes;
|
|
4241
4833
|
}
|
|
4834
|
+
async claimNonce(key, expiresAt, now) {
|
|
4835
|
+
const result = this.#db.query(`INSERT INTO nonces (key, expires_at) VALUES ($key, $expires_at)
|
|
4836
|
+
ON CONFLICT (key) DO UPDATE SET expires_at = excluded.expires_at
|
|
4837
|
+
WHERE nonces.expires_at <= $now`).run({ key, expires_at: expiresAt, now });
|
|
4838
|
+
return result.changes === 1;
|
|
4839
|
+
}
|
|
4242
4840
|
async close() {
|
|
4243
4841
|
this.#db.close();
|
|
4244
4842
|
}
|
|
@@ -4267,6 +4865,13 @@ var FORWARDED_HEADERS = new Set([
|
|
|
4267
4865
|
"traceparent",
|
|
4268
4866
|
"tracestate"
|
|
4269
4867
|
]);
|
|
4868
|
+
function developmentUrl(host, port) {
|
|
4869
|
+
if (host === "127.0.0.1")
|
|
4870
|
+
return `http://127.0.0.1:${port}`;
|
|
4871
|
+
if (host === "::1" || host === "[::1]")
|
|
4872
|
+
return `http://[::1]:${port}`;
|
|
4873
|
+
return `http://localhost:${port}`;
|
|
4874
|
+
}
|
|
4270
4875
|
var HEALTH_STORE_TIMEOUT_MS = 1000;
|
|
4271
4876
|
var HEALTH_REUSE_MS = 1000;
|
|
4272
4877
|
var HEALTH_HUNG_RETRY_MS = 30000;
|
|
@@ -5133,7 +5738,7 @@ function startRelay(options, hooks) {
|
|
|
5133
5738
|
let closing = false;
|
|
5134
5739
|
const port = server.port ?? config.port;
|
|
5135
5740
|
if (publicUrl === "")
|
|
5136
|
-
publicUrl =
|
|
5741
|
+
publicUrl = developmentUrl(config.host, port);
|
|
5137
5742
|
const sweep = setInterval(() => {
|
|
5138
5743
|
auth.sweep();
|
|
5139
5744
|
store.deleteExpired(Date.now()).then((n) => {
|
|
@@ -5206,5 +5811,5 @@ export {
|
|
|
5206
5811
|
resolveConfig
|
|
5207
5812
|
};
|
|
5208
5813
|
|
|
5209
|
-
//# debugId=
|
|
5814
|
+
//# debugId=F0E5B49004089DE664756E2164756E21
|
|
5210
5815
|
//# sourceMappingURL=index.js.map
|