From a7d63d08bede803e224ff4218dc15e1198c7faea Mon Sep 17 00:00:00 2001 From: kipavy Date: Tue, 18 Aug 2026 22:55:28 +0000 Subject: [PATCH] feat(landing): bridge the invite, plugin-install and snippet-install routes --- landing/app/open/fragment.test.ts | 35 ++++++++++++++++++++++++++++++- landing/app/open/fragment.ts | 32 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/landing/app/open/fragment.test.ts b/landing/app/open/fragment.test.ts index 0c00f04..466c106 100644 --- a/landing/app/open/fragment.test.ts +++ b/landing/app/open/fragment.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, test } from "vitest"; import { readFragment } from "./fragment"; const UUID = "3f2504e0-4f89-11d3-9a0c-0305e82c3301"; @@ -119,3 +119,36 @@ describe("the navigate routes", () => { expect(readFragment("#billing")).toEqual({ url: "voltius://billing", code: null }); }); }); + +test("an invite fragment forwards a well-formed handle and rejects anything else", () => { + expect(readFragment("#invite?h=%40kevin-p")?.url).toBe("voltius://invite?h=%40kevin-p"); + expect(readFragment("#invite?h=kevin-p")?.url).toBe("voltius://invite?h=kevin-p"); + expect(readFragment("#invite?h=ab")).toBeNull(); + expect(readFragment("#invite?h=-kevin")).toBeNull(); + expect(readFragment("#invite")).toBeNull(); +}); + +test("a plugin-install fragment forwards a valid id and rejects a path-escaping one", () => { + expect(readFragment("#plugin-install?id=docker&src=voltius")?.url).toBe( + "voltius://plugin-install?id=docker&src=voltius", + ); + expect(readFragment("#plugin-install?id=docker")?.url).toBe("voltius://plugin-install?id=docker"); + expect(readFragment("#plugin-install?id=../evil")).toBeNull(); + expect(readFragment("#plugin-install?id=__meta__")).toBeNull(); + expect(readFragment("#plugin-install?id=Docker")).toBeNull(); + expect(readFragment("#plugin-install")).toBeNull(); +}); + +test("a snippet-install fragment forwards a non-empty id and rejects an over-long one", () => { + expect(readFragment("#snippet-install?id=docker-cleanup")?.url).toBe( + "voltius://snippet-install?id=docker-cleanup", + ); + expect(readFragment("#snippet-install?id=")).toBeNull(); + expect(readFragment("#snippet-install?id=" + "a".repeat(101))).toBeNull(); +}); + +test("no new route produces a paste-able code", () => { + expect(readFragment("#invite?h=kevin-p")?.code).toBeNull(); + expect(readFragment("#plugin-install?id=docker")?.code).toBeNull(); + expect(readFragment("#snippet-install?id=docker-cleanup")?.code).toBeNull(); +}); diff --git a/landing/app/open/fragment.ts b/landing/app/open/fragment.ts index 0ac9352..e6ddb21 100644 --- a/landing/app/open/fragment.ts +++ b/landing/app/open/fragment.ts @@ -22,6 +22,20 @@ const SETTINGS_SECTIONS = new Set([ // Mirrors `MAX_ENTRY_ID` in voltius (src/services/deepLinkUrl.ts). const MAX_ENTRY_ID = 200; +// Mirrors `HANDLE_RE` in voltius (src/services/deepLinkUrl.ts), itself the +// server's `validate_custom_handle` rule. +const HANDLE_RE = /^[a-z0-9][a-z0-9_-]{1,28}[a-z0-9]$/; + +// Mirrors `isValidPluginId` in voltius (src/plugins/pluginId.ts). The leading +// alphanumeric is what keeps `__meta__` and `..` unclaimable, so it is not +// cosmetic — a plugin id becomes a directory name on the client. +const PLUGIN_ID_RE = /^[a-z0-9][a-z0-9._-]*$/; +const PLUGIN_ID_MAX_LENGTH = 64; + +// Mirrors `MAX_SOURCE_ID` and `MAX_CATALOG_ID` in voltius (src/services/deepLinkUrl.ts). +const MAX_SOURCE_ID = 100; +const MAX_CATALOG_ID = 100; + const ROUTE_VALIDATORS: Record boolean> = { join: (params) => isSessionId(params.get("s") ?? "") && !!params.get("t"), verified: (params) => isSessionId(params.get("u") ?? ""), @@ -30,6 +44,24 @@ const ROUTE_VALIDATORS: Record boolean> = { notification: (params) => (params.get("n") ?? "").length <= MAX_ENTRY_ID, settings: (params) => SETTINGS_SECTIONS.has(params.get("section") ?? ""), billing: () => true, + invite: (params) => HANDLE_RE.test((params.get("h") ?? "").replace(/^@/, "").toLowerCase()), + // The source is a catalogue *id* the client resolves against its own configured + // sources, never a URL — so only its length is checked here. + "plugin-install": (params) => { + const id = params.get("id") ?? ""; + return ( + id.length > 0 && + id.length <= PLUGIN_ID_MAX_LENGTH && + PLUGIN_ID_RE.test(id) && + (params.get("src") ?? "").length <= MAX_SOURCE_ID + ); + }, + // The catalogue id is opaque — the client fetches the catalogue at confirm time — + // so only its presence and length are checked. + "snippet-install": (params) => { + const id = params.get("id") ?? ""; + return id.length > 0 && id.length <= MAX_CATALOG_ID; + }, }; export type Target = {