From ee76fdb57871ff94a22ba07415930cc296284840 Mon Sep 17 00:00:00 2001 From: hututuQQQ <245432843+hututuQQQ@users.noreply.github.com> Date: Thu, 6 Aug 2026 07:38:16 +0800 Subject: [PATCH] feat: add macOS ARM64 desktop release --- apps/desktop/package.json | 2 +- apps/server/package.json | 2 +- apps/web/package.json | 2 +- packages/contracts/package.json | 2 +- pnpm-workspace.yaml | 4 +- scripts/build-desktop-artifact.test.ts | 88 ++++++++++- scripts/build-desktop-artifact.ts | 180 +++++++++++++++++++--- scripts/desktop-release-evidence.test.ts | 71 +++++++++ scripts/desktop-release-evidence.ts | 187 +++++++++++++++++++++++ 9 files changed, 502 insertions(+), 36 deletions(-) create mode 100644 scripts/desktop-release-evidence.test.ts create mode 100644 scripts/desktop-release-evidence.ts diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 2725e7ade..c0e740d71 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/desktop", - "version": "0.1.5", + "version": "0.1.6", "private": true, "type": "module", "main": "dist-electron/main.cjs", diff --git a/apps/server/package.json b/apps/server/package.json index bca383219..62ac68f62 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "t3", - "version": "0.1.5", + "version": "0.1.6", "private": true, "license": "MIT", "bin": { diff --git a/apps/web/package.json b/apps/web/package.json index 5f2b5affa..91bb74474 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/web", - "version": "0.1.5", + "version": "0.1.6", "private": true, "type": "module", "scripts": { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index cf7329118..135a0fdb2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/contracts", - "version": "0.1.5", + "version": "0.1.6", "private": true, "files": [ "dist" diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 42529f56e..18155d8e5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -145,6 +145,6 @@ peerDependencyRules: vite: "*" supportedArchitectures: - cpu: [current, x64] + cpu: [current, x64, arm64] libc: [current, glibc] - os: [current, linux] + os: [current, linux, darwin] diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 146ad986a..89b49a075 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -17,6 +17,7 @@ import { createStagePatchedDependencies, createBuildConfig, DESKTOP_ASAR_UNPACK, + IncompleteMacSigningCredentialsError, InvalidMacPasskeyRpDomainError, InvalidMacPasskeyPublishableKeyError, InvalidMockUpdateServerPortError, @@ -25,8 +26,10 @@ import { MacPasskeySigningConfigurationResolutionError, MissingMacPasskeyProvisioningProfileError, renderMacPasskeyEntitlements, + renderMacSigningEntitlements, resolveClerkPasskeyNativeArtifacts, resolveMacPasskeySigningConfiguration, + resolveMacSigningCredentialState, resolveDesktopRuntimeDependencies, resolveFffNativeDependencies, resolveBuildOptions, @@ -405,6 +408,32 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.include(entitlements, "com.apple.security.cs.allow-jit"); }); + it("enables macOS signing only for the complete fixed credential interface", () => { + assert.deepStrictEqual(resolveMacSigningCredentialState({}), { mode: "unsigned" }); + assert.deepStrictEqual( + resolveMacSigningCredentialState({ + CSC_LINK: "certificate", + CSC_KEY_PASSWORD: "password", + APPLE_API_KEY: "private-key", + APPLE_API_KEY_ID: "KEY123", + APPLE_API_ISSUER: "issuer", + APPLE_TEAM_ID: "abc1234567", + }), + { mode: "signed", teamId: "ABC1234567" }, + ); + assert.throws( + () => resolveMacSigningCredentialState({ CSC_LINK: "certificate" }), + IncompleteMacSigningCredentialsError, + ); + }); + + it("renders hardened-runtime JIT entitlements without requiring Clerk provisioning", () => { + const entitlements = renderMacSigningEntitlements("ABC1234567"); + assert.include(entitlements, "com.apple.security.cs.allow-jit"); + assert.notInclude(entitlements, "com.apple.application-identifier"); + assert.notInclude(entitlements, "com.apple.developer.associated-domains"); + }); + it("rejects incomplete macOS passkey signing configuration", () => { const captureError = (env: Readonly>) => { try { @@ -495,17 +524,32 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { true, false, undefined, - ["node_modules/tr46/lib/.gitkeep"], + [ + "bin/node", + "bin/sigma-exec", + "node_modules/native/addon.node", + "node_modules/tr46/lib/.gitkeep", + ], { entitlementsPath: "/tmp/entitlements.mac.plist", provisioningProfilePath: "/tmp/sigma-code.provisionprofile", }, + ["bin/node", "bin/sigma-exec", "node_modules/native/addon.node"], ); const mac = config.mac as Record; assert.equal(config.appId, "io.github.hututuqqq.sigmacode"); assert.equal(mac.entitlements, "/tmp/entitlements.mac.plist"); + assert.equal(mac.entitlementsInherit, "/tmp/entitlements.mac.plist"); assert.equal(mac.provisioningProfile, "/tmp/sigma-code.provisionprofile"); + assert.equal(mac.minimumSystemVersion, "13.5"); + assert.equal(mac.hardenedRuntime, true); + assert.equal(mac.notarize, true); + assert.deepStrictEqual(mac.binaries, [ + "Contents/Resources/sigma-runtime/bin/node", + "Contents/Resources/sigma-runtime/bin/sigma-exec", + "Contents/Resources/sigma-runtime/node_modules/native/addon.node", + ]); assert.equal(mac.executableName, "sigma-code"); assert.deepStrictEqual(config.protocols, [ { name: "Sigma Code", schemes: ["sigmacode", "sigmacode-dev"] }, @@ -513,6 +557,12 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.deepStrictEqual(config.extraResources, [ { from: "legal", to: "legal" }, { from: ".", to: ".", filter: ["sigma-runtime/**/*"] }, + { from: "sigma-runtime/bin/node", to: "sigma-runtime/bin/node" }, + { from: "sigma-runtime/bin/sigma-exec", to: "sigma-runtime/bin/sigma-exec" }, + { + from: "sigma-runtime/node_modules/native/addon.node", + to: "sigma-runtime/node_modules/native/addon.node", + }, { from: "sigma-runtime/node_modules/tr46/lib/.gitkeep", to: "sigma-runtime/node_modules/tr46/lib/.gitkeep", @@ -521,6 +571,26 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))), ); + it.effect("forces unsigned macOS previews to disable identity, hardening, and notarization", () => + Effect.gen(function* () { + const config = yield* createBuildConfig( + "mac", + "dmg", + "1.2.3", + false, + false, + undefined, + [], + undefined, + ); + const mac = config.mac as Record; + assert.strictEqual(mac.identity, null); + assert.equal(mac.hardenedRuntime, false); + assert.equal(mac.notarize, false); + assert.equal(mac.minimumSystemVersion, "13.5"); + }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))), + ); + it.effect("stages the verified Sigma Runtime bundle as an opaque resource", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -531,7 +601,10 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { const runtimePath = path.join(tempDir, "runtime"); const stageAppDir = path.join(tempDir, "app"); const requiredFiles = [ - "bin/sigma.cmd", + "bin/sigma", + "bin/node", + "bin/sigma-exec", + "node_modules/native/addon.node", "LICENSE", "integrity-manifest.json", "package-metadata.json", @@ -556,12 +629,17 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { integrityManifest, ); - const exactCopyPaths = yield* stageBundledSigmaRuntime({ + const stagedPaths = yield* stageBundledSigmaRuntime({ stageAppDir, - platform: "win", + platform: "mac", runtimePath, }); - assert.deepStrictEqual(exactCopyPaths, ["node_modules/tr46/lib/.gitkeep"]); + assert.deepStrictEqual(stagedPaths.exactCopyPaths, ["node_modules/tr46/lib/.gitkeep"]); + assert.deepStrictEqual(stagedPaths.signingBinaryPaths, [ + "bin/node", + "bin/sigma-exec", + "node_modules/native/addon.node", + ]); for (const relativePath of requiredFiles) { assert.isTrue( diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 82f1db378..b434c8bf9 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -3,7 +3,7 @@ import * as NodeModule from "node:module"; import { fromYaml } from "@t3tools/shared/schemaYaml"; -import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; +import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import rootPackageJson from "../package.json" with { type: "json" }; @@ -37,7 +37,16 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const LINUX_ICON_SIZES = [16, 22, 24, 32, 48, 64, 128, 256, 512] as const; const DESKTOP_APP_ID = "io.github.hututuqqq.sigmacode"; +export const MACOS_MINIMUM_SYSTEM_VERSION = "13.5"; const APPLE_TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u; +export const MAC_SIGNING_CREDENTIAL_NAMES = [ + "CSC_LINK", + "CSC_KEY_PASSWORD", + "APPLE_API_KEY", + "APPLE_API_KEY_ID", + "APPLE_API_ISSUER", + "APPLE_TEAM_ID", +] as const; const BuildPlatform = Schema.Literals(["mac", "linux", "win"]); const BuildArch = Schema.Literals(["arm64", "x64", "universal"]); @@ -292,6 +301,18 @@ export class UnsupportedDesktopBuildPlatformError extends Schema.TaggedErrorClas } } +export class NativeMacArm64BuildRequiredError extends Schema.TaggedErrorClass()( + "NativeMacArm64BuildRequiredError", + { + hostPlatform: Schema.String, + hostArch: Schema.String, + }, +) { + override get message(): string { + return `macOS ARM64 desktop artifacts require a native darwin/arm64 host, got ${this.hostPlatform}/${this.hostArch}.`; + } +} + const dependencyResolutionDescriptions = { "server-production": "production dependencies", "workspace-overrides": "overrides", @@ -656,7 +677,7 @@ export class InvalidAppleTeamIdError extends Schema.TaggedErrorClass()( + "IncompleteMacSigningCredentialsError", + { + configuredNames: Schema.Array(Schema.String), + missingNames: Schema.Array(Schema.String), + }, +) { + override get message(): string { + return `macOS signing credentials are partially configured; missing: ${this.missingNames.join(", ")}.`; + } +} + +export type MacSigningCredentialState = + | { readonly mode: "unsigned" } + | { readonly mode: "signed"; readonly teamId: string }; + +export function resolveMacSigningCredentialState( + env: Readonly>, +): MacSigningCredentialState { + const configuredNames = MAC_SIGNING_CREDENTIAL_NAMES.filter( + (name) => (env[name] ?? "").trim().length > 0, + ); + if (configuredNames.length === 0) return { mode: "unsigned" }; + const missingNames = MAC_SIGNING_CREDENTIAL_NAMES.filter( + (name) => !configuredNames.includes(name), + ); + if (missingNames.length > 0) { + throw new IncompleteMacSigningCredentialsError({ + configuredNames: [...configuredNames], + missingNames: [...missingNames], + }); + } + const teamId = env.APPLE_TEAM_ID!.trim().toUpperCase(); + if (!APPLE_TEAM_ID_PATTERN.test(teamId)) { + throw new InvalidAppleTeamIdError({ teamId }); + } + return { mode: "signed", teamId }; +} + export class InvalidMacPasskeyPublishableKeyError extends Schema.TaggedErrorClass()( "InvalidMacPasskeyPublishableKeyError", { @@ -753,7 +813,7 @@ function normalizePasskeyRpDomain(value: string): string { export function resolveMacPasskeySigningConfiguration( env: Readonly>, ): MacPasskeySigningConfiguration { - const teamId = env.SIGMACODE_APPLE_TEAM_ID?.trim().toUpperCase() ?? ""; + const teamId = (env.APPLE_TEAM_ID ?? env.SIGMACODE_APPLE_TEAM_ID)?.trim().toUpperCase() ?? ""; if (!APPLE_TEAM_ID_PATTERN.test(teamId)) { throw new InvalidAppleTeamIdError({ teamId }); } @@ -833,6 +893,26 @@ ${associatedDomains} `; } +export function renderMacSigningEntitlements( + _teamId: string, + passkeys?: MacPasskeySigningConfiguration, +): string { + if (passkeys) return renderMacPasskeyEntitlements(passkeys); + return ` + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + +`; +} + export function resolveFffNativeDependencies( platform: typeof BuildPlatform.Type, arch: typeof BuildArch.Type, @@ -1422,12 +1502,13 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( mockUpdates: boolean, mockUpdateServerPort: number | undefined, sigmaRuntimeExactCopyPaths: ReadonlyArray, - macPasskeySigning: + macSigning: | { readonly entitlementsPath: string; - readonly provisioningProfilePath: string; + readonly provisioningProfilePath?: string; } | undefined, + sigmaRuntimeSigningBinaryPaths: ReadonlyArray = [], ) { const buildConfig: Record = { appId: DESKTOP_APP_ID, @@ -1486,15 +1567,27 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( } if (platform === "mac") { + const runtimeBinaries = sigmaRuntimeSigningBinaryPaths.map( + (relativePath) => `Contents/Resources/sigma-runtime/${relativePath}`, + ); buildConfig.mac = { target: target === "dmg" ? [target, "zip"] : [target], executableName: "sigma-code", icon: "icon.icns", category: "public.app-category.developer-tools", - ...(macPasskeySigning + minimumSystemVersion: MACOS_MINIMUM_SYSTEM_VERSION, + hardenedRuntime: signed, + notarize: signed, + gatekeeperAssess: false, + ...(signed ? {} : { identity: null }), + ...(signed ? { binaries: runtimeBinaries } : {}), + ...(macSigning ? { - entitlements: macPasskeySigning.entitlementsPath, - provisioningProfile: macPasskeySigning.provisioningProfilePath, + entitlements: macSigning.entitlementsPath, + entitlementsInherit: macSigning.entitlementsPath, + ...(macSigning.provisioningProfilePath + ? { provisioningProfile: macSigning.provisioningProfilePath } + : {}), } : {}), }; @@ -1668,14 +1761,24 @@ export const stageBundledSigmaRuntime = Effect.fn("stageBundledSigmaRuntime")(fu path.join(input.runtimePath, "integrity-manifest.json"), ); const decodedIntegrityManifest = yield* decodeBundledRuntimeIntegrityManifest(integrityManifest); - const exactCopyPaths = decodedIntegrityManifest.entries - .map((entry) => entry.path.replaceAll("\\", "/")) + const manifestPaths = decodedIntegrityManifest.entries.map((entry) => + entry.path.replaceAll("\\", "/"), + ); + const exactCopyPaths = manifestPaths .filter((relativePath) => path.basename(relativePath) === ".gitkeep") .sort(); + const signingBinaryPaths = manifestPaths + .filter( + (relativePath) => + relativePath === "bin/node" || + relativePath === "bin/sigma-exec" || + relativePath.endsWith(".node"), + ) + .sort(); yield* fs.copy(input.runtimePath, path.join(input.stageAppDir, "sigma-runtime")); yield* Effect.log(`[desktop-artifact] Staged bundled Sigma Runtime from ${input.runtimePath}.`); - return exactCopyPaths; + return { exactCopyPaths, signingBinaryPaths }; }); const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( @@ -1685,11 +1788,27 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; const hostPlatform = yield* HostProcessPlatform; + const hostArchitecture = yield* HostProcessArchitecture; const workspaceConfig = yield* readWorkspaceConfig(); const workspaceCatalog = workspaceConfig.catalog ?? {}; const workspaceOverrides = workspaceConfig.overrides ?? {}; const workspacePatchedDependencies = workspaceConfig.patchedDependencies ?? {}; const workspaceAllowBuilds = workspaceConfig.allowBuilds ?? {}; + if ( + options.platform === "mac" && + options.arch === "arm64" && + (hostPlatform !== "darwin" || hostArchitecture !== "arm64") + ) { + return yield* new NativeMacArm64BuildRequiredError({ + hostPlatform, + hostArch: hostArchitecture, + }); + } + const repoEnv = loadRepoEnv({ repoRoot }); + const macSigningCredentialState = + options.platform === "mac" ? resolveMacSigningCredentialState(repoEnv) : undefined; + const signed = + options.platform === "mac" ? macSigningCredentialState?.mode === "signed" : options.signed; const platformConfig = PLATFORM_CONFIG[options.platform]; if (!platformConfig) { @@ -1810,7 +1929,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( path.join(legalDir, legalResource.targetFileName), ); } - const sigmaRuntimeExactCopyPaths = yield* stageBundledSigmaRuntime({ + const sigmaRuntimePaths = yield* stageBundledSigmaRuntime({ stageAppDir, platform: options.platform, runtimePath: options.sigmaRuntime, @@ -1830,10 +1949,15 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( // electron-builder is filtering out stageResourcesDir directory in the AppImage for production yield* fs.copy(stageResourcesDir, path.join(stageAppDir, "apps/desktop/prod-resources")); + const hasMacPasskeyConfiguration = [ + "SIGMACODE_MACOS_PROVISIONING_PROFILE", + "SIGMACODE_CLERK_PASSKEY_RP_DOMAINS", + "SIGMACODE_CLERK_PUBLISHABLE_KEY", + ].some((name) => (repoEnv[name] ?? "").trim().length > 0); const configuredMacPasskeySigning = - options.platform === "mac" && options.signed + options.platform === "mac" && signed && hasMacPasskeyConfiguration ? yield* Effect.try({ - try: () => resolveMacPasskeySigningConfiguration(loadRepoEnv({ repoRoot })), + try: () => resolveMacPasskeySigningConfiguration(repoEnv), catch: MacPasskeySigningConfigurationResolutionError.fromCause, }) : undefined; @@ -1846,16 +1970,19 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( ), } : undefined; - const macEntitlementsPath = macPasskeySigning - ? path.join(stageAppDir, "entitlements.mac.plist") - : undefined; - if (macPasskeySigning && macEntitlementsPath) { + const macEntitlementsPath = signed ? path.join(stageAppDir, "entitlements.mac.plist") : undefined; + if (macPasskeySigning) { if (!(yield* fs.exists(macPasskeySigning.provisioningProfilePath))) { return yield* new MacProvisioningProfileNotFoundError({ provisioningProfilePath: macPasskeySigning.provisioningProfilePath, }); } - yield* fs.writeFileString(macEntitlementsPath, renderMacPasskeyEntitlements(macPasskeySigning)); + } + if (macEntitlementsPath && macSigningCredentialState?.mode === "signed") { + yield* fs.writeFileString( + macEntitlementsPath, + renderMacSigningEntitlements(macSigningCredentialState.teamId, macPasskeySigning), + ); } const stageDependencies = { @@ -1896,16 +2023,19 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( options.platform, options.target, appVersion, - options.signed, + signed, options.mockUpdates, options.mockUpdateServerPort, - sigmaRuntimeExactCopyPaths, - macPasskeySigning && macEntitlementsPath + sigmaRuntimePaths.exactCopyPaths, + macEntitlementsPath ? { entitlementsPath: macEntitlementsPath, - provisioningProfilePath: macPasskeySigning.provisioningProfilePath, + ...(macPasskeySigning + ? { provisioningProfilePath: macPasskeySigning.provisioningProfilePath } + : {}), } : undefined, + sigmaRuntimePaths.signingBinaryPaths, ), dependencies: stageDependencies, devDependencies: { @@ -1965,7 +2095,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( delete buildEnv[key]; } } - if (!options.signed) { + if (!signed) { buildEnv.CSC_IDENTITY_AUTO_DISCOVERY = "false"; delete buildEnv.CSC_LINK; delete buildEnv.CSC_KEY_PASSWORD; @@ -2092,7 +2222,7 @@ const buildDesktopArtifactCli = Command.make("build-desktop-artifact", { ), signed: Flag.boolean("signed").pipe( Flag.withDescription( - "Enable signing/notarization discovery; Windows uses Azure Trusted Signing (env: SIGMACODE_DESKTOP_SIGNED).", + "Enable Windows Azure Trusted Signing. macOS signing is enabled only by the complete Apple credential set (env: SIGMACODE_DESKTOP_SIGNED).", ), Flag.optional, ), diff --git a/scripts/desktop-release-evidence.test.ts b/scripts/desktop-release-evidence.test.ts new file mode 100644 index 000000000..e253a5113 --- /dev/null +++ b/scripts/desktop-release-evidence.test.ts @@ -0,0 +1,71 @@ +// @effect-diagnostics nodeBuiltinImport:off +import { assert, it } from "@effect/vitest"; +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + +import { writeDesktopReleaseEvidence } from "./desktop-release-evidence.ts"; + +it("writes checksum, runtime provenance, and unsigned preview status", async () => { + const root = await NodeFSP.mkdtemp( + NodePath.join(NodeOS.tmpdir(), "sigma-desktop-evidence-test-"), + ); + const artifactPath = NodePath.join(root, "Sigma-Code-0.1.6-arm64.dmg"); + const runtimeProvenancePath = NodePath.join(root, "agent-cli-darwin-arm64.provenance.json"); + await NodeFSP.writeFile(artifactPath, "dmg fixture", "utf8"); + await NodeFSP.writeFile( + runtimeProvenancePath, + '{"predicate":{"buildDefinition":{"externalParameters":{"version":"0.1.6","targetPlatform":"darwin","targetArch":"arm64"}}}}\n', + "utf8", + ); + + const result = await writeDesktopReleaseEvidence({ + artifactPath, + runtimeProvenancePath, + platform: "darwin", + arch: "arm64", + version: "0.1.6", + signingMode: "unsigned", + codesign: "not-signed", + gatekeeper: "not-applicable", + stapler: "not-applicable", + }); + const provenance = JSON.parse(await NodeFSP.readFile(result.provenancePath, "utf8")); + const signing = JSON.parse(await NodeFSP.readFile(result.signingPath, "utf8")); + assert.equal(provenance.product.minimumSystemVersion, "13.5"); + assert.equal(provenance.runtime.target, "darwin-arm64"); + assert.equal(signing.mode, "unsigned"); + assert.isFalse(signing.hardenedRuntime); +}); + +it("rejects signed evidence unless every notarization gate passed", async () => { + const root = await NodeFSP.mkdtemp( + NodePath.join(NodeOS.tmpdir(), "sigma-desktop-evidence-signed-test-"), + ); + const artifactPath = NodePath.join(root, "Sigma-Code-0.1.6-arm64.dmg"); + const runtimeProvenancePath = NodePath.join(root, "runtime.json"); + await NodeFSP.writeFile(artifactPath, "dmg fixture", "utf8"); + await NodeFSP.writeFile( + runtimeProvenancePath, + '{"predicate":{"buildDefinition":{"externalParameters":{"version":"0.1.6","targetPlatform":"darwin","targetArch":"arm64"}}}}\n', + "utf8", + ); + let failure: unknown; + try { + await writeDesktopReleaseEvidence({ + artifactPath, + runtimeProvenancePath, + platform: "darwin", + arch: "arm64", + version: "0.1.6", + signingMode: "signed", + codesign: "pass", + gatekeeper: "pass", + stapler: "not-applicable", + }); + } catch (error) { + failure = error; + } + assert.instanceOf(failure, Error); + assert.match((failure as Error).message, /stapler=pass/u); +}); diff --git a/scripts/desktop-release-evidence.ts b/scripts/desktop-release-evidence.ts new file mode 100644 index 000000000..b54bfcfad --- /dev/null +++ b/scripts/desktop-release-evidence.ts @@ -0,0 +1,187 @@ +#!/usr/bin/env node +// @effect-diagnostics nodeBuiltinImport:off + +import * as NodeCrypto from "node:crypto"; +import * as NodeFSP from "node:fs/promises"; +import * as NodePath from "node:path"; + +const APP_ID = "io.github.hututuqqq.sigmacode"; +const MACOS_MINIMUM_SYSTEM_VERSION = "13.5"; + +type Platform = "darwin" | "win32"; +type Arch = "arm64" | "x64"; +type SigningMode = "signed" | "unsigned"; +type Verification = "pass" | "not-signed" | "not-applicable"; + +export interface DesktopReleaseEvidenceOptions { + readonly artifactPath: string; + readonly runtimeProvenancePath: string; + readonly platform: Platform; + readonly arch: Arch; + readonly version: string; + readonly signingMode: SigningMode; + readonly codesign: Verification; + readonly gatekeeper: Verification; + readonly stapler: Verification; +} + +async function sha256(filePath: string): Promise { + return NodeCrypto.createHash("sha256") + .update(await NodeFSP.readFile(filePath)) + .digest("hex"); +} + +function validateOptions(options: DesktopReleaseEvidenceOptions): void { + if (!(["darwin", "win32"] as const).includes(options.platform)) { + throw new Error(`Unsupported desktop evidence platform: ${options.platform}`); + } + if (!(["arm64", "x64"] as const).includes(options.arch)) { + throw new Error(`Unsupported desktop evidence architecture: ${options.arch}`); + } + if (!(["signed", "unsigned"] as const).includes(options.signingMode)) { + throw new Error(`Unsupported desktop signing mode: ${options.signingMode}`); + } + if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(options.version)) { + throw new Error(`Invalid desktop release version: ${options.version}`); + } + const extension = options.platform === "darwin" ? "dmg" : "exe"; + const expectedName = `Sigma-Code-${options.version}-${options.arch}.${extension}`; + if (NodePath.basename(options.artifactPath) !== expectedName) { + throw new Error(`Desktop artifact must be named ${expectedName}.`); + } + if (options.platform === "darwin" && options.arch !== "arm64") { + throw new Error("This release supports only macOS ARM64."); + } + const expected = + options.signingMode === "signed" + ? { codesign: "pass", gatekeeper: "pass", stapler: "pass" } + : { codesign: "not-signed", gatekeeper: "not-applicable", stapler: "not-applicable" }; + for (const key of ["codesign", "gatekeeper", "stapler"] as const) { + if (options[key] !== expected[key]) { + throw new Error(`${options.signingMode} evidence requires ${key}=${expected[key]}.`); + } + } +} + +export async function writeDesktopReleaseEvidence(options: DesktopReleaseEvidenceOptions): Promise<{ + readonly checksumPath: string; + readonly provenancePath: string; + readonly signingPath: string; +}> { + validateOptions(options); + const runtimeProvenanceRaw = await NodeFSP.readFile(options.runtimeProvenancePath, "utf8"); + const runtimeProvenance = JSON.parse(runtimeProvenanceRaw) as Record; + const runtimeParameters = ( + runtimeProvenance.predicate as + | { readonly buildDefinition?: { readonly externalParameters?: unknown } } + | undefined + )?.buildDefinition?.externalParameters as + | { + readonly version?: unknown; + readonly targetPlatform?: unknown; + readonly targetArch?: unknown; + } + | undefined; + if ( + runtimeParameters?.version !== options.version || + runtimeParameters.targetPlatform !== options.platform || + runtimeParameters.targetArch !== options.arch + ) { + throw new Error( + `Runtime provenance does not match ${options.version}/${options.platform}/${options.arch}.`, + ); + } + const artifactSha256 = await sha256(options.artifactPath); + const artifactName = NodePath.basename(options.artifactPath); + const checksumPath = `${options.artifactPath}.sha256`; + const provenancePath = `${options.artifactPath}.desktop-provenance.json`; + const signingPath = `${options.artifactPath}.signing.json`; + await NodeFSP.writeFile(checksumPath, `${artifactSha256} ${artifactName}\n`, "utf8"); + + const buildSource = { + repository: process.env.GITHUB_REPOSITORY ?? null, + runtimeCommit: process.env.GITHUB_SHA ?? null, + sigmaCodeCommit: process.env.SIGMACODE_SOURCE_COMMIT ?? null, + ref: process.env.GITHUB_REF ?? null, + workflowRunId: process.env.GITHUB_RUN_ID ?? null, + workflowRunAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null, + }; + const provenance = { + schemaVersion: 1, + kind: "sigma-code.desktop-provenance", + artifact: { name: artifactName, sha256: artifactSha256 }, + product: { + appId: APP_ID, + version: options.version, + platform: options.platform, + arch: options.arch, + minimumSystemVersion: options.platform === "darwin" ? MACOS_MINIMUM_SYSTEM_VERSION : null, + }, + runtime: { + provenanceFile: NodePath.basename(options.runtimeProvenancePath), + provenanceSha256: await sha256(options.runtimeProvenancePath), + target: `${options.platform}-${options.arch}`, + }, + buildSource, + }; + const signing = { + schemaVersion: 1, + kind: "sigma-code.desktop-signing-status", + artifact: { name: artifactName, sha256: artifactSha256 }, + mode: options.signingMode, + hardenedRuntime: options.platform === "darwin" && options.signingMode === "signed", + notarized: options.platform === "darwin" && options.signingMode === "signed", + verification: { + codesign: options.codesign, + gatekeeper: options.gatekeeper, + stapler: options.stapler, + }, + }; + await Promise.all([ + NodeFSP.writeFile(provenancePath, `${JSON.stringify(provenance, null, 2)}\n`, "utf8"), + NodeFSP.writeFile(signingPath, `${JSON.stringify(signing, null, 2)}\n`, "utf8"), + ]); + return { checksumPath, provenancePath, signingPath }; +} + +function parseArgs(argv: readonly string[]): DesktopReleaseEvidenceOptions { + const values = new Map(); + for (let index = 0; index < argv.length; index += 2) { + const name = argv[index]; + const value = argv[index + 1]; + if (!name?.startsWith("--") || !value) + throw new Error(`Invalid argument: ${name ?? ""}`); + values.set(name.slice(2), value); + } + const required = [ + "artifact", + "runtime-provenance", + "platform", + "arch", + "version", + "signing-mode", + "codesign", + "gatekeeper", + "stapler", + ]; + const missing = required.filter((name) => !values.has(name)); + if (missing.length > 0) throw new Error(`Missing arguments: ${missing.join(", ")}`); + return { + artifactPath: NodePath.resolve(values.get("artifact")!), + runtimeProvenancePath: NodePath.resolve(values.get("runtime-provenance")!), + platform: values.get("platform") as Platform, + arch: values.get("arch") as Arch, + version: values.get("version")!, + signingMode: values.get("signing-mode") as SigningMode, + codesign: values.get("codesign") as Verification, + gatekeeper: values.get("gatekeeper") as Verification, + stapler: values.get("stapler") as Verification, + }; +} + +if (import.meta.main) { + writeDesktopReleaseEvidence(parseArgs(process.argv.slice(2))).catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + }); +}