@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/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";
|
|
@@ -986,6 +986,11 @@ class Exchange {
|
|
|
986
986
|
}
|
|
987
987
|
}
|
|
988
988
|
|
|
989
|
+
// src/icons.ts
|
|
990
|
+
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>';
|
|
991
|
+
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"));
|
|
992
|
+
var APPLE_TOUCH_ICON = new Uint8Array(Buffer.from("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", "base64"));
|
|
993
|
+
|
|
989
994
|
// src/jwt.ts
|
|
990
995
|
var JWT_ALGORITHMS = [
|
|
991
996
|
"RS256",
|
|
@@ -1003,6 +1008,7 @@ var MAX_JWT_BYTES = 8192;
|
|
|
1003
1008
|
var MAX_JWKS_BYTES = 65536;
|
|
1004
1009
|
var JWKS_TTL_MS = 10 * 60000;
|
|
1005
1010
|
var JWKS_REFETCH_MS = 30000;
|
|
1011
|
+
var UNKNOWN_KID_REFETCH_MS = 5000;
|
|
1006
1012
|
var JWKS_GRACE_MS = 60 * 60000;
|
|
1007
1013
|
var FETCH_TIMEOUT_MS = 5000;
|
|
1008
1014
|
|
|
@@ -1167,6 +1173,7 @@ class HttpJsonError extends Error {
|
|
|
1167
1173
|
class JwksCache {
|
|
1168
1174
|
#url;
|
|
1169
1175
|
#now;
|
|
1176
|
+
#load;
|
|
1170
1177
|
#keys = [];
|
|
1171
1178
|
#fetchedAt = 0;
|
|
1172
1179
|
#attemptedAt = Number.NEGATIVE_INFINITY;
|
|
@@ -1175,14 +1182,17 @@ class JwksCache {
|
|
|
1175
1182
|
constructor(url, options = {}) {
|
|
1176
1183
|
this.#url = typeof url === "string" ? () => Promise.resolve(url) : url;
|
|
1177
1184
|
this.#now = options.now ?? Date.now;
|
|
1185
|
+
this.#load = options.load ?? ((u) => fetchJson(u));
|
|
1178
1186
|
}
|
|
1179
1187
|
async keys(kid) {
|
|
1188
|
+
const entered = this.#now();
|
|
1180
1189
|
if (this.#inflight !== undefined)
|
|
1181
1190
|
await this.#inflight.catch(() => {});
|
|
1182
1191
|
const now = this.#now();
|
|
1183
1192
|
const stale = now - this.#fetchedAt >= JWKS_TTL_MS;
|
|
1184
|
-
const unknown = kid !== undefined && !this.#
|
|
1185
|
-
|
|
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) {
|
|
1186
1196
|
try {
|
|
1187
1197
|
await this.#refresh();
|
|
1188
1198
|
} catch (error) {
|
|
@@ -1192,13 +1202,21 @@ class JwksCache {
|
|
|
1192
1202
|
if (this.#now() - this.#fetchedAt >= JWKS_TTL_MS + JWKS_GRACE_MS) {
|
|
1193
1203
|
throw this.#lastError ?? new Error("the JWKS could not be loaded");
|
|
1194
1204
|
}
|
|
1195
|
-
|
|
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);
|
|
1196
1214
|
}
|
|
1197
1215
|
#refresh() {
|
|
1198
1216
|
this.#inflight ??= (async () => {
|
|
1199
1217
|
this.#attemptedAt = this.#now();
|
|
1200
1218
|
try {
|
|
1201
|
-
const doc = await
|
|
1219
|
+
const doc = await this.#load(await this.#url());
|
|
1202
1220
|
const keys = doc?.keys;
|
|
1203
1221
|
if (!Array.isArray(keys))
|
|
1204
1222
|
throw new Error("a JWKS needs keys");
|
|
@@ -1683,6 +1701,142 @@ class RateLimiter {
|
|
|
1683
1701
|
}
|
|
1684
1702
|
}
|
|
1685
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
|
+
|
|
1686
1840
|
// src/tokens.ts
|
|
1687
1841
|
var PREFIX = {
|
|
1688
1842
|
access: "tmcp_at_",
|
|
@@ -1712,7 +1866,8 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1712
1866
|
scopes: grant.scopes,
|
|
1713
1867
|
grantCreatedAt: grant.grantCreatedAt,
|
|
1714
1868
|
issuedAt: now,
|
|
1715
|
-
...grant.subject === undefined ? {} : { subject: grant.subject }
|
|
1869
|
+
...grant.subject === undefined ? {} : { subject: grant.subject },
|
|
1870
|
+
...grant.clientAuth === undefined ? {} : { clientAuth: grant.clientAuth }
|
|
1716
1871
|
};
|
|
1717
1872
|
const accessExpires = Math.min(now + options.accessTtlMs, options.grantEndsAt);
|
|
1718
1873
|
const refreshStored = await store.put({
|
|
@@ -1738,6 +1893,75 @@ async function issueTokenPair(store, grant, options) {
|
|
|
1738
1893
|
};
|
|
1739
1894
|
}
|
|
1740
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
|
+
|
|
1741
1965
|
// src/oauth/cimd.ts
|
|
1742
1966
|
class ClientMetadataError extends Error {
|
|
1743
1967
|
name = "ClientMetadataError";
|
|
@@ -1747,6 +1971,16 @@ class ClientMetadataError extends Error {
|
|
|
1747
1971
|
this.reason = reason;
|
|
1748
1972
|
}
|
|
1749
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;
|
|
1750
1984
|
var MAX_CLIENT_ID_LENGTH = 2048;
|
|
1751
1985
|
var MAX_NAME_LENGTH = 64;
|
|
1752
1986
|
var MAX_REDIRECT_URIS = 20;
|
|
@@ -1754,6 +1988,9 @@ var MIN_CACHE_MS = 5 * 60000;
|
|
|
1754
1988
|
var DEFAULT_CACHE_MS = 60 * 60000;
|
|
1755
1989
|
var MAX_CACHE_MS = 24 * 60 * 60000;
|
|
1756
1990
|
var MAX_CACHE_ENTRIES = 1000;
|
|
1991
|
+
var MAX_CACHE_PER_DOMAIN = 100;
|
|
1992
|
+
var FAILURE_CACHE_MS = 60000;
|
|
1993
|
+
var MAX_FAILURES = 1000;
|
|
1757
1994
|
async function defaultLookup(hostname) {
|
|
1758
1995
|
const records = await Bun.dns.lookup(hostname, {});
|
|
1759
1996
|
return records.map((r) => r.address);
|
|
@@ -1787,12 +2024,22 @@ function limitKey(hostname) {
|
|
|
1787
2024
|
return h;
|
|
1788
2025
|
return h.split(".").slice(-2).join(".");
|
|
1789
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
|
+
}
|
|
1790
2034
|
|
|
1791
2035
|
class ClientMetadataFetcher {
|
|
1792
|
-
#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;
|
|
1793
2038
|
#perHost = new RateLimiter({ capacity: 5, refillPerSecond: 0.2 });
|
|
1794
2039
|
#perDomain = new RateLimiter({ capacity: 15, refillPerSecond: 0.6 });
|
|
1795
2040
|
#global = new RateLimiter({ capacity: 30, refillPerSecond: 1 });
|
|
2041
|
+
#refreshes = knownBudget();
|
|
2042
|
+
#keys = knownBudget();
|
|
1796
2043
|
#allowedHosts;
|
|
1797
2044
|
#logger;
|
|
1798
2045
|
#timeoutMs;
|
|
@@ -1811,34 +2058,106 @@ class ClientMetadataFetcher {
|
|
|
1811
2058
|
this.#testMode = options.testMode;
|
|
1812
2059
|
this.#now = options.now ?? Date.now;
|
|
1813
2060
|
}
|
|
1814
|
-
async get(clientId) {
|
|
2061
|
+
async get(clientId, options = {}) {
|
|
1815
2062
|
const url = this.#checkUrl(clientId);
|
|
1816
2063
|
const now = this.#now();
|
|
1817
2064
|
const cached = this.#cache.get(clientId);
|
|
1818
|
-
if (cached !== undefined && cached.until > now)
|
|
1819
|
-
this.#cache.delete(clientId);
|
|
1820
|
-
this.#cache.set(clientId, cached);
|
|
2065
|
+
if (cached !== undefined && cached.until > now)
|
|
1821
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;
|
|
1822
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
|
+
};
|
|
1823
2139
|
const key = limitKey(url.hostname);
|
|
1824
|
-
if (!
|
|
2140
|
+
if (!perHost.available(url.host, now) || !perDomain.available(key, now)) {
|
|
1825
2141
|
throw new ClientMetadataError("rate_limited", "Too many requests for this AI app's details. Try again in a minute.");
|
|
1826
2142
|
}
|
|
1827
|
-
if (!
|
|
2143
|
+
if (!global.take("all", now)) {
|
|
1828
2144
|
throw new ClientMetadataError("rate_limited", "The relay is busy. Try again in a minute.");
|
|
1829
2145
|
}
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
this.#
|
|
1835
|
-
if (
|
|
1836
|
-
|
|
1837
|
-
if (oldest !== undefined)
|
|
1838
|
-
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.");
|
|
1839
2153
|
}
|
|
1840
|
-
this.#
|
|
1841
|
-
|
|
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;
|
|
1842
2161
|
}
|
|
1843
2162
|
#checkUrl(clientId) {
|
|
1844
2163
|
const refuse = (message) => new ClientMetadataError("invalid_client_id", message);
|
|
@@ -1900,9 +2219,9 @@ class ClientMetadataFetcher {
|
|
|
1900
2219
|
}
|
|
1901
2220
|
return addresses.find((a) => parseIpv4(a) !== undefined) ?? addresses[0] ?? "";
|
|
1902
2221
|
}
|
|
1903
|
-
async#
|
|
2222
|
+
async#fetchJson(url, address, types) {
|
|
1904
2223
|
const fail = (detail) => this.#notLoaded("fetch_failed", url, detail);
|
|
1905
|
-
const pinned = new URL(
|
|
2224
|
+
const pinned = new URL(url.href);
|
|
1906
2225
|
pinned.hostname = address.includes(":") ? `[${address}]` : address;
|
|
1907
2226
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
1908
2227
|
const tls = url.protocol === "https:" ? {
|
|
@@ -1927,8 +2246,9 @@ class ClientMetadataFetcher {
|
|
|
1927
2246
|
if (response.status !== 200)
|
|
1928
2247
|
throw fail(`status_${response.status}`);
|
|
1929
2248
|
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1930
|
-
if (type
|
|
2249
|
+
if (type === undefined || !types.includes(type)) {
|
|
1931
2250
|
throw fail("not_json");
|
|
2251
|
+
}
|
|
1932
2252
|
const text = await this.#readLimited(response, url);
|
|
1933
2253
|
let doc;
|
|
1934
2254
|
try {
|
|
@@ -1936,8 +2256,7 @@ class ClientMetadataFetcher {
|
|
|
1936
2256
|
} catch {
|
|
1937
2257
|
throw fail("bad_json");
|
|
1938
2258
|
}
|
|
1939
|
-
|
|
1940
|
-
return { meta, ttlMs: cacheMs(response.headers.get("cache-control")) };
|
|
2259
|
+
return { doc, cacheControl: response.headers.get("cache-control") };
|
|
1941
2260
|
} finally {
|
|
1942
2261
|
if (!response.bodyUsed)
|
|
1943
2262
|
response.body?.cancel().catch(() => {});
|
|
@@ -1992,7 +2311,7 @@ function foreignDomain(name, hostname) {
|
|
|
1992
2311
|
}
|
|
1993
2312
|
return false;
|
|
1994
2313
|
}
|
|
1995
|
-
function validateDocument(doc, clientId, url) {
|
|
2314
|
+
function validateDocument(doc, clientId, url, options = {}) {
|
|
1996
2315
|
const invalid = (message) => new ClientMetadataError("invalid_metadata", message);
|
|
1997
2316
|
if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
|
|
1998
2317
|
throw invalid("The AI app's details are not a JSON object.");
|
|
@@ -2018,10 +2337,7 @@ function validateDocument(doc, clientId, url) {
|
|
|
2018
2337
|
if (!Array.isArray(uris) || uris.length === 0 || uris.length > MAX_REDIRECT_URIS || !uris.every((u) => typeof u === "string" && isRedirectUri(u))) {
|
|
2019
2338
|
throw invalid("The AI app's details have no valid redirect_uris.");
|
|
2020
2339
|
}
|
|
2021
|
-
const
|
|
2022
|
-
if (method !== undefined && method !== "none") {
|
|
2023
|
-
throw invalid('The AI app must use token_endpoint_auth_method "none" with this relay.');
|
|
2024
|
-
}
|
|
2340
|
+
const auth = clientAuth(d, url, options, invalid);
|
|
2025
2341
|
const grants = d["grant_types"];
|
|
2026
2342
|
if (grants !== undefined && (!Array.isArray(grants) || !grants.includes("authorization_code"))) {
|
|
2027
2343
|
throw invalid("The AI app's details do not allow the authorization code grant.");
|
|
@@ -2034,9 +2350,81 @@ function validateDocument(doc, clientId, url) {
|
|
|
2034
2350
|
clientId,
|
|
2035
2351
|
name: displayName(name),
|
|
2036
2352
|
host: url.host,
|
|
2037
|
-
redirectUris: uris
|
|
2353
|
+
redirectUris: uris,
|
|
2354
|
+
auth
|
|
2038
2355
|
};
|
|
2039
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
|
+
}
|
|
2040
2428
|
var PRIVATE_USE_SCHEME = /^[a-z][a-z0-9+-]*(\.[a-z0-9+-]+)+:$/;
|
|
2041
2429
|
function isRedirectUri(value) {
|
|
2042
2430
|
if (value.length > 2048 || value.includes("#"))
|
|
@@ -2080,6 +2468,151 @@ function redirectUriMatches(requested, registered) {
|
|
|
2080
2468
|
return a.href === b.href;
|
|
2081
2469
|
}
|
|
2082
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
|
+
|
|
2083
2616
|
// src/oauth/html.ts
|
|
2084
2617
|
var ESCAPES = {
|
|
2085
2618
|
"&": "&",
|
|
@@ -2130,6 +2663,9 @@ function renderPage(title, body) {
|
|
|
2130
2663
|
<meta name="robots" content="noindex">
|
|
2131
2664
|
<title>${escapeHtml(title)}</title>
|
|
2132
2665
|
<link rel="stylesheet" href="/authorize.css">
|
|
2666
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
|
2667
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
2668
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
2133
2669
|
</head>
|
|
2134
2670
|
<body>
|
|
2135
2671
|
<main>
|
|
@@ -2646,6 +3182,12 @@ var REFRESH_REUSE_WINDOW_MS = 24 * 60 * 60000;
|
|
|
2646
3182
|
var MAX_CODES = 1e4;
|
|
2647
3183
|
var MAX_FORM_BYTES = 8192;
|
|
2648
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
|
+
}
|
|
2649
3191
|
|
|
2650
3192
|
class OAuthError extends Error {
|
|
2651
3193
|
status;
|
|
@@ -2695,10 +3237,16 @@ function hostOf(url) {
|
|
|
2695
3237
|
return url;
|
|
2696
3238
|
}
|
|
2697
3239
|
}
|
|
3240
|
+
function logHost(clientId) {
|
|
3241
|
+
return clientId !== undefined && URL.canParse(clientId) ? hostOf(clientId).slice(0, 100) : "(not a URL)";
|
|
3242
|
+
}
|
|
2698
3243
|
|
|
2699
3244
|
class AuthorizationServer {
|
|
2700
3245
|
#host;
|
|
2701
3246
|
#cimd;
|
|
3247
|
+
#clientKeys;
|
|
3248
|
+
#nonces = new NonceSet;
|
|
3249
|
+
#nonceQuota = new NonceQuota(MAX_NONCES_PER_HOST, MAX_NONCES);
|
|
2702
3250
|
#pending = new Map;
|
|
2703
3251
|
#pendingPerIp = new Map;
|
|
2704
3252
|
#pendingPerAddress = new Map;
|
|
@@ -2749,6 +3297,8 @@ class AuthorizationServer {
|
|
|
2749
3297
|
logger: host.logger,
|
|
2750
3298
|
...testMode?.cimd === undefined ? {} : { testMode: testMode.cimd }
|
|
2751
3299
|
});
|
|
3300
|
+
const cimd = this.#cimd;
|
|
3301
|
+
this.#clientKeys = new ClientKeySets((url) => cimd.fetchJwks(url));
|
|
2752
3302
|
}
|
|
2753
3303
|
get #issuer() {
|
|
2754
3304
|
return this.#host.publicUrl();
|
|
@@ -2802,8 +3352,10 @@ class AuthorizationServer {
|
|
|
2802
3352
|
response_modes_supported: ["query"],
|
|
2803
3353
|
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
2804
3354
|
code_challenge_methods_supported: ["S256"],
|
|
2805
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
2806
|
-
|
|
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,
|
|
2807
3359
|
scopes_supported: [SCOPE],
|
|
2808
3360
|
client_id_metadata_document_supported: true,
|
|
2809
3361
|
authorization_response_iss_parameter_supported: true,
|
|
@@ -3302,20 +3854,21 @@ ${cancel}
|
|
|
3302
3854
|
"code_verifier",
|
|
3303
3855
|
"refresh_token",
|
|
3304
3856
|
"resource",
|
|
3305
|
-
"scope"
|
|
3857
|
+
"scope",
|
|
3858
|
+
"client_assertion",
|
|
3859
|
+
"client_assertion_type"
|
|
3306
3860
|
]);
|
|
3307
3861
|
if ("duplicate" in p) {
|
|
3308
3862
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3309
3863
|
}
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
}
|
|
3864
|
+
const client = await this.#authenticateClient(p, [
|
|
3865
|
+
`${this.#issuer}/token`
|
|
3866
|
+
]);
|
|
3314
3867
|
let body;
|
|
3315
3868
|
if (p["grant_type"] === "authorization_code") {
|
|
3316
|
-
body = await this.#codeGrant(p,
|
|
3869
|
+
body = await this.#codeGrant(p, client);
|
|
3317
3870
|
} else if (p["grant_type"] === "refresh_token") {
|
|
3318
|
-
body = await this.#refreshGrant(p,
|
|
3871
|
+
body = await this.#refreshGrant(p, client);
|
|
3319
3872
|
} else if (p["grant_type"] === undefined) {
|
|
3320
3873
|
throw new OAuthError(400, "invalid_request", "grant_type is required");
|
|
3321
3874
|
} else {
|
|
@@ -3349,19 +3902,96 @@ ${cancel}
|
|
|
3349
3902
|
throw new OAuthError(400, "invalid_request", "the body must be application/x-www-form-urlencoded");
|
|
3350
3903
|
}
|
|
3351
3904
|
if (req.headers.get("authorization") !== null) {
|
|
3352
|
-
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)");
|
|
3353
3906
|
}
|
|
3354
3907
|
const text = await this.#readForm(req);
|
|
3355
3908
|
if (text === undefined) {
|
|
3356
3909
|
throw new OAuthError(413, "invalid_request", "the request is too large");
|
|
3357
3910
|
}
|
|
3358
3911
|
const params = new URLSearchParams(text);
|
|
3359
|
-
if (params.has("client_secret")
|
|
3360
|
-
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)");
|
|
3361
3914
|
}
|
|
3362
3915
|
return params;
|
|
3363
3916
|
}
|
|
3364
|
-
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;
|
|
3365
3995
|
const code = p["code"];
|
|
3366
3996
|
const redirectUri = p["redirect_uri"];
|
|
3367
3997
|
const verifier = p["code_verifier"];
|
|
@@ -3373,6 +4003,12 @@ ${cancel}
|
|
|
3373
4003
|
const invalid = (why) => new OAuthError(400, "invalid_grant", why);
|
|
3374
4004
|
if (issued === undefined)
|
|
3375
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
|
+
}
|
|
3376
4012
|
if (issued.used) {
|
|
3377
4013
|
if (issued.grantId !== undefined) {
|
|
3378
4014
|
await this.#revokeGrant(issued.addressId, issued.grantId, "reuse_detected");
|
|
@@ -3382,9 +4018,6 @@ ${cancel}
|
|
|
3382
4018
|
issued.used = true;
|
|
3383
4019
|
if (issued.expiresAt <= Date.now())
|
|
3384
4020
|
throw invalid("the code has expired");
|
|
3385
|
-
if (!constantTimeEqual(clientId, issued.client.clientId)) {
|
|
3386
|
-
throw invalid("the code was issued to another client");
|
|
3387
|
-
}
|
|
3388
4021
|
if (redirectUri !== issued.redirectUri) {
|
|
3389
4022
|
throw invalid("redirect_uri does not match the authorization request");
|
|
3390
4023
|
}
|
|
@@ -3408,7 +4041,8 @@ ${cancel}
|
|
|
3408
4041
|
clientName: issued.client.name,
|
|
3409
4042
|
scopes: issued.scopes,
|
|
3410
4043
|
grantCreatedAt: now,
|
|
3411
|
-
...issued.subject === undefined ? {} : { subject: issued.subject }
|
|
4044
|
+
...issued.subject === undefined ? {} : { subject: issued.subject },
|
|
4045
|
+
...client.assertion ? { clientAuth: "private_key_jwt" } : {}
|
|
3412
4046
|
}, {
|
|
3413
4047
|
accessTtlMs: config.accessTokenTtlMs,
|
|
3414
4048
|
refreshTtlMs: config.refreshTokenTtlMs,
|
|
@@ -3442,7 +4076,8 @@ ${cancel}
|
|
|
3442
4076
|
scope: scopes.join(" ")
|
|
3443
4077
|
};
|
|
3444
4078
|
}
|
|
3445
|
-
async#refreshGrant(p,
|
|
4079
|
+
async#refreshGrant(p, client) {
|
|
4080
|
+
const clientId = client.clientId;
|
|
3446
4081
|
const token = p["refresh_token"];
|
|
3447
4082
|
if (token === undefined) {
|
|
3448
4083
|
throw new OAuthError(400, "invalid_request", "refresh_token is required");
|
|
@@ -3456,6 +4091,9 @@ ${cancel}
|
|
|
3456
4091
|
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
3457
4092
|
throw invalid("the refresh token was issued to another client");
|
|
3458
4093
|
}
|
|
4094
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4095
|
+
throw this.#clientAuthFailed(clientId, "assertion_required");
|
|
4096
|
+
}
|
|
3459
4097
|
const now = Date.now();
|
|
3460
4098
|
if (record.expiresAt <= now)
|
|
3461
4099
|
throw invalid("the refresh token has expired");
|
|
@@ -3487,7 +4125,8 @@ ${cancel}
|
|
|
3487
4125
|
clientName: record.clientName,
|
|
3488
4126
|
scopes: record.scopes,
|
|
3489
4127
|
grantCreatedAt: record.grantCreatedAt,
|
|
3490
|
-
...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" } : {}
|
|
3491
4130
|
}, {
|
|
3492
4131
|
accessTtlMs: this.#host.config.accessTokenTtlMs,
|
|
3493
4132
|
refreshTtlMs: this.#host.config.refreshTokenTtlMs,
|
|
@@ -3523,24 +4162,36 @@ ${cancel}
|
|
|
3523
4162
|
throw new OAuthError(429, "temporarily_unavailable", "too many requests; try again shortly");
|
|
3524
4163
|
}
|
|
3525
4164
|
const params = await this.#formParams(req);
|
|
3526
|
-
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
|
+
]);
|
|
3527
4172
|
if ("duplicate" in p) {
|
|
3528
4173
|
throw new OAuthError(400, "invalid_request", `${p.duplicate} was sent more than once`);
|
|
3529
4174
|
}
|
|
3530
4175
|
const token = p["token"];
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
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");
|
|
3534
4178
|
}
|
|
4179
|
+
const client = await this.#authenticateClient(p, [
|
|
4180
|
+
`${this.#issuer}/revoke`,
|
|
4181
|
+
`${this.#issuer}/token`
|
|
4182
|
+
]);
|
|
3535
4183
|
const record = await this.#host.store.get(hashToken(token));
|
|
3536
4184
|
if (record === undefined)
|
|
3537
4185
|
return new Response(null, {
|
|
3538
4186
|
status: 200,
|
|
3539
4187
|
headers: { ...this.#cors(), ...noStore }
|
|
3540
4188
|
});
|
|
3541
|
-
if (!constantTimeEqual(clientId, record.clientId)) {
|
|
4189
|
+
if (!constantTimeEqual(client.clientId, record.clientId)) {
|
|
3542
4190
|
throw new OAuthError(400, "invalid_client", "the token was issued to another client");
|
|
3543
4191
|
}
|
|
4192
|
+
if (record.clientAuth === "private_key_jwt" && !client.assertion) {
|
|
4193
|
+
throw this.#clientAuthFailed(client.clientId, "assertion_required");
|
|
4194
|
+
}
|
|
3544
4195
|
await this.#revokeGrant(record.addressId, record.grantId, "revoked");
|
|
3545
4196
|
this.#host.logger.info("oauth.revoked", {
|
|
3546
4197
|
address: shortAddress(record.addressId),
|
|
@@ -3586,6 +4237,7 @@ ${cancel}
|
|
|
3586
4237
|
this.#guessesPerAddress.sweep(now);
|
|
3587
4238
|
this.#tokenPerIp.sweep(now);
|
|
3588
4239
|
this.#refreshesPerGrant.sweep(now);
|
|
4240
|
+
this.#nonces.sweep(now);
|
|
3589
4241
|
}
|
|
3590
4242
|
}
|
|
3591
4243
|
|
|
@@ -3994,112 +4646,10 @@ class TabSession {
|
|
|
3994
4646
|
}
|
|
3995
4647
|
}
|
|
3996
4648
|
|
|
3997
|
-
// src/store/memory.ts
|
|
3998
|
-
function copy(record) {
|
|
3999
|
-
return { ...record, scopes: [...record.scopes] };
|
|
4000
|
-
}
|
|
4001
|
-
function matches(record, selector) {
|
|
4002
|
-
return "grantId" in selector ? record.grantId === selector.grantId : record.addressId === selector.addressId;
|
|
4003
|
-
}
|
|
4004
|
-
function isLive(record, now) {
|
|
4005
|
-
return record.expiresAt > now && (record.kind === "access" || record.usedAt === undefined);
|
|
4006
|
-
}
|
|
4007
|
-
function summarizeGrants(records, now) {
|
|
4008
|
-
const grants = new Map;
|
|
4009
|
-
for (const r of records) {
|
|
4010
|
-
if (!isLive(r, now) || grants.has(r.grantId))
|
|
4011
|
-
continue;
|
|
4012
|
-
const summary = {
|
|
4013
|
-
grantId: r.grantId,
|
|
4014
|
-
clientId: r.clientId,
|
|
4015
|
-
clientName: r.clientName,
|
|
4016
|
-
scopes: [...r.scopes],
|
|
4017
|
-
createdAt: r.grantCreatedAt
|
|
4018
|
-
};
|
|
4019
|
-
if (r.subject !== undefined)
|
|
4020
|
-
summary.subject = r.subject;
|
|
4021
|
-
grants.set(r.grantId, summary);
|
|
4022
|
-
}
|
|
4023
|
-
return [...grants.values()].sort((a, b) => a.createdAt - b.createdAt || a.grantId.localeCompare(b.grantId));
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
|
-
class MemoryTokenStore {
|
|
4027
|
-
#records = new Map;
|
|
4028
|
-
#revokedGrants = new Map;
|
|
4029
|
-
#revokedAddresses = new Map;
|
|
4030
|
-
async put(record) {
|
|
4031
|
-
if (this.#revokedGrants.has(record.grantId))
|
|
4032
|
-
return false;
|
|
4033
|
-
const address = this.#revokedAddresses.get(record.addressId);
|
|
4034
|
-
if (address !== undefined && record.grantCreatedAt <= address.at) {
|
|
4035
|
-
return false;
|
|
4036
|
-
}
|
|
4037
|
-
this.#records.set(record.hash, copy(record));
|
|
4038
|
-
return true;
|
|
4039
|
-
}
|
|
4040
|
-
async get(hash) {
|
|
4041
|
-
const r = this.#records.get(hash);
|
|
4042
|
-
return r === undefined ? undefined : copy(r);
|
|
4043
|
-
}
|
|
4044
|
-
async markUsed(hash, now, keepUntil) {
|
|
4045
|
-
const r = this.#records.get(hash);
|
|
4046
|
-
if (r === undefined || r.usedAt !== undefined)
|
|
4047
|
-
return false;
|
|
4048
|
-
r.usedAt = now;
|
|
4049
|
-
r.expiresAt = Math.min(r.expiresAt, keepUntil);
|
|
4050
|
-
return true;
|
|
4051
|
-
}
|
|
4052
|
-
async revoke(selector, options) {
|
|
4053
|
-
if ("grantId" in selector) {
|
|
4054
|
-
this.#revokedGrants.set(selector.grantId, options.keepUntil);
|
|
4055
|
-
} else {
|
|
4056
|
-
const old = this.#revokedAddresses.get(selector.addressId);
|
|
4057
|
-
this.#revokedAddresses.set(selector.addressId, {
|
|
4058
|
-
at: Math.max(old?.at ?? 0, options.now),
|
|
4059
|
-
until: Math.max(old?.until ?? 0, options.keepUntil)
|
|
4060
|
-
});
|
|
4061
|
-
}
|
|
4062
|
-
let n = 0;
|
|
4063
|
-
for (const [hash, r] of this.#records) {
|
|
4064
|
-
if (matches(r, selector)) {
|
|
4065
|
-
this.#records.delete(hash);
|
|
4066
|
-
n++;
|
|
4067
|
-
}
|
|
4068
|
-
}
|
|
4069
|
-
return n;
|
|
4070
|
-
}
|
|
4071
|
-
async listGrants(addressId, now) {
|
|
4072
|
-
const own = [...this.#records.values()].filter((r) => r.addressId === addressId);
|
|
4073
|
-
return summarizeGrants(own, now);
|
|
4074
|
-
}
|
|
4075
|
-
async deleteExpired(now) {
|
|
4076
|
-
for (const [id, until] of this.#revokedGrants) {
|
|
4077
|
-
if (until <= now)
|
|
4078
|
-
this.#revokedGrants.delete(id);
|
|
4079
|
-
}
|
|
4080
|
-
for (const [id, t] of this.#revokedAddresses) {
|
|
4081
|
-
if (t.until <= now)
|
|
4082
|
-
this.#revokedAddresses.delete(id);
|
|
4083
|
-
}
|
|
4084
|
-
let n = 0;
|
|
4085
|
-
for (const [hash, r] of this.#records) {
|
|
4086
|
-
if (r.expiresAt <= now) {
|
|
4087
|
-
this.#records.delete(hash);
|
|
4088
|
-
n++;
|
|
4089
|
-
}
|
|
4090
|
-
}
|
|
4091
|
-
return n;
|
|
4092
|
-
}
|
|
4093
|
-
async close() {
|
|
4094
|
-
this.#records.clear();
|
|
4095
|
-
this.#revokedGrants.clear();
|
|
4096
|
-
this.#revokedAddresses.clear();
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4099
|
-
|
|
4100
4649
|
// src/store/sqlite.ts
|
|
4101
4650
|
import { Database } from "bun:sqlite";
|
|
4102
|
-
var SCHEMA_VERSION =
|
|
4651
|
+
var SCHEMA_VERSION = 5;
|
|
4652
|
+
var MIN_READER_VERSION = 5;
|
|
4103
4653
|
var REUSE_WINDOW_MS = 86400000;
|
|
4104
4654
|
function toRecord(row) {
|
|
4105
4655
|
const record = {
|
|
@@ -4118,6 +4668,8 @@ function toRecord(row) {
|
|
|
4118
4668
|
record.usedAt = row.used_at;
|
|
4119
4669
|
if (row.subject !== null)
|
|
4120
4670
|
record.subject = row.subject;
|
|
4671
|
+
if (row.client_auth === "private_key_jwt")
|
|
4672
|
+
record.clientAuth = "private_key_jwt";
|
|
4121
4673
|
return record;
|
|
4122
4674
|
}
|
|
4123
4675
|
|
|
@@ -4128,7 +4680,24 @@ class SqliteTokenStore {
|
|
|
4128
4680
|
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
4129
4681
|
if (path !== ":memory:")
|
|
4130
4682
|
this.#db.run("PRAGMA journal_mode = WAL");
|
|
4683
|
+
this.#checkReader();
|
|
4131
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
|
+
}
|
|
4132
4701
|
}
|
|
4133
4702
|
#migrate() {
|
|
4134
4703
|
const row = this.#db.query("PRAGMA user_version").get();
|
|
@@ -4154,12 +4723,20 @@ class SqliteTokenStore {
|
|
|
4154
4723
|
issued_at INTEGER NOT NULL,
|
|
4155
4724
|
expires_at INTEGER NOT NULL,
|
|
4156
4725
|
used_at INTEGER,
|
|
4157
|
-
subject TEXT
|
|
4726
|
+
subject TEXT,
|
|
4727
|
+
client_auth TEXT
|
|
4158
4728
|
) STRICT`);
|
|
4159
4729
|
const columns = this.#db.query("PRAGMA table_info(tokens)").all();
|
|
4160
4730
|
if (!columns.some((c) => c.name === "subject")) {
|
|
4161
4731
|
this.#db.run("ALTER TABLE tokens ADD COLUMN subject TEXT");
|
|
4162
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`);
|
|
4163
4740
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_grant ON tokens (grant_id)");
|
|
4164
4741
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_address ON tokens (address_id)");
|
|
4165
4742
|
this.#db.run("CREATE INDEX IF NOT EXISTS tokens_expires ON tokens (expires_at)");
|
|
@@ -4184,9 +4761,11 @@ class SqliteTokenStore {
|
|
|
4184
4761
|
}
|
|
4185
4762
|
#insert(r) {
|
|
4186
4763
|
this.#db.query(`INSERT OR REPLACE INTO tokens (hash, kind, grant_id, address_id, client_id,
|
|
4187
|
-
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)
|
|
4188
4766
|
VALUES ($hash, $kind, $grant_id, $address_id, $client_id, $client_name,
|
|
4189
|
-
$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({
|
|
4190
4769
|
hash: r.hash,
|
|
4191
4770
|
kind: r.kind,
|
|
4192
4771
|
grant_id: r.grantId,
|
|
@@ -4198,7 +4777,8 @@ class SqliteTokenStore {
|
|
|
4198
4777
|
issued_at: r.issuedAt,
|
|
4199
4778
|
expires_at: r.expiresAt,
|
|
4200
4779
|
used_at: r.usedAt ?? null,
|
|
4201
|
-
subject: r.subject ?? null
|
|
4780
|
+
subject: r.subject ?? null,
|
|
4781
|
+
client_auth: r.clientAuth ?? null
|
|
4202
4782
|
});
|
|
4203
4783
|
}
|
|
4204
4784
|
async get(hash) {
|
|
@@ -4234,8 +4814,15 @@ class SqliteTokenStore {
|
|
|
4234
4814
|
}
|
|
4235
4815
|
async deleteExpired(now) {
|
|
4236
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 });
|
|
4237
4818
|
return this.#db.query("DELETE FROM tokens WHERE expires_at <= $now").run({ now }).changes;
|
|
4238
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
|
+
}
|
|
4239
4826
|
async close() {
|
|
4240
4827
|
this.#db.close();
|
|
4241
4828
|
}
|
|
@@ -4323,12 +4910,24 @@ function metaVersion(body) {
|
|
|
4323
4910
|
}
|
|
4324
4911
|
var INDEX_HTML = `<!doctype html>
|
|
4325
4912
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4326
|
-
<title>tab-mcp relay</title
|
|
4913
|
+
<title>tab-mcp relay</title>
|
|
4914
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32"><link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
4915
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png"></head>
|
|
4327
4916
|
<body><h1>tab-mcp relay</h1>
|
|
4328
4917
|
<p>This server relays AI apps' MCP requests to tools offered by web pages open in a browser tab.
|
|
4329
4918
|
It does not log request contents. Source and documentation:
|
|
4330
4919
|
<a href="https://github.com/ractive/tab-mcp">github.com/ractive/tab-mcp</a>.</p>
|
|
4331
4920
|
</body></html>`;
|
|
4921
|
+
function icon(body, type) {
|
|
4922
|
+
return new Response(body, {
|
|
4923
|
+
headers: {
|
|
4924
|
+
"content-type": type,
|
|
4925
|
+
"content-security-policy": "default-src 'none'",
|
|
4926
|
+
"x-content-type-options": "nosniff",
|
|
4927
|
+
"cache-control": "public, max-age=86400"
|
|
4928
|
+
}
|
|
4929
|
+
});
|
|
4930
|
+
}
|
|
4332
4931
|
function createRelay(options = {}) {
|
|
4333
4932
|
return startRelay(options, undefined);
|
|
4334
4933
|
}
|
|
@@ -5040,12 +5639,17 @@ function startRelay(options, hooks) {
|
|
|
5040
5639
|
GET: () => new Response(INDEX_HTML, {
|
|
5041
5640
|
headers: {
|
|
5042
5641
|
"content-type": "text/html; charset=utf-8",
|
|
5043
|
-
"content-security-policy": "default-src 'none'; frame-ancestors 'none'",
|
|
5642
|
+
"content-security-policy": "default-src 'none'; img-src 'self'; frame-ancestors 'none'",
|
|
5044
5643
|
"x-content-type-options": "nosniff",
|
|
5045
5644
|
"cache-control": "public, max-age=3600"
|
|
5046
5645
|
}
|
|
5047
5646
|
})
|
|
5048
5647
|
},
|
|
5648
|
+
"/favicon.ico": { GET: () => icon(FAVICON_ICO, "image/x-icon") },
|
|
5649
|
+
"/favicon.svg": { GET: () => icon(FAVICON_SVG, "image/svg+xml") },
|
|
5650
|
+
"/apple-touch-icon.png": {
|
|
5651
|
+
GET: () => icon(APPLE_TOUCH_ICON, "image/png")
|
|
5652
|
+
},
|
|
5049
5653
|
"/tab": (req, srv) => handleTab(req, srv),
|
|
5050
5654
|
"/.well-known/oauth-authorization-server": {
|
|
5051
5655
|
GET: () => auth.metadata(),
|
|
@@ -5262,5 +5866,5 @@ var shutdown = () => {
|
|
|
5262
5866
|
process.on("SIGTERM", shutdown);
|
|
5263
5867
|
process.on("SIGINT", shutdown);
|
|
5264
5868
|
|
|
5265
|
-
//# debugId=
|
|
5869
|
+
//# debugId=0F130F38D40E142464756E2164756E21
|
|
5266
5870
|
//# sourceMappingURL=main.js.map
|