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/oauth): add TokenStore protocol + InMemoryTokenStore

+130
+32
Packages/ATProto/Sources/ATProto/OAuth/Storage/StoredTokens.swift
··· 1 + import Foundation 2 + 3 + public struct StoredTokens: Sendable, Equatable, Codable { 4 + public let did: DID 5 + public let accessToken: String 6 + public let refreshToken: String? 7 + public let expiresAt: Date 8 + public let scopes: Set<Scope> 9 + /// Opaque identifier the TokenStore uses to locate the DPoP key 10 + /// (Keychain tag, in-memory map key, etc.). A future Keychain-backed 11 + /// impl reads the key under this identifier. 12 + public let dpopKeyIdentifier: String 13 + public let issuer: String 14 + 15 + public init( 16 + did: DID, 17 + accessToken: String, 18 + refreshToken: String?, 19 + expiresAt: Date, 20 + scopes: Set<Scope>, 21 + dpopKeyIdentifier: String, 22 + issuer: String 23 + ) { 24 + self.did = did 25 + self.accessToken = accessToken 26 + self.refreshToken = refreshToken 27 + self.expiresAt = expiresAt 28 + self.scopes = scopes 29 + self.dpopKeyIdentifier = dpopKeyIdentifier 30 + self.issuer = issuer 31 + } 32 + }
+43
Packages/ATProto/Sources/ATProto/OAuth/Storage/TokenStore.swift
··· 1 + import Foundation 2 + 3 + public protocol TokenStore: Sendable { 4 + func save(_ tokens: StoredTokens) async throws 5 + func load(did: DID) async throws -> StoredTokens? 6 + func delete(did: DID) async throws 7 + func listDIDs() async throws -> [DID] 8 + /// DPoP key operations are paired with token save/load so refresh 9 + /// always has access to the matching key. 10 + func saveDPoPKey(_ key: any DPoPKey, identifier: String) async throws 11 + func loadDPoPKey(identifier: String) async throws -> (any DPoPKey)? 12 + } 13 + 14 + public actor InMemoryTokenStore: TokenStore { 15 + private var tokens: [DID: StoredTokens] = [:] 16 + private var keys: [String: any DPoPKey] = [:] 17 + 18 + public init() {} 19 + 20 + public func save(_ tokens: StoredTokens) async throws { 21 + self.tokens[tokens.did] = tokens 22 + } 23 + 24 + public func load(did: DID) async throws -> StoredTokens? { 25 + tokens[did] 26 + } 27 + 28 + public func delete(did: DID) async throws { 29 + tokens.removeValue(forKey: did) 30 + } 31 + 32 + public func listDIDs() async throws -> [DID] { 33 + Array(tokens.keys) 34 + } 35 + 36 + public func saveDPoPKey(_ key: any DPoPKey, identifier: String) async throws { 37 + keys[identifier] = key 38 + } 39 + 40 + public func loadDPoPKey(identifier: String) async throws -> (any DPoPKey)? { 41 + keys[identifier] 42 + } 43 + }
+55
Packages/ATProto/Tests/ATProtoTests/OAuth/TokenStoreTests.swift
··· 1 + import Foundation 2 + import Testing 3 + @testable import ATProto 4 + 5 + @Suite("TokenStore") 6 + struct TokenStoreTests { 7 + @Test("InMemoryTokenStore persists across load") 8 + func persist() async throws { 9 + let did = DID("did:plc:abc")! 10 + let tokens = StoredTokens( 11 + did: did, 12 + accessToken: "a", 13 + refreshToken: "r", 14 + expiresAt: Date().addingTimeInterval(3600), 15 + scopes: [.atproto], 16 + dpopKeyIdentifier: "key-1", 17 + issuer: "https://bsky.social" 18 + ) 19 + let store = InMemoryTokenStore() 20 + try await store.save(tokens) 21 + 22 + let loaded = try await store.load(did: did) 23 + #expect(loaded?.accessToken == "a") 24 + #expect(loaded?.scopes == [.atproto]) 25 + } 26 + 27 + @Test("delete removes entry") 28 + func delete() async throws { 29 + let did = DID("did:plc:abc")! 30 + let store = InMemoryTokenStore() 31 + try await store.save(StoredTokens( 32 + did: did, accessToken: "a", refreshToken: nil, 33 + expiresAt: Date(), scopes: [], dpopKeyIdentifier: "k", issuer: "i" 34 + )) 35 + try await store.delete(did: did) 36 + #expect(try await store.load(did: did) == nil) 37 + } 38 + 39 + @Test("list returns all DIDs") 40 + func list() async throws { 41 + let store = InMemoryTokenStore() 42 + let didA = DID("did:plc:a")! 43 + let didB = DID("did:plc:b")! 44 + try await store.save(StoredTokens( 45 + did: didA, accessToken: "1", refreshToken: nil, 46 + expiresAt: Date(), scopes: [], dpopKeyIdentifier: "k", issuer: "i" 47 + )) 48 + try await store.save(StoredTokens( 49 + did: didB, accessToken: "2", refreshToken: nil, 50 + expiresAt: Date(), scopes: [], dpopKeyIdentifier: "k", issuer: "i" 51 + )) 52 + let all = Set(try await store.listDIDs()) 53 + #expect(all == [didA, didB]) 54 + } 55 + }