b934b396b1
* feat(api): support creation of oauth based AtpAgents * oauth: misc fixes for confidential clients * fix(xprc): remove ReadableStream.from polyfill * OAuth docs tweaks (#2679) * OAuth: clarification about client_name being shown * OAuth: re-write handle resolution privacy concern * avoid relying on ReadableStream.from in xrpc-server tests * feat(oauth-types): expose "ALLOW_UNSECURE_ORIGINS" constant * feat(handle-resolver): expose "AtprotoIdentityDidMethods" type * fix(oauth-client): ensure that the oauth metadata document contains client_id_metadata_document_supported * fix(oauth-types): prevent unknown query string in loopback client id * fix(identity-resolver): check that handle is in did doc's "alsoKnownAs" * feat(oauth-client:oauth-resolver): allow logging in using either the PDS URL or Entryway URL * fix(oauth-client): return better error in case of invalid "oauth-protected-resource" status code * refactor(did): group atproto specific checks in own * feat(api): relax typing of "appLabelers" and "labelers" AtpClient properties * allow any did as labeller (for tests mainly) * fix(api): allow to override "atproto-proxy" on a per-request basis * remove release candidate versions from changelog * update changeset for api and xrpc packages * Add missing changeset * revert RC versions * Proper wording in OAUTH.md api example * remove "pre" changeset file * xrpc: restore original behavior of setHEader and unsetHeader * docs: add comment for XrpcClient 's constructor arg * feat(api): expose "schemas" publicly * feat(api): allow customizing the whatwg fetch function of the AtpAgent * docs(api): improve migration docs * docs: change reference to BskyAgent to AtpAgent * docs: mention the breaking change regarding setSessionPersistHandler * fix(api): better split AtpClient concerns * fix(xrpc): remove unused import * refactor(api): simplify class hierarchu by removeing AtpClient * fix(api): mock proper method for facets detection * restore ability to restore session asynchronously * feat(api): allow instantiating Agent with same argument as super class * docs(api): properly extend Agent class * style(xrpc): var name * docs(api): remove "async" to header getter --------- Co-authored-by: Devin Ivy <devinivy@gmail.com> Co-authored-by: bnewbold <bnewbold@robocracy.org> Co-authored-by: Hailey <me@haileyok.com>
255 lines
7.7 KiB
TypeScript
255 lines
7.7 KiB
TypeScript
import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env'
|
|
import { AtpAgent } from '@atproto/api'
|
|
import { IdResolver } from '@atproto/identity'
|
|
import basicSeed from './seeds/basic'
|
|
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 network: TestNetworkNoAppView
|
|
let agent: AtpAgent
|
|
let sc: SeedClient
|
|
let ctx: AppContext
|
|
let idResolver: IdResolver
|
|
|
|
const newHandle = 'alice2.test'
|
|
|
|
beforeAll(async () => {
|
|
network = await TestNetworkNoAppView.create({
|
|
dbPostgresSchema: 'handles',
|
|
})
|
|
// @ts-expect-error Error due to circular dependency with the dev-env package
|
|
ctx = network.pds.ctx
|
|
idResolver = new IdResolver({ plcUrl: ctx.cfg.identity.plcUrl })
|
|
agent = network.pds.getClient()
|
|
sc = network.getSeedClient()
|
|
await basicSeed(sc)
|
|
alice = sc.dids.alice
|
|
bob = sc.dids.bob
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
const getHandleFromDb = async (did: string): Promise<string | undefined> => {
|
|
const res = await ctx.accountManager.getAccount(did)
|
|
return res?.handle ?? undefined
|
|
}
|
|
|
|
it('resolves handles', async () => {
|
|
const res = await agent.api.com.atproto.identity.resolveHandle({
|
|
handle: 'alice.test',
|
|
})
|
|
expect(res.data.did).toBe(alice)
|
|
})
|
|
|
|
it('resolves non-normalize handles', async () => {
|
|
const res = await agent.api.com.atproto.identity.resolveHandle({
|
|
handle: 'aLicE.tEst',
|
|
})
|
|
expect(res.data.did).toBe(alice)
|
|
})
|
|
|
|
it('allows a user to change their handle', async () => {
|
|
await agent.api.com.atproto.identity.updateHandle(
|
|
{ handle: newHandle },
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
const attemptOld = agent.api.com.atproto.identity.resolveHandle({
|
|
handle: 'alice.test',
|
|
})
|
|
await expect(attemptOld).rejects.toThrow('Unable to resolve handle')
|
|
const attemptNew = await agent.api.com.atproto.identity.resolveHandle({
|
|
handle: newHandle,
|
|
})
|
|
expect(attemptNew.data.did).toBe(alice)
|
|
})
|
|
|
|
it('updates their did document', async () => {
|
|
const data = await idResolver.did.resolveAtprotoData(alice)
|
|
expect(data.handle).toBe(newHandle)
|
|
})
|
|
|
|
it('allows a user to login with their new handle', async () => {
|
|
const res = await agent.api.com.atproto.server.createSession({
|
|
identifier: newHandle,
|
|
password: sc.accounts[alice].password,
|
|
})
|
|
sc.accounts[alice].accessJwt = res.data.accessJwt
|
|
sc.accounts[alice].refreshJwt = res.data.refreshJwt
|
|
})
|
|
|
|
it('does not allow taking a handle that already exists', async () => {
|
|
const attempt = agent.api.com.atproto.identity.updateHandle(
|
|
{ handle: 'Bob.test' },
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
await expect(attempt).rejects.toThrow('Handle already taken: bob.test')
|
|
})
|
|
|
|
it('handle updates are idempotent', async () => {
|
|
await agent.api.com.atproto.identity.updateHandle(
|
|
{ handle: 'Bob.test' },
|
|
{ headers: sc.getHeaders(bob), encoding: 'application/json' },
|
|
)
|
|
})
|
|
|
|
it('if handle update fails, it does not update their did document', async () => {
|
|
const data = await idResolver.did.resolveAtprotoData(alice)
|
|
expect(data.handle).toBe(newHandle)
|
|
})
|
|
|
|
it('disallows improperly formatted handles', async () => {
|
|
const tryHandle = async (handle: string) => {
|
|
await agent.api.com.atproto.identity.updateHandle(
|
|
{ handle },
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
}
|
|
await expect(tryHandle('did:john')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('john.bsky.io')).rejects.toThrow(
|
|
'External handle did not resolve to DID',
|
|
)
|
|
await expect(tryHandle('j.test')).rejects.toThrow('Handle too short')
|
|
await expect(tryHandle('jayromy-johnber12345678910.test')).rejects.toThrow(
|
|
'Handle too long',
|
|
)
|
|
await expect(tryHandle('jo_hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo!hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo%hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo&hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo*hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo|hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo:hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
await expect(tryHandle('jo/hn.test')).rejects.toThrow(
|
|
'Input/handle must be a valid handle',
|
|
)
|
|
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.identity.updateHandle(
|
|
{
|
|
handle: 'alice.external',
|
|
},
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
const dbHandle = await getHandleFromDb(alice)
|
|
expect(dbHandle).toBe('alice.external')
|
|
|
|
const data = await idResolver.did.resolveAtprotoData(alice)
|
|
expect(data.handle).toBe('alice.external')
|
|
})
|
|
|
|
it('does not allow updating to an invalid dns handle', async () => {
|
|
const attempt = agent.api.com.atproto.identity.updateHandle(
|
|
{
|
|
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.identity.updateHandle(
|
|
{
|
|
handle: 'noexist.external',
|
|
},
|
|
{ headers: sc.getHeaders(alice), encoding: 'application/json' },
|
|
)
|
|
await expect(attempt2).rejects.toThrow(
|
|
'External handle did not resolve to DID',
|
|
)
|
|
|
|
const dbHandle = await getHandleFromDb(alice)
|
|
expect(dbHandle).toBe('alice.external')
|
|
})
|
|
|
|
it('allows admin overrules of service domains', async () => {
|
|
await agent.api.com.atproto.admin.updateAccountHandle(
|
|
{
|
|
did: bob,
|
|
handle: 'bob-alt.test',
|
|
},
|
|
{
|
|
headers: network.pds.adminAuthHeaders(),
|
|
encoding: 'application/json',
|
|
},
|
|
)
|
|
|
|
const dbHandle = await getHandleFromDb(bob)
|
|
expect(dbHandle).toBe('bob-alt.test')
|
|
})
|
|
|
|
it('allows admin override of reserved domains', async () => {
|
|
await agent.api.com.atproto.admin.updateAccountHandle(
|
|
{
|
|
did: bob,
|
|
handle: 'dril.test',
|
|
},
|
|
{
|
|
headers: network.pds.adminAuthHeaders(),
|
|
encoding: 'application/json',
|
|
},
|
|
)
|
|
|
|
const dbHandle = await getHandleFromDb(bob)
|
|
expect(dbHandle).toBe('dril.test')
|
|
})
|
|
|
|
it('requires admin auth', async () => {
|
|
const attempt = agent.api.com.atproto.admin.updateAccountHandle(
|
|
{
|
|
did: bob,
|
|
handle: 'bob-alt.test',
|
|
},
|
|
{
|
|
headers: sc.getHeaders(bob),
|
|
encoding: 'application/json',
|
|
},
|
|
)
|
|
await expect(attempt).rejects.toThrow('Authentication Required')
|
|
const attempt2 = agent.api.com.atproto.admin.updateAccountHandle({
|
|
did: bob,
|
|
handle: 'bob-alt.test',
|
|
})
|
|
await expect(attempt2).rejects.toThrow('Authentication Required')
|
|
})
|
|
})
|