@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.
- package/CHANGELOG.md +9 -7
- package/README.md +148 -0
- package/dist/app-view-handle-resolver.d.ts +33 -0
- package/dist/app-view-handle-resolver.d.ts.map +1 -0
- package/dist/{atproto-lexicon-handle-resolver.js → app-view-handle-resolver.js} +28 -15
- package/dist/app-view-handle-resolver.js.map +1 -0
- package/dist/atproto-doh-handle-resolver.d.ts +9 -0
- package/dist/atproto-doh-handle-resolver.d.ts.map +1 -0
- package/dist/atproto-doh-handle-resolver.js +98 -0
- package/dist/atproto-doh-handle-resolver.js.map +1 -0
- package/dist/atproto-handle-resolver.d.ts +22 -0
- package/dist/atproto-handle-resolver.d.ts.map +1 -0
- package/dist/atproto-handle-resolver.js +69 -0
- package/dist/atproto-handle-resolver.js.map +1 -0
- package/dist/cached-handle-resolver.d.ts +8 -12
- package/dist/cached-handle-resolver.d.ts.map +1 -1
- package/dist/cached-handle-resolver.js +17 -6
- package/dist/cached-handle-resolver.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -10
- package/dist/index.js.map +1 -1
- package/dist/internal-resolvers/dns-handle-resolver.d.ts +12 -0
- package/dist/internal-resolvers/dns-handle-resolver.d.ts.map +1 -0
- package/dist/internal-resolvers/dns-handle-resolver.js +38 -0
- package/dist/internal-resolvers/dns-handle-resolver.js.map +1 -0
- package/dist/internal-resolvers/well-known-handler-resolver.d.ts +18 -0
- package/dist/internal-resolvers/well-known-handler-resolver.d.ts.map +1 -0
- package/dist/{well-known-handler-resolver.js → internal-resolvers/well-known-handler-resolver.js} +9 -7
- package/dist/internal-resolvers/well-known-handler-resolver.js.map +1 -0
- package/dist/{handle-resolver.d.ts → types.d.ts} +13 -6
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +6 -7
- package/src/{atproto-lexicon-handle-resolver.ts → app-view-handle-resolver.ts} +34 -27
- package/src/atproto-doh-handle-resolver.ts +123 -0
- package/src/atproto-handle-resolver.ts +75 -0
- package/src/cached-handle-resolver.ts +19 -27
- package/src/index.ts +6 -8
- package/src/internal-resolvers/dns-handle-resolver.ts +38 -0
- package/src/{well-known-handler-resolver.ts → internal-resolvers/well-known-handler-resolver.ts} +20 -15
- package/src/types.ts +33 -0
- package/tsconfig.build.json +1 -1
- package/dist/atproto-lexicon-handle-resolver.d.ts +0 -36
- package/dist/atproto-lexicon-handle-resolver.d.ts.map +0 -1
- package/dist/atproto-lexicon-handle-resolver.js.map +0 -1
- package/dist/handle-resolver.d.ts.map +0 -1
- package/dist/handle-resolver.js +0 -9
- package/dist/handle-resolver.js.map +0 -1
- package/dist/serial-handle-resolver.d.ts +0 -7
- package/dist/serial-handle-resolver.d.ts.map +0 -1
- package/dist/serial-handle-resolver.js +0 -29
- package/dist/serial-handle-resolver.js.map +0 -1
- package/dist/universal-handle-resolver.d.ts +0 -32
- package/dist/universal-handle-resolver.d.ts.map +0 -1
- package/dist/universal-handle-resolver.js +0 -25
- package/dist/universal-handle-resolver.js.map +0 -1
- package/dist/well-known-handler-resolver.d.ts +0 -11
- package/dist/well-known-handler-resolver.d.ts.map +0 -1
- package/dist/well-known-handler-resolver.js.map +0 -1
- package/src/handle-resolver.ts +0 -27
- package/src/serial-handle-resolver.ts +0 -29
- 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"}
|
package/dist/{well-known-handler-resolver.js → internal-resolvers/well-known-handler-resolver.js}
RENAMED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WellKnownHandleResolver = void 0;
|
|
4
|
-
const
|
|
4
|
+
const types_js_1 = require("../types.js");
|
|
5
5
|
class WellKnownHandleResolver {
|
|
6
|
-
constructor(
|
|
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
|
|
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,
|
|
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
|
|
2
|
-
export type
|
|
1
|
+
import { Did } from '@atproto/did';
|
|
2
|
+
export type ResolveOptions = {
|
|
3
3
|
signal?: AbortSignal;
|
|
4
4
|
noCache?: boolean;
|
|
5
5
|
};
|
|
6
|
-
|
|
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
|
|
21
|
+
* if the resolution is aborted ({@link ResolveOptions}).
|
|
15
22
|
*/
|
|
16
|
-
resolve(handle: string, options?:
|
|
23
|
+
resolve(handle: string, options?: ResolveOptions): Promise<ResolvedHandle>;
|
|
17
24
|
}
|
|
18
|
-
//# sourceMappingURL=
|
|
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
|
|
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
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"@atproto-labs/
|
|
34
|
-
"@atproto
|
|
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 './
|
|
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
|
|
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?:
|
|
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
|
-
|
|
32
|
-
|
|
40
|
+
protected readonly serviceUrl: URL
|
|
41
|
+
protected readonly fetch: typeof globalThis.fetch
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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?:
|
|
50
|
+
options?: ResolveOptions,
|
|
49
51
|
): Promise<ResolvedHandle> {
|
|
50
|
-
const url = new 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
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
84
|
+
throw new TypeError('Invalid response from resolveHandle method')
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
const value: unknown = payload?.did
|
|
81
88
|
|
|
82
|
-
if (!
|
|
83
|
-
throw new
|
|
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,
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
*/
|
|
17
|
-
cache?: GenericStore<string, ResolvedHandle>
|
|
18
|
-
}
|
|
7
|
+
export class CachedHandleResolver implements HandleResolver {
|
|
8
|
+
private getter: CachedGetter<string, ResolvedHandle>
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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?:
|
|
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 './
|
|
1
|
+
export * from './types.js'
|
|
2
2
|
|
|
3
|
-
// Main
|
|
4
|
-
export * from './
|
|
5
|
-
export
|
|
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
|
-
//
|
|
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
|
+
}
|