@patchstack/connect 0.3.33 → 0.4.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 +28 -3
- package/README.md +5 -1
- package/dist/cli.js +186 -32
- package/dist/cli.js.map +1 -1
- package/dist/protect.cjs +369 -73
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +13 -2
- package/dist/protect.d.ts +13 -2
- package/dist/protect.edge.js +369 -73
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +369 -73
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -70,6 +70,10 @@ Only `map` reads your source, and only `map --upload` sends anything derived fro
|
|
|
70
70
|
|
|
71
71
|
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.
|
|
72
72
|
|
|
73
|
+
**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.
|
|
74
|
+
|
|
75
|
+
**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.
|
|
76
|
+
|
|
73
77
|
## Manual setup
|
|
74
78
|
|
|
75
79
|
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:
|
|
@@ -176,8 +180,28 @@ would have stopped while it is still in dry-run. Two separate paths, with differ
|
|
|
176
180
|
|
|
177
181
|
What a detection report contains, per matched rule — on every phase, whatever fired it: the rule id, the revision of the rule when the
|
|
178
182
|
bundle carried one, the identifier of the rule bundle in use, which phase matched,
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
the rule's category and the action it declares, whether it was enforced,
|
|
184
|
+
which call it belongs to, the request path **with the query string's values removed**,
|
|
185
|
+
that query's parameter names, the method, and a timestamp.
|
|
186
|
+
|
|
187
|
+
Which call it belongs to is a token your guard mints and repeats on every rule that matched the same
|
|
188
|
+
request or the same outbound call. It exists because two rules matching one call is ordinary rather than
|
|
189
|
+
unusual — a rule that enforces and a rule that only observes are meant to match the same thing — so
|
|
190
|
+
without it, adding these reports up counts one call more than once.
|
|
191
|
+
|
|
192
|
+
Nothing about the request goes into it: not the address, not the path, not a header. It is drawn from
|
|
193
|
+
your runtime's randomness where that exists, and from the clock plus `Math.random` where it does not,
|
|
194
|
+
which is how this package already makes its own instance id. It is never a secret and never a boundary —
|
|
195
|
+
nothing is authorised by holding it — so what it has to do is not collide between two calls. A request and the response to it share one token; an
|
|
196
|
+
outbound call gets its own, because an outbound attempt is a thing in its own right and one made outside
|
|
197
|
+
any request has no request to belong to.
|
|
198
|
+
|
|
199
|
+
The category and the declared action say what KIND of rule matched — "a secret-exposure rule that
|
|
200
|
+
redacts", "an SSRF rule that blocks". Both are read from the rule your guard was served, and both are
|
|
201
|
+
`null` when that rule declares neither: a rule whose class nobody can state is reported as one, not
|
|
202
|
+
filled in. Neither is the same as `enforced`, which is whether the rule acted on this particular
|
|
203
|
+
request: a rule declaring `block` while only observing reports exactly that, and that is what a
|
|
204
|
+
detect-only deployment consists of. Each batch also carries a count of reports dropped when traffic outran the flush,
|
|
181
205
|
so a partial sample is not read as a complete one.
|
|
182
206
|
|
|
183
207
|
Two fields depend on the phase, because one kind of detection has a client and the other does not. A
|
|
@@ -348,7 +372,8 @@ Two more endpoints the package can call, for completeness:
|
|
|
348
372
|
|
|
349
373
|
- `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`).
|
|
350
374
|
- `npx @patchstack/connect protect --check` verifies the runtime guard is connected to the request path.
|
|
351
|
-
- Load the site in a browser — the "Report a vulnerability" button should appear.
|
|
375
|
+
- 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.
|
|
376
|
+
- On the deployed site, the button appears only after a deploy that includes these source changes.
|
|
352
377
|
|
|
353
378
|
## Answering "is Patchstack installed?" / "is Patchstack removed?"
|
|
354
379
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Connect a JavaScript / Node.js application to [Patchstack](https://patchstack.co
|
|
|
6
6
|
|
|
7
7
|
Copy this request into a coding assistant, or run the same command yourself:
|
|
8
8
|
|
|
9
|
-
> 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.
|
|
9
|
+
> 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
10
|
|
|
11
11
|
`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.
|
|
12
12
|
|
|
@@ -32,6 +32,10 @@ That's it. `setup`:
|
|
|
32
32
|
8. Wires `scan` before builds and `mark-build` after builds, preserving existing commands and using direct build chaining for Bun.
|
|
33
33
|
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`.
|
|
34
34
|
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
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.
|
|
38
|
+
|
|
35
39
|
## Quick start (existing site)
|
|
36
40
|
|
|
37
41
|
If you already created an "Application" site in the Patchstack dashboard, pre-seed the UUID:
|
package/dist/cli.js
CHANGED
|
@@ -2176,7 +2176,7 @@ function ensureSourceMarker(cwd, shell, framework) {
|
|
|
2176
2176
|
}
|
|
2177
2177
|
|
|
2178
2178
|
// src/guide.ts
|
|
2179
|
-
import { existsSync as
|
|
2179
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
2180
2180
|
import path9 from "path";
|
|
2181
2181
|
|
|
2182
2182
|
// src/protect/install/util.ts
|
|
@@ -2193,8 +2193,8 @@ function hasDependency(cwd, name) {
|
|
|
2193
2193
|
return false;
|
|
2194
2194
|
}
|
|
2195
2195
|
}
|
|
2196
|
-
function hasResolvableCredential(cwd) {
|
|
2197
|
-
if ((
|
|
2196
|
+
function hasResolvableCredential(cwd, env = process.env) {
|
|
2197
|
+
if ((env.PATCHSTACK_API_KEY ?? "") !== "" || (env.PATCHSTACK_PULSE_AUTH ?? "") !== "") {
|
|
2198
2198
|
return true;
|
|
2199
2199
|
}
|
|
2200
2200
|
for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
|
|
@@ -2209,6 +2209,9 @@ function hasResolvableCredential(cwd) {
|
|
|
2209
2209
|
}
|
|
2210
2210
|
var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
|
|
2211
2211
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2212
|
+
function isSiteUuid(value) {
|
|
2213
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
2214
|
+
}
|
|
2212
2215
|
function bakeSiteUuid(cwd, guardRelPath) {
|
|
2213
2216
|
const rc = join2(cwd, ".patchstackrc.json");
|
|
2214
2217
|
if (!existsSync2(rc)) {
|
|
@@ -3487,6 +3490,121 @@ var expressAdapter = {
|
|
|
3487
3490
|
verify: (cwd) => verifyRegister(cwd, SPEC6)
|
|
3488
3491
|
};
|
|
3489
3492
|
|
|
3493
|
+
// src/protect/install/reporting.ts
|
|
3494
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3495
|
+
import { join as join12 } from "path";
|
|
3496
|
+
|
|
3497
|
+
// src/protect/reporting-state.js
|
|
3498
|
+
var REPORTING_STATES = Object.freeze([
|
|
3499
|
+
/** Enrolled, managed rules, credential present, not opted out. Events are sent. */
|
|
3500
|
+
"on",
|
|
3501
|
+
/** `PATCHSTACK_REPORT_DETECTIONS=0`. An explicit, per-deployment opt-out. */
|
|
3502
|
+
"disabled-by-config",
|
|
3503
|
+
/** `PATCHSTACK_TELEMETRY=0`. The broader switch, which covers this along with everything else. */
|
|
3504
|
+
"disabled-by-telemetry-opt-out",
|
|
3505
|
+
/** No site identity: a local or unenrolled install. Nothing to report against. */
|
|
3506
|
+
"not-enrolled",
|
|
3507
|
+
/**
|
|
3508
|
+
* A site identity, but the rules running are not the platform's — the caller's own bundle, or none.
|
|
3509
|
+
* There is no managed rule document to attribute a detection to, so a report would name a rule id the
|
|
3510
|
+
* platform never issued.
|
|
3511
|
+
*/
|
|
3512
|
+
"no-managed-rules",
|
|
3513
|
+
/** Enrolled with managed rules, but no credential resolved, so a report would be refused. */
|
|
3514
|
+
"unavailable-no-credential"
|
|
3515
|
+
]);
|
|
3516
|
+
function optedOut(value) {
|
|
3517
|
+
if (value === void 0 || value === null || value === "") return false;
|
|
3518
|
+
return /^(0|false|off|no)$/i.test(String(value));
|
|
3519
|
+
}
|
|
3520
|
+
function reportingState(input) {
|
|
3521
|
+
const env = input.env ?? (typeof process !== "undefined" ? process.env : void 0) ?? {};
|
|
3522
|
+
if (input.configOptOut === true) return { state: "disabled-by-config", reports: false };
|
|
3523
|
+
if (optedOut(env.PATCHSTACK_REPORT_DETECTIONS)) return { state: "disabled-by-config", reports: false };
|
|
3524
|
+
if (optedOut(env.PATCHSTACK_TELEMETRY)) return { state: "disabled-by-telemetry-opt-out", reports: false };
|
|
3525
|
+
const siteUuid = input.siteUuid;
|
|
3526
|
+
if (typeof siteUuid !== "string" || siteUuid === "") return { state: "not-enrolled", reports: false };
|
|
3527
|
+
if (input.hasCredential !== true) return { state: "unavailable-no-credential", reports: false };
|
|
3528
|
+
const managed = input.ruleOrigin === "api" || input.ruleOrigin === "cache";
|
|
3529
|
+
if (!managed) return { state: "no-managed-rules", reports: false };
|
|
3530
|
+
return { state: "on", reports: true };
|
|
3531
|
+
}
|
|
3532
|
+
function explainReportingState(state) {
|
|
3533
|
+
switch (state) {
|
|
3534
|
+
case "on":
|
|
3535
|
+
return "Security events are reported to Patchstack for this site.";
|
|
3536
|
+
case "disabled-by-config":
|
|
3537
|
+
return "Reporting is off because PATCHSTACK_REPORT_DETECTIONS is set to a false value.";
|
|
3538
|
+
case "disabled-by-telemetry-opt-out":
|
|
3539
|
+
return "Reporting is off because PATCHSTACK_TELEMETRY is set to a false value, which covers all telemetry.";
|
|
3540
|
+
case "not-enrolled":
|
|
3541
|
+
return "Reporting is off because this install has no site identity \u2014 it is not enrolled in Patchstack-managed mitigation.";
|
|
3542
|
+
case "no-managed-rules":
|
|
3543
|
+
return "Reporting is off because the rules in force did not come from Patchstack, so a detection could not be attributed to a managed rule.";
|
|
3544
|
+
case "unavailable-no-credential":
|
|
3545
|
+
return "Reporting is unavailable because no API credential resolved, so a report would be refused.";
|
|
3546
|
+
default:
|
|
3547
|
+
return `Unrecognised reporting state: ${String(state)}.`;
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
// src/protect/install/reporting.ts
|
|
3552
|
+
function siteIdentity(cwd, env) {
|
|
3553
|
+
const rc = join12(cwd, ".patchstackrc.json");
|
|
3554
|
+
let recorded;
|
|
3555
|
+
if (existsSync10(rc)) {
|
|
3556
|
+
try {
|
|
3557
|
+
recorded = JSON.parse(read(rc)).siteUuid;
|
|
3558
|
+
} catch {
|
|
3559
|
+
recorded = void 0;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (isSiteUuid(recorded)) return { uuid: recorded, found: true };
|
|
3563
|
+
const fromEnv = env.PATCHSTACK_SITE_UUID;
|
|
3564
|
+
if (isSiteUuid(fromEnv)) return { uuid: fromEnv, found: true };
|
|
3565
|
+
const anything = typeof recorded === "string" && recorded !== "" || (fromEnv ?? "") !== "";
|
|
3566
|
+
return { uuid: null, found: anything };
|
|
3567
|
+
}
|
|
3568
|
+
function reportingChecks(cwd, env = process.env) {
|
|
3569
|
+
const { uuid: siteUuid, found: identityFound } = siteIdentity(cwd, env);
|
|
3570
|
+
const hasCredential = hasResolvableCredential(cwd, env);
|
|
3571
|
+
const { state } = reportingState({ siteUuid, hasCredential, ruleOrigin: "api", env });
|
|
3572
|
+
const checks = [
|
|
3573
|
+
{
|
|
3574
|
+
group: "reporting",
|
|
3575
|
+
label: "this app is enrolled (a site identity is recorded)",
|
|
3576
|
+
ok: siteUuid !== null,
|
|
3577
|
+
hint: identityFound ? "a site identity is recorded but is not a usable UUID \u2014 check `siteUuid` in .patchstackrc.json, or PATCHSTACK_SITE_UUID. The guard protects the app and reports nothing, because there is nothing to attribute a detection to." : "no site identity \u2014 run `npx @patchstack/connect scan` to provision one, or set PATCHSTACK_SITE_UUID in the deployment. Without it the guard protects the app but reports nothing."
|
|
3578
|
+
}
|
|
3579
|
+
];
|
|
3580
|
+
if (state === "on") {
|
|
3581
|
+
checks.push({
|
|
3582
|
+
group: "reporting",
|
|
3583
|
+
label: "detection reporting is switched on for this environment",
|
|
3584
|
+
ok: true,
|
|
3585
|
+
unverifiable: true,
|
|
3586
|
+
hint: "nothing here switches it off, but whether it actually reports depends on the rules the guard resolves at runtime: reporting is on for Patchstack-delivered rules and off for a bundle of your own (`no-managed-rules`). Read `protection.detectionReporting` in the running app for the settled answer. This also only judges the environment it ran in \u2014 a deployment may set an opt-out this machine cannot see."
|
|
3587
|
+
});
|
|
3588
|
+
} else {
|
|
3589
|
+
checks.push({
|
|
3590
|
+
group: "reporting",
|
|
3591
|
+
label: `detection reporting is off for this environment (${state})`,
|
|
3592
|
+
ok: state === "disabled-by-config" || state === "disabled-by-telemetry-opt-out",
|
|
3593
|
+
hint: `${explainReportingState(state)} Judged from this environment only; a deployment's is not this one.`
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
checks.push({
|
|
3597
|
+
group: "reporting",
|
|
3598
|
+
// A wired guard says events CAN be sent. It says nothing about whether any were, or whether the
|
|
3599
|
+
// platform took them, and this is where an operator would otherwise assume both.
|
|
3600
|
+
label: "delivery health (events attempted, acknowledged, refused, dropped)",
|
|
3601
|
+
ok: true,
|
|
3602
|
+
unverifiable: true,
|
|
3603
|
+
hint: "not observable from here \u2014 the counters live in the running guard. Call `protection.detectionHealth()` in the deployment: it is undefined when there is no reporter, and otherwise reports what was attempted, acknowledged, refused and dropped for queue pressure. A wired guard is not evidence that anything arrived."
|
|
3604
|
+
});
|
|
3605
|
+
return checks;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3490
3608
|
// src/protect/install/index.ts
|
|
3491
3609
|
var ADAPTERS = [
|
|
3492
3610
|
tanstackSupabaseAdapter,
|
|
@@ -3537,17 +3655,25 @@ function runVerify(cwd) {
|
|
|
3537
3655
|
const adapter = ADAPTERS.find((a) => a.detect(cwd));
|
|
3538
3656
|
if (adapter) {
|
|
3539
3657
|
const result = adapter.verify(cwd);
|
|
3540
|
-
return {
|
|
3658
|
+
return {
|
|
3659
|
+
stack: adapter.label,
|
|
3660
|
+
...result,
|
|
3661
|
+
checks: [...result.checks, ...credentialNote(cwd, adapter.name), ...reportingChecks(cwd)]
|
|
3662
|
+
};
|
|
3541
3663
|
}
|
|
3542
3664
|
const generic = genericVerify(cwd);
|
|
3543
|
-
return {
|
|
3665
|
+
return {
|
|
3666
|
+
stack: "generic",
|
|
3667
|
+
...generic,
|
|
3668
|
+
checks: [...generic.checks, ...credentialNote(cwd, "generic"), ...reportingChecks(cwd)]
|
|
3669
|
+
};
|
|
3544
3670
|
} catch (err) {
|
|
3545
3671
|
return { stack: "unknown", wired: false, checks: [{ label: "verification failed", ok: false, hint: String(err?.message ?? err) }] };
|
|
3546
3672
|
}
|
|
3547
3673
|
}
|
|
3548
3674
|
|
|
3549
3675
|
// src/widget.ts
|
|
3550
|
-
import { existsSync as
|
|
3676
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
3551
3677
|
import path8 from "path";
|
|
3552
3678
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
3553
3679
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
@@ -3586,7 +3712,7 @@ ${indent}</body>`;
|
|
|
3586
3712
|
}
|
|
3587
3713
|
function findSourceShell(cwd) {
|
|
3588
3714
|
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
3589
|
-
if (
|
|
3715
|
+
if (existsSync11(path8.join(cwd, candidate))) {
|
|
3590
3716
|
return candidate;
|
|
3591
3717
|
}
|
|
3592
3718
|
}
|
|
@@ -3680,7 +3806,7 @@ function detectPackageManager(cwd) {
|
|
|
3680
3806
|
return declared;
|
|
3681
3807
|
}
|
|
3682
3808
|
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
3683
|
-
if (
|
|
3809
|
+
if (existsSync12(path9.join(cwd, filename))) {
|
|
3684
3810
|
return pm;
|
|
3685
3811
|
}
|
|
3686
3812
|
}
|
|
@@ -3777,7 +3903,7 @@ function resolveWidgetFileHint(cwd, framework) {
|
|
|
3777
3903
|
...GENERIC_WIDGET_FILES
|
|
3778
3904
|
];
|
|
3779
3905
|
for (const candidate of candidates) {
|
|
3780
|
-
if (
|
|
3906
|
+
if (existsSync12(path9.join(cwd, candidate))) {
|
|
3781
3907
|
return candidate;
|
|
3782
3908
|
}
|
|
3783
3909
|
}
|
|
@@ -3861,6 +3987,9 @@ function needsSourceProductionMarker(state) {
|
|
|
3861
3987
|
}
|
|
3862
3988
|
return !state.widgetFileHint.toLowerCase().endsWith(".html");
|
|
3863
3989
|
}
|
|
3990
|
+
function widgetTagInPlace(state) {
|
|
3991
|
+
return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
3992
|
+
}
|
|
3864
3993
|
function countRemainingSteps(state) {
|
|
3865
3994
|
return [
|
|
3866
3995
|
state.installed?.section === "dependencies",
|
|
@@ -4007,6 +4136,19 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4007
4136
|
} else {
|
|
4008
4137
|
lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
|
|
4009
4138
|
}
|
|
4139
|
+
if (widgetTagInPlace(state)) {
|
|
4140
|
+
lines.push("");
|
|
4141
|
+
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Refresh the preview to see the widget:")}`);
|
|
4142
|
+
lines.push(' The "Report a vulnerability" button loads with the page, so a preview that was');
|
|
4143
|
+
lines.push(" already open still shows the HTML from before this change. Builders that hot");
|
|
4144
|
+
lines.push(" reload refresh it themselves; if the button is missing, refresh the preview once.");
|
|
4145
|
+
}
|
|
4146
|
+
if (state.siteUuid !== null) {
|
|
4147
|
+
lines.push("");
|
|
4148
|
+
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Deploy to put this on your live site:")}`);
|
|
4149
|
+
lines.push(" These are source changes. Your deployed site keeps serving its previous build,");
|
|
4150
|
+
lines.push(" so visitors only get the widget after you deploy (or hit Publish) again.");
|
|
4151
|
+
}
|
|
4010
4152
|
const remaining = countRemainingSteps(state);
|
|
4011
4153
|
lines.push("");
|
|
4012
4154
|
if (remaining === 0) {
|
|
@@ -4160,10 +4302,10 @@ import { writeFileSync as writeFileSync8 } from "fs";
|
|
|
4160
4302
|
// src/map/ts-loader.ts
|
|
4161
4303
|
import { createRequire } from "module";
|
|
4162
4304
|
import { pathToFileURL } from "url";
|
|
4163
|
-
import { join as
|
|
4305
|
+
import { join as join13 } from "path";
|
|
4164
4306
|
async function loadTypeScript(cwd) {
|
|
4165
4307
|
try {
|
|
4166
|
-
const req = createRequire(pathToFileURL(
|
|
4308
|
+
const req = createRequire(pathToFileURL(join13(cwd, "package.json")));
|
|
4167
4309
|
const resolved = req.resolve("typescript");
|
|
4168
4310
|
const mod = await import(pathToFileURL(resolved).href);
|
|
4169
4311
|
return mod.default ?? mod;
|
|
@@ -4435,7 +4577,7 @@ function npmPackageOf(spec) {
|
|
|
4435
4577
|
|
|
4436
4578
|
// src/map/sources.ts
|
|
4437
4579
|
import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
4438
|
-
import { join as
|
|
4580
|
+
import { join as join14, relative as relative2, isAbsolute } from "path";
|
|
4439
4581
|
|
|
4440
4582
|
// src/map/routes.ts
|
|
4441
4583
|
var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
|
|
@@ -4519,7 +4661,7 @@ function hasEntrySignal(text) {
|
|
|
4519
4661
|
}
|
|
4520
4662
|
function detectFramework(cwd) {
|
|
4521
4663
|
try {
|
|
4522
|
-
const pkg = JSON.parse(readFileSync8(
|
|
4664
|
+
const pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
|
|
4523
4665
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4524
4666
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
4525
4667
|
if (d["next"]) return "next";
|
|
@@ -4531,11 +4673,11 @@ function detectFramework(cwd) {
|
|
|
4531
4673
|
} catch {
|
|
4532
4674
|
}
|
|
4533
4675
|
try {
|
|
4534
|
-
if (statSync3(
|
|
4676
|
+
if (statSync3(join14(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
|
|
4535
4677
|
} catch {
|
|
4536
4678
|
}
|
|
4537
4679
|
try {
|
|
4538
|
-
if (statSync3(
|
|
4680
|
+
if (statSync3(join14(cwd, "functions")).isDirectory()) return "deno-functions";
|
|
4539
4681
|
} catch {
|
|
4540
4682
|
}
|
|
4541
4683
|
return "unknown";
|
|
@@ -4577,7 +4719,7 @@ function probeForSource(dir, budget) {
|
|
|
4577
4719
|
for (const e of entries) {
|
|
4578
4720
|
if (budget.left-- <= 0) return "unknown";
|
|
4579
4721
|
if (e.isDirectory()) {
|
|
4580
|
-
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(
|
|
4722
|
+
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join14(dir, e.name));
|
|
4581
4723
|
} else if (isSourceFile(e.name)) return true;
|
|
4582
4724
|
else if (e.isSymbolicLink()) return "unknown";
|
|
4583
4725
|
}
|
|
@@ -4614,13 +4756,13 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
4614
4756
|
const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
|
|
4615
4757
|
if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
|
|
4616
4758
|
else if (probeable && e.isDirectory()) {
|
|
4617
|
-
const held = probeForSource(
|
|
4618
|
-
if (held === true) stats.skippedWithSource.push(
|
|
4759
|
+
const held = probeForSource(join14(dir, e.name), probeBudget);
|
|
4760
|
+
if (held === true) stats.skippedWithSource.push(join14(dir, e.name));
|
|
4619
4761
|
else if (held === "unknown") stats.skippedUnknown++;
|
|
4620
4762
|
}
|
|
4621
4763
|
continue;
|
|
4622
4764
|
}
|
|
4623
|
-
const full =
|
|
4765
|
+
const full = join14(dir, e.name);
|
|
4624
4766
|
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
4625
4767
|
else if (e.isSymbolicLink()) {
|
|
4626
4768
|
let st, real;
|
|
@@ -4690,7 +4832,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
4690
4832
|
const found = [];
|
|
4691
4833
|
for (const candidate of DEPLOYMENT_SHAPES) {
|
|
4692
4834
|
for (const file of candidate.files ?? []) {
|
|
4693
|
-
const full =
|
|
4835
|
+
const full = join14(cwd, file);
|
|
4694
4836
|
try {
|
|
4695
4837
|
if (statSync3(full).isFile() && inProject(full)) {
|
|
4696
4838
|
found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
|
|
@@ -4700,7 +4842,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
4700
4842
|
}
|
|
4701
4843
|
}
|
|
4702
4844
|
for (const dir of candidate.dirs ?? []) {
|
|
4703
|
-
const full =
|
|
4845
|
+
const full = join14(cwd, dir);
|
|
4704
4846
|
try {
|
|
4705
4847
|
if (!statSync3(full).isDirectory() || !inProject(full)) continue;
|
|
4706
4848
|
} catch {
|
|
@@ -4719,7 +4861,7 @@ function holdsFunctionEntry(dir) {
|
|
|
4719
4861
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
4720
4862
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
4721
4863
|
try {
|
|
4722
|
-
for (const nested of readdirSync5(
|
|
4864
|
+
for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
|
|
4723
4865
|
if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
|
|
4724
4866
|
}
|
|
4725
4867
|
} catch {
|
|
@@ -4735,7 +4877,7 @@ function holdsSourceFile(dir) {
|
|
|
4735
4877
|
if (entry.isFile() && isSourceFile(entry.name)) return true;
|
|
4736
4878
|
if (entry.isDirectory()) {
|
|
4737
4879
|
try {
|
|
4738
|
-
for (const nested of readdirSync5(
|
|
4880
|
+
for (const nested of readdirSync5(join14(dir, entry.name), { withFileTypes: true })) {
|
|
4739
4881
|
if (nested.isFile() && isSourceFile(nested.name)) return true;
|
|
4740
4882
|
}
|
|
4741
4883
|
} catch {
|
|
@@ -4749,7 +4891,7 @@ function holdsSourceFile(dir) {
|
|
|
4749
4891
|
|
|
4750
4892
|
// src/map/surface.ts
|
|
4751
4893
|
import { readFileSync as readFileSync9 } from "fs";
|
|
4752
|
-
import { join as
|
|
4894
|
+
import { join as join15 } from "path";
|
|
4753
4895
|
var STATIC_GENERATORS = [
|
|
4754
4896
|
{ dep: "vite", label: "vite" },
|
|
4755
4897
|
{ dep: "react-scripts", label: "create-react-app" },
|
|
@@ -4787,7 +4929,7 @@ var SHAPE_SIGNAL = {
|
|
|
4787
4929
|
};
|
|
4788
4930
|
function readManifest(cwd) {
|
|
4789
4931
|
try {
|
|
4790
|
-
const parsed = JSON.parse(readFileSync9(
|
|
4932
|
+
const parsed = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
|
|
4791
4933
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
4792
4934
|
} catch {
|
|
4793
4935
|
return null;
|
|
@@ -4800,7 +4942,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
4800
4942
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
4801
4943
|
let text;
|
|
4802
4944
|
try {
|
|
4803
|
-
text = readFileSync9(
|
|
4945
|
+
text = readFileSync9(join15(cwd, file), "utf8");
|
|
4804
4946
|
} catch {
|
|
4805
4947
|
continue;
|
|
4806
4948
|
}
|
|
@@ -5212,7 +5354,7 @@ function dedupeSinks(sinks, owner) {
|
|
|
5212
5354
|
|
|
5213
5355
|
// src/map/module-graph.ts
|
|
5214
5356
|
import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
5215
|
-
import { dirname as dirname7, join as
|
|
5357
|
+
import { dirname as dirname7, join as join16, relative as relative3, resolve as resolvePath } from "path";
|
|
5216
5358
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
5217
5359
|
function createModuleGraph(ts, opts) {
|
|
5218
5360
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -5290,7 +5432,7 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
5290
5432
|
}
|
|
5291
5433
|
candidates.push(base);
|
|
5292
5434
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
5293
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
5435
|
+
for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
|
|
5294
5436
|
for (const c of candidates) {
|
|
5295
5437
|
try {
|
|
5296
5438
|
if (statSync4(c).isFile()) return c;
|
|
@@ -5974,7 +6116,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
5974
6116
|
|
|
5975
6117
|
// src/map/imports.ts
|
|
5976
6118
|
import { readFileSync as readFileSync11 } from "fs";
|
|
5977
|
-
import { join as
|
|
6119
|
+
import { join as join17 } from "path";
|
|
5978
6120
|
var MAX_SITES = 5;
|
|
5979
6121
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
5980
6122
|
function isNpmPackageName(pkg) {
|
|
@@ -5985,7 +6127,7 @@ function readPathAliases(cwd) {
|
|
|
5985
6127
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
5986
6128
|
let paths;
|
|
5987
6129
|
try {
|
|
5988
|
-
const raw = readFileSync11(
|
|
6130
|
+
const raw = readFileSync11(join17(cwd, name), "utf8");
|
|
5989
6131
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
5990
6132
|
paths = parsed.compilerOptions?.paths;
|
|
5991
6133
|
} catch {
|
|
@@ -7213,12 +7355,18 @@ async function runProtectCommand(args) {
|
|
|
7213
7355
|
if (args.flags.get("check") === true) {
|
|
7214
7356
|
const report = runVerify(process.cwd());
|
|
7215
7357
|
console.log(`patchstack protect --check (${report.stack}):`);
|
|
7216
|
-
|
|
7358
|
+
const line = (c) => {
|
|
7217
7359
|
const mark = c.unverifiable ? "?" : c.ok ? "\u2713" : "\u2717";
|
|
7218
7360
|
const note = c.unverifiable || !c.ok ? c.hint : void 0;
|
|
7219
7361
|
console.log(` ${mark} ${c.label}${note ? ` \u2014 ${note}` : ""}`);
|
|
7220
|
-
}
|
|
7362
|
+
};
|
|
7363
|
+
for (const c of report.checks.filter((c2) => c2.group !== "reporting")) line(c);
|
|
7221
7364
|
console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
|
|
7365
|
+
const reporting = report.checks.filter((c) => c.group === "reporting");
|
|
7366
|
+
if (reporting.length > 0) {
|
|
7367
|
+
console.log("reporting to Patchstack (does not affect the verdict above):");
|
|
7368
|
+
for (const c of reporting) line(c);
|
|
7369
|
+
}
|
|
7222
7370
|
if (report.checks.some((c) => c.unverifiable)) {
|
|
7223
7371
|
console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
|
|
7224
7372
|
}
|
|
@@ -7397,6 +7545,12 @@ Run: ${installCommand(before.packageManager)}`
|
|
|
7397
7545
|
console.log("");
|
|
7398
7546
|
console.log(`Setup applied its bounded changes; ${remaining} manual step(s) remain above.`);
|
|
7399
7547
|
}
|
|
7548
|
+
console.log("");
|
|
7549
|
+
console.log("Tell the user:");
|
|
7550
|
+
if (widgetTagInPlace(after)) {
|
|
7551
|
+
console.log(' - refresh the preview if the "Report a vulnerability" button is not showing yet;');
|
|
7552
|
+
}
|
|
7553
|
+
console.log(" - deploy (or hit Publish) when ready, so the live site serves these changes.");
|
|
7400
7554
|
return 0;
|
|
7401
7555
|
}
|
|
7402
7556
|
async function runStatus(args) {
|