From 01bd7908363aea58329d133786fc328ae5052137 Mon Sep 17 00:00:00 2001 From: John-Ryan21337 <227466507+John-Ryan21337@users.noreply.github.com> Date: Fri, 11 Sep 2026 22:31:04 -0700 Subject: [PATCH] fix: bind desktop artifact audit to its packaged update target --- AGENTS.md | 2 + .../src/app/DesktopArtifactAudit.test.ts | 74 ++++++++++++++++++- apps/desktop/src/app/DesktopArtifactAudit.ts | 74 +++++++++++++++++-- scripts/build-desktop-artifact.test.ts | 45 ++++++++++- scripts/build-desktop-artifact.ts | 9 ++- 5 files changed, 195 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c4f7d53ac..96b3d754d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -219,6 +219,8 @@ If a tradeoff is required, choose correctness, durability, and debuggability ove ## Current Runtime Architecture +- Desktop artifact builds record the resolved GitHub update target in the staged runtime manifest. The packaged audit must bind `app-update.yml` to that target, never to the launch environment or a hard-coded fork. Legacy manifests without the field retain the official upstream target. Reject malformed targets, duplicate metadata keys, YAML aliases/nesting, and endpoint overrides; preserve the existing packaging and signing paths on every platform. + - Backend startup must await the final successful `ProviderDaemonManager.ensureRunning` result, including its authenticated lease, before launching the backend. A provisional `currentConfig` can belong to a daemon attempt that is subsequently replaced on the same socket; inheriting it strands the backend with a rejected credential even though the replacement daemon is healthy. IPC/settings preparation may overlap daemon readiness, but provider-capable backend work must not. Startup failure or quitting during readiness must never launch the backend. Keep regression coverage for a superseded first attempt, final readiness failure, and cancellation, and preserve the separate stop-backend -> recover-daemon -> start-backend watchdog ordering. - Daemon marker existence/read/decode errors are inconclusive ownership, not absence. Fail with a content-free marker-inspection error before entering spawn/unlink/reaping paths; only confirmed marker absence permits a fresh daemon. A later retry can adopt the same live owner after the filesystem recovers. diff --git a/apps/desktop/src/app/DesktopArtifactAudit.test.ts b/apps/desktop/src/app/DesktopArtifactAudit.test.ts index 10b85d221..3570d5dd3 100644 --- a/apps/desktop/src/app/DesktopArtifactAudit.test.ts +++ b/apps/desktop/src/app/DesktopArtifactAudit.test.ts @@ -1,6 +1,11 @@ import { assert, describe, it } from "@effect/vitest"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { vi } from "vitest"; import { + auditPackagedDesktopArtifact, containsDesktopArtifactResidue, containsDesktopArtifactSecretMaterial, isDesktopRuntimeManifestValid, @@ -52,12 +57,79 @@ describe("DesktopArtifactAudit", () => { assert.isFalse(containsDesktopArtifactSecretMaterial("erilog-sk-prompt-state-selector")); }); - it("requires updater metadata to target the official release repository", () => { + it("uses the official target only when a build-time target is absent", () => { assert.isTrue( isDesktopUpdateMetadataValid("provider: github\nowner: cafeai\nrepo: cafe-code\n"), ); assert.isFalse( isDesktopUpdateMetadataValid("provider: generic\nurl: https://updates.invalid\n"), ); + assert.isFalse( + isDesktopUpdateMetadataValid("provider: github\nowner: cafeai\nrepo: cafe-code\n", null), + ); + }); + + it("matches the configured fork and accepts the builder's quoted cache and nightly fields", () => { + const target = { provider: "github", owner: "John-Ryan21337", repo: "club-code" }; + const metadata = + "owner: John-Ryan21337\nrepo: club-code\nprovider: github\nreleaseType: prerelease\nchannel: nightly\nupdaterCacheDirName: '@cafecodedesktop-runtime-updater'\n"; + assert.isTrue(isDesktopUpdateMetadataValid(metadata, target)); + assert.isFalse(isDesktopUpdateMetadataValid(metadata)); + assert.isFalse(isDesktopUpdateMetadataValid(metadata, { ...target, repo: "other" })); + assert.isFalse(isDesktopUpdateMetadataValid(metadata, { ...target, owner: "other" })); + }); + + it("rejects ambiguous metadata and endpoint overrides even when matching lines are present", () => { + const valid = "provider: github\nowner: cafeai\nrepo: cafe-code\n"; + for (const suffix of [ + "owner: attacker\n", + "repo: other\n", + "provider: generic\n", + "host: updates.invalid\n", + "url: https://updates.invalid\n", + "protocol: http\n", + "path: alternate\n", + "<<: *override\n", + "channel: unknown\n", + ]) { + assert.isFalse(isDesktopUpdateMetadataValid(valid + suffix), suffix); + } + assert.isFalse(isDesktopUpdateMetadataValid(valid + " ".repeat(16_384))); + assert.isFalse(isDesktopRuntimeManifestValid({ ...validManifest, cafeCodeUpdateTarget: null })); + assert.isFalse( + isDesktopRuntimeManifestValid({ + ...validManifest, + cafeCodeUpdateTarget: { provider: "github", owner: "cafeai", repo: "../other" }, + }), + ); + }); + + it("binds the packaged audit to the manifest instead of the current process environment", async () => { + const resources = await mkdtemp(join(tmpdir(), "desktop-audit-target-")); + const appArchive = join(resources, "app.asar"); + const target = { provider: "github", owner: "release-owner", repo: "desktop-releases" }; + try { + await mkdir(join(appArchive, "apps"), { recursive: true }); + await writeFile(join(appArchive, "apps", "main.js"), "console.log('desktop');\n"); + await writeFile( + join(appArchive, "package.json"), + JSON.stringify({ ...validManifest, cafeCodeUpdateTarget: target }), + ); + vi.stubEnv("GITHUB_REPOSITORY", "attacker/other"); + vi.stubEnv("CAFE_CODE_DESKTOP_UPDATE_REPOSITORY", "attacker/other"); + await writeFile( + join(resources, "app-update.yml"), + "provider: github\nowner: release-owner\nrepo: desktop-releases\n", + ); + assert.isTrue(await auditPackagedDesktopArtifact(resources, "win32")); + await writeFile( + join(resources, "app-update.yml"), + "provider: github\nowner: attacker\nrepo: other\n", + ); + assert.isFalse(await auditPackagedDesktopArtifact(resources, "win32")); + } finally { + vi.unstubAllEnvs(); + await rm(resources, { recursive: true, force: true }); + } }); }); diff --git a/apps/desktop/src/app/DesktopArtifactAudit.ts b/apps/desktop/src/app/DesktopArtifactAudit.ts index 28e2d3864..f8e52e749 100644 --- a/apps/desktop/src/app/DesktopArtifactAudit.ts +++ b/apps/desktop/src/app/DesktopArtifactAudit.ts @@ -68,13 +68,30 @@ function validDependencyMap(value: unknown): boolean { ); } +const DEFAULT_UPDATE_TARGET = { provider: "github", owner: "cafeai", repo: "cafe-code" }; + +function readUpdateTarget(value: unknown): Record | undefined { + const target = readRecord(value); + return target?.provider === "github" && + typeof target.owner === "string" && + /^[a-z0-9][a-z0-9-]{0,38}$/iu.test(target.owner) && + typeof target.repo === "string" && + /^[a-z0-9_.-]{1,100}$/iu.test(target.repo) && + target.repo !== "." && + target.repo !== ".." + ? target + : undefined; +} + export function isDesktopRuntimeManifestValid(value: unknown): boolean { const manifest = readRecord(value); if ( manifest?.name !== "@cafecode/desktop-runtime" || manifest.private !== true || manifest.main !== "apps/desktop/dist-electron/main.cjs" || - !validDependencyMap(manifest.dependencies) + !validDependencyMap(manifest.dependencies) || + (manifest.cafeCodeUpdateTarget !== undefined && + readUpdateTarget(manifest.cafeCodeUpdateTarget) === undefined) ) { return false; } @@ -106,11 +123,53 @@ export function containsDesktopArtifactResidue(path: string, source?: string): b ); } -export function isDesktopUpdateMetadataValid(source: string): boolean { +export function isDesktopUpdateMetadataValid( + source: string, + expectedTarget: unknown = DEFAULT_UPDATE_TARGET, +): boolean { + const target = readUpdateTarget(expectedTarget); + if (!target || source.length > 16_384) return false; + + // Accept only the flat scalar metadata emitted by our builder configuration. + // Duplicate keys, aliases, nested values, and endpoint overrides must fail. + const fields = new Map(); + const allowedFields = new Set([ + "provider", + "owner", + "repo", + "channel", + "releaseType", + "updaterCacheDirName", + ]); + for (const line of source.split(/\r?\n/u)) { + if (line.trim() === "") continue; + const match = /^([a-zA-Z]+):[ \t]*(\S(?:.*\S)?)[ \t]*$/u.exec(line); + if (!match) return false; + const [, key, raw] = match; + if (!key || !raw || !allowedFields.has(key) || fields.has(key)) return false; + let value = raw; + if (raw.startsWith('"')) { + try { + const parsed: unknown = JSON.parse(raw); + if (typeof parsed !== "string") return false; + value = parsed; + } catch { + return false; + } + } else if (raw.startsWith("'")) { + if (!/^'(?:[^']|'')*'$/u.test(raw)) return false; + value = raw.slice(1, -1).replaceAll("''", "'"); + } else if (!/^[a-z0-9_@./-]+$/iu.test(raw)) { + return false; + } + fields.set(key, value); + } return ( - /^provider:\s*github\s*$/mu.test(source) && - /^owner:\s*cafeai\s*$/mu.test(source) && - /^repo:\s*cafe-code\s*$/mu.test(source) + fields.get("provider") === target.provider && + fields.get("owner") === target.owner && + fields.get("repo") === target.repo && + (!fields.has("channel") || ["latest", "nightly"].includes(fields.get("channel")!)) && + (!fields.has("releaseType") || ["release", "prerelease"].includes(fields.get("releaseType")!)) ); } @@ -167,7 +226,10 @@ export async function auditPackagedDesktopArtifact( ]); if (resourceEntries.some((entry) => !expectedTopLevelEntries.has(entry.name))) return false; if ( - !isDesktopUpdateMetadataValid(await readFile(join(resourcesPath, "app-update.yml"), "utf8")) + !isDesktopUpdateMetadataValid( + await readFile(join(resourcesPath, "app-update.yml"), "utf8"), + readRecord(manifest)?.cafeCodeUpdateTarget, + ) ) { return false; } diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index d78eaa147..39cf3a77b 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -3,9 +3,11 @@ import { assert, it } from "@effect/vitest"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; +import { afterEach, beforeEach, vi } from "vitest"; import { MANAGED_WINDOWS_NODE_VERSION, + createBuildConfig, desktopArtifactListSatisfiesTarget, resolveBuildOptions, resolveDesktopBuildIconAssets, @@ -23,7 +25,14 @@ import { import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts"; it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { - it("always emits deterministic official updater metadata", () => { + beforeEach(() => { + // Each case chooses its update source independently of the CI repository. + vi.stubEnv("CAFE_CODE_DESKTOP_UPDATE_REPOSITORY", undefined); + vi.stubEnv("GITHUB_REPOSITORY", undefined); + }); + afterEach(() => vi.unstubAllEnvs()); + + it("emits official updater metadata when repository settings are absent", () => { assert.deepStrictEqual(resolveGitHubPublishConfig("latest"), { provider: "github", owner: "cafeai", @@ -39,6 +48,40 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }); }); + it.effect("binds the manifest to the resolved publish identity on each platform", () => + Effect.gen(function* () { + for (const [workflowRepository, explicitRepository, owner, repo] of [ + [undefined, undefined, "cafeai", "cafe-code"], + ["fork-owner/cafe-fork", undefined, "fork-owner", "cafe-fork"], + [ + "fork-owner/cafe-fork", + "release-owner/desktop-releases", + "release-owner", + "desktop-releases", + ], + ]) { + vi.stubEnv("GITHUB_REPOSITORY", workflowRepository); + vi.stubEnv("CAFE_CODE_DESKTOP_UPDATE_REPOSITORY", explicitRepository); + for (const platform of ["mac", "linux", "win"] as const) { + const config = yield* createBuildConfig( + platform, + platform === "mac" ? "dmg" : platform === "linux" ? "AppImage" : "nsis", + "0.0.17-nightly.20260413.42", + false, + false, + undefined, + ); + assert.deepStrictEqual(config.extraMetadata, { + cafeCodeUpdateTarget: { provider: "github", owner, repo }, + }); + assert.deepStrictEqual(config.publish, [ + { provider: "github", owner, repo, releaseType: "prerelease", channel: "nightly" }, + ]); + } + } + }), + ); + it("resolves the dedicated nightly updater channel from nightly versions", () => { assert.equal(resolveDesktopUpdateChannel("0.0.17-nightly.20260413.42"), "nightly"); assert.equal(resolveDesktopUpdateChannel("0.0.17"), "latest"); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 96befdcdf..a73d5c8c8 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -915,7 +915,7 @@ export function resolveLinuxDesktopBuildConfig(target: string): Record