Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c533b8c37 | |||
| 795e0a75ca | |||
| 72b907947b | |||
| e3489f3b06 | |||
| 403776cc1c | |||
| c23a5d6a22 | |||
| fa26c84bd2 | |||
| c5c9bb5527 | |||
| feb1f3db65 | |||
| de3dc87920 | |||
| a0aa9384d7 | |||
| a938d2ed5f | |||
| 4863ade45b | |||
| 1837ceff58 | |||
| 3e77127687 | |||
| 88bd0c5942 | |||
| 6b55c18610 | |||
| 1d26646a96 | |||
| c68dddfc01 | |||
| 44db5db838 | |||
| ab59476d10 | |||
| b26ebdda02 | |||
| f2fa10c178 | |||
| f11a5c84d9 | |||
| 400c9be3a7 | |||
| 5ea9b24912 | |||
| aed628a49b | |||
| 0bbc125806 | |||
| 878d1dc701 | |||
| 0c187a35fa | |||
| 1dc10f569d | |||
| fd4208d06f | |||
| 0aceb1fd74 | |||
| 141fea674f | |||
| 6b980f884a | |||
| 0841eb0f79 | |||
| f4844aa8f1 | |||
| b03e30d0dd | |||
| 0aff10fe0d | |||
| 76f2d9f22a | |||
| b73f23c1eb | |||
| e7b18758b2 | |||
| 84d5daebab | |||
| 3a866accd9 | |||
| a17fa618cd | |||
| 0d03e1656a | |||
| 1c1527a224 | |||
| 360988de5e | |||
| b8c5c4123a | |||
| 8c8327df92 | |||
| 593c60c782 | |||
| 45ed1d7f36 | |||
| 5ee84c97e8 | |||
| 9cac75e85c | |||
| c923f887a0 | |||
| 628ff8828b | |||
| 7c721d4e5c | |||
| d1a6c957f7 | |||
| 4654a812a0 |
@@ -40,6 +40,8 @@ jobs:
|
|||||||
web:
|
web:
|
||||||
name: Shared client and Web
|
name: Shared client and Web
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
@@ -104,6 +106,8 @@ jobs:
|
|||||||
desktop:
|
desktop:
|
||||||
name: macOS Desktop
|
name: macOS Desktop
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
env:
|
||||||
|
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
@@ -14,9 +14,40 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: desktop-release-candidate-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
release-policy:
|
||||||
|
name: Resolve desktop release signing policy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
platform_signing_mode: ${{ steps.policy.outputs.platform_signing_mode }}
|
||||||
|
macos_signing: ${{ steps.policy.outputs.macos_signing }}
|
||||||
|
windows_signing: ${{ steps.policy.outputs.windows_signing }}
|
||||||
|
artifact_label: ${{ steps.policy.outputs.artifact_label }}
|
||||||
|
warning_required: ${{ steps.policy.outputs.warning_required }}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
|
steps:
|
||||||
|
- name: Checkout release source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.13"
|
||||||
|
|
||||||
|
- name: Resolve version-scoped signing policy
|
||||||
|
id: policy
|
||||||
|
shell: bash
|
||||||
|
run: npm run desktop:release-policy:check -- --require-tag --github-output "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
macos-release-candidate:
|
macos-release-candidate:
|
||||||
name: Signed macOS release candidate
|
name: macOS release candidate
|
||||||
|
needs: release-policy
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -24,8 +55,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VITE_BUILD_CHANNEL: release
|
VITE_BUILD_CHANNEL: release
|
||||||
VITE_BUILD_COMMIT: ${{ github.sha }}
|
VITE_BUILD_COMMIT: ${{ github.sha }}
|
||||||
|
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
|
||||||
RELEASE_BUILD: "true"
|
RELEASE_BUILD: "true"
|
||||||
REQUIRE_DESKTOP_SIGNING: "true"
|
DESKTOP_RELEASE_PLATFORM: macos
|
||||||
|
DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }}
|
||||||
|
REQUIRE_UPDATER_SIGNING: "true"
|
||||||
|
REQUIRE_DESKTOP_SIGNING: ${{ needs.release-policy.outputs.platform_signing_mode == 'signed' && 'true' || 'false' }}
|
||||||
|
ALLOW_UNSIGNED_PLATFORM_RELEASE: ${{ needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception' && 'true' || 'false' }}
|
||||||
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
@@ -50,7 +86,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
|
if [[ "${GITHUB_REF_TYPE}" != "tag" ]]; then
|
||||||
echo "Signed desktop release candidates must run from a vX.Y.Z tag."
|
echo "Desktop release candidates must run from a vX.Y.Z tag."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
expected_tag="v$(node -p "require('./package.json').version")"
|
expected_tag="v$(node -p "require('./package.json').version")"
|
||||||
@@ -67,10 +103,13 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Audit dependencies
|
||||||
|
run: npm audit
|
||||||
|
|
||||||
- name: Check release build metadata and signing environment
|
- name: Check release build metadata and signing environment
|
||||||
run: npm run release:env:check
|
run: npm run release:env:check
|
||||||
|
|
||||||
- name: Check signed desktop release readiness
|
- name: Check desktop release readiness
|
||||||
run: npm run desktop:release-readiness:check
|
run: npm run desktop:release-readiness:check
|
||||||
|
|
||||||
- name: Check synchronized client version
|
- name: Check synchronized client version
|
||||||
@@ -94,41 +133,324 @@ jobs:
|
|||||||
- name: Build Web artifact
|
- name: Build Web artifact
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Build signed Universal macOS artifacts
|
- name: Build Apple-signed and notarized Universal macOS artifacts
|
||||||
|
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
||||||
run: npm run desktop:build:macos-release -- --ci
|
run: npm run desktop:build:macos-release -- --ci
|
||||||
|
|
||||||
- name: Create desktop update feed
|
- name: Build ad-hoc Universal macOS artifacts
|
||||||
|
if: needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception'
|
||||||
|
run: npm run desktop:build:macos-unsigned-release -- --ci
|
||||||
|
|
||||||
|
- name: Verify and stage macOS artifacts
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [[ -z "${DESKTOP_UPDATE_BASE_URL}" ]]; then
|
app="$(find src-tauri/target -path '*/release/bundle/macos/*.app' -print -quit)"
|
||||||
echo "DESKTOP_UPDATE_BASE_URL or workflow input artifact_base_url is required."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
artifact="$(find src-tauri/target -path '*/release/bundle/macos/*.app.tar.gz' -print -quit)"
|
artifact="$(find src-tauri/target -path '*/release/bundle/macos/*.app.tar.gz' -print -quit)"
|
||||||
if [[ -z "${artifact}" ]]; then
|
dmg="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)"
|
||||||
echo "No macOS updater artifact was produced."
|
if [[ -z "${app}" || -z "${artifact}" || -z "${dmg}" || ! -s "${artifact}.sig" ]]; then
|
||||||
|
echo "macOS app, updater artifact/signature, and DMG are all required."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
include_args=()
|
codesign --verify --deep --strict --verbose=2 "${app}"
|
||||||
dmg="$(find src-tauri/target -path '*/release/bundle/dmg/*.dmg' -print -quit)"
|
if [[ "${DESKTOP_PLATFORM_SIGNING_MODE}" == "signed" ]]; then
|
||||||
if [[ -n "${dmg}" ]]; then
|
spctl --assess --type execute --verbose=2 "${app}"
|
||||||
include_args+=(--include "${dmg}")
|
xcrun stapler validate "${app}"
|
||||||
|
xcrun stapler validate "${dmg}"
|
||||||
|
else
|
||||||
|
signature_info="$(codesign -dv --verbose=4 "${app}" 2>&1)"
|
||||||
|
if ! grep -q "Signature=adhoc" <<<"${signature_info}"; then
|
||||||
|
echo "The macOS unsigned-platform exception must produce an ad-hoc signed app."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
stage="src-tauri/target/desktop-release-macos"
|
||||||
|
mkdir -p "${stage}"
|
||||||
|
if [[ "${DESKTOP_PLATFORM_SIGNING_MODE}" == "unsigned-exception" ]]; then
|
||||||
|
artifact_name="$(basename "${artifact}" .app.tar.gz)_UNSIGNED.app.tar.gz"
|
||||||
|
dmg_name="$(basename "${dmg}" .dmg)_UNSIGNED.dmg"
|
||||||
|
cp "${artifact}" "${stage}/${artifact_name}"
|
||||||
|
cp "${artifact}.sig" "${stage}/${artifact_name}.sig"
|
||||||
|
cp "${dmg}" "${stage}/${dmg_name}"
|
||||||
|
cp desktop-release-unsigned-warning.txt "${stage}/UNSIGNED-PLATFORM-RELEASE.txt"
|
||||||
|
else
|
||||||
|
cp "${artifact}" "${artifact}.sig" "${dmg}" "${stage}/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload macOS candidate artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ctms-desktop-macos-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
||||||
|
path: frontend/src-tauri/target/desktop-release-macos/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
windows-release-candidate:
|
||||||
|
name: Windows release candidate
|
||||||
|
needs: release-policy
|
||||||
|
runs-on: windows-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
|
env:
|
||||||
|
VITE_BUILD_CHANNEL: release
|
||||||
|
VITE_BUILD_COMMIT: ${{ github.sha }}
|
||||||
|
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
|
||||||
|
RELEASE_BUILD: "true"
|
||||||
|
DESKTOP_RELEASE_PLATFORM: windows
|
||||||
|
DESKTOP_PLATFORM_SIGNING_MODE: ${{ needs.release-policy.outputs.platform_signing_mode }}
|
||||||
|
REQUIRE_UPDATER_SIGNING: "true"
|
||||||
|
REQUIRE_WINDOWS_SIGNING: ${{ needs.release-policy.outputs.platform_signing_mode == 'signed' && 'true' || 'false' }}
|
||||||
|
ALLOW_UNSIGNED_PLATFORM_RELEASE: ${{ needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception' && 'true' || 'false' }}
|
||||||
|
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||||
|
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||||
|
WINDOWS_TIMESTAMP_URL: ${{ vars.WINDOWS_TIMESTAMP_URL }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout release source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.13"
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Enforce release tag context
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if ($env:GITHUB_REF_TYPE -ne "tag") {
|
||||||
|
throw "Desktop release candidates must run from a vX.Y.Z tag."
|
||||||
|
}
|
||||||
|
$expectedTag = "v$(node -p "require('./package.json').version")"
|
||||||
|
if ($env:GITHUB_REF_NAME -ne $expectedTag) {
|
||||||
|
throw "Release tag $env:GITHUB_REF_NAME does not match package version $expectedTag."
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Audit dependencies
|
||||||
|
run: npm audit
|
||||||
|
|
||||||
|
- name: Check release build metadata and signing environment
|
||||||
|
run: npm run release:env:check
|
||||||
|
|
||||||
|
- name: Check desktop release readiness
|
||||||
|
run: npm run desktop:release-readiness:check
|
||||||
|
|
||||||
|
- name: Check synchronized client version
|
||||||
|
run: npm run version:check
|
||||||
|
|
||||||
|
- name: Check runtime boundary
|
||||||
|
run: npm run runtime:check
|
||||||
|
|
||||||
|
- name: Check desktop release and security gate
|
||||||
|
run: npm run desktop:release:check
|
||||||
|
|
||||||
|
- name: Check UI contract
|
||||||
|
run: npm run ui:contract
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npm run type-check
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: npm run test:unit
|
||||||
|
|
||||||
|
- name: Build Web artifact
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Import Windows code-signing certificate
|
||||||
|
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$pfxPath = Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx"
|
||||||
|
$encodedCertificate = $env:WINDOWS_CERTIFICATE `
|
||||||
|
-replace "-----BEGIN [^-]+-----", "" `
|
||||||
|
-replace "-----END [^-]+-----", "" `
|
||||||
|
-replace "\s", ""
|
||||||
|
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($encodedCertificate))
|
||||||
|
$password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force
|
||||||
|
$importedCertificates = Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation "Cert:\CurrentUser\My" -Password $password -Exportable:$false
|
||||||
|
$certificate = $importedCertificates | Where-Object { $_.HasPrivateKey } | Select-Object -First 1
|
||||||
|
if (-not $certificate -or -not $certificate.HasPrivateKey) {
|
||||||
|
throw "The imported Windows code-signing certificate is missing its private key."
|
||||||
|
}
|
||||||
|
$now = Get-Date
|
||||||
|
if ($certificate.NotBefore -gt $now -or $certificate.NotAfter -le $now) {
|
||||||
|
throw "The Windows code-signing certificate is not currently valid."
|
||||||
|
}
|
||||||
|
$codeSigningEku = $certificate.EnhancedKeyUsageList | Where-Object { $_.ObjectId.Value -eq "1.3.6.1.5.5.7.3.3" }
|
||||||
|
if (-not $codeSigningEku) {
|
||||||
|
throw "The Windows certificate does not contain the Code Signing enhanced key usage."
|
||||||
|
}
|
||||||
|
"WINDOWS_CERTIFICATE_THUMBPRINT=$($certificate.Thumbprint)" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||||
|
Remove-Item $pfxPath -Force
|
||||||
|
|
||||||
|
- name: Write signed Windows Tauri config
|
||||||
|
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$config = @{
|
||||||
|
bundle = @{
|
||||||
|
windows = @{
|
||||||
|
certificateThumbprint = $env:WINDOWS_CERTIFICATE_THUMBPRINT
|
||||||
|
digestAlgorithm = "sha256"
|
||||||
|
timestampUrl = $env:WINDOWS_TIMESTAMP_URL
|
||||||
|
tsp = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 8
|
||||||
|
Set-Content -Path "tauri.windows.release.conf.json" -Value $config -Encoding utf8
|
||||||
|
Get-Content "tauri.windows.release.conf.json"
|
||||||
|
|
||||||
|
- name: Build signed Windows NSIS artifacts
|
||||||
|
if: needs.release-policy.outputs.platform_signing_mode == 'signed'
|
||||||
|
timeout-minutes: 30
|
||||||
|
run: npm run desktop:build:windows-release -- --config tauri.windows.release.conf.json --ci
|
||||||
|
|
||||||
|
- name: Build unsigned Windows NSIS artifacts
|
||||||
|
if: needs.release-policy.outputs.platform_signing_mode == 'unsigned-exception'
|
||||||
|
timeout-minutes: 30
|
||||||
|
run: npm run desktop:build:windows-release -- --ci
|
||||||
|
|
||||||
|
- name: Verify platform signing mode and stage Windows artifacts
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$bundleDir = "src-tauri/target/release/bundle/nsis"
|
||||||
|
$installers = @(Get-ChildItem -Path $bundleDir -Filter "*.exe" -File)
|
||||||
|
$updaters = @(Get-ChildItem -Path $bundleDir -Filter "*.nsis.zip" -File)
|
||||||
|
$appExecutables = @(Get-ChildItem -Path "src-tauri/target/release" -Filter "*.exe" -File)
|
||||||
|
if ($installers.Count -ne 1 -or $updaters.Count -ne 1 -or $appExecutables.Count -eq 0) {
|
||||||
|
throw "Exactly one NSIS installer/updater and at least one application executable are required."
|
||||||
|
}
|
||||||
|
|
||||||
|
$signaturePath = "$($updaters[0].FullName).sig"
|
||||||
|
if (-not (Test-Path $signaturePath) -or (Get-Item $signaturePath).Length -eq 0) {
|
||||||
|
throw "The Windows updater signature is missing or empty."
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($executable in @($appExecutables + $installers)) {
|
||||||
|
$signature = Get-AuthenticodeSignature -FilePath $executable.FullName
|
||||||
|
if ($env:DESKTOP_PLATFORM_SIGNING_MODE -eq "signed") {
|
||||||
|
if ($signature.Status -ne "Valid" -or -not $signature.SignerCertificate -or -not $signature.TimeStamperCertificate) {
|
||||||
|
throw "Authenticode signature or RFC 3161 timestamp is invalid for $($executable.FullName): $($signature.StatusMessage)"
|
||||||
|
}
|
||||||
|
} elseif ($signature.Status -ne "NotSigned") {
|
||||||
|
throw "The Windows unsigned-platform exception must produce an Authenticode-unsigned executable: $($executable.FullName) returned $($signature.Status)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stage = "src-tauri/target/desktop-release-windows"
|
||||||
|
New-Item -ItemType Directory -Path $stage -Force | Out-Null
|
||||||
|
if ($env:DESKTOP_PLATFORM_SIGNING_MODE -eq "unsigned-exception") {
|
||||||
|
$installerName = $installers[0].Name -replace '\.exe$', '_UNSIGNED.exe'
|
||||||
|
$updaterName = $updaters[0].Name -replace '\.nsis\.zip$', '_UNSIGNED.nsis.zip'
|
||||||
|
Copy-Item $installers[0].FullName -Destination (Join-Path $stage $installerName)
|
||||||
|
Copy-Item $updaters[0].FullName -Destination (Join-Path $stage $updaterName)
|
||||||
|
Copy-Item $signaturePath -Destination (Join-Path $stage "$updaterName.sig")
|
||||||
|
Copy-Item "desktop-release-unsigned-warning.txt" -Destination (Join-Path $stage "UNSIGNED-PLATFORM-RELEASE.txt")
|
||||||
|
} else {
|
||||||
|
Copy-Item $installers[0].FullName, $updaters[0].FullName, $signaturePath -Destination $stage
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Remove Windows signing certificate
|
||||||
|
if: always()
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if ($env:WINDOWS_CERTIFICATE_THUMBPRINT) {
|
||||||
|
Remove-Item "Cert:\CurrentUser\My\$env:WINDOWS_CERTIFICATE_THUMBPRINT" -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
Remove-Item (Join-Path $env:RUNNER_TEMP "ctms-windows-signing.pfx") -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item "tauri.windows.release.conf.json" -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
- name: Upload Windows candidate artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ctms-desktop-windows-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
||||||
|
path: frontend/src-tauri/target/desktop-release-windows/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
aggregate-release-candidate:
|
||||||
|
name: Verify combined desktop release directory
|
||||||
|
needs:
|
||||||
|
- release-policy
|
||||||
|
- macos-release-candidate
|
||||||
|
- windows-release-candidate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
|
env:
|
||||||
|
DESKTOP_UPDATE_BASE_URL: ${{ github.event_name == 'workflow_dispatch' && inputs.artifact_base_url || vars.DESKTOP_UPDATE_BASE_URL }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout release source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22.13"
|
||||||
|
|
||||||
|
- name: Download macOS candidate artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ctms-desktop-macos-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
||||||
|
path: frontend/src-tauri/target/desktop-release-input/macos
|
||||||
|
|
||||||
|
- name: Download Windows candidate artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ctms-desktop-windows-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
||||||
|
path: frontend/src-tauri/target/desktop-release-input/windows
|
||||||
|
|
||||||
|
- name: Create desktop release provenance
|
||||||
|
run: npm run desktop:release-provenance:create -- --macos-signing "${{ needs.release-policy.outputs.macos_signing }}" --windows-signing "${{ needs.release-policy.outputs.windows_signing }}" --output src-tauri/target/desktop-release-input/DESKTOP-RELEASE-PROVENANCE.json
|
||||||
|
|
||||||
|
- name: Create combined desktop update feed
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mapfile -t mac_artifacts < <(find src-tauri/target/desktop-release-input/macos -name '*.app.tar.gz' -type f)
|
||||||
|
mapfile -t dmgs < <(find src-tauri/target/desktop-release-input/macos -name '*.dmg' -type f)
|
||||||
|
mapfile -t windows_artifacts < <(find src-tauri/target/desktop-release-input/windows -name '*.nsis.zip' -type f)
|
||||||
|
mapfile -t installers < <(find src-tauri/target/desktop-release-input/windows -name '*.exe' -type f)
|
||||||
|
if [[ ${#mac_artifacts[@]} -ne 1 || ${#dmgs[@]} -ne 1 || ${#windows_artifacts[@]} -ne 1 || ${#installers[@]} -ne 1 ]]; then
|
||||||
|
echo "Exactly one macOS updater/DMG and one Windows updater/installer are required."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
include_args=(--include "${dmgs[0]}" --include "${installers[0]}" --include src-tauri/target/desktop-release-input/DESKTOP-RELEASE-PROVENANCE.json)
|
||||||
|
notes_args=()
|
||||||
|
if [[ "${{ needs.release-policy.outputs.warning_required }}" == "true" ]]; then
|
||||||
|
mapfile -t warnings < <(find src-tauri/target/desktop-release-input/macos -name 'UNSIGNED-PLATFORM-RELEASE.txt' -type f)
|
||||||
|
if [[ ${#warnings[@]} -ne 1 ]]; then
|
||||||
|
echo "The unsigned-platform release warning is required."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
include_args+=(--include "${warnings[0]}")
|
||||||
|
notes_args+=(--notes "UNSIGNED PLATFORM RELEASE: macOS is ad-hoc signed and not notarized; Windows is not Authenticode-signed. Gatekeeper and SmartScreen warnings are expected. Verify tag, commit, updater signatures, and SHA256SUMS.txt before installation.")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
npm run desktop:update-feed:create -- \
|
npm run desktop:update-feed:create -- \
|
||||||
--artifact "${artifact}" \
|
--artifact "${mac_artifacts[0]}" \
|
||||||
|
--platform-artifact "windows-x86_64=${windows_artifacts[0]}" \
|
||||||
"${include_args[@]}" \
|
"${include_args[@]}" \
|
||||||
|
"${notes_args[@]}" \
|
||||||
--output-dir src-tauri/target/desktop-release-feed \
|
--output-dir src-tauri/target/desktop-release-feed \
|
||||||
--base-url "${DESKTOP_UPDATE_BASE_URL}"
|
--base-url "${DESKTOP_UPDATE_BASE_URL}"
|
||||||
|
|
||||||
- name: Verify desktop update feed
|
- name: Verify combined desktop update feed
|
||||||
run: npm run desktop:update-feed:check -- --feed src-tauri/target/desktop-release-feed/latest.json --artifacts-dir src-tauri/target/desktop-release-feed --base-url "${DESKTOP_UPDATE_BASE_URL}"
|
run: npm run desktop:update-feed:check -- --feed src-tauri/target/desktop-release-feed/latest.json --artifacts-dir src-tauri/target/desktop-release-feed --base-url "${DESKTOP_UPDATE_BASE_URL}" --require-platform windows-x86_64 --require-provenance
|
||||||
|
|
||||||
- name: Upload verified desktop release directory
|
- name: Upload verified desktop release directory
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ctms-desktop-release-${{ github.ref_name }}
|
name: ctms-desktop-release-${{ github.ref_name }}-${{ needs.release-policy.outputs.artifact_label }}
|
||||||
path: frontend/src-tauri/target/desktop-release-feed/*
|
path: frontend/src-tauri/target/desktop-release-feed/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ jobs:
|
|||||||
windows-internal:
|
windows-internal:
|
||||||
name: Windows NSIS internal validation
|
name: Windows NSIS internal validation
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
VITE_DESKTOP_SERVER_URL: ${{ vars.VITE_DESKTOP_SERVER_URL }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
桌面端项目计划书已完成并移除;当前桌面端执行约束以本文件为准。桌面端任务包括但不限于 Tauri、macOS、Windows、桌面打包、桌面存储、文件集成、系统通知、自动更新和桌面端安全边界。历史设计只作追溯参考,见 `docs/desktop-phase-1-design.md`、`docs/desktop-phase-2-design.md`、`docs/audits/desktop-release-stabilization-checklist.md`。
|
桌面端项目计划书已完成并移除;当前桌面端执行约束以本文件为准。桌面端任务包括但不限于 Tauri、macOS、Windows、桌面打包、桌面存储、文件集成、系统通知、自动更新和桌面端安全边界。历史设计只作追溯参考,见 `docs/desktop-phase-1-design.md`、`docs/desktop-phase-2-design.md`、`docs/audits/desktop-release-stabilization-checklist.md`。
|
||||||
|
|
||||||
桌面端当前不是空白初始化项目。Tauri 基线、macOS 在线桌面壳和第二阶段原生能力主体已经形成;后续工作限于修复、稳定化、体验收口、发布准备、在线辅助缓存和 Windows 兼容验证。不得重新按第一阶段空白项目初始化 Tauri,不得绕过现有 `frontend/src/runtime/` 适配层直接在业务模块中使用 Tauri API。
|
桌面端当前不是空白初始化项目。Tauri 基线、macOS 在线桌面壳和第二阶段原生能力主体已经形成;后续工作限于修复、稳定化、体验收口、发布准备、在线辅助缓存、Windows 兼容验证和已批准的 Windows 正式发布。不得重新按第一阶段空白项目初始化 Tauri,不得绕过现有 `frontend/src/runtime/` 适配层直接在业务模块中使用 Tauri API。
|
||||||
|
|
||||||
除非用户明确要求先调整本文件中的约束,否则不要实现离线功能、本地业务权威数据存储、内嵌后端服务、离线同步、本地优先工作流或新的阶段性桌面产品线。在线辅助本地缓存只允许作为已认证在线客户端的体验加速能力;处理桌面本地缓存、请求去重、条件请求、缓存诊断或缓存清理相关任务时,必须阅读并遵守 `docs/desktop-local-cache-plan.md`。
|
除非用户明确要求先调整本文件中的约束,否则不要实现离线功能、本地业务权威数据存储、内嵌后端服务、离线同步、本地优先工作流或新的阶段性桌面产品线。在线辅助本地缓存只允许作为已认证在线客户端的体验加速能力;处理桌面本地缓存、请求去重、条件请求、缓存诊断或缓存清理相关任务时,必须阅读并遵守 `docs/desktop-local-cache-plan.md`。
|
||||||
|
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
- 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。
|
- 未完成后端 token 校验和 `/me` 身份确认前,不得展示业务缓存;登出、切换服务器、切换用户、401/403、权限上下文变化或缓存 schema 变化时必须清理或失效相关缓存。
|
||||||
- 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs`、`npm run runtime:check` 和桌面发布检查清单是否需要更新。
|
- 新增或调整 Tauri command、capability、CSP、updater、凭据、文件、通知、本地缓存持久化或底层存储能力时,必须同步评估 `frontend/scripts/verify-desktop-release.mjs`、`npm run runtime:check` 和桌面发布检查清单是否需要更新。
|
||||||
- token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。
|
- token、附件下载凭据和敏感业务信息不得写入 URL、日志、系统通知正文或明文浏览器存储。
|
||||||
- Windows 仍只作为第二阶段兼容性验证目标;未获明确批准前不发布正式 Windows 安装包。Windows 内测构建只能使用 `.github/workflows/desktop-windows-internal.yml` 的手动验证入口,不得生成生产 `latest.json`、updater feed 或正式发布制品。
|
- 正式桌面客户端的默认 CTMS 服务端入口必须由构建环境变量 `VITE_DESKTOP_SERVER_URL` 注入,不得在运行时代码中写死生产域名;用户仍可在桌面服务器设置中手动覆盖,手动值优先并沿用既有的切换服务器登出与缓存失效边界。该变量只表示业务服务端 origin,不得与 updater 制品前缀 `DESKTOP_UPDATE_BASE_URL` 混用。
|
||||||
|
- Windows x64 NSIS 已获准作为正式桌面发布目标;正式制品必须由 `.github/workflows/desktop-release-candidate.yml` 从与 macOS/Web 相同的 `vX.Y.Z` tag 和 SHA 构建。平台签名默认要求组织 Windows 代码签名证书、RFC 3161 时间戳和 Authenticode 校验;只有 `frontend/desktop-release-policy.json` 中按精确版本记录、经发布负责人批准的例外可跳过平台签名。无论是否采用平台签名例外,都必须使用 updater 私钥、校验 updater feed,并明确标记平台未签名风险。`.github/workflows/desktop-windows-internal.yml` 仍只用于分支上的无签名兼容性验证,不得生成生产 `latest.json`、updater feed 或正式发布制品。
|
||||||
|
- v0.1.0 另获准在 GitHub Actions 额度不可用时,从最终 `release` 上不可移动的 `v0.1.0` tag/SHA 在受控 macOS 主机本地构建并先行上传 macOS ad-hoc 制品;Windows 只能在额度恢复后从同一 tag/SHA 后补。面向安装用户的 GitHub Release 只保留 DMG 与只校验该安装包的 `SHA256SUMS.txt`,平台未签名风险和 Windows pending 状态写入 Release Notes。macOS updater 包及 `.sig`、完整 checksum、provenance、`UNSIGNED-PLATFORM` 与 Windows pending 证据必须保存在被 Git 忽略的私有发布目录,待 Windows `NotSigned` 制品和联合 feed 均验证通过后再发布到可匿名读取的独立 HTTPS updater 源;此前不得发布或替换生产 `latest.json`。该本地/分阶段例外不适用于后续版本。
|
||||||
|
|
||||||
## 分支与发布治理
|
## 分支与发布治理
|
||||||
|
|
||||||
@@ -57,4 +59,4 @@ npm run desktop:build:app
|
|||||||
|
|
||||||
本地缓存相关变更至少执行 `runtime:check`、`desktop:release:check`、`ui:contract`、`type-check`、`test:unit` 和 `build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。
|
本地缓存相关变更至少执行 `runtime:check`、`desktop:release:check`、`ui:contract`、`type-check`、`test:unit` 和 `build`;若新增 Tauri command、capability、CSP 或底层持久化存储,还需执行 `desktop:build:app` 并在真实桌面 App 中验证缓存清理和诊断入口。
|
||||||
|
|
||||||
正式桌面发布构建仍必须使用组织批准的 updater 签名私钥和 Apple 签名/公证流程;未签名或 ad-hoc 构建只能作为内部验证构建描述。
|
正式桌面发布构建必须使用组织批准的 updater 签名私钥,updater 签名不可因平台签名例外而关闭。平台签名默认要求 macOS 完成 Apple 签名/公证、Windows 完成组织代码签名、RFC 3161 时间戳和 Authenticode 校验。当前仅批准 v0.1.0 采用受控分发例外:macOS 使用 ad-hoc 签名且不公证,Windows 应用和安装器保持 Authenticode 未签名;制品名、Release Notes、私有发布证据、完整 updater 校验清单和 provenance 必须清楚标注 `UNSIGNED-PLATFORM`,并提示 Gatekeeper/SmartScreen 警告。面向安装用户的 GitHub Release 可按精确版本策略精简为安装包与对应 checksum,但不得因此删除私有 updater 签名制品或验证证据。该例外不自动适用于后续版本。
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# CTMS Desktop Release Stabilization Checklist
|
# CTMS Desktop Release Stabilization Checklist
|
||||||
|
|
||||||
状态: `active`
|
状态: `active`
|
||||||
适用范围: Web 与 macOS Desktop 统一客户端发布
|
适用范围: Web、macOS Desktop 与 Windows x64 Desktop 统一客户端发布
|
||||||
最后更新: `2026-07-14`
|
最后更新: `2026-07-17`
|
||||||
|
|
||||||
本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。当前允许按 `docs/desktop-local-cache-plan.md` 引入在线辅助本地缓存,但不引入离线登录、离线写入、本地业务权威数据、内嵌后端服务或离线同步。
|
本清单用于第一、二阶段桌面端能力完成后的准发布稳定化。当前允许按 `docs/desktop-local-cache-plan.md` 引入在线辅助本地缓存,但不引入离线登录、离线写入、本地业务权威数据、内嵌后端服务或离线同步。
|
||||||
|
|
||||||
@@ -28,12 +28,14 @@ npm run desktop:build:app
|
|||||||
|
|
||||||
- [ ] `frontend/package.json`、`package-lock.json`、Tauri 配置、Cargo manifest/lock 版本一致。
|
- [ ] `frontend/package.json`、`package-lock.json`、Tauri 配置、Cargo manifest/lock 版本一致。
|
||||||
- [ ] `VITE_BUILD_CHANNEL=release` 和 `VITE_BUILD_COMMIT=<release tag commit>` 由 CI 注入,且 `npm run release:env:check` 通过。
|
- [ ] `VITE_BUILD_CHANNEL=release` 和 `VITE_BUILD_COMMIT=<release tag commit>` 由 CI 注入,且 `npm run release:env:check` 通过。
|
||||||
- [ ] 在正式 release tag 和签名环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、签名/公证变量、updater 私钥和生产 artifact HTTPS 基址齐备。
|
- [ ] 在正式 release tag 环境中执行 `npm run desktop:release-readiness:check`,确认 tag、构建元数据、当前精确版本的平台签名策略、updater 私钥和生产 artifact HTTPS 基址齐备。
|
||||||
- [ ] macOS app 已签名和公证。
|
- [ ] macOS app 已完成 Apple 签名/公证;若当前精确版本获批平台签名例外,则已验证 `Signature=adhoc`、确认未公证并附带 Gatekeeper 风险说明。
|
||||||
|
- [ ] Windows 应用和 NSIS 安装器已使用组织证书签名并带有效 RFC 3161 时间戳,`Get-AuthenticodeSignature` 返回 `Valid`;若当前精确版本获批例外,则应用和安装器必须返回 `NotSigned` 并附带 SmartScreen 风险说明。
|
||||||
- [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。
|
- [ ] updater `.sig` 使用组织 CI secret 或密钥库中的私钥生成,私钥未进入仓库。
|
||||||
- [ ] 设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 Apple 签名/公证变量后,以 `REQUIRE_DESKTOP_SIGNING=true` 再次执行 `npm run release:env:check`,随后执行 `npm run desktop:build:macos-release -- --ci`。
|
- [ ] 始终设置 `TAURI_SIGNING_PRIVATE_KEY`、`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 和 `REQUIRE_UPDATER_SIGNING=true`;默认 macOS 路径再设置 Apple 变量和 `REQUIRE_DESKTOP_SIGNING=true`,例外路径则设置 `DESKTOP_PLATFORM_SIGNING_MODE=unsigned-exception`、`ALLOW_UNSIGNED_PLATFORM_RELEASE=true` 并执行 `npm run desktop:build:macos-unsigned-release -- --ci`。
|
||||||
- [ ] 正式 updater feed 先执行 `npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>` 生成 `latest.json` 与 `SHA256SUMS.txt`。
|
- [ ] 默认 Windows 路径设置 PFX、密码、`WINDOWS_TIMESTAMP_URL` 和 `REQUIRE_WINDOWS_SIGNING=true`;例外路径不得伪装签名状态,构建后必须对应用和安装器验证 `NotSigned`。
|
||||||
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>`,并确认 checksum manifest、updater artifact、`.sig` 和 `latest.json` 均通过校验。
|
- [ ] 正式 updater feed 使用 `--artifact <CTMS.app.tar.gz>` 和 `--platform-artifact windows-x86_64=<CTMS.nsis.zip>` 汇总生成同一个 `latest.json` 与 `SHA256SUMS.txt`。
|
||||||
|
- [ ] 正式 updater feed 执行 `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64 --require-provenance`,并确认两个平台的 updater artifact、`.sig`、安装包、provenance、checksum manifest 和 `latest.json` 均通过校验。
|
||||||
- [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`。
|
- [ ] 不可变制品先上传,`latest.json` 最后原子替换;若 feed 校验未通过,不替换线上 `latest.json`。
|
||||||
- [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。
|
- [ ] Web 与 Desktop 制品记录同一产品版本、Git 标签和完整提交 SHA。
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ npm run desktop:build:app
|
|||||||
- [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。
|
- [ ] updater capability 不直接暴露给 WebView,自动更新只走受控 Tauri command。
|
||||||
- [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。
|
- [ ] 更新弹窗 release notes 过滤 URL、token 查询参数和 Authorization/Bearer 形态文本。
|
||||||
- [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。
|
- [ ] CI release 候选 workflow 包含 version/runtime/desktop/ui/type/unit/build/desktop app smoke 门禁。
|
||||||
- [ ] signed macOS release candidate workflow 只允许从 `vX.Y.Z` tag 运行,并包含签名环境检查、Universal macOS 构建、update feed 生成、checksum 校验和 verified release directory 上传。
|
- [ ] Desktop release candidate workflow 只允许从 `vX.Y.Z` tag 运行;先按精确版本解析平台签名策略,macOS 和 Windows 原生 job 分别验证实际签名状态,再由聚合 job 生成包含 `darwin-aarch64`、`darwin-x86_64` 和 `windows-x86_64` 的 feed、provenance、checksum 清单和 verified release directory。
|
||||||
|
|
||||||
人工复审还必须确认:
|
人工复审还必须确认:
|
||||||
|
|
||||||
@@ -86,7 +88,8 @@ npm run desktop:build:app
|
|||||||
| 登录与项目恢复 | 必测 | 必测 | 登录成功后恢复可访问项目;401 后重新登录 |
|
| 登录与项目恢复 | 必测 | 必测 | 登录成功后恢复可访问项目;401 后重新登录 |
|
||||||
| 记住密码 | 必测 | 必测 | Web 使用浏览器凭据管理/自动填充;Desktop 使用系统凭据库;未勾选时不继续写入保存密码 |
|
| 记住密码 | 必测 | 必测 | Web 使用浏览器凭据管理/自动填充;Desktop 使用系统凭据库;未勾选时不继续写入保存密码 |
|
||||||
| 30 天免登录 | 不适用 | 必测 | 关闭并重启 App 后复用系统凭据库中的后端在线会话;超过 30 天或 `/me` 校验失败后重新登录 |
|
| 30 天免登录 | 不适用 | 必测 | 关闭并重启 App 后复用系统凭据库中的后端在线会话;超过 30 天或 `/me` 校验失败后重新登录 |
|
||||||
| 服务器地址未配置 | 不适用 | 必测 | 自动进入服务器设置,不进入业务页 |
|
| 构建默认服务器地址已注入 | 不适用 | 必测 | 首次启动使用 `VITE_DESKTOP_SERVER_URL`,不要求重复输入;运行时代码不含生产域名 |
|
||||||
|
| 构建默认地址和本地配置均缺失 | 不适用 | 必测 | 自动进入服务器设置,不进入业务页 |
|
||||||
| 服务器地址切换 | 不适用 | 必测 | 清除当前会话和项目上下文,要求重新登录 |
|
| 服务器地址切换 | 不适用 | 必测 | 清除当前会话和项目上下文,要求重新登录 |
|
||||||
| 服务端不可达 | 必测 | 必测 | 显示可恢复错误,不进入离线模式 |
|
| 服务端不可达 | 必测 | 必测 | 显示可恢复错误,不进入离线模式 |
|
||||||
| 本地缓存命中 | 必测 | 必测 | `/me` 校验通过后可先展示缓存再后台刷新 |
|
| 本地缓存命中 | 必测 | 必测 | `/me` 校验通过后可先展示缓存再后台刷新 |
|
||||||
@@ -261,3 +264,35 @@ npm run desktop:build:app
|
|||||||
真实 `.app` 仍需人工验证:红色按钮真正关闭主窗口后 Dock 重建、菜单顺序、个人中心自身关闭按钮、编辑菜单文本输入行为、自定义快捷键即时同步,以及系统明暗模式切换时的标题栏一致性。
|
真实 `.app` 仍需人工验证:红色按钮真正关闭主窗口后 Dock 重建、菜单顺序、个人中心自身关闭按钮、编辑菜单文本输入行为、自定义快捷键即时同步,以及系统明暗模式切换时的标题栏一致性。
|
||||||
|
|
||||||
本轮未引入以下条件性 Swift 能力:Quick Look 需先确认附件审阅频率和 Windows 降级语义;Touch ID 会改变现有 30 天会话恢复体验,需先确定凭据访问控制策略;通知点击回跳需先定义固定、无敏感信息的动作和目标页面。三项均不是当前发布前置条件。
|
本轮未引入以下条件性 Swift 能力:Quick Look 需先确认附件审阅频率和 Windows 降级语义;Touch ID 会改变现有 30 天会话恢复体验,需先确定凭据访问控制策略;通知点击回跳需先定义固定、无敏感信息的动作和目标页面。三项均不是当前发布前置条件。
|
||||||
|
|
||||||
|
## 12. 2026-07-17 Windows 正式发布链路记录
|
||||||
|
|
||||||
|
经发布负责人明确批准,Windows x64 NSIS 从内部兼容性验证目标提升为正式桌面发布目标。本轮完成:
|
||||||
|
|
||||||
|
- 正式 Desktop candidate workflow 拆分为 macOS 签名/公证、Windows 代码签名和跨平台聚合三个 job;两端都只接受与客户端版本一致的 `vX.Y.Z` tag。
|
||||||
|
- Windows job 从组织 secret 导入 Base64 PFX,动态配置证书指纹、SHA-256、RFC 3161 时间戳,构建 NSIS installer/updater,并校验应用与安装器的 Authenticode signer 和 timestamp certificate。
|
||||||
|
- 聚合 job 只在两个原生 job 均通过后生成统一 `latest.json`,同时包含 Universal macOS 的两个平台键和 `windows-x86_64`,并校验所有制品、`.sig` 与 checksum。
|
||||||
|
- 无签名 `.github/workflows/desktop-windows-internal.yml` 继续保持 branch-only 内部验证语义,不生成正式 updater feed。
|
||||||
|
- npm 依赖锁文件已升级至当前安全修复版本,生产和完整依赖审计均为 0 个已知漏洞;Axios 新 header 类型通过受控字符串归一化适配并新增单元测试。
|
||||||
|
|
||||||
|
创建正式 tag 前仍必须由发布负责人确认:
|
||||||
|
|
||||||
|
- GitHub Actions 已配置 updater secrets 和 versioned artifact base URL;默认平台签名路径还必须配置 Apple 与 Windows secrets 和 Windows timestamp URL。
|
||||||
|
- 若使用默认 Windows 签名路径,组织证书允许 PFX 导入 CI;若为硬件或云托管密钥,先将 workflow 切换为经审计的 Tauri `signCommand` provider。
|
||||||
|
- macOS/Windows 原生 candidate job、联合 feed 校验、真实安装/升级和生产下载源上传全部通过。
|
||||||
|
|
||||||
|
## 13. 2026-07-17 v0.1.0 平台签名例外记录
|
||||||
|
|
||||||
|
经发布负责人明确批准,v0.1.0 可在无法提供 Apple Developer 和 Windows 组织代码签名凭据时受控分发。该批准仅覆盖精确版本 `0.1.0`:
|
||||||
|
|
||||||
|
- `frontend/desktop-release-policy.json` 默认仍为 `signed`,仅将 v0.1.0 列入 `unsignedPlatformExceptions`;后续版本不会继承本例外。
|
||||||
|
- macOS 使用 Tauri `signingIdentity: "-"` 生成 ad-hoc 签名 Universal app/DMG,必须通过 `codesign --verify` 并确认 `Signature=adhoc`,不执行或声称 Apple 公证。
|
||||||
|
- Windows 应用和 NSIS 安装器不做 Authenticode 签名,必须通过 `Get-AuthenticodeSignature` 确认 `NotSigned`,不执行或声称 RFC 3161 时间戳。
|
||||||
|
- 两端仍必须从同一 `v0.1.0` tag 和 SHA 构建,使用同一 updater 私钥生成 `.sig`,并通过联合 feed、checksum 和 provenance 校验。
|
||||||
|
- GitHub Actions 额度不可用期间,发布负责人额外批准从最终 `v0.1.0` tag/SHA 在受控 macOS 主机本地构建并先行上传 macOS 制品;Windows 必须在恢复后从同一 tag/SHA 后补。
|
||||||
|
- macOS 先行的用户可见 GitHub Release 只包含 DMG、仅覆盖该 DMG 的 checksum 和 GitHub 自动源码归档;Release Notes 必须明确 macOS ad-hoc/未公证风险和 Windows pending,且不得包含或替换生产 `latest.json`。updater 包及 `.sig`、完整 checksum、provenance、`UNSIGNED-PLATFORM` 和 Windows pending 证据必须先复制到权限受限且被 Git 忽略的私有发布目录。
|
||||||
|
- Windows `NotSigned` 实物和联合 feed 全部验证通过后,才允许激活生产 updater feed;tag 不得为补充 Windows 制品而移动。
|
||||||
|
- 制品与 Actions artifact 名称必须含 `_UNSIGNED` 或 `UNSIGNED-PLATFORM`,私有证据/完整 updater 发布目录必须携带 `UNSIGNED-PLATFORM-RELEASE.txt` 和 `DESKTOP-RELEASE-PROVENANCE.json`;installer-only GitHub Release 通过 `_UNSIGNED` 安装包名和 Release Notes 呈现风险,不要求公开展示内部证据文件。
|
||||||
|
- 受控分发说明必须明确提示 macOS Gatekeeper 与 Windows SmartScreen 警告;平台未签名制品不得描述为 Apple/Microsoft 信任或已公证/已 Authenticode 签名。
|
||||||
|
|
||||||
|
本地实物验证已完成:使用当前 updater 私钥成功构建 Universal `x86_64 arm64` macOS app、DMG、`.app.tar.gz` 和 `.sig`;`codesign --verify --deep --strict` 通过,签名详情为 `Signature=adhoc`、`TeamIdentifier=not set`,Gatekeeper 拒绝符合预期。Windows `NotSigned`、NSIS 和 updater `.sig` 仍须由 `windows-latest` 原生 job 在正式 tag 上验证。
|
||||||
|
|||||||
@@ -305,9 +305,33 @@ Before promoting `main` to `release`, confirm:
|
|||||||
|
|
||||||
- release checklist is complete
|
- release checklist is complete
|
||||||
- production initialization, build, and smoke verification are complete
|
- production initialization, build, and smoke verification are complete
|
||||||
|
- Desktop updater signatures are present and verified
|
||||||
|
- platform signing is either complete or the exact version has an approved,
|
||||||
|
checked-in exception in `frontend/desktop-release-policy.json`; the current
|
||||||
|
v0.1.0 exception requires macOS ad-hoc signing, unsigned Windows artifacts,
|
||||||
|
controlled distribution, and explicit trust warnings
|
||||||
- rollback or remediation path is clear
|
- rollback or remediation path is clear
|
||||||
- release tag has been prepared
|
- release tag has been prepared
|
||||||
|
|
||||||
|
Platform signing remains the default for versions without an exact approved
|
||||||
|
exception. An exception never permits disabling Tauri updater signatures or
|
||||||
|
building Web, macOS, and Windows from different tags or commits.
|
||||||
|
|
||||||
|
For v0.1.0 only, the release owner also approved a staged contingency while
|
||||||
|
hosted Actions capacity is unavailable: macOS may be built on a controlled
|
||||||
|
local macOS host from the immutable final `v0.1.0` tag and published first.
|
||||||
|
Windows remains pending and must later be built from that same tag and SHA.
|
||||||
|
The user-facing macOS-only GitHub Release may contain only the DMG and a
|
||||||
|
checksum manifest that covers that installer; platform-trust and
|
||||||
|
Windows-pending status must remain explicit in the Release Notes. The updater
|
||||||
|
package and signature, full checksum manifest, provenance, warning, and
|
||||||
|
Windows-pending evidence must be retained in a Git-ignored private release
|
||||||
|
directory. After Windows is built from the same tag and SHA, those updater
|
||||||
|
artifacts may be published only to a separately configured anonymous HTTPS
|
||||||
|
update origin. The staged release must not publish or replace production
|
||||||
|
`latest.json`; updater feed activation waits for combined macOS and Windows
|
||||||
|
verification.
|
||||||
|
|
||||||
Reference:
|
Reference:
|
||||||
|
|
||||||
- [docs/guides/release-checklist.md](guides/release-checklist.md)
|
- [docs/guides/release-checklist.md](guides/release-checklist.md)
|
||||||
|
|||||||
@@ -106,8 +106,9 @@ npm run desktop:build:app
|
|||||||
npm run desktop:build:app
|
npm run desktop:build:app
|
||||||
```
|
```
|
||||||
|
|
||||||
正式桌面发布构建仍必须设置 updater 签名私钥和 Apple 签名/公证变量后,先执行
|
正式桌面发布构建必须从同一正式 tag 分别在 macOS 和 Windows 原生 CI job 执行。两端都必须设置 updater 签名私钥,updater 签名不可关闭。平台签名默认路径仍要求 macOS 设置 Apple 签名/公证变量并以 `REQUIRE_DESKTOP_SIGNING=true` 执行,Windows 设置 PFX、密码和 RFC 3161 时间戳变量并以 `REQUIRE_WINDOWS_SIGNING=true` 执行。只有 `frontend/desktop-release-policy.json` 对当前精确版本存在已批准例外时,workflow 才可改为 macOS ad-hoc、Windows Authenticode 未签名路径;例外制品必须标注 `UNSIGNED-PLATFORM`、生成 provenance 和风险说明。两个平台与联合 feed 校验通过后才能汇总正式 updater feed。
|
||||||
`npm run desktop:release-readiness:check`,再执行 `npm run desktop:build:macos-release -- --ci`。
|
|
||||||
|
v0.1.0 额外批准 GitHub Actions 额度不可用时的分阶段应急路径:先在受控 macOS 主机从最终、不可移动的 `v0.1.0` tag/SHA 本地构建 macOS ad-hoc 制品,面向安装用户的 GitHub Release 只保留 DMG 与仅覆盖该安装包的 checksum,平台未签名警告和 Windows pending 状态写入 Release Notes。macOS updater 包及 `.sig`、完整 checksum、provenance、风险说明和 Windows pending 证据必须保存在被 Git 忽略的私有发布目录;Windows 恢复后必须从同一 tag/SHA 后补,并在联合验证通过后把 updater 制品发布到可匿名读取的独立 HTTPS 更新源。macOS 先行阶段不得发布或替换生产 `latest.json`,联合 feed 必须等待 Windows `NotSigned` 实物验证完成。
|
||||||
|
|
||||||
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
|
后端改动应补充执行受影响模块的后端测试、迁移检查和接口回归。
|
||||||
|
|
||||||
@@ -207,6 +208,7 @@ dev -> main
|
|||||||
- 前后端测试通过
|
- 前后端测试通过
|
||||||
- 网页端构建通过
|
- 网页端构建通过
|
||||||
- macOS 桌面端构建通过
|
- macOS 桌面端构建通过
|
||||||
|
- Windows x64 NSIS 兼容性构建通过;正式发布时平台签名和时间戳验证通过,或当前精确版本的已批准平台签名例外验证通过
|
||||||
- 数据库迁移经过验证
|
- 数据库迁移经过验证
|
||||||
- 已知风险和回滚方式已记录
|
- 已知风险和回滚方式已记录
|
||||||
|
|
||||||
@@ -281,7 +283,7 @@ main -> release
|
|||||||
- 回归测试通过
|
- 回归测试通过
|
||||||
- 数据库迁移和回滚方案确认
|
- 数据库迁移和回滚方案确认
|
||||||
- 网页端生产构建通过
|
- 网页端生产构建通过
|
||||||
- 桌面端生产构建通过
|
- macOS 与 Windows 桌面端生产构建均从候选 tag 通过
|
||||||
- 发布说明完成
|
- 发布说明完成
|
||||||
- 生产配置和密钥不在仓库中
|
- 生产配置和密钥不在仓库中
|
||||||
- 正式版本号已经统一
|
- 正式版本号已经统一
|
||||||
@@ -295,7 +297,9 @@ git tag -a v1.2.0 -m "CTMS v1.2.0"
|
|||||||
git push origin v1.2.0
|
git push origin v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
网页端和桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。
|
网页端、macOS 和 Windows 桌面端必须从同一个 `v1.2.0` 标签构建。不得从不同分支、不同提交或本地未提交状态构建正式制品。两个平台的 updater 签名制品必须始终验证通过;macOS 签名/公证和 Windows 代码签名/RFC 3161 时间戳必须验证通过,除非 `frontend/desktop-release-policy.json` 对该精确版本记录了已批准例外。采用例外时必须验证 macOS 确为 ad-hoc、Windows 确为 `NotSigned`,并在 Release Notes、私有发布证据、完整 updater checksum 和 provenance 中记录 `UNSIGNED-PLATFORM` 风险;面向安装用户的 Release 可按精确版本策略只显示安装包与安装包 checksum,但 updater 制品及验证证据必须保留,之后才能最后原子替换生产 `latest.json`。
|
||||||
|
|
||||||
|
若执行 v0.1.0 的已批准分阶段应急路径,首次 macOS Release 可先于 Windows 发布,但 tag/SHA 不得移动;GitHub Release 只保留 DMG、仅覆盖该 DMG 的 checksum 和 GitHub 自动提供的源码归档,并在 Release Notes 中明确标记未签名风险与 Windows pending。updater 包、`.sig`、完整 checksum、provenance 和说明文件必须在私有发布目录留存。Windows 后补且联合 feed 验证通过后,才允许把这些 updater 制品发布到独立匿名 HTTPS 更新源并按上述顺序激活生产 updater feed。
|
||||||
|
|
||||||
发布记录至少包含:
|
发布记录至少包含:
|
||||||
|
|
||||||
@@ -450,6 +454,7 @@ git log --oneline --decorate --graph --all -30
|
|||||||
- [ ] `npm run build` 通过
|
- [ ] `npm run build` 通过
|
||||||
- [ ] `npm run desktop:build:app` 通过
|
- [ ] `npm run desktop:build:app` 通过
|
||||||
- [ ] 正式桌面发布构建已使用 updater 签名私钥执行
|
- [ ] 正式桌面发布构建已使用 updater 签名私钥执行
|
||||||
|
- [ ] 平台签名已验证,或当前精确版本已在 `frontend/desktop-release-policy.json` 获批例外且 Release Notes、私有 `UNSIGNED-PLATFORM` 证据、provenance 和完整 updater checksum 齐备;若 GitHub Release 采用 installer-only profile,公开 checksum 只覆盖公开安装包
|
||||||
- [ ] 数据库迁移与回滚方案确认
|
- [ ] 数据库迁移与回滚方案确认
|
||||||
- [ ] 发布说明完成
|
- [ ] 发布说明完成
|
||||||
- [ ] `main -> release` 合并完成
|
- [ ] `main -> release` 合并完成
|
||||||
|
|||||||
+129
-28
@@ -66,8 +66,8 @@ Manual edits that leave these files inconsistent fail CI.
|
|||||||
## Stabilization Gates
|
## Stabilization Gates
|
||||||
|
|
||||||
Client builds require Node.js 22.13.0 or newer. The development frontend container,
|
Client builds require Node.js 22.13.0 or newer. The development frontend container,
|
||||||
Web CI, macOS release candidates, Windows internal validation, and the production
|
Web CI, macOS and Windows release candidates, Windows internal validation, and the
|
||||||
Web image use the same Node 22.13 baseline.
|
production Web image use the same Node 22.13 baseline.
|
||||||
|
|
||||||
Client release candidates must pass the shared Web checks and the Desktop
|
Client release candidates must pass the shared Web checks and the Desktop
|
||||||
release/security gate before promotion:
|
release/security gate before promotion:
|
||||||
@@ -85,8 +85,13 @@ npm run build
|
|||||||
npm run desktop:build:app
|
npm run desktop:build:app
|
||||||
```
|
```
|
||||||
|
|
||||||
`release:env:check` verifies build channel and commit metadata, and can be
|
`release:env:check` verifies build channel and commit metadata, and becomes
|
||||||
made strict for signed Desktop builds with `REQUIRE_DESKTOP_SIGNING=true`.
|
platform-strict with `REQUIRE_DESKTOP_SIGNING=true` on macOS or
|
||||||
|
`REQUIRE_WINDOWS_SIGNING=true` on Windows. Formal native jobs also set
|
||||||
|
`REQUIRE_UPDATER_SIGNING=true`, `DESKTOP_RELEASE_PLATFORM`, and the
|
||||||
|
version-derived `DESKTOP_PLATFORM_SIGNING_MODE`. The exact-version policy in
|
||||||
|
`frontend/desktop-release-policy.json` is checked with
|
||||||
|
`npm run desktop:release-policy:check`.
|
||||||
`desktop:release:check` statically verifies the Tauri bundle, updater public
|
`desktop:release:check` statically verifies the Tauri bundle, updater public
|
||||||
key, CSP, capability scopes, command allowlist, query-token ban, generic system
|
key, CSP, capability scopes, command allowlist, query-token ban, generic system
|
||||||
notification boundary, CI gate coverage, and secure session token boundary. The
|
notification boundary, CI gate coverage, and secure session token boundary. The
|
||||||
@@ -96,7 +101,7 @@ full manual release, security, regression, and Desktop UX checklist lives in
|
|||||||
## Promotion
|
## Promotion
|
||||||
|
|
||||||
1. Merge feature branches into `dev`.
|
1. Merge feature branches into `dev`.
|
||||||
2. Require the shared client/Web and macOS Desktop CI jobs to pass.
|
2. Require the shared client/Web and macOS/Windows Desktop CI jobs to pass.
|
||||||
3. Promote the accepted scope from `dev` to `main`.
|
3. Promote the accepted scope from `dev` to `main`.
|
||||||
4. Set the release version and complete regression testing on `main`.
|
4. Set the release version and complete regression testing on `main`.
|
||||||
5. Promote `main` to `release`.
|
5. Promote `main` to `release`.
|
||||||
@@ -130,11 +135,11 @@ for `localhost`, `127.0.0.1`, or `::1`.
|
|||||||
The release pipeline must:
|
The release pipeline must:
|
||||||
|
|
||||||
1. build from the accepted release tag and commit;
|
1. build from the accepted release tag and commit;
|
||||||
2. build macOS Universal desktop artifacts;
|
2. build the Universal macOS app/DMG and Windows x64 NSIS installer from that tag;
|
||||||
3. sign and notarize the macOS app;
|
3. use Apple signing/notarization and Windows Authenticode/RFC 3161 signing by default, or use a checked-in exact-version exception that constrains macOS to ad-hoc signing and Windows to Authenticode-unsigned output;
|
||||||
4. produce updater artifacts and `.sig` files with the updater private key;
|
4. produce macOS `.app.tar.gz` and Windows `.nsis.zip` updater artifacts plus their `.sig` files with the shared updater private key;
|
||||||
5. generate `latest.json` and a checksum manifest with `npm run desktop:update-feed:create`;
|
5. generate `DESKTOP-RELEASE-PROVENANCE.json`, one combined `latest.json`, and a checksum manifest;
|
||||||
6. verify the feed with `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>`;
|
6. verify all macOS and Windows entries with `npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64 --require-provenance`;
|
||||||
7. upload immutable artifacts first;
|
7. upload immutable artifacts first;
|
||||||
8. atomically replace `latest.json` last.
|
8. atomically replace `latest.json` last.
|
||||||
|
|
||||||
@@ -142,28 +147,86 @@ For Universal macOS artifacts, `latest.json` must provide both
|
|||||||
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
|
`darwin-aarch64` and `darwin-x86_64` entries pointing at the same Universal
|
||||||
update package.
|
update package.
|
||||||
|
|
||||||
The signed release candidate workflow lives at
|
The same feed must also contain `windows-x86_64`, pointing to the updater-signed
|
||||||
|
NSIS `.nsis.zip` package. The Windows `.exe` installer is distributed next to
|
||||||
|
the updater package but is not used as the updater URL. Tauri updater signing is
|
||||||
|
mandatory in both platform-signed and platform-signing-exception modes.
|
||||||
|
|
||||||
|
The formal release candidate workflow lives at
|
||||||
`.github/workflows/desktop-release-candidate.yml`. It must be run from a
|
`.github/workflows/desktop-release-candidate.yml`. It must be run from a
|
||||||
matching `vX.Y.Z` tag and produces a verified release directory as a GitHub
|
matching `vX.Y.Z` tag and produces a verified release directory as a GitHub
|
||||||
artifact. That artifact is still only a release candidate; the release owner
|
artifact. That artifact is still only a release candidate; the release owner
|
||||||
must upload immutable files to the production download origin and replace
|
must upload immutable files to the production download origin and replace
|
||||||
`latest.json` atomically after validation.
|
`latest.json` atomically after validation.
|
||||||
|
|
||||||
## Windows Build Readiness
|
Platform signing defaults to `signed`. An exception is allowed only when
|
||||||
|
`frontend/desktop-release-policy.json` names the exact product version and
|
||||||
|
records release-owner approval. The current v0.1.0 exception uses macOS ad-hoc
|
||||||
|
signing and unsigned Windows application/installer binaries. Its artifact names
|
||||||
|
and verified updater release directory contain `UNSIGNED-PLATFORM`; the private
|
||||||
|
evidence/update directory must include `UNSIGNED-PLATFORM-RELEASE.txt`,
|
||||||
|
`DESKTOP-RELEASE-PROVENANCE.json`, and the full `SHA256SUMS.txt`. The
|
||||||
|
user-facing GitHub Release may use the exact-version
|
||||||
|
`installer-and-checksum-only` profile, with platform warnings in Release Notes
|
||||||
|
and a separate checksum manifest covering only the displayed installer. This
|
||||||
|
does not establish Apple or Microsoft publisher trust, and Gatekeeper or
|
||||||
|
SmartScreen warnings are expected. Later versions return to the signed default
|
||||||
|
unless separately approved.
|
||||||
|
|
||||||
Second-phase Windows work is limited to CI compatibility validation. A
|
### v0.1.0 staged macOS contingency
|
||||||
`windows-latest` x64 NSIS build may be produced for verification, but it is not
|
|
||||||
a formal deliverable until Windows code signing and release support are
|
|
||||||
approved.
|
|
||||||
|
|
||||||
Windows validation must cover:
|
The release owner approved one additional v0.1.0 contingency for unavailable
|
||||||
|
hosted Actions capacity. A controlled local macOS host may build the macOS
|
||||||
|
ad-hoc artifacts from the immutable final `v0.1.0` tag and publish them first.
|
||||||
|
That initial user-facing GitHub Release contains only the DMG and a
|
||||||
|
`SHA256SUMS.txt` that covers the DMG, in addition to GitHub's automatic source
|
||||||
|
archives. The Release Notes must state that macOS is ad-hoc/not notarized and
|
||||||
|
that Windows remains pending. The macOS updater package and `.sig`, full
|
||||||
|
checksum manifest, provenance, unsigned-platform warning, and Windows-pending
|
||||||
|
notice must be copied to a permission-restricted, Git-ignored private release
|
||||||
|
directory before any visible asset is removed. Windows must later be built from
|
||||||
|
the same tag and SHA.
|
||||||
|
|
||||||
|
The staged macOS release is an installer distribution, not an activated
|
||||||
|
cross-platform updater release. Do not upload or replace production
|
||||||
|
`latest.json` until the Windows `NotSigned` installer/updater has been built and
|
||||||
|
the combined macOS/Windows feed passes full verification. The combined updater
|
||||||
|
artifacts, signatures, provenance, full checksum manifest, and `latest.json`
|
||||||
|
must use a separately configured HTTPS update origin that anonymous production
|
||||||
|
clients can read; a private GitHub Release is not a valid production updater
|
||||||
|
origin. This fallback is limited to v0.1.0 and does not authorize local formal
|
||||||
|
builds for later versions.
|
||||||
|
|
||||||
|
## Windows Release and Internal Validation
|
||||||
|
|
||||||
|
Windows x64 NSIS is an approved formal Desktop target. Formal Windows builds
|
||||||
|
run in `.github/workflows/desktop-release-candidate.yml` from the same exact
|
||||||
|
`vX.Y.Z` tag and SHA as Web and macOS. The default signed path must:
|
||||||
|
|
||||||
|
- import a Base64-encoded PFX from `WINDOWS_CERTIFICATE` using
|
||||||
|
`WINDOWS_CERTIFICATE_PASSWORD`;
|
||||||
|
- configure the imported certificate thumbprint, SHA-256 digest,
|
||||||
|
`WINDOWS_TIMESTAMP_URL`, and RFC 3161 timestamp mode dynamically in CI;
|
||||||
|
- set `REQUIRE_WINDOWS_SIGNING=true` and use the updater signing secrets;
|
||||||
|
- verify the application and installer with `Get-AuthenticodeSignature`,
|
||||||
|
including signer and timestamp certificates;
|
||||||
|
- publish the signed NSIS `.exe`, `.nsis.zip`, and `.nsis.zip.sig` into the
|
||||||
|
combined verified Desktop release directory.
|
||||||
|
|
||||||
|
For an approved exact-version unsigned-platform exception, the Windows job
|
||||||
|
must instead leave the application and installer Authenticode-unsigned, require
|
||||||
|
`Get-AuthenticodeSignature` to return `NotSigned`, append `_UNSIGNED` to the
|
||||||
|
installer and updater artifact names, and still create and verify the updater
|
||||||
|
`.sig`. A certificate secret and timestamp URL are not required in this mode.
|
||||||
|
|
||||||
|
Windows release validation also covers:
|
||||||
|
|
||||||
- WebView2 runtime prerequisite behavior;
|
- WebView2 runtime prerequisite behavior;
|
||||||
- user-level installer assumptions;
|
- user-level installer assumptions;
|
||||||
- Windows Credential Manager session storage;
|
- Windows Credential Manager session storage;
|
||||||
- path handling and temporary file cleanup;
|
- path handling and temporary file cleanup;
|
||||||
- notification and updater compilation;
|
- notification and updater compilation;
|
||||||
- future code-signing requirements.
|
- code-signing trust, timestamp validity, and updater signature verification.
|
||||||
|
|
||||||
The manual internal Windows validation workflow lives at
|
The manual internal Windows validation workflow lives at
|
||||||
`.github/workflows/desktop-windows-internal.yml`. It is `workflow_dispatch`
|
`.github/workflows/desktop-windows-internal.yml`. It is `workflow_dispatch`
|
||||||
@@ -172,8 +235,39 @@ only, runs on `windows-latest`, injects `VITE_BUILD_CHANNEL` and
|
|||||||
and Desktop safety gates, builds an unsigned NSIS installer with updater
|
and Desktop safety gates, builds an unsigned NSIS installer with updater
|
||||||
artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub
|
artifacts disabled, and uploads the `.exe` plus `SHA256SUMS.txt` as a GitHub
|
||||||
Actions artifact. This workflow is for internal compatibility verification
|
Actions artifact. This workflow is for internal compatibility verification
|
||||||
only; it must not generate `latest.json`, update feeds, signed release
|
only; it must not generate `latest.json`, update feeds, verified release
|
||||||
directories, or formal Windows release artifacts.
|
directories, or formal Windows release artifacts. A successful internal build
|
||||||
|
does not substitute for the tag-only formal workflow, including when that
|
||||||
|
formal workflow uses an approved platform-signing exception.
|
||||||
|
|
||||||
|
The formal workflow always requires these organization settings:
|
||||||
|
|
||||||
|
- secrets: `TAURI_SIGNING_PRIVATE_KEY` and
|
||||||
|
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD`;
|
||||||
|
- client default CTMS origin: repository variable `VITE_DESKTOP_SERVER_URL`;
|
||||||
|
- shared versioned HTTPS artifact prefix: `DESKTOP_UPDATE_BASE_URL` or the
|
||||||
|
manual workflow input.
|
||||||
|
|
||||||
|
`VITE_DESKTOP_SERVER_URL` must be an HTTPS origin without credentials, path,
|
||||||
|
query, or fragment. Vite embeds it as the Desktop client's first-run default;
|
||||||
|
the runtime source must not contain the production hostname. A user can still
|
||||||
|
override this default in Desktop server settings, where the persisted manual
|
||||||
|
value takes precedence and switching origins clears the existing session and
|
||||||
|
server-scoped client state. Do not reuse this value as
|
||||||
|
`DESKTOP_UPDATE_BASE_URL`: the latter points to immutable updater artifacts,
|
||||||
|
not the CTMS business API.
|
||||||
|
|
||||||
|
The default signed path additionally requires `WINDOWS_CERTIFICATE`,
|
||||||
|
`WINDOWS_CERTIFICATE_PASSWORD`, and `WINDOWS_TIMESTAMP_URL`, plus the Apple
|
||||||
|
credentials documented by the release owner. The v0.1.0 exception does not
|
||||||
|
require those platform certificate settings.
|
||||||
|
|
||||||
|
The checked-in workflow implements the exportable PFX path. Confirm that the
|
||||||
|
organization's certificate policy permits an exportable CI certificate before
|
||||||
|
provisioning it. If the selected CA provides only hardware- or cloud-backed
|
||||||
|
keys, replace the PFX import with a reviewed Tauri `signCommand` integration
|
||||||
|
(for example, the organization's Azure signing provider) while retaining the
|
||||||
|
same Authenticode and timestamp verification gates.
|
||||||
|
|
||||||
## Required Checks
|
## Required Checks
|
||||||
|
|
||||||
@@ -183,6 +277,7 @@ npm ci
|
|||||||
npm run version:check
|
npm run version:check
|
||||||
export VITE_BUILD_CHANNEL=release
|
export VITE_BUILD_CHANNEL=release
|
||||||
export VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
|
export VITE_BUILD_COMMIT="$(git rev-parse HEAD)"
|
||||||
|
export VITE_DESKTOP_SERVER_URL="https://ctms.example.com"
|
||||||
npm run release:env:check
|
npm run release:env:check
|
||||||
npm run runtime:check
|
npm run runtime:check
|
||||||
npm run desktop:release:check
|
npm run desktop:release:check
|
||||||
@@ -193,15 +288,21 @@ npm run build
|
|||||||
npm run desktop:build:app
|
npm run desktop:build:app
|
||||||
export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
|
export TAURI_SIGNING_PRIVATE_KEY="$UPDATER_PRIVATE_KEY"
|
||||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
|
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$UPDATER_PRIVATE_KEY_PASSWORD"
|
||||||
export REQUIRE_DESKTOP_SIGNING=true
|
export REQUIRE_UPDATER_SIGNING=true
|
||||||
|
export DESKTOP_RELEASE_PLATFORM=macos
|
||||||
|
export DESKTOP_PLATFORM_SIGNING_MODE=unsigned-exception
|
||||||
|
export ALLOW_UNSIGNED_PLATFORM_RELEASE=true
|
||||||
npm run release:env:check
|
npm run release:env:check
|
||||||
npm run desktop:release-readiness:check
|
npm run desktop:release-readiness:check
|
||||||
npm run desktop:build:macos-release -- --ci
|
npm run desktop:build:macos-unsigned-release -- --ci
|
||||||
npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>
|
npm run desktop:update-feed:create -- --artifact <CTMS.app.tar.gz> --platform-artifact windows-x86_64=<CTMS.nsis.zip> --include <CTMS.dmg> --include <CTMS-installer.exe> --base-url <versioned-https-artifact-prefix> --output-dir <release-dir>
|
||||||
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir>
|
npm run desktop:update-feed:check -- --feed <release-dir>/latest.json --artifacts-dir <release-dir> --require-platform windows-x86_64 --require-provenance
|
||||||
```
|
```
|
||||||
|
|
||||||
The Desktop build must run on macOS for the current first-phase target. A signed
|
The macOS and Windows builds run in their native CI jobs and always use the same
|
||||||
or notarized public release additionally requires the Apple credentials defined
|
updater signing key. In the default path, macOS requires Apple
|
||||||
by the release owner. A formal second-phase desktop release also requires the
|
signing/notarization credentials and Windows requires the PFX/password and
|
||||||
updater signing key; unsigned internal builds are not formal distributions.
|
timestamp URL. In the approved v0.1.0 exception, macOS must verify
|
||||||
|
`Signature=adhoc`, Windows must verify `NotSigned`, and both must carry explicit
|
||||||
|
platform-trust warnings. Native artifacts are aggregated only after both jobs
|
||||||
|
and the combined updater feed pass.
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
VITE_RUNTIME_ENV=production
|
VITE_RUNTIME_ENV=production
|
||||||
VITE_ALLOW_INSECURE_DEV_LOGIN=false
|
VITE_ALLOW_INSECURE_DEV_LOGIN=false
|
||||||
VITE_DEV_API_PROXY_TARGET=http://backend:8000
|
VITE_DEV_API_PROXY_TARGET=http://backend:8000
|
||||||
|
# Default CTMS origin embedded in Desktop builds; users can override it in Desktop settings.
|
||||||
|
VITE_DESKTOP_SERVER_URL=https://ctms.example.com
|
||||||
# Set to 8888 when Vite HMR is accessed through the Docker nginx dev entry.
|
# Set to 8888 when Vite HMR is accessed through the Docker nginx dev entry.
|
||||||
VITE_HMR_CLIENT_PORT=
|
VITE_HMR_CLIENT_PORT=
|
||||||
VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3
|
VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"defaultPlatformSigningMode": "signed",
|
||||||
|
"updaterSigningRequired": true,
|
||||||
|
"unsignedPlatformExceptions": [
|
||||||
|
{
|
||||||
|
"version": "0.1.0",
|
||||||
|
"status": "approved",
|
||||||
|
"macos": "ad-hoc",
|
||||||
|
"windows": "unsigned",
|
||||||
|
"distribution": "controlled",
|
||||||
|
"macosLocalExactTagFallback": true,
|
||||||
|
"windowsDelivery": "deferred-same-tag",
|
||||||
|
"updaterFeedActivation": "after-combined-platform-verification",
|
||||||
|
"githubReleaseAssetProfile": "installer-and-checksum-only",
|
||||||
|
"stagedEvidenceRetention": "private-until-updater-publish",
|
||||||
|
"updaterArtifactPublishTarget": "anonymous-https-origin",
|
||||||
|
"approvedOn": "2026-07-17",
|
||||||
|
"reason": "The v0.1.0 release owner cannot currently provide Apple Developer signing/notarization credentials or an organization Windows code-signing certificate, and hosted Windows build capacity is temporarily unavailable."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
CTMS DESKTOP PLATFORM SIGNING WARNING
|
||||||
|
|
||||||
|
This release is distributed under the version-scoped v0.1.0 platform-signing exception.
|
||||||
|
|
||||||
|
- The macOS application uses an ad-hoc signature and is not Apple-notarized.
|
||||||
|
- The Windows application and installer have no Authenticode signature or RFC 3161 timestamp.
|
||||||
|
- macOS Gatekeeper and Windows SmartScreen warnings are expected.
|
||||||
|
- Tauri updater artifacts remain cryptographically signed and must pass updater feed verification.
|
||||||
|
- Platform availability may be staged; production latest.json remains withheld until macOS and Windows artifacts from the same tag pass combined verification.
|
||||||
|
- Install only after verifying the release tag, source commit, SHA256SUMS.txt, and DESKTOP-RELEASE-PROVENANCE.json through a trusted channel.
|
||||||
|
|
||||||
|
This exception does not apply to later versions unless they are separately approved in desktop-release-policy.json.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CTMS v0.1.0 WINDOWS RELEASE PENDING
|
||||||
|
|
||||||
|
This GitHub Release is the approved first stage of the v0.1.0 Desktop distribution.
|
||||||
|
|
||||||
|
- macOS Universal artifacts are available and use an ad-hoc platform signature.
|
||||||
|
- Windows x64 NSIS artifacts are not yet available.
|
||||||
|
- Windows must later be built from the same immutable v0.1.0 tag and Git commit.
|
||||||
|
- Production latest.json is intentionally withheld until Windows NotSigned artifacts and the combined updater feed pass verification.
|
||||||
|
- Do not interpret this staged release as a complete cross-platform automatic-update release.
|
||||||
Generated
+316
-187
@@ -1001,9 +1001,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
|
||||||
"integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==",
|
"integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -1015,9 +1015,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
|
||||||
"integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==",
|
"integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1028,10 +1028,24 @@
|
|||||||
"android"
|
"android"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.62.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
|
||||||
|
"integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
|
||||||
"integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==",
|
"integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1043,9 +1057,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
|
||||||
"integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==",
|
"integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1057,9 +1071,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
|
||||||
"integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==",
|
"integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1071,13 +1085,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
|
||||||
"integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==",
|
"integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1085,13 +1102,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
|
||||||
"integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==",
|
"integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1099,13 +1119,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==",
|
"integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1113,11 +1136,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
|
||||||
|
"integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1125,13 +1153,33 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==",
|
"integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||||
|
"version": "4.62.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
|
||||||
|
"integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1139,13 +1187,33 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==",
|
"integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||||
|
"version": "4.62.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
|
||||||
|
"integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1153,13 +1221,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==",
|
"integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1167,13 +1238,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
|
||||||
"integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==",
|
"integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1181,13 +1255,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==",
|
"integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1195,13 +1272,16 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==",
|
"integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1209,9 +1289,26 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
|
||||||
"integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==",
|
"integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||||
|
"version": "4.62.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
|
||||||
|
"integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1219,13 +1316,13 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"openbsd"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
|
||||||
"integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==",
|
"integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1237,9 +1334,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
|
||||||
"integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==",
|
"integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1251,9 +1348,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
|
||||||
"integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==",
|
"integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1265,9 +1362,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
|
||||||
"integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==",
|
"integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1279,9 +1376,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
|
||||||
"integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==",
|
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1589,7 +1686,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||||
|
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -1634,15 +1733,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz",
|
||||||
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
|
"integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1651,13 +1750,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz",
|
||||||
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
"integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.17"
|
"magic-string": "^0.30.17"
|
||||||
},
|
},
|
||||||
@@ -1688,9 +1787,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz",
|
||||||
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
|
"integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1701,13 +1800,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz",
|
||||||
"integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
|
"integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"strip-literal": "^3.0.0"
|
"strip-literal": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -1716,13 +1815,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz",
|
||||||
"integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
|
"integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.4",
|
"@vitest/pretty-format": "3.2.7",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -1731,9 +1830,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz",
|
||||||
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
|
"integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1744,13 +1843,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz",
|
||||||
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
|
"integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.2.4",
|
"@vitest/pretty-format": "3.2.7",
|
||||||
"loupe": "^3.1.4",
|
"loupe": "^3.1.4",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -2002,12 +2101,40 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.2",
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.16.0",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -2181,7 +2308,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -2239,21 +2365,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/echarts": {
|
"node_modules/echarts": {
|
||||||
"version": "6.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
|
||||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.3.0",
|
"tslib": "2.3.0",
|
||||||
"zrender": "6.0.0"
|
"zrender": "6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/echarts/node_modules/tslib": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
|
||||||
"license": "0BSD"
|
|
||||||
},
|
|
||||||
"node_modules/editorconfig": {
|
"node_modules/editorconfig": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.7.tgz",
|
||||||
@@ -2432,7 +2552,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -2467,14 +2589,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.4",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -2591,7 +2715,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -2781,11 +2907,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash-unified": {
|
"node_modules/lodash-unified": {
|
||||||
@@ -2876,7 +3006,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/muggle-string": {
|
"node_modules/muggle-string": {
|
||||||
@@ -2887,7 +3016,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
|
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3030,9 +3161,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3063,7 +3194,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
||||||
|
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -3080,7 +3213,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -3096,8 +3229,13 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.0",
|
||||||
"license": "MIT"
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
@@ -3110,11 +3248,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.53.5",
|
"version": "4.62.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
|
||||||
|
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.9"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
@@ -3124,45 +3264,34 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.53.5",
|
"@rollup/rollup-android-arm-eabi": "4.62.2",
|
||||||
"@rollup/rollup-android-arm64": "4.53.5",
|
"@rollup/rollup-android-arm64": "4.62.2",
|
||||||
"@rollup/rollup-darwin-arm64": "4.53.5",
|
"@rollup/rollup-darwin-arm64": "4.62.2",
|
||||||
"@rollup/rollup-darwin-x64": "4.53.5",
|
"@rollup/rollup-darwin-x64": "4.62.2",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.53.5",
|
"@rollup/rollup-freebsd-arm64": "4.62.2",
|
||||||
"@rollup/rollup-freebsd-x64": "4.53.5",
|
"@rollup/rollup-freebsd-x64": "4.62.2",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.53.5",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.53.5",
|
"@rollup/rollup-linux-arm-musleabihf": "4.62.2",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.53.5",
|
"@rollup/rollup-linux-arm64-gnu": "4.62.2",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.53.5",
|
"@rollup/rollup-linux-arm64-musl": "4.62.2",
|
||||||
"@rollup/rollup-linux-loong64-gnu": "4.53.5",
|
"@rollup/rollup-linux-loong64-gnu": "4.62.2",
|
||||||
"@rollup/rollup-linux-ppc64-gnu": "4.53.5",
|
"@rollup/rollup-linux-loong64-musl": "4.62.2",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.53.5",
|
"@rollup/rollup-linux-ppc64-gnu": "4.62.2",
|
||||||
"@rollup/rollup-linux-riscv64-musl": "4.53.5",
|
"@rollup/rollup-linux-ppc64-musl": "4.62.2",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.53.5",
|
"@rollup/rollup-linux-riscv64-gnu": "4.62.2",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.53.5",
|
"@rollup/rollup-linux-riscv64-musl": "4.62.2",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.53.5",
|
"@rollup/rollup-linux-s390x-gnu": "4.62.2",
|
||||||
"@rollup/rollup-openharmony-arm64": "4.53.5",
|
"@rollup/rollup-linux-x64-gnu": "4.62.2",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.53.5",
|
"@rollup/rollup-linux-x64-musl": "4.62.2",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.53.5",
|
"@rollup/rollup-openbsd-x64": "4.62.2",
|
||||||
"@rollup/rollup-win32-x64-gnu": "4.53.5",
|
"@rollup/rollup-openharmony-arm64": "4.62.2",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.53.5",
|
"@rollup/rollup-win32-arm64-msvc": "4.62.2",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.62.2",
|
||||||
|
"@rollup/rollup-win32-x64-gnu": "4.62.2",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.62.2",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
|
|
||||||
"version": "4.53.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz",
|
|
||||||
"integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/rrweb-cssom": {
|
"node_modules/rrweb-cssom": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
@@ -3498,6 +3627,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
@@ -3516,13 +3651,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.3.1",
|
"version": "7.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
@@ -3614,20 +3749,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz",
|
||||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
"integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.7",
|
||||||
"@vitest/mocker": "3.2.4",
|
"@vitest/mocker": "3.2.7",
|
||||||
"@vitest/pretty-format": "^3.2.4",
|
"@vitest/pretty-format": "^3.2.7",
|
||||||
"@vitest/runner": "3.2.4",
|
"@vitest/runner": "3.2.7",
|
||||||
"@vitest/snapshot": "3.2.4",
|
"@vitest/snapshot": "3.2.7",
|
||||||
"@vitest/spy": "3.2.4",
|
"@vitest/spy": "3.2.7",
|
||||||
"@vitest/utils": "3.2.4",
|
"@vitest/utils": "3.2.7",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"expect-type": "^1.2.1",
|
"expect-type": "^1.2.1",
|
||||||
@@ -3657,8 +3792,8 @@
|
|||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"@vitest/browser": "3.2.4",
|
"@vitest/browser": "3.2.7",
|
||||||
"@vitest/ui": "3.2.4",
|
"@vitest/ui": "3.2.7",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
@@ -3976,9 +4111,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.19.0",
|
"version": "8.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4015,19 +4150,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/zrender": {
|
"node_modules/zrender": {
|
||||||
"version": "6.0.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
|
||||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.3.0"
|
"tslib": "2.3.0"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/zrender/node_modules/tslib": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
|
||||||
"license": "0BSD"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
"desktop:build": "tauri build",
|
"desktop:build": "tauri build",
|
||||||
"desktop:build:app": "tauri build --config '{\"bundle\":{\"createUpdaterArtifacts\":false}}' --bundles app",
|
"desktop:build:app": "tauri build --config '{\"bundle\":{\"createUpdaterArtifacts\":false}}' --bundles app",
|
||||||
"desktop:build:macos-release": "tauri build --target universal-apple-darwin --bundles app,dmg",
|
"desktop:build:macos-release": "tauri build --target universal-apple-darwin --bundles app,dmg",
|
||||||
|
"desktop:build:macos-unsigned-release": "tauri build --target universal-apple-darwin --bundles app,dmg --config '{\"bundle\":{\"macOS\":{\"signingIdentity\":\"-\"}}}'",
|
||||||
|
"desktop:build:windows-release": "tauri build --bundles nsis",
|
||||||
|
"desktop:release-policy:check": "node scripts/resolve-desktop-release-policy.mjs",
|
||||||
|
"desktop:release-provenance:create": "node scripts/create-desktop-release-provenance.mjs",
|
||||||
"desktop:update-feed:create": "node scripts/create-desktop-update-feed.mjs",
|
"desktop:update-feed:create": "node scripts/create-desktop-update-feed.mjs",
|
||||||
"desktop:bundle:dmg": "tauri build --bundles dmg",
|
"desktop:bundle:dmg": "tauri build --bundles dmg",
|
||||||
"desktop:update-feed:check": "node scripts/verify-desktop-update-feed.mjs",
|
"desktop:update-feed:check": "node scripts/verify-desktop-update-feed.mjs",
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
||||||
|
|
||||||
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const value = (name) => {
|
||||||
|
const index = args.indexOf(name);
|
||||||
|
return index >= 0 ? args[index + 1] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const policy = await loadDesktopReleasePolicy();
|
||||||
|
const resolution = resolveDesktopReleasePolicy(packageInfo.version, policy);
|
||||||
|
const tag = value("--tag") || process.env.GITHUB_REF_NAME;
|
||||||
|
const commit = value("--commit") || process.env.GITHUB_SHA;
|
||||||
|
const macosSigning = value("--macos-signing");
|
||||||
|
const windowsSigning = value("--windows-signing");
|
||||||
|
const releaseStage = value("--stage") || "complete";
|
||||||
|
const outputPath = resolve(
|
||||||
|
frontendDir,
|
||||||
|
value("--output") || "src-tauri/target/desktop-release-feed/DESKTOP-RELEASE-PROVENANCE.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
const failures = [];
|
||||||
|
const assert = (condition, message) => {
|
||||||
|
if (!condition) failures.push(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
assert(tag === `v${packageInfo.version}`, `Release provenance tag must be v${packageInfo.version}.`);
|
||||||
|
assert(/^[0-9a-f]{40}$/i.test(commit || ""), "Release provenance commit must be a full 40-character SHA.");
|
||||||
|
assert(macosSigning === resolution.macosSigning, `macOS signing mode must be ${resolution.macosSigning}.`);
|
||||||
|
assert(windowsSigning === resolution.windowsSigning, `Windows signing mode must be ${resolution.windowsSigning}.`);
|
||||||
|
assert(["complete", "macos-first"].includes(releaseStage), "Release provenance stage must be complete or macos-first.");
|
||||||
|
if (releaseStage === "macos-first") {
|
||||||
|
assert(
|
||||||
|
resolution.macosLocalExactTagFallback === true && resolution.windowsDelivery === "deferred-same-tag",
|
||||||
|
`v${packageInfo.version} does not approve a staged local macOS-first release.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
throw new Error(`Desktop release provenance creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provenance = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
product: "CTMS Desktop",
|
||||||
|
version: packageInfo.version,
|
||||||
|
tag,
|
||||||
|
commit,
|
||||||
|
platformSigningMode: resolution.platformSigningMode,
|
||||||
|
platformSigning: {
|
||||||
|
macos: macosSigning,
|
||||||
|
windows: windowsSigning,
|
||||||
|
},
|
||||||
|
updaterSigning: "required-and-verified",
|
||||||
|
artifactLabel: resolution.artifactLabel,
|
||||||
|
distribution: resolution.distribution,
|
||||||
|
stage: releaseStage,
|
||||||
|
platformAvailability:
|
||||||
|
releaseStage === "macos-first"
|
||||||
|
? { macos: "available", windows: "pending-same-tag" }
|
||||||
|
: { macos: "available", windows: "available" },
|
||||||
|
updaterFeedActivation:
|
||||||
|
releaseStage === "macos-first" ? "withheld-pending-combined-verification" : "combined-platform-verification-required",
|
||||||
|
warnings: resolution.warningRequired
|
||||||
|
? [
|
||||||
|
"macOS is ad-hoc signed and not Apple-notarized; Gatekeeper warnings are expected.",
|
||||||
|
"Windows is not Authenticode-signed or RFC 3161 timestamped; SmartScreen warnings are expected.",
|
||||||
|
"Verify the release tag, commit, updater signatures, and SHA256SUMS.txt through a trusted channel before installation.",
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await mkdir(dirname(outputPath), { recursive: true });
|
||||||
|
await writeFile(outputPath, `${JSON.stringify(provenance, null, 2)}\n`);
|
||||||
|
console.log(`Desktop release provenance created at ${outputPath}`);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||||
import { basename, resolve } from "node:path";
|
import { basename, resolve } from "node:path";
|
||||||
import { createHash } from "node:crypto";
|
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
@@ -25,7 +25,7 @@ const assert = (condition, message) => {
|
|||||||
if (!condition) fail(message);
|
if (!condition) fail(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
const artifactPath = value("--artifact") || process.env.DESKTOP_UPDATE_ARTIFACT;
|
const macosArtifactPath = value("--artifact") || process.env.DESKTOP_UPDATE_ARTIFACT;
|
||||||
const outputDir = resolve(
|
const outputDir = resolve(
|
||||||
frontendDir,
|
frontendDir,
|
||||||
value("--output-dir") || process.env.DESKTOP_UPDATE_OUTPUT_DIR || "src-tauri/target/release/desktop-update-feed",
|
value("--output-dir") || process.env.DESKTOP_UPDATE_OUTPUT_DIR || "src-tauri/target/release/desktop-update-feed",
|
||||||
@@ -34,18 +34,40 @@ const baseUrlRaw = value("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
|||||||
const pubDate = value("--date") || process.env.DESKTOP_UPDATE_PUB_DATE || new Date().toISOString();
|
const pubDate = value("--date") || process.env.DESKTOP_UPDATE_PUB_DATE || new Date().toISOString();
|
||||||
const notes = value("--notes") || process.env.DESKTOP_UPDATE_NOTES;
|
const notes = value("--notes") || process.env.DESKTOP_UPDATE_NOTES;
|
||||||
const includes = values("--include").map((path) => resolve(frontendDir, path));
|
const includes = values("--include").map((path) => resolve(frontendDir, path));
|
||||||
|
const platformArtifactSpecs = values("--platform-artifact");
|
||||||
|
const platformPattern = /^(?:darwin|windows|linux)-(?:x86_64|aarch64|i686|armv7)$/;
|
||||||
|
|
||||||
assert(Boolean(artifactPath), "--artifact or DESKTOP_UPDATE_ARTIFACT is required.");
|
assert(Boolean(macosArtifactPath), "--artifact or DESKTOP_UPDATE_ARTIFACT is required for the Universal macOS updater artifact.");
|
||||||
assert(Boolean(baseUrlRaw), "--base-url or DESKTOP_UPDATE_BASE_URL is required.");
|
assert(Boolean(baseUrlRaw), "--base-url or DESKTOP_UPDATE_BASE_URL is required.");
|
||||||
|
|
||||||
const resolvedArtifactPath = artifactPath ? resolve(frontendDir, artifactPath) : undefined;
|
const platformArtifacts = new Map();
|
||||||
const artifactName = resolvedArtifactPath ? basename(resolvedArtifactPath) : undefined;
|
if (macosArtifactPath) {
|
||||||
const signaturePath = resolvedArtifactPath ? `${resolvedArtifactPath}.sig` : undefined;
|
const resolvedMacosArtifact = resolve(frontendDir, macosArtifactPath);
|
||||||
const uploadFiles = [];
|
platformArtifacts.set("darwin-aarch64", resolvedMacosArtifact);
|
||||||
|
platformArtifacts.set("darwin-x86_64", resolvedMacosArtifact);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const spec of platformArtifactSpecs) {
|
||||||
|
const separator = spec.indexOf("=");
|
||||||
|
if (separator <= 0 || separator === spec.length - 1) {
|
||||||
|
fail(`--platform-artifact must use <os>-<arch>=<path>: ${spec}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = spec.slice(0, separator);
|
||||||
|
const path = spec.slice(separator + 1);
|
||||||
|
assert(platformPattern.test(platform), `Unsupported updater platform key: ${platform}`);
|
||||||
|
assert(!platformArtifacts.has(platform), `Updater platform is configured more than once: ${platform}`);
|
||||||
|
if (platformPattern.test(platform) && !platformArtifacts.has(platform)) {
|
||||||
|
platformArtifacts.set(platform, resolve(frontendDir, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const readRequiredFile = async (path, description) => {
|
const readRequiredFile = async (path, description) => {
|
||||||
try {
|
try {
|
||||||
return await readFile(path);
|
const data = await readFile(path);
|
||||||
|
assert(data.length > 0, `${description} must not be empty: ${path}`);
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
fail(`${description} cannot be read: ${path} (${error.message})`);
|
fail(`${description} cannot be read: ${path} (${error.message})`);
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -72,20 +94,16 @@ const normalizedBaseUrl = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyIntoOutput = async (sourcePath) => {
|
const artifactSignatures = new Map();
|
||||||
const destination = resolve(outputDir, basename(sourcePath));
|
const uniqueArtifactPaths = [...new Set(platformArtifacts.values())];
|
||||||
if (sourcePath !== destination) {
|
for (const artifactPath of uniqueArtifactPaths) {
|
||||||
await copyFile(sourcePath, destination);
|
const signaturePath = `${artifactPath}.sig`;
|
||||||
}
|
await readRequiredFile(artifactPath, "Updater artifact");
|
||||||
return destination;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (resolvedArtifactPath && signaturePath) {
|
|
||||||
await readRequiredFile(resolvedArtifactPath, "Updater artifact");
|
|
||||||
const signature = await readRequiredFile(signaturePath, "Updater artifact signature");
|
const signature = await readRequiredFile(signaturePath, "Updater artifact signature");
|
||||||
if (signature) {
|
if (signature) {
|
||||||
const signatureText = signature.toString("utf8").trim();
|
const signatureText = signature.toString("utf8").trim();
|
||||||
assert(signatureText.length > 80, "Updater artifact signature is unexpectedly short.");
|
assert(signatureText.length > 80, `Updater artifact signature is unexpectedly short: ${signaturePath}`);
|
||||||
|
artifactSignatures.set(artifactPath, signatureText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,56 +116,75 @@ for (const includePath of includes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const [platform, artifactPath] of platformArtifacts) {
|
||||||
|
if (platform.startsWith("darwin-")) {
|
||||||
|
assert(artifactPath.endsWith(".app.tar.gz"), `${platform} must use a macOS .app.tar.gz updater artifact.`);
|
||||||
|
}
|
||||||
|
if (platform === "windows-x86_64") {
|
||||||
|
assert(artifactPath.endsWith(".nsis.zip"), "windows-x86_64 must use an NSIS .nsis.zip updater artifact.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadSources = [
|
||||||
|
...uniqueArtifactPaths.flatMap((path) => [path, `${path}.sig`]),
|
||||||
|
...includes,
|
||||||
|
];
|
||||||
|
const sourceByName = new Map();
|
||||||
|
for (const sourcePath of uploadSources) {
|
||||||
|
const name = basename(sourcePath);
|
||||||
|
const existing = sourceByName.get(name);
|
||||||
|
assert(!existing || existing === sourcePath, `Release files must have unique basenames: ${name}`);
|
||||||
|
sourceByName.set(name, sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = normalizedBaseUrl();
|
const baseUrl = normalizedBaseUrl();
|
||||||
|
|
||||||
if (failures.length === 0 && resolvedArtifactPath && signaturePath && artifactName && baseUrl) {
|
if (failures.length === 0 && baseUrl) {
|
||||||
await mkdir(outputDir, { recursive: true });
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
const artifactUrl = new URL(artifactName, baseUrl).toString();
|
const platforms = {};
|
||||||
|
for (const [platform, artifactPath] of platformArtifacts) {
|
||||||
|
const artifactUrl = new URL(basename(artifactPath), baseUrl).toString();
|
||||||
assert(!artifactUrl.endsWith("/latest.json"), "Updater artifact URL must not point at latest.json.");
|
assert(!artifactUrl.endsWith("/latest.json"), "Updater artifact URL must not point at latest.json.");
|
||||||
assert(!/[?&]token=/i.test(new URL(artifactUrl).search), "Updater artifact URL must not include token query parameters.");
|
assert(!/[?&]token=/i.test(new URL(artifactUrl).search), "Updater artifact URL must not include token query parameters.");
|
||||||
|
platforms[platform] = {
|
||||||
|
signature: artifactSignatures.get(artifactPath),
|
||||||
|
url: artifactUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const signature = (await readFile(signaturePath, "utf8")).trim();
|
|
||||||
const latest = {
|
const latest = {
|
||||||
version: packageInfo.version,
|
version: packageInfo.version,
|
||||||
pub_date: pubDate,
|
pub_date: pubDate,
|
||||||
platforms: {
|
platforms,
|
||||||
"darwin-aarch64": {
|
|
||||||
signature,
|
|
||||||
url: artifactUrl,
|
|
||||||
},
|
|
||||||
"darwin-x86_64": {
|
|
||||||
signature,
|
|
||||||
url: artifactUrl,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notes) {
|
if (notes) {
|
||||||
latest.notes = notes;
|
latest.notes = notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFiles.push(await copyIntoOutput(resolvedArtifactPath));
|
const copiedFiles = [];
|
||||||
uploadFiles.push(await copyIntoOutput(signaturePath));
|
for (const sourcePath of sourceByName.values()) {
|
||||||
for (const includePath of includes) {
|
const destination = resolve(outputDir, basename(sourcePath));
|
||||||
uploadFiles.push(await copyIntoOutput(includePath));
|
if (sourcePath !== destination) {
|
||||||
|
await copyFile(sourcePath, destination);
|
||||||
|
}
|
||||||
|
copiedFiles.push(destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestPath = resolve(outputDir, "latest.json");
|
const latestPath = resolve(outputDir, "latest.json");
|
||||||
await writeFile(latestPath, `${JSON.stringify(latest, null, 2)}\n`);
|
await writeFile(latestPath, `${JSON.stringify(latest, null, 2)}\n`);
|
||||||
uploadFiles.push(latestPath);
|
copiedFiles.push(latestPath);
|
||||||
|
|
||||||
const uniqueFiles = [...new Map(uploadFiles.map((path) => [basename(path), path])).values()];
|
|
||||||
const checksumLines = [];
|
const checksumLines = [];
|
||||||
for (const filePath of uniqueFiles) {
|
for (const filePath of copiedFiles) {
|
||||||
checksumLines.push(`${await sha256(filePath)} ${basename(filePath)}`);
|
checksumLines.push(`${await sha256(filePath)} ${basename(filePath)}`);
|
||||||
}
|
}
|
||||||
const checksumPath = resolve(outputDir, "SHA256SUMS.txt");
|
const checksumPath = resolve(outputDir, "SHA256SUMS.txt");
|
||||||
await writeFile(checksumPath, `${checksumLines.join("\n")}\n`);
|
await writeFile(checksumPath, `${checksumLines.join("\n")}\n`);
|
||||||
|
|
||||||
console.log(`Desktop update feed created in ${outputDir}`);
|
console.log(`Desktop update feed created in ${outputDir}`);
|
||||||
console.log(` - ${uniqueFiles.map((path) => basename(path)).join("\n - ")}`);
|
console.log(` - ${copiedFiles.map((path) => basename(path)).join("\n - ")}`);
|
||||||
console.log(` - SHA256SUMS.txt`);
|
console.log(" - SHA256SUMS.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
|
export const desktopReleasePolicyPath = resolve(frontendDir, "desktop-release-policy.json");
|
||||||
|
|
||||||
|
const semverPattern = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
||||||
|
|
||||||
|
const validatePolicy = (policy) => {
|
||||||
|
const failures = [];
|
||||||
|
const assert = (condition, message) => {
|
||||||
|
if (!condition) failures.push(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
assert(policy?.schemaVersion === 1, "desktop release policy schemaVersion must be 1.");
|
||||||
|
assert(
|
||||||
|
policy?.defaultPlatformSigningMode === "signed",
|
||||||
|
"desktop release policy must default platform signing to signed.",
|
||||||
|
);
|
||||||
|
assert(policy?.updaterSigningRequired === true, "desktop release policy must always require updater signing.");
|
||||||
|
assert(
|
||||||
|
Array.isArray(policy?.unsignedPlatformExceptions),
|
||||||
|
"desktop release policy unsignedPlatformExceptions must be an array.",
|
||||||
|
);
|
||||||
|
|
||||||
|
const versions = new Set();
|
||||||
|
for (const exception of policy?.unsignedPlatformExceptions || []) {
|
||||||
|
const prefix = `unsigned platform exception ${exception?.version || "<missing>"}`;
|
||||||
|
assert(semverPattern.test(exception?.version || ""), `${prefix} must use an exact X.Y.Z version.`);
|
||||||
|
assert(!versions.has(exception?.version), `${prefix} is duplicated.`);
|
||||||
|
versions.add(exception?.version);
|
||||||
|
assert(exception?.status === "approved", `${prefix} must have status approved.`);
|
||||||
|
assert(exception?.macos === "ad-hoc", `${prefix} must constrain macOS to ad-hoc signing.`);
|
||||||
|
assert(exception?.windows === "unsigned", `${prefix} must constrain Windows to unsigned.`);
|
||||||
|
assert(exception?.distribution === "controlled", `${prefix} must constrain distribution to controlled.`);
|
||||||
|
if (exception?.version === "0.1.0") {
|
||||||
|
assert(exception?.macosLocalExactTagFallback === true, `${prefix} must explicitly approve the local exact-tag macOS fallback.`);
|
||||||
|
assert(exception?.windowsDelivery === "deferred-same-tag", `${prefix} must defer Windows only from the same tag.`);
|
||||||
|
assert(
|
||||||
|
exception?.updaterFeedActivation === "after-combined-platform-verification",
|
||||||
|
`${prefix} must withhold the production updater feed until both platforms are verified.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
exception?.githubReleaseAssetProfile === "installer-and-checksum-only",
|
||||||
|
`${prefix} must keep the user-facing GitHub Release limited to installers and their checksum manifest.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
exception?.stagedEvidenceRetention === "private-until-updater-publish",
|
||||||
|
`${prefix} must retain updater artifacts and release evidence privately until updater publication.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
exception?.updaterArtifactPublishTarget === "anonymous-https-origin",
|
||||||
|
`${prefix} must publish updater artifacts to an anonymous HTTPS origin.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert(/^\d{4}-\d{2}-\d{2}$/.test(exception?.approvedOn || ""), `${prefix} must record an approval date.`);
|
||||||
|
assert(
|
||||||
|
typeof exception?.reason === "string" && exception.reason.trim().length >= 30,
|
||||||
|
`${prefix} must record a substantive reason.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
throw new Error(failures.join("\n"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadDesktopReleasePolicy = async () => {
|
||||||
|
const policy = JSON.parse(await readFile(desktopReleasePolicyPath, "utf8"));
|
||||||
|
validatePolicy(policy);
|
||||||
|
return policy;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveDesktopReleasePolicy = (version, policy) => {
|
||||||
|
if (!semverPattern.test(version || "")) {
|
||||||
|
throw new Error(`Desktop release version must use exact X.Y.Z semver; found ${version || "<missing>"}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exception = policy.unsignedPlatformExceptions.find(
|
||||||
|
(candidate) => candidate.version === version && candidate.status === "approved",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exception) {
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
platformSigningMode: "signed",
|
||||||
|
macosSigning: "apple-developer",
|
||||||
|
windowsSigning: "authenticode",
|
||||||
|
artifactLabel: "SIGNED-PLATFORM",
|
||||||
|
distribution: "formal",
|
||||||
|
warningRequired: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
platformSigningMode: "unsigned-exception",
|
||||||
|
macosSigning: exception.macos,
|
||||||
|
windowsSigning: exception.windows,
|
||||||
|
artifactLabel: "UNSIGNED-PLATFORM",
|
||||||
|
distribution: exception.distribution,
|
||||||
|
warningRequired: true,
|
||||||
|
macosLocalExactTagFallback: exception.macosLocalExactTagFallback === true,
|
||||||
|
windowsDelivery: exception.windowsDelivery,
|
||||||
|
updaterFeedActivation: exception.updaterFeedActivation,
|
||||||
|
githubReleaseAssetProfile: exception.githubReleaseAssetProfile,
|
||||||
|
stagedEvidenceRetention: exception.stagedEvidenceRetention,
|
||||||
|
updaterArtifactPublishTarget: exception.updaterArtifactPublishTarget,
|
||||||
|
exception,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { appendFile, readFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
||||||
|
|
||||||
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const value = (name) => {
|
||||||
|
const index = args.indexOf(name);
|
||||||
|
return index >= 0 ? args[index + 1] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const policy = await loadDesktopReleasePolicy();
|
||||||
|
const resolution = resolveDesktopReleasePolicy(packageInfo.version, policy);
|
||||||
|
const expectedTag = `v${packageInfo.version}`;
|
||||||
|
|
||||||
|
if (args.includes("--require-tag")) {
|
||||||
|
if (process.env.GITHUB_REF_TYPE !== "tag" || process.env.GITHUB_REF_NAME !== expectedTag) {
|
||||||
|
throw new Error(
|
||||||
|
`Desktop release policy must be resolved from tag ${expectedTag}; found ${process.env.GITHUB_REF_TYPE || "<missing>"} ${process.env.GITHUB_REF_NAME || "<missing>"}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputs = {
|
||||||
|
version: resolution.version,
|
||||||
|
platform_signing_mode: resolution.platformSigningMode,
|
||||||
|
macos_signing: resolution.macosSigning,
|
||||||
|
windows_signing: resolution.windowsSigning,
|
||||||
|
artifact_label: resolution.artifactLabel,
|
||||||
|
distribution: resolution.distribution,
|
||||||
|
warning_required: String(resolution.warningRequired),
|
||||||
|
updater_signing_required: String(policy.updaterSigningRequired),
|
||||||
|
};
|
||||||
|
|
||||||
|
const githubOutput = value("--github-output");
|
||||||
|
if (githubOutput) {
|
||||||
|
await appendFile(githubOutput, `${Object.entries(outputs).map(([key, output]) => `${key}=${output}`).join("\n")}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Desktop release policy check passed.");
|
||||||
|
console.log(` version: ${resolution.version}`);
|
||||||
|
console.log(` platform signing mode: ${resolution.platformSigningMode}`);
|
||||||
|
console.log(` macOS: ${resolution.macosSigning}`);
|
||||||
|
console.log(` Windows: ${resolution.windowsSigning}`);
|
||||||
|
console.log(" updater signing: required");
|
||||||
@@ -2,22 +2,27 @@ import { execFileSync } from "node:child_process";
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
const rootDir = resolve(frontendDir, "..");
|
const rootDir = resolve(frontendDir, "..");
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
|
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
||||||
|
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
||||||
const failures = [];
|
const failures = [];
|
||||||
|
|
||||||
const env = process.env;
|
const env = process.env;
|
||||||
const fullShaPattern = /^[0-9a-f]{40}$/i;
|
const fullShaPattern = /^[0-9a-f]{40}$/i;
|
||||||
const expectedTag = `v${packageInfo.version}`;
|
const expectedTag = `v${packageInfo.version}`;
|
||||||
const requiredSecretLikeEnv = [
|
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
|
||||||
"TAURI_SIGNING_PRIVATE_KEY",
|
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
|
||||||
"TAURI_SIGNING_PRIVATE_KEY_PASSWORD",
|
const requiresUpdaterSigning = env.REQUIRE_UPDATER_SIGNING === "true";
|
||||||
"APPLE_ID",
|
const allowsUnsignedPlatformRelease = env.ALLOW_UNSIGNED_PLATFORM_RELEASE === "true";
|
||||||
"APPLE_PASSWORD",
|
const desktopPlatformSigningMode = env.DESKTOP_PLATFORM_SIGNING_MODE;
|
||||||
"APPLE_TEAM_ID",
|
const desktopReleasePlatform = env.DESKTOP_RELEASE_PLATFORM;
|
||||||
];
|
const requiredUpdaterEnv = ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"];
|
||||||
|
const requiredMacosEnv = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID"];
|
||||||
|
const requiredWindowsEnv = ["WINDOWS_CERTIFICATE", "WINDOWS_CERTIFICATE_PASSWORD"];
|
||||||
|
|
||||||
const fail = (message) => failures.push(message);
|
const fail = (message) => failures.push(message);
|
||||||
const assert = (condition, message) => {
|
const assert = (condition, message) => {
|
||||||
@@ -40,7 +45,7 @@ const gitMaybe = (args) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requireEnv = (name) => {
|
const requireEnv = (name) => {
|
||||||
assert(Boolean(env[name]), `${name} must be configured for signed desktop release readiness.`);
|
assert(Boolean(env[name]), `${name} must be configured for desktop release readiness.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateBaseUrl = () => {
|
const validateBaseUrl = () => {
|
||||||
@@ -65,6 +70,43 @@ const validateBaseUrl = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateClientServerUrl = () => {
|
||||||
|
const raw = env.VITE_DESKTOP_SERVER_URL;
|
||||||
|
requireEnv("VITE_DESKTOP_SERVER_URL");
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(raw);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`VITE_DESKTOP_SERVER_URL is invalid: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(url.protocol === "https:", "VITE_DESKTOP_SERVER_URL must use HTTPS.");
|
||||||
|
assert(url.username === "" && url.password === "", "VITE_DESKTOP_SERVER_URL must not include credentials.");
|
||||||
|
assert(url.pathname === "/", "VITE_DESKTOP_SERVER_URL must be an origin without a path.");
|
||||||
|
assert(url.search === "" && url.hash === "", "VITE_DESKTOP_SERVER_URL must not include query parameters or fragments.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateWindowsTimestampUrl = () => {
|
||||||
|
const raw = env.WINDOWS_TIMESTAMP_URL;
|
||||||
|
requireEnv("WINDOWS_TIMESTAMP_URL");
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(raw);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`WINDOWS_TIMESTAMP_URL is invalid: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(["http:", "https:"].includes(url.protocol), "WINDOWS_TIMESTAMP_URL must use HTTP or HTTPS.");
|
||||||
|
assert(url.username === "" && url.password === "", "WINDOWS_TIMESTAMP_URL must not include credentials.");
|
||||||
|
assert(!/[?&]token=/i.test(url.search), "WINDOWS_TIMESTAMP_URL must not include token query parameters.");
|
||||||
|
};
|
||||||
|
|
||||||
const headSha = gitMaybe(["rev-parse", "HEAD"]);
|
const headSha = gitMaybe(["rev-parse", "HEAD"]);
|
||||||
const exactTag = gitMaybe(["describe", "--tags", "--exact-match", "HEAD"]);
|
const exactTag = gitMaybe(["describe", "--tags", "--exact-match", "HEAD"]);
|
||||||
const status = gitMaybe(["status", "--porcelain"]);
|
const status = gitMaybe(["status", "--porcelain"]);
|
||||||
@@ -79,10 +121,59 @@ if (headSha && env.VITE_BUILD_COMMIT) {
|
|||||||
assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit.");
|
assert(env.VITE_BUILD_COMMIT === headSha, "VITE_BUILD_COMMIT must match the current release commit.");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const name of requiredSecretLikeEnv) {
|
assert(
|
||||||
|
!(requiresMacosSigning && requiresWindowsSigning),
|
||||||
|
"macOS and Windows signing readiness must be checked in their native jobs.",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
desktopReleasePlatform === "macos" || desktopReleasePlatform === "windows",
|
||||||
|
"DESKTOP_RELEASE_PLATFORM must be macos or windows.",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
desktopPlatformSigningMode === desktopReleaseResolution.platformSigningMode,
|
||||||
|
`DESKTOP_PLATFORM_SIGNING_MODE must be ${desktopReleaseResolution.platformSigningMode} for v${packageInfo.version}.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
desktopReleasePolicy.updaterSigningRequired === true && requiresUpdaterSigning,
|
||||||
|
"Release readiness requires REQUIRE_UPDATER_SIGNING=true; updater signing cannot be disabled.",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const name of requiredUpdaterEnv) {
|
||||||
requireEnv(name);
|
requireEnv(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (desktopPlatformSigningMode === "signed") {
|
||||||
|
assert(!allowsUnsignedPlatformRelease, "Signed releases must not enable ALLOW_UNSIGNED_PLATFORM_RELEASE.");
|
||||||
|
assert(
|
||||||
|
(desktopReleasePlatform === "macos" && requiresMacosSigning && !requiresWindowsSigning) ||
|
||||||
|
(desktopReleasePlatform === "windows" && requiresWindowsSigning && !requiresMacosSigning),
|
||||||
|
"Signed readiness must enable only the matching native platform signing requirement.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktopPlatformSigningMode === "unsigned-exception") {
|
||||||
|
assert(
|
||||||
|
desktopReleaseResolution.platformSigningMode === "unsigned-exception",
|
||||||
|
`v${packageInfo.version} has no approved unsigned platform release exception.`,
|
||||||
|
);
|
||||||
|
assert(allowsUnsignedPlatformRelease, "The approved exception requires ALLOW_UNSIGNED_PLATFORM_RELEASE=true.");
|
||||||
|
assert(
|
||||||
|
!requiresMacosSigning && !requiresWindowsSigning,
|
||||||
|
"Unsigned platform readiness must not claim Apple or Windows platform signing.",
|
||||||
|
);
|
||||||
|
if (desktopReleasePlatform === "macos") {
|
||||||
|
assert(process.platform === "darwin", "The macOS ad-hoc readiness check must run on macOS.");
|
||||||
|
}
|
||||||
|
if (desktopReleasePlatform === "windows") {
|
||||||
|
assert(process.platform === "win32", "The unsigned Windows readiness check must run on Windows.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresMacosSigning) {
|
||||||
|
assert(process.platform === "darwin", "Signed macOS desktop release readiness must run on macOS.");
|
||||||
|
for (const name of requiredMacosEnv) {
|
||||||
|
requireEnv(name);
|
||||||
|
}
|
||||||
assert(
|
assert(
|
||||||
Boolean(env.APPLE_CERTIFICATE || env.APPLE_SIGNING_IDENTITY),
|
Boolean(env.APPLE_CERTIFICATE || env.APPLE_SIGNING_IDENTITY),
|
||||||
"APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY must be configured for macOS signing.",
|
"APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY must be configured for macOS signing.",
|
||||||
@@ -90,8 +181,18 @@ assert(
|
|||||||
if (env.APPLE_CERTIFICATE) {
|
if (env.APPLE_CERTIFICATE) {
|
||||||
requireEnv("APPLE_CERTIFICATE_PASSWORD");
|
requireEnv("APPLE_CERTIFICATE_PASSWORD");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresWindowsSigning) {
|
||||||
|
assert(process.platform === "win32", "Signed Windows desktop release readiness must run on Windows.");
|
||||||
|
for (const name of requiredWindowsEnv) {
|
||||||
|
requireEnv(name);
|
||||||
|
}
|
||||||
|
validateWindowsTimestampUrl();
|
||||||
|
}
|
||||||
|
|
||||||
validateBaseUrl();
|
validateBaseUrl();
|
||||||
|
validateClientServerUrl();
|
||||||
|
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
console.error(`Desktop release readiness check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
console.error(`Desktop release readiness check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const verifyTauriConfig = async () => {
|
|||||||
assert(targetList.includes("dmg"), "Tauri bundle targets must include dmg for macOS distribution.");
|
assert(targetList.includes("dmg"), "Tauri bundle targets must include dmg for macOS distribution.");
|
||||||
assert(
|
assert(
|
||||||
tauriConfig.bundle?.createUpdaterArtifacts === true,
|
tauriConfig.bundle?.createUpdaterArtifacts === true,
|
||||||
"Tauri must create updater artifacts for signed desktop release builds.",
|
"Tauri must create signed updater artifacts for formal desktop release builds.",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
typeof tauriConfig.plugins?.updater?.pubkey === "string" && tauriConfig.plugins.updater.pubkey.length > 80,
|
typeof tauriConfig.plugins?.updater?.pubkey === "string" && tauriConfig.plugins.updater.pubkey.length > 80,
|
||||||
@@ -387,11 +387,28 @@ const verifyUpdaterBoundary = async () => {
|
|||||||
assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials.");
|
assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials.");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const verifyDesktopServerConfigurationBoundary = async () => {
|
||||||
|
const source = await readFile(resolve(sourceDir, "runtime/desktopServerConfig.ts"), "utf8");
|
||||||
|
assert(
|
||||||
|
source.includes("import.meta.env.VITE_DESKTOP_SERVER_URL"),
|
||||||
|
"Desktop server defaults must come from VITE_DESKTOP_SERVER_URL.",
|
||||||
|
);
|
||||||
|
assert(!source.includes("ctms.huapont.cn"), "The production CTMS origin must not be hard-coded in Desktop runtime source.");
|
||||||
|
assert(
|
||||||
|
source.includes("getStoredDesktopServerUrl() || getDefaultDesktopServerUrl()"),
|
||||||
|
"A manually stored Desktop server URL must override the build-time default.",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const verifyWorkflowGates = async () => {
|
const verifyWorkflowGates = async () => {
|
||||||
const packageInfo = await readJson(resolve(frontendDir, "package.json"));
|
const packageInfo = await readJson(resolve(frontendDir, "package.json"));
|
||||||
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
|
assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0.");
|
||||||
const requiredScripts = [
|
const requiredScripts = [
|
||||||
"desktop:build:macos-release",
|
"desktop:build:macos-release",
|
||||||
|
"desktop:build:macos-unsigned-release",
|
||||||
|
"desktop:build:windows-release",
|
||||||
|
"desktop:release-policy:check",
|
||||||
|
"desktop:release-provenance:create",
|
||||||
"desktop:update-feed:create",
|
"desktop:update-feed:create",
|
||||||
"desktop:update-feed:check",
|
"desktop:update-feed:check",
|
||||||
"desktop:release-readiness:check",
|
"desktop:release-readiness:check",
|
||||||
@@ -402,6 +419,47 @@ const verifyWorkflowGates = async () => {
|
|||||||
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
|
assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provenanceSource = await readFile(resolve(frontendDir, "scripts/create-desktop-release-provenance.mjs"), "utf8");
|
||||||
|
assert(provenanceSource.includes('"macos-first"'), "Release provenance must support the approved v0.1.0 macOS-first stage.");
|
||||||
|
assert(
|
||||||
|
provenanceSource.includes("withheld-pending-combined-verification"),
|
||||||
|
"Staged macOS provenance must record that updater feed activation is withheld.",
|
||||||
|
);
|
||||||
|
const windowsPendingNotice = await readFile(resolve(frontendDir, "desktop-release-windows-pending.txt"), "utf8");
|
||||||
|
assert(
|
||||||
|
windowsPendingNotice.includes("same immutable v0.1.0 tag") && windowsPendingNotice.includes("latest.json is intentionally withheld"),
|
||||||
|
"The staged v0.1.0 private release evidence must include an explicit Windows-pending and updater-feed notice.",
|
||||||
|
);
|
||||||
|
|
||||||
|
const releasePolicy = await readJson(resolve(frontendDir, "desktop-release-policy.json"));
|
||||||
|
assert(releasePolicy.schemaVersion === 1, "Desktop release policy schemaVersion must be 1.");
|
||||||
|
assert(
|
||||||
|
releasePolicy.defaultPlatformSigningMode === "signed",
|
||||||
|
"Desktop release policy must default future versions to signed platform releases.",
|
||||||
|
);
|
||||||
|
assert(releasePolicy.updaterSigningRequired === true, "Desktop release policy must require updater signing.");
|
||||||
|
const currentUnsignedException = releasePolicy.unsignedPlatformExceptions?.find(
|
||||||
|
(exception) => exception.version === packageInfo.version,
|
||||||
|
);
|
||||||
|
if (packageInfo.version === "0.1.0") {
|
||||||
|
assert(Boolean(currentUnsignedException), "Desktop release policy must record the approved v0.1.0 exception.");
|
||||||
|
assert(
|
||||||
|
currentUnsignedException?.macosLocalExactTagFallback === true &&
|
||||||
|
currentUnsignedException?.windowsDelivery === "deferred-same-tag" &&
|
||||||
|
currentUnsignedException?.updaterFeedActivation === "after-combined-platform-verification" &&
|
||||||
|
currentUnsignedException?.githubReleaseAssetProfile === "installer-and-checksum-only" &&
|
||||||
|
currentUnsignedException?.stagedEvidenceRetention === "private-until-updater-publish" &&
|
||||||
|
currentUnsignedException?.updaterArtifactPublishTarget === "anonymous-https-origin",
|
||||||
|
"The v0.1.0 exception must constrain local macOS staging, deferred Windows delivery, visible GitHub assets, private evidence retention, and updater publication.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentUnsignedException) {
|
||||||
|
assert(
|
||||||
|
currentUnsignedException.macos === "ad-hoc" && currentUnsignedException.windows === "unsigned",
|
||||||
|
`The v${packageInfo.version} exception must use macOS ad-hoc and unsigned Windows modes.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8");
|
const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8");
|
||||||
const requiredCommands = [
|
const requiredCommands = [
|
||||||
"npm run version:check",
|
"npm run version:check",
|
||||||
@@ -420,19 +478,46 @@ const verifyWorkflowGates = async () => {
|
|||||||
}
|
}
|
||||||
assert(workflow.includes("VITE_BUILD_CHANNEL"), "Client quality gates workflow must inject VITE_BUILD_CHANNEL.");
|
assert(workflow.includes("VITE_BUILD_CHANNEL"), "Client quality gates workflow must inject VITE_BUILD_CHANNEL.");
|
||||||
assert(workflow.includes("VITE_BUILD_COMMIT"), "Client quality gates workflow must inject VITE_BUILD_COMMIT.");
|
assert(workflow.includes("VITE_BUILD_COMMIT"), "Client quality gates workflow must inject VITE_BUILD_COMMIT.");
|
||||||
|
assert(workflow.includes("VITE_DESKTOP_SERVER_URL"), "Client quality gates workflow must inject VITE_DESKTOP_SERVER_URL.");
|
||||||
assert(workflow.match(/node-version: "22\.13"/g)?.length === 2, "Client quality gates must use Node.js 22.13 for Web and Desktop jobs.");
|
assert(workflow.match(/node-version: "22\.13"/g)?.length === 2, "Client quality gates must use Node.js 22.13 for Web and Desktop jobs.");
|
||||||
|
|
||||||
const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8");
|
const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8");
|
||||||
const requiredReleaseWorkflowTokens = [
|
const requiredReleaseWorkflowTokens = [
|
||||||
|
"runs-on: macos-latest",
|
||||||
|
"runs-on: windows-latest",
|
||||||
"REQUIRE_DESKTOP_SIGNING",
|
"REQUIRE_DESKTOP_SIGNING",
|
||||||
|
"REQUIRE_WINDOWS_SIGNING",
|
||||||
|
"REQUIRE_UPDATER_SIGNING",
|
||||||
|
"ALLOW_UNSIGNED_PLATFORM_RELEASE",
|
||||||
|
"DESKTOP_PLATFORM_SIGNING_MODE",
|
||||||
|
"desktop:release-policy:check",
|
||||||
|
"desktop:build:macos-unsigned-release",
|
||||||
|
"Signature=adhoc",
|
||||||
|
'"NotSigned"',
|
||||||
|
"UNSIGNED-PLATFORM",
|
||||||
|
"DESKTOP-RELEASE-PROVENANCE.json",
|
||||||
|
"--require-provenance",
|
||||||
"TAURI_SIGNING_PRIVATE_KEY",
|
"TAURI_SIGNING_PRIVATE_KEY",
|
||||||
|
"VITE_DESKTOP_SERVER_URL",
|
||||||
"APPLE_ID",
|
"APPLE_ID",
|
||||||
"APPLE_PASSWORD",
|
"APPLE_PASSWORD",
|
||||||
"APPLE_TEAM_ID",
|
"APPLE_TEAM_ID",
|
||||||
|
"WINDOWS_CERTIFICATE",
|
||||||
|
"WINDOWS_CERTIFICATE_PASSWORD",
|
||||||
|
"WINDOWS_TIMESTAMP_URL",
|
||||||
|
"npm audit",
|
||||||
"npm run desktop:build:macos-release",
|
"npm run desktop:build:macos-release",
|
||||||
|
"npm run desktop:build:windows-release",
|
||||||
|
"Get-AuthenticodeSignature",
|
||||||
|
"EnhancedKeyUsageList",
|
||||||
|
"TimeStamperCertificate",
|
||||||
|
".nsis.zip",
|
||||||
"npm run desktop:update-feed:create",
|
"npm run desktop:update-feed:create",
|
||||||
|
"--platform-artifact",
|
||||||
"npm run desktop:update-feed:check",
|
"npm run desktop:update-feed:check",
|
||||||
|
"--require-platform windows-x86_64",
|
||||||
"npm run desktop:release-readiness:check",
|
"npm run desktop:release-readiness:check",
|
||||||
|
"actions/download-artifact",
|
||||||
"actions/upload-artifact",
|
"actions/upload-artifact",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -447,6 +532,7 @@ const verifyWorkflowGates = async () => {
|
|||||||
"runs-on: windows-latest",
|
"runs-on: windows-latest",
|
||||||
"VITE_BUILD_CHANNEL",
|
"VITE_BUILD_CHANNEL",
|
||||||
"VITE_BUILD_COMMIT",
|
"VITE_BUILD_COMMIT",
|
||||||
|
"VITE_DESKTOP_SERVER_URL",
|
||||||
"npm run release:env:check",
|
"npm run release:env:check",
|
||||||
"npm run version:check",
|
"npm run version:check",
|
||||||
"npm run runtime:check",
|
"npm run runtime:check",
|
||||||
@@ -505,6 +591,7 @@ await verifySourceSafety();
|
|||||||
await verifyNotificationBoundary();
|
await verifyNotificationBoundary();
|
||||||
await verifySessionBoundary();
|
await verifySessionBoundary();
|
||||||
await verifyUpdaterBoundary();
|
await verifyUpdaterBoundary();
|
||||||
|
await verifyDesktopServerConfigurationBoundary();
|
||||||
await verifyOnlyOfficeBoundary();
|
await verifyOnlyOfficeBoundary();
|
||||||
await verifyWorkflowGates();
|
await verifyWorkflowGates();
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { access, readFile } from "node:fs/promises";
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { basename, resolve } from "node:path";
|
import { basename, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
|
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
||||||
|
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
||||||
const failures = [];
|
const failures = [];
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
@@ -12,6 +15,8 @@ const optionValue = (name) => {
|
|||||||
const index = args.indexOf(name);
|
const index = args.indexOf(name);
|
||||||
return index >= 0 ? args[index + 1] : undefined;
|
return index >= 0 ? args[index + 1] : undefined;
|
||||||
};
|
};
|
||||||
|
const optionValues = (name) =>
|
||||||
|
args.reduce((result, argument, index) => (argument === name && args[index + 1] ? [...result, args[index + 1]] : result), []);
|
||||||
|
|
||||||
const feedPath = resolve(
|
const feedPath = resolve(
|
||||||
frontendDir,
|
frontendDir,
|
||||||
@@ -19,6 +24,17 @@ const feedPath = resolve(
|
|||||||
);
|
);
|
||||||
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
const artifactDir = optionValue("--artifacts-dir") || process.env.DESKTOP_UPDATE_ARTIFACTS_DIR;
|
||||||
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
const expectedBaseUrl = optionValue("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
|
||||||
|
const requiresProvenance = args.includes("--require-provenance");
|
||||||
|
const provenancePath =
|
||||||
|
optionValue("--provenance") ||
|
||||||
|
process.env.DESKTOP_RELEASE_PROVENANCE ||
|
||||||
|
(artifactDir ? resolve(artifactDir, "DESKTOP-RELEASE-PROVENANCE.json") : undefined);
|
||||||
|
const requiredPlatforms = new Set([
|
||||||
|
"darwin-aarch64",
|
||||||
|
"darwin-x86_64",
|
||||||
|
...optionValues("--require-platform"),
|
||||||
|
]);
|
||||||
|
const platformPattern = /^(?:darwin|windows|linux)-(?:x86_64|aarch64|i686|armv7)$/;
|
||||||
const checksumManifestPath =
|
const checksumManifestPath =
|
||||||
optionValue("--checksum-manifest") ||
|
optionValue("--checksum-manifest") ||
|
||||||
process.env.DESKTOP_UPDATE_CHECKSUM_MANIFEST ||
|
process.env.DESKTOP_UPDATE_CHECKSUM_MANIFEST ||
|
||||||
@@ -86,6 +102,56 @@ const assertManifestEntries = async (checksums) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const verifyProvenance = async (checksums) => {
|
||||||
|
if (!requiresProvenance) return;
|
||||||
|
if (!provenancePath) {
|
||||||
|
fail("Desktop release provenance path is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let provenance;
|
||||||
|
try {
|
||||||
|
provenance = JSON.parse(await readFile(provenancePath, "utf8"));
|
||||||
|
} catch (error) {
|
||||||
|
fail(`Cannot read desktop release provenance ${provenancePath}: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(provenance.schemaVersion === 1, "Desktop release provenance schemaVersion must be 1.");
|
||||||
|
assert(provenance.version === packageInfo.version, "Desktop release provenance version must match the package version.");
|
||||||
|
assert(provenance.tag === `v${packageInfo.version}`, "Desktop release provenance tag must match the package version.");
|
||||||
|
assert(/^[0-9a-f]{40}$/i.test(provenance.commit || ""), "Desktop release provenance must contain a full commit SHA.");
|
||||||
|
assert(
|
||||||
|
provenance.platformSigningMode === desktopReleaseResolution.platformSigningMode,
|
||||||
|
`Desktop release provenance platformSigningMode must be ${desktopReleaseResolution.platformSigningMode}.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
provenance.platformSigning?.macos === desktopReleaseResolution.macosSigning,
|
||||||
|
`Desktop release provenance macOS signing must be ${desktopReleaseResolution.macosSigning}.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
provenance.platformSigning?.windows === desktopReleaseResolution.windowsSigning,
|
||||||
|
`Desktop release provenance Windows signing must be ${desktopReleaseResolution.windowsSigning}.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
provenance.updaterSigning === "required-and-verified",
|
||||||
|
"Desktop release provenance must record required-and-verified updater signing.",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
provenance.artifactLabel === desktopReleaseResolution.artifactLabel,
|
||||||
|
`Desktop release provenance artifactLabel must be ${desktopReleaseResolution.artifactLabel}.`,
|
||||||
|
);
|
||||||
|
if (desktopReleaseResolution.warningRequired) {
|
||||||
|
assert(
|
||||||
|
Array.isArray(provenance.warnings) && provenance.warnings.length >= 2,
|
||||||
|
"Unsigned platform release provenance must include installation trust warnings.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (artifactDir) {
|
||||||
|
await assertChecksum(checksums, provenancePath, "desktop release provenance");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let feed;
|
let feed;
|
||||||
try {
|
try {
|
||||||
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
feed = JSON.parse(await readFile(feedPath, "utf8"));
|
||||||
@@ -96,6 +162,7 @@ try {
|
|||||||
if (feed) {
|
if (feed) {
|
||||||
const checksums = await readChecksumManifest();
|
const checksums = await readChecksumManifest();
|
||||||
await assertManifestEntries(checksums);
|
await assertManifestEntries(checksums);
|
||||||
|
await verifyProvenance(checksums);
|
||||||
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
const normalizedFeedVersion = String(feed.version || "").replace(/^v/, "");
|
||||||
const platforms = feed.platforms || {};
|
const platforms = feed.platforms || {};
|
||||||
const darwinArm = platforms["darwin-aarch64"];
|
const darwinArm = platforms["darwin-aarch64"];
|
||||||
@@ -103,15 +170,16 @@ if (feed) {
|
|||||||
|
|
||||||
assert(normalizedFeedVersion === packageInfo.version, `latest.json version must match package version ${packageInfo.version}.`);
|
assert(normalizedFeedVersion === packageInfo.version, `latest.json version must match package version ${packageInfo.version}.`);
|
||||||
assert(Boolean(feed.pub_date || feed.pubDate), "latest.json must include a publication date.");
|
assert(Boolean(feed.pub_date || feed.pubDate), "latest.json must include a publication date.");
|
||||||
assert(Boolean(darwinArm), "latest.json must include darwin-aarch64.");
|
for (const platform of requiredPlatforms) {
|
||||||
assert(Boolean(darwinIntel), "latest.json must include darwin-x86_64.");
|
assert(platformPattern.test(platform), `Required updater platform key is unsupported: ${platform}.`);
|
||||||
|
assert(Boolean(platforms[platform]), `latest.json must include ${platform}.`);
|
||||||
|
}
|
||||||
|
|
||||||
const entries = [
|
const entries = Object.entries(platforms);
|
||||||
["darwin-aarch64", darwinArm],
|
assert(entries.length > 0, "latest.json must include at least one platform entry.");
|
||||||
["darwin-x86_64", darwinIntel],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [platform, entry] of entries) {
|
for (const [platform, entry] of entries) {
|
||||||
|
assert(platformPattern.test(platform), `latest.json contains an unsupported platform key: ${platform}.`);
|
||||||
const rawUrl = entry?.url;
|
const rawUrl = entry?.url;
|
||||||
const signature = entry?.signature;
|
const signature = entry?.signature;
|
||||||
assert(typeof signature === "string" && signature.length > 80, `${platform} must include an updater signature.`);
|
assert(typeof signature === "string" && signature.length > 80, `${platform} must include an updater signature.`);
|
||||||
@@ -133,6 +201,12 @@ if (feed) {
|
|||||||
if (expectedBaseUrl) {
|
if (expectedBaseUrl) {
|
||||||
assert(rawUrl.startsWith(expectedBaseUrl), `${platform} artifact URL must start with ${expectedBaseUrl}.`);
|
assert(rawUrl.startsWith(expectedBaseUrl), `${platform} artifact URL must start with ${expectedBaseUrl}.`);
|
||||||
}
|
}
|
||||||
|
if (platform.startsWith("darwin-")) {
|
||||||
|
assert(url.pathname.endsWith(".app.tar.gz"), `${platform} must point to a macOS .app.tar.gz updater artifact.`);
|
||||||
|
}
|
||||||
|
if (platform === "windows-x86_64") {
|
||||||
|
assert(url.pathname.endsWith(".nsis.zip"), "windows-x86_64 must point to an NSIS .nsis.zip updater artifact.");
|
||||||
|
}
|
||||||
|
|
||||||
if (artifactDir) {
|
if (artifactDir) {
|
||||||
const artifactPath = resolve(artifactDir, basename(url.pathname));
|
const artifactPath = resolve(artifactDir, basename(url.pathname));
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { loadDesktopReleasePolicy, resolveDesktopReleasePolicy } from "./desktop-release-policy.mjs";
|
||||||
|
|
||||||
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
||||||
|
const rootDir = resolve(frontendDir, "..");
|
||||||
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
||||||
|
const desktopReleasePolicy = await loadDesktopReleasePolicy();
|
||||||
|
const desktopReleaseResolution = resolveDesktopReleasePolicy(packageInfo.version, desktopReleasePolicy);
|
||||||
const failures = [];
|
const failures = [];
|
||||||
|
|
||||||
const allowedChannels = new Set(["dev", "main", "release", "local"]);
|
const allowedChannels = new Set(["dev", "main", "release", "local"]);
|
||||||
@@ -16,6 +21,38 @@ const commit = env.VITE_BUILD_COMMIT || "local";
|
|||||||
const isCi = env.CI === "true" || env.GITHUB_ACTIONS === "true";
|
const isCi = env.CI === "true" || env.GITHUB_ACTIONS === "true";
|
||||||
const isTagBuild = env.GITHUB_REF_TYPE === "tag";
|
const isTagBuild = env.GITHUB_REF_TYPE === "tag";
|
||||||
const isReleaseBuild = env.RELEASE_BUILD === "true" || isTagBuild || channel === "release";
|
const isReleaseBuild = env.RELEASE_BUILD === "true" || isTagBuild || channel === "release";
|
||||||
|
const requiresMacosSigning = env.REQUIRE_DESKTOP_SIGNING === "true";
|
||||||
|
const requiresWindowsSigning = env.REQUIRE_WINDOWS_SIGNING === "true";
|
||||||
|
const requiresUpdaterSigning = env.REQUIRE_UPDATER_SIGNING === "true";
|
||||||
|
const allowsUnsignedPlatformRelease = env.ALLOW_UNSIGNED_PLATFORM_RELEASE === "true";
|
||||||
|
const desktopPlatformSigningMode = env.DESKTOP_PLATFORM_SIGNING_MODE;
|
||||||
|
const desktopReleasePlatform = env.DESKTOP_RELEASE_PLATFORM;
|
||||||
|
const isNativeDesktopRelease = Boolean(
|
||||||
|
desktopReleasePlatform ||
|
||||||
|
desktopPlatformSigningMode ||
|
||||||
|
requiresMacosSigning ||
|
||||||
|
requiresWindowsSigning ||
|
||||||
|
requiresUpdaterSigning,
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateDesktopServerUrl = () => {
|
||||||
|
const raw = env.VITE_DESKTOP_SERVER_URL;
|
||||||
|
requireEnv("VITE_DESKTOP_SERVER_URL");
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(raw);
|
||||||
|
} catch (error) {
|
||||||
|
fail(`VITE_DESKTOP_SERVER_URL is invalid: ${error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(url.protocol === "https:", "VITE_DESKTOP_SERVER_URL must use HTTPS.");
|
||||||
|
assert(url.username === "" && url.password === "", "VITE_DESKTOP_SERVER_URL must not include credentials.");
|
||||||
|
assert(url.pathname === "/", "VITE_DESKTOP_SERVER_URL must be an origin without a path.");
|
||||||
|
assert(url.search === "" && url.hash === "", "VITE_DESKTOP_SERVER_URL must not include query parameters or fragments.");
|
||||||
|
};
|
||||||
|
|
||||||
const fail = (message) => failures.push(message);
|
const fail = (message) => failures.push(message);
|
||||||
const assert = (condition, message) => {
|
const assert = (condition, message) => {
|
||||||
@@ -23,7 +60,19 @@ const assert = (condition, message) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requireEnv = (name) => {
|
const requireEnv = (name) => {
|
||||||
assert(Boolean(env[name]), `${name} must be configured for signed desktop release builds.`);
|
assert(Boolean(env[name]), `${name} must be configured for this desktop release build.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const gitHead = () => {
|
||||||
|
try {
|
||||||
|
return execFileSync("git", ["rev-parse", "HEAD"], {
|
||||||
|
cwd: rootDir,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
}).trim();
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...allowedChannels].join(", ")}.`);
|
assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...allowedChannels].join(", ")}.`);
|
||||||
@@ -31,6 +80,7 @@ assert(allowedChannels.has(channel), `VITE_BUILD_CHANNEL must be one of ${[...al
|
|||||||
if (isCi || isReleaseBuild) {
|
if (isCi || isReleaseBuild) {
|
||||||
assert(channel !== "local", "CI and release builds must inject VITE_BUILD_CHANNEL.");
|
assert(channel !== "local", "CI and release builds must inject VITE_BUILD_CHANNEL.");
|
||||||
assert(fullShaPattern.test(commit), "CI and release builds must inject a full 40-character VITE_BUILD_COMMIT.");
|
assert(fullShaPattern.test(commit), "CI and release builds must inject a full 40-character VITE_BUILD_COMMIT.");
|
||||||
|
validateDesktopServerUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.GITHUB_SHA) {
|
if (env.GITHUB_SHA) {
|
||||||
@@ -40,18 +90,77 @@ if (env.GITHUB_SHA) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isReleaseBuild && fullShaPattern.test(commit)) {
|
||||||
|
const headSha = gitHead();
|
||||||
|
assert(Boolean(headSha), "Release builds must be able to resolve the current Git commit.");
|
||||||
|
if (headSha) {
|
||||||
|
assert(commit === headSha, "VITE_BUILD_COMMIT must match the current Git HEAD for release builds.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isTagBuild) {
|
if (isTagBuild) {
|
||||||
assert(env.GITHUB_REF_NAME === semverTag, `Release tag must be ${semverTag}; found ${env.GITHUB_REF_NAME || "<missing>"}.`);
|
assert(env.GITHUB_REF_NAME === semverTag, `Release tag must be ${semverTag}; found ${env.GITHUB_REF_NAME || "<missing>"}.`);
|
||||||
assert(channel === "release", "Release tag builds must set VITE_BUILD_CHANNEL=release.");
|
assert(channel === "release", "Release tag builds must set VITE_BUILD_CHANNEL=release.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.REQUIRE_DESKTOP_SIGNING === "true") {
|
assert(
|
||||||
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
|
!(requiresMacosSigning && requiresWindowsSigning),
|
||||||
|
"macOS and Windows signing requirements must be checked in their native jobs.",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNativeDesktopRelease) {
|
||||||
|
assert(isReleaseBuild, "Native desktop release settings may only be used for release builds.");
|
||||||
|
assert(
|
||||||
|
desktopReleasePolicy.updaterSigningRequired === true && requiresUpdaterSigning,
|
||||||
|
"Formal desktop releases must set REQUIRE_UPDATER_SIGNING=true; updater signing cannot be disabled.",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
desktopPlatformSigningMode === desktopReleaseResolution.platformSigningMode,
|
||||||
|
`DESKTOP_PLATFORM_SIGNING_MODE must be ${desktopReleaseResolution.platformSigningMode} for v${packageInfo.version}.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
desktopReleasePlatform === "macos" || desktopReleasePlatform === "windows",
|
||||||
|
"DESKTOP_RELEASE_PLATFORM must be macos or windows for native desktop release builds.",
|
||||||
|
);
|
||||||
if (isCi) {
|
if (isCi) {
|
||||||
assert(isTagBuild, "Signed desktop release candidate builds in CI must run from a release tag.");
|
assert(isTagBuild, "Native desktop release candidate builds in CI must run from a release tag.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresUpdaterSigning) {
|
||||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
requireEnv("TAURI_SIGNING_PRIVATE_KEY");
|
||||||
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
requireEnv("TAURI_SIGNING_PRIVATE_KEY_PASSWORD");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktopPlatformSigningMode === "signed") {
|
||||||
|
assert(!allowsUnsignedPlatformRelease, "Signed releases must not enable ALLOW_UNSIGNED_PLATFORM_RELEASE.");
|
||||||
|
assert(
|
||||||
|
(desktopReleasePlatform === "macos" && requiresMacosSigning && !requiresWindowsSigning) ||
|
||||||
|
(desktopReleasePlatform === "windows" && requiresWindowsSigning && !requiresMacosSigning),
|
||||||
|
"Signed native release jobs must enable only their matching platform signing requirement.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desktopPlatformSigningMode === "unsigned-exception") {
|
||||||
|
assert(
|
||||||
|
desktopReleaseResolution.platformSigningMode === "unsigned-exception",
|
||||||
|
`v${packageInfo.version} has no approved unsigned platform release exception.`,
|
||||||
|
);
|
||||||
|
assert(allowsUnsignedPlatformRelease, "The approved exception requires ALLOW_UNSIGNED_PLATFORM_RELEASE=true.");
|
||||||
|
assert(
|
||||||
|
!requiresMacosSigning && !requiresWindowsSigning,
|
||||||
|
"Unsigned platform exceptions must not claim Apple or Windows platform signing.",
|
||||||
|
);
|
||||||
|
if (desktopReleasePlatform === "macos") {
|
||||||
|
assert(process.platform === "darwin", "The macOS ad-hoc release exception must run on macOS.");
|
||||||
|
}
|
||||||
|
if (desktopReleasePlatform === "windows") {
|
||||||
|
assert(process.platform === "win32", "The unsigned Windows release exception must run on Windows.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiresMacosSigning) {
|
||||||
|
assert(process.platform === "darwin", "Signed macOS desktop release builds must run on macOS.");
|
||||||
requireEnv("APPLE_ID");
|
requireEnv("APPLE_ID");
|
||||||
requireEnv("APPLE_PASSWORD");
|
requireEnv("APPLE_PASSWORD");
|
||||||
requireEnv("APPLE_TEAM_ID");
|
requireEnv("APPLE_TEAM_ID");
|
||||||
@@ -64,6 +173,13 @@ if (env.REQUIRE_DESKTOP_SIGNING === "true") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requiresWindowsSigning) {
|
||||||
|
assert(process.platform === "win32", "Signed Windows desktop release builds must run on Windows.");
|
||||||
|
requireEnv("WINDOWS_CERTIFICATE");
|
||||||
|
requireEnv("WINDOWS_CERTIFICATE_PASSWORD");
|
||||||
|
requireEnv("WINDOWS_TIMESTAMP_URL");
|
||||||
|
}
|
||||||
|
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
console.error(`Release build environment check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
console.error(`Release build environment check failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwQjI5MUZFMjQ2NUM5QwpSV1NjWEViaUh5a0xBdE52U2Rhb29mZlVYZ3lnWGlDVGs1WE1RUGoyeWtSWW9pNzBnNW9qUGNaaAo="
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU5MzI4NzBDRDlCRDUzMUIKUldRYlU3M1pESWN5V1ZaYzZOdVlhbk5xVmFlQW4zRmpGbmFUdWV1UUN3WFBsYU16clZJMzRaVjEK"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -3,6 +3,7 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_BUILD_CHANNEL?: "dev" | "main" | "release" | "local";
|
readonly VITE_BUILD_CHANNEL?: "dev" | "main" | "release" | "local";
|
||||||
readonly VITE_BUILD_COMMIT?: string;
|
readonly VITE_BUILD_COMMIT?: string;
|
||||||
|
readonly VITE_DESKTOP_SERVER_URL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import {
|
import {
|
||||||
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
||||||
DESKTOP_SERVER_URL_KEY,
|
DESKTOP_SERVER_URL_KEY,
|
||||||
|
getDefaultDesktopServerUrl,
|
||||||
getDesktopServerUrl,
|
getDesktopServerUrl,
|
||||||
normalizeDesktopServerUrl,
|
normalizeDesktopServerUrl,
|
||||||
setDesktopServerUrl,
|
setDesktopServerUrl,
|
||||||
@@ -31,6 +32,7 @@ const createMemoryStorage = (): Storage => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "");
|
||||||
Object.defineProperty(window, "localStorage", {
|
Object.defineProperty(window, "localStorage", {
|
||||||
value: createMemoryStorage(),
|
value: createMemoryStorage(),
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -40,6 +42,7 @@ beforeEach(() => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
setTauriRuntime(false);
|
setTauriRuntime(false);
|
||||||
|
vi.unstubAllEnvs();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("desktop server config", () => {
|
describe("desktop server config", () => {
|
||||||
@@ -70,6 +73,23 @@ describe("desktop server config", () => {
|
|||||||
expect(listener).toHaveBeenCalledOnce();
|
expect(listener).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses the build-time default while allowing a persisted manual override", () => {
|
||||||
|
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "https://default.ctms.example.com");
|
||||||
|
|
||||||
|
expect(getDefaultDesktopServerUrl()).toBe("https://default.ctms.example.com/");
|
||||||
|
expect(getDesktopServerUrl()).toBe("https://default.ctms.example.com/");
|
||||||
|
|
||||||
|
setDesktopServerUrl("https://manual.ctms.example.com");
|
||||||
|
expect(getDesktopServerUrl()).toBe("https://manual.ctms.example.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores an invalid build-time default", () => {
|
||||||
|
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "http://public.ctms.example.com");
|
||||||
|
|
||||||
|
expect(getDefaultDesktopServerUrl()).toBeNull();
|
||||||
|
expect(getDesktopServerUrl()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("requires a server URL only inside the Tauri runtime", () => {
|
it("requires a server URL only inside the Tauri runtime", () => {
|
||||||
expect(shouldRequireDesktopServerUrl()).toBe(false);
|
expect(shouldRequireDesktopServerUrl()).toBe(false);
|
||||||
setTauriRuntime(true);
|
setTauriRuntime(true);
|
||||||
@@ -77,4 +97,11 @@ describe("desktop server config", () => {
|
|||||||
setDesktopServerUrl("https://ctms.example.com");
|
setDesktopServerUrl("https://ctms.example.com");
|
||||||
expect(shouldRequireDesktopServerUrl()).toBe(false);
|
expect(shouldRequireDesktopServerUrl()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not require first-run configuration when a valid build-time default exists", () => {
|
||||||
|
vi.stubEnv("VITE_DESKTOP_SERVER_URL", "https://default.ctms.example.com");
|
||||||
|
setTauriRuntime(true);
|
||||||
|
|
||||||
|
expect(shouldRequireDesktopServerUrl()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,13 +51,22 @@ export const normalizeDesktopServerUrl = (value: string): DesktopServerUrlValida
|
|||||||
return { ok: true, url: `${parsed.origin}/` };
|
return { ok: true, url: `${parsed.origin}/` };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDesktopServerUrl = (): string | null => {
|
export const getDefaultDesktopServerUrl = (): string | null => {
|
||||||
|
const configured = import.meta.env.VITE_DESKTOP_SERVER_URL?.trim();
|
||||||
|
if (!configured) return null;
|
||||||
|
const result = normalizeDesktopServerUrl(configured);
|
||||||
|
return result.ok ? result.url : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStoredDesktopServerUrl = (): string | null => {
|
||||||
const stored = getStorage()?.getItem(DESKTOP_SERVER_URL_KEY);
|
const stored = getStorage()?.getItem(DESKTOP_SERVER_URL_KEY);
|
||||||
if (!stored) return null;
|
if (!stored) return null;
|
||||||
const result = normalizeDesktopServerUrl(stored);
|
const result = normalizeDesktopServerUrl(stored);
|
||||||
return result.ok ? result.url : null;
|
return result.ok ? result.url : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDesktopServerUrl = (): string | null => getStoredDesktopServerUrl() || getDefaultDesktopServerUrl();
|
||||||
|
|
||||||
export const hasDesktopServerUrl = (): boolean => Boolean(getDesktopServerUrl());
|
export const hasDesktopServerUrl = (): boolean => Boolean(getDesktopServerUrl());
|
||||||
|
|
||||||
export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationResult => {
|
export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationResult => {
|
||||||
@@ -74,7 +83,7 @@ export const setDesktopServerUrl = (value: string): DesktopServerUrlValidationRe
|
|||||||
export const clearDesktopServerUrl = (): void => {
|
export const clearDesktopServerUrl = (): void => {
|
||||||
const previous = getDesktopServerUrl();
|
const previous = getDesktopServerUrl();
|
||||||
getStorage()?.removeItem(DESKTOP_SERVER_URL_KEY);
|
getStorage()?.removeItem(DESKTOP_SERVER_URL_KEY);
|
||||||
emitServerUrlChanged(previous, null);
|
emitServerUrlChanged(previous, getDesktopServerUrl());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldRequireDesktopServerUrl = (): boolean => isTauriRuntime() && !hasDesktopServerUrl();
|
export const shouldRequireDesktopServerUrl = (): boolean => isTauriRuntime() && !hasDesktopServerUrl();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { clientRuntime, type ClientRuntime, type RuntimeCapabilities } from "./c
|
|||||||
export {
|
export {
|
||||||
clearDesktopServerUrl,
|
clearDesktopServerUrl,
|
||||||
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
DESKTOP_SERVER_URL_CHANGED_EVENT,
|
||||||
|
getDefaultDesktopServerUrl,
|
||||||
getDesktopServerUrl,
|
getDesktopServerUrl,
|
||||||
hasDesktopServerUrl,
|
hasDesktopServerUrl,
|
||||||
normalizeDesktopServerUrl,
|
normalizeDesktopServerUrl,
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getContentDispositionFilename } from "./contentDisposition";
|
import { getContentDispositionFilename, getHttpHeaderString } from "./contentDisposition";
|
||||||
|
|
||||||
|
describe("getHttpHeaderString", () => {
|
||||||
|
it("keeps string header values and joins string arrays", () => {
|
||||||
|
expect(getHttpHeaderString("application/pdf")).toBe("application/pdf");
|
||||||
|
expect(getHttpHeaderString(["attachment", "filename=protocol.pdf"])).toBe("attachment, filename=protocol.pdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects non-string Axios header values", () => {
|
||||||
|
expect(getHttpHeaderString(200)).toBeUndefined();
|
||||||
|
expect(getHttpHeaderString(true)).toBeUndefined();
|
||||||
|
expect(getHttpHeaderString({ value: "application/pdf" })).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getContentDispositionFilename", () => {
|
describe("getContentDispositionFilename", () => {
|
||||||
it("prefers the UTF-8 filename even when the ASCII fallback appears first", () => {
|
it("prefers the UTF-8 filename even when the ASCII fallback appears first", () => {
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
export const getHttpHeaderString = (value: unknown): string | undefined => {
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
||||||
|
return value.join(", ");
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const getContentDispositionFilename = (header?: string | null): string | null => {
|
export const getContentDispositionFilename = (header?: string | null): string | null => {
|
||||||
if (!header) return null;
|
if (!header) return null;
|
||||||
const encoded = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header)?.[1];
|
const encoded = /filename\*\s*=\s*UTF-8''([^;]+)/i.exec(header)?.[1];
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ import StateLoading from "../../components/StateLoading.vue";
|
|||||||
import { onDesktopRefreshCurrentView } from "../../composables/useDesktopRefresh";
|
import { onDesktopRefreshCurrentView } from "../../composables/useDesktopRefresh";
|
||||||
import { openFileWithFeedback, pickFilesWithFeedback, saveFileWithFeedback } from "../../utils/fileTaskFeedback";
|
import { openFileWithFeedback, pickFilesWithFeedback, saveFileWithFeedback } from "../../utils/fileTaskFeedback";
|
||||||
import { getApiErrorMessage } from "../../utils/apiErrorMessage";
|
import { getApiErrorMessage } from "../../utils/apiErrorMessage";
|
||||||
import { getContentDispositionFilename } from "../../utils/contentDisposition";
|
import { getContentDispositionFilename, getHttpHeaderString } from "../../utils/contentDisposition";
|
||||||
import { prepareSaveFile } from "../../runtime";
|
import { prepareSaveFile } from "../../runtime";
|
||||||
import { detectFilePreviewKind, ONLYOFFICE_FILE_EXTENSIONS } from "../../utils/officePreview";
|
import { detectFilePreviewKind, ONLYOFFICE_FILE_EXTENSIONS } from "../../utils/officePreview";
|
||||||
|
|
||||||
@@ -803,7 +803,8 @@ const previewVersion = async (version: DocumentVersion) => {
|
|||||||
previewLoading.value = true;
|
previewLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await downloadDocumentVersion(version.id);
|
const response = await downloadDocumentVersion(version.id);
|
||||||
const contentType = response.headers?.["content-type"] || version.mime_type || "application/octet-stream";
|
const contentType =
|
||||||
|
getHttpHeaderString(response.headers?.["content-type"]) || version.mime_type || "application/octet-stream";
|
||||||
const blob = new Blob([response.data], { type: contentType });
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
previewObjectUrl.value = URL.createObjectURL(blob);
|
previewObjectUrl.value = URL.createObjectURL(blob);
|
||||||
previewUrl.value = previewObjectUrl.value;
|
previewUrl.value = previewObjectUrl.value;
|
||||||
@@ -823,8 +824,9 @@ const downloadVersion = async (version: DocumentVersion) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await downloadDocumentVersion(version.id);
|
const response = await downloadDocumentVersion(version.id);
|
||||||
const contentType = response.headers?.["content-type"] || "application/octet-stream";
|
const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream";
|
||||||
const filename = getContentDispositionFilename(response.headers?.["content-disposition"]) || suggestedName;
|
const filename =
|
||||||
|
getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || suggestedName;
|
||||||
const blob = new Blob([response.data], { type: contentType });
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
await saveFileWithFeedback({ suggestedName: filename, mimeType: contentType, data: blob }, {}, destination);
|
await saveFileWithFeedback({ suggestedName: filename, mimeType: contentType, data: blob }, {}, destination);
|
||||||
} catch (e: any) { ElMessage.error(await getApiErrorMessage(e, TEXT.common.messages.downloadFailed)); }
|
} catch (e: any) { ElMessage.error(await getApiErrorMessage(e, TEXT.common.messages.downloadFailed)); }
|
||||||
@@ -840,10 +842,11 @@ const openVersion = async (version: DocumentVersion) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contentType = response.headers?.["content-type"] || "application/octet-stream";
|
const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream";
|
||||||
const filename = getContentDispositionFilename(response.headers?.["content-disposition"])
|
const filename =
|
||||||
|| getVersionFileName(version)
|
getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) ||
|
||||||
|| `document-${version.version_no || version.id}.bin`;
|
getVersionFileName(version) ||
|
||||||
|
`document-${version.version_no || version.id}.bin`;
|
||||||
await openFileWithFeedback({
|
await openFileWithFeedback({
|
||||||
suggestedName: filename,
|
suggestedName: filename,
|
||||||
mimeType: contentType,
|
mimeType: contentType,
|
||||||
|
|||||||
@@ -474,6 +474,7 @@ import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
|
|||||||
import { displayDateTime } from "../../utils/display";
|
import { displayDateTime } from "../../utils/display";
|
||||||
import { getProjectRole, isSystemAdmin } from "../../utils/roles";
|
import { getProjectRole, isSystemAdmin } from "../../utils/roles";
|
||||||
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
|
import { useDrawerDirtyGuard } from "../../utils/drawerDirtyGuard";
|
||||||
|
import { getContentDispositionFilename, getHttpHeaderString } from "../../utils/contentDisposition";
|
||||||
|
|
||||||
interface MonitoringIssueRow {
|
interface MonitoringIssueRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -937,13 +938,6 @@ const removeIssue = async (row: MonitoringIssueRow) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFilename = (header?: string | null) => {
|
|
||||||
if (!header) return null;
|
|
||||||
const match = /filename\*=UTF-8''([^;]+)|filename="?([^\";]+)"?/i.exec(header);
|
|
||||||
if (!match) return null;
|
|
||||||
return decodeURIComponent(match[1] || match[2] || "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportExcel = async () => {
|
const handleExportExcel = async () => {
|
||||||
const studyId = study.currentStudy?.id;
|
const studyId = study.currentStudy?.id;
|
||||||
if (!studyId || !canListIssues.value) return;
|
if (!studyId || !canListIssues.value) return;
|
||||||
@@ -975,8 +969,9 @@ const handleExportExcel = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await exportMonitoringVisitIssues(studyId, params);
|
const response = await exportMonitoringVisitIssues(studyId, params);
|
||||||
const contentType = response.headers?.["content-type"] || "application/octet-stream";
|
const contentType = getHttpHeaderString(response.headers?.["content-type"]) || "application/octet-stream";
|
||||||
const filename = getFilename(response.headers?.["content-disposition"]) || "监查访视问题.xlsx";
|
const filename =
|
||||||
|
getContentDispositionFilename(getHttpHeaderString(response.headers?.["content-disposition"])) || "监查访视问题.xlsx";
|
||||||
const blob = new Blob([response.data], { type: contentType });
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
await saveFileWithFeedback(
|
await saveFileWithFeedback(
|
||||||
{ suggestedName: filename, mimeType: contentType, data: blob },
|
{ suggestedName: filename, mimeType: contentType, data: blob },
|
||||||
|
|||||||
Reference in New Issue
Block a user