import test from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { DeployKeyLifecycleService } = require("../src/main/deploy-key-lifecycle-service.cjs"); const { UnraidDeployKeyHost, parseDeployKeyMarker } = require("../src/main/unraid-deploy-key-host.cjs"); const repository = { fullName: "Jens/Portfolio" }; const baseProfile = { id: "production", serverId: "unraid", deploymentMode: "server-git", serverGitAccess: { configured: true, deployKeyId: 7, keyFingerprint: "SHA256:old", hostFingerprint: "SHA256:host" } }; function fixture(overrides = {}) { let profile = structuredClone(baseProfile); const events = []; const remoteKeys = overrides.remoteKeys || [{ id: 7, title: "ForgeFlow old", read_only: true, key: "ssh-ed25519 T0xE old" }]; const store = { getDeploymentProfile: () => profile, getServer: () => ({ id: "unraid", name: "Unraid" }), getRepositories: () => [{ fullName: repository.fullName }], getDeploymentProfiles: () => [profile, ...(overrides.otherProfiles || [])], saveDeploymentProfile: async (_name, value) => { if (overrides.saveFails) throw new Error("switch failed"); profile = structuredClone(value); events.push("profile-saved"); return profile; }, createRecoverySnapshot: async () => ({ filePath: "snapshot.json" }), }; const gitea = { listDeployKeys: async () => structuredClone(remoteKeys), createReadOnlyDeployKey: async () => { if (overrides.registrationFails) throw new Error("registration failed"); return { id: 8, title: "new", read_only: overrides.writable !== true, key: "ssh-ed25519 TkVX new" }; }, deleteDeployKey: async (_owner, _repo, id) => { events.push(`delete:${id}`); if (overrides.deleteOldFails && id === 7) throw new Error("old revoke failed"); return { deleted: true }; }, }; const keyHost = { inspect: async () => overrides.inspect || ({ privateKeyPresent: true, publicKey: "ssh-ed25519 T0xE old", fingerprint: overrides.changedFingerprint ? "SHA256:changed" : "SHA256:old", hostFingerprint: "SHA256:host" }), backup: async () => ({ recovery: "server-backup", publicKey: "ssh-ed25519 T0xE old" }), generate: async () => ({ publicKey: "ssh-ed25519 TkVX new", fingerprint: "SHA256:new" }), verifyCandidate: async () => overrides.verifyFails ? ({ ready: false, fingerprint: "SHA256:new" }) : ({ ready: true, fingerprint: "SHA256:new", hostFingerprint: "SHA256:host", remoteSha: "a".repeat(40) }), preflightCandidate: async () => { if (overrides.preflightFails) throw new Error("preflight failed"); events.push("preflight"); }, promote: async () => { events.push("promote"); }, verifyActive: async () => overrides.postFails ? ({ ready: false }) : ({ ready: true, fingerprint: "SHA256:new" }), commit: async () => { events.push("commit"); }, rollback: async () => { events.push("rollback"); }, revoke: async () => { events.push("revoke-server"); if (overrides.revokeFails) throw new Error("server revoke failed"); }, restore: async () => ({ publicKey: "ssh-ed25519 UkVTVE9SRQ restored", fingerprint: "SHA256:restored", hostFingerprint: "SHA256:host" }), }; const audit = { append: async (name) => events.push(name) }; const service = new DeployKeyLifecycleService({ store, gitea, keyHost, audit, clock: () => "2026-07-29T00:00:00.000Z" }); return { service, events, getProfile: () => profile }; } test("deploy-key rotation verifies, switches, revokes and post-verifies in order", async () => { const { service, events, getProfile } = fixture(); const plan = await service.planRotation({ repository, profileId: "production" }); const result = await service.rotate({ repository, profileId: "production", expectedPlanId: plan.id }); assert.equal(result.profile.serverGitAccess.deployKeyId, 8); assert.equal(getProfile().serverGitAccess.keyFingerprint, "SHA256:new"); assert.deepEqual(events.filter((event) => ["preflight", "promote", "profile-saved", "delete:7", "commit"].includes(event)), ["preflight", "promote", "profile-saved", "delete:7", "commit"]); }); for (const [name, overrides, message] of [ ["registration failure", { registrationFails: true }, /registration failed/], ["writable candidate", { writable: true }, /write access/], ["candidate verification failure", { verifyFails: true }, /could not prove/], ["candidate preflight failure", { preflightFails: true }, /preflight failed/], ["atomic profile switch failure", { saveFails: true }, /switch failed/], ["old key revocation failure", { deleteOldFails: true }, /old revoke failed/], ["post-rotation failure", { postFails: true }, /Post-rotation verification failed/], ]) test(`deploy-key rotation rolls back on ${name}`, async () => { const { service, events } = fixture(overrides); const plan = await service.planRotation({ repository, profileId: "production" }); await assert.rejects(service.rotate({ repository, profileId: "production", expectedPlanId: plan.id }), message); assert.ok(events.includes("rollback")); }); test("rotation rejects a stale content-addressed plan", async () => { const { service } = fixture(); await assert.rejects(service.rotate({ repository, profileId: "production", expectedPlanId: "stale" }), (error) => error.code === "DEPLOY_KEY_ROTATION_PLAN_STALE"); }); test("inventory detects stale, orphaned, shared, conflicting and changed-fingerprint keys", async () => { const { service } = fixture({ changedFingerprint: true, remoteKeys: [{ id: 9, title: "ForgeFlow orphan", read_only: true, key: "ssh-ed25519 T1JQSEFO orphan" }, { id: 10, title: "writable", read_only: false, key: "ssh-ed25519 T0xE old" }], otherProfiles: [{ id: "staging", serverId: "unraid", serverGitAccess: { deployKeyId: 11, keyFingerprint: "SHA256:changed" } }], }); const report = await service.inventory({ repository, profileId: "production" }); assert.equal(report.stale, true); assert.equal(report.orphaned.length, 1); assert.equal(report.shared.length, 1); assert.equal(report.conflicts.length, 1); assert.equal(report.ready, false); }); test("revocation requires reviewed impact, disables pull and preserves recovery", async () => { const { service, events, getProfile } = fixture(); const plan = await service.planRevocation({ repository, profileId: "production" }); assert.equal(plan.containersUnaffected, true); await assert.rejects(service.revoke({ repository, profileId: "production" }), (error) => error.code === "DEPLOY_KEY_REVOCATION_PLAN_REQUIRED"); const result = await service.revoke({ repository, profileId: "production", expectedPlanId: plan.id }); assert.equal(result.recovery, "server-backup"); assert.equal(getProfile().deploymentMode, "monitor-only"); assert.ok(events.includes("delete:7")); assert.ok(events.includes("revoke-server")); }); test("revoked access can be restored and verified", async () => { const { service } = fixture(); const result = await service.restore({ repository, profileId: "production" }); assert.equal(result.profile.deploymentMode, "server-git"); assert.equal(result.profile.serverGitAccess.keyFingerprint, "SHA256:restored"); assert.equal(result.proof.ready, true); }); test("failed server revocation restores repository access", async () => { const { service, events, getProfile } = fixture({ revokeFails: true }); const plan = await service.planRevocation({ repository, profileId: "production" }); await assert.rejects(service.revoke({ repository, profileId: "production", expectedPlanId: plan.id }), /server revoke failed/); assert.equal(getProfile().deploymentMode, "server-git"); assert.ok(events.includes("delete:7")); }); test("Unraid key host parser rejects unverifiable output", () => { assert.throws(() => parseDeployKeyMarker("ordinary ssh output", "__FORGEFLOW_KEY__"), /did not return/); }); test("Unraid candidate generation returns public evidence and paths but never private key content", async () => { const publicKey = "ssh-ed25519 TkVX forgeflow"; const ssh = { exec: async () => ({ stdout: `__FORGEFLOW_KEY_CANDIDATE__\npublicKey=${Buffer.from(publicKey).toString("base64")}\nfingerprint=SHA256:new\nhostFingerprint=SHA256:host\n` }) }; const host = new UnraidDeployKeyHost({ ssh }); const candidate = await host.generate({ repository: { fullName: "Jens/Portfolio" }, server: { id: "unraid", basePath: "/mnt/user/appdata" } }); assert.equal(candidate.publicKey, publicKey); assert.equal(candidate.fingerprint, "SHA256:new"); assert.equal(candidate.privateKey, undefined); assert.match(candidate.paths.privateKey, /candidate-[0-9a-f-]+\/deploy-key$/); }); test("Unraid active key inspection exposes only public metadata", async () => { const publicKey = "ssh-ed25519 T0xE forgeflow"; const ssh = { exec: async () => ({ stdout: `__FORGEFLOW_KEY_INSPECT__\nprivateKeyPresent=true\npublicKey=${Buffer.from(publicKey).toString("base64")}\nfingerprint=SHA256:old\nhostFingerprint=SHA256:host\n` }) }; const host = new UnraidDeployKeyHost({ ssh }); const evidence = await host.inspect({ repository: { fullName: "Jens/Portfolio" }, server: { id: "unraid", basePath: "/mnt/user/appdata" } }); assert.deepEqual(evidence, { privateKeyPresent: true, publicKey, fingerprint: "SHA256:old", hostFingerprint: "SHA256:host" }); });