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

add sites xrpc routes

+1319 -139
+3
README.md
··· 60 60 61 61 - `place.wisp.v2.domain.claimSubdomain` (procedure / POST, wisp handles) 62 62 - `place.wisp.v2.domain.claim` (procedure / POST) 63 + - `place.wisp.v2.domain.addSite` (procedure / POST) 63 64 - `place.wisp.v2.domain.delete` (procedure / POST) 64 65 - `place.wisp.v2.domain.getList` (query / GET) 65 66 - `place.wisp.v2.domain.getStatus` (query / GET) 67 + - `place.wisp.v2.site.getList` (query / GET) 68 + - `place.wisp.v2.site.delete` (procedure / POST) 66 69 67 70 The server validates **serviceAuth JWTs** on `/xrpc/*`. 68 71
+2 -135
apps/main-app/src/lib/db.ts
··· 1 1 import { SQL } from "bun"; 2 2 import { isValidHandle, toDomain } from "./domain-utils"; 3 + import { runDatabaseMigrations } from "./migrations"; 3 4 4 5 export { isValidHandle, toDomain } from "./domain-utils"; 5 6 ··· 66 67 ) 67 68 `; 68 69 69 - // Add columns if they don't exist (for existing databases) 70 - try { 71 - await db`ALTER TABLE domains ADD COLUMN IF NOT EXISTS rkey TEXT`; 72 - } catch (err) { 73 - // Column might already exist, ignore 74 - } 75 - 76 - try { 77 - await db`ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS expires_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()) + 2592000`; 78 - } catch (err) { 79 - // Column might already exist, ignore 80 - } 81 - 82 - try { 83 - await db`ALTER TABLE oauth_keys ADD COLUMN IF NOT EXISTS created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())`; 84 - } catch (err) { 85 - // Column might already exist, ignore 86 - } 87 - 88 - try { 89 - await db`ALTER TABLE oauth_states ADD COLUMN IF NOT EXISTS expires_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) + 3600`; 90 - } catch (err) { 91 - // Column might already exist, ignore 92 - } 93 - 94 - try { 95 - await db`ALTER TABLE service_identity_keys ADD COLUMN IF NOT EXISTS updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())`; 96 - } catch (err) { 97 - // Column might already exist, ignore 98 - } 99 - 100 - try { 101 - await db`ALTER TABLE service_identity_keys ADD COLUMN IF NOT EXISTS private_key_multibase TEXT`; 102 - } catch (err) { 103 - // Column might already exist, ignore 104 - } 105 - 106 - // Remove the unique constraint on domains.did to allow multiple domains per user 107 - try { 108 - await db`ALTER TABLE domains DROP CONSTRAINT IF EXISTS domains_did_key`; 109 - } catch (err) { 110 - // Constraint might already be removed, ignore 111 - } 112 - 113 70 // Custom domains table for BYOD (bring your own domain) 114 71 await db` 115 72 CREATE TABLE IF NOT EXISTS custom_domains ( ··· 122 79 created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) 123 80 ) 124 81 `; 125 - 126 - // Migrate existing tables to make rkey nullable and remove default 127 - try { 128 - await db`ALTER TABLE custom_domains ALTER COLUMN rkey DROP NOT NULL`; 129 - } catch (err) { 130 - // Column might already be nullable, ignore 131 - } 132 - try { 133 - await db`ALTER TABLE custom_domains ALTER COLUMN rkey DROP DEFAULT`; 134 - } catch (err) { 135 - // Default might already be removed, ignore 136 - } 137 82 138 83 // Sites table - cache of place.wisp.fs records from PDS 139 84 await db` ··· 186 131 ) 187 132 `; 188 133 189 - // Insert initial supporter 190 - await db` 191 - INSERT INTO supporter (did) 192 - VALUES ('did:plc:ttdrpj45ibqunmfhdsb4zdwq') 193 - ON CONFLICT (did) DO NOTHING 194 - `; 195 - 196 - // Create indexes for common query patterns 197 - await Promise.all([ 198 - // oauth_states cleanup queries 199 - db`CREATE INDEX IF NOT EXISTS idx_oauth_states_expires_at ON oauth_states(expires_at)`.catch(err => { 200 - if (!err.message?.includes('already exists')) { 201 - console.error('Failed to create idx_oauth_states_expires_at:', err); 202 - } 203 - }), 204 - 205 - // oauth_sessions cleanup queries 206 - db`CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires_at ON oauth_sessions(expires_at)`.catch(err => { 207 - if (!err.message?.includes('already exists')) { 208 - console.error('Failed to create idx_oauth_sessions_expires_at:', err); 209 - } 210 - }), 211 - 212 - // oauth_keys key rotation queries 213 - db`CREATE INDEX IF NOT EXISTS idx_oauth_keys_created_at ON oauth_keys(created_at)`.catch(err => { 214 - if (!err.message?.includes('already exists')) { 215 - console.error('Failed to create idx_oauth_keys_created_at:', err); 216 - } 217 - }), 218 - 219 - // domains queries by (did, rkey) 220 - db`CREATE INDEX IF NOT EXISTS idx_domains_did_rkey ON domains(did, rkey)`.catch(err => { 221 - if (!err.message?.includes('already exists')) { 222 - console.error('Failed to create idx_domains_did_rkey:', err); 223 - } 224 - }), 225 - 226 - // custom_domains queries by did 227 - db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did ON custom_domains(did)`.catch(err => { 228 - if (!err.message?.includes('already exists')) { 229 - console.error('Failed to create idx_custom_domains_did:', err); 230 - } 231 - }), 232 - 233 - // custom_domains queries by (did, rkey) 234 - db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did_rkey ON custom_domains(did, rkey)`.catch(err => { 235 - if (!err.message?.includes('already exists')) { 236 - console.error('Failed to create idx_custom_domains_did_rkey:', err); 237 - } 238 - }), 239 - 240 - // custom_domains DNS verification worker queries 241 - db`CREATE INDEX IF NOT EXISTS idx_custom_domains_verified ON custom_domains(verified)`.catch(err => { 242 - if (!err.message?.includes('already exists')) { 243 - console.error('Failed to create idx_custom_domains_verified:', err); 244 - } 245 - }), 246 - 247 - // sites queries by did 248 - db`CREATE INDEX IF NOT EXISTS idx_sites_did ON sites(did)`.catch(err => { 249 - if (!err.message?.includes('already exists')) { 250 - console.error('Failed to create idx_sites_did:', err); 251 - } 252 - }), 253 - 254 - // site_cache queries by did 255 - db`CREATE INDEX IF NOT EXISTS idx_site_cache_did ON site_cache(did)`.catch(err => { 256 - if (!err.message?.includes('already exists')) { 257 - console.error('Failed to create idx_site_cache_did:', err); 258 - } 259 - }), 260 - 261 - // site_cache queries by updated_at (for cleanup/monitoring) 262 - db`CREATE INDEX IF NOT EXISTS idx_site_cache_updated ON site_cache(updated_at)`.catch(err => { 263 - if (!err.message?.includes('already exists')) { 264 - console.error('Failed to create idx_site_cache_updated:', err); 265 - } 266 - }) 267 - ]); 134 + await runDatabaseMigrations(db); 268 135 269 136 export const getDomainByDid = async (did: string): Promise<string | null> => { 270 137 const rows = await db`SELECT domain FROM domains WHERE did = ${did} ORDER BY created_at ASC LIMIT 1`;
+179
apps/main-app/src/lib/migrations.ts
··· 1 + import type { SQL } from "bun"; 2 + 3 + const hasAlreadyExists = (err: unknown): boolean => { 4 + const message = err instanceof Error ? err.message : String(err); 5 + return message.includes("already exists"); 6 + }; 7 + 8 + const runMigration = async ( 9 + name: string, 10 + fn: () => Promise<unknown>, 11 + options?: { ignoreAlreadyExists?: boolean; silent?: boolean } 12 + ) => { 13 + try { 14 + await fn(); 15 + } catch (err) { 16 + if (options?.ignoreAlreadyExists && hasAlreadyExists(err)) { 17 + return; 18 + } 19 + if (!options?.silent) { 20 + console.error(`[DB Migration] ${name} failed:`, err); 21 + } 22 + } 23 + }; 24 + 25 + export const runDatabaseMigrations = async (db: SQL): Promise<void> => { 26 + // Add columns if they don't exist (for existing databases) 27 + await runMigration("add domains.rkey", async () => { 28 + await db`ALTER TABLE domains ADD COLUMN IF NOT EXISTS rkey TEXT`; 29 + }, { silent: true }); 30 + 31 + await runMigration("add oauth_sessions.expires_at", async () => { 32 + await db`ALTER TABLE oauth_sessions ADD COLUMN IF NOT EXISTS expires_at BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()) + 2592000`; 33 + }, { silent: true }); 34 + 35 + await runMigration("add oauth_keys.created_at", async () => { 36 + await db`ALTER TABLE oauth_keys ADD COLUMN IF NOT EXISTS created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())`; 37 + }, { silent: true }); 38 + 39 + await runMigration("add oauth_states.expires_at", async () => { 40 + await db`ALTER TABLE oauth_states ADD COLUMN IF NOT EXISTS expires_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) + 3600`; 41 + }, { silent: true }); 42 + 43 + await runMigration("add service_identity_keys.updated_at", async () => { 44 + await db`ALTER TABLE service_identity_keys ADD COLUMN IF NOT EXISTS updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW())`; 45 + }, { silent: true }); 46 + 47 + await runMigration("add service_identity_keys.private_key_multibase", async () => { 48 + await db`ALTER TABLE service_identity_keys ADD COLUMN IF NOT EXISTS private_key_multibase TEXT`; 49 + }, { silent: true }); 50 + 51 + // Remove the unique constraint on domains.did to allow multiple domains per user 52 + await runMigration("drop legacy domains_did_key", async () => { 53 + await db`ALTER TABLE domains DROP CONSTRAINT IF EXISTS domains_did_key`; 54 + }, { silent: true }); 55 + 56 + // Make custom_domains.rkey nullable and remove default 57 + await runMigration("custom_domains.rkey drop not null", async () => { 58 + await db`ALTER TABLE custom_domains ALTER COLUMN rkey DROP NOT NULL`; 59 + }, { silent: true }); 60 + 61 + await runMigration("custom_domains.rkey drop default", async () => { 62 + await db`ALTER TABLE custom_domains ALTER COLUMN rkey DROP DEFAULT`; 63 + }, { silent: true }); 64 + 65 + // Ensure existing domain mappings only point to owned sites before adding FK constraints. 66 + await runMigration("normalize invalid domains.rkey mappings", async () => { 67 + await db` 68 + UPDATE domains d 69 + SET rkey = NULL 70 + WHERE rkey IS NOT NULL 71 + AND NOT EXISTS ( 72 + SELECT 1 73 + FROM sites s 74 + WHERE s.did = d.did 75 + AND s.rkey = d.rkey 76 + ) 77 + `; 78 + }); 79 + 80 + await runMigration("normalize invalid custom_domains.rkey mappings", async () => { 81 + await db` 82 + UPDATE custom_domains d 83 + SET rkey = NULL 84 + WHERE rkey IS NOT NULL 85 + AND NOT EXISTS ( 86 + SELECT 1 87 + FROM sites s 88 + WHERE s.did = d.did 89 + AND s.rkey = d.rkey 90 + ) 91 + `; 92 + }); 93 + 94 + // Enforce mapped site rkeys belong to same DID as mapped domain. 95 + await runMigration("add fk_domains_site_owner", async () => { 96 + await db` 97 + ALTER TABLE domains 98 + ADD CONSTRAINT fk_domains_site_owner 99 + FOREIGN KEY (did, rkey) 100 + REFERENCES sites(did, rkey) 101 + ON UPDATE CASCADE 102 + ON DELETE SET NULL 103 + `; 104 + }, { ignoreAlreadyExists: true }); 105 + 106 + await runMigration("add fk_custom_domains_site_owner", async () => { 107 + await db` 108 + ALTER TABLE custom_domains 109 + ADD CONSTRAINT fk_custom_domains_site_owner 110 + FOREIGN KEY (did, rkey) 111 + REFERENCES sites(did, rkey) 112 + ON UPDATE CASCADE 113 + ON DELETE SET NULL 114 + `; 115 + }, { ignoreAlreadyExists: true }); 116 + 117 + // Seed initial supporter DID 118 + await runMigration("seed initial supporter", async () => { 119 + await db` 120 + INSERT INTO supporter (did) 121 + VALUES ('did:plc:ttdrpj45ibqunmfhdsb4zdwq') 122 + ON CONFLICT (did) DO NOTHING 123 + `; 124 + }); 125 + 126 + // Create indexes for common query patterns 127 + await Promise.all([ 128 + db`CREATE INDEX IF NOT EXISTS idx_oauth_states_expires_at ON oauth_states(expires_at)`.catch((err) => { 129 + if (!hasAlreadyExists(err)) { 130 + console.error("Failed to create idx_oauth_states_expires_at:", err); 131 + } 132 + }), 133 + db`CREATE INDEX IF NOT EXISTS idx_oauth_sessions_expires_at ON oauth_sessions(expires_at)`.catch((err) => { 134 + if (!hasAlreadyExists(err)) { 135 + console.error("Failed to create idx_oauth_sessions_expires_at:", err); 136 + } 137 + }), 138 + db`CREATE INDEX IF NOT EXISTS idx_oauth_keys_created_at ON oauth_keys(created_at)`.catch((err) => { 139 + if (!hasAlreadyExists(err)) { 140 + console.error("Failed to create idx_oauth_keys_created_at:", err); 141 + } 142 + }), 143 + db`CREATE INDEX IF NOT EXISTS idx_domains_did_rkey ON domains(did, rkey)`.catch((err) => { 144 + if (!hasAlreadyExists(err)) { 145 + console.error("Failed to create idx_domains_did_rkey:", err); 146 + } 147 + }), 148 + db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did ON custom_domains(did)`.catch((err) => { 149 + if (!hasAlreadyExists(err)) { 150 + console.error("Failed to create idx_custom_domains_did:", err); 151 + } 152 + }), 153 + db`CREATE INDEX IF NOT EXISTS idx_custom_domains_did_rkey ON custom_domains(did, rkey)`.catch((err) => { 154 + if (!hasAlreadyExists(err)) { 155 + console.error("Failed to create idx_custom_domains_did_rkey:", err); 156 + } 157 + }), 158 + db`CREATE INDEX IF NOT EXISTS idx_custom_domains_verified ON custom_domains(verified)`.catch((err) => { 159 + if (!hasAlreadyExists(err)) { 160 + console.error("Failed to create idx_custom_domains_verified:", err); 161 + } 162 + }), 163 + db`CREATE INDEX IF NOT EXISTS idx_sites_did ON sites(did)`.catch((err) => { 164 + if (!hasAlreadyExists(err)) { 165 + console.error("Failed to create idx_sites_did:", err); 166 + } 167 + }), 168 + db`CREATE INDEX IF NOT EXISTS idx_site_cache_did ON site_cache(did)`.catch((err) => { 169 + if (!hasAlreadyExists(err)) { 170 + console.error("Failed to create idx_site_cache_did:", err); 171 + } 172 + }), 173 + db`CREATE INDEX IF NOT EXISTS idx_site_cache_updated ON site_cache(updated_at)`.catch((err) => { 174 + if (!hasAlreadyExists(err)) { 175 + console.error("Failed to create idx_site_cache_updated:", err); 176 + } 177 + }), 178 + ]); 179 + };
+232
apps/main-app/src/routes/xrpc.ts
··· 6 6 import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 7 7 import { ServiceJwtVerifier } from '@atcute/xrpc-server/auth'; 8 8 import { 9 + PlaceWispV2DomainAddSite, 9 10 PlaceWispV2DomainClaim, 10 11 PlaceWispV2DomainClaimSubdomain, 11 12 PlaceWispV2DomainDelete, 12 13 PlaceWispV2DomainGetList, 13 14 PlaceWispV2DomainGetStatus, 15 + PlaceWispV2SiteDelete, 16 + PlaceWispV2SiteGetList, 14 17 } from '@wispplace/lexicons/atcute'; 15 18 import { BASE_HOST } from '@wispplace/constants'; 16 19 ··· 20 23 claimCustomDomain, 21 24 claimDomain, 22 25 deleteCustomDomain, 26 + deleteSite, 23 27 deleteWispDomain, 28 + getDomainsBySite, 24 29 getAllWispDomains, 25 30 getCustomDomainsByDid, 26 31 getCustomDomainInfo, 32 + getSitesByDid, 27 33 isDomainRegistered, 28 34 updateCustomDomainRkey, 29 35 updateWispDomainSite, ··· 56 62 }); 57 63 58 64 const NSID_ALIASES: Record<string, string> = { 65 + 'place.wisp.v2.domain.add-site': 'place.wisp.v2.domain.addSite', 66 + 'place.wisp.v2.domain.addsite': 'place.wisp.v2.domain.addSite', 59 67 'place.wisp.v2.domain.claim-subdomain': 'place.wisp.v2.domain.claimSubdomain', 60 68 'place.wisp.v2.domain.claimsubdomain': 'place.wisp.v2.domain.claimSubdomain', 61 69 'place.wisp.v2.domain.claimsub-domain': 'place.wisp.v2.domain.claimSubdomain', ··· 63 71 'place.wisp.v2.domain.getlist': 'place.wisp.v2.domain.getList', 64 72 'place.wisp.v2.domain.getstatus': 'place.wisp.v2.domain.getStatus', 65 73 'place.wisp.v2.domain.get-status': 'place.wisp.v2.domain.getStatus', 74 + 'place.wisp.v2.site.delete-site': 'place.wisp.v2.site.delete', 75 + 'place.wisp.v2.site.deletesite': 'place.wisp.v2.site.delete', 76 + 'place.wisp.v2.site.get-list': 'place.wisp.v2.site.getList', 77 + 'place.wisp.v2.site.getlist': 'place.wisp.v2.site.getList', 66 78 }; 67 79 68 80 const XRPC_NSIDS = { 81 + addSite: 'place.wisp.v2.domain.addSite', 69 82 getStatus: 'place.wisp.v2.domain.getStatus', 70 83 getList: 'place.wisp.v2.domain.getList', 84 + siteGetList: 'place.wisp.v2.site.getList', 71 85 claimSubdomain: 'place.wisp.v2.domain.claimSubdomain', 72 86 claim: 'place.wisp.v2.domain.claim', 73 87 delete: 'place.wisp.v2.domain.delete', 88 + deleteSite: 'place.wisp.v2.site.delete', 74 89 } as const; 75 90 76 91 const toIsoFromEpoch = (epoch: unknown): string | undefined => { ··· 139 154 throw new XRPCError({ 140 155 status: 404, 141 156 error: 'NotFound', 157 + description, 158 + }); 159 + }; 160 + 161 + const invalidRequest = (description: string): never => { 162 + throw new XRPCError({ 163 + status: 400, 164 + error: 'InvalidRequest', 142 165 description, 143 166 }); 144 167 }; ··· 393 416 }); 394 417 }; 395 418 419 + const mapDomainToSiteForDid = async ( 420 + did: DidString, 421 + input: { domain: string; siteRkey: string }, 422 + ) => { 423 + const domain = normalizeDomain(input.domain); 424 + if (domain.length === 0) { 425 + invalidDomain('domain is required'); 426 + } 427 + 428 + const siteRkey = input.siteRkey.trim(); 429 + if (siteRkey.length === 0) { 430 + invalidRequest('siteRkey is required'); 431 + } 432 + 433 + const sites = await getSitesByDid(did); 434 + const ownsSite = sites.some((entry: { rkey: string }) => entry.rkey === siteRkey); 435 + if (!ownsSite) { 436 + notFound('site not found'); 437 + } 438 + 439 + const existing = await isDomainRegistered(domain); 440 + if (!existing.registered || existing.did !== did) { 441 + notFound('domain not found'); 442 + } 443 + 444 + if (existing.type === 'wisp') { 445 + await updateWispDomainSite(domain, siteRkey); 446 + 447 + return json({ 448 + domain, 449 + kind: 'wisp', 450 + status: 'verified', 451 + siteRkey, 452 + mapped: true, 453 + }); 454 + } 455 + 456 + const custom = await getCustomDomainInfo(domain); 457 + if (!custom || custom.did !== did) { 458 + notFound('domain not found'); 459 + } 460 + 461 + await updateCustomDomainRkey(custom.id as string, siteRkey); 462 + 463 + return json({ 464 + domain, 465 + kind: 'custom', 466 + status: custom.verified ? 'verified' : 'pendingVerification', 467 + siteRkey, 468 + mapped: true, 469 + }); 470 + }; 471 + 472 + const deleteSiteForDid = async ( 473 + did: DidString, 474 + input: { siteRkey: string }, 475 + ) => { 476 + const siteRkey = input.siteRkey.trim(); 477 + if (siteRkey.length === 0) { 478 + invalidRequest('siteRkey is required'); 479 + } 480 + 481 + const sites = await getSitesByDid(did); 482 + const ownsSite = sites.some((entry: { rkey: string }) => entry.rkey === siteRkey); 483 + if (!ownsSite) { 484 + notFound('site not found'); 485 + } 486 + 487 + const mappedDomains = await getDomainsBySite(did, siteRkey); 488 + 489 + const unmappedDomains: Array<{ 490 + domain: string; 491 + kind: 'wisp' | 'custom'; 492 + status: 'pendingVerification' | 'verified'; 493 + }> = []; 494 + 495 + for (const mapped of mappedDomains as Array<{ 496 + type: 'wisp' | 'custom'; 497 + domain: string; 498 + id?: string; 499 + verified?: boolean; 500 + }>) { 501 + if (mapped.type === 'wisp') { 502 + await updateWispDomainSite(mapped.domain, null); 503 + unmappedDomains.push({ 504 + domain: mapped.domain, 505 + kind: 'wisp', 506 + status: 'verified', 507 + }); 508 + continue; 509 + } 510 + 511 + if (mapped.id) { 512 + await updateCustomDomainRkey(mapped.id, null); 513 + unmappedDomains.push({ 514 + domain: mapped.domain, 515 + kind: 'custom', 516 + status: mapped.verified ? 'verified' : 'pendingVerification', 517 + }); 518 + } 519 + } 520 + 521 + const deleted = await deleteSite(did, siteRkey); 522 + if (!deleted.success) { 523 + throw new XRPCError({ 524 + status: 500, 525 + error: 'InternalServerError', 526 + description: 'failed to delete site', 527 + }); 528 + } 529 + 530 + return json({ 531 + siteRkey, 532 + deleted: true, 533 + unmappedDomains: unmappedDomains.sort((a, b) => a.domain.localeCompare(b.domain)), 534 + }); 535 + }; 536 + 396 537 export const xrpcRoutes = () => { 397 538 const authByRequest = new WeakMap<Request, XrpcAuthContext>(); 398 539 const router = new XRPCRouter(); 399 540 const registeredNsids = [ 541 + XRPC_NSIDS.addSite, 400 542 XRPC_NSIDS.getStatus, 401 543 XRPC_NSIDS.getList, 544 + XRPC_NSIDS.siteGetList, 402 545 XRPC_NSIDS.claimSubdomain, 403 546 XRPC_NSIDS.claim, 404 547 XRPC_NSIDS.delete, 548 + XRPC_NSIDS.deleteSite, 405 549 ]; 550 + 551 + addProcedureWithAliases( 552 + router, 553 + withNsid(PlaceWispV2DomainAddSite.mainSchema as any, XRPC_NSIDS.addSite), 554 + ['place.wisp.v2.domain.addsite', 'place.wisp.v2.domain.add-site'], 555 + { 556 + async handler({ input, request }) { 557 + const auth = requireAuthenticated(authByRequest.get(request)); 558 + const did = auth.did as DidString; 559 + 560 + return mapDomainToSiteForDid(did, { 561 + domain: input.domain, 562 + siteRkey: input.siteRkey, 563 + }); 564 + }, 565 + }, 566 + ); 406 567 407 568 addQueryWithAliases( 408 569 router, ··· 509 670 }, 510 671 ); 511 672 673 + addQueryWithAliases( 674 + router, 675 + withNsid(PlaceWispV2SiteGetList.mainSchema as any, XRPC_NSIDS.siteGetList), 676 + ['place.wisp.v2.site.getlist', 'place.wisp.v2.site.get-list'], 677 + { 678 + async handler({ request }) { 679 + const auth = requireAuthenticated(authByRequest.get(request)); 680 + const did = auth.did as DidString; 681 + 682 + const sites = await getSitesByDid(did); 683 + 684 + const siteSummaries = await Promise.all( 685 + sites.map(async (site: { 686 + rkey: string; 687 + display_name?: string | null; 688 + created_at?: number | string | null; 689 + updated_at?: number | string | null; 690 + }) => { 691 + const mappedDomains = await getDomainsBySite(did, site.rkey); 692 + const domains = (mappedDomains as Array<{ 693 + type: 'wisp' | 'custom'; 694 + domain: string; 695 + verified?: boolean; 696 + }>) 697 + .map((entry) => ({ 698 + domain: entry.domain, 699 + kind: entry.type, 700 + status: 701 + entry.type === 'wisp' 702 + ? ('verified' as const) 703 + : entry.verified 704 + ? ('verified' as const) 705 + : ('pendingVerification' as const), 706 + verified: entry.type === 'wisp' ? true : Boolean(entry.verified), 707 + })) 708 + .sort((a, b) => a.domain.localeCompare(b.domain)); 709 + 710 + return { 711 + siteRkey: site.rkey, 712 + displayName: site.display_name ?? undefined, 713 + createdAt: toIsoFromEpoch(site.created_at), 714 + updatedAt: toIsoFromEpoch(site.updated_at), 715 + domains, 716 + }; 717 + }), 718 + ); 719 + 720 + return json({ sites: siteSummaries }); 721 + }, 722 + }, 723 + ); 724 + 512 725 addProcedureWithAliases( 513 726 router, 514 727 withNsid(PlaceWispV2DomainClaimSubdomain.mainSchema as any, XRPC_NSIDS.claimSubdomain), ··· 585 798 }, 586 799 ); 587 800 801 + addProcedureWithAliases( 802 + router, 803 + withNsid(PlaceWispV2SiteDelete.mainSchema as any, XRPC_NSIDS.deleteSite), 804 + ['place.wisp.v2.site.deletesite', 'place.wisp.v2.site.delete-site'], 805 + { 806 + async handler({ input, request }) { 807 + const auth = requireAuthenticated(authByRequest.get(request)); 808 + const did = auth.did as DidString; 809 + 810 + return deleteSiteForDid(did, { 811 + siteRkey: input.siteRkey, 812 + }); 813 + }, 814 + }, 815 + ); 816 + 588 817 const schemaNsids = { 818 + addSite: (PlaceWispV2DomainAddSite.mainSchema as any).nsid, 589 819 getStatus: (PlaceWispV2DomainGetStatus.mainSchema as any).nsid, 590 820 getList: (PlaceWispV2DomainGetList.mainSchema as any).nsid, 821 + siteGetList: (PlaceWispV2SiteGetList.mainSchema as any).nsid, 591 822 claimSubdomain: (PlaceWispV2DomainClaimSubdomain.mainSchema as any).nsid, 592 823 claim: (PlaceWispV2DomainClaim.mainSchema as any).nsid, 593 824 delete: (PlaceWispV2DomainDelete.mainSchema as any).nsid, 825 + deleteSite: (PlaceWispV2SiteDelete.mainSchema as any).nsid, 594 826 }; 595 827 logger.info('[XRPC] Registered methods', { 596 828 expectedNsids: registeredNsids,
+64
lexicons/domain-add-site-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domain.addSite", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Map an owned domain to one owned site record.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["domain", "siteRkey"], 13 + "properties": { 14 + "domain": { 15 + "type": "string", 16 + "description": "Fully-qualified domain to map.", 17 + "minLength": 3, 18 + "maxLength": 253 19 + }, 20 + "siteRkey": { 21 + "type": "string", 22 + "format": "record-key", 23 + "description": "Owned place.wisp.fs record key to map this domain to." 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": ["domain", "kind", "status", "siteRkey", "mapped"], 33 + "properties": { 34 + "domain": { 35 + "type": "string" 36 + }, 37 + "kind": { 38 + "type": "string", 39 + "enum": ["wisp", "custom"] 40 + }, 41 + "status": { 42 + "type": "string", 43 + "enum": ["pendingVerification", "verified"] 44 + }, 45 + "siteRkey": { 46 + "type": "string", 47 + "format": "record-key" 48 + }, 49 + "mapped": { 50 + "type": "boolean", 51 + "const": true 52 + } 53 + } 54 + } 55 + }, 56 + "errors": [ 57 + { "name": "AuthenticationRequired" }, 58 + { "name": "InvalidDomain" }, 59 + { "name": "InvalidRequest" }, 60 + { "name": "NotFound" } 61 + ] 62 + } 63 + } 64 + }
+73
lexicons/site-delete-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.site.delete", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete one owned site metadata entry and unmap any domains pointing to it.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["siteRkey"], 13 + "properties": { 14 + "siteRkey": { 15 + "type": "string", 16 + "format": "record-key", 17 + "description": "Owned place.wisp.fs record key to delete from wisp metadata." 18 + } 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["siteRkey", "deleted", "unmappedDomains"], 27 + "properties": { 28 + "siteRkey": { 29 + "type": "string", 30 + "format": "record-key" 31 + }, 32 + "deleted": { 33 + "type": "boolean", 34 + "const": true 35 + }, 36 + "unmappedDomains": { 37 + "type": "array", 38 + "description": "Domains that were detached from this site before deletion.", 39 + "items": { 40 + "type": "ref", 41 + "ref": "#unmappedDomain" 42 + } 43 + } 44 + } 45 + } 46 + }, 47 + "errors": [ 48 + { "name": "AuthenticationRequired" }, 49 + { "name": "InvalidRequest" }, 50 + { "name": "NotFound" } 51 + ] 52 + }, 53 + "unmappedDomain": { 54 + "type": "object", 55 + "required": ["domain", "kind", "status"], 56 + "properties": { 57 + "domain": { 58 + "type": "string", 59 + "minLength": 3, 60 + "maxLength": 253 61 + }, 62 + "kind": { 63 + "type": "string", 64 + "enum": ["wisp", "custom"] 65 + }, 66 + "status": { 67 + "type": "string", 68 + "enum": ["pendingVerification", "verified"] 69 + } 70 + } 71 + } 72 + } 73 + }
+80
lexicons/site-get-list-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.site.getList", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List owned sites and the domains currently mapped to each site.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["sites"], 13 + "properties": { 14 + "sites": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "#siteSummary" 19 + } 20 + } 21 + } 22 + } 23 + }, 24 + "errors": [ 25 + { "name": "AuthenticationRequired" } 26 + ] 27 + }, 28 + "siteSummary": { 29 + "type": "object", 30 + "required": ["siteRkey", "domains"], 31 + "properties": { 32 + "siteRkey": { 33 + "type": "string", 34 + "format": "record-key" 35 + }, 36 + "displayName": { 37 + "type": "string", 38 + "maxLength": 200 39 + }, 40 + "createdAt": { 41 + "type": "string", 42 + "format": "datetime" 43 + }, 44 + "updatedAt": { 45 + "type": "string", 46 + "format": "datetime" 47 + }, 48 + "domains": { 49 + "type": "array", 50 + "items": { 51 + "type": "ref", 52 + "ref": "#siteDomain" 53 + } 54 + } 55 + } 56 + }, 57 + "siteDomain": { 58 + "type": "object", 59 + "required": ["domain", "kind", "status", "verified"], 60 + "properties": { 61 + "domain": { 62 + "type": "string", 63 + "minLength": 3, 64 + "maxLength": 253 65 + }, 66 + "kind": { 67 + "type": "string", 68 + "enum": ["wisp", "custom"] 69 + }, 70 + "status": { 71 + "type": "string", 72 + "enum": ["pendingVerification", "verified"] 73 + }, 74 + "verified": { 75 + "type": "boolean" 76 + } 77 + } 78 + } 79 + } 80 + }
+4 -2
packages/@wispplace/lexicons/package.json
··· 60 60 } 61 61 }, 62 62 "scripts": { 63 - "codegen": "lex gen-server ./src ../../../lexicons/*.json", 64 - "codegen:atcute": "lex-cli generate -c lex.atcute.config.js" 63 + "codegen": "bun run codegen:server && bun run codegen:atcute && bun run codegen:verify", 64 + "codegen:server": "lex gen-server ./src ../../../lexicons/*.json", 65 + "codegen:atcute": "lex-cli generate -c lex.atcute.config.js", 66 + "codegen:verify": "test -f ./src/atcute/lexicons/index.ts" 65 67 }, 66 68 "dependencies": { 67 69 "@atcute/lexicons": "^1.2.9",
+3
packages/@wispplace/lexicons/src/atcute/lexicons/index.ts
··· 1 + export * as PlaceWispV2DomainAddSite from "./types/place/wisp/v2/domain/addSite.js"; 1 2 export * as PlaceWispV2DomainClaim from "./types/place/wisp/v2/domain/claim.js"; 2 3 export * as PlaceWispV2DomainClaimSubdomain from "./types/place/wisp/v2/domain/claimSubdomain.js"; 3 4 export * as PlaceWispV2DomainDelete from "./types/place/wisp/v2/domain/delete.js"; 4 5 export * as PlaceWispV2DomainGetList from "./types/place/wisp/v2/domain/getList.js"; 5 6 export * as PlaceWispV2DomainGetStatus from "./types/place/wisp/v2/domain/getStatus.js"; 6 7 export * as PlaceWispV2Domains from "./types/place/wisp/v2/domains.js"; 8 + export * as PlaceWispV2SiteDelete from "./types/place/wisp/v2/site/delete.js"; 9 + export * as PlaceWispV2SiteGetList from "./types/place/wisp/v2/site/getList.js";
+50
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/addSite.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.addSite", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + /** 11 + * Fully-qualified domain to map. 12 + * @minLength 3 13 + * @maxLength 253 14 + */ 15 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 16 + /*#__PURE__*/ v.stringLength(3, 253), 17 + ]), 18 + /** 19 + * Owned place.wisp.fs record key to map this domain to. 20 + */ 21 + siteRkey: /*#__PURE__*/ v.recordKeyString(), 22 + }), 23 + }, 24 + output: { 25 + type: "lex", 26 + schema: /*#__PURE__*/ v.object({ 27 + domain: /*#__PURE__*/ v.string(), 28 + kind: /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 29 + mapped: /*#__PURE__*/ v.literal(true), 30 + siteRkey: /*#__PURE__*/ v.recordKeyString(), 31 + status: /*#__PURE__*/ v.literalEnum(["pendingVerification", "verified"]), 32 + }), 33 + }, 34 + }); 35 + 36 + type main$schematype = typeof _mainSchema; 37 + 38 + export interface mainSchema extends main$schematype {} 39 + 40 + export const mainSchema = _mainSchema as mainSchema; 41 + 42 + export interface $params {} 43 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 44 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 45 + 46 + declare module "@atcute/lexicons/ambient" { 47 + interface XRPCProcedures { 48 + "place.wisp.v2.domain.addSite": mainSchema; 49 + } 50 + }
+67
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/site/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.site.delete", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + /** 11 + * Owned place.wisp.fs record key to delete from wisp metadata. 12 + */ 13 + siteRkey: /*#__PURE__*/ v.recordKeyString(), 14 + }), 15 + }, 16 + output: { 17 + type: "lex", 18 + schema: /*#__PURE__*/ v.object({ 19 + deleted: /*#__PURE__*/ v.literal(true), 20 + siteRkey: /*#__PURE__*/ v.recordKeyString(), 21 + /** 22 + * Domains that were detached from this site before deletion. 23 + */ 24 + get unmappedDomains() { 25 + return /*#__PURE__*/ v.array(unmappedDomainSchema); 26 + }, 27 + }), 28 + }, 29 + }); 30 + const _unmappedDomainSchema = /*#__PURE__*/ v.object({ 31 + $type: /*#__PURE__*/ v.optional( 32 + /*#__PURE__*/ v.literal("place.wisp.v2.site.delete#unmappedDomain"), 33 + ), 34 + /** 35 + * @minLength 3 36 + * @maxLength 253 37 + */ 38 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 39 + /*#__PURE__*/ v.stringLength(3, 253), 40 + ]), 41 + kind: /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 42 + status: /*#__PURE__*/ v.literalEnum(["pendingVerification", "verified"]), 43 + }); 44 + 45 + type main$schematype = typeof _mainSchema; 46 + type unmappedDomain$schematype = typeof _unmappedDomainSchema; 47 + 48 + export interface mainSchema extends main$schematype {} 49 + export interface unmappedDomainSchema extends unmappedDomain$schematype {} 50 + 51 + export const mainSchema = _mainSchema as mainSchema; 52 + export const unmappedDomainSchema = 53 + _unmappedDomainSchema as unmappedDomainSchema; 54 + 55 + export interface UnmappedDomain extends v.InferInput< 56 + typeof unmappedDomainSchema 57 + > {} 58 + 59 + export interface $params {} 60 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 61 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 62 + 63 + declare module "@atcute/lexicons/ambient" { 64 + interface XRPCProcedures { 65 + "place.wisp.v2.site.delete": mainSchema; 66 + } 67 + }
+73
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/site/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 _mainSchema = /*#__PURE__*/ v.query("place.wisp.v2.site.getList", { 6 + params: null, 7 + output: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + get sites() { 11 + return /*#__PURE__*/ v.array(siteSummarySchema); 12 + }, 13 + }), 14 + }, 15 + }); 16 + const _siteDomainSchema = /*#__PURE__*/ v.object({ 17 + $type: /*#__PURE__*/ v.optional( 18 + /*#__PURE__*/ v.literal("place.wisp.v2.site.getList#siteDomain"), 19 + ), 20 + /** 21 + * @minLength 3 22 + * @maxLength 253 23 + */ 24 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 25 + /*#__PURE__*/ v.stringLength(3, 253), 26 + ]), 27 + kind: /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 28 + status: /*#__PURE__*/ v.literalEnum(["pendingVerification", "verified"]), 29 + verified: /*#__PURE__*/ v.boolean(), 30 + }); 31 + const _siteSummarySchema = /*#__PURE__*/ v.object({ 32 + $type: /*#__PURE__*/ v.optional( 33 + /*#__PURE__*/ v.literal("place.wisp.v2.site.getList#siteSummary"), 34 + ), 35 + createdAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 36 + /** 37 + * @maxLength 200 38 + */ 39 + displayName: /*#__PURE__*/ v.optional( 40 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 41 + /*#__PURE__*/ v.stringLength(0, 200), 42 + ]), 43 + ), 44 + get domains() { 45 + return /*#__PURE__*/ v.array(siteDomainSchema); 46 + }, 47 + siteRkey: /*#__PURE__*/ v.recordKeyString(), 48 + updatedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 49 + }); 50 + 51 + type main$schematype = typeof _mainSchema; 52 + type siteDomain$schematype = typeof _siteDomainSchema; 53 + type siteSummary$schematype = typeof _siteSummarySchema; 54 + 55 + export interface mainSchema extends main$schematype {} 56 + export interface siteDomainSchema extends siteDomain$schematype {} 57 + export interface siteSummarySchema extends siteSummary$schematype {} 58 + 59 + export const mainSchema = _mainSchema as mainSchema; 60 + export const siteDomainSchema = _siteDomainSchema as siteDomainSchema; 61 + export const siteSummarySchema = _siteSummarySchema as siteSummarySchema; 62 + 63 + export interface SiteDomain extends v.InferInput<typeof siteDomainSchema> {} 64 + export interface SiteSummary extends v.InferInput<typeof siteSummarySchema> {} 65 + 66 + export interface $params {} 67 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 68 + 69 + declare module "@atcute/lexicons/ambient" { 70 + interface XRPCQueries { 71 + "place.wisp.v2.site.getList": mainSchema; 72 + } 73 + }
+49
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 PlaceWispV2DomainAddSite from './types/place/wisp/v2/domain/addSite.js' 13 14 import * as PlaceWispV2DomainClaimSubdomain from './types/place/wisp/v2/domain/claimSubdomain.js' 14 15 import * as PlaceWispV2DomainClaim from './types/place/wisp/v2/domain/claim.js' 15 16 import * as PlaceWispV2DomainDelete from './types/place/wisp/v2/domain/delete.js' 16 17 import * as PlaceWispV2DomainGetList from './types/place/wisp/v2/domain/getList.js' 17 18 import * as PlaceWispV2DomainGetStatus from './types/place/wisp/v2/domain/getStatus.js' 19 + import * as PlaceWispV2SiteDelete from './types/place/wisp/v2/site/delete.js' 20 + import * as PlaceWispV2SiteGetList from './types/place/wisp/v2/site/getList.js' 18 21 19 22 export function createServer(options?: XrpcOptions): Server { 20 23 return new Server(options) ··· 53 56 export class PlaceWispV2NS { 54 57 _server: Server 55 58 domain: PlaceWispV2DomainNS 59 + site: PlaceWispV2SiteNS 56 60 57 61 constructor(server: Server) { 58 62 this._server = server 59 63 this.domain = new PlaceWispV2DomainNS(server) 64 + this.site = new PlaceWispV2SiteNS(server) 60 65 } 61 66 } 62 67 ··· 67 72 this._server = server 68 73 } 69 74 75 + addSite<A extends Auth = void>( 76 + cfg: MethodConfigOrHandler< 77 + A, 78 + PlaceWispV2DomainAddSite.QueryParams, 79 + PlaceWispV2DomainAddSite.HandlerInput, 80 + PlaceWispV2DomainAddSite.HandlerOutput 81 + >, 82 + ) { 83 + const nsid = 'place.wisp.v2.domain.addSite' // @ts-ignore 84 + return this._server.xrpc.method(nsid, cfg) 85 + } 86 + 70 87 claimSubdomain<A extends Auth = void>( 71 88 cfg: MethodConfigOrHandler< 72 89 A, ··· 127 144 return this._server.xrpc.method(nsid, cfg) 128 145 } 129 146 } 147 + 148 + export class PlaceWispV2SiteNS { 149 + _server: Server 150 + 151 + constructor(server: Server) { 152 + this._server = server 153 + } 154 + 155 + delete<A extends Auth = void>( 156 + cfg: MethodConfigOrHandler< 157 + A, 158 + PlaceWispV2SiteDelete.QueryParams, 159 + PlaceWispV2SiteDelete.HandlerInput, 160 + PlaceWispV2SiteDelete.HandlerOutput 161 + >, 162 + ) { 163 + const nsid = 'place.wisp.v2.site.delete' // @ts-ignore 164 + return this._server.xrpc.method(nsid, cfg) 165 + } 166 + 167 + getList<A extends Auth = void>( 168 + cfg: MethodConfigOrHandler< 169 + A, 170 + PlaceWispV2SiteGetList.QueryParams, 171 + PlaceWispV2SiteGetList.HandlerInput, 172 + PlaceWispV2SiteGetList.HandlerOutput 173 + >, 174 + ) { 175 + const nsid = 'place.wisp.v2.site.getList' // @ts-ignore 176 + return this._server.xrpc.method(nsid, cfg) 177 + } 178 + }
+241
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 + PlaceWispV2DomainAddSite: { 14 + lexicon: 1, 15 + id: 'place.wisp.v2.domain.addSite', 16 + defs: { 17 + main: { 18 + type: 'procedure', 19 + description: 'Map an owned domain to one owned site record.', 20 + input: { 21 + encoding: 'application/json', 22 + schema: { 23 + type: 'object', 24 + required: ['domain', 'siteRkey'], 25 + properties: { 26 + domain: { 27 + type: 'string', 28 + description: 'Fully-qualified domain to map.', 29 + minLength: 3, 30 + maxLength: 253, 31 + }, 32 + siteRkey: { 33 + type: 'string', 34 + format: 'record-key', 35 + description: 36 + 'Owned place.wisp.fs record key to map this domain to.', 37 + }, 38 + }, 39 + }, 40 + }, 41 + output: { 42 + encoding: 'application/json', 43 + schema: { 44 + type: 'object', 45 + required: ['domain', 'kind', 'status', 'siteRkey', 'mapped'], 46 + properties: { 47 + domain: { 48 + type: 'string', 49 + }, 50 + kind: { 51 + type: 'string', 52 + enum: ['wisp', 'custom'], 53 + }, 54 + status: { 55 + type: 'string', 56 + enum: ['pendingVerification', 'verified'], 57 + }, 58 + siteRkey: { 59 + type: 'string', 60 + format: 'record-key', 61 + }, 62 + mapped: { 63 + type: 'boolean', 64 + const: true, 65 + }, 66 + }, 67 + }, 68 + }, 69 + errors: [ 70 + { 71 + name: 'AuthenticationRequired', 72 + }, 73 + { 74 + name: 'InvalidDomain', 75 + }, 76 + { 77 + name: 'InvalidRequest', 78 + }, 79 + { 80 + name: 'NotFound', 81 + }, 82 + ], 83 + }, 84 + }, 85 + }, 13 86 PlaceWispV2DomainClaimSubdomain: { 14 87 lexicon: 1, 15 88 id: 'place.wisp.v2.domain.claimSubdomain', ··· 678 751 }, 679 752 }, 680 753 }, 754 + PlaceWispV2SiteDelete: { 755 + lexicon: 1, 756 + id: 'place.wisp.v2.site.delete', 757 + defs: { 758 + main: { 759 + type: 'procedure', 760 + description: 761 + 'Delete one owned site metadata entry and unmap any domains pointing to it.', 762 + input: { 763 + encoding: 'application/json', 764 + schema: { 765 + type: 'object', 766 + required: ['siteRkey'], 767 + properties: { 768 + siteRkey: { 769 + type: 'string', 770 + format: 'record-key', 771 + description: 772 + 'Owned place.wisp.fs record key to delete from wisp metadata.', 773 + }, 774 + }, 775 + }, 776 + }, 777 + output: { 778 + encoding: 'application/json', 779 + schema: { 780 + type: 'object', 781 + required: ['siteRkey', 'deleted', 'unmappedDomains'], 782 + properties: { 783 + siteRkey: { 784 + type: 'string', 785 + format: 'record-key', 786 + }, 787 + deleted: { 788 + type: 'boolean', 789 + const: true, 790 + }, 791 + unmappedDomains: { 792 + type: 'array', 793 + description: 794 + 'Domains that were detached from this site before deletion.', 795 + items: { 796 + type: 'ref', 797 + ref: 'lex:place.wisp.v2.site.delete#unmappedDomain', 798 + }, 799 + }, 800 + }, 801 + }, 802 + }, 803 + errors: [ 804 + { 805 + name: 'AuthenticationRequired', 806 + }, 807 + { 808 + name: 'InvalidRequest', 809 + }, 810 + { 811 + name: 'NotFound', 812 + }, 813 + ], 814 + }, 815 + unmappedDomain: { 816 + type: 'object', 817 + required: ['domain', 'kind', 'status'], 818 + properties: { 819 + domain: { 820 + type: 'string', 821 + minLength: 3, 822 + maxLength: 253, 823 + }, 824 + kind: { 825 + type: 'string', 826 + enum: ['wisp', 'custom'], 827 + }, 828 + status: { 829 + type: 'string', 830 + enum: ['pendingVerification', 'verified'], 831 + }, 832 + }, 833 + }, 834 + }, 835 + }, 836 + PlaceWispV2SiteGetList: { 837 + lexicon: 1, 838 + id: 'place.wisp.v2.site.getList', 839 + defs: { 840 + main: { 841 + type: 'query', 842 + description: 843 + 'List owned sites and the domains currently mapped to each site.', 844 + output: { 845 + encoding: 'application/json', 846 + schema: { 847 + type: 'object', 848 + required: ['sites'], 849 + properties: { 850 + sites: { 851 + type: 'array', 852 + items: { 853 + type: 'ref', 854 + ref: 'lex:place.wisp.v2.site.getList#siteSummary', 855 + }, 856 + }, 857 + }, 858 + }, 859 + }, 860 + errors: [ 861 + { 862 + name: 'AuthenticationRequired', 863 + }, 864 + ], 865 + }, 866 + siteSummary: { 867 + type: 'object', 868 + required: ['siteRkey', 'domains'], 869 + properties: { 870 + siteRkey: { 871 + type: 'string', 872 + format: 'record-key', 873 + }, 874 + displayName: { 875 + type: 'string', 876 + maxLength: 200, 877 + }, 878 + createdAt: { 879 + type: 'string', 880 + format: 'datetime', 881 + }, 882 + updatedAt: { 883 + type: 'string', 884 + format: 'datetime', 885 + }, 886 + domains: { 887 + type: 'array', 888 + items: { 889 + type: 'ref', 890 + ref: 'lex:place.wisp.v2.site.getList#siteDomain', 891 + }, 892 + }, 893 + }, 894 + }, 895 + siteDomain: { 896 + type: 'object', 897 + required: ['domain', 'kind', 'status', 'verified'], 898 + properties: { 899 + domain: { 900 + type: 'string', 901 + minLength: 3, 902 + maxLength: 253, 903 + }, 904 + kind: { 905 + type: 'string', 906 + enum: ['wisp', 'custom'], 907 + }, 908 + status: { 909 + type: 'string', 910 + enum: ['pendingVerification', 'verified'], 911 + }, 912 + verified: { 913 + type: 'boolean', 914 + }, 915 + }, 916 + }, 917 + }, 918 + }, 681 919 PlaceWispSubfs: { 682 920 lexicon: 1, 683 921 id: 'place.wisp.subfs', ··· 823 1061 } 824 1062 825 1063 export const ids = { 1064 + PlaceWispV2DomainAddSite: 'place.wisp.v2.domain.addSite', 826 1065 PlaceWispV2DomainClaimSubdomain: 'place.wisp.v2.domain.claimSubdomain', 827 1066 PlaceWispV2DomainClaim: 'place.wisp.v2.domain.claim', 828 1067 PlaceWispV2DomainDelete: 'place.wisp.v2.domain.delete', ··· 831 1070 PlaceWispV2Domains: 'place.wisp.v2.domains', 832 1071 PlaceWispFs: 'place.wisp.fs', 833 1072 PlaceWispSettings: 'place.wisp.settings', 1073 + PlaceWispV2SiteDelete: 'place.wisp.v2.site.delete', 1074 + PlaceWispV2SiteGetList: 'place.wisp.v2.site.getList', 834 1075 PlaceWispSubfs: 'place.wisp.subfs', 835 1076 } as const
+55
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/addSite.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.addSite' 16 + 17 + export type QueryParams = {} 18 + 19 + export interface InputSchema { 20 + /** Fully-qualified domain to map. */ 21 + domain: string 22 + /** Owned place.wisp.fs record key to map this domain to. */ 23 + siteRkey: string 24 + } 25 + 26 + export interface OutputSchema { 27 + domain: string 28 + kind: 'wisp' | 'custom' 29 + status: 'pendingVerification' | 'verified' 30 + siteRkey: string 31 + mapped: true 32 + } 33 + 34 + export interface HandlerInput { 35 + encoding: 'application/json' 36 + body: InputSchema 37 + } 38 + 39 + export interface HandlerSuccess { 40 + encoding: 'application/json' 41 + body: OutputSchema 42 + headers?: { [key: string]: string } 43 + } 44 + 45 + export interface HandlerError { 46 + status: number 47 + message?: string 48 + error?: 49 + | 'AuthenticationRequired' 50 + | 'InvalidDomain' 51 + | 'InvalidRequest' 52 + | 'NotFound' 53 + } 54 + 55 + export type HandlerOutput = HandlerError | HandlerSuccess
+65
packages/@wispplace/lexicons/src/types/place/wisp/v2/site/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.site.delete' 16 + 17 + export type QueryParams = {} 18 + 19 + export interface InputSchema { 20 + /** Owned place.wisp.fs record key to delete from wisp metadata. */ 21 + siteRkey: string 22 + } 23 + 24 + export interface OutputSchema { 25 + siteRkey: string 26 + deleted: true 27 + /** Domains that were detached from this site before deletion. */ 28 + unmappedDomains: UnmappedDomain[] 29 + } 30 + 31 + export interface HandlerInput { 32 + encoding: 'application/json' 33 + body: InputSchema 34 + } 35 + 36 + export interface HandlerSuccess { 37 + encoding: 'application/json' 38 + body: OutputSchema 39 + headers?: { [key: string]: string } 40 + } 41 + 42 + export interface HandlerError { 43 + status: number 44 + message?: string 45 + error?: 'AuthenticationRequired' | 'InvalidRequest' | 'NotFound' 46 + } 47 + 48 + export type HandlerOutput = HandlerError | HandlerSuccess 49 + 50 + export interface UnmappedDomain { 51 + $type?: 'place.wisp.v2.site.delete#unmappedDomain' 52 + domain: string 53 + kind: 'wisp' | 'custom' 54 + status: 'pendingVerification' | 'verified' 55 + } 56 + 57 + const hashUnmappedDomain = 'unmappedDomain' 58 + 59 + export function isUnmappedDomain<V>(v: V) { 60 + return is$typed(v, id, hashUnmappedDomain) 61 + } 62 + 63 + export function validateUnmappedDomain<V>(v: V) { 64 + return validate<UnmappedDomain & V>(v, id, hashUnmappedDomain) 65 + }
+75
packages/@wispplace/lexicons/src/types/place/wisp/v2/site/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.site.getList' 16 + 17 + export type QueryParams = {} 18 + export type InputSchema = undefined 19 + 20 + export interface OutputSchema { 21 + sites: SiteSummary[] 22 + } 23 + 24 + export type HandlerInput = void 25 + 26 + export interface HandlerSuccess { 27 + encoding: 'application/json' 28 + body: OutputSchema 29 + headers?: { [key: string]: string } 30 + } 31 + 32 + export interface HandlerError { 33 + status: number 34 + message?: string 35 + error?: 'AuthenticationRequired' 36 + } 37 + 38 + export type HandlerOutput = HandlerError | HandlerSuccess 39 + 40 + export interface SiteSummary { 41 + $type?: 'place.wisp.v2.site.getList#siteSummary' 42 + siteRkey: string 43 + displayName?: string 44 + createdAt?: string 45 + updatedAt?: string 46 + domains: SiteDomain[] 47 + } 48 + 49 + const hashSiteSummary = 'siteSummary' 50 + 51 + export function isSiteSummary<V>(v: V) { 52 + return is$typed(v, id, hashSiteSummary) 53 + } 54 + 55 + export function validateSiteSummary<V>(v: V) { 56 + return validate<SiteSummary & V>(v, id, hashSiteSummary) 57 + } 58 + 59 + export interface SiteDomain { 60 + $type?: 'place.wisp.v2.site.getList#siteDomain' 61 + domain: string 62 + kind: 'wisp' | 'custom' 63 + status: 'pendingVerification' | 'verified' 64 + verified: boolean 65 + } 66 + 67 + const hashSiteDomain = 'siteDomain' 68 + 69 + export function isSiteDomain<V>(v: V) { 70 + return is$typed(v, id, hashSiteDomain) 71 + } 72 + 73 + export function validateSiteDomain<V>(v: V) { 74 + return validate<SiteDomain & V>(v, id, hashSiteDomain) 75 + }
+4 -2
scripts/codegen.sh
··· 14 14 cd "$ROOT_DIR/packages/@wispplace/lexicons" 15 15 eval "$AUTO_ACCEPT bun run codegen" 16 16 17 - echo "=== Generating atcute lexicons ===" 18 - eval "$AUTO_ACCEPT bun run codegen:atcute" 17 + if [[ ! -f "$ROOT_DIR/packages/@wispplace/lexicons/src/atcute/lexicons/index.ts" ]]; then 18 + echo "ERROR: missing generated atcute lexicons index at packages/@wispplace/lexicons/src/atcute/lexicons/index.ts" >&2 19 + exit 1 20 + fi 19 21 20 22 echo "=== Done ==="