diff --git a/apps/main-app/public/landingpage.html b/apps/main-app/public/landingpage.html index 07c8407..835094f 100644 --- a/apps/main-app/public/landingpage.html +++ b/apps/main-app/public/landingpage.html @@ -306,6 +306,46 @@ font-size: 0.9375rem; } + /* Features */ + .features { + padding: 6rem 2rem; + background: var(--bg-alt); + } + + .features-inner { + max-width: 1100px; + margin: 0 auto; + } + + .features-list { + list-style: none; + margin: 2.5rem 0 0; + padding: 0; + display: grid; + gap: 1.25rem; + max-width: 46rem; + } + + .features-list li { + position: relative; + padding-left: 1.75rem; + font-size: 1.125rem; + color: var(--text); + line-height: 1.5; + } + + .features-list li::before { + content: "•"; + position: absolute; + left: 0.25rem; + color: var(--text-subtle); + } + + .features-list .hl { + color: var(--success); + font-weight: 600; + } + /* CLI Section */ .cli-section { padding: 6rem 2rem; @@ -520,6 +560,19 @@ +
+
+ +

A passion project, not a startup

+ +
+
+
diff --git a/apps/main-app/src/routes/xrpc.ts b/apps/main-app/src/routes/xrpc.ts index 77cf6a1..2b77cf9 100644 --- a/apps/main-app/src/routes/xrpc.ts +++ b/apps/main-app/src/routes/xrpc.ts @@ -10,6 +10,7 @@ import { PlaceWispV2DomainDelete, PlaceWispV2DomainGetList, PlaceWispV2DomainGetStatus, + PlaceWispV2DomainVerify, PlaceWispV2SecretCreate, PlaceWispV2SecretDelete, PlaceWispV2SecretList, @@ -20,7 +21,7 @@ import { } from '@wispplace/lexicons/atcute' import { createLogger } from '@wispplace/observability' import { Elysia } from 'elysia' - +import { publishDomainCacheInvalidation } from '../lib/cache-invalidation' import { claimCustomDomain, claimDomain, @@ -37,8 +38,10 @@ import { listWebhookSecrets, rotateWebhookSecret, updateCustomDomainRkey, + updateCustomDomainVerification, updateWispDomainSite, } from '../lib/db' +import { verifyCustomDomain } from '../lib/dns-verify' import { extractWispHandle, isValidHandle, normalizeDomain, toDomain, validateCustomDomain } from '../lib/domain-utils' const logger = createLogger('main-app') @@ -87,6 +90,7 @@ const XRPC_NSIDS = { claimSubdomain: 'place.wisp.v2.domain.claimSubdomain', claim: 'place.wisp.v2.domain.claim', delete: 'place.wisp.v2.domain.delete', + verify: 'place.wisp.v2.domain.verify', deleteSite: 'place.wisp.v2.site.delete', secretCreate: 'place.wisp.v2.secret.create', secretList: 'place.wisp.v2.secret.list', @@ -756,11 +760,11 @@ export const xrpcRoutes = () => { }) addProcedureWithAliases(router, withNsid(PlaceWispV2DomainDelete.mainSchema as any, XRPC_NSIDS.delete), [], { - async handler({ params, request }) { + async handler({ input, request }) { const auth = requireAuthenticated(authByRequest.get(request)) const did = auth.did as DidString - const domain = normalizeDomain(params.domain) + const domain = normalizeDomain(input.domain) if (domain.length === 0) { invalidDomain('domain is required') } @@ -792,6 +796,47 @@ export const xrpcRoutes = () => { }, }) + addProcedureWithAliases(router, withNsid(PlaceWispV2DomainVerify.mainSchema as any, XRPC_NSIDS.verify), [], { + async handler({ input, request }) { + const auth = requireAuthenticated(authByRequest.get(request)) + const did = auth.did as DidString + + const domain = normalizeDomain(input.domain) + if (domain.length === 0) { + invalidDomain('domain is required') + } + + // Only custom domains require DNS verification; wisp subdomains are + // implicitly verified, so there is nothing to check for those. + const registered = await isDomainRegistered(domain) + if (!registered.registered || registered.type !== 'custom') { + notFound() + } + + const custom = await getCustomDomainInfo(domain) + if (!custom || custom.did !== did) { + notFound() + } + + const id = custom.id as string + const result = await verifyCustomDomain(domain, did, id) + + await updateCustomDomainVerification(id, result.verified) + await publishDomainCacheInvalidation(domain, 'custom', id) + + return json({ + domain, + kind: 'custom', + status: result.verified ? 'verified' : 'pendingVerification', + verified: result.verified, + error: result.verified ? undefined : result.error, + warning: result.warning, + txtFound: result.found?.txt?.join(', '), + cnameFound: result.found?.cname, + }) + }, + }) + addProcedureWithAliases( router, withNsid(PlaceWispV2SiteDelete.mainSchema as any, XRPC_NSIDS.deleteSite), @@ -861,6 +906,7 @@ export const xrpcRoutes = () => { claimSubdomain: (PlaceWispV2DomainClaimSubdomain.mainSchema as any).nsid, claim: (PlaceWispV2DomainClaim.mainSchema as any).nsid, delete: (PlaceWispV2DomainDelete.mainSchema as any).nsid, + verify: (PlaceWispV2DomainVerify.mainSchema as any).nsid, deleteSite: (PlaceWispV2SiteDelete.mainSchema as any).nsid, } logger.info('[XRPC] Registered methods', { diff --git a/bun.lock b/bun.lock index e0f0b43..43e537d 100644 --- a/bun.lock +++ b/bun.lock @@ -2132,7 +2132,7 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@wispplace/bun-firehose/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@wispplace/bun-firehose/@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@wispplace/tiered-storage/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], @@ -2326,7 +2326,7 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@wispplace/bun-firehose/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@wispplace/bun-firehose/@types/bun/bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "@wispplace/tiered-storage/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], @@ -2508,8 +2508,6 @@ "@types/bun/bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@wispplace/bun-firehose/@types/bun/bun-types/@types/node": ["@types/node@24.10.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow=="], - "wispctl/@types/bun/bun-types/@types/node": ["@types/node@24.10.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow=="], "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], @@ -2538,8 +2536,6 @@ "@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], - "@wispplace/bun-firehose/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "wispctl/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], diff --git a/cli/commands/domain.ts b/cli/commands/domain.ts index 3850f0e..6e5beed 100644 --- a/cli/commands/domain.ts +++ b/cli/commands/domain.ts @@ -3,6 +3,7 @@ import type { OutputSchema as DomainClaimOutput } from '@wispplace/lexicons/type import type { OutputSchema as DomainClaimSubdomainOutput } from '@wispplace/lexicons/types/place/wisp/v2/domain/claimSubdomain' import type { OutputSchema as DomainDeleteOutput } from '@wispplace/lexicons/types/place/wisp/v2/domain/delete' import type { OutputSchema as DomainGetStatusOutput } from '@wispplace/lexicons/types/place/wisp/v2/domain/getStatus' +import type { OutputSchema as DomainVerifyOutput } from '@wispplace/lexicons/types/place/wisp/v2/domain/verify' import type { OutputSchema as SiteDeleteOutput } from '@wispplace/lexicons/types/place/wisp/v2/site/delete' import { authenticateForXrpc, type XrpcCommandOptions } from '../lib/command-utils.ts' import { createSpinner, pc } from '../lib/progress.ts' @@ -138,7 +139,7 @@ export async function deleteDomain( const spinner = createSpinner(`Deleting ${domain}...`).start() const data = await callWispXrpc(agent, 'place.wisp.v2.domain.delete', { serviceDid, - params: { domain }, + data: { domain }, }) spinner.succeed(`Deleted ${data.domain}`) @@ -150,6 +151,36 @@ export async function deleteDomain( console.log(`${pc.bold(data.domain)} deleted`) } +export async function verifyDomain( + identifier: string | undefined, + domain: string, + options: DomainCommandOptions, +): Promise { + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) + + const spinner = createSpinner(`Verifying ${domain}...`).start() + const data = await callWispXrpc(agent, 'place.wisp.v2.domain.verify', { + serviceDid, + data: { domain }, + }) + + if (data.verified) { + spinner.succeed(`Verified ${data.domain}`) + } else { + spinner.fail(`${data.domain} not verified${data.error ? `: ${data.error}` : ''}`) + } + + if (options.json) { + console.log(JSON.stringify(data, null, 2)) + return + } + + console.log(`${pc.bold(data.domain)} ${data.verified ? pc.green('verified') : pc.yellow(data.status)}`) + if (data.warning) { + console.log(pc.yellow(`warning: ${data.warning}`)) + } +} + export async function deleteSite( identifier: string | undefined, siteRkey: string, diff --git a/cli/index.ts b/cli/index.ts index f9b30bb..fd80fbb 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -10,6 +10,7 @@ import { deleteDomain, getDomainStatus, mapDomainToSite, + verifyDomain, } from './commands/domain.ts' import { listDomains, listSites } from './commands/list.ts' import { pull } from './commands/pull.ts' @@ -142,7 +143,10 @@ async function deleteSiteWithSelection( } } -program.name('wisp-cli').description('CLI for wisp.place - deploy static sites to the AT Protocol').version('1.1.2') +program + .name('wisp-cli') + .description('CLI for wisp.place - deploy static sites to the AT Protocol') + .version('1.1.2') .option('-q, --quiet', 'Suppress progress output — useful for CI/agents (also set via WISPCTL_NO_PROGRESS=1)') // Deploy command (default) @@ -423,6 +427,24 @@ addXrpcAuthOptions( }), ) +addXrpcAuthOptions( + domainCommand + .command('verify [handle]') + .description('Run DNS verification for a claimed custom domain') + .option('-d, --domain ', 'Domain'), +).action( + withExit(async (handle: string | undefined, options) => { + const domain = + options.domain ?? + (await promptRequiredText('Domain', { + placeholder: 'example.com', + cancelMessage: 'Verify cancelled', + validate: (value) => (!value ? 'Domain is required' : undefined), + })) + await verifyDomain(handle, domain, options) + }), +) + const siteCommand = program.command('site').description('Manage sites with wisp XRPC').enablePositionalOptions() addXrpcAuthOptions(domainCommand).action( @@ -435,6 +457,7 @@ addXrpcAuthOptions(domainCommand).action( { value: 'claim-subdomain', label: 'Claim wisp subdomain' }, { value: 'status', label: 'Get domain status' }, { value: 'add-site', label: 'Map domain to site' }, + { value: 'verify', label: 'Verify domain' }, { value: 'delete', label: 'Delete domain' }, ], 'Domain command cancelled', @@ -485,6 +508,16 @@ addXrpcAuthOptions(domainCommand).action( return } + if (action === 'verify') { + const domain = await promptRequiredText('Domain', { + placeholder: 'example.com', + cancelMessage: 'Verify cancelled', + validate: (value) => (!value ? 'Domain is required' : undefined), + }) + await verifyDomain(undefined, domain, options) + return + } + const domain = await promptRequiredText('Domain', { placeholder: 'example.com', cancelMessage: 'Delete cancelled', diff --git a/lexicons/domain-delete-v2.json b/lexicons/domain-delete-v2.json index 2acc0e1..5c06550 100644 --- a/lexicons/domain-delete-v2.json +++ b/lexicons/domain-delete-v2.json @@ -5,15 +5,18 @@ "main": { "type": "procedure", "description": "Delete a claimed domain owned by the authenticated DID.", - "parameters": { - "type": "params", - "required": ["domain"], - "properties": { - "domain": { - "type": "string", - "description": "Fully-qualified domain to delete (wisp subdomain or custom domain).", - "minLength": 3, - "maxLength": 253 + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["domain"], + "properties": { + "domain": { + "type": "string", + "description": "Fully-qualified domain to delete (wisp subdomain or custom domain).", + "minLength": 3, + "maxLength": 253 + } } } }, diff --git a/lexicons/domain-verify-v2.json b/lexicons/domain-verify-v2.json new file mode 100644 index 0000000..62d8ec8 --- /dev/null +++ b/lexicons/domain-verify-v2.json @@ -0,0 +1,69 @@ +{ + "lexicon": 1, + "id": "place.wisp.v2.domain.verify", + "defs": { + "main": { + "type": "procedure", + "description": "Run DNS verification for a custom domain owned by the authenticated DID and update its verified status.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["domain"], + "properties": { + "domain": { + "type": "string", + "description": "Custom domain FQDN to verify (for example, example.com).", + "minLength": 3, + "maxLength": 253 + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["domain", "kind", "status", "verified"], + "properties": { + "domain": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": ["custom"] + }, + "status": { + "type": "string", + "enum": ["pendingVerification", "verified"] + }, + "verified": { + "type": "boolean" + }, + "error": { + "type": "string", + "description": "Human-readable reason verification did not pass (when not verified)." + }, + "warning": { + "type": "string", + "description": "Non-fatal advisory (for example, CNAME could not be confirmed due to flattening)." + }, + "txtFound": { + "type": "string", + "description": "The TXT value observed during verification, if any." + }, + "cnameFound": { + "type": "string", + "description": "The CNAME target observed during verification, if any." + } + } + } + }, + "errors": [ + { "name": "AuthenticationRequired" }, + { "name": "InvalidDomain" }, + { "name": "NotFound" } + ] + } + } +} diff --git a/packages/@wispplace/lexicons/package.json b/packages/@wispplace/lexicons/package.json index e24a13c..6ee50ef 100644 --- a/packages/@wispplace/lexicons/package.json +++ b/packages/@wispplace/lexicons/package.json @@ -46,6 +46,10 @@ "types": "./src/types/place/wisp/v2/domain/delete.ts", "default": "./src/types/place/wisp/v2/domain/delete.ts" }, + "./types/place/wisp/v2/domain/verify": { + "types": "./src/types/place/wisp/v2/domain/verify.ts", + "default": "./src/types/place/wisp/v2/domain/verify.ts" + }, "./types/place/wisp/v2/domain/getList": { "types": "./src/types/place/wisp/v2/domain/getList.ts", "default": "./src/types/place/wisp/v2/domain/getList.ts" diff --git a/packages/@wispplace/lexicons/src/atcute/lexicons/index.ts b/packages/@wispplace/lexicons/src/atcute/lexicons/index.ts index b061129..bc8b057 100644 --- a/packages/@wispplace/lexicons/src/atcute/lexicons/index.ts +++ b/packages/@wispplace/lexicons/src/atcute/lexicons/index.ts @@ -7,6 +7,7 @@ export * as PlaceWispV2DomainClaimSubdomain from "./types/place/wisp/v2/domain/c export * as PlaceWispV2DomainDelete from "./types/place/wisp/v2/domain/delete.js"; export * as PlaceWispV2DomainGetList from "./types/place/wisp/v2/domain/getList.js"; export * as PlaceWispV2DomainGetStatus from "./types/place/wisp/v2/domain/getStatus.js"; +export * as PlaceWispV2DomainVerify from "./types/place/wisp/v2/domain/verify.js"; export * as PlaceWispV2Domains from "./types/place/wisp/v2/domains.js"; export * as PlaceWispV2SecretCreate from "./types/place/wisp/v2/secret/create.js"; export * as PlaceWispV2SecretDelete from "./types/place/wisp/v2/secret/delete.js"; diff --git a/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/delete.ts b/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/delete.ts index 75fd352..fc1abb2 100644 --- a/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/delete.ts +++ b/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/delete.ts @@ -3,17 +3,20 @@ import * as v from "@atcute/lexicons/validations"; import type {} from "@atcute/lexicons/ambient"; const _mainSchema = /*#__PURE__*/ v.procedure("place.wisp.v2.domain.delete", { - params: /*#__PURE__*/ v.object({ - /** - * Fully-qualified domain to delete (wisp subdomain or custom domain). - * @minLength 3 - * @maxLength 253 - */ - domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ - /*#__PURE__*/ v.stringLength(3, 253), - ]), - }), - input: null, + params: null, + input: { + type: "lex", + schema: /*#__PURE__*/ v.object({ + /** + * Fully-qualified domain to delete (wisp subdomain or custom domain). + * @minLength 3 + * @maxLength 253 + */ + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ + /*#__PURE__*/ v.stringLength(3, 253), + ]), + }), + }, output: { type: "lex", schema: /*#__PURE__*/ v.object({ @@ -29,7 +32,8 @@ export interface mainSchema extends main$schematype {} export const mainSchema = _mainSchema as mainSchema; -export interface $params extends v.InferInput {} +export interface $params {} +export interface $input extends v.InferXRPCBodyInput {} export interface $output extends v.InferXRPCBodyInput {} declare module "@atcute/lexicons/ambient" { diff --git a/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/verify.ts b/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/verify.ts new file mode 100644 index 0000000..7a83c29 --- /dev/null +++ b/packages/@wispplace/lexicons/src/atcute/lexicons/types/place/wisp/v2/domain/verify.ts @@ -0,0 +1,61 @@ +import type {} from "@atcute/lexicons"; +import * as v from "@atcute/lexicons/validations"; +import type {} from "@atcute/lexicons/ambient"; + +const _mainSchema = /*#__PURE__*/ v.procedure("place.wisp.v2.domain.verify", { + params: null, + input: { + type: "lex", + schema: /*#__PURE__*/ v.object({ + /** + * Custom domain FQDN to verify (for example, example.com). + * @minLength 3 + * @maxLength 253 + */ + domain: /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.string(), [ + /*#__PURE__*/ v.stringLength(3, 253), + ]), + }), + }, + output: { + type: "lex", + schema: /*#__PURE__*/ v.object({ + /** + * The CNAME target observed during verification, if any. + */ + cnameFound: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), + domain: /*#__PURE__*/ v.string(), + /** + * Human-readable reason verification did not pass (when not verified). + */ + error: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), + kind: /*#__PURE__*/ v.literalEnum(["custom"]), + status: /*#__PURE__*/ v.literalEnum(["pendingVerification", "verified"]), + /** + * The TXT value observed during verification, if any. + */ + txtFound: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), + verified: /*#__PURE__*/ v.boolean(), + /** + * Non-fatal advisory (for example, CNAME could not be confirmed due to flattening). + */ + warning: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), + }), + }, +}); + +type main$schematype = typeof _mainSchema; + +export interface mainSchema extends main$schematype {} + +export const mainSchema = _mainSchema as mainSchema; + +export interface $params {} +export interface $input extends v.InferXRPCBodyInput {} +export interface $output extends v.InferXRPCBodyInput {} + +declare module "@atcute/lexicons/ambient" { + interface XRPCProcedures { + "place.wisp.v2.domain.verify": mainSchema; + } +} diff --git a/packages/@wispplace/lexicons/src/index.ts b/packages/@wispplace/lexicons/src/index.ts index de073e2..336db68 100644 --- a/packages/@wispplace/lexicons/src/index.ts +++ b/packages/@wispplace/lexicons/src/index.ts @@ -16,6 +16,7 @@ import * as PlaceWispV2DomainClaim from './types/place/wisp/v2/domain/claim.js' import * as PlaceWispV2DomainDelete from './types/place/wisp/v2/domain/delete.js' import * as PlaceWispV2DomainGetList from './types/place/wisp/v2/domain/getList.js' import * as PlaceWispV2DomainGetStatus from './types/place/wisp/v2/domain/getStatus.js' +import * as PlaceWispV2DomainVerify from './types/place/wisp/v2/domain/verify.js' import * as PlaceWispV2SecretCreate from './types/place/wisp/v2/secret/create.js' import * as PlaceWispV2SecretDelete from './types/place/wisp/v2/secret/delete.js' import * as PlaceWispV2SecretList from './types/place/wisp/v2/secret/list.js' @@ -150,6 +151,18 @@ export class PlaceWispV2DomainNS { const nsid = 'place.wisp.v2.domain.getStatus' // @ts-ignore return this._server.xrpc.method(nsid, cfg) } + + verify( + cfg: MethodConfigOrHandler< + A, + PlaceWispV2DomainVerify.QueryParams, + PlaceWispV2DomainVerify.HandlerInput, + PlaceWispV2DomainVerify.HandlerOutput + >, + ) { + const nsid = 'place.wisp.v2.domain.verify' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } export class PlaceWispV2SecretNS { diff --git a/packages/@wispplace/lexicons/src/lexicons.ts b/packages/@wispplace/lexicons/src/lexicons.ts index ce5119d..c0472c8 100644 --- a/packages/@wispplace/lexicons/src/lexicons.ts +++ b/packages/@wispplace/lexicons/src/lexicons.ts @@ -263,16 +263,19 @@ export const schemaDict = { main: { type: 'procedure', description: 'Delete a claimed domain owned by the authenticated DID.', - parameters: { - type: 'params', - required: ['domain'], - properties: { - domain: { - type: 'string', - description: - 'Fully-qualified domain to delete (wisp subdomain or custom domain).', - minLength: 3, - maxLength: 253, + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['domain'], + properties: { + domain: { + type: 'string', + description: + 'Fully-qualified domain to delete (wisp subdomain or custom domain).', + minLength: 3, + maxLength: 253, + }, }, }, }, @@ -435,6 +438,87 @@ export const schemaDict = { }, }, }, + PlaceWispV2DomainVerify: { + lexicon: 1, + id: 'place.wisp.v2.domain.verify', + defs: { + main: { + type: 'procedure', + description: + 'Run DNS verification for a custom domain owned by the authenticated DID and update its verified status.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['domain'], + properties: { + domain: { + type: 'string', + description: + 'Custom domain FQDN to verify (for example, example.com).', + minLength: 3, + maxLength: 253, + }, + }, + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + required: ['domain', 'kind', 'status', 'verified'], + properties: { + domain: { + type: 'string', + }, + kind: { + type: 'string', + enum: ['custom'], + }, + status: { + type: 'string', + enum: ['pendingVerification', 'verified'], + }, + verified: { + type: 'boolean', + }, + error: { + type: 'string', + description: + 'Human-readable reason verification did not pass (when not verified).', + }, + warning: { + type: 'string', + description: + 'Non-fatal advisory (for example, CNAME could not be confirmed due to flattening).', + }, + txtFound: { + type: 'string', + description: + 'The TXT value observed during verification, if any.', + }, + cnameFound: { + type: 'string', + description: + 'The CNAME target observed during verification, if any.', + }, + }, + }, + }, + errors: [ + { + name: 'AuthenticationRequired', + }, + { + name: 'InvalidDomain', + }, + { + name: 'NotFound', + }, + ], + }, + }, + }, PlaceWispV2Domains: { lexicon: 1, id: 'place.wisp.v2.domains', @@ -1398,6 +1482,7 @@ export const ids = { PlaceWispV2DomainDelete: 'place.wisp.v2.domain.delete', PlaceWispV2DomainGetList: 'place.wisp.v2.domain.getList', PlaceWispV2DomainGetStatus: 'place.wisp.v2.domain.getStatus', + PlaceWispV2DomainVerify: 'place.wisp.v2.domain.verify', PlaceWispV2Domains: 'place.wisp.v2.domains', PlaceWispFs: 'place.wisp.fs', PlaceWispV2SecretCreate: 'place.wisp.v2.secret.create', diff --git a/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/delete.ts b/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/delete.ts index f7016ba..2a95c32 100644 --- a/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/delete.ts +++ b/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/delete.ts @@ -14,18 +14,22 @@ const is$typed = _is$typed, validate = _validate const id = 'place.wisp.v2.domain.delete' -export type QueryParams = { +export type QueryParams = {} + +export interface InputSchema { /** Fully-qualified domain to delete (wisp subdomain or custom domain). */ domain: string } -export type InputSchema = undefined export interface OutputSchema { domain: string deleted: true } -export type HandlerInput = void +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} export interface HandlerSuccess { encoding: 'application/json' diff --git a/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/verify.ts b/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/verify.ts new file mode 100644 index 0000000..06f11e5 --- /dev/null +++ b/packages/@wispplace/lexicons/src/types/place/wisp/v2/domain/verify.ts @@ -0,0 +1,56 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../../../lexicons' +import { + type $Typed, + is$typed as _is$typed, + type OmitKey, +} from '../../../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'place.wisp.v2.domain.verify' + +export type QueryParams = {} + +export interface InputSchema { + /** Custom domain FQDN to verify (for example, example.com). */ + domain: string +} + +export interface OutputSchema { + domain: string + kind: 'custom' + status: 'pendingVerification' | 'verified' + verified: boolean + /** Human-readable reason verification did not pass (when not verified). */ + error?: string + /** Non-fatal advisory (for example, CNAME could not be confirmed due to flattening). */ + warning?: string + /** The TXT value observed during verification, if any. */ + txtFound?: string + /** The CNAME target observed during verification, if any. */ + cnameFound?: string +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string + error?: 'AuthenticationRequired' | 'InvalidDomain' | 'NotFound' +} + +export type HandlerOutput = HandlerError | HandlerSuccess