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

more xrpc routes

+1126 -174
+5 -18
README.md
··· 59 59 60 60 `apps/main-app` exposes domain claim/status XRPC endpoints: 61 61 62 + - `place.wisp.v2.domain.claimSubdomain` (procedure / POST, wisp handles) 62 63 - `place.wisp.v2.domain.claim` (procedure / POST) 64 + - `place.wisp.v2.domain.delete` (procedure / POST) 65 + - `place.wisp.v2.domain.getList` (query / GET) 63 66 - `place.wisp.v2.domain.getStatus` (query / GET) 64 67 65 68 The server validates **serviceAuth JWTs** (not cookie auth, not direct end-user access JWTs) on `/xrpc/*`. ··· 83 86 84 87 ### Local TLS Requirement (No Auto Cert Generation) 85 88 86 - Some PDS proxy flows require HTTPS on `:443` for the proxied service endpoint. 87 - Cert generation is intentionally manual so SANs are explicit and correct for your environment. 88 - 89 - Example with `mkcert`: 90 - 91 - ```bash 92 - mkcert -cert-file certs/dev-cert.pem -key-file certs/dev-key.pem regentsmacbookair localhost 100.64.0.2 93 - ``` 94 - 95 - Use SANs that match exactly what your PDS will call (hostname and/or IP). 96 - `apps/main-app` can terminate TLS directly in local dev with: 97 - 98 - ```env 99 - PORT=443 100 - LOCAL_DEV_TLS=true 101 - LOCAL_TLS_CERT_PATH=./certs/dev-cert.pem 102 - LOCAL_TLS_KEY_PATH=./certs/dev-key.pem 103 - ``` 89 + `apps/main-app` now serves HTTP only. If you need HTTPS in local/proxy flows, 90 + terminate TLS in your reverse proxy or tunnel layer and forward plain HTTP to main-app. 104 91 105 92 106 93 ```bash
+25 -38
apps/main-app/src/index.ts
··· 1 1 // Fix for Elysia issue with Bun, (see https://github.com/oven-sh/bun/issues/12161) 2 2 process.getBuiltinModule = require; 3 3 4 - import { existsSync } from 'node:fs' 5 - 6 4 import { Elysia, t } from 'elysia' 7 5 import type { Context } from 'elysia' 8 6 import { cors } from '@elysiajs/cors' ··· 50 48 const didServiceIds = parsedServiceIds.length > 0 51 49 ? Array.from(new Set(parsedServiceIds)) 52 50 : ['#wisp_xrpc'] 53 - const serverPort = Number(Bun.env.PORT ?? (isLocalDev ? '443' : '80')) 54 - const localTlsEnabled = isLocalDev && Bun.env.LOCAL_DEV_TLS !== 'false' 55 - const localTlsCertPath = Bun.env.LOCAL_TLS_CERT_PATH ?? './certs/dev-cert.pem' 56 - const localTlsKeyPath = Bun.env.LOCAL_TLS_KEY_PATH ?? './certs/dev-key.pem' 51 + const serverPort = Number(Bun.env.PORT ?? (isLocalDev ? '8000' : '80')) 57 52 58 - logger.info('[Server] Local TLS config', { 53 + logger.info('[Server] Startup config', { 59 54 isLocalDev, 60 - localTlsEnabled, 61 - port: serverPort, 62 - certPath: localTlsEnabled ? localTlsCertPath : undefined, 63 - keyPath: localTlsEnabled ? localTlsKeyPath : undefined 55 + port: serverPort 64 56 }) 65 57 66 58 const config: Config = { ··· 124 116 }) 125 117 // Observability middleware 126 118 .onBeforeHandle(observabilityMiddleware('main-app').beforeHandle) 119 + .onRequest(({ request }) => { 120 + if (isLocalDev) { 121 + const pathname = new URL(request.url).pathname 122 + if (pathname.startsWith('/xrpc/')) { 123 + console.log('[Server] Incoming /xrpc request', { 124 + method: request.method, 125 + path: pathname 126 + }) 127 + } 128 + } 129 + }) 127 130 .onAfterHandle((ctx: Context) => { 128 131 observabilityMiddleware('main-app').afterHandle(ctx) 129 132 // Security headers middleware ··· 214 217 return html.replaceAll('{{ATPROTO_LOGIN_URL}}', atprotoLoginUrl) 215 218 }) 216 219 .use(authRoutes(client, cookieSecret)) 217 - .use(xrpcRoutes()) 218 220 .use(wispRoutes(client, cookieSecret)) 219 221 .use(domainRoutes(client, cookieSecret)) 220 222 .use(userRoutes(client, cookieSecret)) ··· 223 225 .use( 224 226 await staticPlugin({ 225 227 assets: './apps/main-app/public', 226 - prefix: '/' 228 + prefix: '/', 229 + // Prevent dev-mode GET /* fallback from swallowing XRPC GET routes. 230 + alwaysStatic: true, 231 + staticLimit: 10000 227 232 }) 228 233 ) 229 234 // Production only: serve built assets from dist ··· 257 262 }) 258 263 : (app) => app 259 264 ) 265 + // Keep XRPC after static in dev, since staticPlugin(prefix='/') installs GET /* fallback. 266 + .use(xrpcRoutes()) 260 267 // Production only: serve built admin assets 261 268 .use( 262 269 Bun.env.NODE_ENV === 'production' ··· 436 443 exposeHeaders: ['Content-Type', 'DPoP-Nonce', 'dpop-nonce'], 437 444 maxAge: 86400 // 24 hours 438 445 })) 439 - .listen( 440 - localTlsEnabled 441 - ? (() => { 442 - if (!existsSync(localTlsCertPath)) { 443 - throw new Error(`LOCAL_DEV TLS cert not found at ${localTlsCertPath}`) 444 - } 445 - if (!existsSync(localTlsKeyPath)) { 446 - throw new Error(`LOCAL_DEV TLS key not found at ${localTlsKeyPath}`) 447 - } 448 - 449 - return { 450 - port: serverPort, 451 - hostname: '0.0.0.0', 452 - tls: { 453 - cert: Bun.file(localTlsCertPath), 454 - key: Bun.file(localTlsKeyPath) 455 - } 456 - } 457 - })() 458 - : { 459 - port: serverPort, 460 - hostname: '0.0.0.0' 461 - } 462 - ) 446 + .listen({ 447 + port: serverPort, 448 + hostname: '0.0.0.0' 449 + }) 463 450 464 451 console.log( 465 - `🦊 Elysia is running at ${localTlsEnabled ? 'https' : 'http'}://${app.server?.hostname}:${app.server?.port}` 452 + `🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}` 466 453 ) 467 454 468 455 // Graceful shutdown
+1 -2
apps/main-app/src/lib/db.ts
··· 421 421 export const claimCustomDomain = async (did: string, domain: string, hash: string, rkey: string | null = null) => { 422 422 const domainLower = domain.toLowerCase(); 423 423 try { 424 - // Use UPSERT with ON CONFLICT to handle existing pending domains 425 424 const result = await db` 426 425 INSERT INTO custom_domains (id, domain, did, rkey, verified, created_at) 427 426 VALUES (${hash}, ${domainLower}, ${did}, ${rkey}, false, EXTRACT(EPOCH FROM NOW())) ··· 436 435 `; 437 436 438 437 if (result.length === 0) { 439 - // No rows were updated, meaning the domain exists and is verified 438 + console.log('Failed to claim custom domain - already verified by another user'); 440 439 throw new Error('conflict'); 441 440 } 442 441
+12 -3
apps/main-app/src/routes/domain.ts
··· 219 219 throw new Error(`Invalid domain: ${domainError}`) 220 220 } 221 221 222 - // Check if already exists and is verified 222 + // Verified claims are DID-locked. Pending claims can be reclaimed. 223 223 const existing = await getCustomDomainInfo(domainLower); 224 - if (existing && existing.verified) { 224 + if (existing && existing.verified && existing.did !== auth.did) { 225 225 set.status = 409 226 - throw new Error('Domain already verified and claimed'); 226 + throw new Error('Domain already claimed'); 227 + } 228 + 229 + if (existing && existing.did === auth.did) { 230 + return { 231 + success: true, 232 + id: existing.id, 233 + domain: domainLower, 234 + verified: Boolean(existing.verified) 235 + }; 227 236 } 228 237 229 238 // Create hash for ID
+353 -98
apps/main-app/src/routes/xrpc.ts
··· 5 5 import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver'; 6 6 import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 7 7 import { ServiceJwtVerifier } from '@atcute/xrpc-server/auth'; 8 - import { PlaceWispV2DomainClaim, PlaceWispV2DomainGetStatus } from '@wispplace/lexicons/atcute'; 8 + import { 9 + PlaceWispV2DomainClaim, 10 + PlaceWispV2DomainClaimSubdomain, 11 + PlaceWispV2DomainDelete, 12 + PlaceWispV2DomainGetList, 13 + PlaceWispV2DomainGetStatus, 14 + } from '@wispplace/lexicons/atcute'; 9 15 import { BASE_HOST } from '@wispplace/constants'; 10 16 11 17 import { createLogger } from '@wispplace/observability'; ··· 13 19 import { 14 20 claimCustomDomain, 15 21 claimDomain, 22 + deleteCustomDomain, 23 + deleteWispDomain, 24 + getAllWispDomains, 25 + getCustomDomainsByDid, 16 26 getCustomDomainInfo, 17 27 isDomainRegistered, 18 28 updateCustomDomainRkey, ··· 22 32 extractWispHandle, 23 33 isValidHandle, 24 34 normalizeDomain, 35 + toDomain, 25 36 validateCustomDomain, 26 37 } from '../lib/domain-utils'; 27 38 ··· 45 56 }); 46 57 47 58 const NSID_ALIASES: Record<string, string> = { 59 + 'place.wisp.v2.domain.claim-subdomain': 'place.wisp.v2.domain.claimSubdomain', 60 + 'place.wisp.v2.domain.claimsubdomain': 'place.wisp.v2.domain.claimSubdomain', 61 + 'place.wisp.v2.domain.claimsub-domain': 'place.wisp.v2.domain.claimSubdomain', 62 + 'place.wisp.v2.domain.get-list': 'place.wisp.v2.domain.getList', 63 + 'place.wisp.v2.domain.getlist': 'place.wisp.v2.domain.getList', 48 64 'place.wisp.v2.domain.getstatus': 'place.wisp.v2.domain.getStatus', 49 65 'place.wisp.v2.domain.get-status': 'place.wisp.v2.domain.getStatus', 50 66 }; 67 + 68 + const XRPC_NSIDS = { 69 + getStatus: 'place.wisp.v2.domain.getStatus', 70 + getList: 'place.wisp.v2.domain.getList', 71 + claimSubdomain: 'place.wisp.v2.domain.claimSubdomain', 72 + claim: 'place.wisp.v2.domain.claim', 73 + delete: 'place.wisp.v2.domain.delete', 74 + } as const; 51 75 52 76 const toIsoFromEpoch = (epoch: unknown): string | undefined => { 53 77 let numeric: number | undefined; ··· 111 135 }); 112 136 }; 113 137 138 + const notFound = (description = 'domain not found'): never => { 139 + throw new XRPCError({ 140 + status: 404, 141 + error: 'NotFound', 142 + description, 143 + }); 144 + }; 145 + 114 146 const requireAuthenticated = (auth: XrpcAuthContext | undefined): XrpcAuthContext => { 115 147 if (!auth) { 116 148 authRequired(); ··· 193 225 194 226 const normalizeNsidPath = (request: Request): { request: Request; rawNsid: string; nsid: string } => { 195 227 const url = new URL(request.url); 196 - const rawNsid = url.pathname.startsWith('/xrpc/') ? url.pathname.slice('/xrpc/'.length) : url.pathname; 228 + const rawNsidFull = url.pathname.startsWith('/xrpc/') ? url.pathname.slice('/xrpc/'.length) : url.pathname; 229 + const rawNsid = rawNsidFull.replace(/^\/+|\/+$/g, ''); 197 230 const nsid = NSID_ALIASES[rawNsid] ?? rawNsid; 198 231 199 232 if (nsid === rawNsid) { ··· 209 242 }; 210 243 }; 211 244 245 + const withNsid = <T extends { nsid: string }>(schema: T, nsid: string): T => { 246 + if (schema.nsid === nsid) { 247 + return schema; 248 + } 249 + 250 + return { ...schema, nsid } as T; 251 + }; 252 + 253 + const addQueryWithAliases = ( 254 + router: XRPCRouter, 255 + schema: { nsid: string }, 256 + aliases: readonly string[], 257 + config: { handler: (ctx: any) => Promise<Response> | Response }, 258 + ) => { 259 + const seen = new Set<string>(); 260 + for (const nsid of [schema.nsid, ...aliases]) { 261 + if (seen.has(nsid)) { 262 + continue; 263 + } 264 + seen.add(nsid); 265 + router.addQuery(withNsid(schema as any, nsid), config as any); 266 + } 267 + }; 268 + 269 + const addProcedureWithAliases = ( 270 + router: XRPCRouter, 271 + schema: { nsid: string }, 272 + aliases: readonly string[], 273 + config: { handler: (ctx: any) => Promise<Response> | Response }, 274 + ) => { 275 + const seen = new Set<string>(); 276 + for (const nsid of [schema.nsid, ...aliases]) { 277 + if (seen.has(nsid)) { 278 + continue; 279 + } 280 + seen.add(nsid); 281 + router.addProcedure(withNsid(schema as any, nsid), config as any); 282 + } 283 + }; 284 + 285 + const claimWispSubdomain = async ( 286 + did: DidString, 287 + input: { handle: string; siteRkey?: string }, 288 + ) => { 289 + const handle = input.handle.trim().toLowerCase(); 290 + if (!isValidHandle(handle)) { 291 + invalidDomain('invalid wisp subdomain handle'); 292 + } 293 + 294 + const domain = toDomain(handle); 295 + const existing = await isDomainRegistered(domain); 296 + if (existing.registered && existing.did !== did) { 297 + alreadyClaimed('domain is already claimed'); 298 + } 299 + 300 + if (existing.registered && existing.did === did) { 301 + if (input.siteRkey !== undefined) { 302 + await updateWispDomainSite(domain, input.siteRkey); 303 + } 304 + 305 + return json({ 306 + domain, 307 + kind: 'wisp', 308 + status: 'verified', 309 + siteRkey: input.siteRkey ?? existing.rkey ?? undefined, 310 + }); 311 + } 312 + 313 + try { 314 + await claimDomain(did, handle); 315 + } catch (err) { 316 + const message = err instanceof Error ? err.message : ''; 317 + 318 + if (message === 'domain_limit_reached') { 319 + domainLimitReached(); 320 + } 321 + if (message === 'invalid_handle') { 322 + invalidDomain('invalid wisp subdomain handle'); 323 + } 324 + 325 + alreadyClaimed('domain is already claimed'); 326 + } 327 + 328 + if (input.siteRkey !== undefined) { 329 + await updateWispDomainSite(domain, input.siteRkey); 330 + } 331 + 332 + return json({ 333 + domain, 334 + kind: 'wisp', 335 + status: 'verified', 336 + siteRkey: input.siteRkey, 337 + }); 338 + }; 339 + 340 + const claimCustomDomainForDid = async ( 341 + did: DidString, 342 + input: { domain: string; siteRkey?: string }, 343 + ) => { 344 + const domain = normalizeDomain(input.domain); 345 + if (domain.length === 0) { 346 + invalidDomain('domain is required'); 347 + } 348 + 349 + if (extractWispHandle(domain) !== null) { 350 + invalidDomain('wisp subdomains must be claimed via place.wisp.v2.domain.claimSubdomain'); 351 + } 352 + 353 + const customError = validateCustomDomain(domain); 354 + if (customError !== null) { 355 + invalidDomain(customError); 356 + } 357 + 358 + const existing = await getCustomDomainInfo(domain); 359 + if (existing && existing.verified && existing.did !== did) { 360 + alreadyClaimed('domain is already claimed'); 361 + } 362 + 363 + if (existing && existing.did === did) { 364 + if (input.siteRkey !== undefined) { 365 + await updateCustomDomainRkey(existing.id, input.siteRkey); 366 + } 367 + 368 + const status = existing.verified ? 'verified' : 'pendingVerification'; 369 + 370 + return json({ 371 + domain, 372 + kind: 'custom', 373 + status, 374 + siteRkey: input.siteRkey ?? existing.rkey ?? undefined, 375 + ...buildCustomDnsInstructions(domain, did, existing.id), 376 + }); 377 + } 378 + 379 + const challengeId = createHash('sha256').update(`${did}:${domain}`).digest('hex').substring(0, 16); 380 + 381 + try { 382 + await claimCustomDomain(did, domain, challengeId, input.siteRkey ?? null); 383 + } catch { 384 + alreadyClaimed('domain is already claimed'); 385 + } 386 + 387 + return json({ 388 + domain, 389 + kind: 'custom', 390 + status: 'pendingVerification', 391 + siteRkey: input.siteRkey, 392 + ...buildCustomDnsInstructions(domain, did, challengeId), 393 + }); 394 + }; 395 + 212 396 export const xrpcRoutes = () => { 213 397 const authByRequest = new WeakMap<Request, XrpcAuthContext>(); 214 398 const router = new XRPCRouter(); 399 + const registeredNsids = [ 400 + XRPC_NSIDS.getStatus, 401 + XRPC_NSIDS.getList, 402 + XRPC_NSIDS.claimSubdomain, 403 + XRPC_NSIDS.claim, 404 + XRPC_NSIDS.delete, 405 + ]; 215 406 216 - router.addQuery(PlaceWispV2DomainGetStatus.mainSchema, { 407 + addQueryWithAliases( 408 + router, 409 + withNsid(PlaceWispV2DomainGetStatus.mainSchema as any, XRPC_NSIDS.getStatus), 410 + ['place.wisp.v2.domain.getstatus', 'place.wisp.v2.domain.get-status'], 411 + { 217 412 async handler({ params, request }) { 218 413 const domain = normalizeDomain(params.domain); 219 414 const auth = authByRequest.get(request); ··· 233 428 const kind = info.type; 234 429 const ownedByCaller = auth ? auth.did === info.did : undefined; 235 430 236 - if (auth && ownedByCaller === false) { 431 + if ( 432 + auth && 433 + ownedByCaller === false && 434 + (kind === 'wisp' || (kind === 'custom' && Boolean(info.verified))) 435 + ) { 237 436 return json({ 238 437 domain, 239 438 kind, ··· 265 464 siteRkey: info.rkey ?? undefined, 266 465 }); 267 466 }, 268 - }); 467 + }, 468 + ); 269 469 270 - router.addProcedure(PlaceWispV2DomainClaim.mainSchema, { 271 - async handler({ input, request }) { 470 + addQueryWithAliases( 471 + router, 472 + withNsid(PlaceWispV2DomainGetList.mainSchema as any, XRPC_NSIDS.getList), 473 + ['place.wisp.v2.domain.getlist', 'place.wisp.v2.domain.get-list'], 474 + { 475 + async handler({ request }) { 272 476 const auth = requireAuthenticated(authByRequest.get(request)); 273 477 const did = auth.did as DidString; 274 478 275 - const domain = normalizeDomain(input.domain); 276 - if (domain.length === 0) { 277 - invalidDomain('domain is required'); 278 - } 479 + const [wispDomains, customDomains] = await Promise.all([ 480 + getAllWispDomains(did), 481 + getCustomDomainsByDid(did), 482 + ]); 279 483 280 - const wispHandle = extractWispHandle(domain); 281 - if (wispHandle !== null) { 282 - if (!isValidHandle(wispHandle)) { 283 - invalidDomain('invalid wisp subdomain handle'); 284 - } 484 + const domains = [ 485 + ...wispDomains.map((entry: { domain: string; rkey: string | null }) => ({ 486 + domain: entry.domain as string, 487 + kind: 'wisp' as const, 488 + status: 'verified' as const, 489 + verified: true, 490 + siteRkey: entry.rkey ?? undefined, 491 + })), 492 + ...customDomains.map((entry: { 493 + domain: string; 494 + verified: boolean; 495 + rkey: string | null; 496 + last_verified_at?: number | string | null; 497 + }) => ({ 498 + domain: entry.domain as string, 499 + kind: 'custom' as const, 500 + status: entry.verified ? 'verified' as const : 'pendingVerification' as const, 501 + verified: Boolean(entry.verified), 502 + siteRkey: entry.rkey ?? undefined, 503 + lastCheckedAt: toIsoFromEpoch(entry.last_verified_at), 504 + })), 505 + ].sort((a, b) => a.domain.localeCompare(b.domain)); 285 506 286 - const existing = await isDomainRegistered(domain); 287 - if (existing.registered && existing.did !== did) { 288 - alreadyClaimed('domain is already claimed'); 289 - } 507 + return json({ domains }); 508 + }, 509 + }, 510 + ); 290 511 291 - if (existing.registered && existing.did === did) { 292 - if (input.siteRkey !== undefined) { 293 - await updateWispDomainSite(domain, input.siteRkey); 294 - } 512 + addProcedureWithAliases( 513 + router, 514 + withNsid(PlaceWispV2DomainClaimSubdomain.mainSchema as any, XRPC_NSIDS.claimSubdomain), 515 + ['place.wisp.v2.domain.claimsubdomain', 'place.wisp.v2.domain.claim-subdomain'], 516 + { 517 + async handler({ input, request }) { 518 + const auth = requireAuthenticated(authByRequest.get(request)); 519 + const did = auth.did as DidString; 295 520 296 - return json({ 297 - domain, 298 - kind: 'wisp', 299 - status: 'alreadyClaimed', 300 - siteRkey: input.siteRkey ?? existing.rkey ?? undefined, 301 - }); 302 - } 521 + return claimWispSubdomain(did, { 522 + handle: input.handle, 523 + siteRkey: input.siteRkey, 524 + }); 525 + }, 526 + }, 527 + ); 303 528 304 - try { 305 - await claimDomain(did, wispHandle); 306 - } catch (err) { 307 - const message = err instanceof Error ? err.message : ''; 308 - 309 - if (message === 'domain_limit_reached') { 310 - domainLimitReached(); 311 - } 312 - if (message === 'invalid_handle') { 313 - invalidDomain('invalid wisp subdomain handle'); 314 - } 529 + addProcedureWithAliases( 530 + router, 531 + withNsid(PlaceWispV2DomainClaim.mainSchema as any, XRPC_NSIDS.claim), 532 + [], 533 + { 534 + async handler({ input, request }) { 535 + const auth = requireAuthenticated(authByRequest.get(request)); 536 + const did = auth.did as DidString; 315 537 316 - alreadyClaimed('domain is already claimed'); 317 - } 538 + return claimCustomDomainForDid(did, { 539 + domain: input.domain, 540 + siteRkey: input.siteRkey, 541 + }); 542 + }, 543 + }, 544 + ); 318 545 319 - if (input.siteRkey !== undefined) { 320 - await updateWispDomainSite(domain, input.siteRkey); 321 - } 546 + addProcedureWithAliases( 547 + router, 548 + withNsid(PlaceWispV2DomainDelete.mainSchema as any, XRPC_NSIDS.delete), 549 + [], 550 + { 551 + async handler({ params, request }) { 552 + const auth = requireAuthenticated(authByRequest.get(request)); 553 + const did = auth.did as DidString; 322 554 323 - return json({ 324 - domain, 325 - kind: 'wisp', 326 - status: 'verified', 327 - siteRkey: input.siteRkey, 328 - }); 555 + const domain = normalizeDomain(params.domain); 556 + if (domain.length === 0) { 557 + invalidDomain('domain is required'); 329 558 } 330 559 331 - const customError = validateCustomDomain(domain); 332 - if (customError !== null) { 333 - invalidDomain(customError); 560 + const existing = await isDomainRegistered(domain); 561 + if (!existing.registered) { 562 + notFound(); 334 563 } 335 564 336 - const existing = await getCustomDomainInfo(domain); 337 - if (existing && existing.verified && existing.did !== did) { 338 - alreadyClaimed('domain already verified and owned by another user'); 565 + if (existing.did !== did) { 566 + notFound(); 339 567 } 340 568 341 - if (existing && existing.did === did) { 342 - if (input.siteRkey !== undefined) { 343 - await updateCustomDomainRkey(existing.id, input.siteRkey); 569 + if (existing.type === 'wisp') { 570 + await deleteWispDomain(domain); 571 + } else { 572 + const custom = await getCustomDomainInfo(domain); 573 + if (!custom || custom.did !== did) { 574 + notFound(); 344 575 } 345 576 346 - const status = existing.verified ? 'verified' : 'pendingVerification'; 347 - 348 - return json({ 349 - domain, 350 - kind: 'custom', 351 - status, 352 - siteRkey: input.siteRkey ?? existing.rkey ?? undefined, 353 - ...buildCustomDnsInstructions(domain, did, existing.id), 354 - }); 355 - } 356 - 357 - const challengeId = createHash('sha256').update(`${did}:${domain}`).digest('hex').substring(0, 16); 358 - 359 - try { 360 - await claimCustomDomain(did, domain, challengeId, input.siteRkey ?? null); 361 - } catch (err) { 362 - alreadyClaimed('domain already verified and owned by another user'); 577 + await deleteCustomDomain(custom.id as string); 363 578 } 364 579 365 580 return json({ 366 581 domain, 367 - kind: 'custom', 368 - status: 'pendingVerification', 369 - siteRkey: input.siteRkey, 370 - ...buildCustomDnsInstructions(domain, did, challengeId), 582 + deleted: true, 371 583 }); 372 584 }, 585 + }, 586 + ); 587 + 588 + const schemaNsids = { 589 + getStatus: (PlaceWispV2DomainGetStatus.mainSchema as any).nsid, 590 + getList: (PlaceWispV2DomainGetList.mainSchema as any).nsid, 591 + claimSubdomain: (PlaceWispV2DomainClaimSubdomain.mainSchema as any).nsid, 592 + claim: (PlaceWispV2DomainClaim.mainSchema as any).nsid, 593 + delete: (PlaceWispV2DomainDelete.mainSchema as any).nsid, 594 + }; 595 + logger.info('[XRPC] Registered methods', { 596 + expectedNsids: registeredNsids, 597 + schemaNsids, 373 598 }); 599 + if (isLocalDev) { 600 + console.log('[XRPC] Registered methods', { 601 + expectedNsids: registeredNsids, 602 + schemaNsids, 603 + }); 604 + } 374 605 375 - return new Elysia().all('/xrpc/*', async ({ body, request }) => { 606 + const handleXrpcRequest = async (request: Request, body: unknown): Promise<Response> => { 376 607 const startedAt = Date.now(); 377 608 let xrpcRequest: Request | undefined; 378 609 let nsid = ''; ··· 387 618 nsid = normalized.nsid; 388 619 389 620 const authorization = xrpcRequest.headers.get('authorization'); 390 - logger.info('[XRPC] Incoming request', { 621 + const origin = xrpcRequest.headers.get('origin') ?? '-'; 622 + const authScheme = authorization ? authorization.split(' ')[0] : '-'; 623 + logger.info('[XRPC] Incoming', { 391 624 method: xrpcRequest.method, 392 625 rawNsid, 393 626 nsid, 394 - origin: xrpcRequest.headers.get('origin') ?? undefined, 395 - hasAuthorization: Boolean(authorization), 396 - authorizationScheme: authorization ? authorization.split(' ')[0] : undefined, 627 + origin, 628 + hasAuth: Boolean(authorization), 629 + scheme: authScheme, 397 630 }); 631 + if (isLocalDev) { 632 + console.log('[XRPC] Incoming', { 633 + method: xrpcRequest.method, 634 + rawNsid, 635 + nsid, 636 + origin, 637 + hasAuth: Boolean(authorization), 638 + scheme: authScheme, 639 + }); 640 + } 398 641 399 642 auth = await resolveServiceAuth(xrpcRequest, nsid); 400 643 if (auth) { ··· 411 654 responseData = await response.clone().text(); 412 655 } 413 656 414 - logger.warn('[XRPC] Request failed', { 657 + logger.warn('[XRPC] Failed', { 415 658 method: xrpcRequest.method, 416 659 rawNsid, 417 660 nsid, 418 661 status: response.status, 419 - did: auth?.did, 662 + did: auth?.did ?? '-', 663 + durationMs: Date.now() - startedAt, 420 664 origin: xrpcRequest.headers.get('origin') ?? undefined, 421 665 requestBodyUsed: request.bodyUsed, 422 666 error: responseData, 423 - durationMs: Date.now() - startedAt, 424 667 }); 425 668 } else { 426 - logger.info('[XRPC] Request succeeded', { 669 + logger.info('[XRPC] Succeeded', { 427 670 method: xrpcRequest.method, 428 671 rawNsid, 429 672 nsid, 430 673 status: response.status, 431 - did: auth?.did, 674 + did: auth?.did ?? '-', 432 675 durationMs: Date.now() - startedAt, 433 676 }); 434 677 } 435 678 436 679 return response; 437 680 } catch (err) { 438 - logger.error('[XRPC] Handler error', { 681 + logger.error('[XRPC] Handler error', err, { 439 682 method: xrpcRequest?.method ?? request.method, 440 - rawNsid: rawNsid || undefined, 441 - nsid: nsid || undefined, 442 - origin: request.headers.get('origin') ?? undefined, 683 + rawNsid: rawNsid || '-', 684 + nsid: nsid || '-', 443 685 durationMs: Date.now() - startedAt, 444 686 error: err instanceof Error ? err.message : String(err), 687 + origin: request.headers.get('origin') ?? undefined, 445 688 }); 446 689 throw err; 447 690 } finally { ··· 449 692 authByRequest.delete(xrpcRequest); 450 693 } 451 694 } 452 - }); 695 + }; 696 + 697 + return new Elysia() 698 + .all('/xrpc/:nsid', ({ body, request }) => handleXrpcRequest(request, body)) 699 + .all('/xrpc/:nsid/', async ({ body, request }) => { 700 + const url = new URL(request.url); 701 + if (url.pathname.endsWith('/') && url.pathname.length > '/xrpc/'.length) { 702 + url.pathname = url.pathname.slice(0, -1); 703 + } 704 + 705 + const rewritten = new Request(url.toString(), request); 706 + return handleXrpcRequest(rewritten, body); 707 + }); 453 708 };
+61
lexicons/domain-claim-subdomain-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domain.claimSubdomain", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Claim a wisp.place subdomain handle for the authenticated DID.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["handle"], 13 + "properties": { 14 + "handle": { 15 + "type": "string", 16 + "description": "Subdomain label only (for example, alice).", 17 + "minLength": 3, 18 + "maxLength": 63 19 + }, 20 + "siteRkey": { 21 + "type": "string", 22 + "format": "record-key", 23 + "description": "Optional place.wisp.fs rkey to map immediately after claim." 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": ["domain", "kind", "status"], 33 + "properties": { 34 + "domain": { 35 + "type": "string" 36 + }, 37 + "kind": { 38 + "type": "string", 39 + "enum": ["wisp"] 40 + }, 41 + "status": { 42 + "type": "string", 43 + "enum": ["verified", "alreadyClaimed"] 44 + }, 45 + "siteRkey": { 46 + "type": "string", 47 + "format": "record-key" 48 + } 49 + } 50 + } 51 + }, 52 + "errors": [ 53 + { "name": "AuthenticationRequired" }, 54 + { "name": "InvalidDomain" }, 55 + { "name": "AlreadyClaimed" }, 56 + { "name": "DomainLimitReached" }, 57 + { "name": "RateLimitExceeded" } 58 + ] 59 + } 60 + } 61 + }
+3 -3
lexicons/domain-claim-v2.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "procedure", 7 - "description": "Claim a domain for the authenticated DID. Returns DNS setup instructions for custom domains.", 7 + "description": "Claim a custom domain for the authenticated DID. Returns DNS setup instructions.", 8 8 "input": { 9 9 "encoding": "application/json", 10 10 "schema": { ··· 13 13 "properties": { 14 14 "domain": { 15 15 "type": "string", 16 - "description": "Domain to claim (wisp subdomain FQDN or custom domain FQDN).", 16 + "description": "Custom domain FQDN to claim (for example, example.com).", 17 17 "minLength": 3, 18 18 "maxLength": 253 19 19 }, ··· 36 36 }, 37 37 "kind": { 38 38 "type": "string", 39 - "enum": ["wisp", "custom"] 39 + "enum": ["custom"] 40 40 }, 41 41 "status": { 42 42 "type": "string",
+43
lexicons/domain-delete-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domain.delete", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a claimed domain owned by the authenticated DID.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["domain"], 11 + "properties": { 12 + "domain": { 13 + "type": "string", 14 + "description": "Fully-qualified domain to delete (wisp subdomain or custom domain).", 15 + "minLength": 3, 16 + "maxLength": 253 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": ["domain", "deleted"], 25 + "properties": { 26 + "domain": { 27 + "type": "string" 28 + }, 29 + "deleted": { 30 + "type": "boolean", 31 + "const": true 32 + } 33 + } 34 + } 35 + }, 36 + "errors": [ 37 + { "name": "AuthenticationRequired" }, 38 + { "name": "InvalidDomain" }, 39 + { "name": "NotFound" } 40 + ] 41 + } 42 + } 43 + }
+62
lexicons/domain-get-list-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domain.getList", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List domains for the authenticated DID (wisp subdomains + custom).", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["domains"], 13 + "properties": { 14 + "domains": { 15 + "type": "array", 16 + "description": "Domains owned by the caller DID.", 17 + "items": { 18 + "type": "ref", 19 + "ref": "#domainSummary" 20 + } 21 + } 22 + } 23 + } 24 + }, 25 + "errors": [ 26 + { "name": "AuthenticationRequired" }, 27 + { "name": "InvalidRequest" } 28 + ] 29 + }, 30 + "domainSummary": { 31 + "type": "object", 32 + "description": "Summary of a claimed domain for list views.", 33 + "required": ["domain", "kind", "status", "verified"], 34 + "properties": { 35 + "domain": { 36 + "type": "string", 37 + "minLength": 3, 38 + "maxLength": 253 39 + }, 40 + "kind": { 41 + "type": "string", 42 + "enum": ["wisp", "custom"] 43 + }, 44 + "status": { 45 + "type": "string", 46 + "enum": ["pendingVerification", "verified"] 47 + }, 48 + "verified": { 49 + "type": "boolean" 50 + }, 51 + "siteRkey": { 52 + "type": "string", 53 + "format": "record-key" 54 + }, 55 + "lastCheckedAt": { 56 + "type": "string", 57 + "format": "datetime" 58 + } 59 + } 60 + } 61 + } 62 + }
+14 -2
packages/@wispplace/lexicons/package.json
··· 30 30 "types": "./src/types/place/wisp/v2/domain/claim.ts", 31 31 "default": "./src/types/place/wisp/v2/domain/claim.ts" 32 32 }, 33 + "./types/place/wisp/v2/domain/claimSubdomain": { 34 + "types": "./src/types/place/wisp/v2/domain/claimSubdomain.ts", 35 + "default": "./src/types/place/wisp/v2/domain/claimSubdomain.ts" 36 + }, 37 + "./types/place/wisp/v2/domain/delete": { 38 + "types": "./src/types/place/wisp/v2/domain/delete.ts", 39 + "default": "./src/types/place/wisp/v2/domain/delete.ts" 40 + }, 41 + "./types/place/wisp/v2/domain/getList": { 42 + "types": "./src/types/place/wisp/v2/domain/getList.ts", 43 + "default": "./src/types/place/wisp/v2/domain/getList.ts" 44 + }, 33 45 "./types/place/wisp/v2/domain/getStatus": { 34 46 "types": "./src/types/place/wisp/v2/domain/getStatus.ts", 35 47 "default": "./src/types/place/wisp/v2/domain/getStatus.ts" 36 48 }, 37 49 "./atcute": { 38 - "types": "./src/atcute/index.ts", 39 - "default": "./src/atcute/index.ts" 50 + "types": "./src/atcute/lexicons/index.ts", 51 + "default": "./src/atcute/lexicons/index.ts" 40 52 }, 41 53 "./lexicons": { 42 54 "types": "./src/lexicons.ts",
-1
packages/@wispplace/lexicons/src/atcute/index.ts
··· 1 - export * from './lexicons/index';
+3
packages/@wispplace/lexicons/src/atcute/lexicons/index.ts
··· 1 1 export * as PlaceWispV2DomainClaim from "./types/place/wisp/v2/domain/claim.js"; 2 + export * as PlaceWispV2DomainClaimSubdomain from "./types/place/wisp/v2/domain/claimSubdomain.js"; 3 + export * as PlaceWispV2DomainDelete from "./types/place/wisp/v2/domain/delete.js"; 4 + export * as PlaceWispV2DomainGetList from "./types/place/wisp/v2/domain/getList.js"; 2 5 export * as PlaceWispV2DomainGetStatus from "./types/place/wisp/v2/domain/getStatus.js"; 3 6 export * as PlaceWispV2Domains from "./types/place/wisp/v2/domains.js";
+2 -4
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/claim.ts
··· 8 8 type: "lex", 9 9 schema: /*#__PURE__*/ v.object({ 10 10 /** 11 - * Domain to claim (wisp subdomain FQDN or custom domain FQDN). 11 + * Custom domain FQDN to claim (for example, example.com). 12 12 * @minLength 3 13 13 * @maxLength 253 14 14 */ ··· 45 45 ]), 46 46 ), 47 47 domain: /*#__PURE__*/ v.string(), 48 - kind: /*#__PURE__*/ v.optional( 49 - /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 50 - ), 48 + kind: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.literalEnum(["custom"])), 51 49 siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 52 50 status: /*#__PURE__*/ v.literalEnum([ 53 51 "alreadyClaimed",
+52
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/claimSubdomain.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure( 6 + "place.wisp.v2.domain.claimSubdomain", 7 + { 8 + params: null, 9 + input: { 10 + type: "lex", 11 + schema: /*#__PURE__*/ v.object({ 12 + /** 13 + * Subdomain label only (for example, alice). 14 + * @minLength 3 15 + * @maxLength 63 16 + */ 17 + handle: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 18 + /*#__PURE__*/ v.stringLength(3, 63), 19 + ]), 20 + /** 21 + * Optional place.wisp.fs rkey to map immediately after claim. 22 + */ 23 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 24 + }), 25 + }, 26 + output: { 27 + type: "lex", 28 + schema: /*#__PURE__*/ v.object({ 29 + domain: /*#__PURE__*/ v.string(), 30 + kind: /*#__PURE__*/ v.literalEnum(["wisp"]), 31 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 32 + status: /*#__PURE__*/ v.literalEnum(["alreadyClaimed", "verified"]), 33 + }), 34 + }, 35 + }, 36 + ); 37 + 38 + type main$schematype = typeof _mainSchema; 39 + 40 + export interface mainSchema extends main$schematype {} 41 + 42 + export const mainSchema = _mainSchema as mainSchema; 43 + 44 + export interface $params {} 45 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 46 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 47 + 48 + declare module "@atcute/lexicons/ambient" { 49 + interface XRPCProcedures { 50 + "place.wisp.v2.domain.claimSubdomain": mainSchema; 51 + } 52 + }
+39
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/delete.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.procedure("place.wisp.v2.domain.delete", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Fully-qualified domain to delete (wisp subdomain or custom domain). 9 + * @minLength 3 10 + * @maxLength 253 11 + */ 12 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 13 + /*#__PURE__*/ v.stringLength(3, 253), 14 + ]), 15 + }), 16 + input: null, 17 + output: { 18 + type: "lex", 19 + schema: /*#__PURE__*/ v.object({ 20 + deleted: /*#__PURE__*/ v.literal(true), 21 + domain: /*#__PURE__*/ v.string(), 22 + }), 23 + }, 24 + }); 25 + 26 + type main$schematype = typeof _mainSchema; 27 + 28 + export interface mainSchema extends main$schematype {} 29 + 30 + export const mainSchema = _mainSchema as mainSchema; 31 + 32 + export interface $params extends v.InferInput<mainSchema["params"]> {} 33 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 34 + 35 + declare module "@atcute/lexicons/ambient" { 36 + interface XRPCProcedures { 37 + "place.wisp.v2.domain.delete": mainSchema; 38 + } 39 + }
+57
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/getList.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import type {} from "@atcute/lexicons/ambient"; 4 + 5 + const _domainSummarySchema = /*#__PURE__*/ v.object({ 6 + $type: /*#__PURE__*/ v.optional( 7 + /*#__PURE__*/ v.literal("place.wisp.v2.domain.getList#domainSummary"), 8 + ), 9 + /** 10 + * @minLength 3 11 + * @maxLength 253 12 + */ 13 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 14 + /*#__PURE__*/ v.stringLength(3, 253), 15 + ]), 16 + kind: /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 17 + lastCheckedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 18 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 19 + status: /*#__PURE__*/ v.literalEnum(["pendingVerification", "verified"]), 20 + verified: /*#__PURE__*/ v.boolean(), 21 + }); 22 + const _mainSchema = /*#__PURE__*/ v.query("place.wisp.v2.domain.getList", { 23 + params: null, 24 + output: { 25 + type: "lex", 26 + schema: /*#__PURE__*/ v.object({ 27 + /** 28 + * Domains owned by the caller DID. 29 + */ 30 + get domains() { 31 + return /*#__PURE__*/ v.array(domainSummarySchema); 32 + }, 33 + }), 34 + }, 35 + }); 36 + 37 + type domainSummary$schematype = typeof _domainSummarySchema; 38 + type main$schematype = typeof _mainSchema; 39 + 40 + export interface domainSummarySchema extends domainSummary$schematype {} 41 + export interface mainSchema extends main$schematype {} 42 + 43 + export const domainSummarySchema = _domainSummarySchema as domainSummarySchema; 44 + export const mainSchema = _mainSchema as mainSchema; 45 + 46 + export interface DomainSummary extends v.InferInput< 47 + typeof domainSummarySchema 48 + > {} 49 + 50 + export interface $params {} 51 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 52 + 53 + declare module "@atcute/lexicons/ambient" { 54 + interface XRPCQueries { 55 + "place.wisp.v2.domain.getList": mainSchema; 56 + } 57 + }
+39
packages/@wispplace/lexicons/src/index.ts
··· 10 10 createServer as createXrpcServer, 11 11 } from '@atproto/xrpc-server' 12 12 import { schemas } from './lexicons.js' 13 + import * as PlaceWispV2DomainClaimSubdomain from './types/place/wisp/v2/domain/claimSubdomain.js' 13 14 import * as PlaceWispV2DomainClaim from './types/place/wisp/v2/domain/claim.js' 15 + import * as PlaceWispV2DomainDelete from './types/place/wisp/v2/domain/delete.js' 16 + import * as PlaceWispV2DomainGetList from './types/place/wisp/v2/domain/getList.js' 14 17 import * as PlaceWispV2DomainGetStatus from './types/place/wisp/v2/domain/getStatus.js' 15 18 16 19 export function createServer(options?: XrpcOptions): Server { ··· 64 67 this._server = server 65 68 } 66 69 70 + claimSubdomain<A extends Auth = void>( 71 + cfg: MethodConfigOrHandler< 72 + A, 73 + PlaceWispV2DomainClaimSubdomain.QueryParams, 74 + PlaceWispV2DomainClaimSubdomain.HandlerInput, 75 + PlaceWispV2DomainClaimSubdomain.HandlerOutput 76 + >, 77 + ) { 78 + const nsid = 'place.wisp.v2.domain.claimSubdomain' // @ts-ignore 79 + return this._server.xrpc.method(nsid, cfg) 80 + } 81 + 67 82 claim<A extends Auth = void>( 68 83 cfg: MethodConfigOrHandler< 69 84 A, ··· 73 88 >, 74 89 ) { 75 90 const nsid = 'place.wisp.v2.domain.claim' // @ts-ignore 91 + return this._server.xrpc.method(nsid, cfg) 92 + } 93 + 94 + delete<A extends Auth = void>( 95 + cfg: MethodConfigOrHandler< 96 + A, 97 + PlaceWispV2DomainDelete.QueryParams, 98 + PlaceWispV2DomainDelete.HandlerInput, 99 + PlaceWispV2DomainDelete.HandlerOutput 100 + >, 101 + ) { 102 + const nsid = 'place.wisp.v2.domain.delete' // @ts-ignore 103 + return this._server.xrpc.method(nsid, cfg) 104 + } 105 + 106 + getList<A extends Auth = void>( 107 + cfg: MethodConfigOrHandler< 108 + A, 109 + PlaceWispV2DomainGetList.QueryParams, 110 + PlaceWispV2DomainGetList.HandlerInput, 111 + PlaceWispV2DomainGetList.HandlerOutput 112 + >, 113 + ) { 114 + const nsid = 'place.wisp.v2.domain.getList' // @ts-ignore 76 115 return this._server.xrpc.method(nsid, cfg) 77 116 } 78 117
+196 -3
packages/@wispplace/lexicons/src/lexicons.ts
··· 10 10 import { type $Typed, is$typed, maybe$typed } from './util.js' 11 11 12 12 export const schemaDict = { 13 + PlaceWispV2DomainClaimSubdomain: { 14 + lexicon: 1, 15 + id: 'place.wisp.v2.domain.claimSubdomain', 16 + defs: { 17 + main: { 18 + type: 'procedure', 19 + description: 20 + 'Claim a wisp.place subdomain handle for the authenticated DID.', 21 + input: { 22 + encoding: 'application/json', 23 + schema: { 24 + type: 'object', 25 + required: ['handle'], 26 + properties: { 27 + handle: { 28 + type: 'string', 29 + description: 'Subdomain label only (for example, alice).', 30 + minLength: 3, 31 + maxLength: 63, 32 + }, 33 + siteRkey: { 34 + type: 'string', 35 + format: 'record-key', 36 + description: 37 + 'Optional place.wisp.fs rkey to map immediately after claim.', 38 + }, 39 + }, 40 + }, 41 + }, 42 + output: { 43 + encoding: 'application/json', 44 + schema: { 45 + type: 'object', 46 + required: ['domain', 'kind', 'status'], 47 + properties: { 48 + domain: { 49 + type: 'string', 50 + }, 51 + kind: { 52 + type: 'string', 53 + enum: ['wisp'], 54 + }, 55 + status: { 56 + type: 'string', 57 + enum: ['verified', 'alreadyClaimed'], 58 + }, 59 + siteRkey: { 60 + type: 'string', 61 + format: 'record-key', 62 + }, 63 + }, 64 + }, 65 + }, 66 + errors: [ 67 + { 68 + name: 'AuthenticationRequired', 69 + }, 70 + { 71 + name: 'InvalidDomain', 72 + }, 73 + { 74 + name: 'AlreadyClaimed', 75 + }, 76 + { 77 + name: 'DomainLimitReached', 78 + }, 79 + { 80 + name: 'RateLimitExceeded', 81 + }, 82 + ], 83 + }, 84 + }, 85 + }, 13 86 PlaceWispV2DomainClaim: { 14 87 lexicon: 1, 15 88 id: 'place.wisp.v2.domain.claim', ··· 17 90 main: { 18 91 type: 'procedure', 19 92 description: 20 - 'Claim a domain for the authenticated DID. Returns DNS setup instructions for custom domains.', 93 + 'Claim a custom domain for the authenticated DID. Returns DNS setup instructions.', 21 94 input: { 22 95 encoding: 'application/json', 23 96 schema: { ··· 27 100 domain: { 28 101 type: 'string', 29 102 description: 30 - 'Domain to claim (wisp subdomain FQDN or custom domain FQDN).', 103 + 'Custom domain FQDN to claim (for example, example.com).', 31 104 minLength: 3, 32 105 maxLength: 253, 33 106 }, ··· 51 124 }, 52 125 kind: { 53 126 type: 'string', 54 - enum: ['wisp', 'custom'], 127 + enum: ['custom'], 55 128 }, 56 129 status: { 57 130 type: 'string', ··· 107 180 name: 'RateLimitExceeded', 108 181 }, 109 182 ], 183 + }, 184 + }, 185 + }, 186 + PlaceWispV2DomainDelete: { 187 + lexicon: 1, 188 + id: 'place.wisp.v2.domain.delete', 189 + defs: { 190 + main: { 191 + type: 'procedure', 192 + description: 'Delete a claimed domain owned by the authenticated DID.', 193 + parameters: { 194 + type: 'params', 195 + required: ['domain'], 196 + properties: { 197 + domain: { 198 + type: 'string', 199 + description: 200 + 'Fully-qualified domain to delete (wisp subdomain or custom domain).', 201 + minLength: 3, 202 + maxLength: 253, 203 + }, 204 + }, 205 + }, 206 + output: { 207 + encoding: 'application/json', 208 + schema: { 209 + type: 'object', 210 + required: ['domain', 'deleted'], 211 + properties: { 212 + domain: { 213 + type: 'string', 214 + }, 215 + deleted: { 216 + type: 'boolean', 217 + const: true, 218 + }, 219 + }, 220 + }, 221 + }, 222 + errors: [ 223 + { 224 + name: 'AuthenticationRequired', 225 + }, 226 + { 227 + name: 'InvalidDomain', 228 + }, 229 + { 230 + name: 'NotFound', 231 + }, 232 + ], 233 + }, 234 + }, 235 + }, 236 + PlaceWispV2DomainGetList: { 237 + lexicon: 1, 238 + id: 'place.wisp.v2.domain.getList', 239 + defs: { 240 + main: { 241 + type: 'query', 242 + description: 243 + 'List domains for the authenticated DID (wisp subdomains + custom).', 244 + output: { 245 + encoding: 'application/json', 246 + schema: { 247 + type: 'object', 248 + required: ['domains'], 249 + properties: { 250 + domains: { 251 + type: 'array', 252 + description: 'Domains owned by the caller DID.', 253 + items: { 254 + type: 'ref', 255 + ref: 'lex:place.wisp.v2.domain.getList#domainSummary', 256 + }, 257 + }, 258 + }, 259 + }, 260 + }, 261 + errors: [ 262 + { 263 + name: 'AuthenticationRequired', 264 + }, 265 + { 266 + name: 'InvalidRequest', 267 + }, 268 + ], 269 + }, 270 + domainSummary: { 271 + type: 'object', 272 + description: 'Summary of a claimed domain for list views.', 273 + required: ['domain', 'kind', 'status', 'verified'], 274 + properties: { 275 + domain: { 276 + type: 'string', 277 + minLength: 3, 278 + maxLength: 253, 279 + }, 280 + kind: { 281 + type: 'string', 282 + enum: ['wisp', 'custom'], 283 + }, 284 + status: { 285 + type: 'string', 286 + enum: ['pendingVerification', 'verified'], 287 + }, 288 + verified: { 289 + type: 'boolean', 290 + }, 291 + siteRkey: { 292 + type: 'string', 293 + format: 'record-key', 294 + }, 295 + lastCheckedAt: { 296 + type: 'string', 297 + format: 'datetime', 298 + }, 299 + }, 110 300 }, 111 301 }, 112 302 }, ··· 633 823 } 634 824 635 825 export const ids = { 826 + PlaceWispV2DomainClaimSubdomain: 'place.wisp.v2.domain.claimSubdomain', 636 827 PlaceWispV2DomainClaim: 'place.wisp.v2.domain.claim', 828 + PlaceWispV2DomainDelete: 'place.wisp.v2.domain.delete', 829 + PlaceWispV2DomainGetList: 'place.wisp.v2.domain.getList', 637 830 PlaceWispV2DomainGetStatus: 'place.wisp.v2.domain.getStatus', 638 831 PlaceWispV2Domains: 'place.wisp.v2.domains', 639 832 PlaceWispFs: 'place.wisp.fs',
+2 -2
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/claim.ts
··· 17 17 export type QueryParams = {} 18 18 19 19 export interface InputSchema { 20 - /** Domain to claim (wisp subdomain FQDN or custom domain FQDN). */ 20 + /** Custom domain FQDN to claim (for example, example.com). */ 21 21 domain: string 22 22 /** Optional place.wisp.fs rkey to map immediately after claim. */ 23 23 siteRkey?: string ··· 25 25 26 26 export interface OutputSchema { 27 27 domain: string 28 - kind?: 'wisp' | 'custom' 28 + kind?: 'custom' 29 29 status: 'alreadyClaimed' | 'pendingVerification' | 'verified' 30 30 /** Identifier used to construct DNS challenge targets for custom domains. */ 31 31 challengeId?: string
+55
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/claimSubdomain.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../../lexicons' 7 + import { 8 + type $Typed, 9 + is$typed as _is$typed, 10 + type OmitKey, 11 + } from '../../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'place.wisp.v2.domain.claimSubdomain' 16 + 17 + export type QueryParams = {} 18 + 19 + export interface InputSchema { 20 + /** Subdomain label only (for example, alice). */ 21 + handle: string 22 + /** Optional place.wisp.fs rkey to map immediately after claim. */ 23 + siteRkey?: string 24 + } 25 + 26 + export interface OutputSchema { 27 + domain: string 28 + kind: 'wisp' 29 + status: 'verified' | 'alreadyClaimed' 30 + siteRkey?: string 31 + } 32 + 33 + export interface HandlerInput { 34 + encoding: 'application/json' 35 + body: InputSchema 36 + } 37 + 38 + export interface HandlerSuccess { 39 + encoding: 'application/json' 40 + body: OutputSchema 41 + headers?: { [key: string]: string } 42 + } 43 + 44 + export interface HandlerError { 45 + status: number 46 + message?: string 47 + error?: 48 + | 'AuthenticationRequired' 49 + | 'InvalidDomain' 50 + | 'AlreadyClaimed' 51 + | 'DomainLimitReached' 52 + | 'RateLimitExceeded' 53 + } 54 + 55 + export type HandlerOutput = HandlerError | HandlerSuccess
+42
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/delete.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../../lexicons' 7 + import { 8 + type $Typed, 9 + is$typed as _is$typed, 10 + type OmitKey, 11 + } from '../../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'place.wisp.v2.domain.delete' 16 + 17 + export type QueryParams = { 18 + /** Fully-qualified domain to delete (wisp subdomain or custom domain). */ 19 + domain: string 20 + } 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + domain: string 25 + deleted: true 26 + } 27 + 28 + export type HandlerInput = void 29 + 30 + export interface HandlerSuccess { 31 + encoding: 'application/json' 32 + body: OutputSchema 33 + headers?: { [key: string]: string } 34 + } 35 + 36 + export interface HandlerError { 37 + status: number 38 + message?: string 39 + error?: 'AuthenticationRequired' | 'InvalidDomain' | 'NotFound' 40 + } 41 + 42 + export type HandlerOutput = HandlerError | HandlerSuccess
+60
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/getList.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../../lexicons' 7 + import { 8 + type $Typed, 9 + is$typed as _is$typed, 10 + type OmitKey, 11 + } from '../../../../../util' 12 + 13 + const is$typed = _is$typed, 14 + validate = _validate 15 + const id = 'place.wisp.v2.domain.getList' 16 + 17 + export type QueryParams = {} 18 + export type InputSchema = undefined 19 + 20 + export interface OutputSchema { 21 + /** Domains owned by the caller DID. */ 22 + domains: DomainSummary[] 23 + } 24 + 25 + export type HandlerInput = void 26 + 27 + export interface HandlerSuccess { 28 + encoding: 'application/json' 29 + body: OutputSchema 30 + headers?: { [key: string]: string } 31 + } 32 + 33 + export interface HandlerError { 34 + status: number 35 + message?: string 36 + error?: 'AuthenticationRequired' | 'InvalidRequest' 37 + } 38 + 39 + export type HandlerOutput = HandlerError | HandlerSuccess 40 + 41 + /** Summary of a claimed domain for list views. */ 42 + export interface DomainSummary { 43 + $type?: 'place.wisp.v2.domain.getList#domainSummary' 44 + domain: string 45 + kind: 'wisp' | 'custom' 46 + status: 'pendingVerification' | 'verified' 47 + verified: boolean 48 + siteRkey?: string 49 + lastCheckedAt?: string 50 + } 51 + 52 + const hashDomainSummary = 'domainSummary' 53 + 54 + export function isDomainSummary<V>(v: V) { 55 + return is$typed(v, id, hashDomainSummary) 56 + } 57 + 58 + export function validateDomainSummary<V>(v: V) { 59 + return validate<DomainSummary & V>(v, id, hashDomainSummary) 60 + }