Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

init cli migration work

+3108 -669
+307
apps/main-app/public/landingpage.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 + <title>wisp.place</title> 7 + <meta name="description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution. Built on Bluesky's decentralized network." /> 8 + 9 + <!-- Open Graph / Facebook --> 10 + <meta property="og:type" content="website" /> 11 + <meta property="og:url" content="https://wisp.place/" /> 12 + <meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" /> 13 + <meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 14 + <meta property="og:site_name" content="wisp.place" /> 15 + 16 + <!-- Twitter --> 17 + <meta name="twitter:card" content="summary_large_image" /> 18 + <meta name="twitter:url" content="https://wisp.place/" /> 19 + <meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" /> 20 + <meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 21 + 22 + <!-- Theme --> 23 + <meta name="theme-color" content="#000000" /> 24 + 25 + <link rel="icon" type="image/x-icon" href="./favicon.ico"> 26 + <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> 27 + <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> 28 + <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> 29 + <link rel="manifest" href="./site.webmanifest"> 30 + 31 + <link rel="preconnect" href="https://fonts.googleapis.com"> 32 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 33 + <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" rel="stylesheet"> 34 + 35 + <style> 36 + * { 37 + margin: 0; 38 + padding: 0; 39 + box-sizing: border-box; 40 + } 41 + 42 + :root { 43 + --bg: #fafafa; 44 + --text: #000; 45 + --text-muted: #666; 46 + --text-subtle: #999; 47 + --border: #ddd; 48 + --cta-bg: #000; 49 + --cta-text: #fff; 50 + --cta-hover-bg: #fff; 51 + --cta-hover-text: #000; 52 + --code-bg: #000; 53 + --code-text: #0f0; 54 + --link: #000; 55 + } 56 + 57 + @media (prefers-color-scheme: dark) { 58 + :root { 59 + --bg: #0a0a0a; 60 + --text: #fafafa; 61 + --text-muted: #999; 62 + --text-subtle: #666; 63 + --border: #333; 64 + --cta-bg: #fff; 65 + --cta-text: #000; 66 + --cta-hover-bg: #0a0a0a; 67 + --cta-hover-text: #fff; 68 + --code-bg: #111; 69 + --code-text: #0f0; 70 + --link: #fff; 71 + } 72 + } 73 + 74 + body { 75 + font-family: "Fira Mono", monospace; 76 + font-weight: 400; 77 + background: var(--bg); 78 + color: var(--text); 79 + min-height: 100vh; 80 + display: flex; 81 + flex-direction: column; 82 + padding-top: 6rem; 83 + } 84 + 85 + .container { 86 + max-width: 800px; 87 + margin: 0 auto; 88 + padding: 0 2rem; 89 + width: 100%; 90 + } 91 + 92 + main { 93 + flex: 1; 94 + display: flex; 95 + align-items: center; 96 + justify-content: center; 97 + } 98 + 99 + .hero { 100 + text-align: center; 101 + padding: 4rem 0; 102 + } 103 + 104 + h1 { 105 + font-size: 5rem; 106 + font-weight: 700; 107 + margin-bottom: 4rem; 108 + letter-spacing: -0.02em; 109 + color: #4a4a4a; 110 + text-shadow: 111 + 1px 1px 0 #fff, 112 + -1px -1px 0 #2a2a2a, 113 + 2px 2px 3px rgba(0, 0, 0, 0.3); 114 + } 115 + 116 + @media (prefers-color-scheme: dark) { 117 + h1 { 118 + color: #888; 119 + text-shadow: 120 + 1px 1px 0 #222, 121 + -1px -1px 0 #000, 122 + 2px 2px 3px rgba(0, 0, 0, 0.5); 123 + } 124 + } 125 + 126 + h1::after { 127 + content: '_'; 128 + animation: blink 1s infinite; 129 + } 130 + 131 + @keyframes blink { 132 + 0%, 50% { opacity: 1; } 133 + 51%, 100% { opacity: 0; } 134 + } 135 + 136 + .cta { 137 + display: inline-block; 138 + background: var(--cta-bg); 139 + color: var(--cta-text); 140 + padding: 2rem 4rem; 141 + font-size: 1.5rem; 142 + text-decoration: none; 143 + border: 3px solid var(--cta-bg); 144 + transition: all 0.1s; 145 + font-weight: 700; 146 + margin-bottom: 3rem; 147 + } 148 + 149 + .cta:hover { 150 + background: var(--cta-hover-bg); 151 + color: var(--cta-hover-text); 152 + border-color: var(--cta-bg); 153 + } 154 + 155 + .tagline { 156 + font-size: 1.2rem; 157 + color: var(--text-muted); 158 + margin-bottom: 6rem; 159 + } 160 + 161 + .secondary { 162 + border-top: 1px solid var(--border); 163 + padding-top: 3rem; 164 + margin-top: 4rem; 165 + } 166 + 167 + .secondary h2 { 168 + font-size: 1rem; 169 + margin-bottom: 1.5rem; 170 + font-weight: 700; 171 + text-transform: lowercase; 172 + } 173 + 174 + .code-block { 175 + background: var(--code-bg); 176 + color: var(--code-text); 177 + padding: 1.5rem; 178 + margin: 1rem 0; 179 + font-size: 0.9rem; 180 + overflow-x: auto; 181 + } 182 + 183 + .code-block code { 184 + font-family: "Fira Mono", monospace; 185 + } 186 + 187 + .secondary p { 188 + color: var(--text-muted); 189 + margin-bottom: 1rem; 190 + font-size: 0.95rem; 191 + } 192 + 193 + .secondary a { 194 + color: var(--link); 195 + text-decoration: none; 196 + border-bottom: 1px solid var(--link); 197 + } 198 + 199 + .secondary a:hover { 200 + border-bottom: 2px solid var(--link); 201 + } 202 + 203 + footer { 204 + border-top: 1px solid var(--border); 205 + padding: 3rem 0; 206 + text-align: center; 207 + margin-top: 6rem; 208 + } 209 + 210 + .quote { 211 + font-size: 0.85rem; 212 + color: var(--text-subtle); 213 + font-style: italic; 214 + } 215 + 216 + .links { 217 + margin-top: 2rem; 218 + font-size: 0.85rem; 219 + } 220 + 221 + .links a { 222 + color: var(--text-muted); 223 + text-decoration: none; 224 + margin: 0 1rem; 225 + } 226 + 227 + .links a:hover { 228 + color: var(--text); 229 + } 230 + 231 + @media (max-width: 768px) { 232 + h1 { 233 + font-size: 3rem; 234 + } 235 + 236 + .cta { 237 + padding: 1.5rem 3rem; 238 + font-size: 1.2rem; 239 + } 240 + 241 + .tagline { 242 + font-size: 1rem; 243 + } 244 + } 245 + </style> 246 + </head> 247 + <body> 248 + <main> 249 + <div class="container"> 250 + <div class="hero"> 251 + <h1>wisp.place</h1> 252 + 253 + <a href="{{ATPROTO_LOGIN_URL}}" class="cta">SIGN IN WITH AT PROTOCOL</a> 254 + 255 + <p class="tagline">Deploy static sites like it's localhost.</p> 256 + 257 + <div class="secondary"> 258 + <h2>are you a nerd? From your terminal to your server, nothing in between.</h2> 259 + <div id="cli-download" class="code-block"> 260 + <code>curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli</code> 261 + </div> 262 + <div class="code-block"> 263 + <code>wisp-cli deploy alice.bsky.social --site MyBlog</code> 264 + </div> 265 + <p>host on our infrastructure for free<br> 266 + or use wisp-cli to host on your own infra with seamless deployments</p> 267 + <p>need docs? <a href="https://docs.wisp.place">docs.wisp.place</a></p> 268 + </div> 269 + </div> 270 + </div> 271 + </main> 272 + 273 + <footer> 274 + <div class="container"> 275 + <p class="quote">"The easiest way to get static HTML going."</p> 276 + <div class="links"> 277 + <a href="https://docs.wisp.place">docs</a> 278 + </div> 279 + </div> 280 + </footer> 281 + 282 + <script> 283 + const CLI_BASE = 'https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries'; 284 + 285 + function detectOS() { 286 + const ua = navigator.userAgent.toLowerCase(); 287 + const platform = navigator.platform.toLowerCase(); 288 + if (platform.includes('mac') || ua.includes('mac')) return 'macos'; 289 + return 'linux'; 290 + } 291 + 292 + function updateCurlCommand() { 293 + const container = document.getElementById('cli-download'); 294 + const os = detectOS(); 295 + 296 + if (os === 'macos') { 297 + container.innerHTML = `<code>curl ${CLI_BASE}/wisp-cli-darwin-universal -o wisp-cli</code>`; 298 + } else { 299 + container.innerHTML = `<code>curl ${CLI_BASE}/wisp-cli-x86_64-linux -o wisp-cli</code> 300 + <code># ARM: curl ${CLI_BASE}/wisp-cli-aarch64-linux -o wisp-cli</code>`; 301 + } 302 + } 303 + 304 + updateCurlCommand(); 305 + </script> 306 + </body> 307 + </html>
+3 -284
apps/main-app/src/index.ts
··· 121 121 }) 122 122 .onError(observabilityMiddleware('main-app').onError) 123 123 .use(csrfProtection()) 124 - .get('/', ({ set }) => { 124 + .get('/', async ({ set }) => { 125 125 // Build dynamic login URL for AT Protocol OAuth entryway 126 - // atproto.wisp.place will redirect to this endpoint with the saved handle 127 126 const isLocalDev = Bun.env.LOCAL_DEV === 'true' 128 127 const loginUrl = isLocalDev 129 128 ? 'http://127.0.0.1:8000/api/auth/login' ··· 132 131 133 132 set.headers['Content-Type'] = 'text/html; charset=utf-8' 134 133 135 - return `<!DOCTYPE html> 136 - <html lang="en"> 137 - <head> 138 - <meta charset="UTF-8"> 139 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 140 - <title>wisp.place</title> 141 - <meta name="description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution. Built on Bluesky's decentralized network." /> 142 - 143 - <!-- Open Graph / Facebook --> 144 - <meta property="og:type" content="website" /> 145 - <meta property="og:url" content="https://wisp.place/" /> 146 - <meta property="og:title" content="wisp.place - Decentralized Static Site Hosting" /> 147 - <meta property="og:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 148 - <meta property="og:site_name" content="wisp.place" /> 149 - 150 - <!-- Twitter --> 151 - <meta name="twitter:card" content="summary_large_image" /> 152 - <meta name="twitter:url" content="https://wisp.place/" /> 153 - <meta name="twitter:title" content="wisp.place - Decentralized Static Site Hosting" /> 154 - <meta name="twitter:description" content="Host static websites directly in your AT Protocol account. Keep full ownership and control with fast CDN distribution." /> 155 - 156 - <!-- Theme --> 157 - <meta name="theme-color" content="#000000" /> 158 - 159 - <link rel="icon" type="image/x-icon" href="./favicon.ico"> 160 - <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> 161 - <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> 162 - <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> 163 - <link rel="manifest" href="./site.webmanifest"> 164 - 165 - <link rel="preconnect" href="https://fonts.googleapis.com"> 166 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 167 - <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" rel="stylesheet"> 168 - 169 - <style> 170 - * { 171 - margin: 0; 172 - padding: 0; 173 - box-sizing: border-box; 174 - } 175 - 176 - :root { 177 - --bg: #fafafa; 178 - --text: #000; 179 - --text-muted: #666; 180 - --text-subtle: #999; 181 - --border: #ddd; 182 - --cta-bg: #000; 183 - --cta-text: #fff; 184 - --cta-hover-bg: #fff; 185 - --cta-hover-text: #000; 186 - --code-bg: #000; 187 - --code-text: #0f0; 188 - --link: #000; 189 - } 190 - 191 - @media (prefers-color-scheme: dark) { 192 - :root { 193 - --bg: #0a0a0a; 194 - --text: #fafafa; 195 - --text-muted: #999; 196 - --text-subtle: #666; 197 - --border: #333; 198 - --cta-bg: #fff; 199 - --cta-text: #000; 200 - --cta-hover-bg: #0a0a0a; 201 - --cta-hover-text: #fff; 202 - --code-bg: #111; 203 - --code-text: #0f0; 204 - --link: #fff; 205 - } 206 - } 207 - 208 - body { 209 - font-family: "Fira Mono", monospace; 210 - font-weight: 400; 211 - background: var(--bg); 212 - color: var(--text); 213 - min-height: 100vh; 214 - display: flex; 215 - flex-direction: column; 216 - padding-top: 6rem; 217 - } 218 - 219 - .container { 220 - max-width: 800px; 221 - margin: 0 auto; 222 - padding: 0 2rem; 223 - width: 100%; 224 - } 225 - 226 - main { 227 - flex: 1; 228 - display: flex; 229 - align-items: center; 230 - justify-content: center; 231 - } 232 - 233 - .hero { 234 - text-align: center; 235 - padding: 4rem 0; 236 - } 237 - 238 - h1 { 239 - font-size: 5rem; 240 - font-weight: 700; 241 - margin-bottom: 4rem; 242 - letter-spacing: -0.02em; 243 - color: #4a4a4a; 244 - text-shadow: 245 - 1px 1px 0 #fff, 246 - -1px -1px 0 #2a2a2a, 247 - 2px 2px 3px rgba(0, 0, 0, 0.3); 248 - } 249 - 250 - @media (prefers-color-scheme: dark) { 251 - h1 { 252 - color: #888; 253 - text-shadow: 254 - 1px 1px 0 #222, 255 - -1px -1px 0 #000, 256 - 2px 2px 3px rgba(0, 0, 0, 0.5); 257 - } 258 - } 259 - 260 - h1::after { 261 - content: '_'; 262 - animation: blink 1s infinite; 263 - } 264 - 265 - @keyframes blink { 266 - 0%, 50% { opacity: 1; } 267 - 51%, 100% { opacity: 0; } 268 - } 269 - 270 - .cta { 271 - display: inline-block; 272 - background: var(--cta-bg); 273 - color: var(--cta-text); 274 - padding: 2rem 4rem; 275 - font-size: 1.5rem; 276 - text-decoration: none; 277 - border: 3px solid var(--cta-bg); 278 - transition: all 0.1s; 279 - font-weight: 700; 280 - margin-bottom: 3rem; 281 - } 282 - 283 - .cta:hover { 284 - background: var(--cta-hover-bg); 285 - color: var(--cta-hover-text); 286 - border-color: var(--cta-bg); 287 - } 288 - 289 - .tagline { 290 - font-size: 1.2rem; 291 - color: var(--text-muted); 292 - margin-bottom: 6rem; 293 - } 294 - 295 - .secondary { 296 - border-top: 1px solid var(--border); 297 - padding-top: 3rem; 298 - margin-top: 4rem; 299 - } 300 - 301 - .secondary h2 { 302 - font-size: 1rem; 303 - margin-bottom: 1.5rem; 304 - font-weight: 700; 305 - text-transform: lowercase; 306 - } 307 - 308 - .code-block { 309 - background: var(--code-bg); 310 - color: var(--code-text); 311 - padding: 1.5rem; 312 - margin: 1rem 0; 313 - font-size: 0.9rem; 314 - overflow-x: auto; 315 - } 316 - 317 - .code-block code { 318 - font-family: "Fira Mono", monospace; 319 - } 320 - 321 - .secondary p { 322 - color: var(--text-muted); 323 - margin-bottom: 1rem; 324 - font-size: 0.95rem; 325 - } 326 - 327 - .secondary a { 328 - color: var(--link); 329 - text-decoration: none; 330 - border-bottom: 1px solid var(--link); 331 - } 332 - 333 - .secondary a:hover { 334 - border-bottom: 2px solid var(--link); 335 - } 336 - 337 - footer { 338 - border-top: 1px solid var(--border); 339 - padding: 3rem 0; 340 - text-align: center; 341 - margin-top: 6rem; 342 - } 343 - 344 - .quote { 345 - font-size: 0.85rem; 346 - color: var(--text-subtle); 347 - font-style: italic; 348 - } 349 - 350 - .links { 351 - margin-top: 2rem; 352 - font-size: 0.85rem; 353 - } 354 - 355 - .links a { 356 - color: var(--text-muted); 357 - text-decoration: none; 358 - margin: 0 1rem; 359 - } 360 - 361 - .links a:hover { 362 - color: var(--text); 363 - } 364 - 365 - @media (max-width: 768px) { 366 - h1 { 367 - font-size: 3rem; 368 - } 369 - 370 - .cta { 371 - padding: 1.5rem 3rem; 372 - font-size: 1.2rem; 373 - } 374 - 375 - .tagline { 376 - font-size: 1rem; 377 - } 378 - } 379 - </style> 380 - </head> 381 - <body> 382 - <main> 383 - <div class="container"> 384 - <div class="hero"> 385 - <h1>wisp.place</h1> 386 - 387 - <a href="${atprotoLoginUrl}" class="cta">SIGN IN WITH AT PROTOCOL</a> 388 - 389 - <p class="tagline">Drop files. They're live.</p> 390 - 391 - <div class="secondary"> 392 - <h2>are you a terminal nerd?</h2> 393 - <div class="code-block"> 394 - <code>curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli</code> 395 - </div> 396 - <div class="code-block"> 397 - <code>wisp-cli alice.bsky.social --site MyBlog</code> 398 - </div> 399 - <p>host on our infrastructure for free<br> 400 - or use wisp-cli to host on your own infra with seamless deployments</p> 401 - <p>need docs? <a href="https://docs.wisp.place">docs.wisp.place</a></p> 402 - </div> 403 - </div> 404 - </div> 405 - </main> 406 - 407 - <footer> 408 - <div class="container"> 409 - <p class="quote">"The easiest way to get static HTML going."</p> 410 - <div class="links"> 411 - <a href="https://docs.wisp.place">docs</a> 412 - </div> 413 - </div> 414 - </footer> 415 - </body> 416 - </html>` 134 + const html = await Bun.file('./apps/main-app/public/landingpage.html').text() 135 + return html.replace('{{ATPROTO_LOGIN_URL}}', atprotoLoginUrl) 417 136 }) 418 137 .use(authRoutes(client, cookieSecret)) 419 138 .use(wispRoutes(client, cookieSecret))
+260 -27
bun.lock
··· 104 104 "typescript": "^5.9.3", 105 105 }, 106 106 }, 107 + "cli": { 108 + "name": "wisp-cli", 109 + "version": "1.0.0", 110 + "bin": { 111 + "wisp-cli": "./index.ts", 112 + }, 113 + "dependencies": { 114 + "@atproto/api": "^0.18.17", 115 + "@atproto/oauth-client-node": "^0.3.15", 116 + "@atproto/sync": "^0.1.39", 117 + "@wisp/atproto-utils": "workspace:*", 118 + "@wisp/constants": "workspace:*", 119 + "@wisp/fs-utils": "workspace:*", 120 + "@wisp/lexicons": "workspace:*", 121 + "commander": "^14.0.2", 122 + "ignore": "^7.0.5", 123 + "mime-types": "^3.0.2", 124 + "open": "^11.0.0", 125 + "ora": "^9.1.0", 126 + "picocolors": "^1.1.1", 127 + }, 128 + "devDependencies": { 129 + "@types/bun": "latest", 130 + "@types/mime-types": "^3.0.1", 131 + }, 132 + "peerDependencies": { 133 + "typescript": "^5", 134 + }, 135 + }, 107 136 "packages/@wisp/atproto-utils": { 108 137 "name": "@wisp/atproto-utils", 109 138 "version": "1.0.0", 110 139 "dependencies": { 111 - "@atproto/api": "^0.14.1", 140 + "@atproto/api": "^0.18.17", 112 141 "@wisp/lexicons": "workspace:*", 113 142 "multiformats": "^13.3.1", 114 143 }, 115 144 "devDependencies": { 116 - "@atproto/lexicon": "^0.5.2", 145 + "@atproto/lexicon": "^0.6.1", 117 146 }, 118 147 }, 119 148 "packages/@wisp/constants": { ··· 131 160 "name": "@wisp/fs-utils", 132 161 "version": "1.0.0", 133 162 "dependencies": { 134 - "@atproto/api": "^0.14.1", 163 + "@atproto/api": "^0.18.17", 135 164 "@wisp/lexicons": "workspace:*", 136 165 }, 137 166 }, ··· 220 249 221 250 "@atproto-labs/simple-store-memory": ["@atproto-labs/simple-store-memory@0.1.4", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "lru-cache": "^10.2.0" } }, "sha512-3mKY4dP8I7yKPFj9VKpYyCRzGJOi5CEpOLPlRhoJyLmgs3J4RzDrjn323Oakjz2Aj2JzRU/AIvWRAZVhpYNJHw=="], 222 251 223 - "@atproto/api": ["@atproto/api@0.14.22", "", { "dependencies": { "@atproto/common-web": "^0.4.1", "@atproto/lexicon": "^0.4.10", "@atproto/syntax": "^0.4.0", "@atproto/xrpc": "^0.6.12", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-ziXPau+sUdFovObSnsoN7JbOmUw1C5e5L28/yXf3P8vbEnSS3HVVGD1jYcscBYY34xQqi4bVDpwMYx/4yRsTuQ=="], 252 + "@atproto/api": ["@atproto/api@0.18.17", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/lexicon": "^0.6.1", "@atproto/syntax": "^0.4.3", "@atproto/xrpc": "^0.7.7", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-TeJkLGPkiK3jblwTDSNTH+CnS6WgaOiHDZeVVzywtxomyyF0FpQVSMz5eP3sDhxyHJqpI3E2AOYD7PO/JSbzJw=="], 224 253 225 254 "@atproto/common": ["@atproto/common@0.4.12", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@ipld/dag-cbor": "^7.0.3", "cbor-x": "^1.5.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-NC+TULLQiqs6MvNymhQS5WDms3SlbIKGLf4n33tpftRJcalh507rI+snbcUb7TLIkKw7VO17qMqxEXtIdd5auQ=="], 226 255 ··· 238 267 239 268 "@atproto/jwk-webcrypto": ["@atproto/jwk-webcrypto@0.2.0", "", { "dependencies": { "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "zod": "^3.23.8" } }, "sha512-UmgRrrEAkWvxwhlwe30UmDOdTEFidlIzBC7C3cCbeJMcBN1x8B3KH+crXrsTqfWQBG58mXgt8wgSK3Kxs2LhFg=="], 240 269 241 - "@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-sTr3UCL2SgxEoYVpzJGgWTnNl4TpngP5tMcRyaOvi21Se4m3oR4RDsoVDPz8AS6XphiteRwzwPstquN7aWWMbA=="], 270 + "@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-szkS569j1eZsIxZKh2VZHVq7pSpewy1wHh8c6nVYekHfYcJhFkevQq/DjTeatZ7YZKNReGYthQulgaZq2ytfWQ=="], 242 271 243 272 "@atproto/lex-cli": ["@atproto/lex-cli@0.9.7", "", { "dependencies": { "@atproto/lexicon": "^0.5.2", "@atproto/syntax": "^0.4.1", "chalk": "^4.1.2", "commander": "^9.4.0", "prettier": "^3.2.5", "ts-morph": "^24.0.0", "yesno": "^0.4.0", "zod": "^3.23.8" }, "bin": { "lex": "dist/index.js" } }, "sha512-UZVf0pK0mB4qiuwbnrxmV0mC9/Vk2v7W3u9pd4wc4GFojzAyGP76MF2TiwWFya5mgzC7723/r5Jb4ADg0rtfng=="], 244 273 ··· 246 275 247 276 "@atproto/lex-json": ["@atproto/lex-json@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "tslib": "^2.8.1" } }, "sha512-Pd72lO+l2rhOTutnf11omh9ZkoB/elbzE3HSmn2wuZlyH1mRhTYvoH8BOGokWQwbZkCE8LL3nOqMT3gHCD2l7g=="], 248 277 249 - "@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 278 + "@atproto/lexicon": ["@atproto/lexicon@0.6.1", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/syntax": "^0.4.3", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-/vI1kVlY50Si+5MXpvOucelnYwb0UJ6Qto5mCp+7Q5C+Jtp+SoSykAPVvjVtTnQUH2vrKOFOwpb3C375vSKzXw=="], 250 279 251 280 "@atproto/oauth-client": ["@atproto/oauth-client@0.5.10", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.4", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.4", "@atproto-labs/identity-resolver": "0.3.4", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.5.2", "@atproto/xrpc": "0.7.6", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-2mdJFyYbaOw3e/1KMBOQ2/J9p+MfWW8kE6FKdExWrJ7JPJpTJw2ZF2EmdGHCVeXw386dQgXbLkr+w4vbgSqfMQ=="], 252 281 ··· 254 283 255 284 "@atproto/oauth-types": ["@atproto/oauth-types@0.5.2", "", { "dependencies": { "@atproto/did": "0.2.3", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-9DCDvtvCanTwAaU5UakYDO0hzcOITS3RutK5zfLytE5Y9unj0REmTDdN8Xd8YCfUJl7T/9pYpf04Uyq7bFTASg=="], 256 285 257 - "@atproto/repo": ["@atproto/repo@0.8.11", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.2", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-b/WCu5ITws4ILHoXiZz0XXB5U9C08fUVzkBQDwpnme62GXv8gUaAPL/ttG61OusW09ARwMMQm4vxoP0hTFg+zA=="], 286 + "@atproto/repo": ["@atproto/repo@0.8.12", "", { "dependencies": { "@atproto/common": "^0.5.3", "@atproto/common-web": "^0.4.7", "@atproto/crypto": "^0.4.5", "@atproto/lexicon": "^0.6.0", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-QpVTVulgfz5PUiCTELlDBiRvnsnwrFWi+6CfY88VwXzrRHd9NE8GItK7sfxQ6U65vD/idH8ddCgFrlrsn1REPQ=="], 258 287 259 - "@atproto/sync": ["@atproto/sync@0.1.38", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/identity": "^0.4.10", "@atproto/lexicon": "^0.5.2", "@atproto/repo": "^0.8.11", "@atproto/syntax": "^0.4.1", "@atproto/xrpc-server": "^0.10.0", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-2rE0SM21Nk4hWw/XcIYFnzlWO6/gBg8mrzuWbOvDhD49sA/wW4zyjaHZ5t1gvk28/SLok2VZiIR8nYBdbf7F5Q=="], 288 + "@atproto/sync": ["@atproto/sync@0.1.39", "", { "dependencies": { "@atproto/common": "^0.5.3", "@atproto/identity": "^0.4.10", "@atproto/lexicon": "^0.6.0", "@atproto/repo": "^0.8.12", "@atproto/syntax": "^0.4.2", "@atproto/xrpc-server": "^0.10.3", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-JE0flkb6cDHc1dFNclkX6QB2PYXR+Taa1HDP7prI1lyFtkEASO0AOt+VtbL2JKhEa7VEy8ckko1T9glpCwGNYA=="], 260 289 261 - "@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 290 + "@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 262 291 263 292 "@atproto/ws-client": ["@atproto/ws-client@0.0.2", "", { "dependencies": { "@atproto/common": "^0.4.12", "ws": "^8.12.0" } }, "sha512-yb11WtI9cZfx/00MTgZRabB97Quf/TerMmtzIm2H2YirIq2oW++NPoufXYCuXuQGR4ep4fvCyzz0/GX95jCONQ=="], 264 293 ··· 778 807 779 808 "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], 780 809 781 - "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 810 + "@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="], 782 811 783 812 "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], 784 813 ··· 814 843 815 844 "actor-typeahead": ["actor-typeahead@0.1.2", "", {}, "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A=="], 816 845 817 - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 846 + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 818 847 819 848 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 820 849 ··· 848 877 849 878 "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 850 879 880 + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 881 + 851 882 "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 852 883 853 884 "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], ··· 866 897 867 898 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], 868 899 900 + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], 901 + 902 + "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], 903 + 869 904 "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], 870 905 871 906 "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], ··· 876 911 877 912 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 878 913 879 - "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], 914 + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], 880 915 881 916 "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], 882 917 ··· 892 927 893 928 "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 894 929 930 + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], 931 + 932 + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], 933 + 934 + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], 935 + 895 936 "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 896 937 897 938 "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], ··· 962 1003 963 1004 "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 964 1005 1006 + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 1007 + 965 1008 "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 966 1009 967 1010 "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], ··· 1002 1045 1003 1046 "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], 1004 1047 1048 + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 1049 + 1005 1050 "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 1006 1051 1007 1052 "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 1008 1053 1009 1054 "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 1010 1055 1056 + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], 1057 + 1058 + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], 1059 + 1060 + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], 1061 + 1011 1062 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 1063 + 1064 + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], 1065 + 1066 + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 1012 1067 1013 1068 "iso-datestring-validator": ["iso-datestring-validator@2.2.2", "", {}, "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="], 1014 1069 ··· 1041 1096 "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], 1042 1097 1043 1098 "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], 1099 + 1100 + "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], 1044 1101 1045 1102 "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], 1046 1103 ··· 1064 1121 1065 1122 "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], 1066 1123 1067 - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1124 + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 1068 1125 1069 - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 1126 + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 1127 + 1128 + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], 1070 1129 1071 1130 "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 1072 1131 ··· 1090 1149 1091 1150 "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 1092 1151 1152 + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], 1153 + 1154 + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], 1155 + 1093 1156 "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], 1157 + 1158 + "ora": ["ora@9.1.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0" } }, "sha512-53uuLsXHOAJl5zLrUrzY9/kE+uIFEx7iaH4g2BIJQK4LZjY4LpCCYZVKDWIkL+F01wAaCg93duQ1whnK/AmY1A=="], 1094 1159 1095 1160 "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="], 1096 1161 ··· 1121 1186 "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], 1122 1187 1123 1188 "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], 1189 + 1190 + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], 1124 1191 1125 1192 "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], 1126 1193 ··· 1165 1232 "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], 1166 1233 1167 1234 "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 1235 + 1236 + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], 1237 + 1238 + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], 1168 1239 1169 1240 "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], 1170 1241 ··· 1190 1261 1191 1262 "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 1192 1263 1264 + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 1265 + 1193 1266 "sonic-boom": ["sonic-boom@3.8.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg=="], 1194 1267 1195 1268 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], ··· 1197 1270 "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 1198 1271 1199 1272 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 1273 + 1274 + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], 1200 1275 1201 1276 "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], 1202 1277 1203 - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1278 + "string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], 1204 1279 1205 1280 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], 1206 1281 1207 - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1282 + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 1208 1283 1209 1284 "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], 1210 1285 ··· 1274 1349 1275 1350 "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 1276 1351 1352 + "wisp-cli": ["wisp-cli@workspace:cli"], 1353 + 1277 1354 "wisp-hosting-service": ["wisp-hosting-service@workspace:apps/hosting-service"], 1278 1355 1279 1356 "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], 1280 1357 1281 1358 "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], 1359 + 1360 + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], 1282 1361 1283 1362 "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 1284 1363 ··· 1287 1366 "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 1288 1367 1289 1368 "yesno": ["yesno@0.4.0", "", {}, "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="], 1369 + 1370 + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], 1290 1371 1291 1372 "zlib": ["zlib@1.0.5", "", {}, "sha512-40fpE2II+Cd3k8HWTWONfeKE2jL+P42iWJ1zzps5W51qcTsOUKM5Q5m2PFb0CLxlmFAaUuUdJGc3OfZy947v0w=="], 1292 1373 ··· 1294 1375 1295 1376 "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], 1296 1377 1297 - "@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.4.14", "", { "dependencies": { "@atproto/common-web": "^0.4.2", "@atproto/syntax": "^0.4.0", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ=="], 1378 + "@atproto/api/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1298 1379 1299 - "@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.6.12", "", { "dependencies": { "@atproto/lexicon": "^0.4.10", "zod": "^3.23.8" } }, "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w=="], 1380 + "@atproto/api/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1300 1381 1301 1382 "@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1302 1383 ··· 1304 1385 1305 1386 "@atproto/jwk/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1306 1387 1307 - "@atproto/lex-cbor/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1388 + "@atproto/lex-cbor/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1389 + 1390 + "@atproto/lex-cli/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1391 + 1392 + "@atproto/lex-cli/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1393 + 1394 + "@atproto/lex-cli/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], 1395 + 1396 + "@atproto/lex-data/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1308 1397 1309 1398 "@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1399 + 1400 + "@atproto/lexicon/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1310 1401 1311 1402 "@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1312 1403 1313 1404 "@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1314 1405 1315 - "@atproto/repo/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="], 1406 + "@atproto/repo/@atproto/common": ["@atproto/common@0.5.9", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/lex-cbor": "0.0.9", "@atproto/lex-data": "0.0.9", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-rzl8dB7ErpA/VUgCidahUtbxEph50J4g7j68bZmlwwrHlrtvTe8DjrwH5EUFEcegl9dadIhcVJ3qi0kPKEUr+g=="], 1407 + 1408 + "@atproto/repo/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1316 1409 1317 1410 "@atproto/repo/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1318 1411 1319 - "@atproto/sync/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="], 1412 + "@atproto/sync/@atproto/common": ["@atproto/common@0.5.9", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/lex-cbor": "0.0.9", "@atproto/lex-data": "0.0.9", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-rzl8dB7ErpA/VUgCidahUtbxEph50J4g7j68bZmlwwrHlrtvTe8DjrwH5EUFEcegl9dadIhcVJ3qi0kPKEUr+g=="], 1320 1413 1321 - "@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.2", "", { "dependencies": { "@atproto/common": "^0.5.2", "@atproto/crypto": "^0.4.5", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "@atproto/lexicon": "^0.5.2", "@atproto/ws-client": "^0.0.3", "@atproto/xrpc": "^0.7.6", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-5AzN8xoV8K1Omn45z6qKH414+B3Z35D536rrScwF3aQGDEdpObAS+vya9UoSg+Gvm2+oOtVEbVri7riLTBW3Vg=="], 1414 + "@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.10", "", { "dependencies": { "@atproto/common": "^0.5.9", "@atproto/crypto": "^0.4.5", "@atproto/lex-cbor": "0.0.9", "@atproto/lex-data": "0.0.9", "@atproto/lexicon": "^0.6.1", "@atproto/ws-client": "^0.0.4", "@atproto/xrpc": "^0.7.7", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-USDjOZGiletqzuWHC3Q2fk30hJDk4uYt6KPgvnZidShCouTf3hzwJZ8d2eOKOofSjGXW+GC0QYp7fYJFn6lZ2Q=="], 1322 1415 1323 1416 "@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1417 + 1418 + "@atproto/xrpc/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1419 + 1420 + "@atproto/xrpc-server/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1421 + 1422 + "@atproto/xrpc-server/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 1324 1423 1325 1424 "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], 1326 1425 ··· 1488 1587 1489 1588 "@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 1490 1589 1590 + "@wisp/lexicons/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1591 + 1491 1592 "@wisp/main-app/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1492 1593 1493 1594 "@wisp/observability/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], 1494 1595 1596 + "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 1597 + 1598 + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1599 + 1600 + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1601 + 1495 1602 "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1496 1603 1497 1604 "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], ··· 1499 1606 "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 1500 1607 1501 1608 "node-gyp-build-optional-packages/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], 1609 + 1610 + "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 1502 1611 1503 1612 "pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 1504 1613 ··· 1512 1621 1513 1622 "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], 1514 1623 1515 - "tiered-storage/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 1516 - 1517 1624 "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1518 1625 1519 1626 "tsx/esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="], 1520 1627 1521 1628 "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 1522 1629 1630 + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 1631 + 1523 1632 "uint8arrays/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1524 1633 1634 + "wisp-cli/@atproto/oauth-client-node": ["@atproto/oauth-client-node@0.3.15", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver-node": "0.1.24", "@atproto-labs/simple-store": "0.3.0", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/jwk-jose": "0.1.11", "@atproto/jwk-webcrypto": "0.2.0", "@atproto/oauth-client": "0.5.13", "@atproto/oauth-types": "0.6.1" } }, "sha512-iuT7QrLli7IyB4px1+lHvm/YoIRfNRpbNG9seJRtu5eX4N5aLsBP6vpXs9rCygd1+/15LcLRAAGKVEcrLT9tXA=="], 1635 + 1636 + "wisp-cli/@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], 1637 + 1525 1638 "wisp-hosting-service/@atproto/api": ["@atproto/api@0.17.7", "", { "dependencies": { "@atproto/common-web": "^0.4.3", "@atproto/lexicon": "^0.5.1", "@atproto/syntax": "^0.4.1", "@atproto/xrpc": "^0.7.5", "await-lock": "^2.2.2", "multiformats": "^9.9.0", "tlds": "^1.234.0", "zod": "^3.23.8" } }, "sha512-V+OJBZq9chcrD21xk1bUa6oc5DSKfQj5DmUPf5rmZncqL1w9ZEbS38H5cMyqqdhfgo2LWeDRdZHD0rvNyJsIaw=="], 1526 1639 1527 - "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="], 1640 + "wisp-hosting-service/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1641 + 1642 + "wisp-hosting-service/@atproto/sync": ["@atproto/sync@0.1.38", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/identity": "^0.4.10", "@atproto/lexicon": "^0.5.2", "@atproto/repo": "^0.8.11", "@atproto/syntax": "^0.4.1", "@atproto/xrpc-server": "^0.10.0", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-2rE0SM21Nk4hWw/XcIYFnzlWO6/gBg8mrzuWbOvDhD49sA/wW4zyjaHZ5t1gvk28/SLok2VZiIR8nYBdbf7F5Q=="], 1643 + 1644 + "wisp-hosting-service/@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], 1645 + 1646 + "wisp-hosting-service/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 1647 + 1648 + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1649 + 1650 + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1651 + 1652 + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1653 + 1654 + "@atproto/api/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1655 + 1656 + "@atproto/api/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1657 + 1658 + "@atproto/lex-cbor/@atproto/lex-data/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1659 + 1660 + "@atproto/lex-cli/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1661 + 1662 + "@atproto/lexicon/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1663 + 1664 + "@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1665 + 1666 + "@atproto/repo/@atproto/common/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1667 + 1668 + "@atproto/repo/@atproto/common-web/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1669 + 1670 + "@atproto/repo/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1671 + 1672 + "@atproto/sync/@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 1673 + 1674 + "@atproto/sync/@atproto/common/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1675 + 1676 + "@atproto/sync/@atproto/xrpc-server/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 1677 + 1678 + "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.4", "", { "dependencies": { "@atproto/common": "^0.5.3", "ws": "^8.12.0" } }, "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg=="], 1679 + 1680 + "@atproto/sync/@atproto/xrpc-server/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1681 + 1682 + "@atproto/sync/@atproto/xrpc-server/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 1683 + 1684 + "@atproto/xrpc-server/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1685 + 1686 + "@atproto/xrpc-server/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1687 + 1688 + "@atproto/xrpc-server/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1689 + 1690 + "@atproto/xrpc/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1691 + 1692 + "@atproto/xrpc/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1528 1693 1529 1694 "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], 1530 1695 ··· 1578 1743 1579 1744 "@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1580 1745 1746 + "@wisp/lexicons/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1747 + 1748 + "@wisp/lexicons/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1749 + 1750 + "@wisp/main-app/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 1751 + 1752 + "@wisp/main-app/@atproto/api/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1753 + 1581 1754 "@wisp/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1755 + 1756 + "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1757 + 1758 + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1582 1759 1583 1760 "pino-abstract-transport/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], 1584 1761 ··· 1591 1768 "serve-static/send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 1592 1769 1593 1770 "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], 1594 - 1595 - "tiered-storage/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 1596 1771 1597 1772 "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], 1598 1773 ··· 1646 1821 1647 1822 "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], 1648 1823 1824 + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1825 + 1826 + "wisp-cli/@atproto/oauth-client-node/@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.5", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ=="], 1827 + 1828 + "wisp-cli/@atproto/oauth-client-node/@atproto-labs/handle-resolver-node": ["@atproto-labs/handle-resolver-node@0.1.24", "", { "dependencies": { "@atproto-labs/fetch-node": "0.2.0", "@atproto-labs/handle-resolver": "0.3.5", "@atproto/did": "0.2.4" } }, "sha512-w/zvktigmRQpOLQQclp48tbb2K/2XW8j1szoIpT8T8v6P5dZ8GGVDIEF142xQMX9vWToFqMTu1P2yOuz8e3Ilg=="], 1829 + 1830 + "wisp-cli/@atproto/oauth-client-node/@atproto/did": ["@atproto/did@0.2.4", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-nxNiCgXeo7pfjojq9fpfZxCO0X0xUipNVKW+AHNZwQKiUDt6zYL0VXEfm8HBUwQOCmKvj2pRRSM1Cur+tUWk3g=="], 1831 + 1832 + "wisp-cli/@atproto/oauth-client-node/@atproto/oauth-client": ["@atproto/oauth-client@0.5.13", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/fetch": "0.2.3", "@atproto-labs/handle-resolver": "0.3.5", "@atproto-labs/identity-resolver": "0.3.5", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "@atproto/oauth-types": "0.6.1", "@atproto/xrpc": "0.7.7", "core-js": "^3", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-FLbqHkC7BAVZ90LHVzSxQf+s8ZNIQI4TsDuhYDyzi7lYtktFHDbgd88KuM2ClJFOtGCsSS17yR1Joy925tDSaA=="], 1833 + 1834 + "wisp-cli/@atproto/oauth-client-node/@atproto/oauth-types": ["@atproto/oauth-types@0.6.1", "", { "dependencies": { "@atproto/did": "0.2.4", "@atproto/jwk": "0.6.0", "zod": "^3.23.8" } }, "sha512-3z92GN/6zCq9E2GTTfZM27tWEbvi1qwFSA7KoS5+wqBC4kSsLvnLxmbKH402Z40DfWS4YWqw0DkHsgP0LNFDEA=="], 1835 + 1836 + "wisp-cli/@types/bun/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], 1837 + 1838 + "wisp-hosting-service/@atproto/api/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1839 + 1649 1840 "wisp-hosting-service/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1650 1841 1842 + "wisp-hosting-service/@atproto/lexicon/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1843 + 1844 + "wisp-hosting-service/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1845 + 1846 + "wisp-hosting-service/@atproto/sync/@atproto/common": ["@atproto/common@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.6", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-7KdU8FcIfnwS2kmv7M86pKxtw/fLvPY2bSI1rXpG+AmA8O++IUGlSCujBGzbrPwnQvY/z++f6Le4rdBzu8bFaA=="], 1847 + 1848 + "wisp-hosting-service/@atproto/sync/@atproto/repo": ["@atproto/repo@0.8.11", "", { "dependencies": { "@atproto/common": "^0.5.0", "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4", "@atproto/lexicon": "^0.5.2", "@ipld/dag-cbor": "^7.0.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", "varint": "^6.0.0", "zod": "^3.23.8" } }, "sha512-b/WCu5ITws4ILHoXiZz0XXB5U9C08fUVzkBQDwpnme62GXv8gUaAPL/ttG61OusW09ARwMMQm4vxoP0hTFg+zA=="], 1849 + 1850 + "wisp-hosting-service/@atproto/sync/@atproto/syntax": ["@atproto/syntax@0.4.2", "", {}, "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA=="], 1851 + 1852 + "wisp-hosting-service/@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.2", "", { "dependencies": { "@atproto/common": "^0.5.2", "@atproto/crypto": "^0.4.5", "@atproto/lex-cbor": "0.0.2", "@atproto/lex-data": "0.0.2", "@atproto/lexicon": "^0.5.2", "@atproto/ws-client": "^0.0.3", "@atproto/xrpc": "^0.7.6", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0", "zod": "^3.23.8" } }, "sha512-5AzN8xoV8K1Omn45z6qKH414+B3Z35D536rrScwF3aQGDEdpObAS+vya9UoSg+Gvm2+oOtVEbVri7riLTBW3Vg=="], 1853 + 1854 + "wisp-hosting-service/@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1855 + 1856 + "wisp-hosting-service/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1857 + 1858 + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1859 + 1860 + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1861 + 1862 + "@atproto/sync/@atproto/common/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 1863 + 1864 + "@atproto/sync/@atproto/xrpc-server/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 1865 + 1651 1866 "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 1652 1867 1653 1868 "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 1654 1869 1655 1870 "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], 1871 + 1872 + "wisp-cli/@atproto/oauth-client-node/@atproto-labs/handle-resolver-node/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="], 1873 + 1874 + "wisp-cli/@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/handle-resolver": ["@atproto-labs/handle-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-r3b+plCh/0arN535Aool9gL6yTSbAPDOyReURbA2TWAaeW4vrSJPwR6yYUx0k0vmVPjkZPIdUVd63bG/+VG5MA=="], 1875 + 1876 + "wisp-cli/@atproto/oauth-client-node/@atproto/oauth-client/@atproto-labs/identity-resolver": ["@atproto-labs/identity-resolver@0.3.5", "", { "dependencies": { "@atproto-labs/did-resolver": "0.2.5", "@atproto-labs/handle-resolver": "0.3.5" } }, "sha512-kSxnreUSPhKL77doUbSl/9I6Y9qpkpD7MMJoYFQVU/WG0PB90tzfIb6DNuWsjbU2I5Q91Nzc4Tm4VJMV+OPKGQ=="], 1877 + 1878 + "wisp-cli/@atproto/oauth-client-node/@atproto/oauth-client/@atproto/xrpc": ["@atproto/xrpc@0.7.7", "", { "dependencies": { "@atproto/lexicon": "^0.6.0", "zod": "^3.23.8" } }, "sha512-K1ZyO/BU8JNtXX5dmPp7b5UrkLMMqpsIa/Lrj5D3Su+j1Xwq1m6QJ2XJ1AgjEjkI1v4Muzm7klianLE6XGxtmA=="], 1879 + 1880 + "wisp-cli/@atproto/oauth-client-node/@atproto/oauth-client/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 1881 + 1882 + "wisp-hosting-service/@atproto/sync/@atproto/common/@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-sTr3UCL2SgxEoYVpzJGgWTnNl4TpngP5tMcRyaOvi21Se4m3oR4RDsoVDPz8AS6XphiteRwzwPstquN7aWWMbA=="], 1883 + 1884 + "wisp-hosting-service/@atproto/sync/@atproto/xrpc-server/@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.2", "", { "dependencies": { "@atproto/lex-data": "0.0.2", "multiformats": "^9.9.0", "tslib": "^2.8.1" } }, "sha512-sTr3UCL2SgxEoYVpzJGgWTnNl4TpngP5tMcRyaOvi21Se4m3oR4RDsoVDPz8AS6XphiteRwzwPstquN7aWWMbA=="], 1885 + 1886 + "wisp-hosting-service/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.3", "", { "dependencies": { "@atproto/common": "^0.5.0", "ws": "^8.12.0" } }, "sha512-eKqkTWBk6zuMY+6gs02eT7mS8Btewm8/qaL/Dp00NDCqpNC+U59MWvQsOWT3xkNGfd9Eip+V6VI4oyPvAfsfTA=="], 1887 + 1888 + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1656 1889 } 1657 1890 }
+31 -21
cli/.gitignore
··· 1 - test/ 2 - .DS_STORE 3 - jacquard/ 4 - binaries/ 5 - # Generated by Cargo 6 - # will have compiled files and executables 7 - debug 8 - target 1 + test-site 2 + # dependencies (bun install) 3 + node_modules 9 4 10 - # These are backup files generated by rustfmt 11 - **/*.rs.bk 5 + # output 6 + out 7 + dist 8 + *.tgz 12 9 13 - # MSVC Windows builds of rustc generate these, which store debugging information 14 - *.pdb 10 + # code coverage 11 + coverage 12 + *.lcov 15 13 16 - # Generated by cargo mutants 17 - # Contains mutation testing data 18 - **/mutants.out*/ 14 + # logs 15 + logs 16 + _.log 17 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 19 18 20 - # RustRover 21 - # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 22 - # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 23 - # and can be added to the global gitignore or merged into this file. For a more nuclear 24 - # option (not recommended) you can uncomment the following to ignore the entire idea folder. 25 - #.idea/ 19 + # dotenv environment variable files 20 + .env 21 + .env.development.local 22 + .env.test.local 23 + .env.production.local 24 + .env.local 25 + 26 + # caches 27 + .eslintcache 28 + .cache 29 + *.tsbuildinfo 30 + 31 + # IntelliJ based IDEs 32 + .idea 33 + 34 + # Finder (MacOS) folder config 35 + .DS_Store
+106
cli/CLAUDE.md
··· 1 + 2 + Default to using Bun instead of Node.js. 3 + 4 + - Use `bun <file>` instead of `node <file>` or `ts-node <file>` 5 + - Use `bun test` instead of `jest` or `vitest` 6 + - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild` 7 + - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` 8 + - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>` 9 + - Use `bunx <package> <command>` instead of `npx <package> <command>` 10 + - Bun automatically loads .env, so don't use dotenv. 11 + 12 + ## APIs 13 + 14 + - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. 15 + - `bun:sqlite` for SQLite. Don't use `better-sqlite3`. 16 + - `Bun.redis` for Redis. Don't use `ioredis`. 17 + - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. 18 + - `WebSocket` is built-in. Don't use `ws`. 19 + - Prefer `Bun.file` over `node:fs`'s readFile/writeFile 20 + - Bun.$`ls` instead of execa. 21 + 22 + ## Testing 23 + 24 + Use `bun test` to run tests. 25 + 26 + ```ts#index.test.ts 27 + import { test, expect } from "bun:test"; 28 + 29 + test("hello world", () => { 30 + expect(1).toBe(1); 31 + }); 32 + ``` 33 + 34 + ## Frontend 35 + 36 + Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind. 37 + 38 + Server: 39 + 40 + ```ts#index.ts 41 + import index from "./index.html" 42 + 43 + Bun.serve({ 44 + routes: { 45 + "/": index, 46 + "/api/users/:id": { 47 + GET: (req) => { 48 + return new Response(JSON.stringify({ id: req.params.id })); 49 + }, 50 + }, 51 + }, 52 + // optional websocket support 53 + websocket: { 54 + open: (ws) => { 55 + ws.send("Hello, world!"); 56 + }, 57 + message: (ws, message) => { 58 + ws.send(message); 59 + }, 60 + close: (ws) => { 61 + // handle close 62 + } 63 + }, 64 + development: { 65 + hmr: true, 66 + console: true, 67 + } 68 + }) 69 + ``` 70 + 71 + HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle. 72 + 73 + ```html#index.html 74 + <html> 75 + <body> 76 + <h1>Hello, world!</h1> 77 + <script type="module" src="./frontend.tsx"></script> 78 + </body> 79 + </html> 80 + ``` 81 + 82 + With the following `frontend.tsx`: 83 + 84 + ```tsx#frontend.tsx 85 + import React from "react"; 86 + import { createRoot } from "react-dom/client"; 87 + 88 + // import .css files directly and it works 89 + import './index.css'; 90 + 91 + const root = createRoot(document.body); 92 + 93 + export default function Frontend() { 94 + return <h1>Hello, world!</h1>; 95 + } 96 + 97 + root.render(<Frontend />); 98 + ``` 99 + 100 + Then, run index.ts 101 + 102 + ```sh 103 + bun --hot ./index.ts 104 + ``` 105 + 106 + For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
cli/Cargo.lock rust-cli/Cargo.lock
cli/Cargo.toml rust-cli/Cargo.toml
+6 -332
cli/README.md
··· 1 - # Wisp CLI 2 - 3 - A command-line tool for deploying static sites to your AT Protocol repo to be served on [wisp.place](https://wisp.place), an AT indexer to serve such sites. 4 - 5 - ## Why? 6 - 7 - The PDS serves as a way to verfiably, cryptographically prove that you own your site. That it was you (or at least someone who controls your account) who uploaded it. It is also a manifest of each file in the site to ensure file integrity. Keeping hosting seperate ensures that you could move your site across other servers or even serverless solutions to ensure speedy delievery while keeping it backed by an absolute source of truth being the manifest record and the blobs of each file in your repo. 8 - 9 - ## Features 10 - 11 - - Deploy static sites directly to your AT Protocol repo 12 - - Supports both OAuth and app password authentication 13 - - Preserves directory structure and file integrity 14 - 15 - ## Soon 1 + # cli 16 2 17 - -- Host sites 18 - -- Manage and delete sites 19 - -- Metrics and logs for self hosting. 20 - 21 - ## Installation 22 - 23 - ### From Source 3 + To install dependencies: 24 4 25 5 ```bash 26 - cargo build --release 27 - ``` 28 - 29 - Check out the build scripts for cross complation using nix-shell. 30 - 31 - The binary will be available at `target/release/wisp-cli`. 32 - 33 - ## Usage 34 - 35 - ### Commands 36 - 37 - The CLI supports three main commands: 38 - - **deploy**: Upload a site to your PDS (default command) 39 - - **pull**: Download a site from a PDS to a local directory 40 - - **serve**: Serve a site locally with real-time firehose updates 41 - 42 - ### Basic Deployment 43 - 44 - Deploy the current directory: 45 - 46 - ```bash 47 - wisp-cli nekomimi.pet --path . --site my-site 48 - ``` 49 - 50 - Deploy a specific directory: 51 - 52 - ```bash 53 - wisp-cli alice.bsky.social --path ./dist/ --site my-site 54 - ``` 55 - 56 - Or use the explicit `deploy` subcommand: 57 - 58 - ```bash 59 - wisp-cli deploy alice.bsky.social --path ./dist/ --site my-site 60 - ``` 61 - 62 - ### Pull a Site 63 - 64 - Download a site from a PDS to a local directory: 65 - 66 - ```bash 67 - wisp-cli pull alice.bsky.social --site my-site --path ./downloaded-site 68 - ``` 69 - 70 - This will download all files from the site to the specified directory. 71 - 72 - ### Serve a Site Locally 73 - 74 - Serve a site locally with real-time updates from the firehose: 75 - 76 - ```bash 77 - wisp-cli serve alice.bsky.social --site my-site --path ./site --port 8080 78 - ``` 79 - 80 - This will: 81 - 1. Download the site to the specified path 82 - 2. Start a local server on the specified port (default: 8080) 83 - 3. Watch the firehose for updates and automatically reload files when changed 84 - 85 - ### Authentication Methods 86 - 87 - #### OAuth (Recommended) 88 - 89 - By default, the CLI uses OAuth authentication with a local loopback server: 90 - 91 - ```bash 92 - wisp-cli alice.bsky.social --path ./my-site --site my-site 93 - ``` 94 - 95 - This will: 96 - 1. Open your browser for authentication 97 - 2. Save the session to a file (default: `/tmp/wisp-oauth-session.json`) 98 - 3. Reuse the session for future deployments 99 - 100 - Specify a custom session file location: 101 - 102 - ```bash 103 - wisp-cli alice.bsky.social --path ./my-site --site my-site --store ~/.wisp-session.json 104 - ``` 105 - 106 - #### App Password 107 - 108 - For headless environments or CI/CD, use an app password: 109 - 110 - ```bash 111 - wisp-cli alice.bsky.social --path ./my-site --site my-site --password YOUR_APP_PASSWORD 112 - ``` 113 - 114 - **Note:** When using `--password`, the `--store` option is ignored. 115 - 116 - ## Command-Line Options 117 - 118 - ### Deploy Command 119 - 120 - ``` 121 - wisp-cli [deploy] [OPTIONS] <INPUT> 122 - 123 - Arguments: 124 - <INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL 125 - 126 - Options: 127 - -p, --path <PATH> Path to the directory containing your static site [default: .] 128 - -s, --site <SITE> Site name (defaults to directory name) 129 - --store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json] 130 - --password <PASSWORD> App Password for authentication (alternative to OAuth) 131 - --directory Enable directory listing mode for paths without index files 132 - --spa Enable SPA mode (serve index.html for all routes) 133 - -y, --yes Skip confirmation prompts (automatically accept warnings) 134 - -h, --help Print help 135 - -V, --version Print version 136 - ``` 137 - 138 - ### Pull Command 139 - 140 - ``` 141 - wisp-cli pull [OPTIONS] --site <SITE> <INPUT> 142 - 143 - Arguments: 144 - <INPUT> Handle (e.g., alice.bsky.social) or DID 145 - 146 - Options: 147 - -s, --site <SITE> Site name (record key) 148 - -p, --path <PATH> Output directory for the downloaded site [default: .] 149 - -h, --help Print help 150 - ``` 151 - 152 - ### Serve Command 153 - 154 - ``` 155 - wisp-cli serve [OPTIONS] --site <SITE> <INPUT> 156 - 157 - Arguments: 158 - <INPUT> Handle (e.g., alice.bsky.social) or DID 159 - 160 - Options: 161 - -s, --site <SITE> Site name (record key) 162 - -p, --path <PATH> Output directory for the site files [default: .] 163 - -P, --port <PORT> Port to serve on [default: 8080] 164 - -h, --help Print help 165 - ``` 166 - 167 - ## How It Works 168 - 169 - 1. **Authentication**: Authenticates using OAuth or app password 170 - 2. **File Processing**: 171 - - Recursively walks the directory tree 172 - - Skips hidden files (starting with `.`) 173 - - Detects MIME types automatically 174 - - Compresses files with gzip 175 - - Base64 encodes compressed content 176 - 3. **Upload**: 177 - - Uploads files as blobs to your PDS 178 - - Processes up to 5 files concurrently 179 - - Creates a `place.wisp.fs` record with the site manifest 180 - 4. **Deployment**: Site is immediately available at `https://sites.wisp.place/{did}/{site-name}` 181 - 182 - ## File Processing 183 - 184 - All files are automatically: 185 - 186 - - **Compressed** with gzip (level 9) 187 - - **Base64 encoded** to bypass PDS content sniffing 188 - - **Uploaded** as `application/octet-stream` blobs 189 - - **Stored** with original MIME type metadata 190 - 191 - The hosting service automatically decompresses non HTML/CSS/JS files when serving them. 192 - 193 - ## Limitations 194 - 195 - - **Max file size**: 100MB per file (after compression) (this is a PDS limit, but not enforced by the CLI in case yours is higher) 196 - - **Max file count**: 2000 files 197 - - **Site name** must follow AT Protocol rkey format rules (alphanumeric, hyphens, underscores) 198 - 199 - ## Deploy with CI/CD 200 - 201 - ### GitHub Actions 202 - 203 - ```yaml 204 - name: Deploy to Wisp 205 - on: 206 - push: 207 - branches: [main] 208 - 209 - jobs: 210 - deploy: 211 - runs-on: ubuntu-latest 212 - steps: 213 - - uses: actions/checkout@v3 214 - 215 - - name: Setup Node 216 - uses: actions/setup-node@v3 217 - with: 218 - node-version: '25' 219 - 220 - - name: Install dependencies 221 - run: npm install 222 - 223 - - name: Build site 224 - run: npm run build 225 - 226 - - name: Download Wisp CLI 227 - run: | 228 - curl -L https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 229 - chmod +x wisp-cli 230 - 231 - - name: Deploy to Wisp 232 - env: 233 - WISP_APP_PASSWORD: ${{ secrets.WISP_APP_PASSWORD }} 234 - run: | 235 - ./wisp-cli alice.bsky.social \ 236 - --path ./dist \ 237 - --site my-site \ 238 - --password "$WISP_APP_PASSWORD" 6 + bun install 239 7 ``` 240 8 241 - ### Tangled.org 242 - 243 - ```yaml 244 - when: 245 - - event: ['push'] 246 - branch: ['main'] 247 - - event: ['manual'] 248 - 249 - engine: 'nixery' 250 - 251 - clone: 252 - skip: false 253 - depth: 1 254 - submodules: false 255 - 256 - dependencies: 257 - nixpkgs: 258 - - nodejs 259 - - coreutils 260 - - curl 261 - github:NixOS/nixpkgs/nixpkgs-unstable: 262 - - bun 263 - 264 - environment: 265 - SITE_PATH: 'dist' 266 - SITE_NAME: 'my-site' 267 - WISP_HANDLE: 'your-handle.bsky.social' 268 - 269 - steps: 270 - - name: build site 271 - command: | 272 - export PATH="$HOME/.nix-profile/bin:$PATH" 273 - 274 - # regenerate lockfile 275 - rm package-lock.json bun.lock 276 - bun install @rolldown/binding-linux-arm64-gnu --save-optional 277 - bun install 278 - 279 - # build with vite 280 - bun node_modules/.bin/vite build 281 - 282 - - name: deploy to wisp 283 - command: | 284 - # Download Wisp CLI 285 - curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 286 - chmod +x wisp-cli 287 - 288 - # Deploy to Wisp 289 - ./wisp-cli \ 290 - "$WISP_HANDLE" \ 291 - --path "$SITE_PATH" \ 292 - --site "$SITE_NAME" \ 293 - --password "$WISP_APP_PASSWORD" 294 - ``` 295 - 296 - ### Generic Shell Script 9 + To run: 297 10 298 11 ```bash 299 - # Use app password from environment variable 300 - wisp-cli alice.bsky.social --path ./dist --site my-site --password "$WISP_APP_PASSWORD" 301 - ``` 302 - 303 - ## Output 304 - 305 - Upon successful deployment, you'll see: 306 - 307 - ``` 308 - Deployed site 'my-site': at://did:plc:abc123xyz/place.wisp.fs/my-site 309 - Available at: https://sites.wisp.place/did:plc:abc123xyz/my-site 12 + bun run index.ts 310 13 ``` 311 14 312 - ### Dependencies 313 - 314 - - **jacquard**: AT Protocol client library 315 - - **clap**: Command-line argument parsing 316 - - **tokio**: Async runtime 317 - - **flate2**: Gzip compression 318 - - **base64**: Base64 encoding 319 - - **walkdir**: Directory traversal 320 - - **mime_guess**: MIME type detection 321 - 322 - ## License 323 - 324 - MIT License 325 - 326 - ## Contributing 327 - 328 - Just don't give me entirely claude slop especailly not in the PR description itself. You should be responsible for code you submit and aware of what it even is you're submitting. 329 - 330 - ## Links 331 - 332 - - **Website**: https://wisp.place 333 - - **Main Repository**: https://tangled.org/@nekomimi.pet/wisp.place-monorepo 334 - - **AT Protocol**: https://atproto.com 335 - - **Jacquard Library**: https://tangled.org/@nonbinary.computer/jacquard 336 - 337 - ## Support 338 - 339 - For issues and questions: 340 - - Check the main wisp.place documentation 341 - - Open an issue in the main repository 15 + This project was created using `bun init` in bun v1.3.5. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
cli/build-linux.sh rust-cli/build-linux.sh
cli/build-macos.sh rust-cli/build-macos.sh
+26
cli/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "cli", 7 + "devDependencies": { 8 + "@types/bun": "latest", 9 + }, 10 + "peerDependencies": { 11 + "typescript": "^5", 12 + }, 13 + }, 14 + }, 15 + "packages": { 16 + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], 17 + 18 + "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], 19 + 20 + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], 21 + 22 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 23 + 24 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 25 + } 26 + }
+472
cli/commands/deploy.ts
··· 1 + import type { Agent, BlobRef } from '@atproto/api'; 2 + import type { Directory, Record as FsRecord } from '@wisp/lexicons/types/place/wisp/fs'; 3 + import type { Record as SubfsRecord } from '@wisp/lexicons/types/place/wisp/subfs'; 4 + import type { Record as SettingsRecord } from '@wisp/lexicons/types/place/wisp/settings'; 5 + import { 6 + processUploadedFiles, 7 + updateFileBlobs, 8 + createManifest, 9 + estimateDirectorySize, 10 + findLargeDirectories, 11 + replaceDirectoryWithSubfs, 12 + countFilesInDirectory, 13 + type UploadedFile, 14 + type FileUploadResult 15 + } from '@wisp/fs-utils'; 16 + import { computeCID, extractBlobMap, shouldCompressFile, compressFile, extractSubfsUris } from '@wisp/atproto-utils'; 17 + import { MAX_SITE_SIZE, MAX_FILE_COUNT, MAX_FILE_SIZE } from '@wisp/constants'; 18 + import { readdirSync, statSync, readFileSync, existsSync } from 'fs'; 19 + import { join, relative, basename } from 'path'; 20 + import ignore, { type Ignore } from 'ignore'; 21 + import { lookup } from 'mime-types'; 22 + import { createSpinner, formatBytes, pc } from '../lib/progress.ts'; 23 + 24 + // Constants for manifest splitting 25 + const MAX_MANIFEST_SIZE = 140 * 1024; // 140KB (PDS limit is 150KB) 26 + const FILE_COUNT_THRESHOLD = 250; 27 + const MAX_SUBFS_SIZE = 75 * 1024; // 75KB per subfs 28 + const MAX_CONCURRENT_UPLOADS = 5; 29 + 30 + export interface DeployOptions { 31 + path: string; 32 + site?: string; 33 + directory?: boolean; 34 + spa?: boolean; 35 + yes?: boolean; 36 + } 37 + 38 + interface FileInfo { 39 + path: string; 40 + relativePath: string; 41 + size: number; 42 + } 43 + 44 + // Default ignore patterns 45 + const DEFAULT_IGNORE_PATTERNS = [ 46 + '.git', '.git/**', '.github', '.github/**', '.gitlab', '.gitlab/**', 47 + '.DS_Store', '.wisp-metadata.json', '.env', '.env.*', 48 + 'node_modules', 'node_modules/**', 'Thumbs.db', 'desktop.ini', 49 + '._*', '.Spotlight-V100/**', '.Trashes/**', '.fseventsd/**', 50 + '.cache/**', '.temp/**', '.tmp/**', '__pycache__/**', '*.pyc', 51 + '.venv/**', 'venv/**', '*.swp', '*.swo', '*~', '.tangled/**' 52 + ]; 53 + 54 + function createIgnoreMatcher(siteDir: string): Ignore { 55 + const ig = ignore(); 56 + ig.add(DEFAULT_IGNORE_PATTERNS); 57 + 58 + // Load custom .wispignore if exists 59 + const wispignorePath = join(siteDir, '.wispignore'); 60 + if (existsSync(wispignorePath)) { 61 + const content = readFileSync(wispignorePath, 'utf-8'); 62 + const patterns = content.split('\n') 63 + .map(l => l.trim()) 64 + .filter(l => l && !l.startsWith('#')); 65 + ig.add(patterns); 66 + } 67 + 68 + return ig; 69 + } 70 + 71 + function collectFiles(dir: string, ig: Ignore, baseDir: string): FileInfo[] { 72 + const files: FileInfo[] = []; 73 + 74 + const entries = readdirSync(dir, { withFileTypes: true }); 75 + for (const entry of entries) { 76 + const fullPath = join(dir, entry.name); 77 + const relativePath = relative(baseDir, fullPath); 78 + 79 + if (ig.ignores(relativePath)) continue; 80 + 81 + if (entry.isDirectory()) { 82 + files.push(...collectFiles(fullPath, ig, baseDir)); 83 + } else if (entry.isFile()) { 84 + const stat = statSync(fullPath); 85 + files.push({ 86 + path: fullPath, 87 + relativePath, 88 + size: stat.size 89 + }); 90 + } 91 + } 92 + 93 + return files; 94 + } 95 + 96 + async function fetchExistingManifest( 97 + agent: Agent, 98 + did: string, 99 + rkey: string 100 + ): Promise<{ record: FsRecord; blobMap: Map<string, { blobRef: BlobRef; cid: string }> } | null> { 101 + try { 102 + const response = await agent.com.atproto.repo.getRecord({ 103 + repo: did, 104 + collection: 'place.wisp.fs', 105 + rkey 106 + }); 107 + 108 + const record = response.data.value as FsRecord; 109 + const blobMap = extractBlobMap(record.root); 110 + 111 + // Also fetch any subfs records and merge their blob maps 112 + const subfsUris = extractSubfsUris(record.root); 113 + for (const { uri } of subfsUris) { 114 + try { 115 + const parts = uri.replace('at://', '').split('/'); 116 + const subfsRepo = parts[0]!; 117 + const subfsRkey = parts[2]!; 118 + 119 + const subfsResponse = await agent.com.atproto.repo.getRecord({ 120 + repo: subfsRepo, 121 + collection: 'place.wisp.subfs', 122 + rkey: subfsRkey 123 + }); 124 + 125 + const subfsRecord = subfsResponse.data.value as SubfsRecord; 126 + const subfsBlobMap = extractBlobMap(subfsRecord.root as unknown as Directory); 127 + subfsBlobMap.forEach((value, key) => blobMap.set(key, value)); 128 + } catch { 129 + // Subfs not found, skip 130 + } 131 + } 132 + 133 + return { record, blobMap }; 134 + } catch { 135 + return null; 136 + } 137 + } 138 + 139 + async function uploadBlob( 140 + agent: Agent, 141 + content: Buffer, 142 + mimeType: string, 143 + retries = 3 144 + ): Promise<BlobRef> { 145 + for (let attempt = 0; attempt < retries; attempt++) { 146 + try { 147 + const response = await agent.com.atproto.repo.uploadBlob(content, { 148 + encoding: mimeType 149 + }); 150 + return response.data.blob; 151 + } catch (err: any) { 152 + if (attempt === retries - 1) throw err; 153 + 154 + // Handle rate limits 155 + if (err?.status === 429) { 156 + const delay = Math.pow(2, attempt) * 2000; 157 + await new Promise(r => setTimeout(r, delay)); 158 + } else { 159 + const delay = Math.pow(2, attempt) * 500; 160 + await new Promise(r => setTimeout(r, delay)); 161 + } 162 + } 163 + } 164 + throw new Error('Failed to upload blob after retries'); 165 + } 166 + 167 + async function processAndUploadFiles( 168 + agent: Agent, 169 + files: FileInfo[], 170 + existingBlobMap: Map<string, { blobRef: BlobRef; cid: string }> 171 + ): Promise<{ uploadedFiles: UploadedFile[]; uploadResults: FileUploadResult[]; filePaths: string[] }> { 172 + const spinner = createSpinner(`Processing ${files.length} files...`).start(); 173 + 174 + const uploadedFiles: UploadedFile[] = []; 175 + const uploadResults: FileUploadResult[] = []; 176 + const filePaths: string[] = []; 177 + 178 + let uploaded = 0; 179 + let reused = 0; 180 + 181 + // Process in batches for concurrency 182 + for (let i = 0; i < files.length; i += MAX_CONCURRENT_UPLOADS) { 183 + const batch = files.slice(i, i + MAX_CONCURRENT_UPLOADS); 184 + 185 + await Promise.all(batch.map(async (file) => { 186 + const content = readFileSync(file.path); 187 + const mimeType = lookup(file.relativePath) || 'application/octet-stream'; 188 + const shouldCompress = shouldCompressFile(mimeType, file.relativePath); 189 + 190 + let processedContent: Buffer; 191 + let encoding: 'gzip' | undefined; 192 + let base64Encoded = false; 193 + 194 + if (shouldCompress) { 195 + // Compress with gzip 196 + const compressed = compressFile(content); 197 + // Base64 encode 198 + processedContent = Buffer.from(compressed.toString('base64')); 199 + encoding = 'gzip'; 200 + base64Encoded = true; 201 + } else { 202 + processedContent = content; 203 + } 204 + 205 + // Compute CID 206 + const cid = computeCID(processedContent); 207 + 208 + // Check if blob already exists 209 + const existing = existingBlobMap.get(file.relativePath); 210 + let blobRef: BlobRef; 211 + 212 + if (existing && existing.cid === cid) { 213 + // Reuse existing blob 214 + blobRef = existing.blobRef; 215 + reused++; 216 + } else { 217 + // Upload new blob 218 + blobRef = await uploadBlob(agent, processedContent, 'application/octet-stream'); 219 + uploaded++; 220 + } 221 + 222 + uploadedFiles.push({ 223 + name: file.relativePath, 224 + content: processedContent, 225 + mimeType, 226 + size: processedContent.length, 227 + compressed: shouldCompress, 228 + base64Encoded, 229 + originalMimeType: mimeType 230 + }); 231 + 232 + uploadResults.push({ 233 + hash: cid, 234 + blobRef, 235 + encoding, 236 + mimeType, 237 + base64: base64Encoded || undefined 238 + }); 239 + 240 + filePaths.push(file.relativePath); 241 + 242 + spinner.text = `Processing files: ${uploaded + reused}/${files.length} (${uploaded} uploaded, ${reused} reused)`; 243 + })); 244 + } 245 + 246 + spinner.succeed(`Processed ${files.length} files (${uploaded} uploaded, ${reused} reused)`); 247 + 248 + return { uploadedFiles, uploadResults, filePaths }; 249 + } 250 + 251 + async function createSubfsRecord( 252 + agent: Agent, 253 + did: string, 254 + directory: Directory, 255 + rkey: string 256 + ): Promise<string> { 257 + const record = { 258 + $type: 'place.wisp.subfs' as const, 259 + root: directory as any, // fs Directory and subfs Directory are compatible at runtime 260 + fileCount: countFilesInDirectory(directory), 261 + createdAt: new Date().toISOString() 262 + }; 263 + 264 + await agent.com.atproto.repo.putRecord({ 265 + repo: did, 266 + collection: 'place.wisp.subfs', 267 + rkey, 268 + record 269 + }); 270 + 271 + return `at://${did}/place.wisp.subfs/${rkey}`; 272 + } 273 + 274 + async function splitIntoSubfs( 275 + agent: Agent, 276 + did: string, 277 + directory: Directory, 278 + siteRkey: string 279 + ): Promise<{ directory: Directory; subfsRkeys: string[] }> { 280 + const spinner = createSpinner('Splitting large site into subfs records...').start(); 281 + const subfsRkeys: string[] = []; 282 + let currentDir = directory; 283 + let iteration = 0; 284 + 285 + while ( 286 + (estimateDirectorySize(currentDir) > MAX_MANIFEST_SIZE || 287 + countFilesInDirectory(currentDir) > FILE_COUNT_THRESHOLD) && 288 + iteration < 50 289 + ) { 290 + iteration++; 291 + 292 + // Find largest directories 293 + const largeDirs = findLargeDirectories(currentDir) 294 + .filter(d => d.size > 1000) // Only consider dirs with meaningful size 295 + .sort((a, b) => b.size - a.size); 296 + 297 + if (largeDirs.length === 0) break; 298 + 299 + const largest = largeDirs[0]!; 300 + const subfsRkey = `${siteRkey}-subfs-${iteration}`; 301 + 302 + spinner.text = `Creating subfs ${iteration} for ${largest.path} (${formatBytes(largest.size)})`; 303 + 304 + // Create subfs record for this directory 305 + const subfsUri = await createSubfsRecord(agent, did, largest.directory, subfsRkey); 306 + subfsRkeys.push(subfsRkey); 307 + 308 + // Replace directory with subfs reference 309 + currentDir = replaceDirectoryWithSubfs(currentDir, largest.path, subfsUri); 310 + } 311 + 312 + spinner.succeed(`Created ${subfsRkeys.length} subfs records`); 313 + 314 + return { directory: currentDir, subfsRkeys }; 315 + } 316 + 317 + async function deleteOldSubfsRecords( 318 + agent: Agent, 319 + did: string, 320 + oldRecord: FsRecord | null, 321 + newSubfsRkeys: string[] 322 + ): Promise<void> { 323 + if (!oldRecord) return; 324 + 325 + const oldSubfsUris = extractSubfsUris(oldRecord.root); 326 + const newSubfsSet = new Set(newSubfsRkeys); 327 + 328 + for (const { uri } of oldSubfsUris) { 329 + const parts = uri.replace('at://', '').split('/'); 330 + const rkey = parts[2]; 331 + if (rkey && !newSubfsSet.has(rkey)) { 332 + try { 333 + await agent.com.atproto.repo.deleteRecord({ 334 + repo: did, 335 + collection: 'place.wisp.subfs', 336 + rkey 337 + }); 338 + } catch { 339 + // Ignore deletion errors 340 + } 341 + } 342 + } 343 + } 344 + 345 + export async function deploy( 346 + agent: Agent, 347 + did: string, 348 + options: DeployOptions 349 + ): Promise<{ uri: string; url: string }> { 350 + const siteDir = options.path; 351 + const siteName = options.site || basename(siteDir); 352 + 353 + // Validate site name (AT Protocol rkey format) 354 + if (!/^[a-zA-Z0-9._~:-]{1,512}$/.test(siteName)) { 355 + throw new Error(`Invalid site name: ${siteName}. Must be 1-512 chars of [a-zA-Z0-9._~:-]`); 356 + } 357 + 358 + console.log(pc.cyan(`\nDeploying ${pc.bold(siteName)} from ${siteDir}\n`)); 359 + 360 + // 1. Collect files 361 + const spinner = createSpinner('Scanning directory...').start(); 362 + const ig = createIgnoreMatcher(siteDir); 363 + const files = collectFiles(siteDir, ig, siteDir); 364 + 365 + if (files.length === 0) { 366 + spinner.fail('No files found to deploy'); 367 + throw new Error('No files found'); 368 + } 369 + 370 + const totalSize = files.reduce((sum, f) => sum + f.size, 0); 371 + spinner.succeed(`Found ${files.length} files (${formatBytes(totalSize)})`); 372 + 373 + // 2. Validate limits 374 + if (files.length > MAX_FILE_COUNT) { 375 + console.log(pc.yellow(`\nWarning: Site has ${files.length} files (limit: ${MAX_FILE_COUNT})`)); 376 + console.log(pc.yellow('Site may not be cached by the hosting service.\n')); 377 + } 378 + 379 + if (totalSize > MAX_SITE_SIZE) { 380 + console.log(pc.yellow(`\nWarning: Site is ${formatBytes(totalSize)} (limit: ${formatBytes(MAX_SITE_SIZE)})`)); 381 + console.log(pc.yellow('Site may not be cached by the hosting service.\n')); 382 + } 383 + 384 + // Check individual file sizes 385 + for (const file of files) { 386 + if (file.size > MAX_FILE_SIZE) { 387 + throw new Error(`File ${file.relativePath} exceeds max size (${formatBytes(file.size)} > ${formatBytes(MAX_FILE_SIZE)})`); 388 + } 389 + } 390 + 391 + // 3. Fetch existing manifest for incremental upload 392 + const existingSpinner = createSpinner('Checking for existing site...').start(); 393 + const existing = await fetchExistingManifest(agent, did, siteName); 394 + const existingBlobMap = existing?.blobMap || new Map(); 395 + 396 + if (existing) { 397 + existingSpinner.succeed('Found existing site, will reuse unchanged files'); 398 + } else { 399 + existingSpinner.succeed('No existing site found, uploading all files'); 400 + } 401 + 402 + // 4. Process and upload files 403 + const { uploadedFiles, uploadResults, filePaths } = await processAndUploadFiles( 404 + agent, 405 + files, 406 + existingBlobMap 407 + ); 408 + 409 + // 5. Build directory structure 410 + const { directory: rawDirectory, fileCount } = processUploadedFiles(uploadedFiles); 411 + const successfulPaths = new Set(filePaths); 412 + const directory = updateFileBlobs(rawDirectory, uploadResults, filePaths, '', successfulPaths); 413 + 414 + // 6. Split into subfs if needed 415 + let finalDirectory = directory; 416 + let subfsRkeys: string[] = []; 417 + 418 + if ( 419 + estimateDirectorySize(directory) > MAX_MANIFEST_SIZE || 420 + fileCount > FILE_COUNT_THRESHOLD 421 + ) { 422 + const result = await splitIntoSubfs(agent, did, directory, siteName); 423 + finalDirectory = result.directory; 424 + subfsRkeys = result.subfsRkeys; 425 + } 426 + 427 + // 7. Create manifest 428 + const manifestSpinner = createSpinner('Creating manifest...').start(); 429 + const manifest = createManifest(siteName, finalDirectory, countFilesInDirectory(finalDirectory)); 430 + 431 + await agent.com.atproto.repo.putRecord({ 432 + repo: did, 433 + collection: 'place.wisp.fs', 434 + rkey: siteName, 435 + record: manifest 436 + }); 437 + 438 + manifestSpinner.succeed('Created manifest record'); 439 + 440 + // 8. Clean up old subfs records 441 + await deleteOldSubfsRecords(agent, did, existing?.record || null, subfsRkeys); 442 + 443 + // 9. Create settings if requested 444 + if (options.directory || options.spa) { 445 + const settingsSpinner = createSpinner('Creating settings...').start(); 446 + 447 + const settings: SettingsRecord = { 448 + $type: 'place.wisp.settings', 449 + directoryListing: options.directory || false, 450 + cleanUrls: true, 451 + ...(options.spa && { spaMode: 'index.html' }) 452 + }; 453 + 454 + await agent.com.atproto.repo.putRecord({ 455 + repo: did, 456 + collection: 'place.wisp.settings', 457 + rkey: siteName, 458 + record: settings 459 + }); 460 + 461 + settingsSpinner.succeed('Created settings record'); 462 + } 463 + 464 + const uri = `at://${did}/place.wisp.fs/${siteName}`; 465 + const url = `https://sites.wisp.place/${did}/${siteName}`; 466 + 467 + console.log(pc.green(`\n✓ Deployed successfully!`)); 468 + console.log(pc.dim(` URI: ${uri}`)); 469 + console.log(pc.cyan(` URL: ${url}\n`)); 470 + 471 + return { uri, url }; 472 + }
+401
cli/commands/pull.ts
··· 1 + import { AtpAgent } from '@atproto/api'; 2 + import type { Directory, Entry, File, Record as FsRecord } from '@wisp/lexicons/types/place/wisp/fs'; 3 + import type { Record as SubfsRecord } from '@wisp/lexicons/types/place/wisp/subfs'; 4 + import { extractBlobCid } from '@wisp/atproto-utils'; 5 + import { sanitizePath } from '@wisp/fs-utils'; 6 + import { existsSync, mkdirSync, writeFileSync, rmSync, renameSync } from 'fs'; 7 + import { dirname, join } from 'path'; 8 + import { gunzipSync } from 'zlib'; 9 + import { createSpinner, formatBytes, pc } from '../lib/progress.ts'; 10 + import { loadMetadata, saveMetadata, type SiteMetadata } from '../lib/metadata.ts'; 11 + 12 + const MAX_CONCURRENT_DOWNLOADS = 20; 13 + 14 + export interface PullOptions { 15 + site: string; 16 + path: string; 17 + } 18 + 19 + async function resolveDid(identifier: string): Promise<string | null> { 20 + if (identifier.startsWith('did:')) { 21 + return identifier; 22 + } 23 + 24 + const agent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 25 + const response = await agent.resolveHandle({ handle: identifier }); 26 + return response.data.did; 27 + } 28 + 29 + async function getPdsForDid(did: string): Promise<string | null> { 30 + let doc: { service?: Array<{ id: string; serviceEndpoint?: string }> }; 31 + 32 + if (did.startsWith('did:plc:')) { 33 + const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`); 34 + doc = await res.json() as typeof doc; 35 + } else if (did.startsWith('did:web:')) { 36 + const didParts = did.split(':'); 37 + const domain = didParts[2]; 38 + const pathParts = didParts.slice(3); 39 + const url = pathParts.length === 0 40 + ? `https://${domain}/.well-known/did.json` 41 + : `https://${domain}/${pathParts.join('/')}/did.json`; 42 + const res = await fetch(url); 43 + doc = await res.json() as typeof doc; 44 + } else { 45 + return null; 46 + } 47 + 48 + const services = doc.service || []; 49 + const pdsService = services.find((s) => s.id === '#atproto_pds'); 50 + return pdsService?.serviceEndpoint || null; 51 + } 52 + 53 + async function fetchRecord(pdsEndpoint: string, did: string, collection: string, rkey: string): Promise<any> { 54 + const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}`; 55 + const res = await fetch(url); 56 + if (!res.ok) { 57 + throw new Error(`Failed to fetch record: ${res.status}`); 58 + } 59 + return res.json(); 60 + } 61 + 62 + function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { 63 + const uris: Array<{ uri: string; path: string }> = []; 64 + 65 + for (const entry of directory.entries) { 66 + const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 67 + 68 + if ('type' in entry.node) { 69 + if (entry.node.type === 'subfs') { 70 + const subfsNode = entry.node as any; 71 + if (subfsNode.subject) { 72 + uris.push({ uri: subfsNode.subject, path: fullPath }); 73 + } 74 + } else if (entry.node.type === 'directory') { 75 + const subUris = extractSubfsUris(entry.node as Directory, fullPath); 76 + uris.push(...subUris); 77 + } 78 + } 79 + } 80 + 81 + return uris; 82 + } 83 + 84 + async function expandSubfsNodes( 85 + directory: Directory, 86 + pdsEndpoint: string, 87 + depth: number = 0, 88 + subfsCache: Map<string, SubfsRecord | null> = new Map() 89 + ): Promise<Directory> { 90 + const MAX_DEPTH = 10; 91 + 92 + if (depth >= MAX_DEPTH) { 93 + console.warn('Max subfs expansion depth reached'); 94 + return directory; 95 + } 96 + 97 + const subfsUris = extractSubfsUris(directory); 98 + if (subfsUris.length === 0) { 99 + return directory; 100 + } 101 + 102 + // Fetch uncached subfs records 103 + const uncachedUris = subfsUris.filter(({ uri }) => !subfsCache.has(uri)); 104 + 105 + if (uncachedUris.length > 0) { 106 + await Promise.all(uncachedUris.map(async ({ uri }) => { 107 + try { 108 + const parts = uri.replace('at://', '').split('/'); 109 + const did = parts[0]!; 110 + const collection = parts[1]!; 111 + const rkey = parts[2]!; 112 + 113 + const data = await fetchRecord(pdsEndpoint, did, collection, rkey); 114 + subfsCache.set(uri, data.value as SubfsRecord); 115 + } catch { 116 + subfsCache.set(uri, null); 117 + } 118 + })); 119 + } 120 + 121 + // Build map of path -> entries 122 + const subfsMap = new Map<string, Entry[]>(); 123 + for (const { uri, path } of subfsUris) { 124 + const record = subfsCache.get(uri); 125 + if (record?.root?.entries) { 126 + subfsMap.set(path, record.root.entries as unknown as Entry[]); 127 + } 128 + } 129 + 130 + // Replace subfs nodes with their content 131 + function replaceSubfsInEntries(entries: Entry[], currentPath: string = ''): Entry[] { 132 + const result: Entry[] = []; 133 + 134 + for (const entry of entries) { 135 + const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 136 + const node = entry.node; 137 + 138 + if ('type' in node && node.type === 'subfs') { 139 + const subfsNode = node as any; 140 + const isFlat = subfsNode.flat !== false; 141 + const subfsEntries = subfsMap.get(fullPath); 142 + 143 + if (subfsEntries) { 144 + if (isFlat) { 145 + const processedEntries = replaceSubfsInEntries(subfsEntries, currentPath); 146 + result.push(...processedEntries); 147 + } else { 148 + const processedEntries = replaceSubfsInEntries(subfsEntries, fullPath); 149 + result.push({ 150 + name: entry.name, 151 + node: { 152 + type: 'directory', 153 + entries: processedEntries 154 + } as any 155 + }); 156 + } 157 + } else { 158 + result.push(entry); 159 + } 160 + } else if ('type' in node && node.type === 'directory' && 'entries' in node) { 161 + result.push({ 162 + ...entry, 163 + node: { 164 + ...node, 165 + entries: replaceSubfsInEntries(node.entries, fullPath) 166 + } 167 + }); 168 + } else { 169 + result.push(entry); 170 + } 171 + } 172 + 173 + return result; 174 + } 175 + 176 + const partiallyExpanded = { 177 + ...directory, 178 + entries: replaceSubfsInEntries(directory.entries) 179 + }; 180 + 181 + return expandSubfsNodes(partiallyExpanded, pdsEndpoint, depth + 1, subfsCache); 182 + } 183 + 184 + interface FileToDownload { 185 + path: string; 186 + cid: string; 187 + encoding?: 'gzip'; 188 + mimeType?: string; 189 + base64?: boolean; 190 + } 191 + 192 + function collectFiles( 193 + entries: Entry[], 194 + pathPrefix: string, 195 + existingCids: Record<string, string> 196 + ): { toDownload: FileToDownload[]; toSkip: number } { 197 + const toDownload: FileToDownload[] = []; 198 + let toSkip = 0; 199 + 200 + function collect(entries: Entry[], currentPath: string) { 201 + for (const entry of entries) { 202 + const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; 203 + const node = entry.node; 204 + 205 + if ('type' in node && node.type === 'directory' && 'entries' in node) { 206 + collect(node.entries, fullPath); 207 + } else if ('type' in node && node.type === 'file' && 'blob' in node) { 208 + const fileNode = node as File; 209 + const cid = extractBlobCid(fileNode.blob); 210 + 211 + if (!cid) continue; 212 + 213 + if (existingCids[fullPath] === cid) { 214 + toSkip++; 215 + } else { 216 + toDownload.push({ 217 + path: fullPath, 218 + cid, 219 + encoding: fileNode.encoding, 220 + mimeType: fileNode.mimeType, 221 + base64: fileNode.base64 222 + }); 223 + } 224 + } 225 + } 226 + } 227 + 228 + collect(entries, pathPrefix); 229 + return { toDownload, toSkip }; 230 + } 231 + 232 + async function downloadBlob( 233 + pdsEndpoint: string, 234 + did: string, 235 + file: FileToDownload 236 + ): Promise<Buffer> { 237 + const url = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(file.cid)}`; 238 + const res = await fetch(url); 239 + 240 + if (!res.ok) { 241 + throw new Error(`Failed to download blob ${file.cid}: ${res.status}`); 242 + } 243 + 244 + let content = Buffer.from(await res.arrayBuffer()); 245 + 246 + // Decode base64 if needed 247 + if (file.base64) { 248 + const base64String = content.toString('utf-8'); 249 + content = Buffer.from(base64String, 'base64'); 250 + } 251 + 252 + // Decompress gzip 253 + if (file.encoding === 'gzip' && content.length >= 2 && content[0] === 0x1f && content[1] === 0x8b) { 254 + try { 255 + content = gunzipSync(content); 256 + } catch { 257 + // Keep original content if decompression fails 258 + } 259 + } 260 + 261 + return content; 262 + } 263 + 264 + export async function pull( 265 + identifier: string, 266 + options: PullOptions 267 + ): Promise<void> { 268 + const { site, path: outputPath } = options; 269 + 270 + console.log(pc.cyan(`\nPulling ${pc.bold(site)} from ${identifier}\n`)); 271 + 272 + // 1. Resolve DID 273 + const spinner = createSpinner('Resolving identity...').start(); 274 + const did = await resolveDid(identifier); 275 + 276 + if (!did) { 277 + spinner.fail('Failed to resolve identity'); 278 + throw new Error(`Could not resolve: ${identifier}`); 279 + } 280 + 281 + spinner.succeed(`Resolved to ${did}`); 282 + 283 + // 2. Get PDS endpoint 284 + const pdsSpinner = createSpinner('Getting PDS endpoint...').start(); 285 + const pdsEndpoint = await getPdsForDid(did); 286 + 287 + if (!pdsEndpoint) { 288 + pdsSpinner.fail('Failed to get PDS endpoint'); 289 + throw new Error(`Could not get PDS for: ${did}`); 290 + } 291 + 292 + pdsSpinner.succeed(`PDS: ${pdsEndpoint}`); 293 + 294 + // 3. Fetch site record 295 + const recordSpinner = createSpinner('Fetching site record...').start(); 296 + let recordData; 297 + 298 + try { 299 + recordData = await fetchRecord(pdsEndpoint, did, 'place.wisp.fs', site); 300 + } catch { 301 + recordSpinner.fail('Site not found'); 302 + throw new Error(`Site not found: ${site}`); 303 + } 304 + 305 + const record = recordData.value as FsRecord; 306 + const recordCid = recordData.cid || ''; 307 + recordSpinner.succeed('Fetched site record'); 308 + 309 + // 4. Expand subfs nodes 310 + const expandSpinner = createSpinner('Expanding subfs nodes...').start(); 311 + const expandedRoot = await expandSubfsNodes(record.root, pdsEndpoint); 312 + expandSpinner.succeed('Expanded subfs nodes'); 313 + 314 + // 5. Load existing metadata for incremental updates 315 + const existingMetadata = loadMetadata(outputPath); 316 + const existingCids = existingMetadata?.fileCids || {}; 317 + 318 + // 6. Collect files to download 319 + const { toDownload, toSkip } = collectFiles(expandedRoot.entries, '', existingCids); 320 + 321 + console.log(pc.dim(`Files to download: ${toDownload.length}, unchanged: ${toSkip}`)); 322 + 323 + if (toDownload.length === 0 && toSkip > 0) { 324 + console.log(pc.green('\n✓ Site is already up to date\n')); 325 + return; 326 + } 327 + 328 + // 7. Create temp directory 329 + const tempDir = `${outputPath}.tmp-${Date.now()}`; 330 + mkdirSync(tempDir, { recursive: true }); 331 + 332 + // 8. Download files 333 + const downloadSpinner = createSpinner(`Downloading ${toDownload.length} files...`).start(); 334 + const newFileCids: Record<string, string> = { ...existingCids }; 335 + let downloaded = 0; 336 + 337 + try { 338 + for (let i = 0; i < toDownload.length; i += MAX_CONCURRENT_DOWNLOADS) { 339 + const batch = toDownload.slice(i, i + MAX_CONCURRENT_DOWNLOADS); 340 + 341 + await Promise.all(batch.map(async (file) => { 342 + const content = await downloadBlob(pdsEndpoint, did, file); 343 + const filePath = join(tempDir, sanitizePath(file.path)); 344 + 345 + mkdirSync(dirname(filePath), { recursive: true }); 346 + writeFileSync(filePath, content); 347 + 348 + newFileCids[file.path] = file.cid; 349 + downloaded++; 350 + downloadSpinner.text = `Downloading files: ${downloaded}/${toDownload.length}`; 351 + })); 352 + } 353 + 354 + downloadSpinner.succeed(`Downloaded ${downloaded} files`); 355 + 356 + // 9. Copy unchanged files from existing directory 357 + if (toSkip > 0 && existsSync(outputPath)) { 358 + const copySpinner = createSpinner(`Copying ${toSkip} unchanged files...`).start(); 359 + 360 + for (const [filePath, cid] of Object.entries(existingCids)) { 361 + if (!toDownload.find(f => f.path === filePath)) { 362 + const srcPath = join(outputPath, sanitizePath(filePath)); 363 + const destPath = join(tempDir, sanitizePath(filePath)); 364 + 365 + if (existsSync(srcPath)) { 366 + mkdirSync(dirname(destPath), { recursive: true }); 367 + const content = Bun.file(srcPath).arrayBuffer(); 368 + writeFileSync(destPath, Buffer.from(await content)); 369 + } 370 + } 371 + } 372 + 373 + copySpinner.succeed(`Copied ${toSkip} unchanged files`); 374 + } 375 + 376 + // 10. Atomic replace 377 + if (existsSync(outputPath)) { 378 + const backupPath = `${outputPath}.backup-${Date.now()}`; 379 + renameSync(outputPath, backupPath); 380 + renameSync(tempDir, outputPath); 381 + rmSync(backupPath, { recursive: true, force: true }); 382 + } else { 383 + renameSync(tempDir, outputPath); 384 + } 385 + 386 + // 11. Save metadata 387 + const metadata: SiteMetadata = { 388 + recordCid, 389 + fileCids: newFileCids, 390 + lastSync: Date.now() 391 + }; 392 + saveMetadata(outputPath, metadata); 393 + 394 + console.log(pc.green(`\n✓ Pulled ${site} to ${outputPath}\n`)); 395 + 396 + } catch (err) { 397 + // Cleanup temp dir on error 398 + rmSync(tempDir, { recursive: true, force: true }); 399 + throw err; 400 + } 401 + }
+367
cli/commands/serve.ts
··· 1 + import { AtpAgent } from '@atproto/api'; 2 + import { Firehose } from '@atproto/sync'; 3 + import type { Record as SettingsRecord } from '@wisp/lexicons/types/place/wisp/settings'; 4 + import { existsSync, readFileSync, statSync, readdirSync } from 'fs'; 5 + import { join, extname } from 'path'; 6 + import { lookup } from 'mime-types'; 7 + import { pull } from './pull.ts'; 8 + import { createSpinner, pc } from '../lib/progress.ts'; 9 + import { parseRedirectsFile, matchRedirectRule, parseQueryString, type RedirectRule } from '../lib/redirects.ts'; 10 + 11 + export interface ServeOptions { 12 + site: string; 13 + path: string; 14 + port: number; 15 + } 16 + 17 + interface SiteState { 18 + did: string; 19 + rkey: string; 20 + pdsEndpoint: string; 21 + siteDir: string; 22 + settings: SettingsRecord | null; 23 + redirectRules: RedirectRule[]; 24 + } 25 + 26 + async function resolveDid(identifier: string): Promise<string | null> { 27 + if (identifier.startsWith('did:')) { 28 + return identifier; 29 + } 30 + const agent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 31 + const response = await agent.resolveHandle({ handle: identifier }); 32 + return response.data.did; 33 + } 34 + 35 + async function getPdsForDid(did: string): Promise<string | null> { 36 + let doc: { service?: Array<{ id: string; serviceEndpoint?: string }> }; 37 + 38 + if (did.startsWith('did:plc:')) { 39 + const res = await fetch(`https://plc.directory/${encodeURIComponent(did)}`); 40 + doc = await res.json() as typeof doc; 41 + } else if (did.startsWith('did:web:')) { 42 + const didParts = did.split(':'); 43 + const domain = didParts[2]; 44 + const pathParts = didParts.slice(3); 45 + const url = pathParts.length === 0 46 + ? `https://${domain}/.well-known/did.json` 47 + : `https://${domain}/${pathParts.join('/')}/did.json`; 48 + const res = await fetch(url); 49 + doc = await res.json() as typeof doc; 50 + } else { 51 + return null; 52 + } 53 + 54 + const services = doc.service || []; 55 + const pdsService = services.find((s) => s.id === '#atproto_pds'); 56 + return pdsService?.serviceEndpoint || null; 57 + } 58 + 59 + async function fetchSettings(pdsEndpoint: string, did: string, rkey: string): Promise<SettingsRecord | null> { 60 + try { 61 + const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=place.wisp.settings&rkey=${encodeURIComponent(rkey)}`; 62 + const res = await fetch(url); 63 + if (!res.ok) return null; 64 + const data = await res.json() as { value: SettingsRecord }; 65 + return data.value; 66 + } catch { 67 + return null; 68 + } 69 + } 70 + 71 + function loadRedirectRules(siteDir: string): RedirectRule[] { 72 + const redirectsPath = join(siteDir, '_redirects'); 73 + if (!existsSync(redirectsPath)) { 74 + return []; 75 + } 76 + try { 77 + const content = readFileSync(redirectsPath, 'utf-8'); 78 + return parseRedirectsFile(content); 79 + } catch { 80 + return []; 81 + } 82 + } 83 + 84 + function getIndexFiles(settings: SettingsRecord | null): string[] { 85 + return settings?.indexFiles || ['index.html', 'index.htm']; 86 + } 87 + 88 + function generateDirectoryListing(dirPath: string, urlPath: string): string { 89 + const entries = readdirSync(dirPath, { withFileTypes: true }); 90 + 91 + const items = entries 92 + .filter(e => !e.name.startsWith('.')) 93 + .sort((a, b) => { 94 + if (a.isDirectory() && !b.isDirectory()) return -1; 95 + if (!a.isDirectory() && b.isDirectory()) return 1; 96 + return a.name.localeCompare(b.name); 97 + }) 98 + .map(entry => { 99 + const isDir = entry.isDirectory(); 100 + const name = isDir ? `${entry.name}/` : entry.name; 101 + const href = urlPath === '/' ? `/${entry.name}` : `${urlPath}/${entry.name}`; 102 + return `<li><a href="${href}">${name}</a></li>`; 103 + }); 104 + 105 + const parentLink = urlPath !== '/' 106 + ? `<li><a href="${urlPath.split('/').slice(0, -1).join('/') || '/'}">..</a></li>` 107 + : ''; 108 + 109 + return `<!DOCTYPE html> 110 + <html> 111 + <head><title>Index of ${urlPath}</title> 112 + <style>body{font-family:system-ui;padding:2rem}ul{list-style:none;padding:0}li{padding:0.25rem 0}a{color:#0066cc}</style> 113 + </head> 114 + <body> 115 + <h1>Index of ${urlPath}</h1> 116 + <ul>${parentLink}${items.join('')}</ul> 117 + </body> 118 + </html>`; 119 + } 120 + 121 + function generate404Page(): string { 122 + return `<!DOCTYPE html> 123 + <html> 124 + <head><title>404 Not Found</title> 125 + <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0} 126 + .container{text-align:center}h1{font-size:4rem;margin:0;color:#666}p{color:#999}</style> 127 + </head> 128 + <body> 129 + <div class="container"><h1>404</h1><p>Page not found</p></div> 130 + </body> 131 + </html>`; 132 + } 133 + 134 + function serveFile(filePath: string): Response { 135 + const content = readFileSync(filePath); 136 + const mimeType = lookup(filePath) || 'application/octet-stream'; 137 + 138 + return new Response(content, { 139 + headers: { 140 + 'Content-Type': mimeType, 141 + 'Cache-Control': 'no-cache' 142 + } 143 + }); 144 + } 145 + 146 + function handleRequest(req: Request, state: SiteState): Response { 147 + const url = new URL(req.url); 148 + let urlPath = decodeURIComponent(url.pathname); 149 + 150 + // Prevent directory traversal 151 + if (urlPath.includes('..')) { 152 + return new Response('Forbidden', { status: 403 }); 153 + } 154 + 155 + // Check redirect rules first 156 + const queryParams = parseQueryString(url.search); 157 + const redirectMatch = matchRedirectRule(urlPath, state.redirectRules, { queryParams }); 158 + 159 + if (redirectMatch) { 160 + if (redirectMatch.status === 200) { 161 + // Rewrite - serve the target path instead 162 + urlPath = redirectMatch.targetPath; 163 + } else if ([301, 302, 307, 308].includes(redirectMatch.status)) { 164 + // Redirect 165 + return new Response(null, { 166 + status: redirectMatch.status, 167 + headers: { Location: redirectMatch.targetPath } 168 + }); 169 + } else if (redirectMatch.status === 404) { 170 + // Custom 404 171 + const custom404Path = join(state.siteDir, redirectMatch.targetPath); 172 + if (existsSync(custom404Path)) { 173 + const content = readFileSync(custom404Path); 174 + return new Response(content, { 175 + status: 404, 176 + headers: { 'Content-Type': 'text/html' } 177 + }); 178 + } 179 + } 180 + } 181 + 182 + // Resolve file path 183 + let filePath = join(state.siteDir, urlPath); 184 + 185 + // Check if it's a directory 186 + if (existsSync(filePath) && statSync(filePath).isDirectory()) { 187 + // Try index files 188 + const indexFiles = getIndexFiles(state.settings); 189 + for (const indexFile of indexFiles) { 190 + const indexPath = join(filePath, indexFile); 191 + if (existsSync(indexPath)) { 192 + return serveFile(indexPath); 193 + } 194 + } 195 + 196 + // Directory listing if enabled 197 + if (state.settings?.directoryListing) { 198 + const html = generateDirectoryListing(filePath, urlPath); 199 + return new Response(html, { 200 + headers: { 'Content-Type': 'text/html' } 201 + }); 202 + } 203 + } 204 + 205 + // Try exact file 206 + if (existsSync(filePath) && statSync(filePath).isFile()) { 207 + return serveFile(filePath); 208 + } 209 + 210 + // Clean URLs - try adding .html 211 + if (state.settings?.cleanUrls !== false) { 212 + const htmlPath = `${filePath}.html`; 213 + if (existsSync(htmlPath) && statSync(htmlPath).isFile()) { 214 + return serveFile(htmlPath); 215 + } 216 + 217 + // Try /path/index.html 218 + const indexPath = join(filePath, 'index.html'); 219 + if (existsSync(indexPath) && statSync(indexPath).isFile()) { 220 + return serveFile(indexPath); 221 + } 222 + } 223 + 224 + // SPA mode - serve index.html for all routes 225 + if (state.settings?.spaMode) { 226 + const spaPath = join(state.siteDir, state.settings.spaMode); 227 + if (existsSync(spaPath)) { 228 + return serveFile(spaPath); 229 + } 230 + } 231 + 232 + // Custom 404 233 + if (state.settings?.custom404) { 234 + const custom404Path = join(state.siteDir, state.settings.custom404); 235 + if (existsSync(custom404Path)) { 236 + const content = readFileSync(custom404Path); 237 + return new Response(content, { 238 + status: 404, 239 + headers: { 'Content-Type': 'text/html' } 240 + }); 241 + } 242 + } 243 + 244 + // Auto-detect 404.html 245 + const auto404Paths = ['404.html', 'not_found.html']; 246 + for (const notFoundFile of auto404Paths) { 247 + const notFoundPath = join(state.siteDir, notFoundFile); 248 + if (existsSync(notFoundPath)) { 249 + const content = readFileSync(notFoundPath); 250 + return new Response(content, { 251 + status: 404, 252 + headers: { 'Content-Type': 'text/html' } 253 + }); 254 + } 255 + } 256 + 257 + // Default 404 258 + return new Response(generate404Page(), { 259 + status: 404, 260 + headers: { 'Content-Type': 'text/html' } 261 + }); 262 + } 263 + 264 + export async function serve( 265 + identifier: string, 266 + options: ServeOptions 267 + ): Promise<void> { 268 + const { site, path: outputPath, port } = options; 269 + 270 + console.log(pc.cyan(`\nServing ${pc.bold(site)} from ${identifier}\n`)); 271 + 272 + // 1. Resolve DID 273 + const spinner = createSpinner('Resolving identity...').start(); 274 + const did = await resolveDid(identifier); 275 + 276 + if (!did) { 277 + spinner.fail('Failed to resolve identity'); 278 + throw new Error(`Could not resolve: ${identifier}`); 279 + } 280 + 281 + spinner.succeed(`Resolved to ${did}`); 282 + 283 + // 2. Get PDS endpoint 284 + const pdsSpinner = createSpinner('Getting PDS endpoint...').start(); 285 + const pdsEndpoint = await getPdsForDid(did); 286 + 287 + if (!pdsEndpoint) { 288 + pdsSpinner.fail('Failed to get PDS endpoint'); 289 + throw new Error(`Could not get PDS for: ${did}`); 290 + } 291 + 292 + pdsSpinner.succeed(`PDS: ${pdsEndpoint}`); 293 + 294 + // 3. Initial pull 295 + await pull(identifier, { site, path: outputPath }); 296 + 297 + // 4. Load settings and redirects 298 + const settings = await fetchSettings(pdsEndpoint, did, site); 299 + const redirectRules = loadRedirectRules(outputPath); 300 + 301 + const state: SiteState = { 302 + did, 303 + rkey: site, 304 + pdsEndpoint, 305 + siteDir: outputPath, 306 + settings, 307 + redirectRules 308 + }; 309 + 310 + // 5. Start HTTP server 311 + const server = Bun.serve({ 312 + port, 313 + fetch(req) { 314 + return handleRequest(req, state); 315 + } 316 + }); 317 + 318 + console.log(pc.green(`\n✓ Server running at http://localhost:${port}\n`)); 319 + console.log(pc.dim('Watching for updates via firehose...\n')); 320 + 321 + // 6. Connect to firehose for live updates 322 + const firehose = new Firehose({ 323 + service: pdsEndpoint.replace('https://', 'wss://').replace('http://', 'ws://'), 324 + handleEvent: async (evt: any) => { 325 + if (evt.event !== 'commit') return; 326 + 327 + const commit = evt.commit; 328 + if (!commit || commit.repo !== did) return; 329 + 330 + for (const op of commit.ops || []) { 331 + const collection = op.path?.split('/')[0]; 332 + const rkey = op.path?.split('/')[1]; 333 + 334 + if (rkey !== site) continue; 335 + 336 + if (collection === 'place.wisp.fs') { 337 + console.log(pc.yellow('\nSite updated, re-pulling...\n')); 338 + await pull(identifier, { site, path: outputPath }); 339 + 340 + // Reload redirects 341 + state.redirectRules = loadRedirectRules(outputPath); 342 + console.log(pc.green('✓ Site reloaded\n')); 343 + } else if (collection === 'place.wisp.settings') { 344 + console.log(pc.yellow('\nSettings updated...\n')); 345 + state.settings = await fetchSettings(pdsEndpoint, did, site); 346 + console.log(pc.green('✓ Settings reloaded\n')); 347 + } 348 + } 349 + }, 350 + onError: (err: Error) => { 351 + console.error(pc.red('Firehose error:'), err.message); 352 + } 353 + }); 354 + 355 + firehose.start(); 356 + 357 + // Handle shutdown 358 + process.on('SIGINT', () => { 359 + console.log(pc.dim('\nShutting down...')); 360 + firehose.destroy(); 361 + server.stop(); 362 + process.exit(0); 363 + }); 364 + 365 + // Keep process alive 366 + await new Promise(() => {}); 367 + }
cli/crates/lexicons/Cargo.toml rust-cli/crates/lexicons/Cargo.toml
cli/crates/lexicons/src/builder_types.rs rust-cli/crates/lexicons/src/builder_types.rs
cli/crates/lexicons/src/lib.rs rust-cli/crates/lexicons/src/lib.rs
cli/crates/lexicons/src/place_wisp.rs rust-cli/crates/lexicons/src/place_wisp.rs
cli/crates/lexicons/src/place_wisp/fs.rs rust-cli/crates/lexicons/src/place_wisp/fs.rs
cli/crates/lexicons/src/place_wisp/settings.rs rust-cli/crates/lexicons/src/place_wisp/settings.rs
cli/crates/lexicons/src/place_wisp/subfs.rs rust-cli/crates/lexicons/src/place_wisp/subfs.rs
cli/default.nix rust-cli/default.nix
cli/flake.lock rust-cli/flake.lock
cli/flake.nix rust-cli/flake.nix
+94
cli/index.ts
··· 1 + #!/usr/bin/env bun 2 + import { Command } from 'commander'; 3 + import { authenticate, clearSessions } from './lib/auth.ts'; 4 + import { deploy } from './commands/deploy.ts'; 5 + import { pull } from './commands/pull.ts'; 6 + import { serve } from './commands/serve.ts'; 7 + import { pc } from './lib/progress.ts'; 8 + 9 + const program = new Command(); 10 + 11 + program 12 + .name('wisp-cli') 13 + .description('CLI for wisp.place - deploy static sites to the AT Protocol') 14 + .version('1.0.0'); 15 + 16 + // Deploy command (default) 17 + program 18 + .command('deploy <handle>', { isDefault: true }) 19 + .description('Deploy a static site to wisp.place') 20 + .option('-p, --path <path>', 'Directory to deploy', '.') 21 + .option('-s, --site <name>', 'Site name (defaults to directory name)') 22 + .option('--directory', 'Enable directory listing') 23 + .option('--spa', 'Enable SPA mode (serve index.html for all routes)') 24 + .option('--password <password>', 'App password for headless authentication') 25 + .option('--store <path>', 'OAuth session store path') 26 + .option('-y, --yes', 'Skip confirmation prompts') 27 + .action(async (handle: string, options) => { 28 + try { 29 + const { agent, did } = await authenticate(handle, { 30 + appPassword: options.password, 31 + storePath: options.store 32 + }); 33 + 34 + await deploy(agent, did, { 35 + path: options.path, 36 + site: options.site, 37 + directory: options.directory, 38 + spa: options.spa, 39 + yes: options.yes 40 + }); 41 + } catch (err: any) { 42 + console.error(pc.red(`\nError: ${err.message}\n`)); 43 + process.exit(1); 44 + } 45 + }); 46 + 47 + // Pull command 48 + program 49 + .command('pull <handle>') 50 + .description('Download a site from wisp.place to a local directory') 51 + .requiredOption('-s, --site <name>', 'Site name to pull') 52 + .option('-p, --path <path>', 'Output directory', '.') 53 + .action(async (handle: string, options) => { 54 + try { 55 + await pull(handle, { 56 + site: options.site, 57 + path: options.path 58 + }); 59 + } catch (err: any) { 60 + console.error(pc.red(`\nError: ${err.message}\n`)); 61 + process.exit(1); 62 + } 63 + }); 64 + 65 + // Serve command 66 + program 67 + .command('serve <handle>') 68 + .description('Serve a site locally with live updates from firehose') 69 + .requiredOption('-s, --site <name>', 'Site name to serve') 70 + .option('-p, --path <path>', 'Local directory to cache site', '.wisp-serve') 71 + .option('-P, --port <port>', 'Port to serve on', '8080') 72 + .action(async (handle: string, options) => { 73 + try { 74 + await serve(handle, { 75 + site: options.site, 76 + path: options.path, 77 + port: parseInt(options.port, 10) 78 + }); 79 + } catch (err: any) { 80 + console.error(pc.red(`\nError: ${err.message}\n`)); 81 + process.exit(1); 82 + } 83 + }); 84 + 85 + // Logout command 86 + program 87 + .command('logout') 88 + .description('Clear stored OAuth sessions') 89 + .option('--store <path>', 'OAuth session store path') 90 + .action((options) => { 91 + clearSessions(options.store); 92 + }); 93 + 94 + program.parse();
+258
cli/lib/auth.ts
··· 1 + import { NodeOAuthClient, type NodeSavedSession, type NodeSavedState, type NodeSavedStateStore, type NodeSavedSessionStore } from "@atproto/oauth-client-node"; 2 + import { Agent, CredentialSession } from "@atproto/api"; 3 + import open from "open"; 4 + import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs"; 5 + import { dirname, join } from "path"; 6 + import { homedir } from "os"; 7 + 8 + // OAuth scope for CLI 9 + const OAUTH_SCOPE = 'atproto repo:place.wisp.fs repo:place.wisp.subfs repo:place.wisp.settings blob:*/*'; 10 + 11 + // Default session store path 12 + const DEFAULT_STORE_PATH = join(homedir(), '.wisp', 'oauth-session.json'); 13 + 14 + // Loopback server config 15 + const LOOPBACK_PORT = 4000; 16 + const LOOPBACK_HOST = '127.0.0.1'; 17 + 18 + interface StoredData { 19 + states: Record<string, NodeSavedState>; 20 + sessions: Record<string, NodeSavedSession>; 21 + } 22 + 23 + function ensureDir(filePath: string) { 24 + const dir = dirname(filePath); 25 + if (!existsSync(dir)) { 26 + mkdirSync(dir, { recursive: true }); 27 + } 28 + } 29 + 30 + function loadStore(storePath: string): StoredData { 31 + if (!existsSync(storePath)) { 32 + return { states: {}, sessions: {} }; 33 + } 34 + try { 35 + const content = readFileSync(storePath, 'utf-8'); 36 + return JSON.parse(content); 37 + } catch { 38 + return { states: {}, sessions: {} }; 39 + } 40 + } 41 + 42 + function saveStore(storePath: string, data: StoredData) { 43 + ensureDir(storePath); 44 + writeFileSync(storePath, JSON.stringify(data, null, 2)); 45 + } 46 + 47 + function createStateStore(storePath: string): NodeSavedStateStore { 48 + return { 49 + async set(key: string, state: NodeSavedState) { 50 + const data = loadStore(storePath); 51 + data.states[key] = state; 52 + saveStore(storePath, data); 53 + }, 54 + async get(key: string) { 55 + const data = loadStore(storePath); 56 + return data.states[key]; 57 + }, 58 + async del(key: string) { 59 + const data = loadStore(storePath); 60 + delete data.states[key]; 61 + saveStore(storePath, data); 62 + } 63 + }; 64 + } 65 + 66 + function createSessionStore(storePath: string): NodeSavedSessionStore { 67 + return { 68 + async set(sub: string, session: NodeSavedSession) { 69 + const data = loadStore(storePath); 70 + data.sessions[sub] = session; 71 + saveStore(storePath, data); 72 + }, 73 + async get(sub: string) { 74 + const data = loadStore(storePath); 75 + return data.sessions[sub]; 76 + }, 77 + async del(sub: string) { 78 + const data = loadStore(storePath); 79 + delete data.sessions[sub]; 80 + saveStore(storePath, data); 81 + } 82 + }; 83 + } 84 + 85 + export interface AuthOptions { 86 + storePath?: string; 87 + appPassword?: string; 88 + } 89 + 90 + /** 91 + * Authenticate with AT Protocol using OAuth loopback flow 92 + */ 93 + export async function authenticateOAuth( 94 + handle: string, 95 + options: AuthOptions = {} 96 + ): Promise<{ agent: Agent; did: string }> { 97 + const storePath = options.storePath || DEFAULT_STORE_PATH; 98 + 99 + // Build loopback client metadata 100 + const redirectUri = `http://${LOOPBACK_HOST}:${LOOPBACK_PORT}/oauth/callback`; 101 + const clientIdParams = new URLSearchParams(); 102 + clientIdParams.append('redirect_uri', redirectUri); 103 + clientIdParams.append('scope', OAUTH_SCOPE); 104 + 105 + const client = new NodeOAuthClient({ 106 + clientMetadata: { 107 + client_id: `http://localhost?${clientIdParams.toString()}`, 108 + client_name: "Wisp CLI", 109 + client_uri: "https://wisp.place", 110 + redirect_uris: [redirectUri], 111 + grant_types: ['authorization_code', 'refresh_token'], 112 + response_types: ['code'], 113 + application_type: 'web', 114 + token_endpoint_auth_method: 'none', 115 + scope: OAUTH_SCOPE, 116 + dpop_bound_access_tokens: false, 117 + }, 118 + stateStore: createStateStore(storePath), 119 + sessionStore: createSessionStore(storePath), 120 + }); 121 + 122 + // Try to restore existing session 123 + const data = loadStore(storePath); 124 + const existingSessions = Object.keys(data.sessions); 125 + 126 + // Check if we have a session for this handle's DID 127 + for (const sub of existingSessions) { 128 + try { 129 + const session = await client.restore(sub); 130 + if (session) { 131 + // Verify session is still valid 132 + const agent = new Agent(session); 133 + const profile = await agent.getProfile({ actor: sub }); 134 + 135 + // Check if this is the handle we want 136 + if (profile.data.handle === handle || sub === handle) { 137 + console.log(`Restored existing session for ${profile.data.handle}`); 138 + return { agent, did: sub }; 139 + } 140 + } 141 + } catch { 142 + // Session invalid, continue 143 + } 144 + } 145 + 146 + // Start new OAuth flow 147 + console.log(`Starting OAuth flow for ${handle}...`); 148 + 149 + // Create loopback server to receive callback 150 + const callbackPromise = new Promise<{ params: URLSearchParams }>((resolve, reject) => { 151 + const server = Bun.serve({ 152 + port: LOOPBACK_PORT, 153 + hostname: LOOPBACK_HOST, 154 + fetch(req) { 155 + const url = new URL(req.url); 156 + 157 + if (url.pathname === '/oauth/callback') { 158 + const params = new URLSearchParams(url.search); 159 + 160 + // Close server after receiving callback 161 + setTimeout(() => server.stop(), 100); 162 + 163 + resolve({ params }); 164 + 165 + return new Response(` 166 + <html> 167 + <head><title>Wisp CLI - Authentication Successful</title></head> 168 + <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;"> 169 + <div style="text-align: center;"> 170 + <h1>Authentication Successful</h1> 171 + <p>You can close this window and return to the CLI.</p> 172 + </div> 173 + </body> 174 + </html> 175 + `, { 176 + headers: { 'Content-Type': 'text/html' } 177 + }); 178 + } 179 + 180 + return new Response('Not found', { status: 404 }); 181 + }, 182 + }); 183 + 184 + // Timeout after 5 minutes 185 + setTimeout(() => { 186 + server.stop(); 187 + reject(new Error('OAuth callback timeout')); 188 + }, 5 * 60 * 1000); 189 + }); 190 + 191 + // Get authorization URL 192 + const authUrl = await client.authorize(handle, { 193 + scope: OAUTH_SCOPE, 194 + }); 195 + 196 + // Open browser 197 + console.log(`Opening browser for authentication...`); 198 + console.log(`If browser doesn't open, visit: ${authUrl}`); 199 + await open(authUrl.toString()); 200 + 201 + // Wait for callback 202 + const { params } = await callbackPromise; 203 + 204 + // Handle callback 205 + const { session } = await client.callback(params); 206 + 207 + const agent = new Agent(session); 208 + const did = session.did; 209 + 210 + console.log(`Successfully authenticated as ${did}`); 211 + 212 + return { agent, did }; 213 + } 214 + 215 + /** 216 + * Authenticate with AT Protocol using app password (for CI/headless) 217 + */ 218 + export async function authenticateAppPassword( 219 + identifier: string, 220 + password: string, 221 + pdsUrl?: string 222 + ): Promise<{ agent: Agent; did: string }> { 223 + const serviceUrl = pdsUrl || 'https://bsky.social'; 224 + 225 + const credSession = new CredentialSession(new URL(serviceUrl)); 226 + await credSession.login({ identifier, password }); 227 + 228 + const agent = new Agent(credSession); 229 + const did = credSession.did!; 230 + 231 + console.log(`Successfully authenticated as ${did}`); 232 + 233 + return { agent, did }; 234 + } 235 + 236 + /** 237 + * Authenticate - tries OAuth if no password provided, otherwise uses app password 238 + */ 239 + export async function authenticate( 240 + handle: string, 241 + options: AuthOptions = {} 242 + ): Promise<{ agent: Agent; did: string }> { 243 + if (options.appPassword) { 244 + return authenticateAppPassword(handle, options.appPassword); 245 + } 246 + return authenticateOAuth(handle, options); 247 + } 248 + 249 + /** 250 + * Clear stored OAuth sessions 251 + */ 252 + export function clearSessions(storePath?: string) { 253 + const path = storePath || DEFAULT_STORE_PATH; 254 + if (existsSync(path)) { 255 + unlinkSync(path); 256 + console.log('Cleared stored OAuth sessions'); 257 + } 258 + }
+32
cli/lib/metadata.ts
··· 1 + import { existsSync, readFileSync, writeFileSync } from 'fs'; 2 + import { join } from 'path'; 3 + 4 + export interface SiteMetadata { 5 + recordCid: string; 6 + fileCids: Record<string, string>; // path -> blob CID 7 + lastSync: number; // Unix timestamp 8 + } 9 + 10 + const METADATA_FILE = '.wisp-metadata.json'; 11 + 12 + export function getMetadataPath(siteDir: string): string { 13 + return join(siteDir, METADATA_FILE); 14 + } 15 + 16 + export function loadMetadata(siteDir: string): SiteMetadata | null { 17 + const path = getMetadataPath(siteDir); 18 + if (!existsSync(path)) { 19 + return null; 20 + } 21 + try { 22 + const content = readFileSync(path, 'utf-8'); 23 + return JSON.parse(content) as SiteMetadata; 24 + } catch { 25 + return null; 26 + } 27 + } 28 + 29 + export function saveMetadata(siteDir: string, metadata: SiteMetadata): void { 30 + const path = getMetadataPath(siteDir); 31 + writeFileSync(path, JSON.stringify(metadata, null, 2)); 32 + }
+16
cli/lib/progress.ts
··· 1 + import ora, { type Ora } from 'ora'; 2 + import pc from 'picocolors'; 3 + 4 + export { ora, pc }; 5 + 6 + export function formatBytes(bytes: number): string { 7 + if (bytes === 0) return '0 B'; 8 + const k = 1024; 9 + const sizes = ['B', 'KB', 'MB', 'GB']; 10 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 11 + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i] ?? 'TB'}`; 12 + } 13 + 14 + export function createSpinner(text: string): Ora { 15 + return ora({ text, color: 'cyan' }); 16 + }
+274
cli/lib/redirects.ts
··· 1 + /** 2 + * _redirects file parsing - adapted from hosting-service 3 + */ 4 + 5 + export interface RedirectRule { 6 + from: string; 7 + to: string; 8 + status: number; 9 + force: boolean; 10 + conditions?: { 11 + country?: string[]; 12 + language?: string[]; 13 + role?: string[]; 14 + cookie?: string[]; 15 + }; 16 + fromPattern?: RegExp; 17 + fromParams?: string[]; 18 + queryParams?: Record<string, string>; 19 + } 20 + 21 + export interface RedirectMatch { 22 + rule: RedirectRule; 23 + targetPath: string; 24 + status: number; 25 + } 26 + 27 + const MAX_REDIRECT_RULES = 1000; 28 + 29 + export function parseRedirectsFile(content: string): RedirectRule[] { 30 + const lines = content.split('\n'); 31 + const rules: RedirectRule[] = []; 32 + 33 + for (let lineNum = 0; lineNum < lines.length; lineNum++) { 34 + const lineRaw = lines[lineNum]; 35 + if (!lineRaw) continue; 36 + 37 + const line = lineRaw.trim(); 38 + if (!line || line.startsWith('#')) continue; 39 + if (rules.length >= MAX_REDIRECT_RULES) break; 40 + 41 + try { 42 + const rule = parseRedirectLine(line); 43 + if (rule?.fromPattern) { 44 + rules.push(rule); 45 + } 46 + } catch { 47 + // Skip invalid lines 48 + } 49 + } 50 + 51 + return rules; 52 + } 53 + 54 + function parseRedirectLine(line: string): RedirectRule | null { 55 + const parts = line.split(/\s+/); 56 + if (parts.length < 2) return null; 57 + 58 + let idx = 0; 59 + const from = parts[idx++]; 60 + if (!from) return null; 61 + 62 + let status = 301; 63 + let force = false; 64 + const conditions: NonNullable<RedirectRule['conditions']> = {}; 65 + const queryParams: Record<string, string> = {}; 66 + 67 + // Parse query parameters before destination 68 + while (idx < parts.length) { 69 + const part = parts[idx]; 70 + if (!part) { idx++; continue; } 71 + if (part.startsWith('/') || part.startsWith('http://') || part.startsWith('https://')) break; 72 + if (part.includes('=')) { 73 + const splitIndex = part.indexOf('='); 74 + const key = part.slice(0, splitIndex); 75 + const value = part.slice(splitIndex + 1); 76 + if (key && value) queryParams[key] = value; 77 + idx++; 78 + } else { 79 + break; 80 + } 81 + } 82 + 83 + if (idx >= parts.length) return null; 84 + const to = parts[idx++]; 85 + if (!to) return null; 86 + 87 + // Parse status and conditions 88 + for (let i = idx; i < parts.length; i++) { 89 + const part = parts[i]; 90 + if (!part) continue; 91 + 92 + if (/^\d+!?$/.test(part)) { 93 + if (part.endsWith('!')) { 94 + force = true; 95 + status = parseInt(part.slice(0, -1)); 96 + } else { 97 + status = parseInt(part); 98 + } 99 + continue; 100 + } 101 + 102 + if (part.includes('=')) { 103 + const splitIndex = part.indexOf('='); 104 + const key = part.slice(0, splitIndex); 105 + const value = part.slice(splitIndex + 1); 106 + if (!key || !value) continue; 107 + 108 + const keyLower = key.toLowerCase(); 109 + if (keyLower === 'country') { 110 + conditions.country = value.split(',').map(v => v.trim().toLowerCase()); 111 + } else if (keyLower === 'language') { 112 + conditions.language = value.split(',').map(v => v.trim().toLowerCase()); 113 + } else if (keyLower === 'role') { 114 + conditions.role = value.split(',').map(v => v.trim()); 115 + } else if (keyLower === 'cookie') { 116 + conditions.cookie = value.split(',').map(v => v.trim().toLowerCase()); 117 + } 118 + } 119 + } 120 + 121 + const { pattern, params } = convertPathToRegex(from); 122 + 123 + return { 124 + from, 125 + to, 126 + status, 127 + force, 128 + conditions: Object.keys(conditions).length > 0 ? conditions : undefined, 129 + queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined, 130 + fromPattern: pattern, 131 + fromParams: params, 132 + }; 133 + } 134 + 135 + function convertPathToRegex(pattern: string): { pattern: RegExp; params: string[] } { 136 + const params: string[] = []; 137 + let regexStr = '^'; 138 + 139 + const pathPart = pattern.split('?')[0] || pattern; 140 + let escaped = pathPart.replace(/[.+^${}()|[\]\\]/g, '\\$&'); 141 + 142 + escaped = escaped.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_match, paramName) => { 143 + params.push(paramName); 144 + return '([^/?]+)'; 145 + }); 146 + 147 + if (escaped.includes('*')) { 148 + escaped = escaped.replace(/\*/g, '(.*)'); 149 + params.push('splat'); 150 + } 151 + 152 + regexStr += escaped; 153 + if (!regexStr.endsWith('.*')) { 154 + regexStr += '/?'; 155 + } 156 + regexStr += '$'; 157 + 158 + return { pattern: new RegExp(regexStr), params }; 159 + } 160 + 161 + export function matchRedirectRule( 162 + requestPath: string, 163 + rules: RedirectRule[], 164 + context?: { 165 + queryParams?: Record<string, string>; 166 + headers?: Record<string, string>; 167 + cookies?: Record<string, string>; 168 + }, 169 + visitedPaths: Set<string> = new Set() 170 + ): RedirectMatch | null { 171 + let normalizedPath = requestPath.startsWith('/') ? requestPath : `/${requestPath}`; 172 + 173 + if (visitedPaths.has(normalizedPath)) return null; 174 + visitedPaths.add(normalizedPath); 175 + if (visitedPaths.size > 10) return null; 176 + 177 + for (const rule of rules) { 178 + // Check query params 179 + if (rule.queryParams) { 180 + if (!context?.queryParams) continue; 181 + const queryMatches = Object.entries(rule.queryParams).every(([key, expectedValue]) => { 182 + const actualValue = context.queryParams?.[key]; 183 + if (actualValue === undefined) return false; 184 + if (expectedValue && !expectedValue.startsWith(':')) { 185 + return actualValue === expectedValue; 186 + } 187 + return true; 188 + }); 189 + if (!queryMatches) continue; 190 + } 191 + 192 + // Check conditions 193 + if (rule.conditions) { 194 + if (rule.conditions.country && context?.headers) { 195 + const country = context.headers['cf-ipcountry']?.toLowerCase() || context.headers['x-country']?.toLowerCase(); 196 + if (!country || !rule.conditions.country.includes(country)) continue; 197 + } 198 + if (rule.conditions.language && context?.headers) { 199 + const acceptLang = context.headers['accept-language']; 200 + if (!acceptLang) continue; 201 + const langs = acceptLang.split(',').map(l => l.split(';')[0]?.trim().toLowerCase() || '').filter(Boolean); 202 + const hasMatch = rule.conditions.language.some(lang => langs.some(l => l === lang || l.startsWith(lang + '-'))); 203 + if (!hasMatch) continue; 204 + } 205 + if (rule.conditions.cookie && context?.cookies) { 206 + const hasCookie = rule.conditions.cookie.some(cookieName => context.cookies && cookieName in context.cookies); 207 + if (!hasCookie) continue; 208 + } 209 + if (rule.conditions.role) continue; 210 + } 211 + 212 + const match = rule.fromPattern?.exec(normalizedPath); 213 + if (!match) continue; 214 + 215 + let targetPath = rule.to; 216 + 217 + if (rule.fromParams && match.length > 1) { 218 + for (let i = 0; i < rule.fromParams.length; i++) { 219 + const paramName = rule.fromParams[i]; 220 + const paramValue = match[i + 1]; 221 + if (!paramName || !paramValue) continue; 222 + 223 + const encodedValue = encodeURIComponent(paramValue); 224 + if (paramName === 'splat') { 225 + targetPath = targetPath.replace(':splat', encodedValue.replace(/%2F/g, '/')); 226 + } else { 227 + targetPath = targetPath.replace(`:${paramName}`, encodedValue); 228 + } 229 + } 230 + } 231 + 232 + if (rule.queryParams && context?.queryParams) { 233 + for (const [key, placeholder] of Object.entries(rule.queryParams)) { 234 + const actualValue = context.queryParams[key]; 235 + if (actualValue && placeholder?.startsWith(':')) { 236 + const paramName = placeholder.slice(1); 237 + if (paramName) { 238 + targetPath = targetPath.replace(`:${paramName}`, encodeURIComponent(actualValue)); 239 + } 240 + } 241 + } 242 + } 243 + 244 + return { rule, targetPath, status: rule.status }; 245 + } 246 + 247 + return null; 248 + } 249 + 250 + export function parseCookies(cookieHeader?: string): Record<string, string> { 251 + if (!cookieHeader) return {}; 252 + const cookies: Record<string, string> = {}; 253 + for (const part of cookieHeader.split(';')) { 254 + const [key, ...valueParts] = part.split('='); 255 + if (key && valueParts.length > 0) { 256 + cookies[key.trim()] = valueParts.join('=').trim(); 257 + } 258 + } 259 + return cookies; 260 + } 261 + 262 + export function parseQueryString(url: string): Record<string, string> { 263 + const queryStart = url.indexOf('?'); 264 + if (queryStart === -1) return {}; 265 + const queryString = url.slice(queryStart + 1); 266 + const params: Record<string, string> = {}; 267 + for (const pair of queryString.split('&')) { 268 + const [key, value] = pair.split('='); 269 + if (key) { 270 + params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : ''; 271 + } 272 + } 273 + return params; 274 + }
+35
cli/package.json
··· 1 + { 2 + "name": "wisp-cli", 3 + "version": "1.0.0", 4 + "description": "CLI for wisp.place - deploy static sites to the AT Protocol", 5 + "type": "module", 6 + "bin": { 7 + "wisp-cli": "./index.ts" 8 + }, 9 + "scripts": { 10 + "dev": "bun run index.ts", 11 + "typecheck": "tsc --noEmit" 12 + }, 13 + "dependencies": { 14 + "@atproto/api": "^0.18.17", 15 + "@atproto/oauth-client-node": "^0.3.15", 16 + "@atproto/sync": "^0.1.39", 17 + "@wisp/atproto-utils": "workspace:*", 18 + "@wisp/constants": "workspace:*", 19 + "@wisp/fs-utils": "workspace:*", 20 + "@wisp/lexicons": "workspace:*", 21 + "commander": "^14.0.2", 22 + "ignore": "^7.0.5", 23 + "mime-types": "^3.0.2", 24 + "open": "^11.0.0", 25 + "ora": "^9.1.0", 26 + "picocolors": "^1.1.1" 27 + }, 28 + "devDependencies": { 29 + "@types/bun": "latest", 30 + "@types/mime-types": "^3.0.1" 31 + }, 32 + "peerDependencies": { 33 + "typescript": "^5" 34 + } 35 + }
cli/src/blob_map.rs rust-cli/src/blob_map.rs
cli/src/cid.rs rust-cli/src/cid.rs
cli/src/download.rs rust-cli/src/download.rs
cli/src/ignore_patterns.rs rust-cli/src/ignore_patterns.rs
cli/src/main.rs rust-cli/src/main.rs
cli/src/metadata.rs rust-cli/src/metadata.rs
cli/src/pull.rs rust-cli/src/pull.rs
cli/src/redirects.rs rust-cli/src/redirects.rs
cli/src/serve.rs rust-cli/src/serve.rs
cli/src/subfs_utils.rs rust-cli/src/subfs_utils.rs
+38
cli/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + // Some stricter flags (disabled by default) 25 + "noUnusedLocals": false, 26 + "noUnusedParameters": false, 27 + "noPropertyAccessFromIndexSignature": false, 28 + 29 + // Workspace paths 30 + "baseUrl": ".", 31 + "paths": { 32 + "@wisp/*": ["../packages/@wisp/*/src"] 33 + }, 34 + "types": ["bun"] 35 + }, 36 + "include": ["./**/*.ts"], 37 + "exclude": ["node_modules"] 38 + }
+2 -1
package.json
··· 5 5 "workspaces": [ 6 6 "packages/@wisp/*", 7 7 "apps/main-app", 8 - "apps/hosting-service" 8 + "apps/hosting-service", 9 + "cli" 9 10 ], 10 11 "dependencies": { 11 12 "@tailwindcss/cli": "^4.1.17",
+2 -2
packages/@wisp/atproto-utils/package.json
··· 24 24 } 25 25 }, 26 26 "dependencies": { 27 - "@atproto/api": "^0.14.1", 27 + "@atproto/api": "^0.18.17", 28 28 "@wisp/lexicons": "workspace:*", 29 29 "multiformats": "^13.3.1" 30 30 }, 31 31 "devDependencies": { 32 - "@atproto/lexicon": "^0.5.2" 32 + "@atproto/lexicon": "^0.6.1" 33 33 } 34 34 }
+1 -1
packages/@wisp/atproto-utils/src/blob.ts
··· 1 - import type { BlobRef } from "@atproto/lexicon"; 1 + import type { BlobRef } from "@atproto/api"; 2 2 import type { Directory, File } from "@wisp/lexicons/types/place/wisp/fs"; 3 3 import { CID } from 'multiformats/cid'; 4 4 import { sha256 } from 'multiformats/hashes/sha2';
+10
packages/@wisp/constants/src/index.ts
··· 30 30 31 31 // Compression settings 32 32 export const GZIP_COMPRESSION_LEVEL = 9; 33 + 34 + // CLI Binary URLs 35 + export const CLI_BINARY_BASE_URL = "https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries"; 36 + export const CLI_BINARIES = { 37 + "darwin-universal": `${CLI_BINARY_BASE_URL}/wisp-cli-darwin-universal`, 38 + "darwin-arm64": `${CLI_BINARY_BASE_URL}/wisp-cli-aarch64-darwin`, 39 + "darwin-x86_64": `${CLI_BINARY_BASE_URL}/wisp-cli-darwin-x86_64`, 40 + "linux-arm64": `${CLI_BINARY_BASE_URL}/wisp-cli-aarch64-linux`, 41 + "linux-x86_64": `${CLI_BINARY_BASE_URL}/wisp-cli-x86_64-linux`, 42 + } as const;
+1 -1
packages/@wisp/fs-utils/package.json
··· 28 28 } 29 29 }, 30 30 "dependencies": { 31 - "@atproto/api": "^0.14.1", 31 + "@atproto/api": "^0.18.17", 32 32 "@wisp/lexicons": "workspace:*" 33 33 } 34 34 }
+25
rust-cli/.gitignore
··· 1 + test/ 2 + .DS_STORE 3 + jacquard/ 4 + binaries/ 5 + # Generated by Cargo 6 + # will have compiled files and executables 7 + debug 8 + target 9 + 10 + # These are backup files generated by rustfmt 11 + **/*.rs.bk 12 + 13 + # MSVC Windows builds of rustc generate these, which store debugging information 14 + *.pdb 15 + 16 + # Generated by cargo mutants 17 + # Contains mutation testing data 18 + **/mutants.out*/ 19 + 20 + # RustRover 21 + # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 22 + # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 23 + # and can be added to the global gitignore or merged into this file. For a more nuclear 24 + # option (not recommended) you can uncomment the following to ignore the entire idea folder. 25 + #.idea/
+341
rust-cli/README.md
··· 1 + # Wisp CLI 2 + 3 + A command-line tool for deploying static sites to your AT Protocol repo to be served on [wisp.place](https://wisp.place), an AT indexer to serve such sites. 4 + 5 + ## Why? 6 + 7 + The PDS serves as a way to verfiably, cryptographically prove that you own your site. That it was you (or at least someone who controls your account) who uploaded it. It is also a manifest of each file in the site to ensure file integrity. Keeping hosting seperate ensures that you could move your site across other servers or even serverless solutions to ensure speedy delievery while keeping it backed by an absolute source of truth being the manifest record and the blobs of each file in your repo. 8 + 9 + ## Features 10 + 11 + - Deploy static sites directly to your AT Protocol repo 12 + - Supports both OAuth and app password authentication 13 + - Preserves directory structure and file integrity 14 + 15 + ## Soon 16 + 17 + -- Host sites 18 + -- Manage and delete sites 19 + -- Metrics and logs for self hosting. 20 + 21 + ## Installation 22 + 23 + ### From Source 24 + 25 + ```bash 26 + cargo build --release 27 + ``` 28 + 29 + Check out the build scripts for cross complation using nix-shell. 30 + 31 + The binary will be available at `target/release/wisp-cli`. 32 + 33 + ## Usage 34 + 35 + ### Commands 36 + 37 + The CLI supports three main commands: 38 + - **deploy**: Upload a site to your PDS (default command) 39 + - **pull**: Download a site from a PDS to a local directory 40 + - **serve**: Serve a site locally with real-time firehose updates 41 + 42 + ### Basic Deployment 43 + 44 + Deploy the current directory: 45 + 46 + ```bash 47 + wisp-cli nekomimi.pet --path . --site my-site 48 + ``` 49 + 50 + Deploy a specific directory: 51 + 52 + ```bash 53 + wisp-cli alice.bsky.social --path ./dist/ --site my-site 54 + ``` 55 + 56 + Or use the explicit `deploy` subcommand: 57 + 58 + ```bash 59 + wisp-cli deploy alice.bsky.social --path ./dist/ --site my-site 60 + ``` 61 + 62 + ### Pull a Site 63 + 64 + Download a site from a PDS to a local directory: 65 + 66 + ```bash 67 + wisp-cli pull alice.bsky.social --site my-site --path ./downloaded-site 68 + ``` 69 + 70 + This will download all files from the site to the specified directory. 71 + 72 + ### Serve a Site Locally 73 + 74 + Serve a site locally with real-time updates from the firehose: 75 + 76 + ```bash 77 + wisp-cli serve alice.bsky.social --site my-site --path ./site --port 8080 78 + ``` 79 + 80 + This will: 81 + 1. Download the site to the specified path 82 + 2. Start a local server on the specified port (default: 8080) 83 + 3. Watch the firehose for updates and automatically reload files when changed 84 + 85 + ### Authentication Methods 86 + 87 + #### OAuth (Recommended) 88 + 89 + By default, the CLI uses OAuth authentication with a local loopback server: 90 + 91 + ```bash 92 + wisp-cli alice.bsky.social --path ./my-site --site my-site 93 + ``` 94 + 95 + This will: 96 + 1. Open your browser for authentication 97 + 2. Save the session to a file (default: `/tmp/wisp-oauth-session.json`) 98 + 3. Reuse the session for future deployments 99 + 100 + Specify a custom session file location: 101 + 102 + ```bash 103 + wisp-cli alice.bsky.social --path ./my-site --site my-site --store ~/.wisp-session.json 104 + ``` 105 + 106 + #### App Password 107 + 108 + For headless environments or CI/CD, use an app password: 109 + 110 + ```bash 111 + wisp-cli alice.bsky.social --path ./my-site --site my-site --password YOUR_APP_PASSWORD 112 + ``` 113 + 114 + **Note:** When using `--password`, the `--store` option is ignored. 115 + 116 + ## Command-Line Options 117 + 118 + ### Deploy Command 119 + 120 + ``` 121 + wisp-cli [deploy] [OPTIONS] <INPUT> 122 + 123 + Arguments: 124 + <INPUT> Handle (e.g., alice.bsky.social), DID, or PDS URL 125 + 126 + Options: 127 + -p, --path <PATH> Path to the directory containing your static site [default: .] 128 + -s, --site <SITE> Site name (defaults to directory name) 129 + --store <STORE> Path to auth store file (only used with OAuth) [default: /tmp/wisp-oauth-session.json] 130 + --password <PASSWORD> App Password for authentication (alternative to OAuth) 131 + --directory Enable directory listing mode for paths without index files 132 + --spa Enable SPA mode (serve index.html for all routes) 133 + -y, --yes Skip confirmation prompts (automatically accept warnings) 134 + -h, --help Print help 135 + -V, --version Print version 136 + ``` 137 + 138 + ### Pull Command 139 + 140 + ``` 141 + wisp-cli pull [OPTIONS] --site <SITE> <INPUT> 142 + 143 + Arguments: 144 + <INPUT> Handle (e.g., alice.bsky.social) or DID 145 + 146 + Options: 147 + -s, --site <SITE> Site name (record key) 148 + -p, --path <PATH> Output directory for the downloaded site [default: .] 149 + -h, --help Print help 150 + ``` 151 + 152 + ### Serve Command 153 + 154 + ``` 155 + wisp-cli serve [OPTIONS] --site <SITE> <INPUT> 156 + 157 + Arguments: 158 + <INPUT> Handle (e.g., alice.bsky.social) or DID 159 + 160 + Options: 161 + -s, --site <SITE> Site name (record key) 162 + -p, --path <PATH> Output directory for the site files [default: .] 163 + -P, --port <PORT> Port to serve on [default: 8080] 164 + -h, --help Print help 165 + ``` 166 + 167 + ## How It Works 168 + 169 + 1. **Authentication**: Authenticates using OAuth or app password 170 + 2. **File Processing**: 171 + - Recursively walks the directory tree 172 + - Skips hidden files (starting with `.`) 173 + - Detects MIME types automatically 174 + - Compresses files with gzip 175 + - Base64 encodes compressed content 176 + 3. **Upload**: 177 + - Uploads files as blobs to your PDS 178 + - Processes up to 5 files concurrently 179 + - Creates a `place.wisp.fs` record with the site manifest 180 + 4. **Deployment**: Site is immediately available at `https://sites.wisp.place/{did}/{site-name}` 181 + 182 + ## File Processing 183 + 184 + All files are automatically: 185 + 186 + - **Compressed** with gzip (level 9) 187 + - **Base64 encoded** to bypass PDS content sniffing 188 + - **Uploaded** as `application/octet-stream` blobs 189 + - **Stored** with original MIME type metadata 190 + 191 + The hosting service automatically decompresses non HTML/CSS/JS files when serving them. 192 + 193 + ## Limitations 194 + 195 + - **Max file size**: 100MB per file (after compression) (this is a PDS limit, but not enforced by the CLI in case yours is higher) 196 + - **Max file count**: 2000 files 197 + - **Site name** must follow AT Protocol rkey format rules (alphanumeric, hyphens, underscores) 198 + 199 + ## Deploy with CI/CD 200 + 201 + ### GitHub Actions 202 + 203 + ```yaml 204 + name: Deploy to Wisp 205 + on: 206 + push: 207 + branches: [main] 208 + 209 + jobs: 210 + deploy: 211 + runs-on: ubuntu-latest 212 + steps: 213 + - uses: actions/checkout@v3 214 + 215 + - name: Setup Node 216 + uses: actions/setup-node@v3 217 + with: 218 + node-version: '25' 219 + 220 + - name: Install dependencies 221 + run: npm install 222 + 223 + - name: Build site 224 + run: npm run build 225 + 226 + - name: Download Wisp CLI 227 + run: | 228 + curl -L https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 229 + chmod +x wisp-cli 230 + 231 + - name: Deploy to Wisp 232 + env: 233 + WISP_APP_PASSWORD: ${{ secrets.WISP_APP_PASSWORD }} 234 + run: | 235 + ./wisp-cli alice.bsky.social \ 236 + --path ./dist \ 237 + --site my-site \ 238 + --password "$WISP_APP_PASSWORD" 239 + ``` 240 + 241 + ### Tangled.org 242 + 243 + ```yaml 244 + when: 245 + - event: ['push'] 246 + branch: ['main'] 247 + - event: ['manual'] 248 + 249 + engine: 'nixery' 250 + 251 + clone: 252 + skip: false 253 + depth: 1 254 + submodules: false 255 + 256 + dependencies: 257 + nixpkgs: 258 + - nodejs 259 + - coreutils 260 + - curl 261 + github:NixOS/nixpkgs/nixpkgs-unstable: 262 + - bun 263 + 264 + environment: 265 + SITE_PATH: 'dist' 266 + SITE_NAME: 'my-site' 267 + WISP_HANDLE: 'your-handle.bsky.social' 268 + 269 + steps: 270 + - name: build site 271 + command: | 272 + export PATH="$HOME/.nix-profile/bin:$PATH" 273 + 274 + # regenerate lockfile 275 + rm package-lock.json bun.lock 276 + bun install @rolldown/binding-linux-arm64-gnu --save-optional 277 + bun install 278 + 279 + # build with vite 280 + bun node_modules/.bin/vite build 281 + 282 + - name: deploy to wisp 283 + command: | 284 + # Download Wisp CLI 285 + curl https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-linux -o wisp-cli 286 + chmod +x wisp-cli 287 + 288 + # Deploy to Wisp 289 + ./wisp-cli \ 290 + "$WISP_HANDLE" \ 291 + --path "$SITE_PATH" \ 292 + --site "$SITE_NAME" \ 293 + --password "$WISP_APP_PASSWORD" 294 + ``` 295 + 296 + ### Generic Shell Script 297 + 298 + ```bash 299 + # Use app password from environment variable 300 + wisp-cli alice.bsky.social --path ./dist --site my-site --password "$WISP_APP_PASSWORD" 301 + ``` 302 + 303 + ## Output 304 + 305 + Upon successful deployment, you'll see: 306 + 307 + ``` 308 + Deployed site 'my-site': at://did:plc:abc123xyz/place.wisp.fs/my-site 309 + Available at: https://sites.wisp.place/did:plc:abc123xyz/my-site 310 + ``` 311 + 312 + ### Dependencies 313 + 314 + - **jacquard**: AT Protocol client library 315 + - **clap**: Command-line argument parsing 316 + - **tokio**: Async runtime 317 + - **flate2**: Gzip compression 318 + - **base64**: Base64 encoding 319 + - **walkdir**: Directory traversal 320 + - **mime_guess**: MIME type detection 321 + 322 + ## License 323 + 324 + MIT License 325 + 326 + ## Contributing 327 + 328 + Just don't give me entirely claude slop especailly not in the PR description itself. You should be responsible for code you submit and aware of what it even is you're submitting. 329 + 330 + ## Links 331 + 332 + - **Website**: https://wisp.place 333 + - **Main Repository**: https://tangled.org/@nekomimi.pet/wisp.place-monorepo 334 + - **AT Protocol**: https://atproto.com 335 + - **Jacquard Library**: https://tangled.org/@nonbinary.computer/jacquard 336 + 337 + ## Support 338 + 339 + For issues and questions: 340 + - Check the main wisp.place documentation 341 + - Open an issue in the main repository