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

initial XRPC work

+2072 -139
+47
README.md
··· 55 55 LOCAL_DEV=true 56 56 ``` 57 57 58 + ### Local Domain XRPC (ServiceAuth via PDS Proxy) 59 + 60 + `apps/main-app` exposes domain claim/status XRPC endpoints: 61 + 62 + - `place.wisp.v2.domain.claim` (procedure / POST) 63 + - `place.wisp.v2.domain.getStatus` (query / GET) 64 + 65 + The server validates **serviceAuth JWTs** (not cookie auth, not direct end-user access JWTs) on `/xrpc/*`. 66 + 67 + Set these env vars in your active `.env`: 68 + 69 + ```env 70 + LOCAL_DEV=true 71 + SERVICE_DID=did:web:regentsmacbookair 72 + SERVICE_IDS="#wisp_xrpc" 73 + SERVICE_ENDPOINT=https://regentsmacbookair 74 + ``` 75 + 76 + Notes: 77 + 78 + - `/.well-known/atproto-did` returns `SERVICE_DID`. 79 + - `/.well-known/did.json` publishes the DID doc, including `service` entries from `SERVICE_IDS`. 80 + - `SERVICE_IDS` must be quoted when it starts with `#` (otherwise dotenv treats it as a comment). 81 + - Service identity keys are stored in `service_identity_keys` in Postgres. 82 + If `SERVICE_PUBLIC_KEY_MULTIBASE` and `SERVICE_PRIVATE_KEY_MULTIBASE` are not set, a keypair is generated once and persisted. 83 + 84 + ### Local TLS Requirement (No Auto Cert Generation) 85 + 86 + Some PDS proxy flows require HTTPS on `:443` for the proxied service endpoint. 87 + Cert generation is intentionally manual so SANs are explicit and correct for your environment. 88 + 89 + Example with `mkcert`: 90 + 91 + ```bash 92 + mkcert -cert-file certs/dev-cert.pem -key-file certs/dev-key.pem regentsmacbookair localhost 100.64.0.2 93 + ``` 94 + 95 + Use SANs that match exactly what your PDS will call (hostname and/or IP). 96 + `apps/main-app` can terminate TLS directly in local dev with: 97 + 98 + ```env 99 + PORT=443 100 + LOCAL_DEV_TLS=true 101 + LOCAL_TLS_CERT_PATH=./certs/dev-cert.pem 102 + LOCAL_TLS_KEY_PATH=./certs/dev-key.pem 103 + ``` 104 + 58 105 59 106 ```bash 60 107 # Hosting service
+6 -1
apps/main-app/package.json
··· 12 12 "screenshot": "bun run scripts/screenshot-sites.ts" 13 13 }, 14 14 "dependencies": { 15 + "@atcute/crypto": "^2.3.0", 16 + "@atcute/identity": "^1.1.3", 17 + "@atcute/identity-resolver": "^1.2.2", 18 + "@atcute/oauth-crypto": "^0.1.0", 19 + "@atcute/xrpc-server": "^0.1.10", 15 20 "@atproto-labs/did-resolver": "^0.2.4", 16 21 "@atproto/api": "^0.17.7", 17 22 "@atproto/common-web": "^0.4.6", ··· 32 37 "@radix-ui/react-tabs": "^1.1.13", 33 38 "@tanstack/react-query": "^5.90.2", 34 39 "@wispplace/atproto-utils": "workspace:*", 35 - "@wispplace/css": "workspace:*", 36 40 "@wispplace/constants": "workspace:*", 41 + "@wispplace/css": "workspace:*", 37 42 "@wispplace/database": "workspace:*", 38 43 "@wispplace/fs-utils": "workspace:*", 39 44 "@wispplace/lexicons": "workspace:*",
+123 -12
apps/main-app/src/index.ts
··· 1 1 // Fix for Elysia issue with Bun, (see https://github.com/oven-sh/bun/issues/12161) 2 2 process.getBuiltinModule = require; 3 3 4 + import { existsSync } from 'node:fs' 5 + 4 6 import { Elysia, t } from 'elysia' 5 7 import type { Context } from 'elysia' 6 8 import { cors } from '@elysiajs/cors' ··· 16 18 rotateKeysIfNeeded 17 19 } from './lib/oauth-client' 18 20 import { getCookieSecret, closeDatabase } from './lib/db' 21 + import { ensureServiceIdentityKeypair } from './lib/service-identity' 19 22 import { authRoutes } from './routes/auth' 20 23 import { wispRoutes } from './routes/wisp' 21 24 import { domainRoutes } from './routes/domain' 22 25 import { userRoutes } from './routes/user' 23 26 import { siteRoutes } from './routes/site' 27 + import { xrpcRoutes } from './routes/xrpc' 24 28 import { csrfProtection } from './lib/csrf' 25 29 import { DNSVerificationWorker } from './lib/dns-verification-worker' 26 30 import { createLogger, logCollector, initializeGrafanaExporters } from '@wispplace/observability' ··· 35 39 }) 36 40 37 41 const logger = createLogger('main-app') 42 + const isLocalDev = Bun.env.LOCAL_DEV === 'true' 43 + const oauthAuthorizationServer = Bun.env.OAUTH_AUTHORIZATION_SERVER ?? 'https://atproto.wisp.place' 44 + const defaultServiceDid = isLocalDev ? 'did:web:localhost' : 'did:web:wisp.place' 45 + const serviceDid = Bun.env.SERVICE_DID ?? defaultServiceDid 46 + const parsedServiceIds = (Bun.env.SERVICE_IDS ?? '') 47 + .split(',') 48 + .map((id) => id.trim()) 49 + .filter((id) => id.length > 0 && id.startsWith('#')) 50 + const didServiceIds = parsedServiceIds.length > 0 51 + ? Array.from(new Set(parsedServiceIds)) 52 + : ['#wisp_xrpc'] 53 + const serverPort = Number(Bun.env.PORT ?? (isLocalDev ? '443' : '80')) 54 + const localTlsEnabled = isLocalDev && Bun.env.LOCAL_DEV_TLS !== 'false' 55 + const localTlsCertPath = Bun.env.LOCAL_TLS_CERT_PATH ?? './certs/dev-cert.pem' 56 + const localTlsKeyPath = Bun.env.LOCAL_TLS_KEY_PATH ?? './certs/dev-key.pem' 57 + 58 + logger.info('[Server] Local TLS config', { 59 + isLocalDev, 60 + localTlsEnabled, 61 + port: serverPort, 62 + certPath: localTlsEnabled ? localTlsCertPath : undefined, 63 + keyPath: localTlsEnabled ? localTlsKeyPath : undefined 64 + }) 38 65 39 66 const config: Config = { 40 67 domain: (Bun.env.DOMAIN ?? `https://${BASE_HOST}`) as Config['domain'], ··· 46 73 47 74 // Get or generate cookie signing secret 48 75 const cookieSecret = await getCookieSecret() 76 + const serviceIdentity = await ensureServiceIdentityKeypair( 77 + Bun.env.SERVICE_PUBLIC_KEY_MULTIBASE ?? null, 78 + Bun.env.SERVICE_PRIVATE_KEY_MULTIBASE ?? null 79 + ) 80 + const servicePublicKeyMultibase = serviceIdentity.publicKeyMultibase 49 81 50 82 const client = await getOAuthClient(config) 51 83 ··· 169 201 } 170 202 }) 171 203 .use(csrfProtection()) 172 - .get('/', async ({ set }) => { 204 + .get('/', async ({ request, set }) => { 173 205 // Build dynamic login URL for AT Protocol OAuth entryway 174 - const isLocalDev = Bun.env.LOCAL_DEV === 'true' 175 206 const loginUrl = isLocalDev 176 - ? 'http://127.0.0.1:8000/api/auth/login' 207 + ? `${new URL(request.url).origin}/api/auth/login` 177 208 : `${config.domain}/api/auth/login` 178 209 const atprotoLoginUrl = `https://atproto.wisp.place/?next=${encodeURIComponent(loginUrl)}` 179 210 ··· 183 214 return html.replaceAll('{{ATPROTO_LOGIN_URL}}', atprotoLoginUrl) 184 215 }) 185 216 .use(authRoutes(client, cookieSecret)) 217 + .use(xrpcRoutes()) 186 218 .use(wispRoutes(client, cookieSecret)) 187 219 .use(domainRoutes(client, cookieSecret)) 188 220 .use(userRoutes(client, cookieSecret)) ··· 278 310 }) 279 311 return metadata 280 312 }) 313 + .get('/.well-known/oauth-protected-resource', ({ request }) => { 314 + const resource = new URL(request.url).origin 315 + 316 + return { 317 + resource, 318 + authorization_servers: [oauthAuthorizationServer], 319 + bearer_methods_supported: ['header'], 320 + } 321 + }) 322 + .get('/.well-known/did.json', ({ request, set }) => { 323 + set.headers['Content-Type'] = 'application/did+json' 324 + 325 + const serviceEndpoint = Bun.env.SERVICE_ENDPOINT ?? new URL(request.url).origin 326 + const contexts = ['https://www.w3.org/ns/did/v1'] 327 + if (servicePublicKeyMultibase) { 328 + contexts.push('https://w3id.org/security/multikey/v1') 329 + } 330 + 331 + const services = didServiceIds.map((id) => ({ 332 + id, 333 + type: 'AtprotoService', 334 + serviceEndpoint 335 + })) 336 + 337 + const verificationMethod = servicePublicKeyMultibase 338 + ? [ 339 + { 340 + id: `${serviceDid}#atproto`, 341 + type: 'Multikey', 342 + controller: serviceDid, 343 + publicKeyMultibase: servicePublicKeyMultibase 344 + } 345 + ] 346 + : undefined 347 + 348 + return { 349 + '@context': contexts, 350 + id: serviceDid, 351 + verificationMethod, 352 + service: services 353 + } 354 + }) 281 355 .get('/jwks.json', async ({ set }) => { 282 356 // Prevent caching to ensure clients always get fresh keys after rotation 283 357 set.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' ··· 335 409 .get('/.well-known/atproto-did', ({ set }) => { 336 410 // Return plain text DID for AT Protocol domain verification 337 411 set.headers['Content-Type'] = 'text/plain' 338 - return 'did:plc:7puq73yz2hkvbcpdhnsze2qw' 412 + return serviceDid 339 413 }) 340 414 .use(cors({ 341 - origin: config.domain, 415 + origin: isLocalDev 416 + ? [ 417 + config.domain, 418 + /^http:\/\/127\.0\.0\.1:\d+$/, 419 + /^http:\/\/localhost:\d+$/, 420 + /^https:\/\/127\.0\.0\.1:\d+$/, 421 + /^https:\/\/localhost:\d+$/ 422 + ] 423 + : config.domain, 342 424 credentials: true, 343 425 methods: ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'OPTIONS'], 344 - allowedHeaders: ['Content-Type', 'Authorization', 'Origin', 'X-Forwarded-Host'], 345 - exposeHeaders: ['Content-Type'], 426 + allowedHeaders: [ 427 + 'Content-Type', 428 + 'Authorization', 429 + 'Origin', 430 + 'X-Forwarded-Host', 431 + 'DPoP', 432 + 'dpop', 433 + 'DPoP-Nonce', 434 + 'dpop-nonce' 435 + ], 436 + exposeHeaders: ['Content-Type', 'DPoP-Nonce', 'dpop-nonce'], 346 437 maxAge: 86400 // 24 hours 347 438 })) 348 - .listen({ 349 - port: 8000, 350 - hostname: '0.0.0.0' 351 - }) 439 + .listen( 440 + localTlsEnabled 441 + ? (() => { 442 + if (!existsSync(localTlsCertPath)) { 443 + throw new Error(`LOCAL_DEV TLS cert not found at ${localTlsCertPath}`) 444 + } 445 + if (!existsSync(localTlsKeyPath)) { 446 + throw new Error(`LOCAL_DEV TLS key not found at ${localTlsKeyPath}`) 447 + } 448 + 449 + return { 450 + port: serverPort, 451 + hostname: '0.0.0.0', 452 + tls: { 453 + cert: Bun.file(localTlsCertPath), 454 + key: Bun.file(localTlsKeyPath) 455 + } 456 + } 457 + })() 458 + : { 459 + port: serverPort, 460 + hostname: '0.0.0.0' 461 + } 462 + ) 352 463 353 464 console.log( 354 - `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` 465 + `🦊 Elysia is running at ${localTlsEnabled ? 'https' : 'http'}://${app.server?.hostname}:${app.server?.port}` 355 466 ) 356 467 357 468 // Graceful shutdown
+62 -29
apps/main-app/src/lib/db.ts
··· 1 1 import { SQL } from "bun"; 2 - import { BASE_HOST } from "@wispplace/constants"; 2 + import { isValidHandle, toDomain } from "./domain-utils"; 3 + 4 + export { isValidHandle, toDomain } from "./domain-utils"; 3 5 4 6 export const db = new SQL( 5 7 process.env.NODE_ENV === 'production' ··· 43 45 ) 44 46 `; 45 47 48 + // Service identity keys for did.json verificationMethod 49 + await db` 50 + CREATE TABLE IF NOT EXISTS service_identity_keys ( 51 + id TEXT PRIMARY KEY DEFAULT 'default', 52 + public_key_multibase TEXT NOT NULL, 53 + private_key_multibase TEXT, 54 + created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()), 55 + updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) 56 + ) 57 + `; 58 + 46 59 // Domains table maps subdomain -> DID (now supports up to 3 domains per user) 47 60 await db` 48 61 CREATE TABLE IF NOT EXISTS domains ( ··· 74 87 75 88 try { 76 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`; 77 102 } catch (err) { 78 103 // Column might already exist, ignore 79 104 } ··· 240 265 } 241 266 }) 242 267 ]); 243 - 244 - const RESERVED_HANDLES = new Set([ 245 - "www", 246 - "api", 247 - "admin", 248 - "static", 249 - "public", 250 - "preview", 251 - "slingshot", 252 - "plc", 253 - "constellation", 254 - "cdn", 255 - "pds", 256 - "staging", 257 - "auth" 258 - ]); 259 - 260 - export const isValidHandle = (handle: string): boolean => { 261 - const h = handle.trim().toLowerCase(); 262 - if (h.length < 3 || h.length > 63) return false; 263 - if (!/^[a-z0-9-]+$/.test(h)) return false; 264 - if (h.startsWith('-') || h.endsWith('-')) return false; 265 - if (h.includes('--')) return false; 266 - if (RESERVED_HANDLES.has(h)) return false; 267 - return true; 268 - }; 269 - 270 - export const toDomain = (handle: string): string => `${handle.toLowerCase()}.${BASE_HOST}`; 271 268 272 269 export const getDomainByDid = async (did: string): Promise<string | null> => { 273 270 const rows = await db`SELECT domain FROM domains WHERE did = ${did} ORDER BY created_at ASC LIMIT 1`; ··· 606 603 607 604 console.log('[CookieSecret] Generated new cookie signing secret'); 608 605 return secret; 606 + }; 607 + 608 + export const getServiceIdentityKeypair = async (): Promise<{ 609 + publicKeyMultibase: string; 610 + privateKeyMultibase: string | null; 611 + } | null> => { 612 + const rows = await db` 613 + SELECT public_key_multibase, private_key_multibase 614 + FROM service_identity_keys 615 + WHERE id = 'default' 616 + LIMIT 1 617 + `; 618 + 619 + if (rows.length === 0) { 620 + return null; 621 + } 622 + 623 + return { 624 + publicKeyMultibase: rows[0].public_key_multibase as string, 625 + privateKeyMultibase: (rows[0].private_key_multibase as string | undefined) ?? null, 626 + }; 627 + }; 628 + 629 + export const setServiceIdentityKeypair = async ( 630 + publicKeyMultibase: string, 631 + privateKeyMultibase: string | null 632 + ): Promise<void> => { 633 + await db` 634 + INSERT INTO service_identity_keys (id, public_key_multibase, private_key_multibase, created_at, updated_at) 635 + VALUES ('default', ${publicKeyMultibase}, ${privateKeyMultibase}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW())) 636 + ON CONFLICT (id) 637 + DO UPDATE SET 638 + public_key_multibase = EXCLUDED.public_key_multibase, 639 + private_key_multibase = EXCLUDED.private_key_multibase, 640 + updated_at = EXTRACT(EPOCH FROM NOW()) 641 + `; 609 642 }; 610 643 611 644 // Supporter management functions
+111
apps/main-app/src/lib/domain-utils.ts
··· 1 + import { BASE_HOST } from '@wispplace/constants' 2 + 3 + const RESERVED_HANDLES = new Set([ 4 + 'www', 5 + 'api', 6 + 'admin', 7 + 'static', 8 + 'public', 9 + 'preview', 10 + 'slingshot', 11 + 'plc', 12 + 'constellation', 13 + 'cdn', 14 + 'pds', 15 + 'staging', 16 + 'auth' 17 + ]) 18 + 19 + const RESERVED_CUSTOM_DOMAINS = new Set([ 20 + 'localhost', 21 + 'example.com', 22 + 'example.org', 23 + 'example.net', 24 + 'test', 25 + 'invalid', 26 + 'local' 27 + ]) 28 + 29 + const RESERVED_CUSTOM_PATTERNS = [ 30 + /^(?:10|127|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\./, 31 + /^(?:\d{1,3}\.){3}\d{1,3}$/ 32 + ] 33 + 34 + const CUSTOM_DOMAIN_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/ 35 + 36 + export const normalizeDomain = (domain: string): string => { 37 + return domain.trim().toLowerCase().replace(/\.$/, '') 38 + } 39 + 40 + export const isValidHandle = (handle: string): boolean => { 41 + const normalized = handle.trim().toLowerCase() 42 + 43 + if (normalized.length < 3 || normalized.length > 63) return false 44 + if (!/^[a-z0-9-]+$/.test(normalized)) return false 45 + if (normalized.startsWith('-') || normalized.endsWith('-')) return false 46 + if (normalized.includes('--')) return false 47 + if (RESERVED_HANDLES.has(normalized)) return false 48 + 49 + return true 50 + } 51 + 52 + export const toDomain = (handle: string): string => `${handle.toLowerCase()}.${BASE_HOST}` 53 + 54 + export const extractWispHandle = (domain: string): string | null => { 55 + const normalized = normalizeDomain(domain) 56 + const suffix = `.${BASE_HOST}` 57 + 58 + if (!normalized.endsWith(suffix)) { 59 + return null 60 + } 61 + 62 + const handle = normalized.slice(0, -suffix.length) 63 + if (handle.length === 0 || handle.includes('.')) { 64 + return null 65 + } 66 + 67 + return handle 68 + } 69 + 70 + export const validateCustomDomain = (domain: string): string | null => { 71 + const normalized = normalizeDomain(domain) 72 + 73 + if (normalized.length < 3 || normalized.length > 253) { 74 + return 'domain must be 3-253 characters' 75 + } 76 + 77 + if (!CUSTOM_DOMAIN_PATTERN.test(normalized)) { 78 + return 'invalid domain format' 79 + } 80 + 81 + const labels = normalized.split('.') 82 + for (const label of labels) { 83 + if (label.length === 0 || label.length > 63) { 84 + return 'domain labels must be 1-63 characters' 85 + } 86 + if (label.startsWith('-') || label.endsWith('-')) { 87 + return 'domain labels cannot start or end with hyphen' 88 + } 89 + } 90 + 91 + const tld = labels[labels.length - 1] 92 + if (tld.length < 2 || /^\d+$/.test(tld)) { 93 + return 'tld must be at least 2 characters and not all numeric' 94 + } 95 + 96 + if (!/^[a-z0-9.-]+$/.test(normalized)) { 97 + return 'domain must use ascii alphanumeric characters, dots, and hyphens' 98 + } 99 + 100 + if (RESERVED_CUSTOM_DOMAINS.has(normalized)) { 101 + return 'reserved or blocked domain' 102 + } 103 + 104 + for (const pattern of RESERVED_CUSTOM_PATTERNS) { 105 + if (pattern.test(normalized)) { 106 + return 'ip addresses are not allowed' 107 + } 108 + } 109 + 110 + return null 111 + }
+72
apps/main-app/src/lib/service-identity.ts
··· 1 + import { P256PrivateKeyExportable } from '@atcute/crypto' 2 + 3 + import { createLogger } from '@wispplace/observability' 4 + 5 + import { getServiceIdentityKeypair, setServiceIdentityKeypair } from './db' 6 + 7 + const logger = createLogger('main-app') 8 + 9 + const validateMultikey = (value: string): string => { 10 + const trimmed = value.trim() 11 + if (!trimmed.startsWith('z')) { 12 + throw new Error('service key must be a multibase base58btc key') 13 + } 14 + 15 + return trimmed 16 + } 17 + 18 + export interface ServiceIdentityKeypair { 19 + publicKeyMultibase: string 20 + privateKeyMultibase: string 21 + } 22 + 23 + const generateAndPersistKeypair = async (): Promise<ServiceIdentityKeypair> => { 24 + const keypair = await P256PrivateKeyExportable.createKeypair() 25 + const publicKeyMultibase = validateMultikey(await keypair.exportPublicKey('multikey')) 26 + const privateKeyMultibase = validateMultikey(await keypair.exportPrivateKey('multikey')) 27 + 28 + await setServiceIdentityKeypair(publicKeyMultibase, privateKeyMultibase) 29 + logger.info('Generated new service identity keypair') 30 + 31 + return { 32 + publicKeyMultibase, 33 + privateKeyMultibase, 34 + } 35 + } 36 + 37 + export const ensureServiceIdentityKeypair = async ( 38 + configuredPublic?: string | null, 39 + configuredPrivate?: string | null, 40 + ): Promise<ServiceIdentityKeypair> => { 41 + const explicitPublic = configuredPublic ? validateMultikey(configuredPublic) : null 42 + const explicitPrivate = configuredPrivate ? validateMultikey(configuredPrivate) : null 43 + 44 + if ((explicitPublic && !explicitPrivate) || (!explicitPublic && explicitPrivate)) { 45 + throw new Error('both SERVICE_PUBLIC_KEY_MULTIBASE and SERVICE_PRIVATE_KEY_MULTIBASE must be set together') 46 + } 47 + 48 + if (explicitPublic && explicitPrivate) { 49 + await setServiceIdentityKeypair(explicitPublic, explicitPrivate) 50 + logger.info('Updated service identity keypair from environment') 51 + 52 + return { 53 + publicKeyMultibase: explicitPublic, 54 + privateKeyMultibase: explicitPrivate, 55 + } 56 + } 57 + 58 + const existing = await getServiceIdentityKeypair() 59 + if (!existing) { 60 + return generateAndPersistKeypair() 61 + } 62 + 63 + if (!existing.privateKeyMultibase) { 64 + logger.warn('Service identity record missing private key; generating replacement keypair') 65 + return generateAndPersistKeypair() 66 + } 67 + 68 + return { 69 + publicKeyMultibase: existing.publicKeyMultibase, 70 + privateKeyMultibase: existing.privateKeyMultibase, 71 + } 72 + }
+18 -74
apps/main-app/src/routes/domain.ts
··· 7 7 getDomainByDid, 8 8 isDomainAvailable, 9 9 isDomainRegistered, 10 - isValidHandle, 11 - toDomain, 12 10 updateDomain, 13 11 countWispDomains, 14 12 deleteWispDomain, ··· 20 18 updateWispDomainSite, 21 19 updateCustomDomainRkey 22 20 } from '../lib/db' 21 + import { 22 + extractWispHandle, 23 + isValidHandle, 24 + normalizeDomain, 25 + toDomain, 26 + validateCustomDomain 27 + } from '../lib/domain-utils' 23 28 import { createHash } from 'crypto' 24 29 import { verifyCustomDomain } from '../lib/dns-verify' 25 30 import { createLogger } from '@wispplace/observability' ··· 73 78 */ 74 79 .get('/registered', async ({ query, set }) => { 75 80 try { 76 - const domain = (query.domain || "").trim().toLowerCase(); 81 + const domain = normalizeDomain(String(query.domain || '')) 77 82 78 83 if (!domain) { 79 84 set.status = 400; ··· 206 211 .post('/custom/add', async ({ body, auth, set }) => { 207 212 try { 208 213 const { domain } = body as { domain: string }; 209 - const domainLower = domain.toLowerCase().trim(); 210 - 211 - // Enhanced domain validation 212 - // 1. Length check (RFC 1035: labels 1-63 chars, total max 253) 213 - if (!domainLower || domainLower.length < 3 || domainLower.length > 253) { 214 - set.status = 400 215 - throw new Error('Invalid domain: must be 3-253 characters'); 216 - } 217 - 218 - // 2. Basic format validation 219 - // - Must contain at least one dot (require TLD) 220 - // - Valid characters: a-z, 0-9, hyphen, dot 221 - // - No consecutive dots, no leading/trailing dots or hyphens 222 - const domainPattern = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/; 223 - if (!domainPattern.test(domainLower)) { 224 - set.status = 400 225 - throw new Error('Invalid domain format'); 226 - } 227 - 228 - // 3. Validate each label (part between dots) 229 - const labels = domainLower.split('.'); 230 - for (const label of labels) { 231 - if (label.length === 0 || label.length > 63) { 232 - set.status = 400 233 - throw new Error('Invalid domain: label length must be 1-63 characters'); 234 - } 235 - if (label.startsWith('-') || label.endsWith('-')) { 236 - set.status = 400 237 - throw new Error('Invalid domain: labels cannot start or end with hyphen'); 238 - } 239 - } 240 - 241 - // 4. TLD validation (require valid TLD, block single-char TLDs and numeric TLDs) 242 - const tld = labels[labels.length - 1]; 243 - if (tld.length < 2 || /^\d+$/.test(tld)) { 244 - set.status = 400 245 - throw new Error('Invalid domain: TLD must be at least 2 characters and not all numeric'); 246 - } 247 - 248 - // 5. Homograph attack protection - block domains with mixed scripts or confusables 249 - // Block non-ASCII characters (Punycode domains should be pre-converted) 250 - if (!/^[a-z0-9.-]+$/.test(domainLower)) { 251 - set.status = 400 252 - throw new Error('Invalid domain: only ASCII alphanumeric, dots, and hyphens allowed'); 253 - } 254 - 255 - // 6. Block localhost, internal IPs, and reserved domains 256 - const blockedDomains = [ 257 - 'localhost', 258 - 'example.com', 259 - 'example.org', 260 - 'example.net', 261 - 'test', 262 - 'invalid', 263 - 'local' 264 - ]; 265 - const blockedPatterns = [ 266 - /^(?:10|127|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\./, // Private IPs 267 - /^(?:\d{1,3}\.){3}\d{1,3}$/, // Any IP address 268 - ]; 214 + const domainLower = normalizeDomain(domain || '') 269 215 270 - if (blockedDomains.includes(domainLower)) { 216 + const domainError = validateCustomDomain(domainLower) 217 + if (domainError) { 271 218 set.status = 400 272 - throw new Error('Invalid domain: reserved or blocked domain'); 273 - } 274 - 275 - for (const pattern of blockedPatterns) { 276 - if (pattern.test(domainLower)) { 277 - set.status = 400 278 - throw new Error('Invalid domain: IP addresses not allowed'); 279 - } 219 + throw new Error(`Invalid domain: ${domainError}`) 280 220 } 281 221 282 222 // Check if already exists and is verified ··· 396 336 const { domain } = params; 397 337 398 338 // Verify domain belongs to user 399 - const domainLower = domain.toLowerCase().trim(); 339 + const domainLower = normalizeDomain(domain) 400 340 const info = await isDomainRegistered(domainLower); 401 341 402 342 if (!info.registered || info.type !== 'wisp') { ··· 414 354 415 355 // Delete from PDS 416 356 const agent = new Agent((url, init) => auth.session.fetchHandler(url, init)); 417 - const handle = domainLower.replace(`.${process.env.BASE_DOMAIN || 'wisp.place'}`, ''); 357 + const handle = extractWispHandle(domainLower); 358 + if (!handle) { 359 + set.status = 400 360 + throw new Error('Invalid wisp domain'); 361 + } 418 362 try { 419 363 await agent.com.atproto.repo.deleteRecord({ 420 364 repo: auth.did,
+453
apps/main-app/src/routes/xrpc.ts
··· 1 + import { createHash } from 'crypto'; 2 + 3 + import { Elysia } from 'elysia'; 4 + 5 + import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver } from '@atcute/identity-resolver'; 6 + import { json, XRPCRouter, XRPCError } from '@atcute/xrpc-server'; 7 + import { ServiceJwtVerifier } from '@atcute/xrpc-server/auth'; 8 + import { PlaceWispV2DomainClaim, PlaceWispV2DomainGetStatus } from '@wispplace/lexicons/atcute'; 9 + import { BASE_HOST } from '@wispplace/constants'; 10 + 11 + import { createLogger } from '@wispplace/observability'; 12 + 13 + import { 14 + claimCustomDomain, 15 + claimDomain, 16 + getCustomDomainInfo, 17 + isDomainRegistered, 18 + updateCustomDomainRkey, 19 + updateWispDomainSite, 20 + } from '../lib/db'; 21 + import { 22 + extractWispHandle, 23 + isValidHandle, 24 + normalizeDomain, 25 + validateCustomDomain, 26 + } from '../lib/domain-utils'; 27 + 28 + const logger = createLogger('main-app'); 29 + const isLocalDev = Bun.env.LOCAL_DEV === 'true'; 30 + 31 + interface XrpcAuthContext { 32 + did: string; 33 + } 34 + 35 + const DEFAULT_SERVICE_DID = isLocalDev ? 'did:web:localhost' : 'did:web:wisp.place'; 36 + const SERVICE_DID = Bun.env.SERVICE_DID ?? DEFAULT_SERVICE_DID; 37 + const serviceJwtVerifier = new ServiceJwtVerifier({ 38 + serviceDid: SERVICE_DID as any, 39 + resolver: new CompositeDidDocumentResolver({ 40 + methods: { 41 + plc: new PlcDidDocumentResolver(), 42 + web: new WebDidDocumentResolver(), 43 + }, 44 + }), 45 + }); 46 + 47 + const NSID_ALIASES: Record<string, string> = { 48 + 'place.wisp.v2.domain.getstatus': 'place.wisp.v2.domain.getStatus', 49 + 'place.wisp.v2.domain.get-status': 'place.wisp.v2.domain.getStatus', 50 + }; 51 + 52 + const toIsoFromEpoch = (epoch: unknown): string | undefined => { 53 + let numeric: number | undefined; 54 + 55 + if (typeof epoch === 'number') { 56 + numeric = epoch; 57 + } else if (typeof epoch === 'string') { 58 + numeric = Number(epoch); 59 + } 60 + 61 + if (!Number.isFinite(numeric)) { 62 + return undefined; 63 + } 64 + 65 + const ms = numeric! < 1_000_000_000_000 ? numeric! * 1000 : numeric!; 66 + const date = new Date(ms); 67 + 68 + if (Number.isNaN(date.getTime())) { 69 + return undefined; 70 + } 71 + 72 + return date.toISOString(); 73 + }; 74 + 75 + type DidString = `did:${string}:${string}`; 76 + 77 + const buildCustomDnsInstructions = (domain: string, did: DidString, challengeId: string) => { 78 + return { 79 + challengeId, 80 + txtName: `_wisp.${domain}`, 81 + txtValue: did, 82 + cnameTarget: `${challengeId}.dns.${BASE_HOST}`, 83 + }; 84 + }; 85 + 86 + const authRequired = (): never => { 87 + throw new XRPCError({ status: 401, error: 'AuthenticationRequired', description: 'authentication required' }); 88 + }; 89 + 90 + const invalidDomain = (description: string): never => { 91 + throw new XRPCError({ 92 + status: 400, 93 + error: 'InvalidDomain', 94 + description, 95 + }); 96 + }; 97 + 98 + const alreadyClaimed = (description: string): never => { 99 + throw new XRPCError({ 100 + status: 409, 101 + error: 'AlreadyClaimed', 102 + description, 103 + }); 104 + }; 105 + 106 + const domainLimitReached = (): never => { 107 + throw new XRPCError({ 108 + status: 400, 109 + error: 'DomainLimitReached', 110 + description: 'free tier users can claim up to 3 wisp subdomains', 111 + }); 112 + }; 113 + 114 + const requireAuthenticated = (auth: XrpcAuthContext | undefined): XrpcAuthContext => { 115 + if (!auth) { 116 + authRequired(); 117 + } 118 + 119 + return auth!; 120 + }; 121 + 122 + const authRequiredWith = (description: string): never => { 123 + throw new XRPCError({ status: 401, error: 'AuthenticationRequired', description }); 124 + }; 125 + 126 + const resolveServiceAuth = async (request: Request, nsid: string): Promise<XrpcAuthContext | undefined> => { 127 + const authorization = request.headers.get('authorization'); 128 + if (!authorization) { 129 + return undefined; 130 + } 131 + 132 + if (!authorization.startsWith('Bearer ')) { 133 + authRequiredWith('missing or invalid authorization header'); 134 + } 135 + 136 + const jwt = authorization.slice('Bearer '.length).trim(); 137 + if (!jwt) { 138 + authRequiredWith('missing service authorization token'); 139 + } 140 + 141 + const result = await serviceJwtVerifier.verify(jwt, { lxm: nsid as any }); 142 + if (!result.ok) { 143 + authRequiredWith(result.error.description); 144 + throw new Error('unreachable'); 145 + } 146 + 147 + return { did: result.value.issuer }; 148 + }; 149 + 150 + const serializeBodyForForwarding = (body: unknown): string | undefined => { 151 + if (body === undefined || body === null) { 152 + return undefined; 153 + } 154 + 155 + if (typeof body === 'string') { 156 + return body; 157 + } 158 + 159 + return JSON.stringify(body); 160 + }; 161 + 162 + const prepareXrpcRequest = async (request: Request, parsedBody: unknown): Promise<Request> => { 163 + if (request.method === 'GET' || request.method === 'HEAD') { 164 + return request; 165 + } 166 + 167 + const headers = new Headers(request.headers); 168 + headers.delete('content-length'); 169 + 170 + let bodyText: string | undefined; 171 + 172 + if (!request.bodyUsed) { 173 + try { 174 + bodyText = await request.text(); 175 + } catch { 176 + bodyText = undefined; 177 + } 178 + } 179 + 180 + if (bodyText === undefined) { 181 + bodyText = serializeBodyForForwarding(parsedBody); 182 + if (bodyText !== undefined && !headers.has('content-type')) { 183 + headers.set('content-type', 'application/json'); 184 + } 185 + } 186 + 187 + return new Request(request.url, { 188 + method: request.method, 189 + headers, 190 + body: bodyText, 191 + }); 192 + }; 193 + 194 + const normalizeNsidPath = (request: Request): { request: Request; rawNsid: string; nsid: string } => { 195 + const url = new URL(request.url); 196 + const rawNsid = url.pathname.startsWith('/xrpc/') ? url.pathname.slice('/xrpc/'.length) : url.pathname; 197 + const nsid = NSID_ALIASES[rawNsid] ?? rawNsid; 198 + 199 + if (nsid === rawNsid) { 200 + return { request, rawNsid, nsid }; 201 + } 202 + 203 + url.pathname = `/xrpc/${nsid}`; 204 + 205 + return { 206 + request: new Request(url.toString(), request), 207 + rawNsid, 208 + nsid, 209 + }; 210 + }; 211 + 212 + export const xrpcRoutes = () => { 213 + const authByRequest = new WeakMap<Request, XrpcAuthContext>(); 214 + const router = new XRPCRouter(); 215 + 216 + router.addQuery(PlaceWispV2DomainGetStatus.mainSchema, { 217 + async handler({ params, request }) { 218 + const domain = normalizeDomain(params.domain); 219 + const auth = authByRequest.get(request); 220 + 221 + if (domain.length === 0) { 222 + invalidDomain('domain parameter is required'); 223 + } 224 + 225 + const info = await isDomainRegistered(domain); 226 + if (!info.registered) { 227 + return json({ 228 + domain, 229 + status: 'unclaimed', 230 + }); 231 + } 232 + 233 + const kind = info.type; 234 + const ownedByCaller = auth ? auth.did === info.did : undefined; 235 + 236 + if (auth && ownedByCaller === false) { 237 + return json({ 238 + domain, 239 + kind, 240 + status: 'alreadyClaimed', 241 + verified: kind === 'custom' ? Boolean(info.verified) : true, 242 + siteRkey: info.rkey ?? undefined, 243 + }); 244 + } 245 + 246 + if (kind === 'custom') { 247 + const custom = await getCustomDomainInfo(domain); 248 + const verified = Boolean(custom?.verified); 249 + 250 + return json({ 251 + domain, 252 + kind, 253 + status: verified ? 'verified' : 'pendingVerification', 254 + verified, 255 + siteRkey: info.rkey ?? undefined, 256 + lastCheckedAt: toIsoFromEpoch(custom?.last_verified_at), 257 + }); 258 + } 259 + 260 + return json({ 261 + domain, 262 + kind, 263 + status: 'verified', 264 + verified: true, 265 + siteRkey: info.rkey ?? undefined, 266 + }); 267 + }, 268 + }); 269 + 270 + router.addProcedure(PlaceWispV2DomainClaim.mainSchema, { 271 + async handler({ input, request }) { 272 + const auth = requireAuthenticated(authByRequest.get(request)); 273 + const did = auth.did as DidString; 274 + 275 + const domain = normalizeDomain(input.domain); 276 + if (domain.length === 0) { 277 + invalidDomain('domain is required'); 278 + } 279 + 280 + const wispHandle = extractWispHandle(domain); 281 + if (wispHandle !== null) { 282 + if (!isValidHandle(wispHandle)) { 283 + invalidDomain('invalid wisp subdomain handle'); 284 + } 285 + 286 + const existing = await isDomainRegistered(domain); 287 + if (existing.registered && existing.did !== did) { 288 + alreadyClaimed('domain is already claimed'); 289 + } 290 + 291 + if (existing.registered && existing.did === did) { 292 + if (input.siteRkey !== undefined) { 293 + await updateWispDomainSite(domain, input.siteRkey); 294 + } 295 + 296 + return json({ 297 + domain, 298 + kind: 'wisp', 299 + status: 'alreadyClaimed', 300 + siteRkey: input.siteRkey ?? existing.rkey ?? undefined, 301 + }); 302 + } 303 + 304 + try { 305 + await claimDomain(did, wispHandle); 306 + } catch (err) { 307 + const message = err instanceof Error ? err.message : ''; 308 + 309 + if (message === 'domain_limit_reached') { 310 + domainLimitReached(); 311 + } 312 + if (message === 'invalid_handle') { 313 + invalidDomain('invalid wisp subdomain handle'); 314 + } 315 + 316 + alreadyClaimed('domain is already claimed'); 317 + } 318 + 319 + if (input.siteRkey !== undefined) { 320 + await updateWispDomainSite(domain, input.siteRkey); 321 + } 322 + 323 + return json({ 324 + domain, 325 + kind: 'wisp', 326 + status: 'verified', 327 + siteRkey: input.siteRkey, 328 + }); 329 + } 330 + 331 + const customError = validateCustomDomain(domain); 332 + if (customError !== null) { 333 + invalidDomain(customError); 334 + } 335 + 336 + const existing = await getCustomDomainInfo(domain); 337 + if (existing && existing.verified && existing.did !== did) { 338 + alreadyClaimed('domain already verified and owned by another user'); 339 + } 340 + 341 + if (existing && existing.did === did) { 342 + if (input.siteRkey !== undefined) { 343 + await updateCustomDomainRkey(existing.id, input.siteRkey); 344 + } 345 + 346 + const status = existing.verified ? 'verified' : 'pendingVerification'; 347 + 348 + return json({ 349 + domain, 350 + kind: 'custom', 351 + status, 352 + siteRkey: input.siteRkey ?? existing.rkey ?? undefined, 353 + ...buildCustomDnsInstructions(domain, did, existing.id), 354 + }); 355 + } 356 + 357 + const challengeId = createHash('sha256').update(`${did}:${domain}`).digest('hex').substring(0, 16); 358 + 359 + try { 360 + await claimCustomDomain(did, domain, challengeId, input.siteRkey ?? null); 361 + } catch (err) { 362 + alreadyClaimed('domain already verified and owned by another user'); 363 + } 364 + 365 + return json({ 366 + domain, 367 + kind: 'custom', 368 + status: 'pendingVerification', 369 + siteRkey: input.siteRkey, 370 + ...buildCustomDnsInstructions(domain, did, challengeId), 371 + }); 372 + }, 373 + }); 374 + 375 + return new Elysia().all('/xrpc/*', async ({ body, request }) => { 376 + const startedAt = Date.now(); 377 + let xrpcRequest: Request | undefined; 378 + let nsid = ''; 379 + let rawNsid = ''; 380 + let auth: XrpcAuthContext | undefined; 381 + 382 + try { 383 + const preparedRequest = await prepareXrpcRequest(request, body); 384 + const normalized = normalizeNsidPath(preparedRequest); 385 + xrpcRequest = normalized.request; 386 + rawNsid = normalized.rawNsid; 387 + nsid = normalized.nsid; 388 + 389 + const authorization = xrpcRequest.headers.get('authorization'); 390 + logger.info('[XRPC] Incoming request', { 391 + method: xrpcRequest.method, 392 + rawNsid, 393 + nsid, 394 + origin: xrpcRequest.headers.get('origin') ?? undefined, 395 + hasAuthorization: Boolean(authorization), 396 + authorizationScheme: authorization ? authorization.split(' ')[0] : undefined, 397 + }); 398 + 399 + auth = await resolveServiceAuth(xrpcRequest, nsid); 400 + if (auth) { 401 + authByRequest.set(xrpcRequest, auth); 402 + } 403 + 404 + const response = await router.fetch(xrpcRequest); 405 + 406 + if (!response.ok) { 407 + let responseData: unknown; 408 + try { 409 + responseData = await response.clone().json(); 410 + } catch { 411 + responseData = await response.clone().text(); 412 + } 413 + 414 + logger.warn('[XRPC] Request failed', { 415 + method: xrpcRequest.method, 416 + rawNsid, 417 + nsid, 418 + status: response.status, 419 + did: auth?.did, 420 + origin: xrpcRequest.headers.get('origin') ?? undefined, 421 + requestBodyUsed: request.bodyUsed, 422 + error: responseData, 423 + durationMs: Date.now() - startedAt, 424 + }); 425 + } else { 426 + logger.info('[XRPC] Request succeeded', { 427 + method: xrpcRequest.method, 428 + rawNsid, 429 + nsid, 430 + status: response.status, 431 + did: auth?.did, 432 + durationMs: Date.now() - startedAt, 433 + }); 434 + } 435 + 436 + return response; 437 + } catch (err) { 438 + logger.error('[XRPC] Handler error', { 439 + method: xrpcRequest?.method ?? request.method, 440 + rawNsid: rawNsid || undefined, 441 + nsid: nsid || undefined, 442 + origin: request.headers.get('origin') ?? undefined, 443 + durationMs: Date.now() - startedAt, 444 + error: err instanceof Error ? err.message : String(err), 445 + }); 446 + throw err; 447 + } finally { 448 + if (xrpcRequest) { 449 + authByRequest.delete(xrpcRequest); 450 + } 451 + } 452 + }); 453 + };
+86 -13
bun.lock
··· 78 78 "name": "@wispplace/main-app", 79 79 "version": "1.0.50", 80 80 "dependencies": { 81 + "@atcute/crypto": "^2.3.0", 82 + "@atcute/identity": "^1.1.3", 83 + "@atcute/identity-resolver": "^1.2.2", 84 + "@atcute/oauth-crypto": "^0.1.0", 85 + "@atcute/xrpc-server": "^0.1.10", 81 86 "@atproto-labs/did-resolver": "^0.2.4", 82 87 "@atproto/api": "^0.17.7", 83 88 "@atproto/common-web": "^0.4.6", ··· 225 230 "name": "@wispplace/lexicons", 226 231 "version": "1.0.0", 227 232 "dependencies": { 233 + "@atcute/lexicons": "^1.2.9", 228 234 "@atproto/lexicon": "^0.5.1", 229 235 "@atproto/xrpc-server": "^0.9.5", 230 236 }, 231 237 "devDependencies": { 238 + "@atcute/lex-cli": "^2.5.3", 232 239 "@atproto/lex-cli": "^0.9.5", 233 240 "multiformats": "^13.4.1", 234 241 }, ··· 303 310 304 311 "@atcute/bluesky": ["@atcute/bluesky@3.2.15", "", { "dependencies": { "@atcute/atproto": "^3.1.10", "@atcute/lexicons": "^1.2.6" } }, "sha512-H4RW3WffjfdKvOZ9issEUQnuSR4KfuAwwJnYu0fclA9VDa99JTJ+pa8tTl9lFeBV9DINtWJAx7rdIbICoVCstQ=="], 305 312 313 + "@atcute/car": ["@atcute/car@5.1.1", "", { "dependencies": { "@atcute/cbor": "^2.3.2", "@atcute/cid": "^2.4.1", "@atcute/uint8array": "^1.1.1", "@atcute/varint": "^2.0.0" } }, "sha512-MeRUJNXYgAHrJZw7mMoZJb9xIqv3LZLQw90rRRAVAo8SGNdICwyqe6Bf2LGesX73QM04MBuYO6Kqhvold3TFfg=="], 314 + 315 + "@atcute/cbor": ["@atcute/cbor@2.3.2", "", { "dependencies": { "@atcute/cid": "^2.4.1", "@atcute/multibase": "^1.1.8", "@atcute/uint8array": "^1.1.1" } }, "sha512-xP2SORSau/VVI00x2V4BjwIkHr6EQ7l/MXEOPaa4LGYtePFc4gnD4L1yN10dT5NEuUnvGEuCh6arLB7gz1smVQ=="], 316 + 317 + "@atcute/cid": ["@atcute/cid@2.4.1", "", { "dependencies": { "@atcute/multibase": "^1.1.8", "@atcute/uint8array": "^1.1.1" } }, "sha512-bwhna69RCv7yetXudtj+2qrMPYvhhIQqvJz6YUpUS98v7OdF3X2dnye9Nig2NDrklZcuyOsu7sQo7GOykJXRLQ=="], 318 + 306 319 "@atcute/client": ["@atcute/client@4.2.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.6" } }, "sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw=="], 320 + 321 + "@atcute/crypto": ["@atcute/crypto@2.3.0", "", { "dependencies": { "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.6", "@noble/secp256k1": "^3.0.0" } }, "sha512-w5pkJKCjbNMQu+F4JRHbR3ROQyhi1wbn+GSC6WDQamcYHkZmEZk1/eoI354bIQOOfkEM6aFLv718iskrkon4GQ=="], 307 322 308 323 "@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="], 309 324 310 325 "@atcute/identity-resolver": ["@atcute/identity-resolver@1.2.2", "", { "dependencies": { "@atcute/lexicons": "^1.2.6", "@atcute/util-fetch": "^1.0.5", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw=="], 311 326 312 - "@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 327 + "@atcute/lex-cli": ["@atcute/lex-cli@2.5.3", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/identity-resolver": "^1.2.2", "@atcute/lexicon-doc": "^2.1.0", "@atcute/lexicon-resolver": "^0.1.6", "@atcute/lexicons": "^1.2.7", "@badrap/valita": "^0.4.6", "@optique/core": "^0.6.10", "@optique/run": "^0.6.10", "picocolors": "^1.1.1", "prettier": "^3.7.4" }, "bin": { "lex-cli": "cli.mjs" } }, "sha512-829rvezMOfRkJQRKvupNT8TWT/YYffJ2QsB80D9aPjkXSogrETZA7xZcPaMZBXg+mJaVbLO9S4ThPQmlF0L4UQ=="], 328 + 329 + "@atcute/lexicon-doc": ["@atcute/lexicon-doc@2.1.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.9", "@atcute/uint8array": "^1.1.1", "@atcute/util-text": "^1.1.1", "@badrap/valita": "^0.4.6" } }, "sha512-DirteHRK0GPLFVsIkaxD2IxUQUtpMO3I/EM8gy+2HAn6nODfN85qDYMefE2oA/QvTa97SVkXaoFNzZkCshx7+g=="], 330 + 331 + "@atcute/lexicon-resolver": ["@atcute/lexicon-resolver@0.1.6", "", { "dependencies": { "@atcute/crypto": "^2.3.0", "@atcute/lexicon-doc": "^2.0.6", "@atcute/lexicons": "^1.2.6", "@atcute/repo": "^0.1.1", "@atcute/util-fetch": "^1.0.5", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.1.0", "@atcute/identity-resolver": "^1.1.3" } }, "sha512-wJC/ChmpP7k+ywpOd07CMvioXjIGaFpF3bDwXLi/086LYjSWHOvtW6pyC+mqP5wLhjyH2hn4wmi77Buew1l1aw=="], 332 + 333 + "@atcute/lexicons": ["@atcute/lexicons@1.2.9", "", { "dependencies": { "@atcute/uint8array": "^1.1.1", "@atcute/util-text": "^1.1.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-/RRHm2Cw9o8Mcsrq0eo8fjS9okKYLGfuFwrQ0YoP/6sdSDsXshaTLJsvLlcUcaDaSJ1YFOuHIo3zr2Om2F/16g=="], 334 + 335 + "@atcute/mst": ["@atcute/mst@0.1.2", "", { "dependencies": { "@atcute/cbor": "^2.3.0", "@atcute/cid": "^2.4.0", "@atcute/uint8array": "^1.0.6" } }, "sha512-Oz5CZTjqauEJLT9B+zkoy/mjl216DrjCxJFrguRV3N+1NkIbCfAcSRf3UDSNjfzDzBkJvC1WjA/3oQkm83duPg=="], 336 + 337 + "@atcute/multibase": ["@atcute/multibase@1.1.8", "", { "dependencies": { "@atcute/uint8array": "^1.1.1" } }, "sha512-pJgtImMZKCjqwRbu+2GzB+4xQjKBXDwdZOzeqe0u97zYKRGftpGYGvYv3+pMe2xXe+msDyu7Nv8iJp+U14otTA=="], 338 + 339 + "@atcute/oauth-crypto": ["@atcute/oauth-crypto@0.1.0", "", { "dependencies": { "@atcute/multibase": "^1.1.7", "@atcute/uint8array": "^1.1.0", "@badrap/valita": "^0.4.6", "nanoid": "^5.1.6" } }, "sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w=="], 340 + 341 + "@atcute/repo": ["@atcute/repo@0.1.2", "", { "dependencies": { "@atcute/car": "^5.1.1", "@atcute/cbor": "^2.3.2", "@atcute/cid": "^2.4.1", "@atcute/crypto": "^2.3.0", "@atcute/lexicons": "^1.2.9", "@atcute/mst": "^0.1.2", "@atcute/uint8array": "^1.1.1" } }, "sha512-mX/k8Nv7XFBbahcz5+qsdY91DVwKe8wbut/BrrmzClmSaUgKpztsHjtNfBCamcvIUKc18Lyv8WcVWzlH9wSf5w=="], 313 342 314 343 "@atcute/tangled": ["@atcute/tangled@1.0.14", "", { "dependencies": { "@atcute/atproto": "^3.1.10", "@atcute/lexicons": "^1.2.6" } }, "sha512-vOJuFUCI/dova2OL5kPFgMzYLFyzFNzxxLCvtPvukgcr7ZIm4qLGkNHdVg3Jk8c0bknULe0pJnaCKUTWA1VPdA=="], 315 344 316 - "@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 345 + "@atcute/uint8array": ["@atcute/uint8array@1.1.1", "", {}, "sha512-3LsC8XB8TKe9q/5hOA5sFuzGaIFdJZJNewC5OKa3o/eU6+K7JR6see9Zy2JbQERNVnRl11EzbNov1efgLMAs4g=="], 317 346 318 347 "@atcute/util-fetch": ["@atcute/util-fetch@1.0.5", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig=="], 319 348 320 - "@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 349 + "@atcute/util-text": ["@atcute/util-text@1.1.1", "", { "dependencies": { "unicode-segmenter": "^0.14.5" } }, "sha512-JH0SxzUQJAmbOBTYyhxQbkkI6M33YpjlVLEcbP5GYt43xgFArzV0FJVmEpvIj0kjsmphHB45b6IitdvxPdec9w=="], 350 + 351 + "@atcute/varint": ["@atcute/varint@2.0.0", "", {}, "sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw=="], 352 + 353 + "@atcute/xrpc-server": ["@atcute/xrpc-server@0.1.10", "", { "dependencies": { "@atcute/cbor": "^2.3.2", "@atcute/crypto": "^2.3.0", "@atcute/identity": "^1.1.3", "@atcute/identity-resolver": "^1.2.2", "@atcute/lexicons": "^1.2.9", "@atcute/multibase": "^1.1.8", "@atcute/uint8array": "^1.1.1", "@badrap/valita": "^0.4.6", "nanoid": "^5.1.6" } }, "sha512-j4I+ajiYn9Oxv8h5jU7Bzv1G9x7GNcsn7xpnFT7gXh4RUq19kyoKnKDToaQ+eHyCUo+lhqJOBDbggCdiC1SuGQ=="], 321 354 322 355 "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.2.5", "", { "dependencies": { "@atproto-labs/fetch": "0.2.3", "@atproto-labs/pipe": "0.1.1", "@atproto-labs/simple-store": "0.3.0", "@atproto-labs/simple-store-memory": "0.1.4", "@atproto/did": "0.2.4", "zod": "^3.23.8" } }, "sha512-he7EC6OMSifNs01a4RT9mta/yYitoKDzlK9ty2TFV5Uj/+HpB4vYMRdIDFrRW0Hcsehy90E2t/dw0t7361MEKQ=="], 323 356 ··· 601 634 602 635 "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], 603 636 637 + "@noble/secp256k1": ["@noble/secp256k1@3.0.0", "", {}, "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg=="], 638 + 604 639 "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], 605 640 606 641 "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.200.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q=="], ··· 657 692 658 693 "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], 659 694 695 + "@optique/core": ["@optique/core@0.6.11", "", {}, "sha512-GVLFihzBA1j78NFlkU5N1Lu0jRqET0k6Z66WK8VQKG/a3cxmCInVGSKMIdQG8i6pgC8wD5OizF6Y3QMztmhAxg=="], 696 + 697 + "@optique/run": ["@optique/run@0.6.11", "", { "dependencies": { "@optique/core": "0.6.11" } }, "sha512-tsXBEygGSzNpFK2gjsRlXBn7FiScUeLFWIZNpoAZ8iG85Km0/3K9xgqlQAXoQ+uEZBe4XplnzyCDvmEgbyNT8w=="], 698 + 660 699 "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27rypIapNkYboOSylkf1tD9UW9Ado2I+P1NBL46Qz29KmOjTL6WuJ7mHDC5O66CYxlOkF5r93NPDAC3lFHYBXw=="], 661 700 662 701 "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-I82xGzPkBxzBKgbl8DsA0RfMQCWTWjNmLjIEkW1ECiv3qK02kHGQ5FGUr/29L/SuvnGsULW4tBTRNZiMzL37nA=="], ··· 1101 1140 1102 1141 "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], 1103 1142 1104 - "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], 1143 + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], 1105 1144 1106 1145 "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 1107 1146 ··· 1435 1474 1436 1475 "multiformats": ["multiformats@13.4.2", "", {}, "sha512-eh6eHCrRi1+POZ3dA+Dq1C6jhP1GNtr9CRINMb67OKzqW9I5DUuZM/3jLPlzhgpGeiNUlEGEbkCYChXMCc/8DQ=="], 1437 1476 1438 - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 1477 + "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], 1439 1478 1440 1479 "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 1441 1480 ··· 1743 1782 1744 1783 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], 1745 1784 1785 + "@atcute/atproto/@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 1786 + 1787 + "@atcute/bluesky/@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 1788 + 1789 + "@atcute/client/@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 1790 + 1791 + "@atcute/identity/@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 1792 + 1793 + "@atcute/identity-resolver/@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 1794 + 1795 + "@atcute/tangled/@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 1796 + 1746 1797 "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], 1747 1798 1748 1799 "@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], ··· 1933 1984 1934 1985 "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1935 1986 1936 - "bun-types/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], 1937 - 1938 1987 "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 1939 1988 1940 1989 "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], ··· 1950 1999 "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 1951 2000 1952 2001 "pino-abstract-transport/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], 2002 + 2003 + "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 1953 2004 1954 2005 "protobufjs/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], 1955 2006 ··· 1985 2036 1986 2037 "wispctl/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], 1987 2038 2039 + "@atcute/atproto/@atcute/lexicons/@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 2040 + 2041 + "@atcute/atproto/@atcute/lexicons/@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 2042 + 2043 + "@atcute/bluesky/@atcute/lexicons/@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 2044 + 2045 + "@atcute/bluesky/@atcute/lexicons/@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 2046 + 2047 + "@atcute/client/@atcute/lexicons/@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 2048 + 2049 + "@atcute/client/@atcute/lexicons/@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 2050 + 2051 + "@atcute/identity-resolver/@atcute/lexicons/@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 2052 + 2053 + "@atcute/identity-resolver/@atcute/lexicons/@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 2054 + 2055 + "@atcute/identity/@atcute/lexicons/@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 2056 + 2057 + "@atcute/identity/@atcute/lexicons/@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 2058 + 2059 + "@atcute/tangled/@atcute/lexicons/@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 2060 + 2061 + "@atcute/tangled/@atcute/lexicons/@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 2062 + 1988 2063 "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.4", "", { "dependencies": { "@atproto/common": "^0.5.3", "ws": "^8.12.0" } }, "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg=="], 1989 2064 1990 2065 "@atproto/sync/@atproto/xrpc-server/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], ··· 2059 2134 2060 2135 "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 2061 2136 2062 - "@wispplace/bun-firehose/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], 2063 - 2064 2137 "@wispplace/lexicons/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 2065 2138 2066 2139 "@wispplace/main-app/@atproto/api/@atproto/lexicon": ["@atproto/lexicon@0.5.2", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/syntax": "^0.4.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "zod": "^3.23.8" } }, "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ=="], 2067 2140 2068 2141 "@wispplace/main-app/@atproto/api/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 2069 2142 2070 - "@wispplace/tiered-storage/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], 2071 - 2072 2143 "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 2073 2144 2074 2145 "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 2075 - 2076 - "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 2077 2146 2078 2147 "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 2079 2148 ··· 2201 2270 2202 2271 "wisp-hosting-service/@atproto/lexicon/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 2203 2272 2273 + "wisp-hosting-service/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], 2274 + 2204 2275 "wisp-hosting-service/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 2205 2276 2206 2277 "wisp-hosting-service/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 2278 + 2279 + "wispctl/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], 2207 2280 2208 2281 "wispctl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 2209 2282
+84
lexicons/domain-claim-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domain.claim", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Claim a domain for the authenticated DID. Returns DNS setup instructions for custom domains.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["domain"], 13 + "properties": { 14 + "domain": { 15 + "type": "string", 16 + "description": "Domain to claim (wisp subdomain FQDN or custom domain FQDN).", 17 + "minLength": 3, 18 + "maxLength": 253 19 + }, 20 + "siteRkey": { 21 + "type": "string", 22 + "format": "record-key", 23 + "description": "Optional place.wisp.fs rkey to map immediately after claim." 24 + } 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": ["domain", "status"], 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": ["alreadyClaimed", "pendingVerification", "verified"] 44 + }, 45 + "challengeId": { 46 + "type": "string", 47 + "description": "Identifier used to construct DNS challenge targets for custom domains.", 48 + "minLength": 8, 49 + "maxLength": 64 50 + }, 51 + "txtName": { 52 + "type": "string", 53 + "description": "TXT hostname to set for ownership proof (custom domains).", 54 + "minLength": 3, 55 + "maxLength": 253 56 + }, 57 + "txtValue": { 58 + "type": "string", 59 + "format": "did", 60 + "description": "TXT value to set for ownership proof (custom domains)." 61 + }, 62 + "cnameTarget": { 63 + "type": "string", 64 + "description": "Advisory CNAME target (custom domains).", 65 + "minLength": 3, 66 + "maxLength": 253 67 + }, 68 + "siteRkey": { 69 + "type": "string", 70 + "format": "record-key" 71 + } 72 + } 73 + } 74 + }, 75 + "errors": [ 76 + { "name": "AuthenticationRequired" }, 77 + { "name": "InvalidDomain" }, 78 + { "name": "AlreadyClaimed" }, 79 + { "name": "DomainLimitReached" }, 80 + { "name": "RateLimitExceeded" } 81 + ] 82 + } 83 + } 84 + }
+57
lexicons/domain-get-status-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domain.getStatus", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get current claim and verification status for a domain.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["domain"], 11 + "properties": { 12 + "domain": { 13 + "type": "string", 14 + "description": "Domain to inspect (FQDN, lowercase preferred).", 15 + "minLength": 3, 16 + "maxLength": 253 17 + } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": ["domain", "status"], 25 + "properties": { 26 + "domain": { 27 + "type": "string" 28 + }, 29 + "status": { 30 + "type": "string", 31 + "enum": ["unclaimed", "pendingVerification", "verified", "alreadyClaimed"] 32 + }, 33 + "kind": { 34 + "type": "string", 35 + "enum": ["wisp", "custom"] 36 + }, 37 + "verified": { 38 + "type": "boolean" 39 + }, 40 + "lastCheckedAt": { 41 + "type": "string", 42 + "format": "datetime" 43 + }, 44 + "lastError": { 45 + "type": "string", 46 + "maxLength": 1000 47 + }, 48 + "siteRkey": { 49 + "type": "string", 50 + "format": "record-key" 51 + } 52 + } 53 + } 54 + } 55 + } 56 + } 57 + }
+105
lexicons/domains-v2.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "place.wisp.v2.domains", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Domain registration metadata for wisp.place subdomains and custom domains.", 8 + "key": "any", 9 + "record": { 10 + "type": "object", 11 + "required": ["domain", "registration", "createdAt", "updatedAt"], 12 + "properties": { 13 + "domain": { 14 + "type": "string", 15 + "description": "Lowercase FQDN for this registration (for example, alice.wisp.place or example.com).", 16 + "minLength": 3, 17 + "maxLength": 253 18 + }, 19 + "registration": { 20 + "type": "union", 21 + "refs": ["#wispRegistration", "#customRegistration"] 22 + }, 23 + "siteRkey": { 24 + "type": "string", 25 + "format": "record-key", 26 + "description": "Optional place.wisp.fs record key currently mapped to this domain." 27 + }, 28 + "createdAt": { 29 + "type": "string", 30 + "format": "datetime" 31 + }, 32 + "updatedAt": { 33 + "type": "string", 34 + "format": "datetime" 35 + } 36 + } 37 + } 38 + }, 39 + "wispRegistration": { 40 + "type": "object", 41 + "description": "Registration for a first-party subdomain under the wisp.place base host.", 42 + "required": ["kind", "handle"], 43 + "properties": { 44 + "kind": { 45 + "type": "string", 46 + "const": "wisp" 47 + }, 48 + "handle": { 49 + "type": "string", 50 + "description": "Subdomain label only (for example, alice).", 51 + "minLength": 3, 52 + "maxLength": 63 53 + } 54 + } 55 + }, 56 + "customRegistration": { 57 + "type": "object", 58 + "description": "Registration metadata for a custom domain.", 59 + "required": ["kind", "challengeId", "verification"], 60 + "properties": { 61 + "kind": { 62 + "type": "string", 63 + "const": "custom" 64 + }, 65 + "challengeId": { 66 + "type": "string", 67 + "description": "Challenge identifier used to derive DNS setup instructions.", 68 + "minLength": 8, 69 + "maxLength": 64 70 + }, 71 + "verification": { 72 + "type": "ref", 73 + "ref": "#verification" 74 + } 75 + } 76 + }, 77 + "verification": { 78 + "type": "object", 79 + "description": "Latest verification state for a custom domain.", 80 + "required": ["status", "method"], 81 + "properties": { 82 + "status": { 83 + "type": "string", 84 + "enum": ["pending", "verified", "failed"] 85 + }, 86 + "method": { 87 + "type": "string", 88 + "enum": ["txt-did-v1"] 89 + }, 90 + "lastCheckedAt": { 91 + "type": "string", 92 + "format": "datetime" 93 + }, 94 + "verifiedAt": { 95 + "type": "string", 96 + "format": "datetime" 97 + }, 98 + "lastError": { 99 + "type": "string", 100 + "maxLength": 1000 101 + } 102 + } 103 + } 104 + } 105 + }
+18 -6
packages/@wispplace/lexicons/README.md
··· 1 - # @wisp/lexicons 1 + # @wispplace/lexicons 2 2 3 3 Shared AT Protocol lexicon definitions and generated TypeScript types for the wisp.place project. 4 4 5 5 ## Contents 6 6 7 7 - `/lexicons` - Source lexicon JSON definitions 8 - - `/src` - Generated TypeScript types and validation functions 8 + - `/src/types` - Generated TypeScript types and validators (`@atproto/lex-cli`) 9 + - `/src/atcute` - Generated atcute bindings (`@atcute/lex-cli`) 9 10 10 11 ## Usage 11 12 12 13 ```typescript 13 - import { ids, lexicons } from '@wisp/lexicons'; 14 - import type { PlaceWispFs } from '@wisp/lexicons/types/place/wisp/fs'; 14 + import { ids, lexicons } from '@wispplace/lexicons'; 15 + import type { PlaceWispFs } from '@wispplace/lexicons/types/place/wisp/fs'; 16 + import { PlaceWispV2DomainClaim } from '@wispplace/lexicons/atcute'; 15 17 ``` 16 18 17 19 ## Code Generation ··· 19 21 To regenerate types from lexicon definitions: 20 22 21 23 ```bash 22 - npm run codegen 24 + bun run codegen 25 + bun run codegen:atcute 23 26 ``` 24 27 25 - This uses `@atproto/lex-cli` to generate TypeScript types from the JSON schemas in `/lexicons`. 28 + From monorepo root you can run both with: 29 + 30 + ```bash 31 + bun run scripts/codegen.sh 32 + ``` 33 + 34 + Generation uses: 35 + 36 + - `@atproto/lex-cli` for `src/types` 37 + - `@atcute/lex-cli` for `src/atcute`
+6
packages/@wispplace/lexicons/lex.atcute.config.js
··· 1 + import { defineLexiconConfig } from '@atcute/lex-cli'; 2 + 3 + export default defineLexiconConfig({ 4 + files: ['../../../lexicons/*-v2.json'], 5 + outdir: 'src/atcute/lexicons', 6 + });
+20 -1
packages/@wispplace/lexicons/package.json
··· 22 22 "types": "./src/types/place/wisp/subfs.ts", 23 23 "default": "./src/types/place/wisp/subfs.ts" 24 24 }, 25 + "./types/place/wisp/v2/domains": { 26 + "types": "./src/types/place/wisp/v2/domains.ts", 27 + "default": "./src/types/place/wisp/v2/domains.ts" 28 + }, 29 + "./types/place/wisp/v2/domain/claim": { 30 + "types": "./src/types/place/wisp/v2/domain/claim.ts", 31 + "default": "./src/types/place/wisp/v2/domain/claim.ts" 32 + }, 33 + "./types/place/wisp/v2/domain/getStatus": { 34 + "types": "./src/types/place/wisp/v2/domain/getStatus.ts", 35 + "default": "./src/types/place/wisp/v2/domain/getStatus.ts" 36 + }, 37 + "./atcute": { 38 + "types": "./src/atcute/index.ts", 39 + "default": "./src/atcute/index.ts" 40 + }, 25 41 "./lexicons": { 26 42 "types": "./src/lexicons.ts", 27 43 "default": "./src/lexicons.ts" ··· 32 48 } 33 49 }, 34 50 "scripts": { 35 - "codegen": "lex gen-server ./src ../../../lexicons/*.json" 51 + "codegen": "lex gen-server ./src ../../../lexicons/*.json", 52 + "codegen:atcute": "lex-cli generate -c lex.atcute.config.js" 36 53 }, 37 54 "dependencies": { 55 + "@atcute/lexicons": "^1.2.9", 38 56 "@atproto/lexicon": "^0.5.1", 39 57 "@atproto/xrpc-server": "^0.9.5" 40 58 }, 41 59 "devDependencies": { 60 + "@atcute/lex-cli": "^2.5.3", 42 61 "@atproto/lex-cli": "^0.9.5", 43 62 "multiformats": "^13.4.1" 44 63 }
+1
packages/@wispplace/lexicons/src/atcute/index.ts
··· 1 + export * from './lexicons/index';
+3
packages/@wispplace/lexicons/src/atcute/lexicons/index.ts
··· 1 + export * as PlaceWispV2DomainClaim from "./types/place/wisp/v2/domain/claim.js"; 2 + export * as PlaceWispV2DomainGetStatus from "./types/place/wisp/v2/domain/getStatus.js"; 3 + export * as PlaceWispV2Domains from "./types/place/wisp/v2/domains.js";
+89
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/claim.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.claim", { 6 + params: null, 7 + input: { 8 + type: "lex", 9 + schema: /*#__PURE__*/ v.object({ 10 + /** 11 + * Domain to claim (wisp subdomain FQDN or custom domain FQDN). 12 + * @minLength 3 13 + * @maxLength 253 14 + */ 15 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 16 + /*#__PURE__*/ v.stringLength(3, 253), 17 + ]), 18 + /** 19 + * Optional place.wisp.fs rkey to map immediately after claim. 20 + */ 21 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 22 + }), 23 + }, 24 + output: { 25 + type: "lex", 26 + schema: /*#__PURE__*/ v.object({ 27 + /** 28 + * Identifier used to construct DNS challenge targets for custom domains. 29 + * @minLength 8 30 + * @maxLength 64 31 + */ 32 + challengeId: /*#__PURE__*/ v.optional( 33 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 34 + /*#__PURE__*/ v.stringLength(8, 64), 35 + ]), 36 + ), 37 + /** 38 + * Advisory CNAME target (custom domains). 39 + * @minLength 3 40 + * @maxLength 253 41 + */ 42 + cnameTarget: /*#__PURE__*/ v.optional( 43 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 44 + /*#__PURE__*/ v.stringLength(3, 253), 45 + ]), 46 + ), 47 + domain: /*#__PURE__*/ v.string(), 48 + kind: /*#__PURE__*/ v.optional( 49 + /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 50 + ), 51 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 52 + status: /*#__PURE__*/ v.literalEnum([ 53 + "alreadyClaimed", 54 + "pendingVerification", 55 + "verified", 56 + ]), 57 + /** 58 + * TXT hostname to set for ownership proof (custom domains). 59 + * @minLength 3 60 + * @maxLength 253 61 + */ 62 + txtName: /*#__PURE__*/ v.optional( 63 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 64 + /*#__PURE__*/ v.stringLength(3, 253), 65 + ]), 66 + ), 67 + /** 68 + * TXT value to set for ownership proof (custom domains). 69 + */ 70 + txtValue: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.didString()), 71 + }), 72 + }, 73 + }); 74 + 75 + type main$schematype = typeof _mainSchema; 76 + 77 + export interface mainSchema extends main$schematype {} 78 + 79 + export const mainSchema = _mainSchema as mainSchema; 80 + 81 + export interface $params {} 82 + export interface $input extends v.InferXRPCBodyInput<mainSchema["input"]> {} 83 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 84 + 85 + declare module "@atcute/lexicons/ambient" { 86 + interface XRPCProcedures { 87 + "place.wisp.v2.domain.claim": mainSchema; 88 + } 89 + }
+57
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/getStatus.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.domain.getStatus", { 6 + params: /*#__PURE__*/ v.object({ 7 + /** 8 + * Domain to inspect (FQDN, lowercase preferred). 9 + * @minLength 3 10 + * @maxLength 253 11 + */ 12 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 13 + /*#__PURE__*/ v.stringLength(3, 253), 14 + ]), 15 + }), 16 + output: { 17 + type: "lex", 18 + schema: /*#__PURE__*/ v.object({ 19 + domain: /*#__PURE__*/ v.string(), 20 + kind: /*#__PURE__*/ v.optional( 21 + /*#__PURE__*/ v.literalEnum(["custom", "wisp"]), 22 + ), 23 + lastCheckedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 24 + /** 25 + * @maxLength 1000 26 + */ 27 + lastError: /*#__PURE__*/ v.optional( 28 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 29 + /*#__PURE__*/ v.stringLength(0, 1000), 30 + ]), 31 + ), 32 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 33 + status: /*#__PURE__*/ v.literalEnum([ 34 + "alreadyClaimed", 35 + "pendingVerification", 36 + "unclaimed", 37 + "verified", 38 + ]), 39 + verified: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.boolean()), 40 + }), 41 + }, 42 + }); 43 + 44 + type main$schematype = typeof _mainSchema; 45 + 46 + export interface mainSchema extends main$schematype {} 47 + 48 + export const mainSchema = _mainSchema as mainSchema; 49 + 50 + export interface $params extends v.InferInput<mainSchema["params"]> {} 51 + export interface $output extends v.InferXRPCBodyInput<mainSchema["output"]> {} 52 + 53 + declare module "@atcute/lexicons/ambient" { 54 + interface XRPCQueries { 55 + "place.wisp.v2.domain.getStatus": mainSchema; 56 + } 57 + }
+110
packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domains.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 _customRegistrationSchema = /*#__PURE__*/ v.object({ 6 + $type: /*#__PURE__*/ v.optional( 7 + /*#__PURE__*/ v.literal("place.wisp.v2.domains#customRegistration"), 8 + ), 9 + /** 10 + * Challenge identifier used to derive DNS setup instructions. 11 + * @minLength 8 12 + * @maxLength 64 13 + */ 14 + challengeId: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 15 + /*#__PURE__*/ v.stringLength(8, 64), 16 + ]), 17 + kind: /*#__PURE__*/ v.literal("custom"), 18 + get verification() { 19 + return verificationSchema; 20 + }, 21 + }); 22 + const _mainSchema = /*#__PURE__*/ v.record( 23 + /*#__PURE__*/ v.string(), 24 + /*#__PURE__*/ v.object({ 25 + $type: /*#__PURE__*/ v.literal("place.wisp.v2.domains"), 26 + createdAt: /*#__PURE__*/ v.datetimeString(), 27 + /** 28 + * Lowercase FQDN for this registration (for example, alice.wisp.place or example.com). 29 + * @minLength 3 30 + * @maxLength 253 31 + */ 32 + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 33 + /*#__PURE__*/ v.stringLength(3, 253), 34 + ]), 35 + get registration() { 36 + return /*#__PURE__*/ v.variant([ 37 + customRegistrationSchema, 38 + wispRegistrationSchema, 39 + ]); 40 + }, 41 + /** 42 + * Optional place.wisp.fs record key currently mapped to this domain. 43 + */ 44 + siteRkey: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.recordKeyString()), 45 + updatedAt: /*#__PURE__*/ v.datetimeString(), 46 + }), 47 + ); 48 + const _verificationSchema = /*#__PURE__*/ v.object({ 49 + $type: /*#__PURE__*/ v.optional( 50 + /*#__PURE__*/ v.literal("place.wisp.v2.domains#verification"), 51 + ), 52 + lastCheckedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 53 + /** 54 + * @maxLength 1000 55 + */ 56 + lastError: /*#__PURE__*/ v.optional( 57 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 58 + /*#__PURE__*/ v.stringLength(0, 1000), 59 + ]), 60 + ), 61 + method: /*#__PURE__*/ v.literalEnum(["txt-did-v1"]), 62 + status: /*#__PURE__*/ v.literalEnum(["failed", "pending", "verified"]), 63 + verifiedAt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.datetimeString()), 64 + }); 65 + const _wispRegistrationSchema = /*#__PURE__*/ v.object({ 66 + $type: /*#__PURE__*/ v.optional( 67 + /*#__PURE__*/ v.literal("place.wisp.v2.domains#wispRegistration"), 68 + ), 69 + /** 70 + * Subdomain label only (for example, alice). 71 + * @minLength 3 72 + * @maxLength 63 73 + */ 74 + handle: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ 75 + /*#__PURE__*/ v.stringLength(3, 63), 76 + ]), 77 + kind: /*#__PURE__*/ v.literal("wisp"), 78 + }); 79 + 80 + type customRegistration$schematype = typeof _customRegistrationSchema; 81 + type main$schematype = typeof _mainSchema; 82 + type verification$schematype = typeof _verificationSchema; 83 + type wispRegistration$schematype = typeof _wispRegistrationSchema; 84 + 85 + export interface customRegistrationSchema extends customRegistration$schematype {} 86 + export interface mainSchema extends main$schematype {} 87 + export interface verificationSchema extends verification$schematype {} 88 + export interface wispRegistrationSchema extends wispRegistration$schematype {} 89 + 90 + export const customRegistrationSchema = 91 + _customRegistrationSchema as customRegistrationSchema; 92 + export const mainSchema = _mainSchema as mainSchema; 93 + export const verificationSchema = _verificationSchema as verificationSchema; 94 + export const wispRegistrationSchema = 95 + _wispRegistrationSchema as wispRegistrationSchema; 96 + 97 + export interface CustomRegistration extends v.InferInput< 98 + typeof customRegistrationSchema 99 + > {} 100 + export interface Main extends v.InferInput<typeof mainSchema> {} 101 + export interface Verification extends v.InferInput<typeof verificationSchema> {} 102 + export interface WispRegistration extends v.InferInput< 103 + typeof wispRegistrationSchema 104 + > {} 105 + 106 + declare module "@atcute/lexicons/ambient" { 107 + interface Records { 108 + "place.wisp.v2.domains": mainSchema; 109 + } 110 + }
+46
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 PlaceWispV2DomainClaim from './types/place/wisp/v2/domain/claim.js' 14 + import * as PlaceWispV2DomainGetStatus from './types/place/wisp/v2/domain/getStatus.js' 13 15 14 16 export function createServer(options?: XrpcOptions): Server { 15 17 return new Server(options) ··· 37 39 38 40 export class PlaceWispNS { 39 41 _server: Server 42 + v2: PlaceWispV2NS 40 43 41 44 constructor(server: Server) { 42 45 this._server = server 46 + this.v2 = new PlaceWispV2NS(server) 47 + } 48 + } 49 + 50 + export class PlaceWispV2NS { 51 + _server: Server 52 + domain: PlaceWispV2DomainNS 53 + 54 + constructor(server: Server) { 55 + this._server = server 56 + this.domain = new PlaceWispV2DomainNS(server) 57 + } 58 + } 59 + 60 + export class PlaceWispV2DomainNS { 61 + _server: Server 62 + 63 + constructor(server: Server) { 64 + this._server = server 65 + } 66 + 67 + claim<A extends Auth = void>( 68 + cfg: MethodConfigOrHandler< 69 + A, 70 + PlaceWispV2DomainClaim.QueryParams, 71 + PlaceWispV2DomainClaim.HandlerInput, 72 + PlaceWispV2DomainClaim.HandlerOutput 73 + >, 74 + ) { 75 + const nsid = 'place.wisp.v2.domain.claim' // @ts-ignore 76 + return this._server.xrpc.method(nsid, cfg) 77 + } 78 + 79 + getStatus<A extends Auth = void>( 80 + cfg: MethodConfigOrHandler< 81 + A, 82 + PlaceWispV2DomainGetStatus.QueryParams, 83 + PlaceWispV2DomainGetStatus.HandlerInput, 84 + PlaceWispV2DomainGetStatus.HandlerOutput 85 + >, 86 + ) { 87 + const nsid = 'place.wisp.v2.domain.getStatus' // @ts-ignore 88 + return this._server.xrpc.method(nsid, cfg) 43 89 } 44 90 }
+278
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 + PlaceWispV2DomainClaim: { 14 + lexicon: 1, 15 + id: 'place.wisp.v2.domain.claim', 16 + defs: { 17 + main: { 18 + type: 'procedure', 19 + description: 20 + 'Claim a domain for the authenticated DID. Returns DNS setup instructions for custom domains.', 21 + input: { 22 + encoding: 'application/json', 23 + schema: { 24 + type: 'object', 25 + required: ['domain'], 26 + properties: { 27 + domain: { 28 + type: 'string', 29 + description: 30 + 'Domain to claim (wisp subdomain FQDN or custom domain FQDN).', 31 + minLength: 3, 32 + maxLength: 253, 33 + }, 34 + siteRkey: { 35 + type: 'string', 36 + format: 'record-key', 37 + description: 38 + 'Optional place.wisp.fs rkey to map immediately after claim.', 39 + }, 40 + }, 41 + }, 42 + }, 43 + output: { 44 + encoding: 'application/json', 45 + schema: { 46 + type: 'object', 47 + required: ['domain', 'status'], 48 + properties: { 49 + domain: { 50 + type: 'string', 51 + }, 52 + kind: { 53 + type: 'string', 54 + enum: ['wisp', 'custom'], 55 + }, 56 + status: { 57 + type: 'string', 58 + enum: ['alreadyClaimed', 'pendingVerification', 'verified'], 59 + }, 60 + challengeId: { 61 + type: 'string', 62 + description: 63 + 'Identifier used to construct DNS challenge targets for custom domains.', 64 + minLength: 8, 65 + maxLength: 64, 66 + }, 67 + txtName: { 68 + type: 'string', 69 + description: 70 + 'TXT hostname to set for ownership proof (custom domains).', 71 + minLength: 3, 72 + maxLength: 253, 73 + }, 74 + txtValue: { 75 + type: 'string', 76 + format: 'did', 77 + description: 78 + 'TXT value to set for ownership proof (custom domains).', 79 + }, 80 + cnameTarget: { 81 + type: 'string', 82 + description: 'Advisory CNAME target (custom domains).', 83 + minLength: 3, 84 + maxLength: 253, 85 + }, 86 + siteRkey: { 87 + type: 'string', 88 + format: 'record-key', 89 + }, 90 + }, 91 + }, 92 + }, 93 + errors: [ 94 + { 95 + name: 'AuthenticationRequired', 96 + }, 97 + { 98 + name: 'InvalidDomain', 99 + }, 100 + { 101 + name: 'AlreadyClaimed', 102 + }, 103 + { 104 + name: 'DomainLimitReached', 105 + }, 106 + { 107 + name: 'RateLimitExceeded', 108 + }, 109 + ], 110 + }, 111 + }, 112 + }, 113 + PlaceWispV2DomainGetStatus: { 114 + lexicon: 1, 115 + id: 'place.wisp.v2.domain.getStatus', 116 + defs: { 117 + main: { 118 + type: 'query', 119 + description: 'Get current claim and verification status for a domain.', 120 + parameters: { 121 + type: 'params', 122 + required: ['domain'], 123 + properties: { 124 + domain: { 125 + type: 'string', 126 + description: 'Domain to inspect (FQDN, lowercase preferred).', 127 + minLength: 3, 128 + maxLength: 253, 129 + }, 130 + }, 131 + }, 132 + output: { 133 + encoding: 'application/json', 134 + schema: { 135 + type: 'object', 136 + required: ['domain', 'status'], 137 + properties: { 138 + domain: { 139 + type: 'string', 140 + }, 141 + status: { 142 + type: 'string', 143 + enum: [ 144 + 'unclaimed', 145 + 'pendingVerification', 146 + 'verified', 147 + 'alreadyClaimed', 148 + ], 149 + }, 150 + kind: { 151 + type: 'string', 152 + enum: ['wisp', 'custom'], 153 + }, 154 + verified: { 155 + type: 'boolean', 156 + }, 157 + lastCheckedAt: { 158 + type: 'string', 159 + format: 'datetime', 160 + }, 161 + lastError: { 162 + type: 'string', 163 + maxLength: 1000, 164 + }, 165 + siteRkey: { 166 + type: 'string', 167 + format: 'record-key', 168 + }, 169 + }, 170 + }, 171 + }, 172 + }, 173 + }, 174 + }, 175 + PlaceWispV2Domains: { 176 + lexicon: 1, 177 + id: 'place.wisp.v2.domains', 178 + defs: { 179 + main: { 180 + type: 'record', 181 + description: 182 + 'Domain registration metadata for wisp.place subdomains and custom domains.', 183 + key: 'any', 184 + record: { 185 + type: 'object', 186 + required: ['domain', 'registration', 'createdAt', 'updatedAt'], 187 + properties: { 188 + domain: { 189 + type: 'string', 190 + description: 191 + 'Lowercase FQDN for this registration (for example, alice.wisp.place or example.com).', 192 + minLength: 3, 193 + maxLength: 253, 194 + }, 195 + registration: { 196 + type: 'union', 197 + refs: [ 198 + 'lex:place.wisp.v2.domains#wispRegistration', 199 + 'lex:place.wisp.v2.domains#customRegistration', 200 + ], 201 + }, 202 + siteRkey: { 203 + type: 'string', 204 + format: 'record-key', 205 + description: 206 + 'Optional place.wisp.fs record key currently mapped to this domain.', 207 + }, 208 + createdAt: { 209 + type: 'string', 210 + format: 'datetime', 211 + }, 212 + updatedAt: { 213 + type: 'string', 214 + format: 'datetime', 215 + }, 216 + }, 217 + }, 218 + }, 219 + wispRegistration: { 220 + type: 'object', 221 + description: 222 + 'Registration for a first-party subdomain under the wisp.place base host.', 223 + required: ['kind', 'handle'], 224 + properties: { 225 + kind: { 226 + type: 'string', 227 + const: 'wisp', 228 + }, 229 + handle: { 230 + type: 'string', 231 + description: 'Subdomain label only (for example, alice).', 232 + minLength: 3, 233 + maxLength: 63, 234 + }, 235 + }, 236 + }, 237 + customRegistration: { 238 + type: 'object', 239 + description: 'Registration metadata for a custom domain.', 240 + required: ['kind', 'challengeId', 'verification'], 241 + properties: { 242 + kind: { 243 + type: 'string', 244 + const: 'custom', 245 + }, 246 + challengeId: { 247 + type: 'string', 248 + description: 249 + 'Challenge identifier used to derive DNS setup instructions.', 250 + minLength: 8, 251 + maxLength: 64, 252 + }, 253 + verification: { 254 + type: 'ref', 255 + ref: 'lex:place.wisp.v2.domains#verification', 256 + }, 257 + }, 258 + }, 259 + verification: { 260 + type: 'object', 261 + description: 'Latest verification state for a custom domain.', 262 + required: ['status', 'method'], 263 + properties: { 264 + status: { 265 + type: 'string', 266 + enum: ['pending', 'verified', 'failed'], 267 + }, 268 + method: { 269 + type: 'string', 270 + enum: ['txt-did-v1'], 271 + }, 272 + lastCheckedAt: { 273 + type: 'string', 274 + format: 'datetime', 275 + }, 276 + verifiedAt: { 277 + type: 'string', 278 + format: 'datetime', 279 + }, 280 + lastError: { 281 + type: 'string', 282 + maxLength: 1000, 283 + }, 284 + }, 285 + }, 286 + }, 287 + }, 13 288 PlaceWispFs: { 14 289 lexicon: 1, 15 290 id: 'place.wisp.fs', ··· 358 633 } 359 634 360 635 export const ids = { 636 + PlaceWispV2DomainClaim: 'place.wisp.v2.domain.claim', 637 + PlaceWispV2DomainGetStatus: 'place.wisp.v2.domain.getStatus', 638 + PlaceWispV2Domains: 'place.wisp.v2.domains', 361 639 PlaceWispFs: 'place.wisp.fs', 362 640 PlaceWispSettings: 'place.wisp.settings', 363 641 PlaceWispSubfs: 'place.wisp.subfs',
+63
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/claim.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.claim' 16 + 17 + export type QueryParams = {} 18 + 19 + export interface InputSchema { 20 + /** Domain to claim (wisp subdomain FQDN or custom domain FQDN). */ 21 + domain: string 22 + /** Optional place.wisp.fs rkey to map immediately after claim. */ 23 + siteRkey?: string 24 + } 25 + 26 + export interface OutputSchema { 27 + domain: string 28 + kind?: 'wisp' | 'custom' 29 + status: 'alreadyClaimed' | 'pendingVerification' | 'verified' 30 + /** Identifier used to construct DNS challenge targets for custom domains. */ 31 + challengeId?: string 32 + /** TXT hostname to set for ownership proof (custom domains). */ 33 + txtName?: string 34 + /** TXT value to set for ownership proof (custom domains). */ 35 + txtValue?: string 36 + /** Advisory CNAME target (custom domains). */ 37 + cnameTarget?: string 38 + siteRkey?: string 39 + } 40 + 41 + export interface HandlerInput { 42 + encoding: 'application/json' 43 + body: InputSchema 44 + } 45 + 46 + export interface HandlerSuccess { 47 + encoding: 'application/json' 48 + body: OutputSchema 49 + headers?: { [key: string]: string } 50 + } 51 + 52 + export interface HandlerError { 53 + status: number 54 + message?: string 55 + error?: 56 + | 'AuthenticationRequired' 57 + | 'InvalidDomain' 58 + | 'AlreadyClaimed' 59 + | 'DomainLimitReached' 60 + | 'RateLimitExceeded' 61 + } 62 + 63 + export type HandlerOutput = HandlerError | HandlerSuccess
+46
packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/getStatus.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.getStatus' 16 + 17 + export type QueryParams = { 18 + /** Domain to inspect (FQDN, lowercase preferred). */ 19 + domain: string 20 + } 21 + export type InputSchema = undefined 22 + 23 + export interface OutputSchema { 24 + domain: string 25 + status: 'unclaimed' | 'pendingVerification' | 'verified' | 'alreadyClaimed' 26 + kind?: 'wisp' | 'custom' 27 + verified?: boolean 28 + lastCheckedAt?: string 29 + lastError?: string 30 + siteRkey?: string 31 + } 32 + 33 + export type HandlerInput = void 34 + 35 + export interface HandlerSuccess { 36 + encoding: 'application/json' 37 + body: OutputSchema 38 + headers?: { [key: string]: string } 39 + } 40 + 41 + export interface HandlerError { 42 + status: number 43 + message?: string 44 + } 45 + 46 + export type HandlerOutput = HandlerError | HandlerSuccess
+103
packages/@wispplace/lexicons/src/types/place/wisp/v2/domains.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.domains' 16 + 17 + export interface Main { 18 + $type: 'place.wisp.v2.domains' 19 + /** Lowercase FQDN for this registration (for example, alice.wisp.place or example.com). */ 20 + domain: string 21 + registration: 22 + | $Typed<WispRegistration> 23 + | $Typed<CustomRegistration> 24 + | { $type: string } 25 + /** Optional place.wisp.fs record key currently mapped to this domain. */ 26 + siteRkey?: string 27 + createdAt: string 28 + updatedAt: string 29 + [k: string]: unknown 30 + } 31 + 32 + const hashMain = 'main' 33 + 34 + export function isMain<V>(v: V) { 35 + return is$typed(v, id, hashMain) 36 + } 37 + 38 + export function validateMain<V>(v: V) { 39 + return validate<Main & V>(v, id, hashMain, true) 40 + } 41 + 42 + export { 43 + type Main as Record, 44 + isMain as isRecord, 45 + validateMain as validateRecord, 46 + } 47 + 48 + /** Registration for a first-party subdomain under the wisp.place base host. */ 49 + export interface WispRegistration { 50 + $type?: 'place.wisp.v2.domains#wispRegistration' 51 + kind: 'wisp' 52 + /** Subdomain label only (for example, alice). */ 53 + handle: string 54 + } 55 + 56 + const hashWispRegistration = 'wispRegistration' 57 + 58 + export function isWispRegistration<V>(v: V) { 59 + return is$typed(v, id, hashWispRegistration) 60 + } 61 + 62 + export function validateWispRegistration<V>(v: V) { 63 + return validate<WispRegistration & V>(v, id, hashWispRegistration) 64 + } 65 + 66 + /** Registration metadata for a custom domain. */ 67 + export interface CustomRegistration { 68 + $type?: 'place.wisp.v2.domains#customRegistration' 69 + kind: 'custom' 70 + /** Challenge identifier used to derive DNS setup instructions. */ 71 + challengeId: string 72 + verification: Verification 73 + } 74 + 75 + const hashCustomRegistration = 'customRegistration' 76 + 77 + export function isCustomRegistration<V>(v: V) { 78 + return is$typed(v, id, hashCustomRegistration) 79 + } 80 + 81 + export function validateCustomRegistration<V>(v: V) { 82 + return validate<CustomRegistration & V>(v, id, hashCustomRegistration) 83 + } 84 + 85 + /** Latest verification state for a custom domain. */ 86 + export interface Verification { 87 + $type?: 'place.wisp.v2.domains#verification' 88 + status: 'pending' | 'verified' | 'failed' 89 + method: 'txt-did-v1' 90 + lastCheckedAt?: string 91 + verifiedAt?: string 92 + lastError?: string 93 + } 94 + 95 + const hashVerification = 'verification' 96 + 97 + export function isVerification<V>(v: V) { 98 + return is$typed(v, id, hashVerification) 99 + } 100 + 101 + export function validateVerification<V>(v: V) { 102 + return validate<Verification & V>(v, id, hashVerification) 103 + }
+3 -1
packages/@wispplace/lexicons/tsconfig.json
··· 4 4 "outDir": "./dist", 5 5 "rootDir": "./src", 6 6 "declaration": true, 7 - "declarationMap": true 7 + "declarationMap": true, 8 + "allowImportingTsExtensions": true, 9 + "noEmit": true 8 10 }, 9 11 "include": ["src/**/*"], 10 12 "exclude": ["node_modules", "dist"]
+5 -2
scripts/codegen.sh
··· 11 11 fi 12 12 13 13 echo "=== Generating TypeScript lexicons ===" 14 - cd "$ROOT_DIR/packages/@wisp/lexicons" 15 - eval "$AUTO_ACCEPT npm run codegen" 14 + cd "$ROOT_DIR/packages/@wispplace/lexicons" 15 + eval "$AUTO_ACCEPT bun run codegen" 16 + 17 + echo "=== Generating atcute lexicons ===" 18 + eval "$AUTO_ACCEPT bun run codegen:atcute" 16 19 17 20 echo "=== Done ==="