Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 10 additions & 41 deletions .github/workflows/desktop-connect-discovery-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ jobs:
- name: Package the target-native desktop app
run: npm run desktop:package

- name: Inspect the unsigned target-native desktop app
run: npm run desktop:smoke:inspect

- name: Run packaged Linux main-to-renderer discovery
if: matrix.platform == 'linux'
shell: bash
Expand All @@ -112,47 +115,13 @@ jobs:
- name: Run packaged Darwin main-to-renderer discovery
if: matrix.platform == 'darwin'
shell: bash
run: |
set -euo pipefail
keychain_root="$(mktemp -d)"
keychain_path="$keychain_root/propr-packaged-connect-smoke.keychain-db"
keychain_password="$(openssl rand -hex 32)"
original_keychains=()
while IFS= read -r keychain; do
keychain="${keychain#"${keychain%%[![:space:]]*}"}"
keychain="${keychain#\"}"
keychain="${keychain%\"}"
if [[ -n "$keychain" ]]; then
original_keychains+=("$keychain")
fi
done < <(security list-keychains -d user)
IFS= read -r original_default < <(security default-keychain -d user)
original_default="${original_default#"${original_default%%[![:space:]]*}"}"
original_default="${original_default#\"}"
original_default="${original_default%\"}"
cleanup_keychain() {
if (( ${#original_keychains[@]} > 0 )); then
security list-keychains -d user -s "${original_keychains[@]}" || true
else
security list-keychains -d user -s || true
fi
if [[ -n "$original_default" ]]; then
security default-keychain -d user -s "$original_default" || true
fi
security delete-keychain "$keychain_path" || true
rm -rf -- "$keychain_root"
}
trap cleanup_keychain EXIT
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security list-keychains -d user -s "$keychain_path"
security default-keychain -d user -s "$keychain_path"
unset keychain_password
safe_storage_secret="$(openssl rand -hex 32)"
security add-generic-password -a "ProPR Desktop" -s "ProPR Desktop Safe Storage" -w "$safe_storage_secret" -A "$keychain_path"
unset safe_storage_secret
npm run smoke:connect-package -w @propr/desktop
run: >-
node apps/desktop/scripts/run-bounded-darwin-command.mjs
--timeout-ms 480000
--termination-grace-ms 90000
--max-output-bytes 1048576
--forward-output true
-- bash apps/desktop/scripts/run-packaged-darwin-connect-smoke.sh '${{ matrix.arch }}'

- name: Run packaged Windows main-to-renderer discovery as an ordinary user
if: matrix.platform == 'win32'
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ executable and fuse inspection without launching a window. Release CI launches b
inspects macOS and Windows packages on their native runners, validates DMG/ZIP/DEB/RPM/MSI packages, and validates
configured OS signatures.

Darwin packaged Connect acceptance first inspects the normal unsigned package, then generates a one-run self-signed
CA:false code-signing leaf in an isolated default keychain and signs only that smoke artifact. The signature uses an
explicit certificate-bound designated requirement that is verified before the pair process and again after the
reprobe process. Chromium creates and reopens its real Safe Storage key in the same disposable keychain; the harness
does not pre-seed or widen access to that item. A signal-aware exit trap restores the runner's original keychain list
and default, deletes the disposable keychain, and removes all temporary signing material.

The first-release Windows MVP packages only the normal desktop application. Native self-update installation authority
is deferred to issue #2000: no broker, bootstrap, launcher, service, or authority custom action is built, copied into
`resources`, or installed by the MSI. Both Windows architectures remain mandatory release targets, and package/MSI
Expand Down
19 changes: 17 additions & 2 deletions apps/desktop/scripts/packaged-connect-lifecycle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,14 @@ export const boundedChildDiagnostics = records => {
...(journeyStageCodes.has(candidateCode)
&& (record.event === CONNECT_DISCOVERY_MILESTONE_EVENT
|| record.event === CONNECT_JOURNEY_STAGE_EVENT)
? { code: candidateCode }
? {
code: candidateCode,
...(candidateCode === 'JOURNEY_STORAGE_BACKEND'
&& (record.storageBackend === 'gnome_libsecret'
|| record.storageBackend === 'os-protected')
? { storageBackend: record.storageBackend }
: {}),
}
: diagnosticPhases.has(phase) && diagnosticPhaseCodes.has(candidateCode)
? {
phase,
Expand Down Expand Up @@ -515,6 +522,7 @@ export const runPackagedConnectLifecycle = async ({
platform,
arch,
authorityMechanism,
expectedStorageBackend,
sensitiveNeedles = [],
treeKillerPath,
spawn = nodeSpawn,
Expand All @@ -528,6 +536,7 @@ export const runPackagedConnectLifecycle = async ({
const first = deferred();
let firstSettled = false;
let invalidReadyObserved = false;
let reportedStorageBackend;
let child;
const settleFirst = value => {
if (firstSettled) return;
Expand All @@ -539,8 +548,14 @@ export const runPackagedConnectLifecycle = async ({
onSensitiveOutput: () => settleFirst({ category: 'output-rejected' }),
onRecord: record => {
if (records.length < RECORD_MAX_COUNT) records.push(record);
if (record.event === CONNECT_JOURNEY_STAGE_EVENT
&& record.code === 'JOURNEY_STORAGE_BACKEND') {
reportedStorageBackend = record.storageBackend;
}
if (record.event !== CONNECT_READY_EVENT) return;
const valid = isExactReadyRecord(record, { platform, arch, authorityMechanism });
const valid = isExactReadyRecord(record, { platform, arch, authorityMechanism })
&& (expectedStorageBackend === undefined
|| reportedStorageBackend === expectedStorageBackend);
if (!valid) invalidReadyObserved = true;
settleFirst(valid ? { category: 'ready' } : { category: 'ready-validation' });
},
Expand Down
43 changes: 43 additions & 0 deletions apps/desktop/scripts/packaged-connect-lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,49 @@ describe('packaged Connect bounded child lifecycle', () => {
assert.equal(invocations.length, 1);
});

test('requires and preserves the expected fixed storage-backend report before readiness', async () => {
const accepted = await run({
expectedStorageBackend: 'os-protected',
onApp: app => {
app.write({
event: CONNECT_JOURNEY_STAGE_EVENT,
code: 'JOURNEY_STORAGE_BACKEND',
storageBackend: 'os-protected',
});
app.write(readyRecord());
queueMicrotask(() => app.close(0, null));
},
});
assert.equal(accepted.result.ok, true);
assert.deepEqual(accepted.result.records, [
{
event: CONNECT_JOURNEY_STAGE_EVENT,
code: 'JOURNEY_STORAGE_BACKEND',
storageBackend: 'os-protected',
},
{ event: CONNECT_READY_EVENT },
]);

for (const storageBackend of [undefined, 'gnome_libsecret']) {
const rejected = await run({
expectedStorageBackend: 'os-protected',
onApp: app => {
if (storageBackend) {
app.write({
event: CONNECT_JOURNEY_STAGE_EVENT,
code: 'JOURNEY_STORAGE_BACKEND',
storageBackend,
});
}
app.write(readyRecord());
queueMicrotask(() => app.close(0, null));
},
});
assert.equal(rejected.result.ok, false);
assert.equal(rejected.result.category, 'ready-validation');
}
});

test('does not accept an intermediate discovery milestone as terminal readiness', async () => {
const { result } = await run({
onApp: app => {
Expand Down
Loading
Loading