import { createHash } from 'node:crypto'; import { db } from '../db/index.js'; import { createScript, getScript, getVersion, uploadNewVersion, type ScopeType } from './scripts.js'; import { findRunningBetaForScript } from './beta.js'; import { downloadGithubAsset, getGithubRelease, GithubReleaseError } from '../integrations/github-releases.js'; import type { PluginSource, PluginUpdate } from '$lib/plugin-github.js'; export function pluginSource(pluginId: number): PluginSource | null { return db().prepare(`SELECT p.* FROM plugin_imports p JOIN script_versions v ON v.id = p.version_id WHERE v.script_id = ? ORDER BY v.version DESC LIMIT 1`).get(pluginId) as PluginSource | undefined ?? null; } export async function checkPluginUpdate(pluginId: number): Promise { const plugin = getScript(pluginId); if (!plugin || plugin.kind !== 'plugin') throw new GithubReleaseError('Plugin not found.', 404); const source = pluginSource(pluginId); const base = { pluginId, name: plugin.name, source }; if (!source) return { ...base, release: null, status: 'untracked' }; try { const release = await getGithubRelease(`https://github.com/${source.repository}`); // A latest-stable release may already be in history when a newer prerelease is imported. if (release.id !== source.release_id && Date.parse(release.publishedAt) <= Date.parse(source.published_at)) { return { ...base, release, status: 'ahead' }; } const imported = db().prepare(`SELECT p.version_id FROM plugin_imports p JOIN script_versions v ON v.id = p.version_id WHERE v.script_id = ? AND lower(p.repository) = lower(?) AND p.release_id = ? AND p.asset_name = ? ORDER BY v.version DESC LIMIT 1`).get(pluginId, source.repository, release.id, source.asset_name) as { version_id: number } | undefined; if (imported) return { ...base, release, status: imported.version_id === plugin.current_version_id ? 'current' : 'imported', versionId: imported.version_id }; return { ...base, release, status: 'available' }; } catch (error) { return { ...base, release: null, status: 'error', error: (error as Error).message }; } } export interface ImportPluginReleaseInput { url: string; assetId: number; pluginId?: number; name?: string; scopeType?: ScopeType | null; scopeValue?: string | null; } export async function importPluginRelease(input: ImportPluginReleaseInput) { if (typeof input.url !== 'string' || !Number.isSafeInteger(input.assetId) || input.assetId < 1) { throw new GithubReleaseError('A release URL and DLL asset selection are required.'); } if (input.pluginId !== undefined && (!Number.isSafeInteger(input.pluginId) || input.pluginId < 1)) { throw new GithubReleaseError('Invalid plugin id.'); } const requirePlugin = () => { const plugin = input.pluginId === undefined ? undefined : getScript(input.pluginId); if (input.pluginId !== undefined && (!plugin || plugin.kind !== 'plugin')) throw new GithubReleaseError('Plugin not found.', 404); if (plugin && findRunningBetaForScript(plugin.id)) throw new GithubReleaseError('Finish the running beta before importing another release.', 409); return plugin; }; requirePlugin(); if (input.pluginId === undefined && (typeof input.name !== 'string' || !input.name.trim())) throw new GithubReleaseError('A plugin name is required.'); // Refetch the exact reviewed release: asset names, notes, tags and digests are never trusted from the client. const release = await getGithubRelease(input.url, true); const asset = release.assets.find(a => a.id === input.assetId); if (!asset) throw new GithubReleaseError('That DLL is no longer available in this release. Preview it again.'); const content = await downloadGithubAsset(release, asset); const sha = createHash('sha256').update(content).digest('hex'); return db().transaction(() => { const plugin = requirePlugin(); if (plugin) { const prior = db().prepare(`SELECT v.id FROM script_versions v JOIN plugin_imports p ON p.version_id = v.id WHERE v.script_id = ? AND lower(p.repository) = lower(?) AND p.release_id = ? AND p.asset_id = ? AND v.sha256 = ? ORDER BY v.version DESC LIMIT 1`).get(plugin.id, release.repository, release.id, asset.id, sha) as { id: number } | undefined; if (prior) return result(plugin.id, getVersion(prior.id)!, true); } const created = plugin ? { script: plugin, version: uploadNewVersion(plugin.id, content, release.notes, release.tag) } : createScript({ name: input.name!.trim(), kind: 'plugin', targetRelPath: asset.name, content, releaseVersion: release.tag, notes: release.notes, scopeType: input.scopeType, scopeValue: input.scopeValue }); db().prepare(`INSERT INTO plugin_imports (version_id, repository, release_id, release_url, asset_id, asset_name, published_at) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(created.version.id, release.repository, release.id, release.url, asset.id, asset.name, release.publishedAt); return result(created.script.id, created.version, false); })(); } function result(pluginId: number, version: NonNullable>, alreadyImported: boolean) { return { pluginId, versionId: version.id, releaseVersion: version.release_version, sha256: version.sha256, alreadyImported, betaUrl: `/betas?pluginId=${pluginId}&versionId=${version.id}` }; }