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.

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 ==="