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
8 changes: 6 additions & 2 deletions AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 && <existing build command> && 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 `</body>` (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 `</body>` (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
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
Expand Down Expand Up @@ -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

Expand Down
31 changes: 21 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}.`);
Expand Down Expand Up @@ -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;
Expand All @@ -1273,7 +1282,9 @@ async function runStatus(args: ParsedArgs): Promise<number> {
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':
Expand Down
34 changes: 24 additions & 10 deletions src/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)'));
}
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion tests/guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Loading