From 6594eab72eb130f169ad0fcf2d058f21aab70357 Mon Sep 17 00:00:00 2001 From: Mario Tarosso Date: Mon, 21 Sep 2026 14:12:35 +0100 Subject: [PATCH] Offer all three ways to connect a site, and correct what the widget shows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI named two routes to attaching a site to an account — the dashboard link and `claim` — and left out the shortest one: the widget's own "Connect this website" panel, already rendered on the preview the person is looking at. All three now appear, with the panel first. The same output also promised a visible "Report a vulnerability" button straight after install. The widget serves the connect panel instead of that button until the site is claimed, so on a fresh install that promise is always wrong, and the person refreshes the preview looking for a control that is not there yet. Co-Authored-By: Claude Opus 5 (1M context) --- AGENT-INSTALL.md | 8 ++++++-- src/cli.ts | 31 +++++++++++++++++++++---------- src/guide.ts | 34 ++++++++++++++++++++++++---------- tests/guide.test.ts | 5 ++++- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/AGENT-INSTALL.md b/AGENT-INSTALL.md index ab08ed3..d61db0b 100644 --- a/AGENT-INSTALL.md +++ b/AGENT-INSTALL.md @@ -200,7 +200,7 @@ Handle it in this order: **Bun-managed projects:** `bun run` does not execute npm-style `pre`/`post` scripts, so wire the build script directly instead: `"build": "patchstack-connect scan && && patchstack-connect mark-build"`. -3. **Verify the disclosure widget** — a floating "Report a vulnerability" button. `scan` installs it automatically into a plain HTML shell **or a JSX root** (Next, Remix, React Router, TanStack Start, Gatsby), and `mark-build` carries it into built HTML. Only when `scan` reported that it found no editable shell at all — a root whose head mechanism is not a plain script tag, e.g. Nuxt's `useHead` or an Astro layout — add the one-liner it printed to the root layout yourself, just before `` (never a JS entry point), reading `siteUuid` from `.patchstackrc.json`. On those same roots the widget also needs the production marker above the tag — `scan` adds it automatically to a JSX root, and prints it to paste when it finds no anchor. A server-rendered site without the marker serves the build-mode claim flow to its visitors: +3. **Verify the disclosure widget** — a floating control whose form follows the site's claim state: while the site is unclaimed it is a one-time "Connect this website" panel, and it becomes the public "Report a vulnerability" button once the site is claimed. Do not tell the user the report button will appear on a site that has not been connected to an account yet. `scan` installs it automatically into a plain HTML shell **or a JSX root** (Next, Remix, React Router, TanStack Start, Gatsby), and `mark-build` carries it into built HTML. Only when `scan` reported that it found no editable shell at all — a root whose head mechanism is not a plain script tag, e.g. Nuxt's `useHead` or an Astro layout — add the one-liner it printed to the root layout yourself, just before `` (never a JS entry point), reading `siteUuid` from `.patchstackrc.json`. On those same roots the widget also needs the production marker above the tag — `scan` adds it automatically to a JSX root, and prints it to paste when it finds no anchor. A server-rendered site without the marker serves the build-mode claim flow to its visitors: ```html @@ -268,7 +268,11 @@ It is server-only. Never put it in the widget tag, client bundles, or public env **Do not commit `.patchstackrc.local.json`.** That file holds the API key issued at provision; the scan writes it and adds it to `.gitignore`, and tells you if it could not. `.patchstackrc.json` holds only the site UUID and settings, and the UUID is public by design — it ships in the widget tag in served HTML. -6. **Open the dashboard link** from the scan in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after connecting it to an account. The same connection flow is available from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending `#patchstack` to the live URL. +6. **Connect the site to a Patchstack account.** The site is monitored either way, but its vulnerability reports are only visible once it is attached to an account, and an unattached site stays claimable by anyone who loads the page — the site UUID ships in the HTML and claiming is first-come. Three routes reach the same place; tell the user all three and lead with the first, which needs no terminal and no copied URL: + + 1. **The widget's "Connect this website" panel**, already on the preview. While the site is unclaimed the widget serves this panel *instead of* the report button, and signing in there attaches the site. On a published build it is hidden from visitors; the owner reveals it by appending `#patchstack` (or `?patchstack`) to the live URL. + 2. **The dashboard link** the scan printed — open it in a browser and sign in. + 3. **`npx @patchstack/connect claim`** from the terminal, which prints a link to sign in with and then attaches the site. ## Rules diff --git a/src/cli.ts b/src/cli.ts index f38ab51..e5ebca8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -805,16 +805,24 @@ async function runScan( const linkUuid = response.uuid ?? config.siteUuid; if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== undefined && linkUuid.length > 0) { console.log(''); - console.log('Open this dashboard link to view vulnerability reports:'); - console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`); + console.log('Connect this site to your Patchstack account — any one of these:'); + // The widget's own panel is the shortest route and is already rendered on the preview, so it + // leads. The link and `claim` follow for a project with no preview open: in a terminal the + // link is output nobody is looking at, which is why the command is named too. + if (config.widget) { + console.log(' 1. In the preview — the widget shows a "Connect this website" panel while the'); + console.log(' site is unclaimed. Signing in there attaches it.'); + console.log(' 2. Open this dashboard link in a browser:'); + console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`); + console.log(' 3. From this terminal: npx @patchstack/connect claim'); + } else { + console.log(' 1. Open this dashboard link in a browser:'); + console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`); + console.log(' 2. From this terminal: npx @patchstack/connect claim'); + } if (config.endpoint !== DEFAULT_ENDPOINT) { console.log(' (this URL inherits the endpoint override above)'); } - // A site provisioned by this scan has no owner yet. In a terminal the link above is output - // nobody is looking at, so name the command that does the same thing from here. - console.log(''); - console.log('Or attach it to your account from this terminal:'); - console.log(' npx @patchstack/connect claim'); } // A scan can't wire the build hooks itself — an agent that runs `scan` but not @@ -855,7 +863,8 @@ function reportSourceWidget(siteUuid: string, framework: string | null): void { const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell); switch (result.action) { case 'added': - console.log(`Widget: added the "Report a vulnerability" tag to ${result.shell}. Reload your preview to see it.`); + console.log(`Widget: added the disclosure widget tag to ${result.shell}. Reload your preview to see it.`); + console.log(' Unclaimed, it shows a "Connect this website" panel; the "Report a vulnerability" button replaces it once the site is claimed.'); break; case 'updated': console.log(`Widget: updated the managed tag in ${result.shell} to site ${siteUuid}.`); @@ -1253,7 +1262,7 @@ function setupOutcome( if (protection.install.status === 'not-applicable' && protection.install.leftovers.length > 0) { warnings.push(`earlier guard scaffold does nothing here and can be deleted: ${protection.install.leftovers.join(', ')}`); } - if (state.claimUrl !== null) warnings.push('the site is not attached to an account until the dashboard link is opened or `npx @patchstack/connect claim` completes'); + if (state.claimUrl !== null) warnings.push('the site is not attached to an account until someone signs in through the widget\'s "Connect this website" panel, the dashboard link is opened, or `npx @patchstack/connect claim` completes'); if (warnings.length > 0) lines.push(['Warnings', warnings.join('; ')]); return lines; @@ -1273,7 +1282,9 @@ async function runStatus(args: ParsedArgs): Promise { console.log(`Environment: ${config.environment}`); if (config.siteUuid !== null) { console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`); - console.log(' Not attached to an account yet? Run `npx @patchstack/connect claim`.'); + console.log(' Not attached to an account yet? Sign in through the widget\'s "Connect this'); + console.log(' website" panel on the preview, open the link above, or run'); + console.log(' `npx @patchstack/connect claim`.'); switch (await fetchSiteStatus(config)) { case 'active': diff --git a/src/guide.ts b/src/guide.ts index 3dc170c..158e5fa 100644 --- a/src/guide.ts +++ b/src/guide.ts @@ -611,14 +611,24 @@ export function renderGuideChecklist(state: GuideState, useColor: boolean): stri lines.push(detail('Prove a request reaches the guard (starts your app) → npx @patchstack/connect protect --check --runtime')); } - // 7. Dashboard access — always keep the URL prominent. + // 7. Attaching the site to an account. Three routes reach the same place, and the widget's own + // panel leads because it is already on the page the person is looking at. The link and `claim` + // are for a project with no preview open, or none carrying the widget. lines.push(''); if (state.claimUrl !== null) { - lines.push(` ${paint(ANSI.cyan, '➜')} ${paint(ANSI.bold, 'Dashboard link (open to view reports):')}`); - lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`); - lines.push(detail('Open this link in a browser. The CLI never opens it.')); - lines.push(detail('Or from this terminal → npx @patchstack/connect claim')); - lines.push(detail(' (prints a link to sign in with, then attaches the site to that account)')); + lines.push(` ${paint(ANSI.cyan, '➜')} ${paint(ANSI.bold, 'Connect this site to your Patchstack account:')}`); + let route = 1; + if (widgetTagInPlace(state)) { + lines.push(` ${route++}. In the preview — while the site is unclaimed the widget shows a`); + lines.push(' "Connect this website" panel. Signing in there attaches the site.'); + } + lines.push(` ${route++}. In a browser — open the dashboard link (the CLI never opens it):`); + lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`); + lines.push(` ${route}. From this terminal → npx @patchstack/connect claim`); + lines.push(' (prints a link to sign in with, then attaches the site to that account)'); + lines.push(detail('Reports have no owner to reach until one of these completes. The site UUID ships')); + lines.push(detail('in the page and claiming is first-come, so an unclaimed site stays claimable by')); + lines.push(detail('anyone who loads it.')); if (state.endpointOverride !== null) { lines.push(detail('(this URL inherits the endpoint override above)')); } @@ -627,13 +637,17 @@ export function renderGuideChecklist(state: GuideState, useColor: boolean): stri } // 8. Preview refresh. The tag is in the source, but a page that was already open - // loaded before it existed and renders no button until it reloads. + // loaded before it existed and renders nothing until it reloads. Which control appears + // then depends on claim state: the widget serves the connect panel until the site has an + // owner, and the report button only after. if (widgetTagInPlace(state)) { lines.push(''); lines.push(` ${paint(ANSI.cyan, '➜')} ${paint(ANSI.bold, 'Refresh the preview to see the widget:')}`); - lines.push(' The "Report a vulnerability" button loads with the page, so a preview that was'); - lines.push(' already open still shows the HTML from before this change. Builders that hot'); - lines.push(' reload refresh it themselves; if the button is missing, refresh the preview once.'); + lines.push(' The widget loads with the page, so a preview that was already open still shows'); + lines.push(' the HTML from before this change. Builders that hot reload refresh it themselves;'); + lines.push(' if nothing appears, refresh the preview once.'); + lines.push(' Unclaimed, the widget shows the "Connect this website" panel; the public'); + lines.push(' "Report a vulnerability" button takes its place once the site is claimed.'); } // 9. Deploy. Everything above is a source change, so the running production site keeps diff --git a/tests/guide.test.ts b/tests/guide.test.ts index 53da3d2..76c1018 100644 --- a/tests/guide.test.ts +++ b/tests/guide.test.ts @@ -411,7 +411,10 @@ describe('guide', () => { expect(output).toContain('Refresh the preview to see the widget'); // Not an unconditional "refresh now": a builder that hot reloads has already done it, // and telling someone to refresh a page that just refreshed itself reads as a fault. - expect(output).toContain('if the button is missing, refresh the preview once'); + expect(output).toContain('if nothing appears, refresh the preview once'); + // An unclaimed site gets the connect panel, not the report button, so the checklist + // must not promise the button before there is an owner. + expect(output).toContain('"Connect this website" panel'); }); it('stays quiet while the tag is still missing', async () => {