Skip to content

Commit 608a6f4

Browse files
committed
feat(identity-resolver-node): initial commit
1 parent aac090b commit 608a6f4

File tree

8 files changed

+198
-2
lines changed

8 files changed

+198
-2
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ of atcute.
4040
| **Identity packages** _(work in progress)_ |
4141
| [`did-plc`](./packages/identity/did-plc): validations, type definitions and schemas for did:plc operations |
4242
| [`identity`](./packages/identity/identity): syntax, type definitions and schemas for handles, DIDs and DID documents |
43-
| [`identity-resolver`]('./packages/identity/identity-resolver): handle and DID document resolution |
44-
| `identity-resolver-node`: `node:dns`-powered handle resolution |
43+
| [`identity-resolver`](./packages/identity/identity-resolver): handle and DID document resolution |
44+
| [`identity-resolver-node`](./packages/identity/identity-resolver-node): additional identity resolvers for Node.js |
4545
| **Lexicon definitions** |
4646
| [`bluemoji`](./packages/definitions/bluemoji): adds `blue.moji.*` lexicons |
4747
| [`bluesky`](./packages/definitions/bluesky): adds `app.bsky.*` and `chat.bsky.*` lexicons |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# @atcute/identity-resolver-node
2+
3+
> [!NOTE]
4+
> not yet published.
5+
6+
additional atproto identity resolvers for Node.js
7+
8+
```ts
9+
// handle resolution
10+
const handleResolver = new CompositeHandleResolver({
11+
strategy: 'race',
12+
methods: {
13+
dns: new NodeDnsHandleResolver(),
14+
http: new WellKnownHandleResolver(),
15+
},
16+
});
17+
18+
try {
19+
const handle = await didResolver.resolve('bsky.app');
20+
// ^? 'did:plc:z72i7hdynmk6r22z27h6tvur'
21+
} catch (err) {
22+
if (err instanceof DidNotFoundError) {
23+
// handle returned no did
24+
}
25+
if (err instanceof InvalidResolvedHandleError) {
26+
// handle returned a did, but isn't a valid atproto did
27+
}
28+
if (err instanceof AmbiguousHandleError) {
29+
// handle returned multiple did values
30+
}
31+
if (err instanceof FailedHandleResolutionError) {
32+
// handle resolution had thrown something unexpected (fetch error)
33+
}
34+
35+
if (err instanceof HandleResolutionError) {
36+
// the errors above extend this class, so you can do a catch-all.
37+
}
38+
}
39+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import dns from 'node:dns/promises';
2+
3+
import { isAtprotoDid, type AtprotoDid, type Handle } from '@atcute/identity';
4+
import {
5+
AmbiguousHandleError,
6+
DidNotFoundError,
7+
FailedHandleResolutionError,
8+
InvalidResolvedHandleError,
9+
type HandleResolver,
10+
type ResolveHandleOptions,
11+
} from '@atcute/identity-resolver';
12+
13+
const SUBDOMAIN = '_atproto';
14+
const PREFIX = 'did=';
15+
16+
export interface NodeDnsHandleResolverOptions {
17+
nameservers?: string[];
18+
}
19+
20+
export class NodeDnsHandleResolver implements HandleResolver {
21+
#resolver: dns.Resolver | null = null;
22+
23+
get nameservers(): string[] | undefined {
24+
return this.#resolver?.getServers();
25+
}
26+
27+
constructor({ nameservers }: NodeDnsHandleResolverOptions = {}) {
28+
if (nameservers) {
29+
this.#resolver = new dns.Resolver();
30+
this.#resolver.setServers(nameservers);
31+
}
32+
}
33+
34+
async resolve(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid> {
35+
let results: string[][];
36+
37+
try {
38+
const signal = options?.signal;
39+
const resolver = this.#resolver ?? dns;
40+
41+
results = await resolver.resolveTxt(`${SUBDOMAIN}.${handle}`);
42+
signal?.throwIfAborted();
43+
} catch (cause) {
44+
if (cause instanceof Error && 'code' in cause && cause.code === 'ENOTFOUND') {
45+
throw new DidNotFoundError(handle);
46+
}
47+
48+
throw new FailedHandleResolutionError(handle, { cause });
49+
}
50+
51+
const records = results.map((record) => record.join('').replace(/^"|"$/g, '').replace(/\\"/g, '"'));
52+
for (let i = 0, il = records.length; i < il; i++) {
53+
const data = records[i];
54+
55+
if (!data.startsWith(PREFIX)) {
56+
continue;
57+
}
58+
59+
for (let j = i + 1; j < il; j++) {
60+
const data = records[j];
61+
if (data.startsWith(PREFIX)) {
62+
throw new AmbiguousHandleError(handle);
63+
}
64+
}
65+
66+
const did = data.slice(PREFIX.length);
67+
if (!isAtprotoDid(did)) {
68+
throw new InvalidResolvedHandleError(handle, did);
69+
}
70+
71+
return did;
72+
}
73+
74+
// theoretically this shouldn't happen, it should've returned ENOTFOUND
75+
throw new DidNotFoundError(handle);
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './did/methods/node.js';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"type": "module",
3+
"name": "@atcute/identity-resolver-node",
4+
"version": "1.0.0",
5+
"description": "additional atproto identity resolvers for Node.js",
6+
"keywords": [
7+
"atproto",
8+
"did"
9+
],
10+
"license": "MIT",
11+
"repository": {
12+
"url": "https://github.com/mary-ext/atcute",
13+
"directory": "packages/identity/identity-resolver-node"
14+
},
15+
"files": [
16+
"dist/",
17+
"lib/",
18+
"!lib/**/*.bench.ts",
19+
"!lib/**/*.test.ts"
20+
],
21+
"exports": {
22+
".": "./dist/index.js"
23+
},
24+
"sideEffects": false,
25+
"scripts": {
26+
"build": "tsc --project tsconfig.build.json",
27+
"test": "bun test --coverage",
28+
"prepublish": "rm -rf dist; pnpm run build"
29+
},
30+
"peerDependencies": {
31+
"@atcute/identity": "^1.0.0",
32+
"@atcute/identity-resolver": "^1.0.0"
33+
},
34+
"devDependencies": {
35+
"@atcute/identity": "workspace:^",
36+
"@atcute/identity-resolver": "workspace:^",
37+
"@types/bun": "^1.2.1"
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"exclude": ["**/*.test.ts"]
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"types": ["bun"],
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+
"stripInternal": true,
22+
},
23+
"include": ["lib"],
24+
}

pnpm-lock.yaml

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)