/** * Download Workflow * * Handles downloading plugin files with verification and progress tracking */ import type { PluginInfo, PluginUpdateInfo, ScannedPlugin } from '../core/types/index.js'; import { type OperationLogger } from '../infrastructure/logger.js'; /** * Strip the query string (and fragment) from a download URL before it is * logged or persisted. * * Premium channels (P3, June 2026 pre-PR review): VoxelSource's * `getDownloadURL` returns a SIGNED, time-limited download URL whose query * string can embed a short-lived buyer token. That URL becomes * `info.downloadUrl`, which the file logger and the provenance store would * otherwise persist verbatim — the provenance store's own privacy doc forbids * token persistence, and the file log is just as durable. Logging only * `origin + pathname` keeps the artifact's location useful for * debugging/provenance while dropping every secret-bearing query parameter. * * The ACTUAL download still uses the full `info.downloadUrl` (the token must * survive to the fetch) — only the log/record copies are redacted. Malformed * URLs fall back to '' rather than leaking a raw string. */ export declare function redactUrl(url: string | undefined): string; /** * Update the TEST plugin config (plugins-test.json) with the new version * after a successful download. * * Phase 4 (audit update-pipeline Finding 10, P2): this used to write the new * version into plugins-prod.json (and the legacy combined plugins.json) even * though prod's JAR was untouched — CLI checks then reported prod "up to * date" while prod still ran the old JAR. Downloads land on the TEST server * (the active workspace; PROD is read-only by design), so only the test * config is updated here. Prod bookkeeping changes ONLY via the * sync/promotion path. The legacy combined plugins.json is updated only when * plugins-test.json does not exist — pure-legacy setups read it as their * test config via loadPlugins()'s fallback (v2.5.11). * * Exported for tests only (the prod-untouched regression pin). */ export declare function updatePluginConfigVersion(pluginName: string, version: string, paths?: { test: string; legacy: string; }): Promise; /** * Download progress event */ export interface DownloadProgress { pluginName: string; percent: number; bytesDownloaded: number; totalBytes: number; status: 'downloading' | 'verifying' | 'done' | 'error'; message?: string; } /** * How a validated JAR was applied to the target server (Phase 4 safe-apply, * roadmap 4.2). * * - 'installed': swapped into the plugins directory — the change is live (or * will be the moment the server starts). * - 'staged-for-restart': the target server was RUNNING, so the validated * JAR was placed in Bukkit's update folder (`plugins/update/` by default) * instead of swapped in place. The server applies it on its next restart. * Consumers must say "applies on next restart" — never "updated". */ export type InstallApplyStatus = 'installed' | 'staged-for-restart'; /** * Download result */ export interface DownloadResult { success: boolean; pluginName: string; version: string; filePath: string; bytesDownloaded: number; checksumValid?: boolean; error?: string; backedUpFile?: string; /** * Apply state of a successful download (Phase 4 safe-apply). Present on * every success; 'staged-for-restart' means the JAR sits in the update * folder until the server restarts. */ status?: InstallApplyStatus; /** * Non-blocking compatibility-gate findings (MC version / Java class * version, level 'warn' or 'unknown') — carried into results, never * swallowed (Phase 4 roadmap 4.2). */ warnings?: string[]; } /** * Batch download result */ export interface BatchDownloadResult { /** Applied in place (status 'installed') */ successful: DownloadResult[]; /** * Validated + staged into the server's update folder — these apply on the * next server restart (Phase 4 safe-apply). Deliberately NOT in * `successful` so no consumer can report them as already updated. */ staged: DownloadResult[]; failed: DownloadResult[]; duration: number; } /** * Download workflow options */ export interface DownloadWorkflowOptions { targetDir: string; backupExisting?: boolean; backupDir?: string; verifyChecksums?: boolean; strictChecksumVerification?: boolean; useStreaming?: boolean; concurrency?: number; onProgress?: (progress: DownloadProgress) => void; manualDownloadPlugins?: Set; /** * Optional AbortSignal — `downloadBatch` checks at the per-plugin boundary * and stops launching new downloads. Phase 3 audit, finding API [11]. */ signal?: AbortSignal; /** * Running-state probe for the target server (Phase 4 safe-apply). Default: * {@link createRunningServerProbe} on `targetDir` — memoized, one probe per * downloader. Tests inject a stub. When the probe answers true, updates to * already-installed plugins are STAGED into the update folder instead of * swapped in place. */ isTargetServerRunning?: () => Promise; /** * Server Minecraft version used as the compat-gate fallback when * detection from the server directory (version_history.json / server jar * name) finds nothing. Callers thread the configured TEST_MC_VERSION / * PROD_MC_VERSION here; final fallback is 'unknown' (never blocks). */ serverMcVersion?: string; /** * Host JVM feature release (e.g. 21) for the Java class-version gate. * Default: one `java -version` probe per process (memoized); unknown never * blocks. */ hostJavaMajor?: number; /** * Escalate "plugin targets a newer MC major than the server" from 'warn' * to 'block' (compat-gate strictCompat). Default false. */ strictCompat?: boolean; } /** * Integrity gate (Phase 1 roadmap 1.1): the downloaded bytes must be a real * plugin JAR BEFORE any swap or old-version deletion. Validates ZIP magic * bytes, then requires a parseable plugin.yml/paper-plugin.yml via scanJar. * * Exported (Phase 3 roadmap 3.2) so the manual-drop completion workflow runs * browser-downloaded files through the SAME gate as automatic downloads. * * @throws WorkflowError (DOWNLOAD_FAILED, non-retryable) carrying the real reason */ export declare function validateDownloadedJar(filePath: string, contentType: string | undefined): Promise; /** One lock name for every server-mutating update entry point. */ export declare const UPDATE_PROCESS_LOCK = "update"; /** * Run `fn` holding the cross-process 'update' lock (Phase 4 roadmap 4.5: * overlapping cron runs collide today). Fail-fast on contention — the * previous run is still working; this one bows out with a clean error. * Re-entrant within the process so runAutoUpdate → downloadBatch nests. * * @throws WorkflowError "another Pluginator update run is in progress (pid N)" */ export declare function withUpdateProcessLock(fn: () => Promise): Promise; /** Resolved inputs for {@link runPreInstallGates}. */ export interface PreInstallGateOptions { /** Server MC version ('unknown' never blocks). */ serverMcVersion: string; /** Host JVM feature release; undefined → Java gate yields 'unknown'. */ hostJavaMajor?: number; /** Escalate newer-MC-major mismatches from 'warn' to 'block'. */ strictCompat?: boolean; } /** Outcome of the pre-install gates — block reason XOR carried warnings. */ export interface PreInstallGateOutcome { /** Set only when a gate BLOCKED the install (the honest reason). */ blockReason?: string; /** 'warn'/'unknown' findings — proceed, but carry them into results. */ warnings: string[]; } /** * Resolve the compat-gate inputs for a target plugins directory: * server MC version detected from the server root (version_history.json / * server jar name) → caller-supplied config fallback → 'unknown'; host Java * from the option or a memoized `java -version` probe. * * Exported so complete-manual installs resolve through the SAME path. */ export declare function resolvePreInstallGateOptions(targetDir: string, overrides?: { serverMcVersion?: string; hostJavaMajor?: number; strictCompat?: boolean; }): Promise; /** * Run the install-time compatibility gates against a VALIDATED jar (Phase 4 * roadmap 4.2): MC-version gate (source-declared compatibility vs the * server's MC version) and Java class-file-version gate (can the host JVM * load it?). 'block' stops the install with the gate's reason; 'warn' and * 'unknown' proceed with the finding carried into results — never swallowed, * never blocking on missing data (house rule). * * Shared by the automatic download path and complete-manual installs. */ export declare function runPreInstallGates(jarPath: string, plugin: Pick, options: PreInstallGateOptions): Promise; /** * Resolve the server's update-folder NAME from `/bukkit.yml` * (`settings.update-folder`), default 'update'. Returns null when the * mechanism is explicitly DISABLED (empty string). * * Verified Bukkit semantics (Bukkit/Bukkit `SimplePluginManager.loadPlugins`, * read 2026-06-10): the update directory is resolved RELATIVE TO THE PLUGINS * DIRECTORY (`updateDirectory = new File(directory, server.getUpdateFolder())`) * and only when `getUpdateFolder() !== ""` — an empty string disables it. */ export declare function resolveUpdateFolderName(pluginsDir: string): Promise; /** * Context for the post-validation install steps. The temp file must already * sit INSIDE `targetDir` (the swap is a same-filesystem rename) and must have * passed {@link validateDownloadedJar}. */ export interface ValidatedInstallContext { targetDir: string; /** scanJar result from the integrity gate ({@link validateDownloadedJar}) */ scannedPlugin: ScannedPlugin; /** Identity + version + source context recorded into config/cache/provenance */ info: PluginInfo; backupExisting?: boolean; backupDir?: string; /** * Lazily-computed filename used only when plugin.yml carries no name * (laziness preserved from the original branch so URL parsing never runs * on the common path). */ getFallbackFilename: () => string; /** Caller's operation logger — keeps log lines attributed to the owning workflow op */ op: OperationLogger; /** * Resolved running state of the target server (Phase 4 safe-apply). * When true and an ENABLED version of the plugin is already installed, the * validated JAR is STAGED into the update folder instead of swapped in * place. Default false (the pre-Phase-4 swap behavior). */ serverRunning?: boolean; } /** Result of {@link swapValidatedJarIntoPlace}. */ export interface ValidatedInstallResult { /** * Actual on-disk path: the installed jar (post disabled-state preservation * rename), or the staged jar inside the update folder. */ filePath: string; /** Canonical enabled-name path (pre disabled-state rename) */ finalFilePath: string; backedUpFile?: string; /** How the JAR was applied (Phase 4): swapped in, or staged for restart. */ status: InstallApplyStatus; /** ID of the pre-update snapshot taken before any mutation, when one exists. */ snapshotId?: string; } /** * Apply a validated temp JAR to the target directory (Phase 4 safe-apply): * * 1. Final-filename determination (plugin.yml name/version). * 2. Pre-mutation reconnaissance: scan for already-installed versions. * 3. **Snapshot-before-swap** (roadmap 4.3): when a version is installed, * archive its JAR + `plugins//` data folder via the snapshot store. * Snapshot FAILURE aborts the install — the safety artifact is mandatory. * A jar-only snapshot (data folder missing/over-cap, explicit * skippedReason) is an acceptable fallback and proceeds. * 4. When the server is RUNNING and an enabled version is installed: stage * into the Bukkit update folder ('staged-for-restart') — no in-place * mutation of a live server. A running server with no enabled install * falls through: writing a NEW file never disturbs a live server (nothing * replaces an open file); it loads on the next restart. * 5. Otherwise the original swap: optional backup, atomic rename, * old-version cleanup, disabled-state (.jarDIS et al) preservation. * * @throws Propagates snapshot-abort/backup/rename failures — the caller owns * temp-file cleanup; the old JAR is untouched on any throw before step 5 */ export declare function swapValidatedJarIntoPlace(tempFilePath: string, ctx: ValidatedInstallContext): Promise; /** * Post-swap success bookkeeping: plugins-config version update, update-results * cache refresh + failure clearing, and the local download-provenance record. * Never throws — every step swallows its own errors exactly as the inline * original did (bookkeeping must not fail an already-successful install). */ export declare function recordSuccessfulInstall(installedPath: string, info: PluginInfo, op: OperationLogger, measuredDigests?: Partial>, applyStatus?: InstallApplyStatus): Promise; /** * Plugin downloader class */ export declare class PluginDownloader { private options; /** Memoized running-server probe — one detection pass per downloader. */ private runningProbe; /** Memoized compat-gate inputs — one MC-version detection per downloader. */ private gateOptionsPromise; constructor(options: DownloadWorkflowOptions); /** Resolved running state of the target server (Phase 4 safe-apply). */ private isTargetServerRunning; /** Resolved compat-gate inputs (Phase 4 roadmap 4.2). */ private resolveGateOptions; /** * Download a single plugin */ download(info: PluginInfo): Promise; /** * Download multiple plugins. * * Guarded by the cross-process 'update' lock (Phase 4 roadmap 4.5) so * overlapping cron runs / a daemon + interactive session can't mutate the * same server directory simultaneously, plus the in-process workflowLock * and the crash-recovery journal. * * @throws WorkflowError on cross-process lock contention ("another * Pluginator update run is in progress (pid N)") */ downloadBatch(plugins: PluginInfo[]): Promise; private downloadBatchInternal; /** * Download updates from update check results */ downloadUpdates(updates: PluginUpdateInfo[]): Promise; /** * Get the filename for a plugin */ private getFilename; } /** * Create a plugin downloader */ export declare function createDownloader(options: DownloadWorkflowOptions): PluginDownloader; /** * Quick download a single plugin */ export declare function quickDownload(info: PluginInfo, targetDir: string, onProgress?: (progress: DownloadProgress) => void): Promise; //# sourceMappingURL=download.d.ts.map