@atproto-labs/handle-resolver 0.0.1 → 0.1.0

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +9 -7
  2. package/README.md +148 -0
  3. package/dist/app-view-handle-resolver.d.ts +33 -0
  4. package/dist/app-view-handle-resolver.d.ts.map +1 -0
  5. package/dist/{atproto-lexicon-handle-resolver.js → app-view-handle-resolver.js} +28 -15
  6. package/dist/app-view-handle-resolver.js.map +1 -0
  7. package/dist/atproto-doh-handle-resolver.d.ts +9 -0
  8. package/dist/atproto-doh-handle-resolver.d.ts.map +1 -0
  9. package/dist/atproto-doh-handle-resolver.js +98 -0
  10. package/dist/atproto-doh-handle-resolver.js.map +1 -0
  11. package/dist/atproto-handle-resolver.d.ts +22 -0
  12. package/dist/atproto-handle-resolver.d.ts.map +1 -0
  13. package/dist/atproto-handle-resolver.js +69 -0
  14. package/dist/atproto-handle-resolver.js.map +1 -0
  15. package/dist/cached-handle-resolver.d.ts +8 -12
  16. package/dist/cached-handle-resolver.d.ts.map +1 -1
  17. package/dist/cached-handle-resolver.js +17 -6
  18. package/dist/cached-handle-resolver.js.map +1 -1
  19. package/dist/index.d.ts +4 -6
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +6 -10
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal-resolvers/dns-handle-resolver.d.ts +12 -0
  24. package/dist/internal-resolvers/dns-handle-resolver.d.ts.map +1 -0
  25. package/dist/internal-resolvers/dns-handle-resolver.js +38 -0
  26. package/dist/internal-resolvers/dns-handle-resolver.js.map +1 -0
  27. package/dist/internal-resolvers/well-known-handler-resolver.d.ts +18 -0
  28. package/dist/internal-resolvers/well-known-handler-resolver.d.ts.map +1 -0
  29. package/dist/{well-known-handler-resolver.js → internal-resolvers/well-known-handler-resolver.js} +9 -7
  30. package/dist/internal-resolvers/well-known-handler-resolver.js.map +1 -0
  31. package/dist/{handle-resolver.d.ts → types.d.ts} +13 -6
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +12 -0
  34. package/dist/types.js.map +1 -0
  35. package/package.json +6 -7
  36. package/src/{atproto-lexicon-handle-resolver.ts → app-view-handle-resolver.ts} +34 -27
  37. package/src/atproto-doh-handle-resolver.ts +123 -0
  38. package/src/atproto-handle-resolver.ts +75 -0
  39. package/src/cached-handle-resolver.ts +19 -27
  40. package/src/index.ts +6 -8
  41. package/src/internal-resolvers/dns-handle-resolver.ts +38 -0
  42. package/src/{well-known-handler-resolver.ts → internal-resolvers/well-known-handler-resolver.ts} +20 -15
  43. package/src/types.ts +33 -0
  44. package/tsconfig.build.json +1 -1
  45. package/dist/atproto-lexicon-handle-resolver.d.ts +0 -36
  46. package/dist/atproto-lexicon-handle-resolver.d.ts.map +0 -1
  47. package/dist/atproto-lexicon-handle-resolver.js.map +0 -1
  48. package/dist/handle-resolver.d.ts.map +0 -1
  49. package/dist/handle-resolver.js +0 -9
  50. package/dist/handle-resolver.js.map +0 -1
  51. package/dist/serial-handle-resolver.d.ts +0 -7
  52. package/dist/serial-handle-resolver.d.ts.map +0 -1
  53. package/dist/serial-handle-resolver.js +0 -29
  54. package/dist/serial-handle-resolver.js.map +0 -1
  55. package/dist/universal-handle-resolver.d.ts +0 -32
  56. package/dist/universal-handle-resolver.d.ts.map +0 -1
  57. package/dist/universal-handle-resolver.js +0 -25
  58. package/dist/universal-handle-resolver.js.map +0 -1
  59. package/dist/well-known-handler-resolver.d.ts +0 -11
  60. package/dist/well-known-handler-resolver.d.ts.map +0 -1
  61. package/dist/well-known-handler-resolver.js.map +0 -1
  62. package/src/handle-resolver.ts +0 -27
  63. package/src/serial-handle-resolver.ts +0 -29
  64. package/src/universal-handle-resolver.ts +0 -58
@@ -0,0 +1,12 @@
1
+ import { HandleResolver, ResolvedHandle } from '../types';
2
+ /**
3
+ * DNS TXT record resolver. Return `null` if the hostname successfully does not
4
+ * resolve to a valid DID. Throw an error if an unexpected error occurs.
5
+ */
6
+ export type ResolveTxt = (hostname: string) => Promise<null | string[]>;
7
+ export declare class DnsHandleResolver implements HandleResolver {
8
+ protected resolveTxt: ResolveTxt;
9
+ constructor(resolveTxt: ResolveTxt);
10
+ resolve(handle: string): Promise<ResolvedHandle>;
11
+ }
12
+ //# sourceMappingURL=dns-handle-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dns-handle-resolver.d.ts","sourceRoot":"","sources":["../../src/internal-resolvers/dns-handle-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAoB,MAAM,UAAU,CAAA;AAK3E;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,MAAM,EAAE,CAAC,CAAA;AAEvE,qBAAa,iBAAkB,YAAW,cAAc;IAC1C,SAAS,CAAC,UAAU,EAAE,UAAU;gBAAtB,UAAU,EAAE,UAAU;IAEtC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAuBvD"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DnsHandleResolver = void 0;
4
+ const types_1 = require("../types");
5
+ const SUBDOMAIN = '_atproto';
6
+ const PREFIX = 'did=';
7
+ class DnsHandleResolver {
8
+ constructor(resolveTxt) {
9
+ Object.defineProperty(this, "resolveTxt", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: resolveTxt
14
+ });
15
+ }
16
+ async resolve(handle) {
17
+ const results = await this.resolveTxt.call(null, `${SUBDOMAIN}.${handle}`);
18
+ if (!results)
19
+ return null;
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))
23
+ continue;
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))
27
+ return null;
28
+ }
29
+ // Note: No trimming (to be consistent with spec)
30
+ const did = results[i].slice(PREFIX.length);
31
+ // Invalid DBS record
32
+ return (0, types_1.isResolvedHandle)(did) ? did : null;
33
+ }
34
+ return null;
35
+ }
36
+ }
37
+ exports.DnsHandleResolver = DnsHandleResolver;
38
+ //# sourceMappingURL=dns-handle-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dns-handle-resolver.js","sourceRoot":"","sources":["../../src/internal-resolvers/dns-handle-resolver.ts"],"names":[],"mappings":";;;AAAA,oCAA2E;AAE3E,MAAM,SAAS,GAAG,UAAU,CAAA;AAC5B,MAAM,MAAM,GAAG,MAAM,CAAA;AAQrB,MAAa,iBAAiB;IAC5B,YAAsB,UAAsB;QAAhC;;;;mBAAU,UAAU;WAAY;IAAG,CAAC;IAEhD,KAAK,CAAC,OAAO,CAAC,MAAc;QAC1B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,SAAS,IAAI,MAAM,EAAE,CAAC,CAAA;QAE1E,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QAEzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,kDAAkD;YAClD,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;gBAAE,SAAQ;YAE5C,qDAAqD;YACrD,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC5C,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;oBAAE,OAAO,IAAI,CAAA;YAChD,CAAC;YAED,iDAAiD;YACjD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAE3C,qBAAqB;YACrB,OAAO,IAAA,wBAAgB,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;QAC3C,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;CACF;AA1BD,8CA0BC"}
@@ -0,0 +1,18 @@
1
+ import { ResolveOptions, HandleResolver, ResolvedHandle } from '../types.js';
2
+ export type WellKnownHandleResolverOptions = {
3
+ /**
4
+ * Fetch function to use for HTTP requests. Allows customizing the request
5
+ * behavior, e.g. adding headers, setting a timeout, mocking, etc. The
6
+ * provided fetch function will be wrapped with a safeFetchWrap function that
7
+ * adds SSRF protection.
8
+ *
9
+ * @default `globalThis.fetch`
10
+ */
11
+ fetch?: typeof globalThis.fetch;
12
+ };
13
+ export declare class WellKnownHandleResolver implements HandleResolver {
14
+ protected readonly fetch: typeof globalThis.fetch;
15
+ constructor(options?: WellKnownHandleResolverOptions);
16
+ resolve(handle: string, options?: ResolveOptions): Promise<ResolvedHandle>;
17
+ }
18
+ //# sourceMappingURL=well-known-handler-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"well-known-handler-resolver.d.ts","sourceRoot":"","sources":["../../src/internal-resolvers/well-known-handler-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EAEf,MAAM,aAAa,CAAA;AAEpB,MAAM,MAAM,8BAA8B,GAAG;IAC3C;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAChC,CAAA;AAED,qBAAa,uBAAwB,YAAW,cAAc;IAC5D,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;gBAErC,OAAO,CAAC,EAAE,8BAA8B;IAIvC,OAAO,CAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC;CA0B3B"}
@@ -1,28 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WellKnownHandleResolver = void 0;
4
- const handle_resolver_js_1 = require("./handle-resolver.js");
4
+ const types_js_1 = require("../types.js");
5
5
  class WellKnownHandleResolver {
6
- constructor({ fetch = globalThis.fetch, } = {}) {
6
+ constructor(options) {
7
7
  Object.defineProperty(this, "fetch", {
8
8
  enumerable: true,
9
9
  configurable: true,
10
10
  writable: true,
11
11
  value: void 0
12
12
  });
13
- this.fetch = fetch;
13
+ this.fetch = options?.fetch ?? globalThis.fetch;
14
14
  }
15
15
  async resolve(handle, options) {
16
16
  const url = new URL('/.well-known/atproto-did', `https://${handle}`);
17
17
  const headers = new Headers();
18
18
  if (options?.noCache)
19
19
  headers.set('cache-control', 'no-cache');
20
- const request = new Request(url, { headers, signal: options?.signal });
21
20
  try {
22
- const response = await (0, this.fetch)(request);
21
+ const response = await this.fetch.call(null, url, {
22
+ headers,
23
+ signal: options?.signal,
24
+ redirect: 'error',
25
+ });
23
26
  const text = await response.text();
24
27
  const firstLine = text.split('\n')[0].trim();
25
- if ((0, handle_resolver_js_1.isResolvedHandle)(firstLine))
28
+ if ((0, types_js_1.isResolvedHandle)(firstLine))
26
29
  return firstLine;
27
30
  return null;
28
31
  }
@@ -30,7 +33,6 @@ class WellKnownHandleResolver {
30
33
  // The the request failed, assume the handle does not resolve to a DID,
31
34
  // unless the failure was due to the signal being aborted.
32
35
  options?.signal?.throwIfAborted();
33
- // TODO: propagate some errors as-is (?)
34
36
  return null;
35
37
  }
36
38
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"well-known-handler-resolver.js","sourceRoot":"","sources":["../../src/internal-resolvers/well-known-handler-resolver.ts"],"names":[],"mappings":";;;AAAA,0CAKoB;AAcpB,MAAa,uBAAuB;IAGlC,YAAY,OAAwC;QAFjC;;;;;WAA8B;QAG/C,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,UAAU,CAAC,KAAK,CAAA;IACjD,CAAC;IAEM,KAAK,CAAC,OAAO,CAClB,MAAc,EACd,OAAwB;QAExB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,0BAA0B,EAAE,WAAW,MAAM,EAAE,CAAC,CAAA;QAEpE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;QAC7B,IAAI,OAAO,EAAE,OAAO;YAAE,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,CAAC,CAAA;QAE9D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE;gBAChD,OAAO;gBACP,MAAM,EAAE,OAAO,EAAE,MAAM;gBACvB,QAAQ,EAAE,OAAO;aAClB,CAAC,CAAA;YACF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;YAClC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAA;YAE7C,IAAI,IAAA,2BAAgB,EAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAA;YAEjD,OAAO,IAAI,CAAA;QACb,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,uEAAuE;YACvE,0DAA0D;YAC1D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA;YAEjC,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;CACF;AApCD,0DAoCC"}
@@ -1,9 +1,16 @@
1
- import { Did } from '@atproto-labs/did';
2
- export type HandleResolveOptions = {
1
+ import { Did } from '@atproto/did';
2
+ export type ResolveOptions = {
3
3
  signal?: AbortSignal;
4
4
  noCache?: boolean;
5
5
  };
6
- export type ResolvedHandle = null | Did;
6
+ /**
7
+ * @see {@link https://atproto.com/specs/did#blessed-did-methods}
8
+ */
9
+ export type ResolvedHandle = null | Did<'plc' | 'web'>;
10
+ export { type Did };
11
+ /**
12
+ * @see {@link https://atproto.com/specs/did#blessed-did-methods}
13
+ */
7
14
  export declare function isResolvedHandle<T = unknown>(value: T): value is T & ResolvedHandle;
8
15
  export interface HandleResolver {
9
16
  /**
@@ -11,8 +18,8 @@ export interface HandleResolver {
11
18
  * is found. `null` should only be returned if no unexpected behavior occurred
12
19
  * during the resolution process.
13
20
  * @throws Error if the resolution method fails due to an unexpected error, or
14
- * if the resolution is aborted ({@link HandleResolveOptions.signal}).
21
+ * if the resolution is aborted ({@link ResolveOptions}).
15
22
  */
16
- resolve(handle: string, options?: HandleResolveOptions): Promise<ResolvedHandle>;
23
+ resolve(handle: string, options?: ResolveOptions): Promise<ResolvedHandle>;
17
24
  }
18
- //# sourceMappingURL=handle-resolver.d.ts.map
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAA6B,MAAM,cAAc,CAAA;AAE7D,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAA;AAEtD,OAAO,EAAE,KAAK,GAAG,EAAE,CAAA;AAEnB;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAC1C,KAAK,EAAE,CAAC,GACP,KAAK,IAAI,CAAC,GAAG,cAAc,CAE7B;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;CAC3E"}
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isResolvedHandle = void 0;
4
+ const did_1 = require("@atproto/did");
5
+ /**
6
+ * @see {@link https://atproto.com/specs/did#blessed-did-methods}
7
+ */
8
+ function isResolvedHandle(value) {
9
+ return value === null || (0, did_1.isDidPlc)(value) || (0, did_1.isAtprotoDidWeb)(value);
10
+ }
11
+ exports.isResolvedHandle = isResolvedHandle;
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAAA,sCAA6D;AAc7D;;GAEG;AACH,SAAgB,gBAAgB,CAC9B,KAAQ;IAER,OAAO,KAAK,KAAK,IAAI,IAAI,IAAA,cAAQ,EAAC,KAAK,CAAC,IAAI,IAAA,qBAAe,EAAC,KAAK,CAAC,CAAA;AACpE,CAAC;AAJD,4CAIC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto-labs/handle-resolver",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "description": "Isomorphic ATProto handle to DID resolver",
6
6
  "keywords": [
@@ -16,7 +16,7 @@
16
16
  "repository": {
17
17
  "type": "git",
18
18
  "url": "https://github.com/bluesky-social/atproto",
19
- "directory": "packages/handle-resolver"
19
+ "directory": "packages/internal/handle-resolver"
20
20
  },
21
21
  "type": "commonjs",
22
22
  "main": "dist/index.js",
@@ -28,11 +28,10 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "lru-cache": "^10.2.0",
32
- "zod": "^3.22.4",
33
- "@atproto-labs/caching": "0.0.1",
34
- "@atproto-labs/did": "0.0.1",
35
- "@atproto-labs/fetch": "0.0.1"
31
+ "zod": "^3.23.8",
32
+ "@atproto-labs/simple-store": "0.1.0",
33
+ "@atproto-labs/simple-store-memory": "0.1.0",
34
+ "@atproto/did": "0.1.0"
36
35
  },
37
36
  "devDependencies": {
38
37
  "typescript": "^5.3.3"
@@ -1,61 +1,68 @@
1
- import { Fetch } from '@atproto-labs/fetch'
2
1
  import z from 'zod'
3
2
 
4
3
  import {
5
- HandleResolveOptions,
6
4
  HandleResolver,
5
+ ResolveOptions,
7
6
  ResolvedHandle,
8
7
  isResolvedHandle,
9
- } from './handle-resolver.js'
8
+ } from './types.js'
10
9
 
11
10
  export const xrpcErrorSchema = z.object({
12
11
  error: z.string(),
13
12
  message: z.string().optional(),
14
13
  })
15
14
 
16
- export type AtprotoLexiconHandleResolverOptions = {
15
+ export type AppViewHandleResolverOptions = {
17
16
  /**
18
17
  * Fetch function to use for HTTP requests. Allows customizing the request
19
18
  * behavior, e.g. adding headers, setting a timeout, mocking, etc.
20
19
  *
21
20
  * @default globalThis.fetch
22
21
  */
23
- fetch?: Fetch
22
+ fetch?: typeof globalThis.fetch
23
+ }
24
+
25
+ export class AppViewHandleResolver implements HandleResolver {
26
+ static from(
27
+ service: URL | string | HandleResolver,
28
+ options?: AppViewHandleResolverOptions,
29
+ ): HandleResolver {
30
+ if (typeof service === 'string' || service instanceof URL) {
31
+ return new AppViewHandleResolver(service, options)
32
+ }
33
+ return service
34
+ }
24
35
 
25
36
  /**
26
37
  * URL of the atproto lexicon server. This is the base URL where the
27
38
  * `com.atproto.identity.resolveHandle` XRPC method is located.
28
- *
29
- * @default 'https://bsky.social'
30
39
  */
31
- url?: URL | string
32
- }
40
+ protected readonly serviceUrl: URL
41
+ protected readonly fetch: typeof globalThis.fetch
33
42
 
34
- export class AtprotoLexiconHandleResolver implements HandleResolver {
35
- protected readonly url: URL
36
- protected readonly fetch: Fetch
37
-
38
- constructor({
39
- url = 'https://bsky.social/',
40
- fetch = globalThis.fetch,
41
- }: AtprotoLexiconHandleResolverOptions = {}) {
42
- this.url = new URL(url)
43
- this.fetch = fetch
43
+ constructor(service: URL | string, options?: AppViewHandleResolverOptions) {
44
+ this.serviceUrl = new URL(service)
45
+ this.fetch = options?.fetch ?? globalThis.fetch
44
46
  }
45
47
 
46
48
  public async resolve(
47
49
  handle: string,
48
- options?: HandleResolveOptions,
50
+ options?: ResolveOptions,
49
51
  ): Promise<ResolvedHandle> {
50
- const url = new URL('/xrpc/com.atproto.identity.resolveHandle', this.url)
52
+ const url = new URL(
53
+ '/xrpc/com.atproto.identity.resolveHandle',
54
+ this.serviceUrl,
55
+ )
51
56
  url.searchParams.set('handle', handle)
52
57
 
53
58
  const headers = new Headers()
54
59
  if (options?.noCache) headers.set('cache-control', 'no-cache')
55
60
 
56
- const request = new Request(url, { headers, signal: options?.signal })
57
-
58
- const response = await this.fetch.call(null, request)
61
+ const response = await this.fetch.call(null, url, {
62
+ headers,
63
+ signal: options?.signal,
64
+ redirect: 'error',
65
+ })
59
66
  const payload = await response.json()
60
67
 
61
68
  // The response should either be
@@ -74,13 +81,13 @@ export class AtprotoLexiconHandleResolver implements HandleResolver {
74
81
  }
75
82
 
76
83
  if (!response.ok) {
77
- throw new Error('Invalid response from resolveHandle method')
84
+ throw new TypeError('Invalid response from resolveHandle method')
78
85
  }
79
86
 
80
87
  const value: unknown = payload?.did
81
88
 
82
- if (!value || !isResolvedHandle(value)) {
83
- throw new Error('Invalid DID returned from resolveHandle method')
89
+ if (!isResolvedHandle(value)) {
90
+ throw new TypeError('Invalid DID returned from resolveHandle method')
84
91
  }
85
92
 
86
93
  return value
@@ -0,0 +1,123 @@
1
+ import {
2
+ AtprotoHandleResolver,
3
+ AtprotoHandleResolverOptions,
4
+ } from './atproto-handle-resolver.js'
5
+ import { HandleResolver } from './types.js'
6
+ import { ResolveTxt } from './internal-resolvers/dns-handle-resolver.js'
7
+
8
+ export type AtprotoDohHandleResolverOptions = Omit<
9
+ AtprotoHandleResolverOptions,
10
+ 'resolveTxt' | 'resolveTxtFallback'
11
+ > & {
12
+ dohEndpoint: string | URL
13
+ }
14
+
15
+ export class AtprotoDohHandleResolver
16
+ extends AtprotoHandleResolver
17
+ implements HandleResolver
18
+ {
19
+ constructor(options: AtprotoDohHandleResolverOptions) {
20
+ super({
21
+ ...options,
22
+ resolveTxt: dohResolveTxtFactory(options),
23
+ resolveTxtFallback: undefined,
24
+ })
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Resolver for DNS-over-HTTPS (DoH) handles. Only works with servers supporting
30
+ * Google Flavoured "application/dns-json" queries.
31
+ *
32
+ * @see {@link https://developers.google.com/speed/public-dns/docs/doh/json}
33
+ * @see {@link https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/}
34
+ * @todo Add support for DoH using application/dns-message (?)
35
+ */
36
+ function dohResolveTxtFactory({
37
+ dohEndpoint,
38
+ fetch = globalThis.fetch,
39
+ }: AtprotoDohHandleResolverOptions): ResolveTxt {
40
+ return async (hostname) => {
41
+ const url = new URL(dohEndpoint)
42
+ url.searchParams.set('type', 'TXT')
43
+ url.searchParams.set('name', hostname)
44
+
45
+ const response = await fetch(url, {
46
+ method: 'GET',
47
+ headers: { accept: 'application/dns-json' },
48
+ redirect: 'follow',
49
+ })
50
+ try {
51
+ const contentType = response.headers.get('content-type')?.trim()
52
+ if (!response.ok) {
53
+ const message = contentType?.startsWith('text/plain')
54
+ ? await response.text()
55
+ : `Failed to resolve ${hostname}`
56
+ throw new TypeError(message)
57
+ } else if (contentType !== 'application/dns-json') {
58
+ throw new TypeError('Unexpected response from DoH server')
59
+ }
60
+
61
+ const result = asResult(await response.json())
62
+ return result.Answer?.filter(isAnswerTxt).map(extractTxtData) ?? null
63
+ } finally {
64
+ // Make sure to always cancel the response body as some engines (Node 👀)
65
+ // do not do this automatically.
66
+ // https://undici.nodejs.org/#/?id=garbage-collection
67
+ if (response.bodyUsed === false) {
68
+ // Handle rejection asynchronously
69
+ void response.body?.cancel().catch(onCancelError)
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ function onCancelError(err: unknown) {
76
+ if (!(err instanceof DOMException) || err.name !== 'AbortError') {
77
+ console.error('An error occurred while cancelling the response body:', err)
78
+ }
79
+ }
80
+
81
+ type Result = { Status: number; Answer?: Answer[] }
82
+ function isResult(result: unknown): result is Result {
83
+ if (typeof result !== 'object' || result === null) return false
84
+ if (!('Status' in result) || typeof result.Status !== 'number') return false
85
+ if ('Answer' in result && !isArrayOf(result.Answer, isAnswer)) return false
86
+ return true
87
+ }
88
+ function asResult(result: unknown): Result {
89
+ if (isResult(result)) return result
90
+ throw new TypeError('Invalid DoH response')
91
+ }
92
+
93
+ function isArrayOf<T>(
94
+ value: unknown,
95
+ predicate: (v: unknown) => v is T,
96
+ ): value is T[] {
97
+ return Array.isArray(value) && value.every(predicate)
98
+ }
99
+
100
+ type Answer = { name: string; type: number; data: string; TTL: number }
101
+ function isAnswer(answer: unknown): answer is Answer {
102
+ return (
103
+ typeof answer === 'object' &&
104
+ answer !== null &&
105
+ 'name' in answer &&
106
+ typeof answer.name === 'string' &&
107
+ 'type' in answer &&
108
+ typeof answer.type === 'number' &&
109
+ 'data' in answer &&
110
+ typeof answer.data === 'string' &&
111
+ 'TTL' in answer &&
112
+ typeof answer.TTL === 'number'
113
+ )
114
+ }
115
+
116
+ type AnswerTxt = Answer & { type: 16 }
117
+ function isAnswerTxt(answer: Answer): answer is AnswerTxt {
118
+ return answer.type === 16
119
+ }
120
+
121
+ function extractTxtData(answer: AnswerTxt): string {
122
+ return answer.data.replace(/^"|"$/g, '').replace(/\\"/g, '"')
123
+ }
@@ -0,0 +1,75 @@
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 { HandleResolver, ResolveOptions, ResolvedHandle } from './types.js'
10
+
11
+ export type { ResolveTxt }
12
+ export type AtprotoHandleResolverOptions = WellKnownHandleResolverOptions & {
13
+ resolveTxt: ResolveTxt
14
+ resolveTxtFallback?: ResolveTxt
15
+ }
16
+
17
+ const noop = () => {}
18
+
19
+ /**
20
+ * Implementation of the official ATPROTO handle resolution strategy.
21
+ * This implementation relies on two primitives:
22
+ * - HTTP Well-Known URI resolution (requires a `fetch()` implementation)
23
+ * - DNS TXT record resolution (requires a `resolveTxt()` function)
24
+ */
25
+ export class AtprotoHandleResolver implements HandleResolver {
26
+ private readonly httpResolver: HandleResolver
27
+ private readonly dnsResolver: HandleResolver
28
+ private readonly dnsResolverFallback?: HandleResolver
29
+
30
+ constructor(options: AtprotoHandleResolverOptions) {
31
+ this.httpResolver = new WellKnownHandleResolver(options)
32
+ this.dnsResolver = new DnsHandleResolver(options.resolveTxt)
33
+ this.dnsResolverFallback = options.resolveTxtFallback
34
+ ? new DnsHandleResolver(options.resolveTxtFallback)
35
+ : undefined
36
+ }
37
+
38
+ async resolve(
39
+ handle: string,
40
+ options?: ResolveOptions,
41
+ ): Promise<ResolvedHandle> {
42
+ options?.signal?.throwIfAborted()
43
+
44
+ const abortController = new AbortController()
45
+ const { signal } = abortController
46
+ options?.signal?.addEventListener('abort', () => abortController.abort(), {
47
+ signal,
48
+ })
49
+
50
+ const wrappedOptions = { ...options, signal }
51
+
52
+ try {
53
+ const dnsPromise = this.dnsResolver.resolve(handle, wrappedOptions)
54
+ const httpPromise = this.httpResolver.resolve(handle, wrappedOptions)
55
+
56
+ // Prevent uncaught promise rejection
57
+ httpPromise.catch(noop)
58
+
59
+ const dnsRes = await dnsPromise
60
+ if (dnsRes) return dnsRes
61
+
62
+ signal.throwIfAborted()
63
+
64
+ const res = await httpPromise
65
+ if (res) return res
66
+
67
+ signal.throwIfAborted()
68
+
69
+ return this.dnsResolverFallback?.resolve(handle, wrappedOptions) ?? null
70
+ } finally {
71
+ // Cancel pending requests, and remove "abort" listener on incoming signal
72
+ abortController.abort()
73
+ }
74
+ }
75
+ }
@@ -1,40 +1,32 @@
1
- import { CachedGetter, GenericStore, MemoryStore } from '@atproto-labs/caching'
2
- import {
3
- HandleResolveOptions,
4
- HandleResolver,
5
- ResolvedHandle,
6
- } from './handle-resolver.js'
1
+ import { CachedGetter, SimpleStore } from '@atproto-labs/simple-store'
2
+ import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
3
+ import { ResolveOptions, HandleResolver, ResolvedHandle } from './types.js'
7
4
 
8
- export type CachedHandleResolverOptions = {
9
- /**
10
- * The resolver that will be used to resolve handles.
11
- */
12
- resolver: HandleResolver
5
+ export type HandleCache = SimpleStore<string, ResolvedHandle>
13
6
 
14
- /**
15
- * A store that will be used to cache resolved values.
16
- */
17
- cache?: GenericStore<string, ResolvedHandle>
18
- }
7
+ export class CachedHandleResolver implements HandleResolver {
8
+ private getter: CachedGetter<string, ResolvedHandle>
19
9
 
20
- export class CachedHandleResolver
21
- extends CachedGetter<string, ResolvedHandle>
22
- implements HandleResolver
23
- {
24
- constructor({
25
- resolver,
26
- cache = new MemoryStore<string, ResolvedHandle>({
10
+ constructor(
11
+ /**
12
+ * The resolver that will be used to resolve handles.
13
+ */
14
+ resolver: HandleResolver,
15
+ cache: HandleCache = new SimpleStoreMemory<string, ResolvedHandle>({
27
16
  max: 1000,
28
17
  ttl: 10 * 60e3,
29
18
  }),
30
- }: CachedHandleResolverOptions) {
31
- super((handle, options) => resolver.resolve(handle, options), cache)
19
+ ) {
20
+ this.getter = new CachedGetter<string, ResolvedHandle>(
21
+ (handle, options) => resolver.resolve(handle, options),
22
+ cache,
23
+ )
32
24
  }
33
25
 
34
26
  async resolve(
35
27
  handle: string,
36
- options?: HandleResolveOptions,
28
+ options?: ResolveOptions,
37
29
  ): Promise<ResolvedHandle> {
38
- return this.get(handle, options)
30
+ return this.getter.get(handle, options)
39
31
  }
40
32
  }
package/src/index.ts CHANGED
@@ -1,11 +1,9 @@
1
- export * from './handle-resolver.js'
1
+ export * from './types.js'
2
2
 
3
- // Main export
4
- export * from './universal-handle-resolver.js'
5
- export { UniversalHandleResolver as default } from './universal-handle-resolver.js'
3
+ // Main Handle Resolvers strategies
4
+ export * from './app-view-handle-resolver.js'
5
+ export * from './atproto-doh-handle-resolver.js'
6
+ export * from './atproto-handle-resolver.js'
6
7
 
7
- // Utilities
8
+ // Handle Resolver Caching utility
8
9
  export * from './cached-handle-resolver.js'
9
- export * from './atproto-lexicon-handle-resolver.js'
10
- export * from './serial-handle-resolver.js'
11
- export * from './well-known-handler-resolver.js'
@@ -0,0 +1,38 @@
1
+ import { HandleResolver, ResolvedHandle, isResolvedHandle } from '../types'
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
+ }