@atproto-labs/handle-resolver 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atproto-labs/handle-resolver
2
2
 
3
+ ## 0.4.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Update TypeScript build to rely on references to composite internal projects
8
+
9
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Bundle only necessary files in the NPM tarball, including the `CHANGELOG.md` and `README.md` files (if present).
10
+
11
+ - [#5099](https://github.com/bluesky-social/atproto/pull/5099) [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Build with `noImplicitAny` enabled
12
+
13
+ - Updated dependencies [[`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07), [`b43ec31`](https://github.com/bluesky-social/atproto/commit/b43ec31f247f4461725b01226885f88bd430ca07)]:
14
+ - @atproto-labs/simple-store-memory@0.2.3
15
+ - @atproto-labs/simple-store@0.4.3
16
+ - @atproto/did@0.5.3
17
+
3
18
  ## 0.4.3
4
19
 
5
20
  ### Patch Changes
package/package.json CHANGED
@@ -1,9 +1,6 @@
1
1
  {
2
2
  "name": "@atproto-labs/handle-resolver",
3
- "version": "0.4.3",
4
- "engines": {
5
- "node": ">=22"
6
- },
3
+ "version": "0.4.4",
7
4
  "license": "MIT",
8
5
  "description": "Isomorphic ATProto handle to DID resolver",
9
6
  "keywords": [
@@ -21,6 +18,10 @@
21
18
  "url": "https://github.com/bluesky-social/atproto",
22
19
  "directory": "packages/internal/handle-resolver"
23
20
  },
21
+ "files": [
22
+ "./dist",
23
+ "./CHANGELOG.md"
24
+ ],
24
25
  "type": "module",
25
26
  "exports": {
26
27
  ".": {
@@ -28,13 +29,15 @@
28
29
  "default": "./dist/index.js"
29
30
  }
30
31
  },
32
+ "engines": {
33
+ "node": ">=22"
34
+ },
31
35
  "dependencies": {
32
36
  "zod": "^3.23.8",
33
- "@atproto-labs/simple-store-memory": "^0.2.2",
34
- "@atproto/did": "^0.5.2",
35
- "@atproto-labs/simple-store": "^0.4.2"
37
+ "@atproto-labs/simple-store": "^0.4.3",
38
+ "@atproto/did": "^0.5.3",
39
+ "@atproto-labs/simple-store-memory": "^0.2.3"
36
40
  },
37
- "devDependencies": {},
38
41
  "scripts": {
39
42
  "build": "tsgo --build tsconfig.build.json"
40
43
  }
@@ -1,124 +0,0 @@
1
- import {
2
- AtprotoHandleResolver,
3
- AtprotoHandleResolverOptions,
4
- } from './atproto-handle-resolver.js'
5
- import { HandleResolverError } from './handle-resolver-error.js'
6
- import { ResolveTxt } from './internal-resolvers/dns-handle-resolver.js'
7
- import { HandleResolver } from './types.js'
8
-
9
- export type AtprotoDohHandleResolverOptions = Omit<
10
- AtprotoHandleResolverOptions,
11
- 'resolveTxt' | 'resolveTxtFallback'
12
- > & {
13
- dohEndpoint: string | URL
14
- }
15
-
16
- export class AtprotoDohHandleResolver
17
- extends AtprotoHandleResolver
18
- implements HandleResolver
19
- {
20
- constructor(options: AtprotoDohHandleResolverOptions) {
21
- super({
22
- ...options,
23
- resolveTxt: dohResolveTxtFactory(options),
24
- resolveTxtFallback: undefined,
25
- })
26
- }
27
- }
28
-
29
- /**
30
- * Resolver for DNS-over-HTTPS (DoH) handles. Only works with servers supporting
31
- * Google Flavoured "application/dns-json" queries.
32
- *
33
- * @see {@link https://developers.google.com/speed/public-dns/docs/doh/json}
34
- * @see {@link https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/}
35
- * @todo Add support for DoH using application/dns-message (?)
36
- */
37
- function dohResolveTxtFactory({
38
- dohEndpoint,
39
- fetch = globalThis.fetch,
40
- }: AtprotoDohHandleResolverOptions): ResolveTxt {
41
- return async (hostname) => {
42
- const url = new URL(dohEndpoint)
43
- url.searchParams.set('type', 'TXT')
44
- url.searchParams.set('name', hostname)
45
-
46
- const response = await fetch(url, {
47
- method: 'GET',
48
- headers: { accept: 'application/dns-json' },
49
- redirect: 'follow',
50
- })
51
- try {
52
- const contentType = response.headers.get('content-type')?.trim()
53
- if (!response.ok) {
54
- const message = contentType?.startsWith('text/plain')
55
- ? await response.text()
56
- : `Failed to resolve ${hostname}`
57
- throw new HandleResolverError(message)
58
- } else if (contentType?.match(/application\/(dns-)?json/i) == null) {
59
- throw new HandleResolverError('Unexpected response from DoH server')
60
- }
61
-
62
- const result = asResult(await response.json())
63
- return result.Answer?.filter(isAnswerTxt).map(extractTxtData) ?? null
64
- } finally {
65
- // Make sure to always cancel the response body as some engines (Node 👀)
66
- // do not do this automatically.
67
- // https://undici.nodejs.org/#/?id=garbage-collection
68
- if (response.bodyUsed === false) {
69
- // Handle rejection asynchronously
70
- void response.body?.cancel().catch(onCancelError)
71
- }
72
- }
73
- }
74
- }
75
-
76
- function onCancelError(err: unknown) {
77
- if (!(err instanceof DOMException) || err.name !== 'AbortError') {
78
- console.error('An error occurred while cancelling the response body:', err)
79
- }
80
- }
81
-
82
- type Result = { Status: number; Answer?: Answer[] }
83
- function isResult(result: unknown): result is Result {
84
- if (typeof result !== 'object' || result === null) return false
85
- if (!('Status' in result) || typeof result.Status !== 'number') return false
86
- if ('Answer' in result && !isArrayOf(result.Answer, isAnswer)) return false
87
- return true
88
- }
89
- function asResult(result: unknown): Result {
90
- if (isResult(result)) return result
91
- throw new HandleResolverError('Invalid DoH response')
92
- }
93
-
94
- function isArrayOf<T>(
95
- value: unknown,
96
- predicate: (v: unknown) => v is T,
97
- ): value is T[] {
98
- return Array.isArray(value) && value.every(predicate)
99
- }
100
-
101
- type Answer = { name: string; type: number; data: string; TTL: number }
102
- function isAnswer(answer: unknown): answer is Answer {
103
- return (
104
- typeof answer === 'object' &&
105
- answer !== null &&
106
- 'name' in answer &&
107
- typeof answer.name === 'string' &&
108
- 'type' in answer &&
109
- typeof answer.type === 'number' &&
110
- 'data' in answer &&
111
- typeof answer.data === 'string' &&
112
- 'TTL' in answer &&
113
- typeof answer.TTL === 'number'
114
- )
115
- }
116
-
117
- type AnswerTxt = Answer & { type: 16 }
118
- function isAnswerTxt(answer: Answer): answer is AnswerTxt {
119
- return answer.type === 16
120
- }
121
-
122
- function extractTxtData(answer: AnswerTxt): string {
123
- return answer.data.replace(/^"|"$/g, '').replace(/\\"/g, '"')
124
- }
@@ -1,79 +0,0 @@
1
- import {
2
- DnsHandleResolver,
3
- ResolveTxt,
4
- } from './internal-resolvers/dns-handle-resolver.js'
5
- import {
6
- WellKnownHandleResolver,
7
- WellKnownHandleResolverOptions,
8
- } from './internal-resolvers/well-known-handler-resolver.js'
9
- import {
10
- HandleResolver,
11
- ResolveHandleOptions,
12
- ResolvedHandle,
13
- } from './types.js'
14
-
15
- export type { ResolveTxt }
16
- export type AtprotoHandleResolverOptions = WellKnownHandleResolverOptions & {
17
- resolveTxt: ResolveTxt
18
- resolveTxtFallback?: ResolveTxt
19
- }
20
-
21
- const noop = () => {}
22
-
23
- /**
24
- * Implementation of the official ATPROTO handle resolution strategy.
25
- * This implementation relies on two primitives:
26
- * - HTTP Well-Known URI resolution (requires a `fetch()` implementation)
27
- * - DNS TXT record resolution (requires a `resolveTxt()` function)
28
- */
29
- export class AtprotoHandleResolver implements HandleResolver {
30
- private readonly httpResolver: HandleResolver
31
- private readonly dnsResolver: HandleResolver
32
- private readonly dnsResolverFallback?: HandleResolver
33
-
34
- constructor(options: AtprotoHandleResolverOptions) {
35
- this.httpResolver = new WellKnownHandleResolver(options)
36
- this.dnsResolver = new DnsHandleResolver(options.resolveTxt)
37
- this.dnsResolverFallback = options.resolveTxtFallback
38
- ? new DnsHandleResolver(options.resolveTxtFallback)
39
- : undefined
40
- }
41
-
42
- async resolve(
43
- handle: string,
44
- options?: ResolveHandleOptions,
45
- ): Promise<ResolvedHandle> {
46
- options?.signal?.throwIfAborted()
47
-
48
- const abortController = new AbortController()
49
- const { signal } = abortController
50
- options?.signal?.addEventListener('abort', () => abortController.abort(), {
51
- signal,
52
- })
53
-
54
- const wrappedOptions = { ...options, signal }
55
-
56
- try {
57
- const dnsPromise = this.dnsResolver.resolve(handle, wrappedOptions)
58
- const httpPromise = this.httpResolver.resolve(handle, wrappedOptions)
59
-
60
- // Prevent uncaught promise rejection
61
- httpPromise.catch(noop)
62
-
63
- const dnsRes = await dnsPromise
64
- if (dnsRes) return dnsRes
65
-
66
- signal.throwIfAborted()
67
-
68
- const res = await httpPromise
69
- if (res) return res
70
-
71
- signal.throwIfAborted()
72
-
73
- return this.dnsResolverFallback?.resolve(handle, wrappedOptions) ?? null
74
- } finally {
75
- // Cancel pending requests, and remove "abort" listener on incoming signal
76
- abortController.abort()
77
- }
78
- }
79
- }
@@ -1,36 +0,0 @@
1
- import { CachedGetter, SimpleStore } from '@atproto-labs/simple-store'
2
- import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
3
- import {
4
- HandleResolver,
5
- ResolveHandleOptions,
6
- ResolvedHandle,
7
- } from './types.js'
8
-
9
- export type HandleCache = SimpleStore<string, ResolvedHandle>
10
-
11
- export class CachedHandleResolver implements HandleResolver {
12
- private getter: CachedGetter<string, ResolvedHandle>
13
-
14
- constructor(
15
- /**
16
- * The resolver that will be used to resolve handles.
17
- */
18
- resolver: HandleResolver,
19
- cache: HandleCache = new SimpleStoreMemory<string, ResolvedHandle>({
20
- max: 1000,
21
- ttl: 10 * 60e3,
22
- }),
23
- ) {
24
- this.getter = new CachedGetter<string, ResolvedHandle>(
25
- (handle, options) => resolver.resolve(handle, options),
26
- cache,
27
- )
28
- }
29
-
30
- async resolve(
31
- handle: string,
32
- options?: ResolveHandleOptions,
33
- ): Promise<ResolvedHandle> {
34
- return this.getter.get(handle, options)
35
- }
36
- }
@@ -1,28 +0,0 @@
1
- import { CachedHandleResolver, HandleCache } from './cached-handle-resolver.js'
2
- import { HandleResolver } from './types.js'
3
- import {
4
- XrpcHandleResolver,
5
- XrpcHandleResolverOptions,
6
- } from './xrpc-handle-resolver.js'
7
-
8
- export type CreateHandleResolverOptions = {
9
- handleResolver: URL | string | HandleResolver
10
- handleCache?: HandleCache
11
- } & Partial<XrpcHandleResolverOptions>
12
-
13
- export function createHandleResolver(
14
- options: CreateHandleResolverOptions,
15
- ): HandleResolver {
16
- const { handleResolver, handleCache } = options
17
-
18
- if (handleResolver instanceof CachedHandleResolver && !handleCache) {
19
- return handleResolver
20
- }
21
-
22
- return new CachedHandleResolver(
23
- typeof handleResolver === 'string' || handleResolver instanceof URL
24
- ? new XrpcHandleResolver(handleResolver, options)
25
- : handleResolver,
26
- handleCache,
27
- )
28
- }
@@ -1,3 +0,0 @@
1
- export class HandleResolverError extends Error {
2
- name = 'HandleResolverError'
3
- }
package/src/index.ts DELETED
@@ -1,13 +0,0 @@
1
- export * from './handle-resolver-error.js'
2
- export * from './types.js'
3
-
4
- // Main Handle Resolvers strategies
5
- export * from './xrpc-handle-resolver.js'
6
- export * from './atproto-doh-handle-resolver.js'
7
- export * from './atproto-handle-resolver.js'
8
-
9
- // Handle Resolver Caching utility
10
- export * from './cached-handle-resolver.js'
11
-
12
- // utils
13
- export * from './create-handle-resolver.js'
@@ -1,38 +0,0 @@
1
- import { HandleResolver, ResolvedHandle, isResolvedHandle } from '../types.js'
2
-
3
- const SUBDOMAIN = '_atproto'
4
- const PREFIX = 'did='
5
-
6
- /**
7
- * DNS TXT record resolver. Return `null` if the hostname successfully does not
8
- * resolve to a valid DID. Throw an error if an unexpected error occurs.
9
- */
10
- export type ResolveTxt = (hostname: string) => Promise<null | string[]>
11
-
12
- export class DnsHandleResolver implements HandleResolver {
13
- constructor(protected resolveTxt: ResolveTxt) {}
14
-
15
- async resolve(handle: string): Promise<ResolvedHandle> {
16
- const results = await this.resolveTxt.call(null, `${SUBDOMAIN}.${handle}`)
17
-
18
- if (!results) return null
19
-
20
- for (let i = 0; i < results.length; i++) {
21
- // If the line does not start with "did=", skip it
22
- if (!results[i].startsWith(PREFIX)) continue
23
-
24
- // Ensure no other entry starting with "did=" follows
25
- for (let j = i + 1; j < results.length; j++) {
26
- if (results[j].startsWith(PREFIX)) return null
27
- }
28
-
29
- // Note: No trimming (to be consistent with spec)
30
- const did = results[i].slice(PREFIX.length)
31
-
32
- // Invalid DBS record
33
- return isResolvedHandle(did) ? did : null
34
- }
35
-
36
- return null
37
- }
38
- }
@@ -1,53 +0,0 @@
1
- import {
2
- HandleResolver,
3
- ResolveHandleOptions,
4
- ResolvedHandle,
5
- isResolvedHandle,
6
- } from '../types.js'
7
-
8
- export type WellKnownHandleResolverOptions = {
9
- /**
10
- * Fetch function to use for HTTP requests. Allows customizing the request
11
- * behavior, e.g. adding headers, setting a timeout, mocking, etc. The
12
- * provided fetch function will be wrapped with a safeFetchWrap function that
13
- * adds SSRF protection.
14
- *
15
- * @default `globalThis.fetch`
16
- */
17
- fetch?: typeof globalThis.fetch
18
- }
19
-
20
- export class WellKnownHandleResolver implements HandleResolver {
21
- protected readonly fetch: typeof globalThis.fetch
22
-
23
- constructor(options?: WellKnownHandleResolverOptions) {
24
- this.fetch = options?.fetch ?? globalThis.fetch
25
- }
26
-
27
- public async resolve(
28
- handle: string,
29
- options?: ResolveHandleOptions,
30
- ): Promise<ResolvedHandle> {
31
- const url = new URL('/.well-known/atproto-did', `https://${handle}`)
32
-
33
- try {
34
- const response = await this.fetch.call(null, url, {
35
- cache: options?.noCache ? 'no-cache' : undefined,
36
- signal: options?.signal,
37
- redirect: 'error',
38
- })
39
- const text = await response.text()
40
- const firstLine = text.split('\n')[0]!.trim()
41
-
42
- if (isResolvedHandle(firstLine)) return firstLine
43
-
44
- return null
45
- } catch (err) {
46
- // The the request failed, assume the handle does not resolve to a DID,
47
- // unless the failure was due to the signal being aborted.
48
- options?.signal?.throwIfAborted()
49
-
50
- return null
51
- }
52
- }
53
- }
package/src/types.ts DELETED
@@ -1,37 +0,0 @@
1
- import { AtprotoDid, isAtprotoDid } from '@atproto/did'
2
- export type { AtprotoDid, AtprotoIdentityDidMethods } from '@atproto/did'
3
-
4
- export type ResolveHandleOptions = {
5
- signal?: AbortSignal
6
- noCache?: boolean
7
- }
8
-
9
- /**
10
- * @see {@link https://atproto.com/specs/did#blessed-did-methods}
11
- */
12
- export type ResolvedHandle = null | AtprotoDid
13
-
14
- /**
15
- * @see {@link https://atproto.com/specs/did#blessed-did-methods}
16
- */
17
- export function isResolvedHandle(value: unknown): value is ResolvedHandle {
18
- return value === null || isAtprotoDid(value)
19
- }
20
-
21
- export function asResolvedHandle<T>(value: T): null | (T & AtprotoDid) {
22
- return isResolvedHandle(value) ? value : null
23
- }
24
-
25
- export interface HandleResolver {
26
- /**
27
- * @returns the DID that corresponds to the given handle, or `null` if no DID
28
- * is found. `null` should only be returned if no unexpected behavior occurred
29
- * during the resolution process.
30
- * @throws Error if the resolution method fails due to an unexpected error, or
31
- * if the resolution is aborted ({@link ResolveHandleOptions}).
32
- */
33
- resolve(
34
- handle: string,
35
- options?: ResolveHandleOptions,
36
- ): Promise<ResolvedHandle>
37
- }
@@ -1,92 +0,0 @@
1
- import { z } from 'zod'
2
- import { HandleResolverError } from './handle-resolver-error.js'
3
- import {
4
- HandleResolver,
5
- ResolveHandleOptions,
6
- ResolvedHandle,
7
- isResolvedHandle,
8
- } from './types.js'
9
-
10
- export const xrpcErrorSchema = z.object({
11
- error: z.string(),
12
- message: z.string().optional(),
13
- })
14
-
15
- export type XrpcHandleResolverOptions = {
16
- /**
17
- * Fetch function to use for HTTP requests. Allows customizing the request
18
- * behavior, e.g. adding headers, setting a timeout, mocking, etc.
19
- *
20
- * @default globalThis.fetch
21
- */
22
- fetch?: typeof globalThis.fetch
23
- }
24
-
25
- export class XrpcHandleResolver implements HandleResolver {
26
- /**
27
- * URL of the atproto lexicon server. This is the base URL where the
28
- * `com.atproto.identity.resolveHandle` XRPC method is located.
29
- */
30
- protected readonly serviceUrl: URL
31
- protected readonly fetch: typeof globalThis.fetch
32
-
33
- constructor(service: URL | string, options?: XrpcHandleResolverOptions) {
34
- this.serviceUrl = new URL(service)
35
- this.fetch = options?.fetch ?? globalThis.fetch
36
- }
37
-
38
- public async resolve(
39
- handle: string,
40
- options?: ResolveHandleOptions,
41
- ): Promise<ResolvedHandle> {
42
- const url = new URL(
43
- '/xrpc/com.atproto.identity.resolveHandle',
44
- this.serviceUrl,
45
- )
46
- url.searchParams.set('handle', handle)
47
-
48
- const response = await this.fetch.call(null, url, {
49
- cache: options?.noCache ? 'no-cache' : undefined,
50
- signal: options?.signal,
51
- redirect: 'error',
52
- })
53
- const payload = await response.json()
54
-
55
- // The response should either be
56
- // - 400 Bad Request with { error: 'InvalidRequest', message: 'Unable to resolve handle' }
57
- // - 200 OK with { did: NonNullable<ResolvedHandle> }
58
- // Any other response is considered unexpected behavior an should throw an error.
59
-
60
- if (response.status === 400) {
61
- const { error, data } = xrpcErrorSchema.safeParse(payload)
62
- if (error) {
63
- throw new HandleResolverError(
64
- `Invalid response from resolveHandle method: ${error.message}`,
65
- { cause: error },
66
- )
67
- }
68
- if (
69
- data.error === 'InvalidRequest' &&
70
- data.message === 'Unable to resolve handle'
71
- ) {
72
- return null
73
- }
74
- }
75
-
76
- if (!response.ok) {
77
- throw new HandleResolverError(
78
- 'Invalid status code from resolveHandle method',
79
- )
80
- }
81
-
82
- const value: unknown = payload?.did
83
-
84
- if (!isResolvedHandle(value)) {
85
- throw new HandleResolverError(
86
- 'Invalid DID returned from resolveHandle method',
87
- )
88
- }
89
-
90
- return value
91
- }
92
- }
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig/isomorphic.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist",
6
- },
7
- "include": ["./src"],
8
- }
@@ -1 +0,0 @@
1
- {"version":"7.0.0-dev.20260614.1","root":["./src/atproto-doh-handle-resolver.ts","./src/atproto-handle-resolver.ts","./src/cached-handle-resolver.ts","./src/create-handle-resolver.ts","./src/handle-resolver-error.ts","./src/index.ts","./src/types.ts","./src/xrpc-handle-resolver.ts","./src/internal-resolvers/dns-handle-resolver.ts","./src/internal-resolvers/well-known-handler-resolver.ts"]}
package/tsconfig.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "include": [],
3
- "references": [{ "path": "./tsconfig.build.json" }],
4
- }