Files
iptvnator/tools/dependencies/app-builder-lib-keychain-password.test.mjs
T
4grayandClaude Fable 5.1 52b33fe5a3 fix(release): pass the keychain password to set-key-partition-list on macOS
`Build on macos arm64` started failing on master with

    security set-key-partition-list -S apple-tool:,apple: -s -k *** <tmp>.keychain
    SecKeychainUnlock: The user name or passphrase you entered is not correct.

Every failure ran the `macos-26-arm64` runner image 20260831.0337; the same
job on image 20260728.0273 kept passing, as did `macos-15-intel`. The image
exposed an electron-builder bug: `app-builder-lib` hands the certificate's
`.p12` import password to `set-key-partition-list -k`, which authenticates
against the temporary keychain and therefore needs the keychain's own
generated password. Older macOS builds accepted the wrong password once the
keychain was unlocked; the new one verifies it.

Upstream fixed this in electron-userland/electron-builder#10101 (master,
v27 alpha) and backported it in #10172 to release/v26 on 2026-09-03, but no
26.x containing it is published (26.16.0 predates the backport, #10167).
Apply the backport to the installed 26.15.7 as a pnpm patch, guarded by
`tools/dependencies/app-builder-lib-keychain-password.test.mjs`: it checks
that the patched version is the installed one, that the compiled source
passes `keychainPassword`, and — with `security` recorded — that
`createKeychain` unlocks the partition list with the password it gave
`create-keychain`, not the import password. The test fails 2/3 on the
unpatched code. CI runs it next to the Vite patch guard; CLAUDE.md and
AGENTS.md document when to retire the patch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 17:07:27 +02:00

180 lines
7.2 KiB
JavaScript

/**
* Guards `patches/app-builder-lib@<version>.patch`.
*
* electron-builder 26.15.x hands the certificate's `.p12` import password to
* `security set-key-partition-list -k`, which authenticates against the
* temporary keychain — so it needs the keychain's own generated password
* (upstream #10066, fixed on master in #10101 and backported to release/v26 in
* #10172, not yet in a published 26.x). macOS runner images since
* `macos-26-arm64` 20260831 verify that password, and `Build on macos arm64`
* failed with `SecKeychainUnlock: The user name or passphrase you entered is
* not correct`. The patch applies the backport to the compiled package.
*
* Two checks: the installed source carries the fix (a dependency bump that
* drops the patch must not silently reintroduce the bug), and the behavior
* holds when `createKeychain` runs against a recorded `security` — the
* partition-list call must use the password `create-keychain` was given, not
* the import password.
*
* Retire this test together with the patch once electron-builder resolves an
* `app-builder-lib` that contains #10172.
*/
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { after, before, describe, it } from 'node:test';
const require = createRequire(import.meta.url);
// app-builder-lib is a transitive dependency of electron-builder; pnpm's
// strict layout keeps it out of the root node_modules, so resolve it the way
// electron-builder itself does.
const electronBuilderDir = path.dirname(
require.resolve('electron-builder/package.json')
);
const appBuilderLibPackage = require.resolve('app-builder-lib/package.json', {
paths: [electronBuilderDir],
});
const appBuilderLibDir = path.dirname(appBuilderLibPackage);
const macCodeSignPath = path.join(
appBuilderLibDir,
'out/codeSign/macCodeSign.js'
);
const IMPORT_PASSWORD = 'certificate-import-password';
describe('app-builder-lib keychain password patch', () => {
it('targets the version the patch was written for', () => {
const { version } = JSON.parse(
readFileSync(appBuilderLibPackage, 'utf8')
);
const rootPackage = JSON.parse(
readFileSync(new URL('../../package.json', import.meta.url), 'utf8')
);
const patched = Object.keys(
rootPackage.pnpm?.patchedDependencies ?? {}
);
assert.ok(
patched.includes(`app-builder-lib@${version}`),
`installed app-builder-lib ${version} has no entry in pnpm.patchedDependencies (${patched.join(', ')}) — bump or retire the patch`
);
});
it('passes the keychain password, not the import password, to set-key-partition-list', () => {
const source = readFileSync(macCodeSignPath, 'utf8');
assert.match(
source,
/"set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile/
);
assert.match(
source,
/importCerts\(keychainFile, certPaths, cscPasswords, keychainPassword\)/
);
assert.doesNotMatch(
source,
/"set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile/
);
});
describe('createKeychain against a recorded security binary', () => {
const calls = [];
let cacheDir;
let tmpRoot;
let builderUtil;
let originalExec;
let macCodeSign;
before(() => {
cacheDir = mkdtempSync(
path.join(tmpdir(), 'app-builder-lib-cache-')
);
tmpRoot = mkdtempSync(path.join(tmpdir(), 'app-builder-lib-tmp-'));
// Keep createKeychain's only real filesystem side effects (the bundled
// root-certs keychain copy and the temp keychain path) out of the
// user's cache and temp directories.
process.env.ELECTRON_BUILDER_CACHE = cacheDir;
process.env.APP_BUILDER_TMP_DIR = tmpRoot;
// The compiled code reads `exec` off builder-util's util module at
// call time (`(0, builder_util_1.exec)(...)`), so replacing the
// export records every `/usr/bin/security` invocation without
// touching a real keychain.
builderUtil = require(
require.resolve('builder-util/out/util', {
paths: [appBuilderLibDir],
})
);
originalExec = builderUtil.exec;
builderUtil.exec = async (file, args) => {
calls.push({ file, args: [...(args ?? [])] });
return '';
};
macCodeSign = require(macCodeSignPath);
});
after(() => {
builderUtil.exec = originalExec;
delete process.env.ELECTRON_BUILDER_CACHE;
delete process.env.APP_BUILDER_TMP_DIR;
rmSync(cacheDir, { recursive: true, force: true });
rmSync(tmpRoot, { recursive: true, force: true });
});
it('unlocks the partition list with the generated keychain password', async () => {
const { TmpDir } = require(
require.resolve('builder-util', { paths: [appBuilderLibDir] })
);
const tmpDir = new TmpDir('keychain-password-test');
try {
await macCodeSign.createKeychain({
tmpDir,
// Base64 links are written to a temp file without inspection;
// only the path reaches the recorded `security import`.
cscLink: Buffer.from('not a real p12').toString('base64'),
cscKeyPassword: IMPORT_PASSWORD,
currentDir: tmpRoot,
});
} finally {
await tmpDir.cleanup();
}
const security = calls.filter(
(call) => call.file === '/usr/bin/security'
);
const argAfter = (args, flag) => args[args.indexOf(flag) + 1];
const created = security.find(
(call) => call.args[0] === 'create-keychain'
);
const imported = security.find((call) => call.args[0] === 'import');
const partition = security.find(
(call) => call.args[0] === 'set-key-partition-list'
);
assert.ok(created, 'create-keychain was not invoked');
assert.ok(imported, 'security import was not invoked');
assert.ok(partition, 'set-key-partition-list was not invoked');
const keychainPassword = argAfter(created.args, '-p');
assert.ok(keychainPassword, 'create-keychain carried no password');
assert.notEqual(keychainPassword, IMPORT_PASSWORD);
assert.equal(argAfter(imported.args, '-P'), IMPORT_PASSWORD);
assert.equal(
argAfter(partition.args, '-k'),
keychainPassword,
'set-key-partition-list must authenticate with the keychain password'
);
assert.equal(
partition.args.at(-1),
created.args.at(-1),
'same keychain file'
);
});
});
});