Prepare ForgeFlow for public release
This commit is contained in:
@@ -193,7 +193,11 @@ test("setup, Gitea updates and generic patches retain normalized public state",
|
||||
assert.equal(completed.state.setupComplete, true);
|
||||
assert.equal(completed.state.gitea.hasToken, true);
|
||||
assert.deepEqual(completed.state.workspaceRoots, ["C:/Projects"]);
|
||||
const update = await store.updateGitea({ baseUrl: "https://new.test", token: "", user: null });
|
||||
await assert.rejects(
|
||||
store.updateGitea({ baseUrl: "https://new.test", token: "", user: null }),
|
||||
(error) => error.code === "GITEA_TOKEN_ORIGIN_CHANGED"
|
||||
);
|
||||
const update = await store.updateGitea({ baseUrl: "https://gitea.test", token: "", user: null });
|
||||
assert.equal(update.preserved, true);
|
||||
assert.equal(store.data.gitea.user.login, "jens");
|
||||
const patched = await store.patch({ appearance: "light", workspaceRoots: ["D:/Code", "D:/Code"] });
|
||||
@@ -215,6 +219,20 @@ test("server saves reject absent credentials before mutating configuration", asy
|
||||
assert.deepEqual(store.data.servers, []);
|
||||
});
|
||||
|
||||
test("server credentials and trust are cleared when the connection identity changes", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
store.encryptSecret = (value) => `encrypted:${value}`;
|
||||
const saved = await store.saveServer({ host: "server-one", username: "deploy", authType: "password", basePath: "/mnt/apps", hostFingerprint: "SHA256:trusted" }, { password: "test-password" });
|
||||
await assert.rejects(
|
||||
store.saveServer({ ...saved, host: "server-two" }, {}),
|
||||
/password is required/i
|
||||
);
|
||||
assert.equal(store.data.servers[0].host, "server-one");
|
||||
const changed = await store.saveServer({ ...saved, host: "server-two" }, { password: "replacement-password" });
|
||||
assert.equal(changed.hostFingerprint, "");
|
||||
assert.equal(changed.hasPassword, true);
|
||||
});
|
||||
|
||||
test("deployment profile normalization covers safe defaults and every optional Unraid control", async (t) => {
|
||||
const { store } = await storeFixture(t);
|
||||
const actions = store.normalizeDeploymentProfile({ environment: "qa", statusUrl: "https://app.test/status" });
|
||||
|
||||
@@ -41,7 +41,9 @@ test('writes structured local diagnostics and exports a secret-free support bund
|
||||
authorization: `token ${secret}`,
|
||||
password: 'unsafe-password',
|
||||
message: `request failed with ${secret}`,
|
||||
path: path.join(os.homedir(), 'private', 'repository')
|
||||
path: path.join(os.homedir(), 'private', 'repository'),
|
||||
host: '192.168.10.20',
|
||||
basePath: '/mnt/user/appdata/private-app'
|
||||
});
|
||||
await service.flush();
|
||||
|
||||
@@ -57,7 +59,7 @@ test('writes structured local diagnostics and exports a secret-free support bund
|
||||
const result = await service.exportSupportBundle({
|
||||
destinationPath: destination,
|
||||
privacyMode: 'strict',
|
||||
publicState: { gitea: { baseUrl: 'https://gitea.example.test', hasToken: true, encryptedToken: 'ciphertext' }, preferences: {} },
|
||||
publicState: { gitea: { baseUrl: 'https://gitea.example.test', hasToken: true, encryptedToken: 'ciphertext' }, servers: [{ host: '192.168.10.20', username: 'deploy', basePath: '/mnt/user/appdata/private-app' }], preferences: {} },
|
||||
repositories: [{ id: 1, fullName: 'jens/private-repo', localPath: path.join(os.homedir(), 'private-repo'), localStatus: { head: 'a'.repeat(40), branch: { head: 'main' }, counts: {}, clean: true } }],
|
||||
operations: [{ repository: 'jens/private-repo', status: 'failed', runnerLog: `Authorization: token ${secret}` }],
|
||||
preflight: { checks: [] }
|
||||
@@ -67,6 +69,7 @@ test('writes structured local diagnostics and exports a secret-free support bund
|
||||
const bundleText = [...entries.values()].map((value) => value.toString('utf8')).join('\n');
|
||||
assert.doesNotMatch(bundleText, new RegExp(secret));
|
||||
assert.doesNotMatch(bundleText, /ciphertext|unsafe-password|jens\/private-repo/);
|
||||
assert.doesNotMatch(bundleText, /192\.168\.10\.20|\/mnt\/user\/appdata\/private-app/);
|
||||
assert.match(entries.get('manifest.json').toString(), /"containsSecrets": false/);
|
||||
assert.match(entries.get('repositories-sanitized.json').toString(), /fullname-[a-f0-9]{12}/);
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import toolsModule from '../src/main/external-tools-service.cjs';
|
||||
const { normalizeTool, expandTool } = toolsModule;
|
||||
|
||||
test('external tool templates expand as argument arrays without a shell', () => {
|
||||
const tool = normalizeTool({ executable: 'code.exe', args: ['--goto', '{file}:{line}', '{path}'] }, {});
|
||||
const tool = normalizeTool({ executable: 'code.exe', args: ['--malicious', 'ignored'] }, { executable: 'code.exe' }, 'editor');
|
||||
const invocation = expandTool(tool, { path: 'C:\\Projects\\App', file: 'C:\\Projects\\App\\src\\app.js', line: 12 });
|
||||
assert.equal(invocation.executable, 'code.exe');
|
||||
assert.deepEqual(invocation.args, ['--goto', 'C:\\Projects\\App\\src\\app.js:12', 'C:\\Projects\\App']);
|
||||
assert.throws(() => normalizeTool({ executable: 'code.exe\ncalc.exe', args: [] }, {}), /invalid/);
|
||||
assert.deepEqual(invocation.args, ['--reuse-window', '--goto', 'C:\\Projects\\App\\src\\app.js:12']);
|
||||
assert.throws(() => normalizeTool({ executable: 'code.exe\ncalc.exe', args: [] }, {}, 'editor'), /invalid/);
|
||||
assert.throws(() => normalizeTool({ executable: 'powershell.exe', args: ['-Command', 'calc'] }, {}, 'terminal'), /unsupported terminal tool/i);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import redaction from '../src/main/log-redaction.cjs';
|
||||
|
||||
const { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias } = redaction;
|
||||
const { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias, redactPrivateInfrastructure } = redaction;
|
||||
|
||||
test('redacts runtime credentials, structured secrets, private keys and URL credentials', () => {
|
||||
const token = ['gitea', 'TEST', 'ONLY', 'SecretToken123456'].join('_');
|
||||
@@ -31,15 +31,22 @@ test('sanitizes nested sensitive keys and aliases user paths', () => {
|
||||
});
|
||||
|
||||
test('strict privacy mode replaces stable identifiers deterministically', () => {
|
||||
const first = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens' }, { strictIdentifiers: true });
|
||||
const second = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens' }, { strictIdentifiers: true });
|
||||
const first = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens', host: '192.168.10.20', basePath: '/mnt/user/appdata' }, { strictIdentifiers: true });
|
||||
const second = sanitizeForDiagnostics({ fullName: 'jens/private-project', login: 'jens', host: '192.168.10.20', basePath: '/mnt/user/appdata' }, { strictIdentifiers: true });
|
||||
assert.equal(first.fullName, second.fullName);
|
||||
assert.equal(first.login, second.login);
|
||||
assert.notEqual(first.fullName, 'jens/private-project');
|
||||
assert.match(first.fullName, /^fullname-[a-f0-9]{12}$/);
|
||||
assert.notEqual(first.host, '192.168.10.20');
|
||||
assert.notEqual(first.basePath, '/mnt/user/appdata');
|
||||
assert.equal(stableAlias('same', 'repo'), stableAlias('same', 'repo'));
|
||||
});
|
||||
|
||||
test('strict privacy redacts private addresses, infrastructure URLs and server paths in log text', () => {
|
||||
const result = redactPrivateInfrastructure('host 192.168.10.20 url https://internal.example.test/status path /mnt/user/appdata/example');
|
||||
assert.doesNotMatch(result, /192\.168\.10\.20|internal\.example\.test|\/mnt\/user\/appdata/);
|
||||
});
|
||||
|
||||
test('path aliasing handles slash variants', () => {
|
||||
const result = pathAlias('C:\\Users\\Jens\\src and C:/Users/Jens/src', { homeDir: 'C:\\Users\\Jens', cwd: 'D:\\ForgeFlow' });
|
||||
assert.doesNotMatch(result, /Users[\\/]Jens/);
|
||||
|
||||
@@ -118,7 +118,7 @@ test("update repository parts reject path injection", async () => {
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("source updater confirms an external STARTED marker before ForgeFlow may close", async () => {
|
||||
test("source updater refuses an unsigned archive before launching a helper", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-update-handshake-"),
|
||||
);
|
||||
@@ -181,10 +181,11 @@ test("source updater confirms an external STARTED marker before ForgeFlow may cl
|
||||
remoteSha: "a".repeat(40),
|
||||
sha256: "b".repeat(64),
|
||||
};
|
||||
const result = await service.apply();
|
||||
assert.equal(result.confirmed, true);
|
||||
assert.ok(capturedArgs.includes("-StatusPath"));
|
||||
assert.ok(capturedArgs.includes("-UpdateId"));
|
||||
await assert.rejects(
|
||||
service.apply(),
|
||||
(error) => error.code === "SIGNED_SOURCE_UPDATE_REQUIRED",
|
||||
);
|
||||
assert.equal(capturedArgs, null);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -710,11 +711,12 @@ test("release manifest verification rejects a different publisher key", () => {
|
||||
});
|
||||
|
||||
test("Windows release pipeline emits signed provenance, manifest and SBOM evidence", async () => {
|
||||
const [pkgSource, signatureSource, checksumSource, manifestSigner] = await Promise.all([
|
||||
const [pkgSource, signatureSource, checksumSource, manifestSigner, releaseWorkflow] = await Promise.all([
|
||||
readFile(new URL("../package.json", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"),
|
||||
readFile(new URL("../.gitea/workflows/release.yml", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(pkgSource, /verify-release-signatures\.mjs/);
|
||||
assert.match(signatureSource, /FORGEFLOW_SIGNED_RELEASE/);
|
||||
@@ -729,6 +731,16 @@ test("Windows release pipeline emits signed provenance, manifest and SBOM eviden
|
||||
assert.match(manifestSigner, /Ed25519/);
|
||||
assert.match(manifestSigner, /release-manifest\.json/);
|
||||
assert.match(pkgSource, /sign-release-manifest\.mjs/);
|
||||
assert.doesNotMatch(releaseWorkflow, /checkout@v\d|setup-node@v\d/);
|
||||
assert.match(releaseWorkflow, /checkout@[a-f0-9]{40}/);
|
||||
assert.match(releaseWorkflow, /setup-node@[a-f0-9]{40}/);
|
||||
assert.ok(
|
||||
releaseWorkflow.indexOf("Validate version bump and build release artifacts") <
|
||||
releaseWorkflow.indexOf("FORGEFLOW_RELEASE_SIGNING_KEY_PEM"),
|
||||
"signing secrets must not be present during dependency installation and quality checks",
|
||||
);
|
||||
assert.match(releaseWorkflow, /finally \{/);
|
||||
assert.match(releaseWorkflow, /Remove-Item -LiteralPath \$privateKeyPath -Force/);
|
||||
const publisher = await readFile(new URL("../scripts/publish-binary-release.cjs", import.meta.url), "utf8");
|
||||
assert.match(publisher, /draft: true/);
|
||||
assert.match(publisher, /requiredAssets/);
|
||||
|
||||
Reference in New Issue
Block a user