feat: added initial server package ec148ac7
Steve · 2026-03-04 22:15 16 file(s) · +1374 −16
bun.lock +122 −15
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
}
package.json +3 −1
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",
packages/server/.env.example (added) +20 −0
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
packages/server/Dockerfile (added) +15 −0
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"]
packages/server/README.md (added) +93 −0
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.
packages/server/docker-compose.yml (added) +32 −0
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:
packages/server/package.json (added) +17 −0
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 +
}
packages/server/src/env.ts (added) +20 −0
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 +
}
packages/server/src/index.ts (added) +52 −0
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 +
};
packages/server/src/lib/oauth-client.ts (added) +53 −0
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 +
}
packages/server/src/lib/redis-stores.ts (added) +77 −0
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 +
}
packages/server/src/lib/session.ts (added) +76 −0
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 +
}
packages/server/src/lib/theme.ts (added) +199 −0
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 +
}
packages/server/src/routes/auth.ts (added) +156 −0
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;
packages/server/src/routes/subscribe.ts (added) +426 −0
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;
packages/server/tsconfig.json (added) +13 −0
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 +
}