Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
86
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>