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

Configure Feed

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

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 + }