Laravel AT Protocol Client (alpha & unstable)
3
fork

Configure Feed

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

Implement Phase 1: Foundation & Data Objects

+292
+30
src/Contracts/CredentialProvider.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Contracts; 4 + 5 + use SocialDept\AtpClient\Data\AccessToken; 6 + use SocialDept\AtpClient\Data\Credentials; 7 + 8 + interface CredentialProvider 9 + { 10 + /** 11 + * Get credentials for the given identifier 12 + */ 13 + public function getCredentials(string $identifier): ?Credentials; 14 + 15 + /** 16 + * Store new credentials (initial OAuth or app password login) 17 + */ 18 + public function storeCredentials(string $identifier, AccessToken $token): void; 19 + 20 + /** 21 + * Update credentials after token refresh 22 + * CRITICAL: Refresh tokens are single-use! 23 + */ 24 + public function updateCredentials(string $identifier, AccessToken $token): void; 25 + 26 + /** 27 + * Remove credentials 28 + */ 29 + public function removeCredentials(string $identifier): void; 30 + }
+28
src/Contracts/KeyStore.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Contracts; 4 + 5 + use SocialDept\AtpClient\Data\DPoPKey; 6 + 7 + interface KeyStore 8 + { 9 + /** 10 + * Store DPoP key for session 11 + */ 12 + public function store(string $sessionId, DPoPKey $key): void; 13 + 14 + /** 15 + * Retrieve DPoP key 16 + */ 17 + public function get(string $sessionId): ?DPoPKey; 18 + 19 + /** 20 + * Delete DPoP key 21 + */ 22 + public function delete(string $sessionId): void; 23 + 24 + /** 25 + * Check if key exists 26 + */ 27 + public function exists(string $sessionId): bool; 28 + }
+16
src/Contracts/Recordable.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Contracts; 4 + 5 + interface Recordable 6 + { 7 + /** 8 + * Convert record to array for XRPC 9 + */ 10 + public function toArray(): array; 11 + 12 + /** 13 + * Get the record type (lexicon NSID) 14 + */ 15 + public function getType(): string; 16 + }
+25
src/Data/AccessToken.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Data; 4 + 5 + class AccessToken 6 + { 7 + public function __construct( 8 + public readonly string $accessJwt, 9 + public readonly string $refreshJwt, 10 + public readonly string $did, 11 + public readonly \DateTimeInterface $expiresAt, 12 + public readonly ?string $handle = null, 13 + ) {} 14 + 15 + public static function fromResponse(array $data): self 16 + { 17 + return new self( 18 + accessJwt: $data['accessJwt'], 19 + refreshJwt: $data['refreshJwt'], 20 + did: $data['did'], 21 + expiresAt: now()->addSeconds($data['expiresIn'] ?? 300), 22 + handle: $data['handle'] ?? null, 23 + ); 24 + } 25 + }
+24
src/Data/Credentials.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Data; 4 + 5 + class Credentials 6 + { 7 + public function __construct( 8 + public readonly string $identifier, 9 + public readonly string $did, 10 + public readonly string $accessToken, 11 + public readonly string $refreshToken, 12 + public readonly \DateTimeInterface $expiresAt, 13 + ) {} 14 + 15 + public function isExpired(): bool 16 + { 17 + return now()->isAfter($this->expiresAt); 18 + } 19 + 20 + public function expiresIn(): int 21 + { 22 + return now()->diffInSeconds($this->expiresAt, false); 23 + } 24 + }
+24
src/Data/DPoPKey.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Data; 4 + 5 + use Jose\Component\Core\JWK; 6 + 7 + class DPoPKey 8 + { 9 + public function __construct( 10 + public readonly JWK $privateKey, 11 + public readonly JWK $publicKey, 12 + public readonly string $keyId, 13 + ) {} 14 + 15 + public function getPublicJwk(): array 16 + { 17 + return $this->publicKey->jsonSerialize(); 18 + } 19 + 20 + public function getPrivateJwk(): array 21 + { 22 + return $this->privateKey->jsonSerialize(); 23 + } 24 + }
+27
src/Data/StrongRef.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Data; 4 + 5 + class StrongRef 6 + { 7 + public function __construct( 8 + public readonly string $uri, 9 + public readonly string $cid, 10 + ) {} 11 + 12 + public static function fromResponse(array $data): self 13 + { 14 + return new self( 15 + uri: $data['uri'], 16 + cid: $data['cid'], 17 + ); 18 + } 19 + 20 + public function toArray(): array 21 + { 22 + return [ 23 + 'uri' => $this->uri, 24 + 'cid' => $this->cid, 25 + ]; 26 + } 27 + }
+43
src/Providers/ArrayCredentialProvider.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Providers; 4 + 5 + use SocialDept\AtpClient\Contracts\CredentialProvider; 6 + use SocialDept\AtpClient\Data\AccessToken; 7 + use SocialDept\AtpClient\Data\Credentials; 8 + 9 + class ArrayCredentialProvider implements CredentialProvider 10 + { 11 + /** 12 + * In-memory credential storage. 13 + * 14 + * @var array<string, Credentials> 15 + */ 16 + protected array $credentials = []; 17 + 18 + public function getCredentials(string $identifier): ?Credentials 19 + { 20 + return $this->credentials[$identifier] ?? null; 21 + } 22 + 23 + public function storeCredentials(string $identifier, AccessToken $token): void 24 + { 25 + $this->credentials[$identifier] = new Credentials( 26 + identifier: $identifier, 27 + did: $token->did, 28 + accessToken: $token->accessJwt, 29 + refreshToken: $token->refreshJwt, 30 + expiresAt: $token->expiresAt, 31 + ); 32 + } 33 + 34 + public function updateCredentials(string $identifier, AccessToken $token): void 35 + { 36 + $this->storeCredentials($identifier, $token); 37 + } 38 + 39 + public function removeCredentials(string $identifier): void 40 + { 41 + unset($this->credentials[$identifier]); 42 + } 43 + }
+75
src/Storage/EncryptedFileKeyStore.php
··· 1 + <?php 2 + 3 + namespace SocialDept\AtpClient\Storage; 4 + 5 + use Illuminate\Contracts\Encryption\Encrypter; 6 + use Jose\Component\Core\JWK; 7 + use SocialDept\AtpClient\Contracts\KeyStore; 8 + use SocialDept\AtpClient\Data\DPoPKey; 9 + 10 + class EncryptedFileKeyStore implements KeyStore 11 + { 12 + public function __construct( 13 + protected string $storagePath, 14 + protected ?Encrypter $encrypter = null, 15 + ) { 16 + $this->encrypter = $this->encrypter ?? app('encrypter'); 17 + 18 + if (! is_dir($this->storagePath)) { 19 + mkdir($this->storagePath, 0755, true); 20 + } 21 + } 22 + 23 + public function store(string $sessionId, DPoPKey $key): void 24 + { 25 + $data = [ 26 + 'privateKey' => $key->getPrivateJwk(), 27 + 'publicKey' => $key->getPublicJwk(), 28 + 'keyId' => $key->keyId, 29 + ]; 30 + 31 + $encrypted = $this->encrypter->encrypt($data); 32 + 33 + file_put_contents( 34 + $this->getKeyPath($sessionId), 35 + $encrypted 36 + ); 37 + } 38 + 39 + public function get(string $sessionId): ?DPoPKey 40 + { 41 + $path = $this->getKeyPath($sessionId); 42 + 43 + if (! file_exists($path)) { 44 + return null; 45 + } 46 + 47 + $encrypted = file_get_contents($path); 48 + $data = $this->encrypter->decrypt($encrypted); 49 + 50 + return new DPoPKey( 51 + privateKey: JWK::createFromJson(json_encode($data['privateKey'])), 52 + publicKey: JWK::createFromJson(json_encode($data['publicKey'])), 53 + keyId: $data['keyId'], 54 + ); 55 + } 56 + 57 + public function delete(string $sessionId): void 58 + { 59 + $path = $this->getKeyPath($sessionId); 60 + 61 + if (file_exists($path)) { 62 + unlink($path); 63 + } 64 + } 65 + 66 + public function exists(string $sessionId): bool 67 + { 68 + return file_exists($this->getKeyPath($sessionId)); 69 + } 70 + 71 + protected function getKeyPath(string $sessionId): string 72 + { 73 + return $this->storagePath.'/'.hash('sha256', $sessionId).'.key'; 74 + } 75 + }