an app to share curated trails sidetrail.app
atproto nextjs react rsc
50
fork

Configure Feed

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

move oauth caches to redis

+78 -13
+15 -13
auth/client.ts
··· 6 6 JoseKey, 7 7 } from "@atproto/oauth-client-node"; 8 8 import { 9 - AuthorizationServerMetadataCache, 10 - ProtectedResourceMetadataCache, 11 - } from "@atproto/oauth-client"; 12 - import { SimpleStoreMemory } from "@atproto-labs/simple-store-memory"; 9 + OAuthAuthorizationServerMetadata, 10 + OAuthProtectedResourceMetadata, 11 + } from "@atproto/oauth-types"; 13 12 import { StateStore, SessionStore, initAuthTables, requestLock } from "./storage"; 13 + import { SimpleStoreRedis } from "./redis-store"; 14 14 15 - // Cache OAuth server metadata for 24 hours (default is 60 seconds) 16 - // This metadata rarely changes - Bluesky would announce OAuth infrastructure changes 17 - const authServerMetadataCache: AuthorizationServerMetadataCache = new SimpleStoreMemory({ 18 - ttl: 24 * 60 * 60 * 1000, 19 - max: 100, 15 + const authServerMetadataCache = new SimpleStoreRedis<OAuthAuthorizationServerMetadata>({ 16 + keyPrefix: "oauth:as-metadata:", 17 + ttlSeconds: 24 * 60 * 60, 18 + }); 19 + const protectedResourceMetadataCache = new SimpleStoreRedis<OAuthProtectedResourceMetadata>({ 20 + keyPrefix: "oauth:pr-metadata:", 21 + ttlSeconds: 24 * 60 * 60, 20 22 }); 21 - const protectedResourceMetadataCache: ProtectedResourceMetadataCache = new SimpleStoreMemory({ 22 - ttl: 24 * 60 * 60 * 1000, 23 - max: 100, 23 + const dpopNonceCache = new SimpleStoreRedis<string>({ 24 + keyPrefix: "oauth:dpop-nonce:", 25 + ttlSeconds: 24 * 60 * 60, 24 26 }); 25 27 26 28 // Environment variables ··· 114 116 stateStore: new StateStore(), 115 117 sessionStore: new SessionStore(), 116 118 requestLock, 117 - // Use longer-lived cache for OAuth metadata (rarely changes) 118 119 authorizationServerMetadataCache: authServerMetadataCache, 119 120 protectedResourceMetadataCache, 121 + dpopNonceCache, 120 122 }); 121 123 } 122 124
+63
auth/redis-store.ts
··· 1 + import "server-only"; 2 + import Redis from "ioredis"; 3 + 4 + let redis: Redis | null = null; 5 + 6 + function getRedis(): Redis { 7 + if (!redis) { 8 + redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379", { 9 + maxRetriesPerRequest: 3, 10 + lazyConnect: true, 11 + }); 12 + redis.on("error", (err) => { 13 + console.error("[redis] connection error:", err.message); 14 + }); 15 + } 16 + return redis; 17 + } 18 + 19 + export type SimpleStoreRedisOptions = { 20 + keyPrefix: string; 21 + ttlSeconds: number; 22 + }; 23 + 24 + /** 25 + * Simple Redis-backed store compatible with @atproto SimpleStore interface. 26 + * Used for OAuth caches that should survive server restarts. 27 + */ 28 + export class SimpleStoreRedis<V> { 29 + private keyPrefix: string; 30 + private ttlSeconds: number; 31 + 32 + constructor(options: SimpleStoreRedisOptions) { 33 + this.keyPrefix = options.keyPrefix; 34 + this.ttlSeconds = options.ttlSeconds; 35 + } 36 + 37 + async get(key: string): Promise<V | undefined> { 38 + try { 39 + const value = await getRedis().get(this.keyPrefix + key); 40 + if (!value) return undefined; 41 + return JSON.parse(value) as V; 42 + } catch (err) { 43 + console.error("[redis-store] get error:", err); 44 + return undefined; 45 + } 46 + } 47 + 48 + async set(key: string, value: V): Promise<void> { 49 + try { 50 + await getRedis().set(this.keyPrefix + key, JSON.stringify(value), "EX", this.ttlSeconds); 51 + } catch (err) { 52 + console.error("[redis-store] set error:", err); 53 + } 54 + } 55 + 56 + async del(key: string): Promise<void> { 57 + try { 58 + await getRedis().del(this.keyPrefix + key); 59 + } catch (err) { 60 + console.error("[redis-store] del error:", err); 61 + } 62 + } 63 + }