For power users
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