diff --git a/docs/features.md b/docs/features.md
index bce3a51a..3e65e844 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -1517,14 +1517,17 @@ changing a feature: each entry records what the owner decided, why, and what hol
it is the owner's decision, not made. Held by `phoneAccess.test.ts`, `places.test.ts`, the
server test "never lists or reads a folder that is really on another computer" and the
`phone` e2e ("a code is refused once the dialog closed").
- **THE ADDRESS PHONES USE** (#406; owner, 2026-10-16: "so that we can support custom web
- addresses"): optional, More > Phone. The QR and link carry it, so a phone can reach Prism by
- a friendly name or over real HTTPS through the user's own reverse proxy, which is what
- encrypts the token. Only an http(s) origin is kept (`parseAddress`); empty is this PC's
- address. Through a proxy, every client address the request names must also be on the user's
- own network (`forwardedLocal`), so an internet-facing proxy cannot carry the internet in.
- Setup guide: `docs/phone-address.md`. Held by `store.test.ts`, `lan.test.ts`, the server test
- "refuses a request a proxy forwarded from outside the network" and the `phone` e2e.
+ **NO "ADDRESS PHONES USE"; REMOTE ACCESS BECOMES AN APP** (owner, 2026-10-16, reversing
+ #406 the same day: "remove the reverse proxy thing from Prism... we'll instead start
+ eventually looking at making it an app where you can just remote into your PC safely... the
+ profile can be combined with that app"). The optional address (#406, 0.116.0) is gone: a
+ trusted HTTPS name needed either the user's own domain and proxy, or a trust profile
+ installed on every phone, which is not something people should have to do by hand. HTTPS
+ and remote access come later, in a dedicated app that can carry the trust itself. What stays
+ from #406 is a wall: through any proxy a user puts in front of Prism by hand, every client
+ address the request names must also be on the user's own network (`forwardedLocal`). Held by
+ `lan.test.ts` and the server test "refuses a request a proxy forwarded from outside the
+ network".
- **THE PHONE SEES THE OPEN TABS AND SWITCHES BETWEEN THEM** (superseded 2026-10-14, above; owner, 2026-09-08, from the
same session: "i should be able to switch tabs without scanning a new qr code. i should be
able to see the available tabs and switch"; #107). This REVERSES the design's own first
diff --git a/docs/phone-address.md b/docs/phone-address.md
deleted file mode 100644
index 58b85a15..00000000
--- a/docs/phone-address.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# The address phones use (#406)
-
-Optional. Without it a phone pairs with `http://:`, as it always has.
-With it, the pairing QR and link carry an address of your choosing: a friendly name on your
-own network, or HTTPS through your own reverse proxy. Prism never ships a certificate and never
-needs any of this.
-
-Set it in **More > Phone > Address phones use**. Only an origin is kept: `http://` or
-`https://`, a host and an optional port. No path, query or login. Empty goes back to this PC's
-address.
-
-## Why HTTPS matters here
-
-Prism's phone server is plain HTTP, so the phone's access token travels unencrypted on your
-Wi-Fi (security review, `research/prism/2026-10-14-phone-access-review.md`). A reverse proxy
-with a real certificate encrypts it, and the phone shows no warning. A certificate from Let's
-Encrypt needs a domain you own. A name that only your own network knows (`prism.home`) can be
-friendly, but it cannot get a certificate a phone trusts.
-
-## One way to set it up (Nginx Proxy Manager and AdGuard Home)
-
-The owner's setup, `Documents\homelab\compose.yaml`; any reverse proxy and local DNS work the
-same way.
-
-1. **Name to PC.** In AdGuard Home (admin `http://127.0.0.1:8080`), Filters > DNS rewrites, add
- `prism.example.com` -> this PC's LAN address. The phone must use AdGuard for DNS (router DHCP
- or the phone's own DNS setting).
-2. **Proxy to Prism.** In Nginx Proxy Manager (admin `http://127.0.0.1:81`), add a proxy host for
- `prism.example.com`, forwarding to `http://:` (the
- port is shown in More > Phone). Turn on "Websockets support" and "Block common exploits".
-3. **Certificate.** On the SSL tab, request a Let's Encrypt certificate with a **DNS challenge**
- (Cloudflare and others): it works without opening any port to the internet. Turn on "Force
- SSL".
-4. **Prism.** More > Phone > Address phones use: `https://prism.example.com`, Save. Pair again.
-
-## The walls, through a proxy
-
-- Every request reaches Prism from the proxy, which is on your own network, so Prism's
- "own network only" rule passes it. Every client address the proxy reports (`X-Real-IP`, each
- `X-Forwarded-For` hop, `Forwarded: for=`) must ALSO be on your own network, or the request is
- refused (`forwardedLocal` in `src/main/phone/lan.ts`). A proxy that is reachable from the
- internet therefore still cannot carry the internet in.
-- **Do not forward ports 80 or 443 to this PC on your router.** The DNS challenge in step 3 is
- what makes that unnecessary.
-- Pairing, the code's two minutes, read-only access and the per-phone walls are unchanged.
diff --git a/package-lock.json b/package-lock.json
index 2ec9f76f..9c6a2d97 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "prism",
- "version": "0.117.8",
+ "version": "0.117.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prism",
- "version": "0.117.8",
+ "version": "0.117.9",
"license": "MIT",
"dependencies": {
"@codemirror/commands": "^6.11.0",
diff --git a/package.json b/package.json
index 543638fb..02f55908 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "prism",
- "version": "0.117.8",
+ "version": "0.117.9",
"description": "A fast, universal media viewer for Windows.",
"author": "Max",
"license": "MIT",
diff --git a/src/main/phone/index.ts b/src/main/phone/index.ts
index 886d272d..defc7215 100644
--- a/src/main/phone/index.ts
+++ b/src/main/phone/index.ts
@@ -7,9 +7,9 @@ import { parsePins, type PhonePin } from './places'
import { DEFAULT_PORT, PhoneServer } from './server'
import type { HlsJobs } from './jobs'
import type { Positions } from '../positions'
-import { parseAddress, parseStore, serializeStore, type PhoneStore } from './store'
+import { parseStore, serializeStore, type PhoneStore } from './store'
import { lanAddresses } from './lan'
-import { pairLink, pairLinkAt } from './routes'
+import { pairLink } from './routes'
import { qrSvg } from './qr'
import { forget as forgetPhone } from './pairing'
import { writeAtomic } from '../latestWrite'
@@ -117,18 +117,15 @@ export function startPhoneService(opts: {
addresses: E2E ? ['127.0.0.1'] : lanAddresses(networkInterfaces()),
phones: phoneStore.pairing.phones.map((p) => ({ ...p })),
watching: phone?.watching() ?? [],
- error: phoneError || undefined,
- address: phoneStore.address
+ error: phoneError || undefined
}
// A code from ANY tab (2026-10-14): it carries the folder that tab is
// showing as the phone's first stop, or nothing (This PC).
- if (phone && (phoneStore.address || state.addresses[0])) {
+ if (phone && state.addresses[0]) {
const now = Date.now()
const live = phoneStore.pairing.codes.find((c) => c.start === start && c.expires > now)
const { code, expires } = live ?? phone.issue(start)
- const link = phoneStore.address
- ? pairLinkAt(phoneStore.address, code)
- : pairLink(state.addresses[0], phone.port ?? DEFAULT_PORT, code)
+ const link = pairLink(state.addresses[0], phone.port ?? DEFAULT_PORT, code)
state.code = { code, link, svg: await qrSvg(link), expires }
}
return state
@@ -144,18 +141,6 @@ export function startPhoneService(opts: {
else await stopPhone()
return phoneState(startArg(start))
})
- // The address phones use (#406): empty clears it; anything that is not an
- // http(s) origin is refused and the old value stays.
- ipcMain.handle(CALL.phoneAddressSet, async (_e, raw: unknown, start: unknown) => {
- await phoneReady
- const empty = typeof raw === 'string' && !raw.trim()
- const address = parseAddress(raw)
- if (empty || address) {
- phoneStore.address = empty ? null : address
- savePhone()
- }
- return phoneState(startArg(start))
- })
ipcMain.handle(CALL.phoneCode, async (_e, start: unknown) => {
await phoneReady
const at = startArg(start)
diff --git a/src/main/phone/routes.ts b/src/main/phone/routes.ts
index d99bb02d..baa7690e 100644
--- a/src/main/phone/routes.ts
+++ b/src/main/phone/routes.ts
@@ -95,8 +95,3 @@ export function tokenOf(query: URLSearchParams, auth: string | undefined): strin
export function pairLink(address: string, port: number, code: string): string {
return `http://${address}:${port}/?code=${code}`
}
-
-/** The same link through the address phones use (#406): an origin `parseAddress` kept. */
-export function pairLinkAt(origin: string, code: string): string {
- return `${origin}/?code=${code}`
-}
diff --git a/src/main/phone/store.test.ts b/src/main/phone/store.test.ts
index 8c4f61bf..9131c569 100644
--- a/src/main/phone/store.test.ts
+++ b/src/main/phone/store.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { parseAddress, parseStore, serializeStore } from './store'
+import { parseStore, serializeStore } from './store'
describe('phone store', () => {
it('round-trips and defaults', () => {
@@ -9,8 +9,8 @@ describe('phone store', () => {
expect(s.on).toBe(true)
expect(s.port).toBe(47321)
expect(s.pairing.phones).toHaveLength(1)
- expect(parseStore('soup')).toEqual({ on: false, port: null, address: null, pairing: { codes: [], phones: [] } })
- expect(parseStore('')).toEqual({ on: false, port: null, address: null, pairing: { codes: [], phones: [] } })
+ expect(parseStore('soup')).toEqual({ on: false, port: null, pairing: { codes: [], phones: [] } })
+ expect(parseStore('')).toEqual({ on: false, port: null, pairing: { codes: [], phones: [] } })
expect(JSON.parse(serializeStore(s))).toMatchObject({ on: true, port: 47321 })
})
@@ -25,18 +25,4 @@ describe('phone store', () => {
expect(serializeStore(s)).not.toContain('ABCDEF')
expect(JSON.parse(serializeStore(s))).toEqual({ on: false, port: null, phones: [] })
})
-
- it('keeps the address phones use (#406), as an origin only', () => {
- const s = parseStore('{"on":true,"port":47321,"address":"https://prism.example.com/","phones":[]}')
- expect(s.address).toBe('https://prism.example.com')
- expect(JSON.parse(serializeStore(s)).address).toBe('https://prism.example.com')
- expect(JSON.parse(serializeStore({ ...s, address: null }))).not.toHaveProperty('address')
- })
-
- it('takes http or https with a host and a port, nothing else', () => {
- expect(parseAddress('http://prism.home')).toBe('http://prism.home')
- expect(parseAddress(' https://Prism.Example.com:8443 ')).toBe('https://prism.example.com:8443')
- for (const bad of ['', 'prism.home', 'ftp://x', 'javascript:alert(1)', 'https://u:p@x.com', 'https://x.com/path', 'https://x.com/?q=1', 'https://x.com/#h', 42, null])
- expect(parseAddress(bad)).toBeNull()
- })
})
diff --git a/src/main/phone/store.ts b/src/main/phone/store.ts
index 7e662892..31a58eb3 100644
--- a/src/main/phone/store.ts
+++ b/src/main/phone/store.ts
@@ -9,55 +9,23 @@ import { emptyState, parseState, type PairState } from './pairing'
export interface PhoneStore {
on: boolean
port: number | null
- /** The address phones use (#406), or null for this PC's own address. */
- address: string | null
pairing: PairState
}
-/**
- * THE ADDRESS PHONES USE (#406; owner, 2026-10-16: "so that we can support
- * custom web addresses"). Optional: a friendly name the user's own network
- * resolves (`http://prism.home`) or HTTPS through the user's own reverse
- * proxy (`https://prism.example.com`), which is what lets the token travel
- * encrypted with no certificate warning on the phone. Prism never ships a
- * certificate and never needs one of these. Only an origin is kept: http or
- * https, a host, an optional port; no user, no path, no query.
- */
-export function parseAddress(raw: unknown): string | null {
- if (typeof raw !== 'string') return null
- const text = raw.trim()
- if (!text || text.length > 300) return null
- let url: URL
- try {
- url = new URL(text)
- } catch {
- return null
- }
- if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
- if (!url.hostname || url.username || url.password || url.search || url.hash) return null
- if (url.pathname !== '/' && url.pathname !== '') return null
- return url.origin
-}
-
export function parseStore(raw: string): PhoneStore {
try {
- const j = JSON.parse(raw) as { on?: unknown; port?: unknown; address?: unknown } | null
- if (!j || typeof j !== 'object') return { on: false, port: null, address: null, pairing: emptyState() }
+ const j = JSON.parse(raw) as { on?: unknown; port?: unknown } | null
+ if (!j || typeof j !== 'object') return { on: false, port: null, pairing: emptyState() }
return {
on: j.on === true,
port: typeof j.port === 'number' && Number.isInteger(j.port) && j.port > 0 && j.port < 65536 ? j.port : null,
- address: parseAddress(j.address),
pairing: parseState(raw)
}
} catch {
- return { on: false, port: null, address: null, pairing: emptyState() }
+ return { on: false, port: null, pairing: emptyState() }
}
}
export function serializeStore(s: PhoneStore): string {
- return JSON.stringify(
- { on: s.on, port: s.port, ...(s.address ? { address: s.address } : {}), phones: s.pairing.phones },
- null,
- 2
- )
+ return JSON.stringify({ on: s.on, port: s.port, phones: s.pairing.phones }, null, 2)
}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 85879127..a0bd088b 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -225,9 +225,6 @@ const api = {
ipcRenderer.invoke(CALL.phoneSetOn, on, start),
/** A fresh code for `start`, replacing the one it had. */
phoneCode: (start: string | null): Promise => ipcRenderer.invoke(CALL.phoneCode, start),
- /** The address phones use (#406); an empty string clears it. */
- phoneAddressSet: (address: string, start: string | null): Promise =>
- ipcRenderer.invoke(CALL.phoneAddressSet, address, start),
/** The dialog closed: no code it showed may still pair a phone. */
phoneCodeDrop: (): void => ipcRenderer.send(CALL.phoneCodeDrop),
/** Forget a paired phone by token. */
diff --git a/src/renderer/src/components/PhoneDialog.tsx b/src/renderer/src/components/PhoneDialog.tsx
index 4b4cb567..1056d4ac 100644
--- a/src/renderer/src/components/PhoneDialog.tsx
+++ b/src/renderer/src/components/PhoneDialog.tsx
@@ -80,7 +80,6 @@ export function PhoneDialog({
)}
{state.error &&
{state.error}
}
-
{state.on && state.code && (
{/* The SVG is main's own: `qrcode` rendering a link main built from
@@ -148,71 +147,3 @@ export function PhoneDialog({
/>
)
}
-
-/** What `parseAddress` in main would keep, for telling a refusal from a save. */
-function originOf(text: string): string | null {
- try {
- return new URL(text.trim()).origin
- } catch {
- return null
- }
-}
-
-/**
- * THE ADDRESS PHONES USE (#406; owner, 2026-10-16). Optional: empty means this
- * PC's own address, as before. Set, the QR and the link use it: a name the
- * user's network resolves, or HTTPS through the user's own reverse proxy.
- * Main validates and keeps only an http(s) origin; a refusal leaves the old
- * value and says so here.
- */
-function AddressField({
- address,
- start,
- onState
-}: {
- address: string | null
- start: string | null
- onState: (s: PhoneState) => void
-}): JSX.Element {
- const [text, setText] = useState(address ?? '')
- const [refused, setRefused] = useState(false)
- const save = async (): Promise => {
- const next = await window.prism.phoneAddressSet(text, start)
- setRefused(!!text.trim() && next.address !== originOf(text))
- onState(next)
- }
- return (
-
-
- {refused && (
-
- Not an address: use http:// or https:// and a name, with no path.
-
- )}
-
- )
-}
diff --git a/src/shared/channels.ts b/src/shared/channels.ts
index 1261ac5d..025c72f6 100644
--- a/src/shared/channels.ts
+++ b/src/shared/channels.ts
@@ -74,7 +74,6 @@ export const CALL = {
phoneGet: 'phone:get',
phoneSetOn: 'phone:set-on',
phoneCode: 'phone:code',
- phoneAddressSet: 'phone:address-set',
phoneCodeDrop: 'phone:code-drop',
phoneForget: 'phone:forget',
phonePins: 'phone:pins',
diff --git a/src/shared/types.ts b/src/shared/types.ts
index fdeb9f5a..8c5ccd7d 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -385,6 +385,4 @@ export interface PhoneState {
/** A pairing code for the folder in front (or This PC): link and QR (SVG), when the server is up. */
code?: { code: string; link: string; svg: string; expires: number }
error?: string
- /** The address phones use (#406), or null for this PC's own address. */
- address: string | null
}
diff --git a/tools/e2e/run.mjs b/tools/e2e/run.mjs
index 87d7f48f..1f6d870e 100644
--- a/tools/e2e/run.mjs
+++ b/tools/e2e/run.mjs
@@ -13732,17 +13732,6 @@ async function phoneScenario(fixtures) {
})
ok(!!shown.code && late.status !== 200, `a code is refused once the dialog closed (${late.status})`)
}
- // THE ADDRESS PHONES USE (#406): the QR and the link carry it; anything
- // that is not an http(s) origin is refused; empty goes back to this PC.
- {
- const set = await win.evaluate(() => window.prism.phoneAddressSet('https://prism.example.com/', null))
- ok(set.address === 'https://prism.example.com', `the address is kept as an origin (${set.address})`)
- ok(!!set.code?.link.startsWith('https://prism.example.com/?code='), `the link uses it (${set.code?.link})`)
- const bad = await win.evaluate(() => window.prism.phoneAddressSet('prism.example.com/path', null))
- ok(bad.address === 'https://prism.example.com', 'a bad address is refused and the old one stays')
- const cleared = await win.evaluate(() => window.prism.phoneAddressSet('', null))
- ok(cleared.address === null && !!cleared.code?.link.startsWith('http://127.0.0.1:'), `empty goes back to this PC (${cleared.code?.link})`)
- }
ok(!!state.code && state.code.svg.startsWith('