@ractive-ch/tab-mcp 0.5.0 → 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/icons.d.ts +6 -0
- package/index.js +774 -170
- package/index.js.map +14 -11
- package/jwt.d.ts +10 -2
- package/main.js +774 -170
- package/main.js.map +14 -11
- package/oauth/cimd.d.ts +49 -4
- package/oauth/client-auth.d.ts +86 -0
- package/oauth/domain-cache.d.ts +24 -0
- package/oauth/server.d.ts +1 -0
- package/package.json +2 -2
- package/store/memory.d.ts +13 -0
- package/store/sqlite.d.ts +2 -0
- package/store/types.d.ts +17 -0
- package/testing.js +34 -1
- package/testing.js.map +3 -3
- package/tokens.d.ts +4 -1
package/index.js
CHANGED
|
@@ -462,6 +462,7 @@ var MAX_JWT_BYTES = 8192;
|
|
|
462
462
|
var MAX_JWKS_BYTES = 65536;
|
|
463
463
|
var JWKS_TTL_MS = 10 * 60000;
|
|
464
464
|
var JWKS_REFETCH_MS = 30000;
|
|
465
|
+
var UNKNOWN_KID_REFETCH_MS = 5000;
|
|
465
466
|
var JWKS_GRACE_MS = 60 * 60000;
|
|
466
467
|
var FETCH_TIMEOUT_MS = 5000;
|
|
467
468
|
|
|
@@ -626,6 +627,7 @@ class HttpJsonError extends Error {
|
|
|
626
627
|
class JwksCache {
|
|
627
628
|
#url;
|
|
628
629
|
#now;
|
|
630
|
+
#load;
|
|
629
631
|
#keys = [];
|
|
630
632
|
#fetchedAt = 0;
|
|
631
633
|
#attemptedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -634,14 +636,17 @@ class JwksCache {
|
|
|
634
636
|
constructor(url, options = {}) {
|
|
635
637
|
this.#url = typeof url === "string" ? () => Promise.resolve(url) : url;
|
|
636
638
|
this.#now = options.now ?? Date.now;
|
|
639
|
+
this.#load = options.load ?? ((u) => fetchJson(u));
|
|
637
640
|
}
|
|
638
641
|
async keys(kid) {
|
|
642
|
+
const entered = this.#now();
|
|
639
643
|
if (this.#inflight !== undefined)
|
|
640
644
|
await this.#inflight.catch(() => {});
|
|
641
645
|
const now = this.#now();
|
|
642
646
|
const stale = now - this.#fetchedAt >= JWKS_TTL_MS;
|
|
643
|
-
const unknown = kid !== undefined && !this.#
|
|
644
|
-
|
|
647
|
+
const unknown = kid !== undefined && !this.#has(kid);
|
|
648
|
+
const floor = unknown && this.#lastError === undefined ? UNKNOWN_KID_REFETCH_MS : JWKS_REFETCH_MS;
|
|
649
|
+
if ((stale || unknown) && now - this.#attemptedAt >= floor) {
|
|
645
650
|
try {
|
|
646
651
|
await this.#refresh();
|
|
647
652
|
} catch (error) {
|
|
@@ -651,13 +656,21 @@ class JwksCache {
|
|
|
651
656
|
if (this.#now() - this.#fetchedAt >= JWKS_TTL_MS + JWKS_GRACE_MS) {
|
|
652
657
|
throw this.#lastError ?? new Error("the JWKS could not be loaded");
|
|
653
658
|
}
|
|
654
|
-
|
|
659
|
+
if (kid === undefined)
|
|
660
|
+
return this.#keys;
|
|
661
|
+
if (!this.#has(kid) && this.#fetchedAt < entered) {
|
|
662
|
+
throw this.#lastError ?? new Error("an unknown key id; the JWKS was fetched too recently");
|
|
663
|
+
}
|
|
664
|
+
return this.#keys.filter((k) => k.kid === kid);
|
|
665
|
+
}
|
|
666
|
+
#has(kid) {
|
|
667
|
+
return this.#keys.some((k) => k.kid === kid);
|
|
655
668
|
}
|
|
656
669
|
#refresh() {
|
|
657
670
|
this.#inflight ??= (async () => {
|
|
658
671
|
this.#attemptedAt = this.#now();
|
|
659
672
|
try {
|
|
660
|
-
const doc = await
|
|
673
|
+
const doc = await this.#load(await this.#url());
|
|
661
674
|
const keys = doc?.keys;
|
|
662
675
|
if (!Array.isArray(keys))
|
|
663
676
|
throw new Error("a JWKS needs keys");
|
|
@@ -1205,7 +1218,7 @@ function parseTabMessage(text) {
|
|
|
1205
1218
|
}
|
|
1206
1219
|
}
|
|
1207
1220
|
// package.json
|
|
1208
|
-
var version = "0.
|
|
1221
|
+
var version = "0.6.0";
|
|
1209
1222
|
|
|
1210
1223
|
// src/exchange.ts
|
|
1211
1224
|
var RelayErrorCode = {
|
|
@@ -1550,6 +1563,11 @@ class Exchange {
|
|
|
1550
1563
|
}
|
|
1551
1564
|
}
|
|
1552
1565
|
|
|
1566
|
+
// src/icons.ts
|
|
1567
|
+
var FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><title>tab-mcp relay</title><rect width="64" height="64" rx="14" fill="#1d4ed8"/><path d="M32 22l3.4-7A3.5 3.5 0 0 1 38.6 13h9.8a3.5 3.5 0 0 1 3.2 2l3.4 7z" fill="#fff" fill-opacity=".45"/><path d="M9 49V26a5 5 0 0 1 5-5h.5l3.7-7.4A3.5 3.5 0 0 1 21.3 12h9.4a3.5 3.5 0 0 1 3.1 1.6L37.5 21H50a5 5 0 0 1 5 5v23a5 5 0 0 1-5 5H14a5 5 0 0 1-5-5z" fill="#fff"/><rect x="22" y="35.5" width="20" height="5" fill="#1d4ed8"/><circle cx="21" cy="38" r="6" fill="#1d4ed8"/><circle cx="43" cy="38" r="6" fill="#1d4ed8"/></svg>';
|
|
1568
|
+
var FAVICON_ICO = new Uint8Array(Buffer.from("AAABAAMAEBAAAAEAIABoBAAANgAAACAgAAABACAAqBAAAJ4EAAAwMAAAAQAgAKglAABGFQAAKAAAABAAAAAgAAAAAQAgAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAANhOHQfYTh1x2E4d5dhOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3n2E4dd9hOHQnYTh1u2E4d89hNG//XTBr/2Ewa/9hMGv/YTBr/2Ewa/9hMGv/YTBr/2Ewa/9hMGv/XTBr/2E0b/9hOHfXYTh132E4d39hNHP/ea0L/66WM/+2slP/tq5T/7auU/+2rlP/tq5T/7auU/+2rlP/trJT/66aN/99uRv/YTRz/2E4d59hOHf7YTx//8b2r///////////////////////////////////////////////////////yw7P/2VEg/9hOHf/YTh3/2VIi//TMvf///////fbz//vs5////v7/////////////////++3o//318v//////9dHE/9lUJP/YTh3/2E4d/9lSIv/0zL7//PLu/+SGZf/dZjz/7a6X//fZzv/32c7/7rGc/95nPf/kg2D/++/r//XSxf/ZUyT/2E4d/9hOHf/ZUiL/9M7A//bWy//ZUiL/10oX/9lUJf/cYjf/3GI3/9pVJv/XShj/2FAf//XRxP/108f/2VMk/9hOHf/YTh3/2VIi//TMvv/88/D/5Ytr/95qQf/us57/+N7U//je1P/vt6P/32xD/+WHZv/88e3/9dLF/9lTJP/YTh3/2E4d/9lSIv/0y73///////349v/88Ov///////////////////////zw7P/99/X///////XRxP/ZVCT/2E4d/9hOHf/YUCD/8sa2///////////////////////////////////////////////////////zy7z/2VIi/9hOHf/YTh3/2E0c/+J9WP/21cn////+///////////////////////77Ob/+eLa//nj2//44dn/6Jh7/9hNHP/YTh3/2E4d/9hOHf/XTBr/3WY8//nm3//////////////////88/D/7a+Y/+qghv/qoYf/6p2D/95oPv/YTRz/2E4d/9hOHf3YTh3/2E4d/9hNHP/pm3//+unj//rq5P/66eP/7KiQ/+N/W//khmX/5Idl/+F4U//ZUiL/2E4d/9hOHf/YTh3d2E4d/9hOHf/YTh3/2VIi/9xhNv/dZDn/3GI2/9lSIv/YTh3/2FAf/9hQH//YTh3/2E4d/9hOHf/YTh3l2E4daNhOHfHYTh3/2E4d/9hOHf/YTRv/2E0b/9hNG//YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3z2E4dcdhOHQXYTh1o2E4d3dhOHf3YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf7YTh3f2E4dbthOHQcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAACAAAABAAAAAAQAgAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAANhOHQDYTh0A2E4dANhOHTrYTh2l2E4d6thOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d7thOHazYTh1C2E4dAdhOHQDYTh0A2E4dANhOHQTYTh1p2E4d5NhOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHenYTh112E4dB9hOHQDYTh0A2E4dZthOHfbYTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHfrYTh112E4dAdhOHTTYTh3g2E4d/9hOHf/YTh3/2E4d/9hNHP/YTBv/2Ewb/9hMG//YTBv/2Ewb/9hMG//YTBv/2Ewb/9hMG//YTBv/2Ewb/9hMG//YTBv/2Ewb/9hMG//YTBv/2Ewb/9hMG//YTRv/2E4d/9hOHf/YTh3/2E4d/9hOHenYTh1C2E4dmNhOHf/YTh3/2E4d/9hNHP/YTx7/3F8z/95pQP/eaUD/3mlA/95pQP/eaUD/3mlA/95pQP/eaUD/3mlA/95pQP/eaUD/3mlA/95pQP/eaUD/3mlA/95pQP/eaUD/3mlA/9xhNf/YTx//2E0c/9hOHf/YTh3/2E4d/9hOHazYTh3e2E4d/9hOHf/YTh3/2VMj/+qghv/55N3/++/q//vv6v/77+r/++/q//vv6v/77+r/++/q//vv6v/77+r/++/q//vv6v/77+r/++/q//vv6v/77+r/++/q//vv6v/77+v/+ubf/+yokP/aVSb/2E0c/9hOHf/YTh3/2E4d7thOHfnYTh3/2E4d/9dMGv/jf1z//fj2/////////////////////////////////////////////////////////////////////////////////////////////////////////////vz7/+WKav/XSxr/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/10sZ/+qghv//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7a2W/9hMG//YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/XSxr/66SL///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////usZv/2Ewb/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/rpIv///////////////////////zz8P/55d7//PLu/////////////////////////////////////////////PPw//nl3v/88u7//////////////////////+6xm//YTBv/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/10sa/+uki//////////////////zx7j/4XVO/9xeMv/gcUn/8cCu//////////////////////////////////PHuP/gdE7/3F4y/+BxSf/xv67///79////////////7rGb/9hMG//YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/XSxr/66SL////////////+N7U/9xhNf/XSxn/2E0c/9dLGf/bWy7/7rCa//LDs//ywrH/8sKx//LDsv/vtaD/3GE1/9dLGf/YTRz/10sZ/9tbLv/21cn////////////usZv/2Ewb/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/rpIv////////////usZz/2Ewb/9hOHf/YTh3/2E4d/9hNHP/YTh3/2E8e/9hPHv/YTx7/2E8e/9hOHf/YTRz/2E4d/9hOHf/YTh3/10sZ/+uljP///////////+6xm//YTBv/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/10sa/+uki////////////+6znv/YTBv/2E4d/9hOHf/YTh3/2E0c/9lQIP/ZUiP/2VIi/9lSIv/ZUiP/2VEh/9hNHP/YTh3/2E4d/9hOHf/XSxn/7KeO////////////7rGb/9hMG//YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/XSxr/66SL////////////+eLa/91mPP/XShj/2Ewb/9dLGf/cYDT/8Luo//TPwf/0zcD/9M3A//TOwf/xwK//3WU7/9dKGP/YTBv/10sZ/9xgM//32s/////////////usZv/2Ewb/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/rpIv/////////////////9dDD/+N/W//dZjv/4npW//PJuv/////////////////////////////////10MP/439b/91mO//ie1b/88m6/////////////////+6xm//YTBv/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/10sa/+uki////////////////////////fj2//vt5//99/T////////////////////////////////////////////9+Pb/++3n//339P//////////////////////7rGb/9hMG//YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/XSxr/66SL///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////usZv/2Ewb/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/rpIv//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+6wm//YTBv/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/10sZ/+eSdP//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////6p6D/9dLGv/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTRv/3GE2//XTxv////////////////////////////////////////////////////////////////////////////////////////////////////////////nj2//fbkb/2Ewb/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTRz/3GE2/+eSc//21sv///////////////////////////////////////////////////////vs5//0zb//9M2///TNv//0zb//9M2///TNv//yxbX/66aN/91kOf/YTRz/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTRv/10kX/+N/W//99/T////////////////////////////////////////////+/fz/8b6s/+mbf//pnIH/6ZyB/+mcgf/pnIH/6ZyB/+mdgv/liWn/2VMj/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YThz/2VMk//LGtv////////////////////////////////////////////nl3v/ro4n/6Z2C/+qeg//qnoP/6p6D/+qeg//qnoP/6Z2B/95rQv/YTRz/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/XTBr/439b//339f/////////////////////////////////++vn/6p+F/+iWef/qn4T/6p+E/+qfhP/qn4T/6p+E/+qfhP/liGf/2VMk/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d+NhOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/ZUiP/7KiP//nm3//66uT/+urk//rq5P/66uT/+ufg/+6wmv/aVij/3WQ5/+B0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Hf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3y2E4dUNhOHQDYTh0L2E4dr9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4dxdhOHRfYTh1M2E4d8thOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hNHP/XSxn/2Ewb/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0c/9hNHP/YTRz/2E0b/9dLGf/YTRv/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d+thOHWfYTh2f2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0b/9xhNv/nlHb/7rCa/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/77Wg/++1oP/vtaD/7rGc/+iYfP/eZz3/2E0c/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHbzYTh3Y2E4d/9hOHf/YTh3/2E4d/9hOHf/YTBv/4HFJ//fZz//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////54tr/4nxY/9hMG//YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHfDYTh3z2E4d/9hOHf/YTh3/2E4d/9hNHP/aVif/88q7////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////99nO/9tdMP/YTRz/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3+2E4d/9hOHf/YTh3/2E4d/9hMG//fbkX//PLu/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////vn4/+J8WP/XSxr/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/ifVn//vn4//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7+/+aObv/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////////////////////zy7v/44dn/+ebf//759//////////////////////////////////////////////////////////////////++/r/+ujh//jg2P/87+v///////////////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4///////////////////////99fP/7a+Y/99wSP/bXC//3GA0/+OBX//0z8H///////////////////////////////////////////////////////bYzf/liWj/3GI3/9tcLv/fbEP/66WM//zw7P////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////759//ol3v/2E4d/9dMGv/YTRz/2E0b/9dLGf/bXTD/88i4////////////////////////////////////////////9tXK/91lOv/XSxn/2E0b/9hNHP/YTBr/2E0b/+WJaP/88/D///////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////PHuP/ZUyP/2E0c/9hOHf/YTh3/2E4d/9hOHf/YTBr/3mk//+ynjv/sqpL/7KmS/+ypkv/sqZL/7KmS/+ypkv/sqZH/4HJL/9dMGv/YTh3/2E4d/9hOHf/YTh3/2E4d/9hPHv/vtqL///////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4/////////////////+eUd//XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0c/9dMGv/XTBr/10wa/9dMGv/XTBr/10wa/9dMGv/XTBr/2E0b/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGf/kg2D//vv6//////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4/////////////vz8/+SFY//XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dMGv/hdU7//fb0//////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4/////////////////+iZfP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0c/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0c/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGf/lh2b//vz7//////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////TOwf/aVyj/2E0c/9hOHf/YTh3/2E4d/9hOHf/XSxr/4HJK//C5pv/wvKr/8Lyp//C8qf/wvKn/8Lyp//C8qv/wvKn/4nxY/9dLGv/YTh3/2E4d/9hOHf/YTh3/2E0c/9lRIf/xvq3///////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////78+//rpYz/2VIj/9dLGf/YTBv/2Ewa/9dLGf/dZTv/9dPG////////////////////////////////////////////+N/W/99vR//XSxn/2Ewa/9hMG//XSxr/2E8f/+iWef/9+Pb///////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4///////////////////////++vn/8b+u/+N/W//eZz3/32xE/+eSdP/33NL///////////////////////////////////////////////////////nk3P/pmn7/329H/91mPP/ielX/77ai//329P////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////////////////////759//77ej//PHt///9/f////////////////////////////////////////////////////////////////////7//PLu//vs5//99/X///////////////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////7+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/jflr//vr4///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+/+aPcP/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9dLGv/heVT//fj2//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////39/+WKaf/XSxn/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hNG//dZTr/+urk/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////PPw/+BySv/YTBr/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTx//7KuU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8Luo/9lTJP/YThz/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTRz/21os/+yrlP/66uT//vn3//////////////////////////////////////////////////////////////////////////////////////////////38//78+//+/Pv//vz7//78+//+/Pv//vz7//78+//+/Pv//vv6//318v/21cn/5oxs/9pYKv/YTRz/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0c/9hPH//dZDn/44Jf//nk3f/////////////////////////////////////////////////////////////////////////////////99fP/8cGv/++3pP/vuKT/77ik/++4pP/vuKT/77ik/++4pP/vuKT/77ai/+2slf/qoIb/5Ytr/9lUJf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTBv/10oZ/+qfhf/////////////////////////////////////////////////////////////////////////////////21Mj/6Z2C/+mcgf/pnIH/6ZyB/+mcgf/pnIH/6ZyB/+mcgf/pnIH/6ZyB/+mdgv/pnIH/32xD/9hNHP/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0b/9xiN//54tr///////////////////////////////////////////////////////////////////////318v/tr5n/6Z2B/+qeg//qnoP/6p6D/+qeg//qnoP/6p6D/+qeg//qnoP/6p6D/+qfhP/mjG3/2VQl/9hOHP/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hMG//qoIX///////////////////////////////////////////////////////////////////////bUyP/qnoP/6p6D/+qeg//qnoP/6p6D/+qeg//qnoP/6p6D/+qeg//qnoP/6p6D/+mdgv/fbkb/2E0c/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hNG//cYjf/+eLa////////////////////////////////////////////////////////////++zn/+iVeP/pnYL/6p6D/+qeg//qnoP/6p6D/+qeg//qnoP/6p6D/+qeg//qnoP/6p+F/+aNbv/aVSb/2E4c/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3+2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTBv/6Z2C////////////////////////////////////////////////////////////7a2X/9pWJ//kgmD/6Zl9/+mbgP/pm4D/6ZuA/+mbgP/pm4D/6ZuA/+mbgP/pm3//5pBx/91kOf/YTRz/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3x2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTRz/2lcp/+yokP/44dj/+urk//rq5P/66uT/+urk//rq5P/66uT/+urk//nj2//us57/210w/9hMG//ZUiL/3F8y/91jOP/dZDn/3WQ5/91kOf/dZDn/3WQ5/91kOf/cYjb/2lYo/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3T2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E0c/9hPHv/bXC//3WM4/91jOP/dYzj/3WM4/91jOP/dYzj/3WM4/9teMf/YUB//2E0c/9hOHf/YTh3/2E0c/9hNG//YTRv/2E0b/9hNG//YTRv/2E0b/9hNG//YTRz/2E0c/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHevYTh2W2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTRz/2E0b/9hNG//YTRv/2E0b/9hNG//YTRv/2E0b/9hNHP/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHbPYTh1D2E4d7thOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d99hOHVzYTh0H2E4do9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4dudhOHRDYTh0A2E4dL9hOHd7YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3q2E4dQdhOHQDYTh0A2E4dANhOHVPYTh3r2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHfTYTh1o2E4dANhOHQDYTh0A2E4dANhOHQDYTh1T2E4d3thOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d59hOHWXYTh0D2E4dANhOHQAAAAAA2E4dANhOHQDYTh0A2E4dL9hOHaPYTh3u2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHfLYTh2v2E4dO9hOHQDYTh0A2E4dAAAAAAAAAAAAAAAAANhOHQDYTh0A2E4dANhOHQfYTh1D2E4dlthOHdPYTh3x2E4d/thOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/9hOHf/YTh3/2E4d/thOHfPYTh3Y2E4dn9hOHUzYTh0L2E4dANhOHQDYTh0AAAAAAAAAAADYAAAAABsAALAAAAAABQAAQAAAAAACAADAAAAAAAEAAIAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAQAAwAAAAAADAABgAAAAAAIAALAAAAAADQAA2AAAAAAbAAA=", "base64"));
|
|
1569
|
+
var APPLE_TOUCH_ICON = new Uint8Array(Buffer.from("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", "base64"));
|
|
1570
|
+
|
|
1553
1571
|
// src/notopen.ts
|
|
1554
1572
|
var RETRYABLE = new Set(["tools/list"]);
|
|
1555
1573
|
function isRetryable(method) {
|
|
@@ -1677,6 +1695,142 @@ class RateLimiter {
|
|
|
1677
1695
|
}
|
|
1678
1696
|
}
|
|
1679
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
|
+
|
|
1680
1834
|
// src/tokens.ts
|
|
1681
1835
|
var PREFIX = {
|
|
1682
1836
|
access: "tmcp_at_",
|
|
@@ -1706,7 +1860,8 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1706
1860
|
scopes: grant.scopes,
|
|
1707
1861
|
grantCreatedAt: grant.grantCreatedAt,
|
|
1708
1862
|
issuedAt: now,
|
|
1709
|
-
...grant.subject === undefined ? {} : { subject: grant.subject }
|
|
1863
|
+
...grant.subject === undefined ? {} : { subject: grant.subject },
|
|
1864
|
+
...grant.clientAuth === undefined ? {} : { clientAuth: grant.clientAuth }
|
|
1710
1865
|
};
|
|
1711
1866
|
const accessExpires = Math.min(now + options.accessTtlMs, options.grantEndsAt);
|
|
1712
1867
|
const refreshStored = await store.put({
|
|
@@ -1732,6 +1887,75 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1732
1887
|
};
|
|
1733
1888
|
}
|
|
1734
1889
|
|
|
1890
|
+
// src/oauth/domain-cache.ts
|
|
1891
|
+
class DomainCache {
|
|
1892
|
+
#entries = new Map;
|
|
1893
|
+
#counts = new Map;
|
|
1894
|
+
#max;
|
|
1895
|
+
#perDomain;
|
|
1896
|
+
#isDead;
|
|
1897
|
+
constructor(max, perDomain, isDead = () => false) {
|
|
1898
|
+
this.#max = max;
|
|
1899
|
+
this.#perDomain = perDomain;
|
|
1900
|
+
this.#isDead = isDead;
|
|
1901
|
+
}
|
|
1902
|
+
get size() {
|
|
1903
|
+
return this.#entries.size;
|
|
1904
|
+
}
|
|
1905
|
+
get(key) {
|
|
1906
|
+
const entry = this.#entries.get(key);
|
|
1907
|
+
if (entry === undefined)
|
|
1908
|
+
return;
|
|
1909
|
+
this.#entries.delete(key);
|
|
1910
|
+
this.#entries.set(key, entry);
|
|
1911
|
+
return entry.value;
|
|
1912
|
+
}
|
|
1913
|
+
delete(key) {
|
|
1914
|
+
const entry = this.#entries.get(key);
|
|
1915
|
+
if (entry === undefined)
|
|
1916
|
+
return;
|
|
1917
|
+
this.#entries.delete(key);
|
|
1918
|
+
const left = (this.#counts.get(entry.domain) ?? 1) - 1;
|
|
1919
|
+
if (left > 0)
|
|
1920
|
+
this.#counts.set(entry.domain, left);
|
|
1921
|
+
else
|
|
1922
|
+
this.#counts.delete(entry.domain);
|
|
1923
|
+
}
|
|
1924
|
+
set(key, domain, value, now) {
|
|
1925
|
+
this.delete(key);
|
|
1926
|
+
if ((this.#counts.get(domain) ?? 0) >= this.#perDomain) {
|
|
1927
|
+
this.#evictOldestOf(domain);
|
|
1928
|
+
} else if (this.#entries.size >= this.#max) {
|
|
1929
|
+
this.#evict(now);
|
|
1930
|
+
}
|
|
1931
|
+
this.#entries.set(key, { domain, value });
|
|
1932
|
+
this.#counts.set(domain, (this.#counts.get(domain) ?? 0) + 1);
|
|
1933
|
+
}
|
|
1934
|
+
#evict(now) {
|
|
1935
|
+
for (const [key, entry] of this.#entries) {
|
|
1936
|
+
if (this.#isDead(entry.value, now)) {
|
|
1937
|
+
this.delete(key);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
const most = Math.max(0, ...this.#counts.values());
|
|
1942
|
+
for (const [key, entry] of this.#entries) {
|
|
1943
|
+
if (this.#counts.get(entry.domain) === most) {
|
|
1944
|
+
this.delete(key);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
#evictOldestOf(domain) {
|
|
1950
|
+
for (const [key, entry] of this.#entries) {
|
|
1951
|
+
if (entry.domain === domain) {
|
|
1952
|
+
this.delete(key);
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1735
1959
|
// src/oauth/cimd.ts
|
|
1736
1960
|
class ClientMetadataError extends Error {
|
|
1737
1961
|
name = "ClientMetadataError";
|
|
@@ -1741,6 +1965,16 @@ class ClientMetadataError extends Error {
|
|
|
1741
1965
|
this.reason = reason;
|
|
1742
1966
|
}
|
|
1743
1967
|
}
|
|
1968
|
+
var TRANSIENT_REASONS = new Set([
|
|
1969
|
+
"rate_limited",
|
|
1970
|
+
"fetch_failed",
|
|
1971
|
+
"unresolvable",
|
|
1972
|
+
"too_large"
|
|
1973
|
+
]);
|
|
1974
|
+
function isTransient(error) {
|
|
1975
|
+
return TRANSIENT_REASONS.has(error.reason);
|
|
1976
|
+
}
|
|
1977
|
+
var STALE_GRACE_MS = 60 * 60000;
|
|
1744
1978
|
var MAX_CLIENT_ID_LENGTH = 2048;
|
|
1745
1979
|
var MAX_NAME_LENGTH = 64;
|
|
1746
1980
|
var MAX_REDIRECT_URIS = 20;
|
|
@@ -1748,6 +1982,9 @@ var MIN_CACHE_MS = 5 * 60000;
|
|
|
1748
1982
|
var DEFAULT_CACHE_MS = 60 * 60000;
|
|
1749
1983
|
var MAX_CACHE_MS = 24 * 60 * 60000;
|
|
1750
1984
|
var MAX_CACHE_ENTRIES = 1000;
|
|
1985
|
+
var MAX_CACHE_PER_DOMAIN = 100;
|
|
1986
|
+
var FAILURE_CACHE_MS = 60000;
|
|
1987
|
+
var MAX_FAILURES = 1000;
|
|
1751
1988
|
async function defaultLookup(hostname) {
|
|
1752
1989
|
const records = await Bun.dns.lookup(hostname, {});
|
|
1753
1990
|
return records.map((r) => r.address);
|
|
@@ -1781,12 +2018,22 @@ function limitKey(hostname) {
|
|
|
1781
2018
|
return h;
|
|
1782
2019
|
return h.split(".").slice(-2).join(".");
|
|
1783
2020
|
}
|
|
2021
|
+
function knownBudget() {
|
|
2022
|
+
return {
|
|
2023
|
+
perHost: new RateLimiter({ capacity: 5, refillPerSecond: 0.2 }),
|
|
2024
|
+
perDomain: new RateLimiter({ capacity: 15, refillPerSecond: 0.6 }),
|
|
2025
|
+
global: new RateLimiter({ capacity: 100, refillPerSecond: 10 })
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
1784
2028
|
|
|
1785
2029
|
class ClientMetadataFetcher {
|
|
1786
|
-
#cache = new
|
|
2030
|
+
#cache = new DomainCache(MAX_CACHE_ENTRIES, MAX_CACHE_PER_DOMAIN, (entry, now) => entry.until + STALE_GRACE_MS <= now);
|
|
2031
|
+
#failures = new Map;
|
|
1787
2032
|
#perHost = new RateLimiter({ capacity: 5, refillPerSecond: 0.2 });
|
|
1788
2033
|
#perDomain = new RateLimiter({ capacity: 15, refillPerSecond: 0.6 });
|
|
1789
2034
|
#global = new RateLimiter({ capacity: 30, refillPerSecond: 1 });
|
|
2035
|
+
#refreshes = knownBudget();
|
|
2036
|
+
#keys = knownBudget();
|
|
1790
2037
|
#allowedHosts;
|
|
1791
2038
|
#logger;
|
|
1792
2039
|
#timeoutMs;
|
|
@@ -1805,34 +2052,106 @@ class ClientMetadataFetcher {
|
|
|
1805
2052
|
this.#testMode = options.testMode;
|
|
1806
2053
|
this.#now = options.now ?? Date.now;
|
|
1807
2054
|
}
|
|
1808
|
-
async get(clientId) {
|
|
2055
|
+
async get(clientId, options = {}) {
|
|
1809
2056
|
const url = this.#checkUrl(clientId);
|
|
1810
2057
|
const now = this.#now();
|
|
1811
2058
|
const cached = this.#cache.get(clientId);
|
|
1812
|
-
if (cached !== undefined && cached.until > now)
|
|
1813
|
-
this.#cache.delete(clientId);
|
|
1814
|
-
this.#cache.set(clientId, cached);
|
|
2059
|
+
if (cached !== undefined && cached.until > now)
|
|
1815
2060
|
return cached.meta;
|
|
2061
|
+
const known = cached !== undefined && now < cached.until + STALE_GRACE_MS;
|
|
2062
|
+
const stale = options.allowStale === true && known ? cached?.meta : undefined;
|
|
2063
|
+
try {
|
|
2064
|
+
const failed = this.#failures.get(clientId);
|
|
2065
|
+
if (failed !== undefined && failed.until > now)
|
|
2066
|
+
throw failed.error;
|
|
2067
|
+
return await this.#refresh(clientId, url, now, known);
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
const transient = error instanceof ClientMetadataError && isTransient(error);
|
|
2070
|
+
if (!transient)
|
|
2071
|
+
this.#cache.delete(clientId);
|
|
2072
|
+
if (stale === undefined || !transient)
|
|
2073
|
+
throw error;
|
|
2074
|
+
this.#logger?.info("oauth.cimd_stale", {
|
|
2075
|
+
host: url.host,
|
|
2076
|
+
reason: error.reason
|
|
2077
|
+
});
|
|
2078
|
+
return stale;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
async#refresh(clientId, url, now, known) {
|
|
2082
|
+
this.#admit(url, now, known ? "refresh" : "new");
|
|
2083
|
+
let meta;
|
|
2084
|
+
let ttlMs;
|
|
2085
|
+
try {
|
|
2086
|
+
const address = await this.#vettedAddress(url);
|
|
2087
|
+
const response = await this.#fetchJson(url, address, [
|
|
2088
|
+
"application/json"
|
|
2089
|
+
]);
|
|
2090
|
+
meta = validateDocument(response.doc, clientId, url, {
|
|
2091
|
+
test: this.#testMode !== undefined,
|
|
2092
|
+
onFallback: (reason) => this.#logger?.info("oauth.cimd_auth_fallback", {
|
|
2093
|
+
host: url.host,
|
|
2094
|
+
reason
|
|
2095
|
+
})
|
|
2096
|
+
});
|
|
2097
|
+
ttlMs = cacheMs(response.cacheControl);
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
if (error instanceof ClientMetadataError) {
|
|
2100
|
+
this.#rememberFailure(clientId, error, now);
|
|
2101
|
+
}
|
|
2102
|
+
throw error;
|
|
1816
2103
|
}
|
|
2104
|
+
this.#failures.delete(clientId);
|
|
2105
|
+
this.#cache.set(clientId, limitKey(url.hostname), { meta, until: now + ttlMs }, now);
|
|
2106
|
+
return meta;
|
|
2107
|
+
}
|
|
2108
|
+
#rememberFailure(clientId, error, now) {
|
|
2109
|
+
this.#failures.delete(clientId);
|
|
2110
|
+
if (this.#failures.size >= MAX_FAILURES) {
|
|
2111
|
+
const oldest = this.#failures.keys().next().value;
|
|
2112
|
+
if (oldest !== undefined)
|
|
2113
|
+
this.#failures.delete(oldest);
|
|
2114
|
+
}
|
|
2115
|
+
this.#failures.set(clientId, { error, until: now + FAILURE_CACHE_MS });
|
|
2116
|
+
}
|
|
2117
|
+
async fetchJwks(jwksUri) {
|
|
2118
|
+
const url = this.#checkJwksUri(jwksUri);
|
|
2119
|
+
this.#admit(url, this.#now(), "keys");
|
|
2120
|
+
const address = await this.#vettedAddress(url);
|
|
2121
|
+
const response = await this.#fetchJson(url, address, [
|
|
2122
|
+
"application/json",
|
|
2123
|
+
"application/jwk-set+json"
|
|
2124
|
+
]);
|
|
2125
|
+
return response.doc;
|
|
2126
|
+
}
|
|
2127
|
+
#admit(url, now, budget) {
|
|
2128
|
+
const { perHost, perDomain, global } = budget === "refresh" ? this.#refreshes : budget === "keys" ? this.#keys : {
|
|
2129
|
+
perHost: this.#perHost,
|
|
2130
|
+
perDomain: this.#perDomain,
|
|
2131
|
+
global: this.#global
|
|
2132
|
+
};
|
|
1817
2133
|
const key = limitKey(url.hostname);
|
|
1818
|
-
if (!
|
|
2134
|
+
if (!perHost.available(url.host, now) || !perDomain.available(key, now)) {
|
|
1819
2135
|
throw new ClientMetadataError("rate_limited", "Too many requests for this AI app's details. Try again in a minute.");
|
|
1820
2136
|
}
|
|
1821
|
-
if (!
|
|
2137
|
+
if (!global.take("all", now)) {
|
|
1822
2138
|
throw new ClientMetadataError("rate_limited", "The relay is busy. Try again in a minute.");
|
|
1823
2139
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
this.#
|
|
1829
|
-
if (
|
|
1830
|
-
|
|
1831
|
-
if (oldest !== undefined)
|
|
1832
|
-
this.#cache.delete(oldest);
|
|
2140
|
+
perHost.take(url.host, now);
|
|
2141
|
+
perDomain.take(key, now);
|
|
2142
|
+
}
|
|
2143
|
+
#checkJwksUri(jwksUri) {
|
|
2144
|
+
const url = parseJwksUri(jwksUri, this.#testMode !== undefined);
|
|
2145
|
+
if (url === undefined) {
|
|
2146
|
+
throw new ClientMetadataError("invalid_jwks_uri", "The AI app's jwks_uri is not accepted.");
|
|
1833
2147
|
}
|
|
1834
|
-
this.#
|
|
1835
|
-
|
|
2148
|
+
if (this.#allowedHosts.length > 0 && !this.#allowedHosts.includes(url.host)) {
|
|
2149
|
+
throw new ClientMetadataError("host_not_allowed", `This relay does not accept AI apps from ${url.host}.`);
|
|
2150
|
+
}
|
|
2151
|
+
if (isIpLiteral(url.hostname) && !this.#addressAllowed(url.hostname)) {
|
|
2152
|
+
throw this.#notLoaded("private_address", url);
|
|
2153
|
+
}
|
|
2154
|
+
return url;
|
|
1836
2155
|
}
|
|
1837
2156
|
#checkUrl(clientId) {
|
|
1838
2157
|
const refuse = (message) => new ClientMetadataError("invalid_client_id", message);
|
|
@@ -1894,9 +2213,9 @@ class ClientMetadataFetcher {
|
|
|
1894
2213
|
}
|
|
1895
2214
|
return addresses.find((a) => parseIpv4(a) !== undefined) ?? addresses[0] ?? "";
|
|
1896
2215
|
}
|
|
1897
|
-
async#
|
|
2216
|
+
async#fetchJson(url, address, types) {
|
|
1898
2217
|
const fail = (detail) => this.#notLoaded("fetch_failed", url, detail);
|
|
1899
|
-
const pinned = new URL(
|
|
2218
|
+
const pinned = new URL(url.href);
|
|
1900
2219
|
pinned.hostname = address.includes(":") ? `[${address}]` : address;
|
|
1901
2220
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
1902
2221
|
const tls = url.protocol === "https:" ? {
|
|
@@ -1921,8 +2240,9 @@ class ClientMetadataFetcher {
|
|
|
1921
2240
|
if (response.status !== 200)
|
|
1922
2241
|
throw fail(`status_${response.status}`);
|
|
1923
2242
|
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1924
|
-
if (type
|
|
2243
|
+
if (type === undefined || !types.includes(type)) {
|
|
1925
2244
|
throw fail("not_json");
|
|
2245
|
+
}
|
|
1926
2246
|
const text = await this.#readLimited(response, url);
|
|
1927
2247
|
let doc;
|
|
1928
2248
|
try {
|
|
@@ -1930,8 +2250,7 @@ class ClientMetadataFetcher {
|
|
|
1930
2250
|
} catch {
|
|
1931
2251
|
throw fail("bad_json");
|
|
1932
2252
|
}
|
|
1933
|
-
|
|
1934
|
-
return { meta, ttlMs: cacheMs(response.headers.get("cache-control")) };
|
|
2253
|
+
return { doc, cacheControl: response.headers.get("cache-control") };
|
|
1935
2254
|
} finally {
|
|
1936
2255
|
if (!response.bodyUsed)
|
|
1937
2256
|
response.body?.cancel().catch(() => {});
|
|
@@ -1986,7 +2305,7 @@ function foreignDomain(name, hostname) {
|
|
|
1986
2305
|
}
|
|
1987
2306
|
return false;
|
|
1988
2307
|
}
|
|
1989
|
-
function validateDocument(doc, clientId, url) {
|
|
2308
|
+
function validateDocument(doc, clientId, url, options = {}) {
|
|
1990
2309
|
const invalid = (message) => new ClientMetadataError("invalid_metadata", message);
|
|
1991
2310
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
1992
2311
|
throw invalid("The AI app's details are not a JSON object.");
|
|
@@ -2012,10 +2331,7 @@ function validateDocument(doc, clientId, url) {
|
|
|
2012
2331
|
if (!Array.isArray(uris) || uris.length === 0 || uris.length > MAX_REDIRECT_URIS || !uris.every((u) => typeof u === "string" && isRedirectUri(u))) {
|
|
2013
2332
|
throw invalid("The AI app's details have no valid redirect_uris.");
|
|
2014
2333
|
}
|
|
2015
|
-
const
|
|
2016
|
-
if (method !== undefined && method !== "none") {
|
|
2017
|
-
throw invalid('The AI app must use token_endpoint_auth_method "none" with this relay.');
|
|
2018
|
-
}
|
|
2334
|
+
const auth = clientAuth(d, url, options, invalid);
|
|
2019
2335
|
const grants = d["grant_types"];
|
|
2020
2336
|
if (grants !== undefined && (!Array.isArray(grants) || !grants.includes("authorization_code"))) {
|
|
2021
2337
|
throw invalid("The AI app's details do not allow the authorization code grant.");
|
|
@@ -2028,9 +2344,81 @@ function validateDocument(doc, clientId, url) {
|
|
|
2028
2344
|
clientId,
|
|
2029
2345
|
name: displayName(name),
|
|
2030
2346
|
host: url.host,
|
|
2031
|
-
redirectUris: uris
|
|
2347
|
+
redirectUris: uris,
|
|
2348
|
+
auth
|
|
2032
2349
|
};
|
|
2033
2350
|
}
|
|
2351
|
+
function clientAuth(d, url, options, invalid) {
|
|
2352
|
+
const method = d["token_endpoint_auth_method"];
|
|
2353
|
+
const supported = d["token_endpoint_auth_methods_supported"];
|
|
2354
|
+
const listsNone = Array.isArray(supported) && supported.includes("none");
|
|
2355
|
+
if (method === undefined || method === "none")
|
|
2356
|
+
return { method: "none" };
|
|
2357
|
+
const checked = method === "private_key_jwt" ? privateKeyJwt(d, url, options.test === true) : {
|
|
2358
|
+
problem: "unsupported_method",
|
|
2359
|
+
message: 'The AI app must use token_endpoint_auth_method "none" or "private_key_jwt" with this relay, or list "none" in token_endpoint_auth_methods_supported.'
|
|
2360
|
+
};
|
|
2361
|
+
if ("jwksUri" in checked) {
|
|
2362
|
+
return { method: "private_key_jwt", ...checked, publicAllowed: listsNone };
|
|
2363
|
+
}
|
|
2364
|
+
if (listsNone) {
|
|
2365
|
+
options.onFallback?.(checked.problem);
|
|
2366
|
+
return { method: "none" };
|
|
2367
|
+
}
|
|
2368
|
+
throw invalid(checked.message);
|
|
2369
|
+
}
|
|
2370
|
+
function privateKeyJwt(d, url, test) {
|
|
2371
|
+
const jwksUri = d["jwks_uri"];
|
|
2372
|
+
if (jwksUri === undefined) {
|
|
2373
|
+
return {
|
|
2374
|
+
problem: "no_jwks_uri",
|
|
2375
|
+
message: "The AI app uses private_key_jwt but publishes no jwks_uri (an inline jwks is not accepted)."
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
if (d["jwks"] !== undefined) {
|
|
2379
|
+
return {
|
|
2380
|
+
problem: "jwks_and_jwks_uri",
|
|
2381
|
+
message: "The AI app's details have both jwks and jwks_uri."
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
const parsed = typeof jwksUri === "string" ? parseJwksUri(jwksUri, test) : undefined;
|
|
2385
|
+
if (parsed === undefined || parsed.protocol !== url.protocol || parsed.host !== url.host) {
|
|
2386
|
+
return {
|
|
2387
|
+
problem: "bad_jwks_uri",
|
|
2388
|
+
message: "The AI app's jwks_uri must be an https:// URL on the host of its client_id."
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
const alg = d["token_endpoint_auth_signing_alg"];
|
|
2392
|
+
if (alg !== undefined && !isAlgorithm(alg)) {
|
|
2393
|
+
return {
|
|
2394
|
+
problem: "signing_alg",
|
|
2395
|
+
message: "The AI app's token_endpoint_auth_signing_alg is not one this relay accepts."
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
return {
|
|
2399
|
+
jwksUri: parsed.href,
|
|
2400
|
+
algorithms: alg === undefined ? JWT_ALGORITHMS : [alg]
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
function parseJwksUri(value, test) {
|
|
2404
|
+
if (value.length > MAX_CLIENT_ID_LENGTH)
|
|
2405
|
+
return;
|
|
2406
|
+
let url;
|
|
2407
|
+
try {
|
|
2408
|
+
url = new URL(value);
|
|
2409
|
+
} catch {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const scheme = url.protocol === "https:" || test && url.protocol === "http:";
|
|
2413
|
+
if (!scheme || url.href !== value)
|
|
2414
|
+
return;
|
|
2415
|
+
if (url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (url.port !== "" && !test)
|
|
2419
|
+
return;
|
|
2420
|
+
return url;
|
|
2421
|
+
}
|
|
2034
2422
|
var PRIVATE_USE_SCHEME = /^[a-z][a-z0-9+-]*(\.[a-z0-9+-]+)+:$/;
|
|
2035
2423
|
function isRedirectUri(value) {
|
|
2036
2424
|
if (value.length > 2048 || value.includes("#"))
|
|
@@ -2074,6 +2462,151 @@ function redirectUriMatches(requested, registered) {
|
|
|
2074
2462
|
return a.href === b.href;
|
|
2075
2463
|
}
|
|
2076
2464
|
|
|
2465
|
+
// src/oauth/client-auth.ts
|
|
2466
|
+
var CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
2467
|
+
var CLIENT_ASSERTION_MAX_LIFETIME_MS = 60 * 60000;
|
|
2468
|
+
var CLIENT_ASSERTION_MAX_AGE_MS = 5 * 60000;
|
|
2469
|
+
var CLIENT_ASSERTION_LEEWAY_MS = 30000;
|
|
2470
|
+
var MAX_JTI2 = 256;
|
|
2471
|
+
var MAX_KEY_SETS = 1000;
|
|
2472
|
+
var MAX_KEY_SETS_PER_DOMAIN = 100;
|
|
2473
|
+
var SWEEP_INTERVAL_MS = 1000;
|
|
2474
|
+
var ACCEPTED_TYPES = new Set(["jwt", "client-authentication+jwt"]);
|
|
2475
|
+
function assertionSubject(assertion) {
|
|
2476
|
+
try {
|
|
2477
|
+
const sub = decodeJwt(assertion).payload["sub"];
|
|
2478
|
+
return typeof sub === "string" && sub !== "" ? sub : undefined;
|
|
2479
|
+
} catch {
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
function nonceKey(clientId, jti) {
|
|
2484
|
+
return hashToken(JSON.stringify(["client_assertion", clientId, jti]));
|
|
2485
|
+
}
|
|
2486
|
+
async function verifyClientAssertion(assertion, check) {
|
|
2487
|
+
const { header, payload } = await verifyJwt(assertion, check.keys, {
|
|
2488
|
+
algorithms: check.auth.algorithms
|
|
2489
|
+
});
|
|
2490
|
+
const typ = header["typ"];
|
|
2491
|
+
if (typ !== undefined && (typeof typ !== "string" || !ACCEPTED_TYPES.has(typ.toLowerCase()))) {
|
|
2492
|
+
throw new JwtError("typ", "the assertion's type is not a client assertion");
|
|
2493
|
+
}
|
|
2494
|
+
if (payload["iss"] !== check.clientId) {
|
|
2495
|
+
throw new JwtError("iss", "the assertion's issuer is not the client");
|
|
2496
|
+
}
|
|
2497
|
+
if (payload["sub"] !== check.clientId) {
|
|
2498
|
+
throw new JwtError("sub", "the assertion's subject is not the client");
|
|
2499
|
+
}
|
|
2500
|
+
if (!audiences(payload).some((a) => check.audiences.includes(a))) {
|
|
2501
|
+
throw new JwtError("aud", "the assertion is not for this relay");
|
|
2502
|
+
}
|
|
2503
|
+
const usableUntil = checkTimes(payload, check.now);
|
|
2504
|
+
const jti = payload["jti"];
|
|
2505
|
+
if (typeof jti !== "string" || jti === "" || jti.length > MAX_JTI2) {
|
|
2506
|
+
throw new JwtError("jti", "the assertion needs a jti");
|
|
2507
|
+
}
|
|
2508
|
+
const key = nonceKey(check.clientId, jti);
|
|
2509
|
+
if (!await check.claimNonce(key, usableUntil, check.now)) {
|
|
2510
|
+
throw new JwtError("replayed", "the assertion was already used");
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function checkTimes(payload, now) {
|
|
2514
|
+
const leeway = CLIENT_ASSERTION_LEEWAY_MS;
|
|
2515
|
+
const exp = numericDate(payload, "exp");
|
|
2516
|
+
if (exp === undefined)
|
|
2517
|
+
throw new JwtError("exp", "the assertion has no exp");
|
|
2518
|
+
if (exp <= now - leeway) {
|
|
2519
|
+
throw new JwtError("expired", "the assertion has expired");
|
|
2520
|
+
}
|
|
2521
|
+
if (exp > now + CLIENT_ASSERTION_MAX_LIFETIME_MS + leeway) {
|
|
2522
|
+
throw new JwtError("lifetime", "the assertion lives longer than an hour");
|
|
2523
|
+
}
|
|
2524
|
+
const iat = numericDate(payload, "iat");
|
|
2525
|
+
let end = exp;
|
|
2526
|
+
if (iat === undefined) {
|
|
2527
|
+
if (exp > now + CLIENT_ASSERTION_MAX_AGE_MS + leeway) {
|
|
2528
|
+
throw new JwtError("lifetime", "an assertion without iat may live five minutes");
|
|
2529
|
+
}
|
|
2530
|
+
} else {
|
|
2531
|
+
if (iat > now + leeway) {
|
|
2532
|
+
throw new JwtError("iat", "the assertion was issued in the future");
|
|
2533
|
+
}
|
|
2534
|
+
if (iat + CLIENT_ASSERTION_MAX_AGE_MS <= now - leeway) {
|
|
2535
|
+
throw new JwtError("too_old", "the assertion was issued too long ago");
|
|
2536
|
+
}
|
|
2537
|
+
end = Math.min(exp, iat + CLIENT_ASSERTION_MAX_AGE_MS);
|
|
2538
|
+
}
|
|
2539
|
+
const nbf = numericDate(payload, "nbf");
|
|
2540
|
+
if (nbf !== undefined && nbf > now + leeway) {
|
|
2541
|
+
throw new JwtError("nbf", "the assertion is not valid yet");
|
|
2542
|
+
}
|
|
2543
|
+
return end + leeway;
|
|
2544
|
+
}
|
|
2545
|
+
function quotaKey(clientId) {
|
|
2546
|
+
return ipPrefixKey(new URL(clientId).hostname.replace(/\.$/, ""));
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
class NonceQuota {
|
|
2550
|
+
#until = new Map;
|
|
2551
|
+
#perDomain;
|
|
2552
|
+
#max;
|
|
2553
|
+
#total = 0;
|
|
2554
|
+
#sweptAt = Number.NEGATIVE_INFINITY;
|
|
2555
|
+
constructor(perDomain, max) {
|
|
2556
|
+
this.#perDomain = perDomain;
|
|
2557
|
+
this.#max = max;
|
|
2558
|
+
}
|
|
2559
|
+
refusal(domain, now) {
|
|
2560
|
+
this.sweep(now);
|
|
2561
|
+
if ((this.#until.get(domain)?.length ?? 0) >= this.#perDomain) {
|
|
2562
|
+
return "domain_full";
|
|
2563
|
+
}
|
|
2564
|
+
return this.#total >= this.#max ? "full" : undefined;
|
|
2565
|
+
}
|
|
2566
|
+
add(domain, until) {
|
|
2567
|
+
const list = this.#until.get(domain);
|
|
2568
|
+
if (list === undefined)
|
|
2569
|
+
this.#until.set(domain, [until]);
|
|
2570
|
+
else
|
|
2571
|
+
list.push(until);
|
|
2572
|
+
this.#total++;
|
|
2573
|
+
}
|
|
2574
|
+
sweep(now) {
|
|
2575
|
+
if (now - this.#sweptAt < SWEEP_INTERVAL_MS)
|
|
2576
|
+
return false;
|
|
2577
|
+
this.#sweptAt = now;
|
|
2578
|
+
this.#total = 0;
|
|
2579
|
+
for (const [domain, list] of this.#until) {
|
|
2580
|
+
const live = list.filter((until) => until > now);
|
|
2581
|
+
if (live.length === 0)
|
|
2582
|
+
this.#until.delete(domain);
|
|
2583
|
+
else
|
|
2584
|
+
this.#until.set(domain, live);
|
|
2585
|
+
this.#total += live.length;
|
|
2586
|
+
}
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
class ClientKeySets {
|
|
2592
|
+
#sets = new DomainCache(MAX_KEY_SETS, MAX_KEY_SETS_PER_DOMAIN);
|
|
2593
|
+
#load;
|
|
2594
|
+
#now;
|
|
2595
|
+
constructor(load, now = Date.now) {
|
|
2596
|
+
this.#load = load;
|
|
2597
|
+
this.#now = now;
|
|
2598
|
+
}
|
|
2599
|
+
for(jwksUri) {
|
|
2600
|
+
const known = this.#sets.get(jwksUri);
|
|
2601
|
+
if (known !== undefined)
|
|
2602
|
+
return known;
|
|
2603
|
+
const set = new JwksCache(jwksUri, { load: this.#load, now: this.#now });
|
|
2604
|
+
const domain = limitKey(new URL(jwksUri).hostname);
|
|
2605
|
+
this.#sets.set(jwksUri, domain, set, this.#now());
|
|
2606
|
+
return set;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2077
2610
|
// src/oauth/html.ts
|
|
2078
2611
|
var ESCAPES = {
|
|
2079
2612
|
"&": "&",
|
|
@@ -2124,6 +2657,9 @@ function renderPage(title, body) {
|
|
|
2124
2657
|
<meta name="robots" content="noindex">
|
|
2125
2658
|
<title>${escapeHtml(title)}</title>
|
|
2126
2659
|
<link rel="stylesheet" href="/authorize.css">
|
|
2660
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
|
2661
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
2662
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
2127
2663
|
</head>
|
|
2128
2664
|
<body>
|
|
2129
2665
|
<main>
|
|
@@ -2640,6 +3176,12 @@ var REFRESH_REUSE_WINDOW_MS = 24 * 60 * 60000;
|
|
|
2640
3176
|
var MAX_CODES = 1e4;
|
|
2641
3177
|
var MAX_FORM_BYTES = 8192;
|
|
2642
3178
|
var MAX_STATE_LENGTH = 1024;
|
|
3179
|
+
var MAX_NONCES_PER_HOST = 1e4;
|
|
3180
|
+
var MAX_NONCES = 1e5;
|
|
3181
|
+
var CLIENT_AUTH_FAILED = "client authentication failed";
|
|
3182
|
+
function assertionRequired(auth) {
|
|
3183
|
+
return auth.method === "private_key_jwt" && !auth.publicAllowed;
|
|
3184
|
+
}
|
|
2643
3185
|
|
|
2644
3186
|
class OAuthError extends Error {
|
|
2645
3187
|
status;
|
|
@@ -2689,10 +3231,16 @@ function hostOf(url) {
|
|
|
2689
3231
|
return url;
|
|
2690
3232
|
}
|
|
2691
3233
|
}
|
|
3234
|
+
function logHost(clientId) {
|
|
3235
|
+
return clientId !== undefined && URL.canParse(clientId) ? hostOf(clientId).slice(0, 100) : "(not a URL)";
|
|
3236
|
+
}
|
|
2692
3237
|
|
|
2693
3238
|
class AuthorizationServer {
|
|
2694
3239
|
#host;
|
|
2695
3240
|
#cimd;
|
|
3241
|
+
#clientKeys;
|
|
3242
|
+
#nonces = new NonceSet;
|
|
3243
|
+
#nonceQuota = new NonceQuota(MAX_NONCES_PER_HOST, MAX_NONCES);
|
|
2696
3244
|
#pending = new Map;
|
|
2697
3245
|
#pendingPerIp = new Map;
|
|
2698
3246
|
#pendingPerAddress = new Map;
|
|
@@ -2743,6 +3291,8 @@ class AuthorizationServer {
|
|
|
2743
3291
|
logger: host.logger,
|
|
2744
3292
|
...testMode?.cimd === undefined ? {} : { testMode: testMode.cimd }
|
|
2745
3293
|
});
|
|
3294
|
+
const cimd = this.#cimd;
|
|
3295
|
+
this.#clientKeys = new ClientKeySets((url) => cimd.fetchJwks(url));
|
|
2746
3296
|
}
|
|
2747
3297
|
get #issuer() {
|
|
2748
3298
|
return this.#host.publicUrl();
|
|
@@ -2796,8 +3346,10 @@ class AuthorizationServer {
|
|
|
2796
3346
|
response_modes_supported: ["query"],
|
|
2797
3347
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2798
3348
|
code_challenge_methods_supported: ["S256"],
|
|
2799
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
2800
|
-
|
|
3349
|
+
token_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3350
|
+
token_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
3351
|
+
revocation_endpoint_auth_methods_supported: ["none", "private_key_jwt"],
|
|
3352
|
+
revocation_endpoint_auth_signing_alg_values_supported: JWT_ALGORITHMS,
|
|
2801
3353
|
scopes_supported: [SCOPE],
|
|
2802
3354
|
client_id_metadata_document_supported: true,
|
|
2803
3355
|
authorization_response_iss_parameter_supported: true,
|
|
@@ -3296,20 +3848,21 @@ ${cancel}
|
|
|
3296
3848
|
"code_verifier",
|
|
3297
3849
|
"refresh_token",
|
|
3298
3850
|
"resource",
|
|
3299
|
-
"scope"
|
|
3851
|
+
"scope",
|
|
3852
|
+
"client_assertion",
|
|
3853
|
+
"client_assertion_type"
|
|
3300
3854
|
]);
|
|
3301
3855
|
if ("duplicate" in p) {
|
|
3302
3856
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3303
3857
|
}
|
|
3304
|
-
const
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
}
|
|
3858
|
+
const client = await this.#authenticateClient(p, [
|
|
3859
|
+
`${this.#issuer}/token`
|
|
3860
|
+
]);
|
|
3308
3861
|
let body;
|
|
3309
3862
|
if (p["grant_type"] === "authorization_code") {
|
|
3310
|
-
body = await this.#codeGrant(p,
|
|
3863
|
+
body = await this.#codeGrant(p, client);
|
|
3311
3864
|
} else if (p["grant_type"] === "refresh_token") {
|
|
3312
|
-
body = await this.#refreshGrant(p,
|
|
3865
|
+
body = await this.#refreshGrant(p, client);
|
|
3313
3866
|
} else if (p["grant_type"] === undefined) {
|
|
3314
3867
|
throw new OAuthError(400, "invalid_request", "grant_type is required");
|
|
3315
3868
|
} else {
|
|
@@ -3343,19 +3896,96 @@ ${cancel}
|
|
|
3343
3896
|
throw new OAuthError(400, "invalid_request", "the body must be application/x-www-form-urlencoded");
|
|
3344
3897
|
}
|
|
3345
3898
|
if (req.headers.get("authorization") !== null) {
|
|
3346
|
-
throw new OAuthError(401, "invalid_client", "
|
|
3899
|
+
throw new OAuthError(401, "invalid_client", "HTTP authentication is not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3347
3900
|
}
|
|
3348
3901
|
const text = await this.#readForm(req);
|
|
3349
3902
|
if (text === undefined) {
|
|
3350
3903
|
throw new OAuthError(413, "invalid_request", "the request is too large");
|
|
3351
3904
|
}
|
|
3352
3905
|
const params = new URLSearchParams(text);
|
|
3353
|
-
if (params.has("client_secret")
|
|
3354
|
-
throw new OAuthError(400, "invalid_client", "client
|
|
3906
|
+
if (params.has("client_secret")) {
|
|
3907
|
+
throw new OAuthError(400, "invalid_client", "client secrets are not supported (token_endpoint_auth_method none or private_key_jwt)");
|
|
3355
3908
|
}
|
|
3356
3909
|
return params;
|
|
3357
3910
|
}
|
|
3358
|
-
async#
|
|
3911
|
+
async#authenticateClient(p, endpoints) {
|
|
3912
|
+
const assertion = p["client_assertion"];
|
|
3913
|
+
const type = p["client_assertion_type"];
|
|
3914
|
+
if (assertion === undefined && type === undefined) {
|
|
3915
|
+
const clientId = p["client_id"];
|
|
3916
|
+
if (clientId === undefined) {
|
|
3917
|
+
throw new OAuthError(400, "invalid_request", "client_id is required");
|
|
3918
|
+
}
|
|
3919
|
+
return { clientId, assertion: false };
|
|
3920
|
+
}
|
|
3921
|
+
if (assertion === undefined || type === undefined) {
|
|
3922
|
+
throw new OAuthError(400, "invalid_request", "client_assertion and client_assertion_type go together");
|
|
3923
|
+
}
|
|
3924
|
+
const clientId = p["client_id"] ?? assertionSubject(assertion);
|
|
3925
|
+
if (type !== CLIENT_ASSERTION_TYPE) {
|
|
3926
|
+
throw this.#clientAuthFailed(clientId, "assertion_type");
|
|
3927
|
+
}
|
|
3928
|
+
if (clientId === undefined) {
|
|
3929
|
+
throw this.#clientAuthFailed(clientId, "no_client_id");
|
|
3930
|
+
}
|
|
3931
|
+
let client;
|
|
3932
|
+
try {
|
|
3933
|
+
client = await this.#cimd.get(clientId, { allowStale: true });
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
if (!(error instanceof ClientMetadataError))
|
|
3936
|
+
throw error;
|
|
3937
|
+
const reason = `metadata_${error.reason}`;
|
|
3938
|
+
throw isTransient(error) || error.reason === "private_address" ? this.#clientAuthUnavailable(clientId, reason) : this.#clientAuthFailed(clientId, reason);
|
|
3939
|
+
}
|
|
3940
|
+
if (client.auth.method !== "private_key_jwt") {
|
|
3941
|
+
throw this.#clientAuthFailed(clientId, "not_private_key_jwt");
|
|
3942
|
+
}
|
|
3943
|
+
const domain = quotaKey(clientId);
|
|
3944
|
+
try {
|
|
3945
|
+
await verifyClientAssertion(assertion, {
|
|
3946
|
+
clientId,
|
|
3947
|
+
auth: client.auth,
|
|
3948
|
+
keys: this.#clientKeys.for(client.auth.jwksUri),
|
|
3949
|
+
audiences: [this.#issuer, ...endpoints],
|
|
3950
|
+
claimNonce: (key, until, now) => this.#claimNonce(clientId, domain, key, until, now),
|
|
3951
|
+
now: Date.now()
|
|
3952
|
+
});
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
if (!(error instanceof JwtError))
|
|
3955
|
+
throw error;
|
|
3956
|
+
throw error.reason === "jwks_unavailable" ? this.#clientAuthUnavailable(clientId, error.reason) : this.#clientAuthFailed(clientId, error.reason);
|
|
3957
|
+
}
|
|
3958
|
+
return { clientId, assertion: true };
|
|
3959
|
+
}
|
|
3960
|
+
#clientAuthFailed(clientId, reason) {
|
|
3961
|
+
this.#host.logger.info("oauth.client_auth_failed", {
|
|
3962
|
+
client: logHost(clientId),
|
|
3963
|
+
reason
|
|
3964
|
+
});
|
|
3965
|
+
return new OAuthError(401, "invalid_client", CLIENT_AUTH_FAILED);
|
|
3966
|
+
}
|
|
3967
|
+
#clientAuthUnavailable(clientId, reason) {
|
|
3968
|
+
this.#host.logger.warn("oauth.client_auth_unavailable", {
|
|
3969
|
+
client: logHost(clientId),
|
|
3970
|
+
reason
|
|
3971
|
+
});
|
|
3972
|
+
return new OAuthError(503, "temporarily_unavailable", "the client cannot be authenticated right now; try again shortly");
|
|
3973
|
+
}
|
|
3974
|
+
async#claimNonce(clientId, domain, key, until, now) {
|
|
3975
|
+
if (this.#nonceQuota.sweep(now))
|
|
3976
|
+
this.#nonces.sweep(now);
|
|
3977
|
+
const refusal = this.#nonceQuota.refusal(domain, now);
|
|
3978
|
+
if (refusal !== undefined) {
|
|
3979
|
+
throw this.#clientAuthUnavailable(clientId, `nonces_${refusal}`);
|
|
3980
|
+
}
|
|
3981
|
+
const store = this.#host.store;
|
|
3982
|
+
const claimed = store.claimNonce === undefined ? this.#nonces.claim(key, until, now) : await store.claimNonce(key, until, now);
|
|
3983
|
+
if (claimed)
|
|
3984
|
+
this.#nonceQuota.add(domain, until);
|
|
3985
|
+
return claimed;
|
|
3986
|
+
}
|
|
3987
|
+
async#codeGrant(p, client) {
|
|
3988
|
+
const clientId = client.clientId;
|
|
3359
3989
|
const code = p["code"];
|
|
3360
3990
|
const redirectUri = p["redirect_uri"];
|
|
3361
3991
|
const verifier = p["code_verifier"];
|
|
@@ -3367,6 +3997,12 @@ ${cancel}
|
|
|
3367
3997
|
const invalid = (why) => new OAuthError(400, "invalid_grant", why);
|
|
3368
3998
|
if (issued === undefined)
|
|
3369
3999
|
throw invalid("the code is not valid");
|
|
4000
|
+
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
4001
|
+
throw invalid("the code was issued to another client");
|
|
4002
|
+
}
|
|
4003
|
+
if (assertionRequired(issued.client.auth) && !client.assertion) {
|
|
4004
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4005
|
+
}
|
|
3370
4006
|
if (issued.used) {
|
|
3371
4007
|
if (issued.grantId !== undefined) {
|
|
3372
4008
|
await this.#revokeGrant(issued.addressId, issued.grantId, "reuse_detected");
|
|
@@ -3376,9 +4012,6 @@ ${cancel}
|
|
|
3376
4012
|
issued.used = true;
|
|
3377
4013
|
if (issued.expiresAt <= Date.now())
|
|
3378
4014
|
throw invalid("the code has expired");
|
|
3379
|
-
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
3380
|
-
throw invalid("the code was issued to another client");
|
|
3381
|
-
}
|
|
3382
4015
|
if (redirectUri !== issued.redirectUri) {
|
|
3383
4016
|
throw invalid("redirect_uri does not match the authorization request");
|
|
3384
4017
|
}
|
|
@@ -3402,7 +4035,8 @@ ${cancel}
|
|
|
3402
4035
|
clientName: issued.client.name,
|
|
3403
4036
|
scopes: issued.scopes,
|
|
3404
4037
|
grantCreatedAt: now,
|
|
3405
|
-
...issued.subject === undefined ? {} : { subject: issued.subject }
|
|
4038
|
+
...issued.subject === undefined ? {} : { subject: issued.subject },
|
|
4039
|
+
...client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3406
4040
|
}, {
|
|
3407
4041
|
accessTtlMs: config.accessTokenTtlMs,
|
|
3408
4042
|
refreshTtlMs: config.refreshTokenTtlMs,
|
|
@@ -3436,7 +4070,8 @@ ${cancel}
|
|
|
3436
4070
|
scope: scopes.join(" ")
|
|
3437
4071
|
};
|
|
3438
4072
|
}
|
|
3439
|
-
async#refreshGrant(p,
|
|
4073
|
+
async#refreshGrant(p, client) {
|
|
4074
|
+
const clientId = client.clientId;
|
|
3440
4075
|
const token = p["refresh_token"];
|
|
3441
4076
|
if (token === undefined) {
|
|
3442
4077
|
throw new OAuthError(400, "invalid_request", "refresh_token is required");
|
|
@@ -3450,6 +4085,9 @@ ${cancel}
|
|
|
3450
4085
|
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
3451
4086
|
throw invalid("the refresh token was issued to another client");
|
|
3452
4087
|
}
|
|
4088
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4089
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4090
|
+
}
|
|
3453
4091
|
const now = Date.now();
|
|
3454
4092
|
if (record.expiresAt <= now)
|
|
3455
4093
|
throw invalid("the refresh token has expired");
|
|
@@ -3481,7 +4119,8 @@ ${cancel}
|
|
|
3481
4119
|
clientName: record.clientName,
|
|
3482
4120
|
scopes: record.scopes,
|
|
3483
4121
|
grantCreatedAt: record.grantCreatedAt,
|
|
3484
|
-
...record.subject === undefined ? {} : { subject: record.subject }
|
|
4122
|
+
...record.subject === undefined ? {} : { subject: record.subject },
|
|
4123
|
+
...record.clientAuth === "private_key_jwt" || client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3485
4124
|
}, {
|
|
3486
4125
|
accessTtlMs: this.#host.config.accessTokenTtlMs,
|
|
3487
4126
|
refreshTtlMs: this.#host.config.refreshTokenTtlMs,
|
|
@@ -3517,24 +4156,36 @@ ${cancel}
|
|
|
3517
4156
|
throw new OAuthError(429, "temporarily_unavailable", "too many requests; try again shortly");
|
|
3518
4157
|
}
|
|
3519
4158
|
const params = await this.#formParams(req);
|
|
3520
|
-
const p = single(params, [
|
|
4159
|
+
const p = single(params, [
|
|
4160
|
+
"token",
|
|
4161
|
+
"client_id",
|
|
4162
|
+
"token_type_hint",
|
|
4163
|
+
"client_assertion",
|
|
4164
|
+
"client_assertion_type"
|
|
4165
|
+
]);
|
|
3521
4166
|
if ("duplicate" in p) {
|
|
3522
4167
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3523
4168
|
}
|
|
3524
4169
|
const token = p["token"];
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
throw new OAuthError(400, "invalid_request", "token and client_id are required");
|
|
4170
|
+
if (token === undefined) {
|
|
4171
|
+
throw new OAuthError(400, "invalid_request", "token is required");
|
|
3528
4172
|
}
|
|
4173
|
+
const client = await this.#authenticateClient(p, [
|
|
4174
|
+
`${this.#issuer}/revoke`,
|
|
4175
|
+
`${this.#issuer}/token`
|
|
4176
|
+
]);
|
|
3529
4177
|
const record = await this.#host.store.get(hashToken(token));
|
|
3530
4178
|
if (record === undefined)
|
|
3531
4179
|
return new Response(null, {
|
|
3532
4180
|
status: 200,
|
|
3533
4181
|
headers: { ...this.#cors(), ...noStore }
|
|
3534
4182
|
});
|
|
3535
|
-
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
4183
|
+
if (!constantTimeEqual(client.clientId, record.clientId)) {
|
|
3536
4184
|
throw new OAuthError(400, "invalid_client", "the token was issued to another client");
|
|
3537
4185
|
}
|
|
4186
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4187
|
+
throw this.#clientAuthFailed(client.clientId, "assertion_required");
|
|
4188
|
+
}
|
|
3538
4189
|
await this.#revokeGrant(record.addressId, record.grantId, "revoked");
|
|
3539
4190
|
this.#host.logger.info("oauth.revoked", {
|
|
3540
4191
|
address: shortAddress(record.addressId),
|
|
@@ -3580,6 +4231,7 @@ ${cancel}
|
|
|
3580
4231
|
this.#guessesPerAddress.sweep(now);
|
|
3581
4232
|
this.#tokenPerIp.sweep(now);
|
|
3582
4233
|
this.#refreshesPerGrant.sweep(now);
|
|
4234
|
+
this.#nonces.sweep(now);
|
|
3583
4235
|
}
|
|
3584
4236
|
}
|
|
3585
4237
|
|
|
@@ -3988,112 +4640,10 @@ class TabSession {
|
|
|
3988
4640
|
}
|
|
3989
4641
|
}
|
|
3990
4642
|
|
|
3991
|
-
// src/store/memory.ts
|
|
3992
|
-
function copy(record) {
|
|
3993
|
-
return { ...record, scopes: [...record.scopes] };
|
|
3994
|
-
}
|
|
3995
|
-
function matches(record, selector) {
|
|
3996
|
-
return "grantId" in selector ? record.grantId === selector.grantId : record.addressId === selector.addressId;
|
|
3997
|
-
}
|
|
3998
|
-
function isLive(record, now) {
|
|
3999
|
-
return record.expiresAt > now && (record.kind === "access" || record.usedAt === undefined);
|
|
4000
|
-
}
|
|
4001
|
-
function summarizeGrants(records, now) {
|
|
4002
|
-
const grants = new Map;
|
|
4003
|
-
for (const r of records) {
|
|
4004
|
-
if (!isLive(r, now) || grants.has(r.grantId))
|
|
4005
|
-
continue;
|
|
4006
|
-
const summary = {
|
|
4007
|
-
grantId: r.grantId,
|
|
4008
|
-
clientId: r.clientId,
|
|
4009
|
-
clientName: r.clientName,
|
|
4010
|
-
scopes: [...r.scopes],
|
|
4011
|
-
createdAt: r.grantCreatedAt
|
|
4012
|
-
};
|
|
4013
|
-
if (r.subject !== undefined)
|
|
4014
|
-
summary.subject = r.subject;
|
|
4015
|
-
grants.set(r.grantId, summary);
|
|
4016
|
-
}
|
|
4017
|
-
return [...grants.values()].sort((a, b) => a.createdAt - b.createdAt || a.grantId.localeCompare(b.grantId));
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
|
-
class MemoryTokenStore {
|
|
4021
|
-
#records = new Map;
|
|
4022
|
-
#revokedGrants = new Map;
|
|
4023
|
-
#revokedAddresses = new Map;
|
|
4024
|
-
async put(record) {
|
|
4025
|
-
if (this.#revokedGrants.has(record.grantId))
|
|
4026
|
-
return false;
|
|
4027
|
-
const address = this.#revokedAddresses.get(record.addressId);
|
|
4028
|
-
if (address !== undefined && record.grantCreatedAt <= address.at) {
|
|
4029
|
-
return false;
|
|
4030
|
-
}
|
|
4031
|
-
this.#records.set(record.hash, copy(record));
|
|
4032
|
-
return true;
|
|
4033
|
-
}
|
|
4034
|
-
async get(hash) {
|
|
4035
|
-
const r = this.#records.get(hash);
|
|
4036
|
-
return r === undefined ? undefined : copy(r);
|
|
4037
|
-
}
|
|
4038
|
-
async markUsed(hash, now, keepUntil) {
|
|
4039
|
-
const r = this.#records.get(hash);
|
|
4040
|
-
if (r === undefined || r.usedAt !== undefined)
|
|
4041
|
-
return false;
|
|
4042
|
-
r.usedAt = now;
|
|
4043
|
-
r.expiresAt = Math.min(r.expiresAt, keepUntil);
|
|
4044
|
-
return true;
|
|
4045
|
-
}
|
|
4046
|
-
async revoke(selector, options) {
|
|
4047
|
-
if ("grantId" in selector) {
|
|
4048
|
-
this.#revokedGrants.set(selector.grantId, options.keepUntil);
|
|
4049
|
-
} else {
|
|
4050
|
-
const old = this.#revokedAddresses.get(selector.addressId);
|
|
4051
|
-
this.#revokedAddresses.set(selector.addressId, {
|
|
4052
|
-
at: Math.max(old?.at ?? 0, options.now),
|
|
4053
|
-
until: Math.max(old?.until ?? 0, options.keepUntil)
|
|
4054
|
-
});
|
|
4055
|
-
}
|
|
4056
|
-
let n = 0;
|
|
4057
|
-
for (const [hash, r] of this.#records) {
|
|
4058
|
-
if (matches(r, selector)) {
|
|
4059
|
-
this.#records.delete(hash);
|
|
4060
|
-
n++;
|
|
4061
|
-
}
|
|
4062
|
-
}
|
|
4063
|
-
return n;
|
|
4064
|
-
}
|
|
4065
|
-
async listGrants(addressId, now) {
|
|
4066
|
-
const own = [...this.#records.values()].filter((r) => r.addressId === addressId);
|
|
4067
|
-
return summarizeGrants(own, now);
|
|
4068
|
-
}
|
|
4069
|
-
async deleteExpired(now) {
|
|
4070
|
-
for (const [id, until] of this.#revokedGrants) {
|
|
4071
|
-
if (until <= now)
|
|
4072
|
-
this.#revokedGrants.delete(id);
|
|
4073
|
-
}
|
|
4074
|
-
for (const [id, t] of this.#revokedAddresses) {
|
|
4075
|
-
if (t.until <= now)
|
|
4076
|
-
this.#revokedAddresses.delete(id);
|
|
4077
|
-
}
|
|
4078
|
-
let n = 0;
|
|
4079
|
-
for (const [hash, r] of this.#records) {
|
|
4080
|
-
if (r.expiresAt <= now) {
|
|
4081
|
-
this.#records.delete(hash);
|
|
4082
|
-
n++;
|
|
4083
|
-
}
|
|
4084
|
-
}
|
|
4085
|
-
return n;
|
|
4086
|
-
}
|
|
4087
|
-
async close() {
|
|
4088
|
-
this.#records.clear();
|
|
4089
|
-
this.#revokedGrants.clear();
|
|
4090
|
-
this.#revokedAddresses.clear();
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
|
|
4094
4643
|
// src/store/sqlite.ts
|
|
4095
4644
|
import { Database } from "bun:sqlite";
|
|
4096
|
-
var SCHEMA_VERSION =
|
|
4645
|
+
var SCHEMA_VERSION = 5;
|
|
4646
|
+
var MIN_READER_VERSION = 5;
|
|
4097
4647
|
var REUSE_WINDOW_MS = 86400000;
|
|
4098
4648
|
function toRecord(row) {
|
|
4099
4649
|
const record = {
|
|
@@ -4112,6 +4662,8 @@ function toRecord(row) {
|
|
|
4112
4662
|
record.usedAt = row.used_at;
|
|
4113
4663
|
if (row.subject !== null)
|
|
4114
4664
|
record.subject = row.subject;
|
|
4665
|
+
if (row.client_auth === "private_key_jwt")
|
|
4666
|
+
record.clientAuth = "private_key_jwt";
|
|
4115
4667
|
return record;
|
|
4116
4668
|
}
|
|
4117
4669
|
|
|
@@ -4122,7 +4674,24 @@ class SqliteTokenStore {
|
|
|
4122
4674
|
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
4123
4675
|
if (path !== ":memory:")
|
|
4124
4676
|
this.#db.run("PRAGMA journal_mode = WAL");
|
|
4677
|
+
this.#checkReader();
|
|
4125
4678
|
this.#migrate();
|
|
4679
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS meta (
|
|
4680
|
+
key TEXT PRIMARY KEY,
|
|
4681
|
+
value INTEGER NOT NULL
|
|
4682
|
+
) STRICT`);
|
|
4683
|
+
this.#db.query(`INSERT INTO meta (key, value) VALUES ('min_reader_version', $v)
|
|
4684
|
+
ON CONFLICT (key) DO UPDATE SET value = max(value, excluded.value)`).run({ v: MIN_READER_VERSION });
|
|
4685
|
+
}
|
|
4686
|
+
#checkReader() {
|
|
4687
|
+
const table = this.#db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
|
4688
|
+
if (table === null)
|
|
4689
|
+
return;
|
|
4690
|
+
const row = this.#db.query("SELECT value FROM meta WHERE key = 'min_reader_version'").get();
|
|
4691
|
+
if (row !== null && row.value > SCHEMA_VERSION) {
|
|
4692
|
+
this.#db.close();
|
|
4693
|
+
throw new Error(`the token store file needs a newer relay (schema ${row.value}; this relay knows ${SCHEMA_VERSION})`);
|
|
4694
|
+
}
|
|
4126
4695
|
}
|
|
4127
4696
|
#migrate() {
|
|
4128
4697
|
const row = this.#db.query("PRAGMA user_version").get();
|
|
@@ -4148,12 +4717,20 @@ class SqliteTokenStore {
|
|
|
4148
4717
|
issued_at INTEGER NOT NULL,
|
|
4149
4718
|
expires_at INTEGER NOT NULL,
|
|
4150
4719
|
used_at INTEGER,
|
|
4151
|
-
subject TEXT
|
|
4720
|
+
subject TEXT,
|
|
4721
|
+
client_auth TEXT
|
|
4152
4722
|
) STRICT`);
|
|
4153
4723
|
const columns = this.#db.query("PRAGMA table_info(tokens)").all();
|
|
4154
4724
|
if (!columns.some((c) => c.name === "subject")) {
|
|
4155
4725
|
this.#db.run("ALTER TABLE tokens ADD COLUMN subject TEXT");
|
|
4156
4726
|
}
|
|
4727
|
+
if (!columns.some((c) => c.name === "client_auth")) {
|
|
4728
|
+
this.#db.run("ALTER TABLE tokens ADD COLUMN client_auth TEXT");
|
|
4729
|
+
}
|
|
4730
|
+
this.#db.run(`CREATE TABLE IF NOT EXISTS nonces (
|
|
4731
|
+
key TEXT PRIMARY KEY,
|
|
4732
|
+
expires_at INTEGER NOT NULL
|
|
4733
|
+
) STRICT`);
|
|
4157
4734
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_grant ON tokens (grant_id)");
|
|
4158
4735
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_address ON tokens (address_id)");
|
|
4159
4736
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_expires ON tokens (expires_at)");
|
|
@@ -4178,9 +4755,11 @@ class SqliteTokenStore {
|
|
|
4178
4755
|
}
|
|
4179
4756
|
#insert(r) {
|
|
4180
4757
|
this.#db.query(`INSERT OR REPLACE INTO tokens (hash, kind, grant_id, address_id, client_id,
|
|
4181
|
-
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject
|
|
4758
|
+
client_name, scopes, grant_created_at, issued_at, expires_at, used_at, subject,
|
|
4759
|
+
client_auth)
|
|
4182
4760
|
VALUES ($hash, $kind, $grant_id, $address_id, $client_id, $client_name,
|
|
4183
|
-
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject
|
|
4761
|
+
$scopes, $grant_created_at, $issued_at, $expires_at, $used_at, $subject,
|
|
4762
|
+
$client_auth)`).run({
|
|
4184
4763
|
hash: r.hash,
|
|
4185
4764
|
kind: r.kind,
|
|
4186
4765
|
grant_id: r.grantId,
|
|
@@ -4192,7 +4771,8 @@ class SqliteTokenStore {
|
|
|
4192
4771
|
issued_at: r.issuedAt,
|
|
4193
4772
|
expires_at: r.expiresAt,
|
|
4194
4773
|
used_at: r.usedAt ?? null,
|
|
4195
|
-
subject: r.subject ?? null
|
|
4774
|
+
subject: r.subject ?? null,
|
|
4775
|
+
client_auth: r.clientAuth ?? null
|
|
4196
4776
|
});
|
|
4197
4777
|
}
|
|
4198
4778
|
async get(hash) {
|
|
@@ -4228,8 +4808,15 @@ class SqliteTokenStore {
|
|
|
4228
4808
|
}
|
|
4229
4809
|
async deleteExpired(now) {
|
|
4230
4810
|
this.#db.query("DELETE FROM revocations WHERE keep_until <= $now").run({ now });
|
|
4811
|
+
this.#db.query("DELETE FROM nonces WHERE expires_at <= $now").run({ now });
|
|
4231
4812
|
return this.#db.query("DELETE FROM tokens WHERE expires_at <= $now").run({ now }).changes;
|
|
4232
4813
|
}
|
|
4814
|
+
async claimNonce(key, expiresAt, now) {
|
|
4815
|
+
const result = this.#db.query(`INSERT INTO nonces (key, expires_at) VALUES ($key, $expires_at)
|
|
4816
|
+
ON CONFLICT (key) DO UPDATE SET expires_at = excluded.expires_at
|
|
4817
|
+
WHERE nonces.expires_at <= $now`).run({ key, expires_at: expiresAt, now });
|
|
4818
|
+
return result.changes === 1;
|
|
4819
|
+
}
|
|
4233
4820
|
async close() {
|
|
4234
4821
|
this.#db.close();
|
|
4235
4822
|
}
|
|
@@ -4317,12 +4904,24 @@ function metaVersion(body) {
|
|
|
4317
4904
|
}
|
|
4318
4905
|
var INDEX_HTML = `<!doctype html>
|
|
4319
4906
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4320
|
-
<title>tab-mcp relay</title
|
|
4907
|
+
<title>tab-mcp relay</title>
|
|
4908
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32"><link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
4909
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png"></head>
|
|
4321
4910
|
<body><h1>tab-mcp relay</h1>
|
|
4322
4911
|
<p>This server relays AI apps' MCP requests to tools offered by web pages open in a browser tab.
|
|
4323
4912
|
It does not log request contents. Source and documentation:
|
|
4324
4913
|
<a href="https://github.com/ractive/tab-mcp">github.com/ractive/tab-mcp</a>.</p>
|
|
4325
4914
|
</body></html>`;
|
|
4915
|
+
function icon(body, type) {
|
|
4916
|
+
return new Response(body, {
|
|
4917
|
+
headers: {
|
|
4918
|
+
"content-type": type,
|
|
4919
|
+
"content-security-policy": "default-src 'none'",
|
|
4920
|
+
"x-content-type-options": "nosniff",
|
|
4921
|
+
"cache-control": "public, max-age=86400"
|
|
4922
|
+
}
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4326
4925
|
function createRelay(options = {}) {
|
|
4327
4926
|
return startRelay(options, undefined);
|
|
4328
4927
|
}
|
|
@@ -5034,12 +5633,17 @@ function startRelay(options, hooks) {
|
|
|
5034
5633
|
GET: () => new Response(INDEX_HTML, {
|
|
5035
5634
|
headers: {
|
|
5036
5635
|
"content-type": "text/html; charset=utf-8",
|
|
5037
|
-
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
|
|
5636
|
+
"content-security-policy": "default-src 'none'; img-src 'self'; frame-ancestors 'none'",
|
|
5038
5637
|
"x-content-type-options": "nosniff",
|
|
5039
5638
|
"cache-control": "public, max-age=3600"
|
|
5040
5639
|
}
|
|
5041
5640
|
})
|
|
5042
5641
|
},
|
|
5642
|
+
"/favicon.ico": { GET: () => icon(FAVICON_ICO, "image/x-icon") },
|
|
5643
|
+
"/favicon.svg": { GET: () => icon(FAVICON_SVG, "image/svg+xml") },
|
|
5644
|
+
"/apple-touch-icon.png": {
|
|
5645
|
+
GET: () => icon(APPLE_TOUCH_ICON, "image/png")
|
|
5646
|
+
},
|
|
5043
5647
|
"/tab": (req, srv) => handleTab(req, srv),
|
|
5044
5648
|
"/.well-known/oauth-authorization-server": {
|
|
5045
5649
|
GET: () => auth.metadata(),
|
|
@@ -5180,5 +5784,5 @@ export {
|
|
|
5180
5784
|
resolveConfig
|
|
5181
5785
|
};
|
|
5182
5786
|
|
|
5183
|
-
//# debugId=
|
|
5787
|
+
//# debugId=61513056506259C164756E2164756E21
|
|
5184
5788
|
//# sourceMappingURL=index.js.map
|