Barazo AppView backend
barazo.forum
1import { Agent } from '@atproto/api'
2
3import type { Logger } from '../logger.js'
4
5import type {
6 PluginContext,
7 PluginSettings,
8 ScopedAtProto,
9 ScopedCache,
10 ScopedDatabase,
11} from './types.js'
12
13/** Adapter interface for the underlying cache (e.g. Valkey/ioredis). */
14export interface CacheAdapter {
15 get(key: string): Promise<string | null>
16 set(key: string, value: string, ttlSeconds?: number): Promise<void>
17 del(key: string): Promise<void>
18}
19
20export interface PluginContextOptions {
21 pluginName: string
22 pluginVersion: string
23 permissions: string[]
24 settings: Record<string, unknown>
25 db: unknown
26 cache: CacheAdapter | null
27 oauthClient: unknown // NodeOAuthClient | null — typed as unknown to avoid coupling
28 logger: Logger
29 communityDid: string
30}
31
32function createPluginSettings(values: Record<string, unknown>): PluginSettings {
33 const copy = { ...values }
34 return {
35 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- matches PluginSettings interface
36 get<T = unknown>(key: string): T | undefined {
37 return copy[key] as T | undefined
38 },
39 getAll(): Record<string, unknown> {
40 return { ...copy }
41 },
42 }
43}
44
45function createScopedCache(cache: CacheAdapter, pluginName: string): ScopedCache {
46 const prefix = `plugin:${pluginName}:`
47 return {
48 get(key: string): Promise<string | null> {
49 return cache.get(`${prefix}${key}`)
50 },
51 set(key: string, value: string, ttlSeconds?: number): Promise<void> {
52 return cache.set(`${prefix}${key}`, value, ttlSeconds)
53 },
54 del(key: string): Promise<void> {
55 return cache.del(`${prefix}${key}`)
56 },
57 }
58}
59
60function createScopedDatabase(db: unknown, _permissions: string[]): ScopedDatabase {
61 return {
62 execute(query: unknown): Promise<unknown> {
63 return (db as { execute(q: unknown): Promise<unknown> }).execute(query)
64 },
65 query(_tableName: string): unknown {
66 throw new Error('ScopedDatabase.query() is not yet implemented')
67 },
68 }
69}
70
71const BSKY_PUBLIC_API = 'https://public.api.bsky.app'
72
73interface OAuthClientLike {
74 restore(did: string): Promise<unknown>
75}
76
77function createScopedAtProto(
78 oauthClient: OAuthClientLike,
79 logger: Logger,
80 pluginName: string
81): ScopedAtProto {
82 return {
83 async getRecord(did: string, collection: string, rkey: string): Promise<unknown> {
84 try {
85 const agent = new Agent(new URL(BSKY_PUBLIC_API))
86 const response = await agent.com.atproto.repo.getRecord({
87 repo: did,
88 collection,
89 rkey,
90 })
91 return response.data.value
92 } catch (err: unknown) {
93 logger.debug(
94 { err, plugin: pluginName, did, collection, rkey },
95 'ScopedAtProto getRecord failed'
96 )
97 return null
98 }
99 },
100
101 async putRecord(did: string, collection: string, rkey: string, record: unknown): Promise<void> {
102 const session = await oauthClient.restore(did)
103 const agent = new Agent(session as ConstructorParameters<typeof Agent>[0])
104 await agent.com.atproto.repo.putRecord({
105 repo: did,
106 collection,
107 rkey,
108 record: { $type: collection, ...(record as Record<string, unknown>) },
109 })
110 },
111
112 async deleteRecord(did: string, collection: string, rkey: string): Promise<void> {
113 const session = await oauthClient.restore(did)
114 const agent = new Agent(session as ConstructorParameters<typeof Agent>[0])
115 await agent.com.atproto.repo.deleteRecord({
116 repo: did,
117 collection,
118 rkey,
119 })
120 },
121 }
122}
123
124export function createPluginContext(options: PluginContextOptions): PluginContext {
125 const {
126 pluginName,
127 pluginVersion,
128 permissions,
129 settings,
130 db,
131 cache,
132 oauthClient,
133 logger,
134 communityDid,
135 } = options
136
137 const hasCachePermission =
138 permissions.includes('cache:read') || permissions.includes('cache:write')
139
140 const scopedCache = hasCachePermission && cache ? createScopedCache(cache, pluginName) : undefined
141
142 const hasPdsPermission = permissions.includes('pds:read') || permissions.includes('pds:write')
143 const scopedAtProto =
144 hasPdsPermission && oauthClient
145 ? createScopedAtProto(oauthClient as OAuthClientLike, logger, pluginName)
146 : undefined
147
148 return {
149 pluginName,
150 pluginVersion,
151 communityDid,
152 db: createScopedDatabase(db, permissions),
153 settings: createPluginSettings(settings),
154 logger: logger.child({ plugin: pluginName }),
155 ...(scopedCache ? { cache: scopedCache } : {}),
156 ...(scopedAtProto ? { atproto: scopedAtProto } : {}),
157 } satisfies PluginContext
158}