an app to share curated trails sidetrail.app
1

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 + }