/** * Pre-Update Snapshot Store * * Archives a plugin's CURRENT state (JAR + `plugins//` data folder) * immediately BEFORE an update replaces it, so a bad update can be rolled * back without losing config migrations. Snapshots live under * `~/.pluginator/snapshots///`: * * ``` * snapshots/// * ├── .jar # archived copy of the pre-update JAR * ├── data.tar.gz # tar of plugins// (when present + under cap) * └── manifest.json # versions, paths, sha256 of jar, createdAt * ``` * * Safety properties (roadmap Phase 4.3 / audit workflows-infra.md §3): * - Snapshots are built in a `.partial-` staging dir and renamed into place, * so a crash mid-snapshot never produces a half-snapshot that looks valid. * - Data folders over the size cap (default 500 MB) are skipped with an * explicit `skippedReason` marker in the manifest — an honest jar-only * snapshot instead of a silent multi-GB tar. * - Retention is bounded: the newest N snapshots per plugin are kept * (default 3, configurable). * - The manifest records the archived JAR's sha256; rollback refuses to * touch the server unless the archive still matches it. * * The next-wave SafeApply agent calls {@link SnapshotStore.createSnapshot} * from the download/manual-install success paths; `workflows/rollback.ts` * consumes snapshots for restore. * * @since v2.12.4 (roadmap Phase 4, assignment B4) */ /** Keep the newest N snapshots per plugin (configurable per store) */ export declare const DEFAULT_MAX_SNAPSHOTS_PER_PLUGIN = 3; /** Skip data folders bigger than this (configurable per store) — 500 MB */ export declare const DEFAULT_DATA_SIZE_CAP_BYTES: number; /** The update event a snapshot was taken for */ export interface SnapshotEvent { /** Version installed when the snapshot was taken (what rollback restores) */ fromVersion: string; /** Version of the incoming update the snapshot precedes */ toVersion: string; /** Source channel of the incoming update (PluginSourceType or 'unknown'/'manual') */ sourceType: string; } /** Archived JAR details */ export interface SnapshotJarInfo { /** Original JAR filename (restore target name) */ filename: string; /** Absolute path the JAR was archived from */ originalPath: string; /** sha256 of the archived copy — rollback validity gate */ sha256: string; /** Size in bytes */ sizeBytes: number; } /** Data-folder archive details — `included: false` is always accompanied by a reason */ export interface SnapshotDataInfo { /** Whether a data archive was captured */ included: boolean; /** Basename of the plugin data folder (restore target name) */ dataDirName?: string; /** Absolute path the data folder was archived from */ originalPath?: string; /** Archive filename inside the snapshot dir (data.tar.gz) */ archiveFile?: string; /** Number of files archived */ fileCount?: number; /** Total uncompressed bytes of the data folder */ totalSizeBytes?: number; /** Explicit honest marker for why no data archive exists (when included=false) */ skippedReason?: string; } /** Per-snapshot manifest (manifest.json) */ export interface SnapshotManifest { manifestVersion: string; snapshotId: string; pluginName: string; createdAt: string; event: SnapshotEvent; jar: SnapshotJarInfo; data: SnapshotDataInfo; } /** Options for {@link SnapshotStore.createSnapshot} */ export interface CreateSnapshotOptions { /** Plugin name (used as the snapshot grouping key) */ pluginName: string; /** Absolute path to the CURRENT (pre-update) JAR */ jarPath: string; /** Absolute path to the plugin's data folder (plugins//), when it exists */ dataDir?: string; /** The update event this snapshot precedes */ event: SnapshotEvent; } /** Result of {@link SnapshotStore.createSnapshot} — never throws; failures carry a reason */ export interface CreateSnapshotResult { success: boolean; manifest?: SnapshotManifest; /** Absolute path of the created snapshot directory */ snapshotDir?: string; /** Snapshot IDs pruned by retention after this snapshot was created */ prunedSnapshotIds: string[]; error?: string; } /** Result of {@link SnapshotStore.verifySnapshotJar} */ export interface SnapshotJarVerification { valid: boolean; expectedSha256?: string; actualSha256?: string; error?: string; } /** * Sanitize a plugin name into a filesystem-safe snapshot key. * Lowercased; anything outside [a-z0-9._-] becomes '_'; leading dots stripped * (prevents traversal and collision with dot-prefixed staging dirs). */ export declare function sanitizePluginKey(pluginName: string): string; /** Options for constructing a {@link SnapshotStore} */ export interface SnapshotStoreOptions { /** Snapshot root directory (tests pin a temp dir) */ root?: string; /** Retention bound — newest N snapshots kept per plugin */ maxSnapshotsPerPlugin?: number; /** Data folders larger than this are skipped (jar-only snapshot + marker) */ dataSizeCapBytes?: number; } /** * Snapshot store — owns the snapshot directory tree. * * All deletion in this module is confined to store-owned paths (retention * pruning of old snapshot dirs, stale staging sweep, temp extraction * cleanup). Server directories are NEVER deleted from here. */ export declare class SnapshotStore { private root; private maxSnapshotsPerPlugin; private dataSizeCapBytes; constructor(options?: SnapshotStoreOptions); /** Absolute path of a snapshot directory */ getSnapshotDir(pluginName: string, snapshotId: string): string; /** Absolute path of a snapshot's archived JAR */ getJarPath(manifest: SnapshotManifest): string; /** Absolute path of a snapshot's data archive, or null when jar-only */ getDataArchivePath(manifest: SnapshotManifest): string | null; /** * Archive the plugin's current JAR + data folder before an update. * Never throws — failures return `{ success: false, error }` and leave no * partial snapshot behind (staging dir is cleaned up best-effort). */ createSnapshot(options: CreateSnapshotOptions): Promise; /** Build the data portion of the manifest, archiving when appropriate */ private archiveDataDir; /** * List snapshots (newest first). With a plugin name, only that plugin's; * otherwise all plugins. Corrupt/incomplete snapshot dirs are skipped with * a warning log — they are never silently treated as restorable. */ listSnapshots(pluginName?: string): Promise; /** Load a specific snapshot's manifest, or null when missing/invalid */ getSnapshot(pluginName: string, snapshotId: string): Promise; /** Verify the archived JAR still matches the manifest's recorded sha256 */ verifySnapshotJar(pluginName: string, snapshotId: string): Promise; /** * Extract a snapshot's data archive into a temp dir, run `fn` against it, * and clean the temp dir up afterwards. Throws when the snapshot has no * data archive (check `manifest.data.included` first). */ withExtractedData(pluginName: string, snapshotId: string, fn: (extractedDir: string) => Promise): Promise; /** Delete one snapshot dir (store-owned). Returns whether it existed. */ deleteSnapshot(pluginName: string, snapshotId: string): Promise; private readManifest; /** * Retention: keep the newest `maxSnapshotsPerPlugin` snapshots, delete the * rest (store-owned dirs only). Also sweeps stale `.partial-` staging dirs * left behind by crashed snapshot attempts. */ private prune; } /** * Get or create the global snapshot store. * @param options Pins root/retention/cap on FIRST call (tests use a temp dir) */ export declare function getSnapshotStore(options?: SnapshotStoreOptions): SnapshotStore; /** Reset the global snapshot store (mainly for testing) */ export declare function resetSnapshotStore(): void; //# sourceMappingURL=snapshot-store.d.ts.map