@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/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.5.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.#keys.some((k) => k.kid === kid);
1185
- if ((stale || unknown) && now - this.#attemptedAt >= JWKS_REFETCH_MS) {
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
- return kid === undefined ? this.#keys : this.#keys.filter((k) => k.kid === kid);
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 fetchJson(await this.#url());
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 Map;
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 (!this.#perHost.available(url.host, now) || !this.#perDomain.available(key, now)) {
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 (!this.#global.take("all", now)) {
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
- this.#perHost.take(url.host, now);
1831
- this.#perDomain.take(key, now);
1832
- const address = await this.#vettedAddress(url);
1833
- const { meta, ttlMs } = await this.#fetchDocument(url, clientId, address);
1834
- this.#cache.delete(clientId);
1835
- if (this.#cache.size >= MAX_CACHE_ENTRIES) {
1836
- const oldest = this.#cache.keys().next().value;
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.#cache.set(clientId, { meta, until: now + ttlMs });
1841
- return meta;
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#fetchDocument(url, clientId, address) {
2222
+ async#fetchJson(url, address, types) {
1904
2223
  const fail = (detail) => this.#notLoaded("fetch_failed", url, detail);
1905
- const pinned = new URL(clientId);
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 !== "application/json")
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
- const meta = validateDocument(doc, clientId, url);
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 method = d["token_endpoint_auth_method"];
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
  "&": "&amp;",
@@ -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
- revocation_endpoint_auth_methods_supported: ["none"],
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 clientId = p["client_id"];
3311
- if (clientId === undefined) {
3312
- throw new OAuthError(400, "invalid_request", "client_id is required");
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, clientId);
3869
+ body = await this.#codeGrant(p, client);
3317
3870
  } else if (p["grant_type"] === "refresh_token") {
3318
- body = await this.#refreshGrant(p, clientId);
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", "client authentication is not supported (token_endpoint_auth_method none)");
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") || params.has("client_assertion")) {
3360
- throw new OAuthError(400, "invalid_client", "client authentication is not supported (token_endpoint_auth_method none)");
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#codeGrant(p, clientId) {
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, clientId) {
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, ["token", "client_id", "token_type_hint"]);
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
- const clientId = p["client_id"];
3532
- if (token === undefined || clientId === undefined) {
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 = 4;
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)`).run({
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></head>
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=ED4BD668E1904D7C64756E2164756E21
5869
+ //# debugId=0F130F38D40E142464756E2164756E21
5266
5870
  //# sourceMappingURL=main.js.map