authorgravatar for lucenz@proton.meSimone <lucenz@proton.me> 2026-07-31 00:17:38+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 22:17:38+00:00
log1d694dcbd8d27c994b3bdc66c4bdb199faf2efc3
treeb5610d5af95cb724327d4f563ca697fcbcc2dcb6
parent32407df9c5ffd3c5dda2e745568997ac33c15ed1
signature Signed by PGP key B5690EEEBB952194

fix(mobile): default bare IP pairing to HTTP (#4990)


4 files changed, 40 insertions(+), 2 deletions(-)

.agents/skills/test-t3-mobile/SKILL.md+1-1
......@@ -66,7 +66,7 @@ Use these client origins:
6666- Android Emulator: `http://10.0.2.2:<server-port>`
6767- Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin
6868
69Always enter the complete `http://` origin; the mobile host field otherwise assumes HTTPS. When testing web and mobile together, run `vp run dev --home-dir <base-dir> --host 127.0.0.1` instead and do not launch a second backend over the same base directory.
69Enter the complete `http://` origin to make the test transport explicit. Bare IP addresses default to HTTP, while bare hostnames default to HTTPS. When testing web and mobile together, run `vp run dev --home-dir <base-dir> --host 127.0.0.1` instead and do not launch a second backend over the same base directory.
7070
7171## Start or reuse Metro safely
7272
apps/mobile/src/features/connection/pairing.test.ts+21
......@@ -1,11 +1,32 @@
11import { describe, expect, it } from "vite-plus/test";
22
33import {
4 buildPairingUrl,
45 extractPairingUrlFromQrPayload,
56 PairingQrPayloadEmptyError,
67 parsePairingUrl,
78} from "./pairing";
89
10describe("buildPairingUrl", () => {
11 it("uses HTTP for a schemeless IP address", () => {
12 expect(buildPairingUrl("192.168.1.100:3773", "pairing-token")).toBe(
13 "http://192.168.1.100:3773/#token=pairing-token",
14 );
15 });
16
17 it("keeps HTTPS as the default for a schemeless hostname", () => {
18 expect(buildPairingUrl("remote.example.com", "pairing-token")).toBe(
19 "https://remote.example.com/#token=pairing-token",
20 );
21 });
22
23 it("preserves an explicit scheme for an IP address", () => {
24 expect(buildPairingUrl("https://192.168.1.100:3773", "pairing-token")).toBe(
25 "https://192.168.1.100:3773/#token=pairing-token",
26 );
27 });
28});
29
930describe("extractPairingUrlFromQrPayload", () => {
1031 it("trims raw pairing urls from qr payloads", () => {
1132 expect(
apps/mobile/src/features/connection/pairing.ts+16-1
......@@ -3,6 +3,21 @@ import * as Schema from "effect/Schema";
33
44const MOBILE_PAIRING_URL_PARAM = "pairingUrl";
55
6function isIpLiteral(host: string): boolean {
7 try {
8 const hostname = new URL(`http://${host}`).hostname.replace(/^\[|\]$/g, "");
9 if (hostname.includes(":")) return true;
10
11 const octets = hostname.split(".");
12 return (
13 octets.length === 4 &&
14 octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255)
15 );
16 } catch {
17 return false;
18 }
19}
20
621export class PairingQrPayloadEmptyError extends Schema.TaggedErrorClass<PairingQrPayloadEmptyError>()(
722 "PairingQrPayloadEmptyError",
823 {},
......@@ -19,7 +34,7 @@ export function buildPairingUrl(host: string, code: string): string {
1934 if (!c) return h;
2035
2136 try {
22 const url = new URL(h.includes("://") ? h : `https://${h}`);
37 const url = new URL(h.includes("://") ? h : `${isIpLiteral(h) ? "http" : "https"}://${h}`);
2338 url.hash = new URLSearchParams([["token", c]]).toString();
2439 return url.toString();
2540 } catch {
docs/user/remote-access.md+2
......@@ -38,6 +38,8 @@ available. You can set another endpoint as the default from the expanded endpoin
3838
3939If the copied link points directly at `http://192.168.x.y:3773`, open it from a client that can reach that LAN address. If it points at `https://app.t3.codes/pair?...`, the hosted web app will save the environment and connect directly to the backend URL in the link.
4040
41In the mobile app's **Add Environment** form, a numeric IP address without a scheme uses HTTP. Include `https://` explicitly when the backend is served over HTTPS.
42
4143### Tailscale Endpoints
4244
4345When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list.