diff --git a/package-lock.json b/package-lock.json index 54bcf29f..bc39f7e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,11 @@ "dependencies": { "@astronautlabs/mdns": "^1.0.7", "@atcute/bluesky": "^4.0.5", - "@atcute/identity-resolver-node": "^1.0.3", + "@atcute/client": "^5.0.0", + "@atcute/identity": "^2.0.0", + "@atcute/identity-resolver": "^2.0.0", "@atcute/lexicons": "^2.0.0", + "@atcute/password-session": "^1.0.0", "@atcute/repo": "^1.0.0", "@atproto/api": "^0.18.0", "@atproto/oauth-client-node": "^0.3.10", @@ -345,6 +348,19 @@ "@atcute/uint8array": "^1.1.1" } }, + "node_modules/@atcute/client": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@atcute/client/-/client-5.0.0.tgz", + "integrity": "sha512-Dtrc1no1oAtpUTmkBxH0xKSgy8qcnk8orPf/PkEATW+MB3k8FPHtPH2fshiKd55rioZkb+xaN+7A29WtqPQHRA==", + "license": "0BSD", + "dependencies": { + "@atcute/identity": "^2.0.0", + "@atcute/lexicons": "^2.0.0" + }, + "peerDependencies": { + "@atcute/lexicons": "^2.0.0" + } + }, "node_modules/@atcute/crypto": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@atcute/crypto/-/crypto-2.4.1.tgz", @@ -357,85 +373,31 @@ } }, "node_modules/@atcute/identity": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.1.5.tgz", - "integrity": "sha512-5i9nl1UVnBDPCumUwrLNl4BZpGvQ/XABEXbjhiw3PQwRUfpQA8FqByDGxXy2gWpFDrNvQ9yVuOoNsjzxJgjjVA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-2.0.0.tgz", + "integrity": "sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==", "license": "0BSD", - "peer": true, "dependencies": { - "@atcute/lexicons": "^1.3.1", - "@badrap/valita": "^0.4.6" + "@atcute/lexicons": "^2.0.0", + "valibot": "^1.4.0" }, "peerDependencies": { - "@atcute/lexicons": "^1.0.0" + "@atcute/lexicons": "^2.0.0" } }, "node_modules/@atcute/identity-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.2.3.tgz", - "integrity": "sha512-q891zLUMtgoD60y5Pv2YmTg4wIf4ipFuIhZlIuQTTB7qM9sGxmBbe2quzTRCTl3pnsLEmTz7l+3Dszgoutsp5w==", - "license": "0BSD", - "peer": true, - "dependencies": { - "@atcute/lexicons": "^1.3.1", - "@atcute/util-fetch": "^1.0.5", - "@badrap/valita": "^0.4.6" - }, - "peerDependencies": { - "@atcute/identity": "^1.0.0", - "@atcute/lexicons": "^1.0.0" - } - }, - "node_modules/@atcute/identity-resolver-node": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@atcute/identity-resolver-node/-/identity-resolver-node-1.0.4.tgz", - "integrity": "sha512-bXc3/X2xE5A9fMD1QrjQJkmBrxESTqwwezvfzu518Fyf050jejC7a6E+ld7k3hsWJBiVWBqc+GkIjCNBeU3Wag==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-2.0.0.tgz", + "integrity": "sha512-IKg1BDQAF2bIdN10DL6KAXmTjK+3enTU2IRbuani9TsFahBwGZ7O5FiVmTiL6QlGfauGNW5S0xNCOxWXWMoR2Q==", "license": "0BSD", "dependencies": { - "@atcute/lexicons": "^1.3.1" + "@atcute/lexicons": "^2.0.0", + "@atcute/util-fetch": "^2.0.0", + "valibot": "^1.4.0" }, "peerDependencies": { - "@atcute/identity": "^1.0.0", - "@atcute/identity-resolver": "^1.0.0", - "@atcute/lexicons": "^1.0.0" - } - }, - "node_modules/@atcute/identity-resolver-node/node_modules/@atcute/lexicons": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.3.1.tgz", - "integrity": "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw==", - "license": "0BSD", - "dependencies": { - "@atcute/uint8array": "^1.1.1", - "@atcute/util-text": "^1.3.1", - "@standard-schema/spec": "^1.1.0", - "esm-env": "^1.2.2" - } - }, - "node_modules/@atcute/identity-resolver/node_modules/@atcute/lexicons": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.3.1.tgz", - "integrity": "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw==", - "license": "0BSD", - "peer": true, - "dependencies": { - "@atcute/uint8array": "^1.1.1", - "@atcute/util-text": "^1.3.1", - "@standard-schema/spec": "^1.1.0", - "esm-env": "^1.2.2" - } - }, - "node_modules/@atcute/identity/node_modules/@atcute/lexicons": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.3.1.tgz", - "integrity": "sha512-2JVxDmHt+QwsUoPyVYWIN7ZLRLfLx4GeJxKFjA9ofStuby9hCMv7Q4GAPIXuJD8wPv8vrnhr1yRNQhiJX+bthw==", - "license": "0BSD", - "peer": true, - "dependencies": { - "@atcute/uint8array": "^1.1.1", - "@atcute/util-text": "^1.3.1", - "@standard-schema/spec": "^1.1.0", - "esm-env": "^1.2.2" + "@atcute/identity": "^2.0.0", + "@atcute/lexicons": "^2.0.0" } }, "node_modules/@atcute/lex-cli": { @@ -460,36 +422,6 @@ "lex-cli": "cli.mjs" } }, - "node_modules/@atcute/lex-cli/node_modules/@atcute/identity": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-2.0.0.tgz", - "integrity": "sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==", - "dev": true, - "license": "0BSD", - "dependencies": { - "@atcute/lexicons": "^2.0.0", - "valibot": "^1.4.0" - }, - "peerDependencies": { - "@atcute/lexicons": "^2.0.0" - } - }, - "node_modules/@atcute/lex-cli/node_modules/@atcute/identity-resolver": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-2.0.0.tgz", - "integrity": "sha512-IKg1BDQAF2bIdN10DL6KAXmTjK+3enTU2IRbuani9TsFahBwGZ7O5FiVmTiL6QlGfauGNW5S0xNCOxWXWMoR2Q==", - "dev": true, - "license": "0BSD", - "dependencies": { - "@atcute/lexicons": "^2.0.0", - "@atcute/util-fetch": "^2.0.0", - "valibot": "^1.4.0" - }, - "peerDependencies": { - "@atcute/identity": "^2.0.0", - "@atcute/lexicons": "^2.0.0" - } - }, "node_modules/@atcute/lex-cli/node_modules/@atcute/lexicon-resolver": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@atcute/lexicon-resolver/-/lexicon-resolver-1.0.0.tgz", @@ -511,16 +443,6 @@ "@atcute/lexicons": "^2.0.0" } }, - "node_modules/@atcute/lex-cli/node_modules/@atcute/util-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-2.0.0.tgz", - "integrity": "sha512-v+4aFQ/tuBqTV+URDJaFgm3mASWdglKXiPaGutJ1bs7QtQKmPZeesPY5MzW/a+MtI8GWCEJk8X9wOfalPOFSlg==", - "dev": true, - "license": "0BSD", - "dependencies": { - "valibot": "^1.4.0" - } - }, "node_modules/@atcute/lexicon-doc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@atcute/lexicon-doc/-/lexicon-doc-3.0.0.tgz", @@ -538,20 +460,6 @@ "@atcute/lexicons": "^2.0.0" } }, - "node_modules/@atcute/lexicon-doc/node_modules/@atcute/identity": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-2.0.0.tgz", - "integrity": "sha512-YXFsggO7eJYifqkN85+kUXJE2a1iI9AyuzPTDjtS/4WE1Zs1/XiPkWmwZlAgtp+pYhVtjm3mJqy/h/mZ0OnIVw==", - "dev": true, - "license": "0BSD", - "dependencies": { - "@atcute/lexicons": "^2.0.0", - "valibot": "^1.4.0" - }, - "peerDependencies": { - "@atcute/lexicons": "^2.0.0" - } - }, "node_modules/@atcute/lexicons": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-2.0.0.tgz", @@ -588,6 +496,21 @@ "@atcute/uint8array": "^1.1.1" } }, + "node_modules/@atcute/password-session": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@atcute/password-session/-/password-session-1.0.0.tgz", + "integrity": "sha512-yUYkLnYjQMOjKV6jHlE5oBAIDuRn18nTptNjLaF3LrTtsBVEXziwC5ByKkl65MLDHm27iUkzTehB4giqnwQnIA==", + "license": "0BSD", + "dependencies": { + "@atcute/client": "^5.0.0", + "@atcute/identity": "^2.0.0", + "@atcute/lexicons": "^2.0.0" + }, + "peerDependencies": { + "@atcute/identity": "^2.0.0", + "@atcute/lexicons": "^2.0.0" + } + }, "node_modules/@atcute/repo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@atcute/repo/-/repo-1.0.0.tgz", @@ -615,13 +538,12 @@ "license": "0BSD" }, "node_modules/@atcute/util-fetch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.5.tgz", - "integrity": "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-2.0.0.tgz", + "integrity": "sha512-v+4aFQ/tuBqTV+URDJaFgm3mASWdglKXiPaGutJ1bs7QtQKmPZeesPY5MzW/a+MtI8GWCEJk8X9wOfalPOFSlg==", "license": "0BSD", - "peer": true, "dependencies": { - "@badrap/valita": "^0.4.6" + "valibot": "^1.4.0" } }, "node_modules/@atcute/util-text": { @@ -1223,16 +1145,6 @@ "node": ">=6.9.0" } }, - "node_modules/@badrap/valita": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz", - "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18" - } - }, "node_modules/@bufbuild/protobuf": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz", @@ -18245,7 +18157,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.1.tgz", "integrity": "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==", - "devOptional": true, "license": "MIT", "peerDependencies": { "typescript": ">=5" diff --git a/package.json b/package.json index 70c6182c..a37a5da4 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,11 @@ "dependencies": { "@astronautlabs/mdns": "^1.0.7", "@atcute/bluesky": "^4.0.5", - "@atcute/identity-resolver-node": "^1.0.3", + "@atcute/client": "^5.0.0", + "@atcute/identity": "^2.0.0", + "@atcute/identity-resolver": "^2.0.0", "@atcute/lexicons": "^2.0.0", + "@atcute/password-session": "^1.0.0", "@atcute/repo": "^1.0.0", "@atproto/api": "^0.18.0", "@atproto/oauth-client-node": "^0.3.10", diff --git a/src/backend/common/infrastructure/config/client/atproto.ts b/src/backend/common/infrastructure/config/client/atproto.ts index 6915835c..69244dc5 100644 --- a/src/backend/common/infrastructure/config/client/atproto.ts +++ b/src/backend/common/infrastructure/config/client/atproto.ts @@ -1,4 +1,4 @@ -import { AtprotoDid, DidDocument } from "@atproto/oauth-client-node"; +import { AtprotoDid } from "@atcute/lexicons/syntax"; export interface ATProtoUserIdentifierData { /** @@ -8,7 +8,7 @@ export interface ATProtoUserIdentifierData { * * For **Oauth** - your handle minus the @ */ identifier: string - did?: AtprotoDid + did?: string } export interface ATProtoAppData { @@ -20,4 +20,10 @@ export interface ATProtoAppData { * **Use this if you are self-hosting Multi-Scrobbler on localhost or accessed like http://IP:PORT** */ appPassword: string +} + +export interface HandleData { + did: AtprotoDid + pds: string + handle: string } \ No newline at end of file diff --git a/src/backend/common/vendor/atproto/ATProtoAppApiClient.ts b/src/backend/common/vendor/atproto/ATProtoAppApiClient.ts index 9b024483..f88706ec 100644 --- a/src/backend/common/vendor/atproto/ATProtoAppApiClient.ts +++ b/src/backend/common/vendor/atproto/ATProtoAppApiClient.ts @@ -2,7 +2,7 @@ import { AbstractApiOptions } from "../../infrastructure/Atomic.js"; import { TealClientData } from "../../infrastructure/config/client/tealfm.js"; import { Agent, CredentialSession, AtpSessionEvent, AtpSessionData } from "@atproto/api"; import { AbstractATProtoApiClient } from "./AbstractATProtoApiClient.js"; -import { identifierToAtProtoHandle, isDID } from "./atUtils.js"; +import { getATProtoIdentifier, identifierToAtProtoHandle, isDID } from "./atUtils.js"; import { ATProtoAppData, ATProtoUserIdentifierData } from "../../infrastructure/config/client/atproto.js"; export class ATProtoAppApiClient extends AbstractATProtoApiClient { @@ -28,7 +28,7 @@ export class ATProtoAppApiClient extends AbstractATProtoApiClient { } async initClient(): Promise { - const hd = await this.getATProtoIdentifier(this.config); + const hd = await getATProtoIdentifier(this.config, {logger: this.logger, cache: this.cache.cacheAuth}); this.logger.verbose(`Using ${hd.did} on PDS ${hd.pds}`); this.appSession = new CredentialSession(new URL(hd.pds), undefined, (evt: AtpSessionEvent, sess?: AtpSessionData) => { this.cache.cacheAuth.set(`appPwSession-${this.name}-${hd.did}`, sess, '1000h'); @@ -37,7 +37,7 @@ export class ATProtoAppApiClient extends AbstractATProtoApiClient { } restoreSession = async (): Promise => { - const hd = await this.getATProtoIdentifier(this.config); + const hd = await getATProtoIdentifier(this.config, {logger: this.logger, cache: this.cache.cacheAuth}); const savedSession = await this.cache.cacheAuth.get(`appPwSession-${this.name}-${hd.did}`); if (savedSession !== undefined) { try { diff --git a/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts b/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts index b5b65f3f..082a2419 100644 --- a/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts +++ b/src/backend/common/vendor/atproto/AbstractATProtoApiClient.ts @@ -5,23 +5,9 @@ import AbstractApiClient from "../AbstractApiClient.js"; import { Agent, ComAtprotoRepoListRecords } from "@atproto/api"; import { MSCache } from "../../Cache.js"; import { UpstreamError } from "../../errors/UpstreamError.js"; -import { isPortReachableConnect, normalizeWebAddress, streamBodyProgress } from "../../../utils/NetworkUtils.js"; -import { - CompositeDidDocumentResolver, - CompositeHandleResolver, - DohJsonHandleResolver, - PlcDidDocumentResolver, - WebDidDocumentResolver, - WellKnownHandleResolver, -} from "@atcute/identity-resolver"; -import { AtprotoDid, DidDocument } from "@atproto/oauth-client-node"; -import { isNodeNetworkException } from "../../errors/NodeErrors.js"; +import { streamBodyProgress } from "../../../utils/NetworkUtils.js"; import { ATProtoUserIdentifierData } from "../../infrastructure/config/client/atproto.js"; - -interface HandleData { - did: string - pds: string -} +import { getATProtoIdentifier, checkPds } from "./atUtils.js"; export abstract class AbstractATProtoApiClient extends AbstractApiClient { @@ -55,88 +41,8 @@ export abstract class AbstractATProtoApiClient extends AbstractApiClient { } } - protected async getATProtoIdentifier(data: ATProtoUserIdentifierData): Promise { - - let hd: HandleData; - hd = await this.cache.cacheAuth.get(`${this.name}-handleData`); - if (hd !== undefined) { - this.logger.debug('Found cached handle data'); - return hd; - } else { - this.logger.debug('Handle data not cached, attempting to resolve...'); - } - - const handleResolver = new CompositeHandleResolver({ - strategy: "race", - methods: { - dns: new DohJsonHandleResolver({ - dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", - }), - http: new WellKnownHandleResolver(), - }, - }); - - const { - did: givenDid, - identifier - } = data; - - let did: AtprotoDid = givenDid; - if(did === undefined) { - try { - did = await handleResolver.resolve(identifier as `${string}.${string}`); - this.logger.debug(`Resolved ${did}`); - } catch (e) { - throw new Error('Unable to resolve handle', { cause: e }); - } - } - - const docResolver = new CompositeDidDocumentResolver({ - methods: { - plc: new PlcDidDocumentResolver(), - web: new WebDidDocumentResolver(), - }, - }); - - let doc: Awaited>; - try { - doc = await docResolver.resolve(did); - } catch (e) { - throw new Error('Unable to resolve did document', { cause: e }); - } - if (doc.service === undefined || doc.service.length === 0) { - throw new Error('did document did not return a service'); - } - - if (typeof doc.service[0].serviceEndpoint !== 'string') { - throw new Error(`Do not know how to handle this serviceEndpoint data structure!\n${JSON.stringify(doc.service[0].serviceEndpoint)}`); - } - hd = { did, pds: doc.service[0].serviceEndpoint }; - - this.cache.cacheAuth.set(`${this.name}-handleData`, hd, '1d'); - return hd; - } - - async checkPds(data: ATProtoUserIdentifierData): Promise { - let hd: HandleData; - try { - hd = await this.getATProtoIdentifier(data); - } catch(e) { - throw new Error('Unable to get handle data', {cause: e}); - } - - const normal = normalizeWebAddress(hd.pds); - - try { - await isPortReachableConnect(normal.port, {host: normal.url.hostname}); - return true; - } catch (e) { - if(isNodeNetworkException(e)) { - throw new Error('Could not communicate with PDS server', {cause: e}); - } - throw new Error('Unexpected error when trying to communicate with PDS server', {cause: e}); - } + return await checkPds(data, {logger: this.logger, cache: this.cache.cacheAuth}); } async getCAR() { diff --git a/src/backend/common/vendor/atproto/atUtils.ts b/src/backend/common/vendor/atproto/atUtils.ts index 69d77bb7..1464e46d 100644 --- a/src/backend/common/vendor/atproto/atUtils.ts +++ b/src/backend/common/vendor/atproto/atUtils.ts @@ -1,9 +1,26 @@ import { Handle } from "@atcute/lexicons"; -import { isHandle } from "@atcute/lexicons/syntax"; +import { isHandle, AtprotoDid } from "@atcute/lexicons/syntax"; import { Logger } from "@foxxmd/logging"; import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; -import { MaybeLogger } from '../../MaybeLogger.js'; -import { AtprotoDid } from "@atproto/oauth-client-node"; +import { loggerNoop, MaybeLogger } from '../../MaybeLogger.js'; +import { ATProtoUserIdentifierData, HandleData } from "../../infrastructure/config/client/atproto.js"; +import { Cacheable } from "cacheable"; +import { + CompositeDidDocumentResolver, + CompositeHandleResolver, + DohJsonHandleResolver, + PlcDidDocumentResolver, + WebDidDocumentResolver, + WellKnownHandleResolver, +} from "@atcute/identity-resolver"; +import { + getPdsEndpoint, + getAtprotoHandle, + isAtprotoDid, +} from '@atcute/identity'; +import assert from "node:assert"; +import { isPortReachableConnect, normalizeWebAddress } from "../../../utils/NetworkUtils.js"; +import { isNodeNetworkException } from "../../errors/NodeErrors.js"; export const HANDLE_REGEX = new RegExp(/.+\..+/); export const ATSIGN_REGEX = new RegExp(/^@(.+)/); @@ -17,7 +34,7 @@ export interface HandleOptions { export const isDID = (str: string): str is AtprotoDid => DID_REGEX.test(str); export const identifierToAtProtoHandle = (str: string, options: HandleOptions = {}): Handle => { - if(isHandle(str)) { + if (isHandle(str)) { return str; } @@ -26,13 +43,13 @@ export const identifierToAtProtoHandle = (str: string, options: HandleOptions = let cleanHandle: string = str; const atRes = parseRegexSingle(ATSIGN_REGEX, str); - if(atRes !== undefined) { + if (atRes !== undefined) { logger.warn(`Handle '${cleanHandle}' has '@' at beginning, removing this.`); cleanHandle = atRes.groups[0]; } - if(undefined == parseRegexSingle(HANDLE_REGEX, cleanHandle)) { - if(options.defaultDomain === undefined) { + if (undefined == parseRegexSingle(HANDLE_REGEX, cleanHandle)) { + if (options.defaultDomain === undefined) { throw new Error(`No domain found for ATProto handle '${cleanHandle}'`); } @@ -41,8 +58,110 @@ export const identifierToAtProtoHandle = (str: string, options: HandleOptions = cleanHandle = fqId; } - if(isHandle(cleanHandle)) { + if (isHandle(cleanHandle)) { return cleanHandle; } throw new Error(`Identifier ${cleanHandle} is not a valid ATProto handle`); +} + +export interface IdentifyOptions { + logger?: Logger + cache?: Cacheable +} + +export const getATProtoIdentifier = async (data: ATProtoUserIdentifierData, opts: IdentifyOptions = {}): Promise => { + + const { + logger = loggerNoop, + cache + } = opts; + + const key = [data.did, data.identifier].filter(x => x !== undefined).join('-'); + + let hd: HandleData; + if (cache !== undefined) { + hd = await cache.get(`${key}-handleData`); + if (hd !== undefined) { + logger.debug('Found cached handle data'); + return hd; + } else { + logger.debug('Handle data not cached, attempting to resolve...'); + } + } + + + const handleResolver = new CompositeHandleResolver({ + strategy: "race", + methods: { + dns: new DohJsonHandleResolver({ + dohUrl: "https://mozilla.cloudflare-dns.com/dns-query", + }), + http: new WellKnownHandleResolver(), + }, + }); + + const { + did: givenDid, + identifier + } = data; + + assert(isAtprotoDid(givenDid), `Given DID is not an ATProto DID: ${givenDid}`); + let did: AtprotoDid = givenDid; + if (did === undefined) { + try { + did = await handleResolver.resolve(identifier as `${string}.${string}`); + logger.debug(`Resolved ${did}`); + } catch (e) { + throw new Error('Unable to resolve handle', { cause: e }); + } + } + + const docResolver = new CompositeDidDocumentResolver({ + methods: { + plc: new PlcDidDocumentResolver(), + web: new WebDidDocumentResolver(), + }, + }); + + let doc: Awaited>; + try { + doc = await docResolver.resolve(did); + } catch (e) { + throw new Error('Unable to resolve did document', { cause: e }); + } + if (doc.service === undefined || doc.service.length === 0) { + throw new Error('did document did not return a service'); + } + + if (typeof doc.service[0].serviceEndpoint !== 'string') { + throw new Error(`Do not know how to handle this serviceEndpoint data structure!\n${JSON.stringify(doc.service[0].serviceEndpoint)}`); + } + hd = { did, pds: getPdsEndpoint(doc), handle: getAtprotoHandle(doc) }; + + if (cache !== undefined) { + cache.set(`${key}-handleData`, hd, '1d'); + } + + return hd; +} + +export const checkPds = async (data: ATProtoUserIdentifierData, opts: IdentifyOptions): Promise => { + let hd: HandleData; + try { + hd = await getATProtoIdentifier(data, opts); + } catch (e) { + throw new Error('Unable to get handle data', { cause: e }); + } + + const normal = normalizeWebAddress(hd.pds); + + try { + await isPortReachableConnect(normal.port, { host: normal.url.hostname }); + return true; + } catch (e) { + if (isNodeNetworkException(e)) { + throw new Error('Could not communicate with PDS server', { cause: e }); + } + throw new Error('Unexpected error when trying to communicate with PDS server', { cause: e }); + } } \ No newline at end of file