| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { randomBytes } from 'crypto' |
| import { readFile, rename, unlink, writeFile } from 'fs/promises' |
| import { join } from 'path' |
| import { logForDebugging } from '../debug.js' |
| import { getFsImplementation } from '../fsOperations.js' |
| import { logError } from '../log.js' |
| import { jsonParse, jsonStringify } from '../slowOperations.js' |
| import { getPluginsDirectory } from './pluginDirectories.js' |
|
|
| const FLAGGED_PLUGINS_FILENAME = 'flagged-plugins.json' |
|
|
| export type FlaggedPlugin = { |
| flaggedAt: string |
| seenAt?: string |
| } |
|
|
| const SEEN_EXPIRY_MS = 48 * 60 * 60 * 1000 |
|
|
| |
| let cache: Record<string, FlaggedPlugin> | null = null |
|
|
| function getFlaggedPluginsPath(): string { |
| return join(getPluginsDirectory(), FLAGGED_PLUGINS_FILENAME) |
| } |
|
|
| function parsePluginsData(content: string): Record<string, FlaggedPlugin> { |
| const parsed = jsonParse(content) as unknown |
| if ( |
| typeof parsed !== 'object' || |
| parsed === null || |
| !('plugins' in parsed) || |
| typeof (parsed as { plugins: unknown }).plugins !== 'object' || |
| (parsed as { plugins: unknown }).plugins === null |
| ) { |
| return {} |
| } |
| const plugins = (parsed as { plugins: Record<string, unknown> }).plugins |
| const result: Record<string, FlaggedPlugin> = {} |
| for (const [id, entry] of Object.entries(plugins)) { |
| if ( |
| entry && |
| typeof entry === 'object' && |
| 'flaggedAt' in entry && |
| typeof (entry as { flaggedAt: unknown }).flaggedAt === 'string' |
| ) { |
| const parsed: FlaggedPlugin = { |
| flaggedAt: (entry as { flaggedAt: string }).flaggedAt, |
| } |
| if ( |
| 'seenAt' in entry && |
| typeof (entry as { seenAt: unknown }).seenAt === 'string' |
| ) { |
| parsed.seenAt = (entry as { seenAt: string }).seenAt |
| } |
| result[id] = parsed |
| } |
| } |
| return result |
| } |
|
|
| async function readFromDisk(): Promise<Record<string, FlaggedPlugin>> { |
| try { |
| const content = await readFile(getFlaggedPluginsPath(), { |
| encoding: 'utf-8', |
| }) |
| return parsePluginsData(content) |
| } catch { |
| return {} |
| } |
| } |
|
|
| async function writeToDisk( |
| plugins: Record<string, FlaggedPlugin>, |
| ): Promise<void> { |
| const filePath = getFlaggedPluginsPath() |
| const tempPath = `${filePath}.${randomBytes(8).toString('hex')}.tmp` |
|
|
| try { |
| await getFsImplementation().mkdir(getPluginsDirectory()) |
|
|
| const content = jsonStringify({ plugins }, null, 2) |
| await writeFile(tempPath, content, { |
| encoding: 'utf-8', |
| mode: 0o600, |
| }) |
| await rename(tempPath, filePath) |
| cache = plugins |
| } catch (error) { |
| logError(error) |
| try { |
| await unlink(tempPath) |
| } catch { |
| |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| export async function loadFlaggedPlugins(): Promise<void> { |
| const all = await readFromDisk() |
| const now = Date.now() |
| let changed = false |
|
|
| for (const [id, entry] of Object.entries(all)) { |
| if ( |
| entry.seenAt && |
| now - new Date(entry.seenAt).getTime() >= SEEN_EXPIRY_MS |
| ) { |
| delete all[id] |
| changed = true |
| } |
| } |
|
|
| cache = all |
| if (changed) { |
| await writeToDisk(all) |
| } |
| } |
|
|
| |
| |
| |
| |
| export function getFlaggedPlugins(): Record<string, FlaggedPlugin> { |
| return cache ?? {} |
| } |
|
|
| |
| |
| |
| |
| |
| export async function addFlaggedPlugin(pluginId: string): Promise<void> { |
| if (cache === null) { |
| cache = await readFromDisk() |
| } |
|
|
| const updated = { |
| ...cache, |
| [pluginId]: { |
| flaggedAt: new Date().toISOString(), |
| }, |
| } |
|
|
| await writeToDisk(updated) |
| logForDebugging(`Flagged plugin: ${pluginId}`) |
| } |
|
|
| |
| |
| |
| |
| |
| export async function markFlaggedPluginsSeen( |
| pluginIds: string[], |
| ): Promise<void> { |
| if (cache === null) { |
| cache = await readFromDisk() |
| } |
| const now = new Date().toISOString() |
| let changed = false |
|
|
| const updated = { ...cache } |
| for (const id of pluginIds) { |
| const entry = updated[id] |
| if (entry && !entry.seenAt) { |
| updated[id] = { ...entry, seenAt: now } |
| changed = true |
| } |
| } |
|
|
| if (changed) { |
| await writeToDisk(updated) |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function removeFlaggedPlugin(pluginId: string): Promise<void> { |
| if (cache === null) { |
| cache = await readFromDisk() |
| } |
| if (!(pluginId in cache)) return |
|
|
| const { [pluginId]: _, ...rest } = cache |
| cache = rest |
| await writeToDisk(rest) |
| } |
|
|