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