WIP: Another at:// production from me

atproto session stores in valkey

+239 -34
+19
dev.compose.yml
··· 1 + services: 2 + valkey: 3 + image: valkey/valkey:9.0 4 + ports: 5 + - '${FORWARD_VAL_KEY_PORT:-6379}:6379' 6 + volumes: 7 + - 'valkey_data:/data' 8 + healthcheck: 9 + test: [ "CMD", "valkey-cli", "ping" ] 10 + retries: 3 11 + timeout: 5s 12 + networks: 13 + - services-network 14 + volumes: 15 + valkey_data: 16 + app_data: 17 + networks: 18 + services-network: 19 + driver: bridge
+1
package.json
··· 50 50 "@oslojs/crypto": "^1.0.1", 51 51 "@oslojs/encoding": "^1.1.0", 52 52 "@sveltejs/adapter-node": "^5.4.0", 53 + "@valkey/valkey-glide": "^2.2.1", 53 54 "better-sqlite3": "12.4.1", 54 55 "node-schedule": "^2.1.1", 55 56 "pino": "^10.1.0"
+144
pnpm-lock.yaml
··· 41 41 '@sveltejs/adapter-node': 42 42 specifier: ^5.4.0 43 43 version: 5.4.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.2))) 44 + '@valkey/valkey-glide': 45 + specifier: ^2.2.1 46 + version: 2.2.1 44 47 better-sqlite3: 45 48 specifier: 12.4.1 46 49 version: 12.4.1 ··· 615 618 '@polka/url@1.0.0-next.29': 616 619 resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} 617 620 621 + '@protobufjs/aspromise@1.1.2': 622 + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} 623 + 624 + '@protobufjs/base64@1.1.2': 625 + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} 626 + 627 + '@protobufjs/codegen@2.0.4': 628 + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} 629 + 630 + '@protobufjs/eventemitter@1.1.0': 631 + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} 632 + 633 + '@protobufjs/fetch@1.1.0': 634 + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} 635 + 636 + '@protobufjs/float@1.0.2': 637 + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} 638 + 639 + '@protobufjs/inquire@1.1.0': 640 + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} 641 + 642 + '@protobufjs/path@1.1.2': 643 + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} 644 + 645 + '@protobufjs/pool@1.1.0': 646 + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} 647 + 648 + '@protobufjs/utf8@1.1.0': 649 + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} 650 + 618 651 '@rollup/plugin-commonjs@28.0.9': 619 652 resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==} 620 653 engines: {node: '>=16.0.0 || 14 >= 14.17'} ··· 884 917 resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} 885 918 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 886 919 920 + '@valkey/valkey-glide-darwin-arm64@2.2.1': 921 + resolution: {integrity: sha512-4peyagRot9TOyYn1Iogt5lbQo8jd/aqAAn99iCbPCz8c4iUXeBT57KcDDql3lJinzw/YEJF35xJU2FO5XP7pyg==} 922 + cpu: [arm64] 923 + os: [darwin] 924 + 925 + '@valkey/valkey-glide-darwin-x64@2.2.1': 926 + resolution: {integrity: sha512-+jxY9PnnlbIEuaf9guETibvoayErOMYnWAfVw7PirAAcliwwQ/xzZnrq9kEosU7jm3RClyPrSmVP3XdyRVzctg==} 927 + cpu: [x64] 928 + os: [darwin] 929 + 930 + '@valkey/valkey-glide-linux-arm64-gnu@2.2.1': 931 + resolution: {integrity: sha512-B28NzpIk25bAP45nMnU9gt89TaCg/dTBgyBCTc3fNKtHRuRSdOF0pwrI03nhQ7b5EnEAOhjnHMEVnXJIB8aFtA==} 932 + cpu: [arm64] 933 + os: [linux] 934 + 935 + '@valkey/valkey-glide-linux-arm64-musl@2.2.1': 936 + resolution: {integrity: sha512-TQDYDOG8GzBNybSx1789ik8UTo5DpCRMrlRHKA+ClhoYg1uGa48druRerqOHqkEOOBzK7IKjHOQiKW9QJNed0A==} 937 + cpu: [arm64] 938 + os: [linux] 939 + 940 + '@valkey/valkey-glide-linux-x64-gnu@2.2.1': 941 + resolution: {integrity: sha512-W3pZgFJmV2DVzNeFV/uX/885Fizx9px//SDOBNu+F63aTwkm+FBZaqJ0qaAg81GaABdaqq8VfTmh5aidH77t7A==} 942 + cpu: [x64] 943 + os: [linux] 944 + 945 + '@valkey/valkey-glide-linux-x64-musl@2.2.1': 946 + resolution: {integrity: sha512-Pyzv5535g8A92iKDzVGI8vXX0Hn7+G3Y6VclqHusZvz8xULWV46Kzsq4HUlhvnABm5zm2rvObIo9eoaMxhQooQ==} 947 + cpu: [x64] 948 + os: [linux] 949 + 950 + '@valkey/valkey-glide@2.2.1': 951 + resolution: {integrity: sha512-VsF2GbtSE2olsFdKHEKffnVIkriH5k63d1ammcL/xALZMGg6OZTubuwRPB0yXdLo26QVTMcrs+d2xptE00S/qA==} 952 + engines: {node: '>=16'} 953 + 887 954 abort-controller@3.0.0: 888 955 resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} 889 956 engines: {node: '>=6.5'} ··· 1412 1479 long-timeout@0.1.1: 1413 1480 resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} 1414 1481 1482 + long@5.3.2: 1483 + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} 1484 + 1415 1485 lru-cache@10.4.3: 1416 1486 resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 1417 1487 ··· 1584 1654 resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} 1585 1655 engines: {node: '>= 0.6.0'} 1586 1656 1657 + protobufjs@7.5.4: 1658 + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} 1659 + engines: {node: '>=12.0.0'} 1660 + 1587 1661 pump@3.0.3: 1588 1662 resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} 1589 1663 ··· 2329 2403 2330 2404 '@polka/url@1.0.0-next.29': {} 2331 2405 2406 + '@protobufjs/aspromise@1.1.2': {} 2407 + 2408 + '@protobufjs/base64@1.1.2': {} 2409 + 2410 + '@protobufjs/codegen@2.0.4': {} 2411 + 2412 + '@protobufjs/eventemitter@1.1.0': {} 2413 + 2414 + '@protobufjs/fetch@1.1.0': 2415 + dependencies: 2416 + '@protobufjs/aspromise': 1.1.2 2417 + '@protobufjs/inquire': 1.1.0 2418 + 2419 + '@protobufjs/float@1.0.2': {} 2420 + 2421 + '@protobufjs/inquire@1.1.0': {} 2422 + 2423 + '@protobufjs/path@1.1.2': {} 2424 + 2425 + '@protobufjs/pool@1.1.0': {} 2426 + 2427 + '@protobufjs/utf8@1.1.0': {} 2428 + 2332 2429 '@rollup/plugin-commonjs@28.0.9(rollup@4.53.3)': 2333 2430 dependencies: 2334 2431 '@rollup/pluginutils': 5.3.0(rollup@4.53.3) ··· 2596 2693 '@typescript-eslint/types': 8.49.0 2597 2694 eslint-visitor-keys: 4.2.1 2598 2695 2696 + '@valkey/valkey-glide-darwin-arm64@2.2.1': 2697 + optional: true 2698 + 2699 + '@valkey/valkey-glide-darwin-x64@2.2.1': 2700 + optional: true 2701 + 2702 + '@valkey/valkey-glide-linux-arm64-gnu@2.2.1': 2703 + optional: true 2704 + 2705 + '@valkey/valkey-glide-linux-arm64-musl@2.2.1': 2706 + optional: true 2707 + 2708 + '@valkey/valkey-glide-linux-x64-gnu@2.2.1': 2709 + optional: true 2710 + 2711 + '@valkey/valkey-glide-linux-x64-musl@2.2.1': 2712 + optional: true 2713 + 2714 + '@valkey/valkey-glide@2.2.1': 2715 + dependencies: 2716 + long: 5.3.2 2717 + protobufjs: 7.5.4 2718 + optionalDependencies: 2719 + '@valkey/valkey-glide-darwin-arm64': 2.2.1 2720 + '@valkey/valkey-glide-darwin-x64': 2.2.1 2721 + '@valkey/valkey-glide-linux-arm64-gnu': 2.2.1 2722 + '@valkey/valkey-glide-linux-arm64-musl': 2.2.1 2723 + '@valkey/valkey-glide-linux-x64-gnu': 2.2.1 2724 + '@valkey/valkey-glide-linux-x64-musl': 2.2.1 2725 + 2599 2726 abort-controller@3.0.0: 2600 2727 dependencies: 2601 2728 event-target-shim: 5.0.1 ··· 3044 3171 3045 3172 long-timeout@0.1.1: {} 3046 3173 3174 + long@5.3.2: {} 3175 + 3047 3176 lru-cache@10.4.3: {} 3048 3177 3049 3178 luxon@3.7.2: {} ··· 3216 3345 process-warning@5.0.0: {} 3217 3346 3218 3347 process@0.11.10: {} 3348 + 3349 + protobufjs@7.5.4: 3350 + dependencies: 3351 + '@protobufjs/aspromise': 1.1.2 3352 + '@protobufjs/base64': 1.1.2 3353 + '@protobufjs/codegen': 2.0.4 3354 + '@protobufjs/eventemitter': 1.1.0 3355 + '@protobufjs/fetch': 1.1.0 3356 + '@protobufjs/float': 1.0.2 3357 + '@protobufjs/inquire': 1.1.0 3358 + '@protobufjs/path': 1.1.2 3359 + '@protobufjs/pool': 1.1.0 3360 + '@protobufjs/utf8': 1.1.0 3361 + '@types/node': 24.10.2 3362 + long: 5.3.2 3219 3363 3220 3364 pump@3.0.3: 3221 3365 dependencies:
+1
pnpm-workspace.yaml
··· 2 2 - better-sqlite3 3 3 - core-js 4 4 - esbuild 5 + - protobufjs
+7 -3
src/lib/server/atproto/client.ts
··· 2 2 3 3 import { atprotoLoopbackClientMetadata, Keyset, NodeOAuthClient } from '@atproto/oauth-client-node'; 4 4 import { JoseKey } from '@atproto/jwk-jose'; 5 - import { db } from '$lib/server/db'; 6 5 import { SessionStore, StateStore } from '$lib/server/atproto/storage'; 7 6 import { env } from '$env/dynamic/private'; 8 7 import type { OAuthClientMetadataInput } from '@atproto/oauth-types'; 8 + import { getAValKeyClient } from '$lib/server/cache'; 9 + 9 10 10 11 //You will need to change these if you are using another collection, can also change by setting the env OAUTH_SCOPES 11 12 //For permission to all you can uncomment below ··· 28 29 export const atpOAuthClient = async () => { 29 30 if (!client) { 30 31 client = (async () => { 32 + 33 + const valKeyClient = await getAValKeyClient(); 34 + 31 35 const rootDomain = env.OAUTH_DOMAIN ?? '127.0.0.1:5173'; 32 36 const dev = env.DEV !== undefined; 33 37 const isConfidential = env.OAUTH_JWK !== undefined; ··· 67 71 }; 68 72 69 73 return new NodeOAuthClient({ 70 - stateStore: new StateStore(db), 71 - sessionStore: new SessionStore(db), 74 + stateStore: new StateStore(valKeyClient), 75 + sessionStore: new SessionStore(valKeyClient), 72 76 keyset, 73 77 clientMetadata, 74 78 // Not needed since this all runs locally to one machine I believe. But if you do run multiple instances and change out the DB from sqlite may need this
+6 -5
src/lib/server/atproto/storage.ts
··· 7 7 NodeSavedStateStore 8 8 } from '@atproto/oauth-client-node'; 9 9 import { Cache, SESSION_STORE, STATE_STORE } from '$lib/server/cache'; 10 - import { db } from '$lib/server/db'; 10 + import { GlideClient } from '@valkey/valkey-glide'; 11 + 11 12 12 13 export class StateStore implements NodeSavedStateStore{ 13 14 14 15 cache: Cache; 15 16 16 - constructor(database: typeof db) { 17 - this.cache = new Cache(database, STATE_STORE); 17 + constructor(valKeyClient: GlideClient) { 18 + this.cache = new Cache(valKeyClient, STATE_STORE, 1_800); 18 19 } 19 20 20 21 async del(key: string) { ··· 36 37 37 38 cache: Cache; 38 39 39 - constructor(database: typeof db) { 40 - this.cache = new Cache(database, SESSION_STORE); 40 + constructor(valKeyClient: GlideClient) { 41 + this.cache = new Cache(valKeyClient, SESSION_STORE); 41 42 } 42 43 43 44 async del(key: string) {
+61 -26
src/lib/server/cache.ts
··· 1 - // A key value key to the database that is mostly used for atproto session storage and state storage during the oauth session creation 2 - // The "stores" are divided up by a where on the store type so it can share the same interface just with that 1 + import { logger } from './logger'; 2 + import { env } from '$env/dynamic/private'; 3 + import { GlideClient, TimeUnit } from '@valkey/valkey-glide'; 3 4 4 - import { db } from './db'; 5 - import { keyValueStore } from './db/schema'; 6 - import { and, eq } from 'drizzle-orm'; 5 + export const SESSION_STORE = 'atp_sessions:'; 6 + export const STATE_STORE = 'atp_states:'; 7 7 8 - export const SESSION_STORE = 'sessions'; 9 - export const STATE_STORE = 'states'; 8 + 9 + let valKeyClient: Promise<GlideClient> | null = null; 10 + 11 + 12 + export const getAValKeyClient = async () => { 13 + if (!valKeyClient) { 14 + valKeyClient = (async () => { 15 + logger.info('Creating valkey client'); 16 + 17 + const addresses = [ 18 + { 19 + host: env.REDIS_HOST ?? 'localhost', 20 + // @ts-expect-error Going to leave it to the redis client to throw a run time error for this since 21 + // it is a run time error to not be able to have redis to connect 22 + port: env.REDIS_PORT as number ?? 6379 23 + }, 24 + ]; 25 + 26 + return await GlideClient.createClient({ 27 + addresses, 28 + credentials: env.REDIS_PASSWORD ? { password: env.REDIS_PASSWORD } : undefined, 29 + useTLS: env.REDIS_TLS === 'true', 30 + //This may be a bit extreme, will see 31 + requestTimeout: 500, // 500ms timeout 32 + }); 33 + })(); 34 + } 35 + return valKeyClient; 36 + }; 37 + 10 38 11 39 export class Cache { 12 40 13 - db: typeof db; 14 - cacheName: string; 41 + valKeyClient: GlideClient; 42 + //Set if the cache set should have an expiration 43 + expire: number | undefined; 44 + cachePrefix: string; 15 45 16 - constructor(database: typeof db, cacheName: string) { 17 - this.db = database; 18 - this.cacheName = cacheName; 46 + 47 + constructor(glideClient: GlideClient, cachePrefix: string, expire: number | undefined = undefined) { 48 + this.valKeyClient = glideClient; 49 + this.expire = expire; 50 + this.cachePrefix = cachePrefix; 51 + } 52 + 53 + $key(key: string) { 54 + return `${this.cachePrefix}${key}`; 19 55 } 20 56 21 57 async get(key: string) { 22 - const result = await this.db.select().from(keyValueStore).where(and( 23 - eq(keyValueStore.key, key), 24 - eq(keyValueStore.storeName, this.cacheName) 25 - )).limit(1); 26 - if(result.length > 0){ 27 - return result[0].value; 58 + const result = await this.valKeyClient.get(this.$key(key)); 59 + if(result){ 60 + return result.toString(); 28 61 } 29 - return null; 62 + return undefined; 30 63 } 31 64 32 65 async set(key: string, value: string) { 33 - return this.db.insert(keyValueStore) 34 - .values({ key, value, storeName: this.cacheName, createdAt: new Date() }) 35 - .onConflictDoUpdate({ 36 - target: keyValueStore.key, 37 - set: { value, createdAt: new Date() } 38 - }); 66 + const expiryOptions = this.expire ? 67 + { 68 + expiry: { 69 + type: TimeUnit.Seconds, 70 + count: this.expire as number 71 + } 72 + } : undefined; 73 + return await this.valKeyClient.set(this.$key(key), value, expiryOptions); 39 74 } 40 75 41 76 async delete(key: string) { 42 - return this.db.delete(keyValueStore).where(eq(keyValueStore.key, key)); 77 + await this.valKeyClient.del([this.$key(key)]); 43 78 } 44 79 45 80 }