mount public data from the atmosphere to a virtual filesystem (macos only) pdfs.at
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(atproto): resolve DID docs via PLC directory and did:web

+155
+41
Packages/ATProto/Sources/ATProto/Identity/DIDDocument.swift
··· 1 + import Foundation 2 + 3 + public struct DIDDocument: Sendable, Equatable { 4 + public let id: String 5 + public let alsoKnownAs: [String] 6 + public let services: [Service] 7 + 8 + public struct Service: Sendable, Equatable, Decodable { 9 + public let id: String 10 + public let type: String 11 + public let serviceEndpoint: String 12 + } 13 + 14 + /// Handles parsed from `alsoKnownAs` entries like `at://natemoo.re`. 15 + public var handles: [String] { 16 + alsoKnownAs.compactMap { 17 + $0.hasPrefix("at://") ? String($0.dropFirst("at://".count)) : nil 18 + } 19 + } 20 + 21 + /// First PDS endpoint service URL, if present. 22 + public var pdsEndpoint: URL? { 23 + guard let svc = services.first(where: { $0.type == "AtprotoPersonalDataServer" }) else { 24 + return nil 25 + } 26 + return URL(string: svc.serviceEndpoint) 27 + } 28 + } 29 + 30 + extension DIDDocument: Decodable { 31 + enum CodingKeys: String, CodingKey { 32 + case id, alsoKnownAs, service 33 + } 34 + 35 + public init(from decoder: Decoder) throws { 36 + let c = try decoder.container(keyedBy: CodingKeys.self) 37 + id = try c.decode(String.self, forKey: .id) 38 + alsoKnownAs = try c.decodeIfPresent([String].self, forKey: .alsoKnownAs) ?? [] 39 + services = try c.decodeIfPresent([Service].self, forKey: .service) ?? [] 40 + } 41 + }
+49
Packages/ATProto/Sources/ATProto/Identity/DIDResolver.swift
··· 1 + import Foundation 2 + 3 + public struct DIDResolver: Sendable { 4 + let session: URLSession 5 + let plcDirectory: URL 6 + 7 + public init( 8 + session: URLSession = .shared, 9 + plcDirectory: URL = URL(string: "https://plc.directory")! 10 + ) { 11 + self.session = session 12 + self.plcDirectory = plcDirectory 13 + } 14 + 15 + public func resolve(did: DID) async throws -> DIDDocument { 16 + switch did.method { 17 + case .plc: 18 + return try await resolvePLC(did: did) 19 + case .web: 20 + return try await resolveWeb(did: did) 21 + case .none: 22 + throw ATProtoError.invalidIdentity("unsupported DID method: \(did)") 23 + } 24 + } 25 + 26 + func resolvePLC(did: DID) async throws -> DIDDocument { 27 + let url = plcDirectory.appendingPathComponent(did.rawValue) 28 + let (data, response) = try await session.data(for: URLRequest(url: url)) 29 + guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { 30 + throw ATProtoError.invalidIdentity("plc.directory returned non-2xx for \(did)") 31 + } 32 + return try JSONDecoder().decode(DIDDocument.self, from: data) 33 + } 34 + 35 + func resolveWeb(did: DID) async throws -> DIDDocument { 36 + // did:web:<host>[:path...] → https://<host>/<path>/.well-known/did.json 37 + let parts = did.rawValue.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false) 38 + guard parts.count == 3 else { throw ATProtoError.invalidIdentity("malformed did:web \(did)") } 39 + let rest = parts[2].replacingOccurrences(of: ":", with: "/") 40 + guard let url = URL(string: "https://\(rest)/.well-known/did.json") else { 41 + throw ATProtoError.invalidIdentity("bad did:web URL for \(did)") 42 + } 43 + let (data, response) = try await session.data(for: URLRequest(url: url)) 44 + guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { 45 + throw ATProtoError.invalidIdentity("did:web returned non-2xx for \(did)") 46 + } 47 + return try JSONDecoder().decode(DIDDocument.self, from: data) 48 + } 49 + }
+65
Packages/ATProto/Tests/ATProtoTests/DIDResolverTests.swift
··· 1 + import Foundation 2 + import Testing 3 + @testable import ATProto 4 + 5 + @Suite("DIDResolver", .serialized) 6 + struct DIDResolverTests { 7 + @Test("resolves did:plc via plc.directory") 8 + func plcResolve() async throws { 9 + let session = URLProtocolStub.install { request in 10 + if request.url?.absoluteString == "https://plc.directory/did:plc:abc" { 11 + return .json(#""" 12 + { 13 + "id":"did:plc:abc", 14 + "alsoKnownAs":["at://natemoo.re"], 15 + "service":[ 16 + {"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://bsky.social"} 17 + ] 18 + } 19 + """#) 20 + } 21 + return URLProtocolStub.Response(statusCode: 404, headers: [:], body: Data()) 22 + } 23 + defer { URLProtocolStub.reset(session: session) } 24 + 25 + let resolver = DIDResolver(session: session) 26 + let doc = try await resolver.resolve(did: DID("did:plc:abc")!) 27 + #expect(doc.id == "did:plc:abc") 28 + #expect(doc.pdsEndpoint?.absoluteString == "https://bsky.social") 29 + #expect(doc.handles.first == "natemoo.re") 30 + } 31 + 32 + @Test("resolves did:web via .well-known") 33 + func webResolve() async throws { 34 + let session = URLProtocolStub.install { request in 35 + if request.url?.absoluteString == "https://pdfs.at/.well-known/did.json" { 36 + return .json(#""" 37 + { 38 + "id":"did:web:pdfs.at", 39 + "service":[ 40 + {"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://pds.pdfs.at"} 41 + ] 42 + } 43 + """#) 44 + } 45 + return URLProtocolStub.Response(statusCode: 404, headers: [:], body: Data()) 46 + } 47 + defer { URLProtocolStub.reset(session: session) } 48 + 49 + let resolver = DIDResolver(session: session) 50 + let doc = try await resolver.resolve(did: DID("did:web:pdfs.at")!) 51 + #expect(doc.pdsEndpoint?.absoluteString == "https://pds.pdfs.at") 52 + } 53 + 54 + @Test("throws when no PDS endpoint in doc") 55 + func missingPDS() async throws { 56 + let session = URLProtocolStub.install { _ in 57 + .json(#"{"id":"did:plc:abc","service":[]}"#) 58 + } 59 + defer { URLProtocolStub.reset(session: session) } 60 + 61 + let resolver = DIDResolver(session: session) 62 + let doc = try await resolver.resolve(did: DID("did:plc:abc")!) 63 + #expect(doc.pdsEndpoint == nil) 64 + } 65 + }