an attempt to make a lightweight, easily self-hostable, scoped bluesky appview

Compare changes

Choose any two refs to compare.

-12
.env
··· 1 - # main indexers 2 - JETSTREAM_URL="wss://jetstream.whey.party" 3 - SPACEDUST_URL="wss://spacedust.whey.party" 4 - 5 - # for backfill (useless if you just started the instance right now) 6 - CONSTELLATION_URL="https://constellation.microcosm.blue" 7 - # i dont actually know why i need this 8 - SLINGSHOT_URL="https://slingshot.whey.party" 9 - 10 - SERVICE_DID="did:web:local3768forumtest.whey.party" 11 - SERVICE_ENDPOINT="https://local3768forumtest.whey.party" 12 - SERVER_PORT="3768"
+8
.gitignore
··· 1 1 whatever.db 2 + system.db 2 3 **/.DS_Store 4 + docs/.vite/ 5 + .gitignore 6 + indexserver.ts 7 + dbs/ 8 + config.jsonc 9 + config.json 10 + cache/
+35
config.jsonc.example
··· 1 + { 2 + // Main indexers 3 + "jetstream": "wss://jetstream1.us-east.bsky.network", // you can self host it -> https://github.com/bluesky-social/jetstream 4 + "spacedust": "wss://spacedust.your.site", // you can self host it -> https://www.microcosm.blue 5 + 6 + // For backfill (optional) 7 + "constellation": "https://constellation.microcosm.blue", // (not useful on a new setup โ€” requires pre-existing data to backfill) 8 + 9 + // Utility services 10 + "slingshot": "https://slingshot.your.site", // you can self host it -> https://www.microcosm.blue 11 + 12 + // Index Server config 13 + "indexServer": { 14 + "inviteOnly": true, 15 + "port": 3767, 16 + "did": "did:web:skyliteindexserver.your.site", // should be the same domain as the endpoint 17 + "host": "https://skyliteindexserver.your.site" 18 + }, 19 + 20 + // View Server config 21 + "viewServer": { 22 + "inviteOnly": true, 23 + "port": 3768, 24 + "did": "did:web:skyliteviewserver.your.site", // should be the same domain as the endpoint 25 + "host": "https://skyliteviewserver.your.site", 26 + 27 + // In order of which skylite index servers or bsky appviews to use first 28 + "indexPriority": [ 29 + "user#skylite_index", // user resolved skylite index server 30 + "did:web:backupindexserver.your.site#skylite_index", // a specific skylite index server 31 + "user#bsky_appview", // user resolved bsky appview 32 + "did:web:api.bsky.app#bsky_appview" // a specific bsky appview 33 + ] 34 + } 35 + }
+45
config.ts
··· 1 + import { parse } from "jsr:@std/jsonc"; 2 + import * as z from "npm:zod"; 3 + 4 + // configure these from the config.jsonc file (you can use config.jsonc.example as reference) 5 + export const indexTarget = z.string().refine( 6 + (val) => { 7 + const parts = val.split("#"); 8 + if (parts.length !== 2) return false; 9 + 10 + const [prefix, suffix] = parts; 11 + const validPrefix = prefix === "user" || prefix.startsWith("did:web:"); 12 + const validSuffix = suffix === "skylite_index" || suffix === "bsky_appview"; 13 + 14 + return validPrefix && validSuffix; 15 + }, 16 + { 17 + message: 18 + "Each indexPriority entry must be in the form 'user#skylite_index', 'user#bsky_appview', 'did:web:...#skylite_index', or 'did:web:...#bsky_appview'", 19 + } 20 + ); 21 + 22 + const ConfigSchema = z.object({ 23 + jetstream: z.string(), 24 + spacedust: z.string(), 25 + constellation: z.string(), 26 + slingshot: z.string(), 27 + indexServer: z.object({ 28 + inviteOnly: z.boolean(), 29 + port: z.number(), 30 + did: z.string(), 31 + host: z.string(), 32 + }), 33 + viewServer: z.object({ 34 + inviteOnly: z.boolean(), 35 + port: z.number(), 36 + did: z.string(), 37 + host: z.string(), 38 + indexPriority: z.array(indexTarget), 39 + }), 40 + }); 41 + 42 + const raw = await Deno.readTextFile("config.jsonc"); 43 + const config = ConfigSchema.parse(parse(raw)); 44 + 45 + export { config };
+2 -1
deno.json
··· 1 1 { 2 2 "tasks": { 3 - "dev": "deno run --watch -A --env-file main.ts" 3 + "index": "deno run --watch -A --env-file --unstable-broadcast-channel main-index.ts", 4 + "view": "deno run --watch -A --env-file --unstable-broadcast-channel main-view.ts" 4 5 }, 5 6 "imports": { 6 7 "@std/assert": "jsr:@std/assert@1"
+1072 -329
deno.lock
··· 3 3 "specifiers": { 4 4 "jsr:@db/sqlite@0.11": "0.11.1", 5 5 "jsr:@denosaurs/plug@1": "1.1.0", 6 - "jsr:@noble/secp256k1@*": "2.3.0", 7 - "jsr:@panva/jose@*": "6.0.12", 8 6 "jsr:@std/assert@0.217": "0.217.0", 9 7 "jsr:@std/encoding@1": "1.0.10", 10 8 "jsr:@std/fmt@1": "1.0.8", 11 9 "jsr:@std/fs@1": "1.0.19", 12 10 "jsr:@std/internal@^1.0.9": "1.0.10", 11 + "jsr:@std/jsonc@*": "1.0.2", 13 12 "jsr:@std/path@0.217": "0.217.0", 14 13 "jsr:@std/path@1": "1.1.1", 15 14 "jsr:@std/path@^1.1.1": "1.1.1", 16 - "npm:@atproto/api@*": "0.15.12", 15 + "npm:@atproto/api@*": "0.16.2", 17 16 "npm:@atproto/identity@*": "0.4.8", 18 - "npm:@atproto/lexicon@*": "0.4.11", 17 + "npm:@atproto/lexicon@*": "0.4.12", 19 18 "npm:@atproto/xrpc-server@*": "0.9.1", 20 - "npm:@ipld/car@*": "5.4.2", 21 - "npm:@ipld/dag-cbor@*": "9.2.4", 22 - "npm:@types/express@4.17.15": "4.17.15", 23 - "npm:@types/node@*": "22.15.15", 19 + "npm:@atproto/xrpc@*": "0.7.1", 20 + "npm:@babel/core@*": "7.28.3", 21 + "npm:babel-plugin-react-compiler@*": "19.1.0-rc.2", 24 22 "npm:did-jwt@*": "8.0.17", 25 23 "npm:did-resolver@*": "4.1.0", 26 - "npm:express@*": "5.1.0", 24 + "npm:esbuild-plugin-cache@*": "0.2.10", 25 + "npm:esbuild@0.20.2": "0.20.2", 27 26 "npm:ky@*": "1.8.1", 28 27 "npm:multiformats@*": "13.4.0", 29 28 "npm:quick-lru@*": "7.0.1", 30 - "npm:web-did-resolver@*": "2.0.30" 29 + "npm:web-did-resolver@*": "2.0.30", 30 + "npm:zod@*": "4.0.17" 31 31 }, 32 32 "jsr": { 33 33 "@db/sqlite@0.11.1": { ··· 46 46 "jsr:@std/path@1" 47 47 ] 48 48 }, 49 - "@noble/secp256k1@2.3.0": { 50 - "integrity": "63eb4479a7c548e0ddea1cb5ea9bf7efacafe495e7f1c07bce1b4108df95967f" 51 - }, 52 - "@panva/jose@6.0.12": { 53 - "integrity": "b228cf79558ccc979046855bb140cd5d7bd86564dc047e563632a686b1346dcc" 54 - }, 55 49 "@std/assert@0.217.0": { 56 50 "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" 57 51 }, ··· 71 65 "@std/internal@1.0.10": { 72 66 "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" 73 67 }, 68 + "@std/jsonc@1.0.2": { 69 + "integrity": "909605dae3af22bd75b1cbda8d64a32cf1fd2cf6efa3f9e224aba6d22c0f44c7" 70 + }, 74 71 "@std/path@0.217.0": { 75 72 "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", 76 73 "dependencies": [ ··· 85 82 } 86 83 }, 87 84 "npm": { 88 - "@atproto/api@0.15.12": { 89 - "integrity": "sha512-51IHenZMA+Ekfe2OlZL/mTFqvZQU93jI4xsLvTFhGc4tSQYCHV9r/AJTANPZLFrhm9GfWZ0n90r/9IQl9eicjg==", 85 + "@ampproject/remapping@2.3.0": { 86 + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", 87 + "dependencies": [ 88 + "@jridgewell/gen-mapping", 89 + "@jridgewell/trace-mapping" 90 + ] 91 + }, 92 + "@atproto/api@0.16.2": { 93 + "integrity": "sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==", 90 94 "dependencies": [ 91 95 "@atproto/common-web", 92 - "@atproto/lexicon@0.4.11", 96 + "@atproto/lexicon", 93 97 "@atproto/syntax", 94 - "@atproto/xrpc@0.7.0", 98 + "@atproto/xrpc", 95 99 "await-lock", 96 100 "multiformats@9.9.0", 97 101 "tlds", 98 - "zod" 102 + "zod@3.25.76" 99 103 ] 100 104 }, 101 105 "@atproto/common-web@0.4.2": { ··· 104 108 "graphemer", 105 109 "multiformats@9.9.0", 106 110 "uint8arrays@3.0.0", 107 - "zod" 111 + "zod@3.25.76" 108 112 ] 109 113 }, 110 114 "@atproto/common@0.4.11": { 111 115 "integrity": "sha512-Knv0viYXNMfCdIE7jLUiWJKnnMfEwg+vz2epJQi8WOjqtqCFb3W/3Jn72ZiuovIfpdm13MaOiny6w2NErUQC6g==", 112 116 "dependencies": [ 113 117 "@atproto/common-web", 114 - "@ipld/dag-cbor@7.0.3", 118 + "@ipld/dag-cbor", 115 119 "cbor-x", 116 120 "iso-datestring-validator", 117 121 "multiformats@9.9.0", ··· 133 137 "@atproto/crypto" 134 138 ] 135 139 }, 136 - "@atproto/lexicon@0.4.11": { 137 - "integrity": "sha512-btefdnvNz2Ao2I+qbmj0F06HC8IlrM/IBz6qOBS50r0S6uDf5tOO+Mv2tSVdimFkdzyDdLtBI1sV36ONxz2cOw==", 138 - "dependencies": [ 139 - "@atproto/common-web", 140 - "@atproto/syntax", 141 - "iso-datestring-validator", 142 - "multiformats@9.9.0", 143 - "zod" 144 - ] 145 - }, 146 140 "@atproto/lexicon@0.4.12": { 147 141 "integrity": "sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==", 148 142 "dependencies": [ ··· 150 144 "@atproto/syntax", 151 145 "iso-datestring-validator", 152 146 "multiformats@9.9.0", 153 - "zod" 147 + "zod@3.25.76" 154 148 ] 155 149 }, 156 150 "@atproto/syntax@0.4.0": { ··· 161 155 "dependencies": [ 162 156 "@atproto/common", 163 157 "@atproto/crypto", 164 - "@atproto/lexicon@0.4.12", 165 - "@atproto/xrpc@0.7.1", 158 + "@atproto/lexicon", 159 + "@atproto/xrpc", 166 160 "cbor-x", 167 - "express@4.21.2", 161 + "express", 168 162 "http-errors", 169 - "mime-types@2.1.35", 163 + "mime-types", 170 164 "rate-limiter-flexible", 171 165 "uint8arrays@3.0.0", 172 166 "ws", 173 - "zod" 167 + "zod@3.25.76" 168 + ] 169 + }, 170 + "@atproto/xrpc@0.7.1": { 171 + "integrity": "sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==", 172 + "dependencies": [ 173 + "@atproto/lexicon", 174 + "zod@3.25.76" 175 + ] 176 + }, 177 + "@babel/code-frame@7.27.1": { 178 + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", 179 + "dependencies": [ 180 + "@babel/helper-validator-identifier", 181 + "js-tokens", 182 + "picocolors" 183 + ] 184 + }, 185 + "@babel/compat-data@7.28.0": { 186 + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==" 187 + }, 188 + "@babel/core@7.28.3": { 189 + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", 190 + "dependencies": [ 191 + "@ampproject/remapping", 192 + "@babel/code-frame", 193 + "@babel/generator", 194 + "@babel/helper-compilation-targets", 195 + "@babel/helper-module-transforms", 196 + "@babel/helpers", 197 + "@babel/parser", 198 + "@babel/template", 199 + "@babel/traverse", 200 + "@babel/types", 201 + "convert-source-map", 202 + "debug@4.4.1", 203 + "gensync", 204 + "json5", 205 + "semver" 206 + ] 207 + }, 208 + "@babel/generator@7.28.3": { 209 + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", 210 + "dependencies": [ 211 + "@babel/parser", 212 + "@babel/types", 213 + "@jridgewell/gen-mapping", 214 + "@jridgewell/trace-mapping", 215 + "jsesc" 216 + ] 217 + }, 218 + "@babel/helper-compilation-targets@7.27.2": { 219 + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", 220 + "dependencies": [ 221 + "@babel/compat-data", 222 + "@babel/helper-validator-option", 223 + "browserslist", 224 + "lru-cache", 225 + "semver" 226 + ] 227 + }, 228 + "@babel/helper-globals@7.28.0": { 229 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" 230 + }, 231 + "@babel/helper-module-imports@7.27.1": { 232 + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", 233 + "dependencies": [ 234 + "@babel/traverse", 235 + "@babel/types" 236 + ] 237 + }, 238 + "@babel/helper-module-transforms@7.28.3_@babel+core@7.28.3": { 239 + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", 240 + "dependencies": [ 241 + "@babel/core", 242 + "@babel/helper-module-imports", 243 + "@babel/helper-validator-identifier", 244 + "@babel/traverse" 245 + ] 246 + }, 247 + "@babel/helper-string-parser@7.27.1": { 248 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" 249 + }, 250 + "@babel/helper-validator-identifier@7.27.1": { 251 + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" 252 + }, 253 + "@babel/helper-validator-option@7.27.1": { 254 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" 255 + }, 256 + "@babel/helpers@7.28.3": { 257 + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", 258 + "dependencies": [ 259 + "@babel/template", 260 + "@babel/types" 261 + ] 262 + }, 263 + "@babel/parser@7.28.3": { 264 + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", 265 + "dependencies": [ 266 + "@babel/types" 267 + ], 268 + "bin": true 269 + }, 270 + "@babel/template@7.27.2": { 271 + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", 272 + "dependencies": [ 273 + "@babel/code-frame", 274 + "@babel/parser", 275 + "@babel/types" 174 276 ] 175 277 }, 176 - "@atproto/xrpc@0.7.0": { 177 - "integrity": "sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw==", 278 + "@babel/traverse@7.28.3": { 279 + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", 178 280 "dependencies": [ 179 - "@atproto/lexicon@0.4.11", 180 - "zod" 281 + "@babel/code-frame", 282 + "@babel/generator", 283 + "@babel/helper-globals", 284 + "@babel/parser", 285 + "@babel/template", 286 + "@babel/types", 287 + "debug@4.4.1" 181 288 ] 182 289 }, 183 - "@atproto/xrpc@0.7.1": { 184 - "integrity": "sha512-ANHEzlskYlMEdH18m+Itp3a8d0pEJao2qoDybDoMupTnoeNkya4VKIaOgAi6ERQnqatBBZyn9asW+7rJmSt/8g==", 290 + "@babel/types@7.28.2": { 291 + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", 185 292 "dependencies": [ 186 - "@atproto/lexicon@0.4.12", 187 - "zod" 293 + "@babel/helper-string-parser", 294 + "@babel/helper-validator-identifier" 188 295 ] 189 296 }, 190 297 "@cbor-extract/cbor-extract-darwin-arm64@2.2.0": { ··· 217 324 "os": ["win32"], 218 325 "cpu": ["x64"] 219 326 }, 220 - "@ipld/car@5.4.2": { 221 - "integrity": "sha512-gfyrJvePyXnh2Fbj8mPg4JYvEZ3izhk8C9WgAle7xIYbrJNSXmNQ6BxAls8Gof97vvGbCROdxbTWRmHJtTCbcg==", 222 - "dependencies": [ 223 - "@ipld/dag-cbor@9.2.4", 224 - "cborg@4.2.11", 225 - "multiformats@13.4.0", 226 - "varint" 227 - ] 327 + "@esbuild/aix-ppc64@0.20.2": { 328 + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", 329 + "os": ["aix"], 330 + "cpu": ["ppc64"] 331 + }, 332 + "@esbuild/android-arm64@0.20.2": { 333 + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", 334 + "os": ["android"], 335 + "cpu": ["arm64"] 336 + }, 337 + "@esbuild/android-arm@0.20.2": { 338 + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", 339 + "os": ["android"], 340 + "cpu": ["arm"] 341 + }, 342 + "@esbuild/android-x64@0.20.2": { 343 + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", 344 + "os": ["android"], 345 + "cpu": ["x64"] 346 + }, 347 + "@esbuild/darwin-arm64@0.20.2": { 348 + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", 349 + "os": ["darwin"], 350 + "cpu": ["arm64"] 351 + }, 352 + "@esbuild/darwin-x64@0.20.2": { 353 + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", 354 + "os": ["darwin"], 355 + "cpu": ["x64"] 356 + }, 357 + "@esbuild/freebsd-arm64@0.20.2": { 358 + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", 359 + "os": ["freebsd"], 360 + "cpu": ["arm64"] 361 + }, 362 + "@esbuild/freebsd-x64@0.20.2": { 363 + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", 364 + "os": ["freebsd"], 365 + "cpu": ["x64"] 366 + }, 367 + "@esbuild/linux-arm64@0.20.2": { 368 + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", 369 + "os": ["linux"], 370 + "cpu": ["arm64"] 371 + }, 372 + "@esbuild/linux-arm@0.20.2": { 373 + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", 374 + "os": ["linux"], 375 + "cpu": ["arm"] 376 + }, 377 + "@esbuild/linux-ia32@0.20.2": { 378 + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", 379 + "os": ["linux"], 380 + "cpu": ["ia32"] 381 + }, 382 + "@esbuild/linux-loong64@0.20.2": { 383 + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", 384 + "os": ["linux"], 385 + "cpu": ["loong64"] 386 + }, 387 + "@esbuild/linux-mips64el@0.20.2": { 388 + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", 389 + "os": ["linux"], 390 + "cpu": ["mips64el"] 391 + }, 392 + "@esbuild/linux-ppc64@0.20.2": { 393 + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", 394 + "os": ["linux"], 395 + "cpu": ["ppc64"] 396 + }, 397 + "@esbuild/linux-riscv64@0.20.2": { 398 + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", 399 + "os": ["linux"], 400 + "cpu": ["riscv64"] 401 + }, 402 + "@esbuild/linux-s390x@0.20.2": { 403 + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", 404 + "os": ["linux"], 405 + "cpu": ["s390x"] 406 + }, 407 + "@esbuild/linux-x64@0.20.2": { 408 + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", 409 + "os": ["linux"], 410 + "cpu": ["x64"] 411 + }, 412 + "@esbuild/netbsd-x64@0.20.2": { 413 + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", 414 + "os": ["netbsd"], 415 + "cpu": ["x64"] 416 + }, 417 + "@esbuild/openbsd-x64@0.20.2": { 418 + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", 419 + "os": ["openbsd"], 420 + "cpu": ["x64"] 421 + }, 422 + "@esbuild/sunos-x64@0.20.2": { 423 + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", 424 + "os": ["sunos"], 425 + "cpu": ["x64"] 426 + }, 427 + "@esbuild/win32-arm64@0.20.2": { 428 + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", 429 + "os": ["win32"], 430 + "cpu": ["arm64"] 431 + }, 432 + "@esbuild/win32-ia32@0.20.2": { 433 + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", 434 + "os": ["win32"], 435 + "cpu": ["ia32"] 436 + }, 437 + "@esbuild/win32-x64@0.20.2": { 438 + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", 439 + "os": ["win32"], 440 + "cpu": ["x64"] 228 441 }, 229 442 "@ipld/dag-cbor@7.0.3": { 230 443 "integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==", 231 444 "dependencies": [ 232 - "cborg@1.10.2", 445 + "cborg", 233 446 "multiformats@9.9.0" 234 447 ] 235 448 }, 236 - "@ipld/dag-cbor@9.2.4": { 237 - "integrity": "sha512-GbDWYl2fdJgkYtIJN0HY9oO0o50d1nB4EQb7uYWKUd2ztxCjxiEW3PjwGG0nqUpN1G4Cug6LX8NzbA7fKT+zfA==", 449 + "@jridgewell/gen-mapping@0.3.13": { 450 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 451 + "dependencies": [ 452 + "@jridgewell/sourcemap-codec", 453 + "@jridgewell/trace-mapping" 454 + ] 455 + }, 456 + "@jridgewell/resolve-uri@3.1.2": { 457 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" 458 + }, 459 + "@jridgewell/sourcemap-codec@1.5.5": { 460 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" 461 + }, 462 + "@jridgewell/trace-mapping@0.3.30": { 463 + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", 238 464 "dependencies": [ 239 - "cborg@4.2.11", 240 - "multiformats@13.4.0" 465 + "@jridgewell/resolve-uri", 466 + "@jridgewell/sourcemap-codec" 241 467 ] 242 468 }, 243 469 "@multiformats/base-x@4.0.1": { ··· 258 484 "@scure/base@1.2.6": { 259 485 "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==" 260 486 }, 261 - "@types/body-parser@1.19.6": { 262 - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", 487 + "@types/node-fetch@2.6.13": { 488 + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", 263 489 "dependencies": [ 264 - "@types/connect", 265 - "@types/node" 266 - ] 267 - }, 268 - "@types/connect@3.4.38": { 269 - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 270 - "dependencies": [ 271 - "@types/node" 272 - ] 273 - }, 274 - "@types/express-serve-static-core@4.19.6": { 275 - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", 276 - "dependencies": [ 277 - "@types/node", 278 - "@types/qs", 279 - "@types/range-parser", 280 - "@types/send" 281 - ] 282 - }, 283 - "@types/express@4.17.15": { 284 - "integrity": "sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ==", 285 - "dependencies": [ 286 - "@types/body-parser", 287 - "@types/express-serve-static-core", 288 - "@types/qs", 289 - "@types/serve-static" 490 + "@types/node@22.15.15", 491 + "form-data" 290 492 ] 291 493 }, 292 - "@types/http-errors@2.0.5": { 293 - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" 294 - }, 295 - "@types/mime@1.3.5": { 296 - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" 494 + "@types/node@14.18.63": { 495 + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" 297 496 }, 298 497 "@types/node@22.15.15": { 299 498 "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", ··· 301 500 "undici-types" 302 501 ] 303 502 }, 304 - "@types/qs@6.14.0": { 305 - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" 306 - }, 307 - "@types/range-parser@1.2.7": { 308 - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" 309 - }, 310 - "@types/send@0.17.5": { 311 - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", 312 - "dependencies": [ 313 - "@types/mime", 314 - "@types/node" 315 - ] 316 - }, 317 - "@types/serve-static@1.15.8": { 318 - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", 319 - "dependencies": [ 320 - "@types/http-errors", 321 - "@types/node", 322 - "@types/send" 323 - ] 324 - }, 325 503 "abort-controller@3.0.0": { 326 504 "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 327 505 "dependencies": [ ··· 331 509 "accepts@1.3.8": { 332 510 "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 333 511 "dependencies": [ 334 - "mime-types@2.1.35", 335 - "negotiator@0.6.3" 336 - ] 337 - }, 338 - "accepts@2.0.0": { 339 - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 340 - "dependencies": [ 341 - "mime-types@3.0.1", 342 - "negotiator@1.0.0" 512 + "mime-types", 513 + "negotiator" 343 514 ] 344 515 }, 345 516 "array-flatten@1.1.1": { 346 517 "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 347 518 }, 519 + "asynckit@0.4.0": { 520 + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 521 + }, 348 522 "atomic-sleep@1.0.0": { 349 523 "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" 350 524 }, 351 525 "await-lock@2.2.2": { 352 526 "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==" 353 527 }, 528 + "babel-plugin-react-compiler@19.1.0-rc.2": { 529 + "integrity": "sha512-kSNA//p5fMO6ypG8EkEVPIqAjwIXm5tMjfD1XRPL/sRjYSbJ6UsvORfaeolNWnZ9n310aM0xJP7peW26BuCVzA==", 530 + "dependencies": [ 531 + "@babel/types" 532 + ] 533 + }, 354 534 "base64-js@1.5.1": { 355 535 "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 356 536 }, ··· 363 543 "depd", 364 544 "destroy", 365 545 "http-errors", 366 - "iconv-lite@0.4.24", 546 + "iconv-lite", 367 547 "on-finished", 368 - "qs@6.13.0", 369 - "raw-body@2.5.2", 370 - "type-is@1.6.18", 548 + "qs", 549 + "raw-body", 550 + "type-is", 371 551 "unpipe" 372 552 ] 373 553 }, 374 - "body-parser@2.2.0": { 375 - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 554 + "browserslist@4.25.3": { 555 + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", 376 556 "dependencies": [ 377 - "bytes", 378 - "content-type", 379 - "debug@4.4.1", 380 - "http-errors", 381 - "iconv-lite@0.6.3", 382 - "on-finished", 383 - "qs@6.14.0", 384 - "raw-body@3.0.0", 385 - "type-is@2.0.1" 386 - ] 557 + "caniuse-lite", 558 + "electron-to-chromium", 559 + "node-releases", 560 + "update-browserslist-db" 561 + ], 562 + "bin": true 387 563 }, 388 564 "buffer@6.0.3": { 389 565 "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", ··· 409 585 "get-intrinsic" 410 586 ] 411 587 }, 588 + "caniuse-lite@1.0.30001737": { 589 + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==" 590 + }, 412 591 "canonicalize@2.1.0": { 413 592 "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", 414 593 "bin": true ··· 439 618 "integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==", 440 619 "bin": true 441 620 }, 442 - "cborg@4.2.11": { 443 - "integrity": "sha512-7gs3iaqtsD9OHowgqzc6ixQGwSBONqosVR2co0Bg0pARgrLap+LCcEIXJuuIz2jHy0WWQeDMFPEsU2r17I2XPQ==", 444 - "bin": true 445 - }, 446 - "content-disposition@0.5.4": { 447 - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 621 + "combined-stream@1.0.8": { 622 + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 448 623 "dependencies": [ 449 - "safe-buffer" 624 + "delayed-stream" 450 625 ] 451 626 }, 452 - "content-disposition@1.0.0": { 453 - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 627 + "content-disposition@0.5.4": { 628 + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 454 629 "dependencies": [ 455 630 "safe-buffer" 456 631 ] ··· 458 633 "content-type@1.0.5": { 459 634 "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" 460 635 }, 636 + "convert-source-map@2.0.0": { 637 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" 638 + }, 461 639 "cookie-signature@1.0.6": { 462 640 "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 463 641 }, 464 - "cookie-signature@1.2.2": { 465 - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" 466 - }, 467 642 "cookie@0.7.1": { 468 643 "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" 469 - }, 470 - "cookie@0.7.2": { 471 - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" 472 644 }, 473 645 "cross-fetch@4.1.0": { 474 646 "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", ··· 488 660 "ms@2.1.3" 489 661 ] 490 662 }, 663 + "delayed-stream@1.0.0": { 664 + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 665 + }, 666 + "deno-cache@0.2.12": { 667 + "integrity": "sha512-Jv8utRPQhsm+kx9ky0OdUnTWBLKGlFcBoLjQqrpuDd9zhuciCLPmklbz1YYfdaeM0dgp1nwRoqlHu5sH3vmJGQ==", 668 + "dependencies": [ 669 + "@types/node@14.18.63", 670 + "@types/node-fetch", 671 + "node-fetch" 672 + ] 673 + }, 674 + "deno-importmap@0.1.6": { 675 + "integrity": "sha512-nZ5ZA8qW5F0Yzq1VhRp1wARpWSfD0FQvI1IUHXbE3oROO6tcYomTIWSAZGzO4LGQl1hTG6UmhPNTP3d4uMXzMg==", 676 + "dependencies": [ 677 + "@types/node@14.18.63" 678 + ] 679 + }, 491 680 "depd@2.0.0": { 492 681 "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 493 682 }, ··· 525 714 "ee-first@1.1.1": { 526 715 "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 527 716 }, 717 + "electron-to-chromium@1.5.209": { 718 + "integrity": "sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==" 719 + }, 528 720 "encodeurl@1.0.2": { 529 721 "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 530 722 }, ··· 543 735 "es-errors" 544 736 ] 545 737 }, 738 + "es-set-tostringtag@2.1.0": { 739 + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 740 + "dependencies": [ 741 + "es-errors", 742 + "get-intrinsic", 743 + "has-tostringtag", 744 + "hasown" 745 + ] 746 + }, 747 + "esbuild-plugin-cache@0.2.10": { 748 + "integrity": "sha512-e2Z8TgorvVKuj2A8/VP+sC04rt47JTpaew+9uP4CN7106W/cQxY1cqC1KWm+szwpfzJbBqXOpAqN4JPRySbW+A==", 749 + "dependencies": [ 750 + "deno-cache", 751 + "deno-importmap" 752 + ] 753 + }, 754 + "esbuild@0.20.2": { 755 + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", 756 + "optionalDependencies": [ 757 + "@esbuild/aix-ppc64", 758 + "@esbuild/android-arm", 759 + "@esbuild/android-arm64", 760 + "@esbuild/android-x64", 761 + "@esbuild/darwin-arm64", 762 + "@esbuild/darwin-x64", 763 + "@esbuild/freebsd-arm64", 764 + "@esbuild/freebsd-x64", 765 + "@esbuild/linux-arm", 766 + "@esbuild/linux-arm64", 767 + "@esbuild/linux-ia32", 768 + "@esbuild/linux-loong64", 769 + "@esbuild/linux-mips64el", 770 + "@esbuild/linux-ppc64", 771 + "@esbuild/linux-riscv64", 772 + "@esbuild/linux-s390x", 773 + "@esbuild/linux-x64", 774 + "@esbuild/netbsd-x64", 775 + "@esbuild/openbsd-x64", 776 + "@esbuild/sunos-x64", 777 + "@esbuild/win32-arm64", 778 + "@esbuild/win32-ia32", 779 + "@esbuild/win32-x64" 780 + ], 781 + "scripts": true, 782 + "bin": true 783 + }, 784 + "escalade@3.2.0": { 785 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" 786 + }, 546 787 "escape-html@1.0.3": { 547 788 "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 548 789 }, ··· 558 799 "express@4.21.2": { 559 800 "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 560 801 "dependencies": [ 561 - "accepts@1.3.8", 802 + "accepts", 562 803 "array-flatten", 563 - "body-parser@1.20.3", 564 - "content-disposition@0.5.4", 804 + "body-parser", 805 + "content-disposition", 565 806 "content-type", 566 - "cookie@0.7.1", 567 - "cookie-signature@1.0.6", 807 + "cookie", 808 + "cookie-signature", 568 809 "debug@2.6.9", 569 810 "depd", 570 811 "encodeurl@2.0.0", 571 812 "escape-html", 572 813 "etag", 573 - "finalhandler@1.3.1", 574 - "fresh@0.5.2", 814 + "finalhandler", 815 + "fresh", 575 816 "http-errors", 576 - "merge-descriptors@1.0.3", 817 + "merge-descriptors", 577 818 "methods", 578 819 "on-finished", 579 820 "parseurl", 580 - "path-to-regexp@0.1.12", 821 + "path-to-regexp", 581 822 "proxy-addr", 582 - "qs@6.13.0", 823 + "qs", 583 824 "range-parser", 584 825 "safe-buffer", 585 - "send@0.19.0", 586 - "serve-static@1.16.2", 826 + "send", 827 + "serve-static", 587 828 "setprototypeof", 588 - "statuses@2.0.1", 589 - "type-is@1.6.18", 829 + "statuses", 830 + "type-is", 590 831 "utils-merge", 591 832 "vary" 592 833 ] 593 834 }, 594 - "express@5.1.0": { 595 - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 596 - "dependencies": [ 597 - "accepts@2.0.0", 598 - "body-parser@2.2.0", 599 - "content-disposition@1.0.0", 600 - "content-type", 601 - "cookie@0.7.2", 602 - "cookie-signature@1.2.2", 603 - "debug@4.4.1", 604 - "encodeurl@2.0.0", 605 - "escape-html", 606 - "etag", 607 - "finalhandler@2.1.0", 608 - "fresh@2.0.0", 609 - "http-errors", 610 - "merge-descriptors@2.0.0", 611 - "mime-types@3.0.1", 612 - "on-finished", 613 - "once", 614 - "parseurl", 615 - "proxy-addr", 616 - "qs@6.14.0", 617 - "range-parser", 618 - "router", 619 - "send@1.2.0", 620 - "serve-static@2.2.0", 621 - "statuses@2.0.2", 622 - "type-is@2.0.1", 623 - "vary" 624 - ] 625 - }, 626 835 "fast-redact@3.5.0": { 627 836 "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==" 628 837 }, ··· 634 843 "escape-html", 635 844 "on-finished", 636 845 "parseurl", 637 - "statuses@2.0.1", 846 + "statuses", 638 847 "unpipe" 639 848 ] 640 849 }, 641 - "finalhandler@2.1.0": { 642 - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 850 + "form-data@4.0.4": { 851 + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", 643 852 "dependencies": [ 644 - "debug@4.4.1", 645 - "encodeurl@2.0.0", 646 - "escape-html", 647 - "on-finished", 648 - "parseurl", 649 - "statuses@2.0.2" 853 + "asynckit", 854 + "combined-stream", 855 + "es-set-tostringtag", 856 + "hasown", 857 + "mime-types" 650 858 ] 651 859 }, 652 860 "forwarded@0.2.0": { ··· 655 863 "fresh@0.5.2": { 656 864 "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 657 865 }, 658 - "fresh@2.0.0": { 659 - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" 660 - }, 661 866 "function-bind@1.1.2": { 662 867 "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" 868 + }, 869 + "gensync@1.0.0-beta.2": { 870 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" 663 871 }, 664 872 "get-intrinsic@1.3.0": { 665 873 "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", ··· 692 900 "has-symbols@1.1.0": { 693 901 "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" 694 902 }, 903 + "has-tostringtag@1.0.2": { 904 + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 905 + "dependencies": [ 906 + "has-symbols" 907 + ] 908 + }, 695 909 "hasown@2.0.2": { 696 910 "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 697 911 "dependencies": [ ··· 704 918 "depd", 705 919 "inherits", 706 920 "setprototypeof", 707 - "statuses@2.0.1", 921 + "statuses", 708 922 "toidentifier" 709 923 ] 710 924 }, ··· 714 928 "safer-buffer" 715 929 ] 716 930 }, 717 - "iconv-lite@0.6.3": { 718 - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 719 - "dependencies": [ 720 - "safer-buffer" 721 - ] 722 - }, 723 931 "ieee754@1.2.1": { 724 932 "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 725 933 }, ··· 729 937 "ipaddr.js@1.9.1": { 730 938 "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 731 939 }, 732 - "is-promise@4.0.0": { 733 - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" 734 - }, 735 940 "iso-datestring-validator@2.2.2": { 736 941 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" 737 942 }, 943 + "js-tokens@4.0.0": { 944 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 945 + }, 946 + "jsesc@3.1.0": { 947 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 948 + "bin": true 949 + }, 950 + "json5@2.2.3": { 951 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", 952 + "bin": true 953 + }, 738 954 "ky@1.8.1": { 739 955 "integrity": "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw==" 740 956 }, 957 + "lru-cache@5.1.1": { 958 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 959 + "dependencies": [ 960 + "yallist" 961 + ] 962 + }, 741 963 "math-intrinsics@1.1.0": { 742 964 "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" 743 965 }, 744 966 "media-typer@0.3.0": { 745 967 "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 746 - }, 747 - "media-typer@1.1.0": { 748 - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==" 749 968 }, 750 969 "merge-descriptors@1.0.3": { 751 970 "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" 752 971 }, 753 - "merge-descriptors@2.0.0": { 754 - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" 755 - }, 756 972 "methods@1.1.2": { 757 973 "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 758 974 }, 759 975 "mime-db@1.52.0": { 760 976 "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 761 - }, 762 - "mime-db@1.54.0": { 763 - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" 764 977 }, 765 978 "mime-types@2.1.35": { 766 979 "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 767 980 "dependencies": [ 768 - "mime-db@1.52.0" 769 - ] 770 - }, 771 - "mime-types@3.0.1": { 772 - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 773 - "dependencies": [ 774 - "mime-db@1.54.0" 981 + "mime-db" 775 982 ] 776 983 }, 777 984 "mime@1.6.0": { ··· 799 1006 }, 800 1007 "negotiator@0.6.3": { 801 1008 "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 802 - }, 803 - "negotiator@1.0.0": { 804 - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" 805 1009 }, 806 1010 "node-fetch@2.7.0": { 807 1011 "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", ··· 816 1020 ], 817 1021 "bin": true 818 1022 }, 1023 + "node-releases@2.0.19": { 1024 + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" 1025 + }, 819 1026 "object-inspect@1.13.4": { 820 1027 "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" 821 1028 }, ··· 828 1035 "ee-first" 829 1036 ] 830 1037 }, 831 - "once@1.4.0": { 832 - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 833 - "dependencies": [ 834 - "wrappy" 835 - ] 836 - }, 837 1038 "parseurl@1.3.3": { 838 1039 "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 839 1040 }, 840 1041 "path-to-regexp@0.1.12": { 841 1042 "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" 842 1043 }, 843 - "path-to-regexp@8.2.0": { 844 - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" 1044 + "picocolors@1.1.1": { 1045 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 845 1046 }, 846 1047 "pino-abstract-transport@1.2.0": { 847 1048 "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", ··· 889 1090 "side-channel" 890 1091 ] 891 1092 }, 892 - "qs@6.14.0": { 893 - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 894 - "dependencies": [ 895 - "side-channel" 896 - ] 897 - }, 898 1093 "quick-format-unescaped@4.0.4": { 899 1094 "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" 900 1095 }, ··· 912 1107 "dependencies": [ 913 1108 "bytes", 914 1109 "http-errors", 915 - "iconv-lite@0.4.24", 916 - "unpipe" 917 - ] 918 - }, 919 - "raw-body@3.0.0": { 920 - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 921 - "dependencies": [ 922 - "bytes", 923 - "http-errors", 924 - "iconv-lite@0.6.3", 1110 + "iconv-lite", 925 1111 "unpipe" 926 1112 ] 927 1113 }, ··· 938 1124 "real-require@0.2.0": { 939 1125 "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" 940 1126 }, 941 - "router@2.2.0": { 942 - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 943 - "dependencies": [ 944 - "debug@4.4.1", 945 - "depd", 946 - "is-promise", 947 - "parseurl", 948 - "path-to-regexp@8.2.0" 949 - ] 950 - }, 951 1127 "safe-buffer@5.2.1": { 952 1128 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 953 1129 }, ··· 957 1133 "safer-buffer@2.1.2": { 958 1134 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 959 1135 }, 1136 + "semver@6.3.1": { 1137 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 1138 + "bin": true 1139 + }, 960 1140 "send@0.19.0": { 961 1141 "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 962 1142 "dependencies": [ ··· 966 1146 "encodeurl@1.0.2", 967 1147 "escape-html", 968 1148 "etag", 969 - "fresh@0.5.2", 1149 + "fresh", 970 1150 "http-errors", 971 1151 "mime", 972 1152 "ms@2.1.3", 973 1153 "on-finished", 974 1154 "range-parser", 975 - "statuses@2.0.1" 976 - ] 977 - }, 978 - "send@1.2.0": { 979 - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 980 - "dependencies": [ 981 - "debug@4.4.1", 982 - "encodeurl@2.0.0", 983 - "escape-html", 984 - "etag", 985 - "fresh@2.0.0", 986 - "http-errors", 987 - "mime-types@3.0.1", 988 - "ms@2.1.3", 989 - "on-finished", 990 - "range-parser", 991 - "statuses@2.0.2" 1155 + "statuses" 992 1156 ] 993 1157 }, 994 1158 "serve-static@1.16.2": { ··· 997 1161 "encodeurl@2.0.0", 998 1162 "escape-html", 999 1163 "parseurl", 1000 - "send@0.19.0" 1001 - ] 1002 - }, 1003 - "serve-static@2.2.0": { 1004 - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 1005 - "dependencies": [ 1006 - "encodeurl@2.0.0", 1007 - "escape-html", 1008 - "parseurl", 1009 - "send@1.2.0" 1164 + "send" 1010 1165 ] 1011 1166 }, 1012 1167 "setprototypeof@1.2.0": { ··· 1060 1215 "statuses@2.0.1": { 1061 1216 "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 1062 1217 }, 1063 - "statuses@2.0.2": { 1064 - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" 1065 - }, 1066 1218 "string_decoder@1.3.0": { 1067 1219 "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1068 1220 "dependencies": [ ··· 1088 1240 "type-is@1.6.18": { 1089 1241 "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1090 1242 "dependencies": [ 1091 - "media-typer@0.3.0", 1092 - "mime-types@2.1.35" 1093 - ] 1094 - }, 1095 - "type-is@2.0.1": { 1096 - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 1097 - "dependencies": [ 1098 - "content-type", 1099 - "media-typer@1.1.0", 1100 - "mime-types@3.0.1" 1243 + "media-typer", 1244 + "mime-types" 1101 1245 ] 1102 1246 }, 1103 1247 "uint8arrays@3.0.0": { ··· 1118 1262 "unpipe@1.0.0": { 1119 1263 "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1120 1264 }, 1265 + "update-browserslist-db@1.1.3_browserslist@4.25.3": { 1266 + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", 1267 + "dependencies": [ 1268 + "browserslist", 1269 + "escalade", 1270 + "picocolors" 1271 + ], 1272 + "bin": true 1273 + }, 1121 1274 "utils-merge@1.0.1": { 1122 1275 "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 1123 - }, 1124 - "varint@6.0.0": { 1125 - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" 1126 1276 }, 1127 1277 "vary@1.1.2": { 1128 1278 "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" ··· 1144 1294 "webidl-conversions" 1145 1295 ] 1146 1296 }, 1147 - "wrappy@1.0.2": { 1148 - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1149 - }, 1150 1297 "ws@8.18.3": { 1151 1298 "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==" 1152 1299 }, 1153 - "zod@3.25.46": { 1154 - "integrity": "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==" 1300 + "yallist@3.1.1": { 1301 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 1302 + }, 1303 + "zod@3.25.76": { 1304 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" 1305 + }, 1306 + "zod@4.0.17": { 1307 + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==" 1155 1308 } 1156 1309 }, 1310 + "redirects": { 1311 + "https://esm.sh/@alloc/quick-lru@^5.2.0?target=denonext": "https://esm.sh/@alloc/quick-lru@5.2.0?target=denonext", 1312 + "https://esm.sh/@atproto/api": "https://esm.sh/@atproto/api@0.16.3", 1313 + "https://esm.sh/@atproto/common-web@^0.4.2?target=denonext": "https://esm.sh/@atproto/common-web@0.4.2?target=denonext", 1314 + "https://esm.sh/@atproto/lexicon@^0.4.13?target=denonext": "https://esm.sh/@atproto/lexicon@0.4.13?target=denonext", 1315 + "https://esm.sh/@atproto/oauth-client-browser": "https://esm.sh/@atproto/oauth-client-browser@0.3.30", 1316 + "https://esm.sh/@atproto/syntax@^0.4.0?target=denonext": "https://esm.sh/@atproto/syntax@0.4.0?target=denonext", 1317 + "https://esm.sh/@atproto/xrpc@^0.7.2?target=denonext": "https://esm.sh/@atproto/xrpc@0.7.2?target=denonext", 1318 + "https://esm.sh/@jridgewell/gen-mapping@^0.3.2?target=denonext": "https://esm.sh/@jridgewell/gen-mapping@0.3.13?target=denonext", 1319 + "https://esm.sh/@jridgewell/resolve-uri@^3.1.0?target=denonext": "https://esm.sh/@jridgewell/resolve-uri@3.1.2?target=denonext", 1320 + "https://esm.sh/@jridgewell/sourcemap-codec@^1.4.14?target=denonext": "https://esm.sh/@jridgewell/sourcemap-codec@1.5.5?target=denonext", 1321 + "https://esm.sh/@jridgewell/sourcemap-codec@^1.5.0?target=denonext": "https://esm.sh/@jridgewell/sourcemap-codec@1.5.5?target=denonext", 1322 + "https://esm.sh/@jridgewell/trace-mapping@^0.3.24?target=denonext": "https://esm.sh/@jridgewell/trace-mapping@0.3.30?target=denonext", 1323 + "https://esm.sh/@nodelib/fs.stat@^2.0.2?target=denonext": "https://esm.sh/@nodelib/fs.stat@2.0.5?target=denonext", 1324 + "https://esm.sh/@nodelib/fs.walk@^1.2.3?target=denonext": "https://esm.sh/@nodelib/fs.walk@1.2.8?target=denonext", 1325 + "https://esm.sh/@tailwindcss/line-clamp?target=denonext": "https://esm.sh/@tailwindcss/line-clamp@0.4.4?target=denonext", 1326 + "https://esm.sh/autoprefixer@10": "https://esm.sh/autoprefixer@10.4.21", 1327 + "https://esm.sh/await-lock@^2.2.2?target=denonext": "https://esm.sh/await-lock@2.2.2?target=denonext", 1328 + "https://esm.sh/braces@^3.0.3?target=denonext": "https://esm.sh/braces@3.0.3?target=denonext", 1329 + "https://esm.sh/browserslist@^4.24.4?target=denonext": "https://esm.sh/browserslist@4.25.3?target=denonext", 1330 + "https://esm.sh/camelcase-css@^2.0.1?target=denonext": "https://esm.sh/camelcase-css@2.0.1?target=denonext", 1331 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/background-clip-text?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/background-clip-text?target=denonext", 1332 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/background-img-opts?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/background-img-opts?target=denonext", 1333 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/border-image?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/border-image?target=denonext", 1334 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/border-radius?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/border-radius?target=denonext", 1335 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/calc?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/calc?target=denonext", 1336 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-animation?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-animation?target=denonext", 1337 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-any-link?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-any-link?target=denonext", 1338 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-appearance?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-appearance?target=denonext", 1339 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-autofill?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-autofill?target=denonext", 1340 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-backdrop-filter?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-backdrop-filter?target=denonext", 1341 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-boxdecorationbreak?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-boxdecorationbreak?target=denonext", 1342 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-boxshadow?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-boxshadow?target=denonext", 1343 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-clip-path?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-clip-path?target=denonext", 1344 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-crisp-edges?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-crisp-edges?target=denonext", 1345 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-cross-fade?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-cross-fade?target=denonext", 1346 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-deviceadaptation?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-deviceadaptation?target=denonext", 1347 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-element-function?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-element-function?target=denonext", 1348 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-featurequeries?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-featurequeries?target=denonext", 1349 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-file-selector-button?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-file-selector-button?target=denonext", 1350 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-filter-function?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-filter-function?target=denonext", 1351 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-filters?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-filters?target=denonext", 1352 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-gradients?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-gradients?target=denonext", 1353 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-grid?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-grid?target=denonext", 1354 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-hyphens?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-hyphens?target=denonext", 1355 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-image-set?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-image-set?target=denonext", 1356 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-logical-props?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-logical-props?target=denonext", 1357 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-masks?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-masks?target=denonext", 1358 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-media-resolution?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-media-resolution?target=denonext", 1359 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-overscroll-behavior?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-overscroll-behavior?target=denonext", 1360 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-placeholder-shown?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-placeholder-shown?target=denonext", 1361 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-placeholder?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-placeholder?target=denonext", 1362 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-print-color-adjust?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-print-color-adjust?target=denonext", 1363 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-read-only-write?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-read-only-write?target=denonext", 1364 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-regions?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-regions?target=denonext", 1365 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-selection?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-selection?target=denonext", 1366 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-shapes?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-shapes?target=denonext", 1367 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-snappoints?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-snappoints?target=denonext", 1368 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-sticky?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-sticky?target=denonext", 1369 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-text-align-last?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-text-align-last?target=denonext", 1370 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-text-orientation?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-text-orientation?target=denonext", 1371 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-text-spacing?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-text-spacing?target=denonext", 1372 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-transitions?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-transitions?target=denonext", 1373 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-width-stretch?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-width-stretch?target=denonext", 1374 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css-writing-mode?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-writing-mode?target=denonext", 1375 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css3-boxsizing?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-boxsizing?target=denonext", 1376 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css3-cursors-grab?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-cursors-grab?target=denonext", 1377 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css3-cursors-newer?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-cursors-newer?target=denonext", 1378 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/css3-tabsize?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-tabsize?target=denonext", 1379 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/flexbox?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/flexbox?target=denonext", 1380 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/font-feature?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/font-feature?target=denonext", 1381 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/font-kerning?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/font-kerning?target=denonext", 1382 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/fullscreen?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/fullscreen?target=denonext", 1383 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/intrinsic-width?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/intrinsic-width?target=denonext", 1384 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-css-backdrop-pseudo-element?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-backdrop-pseudo-element?target=denonext", 1385 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-css-unicode-bidi-isolate-override?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-unicode-bidi-isolate-override?target=denonext", 1386 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-css-unicode-bidi-isolate?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-unicode-bidi-isolate?target=denonext", 1387 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-css-unicode-bidi-plaintext?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-unicode-bidi-plaintext?target=denonext", 1388 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-text-decoration-color?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-color?target=denonext", 1389 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-text-decoration-line?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-line?target=denonext", 1390 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-text-decoration-shorthand?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-shorthand?target=denonext", 1391 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/mdn-text-decoration-style?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-style?target=denonext", 1392 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/multicolumn?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/multicolumn?target=denonext", 1393 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/object-fit?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/object-fit?target=denonext", 1394 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/pointer?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/pointer?target=denonext", 1395 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/text-decoration?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-decoration?target=denonext", 1396 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/text-emphasis?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-emphasis?target=denonext", 1397 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/text-overflow?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-overflow?target=denonext", 1398 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/text-size-adjust?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-size-adjust?target=denonext", 1399 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/transforms2d?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/transforms2d?target=denonext", 1400 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/transforms3d?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/transforms3d?target=denonext", 1401 + "https://esm.sh/caniuse-lite@^1.0.30001702/data/features/user-select-none?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/data/features/user-select-none?target=denonext", 1402 + "https://esm.sh/caniuse-lite@^1.0.30001702/dist/unpacker/agents?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/agents?target=denonext", 1403 + "https://esm.sh/caniuse-lite@^1.0.30001702/dist/unpacker/feature?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/feature?target=denonext", 1404 + "https://esm.sh/caniuse-lite@^1.0.30001735/dist/unpacker/agents?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/agents?target=denonext", 1405 + "https://esm.sh/caniuse-lite@^1.0.30001735/dist/unpacker/feature?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/feature?target=denonext", 1406 + "https://esm.sh/caniuse-lite@^1.0.30001735/dist/unpacker/region?target=denonext": "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/region?target=denonext", 1407 + "https://esm.sh/cssesc@^3.0.0?target=denonext": "https://esm.sh/cssesc@3.0.0?target=denonext", 1408 + "https://esm.sh/didyoumean@^1.2.2?target=denonext": "https://esm.sh/didyoumean@1.2.2?target=denonext", 1409 + "https://esm.sh/dlv@^1.1.3?target=denonext": "https://esm.sh/dlv@1.1.3?target=denonext", 1410 + "https://esm.sh/electron-to-chromium@^1.5.204/versions?target=denonext": "https://esm.sh/electron-to-chromium@1.5.209/versions?target=denonext", 1411 + "https://esm.sh/fast-glob@^3.3.2?target=denonext": "https://esm.sh/fast-glob@3.3.3?target=denonext", 1412 + "https://esm.sh/fastq@^1.6.0?target=denonext": "https://esm.sh/fastq@1.19.1?target=denonext", 1413 + "https://esm.sh/fill-range@^7.1.1?target=denonext": "https://esm.sh/fill-range@7.1.1?target=denonext", 1414 + "https://esm.sh/fraction.js@^4.3.7?target=denonext": "https://esm.sh/fraction.js@4.3.7?target=denonext", 1415 + "https://esm.sh/glob-parent@^5.1.2?target=denonext": "https://esm.sh/glob-parent@5.1.2?target=denonext", 1416 + "https://esm.sh/glob-parent@^6.0.2?target=denonext": "https://esm.sh/glob-parent@6.0.2?target=denonext", 1417 + "https://esm.sh/graphemer@^1.4.0?target=denonext": "https://esm.sh/graphemer@1.4.0?target=denonext", 1418 + "https://esm.sh/is-extglob@^2.1.1?target=denonext": "https://esm.sh/is-extglob@2.1.1?target=denonext", 1419 + "https://esm.sh/is-glob@^4.0.1?target=denonext": "https://esm.sh/is-glob@4.0.3?target=denonext", 1420 + "https://esm.sh/is-glob@^4.0.3?target=denonext": "https://esm.sh/is-glob@4.0.3?target=denonext", 1421 + "https://esm.sh/iso-datestring-validator@^2.2.2?target=denonext": "https://esm.sh/iso-datestring-validator@2.2.2?target=denonext", 1422 + "https://esm.sh/jiti@^1.21.6/dist/babel?target=denonext": "https://esm.sh/jiti@1.21.7/dist/babel?target=denonext", 1423 + "https://esm.sh/jiti@^1.21.6?target=denonext": "https://esm.sh/jiti@1.21.7?target=denonext", 1424 + "https://esm.sh/jose@^5.2.0?target=denonext": "https://esm.sh/jose@5.10.0?target=denonext", 1425 + "https://esm.sh/lines-and-columns@^1.1.6?target=denonext": "https://esm.sh/lines-and-columns@1.2.4?target=denonext", 1426 + "https://esm.sh/lru-cache@^10.2.0?target=denonext": "https://esm.sh/lru-cache@10.4.3?target=denonext", 1427 + "https://esm.sh/merge2@^1.3.0?target=denonext": "https://esm.sh/merge2@1.4.1?target=denonext", 1428 + "https://esm.sh/micromatch@^4.0.8?target=denonext": "https://esm.sh/micromatch@4.0.8?target=denonext", 1429 + "https://esm.sh/multiformats@^9.4.2/basics?target=denonext": "https://esm.sh/multiformats@9.9.0/basics?target=denonext", 1430 + "https://esm.sh/multiformats@^9.9.0/bases/base64?target=denonext": "https://esm.sh/multiformats@9.9.0/bases/base64?target=denonext", 1431 + "https://esm.sh/multiformats@^9.9.0/cid?target=denonext": "https://esm.sh/multiformats@9.9.0/cid?target=denonext", 1432 + "https://esm.sh/nanoid@^3.3.11/non-secure?target=denonext": "https://esm.sh/nanoid@3.3.11/non-secure?target=denonext", 1433 + "https://esm.sh/node-releases@^2.0.19/data/processed/envs.json?module": "https://esm.sh/node-releases@2.0.19/data/processed/envs.json?module", 1434 + "https://esm.sh/node-releases@^2.0.19/data/release-schedule/release-schedule.json?module": "https://esm.sh/node-releases@2.0.19/data/release-schedule/release-schedule.json?module", 1435 + "https://esm.sh/normalize-path@^3.0.0?target=denonext": "https://esm.sh/normalize-path@3.0.0?target=denonext", 1436 + "https://esm.sh/normalize-range@^0.1.2?target=denonext": "https://esm.sh/normalize-range@0.1.2?target=denonext", 1437 + "https://esm.sh/object-hash@^3.0.0?target=denonext": "https://esm.sh/object-hash@3.0.0?target=denonext", 1438 + "https://esm.sh/picocolors@^1.1.1?target=denonext": "https://esm.sh/picocolors@1.1.1?target=denonext", 1439 + "https://esm.sh/picomatch@^2.3.1/lib/utils?target=denonext": "https://esm.sh/picomatch@2.3.1/lib/utils?target=denonext", 1440 + "https://esm.sh/picomatch@^2.3.1?target=denonext": "https://esm.sh/picomatch@2.3.1?target=denonext", 1441 + "https://esm.sh/postcss-js@^4.0.1?target=denonext": "https://esm.sh/postcss-js@4.0.1?target=denonext", 1442 + "https://esm.sh/postcss-nested@^6.2.0?target=denonext": "https://esm.sh/postcss-nested@6.2.0?target=denonext", 1443 + "https://esm.sh/postcss-selector-parser@^6.1.1?target=denonext": "https://esm.sh/postcss-selector-parser@6.1.2?target=denonext", 1444 + "https://esm.sh/postcss-selector-parser@^6.1.2/dist/util/unesc?target=denonext": "https://esm.sh/postcss-selector-parser@6.1.2/dist/util/unesc?target=denonext", 1445 + "https://esm.sh/postcss-selector-parser@^6.1.2?target=denonext": "https://esm.sh/postcss-selector-parser@6.1.2?target=denonext", 1446 + "https://esm.sh/postcss-value-parser@^4.2.0?target=denonext": "https://esm.sh/postcss-value-parser@4.2.0?target=denonext", 1447 + "https://esm.sh/postcss@8": "https://esm.sh/postcss@8.5.6", 1448 + "https://esm.sh/postcss@^8.1.0?target=denonext": "https://esm.sh/postcss@8.5.6?target=denonext", 1449 + "https://esm.sh/postcss@^8.2.14?target=denonext": "https://esm.sh/postcss@8.5.6?target=denonext", 1450 + "https://esm.sh/postcss@^8.4.21?target=denonext": "https://esm.sh/postcss@8.5.6?target=denonext", 1451 + "https://esm.sh/postcss@^8.4.47?target=denonext": "https://esm.sh/postcss@8.5.6?target=denonext", 1452 + "https://esm.sh/queue-microtask@^1.2.2?target=denonext": "https://esm.sh/queue-microtask@1.2.3?target=denonext", 1453 + "https://esm.sh/reusify@^1.0.4?target=denonext": "https://esm.sh/reusify@1.1.0?target=denonext", 1454 + "https://esm.sh/run-parallel@^1.1.9?target=denonext": "https://esm.sh/run-parallel@1.2.0?target=denonext", 1455 + "https://esm.sh/scheduler@^0.26.0?target=denonext": "https://esm.sh/scheduler@0.26.0?target=denonext", 1456 + "https://esm.sh/source-map-js@^1.2.1?target=denonext": "https://esm.sh/source-map-js@1.2.1?target=denonext", 1457 + "https://esm.sh/sucrase@^3.35.0?target=denonext": "https://esm.sh/sucrase@3.35.0?target=denonext", 1458 + "https://esm.sh/tailwindcss@3": "https://esm.sh/tailwindcss@3.4.17", 1459 + "https://esm.sh/tailwindcss@^4.0.0-beta.9/plugin?target=denonext": "https://esm.sh/tailwindcss@4.1.12/plugin?target=denonext", 1460 + "https://esm.sh/tlds@^1.234.0?target=denonext": "https://esm.sh/tlds@1.259.0?target=denonext", 1461 + "https://esm.sh/to-regex-range@^5.0.1?target=denonext": "https://esm.sh/to-regex-range@5.0.1?target=denonext", 1462 + "https://esm.sh/ts-interface-checker@^0.1.9?target=denonext": "https://esm.sh/ts-interface-checker@0.1.13?target=denonext", 1463 + "https://esm.sh/util-deprecate@^1.0.2?target=denonext": "https://esm.sh/util-deprecate@1.0.2?target=denonext", 1464 + "https://esm.sh/zod@^3.23.8?target=denonext": "https://esm.sh/zod@3.25.76?target=denonext" 1465 + }, 1157 1466 "remote": { 1158 - "https://deno.land/x/lru@1.0.2/mod.ts": "1d44b87c4d40ff33749ae5fd85fe234344e0dace835fdfeb48413edea9461159", 1159 - "https://esm.sh/multiformats@13.1.1/bases/base58": "3ec9726ed217d96c33924e3357102a0c4ee9130f68ea362525a1a4df0ff306a4", 1160 - "https://esm.sh/multiformats@13.1.1/denonext/bases/base58.mjs": "d99841a98cda1b2685519b4914e62e61fd22dc2647a02cc6f70bee0658635302", 1161 - "https://esm.sh/multiformats@13.1.1/denonext/bytes.mjs": "d8578dcc54b8e5c345f6b3b83ed1ada64f400353fc2e7d8b15179723856d7315", 1162 - "https://esm.sh/multiformats@13.1.1/denonext/dist/src/bases/base.mjs": "a72250e285c1709319c71742654598cd0f149b03b616b268a0abd63fe396fc6c" 1467 + "https://esm.sh/@alloc/quick-lru@5.2.0/denonext/quick-lru.mjs": "18d5ab8b0ee2b8c0ba55eab65a86f126f480d5d0ca85eeefa32f9ee2e399f110", 1468 + "https://esm.sh/@alloc/quick-lru@5.2.0?target=denonext": "8506b81cc497d3d01a8b06176598c36333efab2f6773b5f681f4bfbeba33079d", 1469 + "https://esm.sh/@atproto-labs/did-resolver@0.2.0/denonext/did-resolver.mjs": "a88663c45be439196b85c03cd1b6bc904b5b33c2aca0f3bab716f1a8c305d7e6", 1470 + "https://esm.sh/@atproto-labs/fetch@0.2.3/denonext/fetch.mjs": "eaabcb75b50ef9b62d749e0e6843f11d60204526a0296ff84a99c8d8385862bc", 1471 + "https://esm.sh/@atproto-labs/handle-resolver@0.3.0/denonext/handle-resolver.mjs": "5ce886fa149d802d864b9988d0be71d18e37e06b7aab5a941a482aaa7b2d9da6", 1472 + "https://esm.sh/@atproto-labs/identity-resolver@0.3.0/denonext/identity-resolver.mjs": "36cf98e9478813b6e5eb6e33da8771f2dd33a2e8ba587d32803dc0b766cd5cab", 1473 + "https://esm.sh/@atproto-labs/pipe@0.1.1/denonext/pipe.mjs": "9432ecdd24724682a612fc0f47f019366a1ebf5e3fd2ffa9ef6b5176419ed84f", 1474 + "https://esm.sh/@atproto-labs/simple-store-memory@0.1.3/denonext/simple-store-memory.mjs": "789ef49fe6b81024631fd4340183c670f3f35b1be8ca773d5c14f7a040dd0b89", 1475 + "https://esm.sh/@atproto-labs/simple-store@0.2.0/denonext/simple-store.mjs": "1829d20db9d7157cfd85b3968dacf60aaab1fa77742612ae0ea193a616feec21", 1476 + "https://esm.sh/@atproto/api@0.16.3": "c50f915ec53d1d48b2fbf88c74ae6d41da255e9ac6e7449ca6c8a723a1fa419b", 1477 + "https://esm.sh/@atproto/api@0.16.3/denonext/api.mjs": "1fd08820fbc9cc67f29c4dfacf1f5b806ac67d1ee3bbdfcbc0bafcf07fda0cf2", 1478 + "https://esm.sh/@atproto/common-web@0.4.2/denonext/common-web.mjs": "350a01dc9ea0e21c5b85c166d269f53c3348ec3daaf94b97e035d0256366a2ef", 1479 + "https://esm.sh/@atproto/common-web@0.4.2?target=denonext": "bb0d13657e0fc4565eaf217a456ea6a48c2ca5178ecbb6692eb0d538ef51a4cd", 1480 + "https://esm.sh/@atproto/did@0.1.5/denonext/did.mjs": "1ae8eff7a842a509499f7efd1a1db842087499e2f03138ac8400f9558f926733", 1481 + "https://esm.sh/@atproto/jwk-jose@0.1.10/denonext/jwk-jose.mjs": "d4e05a0828e23532f185e301ec2b70c5bc4632691992b21f7d4af43e5ffe784c", 1482 + "https://esm.sh/@atproto/jwk-webcrypto@0.1.10/denonext/jwk-webcrypto.mjs": "ba106ae637353dcbaa7e8b7e6a0cb888e707261383a9c56eaa4a7fe4d8ad3519", 1483 + "https://esm.sh/@atproto/jwk@0.5.0/denonext/jwk.mjs": "5791d0fc7369da7993594d23ff00df612d43b33611b293df66476e07a0033b0a", 1484 + "https://esm.sh/@atproto/lexicon@0.4.13/denonext/lexicon.mjs": "eed494b224e5d28b2d1b1c0e88740b16eeaab1b1739c1d9b3120feb35f238de4", 1485 + "https://esm.sh/@atproto/lexicon@0.4.13?target=denonext": "df4a5757c00257ca4289db10271075cd3e89d48c9d5664d1e40ad3db933ea3ab", 1486 + "https://esm.sh/@atproto/oauth-client-browser@0.3.30": "d5df31ac9f9536879c8e1dbc8a5f3b9dc6635321044aaceda6f89028e416bd84", 1487 + "https://esm.sh/@atproto/oauth-client-browser@0.3.30/denonext/oauth-client-browser.mjs": "cae96820945c788497c365ce0b99decacf17b6b0b08ab3489e71fa2f7a62fdc6", 1488 + "https://esm.sh/@atproto/oauth-client@0.5.4/denonext/oauth-client.mjs": "75c5656274abcd6fba39aef3d6eb5b1ae53459a01665b17e9699db87278891b0", 1489 + "https://esm.sh/@atproto/oauth-types@0.4.1/denonext/oauth-types.mjs": "aa73bced2c5dda2b693f2c7eccddf2c26ef8f263553ee3be3ce0bd387ce75de0", 1490 + "https://esm.sh/@atproto/syntax@0.4.0/denonext/syntax.mjs": "e03d230b4bce87f75b6af7826e654db0ccc1ee1e8a0399522b708bfbb72534f9", 1491 + "https://esm.sh/@atproto/syntax@0.4.0?target=denonext": "d64c2ed239cf18dde908f4e6e78ad2771d29f879318eec4b567e9737f0b4a0cf", 1492 + "https://esm.sh/@atproto/xrpc@0.7.2/denonext/xrpc.mjs": "8b6a604c52f49f0e958185275f5553326d93178d468de49b0a6a8081882f8840", 1493 + "https://esm.sh/@atproto/xrpc@0.7.2?target=denonext": "0d01d68187689e970d63c99364a99659c6846e0eedc7abb9227ba082d0546a70", 1494 + "https://esm.sh/@jridgewell/gen-mapping@0.3.13/denonext/gen-mapping.mjs": "297f733cd1f48cde4050055e4379df23b701394106946ec8692f41fd4fbc06a3", 1495 + "https://esm.sh/@jridgewell/gen-mapping@0.3.13?target=denonext": "d3eb8a4aee09dd989ae0d8121874e9ef41d7b1c2261a8809250654d040b54e83", 1496 + "https://esm.sh/@jridgewell/resolve-uri@3.1.2/denonext/resolve-uri.mjs": "2da147ad4f55ddeff542640347c789ff19db8db4ead77cce27a0c171f5f5c349", 1497 + "https://esm.sh/@jridgewell/resolve-uri@3.1.2?target=denonext": "9f734a6f626960d09b0ddb1965c08a7d5960fec667f891b4a0a55d23d0bfac51", 1498 + "https://esm.sh/@jridgewell/sourcemap-codec@1.5.5/denonext/sourcemap-codec.mjs": "51baa3e273e77b02f11ce4ed87b71cad04b839a47ba88ab320aafd5d96780eb8", 1499 + "https://esm.sh/@jridgewell/sourcemap-codec@1.5.5?target=denonext": "92c30f6ceeb81e53eb90d72ecbe02f22216c2c8613582768f10fca120d3812ab", 1500 + "https://esm.sh/@jridgewell/trace-mapping@0.3.30/denonext/trace-mapping.mjs": "76ce197a54a9e8296ccefda22951f70f8c889c29bc6ba3ee49f11b6b51084478", 1501 + "https://esm.sh/@jridgewell/trace-mapping@0.3.30?target=denonext": "b48a6080590350f580de059f118f46810488cdceb1158dc2f53513a25451cc08", 1502 + "https://esm.sh/@nodelib/fs.scandir@2.1.5/denonext/fs.scandir.mjs": "274e376535b7b253c73f86a42b3bda10049b6fea75db4cc7b53b88db339286b9", 1503 + "https://esm.sh/@nodelib/fs.stat@2.0.5/denonext/fs.stat.mjs": "54da00841dc5e6b5581f50e299fb9fd23864c11526145c2bd581e5b40737cecf", 1504 + "https://esm.sh/@nodelib/fs.stat@2.0.5?target=denonext": "7703befd72f56697b978206aa0942d7f53aab31b0a55f1244b4fd794fc81499c", 1505 + "https://esm.sh/@nodelib/fs.walk@1.2.8/denonext/fs.walk.mjs": "ce895194f61083880e5aef2390ea4edcfc12597f308be1561a1266392f0f67eb", 1506 + "https://esm.sh/@nodelib/fs.walk@1.2.8?target=denonext": "a68ec3adf7a2812351a3ed3cb8df689acad4c7afc70fee986dcd895be3a9bbbb", 1507 + "https://esm.sh/@tailwindcss/line-clamp@0.4.4/denonext/line-clamp.mjs": "252fa8d82ac4e569e3ee8140c9ca3e216760631da1019cac48e6750feb89d8d3", 1508 + "https://esm.sh/@tailwindcss/line-clamp@0.4.4?target=denonext": "1fbc5f55d06b23c8fc79d58f00831983224f1c15b5249de3cd0a2d51fed22572", 1509 + "https://esm.sh/autoprefixer@10.4.21": "2bb14916b2e4881a32ee311720679c3e57e2d04f45ac808f4f71ae4e0923801c", 1510 + "https://esm.sh/autoprefixer@10.4.21/denonext/autoprefixer.mjs": "0d9874129a69bb35c1392f38f09e6a5a747c0eeb8c2b29e7fc8d4c1868337d92", 1511 + "https://esm.sh/await-lock@2.2.2/denonext/await-lock.mjs": "6787201229bc4ccae3aabbcee4086ea60b803bb476b37e576ec135e954f3ae5a", 1512 + "https://esm.sh/await-lock@2.2.2?target=denonext": "62d8dd2ac9a50e115f76d05f309f437a8dbc6e53ff508c70addf07ed40ad0077", 1513 + "https://esm.sh/braces@3.0.3/denonext/braces.mjs": "a3909393c947301c3b6348ab2378bd4f96fe001009b9beb44dcf1030dd717f47", 1514 + "https://esm.sh/braces@3.0.3?target=denonext": "58c0fed5cc432a2192aad7eeaf6e3988b96830a6c6ec0ad51dfc97578b2e8dee", 1515 + "https://esm.sh/browserslist@4.25.3/denonext/browserslist.mjs": "df95a5657a9d8a83aa8e834a82e1747b3d6e8ea1ee8d41676c665e05d803c103", 1516 + "https://esm.sh/browserslist@4.25.3?target=denonext": "31d80d40c1963ce619917d0fdb33cfad4ad4c3fb4d431522fde74a5b6dcab228", 1517 + "https://esm.sh/camelcase-css@2.0.1/denonext/camelcase-css.mjs": "1aa67ed93327d93dae71a70444bf1408a8ab3eab8bba2a710d3643b4ed415901", 1518 + "https://esm.sh/camelcase-css@2.0.1?target=denonext": "39dbe8afdc8dc441073d55efa7ce04adb31a07d8d31619fed5d31e299be32c93", 1519 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/background-clip-text?target=denonext": "410cd6f12e4acbbb198bcffa735247c11b114d672cd71e5c355e550394b485fc", 1520 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/background-img-opts?target=denonext": "59bc277f34121910caebe8cbebae46596ad122040cb0fc0a32d91980641cc6e5", 1521 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/border-image?target=denonext": "eaa8f44f48530b9f5b5bde4040ec6bedc60482324912efdbb9c7cbe23b4c40e6", 1522 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/border-radius?target=denonext": "2793c2fb8b38abcc35fd76f4151a8562b1ffbb360090f54d43ef0f69cabdd953", 1523 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/calc?target=denonext": "6810ac3373ad65b4f1df8d5f5df2ee51de539337676380505c69fdfba33b718a", 1524 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-animation?target=denonext": "cd08f5a8d560b438fc4742ccdb35babf59978cee4f0f4d0ea0ce35e532280b77", 1525 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-any-link?target=denonext": "5f170cdba78f28591bbc12bc2c353345fa9c9b99302767673b8ed84518a8f148", 1526 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-appearance?target=denonext": "c6fe9a1b6943c32b48604e96e3719ab7de884c02d94a5a86b299798a56c0ced3", 1527 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-autofill?target=denonext": "2d0fac9e144ea16f0cb22fc09748b4c1525475ed76df9069056e1016ca04694f", 1528 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-backdrop-filter?target=denonext": "1cbb5e4e0dc743651ad302d564dabadf9d7db009e8ecfc67459e68d6899276ef", 1529 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-boxdecorationbreak?target=denonext": "a384c16f0a989c7c11037a7beac2704b6e20880884b20e2b68ccd5caa09b1041", 1530 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-boxshadow?target=denonext": "e2d56528ee8e1fdf7b1c84f7d742411c910d8e1c5e774ca7c6f35fd4aff0185b", 1531 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-clip-path?target=denonext": "da574566b0b2e0f75e4dd20da5ace9303cc3aa783051c9a74d4990ce5783875d", 1532 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-crisp-edges?target=denonext": "d836bcdd65d84c66e14dec3713cc577416fab771869eff01d1c259d0731c60ca", 1533 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-cross-fade?target=denonext": "a1255ba2cc65f09a24d794b120e9042e53cf8182a39088f3deee1acfe4ba30e8", 1534 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-deviceadaptation?target=denonext": "5d279ede28f380339f2317871583446a89858852ff404a7c2f2681169dcdf63c", 1535 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-element-function?target=denonext": "86df7c505abc3598c283cf3bb4e1262272185d5c9c04d0bd0bc4241e744d07af", 1536 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-featurequeries?target=denonext": "cb26e40828c397b118377fe9c46d8827bb49a5c95720ed47155368f9dd9072d5", 1537 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-file-selector-button?target=denonext": "d2efe044761f04e8daa85c6931f2edb53ae8a076efee72a62bbcde98404c4546", 1538 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-filter-function?target=denonext": "00dfd6452d13e7fd78eac05c4b4460d5fca56e59a16f900e6b1aaddc13316c34", 1539 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-filters?target=denonext": "c0ed7f7a41725e851ab52e6f67c7d147566d04e2b501e3dab2c8b3d2dc8f7454", 1540 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-gradients?target=denonext": "723eac5e8252240a41abde46919d8b977516f1a1ba6acb8173a49a547d5b4c47", 1541 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-grid?target=denonext": "6dda1d6ba3b27b5d73f6b924bb8a82a34f1eb109415a20ac8f3284e75fa2c075", 1542 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-hyphens?target=denonext": "1e16430cc4bf0439bf66e6e80965ad89ac2c83a4f684b959289f1cef5929f382", 1543 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-image-set?target=denonext": "c74d4ea198e1e44dbe3a4437a58dd45ad7f9c0dbc68731903643d6aff28159eb", 1544 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-logical-props?target=denonext": "e1d3d069d199e623d5cbfd5a99b74f838fa8a29b4b9535809cc3e6061678fd71", 1545 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-masks?target=denonext": "49b3001ebf77fd07798fb3d1f8bfa397926a86c15d66ec3c745e19df652b0266", 1546 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-media-resolution?target=denonext": "44c93f9033a112f25bb41028a7aea9284c25fa0950f2b14005b5649b86acc2c2", 1547 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-overscroll-behavior?target=denonext": "fa8b69dabf3ad7cca1b765ce9b8277469d12fd92bf8dad3fe537c25ba0bc46ad", 1548 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-placeholder-shown?target=denonext": "eb885bd8860c72500e69088c19d985752d839d0bccbb21f8f5a8515afdc21e2a", 1549 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-placeholder?target=denonext": "3b845f3c43353b5bf195ec1e826bf79d299535693f0deb208693160cdf87de00", 1550 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-print-color-adjust?target=denonext": "89d6b59aef0ca1baacd771a908372ef4f3f011f9e01cd6a2c01b8ae6cb9cbc91", 1551 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-read-only-write?target=denonext": "98a11fd4a5f865982e18d76eea1c3f154bf78e3474a6c3b66e794ce59b3c6fc7", 1552 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-regions?target=denonext": "ed41dcd2492b49a91a8d3b9cdb195f56c231c0feb97470954af91c1d95f5c45f", 1553 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-selection?target=denonext": "166b5f4697cba38a485f734f88fc111158b6e8d022cc95f0da91740ef7068f4a", 1554 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-shapes?target=denonext": "62eb44b6c358eeaf93589babc2f3209f224b6749d56d8d4a87cc459ad014c085", 1555 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-snappoints?target=denonext": "8ec2ae78e2fa3635526e139a5751234da704942a912e753ba4c270aa0b3ab87d", 1556 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-sticky?target=denonext": "4b0cc7d05e9ef45fbe86a3285e915af119ecee64fa11d745ca6ae66dfa416a28", 1557 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-text-align-last?target=denonext": "11314b76a045f01304bca81b40b61d23c93ea7f157418396775a5d350d55b6bd", 1558 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-text-orientation?target=denonext": "569d5ce1d38a5003618a558a522265c2b68bc6227cd15f21fe4a2a2909c85c2d", 1559 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-text-spacing?target=denonext": "4733902f15a9f42900bc1b1fc19fb468efd7ff5afe4c6cb547779b1ddaaeb2bc", 1560 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-transitions?target=denonext": "35b76706059c4ccb1e74d7274559bb8332649a0c29cd6b198ec8174ffb5cf4f1", 1561 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-width-stretch?target=denonext": "97db70aa18a7c3500e32d8caba17de65acbc533073796bf81e032995d99fb460", 1562 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css-writing-mode?target=denonext": "d8c2c92808c011fdf3948e28938f089d0656da7aef69845745e9844f2c1f5bf5", 1563 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-boxsizing?target=denonext": "8b65cba0f1db351d9f412b1a48180b7ba8e3837c2268dae681b2f957d2c76c4b", 1564 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-cursors-grab?target=denonext": "d092f9cffa858b742b93d14366f9fa0baff600b34367098da6b6b3a76efd98f4", 1565 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-cursors-newer?target=denonext": "3f6f81cf79f4f977d6bebe979392c0e19b0c4e865528d80c1869230309dba559", 1566 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/css3-tabsize?target=denonext": "bf253ccfb4095831b38de00ff84e4888b33a027ff3826bfbc44738018cbb9780", 1567 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/flexbox?target=denonext": "9a6f56410f6e89c8aa34e4df56fe24ded91f4a5df9d7164abadaef021ab26642", 1568 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/font-feature?target=denonext": "4b0b57ea532fa7de37e99570197c4fb4af9dec2aed32da35e8fac42645400729", 1569 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/font-kerning?target=denonext": "e5155e132f2a99faa3c7585d36581a4bd11d64204b156be806287b4d4547b262", 1570 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/fullscreen?target=denonext": "62069c47a7f1f1eb3ce947d4af0c777859bcb9524b5393669e5fc731915e58d0", 1571 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/intrinsic-width?target=denonext": "3d081e99e46156d3630ea3ee2a027dc5fca3f256b693daf160d8348720ae70b0", 1572 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-backdrop-pseudo-element?target=denonext": "c36d9ae3ea67434ef7141f9e57750b9fbef89723d9fe4a4e021bebe73ee00ed6", 1573 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-unicode-bidi-isolate-override?target=denonext": "b5cf96212c3858b21e4345ea5164b23d54cc0b8d205b3e469ef7a5b2562f4266", 1574 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-unicode-bidi-isolate?target=denonext": "249db094ff9a765fe829d325dbca9efd06622515d6435f865bd2761040850b64", 1575 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-css-unicode-bidi-plaintext?target=denonext": "02a939cc49f116e74495813b3912600f7c1f541a71be82ab897c5eabe5910e7a", 1576 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-color?target=denonext": "364776cd45c73a6b11089ae8c8d3352fe829950c3a2dc06ed47b091b544fb146", 1577 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-line?target=denonext": "a639f52a7f11ae01303194ae61238c3f1ea228d6d4349846e9a76ef5dacb4405", 1578 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-shorthand?target=denonext": "8d27f04a38b7be26f8bdb2e2a8c4b5b9cb53adcccea404fdb538bb273ec1f3fc", 1579 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/mdn-text-decoration-style?target=denonext": "353e35877b7ec3ade3679f7101ba8b2363e6c39ea5eea8972ea1d386d6998038", 1580 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/multicolumn?target=denonext": "5c819517e4c89e4cdc626a97802996df69d2c269d01ea1ce6aca75a684448517", 1581 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/object-fit?target=denonext": "fd6fc4f7ae57de5712e5073ee6961bb57afb0c9e947b9837ea38f1842924468d", 1582 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/pointer?target=denonext": "783269befdd4f21a426a12bbb95bdc4667ecaf9e240ca8851d549e0a200494ac", 1583 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-decoration?target=denonext": "090bbeb9e7969784e4db951257aed5babaf0a4e98552877dbc9cf138917a2155", 1584 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-emphasis?target=denonext": "c21d9db705a467e4b192a86e760b2e071edababdc106db810230262a1f262522", 1585 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-overflow?target=denonext": "6c3c48dd88e402d658461c15cb1d7eb053d91f0866a08b416914e572f149d320", 1586 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/text-size-adjust?target=denonext": "f471ff7716b89dfda4b5296ef98dbc7fe58f3de3a6640f72b553c5f1944d6620", 1587 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/transforms2d?target=denonext": "0dcb50846de392278e49bc7f6bcd2ee37d40457ae103e2e6292726f9670cdbe9", 1588 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/transforms3d?target=denonext": "a7159901d2b73dccc4caa04bd0c0b2ab171c65cf91d7d48b3596cc727fd08316", 1589 + "https://esm.sh/caniuse-lite@1.0.30001737/data/features/user-select-none?target=denonext": "1c5d634aed511a26198d2df4ab1a6ec00fbdde084617140d391c85cf5fea8b04", 1590 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/background-clip-text.mjs": "bdb85956ae42e455d4a62d5e6f250c7fb07ae615ec130480bb3c9d3eb5ec057e", 1591 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/background-img-opts.mjs": "86ec64e7c7278e916e4c7b646010b5648880b33bbde7656467d1b99f91f5e8a7", 1592 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/border-image.mjs": "93cac91d3b2beba05865ad31c591d1e25fb851b59a6ec357e1fa87c6314aa0b7", 1593 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/border-radius.mjs": "381bf81a142193a85d7a0165efea15ea2910ccf0a2bd4e3a8299b286bd30bb5d", 1594 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/calc.mjs": "eab741e8c7640da2c85222bf217e8d17c2f4c8669326d8c57a319ef4b20b79ac", 1595 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-animation.mjs": "607f634b5572f61d0a68604ea809cff0f2b085a860867e331d413dbb4c2745ee", 1596 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-any-link.mjs": "e25fe5d99e87b5585531cf89b8cb79ae610b54cf071f6e177d749d2ce35daf08", 1597 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-appearance.mjs": "87f1fafa407b96e8cda675edc88dd59ad43ac1564c48dd31de9b9f1751143827", 1598 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-autofill.mjs": "697f47c9f40f8a67bc1dcd5b2e97373bb5c5c74550e4d69f8b467d510403adb1", 1599 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-backdrop-filter.mjs": "e72583cd26a0e13335d2558bc5d0b9e0c83d6a71d73d5924a165585a638211c4", 1600 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-boxdecorationbreak.mjs": "a76be63656f090096b4f57e6dae05abb807e5d8eb8315e34b74480ab606eddc6", 1601 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-boxshadow.mjs": "3a9efa13ee4413ad69f18264ab56cd673ca0a5568d854061fb996652df3eeea6", 1602 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-clip-path.mjs": "05c24465e6080304e0d71f12996d004619dd098f1009aa6a6b4558defc46ca72", 1603 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-crisp-edges.mjs": "60822ba941125c8a9727a8157c2c42a2be08babd937da435921a1a7ee511667a", 1604 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-cross-fade.mjs": "d19d727301af8c921be97d963406203addba4dae4b2fef206b54fe5bb354bf37", 1605 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-deviceadaptation.mjs": "b6281816e7439f8c01be80825e7a183991ce53e50f0a1dc4ec1a938bbedf2874", 1606 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-element-function.mjs": "13d5108f90aed723cc0ae3ea8cd3f62e3884f4a28ff43b69b3e41c62e47d480b", 1607 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-featurequeries.mjs": "7352ae67e14b4a01c1add6a640789e7349419f2ef84ee010627d12c76093099b", 1608 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-file-selector-button.mjs": "91c7b2e32daa62f755276ee2cfd4b8891b8582b554f83952b654110754882f15", 1609 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-filter-function.mjs": "70759427567e55124a2ff510aacbd533959dbe4112219da4c5374e268cedb278", 1610 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-filters.mjs": "26f815df8032a8a379d2c7e606d18e3911720c6e31c4e276377f33a5ebd476d4", 1611 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-gradients.mjs": "e87494876bc0b56246c94253b544c73e9b1dc0d8a84e028b21dd629e8c9c3948", 1612 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-grid.mjs": "4fe4b29dc613965e55e5c22e31e229dfc9fbb80150d4efc1a8abc72f5f9b8e14", 1613 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-hyphens.mjs": "edcc22d5457cf0b7b503c60e55b5b0c951a4f0c523d523fea84b8bd88d15946c", 1614 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-image-set.mjs": "e7e7703827d9423f51956269a1a6166bca4ae457c07267ae442dc05fdcf61d67", 1615 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-logical-props.mjs": "ea1c94a9d807f68b0d40e037ebf30a290efcfb373aa7dbe272352a749efdf254", 1616 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-masks.mjs": "669925ddf125deee84c2d7adb1b87837fc537269cdd3ee51683d2ec4a1a52d23", 1617 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-media-resolution.mjs": "078b7975f25d500f87ef521fa7378e52ab97955bbba5918d7a04404107afeba0", 1618 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-overscroll-behavior.mjs": "dce019ccb76022b24143c73121c3851163256b61a2c68e6149751fd7652f54fb", 1619 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-placeholder-shown.mjs": "2b5179cefe731d536a0dd8c815e05d79726a7d7cc7061305e2eb50108cbd05f7", 1620 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-placeholder.mjs": "57df9a96cb719d02395d1f9935646e0218a1ceea5b903ac9c8c6ab890a56ccbb", 1621 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-print-color-adjust.mjs": "1b187f9fd1a418111ab3a0c76e86556a520f47123f5557010294d48dd409c13e", 1622 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-read-only-write.mjs": "2c1a671c7dacb1482cf9e2e0a0415338d3026bfe5e18c0058006fe98760d50d3", 1623 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-regions.mjs": "7222af95c5ccc737fa75507c9da87854d20ecb4c5077b1f629dc6fc6e47fc2a8", 1624 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-selection.mjs": "00e7b6132d470987b628b86a22f069832b31f8183f69b46d450802e47e1f74ea", 1625 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-shapes.mjs": "8e8d0689a39a85dbeabb92a90af7077bb939fd00e2817979c48bc7701f163ce0", 1626 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-snappoints.mjs": "d10e85353ec7514cf80111f85b359adcf476e3c7bb3572bf33e8829347075302", 1627 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-sticky.mjs": "6eae33fbcf2b2262f45bc5684001ed8736cf75b77e6f89870534c4deb7a61be6", 1628 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-text-align-last.mjs": "cd77db521f58d08224873c34a17f0c91009f0c6d09ab4568ff87cd7a564819eb", 1629 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-text-orientation.mjs": "ae59a4601580e55198ac6fe5fd4c947b6a9e03e37a276412a7ebe9592671c187", 1630 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-text-spacing.mjs": "6fe05cca556cc6b2f631aec00f9e55d7e69eda940cd781be84b41140f67b4ebb", 1631 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-transitions.mjs": "ca2f6ab6dac7f45a95edd0dc36d49df93b41db39afec2e476921e0649ec4daf8", 1632 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-width-stretch.mjs": "24ec65468afeb2d23746bf98d6e1057ae13712167d72c135a07333918e50fdb4", 1633 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css-writing-mode.mjs": "dc558b48d0c72a8b0af415edcb356a4b275214e91a57c2353eb4a1fa8ba116a5", 1634 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css3-boxsizing.mjs": "2d449b364db28710869692dd514313a198ffbda248508829c520f9f33c94142e", 1635 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css3-cursors-grab.mjs": "8b59711eec9bf66bf1b8fc05cf90150a0faaba7041e2a9e043ddbcc12807e7dd", 1636 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css3-cursors-newer.mjs": "d4d1aa096ee42dd314b903f4dd57105e7a3cb35a1162f7bed622d3015d2854b4", 1637 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/css3-tabsize.mjs": "5ffa1dd48a6e51d88f2c670da06097987114f79936fe2edde5994315613494d9", 1638 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/flexbox.mjs": "9534b126de76800b2525ec28866c6305213ef913ea733455769ffbbc2cec0794", 1639 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/font-feature.mjs": "7a980d615bf64ff3745fa5cf0905e294480783ce7c6c03194007a575adc58c8e", 1640 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/font-kerning.mjs": "b83954bf08d971e63fb497fa819c1adc648cdfa785837e6524697b9381ff3c1d", 1641 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/fullscreen.mjs": "5891ff5b7ec68b989b975ed6a503954dbe80948eba1a4f94bdb324fb6d100d68", 1642 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/intrinsic-width.mjs": "f20c350a8a3fe128c6199dd420567261f1b7725f034a9fd88f83c30152c53a54", 1643 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-css-backdrop-pseudo-element.mjs": "179776d08999cb1b2e8c4ae8e263b49763df8399d471f4909644a6e2c5d012b7", 1644 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-css-unicode-bidi-isolate-override.mjs": "54c2e15741d8258d040ca81fdd37bdc6f918efc32750980734885fe664a832f7", 1645 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-css-unicode-bidi-isolate.mjs": "24d550a606b8b4106781d8e563f05e2ada55ca7a1699be521a92a70c819666dd", 1646 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-css-unicode-bidi-plaintext.mjs": "42b7bc117feb8219fc7f7677bcb15c2efe49a3f6548611a8cca1b557c7ad0305", 1647 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-text-decoration-color.mjs": "fd03e9cf4d486b231246916f5085563ddc619d36bdfc2bf609a1c55571fb9c96", 1648 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-text-decoration-line.mjs": "db5a1a9338a7dbe3e88cd5abef6787a232e5434b64db37888d993ef6d55fa40e", 1649 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-text-decoration-shorthand.mjs": "9b738afd412e2c34b315ed70e59a69a7ab6ea274178720bf464876fe90fc38d9", 1650 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/mdn-text-decoration-style.mjs": "a0d504013edd6c6df38fe148fc799a957bf666b34510f24a1a610374675f7b78", 1651 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/multicolumn.mjs": "bd22ce2f445294b60b1a2b5776c7dc07d571accc521b44392dfe836adefceb87", 1652 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/object-fit.mjs": "39404d9bb268bae0c0c4b3083ae0d87e78795a02b0309ef5fe083b5fe3607e7e", 1653 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/pointer.mjs": "dfbdd4ee5120d8d674b2e23f260a1e0494cbab76c6b55745f7dff1e6580df69e", 1654 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/text-decoration.mjs": "8f6c7af83602e141080880a6654652aea09134d29db67b24f53ad599590a3f87", 1655 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/text-emphasis.mjs": "38205c00c7d5604831283bfe1b1a84561914dee827a61bd1d6f2bd8f4ad6c627", 1656 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/text-overflow.mjs": "7d011a8bc6868ecdc1fdaafa9f6f41f6e84838e406dd46bcb4a27bab7d5f2c48", 1657 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/text-size-adjust.mjs": "10c6dad907ea072ef5f0f1e33c1bde52f01671628f527cd769f499961ea6b6a2", 1658 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/transforms2d.mjs": "aa8f3bf911095e1aabd71aae3fa093e352a99e24f6f8da8821a78276d70efadb", 1659 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/transforms3d.mjs": "f54066b13aea8a87af40013481dd0967f930dae7c860168ec120b5feacab98a9", 1660 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/data/features/user-select-none.mjs": "3342ce1ea8ef2d0728d97ae94e620416ee068471998df49d9f0cb4d29323d90b", 1661 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/dist/unpacker/agents.mjs": "41dae44eebe85a886f99258ca97e9c75994f454581fdb3e35e0e2e7592fbf1cf", 1662 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/dist/unpacker/feature.mjs": "4534f80567e4d604455937c88d77df53f9b0b080d6e2c5f69bcae8e315a4a840", 1663 + "https://esm.sh/caniuse-lite@1.0.30001737/denonext/dist/unpacker/region.mjs": "736d74f47016712cdfc6422f6285c3fb661f42358d2cd8c43d97902f65bb06d7", 1664 + "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/agents?target=denonext": "36927a98266c6a725d25d812fea7c8aa1cc889b85f39a6a4323d9bb62d3e6e4f", 1665 + "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/feature?target=denonext": "f258799b1015a81ad8a835cd3f9300276cad296e2380d56da2df0bffd1316c6b", 1666 + "https://esm.sh/caniuse-lite@1.0.30001737/dist/unpacker/region?target=denonext": "e6aec30f45515f3e7b72249ba55d7344395f1f524179cb2dd1fc1914299bbf96", 1667 + "https://esm.sh/cssesc@3.0.0/denonext/cssesc.mjs": "c548acc0c3c5ae183898994347dc3ae57b71cea73f3df1d1643c096c84e75d65", 1668 + "https://esm.sh/cssesc@3.0.0?target=denonext": "46b22fe658d7c495f8c683cf71f88369ec073d6aef6f0f94729672791842b80c", 1669 + "https://esm.sh/didyoumean@1.2.2/denonext/didyoumean.mjs": "eaee3cce9c279c82404e5b2e2d82a6f94c439b7bff49a2643ffed05b279e1a1c", 1670 + "https://esm.sh/didyoumean@1.2.2?target=denonext": "9c183f2cb26fa0cbefe396d71a209ccd3fb48543050a839379a69fd403c3b9d6", 1671 + "https://esm.sh/dlv@1.1.3/denonext/dlv.mjs": "0223283fdcab2dae38eb62b13a9a77bc9fe6026b4b62f2a8f94cf2cd35cac82c", 1672 + "https://esm.sh/dlv@1.1.3?target=denonext": "a3e3bfa84c6b020d6b8cf93f6ac21f95ddea3c4b486e44f0af39696a6468069b", 1673 + "https://esm.sh/electron-to-chromium@1.5.209/denonext/versions.mjs": "c20511234422f9073a83e340b2e9bec41139e3ddabb1f0beae7d78f8e50bec25", 1674 + "https://esm.sh/electron-to-chromium@1.5.209/versions?target=denonext": "249492c791195b333ade914a005df4d04da72a6070149595c95dda573caa8487", 1675 + "https://esm.sh/fast-glob@3.3.3/denonext/fast-glob.mjs": "65877d873498643483aa01a3d773ebf3c5283352687e3ed451fd8e3a04473eae", 1676 + "https://esm.sh/fast-glob@3.3.3?target=denonext": "013bbfcf40dbc297f3ae57f7c85a9fa6af2029e24258e2fd69076dbf0fac7e9f", 1677 + "https://esm.sh/fastq@1.19.1/denonext/fastq.mjs": "a637080c2d32098ce78735f27be3a80a3f33d512f4e3a38b2168e6af6b7f692b", 1678 + "https://esm.sh/fastq@1.19.1?target=denonext": "232620945fc592798bd1f7de68116c945f458f9edc9125ed4ad6aa5d9dd61b88", 1679 + "https://esm.sh/fill-range@7.1.1/denonext/fill-range.mjs": "a617c46f1e0a36d1aaefdcda2790bcf05c986c5ecff805ecc615faad25ff8d02", 1680 + "https://esm.sh/fill-range@7.1.1?target=denonext": "3a1d3f856665be6a3a26a1007b55729d1eaa4142397a605b14b752458b997241", 1681 + "https://esm.sh/fraction.js@4.3.7/denonext/fraction.mjs": "df20348b8214e3af3c8ff08526c03e86fd4ecc23bc0857706278902199d11828", 1682 + "https://esm.sh/fraction.js@4.3.7?target=denonext": "9d79c0e257aa7b4cde79d03065818bcc604a3f27f33dfe53a19e2f94169db123", 1683 + "https://esm.sh/glob-parent@5.1.2/denonext/glob-parent.mjs": "656e4f93468626b7b0c388908d24132e893315386ec1e83322b74cb0dcde5d1c", 1684 + "https://esm.sh/glob-parent@5.1.2?target=denonext": "0e04ba2ebac5a052f7d5671a73c19f72407c2660bd3d5a5be577dd1ffcdfc7dc", 1685 + "https://esm.sh/glob-parent@6.0.2/denonext/glob-parent.mjs": "e629258fece4c22cb6d3a108a97c95ea8df4d735e4c79630f2a7da870bbafa23", 1686 + "https://esm.sh/glob-parent@6.0.2?target=denonext": "e30aa6b9dc68fbde857f7495eb6f05c7c41b5e5a7453be63a48cfd62269aacdb", 1687 + "https://esm.sh/graphemer@1.4.0/denonext/graphemer.mjs": "5a2c9e558258abf5f67d95a6814b6a3ef0bb3ea649ed231505f42a2863d1b317", 1688 + "https://esm.sh/graphemer@1.4.0?target=denonext": "09e56a40cab2fec5dbcce661e11517580d59b8854034d2414e4bb1576cd3254c", 1689 + "https://esm.sh/is-extglob@2.1.1/denonext/is-extglob.mjs": "adbceb33b529cc88849ee39364a0eea16088cefb795221d3be31c7d6c2b7a47d", 1690 + "https://esm.sh/is-extglob@2.1.1?target=denonext": "cc21d63edf9dcc600d613af9dc30eeac8c8ee324104dab9f978fa567ec8f3ae4", 1691 + "https://esm.sh/is-glob@4.0.3/denonext/is-glob.mjs": "4555ca21a3af8a883c44c977fd1ff03be73dd7927dea188443992d01749b845d", 1692 + "https://esm.sh/is-glob@4.0.3?target=denonext": "5fef477a765ea5f14bff33ee18156cd8398e8fba5fea6fa636ebe2280349e3ce", 1693 + "https://esm.sh/iso-datestring-validator@2.2.2/denonext/iso-datestring-validator.mjs": "ed32c001968af2e58843b98d99f95fa09d7e5b056ae4ad2a30ce0d436daa087e", 1694 + "https://esm.sh/iso-datestring-validator@2.2.2?target=denonext": "de296e64bf5da2fb01f4b909d22a6c1676fcc52a1c6f176aa5307df37eacb217", 1695 + "https://esm.sh/jiti@1.21.7/denonext/dist/babel.mjs": "95d8f0b529fdeeb7b12eacfa93d6ad80c4548b2b407022e08608c6789b8e7d9f", 1696 + "https://esm.sh/jiti@1.21.7/denonext/jiti.mjs": "75830015c5c1f473ae2014286ed7f9f66b88404641efdfef8c18731463295c7b", 1697 + "https://esm.sh/jiti@1.21.7/dist/babel?target=denonext": "238960d30e4169f10b16b90db8b8e0e3695d3d13b589c2cbde7a70d5eb8761b2", 1698 + "https://esm.sh/jiti@1.21.7?target=denonext": "8b16749a8a79eafecd65003c0a14082274620e9521e2a0137e5825ee353898e8", 1699 + "https://esm.sh/jose@5.10.0/denonext/base64url.mjs": "539aace7c49b2261745da4ec543d941618d11a294c4e7799d6face203aebfc45", 1700 + "https://esm.sh/jose@5.10.0/denonext/decode/protected_header.mjs": "73ce892cbc903722e5628006b329d836d29af384eeb99e18f4e8ad3888d5c241", 1701 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/jwt/produce.mjs": "72409fb814f902126eeca273e6c6855685ab0715f2ef95a5f61b29015862472b", 1702 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/aesgcmkw.mjs": "c0363171d95389f30ed6a8de16fdb3c8f123e1d7cb92f39011393c1038faf044", 1703 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/buffer_utils.mjs": "2b23cfebda877dbfc41c9a69438a158ffb31731d97308baa01621d73ef959f2b", 1704 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/cek.mjs": "85c4e75e70bb1da9960b6825e1bf985eb37881d7d23fa76e2bc2efb8ca071812", 1705 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/check_iv_length.mjs": "38783fdf60c458e435a42e588da9d4097fd2d096d92129673674da037d1defd8", 1706 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/check_key_type.mjs": "9d4b6990a0b8523681db4cef0bde67b957ef6f29f5708ce871225eb6f24d4e63", 1707 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/crypto_key.mjs": "59dc27357c645bbe213439acee0ef9ad481bd9ed751041ebe8801a108431aa00", 1708 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/encrypt_key_management.mjs": "35dd301ef6820956d2f5668d92786764dcbb459fae1136d5d11dae3d839a2e9d", 1709 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/epoch.mjs": "b594e6e3d563fd8b437b924b8738ce883d5d4fc1126dfd0ca230fd5c0fae02e8", 1710 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/invalid_key_input.mjs": "19a65328e952d4031c09c1315abe803ea1fdc9934f0fa4f455379bc3c072921a", 1711 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/is_disjoint.mjs": "85ebfe6f46e0feb55cd79e41626717594b2f1e997d54c9a66a255998b626bc51", 1712 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/is_jwk.mjs": "12240833a4979333cb4f8fad5f065c7cb9fff1d5e4d589a073ec99bdf2fc234d", 1713 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/is_object.mjs": "a477fb6116adc68383ea964f6e9b5542427e7638d192a23e93a683c5797ab387", 1714 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/iv.mjs": "3edfe59dfc97f12b3fff5535f95f0bf690d6dd1bd0140baf7a86d705313f09c2", 1715 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/jwt_claims_set.mjs": "67658e0bde825ea75aeecb740e34cee41b754ed0092b25f6a4e822d9d06f3d92", 1716 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/private_symbols.mjs": "5c8542caf07cd0e88f442c3cf16bf67b74108b8fa10f93f41141840fed183af9", 1717 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/secs.mjs": "aaab2389b4a39d7cdd422908aa1f60fb93b075df47acab0c4f42838ccca23c81", 1718 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/validate_algorithms.mjs": "095384ff18fb772f980bdb504474dfe87a5ec02730d5c510d59ef2d95e5efd06", 1719 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/lib/validate_crit.mjs": "176fa8b63805db36ef0eaa7490eef27c43352eec53ac2843920ab7b353282809", 1720 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/aeskw.mjs": "1ca307484491e42a7cd7be95f9a3a9a6504d433f4920f1a611f3faeb27ab9848", 1721 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/asn1.mjs": "71e4c6e67ed71454ad786e5ba25f1ed0634c527d1ef15f053116927828b9ed5b", 1722 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/base64url.mjs": "32bb04a70098ab611900258e09901161e0022265a4ff09c1f6a585f0d0dfe308", 1723 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/bogus.mjs": "26b5a2bace42941799dcb891f5676678bb2017ab64778e94923fe71a040d1719", 1724 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/check_cek_length.mjs": "b1511d0ecbc2bf90a8b50ebe9baa15eab169dfa3d3ad3a2a556474a69cb2a344", 1725 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/check_key_length.mjs": "3aec7aaa6c36c0b1c984cccfd476b310c3678dd8a5b07331e292c0a005b97798", 1726 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/decrypt.mjs": "f81f1f103e787deee890cf5d97a2c91b59a03e63efaf479c7844b0c20983a454", 1727 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/digest.mjs": "b170cf6543920f6c12d147c6a8914f8e626d686226e50af820fc0effadfa413e", 1728 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/ecdhes.mjs": "5eb732da9266ebdeb62b424dd898523ddbf4c09ca6144e53cca86793e611d65b", 1729 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/encrypt.mjs": "5a8563d9813f837850edf6e814f535e36fc6373673850ec4e75221f0de3ee90e", 1730 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/generate.mjs": "b6e73ada71e897f6f189ef2e2ffa7a4a91fbaf675d6d0aa86d6121d4c68dc07a", 1731 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/get_sign_verify_key.mjs": "8e91861e258c0fdf9988b0460a113158a01294b5541c56292314e6f345caa6a6", 1732 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/is_key_like.mjs": "43e10261870d4a65beb24cb868befc0e9aaff002b9ebf90257ffdaf073c636d7", 1733 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/jwk_to_key.mjs": "46ef0a4df4d689f2065bac957dae4e6572071797675123a08749bf83992abacc", 1734 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/normalize_key.mjs": "0e915ab84055caa0f3e031f9d5a545608e22976d22b03b35485b38611a8c282e", 1735 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/pbes2kw.mjs": "d964c662b1e8cd72e143bf13fe90eb7213ce21fa7b4aadeebba8394e0ec32db8", 1736 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/random.mjs": "1fd349b6114345bbd8d67407dcb83efca1720b9b862a4e1ca232427149a167c0", 1737 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/rsaes.mjs": "12cdf94a5b92eec19456f9df4b8ef2265cad392612345c36aa28065506cb8c51", 1738 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/subtle_dsa.mjs": "04a41e3a9ac0f77c9b34b84be050ff0a27c2d768eadb30a464d57567f857cd46", 1739 + "https://esm.sh/jose@5.10.0/denonext/dist/browser/runtime/webcrypto.mjs": "a7ca0ccea9e87fa0698d48f5a0832cb28fea137705f4c494ff3f3c83bde57cf1", 1740 + "https://esm.sh/jose@5.10.0/denonext/errors.mjs": "10bd291dfa016ffa8a5228e18bbc0c147b24c88725061bec323d1c1498bfde3b", 1741 + "https://esm.sh/jose@5.10.0/denonext/jose.mjs": "745abba7b218a451fb747ab7250eb67bf62c7eaa0d2ed98fc7db4320b3ac2371", 1742 + "https://esm.sh/jose@5.10.0/denonext/jwe/compact/decrypt.mjs": "868abeef0f4bc4bf499bbb4d64febc9121bf7f9d01132df8e19b7d7d50a0af07", 1743 + "https://esm.sh/jose@5.10.0/denonext/jwe/compact/encrypt.mjs": "f0063936a5a8f9e4c50a978ad0bb23baf842eb20dc4e4982fa5363045685871e", 1744 + "https://esm.sh/jose@5.10.0/denonext/jwe/flattened/decrypt.mjs": "49596d5a985ab0a850eb7924f53888253669f059f4687320790cbe04641ddea8", 1745 + "https://esm.sh/jose@5.10.0/denonext/jwe/flattened/encrypt.mjs": "b70bb208e1cefcd08758fe39af6e9bc46dd475b3182c209c084dd6fd6575819b", 1746 + "https://esm.sh/jose@5.10.0/denonext/jwe/general/decrypt.mjs": "16d59263b205635a9063a9c6256d92373d1e32ddf632d1ccc06a1a4c0cf769f7", 1747 + "https://esm.sh/jose@5.10.0/denonext/jwe/general/encrypt.mjs": "6acc66f0eee89603c2bb5089af9cef5f579ab38533c3e0fe49625b479736efaa", 1748 + "https://esm.sh/jose@5.10.0/denonext/jwk/embedded.mjs": "c13d8092a672bba2ce7fcf6d94804f89ce278842f829a041a1c8eef6019bb489", 1749 + "https://esm.sh/jose@5.10.0/denonext/jwk/thumbprint.mjs": "428799b05c6ab4455e0d34a2974be42053f3fbcee2af5a7c829dcfc4d457aab7", 1750 + "https://esm.sh/jose@5.10.0/denonext/jwks/local.mjs": "2f1492c3761af03bae601c48494d43b481740dcb01286461a0589f0978033696", 1751 + "https://esm.sh/jose@5.10.0/denonext/jwks/remote.mjs": "7c352605a7b5f7048f139452253ad5705db74ed7391e36bbe6be3e947b83fd83", 1752 + "https://esm.sh/jose@5.10.0/denonext/jws/compact/sign.mjs": "4f345eac0d98cf5074193e251879b67b9c6b5d4aea6648f0e19169776f4ee59f", 1753 + "https://esm.sh/jose@5.10.0/denonext/jws/compact/verify.mjs": "25d3749082196faefe6ca4b656b2aba0a3c030e21a6b8b06355459dc203549e7", 1754 + "https://esm.sh/jose@5.10.0/denonext/jws/flattened/sign.mjs": "c00c6af48d50c0d6d50490ec24666bcabb85b4f2cdef1216fd2eb7c1fff2959e", 1755 + "https://esm.sh/jose@5.10.0/denonext/jws/flattened/verify.mjs": "d5fe93eb720b2ea659be49bbf0cadcea137de0024edd5c630ab058a69691fae7", 1756 + "https://esm.sh/jose@5.10.0/denonext/jws/general/sign.mjs": "612872a9dda21c24756b95fdfd8555cd5e7a7541e996fdfa95d0f68a3545c4d8", 1757 + "https://esm.sh/jose@5.10.0/denonext/jws/general/verify.mjs": "fc43cdd11c19eb9e8636b3710d4728a36f82480c19b2e75bfd9a9f4c31f7d029", 1758 + "https://esm.sh/jose@5.10.0/denonext/jwt/decode.mjs": "40b1699324c4c97c95661ae286fff14c5b18dabe6d96343b04c31456ef73026a", 1759 + "https://esm.sh/jose@5.10.0/denonext/jwt/decrypt.mjs": "acdf2a910bb295403edc53e7fb5f098e8d8c45c59cd079082745abd9b225f22e", 1760 + "https://esm.sh/jose@5.10.0/denonext/jwt/encrypt.mjs": "3fe7a9dde4a875028bbd72828c86a1fcf82f168a8eeee69ce1662a6b2a4ea635", 1761 + "https://esm.sh/jose@5.10.0/denonext/jwt/sign.mjs": "490e0d0f9f16eb648362686ec477694a37065fc5c7a4c64c5b1e6ddb2e281248", 1762 + "https://esm.sh/jose@5.10.0/denonext/jwt/unsecured.mjs": "f20d4176b9b09f31460d141899d860b0d56d5dae4429f7625b1186ea0807489b", 1763 + "https://esm.sh/jose@5.10.0/denonext/jwt/verify.mjs": "8baef7b4f3d23422fb51192d1df07326719db00f8fc601b35463fd8515c45a3e", 1764 + "https://esm.sh/jose@5.10.0/denonext/key/export.mjs": "817e555aa3e0f87a9d4715affd72447f93e3bb0e4c8feafc16095f826759efcd", 1765 + "https://esm.sh/jose@5.10.0/denonext/key/generate/keypair.mjs": "5722cb2dc742d9e1f9eec179288294a13a7c26b8b273b25a3e28830c7017b687", 1766 + "https://esm.sh/jose@5.10.0/denonext/key/generate/secret.mjs": "74453b742576819a5f5245f7fc47617b5da766a60afd9751b5b1e14a099c9fff", 1767 + "https://esm.sh/jose@5.10.0/denonext/key/import.mjs": "e488ba70e5eee01134d352c7ba00e3eb25093252c05a63e9d6e4185b2e6836eb", 1768 + "https://esm.sh/jose@5.10.0?target=denonext": "43705c1f40e5191081bcac2851ad8265f03c23bf751f9b84260ac2874f8ce209", 1769 + "https://esm.sh/lines-and-columns@1.2.4/denonext/lines-and-columns.mjs": "785cd1f5843f3645f9304248e4cf77987a71c6e2e7207d5b5200c9ea8ed67cdc", 1770 + "https://esm.sh/lines-and-columns@1.2.4?target=denonext": "24df63eccd321681ded34cf71de26b252de505ac6fe86fff5f5fafb810f86441", 1771 + "https://esm.sh/lru-cache@10.4.3/denonext/lru-cache.mjs": "ba5ee5ff9067a3a33de530b47c0ce7e81cdff92937e5519258ed422b7e23fbdb", 1772 + "https://esm.sh/lru-cache@10.4.3?target=denonext": "e0a3f66d0dbb61c67fbec24fb29ba1b18e21dab43178cfba399481560e076382", 1773 + "https://esm.sh/merge2@1.4.1/denonext/merge2.mjs": "b7e2fe0b629b75aabc963fd1e6635d6dcf59f5f2df42111df1ced9d3d064fcec", 1774 + "https://esm.sh/merge2@1.4.1?target=denonext": "a1286ee0e809c60cde57dcd9357fe3979d10abf74324608c83eb5640088c5053", 1775 + "https://esm.sh/micromatch@4.0.8/denonext/micromatch.mjs": "3663f155546be362a116c4a6bcececb00df29ed5870b308809ddb255ade2a78b", 1776 + "https://esm.sh/micromatch@4.0.8?target=denonext": "5e66db24ea49ffe179d951391cb92d5173e4911a45a6793d189a83861dae0b2e", 1777 + "https://esm.sh/multiformats@9.9.0/bases/base64?target=denonext": "9622b652809e84b8cbdc3834d15a306da9a9305552527668b13b1b16a6d686da", 1778 + "https://esm.sh/multiformats@9.9.0/basics?target=denonext": "97fa53d99c2a3aa56367e5e2a34ab3d1b3dc496e4a4fe490460d2955de707361", 1779 + "https://esm.sh/multiformats@9.9.0/cid?target=denonext": "be24427188844af21bc0b29187229c4c6e5a681b676db65b8c7fcf5277652b4b", 1780 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base10.mjs": "07ac037675bfbbf0621e7f8fd3cfeb242d1ab0955d7e965f4f3a2daa1c369b85", 1781 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base16.mjs": "7f0c9b5860c52b54170cbc8b058fe46eee1b81f52d0908055d38d2a63ec8b721", 1782 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base2.mjs": "46527ded4d9b868600b2fd8902398a31226655790bd3c5f61ffa0bd8737b0698", 1783 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base256emoji.mjs": "7c16b9576b295024837fe5d192f0b854951ac3a7c0be1be8a3c91d5f62505066", 1784 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base32.mjs": "2c42d149c299e8b8934e51ccb284b01b113d3fe432177a55b7a781d10cfbe5b2", 1785 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base36.mjs": "52dfe773e2d2650ed87c7c353e909a0d710ed83cb01eb553858eae4879a6664d", 1786 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base58.mjs": "d45f93c89f6f8a05c7ddc132c99a2bc866d1de2e8475747b6722f6322482feab", 1787 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base64.mjs": "1365d8ab96a8998be1663e7277eebc58d3207f785c9ab53533966626f153461b", 1788 + "https://esm.sh/multiformats@9.9.0/denonext/bases/base8.mjs": "9336d2259eb06c31fd0d1e7e5ae36e0826f39b2f73639849a403933348c7526c", 1789 + "https://esm.sh/multiformats@9.9.0/denonext/bases/identity.mjs": "28acc5f7d4dfe7d5647e1c8d250540a50ba6397ff4f375bc41dc37fc5d5de510", 1790 + "https://esm.sh/multiformats@9.9.0/denonext/basics.mjs": "edd1f5f7171a026940535586a6682c88f036b972a0158d10db2a5936239182ec", 1791 + "https://esm.sh/multiformats@9.9.0/denonext/cid.mjs": "1945384d570468b0bb5bd0f394184835038d5778d701b50b6b7eb7273748d288", 1792 + "https://esm.sh/multiformats@9.9.0/denonext/codecs/json.mjs": "a31ef601f2480daa1ed34393e91f937cef522517ca7d934b86939f8d761fe077", 1793 + "https://esm.sh/multiformats@9.9.0/denonext/codecs/raw.mjs": "6d6b44e3bea526dd9930d61631709f45e9f10bf2e190dc19168e9b0297fb30dd", 1794 + "https://esm.sh/multiformats@9.9.0/denonext/esm/src/bases/base.mjs": "f0057681c3f918b72d77de38d89d1f532ae4fc78b86f35db7f617c81e7ac504f", 1795 + "https://esm.sh/multiformats@9.9.0/denonext/esm/src/bytes.mjs": "d2fa273fd87212f525dcd3863af8a3d2ca1e3ee42aca3eaee5ddd4ad6e43ba04", 1796 + "https://esm.sh/multiformats@9.9.0/denonext/esm/src/varint.mjs": "5ea3af2ab0109f1e9f4f56fe35883f593f6b047899ada90095a5ed518d635899", 1797 + "https://esm.sh/multiformats@9.9.0/denonext/hashes/digest.mjs": "fc07873514b182ae0897159ebdcad3dd3ceced04740aa76caf4b2e796ff6ca25", 1798 + "https://esm.sh/multiformats@9.9.0/denonext/hashes/hasher.mjs": "44bfd064461927c2e8f161db65053d711e59e3df4b55b76bb3f9bf4c316492ad", 1799 + "https://esm.sh/multiformats@9.9.0/denonext/hashes/identity.mjs": "43eaf0a3160c8344e2f70387d0a124f3f21363d82d3e246a7a7df4053c6199e4", 1800 + "https://esm.sh/multiformats@9.9.0/denonext/hashes/sha2.mjs": "a0fd2e20d8753f6ca30db815bb7f20a57e2a23dd691384b11d07b5a52dbec74f", 1801 + "https://esm.sh/multiformats@9.9.0/denonext/multiformats.mjs": "7449f492d80b1dcffcbbfb5599707e5f9c0e5a54694532f330b0f64defe27809", 1802 + "https://esm.sh/nanoid@3.3.11/denonext/non-secure.mjs": "1f166118c1c4b4d4b5356ef0050fed87acf28cdb8a76813803fee72188f18b30", 1803 + "https://esm.sh/nanoid@3.3.11/non-secure?target=denonext": "ac0c34cc5f9846db51a5d868ce6ee815f831a19b1d57a1b8bae9226fc8d68dec", 1804 + "https://esm.sh/node-releases@2.0.19/data/processed/envs.json?module": "62fef61ca0cdd9c90f89cdbf60280aa1ebff036ddb965f4174e69a30e6efb196", 1805 + "https://esm.sh/node-releases@2.0.19/data/release-schedule/release-schedule.json?module": "32b509aa9392ed0744a12a011b07eb5f86d1e0b361392699a20c1b70f631d6aa", 1806 + "https://esm.sh/normalize-path@3.0.0/denonext/normalize-path.mjs": "ecabfab5ffc4bce022eed2b9c50a777cc04b44b17c477eeef8790d7602f4eb93", 1807 + "https://esm.sh/normalize-path@3.0.0?target=denonext": "4fd9f901c0c9062a18ca83ede4b9122f9b3b67ce3839a86344bb8a4d98158359", 1808 + "https://esm.sh/normalize-range@0.1.2/denonext/normalize-range.mjs": "33d8ef29dd3b379c687be93ed8df5b1a001eb0fa2b16e14aa50191377c5dc18f", 1809 + "https://esm.sh/normalize-range@0.1.2?target=denonext": "10ac8bcf3a32c783d03b72181ab7f243aa17a1f7c8a7a87655e2573f2210a380", 1810 + "https://esm.sh/object-hash@3.0.0/denonext/object-hash.mjs": "a0de679d08f71d0ef12da988f1be37af4d050e5ef55e8a35d90d7d3489b3e9da", 1811 + "https://esm.sh/object-hash@3.0.0?target=denonext": "e218e75f3503f2bdd9f9b758658a59f8ce0a76c387ec49e979b5d2039373a97d", 1812 + "https://esm.sh/picocolors@1.1.1/denonext/picocolors.mjs": "fa0033734392d8b8ad0c565338a77fa835e8b00e9ef244bf6f36570dd699ea3f", 1813 + "https://esm.sh/picocolors@1.1.1?target=denonext": "97aaf037b1af74a20fe9f9f2beabba700924823101e86bdcd3ae2c9327376f74", 1814 + "https://esm.sh/picomatch@2.3.1/denonext/lib/utils.mjs": "78f1e101706724181555315f3d8b8d0bb19c602caec154ae911e8a645d1b0b47", 1815 + "https://esm.sh/picomatch@2.3.1/denonext/picomatch.mjs": "b238291b792bb898d06a490c316425f0921457a9ecb30b12909e00b47abd32ec", 1816 + "https://esm.sh/picomatch@2.3.1/lib/utils?target=denonext": "07ebea80a4b1cbb4dc99b7ae7f6dd81321eb5e4ba254cc39fe57bd5a61082b7b", 1817 + "https://esm.sh/picomatch@2.3.1?target=denonext": "e1e7cb8fa5f73ba0cefa1e19003d8adebe6c1dda6d934d656361df441ee5f4b0", 1818 + "https://esm.sh/postcss-js@4.0.1/denonext/async.mjs": "90a7602a50926d3491ea00b07135429174b88b537929703352c8128626c3cd8d", 1819 + "https://esm.sh/postcss-js@4.0.1/denonext/objectifier.mjs": "df146288008cae08140eede11a4152b8d3db59229a69b4d068a4254bd47850c3", 1820 + "https://esm.sh/postcss-js@4.0.1/denonext/parser.mjs": "de35017f0f8fef175ebf8759539fb7d323df119dde251b09d6d1904035d9bab6", 1821 + "https://esm.sh/postcss-js@4.0.1/denonext/postcss-js.mjs": "bc84d0914b55660b458011f5451d98d8c1d6c94096a866883443285482d6a5a7", 1822 + "https://esm.sh/postcss-js@4.0.1/denonext/process-result.mjs": "96840377db6a0c0917035314f38181d3f13451100a33c71f3412d458c4a2a2b5", 1823 + "https://esm.sh/postcss-js@4.0.1/denonext/sync.mjs": "abd39a2f4a9bc2b8c9c1cceeae397cf07f2d7fc016edc0c0edeb98b112bea04c", 1824 + "https://esm.sh/postcss-js@4.0.1?target=denonext": "aba48f8bfa332370cb382dcb55fa57ba1a98952f3f8a4f74f4611b10058a7e3a", 1825 + "https://esm.sh/postcss-nested@6.2.0/denonext/postcss-nested.mjs": "2c772eb1021a4642b60b6ce56739d8727c4f5f5f3f0598b2f25c3805a50c969d", 1826 + "https://esm.sh/postcss-nested@6.2.0?target=denonext": "3f977ced1f70abf693275963f2dfef7491c1e434736b966bd194b3561f3a0ab6", 1827 + "https://esm.sh/postcss-selector-parser@6.1.2/denonext/dist/util/unesc.mjs": "737a8526d9e2c2ace54928cbb4dc6205d850bd81550069faccf6f90dc1ce4d00", 1828 + "https://esm.sh/postcss-selector-parser@6.1.2/denonext/postcss-selector-parser.mjs": "67baf457834e84c652cdc2d2073d072226ba2ae531996094a145ab99df3fb689", 1829 + "https://esm.sh/postcss-selector-parser@6.1.2/dist/util/unesc?target=denonext": "c14466135461e10e035138f0e98fe4c514ed63c922e9a782d67cf6d568e3f9e6", 1830 + "https://esm.sh/postcss-selector-parser@6.1.2?target=denonext": "2537850e0eef50737c135b11ecf6d71bcf2b19daea0a26464889e276ab46b3ec", 1831 + "https://esm.sh/postcss-value-parser@4.2.0/denonext/postcss-value-parser.mjs": "2734c0eb4eb5e6a9905fb179ed2f329a346ac076735dbf0bc889276cbfe952bc", 1832 + "https://esm.sh/postcss-value-parser@4.2.0?target=denonext": "f11ba11f7fe1a5768433011193e133bf80a697154aab65f582bf653305a571d1", 1833 + "https://esm.sh/postcss@8.5.6": "d74fb26c8e5f2355773381813a2514b103cd99d9262961b6d43148f714f343da", 1834 + "https://esm.sh/postcss@8.5.6/denonext/lib/at-rule.mjs": "4d36edc3ca42766858a10199b14ea4295b76ff96ea6b1a81aa29b7b22af7dd45", 1835 + "https://esm.sh/postcss@8.5.6/denonext/lib/comment.mjs": "c697b26e5ba49d581727081fd3de61a65df93ae69f5ab1080df36bc523a3586f", 1836 + "https://esm.sh/postcss@8.5.6/denonext/lib/container.mjs": "842ed0e58a3120a02b91bcc27e0bf8fa8e82281dc48f04f72e78add92fe215a6", 1837 + "https://esm.sh/postcss@8.5.6/denonext/lib/css-syntax-error.mjs": "cde795b92e9c71048d87b903b9565e1ede241bc81e346cb0f9171c2c7825c6a4", 1838 + "https://esm.sh/postcss@8.5.6/denonext/lib/declaration.mjs": "e2fa6617ff8a74fbcae38ab0d4f2ff5417bdb5c300d7695fcd662acdfa300cda", 1839 + "https://esm.sh/postcss@8.5.6/denonext/lib/document.mjs": "39f9f35f25834ffd4cae54678faf5020eb68a9d990c1921cb1f4a16e8d96ec81", 1840 + "https://esm.sh/postcss@8.5.6/denonext/lib/fromJSON.mjs": "9f2e849366a659250ed1bfa3c67faf2cf1c38b5b2677594002498a1ee0fc594e", 1841 + "https://esm.sh/postcss@8.5.6/denonext/lib/input.mjs": "72c43722d17c332720bdcc5ca67925f08b7260643faf33a4328bf87532e9cd4d", 1842 + "https://esm.sh/postcss@8.5.6/denonext/lib/lazy-result.mjs": "61618d7d7bc6f68ce8c7e380b15317e18036e07a29c02cdda521e632559e034a", 1843 + "https://esm.sh/postcss@8.5.6/denonext/lib/list.mjs": "4a56db4aea2f2a981afdce9455086caea47e732c678d75b85cdb76df241314e0", 1844 + "https://esm.sh/postcss@8.5.6/denonext/lib/map-generator.mjs": "d609c0e2114a55cc61969344edec0c8652fdabec840bdd481e133ab04f817866", 1845 + "https://esm.sh/postcss@8.5.6/denonext/lib/no-work-result.mjs": "582acccf822251d8d664518339e62dd8fc7105bbb4488169ffe7cf5ef99fd392", 1846 + "https://esm.sh/postcss@8.5.6/denonext/lib/node.mjs": "9cda4baedeafe756b6a10e76e906720bce6da9f9b808c74fb247d85ae6b96965", 1847 + "https://esm.sh/postcss@8.5.6/denonext/lib/parse.mjs": "9095142e0c755897446b8fd8f9ac8633599e3ba44773498206cdcd36538bfa0a", 1848 + "https://esm.sh/postcss@8.5.6/denonext/lib/parser.mjs": "5fa9d73cc19247b2abb636ff39973fdb4d06149f183dc4670aa8106594b3ae36", 1849 + "https://esm.sh/postcss@8.5.6/denonext/lib/previous-map.mjs": "18e4af2bd986f9211a4961ca81e6b6d8f1ec10adbf6df20a0cbcd6eb8c696efa", 1850 + "https://esm.sh/postcss@8.5.6/denonext/lib/processor.mjs": "bef379a8c94cdb173337b51bde0a5576db39262bf7a759c3650b4471e1fa1c96", 1851 + "https://esm.sh/postcss@8.5.6/denonext/lib/result.mjs": "c1ba7aafbb6c66f9a213beb7fcd9bbc5c9bf8cf96c8d9d52af9ae6b6a4436b51", 1852 + "https://esm.sh/postcss@8.5.6/denonext/lib/root.mjs": "b4fdc0d0380774f92382e32a39f2c0901e111c58d0309084e728ae085570fa72", 1853 + "https://esm.sh/postcss@8.5.6/denonext/lib/rule.mjs": "35cb6df8a6b88a25c2cdae3c67153e1e322f0c5b2ff0af2a4b82621801818aa9", 1854 + "https://esm.sh/postcss@8.5.6/denonext/lib/stringifier.mjs": "f7d92b74abe94e471c03e4be188b0d63f1ef59b6124be4ce1c7e6673940f8a01", 1855 + "https://esm.sh/postcss@8.5.6/denonext/lib/stringify.mjs": "0199bda16f538996aacee2bc24d6425960706407bb966700c5e90e4ea6a18144", 1856 + "https://esm.sh/postcss@8.5.6/denonext/lib/symbols.mjs": "62ba62162b364f32caa44d4025486db497a84f1956b40047b7cc3d0ca336103b", 1857 + "https://esm.sh/postcss@8.5.6/denonext/lib/terminal-highlight.mjs": "261e50c0a1061a389cdd0abc7177a411f63916657a2d2493e42d2eabf8ca66e2", 1858 + "https://esm.sh/postcss@8.5.6/denonext/lib/tokenize.mjs": "8ae691fe6c8535964103e6cbb4be154916c96a80f22746daf6c595b6186b92fa", 1859 + "https://esm.sh/postcss@8.5.6/denonext/lib/warn-once.mjs": "29cca6093c98344a9731ad66d1935501bebfd35e11cc16c47b12ebdbb2835500", 1860 + "https://esm.sh/postcss@8.5.6/denonext/lib/warning.mjs": "71973428e45464675ca11b47c2e463ea00f88ce53aa12335fce71f22b81b1002", 1861 + "https://esm.sh/postcss@8.5.6/denonext/postcss.mjs": "95c0fe458dc732c2d727eb665f410662deba1982e63d7ae31eb09475e77dd0a4", 1862 + "https://esm.sh/postcss@8.5.6?target=denonext": "d74fb26c8e5f2355773381813a2514b103cd99d9262961b6d43148f714f343da", 1863 + "https://esm.sh/queue-microtask@1.2.3/denonext/queue-microtask.mjs": "b0e153a240d836527feea7b9ecfa99fa8e5a5ef7e48b43092b7cecd8649a8712", 1864 + "https://esm.sh/queue-microtask@1.2.3?target=denonext": "fc6002fc339a8edbab36e4e6df1d245a0c3a5dc507edf4e5fb7e857f8b117aa2", 1865 + "https://esm.sh/react-dom@19.1.1/client": "e610fc905fb8a0f45ef487496e6084a9c46e6a9b1b1fc9e77ae388d8a9489a9f", 1866 + "https://esm.sh/react-dom@19.1.1/denonext/cjs/react-dom-server-legacy.browser.production.mjs": "46093e4d958c744635246158bbe9d1448c1168acc16ee248a1cb744c0194cc72", 1867 + "https://esm.sh/react-dom@19.1.1/denonext/cjs/react-dom-server.browser.production.mjs": "1b65edfd064bcb0377f63e6d3d15176967ead3acd6b3aec869bd194e48522676", 1868 + "https://esm.sh/react-dom@19.1.1/denonext/client.mjs": "747fb2c2a65ba2fc0e191c6be104de92f26e7b3559425fd2bebc71decf9358c9", 1869 + "https://esm.sh/react-dom@19.1.1/denonext/react-dom.mjs": "a31239e9832b73257ff344fb3d8292982ee25cbc1da5edab9ba81648c30f4abd", 1870 + "https://esm.sh/react-dom@19.1.1/denonext/server.mjs": "513c3f84f3402259c315ae6788c2dd9e671e8dc2adfa2f37d51165e156b4932f", 1871 + "https://esm.sh/react-dom@19.1.1/server": "d8d537b44c19a05a7805396eac77f18c628e1019fb2a61cb1f1a1bce0eeeb939", 1872 + "https://esm.sh/react@19.1.1": "4583aedf2a721df5ea4aa7308de82b5bcbee12e5ed7910061940f7434982847d", 1873 + "https://esm.sh/react@19.1.1/denonext/react.mjs": "588ff2562af9e974efdb79cc945049768bf5ee0252543fd8fb6a48472f3db587", 1874 + "https://esm.sh/reusify@1.1.0/denonext/reusify.mjs": "3b91b42988a3ec3984cef34b99cd5f67b4a008609c3d4fc5d35e8f2d1aab8cef", 1875 + "https://esm.sh/reusify@1.1.0?target=denonext": "aea1fd894bd8993f740cf053799f9639ee649122854c1fe4a5425d5aa2dc5e96", 1876 + "https://esm.sh/run-parallel@1.2.0/denonext/run-parallel.mjs": "77808968c9de972bb725e73559c80e64bf9524b7d2f781941115d023fd42e7b3", 1877 + "https://esm.sh/run-parallel@1.2.0?target=denonext": "2acc47cfa930a6c5e502877b2e5ee9cd2a2ec81e4fcc579f3c0bdebe32590de6", 1878 + "https://esm.sh/scheduler@0.26.0/denonext/scheduler.mjs": "b62267951094a7c42b7170c51e31f57198981b08ebfae60be0d076101a4c6f91", 1879 + "https://esm.sh/scheduler@0.26.0?target=denonext": "102f3b685ea5271452bd93de89ab0d99668aecb08f7706e3b9c6629699dd92e0", 1880 + "https://esm.sh/source-map-js@1.2.1/denonext/source-map-js.mjs": "2129223e17c258b391a47907a69c0b86d1c87cbb66eea7e9b25ff555ec0bbc7c", 1881 + "https://esm.sh/source-map-js@1.2.1?target=denonext": "33ba5cf2ee7c1fe6b63b36c31e9075e290bc5c574055acbdc48c2f24a21d2f83", 1882 + "https://esm.sh/sucrase@3.35.0/denonext/sucrase.mjs": "1d1c05878a5f0656226ee064c90845d01adf23b2bff84521480ffaad6da16d21", 1883 + "https://esm.sh/sucrase@3.35.0?target=denonext": "2e4c449f97b0c91e2800bdb6a1192f46dfaba56fa048e0c50eb8d150bd513195", 1884 + "https://esm.sh/tailwindcss@3.4.17": "0d5c6546923ca69e2d2a1cec444214f239d2df1e5f2b90708b76043809b84c12", 1885 + "https://esm.sh/tailwindcss@3.4.17/denonext/tailwindcss.mjs": "382a6a4ed82273daead6e2eb6af426822a3fbf7303387ee49db16be7699f8b45", 1886 + "https://esm.sh/tailwindcss@4.1.12/denonext/plugin.mjs": "349369852f7afd8ca09ef1682d94766d02e84bc45f927d0f02b374b20be70c9f", 1887 + "https://esm.sh/tailwindcss@4.1.12/plugin?target=denonext": "05b784ebe25c01018a0eab3fe1e1dab62bdbceb114e2371e748f967552510c00", 1888 + "https://esm.sh/tlds@1.259.0/denonext/tlds.mjs": "574135861b248c619a791bd6a1769ee6b5b45d1d7e11f8d052c8e0e43ae6e009", 1889 + "https://esm.sh/tlds@1.259.0?target=denonext": "9a5c7ee3354f90209f3fee0255a201ec5c9f46d28dc500ac161325c945af7389", 1890 + "https://esm.sh/to-regex-range@5.0.1/denonext/to-regex-range.mjs": "3f099b66f908b58dd6ee20afd1fed4616ea02fd0c118dc91ff1ed3e72d795bc7", 1891 + "https://esm.sh/to-regex-range@5.0.1?target=denonext": "5d6ed8b93f9fc4009378f84ade74e2681c2264b37e79f978c557037c612086ed", 1892 + "https://esm.sh/ts-interface-checker@0.1.13/denonext/ts-interface-checker.mjs": "482a9d9214862af127455b80dbe15559b107bb8283dc4b6ba10da16757b3dfe8", 1893 + "https://esm.sh/ts-interface-checker@0.1.13?target=denonext": "b1c4bedafbacf4fc6ca1b02c22601fe4e3dee3d3dc14ca3e8fb6f8656a368e72", 1894 + "https://esm.sh/uint8arrays@3.0.0/denonext/compare.mjs": "2b8ed67b92836546e504b87733eacd3f0569050ea645920d3f2503e8e335cd01", 1895 + "https://esm.sh/uint8arrays@3.0.0/denonext/concat.mjs": "ce3cfbcdd3cc6d1d9fa75f13963c7398a377d8a1dbd6302820f2058f42545fc7", 1896 + "https://esm.sh/uint8arrays@3.0.0/denonext/equals.mjs": "2c9a9504f97abc172b755fb5069cb9b69aac2e177ad4bc2cf9afdd6a4a322694", 1897 + "https://esm.sh/uint8arrays@3.0.0/denonext/esm/src/util/bases.mjs": "03de06f47b410a6ed09da73d6e58696ca34ac4e51bce4a0bcfac5cf88c23b8da", 1898 + "https://esm.sh/uint8arrays@3.0.0/denonext/from-string.mjs": "6df06b3ed43db82fa33500d4ab01a97ae4ecef234f76e06c1774b915d913bbe3", 1899 + "https://esm.sh/uint8arrays@3.0.0/denonext/to-string.mjs": "bafce61afad1706118c9c6bd08d6d2f53dada2c0892b8fe14da8124e5951390c", 1900 + "https://esm.sh/uint8arrays@3.0.0/denonext/uint8arrays.mjs": "2627bbf05b4b496ffe067a4f090dd91d4aa7bc8c815d36f1e543531fb0d9342f", 1901 + "https://esm.sh/uint8arrays@3.0.0/denonext/xor.mjs": "cc93f46198dca299adc26ae5344768914923bbc88e78eefd28699d0ec28c8e71", 1902 + "https://esm.sh/util-deprecate@1.0.2/denonext/util-deprecate.mjs": "083639894972cb68837eef26346c43bdd01357977149e0a4493f76192a4008b8", 1903 + "https://esm.sh/util-deprecate@1.0.2?target=denonext": "859f4df8ba771a4c33143185d3db6a7edb824fab1ed4f9a4b96ac0e6bc3ef1a4", 1904 + "https://esm.sh/zod@3.25.76/denonext/zod.mjs": "9f643d0cc560840b7fa3b6e6fc3a5a41595f12ff8863cac0ccbae3ec98989284", 1905 + "https://esm.sh/zod@3.25.76?target=denonext": "7d2e5b7450d6f99b9b3b228958bd1291d5a1f8378f82b8cdd2ce83b2f14961c2" 1163 1906 }, 1164 1907 "workspace": { 1165 1908 "dependencies": [
docs/.nojekyll

This is a binary file and will not be displayed.

+18
docs/README.md
··· 1 + # Skylite 2 + 3 + > An attempt at splitting the (Bluesky) AppView 4 + 5 + - Uses [Microcosm](https://microcosm.blue/) 6 + - Portable with [SQLite](https://jsr.io/@db/sqlite) and [Deno](https://deno.land/) 7 + - Built with [type safety](https://www.npmjs.com/package/@atproto/lex-cli) 8 + 9 + ![tldrawgraph.png](assets/tldrawgraph.png) 10 + 11 + read more about the structure of Skylite in [structure.md](structure.md) 12 + 13 + # Skylite Dev Docs 14 + skylite dev docs is a documentation website to host all of the notes for developing skylite 15 + 16 + all lexicons are listed in the [Lexicons](httpindex) section 17 + 18 + more notes in [Dev Notes](notesindex) section
+11
docs/_coverpage.md
··· 1 + <!-- ![logo](_media/icon.svg) --> 2 + 3 + # Skylite Dev Docs <small>pre alpha</small> 4 + 5 + > Documentation for Designing and Developing Skylite 6 + 7 + docs are still being written! 8 + 9 + [Git Repo](https://tangled.sh/@whey.party/skylite) 10 + [Dev Notes](notesindex.md) 11 + [Lexicons](httpindex.md)
+4
docs/_navbar.md
··· 1 + - [Home](/) 2 + - [Dev Notes](notesindex.md) 3 + - [Lexicons](httpindex.md) 4 + - [Git Repo](https://tangled.sh/@whey.party/skylite)
+27
docs/_sidebar.md
··· 1 + - [Home](/) 2 + - [Dev Notes](notesindex.md) 3 + - [todo](todo.md) 4 + - [Structure](structure.md) 5 + - [Indexing](indexing.md) 6 + - [Lexicons](httpindex.md) 7 + - [Why?](whylexicons.md) 8 + - [app.bsky.actor.getProfile](app.bsky.actor.getProfile.md) 9 + - [app.bsky.actor.getProfiles](app.bsky.actor.getProfiles.md) 10 + - [app.bsky.feed.getActorFeeds](app.bsky.feed.getActorFeeds.md) 11 + - [app.bsky.feed.getFeedGenerator](app.bsky.feed.getFeedGenerator.md) 12 + - [app.bsky.feed.getFeedGenerators](app.bsky.feed.getFeedGenerators.md) 13 + - [app.bsky.feed.getPosts](app.bsky.feed.getPosts.md) 14 + - [custom defs](customdefs.md) 15 + - [party.whey.app.bsky.feed.getActorLikesPartial](party.whey.app.bsky.feed.getActorLikesPartial.md) 16 + - [party.whey.app.bsky.feed.getAuthorFeedPartial](party.whey.app.bsky.feed.getAuthorFeedPartial.md) 17 + - [party.whey.app.bsky.feed.getLikesPartial](party.whey.app.bsky.feed.getLikesPartial.md) 18 + - [party.whey.app.bsky.feed.getPostThreadPartial](party.whey.app.bsky.feed.getPostThreadPartial.md) 19 + - [party.whey.app.bsky.feed.getQuotesPartial](party.whey.app.bsky.feed.getQuotesPartial.md) 20 + - [party.whey.app.bsky.feed.getRepostedByPartial](party.whey.app.bsky.feed.getRepostedByPartial.md) 21 + <!-- idk about these --> 22 + - [party.whey.app.bsky.feed.getListFeedPartial](party.whey.app.bsky.feed.getListFeedPartial.md) 23 + <!-- not implemented yet --> 24 + - [app.bsky.graph.getLists](app.bsky.graph.getLists.md) 25 + - [app.bsky.graph.getList](app.bsky.graph.getList.md) 26 + - [app.bsky.graph.getActorStarterPacks](app.bsky.graph.getActorStarterPacks.md) 27 + - [Git Repo](https://tangled.sh/@whey.party/skylite)
+35
docs/app.bsky.actor.getProfile.md
··· 1 + ## app.bsky.actor.getProfile 2 + 3 + ```js 4 + { 5 + "lexicon": 1, 6 + "id": "app.bsky.actor.getProfile", 7 + "defs": { 8 + "main": { 9 + "type": "query", 10 + "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth.", 11 + "parameters": { 12 + "type": "params", 13 + "required": [ 14 + "actor" 15 + ], 16 + "properties": { 17 + "actor": { 18 + "type": "string", 19 + "format": "at-identifier", 20 + "description": "Handle or DID of account to fetch profile of." 21 + } 22 + } 23 + }, 24 + "output": { 25 + "encoding": "application/json", 26 + "schema": { 27 + "type": "ref", 28 + "ref": "app.bsky.actor.defs#profileViewDetailed" 29 + } 30 + } 31 + } 32 + } 33 + } 34 + ``` 35 + ---
+50
docs/app.bsky.actor.getProfiles.md
··· 1 + 2 + ## app.bsky.actor.getProfiles 3 + 4 + ```js 5 + { 6 + "lexicon": 1, 7 + "id": "app.bsky.actor.getProfiles", 8 + "defs": { 9 + "main": { 10 + "type": "query", 11 + "description": "Get detailed profile views of multiple actors.", 12 + "parameters": { 13 + "type": "params", 14 + "required": [ 15 + "actors" 16 + ], 17 + "properties": { 18 + "actors": { 19 + "type": "array", 20 + "items": { 21 + "type": "string", 22 + "format": "at-identifier" 23 + }, 24 + "maxLength": 25 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "profiles" 34 + ], 35 + "properties": { 36 + "profiles": { 37 + "type": "array", 38 + "items": { 39 + "type": "ref", 40 + "ref": "app.bsky.actor.defs#profileViewDetailed" 41 + } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + } 48 + } 49 + ``` 50 + ---
+57
docs/app.bsky.feed.getActorFeeds.md
··· 1 + ## app.bsky.feed.getActorFeeds 2 + 3 + ```js 4 + { 5 + "lexicon": 1, 6 + "id": "app.bsky.feed.getActorFeeds", 7 + "defs": { 8 + "main": { 9 + "type": "query", 10 + "description": "Get a list of feeds (feed generator records) created by the actor (in the actor's repo).", 11 + "parameters": { 12 + "type": "params", 13 + "required": [ 14 + "actor" 15 + ], 16 + "properties": { 17 + "actor": { 18 + "type": "string", 19 + "format": "at-identifier" 20 + }, 21 + "limit": { 22 + "type": "integer", 23 + "minimum": 1, 24 + "maximum": 100, 25 + "default": 50 26 + }, 27 + "cursor": { 28 + "type": "string" 29 + } 30 + } 31 + }, 32 + "output": { 33 + "encoding": "application/json", 34 + "schema": { 35 + "type": "object", 36 + "required": [ 37 + "feeds" 38 + ], 39 + "properties": { 40 + "cursor": { 41 + "type": "string" 42 + }, 43 + "feeds": { 44 + "type": "array", 45 + "items": { 46 + "type": "ref", 47 + "ref": "app.bsky.feed.defs#generatorView" 48 + } 49 + } 50 + } 51 + } 52 + } 53 + } 54 + } 55 + } 56 + ``` 57 + ---
+54
docs/app.bsky.feed.getFeedGenerator.md
··· 1 + 2 + ## app.bsky.feed.getFeedGenerator 3 + 4 + ```js 5 + { 6 + "lexicon": 1, 7 + "id": "app.bsky.feed.getFeedGenerator", 8 + "defs": { 9 + "main": { 10 + "type": "query", 11 + "description": "Get information about a feed generator. Implemented by AppView.", 12 + "parameters": { 13 + "type": "params", 14 + "required": [ 15 + "feed" 16 + ], 17 + "properties": { 18 + "feed": { 19 + "type": "string", 20 + "format": "at-uri", 21 + "description": "AT-URI of the feed generator record." 22 + } 23 + } 24 + }, 25 + "output": { 26 + "encoding": "application/json", 27 + "schema": { 28 + "type": "object", 29 + "required": [ 30 + "view", 31 + "isOnline", 32 + "isValid" 33 + ], 34 + "properties": { 35 + "view": { 36 + "type": "ref", 37 + "ref": "app.bsky.feed.defs#generatorView" 38 + }, 39 + "isOnline": { 40 + "type": "boolean", 41 + "description": "Indicates whether the feed generator service has been online recently, or else seems to be inactive." 42 + }, 43 + "isValid": { 44 + "type": "boolean", 45 + "description": "Indicates whether the feed generator service is compatible with the record declaration." 46 + } 47 + } 48 + } 49 + } 50 + } 51 + } 52 + } 53 + ``` 54 + ---
+50
docs/app.bsky.feed.getFeedGenerators.md
··· 1 + 2 + 3 + ## app.bsky.feed.getFeedGenerators 4 + 5 + ```js 6 + { 7 + "lexicon": 1, 8 + "id": "app.bsky.feed.getFeedGenerators", 9 + "defs": { 10 + "main": { 11 + "type": "query", 12 + "description": "Get information about a list of feed generators.", 13 + "parameters": { 14 + "type": "params", 15 + "required": [ 16 + "feeds" 17 + ], 18 + "properties": { 19 + "feeds": { 20 + "type": "array", 21 + "items": { 22 + "type": "string", 23 + "format": "at-uri" 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "feeds" 34 + ], 35 + "properties": { 36 + "feeds": { 37 + "type": "array", 38 + "items": { 39 + "type": "ref", 40 + "ref": "app.bsky.feed.defs#generatorView" 41 + } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + } 48 + } 49 + ``` 50 + ---
+52
docs/app.bsky.feed.getPosts.md
··· 1 + 2 + 3 + ## app.bsky.feed.getPosts 4 + 5 + ```js 6 + { 7 + "lexicon": 1, 8 + "id": "app.bsky.feed.getPosts", 9 + "defs": { 10 + "main": { 11 + "type": "query", 12 + "description": "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'.", 13 + "parameters": { 14 + "type": "params", 15 + "required": [ 16 + "uris" 17 + ], 18 + "properties": { 19 + "uris": { 20 + "type": "array", 21 + "description": "List of post AT-URIs to return hydrated views for.", 22 + "items": { 23 + "type": "string", 24 + "format": "at-uri" 25 + }, 26 + "maxLength": 25 27 + } 28 + } 29 + }, 30 + "output": { 31 + "encoding": "application/json", 32 + "schema": { 33 + "type": "object", 34 + "required": [ 35 + "posts" 36 + ], 37 + "properties": { 38 + "posts": { 39 + "type": "array", 40 + "items": { 41 + "type": "ref", 42 + "ref": "app.bsky.feed.defs#postView" 43 + } 44 + } 45 + } 46 + } 47 + } 48 + } 49 + } 50 + } 51 + ``` 52 + ---
docs/assets/indexapistatus.png

This is a binary file and will not be displayed.

docs/assets/tldrawgraph.png

This is a binary file and will not be displayed.

docs/assets/viewapistatus.png

This is a binary file and will not be displayed.

+191
docs/customdefs.md
··· 1 + ## party.whey.app.bsky.actor.defs 2 + 3 + ```js 4 + { 5 + "lexicon": 1, 6 + "id": "party.whey.app.bsky.actor.defs", 7 + "defs": { 8 + "profileViewBasicRef": { 9 + "type": "object", 10 + "required": [ 11 + "did" 12 + ], 13 + "properties": { 14 + "did": { 15 + "type": "string", 16 + "format": "did" 17 + } 18 + } 19 + }, 20 + "profileViewRef": { 21 + "type": "object", 22 + "required": [ 23 + "did" 24 + ], 25 + "properties": { 26 + "did": { 27 + "type": "string", 28 + "format": "did" 29 + } 30 + } 31 + }, 32 + "profileViewDetailedRef": { 33 + "type": "object", 34 + "required": [ 35 + "did" 36 + ], 37 + "properties": { 38 + "did": { 39 + "type": "string", 40 + "format": "did" 41 + } 42 + } 43 + } 44 + } 45 + } 46 + ``` 47 + --- 48 + 49 + ## party.whey.app.bsky.feed.defs 50 + 51 + ```js 52 + { 53 + "lexicon": 1, 54 + "id": "party.whey.app.bsky.feed.defs", 55 + "defs": { 56 + "postViewRef": { 57 + "type": "object", 58 + "description": "A pointer to a app.bsky.feed.defs#postView", 59 + "required": [ 60 + "uri", 61 + "cid" 62 + ], 63 + "properties": { 64 + "uri": { 65 + "type": "string", 66 + "format": "at-uri" 67 + }, 68 + "cid": { 69 + "type": "string", 70 + "format": "cid" 71 + } 72 + } 73 + }, 74 + "feedViewPostRef": { 75 + "type": "object", 76 + "required": [ 77 + "post" 78 + ], 79 + "properties": { 80 + "post": { 81 + "type": "union", 82 + "refs": [ 83 + "app.bsky.feed.defs#postView", 84 + "#postViewRef" 85 + ] 86 + }, 87 + "reply": { 88 + "type": "union", 89 + "refs": [ 90 + "app.bsky.feed.defs#replyRef", 91 + "#replyRef" 92 + ] 93 + }, 94 + "reason": { 95 + "type": "union", 96 + "refs": [ 97 + "app.bsky.feed.defs#reasonRepost", 98 + "app.bsky.feed.defs#reasonPin" 99 + ] 100 + }, 101 + "feedContext": { 102 + "type": "string", 103 + "description": "Context provided by feed generator that may be passed back alongside interactions.", 104 + "maxLength": 2000 105 + }, 106 + "reqId": { 107 + "type": "string", 108 + "description": "Unique identifier per request that may be passed back alongside interactions.", 109 + "maxLength": 100 110 + } 111 + } 112 + }, 113 + "replyRef": { 114 + "type": "object", 115 + "required": [ 116 + "root", 117 + "parent" 118 + ], 119 + "properties": { 120 + "root": { 121 + "type": "union", 122 + "refs": [ 123 + "#postViewRef", 124 + "app.bsky.feed.defs#postView", 125 + "app.bsky.feed.defs#notFoundPost", 126 + "app.bsky.feed.defs#blockedPost" 127 + ] 128 + }, 129 + "parent": { 130 + "type": "union", 131 + "refs": [ 132 + "#postViewRef", 133 + "app.bsky.feed.defs#postView", 134 + "app.bsky.feed.defs#notFoundPost", 135 + "app.bsky.feed.defs#blockedPost" 136 + ] 137 + }, 138 + "grandparentAuthor": { 139 + "type": "union", 140 + "refs": [ 141 + "party.whey.app.bsky.actor.defs#profileViewBasicRef", 142 + "app.bsky.actor.defs#profileViewBasic" 143 + ], 144 + "description": "When parent is a reply to another post, this is the author of that post." 145 + } 146 + } 147 + }, 148 + "threadViewPostRef": { 149 + "type": "object", 150 + "required": [ 151 + "post" 152 + ], 153 + "properties": { 154 + "post": { 155 + "type": "union", 156 + "refs": [ 157 + "#postViewRef", 158 + "app.bsky.feed.defs#postView" 159 + ] 160 + }, 161 + "parent": { 162 + "type": "union", 163 + "refs": [ 164 + "#threadViewPostRef", 165 + "app.bsky.feed.defs#threadViewPost", 166 + "app.bsky.feed.defs#notFoundPost", 167 + "app.bsky.feed.defs#blockedPost" 168 + ] 169 + }, 170 + "replies": { 171 + "type": "array", 172 + "items": { 173 + "type": "union", 174 + "refs": [ 175 + "#threadViewPostRef", 176 + "app.bsky.feed.defs#threadViewPost", 177 + "app.bsky.feed.defs#notFoundPost", 178 + "app.bsky.feed.defs#blockedPost" 179 + ] 180 + } 181 + }, 182 + "threadContext": { 183 + "type": "ref", 184 + "ref": "app.bsky.feed.defs#threadContext" 185 + } 186 + } 187 + } 188 + } 189 + } 190 + ``` 191 + ---
+21
docs/httpindex.md
··· 1 + # Lexicons 2 + - [Why?](whylexicons.md) 3 + - [app.bsky.actor.getProfile](app.bsky.actor.getProfile.md) 4 + - [app.bsky.actor.getProfiles](app.bsky.actor.getProfiles.md) 5 + - [app.bsky.feed.getActorFeeds](app.bsky.feed.getActorFeeds.md) 6 + - [app.bsky.feed.getFeedGenerator](app.bsky.feed.getFeedGenerator.md) 7 + - [app.bsky.feed.getFeedGenerators](app.bsky.feed.getFeedGenerators.md) 8 + - [app.bsky.feed.getPosts](app.bsky.feed.getPosts.md) 9 + - [custom defs](customdefs.md) 10 + - [party.whey.app.bsky.feed.getActorLikesPartial](party.whey.app.bsky.feed.getActorLikesPartial.md) 11 + - [party.whey.app.bsky.feed.getAuthorFeedPartial](party.whey.app.bsky.feed.getAuthorFeedPartial.md) 12 + - [party.whey.app.bsky.feed.getLikesPartial](party.whey.app.bsky.feed.getLikesPartial.md) 13 + - [party.whey.app.bsky.feed.getPostThreadPartial](party.whey.app.bsky.feed.getPostThreadPartial.md) 14 + - [party.whey.app.bsky.feed.getQuotesPartial](party.whey.app.bsky.feed.getQuotesPartial.md) 15 + - [party.whey.app.bsky.feed.getRepostedByPartial](party.whey.app.bsky.feed.getRepostedByPartial.md) 16 + <!-- idk about these --> 17 + - [party.whey.app.bsky.feed.getListFeedPartial](party.whey.app.bsky.feed.getListFeedPartial.md) 18 + <!-- not implemented yet --> 19 + - [app.bsky.graph.getLists](app.bsky.graph.getLists.md) 20 + - [app.bsky.graph.getList](app.bsky.graph.getList.md) 21 + - [app.bsky.graph.getActorStarterPacks](app.bsky.graph.getActorStarterPacks.md)
+48
docs/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <title>Document</title> 6 + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 7 + <meta name="description" content="Description" /> 8 + <meta 9 + name="viewport" 10 + content="width=device-width, initial-scale=1.0, minimum-scale=1.0" 11 + /> 12 + <link rel="stylesheet" media="(prefers-color-scheme: light)" href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple.css"> 13 + <link rel="stylesheet" media="(prefers-color-scheme: dark)" href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple-dark.css"> 14 + <style> 15 + .cover.show{ 16 + max-height: 50dvh; 17 + height: 50dvh; 18 + min-height: 400px; 19 + } 20 + .app-nav{ 21 + position: fixed; 22 + } 23 + section.show+main>aside{ 24 + display: none; 25 + } 26 + section.show+main>.sidebar-toggle{ 27 + display: none; 28 + } 29 + section.show+main>.content{ 30 + margin-left: 0; 31 + } 32 + </style> 33 + </head> 34 + <body> 35 + <div id="app"></div> 36 + <script> 37 + window.$docsify = { 38 + name: "Skylite Dev Docs", 39 + //repo: "https://tangled.sh/@whey.party/skylite", 40 + loadSidebar: true, 41 + loadNavbar: true, 42 + coverpage: true, 43 + }; 44 + </script> 45 + <!-- Docsify v4 --> 46 + <script src="//cdn.jsdelivr.net/npm/docsify@4"></script> 47 + </body> 48 + </html>
+1
docs/indexing.md
··· 1 + # Indexing
+6
docs/notesindex.md
··· 1 + # Development Notes 2 + skylite is a large project and im scared of doing it without docs 3 + 4 + i just need a place to put all of my thoughts in a structured format 5 + 6 + and maybe it might be useful to make it available publicly? idk
+68
docs/party.whey.app.bsky.feed.getActorLikesPartial.md
··· 1 + ## party.whey.app.bsky.feed.getActorLikesPartial 2 + 3 + ```js 4 + { 5 + "lexicon": 1, 6 + "id": "party.whey.app.bsky.feed.getActorLikesPartial", 7 + "defs": { 8 + "main": { 9 + "type": "query", 10 + "description": "Get a list of posts liked by an actor. May require auth, (if so, actor must be the requesting account).", 11 + "parameters": { 12 + "type": "params", 13 + "required": [ 14 + "actor" 15 + ], 16 + "properties": { 17 + "actor": { 18 + "type": "string", 19 + "format": "at-identifier" 20 + }, 21 + "limit": { 22 + "type": "integer", 23 + "minimum": 1, 24 + "maximum": 100, 25 + "default": 50 26 + }, 27 + "cursor": { 28 + "type": "string" 29 + } 30 + } 31 + }, 32 + "output": { 33 + "encoding": "application/json", 34 + "schema": { 35 + "type": "object", 36 + "required": [ 37 + "feed" 38 + ], 39 + "properties": { 40 + "cursor": { 41 + "type": "string" 42 + }, 43 + "feed": { 44 + "type": "array", 45 + "items": { 46 + "type": "union", 47 + "refs": [ 48 + "party.whey.app.bsky.feed.defs#feedViewPostRef", 49 + "app.bsky.feed.defs#feedViewPost" 50 + ] 51 + } 52 + } 53 + } 54 + } 55 + }, 56 + "errors": [ 57 + { 58 + "name": "BlockedActor" 59 + }, 60 + { 61 + "name": "BlockedByActor" 62 + } 63 + ] 64 + } 65 + } 66 + } 67 + ``` 68 + ---
+85
docs/party.whey.app.bsky.feed.getAuthorFeedPartial.md
··· 1 + 2 + ## party.whey.app.bsky.feed.getAuthorFeedPartial 3 + 4 + ```js 5 + { 6 + "lexicon": 1, 7 + "id": "party.whey.app.bsky.feed.getAuthorFeedPartial", 8 + "defs": { 9 + "main": { 10 + "type": "query", 11 + "description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth.", 12 + "parameters": { 13 + "type": "params", 14 + "required": [ 15 + "actor" 16 + ], 17 + "properties": { 18 + "actor": { 19 + "type": "string", 20 + "format": "at-identifier" 21 + }, 22 + "limit": { 23 + "type": "integer", 24 + "minimum": 1, 25 + "maximum": 100, 26 + "default": 50 27 + }, 28 + "cursor": { 29 + "type": "string" 30 + }, 31 + "filter": { 32 + "type": "string", 33 + "description": "Combinations of post/repost types to include in response.", 34 + "knownValues": [ 35 + "posts_with_replies", 36 + "posts_no_replies", 37 + "posts_with_media", 38 + "posts_and_author_threads", 39 + "posts_with_video" 40 + ], 41 + "default": "posts_with_replies" 42 + }, 43 + "includePins": { 44 + "type": "boolean", 45 + "default": false 46 + } 47 + } 48 + }, 49 + "output": { 50 + "encoding": "application/json", 51 + "schema": { 52 + "type": "object", 53 + "required": [ 54 + "feed" 55 + ], 56 + "properties": { 57 + "cursor": { 58 + "type": "string" 59 + }, 60 + "feed": { 61 + "type": "array", 62 + "items": { 63 + "type": "union", 64 + "refs": [ 65 + "party.whey.app.bsky.feed.defs#feedViewPostRef", 66 + "app.bsky.feed.defs#feedViewPost" 67 + ] 68 + } 69 + } 70 + } 71 + } 72 + }, 73 + "errors": [ 74 + { 75 + "name": "BlockedActor" 76 + }, 77 + { 78 + "name": "BlockedByActor" 79 + } 80 + ] 81 + } 82 + } 83 + } 84 + ``` 85 + ---
+95
docs/party.whey.app.bsky.feed.getLikesPartial.md
··· 1 + 2 + ## party.whey.app.bsky.feed.getLikesPartial 3 + 4 + ```js 5 + { 6 + "lexicon": 1, 7 + "id": "party.whey.app.bsky.feed.getLikesPartial", 8 + "defs": { 9 + "main": { 10 + "type": "query", 11 + "description": "Get like records which reference a subject (by AT-URI and CID).", 12 + "parameters": { 13 + "type": "params", 14 + "required": [ 15 + "uri" 16 + ], 17 + "properties": { 18 + "uri": { 19 + "type": "string", 20 + "format": "at-uri", 21 + "description": "AT-URI of the subject (eg, a post record)." 22 + }, 23 + "cid": { 24 + "type": "string", 25 + "format": "cid", 26 + "description": "CID of the subject record (aka, specific version of record), to filter likes." 27 + }, 28 + "limit": { 29 + "type": "integer", 30 + "minimum": 1, 31 + "maximum": 100, 32 + "default": 50 33 + }, 34 + "cursor": { 35 + "type": "string" 36 + } 37 + } 38 + }, 39 + "output": { 40 + "encoding": "application/json", 41 + "schema": { 42 + "type": "object", 43 + "required": [ 44 + "uri", 45 + "likes" 46 + ], 47 + "properties": { 48 + "uri": { 49 + "type": "string", 50 + "format": "at-uri" 51 + }, 52 + "cid": { 53 + "type": "string", 54 + "format": "cid" 55 + }, 56 + "cursor": { 57 + "type": "string" 58 + }, 59 + "likes": { 60 + "type": "array", 61 + "items": { 62 + "type": "ref", 63 + "ref": "#like" 64 + } 65 + } 66 + } 67 + } 68 + } 69 + }, 70 + "like": { 71 + "type": "object", 72 + "required": [ 73 + "indexedAt", 74 + "createdAt", 75 + "actor" 76 + ], 77 + "properties": { 78 + "indexedAt": { 79 + "type": "string", 80 + "format": "datetime" 81 + }, 82 + "createdAt": { 83 + "type": "string", 84 + "format": "datetime" 85 + }, 86 + "actor": { 87 + "type": "ref", 88 + "ref": "party.whey.app.bsky.actor.defs#profileViewRef" 89 + } 90 + } 91 + } 92 + } 93 + } 94 + ``` 95 + ---
+65
docs/party.whey.app.bsky.feed.getListFeedPartial.md
··· 1 + 2 + 3 + ## party.whey.app.bsky.feed.getListFeedPartial 4 + 5 + ```js 6 + { 7 + "lexicon": 1, 8 + "id": "party.whey.app.bsky.feed.getListFeedPartial", 9 + "defs": { 10 + "main": { 11 + "type": "query", 12 + "description": "Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth.", 13 + "parameters": { 14 + "type": "params", 15 + "required": [ 16 + "list" 17 + ], 18 + "properties": { 19 + "list": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "Reference (AT-URI) to the list record." 23 + }, 24 + "limit": { 25 + "type": "integer", 26 + "minimum": 1, 27 + "maximum": 100, 28 + "default": 50 29 + }, 30 + "cursor": { 31 + "type": "string" 32 + } 33 + } 34 + }, 35 + "output": { 36 + "encoding": "application/json", 37 + "schema": { 38 + "type": "object", 39 + "required": [ 40 + "feed" 41 + ], 42 + "properties": { 43 + "cursor": { 44 + "type": "string" 45 + }, 46 + "feed": { 47 + "type": "array", 48 + "items": { 49 + "type": "ref", 50 + "ref": "party.whey.app.bsky.feed.defs#feedViewPostRef" 51 + } 52 + } 53 + } 54 + } 55 + }, 56 + "errors": [ 57 + { 58 + "name": "UnknownList" 59 + } 60 + ] 61 + } 62 + } 63 + } 64 + ``` 65 + ---
+73
docs/party.whey.app.bsky.feed.getPostThreadPartial.md
··· 1 + 2 + 3 + ## party.whey.app.bsky.feed.getPostThreadPartial 4 + 5 + ```js 6 + { 7 + "lexicon": 1, 8 + "id": "party.whey.app.bsky.feed.getPostThreadPartial", 9 + "defs": { 10 + "main": { 11 + "type": "query", 12 + "description": "Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests.", 13 + "parameters": { 14 + "type": "params", 15 + "required": [ 16 + "uri" 17 + ], 18 + "properties": { 19 + "uri": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "Reference (AT-URI) to post record." 23 + }, 24 + "depth": { 25 + "type": "integer", 26 + "description": "How many levels of reply depth should be included in response.", 27 + "default": 6, 28 + "minimum": 0, 29 + "maximum": 1000 30 + }, 31 + "parentHeight": { 32 + "type": "integer", 33 + "description": "How many levels of parent (and grandparent, etc) post to include.", 34 + "default": 80, 35 + "minimum": 0, 36 + "maximum": 1000 37 + } 38 + } 39 + }, 40 + "output": { 41 + "encoding": "application/json", 42 + "schema": { 43 + "type": "object", 44 + "required": [ 45 + "thread" 46 + ], 47 + "properties": { 48 + "thread": { 49 + "type": "union", 50 + "refs": [ 51 + "party.whey.app.bsky.feed.defs#threadViewPostRef", 52 + "app.bsky.feed.defs#threadViewPost", 53 + "app.bsky.feed.defs#notFoundPost", 54 + "app.bsky.feed.defs#blockedPost" 55 + ] 56 + }, 57 + "threadgate": { 58 + "type": "ref", 59 + "ref": "app.bsky.feed.defs#threadgateView" 60 + } 61 + } 62 + } 63 + }, 64 + "errors": [ 65 + { 66 + "name": "NotFound" 67 + } 68 + ] 69 + } 70 + } 71 + } 72 + ``` 73 + ---
+77
docs/party.whey.app.bsky.feed.getQuotesPartial.md
··· 1 + 2 + 3 + ## party.whey.app.bsky.feed.getQuotesPartial 4 + 5 + ```js 6 + { 7 + "lexicon": 1, 8 + "id": "party.whey.app.bsky.feed.getQuotesPartial", 9 + "defs": { 10 + "main": { 11 + "type": "query", 12 + "description": "Get a list of quotes for a given post.", 13 + "parameters": { 14 + "type": "params", 15 + "required": [ 16 + "uri" 17 + ], 18 + "properties": { 19 + "uri": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "Reference (AT-URI) of post record" 23 + }, 24 + "cid": { 25 + "type": "string", 26 + "format": "cid", 27 + "description": "If supplied, filters to quotes of specific version (by CID) of the post record." 28 + }, 29 + "limit": { 30 + "type": "integer", 31 + "minimum": 1, 32 + "maximum": 100, 33 + "default": 50 34 + }, 35 + "cursor": { 36 + "type": "string" 37 + } 38 + } 39 + }, 40 + "output": { 41 + "encoding": "application/json", 42 + "schema": { 43 + "type": "object", 44 + "required": [ 45 + "uri", 46 + "posts" 47 + ], 48 + "properties": { 49 + "uri": { 50 + "type": "string", 51 + "format": "at-uri" 52 + }, 53 + "cid": { 54 + "type": "string", 55 + "format": "cid" 56 + }, 57 + "cursor": { 58 + "type": "string" 59 + }, 60 + "posts": { 61 + "type": "array", 62 + "items": { 63 + "type": "union", 64 + "refs": [ 65 + "party.whey.app.bsky.feed.defs#postViewRef", 66 + "app.bsky.feed.defs#postView" 67 + ] 68 + } 69 + } 70 + } 71 + } 72 + } 73 + } 74 + } 75 + } 76 + ``` 77 + ---
+75
docs/party.whey.app.bsky.feed.getRepostedByPartial.md
··· 1 + 2 + ## party.whey.app.bsky.feed.getRepostedByPartial 3 + 4 + ```js 5 + { 6 + "lexicon": 1, 7 + "id": "party.whey.app.bsky.feed.getRepostedByPartial", 8 + "defs": { 9 + "main": { 10 + "type": "query", 11 + "description": "Get a list of reposts for a given post.", 12 + "parameters": { 13 + "type": "params", 14 + "required": [ 15 + "uri" 16 + ], 17 + "properties": { 18 + "uri": { 19 + "type": "string", 20 + "format": "at-uri", 21 + "description": "Reference (AT-URI) of post record" 22 + }, 23 + "cid": { 24 + "type": "string", 25 + "format": "cid", 26 + "description": "If supplied, filters to reposts of specific version (by CID) of the post record." 27 + }, 28 + "limit": { 29 + "type": "integer", 30 + "minimum": 1, 31 + "maximum": 100, 32 + "default": 50 33 + }, 34 + "cursor": { 35 + "type": "string" 36 + } 37 + } 38 + }, 39 + "output": { 40 + "encoding": "application/json", 41 + "schema": { 42 + "type": "object", 43 + "required": [ 44 + "uri", 45 + "repostedBy" 46 + ], 47 + "properties": { 48 + "uri": { 49 + "type": "string", 50 + "format": "at-uri" 51 + }, 52 + "cid": { 53 + "type": "string", 54 + "format": "cid" 55 + }, 56 + "cursor": { 57 + "type": "string" 58 + }, 59 + "repostedBy": { 60 + "type": "array", 61 + "items": { 62 + "type": "union", 63 + "refs": [ 64 + "party.whey.app.bsky.actor.defs#profileViewRef", 65 + "app.bsky.actor.defs#profileView" 66 + ] 67 + } 68 + } 69 + } 70 + } 71 + } 72 + } 73 + } 74 + } 75 + ```
+11
docs/structure.md
··· 1 + # Structure 2 + 3 + ![tldrawgraph.png](assets/tldrawgraph.png) 4 + 5 + skylite splits the appview into two parts 6 + 7 + - Public Access Index Server (Index) 8 + - Contextual View Compositor Server (View) 9 + 10 + in short, the index server hosts the hydration and skeleton indexes for users. and the view server collects data from many index servers and layers on top personal preferences and moderation data before handing it off to the application. 11 +
+6
docs/todo.md
··· 1 + # Todo 2 + The central catch-all board of tasks, serving as a temporary replacement for a Kanban board until one exists on Tangled. 3 + lmao 4 + 5 + - [x] hey 6 + - [ ] hey
+13
docs/whylexicons.md
··· 1 + # why 2 + 3 + designing lexicons are weird 4 + 5 + these are the XRPC methods i think are required for a minimal functioning bluesky index to function and pass around data 6 + 7 + some of the methods here reuse existing app.bsky.* XRPC methods, but some are custom under the party.whey.* namespace. 8 + 9 + this is done because i believe that it is expensive to force every single node to return inlined / hydrated responses, when the view server that ingests and composites the final view might not even use the provided inlined / hydrated data in the first place. 10 + 11 + and so, i created "Partial" variants of existing app.bsky.* methods, that allow for "skeleton" responses. specifically, for all methods relating to posts, they are allowed to return "PostViewRef" objects instead of the hydrated "PostView" object. PostViewRef is currently just identical to a strongRef and i am not yet certain if we should add more optional fields to it. 12 + 13 + i do not think that the current lexicons are perfect, but they are good enough that i can continue development of other parts of the skylite system for now
+5 -3
index/jetstream.ts
··· 1 - import { handleIndex, jetstreamManager } from "../main.ts"; 1 + import { Database } from "jsr:@db/sqlite@0.11"; 2 + import { config } from "../config.ts"; 2 3 import { resolveRecordFromURI } from "../utils/records.ts"; 4 + import { JetstreamManager } from "../utils/sharders.ts"; 3 5 4 - export function startJetstream() { 6 + export function startJetstream(jetstreamManager: JetstreamManager) { 5 7 jetstreamManager.start({ 6 8 // for realsies pls get from db or something instead of this shit 7 9 wantedDids: [ ··· 27 29 }); 28 30 } 29 31 30 - export async function handleJetstream(msg: any) { 32 + export async function handleJetstream(msg: any, handleIndex: Function) { 31 33 console.log("Received Jetstream message: ", msg); 32 34 33 35 const op = msg.commit.operation;
+6 -2
index/onboardingBackfill.ts
··· 1 - import { systemDB, handleIndex } from "../main.ts" 1 + import { genericIndexServer } from "../main-index.ts"; 2 + import { config } from "../config.ts" 2 3 import { FINEPDSAndHandleFromDid } from "../utils/identity.ts"; 3 4 4 5 ··· 67 68 const doer = did; 68 69 const rev = undefined; 69 70 const aturi = uri; 71 + const db = genericIndexServer.userManager.getDbForDid(doer); 72 + if (!db) return; 70 73 71 - handleIndex({ 74 + genericIndexServer.indexServerIndexer({ 72 75 op, 73 76 doer, 74 77 rev, 75 78 aturi, 76 79 value, 77 80 indexsrc: "onboarding_backfill", 81 + db: db, 78 82 }) 79 83 return; 80 84 // console.log(`[BACKFILL] ${collection} -> ${uri}`);
+10 -5
index/spacedust.ts
··· 1 - import { db, handleIndex, spacedustManager } from "../main.ts"; 1 + import { Database } from "jsr:@db/sqlite@0.11"; 2 + import { config } from "../config.ts"; 2 3 import { parseAtUri } from "../utils/aturi.ts"; 3 4 import { resolveRecordFromURI } from "../utils/records.ts"; 5 + import { SpacedustManager } from "../utils/sharders.ts"; 4 6 5 - export function startSpacedust() { 7 + export function startSpacedust(spacedustManager: SpacedustManager) { 6 8 spacedustManager.start({ 7 9 wantedSources: [ 8 10 "app.bsky.feed.like:subject.uri", // like ··· 39 41 "did:plc:zzhzjga3ab5fcs2vnsv2ist3", 40 42 "did:plc:jz4ibztn56hygfld6j6zjszg", 41 43 ], // this would be all users in the instance to listen for all remote mentions 44 + instant: ["true"] 42 45 // future question for managing this specifically for like remote content tracking (followed users? items that appears in custom feeds?) 43 46 }); 44 47 } ··· 54 57 }; 55 58 }; 56 59 57 - export async function handleSpacedust(msg: SpacedustLinkMessage) { 60 + export async function handleSpacedust(db: Database, msg: SpacedustLinkMessage) { 58 61 if (!msg || !msg.link || !msg.link.source_record) return; 59 62 console.log("Received Spacedust message: ", msg); 60 63 ··· 77 80 srccol, 78 81 suburi, 79 82 subdid, 80 - subcol 83 + subcol, 84 + indexedAt 81 85 ) VALUES ( 82 86 '${aturi}', 83 87 '${srcdid}', ··· 85 89 '${msg.link.source}', 86 90 '${subject}', 87 91 '${subdid}', 88 - '${subscol}' 92 + '${subscol}', 93 + '${Date.now()}' 89 94 ); 90 95 `); 91 96 //if (!value) return;
+5 -1
index/types.ts
··· 1 + import { Database } from "jsr:@db/sqlite@0.11"; 2 + 1 3 export type indexHandlerContext = { 2 4 op: string; 3 5 doer: string; // the formal term for this is "repo" but whatever right ··· 5 7 cid?: string; 6 8 aturi: string; 7 9 indexsrc: string; 8 - value: Record<string, unknown> 10 + value: Record<string, unknown>; 11 + //userdbname: string; 12 + db: Database; 9 13 }
+2164 -7
indexserver.ts
··· 1 - export async function indexServerHandler(req: Request): Promise<Response> { 2 - const url = new URL(req.url); 3 - 4 - if (url.pathname === "/ping") { 5 - return new Response("pong", { status: 200 }); 1 + import { indexHandlerContext } from "./index/types.ts"; 2 + 3 + import { assertRecord, validateRecord } from "./utils/records.ts"; 4 + import { 5 + buildBlobUrl, 6 + getSlingshotRecord, 7 + resolveIdentity, 8 + searchParamsToJson, 9 + withCors, 10 + } from "./utils/server.ts"; 11 + import * as IndexServerTypes from "./utils/indexservertypes.ts"; 12 + import { Database } from "jsr:@db/sqlite@0.11"; 13 + import { setupUserDb } from "./utils/dbuser.ts"; 14 + // import { systemDB } from "./env.ts"; 15 + import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts"; 16 + import { handleSpacedust, SpacedustLinkMessage } from "./index/spacedust.ts"; 17 + import { handleJetstream } from "./index/jetstream.ts"; 18 + import * as ATPAPI from "npm:@atproto/api"; 19 + import { AtUri } from "npm:@atproto/api"; 20 + import * as IndexServerAPI from "./indexclient/index.ts"; 21 + import * as IndexServerUtils from "./indexclient/util.ts"; 22 + import { isPostView } from "./indexclient/types/app/bsky/feed/defs.ts"; 23 + 24 + export interface IndexServerConfig { 25 + baseDbPath: string; 26 + systemDbPath: string; 27 + } 28 + 29 + interface BaseRow { 30 + uri: string; 31 + did: string; 32 + cid: string | null; 33 + rev: string | null; 34 + createdat: number | null; 35 + indexedat: number; 36 + json: string | null; 37 + } 38 + interface GeneratorRow extends BaseRow { 39 + displayname: string | null; 40 + description: string | null; 41 + avatarcid: string | null; 42 + } 43 + interface LikeRow extends BaseRow { 44 + subject: string; 45 + } 46 + interface RepostRow extends BaseRow { 47 + subject: string; 48 + } 49 + interface BacklinkRow { 50 + srcuri: string; 51 + srcdid: string; 52 + } 53 + 54 + const FEED_LIMIT = 50; 55 + 56 + export class IndexServer { 57 + private config: IndexServerConfig; 58 + public userManager: IndexServerUserManager; 59 + public systemDB: Database; 60 + 61 + constructor(config: IndexServerConfig) { 62 + this.config = config; 63 + 64 + // We will initialize the system DB and user manager here 65 + this.systemDB = new Database(this.config.systemDbPath); 66 + // TODO: We need to setup the system DB schema if it's new 67 + 68 + this.userManager = new IndexServerUserManager(this); // Pass the server instance 69 + } 70 + 71 + public start() { 72 + // This is where we'll kick things off, like the cold start 73 + this.userManager.coldStart(this.systemDB); 74 + console.log("IndexServer started."); 75 + } 76 + 77 + public async handleRequest(req: Request): Promise<Response> { 78 + const url = new URL(req.url); 79 + // We will add routing logic here later to call our handlers 80 + if (url.pathname.startsWith("/xrpc/")) { 81 + return this.indexServerHandler(req); 82 + } 83 + if (url.pathname.startsWith("/links")) { 84 + return this.constellationAPIHandler(req); 85 + } 86 + return new Response("Not Found", { status: 404 }); 87 + } 88 + 89 + public handlesDid(did: string): boolean { 90 + return this.userManager.handlesDid(did); 91 + } 92 + async unspeccedGetRegisteredUsers(): Promise<{ 93 + did: string; 94 + role: string; 95 + registrationdate: string; 96 + onboardingstatus: string; 97 + pfp?: string; 98 + displayname: string; 99 + handle: string; 100 + }[]|undefined> { 101 + const stmt = this.systemDB.prepare(` 102 + SELECT * 103 + FROM users; 104 + `); 105 + const result = stmt.all() as 106 + { 107 + did: string; 108 + role: string; 109 + registrationdate: string; 110 + onboardingstatus: string; 111 + }[]; 112 + const hydrated = await Promise.all( result.map(async (user)=>{ 113 + const identity = await resolveIdentity(user.did); 114 + const profile = (await getSlingshotRecord(identity.did,"app.bsky.actor.profile","self")).value as ATPAPI.AppBskyActorProfile.Record; 115 + const avatarcid = uncid(profile.avatar?.ref); 116 + const avatar = avatarcid 117 + ? buildBlobUrl(identity.pds, identity.did, avatarcid) 118 + : undefined; 119 + return {...user,handle: identity.handle,pfp: avatar, displayname:profile.displayName ?? identity.handle } 120 + })) 121 + //const exists = result !== undefined; 122 + return hydrated; 123 + } 124 + 125 + // We will move all the global functions into this class as methods... 126 + async indexServerHandler(req: Request): Promise<Response> { 127 + const url = new URL(req.url); 128 + const pathname = url.pathname; 129 + //const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 130 + //const hasAuth = req.headers.has("authorization"); 131 + const xrpcMethod = pathname.startsWith("/xrpc/") 132 + ? pathname.slice("/xrpc/".length) 133 + : null; 134 + const searchParams = searchParamsToJson(url.searchParams); 135 + console.log(JSON.stringify(searchParams, null, 2)); 136 + const jsonUntyped = searchParams; 137 + 138 + switch (xrpcMethod) { 139 + case "app.bsky.actor.getProfile": { 140 + const jsonTyped = 141 + jsonUntyped as IndexServerTypes.AppBskyActorGetProfile.QueryParams; 142 + 143 + const res = await this.queryProfileView(jsonTyped.actor, "Detailed"); 144 + if (!res) 145 + return new Response( 146 + JSON.stringify({ 147 + error: "User not found", 148 + }), 149 + { 150 + status: 404, 151 + headers: withCors({ "Content-Type": "application/json" }), 152 + } 153 + ); 154 + const response: IndexServerTypes.AppBskyActorGetProfile.OutputSchema = 155 + res; 156 + 157 + return new Response(JSON.stringify(response), { 158 + headers: withCors({ "Content-Type": "application/json" }), 159 + }); 160 + } 161 + case "app.bsky.actor.getProfiles": { 162 + const jsonTyped = 163 + jsonUntyped as IndexServerTypes.AppBskyActorGetProfiles.QueryParams; 164 + 165 + if (typeof jsonUntyped?.actors === "string") { 166 + const res = await this.queryProfileView( 167 + jsonUntyped.actors as string, 168 + "Detailed" 169 + ); 170 + if (!res) 171 + return new Response( 172 + JSON.stringify({ 173 + error: "User not found", 174 + }), 175 + { 176 + status: 404, 177 + headers: withCors({ "Content-Type": "application/json" }), 178 + } 179 + ); 180 + const response: IndexServerTypes.AppBskyActorGetProfiles.OutputSchema = 181 + { 182 + profiles: [res], 183 + }; 184 + 185 + return new Response(JSON.stringify(response), { 186 + headers: withCors({ "Content-Type": "application/json" }), 187 + }); 188 + } 189 + 190 + const res: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] = 191 + await Promise.all( 192 + jsonTyped.actors 193 + .map(async (actor) => { 194 + return await this.queryProfileView(actor, "Detailed"); 195 + }) 196 + .filter( 197 + ( 198 + x 199 + ): x is Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed> => 200 + x !== undefined 201 + ) 202 + ); 203 + 204 + if (!res) 205 + return new Response( 206 + JSON.stringify({ 207 + error: "User not found", 208 + }), 209 + { 210 + status: 404, 211 + headers: withCors({ "Content-Type": "application/json" }), 212 + } 213 + ); 214 + 215 + const response: IndexServerTypes.AppBskyActorGetProfiles.OutputSchema = 216 + { 217 + profiles: res, 218 + }; 219 + 220 + return new Response(JSON.stringify(response), { 221 + headers: withCors({ "Content-Type": "application/json" }), 222 + }); 223 + } 224 + case "app.bsky.feed.getActorFeeds": { 225 + const jsonTyped = 226 + jsonUntyped as IndexServerTypes.AppBskyFeedGetActorFeeds.QueryParams; 227 + 228 + const qresult = await this.queryActorFeeds(jsonTyped.actor); 229 + 230 + const response: IndexServerTypes.AppBskyFeedGetActorFeeds.OutputSchema = 231 + { 232 + feeds: qresult, 233 + }; 234 + 235 + return new Response(JSON.stringify(response), { 236 + headers: withCors({ "Content-Type": "application/json" }), 237 + }); 238 + } 239 + case "app.bsky.feed.getFeedGenerator": { 240 + const jsonTyped = 241 + jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerator.QueryParams; 242 + 243 + const qresult = await this.queryFeedGenerator(jsonTyped.feed); 244 + if (!qresult) { 245 + return new Response( 246 + JSON.stringify({ 247 + error: "Feed not found", 248 + }), 249 + { 250 + status: 404, 251 + headers: withCors({ "Content-Type": "application/json" }), 252 + } 253 + ); 254 + } 255 + 256 + const response: IndexServerTypes.AppBskyFeedGetFeedGenerator.OutputSchema = 257 + { 258 + view: qresult, 259 + isOnline: true, // lmao 260 + isValid: true, // lmao 261 + }; 262 + 263 + return new Response(JSON.stringify(response), { 264 + headers: withCors({ "Content-Type": "application/json" }), 265 + }); 266 + } 267 + case "app.bsky.feed.getFeedGenerators": { 268 + const jsonTyped = 269 + jsonUntyped as IndexServerTypes.AppBskyFeedGetFeedGenerators.QueryParams; 270 + 271 + const qresult = await this.queryFeedGenerators(jsonTyped.feeds); 272 + if (!qresult) { 273 + return new Response( 274 + JSON.stringify({ 275 + error: "Feed not found", 276 + }), 277 + { 278 + status: 404, 279 + headers: withCors({ "Content-Type": "application/json" }), 280 + } 281 + ); 282 + } 283 + 284 + const response: IndexServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema = 285 + { 286 + feeds: qresult, 287 + }; 288 + 289 + return new Response(JSON.stringify(response), { 290 + headers: withCors({ "Content-Type": "application/json" }), 291 + }); 292 + } 293 + case "app.bsky.feed.getPosts": { 294 + const jsonTyped = 295 + jsonUntyped as IndexServerTypes.AppBskyFeedGetPosts.QueryParams; 296 + 297 + const posts: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema["posts"] = 298 + ( 299 + await Promise.all( 300 + jsonTyped.uris.map((uri) => this.queryPostView(uri)) 301 + ) 302 + ).filter((p): p is ATPAPI.AppBskyFeedDefs.PostView => Boolean(p)); 303 + 304 + const response: IndexServerTypes.AppBskyFeedGetPosts.OutputSchema = { 305 + posts, 306 + }; 307 + 308 + return new Response(JSON.stringify(response), { 309 + headers: withCors({ "Content-Type": "application/json" }), 310 + }); 311 + } 312 + case "party.whey.app.bsky.feed.getActorLikesPartial": { 313 + const jsonTyped = 314 + jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetActorLikesPartial.QueryParams; 315 + 316 + // TODO: not partial yet, currently skips refs 317 + 318 + const qresult = await this.queryActorLikesPartial( 319 + jsonTyped.actor, 320 + jsonTyped.cursor 321 + ); 322 + if (!qresult) { 323 + return new Response( 324 + JSON.stringify({ 325 + error: "Feed not found", 326 + }), 327 + { 328 + status: 404, 329 + headers: withCors({ "Content-Type": "application/json" }), 330 + } 331 + ); 332 + } 333 + 334 + const response: IndexServerTypes.PartyWheyAppBskyFeedGetActorLikesPartial.OutputSchema = 335 + { 336 + feed: qresult.items as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.FeedViewPost>[], 337 + cursor: qresult.cursor, 338 + }; 339 + 340 + return new Response(JSON.stringify(response), { 341 + headers: withCors({ "Content-Type": "application/json" }), 342 + }); 343 + } 344 + case "party.whey.app.bsky.feed.getAuthorFeedPartial": { 345 + const jsonTyped = 346 + jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetAuthorFeedPartial.QueryParams; 347 + 348 + // TODO: not partial yet, currently skips refs 349 + 350 + const qresult = await this.queryAuthorFeedPartial( 351 + jsonTyped.actor, 352 + jsonTyped.cursor 353 + ); 354 + if (!qresult) { 355 + return new Response( 356 + JSON.stringify({ 357 + error: "Feed not found", 358 + }), 359 + { 360 + status: 404, 361 + headers: withCors({ "Content-Type": "application/json" }), 362 + } 363 + ); 364 + } 365 + 366 + const response: IndexServerTypes.PartyWheyAppBskyFeedGetAuthorFeedPartial.OutputSchema = 367 + { 368 + feed: qresult.items as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.FeedViewPost>[], 369 + cursor: qresult.cursor, 370 + }; 371 + 372 + return new Response(JSON.stringify(response), { 373 + headers: withCors({ "Content-Type": "application/json" }), 374 + }); 375 + } 376 + case "party.whey.app.bsky.feed.getLikesPartial": { 377 + const jsonTyped = 378 + jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetLikesPartial.QueryParams; 379 + 380 + // TODO: not partial yet, currently skips refs 381 + 382 + const qresult = this.queryLikes(jsonTyped.uri); 383 + if (!qresult) { 384 + return new Response( 385 + JSON.stringify({ 386 + error: "Feed not found", 387 + }), 388 + { 389 + status: 404, 390 + headers: withCors({ "Content-Type": "application/json" }), 391 + } 392 + ); 393 + } 394 + const response: IndexServerTypes.PartyWheyAppBskyFeedGetLikesPartial.OutputSchema = 395 + { 396 + // @ts-ignore whatever i dont care TODO: fix ts ignores 397 + likes: qresult, 398 + }; 399 + 400 + return new Response(JSON.stringify(response), { 401 + headers: withCors({ "Content-Type": "application/json" }), 402 + }); 403 + } 404 + case "party.whey.app.bsky.feed.getPostThreadPartial": { 405 + const jsonTyped = 406 + jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.QueryParams; 407 + 408 + // TODO: not partial yet, currently skips refs 409 + 410 + const qresult = await this.queryPostThreadPartial(jsonTyped.uri); 411 + if (!qresult) { 412 + return new Response( 413 + JSON.stringify({ 414 + error: "Feed not found", 415 + }), 416 + { 417 + status: 404, 418 + headers: withCors({ "Content-Type": "application/json" }), 419 + } 420 + ); 421 + } 422 + const response: IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema = 423 + qresult; 424 + 425 + return new Response(JSON.stringify(response), { 426 + headers: withCors({ "Content-Type": "application/json" }), 427 + }); 428 + } 429 + case "party.whey.app.bsky.feed.getQuotesPartial": { 430 + const jsonTyped = 431 + jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetQuotesPartial.QueryParams; 432 + 433 + // TODO: not partial yet, currently skips refs 434 + 435 + const qresult = await this.queryQuotes(jsonTyped.uri); 436 + if (!qresult) { 437 + return new Response( 438 + JSON.stringify({ 439 + error: "Feed not found", 440 + }), 441 + { 442 + status: 404, 443 + headers: withCors({ "Content-Type": "application/json" }), 444 + } 445 + ); 446 + } 447 + const response: IndexServerTypes.PartyWheyAppBskyFeedGetQuotesPartial.OutputSchema = 448 + { 449 + uri: jsonTyped.uri, 450 + posts: qresult.map((feedviewpost) => { 451 + return feedviewpost.post as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>; 452 + }), 453 + }; 454 + 455 + return new Response(JSON.stringify(response), { 456 + headers: withCors({ "Content-Type": "application/json" }), 457 + }); 458 + } 459 + case "party.whey.app.bsky.feed.getRepostedByPartial": { 460 + const jsonTyped = 461 + jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetRepostedByPartial.QueryParams; 462 + 463 + // TODO: not partial yet, currently skips refs 464 + 465 + const qresult = await this.queryReposts(jsonTyped.uri); 466 + if (!qresult) { 467 + return new Response( 468 + JSON.stringify({ 469 + error: "Feed not found", 470 + }), 471 + { 472 + status: 404, 473 + headers: withCors({ "Content-Type": "application/json" }), 474 + } 475 + ); 476 + } 477 + const response: IndexServerTypes.PartyWheyAppBskyFeedGetRepostedByPartial.OutputSchema = 478 + { 479 + uri: jsonTyped.uri, 480 + repostedBy: 481 + qresult as ATPAPI.$Typed<ATPAPI.AppBskyActorDefs.ProfileView>[], 482 + }; 483 + 484 + return new Response(JSON.stringify(response), { 485 + headers: withCors({ "Content-Type": "application/json" }), 486 + }); 487 + } 488 + // TODO: too hard for now 489 + // case "party.whey.app.bsky.feed.getListFeedPartial": { 490 + // const jsonTyped = 491 + // jsonUntyped as IndexServerTypes.PartyWheyAppBskyFeedGetListFeedPartial.QueryParams; 492 + 493 + // const response: IndexServerTypes.PartyWheyAppBskyFeedGetListFeedPartial.OutputSchema = 494 + // {}; 495 + 496 + // return new Response(JSON.stringify(response), { 497 + // headers: withCors({ "Content-Type": "application/json" }), 498 + // }); 499 + // } 500 + /* three more coming soon 501 + app.bsky.graph.getLists 502 + app.bsky.graph.getList 503 + app.bsky.graph.getActorStarterPacks 504 + */ 505 + default: { 506 + return new Response( 507 + JSON.stringify({ 508 + error: "XRPCNotSupported", 509 + message: 510 + "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 511 + }), 512 + { 513 + status: 404, 514 + headers: withCors({ "Content-Type": "application/json" }), 515 + } 516 + ); 517 + } 518 + } 519 + 520 + // return new Response("Not Found", { status: 404 }); 521 + } 522 + 523 + constellationAPIHandler(req: Request): Response { 524 + const url = new URL(req.url); 525 + const pathname = url.pathname; 526 + const searchParams = searchParamsToJson(url.searchParams) as linksQuery; 527 + const jsonUntyped = searchParams; 528 + 529 + if (!jsonUntyped.target) { 530 + return new Response( 531 + JSON.stringify({ error: "Missing required parameter: target" }), 532 + { 533 + status: 400, 534 + headers: withCors({ "Content-Type": "application/json" }), 535 + } 536 + ); 537 + } 538 + 539 + const did = isDid(searchParams.target) 540 + ? searchParams.target 541 + : new AtUri(searchParams.target).host; 542 + const db = this.userManager.getDbForDid(did); 543 + if (!db) { 544 + return new Response( 545 + JSON.stringify({ 546 + error: "User not found", 547 + }), 548 + { 549 + status: 404, 550 + headers: withCors({ "Content-Type": "application/json" }), 551 + } 552 + ); 553 + } 554 + 555 + const limit = 16; //Math.min(parseInt(searchParams.limit || "50", 10), 100); 556 + const offset = parseInt(searchParams.cursor || "0", 10); 557 + 558 + switch (pathname) { 559 + case "/links": { 560 + const jsonTyped = jsonUntyped as linksQuery; 561 + if (!jsonTyped.collection || !jsonTyped.path) { 562 + return new Response( 563 + JSON.stringify({ 564 + error: "Missing required parameters: collection, path", 565 + }), 566 + { 567 + status: 400, 568 + headers: withCors({ "Content-Type": "application/json" }), 569 + } 570 + ); 571 + } 572 + 573 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 574 + /^\./, 575 + "" 576 + )}`; 577 + 578 + const paginatedSql = `${SQL.links} LIMIT ? OFFSET ?`; 579 + const rows = db 580 + .prepare(paginatedSql) 581 + .all(jsonTyped.target, jsonTyped.collection, field, limit, offset); 582 + 583 + const countResult = db 584 + .prepare(SQL.count) 585 + .get(jsonTyped.target, jsonTyped.collection, field); 586 + const total = countResult ? Number(countResult.total) : 0; 587 + 588 + const linking_records: linksRecord[] = rows.map((row: any) => { 589 + const rkey = row.srcuri.split("/").pop()!; 590 + return { 591 + did: row.srcdid, 592 + collection: row.srccol, 593 + rkey, 594 + }; 595 + }); 596 + 597 + const response: linksRecordsResponse = { 598 + total: total.toString(), 599 + linking_records, 600 + }; 601 + 602 + const nextCursor = offset + linking_records.length; 603 + if (nextCursor < total) { 604 + response.cursor = nextCursor.toString(); 605 + } 606 + 607 + return new Response(JSON.stringify(response), { 608 + headers: withCors({ "Content-Type": "application/json" }), 609 + }); 610 + } 611 + case "/links/distinct-dids": { 612 + const jsonTyped = jsonUntyped as linksQuery; 613 + if (!jsonTyped.collection || !jsonTyped.path) { 614 + return new Response( 615 + JSON.stringify({ 616 + error: "Missing required parameters: collection, path", 617 + }), 618 + { 619 + status: 400, 620 + headers: withCors({ "Content-Type": "application/json" }), 621 + } 622 + ); 623 + } 624 + 625 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 626 + /^\./, 627 + "" 628 + )}`; 629 + 630 + const paginatedSql = `${SQL.distinctDids} LIMIT ? OFFSET ?`; 631 + const rows = db 632 + .prepare(paginatedSql) 633 + .all(jsonTyped.target, jsonTyped.collection, field, limit, offset); 634 + 635 + const countResult = db 636 + .prepare(SQL.countDistinctDids) 637 + .get(jsonTyped.target, jsonTyped.collection, field); 638 + const total = countResult ? Number(countResult.total) : 0; 639 + 640 + const linking_dids: string[] = rows.map((row: any) => row.srcdid); 641 + 642 + const response: linksDidsResponse = { 643 + total: total.toString(), 644 + linking_dids, 645 + }; 646 + 647 + const nextCursor = offset + linking_dids.length; 648 + if (nextCursor < total) { 649 + response.cursor = nextCursor.toString(); 650 + } 651 + 652 + return new Response(JSON.stringify(response), { 653 + headers: withCors({ "Content-Type": "application/json" }), 654 + }); 655 + } 656 + case "/links/count": { 657 + const jsonTyped = jsonUntyped as linksQuery; 658 + if (!jsonTyped.collection || !jsonTyped.path) { 659 + return new Response( 660 + JSON.stringify({ 661 + error: "Missing required parameters: collection, path", 662 + }), 663 + { 664 + status: 400, 665 + headers: withCors({ "Content-Type": "application/json" }), 666 + } 667 + ); 668 + } 669 + 670 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 671 + /^\./, 672 + "" 673 + )}`; 674 + 675 + const result = db 676 + .prepare(SQL.count) 677 + .get(jsonTyped.target, jsonTyped.collection, field); 678 + 679 + const response: linksCountResponse = { 680 + total: result && result.total ? result.total.toString() : "0", 681 + }; 682 + 683 + return new Response(JSON.stringify(response), { 684 + headers: withCors({ "Content-Type": "application/json" }), 685 + }); 686 + } 687 + case "/links/count/distinct-dids": { 688 + const jsonTyped = jsonUntyped as linksQuery; 689 + if (!jsonTyped.collection || !jsonTyped.path) { 690 + return new Response( 691 + JSON.stringify({ 692 + error: "Missing required parameters: collection, path", 693 + }), 694 + { 695 + status: 400, 696 + headers: withCors({ "Content-Type": "application/json" }), 697 + } 698 + ); 699 + } 700 + 701 + const field = `${jsonTyped.collection}:${jsonTyped.path.replace( 702 + /^\./, 703 + "" 704 + )}`; 705 + 706 + const result = db 707 + .prepare(SQL.countDistinctDids) 708 + .get(jsonTyped.target, jsonTyped.collection, field); 709 + 710 + const response: linksCountResponse = { 711 + total: result && result.total ? result.total.toString() : "0", 712 + }; 713 + 714 + return new Response(JSON.stringify(response), { 715 + headers: withCors({ "Content-Type": "application/json" }), 716 + }); 717 + } 718 + case "/links/all": { 719 + const jsonTyped = jsonUntyped as linksAllQuery; 720 + 721 + const rows = db.prepare(SQL.all).all(jsonTyped.target) as any[]; 722 + 723 + const links: linksAllResponse["links"] = {}; 724 + 725 + for (const row of rows) { 726 + if (!links[row.suburi]) { 727 + links[row.suburi] = {}; 728 + } 729 + links[row.suburi][row.srccol] = { 730 + records: row.records, 731 + distinct_dids: row.distinct_dids, 732 + }; 733 + } 734 + 735 + const response: linksAllResponse = { 736 + links, 737 + }; 738 + 739 + return new Response(JSON.stringify(response), { 740 + headers: withCors({ "Content-Type": "application/json" }), 741 + }); 742 + } 743 + default: { 744 + return new Response( 745 + JSON.stringify({ 746 + error: "NotSupported", 747 + message: 748 + "The requested endpoint is not supported by this Constellation implementation.", 749 + }), 750 + { 751 + status: 404, 752 + headers: withCors({ "Content-Type": "application/json" }), 753 + } 754 + ); 755 + } 756 + } 757 + } 758 + 759 + indexServerIndexer(ctx: indexHandlerContext) { 760 + const record = assertRecord(ctx.value); 761 + //const record = validateRecord(ctx.value); 762 + const db = this.userManager.getDbForDid(ctx.doer); 763 + if (!db) return; 764 + console.log("indexering"); 765 + switch (record?.$type) { 766 + case "app.bsky.feed.like": { 767 + return; 768 + } 769 + case "app.bsky.actor.profile": { 770 + console.log("bsky profuile"); 771 + 772 + try { 773 + const stmt = db.prepare(` 774 + INSERT OR IGNORE INTO app_bsky_actor_profile ( 775 + uri, did, cid, rev, createdat, indexedat, json, 776 + displayname, 777 + description, 778 + avatarcid, 779 + avatarmime, 780 + bannercid, 781 + bannermime 782 + ) VALUES (?, ?, ?, ?, ?, ?, ?, 783 + ?, ?, ?, 784 + ?, ?, ?) 785 + `); 786 + console.log({ 787 + uri: ctx.aturi, 788 + did: ctx.doer, 789 + cid: ctx.cid, 790 + rev: ctx.rev, 791 + createdat: record.createdAt, 792 + indexedat: Date.now(), 793 + json: JSON.stringify(record), 794 + displayname: record.displayName, 795 + description: record.description, 796 + avatarcid: uncid(record.avatar?.ref), 797 + avatarmime: record.avatar?.mimeType, 798 + bannercid: uncid(record.banner?.ref), 799 + bannermime: record.banner?.mimeType, 800 + }); 801 + stmt.run( 802 + ctx.aturi ?? null, 803 + ctx.doer ?? null, 804 + ctx.cid ?? null, 805 + ctx.rev ?? null, 806 + record.createdAt ?? null, 807 + Date.now(), 808 + JSON.stringify(record), 809 + 810 + record.displayName ?? null, 811 + record.description ?? null, 812 + uncid(record.avatar?.ref) ?? null, 813 + record.avatar?.mimeType ?? null, 814 + uncid(record.banner?.ref) ?? null, 815 + record.banner?.mimeType ?? null 816 + // TODO please add pinned posts 817 + ); 818 + } catch (err) { 819 + console.error("stmt.run failed:", err); 820 + } 821 + return; 822 + } 823 + case "app.bsky.feed.post": { 824 + console.log("bsky post"); 825 + const stmt = db.prepare(` 826 + INSERT OR IGNORE INTO app_bsky_feed_post ( 827 + uri, did, cid, rev, createdat, indexedat, json, 828 + text, replyroot, replyparent, quote, 829 + imagecount, image1cid, image1mime, image1aspect, 830 + image2cid, image2mime, image2aspect, 831 + image3cid, image3mime, image3aspect, 832 + image4cid, image4mime, image4aspect, 833 + videocount, videocid, videomime, videoaspect 834 + ) VALUES (?, ?, ?, ?, ?, ?, ?, 835 + ?, ?, ?, ?, 836 + ?, ?, ?, ?, 837 + ?, ?, ?, 838 + ?, ?, ?, 839 + ?, ?, ?, 840 + ?, ?, ?, ?) 841 + `); 842 + 843 + const embed = record.embed; 844 + 845 + const images = extractImages(embed); 846 + const video = extractVideo(embed); 847 + const quoteUri = extractQuoteUri(embed); 848 + try { 849 + stmt.run( 850 + ctx.aturi ?? null, 851 + ctx.doer ?? null, 852 + ctx.cid ?? null, 853 + ctx.rev ?? null, 854 + record.createdAt, 855 + Date.now(), 856 + JSON.stringify(record), 857 + 858 + record.text ?? null, 859 + record.reply?.root?.uri ?? null, 860 + record.reply?.parent?.uri ?? null, 861 + 862 + quoteUri, 863 + 864 + images.length, 865 + uncid(images[0]?.image?.ref) ?? null, 866 + images[0]?.image?.mimeType ?? null, 867 + images[0]?.aspectRatio && 868 + images[0].aspectRatio.width && 869 + images[0].aspectRatio.height 870 + ? `${images[0].aspectRatio.width}:${images[0].aspectRatio.height}` 871 + : null, 872 + 873 + uncid(images[1]?.image?.ref) ?? null, 874 + images[1]?.image?.mimeType ?? null, 875 + images[1]?.aspectRatio && 876 + images[1].aspectRatio.width && 877 + images[1].aspectRatio.height 878 + ? `${images[1].aspectRatio.width}:${images[1].aspectRatio.height}` 879 + : null, 880 + 881 + uncid(images[2]?.image?.ref) ?? null, 882 + images[2]?.image?.mimeType ?? null, 883 + images[2]?.aspectRatio && 884 + images[2].aspectRatio.width && 885 + images[2].aspectRatio.height 886 + ? `${images[2].aspectRatio.width}:${images[2].aspectRatio.height}` 887 + : null, 888 + 889 + uncid(images[3]?.image?.ref) ?? null, 890 + images[3]?.image?.mimeType ?? null, 891 + images[3]?.aspectRatio && 892 + images[3].aspectRatio.width && 893 + images[3].aspectRatio.height 894 + ? `${images[3].aspectRatio.width}:${images[3].aspectRatio.height}` 895 + : null, 896 + 897 + uncid(video?.video) ? 1 : 0, 898 + uncid(video?.video) ?? null, 899 + uncid(video?.video) ? "video/mp4" : null, 900 + video?.aspectRatio 901 + ? `${video.aspectRatio.width}:${video.aspectRatio.height}` 902 + : null 903 + ); 904 + } catch (err) { 905 + console.error("stmt.run failed:", err); 906 + } 907 + return; 908 + } 909 + default: { 910 + // what the hell 911 + return; 912 + } 913 + } 914 + } 915 + 916 + // user data 917 + async queryProfileView( 918 + did: string, 919 + type: "" 920 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileView | undefined>; 921 + async queryProfileView( 922 + did: string, 923 + type: "Basic" 924 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined>; 925 + async queryProfileView( 926 + did: string, 927 + type: "Detailed" 928 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined>; 929 + async queryProfileView( 930 + did: string, 931 + type: "" | "Basic" | "Detailed" 932 + ): Promise< 933 + | ATPAPI.AppBskyActorDefs.ProfileView 934 + | ATPAPI.AppBskyActorDefs.ProfileViewBasic 935 + | ATPAPI.AppBskyActorDefs.ProfileViewDetailed 936 + | undefined 937 + > { 938 + if (!this.isRegisteredIndexUser(did)) return; 939 + const db = this.userManager.getDbForDid(did); 940 + if (!db) return; 941 + 942 + const stmt = db.prepare(` 943 + SELECT * 944 + FROM app_bsky_actor_profile 945 + WHERE did = ? 946 + LIMIT 1; 947 + `); 948 + 949 + const row = stmt.get(did) as ProfileRow; 950 + 951 + const identity = await resolveIdentity(did); 952 + const avatar = row.avatarcid ? buildBlobUrl( 953 + identity.pds, 954 + identity.did, 955 + row.avatarcid 956 + ) : undefined 957 + const banner = row.bannercid ? buildBlobUrl( 958 + identity.pds, 959 + identity.did, 960 + row.bannercid 961 + ) : undefined 962 + // simulate different types returned 963 + switch (type) { 964 + case "": { 965 + const result: ATPAPI.AppBskyActorDefs.ProfileView = { 966 + $type: "app.bsky.actor.defs#profileView", 967 + did: did, 968 + handle: identity.handle, // TODO: Resolve user identity here for the handle 969 + displayName: row.displayname ?? undefined, 970 + description: row.description ?? undefined, 971 + avatar: avatar, // create profile URL from resolved identity 972 + //associated?: ProfileAssociated, 973 + indexedAt: row.createdat 974 + ? new Date(row.createdat).toISOString() 975 + : undefined, 976 + createdAt: row.createdat 977 + ? new Date(row.createdat).toISOString() 978 + : undefined, 979 + //viewer?: ViewerState, 980 + //labels?: ComAtprotoLabelDefs.Label[], 981 + //verification?: VerificationState, 982 + //status?: StatusView, 983 + }; 984 + return result; 985 + } 986 + case "Basic": { 987 + const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = { 988 + $type: "app.bsky.actor.defs#profileViewBasic", 989 + did: did, 990 + handle: identity.handle, // TODO: Resolve user identity here for the handle 991 + displayName: row.displayname ?? undefined, 992 + avatar: avatar, // create profile URL from resolved identity 993 + //associated?: ProfileAssociated, 994 + createdAt: row.createdat 995 + ? new Date(row.createdat).toISOString() 996 + : undefined, 997 + //viewer?: ViewerState, 998 + //labels?: ComAtprotoLabelDefs.Label[], 999 + //verification?: VerificationState, 1000 + //status?: StatusView, 1001 + }; 1002 + return result; 1003 + } 1004 + case "Detailed": { 1005 + // Query for follower count from the backlink_skeleton table 1006 + const followersStmt = db.prepare(` 1007 + SELECT COUNT(*) as count 1008 + FROM backlink_skeleton 1009 + WHERE subdid = ? AND srccol = 'app.bsky.graph.follow' 1010 + `); 1011 + const followersResult = followersStmt.get(did) as { count: number }; 1012 + const followersCount = followersResult?.count ?? 0; 1013 + 1014 + // Query for following count from the app_bsky_graph_follow table 1015 + const followingStmt = db.prepare(` 1016 + SELECT COUNT(*) as count 1017 + FROM app_bsky_graph_follow 1018 + WHERE did = ? 1019 + `); 1020 + const followingResult = followingStmt.get(did) as { count: number }; 1021 + const followsCount = followingResult?.count ?? 0; 1022 + 1023 + // Query for post count from the app_bsky_feed_post table 1024 + const postsStmt = db.prepare(` 1025 + SELECT COUNT(*) as count 1026 + FROM app_bsky_feed_post 1027 + WHERE did = ? 1028 + `); 1029 + const postsResult = postsStmt.get(did) as { count: number }; 1030 + const postsCount = postsResult?.count ?? 0; 1031 + 1032 + const result: ATPAPI.AppBskyActorDefs.ProfileViewDetailed = { 1033 + $type: "app.bsky.actor.defs#profileViewDetailed", 1034 + did: did, 1035 + handle: identity.handle, // TODO: Resolve user identity here for the handle 1036 + displayName: row.displayname ?? undefined, 1037 + description: row.description ?? undefined, 1038 + avatar: avatar, // TODO: create profile URL from resolved identity 1039 + banner: banner, // same here 1040 + followersCount: followersCount, 1041 + followsCount: followsCount, 1042 + postsCount: postsCount, 1043 + //associated?: ProfileAssociated, 1044 + //joinedViaStarterPack?: // AppBskyGraphDefs.StarterPackViewBasic; 1045 + indexedAt: row.createdat 1046 + ? new Date(row.createdat).toISOString() 1047 + : undefined, 1048 + createdAt: row.createdat 1049 + ? new Date(row.createdat).toISOString() 1050 + : undefined, 1051 + //viewer?: ViewerState, 1052 + //labels?: ComAtprotoLabelDefs.Label[], 1053 + pinnedPost: undefined, //row.; // TODO: i forgot to put pinnedp posts in db schema oops 1054 + //verification?: VerificationState, 1055 + //status?: StatusView, 1056 + }; 1057 + return result; 1058 + } 1059 + default: 1060 + throw new Error("Invalid type"); 1061 + } 1062 + } 1063 + 1064 + // post hydration 1065 + async queryPostView( 1066 + uri: string 1067 + ): Promise<ATPAPI.AppBskyFeedDefs.PostView | undefined> { 1068 + const URI = new AtUri(uri); 1069 + const did = URI.host; 1070 + if (!this.isRegisteredIndexUser(did)) return; 1071 + const db = this.userManager.getDbForDid(did); 1072 + if (!db) return; 1073 + 1074 + const stmt = db.prepare(` 1075 + SELECT * 1076 + FROM app_bsky_feed_post 1077 + WHERE uri = ? 1078 + LIMIT 1; 1079 + `); 1080 + 1081 + const row = stmt.get(uri) as PostRow; 1082 + const profileView = await this.queryProfileView(did, "Basic"); 1083 + if (!row || !row.cid || !profileView || !row.json) return; 1084 + const value = JSON.parse(row.json) as ATPAPI.AppBskyFeedPost.Record; 1085 + 1086 + const post: ATPAPI.AppBskyFeedDefs.PostView = { 1087 + uri: row.uri, 1088 + cid: row.cid, 1089 + author: profileView, 1090 + record: value, 1091 + indexedAt: new Date(row.indexedat).toISOString(), 1092 + embed: value.embed, 1093 + }; 1094 + 1095 + return post; 1096 + } 1097 + 1098 + constructPostViewRef( 1099 + uri: string 1100 + ): IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef { 1101 + const post: IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef = { 1102 + uri: uri, 1103 + cid: "cid.invalid", // oh shit we dont know the cid TODO: major design flaw 1104 + }; 1105 + 1106 + return post; 1107 + } 1108 + 1109 + async queryFeedViewPost( 1110 + uri: string 1111 + ): Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost | undefined> { 1112 + const post = await this.queryPostView(uri); 1113 + if (!post) return; 1114 + 1115 + const feedviewpost: ATPAPI.AppBskyFeedDefs.FeedViewPost = { 1116 + $type: "app.bsky.feed.defs#feedViewPost", 1117 + post: post, 1118 + //reply: ReplyRef, 1119 + //reason: , 1120 + }; 1121 + 1122 + return feedviewpost; 1123 + } 1124 + 1125 + constructFeedViewPostRef( 1126 + uri: string 1127 + ): IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef { 1128 + const post = this.constructPostViewRef(uri); 1129 + 1130 + const feedviewpostref: IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef = 1131 + { 1132 + $type: "party.whey.app.bsky.feed.defs#feedViewPostRef", 1133 + post: post as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>, 1134 + }; 1135 + 1136 + return feedviewpostref; 1137 + } 1138 + 1139 + // user feedgens 1140 + 1141 + async queryActorFeeds( 1142 + did: string 1143 + ): Promise<ATPAPI.AppBskyFeedDefs.GeneratorView[]> { 1144 + if (!this.isRegisteredIndexUser(did)) return []; 1145 + const db = this.userManager.getDbForDid(did); 1146 + if (!db) return []; 1147 + 1148 + const stmt = db.prepare(` 1149 + SELECT uri, cid, did, json, indexedat 1150 + FROM app_bsky_feed_generator 1151 + WHERE did = ? 1152 + ORDER BY createdat DESC; 1153 + `); 1154 + 1155 + const rows = stmt.all(did) as unknown as GeneratorRow[]; 1156 + const creatorView = await this.queryProfileView(did, "Basic"); 1157 + if (!creatorView) return []; 1158 + 1159 + return rows 1160 + .map((row) => { 1161 + try { 1162 + if (!row.json) return; 1163 + const record = JSON.parse( 1164 + row.json 1165 + ) as ATPAPI.AppBskyFeedGenerator.Record; 1166 + return { 1167 + $type: "app.bsky.feed.defs#generatorView", 1168 + uri: row.uri, 1169 + cid: row.cid, 1170 + did: row.did, 1171 + creator: creatorView, 1172 + displayName: record.displayName, 1173 + description: record.description, 1174 + descriptionFacets: record.descriptionFacets, 1175 + avatar: record.avatar, 1176 + likeCount: 0, // TODO: this should be easy 1177 + indexedAt: new Date(row.indexedat).toISOString(), 1178 + } as ATPAPI.AppBskyFeedDefs.GeneratorView; 1179 + } catch { 1180 + return undefined; 1181 + } 1182 + }) 1183 + .filter((v): v is ATPAPI.AppBskyFeedDefs.GeneratorView => !!v); 1184 + } 1185 + 1186 + async queryFeedGenerator( 1187 + uri: string 1188 + ): Promise<ATPAPI.AppBskyFeedDefs.GeneratorView | undefined> { 1189 + const gens = await this.queryFeedGenerators([uri]); // gens: GeneratorView[] 1190 + return gens[0]; 1191 + } 1192 + 1193 + async queryFeedGenerators( 1194 + uris: string[] 1195 + ): Promise<ATPAPI.AppBskyFeedDefs.GeneratorView[]> { 1196 + const generators: ATPAPI.AppBskyFeedDefs.GeneratorView[] = []; 1197 + const urisByDid = new Map<string, string[]>(); 1198 + 1199 + for (const uri of uris) { 1200 + try { 1201 + const { host: did } = new AtUri(uri); 1202 + if (!urisByDid.has(did)) { 1203 + urisByDid.set(did, []); 1204 + } 1205 + urisByDid.get(did)!.push(uri); 1206 + } catch {} 1207 + } 1208 + 1209 + for (const [did, didUris] of urisByDid.entries()) { 1210 + if (!this.isRegisteredIndexUser(did)) continue; 1211 + const db = this.userManager.getDbForDid(did); 1212 + if (!db) continue; 1213 + 1214 + const placeholders = didUris.map(() => "?").join(","); 1215 + const stmt = db.prepare(` 1216 + SELECT uri, cid, did, json, indexedat 1217 + FROM app_bsky_feed_generator 1218 + WHERE uri IN (${placeholders}); 1219 + `); 1220 + 1221 + const rows = stmt.all(...didUris) as unknown as GeneratorRow[]; 1222 + if (rows.length === 0) continue; 1223 + 1224 + const creatorView = await this.queryProfileView(did, ""); 1225 + if (!creatorView) continue; 1226 + 1227 + for (const row of rows) { 1228 + try { 1229 + if (!row.json || !row.cid) continue; 1230 + const record = JSON.parse( 1231 + row.json 1232 + ) as ATPAPI.AppBskyFeedGenerator.Record; 1233 + generators.push({ 1234 + $type: "app.bsky.feed.defs#generatorView", 1235 + uri: row.uri, 1236 + cid: row.cid, 1237 + did: row.did, 1238 + creator: creatorView, 1239 + displayName: record.displayName, 1240 + description: record.description, 1241 + descriptionFacets: record.descriptionFacets, 1242 + avatar: record.avatar as string | undefined, 1243 + likeCount: 0, 1244 + indexedAt: new Date(row.indexedat).toISOString(), 1245 + }); 1246 + } catch {} 1247 + } 1248 + } 1249 + return generators; 1250 + } 1251 + 1252 + // user feeds 1253 + 1254 + async queryAuthorFeedPartial( 1255 + did: string, 1256 + cursor?: string 1257 + ): Promise< 1258 + | { 1259 + items: ( 1260 + | ATPAPI.AppBskyFeedDefs.FeedViewPost 1261 + | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef 1262 + )[]; 1263 + cursor: string | undefined; 1264 + } 1265 + | undefined 1266 + > { 1267 + if (!this.isRegisteredIndexUser(did)) return; 1268 + const db = this.userManager.getDbForDid(did); 1269 + if (!db) return; 1270 + 1271 + const subquery = ` 1272 + SELECT uri, cid, indexedat, 'post' as type, null as subject 1273 + FROM app_bsky_feed_post 1274 + WHERE did = ? 1275 + UNION ALL 1276 + SELECT uri, cid, indexedat, 'repost' as type, subject 1277 + FROM app_bsky_feed_repost 1278 + WHERE did = ? 1279 + `; 1280 + 1281 + let query = `SELECT * FROM (${subquery}) as feed_items`; 1282 + const params: (string | number)[] = [did, did]; 1283 + 1284 + if (cursor) { 1285 + const [indexedat, cid] = cursor.split("::"); 1286 + query += ` WHERE (indexedat < ? OR (indexedat = ? AND cid < ?))`; 1287 + params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid); 1288 + } 1289 + 1290 + query += ` ORDER BY indexedat DESC, cid DESC LIMIT ${FEED_LIMIT}`; 1291 + 1292 + const stmt = db.prepare(query); 1293 + const rows = stmt.all(...params) as { 1294 + uri: string; 1295 + indexedat: number; 1296 + cid: string; 1297 + type: "post" | "repost"; 1298 + subject: string | null; 1299 + }[]; 1300 + 1301 + const authorProfile = await this.queryProfileView(did, "Basic"); 1302 + 1303 + const items = await Promise.all( 1304 + rows 1305 + .map((row) => { 1306 + if (row.type === "repost" && row.subject) { 1307 + const subjectDid = new AtUri(row.subject).host; 1308 + 1309 + const originalPost = this.handlesDid(subjectDid) 1310 + ? this.queryFeedViewPost(row.subject) 1311 + : this.constructFeedViewPostRef(row.subject); 1312 + 1313 + if (!originalPost || !authorProfile) return null; 1314 + 1315 + return { 1316 + post: originalPost, 1317 + reason: { 1318 + $type: "app.bsky.feed.defs#reasonRepost", 1319 + by: authorProfile, 1320 + indexedAt: new Date(row.indexedat).toISOString(), 1321 + }, 1322 + }; 1323 + } else { 1324 + return this.queryFeedViewPost(row.uri); 1325 + } 1326 + }) 1327 + .filter((p): p is Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost> => !!p) 1328 + ); 1329 + 1330 + const lastItem = rows[rows.length - 1]; 1331 + const nextCursor = lastItem 1332 + ? `${lastItem.indexedat}::${lastItem.cid}` 1333 + : undefined; 1334 + 1335 + return { items, cursor: nextCursor }; 1336 + } 1337 + 1338 + queryListFeed( 1339 + uri: string, 1340 + cursor?: string 1341 + ): 1342 + | { 1343 + items: ATPAPI.AppBskyFeedDefs.FeedViewPost[]; 1344 + cursor: string | undefined; 1345 + } 1346 + | undefined { 1347 + return { items: [], cursor: undefined }; 1348 + } 1349 + 1350 + async queryActorLikesPartial( 1351 + did: string, 1352 + cursor?: string 1353 + ): Promise< 1354 + | { 1355 + items: ( 1356 + | ATPAPI.AppBskyFeedDefs.FeedViewPost 1357 + | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef 1358 + )[]; 1359 + cursor: string | undefined; 1360 + } 1361 + | undefined 1362 + > { 1363 + // early return only if the actor did is not registered 1364 + if (!this.isRegisteredIndexUser(did)) return; 1365 + const db = this.userManager.getDbForDid(did); 1366 + if (!db) return; 1367 + 1368 + let query = ` 1369 + SELECT subject, indexedat, cid 1370 + FROM app_bsky_feed_like 1371 + WHERE did = ? 1372 + `; 1373 + const params: (string | number)[] = [did]; 1374 + 1375 + if (cursor) { 1376 + const [indexedat, cid] = cursor.split("::"); 1377 + query += ` AND (indexedat < ? OR (indexedat = ? AND cid < ?))`; 1378 + params.push(parseInt(indexedat, 10), parseInt(indexedat, 10), cid); 1379 + } 1380 + 1381 + query += ` ORDER BY indexedat DESC, cid DESC LIMIT ${FEED_LIMIT}`; 1382 + 1383 + const stmt = db.prepare(query); 1384 + const rows = stmt.all(...params) as { 1385 + subject: string; 1386 + indexedat: number; 1387 + cid: string; 1388 + }[]; 1389 + 1390 + const items = await Promise.all( 1391 + rows 1392 + .map(async (row) => { 1393 + const subjectDid = new AtUri(row.subject).host; 1394 + 1395 + if (this.handlesDid(subjectDid)) { 1396 + return await this.queryFeedViewPost(row.subject); 1397 + } else { 1398 + return this.constructFeedViewPostRef(row.subject); 1399 + } 1400 + }) 1401 + .filter( 1402 + ( 1403 + p 1404 + ): p is Promise< 1405 + | ATPAPI.AppBskyFeedDefs.FeedViewPost 1406 + | IndexServerAPI.PartyWheyAppBskyFeedDefs.FeedViewPostRef 1407 + > => !!p 1408 + ) 1409 + ); 1410 + 1411 + const lastItem = rows[rows.length - 1]; 1412 + const nextCursor = lastItem 1413 + ? `${lastItem.indexedat}::${lastItem.cid}` 1414 + : undefined; 1415 + 1416 + return { items, cursor: nextCursor }; 1417 + } 1418 + 1419 + // post metadata 1420 + 1421 + async queryLikes( 1422 + uri: string 1423 + ): Promise<ATPAPI.AppBskyFeedGetLikes.Like[] | undefined> { 1424 + const postUri = new AtUri(uri); 1425 + const postAuthorDid = postUri.hostname; 1426 + if (!this.isRegisteredIndexUser(postAuthorDid)) return; 1427 + const db = this.userManager.getDbForDid(postAuthorDid); 1428 + if (!db) return; 1429 + 1430 + const stmt = db.prepare(` 1431 + SELECT b.srcdid, b.srcuri 1432 + FROM backlink_skeleton AS b 1433 + WHERE b.suburi = ? AND b.srccol = 'app_bsky_feed_like' 1434 + ORDER BY b.id DESC; 1435 + `); 1436 + 1437 + const rows = stmt.all(uri) as unknown as BacklinkRow[]; 1438 + 1439 + return await Promise.all( 1440 + rows 1441 + .map(async (row) => { 1442 + const actor = await this.queryProfileView(row.srcdid, ""); 1443 + if (!actor) return; 1444 + 1445 + return { 1446 + // TODO write indexedAt for spacedust indexes 1447 + createdAt: new Date(Date.now()).toISOString(), 1448 + indexedAt: new Date(Date.now()).toISOString(), 1449 + actor: actor, 1450 + }; 1451 + }) 1452 + .filter( 1453 + (like): like is Promise<ATPAPI.AppBskyFeedGetLikes.Like> => !!like 1454 + ) 1455 + ); 1456 + } 1457 + 1458 + async queryReposts( 1459 + uri: string 1460 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileView[]> { 1461 + const postUri = new AtUri(uri); 1462 + const postAuthorDid = postUri.hostname; 1463 + if (!this.isRegisteredIndexUser(postAuthorDid)) return []; 1464 + const db = this.userManager.getDbForDid(postAuthorDid); 1465 + if (!db) return []; 1466 + 1467 + const stmt = db.prepare(` 1468 + SELECT srcdid 1469 + FROM backlink_skeleton 1470 + WHERE suburi = ? AND srccol = 'app_bsky_feed_repost' 1471 + ORDER BY id DESC; 1472 + `); 1473 + 1474 + const rows = stmt.all(uri) as { srcdid: string }[]; 1475 + 1476 + return await Promise.all( 1477 + rows 1478 + .map(async (row) => await this.queryProfileView(row.srcdid, "")) 1479 + .filter((p): p is Promise<ATPAPI.AppBskyActorDefs.ProfileView> => !!p) 1480 + ); 1481 + } 1482 + 1483 + async queryQuotes( 1484 + uri: string 1485 + ): Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost[]> { 1486 + const postUri = new AtUri(uri); 1487 + const postAuthorDid = postUri.hostname; 1488 + if (!this.isRegisteredIndexUser(postAuthorDid)) return []; 1489 + const db = this.userManager.getDbForDid(postAuthorDid); 1490 + if (!db) return []; 1491 + 1492 + const stmt = db.prepare(` 1493 + SELECT srcuri 1494 + FROM backlink_skeleton 1495 + WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'quote' 1496 + ORDER BY id DESC; 1497 + `); 1498 + 1499 + const rows = stmt.all(uri) as { srcuri: string }[]; 1500 + 1501 + return await Promise.all( 1502 + rows 1503 + .map(async (row) => await this.queryFeedViewPost(row.srcuri)) 1504 + .filter((p): p is Promise<ATPAPI.AppBskyFeedDefs.FeedViewPost> => !!p) 1505 + ); 1506 + } 1507 + async _getPostViewUnion( 1508 + uri: string 1509 + ): Promise< 1510 + | ATPAPI.AppBskyFeedDefs.PostView 1511 + | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef 1512 + | undefined 1513 + > { 1514 + try { 1515 + const postDid = new AtUri(uri).hostname; 1516 + if (this.handlesDid(postDid)) { 1517 + return await this.queryPostView(uri); 1518 + } else { 1519 + return this.constructPostViewRef(uri); 1520 + } 1521 + } catch (_e) { 1522 + return undefined; 1523 + } 1524 + } 1525 + async queryPostThreadPartial( 1526 + uri: string 1527 + ): Promise< 1528 + | IndexServerTypes.PartyWheyAppBskyFeedGetPostThreadPartial.OutputSchema 1529 + | undefined 1530 + > { 1531 + const post = await this._getPostViewUnion(uri); 1532 + 1533 + if (!post) { 1534 + return { 1535 + thread: { 1536 + $type: "app.bsky.feed.defs#notFoundPost", 1537 + uri: uri, 1538 + notFound: true, 1539 + } as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.NotFoundPost>, 1540 + }; 1541 + } 1542 + 1543 + const thread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = { 1544 + $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", 1545 + post: post as 1546 + | ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView> 1547 + | IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>, 1548 + replies: [], 1549 + }; 1550 + 1551 + let current = thread; 1552 + // we can only climb the parent tree if we have the full post record. 1553 + // which is not implemented yet (sad i know) 1554 + if ( 1555 + isPostView(current.post) && 1556 + isFeedPostRecord(current.post.record) && 1557 + current.post.record?.reply?.parent?.uri 1558 + ) { 1559 + let parentUri: string | undefined = current.post.record.reply.parent.uri; 1560 + 1561 + // keep climbing as long as we find a valid parent post. 1562 + while (parentUri) { 1563 + const parentPost = await this._getPostViewUnion(parentUri); 1564 + if (!parentPost) break; // stop if a parent in the chain is not found. 1565 + 1566 + const parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = 1567 + { 1568 + $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", 1569 + post: parentPost as ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView>, 1570 + replies: [ 1571 + current as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>, 1572 + ], 1573 + }; 1574 + current.parent = 1575 + parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>; 1576 + current = parentThread; 1577 + 1578 + // check if the new current post has a parent to continue the loop 1579 + parentUri = 1580 + isPostView(current.post) && isFeedPostRecord(current.post.record) 1581 + ? current.post.record?.reply?.parent?.uri 1582 + : undefined; 1583 + } 1584 + } 1585 + 1586 + const seenUris = new Set<string>(); 1587 + const fetchReplies = async ( 1588 + parentThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef 1589 + ) => { 1590 + if (!parentThread.post || !("uri" in parentThread.post)) { 1591 + return; 1592 + } 1593 + if (seenUris.has(parentThread.post.uri)) return; 1594 + seenUris.add(parentThread.post.uri); 1595 + 1596 + const parentUri = new AtUri(parentThread.post.uri); 1597 + const parentAuthorDid = parentUri.hostname; 1598 + 1599 + // replies can only be discovered for local posts where we have the backlink data 1600 + if (!this.handlesDid(parentAuthorDid)) return; 1601 + 1602 + const db = this.userManager.getDbForDid(parentAuthorDid); 1603 + if (!db) return; 1604 + 1605 + const stmt = db.prepare(` 1606 + SELECT srcuri 1607 + FROM backlink_skeleton 1608 + WHERE suburi = ? AND srccol = 'app_bsky_feed_post' AND srcfield = 'replyparent' 1609 + `); 1610 + const replyRows = stmt.all(parentThread.post.uri) as { srcuri: string }[]; 1611 + 1612 + const replies = await Promise.all( 1613 + replyRows 1614 + .map(async (row) => await this._getPostViewUnion(row.srcuri)) 1615 + .filter( 1616 + ( 1617 + p 1618 + ): p is Promise< 1619 + | ATPAPI.AppBskyFeedDefs.PostView 1620 + | IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef 1621 + > => !!p 1622 + ) 1623 + ); 1624 + 1625 + for (const replyPost of replies) { 1626 + const replyThread: IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef = 1627 + { 1628 + $type: "party.whey.app.bsky.feed.defs#threadViewPostRef", 1629 + post: replyPost as 1630 + | ATPAPI.$Typed<ATPAPI.AppBskyFeedDefs.PostView> 1631 + | IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.PostViewRef>, 1632 + parent: 1633 + parentThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>, 1634 + replies: [], 1635 + }; 1636 + parentThread.replies?.push( 1637 + replyThread as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef> 1638 + ); 1639 + fetchReplies(replyThread); // recurse 1640 + } 1641 + }; 1642 + 1643 + fetchReplies(thread); 1644 + 1645 + const returned = 1646 + current as unknown as IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef; 1647 + 1648 + return { 1649 + thread: 1650 + returned as IndexServerUtils.$Typed<IndexServerAPI.PartyWheyAppBskyFeedDefs.ThreadViewPostRef>, 1651 + }; 1652 + } 1653 + 1654 + /** 1655 + * please do not use this, use openDbForDid() instead 1656 + * @param did 1657 + * @returns 1658 + */ 1659 + internalCreateDbForDid(did: string): Database { 1660 + const path = `${this.config.baseDbPath}/${did}.sqlite`; 1661 + const db = new Database(path); 1662 + setupUserDb(db); 1663 + //await db.exec(/* CREATE IF NOT EXISTS statements */); 1664 + return db; 1665 + } 1666 + /** 1667 + * @deprecated use handlesDid() instead 1668 + * @param did 1669 + * @returns 1670 + */ 1671 + isRegisteredIndexUser(did: string): boolean { 1672 + const stmt = this.systemDB.prepare(` 1673 + SELECT 1 1674 + FROM users 1675 + WHERE did = ? 1676 + AND onboardingstatus != 'onboarding-backfill' 1677 + LIMIT 1; 1678 + `); 1679 + const result = stmt.value<[number]>(did); 1680 + const exists = result !== undefined; 1681 + return exists; 1682 + } 1683 + } 1684 + 1685 + export class IndexServerUserManager { 1686 + public indexServer: IndexServer; 1687 + 1688 + constructor(indexServer: IndexServer) { 1689 + this.indexServer = indexServer; 1690 + } 1691 + 1692 + public users = new Map<string, UserIndexServer>(); 1693 + public handlesDid(did: string): boolean { 1694 + return this.users.has(did); 1695 + } 1696 + 1697 + /*async*/ addUser(did: string) { 1698 + if (this.users.has(did)) return; 1699 + const instance = new UserIndexServer(this, did); 1700 + //await instance.initialize(); 1701 + this.users.set(did, instance); 1702 + } 1703 + 1704 + // async handleRequest({ 1705 + // did, 1706 + // route, 1707 + // req, 1708 + // }: { 1709 + // did: string; 1710 + // route: string; 1711 + // req: Request; 1712 + // }) { 1713 + // if (!this.users.has(did)) await this.addUser(did); 1714 + // const user = this.users.get(did)!; 1715 + // return await user.handleHttpRequest(route, req); 1716 + // } 1717 + 1718 + removeUser(did: string) { 1719 + const instance = this.users.get(did); 1720 + if (!instance) return; 1721 + /*await*/ instance.shutdown(); 1722 + this.users.delete(did); 1723 + } 1724 + 1725 + getDbForDid(did: string): Database | null { 1726 + if (!this.users.has(did)) { 1727 + return null; 1728 + } 1729 + return this.users.get(did)?.db ?? null; 1730 + } 1731 + 1732 + coldStart(db: Database) { 1733 + const rows = db.prepare("SELECT did FROM users").all(); 1734 + for (const row of rows) { 1735 + this.addUser(row.did); 1736 + } 1737 + } 1738 + } 1739 + 1740 + class UserIndexServer { 1741 + public indexServerUserManager: IndexServerUserManager; 1742 + did: string; 1743 + db: Database; // | undefined; 1744 + jetstream: JetstreamManager; // | undefined; 1745 + spacedust: SpacedustManager; // | undefined; 1746 + 1747 + constructor(indexServerUserManager: IndexServerUserManager, did: string) { 1748 + this.did = did; 1749 + this.indexServerUserManager = indexServerUserManager; 1750 + this.db = this.indexServerUserManager.indexServer.internalCreateDbForDid( 1751 + this.did 1752 + ); 1753 + // should probably put the params of exactly what were listening to here 1754 + this.jetstream = new JetstreamManager((msg) => { 1755 + console.log("Received Jetstream message: ", msg); 1756 + 1757 + const op = msg.commit.operation; 1758 + const doer = msg.did; 1759 + const rev = msg.commit.rev; 1760 + const aturi = `${msg.did}/${msg.commit.collection}/${msg.commit.rkey}`; 1761 + const value = msg.commit.record; 1762 + 1763 + if (!doer || !value) return; 1764 + this.indexServerUserManager.indexServer.indexServerIndexer({ 1765 + op, 1766 + doer, 1767 + cid: msg.commit.cid, 1768 + rev, 1769 + aturi, 1770 + value, 1771 + indexsrc: `jetstream-${op}`, 1772 + db: this.db, 1773 + }); 1774 + }); 1775 + this.jetstream.start({ 1776 + // for realsies pls get from db or something instead of this shit 1777 + wantedDids: [ 1778 + this.did, 1779 + // "did:plc:mn45tewwnse5btfftvd3powc", 1780 + // "did:plc:yy6kbriyxtimkjqonqatv2rb", 1781 + // "did:plc:zzhzjga3ab5fcs2vnsv2ist3", 1782 + // "did:plc:jz4ibztn56hygfld6j6zjszg", 1783 + ], 1784 + wantedCollections: [ 1785 + "app.bsky.actor.profile", 1786 + "app.bsky.feed.generator", 1787 + "app.bsky.feed.like", 1788 + "app.bsky.feed.post", 1789 + "app.bsky.feed.repost", 1790 + "app.bsky.feed.threadgate", 1791 + "app.bsky.graph.block", 1792 + "app.bsky.graph.follow", 1793 + "app.bsky.graph.list", 1794 + "app.bsky.graph.listblock", 1795 + "app.bsky.graph.listitem", 1796 + "app.bsky.notification.declaration", 1797 + ], 1798 + }); 1799 + //await connectToJetstream(this.did, this.db); 1800 + this.spacedust = new SpacedustManager((msg: SpacedustLinkMessage) => { 1801 + console.log("Received Spacedust message: ", msg); 1802 + const operation = msg.link.operation; 1803 + 1804 + const sourceURI = new AtUri(msg.link.source_record); 1805 + const srcUri = msg.link.source_record; 1806 + const srcDid = sourceURI.host; 1807 + const srcField = msg.link.source; 1808 + const srcCol = sourceURI.collection; 1809 + const subjectURI = new AtUri(msg.link.subject); 1810 + const subUri = msg.link.subject; 1811 + const subDid = subjectURI.host; 1812 + const subCol = subjectURI.collection; 1813 + 1814 + if (operation === "delete") { 1815 + this.db.run( 1816 + `DELETE FROM backlink_skeleton 1817 + WHERE srcuri = ? AND srcfield = ? AND suburi = ?`, 1818 + [srcUri, srcField, subUri] 1819 + ); 1820 + } else if (operation === "create") { 1821 + this.db.run( 1822 + `INSERT OR REPLACE INTO backlink_skeleton ( 1823 + srcuri, 1824 + srcdid, 1825 + srcfield, 1826 + srccol, 1827 + suburi, 1828 + subdid, 1829 + subcol, 1830 + indexedAt 1831 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 1832 + [ 1833 + srcUri, // full AT URI of the source record 1834 + srcDid, // did: of the source 1835 + srcField, // e.g., "reply.parent.uri" or "facets.features.did" 1836 + srcCol, // e.g., "app.bsky.feed.post" 1837 + subUri, // full AT URI of the subject (linked record) 1838 + subDid, // did: of the subject 1839 + subCol, // subject collection (can be inferred or passed) 1840 + Date.now() 1841 + ] 1842 + ); 1843 + } 1844 + }); 1845 + this.spacedust.start({ 1846 + wantedSources: [ 1847 + "app.bsky.feed.like:subject.uri", // like 1848 + "app.bsky.feed.like:via.uri", // liked repost 1849 + "app.bsky.feed.repost:subject.uri", // repost 1850 + "app.bsky.feed.repost:via.uri", // reposted repost 1851 + "app.bsky.feed.post:reply.root.uri", // thread OP 1852 + "app.bsky.feed.post:reply.parent.uri", // direct parent 1853 + "app.bsky.feed.post:embed.media.record.record.uri", // quote with media 1854 + "app.bsky.feed.post:embed.record.uri", // quote without media 1855 + "app.bsky.feed.threadgate:post", // threadgate subject 1856 + "app.bsky.feed.threadgate:hiddenReplies", // threadgate items (array) 1857 + "app.bsky.feed.post:facets.features.did", // facet item (array): mention 1858 + "app.bsky.graph.block:subject", // blocks 1859 + "app.bsky.graph.follow:subject", // follow 1860 + "app.bsky.graph.listblock:subject", // list item (blocks) 1861 + "app.bsky.graph.listblock:list", // blocklist mention (might not exist) 1862 + "app.bsky.graph.listitem:subject", // list item (blocks) 1863 + "app.bsky.graph.listitem:list", // list mention 1864 + ], 1865 + // should be getting from DB but whatever right 1866 + wantedSubjects: [ 1867 + // as noted i dont need to write down each post, just the user to listen to ! 1868 + // hell yeah 1869 + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybv7b6ic2h", 1870 + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybws4avc2h", 1871 + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvvkcxcscs2h", 1872 + // "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3l63ogxocq42f", 1873 + // "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3lw3wamvflu23", 1874 + ], 1875 + wantedSubjectDids: [ 1876 + this.did, 1877 + //"did:plc:mn45tewwnse5btfftvd3powc", 1878 + //"did:plc:yy6kbriyxtimkjqonqatv2rb", 1879 + //"did:plc:zzhzjga3ab5fcs2vnsv2ist3", 1880 + //"did:plc:jz4ibztn56hygfld6j6zjszg", 1881 + ], 1882 + instant: ["true"] 1883 + }); 1884 + //await connectToConstellation(this.did, this.db); 6 1885 } 7 1886 8 - return new Response("Not Found", { status: 404 }); 9 - } 1887 + // initialize() { 1888 + 1889 + // } 1890 + 1891 + // async handleHttpRequest(route: string, req: Request): Promise<Response> { 1892 + // if (route === "posts") { 1893 + // const posts = await this.queryPosts(); 1894 + // return new Response(JSON.stringify(posts), { 1895 + // headers: { "content-type": "application/json" }, 1896 + // }); 1897 + // } 1898 + 1899 + // return new Response("Unknown route", { status: 404 }); 1900 + // } 1901 + 1902 + // private async queryPosts() { 1903 + // return this.db.run( 1904 + // "SELECT * FROM posts ORDER BY created_at DESC LIMIT 100" 1905 + // ); 1906 + // } 1907 + 1908 + shutdown() { 1909 + this.jetstream.stop(); 1910 + this.spacedust.stop(); 1911 + this.db.close?.(); 1912 + } 1913 + } 1914 + 1915 + // /** 1916 + // * please do not use this, use openDbForDid() instead 1917 + // * @param did 1918 + // * @returns 1919 + // */ 1920 + // function internalCreateDbForDid(did: string): Database { 1921 + // const path = `./dbs/${did}.sqlite`; 1922 + // const db = new Database(path); 1923 + // setupUserDb(db); 1924 + // //await db.exec(/* CREATE IF NOT EXISTS statements */); 1925 + // return db; 1926 + // } 1927 + 1928 + // function getDbForDid(did: string): Database | undefined { 1929 + // const db = indexerUserManager.getDbForDid(did); 1930 + // if (!db) return; 1931 + // return db; 1932 + // } 1933 + 1934 + // async function connectToJetstream(did: string, db: Database): Promise<WebSocket> { 1935 + // const url = `${jetstreamurl}/xrpc/com.atproto.sync.subscribeRepos?did=${did}`; 1936 + // const ws = new WebSocket(url); 1937 + // ws.onmessage = (msg) => { 1938 + // //handleJetstreamMessage(evt.data, db) 1939 + 1940 + // const op = msg.commit.operation; 1941 + // const doer = msg.did; 1942 + // const rev = msg.commit.rev; 1943 + // const aturi = `${msg.did}/${msg.commit.collection}/${msg.commit.rkey}`; 1944 + // const value = msg.commit.record; 1945 + 1946 + // if (!doer || !value) return; 1947 + // indexServerIndexer({ 1948 + // op, 1949 + // doer, 1950 + // rev, 1951 + // aturi, 1952 + // value, 1953 + // indexsrc: "onboarding_backfill", 1954 + // userdbname: did, 1955 + // }) 1956 + // }; 1957 + 1958 + // return ws; 1959 + // } 1960 + 1961 + // async function connectToConstellation(did: string, db: D1Database): Promise<WebSocket> { 1962 + // const url = `wss://bsky.social/xrpc/com.atproto.sync.subscribeLabels?did=${did}`; 1963 + // const ws = new WebSocket(url); 1964 + // ws.onmessage = (evt) => handleConstellationMessage(evt.data, db); 1965 + // return ws; 1966 + // } 1967 + 1968 + type PostRow = { 1969 + uri: string; 1970 + did: string; 1971 + cid: string | null; 1972 + rev: string | null; 1973 + createdat: number | null; 1974 + indexedat: number; 1975 + json: string | null; 1976 + 1977 + text: string | null; 1978 + replyroot: string | null; 1979 + replyparent: string | null; 1980 + quote: string | null; 1981 + 1982 + imagecount: number | null; 1983 + image1cid: string | null; 1984 + image1mime: string | null; 1985 + image1aspect: string | null; 1986 + image2cid: string | null; 1987 + image2mime: string | null; 1988 + image2aspect: string | null; 1989 + image3cid: string | null; 1990 + image3mime: string | null; 1991 + image3aspect: string | null; 1992 + image4cid: string | null; 1993 + image4mime: string | null; 1994 + image4aspect: string | null; 1995 + 1996 + videocount: number | null; 1997 + videocid: string | null; 1998 + videomime: string | null; 1999 + videoaspect: string | null; 2000 + }; 2001 + 2002 + type ProfileRow = { 2003 + uri: string; 2004 + cid: string | null; 2005 + rev: string | null; 2006 + createdat: number | null; 2007 + indexedat: number; 2008 + json: string | null; 2009 + displayname: string | null; 2010 + description: string | null; 2011 + avatarcid: string | null; 2012 + avatarmime: string | null; 2013 + bannercid: string | null; 2014 + bannermime: string | null; 2015 + }; 2016 + 2017 + type linksQuery = { 2018 + target: string; 2019 + collection: string; 2020 + path: string; 2021 + cursor?: string; 2022 + }; 2023 + type linksRecord = { 2024 + did: string; 2025 + collection: string; 2026 + rkey: string; 2027 + }; 2028 + type linksRecordsResponse = { 2029 + total: string; 2030 + linking_records: linksRecord[]; 2031 + cursor?: string; 2032 + }; 2033 + type linksDidsResponse = { 2034 + total: string; 2035 + linking_dids: string[]; 2036 + cursor?: string; 2037 + }; 2038 + type linksCountResponse = { 2039 + total: string; 2040 + }; 2041 + type linksAllResponse = { 2042 + links: Record< 2043 + string, 2044 + Record< 2045 + string, 2046 + { 2047 + records: number; 2048 + distinct_dids: number; 2049 + } 2050 + > 2051 + >; 2052 + }; 2053 + 2054 + type linksAllQuery = { 2055 + target: string; 2056 + }; 2057 + 2058 + const SQL = { 2059 + links: ` 2060 + SELECT srcuri, srcdid, srccol 2061 + FROM backlink_skeleton 2062 + WHERE suburi = ? AND srccol = ? AND srcfield = ? 2063 + `, 2064 + distinctDids: ` 2065 + SELECT DISTINCT srcdid 2066 + FROM backlink_skeleton 2067 + WHERE suburi = ? AND srccol = ? AND srcfield = ? 2068 + `, 2069 + count: ` 2070 + SELECT COUNT(*) as total 2071 + FROM backlink_skeleton 2072 + WHERE suburi = ? AND srccol = ? AND srcfield = ? 2073 + `, 2074 + countDistinctDids: ` 2075 + SELECT COUNT(DISTINCT srcdid) as total 2076 + FROM backlink_skeleton 2077 + WHERE suburi = ? AND srccol = ? AND srcfield = ? 2078 + `, 2079 + all: ` 2080 + SELECT suburi, srccol, COUNT(*) as records, COUNT(DISTINCT srcdid) as distinct_dids 2081 + FROM backlink_skeleton 2082 + WHERE suburi = ? 2083 + GROUP BY suburi, srccol 2084 + `, 2085 + }; 2086 + 2087 + export function isDid(str: string): boolean { 2088 + return typeof str === "string" && str.startsWith("did:"); 2089 + } 2090 + 2091 + function isFeedPostRecord( 2092 + post: unknown 2093 + ): post is ATPAPI.AppBskyFeedPost.Record { 2094 + return ( 2095 + typeof post === "object" && 2096 + post !== null && 2097 + "$type" in post && 2098 + (post as any).$type === "app.bsky.feed.post" 2099 + ); 2100 + } 2101 + 2102 + function isImageEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedImages.Main { 2103 + return ( 2104 + typeof embed === "object" && 2105 + embed !== null && 2106 + "$type" in embed && 2107 + (embed as any).$type === "app.bsky.embed.images" 2108 + ); 2109 + } 2110 + 2111 + function isVideoEmbed(embed: unknown): embed is ATPAPI.AppBskyEmbedVideo.Main { 2112 + return ( 2113 + typeof embed === "object" && 2114 + embed !== null && 2115 + "$type" in embed && 2116 + (embed as any).$type === "app.bsky.embed.video" 2117 + ); 2118 + } 2119 + 2120 + function isRecordEmbed( 2121 + embed: unknown 2122 + ): embed is ATPAPI.AppBskyEmbedRecord.Main { 2123 + return ( 2124 + typeof embed === "object" && 2125 + embed !== null && 2126 + "$type" in embed && 2127 + (embed as any).$type === "app.bsky.embed.record" 2128 + ); 2129 + } 2130 + 2131 + function isRecordWithMediaEmbed( 2132 + embed: unknown 2133 + ): embed is ATPAPI.AppBskyEmbedRecordWithMedia.Main { 2134 + return ( 2135 + typeof embed === "object" && 2136 + embed !== null && 2137 + "$type" in embed && 2138 + (embed as any).$type === "app.bsky.embed.recordWithMedia" 2139 + ); 2140 + } 2141 + 2142 + export function uncid(anything: any): string | null { 2143 + return ( 2144 + ((anything as Record<string, unknown>)?.["$link"] as string | null) || null 2145 + ); 2146 + } 2147 + 2148 + function extractImages(embed: unknown) { 2149 + if (isImageEmbed(embed)) return embed.images; 2150 + if (isRecordWithMediaEmbed(embed) && isImageEmbed(embed.media)) 2151 + return embed.media.images; 2152 + return []; 2153 + } 2154 + 2155 + function extractVideo(embed: unknown) { 2156 + if (isVideoEmbed(embed)) return embed; 2157 + if (isRecordWithMediaEmbed(embed) && isVideoEmbed(embed.media)) 2158 + return embed.media; 2159 + return null; 2160 + } 2161 + 2162 + function extractQuoteUri(embed: unknown): string | null { 2163 + if (isRecordEmbed(embed)) return embed.record.uri; 2164 + if (isRecordWithMediaEmbed(embed)) return embed.record.record.uri; 2165 + return null; 2166 + }
+255
main-index.ts
··· 1 + import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts"; 2 + import { setupSystemDb } from "./utils/dbsystem.ts"; 3 + import { didDocument } from "./utils/diddoc.ts"; 4 + import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts"; 5 + import { IndexServer, IndexServerConfig } from "./indexserver.ts"; 6 + import { extractDid } from "./utils/identity.ts"; 7 + import { config } from "./config.ts"; 8 + import { compile, devWatch } from "./shared-landing/build.ts"; 9 + // ------------------------------------------ 10 + // AppView Setup 11 + // ------------------------------------------ 12 + 13 + const indexServerConfig: IndexServerConfig = { 14 + baseDbPath: "./dbs/index/registered-users", // The directory for user databases 15 + systemDbPath: "./dbs/index/registered-users/system.db", // The path for the main system database 16 + }; 17 + export const genericIndexServer = new IndexServer(indexServerConfig); 18 + setupSystemDb(genericIndexServer.systemDB); 19 + 20 + let { js, html, css } = await compile({ 21 + target: "index", 22 + initialData: { 23 + config: config.indexServer, 24 + users: (await genericIndexServer.unspeccedGetRegisteredUsers()) ?? [], 25 + }, 26 + }); 27 + 28 + // add me lol 29 + genericIndexServer.systemDB.exec(` 30 + INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) 31 + VALUES ( 32 + 'did:plc:mn45tewwnse5btfftvd3powc', 33 + 'admin', 34 + datetime('now'), 35 + 'ready' 36 + ); 37 + 38 + INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) 39 + VALUES ( 40 + 'did:web:did12.whey.party', 41 + 'admin', 42 + datetime('now'), 43 + 'ready' 44 + ); 45 + `); 46 + 47 + genericIndexServer.start(); 48 + 49 + // ------------------------------------------ 50 + // XRPC Method Implementations 51 + // ------------------------------------------ 52 + 53 + // const indexServerRoutes = new Set([ 54 + // "/xrpc/app.bsky.actor.getProfile", 55 + // "/xrpc/app.bsky.actor.getProfiles", 56 + // "/xrpc/app.bsky.feed.getActorFeeds", 57 + // "/xrpc/app.bsky.feed.getFeedGenerator", 58 + // "/xrpc/app.bsky.feed.getFeedGenerators", 59 + // "/xrpc/app.bsky.feed.getPosts", 60 + // "/xrpc/party.whey.app.bsky.feed.getActorLikesPartial", 61 + // "/xrpc/party.whey.app.bsky.feed.getAuthorFeedPartial", 62 + // "/xrpc/party.whey.app.bsky.feed.getLikesPartial", 63 + // "/xrpc/party.whey.app.bsky.feed.getPostThreadPartial", 64 + // "/xrpc/party.whey.app.bsky.feed.getQuotesPartial", 65 + // "/xrpc/party.whey.app.bsky.feed.getRepostedByPartial", 66 + // // more federated endpoints, not planned yet, lexicons will come later 67 + // /* 68 + // app.bsky.graph.getLists // doesnt need to because theres no items[], and its self ProfileViewBasic 69 + // app.bsky.graph.getList // needs to be Partial-ed (items[] union with ProfileViewRef) 70 + // app.bsky.graph.getActorStarterPacks // maybe doesnt need to be Partial-ed because its self ProfileViewBasic 71 + 72 + // app.bsky.feed.getListFeed // uhh actually already exists its getListFeedPartial 73 + // */ 74 + // "/xrpc/party.whey.app.bsky.feed.getListFeedPartial", 75 + // ]); 76 + const placeholderselfcheckstatus = { 77 + "#skylite_index:/xrpc/app.bsky.actor.getProfile": "green", 78 + "#skylite_index:/xrpc/app.bsky.actor.getProfiles": "green", 79 + "#skylite_index:/xrpc/app.bsky.feed.getActorFeeds": "green", 80 + "#skylite_index:/xrpc/app.bsky.feed.getFeedGenerator": "green", 81 + "#skylite_index:/xrpc/app.bsky.feed.getFeedGenerators": "green", 82 + "#skylite_index:/xrpc/app.bsky.feed.getPosts": "green", 83 + "#skylite_index:/xrpc/app.bsky.graph.getLists": "black", 84 + "#skylite_index:/xrpc/app.bsky.graph.getList": "black", 85 + "#skylite_index:/xrpc/app.bsky.graph.getActorStarterPacks": "black", 86 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getActorLikesPartial": "green", 87 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getAuthorFeedPartial": "green", 88 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getLikesPartial": "orange", 89 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getPostThreadPartial": "green", 90 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getQuotesPartial": "orange", 91 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getRepostedByPartial": 92 + "orange", 93 + "#skylite_index:/xrpc/party.whey.app.bsky.feed.getListFeedPartial": "black", 94 + 95 + "constellation:/links": "green", 96 + "constellation:/links/distinct-dids": "green", 97 + "constellation:/links/count": "green", 98 + "constellation:/links/count/distinct-dids": "green", 99 + "constellation:/links/all": "green", 100 + }; 101 + 102 + //console.log("ready to serve"); 103 + Deno.serve( 104 + { port: config.indexServer.port }, 105 + async (req: Request): Promise<Response> => { 106 + const url = new URL(req.url); 107 + const pathname = url.pathname; 108 + const searchParams = searchParamsToJson(url.searchParams); 109 + 110 + const publicdir = "/public"; 111 + if (pathname.startsWith(publicdir)) { 112 + const filepath = decodeURIComponent(pathname.slice(publicdir.length)); 113 + try { 114 + const file = await Deno.open("./public" + filepath, { read: true }); 115 + return new Response(file.readable); 116 + } catch { 117 + return new Response("404 Not Found", { status: 404 }); 118 + } 119 + } 120 + 121 + const todopleasespecthis = "/_unspecced"; 122 + if (pathname.startsWith(todopleasespecthis)) { 123 + const unspeccedroute = decodeURIComponent( 124 + pathname.slice(todopleasespecthis.length) 125 + ); 126 + if (unspeccedroute === "/config") { 127 + const safeconfig = { 128 + inviteOnly: config.indexServer.inviteOnly, 129 + //port: number, 130 + did: config.indexServer.did, 131 + host: config.indexServer.host, 132 + }; 133 + return new Response(JSON.stringify(safeconfig), { 134 + headers: withCors({ 135 + "content-type": "application/json; charset=utf-8", 136 + }), 137 + }); 138 + } 139 + if (unspeccedroute === "/users") { 140 + const res = await genericIndexServer.unspeccedGetRegisteredUsers(); 141 + return new Response(JSON.stringify(res), { 142 + headers: withCors({ 143 + "content-type": "application/json; charset=utf-8", 144 + }), 145 + }); 146 + } 147 + if (unspeccedroute === "/apitest") { 148 + return new Response(JSON.stringify(placeholderselfcheckstatus), { 149 + headers: withCors({ 150 + "content-type": "application/json; charset=utf-8", 151 + }), 152 + }); 153 + } 154 + } 155 + 156 + if (html && js) { 157 + if (pathname === "/" || pathname === "") { 158 + return new Response(html, { 159 + headers: withCors({ "content-type": "text/html; charset=utf-8" }), 160 + }); 161 + } 162 + if (pathname === "/landing-index.js") { 163 + return new Response(js, { 164 + headers: withCors({ 165 + "content-type": "application/javascript; charset=utf-8", 166 + }), 167 + }); 168 + } 169 + } else { 170 + if (pathname === "/" || pathname === "") { 171 + return new Response(`server is compiling your webpage. loading...`, { 172 + headers: withCors({ "content-type": "text/html; charset=utf-8" }), 173 + }); 174 + } 175 + } 176 + if (pathname === "/app.css") { 177 + return new Response(css, { 178 + headers: withCors({ 179 + "content-type": "text/css; charset=utf-8", 180 + }), 181 + }); 182 + } 183 + 184 + if (pathname === "/.well-known/did.json") { 185 + return new Response( 186 + JSON.stringify( 187 + didDocument( 188 + "index", 189 + config.indexServer.did, 190 + config.indexServer.host, 191 + "whatever" 192 + ) 193 + ), 194 + { 195 + headers: withCors({ "Content-Type": "application/json" }), 196 + } 197 + ); 198 + } 199 + if (pathname === "/health") { 200 + return new Response("OK", { 201 + status: 200, 202 + headers: withCors({ 203 + "Content-Type": "text/plain", 204 + }), 205 + }); 206 + } 207 + if (req.method === "OPTIONS") { 208 + return new Response(null, { 209 + status: 204, 210 + headers: { 211 + "Access-Control-Allow-Origin": "*", 212 + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 213 + "Access-Control-Allow-Headers": "*", 214 + }, 215 + }); 216 + } 217 + console.log(`request for "${pathname}"`); 218 + const constellation = pathname.startsWith("/links"); 219 + 220 + if (constellation) { 221 + const target = searchParams?.target as string; 222 + const safeDid = extractDid(target); 223 + const targetserver = genericIndexServer.handlesDid(safeDid); 224 + if (targetserver) { 225 + return genericIndexServer.constellationAPIHandler(req); 226 + } else { 227 + return new Response( 228 + JSON.stringify({ 229 + error: "User not found", 230 + }), 231 + { 232 + status: 404, 233 + headers: withCors({ "Content-Type": "application/json" }), 234 + } 235 + ); 236 + } 237 + } else { 238 + // indexServerRoutes.has(pathname) 239 + return await genericIndexServer.indexServerHandler(req); 240 + } 241 + } 242 + ); 243 + 244 + devWatch({ 245 + target: "index", 246 + initialData: { 247 + config: config.indexServer, 248 + users: (await genericIndexServer.unspeccedGetRegisteredUsers()) ?? [], 249 + }, 250 + onBuild: ({ js: newjs, html: newhtml, css: newcss }) => { 251 + js = newjs; 252 + html = newhtml; 253 + css = newcss; 254 + }, 255 + });
+251
main-view.ts
··· 1 + import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts"; 2 + import { setupSystemDb } from "./utils/dbsystem.ts"; 3 + import { didDocument } from "./utils/diddoc.ts"; 4 + import { cachedFetch, searchParamsToJson, withCors } from "./utils/server.ts"; 5 + import { ViewServer, ViewServerConfig } from "./viewserver.ts"; 6 + import { extractDid } from "./utils/identity.ts"; 7 + import { config } from "./config.ts"; 8 + import { compile, devWatch } from "./shared-landing/build.ts"; 9 + 10 + // ------------------------------------------ 11 + // AppView Setup 12 + // ------------------------------------------ 13 + 14 + setupAuth({ 15 + serviceDid: config.viewServer.did, 16 + //keyCacheSize: 500, 17 + //keyCacheTTL: 10 * 60 * 1000, 18 + }); 19 + 20 + const viewServerConfig: ViewServerConfig = { 21 + baseDbPath: "./dbs/view/registered-users", // The directory for user databases 22 + systemDbPath: "./dbs/view/registered-users/system.db", // The path for the main system database 23 + }; 24 + export const genericViewServer = new ViewServer(viewServerConfig); 25 + setupSystemDb(genericViewServer.systemDB); 26 + let { js, html, css } = await compile({ 27 + target: "view", 28 + initialData: { 29 + config: config.viewServer, 30 + users: (await genericViewServer.unspeccedGetRegisteredUsers()) ?? [], 31 + }, 32 + }); 33 + 34 + // add me lol 35 + genericViewServer.systemDB.exec(` 36 + INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) 37 + VALUES ( 38 + 'did:plc:mn45tewwnse5btfftvd3powc', 39 + 'admin', 40 + datetime('now'), 41 + 'ready' 42 + ); 43 + 44 + INSERT OR IGNORE INTO users (did, role, registrationdate, onboardingstatus) 45 + VALUES ( 46 + 'did:web:did12.whey.party', 47 + 'admin', 48 + datetime('now'), 49 + 'ready' 50 + ); 51 + `); 52 + 53 + genericViewServer.start(); 54 + 55 + // ------------------------------------------ 56 + // XRPC Method Implementations 57 + // ------------------------------------------ 58 + 59 + const placeholderselfcheckstatus = { 60 + "#bsky_appview:/xrpc/app.bsky.actor.getPreferences": "black", 61 + "#bsky_appview:/xrpc/app.bsky.actor.getProfile": "green", 62 + "#bsky_appview:/xrpc/app.bsky.actor.getProfiles": "green", 63 + "#bsky_appview:/xrpc/app.bsky.actor.getSuggestions": "black", 64 + "#bsky_appview:/xrpc/app.bsky.actor.putPreferences": "black", 65 + "#bsky_appview:/xrpc/app.bsky.actor.searchActorsTypeahead": "black", 66 + "#bsky_appview:/xrpc/app.bsky.actor.searchActors": "black", 67 + "#bsky_appview:/xrpc/app.bsky.feed.describeFeedGenerator": "black", 68 + "#bsky_appview:/xrpc/app.bsky.feed.getActorFeeds": "black", 69 + "#bsky_appview:/xrpc/app.bsky.feed.getActorLikes": "black", 70 + "#bsky_appview:/xrpc/app.bsky.feed.getAuthorFeed": "red", 71 + "#bsky_appview:/xrpc/app.bsky.feed.getFeedGenerator": "black", 72 + "#bsky_appview:/xrpc/app.bsky.feed.getFeedGenerators": "green", // arguably 73 + "#bsky_appview:/xrpc/app.bsky.feed.getFeedSkeleton": "black", 74 + "#bsky_appview:/xrpc/app.bsky.feed.getFeed": "red", 75 + "#bsky_appview:/xrpc/app.bsky.feed.getLikes": "black", 76 + "#bsky_appview:/xrpc/app.bsky.feed.getListFeed": "black", 77 + "#bsky_appview:/xrpc/app.bsky.feed.getPostThread": "red", 78 + "#bsky_appview:/xrpc/app.bsky.unspecced.getPostThreadV2": "red", 79 + "#bsky_appview:/xrpc/app.bsky.feed.getPosts": "green", 80 + "#bsky_appview:/xrpc/app.bsky.feed.getQuotes": "black", 81 + "#bsky_appview:/xrpc/app.bsky.feed.getRepostedBy": "black", 82 + "#bsky_appview:/xrpc/app.bsky.feed.getSuggestedFeeds": "black", 83 + "#bsky_appview:/xrpc/app.bsky.feed.getTimeline": "black", 84 + "#bsky_appview:/xrpc/app.bsky.feed.searchPosts": "black", 85 + "#bsky_appview:/xrpc/app.bsky.feed.sendInteractions": "black", 86 + "#bsky_appview:/xrpc/app.bsky.graph.getActorStarterPacks": "black", 87 + "#bsky_appview:/xrpc/app.bsky.graph.getBlocks": "black", 88 + "#bsky_appview:/xrpc/app.bsky.graph.getFollowers": "black", 89 + "#bsky_appview:/xrpc/app.bsky.graph.getFollows": "black", 90 + "#bsky_appview:/xrpc/app.bsky.graph.getKnownFollowers": "black", 91 + "#bsky_appview:/xrpc/app.bsky.graph.getListBlocks": "black", 92 + "#bsky_appview:/xrpc/app.bsky.graph.getListMutes": "black", 93 + "#bsky_appview:/xrpc/app.bsky.graph.getList": "black", 94 + "#bsky_appview:/xrpc/app.bsky.graph.getLists": "black", 95 + "#bsky_appview:/xrpc/app.bsky.graph.getMutes": "black", 96 + "#bsky_appview:/xrpc/app.bsky.graph.getRelationships": "black", 97 + "#bsky_appview:/xrpc/app.bsky.graph.getStarterPack": "black", 98 + "#bsky_appview:/xrpc/app.bsky.graph.getStarterPacks": "black", 99 + "#bsky_appview:/xrpc/app.bsky.graph.getSuggestedFollowsByActor": "black", 100 + "#bsky_appview:/xrpc/app.bsky.graph.muteActorList": "black", 101 + "#bsky_appview:/xrpc/app.bsky.graph.muteActor": "black", 102 + "#bsky_appview:/xrpc/app.bsky.graph.muteThread": "black", 103 + "#bsky_appview:/xrpc/app.bsky.graph.searchStarterPacks": "black", 104 + "#bsky_appview:/xrpc/app.bsky.graph.unmuteActorList": "black", 105 + "#bsky_appview:/xrpc/app.bsky.graph.unmuteActor": "black", 106 + "#bsky_appview:/xrpc/app.bsky.graph.unmuteThread": "black", 107 + "#bsky_appview:/xrpc/app.bsky.labeler.getServices": "black", 108 + "#bsky_appview:/xrpc/app.bsky.notification.getUnreadCount": "black", 109 + "#bsky_appview:/xrpc/app.bsky.notification.listNotifications": "green", 110 + "#bsky_appview:/xrpc/app.bsky.notification.putPreferences": "black", 111 + "#bsky_appview:/xrpc/app.bsky.notification.registerPush": "black", 112 + "#bsky_appview:/xrpc/app.bsky.notification.updateSeen": "black", 113 + "#bsky_appview:/xrpc/app.bsky.video.getJobStatus": "black", 114 + "#bsky_appview:/xrpc/app.bsky.video.getUploadLimits": "black", 115 + "#bsky_appview:/xrpc/app.bsky.video.uploadVideo": "black", 116 + "#bsky_appview:/xrpc/app.bsky.unspecced.getTrendingTopics": "red", 117 + "#bsky_appview:/xrpc/app.bsky.unspecced.getConfig": "red", 118 + }; 119 + 120 + Deno.serve( 121 + { port: config.viewServer.port }, 122 + async (req: Request): Promise<Response> => { 123 + const url = new URL(req.url); 124 + const pathname = url.pathname; 125 + const searchParams = searchParamsToJson(url.searchParams); 126 + 127 + const publicdir = "/public"; 128 + if (pathname.startsWith(publicdir)) { 129 + const filepath = decodeURIComponent(pathname.slice(publicdir.length)); 130 + try { 131 + const file = await Deno.open("." + filepath, { read: true }); 132 + return new Response(file.readable); 133 + } catch { 134 + return new Response("404 Not Found", { status: 404 }); 135 + } 136 + } 137 + 138 + const todopleasespecthis = "/_unspecced"; 139 + if (pathname.startsWith(todopleasespecthis)) { 140 + const unspeccedroute = decodeURIComponent( 141 + pathname.slice(todopleasespecthis.length) 142 + ); 143 + if (unspeccedroute === "/config") { 144 + const safeconfig = { 145 + inviteOnly: config.viewServer.inviteOnly, 146 + //port: number, 147 + did: config.viewServer.did, 148 + host: config.viewServer.host, 149 + indexPriority: config.viewServer.indexPriority, 150 + }; 151 + return new Response(JSON.stringify(safeconfig), { 152 + headers: withCors({ 153 + "content-type": "application/json; charset=utf-8", 154 + }), 155 + }); 156 + } 157 + if (unspeccedroute === "/users") { 158 + const res = await genericViewServer.unspeccedGetRegisteredUsers(); 159 + return new Response(JSON.stringify(res), { 160 + headers: withCors({ 161 + "content-type": "application/json; charset=utf-8", 162 + }), 163 + }); 164 + } 165 + if (unspeccedroute === "/apitest") { 166 + return new Response(JSON.stringify(placeholderselfcheckstatus), { 167 + headers: withCors({ 168 + "content-type": "application/json; charset=utf-8", 169 + }), 170 + }); 171 + } 172 + } 173 + 174 + if (html && js) { 175 + if (pathname === "/" || pathname === "") { 176 + return new Response(html, { 177 + headers: withCors({ "content-type": "text/html; charset=utf-8" }), 178 + }); 179 + } 180 + if (pathname === "/landing-view.js") { 181 + return new Response(js, { 182 + headers: withCors({ 183 + "content-type": "application/javascript; charset=utf-8", 184 + }), 185 + }); 186 + } 187 + } else { 188 + if (pathname === "/" || pathname === "") { 189 + return new Response(`server is compiling your webpage. loading...`, { 190 + headers: withCors({ "content-type": "text/html; charset=utf-8" }), 191 + }); 192 + } 193 + } 194 + if (pathname === "/app.css") { 195 + return new Response(css, { 196 + headers: withCors({ 197 + "content-type": "text/css; charset=utf-8", 198 + }), 199 + }); 200 + } 201 + 202 + if (pathname === "/.well-known/did.json") { 203 + return new Response( 204 + JSON.stringify( 205 + didDocument( 206 + "view", 207 + config.viewServer.did, 208 + config.viewServer.host, 209 + "whatever" 210 + ) 211 + ), 212 + { 213 + headers: withCors({ "Content-Type": "application/json" }), 214 + } 215 + ); 216 + } 217 + if (pathname === "/health") { 218 + return new Response("OK", { 219 + status: 200, 220 + headers: withCors({ 221 + "Content-Type": "text/plain", 222 + }), 223 + }); 224 + } 225 + if (req.method === "OPTIONS") { 226 + return new Response(null, { 227 + status: 204, 228 + headers: { 229 + "Access-Control-Allow-Origin": "*", 230 + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 231 + "Access-Control-Allow-Headers": "*", 232 + }, 233 + }); 234 + } 235 + console.log(`request for "${pathname}"`); 236 + return await genericViewServer.viewServerHandler(req); 237 + } 238 + ); 239 + 240 + devWatch({ 241 + target: "view", 242 + initialData: { 243 + config: config.viewServer, 244 + users: await genericViewServer.unspeccedGetRegisteredUsers() ?? [], 245 + }, 246 + onBuild: ({ js: newjs, html: newhtml, css: newcss }) => { 247 + js = newjs; 248 + html = newhtml; 249 + css = newcss; 250 + }, 251 + });
-145
main.ts
··· 1 - import { setupAuth, getAuthenticatedDid, authVerifier } from "./utils/auth.ts"; 2 - import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts"; 3 - import { resolveRecordFromURI, validateRecord } from "./utils/records.ts"; 4 - import { setupSystemDb } from "./utils/dbsystem.ts"; 5 - import { setupUserDb } from "./utils/dbuser.ts"; 6 - import { handleSpacedust, startSpacedust } from "./index/spacedust.ts"; 7 - import { handleJetstream, startJetstream } from "./index/jetstream.ts"; 8 - import { Database } from "jsr:@db/sqlite@0.11"; 9 - //import express from "npm:express"; 10 - //import { createServer } from "./xrpc/index.ts"; 11 - import { indexHandlerContext } from "./index/types.ts"; 12 - import * as IndexServerTypes from "./utils/indexservertypes.ts"; 13 - import * as ViewServerTypes from "./utils/viewservertypes.ts"; 14 - import * as ATPAPI from "npm:@atproto/api"; 15 - import { didDocument } from "./utils/diddoc.ts"; 16 - import { cachedFetch, searchParamsToJson } from "./utils/server.ts"; 17 - import { indexServerHandler } from "./indexserver.ts"; 18 - import { viewServerHandler } from "./viewserver.ts"; 19 - 20 - export const slingshoturl = Deno.env.get("SLINGSHOT_URL"); 21 - export const constellationurl = Deno.env.get("CONSTELLATION_URL"); 22 - export const spacedusturl = Deno.env.get("SPACEDUST_URL"); 23 - 24 - // ------------------------------------------ 25 - // AppView Setup 26 - // ------------------------------------------ 27 - 28 - export const systemDB = new Database("system.db"); 29 - setupSystemDb(systemDB); 30 - 31 - export const spacedustManager = new SpacedustManager((msg) => 32 - handleSpacedust(msg) 33 - ); 34 - export const jetstreamManager = new JetstreamManager((msg) => 35 - handleJetstream(msg) 36 - ); 37 - startSpacedust(); 38 - startJetstream(); 39 - 40 - setupAuth({ 41 - // local3768forumtest is just my tunnel from my dev env to the outside web that im reusing from forumtest 42 - serviceDid: `${Deno.env.get("SERVICE_DID")}`, 43 - //keyCacheSize: 500, 44 - //keyCacheTTL: 10 * 60 * 1000, 45 - }); 46 - 47 - // ------------------------------------------ 48 - // XRPC Method Implementations 49 - // ------------------------------------------ 50 - 51 - const indexServerRoutes = new Set([ 52 - "/xrpc/app.bsky.actor.getProfile", 53 - "/xrpc/app.bsky.actor.getProfiles", 54 - "/xrpc/app.bsky.feed.getActorFeeds", 55 - "/xrpc/app.bsky.feed.getFeedGenerator", 56 - "/xrpc/app.bsky.feed.getFeedGenerators", 57 - "/xrpc/app.bsky.feed.getPosts", 58 - "/xrpc/party.whey.app.bsky.feed.getActorLikesPartial", 59 - "/xrpc/party.whey.app.bsky.feed.getAuthorFeedPartial", 60 - "/xrpc/party.whey.app.bsky.feed.getLikesPartial", 61 - "/xrpc/party.whey.app.bsky.feed.getPostThreadPartial", 62 - "/xrpc/party.whey.app.bsky.feed.getQuotesPartial", 63 - "/xrpc/party.whey.app.bsky.feed.getRepostedByPartial", 64 - ]); 65 - 66 - Deno.serve( 67 - { port: Number(`${Deno.env.get("SERVER_PORT")}`) }, 68 - async (req: Request): Promise<Response> => { 69 - const url = new URL(req.url); 70 - const pathname = url.pathname; 71 - // const searchParams = searchParamsToJson(url.searchParams); 72 - // let reqBody: undefined | string; 73 - // let jsonbody: undefined | Record<string, unknown>; 74 - // try { 75 - // const clone = req.clone(); 76 - // jsonbody = await clone.json(); 77 - // } catch (e) { 78 - // console.warn("Request body is not valid JSON:", e); 79 - // } 80 - if (pathname === "/.well-known/did.json") { 81 - return new Response(JSON.stringify(didDocument), { 82 - headers: withCors({ "Content-Type": "application/json" }), 83 - }); 84 - } 85 - if (pathname === "/health") { 86 - return new Response("OK", { 87 - status: 200, 88 - headers: withCors({ 89 - "Content-Type": "text/plain", 90 - }), 91 - }); 92 - } 93 - if (req.method === "OPTIONS") { 94 - return new Response(null, { 95 - status: 204, 96 - headers: { 97 - "Access-Control-Allow-Origin": "*", 98 - "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 99 - "Access-Control-Allow-Headers": "*", 100 - }, 101 - }); 102 - } 103 - // const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 104 - // const hasAuth = req.headers.has("authorization"); 105 - // const xrpcMethod = pathname.startsWith("/xrpc/") 106 - // ? pathname.slice("/xrpc/".length) 107 - // : null; 108 - 109 - if (indexServerRoutes.has(pathname)) { 110 - return await indexServerHandler(req); 111 - } else { 112 - return await viewServerHandler(req); 113 - } 114 - } 115 - ); 116 - 117 - export function withCors(headers: HeadersInit = {}) { 118 - return { 119 - "Access-Control-Allow-Origin": "*", 120 - ...headers, 121 - }; 122 - } 123 - const corsfree = { 124 - "Access-Control-Allow-Origin": "*", 125 - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 126 - "Access-Control-Allow-Headers": "Content-Type, Authorization", 127 - }; 128 - const json = "application/json"; 129 - 130 - // ------------------------------------------ 131 - // Indexer 132 - // ------------------------------------------ 133 - 134 - export function handleIndex(ctx: indexHandlerContext) { 135 - const record = validateRecord(ctx.value); 136 - switch (record?.$type) { 137 - case "app.bsky.feed.like": { 138 - return; 139 - } 140 - default: { 141 - // what the hell 142 - return; 143 - } 144 - } 145 - }
+22
public/client-metadata.json
··· 1 + { 2 + "client_id": "https://local3768forumtest.whey.party/client-metadata.json", 3 + "client_name": "ForumTest", 4 + "client_uri": "https://local3768forumtest.whey.party", 5 + "logo_uri": "https://local3768forumtest.whey.party/logo192.png", 6 + "tos_uri": "https://local3768forumtest.whey.party/terms-of-service", 7 + "policy_uri": "https://local3768forumtest.whey.party/privacy-policy", 8 + "redirect_uris": [ 9 + "https://local3768forumtest.whey.party/callback" 10 + ], 11 + "scope": "atproto transition:generic", 12 + "grant_types": [ 13 + "authorization_code", 14 + "refresh_token" 15 + ], 16 + "response_types": [ 17 + "code" 18 + ], 19 + "token_endpoint_auth_method": "none", 20 + "application_type": "web", 21 + "dpop_bound_access_tokens": true 22 + }
public/index.ico

This is a binary file and will not be displayed.

public/view.ico

This is a binary file and will not be displayed.

+50 -35
readme.md
··· 1 - # (wip)(not done)(pre alpha) skylite (temporary name probably)(test thing) 2 - an attempt to make a lightweight, easily self-hostable, scoped appview (kinda like fedi instances, so that means users need to register to an instance to have the content they should see be indexed) using: 3 - - live sync systems: 4 - - [jetstream](https://github.com/bluesky-social/jetstream) 5 - - [spacedust](https://spacedust.microcosm.blue/) 6 - - backfill: 7 - - [listRecords](https://docs.bsky.app/docs/api/com-atproto-repo-list-records) 8 - - [constellation](https://constellation.microcosm.blue/) 9 - - the server stuff: 10 - - [sqlite](https://jsr.io/@db/sqlite) 11 - - the usage of [typescript](https://www.npmjs.com/package/@atproto/lex-cli) 12 - - [deno](https://deno.com/) 1 + # skylite (pre alpha) 2 + an attempt to make a lightweight, easily self-hostable, scoped Bluesky appview 3 + 4 + (as of 28 aug 2025) 5 + currently the state of the project is: 6 + ![screenshot of the index server](./docs/assets/indexapistatus.png) 7 + ![screenshot of the view server](./docs/assets/viewapistatus.png) 8 + 9 + this project uses: 10 + - live sync systems: [jetstream](https://github.com/bluesky-social/jetstream) and [spacedust](https://spacedust.microcosm.blue/) 11 + - backfill: [listRecords](https://docs.bsky.app/docs/api/com-atproto-repo-list-records) and [constellation](https://constellation.microcosm.blue/) 12 + - the backend server stuff: [sqlite](https://jsr.io/@db/sqlite) db, typescript with [codegen](https://www.npmjs.com/package/@atproto/lex-cli), and [deno](https://deno.com/) 13 + - frontend: still deno and esbuild and tailwind and react and jsx and typescript (was fun getting these to run on deno) 14 + 15 + ## Running 16 + this project is pre-alpha and not intended for general use yet. you are welcome to experiment if you dont mind errors or breaking changes. 17 + 18 + the project is split into two, the "Index Server" and the "View Server". 19 + despite both living in this repo, they run different http servers with different configs 20 + 21 + example configuration is in the `config.jsonc.example` file 22 + 23 + ### Index Server 24 + start it by running 25 + ```sh 26 + deno task index 27 + ``` 28 + it should just work actually 29 + 30 + there is no way to register users to be indexed by the server yet (either Index nor View servers) so you can just manually add your account to the `system.db` file for now 13 31 14 - this uses XRPC server tooling and codegen so the repo is kinda large despite not being functional at all 32 + ### View Server 33 + start it by running 34 + ```sh 35 + deno task view 36 + ``` 37 + expose your localhost to the web using a tunnel or something and use that url as the custom appview url 15 38 16 - currently the state of the project is: 17 - - db: 18 - - is not ready but ive started working on it 19 - - db schema is kinda there but still iffy on the insertion (kysely with better-sqlite3 or just use raw sql queries with deno jsr:@db/sqlite ?) (or maybe should it not use sqlite at all?) 20 - - indexing: 21 - - the meta framework of the parsing incoming data is kinda done (good enough for MVP but i would like more indexing provenance) 22 - - but the actual logic for index handling to the db is not done 23 - - registration: 24 - - user registration is not there yet, the wrapper around listRecords exists though (for onboarding/backfill) 25 - - XRPC Server: 26 - - setup is done, just need to actually implement all 80+ routes 27 - - got auth working recently but havent hooked up the validator to the xrpc method/route handler so it can access who requested it 39 + this should work on any bluesky client that supports changing the appview URL (im using an unreleased custom fork for development) as the view server implements the `#bsky_appview` routes for compatibility with existing clients 28 40 41 + ive got a custom `social-app` fork here [https://github.com/rimar1337/social-app/tree/publicappview-colorable](https://github.com/rimar1337/social-app/tree/publicappview-colorable) 29 42 30 - there is still a lot of design work i havent done regarding stuff like: 31 - - moderation 32 - - indexing provenance 33 - - registration/unregistration APIs 34 - - cross instance backfills/viewing 35 - - also im pretty sure im not listening to all of the app.bsky.* records that i should be listening to but i havent re checked it even though i already imported the entire lexicon directory from the bsky atproto git repo but i just havent checked yet 36 - - and more 43 + the view server has extra configurations that you need to understand. 44 + the view server hydrates content by calling other servers (either an `#skylite_index` or `#bsky_appview`) and so you need to write the order of which servers are prioritized first for resolving the hydration endpoints 45 + ```js 46 + // In order of which skylite index servers or bsky appviews to use first 47 + "indexPriority": [ 48 + "user#skylite_index", // user resolved skylite index server 49 + "did:web:backupindexserver.your.site#skylite_index", // a specific skylite index server 50 + "user#bsky_appview", // user resolved bsky appview 51 + "did:web:api.bsky.app#bsky_appview" // a specific bsky appview 52 + ] 53 + ``` 37 54 38 - still very early 39 - this does not run on any bsky clients yet 40 - practically none of the api routes have been implemented yet 55 + id say this project is like uhh ~20% done so not a lot of things you can do with this right now
+4
shared-landing/app.css
··· 1 + /*@import "tailwindcss";*/ 2 + @tailwind base; 3 + @tailwind components; 4 + @tailwind utilities;
+9
shared-landing/browser/ClientOnly.tsx
··· 1 + 'use client'; 2 + import React from "https://esm.sh/react@19.1.1"; 3 + import { useEffect, useState } from 'https://esm.sh/react@19.1.1'; 4 + 5 + export function ClientOnly({ children }: { children: React.ReactNode }) { 6 + const [mounted, setMounted] = useState(false); 7 + useEffect(() => setMounted(true), []); 8 + return mounted ? <>{children}</> : null; 9 + }
+13
shared-landing/browser/deno.json
··· 1 + { 2 + "compilerOptions": { 3 + "lib": ["dom", "esnext"], 4 + "strict": true 5 + }, 6 + "include": ["*.ts", "*.tsx"], 7 + "lint": { 8 + "rules": { 9 + "tags": ["recommended"], 10 + "exclude": ["no-window"] 11 + } 12 + } 13 + }
+10
shared-landing/browser/icons.tsx
··· 1 + import React from 'https://esm.sh/react@19.1.1'; 2 + import type { SVGProps } from 'https://esm.sh/react@19.1.1'; 3 + // Font Awesome Regularby Dave Gandy 4 + // CC BY 4.0 Attribution Required 5 + export function Fa7RegularMap(props: SVGProps<SVGSVGElement>) { 6 + return (<svg xmlns="http://www.w3.org/2000/svg" width={360} height={360} viewBox="0 0 512 512" {...props}><path fill="currentColor" d="M512 48c0-8.3-4.3-16-11.3-20.4s-15.9-4.8-23.3-1.1L352.5 88.1L180 29.4c-13.7-4.7-28.7-3.8-41.9 2.3L13.8 90.3C5.4 94.2 0 102.7 0 112v352c0 8.2 4.2 15.9 11.1 20.3s15.6 4.9 23.1 1.4l127.3-59.9l170.7 56.9c13.7 4.6 28.5 3.7 41.6-2.5l124.4-58.5c8.4-4 13.8-12.4 13.8-21.7zM144 82.1v299l-96 45.2v-299zm48 303.3V84.3l128 43.5v300.3zM368 134l96-47.4v298.2L368 430z"></path></svg>); 7 + } 8 + export function Fa7RegularContactBook(props: SVGProps<SVGSVGElement>) { 9 + return (<svg xmlns="http://www.w3.org/2000/svg" width={360} height={360} viewBox="0 0 512 512" {...props}><path fill="currentColor" d="M384 48c8.8 0 16 7.2 16 16v384c0 8.8-7.2 16-16 16H96c-8.8 0-16-7.2-16-16V64c0-8.8 7.2-16 16-16zM96 0C60.7 0 32 28.7 32 64v384c0 35.3 28.7 64 64 64h288c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64zm144 248a56 56 0 1 0 0-112a56 56 0 1 0 0 112m-32 40c-44.2 0-80 35.8-80 80c0 8.8 7.2 16 16 16h192c8.8 0 16-7.2 16-16c0-44.2-35.8-80-80-80zM512 80c0-8.8-7.2-16-16-16s-16 7.2-16 16v64c0 8.8 7.2 16 16 16s16-7.2 16-16zm-16 112c-8.8 0-16 7.2-16 16v64c0 8.8 7.2 16 16 16s16-7.2 16-16v-64c0-8.8-7.2-16-16-16m16 144c0-8.8-7.2-16-16-16s-16 7.2-16 16v64c0 8.8 7.2 16 16 16s16-7.2 16-16z"></path></svg>); 10 + }
+14
shared-landing/browser/landing-index.tsx
··· 1 + import React, { useState } from "https://esm.sh/react@19.1.1"; 2 + import { createRoot, hydrateRoot } from "https://esm.sh/react-dom@19.1.1/client"; 3 + import * as ATPAPI from "https://esm.sh/@atproto/api"; 4 + import { AuthProvider } from "../browser/passauthprovider.tsx"; 5 + import { Root } from "../browser/landing-shared.tsx"; 6 + 7 + const initialDataEl = document.getElementById("initial-data"); 8 + const initialData = initialDataEl ? JSON.parse(initialDataEl.textContent!) : undefined; 9 + // rule of thumb this is the only place where we can do browser-only code 10 + 11 + createRoot(document.getElementById("root")!).render( 12 + <Root type="index" initialData={initialData}/> 13 + ); 14 + //hydrateRoot(document.getElementById("root")!, <App />);
+752
shared-landing/browser/landing-shared.tsx
··· 1 + import React, { 2 + useState, 3 + useEffect, 4 + PropsWithChildren, 5 + } from "https://esm.sh/react@19.1.1"; 6 + import { 7 + createRoot, 8 + hydrateRoot, 9 + } from "https://esm.sh/react-dom@19.1.1/client"; 10 + import * as ATPAPI from "https://esm.sh/@atproto/api"; 11 + import { AuthProvider, useAuth } from "./passauthprovider.tsx"; 12 + import Login from "./passlogin.tsx"; 13 + import { Fa7RegularContactBook, Fa7RegularMap } from "./icons.tsx"; 14 + import { 15 + boolean, 16 + number, 17 + string, 18 + } from "https://esm.sh/zod@3.25.76/index.d.cts"; 19 + console.log("script loaded"); 20 + 21 + // TODO it should read from the config.jsonc instead 22 + const instanceConfig = { 23 + name: "demo instance", 24 + description: "test instance for demo-ing skylite", 25 + repoUrl: "https://tangled.sh/@whey.party/skylite", 26 + inviteRequired: true, 27 + socialAppUrl: "https://bsky.app", 28 + }; 29 + 30 + const Card = ({ 31 + children, 32 + className = "", 33 + }: PropsWithChildren<{ className?: string }>) => ( 34 + <div className={`bg-white p-6 rounded-lg shadow-md ${className}`}> 35 + {children} 36 + </div> 37 + ); 38 + 39 + function Header({ 40 + isLoggedIn, 41 + agent, 42 + isIndex, 43 + capitaltitle, 44 + instancehost, 45 + }: { 46 + isLoggedIn: boolean; 47 + agent?: ATPAPI.AtpAgent; 48 + isIndex: boolean; 49 + capitaltitle: string; 50 + instancehost?: string; 51 + }) { 52 + const userHandle = agent?.session?.handle; 53 + return ( 54 + <header className="bg-white shadow-sm sticky top-0 z-10"> 55 + <nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 56 + <div className="flex items-center justify-between h-16"> 57 + <div className="flex items-center"> 58 + <div className="flex-shrink-0 flex items-center gap-3 text-2xl font-bold text-blue-600"> 59 + {isIndex ? ( 60 + <Fa7RegularContactBook className="h-[1em] w-[1em]" /> 61 + ) : ( 62 + <Fa7RegularMap className="h-[1em] w-[1em]" /> 63 + )} 64 + <span> 65 + Skylite {capitaltitle} Server{" "} 66 + {instancehost && ( 67 + <span className="text-sm">({/*instancehost*/ "alpha"})</span> 68 + )} 69 + </span> 70 + </div> 71 + </div> 72 + <div className="flex items-center gap-4"> 73 + {isLoggedIn && userHandle && ( 74 + <span className="text-gray-700 hidden sm:block"> 75 + Welcome, <span className="font-semibold">@{userHandle}</span> 76 + </span> 77 + )} 78 + <Login compact /> 79 + </div> 80 + </div> 81 + </nav> 82 + </header> 83 + ); 84 + } 85 + 86 + function DataManager({ agent }: { agent?: ATPAPI.AtpAgent }) { 87 + const userDid = agent?.session?.did; 88 + return ( 89 + <Card> 90 + <h2 className="text-xl font-bold text-gray-800 mb-4">My Data Manager</h2> 91 + <p className="text-gray-600 mb-2"> 92 + Manage your data and account settings on this server. 93 + </p> 94 + <p className="text-sm text-gray-600 mb-4">Your DID:</p> 95 + <pre className="p-2 bg-gray-100 text-gray-800 rounded-md overflow-x-auto text-xs"> 96 + {userDid} 97 + </pre> 98 + <div className="mt-6 border-t pt-4"> 99 + <h3 className="text-lg font-semibold text-gray-700">Account Actions</h3> 100 + <p className="text-gray-500 text-sm mt-2"> 101 + (Feature in development) Actions like exporting or deleting your data 102 + will be available here. 103 + </p> 104 + <button 105 + disabled 106 + className="mt-4 px-4 py-2 rounded-md bg-red-600 text-white font-semibold shadow-sm disabled:bg-red-300 disabled:cursor-not-allowed" 107 + > 108 + Delete My Account from this Server 109 + </button> 110 + </div> 111 + </Card> 112 + ); 113 + } 114 + 115 + function ApiTester() { 116 + const [endpoint, setEndpoint] = useState("/xrpc/app.bsky.feed.getPosts"); 117 + const [method, setMethod] = useState("GET"); 118 + const [body, setBody] = useState(""); 119 + const [response, setResponse] = useState<string | null>(null); 120 + const [error, setError] = useState(""); 121 + const [isLoading, setIsLoading] = useState(false); 122 + 123 + const handleSubmit = async (e: any) => { 124 + e.preventDefault(); 125 + setIsLoading(true); 126 + setError(""); 127 + setResponse(null); 128 + try { 129 + const options: any = { 130 + method, 131 + headers: { "Content-Type": "application/json" }, 132 + }; 133 + if (method !== "GET" && method !== "HEAD" && body) { 134 + options.body = body; 135 + } 136 + const res = await fetch(endpoint, options); 137 + try { 138 + const data = await res.clone().json(); 139 + setResponse(JSON.stringify(data, null, 2)); 140 + } catch (_e) { 141 + setResponse(await res.text()); 142 + } 143 + if (!res.ok) { 144 + setError(`HTTP error! status: ${res.status}`); 145 + } 146 + } catch (err: any) { 147 + setError(err.message); 148 + setResponse(null); 149 + } finally { 150 + setIsLoading(false); 151 + } 152 + }; 153 + 154 + return ( 155 + <Card> 156 + <h2 className="text-xl font-bold text-gray-800 mb-4">API Test</h2> 157 + <form onSubmit={handleSubmit} className="space-y-4"> 158 + <div className="flex flex-col sm:flex-row gap-2"> 159 + <select 160 + value={method} 161 + onChange={(e) => setMethod(e.target.value)} 162 + className="px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" 163 + > 164 + <option>GET</option> 165 + <option>POST</option> 166 + </select> 167 + <input 168 + type="text" 169 + value={endpoint} 170 + onChange={(e) => setEndpoint(e.target.value)} 171 + placeholder="/xrpc/endpoint.name" 172 + className="flex-grow block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" 173 + required 174 + /> 175 + </div> 176 + {method === "POST" && ( 177 + <div> 178 + <label 179 + htmlFor="body" 180 + className="block text-sm font-medium text-gray-600" 181 + > 182 + Request Body (JSON) 183 + </label> 184 + <textarea 185 + id="body" 186 + rows={3} 187 + value={body} 188 + onChange={(e) => setBody(e.target.value)} 189 + placeholder='{ "key": "value" }' 190 + className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 font-mono text-sm" 191 + /> 192 + </div> 193 + )} 194 + <button 195 + type="submit" 196 + disabled={isLoading} 197 + className="w-full px-4 py-2 rounded-md bg-blue-600 text-white font-semibold shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-400" 198 + > 199 + {isLoading ? "Sending..." : "Send Request"} 200 + </button> 201 + </form> 202 + <div className="mt-6"> 203 + <h3 className="text-lg font-semibold text-gray-700">Response</h3> 204 + {error && ( 205 + <pre className="mt-2 p-3 bg-red-50 text-red-700 text-xs rounded-md overflow-x-auto"> 206 + Error: {error} 207 + </pre> 208 + )} 209 + {response && ( 210 + <pre className="mt-2 p-3 bg-gray-800 text-white text-xs rounded-md overflow-x-auto"> 211 + {response} 212 + </pre> 213 + )} 214 + {!isLoading && !error && !response && ( 215 + <p className="text-gray-500 mt-2 text-sm"> 216 + Response will appear here. 217 + </p> 218 + )} 219 + </div> 220 + </Card> 221 + ); 222 + } 223 + 224 + function SocialAppButton({ did }: { did?: string }) { 225 + return ( 226 + <Card className="text-center gap-2 flex flex-col"> 227 + <h2 className="text-xl font-semibold text-gray-700"> 228 + Explore the Network 229 + </h2> 230 + <p className="text-sm text-gray-600 space-y-3"> 231 + Use the hosted client to browse this View Server. 232 + </p> 233 + <a 234 + href={instanceConfig.socialAppUrl} 235 + target="_blank" 236 + rel="noopener noreferrer" 237 + className="w-full px-4 py-2 rounded-md bg-blue-600 text-white font-semibold shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-400" 238 + > 239 + Open Social App 240 + </a> 241 + <p className="text-sm text-gray-600 space-y-3"> 242 + Or use any other #bsky_appview compatible client. 243 + </p> 244 + <pre className="text-sm bg-gray-100 py-2 px-3 rounded-sm overflow-y-auto"> 245 + {did}#bsky_appview 246 + </pre> 247 + <p className="text-sm text-gray-600 space-y-3 italic"> 248 + (works best for registered users) 249 + </p> 250 + </Card> 251 + ); 252 + } 253 + 254 + function InstanceInfo({ 255 + config, 256 + configloading, 257 + }: { 258 + config: 259 + | { 260 + inviteOnly: boolean; 261 + //port: number; 262 + did: string; 263 + host: string; 264 + indexPriority?: string[]; 265 + } 266 + | undefined; 267 + configloading: boolean; 268 + }) { 269 + return ( 270 + <> 271 + <h2 className="text-xl font-semibold text-gray-700 mb-2"> 272 + About <span className="">{instanceConfig.name}</span> ({config?.host}) 273 + </h2> 274 + <p className="text-gray-600">{instanceConfig.description}</p> 275 + {config && ( 276 + <div className="bg-gray-100 rounded-xl p-4"> 277 + <p> 278 + <strong>DID:</strong> {config.did} 279 + </p> 280 + {/* <p> 281 + <strong>Port:</strong> {config.port} 282 + </p> */} 283 + <p> 284 + <strong>Host:</strong> {config.host} 285 + </p> 286 + <p> 287 + <strong>Invite only:</strong> {config.inviteOnly ? "Yes" : "No"} 288 + </p> 289 + 290 + {config.indexPriority && ( 291 + <div> 292 + <strong>Index Priority:</strong> 293 + <ol className="list-decimal list-inside ml-4"> 294 + {config.indexPriority.map((priority, idx) => ( 295 + <li key={idx}>{priority}</li> 296 + ))} 297 + </ol> 298 + </div> 299 + )} 300 + </div> 301 + )} 302 + </> 303 + ); 304 + } 305 + function APIStatus() { 306 + const [results, setResults] = useState<Record<string, string> | undefined>(); 307 + const [loading, setLoading] = useState(true); 308 + 309 + useEffect(() => { 310 + async function fetchResults() { 311 + try { 312 + const response = await fetch("/_unspecced/apitest"); 313 + if (!response.ok) throw new Error("Failed to fetch API test results"); 314 + const data = await response.json(); 315 + setResults(data); 316 + } catch (error) { 317 + console.error("Error fetching API test:", error); 318 + } finally { 319 + setLoading(false); 320 + } 321 + } 322 + fetchResults(); 323 + }, []); 324 + 325 + if (loading) return <p>Loading API test...</p>; 326 + if (!results) return <p>Failed to load API test results.</p>; 327 + 328 + const colorMap: Record<string, string> = { 329 + green: "bg-green-500", 330 + orange: "bg-orange-400", 331 + red: "bg-red-500", 332 + black: "bg-gray-700", 333 + }; 334 + 335 + const statuses = Object.values(results); 336 + const sortedStatuses = [...statuses].sort( 337 + (a, b) => 338 + ["green", "orange", "red", "black"].indexOf(a) - 339 + ["green", "orange", "red", "black"].indexOf(b) 340 + ); 341 + 342 + const categories: Record<string, Record<string, string>> = {}; 343 + for (const [rawKey, status] of Object.entries(results)) { 344 + const [category, route] = rawKey.includes(":") 345 + ? rawKey.split(/:(.+)/) 346 + : ["uncategorized", rawKey]; 347 + 348 + if (!categories[category]) categories[category] = {}; 349 + categories[category][route] = status; 350 + } 351 + 352 + return ( 353 + <> 354 + <h2 className="text-xl font-semibold text-gray-700 mb-2">API Status</h2> 355 + <div className="flex flex-row gap-4"> 356 + <div className="bg-gray-100 rounded-xl p-4 mb-4"> 357 + <h3 className="font-semibold text-gray-700 mb-2">Legend</h3> 358 + <ul className="space-y-1"> 359 + <li className="flex items-center space-x-2"> 360 + <span 361 + className={`w-3 h-3 rounded-full ${colorMap["green"]}`} 362 + ></span> 363 + <span>= done</span> 364 + </li> 365 + <li className="flex items-center space-x-2"> 366 + <span 367 + className={`w-3 h-3 rounded-full ${colorMap["orange"]}`} 368 + ></span> 369 + <span>= half-done</span> 370 + </li> 371 + <li className="flex items-center space-x-2"> 372 + <span 373 + className={`w-3 h-3 rounded-full ${colorMap["red"]}`} 374 + ></span> 375 + <span>= actively wrong</span> 376 + </li> 377 + <li className="flex items-center space-x-2"> 378 + <span 379 + className={`w-3 h-3 rounded-full ${colorMap["black"]}`} 380 + ></span> 381 + <span>= not there</span> 382 + </li> 383 + </ul> 384 + </div> 385 + <div className="bg-gray-100 rounded-xl p-4 space-y-4 mb-4 flex-1"> 386 + <h3 className="font-semibold text-gray-700 mb-2">Overview</h3> 387 + <div className="flex flex-wrap gap-2"> 388 + {sortedStatuses.map((status, idx) => ( 389 + <div 390 + key={idx} 391 + className={`w-4 h-4 rounded-full ${colorMap[status]}`} 392 + title={status} 393 + ></div> 394 + ))} 395 + </div> 396 + </div> 397 + </div> 398 + 399 + <div className="bg-gray-100 rounded-xl p-4 space-y-4"> 400 + <div className="space-y-4"> 401 + {Object.entries(categories).map(([category, routes]) => ( 402 + <div key={category}> 403 + <h3 className="font-semibold text-lg text-gray-700"> 404 + {category} 405 + </h3> 406 + <ul className="ml-4 space-y-1"> 407 + {Object.entries(routes).map(([route, status]) => ( 408 + <li key={route} className="flex items-center space-x-2"> 409 + <span 410 + className={`w-3 h-3 rounded-full ${ 411 + colorMap[status] ?? "bg-gray-400" 412 + }`} 413 + ></span> 414 + <span className="font-mono">{route}</span> 415 + </li> 416 + ))} 417 + </ul> 418 + </div> 419 + ))} 420 + </div> 421 + </div> 422 + </> 423 + ); 424 + } 425 + 426 + function AboutSkylite({ type }: { type: "index" | "view" }) { 427 + const isIndex = type === "index"; 428 + return ( 429 + <div className="gap-2 flex flex-col"> 430 + <h2 className="text-xl font-semibold text-gray-700"> 431 + What is a Skylite {isIndex ? "Index Server" : "View Server"}? 432 + </h2> 433 + <div className="text-sm text-gray-600 space-y-3"> 434 + <p> 435 + {isIndex 436 + ? `An Index Server is where your social data lives on the Bluesky network. It stores all your network activity, including posts, replies, likes, and followers. The scoped nature of Index Servers makes them easier to host and manage, while also strengthening the overall resilience of the Bluesky network.` 437 + : `A View Server is a service that presents data from multiple collections (indexes) across the network. It enhances public data with automatic resolving, caches, moderation, following feeds, and notifications, providing a unified "view" for client applications.`} 438 + </p> 439 + <p> 440 + {isIndex 441 + ? `Want to use this data? Explore Skylite View Servers to get started.` 442 + : `Skylite View Servers collect decentralized indexed data from multiple Index Servers (and AppViews too!), giving clients a broader and more resilient view of the network.`} 443 + </p> 444 + </div> 445 + </div> 446 + ); 447 + } 448 + 449 + function UserList({ 450 + users, 451 + isLoading, 452 + }: { 453 + users: { 454 + did: string; 455 + role: string; 456 + registrationdate: string; 457 + onboardingstatus: string; 458 + pfp?: string; 459 + displayname: string; 460 + handle: string; 461 + }[]; 462 + isLoading: boolean; 463 + }) { 464 + return ( 465 + <> 466 + <h2 className="text-xl font-semibold text-gray-700 mb-4"> 467 + Registered Users ({users.length}) 468 + </h2> 469 + <div className="space-y-4 max-h-[300px] overflow-y-auto border p-2 rounded-md"> 470 + {isLoading ? ( 471 + <p className="text-gray-500 p-2">Loading users...</p> 472 + ) : users.length === 0 ? ( 473 + <p className="text-gray-500 p-2">No users have registered yet.</p> 474 + ) : ( 475 + users.map((user) => ( 476 + <div key={user.did} className="flex items-center space-x-4"> 477 + <img 478 + src={user.pfp} 479 + alt={user.did} 480 + className="w-12 h-12 rounded-full bg-gray-200" 481 + /> 482 + <div> 483 + <p className="font-bold text-gray-800">{user.displayname}</p> 484 + <p className="text-sm text-gray-500">@{user.handle}</p> 485 + <p className="text-sm text-gray-500"> 486 + {user.role} - {user.onboardingstatus} 487 + </p> 488 + {/* <p className="text-sm text-gray-500">{user.onboardingstatus}</p> */} 489 + </div> 490 + </div> 491 + )) 492 + )} 493 + </div> 494 + </> 495 + ); 496 + } 497 + 498 + function RegistrationForm({ 499 + isLoggedIn, 500 + agent, 501 + inviteRequired, 502 + }: { 503 + isLoggedIn: boolean; 504 + agent: ATPAPI.BskyAgent | null; 505 + inviteRequired: boolean; 506 + }) { 507 + const [inviteCode, setInviteCode] = useState(""); 508 + const [error, setError] = useState(""); 509 + const [success, setSuccess] = useState(""); 510 + const [isSubmitting, setIsSubmitting] = useState(false); 511 + 512 + const handleSubmit = async (e: React.FormEvent) => { 513 + e.preventDefault(); 514 + if (!agent || !isLoggedIn) { 515 + setError("You must be logged in to register."); 516 + return; 517 + } 518 + setError(""); 519 + setSuccess(""); 520 + setIsSubmitting(true); 521 + // TODO implement registration logic 522 + setTimeout(() => { 523 + setError("Registration endpoint not implemented."); 524 + setIsSubmitting(false); 525 + }, 1000); 526 + }; 527 + 528 + return ( 529 + <> 530 + <h2 className="text-xl font-semibold text-gray-700 mb-4">Register</h2> 531 + <p className="text-sm text-gray-500 mb-4"> 532 + You must be logged in {inviteRequired && "with an invite code"} to 533 + register on this server. 534 + </p> 535 + <form onSubmit={handleSubmit}> 536 + <fieldset 537 + disabled={!isLoggedIn || isSubmitting} 538 + className="space-y-4 disabled:opacity-50" 539 + > 540 + {inviteRequired && ( 541 + <div> 542 + <label 543 + htmlFor="invite" 544 + className="block text-sm font-medium text-gray-600" 545 + > 546 + Invite Code 547 + </label> 548 + <input 549 + type="text" 550 + id="invite" 551 + value={inviteCode} 552 + onChange={(e) => setInviteCode(e.target.value)} 553 + placeholder="xxxx-xxxx-xxxx-xxxx" 554 + className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" 555 + required 556 + /> 557 + </div> 558 + )} 559 + <button 560 + type="submit" 561 + className="w-full px-4 py-2 rounded-md bg-blue-600 text-white font-semibold shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:bg-gray-400" 562 + > 563 + {isSubmitting ? "Registering..." : "Register Account"} 564 + </button> 565 + {error && <p className="text-sm text-red-600 mt-2">{error}</p>} 566 + {success && <p className="text-sm text-green-600 mt-2">{success}</p>} 567 + </fieldset> 568 + </form> 569 + </> 570 + ); 571 + } 572 + 573 + export function App({ 574 + type, 575 + initialData, 576 + }: { 577 + type: "index" | "view"; 578 + initialData?: { 579 + config: { 580 + inviteOnly: boolean; 581 + //port: number; 582 + did: string; 583 + host: string; 584 + indexPriority?: string[]; 585 + }; 586 + users: { 587 + did: string; 588 + role: string; 589 + registrationdate: string; 590 + onboardingstatus: string; 591 + pfp?: string; 592 + displayname: string; 593 + handle: string; 594 + }[]; 595 + }; 596 + }) { 597 + const { agent, loginStatus, loading } = useAuth(); 598 + console.log("hasinitialdata?",initialData ? true : false); 599 + const [users, setUsers] = useState< 600 + { 601 + did: string; 602 + role: string; 603 + registrationdate: string; 604 + onboardingstatus: string; 605 + pfp?: string; 606 + displayname: string; 607 + handle: string; 608 + }[] 609 + >(initialData?.users ?? []); 610 + const [usersLoading, setUsersLoading] = useState(!initialData?.users?.length); 611 + 612 + 613 + const isIndex = type === "index"; 614 + const capitaltitle = isIndex ? "Index" : "View"; 615 + const isLoggedIn: boolean = !!(loginStatus && agent?.did && !loading); 616 + const [config, putConfig] = useState< 617 + | { 618 + inviteOnly: boolean; 619 + //port: number; 620 + did: string; 621 + host: string; 622 + indexPriority?: string[]; 623 + } 624 + | undefined 625 + >(initialData?.config ?? undefined); 626 + const [configloading, setconfigloading] = useState(!initialData?.config); 627 + useEffect(() => { 628 + if (config) return 629 + async function fetchConfig() { 630 + try { 631 + const response = await fetch("/_unspecced/config"); 632 + if (!response.ok) throw new Error("Failed to fetch user list"); 633 + const data = await response.json(); 634 + console.log(data); 635 + putConfig(data); 636 + } catch (error) { 637 + console.error("Error fetching config:", error); 638 + } finally { 639 + setconfigloading(false); 640 + } 641 + } 642 + fetchConfig(); 643 + }, []); 644 + 645 + useEffect(() => { 646 + if (users.length) return 647 + async function fetchUsers() { 648 + try { 649 + const response = await fetch("/_unspecced/users"); 650 + if (!response.ok) throw new Error("Failed to fetch user list"); 651 + const data = await response.json(); 652 + setUsers(data); 653 + } catch (error) { 654 + console.error("Error fetching users:", error); 655 + } finally { 656 + setUsersLoading(false); 657 + } 658 + } 659 + fetchUsers(); 660 + }, []); 661 + 662 + const instancehost = config?.did 663 + ? (config?.did).slice("did:web:".length) 664 + : undefined; 665 + 666 + return ( 667 + <div className="bg-gray-100 min-h-screen font-sans"> 668 + <Header 669 + isLoggedIn={isLoggedIn} 670 + agent={agent ?? undefined} 671 + isIndex={isIndex} 672 + capitaltitle={capitaltitle} 673 + instancehost={instancehost} 674 + /> 675 + 676 + <main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8"> 677 + <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start"> 678 + <div className="lg:col-span-2 space-y-8"> 679 + <Card> 680 + <InstanceInfo config={config} configloading={configloading} /> 681 + </Card> 682 + <Card> 683 + <UserList users={users} isLoading={usersLoading} /> 684 + </Card> 685 + {isLoggedIn && <DataManager agent={agent ?? undefined} />} 686 + <Card> 687 + <APIStatus /> 688 + </Card> 689 + </div> 690 + 691 + <div className="space-y-8"> 692 + <Card> 693 + <RegistrationForm 694 + isLoggedIn={isLoggedIn} 695 + agent={agent} 696 + inviteRequired={config?.inviteOnly ?? false} 697 + /> 698 + </Card> 699 + <Card> 700 + <AboutSkylite type={type} /> 701 + </Card> 702 + {isIndex && <ApiTester />} 703 + {!isIndex && <SocialAppButton did={config?.did} />} 704 + </div> 705 + </div> 706 + </main> 707 + 708 + <footer className="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 text-xs text-gray-500 border-t border-gray-200 mt-8 flex justify-end gap-4"> 709 + <a 710 + href={instanceConfig.repoUrl} 711 + target="_blank" 712 + rel="noopener noreferrer" 713 + className="hover:underline" 714 + > 715 + Skylite Git Repo 716 + </a> 717 + <span>Icons by Font Awesome (CC BY 4.0)</span> 718 + </footer> 719 + </div> 720 + ); 721 + } 722 + 723 + export function Root({ 724 + type, 725 + initialData, 726 + }: { 727 + type: "index" | "view"; 728 + initialData?: { 729 + config: { 730 + inviteOnly: boolean; 731 + //port: number; 732 + did: string; 733 + host: string; 734 + indexPriority?: string[]; 735 + }; 736 + users: { 737 + did: string; 738 + role: string; 739 + registrationdate: string; 740 + onboardingstatus: string; 741 + pfp?: string; 742 + displayname: string; 743 + handle: string; 744 + }[]; 745 + }; 746 + }) { 747 + return ( 748 + <AuthProvider> 749 + <App type={type} initialData={initialData} /> 750 + </AuthProvider> 751 + ); 752 + }
+14
shared-landing/browser/landing-view.tsx
··· 1 + import React, { useState } from "https://esm.sh/react@19.1.1"; 2 + import { createRoot, hydrateRoot } from "https://esm.sh/react-dom@19.1.1/client"; 3 + import * as ATPAPI from "https://esm.sh/@atproto/api"; 4 + import { AuthProvider } from "./passauthprovider.tsx"; 5 + import { Root } from "./landing-shared.tsx"; 6 + 7 + const initialDataEl = document.getElementById("initial-data"); 8 + const initialData = initialDataEl ? JSON.parse(initialDataEl.textContent!) : undefined; 9 + // rule of thumb this is the only place where we can do browser-only code 10 + 11 + createRoot(document.getElementById("root")!).render( 12 + <Root type="view" initialData={initialData}/> 13 + ); 14 + //hydrateRoot(document.getElementById("root")!, <App />);
+144
shared-landing/browser/passauthprovider.tsx
··· 1 + import React, { createContext, useState, useEffect, useContext } from 'https://esm.sh/react@19.1.1'; 2 + import { AtpAgent, type AtpSessionData } from 'https://esm.sh/@atproto/api'; 3 + 4 + interface AuthContextValue { 5 + agent: AtpAgent | null; 6 + loginStatus: boolean; 7 + login: (user: string, password: string, service?: string) => Promise<void>; 8 + logout: () => Promise<void>; 9 + loading: boolean; 10 + authed: boolean | undefined; 11 + } 12 + 13 + const AuthContext = createContext<AuthContextValue>({} as AuthContextValue); 14 + 15 + export const AuthProvider = ({ children } : { children: React.ReactNode }) => { 16 + const [agent, setAgent] = useState<AtpAgent | null>(null); 17 + const [loginStatus, setLoginStatus] = useState(false); 18 + const [loading, setLoading] = useState(true); 19 + const [increment, setIncrement] = useState(0); 20 + const [authed, setAuthed] = useState<boolean | undefined>(undefined); 21 + 22 + useEffect(() => { 23 + const initialize = async () => { 24 + try { 25 + const service = localStorage.getItem('service'); 26 + // const user = await AsyncStorage.getItem('user'); 27 + // const password = await AsyncStorage.getItem('password'); 28 + const session = localStorage.getItem("sess"); 29 + 30 + if (service && session) { 31 + console.log("Auto-login service is:", service); 32 + const apiAgent = new AtpAgent({ service }); 33 + try { 34 + if (!apiAgent) { 35 + console.log("Agent is null or undefined"); 36 + return; 37 + } 38 + let sess: AtpSessionData = JSON.parse(session); 39 + console.log("resuming session is:", sess); 40 + const { data } = await apiAgent.resumeSession(sess); 41 + console.log("!!!8!!! agent resume session") 42 + setAgent(apiAgent); 43 + setLoginStatus(true); 44 + setLoading(false); 45 + setAuthed(true); 46 + } catch (e) { 47 + console.log("Failed to resume session" + e); 48 + setLoginStatus(true); 49 + localStorage.removeItem("sess"); 50 + localStorage.removeItem('service'); 51 + const apiAgent = new AtpAgent({ service: 'https://api.bsky.app' }); 52 + setAgent(apiAgent); 53 + setLoginStatus(true); 54 + setLoading(false); 55 + setAuthed(false); 56 + return; 57 + } 58 + } 59 + else { 60 + const apiAgent = new AtpAgent({ service: 'https://api.bsky.app' }); 61 + setAgent(apiAgent); 62 + setLoginStatus(true); 63 + setLoading(false); 64 + setAuthed(false); 65 + } 66 + } catch (e) { 67 + console.log('Failed to auto-login:', e); 68 + } finally { 69 + setLoading(false); 70 + } 71 + }; 72 + 73 + initialize(); 74 + }, [increment]); 75 + 76 + const login = async (user: string, password: string, service: string = 'https://bsky.social') => { 77 + try { 78 + let sessionthing 79 + const apiAgent = new AtpAgent({ 80 + service: service, 81 + persistSession: (evt, sess) => { 82 + sessionthing = sess; 83 + }, 84 + }); 85 + await apiAgent.login({ identifier: user, password }); 86 + console.log("!!!8!!! agent logged on") 87 + 88 + localStorage.setItem('service', service); 89 + // await AsyncStorage.setItem('user', user); 90 + // await AsyncStorage.setItem('password', password); 91 + if (sessionthing) { 92 + localStorage.setItem('sess', JSON.stringify(sessionthing)); 93 + } else { 94 + localStorage.setItem('sess', '{}'); 95 + } 96 + 97 + setAgent(apiAgent); 98 + setLoginStatus(true); 99 + setAuthed(true); 100 + } catch (e) { 101 + console.error('Login failed:', e); 102 + } 103 + }; 104 + 105 + const logout = async () => { 106 + if (!agent) { 107 + console.error("Agent is null or undefined"); 108 + return; 109 + } 110 + setLoading(true); 111 + try { 112 + // check if its even in async storage before removing 113 + if (localStorage.getItem('service') && localStorage.getItem('sess')) { 114 + localStorage.removeItem('service'); 115 + localStorage.removeItem('sess'); 116 + } 117 + await agent.logout(); 118 + console.log("!!!8!!! agent logout") 119 + setLoginStatus(false); 120 + setAuthed(undefined); 121 + await agent.com.atproto.server.deleteSession(); 122 + console.log("!!!8!!! agent deltesession") 123 + //setAgent(null); 124 + setIncrement(increment + 1); 125 + } catch (e) { 126 + console.error("Logout failed:", e); 127 + } finally { 128 + setLoading(false); 129 + } 130 + }; 131 + 132 + // why the hell are we doing this 133 + /*if (loading) { 134 + return <div><span>Laoding...ae</span></div>; 135 + }*/ 136 + 137 + return ( 138 + <AuthContext.Provider value={{ agent, loginStatus, login, logout, loading, authed }}> 139 + {children} 140 + </AuthContext.Provider> 141 + ); 142 + }; 143 + 144 + export const useAuth = () => useContext(AuthContext);
+214
shared-landing/browser/passlogin.tsx
··· 1 + import React, { useEffect, useState, useRef } from 'https://esm.sh/react@19.1.1'; 2 + import { useAuth } from './passauthprovider.tsx'; 3 + 4 + interface LoginProps { 5 + compact?: boolean; 6 + } 7 + 8 + export default function Login({ compact = false }: LoginProps) { 9 + const { loginStatus, login, logout, loading, authed } = useAuth(); 10 + const [user, setUser] = useState(''); 11 + const [password, setPassword] = useState(''); 12 + const [serviceURL, setServiceURL] = useState('bsky.social'); 13 + const [showLoginForm, setShowLoginForm] = useState(false); 14 + const formRef = useRef<HTMLDivElement>(null); 15 + 16 + useEffect(() => { 17 + function handleClickOutside(event: MouseEvent) { 18 + if (formRef.current && !formRef.current.contains(event.target as Node)) { 19 + setShowLoginForm(false); 20 + } 21 + } 22 + 23 + if (showLoginForm) { 24 + document.addEventListener('mousedown', handleClickOutside); 25 + } 26 + 27 + return () => { 28 + document.removeEventListener('mousedown', handleClickOutside); 29 + }; 30 + }, [showLoginForm]); 31 + 32 + if (loading) { 33 + return ( 34 + <div className="flex items-center justify-center p-0 text-gray-500 dark:text-gray-400"> 35 + Loading... 36 + </div> 37 + ); 38 + } 39 + 40 + if (compact) { 41 + if (authed) { 42 + return ( 43 + <button 44 + onClick={logout} 45 + className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors" 46 + > 47 + Log out 48 + </button> 49 + ); 50 + } else { 51 + return ( 52 + <div className="relative" ref={formRef}> 53 + <button 54 + onClick={() => setShowLoginForm(!showLoginForm)} 55 + className="text-sm bg-gray-600 hover:bg-gray-700 text-white rounded px-3 py-1 font-medium transition-colors" 56 + > 57 + Log in 58 + </button> 59 + {showLoginForm && ( 60 + <div className="absolute top-full right-0 mt-2 w-80 bg-white dark:bg-gray-900 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-4 z-50"> 61 + <form 62 + onSubmit={e => { 63 + e.preventDefault(); 64 + login(user, password, `https://${serviceURL}`); 65 + setShowLoginForm(false); 66 + }} 67 + className="flex flex-col gap-3" 68 + > 69 + <p className="text-xs text-gray-500 dark:text-gray-400">sorry for the temporary login,<br />oauth will come soon enough i swear</p> 70 + <input 71 + type="text" 72 + placeholder="Username" 73 + value={user} 74 + onChange={e => setUser(e.target.value)} 75 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 76 + autoComplete="username" 77 + /> 78 + <input 79 + type="password" 80 + placeholder="Password" 81 + value={password} 82 + onChange={e => setPassword(e.target.value)} 83 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 84 + autoComplete="current-password" 85 + /> 86 + <input 87 + type="text" 88 + placeholder="bsky.social" 89 + value={serviceURL} 90 + onChange={e => setServiceURL(e.target.value)} 91 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 92 + /> 93 + <button 94 + type="submit" 95 + className="bg-gray-600 hover:bg-gray-700 text-white rounded px-4 py-2 font-medium text-sm transition-colors" 96 + > 97 + Log in 98 + </button> 99 + </form> 100 + </div> 101 + )} 102 + </div> 103 + ); 104 + } 105 + } 106 + 107 + return ( 108 + <div className="p-6 bg-gray-100 dark:bg-gray-900 rounded-xl shadow border border-gray-200 dark:border-gray-800 mt-6 mx-4"> 109 + {authed ? ( 110 + <div className="flex flex-col items-center justify-center text-center"> 111 + <p className="text-lg font-semibold mb-6 text-gray-800 dark:text-gray-100">You are logged in!</p> 112 + <button 113 + onClick={logout} 114 + className="bg-gray-600 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors" 115 + > 116 + Log out 117 + </button> 118 + </div> 119 + ) : ( 120 + <form 121 + onSubmit={e => { 122 + e.preventDefault(); 123 + login(user, password, `https://${serviceURL}`); 124 + }} 125 + className="flex flex-col gap-4" 126 + > 127 + <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">sorry for the temporary login,<br />oauth will come soon enough i swear</p> 128 + <input 129 + type="text" 130 + placeholder="Username" 131 + value={user} 132 + onChange={e => setUser(e.target.value)} 133 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500" 134 + autoComplete="username" 135 + /> 136 + <input 137 + type="password" 138 + placeholder="Password" 139 + value={password} 140 + onChange={e => setPassword(e.target.value)} 141 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500" 142 + autoComplete="current-password" 143 + /> 144 + <input 145 + type="text" 146 + placeholder="bsky.social" 147 + value={serviceURL} 148 + onChange={e => setServiceURL(e.target.value)} 149 + className="px-3 py-2 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-base focus:outline-none focus:ring-2 focus:ring-blue-500" 150 + /> 151 + <button 152 + type="submit" 153 + className="bg-gray-600 hover:bg-gray-700 text-white rounded px-6 py-2 font-semibold text-base transition-colors mt-2" 154 + > 155 + Log in 156 + </button> 157 + </form> 158 + )} 159 + </div> 160 + ); 161 + } 162 + 163 + export const ProfileThing = () => { 164 + const { agent, loading, loginStatus, authed } = useAuth(); 165 + const [response, setResponse] = useState<any>(null); 166 + 167 + useEffect(() => { 168 + if (loginStatus && agent && !loading && authed) { 169 + fetchUser(); 170 + } 171 + }, [loginStatus, agent, loading, authed]); 172 + 173 + const fetchUser = async () => { 174 + if (!agent) { 175 + console.error("Agent is null or undefined"); 176 + return; 177 + } 178 + const res = await agent.app.bsky.actor.getProfile({ 179 + actor: agent.assertDid, 180 + }); 181 + setResponse(res.data); 182 + }; 183 + 184 + if (!authed) { 185 + return ( 186 + <div className="inline-block"> 187 + <span className="text-gray-100 text-base font-medium px-1.5">Login</span> 188 + </div> 189 + ); 190 + } 191 + 192 + if (!response) { 193 + return ( 194 + <div className="flex flex-col items-start gap-1.5"> 195 + <span className="w-5 h-5 border-2 border-gray-200 dark:border-gray-600 border-t-transparent rounded-full animate-spin inline-block" /> 196 + <span className="text-gray-100">Loading... </span> 197 + </div> 198 + ); 199 + } 200 + 201 + return ( 202 + <div className="flex flex-row items-start gap-1.5"> 203 + <img 204 + src={response?.avatar} 205 + alt="avatar" 206 + className="w-[30px] h-[30px] rounded-full object-cover" 207 + /> 208 + <div> 209 + <div className="text-gray-100 text-xs">{response?.displayName}</div> 210 + <div className="text-gray-100 text-xs">@{response?.handle}</div> 211 + </div> 212 + </div> 213 + ); 214 + };
+184
shared-landing/build.ts
··· 1 + import * as esbuild from "npm:esbuild@0.20.2"; 2 + import { ReactCompilerEsbuildPlugin } from "./reactcompileresbuild.ts"; 3 + import { cache } from "npm:esbuild-plugin-cache"; 4 + import tailwindconfig from "../tailwind.config.ts"; 5 + import tailwindcss from "https://esm.sh/tailwindcss@3"; 6 + import postcss from "https://esm.sh/postcss@8"; 7 + import autoprefixer from "https://esm.sh/autoprefixer@10"; 8 + import { renderToString } from "https://esm.sh/react-dom@19.1.1/server"; 9 + import { createElement } from "https://esm.sh/react@19.1.1"; 10 + import { Root } from "./browser/landing-shared.tsx"; 11 + 12 + // helper build function 13 + async function build({ 14 + entry, 15 + initialData, 16 + }: { 17 + entry: "index" | "view"; 18 + initialData?: { 19 + config: { 20 + inviteOnly: boolean; 21 + //port: number; 22 + did: string; 23 + host: string; 24 + indexPriority?: string[]; 25 + }; 26 + users: { 27 + did: string; 28 + role: string; 29 + registrationdate: string; 30 + onboardingstatus: string; 31 + pfp?: string; 32 + displayname: string; 33 + handle: string; 34 + }[]; 35 + }; 36 + }) { 37 + const template = await Deno.readTextFile( 38 + `./shared-landing/template-${entry}.html` 39 + ); 40 + const importmap = { 41 + imports: { 42 + "react/jsx-runtime": "https://esm.sh/react@19.1.1/jsx-runtime", 43 + "react/compiler-runtime": "https://esm.sh/react@19.1.1/compiler-runtime", 44 + }, 45 + }; 46 + 47 + const result = await esbuild.build({ 48 + entryPoints: [`./shared-landing/browser/landing-${entry}.tsx`], 49 + bundle: true, 50 + format: "esm", 51 + jsx: "transform", 52 + write: false, // keep in memory 53 + loader: { ".tsx": "tsx" }, 54 + jsxFactory: "React.createElement", 55 + jsxFragment: "React.Fragment", 56 + platform: "neutral", //"browser", 57 + external: ["react/jsx-runtime", "react/compiler-runtime"], 58 + plugins: [ 59 + cache({ importmap, directory: "./cache" }), 60 + ReactCompilerEsbuildPlugin({ 61 + filter: /\.tsx?$/, 62 + sourceMaps: true, 63 + runtimeModulePath: "https://esm.sh/react@19.1.1/jsx-runtime", 64 + }), 65 + ], 66 + }); 67 + const rawcss = await Deno.readTextFile("./shared-landing/app.css"); 68 + 69 + // @ts-ignore its fiiine 70 + const cssResult = await postcss([ 71 + tailwindcss(tailwindconfig), 72 + autoprefixer(), 73 + ]).process(rawcss, { 74 + from: "./shared-landing/app.css", 75 + map: false, 76 + }); 77 + 78 + const js = result.outputFiles[0].text; 79 + 80 + const jshash = hashString(js); 81 + const csshash = hashString(cssResult.css); 82 + const html = template.replace( 83 + "<!--SCRIPT-INJECT-->", 84 + `<script type="module" src="/landing-${entry}.js?v=${jshash}"></script> 85 + <link href="./app.css?v=${csshash}" rel="stylesheet"> 86 + <style>${cssResult}</style> 87 + <script id="initial-data" type="application/json"> 88 + ${JSON.stringify(initialData)} 89 + </script>` 90 + ); 91 + // const html = template.replace( 92 + // "<!--SCRIPT-INJECT-->", 93 + // `<script type="module" src="/landing-${entry}.js?v=${jshash}"></script> 94 + // <link href="./app.css?v=${csshash}" rel="stylesheet">` 95 + // ); 96 + const ssr = renderToString( 97 + createElement( 98 + () => 99 + Root({ 100 + type: entry, 101 + initialData 102 + }), 103 + null 104 + ) 105 + ); 106 + const ssrhtml = html.replace("<!--APP-INJECT-->", `${ssr}`); 107 + 108 + return { js, html: ssrhtml, css: cssResult.css }; 109 + } 110 + 111 + // public compile function 112 + export async function compile({ 113 + target, 114 + initialData 115 + }: { 116 + target: "index" | "view"; 117 + initialData?: { 118 + config: { 119 + inviteOnly: boolean; 120 + //port: number; 121 + did: string; 122 + host: string; 123 + indexPriority?: string[]; 124 + }; 125 + users: { 126 + did: string; 127 + role: string; 128 + registrationdate: string; 129 + onboardingstatus: string; 130 + pfp?: string; 131 + displayname: string; 132 + handle: string; 133 + }[]; 134 + }; 135 + }) { 136 + return await build({entry: target, initialData}); 137 + } 138 + 139 + // watch loop 140 + export async function devWatch({target,initialData,onBuild}:{ 141 + target: "index" | "view", 142 + initialData?: { 143 + config: { 144 + inviteOnly: boolean; 145 + //port: number; 146 + did: string; 147 + host: string; 148 + indexPriority?: string[]; 149 + }; 150 + users: { 151 + did: string; 152 + role: string; 153 + registrationdate: string; 154 + onboardingstatus: string; 155 + pfp?: string; 156 + displayname: string; 157 + handle: string; 158 + }[]; 159 + }, 160 + onBuild: (data: { js: string; html: string; css: string }) => void 161 + } 162 + ) { 163 + for await (const event of Deno.watchFs(".")) { 164 + if (event.paths.some((p) => p.endsWith(".tsx"))) { 165 + console.log("Rebuilding bundleโ€ฆ"); 166 + const data = await compile({target,initialData}); 167 + onBuild(data); 168 + } 169 + } 170 + } 171 + 172 + async function hashString(content: string): Promise<string> { 173 + const encoder = new TextEncoder(); 174 + const data = encoder.encode(content); 175 + 176 + // SHA-256 hash 177 + const hashBuffer = await crypto.subtle.digest("SHA-256", data); 178 + 179 + // Convert buffer to hex string 180 + return Array.from(new Uint8Array(hashBuffer)) 181 + .map((b) => b.toString(16).padStart(2, "0")) 182 + .join("") 183 + .slice(0, 8); // optional: shorten hash for filenames 184 + }
+85
shared-landing/reactcompileresbuild.ts
··· 1 + //https://gist.github.com/sikanhe/f9ac68dd4c78c914c29cc98e7b875466 2 + import { readFileSync } from "node:fs" 3 + import * as babel from "npm:@babel/core" 4 + //import BabelPluginReactCompiler from "npm:babel-plugin-react-compiler" 5 + import BabelPluginReactCompiler from "npm:babel-plugin-react-compiler" 6 + import * as esbuild from "npm:esbuild@0.20.2"; 7 + import QuickLRU from "npm:quick-lru" 8 + 9 + export function ReactCompilerEsbuildPlugin({ 10 + filter, 11 + sourceMaps, 12 + runtimeModulePath, 13 + }: { filter: RegExp; sourceMaps: boolean; runtimeModulePath: string }): esbuild.Plugin { 14 + return { 15 + name: "esbuild-react-compiler-plugin", 16 + setup(build) { 17 + // Cache previous outputs for incremental rebuilds 18 + const buildCache = new QuickLRU<string, string>({ maxSize: 1000 }) 19 + 20 + let timings: number[] = [] 21 + 22 + build.onEnd(() => { 23 + if (timings.length < 1) return 24 + 25 + const totalTime = timings.reduce((sum, x) => sum + x, 0).toFixed(0) 26 + console.log(`[โš›๏ธ React Compiler] ${timings.length} files changed`) 27 + console.log(`[โš›๏ธ React Compiler] Used ${totalTime} ms`) 28 + 29 + timings = [] 30 + }) 31 + 32 + build.onLoad({ filter, namespace: "" }, (args) => { 33 + const contents = readFileSync(args.path, "utf8") 34 + 35 + const t0 = performance.now() 36 + 37 + if (buildCache.has(contents)) { 38 + return { 39 + contents: buildCache.get(contents), 40 + loader: "js", 41 + } 42 + } 43 + 44 + const output = build.esbuild.transformSync(contents, { 45 + loader: "tsx", 46 + jsx: "automatic", 47 + define: build.initialOptions.define, 48 + target: build.initialOptions.target, 49 + }) 50 + 51 + const transformResult = babel.transformSync(output.code, { 52 + plugins: [ 53 + // Warning: using string config here (ie 'babel-plugin-react-compiler') instead of the directly 54 + // imported object is much slower than directly passing the plugin object because 55 + // Babel has to resolve the plugin file from node_modules 56 + [ 57 + BabelPluginReactCompiler, 58 + { 59 + runtimeModule: runtimeModulePath, 60 + }, 61 + ], 62 + ], 63 + filename: args.path, 64 + caller: { 65 + name: "esbuild-react-compiler-plugin", 66 + supportsStaticESM: true, 67 + }, 68 + // TODO: figure out sourcemap setting and chaining 69 + sourceMaps, 70 + }) 71 + 72 + timings.push(performance.now() - t0) 73 + 74 + if (transformResult?.code) { 75 + buildCache.set(contents, transformResult?.code) 76 + } 77 + 78 + return { 79 + contents: transformResult?.code ?? undefined, 80 + loader: "js", 81 + } 82 + }) 83 + }, 84 + } 85 + }
+14
shared-landing/template-index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6 + <link rel="icon" href="/public/index.ico" /> 7 + <title>Skylite Index Server</title> 8 + <!-- <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> --> 9 + <!--SCRIPT-INJECT--> 10 + </head> 11 + <body class="bg-gray-100 min-h-screen flex items-center justify-center"> 12 + <div class="flex flex-1 flex-col" id="root"><!--APP-INJECT--></div> 13 + </body> 14 + </html>
+14
shared-landing/template-view.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 6 + <link rel="icon" href="/public/view.ico" /> 7 + <title>Skylite View Server</title> 8 + <!-- <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> --> 9 + <!--SCRIPT-INJECT--> 10 + </head> 11 + <body class="bg-gray-100 min-h-screen flex items-center justify-center"> 12 + <div class="flex flex-1 flex-col" id="root"><!--APP-INJECT--></div> 13 + </body> 14 + </html>
+11
tailwind.config.ts
··· 1 + /** @type {import('npm:tailwindcss').Config} */ 2 + export default { 3 + content: ["./**/*.{tsx,html}"], // make sure all your HTML/JSX paths are here 4 + theme: { 5 + extend: {}, 6 + }, 7 + plugins: [ 8 + // add Tailwind UI plugins if you want, like forms/typography 9 + // require("@tailwindcss/forms"), etc. 10 + ], 11 + };
+2 -2
utils/auth.borrowed.ts
··· 22 22 return { DidPlcResolver: resolve } 23 23 } 24 24 const myResolver = getResolver() 25 - const web = getWebResolver() 25 + const webResolver = getWebResolver() 26 26 const resolver: ResolverRegistry = { 27 27 'plc': myResolver.DidPlcResolver as unknown as DIDResolver, 28 - 'web': web as unknown as DIDResolver, 28 + ...webResolver 29 29 } 30 30 export const resolverInstance = new Resolver(resolver) 31 31 export type Service = {
+5 -1
utils/auth.ts
··· 61 61 return null; 62 62 } 63 63 } 64 - 64 + /** 65 + * @deprecated dont use this use getAuthenticatedDid() instead 66 + * @param param0 67 + * @returns 68 + */ 65 69 export const authVerifier: MethodAuthVerifier<AuthResult> = async ({ req }) => { 66 70 //console.log("help us all fuck you",req) 67 71 console.log("you are doing well")
-16
utils/dbsystem.ts
··· 33 33 handle TEXT 34 34 ); 35 35 ${createIndexINE} idx_did_handle ON did(handle); 36 - 37 - -- A global index for relationships between any two pieces of content 38 - ${createTableINE} backlink_skeleton ( 39 - id INTEGER PRIMARY KEY AUTOINCREMENT, 40 - srcuri TEXT, 41 - srcdid TEXT, 42 - srcfield TEXT, 43 - srccol TEXT, 44 - suburi TEXT, 45 - subdid TEXT, 46 - subcol TEXT 47 - ); 48 - ${createIndexINE} idx_backlink_subdid_mod ON backlink_skeleton(subdid, srcdid); 49 - ${createIndexINE} idx_backlink_suburi_mod ON backlink_skeleton(suburi, srcdid); 50 - ${createIndexINE} idx_backlink_subdid_filter_mod ON backlink_skeleton(subdid, srccol, srcdid); 51 - ${createIndexINE} idx_backlink_suburi_filter_mod ON backlink_skeleton(suburi, srccol, srcdid); 52 36 `); 53 37 }
+19 -1
utils/dbuser.ts
··· 32 32 avatarcid TEXT, 33 33 avatarmime TEXT, 34 34 bannercid TEXT, 35 - bannermime TEXT 35 + bannermime TEXT, 36 + pinned TEXT 36 37 ); 37 38 ${createIndexINE} idx_actor_profile_did ON app_bsky_actor_profile(did); 38 39 ··· 131 132 -- User's notification settings declaration 132 133 ${createTableINE} app_bsky_notification_declaration ( ${baseColumns}, allowSubscriptions TEXT ); 133 134 ${createIndexINE} idx_notification_declaration_author ON app_bsky_notification_declaration(did); 135 + 136 + -- A global index for relationships between any two pieces of content 137 + ${createTableINE} backlink_skeleton ( 138 + id INTEGER PRIMARY KEY AUTOINCREMENT, 139 + srcuri TEXT, 140 + srcdid TEXT, 141 + srcfield TEXT, 142 + srccol TEXT, 143 + suburi TEXT, 144 + subdid TEXT, 145 + subcol TEXT, 146 + indexedAt INTEGER NOT NULL 147 + ); 148 + ${createIndexINE} idx_backlink_subdid_mod ON backlink_skeleton(subdid, srcdid); 149 + ${createIndexINE} idx_backlink_suburi_mod ON backlink_skeleton(suburi, srcdid); 150 + ${createIndexINE} idx_backlink_subdid_filter_mod ON backlink_skeleton(subdid, srccol, srcdid); 151 + ${createIndexINE} idx_backlink_suburi_filter_mod ON backlink_skeleton(suburi, srccol, srcdid); 134 152 `); 135 153 }
+37 -22
utils/diddoc.ts
··· 1 - export const didDocument = { 2 - "@context": [ 3 - "https://www.w3.org/ns/did/v1", 4 - "https://w3id.org/security/multikey/v1", 5 - ], 6 - id: `${Deno.env.get("SERVICE_DID")}`, 7 - verificationMethod: [ 8 - { 9 - id: `${Deno.env.get("SERVICE_DID")}#atproto`, 10 - type: "Multikey", 11 - controller: `${Deno.env.get("SERVICE_DID")}`, 12 - publicKeyMultibase: "bullshit", 1 + // type "both" should not be used 2 + export function didDocument( 3 + type: "view" | "index" | "both", 4 + did: string, 5 + endpoint: string, 6 + publicKeyMultibase: string, 7 + ) { 8 + const services = [ 9 + (type === "view" || type === "both") && { 10 + id: "#bsky_appview", 11 + type: "BskyAppView", 12 + serviceEndpoint: endpoint, 13 13 }, 14 - ], 15 - service: [ 16 - { 14 + (type === "view" || type === "both") && { 17 15 id: "#bsky_notif", 18 16 type: "BskyNotificationService", 19 - serviceEndpoint: `${Deno.env.get("SERVICE_ENDPOINT")}`, 17 + serviceEndpoint: endpoint, 20 18 }, 21 - { 22 - id: "#bsky_appview", 23 - type: "BskyAppView", 24 - serviceEndpoint: `${Deno.env.get("SERVICE_ENDPOINT")}`, 19 + (type === "index" || type === "both") && { 20 + id: "#skylite_index", 21 + type: "SkyliteIndexServer", 22 + serviceEndpoint: endpoint, 25 23 }, 26 - ], 27 - }; 24 + ].filter(Boolean); 25 + 26 + return { 27 + "@context": [ 28 + "https://www.w3.org/ns/did/v1", 29 + "https://w3id.org/security/multikey/v1", 30 + ], 31 + id: did, 32 + verificationMethod: [ 33 + { 34 + id: `${did}#atproto`, 35 + type: "Multikey", 36 + controller: did, 37 + publicKeyMultibase: publicKeyMultibase, 38 + }, 39 + ], 40 + service: services, 41 + }; 42 + }
+16 -1
utils/identity.ts
··· 1 1 2 2 import { DidResolver, HandleResolver } from "npm:@atproto/identity"; 3 - import { systemDB } from "../main.ts"; 3 + import { Database } from "jsr:@db/sqlite@0.11"; 4 + import { AtUri } from "npm:@atproto/api"; 5 + const systemDB = new Database("./system.db") // TODO: temporary shim. should seperate this to its own central system db instead of the now instantiated system dbs 4 6 type DidMethod = "web" | "plc"; 5 7 type DidDoc = { 6 8 "@context"?: unknown; ··· 223 225 } catch (err) { 224 226 console.error(`Failed to extract/store PDS and handle for '${did}':`, err); 225 227 return null; 228 + } 229 + } 230 + 231 + export function extractDid(input: string): string { 232 + if (input.startsWith('did:')) { 233 + return input 234 + } 235 + 236 + try { 237 + const uri = new AtUri(input) 238 + return uri.host 239 + } catch (e) { 240 + throw new Error(`Invalid input: expected a DID or a valid AT URI, got "${input}"`) 226 241 } 227 242 }
+12 -1
utils/records.ts
··· 75 75 if (result.success) return result.value; 76 76 return undefined; 77 77 } 78 - 78 + export function assertRecord<T extends KnownRecordType>( 79 + record: unknown 80 + ): RecordTypeMap[T] | undefined { 81 + if (typeof record !== 'object' || record === null) { 82 + return undefined; 83 + } 84 + const type = (record as { $type?: string })?.$type; 85 + if (typeof type !== 'string' || !(type in recordValidators)) { 86 + return undefined; 87 + } 88 + return record as RecordTypeMap[T]; 89 + } 79 90 export async function resolveRecordFromURI({ 80 91 did, 81 92 uri,
+14 -4
utils/server.ts
··· 1 1 import ky from "npm:ky"; 2 2 import QuickLRU from "npm:quick-lru"; 3 3 import { createHash } from "node:crypto"; 4 - import { slingshoturl, constellationurl } from "../main.ts"; 4 + import { config } from "../config.ts"; 5 5 import * as ATPAPI from "npm:@atproto/api"; 6 6 7 7 const cache = new QuickLRU({ maxSize: 10000 }); ··· 57 57 export async function resolveIdentity( 58 58 actor: string 59 59 ): Promise<SlingshotMiniDoc> { 60 - const url = `${slingshoturl}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`; 60 + const url = `${config.slingshot}/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${actor}`; 61 61 return (await cachedFetch(url)) as SlingshotMiniDoc; 62 62 } 63 63 export async function getRecord({ ··· 155 155 collection: string, 156 156 rkey: string 157 157 ): Promise<GetRecord> { 158 - const url = `${slingshoturl}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`; 158 + const identity = await resolveIdentity(did); 159 + //const url = `${config.slingshot}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`; 160 + const url = `${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=${collection}&rkey=${rkey}`; 159 161 const result = (await cachedFetch(url)) as GetRecord; 160 162 return result as GetRecord; 161 163 } ··· 169 171 collection: string; 170 172 path: string; 171 173 }): Promise<number> { 172 - const url = `${constellationurl}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`; 174 + const url = `${config.constellation}/links/count/distinct-dids?target=${did}&collection=${collection}&path=${path}`; 173 175 const result = (await cachedFetch(url)) as ConstellationDistinctDids; 174 176 return result.total; 175 177 } 178 + 179 + 180 + export function withCors(headers: HeadersInit = {}) { 181 + return { 182 + "Access-Control-Allow-Origin": "*", 183 + ...headers, 184 + }; 185 + }
+5 -2
utils/sharders.ts
··· 1 + import { config } from "../config.ts"; 2 + 1 3 function getShardId(params: URLSearchParams): string { 2 4 params.sort(); 3 5 return params.toString(); ··· 240 242 wantedSubjects: string[]; 241 243 wantedSubjectDids: string[]; 242 244 wantedSources: string[]; 245 + instant: string[]; 243 246 } 244 247 245 248 export class JetstreamManager extends ShardedConnectionManager<JetstreamParams> { 246 249 constructor(onMessage: (msg: any) => void) { 247 250 super( 248 - `${Deno.env.get("JETSTREAM_URL")}/subscribe`, 251 + `${config.jetstream}/subscribe`, 249 252 { wantedDids: 10000, wantedCollections: 100 }, 250 253 onMessage 251 254 ); ··· 255 258 export class SpacedustManager extends ShardedConnectionManager<SpacedustParams> { 256 259 constructor(onMessage: (msg: any) => void) { 257 260 super( 258 - `${Deno.env.get("SPACEDUST_URL")}/subscribe`, 261 + `${config.spacedust}/subscribe`, 259 262 { wantedSubjects: 100, wantedSubjectDids: 100, wantedSources: 100 }, 260 263 onMessage 261 264 );
+1206 -419
viewserver.ts
··· 1 1 import ky from "npm:ky"; 2 2 import { isGeneratorView } from "./indexserver/types/app/bsky/feed/defs.ts"; 3 - import { withCors } from "./main.ts"; 4 3 import * as ViewServerTypes from "./utils/viewservertypes.ts"; 5 4 import * as ATPAPI from "npm:@atproto/api"; 6 5 import { ··· 10 9 cachedFetch, 11 10 didWebToHttps, 12 11 getSlingshotRecord, 12 + withCors, 13 13 } from "./utils/server.ts"; 14 + import QuickLRU from "npm:quick-lru"; 15 + import { validateRecord } from "./utils/records.ts"; 16 + import { indexHandlerContext } from "./index/types.ts"; 17 + import { Database } from "jsr:@db/sqlite@0.11"; 18 + import { JetstreamManager, SpacedustManager } from "./utils/sharders.ts"; 19 + import { SpacedustLinkMessage } from "./index/spacedust.ts"; 20 + import { setupUserDb } from "./utils/dbuser.ts"; 21 + import { config } from "./config.ts"; 22 + import { AtUri } from "npm:@atproto/api"; 23 + import { CID } from "../../Library/Caches/deno/npm/registry.npmjs.org/multiformats/9.9.0/cjs/src/cid.js"; 24 + import { uncid } from "./indexserver.ts"; 25 + import { getAuthenticatedDid } from "./utils/auth.ts"; 14 26 15 - export async function viewServerHandler(req: Request): Promise<Response> { 16 - const url = new URL(req.url); 17 - const pathname = url.pathname; 18 - const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 19 - const hasAuth = req.headers.has("authorization"); 20 - const xrpcMethod = pathname.startsWith("/xrpc/") 21 - ? pathname.slice("/xrpc/".length) 22 - : null; 23 - const searchParams = searchParamsToJson(url.searchParams); 24 - const jsonUntyped = searchParams; 27 + const temporarydevelopmentblockednotiftypes: ATPAPI.AppBskyNotificationListNotifications.Notification["reason"][] = [ 28 + //'like', 29 + //'repost', 30 + //'follow', 31 + //'mention', 32 + //'reply', 33 + //'quote', 34 + //'starterpack-joined', 35 + //'liked-via-repost', 36 + //'repost-via-repost', 37 + ]; 25 38 26 - if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") { 27 - // const jsonTyped = 28 - // jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; 39 + export interface ViewServerConfig { 40 + baseDbPath: string; 41 + systemDbPath: string; 42 + } 29 43 30 - const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ 31 - { 32 - $type: "app.bsky.unspecced.defs#trendingTopic", 33 - topic: "Git Repo", 34 - displayName: "Git Repo", 35 - description: "Git Repo", 36 - link: "https://tangled.sh/@whey.party/skylite", 37 - }, 38 - { 39 - $type: "app.bsky.unspecced.defs#trendingTopic", 40 - topic: "Red Dwarf Lite", 41 - displayName: "Red Dwarf Lite", 42 - description: "Red Dwarf Lite", 43 - link: "https://reddwarflite.whey.party/", 44 - }, 45 - { 46 - $type: "app.bsky.unspecced.defs#trendingTopic", 47 - topic: "whey dot party", 48 - displayName: "whey dot party", 49 - description: "whey dot party", 50 - link: "https://whey.party/", 51 - }, 52 - ]; 44 + interface BaseRow { 45 + uri: string; 46 + did: string; 47 + cid: string | null; 48 + rev: string | null; 49 + createdat: number | null; 50 + indexedAt: number; 51 + json: string | null; 52 + } 53 + interface GeneratorRow extends BaseRow { 54 + displayname: string | null; 55 + description: string | null; 56 + avatarcid: string | null; 57 + } 58 + interface LikeRow extends BaseRow { 59 + subject: string; 60 + } 61 + interface RepostRow extends BaseRow { 62 + subject: string; 63 + } 64 + interface BacklinkRow { 65 + srcuri: string; 66 + srcdid: string; 67 + } 53 68 54 - const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = 55 - { 56 - topics: faketopics, 57 - suggested: faketopics, 58 - }; 69 + const FEED_LIMIT = 50; 59 70 60 - return new Response(JSON.stringify(response), { 61 - headers: withCors({ "Content-Type": "application/json" }), 62 - }); 71 + export class ViewServer { 72 + private config: ViewServerConfig; 73 + public userManager: ViewServerUserManager; 74 + public systemDB: Database; 75 + 76 + constructor(config: ViewServerConfig) { 77 + this.config = config; 78 + 79 + // We will initialize the system DB and user manager here 80 + this.systemDB = new Database(this.config.systemDbPath); 81 + // TODO: We need to setup the system DB schema if it's new 82 + 83 + this.userManager = new ViewServerUserManager(this); // Pass the server instance 63 84 } 64 85 65 - //if (xrpcMethod !== 'app.bsky.actor.getPreferences' && xrpcMethod !== 'app.bsky.notification.listNotifications') { 66 - if ( 67 - !hasAuth 68 - // (!hasAuth || 69 - // xrpcMethod === "app.bsky.labeler.getServices" || 70 - // xrpcMethod === "app.bsky.unspecced.getConfig") && 71 - // xrpcMethod !== "app.bsky.notification.putPreferences" 72 - ) { 73 - return new Response( 74 - JSON.stringify({ 75 - error: "XRPCNotSupported", 76 - message: 77 - "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 78 - }), 79 - { 80 - status: 404, 81 - headers: withCors({ "Content-Type": "application/json" }), 82 - } 83 - ); 84 - //return await sendItToApiBskyApp(req); 86 + public start() { 87 + // This is where we'll kick things off, like the cold start 88 + this.userManager.coldStart(this.systemDB); 89 + console.log("viewServer started."); 85 90 } 86 - if ( 87 - // !hasAuth || 88 - xrpcMethod === "app.bsky.labeler.getServices" || 89 - xrpcMethod === "app.bsky.unspecced.getConfig" //&& 90 - //xrpcMethod !== "app.bsky.notification.putPreferences" 91 - ) { 92 - return new Response( 93 - JSON.stringify({ 94 - error: "XRPCNotSupported", 95 - message: 96 - "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 97 - }), 98 - { 99 - status: 404, 91 + 92 + async unspeccedGetRegisteredUsers(): Promise<{ 93 + did: string; 94 + role: string; 95 + registrationdate: string; 96 + onboardingstatus: string; 97 + pfp?: string; 98 + displayname: string; 99 + handle: string; 100 + }[]|undefined> { 101 + const stmt = this.systemDB.prepare(` 102 + SELECT * 103 + FROM users; 104 + `); 105 + const result = stmt.all() as 106 + { 107 + did: string; 108 + role: string; 109 + registrationdate: string; 110 + onboardingstatus: string; 111 + }[]; 112 + const hydrated = await Promise.all( result.map(async (user)=>{ 113 + const identity = await resolveIdentity(user.did); 114 + const profile = (await getSlingshotRecord(identity.did,"app.bsky.actor.profile","self")).value as ATPAPI.AppBskyActorProfile.Record; 115 + const avatarcid = uncid(profile.avatar?.ref); 116 + const avatar = avatarcid 117 + ? buildBlobUrl(identity.pds, identity.did, avatarcid) 118 + : undefined; 119 + return {...user,handle: identity.handle,pfp: avatar, displayname:profile.displayName ?? identity.handle } 120 + })) 121 + //const exists = result !== undefined; 122 + return hydrated; 123 + } 124 + 125 + async viewServerHandler(req: Request): Promise<Response> { 126 + const url = new URL(req.url); 127 + const pathname = url.pathname; 128 + const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 129 + const hasAuth = req.headers.has("authorization"); 130 + const xrpcMethod = pathname.startsWith("/xrpc/") 131 + ? pathname.slice("/xrpc/".length) 132 + : null; 133 + const searchParams = searchParamsToJson(url.searchParams); 134 + const jsonUntyped = searchParams; 135 + 136 + let tempauthdid: string | undefined = undefined; 137 + try { 138 + tempauthdid = (await getAuthenticatedDid(req)) ?? undefined; 139 + } catch (_e) { 140 + // nothing lol 141 + } 142 + const authdid = tempauthdid 143 + ? this.handlesDid(tempauthdid) 144 + ? tempauthdid 145 + : undefined 146 + : undefined; 147 + console.log("authed:", authdid); 148 + 149 + if (xrpcMethod === "app.bsky.unspecced.getTrendingTopics") { 150 + // const jsonTyped = 151 + // jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; 152 + 153 + const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ 154 + { 155 + $type: "app.bsky.unspecced.defs#trendingTopic", 156 + topic: "Git Repo", 157 + displayName: "Git Repo", 158 + description: "Git Repo", 159 + link: "https://tangled.sh/@whey.party/skylite", 160 + }, 161 + { 162 + $type: "app.bsky.unspecced.defs#trendingTopic", 163 + topic: "this View Server url", 164 + displayName: "this View Server url", 165 + description: "this View Server url", 166 + link: config.viewServer.host, 167 + }, 168 + { 169 + $type: "app.bsky.unspecced.defs#trendingTopic", 170 + topic: "this social-app fork url", 171 + displayName: "this social-app fork url", 172 + description: "this social-app fork url", 173 + link: "https://github.com/rimar1337/social-app/tree/publicappview-colorable", 174 + }, 175 + { 176 + $type: "app.bsky.unspecced.defs#trendingTopic", 177 + topic: "whey dot party", 178 + displayName: "whey dot party", 179 + description: "whey dot party", 180 + link: "https://whey.party/", 181 + }, 182 + ]; 183 + 184 + const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = 185 + { 186 + topics: faketopics, 187 + suggested: faketopics, 188 + }; 189 + 190 + return new Response(JSON.stringify(response), { 100 191 headers: withCors({ "Content-Type": "application/json" }), 101 - } 102 - ); 103 - //return await sendItToApiBskyApp(req); 104 - } 192 + }); 193 + } 105 194 106 - const authDID = "did:plc:mn45tewwnse5btfftvd3powc"; //getAuthenticatedDid(req); 195 + //if (xrpcMethod !== 'app.bsky.actor.getPreferences' && xrpcMethod !== 'app.bsky.notification.listNotifications') { 196 + if ( 197 + !hasAuth 198 + // (!hasAuth || 199 + // xrpcMethod === "app.bsky.labeler.getServices" || 200 + // xrpcMethod === "app.bsky.unspecced.getConfig") && 201 + // xrpcMethod !== "app.bsky.notification.putPreferences" 202 + ) { 203 + return new Response( 204 + JSON.stringify({ 205 + error: "XRPCNotSupported", 206 + message: 207 + "(no auth) HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 208 + }), 209 + { 210 + status: 404, 211 + headers: withCors({ "Content-Type": "application/json" }), 212 + } 213 + ); 214 + //return await sendItToApiBskyApp(req); 215 + } 216 + if ( 217 + // !hasAuth || 218 + xrpcMethod === "app.bsky.labeler.getServices" || 219 + xrpcMethod === "app.bsky.unspecced.getConfig" //&& 220 + //xrpcMethod !== "app.bsky.notification.putPreferences" 221 + ) { 222 + return new Response( 223 + JSON.stringify({ 224 + error: "XRPCNotSupported", 225 + message: 226 + "(getservices / getconfig) HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 227 + }), 228 + { 229 + status: 404, 230 + headers: withCors({ "Content-Type": "application/json" }), 231 + } 232 + ); 233 + //return await sendItToApiBskyApp(req); 234 + } 107 235 108 - switch (xrpcMethod) { 109 - case "app.bsky.feed.getFeedGenerators": { 110 - const jsonTyped = 111 - jsonUntyped as ViewServerTypes.AppBskyFeedGetFeedGenerators.QueryParams; 236 + const authDID = "did:plc:mn45tewwnse5btfftvd3powc"; //getAuthenticatedDid(req); 237 + 238 + switch (xrpcMethod) { 239 + case "app.bsky.feed.getFeedGenerators": { 240 + const jsonTyped = 241 + jsonUntyped as ViewServerTypes.AppBskyFeedGetFeedGenerators.QueryParams; 112 242 113 - const feeds: ATPAPI.AppBskyFeedDefs.GeneratorView[] = ( 114 - await Promise.all( 115 - jsonTyped.feeds.map(async (feed) => { 116 - try { 117 - const did = new ATPAPI.AtUri(feed).hostname; 118 - const rkey = new ATPAPI.AtUri(feed).rkey; 119 - const identity = await resolveIdentity(did); 120 - const feedgetRecord = await getSlingshotRecord( 121 - identity.did, 122 - "app.bsky.feed.generator", 123 - rkey 124 - ); 125 - const profile = ( 126 - await getSlingshotRecord( 243 + const feeds: ATPAPI.AppBskyFeedDefs.GeneratorView[] = ( 244 + await Promise.all( 245 + jsonTyped.feeds.map(async (feed) => { 246 + try { 247 + const did = new ATPAPI.AtUri(feed).hostname; 248 + const rkey = new ATPAPI.AtUri(feed).rkey; 249 + const identity = await resolveIdentity(did); 250 + const feedgetRecord = await getSlingshotRecord( 127 251 identity.did, 128 - "app.bsky.actor.profile", 129 - "self" 130 - ) 131 - ).value as ATPAPI.AppBskyActorProfile.Record; 132 - const anyprofile = profile as any; 133 - const value = 134 - feedgetRecord.value as ATPAPI.AppBskyFeedGenerator.Record; 252 + "app.bsky.feed.generator", 253 + rkey 254 + ); 255 + const profile = ( 256 + await getSlingshotRecord( 257 + identity.did, 258 + "app.bsky.actor.profile", 259 + "self" 260 + ) 261 + ).value as ATPAPI.AppBskyActorProfile.Record; 262 + const anyprofile = profile as any; 263 + const value = 264 + feedgetRecord.value as ATPAPI.AppBskyFeedGenerator.Record; 135 265 136 - return { 137 - $type: "app.bsky.feed.defs#generatorView", 138 - uri: feed, 139 - cid: feedgetRecord.cid, 140 - did: identity.did, 141 - creator: /*AppBskyActorDefs.ProfileView*/ { 142 - $type: "app.bsky.actor.defs#profileView", 266 + return { 267 + $type: "app.bsky.feed.defs#generatorView", 268 + uri: feed, 269 + cid: feedgetRecord.cid, 143 270 did: identity.did, 144 - handle: identity.handle, 145 - displayName: profile.displayName, 146 - description: profile.description, 271 + creator: /*AppBskyActorDefs.ProfileView*/ { 272 + $type: "app.bsky.actor.defs#profileView", 273 + did: identity.did, 274 + handle: identity.handle, 275 + displayName: profile.displayName, 276 + description: profile.description, 277 + avatar: buildBlobUrl( 278 + identity.pds, 279 + identity.did, 280 + anyprofile.avatar.ref["$link"] 281 + ), 282 + //associated?: ProfileAssociated 283 + //indexedAt?: string 284 + //createdAt?: string 285 + //viewer?: ViewerState 286 + //labels?: ComAtprotoLabelDefs.Label[] 287 + //verification?: VerificationState 288 + //status?: StatusView 289 + }, 290 + displayName: value.displayName, 291 + description: value.description, 292 + //descriptionFacets?: AppBskyRichtextFacet.Main[] 147 293 avatar: buildBlobUrl( 148 294 identity.pds, 149 295 identity.did, 150 - anyprofile.avatar.ref["$link"] 296 + (value as any).avatar.ref["$link"] 151 297 ), 152 - //associated?: ProfileAssociated 153 - //indexedAt?: string 154 - //createdAt?: string 155 - //viewer?: ViewerState 298 + //likeCount?: number 299 + //acceptsInteractions?: boolean 156 300 //labels?: ComAtprotoLabelDefs.Label[] 157 - //verification?: VerificationState 158 - //status?: StatusView 159 - }, 160 - displayName: value.displayName, 161 - description: value.description, 162 - //descriptionFacets?: AppBskyRichtextFacet.Main[] 163 - avatar: buildBlobUrl( 164 - identity.pds, 165 - identity.did, 166 - (value as any).avatar.ref["$link"] 167 - ), 168 - //likeCount?: number 169 - //acceptsInteractions?: boolean 170 - //labels?: ComAtprotoLabelDefs.Label[] 171 - //viewer?: GeneratorViewerState 172 - contentMode: value.contentMode, 173 - indexedAt: new Date().toISOString(), 174 - }; 175 - } catch (err) { 176 - return undefined; 301 + //viewer?: GeneratorViewerState 302 + contentMode: value.contentMode, 303 + indexedAt: new Date().toISOString(), 304 + }; 305 + } catch (err) { 306 + return undefined; 307 + } 308 + }) 309 + ) 310 + ).filter(isGeneratorView); 311 + 312 + const response: ViewServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema = 313 + { 314 + feeds: feeds ? feeds : [], 315 + }; 316 + 317 + return new Response(JSON.stringify(response), { 318 + headers: withCors({ "Content-Type": "application/json" }), 319 + }); 320 + } 321 + case "app.bsky.feed.getFeed": { 322 + const jsonTyped = 323 + jsonUntyped as ViewServerTypes.AppBskyFeedGetFeed.QueryParams; 324 + const cursor = jsonTyped.cursor; 325 + const feed = jsonTyped.feed; 326 + const limit = jsonTyped.limit; 327 + const proxyauth = req.headers.get("authorization") || ""; 328 + 329 + const did = new ATPAPI.AtUri(feed).hostname; 330 + const rkey = new ATPAPI.AtUri(feed).rkey; 331 + const identity = await resolveIdentity(did); 332 + const feedgetRecord = ( 333 + await getSlingshotRecord( 334 + identity.did, 335 + "app.bsky.feed.generator", 336 + rkey 337 + ) 338 + ).value as ATPAPI.AppBskyFeedGenerator.Record; 339 + 340 + const skeleton = (await cachedFetch( 341 + `${didWebToHttps( 342 + feedgetRecord.did 343 + )}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${jsonTyped.feed}${ 344 + cursor ? `&cursor=${cursor}` : "" 345 + }${limit ? `&limit=${limit}` : ""}`, 346 + proxyauth 347 + )) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 348 + 349 + const nextcursor = skeleton.cursor; 350 + const dbgrqstid = skeleton.reqId; 351 + const uriarray = skeleton.feed; 352 + 353 + // Step 1: Chunk into 25 max 354 + const chunks = []; 355 + for (let i = 0; i < uriarray.length; i += 25) { 356 + chunks.push(uriarray.slice(i, i + 25)); 357 + } 358 + 359 + // Step 2: Hydrate via getPosts 360 + const hydratedPosts: ATPAPI.AppBskyFeedDefs.FeedViewPost[] = []; 361 + 362 + for (const chunk of chunks) { 363 + const searchParams = new URLSearchParams(); 364 + for (const uri of chunk.map((item) => item.post)) { 365 + searchParams.append("uris", uri); 366 + } 367 + 368 + const postResp = await ky 369 + // TODO aaaaaa dont do this please use the new getServiceEndpointFromIdentity() 370 + .get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, { 371 + // headers: { 372 + // Authorization: proxyauth, 373 + // }, 374 + searchParams, 375 + }) 376 + .json<ATPAPI.AppBskyFeedGetPosts.OutputSchema>(); 377 + 378 + for (const post of postResp.posts) { 379 + const matchingSkeleton = uriarray.find( 380 + (item) => item.post === post.uri 381 + ); 382 + if (matchingSkeleton) { 383 + //post.author.handle = post.author.handle + ".percent40.api.bsky.app"; // or any logic to modify it 384 + hydratedPosts.push({ 385 + post, 386 + reason: matchingSkeleton.reason, 387 + //reply: matchingSkeleton, 388 + }); 177 389 } 178 - }) 179 - ) 180 - ).filter(isGeneratorView); 390 + } 391 + } 181 392 182 - const response: ViewServerTypes.AppBskyFeedGetFeedGenerators.OutputSchema = 183 - { 184 - feeds: feeds ? feeds : [], 393 + // Step 3: Compose final response 394 + const response: ViewServerTypes.AppBskyFeedGetFeed.OutputSchema = { 395 + feed: hydratedPosts, 396 + cursor: nextcursor, 185 397 }; 186 398 187 - return new Response(JSON.stringify(response), { 188 - headers: withCors({ "Content-Type": "application/json" }), 189 - }); 190 - } 191 - case "app.bsky.feed.getFeed": { 192 - const jsonTyped = 193 - jsonUntyped as ViewServerTypes.AppBskyFeedGetFeed.QueryParams; 194 - const cursor = jsonTyped.cursor; 195 - const feed = jsonTyped.feed; 196 - const limit = jsonTyped.limit; 197 - const proxyauth = req.headers.get("authorization") || ""; 399 + return new Response(JSON.stringify(response), { 400 + headers: withCors({ "Content-Type": "application/json" }), 401 + }); 402 + } 403 + case "app.bsky.actor.getProfile": { 404 + const jsonTyped = 405 + jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; 198 406 199 - const did = new ATPAPI.AtUri(feed).hostname; 200 - const rkey = new ATPAPI.AtUri(feed).rkey; 201 - const identity = await resolveIdentity(did); 202 - const feedgetRecord = ( 203 - await getSlingshotRecord(identity.did, "app.bsky.feed.generator", rkey) 204 - ).value as ATPAPI.AppBskyFeedGenerator.Record; 407 + const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = 408 + ((await this.resolveGetProfiles([jsonTyped.actor])) ?? [])[0]; 205 409 206 - const skeleton = (await cachedFetch( 207 - `${didWebToHttps( 208 - feedgetRecord.did 209 - )}/xrpc/app.bsky.feed.getFeedSkeleton?feed=${jsonTyped.feed}${ 210 - cursor ? `&cursor=${cursor}` : "" 211 - }${limit ? `&limit=${limit}` : ""}`, 212 - proxyauth 213 - )) as ATPAPI.AppBskyFeedGetFeedSkeleton.OutputSchema; 410 + return new Response(JSON.stringify(response), { 411 + headers: withCors({ "Content-Type": "application/json" }), 412 + }); 413 + } 214 414 215 - const nextcursor = skeleton.cursor; 216 - const dbgrqstid = skeleton.reqId; 217 - const uriarray = skeleton.feed; 415 + case "app.bsky.actor.getProfiles": { 416 + const jsonhalfTyped = 417 + jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; 418 + const actors = jsonhalfTyped.actors as string[] | string 419 + const queryactors = Array.isArray(actors) 420 + ? actors 421 + : [actors]; 422 + //console.log("queryactors:",jsonTyped.actors) 423 + const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = { 424 + profiles: (await this.resolveGetProfiles(queryactors)) ?? [], 425 + }; 218 426 219 - // Step 1: Chunk into 25 max 220 - const chunks = []; 221 - for (let i = 0; i < uriarray.length; i += 25) { 222 - chunks.push(uriarray.slice(i, i + 25)); 427 + return new Response(JSON.stringify(response), { 428 + headers: withCors({ "Content-Type": "application/json" }), 429 + }); 223 430 } 431 + case "app.bsky.feed.getAuthorFeed": { 432 + const jsonTyped = 433 + jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; 224 434 225 - // Step 2: Hydrate via getPosts 226 - const hydratedPosts: ATPAPI.AppBskyFeedDefs.FeedViewPost[] = []; 435 + const userindexservice = ""; 436 + const isbskyfallback = true; 437 + if (isbskyfallback) { 438 + return this.sendItToApiBskyApp(req); 439 + } 227 440 228 - for (const chunk of chunks) { 229 - const searchParams = new URLSearchParams(); 230 - for (const uri of chunk.map((item) => item.post)) { 231 - searchParams.append("uris", uri); 441 + const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = 442 + {}; 443 + 444 + return new Response(JSON.stringify(response), { 445 + headers: withCors({ "Content-Type": "application/json" }), 446 + }); 447 + } 448 + case "app.bsky.feed.getPostThread": { 449 + const jsonTyped = 450 + jsonUntyped as ViewServerTypes.AppBskyFeedGetPostThread.QueryParams; 451 + 452 + const userindexservice = ""; 453 + const isbskyfallback = true; 454 + if (isbskyfallback) { 455 + return this.sendItToApiBskyApp(req); 232 456 } 233 457 234 - const postResp = await ky 235 - .get(`https://api.bsky.app/xrpc/app.bsky.feed.getPosts`, { 236 - // headers: { 237 - // Authorization: proxyauth, 238 - // }, 239 - searchParams, 240 - }) 241 - .json<ATPAPI.AppBskyFeedGetPosts.OutputSchema>(); 458 + const response: ViewServerTypes.AppBskyFeedGetPostThread.OutputSchema = 459 + {}; 242 460 243 - for (const post of postResp.posts) { 244 - const matchingSkeleton = uriarray.find( 245 - (item) => item.post === post.uri 246 - ); 247 - if (matchingSkeleton) { 248 - //post.author.handle = post.author.handle + ".percent40.api.bsky.app"; // or any logic to modify it 249 - hydratedPosts.push({ 250 - post, 251 - reason: matchingSkeleton.reason, 252 - //reply: matchingSkeleton, 253 - }); 254 - } 461 + return new Response(JSON.stringify(response), { 462 + headers: withCors({ "Content-Type": "application/json" }), 463 + }); 464 + } 465 + case "app.bsky.unspecced.getPostThreadV2": { 466 + const jsonTyped = 467 + jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.QueryParams; 468 + 469 + const userindexservice = ""; 470 + const isbskyfallback = true; 471 + if (isbskyfallback) { 472 + return this.sendItToApiBskyApp(req); 255 473 } 474 + 475 + const response: ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.OutputSchema = 476 + {}; 477 + 478 + return new Response(JSON.stringify(response), { 479 + headers: withCors({ "Content-Type": "application/json" }), 480 + }); 256 481 } 257 482 258 - // Step 3: Compose final response 259 - const response: ViewServerTypes.AppBskyFeedGetFeed.OutputSchema = { 260 - feed: hydratedPosts, 261 - cursor: nextcursor, 262 - }; 483 + // case "app.bsky.actor.getProfile": { 484 + // const jsonTyped = 485 + // jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; 263 486 264 - return new Response(JSON.stringify(response), { 265 - headers: withCors({ "Content-Type": "application/json" }), 266 - }); 267 - } 268 - case "app.bsky.actor.getProfile": { 269 - const jsonTyped = 270 - jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; 487 + // const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema= {}; 271 488 272 - const userindexservice = ""; 273 - const isbskyfallback = true; 274 - if (isbskyfallback) { 275 - return sendItToApiBskyApp(req); 489 + // return new Response(JSON.stringify(response), { 490 + // headers: withCors({ "Content-Type": "application/json" }), 491 + // }); 492 + // } 493 + // case "app.bsky.actor.getProfiles": { 494 + // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; 495 + 496 + // const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {}; 497 + 498 + // return new Response(JSON.stringify(response), { 499 + // headers: withCors({ "Content-Type": "application/json" }), 500 + // }); 501 + // } 502 + // case "whatever": { 503 + // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; 504 + 505 + // const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = {} 506 + 507 + // return new Response(JSON.stringify(response), { 508 + // headers: withCors({ "Content-Type": "application/json" }), 509 + // }); 510 + // } 511 + case "app.bsky.notification.listNotifications": { 512 + if (!authdid) return new Response("Not Found", { status: 404 }); 513 + const jsonTyped = 514 + jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams; 515 + 516 + const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = 517 + await this.queryNotificationsList(authdid, jsonTyped.cursor, jsonTyped.limit); 518 + 519 + return new Response(JSON.stringify(response), { 520 + headers: withCors({ "Content-Type": "application/json" }), 521 + }); 276 522 } 523 + case "app.bsky.feed.getPosts": { 524 + const jsonTyped = 525 + jsonUntyped as ViewServerTypes.AppBskyFeedGetPosts.QueryParams; 526 + const inputUris = Array.isArray(jsonTyped.uris) 527 + ? jsonTyped.uris 528 + : [jsonTyped.uris]; 529 + const response: ViewServerTypes.AppBskyFeedGetPosts.OutputSchema = { 530 + posts: await this.resolveGetPosts(inputUris), 531 + }; 532 + 533 + return new Response(JSON.stringify(response), { 534 + headers: withCors({ "Content-Type": "application/json" }), 535 + }); 536 + } 537 + 538 + case "app.bsky.unspecced.getConfig": { 539 + const jsonTyped = 540 + jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetConfig.QueryParams; 277 541 278 - const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = {}; 542 + const response: ViewServerTypes.AppBskyUnspeccedGetConfig.OutputSchema = 543 + { 544 + checkEmailConfirmed: true, 545 + liveNow: [ 546 + { 547 + $type: "app.bsky.unspecced.getConfig#liveNowConfig", 548 + did: "did:plc:mn45tewwnse5btfftvd3powc", 549 + domains: ["local3768forumtest.whey.party"], 550 + }, 551 + ], 552 + }; 279 553 280 - return new Response(JSON.stringify(response), { 281 - headers: withCors({ "Content-Type": "application/json" }), 282 - }); 283 - } 554 + return new Response(JSON.stringify(response), { 555 + headers: withCors({ "Content-Type": "application/json" }), 556 + }); 557 + } 558 + case "app.bsky.graph.getLists": { 559 + const jsonTyped = 560 + jsonUntyped as ViewServerTypes.AppBskyGraphGetLists.QueryParams; 284 561 285 - case "app.bsky.actor.getProfiles": { 286 - const jsonTyped = 287 - jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; 562 + const response: ViewServerTypes.AppBskyGraphGetLists.OutputSchema = { 563 + lists: [], 564 + }; 288 565 289 - const userindexservice = ""; 290 - const isbskyfallback = true; 291 - if (isbskyfallback) { 292 - return sendItToApiBskyApp(req); 566 + return new Response(JSON.stringify(response), { 567 + headers: withCors({ "Content-Type": "application/json" }), 568 + }); 293 569 } 570 + //https://shimeji.us-east.host.bsky.network/xrpc/app.bsky.unspecced.getTrendingTopics?limit=14 571 + case "app.bsky.unspecced.getTrendingTopics": { 572 + const jsonTyped = 573 + jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; 294 574 295 - const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {}; 575 + const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ 576 + { 577 + $type: "app.bsky.unspecced.defs#trendingTopic", 578 + topic: "Git Repo", 579 + displayName: "Git Repo", 580 + description: "Git Repo", 581 + link: "https://tangled.sh/@whey.party/skylite", 582 + }, 583 + { 584 + $type: "app.bsky.unspecced.defs#trendingTopic", 585 + topic: "Red Dwarf Lite", 586 + displayName: "Red Dwarf Lite", 587 + description: "Red Dwarf Lite", 588 + link: "https://reddwarf.whey.party/", 589 + }, 590 + { 591 + $type: "app.bsky.unspecced.defs#trendingTopic", 592 + topic: "whey dot party", 593 + displayName: "whey dot party", 594 + description: "whey dot party", 595 + link: "https://whey.party/", 596 + }, 597 + ]; 296 598 297 - return new Response(JSON.stringify(response), { 298 - headers: withCors({ "Content-Type": "application/json" }), 299 - }); 300 - } 301 - case "app.bsky.feed.getAuthorFeed": { 302 - const jsonTyped = 303 - jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; 599 + const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = 600 + { 601 + topics: faketopics, 602 + suggested: faketopics, 603 + }; 304 604 305 - const userindexservice = ""; 306 - const isbskyfallback = true; 307 - if (isbskyfallback) { 308 - return sendItToApiBskyApp(req); 605 + return new Response(JSON.stringify(response), { 606 + headers: withCors({ "Content-Type": "application/json" }), 607 + }); 608 + } 609 + default: { 610 + return new Response( 611 + JSON.stringify({ 612 + error: "XRPCNotSupported", 613 + message: 614 + "(default) HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 615 + }), 616 + { 617 + status: 404, 618 + headers: withCors({ "Content-Type": "application/json" }), 619 + } 620 + ); 309 621 } 622 + } 310 623 311 - const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = 312 - {}; 624 + // return new Response("Not Found", { status: 404 }); 625 + } 313 626 314 - return new Response(JSON.stringify(response), { 315 - headers: withCors({ "Content-Type": "application/json" }), 316 - }); 627 + async sendItToApiBskyApp(req: Request): Promise<Response> { 628 + const url = new URL(req.url); 629 + const pathname = url.pathname; 630 + const searchParams = searchParamsToJson(url.searchParams); 631 + let reqBody: undefined | string; 632 + let jsonbody: undefined | Record<string, unknown>; 633 + if (req.body) { 634 + const body = await req.json(); 635 + jsonbody = body; 636 + // console.log( 637 + // `called at euh reqreqreqreq: ${pathname}\n\n${JSON.stringify(body)}` 638 + // ); 639 + reqBody = JSON.stringify(body, null, 2); 317 640 } 318 - case "app.bsky.feed.getPostThread": { 319 - const jsonTyped = 320 - jsonUntyped as ViewServerTypes.AppBskyFeedGetPostThread.QueryParams; 641 + const bskyUrl = `https://public.api.bsky.app${pathname}${url.search}`; 642 + console.log("request", searchParams); 643 + const proxyHeaders = new Headers(req.headers); 644 + 645 + // Remove Authorization and set browser-like User-Agent 646 + proxyHeaders.delete("authorization"); 647 + proxyHeaders.delete("Access-Control-Allow-Origin"), 648 + proxyHeaders.set( 649 + "user-agent", 650 + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" 651 + ); 652 + proxyHeaders.set("Access-Control-Allow-Origin", "*"); 653 + 654 + const proxyRes = await fetch(bskyUrl, { 655 + method: req.method, 656 + headers: proxyHeaders, 657 + body: ["GET", "HEAD"].includes(req.method.toUpperCase()) 658 + ? undefined 659 + : reqBody, 660 + }); 661 + 662 + const resBody = await proxyRes.text(); 663 + 664 + // console.log( 665 + // "โ† Response:", 666 + // JSON.stringify(await JSON.parse(resBody), null, 2) 667 + // ); 321 668 322 - const userindexservice = ""; 323 - const isbskyfallback = true; 324 - if (isbskyfallback) { 325 - return sendItToApiBskyApp(req); 669 + return new Response(resBody, { 670 + status: proxyRes.status, 671 + headers: proxyRes.headers, 672 + }); 673 + } 674 + 675 + viewServerIndexer(ctx: indexHandlerContext) { 676 + const record = validateRecord(ctx.value); 677 + switch (record?.$type) { 678 + case "app.bsky.feed.like": { 679 + return; 326 680 } 681 + default: { 682 + // what the hell 683 + return; 684 + } 685 + } 686 + } 327 687 328 - const response: ViewServerTypes.AppBskyFeedGetPostThread.OutputSchema = 329 - {}; 688 + /** 689 + * please do not use this, use openDbForDid() instead 690 + * @param did 691 + * @returns 692 + */ 693 + internalCreateDbForDid(did: string): Database { 694 + const path = `${this.config.baseDbPath}/${did}.sqlite`; 695 + const db = new Database(path); 696 + // TODO maybe split the user db schema between view server and index server 697 + setupUserDb(db); 698 + //await db.exec(/* CREATE IF NOT EXISTS statements */); 699 + return db; 700 + } 701 + public handlesDid(did: string): boolean { 702 + return this.userManager.handlesDid(did); 703 + } 704 + 705 + async resolveGetPosts( 706 + uris: string[] 707 + ): Promise<ATPAPI.AppBskyFeedDefs.PostView[]> { 708 + const grouped: Record<string, string[]> = {}; 709 + 710 + // Group URIs by resolved endpoint 711 + for (const uri of uris) { 712 + const did = new AtUri(uri).host; 713 + const endpoint = await getSkyliteEndpoint(did); 714 + if (!endpoint) continue; 330 715 331 - return new Response(JSON.stringify(response), { 332 - headers: withCors({ "Content-Type": "application/json" }), 333 - }); 716 + if (!grouped[endpoint]) { 717 + grouped[endpoint] = []; 718 + } 719 + grouped[endpoint].push(uri); 334 720 } 335 - case "app.bsky.unspecced.getPostThreadV2": { 336 - const jsonTyped = 337 - jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.QueryParams; 721 + 722 + const postviews: ATPAPI.AppBskyFeedDefs.PostView[] = []; 338 723 339 - const userindexservice = ""; 340 - const isbskyfallback = true; 341 - if (isbskyfallback) { 342 - return sendItToApiBskyApp(req); 724 + // Fetch posts per endpoint 725 + for (const [endpoint, urisForEndpoint] of Object.entries(grouped)) { 726 + const query = urisForEndpoint 727 + .map((u) => `uris=${encodeURIComponent(u)}`) 728 + .join("&"); 729 + 730 + const url = `${endpoint}/xrpc/app.bsky.feed.getPosts?${query}`; 731 + const resp = await fetch(url); 732 + if (!resp.ok) { 733 + throw new Error( 734 + `Failed to fetch posts from ${endpoint} for uris=${urisForEndpoint.join( 735 + "," 736 + )}` 737 + ); 343 738 } 344 739 345 - const response: ViewServerTypes.AppBskyUnspeccedGetPostThreadV2.OutputSchema = 346 - {}; 740 + const raw = 741 + (await resp.json()) as ATPAPI.AppBskyFeedGetPosts.OutputSchema; 742 + postviews.push(...raw.posts); 743 + } 744 + 745 + return postviews; 746 + } 747 + 748 + async resolveGetProfiles( 749 + dids: string[] 750 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] | undefined> { 751 + const profiles: ATPAPI.AppBskyActorDefs.ProfileViewDetailed[] = []; 752 + 753 + for (const did of dids) { 754 + const endpoint = await getSkyliteEndpoint(did); 755 + const url = `${endpoint}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent( 756 + did 757 + )}`; 758 + const resp = await fetch(url); 759 + if (!resp.ok) 760 + throw new Error(`Failed to fetch profile for ${did} via ${url}`); 347 761 348 - return new Response(JSON.stringify(response), { 349 - headers: withCors({ "Content-Type": "application/json" }), 350 - }); 762 + const raw = 763 + (await resp.json()) as ATPAPI.AppBskyActorGetProfile.OutputSchema; 764 + profiles.push(raw); 351 765 } 352 766 353 - // case "app.bsky.actor.getProfile": { 354 - // const jsonTyped = 355 - // jsonUntyped as ViewServerTypes.AppBskyActorGetProfile.QueryParams; 767 + return profiles; 768 + } 769 + async queryNotificationsList( 770 + did: string, 771 + cursor?: string, 772 + limit?: number 773 + ): Promise<ATPAPI.AppBskyNotificationListNotifications.OutputSchema> { 774 + if (!this.handlesDid(did)) { 775 + return { notifications: [] }; 776 + } 777 + const db = this.userManager.getDbForDid(did); 778 + if (!db) { 779 + return { notifications: [] }; 780 + } 356 781 357 - // const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema= {}; 782 + const NOTIFS_LIMIT = limit ?? 30; 783 + const offset = cursor ? parseInt(cursor, 10) : 0; 358 784 359 - // return new Response(JSON.stringify(response), { 360 - // headers: withCors({ "Content-Type": "application/json" }), 361 - // }); 362 - // } 363 - // case "app.bsky.actor.getProfiles": { 364 - // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyActorGetProfiles.QueryParams; 785 + const mapReason = ( 786 + field: string 787 + ): 788 + | ATPAPI.AppBskyNotificationListNotifications.Notification["reason"] 789 + | undefined => { 790 + switch (field) { 791 + //'like' | 'repost' | 'follow' | 'mention' | 'reply' | 'quote' | 'starterpack-joined' | 'verified' | 'unverified' | 'like-via-repost' | 'repost-via-repost' | 792 + case "app.bsky.feed.like:subject.uri": 793 + return "like"; 794 + case "app.bsky.feed.like:via.uri": 795 + return "liked-via-repost"; 796 + case "app.bsky.feed.repost:subject.uri": 797 + return "repost"; 798 + case "app.bsky.feed.repost:via.uri": 799 + return "repost-via-repost"; 800 + case "app.bsky.feed.post:reply.root.uri": 801 + return "reply"; 802 + case "app.bsky.feed.post:reply.parent.uri": 803 + return "reply"; 804 + case "app.bsky.feed.post:embed.media.record.record.uri": 805 + return "quote"; 806 + case "app.bsky.feed.post:embed.record.uri": 807 + return "quote"; 808 + //case"app.bsky.feed.threadgate:post": return "threadgate subject 809 + //case"app.bsky.feed.threadgate:hiddenReplies": return "threadgate items (array) 810 + case "app.bsky.feed.post:facets.features.did": 811 + return "mention"; 812 + //case"app.bsky.graph.block:subject": return "blocks 813 + case "app.bsky.graph.follow:subject": 814 + return "follow"; 815 + //case"app.bsky.graph.listblock:subject": return "list item (blocks) 816 + //case"app.bsky.graph.listblock:list": return "blocklist mention (might not exist) 817 + //case"app.bsky.graph.listitem:subject": return "list item (blocks) 818 + //"app.bsky.graph.listitem:list": return "list mention 819 + // case "like": return "like"; 820 + // case "repost": return "repost"; 821 + // case "follow": return "follow"; 822 + // case "replyparent": return "reply"; 823 + // case "replyroot": return "reply"; 824 + // case "mention": return "mention"; 825 + default: 826 + return undefined; 827 + } 828 + }; 365 829 366 - // const response: ViewServerTypes.AppBskyActorGetProfiles.OutputSchema = {}; 830 + // --- Build Query --- 831 + let query = ` 832 + SELECT srcuri, suburi, srcfield, indexedAt 833 + FROM backlink_skeleton 834 + WHERE 835 + -- Find actions targeting the user's content or profile 836 + (suburi LIKE ? OR suburi = ?) 837 + -- Exclude notifications from the user themselves 838 + AND srcuri NOT LIKE ? 839 + ORDER BY indexedAt DESC, srcuri DESC 840 + LIMIT ? OFFSET ? 841 + `; 842 + const params: (string | number)[] = [ 843 + `at://${did}/%`, 844 + did, 845 + `at://${did}/%`, 846 + NOTIFS_LIMIT, 847 + offset 848 + ]; 367 849 368 - // return new Response(JSON.stringify(response), { 369 - // headers: withCors({ "Content-Type": "application/json" }), 370 - // }); 850 + // if (cursor) { 851 + // const [indexedAt, srcuri] = cursor.split("::"); 852 + // if (indexedAt && srcuri && !Number.isNaN(+indexedAt)) { 853 + // query += ` AND (indexedAt < ? OR (indexedAt = ? AND srcuri < ?))`; 854 + // params.push(+indexedAt, +indexedAt, srcuri); 855 + // } 371 856 // } 372 - // case "whatever": { 373 - // const jsonTyped = jsonUntyped as ViewServerTypes.AppBskyFeedGetAuthorFeed.QueryParams; 374 857 375 - // const response: ViewServerTypes.AppBskyFeedGetAuthorFeed.OutputSchema = {} 858 + // query += ` ORDER BY indexedAt DESC, srcuri DESC LIMIT ${NOTIFS_LIMIT}`; 376 859 377 - // return new Response(JSON.stringify(response), { 378 - // headers: withCors({ "Content-Type": "application/json" }), 379 - // }); 380 - // } 381 - // case "app.bsky.notification.listNotifications": { 382 - // const jsonTyped = 383 - // jsonUntyped as ViewServerTypes.AppBskyNotificationListNotifications.QueryParams; 860 + // --- Fetch and Process --- 861 + const stmt = db.prepare(query); 862 + const rows = stmt.all(...params) as { 863 + srcuri: string; 864 + suburi: string; // might be uri, might be just a did 865 + srcfield: string; 866 + indexedAt: number; 867 + }[]; 384 868 385 - // const response: ViewServerTypes.AppBskyNotificationListNotifications.OutputSchema = {}; 869 + const notificationPromises = rows.map(async (row) => { 870 + try { 871 + const reason = mapReason(row.srcfield); 872 + // i have a hunch that follow notifs are crashing the client 873 + if (!reason || temporarydevelopmentblockednotiftypes.includes(reason)) { 874 + return null; 875 + } 876 + // Skip if it's a backlink type we don't have a notification for 877 + if (!reason) return null; 386 878 387 - // return new Response(JSON.stringify(response), { 388 - // headers: withCors({ "Content-Type": "application/json" }), 389 - // }); 390 - // } 879 + const srcURI = new AtUri(row.srcuri); 880 + const [authorRes, recordRes] = await Promise.allSettled([ 881 + this.resolveProfileView(srcURI.host, ""), 882 + getSlingshotRecord(srcURI.host, srcURI.collection, srcURI.rkey), 883 + ]); 391 884 392 - case "app.bsky.unspecced.getConfig": { 393 - const jsonTyped = 394 - jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetConfig.QueryParams; 885 + const author = authorRes.status === "fulfilled" ? authorRes.value : null; 886 + const getrecord = recordRes.status === "fulfilled" ? recordRes.value : null; 395 887 396 - const response: ViewServerTypes.AppBskyUnspeccedGetConfig.OutputSchema = { 397 - checkEmailConfirmed: true, 398 - liveNow: [ 399 - { 400 - $type: "app.bsky.unspecced.getConfig#liveNowConfig", 401 - did: "did:plc:mn45tewwnse5btfftvd3powc", 402 - domains: ["local3768forumtest.whey.party"], 403 - }, 404 - ], 405 - }; 888 + const reasonsubject = 889 + row.suburi.startsWith("at://") ? row.suburi : `at://${row.suburi}`; 890 + // If we can't resolve the author or the record, we can't form a valid notification 891 + if (!author || !getrecord || !reason || !reasonsubject) return null; 406 892 407 - return new Response(JSON.stringify(response), { 408 - headers: withCors({ "Content-Type": "application/json" }), 409 - }); 410 - } 411 - case "app.bsky.graph.getLists": { 412 - const jsonTyped = 413 - jsonUntyped as ViewServerTypes.AppBskyGraphGetLists.QueryParams; 893 + author.viewer = { 894 + "muted": false, 895 + "blockedBy": false, 896 + //"following": 897 + } // TODO: proper mutes and blocks here 414 898 415 - const response: ViewServerTypes.AppBskyGraphGetLists.OutputSchema = { 416 - lists: [], 417 - }; 899 + if (!getrecord?.value?.$type) getrecord.value.$type = srcURI.collection 900 + return { 901 + uri: row.srcuri, 902 + cid: getrecord.cid, 903 + author: author, 904 + reason: reason, 905 + // The reasonSubject is the URI of the post that was liked, reposted, or replied to 906 + reasonSubject: reasonsubject, 907 + record: getrecord.value, 908 + isRead: false, // Placeholder for read-state logic 909 + indexedAt: new Date(row.indexedAt).toISOString(), 910 + labels: [], // Placeholder for label logic 911 + } as ATPAPI.AppBskyNotificationListNotifications.Notification; 912 + } catch (e) {console.log("error:",e)} 913 + }); 418 914 419 - return new Response(JSON.stringify(response), { 420 - headers: withCors({ "Content-Type": "application/json" }), 915 + const seen = new Set<string>(); 916 + const notifications = (await Promise.all(notificationPromises)) 917 + .filter((n): n is ATPAPI.AppBskyNotificationListNotifications.Notification => { 918 + if (!n) return false; 919 + const key = `${n.uri}:${n.reason}:${n.reasonSubject}`; 920 + if (seen.has(key)) return false; 921 + seen.add(key); 922 + return true; 421 923 }); 422 - } 423 - //https://shimeji.us-east.host.bsky.network/xrpc/app.bsky.unspecced.getTrendingTopics?limit=14 424 - case "app.bsky.unspecced.getTrendingTopics": { 425 - const jsonTyped = 426 - jsonUntyped as ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.QueryParams; 427 924 428 - const faketopics: ATPAPI.AppBskyUnspeccedDefs.TrendingTopic[] = [ 429 - { 430 - $type: "app.bsky.unspecced.defs#trendingTopic", 431 - topic: "Git Repo", 432 - displayName: "Git Repo", 433 - description: "Git Repo", 434 - link: "https://tangled.sh/@whey.party/skylite", 435 - }, 436 - { 437 - $type: "app.bsky.unspecced.defs#trendingTopic", 438 - topic: "Red Dwarf Lite", 439 - displayName: "Red Dwarf Lite", 440 - description: "Red Dwarf Lite", 441 - link: "https://reddwarf.whey.party/", 442 - }, 443 - { 444 - $type: "app.bsky.unspecced.defs#trendingTopic", 445 - topic: "whey dot party", 446 - displayName: "whey dot party", 447 - description: "whey dot party", 448 - link: "https://whey.party/", 449 - }, 450 - ]; 925 + // --- Create next cursor --- 926 + const nextCursor:number = Number(offset) + Number(limit ?? 0) 927 + // const lastItem = rows[rows.length - 1]; 928 + // const nextCursor = lastItem 929 + // ? `${lastItem.indexedAt}::${lastItem.srcuri}` 930 + // : undefined; 451 931 452 - const response: ViewServerTypes.AppBskyUnspeccedGetTrendingTopics.OutputSchema = 453 - { 454 - topics: faketopics, 455 - suggested: faketopics, 456 - }; 932 + return { 933 + cursor: `${nextCursor}`,//nextCursor, 934 + notifications: notifications, 935 + priority:false, 936 + seenAt: new Date().toISOString() 937 + }; 938 + } 939 + async resolveProfileView( 940 + did: string, 941 + type: "" 942 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileView | undefined>; 943 + async resolveProfileView( 944 + did: string, 945 + type: "Basic" 946 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewBasic | undefined>; 947 + async resolveProfileView( 948 + did: string, 949 + type: "Detailed" 950 + ): Promise<ATPAPI.AppBskyActorDefs.ProfileViewDetailed | undefined>; 951 + async resolveProfileView( 952 + did: string, 953 + type: "" | "Basic" | "Detailed" 954 + ): Promise< 955 + | ATPAPI.AppBskyActorDefs.ProfileView 956 + | ATPAPI.AppBskyActorDefs.ProfileViewBasic 957 + | ATPAPI.AppBskyActorDefs.ProfileViewDetailed 958 + | undefined 959 + > { 960 + const record = ( 961 + await getSlingshotRecord(did, "app.bsky.actor.profile", "self") 962 + ).value as ATPAPI.AppBskyActorProfile.Record; 457 963 458 - return new Response(JSON.stringify(response), { 459 - headers: withCors({ "Content-Type": "application/json" }), 460 - }); 964 + const identity = await resolveIdentity(did); 965 + const avatarcid = uncid(record.avatar?.ref); 966 + const avatar = avatarcid 967 + ? buildBlobUrl(identity.pds, identity.did, avatarcid) 968 + : undefined; 969 + const bannercid = uncid(record.banner?.ref); 970 + const banner = bannercid 971 + ? buildBlobUrl(identity.pds, identity.did, bannercid) 972 + : undefined; 973 + // simulate different types returned 974 + switch (type) { 975 + case "": { 976 + const result: ATPAPI.AppBskyActorDefs.ProfileView = { 977 + $type: "app.bsky.actor.defs#profileView", 978 + did: did, 979 + handle: identity.handle, 980 + displayName: record.displayName ?? identity.handle, 981 + description: record.description ?? undefined, 982 + avatar: avatar, // create profile URL from resolved identity 983 + //associated?: ProfileAssociated, 984 + indexedAt: record.createdAt 985 + ? new Date(record.createdAt).toISOString() 986 + : undefined, 987 + createdAt: record.createdAt 988 + ? new Date(record.createdAt).toISOString() 989 + : undefined, 990 + //viewer?: ViewerState, 991 + //labels?: ComAtprotoLabelDefs.Label[], 992 + //verification?: VerificationState, 993 + //status?: StatusView, 994 + }; 995 + return result; 996 + } 997 + case "Basic": { 998 + const result: ATPAPI.AppBskyActorDefs.ProfileViewBasic = { 999 + $type: "app.bsky.actor.defs#profileViewBasic", 1000 + did: did, 1001 + handle: identity.handle, 1002 + displayName: record.displayName ?? identity.handle, 1003 + avatar: avatar, // create profile URL from resolved identity 1004 + //associated?: ProfileAssociated, 1005 + createdAt: record.createdAt 1006 + ? new Date(record.createdAt).toISOString() 1007 + : undefined, 1008 + //viewer?: ViewerState, 1009 + //labels?: ComAtprotoLabelDefs.Label[], 1010 + //verification?: VerificationState, 1011 + //status?: StatusView, 1012 + }; 1013 + return result; 1014 + } 1015 + case "Detailed": { 1016 + const response: ViewServerTypes.AppBskyActorGetProfile.OutputSchema = 1017 + ((await this.resolveGetProfiles([did])) ?? [])[0]; 1018 + return response; 1019 + } 1020 + default: 1021 + throw new Error("Invalid type"); 461 1022 } 462 - default: { 463 - return new Response( 464 - JSON.stringify({ 465 - error: "XRPCNotSupported", 466 - message: 467 - "HEY hello there my name is whey dot party and you have used my custom appview that is very cool but have you considered that XRPC Not Supported", 468 - }), 469 - { 470 - status: 404, 471 - headers: withCors({ "Content-Type": "application/json" }), 472 - } 473 - ); 1023 + } 1024 + } 1025 + 1026 + export class ViewServerUserManager { 1027 + public viewServer: ViewServer; 1028 + 1029 + constructor(viewServer: ViewServer) { 1030 + this.viewServer = viewServer; 1031 + } 1032 + 1033 + public users = new Map<string, UserViewServer>(); 1034 + public handlesDid(did: string): boolean { 1035 + return this.users.has(did); 1036 + } 1037 + 1038 + /*async*/ addUser(did: string) { 1039 + if (this.users.has(did)) return; 1040 + const instance = new UserViewServer(this, did); 1041 + //await instance.initialize(); 1042 + this.users.set(did, instance); 1043 + } 1044 + 1045 + // async handleRequest({ 1046 + // did, 1047 + // route, 1048 + // req, 1049 + // }: { 1050 + // did: string; 1051 + // route: string; 1052 + // req: Request; 1053 + // }) { 1054 + // if (!this.users.has(did)) await this.addUser(did); 1055 + // const user = this.users.get(did)!; 1056 + // return await user.handleHttpRequest(route, req); 1057 + // } 1058 + 1059 + removeUser(did: string) { 1060 + const instance = this.users.get(did); 1061 + if (!instance) return; 1062 + /*await*/ instance.shutdown(); 1063 + this.users.delete(did); 1064 + } 1065 + 1066 + getDbForDid(did: string): Database | null { 1067 + if (!this.users.has(did)) { 1068 + return null; 474 1069 } 1070 + return this.users.get(did)?.db ?? null; 475 1071 } 476 1072 477 - return new Response("Not Found", { status: 404 }); 1073 + coldStart(db: Database) { 1074 + const rows = db.prepare("SELECT did FROM users").all(); 1075 + for (const row of rows) { 1076 + this.addUser(row.did); 1077 + } 1078 + } 478 1079 } 479 1080 480 - async function sendItToApiBskyApp(req: Request): Promise<Response> { 481 - const url = new URL(req.url); 482 - const pathname = url.pathname; 483 - const searchParams = searchParamsToJson(url.searchParams); 484 - let reqBody: undefined | string; 485 - let jsonbody: undefined | Record<string, unknown>; 486 - if (req.body) { 487 - const body = await req.json(); 488 - jsonbody = body; 489 - // console.log( 490 - // `called at euh reqreqreqreq: ${pathname}\n\n${JSON.stringify(body)}` 491 - // ); 492 - reqBody = JSON.stringify(body, null, 2); 493 - } 494 - const bskyUrl = `https://api.bsky.app${pathname}${url.search}`; 495 - const proxyHeaders = new Headers(req.headers); 1081 + class UserViewServer { 1082 + public viewServerUserManager: ViewServerUserManager; 1083 + did: string; 1084 + db: Database; // | undefined; 1085 + jetstream: JetstreamManager; // | undefined; 1086 + spacedust: SpacedustManager; // | undefined; 496 1087 497 - // Remove Authorization and set browser-like User-Agent 498 - proxyHeaders.delete("authorization"); 499 - proxyHeaders.delete("Access-Control-Allow-Origin"), 500 - proxyHeaders.set( 501 - "user-agent", 502 - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" 1088 + constructor(viewServerUserManager: ViewServerUserManager, did: string) { 1089 + this.did = did; 1090 + this.viewServerUserManager = viewServerUserManager; 1091 + this.db = this.viewServerUserManager.viewServer.internalCreateDbForDid( 1092 + this.did 503 1093 ); 504 - proxyHeaders.set("Access-Control-Allow-Origin", "*"); 1094 + // should probably put the params of exactly what were listening to here 1095 + this.jetstream = new JetstreamManager((msg) => { 1096 + console.log("Received Jetstream message: ", msg); 505 1097 506 - const proxyRes = await fetch(bskyUrl, { 507 - method: req.method, 508 - headers: proxyHeaders, 509 - body: ["GET", "HEAD"].includes(req.method.toUpperCase()) 510 - ? undefined 511 - : reqBody, 512 - }); 1098 + const op = msg.commit.operation; 1099 + const doer = msg.did; 1100 + const rev = msg.commit.rev; 1101 + const aturi = `${msg.did}/${msg.commit.collection}/${msg.commit.rkey}`; 1102 + const value = msg.commit.record; 513 1103 514 - const resBody = await proxyRes.text(); 1104 + if (!doer || !value) return; 1105 + this.viewServerUserManager.viewServer.viewServerIndexer({ 1106 + op, 1107 + doer, 1108 + cid: msg.commit.cid, 1109 + rev, 1110 + aturi, 1111 + value, 1112 + indexsrc: `jetstream-${op}`, 1113 + db: this.db, 1114 + }); 1115 + }); 1116 + this.jetstream.start({ 1117 + // for realsies pls get from db or something instead of this shit 1118 + wantedDids: [ 1119 + this.did, 1120 + // "did:plc:mn45tewwnse5btfftvd3powc", 1121 + // "did:plc:yy6kbriyxtimkjqonqatv2rb", 1122 + // "did:plc:zzhzjga3ab5fcs2vnsv2ist3", 1123 + // "did:plc:jz4ibztn56hygfld6j6zjszg", 1124 + ], 1125 + wantedCollections: [ 1126 + // View server only needs some of the things related to user views mutes, not all of them 1127 + //"app.bsky.actor.profile", 1128 + //"app.bsky.feed.generator", 1129 + //"app.bsky.feed.like", 1130 + //"app.bsky.feed.post", 1131 + //"app.bsky.feed.repost", 1132 + "app.bsky.feed.threadgate", // mod 1133 + "app.bsky.graph.block", // mod 1134 + "app.bsky.graph.follow", // graphing 1135 + //"app.bsky.graph.list", 1136 + "app.bsky.graph.listblock", // mod 1137 + //"app.bsky.graph.listitem", 1138 + "app.bsky.notification.declaration", // mod 1139 + ], 1140 + }); 1141 + //await connectToJetstream(this.did, this.db); 1142 + this.spacedust = new SpacedustManager((msg: SpacedustLinkMessage) => { 1143 + console.log("Received Spacedust message: ", msg); 1144 + const operation = msg.link.operation; 515 1145 516 - // console.log( 517 - // "โ† Response:", 518 - // JSON.stringify(await JSON.parse(resBody), null, 2) 519 - // ); 1146 + const sourceURI = new ATPAPI.AtUri(msg.link.source_record); 1147 + const srcUri = msg.link.source_record; 1148 + const srcDid = sourceURI.host; 1149 + const srcField = msg.link.source; 1150 + const srcCol = sourceURI.collection; 1151 + const subjectURI = new ATPAPI.AtUri(msg.link.subject); 1152 + const subUri = msg.link.subject; 1153 + const subDid = subjectURI.host; 1154 + const subCol = subjectURI.collection; 1155 + 1156 + if (operation === "delete") { 1157 + this.db.run( 1158 + `DELETE FROM backlink_skeleton 1159 + WHERE srcuri = ? AND srcfield = ? AND suburi = ?`, 1160 + [srcUri, srcField, subUri] 1161 + ); 1162 + } else if (operation === "create") { 1163 + this.db.run( 1164 + `INSERT OR REPLACE INTO backlink_skeleton ( 1165 + srcuri, 1166 + srcdid, 1167 + srcfield, 1168 + srccol, 1169 + suburi, 1170 + subdid, 1171 + subcol, 1172 + indexedAt 1173 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, 1174 + [ 1175 + srcUri, // full AT URI of the source record 1176 + srcDid, // did: of the source 1177 + srcField, // e.g., "reply.parent.uri" or "facets.features.did" 1178 + srcCol, // e.g., "app.bsky.feed.post" 1179 + subUri, // full AT URI of the subject (linked record) 1180 + subDid, // did: of the subject 1181 + subCol, // subject collection (can be inferred or passed) 1182 + Date.now(), 1183 + ] 1184 + ); 1185 + } 1186 + }); 1187 + this.spacedust.start({ 1188 + wantedSources: [ 1189 + // view server keeps all of this because notifications are a thing 1190 + "app.bsky.feed.like:subject.uri", // like 1191 + "app.bsky.feed.like:via.uri", // liked repost 1192 + "app.bsky.feed.repost:subject.uri", // repost 1193 + "app.bsky.feed.repost:via.uri", // reposted repost 1194 + "app.bsky.feed.post:reply.root.uri", // thread OP 1195 + "app.bsky.feed.post:reply.parent.uri", // direct parent 1196 + "app.bsky.feed.post:embed.media.record.record.uri", // quote with media 1197 + "app.bsky.feed.post:embed.record.uri", // quote without media 1198 + "app.bsky.feed.threadgate:post", // threadgate subject 1199 + "app.bsky.feed.threadgate:hiddenReplies", // threadgate items (array) 1200 + "app.bsky.feed.post:facets.features.did", // facet item (array): mention 1201 + "app.bsky.graph.block:subject", // blocks 1202 + "app.bsky.graph.follow:subject", // follow 1203 + "app.bsky.graph.listblock:subject", // list item (blocks) 1204 + "app.bsky.graph.listblock:list", // blocklist mention (might not exist) 1205 + "app.bsky.graph.listitem:subject", // list item (blocks) 1206 + "app.bsky.graph.listitem:list", // list mention 1207 + ], 1208 + // should be getting from DB but whatever right 1209 + wantedSubjects: [ 1210 + // as noted i dont need to write down each post, just the user to listen to ! 1211 + // hell yeah 1212 + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybv7b6ic2h", 1213 + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvybws4avc2h", 1214 + // "at://did:plc:mn45tewwnse5btfftvd3powc/app.bsky.feed.post/3lvvkcxcscs2h", 1215 + // "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3l63ogxocq42f", 1216 + // "at://did:plc:yy6kbriyxtimkjqonqatv2rb/app.bsky.feed.post/3lw3wamvflu23", 1217 + ], 1218 + wantedSubjectDids: [ 1219 + this.did, 1220 + //"did:plc:mn45tewwnse5btfftvd3powc", 1221 + //"did:plc:yy6kbriyxtimkjqonqatv2rb", 1222 + //"did:plc:zzhzjga3ab5fcs2vnsv2ist3", 1223 + //"did:plc:jz4ibztn56hygfld6j6zjszg", 1224 + ], 1225 + instant: ["true"] 1226 + }); 1227 + //await connectToConstellation(this.did, this.db); 1228 + } 1229 + 1230 + // initialize() { 1231 + 1232 + // } 1233 + 1234 + // async handleHttpRequest(route: string, req: Request): Promise<Response> { 1235 + // if (route === "posts") { 1236 + // const posts = await this.queryPosts(); 1237 + // return new Response(JSON.stringify(posts), { 1238 + // headers: { "content-type": "application/json" }, 1239 + // }); 1240 + // } 1241 + 1242 + // return new Response("Unknown route", { status: 404 }); 1243 + // } 1244 + 1245 + // private async queryPosts() { 1246 + // return this.db.run( 1247 + // "SELECT * FROM posts ORDER BY created_at DESC LIMIT 100" 1248 + // ); 1249 + // } 1250 + 1251 + shutdown() { 1252 + this.jetstream.stop(); 1253 + this.spacedust.stop(); 1254 + this.db.close?.(); 1255 + } 1256 + } 1257 + 1258 + async function getServiceEndpointFromIdentity( 1259 + did: string, 1260 + kind: "skylite_index" | "bsky_appview" 1261 + ): Promise<string | null> { 1262 + //const identity = await resolveIdentity(did); 1263 + //const declUrl = `${identity.pds}/xrpc/com.atproto.repo.getRecord?repo=${identity.did}&collection=party.whey.skylite.declaration&rkey=self`; 1264 + 1265 + //const data = (await cachedFetch(declUrl)) as any; 1266 + const data = await getSlingshotRecord(did,"party.whey.skylite.declaration","self") as any; 1267 + //if (!resp.ok) throw new Error(`Failed to fetch declaration for ${did}`); 1268 + //const data = await resp.json(); 520 1269 521 - return new Response(resBody, { 522 - status: proxyRes.status, 523 - headers: proxyRes.headers, 524 - }); 1270 + const svc = data?.value?.service?.find((s: any) => s.id === `#${kind}`); 1271 + return svc?.serviceEndpoint ?? null; 525 1272 } 1273 + 1274 + const cache = new QuickLRU({ maxSize: 10000 }); 1275 + 1276 + async function getSkyliteEndpoint(did: string): Promise<string | null> { 1277 + if (cache.has(did)) return cache.get(did) as string; 1278 + for (const resolver of config.viewServer.indexPriority) { 1279 + try { 1280 + const [prefix, suffix] = resolver.split("#") as [ 1281 + "user" | `did:web:${string}`, 1282 + "skylite_index" | "bsky_appview" 1283 + ]; 1284 + if (prefix === "user") { 1285 + return await getServiceEndpointFromIdentity(did, suffix); 1286 + } else if (prefix.startsWith("did:web:")) { 1287 + // map did:web:foo.com -> https://foo.com 1288 + return prefix.replace("did:web:", "https://"); 1289 + } 1290 + } catch (err) { 1291 + // continue to next resolver 1292 + continue; 1293 + } 1294 + } 1295 + return null; //throw new Error(`No endpoint found for ${resolver}`); 1296 + } 1297 + 1298 + // export interface Notification { 1299 + // $type?: 'app.bsky.notification.listNotifications#notification'; 1300 + // uri: string; 1301 + // cid: string; 1302 + // author: AppBskyActorDefs.ProfileView; 1303 + // /** The reason why this notification was delivered - e.g. your post was liked, or you received a new follower. */ 1304 + // reason: 'like' | 'repost' | 'follow' | 'mention' | 'reply' | 'quote' | 'starterpack-joined' | 'verified' | 'unverified' | 'like-via-repost' | 'repost-via-repost' | (string & {}); 1305 + // reasonSubject?: string; 1306 + // record: { 1307 + // [_ in string]: unknown; 1308 + // }; 1309 + // isRead: boolean; 1310 + // indexedAt: string; 1311 + // labels?: ComAtprotoLabelDefs.Label[]; 1312 + // }