@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/main.js
CHANGED
|
@@ -603,7 +603,7 @@ function installCrashLogging(logger, exit = (code) => process.exit(code)) {
|
|
|
603
603
|
};
|
|
604
604
|
}
|
|
605
605
|
// package.json
|
|
606
|
-
var version = "0.
|
|
606
|
+
var version = "0.6.0";
|
|
607
607
|
|
|
608
608
|
// src/address.ts
|
|
609
609
|
var BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
@@ -1008,6 +1008,7 @@ var MAX_JWT_BYTES = 8192;
|
|
|
1008
1008
|
var MAX_JWKS_BYTES = 65536;
|
|
1009
1009
|
var JWKS_TTL_MS = 10 * 60000;
|
|
1010
1010
|
var JWKS_REFETCH_MS = 30000;
|
|
1011
|
+
var UNKNOWN_KID_REFETCH_MS = 5000;
|
|
1011
1012
|
var JWKS_GRACE_MS = 60 * 60000;
|
|
1012
1013
|
var FETCH_TIMEOUT_MS = 5000;
|
|
1013
1014
|
|
|
@@ -1172,6 +1173,7 @@ class HttpJsonError extends Error {
|
|
|
1172
1173
|
class JwksCache {
|
|
1173
1174
|
#url;
|
|
1174
1175
|
#now;
|
|
1176
|
+
#load;
|
|
1175
1177
|
#keys = [];
|
|
1176
1178
|
#fetchedAt = 0;
|
|
1177
1179
|
#attemptedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -1180,14 +1182,17 @@ class JwksCache {
|
|
|
1180
1182
|
constructor(url, options = {}) {
|
|
1181
1183
|
this.#url = typeof url === "string" ? () => Promise.resolve(url) : url;
|
|
1182
1184
|
this.#now = options.now ?? Date.now;
|
|
1185
|
+
this.#load = options.load ?? ((u) => fetchJson(u));
|
|
1183
1186
|
}
|
|
1184
1187
|
async keys(kid) {
|
|
1188
|
+
const entered = this.#now();
|
|
1185
1189
|
if (this.#inflight !== undefined)
|
|
1186
1190
|
await this.#inflight.catch(() => {});
|
|
1187
1191
|
const now = this.#now();
|
|
1188
1192
|
const stale = now - this.#fetchedAt >= JWKS_TTL_MS;
|
|
1189
|
-
const unknown = kid !== undefined && !this.#
|
|
1190
|
-
|
|
1193
|
+
const unknown = kid !== undefined && !this.#has(kid);
|
|
1194
|
+
const floor = unknown && this.#lastError === undefined ? UNKNOWN_KID_REFETCH_MS : JWKS_REFETCH_MS;
|
|
1195
|
+
if ((stale || unknown) && now - this.#attemptedAt >= floor) {
|
|
1191
1196
|
try {
|
|
1192
1197
|
await this.#refresh();
|
|
1193
1198
|
} catch (error) {
|
|
@@ -1197,13 +1202,21 @@ class JwksCache {
|
|
|
1197
1202
|
if (this.#now() - this.#fetchedAt >= JWKS_TTL_MS + JWKS_GRACE_MS) {
|
|
1198
1203
|
throw this.#lastError ?? new Error("the JWKS could not be loaded");
|
|
1199
1204
|
}
|
|
1200
|
-
|
|
1205
|
+
if (kid === undefined)
|
|
1206
|
+
return this.#keys;
|
|
1207
|
+
if (!this.#has(kid) && this.#fetchedAt < entered) {
|
|
1208
|
+
throw this.#lastError ?? new Error("an unknown key id; the JWKS was fetched too recently");
|
|
1209
|
+
}
|
|
1210
|
+
return this.#keys.filter((k) => k.kid === kid);
|
|
1211
|
+
}
|
|
1212
|
+
#has(kid) {
|
|
1213
|
+
return this.#keys.some((k) => k.kid === kid);
|
|
1201
1214
|
}
|
|
1202
1215
|
#refresh() {
|
|
1203
1216
|
this.#inflight ??= (async () => {
|
|
1204
1217
|
this.#attemptedAt = this.#now();
|
|
1205
1218
|
try {
|
|
1206
|
-
const doc = await
|
|
1219
|
+
const doc = await this.#load(await this.#url());
|
|
1207
1220
|
const keys = doc?.keys;
|
|
1208
1221
|
if (!Array.isArray(keys))
|
|
1209
1222
|
throw new Error("a JWKS needs keys");
|
|
@@ -1688,6 +1701,142 @@ class RateLimiter {
|
|
|
1688
1701
|
}
|
|
1689
1702
|
}
|
|
1690
1703
|
|
|
1704
|
+
// src/store/memory.ts
|
|
1705
|
+
function copy(record) {
|
|
1706
|
+
return { ...record, scopes: [...record.scopes] };
|
|
1707
|
+
}
|
|
1708
|
+
function matches(record, selector) {
|
|
1709
|
+
return "grantId" in selector ? record.grantId === selector.grantId : record.addressId === selector.addressId;
|
|
1710
|
+
}
|
|
1711
|
+
function isLive(record, now) {
|
|
1712
|
+
return record.expiresAt > now && (record.kind === "access" || record.usedAt === undefined);
|
|
1713
|
+
}
|
|
1714
|
+
function summarizeGrants(records, now) {
|
|
1715
|
+
const grants = new Map;
|
|
1716
|
+
for (const r of records) {
|
|
1717
|
+
if (!isLive(r, now) || grants.has(r.grantId))
|
|
1718
|
+
continue;
|
|
1719
|
+
const summary = {
|
|
1720
|
+
grantId: r.grantId,
|
|
1721
|
+
clientId: r.clientId,
|
|
1722
|
+
clientName: r.clientName,
|
|
1723
|
+
scopes: [...r.scopes],
|
|
1724
|
+
createdAt: r.grantCreatedAt
|
|
1725
|
+
};
|
|
1726
|
+
if (r.subject !== undefined)
|
|
1727
|
+
summary.subject = r.subject;
|
|
1728
|
+
grants.set(r.grantId, summary);
|
|
1729
|
+
}
|
|
1730
|
+
return [...grants.values()].sort((a, b) => a.createdAt - b.createdAt || a.grantId.localeCompare(b.grantId));
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
class NonceSet {
|
|
1734
|
+
#until = new Map;
|
|
1735
|
+
get size() {
|
|
1736
|
+
return this.#until.size;
|
|
1737
|
+
}
|
|
1738
|
+
claim(key, expiresAt, now) {
|
|
1739
|
+
const until = this.#until.get(key);
|
|
1740
|
+
if (until !== undefined && until > now)
|
|
1741
|
+
return false;
|
|
1742
|
+
this.#until.set(key, expiresAt);
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
sweep(now) {
|
|
1746
|
+
let n = 0;
|
|
1747
|
+
for (const [key, until] of this.#until) {
|
|
1748
|
+
if (until <= now) {
|
|
1749
|
+
this.#until.delete(key);
|
|
1750
|
+
n++;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return n;
|
|
1754
|
+
}
|
|
1755
|
+
clear() {
|
|
1756
|
+
this.#until.clear();
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
class MemoryTokenStore {
|
|
1761
|
+
#records = new Map;
|
|
1762
|
+
#nonces = new NonceSet;
|
|
1763
|
+
#revokedGrants = new Map;
|
|
1764
|
+
#revokedAddresses = new Map;
|
|
1765
|
+
async put(record) {
|
|
1766
|
+
if (this.#revokedGrants.has(record.grantId))
|
|
1767
|
+
return false;
|
|
1768
|
+
const address = this.#revokedAddresses.get(record.addressId);
|
|
1769
|
+
if (address !== undefined && record.grantCreatedAt <= address.at) {
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
this.#records.set(record.hash, copy(record));
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
async get(hash) {
|
|
1776
|
+
const r = this.#records.get(hash);
|
|
1777
|
+
return r === undefined ? undefined : copy(r);
|
|
1778
|
+
}
|
|
1779
|
+
async markUsed(hash, now, keepUntil) {
|
|
1780
|
+
const r = this.#records.get(hash);
|
|
1781
|
+
if (r === undefined || r.usedAt !== undefined)
|
|
1782
|
+
return false;
|
|
1783
|
+
r.usedAt = now;
|
|
1784
|
+
r.expiresAt = Math.min(r.expiresAt, keepUntil);
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
async revoke(selector, options) {
|
|
1788
|
+
if ("grantId" in selector) {
|
|
1789
|
+
this.#revokedGrants.set(selector.grantId, options.keepUntil);
|
|
1790
|
+
} else {
|
|
1791
|
+
const old = this.#revokedAddresses.get(selector.addressId);
|
|
1792
|
+
this.#revokedAddresses.set(selector.addressId, {
|
|
1793
|
+
at: Math.max(old?.at ?? 0, options.now),
|
|
1794
|
+
until: Math.max(old?.until ?? 0, options.keepUntil)
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
let n = 0;
|
|
1798
|
+
for (const [hash, r] of this.#records) {
|
|
1799
|
+
if (matches(r, selector)) {
|
|
1800
|
+
this.#records.delete(hash);
|
|
1801
|
+
n++;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return n;
|
|
1805
|
+
}
|
|
1806
|
+
async listGrants(addressId, now) {
|
|
1807
|
+
const own = [...this.#records.values()].filter((r) => r.addressId === addressId);
|
|
1808
|
+
return summarizeGrants(own, now);
|
|
1809
|
+
}
|
|
1810
|
+
async deleteExpired(now) {
|
|
1811
|
+
for (const [id, until] of this.#revokedGrants) {
|
|
1812
|
+
if (until <= now)
|
|
1813
|
+
this.#revokedGrants.delete(id);
|
|
1814
|
+
}
|
|
1815
|
+
for (const [id, t] of this.#revokedAddresses) {
|
|
1816
|
+
if (t.until <= now)
|
|
1817
|
+
this.#revokedAddresses.delete(id);
|
|
1818
|
+
}
|
|
1819
|
+
this.#nonces.sweep(now);
|
|
1820
|
+
let n = 0;
|
|
1821
|
+
for (const [hash, r] of this.#records) {
|
|
1822
|
+
if (r.expiresAt <= now) {
|
|
1823
|
+
this.#records.delete(hash);
|
|
1824
|
+
n++;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return n;
|
|
1828
|
+
}
|
|
1829
|
+
async claimNonce(key, expiresAt, now) {
|
|
1830
|
+
return this.#nonces.claim(key, expiresAt, now);
|
|
1831
|
+
}
|
|
1832
|
+
async close() {
|
|
1833
|
+
this.#nonces.clear();
|
|
1834
|
+
this.#records.clear();
|
|
1835
|
+
this.#revokedGrants.clear();
|
|
1836
|
+
this.#revokedAddresses.clear();
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1691
1840
|
// src/tokens.ts
|
|
1692
1841
|
var PREFIX = {
|
|
1693
1842
|
access: "tmcp_at_",
|
|
@@ -1717,7 +1866,8 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1717
1866
|
scopes: grant.scopes,
|
|
1718
1867
|
grantCreatedAt: grant.grantCreatedAt,
|
|
1719
1868
|
issuedAt: now,
|
|
1720
|
-
...grant.subject === undefined ? {} : { subject: grant.subject }
|
|
1869
|
+
...grant.subject === undefined ? {} : { subject: grant.subject },
|
|
1870
|
+
...grant.clientAuth === undefined ? {} : { clientAuth: grant.clientAuth }
|
|
1721
1871
|
};
|
|
1722
1872
|
const accessExpires = Math.min(now + options.accessTtlMs, options.grantEndsAt);
|
|
1723
1873
|
const refreshStored = await store.put({
|
|
@@ -1743,6 +1893,75 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1743
1893
|
};
|
|
1744
1894
|
}
|
|
1745
1895
|
|
|
1896
|
+
// src/oauth/domain-cache.ts
|
|
1897
|
+
class DomainCache {
|
|
1898
|
+
#entries = new Map;
|
|
1899
|
+
#counts = new Map;
|
|
1900
|
+
#max;
|
|
1901
|
+
#perDomain;
|
|
1902
|
+
#isDead;
|
|
1903
|
+
constructor(max, perDomain, isDead = () => false) {
|
|
1904
|
+
this.#max = max;
|
|
1905
|
+
this.#perDomain = perDomain;
|
|
1906
|
+
this.#isDead = isDead;
|
|
1907
|
+
}
|
|
1908
|
+
get size() {
|
|
1909
|
+
return this.#entries.size;
|
|
1910
|
+
}
|
|
1911
|
+
get(key) {
|
|
1912
|
+
const entry = this.#entries.get(key);
|
|
1913
|
+
if (entry === undefined)
|
|
1914
|
+
return;
|
|
1915
|
+
this.#entries.delete(key);
|
|
1916
|
+
this.#entries.set(key, entry);
|
|
1917
|
+
return entry.value;
|
|
1918
|
+
}
|
|
1919
|
+
delete(key) {
|
|
1920
|
+
const entry = this.#entries.get(key);
|
|
1921
|
+
if (entry === undefined)
|
|
1922
|
+
return;
|
|
1923
|
+
this.#entries.delete(key);
|
|
1924
|
+
const left = (this.#counts.get(entry.domain) ?? 1) - 1;
|
|
1925
|
+
if (left > 0)
|
|
1926
|
+
this.#counts.set(entry.domain, left);
|
|
1927
|
+
else
|
|
1928
|
+
this.#counts.delete(entry.domain);
|
|
1929
|
+
}
|
|
1930
|
+
set(key, domain, value, now) {
|
|
1931
|
+
this.delete(key);
|
|
1932
|
+
if ((this.#counts.get(domain) ?? 0) >= this.#perDomain) {
|
|
1933
|
+
this.#evictOldestOf(domain);
|
|
1934
|
+
} else if (this.#entries.size >= this.#max) {
|
|
1935
|
+
this.#evict(now);
|
|
1936
|
+
}
|
|
1937
|
+
this.#entries.set(key, { domain, value });
|
|
1938
|
+
this.#counts.set(domain, (this.#counts.get(domain) ?? 0) + 1);
|
|
1939
|
+
}
|
|
1940
|
+
#evict(now) {
|
|
1941
|
+
for (const [key, entry] of this.#entries) {
|
|
1942
|
+
if (this.#isDead(entry.value, now)) {
|
|
1943
|
+
this.delete(key);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const most = Math.max(0, ...this.#counts.values());
|
|
1948
|
+
for (const [key, entry] of this.#entries) {
|
|
1949
|
+
if (this.#counts.get(entry.domain) === most) {
|
|
1950
|
+
this.delete(key);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
#evictOldestOf(domain) {
|
|
1956
|
+
for (const [key, entry] of this.#entries) {
|
|
1957
|
+
if (entry.domain === domain) {
|
|
1958
|
+
this.delete(key);
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1746
1965
|
// src/oauth/cimd.ts
|
|
1747
1966
|
class ClientMetadataError extends Error {
|
|
1748
1967
|
name = "ClientMetadataError";
|
|
@@ -1752,6 +1971,16 @@ class ClientMetadataError extends Error {
|
|
|
1752
1971
|
this.reason = reason;
|
|
1753
1972
|
}
|
|
1754
1973
|
}
|
|
1974
|
+
var TRANSIENT_REASONS = new Set([
|
|
1975
|
+
"rate_limited",
|
|
1976
|
+
"fetch_failed",
|
|
1977
|
+
"unresolvable",
|
|
1978
|
+
"too_large"
|
|
1979
|
+
]);
|
|
1980
|
+
function isTransient(error) {
|
|
1981
|
+
return TRANSIENT_REASONS.has(error.reason);
|
|
1982
|
+
}
|
|
1983
|
+
var STALE_GRACE_MS = 60 * 60000;
|
|
1755
1984
|
var MAX_CLIENT_ID_LENGTH = 2048;
|
|
1756
1985
|
var MAX_NAME_LENGTH = 64;
|
|
1757
1986
|
var MAX_REDIRECT_URIS = 20;
|
|
@@ -1759,6 +1988,9 @@ var MIN_CACHE_MS = 5 * 60000;
|
|
|
1759
1988
|
var DEFAULT_CACHE_MS = 60 * 60000;
|
|
1760
1989
|
var MAX_CACHE_MS = 24 * 60 * 60000;
|
|
1761
1990
|
var MAX_CACHE_ENTRIES = 1000;
|
|
1991
|
+
var MAX_CACHE_PER_DOMAIN = 100;
|
|
1992
|
+
var FAILURE_CACHE_MS = 60000;
|
|
1993
|
+
var MAX_FAILURES = 1000;
|
|
1762
1994
|
async function defaultLookup(hostname) {
|
|
1763
1995
|
const records = await Bun.dns.lookup(hostname, {});
|
|
1764
1996
|
return records.map((r) => r.address);
|
|
@@ -1792,12 +2024,22 @@ function limitKey(hostname) {
|
|
|
1792
2024
|
return h;
|
|
1793
2025
|
return h.split(".").slice(-2).join(".");
|
|
1794
2026
|
}
|
|
2027
|
+
function knownBudget() {
|
|
2028
|
+
return {
|
|
2029
|
+
perHost: new RateLimiter({ capacity: 5, refillPerSecond: 0.2 }),
|
|
2030
|
+
perDomain: new RateLimiter({ capacity: 15, refillPerSecond: 0.6 }),
|
|
2031
|
+
global: new RateLimiter({ capacity: 100, refillPerSecond: 10 })
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
1795
2034
|
|
|
1796
2035
|
class ClientMetadataFetcher {
|
|
1797
|
-
#cache = new
|
|
2036
|
+
#cache = new DomainCache(MAX_CACHE_ENTRIES, MAX_CACHE_PER_DOMAIN, (entry, now) => entry.until + STALE_GRACE_MS <= now);
|
|
2037
|
+
#failures = new Map;
|
|
1798
2038
|
#perHost = new RateLimiter({ capacity: 5, refillPerSecond: 0.2 });
|
|
1799
2039
|
#perDomain = new RateLimiter({ capacity: 15, refillPerSecond: 0.6 });
|
|
1800
2040
|
#global = new RateLimiter({ capacity: 30, refillPerSecond: 1 });
|
|
2041
|
+
#refreshes = knownBudget();
|
|
2042
|
+
#keys = knownBudget();
|
|
1801
2043
|
#allowedHosts;
|
|
1802
2044
|
#logger;
|
|
1803
2045
|
#timeoutMs;
|
|
@@ -1816,34 +2058,106 @@ class ClientMetadataFetcher {
|
|
|
1816
2058
|
this.#testMode = options.testMode;
|
|
1817
2059
|
this.#now = options.now ?? Date.now;
|
|
1818
2060
|
}
|
|
1819
|
-
async get(clientId) {
|
|
2061
|
+
async get(clientId, options = {}) {
|
|
1820
2062
|
const url = this.#checkUrl(clientId);
|
|
1821
2063
|
const now = this.#now();
|
|
1822
2064
|
const cached = this.#cache.get(clientId);
|
|
1823
|
-
if (cached !== undefined && cached.until > now)
|
|
1824
|
-
this.#cache.delete(clientId);
|
|
1825
|
-
this.#cache.set(clientId, cached);
|
|
2065
|
+
if (cached !== undefined && cached.until > now)
|
|
1826
2066
|
return cached.meta;
|
|
2067
|
+
const known = cached !== undefined && now < cached.until + STALE_GRACE_MS;
|
|
2068
|
+
const stale = options.allowStale === true && known ? cached?.meta : undefined;
|
|
2069
|
+
try {
|
|
2070
|
+
const failed = this.#failures.get(clientId);
|
|
2071
|
+
if (failed !== undefined && failed.until > now)
|
|
2072
|
+
throw failed.error;
|
|
2073
|
+
return await this.#refresh(clientId, url, now, known);
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
const transient = error instanceof ClientMetadataError && isTransient(error);
|
|
2076
|
+
if (!transient)
|
|
2077
|
+
this.#cache.delete(clientId);
|
|
2078
|
+
if (stale === undefined || !transient)
|
|
2079
|
+
throw error;
|
|
2080
|
+
this.#logger?.info("oauth.cimd_stale", {
|
|
2081
|
+
host: url.host,
|
|
2082
|
+
reason: error.reason
|
|
2083
|
+
});
|
|
2084
|
+
return stale;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
async#refresh(clientId, url, now, known) {
|
|
2088
|
+
this.#admit(url, now, known ? "refresh" : "new");
|
|
2089
|
+
let meta;
|
|
2090
|
+
let ttlMs;
|
|
2091
|
+
try {
|
|
2092
|
+
const address = await this.#vettedAddress(url);
|
|
2093
|
+
const response = await this.#fetchJson(url, address, [
|
|
2094
|
+
"application/json"
|
|
2095
|
+
]);
|
|
2096
|
+
meta = validateDocument(response.doc, clientId, url, {
|
|
2097
|
+
test: this.#testMode !== undefined,
|
|
2098
|
+
onFallback: (reason) => this.#logger?.info("oauth.cimd_auth_fallback", {
|
|
2099
|
+
host: url.host,
|
|
2100
|
+
reason
|
|
2101
|
+
})
|
|
2102
|
+
});
|
|
2103
|
+
ttlMs = cacheMs(response.cacheControl);
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
if (error instanceof ClientMetadataError) {
|
|
2106
|
+
this.#rememberFailure(clientId, error, now);
|
|
2107
|
+
}
|
|
2108
|
+
throw error;
|
|
1827
2109
|
}
|
|
2110
|
+
this.#failures.delete(clientId);
|
|
2111
|
+
this.#cache.set(clientId, limitKey(url.hostname), { meta, until: now + ttlMs }, now);
|
|
2112
|
+
return meta;
|
|
2113
|
+
}
|
|
2114
|
+
#rememberFailure(clientId, error, now) {
|
|
2115
|
+
this.#failures.delete(clientId);
|
|
2116
|
+
if (this.#failures.size >= MAX_FAILURES) {
|
|
2117
|
+
const oldest = this.#failures.keys().next().value;
|
|
2118
|
+
if (oldest !== undefined)
|
|
2119
|
+
this.#failures.delete(oldest);
|
|
2120
|
+
}
|
|
2121
|
+
this.#failures.set(clientId, { error, until: now + FAILURE_CACHE_MS });
|
|
2122
|
+
}
|
|
2123
|
+
async fetchJwks(jwksUri) {
|
|
2124
|
+
const url = this.#checkJwksUri(jwksUri);
|
|
2125
|
+
this.#admit(url, this.#now(), "keys");
|
|
2126
|
+
const address = await this.#vettedAddress(url);
|
|
2127
|
+
const response = await this.#fetchJson(url, address, [
|
|
2128
|
+
"application/json",
|
|
2129
|
+
"application/jwk-set+json"
|
|
2130
|
+
]);
|
|
2131
|
+
return response.doc;
|
|
2132
|
+
}
|
|
2133
|
+
#admit(url, now, budget) {
|
|
2134
|
+
const { perHost, perDomain, global } = budget === "refresh" ? this.#refreshes : budget === "keys" ? this.#keys : {
|
|
2135
|
+
perHost: this.#perHost,
|
|
2136
|
+
perDomain: this.#perDomain,
|
|
2137
|
+
global: this.#global
|
|
2138
|
+
};
|
|
1828
2139
|
const key = limitKey(url.hostname);
|
|
1829
|
-
if (!
|
|
2140
|
+
if (!perHost.available(url.host, now) || !perDomain.available(key, now)) {
|
|
1830
2141
|
throw new ClientMetadataError("rate_limited", "Too many requests for this AI app's details. Try again in a minute.");
|
|
1831
2142
|
}
|
|
1832
|
-
if (!
|
|
2143
|
+
if (!global.take("all", now)) {
|
|
1833
2144
|
throw new ClientMetadataError("rate_limited", "The relay is busy. Try again in a minute.");
|
|
1834
2145
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
this.#
|
|
1840
|
-
if (
|
|
1841
|
-
|
|
1842
|
-
if (oldest !== undefined)
|
|
1843
|
-
this.#cache.delete(oldest);
|
|
2146
|
+
perHost.take(url.host, now);
|
|
2147
|
+
perDomain.take(key, now);
|
|
2148
|
+
}
|
|
2149
|
+
#checkJwksUri(jwksUri) {
|
|
2150
|
+
const url = parseJwksUri(jwksUri, this.#testMode !== undefined);
|
|
2151
|
+
if (url === undefined) {
|
|
2152
|
+
throw new ClientMetadataError("invalid_jwks_uri", "The AI app's jwks_uri is not accepted.");
|
|
1844
2153
|
}
|
|
1845
|
-
this.#
|
|
1846
|
-
|
|
2154
|
+
if (this.#allowedHosts.length > 0 && !this.#allowedHosts.includes(url.host)) {
|
|
2155
|
+
throw new ClientMetadataError("host_not_allowed", `This relay does not accept AI apps from ${url.host}.`);
|
|
2156
|
+
}
|
|
2157
|
+
if (isIpLiteral(url.hostname) && !this.#addressAllowed(url.hostname)) {
|
|
2158
|
+
throw this.#notLoaded("private_address", url);
|
|
2159
|
+
}
|
|
2160
|
+
return url;
|
|
1847
2161
|
}
|
|
1848
2162
|
#checkUrl(clientId) {
|
|
1849
2163
|
const refuse = (message) => new ClientMetadataError("invalid_client_id", message);
|
|
@@ -1905,9 +2219,9 @@ class ClientMetadataFetcher {
|
|
|
1905
2219
|
}
|
|
1906
2220
|
return addresses.find((a) => parseIpv4(a) !== undefined) ?? addresses[0] ?? "";
|
|
1907
2221
|
}
|
|
1908
|
-
async#
|
|
2222
|
+
async#fetchJson(url, address, types) {
|
|
1909
2223
|
const fail = (detail) => this.#notLoaded("fetch_failed", url, detail);
|
|
1910
|
-
const pinned = new URL(
|
|
2224
|
+
const pinned = new URL(url.href);
|
|
1911
2225
|
pinned.hostname = address.includes(":") ? `[${address}]` : address;
|
|
1912
2226
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
1913
2227
|
const tls = url.protocol === "https:" ? {
|
|
@@ -1932,8 +2246,9 @@ class ClientMetadataFetcher {
|
|
|
1932
2246
|
if (response.status !== 200)
|
|
1933
2247
|
throw fail(`status_${response.status}`);
|
|
1934
2248
|
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1935
|
-
if (type
|
|
2249
|
+
if (type === undefined || !types.includes(type)) {
|
|
1936
2250
|
throw fail("not_json");
|
|
2251
|
+
}
|
|
1937
2252
|
const text = await this.#readLimited(response, url);
|
|
1938
2253
|
let doc;
|
|
1939
2254
|
try {
|
|
@@ -1941,8 +2256,7 @@ class ClientMetadataFetcher {
|
|
|
1941
2256
|
} catch {
|
|
1942
2257
|
throw fail("bad_json");
|
|
1943
2258
|
}
|
|
1944
|
-
|
|
1945
|
-
return { meta, ttlMs: cacheMs(response.headers.get("cache-control")) };
|
|
2259
|
+
return { doc, cacheControl: response.headers.get("cache-control") };
|
|
1946
2260
|
} finally {
|
|
1947
2261
|
if (!response.bodyUsed)
|
|
1948
2262
|
response.body?.cancel().catch(() => {});
|
|
@@ -1997,7 +2311,7 @@ function foreignDomain(name, hostname) {
|
|
|
1997
2311
|
}
|
|
1998
2312
|
return false;
|
|
1999
2313
|
}
|
|
2000
|
-
function validateDocument(doc, clientId, url) {
|
|
2314
|
+
function validateDocument(doc, clientId, url, options = {}) {
|
|
2001
2315
|
const invalid = (message) => new ClientMetadataError("invalid_metadata", message);
|
|
2002
2316
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
2003
2317
|
throw invalid("The AI app's details are not a JSON object.");
|
|
@@ -2023,11 +2337,7 @@ function validateDocument(doc, clientId, url) {
|
|
|
2023
2337
|
if (!Array.isArray(uris) || uris.length === 0 || uris.length > MAX_REDIRECT_URIS || !uris.every((u) => typeof u === "string" && isRedirectUri(u))) {
|
|
2024
2338
|
throw invalid("The AI app's details have no valid redirect_uris.");
|
|
2025
2339
|
}
|
|
2026
|
-
const
|
|
2027
|
-
const supported = d["token_endpoint_auth_methods_supported"];
|
|
2028
|
-
if (method !== undefined && method !== "none" && !(Array.isArray(supported) && supported.includes("none"))) {
|
|
2029
|
-
throw invalid('The AI app must use token_endpoint_auth_method "none" with this relay, or list "none" in token_endpoint_auth_methods_supported.');
|
|
2030
|
-
}
|
|
2340
|
+
const auth = clientAuth(d, url, options, invalid);
|
|
2031
2341
|
const grants = d["grant_types"];
|
|
2032
2342
|
if (grants !== undefined && (!Array.isArray(grants) || !grants.includes("authorization_code"))) {
|
|
2033
2343
|
throw invalid("The AI app's details do not allow the authorization code grant.");
|
|
@@ -2040,9 +2350,81 @@ function validateDocument(doc, clientId, url) {
|
|
|
2040
2350
|
clientId,
|
|
2041
2351
|
name: displayName(name),
|
|
2042
2352
|
host: url.host,
|
|
2043
|
-
redirectUris: uris
|
|
2353
|
+
redirectUris: uris,
|
|
2354
|
+
auth
|
|
2044
2355
|
};
|
|
2045
2356
|
}
|
|
2357
|
+
function clientAuth(d, url, options, invalid) {
|
|
2358
|
+
const method = d["token_endpoint_auth_method"];
|
|
2359
|
+
const supported = d["token_endpoint_auth_methods_supported"];
|
|
2360
|
+
const listsNone = Array.isArray(supported) && supported.includes("none");
|
|
2361
|
+
if (method === undefined || method === "none")
|
|
2362
|
+
return { method: "none" };
|
|
2363
|
+
const checked = method === "private_key_jwt" ? privateKeyJwt(d, url, options.test === true) : {
|
|
2364
|
+
problem: "unsupported_method",
|
|
2365
|
+
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.'
|
|
2366
|
+
};
|
|
2367
|
+
if ("jwksUri" in checked) {
|
|
2368
|
+
return { method: "private_key_jwt", ...checked, publicAllowed: listsNone };
|
|
2369
|
+
}
|
|
2370
|
+
if (listsNone) {
|
|
2371
|
+
options.onFallback?.(checked.problem);
|
|
2372
|
+
return { method: "none" };
|
|
2373
|
+
}
|
|
2374
|
+
throw invalid(checked.message);
|
|
2375
|
+
}
|
|
2376
|
+
function privateKeyJwt(d, url, test) {
|
|
2377
|
+
const jwksUri = d["jwks_uri"];
|
|
2378
|
+
if (jwksUri === undefined) {
|
|
2379
|
+
return {
|
|
2380
|
+
problem: "no_jwks_uri",
|
|
2381
|
+
message: "The AI app uses private_key_jwt but publishes no jwks_uri (an inline jwks is not accepted)."
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
if (d["jwks"] !== undefined) {
|
|
2385
|
+
return {
|
|
2386
|
+
problem: "jwks_and_jwks_uri",
|
|
2387
|
+
message: "The AI app's details have both jwks and jwks_uri."
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
const parsed = typeof jwksUri === "string" ? parseJwksUri(jwksUri, test) : undefined;
|
|
2391
|
+
if (parsed === undefined || parsed.protocol !== url.protocol || parsed.host !== url.host) {
|
|
2392
|
+
return {
|
|
2393
|
+
problem: "bad_jwks_uri",
|
|
2394
|
+
message: "The AI app's jwks_uri must be an https:// URL on the host of its client_id."
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
const alg = d["token_endpoint_auth_signing_alg"];
|
|
2398
|
+
if (alg !== undefined && !isAlgorithm(alg)) {
|
|
2399
|
+
return {
|
|
2400
|
+
problem: "signing_alg",
|
|
2401
|
+
message: "The AI app's token_endpoint_auth_signing_alg is not one this relay accepts."
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
return {
|
|
2405
|
+
jwksUri: parsed.href,
|
|
2406
|
+
algorithms: alg === undefined ? JWT_ALGORITHMS : [alg]
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
function parseJwksUri(value, test) {
|
|
2410
|
+
if (value.length > MAX_CLIENT_ID_LENGTH)
|
|
2411
|
+
return;
|
|
2412
|
+
let url;
|
|
2413
|
+
try {
|
|
2414
|
+
url = new URL(value);
|
|
2415
|
+
} catch {
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
const scheme = url.protocol === "https:" || test && url.protocol === "http:";
|
|
2419
|
+
if (!scheme || url.href !== value)
|
|
2420
|
+
return;
|
|
2421
|
+
if (url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (url.port !== "" && !test)
|
|
2425
|
+
return;
|
|
2426
|
+
return url;
|
|
2427
|
+
}
|
|
2046
2428
|
var PRIVATE_USE_SCHEME = /^[a-z][a-z0-9+-]*(\.[a-z0-9+-]+)+:$/;
|
|
2047
2429
|
function isRedirectUri(value) {
|
|
2048
2430
|
if (value.length > 2048 || value.includes("#"))
|
|
@@ -2086,6 +2468,151 @@ function redirectUriMatches(requested, registered) {
|
|
|
2086
2468
|
return a.href === b.href;
|
|
2087
2469
|
}
|
|
2088
2470
|
|
|
2471
|
+
// src/oauth/client-auth.ts
|
|
2472
|
+
var CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
2473
|
+
var CLIENT_ASSERTION_MAX_LIFETIME_MS = 60 * 60000;
|
|
2474
|
+
var CLIENT_ASSERTION_MAX_AGE_MS = 5 * 60000;
|
|
2475
|
+
var CLIENT_ASSERTION_LEEWAY_MS = 30000;
|
|
2476
|
+
var MAX_JTI2 = 256;
|
|
2477
|
+
var MAX_KEY_SETS = 1000;
|
|
2478
|
+
var MAX_KEY_SETS_PER_DOMAIN = 100;
|
|
2479
|
+
var SWEEP_INTERVAL_MS = 1000;
|
|
2480
|
+
var ACCEPTED_TYPES = new Set(["jwt", "client-authentication+jwt"]);
|
|
2481
|
+
function assertionSubject(assertion) {
|
|
2482
|
+
try {
|
|
2483
|
+
const sub = decodeJwt(assertion).payload["sub"];
|
|
2484
|
+
return typeof sub === "string" && sub !== "" ? sub : undefined;
|
|
2485
|
+
} catch {
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function nonceKey(clientId, jti) {
|
|
2490
|
+
return hashToken(JSON.stringify(["client_assertion", clientId, jti]));
|
|
2491
|
+
}
|
|
2492
|
+
async function verifyClientAssertion(assertion, check) {
|
|
2493
|
+
const { header, payload } = await verifyJwt(assertion, check.keys, {
|
|
2494
|
+
algorithms: check.auth.algorithms
|
|
2495
|
+
});
|
|
2496
|
+
const typ = header["typ"];
|
|
2497
|
+
if (typ !== undefined && (typeof typ !== "string" || !ACCEPTED_TYPES.has(typ.toLowerCase()))) {
|
|
2498
|
+
throw new JwtError("typ", "the assertion's type is not a client assertion");
|
|
2499
|
+
}
|
|
2500
|
+
if (payload["iss"] !== check.clientId) {
|
|
2501
|
+
throw new JwtError("iss", "the assertion's issuer is not the client");
|
|
2502
|
+
}
|
|
2503
|
+
if (payload["sub"] !== check.clientId) {
|
|
2504
|
+
throw new JwtError("sub", "the assertion's subject is not the client");
|
|
2505
|
+
}
|
|
2506
|
+
if (!audiences(payload).some((a) => check.audiences.includes(a))) {
|
|
2507
|
+
throw new JwtError("aud", "the assertion is not for this relay");
|
|
2508
|
+
}
|
|
2509
|
+
const usableUntil = checkTimes(payload, check.now);
|
|
2510
|
+
const jti = payload["jti"];
|
|
2511
|
+
if (typeof jti !== "string" || jti === "" || jti.length > MAX_JTI2) {
|
|
2512
|
+
throw new JwtError("jti", "the assertion needs a jti");
|
|
2513
|
+
}
|
|
2514
|
+
const key = nonceKey(check.clientId, jti);
|
|
2515
|
+
if (!await check.claimNonce(key, usableUntil, check.now)) {
|
|
2516
|
+
throw new JwtError("replayed", "the assertion was already used");
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
function checkTimes(payload, now) {
|
|
2520
|
+
const leeway = CLIENT_ASSERTION_LEEWAY_MS;
|
|
2521
|
+
const exp = numericDate(payload, "exp");
|
|
2522
|
+
if (exp === undefined)
|
|
2523
|
+
throw new JwtError("exp", "the assertion has no exp");
|
|
2524
|
+
if (exp <= now - leeway) {
|
|
2525
|
+
throw new JwtError("expired", "the assertion has expired");
|
|
2526
|
+
}
|
|
2527
|
+
if (exp > now + CLIENT_ASSERTION_MAX_LIFETIME_MS + leeway) {
|
|
2528
|
+
throw new JwtError("lifetime", "the assertion lives longer than an hour");
|
|
2529
|
+
}
|
|
2530
|
+
const iat = numericDate(payload, "iat");
|
|
2531
|
+
let end = exp;
|
|
2532
|
+
if (iat === undefined) {
|
|
2533
|
+
if (exp > now + CLIENT_ASSERTION_MAX_AGE_MS + leeway) {
|
|
2534
|
+
throw new JwtError("lifetime", "an assertion without iat may live five minutes");
|
|
2535
|
+
}
|
|
2536
|
+
} else {
|
|
2537
|
+
if (iat > now + leeway) {
|
|
2538
|
+
throw new JwtError("iat", "the assertion was issued in the future");
|
|
2539
|
+
}
|
|
2540
|
+
if (iat + CLIENT_ASSERTION_MAX_AGE_MS <= now - leeway) {
|
|
2541
|
+
throw new JwtError("too_old", "the assertion was issued too long ago");
|
|
2542
|
+
}
|
|
2543
|
+
end = Math.min(exp, iat + CLIENT_ASSERTION_MAX_AGE_MS);
|
|
2544
|
+
}
|
|
2545
|
+
const nbf = numericDate(payload, "nbf");
|
|
2546
|
+
if (nbf !== undefined && nbf > now + leeway) {
|
|
2547
|
+
throw new JwtError("nbf", "the assertion is not valid yet");
|
|
2548
|
+
}
|
|
2549
|
+
return end + leeway;
|
|
2550
|
+
}
|
|
2551
|
+
function quotaKey(clientId) {
|
|
2552
|
+
return ipPrefixKey(new URL(clientId).hostname.replace(/\.$/, ""));
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
class NonceQuota {
|
|
2556
|
+
#until = new Map;
|
|
2557
|
+
#perDomain;
|
|
2558
|
+
#max;
|
|
2559
|
+
#total = 0;
|
|
2560
|
+
#sweptAt = Number.NEGATIVE_INFINITY;
|
|
2561
|
+
constructor(perDomain, max) {
|
|
2562
|
+
this.#perDomain = perDomain;
|
|
2563
|
+
this.#max = max;
|
|
2564
|
+
}
|
|
2565
|
+
refusal(domain, now) {
|
|
2566
|
+
this.sweep(now);
|
|
2567
|
+
if ((this.#until.get(domain)?.length ?? 0) >= this.#perDomain) {
|
|
2568
|
+
return "domain_full";
|
|
2569
|
+
}
|
|
2570
|
+
return this.#total >= this.#max ? "full" : undefined;
|
|
2571
|
+
}
|
|
2572
|
+
add(domain, until) {
|
|
2573
|
+
const list = this.#until.get(domain);
|
|
2574
|
+
if (list === undefined)
|
|
2575
|
+
this.#until.set(domain, [until]);
|
|
2576
|
+
else
|
|
2577
|
+
list.push(until);
|
|
2578
|
+
this.#total++;
|
|
2579
|
+
}
|
|
2580
|
+
sweep(now) {
|
|
2581
|
+
if (now - this.#sweptAt < SWEEP_INTERVAL_MS)
|
|
2582
|
+
return false;
|
|
2583
|
+
this.#sweptAt = now;
|
|
2584
|
+
this.#total = 0;
|
|
2585
|
+
for (const [domain, list] of this.#until) {
|
|
2586
|
+
const live = list.filter((until) => until > now);
|
|
2587
|
+
if (live.length === 0)
|
|
2588
|
+
this.#until.delete(domain);
|
|
2589
|
+
else
|
|
2590
|
+
this.#until.set(domain, live);
|
|
2591
|
+
this.#total += live.length;
|
|
2592
|
+
}
|
|
2593
|
+
return true;
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
class ClientKeySets {
|
|
2598
|
+
#sets = new DomainCache(MAX_KEY_SETS, MAX_KEY_SETS_PER_DOMAIN);
|
|
2599
|
+
#load;
|
|
2600
|
+
#now;
|
|
2601
|
+
constructor(load, now = Date.now) {
|
|
2602
|
+
this.#load = load;
|
|
2603
|
+
this.#now = now;
|
|
2604
|
+
}
|
|
2605
|
+
for(jwksUri) {
|
|
2606
|
+
const known = this.#sets.get(jwksUri);
|
|
2607
|
+
if (known !== undefined)
|
|
2608
|
+
return known;
|
|
2609
|
+
const set = new JwksCache(jwksUri, { load: this.#load, now: this.#now });
|
|
2610
|
+
const domain = limitKey(new URL(jwksUri).hostname);
|
|
2611
|
+
this.#sets.set(jwksUri, domain, set, this.#now());
|
|
2612
|
+
return set;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2089
2616
|
// src/oauth/html.ts
|
|
2090
2617
|
var ESCAPES = {
|
|
2091
2618
|
"&": "&",
|
|
@@ -2655,6 +3182,12 @@ var REFRESH_REUSE_WINDOW_MS = 24 * 60 * 60000;
|
|
|
2655
3182
|
var MAX_CODES = 1e4;
|
|
2656
3183
|
var MAX_FORM_BYTES = 8192;
|
|
2657
3184
|
var MAX_STATE_LENGTH = 1024;
|
|
3185
|
+
var MAX_NONCES_PER_HOST = 1e4;
|
|
3186
|
+
var MAX_NONCES = 1e5;
|
|
3187
|
+
var CLIENT_AUTH_FAILED = "client authentication failed";
|
|
3188
|
+
function assertionRequired(auth) {
|
|
3189
|
+
return auth.method === "private_key_jwt" && !auth.publicAllowed;
|
|
3190
|
+
}
|
|
2658
3191
|
|
|
2659
3192
|
class OAuthError extends Error {
|
|
2660
3193
|
status;
|
|
@@ -2704,10 +3237,16 @@ function hostOf(url) {
|
|
|
2704
3237
|
return url;
|
|
2705
3238
|
}
|
|
2706
3239
|
}
|
|
3240
|
+
function logHost(clientId) {
|
|
3241
|
+
return clientId !== undefined && URL.canParse(clientId) ? hostOf(clientId).slice(0, 100) : "(not a URL)";
|
|
3242
|
+
}
|
|
2707
3243
|
|
|
2708
3244
|
class AuthorizationServer {
|
|
2709
3245
|
#host;
|
|
2710
3246
|
#cimd;
|
|
3247
|
+
#clientKeys;
|
|
3248
|
+
#nonces = new NonceSet;
|
|
3249
|
+
#nonceQuota = new NonceQuota(MAX_NONCES_PER_HOST, MAX_NONCES);
|
|
2711
3250
|
#pending = new Map;
|
|
2712
3251
|
#pendingPerIp = new Map;
|
|
2713
3252
|
#pendingPerAddress = new Map;
|
|
@@ -2758,6 +3297,8 @@ class AuthorizationServer {
|
|
|
2758
3297
|
logger: host.logger,
|
|
2759
3298
|
...testMode?.cimd === undefined ? {} : { testMode: testMode.cimd }
|
|
2760
3299
|
});
|
|
3300
|
+
const cimd = this.#cimd;
|
|
3301
|
+
this.#clientKeys = new ClientKeySets((url) => cimd.fetchJwks(url));
|
|
2761
3302
|
}
|
|
2762
3303
|
get #issuer() {
|
|
2763
3304
|
return this.#host.publicUrl();
|
|
@@ -2811,8 +3352,10 @@ class AuthorizationServer {
|
|
|
2811
3352
|
response_modes_supported: ["query"],
|
|
2812
3353
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2813
3354
|
code_challenge_methods_supported: ["S256"],
|
|
2814
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
2815
|
-
|
|
3355
|
+
token_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3356
|
+
token_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
3357
|
+
revocation_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3358
|
+
revocation_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
2816
3359
|
scopes_supported: [SCOPE],
|
|
2817
3360
|
client_id_metadata_document_supported: true,
|
|
2818
3361
|
authorization_response_iss_parameter_supported: true,
|
|
@@ -3311,20 +3854,21 @@ ${cancel}
|
|
|
3311
3854
|
"code_verifier",
|
|
3312
3855
|
"refresh_token",
|
|
3313
3856
|
"resource",
|
|
3314
|
-
"scope"
|
|
3857
|
+
"scope",
|
|
3858
|
+
"client_assertion",
|
|
3859
|
+
"client_assertion_type"
|
|
3315
3860
|
]);
|
|
3316
3861
|
if ("duplicate" in p) {
|
|
3317
3862
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3318
3863
|
}
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
}
|
|
3864
|
+
const client = await this.#authenticateClient(p, [
|
|
3865
|
+
`${this.#issuer}/token`
|
|
3866
|
+
]);
|
|
3323
3867
|
let body;
|
|
3324
3868
|
if (p["grant_type"] === "authorization_code") {
|
|
3325
|
-
body = await this.#codeGrant(p,
|
|
3869
|
+
body = await this.#codeGrant(p, client);
|
|
3326
3870
|
} else if (p["grant_type"] === "refresh_token") {
|
|
3327
|
-
body = await this.#refreshGrant(p,
|
|
3871
|
+
body = await this.#refreshGrant(p, client);
|
|
3328
3872
|
} else if (p["grant_type"] === undefined) {
|
|
3329
3873
|
throw new OAuthError(400, "invalid_request", "grant_type is required");
|
|
3330
3874
|
} else {
|
|
@@ -3358,19 +3902,96 @@ ${cancel}
|
|
|
3358
3902
|
throw new OAuthError(400, "invalid_request", "the body must be application/x-www-form-urlencoded");
|
|
3359
3903
|
}
|
|
3360
3904
|
if (req.headers.get("authorization") !== null) {
|
|
3361
|
-
throw new OAuthError(401, "invalid_client", "
|
|
3905
|
+
throw new OAuthError(401, "invalid_client", "HTTP authentication is not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3362
3906
|
}
|
|
3363
3907
|
const text = await this.#readForm(req);
|
|
3364
3908
|
if (text === undefined) {
|
|
3365
3909
|
throw new OAuthError(413, "invalid_request", "the request is too large");
|
|
3366
3910
|
}
|
|
3367
3911
|
const params = new URLSearchParams(text);
|
|
3368
|
-
if (params.has("client_secret")
|
|
3369
|
-
throw new OAuthError(400, "invalid_client", "client
|
|
3912
|
+
if (params.has("client_secret")) {
|
|
3913
|
+
throw new OAuthError(400, "invalid_client", "client secrets are not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3370
3914
|
}
|
|
3371
3915
|
return params;
|
|
3372
3916
|
}
|
|
3373
|
-
async#
|
|
3917
|
+
async#authenticateClient(p, endpoints) {
|
|
3918
|
+
const assertion = p["client_assertion"];
|
|
3919
|
+
const type = p["client_assertion_type"];
|
|
3920
|
+
if (assertion === undefined && type === undefined) {
|
|
3921
|
+
const clientId = p["client_id"];
|
|
3922
|
+
if (clientId === undefined) {
|
|
3923
|
+
throw new OAuthError(400, "invalid_request", "client_id is required");
|
|
3924
|
+
}
|
|
3925
|
+
return { clientId, assertion: false };
|
|
3926
|
+
}
|
|
3927
|
+
if (assertion === undefined || type === undefined) {
|
|
3928
|
+
throw new OAuthError(400, "invalid_request", "client_assertion and client_assertion_type go together");
|
|
3929
|
+
}
|
|
3930
|
+
const clientId = p["client_id"] ?? assertionSubject(assertion);
|
|
3931
|
+
if (type !== CLIENT_ASSERTION_TYPE) {
|
|
3932
|
+
throw this.#clientAuthFailed(clientId, "assertion_type");
|
|
3933
|
+
}
|
|
3934
|
+
if (clientId === undefined) {
|
|
3935
|
+
throw this.#clientAuthFailed(clientId, "no_client_id");
|
|
3936
|
+
}
|
|
3937
|
+
let client;
|
|
3938
|
+
try {
|
|
3939
|
+
client = await this.#cimd.get(clientId, { allowStale: true });
|
|
3940
|
+
} catch (error) {
|
|
3941
|
+
if (!(error instanceof ClientMetadataError))
|
|
3942
|
+
throw error;
|
|
3943
|
+
const reason = `metadata_${error.reason}`;
|
|
3944
|
+
throw isTransient(error) || error.reason === "private_address" ? this.#clientAuthUnavailable(clientId, reason) : this.#clientAuthFailed(clientId, reason);
|
|
3945
|
+
}
|
|
3946
|
+
if (client.auth.method !== "private_key_jwt") {
|
|
3947
|
+
throw this.#clientAuthFailed(clientId, "not_private_key_jwt");
|
|
3948
|
+
}
|
|
3949
|
+
const domain = quotaKey(clientId);
|
|
3950
|
+
try {
|
|
3951
|
+
await verifyClientAssertion(assertion, {
|
|
3952
|
+
clientId,
|
|
3953
|
+
auth: client.auth,
|
|
3954
|
+
keys: this.#clientKeys.for(client.auth.jwksUri),
|
|
3955
|
+
audiences: [this.#issuer, ...endpoints],
|
|
3956
|
+
claimNonce: (key, until, now) => this.#claimNonce(clientId, domain, key, until, now),
|
|
3957
|
+
now: Date.now()
|
|
3958
|
+
});
|
|
3959
|
+
} catch (error) {
|
|
3960
|
+
if (!(error instanceof JwtError))
|
|
3961
|
+
throw error;
|
|
3962
|
+
throw error.reason === "jwks_unavailable" ? this.#clientAuthUnavailable(clientId, error.reason) : this.#clientAuthFailed(clientId, error.reason);
|
|
3963
|
+
}
|
|
3964
|
+
return { clientId, assertion: true };
|
|
3965
|
+
}
|
|
3966
|
+
#clientAuthFailed(clientId, reason) {
|
|
3967
|
+
this.#host.logger.info("oauth.client_auth_failed", {
|
|
3968
|
+
client: logHost(clientId),
|
|
3969
|
+
reason
|
|
3970
|
+
});
|
|
3971
|
+
return new OAuthError(401, "invalid_client", CLIENT_AUTH_FAILED);
|
|
3972
|
+
}
|
|
3973
|
+
#clientAuthUnavailable(clientId, reason) {
|
|
3974
|
+
this.#host.logger.warn("oauth.client_auth_unavailable", {
|
|
3975
|
+
client: logHost(clientId),
|
|
3976
|
+
reason
|
|
3977
|
+
});
|
|
3978
|
+
return new OAuthError(503, "temporarily_unavailable", "the client cannot be authenticated right now; try again shortly");
|
|
3979
|
+
}
|
|
3980
|
+
async#claimNonce(clientId, domain, key, until, now) {
|
|
3981
|
+
if (this.#nonceQuota.sweep(now))
|
|
3982
|
+
this.#nonces.sweep(now);
|
|
3983
|
+
const refusal = this.#nonceQuota.refusal(domain, now);
|
|
3984
|
+
if (refusal !== undefined) {
|
|
3985
|
+
throw this.#clientAuthUnavailable(clientId, `nonces_${refusal}`);
|
|
3986
|
+
}
|
|
3987
|
+
const store = this.#host.store;
|
|
3988
|
+
const claimed = store.claimNonce === undefined ? this.#nonces.claim(key, until, now) : await store.claimNonce(key, until, now);
|
|
3989
|
+
if (claimed)
|
|
3990
|
+
this.#nonceQuota.add(domain, until);
|
|
3991
|
+
return claimed;
|
|
3992
|
+
}
|
|
3993
|
+
async#codeGrant(p, client) {
|
|
3994
|
+
const clientId = client.clientId;
|
|
3374
3995
|
const code = p["code"];
|
|
3375
3996
|
const redirectUri = p["redirect_uri"];
|
|
3376
3997
|
const verifier = p["code_verifier"];
|
|
@@ -3382,6 +4003,12 @@ ${cancel}
|
|
|
3382
4003
|
const invalid = (why) => new OAuthError(400, "invalid_grant", why);
|
|
3383
4004
|
if (issued === undefined)
|
|
3384
4005
|
throw invalid("the code is not valid");
|
|
4006
|
+
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
4007
|
+
throw invalid("the code was issued to another client");
|
|
4008
|
+
}
|
|
4009
|
+
if (assertionRequired(issued.client.auth) && !client.assertion) {
|
|
4010
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4011
|
+
}
|
|
3385
4012
|
if (issued.used) {
|
|
3386
4013
|
if (issued.grantId !== undefined) {
|
|
3387
4014
|
await this.#revokeGrant(issued.addressId, issued.grantId, "reuse_detected");
|
|
@@ -3391,9 +4018,6 @@ ${cancel}
|
|
|
3391
4018
|
issued.used = true;
|
|
3392
4019
|
if (issued.expiresAt <= Date.now())
|
|
3393
4020
|
throw invalid("the code has expired");
|
|
3394
|
-
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
3395
|
-
throw invalid("the code was issued to another client");
|
|
3396
|
-
}
|
|
3397
4021
|
if (redirectUri !== issued.redirectUri) {
|
|
3398
4022
|
throw invalid("redirect_uri does not match the authorization request");
|
|
3399
4023
|
}
|
|
@@ -3417,7 +4041,8 @@ ${cancel}
|
|
|
3417
4041
|
clientName: issued.client.name,
|
|
3418
4042
|
scopes: issued.scopes,
|
|
3419
4043
|
grantCreatedAt: now,
|
|
3420
|
-
...issued.subject === undefined ? {} : { subject: issued.subject }
|
|
4044
|
+
...issued.subject === undefined ? {} : { subject: issued.subject },
|
|
4045
|
+
...client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3421
4046
|
}, {
|
|
3422
4047
|
accessTtlMs: config.accessTokenTtlMs,
|
|
3423
4048
|
refreshTtlMs: config.refreshTokenTtlMs,
|
|
@@ -3451,7 +4076,8 @@ ${cancel}
|
|
|
3451
4076
|
scope: scopes.join(" ")
|
|
3452
4077
|
};
|
|
3453
4078
|
}
|
|
3454
|
-
async#refreshGrant(p,
|
|
4079
|
+
async#refreshGrant(p, client) {
|
|
4080
|
+
const clientId = client.clientId;
|
|
3455
4081
|
const token = p["refresh_token"];
|
|
3456
4082
|
if (token === undefined) {
|
|
3457
4083
|
throw new OAuthError(400, "invalid_request", "refresh_token is required");
|
|
@@ -3465,6 +4091,9 @@ ${cancel}
|
|
|
3465
4091
|
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
3466
4092
|
throw invalid("the refresh token was issued to another client");
|
|
3467
4093
|
}
|
|
4094
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4095
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4096
|
+
}
|
|
3468
4097
|
const now = Date.now();
|
|
3469
4098
|
if (record.expiresAt <= now)
|
|
3470
4099
|
throw invalid("the refresh token has expired");
|
|
@@ -3496,7 +4125,8 @@ ${cancel}
|
|
|
3496
4125
|
clientName: record.clientName,
|
|
3497
4126
|
scopes: record.scopes,
|
|
3498
4127
|
grantCreatedAt: record.grantCreatedAt,
|
|
3499
|
-
...record.subject === undefined ? {} : { subject: record.subject }
|
|
4128
|
+
...record.subject === undefined ? {} : { subject: record.subject },
|
|
4129
|
+
...record.clientAuth === "private_key_jwt" || client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3500
4130
|
}, {
|
|
3501
4131
|
accessTtlMs: this.#host.config.accessTokenTtlMs,
|
|
3502
4132
|
refreshTtlMs: this.#host.config.refreshTokenTtlMs,
|
|
@@ -3532,24 +4162,36 @@ ${cancel}
|
|
|
3532
4162
|
throw new OAuthError(429, "temporarily_unavailable", "too many requests; try again shortly");
|
|
3533
4163
|
}
|
|
3534
4164
|
const params = await this.#formParams(req);
|
|
3535
|
-
const p = single(params, [
|
|
4165
|
+
const p = single(params, [
|
|
4166
|
+
"token",
|
|
4167
|
+
"client_id",
|
|
4168
|
+
"token_type_hint",
|
|
4169
|
+
"client_assertion",
|
|
4170
|
+
"client_assertion_type"
|
|
4171
|
+
]);
|
|
3536
4172
|
if ("duplicate" in p) {
|
|
3537
4173
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3538
4174
|
}
|
|
3539
4175
|
const token = p["token"];
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
throw new OAuthError(400, "invalid_request", "token and client_id are required");
|
|
4176
|
+
if (token === undefined) {
|
|
4177
|
+
throw new OAuthError(400, "invalid_request", "token is required");
|
|
3543
4178
|
}
|
|
4179
|
+
const client = await this.#authenticateClient(p, [
|
|
4180
|
+
`${this.#issuer}/revoke`,
|
|
4181
|
+
`${this.#issuer}/token`
|
|
4182
|
+
]);
|
|
3544
4183
|
const record = await this.#host.store.get(hashToken(token));
|
|
3545
4184
|
if (record === undefined)
|
|
3546
4185
|
return new Response(null, {
|
|
3547
4186
|
status: 200,
|
|
3548
4187
|
headers: { ...this.#cors(), ...noStore }
|
|
3549
4188
|
});
|
|
3550
|
-
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
4189
|
+
if (!constantTimeEqual(client.clientId, record.clientId)) {
|
|
3551
4190
|
throw new OAuthError(400, "invalid_client", "the token was issued to another client");
|
|
3552
4191
|
}
|
|
4192
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4193
|
+
throw this.#clientAuthFailed(client.clientId, "assertion_required");
|
|
4194
|
+
}
|
|
3553
4195
|
await this.#revokeGrant(record.addressId, record.grantId, "revoked");
|
|
3554
4196
|
this.#host.logger.info("oauth.revoked", {
|
|
3555
4197
|
address: shortAddress(record.addressId),
|
|
@@ -3595,6 +4237,7 @@ ${cancel}
|
|
|
3595
4237
|
this.#guessesPerAddress.sweep(now);
|
|
3596
4238
|
this.#tokenPerIp.sweep(now);
|
|
3597
4239
|
this.#refreshesPerGrant.sweep(now);
|
|
4240
|
+
this.#nonces.sweep(now);
|
|
3598
4241
|
}
|
|
3599
4242
|
}
|
|
3600
4243
|
|
|
@@ -4003,112 +4646,10 @@ class TabSession {
|
|
|
4003
4646
|
}
|
|
4004
4647
|
}
|
|
4005
4648
|
|
|
4006
|
-
// src/store/memory.ts
|
|
4007
|
-
function copy(record) {
|
|
4008
|
-
return { ...record, scopes: [...record.scopes] };
|
|
4009
|
-
}
|
|
4010
|
-
function matches(record, selector) {
|
|
4011
|
-
return "grantId" in selector ? record.grantId === selector.grantId : record.addressId === selector.addressId;
|
|
4012
|
-
}
|
|
4013
|
-
function isLive(record, now) {
|
|
4014
|
-
return record.expiresAt > now && (record.kind === "access" || record.usedAt === undefined);
|
|
4015
|
-
}
|
|
4016
|
-
function summarizeGrants(records, now) {
|
|
4017
|
-
const grants = new Map;
|
|
4018
|
-
for (const r of records) {
|
|
4019
|
-
if (!isLive(r, now) || grants.has(r.grantId))
|
|
4020
|
-
continue;
|
|
4021
|
-
const summary = {
|
|
4022
|
-
grantId: r.grantId,
|
|
4023
|
-
clientId: r.clientId,
|
|
4024
|
-
clientName: r.clientName,
|
|
4025
|
-
scopes: [...r.scopes],
|
|
4026
|
-
createdAt: r.grantCreatedAt
|
|
4027
|
-
};
|
|
4028
|
-
if (r.subject !== undefined)
|
|
4029
|
-
summary.subject = r.subject;
|
|
4030
|
-
grants.set(r.grantId, summary);
|
|
4031
|
-
}
|
|
4032
|
-
return [...grants.values()].sort((a, b) => a.createdAt - b.createdAt || a.grantId.localeCompare(b.grantId));
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
|
-
class MemoryTokenStore {
|
|
4036
|
-
#records = new Map;
|
|
4037
|
-
#revokedGrants = new Map;
|
|
4038
|
-
#revokedAddresses = new Map;
|
|
4039
|
-
async put(record) {
|
|
4040
|
-
if (this.#revokedGrants.has(record.grantId))
|
|
4041
|
-
return false;
|
|
4042
|
-
const address = this.#revokedAddresses.get(record.addressId);
|
|
4043
|
-
if (address !== undefined && record.grantCreatedAt <= address.at) {
|
|
4044
|
-
return false;
|
|
4045
|
-
}
|
|
4046
|
-
this.#records.set(record.hash, copy(record));
|
|
4047
|
-
return true;
|
|
4048
|
-
}
|
|
4049
|
-
async get(hash) {
|
|
4050
|
-
const r = this.#records.get(hash);
|
|
4051
|
-
return r === undefined ? undefined : copy(r);
|
|
4052
|
-
}
|
|
4053
|
-
async markUsed(hash, now, keepUntil) {
|
|
4054
|
-
const r = this.#records.get(hash);
|
|
4055
|
-
if (r === undefined || r.usedAt !== undefined)
|
|
4056
|
-
return false;
|
|
4057
|
-
r.usedAt = now;
|
|
4058
|
-
r.expiresAt = Math.min(r.expiresAt, keepUntil);
|
|
4059
|
-
return true;
|
|
4060
|
-
}
|
|
4061
|
-
async revoke(selector, options) {
|
|
4062
|
-
if ("grantId" in selector) {
|
|
4063
|
-
this.#revokedGrants.set(selector.grantId, options.keepUntil);
|
|
4064
|
-
} else {
|
|
4065
|
-
const old = this.#revokedAddresses.get(selector.addressId);
|
|
4066
|
-
this.#revokedAddresses.set(selector.addressId, {
|
|
4067
|
-
at: Math.max(old?.at ?? 0, options.now),
|
|
4068
|
-
until: Math.max(old?.until ?? 0, options.keepUntil)
|
|
4069
|
-
});
|
|
4070
|
-
}
|
|
4071
|
-
let n = 0;
|
|
4072
|
-
for (const [hash, r] of this.#records) {
|
|
4073
|
-
if (matches(r, selector)) {
|
|
4074
|
-
this.#records.delete(hash);
|
|
4075
|
-
n++;
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
return n;
|
|
4079
|
-
}
|
|
4080
|
-
async listGrants(addressId, now) {
|
|
4081
|
-
const own = [...this.#records.values()].filter((r) => r.addressId === addressId);
|
|
4082
|
-
return summarizeGrants(own, now);
|
|
4083
|
-
}
|
|
4084
|
-
async deleteExpired(now) {
|
|
4085
|
-
for (const [id, until] of this.#revokedGrants) {
|
|
4086
|
-
if (until <= now)
|
|
4087
|
-
this.#revokedGrants.delete(id);
|
|
4088
|
-
}
|
|
4089
|
-
for (const [id, t] of this.#revokedAddresses) {
|
|
4090
|
-
if (t.until <= now)
|
|
4091
|
-
this.#revokedAddresses.delete(id);
|
|
4092
|
-
}
|
|
4093
|
-
let n = 0;
|
|
4094
|
-
for (const [hash, r] of this.#records) {
|
|
4095
|
-
if (r.expiresAt <= now) {
|
|
4096
|
-
this.#records.delete(hash);
|
|
4097
|
-
n++;
|
|
4098
|
-
}
|
|
4099
|
-
}
|
|
4100
|
-
return n;
|
|
4101
|
-
}
|
|
4102
|
-
async close() {
|
|
4103
|
-
this.#records.clear();
|
|
4104
|
-
this.#revokedGrants.clear();
|
|
4105
|
-
this.#revokedAddresses.clear();
|
|
4106
|
-
}
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
4649
|
// src/store/sqlite.ts
|
|
4110
4650
|
import { Database } from "bun:sqlite";
|
|
4111
|
-
var SCHEMA_VERSION =
|
|
4651
|
+
var SCHEMA_VERSION = 5;
|
|
4652
|
+
var MIN_READER_VERSION = 5;
|
|
4112
4653
|
var REUSE_WINDOW_MS = 86400000;
|
|
4113
4654
|
function toRecord(row) {
|
|
4114
4655
|
const record = {
|
|
@@ -4127,6 +4668,8 @@ function toRecord(row) {
|
|
|
4127
4668
|
record.usedAt = row.used_at;
|
|
4128
4669
|
if (row.subject !== null)
|
|
4129
4670
|
record.subject = row.subject;
|
|
4671
|
+
if (row.client_auth === "private_key_jwt")
|
|
4672
|
+
record.clientAuth = "private_key_jwt";
|
|
4130
4673
|
return record;
|
|
4131
4674
|
}
|
|
4132
4675
|
|
|
@@ -4137,7 +4680,24 @@ class SqliteTokenStore {
|
|
|
4137
4680
|
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
4138
4681
|
if (path !== ":memory:")
|
|
4139
4682
|
this.#db.run("PRAGMA journal_mode = WAL");
|
|
4683
|
+
this.#checkReader();
|
|
4140
4684
|
this.#migrate();
|
|
4685
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS meta (
|
|
4686
|
+
key TEXT PRIMARY KEY,
|
|
4687
|
+
value INTEGER NOT NULL
|
|
4688
|
+
) STRICT`);
|
|
4689
|
+
this.#db.query(`INSERT INTO meta (key, value) VALUES ('min_reader_version', $v)
|
|
4690
|
+
ON CONFLICT (key) DO UPDATE SET value = max(value, excluded.value)`).run({ v: MIN_READER_VERSION });
|
|
4691
|
+
}
|
|
4692
|
+
#checkReader() {
|
|
4693
|
+
const table = this.#db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
|
4694
|
+
if (table === null)
|
|
4695
|
+
return;
|
|
4696
|
+
const row = this.#db.query("SELECT value FROM meta WHERE key = 'min_reader_version'").get();
|
|
4697
|
+
if (row !== null && row.value > SCHEMA_VERSION) {
|
|
4698
|
+
this.#db.close();
|
|
4699
|
+
throw new Error(`the token store file needs a newer relay (schema ${row.value}; this relay knows ${SCHEMA_VERSION})`);
|
|
4700
|
+
}
|
|
4141
4701
|
}
|
|
4142
4702
|
#migrate() {
|
|
4143
4703
|
const row = this.#db.query("PRAGMA user_version").get();
|
|
@@ -4163,12 +4723,20 @@ class SqliteTokenStore {
|
|
|
4163
4723
|
issued_at INTEGER NOT NULL,
|
|
4164
4724
|
expires_at INTEGER NOT NULL,
|
|
4165
4725
|
used_at INTEGER,
|
|
4166
|
-
subject TEXT
|
|
4726
|
+
subject TEXT,
|
|
4727
|
+
client_auth TEXT
|
|
4167
4728
|
) STRICT`);
|
|
4168
4729
|
const columns = this.#db.query("PRAGMA table_info(tokens)").all();
|
|
4169
4730
|
if (!columns.some((c) => c.name === "subject")) {
|
|
4170
4731
|
this.#db.run("ALTER TABLE tokens ADD COLUMN subject TEXT");
|
|
4171
4732
|
}
|
|
4733
|
+
if (!columns.some((c) => c.name === "client_auth")) {
|
|
4734
|
+
this.#db.run("ALTER TABLE tokens ADD COLUMN client_auth TEXT");
|
|
4735
|
+
}
|
|
4736
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS nonces (
|
|
4737
|
+
key TEXT PRIMARY KEY,
|
|
4738
|
+
expires_at INTEGER NOT NULL
|
|
4739
|
+
) STRICT`);
|
|
4172
4740
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_grant ON tokens (grant_id)");
|
|
4173
4741
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_address ON tokens (address_id)");
|
|
4174
4742
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_expires ON tokens (expires_at)");
|
|
@@ -4193,9 +4761,11 @@ class SqliteTokenStore {
|
|
|
4193
4761
|
}
|
|
4194
4762
|
#insert(r) {
|
|
4195
4763
|
this.#db.query(`INSERT OR REPLACE INTO tokens (hash, kind, grant_id, address_id, client_id,
|
|
4196
|
-
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject
|
|
4764
|
+
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject,
|
|
4765
|
+
client_auth)
|
|
4197
4766
|
VALUES ($hash, $kind, $grant_id, $address_id, $client_id, $client_name,
|
|
4198
|
-
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject
|
|
4767
|
+
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject,
|
|
4768
|
+
$client_auth)`).run({
|
|
4199
4769
|
hash: r.hash,
|
|
4200
4770
|
kind: r.kind,
|
|
4201
4771
|
grant_id: r.grantId,
|
|
@@ -4207,7 +4777,8 @@ class SqliteTokenStore {
|
|
|
4207
4777
|
issued_at: r.issuedAt,
|
|
4208
4778
|
expires_at: r.expiresAt,
|
|
4209
4779
|
used_at: r.usedAt ?? null,
|
|
4210
|
-
subject: r.subject ?? null
|
|
4780
|
+
subject: r.subject ?? null,
|
|
4781
|
+
client_auth: r.clientAuth ?? null
|
|
4211
4782
|
});
|
|
4212
4783
|
}
|
|
4213
4784
|
async get(hash) {
|
|
@@ -4243,8 +4814,15 @@ class SqliteTokenStore {
|
|
|
4243
4814
|
}
|
|
4244
4815
|
async deleteExpired(now) {
|
|
4245
4816
|
this.#db.query("DELETE FROM revocations WHERE keep_until <= $now").run({ now });
|
|
4817
|
+
this.#db.query("DELETE FROM nonces WHERE expires_at <= $now").run({ now });
|
|
4246
4818
|
return this.#db.query("DELETE FROM tokens WHERE expires_at <= $now").run({ now }).changes;
|
|
4247
4819
|
}
|
|
4820
|
+
async claimNonce(key, expiresAt, now) {
|
|
4821
|
+
const result = this.#db.query(`INSERT INTO nonces (key, expires_at) VALUES ($key, $expires_at)
|
|
4822
|
+
ON CONFLICT (key) DO UPDATE SET expires_at = excluded.expires_at
|
|
4823
|
+
WHERE nonces.expires_at <= $now`).run({ key, expires_at: expiresAt, now });
|
|
4824
|
+
return result.changes === 1;
|
|
4825
|
+
}
|
|
4248
4826
|
async close() {
|
|
4249
4827
|
this.#db.close();
|
|
4250
4828
|
}
|
|
@@ -5288,5 +5866,5 @@ var shutdown = () => {
|
|
|
5288
5866
|
process.on("SIGTERM", shutdown);
|
|
5289
5867
|
process.on("SIGINT", shutdown);
|
|
5290
5868
|
|
|
5291
|
-
//# debugId=
|
|
5869
|
+
//# debugId=0F130F38D40E142464756E2164756E21
|
|
5292
5870
|
//# sourceMappingURL=main.js.map
|