Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion landing/app/open/fragment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
});
});
15 changes: 15 additions & 0 deletions landing/app/open/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (params: URLSearchParams) => 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 = {
Expand Down
Loading