A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing

feat: added initial server package

authored by stevedylan.dev and committed by tangled.org ec148ac7 b6b1f627

+1374 -16
+122 -15
bun.lock
··· 58 58 "typescript": "^5", 59 59 }, 60 60 }, 61 + "packages/server": { 62 + "name": "@sequoia/server", 63 + "version": "0.1.0", 64 + "dependencies": { 65 + "@atproto-labs/handle-resolver": "^0.1.5", 66 + "@atproto/api": "^0.13.21", 67 + "@atproto/jwk-jose": "^0.1.3", 68 + "@atproto/oauth-client": "^0.3.3", 69 + "hono": "^4.7.4", 70 + }, 71 + }, 61 72 }, 62 73 "packages": { 63 74 "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], 64 75 65 - "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.6", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg=="], 76 + "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.1.13", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.2.0", "@atproto-labs/simple-store-memory": "0.1.3", "@atproto/did": "0.1.5", "zod": "^3.23.8" } }, "sha512-DG3YNaCKc6PAIv1Gsz3E1Kufw2t14OBxe4LdKK7KKLCNoex51hm+A5yMevShe3BSll+QosqWYIEgkPSc5xBoGQ=="], 66 77 67 78 "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.3", "", { "dependencies": { "@atproto-labs/pipe": "0.1.1" } }, "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw=="], 68 79 69 80 "@atproto-labs/fetch-node": ["@atproto-labs/fetch-node@0.2.0", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "ipaddr.js": "^2.1.0", "undici": "^6.14.1" } }, "sha512-Krq09nH/aeoiU2s9xdHA0FjTEFWG9B5FFenipv1iRixCcPc7V3DhTNDawxG9gI8Ny0k4dBVS9WTRN/IDzBx86Q=="], 70 81 71 - "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA=="], 82 + "@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.1.8", "", { "dependencies": { "@atproto-labs/simple-store": "0.2.0", "@atproto-labs/simple-store-memory": "0.1.3", "@atproto/did": "0.1.5", "zod": "^3.23.8" } }, "sha512-Y0ckccoCGDo/3g4thPkgp9QcORmc+qqEaCBCYCZYtfLIQp4775u22wd+4fyEyJP4DqoReKacninkICgRGfs3dQ=="], 72 83 73 84 "@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.25", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.6", "@atproto/did": "0.3.0" } }, "sha512-NY9WYM2VLd3IuMGRkkmvGBg8xqVEaK/fitv1vD8SMXqFTekdpjOLCCyv7EFtqVHouzmDcL83VOvWRfHVa8V9Yw=="], 74 85 75 - "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver": "0.3.6" } }, "sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg=="], 86 + "@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.1.18", "", { "dependencies": { "@atproto-labs/did-resolver": "0.1.13", "@atproto-labs/handle-resolver": "0.1.8", "@atproto/syntax": "0.4.0" } }, "sha512-DArYXP1hzZJIBcojun0CWEF+TjAhlGKcVq/RwLiGfY1mKq2yPjCiXyHj+5L0+z9jBSZiAB7L65JgcjI2+MFiRg=="], 76 87 77 88 "@atproto-labs/pipe": ["@atproto-labs/pipe@0.1.1", "", {}, "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="], 78 89 79 - "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 90 + "@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.2.0", "", {}, "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA=="], 80 91 81 - "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 92 + "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.3", "", { "dependencies": { "@atproto-labs/simple-store": "0.2.0", "lru-cache": "^10.2.0" } }, "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ=="], 82 93 83 - "@atproto/api": ["@atproto/api@0.19.0", "", { "dependencies": { "@atproto/common-web": "^0.4.17", "@atproto/lexicon": "^0.6.1", "@atproto/syntax": "^0.4.3", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-7u/EGgkIj4bbslGer2RMQPtMWCPvREcpH0mVagaf5om+NcPzUIZeIacWKANVv95BdMJ7jlcHS7xrkEMPmg2dFw=="], 94 + "@atproto/api": ["@atproto/api@0.13.35", "", { "dependencies": { "@atproto/common-web": "^0.4.0", "@atproto/lexicon": "^0.4.6", "@atproto/syntax": "^0.3.2", "@atproto/xrpc": "^0.6.8", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g=="], 84 95 85 96 "@atproto/common-web": ["@atproto/common-web@0.4.17", "", { "dependencies": { "@atproto/lex-data": "^0.0.12", "@atproto/lex-json": "^0.0.12", "@atproto/syntax": "^0.4.3", "zod": "^3.23.8" } }, "sha512-sfxD8NGxyoxhxmM9EUshEFbWcJ3+JHEOZF4Quk6HsCh1UxpHBmLabT/vEsAkDWl+C/8U0ine0+c/gHyE/OZiQQ=="], 86 97 87 - "@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="], 98 + "@atproto/did": ["@atproto/did@0.1.5", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ=="], 88 99 89 100 "@atproto/jwk": ["@atproto/jwk@0.6.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-bDoJPvt7TrQVi/rBfBrSSpGykhtIriKxeYCYQTiPRKFfyRhbgpElF0wPXADjIswnbzZdOwbY63az4E/CFVT3Tw=="], 90 101 ··· 96 107 97 108 "@atproto/lex-json": ["@atproto/lex-json@0.0.12", "", { "dependencies": { "@atproto/lex-data": "^0.0.12", "tslib": "^2.8.1" } }, "sha512-XlEpnWWZdDJ5BIgG25GyH+6iBfyrFL18BI5JSE6rUfMObbFMrQRaCuRLQfryRXNysVz3L3U+Qb9y8KcXbE8AcA=="], 98 109 99 - "@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], 110 + "@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="], 100 111 101 - "@atproto/oauth-client": ["@atproto/oauth-client@0.6.0", "", { "dependencies": { "@atproto-labs/did-resolver": "^0.2.6", "@atproto-labs/fetch": "^0.2.3", "@atproto-labs/handle-resolver": "^0.3.6", "@atproto-labs/identity-resolver": "^0.3.6", "@atproto-labs/simple-store": "^0.3.0", "@atproto-labs/simple-store-memory": "^0.1.4", "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "@atproto/oauth-types": "^0.6.3", "@atproto/xrpc": "^0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-F7ZTKzFptXgyihMkd7QTdRSkrh4XqrS+qTw+V81k5Q6Bh3MB1L3ypvfSJ6v7SSUJa6XxoZYJTCahHC1e+ndE6Q=="], 112 + "@atproto/oauth-client": ["@atproto/oauth-client@0.3.22", "", { "dependencies": { "@atproto-labs/did-resolver": "0.1.13", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.1.8", "@atproto-labs/identity-resolver": "0.1.18", "@atproto-labs/simple-store": "0.2.0", "@atproto-labs/simple-store-memory": "0.1.3", "@atproto/did": "0.1.5", "@atproto/jwk": "0.2.0", "@atproto/oauth-types": "0.2.8", "@atproto/xrpc": "0.7.0", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-IJYkUSGGklV7tQ0S2+5smh8Xmu5MwfxBUNXMtqiooeU2nj+UcNk3/b0nE4MS05JNfwh2BXgHv3P8hrhVG2+RAA=="], 102 113 103 114 "@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.16", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver-node": "0.1.25", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.14", "@atproto/oauth-types": "0.6.2" } }, "sha512-2dooMzxAkiQ4MkOAZlEQ3iwbB9SEovrbIKMNuBbVCLQYORVNxe20tMdjs3lvhrzdpzvaHLlQnJJhw5dA9VELFw=="], 104 115 105 - "@atproto/oauth-types": ["@atproto/oauth-types@0.6.3", "", { "dependencies": { "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "zod": "^3.23.8" } }, "sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng=="], 116 + "@atproto/oauth-types": ["@atproto/oauth-types@0.2.8", "", { "dependencies": { "@atproto/jwk": "0.2.0", "zod": "^3.23.8" } }, "sha512-xcYI2JmhrWwscePDoaKeDawVCCZkcvBqrBFMpMk4gf/OujH0pNSKBD/aWsayc6WvujVbTqwrG2hwPLfRqzJbwg=="], 106 117 107 - "@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 118 + "@atproto/syntax": ["@atproto/syntax@0.3.4", "", {}, "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg=="], 108 119 109 - "@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 120 + "@atproto/xrpc": ["@atproto/xrpc@0.6.12", "", { "dependencies": { "@atproto/lexicon": "^0.4.10", "zod": "^3.23.8" } }, "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w=="], 110 121 111 122 "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], 112 123 ··· 533 544 "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA=="], 534 545 535 546 "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="], 547 + 548 + "@sequoia/server": ["@sequoia/server@workspace:packages/server"], 536 549 537 550 "@shikijs/core": ["@shikijs/core@1.29.2", "", { "dependencies": { "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ=="], 538 551 ··· 1648 1661 1649 1662 "@atproto-labs/fetch-node/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], 1650 1663 1651 - "@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1664 + "@atproto-labs/handle-resolver-node/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA=="], 1665 + 1666 + "@atproto-labs/handle-resolver-node/@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="], 1667 + 1668 + "@atproto-labs/identity-resolver/@atproto/syntax": ["@atproto/syntax@0.4.0", "", {}, "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="], 1669 + 1670 + "@atproto/common-web/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 1671 + 1672 + "@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 1673 + 1674 + "@atproto/oauth-client/@atproto/jwk": ["@atproto/jwk@0.2.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-foOxExbw04XCaoLaGdv9BQj0Ac7snZsk6IpQjOsjYatf+i62Pi9bUkZ0MAoA75HPk8ZmKoDnbA60uBMmiOPPHQ=="], 1675 + 1676 + "@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.0", "", { "dependencies": { "@atproto/lexicon": "^0.4.11", "zod": "^3.23.8" } }, "sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw=="], 1677 + 1678 + "@atproto/oauth-client-node/@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.6", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg=="], 1679 + 1680 + "@atproto/oauth-client-node/@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 1681 + 1682 + "@atproto/oauth-client-node/@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="], 1652 1683 1653 1684 "@atproto/oauth-client-node/@atproto/oauth-client": ["@atproto/oauth-client@0.5.14", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.6", "@atproto-labs/identity-resolver": "0.3.6", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.2", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-sPH+vcdq9maTEAhJI0HzmFcFAMrkCS19np+RUssNkX6kS8Xr3OYr57tvYRCbkcnIyYTfYcxKQgpwHKx3RVEaYw=="], 1654 1685 1655 1686 "@atproto/oauth-client-node/@atproto/oauth-types": ["@atproto/oauth-types@0.6.2", "", { "dependencies": { "@atproto/did": "0.3.0", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-2cuboM4RQBCYR8NQC5uGRkW6KgCgKyq/B5/+tnMmWZYtZGVUQvsUWQHK/ZiMCnVXbcDNtc/RIEJQJDZ8FXMoxg=="], 1687 + 1688 + "@atproto/oauth-types/@atproto/jwk": ["@atproto/jwk@0.2.0", "", { "dependencies": { "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-foOxExbw04XCaoLaGdv9BQj0Ac7snZsk6IpQjOsjYatf+i62Pi9bUkZ0MAoA75HPk8ZmKoDnbA60uBMmiOPPHQ=="], 1656 1689 1657 1690 "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1658 1691 ··· 1710 1743 1711 1744 "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], 1712 1745 1746 + "docs/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA=="], 1747 + 1748 + "docs/@atproto/api": ["@atproto/api@0.19.0", "", { "dependencies": { "@atproto/common-web": "^0.4.17", "@atproto/lexicon": "^0.6.1", "@atproto/syntax": "^0.4.3", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-7u/EGgkIj4bbslGer2RMQPtMWCPvREcpH0mVagaf5om+NcPzUIZeIacWKANVv95BdMJ7jlcHS7xrkEMPmg2dFw=="], 1749 + 1750 + "docs/@atproto/oauth-client": ["@atproto/oauth-client@0.6.0", "", { "dependencies": { "@atproto-labs/did-resolver": "^0.2.6", "@atproto-labs/fetch": "^0.2.3", "@atproto-labs/handle-resolver": "^0.3.6", "@atproto-labs/identity-resolver": "^0.3.6", "@atproto-labs/simple-store": "^0.3.0", "@atproto-labs/simple-store-memory": "^0.1.4", "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "@atproto/oauth-types": "^0.6.3", "@atproto/xrpc": "^0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-F7ZTKzFptXgyihMkd7QTdRSkrh4XqrS+qTw+V81k5Q6Bh3MB1L3ypvfSJ6v7SSUJa6XxoZYJTCahHC1e+ndE6Q=="], 1751 + 1713 1752 "eval/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], 1714 1753 1715 1754 "hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], ··· 1744 1783 1745 1784 "vocs/hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], 1746 1785 1747 - "@atproto/lexicon/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1786 + "@atproto-labs/handle-resolver-node/@atproto-labs/handle-resolver/@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 1787 + 1788 + "@atproto-labs/handle-resolver-node/@atproto-labs/handle-resolver/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 1789 + 1790 + "@atproto/oauth-client-node/@atproto-labs/did-resolver/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 1748 1791 1749 - "@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1792 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-qnSTXvOBNj1EHhp2qTWSX8MS5q3AwYU5LKlt5fBvSbCjgmTr2j0URHCv+ydrwO55KvsojIkTMgeMOh4YuY4fCA=="], 1793 + 1794 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver": "0.3.6" } }, "sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg=="], 1795 + 1796 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 1797 + 1798 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1750 1799 1751 1800 "@radix-ui/react-label/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], 1752 1801 ··· 1816 1865 1817 1866 "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], 1818 1867 1868 + "docs/@atproto-labs/handle-resolver/@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 1869 + 1870 + "docs/@atproto-labs/handle-resolver/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 1871 + 1872 + "docs/@atproto-labs/handle-resolver/@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="], 1873 + 1874 + "docs/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], 1875 + 1876 + "docs/@atproto/api/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 1877 + 1878 + "docs/@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1879 + 1880 + "docs/@atproto/oauth-client/@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.6", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.3.0", "zod": "^3.23.8" } }, "sha512-2K1bC04nI2fmgNcvof+yA28IhGlpWn2JKYlPa7To9JTKI45FINCGkQSGiL2nyXlyzDJJ34fZ1aq6/IRFIOIiqg=="], 1881 + 1882 + "docs/@atproto/oauth-client/@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.6", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.6", "@atproto-labs/handle-resolver": "0.3.6" } }, "sha512-qoWqBDRobln0NR8L8dQjSp79E0chGkBhibEgxQa2f9WD+JbJdjQ0YvwwO5yeQn05pJoJmAwmI2wyJ45zjU7aWg=="], 1883 + 1884 + "docs/@atproto/oauth-client/@atproto-labs/simple-store": ["@atproto-labs/simple-store@0.3.0", "", {}, "sha512-nOb6ONKBRJHRlukW1sVawUkBqReLlLx6hT35VS3imaNPwiXDxLnTK7lxw3Lrl9k5yugSBDQAkZAq3MPTEFSUBQ=="], 1885 + 1886 + "docs/@atproto/oauth-client/@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 1887 + 1888 + "docs/@atproto/oauth-client/@atproto/did": ["@atproto/did@0.3.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-raUPzUGegtW/6OxwCmM8bhZvuIMzxG5t9oWsth6Tp91Kb5fTnHV2h/KKNF1C82doeA4BdXCErTyg7ISwLbQkzA=="], 1889 + 1890 + "docs/@atproto/oauth-client/@atproto/oauth-types": ["@atproto/oauth-types@0.6.3", "", { "dependencies": { "@atproto/did": "^0.3.0", "@atproto/jwk": "^0.6.0", "zod": "^3.23.8" } }, "sha512-jdKuoPknJuh/WjI+mYk7agSbx9mNVMbS6Dr3k1z2YMY2oRiCQjxYBuo4MLKATbxj05nMQaZRWlHRUazoAu5Cng=="], 1891 + 1892 + "docs/@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1893 + 1819 1894 "eval/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1820 1895 1821 1896 "hast-util-from-dom/hastscript/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], ··· 1825 1900 "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1826 1901 1827 1902 "sequoia-cli/@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1903 + 1904 + "sequoia-cli/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], 1905 + 1906 + "sequoia-cli/@atproto/api/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 1907 + 1908 + "sequoia-cli/@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1828 1909 1829 1910 "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], 1830 1911 ··· 1878 1959 1879 1960 "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], 1880 1961 1962 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], 1963 + 1964 + "docs/@atproto/api/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1965 + 1966 + "docs/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], 1967 + 1881 1968 "sequoia-cli/@atproto/api/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1882 1969 1883 1970 "sequoia-cli/@atproto/api/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1971 + 1972 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1973 + 1974 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 1975 + 1976 + "docs/@atproto/api/@atproto/lexicon/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1977 + 1978 + "docs/@atproto/api/@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1979 + 1980 + "docs/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1981 + 1982 + "docs/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 1983 + 1984 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1985 + 1986 + "@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1987 + 1988 + "docs/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1989 + 1990 + "docs/@atproto/oauth-client/@atproto/xrpc/@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1884 1991 } 1885 1992 }
+3 -1
package.json
··· 13 13 "build:docs": "cd docs && bun run build", 14 14 "build:cli": "cd packages/cli && bun run build", 15 15 "deploy:docs": "cd docs && bun run deploy", 16 - "deploy:cli": "cd packages/cli && bun run deploy" 16 + "deploy:cli": "cd packages/cli && bun run deploy", 17 + "dev:server": "cd packages/server && bun run dev", 18 + "start:server": "cd packages/server && bun run start" 17 19 }, 18 20 "devDependencies": { 19 21 "@types/bun": "latest",
+20
packages/server/.env.example
··· 1 + CLIENT_URL=https://your-domain.com 2 + CLIENT_NAME=Sequoia 3 + PORT=3000 4 + REDIS_URL=redis://redis:6379 5 + 6 + # Theme overrides (optional) 7 + # THEME_ACCENT_COLOR=#3A5A40 8 + # THEME_BG_COLOR=#F5F3EF 9 + # THEME_FG_COLOR=#2C2C2C 10 + # THEME_BORDER_COLOR=#D5D1C8 11 + # THEME_ERROR_COLOR=#8B3A3A 12 + # THEME_BORDER_RADIUS=6px 13 + # THEME_FONT_FAMILY=system-ui, sans-serif 14 + # THEME_DARK_BG_COLOR=#1A1A1A 15 + # THEME_DARK_FG_COLOR=#E5E5E5 16 + # THEME_DARK_BORDER_COLOR=#3A3A3A 17 + # THEME_DARK_ERROR_COLOR=#E57373 18 + 19 + # Path to a custom CSS file for full theme control (optional) 20 + # THEME_CSS_PATH=/app/theme.css
+15
packages/server/Dockerfile
··· 1 + FROM oven/bun:1 AS install 2 + WORKDIR /app 3 + COPY package.json bun.lock* ./ 4 + RUN bun install --frozen-lockfile || bun install 5 + 6 + FROM oven/bun:1 7 + WORKDIR /app 8 + COPY --from=install /app/node_modules ./node_modules 9 + COPY package.json ./ 10 + COPY src ./src 11 + 12 + ENV PORT=3000 13 + EXPOSE ${PORT} 14 + 15 + ENTRYPOINT ["bun", "run", "src/index.ts"]
+93
packages/server/README.md
··· 1 + # Sequoia Server 2 + 3 + Self-hostable AT Protocol OAuth and subscription server. Handles Bluesky login and manages `site.standard.graph.subscription` records on behalf of users. Built with Bun, Hono, and Redis. 4 + 5 + ## Quickstart 6 + 7 + ### Docker (recommended) 8 + 9 + ```bash 10 + cp .env.example .env 11 + # Edit .env — at minimum set CLIENT_URL to your public URL 12 + docker compose up 13 + ``` 14 + 15 + ### Local development 16 + 17 + Requires [Bun](https://bun.sh) and a running Redis instance. 18 + 19 + ```bash 20 + bun install 21 + CLIENT_URL=http://localhost:3000 bun run dev 22 + ``` 23 + 24 + ## How it works 25 + 26 + 1. A user visits `/subscribe?publicationUri=at://...` and enters their Bluesky handle 27 + 2. The server initiates an AT Protocol OAuth flow — the user authorizes on Bluesky 28 + 3. After callback, the server creates a `site.standard.graph.subscription` record in the user's repo 29 + 4. The [sequoia-subscribe](https://github.com/standard-schema/sequoia) web component can point to this server for the full flow 30 + 31 + ### Routes 32 + 33 + | Route | Method | Description | 34 + |-------|--------|-------------| 35 + | `/api/health` | GET | Health check | 36 + | `/oauth/client-metadata.json` | GET | OAuth client metadata | 37 + | `/oauth/login?handle=` | GET | Start OAuth flow | 38 + | `/oauth/callback` | GET | OAuth callback | 39 + | `/oauth/logout` | POST | Revoke session | 40 + | `/oauth/status` | GET | Check auth status | 41 + | `/subscribe` | GET | Subscribe page (HTML) | 42 + | `/subscribe` | POST | Subscribe via API (JSON) | 43 + | `/subscribe/check` | GET | Check subscription status | 44 + | `/subscribe/login` | POST | Handle form submission | 45 + 46 + ## Configuration 47 + 48 + | Variable | Required | Default | Description | 49 + |----------|----------|---------|-------------| 50 + | `CLIENT_URL` | Yes | — | Public URL of this server (used for OAuth redirects) | 51 + | `CLIENT_NAME` | No | `Sequoia` | Name shown on Bluesky OAuth consent screen | 52 + | `PORT` | No | `3000` | Server port | 53 + | `REDIS_URL` | No | `redis://localhost:6379` | Redis connection URL | 54 + 55 + ### Theming 56 + 57 + The subscribe pages use CSS custom properties that can be overridden via environment variables: 58 + 59 + | Variable | Default | 60 + |----------|---------| 61 + | `THEME_ACCENT_COLOR` | `#3A5A40` | 62 + | `THEME_BG_COLOR` | `#F5F3EF` | 63 + | `THEME_FG_COLOR` | `#2C2C2C` | 64 + | `THEME_BORDER_COLOR` | `#D5D1C8` | 65 + | `THEME_ERROR_COLOR` | `#8B3A3A` | 66 + | `THEME_BORDER_RADIUS` | `6px` | 67 + | `THEME_FONT_FAMILY` | `system-ui, sans-serif` | 68 + | `THEME_DARK_BG_COLOR` | `#1A1A1A` | 69 + | `THEME_DARK_FG_COLOR` | `#E5E5E5` | 70 + | `THEME_DARK_BORDER_COLOR` | `#3A3A3A` | 71 + | `THEME_DARK_ERROR_COLOR` | `#E57373` | 72 + 73 + For full control, set `THEME_CSS_PATH` to a CSS file path (e.g. `/app/theme.css` mounted via Docker volume). It will be injected after the default styles. 74 + 75 + ## Deployment 76 + 77 + The included `Dockerfile` produces a minimal image: 78 + 79 + ```bash 80 + docker build -t sequoia-server . 81 + docker run -p 3000:3000 \ 82 + -e CLIENT_URL=https://your-domain.com \ 83 + -e REDIS_URL=redis://your-redis:6379 \ 84 + sequoia-server 85 + ``` 86 + 87 + Or use `docker-compose.yml` which bundles Redis: 88 + 89 + ```bash 90 + docker compose up -d 91 + ``` 92 + 93 + Place behind a reverse proxy (Caddy, nginx, Traefik) for TLS.
+32
packages/server/docker-compose.yml
··· 1 + services: 2 + server: 3 + build: . 4 + ports: 5 + - "${PORT:-3000}:${PORT:-3000}" 6 + environment: 7 + - CLIENT_URL=${CLIENT_URL} 8 + - CLIENT_NAME=${CLIENT_NAME:-Sequoia} 9 + - PORT=${PORT:-3000} 10 + - REDIS_URL=redis://redis:6379 11 + - THEME_ACCENT_COLOR=${THEME_ACCENT_COLOR:-} 12 + - THEME_BG_COLOR=${THEME_BG_COLOR:-} 13 + - THEME_FG_COLOR=${THEME_FG_COLOR:-} 14 + - THEME_BORDER_COLOR=${THEME_BORDER_COLOR:-} 15 + - THEME_ERROR_COLOR=${THEME_ERROR_COLOR:-} 16 + - THEME_BORDER_RADIUS=${THEME_BORDER_RADIUS:-} 17 + - THEME_FONT_FAMILY=${THEME_FONT_FAMILY:-} 18 + - THEME_DARK_BG_COLOR=${THEME_DARK_BG_COLOR:-} 19 + - THEME_DARK_FG_COLOR=${THEME_DARK_FG_COLOR:-} 20 + - THEME_DARK_BORDER_COLOR=${THEME_DARK_BORDER_COLOR:-} 21 + - THEME_DARK_ERROR_COLOR=${THEME_DARK_ERROR_COLOR:-} 22 + - THEME_CSS_PATH=${THEME_CSS_PATH:-} 23 + depends_on: 24 + - redis 25 + 26 + redis: 27 + image: redis:7 28 + volumes: 29 + - redis-data:/data 30 + 31 + volumes: 32 + redis-data:
+17
packages/server/package.json
··· 1 + { 2 + "name": "sequoia-server", 3 + "version": "0.0.1", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "dev": "bun --watch src/index.ts", 8 + "start": "bun run src/index.ts" 9 + }, 10 + "dependencies": { 11 + "@atproto/api": "^0.13.21", 12 + "@atproto/jwk-jose": "^0.1.3", 13 + "@atproto/oauth-client": "^0.3.3", 14 + "@atproto-labs/handle-resolver": "^0.1.5", 15 + "hono": "^4.7.4" 16 + } 17 + }
+20
packages/server/src/env.ts
··· 1 + export interface Env { 2 + CLIENT_URL: string; 3 + CLIENT_NAME: string; 4 + PORT: number; 5 + REDIS_URL: string; 6 + } 7 + 8 + export function loadEnv(): Env { 9 + const CLIENT_URL = process.env.CLIENT_URL; 10 + if (!CLIENT_URL) { 11 + throw new Error("CLIENT_URL environment variable is required"); 12 + } 13 + 14 + return { 15 + CLIENT_URL: CLIENT_URL.replace(/\/+$/, ""), 16 + CLIENT_NAME: process.env.CLIENT_NAME || "Sequoia", 17 + PORT: Number(process.env.PORT) || 3000, 18 + REDIS_URL: process.env.REDIS_URL || "redis://localhost:6379", 19 + }; 20 + }
+52
packages/server/src/index.ts
··· 1 + import { Hono } from "hono"; 2 + import { cors } from "hono/cors"; 3 + import { RedisClient } from "bun"; 4 + import { loadEnv } from "./env"; 5 + import type { Env } from "./env"; 6 + import auth from "./routes/auth"; 7 + import subscribe from "./routes/subscribe"; 8 + 9 + const env = loadEnv(); 10 + 11 + const redis = new RedisClient(env.REDIS_URL); 12 + 13 + type Variables = { env: Env; redis: typeof redis }; 14 + 15 + const app = new Hono<{ Variables: Variables }>(); 16 + 17 + // Inject env and redis into all routes 18 + app.use("*", async (c, next) => { 19 + c.set("env", env); 20 + c.set("redis", redis); 21 + await next(); 22 + }); 23 + 24 + // Health check 25 + app.get("/api/health", (c) => c.json({ status: "ok" })); 26 + 27 + // OAuth routes 28 + app.route("/oauth", auth); 29 + 30 + // Subscribe routes with CORS 31 + app.use( 32 + "/subscribe/*", 33 + cors({ 34 + origin: (origin) => origin, 35 + credentials: true, 36 + }), 37 + ); 38 + app.use( 39 + "/subscribe", 40 + cors({ 41 + origin: (origin) => origin, 42 + credentials: true, 43 + }), 44 + ); 45 + app.route("/subscribe", subscribe); 46 + 47 + console.log(`Sequoia server listening on port ${env.PORT}`); 48 + 49 + export default { 50 + port: env.PORT, 51 + fetch: app.fetch, 52 + };
+53
packages/server/src/lib/oauth-client.ts
··· 1 + import { JoseKey } from "@atproto/jwk-jose"; 2 + import { OAuthClient } from "@atproto/oauth-client"; 3 + import { AtprotoDohHandleResolver } from "@atproto-labs/handle-resolver"; 4 + import type { RedisClient } from "bun"; 5 + import { createStateStore, createSessionStore } from "./redis-stores"; 6 + 7 + export const OAUTH_SCOPE = 8 + "atproto repo:site.standard.graph.subscription?action=create&action=delete"; 9 + 10 + export function createOAuthClient( 11 + redis: RedisClient, 12 + clientUrl: string, 13 + clientName = "Sequoia", 14 + ) { 15 + const clientId = `${clientUrl}/oauth/client-metadata.json`; 16 + const redirectUri = `${clientUrl}/oauth/callback`; 17 + 18 + const dohEndpoint = 19 + process.env.DOH_ENDPOINT || "https://cloudflare-dns.com/dns-query"; 20 + 21 + return new OAuthClient({ 22 + responseMode: "query", 23 + handleResolver: new AtprotoDohHandleResolver({ dohEndpoint }), 24 + clientMetadata: { 25 + client_id: clientId, 26 + client_name: clientName, 27 + client_uri: clientUrl, 28 + redirect_uris: [redirectUri], 29 + grant_types: ["authorization_code", "refresh_token"], 30 + response_types: ["code"], 31 + scope: OAUTH_SCOPE, 32 + token_endpoint_auth_method: "none", 33 + application_type: "web", 34 + dpop_bound_access_tokens: true, 35 + }, 36 + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- @atproto Key class mismatch across packages 37 + runtimeImplementation: { 38 + createKey: (algs: string[]) => JoseKey.generate(algs) as any, 39 + getRandomValues: (length: number) => 40 + crypto.getRandomValues(new Uint8Array(length)), 41 + digest: async (data: Uint8Array, { name }: { name: string }) => { 42 + const buf = await crypto.subtle.digest( 43 + name.replace("sha", "SHA-"), 44 + new Uint8Array(data), 45 + ); 46 + return new Uint8Array(buf); 47 + }, 48 + requestLock: <T>(_name: string, fn: () => T | PromiseLike<T>) => fn(), 49 + }, 50 + stateStore: createStateStore(redis), 51 + sessionStore: createSessionStore(redis), 52 + }); 53 + }
+77
packages/server/src/lib/redis-stores.ts
··· 1 + import { JoseKey } from "@atproto/jwk-jose"; 2 + import type { 3 + Key, 4 + InternalStateData, 5 + SessionStore, 6 + StateStore, 7 + } from "@atproto/oauth-client"; 8 + import { RedisClient } from "bun"; 9 + 10 + type SerializedStateData = Omit<InternalStateData, "dpopKey"> & { 11 + dpopJwk: Record<string, unknown>; 12 + }; 13 + 14 + type SerializedSession = Omit<Parameters<SessionStore["set"]>[1], "dpopKey"> & { 15 + dpopJwk: Record<string, unknown>; 16 + }; 17 + 18 + function serializeKey(key: Key): Record<string, unknown> { 19 + const jwk = key.privateJwk; 20 + if (!jwk) throw new Error("Private DPoP JWK is missing"); 21 + return jwk as Record<string, unknown>; 22 + } 23 + 24 + async function deserializeKey(jwk: Record<string, unknown>): Promise<Key> { 25 + return JoseKey.fromJWK(jwk) as unknown as Key; 26 + } 27 + 28 + export function createStateStore(redis: RedisClient, ttl = 600): StateStore { 29 + return { 30 + async set(key, { dpopKey, ...rest }) { 31 + const data: SerializedStateData = { 32 + ...rest, 33 + dpopJwk: serializeKey(dpopKey), 34 + }; 35 + const redisKey = `oauth_state:${key}`; 36 + await redis.set(redisKey, JSON.stringify(data)); 37 + await redis.expire(redisKey, ttl); 38 + }, 39 + async get(key) { 40 + const raw = await redis.get(`oauth_state:${key}`); 41 + if (!raw) return undefined; 42 + const { dpopJwk, ...rest }: SerializedStateData = JSON.parse(raw); 43 + const dpopKey = await deserializeKey(dpopJwk); 44 + return { ...rest, dpopKey }; 45 + }, 46 + async del(key) { 47 + await redis.del(`oauth_state:${key}`); 48 + }, 49 + }; 50 + } 51 + 52 + export function createSessionStore( 53 + redis: RedisClient, 54 + ttl = 60 * 60 * 24 * 14, 55 + ): SessionStore { 56 + return { 57 + async set(sub, { dpopKey, ...rest }) { 58 + const data: SerializedSession = { 59 + ...rest, 60 + dpopJwk: serializeKey(dpopKey), 61 + }; 62 + const redisKey = `oauth_session:${sub}`; 63 + await redis.set(redisKey, JSON.stringify(data)); 64 + await redis.expire(redisKey, ttl); 65 + }, 66 + async get(sub) { 67 + const raw = await redis.get(`oauth_session:${sub}`); 68 + if (!raw) return undefined; 69 + const { dpopJwk, ...rest }: SerializedSession = JSON.parse(raw); 70 + const dpopKey = await deserializeKey(dpopJwk); 71 + return { ...rest, dpopKey }; 72 + }, 73 + async del(sub) { 74 + await redis.del(`oauth_session:${sub}`); 75 + }, 76 + }; 77 + }
+76
packages/server/src/lib/session.ts
··· 1 + import type { Context } from "hono"; 2 + import { deleteCookie, getCookie, setCookie } from "hono/cookie"; 3 + 4 + const SESSION_COOKIE_NAME = "session_id"; 5 + const RETURN_TO_COOKIE_NAME = "login_return_to"; 6 + const SESSION_TTL = 60 * 60 * 24 * 14; // 14 days in seconds 7 + const RETURN_TO_TTL = 600; // 10 minutes in seconds 8 + 9 + function baseCookieOptions(clientUrl: string) { 10 + const isLocalhost = clientUrl.includes("localhost"); 11 + const hostname = new URL(clientUrl).hostname; 12 + return { 13 + httpOnly: true as const, 14 + sameSite: "Lax" as const, 15 + path: "/", 16 + ...(isLocalhost ? {} : { domain: `.${hostname}`, secure: true }), 17 + }; 18 + } 19 + 20 + /** 21 + * Get DID from session cookie 22 + */ 23 + export function getSessionDid(c: Context): string | null { 24 + const value = getCookie(c, SESSION_COOKIE_NAME); 25 + return value ? decodeURIComponent(value) : null; 26 + } 27 + 28 + /** 29 + * Set session cookie with the user's DID 30 + */ 31 + export function setSessionCookie( 32 + c: Context, 33 + did: string, 34 + clientUrl: string, 35 + ): void { 36 + setCookie(c, SESSION_COOKIE_NAME, encodeURIComponent(did), { 37 + ...baseCookieOptions(clientUrl), 38 + maxAge: SESSION_TTL, 39 + }); 40 + } 41 + 42 + /** 43 + * Clear session cookie 44 + */ 45 + export function clearSessionCookie(c: Context, clientUrl: string): void { 46 + deleteCookie(c, SESSION_COOKIE_NAME, baseCookieOptions(clientUrl)); 47 + } 48 + 49 + /** 50 + * Get the post-OAuth return-to URL from the short-lived cookie 51 + */ 52 + export function getReturnToCookie(c: Context): string | null { 53 + const value = getCookie(c, RETURN_TO_COOKIE_NAME); 54 + return value ? decodeURIComponent(value) : null; 55 + } 56 + 57 + /** 58 + * Set a short-lived cookie that redirects back after OAuth completes 59 + */ 60 + export function setReturnToCookie( 61 + c: Context, 62 + returnTo: string, 63 + clientUrl: string, 64 + ): void { 65 + setCookie(c, RETURN_TO_COOKIE_NAME, encodeURIComponent(returnTo), { 66 + ...baseCookieOptions(clientUrl), 67 + maxAge: RETURN_TO_TTL, 68 + }); 69 + } 70 + 71 + /** 72 + * Clear the return-to cookie 73 + */ 74 + export function clearReturnToCookie(c: Context, clientUrl: string): void { 75 + deleteCookie(c, RETURN_TO_COOKIE_NAME, baseCookieOptions(clientUrl)); 76 + }
+199
packages/server/src/lib/theme.ts
··· 1 + import { existsSync, readFileSync } from "fs"; 2 + 3 + interface ThemeVars { 4 + fgColor: string; 5 + bgColor: string; 6 + accentColor: string; 7 + borderColor: string; 8 + errorColor: string; 9 + borderRadius: string; 10 + fontFamily: string; 11 + darkBgColor: string; 12 + darkFgColor: string; 13 + darkBorderColor: string; 14 + darkErrorColor: string; 15 + } 16 + 17 + function getThemeVars(): ThemeVars { 18 + return { 19 + fgColor: process.env.THEME_FG_COLOR || "#2C2C2C", 20 + bgColor: process.env.THEME_BG_COLOR || "#F5F3EF", 21 + accentColor: process.env.THEME_ACCENT_COLOR || "#3A5A40", 22 + borderColor: process.env.THEME_BORDER_COLOR || "#D5D1C8", 23 + errorColor: process.env.THEME_ERROR_COLOR || "#8B3A3A", 24 + borderRadius: process.env.THEME_BORDER_RADIUS || "6px", 25 + fontFamily: process.env.THEME_FONT_FAMILY || "system-ui, sans-serif", 26 + darkBgColor: process.env.THEME_DARK_BG_COLOR || "#1A1A1A", 27 + darkFgColor: process.env.THEME_DARK_FG_COLOR || "#E5E5E5", 28 + darkBorderColor: process.env.THEME_DARK_BORDER_COLOR || "#3A3A3A", 29 + darkErrorColor: process.env.THEME_DARK_ERROR_COLOR || "#E57373", 30 + }; 31 + } 32 + 33 + function getCustomCss(): string { 34 + const cssPath = process.env.THEME_CSS_PATH; 35 + if (!cssPath) return ""; 36 + try { 37 + if (existsSync(cssPath)) { 38 + return readFileSync(cssPath, "utf-8"); 39 + } 40 + } catch { 41 + console.warn(`Failed to read custom CSS file: ${cssPath}`); 42 + } 43 + return ""; 44 + } 45 + 46 + export function generateStyleBlock(): string { 47 + const t = getThemeVars(); 48 + const customCss = getCustomCss(); 49 + 50 + return `<style> 51 + :root { 52 + --sequoia-fg-color: ${t.fgColor}; 53 + --sequoia-bg-color: ${t.bgColor}; 54 + --sequoia-accent-color: ${t.accentColor}; 55 + --sequoia-border-color: ${t.borderColor}; 56 + --sequoia-error-color: ${t.errorColor}; 57 + --sequoia-border-radius: ${t.borderRadius}; 58 + --sequoia-font-family: ${t.fontFamily}; 59 + } 60 + 61 + @media (prefers-color-scheme: dark) { 62 + :root { 63 + --sequoia-fg-color: ${t.darkFgColor}; 64 + --sequoia-bg-color: ${t.darkBgColor}; 65 + --sequoia-border-color: ${t.darkBorderColor}; 66 + --sequoia-error-color: ${t.darkErrorColor}; 67 + } 68 + } 69 + 70 + * { box-sizing: border-box; margin: 0; padding: 0; } 71 + 72 + body { 73 + font-family: var(--sequoia-font-family); 74 + background: var(--sequoia-bg-color); 75 + color: var(--sequoia-fg-color); 76 + line-height: 1.6; 77 + } 78 + 79 + .page-container { 80 + max-width: 480px; 81 + margin: 4rem auto; 82 + padding: 0 1.25rem; 83 + } 84 + 85 + h1 { 86 + font-size: 1.75rem; 87 + font-weight: 700; 88 + margin-bottom: 0.75rem; 89 + } 90 + 91 + p { margin-bottom: 1rem; } 92 + 93 + a { 94 + color: var(--sequoia-accent-color); 95 + text-decoration: underline; 96 + } 97 + 98 + a:hover { text-decoration: none; } 99 + 100 + form { display: flex; flex-direction: column; } 101 + 102 + input[type="text"] { 103 + padding: 0.5rem 0.75rem; 104 + border: 1px solid var(--sequoia-border-color); 105 + border-radius: var(--sequoia-border-radius); 106 + margin-bottom: 1.25rem; 107 + width: 100%; 108 + font-size: 1rem; 109 + font-family: inherit; 110 + background: var(--sequoia-bg-color); 111 + color: var(--sequoia-fg-color); 112 + } 113 + 114 + input[type="text"]:focus { 115 + border-color: var(--sequoia-accent-color); 116 + outline: 2px solid var(--sequoia-accent-color); 117 + outline-offset: 2px; 118 + } 119 + 120 + button { 121 + padding: 0.625rem 1.25rem; 122 + background: var(--sequoia-accent-color); 123 + color: #fff; 124 + border: none; 125 + border-radius: var(--sequoia-border-radius); 126 + font-size: 1rem; 127 + font-family: inherit; 128 + font-weight: 600; 129 + cursor: pointer; 130 + transition: opacity 0.15s; 131 + } 132 + 133 + button:hover { opacity: 0.9; } 134 + 135 + button:focus-visible { 136 + outline: 2px solid var(--sequoia-accent-color); 137 + outline-offset: 2px; 138 + } 139 + 140 + table { 141 + width: 100%; 142 + border-collapse: collapse; 143 + table-layout: fixed; 144 + margin-top: 1rem; 145 + } 146 + 147 + td { 148 + padding: 0.5rem 0.75rem; 149 + border-bottom: 1px solid var(--sequoia-border-color); 150 + vertical-align: top; 151 + } 152 + 153 + td:first-child { 154 + width: 7rem; 155 + font-weight: 600; 156 + } 157 + 158 + td:last-child { overflow: hidden; } 159 + 160 + td code { 161 + font-size: 0.85rem; 162 + word-break: break-all; 163 + } 164 + 165 + td div { 166 + overflow-x: auto; 167 + white-space: nowrap; 168 + } 169 + 170 + .error { color: var(--sequoia-error-color); } 171 + ${customCss ? `\n /* Custom CSS */\n ${customCss}` : ""} 172 + </style>`; 173 + } 174 + 175 + export function page(body: string, headExtra = ""): string { 176 + return `<!DOCTYPE html> 177 + <html lang="en"> 178 + <head> 179 + <meta charset="UTF-8" /> 180 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 181 + <title>Sequoia · Subscribe</title> 182 + ${generateStyleBlock()} 183 + ${headExtra} 184 + </head> 185 + <body> 186 + <div class="page-container"> 187 + ${body} 188 + </div> 189 + </body> 190 + </html>`; 191 + } 192 + 193 + export function escapeHtml(text: string): string { 194 + return text 195 + .replace(/&/g, "&amp;") 196 + .replace(/</g, "&lt;") 197 + .replace(/>/g, "&gt;") 198 + .replace(/"/g, "&quot;"); 199 + }
+156
packages/server/src/routes/auth.ts
··· 1 + import { Hono } from "hono"; 2 + import type { RedisClient } from "bun"; 3 + import { createOAuthClient, OAUTH_SCOPE } from "../lib/oauth-client"; 4 + import { 5 + getSessionDid, 6 + setSessionCookie, 7 + clearSessionCookie, 8 + getReturnToCookie, 9 + clearReturnToCookie, 10 + } from "../lib/session"; 11 + import type { Env } from "../env"; 12 + 13 + type Variables = { env: Env; redis: RedisClient }; 14 + 15 + const auth = new Hono<{ Variables: Variables }>(); 16 + 17 + // OAuth client metadata endpoint 18 + auth.get("/client-metadata.json", (c) => { 19 + const env = c.get("env"); 20 + const clientId = `${env.CLIENT_URL}/oauth/client-metadata.json`; 21 + const redirectUri = `${env.CLIENT_URL}/oauth/callback`; 22 + 23 + return c.json({ 24 + client_id: clientId, 25 + client_name: env.CLIENT_NAME, 26 + client_uri: env.CLIENT_URL, 27 + redirect_uris: [redirectUri], 28 + grant_types: ["authorization_code", "refresh_token"], 29 + response_types: ["code"], 30 + scope: OAUTH_SCOPE, 31 + token_endpoint_auth_method: "none", 32 + application_type: "web", 33 + dpop_bound_access_tokens: true, 34 + }); 35 + }); 36 + 37 + // Start OAuth login flow 38 + auth.get("/login", async (c) => { 39 + const env = c.get("env"); 40 + const redis = c.get("redis"); 41 + 42 + try { 43 + const handle = c.req.query("handle"); 44 + if (!handle) { 45 + return c.redirect(`${env.CLIENT_URL}/?error=missing_handle`); 46 + } 47 + 48 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 49 + const authUrl = await client.authorize(handle, { 50 + scope: OAUTH_SCOPE, 51 + }); 52 + 53 + return c.redirect(authUrl.toString()); 54 + } catch (error) { 55 + console.error("Login error:", error); 56 + return c.redirect(`${env.CLIENT_URL}/?error=login_failed`); 57 + } 58 + }); 59 + 60 + // OAuth callback handler 61 + auth.get("/callback", async (c) => { 62 + const env = c.get("env"); 63 + const redis = c.get("redis"); 64 + 65 + try { 66 + const params = new URLSearchParams(c.req.url.split("?")[1] || ""); 67 + 68 + if (params.get("error")) { 69 + const error = params.get("error"); 70 + console.error("OAuth error:", error, params.get("error_description")); 71 + return c.redirect( 72 + `${env.CLIENT_URL}/?error=${encodeURIComponent(error!)}`, 73 + ); 74 + } 75 + 76 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 77 + const { session } = await client.callback(params); 78 + 79 + // Resolve handle from DID 80 + let handle: string | undefined; 81 + try { 82 + const identity = await client.identityResolver.resolve(session.did); 83 + handle = identity.handle; 84 + } catch { 85 + // Handle resolution is best-effort 86 + } 87 + 88 + // Store handle in Redis alongside the session for quick lookup 89 + if (handle) { 90 + const key = `oauth_handle:${session.did}`; 91 + await redis.set(key, handle); 92 + await redis.expire(key, 60 * 60 * 24 * 14); 93 + } 94 + 95 + setSessionCookie(c, session.did, env.CLIENT_URL); 96 + 97 + // If a subscribe flow set a return URL before initiating OAuth, honor it 98 + const returnTo = getReturnToCookie(c); 99 + clearReturnToCookie(c, env.CLIENT_URL); 100 + 101 + return c.redirect(returnTo ?? `${env.CLIENT_URL}/`); 102 + } catch (error) { 103 + console.error("Callback error:", error); 104 + return c.redirect(`${env.CLIENT_URL}/?error=callback_failed`); 105 + } 106 + }); 107 + 108 + // Logout endpoint 109 + auth.post("/logout", async (c) => { 110 + const env = c.get("env"); 111 + const redis = c.get("redis"); 112 + const did = getSessionDid(c); 113 + 114 + if (did) { 115 + try { 116 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 117 + await client.revoke(did); 118 + } catch (error) { 119 + console.error("Revoke error:", error); 120 + } 121 + await redis.del(`oauth_handle:${did}`); 122 + } 123 + 124 + clearSessionCookie(c, env.CLIENT_URL); 125 + return c.json({ success: true }); 126 + }); 127 + 128 + // Check auth status 129 + auth.get("/status", async (c) => { 130 + const env = c.get("env"); 131 + const redis = c.get("redis"); 132 + const did = getSessionDid(c); 133 + 134 + if (!did) { 135 + return c.json({ authenticated: false }); 136 + } 137 + 138 + try { 139 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 140 + const session = await client.restore(did); 141 + 142 + const handle = await redis.get(`oauth_handle:${session.did}`); 143 + 144 + return c.json({ 145 + authenticated: true, 146 + did: session.did, 147 + handle: handle || undefined, 148 + }); 149 + } catch (error) { 150 + console.error("Session restore failed:", error); 151 + clearSessionCookie(c, env.CLIENT_URL); 152 + return c.json({ authenticated: false }); 153 + } 154 + }); 155 + 156 + export default auth;
+426
packages/server/src/routes/subscribe.ts
··· 1 + import { Agent } from "@atproto/api"; 2 + import { Hono } from "hono"; 3 + import type { RedisClient } from "bun"; 4 + import { createOAuthClient } from "../lib/oauth-client"; 5 + import { getSessionDid, setReturnToCookie } from "../lib/session"; 6 + import { page, escapeHtml } from "../lib/theme"; 7 + import type { Env } from "../env"; 8 + 9 + type Variables = { env: Env; redis: RedisClient }; 10 + 11 + const subscribe = new Hono<{ Variables: Variables }>(); 12 + 13 + const COLLECTION = "site.standard.graph.subscription"; 14 + const REDIRECT_DELAY_SECONDS = 5; 15 + 16 + // ============================================================================ 17 + // Helpers 18 + // ============================================================================ 19 + 20 + function withReturnToParam( 21 + returnTo: string | undefined, 22 + key: string, 23 + value: string, 24 + ): string | undefined { 25 + if (!returnTo) return undefined; 26 + try { 27 + const url = new URL(returnTo); 28 + url.searchParams.set(key, value); 29 + return url.toString(); 30 + } catch { 31 + return returnTo; 32 + } 33 + } 34 + 35 + async function findExistingSubscription( 36 + agent: Agent, 37 + did: string, 38 + publicationUri: string, 39 + ): Promise<string | null> { 40 + let cursor: string | undefined; 41 + 42 + do { 43 + const result = await agent.com.atproto.repo.listRecords({ 44 + repo: did, 45 + collection: COLLECTION, 46 + limit: 100, 47 + cursor, 48 + }); 49 + 50 + for (const record of result.data.records) { 51 + const value = record.value as { publication?: string }; 52 + if (value.publication === publicationUri) { 53 + return record.uri; 54 + } 55 + } 56 + 57 + cursor = result.data.cursor; 58 + } while (cursor); 59 + 60 + return null; 61 + } 62 + 63 + // ============================================================================ 64 + // POST /subscribe 65 + // ============================================================================ 66 + 67 + subscribe.post("/", async (c) => { 68 + const env = c.get("env"); 69 + const redis = c.get("redis"); 70 + 71 + let publicationUri: string; 72 + try { 73 + const body = await c.req.json<{ publicationUri?: string }>(); 74 + publicationUri = body.publicationUri ?? ""; 75 + } catch { 76 + return c.json({ error: "Invalid JSON body" }, 400); 77 + } 78 + 79 + if (!publicationUri || !publicationUri.startsWith("at://")) { 80 + return c.json({ error: "Missing or invalid publicationUri" }, 400); 81 + } 82 + 83 + const did = getSessionDid(c); 84 + if (!did) { 85 + const subscribeUrl = `${env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}`; 86 + return c.json({ authenticated: false, subscribeUrl }, 401); 87 + } 88 + 89 + try { 90 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 91 + const session = await client.restore(did); 92 + const agent = new Agent(session); 93 + 94 + const existingUri = await findExistingSubscription( 95 + agent, 96 + did, 97 + publicationUri, 98 + ); 99 + if (existingUri) { 100 + return c.json({ 101 + subscribed: true, 102 + existing: true, 103 + recordUri: existingUri, 104 + }); 105 + } 106 + 107 + const result = await agent.com.atproto.repo.createRecord({ 108 + repo: did, 109 + collection: COLLECTION, 110 + record: { 111 + $type: COLLECTION, 112 + publication: publicationUri, 113 + }, 114 + }); 115 + 116 + return c.json({ 117 + subscribed: true, 118 + existing: false, 119 + recordUri: result.data.uri, 120 + }); 121 + } catch (error) { 122 + console.error("Subscribe POST error:", error); 123 + const subscribeUrl = `${env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}`; 124 + return c.json({ authenticated: false, subscribeUrl }, 401); 125 + } 126 + }); 127 + 128 + // ============================================================================ 129 + // GET /subscribe 130 + // ============================================================================ 131 + 132 + subscribe.get("/", async (c) => { 133 + const env = c.get("env"); 134 + const redis = c.get("redis"); 135 + 136 + const publicationUri = c.req.query("publicationUri"); 137 + const action = c.req.query("action"); 138 + 139 + if (action && action !== "unsubscribe") { 140 + return c.html(renderError(`Unsupported action: ${action}`), 400); 141 + } 142 + 143 + if (!publicationUri || !publicationUri.startsWith("at://")) { 144 + return c.html(renderError("Missing or invalid publication URI."), 400); 145 + } 146 + 147 + const referer = c.req.header("referer"); 148 + const returnTo = 149 + c.req.query("returnTo") ?? 150 + (referer && !referer.includes("/subscribe") ? referer : undefined); 151 + 152 + const did = getSessionDid(c); 153 + if (!did) { 154 + return c.html( 155 + renderHandleForm(publicationUri, returnTo, undefined, action), 156 + ); 157 + } 158 + 159 + try { 160 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 161 + const session = await client.restore(did); 162 + const agent = new Agent(session); 163 + 164 + if (action === "unsubscribe") { 165 + const existingUri = await findExistingSubscription( 166 + agent, 167 + did, 168 + publicationUri, 169 + ); 170 + if (existingUri) { 171 + const rkey = existingUri.split("/").pop()!; 172 + await agent.com.atproto.repo.deleteRecord({ 173 + repo: did, 174 + collection: COLLECTION, 175 + rkey, 176 + }); 177 + } 178 + 179 + let cleanReturnTo = returnTo; 180 + if (cleanReturnTo) { 181 + try { 182 + const rtUrl = new URL(cleanReturnTo); 183 + rtUrl.searchParams.delete("sequoia_did"); 184 + cleanReturnTo = rtUrl.toString(); 185 + } catch { 186 + // keep as-is 187 + } 188 + } 189 + 190 + return c.html( 191 + renderSuccess( 192 + publicationUri, 193 + null, 194 + "Unsubscribed", 195 + existingUri 196 + ? "You've successfully unsubscribed!" 197 + : "You weren't subscribed to this publication.", 198 + withReturnToParam(cleanReturnTo, "sequoia_unsubscribed", "1"), 199 + ), 200 + ); 201 + } 202 + 203 + const existingUri = await findExistingSubscription( 204 + agent, 205 + did, 206 + publicationUri, 207 + ); 208 + const returnToWithDid = withReturnToParam(returnTo, "sequoia_did", did); 209 + 210 + if (existingUri) { 211 + return c.html( 212 + renderSuccess( 213 + publicationUri, 214 + existingUri, 215 + "Subscribed", 216 + "You're already subscribed to this publication.", 217 + returnToWithDid, 218 + ), 219 + ); 220 + } 221 + 222 + const result = await agent.com.atproto.repo.createRecord({ 223 + repo: did, 224 + collection: COLLECTION, 225 + record: { 226 + $type: COLLECTION, 227 + publication: publicationUri, 228 + }, 229 + }); 230 + 231 + return c.html( 232 + renderSuccess( 233 + publicationUri, 234 + result.data.uri, 235 + "Subscribed", 236 + "You've successfully subscribed!", 237 + returnToWithDid, 238 + ), 239 + ); 240 + } catch (error) { 241 + console.error("Subscribe GET error:", error); 242 + return c.html( 243 + renderHandleForm( 244 + publicationUri, 245 + returnTo, 246 + "Session expired. Please sign in again.", 247 + action, 248 + ), 249 + ); 250 + } 251 + }); 252 + 253 + // ============================================================================ 254 + // GET /subscribe/check 255 + // ============================================================================ 256 + 257 + subscribe.get("/check", async (c) => { 258 + const env = c.get("env"); 259 + const redis = c.get("redis"); 260 + 261 + const publicationUri = c.req.query("publicationUri"); 262 + 263 + if (!publicationUri || !publicationUri.startsWith("at://")) { 264 + return c.json({ error: "Missing or invalid publicationUri" }, 400); 265 + } 266 + 267 + const did = getSessionDid(c) ?? c.req.query("did") ?? null; 268 + if (!did || !did.startsWith("did:")) { 269 + return c.json({ authenticated: false }, 401); 270 + } 271 + 272 + try { 273 + const client = createOAuthClient(redis, env.CLIENT_URL, env.CLIENT_NAME); 274 + const session = await client.restore(did); 275 + const agent = new Agent(session); 276 + const recordUri = await findExistingSubscription( 277 + agent, 278 + did, 279 + publicationUri, 280 + ); 281 + return recordUri 282 + ? c.json({ subscribed: true, recordUri }) 283 + : c.json({ subscribed: false }); 284 + } catch { 285 + return c.json({ authenticated: false }, 401); 286 + } 287 + }); 288 + 289 + // ============================================================================ 290 + // POST /subscribe/login 291 + // ============================================================================ 292 + 293 + subscribe.post("/login", async (c) => { 294 + const env = c.get("env"); 295 + 296 + const body = await c.req.parseBody(); 297 + const handle = (body["handle"] as string | undefined)?.trim(); 298 + const publicationUri = body["publicationUri"] as string | undefined; 299 + const formReturnTo = (body["returnTo"] as string | undefined) || undefined; 300 + const formAction = (body["action"] as string | undefined) || undefined; 301 + 302 + if (!handle || !publicationUri) { 303 + return c.html( 304 + renderError("Missing handle or publication URI."), 305 + 400, 306 + ); 307 + } 308 + 309 + const returnTo = 310 + `${env.CLIENT_URL}/subscribe?publicationUri=${encodeURIComponent(publicationUri)}` + 311 + (formAction ? `&action=${encodeURIComponent(formAction)}` : "") + 312 + (formReturnTo ? `&returnTo=${encodeURIComponent(formReturnTo)}` : ""); 313 + setReturnToCookie(c, returnTo, env.CLIENT_URL); 314 + 315 + return c.redirect( 316 + `${env.CLIENT_URL}/oauth/login?handle=${encodeURIComponent(handle)}`, 317 + ); 318 + }); 319 + 320 + // ============================================================================ 321 + // HTML rendering 322 + // ============================================================================ 323 + 324 + function renderHandleForm( 325 + publicationUri: string, 326 + returnTo?: string, 327 + error?: string, 328 + action?: string, 329 + ): string { 330 + const errorHtml = error 331 + ? `<p class="error">${escapeHtml(error)}</p>` 332 + : ""; 333 + const returnToInput = returnTo 334 + ? `<input type="hidden" name="returnTo" value="${escapeHtml(returnTo)}" />` 335 + : ""; 336 + const actionInput = action 337 + ? `<input type="hidden" name="action" value="${escapeHtml(action)}" />` 338 + : ""; 339 + 340 + return page(` 341 + <h1>Subscribe on Bluesky</h1> 342 + <p>Enter your Bluesky handle to subscribe to this publication.</p> 343 + ${errorHtml} 344 + <form method="POST" action="/subscribe/login"> 345 + <input type="hidden" name="publicationUri" value="${escapeHtml(publicationUri)}" /> 346 + ${returnToInput} 347 + ${actionInput} 348 + <input 349 + type="text" 350 + name="handle" 351 + placeholder="you.bsky.social" 352 + autocomplete="username" 353 + required 354 + autofocus 355 + /> 356 + <button type="submit">Continue on Bluesky</button> 357 + </form> 358 + `); 359 + } 360 + 361 + function renderSuccess( 362 + publicationUri: string, 363 + recordUri: string | null, 364 + heading: string, 365 + msg: string, 366 + returnTo?: string, 367 + ): string { 368 + const escapedPublicationUri = escapeHtml(publicationUri); 369 + const escapedReturnTo = returnTo ? escapeHtml(returnTo) : ""; 370 + 371 + const redirectHtml = returnTo 372 + ? `<p id="redirect-msg">Redirecting to <a href="${escapedReturnTo}">${escapedReturnTo}</a> in <span id="countdown">${REDIRECT_DELAY_SECONDS}</span>\u00a0seconds\u2026</p> 373 + <script> 374 + (function(){ 375 + var secs = ${REDIRECT_DELAY_SECONDS}; 376 + var el = document.getElementById('countdown'); 377 + var iv = setInterval(function(){ 378 + secs--; 379 + if (el) el.textContent = String(secs); 380 + if (secs <= 0) { clearInterval(iv); location.href = ${JSON.stringify(returnTo)}; } 381 + }, 1000); 382 + })(); 383 + </script>` 384 + : ""; 385 + const headExtra = returnTo 386 + ? `<meta http-equiv="refresh" content="${REDIRECT_DELAY_SECONDS};url=${escapedReturnTo}" />` 387 + : ""; 388 + 389 + return page( 390 + ` 391 + <h1>${escapeHtml(heading)}</h1> 392 + <p>${msg}</p> 393 + ${redirectHtml} 394 + <table> 395 + <colgroup><col style="width:7rem;"><col></colgroup> 396 + <tbody> 397 + <tr> 398 + <td>Publication</td> 399 + <td> 400 + <div><code><a href="https://pds.ls/${escapedPublicationUri}">${escapedPublicationUri}</a></code></div> 401 + </td> 402 + </tr> 403 + ${ 404 + recordUri 405 + ? `<tr> 406 + <td>Record</td> 407 + <td> 408 + <div><code><a href="https://pds.ls/${escapeHtml(recordUri)}">${escapeHtml(recordUri)}</a></code></div> 409 + </td> 410 + </tr>` 411 + : "" 412 + } 413 + </tbody> 414 + </table> 415 + `, 416 + headExtra, 417 + ); 418 + } 419 + 420 + function renderError(message: string): string { 421 + return page( 422 + `<h1>Error</h1><p class="error">${escapeHtml(message)}</p>`, 423 + ); 424 + } 425 + 426 + export default subscribe;
+13
packages/server/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ESNext", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "strict": true, 7 + "esModuleInterop": true, 8 + "skipLibCheck": true, 9 + "outDir": "dist", 10 + "rootDir": "src" 11 + }, 12 + "include": ["src"] 13 + }