From 46f3eccb4ddfd1ddcab679badd5b2fcba71d39f0 Mon Sep 17 00:00:00 2001 From: kipavy Date: Tue, 18 Aug 2026 19:50:07 +0000 Subject: [PATCH] feat(landing): let /open bridge the notification, settings and billing routes Mirrors the three navigate routes added to the client's ROUTES table. The settings section list and the notification id cap are copies of the client's own, so a link the app cannot render fails on the page instead of hopping to a scheme URL that gets dropped in silence. --- landing/app/open/fragment.test.ts | 38 ++++++++++++++++++++++++++++++- landing/app/open/fragment.ts | 15 ++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/landing/app/open/fragment.test.ts b/landing/app/open/fragment.test.ts index 8a9f754..0c00f04 100644 --- a/landing/app/open/fragment.test.ts +++ b/landing/app/open/fragment.test.ts @@ -32,10 +32,10 @@ describe("readFragment", () => { // Every route the app understands must also be listed here before a link to // it can work; an unlisted one has to fail closed rather than hop. it.each([ - "#settings?section=mcp", "#plugin/install?id=evil", "#snippet/install?id=evil", "#notification/1", + "#pluginInstall?id=evil", "#connect?host=evil.example", "#JOIN?s=" + UUID + "&t=tok3n", "#join/../verified?u=" + UUID, @@ -83,3 +83,39 @@ describe("readFragment", () => { }); }); }); + +describe("the navigate routes", () => { + it("builds a notification target with and without an entry id", () => { + expect(readFragment("#notification?n=invite%3A42")).toEqual({ + url: "voltius://notification?n=invite%3A42", + code: null, + }); + expect(readFragment("#notification")).toEqual({ + url: "voltius://notification", + code: null, + }); + }); + + it("rejects an entry id past the length the app accepts", () => { + expect(readFragment(`#notification?n=${"a".repeat(201)}`)).toBeNull(); + expect(readFragment(`#notification?n=${"a".repeat(200)}`)).not.toBeNull(); + }); + + it("passes a settings section the app can render", () => { + expect(readFragment("#settings?section=integrations")).toEqual({ + url: "voltius://settings?section=integrations", + code: null, + }); + }); + + it.each(["#settings?section=mcp", "#settings?section=__proto__", "#settings"])( + "rejects %s", + (hash) => { + expect(readFragment(hash)).toBeNull(); + }, + ); + + it("builds a billing target with no query string", () => { + expect(readFragment("#billing")).toEqual({ url: "voltius://billing", code: null }); + }); +}); diff --git a/landing/app/open/fragment.ts b/landing/app/open/fragment.ts index 12a26ab..0ac9352 100644 --- a/landing/app/open/fragment.ts +++ b/landing/app/open/fragment.ts @@ -12,9 +12,24 @@ const isSessionId = (value: string): boolean => UUID_RE.test(value); // reject a malformed one: an unparseable fragment must skip the hop entirely // and render the invalid state, not bounce the user out to a scheme URL the // client silently drops. +// Mirrors `SETTINGS_SECTIONS` in voltius (src/stores/uiStore.ts). A section the +// app cannot render must fail here rather than open an empty modal. +const SETTINGS_SECTIONS = new Set([ + "appearance", "account", "sync", "vaults", "plugins", "integrations", "terminal", + "sftp", "portForwarding", "hosts", "shortcuts", "diagnostics", "about", +]); + +// Mirrors `MAX_ENTRY_ID` in voltius (src/services/deepLinkUrl.ts). +const MAX_ENTRY_ID = 200; + const ROUTE_VALIDATORS: Record boolean> = { join: (params) => isSessionId(params.get("s") ?? "") && !!params.get("t"), verified: (params) => isSessionId(params.get("u") ?? ""), + // The inbox id is opaque — entries are re-derived from server state — so only + // its length is checked; an id the app no longer holds just opens the centre. + notification: (params) => (params.get("n") ?? "").length <= MAX_ENTRY_ID, + settings: (params) => SETTINGS_SECTIONS.has(params.get("section") ?? ""), + billing: () => true, }; export type Target = {