| author | |
| committer | |
| log | 1d694dcbd8d27c994b3bdc66c4bdb199faf2efc3 |
| tree | b5610d5af95cb724327d4f563ca697fcbcc2dcb6 |
| parent | 32407df9c5ffd3c5dda2e745568997ac33c15ed1 |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 40 insertions(+), 2 deletions(-)
.agents/skills/test-t3-mobile/SKILL.md+1-1| ... | @@ -66,7 +66,7 @@ Use these client origins: | ... | @@ -66,7 +66,7 @@ Use these client origins: |
| 66 | - Android Emulator: `http://10.0.2.2:<server-port>` | 66 | - Android Emulator: `http://10.0.2.2:<server-port>` |
| 67 | - Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin | 67 | - Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin |
| 68 | 68 | ||
| 69 | Always 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. | 69 | Enter 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. |
| 70 | 70 | ||
| 71 | ## Start or reuse Metro safely | 71 | ## Start or reuse Metro safely |
| 72 | 72 |
apps/mobile/src/features/connection/pairing.test.ts+21| ... | @@ -1,11 +1,32 @@ | ... | @@ -1,11 +1,32 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | buildPairingUrl, | ||
| 4 | extractPairingUrlFromQrPayload, | 5 | extractPairingUrlFromQrPayload, |
| 5 | PairingQrPayloadEmptyError, | 6 | PairingQrPayloadEmptyError, |
| 6 | parsePairingUrl, | 7 | parsePairingUrl, |
| 7 | } from "./pairing"; | 8 | } from "./pairing"; |
| 8 | 9 | ||
| 10 | describe("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 | |||
| 9 | describe("extractPairingUrlFromQrPayload", () => { | 30 | describe("extractPairingUrlFromQrPayload", () => { |
| 10 | it("trims raw pairing urls from qr payloads", () => { | 31 | it("trims raw pairing urls from qr payloads", () => { |
| 11 | expect( | 32 | expect( |
apps/mobile/src/features/connection/pairing.ts+16-1| ... | @@ -3,6 +3,21 @@ import * as Schema from "effect/Schema"; | ... | @@ -3,6 +3,21 @@ import * as Schema from "effect/Schema"; |
| 3 | 3 | ||
| 4 | const MOBILE_PAIRING_URL_PARAM = "pairingUrl"; | 4 | const MOBILE_PAIRING_URL_PARAM = "pairingUrl"; |
| 5 | 5 | ||
| 6 | function 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 | |||
| 6 | export class PairingQrPayloadEmptyError extends Schema.TaggedErrorClass<PairingQrPayloadEmptyError>()( | 21 | export class PairingQrPayloadEmptyError extends Schema.TaggedErrorClass<PairingQrPayloadEmptyError>()( |
| 7 | "PairingQrPayloadEmptyError", | 22 | "PairingQrPayloadEmptyError", |
| 8 | {}, | 23 | {}, |
| ... | @@ -19,7 +34,7 @@ export function buildPairingUrl(host: string, code: string): string { | ... | @@ -19,7 +34,7 @@ export function buildPairingUrl(host: string, code: string): string { |
| 19 | if (!c) return h; | 34 | if (!c) return h; |
| 20 | 35 | ||
| 21 | try { | 36 | try { |
| 22 | const url = new URL(h.includes("://") ? h : `https://${h}`); | 37 | const url = new URL(h.includes("://") ? h : `${isIpLiteral(h) ? "http" : "https"}://${h}`); |
| 23 | url.hash = new URLSearchParams([["token", c]]).toString(); | 38 | url.hash = new URLSearchParams([["token", c]]).toString(); |
| 24 | return url.toString(); | 39 | return url.toString(); |
| 25 | } catch { | 40 | } catch { |
docs/user/remote-access.md+2| ... | @@ -38,6 +38,8 @@ available. You can set another endpoint as the default from the expanded endpoin | ... | @@ -38,6 +38,8 @@ available. You can set another endpoint as the default from the expanded endpoin |
| 38 | 38 | ||
| 39 | If 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. | 39 | If 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. |
| 40 | 40 | ||
| 41 | In 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 | |||
| 41 | ### Tailscale Endpoints | 43 | ### Tailscale Endpoints |
| 42 | 44 | ||
| 43 | When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list. | 45 | When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list. |