@patchstack/connect 0.5.10 → 0.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +27 -3
- package/README.md +18 -39
- package/dist/cli.js +71 -47
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
This versioned reference ships inside `@patchstack/connect` and documents each setup command and its project changes.
|
|
4
4
|
|
|
5
|
+
## Choose the project path first
|
|
6
|
+
|
|
7
|
+
Use the package setup flow below for an existing JS/Node application. Work in its package directory; a missing `package.json` can mean you are in a subdirectory or looking at generated HTML rather than the source project. A page containing HTML can still belong to a server-rendered application. Do not infer that runtime protection is unnecessary from the file extension alone.
|
|
8
|
+
|
|
9
|
+
### Plain HTML sites
|
|
10
|
+
|
|
11
|
+
For a standalone site made of HTML, CSS, and browser JavaScript, with no package-managed application or server request handler, use the disclosure widget directly. Do not create `package.json`, install a framework, invent build hooks, or add a server just to run Connect. `setup` requires an existing `package.json`; it is not a standalone HTML installer.
|
|
12
|
+
|
|
13
|
+
1. Use the public site UUID or widget snippet for the correct site in the Patchstack dashboard. An existing `.patchstackrc.json` can also supply `siteUuid`. If neither is available, ask the user for the site's public UUID or dashboard-provided snippet before editing the page. Never invent a UUID or use a claim token or API key as the widget identifier.
|
|
14
|
+
2. Add one widget tag before `</body>` in the page or shared layout. Preserve an existing correct tag. For a page published directly without a build step, disable the widget's build-mode onboarding with `data-build-mode="false"`:
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="YOUR_SITE_UUID" data-build-mode="false" defer></script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Replace `YOUR_SITE_UUID` with the real public site UUID before saving. Keep credentials out of the page. The [public widget reference](https://cdn.patchstack.com/llm.html) documents this embed and its options.
|
|
21
|
+
3. Verify the saved tag uses the correct UUID. If a browser preview is available, reload it and check for the report button; otherwise tell the user that the browser check is pending. Do not submit a vulnerability report as an installation test. Save the HTML change and remind the user to publish it when ready; do not deploy it yourself.
|
|
22
|
+
|
|
23
|
+
Report this as **disclosure widget installed**, with any remaining preview or publishing step. This path does not inventory local JavaScript files or scripts loaded from a CDN, scan npm dependencies, or install runtime exploit protection. External APIs used by the page require their own server-side integration.
|
|
24
|
+
|
|
5
25
|
## Command reference
|
|
6
26
|
|
|
7
27
|
Every command at a glance — what it does, whether it reads your source, what it writes, and what leaves your machine. Full behavior, flags, and edge cases follow in the sections below.
|
|
@@ -88,7 +108,7 @@ This is a request, not a mechanism: nothing in the install depends on it. Do it
|
|
|
88
108
|
|
|
89
109
|
**If the request you were given includes a claim token**, pass it exactly as given: `npx @patchstack/connect setup --claim-token <token>`. The site is then created in the person's own Patchstack account and `setup` prints their dashboard link for it — there is no sign-in step to relay. The token comes only from the person's Patchstack dashboard; never invent one, never write it to a file, never print it back. If `setup` reports that the token had expired or was not recognised, the site is not connected: hand over the dashboard link it prints instead, and tell the person they can copy a fresh prompt from the dashboard.
|
|
90
110
|
|
|
91
|
-
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 report `production` only when the build platform's own variables say so — its tier (Vercel, Netlify, Render, Railway, GitLab CI) or a branch named `main`, `master`, `production`, `prod`, `release` or `live` (Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier); a preview, pull request or other branch there reports `sandbox`, as does the Replit workspace, while a Replit Deployment reports `production`. A build in a project the builder generated and builds for itself (Lovable, Replit) reports `production` without an override, because the edit preview is a dev server and a build is the publish step — which is exactly why the sandbox label belongs in the workspace process and not in a file. A scan on a developer's machine, in a CI runner this does not know (`CI=true` alone), or on a platform with no such signal reports `local` on its own, and the dashboard shows that app as configured, not deployed. A
|
|
111
|
+
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 report `production` only when the build platform's own variables say so — its tier (Vercel, Netlify, Render, Railway, GitLab CI) or a branch named `main`, `master`, `production`, `prod`, `release` or `live` (Cloudflare Pages and Workers Builds, AWS Amplify, GitHub Actions, GitLab CI without a tier); a preview, pull request or other branch there reports `sandbox`, as does the Replit workspace, while a Replit Deployment reports `production`. A build in a project the builder generated and builds for itself (Lovable, Replit) reports `production` without an override, because the edit preview is a dev server and a build is the publish step — which is exactly why the sandbox label belongs in the workspace process and not in a file. A scan on a developer's machine, in a CI runner this does not know (`CI=true` alone), or on a platform with no such signal reports `local` on its own, and the dashboard shows that app as configured, not deployed. A positively identified static build reports runtime protection as not applicable. A bundler-only project, including plain Vite, can remain runtime unknown and receive a generic scaffold with incomplete wiring. Report that limitation; do not add an artificial server merely to make the check pass, and never describe a widget or an unwired scaffold as runtime protection.
|
|
92
112
|
|
|
93
113
|
**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.
|
|
94
114
|
|
|
@@ -180,7 +200,7 @@ Handle it in this order:
|
|
|
180
200
|
|
|
181
201
|
**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"`.
|
|
182
202
|
|
|
183
|
-
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:
|
|
203
|
+
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:
|
|
184
204
|
|
|
185
205
|
```html
|
|
186
206
|
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
|
|
@@ -248,7 +268,11 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
|
|
|
248
268
|
|
|
249
269
|
**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.
|
|
250
270
|
|
|
251
|
-
6. **
|
|
271
|
+
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:
|
|
272
|
+
|
|
273
|
+
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.
|
|
274
|
+
2. **The dashboard link** the scan printed — open it in a browser and sign in.
|
|
275
|
+
3. **`npx @patchstack/connect claim`** from the terminal, which prints a link to sign in with and then attaches the site.
|
|
252
276
|
|
|
253
277
|
## Rules
|
|
254
278
|
|
package/README.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# @patchstack/connect
|
|
2
2
|
|
|
3
|
-
Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.com)
|
|
3
|
+
Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.com). Connect does four things:
|
|
4
|
+
|
|
5
|
+
- **Dependency inventory** — reads your lockfile and reports the installed package names and versions, so Patchstack can match them against its vulnerability database and tell you when something needs patching. See *[What gets sent](#what-gets-sent)*.
|
|
6
|
+
- **Runtime guard** — an in-process guard, wired into your server, that virtually patches known vulnerabilities in those dependencies rather than waiting for you to upgrade. See *[Verifying the guard at runtime](#verifying-the-guard-at-runtime-opt-in)*.
|
|
7
|
+
- **Disclosure widget** — a floating button labelled **"Report a vulnerability"** that Connect adds to *your* site, so visitors who spot a security problem have somewhere to send it. This is a channel for reports about your site; it is not how you report a bug in this package. See *[The disclosure widget](#the-disclosure-widget)*.
|
|
8
|
+
- **Attack-surface map** — a description of your server's entry points and the sinks they can reach, built by reading your source locally. See *[`map`](#cli)*.
|
|
9
|
+
|
|
10
|
+
`setup` installs the first three in one command. `map` is never run for you — see the [CLI](#cli) section for what each command does and what it touches.
|
|
4
11
|
|
|
5
12
|
## Agent-assisted setup
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
For an existing JS/Node project, copy this request into a coding assistant, or run the same command yourself. For a standalone HTML/CSS/JavaScript site without a package-managed app, use the [plain HTML widget instructions](AGENT-INSTALL.md#plain-html-sites); do not add Node tooling just for the widget.
|
|
8
15
|
|
|
9
16
|
> 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.
|
|
10
17
|
|
|
@@ -446,55 +453,27 @@ Every scanned source is validated against `package.json`: if the chosen lockfile
|
|
|
446
453
|
## Development
|
|
447
454
|
|
|
448
455
|
```bash
|
|
449
|
-
npm
|
|
456
|
+
npm ci
|
|
450
457
|
npm run typecheck
|
|
458
|
+
npm run build # before the tests: several only run once dist/ exists, and skip silently without it
|
|
451
459
|
npm test
|
|
452
|
-
npm run build
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
### Manifest endpoint testing
|
|
456
|
-
|
|
457
|
-
To post the current lockfile manifest to a local Patchstack API endpoint and provision a new site:
|
|
458
|
-
|
|
459
|
-
```bash
|
|
460
|
-
bun run test:manifest -- --endpoint http://localhost:8000/monitor/pulse/manifest
|
|
461
460
|
```
|
|
462
461
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
```bash
|
|
466
|
-
bun run test:manifest -- --endpoint http://localhost:8000/monitor/pulse/manifest --site-uuid YOUR_REAL_UUID
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
Use `--dry-run` to preview the payload without posting.
|
|
462
|
+
`CONTRIBUTING.md` covers the rest — the Node versions this needs, the packaging checks, and what to run before opening a pull request. Changing onboarding, the install prompt or the setup guide? Read `MAINTAINING.md` first.
|
|
470
463
|
|
|
471
464
|
## Release process
|
|
472
465
|
|
|
473
466
|
Pull requests run typecheck, tests, build, package verification, and a production dependency audit in GitHub Actions.
|
|
474
467
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
To publish a release:
|
|
468
|
+
Releases are cut by the `Release` workflow, which works out the next version, tags it, and hands off to `Publish`:
|
|
478
469
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
4. Create and publish a GitHub Release tagged `v0.2.0`.
|
|
483
|
-
5. The `Publish` workflow verifies the package, then runs `npm publish --provenance --access public`.
|
|
484
|
-
|
|
485
|
-
Before the first release, configure npm trusted publishing for this package:
|
|
470
|
+
```bash
|
|
471
|
+
gh workflow run Release -f bump=patch # or: minor, major
|
|
472
|
+
```
|
|
486
473
|
|
|
487
|
-
|
|
488
|
-
2. Open the `@patchstack/connect` package settings on npmjs.com.
|
|
489
|
-
3. In **Trusted publishing**, choose **GitHub Actions**.
|
|
490
|
-
4. Configure:
|
|
491
|
-
- Organization/user: `patchstack`
|
|
492
|
-
- Repository: `connect`
|
|
493
|
-
- Workflow filename: `publish.yml`
|
|
494
|
-
- Environment name: `npm`
|
|
495
|
-
5. In GitHub repository settings, create an `npm` environment. Optional but recommended: require reviewer approval for that environment.
|
|
474
|
+
The git tag is the source of truth for the published version. `Publish` reads the version out of the tag, writes it into `package.json` in CI, then builds and publishes to npm with provenance — so you do **not** bump `package.json` before releasing. After publishing it opens a pull request bringing the committed manifest up to the version that just went out; merge that.
|
|
496
475
|
|
|
497
|
-
|
|
476
|
+
`RELEASING.md` has the details: how to pick the bump (a compatibility break on a `0.x` version needs at least a minor), the manual fallback, and the npm trusted-publishing configuration.
|
|
498
477
|
|
|
499
478
|
## License
|
|
500
479
|
|
package/dist/cli.js
CHANGED
|
@@ -3082,7 +3082,7 @@ function ensureSourceMarker(cwd, shell, framework, checksum = null) {
|
|
|
3082
3082
|
}
|
|
3083
3083
|
|
|
3084
3084
|
// src/guide.ts
|
|
3085
|
-
import { existsSync as
|
|
3085
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
3086
3086
|
import path12 from "path";
|
|
3087
3087
|
|
|
3088
3088
|
// src/architecture.ts
|
|
@@ -4311,7 +4311,7 @@ var nuxtAdapter = {
|
|
|
4311
4311
|
};
|
|
4312
4312
|
|
|
4313
4313
|
// src/protect/install/find-app.ts
|
|
4314
|
-
import { readFileSync as readFileSync8,
|
|
4314
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
|
|
4315
4315
|
import { join as join11, relative as relative3, dirname as dirname4 } from "path";
|
|
4316
4316
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
4317
4317
|
"node_modules",
|
|
@@ -4333,7 +4333,6 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
4333
4333
|
]);
|
|
4334
4334
|
var MAX_DEPTH = 8;
|
|
4335
4335
|
function findAppInstance(cwd, re) {
|
|
4336
|
-
const root = existsSync7(join11(cwd, "src")) ? join11(cwd, "src") : cwd;
|
|
4337
4336
|
const found = [];
|
|
4338
4337
|
const walk2 = (dir, depth) => {
|
|
4339
4338
|
if (depth > MAX_DEPTH) return;
|
|
@@ -4372,7 +4371,7 @@ function findAppInstance(cwd, re) {
|
|
|
4372
4371
|
}
|
|
4373
4372
|
}
|
|
4374
4373
|
};
|
|
4375
|
-
walk2(
|
|
4374
|
+
walk2(cwd, 0);
|
|
4376
4375
|
if (found.length === 0) return null;
|
|
4377
4376
|
const declared = declaredEntries(cwd);
|
|
4378
4377
|
const chosen = found.slice().sort((a, b) => rankEntry({ ...a, declared: declared.has(a.relPath) }, { ...b, declared: declared.has(b.relPath) }))[0];
|
|
@@ -4430,16 +4429,16 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
|
4430
4429
|
}
|
|
4431
4430
|
|
|
4432
4431
|
// src/protect/install/register.ts
|
|
4433
|
-
import { existsSync as
|
|
4432
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4434
4433
|
import { join as join14, resolve as resolve3 } from "path";
|
|
4435
4434
|
|
|
4436
4435
|
// src/protect/install/generic.ts
|
|
4437
|
-
import { readFileSync as readFileSync9, existsSync as
|
|
4436
|
+
import { readFileSync as readFileSync9, existsSync as existsSync8, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
|
|
4438
4437
|
import { join as join13, dirname as dirname6 } from "path";
|
|
4439
4438
|
|
|
4440
4439
|
// src/protect/install/source-scope.ts
|
|
4441
4440
|
import { execFileSync } from "child_process";
|
|
4442
|
-
import { existsSync as
|
|
4441
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4443
4442
|
import { dirname as dirname5, join as join12, resolve as resolve2 } from "path";
|
|
4444
4443
|
|
|
4445
4444
|
// src/protect/install/node-flags.ts
|
|
@@ -4731,7 +4730,7 @@ function moduleKindOf(filePath, packageIsModule) {
|
|
|
4731
4730
|
function resolvesToGuardModule(specifier, query) {
|
|
4732
4731
|
const resolved = resolve2(dirname5(query.fromFile), specifier);
|
|
4733
4732
|
if (withoutModuleExtension(resolved) !== join12(query.guardDir, "guard")) return false;
|
|
4734
|
-
const existing = GUARD_FILENAMES.filter((name) =>
|
|
4733
|
+
const existing = GUARD_FILENAMES.filter((name) => existsSync7(join12(query.guardDir, name)));
|
|
4735
4734
|
if (existing.length === 0) return false;
|
|
4736
4735
|
const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
|
|
4737
4736
|
if (!query.strictExtensions) return true;
|
|
@@ -4740,7 +4739,7 @@ function resolvesToGuardModule(specifier, query) {
|
|
|
4740
4739
|
if (query.kind === "cjs") return existing.includes("guard.js");
|
|
4741
4740
|
return true;
|
|
4742
4741
|
}
|
|
4743
|
-
if (
|
|
4742
|
+
if (existsSync7(resolved)) return true;
|
|
4744
4743
|
return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
|
|
4745
4744
|
}
|
|
4746
4745
|
function withoutModuleExtension(filePath) {
|
|
@@ -4756,10 +4755,10 @@ function packageIsEsm(cwd) {
|
|
|
4756
4755
|
}
|
|
4757
4756
|
}
|
|
4758
4757
|
function genericDir(cwd) {
|
|
4759
|
-
return
|
|
4758
|
+
return existsSync8(join13(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
4760
4759
|
}
|
|
4761
4760
|
function hasTypeScript(cwd) {
|
|
4762
|
-
return
|
|
4761
|
+
return existsSync8(join13(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
|
|
4763
4762
|
}
|
|
4764
4763
|
function genericGuardTarget(cwd) {
|
|
4765
4764
|
if (hasTypeScript(cwd)) {
|
|
@@ -4773,7 +4772,7 @@ function genericGuardTarget(cwd) {
|
|
|
4773
4772
|
function genericScaffoldFiles(cwd) {
|
|
4774
4773
|
const dir = genericDir(cwd);
|
|
4775
4774
|
const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
|
|
4776
|
-
return candidates.map((file) => `${dir}/${file}`).filter((relative8) =>
|
|
4775
|
+
return candidates.map((file) => `${dir}/${file}`).filter((relative8) => existsSync8(join13(cwd, relative8)));
|
|
4777
4776
|
}
|
|
4778
4777
|
function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
4779
4778
|
const target = genericGuardTarget(cwd);
|
|
@@ -4788,7 +4787,7 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
|
4788
4787
|
const changed = [guardRel];
|
|
4789
4788
|
if (!opts.demo) bakeSiteUuid(cwd, guardRel);
|
|
4790
4789
|
const rulesDst = join13(dst, "rules.json");
|
|
4791
|
-
if (opts.demo || !
|
|
4790
|
+
if (opts.demo || !existsSync8(rulesDst)) {
|
|
4792
4791
|
copyProjectFileSync(cwd, join13(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
4793
4792
|
changed.push(`${dir}/rules.json`);
|
|
4794
4793
|
}
|
|
@@ -4810,10 +4809,10 @@ function candidateEntries(cwd) {
|
|
|
4810
4809
|
"app.ts",
|
|
4811
4810
|
"app.js"
|
|
4812
4811
|
];
|
|
4813
|
-
const hits = names.filter((n) =>
|
|
4812
|
+
const hits = names.filter((n) => existsSync8(join13(cwd, n)));
|
|
4814
4813
|
try {
|
|
4815
4814
|
const pkg = JSON.parse(read(join13(cwd, "package.json")));
|
|
4816
|
-
if (typeof pkg.main === "string" &&
|
|
4815
|
+
if (typeof pkg.main === "string" && existsSync8(join13(cwd, pkg.main))) hits.push(pkg.main);
|
|
4817
4816
|
} catch {
|
|
4818
4817
|
}
|
|
4819
4818
|
return [...new Set(hits)];
|
|
@@ -4924,7 +4923,7 @@ function escapeForRegExp(value) {
|
|
|
4924
4923
|
function genericVerify(cwd) {
|
|
4925
4924
|
const dir = genericDir(cwd);
|
|
4926
4925
|
const target = genericGuardTarget(cwd);
|
|
4927
|
-
const present2 = GUARD_FILENAMES.find((name) =>
|
|
4926
|
+
const present2 = GUARD_FILENAMES.find((name) => existsSync8(join13(cwd, dir, name)));
|
|
4928
4927
|
const scaffolded = present2 !== void 0;
|
|
4929
4928
|
const imported = scaffolded && guardIsImported(cwd, join13(cwd, dir, present2));
|
|
4930
4929
|
return {
|
|
@@ -4962,7 +4961,7 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
|
|
|
4962
4961
|
function guardIsImported(cwd, guardPath) {
|
|
4963
4962
|
const guardDir = dirname6(guardPath);
|
|
4964
4963
|
const packageIsModule = packageIsEsm(cwd);
|
|
4965
|
-
const root =
|
|
4964
|
+
const root = existsSync8(join13(cwd, "src")) ? join13(cwd, "src") : cwd;
|
|
4966
4965
|
let found = false;
|
|
4967
4966
|
const walk2 = (d, depth) => {
|
|
4968
4967
|
if (found || depth > 8) return;
|
|
@@ -5166,10 +5165,10 @@ function serverIsGuarded(cwd, spec, server, guardDir) {
|
|
|
5166
5165
|
return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
|
|
5167
5166
|
}
|
|
5168
5167
|
function verifyRegister(cwd, spec) {
|
|
5169
|
-
const dir =
|
|
5168
|
+
const dir = existsSync9(join14(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
5170
5169
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
5171
5170
|
const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
|
|
5172
|
-
const scaffolded = target ?
|
|
5171
|
+
const scaffolded = target ? existsSync9(join14(cwd, dir, target.file)) : false;
|
|
5173
5172
|
const entrySource = entry ? read(join14(cwd, entry.relPath)) : "";
|
|
5174
5173
|
const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
|
|
5175
5174
|
const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
|
|
@@ -5260,7 +5259,7 @@ var expressAdapter = {
|
|
|
5260
5259
|
};
|
|
5261
5260
|
|
|
5262
5261
|
// src/protect/install/reporting.ts
|
|
5263
|
-
import { existsSync as
|
|
5262
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5264
5263
|
import { join as join15 } from "path";
|
|
5265
5264
|
|
|
5266
5265
|
// src/protect/reporting-state.js
|
|
@@ -5321,7 +5320,7 @@ function explainReportingState(state) {
|
|
|
5321
5320
|
function siteIdentity(cwd, env) {
|
|
5322
5321
|
const rc = join15(cwd, ".patchstackrc.json");
|
|
5323
5322
|
let recorded;
|
|
5324
|
-
if (
|
|
5323
|
+
if (existsSync10(rc)) {
|
|
5325
5324
|
try {
|
|
5326
5325
|
recorded = JSON.parse(read(rc)).siteUuid;
|
|
5327
5326
|
} catch {
|
|
@@ -5470,7 +5469,7 @@ function runVerify(cwd) {
|
|
|
5470
5469
|
}
|
|
5471
5470
|
|
|
5472
5471
|
// src/widget.ts
|
|
5473
|
-
import { existsSync as
|
|
5472
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
5474
5473
|
import path11 from "path";
|
|
5475
5474
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
5476
5475
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
@@ -5509,7 +5508,7 @@ ${indent}</body>`;
|
|
|
5509
5508
|
}
|
|
5510
5509
|
function findSourceShell(cwd) {
|
|
5511
5510
|
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
5512
|
-
if (
|
|
5511
|
+
if (existsSync11(path11.join(cwd, candidate))) {
|
|
5513
5512
|
return candidate;
|
|
5514
5513
|
}
|
|
5515
5514
|
}
|
|
@@ -5521,7 +5520,7 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
5521
5520
|
return { shell: null, action: "no-shell" };
|
|
5522
5521
|
}
|
|
5523
5522
|
const file = path11.join(cwd, shell);
|
|
5524
|
-
if (!
|
|
5523
|
+
if (!existsSync11(file)) {
|
|
5525
5524
|
return { shell: null, action: "no-shell" };
|
|
5526
5525
|
}
|
|
5527
5526
|
const before = readFileSync10(file, "utf8");
|
|
@@ -5606,7 +5605,7 @@ function detectPackageManager(cwd) {
|
|
|
5606
5605
|
return declared;
|
|
5607
5606
|
}
|
|
5608
5607
|
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
5609
|
-
if (
|
|
5608
|
+
if (existsSync12(path12.join(cwd, filename))) {
|
|
5610
5609
|
return pm;
|
|
5611
5610
|
}
|
|
5612
5611
|
}
|
|
@@ -5703,7 +5702,7 @@ function resolveWidgetFileHint(cwd, framework) {
|
|
|
5703
5702
|
...GENERIC_WIDGET_FILES
|
|
5704
5703
|
];
|
|
5705
5704
|
for (const candidate of candidates) {
|
|
5706
|
-
if (
|
|
5705
|
+
if (existsSync12(path12.join(cwd, candidate))) {
|
|
5707
5706
|
return candidate;
|
|
5708
5707
|
}
|
|
5709
5708
|
}
|
|
@@ -5823,7 +5822,11 @@ function renderGuideChecklist(state, useColor) {
|
|
|
5823
5822
|
lines.push("");
|
|
5824
5823
|
if (!state.hasPackageJson) {
|
|
5825
5824
|
lines.push(todo("No package.json found in this directory."));
|
|
5826
|
-
lines.push(detail("
|
|
5825
|
+
lines.push(detail("For a JS/Node app, run the guide from its package directory; check that package.json is readable and valid."));
|
|
5826
|
+
lines.push(detail("For a standalone HTML/CSS/browser-JavaScript site, use the disclosure widget directly."));
|
|
5827
|
+
lines.push(detail("Do not create a Node project, build hooks, or a server just to install the widget."));
|
|
5828
|
+
lines.push(detail("Use the correct site UUID or widget snippet from the Patchstack dashboard; never invent one."));
|
|
5829
|
+
lines.push(detail('See "Plain HTML sites" in AGENT-INSTALL.md. Widget-only setup provides no dependency scan or runtime protection.'));
|
|
5827
5830
|
return lines.join("\n");
|
|
5828
5831
|
}
|
|
5829
5832
|
if (countRemainingSteps(state) > 0) {
|
|
@@ -5945,11 +5948,19 @@ function renderGuideChecklist(state, useColor) {
|
|
|
5945
5948
|
}
|
|
5946
5949
|
lines.push("");
|
|
5947
5950
|
if (state.claimUrl !== null) {
|
|
5948
|
-
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5951
|
+
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Connect this site to your Patchstack account:")}`);
|
|
5952
|
+
let route = 1;
|
|
5953
|
+
if (widgetTagInPlace(state)) {
|
|
5954
|
+
lines.push(` ${route++}. In the preview \u2014 while the site is unclaimed the widget shows a`);
|
|
5955
|
+
lines.push(' "Connect this website" panel. Signing in there attaches the site.');
|
|
5956
|
+
}
|
|
5957
|
+
lines.push(` ${route++}. In a browser \u2014 open the dashboard link (the CLI never opens it):`);
|
|
5958
|
+
lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
|
|
5959
|
+
lines.push(` ${route}. From this terminal \u2192 npx @patchstack/connect claim`);
|
|
5960
|
+
lines.push(" (prints a link to sign in with, then attaches the site to that account)");
|
|
5961
|
+
lines.push(detail("Reports have no owner to reach until one of these completes. The site UUID ships"));
|
|
5962
|
+
lines.push(detail("in the page and claiming is first-come, so an unclaimed site stays claimable by"));
|
|
5963
|
+
lines.push(detail("anyone who loads it."));
|
|
5953
5964
|
if (state.endpointOverride !== null) {
|
|
5954
5965
|
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
5955
5966
|
}
|
|
@@ -5959,9 +5970,11 @@ function renderGuideChecklist(state, useColor) {
|
|
|
5959
5970
|
if (widgetTagInPlace(state)) {
|
|
5960
5971
|
lines.push("");
|
|
5961
5972
|
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Refresh the preview to see the widget:")}`);
|
|
5962
|
-
lines.push(
|
|
5963
|
-
lines.push("
|
|
5964
|
-
lines.push("
|
|
5973
|
+
lines.push(" The widget loads with the page, so a preview that was already open still shows");
|
|
5974
|
+
lines.push(" the HTML from before this change. Builders that hot reload refresh it themselves;");
|
|
5975
|
+
lines.push(" if nothing appears, refresh the preview once.");
|
|
5976
|
+
lines.push(' Unclaimed, the widget shows the "Connect this website" panel; the public');
|
|
5977
|
+
lines.push(' "Report a vulnerability" button takes its place once the site is claimed.');
|
|
5965
5978
|
}
|
|
5966
5979
|
if (state.siteUuid !== null) {
|
|
5967
5980
|
lines.push("");
|
|
@@ -6295,7 +6308,7 @@ async function claim2(config, onPrompt, deps = {}) {
|
|
|
6295
6308
|
import { relative as relative4 } from "path";
|
|
6296
6309
|
|
|
6297
6310
|
// src/protect/install/runtime/entry.ts
|
|
6298
|
-
import { existsSync as
|
|
6311
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
|
|
6299
6312
|
import { join as join16, resolve as resolve4, sep as sep2 } from "path";
|
|
6300
6313
|
var CONVENTIONAL = [
|
|
6301
6314
|
"server.js",
|
|
@@ -6392,14 +6405,14 @@ function resolveEntry(cwd) {
|
|
|
6392
6405
|
}
|
|
6393
6406
|
return {
|
|
6394
6407
|
kind: "unavailable",
|
|
6395
|
-
reason:
|
|
6408
|
+
reason: existsSync13(join16(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
|
|
6396
6409
|
};
|
|
6397
6410
|
}
|
|
6398
6411
|
|
|
6399
6412
|
// src/protect/install/runtime/probe.ts
|
|
6400
6413
|
import { spawn } from "child_process";
|
|
6401
6414
|
import { createHash as createHash3, randomBytes } from "crypto";
|
|
6402
|
-
import { existsSync as
|
|
6415
|
+
import { existsSync as existsSync14 } from "fs";
|
|
6403
6416
|
import { request as httpRequest } from "http";
|
|
6404
6417
|
import { request as httpsRequest, Agent as HttpsAgent } from "https";
|
|
6405
6418
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -6427,7 +6440,7 @@ function preloadPath() {
|
|
|
6427
6440
|
];
|
|
6428
6441
|
for (const candidate of candidates) {
|
|
6429
6442
|
const path18 = fileURLToPath2(candidate);
|
|
6430
|
-
if (
|
|
6443
|
+
if (existsSync14(path18)) return path18;
|
|
6431
6444
|
}
|
|
6432
6445
|
return null;
|
|
6433
6446
|
}
|
|
@@ -9625,14 +9638,21 @@ async function runScan(args, options = {}) {
|
|
|
9625
9638
|
const linkUuid = response.uuid ?? config.siteUuid;
|
|
9626
9639
|
if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
|
|
9627
9640
|
console.log("");
|
|
9628
|
-
console.log("
|
|
9629
|
-
|
|
9641
|
+
console.log("Connect this site to your Patchstack account \u2014 any one of these:");
|
|
9642
|
+
if (config.widget) {
|
|
9643
|
+
console.log(' 1. In the preview \u2014 the widget shows a "Connect this website" panel while the');
|
|
9644
|
+
console.log(" site is unclaimed. Signing in there attaches it.");
|
|
9645
|
+
console.log(" 2. Open this dashboard link in a browser:");
|
|
9646
|
+
console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
|
|
9647
|
+
console.log(" 3. From this terminal: npx @patchstack/connect claim");
|
|
9648
|
+
} else {
|
|
9649
|
+
console.log(" 1. Open this dashboard link in a browser:");
|
|
9650
|
+
console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
|
|
9651
|
+
console.log(" 2. From this terminal: npx @patchstack/connect claim");
|
|
9652
|
+
}
|
|
9630
9653
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
9631
9654
|
console.log(" (this URL inherits the endpoint override above)");
|
|
9632
9655
|
}
|
|
9633
|
-
console.log("");
|
|
9634
|
-
console.log("Or attach it to your account from this terminal:");
|
|
9635
|
-
console.log(" npx @patchstack/connect claim");
|
|
9636
9656
|
}
|
|
9637
9657
|
if (options.showRemainingSetup !== false) {
|
|
9638
9658
|
try {
|
|
@@ -9658,7 +9678,8 @@ function reportSourceWidget(siteUuid, framework) {
|
|
|
9658
9678
|
const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
|
|
9659
9679
|
switch (result.action) {
|
|
9660
9680
|
case "added":
|
|
9661
|
-
console.log(`Widget: added the
|
|
9681
|
+
console.log(`Widget: added the disclosure widget tag to ${result.shell}. Reload your preview to see it.`);
|
|
9682
|
+
console.log(' Unclaimed, it shows a "Connect this website" panel; the "Report a vulnerability" button replaces it once the site is claimed.');
|
|
9662
9683
|
break;
|
|
9663
9684
|
case "updated":
|
|
9664
9685
|
console.log(`Widget: updated the managed tag in ${result.shell} to site ${siteUuid}.`);
|
|
@@ -9890,6 +9911,7 @@ async function runSetup(args) {
|
|
|
9890
9911
|
const before = await collectGuideState(process.cwd());
|
|
9891
9912
|
if (!before.hasPackageJson) {
|
|
9892
9913
|
console.error("Error: no package.json found. Run setup from the project root.");
|
|
9914
|
+
console.error("For a standalone HTML site, use the widget-only instructions in AGENT-INSTALL.md; do not create a Node project just to run setup.");
|
|
9893
9915
|
return 1;
|
|
9894
9916
|
}
|
|
9895
9917
|
if (before.installed === null) {
|
|
@@ -9967,7 +9989,7 @@ function setupOutcome(state, protection, wired) {
|
|
|
9967
9989
|
if (protection.install.status === "not-applicable" && protection.install.leftovers.length > 0) {
|
|
9968
9990
|
warnings.push(`earlier guard scaffold does nothing here and can be deleted: ${protection.install.leftovers.join(", ")}`);
|
|
9969
9991
|
}
|
|
9970
|
-
if (state.claimUrl !== null) warnings.push(
|
|
9992
|
+
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');
|
|
9971
9993
|
if (warnings.length > 0) lines.push(["Warnings", warnings.join("; ")]);
|
|
9972
9994
|
return lines;
|
|
9973
9995
|
}
|
|
@@ -9985,7 +10007,9 @@ async function runStatus(args) {
|
|
|
9985
10007
|
console.log(`Environment: ${config.environment}`);
|
|
9986
10008
|
if (config.siteUuid !== null) {
|
|
9987
10009
|
console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
9988
|
-
console.log(
|
|
10010
|
+
console.log(` Not attached to an account yet? Sign in through the widget's "Connect this`);
|
|
10011
|
+
console.log(' website" panel on the preview, open the link above, or run');
|
|
10012
|
+
console.log(" `npx @patchstack/connect claim`.");
|
|
9989
10013
|
switch (await fetchSiteStatus(config)) {
|
|
9990
10014
|
case "active":
|
|
9991
10015
|
console.log("Site status: active on Patchstack");
|