devin ivy 331a356ce2
Lexicon resolver package (#4069)
* lexicon: doc validation compatibility with published lexicons

* lexicon-resolver: setup new package

* lexicon-resolver: implement record resolution

* lexicon-resolver: implement lexicon resolution

* lexicon-resolver: test record resolution

* repo: add option to verify CIDs found in CARs. tidy.

* lexicon-resolution: verify CIDs in proof CAR

* lexicon-resolution: tests and fixes

* tidy

* lexicon-resolution: add entrypoint

* lexicon-resolver: tidy errors

* lexicon-resolver: readme

* lexicon-resolver: changeset

* prettier

* eslint

* tidy

* tidy

* tidy

* enable CID-to-content verification within CARs by default

* lexicon-resolver: tidy types, application of defaults, gitattributes

* lexicon-resolver: add interface and builder fn for lexicon and record resolvers

* lexicon-resolver: update readme

* tidy

* lexicon-resolver: cover error cases in record resolution

---------

Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
2025-08-17 22:45:51 -04:00

99 lines
3.2 KiB
TypeScript

import { dataToCborBlock } from '@atproto/common'
import { SeedClient, TestNetworkNoAppView, usersSeed } from '@atproto/dev-env'
import { AtprotoRecordResolver, buildRecordResolver } from '../src/index.js'
describe('Record resolution', () => {
let network: TestNetworkNoAppView
let sc: SeedClient
let resolveRecord: AtprotoRecordResolver
beforeAll(async () => {
network = await TestNetworkNoAppView.create({
dbPostgresSchema: 'lex_record_resolution',
})
sc = network.getSeedClient()
await usersSeed(sc)
resolveRecord = buildRecordResolver({
rpc: { fetch },
idResolver: network.pds.ctx.idResolver,
})
})
afterAll(async () => {
await network.close()
})
it('resolves record by AT-URI object.', async () => {
const post = await sc.post(sc.dids.alice, 'post1')
const result = await resolveRecord(post.ref.uri, {
forceRefresh: true,
})
expect(result.commit.did).toEqual(sc.dids.alice)
expect(result.cid.toString()).toEqual(post.ref.cidStr)
expect(result.uri.toString()).toEqual(post.ref.uriStr)
expect(result.record.text).toEqual('post1')
})
it('resolves record by AT-URI string.', async () => {
const post = await sc.post(sc.dids.alice, 'post2')
const result = await resolveRecord(post.ref.uriStr, {
forceRefresh: true,
})
expect(result.commit.did).toEqual(sc.dids.alice)
expect(result.cid.toString()).toEqual(post.ref.cidStr)
expect(result.uri.toString()).toEqual(post.ref.uriStr)
expect(result.record.text).toEqual('post2')
})
it("does not resolve record that doesn't exist.", async () => {
await expect(
resolveRecord(`at://${sc.dids.alice}/app.bsky.feed.post/2222222222222`, {
forceRefresh: true,
}),
).rejects.toThrow('Record not found')
})
it('does not resolve record with bad commit signature.', async () => {
const alicekey = await network.pds.ctx.actorStore.keypair(sc.dids.alice)
const bobkey = await network.pds.ctx.actorStore.keypair(sc.dids.bob)
const post = await sc.post(sc.dids.alice, 'post3')
// switch alice's key away from the one used by her pds
await network.pds.ctx.plcClient.updateAtprotoKey(
sc.dids.alice,
network.pds.ctx.plcRotationKey,
bobkey.did(),
)
await expect(
resolveRecord(post.ref.uri, {
forceRefresh: true,
}),
).rejects.toThrow('Invalid signature on commit')
// reset alice's key
await network.pds.ctx.plcClient.updateAtprotoKey(
sc.dids.alice,
network.pds.ctx.plcRotationKey,
alicekey.did(),
)
})
it('does not resolve record with corrupted CAR block.', async () => {
const post = await sc.post(sc.dids.alice, 'post4')
const badBlock = await dataToCborBlock({})
await network.pds.ctx.actorStore.transact(sc.dids.alice, (txn) =>
txn.repo.db.db
.updateTable('repo_block')
.set({
content: badBlock.bytes,
size: badBlock.bytes.byteLength,
})
.where('cid', '=', post.ref.cidStr)
.execute(),
)
await expect(
resolveRecord(post.ref.uri, {
forceRefresh: true,
}),
).rejects.toThrow('Malformed record proof')
})
})