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
18 changes: 16 additions & 2 deletions apps/desktop/scripts/packaged-connect-evidence.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ export const PACKAGED_CONNECT_EXPECTED_DISCOVERY_COUNT = 10;
export const PACKAGED_CONNECT_EVIDENCE_FAILURE_CODES = Object.freeze([
'DISCOVERY_COUNT_MISMATCH',
'DISCOVERY_AUTHORIZATION_PRESENT',
'PAIRING_START_COUNT_MISMATCH',
'PAIRING_START_MISSING',
'PAIRING_START_DUPLICATE',
'PAIRING_BROWSER_COUNT_MISMATCH',
'PAIRING_POLL_COUNT_MISMATCH',
'PAIRING_ACTIVATION_COUNT_MISMATCH',
'PAIRING_METHOD_MISMATCH',
'PAIRING_BROWSER_CREDENTIAL_PRESENT',
'PAIRING_INTENT_SEQUENCE_MISMATCH',
'PAIRING_LIFECYCLE_ISOLATION_FAILED',
'PAIRING_REQUEST_AFTER_TERMINAL',
'DELAYED_APPROVAL_READINESS_MISSING',
'BOOTSTRAP_AUTHORIZATION_PRESENT',
'AUTHENTICATED_REST_COUNT_MISMATCH',
'AUTHENTICATED_SOCKET_COUNT_MISMATCH',
Expand Down Expand Up @@ -41,10 +48,17 @@ const failureChecks = Object.freeze([
['DISCOVERY_COUNT_MISMATCH', evidence =>
evidence.discoveryCount !== PACKAGED_CONNECT_EXPECTED_DISCOVERY_COUNT],
['DISCOVERY_AUTHORIZATION_PRESENT', evidence => evidence.discoveryAuthorizationPresent],
['PAIRING_START_COUNT_MISMATCH', evidence => evidence.pairingStartCount !== 3],
['PAIRING_START_MISSING', evidence => evidence.pairingStartCount < 3],
['PAIRING_START_DUPLICATE', evidence => evidence.pairingStartCount > 3],
['PAIRING_BROWSER_COUNT_MISMATCH', evidence => evidence.pairingBrowserCount !== 3],
['PAIRING_POLL_COUNT_MISMATCH', evidence => evidence.pairingPollCount !== 1],
['PAIRING_ACTIVATION_COUNT_MISMATCH', evidence => evidence.pairingActivationCount !== 1],
['PAIRING_METHOD_MISMATCH', evidence => !evidence.pairingMethodBoundaryValid],
['PAIRING_BROWSER_CREDENTIAL_PRESENT', evidence => evidence.pairingBrowserCredentialPresent],
['PAIRING_INTENT_SEQUENCE_MISMATCH', evidence => !evidence.pairingIntentSequenceValid],
['PAIRING_LIFECYCLE_ISOLATION_FAILED', evidence => !evidence.pairingLifecycleIsolated],
['PAIRING_REQUEST_AFTER_TERMINAL', evidence => evidence.pairingRequestAfterTerminal],
['DELAYED_APPROVAL_READINESS_MISSING', evidence => !evidence.delayedApprovalReadinessProven],
['BOOTSTRAP_AUTHORIZATION_PRESENT', evidence => evidence.bootstrapAuthorizationPresent],
['AUTHENTICATED_REST_COUNT_MISMATCH', evidence => evidence.authenticatedRestCount < 2],
['AUTHENTICATED_SOCKET_COUNT_MISMATCH', evidence => evidence.authenticatedSocketCount < 2],
Expand Down
15 changes: 14 additions & 1 deletion apps/desktop/scripts/packaged-connect-evidence.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const passingEvidence = () => ({
pairingBrowserCount: 3,
pairingPollCount: 1,
pairingActivationCount: 1,
pairingMethodBoundaryValid: true,
pairingBrowserCredentialPresent: false,
pairingIntentSequenceValid: true,
pairingLifecycleIsolated: true,
pairingRequestAfterTerminal: false,
delayedApprovalReadinessProven: true,
bootstrapAuthorizationPresent: false,
authenticatedRestCount: 2,
authenticatedSocketCount: 2,
Expand All @@ -31,10 +37,17 @@ const passingEvidence = () => ({
const failingEvidence = Object.freeze({
DISCOVERY_COUNT_MISMATCH: { discoveryCount: 8 },
DISCOVERY_AUTHORIZATION_PRESENT: { discoveryAuthorizationPresent: true },
PAIRING_START_COUNT_MISMATCH: { pairingStartCount: 2 },
PAIRING_START_MISSING: { pairingStartCount: 2 },
PAIRING_START_DUPLICATE: { pairingStartCount: 4 },
PAIRING_BROWSER_COUNT_MISMATCH: { pairingBrowserCount: 2 },
PAIRING_POLL_COUNT_MISMATCH: { pairingPollCount: 2 },
PAIRING_ACTIVATION_COUNT_MISMATCH: { pairingActivationCount: 2 },
PAIRING_METHOD_MISMATCH: { pairingMethodBoundaryValid: false },
PAIRING_BROWSER_CREDENTIAL_PRESENT: { pairingBrowserCredentialPresent: true },
PAIRING_INTENT_SEQUENCE_MISMATCH: { pairingIntentSequenceValid: false },
PAIRING_LIFECYCLE_ISOLATION_FAILED: { pairingLifecycleIsolated: false },
PAIRING_REQUEST_AFTER_TERMINAL: { pairingRequestAfterTerminal: true },
DELAYED_APPROVAL_READINESS_MISSING: { delayedApprovalReadinessProven: false },
BOOTSTRAP_AUTHORIZATION_PRESENT: { bootstrapAuthorizationPresent: true },
AUTHENTICATED_REST_COUNT_MISMATCH: { authenticatedRestCount: 1 },
AUTHENTICATED_SOCKET_COUNT_MISMATCH: { authenticatedSocketCount: 1 },
Expand Down
12 changes: 11 additions & 1 deletion apps/desktop/scripts/packaged-connect-lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,19 @@ describe('packaged Connect bounded child lifecycle', () => {
assert.match(accounting, /pairingActivationCount: pairingActivations\.length/u);

const evaluator = await readFile(new URL('./packaged-connect-evidence.mjs', import.meta.url), 'utf8');
assert.match(evaluator, /evidence\.pairingStartCount !== 3/u);
assert.match(evaluator, /evidence\.pairingStartCount < 3/u);
assert.match(evaluator, /evidence\.pairingStartCount > 3/u);
assert.match(evaluator, /evidence\.pairingBrowserCount !== 3/u);
assert.match(evaluator, /evidence\.pairingPollCount !== 1/u);
assert.match(evaluator, /evidence\.pairingActivationCount !== 1/u);
assert.match(harness, /request\.method === 'POST'[\s\S]*?request\.url === '\/api\/desktop\/pairings'/u);
assert.match(harness, /request\.method === 'GET'[\s\S]*?\/\\\/browser\$\//u);
assert.match(harness, /pairingBrowserCredentialPresent: pairingBrowsers\.some/u);
assert.doesNotMatch(evaluator, /pairingPollCount < 3/u);
assert.match(harness, /const approvalReadinessDelayMs = process\.platform === 'darwin' \? 300 : 0/u);
assert.match(harness, /pairingIntentSequenceValid: hasExactModes\(pairingStarts\) && hasExactModes\(pairingBrowsers\)/u);
assert.match(harness, /pairingRequestAfterTerminal: bootstrap\.length !== pairingRequestCountAtPairTerminal/u);
assert.match(harness, /delayedApprovalReadinessProven: process\.platform !== 'darwin'/u);
});

test('accepts an exact ready proof followed by a clean exit', async () => {
Expand Down Expand Up @@ -423,6 +431,8 @@ describe('packaged Connect bounded child lifecycle', () => {
assert.ok(manual >= 0 && browser >= 0 && credential >= 0 && reprobeReady >= 0
&& activation >= 0 && publication >= 0 && react >= 0);
assert.match(main, /await stages\.waitFor\('CREDENTIAL_COMMITTED'\)[\s\S]*?await stages\.waitFor\('AUTHENTICATED_REPROBE_READY'\)[\s\S]*?await stages\.waitFor\('ACTIVATION_COMMITTED'\)[\s\S]*?await stages\.waitFor\('ACTIVATION_PUBLISHED'\)[\s\S]*?await stages\.waitFor\('REACT_CONNECTED'\)/u);
assert.match(main, /Packaged pairing expiry classification failed[\s\S]*?await waitForApprovalIdle\(\)[\s\S]*?JOURNEY_NEGATIVE_CANCEL[\s\S]*?const approvalReady = waitForNextApproval\(\)[\s\S]*?await approvalReady[\s\S]*?Packaged pairing cancellation classification failed[\s\S]*?await waitForApprovalIdle\(\)/u);
assert.match(main, /await waitForApprovalIdle\(\);\s+reportPackagedConnectJourneyStage\(phase === 'pair'\s+\? 'JOURNEY_PAIR_COMPLETE'/u);
assert.match(main, /if \(packagedSmokeTest && !transportSmoke && !connectJourney\)/u);
assert.match(main, /if \(packagedSmokeTest && !connectJourney\) \{/u);
assert.doesNotMatch(main, /JOURNEY_PAIR_RENDERER|JOURNEY_REPROBE_RENDERER/u);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/scripts/packaged-connect-platform.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('packaged Connect target-native credential setup', () => {
const stable = darwinRunner.indexOf('node "$signature_verifier" stable');
assert.ok(establish >= 0 && establish < smoke && smoke < stable);
assert.match(packagedConnectSmoke, /const runPhase = async phase => await runPackagedConnectLifecycle\([\s\S]*?spawn: spawnLifecycleProcess/u);
assert.match(packagedConnectSmoke, /outcome = await runPhase\('pair'\);[\s\S]*?if \(outcome\.ok && journeyFixture\) \{\s*outcome = await runPhase\('reprobe'\);/u);
assert.match(packagedConnectSmoke, /outcome = await runPhase\('pair'\);\s*if \(outcome\.ok && journeyFixture\) \{\s*const pairingRequestCountAtPairTerminal = journeyFixture\.requests\.filter\(request =>[^{};]+\)\.length;\s*outcome = await runPhase\('reprobe'\);/u);
assert.match(workflow, /target: darwin-x64\s+runner: macos-15-intel\s+platform: darwin\s+arch: x64/u);
assert.match(workflow, /target: darwin-arm64\s+runner: macos-15\s+platform: darwin\s+arch: arm64/u);
});
Expand Down
43 changes: 39 additions & 4 deletions apps/desktop/scripts/smoke-packaged-connect.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ const createPackagedJourneyFixture = async () => {
let active = false;
let binding;
let mode = 'success';
let modeGeneration = 0;
const approvalReadinessDelayMs = process.platform === 'darwin' ? 300 : 0;
const cors = {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers': 'Authorization, Content-Type, X-ProPR-Desktop-Transport-Scope',
Expand Down Expand Up @@ -130,7 +132,11 @@ const createPackagedJourneyFixture = async () => {
authorization: request.headers.authorization ?? null,
origin: request.headers.origin ?? null,
transportScope: request.headers[DESKTOP_TRANSPORT_SCOPE_HEADER.toLowerCase()] ?? null,
credentialHeadersPresent: Object.keys(request.headers).some(name =>
['authorization', 'cookie', 'proxy-authorization'].includes(name.toLowerCase())),
socketIo: false,
fixtureMode: mode,
fixtureModeGeneration: modeGeneration,
};
requests.push(record);
if (request.method === 'OPTIONS') {
Expand All @@ -145,6 +151,7 @@ const createPackagedJourneyFixture = async () => {
throw new Error('invalid fixture mode');
}
mode = requestedMode;
modeGeneration += 1;
approved = false;
binding = undefined;
response.writeHead(204, cors);
Expand Down Expand Up @@ -208,6 +215,12 @@ const createPackagedJourneyFixture = async () => {
return;
}
if (request.method === 'GET' && request.url === `/api/desktop/pairings/${pairingId}/browser`) {
if (approvalReadinessDelayMs > 0) {
await new Promise(resolve => setTimeout(resolve, approvalReadinessDelayMs));
record.approvalReadinessDelayed = true;
}
record.fixtureModeStable = record.fixtureMode === mode
&& record.fixtureModeGeneration === modeGeneration;
approved = true;
response.writeHead(200, { 'Cache-Control': 'no-store', 'Content-Type': 'text/html' });
response.end('<!doctype html><title>Desktop approved</title><p>Approved</p>');
Expand Down Expand Up @@ -601,6 +614,10 @@ try {
});
outcome = await runPhase('pair');
if (outcome.ok && journeyFixture) {
const pairingRequestCountAtPairTerminal = journeyFixture.requests.filter(request =>
request.method !== 'OPTIONS'
&& (request.url === '/api/desktop/pairings'
|| /^\/api\/desktop\/pairings\/[^/]+\/(?:browser|poll|activate)$/u.test(request.url ?? ''))).length;
outcome = await runPhase('reprobe');
if (outcome.ok) {
const applicationRequests = journeyFixture.requests.filter(request => request.method !== 'OPTIONS');
Expand All @@ -609,10 +626,17 @@ try {
request.url === '/api/desktop/pairings'
|| /^\/api\/desktop\/pairings\/[^/]+\/(?:poll|activate)$/u.test(request.url ?? '')
|| /\/browser$/u.test(request.url ?? ''));
const pairingStarts = bootstrap.filter(request => request.url === '/api/desktop/pairings');
const pairingBrowsers = bootstrap.filter(request => /\/browser$/u.test(request.url ?? ''));
const pairingPolls = bootstrap.filter(request => /\/poll$/u.test(request.url ?? ''));
const pairingActivations = bootstrap.filter(request => /\/activate$/u.test(request.url ?? ''));
const pairingStarts = bootstrap.filter(request => request.method === 'POST'
&& request.url === '/api/desktop/pairings');
const pairingBrowsers = bootstrap.filter(request => request.method === 'GET'
&& /\/browser$/u.test(request.url ?? ''));
const pairingPolls = bootstrap.filter(request => request.method === 'POST'
&& /\/poll$/u.test(request.url ?? ''));
const pairingActivations = bootstrap.filter(request => request.method === 'POST'
&& /\/activate$/u.test(request.url ?? ''));
const intendedPairingModes = ['expiry', 'cancel', 'success'];
const hasExactModes = requests => requests.length === intendedPairingModes.length
&& requests.every((request, index) => request.fixtureMode === intendedPairingModes[index]);
const authenticatedRest = applicationRequests.filter(request =>
request.socketIo === false
&& request.url === '/api/auth/user'
Expand All @@ -632,6 +656,17 @@ try {
pairingBrowserCount: pairingBrowsers.length,
pairingPollCount: pairingPolls.length,
pairingActivationCount: pairingActivations.length,
pairingMethodBoundaryValid: bootstrap.length === pairingStarts.length
+ pairingBrowsers.length + pairingPolls.length + pairingActivations.length,
pairingBrowserCredentialPresent: pairingBrowsers.some(request =>
request.credentialHeadersPresent === true),
pairingIntentSequenceValid: hasExactModes(pairingStarts) && hasExactModes(pairingBrowsers),
pairingLifecycleIsolated: pairingBrowsers.every(request => request.fixtureModeStable === true)
&& pairingPolls.every(request => request.fixtureMode === 'success')
&& pairingActivations.every(request => request.fixtureMode === 'success'),
pairingRequestAfterTerminal: bootstrap.length !== pairingRequestCountAtPairTerminal,
delayedApprovalReadinessProven: process.platform !== 'darwin'
|| pairingBrowsers.every(request => request.approvalReadinessDelayed === true),
bootstrapAuthorizationPresent: bootstrap.some(request => request.authorization !== null),
authenticatedRestCount: authenticatedRest.length,
authenticatedSocketCount: socketEvidence.authenticatedSocketCount,
Expand Down
21 changes: 18 additions & 3 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { openApprovedDesktopPairingUrl } from './pairing-browser';
import {
clearPackagedApprovalStorage,
createPackagedApprovalNavigation,
createPackagedApprovalTaskTracker,
packagedApprovalPartition,
} from './packaged-approval-session';
import { createDesktopShutdownCoordinator } from './shutdown';
Expand Down Expand Up @@ -604,6 +605,8 @@ const runPackagedConnectJourneySmoke = async (
window: BrowserWindow,
profiles: ProfileStore,
credentials: DesktopCredentialService,
waitForNextApproval: () => Promise<void>,
waitForApprovalIdle: () => Promise<void>,
endpoint: string,
phase: 'pair' | 'reprobe',
stages: PackagedJourneyStageTracker,
Expand Down Expand Up @@ -649,11 +652,14 @@ const runPackagedConnectJourneySmoke = async (
}
},
);
await waitForApprovalIdle();
reportPackagedConnectJourneyStage('JOURNEY_NEGATIVE_CANCEL');
await setMode('cancel');
const approvalReady = waitForNextApproval();
const cancelledPairing = credentials.pair({
id: 'negative-cancel', label: 'Packaged cancel', apiBaseUrl: endpoint,
});
await approvalReady;
await new Promise(resolve => setTimeout(resolve, 50));
credentials.cancelPairing('negative-cancel');
await cancelledPairing.then(
Expand All @@ -664,6 +670,7 @@ const runPackagedConnectJourneySmoke = async (
}
},
);
await waitForApprovalIdle();
reportPackagedConnectJourneyStage('JOURNEY_NEGATIVE_STATE');
const failedProfiles = await profiles.list();
if (failedProfiles.profiles.some(profile => profile.id.startsWith('negative-'))) {
Expand Down Expand Up @@ -774,6 +781,7 @@ const runPackagedConnectJourneySmoke = async (
|| transportEvidence.authenticatedSockets < requiredAuthenticatedRequests) {
throw new Error('Packaged Connect authenticated transport proof timed out');
}
await waitForApprovalIdle();
reportPackagedConnectJourneyStage(phase === 'pair'
? 'JOURNEY_PAIR_COMPLETE'
: 'JOURNEY_REPROBE_COMPLETE');
Expand Down Expand Up @@ -1192,11 +1200,14 @@ if (!hasSingleInstanceLock) {
return status;
},
});
const packagedJourneyApprovals = connectSmoke?.journeyEndpoint
? createPackagedApprovalTaskTracker(openPackagedJourneyApproval)
: null;
const credentials = new DesktopCredentialService({
profiles,
fetch: session.defaultSession.fetch.bind(session.defaultSession) as typeof globalThis.fetch,
openPairingBrowser: connectSmoke?.journeyEndpoint
? openPackagedJourneyApproval
openPairingBrowser: packagedJourneyApprovals
? packagedJourneyApprovals.open
: request => openApprovedDesktopPairingUrl(request, shell),
clientName: `ProPR Desktop (${process.platform})`,
reportRevocationFailure: diagnostic => {
Expand Down Expand Up @@ -1275,11 +1286,15 @@ if (!hasSingleInstanceLock) {
reportPackagedConnectJourneyStage('JOURNEY_DISCOVERY_RENDERER');
const readyFields = await runPackagedConnectDiscoverySmoke(mainWindow);
if (connectSmoke.journeyEndpoint && connectSmoke.journeyPhase) {
if (!journeyStages) throw new Error('Packaged Connect journey stage tracker was unavailable');
if (!journeyStages || !packagedJourneyApprovals) {
throw new Error('Packaged Connect journey stage tracker was unavailable');
}
await runPackagedConnectJourneySmoke(
mainWindow,
profiles,
credentials,
packagedJourneyApprovals.waitForNextOpen,
packagedJourneyApprovals.waitForIdle,
connectSmoke.journeyEndpoint,
connectSmoke.journeyPhase,
journeyStages,
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/src/packaged-approval-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { BrowserWindow, Session } from 'electron';
import {
clearPackagedApprovalStorage,
createPackagedApprovalNavigation,
createPackagedApprovalTaskTracker,
packagedApprovalPartition,
} from './packaged-approval-session';

Expand Down Expand Up @@ -204,6 +205,36 @@ describe('packaged pairing approval isolated session', () => {
await controller.cleanup();
});

it('reports owned approval readiness and drains delayed work before the next pairing case', async () => {
const requested: string[] = [];
const releases: Array<() => void> = [];
const tracker = createPackagedApprovalTaskTracker<string>(async request => {
requested.push(request);
await new Promise<void>(resolve => { releases.push(resolve); });
});

for (const request of ['expiry', 'cancel', 'success']) {
let ready = false;
const readiness = tracker.waitForNextOpen().then(() => { ready = true; });
const concurrentReadiness = tracker.waitForNextOpen();
await new Promise<void>(resolve => setImmediate(resolve));
assert.equal(ready, false);
const opened = tracker.open(request);
await Promise.all([readiness, concurrentReadiness]);
assert.equal(ready, true);
let idle = false;
const drained = tracker.waitForIdle().then(() => { idle = true; });
await new Promise<void>(resolve => setImmediate(resolve));
assert.equal(idle, false);
assert.equal(requested.filter(value => value === request).length, 1);
releases.shift()?.();
await Promise.all([opened, drained]);
assert.equal(idle, true);
}

assert.deepEqual(requested, ['expiry', 'cancel', 'success']);
});

it('cancels an incidental resource without invalidating the exact main-frame approval', async () => {
const value = harness();
const originalLoad = value.window.load;
Expand Down
Loading
Loading