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

Configure Feed

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

windows binary as well as fix app password being swallowed in some shells in cli

+169 -34
+9 -4
apps/main-app/public/editor/tabs/CLITab.tsx
··· 9 9 { 10 10 platform: 'macOS (Apple Silicon)', 11 11 filename: 'wisp-cli-aarch64-darwin', 12 - sha256: '06544b3a3e27a4b8d7b3a46a39fb7205cf90b3061e19fe533b090facd604f375', 12 + sha256: '70ffab694c6c19807dc234eb8e85da358406166230c4dfc4ac2de141b1e9000f', 13 13 }, 14 14 { 15 15 platform: 'macOS (Intel)', 16 16 filename: 'wisp-cli-x86_64-darwin', 17 - sha256: '9ec523e3ceef927b37adc52d449dcd9e13ea84fa49b0b77f0d5932c94cfe262e', 17 + sha256: '19bcd4126382e4d442a5590d65de5d06feb78094bacaf7e17514d25f3999932a', 18 18 }, 19 19 { 20 20 platform: 'Linux (ARM64)', 21 21 filename: 'wisp-cli-aarch64-linux', 22 - sha256: '42a262668e13dce36173a4096cdc2b22358b805cf192335f84534c7f695d395b', 22 + sha256: 'a8999f210d0a8b7bb11ce6f592c8dc4c7e889066e215040812b607db677ada2a', 23 23 }, 24 24 { 25 25 platform: 'Linux (x86_64)', 26 26 filename: 'wisp-cli-x86_64-linux', 27 - sha256: '589ee59f3959ddfbc12fea38d2bcb91701f1362f560ae6fd506bebea3150e2cc', 27 + sha256: '5948d8842e0f7578b00b9ad08d77e5bac0cf3b2fe65f2b19c631868e94ad025a', 28 + }, 29 + { 30 + platform: 'Windows (x86_64)', 31 + filename: 'wisp-cli-x86_64-windows.exe', 32 + sha256: '18c207001cf4d47961cfca63bf149a913d31ffa4ef04c2b01379b7274480d36e', 28 33 }, 29 34 ] as const 30 35
+134 -26
cli/lib/auth.ts
··· 1 1 import { mkdirSync } from 'node:fs' 2 + import { createServer } from 'node:net' 2 3 import { homedir } from 'node:os' 3 4 import { dirname, join } from 'node:path' 4 5 import { cwd } from 'node:process' ··· 63 64 64 65 const LOOPBACK_PORT = 4000 65 66 const LOOPBACK_HOST = '127.0.0.1' 67 + 68 + interface LoopbackPortSelection { 69 + port: number 70 + usedFallback: boolean 71 + } 72 + 73 + function parsePort(value: string | undefined): number | undefined { 74 + if (!value) { 75 + return undefined 76 + } 77 + const parsed = Number.parseInt(value, 10) 78 + if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) { 79 + return undefined 80 + } 81 + return parsed 82 + } 83 + 84 + async function isPortAvailable(host: string, port: number): Promise<boolean> { 85 + return await new Promise<boolean>((resolve, reject) => { 86 + const server = createServer() 87 + 88 + server.once('error', (err: NodeJS.ErrnoException) => { 89 + if (err.code === 'EADDRINUSE') { 90 + resolve(false) 91 + return 92 + } 93 + reject(err) 94 + }) 95 + 96 + server.once('listening', () => { 97 + server.close((err) => { 98 + if (err) { 99 + reject(err) 100 + return 101 + } 102 + resolve(true) 103 + }) 104 + }) 105 + 106 + server.listen({ host, port, exclusive: true }) 107 + }) 108 + } 109 + 110 + async function findRandomAvailablePort(host: string): Promise<number> { 111 + return await new Promise<number>((resolve, reject) => { 112 + const server = createServer() 113 + 114 + server.once('error', reject) 115 + server.once('listening', () => { 116 + const address = server.address() 117 + if (!address || typeof address === 'string') { 118 + server.close(() => reject(new Error('Failed to determine loopback callback port'))) 119 + return 120 + } 121 + 122 + const port = address.port 123 + server.close((err) => { 124 + if (err) { 125 + reject(err) 126 + return 127 + } 128 + resolve(port) 129 + }) 130 + }) 131 + 132 + server.listen({ host, port: 0, exclusive: true }) 133 + }) 134 + } 135 + 136 + async function resolveLoopbackPort(preferredPort: number, host: string): Promise<LoopbackPortSelection> { 137 + if (await isPortAvailable(host, preferredPort)) { 138 + return { port: preferredPort, usedFallback: false } 139 + } 140 + const port = await findRandomAvailablePort(host) 141 + return { port, usedFallback: true } 142 + } 66 143 67 144 // Runtime-agnostic KV adapter — all SQL is baked in so return types are known 68 145 ··· 260 337 } 261 338 } 262 339 263 - const redirectUri = `http://${LOOPBACK_HOST}:${LOOPBACK_PORT}/oauth/callback` 264 - const clientIdParams = new URLSearchParams() 265 - clientIdParams.append('redirect_uri', redirectUri) 266 - clientIdParams.append('scope', WISP_OAUTH_SCOPE) 267 - 268 - const client = new NodeOAuthClient({ 269 - clientMetadata: { 270 - client_id: `http://localhost?${clientIdParams.toString()}`, 271 - client_name: 'Wisp CLI', 272 - client_uri: 'https://wisp.place', 273 - redirect_uris: [redirectUri], 274 - grant_types: ['authorization_code', 'refresh_token'], 275 - response_types: ['code'], 276 - application_type: 'web', 277 - token_endpoint_auth_method: 'none', 278 - scope: WISP_OAUTH_SCOPE, 279 - dpop_bound_access_tokens: false, 280 - }, 281 - stateStore: createStateStore(kv), 282 - sessionStore: createSessionStore(kv, useKeychain ? await getKeyringEntryConstructor() : null), 283 - requestLock: requestLocalLock, 284 - }) 340 + const keyringEntryConstructor = useKeychain ? await getKeyringEntryConstructor() : null 341 + const stateStore = createStateStore(kv) 342 + const sessionStore = createSessionStore(kv, keyringEntryConstructor) 343 + const createOAuthClient = (redirectUri: string): NodeOAuthClient => { 344 + const clientIdParams = new URLSearchParams() 345 + clientIdParams.append('redirect_uri', redirectUri) 346 + clientIdParams.append('scope', WISP_OAUTH_SCOPE) 347 + return new NodeOAuthClient({ 348 + clientMetadata: { 349 + client_id: `http://localhost?${clientIdParams.toString()}`, 350 + client_name: 'Wisp CLI', 351 + client_uri: 'https://wisp.place', 352 + redirect_uris: [redirectUri], 353 + grant_types: ['authorization_code', 'refresh_token'], 354 + response_types: ['code'], 355 + application_type: 'web', 356 + token_endpoint_auth_method: 'none', 357 + scope: WISP_OAUTH_SCOPE, 358 + dpop_bound_access_tokens: false, 359 + }, 360 + stateStore, 361 + sessionStore, 362 + requestLock: requestLocalLock, 363 + }) 364 + } 285 365 286 366 // Try to restore the session mapped to the current directory 287 367 const dirKey = `dir:${cwd()}` 368 + const dirOAuthPortKey = `dir_oauth_port:${cwd()}` 369 + let oauthPort = parsePort(kvGet(kv, dirOAuthPortKey)) ?? LOOPBACK_PORT 370 + let redirectUri = `http://${LOOPBACK_HOST}:${oauthPort}/oauth/callback` 371 + let client = createOAuthClient(redirectUri) 288 372 const storedDid = kvGet(kv, dirKey) 289 373 if (storedDid) { 290 374 try { ··· 304 388 throw new Error('No active session for this directory. Run `wispctl login <handle>` first.') 305 389 } 306 390 391 + const preferredPort = oauthPort 392 + const portSelection = await resolveLoopbackPort(preferredPort, LOOPBACK_HOST) 393 + if (portSelection.usedFallback) { 394 + emitStatus( 395 + options, 396 + `OAuth callback port ${preferredPort} is unavailable. Using ${portSelection.port} for this login flow.`, 397 + ) 398 + } 399 + 400 + if (portSelection.port !== oauthPort) { 401 + oauthPort = portSelection.port 402 + redirectUri = `http://${LOOPBACK_HOST}:${oauthPort}/oauth/callback` 403 + client = createOAuthClient(redirectUri) 404 + } 405 + 307 406 // Start new OAuth flow 308 407 emitStatus(options, `Starting OAuth flow for ${handle}...`) 309 408 ··· 387 486 388 487 if (isBun) { 389 488 const bunServer = Bun.serve({ 390 - port: LOOPBACK_PORT, 489 + port: oauthPort, 391 490 hostname: LOOPBACK_HOST, 392 491 fetch: app.fetch, 393 492 }) ··· 395 494 } else { 396 495 const nodeServer = honoNodeServe({ 397 496 fetch: app.fetch, 398 - port: LOOPBACK_PORT, 497 + port: oauthPort, 399 498 hostname: LOOPBACK_HOST, 400 499 }) 401 500 serverHandle = { close: () => nodeServer.close() } ··· 440 539 441 540 // Map the current directory to this DID for future restores 442 541 kvSet(kv, dirKey, did) 542 + if (oauthPort === LOOPBACK_PORT) { 543 + kv.del(dirOAuthPortKey) 544 + } else { 545 + kvSet(kv, dirOAuthPortKey, String(oauthPort)) 546 + } 443 547 444 548 emitStatus(options, `Authenticated as ${did}`) 445 549 ··· 478 582 * Authenticate - tries OAuth if no password provided, otherwise uses app password 479 583 */ 480 584 export async function authenticate(handle?: string, options: AuthOptions = {}): Promise<{ agent: Agent; did: string }> { 481 - if (options.appPassword) { 585 + if (options.appPassword !== undefined) { 586 + const trimmedPassword = options.appPassword.trim() 587 + if (!trimmedPassword) { 588 + throw new Error('App password is required when using --password') 589 + } 482 590 if (!handle) throw new Error('Handle required with app password authentication') 483 - return authenticateAppPassword(handle, options.appPassword, undefined, options) 591 + return authenticateAppPassword(handle, trimmedPassword, undefined, options) 484 592 } 485 593 return authenticateOAuth(handle, options) 486 594 }
+15
cli/lib/command-utils.ts
··· 34 34 35 35 const OAUTH_FALLBACK_PREFIX = 'If browser does not open, visit: ' 36 36 const MAX_SPINNER_TEXT_LENGTH = 120 37 + const APP_PASSWORD_PATTERN = /^[a-z0-9]{4}(?:-[a-z0-9]{4}){3}$/i 37 38 38 39 function truncateSpinnerText(message: string): string { 39 40 const compact = message.replace(/\s+/g, ' ').trim() ··· 54 55 55 56 spinner.text = truncateSpinnerText(message) 56 57 } 58 + } 59 + 60 + function looksLikeHandle(value: string): boolean { 61 + return value.includes('.') && /^[a-z0-9._:-]+$/i.test(value) 62 + } 63 + 64 + function looksLikeAppPassword(value: string): boolean { 65 + return APP_PASSWORD_PATTERN.test(value) 57 66 } 58 67 59 68 export async function resolveIdentifier( ··· 90 99 identifier: string | undefined, 91 100 options: XrpcCommandOptions, 92 101 ): Promise<{ agent: Agent; serviceDid: string; did: string }> { 102 + if (!identifier && options.password && looksLikeHandle(options.password) && !looksLikeAppPassword(options.password)) { 103 + throw new Error( 104 + '`--password` appears to have consumed the handle argument. Provide a password value and pass the handle separately.', 105 + ) 106 + } 107 + 93 108 // Skip the handle prompt if the current directory already has a stored session 94 109 const resolvedIdentifier = 95 110 identifier ?? ((await hasDirSession(options.db)) ? undefined : await resolveIdentifier(undefined))
+11 -4
docs/src/content/docs/cli.md
··· 45 45 46 46 </a> 47 47 48 + <a href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-windows.exe" class="download-link" download=""> 49 + 50 + <span class="platform">Windows (x86_64):</span> wisp-cli-x86_64-windows.exe 51 + 52 + </a> 53 + 48 54 <h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">SHA-256 Checksums</h3> 49 55 50 56 <pre style="font-size: 0.75rem; padding: 1rem;" class="language-bash" tabindex="0"><code class="language-bash"> 51 - 06544b3a3e27a4b8d7b3a46a39fb7205cf90b3061e19fe533b090facd604f375 wisp-cli-aarch64-darwin 52 - 9ec523e3ceef927b37adc52d449dcd9e13ea84fa49b0b77f0d5932c94cfe262e wisp-cli-x86_64-darwin 53 - 42a262668e13dce36173a4096cdc2b22358b805cf192335f84534c7f695d395b wisp-cli-aarch64-linux 54 - 589ee59f3959ddfbc12fea38d2bcb91701f1362f560ae6fd506bebea3150e2cc wisp-cli-x86_64-linux 57 + 70ffab694c6c19807dc234eb8e85da358406166230c4dfc4ac2de141b1e9000f wisp-cli-aarch64-darwin 58 + 19bcd4126382e4d442a5590d65de5d06feb78094bacaf7e17514d25f3999932a wisp-cli-x86_64-darwin 59 + a8999f210d0a8b7bb11ce6f592c8dc4c7e889066e215040812b607db677ada2a wisp-cli-aarch64-linux 60 + 5948d8842e0f7578b00b9ad08d77e5bac0cf3b2fe65f2b19c631868e94ad025a wisp-cli-x86_64-linux 61 + 18c207001cf4d47961cfca63bf149a913d31ffa4ef04c2b01379b7274480d36e wisp-cli-x86_64-windows.exe 55 62 </code></pre> 56 63 57 64 </div>