DNS handles (#555)
* change pkey on user table to did & rename to user_account * migration * tidy * fixes suggested by bryn * missed merge thing * some updating hanldes scheams * impl + passing test * more handle tests * tidy * update did doc + some new tests * one more test * test handle casing * basic support for dns resolution on handles * handle resolution * fix up account create * tsconfig * workshop handle constraints * bring pds up to speed w handle changes * change dns subdomain * another lil test * bugfix * update dns record format * typo
This commit is contained in:
parent
e3cd9c23a1
commit
d4b1262f28
packages
handle
pds
@ -19,6 +19,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@atproto/common": "*",
|
||||
"@sideway/address": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,25 @@
|
||||
import * as address from '@sideway/address'
|
||||
import { reservedSubdomains } from './reserved'
|
||||
|
||||
export const ensureValid = (
|
||||
handle: string,
|
||||
availableUserDomains: string[],
|
||||
): void => {
|
||||
export * from './resolve'
|
||||
|
||||
export const ensureValid = (handle: string): void => {
|
||||
if (handle.startsWith('did:')) {
|
||||
throw new InvalidHandleError(
|
||||
'Cannot register a handle that starts with `did:`',
|
||||
)
|
||||
}
|
||||
const supportedDomain = availableUserDomains.find((domain) =>
|
||||
handle.endsWith(domain),
|
||||
)
|
||||
if (!supportedDomain) {
|
||||
throw new InvalidHandleError('Not a supported handle domain')
|
||||
}
|
||||
const front = handle.slice(0, handle.length - supportedDomain.length)
|
||||
if (front.length < 3) {
|
||||
throw new InvalidHandleError('Handle too short')
|
||||
} else if (front.length > 20) {
|
||||
throw new InvalidHandleError('Handle too long')
|
||||
} else if (handle.length > 253) {
|
||||
|
||||
if (handle.length > 40) {
|
||||
throw new InvalidHandleError('Handle too long')
|
||||
}
|
||||
|
||||
handle.split('.').map((domainLabel) => {
|
||||
if (domainLabel.length > 63) {
|
||||
if (domainLabel.length > 20) {
|
||||
throw new InvalidHandleError('Handle too long')
|
||||
}
|
||||
})
|
||||
|
||||
if (reservedSubdomains[front]) {
|
||||
throw new ReservedHandleError('Reserved handle')
|
||||
}
|
||||
|
||||
if (front.indexOf('.') > -1) {
|
||||
throw new InvalidHandleError('Invalid characters in handle')
|
||||
}
|
||||
|
||||
const isValid = address.isDomainValid(handle)
|
||||
if (!isValid) {
|
||||
throw new InvalidHandleError('Invalid characters in handle')
|
||||
@ -54,25 +35,59 @@ export const normalize = (handle: string): string => {
|
||||
return handle.toLowerCase()
|
||||
}
|
||||
|
||||
export const normalizeAndEnsureValid = (
|
||||
handle: string,
|
||||
availableUserDomains: string[],
|
||||
): string => {
|
||||
export const normalizeAndEnsureValid = (handle: string): string => {
|
||||
const normalized = normalize(handle)
|
||||
ensureValid(normalized, availableUserDomains)
|
||||
ensureValid(normalized)
|
||||
return normalized
|
||||
}
|
||||
|
||||
export const isValid = (
|
||||
export const isValid = (handle: string): boolean => {
|
||||
try {
|
||||
ensureValid(handle)
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidHandleError) {
|
||||
return false
|
||||
}
|
||||
throw err
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const ensureServiceConstraints = (
|
||||
handle: string,
|
||||
availableUserDomains: string[],
|
||||
reserved = reservedSubdomains,
|
||||
): void => {
|
||||
const supportedDomain = availableUserDomains.find((domain) =>
|
||||
handle.endsWith(domain),
|
||||
)
|
||||
if (!supportedDomain) {
|
||||
throw new UnsupportedDomainError('Not a supported handle domain')
|
||||
}
|
||||
const front = handle.slice(0, handle.length - supportedDomain.length)
|
||||
if (front.indexOf('.') > -1) {
|
||||
throw new InvalidHandleError('Invalid characters in handle')
|
||||
}
|
||||
if (front.length < 3) {
|
||||
throw new InvalidHandleError('Handle too short')
|
||||
}
|
||||
if (reserved[front]) {
|
||||
throw new ReservedHandleError('Reserved handle')
|
||||
}
|
||||
}
|
||||
|
||||
export const fulfillsServiceConstraints = (
|
||||
handle: string,
|
||||
availableUserDomains: string[],
|
||||
reserved = reservedSubdomains,
|
||||
): boolean => {
|
||||
try {
|
||||
ensureValid(handle, availableUserDomains)
|
||||
ensureServiceConstraints(handle, availableUserDomains, reserved)
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof InvalidHandleError ||
|
||||
err instanceof ReservedHandleError
|
||||
err instanceof ReservedHandleError ||
|
||||
err instanceof UnsupportedDomainError
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@ -83,3 +98,4 @@ export const isValid = (
|
||||
|
||||
export class InvalidHandleError extends Error {}
|
||||
export class ReservedHandleError extends Error {}
|
||||
export class UnsupportedDomainError extends Error {}
|
||||
|
23
packages/handle/src/resolve.ts
Normal file
23
packages/handle/src/resolve.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { isErrnoException } from '@atproto/common'
|
||||
import dns from 'dns/promises'
|
||||
|
||||
const SUBDOMAIN = '_atproto'
|
||||
const PREFIX = 'did='
|
||||
|
||||
export const resolveDns = async (handle: string): Promise<string> => {
|
||||
let chunkedResults: string[][]
|
||||
try {
|
||||
chunkedResults = await dns.resolveTxt(`${SUBDOMAIN}.${handle}`)
|
||||
} catch (err) {
|
||||
if (isErrnoException(err) && err.code === 'ENOTFOUND') {
|
||||
throw new NoHandleRecordError()
|
||||
}
|
||||
throw err
|
||||
}
|
||||
const results = chunkedResults.map((chunks) => chunks.join(''))
|
||||
const found = results.find((i) => i.startsWith(PREFIX))
|
||||
if (!found) throw new NoHandleRecordError()
|
||||
return found.slice(PREFIX.length)
|
||||
}
|
||||
|
||||
export class NoHandleRecordError extends Error {}
|
@ -1,78 +1,98 @@
|
||||
import { ensureValid, normalizeAndEnsureValid } from '../src'
|
||||
import {
|
||||
ensureValid,
|
||||
ensureServiceConstraints,
|
||||
normalizeAndEnsureValid,
|
||||
} from '../src'
|
||||
|
||||
describe('handle validation', () => {
|
||||
const domains = ['.bsky.app', '.test']
|
||||
const check = (toCheck: string) => () => {
|
||||
return ensureValid(toCheck, domains)
|
||||
}
|
||||
|
||||
it('allows valid handles', () => {
|
||||
check('john.test')
|
||||
check('jan.test')
|
||||
check('a234567890123456789.test')
|
||||
check('john2.test')
|
||||
check('john-john.test')
|
||||
check('john-.test')
|
||||
check('john.bsky.app')
|
||||
check('0john.test')
|
||||
check('12345.test')
|
||||
check(
|
||||
'short.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.test',
|
||||
)
|
||||
ensureValid('john.test')
|
||||
ensureValid('jan.test')
|
||||
ensureValid('a234567890123456789.test')
|
||||
ensureValid('john2.test')
|
||||
ensureValid('john-john.test')
|
||||
ensureValid('john.bsky.app')
|
||||
ensureValid('0john.test')
|
||||
ensureValid('12345.test')
|
||||
ensureValid('this.has.many.sub.domains.test')
|
||||
})
|
||||
|
||||
it('allows handles that pass service constraints', () => {
|
||||
ensureServiceConstraints('john.test', domains)
|
||||
ensureServiceConstraints('jan.test', domains)
|
||||
ensureServiceConstraints('a234567890123456789.test', domains)
|
||||
ensureServiceConstraints('john2.test', domains)
|
||||
ensureServiceConstraints('john-john.test', domains)
|
||||
ensureServiceConstraints('john.bsky.app', domains)
|
||||
ensureServiceConstraints('0john.test', domains)
|
||||
ensureServiceConstraints('12345.test', domains)
|
||||
})
|
||||
|
||||
it('allows punycode handles', () => {
|
||||
check('xn--ls8h.test') // 💩.test
|
||||
check('xn--bcher-kva.tld') // bücher.tld
|
||||
ensureValid('xn--ls8h.test') // 💩.test
|
||||
ensureValid('xn--bcher-kva.tld') // bücher.tld
|
||||
})
|
||||
|
||||
it('throws on invalid handles', () => {
|
||||
expect(check('did:john.test')).toThrow(
|
||||
const expectThrow = (handle: string, err: string) => {
|
||||
expect(() => ensureValid(handle)).toThrow(err)
|
||||
}
|
||||
|
||||
expectThrow(
|
||||
'did:john.test',
|
||||
'Cannot register a handle that starts with `did:`',
|
||||
)
|
||||
expect(check('john.bsky.io')).toThrow('Not a supported handle domain')
|
||||
expect(check('john.com')).toThrow('Not a supported handle domain')
|
||||
expect(check('j.test')).toThrow('Handle too short')
|
||||
expect(check('uk.test')).toThrow('Handle too short')
|
||||
expect(check('jaymome-johnber123456.test')).toThrow('Handle too long')
|
||||
expect(check('jay.mome-johnber123456.test')).toThrow('Handle too long')
|
||||
expect(
|
||||
check(
|
||||
'short.loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.test',
|
||||
),
|
||||
).toThrow('Handle too long')
|
||||
expect(
|
||||
check(
|
||||
'short.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.loooooooooooooooooooooooong.test',
|
||||
),
|
||||
).toThrow('Handle too long')
|
||||
expect(check('john.test.bsky.app')).toThrow('Invalid characters in handle')
|
||||
expect(check('john..test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo_hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('-john.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('.john.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo!hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo%hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo&hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo@hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo*hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo|hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo:hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('jo/hn.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('john💩.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('bücher.test')).toThrow('Invalid characters in handle')
|
||||
expect(check('about.test')).toThrow('Reserved handle')
|
||||
expect(check('atp.test')).toThrow('Reserved handle')
|
||||
expect(check('barackobama.test')).toThrow('Reserved handle')
|
||||
expectThrow('jaymome-johnber123456.test', 'Handle too long')
|
||||
expectThrow('jay.mome-johnber1234567890.subdomain.test', 'Handle too long')
|
||||
expectThrow(
|
||||
'short.loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.test',
|
||||
'Handle too long',
|
||||
)
|
||||
expectThrow(
|
||||
'short.short.short.short.short.short.short.test',
|
||||
'Handle too long',
|
||||
)
|
||||
expectThrow('john..test', 'Invalid characters in handle')
|
||||
expectThrow('jo_hn.test', 'Invalid characters in handle')
|
||||
expectThrow('-john.test', 'Invalid characters in handle')
|
||||
expectThrow('john-.test', 'Invalid characters in handle')
|
||||
expectThrow('.john.test', 'Invalid characters in handle')
|
||||
expectThrow('jo!hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo%hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo&hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo@hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo*hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo|hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo:hn.test', 'Invalid characters in handle')
|
||||
expectThrow('jo/hn.test', 'Invalid characters in handle')
|
||||
expectThrow('john💩.test', 'Invalid characters in handle')
|
||||
expectThrow('bücher.test', 'Invalid characters in handle')
|
||||
})
|
||||
|
||||
it('throw on handles that violate service constraints', () => {
|
||||
const expectThrow = (handle: string, err: string) => {
|
||||
expect(() => ensureServiceConstraints(handle, domains)).toThrow(err)
|
||||
}
|
||||
|
||||
expectThrow('john.bsky.io', 'Not a supported handle domain')
|
||||
expectThrow('john.com', 'Not a supported handle domain')
|
||||
expectThrow('j.test', 'Handle too short')
|
||||
expectThrow('uk.test', 'Handle too short')
|
||||
expectThrow('john.test.bsky.app', 'Invalid characters in handle')
|
||||
expectThrow('about.test', 'Reserved handle')
|
||||
expectThrow('atp.test', 'Reserved handle')
|
||||
expectThrow('barackobama.test', 'Reserved handle')
|
||||
})
|
||||
|
||||
it('normalizes handles', () => {
|
||||
const normalized = normalizeAndEnsureValid('JoHn.TeST', domains)
|
||||
const normalized = normalizeAndEnsureValid('JoHn.TeST')
|
||||
expect(normalized).toBe('john.test')
|
||||
})
|
||||
|
||||
it('throws on invalid normalized handles', () => {
|
||||
expect(() => normalizeAndEnsureValid('JoH!n.TeST', domains)).toThrow(
|
||||
expect(() => normalizeAndEnsureValid('JoH!n.TeST')).toThrow(
|
||||
'Invalid characters in handle',
|
||||
)
|
||||
})
|
||||
|
48
packages/handle/tests/resolve.test.ts
Normal file
48
packages/handle/tests/resolve.test.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { NoHandleRecordError, resolveDns } from '../src'
|
||||
|
||||
jest.mock('dns/promises', () => {
|
||||
return {
|
||||
resolveTxt: (handle: string) => {
|
||||
if (handle === '_atproto.simple.test') {
|
||||
return [['did=did:example:simpleDid']]
|
||||
}
|
||||
if (handle === '_atproto.noisy.test') {
|
||||
return [
|
||||
['blah blah blah'],
|
||||
['did:example:fakeDid'],
|
||||
['atproto=did:example:fakeDid'],
|
||||
['did=did:example:noisyDid'],
|
||||
[
|
||||
'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',
|
||||
'apsodfiuweproiasudfpoasidfu',
|
||||
],
|
||||
]
|
||||
}
|
||||
if (handle === '_atproto.bad.test') {
|
||||
return [
|
||||
['blah blah blah'],
|
||||
['did:example:fakeDid'],
|
||||
['atproto=did:example:fakeDid'],
|
||||
[
|
||||
'chunk long domain aspdfoiuwerpoaisdfupasodfiuaspdfoiuasdpfoiausdfpaosidfuaspodifuaspdfoiuasdpfoiasudfpasodifuaspdofiuaspdfoiuasd',
|
||||
'apsodfiuweproiasudfpoasidfu',
|
||||
],
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('handle resolution', () => {
|
||||
it('handles a simple DNS resolution', async () => {
|
||||
const did = await resolveDns('simple.test')
|
||||
expect(did).toBe('did:example:simpleDid')
|
||||
})
|
||||
it('handles a noisy DNS resolution', async () => {
|
||||
const did = await resolveDns('noisy.test')
|
||||
expect(did).toBe('did:example:noisyDid')
|
||||
})
|
||||
it('handles a bad DNS resolution', async () => {
|
||||
await expect(resolveDns('bad.test')).rejects.toThrow(NoHandleRecordError)
|
||||
})
|
||||
})
|
@ -6,4 +6,7 @@
|
||||
"emitDeclarationOnly": true
|
||||
},
|
||||
"include": ["./src","__tests__/**/**.ts"],
|
||||
"references": [
|
||||
{ "path": "../common/tsconfig.build.json" },
|
||||
]
|
||||
}
|
@ -26,6 +26,7 @@
|
||||
"postpublish": "npm run update-main-to-src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "*",
|
||||
"@atproto/common": "*",
|
||||
"@atproto/crypto": "*",
|
||||
"@atproto/did-resolver": "*",
|
||||
|
@ -14,15 +14,15 @@ export default function (server: Server, ctx: AppContext) {
|
||||
|
||||
let handle: string
|
||||
try {
|
||||
handle = handleLib.normalizeAndEnsureValid(
|
||||
input.body.handle,
|
||||
ctx.cfg.availableUserDomains,
|
||||
)
|
||||
handle = handleLib.normalizeAndEnsureValid(input.body.handle)
|
||||
handleLib.ensureServiceConstraints(handle, ctx.cfg.availableUserDomains)
|
||||
} catch (err) {
|
||||
if (err instanceof handleLib.InvalidHandleError) {
|
||||
throw new InvalidRequestError(err.message, 'InvalidHandle')
|
||||
} else if (err instanceof handleLib.ReservedHandleError) {
|
||||
throw new InvalidRequestError(err.message, 'HandleNotAvailable')
|
||||
} else if (err instanceof handleLib.UnsupportedDomainError) {
|
||||
throw new InvalidRequestError(err.message, 'UnsupportedDomain')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import ApiAgent from '@atproto/api'
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import * as handleLib from '@atproto/handle'
|
||||
import { Server } from '../../../lexicon'
|
||||
import AppContext from '../../../context'
|
||||
import { UserAlreadyExistsError } from '../../../services/actor'
|
||||
import { httpLogger as log } from '../../../logger'
|
||||
import { XRPCError } from '@atproto/xrpc'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.handle.resolve(async ({ params }) => {
|
||||
@ -13,17 +16,25 @@ export default function (server: Server, ctx: AppContext) {
|
||||
// self
|
||||
did = ctx.cfg.serverDid
|
||||
} else {
|
||||
const supportedHandle = ctx.cfg.availableUserDomains.some((host) =>
|
||||
handle.endsWith(host),
|
||||
)
|
||||
if (!supportedHandle) {
|
||||
throw new InvalidRequestError('Not a supported handle domain')
|
||||
}
|
||||
const user = await ctx.services.actor(ctx.db).getUser(handle, true)
|
||||
if (!user) {
|
||||
throw new InvalidRequestError('Unable to resolve handle')
|
||||
if (user) {
|
||||
did = user.did
|
||||
} else {
|
||||
const supportedHandle = ctx.cfg.availableUserDomains.some((host) =>
|
||||
handle.endsWith(host),
|
||||
)
|
||||
// this should be in our DB & we couldn't find it, so fail
|
||||
if (supportedHandle) {
|
||||
throw new InvalidRequestError('Unable to resolve handle')
|
||||
}
|
||||
|
||||
// this is not someone on our server, but we help with resolving anyway
|
||||
const resolved = await resolveExternalHandle(ctx.cfg.scheme, handle)
|
||||
if (!resolved) {
|
||||
throw new InvalidRequestError('Unable to resolve handle')
|
||||
}
|
||||
did = resolved
|
||||
}
|
||||
did = user.did
|
||||
}
|
||||
|
||||
return {
|
||||
@ -38,19 +49,35 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const requester = auth.credentials.did
|
||||
let handle: string
|
||||
try {
|
||||
handle = handleLib.normalizeAndEnsureValid(
|
||||
input.body.handle,
|
||||
ctx.cfg.availableUserDomains,
|
||||
)
|
||||
handle = handleLib.normalizeAndEnsureValid(input.body.handle)
|
||||
} catch (err) {
|
||||
if (err instanceof handleLib.InvalidHandleError) {
|
||||
throw new InvalidRequestError(err.message, 'InvalidHandle')
|
||||
} else if (err instanceof handleLib.ReservedHandleError) {
|
||||
throw new InvalidRequestError(err.message, 'HandleNotAvailable')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
// test against our service constraints
|
||||
// if not a supported domain, then we must check that the domain correctly links to the DID
|
||||
try {
|
||||
handleLib.ensureServiceConstraints(handle, ctx.cfg.availableUserDomains)
|
||||
} catch (err) {
|
||||
if (err instanceof handleLib.UnsupportedDomainError) {
|
||||
const did = await resolveExternalHandle(ctx.cfg.scheme, handle)
|
||||
if (did !== requester) {
|
||||
throw new InvalidRequestError(
|
||||
'External handle did not resolve to DID',
|
||||
)
|
||||
}
|
||||
} else if (err instanceof handleLib.InvalidHandleError) {
|
||||
throw new InvalidRequestError(err.message, 'InvalidHandle')
|
||||
} else if (err instanceof handleLib.ReservedHandleError) {
|
||||
throw new InvalidRequestError(err.message, 'HandleNotAvailable')
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.db.transaction(async (dbTxn) => {
|
||||
try {
|
||||
await ctx.services.actor(dbTxn).updateHandle(requester, handle)
|
||||
@ -60,9 +87,34 @@ export default function (server: Server, ctx: AppContext) {
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
await ctx.plcClient.updateHandle(requester, handle, ctx.keypair)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const resolveExternalHandle = async (
|
||||
scheme: string,
|
||||
handle: string,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const did = await handleLib.resolveDns(handle)
|
||||
return did
|
||||
} catch (err) {
|
||||
if (err instanceof handleLib.NoHandleRecordError) {
|
||||
// no worries it's just not found
|
||||
} else {
|
||||
log.error({ err, handle }, 'could not resolve dns handle')
|
||||
}
|
||||
}
|
||||
try {
|
||||
const agent = new ApiAgent({ service: `${scheme}://${handle}` })
|
||||
const res = await agent.api.com.atproto.handle.resolve({ handle })
|
||||
return res.data.did
|
||||
} catch (err) {
|
||||
if (err instanceof XRPCError) {
|
||||
return null
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,30 @@ import basicSeed from './seeds/basic'
|
||||
import * as util from './_util'
|
||||
import { AppContext } from '../src'
|
||||
|
||||
// outside of suite so they can be used in mock
|
||||
let alice: string
|
||||
let bob: string
|
||||
|
||||
jest.mock('dns/promises', () => {
|
||||
return {
|
||||
resolveTxt: (domain: string) => {
|
||||
if (domain === '_atproto.alice.external') {
|
||||
return [[`did=${alice}`]]
|
||||
}
|
||||
if (domain === '_atproto.bob.external') {
|
||||
return [[`did=${bob}`]]
|
||||
}
|
||||
return []
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('handles', () => {
|
||||
let agent: AtpAgent
|
||||
let close: util.CloseFn
|
||||
let sc: SeedClient
|
||||
let ctx: AppContext
|
||||
|
||||
let alice: string
|
||||
let bob: string
|
||||
const newHandle = 'alice2.test'
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -120,7 +136,7 @@ describe('handles', () => {
|
||||
'Cannot register a handle that starts with `did:`',
|
||||
)
|
||||
await expect(tryHandle('john.bsky.io')).rejects.toThrow(
|
||||
'Not a supported handle domain',
|
||||
'External handle did not resolve to DID',
|
||||
)
|
||||
await expect(tryHandle('j.test')).rejects.toThrow('Handle too short')
|
||||
await expect(tryHandle('jayromy-johnber123456.test')).rejects.toThrow(
|
||||
@ -153,4 +169,49 @@ describe('handles', () => {
|
||||
await expect(tryHandle('about.test')).rejects.toThrow('Reserved handle')
|
||||
await expect(tryHandle('atp.test')).rejects.toThrow('Reserved handle')
|
||||
})
|
||||
|
||||
it('allows updating to a dns handles', async () => {
|
||||
await agent.api.com.atproto.handle.update(
|
||||
{
|
||||
handle: 'alice.external',
|
||||
},
|
||||
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
||||
)
|
||||
const profile = await agent.api.app.bsky.actor.getProfile(
|
||||
{ actor: alice },
|
||||
{ headers: sc.getHeaders(bob) },
|
||||
)
|
||||
expect(profile.data.handle).toBe('alice.external')
|
||||
|
||||
const data = await ctx.plcClient.getDocumentData(alice)
|
||||
expect(data.handle).toBe('alice.external')
|
||||
})
|
||||
|
||||
it('does not allow updating to an invalid dns handle', async () => {
|
||||
const attempt = agent.api.com.atproto.handle.update(
|
||||
{
|
||||
handle: 'bob.external',
|
||||
},
|
||||
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
||||
)
|
||||
await expect(attempt).rejects.toThrow(
|
||||
'External handle did not resolve to DID',
|
||||
)
|
||||
|
||||
const attempt2 = agent.api.com.atproto.handle.update(
|
||||
{
|
||||
handle: 'noexist.external',
|
||||
},
|
||||
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
||||
)
|
||||
await expect(attempt2).rejects.toThrow(
|
||||
'External handle did not resolve to DID',
|
||||
)
|
||||
|
||||
const profile = await agent.api.app.bsky.actor.getProfile(
|
||||
{ actor: alice },
|
||||
{ headers: sc.getHeaders(bob) },
|
||||
)
|
||||
expect(profile.data.handle).toBe('alice.external')
|
||||
})
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user