AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

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

feat: support prompt=create in serverLogin and /oauth/login route

Extend serverLogin to accept prompt and pds options for account
creation. Update /oauth/login route to pass through prompt and pds
query params. Bump version to 0.0.1-alpha.48.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+53 -17
+1 -1
packages/hatk/package.json
··· 1 1 { 2 2 "name": "@hatk/hatk", 3 - "version": "0.0.1-alpha.47", 3 + "version": "0.0.1-alpha.48", 4 4 "license": "MIT", 5 5 "bin": { 6 6 "hatk": "dist/cli.js"
+47 -13
packages/hatk/src/oauth/server.ts
··· 366 366 367 367 // --- Server-initiated login (no DPoP required from browser) --- 368 368 369 - export async function serverLogin(config: OAuthConfig, handle: string): Promise<string> { 370 - // Resolve handle to DID 371 - let did = handle 372 - if (!did.startsWith('did:')) { 373 - did = await resolveHandle(handle, _relayUrl) 369 + /** 370 + * Initiate a server-side OAuth login or account creation flow. 371 + * 372 + * For account creation, pass `{ prompt: 'create', pds: 'selfhosted.social' }`. 373 + * The `pds` is a bare hostname; the auth server is discovered from its 374 + * protected resource metadata. 375 + */ 376 + export async function serverLogin( 377 + config: OAuthConfig, 378 + handle: string, 379 + options?: { prompt?: string; pds?: string }, 380 + ): Promise<string> { 381 + let did: string | undefined 382 + let pdsAuthServer: string 383 + let pdsEndpoint: string 384 + 385 + if (options?.prompt === 'create' && options?.pds) { 386 + // Account creation: discover auth server from PDS hostname 387 + const pdsUrl = options.pds.startsWith('http') 388 + ? options.pds 389 + : options.pds.match(/^localhost[:/]/) 390 + ? `http://${options.pds}` 391 + : `https://${options.pds}` 392 + pdsEndpoint = pdsUrl 393 + const protectedResource = await fetchProtectedResourceMetadata(pdsUrl) 394 + pdsAuthServer = protectedResource.authorization_servers[0] 395 + if (!pdsAuthServer) throw new Error(`No auth server for PDS ${pdsUrl}`) 396 + } else { 397 + // Normal login: resolve handle to DID 398 + did = handle 399 + if (!did.startsWith('did:')) { 400 + did = await resolveHandle(handle, _relayUrl) 401 + } 402 + const discovery = await discoverAuthServer(did, _plcUrl) 403 + pdsAuthServer = discovery.authServerEndpoint 404 + pdsEndpoint = discovery.pdsEndpoint 374 405 } 375 406 376 - // Discover PDS auth server 377 - const discovery = await discoverAuthServer(did, _plcUrl) 378 - const pdsAuthServer = discovery.authServerEndpoint 379 - const pdsEndpoint = discovery.pdsEndpoint 407 + const authServerMetadata = await fetchAuthServerMetadata(pdsAuthServer) 380 408 381 409 // Create PKCE for PAR to PDS 382 410 const pdsCodeVerifier = randomToken() ··· 384 412 const pdsState = randomToken() 385 413 386 414 // PAR to the PDS 387 - const parEndpoint = discovery.authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par` 415 + const parEndpoint = authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par` 388 416 const serverDpopProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', parEndpoint) 389 417 390 418 const scope = config.scopes?.join(' ') || 'atproto transition:generic' 391 - const pdsParBody = new URLSearchParams({ 419 + const pdsParParams: Record<string, string> = { 392 420 client_id: pdsClientId(config.issuer, config), 393 421 redirect_uri: pdsRedirectUri(config.issuer), 394 422 response_type: 'code', 395 423 code_challenge: pdsCodeChallenge, 396 424 code_challenge_method: 'S256', 397 425 scope, 398 - login_hint: handle, 399 426 state: pdsState, 400 - }) 427 + } 428 + if (options?.prompt === 'create') { 429 + pdsParParams.prompt = 'create' 430 + } 431 + if (did) { 432 + pdsParParams.login_hint = handle 433 + } 434 + const pdsParBody = new URLSearchParams(pdsParParams) 401 435 402 436 let pdsRequestUri: string | undefined 403 437
+5 -3
packages/hatk/src/server.ts
··· 818 818 819 819 // OAuth Login (server-initiated, no DPoP required) 820 820 if (url.pathname === '/oauth/login' && oauth) { 821 - const handle = url.searchParams.get('handle') 822 - if (!handle) return withCors(jsonError(400, 'handle required', acceptEncoding)) 821 + const handle = url.searchParams.get('handle') || '' 822 + const prompt = url.searchParams.get('prompt') || undefined 823 + const pds = url.searchParams.get('pds') || undefined 824 + if (!handle && prompt !== 'create') return withCors(jsonError(400, 'handle required', acceptEncoding)) 823 825 try { 824 - const redirectUrl = await serverLogin(oauth, handle) 826 + const redirectUrl = await serverLogin(oauth, handle, { prompt, pds }) 825 827 return new Response(null, { 826 828 status: 302, 827 829 headers: { Location: redirectUrl, 'Set-Cookie': clearSessionCookieHeader() },