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

Configure Feed

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

fix type issues after prior linting, public xrpc route to query a site's attached domains

authored by

nekomimi.pet and committed by
Tangled
8f7b0756 11755b25

+365 -40
+16 -6
apps/main-app/public/editor/tabs/UploadTab.tsx
··· 37 37 const [isDragging, setIsDragging] = useState(false) 38 38 39 39 // Ref for the drop zone 40 - const dropZoneRef = useRef<HTMLDivElement>(null) 40 + const dropZoneRef = useRef<HTMLButtonElement>(null) 41 41 42 42 // Keep SSE connection alive across tab switches 43 43 const eventSourceRef = useRef<EventSource | null>(null) ··· 106 106 } 107 107 108 108 // Handle dropped items (files or directories) 109 - const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => { 109 + const handleDrop = async (e: React.DragEvent<HTMLButtonElement>) => { 110 110 e.preventDefault() 111 111 e.stopPropagation() 112 112 setIsDragging(false) ··· 144 144 } 145 145 } 146 146 147 - const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { 147 + const handleDragOver = (e: React.DragEvent<HTMLButtonElement>) => { 148 148 e.preventDefault() 149 149 e.stopPropagation() 150 150 if (!isUploading) { ··· 152 152 } 153 153 } 154 154 155 - const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => { 155 + const handleDragEnter = (e: React.DragEvent<HTMLButtonElement>) => { 156 156 e.preventDefault() 157 157 e.stopPropagation() 158 158 if (!isUploading) { ··· 160 160 } 161 161 } 162 162 163 - const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => { 163 + const handleDragLeave = (e: React.DragEvent<HTMLButtonElement>) => { 164 164 e.preventDefault() 165 165 e.stopPropagation() 166 166 // Only set isDragging to false if we're leaving the drop zone entirely 167 167 if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) { 168 168 setIsDragging(false) 169 169 } 170 + } 171 + 172 + const handleDropZoneKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => { 173 + if (isUploading) return 174 + if (e.key !== 'Enter' && e.key !== ' ') return 175 + 176 + e.preventDefault() 177 + document.getElementById('file-upload')?.click() 170 178 } 171 179 172 180 const setupSSE = (jobId: string) => { ··· 491 499 {/* Drop zone */} 492 500 <button 493 501 type="button" 494 - ref={dropZoneRef as React.RefObject<HTMLButtonElement>} 495 502 className={`w-full text-left border-2 border-dashed p-4 flex items-center gap-3 transition-colors ${ 496 503 isDragging 497 504 ? 'border-accent bg-accent/10 cursor-copy' ··· 499 506 ? 'opacity-50 cursor-not-allowed border-border/30' 500 507 : 'border-border/30 hover:border-accent cursor-pointer' 501 508 }`} 509 + ref={dropZoneRef} 510 + disabled={isUploading} 502 511 onDrop={handleDrop} 503 512 onDragOver={handleDragOver} 504 513 onDragEnter={handleDragEnter} 505 514 onDragLeave={handleDragLeave} 515 + onKeyDown={handleDropZoneKeyDown} 506 516 onClick={() => !isUploading && document.getElementById('file-upload')?.click()} 507 517 > 508 518 <Upload
+27
apps/main-app/src/lib/dns-packet.d.ts
··· 1 + declare module 'dns-packet' { 2 + export type RecordType = 'A' | 'NS' | 'TXT' | 'CNAME' 3 + 4 + export interface Question { 5 + type: RecordType 6 + name: string 7 + class?: 'IN' 8 + } 9 + 10 + export interface Answer { 11 + type: RecordType 12 + name: string 13 + data?: string | string[] | Buffer | Buffer[] 14 + } 15 + 16 + export interface Packet { 17 + type: 'query' | 'response' 18 + id?: number 19 + flags?: number 20 + questions?: Question[] 21 + answers?: Answer[] 22 + additionals?: Answer[] 23 + } 24 + 25 + export function encode(packet: Packet): Buffer 26 + export function decode(buffer: Buffer): Packet 27 + }
+10 -5
apps/main-app/src/lib/dns-verify.ts
··· 72 72 73 73 function extractNSFromAnswer(response: dnsPacket.Packet): string[] { 74 74 return (response.answers ?? []) 75 - .filter((r) => r.type === 'NS' && 'data' in r && typeof r.data === 'string') 76 - .map((r) => (r as any).data.toLowerCase().replace(/\.$/, '')) 75 + .filter((record): record is dnsPacket.Answer & { type: 'NS'; data: string } => { 76 + return record.type === 'NS' && typeof record.data === 'string' 77 + }) 78 + .map((record) => record.data.toLowerCase().replace(/\.$/, '')) 77 79 } 78 80 79 81 /** ··· 138 140 async function authoritativeResolve(name: string, type: dnsPacket.RecordType): Promise<dnsPacket.Packet> { 139 141 console.log(`[DNS] Resolving ${type} ${name} via authoritative NS`) 140 142 const servers = await getAuthoritativeServers(name) 143 + const shuffledServers = [...servers].sort(() => Math.random() - 0.5) 141 144 142 145 let lastError: Error | null = null 143 - for (const server of servers.toSorted(() => Math.random() - 0.5)) { 146 + for (const server of shuffledServers) { 144 147 try { 145 148 return await queryDNS(name, type, server) 146 149 } catch (err) { ··· 171 174 async function authoritativeResolveCname(domain: string): Promise<string[]> { 172 175 const response = await authoritativeResolve(domain, 'CNAME') 173 176 return (response.answers ?? []) 174 - .filter((r) => r.type === 'CNAME' && 'data' in r && typeof r.data === 'string') 175 - .map((r) => (r as any).data.toLowerCase().replace(/\.$/, '')) 177 + .filter((record): record is dnsPacket.Answer & { type: 'CNAME'; data: string } => { 178 + return record.type === 'CNAME' && typeof record.data === 'string' 179 + }) 180 + .map((record) => record.data.toLowerCase().replace(/\.$/, '')) 176 181 } 177 182 178 183 /**
+40 -20
apps/main-app/src/routes/xrpc.ts
··· 11 11 PlaceWispV2DomainGetList, 12 12 PlaceWispV2DomainGetStatus, 13 13 PlaceWispV2SiteDelete, 14 + PlaceWispV2SiteGetDomains, 14 15 PlaceWispV2SiteGetList, 15 16 } from '@wispplace/lexicons/atcute' 16 17 import { createLogger } from '@wispplace/observability' ··· 64 65 'place.wisp.v2.domain.get-status': 'place.wisp.v2.domain.getStatus', 65 66 'place.wisp.v2.site.delete-site': 'place.wisp.v2.site.delete', 66 67 'place.wisp.v2.site.deletesite': 'place.wisp.v2.site.delete', 68 + 'place.wisp.v2.site.get-domains': 'place.wisp.v2.site.getDomains', 69 + 'place.wisp.v2.site.getdomains': 'place.wisp.v2.site.getDomains', 67 70 'place.wisp.v2.site.get-list': 'place.wisp.v2.site.getList', 68 71 'place.wisp.v2.site.getlist': 'place.wisp.v2.site.getList', 69 72 } ··· 72 75 addSite: 'place.wisp.v2.domain.addSite', 73 76 getStatus: 'place.wisp.v2.domain.getStatus', 74 77 getList: 'place.wisp.v2.domain.getList', 78 + siteGetDomains: 'place.wisp.v2.site.getDomains', 75 79 siteGetList: 'place.wisp.v2.site.getList', 76 80 claimSubdomain: 'place.wisp.v2.domain.claimSubdomain', 77 81 claim: 'place.wisp.v2.domain.claim', ··· 100 104 } 101 105 102 106 return date.toISOString() 107 + } 108 + 109 + const mapSiteDomains = ( 110 + mappedDomains: Array<{ 111 + type: 'wisp' | 'custom' 112 + domain: string 113 + verified?: boolean 114 + }>, 115 + ) => { 116 + return mappedDomains 117 + .map((entry) => ({ 118 + domain: entry.domain, 119 + kind: entry.type, 120 + status: 121 + entry.type === 'wisp' 122 + ? ('verified' as const) 123 + : entry.verified 124 + ? ('verified' as const) 125 + : ('pendingVerification' as const), 126 + verified: entry.type === 'wisp' ? true : Boolean(entry.verified), 127 + })) 128 + .sort((a, b) => a.domain.localeCompare(b.domain)) 103 129 } 104 130 105 131 type DidString = `did:${string}:${string}` ··· 646 672 647 673 addQueryWithAliases( 648 674 router, 675 + withNsid(PlaceWispV2SiteGetDomains.mainSchema as any, XRPC_NSIDS.siteGetDomains), 676 + ['place.wisp.v2.site.getdomains', 'place.wisp.v2.site.get-domains'], 677 + { 678 + async handler({ params }) { 679 + const domains = mapSiteDomains(await getDomainsBySite(params.did, params.rkey)) 680 + return json({ domains }) 681 + }, 682 + }, 683 + ) 684 + 685 + addQueryWithAliases( 686 + router, 649 687 withNsid(PlaceWispV2SiteGetList.mainSchema as any, XRPC_NSIDS.siteGetList), 650 688 ['place.wisp.v2.site.getlist', 'place.wisp.v2.site.get-list'], 651 689 { ··· 663 701 created_at?: number | string | null 664 702 updated_at?: number | string | null 665 703 }) => { 666 - const mappedDomains = await getDomainsBySite(did, site.rkey) 667 - const domains = ( 668 - mappedDomains as Array<{ 669 - type: 'wisp' | 'custom' 670 - domain: string 671 - verified?: boolean 672 - }> 673 - ) 674 - .map((entry) => ({ 675 - domain: entry.domain, 676 - kind: entry.type, 677 - status: 678 - entry.type === 'wisp' 679 - ? ('verified' as const) 680 - : entry.verified 681 - ? ('verified' as const) 682 - : ('pendingVerification' as const), 683 - verified: entry.type === 'wisp' ? true : Boolean(entry.verified), 684 - })) 685 - .sort((a, b) => a.domain.localeCompare(b.domain)) 704 + const domains = mapSiteDomains(await getDomainsBySite(did, site.rkey)) 686 705 687 706 return { 688 707 siteRkey: site.rkey, ··· 785 804 const schemaNsids = { 786 805 addSite: (PlaceWispV2DomainAddSite.mainSchema as any).nsid, 787 806 getStatus: (PlaceWispV2DomainGetStatus.mainSchema as any).nsid, 807 + siteGetDomains: (PlaceWispV2SiteGetDomains.mainSchema as any).nsid, 788 808 getList: (PlaceWispV2DomainGetList.mainSchema as any).nsid, 789 809 siteGetList: (PlaceWispV2SiteGetList.mainSchema as any).nsid, 790 810 claimSubdomain: (PlaceWispV2DomainClaimSubdomain.mainSchema as any).nsid,
+15 -5
cli/commands/pull.ts
··· 15 15 path: string 16 16 } 17 17 18 - async function fetchRecord(pdsEndpoint: string, did: string, collection: string, rkey: string): Promise<any> { 18 + interface GetRecordResponse<T> { 19 + value: T 20 + cid?: string 21 + } 22 + 23 + async function fetchRecord<T>( 24 + pdsEndpoint: string, 25 + did: string, 26 + collection: string, 27 + rkey: string, 28 + ): Promise<GetRecordResponse<T>> { 19 29 const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}` 20 30 const res = await fetch(url) 21 31 if (!res.ok) { 22 32 throw new Error(`Failed to fetch record: ${res.status}`) 23 33 } 24 - return res.json() 34 + return (await res.json()) as GetRecordResponse<T> 25 35 } 26 36 27 37 function extractSubfsUris(directory: Directory, currentPath: string = ''): Array<{ uri: string; path: string }> { ··· 253 263 254 264 // 3. Fetch site record 255 265 const recordSpinner = createSpinner('Fetching site record...').start() 256 - let recordData: unknown 266 + let recordData: GetRecordResponse<FsRecord> 257 267 258 268 try { 259 - recordData = await fetchRecord(pdsEndpoint, did, 'place.wisp.fs', site) 269 + recordData = await fetchRecord<FsRecord>(pdsEndpoint, did, 'place.wisp.fs', site) 260 270 } catch { 261 271 recordSpinner.fail('Site not found') 262 272 throw new Error(`Site not found: ${site}`) 263 273 } 264 274 265 - const record = recordData.value as FsRecord 275 + const record = recordData.value 266 276 const recordCid = recordData.cid || '' 267 277 recordSpinner.succeed('Fetched site record') 268 278
+3 -3
cli/commands/serve.ts
··· 4 4 import { Firehose } from '@atproto/sync' 5 5 import { serve as honoNodeServe } from '@hono/node-server' 6 6 import { getPdsForDid, resolveDid } from '@wispplace/atproto-utils' 7 - import { BunFirehose, isBun } from '@wispplace/bun-firehose' 7 + import { BunFirehose, type BunFirehoseOptions, isBun } from '@wispplace/bun-firehose' 8 8 import { matchRedirectRule, parseQueryString, parseRedirectsFile, type RedirectRule } from '@wispplace/fs-utils' 9 9 import type { Record as SettingsRecord } from '@wispplace/lexicons/types/place/wisp/settings' 10 10 import { generate404Page, generateDirectoryListing } from '@wispplace/page-generators' ··· 266 266 let serverHandle: { close: () => void } 267 267 268 268 if (isBun) { 269 - // @ts-expect-error - Bun global 270 269 const bunServer = Bun.serve({ 271 270 port, 272 271 fetch: app.fetch, ··· 317 316 318 317 if (isBun) { 319 318 // Use BunFirehose for Bun 319 + const bunIdResolver = idResolver as unknown as BunFirehoseOptions['idResolver'] 320 320 const bunFirehose = new BunFirehose({ 321 - idResolver, 321 + idResolver: bunIdResolver, 322 322 service: pdsEndpoint, 323 323 filterCollections: ['place.wisp.fs', 'place.wisp.settings'], 324 324 handleEvent: firehoseHandleEvent,
-1
cli/lib/auth.ts
··· 307 307 308 308 // Start server based on runtime 309 309 if (isBun) { 310 - // @ts-expect-error - Bun global 311 310 const bunServer = Bun.serve({ 312 311 port: LOOPBACK_PORT, 313 312 hostname: LOOPBACK_HOST,
+62
lexicons/site-get-domains-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.site.getDomains", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List domains currently mapped to a specific site.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["did", "rkey"], 11 + "properties": { 12 + "did": { 13 + "type": "string", 14 + "format": "did" 15 + }, 16 + "rkey": { 17 + "type": "string", 18 + "format": "record-key" 19 + } 20 + } 21 + }, 22 + "output": { 23 + "encoding": "application/json", 24 + "schema": { 25 + "type": "object", 26 + "required": ["domains"], 27 + "properties": { 28 + "domains": { 29 + "type": "array", 30 + "items": { 31 + "type": "ref", 32 + "ref": "#siteDomain" 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }, 39 + "siteDomain": { 40 + "type": "object", 41 + "required": ["domain", "kind", "status", "verified"], 42 + "properties": { 43 + "domain": { 44 + "type": "string", 45 + "minLength": 3, 46 + "maxLength": 253 47 + }, 48 + "kind": { 49 + "type": "string", 50 + "enum": ["wisp", "custom"] 51 + }, 52 + "status": { 53 + "type": "string", 54 + "enum": ["pendingVerification", "verified"] 55 + }, 56 + "verified": { 57 + "type": "boolean" 58 + } 59 + } 60 + } 61 + } 62 + }
+4
packages/@wispplace/lexicons/package.json
··· 58 58 "types": "./src/types/place/wisp/v2/site/delete.ts", 59 59 "default": "./src/types/place/wisp/v2/site/delete.ts" 60 60 }, 61 + "./types/place/wisp/v2/site/getDomains": { 62 + "types": "./src/types/place/wisp/v2/site/getDomains.ts", 63 + "default": "./src/types/place/wisp/v2/site/getDomains.ts" 64 + }, 61 65 "./types/place/wisp/v2/site/getList": { 62 66 "types": "./src/types/place/wisp/v2/site/getList.ts", 63 67 "default": "./src/types/place/wisp/v2/site/getList.ts"
+1
packages/@wispplace/lexicons/src/atcute/lexicons/index.ts
··· 9 9 export * as PlaceWispV2DomainGetStatus from "./types/place/wisp/v2/domain/getStatus.js"; 10 10 export * as PlaceWispV2Domains from "./types/place/wisp/v2/domains.js"; 11 11 export * as PlaceWispV2SiteDelete from "./types/place/wisp/v2/site/delete.js"; 12 + export * as PlaceWispV2SiteGetDomains from "./types/place/wisp/v2/site/getDomains.js"; 12 13 export * as PlaceWispV2SiteGetList from "./types/place/wisp/v2/site/getList.js"; 13 14 export * as PlaceWispV2Wh from "./types/place/wisp/v2/wh.js";
+53
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/site/getDomains.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.getDomains", { 6 + params: /*#__PURE__*/ v.object({ 7 + did: /*#__PURE__*/ v.didString(), 8 + rkey: /*#__PURE__*/ v.recordKeyString(), 9 + }), 10 + output: { 11 + type: "lex", 12 + schema: /*#__PURE__*/ v.object({ 13 + get domains() { 14 + return /*#__PURE__*/ v.array(siteDomainSchema); 15 + }, 16 + }), 17 + }, 18 + }); 19 + const _siteDomainSchema = /*#__PURE__*/ v.object({ 20 + $type: /*#__PURE__*/ v.optional( 21 + /*#__PURE__*/ v.literal("place.wisp.v2.site.getDomains#siteDomain"), 22 + ), 23 + /** 24 + * @minLength 3 25 + * @maxLength 253 26 + */ 27 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 28 + /*#__PURE__*/ v.stringLength(3, 253), 29 + ]), 30 + kind: /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 31 + status: /*#__PURE__*/ v.literalEnum(["pendingVerification", "verified"]), 32 + verified: /*#__PURE__*/ v.boolean(), 33 + }); 34 + 35 + type main$schematype = typeof _mainSchema; 36 + type siteDomain$schematype = typeof _siteDomainSchema; 37 + 38 + export interface mainSchema extends main$schematype {} 39 + export interface siteDomainSchema extends siteDomain$schematype {} 40 + 41 + export const mainSchema = _mainSchema as mainSchema; 42 + export const siteDomainSchema = _siteDomainSchema as siteDomainSchema; 43 + 44 + export interface SiteDomain extends v.InferInput<typeof siteDomainSchema> {} 45 + 46 + export interface $params extends v.InferInput<mainSchema["params"]> {} 47 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 48 + 49 + declare module "@atcute/lexicons/ambient" { 50 + interface XRPCQueries { 51 + "place.wisp.v2.site.getDomains": mainSchema; 52 + } 53 + }
+13
packages/@wispplace/lexicons/src/index.ts
··· 17 17 import * as PlaceWispV2DomainGetList from './types/place/wisp/v2/domain/getList.js' 18 18 import * as PlaceWispV2DomainGetStatus from './types/place/wisp/v2/domain/getStatus.js' 19 19 import * as PlaceWispV2SiteDelete from './types/place/wisp/v2/site/delete.js' 20 + import * as PlaceWispV2SiteGetDomains from './types/place/wisp/v2/site/getDomains.js' 20 21 import * as PlaceWispV2SiteGetList from './types/place/wisp/v2/site/getList.js' 21 22 22 23 export function createServer(options?: XrpcOptions): Server { ··· 161 162 >, 162 163 ) { 163 164 const nsid = 'place.wisp.v2.site.delete' // @ts-ignore 165 + return this._server.xrpc.method(nsid, cfg) 166 + } 167 + 168 + getDomains<A extends Auth = void>( 169 + cfg: MethodConfigOrHandler< 170 + A, 171 + PlaceWispV2SiteGetDomains.QueryParams, 172 + PlaceWispV2SiteGetDomains.HandlerInput, 173 + PlaceWispV2SiteGetDomains.HandlerOutput 174 + >, 175 + ) { 176 + const nsid = 'place.wisp.v2.site.getDomains' // @ts-ignore 164 177 return this._server.xrpc.method(nsid, cfg) 165 178 } 166 179
+63
packages/@wispplace/lexicons/src/lexicons.ts
··· 833 833 }, 834 834 }, 835 835 }, 836 + PlaceWispV2SiteGetDomains: { 837 + lexicon: 1, 838 + id: 'place.wisp.v2.site.getDomains', 839 + defs: { 840 + main: { 841 + type: 'query', 842 + description: 'List domains currently mapped to a specific site.', 843 + parameters: { 844 + type: 'params', 845 + required: ['did', 'rkey'], 846 + properties: { 847 + did: { 848 + type: 'string', 849 + format: 'did', 850 + }, 851 + rkey: { 852 + type: 'string', 853 + format: 'record-key', 854 + }, 855 + }, 856 + }, 857 + output: { 858 + encoding: 'application/json', 859 + schema: { 860 + type: 'object', 861 + required: ['domains'], 862 + properties: { 863 + domains: { 864 + type: 'array', 865 + items: { 866 + type: 'ref', 867 + ref: 'lex:place.wisp.v2.site.getDomains#siteDomain', 868 + }, 869 + }, 870 + }, 871 + }, 872 + }, 873 + }, 874 + siteDomain: { 875 + type: 'object', 876 + required: ['domain', 'kind', 'status', 'verified'], 877 + properties: { 878 + domain: { 879 + type: 'string', 880 + minLength: 3, 881 + maxLength: 253, 882 + }, 883 + kind: { 884 + type: 'string', 885 + enum: ['wisp', 'custom'], 886 + }, 887 + status: { 888 + type: 'string', 889 + enum: ['pendingVerification', 'verified'], 890 + }, 891 + verified: { 892 + type: 'boolean', 893 + }, 894 + }, 895 + }, 896 + }, 897 + }, 836 898 PlaceWispV2SiteGetList: { 837 899 lexicon: 1, 838 900 id: 'place.wisp.v2.site.getList', ··· 1143 1205 PlaceWispFs: 'place.wisp.fs', 1144 1206 PlaceWispSettings: 'place.wisp.settings', 1145 1207 PlaceWispV2SiteDelete: 'place.wisp.v2.site.delete', 1208 + PlaceWispV2SiteGetDomains: 'place.wisp.v2.site.getDomains', 1146 1209 PlaceWispV2SiteGetList: 'place.wisp.v2.site.getList', 1147 1210 PlaceWispSubfs: 'place.wisp.subfs', 1148 1211 PlaceWispV2Wh: 'place.wisp.v2.wh',
+58
packages/@wispplace/lexicons/src/types/place/wisp/v2/site/getDomains.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.getDomains' 16 + 17 + export type QueryParams = { 18 + did: string 19 + rkey: string 20 + } 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + domains: SiteDomain[] 25 + } 26 + 27 + export type HandlerInput = void 28 + 29 + export interface HandlerSuccess { 30 + encoding: 'application/json' 31 + body: OutputSchema 32 + headers?: { [key: string]: string } 33 + } 34 + 35 + export interface HandlerError { 36 + status: number 37 + message?: string 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess 41 + 42 + export interface SiteDomain { 43 + $type?: 'place.wisp.v2.site.getDomains#siteDomain' 44 + domain: string 45 + kind: 'wisp' | 'custom' 46 + status: 'pendingVerification' | 'verified' 47 + verified: boolean 48 + } 49 + 50 + const hashSiteDomain = 'siteDomain' 51 + 52 + export function isSiteDomain<V>(v: V) { 53 + return is$typed(v, id, hashSiteDomain) 54 + } 55 + 56 + export function validateSiteDomain<V>(v: V) { 57 + return validate<SiteDomain & V>(v, id, hashSiteDomain) 58 + }