@atcute/identity-resolver 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 (47) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +66 -0
  3. package/dist/did/composite.d.ts +12 -0
  4. package/dist/did/composite.js +17 -0
  5. package/dist/did/composite.js.map +1 -0
  6. package/dist/did/methods/plc.d.ts +12 -0
  7. package/dist/did/methods/plc.js +34 -0
  8. package/dist/did/methods/plc.js.map +1 -0
  9. package/dist/did/methods/web.d.ts +15 -0
  10. package/dist/did/methods/web.js +63 -0
  11. package/dist/did/methods/web.js.map +1 -0
  12. package/dist/did/utils.d.ts +1 -0
  13. package/dist/did/utils.js +4 -0
  14. package/dist/did/utils.js.map +1 -0
  15. package/dist/errors.d.ts +47 -0
  16. package/dist/errors.js +75 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/handle/composite.d.ts +15 -0
  19. package/dist/handle/composite.js +62 -0
  20. package/dist/handle/composite.js.map +1 -0
  21. package/dist/handle/methods/doh-json.d.ts +12 -0
  22. package/dist/handle/methods/doh-json.js +105 -0
  23. package/dist/handle/methods/doh-json.js.map +1 -0
  24. package/dist/handle/methods/well-known.d.ts +10 -0
  25. package/dist/handle/methods/well-known.js +35 -0
  26. package/dist/handle/methods/well-known.js.map +1 -0
  27. package/dist/handle/methods/xrpc.d.ts +12 -0
  28. package/dist/handle/methods/xrpc.js +38 -0
  29. package/dist/handle/methods/xrpc.js.map +1 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.js +10 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/types.d.ts +15 -0
  34. package/dist/types.js +2 -0
  35. package/dist/types.js.map +1 -0
  36. package/lib/did/composite.ts +27 -0
  37. package/lib/did/methods/plc.ts +51 -0
  38. package/lib/did/methods/web.ts +85 -0
  39. package/lib/did/utils.ts +8 -0
  40. package/lib/errors.ts +86 -0
  41. package/lib/handle/composite.ts +94 -0
  42. package/lib/handle/methods/doh-json.ts +138 -0
  43. package/lib/handle/methods/well-known.ts +50 -0
  44. package/lib/handle/methods/xrpc.ts +65 -0
  45. package/lib/index.ts +11 -0
  46. package/lib/types.ts +19 -0
  47. package/package.json +41 -0
@@ -0,0 +1,35 @@
1
+ import { isAtprotoDid } from '@atcute/identity';
2
+ import { FailedResponseError, isResponseOk, pipe, readResponseAsText } from '@atcute/util-fetch';
3
+ import * as err from '../../errors.js';
4
+ const fetchWellKnownHandler = pipe(isResponseOk, readResponseAsText(2048 + 16));
5
+ export class WellKnownHandleResolver {
6
+ #fetch;
7
+ constructor({ fetch: fetchThis = fetch } = {}) {
8
+ this.#fetch = fetchThis;
9
+ }
10
+ async resolve(handle, options) {
11
+ let text;
12
+ try {
13
+ const url = new URL('/.well-known/atproto-did', `https://${handle}`);
14
+ const response = await (0, this.#fetch)(url, {
15
+ signal: options?.signal,
16
+ cache: options?.noCache ? 'no-cache' : 'default',
17
+ redirect: 'error',
18
+ });
19
+ const handled = await fetchWellKnownHandler(response);
20
+ text = handled.text;
21
+ }
22
+ catch (cause) {
23
+ if (cause instanceof FailedResponseError && cause.status === 404) {
24
+ throw new err.DidNotFoundError(handle);
25
+ }
26
+ throw new err.FailedHandleResolutionError(handle, { cause });
27
+ }
28
+ const did = text.split('\n')[0].trim();
29
+ if (!isAtprotoDid(did)) {
30
+ throw new err.InvalidResolvedHandleError(handle, did);
31
+ }
32
+ return did;
33
+ }
34
+ }
35
+ //# sourceMappingURL=well-known.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"well-known.js","sourceRoot":"","sources":["../../../lib/handle/methods/well-known.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAgC,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,IAAI,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEjG,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAOvC,MAAM,qBAAqB,GAAG,IAAI,CAAC,YAAY,EAAE,kBAAkB,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;AAEhF,MAAM,OAAO,uBAAuB;IACnC,MAAM,CAAe;IAErB,YAAY,EAAE,KAAK,EAAE,SAAS,GAAG,KAAK,KAAqC,EAAE;QAC5E,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,MAAc,EAAE,OAA8B;QAC3D,IAAI,IAAY,CAAC;QAEjB,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,0BAA0B,EAAE,WAAW,MAAM,EAAE,CAAC,CAAC;YAErE,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE;gBAC5C,MAAM,EAAE,OAAO,EAAE,MAAM;gBACvB,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;gBAChD,QAAQ,EAAE,OAAO;aACjB,CAAC,CAAC;YAEH,MAAM,OAAO,GAAG,MAAM,qBAAqB,CAAC,QAAQ,CAAC,CAAC;YAEtD,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,KAAK,YAAY,mBAAmB,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAClE,MAAM,IAAI,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;YACxC,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,2BAA2B,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC;QACxC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,GAAG,CAAC,0BAA0B,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,GAAG,CAAC;IACZ,CAAC;CACD"}
@@ -0,0 +1,12 @@
1
+ import { type AtprotoDid, type Handle } from '@atcute/identity';
2
+ import type { HandleResolver, ResolveHandleOptions } from '../../types.js';
3
+ export interface XrpcHandleResolverOptions {
4
+ serviceUrl: string;
5
+ fetch?: typeof fetch;
6
+ }
7
+ export declare class XrpcHandleResolver implements HandleResolver {
8
+ #private;
9
+ readonly serviceUrl: string;
10
+ constructor({ serviceUrl, fetch: fetchThis }: XrpcHandleResolverOptions);
11
+ resolve(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid>;
12
+ }
@@ -0,0 +1,38 @@
1
+ import * as v from '@badrap/valita';
2
+ import { isAtprotoDid } from '@atcute/identity';
3
+ import { FailedResponseError, isResponseOk, parseResponseAsJson, pipe, validateJsonWith, } from '@atcute/util-fetch';
4
+ import * as err from '../../errors.js';
5
+ const response = v.object({
6
+ did: v.string().assert((input) => isAtprotoDid(input)),
7
+ });
8
+ const fetchXrpcHandler = pipe(isResponseOk, parseResponseAsJson(/^application\/json$/, 4 * 1024), validateJsonWith(response, { mode: 'passthrough' }));
9
+ export class XrpcHandleResolver {
10
+ serviceUrl;
11
+ #fetch;
12
+ constructor({ serviceUrl, fetch: fetchThis = fetch }) {
13
+ this.serviceUrl = serviceUrl;
14
+ this.#fetch = fetchThis;
15
+ }
16
+ async resolve(handle, options) {
17
+ let json;
18
+ try {
19
+ const url = new URL(`/xrpc/com.atproto.identity.resolveHandle`, this.serviceUrl);
20
+ url.searchParams.set('handle', handle);
21
+ const response = await this.#fetch(url, {
22
+ signal: options?.signal,
23
+ cache: options?.noCache ? 'no-cache' : 'default',
24
+ headers: { accept: 'application/json' },
25
+ });
26
+ const handled = await fetchXrpcHandler(response);
27
+ json = handled.json;
28
+ }
29
+ catch (cause) {
30
+ if (cause instanceof FailedResponseError && cause.status === 400) {
31
+ throw new err.DidNotFoundError(handle);
32
+ }
33
+ throw new err.FailedHandleResolutionError(handle, { cause });
34
+ }
35
+ return json.did;
36
+ }
37
+ }
38
+ //# sourceMappingURL=xrpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xrpc.js","sourceRoot":"","sources":["../../../lib/handle/methods/xrpc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,gBAAgB,CAAC;AAEpC,OAAO,EAAE,YAAY,EAAgC,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EACN,mBAAmB,EACnB,YAAY,EACZ,mBAAmB,EACnB,IAAI,EACJ,gBAAgB,GAChB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAC;AAGvC,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC;IACzB,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;CACtD,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,IAAI,CAC5B,YAAY,EACZ,mBAAmB,CAAC,qBAAqB,EAAE,CAAC,GAAG,IAAI,CAAC,EACpD,gBAAgB,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CACnD,CAAC;AAOF,MAAM,OAAO,kBAAkB;IACrB,UAAU,CAAS;IAC5B,MAAM,CAAe;IAErB,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,GAAG,KAAK,EAA6B;QAC9E,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,MAAc,EAAE,OAA8B;QAC3D,IAAI,IAA8B,CAAC;QAEnC,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,0CAA0C,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YACjF,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAEvC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;gBACvC,MAAM,EAAE,OAAO,EAAE,MAAM;gBACvB,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;gBAChD,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;aACvC,CAAC,CAAC;YAEH,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAEjD,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,KAAK,YAAY,mBAAmB,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAClE,MAAM,IAAI,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;YACxC,CAAC;YAED,MAAM,IAAI,GAAG,CAAC,2BAA2B,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,IAAI,CAAC,GAAG,CAAC;IACjB,CAAC;CACD"}
@@ -0,0 +1,9 @@
1
+ export * from './did/composite.js';
2
+ export * from './did/methods/plc.js';
3
+ export * from './did/methods/web.js';
4
+ export * from './handle/composite.js';
5
+ export * from './handle/methods/doh-json.js';
6
+ export * from './handle/methods/well-known.js';
7
+ export * from './handle/methods/xrpc.js';
8
+ export * from './errors.js';
9
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './did/composite.js';
2
+ export * from './did/methods/plc.js';
3
+ export * from './did/methods/web.js';
4
+ export * from './handle/composite.js';
5
+ export * from './handle/methods/doh-json.js';
6
+ export * from './handle/methods/well-known.js';
7
+ export * from './handle/methods/xrpc.js';
8
+ export * from './errors.js';
9
+ export * from './types.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AAErC,cAAc,uBAAuB,CAAC;AACtC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AAEzC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { AtprotoDid, Did, DidDocument, Handle } from '@atcute/identity';
2
+ export interface ResolveDidDocumentOptions {
3
+ signal?: AbortSignal;
4
+ noCache?: boolean;
5
+ }
6
+ export interface DidDocumentResolver<TMethod extends string> {
7
+ resolve(did: Did<TMethod>, options?: ResolveDidDocumentOptions): Promise<DidDocument>;
8
+ }
9
+ export interface ResolveHandleOptions {
10
+ signal?: AbortSignal;
11
+ noCache?: boolean;
12
+ }
13
+ export interface HandleResolver {
14
+ resolve(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid>;
15
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../lib/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,27 @@
1
+ import { extractDidMethod, type Did, type DidDocument } from '@atcute/identity';
2
+
3
+ import * as err from '../errors.js';
4
+ import type { DidDocumentResolver, ResolveDidDocumentOptions } from '../types.js';
5
+
6
+ export interface CompositeDidDocumentResolverOptions<M extends string> {
7
+ methods: { [K in M]: DidDocumentResolver<K> };
8
+ }
9
+
10
+ export class CompositeDidDocumentResolver<M extends string> implements DidDocumentResolver<M> {
11
+ #methods: Map<string, DidDocumentResolver<M>>;
12
+
13
+ constructor({ methods }: CompositeDidDocumentResolverOptions<M>) {
14
+ this.#methods = new Map(Object.entries(methods));
15
+ }
16
+
17
+ async resolve(did: Did<M>, options?: ResolveDidDocumentOptions): Promise<DidDocument> {
18
+ const method = extractDidMethod(did);
19
+
20
+ const resolver = this.#methods.get(method);
21
+ if (resolver === undefined) {
22
+ throw new err.UnsupportedDidMethodError(did);
23
+ }
24
+
25
+ return await resolver.resolve(did, options);
26
+ }
27
+ }
@@ -0,0 +1,51 @@
1
+ import { type Did, type DidDocument } from '@atcute/identity';
2
+ import { FailedResponseError } from '@atcute/util-fetch';
3
+
4
+ import * as err from '../../errors.js';
5
+ import type { DidDocumentResolver, ResolveDidDocumentOptions } from '../../types.js';
6
+ import { fetchDocHandler } from '../utils.js';
7
+
8
+ export interface PlcDidDocumentResolverOptions {
9
+ apiUrl?: string;
10
+ fetch?: typeof fetch;
11
+ }
12
+
13
+ export class PlcDidDocumentResolver implements DidDocumentResolver<'plc'> {
14
+ readonly apiUrl: string;
15
+ #fetch: typeof fetch;
16
+
17
+ constructor({
18
+ apiUrl = 'https://plc.directory',
19
+ fetch: fetchThis = fetch,
20
+ }: PlcDidDocumentResolverOptions = {}) {
21
+ this.apiUrl = apiUrl;
22
+ this.#fetch = fetchThis;
23
+ }
24
+
25
+ async resolve(did: Did<'plc'>, options?: ResolveDidDocumentOptions): Promise<DidDocument> {
26
+ let json: DidDocument;
27
+
28
+ try {
29
+ const url = new URL(`/${encodeURIComponent(did)}`, this.apiUrl);
30
+
31
+ const response = await (0, this.#fetch)(url, {
32
+ signal: options?.signal,
33
+ cache: options?.noCache ? 'no-cache' : 'default',
34
+ redirect: 'error',
35
+ headers: { accept: 'application/did+ld+json,application/json' },
36
+ });
37
+
38
+ const handled = await fetchDocHandler(response);
39
+
40
+ json = handled.json;
41
+ } catch (cause) {
42
+ if (cause instanceof FailedResponseError && cause.status === 404) {
43
+ throw new err.DocumentNotFoundError(did);
44
+ }
45
+
46
+ throw new err.FailedDocumentResolutionError(did, { cause });
47
+ }
48
+
49
+ return json;
50
+ }
51
+ }
@@ -0,0 +1,85 @@
1
+ import { webDidToDocumentUrl, type Did, type DidDocument } from '@atcute/identity';
2
+ import { FailedResponseError } from '@atcute/util-fetch';
3
+
4
+ import * as err from '../../errors.js';
5
+ import type { DidDocumentResolver, ResolveDidDocumentOptions } from '../../types.js';
6
+ import { fetchDocHandler } from '../utils.js';
7
+
8
+ export interface WebDidDocumentResolverOptions {
9
+ fetch?: typeof fetch;
10
+ }
11
+
12
+ export class WebDidDocumentResolver implements DidDocumentResolver<'web'> {
13
+ #fetch: typeof fetch;
14
+
15
+ constructor({ fetch: fetchThis = fetch }: WebDidDocumentResolverOptions = {}) {
16
+ this.#fetch = fetchThis;
17
+ }
18
+
19
+ async resolve(did: Did<'web'>, options?: ResolveDidDocumentOptions): Promise<DidDocument> {
20
+ let json: DidDocument;
21
+
22
+ try {
23
+ const url = webDidToDocumentUrl(did);
24
+
25
+ const response = await (0, this.#fetch)(url, {
26
+ signal: options?.signal,
27
+ cache: options?.noCache ? 'no-cache' : 'default',
28
+ redirect: 'error',
29
+ headers: { accept: 'application/did+ld+json,application/json' },
30
+ });
31
+
32
+ const handled = await fetchDocHandler(response);
33
+
34
+ json = handled.json;
35
+ } catch (cause) {
36
+ if (cause instanceof FailedResponseError && cause.status === 404) {
37
+ throw new err.DocumentNotFoundError(did);
38
+ }
39
+
40
+ throw new err.FailedDocumentResolutionError(did, { cause });
41
+ }
42
+
43
+ return json;
44
+ }
45
+ }
46
+
47
+ export class AtprotoWebDidDocumentResolver implements DidDocumentResolver<'web'> {
48
+ #fetch: typeof fetch;
49
+
50
+ constructor({ fetch: fetchThis = fetch }: WebDidDocumentResolverOptions = {}) {
51
+ this.#fetch = fetchThis;
52
+ }
53
+
54
+ async resolve(did: Did<'web'>, options?: ResolveDidDocumentOptions): Promise<DidDocument> {
55
+ const [host, ...paths] = did.slice(8).split(':').map(decodeURIComponent);
56
+ const url = new URL(`https://${host}/.well-known/did.json`);
57
+
58
+ if (paths.length > 0) {
59
+ throw new err.ImproperDidError(did);
60
+ }
61
+
62
+ let json: DidDocument;
63
+
64
+ try {
65
+ const response = await (0, this.#fetch)(url, {
66
+ signal: options?.signal,
67
+ cache: options?.noCache ? 'no-cache' : 'default',
68
+ redirect: 'error',
69
+ headers: { accept: 'application/did+ld+json,application/json' },
70
+ });
71
+
72
+ const handled = await fetchDocHandler(response);
73
+
74
+ json = handled.json;
75
+ } catch (cause) {
76
+ if (cause instanceof FailedResponseError && cause.status === 404) {
77
+ throw new err.DocumentNotFoundError(did);
78
+ }
79
+
80
+ throw new err.FailedDocumentResolutionError(did, { cause });
81
+ }
82
+
83
+ return json;
84
+ }
85
+ }
@@ -0,0 +1,8 @@
1
+ import { defs } from '@atcute/identity';
2
+ import { isResponseOk, parseResponseAsJson, pipe, validateJsonWith } from '@atcute/util-fetch';
3
+
4
+ export const fetchDocHandler = pipe(
5
+ isResponseOk,
6
+ parseResponseAsJson(/^application\/(did\+ld\+)?json$/, 20 * 1024),
7
+ validateJsonWith(defs.didDocument, { mode: 'passthrough' }),
8
+ );
package/lib/errors.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { Did } from '@atcute/identity';
2
+
3
+ // #region DID document resolution errors
4
+ export class DidDocumentResolutionError extends Error {
5
+ override name = 'DidResolutionError';
6
+ }
7
+
8
+ export class UnsupportedDidMethodError extends DidDocumentResolutionError {
9
+ override name = 'UnsupportedDidMethodError';
10
+
11
+ constructor(public did: Did) {
12
+ super(`unsupported did method; did=${did}`);
13
+ }
14
+ }
15
+
16
+ export class ImproperDidError extends DidDocumentResolutionError {
17
+ override name = 'ImproperDidError';
18
+
19
+ constructor(public did: Did) {
20
+ super(`improper did; did=${did}`);
21
+ }
22
+ }
23
+
24
+ export class DocumentNotFoundError extends DidDocumentResolutionError {
25
+ override name = 'DocumentNotFoundError';
26
+
27
+ constructor(public did: Did) {
28
+ super(`did document not found; did=${did}`);
29
+ }
30
+ }
31
+
32
+ export class FailedDocumentResolutionError extends DidDocumentResolutionError {
33
+ override name = 'FailedDocumentResolutionError';
34
+
35
+ constructor(
36
+ public did: Did,
37
+ options?: ErrorOptions,
38
+ ) {
39
+ super(`failed to resolve did document; did=${did}`, options);
40
+ }
41
+ }
42
+ // #endregion
43
+
44
+ // #region Handle resolution errors
45
+ export class HandleResolutionError extends Error {
46
+ override name = 'HandleResolutionError';
47
+ }
48
+
49
+ export class DidNotFoundError extends HandleResolutionError {
50
+ override name = 'DidNotFoundError';
51
+
52
+ constructor(public handle: string) {
53
+ super(`handle returned no did; handle=${handle}`);
54
+ }
55
+ }
56
+
57
+ export class FailedHandleResolutionError extends HandleResolutionError {
58
+ override name = 'FailedHandleResolutionError';
59
+
60
+ constructor(
61
+ public handle: string,
62
+ options?: ErrorOptions,
63
+ ) {
64
+ super(`failed to resolve handle; handle=${handle}`, options);
65
+ }
66
+ }
67
+
68
+ export class InvalidResolvedHandleError extends HandleResolutionError {
69
+ override name = 'InvalidResolvedHandleError';
70
+
71
+ constructor(
72
+ public handle: string,
73
+ public did: string,
74
+ ) {
75
+ super(`handle returned invalid did; handle=${handle}; did=${did}`);
76
+ }
77
+ }
78
+
79
+ export class AmbiguousHandleError extends HandleResolutionError {
80
+ override name = 'AmbiguousHandleError';
81
+
82
+ constructor(handle: string) {
83
+ super(`handle returned multiple did values; handle=${handle}`);
84
+ }
85
+ }
86
+ // #endregion
@@ -0,0 +1,94 @@
1
+ import type { AtprotoDid, Handle } from '@atcute/identity';
2
+
3
+ import * as err from '../errors.js';
4
+ import type { HandleResolver, ResolveHandleOptions } from '../types.js';
5
+
6
+ export type CompositeStrategy = 'http-first' | 'dns-first' | 'race' | 'both';
7
+
8
+ export interface CompositeHandleResolverOptions {
9
+ /** controls how the resolution is done, defaults to 'race' */
10
+ strategy?: CompositeStrategy;
11
+ /** the methods to use for resolving the handle. */
12
+ methods: Record<'http' | 'dns', HandleResolver>;
13
+ }
14
+
15
+ export class CompositeHandleResolver implements HandleResolver {
16
+ #methods: Record<'http' | 'dns', HandleResolver>;
17
+ strategy: CompositeStrategy;
18
+
19
+ constructor({ methods, strategy = 'race' }: CompositeHandleResolverOptions) {
20
+ this.#methods = methods;
21
+ this.strategy = strategy;
22
+ }
23
+
24
+ async resolve(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid> {
25
+ const { http, dns } = this.#methods;
26
+
27
+ const parentSignal = options?.signal;
28
+ const controller = new AbortController();
29
+ if (parentSignal) {
30
+ parentSignal.addEventListener('abort', () => controller.abort(), { signal: controller.signal });
31
+ }
32
+
33
+ const dnsPromise = dns.resolve(handle, { ...options, signal: controller.signal });
34
+ const httpPromise = http.resolve(handle, { ...options, signal: controller.signal });
35
+
36
+ switch (this.strategy) {
37
+ case 'race': {
38
+ return new Promise((resolve) => {
39
+ dnsPromise.then(
40
+ (did) => {
41
+ controller.abort();
42
+ resolve(did);
43
+ },
44
+ () => resolve(httpPromise),
45
+ );
46
+
47
+ httpPromise.then(
48
+ (did) => {
49
+ controller.abort();
50
+ resolve(did);
51
+ },
52
+ () => resolve(dnsPromise),
53
+ );
54
+ });
55
+ }
56
+ case 'dns-first': {
57
+ httpPromise.catch(noop);
58
+
59
+ const resolved = await dnsPromise.catch(noop);
60
+ if (resolved) {
61
+ controller.abort();
62
+ return resolved;
63
+ }
64
+
65
+ return httpPromise;
66
+ }
67
+ case 'http-first': {
68
+ dnsPromise.catch(noop);
69
+
70
+ const resolved = await httpPromise.catch(noop);
71
+ if (resolved) {
72
+ controller.abort();
73
+ return resolved;
74
+ }
75
+
76
+ return dnsPromise;
77
+ }
78
+ case 'both': {
79
+ const [dnsResponse, httpResponse] = await Promise.allSettled([dnsPromise, httpPromise]);
80
+
81
+ const dnsDid = dnsResponse.status === 'fulfilled' ? dnsResponse.value : undefined;
82
+ const httpDid = httpResponse.status === 'fulfilled' ? httpResponse.value : undefined;
83
+
84
+ if (dnsDid && httpDid && dnsDid !== httpDid) {
85
+ throw new err.AmbiguousHandleError(handle);
86
+ }
87
+
88
+ return dnsDid || httpDid || dnsPromise;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ const noop = () => {};
@@ -0,0 +1,138 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { isAtprotoDid, type AtprotoDid, type Handle } from '@atcute/identity';
4
+ import { isResponseOk, parseResponseAsJson, pipe, validateJsonWith } from '@atcute/util-fetch';
5
+
6
+ import * as err from '../../errors.js';
7
+ import type { HandleResolver, ResolveHandleOptions } from '../../types.js';
8
+
9
+ const uint32 = v.number().assert((input) => Number.isInteger(input) && input >= 0 && input <= 2 ** 32 - 1);
10
+
11
+ const question = v.object({
12
+ name: v.string(),
13
+ type: v.literal(16), // TXT
14
+ });
15
+
16
+ const answer = v.object({
17
+ name: v.string(),
18
+ type: v.literal(16), // TXT
19
+ TTL: uint32,
20
+ data: v.string().chain((input) => {
21
+ return v.ok(input.replace(/^"|"$/g, '').replace(/\\"/g, '"'));
22
+ }),
23
+ });
24
+
25
+ const authority = v.object({
26
+ name: v.string(),
27
+ type: uint32,
28
+ TTL: uint32,
29
+ data: v.string(),
30
+ });
31
+
32
+ const result = v.object({
33
+ /** DNS response code */
34
+ Status: uint32,
35
+ /** Whether response is truncated */
36
+ TC: v.boolean(),
37
+ /** Whether recursive desired bit is set, always true for Google and Cloudflare DoH */
38
+ RD: v.boolean(),
39
+ /** Whether recursive available bit is set, always true for Google and Cloudflare DoH */
40
+ RA: v.boolean(),
41
+ /** Whether response data was validated with DNSSEC */
42
+ AD: v.boolean(),
43
+ /** Whether client asked to disable DNSSEC validation */
44
+ CD: v.boolean(),
45
+ /** Requested records */
46
+ Question: v.tuple([question]),
47
+ /** Answers */
48
+ Answer: v.array(answer).optional(() => []),
49
+ /** Authority */
50
+ Authority: v.array(authority).optional(),
51
+ /** Comment from the DNS server */
52
+ Comment: v.string().optional(),
53
+ });
54
+
55
+ const SUBDOMAIN = '_atproto';
56
+ const PREFIX = 'did=';
57
+
58
+ const fetchDohJsonHandler = pipe(
59
+ isResponseOk,
60
+ parseResponseAsJson(/^application\/(dns-)?json$/, 16 * 1024),
61
+ validateJsonWith(result, { mode: 'passthrough' }),
62
+ );
63
+
64
+ export interface DohJsonHandleResolverOptions {
65
+ dohUrl: string;
66
+ fetch?: typeof fetch;
67
+ }
68
+
69
+ export class DohJsonHandleResolver implements HandleResolver {
70
+ readonly dohUrl: string;
71
+ #fetch: typeof fetch;
72
+
73
+ constructor({ dohUrl, fetch: fetchThis = fetch }: DohJsonHandleResolverOptions) {
74
+ this.dohUrl = dohUrl;
75
+ this.#fetch = fetchThis;
76
+ }
77
+
78
+ async resolve(handle: Handle, options?: ResolveHandleOptions): Promise<AtprotoDid> {
79
+ let json: v.Infer<typeof result>;
80
+
81
+ try {
82
+ const url = new URL(this.dohUrl);
83
+ url.searchParams.set('name', `${SUBDOMAIN}.${handle}`);
84
+ url.searchParams.set('type', 'TXT');
85
+
86
+ const response = await (0, this.#fetch)(url, {
87
+ signal: options?.signal,
88
+ cache: options?.noCache ? 'no-cache' : 'default',
89
+ headers: { accept: 'application/dns-json' },
90
+ });
91
+
92
+ const handled = await fetchDohJsonHandler(response);
93
+
94
+ json = handled.json;
95
+ } catch (cause) {
96
+ throw new err.FailedHandleResolutionError(handle, { cause });
97
+ }
98
+
99
+ const status = json.Status;
100
+ const answers = json.Answer;
101
+
102
+ if (status !== 0 /* NOERROR */) {
103
+ if (status === 3 /* NXDOMAIN */) {
104
+ throw new err.DidNotFoundError(handle);
105
+ }
106
+
107
+ throw new err.FailedHandleResolutionError(handle, {
108
+ cause: new TypeError(`dns returned ${status}`),
109
+ });
110
+ }
111
+
112
+ for (let i = 0, il = answers.length; i < il; i++) {
113
+ const answer = answers[i];
114
+ const data = answer.data;
115
+
116
+ if (!data.startsWith(PREFIX)) {
117
+ continue;
118
+ }
119
+
120
+ for (let j = i + 1; j < il; j++) {
121
+ const data = answers[j].data;
122
+ if (data.startsWith(PREFIX)) {
123
+ throw new err.AmbiguousHandleError(handle);
124
+ }
125
+ }
126
+
127
+ const did = data.slice(PREFIX.length);
128
+ if (!isAtprotoDid(did)) {
129
+ throw new err.InvalidResolvedHandleError(handle, did);
130
+ }
131
+
132
+ return did;
133
+ }
134
+
135
+ // theoretically this shouldn't happen, it should've returned NXDOMAIN
136
+ throw new err.DidNotFoundError(handle);
137
+ }
138
+ }