Relay pairing: prove direct + iOS deeplink smoke
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
appId: com.moboudra.paseo
|
||||
---
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Open in.*Paseo.*\\?"
|
||||
commands:
|
||||
- tapOn: "Open"
|
||||
|
||||
- tapOn:
|
||||
text: "Open"
|
||||
optional: true
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Development servers"
|
||||
commands:
|
||||
- tapOn:
|
||||
text: ".*(localhost|127\\.0\\.0\\.1|10\\.0\\.2\\.2|192\\.168\\.).*"
|
||||
optional: true
|
||||
- tapOn:
|
||||
text: "Enter URL manually"
|
||||
optional: true
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Continue"
|
||||
commands:
|
||||
- tapOn: "Continue"
|
||||
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Go home"
|
||||
commands:
|
||||
- tapOn:
|
||||
point: "50%,20%"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: ".*Online.*"
|
||||
timeout: 90000
|
||||
@@ -0,0 +1,27 @@
|
||||
# NOTE: Maestro requires a config section for every flow file, even when used
|
||||
# via `runFlow`. Keep this aligned with the dev build application id.
|
||||
appId: com.moboudra.paseo
|
||||
---
|
||||
# Reusable flow to handle Expo dev client screens after launch.
|
||||
# Unlike `flows/launch.yaml`, this does not assert any particular app route.
|
||||
|
||||
# Handle dev launcher - tap localhost server (dev builds only)
|
||||
- tapOn:
|
||||
text: ".*(localhost|127\\.0\\.0\\.1|10\\.0\\.2\\.2|192\\.168\\.).*"
|
||||
optional: true
|
||||
|
||||
# Dismiss dev menu intro modal
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Continue"
|
||||
commands:
|
||||
- tapOn: "Continue"
|
||||
|
||||
# Dismiss dev menu if it opens
|
||||
- runFlow:
|
||||
when:
|
||||
visible: "Go home"
|
||||
commands:
|
||||
- tapOn:
|
||||
point: "50%,20%"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NOTE: Maestro requires a config section for every flow file, even when used
|
||||
# via `runFlow`. Keep this aligned with the dev build application id.
|
||||
appId: com.moboudra.paseo.dev
|
||||
appId: com.moboudra.paseo
|
||||
---
|
||||
# Reusable flow to handle dev client screens after launchApp
|
||||
# Works with both release builds (no-ops) and dev builds
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
appId: com.moboudra.paseo
|
||||
---
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: flows/dev-client.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
appId: com.moboudra.paseo.dev
|
||||
appId: com.moboudra.paseo
|
||||
---
|
||||
- launchApp:
|
||||
clearState: true
|
||||
@@ -6,13 +6,13 @@ appId: com.moboudra.paseo.dev
|
||||
|
||||
- tapOn: "Paste pairing link"
|
||||
|
||||
- tapOn:
|
||||
text: "https://app.paseo.sh/#offer=..."
|
||||
- eraseText
|
||||
- inputText: ${PAIRING_OFFER_URL}
|
||||
- tapOn: "Pair"
|
||||
|
||||
- assertVisible: "New Agent"
|
||||
- extendedWaitUntil:
|
||||
visible: "New agent"
|
||||
timeout: 30000
|
||||
|
||||
# Confirm we actually connected (relay is the only available connection).
|
||||
- assertVisible: "Online"
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
appId: com.moboudra.paseo.dev
|
||||
appId: com.moboudra.paseo
|
||||
---
|
||||
- launchApp:
|
||||
clearState: true
|
||||
- runFlow: flows/launch.yaml
|
||||
|
||||
- tapOn: "Paste pairing link"
|
||||
- tapOn:
|
||||
text: "https://app.paseo.sh/#offer=..."
|
||||
- eraseText
|
||||
- inputText: ${PAIRING_OFFER_URL}
|
||||
- tapOn: "Pair"
|
||||
- tapOn:
|
||||
point: "75%,36%"
|
||||
|
||||
- extendedWaitUntil:
|
||||
visible: "New agent"
|
||||
|
||||
@@ -297,15 +297,24 @@ function OfferLinkListener({
|
||||
}: {
|
||||
upsertDaemonFromOfferUrl: (offerUrlOrFragment: string) => Promise<unknown>;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const handleUrl = (url: string | null) => {
|
||||
if (!url) return;
|
||||
if (!url.includes("#offer=")) return;
|
||||
void upsertDaemonFromOfferUrl(url).catch((error) => {
|
||||
if (cancelled) return;
|
||||
console.warn("[Linking] Failed to import pairing offer", error);
|
||||
});
|
||||
void upsertDaemonFromOfferUrl(url)
|
||||
.then((profile) => {
|
||||
if (cancelled) return;
|
||||
const serverId = (profile as any)?.serverId;
|
||||
if (typeof serverId !== "string" || !serverId) return;
|
||||
router.replace({ pathname: "/", params: { serverId } });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
console.warn("[Linking] Failed to import pairing offer", error);
|
||||
});
|
||||
};
|
||||
|
||||
void Linking.getInitialURL().then(handleUrl).catch(() => undefined);
|
||||
@@ -318,7 +327,7 @@ function OfferLinkListener({
|
||||
cancelled = true;
|
||||
subscription.remove();
|
||||
};
|
||||
}, [upsertDaemonFromOfferUrl]);
|
||||
}, [router, upsertDaemonFromOfferUrl]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -124,11 +124,14 @@ export function PairLinkModal({ visible, onClose, onSaved }: PairLinkModalProps)
|
||||
<Text style={styles.label}>Pairing link</Text>
|
||||
<InputComponent
|
||||
testID="pair-link-input"
|
||||
nativeID="pair-link-input"
|
||||
accessibilityLabel="pair-link-input"
|
||||
value={offerUrl}
|
||||
onChangeText={setOfferUrl}
|
||||
placeholder="https://app.paseo.sh/#offer=..."
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
style={styles.input}
|
||||
autoFocus
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType="url"
|
||||
@@ -137,7 +140,15 @@ export function PairLinkModal({ visible, onClose, onSaved }: PairLinkModalProps)
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.button} onPress={handleClose} disabled={isSaving} testID="pair-link-cancel">
|
||||
<Pressable
|
||||
style={styles.button}
|
||||
onPress={handleClose}
|
||||
disabled={isSaving}
|
||||
testID="pair-link-cancel"
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Cancel"
|
||||
>
|
||||
<Text style={styles.buttonText}>Cancel</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
@@ -145,6 +156,9 @@ export function PairLinkModal({ visible, onClose, onSaved }: PairLinkModalProps)
|
||||
onPress={() => void handleSave()}
|
||||
disabled={isSaving}
|
||||
testID="pair-link-submit"
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Pair"
|
||||
>
|
||||
<View style={styles.connectRow}>
|
||||
<Link size={16} color={theme.colors.palette.white} />
|
||||
|
||||
@@ -149,7 +149,7 @@ describe("crypto", () => {
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey);
|
||||
|
||||
// QR code would contain: { s: sessionId, k: daemonPubKeyB64, c: [...urls] }
|
||||
// QR code would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
|
||||
|
||||
// === CLIENT SIDE (scans QR) ===
|
||||
const clientKeyPair = await generateKeyPair();
|
||||
|
||||
@@ -25,18 +25,18 @@ describe("E2E Relay with E2EE", () => {
|
||||
});
|
||||
|
||||
it("full flow: daemon and client exchange encrypted messages through relay", async () => {
|
||||
const sessionId = "test-session-" + Date.now();
|
||||
const serverId = "test-session-" + Date.now();
|
||||
|
||||
// === DAEMON SIDE ===
|
||||
// Generate keypair (public key goes in QR)
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey);
|
||||
|
||||
// QR would contain: { s: sessionId, k: daemonPubKeyB64, c: [...urls] }
|
||||
// QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
|
||||
|
||||
// Daemon connects to relay as "server" role
|
||||
const daemonWs = new WebSocket(
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${sessionId}&role=server`
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${serverId}&role=server`
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -59,7 +59,7 @@ describe("E2E Relay with E2EE", () => {
|
||||
|
||||
// Client connects to relay as "client" role
|
||||
const clientWs = new WebSocket(
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${sessionId}&role=client`
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${serverId}&role=client`
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -149,7 +149,7 @@ describe("E2E Relay with E2EE", () => {
|
||||
});
|
||||
|
||||
it("relay only sees opaque bytes after handshake", async () => {
|
||||
const sessionId = "opaque-test-" + Date.now();
|
||||
const serverId = "opaque-test-" + Date.now();
|
||||
|
||||
// Setup keys
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
@@ -172,10 +172,10 @@ describe("E2E Relay with E2EE", () => {
|
||||
|
||||
// Connect both
|
||||
const daemonWs = new WebSocket(
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${sessionId}&role=server`
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${serverId}&role=server`
|
||||
);
|
||||
const clientWs = new WebSocket(
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${sessionId}&role=client`
|
||||
`ws://127.0.0.1:${TEST_PORT}/ws?serverId=${serverId}&role=client`
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
@@ -212,7 +212,7 @@ describe("E2E Relay with E2EE", () => {
|
||||
});
|
||||
|
||||
it("wrong key cannot decrypt", async () => {
|
||||
const sessionId = "wrong-key-test-" + Date.now();
|
||||
const serverId = "wrong-key-test-" + Date.now();
|
||||
|
||||
// Setup - daemon and client with correct keys
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
|
||||
@@ -35,9 +35,9 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
|
||||
liveIt("bridges encrypted traffic end-to-end", { timeout: 45_000 }, async () => {
|
||||
await withRetry(
|
||||
async () => {
|
||||
const sessionId = `live-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const serverUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(sessionId)}&role=server`;
|
||||
const clientUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(sessionId)}&role=client`;
|
||||
const serverId = `live-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const serverUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(serverId)}&role=server`;
|
||||
const clientUrl = `${RELAY_BASE_URL}/ws?serverId=${encodeURIComponent(serverId)}&role=client`;
|
||||
|
||||
// === Key setup ===
|
||||
const daemonKeyPair = await generateKeyPair();
|
||||
|
||||
Reference in New Issue
Block a user