@patchstack/connect 0.3.26 → 0.3.27
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 +21 -5
- package/dist/cli.js +192 -74
- package/dist/cli.js.map +1 -1
- package/dist/protect.cjs +33 -1
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.edge.js +33 -1
- package/dist/protect.edge.js.map +2 -2
- package/dist/protect.js +33 -1
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -139,14 +139,29 @@ npx @patchstack/connect login
|
|
|
139
139
|
|
|
140
140
|
The command asks Patchstack for a short code, prints a link, and polls until the site's **owner approves it in the dashboard**. On approval it writes the new credential into `.patchstackrc.json` and exits. The link opens the approval page with the code already filled in, so the person only has to confirm.
|
|
141
141
|
|
|
142
|
-
### What you must do, as the agent
|
|
142
|
+
### What you must do, as the agent — two commands, not one
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
**The command exits immediately when you run it.** It detects that its output is being captured rather than watched by a person, prints the link, and returns. It does **not** block waiting for approval, because you would not see the link until it exited — by which time the code would have expired, and it would look like the command had hung.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
1. npx @patchstack/connect login → prints the link, exits straight away
|
|
148
|
+
2. give the user the link, verbatim → they approve it in the browser
|
|
149
|
+
3. npx @patchstack/connect login → the SAME command again, after they confirm.
|
|
150
|
+
It resumes the request and finishes the flow
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **Never wrap step 1 in a timeout or kill it** — it returns on its own. If you find yourself waiting on it, something else is wrong.
|
|
154
|
+
- **Step 3 is the same command.** While a request is still valid it resumes rather than restarting, so running `login` again never invalidates the link the user is looking at. If they have not approved yet it tells you so, with the time remaining, and exits.
|
|
155
|
+
- **Nothing changes until step 3 runs.** Approving only marks the request; the credential is rotated and written when the CLI redeems it. So an abandoned flow is harmless — the site keeps working — but the credential is not restored until you come back.
|
|
156
|
+
- **Surface the link verbatim.** Approval requires the user's signed-in Patchstack account, which you do not have and must never ask for.
|
|
157
|
+
- **Report the outcome.** On success, say the credential was restored *and* that the previous one no longer works — see the warning below.
|
|
158
|
+
|
|
159
|
+
`login --wait` is the blocking variant: it polls until approved instead of returning. Prefer the plain re-run — it keeps each command short, which is what fits a conversation.
|
|
147
160
|
|
|
148
161
|
You cannot complete this alone. It is deliberately a human-in-the-loop step: starting the flow proves nothing about who is running it, so the only authorisation is an owner approving in the browser.
|
|
149
162
|
|
|
163
|
+
(In an interactive terminal the same command prints the link and then waits, since a person can watch it stream. You get the two-step form; a human at a shell gets the one-step form.)
|
|
164
|
+
|
|
150
165
|
### Consequences to tell the user about
|
|
151
166
|
|
|
152
167
|
**Approving rotates the credential — the old one stops working immediately.** Anywhere it was configured needs the new value: CI secrets, hosting-platform env vars, preview environments, other developers' checkouts. Say this before they approve, not after.
|
|
@@ -158,7 +173,8 @@ You cannot complete this alone. It is deliberately a human-in-the-loop step: sta
|
|
|
158
173
|
| Site was never claimed | `409` — no owner exists to approve | Ask the user to claim the site in the dashboard first, or, if the site is disposable, delete `.patchstackrc.json` and `scan` to provision a fresh one |
|
|
159
174
|
| Running in CI | Refuses to start | CI takes its credential from `PATCHSTACK_PULSE_AUTH`; `login` is for a developer machine |
|
|
160
175
|
| No `siteUuid` configured | Refuses to start | There is no site to recover — run `scan` |
|
|
161
|
-
| Code expired |
|
|
176
|
+
| Code expired | `--wait` ends after 10 minutes | Start again from step 1 for a new code |
|
|
177
|
+
| `--wait` with nothing pending | "No login is waiting for approval" | Run step 1 first; `--wait` resumes a request, it does not start one |
|
|
162
178
|
|
|
163
179
|
## Uninstalling
|
|
164
180
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/parsers/index.ts
|
|
7
7
|
import { access } from "fs/promises";
|
|
@@ -931,8 +931,8 @@ function computeManifestChecksum(packages) {
|
|
|
931
931
|
var TOKEN_SKEW_MS = 6e4;
|
|
932
932
|
function buildTokenUrl(manifestEndpoint) {
|
|
933
933
|
const url = new URL(manifestEndpoint);
|
|
934
|
-
const
|
|
935
|
-
url.pathname =
|
|
934
|
+
const path12 = url.pathname.replace(/\/$/, "");
|
|
935
|
+
url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
|
|
936
936
|
url.search = "";
|
|
937
937
|
url.hash = "";
|
|
938
938
|
return url.toString();
|
|
@@ -1018,8 +1018,8 @@ function buildEndpointUrl(base, siteUuid) {
|
|
|
1018
1018
|
}
|
|
1019
1019
|
function buildRulesUrl(manifestEndpoint, siteUuid) {
|
|
1020
1020
|
const url = new URL(manifestEndpoint);
|
|
1021
|
-
const
|
|
1022
|
-
url.pathname =
|
|
1021
|
+
const path12 = url.pathname.replace(/\/$/, "");
|
|
1022
|
+
url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
|
|
1023
1023
|
url.search = "";
|
|
1024
1024
|
url.hash = "";
|
|
1025
1025
|
return url.toString();
|
|
@@ -1034,16 +1034,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
|
|
|
1034
1034
|
}
|
|
1035
1035
|
function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
|
|
1036
1036
|
const url = new URL(manifestEndpoint);
|
|
1037
|
-
const
|
|
1038
|
-
url.pathname =
|
|
1037
|
+
const path12 = url.pathname.replace(/\/$/, "");
|
|
1038
|
+
url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
|
|
1039
1039
|
url.search = "";
|
|
1040
1040
|
url.hash = "";
|
|
1041
1041
|
return url.toString();
|
|
1042
1042
|
}
|
|
1043
1043
|
function buildInputMapUrl(manifestEndpoint, siteUuid) {
|
|
1044
1044
|
const url = new URL(manifestEndpoint);
|
|
1045
|
-
const
|
|
1046
|
-
url.pathname =
|
|
1045
|
+
const path12 = url.pathname.replace(/\/$/, "");
|
|
1046
|
+
url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
|
|
1047
1047
|
url.search = "";
|
|
1048
1048
|
url.hash = "";
|
|
1049
1049
|
return url.toString();
|
|
@@ -2949,17 +2949,40 @@ function renderGuideChecklist(state, useColor) {
|
|
|
2949
2949
|
}
|
|
2950
2950
|
|
|
2951
2951
|
// src/login.ts
|
|
2952
|
+
import { mkdtempSync, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync6 } from "fs";
|
|
2953
|
+
import { tmpdir } from "os";
|
|
2954
|
+
import path10 from "path";
|
|
2955
|
+
import { createHash as createHash2 } from "crypto";
|
|
2952
2956
|
function baseFrom(manifestEndpoint) {
|
|
2953
2957
|
const url = new URL(manifestEndpoint);
|
|
2954
|
-
const
|
|
2955
|
-
url.pathname =
|
|
2958
|
+
const p = url.pathname.replace(/\/$/, "");
|
|
2959
|
+
url.pathname = p.endsWith("/manifest") ? p.slice(0, -"/manifest".length) : "/monitor/pulse";
|
|
2956
2960
|
url.search = "";
|
|
2957
2961
|
url.hash = "";
|
|
2958
2962
|
return url.toString().replace(/\/$/, "");
|
|
2959
2963
|
}
|
|
2960
|
-
|
|
2964
|
+
function pendingPath(siteUuid) {
|
|
2965
|
+
const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
|
|
2966
|
+
return path10.join(tmpdir(), `patchstack-login-${key}.json`);
|
|
2967
|
+
}
|
|
2968
|
+
function savePendingLogin(siteUuid, pending) {
|
|
2969
|
+
writeFileSync6(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
|
|
2970
|
+
}
|
|
2971
|
+
function readPendingLogin(siteUuid) {
|
|
2972
|
+
try {
|
|
2973
|
+
return JSON.parse(readFileSync6(pendingPath(siteUuid), "utf8"));
|
|
2974
|
+
} catch {
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
function clearPendingLogin(siteUuid) {
|
|
2979
|
+
try {
|
|
2980
|
+
rmSync(pendingPath(siteUuid));
|
|
2981
|
+
} catch {
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
async function startLogin(config, deps = {}) {
|
|
2961
2985
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
2962
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2963
2986
|
const now = deps.now ?? (() => Date.now());
|
|
2964
2987
|
if (config.siteUuid === null) {
|
|
2965
2988
|
return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
|
|
@@ -2982,28 +3005,72 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
2982
3005
|
if (!started.ok) {
|
|
2983
3006
|
return { status: "failed", message: `Could not start the login (HTTP ${started.status}).` };
|
|
2984
3007
|
}
|
|
2985
|
-
const
|
|
2986
|
-
const
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3008
|
+
const body = await started.json();
|
|
3009
|
+
const pending = {
|
|
3010
|
+
deviceCode: body.device_code,
|
|
3011
|
+
userCode: body.user_code,
|
|
3012
|
+
// Points at the API, which redirects to the dashboard SPA — the CLI only
|
|
3013
|
+
// knows the API origin, and the approval page lives on the app.
|
|
3014
|
+
verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
|
|
3015
|
+
expiresAt: now() + body.expires_in * 1e3,
|
|
3016
|
+
intervalMs: Math.max(1, body.interval) * 1e3
|
|
3017
|
+
};
|
|
3018
|
+
savePendingLogin(config.siteUuid, pending);
|
|
3019
|
+
return { status: "started", pending };
|
|
3020
|
+
}
|
|
3021
|
+
async function redeemIfApproved(config, pending, deps = {}) {
|
|
3022
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
3023
|
+
const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
|
|
3024
|
+
method: "POST",
|
|
3025
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
3026
|
+
body: JSON.stringify({ device_code: pending.deviceCode })
|
|
3027
|
+
});
|
|
3028
|
+
if (polled.status === 428) return "pending";
|
|
3029
|
+
if (!polled.ok) {
|
|
3030
|
+
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
3031
|
+
return "expired";
|
|
3032
|
+
}
|
|
3033
|
+
const { api_key: apiKey } = await polled.json();
|
|
3034
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
|
|
3035
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
3036
|
+
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
3037
|
+
return "approved";
|
|
3038
|
+
}
|
|
3039
|
+
async function waitForApproval(config, pending, deps = {}) {
|
|
3040
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
3041
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3042
|
+
const now = deps.now ?? (() => Date.now());
|
|
3043
|
+
const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
|
|
3044
|
+
const expired = { status: "expired", message: "The login request expired. Run the command again." };
|
|
2990
3045
|
while (now() < deadline) {
|
|
2991
|
-
await sleep(intervalMs);
|
|
2992
|
-
const polled = await fetchImpl(`${
|
|
3046
|
+
await sleep(pending.intervalMs);
|
|
3047
|
+
const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
|
|
2993
3048
|
method: "POST",
|
|
2994
3049
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
2995
|
-
body: JSON.stringify({ device_code: deviceCode })
|
|
3050
|
+
body: JSON.stringify({ device_code: pending.deviceCode })
|
|
2996
3051
|
});
|
|
2997
3052
|
if (polled.status === 428) continue;
|
|
2998
|
-
if (!polled.ok)
|
|
3053
|
+
if (!polled.ok) {
|
|
3054
|
+
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
3055
|
+
return expired;
|
|
3056
|
+
}
|
|
2999
3057
|
const { api_key: apiKey } = await polled.json();
|
|
3000
3058
|
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
3001
3059
|
return { status: "failed", message: "Patchstack approved the request but returned no credential." };
|
|
3002
3060
|
}
|
|
3003
3061
|
await persistApiKey(process.cwd(), apiKey);
|
|
3004
|
-
|
|
3062
|
+
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
3063
|
+
return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
|
|
3005
3064
|
}
|
|
3006
|
-
return
|
|
3065
|
+
return expired;
|
|
3066
|
+
}
|
|
3067
|
+
async function login(config, onPrompt, deps = {}) {
|
|
3068
|
+
const started = await startLogin(config, deps);
|
|
3069
|
+
if (started.status !== "started" || started.pending === void 0) {
|
|
3070
|
+
return { status: started.status === "started" ? "failed" : started.status, message: started.message };
|
|
3071
|
+
}
|
|
3072
|
+
onPrompt(started.pending.userCode, started.pending.verificationUri);
|
|
3073
|
+
return waitForApproval(config, started.pending, deps);
|
|
3007
3074
|
}
|
|
3008
3075
|
|
|
3009
3076
|
// src/map/ts-loader.ts
|
|
@@ -3027,8 +3094,8 @@ async function loadTypeScript(cwd) {
|
|
|
3027
3094
|
}
|
|
3028
3095
|
|
|
3029
3096
|
// src/map/extract.ts
|
|
3030
|
-
import { readFileSync as
|
|
3031
|
-
import { createHash as
|
|
3097
|
+
import { readFileSync as readFileSync11, realpathSync as realpathSync3 } from "fs";
|
|
3098
|
+
import { createHash as createHash4 } from "crypto";
|
|
3032
3099
|
import { relative as relative4 } from "path";
|
|
3033
3100
|
|
|
3034
3101
|
// src/map/ast.ts
|
|
@@ -3283,7 +3350,7 @@ function npmPackageOf(spec) {
|
|
|
3283
3350
|
}
|
|
3284
3351
|
|
|
3285
3352
|
// src/map/sources.ts
|
|
3286
|
-
import { readFileSync as
|
|
3353
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
3287
3354
|
import { join as join12, relative as relative2, isAbsolute } from "path";
|
|
3288
3355
|
|
|
3289
3356
|
// src/map/routes.ts
|
|
@@ -3368,7 +3435,7 @@ function hasEntrySignal(text) {
|
|
|
3368
3435
|
}
|
|
3369
3436
|
function detectFramework(cwd) {
|
|
3370
3437
|
try {
|
|
3371
|
-
const pkg = JSON.parse(
|
|
3438
|
+
const pkg = JSON.parse(readFileSync7(join12(cwd, "package.json"), "utf8"));
|
|
3372
3439
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3373
3440
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
3374
3441
|
if (d["next"]) return "next";
|
|
@@ -3496,10 +3563,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
3496
3563
|
boundary = realpathSync(boundary);
|
|
3497
3564
|
} catch {
|
|
3498
3565
|
}
|
|
3499
|
-
const inProject = (
|
|
3566
|
+
const inProject = (path12) => {
|
|
3500
3567
|
if (opts.followOutside) return true;
|
|
3501
3568
|
try {
|
|
3502
|
-
return isInside(realpathSync(
|
|
3569
|
+
return isInside(realpathSync(path12), boundary);
|
|
3503
3570
|
} catch {
|
|
3504
3571
|
return false;
|
|
3505
3572
|
}
|
|
@@ -3565,7 +3632,7 @@ function holdsSourceFile(dir) {
|
|
|
3565
3632
|
}
|
|
3566
3633
|
|
|
3567
3634
|
// src/map/surface.ts
|
|
3568
|
-
import { readFileSync as
|
|
3635
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3569
3636
|
import { join as join13 } from "path";
|
|
3570
3637
|
var STATIC_GENERATORS = [
|
|
3571
3638
|
{ dep: "vite", label: "vite" },
|
|
@@ -3604,7 +3671,7 @@ var SHAPE_SIGNAL = {
|
|
|
3604
3671
|
};
|
|
3605
3672
|
function readManifest(cwd) {
|
|
3606
3673
|
try {
|
|
3607
|
-
const parsed = JSON.parse(
|
|
3674
|
+
const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
|
|
3608
3675
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
3609
3676
|
} catch {
|
|
3610
3677
|
return null;
|
|
@@ -3617,7 +3684,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
3617
3684
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
3618
3685
|
let text;
|
|
3619
3686
|
try {
|
|
3620
|
-
text =
|
|
3687
|
+
text = readFileSync8(join13(cwd, file), "utf8");
|
|
3621
3688
|
} catch {
|
|
3622
3689
|
continue;
|
|
3623
3690
|
}
|
|
@@ -3745,7 +3812,7 @@ function surfaceNote(surface) {
|
|
|
3745
3812
|
}
|
|
3746
3813
|
|
|
3747
3814
|
// src/map/sinks.ts
|
|
3748
|
-
import { createHash as
|
|
3815
|
+
import { createHash as createHash3 } from "crypto";
|
|
3749
3816
|
var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
|
|
3750
3817
|
var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
|
|
3751
3818
|
var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
|
|
@@ -4001,7 +4068,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
|
4001
4068
|
return table?.[index] ?? "unknown";
|
|
4002
4069
|
}
|
|
4003
4070
|
function sinkId(s, owner) {
|
|
4004
|
-
return
|
|
4071
|
+
return createHash3("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
|
|
4005
4072
|
}
|
|
4006
4073
|
function dedupeSinks(sinks, owner) {
|
|
4007
4074
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4017,7 +4084,7 @@ function dedupeSinks(sinks, owner) {
|
|
|
4017
4084
|
}
|
|
4018
4085
|
|
|
4019
4086
|
// src/map/module-graph.ts
|
|
4020
|
-
import { readFileSync as
|
|
4087
|
+
import { readFileSync as readFileSync9, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
4021
4088
|
import { dirname as dirname4, join as join14, relative as relative3, resolve as resolvePath } from "path";
|
|
4022
4089
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
4023
4090
|
function createModuleGraph(ts, opts) {
|
|
@@ -4026,7 +4093,7 @@ function createModuleGraph(ts, opts) {
|
|
|
4026
4093
|
if (cache.has(file)) return cache.get(file) ?? null;
|
|
4027
4094
|
let entry = null;
|
|
4028
4095
|
try {
|
|
4029
|
-
const text =
|
|
4096
|
+
const text = readFileSync9(file, "utf8");
|
|
4030
4097
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
4031
4098
|
const bindings = buildModuleBindings(sf, ts);
|
|
4032
4099
|
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
@@ -4125,8 +4192,8 @@ function collectCallees(sf, ts) {
|
|
|
4125
4192
|
}
|
|
4126
4193
|
|
|
4127
4194
|
// src/map/coordinates.ts
|
|
4128
|
-
function runtimeCoordinate(source,
|
|
4129
|
-
if (/\[\d*\]/.test(
|
|
4195
|
+
function runtimeCoordinate(source, path12) {
|
|
4196
|
+
if (/\[\d*\]/.test(path12)) {
|
|
4130
4197
|
return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
|
|
4131
4198
|
}
|
|
4132
4199
|
switch (source) {
|
|
@@ -4135,15 +4202,15 @@ function runtimeCoordinate(source, path11) {
|
|
|
4135
4202
|
case "multipart":
|
|
4136
4203
|
case "body":
|
|
4137
4204
|
case "server-fn-data":
|
|
4138
|
-
return { runtimeParameter: `post.${
|
|
4205
|
+
return { runtimeParameter: `post.${path12}` };
|
|
4139
4206
|
case "query":
|
|
4140
|
-
return { runtimeParameter: `get.${
|
|
4207
|
+
return { runtimeParameter: `get.${path12}` };
|
|
4141
4208
|
case "cookie":
|
|
4142
|
-
return { runtimeParameter: `cookie.${
|
|
4209
|
+
return { runtimeParameter: `cookie.${path12}` };
|
|
4143
4210
|
case "file":
|
|
4144
|
-
return { runtimeParameter: `files.${
|
|
4211
|
+
return { runtimeParameter: `files.${path12}` };
|
|
4145
4212
|
case "header":
|
|
4146
|
-
return { runtimeParameter: `server.HTTP_${
|
|
4213
|
+
return { runtimeParameter: `server.HTTP_${path12.toUpperCase().replace(/-/g, "_")}` };
|
|
4147
4214
|
case "route-param":
|
|
4148
4215
|
return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
|
|
4149
4216
|
default:
|
|
@@ -4175,8 +4242,8 @@ function addressSpaceOf(source) {
|
|
|
4175
4242
|
function isProvenFlow(confidence) {
|
|
4176
4243
|
return confidence === "exact-local" || confidence === "transformed-local";
|
|
4177
4244
|
}
|
|
4178
|
-
function inputIdOf(source,
|
|
4179
|
-
return `${addressSpaceOf(source)}:${
|
|
4245
|
+
function inputIdOf(source, path12) {
|
|
4246
|
+
return `${addressSpaceOf(source)}:${path12}`;
|
|
4180
4247
|
}
|
|
4181
4248
|
function withCoordinates(fields, source) {
|
|
4182
4249
|
return fields.map((f) => {
|
|
@@ -4375,8 +4442,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
|
|
|
4375
4442
|
if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
|
|
4376
4443
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
4377
4444
|
const rootPath = /* @__PURE__ */ new Map();
|
|
4378
|
-
const addRoot = (name,
|
|
4379
|
-
if (!rootPath.has(name)) rootPath.set(name, { path:
|
|
4445
|
+
const addRoot = (name, path12, space) => {
|
|
4446
|
+
if (!rootPath.has(name)) rootPath.set(name, { path: path12, space });
|
|
4380
4447
|
};
|
|
4381
4448
|
for (const p of params ?? []) {
|
|
4382
4449
|
if (!p?.name) continue;
|
|
@@ -4521,8 +4588,8 @@ function dedupeLimitations(list) {
|
|
|
4521
4588
|
function join22(base, seg) {
|
|
4522
4589
|
return base ? `${base}.${seg}` : seg;
|
|
4523
4590
|
}
|
|
4524
|
-
function normalizePath(
|
|
4525
|
-
return
|
|
4591
|
+
function normalizePath(path12) {
|
|
4592
|
+
return path12.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
|
|
4526
4593
|
}
|
|
4527
4594
|
function fluentChainCalls(call, ts) {
|
|
4528
4595
|
let root = call;
|
|
@@ -4768,7 +4835,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
4768
4835
|
}
|
|
4769
4836
|
|
|
4770
4837
|
// src/map/imports.ts
|
|
4771
|
-
import { readFileSync as
|
|
4838
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
4772
4839
|
import { join as join15 } from "path";
|
|
4773
4840
|
var MAX_SITES = 5;
|
|
4774
4841
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
@@ -4780,7 +4847,7 @@ function readPathAliases(cwd) {
|
|
|
4780
4847
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
4781
4848
|
let paths;
|
|
4782
4849
|
try {
|
|
4783
|
-
const raw =
|
|
4850
|
+
const raw = readFileSync10(join15(cwd, name), "utf8");
|
|
4784
4851
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
4785
4852
|
paths = parsed.compilerOptions?.paths;
|
|
4786
4853
|
} catch {
|
|
@@ -5159,7 +5226,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5159
5226
|
const hopTargets = /* @__PURE__ */ new Set();
|
|
5160
5227
|
for (const file of files) {
|
|
5161
5228
|
try {
|
|
5162
|
-
const text =
|
|
5229
|
+
const text = readFileSync11(file, "utf8");
|
|
5163
5230
|
const relFile = relative4(cwd, file);
|
|
5164
5231
|
sourceBytes += text.length;
|
|
5165
5232
|
if (!hasEntrySignal(text)) {
|
|
@@ -5172,7 +5239,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5172
5239
|
}
|
|
5173
5240
|
parsed++;
|
|
5174
5241
|
entryFiles.add(file);
|
|
5175
|
-
const fingerprint =
|
|
5242
|
+
const fingerprint = createHash4("sha256").update(text).digest("hex").slice(0, 16);
|
|
5176
5243
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
5177
5244
|
const bindings = buildModuleBindings(sf, ts);
|
|
5178
5245
|
for (const specifier of bindings.imports) {
|
|
@@ -5214,7 +5281,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5214
5281
|
for (const target of hopTargets) {
|
|
5215
5282
|
if (entryFiles.has(target)) continue;
|
|
5216
5283
|
try {
|
|
5217
|
-
const text =
|
|
5284
|
+
const text = readFileSync11(target, "utf8");
|
|
5218
5285
|
const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
|
|
5219
5286
|
const bindings = buildModuleBindings(sf, ts);
|
|
5220
5287
|
const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
|
|
@@ -5345,8 +5412,8 @@ async function buildInputMap(cwd, options = {}) {
|
|
|
5345
5412
|
}
|
|
5346
5413
|
|
|
5347
5414
|
// src/setup.ts
|
|
5348
|
-
import { readFileSync as
|
|
5349
|
-
import
|
|
5415
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
|
|
5416
|
+
import path11 from "path";
|
|
5350
5417
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
5351
5418
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
5352
5419
|
function setupProtection(cwd) {
|
|
@@ -5364,8 +5431,8 @@ function appendHook(existing, command) {
|
|
|
5364
5431
|
return `${existing} && ${command}`;
|
|
5365
5432
|
}
|
|
5366
5433
|
function wireBuildScripts(cwd, packageManager) {
|
|
5367
|
-
const target =
|
|
5368
|
-
const raw =
|
|
5434
|
+
const target = path11.join(cwd, "package.json");
|
|
5435
|
+
const raw = readFileSync12(target, "utf8");
|
|
5369
5436
|
const pkg = JSON.parse(raw);
|
|
5370
5437
|
const scripts = pkg.scripts ?? {};
|
|
5371
5438
|
const build = scripts.build;
|
|
@@ -5414,7 +5481,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
5414
5481
|
const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
|
|
5415
5482
|
const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
|
|
5416
5483
|
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
5417
|
-
|
|
5484
|
+
writeFileSync7(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
|
|
5418
5485
|
if (build === void 0 || build.trim().length === 0) {
|
|
5419
5486
|
return {
|
|
5420
5487
|
changed: true,
|
|
@@ -5492,10 +5559,17 @@ Usage:
|
|
|
5492
5559
|
what's missing, with tailored commands), then
|
|
5493
5560
|
print the full setup guide. --full prints the
|
|
5494
5561
|
guide even when setup is complete
|
|
5495
|
-
patchstack-connect login [
|
|
5562
|
+
patchstack-connect login [--wait] Recover this site's credential when
|
|
5496
5563
|
.patchstackrc.json has been lost. Prints a link
|
|
5497
|
-
for the site's OWNER to approve in the dashboard
|
|
5498
|
-
then waits
|
|
5564
|
+
for the site's OWNER to approve in the dashboard.
|
|
5565
|
+
In a terminal it then waits. When the output is
|
|
5566
|
+
piped or captured \u2014 an assistant running it \u2014 it
|
|
5567
|
+
prints the link and EXITS, so the link is visible
|
|
5568
|
+
immediately. Run it AGAIN once the user confirms
|
|
5569
|
+
they approved: it resumes the same request rather
|
|
5570
|
+
than starting a new one, and finishes the flow.
|
|
5571
|
+
--wait blocks instead of returning. Use this
|
|
5572
|
+
instead of deleting
|
|
5499
5573
|
.patchstackrc.json and re-scanning, which would
|
|
5500
5574
|
provision a second site. Approving ROTATES the
|
|
5501
5575
|
credential: CI, deploys and other machines using
|
|
@@ -5600,7 +5674,26 @@ async function runLogin(args) {
|
|
|
5600
5674
|
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
5601
5675
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
5602
5676
|
});
|
|
5603
|
-
const
|
|
5677
|
+
const approved = () => {
|
|
5678
|
+
console.log("\n \u2713 Credential restored and saved to .patchstackrc.json.");
|
|
5679
|
+
console.log(" The previous credential no longer works. Update it anywhere else it was set:");
|
|
5680
|
+
console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
|
|
5681
|
+
return 0;
|
|
5682
|
+
};
|
|
5683
|
+
if (args.flags.has("wait")) {
|
|
5684
|
+
const pending = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
5685
|
+
if (pending === null) {
|
|
5686
|
+
console.error("\n No login is waiting for approval. Run `patchstack-connect login` first.\n");
|
|
5687
|
+
return 1;
|
|
5688
|
+
}
|
|
5689
|
+
const resumed = await waitForApproval(config, pending);
|
|
5690
|
+
if (resumed.status === "approved") return approved();
|
|
5691
|
+
console.error(`
|
|
5692
|
+
${resumed.message ?? "Login failed."}
|
|
5693
|
+
`);
|
|
5694
|
+
return 1;
|
|
5695
|
+
}
|
|
5696
|
+
const prompt = (userCode, verificationUri) => {
|
|
5604
5697
|
console.log(`
|
|
5605
5698
|
Your code: ${userCode}`);
|
|
5606
5699
|
console.log(` Approve at: ${verificationUri}
|
|
@@ -5608,14 +5701,39 @@ async function runLogin(args) {
|
|
|
5608
5701
|
console.log(" Open that link and approve it as the site's owner. Approving issues a new");
|
|
5609
5702
|
console.log(" credential and stops the current one working \u2014 CI, deploys and any other");
|
|
5610
5703
|
console.log(" machine using it will need the new value.\n");
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5704
|
+
};
|
|
5705
|
+
if (process.stdout.isTTY !== true) {
|
|
5706
|
+
const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
5707
|
+
if (existing !== null && Date.now() < existing.expiresAt) {
|
|
5708
|
+
const outcome = await redeemIfApproved(config, existing);
|
|
5709
|
+
if (outcome === "approved") return approved();
|
|
5710
|
+
if (outcome === "pending") {
|
|
5711
|
+
const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
|
|
5712
|
+
console.log(`
|
|
5713
|
+
Still waiting for approval of code ${existing.userCode}.`);
|
|
5714
|
+
console.log(` Approve at: ${existing.verificationUri}`);
|
|
5715
|
+
console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
|
|
5716
|
+
`);
|
|
5717
|
+
return 0;
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
const started = await startLogin(config);
|
|
5721
|
+
if (started.status !== "started" || started.pending === void 0) {
|
|
5722
|
+
console.error(`
|
|
5723
|
+
${started.message ?? "Login failed."}
|
|
5724
|
+
`);
|
|
5725
|
+
return 1;
|
|
5726
|
+
}
|
|
5727
|
+
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
5728
|
+
console.log(" Give that link to the user. When they confirm they have approved it, run");
|
|
5729
|
+
console.log(" this same command again (or `login --wait` to block until they do).\n");
|
|
5617
5730
|
return 0;
|
|
5618
5731
|
}
|
|
5732
|
+
const result = await login(config, (userCode, verificationUri) => {
|
|
5733
|
+
prompt(userCode, verificationUri);
|
|
5734
|
+
console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
|
|
5735
|
+
});
|
|
5736
|
+
if (result.status === "approved") return approved();
|
|
5619
5737
|
console.error(`
|
|
5620
5738
|
${result.message ?? "Login failed."}
|
|
5621
5739
|
`);
|
|
@@ -5664,7 +5782,7 @@ async function runMap(args) {
|
|
|
5664
5782
|
const json = JSON.stringify(map, null, 2);
|
|
5665
5783
|
const out = getStringFlag(args.flags, "out");
|
|
5666
5784
|
if (out) {
|
|
5667
|
-
|
|
5785
|
+
writeFileSync8(out, json);
|
|
5668
5786
|
console.error(`patchstack: wrote ${out}`);
|
|
5669
5787
|
} else if (args.flags.get("upload") !== true) {
|
|
5670
5788
|
console.log(json);
|
|
@@ -5950,7 +6068,7 @@ async function runGuide(args) {
|
|
|
5950
6068
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
5951
6069
|
console.log("");
|
|
5952
6070
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
5953
|
-
console.log(
|
|
6071
|
+
console.log(readFileSync13(guidePath, "utf8"));
|
|
5954
6072
|
return 0;
|
|
5955
6073
|
}
|
|
5956
6074
|
async function runSetup(args) {
|
|
@@ -6131,7 +6249,7 @@ async function runMarkBuild(args) {
|
|
|
6131
6249
|
let marked = 0;
|
|
6132
6250
|
let widgetTouched = 0;
|
|
6133
6251
|
for (const file of files) {
|
|
6134
|
-
const before =
|
|
6252
|
+
const before = readFileSync13(file, "utf8");
|
|
6135
6253
|
let after = injectMarker(before, snippet);
|
|
6136
6254
|
if (widgetUuid !== null) {
|
|
6137
6255
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
@@ -6141,7 +6259,7 @@ async function runMarkBuild(args) {
|
|
|
6141
6259
|
after = ensured.html;
|
|
6142
6260
|
}
|
|
6143
6261
|
if (after !== before) {
|
|
6144
|
-
|
|
6262
|
+
writeFileSync8(file, after);
|
|
6145
6263
|
marked += 1;
|
|
6146
6264
|
}
|
|
6147
6265
|
}
|