@patchstack/connect 0.5.0 → 0.5.1
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 +4 -1
- package/README.md +17 -1
- package/dist/cli.js +42 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +9 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +10 -3
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.js +1 -1
- package/dist/{refresh-manifest-LDS35UKV.js → refresh-manifest-2MMPQN2A.js} +10 -3
- package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-LDS35UKV.js.map +0 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -9,7 +9,7 @@ Every command at a glance — what it does, whether it reads your source, what i
|
|
|
9
9
|
| Command | What it does | Reads your source? | Writes to your project | Sends over the network |
|
|
10
10
|
|---|---|---|---|---|
|
|
11
11
|
| `scan` | Provision (or reuse) the site and POST the dependency list for vulnerability matching. Also runs automatically via `setup` and the install/build hooks. | No — lockfile only; `node_modules/` is enumerated when no lockfile can be read (e.g. `bun.lockb`) or when the lockfiles present disagree. It also reads the `<title>` of the root `index.html` and the `name` in `package.json`, to report what the site is called | `.patchstackrc.json` (public: site UUID + settings); `.patchstackrc.local.json` (the API key, created owner-only) and a `.gitignore` entry for it — the CLI says so if it could not add one; the widget `<script>` tag in the root HTML shell — only after a successful post; the production marker in a code root shell — before the post, since it needs no site UUID | Package names + versions; this site's public address and name, where the project or build environment states them |
|
|
12
|
-
| `setup` | One bounded command: `scan` → manage the widget → install + verify `protect` → wire the install/build scans. Never runs the project build. | No | Config, widget tag, production marker, guard files, `package.json` scripts | Package names + versions and the site's public address and name (via `scan`) |
|
|
12
|
+
| `setup` | One bounded command: `scan` → manage the widget → install + verify `protect` → wire the install/build scans. Never runs the project build. | No | Config, widget tag, production marker, guard files, `package.json` scripts | Package names + versions and the site's public address and name (via `scan`); a claim token as a request header, only when you pass one |
|
|
13
13
|
| `map` | Local, read-only attack-surface analysis (entry points → inputs → sinks → evidence-backed flows). Never run by another command. | **Yes** — via the app's own TypeScript | Nothing (only the file named by `--out`) | Nothing — **unless `--upload`**: structure only (routes, parameter names, the package behind each sink, file:line). Never source code or env values |
|
|
14
14
|
| `protect` | Install the always-on runtime guard; auto-wire known stacks, or scaffold a generic guard + print a wiring plan. `--check` verifies the guard is wired (exit 1 if not); `--demo` seeds a broad sample rule set. Runs automatically **only** via `setup` — never by `scan`, `guide`, `status`, or `mark-build`. | No — writes guard files, does not analyze your code | Guard/framework files (e.g. `middleware.ts`, `src/patchstack/`) | Nothing |
|
|
15
15
|
| `demo node-serialize` | Production-backed walkthrough: confirm the vulnerable package is present, scan, wait for live rule `18843`, install + verify the guard, print test requests. Does not install the package or start/restart the app. | No | Same files as `scan` + `protect` | `scan` payload; polls the public Pulse rules endpoint (never the printed test requests) |
|
|
@@ -85,6 +85,8 @@ This is a request, not a mechanism: nothing in the install depends on it. Do it
|
|
|
85
85
|
|
|
86
86
|
This provisions or reuses the site, manages the widget, installs and verifies runtime protection, wires dependency-install and build scans, prints a dashboard link, and finishes with the same status shown by `guide`. Re-running it reuses existing configuration, widget tags, guards, and commands rather than duplicating them.
|
|
87
87
|
|
|
88
|
+
**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.
|
|
89
|
+
|
|
88
90
|
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.
|
|
89
91
|
|
|
90
92
|
**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.
|
|
@@ -149,6 +151,7 @@ It is server-only. Never put it in the widget tag, client bundles, or public env
|
|
|
149
151
|
## Rules
|
|
150
152
|
|
|
151
153
|
- Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
|
|
154
|
+
- Never invent or guess a claim token either. One is only ever handed to you by the person, from their own Patchstack dashboard; pass it with `--claim-token` (or `PATCHSTACK_CLAIM_TOKEN`) and nowhere else — not into `.patchstackrc.json`, not into a committed file, not into your reply.
|
|
152
155
|
- The CLI never opens the dashboard link and never asks for Patchstack credentials.
|
|
153
156
|
- Label hosted workspace scans with `PATCHSTACK_ENVIRONMENT=sandbox` in that process only. Leave production builds unset (the default is `production`) and never commit a sandbox label into files shared with production.
|
|
154
157
|
- If a step fails, stop and report it. Don't proceed with placeholders.
|
package/README.md
CHANGED
|
@@ -96,6 +96,8 @@ patchstack-connect --version Print the installed version
|
|
|
96
96
|
Options (for scan, setup, and status):
|
|
97
97
|
--site-uuid <uuid> Override the configured site UUID
|
|
98
98
|
--endpoint <url> Override the API endpoint
|
|
99
|
+
--claim-token <token> (scan, setup) Connect the site to the account that issued
|
|
100
|
+
the token (from the dashboard's "Connect website" prompt)
|
|
99
101
|
--dry-run (scan only) Print the payload without posting
|
|
100
102
|
|
|
101
103
|
Options (for demo and demo-guide):
|
|
@@ -107,7 +109,7 @@ Options (for demo and demo-guide):
|
|
|
107
109
|
|
|
108
110
|
Precedence (highest wins):
|
|
109
111
|
|
|
110
|
-
1. CLI flag (`--site-uuid`, `--endpoint`)
|
|
112
|
+
1. CLI flag (`--site-uuid`, `--endpoint`, `--claim-token`)
|
|
111
113
|
2. Environment variable
|
|
112
114
|
3. `.patchstackrc.local.json` in the current directory (the credential)
|
|
113
115
|
4. `.patchstackrc.json` in the current directory
|
|
@@ -118,6 +120,7 @@ Environment variables:
|
|
|
118
120
|
- `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`)
|
|
119
121
|
- `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
|
|
120
122
|
- `PATCHSTACK_ENVIRONMENT` — manifest label: `production` (default) or `sandbox`
|
|
123
|
+
- `PATCHSTACK_CLAIM_TOKEN` — connect the site straight to your account (see *Connecting straight to your account*)
|
|
121
124
|
|
|
122
125
|
Two files, because one value is public and the other is not.
|
|
123
126
|
|
|
@@ -152,6 +155,17 @@ The credential's file is never committed, so CI needs `PATCHSTACK_API_KEY` in th
|
|
|
152
155
|
|
|
153
156
|
A `pulseAuth` field is still read if present, and `PATCHSTACK_PULSE_AUTH` still overrides, for deployments that authenticate Pulse ingest with a different credential from block-logs. Neither is written by default, and neither is needed when the two share one.
|
|
154
157
|
|
|
158
|
+
### Connecting straight to your account
|
|
159
|
+
|
|
160
|
+
The dashboard's "Connect website" prompt carries a **claim token**. Pass it to the first `setup` (or `scan`) and the site it provisions is created in your account, so there is no dashboard link to open afterwards:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
npx @patchstack/connect setup --claim-token <token>
|
|
164
|
+
# or: PATCHSTACK_CLAIM_TOKEN=<token> npx @patchstack/connect setup
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The token names your account, not the project: it is never written to `.patchstackrc.json` or the credential file, it is sent to Patchstack as a request header rather than in the manifest body, and it stops working within a day. A token that has expired (or one Patchstack does not recognise) leaves the site exactly as a scan without one would — unconnected, with the dashboard link printed — and `scan` says so. Re-running `setup` with the same token against a site already in your account is a no-op that says the site is already connected; a site that belongs to a different account is left alone.
|
|
168
|
+
|
|
155
169
|
### Sandbox and production manifests
|
|
156
170
|
|
|
157
171
|
Every `scan` sends an environment label with its dependency manifest. The default is `production`; sandboxed builders should set `PATCHSTACK_ENVIRONMENT=sandbox` in the sandbox process only. Patchstack stores and deduplicates manifests per environment, so an iterative workspace scan does not replace the last production manifest.
|
|
@@ -244,6 +258,8 @@ The name is what the dashboard calls the site. It is taken from `name` in `.patc
|
|
|
244
258
|
|
|
245
259
|
You can see exactly what would be sent, without sending it, by running `npx @patchstack/connect scan --dry-run`: the preview it prints is the request body itself. Patchstack only applies either field to a site that does not have one yet: it never re-points a site whose address is real, and never replaces a name set in the dashboard.
|
|
246
260
|
|
|
261
|
+
One thing travels outside that body: a claim token, when you pass one (`--claim-token` / `PATCHSTACK_CLAIM_TOKEN`), is sent as the `X-Patchstack-Claim-Token` request header so that the site is created in your account. Without one, no such header is sent.
|
|
262
|
+
|
|
247
263
|
### `scan --install-paths` (opt-in)
|
|
248
264
|
|
|
249
265
|
Pass it and each entry also carries where that version is installed:
|
package/dist/cli.js
CHANGED
|
@@ -1291,6 +1291,25 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
1291
1291
|
// src/client.ts
|
|
1292
1292
|
var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
1293
1293
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1294
|
+
var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
1295
|
+
function claimTokenHeader(config) {
|
|
1296
|
+
return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1297
|
+
}
|
|
1298
|
+
function claimOutcomeLines(claim2, config) {
|
|
1299
|
+
if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
|
|
1300
|
+
if (claim2?.state === "claimed" || claim2?.state === "owned-by-you") {
|
|
1301
|
+
const dashboard = typeof claim2.dashboard_url === "string" && claim2.dashboard_url !== "" ? [`Dashboard: ${claim2.dashboard_url}`] : [];
|
|
1302
|
+
return [
|
|
1303
|
+
claim2.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
|
|
1304
|
+
...dashboard
|
|
1305
|
+
];
|
|
1306
|
+
}
|
|
1307
|
+
const why = claim2 === void 0 ? "Patchstack did not act on the claim token" : claim2.state === "owned-by-other" ? "this site belongs to a different Patchstack account" : claim2.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
|
|
1308
|
+
return [
|
|
1309
|
+
`Not connected to your account: ${why}.`,
|
|
1310
|
+
"Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
|
|
1311
|
+
];
|
|
1312
|
+
}
|
|
1294
1313
|
function buildEndpointUrl(base, siteUuid) {
|
|
1295
1314
|
const trimmed = base.replace(/\/$/, "");
|
|
1296
1315
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
@@ -1454,7 +1473,8 @@ async function postManifest(config, payload) {
|
|
|
1454
1473
|
headers: {
|
|
1455
1474
|
"Content-Type": "application/json",
|
|
1456
1475
|
Accept: "application/json",
|
|
1457
|
-
"User-Agent": "@patchstack/connect"
|
|
1476
|
+
"User-Agent": "@patchstack/connect",
|
|
1477
|
+
...claimTokenHeader(config)
|
|
1458
1478
|
},
|
|
1459
1479
|
body: JSON.stringify(buildManifestBody(config, payload)),
|
|
1460
1480
|
signal: AbortSignal.timeout(timeoutMs)
|
|
@@ -1944,6 +1964,7 @@ async function resolveConfig(options) {
|
|
|
1944
1964
|
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
1945
1965
|
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1946
1966
|
const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
|
|
1967
|
+
const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
|
|
1947
1968
|
return {
|
|
1948
1969
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1949
1970
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -1953,7 +1974,8 @@ async function resolveConfig(options) {
|
|
|
1953
1974
|
endpoint,
|
|
1954
1975
|
timeoutMs,
|
|
1955
1976
|
environment,
|
|
1956
|
-
widget: fromFile.widget !== false
|
|
1977
|
+
widget: fromFile.widget !== false,
|
|
1978
|
+
claimToken
|
|
1957
1979
|
};
|
|
1958
1980
|
}
|
|
1959
1981
|
async function writeConfigFile(cwd, config) {
|
|
@@ -7214,6 +7236,10 @@ Options (for scan, setup, status, and uninstall):
|
|
|
7214
7236
|
same under a workspace that pins its own copy), so an
|
|
7215
7237
|
advisory can be matched to the copy your code actually
|
|
7216
7238
|
loads. Off by default; never source file paths
|
|
7239
|
+
--claim-token <token> (scan, setup) Connect the site to the Patchstack account
|
|
7240
|
+
that issued the token \u2014 it comes from the dashboard's
|
|
7241
|
+
"Connect website" prompt. PATCHSTACK_CLAIM_TOKEN works
|
|
7242
|
+
too. Never written to a file, never printed back
|
|
7217
7243
|
|
|
7218
7244
|
Options (for mark-build):
|
|
7219
7245
|
--dir <path> Build output directory (default: auto-detect
|
|
@@ -7247,7 +7273,7 @@ Examples:
|
|
|
7247
7273
|
npx @patchstack/connect demo node-serialize
|
|
7248
7274
|
npx @patchstack/connect demo-guide node-serialize
|
|
7249
7275
|
`;
|
|
7250
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out"]);
|
|
7276
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
|
|
7251
7277
|
function parseArgs(argv) {
|
|
7252
7278
|
const args = argv.slice(2);
|
|
7253
7279
|
const positional = [];
|
|
@@ -7381,6 +7407,7 @@ async function runScan(args, options = {}) {
|
|
|
7381
7407
|
cwd: process.cwd(),
|
|
7382
7408
|
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
7383
7409
|
cliEndpoint: getStringFlag(args.flags, "endpoint"),
|
|
7410
|
+
cliClaimToken: getStringFlag(args.flags, "claim-token"),
|
|
7384
7411
|
// The one command that reports them, so the one command that resolves them.
|
|
7385
7412
|
detectSiteIdentity: true
|
|
7386
7413
|
});
|
|
@@ -7421,6 +7448,9 @@ async function runScan(args, options = {}) {
|
|
|
7421
7448
|
if (typeof body.name === "string") {
|
|
7422
7449
|
console.log(`Reporting this app's name as "${body.name}".`);
|
|
7423
7450
|
}
|
|
7451
|
+
if (typeof config.claimToken === "string" && config.claimToken !== "") {
|
|
7452
|
+
console.log("A claim token is set: the site will be connected to the Patchstack account that issued it.");
|
|
7453
|
+
}
|
|
7424
7454
|
if (dryRun) {
|
|
7425
7455
|
console.log("");
|
|
7426
7456
|
if (config.siteUuid === null) {
|
|
@@ -7475,14 +7505,21 @@ async function runScan(args, options = {}) {
|
|
|
7475
7505
|
} else {
|
|
7476
7506
|
console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
|
|
7477
7507
|
}
|
|
7508
|
+
const claimLines = claimOutcomeLines(response.claim, config);
|
|
7509
|
+
if (claimLines.length > 0) {
|
|
7510
|
+
console.log("");
|
|
7511
|
+
for (const line of claimLines) console.log(line);
|
|
7512
|
+
}
|
|
7513
|
+
const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
|
|
7478
7514
|
const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
|
|
7479
7515
|
if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
|
|
7480
7516
|
reportSourceWidget(effectiveUuid);
|
|
7481
7517
|
}
|
|
7482
|
-
|
|
7518
|
+
const linkUuid = response.uuid ?? config.siteUuid;
|
|
7519
|
+
if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
|
|
7483
7520
|
console.log("");
|
|
7484
7521
|
console.log("Open this dashboard link to view vulnerability reports:");
|
|
7485
|
-
console.log(` ${buildClaimUrl(config.endpoint,
|
|
7522
|
+
console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
|
|
7486
7523
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
7487
7524
|
console.log(" (this URL inherits the endpoint override above)");
|
|
7488
7525
|
}
|