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
5 changes: 4 additions & 1 deletion AGENT-INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ The pages get a build step, even though nothing is compiled. The build is what r
2. Install `@patchstack/connect` as a runtime dependency with the project's package manager. If it is missing, `setup` adds it itself.
3. Run `npx @patchstack/connect setup`. In a hosted builder, scope `PATCHSTACK_ENVIRONMENT=sandbox` to that command — see "Automated setup".
4. Finish any `✘` line under `To do` in the report at the end of `setup`. "Automated setup" names each one. Do not start, restart or stop the person's app, and do not touch its data, to check the install: `setup` never runs the app, and the protection check needs an app the person already has running.
5. End by passing on the `──── Summary ────` that `setup` prints: what Patchstack found, the link, what is left to do and the next step, with a reminder to deploy when they are ready. See "The message you end on".
5. Run `npx @patchstack/connect doctor`. Fix every `✗` it reports, as its `Fix:` line says, and run it again until none is left. A `✗` marked "for the app owner" (connecting their Patchstack account, publishing the app) is not yours to fix: the summary already hands it to the person.
6. End by passing on the `──── Summary ────` that `setup` prints: what Patchstack found, the link, what is left to do and the next step, with a reminder to deploy when they are ready. See "The message you end on".

**In a workspace repo** — a root `package.json` that declares `workspaces`, with the app in a package such as `apps/web` (Hostinger Horizons generates this shape) — run every command from the repo root. The site is recorded in the root's `.patchstackrc.json`, and `setup` wires the guard and the widget into the app package from there. Do not run `setup` inside the app package, and do not create a `.patchstackrc.json` there. When `setup`, `scan`, `widget` or `protect` is run inside a package of a repo whose root records the site, it runs at the root instead and prints `Running at the project root (…)`.

Expand All @@ -85,6 +86,7 @@ Every command at a glance — what it does, whether it reads your source, what i
| `guide` | Print this project's setup status: what is left to do and the four steps, with tailored commands. `--full` adds this reference. | Reads local protection wiring for verification | Nothing | Nothing |
| `status` | Re-print the site UUID + dashboard URL and check whether the site still exists (active / removed / could not verify) and whether it is connected to an account. | No | The connected note in `.patchstackrc.json`, when it changed | Site-existence and connection checks |
| `next` | Say where the project stands and the one thing to do now: connect the account (with the link), or go live (with the same steps `go-live` prints, or asking where the app is hosted when the project does not say). Answers the "done" the summary ends on. | Reads the project files `guide` and `go-live` read | The connected note in `.patchstackrc.json`, when it changed | The site UUID, to the public claim page that says whether it is connected (skipped once `.patchstackrc.json` records that it is) |
| `doctor [--url <live app>]` | Check that everything Patchstack needs works, and print each problem with its fix: the install, the site, the account, the guard's wiring, its rules file, the read-only rules credential, the map link, the build steps, and whether the published app fetches its rules. A problem only the app's owner can fix (connecting the account, publishing) is marked as theirs. Exits 1 when it finds a problem. | Reads the project files `guide` and `protect --check` read | Nothing | The site UUID, to the public claim page that says whether it is connected (skipped once `.patchstackrc.json` records that it is); the read-only rules credential from the guard's rules file, and the site key when this machine holds one, each once to `monitor/pulse/token`, to check Patchstack accepts them; one GET with that token to `monitor/pulse/sync-status/<your site uuid>`, which answers only when the published app (read-only credential) and a key-holding copy last fetched rules, and how many (doctor never fetches rules itself, which would be recorded as a fetch); with `--url`, two GET requests to that address: its page, and the same page with a test attack in a `file` parameter, which another firewall in front of the app may also flag |
| `vulns` | Show what Patchstack found in this project's packages: vulnerabilities by severity and outdated packages, as counts, with the link that shows the details. Package names are shown in the dashboard once the site is connected. | No | Nothing | The site UUID, to the public claim page that returns the counts |
| `go-live [--host <name>]` | Show how to add the project's API key to its host and deploy, in three steps. The host is read from project files (`vercel.json`, `netlify.toml`, `wrangler.toml`, `fly.toml`, `render.yaml`, `railway.json`, `amplify.yml`, `Procfile`, `.replit`, a Lovable project's packages) or named with `--host`. The commands it prints read the key from `.patchstackrc.local.json` as they run; it never prints the key. | Reads those project files only | Nothing | Nothing |
| `widget [--remove]` | Add the Patchstack Connector widget tag to the root page (in a workspace repo, the app package's root page), or take the tag Connect added out with `--remove` (a tag added by hand is left alone). Saves the choice as `"widget"` in `.patchstackrc.json`, which `scan` and `mark-build` follow; `setup --widget` saves the same choice. | Reads the root page to find where the tag goes | `.patchstackrc.json`; the widget `<script>` tag in the root HTML shell or framework root | Nothing |
Expand Down Expand Up @@ -956,6 +958,7 @@ Three more endpoints the package can call, for completeness:
## Verifying the install

- `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 doctor` runs every check at once and prints each problem with its fix; `--url <live app>` adds the live app's widget and a test attack.
- `npx @patchstack/connect protect --check` verifies from the source that the runtime guard is connected to the request path. It does not run the app. When the project records a site, it also fails if a guard Connect wrote does not carry it (`the guard carries this project's Patchstack site`): such a guard blocks with the rules it shipped with but fetches no rule updates and reports nothing. Run `npx @patchstack/connect protect` from the project root to fill the site in, then deploy.
- `npx @patchstack/connect protect --check --runtime` additionally **starts the app** on a loopback port and sends it one request, to establish that a request reaches the guard seam. Opt-in, and the only command that runs the application; exit `0`/`1`/`2` as described in step 4.
- Load the site in a browser — the widget should appear, as the "Connect this website" panel while the site is unclaimed. Refresh a page that was already open before the tag was added: the widget only loads with the page.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ patchstack-connect setup [--widget] [options] Run scan, add the widget with
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json with
an existing site UUID
patchstack-connect status [options] Show current configuration
patchstack-connect doctor [--url <live app>] Check that everything Patchstack needs works and
say how to fix each problem
patchstack-connect vulns Show vulnerabilities by severity and outdated
packages, with the link to the details
patchstack-connect go-live [--host <name>] Show how to add your API key to your host
Expand Down
31 changes: 29 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import { formatRuntimeCheck, runRuntimeCheck, runtimeExitCode } from './protect/
import { runMap } from './map-command.js';
import { bundleRulesAllowed } from './bundled-rules.js';
import { getStringFlag } from './flags.js';
import { renderDoctor, runDoctorChecks } from './doctor.js';
import { isCanonicalUuid } from './endpoint-policy.js';
import { setupProtection, wireBuildScripts, wireDevelopmentScript } from './setup.js';
import { runDevelopment } from './dev-command.js';
Expand All @@ -137,6 +138,7 @@ function startScreen(color: boolean): string[] {
'',
'Then:',
command('next', 'what to do next'),
command('doctor', 'check that everything works'),
command('vulns', 'see what was found'),
command('widget', 'show Patchstack inside your app (optional)'),
command('go-live', 'how to go live on your host'),
Expand Down Expand Up @@ -192,6 +194,17 @@ Usage:
patchstack-connect summary Print the summary the last setup wrote, from
.patchstack/summary.md. Sends nothing; writes
nothing
patchstack-connect doctor [--url <live app>] Check that everything Patchstack needs works in this
project, and say how to fix each problem: install,
site, account, protection wiring, the guard's rules
file and its read-only credential, the map link and
the build steps, and whether the published app
fetches its rules. Sends the site UUID to the public
claim page, asks Patchstack whether it accepts the
credentials and when the app last fetched rules;
with --url, also requests the live app's page and
one test attack. Writes nothing.
Exits 1 when it finds a problem
patchstack-connect vulns Show what Patchstack found in this project's
packages: vulnerabilities by severity and
outdated packages, with the link to the
Expand Down Expand Up @@ -1693,6 +1706,18 @@ function runSummary(): number {
* The one thing to do now, for the word the summary ends on. Connection lives on the server, so it is
* looked up unless `.patchstackrc.json` already records it; everything else is read from the project.
*/
/** Check everything Patchstack needs in this project and print each problem with its fix. */
async function runDoctor(args: ParsedArgs): Promise<number> {
const cwd = process.cwd();
const config = await resolveCliConfig(args);
const state = await collectGuideState(cwd);
const url = getStringFlag(args.flags, 'url') ?? null;
const findings = await runDoctorChecks(cwd, state, config, { url, claimPreview: fetchClaimPreview });
for (const line of renderDoctor(findings, { url })) console.log(line);

return findings.some((finding) => finding.status === 'problem') ? 1 : 0;
}

async function runNext(args: ParsedArgs): Promise<number> {
const config = await resolveCliConfig(args);
if (config.siteUuid === null) {
Expand Down Expand Up @@ -2274,12 +2299,12 @@ async function main(): Promise<number> {

/** Commands `runCommand` dispatches; anything else is an unknown command and sends no notice. */
const KNOWN_COMMANDS = new Set([
'init', 'scan', 'vulns', 'next', 'summary', 'go-live', 'widget', 'status', 'uninstall', 'mark-build', 'protect',
'init', 'scan', 'vulns', 'next', 'doctor', 'summary', 'go-live', 'widget', 'status', 'uninstall', 'mark-build', 'protect',
'demo', 'demo-guide', 'guide', 'setup', 'map', 'claim', 'login',
]);

/** Commands that act on the whole project, so in a workspace package they run at the root that holds the site. */
const ROOT_COMMANDS = new Set(['setup', 'scan', 'widget', 'protect']);
const ROOT_COMMANDS = new Set(['setup', 'scan', 'widget', 'protect', 'doctor']);

/**
* A run that sent nothing the site's activity log records still tells Patchstack it ran, by the site's
Expand All @@ -2306,6 +2331,8 @@ async function runCommand(args: ParsedArgs): Promise<number> {
return runSummary();
case 'next':
return runNext(args);
case 'doctor':
return runDoctor(args);
case 'go-live':
return runGoLive(args);
case 'widget':
Expand Down
7 changes: 7 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ export function authFailureMessage(status: number, config: Config): string | nul
}

/** Build the live Patchstack for JavaScript rules URL corresponding to a manifest endpoint override. */
/** Where a site's own credential reads when its app last fetched rules, beside the rules endpoint. */
export function buildSyncStatusUrl(manifestEndpoint: string, siteUuid: string): string {
const url = new URL(buildRulesUrl(manifestEndpoint, siteUuid));
url.pathname = url.pathname.replace(/\/rules\/([^/]+)$/, '/sync-status/$1');
return url.toString();
}

export function buildRulesUrl(manifestEndpoint: string, siteUuid: string): string {
const url = new URL(manifestEndpoint);
const path = url.pathname.replace(/\/$/, '');
Expand Down
Loading
Loading