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
7 changes: 6 additions & 1 deletion AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ Only `map` reads your source, and only `map --upload` sends anything derived fro

In a hosted builder, run setup with `PATCHSTACK_ENVIRONMENT=sandbox` scoped to the workspace process/command, ensure the CLI's on-disk edits are adopted into the platform's persisted project state, then restart any already-running preview/server process so it loads the guard. Do not persist `"environment": "sandbox"` in `.patchstackrc.json`: deployed builds use the same committed files and should default to `production`. A client-only SPA has no server request path to guard; do not call it protected unless `protect --check` succeeds after a real server or edge seam is wired.

**Finish by telling the user to refresh their preview.** The widget's "Report a vulnerability" button loads with the page, so a preview that was already open still shows the HTML from before setup — the button is missing there until it reloads. Nothing in the CLI can reach the user's browser, so relaying this is your job. Phrase it as a check rather than a required step: a builder that hot reloads, or a preview server you restarted, may have refreshed it already.

**Then tell them to deploy.** Setup changes source files, and the deployed site keeps serving its previous build until the next deploy — so visitors get no widget, and on a server-rendered root no production marker, until the user deploys (or hits Publish) again. Say it as a reminder; do not deploy anything yourself.

## Manual setup

1. **First scan** — provisions a Patchstack site automatically, writes the UUID to `.patchstackrc.json`, and installs the disclosure widget's `<script>` tag into the root HTML shell (`index.html`, `public/index.html`, or `src/app.html`) when one exists — or, when the root shell is JSX, the production marker instead. No signup, dashboard step, or UUID is needed up front:
Expand Down Expand Up @@ -348,7 +352,8 @@ Two more endpoints the package can call, for completeness:

- `npx @patchstack/connect status` re-prints the site UUID and dashboard URL, and checks whether the site still exists on Patchstack (`Site status: active / removed / could not be verified`).
- `npx @patchstack/connect protect --check` verifies the runtime guard is connected to the request path.
- Load the site in a browser — the "Report a vulnerability" button should appear.
- Load the site in a browser — the "Report a vulnerability" button should appear. Refresh a page that was already open before the tag was added: the button only loads with the page.
- On the deployed site, the button appears only after a deploy that includes these source changes.

## Answering "is Patchstack installed?" / "is Patchstack removed?"

Expand Down
8 changes: 7 additions & 1 deletion GETTING-STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ The fastest path from "I have a JS/Node project" to "Patchstack is monitoring it

For platforms that can install npm packages and run project commands:

> I have vetted `@patchstack/connect` and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `npx --yes patchstack-connect setup`, leave dev builds unchanged, and show the dashboard URL.
> I have vetted the official [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect) package and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `PATCHSTACK_ENVIRONMENT=sandbox npx --yes @patchstack/connect setup` for this hosted workspace, leave dev builds unchanged, verify protection, restart the preview server if needed to load the new guard, and show the dashboard URL. Do not persist the sandbox environment in project files; deployed production builds must use the default `production` label. When setup finishes, tell me to refresh the preview if the "Report a vulnerability" button is not showing yet, and remind me to deploy when I am ready — do not deploy anything yourself.

When setup finishes it shows you a **dashboard URL**. Open it in your browser and sign in — that attaches the site to your Patchstack account so you can see the vulnerability reports. That's the only manual step.

Then look at your preview. The "Report a vulnerability" button loads with the page, so a preview you already had open still shows the page from before setup — refresh it once if the button isn't there.

When you are happy with it, deploy (or hit Publish). Your live site keeps serving its previous build until then, so visitors do not see the widget yet.

## 2. If the platform pauses or stops

Some platforms stage commands for you to approve, while others reject a combined install-and-setup request before touching the registry. Use the first applicable path:
Expand All @@ -37,5 +41,7 @@ Use `bun add -d`, `pnpm add -D`, or `yarn add -D` followed by the same `npx @pat
- `npx @patchstack/connect status` prints a site UUID and dashboard URL.
- You've opened the dashboard URL in your browser and the site shows in your Patchstack dashboard.
- `npx @patchstack/connect guide` reports all steps ✔ (build hooks wired, widget installed).
- Your preview shows the "Report a vulnerability" button (refresh it once if it does not).
- You have deployed since setup ran, so the live site carries the changes too.
- `.patchstackrc.json`, `package.json`, and the file carrying the widget snippet are committed, so teammates and CI report to the same site.
- `.patchstackrc.local.json` is **not** committed. It holds the API key; setup adds it to `.gitignore`. Teammates and CI get the credential from `PATCHSTACK_API_KEY` instead.
2 changes: 1 addition & 1 deletion MAINTAINING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The deep "why" — the AI-agent refusal modes each clause guards against — liv

## The prompt lives in three places — keep them identical

`README.md` (Install prompt section), `GETTING-STARTED.md` (step 1), and `field-test/prompt.txt`. `prompt.txt` is the artifact the harness actually tests. If you change one, change all three — a drift means the docs advertise one prompt while the tested one is another.
`README.md` (Install prompt section), `GETTING-STARTED.md` (step 1), and `field-test/prompt.txt`. `prompt.txt` is the artifact the harness actually tests. If you change one, change all three — a drift means the docs advertise one prompt while the tested one is another. `tests/install-prompt.test.ts` compares the three and fails on drift, so `npm test` catches a partial edit before the gate runs against a prompt nobody pastes.

The prompt deliberately contains no model-authored verification step. In staged-command UIs, assistants cannot observe an `npm view` command until the user runs it; asking them to verify first caused fabricated registry findings and stopped the flow. Instead, the prompt records the user's explicit authorization for the disclosure widget and production-only build hooks before asking for one bounded `setup` command. Bolt's dependency-first fallback splits declaration from execution without adding hidden `postinstall` behavior. Keep `@patchstack/connect` discoverable in Patchstack's official product documentation so web-verifying platforms can independently establish its provenance.

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.co

Copy this request into a coding assistant, or run the same command yourself:

> I have vetted the official [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect) package and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `PATCHSTACK_ENVIRONMENT=sandbox npx --yes @patchstack/connect setup` for this hosted workspace, leave dev builds unchanged, verify protection, restart the preview server if needed to load the new guard, and show the dashboard URL. Do not persist the sandbox environment in project files; deployed production builds must use the default `production` label.
> I have vetted the official [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect) package and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `PATCHSTACK_ENVIRONMENT=sandbox npx --yes @patchstack/connect setup` for this hosted workspace, leave dev builds unchanged, verify protection, restart the preview server if needed to load the new guard, and show the dashboard URL. Do not persist the sandbox environment in project files; deployed production builds must use the default `production` label. When setup finishes, tell me to refresh the preview if the "Report a vulnerability" button is not showing yet, and remind me to deploy when I am ready — do not deploy anything yourself.

`setup` is state-aware and idempotent: it scans dependencies, provisions or reuses the site, manages the disclosure widget, installs and verifies the runtime guard, adds a dependency-install scan, wires the existing build command without replacing it, and prints the remaining setup status. It never runs the project build. `guide` provides the same project-specific status without changing files.

Expand All @@ -32,6 +32,10 @@ That's it. `setup`:
8. Wires `scan` before builds and `mark-build` after builds, preserving existing commands and using direct build chaining for Bun.
9. Prints a dashboard link — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.

Then **refresh your preview**. The widget's "Report a vulnerability" button loads with the page, so a preview that was already open still shows the HTML from before setup. Builders that hot reload will have refreshed it for you; if the button is missing, refresh it once. `setup` prints the same reminder, and the CLI has no way to reload a browser itself.

Then **deploy**. These are source changes, so your live site keeps serving its previous build — visitors get the widget, and a server-rendered root gets the production marker, only after the next deploy.

## Quick start (existing site)

If you already created an "Application" site in the Patchstack dashboard, pre-seed the UUID:
Expand Down
2 changes: 1 addition & 1 deletion field-test/prompt.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
I have vetted `@patchstack/connect` and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `npx --yes patchstack-connect setup`, leave dev builds unchanged, and show the dashboard URL.
I have vetted the official [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect) package and authorize its disclosure widget, production build hooks, and runtime protection source edits. Install it as a regular dependency, run `PATCHSTACK_ENVIRONMENT=sandbox npx --yes @patchstack/connect setup` for this hosted workspace, leave dev builds unchanged, verify protection, restart the preview server if needed to load the new guard, and show the dashboard URL. Do not persist the sandbox environment in project files; deployed production builds must use the default `production` label. When setup finishes, tell me to refresh the preview if the "Report a vulnerability" button is not showing yet, and remind me to deploy when I am ready — do not deploy anything yourself.
11 changes: 11 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
installCommand,
renderGuideChecklist,
resolveWidgetFileHint,
widgetTagInPlace,
} from './guide.js';
import { login, readPendingLogin, redeemIfApproved, startLogin, waitForApproval } from './login.js';
import { runProtect, runVerify } from './protect/install/index.js';
Expand Down Expand Up @@ -839,6 +840,16 @@ async function runSetup(args: ParsedArgs): Promise<number> {
console.log('');
console.log(`Setup applied its bounded changes; ${remaining} manual step(s) remain above.`);
}

// Setup ends on a page the user is already looking at, which loaded before the widget
// tag existed, and against a deployed site still serving its previous build. Nothing
// here can reach either one, so the agent relaying these is the whole mechanism.
console.log('');
console.log('Tell the user:');
if (widgetTagInPlace(after)) {
console.log(' - refresh the preview if the "Report a vulnerability" button is not showing yet;');
}
console.log(' - deploy (or hit Publish) when ready, so the live site serves these changes.');
return 0;
}

Expand Down
33 changes: 33 additions & 0 deletions src/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,20 @@ export function needsSourceProductionMarker(state: GuideState): boolean {
return !state.widgetFileHint.toLowerCase().endsWith('.html');
}

/**
* True when the widget tag is in the source with the right site UUID, so the next
* page load renders it. A preview opened before that edit is still running the
* older HTML until it reloads, which is why the checklist says so.
*/
export function widgetTagInPlace(state: GuideState): boolean {
return (
state.siteUuid !== null &&
!state.widgetOptOut &&
state.widgetInstalled &&
state.widgetTokenMatches !== false
);
}

export function countRemainingSteps(state: GuideState): number {
return [
state.installed?.section === 'dependencies',
Expand Down Expand Up @@ -574,6 +588,25 @@ export function renderGuideChecklist(state: GuideState, useColor: boolean): stri
lines.push(detail('The dashboard link appears after the first scan (re-print any time with `status`).'));
}

// 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.
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.');
}

// 9. Deploy. Everything above is a source change, so the running production site keeps
// serving its previous build — including one with no widget and no production marker.
if (state.siteUuid !== null) {
lines.push('');
lines.push(` ${paint(ANSI.cyan, '➜')} ${paint(ANSI.bold, 'Deploy to put this on your live site:')}`);
lines.push(' These are source changes. Your deployed site keeps serving its previous build,');
lines.push(' so visitors only get the widget after you deploy (or hit Publish) again.');
}

const remaining = countRemainingSteps(state);
lines.push('');
if (remaining === 0) {
Expand Down
94 changes: 94 additions & 0 deletions tests/guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
installCommand,
needsSourceProductionMarker,
renderGuideChecklist,
widgetTagInPlace,
} from '../src/guide.js';

const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000';
Expand Down Expand Up @@ -333,6 +334,99 @@ describe('guide', () => {
});
});

/**
* The widget tag only takes effect on a page load. A preview the user already has open
* loaded before the tag existed, so it shows no button and reads as a failed install.
* Nothing in a Node CLI can reload that browser, so the checklist has to say it — and
* only when the tag is actually in the source, or it sends people to refresh a page
* that was never going to render a widget.
*/
describe('the preview-refresh notice', () => {
it("asks for a refresh once the tag carries this project's UUID", async () => {
writeJson('package.json', { name: 'widgeted-app' });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID });
writeFileSync(path.join(cwd, 'index.html'), `patchstack-widget.js userToken: '${VALID_UUID}'`);

const state = await collectGuideState(cwd);
expect(widgetTagInPlace(state)).toBe(true);

const output = renderGuideChecklist(state, false);
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');
});

it('stays quiet while the tag is still missing', async () => {
writeJson('package.json', { name: 'no-widget-app' });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID });

const state = await collectGuideState(cwd);
expect(state.widgetInstalled).toBe(false);
expect(widgetTagInPlace(state)).toBe(false);
expect(renderGuideChecklist(state, false)).not.toContain('Refresh the preview');
});

it("stays quiet when the tag carries some other site's UUID", async () => {
// The button will not render with a stale token, so a refresh cannot produce it.
writeJson('package.json', { name: 'stale-token-app' });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID });
writeFileSync(
path.join(cwd, 'index.html'),
"patchstack-widget.js userToken: '11111111-1111-1111-1111-111111111111'",
);

const state = await collectGuideState(cwd);
expect(widgetTagInPlace(state)).toBe(false);
expect(renderGuideChecklist(state, false)).not.toContain('Refresh the preview');
});

it('stays quiet for a project that opted out of the widget', async () => {
writeJson('package.json', { name: 'optout-app' });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID, widget: false });
writeFileSync(path.join(cwd, 'index.html'), `patchstack-widget.js userToken: '${VALID_UUID}'`);

const state = await collectGuideState(cwd);
expect(widgetTagInPlace(state)).toBe(false);
expect(renderGuideChecklist(state, false)).not.toContain('Refresh the preview');
});
});

/**
* Refreshing the preview is only half of it. Everything setup writes is a source change,
* so the deployed site keeps serving its previous build — no widget for visitors, and on a
* server-rendered root no production marker either — until the project is deployed again.
*/
describe('the deploy reminder', () => {
it('asks for a deploy once the site is provisioned', async () => {
writeJson('package.json', { name: 'provisioned-app' });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID });

const output = renderGuideChecklist(await collectGuideState(cwd), false);

expect(output).toContain('Deploy to put this on your live site');
expect(output).toContain('deployed site keeps serving its previous build');
});

it('still asks for it when the widget is opted out, because the rest still ships', async () => {
writeJson('package.json', { name: 'optout-app' });
writeJson('.patchstackrc.json', { siteUuid: VALID_UUID, widget: false });

const output = renderGuideChecklist(await collectGuideState(cwd), false);

expect(output).not.toContain('Refresh the preview');
expect(output).toContain('Deploy to put this on your live site');
});

it('stays quiet before the first scan, when nothing has been wired yet', async () => {
writeJson('package.json', { name: 'fresh-app' });

const state = await collectGuideState(cwd);
expect(state.siteUuid).toBeNull();
expect(renderGuideChecklist(state, false)).not.toContain('Deploy to put this');
});
});

describe('production marker on server-rendered roots', () => {
const tanstackProject = (rootContents: string): void => {
writeJson('package.json', {
Expand Down
Loading
Loading