mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[aidan] fix/spotify-drm: VMP-sign packaged builds so Widevine playback works
This commit is contained in:
@@ -13,6 +13,8 @@ name: Release (macOS)
|
||||
# APPLE_TEAM_ID Apple Developer Team ID
|
||||
# CSC_LINK base64-encoded Developer ID Application .p12
|
||||
# CSC_KEY_PASSWORD password for that .p12
|
||||
# EVS_ACCOUNT_NAME castlabs EVS account name (Widevine VMP signing; free signup)
|
||||
# EVS_PASSWD password for that EVS account
|
||||
# GOOGLE_OAUTH_CLIENT_ID shipped in production .env (Google OAuth)
|
||||
# GOOGLE_OAUTH_CLIENT_SECRET shipped in production .env (Google OAuth)
|
||||
#
|
||||
@@ -70,6 +72,8 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
EVS_ACCOUNT_NAME: ${{ secrets.EVS_ACCOUNT_NAME }}
|
||||
EVS_PASSWD: ${{ secrets.EVS_PASSWD }}
|
||||
PUBLISH_INPUT: ${{ github.event.inputs.publish }}
|
||||
|
||||
steps:
|
||||
@@ -87,6 +91,14 @@ jobs:
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
# Widevine VMP signing tool. The afterPack hook invokes `castlabs_evs.vmp
|
||||
# sign-pkg` with the EVS_* secrets; without this the build aborts (publish
|
||||
# path sets VMP_REQUIRE_SIGN=1) rather than ship a DMG with dead Spotify DRM.
|
||||
- name: Install castlabs-evs (Widevine VMP signing)
|
||||
if: ${{ env.APPLE_ID != '' }}
|
||||
shell: bash
|
||||
run: python3 -m pip install --upgrade castlabs-evs
|
||||
|
||||
- name: Build app
|
||||
# Skip (green) when Apple signing secrets aren't in CI: Mac ships via local
|
||||
# publish.sh, so a secret-less CI run should no-op, not fail red.
|
||||
|
||||
@@ -17,6 +17,8 @@ name: Release (Windows)
|
||||
# AZURE_SIGNING_ENDPOINT e.g. https://wus2.codesigning.azure.net/
|
||||
# AZURE_SIGNING_ACCOUNT mist-code-signing
|
||||
# AZURE_SIGNING_CERT_PROFILE Mist-Windows-Signing
|
||||
# EVS_ACCOUNT_NAME castlabs EVS account name (Widevine VMP signing; free signup)
|
||||
# EVS_PASSWD password for that EVS account
|
||||
# GOOGLE_OAUTH_CLIENT_ID shipped in production .env (Google OAuth)
|
||||
# GOOGLE_OAUTH_CLIENT_SECRET shipped in production .env (Google OAuth)
|
||||
# v1.0.29 cloud-proxied the OAuth flow itself,
|
||||
@@ -77,6 +79,8 @@ jobs:
|
||||
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
|
||||
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
|
||||
AZURE_SIGNING_CERT_PROFILE: ${{ secrets.AZURE_SIGNING_CERT_PROFILE }}
|
||||
EVS_ACCOUNT_NAME: ${{ secrets.EVS_ACCOUNT_NAME }}
|
||||
EVS_PASSWD: ${{ secrets.EVS_PASSWD }}
|
||||
PUBLISH_INPUT: ${{ github.event.inputs.publish }}
|
||||
|
||||
steps:
|
||||
@@ -96,6 +100,14 @@ jobs:
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
# Widevine VMP signing tool. The afterPack hook invokes `castlabs_evs.vmp
|
||||
# sign-pkg` with the EVS_* secrets; the -Sign path sets VMP_REQUIRE_SIGN=1 so
|
||||
# a missing/failed signature aborts the build rather than ship an installer
|
||||
# whose Spotify/Netflix audio is silently dead.
|
||||
- name: Install castlabs-evs (Widevine VMP signing)
|
||||
shell: pwsh
|
||||
run: python -m pip install --upgrade castlabs-evs
|
||||
|
||||
# The signing hook calls `signtool.exe` directly. signtool ships in the
|
||||
# Windows 10 SDK, preinstalled on windows-latest runners — we just need
|
||||
# the dlib for Azure Trusted Signing, pulled via NuGet.
|
||||
|
||||
@@ -37,6 +37,10 @@ platform. See `RELEASE_RUNBOOK.md` for the how; this is the gate.
|
||||
- [ ] macOS Intel (x64), macOS 12+: same.
|
||||
- [ ] Auto-update: previous stable installed → this release detected, downloads,
|
||||
installs on quit, relaunches on the new version. Verify on both platforms.
|
||||
- [ ] Widevine DRM: in a Browser card open a Spotify playlist (or any DRM title)
|
||||
and confirm a track plays PAST the ~10s encrypted boundary and auto-advances,
|
||||
with no `[drm-diag] License response 500` in the logs. A signed-but-not-VMP
|
||||
build boots fine and only fails here, so this box catches it. Both platforms.
|
||||
|
||||
## Promote
|
||||
- [ ] All boxes above ticked.
|
||||
|
||||
@@ -11,8 +11,54 @@
|
||||
// this rescue.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
exports.default = async function afterPack(context) {
|
||||
// Widevine VMP signing of the PACKAGED app. Has to happen here in afterPack, not
|
||||
// at npm-install time on node_modules: the OS code-sign electron-builder runs
|
||||
// right after this seals the VMP signature into the bundle, so signing the source
|
||||
// electron earlier gets stripped/relocated and Spotify's license server then 500s.
|
||||
// Lenient by default (a dev `npm run dist` without an EVS account still produces an
|
||||
// app, just with limited DRM); VMP_REQUIRE_SIGN=1 (set by the signed release paths)
|
||||
// turns a missing/failed signature into a hard build failure so prod never ships
|
||||
// an unsigned-for-DRM client silently.
|
||||
function signVmp(context) {
|
||||
const { appOutDir, electronPlatformName, packager } = context;
|
||||
const required = process.env.VMP_REQUIRE_SIGN === '1';
|
||||
const acct = process.env.EVS_ACCOUNT_NAME;
|
||||
const pass = process.env.EVS_PASSWD;
|
||||
|
||||
if (!acct || !pass) {
|
||||
if (required) {
|
||||
throw new Error('[afterPack] VMP_REQUIRE_SIGN=1 but EVS_ACCOUNT_NAME/EVS_PASSWD are absent — refusing to ship a release whose Widevine DRM (Spotify/Netflix) would be dead');
|
||||
}
|
||||
console.warn('[afterPack] EVS creds absent — skipping VMP signing; DRM playback will be limited (dev build)');
|
||||
return;
|
||||
}
|
||||
|
||||
// mac: sign the .app bundle; win: sign the unpacked dir holding the exe + framework.
|
||||
const target = electronPlatformName === 'darwin'
|
||||
? path.join(appOutDir, `${packager.appInfo.productFilename}.app`)
|
||||
: appOutDir;
|
||||
const py = process.platform === 'win32' ? 'python' : 'python3';
|
||||
|
||||
try {
|
||||
console.log(`[afterPack] VMP-signing ${target}`);
|
||||
// Creds go via the environment (EVS reads EVS_ACCOUNT_NAME/EVS_PASSWD), never on
|
||||
// the argv — a password in a command line is readable by any `ps` on the host.
|
||||
execFileSync(py, ['-m', 'castlabs_evs.vmp', 'sign-pkg', target, '--no-ask'], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, EVS_ACCOUNT_NAME: acct, EVS_PASSWD: pass },
|
||||
});
|
||||
console.log('[afterPack] VMP signing successful — full DRM playback enabled');
|
||||
} catch (err) {
|
||||
if (required) {
|
||||
throw new Error(`[afterPack] VMP signing failed (release would have broken DRM): ${err && err.message}`);
|
||||
}
|
||||
console.warn(`[afterPack] VMP signing failed (non-fatal in dev): ${err && err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stageRouterNodeModules(context) {
|
||||
const { appOutDir, electronPlatformName, packager } = context;
|
||||
const src = path.join(__dirname, '..', 'build-staging', 'router', 'node_modules');
|
||||
if (!fs.existsSync(src)) return; // dev/no-router build; nothing to do
|
||||
@@ -34,4 +80,11 @@ exports.default = async function afterPack(context) {
|
||||
throw new Error(`afterPack: 9Router node_modules/next missing in ${routerDir} after copy`);
|
||||
}
|
||||
console.log(`[afterPack] staged 9Router node_modules into ${routerDir}`);
|
||||
}
|
||||
|
||||
exports.default = async function afterPack(context) {
|
||||
stageRouterNodeModules(context);
|
||||
// VMP signing runs last and unconditionally, after every file is staged, so the
|
||||
// OS code-sign that electron-builder runs next seals the VMP signature too.
|
||||
signVmp(context);
|
||||
};
|
||||
|
||||
@@ -42,6 +42,14 @@ if ! python3 -c "import castlabs_evs" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# When creds are in the env (EVS reads EVS_ACCOUNT_NAME/EVS_PASSWD itself), go
|
||||
# non-interactive so CI / non-TTY runs don't hang on a prompt. Creds stay in the
|
||||
# environment, never on the argv where any `ps` on the host could read them.
|
||||
EVS_AUTH=()
|
||||
if [ -n "${EVS_ACCOUNT_NAME:-}" ] && [ -n "${EVS_PASSWD:-}" ]; then
|
||||
EVS_AUTH=(--no-ask)
|
||||
fi
|
||||
|
||||
VERIFY_OUTPUT=$(python3 -m castlabs_evs.vmp verify-pkg "$ELECTRON_DIR" 2>&1)
|
||||
if echo "$VERIFY_OUTPUT" | grep -q "Signature is valid" && ! echo "$VERIFY_OUTPUT" | grep -q "development only"; then
|
||||
echo "[vmp] Electron already has a valid production VMP signature"
|
||||
@@ -49,7 +57,7 @@ if echo "$VERIFY_OUTPUT" | grep -q "Signature is valid" && ! echo "$VERIFY_OUTPU
|
||||
fi
|
||||
|
||||
echo "[vmp] Signing Electron with production VMP certificate..."
|
||||
if python3 -m castlabs_evs.vmp sign-pkg "$ELECTRON_DIR" 2>&1; then
|
||||
if python3 -m castlabs_evs.vmp sign-pkg "$ELECTRON_DIR" "${EVS_AUTH[@]}" 2>&1; then
|
||||
echo "[vmp] VMP signing successful — full DRM playback enabled"
|
||||
# Re-fix symlinks in case signing modified the bundle
|
||||
fix_framework_symlinks
|
||||
|
||||
@@ -70,6 +70,10 @@ if ($Sign) {
|
||||
Write-Host "Copy .env.windows.example to .env.windows and fill in values."
|
||||
exit 1
|
||||
}
|
||||
# A signed build is one users actually run, so its Widevine VMP signature is
|
||||
# mandatory: the afterPack hook hard-fails on a missing/failed signature rather
|
||||
# than ship an installer whose Spotify/Netflix audio is silently dead.
|
||||
$env:VMP_REQUIRE_SIGN = '1'
|
||||
}
|
||||
|
||||
# --- Step 0: Bundled uv + uvx for Windows ---
|
||||
|
||||
@@ -66,6 +66,10 @@ if $SIGN_MODE; then
|
||||
echo "See script header for details."
|
||||
exit 1
|
||||
fi
|
||||
# A signed build is a build users actually run, so its Widevine VMP signature
|
||||
# is mandatory: the afterPack hook hard-fails on a missing/failed signature
|
||||
# instead of shipping a DMG whose Spotify/Netflix audio is silently dead.
|
||||
export VMP_REQUIRE_SIGN=1
|
||||
fi
|
||||
|
||||
# Step 0: Ensure bundled uv + uvx binaries exist.
|
||||
|
||||
Reference in New Issue
Block a user