a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(lexicon-resolver-node): initial commit

Mary 4e2b3ada 98aef692

+204 -7
+29
packages/lexicons/lexicon-resolver-node/README.md
··· 1 + # @atcute/lexicon-resolver-node 2 + 3 + Node.js lexicon authority resolver using native DNS. 4 + 5 + ```sh 6 + npm install @atcute/lexicon-resolver-node 7 + ``` 8 + 9 + provides `NodeDnsLexiconAuthorityResolver` which resolves lexicon authorities via DNS TXT records 10 + using Node.js's native `dns` module, avoiding the need for HTTP-based DoH resolution. 11 + 12 + ## usage 13 + 14 + ```ts 15 + import { NodeDnsLexiconAuthorityResolver } from '@atcute/lexicon-resolver-node'; 16 + 17 + const authorityResolver = new NodeDnsLexiconAuthorityResolver(); 18 + 19 + const authority = await authorityResolver.resolve('app.bsky.feed.post'); 20 + // -> 'did:plc:4v4y5r3lwsbtmsxhile2ljac' 21 + ``` 22 + 23 + ### custom nameservers 24 + 25 + ```ts 26 + const resolver = new NodeDnsLexiconAuthorityResolver({ 27 + nameservers: ['8.8.8.8', '8.8.4.4'], 28 + }); 29 + ```
+81
packages/lexicons/lexicon-resolver-node/lib/authority/node-dns.ts
··· 1 + import dns from 'node:dns/promises'; 2 + 3 + import { isAtprotoDid } from '@atcute/identity'; 4 + import { 5 + AmbiguousAuthorityError, 6 + AuthorityNotFoundError, 7 + FailedAuthorityResolutionError, 8 + InvalidResolvedAuthorityError, 9 + type LexiconAuthorityResolver, 10 + type ResolveLexiconAuthorityOptions, 11 + nsidToLookupDomain, 12 + } from '@atcute/lexicon-resolver'; 13 + import type { AtprotoDid, Nsid } from '@atcute/lexicons/syntax'; 14 + 15 + const SUBDOMAIN = '_lexicon'; 16 + const PREFIX = 'did='; 17 + 18 + export interface NodeDnsLexiconAuthorityResolverOptions { 19 + nameservers?: string[]; 20 + } 21 + 22 + export class NodeDnsLexiconAuthorityResolver implements LexiconAuthorityResolver { 23 + #resolver: dns.Resolver | null = null; 24 + 25 + get nameservers(): string[] | undefined { 26 + return this.#resolver?.getServers(); 27 + } 28 + 29 + constructor({ nameservers }: NodeDnsLexiconAuthorityResolverOptions = {}) { 30 + if (nameservers) { 31 + this.#resolver = new dns.Resolver(); 32 + this.#resolver.setServers(nameservers); 33 + } 34 + } 35 + 36 + async resolve(nsid: Nsid, options?: ResolveLexiconAuthorityOptions): Promise<AtprotoDid> { 37 + const lookupDomain = nsidToLookupDomain(nsid); 38 + 39 + let results: string[][]; 40 + 41 + try { 42 + const signal = options?.signal; 43 + const resolver = this.#resolver ?? dns; 44 + 45 + results = await resolver.resolveTxt(`${SUBDOMAIN}.${lookupDomain}`); 46 + signal?.throwIfAborted(); 47 + } catch (cause) { 48 + if (cause instanceof Error && 'code' in cause && cause.code === 'ENOTFOUND') { 49 + throw new AuthorityNotFoundError(nsid); 50 + } 51 + 52 + throw new FailedAuthorityResolutionError(nsid, { cause }); 53 + } 54 + 55 + const records = results.map((record) => record.join('').replace(/^"|"$/g, '').replace(/\\"/g, '"')); 56 + for (let i = 0, il = records.length; i < il; i++) { 57 + const data = records[i]; 58 + 59 + if (!data.startsWith(PREFIX)) { 60 + continue; 61 + } 62 + 63 + for (let j = i + 1; j < il; j++) { 64 + const data = records[j]; 65 + if (data.startsWith(PREFIX)) { 66 + throw new AmbiguousAuthorityError(nsid); 67 + } 68 + } 69 + 70 + const did = data.slice(PREFIX.length); 71 + if (!isAtprotoDid(did)) { 72 + throw new InvalidResolvedAuthorityError(nsid, did); 73 + } 74 + 75 + return did; 76 + } 77 + 78 + // theoretically this shouldn't happen, it should've returned ENOTFOUND 79 + throw new AuthorityNotFoundError(nsid); 80 + } 81 + }
+1
packages/lexicons/lexicon-resolver-node/lib/index.ts
··· 1 + export * from './authority/node-dns.js';
+45
packages/lexicons/lexicon-resolver-node/package.json
··· 1 + { 2 + "type": "module", 3 + "name": "@atcute/lexicon-resolver-node", 4 + "version": "0.1.0", 5 + "description": "additional atproto lexicon resolvers for Node.js", 6 + "keywords": [ 7 + "atproto", 8 + "lexicon", 9 + "did" 10 + ], 11 + "license": "0BSD", 12 + "repository": { 13 + "url": "https://github.com/mary-ext/atcute", 14 + "directory": "packages/lexicons/lexicon-resolver-node" 15 + }, 16 + "publishConfig": { 17 + "access": "public" 18 + }, 19 + "files": [ 20 + "dist/", 21 + "lib/", 22 + "!lib/**/*.bench.ts", 23 + "!lib/**/*.test.ts" 24 + ], 25 + "exports": { 26 + ".": "./dist/index.js" 27 + }, 28 + "sideEffects": false, 29 + "scripts": { 30 + "build": "tsgo --project tsconfig.build.json", 31 + "prepublish": "rm -rf dist; pnpm run build" 32 + }, 33 + "peerDependencies": { 34 + "@atcute/identity": "^1.0.0", 35 + "@atcute/lexicon-resolver": "^0.1.0" 36 + }, 37 + "devDependencies": { 38 + "@atcute/identity": "workspace:^", 39 + "@atcute/lexicon-resolver": "workspace:^", 40 + "@types/node": "^22.19.3" 41 + }, 42 + "dependencies": { 43 + "@atcute/lexicons": "workspace:^" 44 + } 45 + }
+4
packages/lexicons/lexicon-resolver-node/tsconfig.build.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "exclude": ["**/*.test.ts"] 4 + }
+25
packages/lexicons/lexicon-resolver-node/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "types": ["node"], 4 + "outDir": "dist/", 5 + "esModuleInterop": true, 6 + "skipLibCheck": true, 7 + "target": "ESNext", 8 + "allowJs": true, 9 + "resolveJsonModule": true, 10 + "moduleDetection": "force", 11 + "isolatedModules": true, 12 + "verbatimModuleSyntax": true, 13 + "strict": true, 14 + "noImplicitOverride": true, 15 + "noUnusedLocals": true, 16 + "noUnusedParameters": true, 17 + "noFallthroughCasesInSwitch": true, 18 + "module": "NodeNext", 19 + "sourceMap": true, 20 + "declaration": true, 21 + "declarationMap": true, 22 + "stripInternal": true 23 + }, 24 + "include": ["lib"] 25 + }
+19 -7
pnpm-lock.yaml
··· 74 74 version: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 75 75 76 76 packages/bluesky/richtext-segmenter: 77 - dependencies: 77 + devDependencies: 78 78 '@atcute/bluesky': 79 79 specifier: workspace:^ 80 80 version: link:../../definitions/bluesky 81 - '@atcute/lexicons': 82 - specifier: workspace:^ 83 - version: link:../../lexicons/lexicons 84 - devDependencies: 85 81 '@vitest/coverage-v8': 86 82 specifier: ^4.0.16 87 83 version: 4.0.16(@vitest/browser@4.0.16(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0))(vitest@4.0.16))(vitest@4.0.16) ··· 675 671 vitest: 676 672 specifier: ^4.0.16 677 673 version: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 674 + 675 + packages/lexicons/lexicon-resolver-node: 676 + dependencies: 677 + '@atcute/lexicons': 678 + specifier: workspace:^ 679 + version: link:../lexicons 680 + devDependencies: 681 + '@atcute/identity': 682 + specifier: workspace:^ 683 + version: link:../../identity/identity 684 + '@atcute/lexicon-resolver': 685 + specifier: workspace:^ 686 + version: link:../lexicon-resolver 687 + '@types/node': 688 + specifier: ^22.19.3 689 + version: 22.19.3 678 690 679 691 packages/lexicons/lexicons: 680 692 dependencies: ··· 6486 6498 6487 6499 '@types/bn.js@5.2.0': 6488 6500 dependencies: 6489 - '@types/node': 25.0.3 6501 + '@types/node': 22.19.3 6490 6502 6491 6503 '@types/bun@1.3.5': 6492 6504 dependencies: ··· 6838 6850 6839 6851 bun-types@1.3.5: 6840 6852 dependencies: 6841 - '@types/node': 25.0.3 6853 + '@types/node': 22.19.3 6842 6854 6843 6855 bytes@3.1.2: {} 6844 6856