@patchstack/connect 0.4.2 → 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.
Files changed (34) hide show
  1. package/AGENT-INSTALL.md +24 -4
  2. package/README.md +27 -3
  3. package/dist/cli.js +319 -61
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.cjs +230 -20
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +65 -0
  8. package/dist/index.d.ts +65 -0
  9. package/dist/index.js +227 -17
  10. package/dist/index.js.map +1 -1
  11. package/dist/protect/templates/astro-middleware.ts +22 -1
  12. package/dist/protect/templates/express-guard.cjs +54 -4
  13. package/dist/protect/templates/express-guard.js +54 -4
  14. package/dist/protect/templates/express-guard.ts +43 -5
  15. package/dist/protect/templates/fastify-plugin.cjs +41 -3
  16. package/dist/protect/templates/fastify-plugin.js +41 -3
  17. package/dist/protect/templates/fastify-plugin.ts +30 -2
  18. package/dist/protect/templates/generic-guard.cjs +58 -5
  19. package/dist/protect/templates/generic-guard.js +58 -5
  20. package/dist/protect/templates/generic-guard.ts +47 -4
  21. package/dist/protect/templates/guard.ts +42 -8
  22. package/dist/protect/templates/next-middleware.ts +22 -1
  23. package/dist/protect/templates/nuxt-middleware.ts +22 -1
  24. package/dist/protect/templates/sveltekit-hooks.ts +22 -1
  25. package/dist/protect.cjs +351 -97
  26. package/dist/protect.cjs.map +1 -1
  27. package/dist/protect.edge.js +56 -24
  28. package/dist/protect.edge.js.map +1 -1
  29. package/dist/protect.js +57 -25
  30. package/dist/protect.js.map +1 -1
  31. package/dist/{refresh-manifest-JNCTIAM5.js → refresh-manifest-2MMPQN2A.js} +243 -35
  32. package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
  33. package/package.json +3 -3
  34. package/dist/refresh-manifest-JNCTIAM5.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1210,8 +1210,8 @@ function computeManifestChecksum(packages) {
1210
1210
  var TOKEN_SKEW_MS = 6e4;
1211
1211
  function buildTokenUrl(manifestEndpoint) {
1212
1212
  const url = new URL(manifestEndpoint);
1213
- const path12 = url.pathname.replace(/\/$/, "");
1214
- url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1213
+ const path13 = url.pathname.replace(/\/$/, "");
1214
+ url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1215
1215
  url.search = "";
1216
1216
  url.hash = "";
1217
1217
  return url.toString();
@@ -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;
@@ -1310,8 +1329,8 @@ function authFailureMessage(status, config) {
1310
1329
  }
1311
1330
  function buildRulesUrl(manifestEndpoint, siteUuid) {
1312
1331
  const url = new URL(manifestEndpoint);
1313
- const path12 = url.pathname.replace(/\/$/, "");
1314
- url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1332
+ const path13 = url.pathname.replace(/\/$/, "");
1333
+ url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
1315
1334
  url.search = "";
1316
1335
  url.hash = "";
1317
1336
  return url.toString();
@@ -1326,16 +1345,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
1326
1345
  }
1327
1346
  function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
1328
1347
  const url = new URL(manifestEndpoint);
1329
- const path12 = url.pathname.replace(/\/$/, "");
1330
- url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1348
+ const path13 = url.pathname.replace(/\/$/, "");
1349
+ url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
1331
1350
  url.search = "";
1332
1351
  url.hash = "";
1333
1352
  return url.toString();
1334
1353
  }
1335
1354
  function buildInputMapUrl(manifestEndpoint, siteUuid) {
1336
1355
  const url = new URL(manifestEndpoint);
1337
- const path12 = url.pathname.replace(/\/$/, "");
1338
- url.pathname = path12.endsWith("/manifest") ? `${path12.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1356
+ const path13 = url.pathname.replace(/\/$/, "");
1357
+ url.pathname = path13.endsWith("/manifest") ? `${path13.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
1339
1358
  url.search = "";
1340
1359
  url.hash = "";
1341
1360
  return url.toString();
@@ -1436,6 +1455,14 @@ async function fetchSiteStatus(config) {
1436
1455
  return "unknown";
1437
1456
  }
1438
1457
  }
1458
+ function buildManifestBody(config, payload) {
1459
+ return {
1460
+ ...payload,
1461
+ environment: config.environment,
1462
+ ...typeof config.siteUrl === "string" && config.siteUrl !== "" ? { url: config.siteUrl } : {},
1463
+ ...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {}
1464
+ };
1465
+ }
1439
1466
  async function postManifest(config, payload) {
1440
1467
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
1441
1468
  const timeoutMs = config.timeoutMs;
@@ -1446,9 +1473,10 @@ async function postManifest(config, payload) {
1446
1473
  headers: {
1447
1474
  "Content-Type": "application/json",
1448
1475
  Accept: "application/json",
1449
- "User-Agent": "@patchstack/connect"
1476
+ "User-Agent": "@patchstack/connect",
1477
+ ...claimTokenHeader(config)
1450
1478
  },
1451
- body: JSON.stringify({ ...payload, environment: config.environment }),
1479
+ body: JSON.stringify(buildManifestBody(config, payload)),
1452
1480
  signal: AbortSignal.timeout(timeoutMs)
1453
1481
  });
1454
1482
  } catch (cause) {
@@ -1711,11 +1739,201 @@ function renderDemoTestCommands(url, scenario) {
1711
1739
  }
1712
1740
 
1713
1741
  // src/config.ts
1714
- import { readFile as readFile8, writeFile, chmod } from "fs/promises";
1742
+ import { readFile as readFile9, writeFile, chmod } from "fs/promises";
1743
+ import path7 from "path";
1744
+
1745
+ // src/site-url.ts
1746
+ var RESERVED_SUFFIXES = [
1747
+ ".local",
1748
+ ".localhost",
1749
+ ".localdomain",
1750
+ ".internal",
1751
+ ".intranet",
1752
+ ".invalid",
1753
+ ".test",
1754
+ ".example",
1755
+ ".home.arpa",
1756
+ ".in-addr.arpa",
1757
+ ".ip6.arpa"
1758
+ ];
1759
+ function isUnpublishableHost(hostname) {
1760
+ const host = hostname.toLowerCase().replace(/\.+$/, "");
1761
+ if (host === "") return true;
1762
+ if (host.startsWith("[")) return true;
1763
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
1764
+ if (!host.includes(".")) return true;
1765
+ return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
1766
+ }
1767
+ function normaliseSiteUrl(value) {
1768
+ const trimmed = (value ?? "").trim();
1769
+ if (trimmed === "") return null;
1770
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
1771
+ let parsed;
1772
+ try {
1773
+ parsed = new URL(withScheme);
1774
+ } catch {
1775
+ return null;
1776
+ }
1777
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
1778
+ if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
1779
+ const origin = `${parsed.protocol}//${parsed.host}`;
1780
+ return origin.length <= 191 ? origin : null;
1781
+ }
1782
+ var PRODUCTION_URL_SOURCES = [
1783
+ {
1784
+ // VERCEL_URL is per-deployment and changes every build, so it is deliberately not read here.
1785
+ platform: "vercel",
1786
+ read: (env) => env.VERCEL_ENV === "production" ? env.VERCEL_PROJECT_PRODUCTION_URL : void 0
1787
+ },
1788
+ {
1789
+ // `URL` is generic enough to belong to anything, so it counts only alongside Netlify's own markers.
1790
+ platform: "netlify",
1791
+ read: (env) => env.NETLIFY === "true" && env.CONTEXT === "production" ? env.URL : void 0
1792
+ },
1793
+ {
1794
+ platform: "render",
1795
+ read: (env) => env.RENDER === "true" && env.IS_PULL_REQUEST !== "true" ? env.RENDER_EXTERNAL_URL : void 0
1796
+ },
1797
+ {
1798
+ platform: "railway",
1799
+ read: (env) => env.RAILWAY_ENVIRONMENT_NAME === "production" ? env.RAILWAY_PUBLIC_DOMAIN : void 0
1800
+ }
1801
+ ];
1802
+ function detectSiteUrl(env = process.env) {
1803
+ for (const source of PRODUCTION_URL_SOURCES) {
1804
+ const url = normaliseSiteUrl(source.read(env));
1805
+ if (url !== null) {
1806
+ return { url, platform: source.platform };
1807
+ }
1808
+ }
1809
+ return null;
1810
+ }
1811
+
1812
+ // src/site-name.ts
1813
+ import { readFile as readFile8 } from "fs/promises";
1715
1814
  import path6 from "path";
1815
+ var NAME_MAX_LENGTH = 191;
1816
+ var TEMPLATE_PACKAGE_NAMES = /* @__PURE__ */ new Set([
1817
+ // Lovable
1818
+ "vite_react_shadcn_ts",
1819
+ // Bolt
1820
+ "vite-react-typescript-starter",
1821
+ // Scaffolders and the names people accept from them
1822
+ "vite-project",
1823
+ "my-app",
1824
+ "my-project",
1825
+ "my-v0-project",
1826
+ "app",
1827
+ "frontend",
1828
+ "web",
1829
+ "client",
1830
+ "project",
1831
+ "template",
1832
+ "example",
1833
+ "react-app",
1834
+ "next-app",
1835
+ "nextjs",
1836
+ "starter",
1837
+ "test"
1838
+ ]);
1839
+ var TEMPLATE_TITLES = /* @__PURE__ */ new Set([
1840
+ "vite + react",
1841
+ "vite + react + ts",
1842
+ "vite + vue",
1843
+ "vite + vue + ts",
1844
+ "vite app",
1845
+ "react app",
1846
+ "create next app",
1847
+ "document",
1848
+ "untitled",
1849
+ "index",
1850
+ "home",
1851
+ "app"
1852
+ ]);
1853
+ var NAMED_ENTITIES = {
1854
+ amp: "&",
1855
+ lt: "<",
1856
+ gt: ">",
1857
+ quot: '"',
1858
+ apos: "'",
1859
+ nbsp: " "
1860
+ };
1861
+ function normaliseSiteName(value) {
1862
+ const collapsed = (value ?? "").replace(/\s+/g, " ").trim();
1863
+ if (collapsed === "") return null;
1864
+ return collapsed.slice(0, NAME_MAX_LENGTH);
1865
+ }
1866
+ function decodeEntities(text) {
1867
+ return text.replace(/&([a-z]+);/gi, (whole, name) => NAMED_ENTITIES[name.toLowerCase()] ?? whole);
1868
+ }
1869
+ function nameFromHtmlTitle(html) {
1870
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html);
1871
+ if (match === null) return null;
1872
+ const name = normaliseSiteName(decodeEntities(match[1] ?? ""));
1873
+ if (name === null || TEMPLATE_TITLES.has(name.toLowerCase())) return null;
1874
+ return name;
1875
+ }
1876
+ function nameFromPackageName(raw) {
1877
+ if (typeof raw !== "string") return null;
1878
+ const unscoped = raw.trim().replace(/^@[^/]+\//, "");
1879
+ if (unscoped === "" || TEMPLATE_PACKAGE_NAMES.has(unscoped.toLowerCase())) return null;
1880
+ const words = unscoped.split(/[-_.\s]+/).filter((word) => word !== "").map((word) => word === word.toUpperCase() ? word : word.charAt(0).toUpperCase() + word.slice(1));
1881
+ return normaliseSiteName(words.join(" "));
1882
+ }
1883
+ async function readIfPresent(file) {
1884
+ try {
1885
+ return await readFile8(file, "utf8");
1886
+ } catch {
1887
+ return null;
1888
+ }
1889
+ }
1890
+ async function detectSiteName(cwd) {
1891
+ for (const shell of ["index.html", path6.join("public", "index.html")]) {
1892
+ const html = await readIfPresent(path6.join(cwd, shell));
1893
+ if (html === null) continue;
1894
+ const name = nameFromHtmlTitle(html);
1895
+ if (name !== null) return { name, source: "index.html" };
1896
+ }
1897
+ const manifest = await readIfPresent(path6.join(cwd, "package.json"));
1898
+ if (manifest !== null) {
1899
+ try {
1900
+ const parsed = JSON.parse(manifest);
1901
+ const name = nameFromPackageName(
1902
+ typeof parsed === "object" && parsed !== null ? parsed.name : void 0
1903
+ );
1904
+ if (name !== null) return { name, source: "package.json" };
1905
+ } catch {
1906
+ }
1907
+ }
1908
+ return null;
1909
+ }
1910
+
1911
+ // src/config.ts
1716
1912
  var CONFIG_FILENAME = ".patchstackrc.json";
1717
1913
  var SECRET_FILENAME = ".patchstackrc.local.json";
1718
1914
  var DEFAULT_ENVIRONMENT = "production";
1915
+ async function resolveSiteIdentity(cwd, fromEnv, fromFile) {
1916
+ const configuredUrl = stated(fromEnv.url) ?? stated(fromFile.url);
1917
+ const configuredName = stated(fromEnv.name) ?? stated(fromFile.name);
1918
+ let url;
1919
+ if (configuredUrl === null) {
1920
+ url = detectSiteUrl(process.env)?.url ?? null;
1921
+ } else {
1922
+ url = normaliseSiteUrl(configuredUrl);
1923
+ if (url === null) {
1924
+ throw new PatchstackError(
1925
+ `Site URL "${configuredUrl}" is not an address the public can reach this site at. Set "url" in .patchstackrc.json (or PATCHSTACK_SITE_URL) to the http(s) address visitors use \u2014 a hostname, not an IP address \u2014 or remove it to let the build environment answer.`,
1926
+ "CONFIG_INVALID"
1927
+ );
1928
+ }
1929
+ }
1930
+ const name = configuredName === null ? (await detectSiteName(cwd))?.name ?? null : normaliseSiteName(configuredName);
1931
+ return { url, name };
1932
+ }
1933
+ function stated(value) {
1934
+ const trimmed = (value ?? "").trim();
1935
+ return trimmed === "" ? null : trimmed;
1936
+ }
1719
1937
  async function resolveConfig(options) {
1720
1938
  const fromFile = await readConfigFile(options.cwd);
1721
1939
  const fromSecretFile = await readSecretFile(options.cwd);
@@ -1745,25 +1963,30 @@ async function resolveConfig(options) {
1745
1963
  }
1746
1964
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
1747
1965
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
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);
1748
1968
  return {
1749
1969
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1750
1970
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
1751
1971
  pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
1972
+ siteUrl: identity.url,
1973
+ siteName: identity.name,
1752
1974
  endpoint,
1753
1975
  timeoutMs,
1754
1976
  environment,
1755
- widget: fromFile.widget !== false
1977
+ widget: fromFile.widget !== false,
1978
+ claimToken
1756
1979
  };
1757
1980
  }
1758
1981
  async function writeConfigFile(cwd, config) {
1759
- const target = path6.join(cwd, CONFIG_FILENAME);
1982
+ const target = path7.join(cwd, CONFIG_FILENAME);
1760
1983
  const content = JSON.stringify(config, null, 2) + "\n";
1761
1984
  await writeFile(target, content, "utf8");
1762
1985
  return target;
1763
1986
  }
1764
1987
  var SECRET_CONFIG_FILENAME = SECRET_FILENAME;
1765
1988
  async function writeSecretFile(cwd, secrets) {
1766
- const target = path6.join(cwd, SECRET_FILENAME);
1989
+ const target = path7.join(cwd, SECRET_FILENAME);
1767
1990
  await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
1768
1991
  try {
1769
1992
  await chmod(target, 384);
@@ -1773,10 +1996,10 @@ async function writeSecretFile(cwd, secrets) {
1773
1996
  return { path: target, ...ignore };
1774
1997
  }
1775
1998
  async function ensureIgnored(cwd, entry) {
1776
- const target = path6.join(cwd, ".gitignore");
1999
+ const target = path7.join(cwd, ".gitignore");
1777
2000
  let existing = "";
1778
2001
  try {
1779
- existing = await readFile8(target, "utf8");
2002
+ existing = await readFile9(target, "utf8");
1780
2003
  } catch (err) {
1781
2004
  const code = err.code;
1782
2005
  if (code !== "ENOENT") {
@@ -1796,7 +2019,7 @@ ${entry}
1796
2019
  return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
1797
2020
  }
1798
2021
  try {
1799
- const written = await readFile8(target, "utf8");
2022
+ const written = await readFile9(target, "utf8");
1800
2023
  if (ignoresEntry(written, entry)) return { ignored: true };
1801
2024
  return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
1802
2025
  } catch (err) {
@@ -1806,7 +2029,7 @@ ${entry}
1806
2029
  }
1807
2030
  async function secretFileIgnored(cwd) {
1808
2031
  try {
1809
- const contents = await readFile8(path6.join(cwd, ".gitignore"), "utf8");
2032
+ const contents = await readFile9(path7.join(cwd, ".gitignore"), "utf8");
1810
2033
  if (ignoresEntry(contents, SECRET_FILENAME)) return { ignored: true };
1811
2034
  return { ignored: false, reason: `.gitignore does not cover ${SECRET_FILENAME}` };
1812
2035
  } catch (err) {
@@ -1853,10 +2076,10 @@ async function readConfigFile(cwd) {
1853
2076
  return readJsonFile(cwd, CONFIG_FILENAME);
1854
2077
  }
1855
2078
  async function readJsonFile(cwd, filename) {
1856
- const target = path6.join(cwd, filename);
2079
+ const target = path7.join(cwd, filename);
1857
2080
  let raw;
1858
2081
  try {
1859
- raw = await readFile8(target, "utf8");
2082
+ raw = await readFile9(target, "utf8");
1860
2083
  } catch (err) {
1861
2084
  if (err.code === "ENOENT") {
1862
2085
  return {};
@@ -1893,6 +2116,8 @@ function readEnv() {
1893
2116
  const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
1894
2117
  return {
1895
2118
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
2119
+ url: process.env.PATCHSTACK_SITE_URL ?? void 0,
2120
+ name: process.env.PATCHSTACK_SITE_NAME ?? void 0,
1896
2121
  apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
1897
2122
  pulseAuth: process.env.PATCHSTACK_PULSE_AUTH ?? void 0,
1898
2123
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
@@ -1909,7 +2134,7 @@ function isEnvironment(value) {
1909
2134
 
1910
2135
  // src/mark-build.ts
1911
2136
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
1912
- import path7 from "path";
2137
+ import path8 from "path";
1913
2138
 
1914
2139
  // src/stack.ts
1915
2140
  var STACK_RULES = [
@@ -2002,11 +2227,11 @@ var MARKER_ATTR = "data-patchstack-build";
2002
2227
  var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
2003
2228
  function resolveBuildDir(cwd, override) {
2004
2229
  if (override !== void 0 && override !== "") {
2005
- const abs = path7.resolve(cwd, override);
2230
+ const abs = path8.resolve(cwd, override);
2006
2231
  return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
2007
2232
  }
2008
2233
  for (const candidate of BUILD_DIR_CANDIDATES) {
2009
- const abs = path7.resolve(cwd, candidate);
2234
+ const abs = path8.resolve(cwd, candidate);
2010
2235
  if (existsSync(abs) && statSync(abs).isDirectory()) {
2011
2236
  return abs;
2012
2237
  }
@@ -2017,7 +2242,7 @@ function findHtmlFiles(dir) {
2017
2242
  const out = [];
2018
2243
  const walk2 = (current) => {
2019
2244
  for (const entry of readdirSync(current, { withFileTypes: true })) {
2020
- const full = path7.join(current, entry.name);
2245
+ const full = path8.join(current, entry.name);
2021
2246
  if (entry.isDirectory()) {
2022
2247
  walk2(full);
2023
2248
  } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
@@ -2166,7 +2391,7 @@ function ensureSourceMarker(cwd, shell, framework) {
2166
2391
  if (shell === null) {
2167
2392
  return { shell: null, source: "", action: "no-anchor" };
2168
2393
  }
2169
- const file = path7.resolve(cwd, shell);
2394
+ const file = path8.resolve(cwd, shell);
2170
2395
  const before = readFileSync(file, "utf8");
2171
2396
  const result = ensureMarkerInJsxShell(before, framework);
2172
2397
  if (result.source !== before) {
@@ -2177,7 +2402,7 @@ function ensureSourceMarker(cwd, shell, framework) {
2177
2402
 
2178
2403
  // src/guide.ts
2179
2404
  import { existsSync as existsSync12, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
2180
- import path9 from "path";
2405
+ import path10 from "path";
2181
2406
 
2182
2407
  // src/protect/install/util.ts
2183
2408
  import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
@@ -3674,7 +3899,7 @@ function runVerify(cwd) {
3674
3899
 
3675
3900
  // src/widget.ts
3676
3901
  import { existsSync as existsSync11, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
3677
- import path8 from "path";
3902
+ import path9 from "path";
3678
3903
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
3679
3904
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
3680
3905
  var WIDGET_NEEDLE = "patchstack-widget";
@@ -3712,7 +3937,7 @@ ${indent}</body>`;
3712
3937
  }
3713
3938
  function findSourceShell(cwd) {
3714
3939
  for (const candidate of SOURCE_SHELL_CANDIDATES) {
3715
- if (existsSync11(path8.join(cwd, candidate))) {
3940
+ if (existsSync11(path9.join(cwd, candidate))) {
3716
3941
  return candidate;
3717
3942
  }
3718
3943
  }
@@ -3723,7 +3948,7 @@ function ensureSourceWidget(cwd, siteUuid) {
3723
3948
  if (shell === null) {
3724
3949
  return { shell: null, action: "no-shell" };
3725
3950
  }
3726
- const file = path8.join(cwd, shell);
3951
+ const file = path9.join(cwd, shell);
3727
3952
  const before = readFileSync5(file, "utf8");
3728
3953
  const { html, action } = ensureWidgetInHtml(before, siteUuid);
3729
3954
  if (html !== before) {
@@ -3806,7 +4031,7 @@ function detectPackageManager(cwd) {
3806
4031
  return declared;
3807
4032
  }
3808
4033
  for (const { filename, pm } of PM_BY_LOCKFILE) {
3809
- if (existsSync12(path9.join(cwd, filename))) {
4034
+ if (existsSync12(path10.join(cwd, filename))) {
3810
4035
  return pm;
3811
4036
  }
3812
4037
  }
@@ -3817,7 +4042,7 @@ function installCommand(pm) {
3817
4042
  }
3818
4043
  function readPackageJson(cwd) {
3819
4044
  try {
3820
- return JSON.parse(readFileSync6(path9.join(cwd, "package.json"), "utf8"));
4045
+ return JSON.parse(readFileSync6(path10.join(cwd, "package.json"), "utf8"));
3821
4046
  } catch {
3822
4047
  return null;
3823
4048
  }
@@ -3825,7 +4050,7 @@ function readPackageJson(cwd) {
3825
4050
  function installedVersion(cwd, declaredRange) {
3826
4051
  try {
3827
4052
  const pkg = JSON.parse(
3828
- readFileSync6(path9.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
4053
+ readFileSync6(path10.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
3829
4054
  );
3830
4055
  if (typeof pkg.version === "string" && pkg.version.length > 0) {
3831
4056
  return pkg.version;
@@ -3853,7 +4078,7 @@ function findWidgetMarker(cwd, siteUuid) {
3853
4078
  if (budget <= 0 || settled()) {
3854
4079
  return;
3855
4080
  }
3856
- const full = path9.join(dir, entry.name);
4081
+ const full = path10.join(dir, entry.name);
3857
4082
  if (entry.isDirectory()) {
3858
4083
  if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
3859
4084
  continue;
@@ -3861,7 +4086,7 @@ function findWidgetMarker(cwd, siteUuid) {
3861
4086
  walk2(full, depth + 1);
3862
4087
  continue;
3863
4088
  }
3864
- if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())) {
4089
+ if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path10.extname(entry.name).toLowerCase())) {
3865
4090
  continue;
3866
4091
  }
3867
4092
  budget -= 1;
@@ -3892,7 +4117,7 @@ function findProductionMarker(cwd, widgetFileHint) {
3892
4117
  return false;
3893
4118
  }
3894
4119
  try {
3895
- return readFileSync6(path9.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
4120
+ return readFileSync6(path10.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
3896
4121
  } catch {
3897
4122
  return false;
3898
4123
  }
@@ -3903,7 +4128,7 @@ function resolveWidgetFileHint(cwd, framework) {
3903
4128
  ...GENERIC_WIDGET_FILES
3904
4129
  ];
3905
4130
  for (const candidate of candidates) {
3906
- if (existsSync12(path9.join(cwd, candidate))) {
4131
+ if (existsSync12(path10.join(cwd, candidate))) {
3907
4132
  return candidate;
3908
4133
  }
3909
4134
  }
@@ -4010,7 +4235,7 @@ function renderGuideChecklist(state, useColor) {
4010
4235
  const headerParts = [state.framework, state.packageManager].filter(
4011
4236
  (part) => part !== null
4012
4237
  );
4013
- const name = state.projectName ?? path9.basename(process.cwd());
4238
+ const name = state.projectName ?? path10.basename(process.cwd());
4014
4239
  lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
4015
4240
  if (state.endpointOverride !== null) {
4016
4241
  lines.push(
@@ -4025,6 +4250,15 @@ function renderGuideChecklist(state, useColor) {
4025
4250
  lines.push(detail("Run the guide from the project root."));
4026
4251
  return lines.join("\n");
4027
4252
  }
4253
+ if (countRemainingSteps(state) > 0) {
4254
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Tell the user what you are doing as you go.")}`);
4255
+ lines.push(" Installs here take minutes and they are watching output they cannot read.");
4256
+ lines.push(" Say it in plain words: their site is built out of hundreds of ready-made");
4257
+ lines.push(" pieces of code, Patchstack keeps track of which ones it uses and warns them");
4258
+ lines.push(" when one turns out to be unsafe, and nothing their visitors see changes.");
4259
+ lines.push(" Avoid lockfile / dependency / manifest / npm / UUID when speaking to them.");
4260
+ lines.push("");
4261
+ }
4028
4262
  if (state.installed?.section === "dependencies") {
4029
4263
  lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
4030
4264
  } else if (state.installed !== null) {
@@ -4174,7 +4408,7 @@ function renderGuideChecklist(state, useColor) {
4174
4408
  // src/login.ts
4175
4409
  import { mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
4176
4410
  import { tmpdir } from "os";
4177
- import path10 from "path";
4411
+ import path11 from "path";
4178
4412
  import { createHash as createHash2 } from "crypto";
4179
4413
  function baseFrom(manifestEndpoint) {
4180
4414
  const url = new URL(manifestEndpoint);
@@ -4186,7 +4420,7 @@ function baseFrom(manifestEndpoint) {
4186
4420
  }
4187
4421
  function pendingPath(siteUuid) {
4188
4422
  const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
4189
- return path10.join(tmpdir(), `patchstack-login-${key}.json`);
4423
+ return path11.join(tmpdir(), `patchstack-login-${key}.json`);
4190
4424
  }
4191
4425
  function savePendingLogin(siteUuid, pending) {
4192
4426
  writeFileSync7(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
@@ -4821,10 +5055,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
4821
5055
  boundary = realpathSync(boundary);
4822
5056
  } catch {
4823
5057
  }
4824
- const inProject = (path12) => {
5058
+ const inProject = (path13) => {
4825
5059
  if (opts.followOutside) return true;
4826
5060
  try {
4827
- return isInside(realpathSync(path12), boundary);
5061
+ return isInside(realpathSync(path13), boundary);
4828
5062
  } catch {
4829
5063
  return false;
4830
5064
  }
@@ -5461,8 +5695,8 @@ function collectCallees(sf, ts) {
5461
5695
  }
5462
5696
 
5463
5697
  // src/map/coordinates.ts
5464
- function runtimeCoordinate(source, path12) {
5465
- if (/\[\d*\]/.test(path12)) {
5698
+ function runtimeCoordinate(source, path13) {
5699
+ if (/\[\d*\]/.test(path13)) {
5466
5700
  return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
5467
5701
  }
5468
5702
  switch (source) {
@@ -5471,15 +5705,15 @@ function runtimeCoordinate(source, path12) {
5471
5705
  case "multipart":
5472
5706
  case "body":
5473
5707
  case "server-fn-data":
5474
- return { runtimeParameter: `post.${path12}` };
5708
+ return { runtimeParameter: `post.${path13}` };
5475
5709
  case "query":
5476
- return { runtimeParameter: `get.${path12}` };
5710
+ return { runtimeParameter: `get.${path13}` };
5477
5711
  case "cookie":
5478
- return { runtimeParameter: `cookie.${path12}` };
5712
+ return { runtimeParameter: `cookie.${path13}` };
5479
5713
  case "file":
5480
- return { runtimeParameter: `files.${path12}` };
5714
+ return { runtimeParameter: `files.${path13}` };
5481
5715
  case "header":
5482
- return { runtimeParameter: `server.HTTP_${path12.toUpperCase().replace(/-/g, "_")}` };
5716
+ return { runtimeParameter: `server.HTTP_${path13.toUpperCase().replace(/-/g, "_")}` };
5483
5717
  case "route-param":
5484
5718
  return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
5485
5719
  default:
@@ -5511,8 +5745,8 @@ function addressSpaceOf(source) {
5511
5745
  function isProvenFlow(confidence) {
5512
5746
  return confidence === "exact-local" || confidence === "transformed-local";
5513
5747
  }
5514
- function inputIdOf(source, path12) {
5515
- return `${addressSpaceOf(source)}:${path12}`;
5748
+ function inputIdOf(source, path13) {
5749
+ return `${addressSpaceOf(source)}:${path13}`;
5516
5750
  }
5517
5751
  function withCoordinates(fields, source) {
5518
5752
  return fields.map((f) => {
@@ -5722,8 +5956,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
5722
5956
  if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
5723
5957
  const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
5724
5958
  const rootPath = /* @__PURE__ */ new Map();
5725
- const addRoot = (name, path12, space) => {
5726
- if (!rootPath.has(name)) rootPath.set(name, { path: path12, space });
5959
+ const addRoot = (name, path13, space) => {
5960
+ if (!rootPath.has(name)) rootPath.set(name, { path: path13, space });
5727
5961
  };
5728
5962
  for (const p of params ?? []) {
5729
5963
  if (!p?.name) continue;
@@ -5868,8 +6102,8 @@ function dedupeLimitations(list) {
5868
6102
  function join22(base, seg) {
5869
6103
  return base ? `${base}.${seg}` : seg;
5870
6104
  }
5871
- function normalizePath(path12) {
5872
- return path12.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
6105
+ function normalizePath(path13) {
6106
+ return path13.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
5873
6107
  }
5874
6108
  function fluentChainCalls(call, ts) {
5875
6109
  let root = call;
@@ -6785,7 +7019,7 @@ async function runMap(flags) {
6785
7019
 
6786
7020
  // src/setup.ts
6787
7021
  import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
6788
- import path11 from "path";
7022
+ import path12 from "path";
6789
7023
  var SCAN_COMMAND = "patchstack-connect scan";
6790
7024
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
6791
7025
  function setupProtection(cwd) {
@@ -6803,7 +7037,7 @@ function appendHook(existing, command) {
6803
7037
  return `${existing} && ${command}`;
6804
7038
  }
6805
7039
  function wireBuildScripts(cwd, packageManager) {
6806
- const target = path11.join(cwd, "package.json");
7040
+ const target = path12.join(cwd, "package.json");
6807
7041
  const raw = readFileSync13(target, "utf8");
6808
7042
  const pkg = JSON.parse(raw);
6809
7043
  const scripts = pkg.scripts ?? {};
@@ -7002,6 +7236,10 @@ Options (for scan, setup, status, and uninstall):
7002
7236
  same under a workspace that pins its own copy), so an
7003
7237
  advisory can be matched to the copy your code actually
7004
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
7005
7243
 
7006
7244
  Options (for mark-build):
7007
7245
  --dir <path> Build output directory (default: auto-detect
@@ -7035,7 +7273,7 @@ Examples:
7035
7273
  npx @patchstack/connect demo node-serialize
7036
7274
  npx @patchstack/connect demo-guide node-serialize
7037
7275
  `;
7038
- 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"]);
7039
7277
  function parseArgs(argv) {
7040
7278
  const args = argv.slice(2);
7041
7279
  const positional = [];
@@ -7168,7 +7406,10 @@ async function runScan(args, options = {}) {
7168
7406
  const config = await resolveConfig({
7169
7407
  cwd: process.cwd(),
7170
7408
  cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
7171
- cliEndpoint: getStringFlag(args.flags, "endpoint")
7409
+ cliEndpoint: getStringFlag(args.flags, "endpoint"),
7410
+ cliClaimToken: getStringFlag(args.flags, "claim-token"),
7411
+ // The one command that reports them, so the one command that resolves them.
7412
+ detectSiteIdentity: true
7172
7413
  });
7173
7414
  const manifest = await scanLockfile(process.cwd());
7174
7415
  for (const warning of manifest.warnings ?? []) {
@@ -7200,6 +7441,16 @@ async function runScan(args, options = {}) {
7200
7441
  `${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
7201
7442
  );
7202
7443
  }
7444
+ const body = buildManifestBody(config, payload);
7445
+ if (typeof body.url === "string") {
7446
+ console.log(`Reporting this app's address as ${body.url}.`);
7447
+ }
7448
+ if (typeof body.name === "string") {
7449
+ console.log(`Reporting this app's name as "${body.name}".`);
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
+ }
7203
7454
  if (dryRun) {
7204
7455
  console.log("");
7205
7456
  if (config.siteUuid === null) {
@@ -7208,7 +7459,7 @@ async function runScan(args, options = {}) {
7208
7459
  console.log(`--dry-run: not posting to Patchstack (site UUID ${config.siteUuid}).`);
7209
7460
  }
7210
7461
  console.log("Payload preview:");
7211
- const preview = JSON.stringify(payload, null, 2).split("\n");
7462
+ const preview = JSON.stringify(body, null, 2).split("\n");
7212
7463
  console.log(preview.slice(0, Math.min(preview.length, 30)).join("\n"));
7213
7464
  if (preview.length > 30) {
7214
7465
  console.log(` ... (${preview.length - 30} more lines)`);
@@ -7254,14 +7505,21 @@ async function runScan(args, options = {}) {
7254
7505
  } else {
7255
7506
  console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
7256
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";
7257
7514
  const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
7258
7515
  if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
7259
7516
  reportSourceWidget(effectiveUuid);
7260
7517
  }
7261
- if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
7518
+ const linkUuid = response.uuid ?? config.siteUuid;
7519
+ if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
7262
7520
  console.log("");
7263
7521
  console.log("Open this dashboard link to view vulnerability reports:");
7264
- console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
7522
+ console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
7265
7523
  if (config.endpoint !== DEFAULT_ENDPOINT) {
7266
7524
  console.log(" (this URL inherits the endpoint override above)");
7267
7525
  }