@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/index.d.cts CHANGED
@@ -41,6 +41,22 @@ interface Config {
41
41
  * Falls back to `apiKey` when unset. Prefer `PATCHSTACK_PULSE_AUTH`.
42
42
  */
43
43
  pulseAuth: string | null;
44
+ /**
45
+ * Where this app is published, reported alongside the manifest so a site provisioned without an
46
+ * address can learn one. `null` when nothing reliable is known — a laptop build, or a platform that
47
+ * publishes no production URL.
48
+ *
49
+ * Optional, and absent unless the config was resolved with `detectSiteIdentity`. Only the manifest
50
+ * push reads it, and it is omitted from that push when it is not a string — so a caller holding a
51
+ * `Config` it built itself keeps working without knowing this field exists.
52
+ */
53
+ siteUrl?: string | null;
54
+ /**
55
+ * What this app is called, reported alongside the manifest so a site provisioned nameless can learn a
56
+ * name. `null` when the project states none — a name Patchstack would only be guessing at is not
57
+ * reported. Optional on the same terms as `siteUrl`.
58
+ */
59
+ siteName?: string | null;
44
60
  endpoint: string;
45
61
  timeoutMs: number;
46
62
  /** Environment to report the manifest under. Defaults to 'production'. */
@@ -51,6 +67,27 @@ interface Config {
51
67
  * `"widget": false` in .patchstackrc.json for dependency-scanning only.
52
68
  */
53
69
  widget: boolean;
70
+ /**
71
+ * The claim token the Patchstack dashboard puts in its install prompt, so the site the first scan
72
+ * provisions is born in that account instead of waiting for a dashboard link. Read from
73
+ * `--claim-token` or `PATCHSTACK_CLAIM_TOKEN` and never written to any file: it names an account,
74
+ * not this project, and it stops working within a day. Optional on the same terms as `siteUrl`.
75
+ */
76
+ claimToken?: string | null;
77
+ }
78
+ /**
79
+ * What Patchstack did with a claim token that rode along with a manifest push.
80
+ *
81
+ * `claimed` — this push gave the site its owner; `owned-by-you` — it already was (a re-run of the
82
+ * prompt); `owned-by-other` — the site belongs to a different account and was left alone;
83
+ * `rejected` — the token had expired or was not one Patchstack issued, and the site stays as it was.
84
+ */
85
+ interface ManifestClaimOutcome {
86
+ state: 'claimed' | 'owned-by-you' | 'owned-by-other' | 'rejected';
87
+ site_id?: number | null;
88
+ /** Where the connected site lives in the dashboard; present for `claimed` and `owned-by-you`. */
89
+ dashboard_url?: string | null;
90
+ reason?: 'expired' | 'invalid' | null;
54
91
  }
55
92
  interface StoreManifestResponse {
56
93
  /** The UUID of the site the manifest was stored against. Always returned. */
@@ -66,6 +103,8 @@ interface StoreManifestResponse {
66
103
  reason?: string;
67
104
  message?: string;
68
105
  error?: string;
106
+ /** Present only when the push carried a claim token. */
107
+ claim?: ManifestClaimOutcome;
69
108
  }
70
109
  declare class PatchstackError extends Error {
71
110
  readonly code: 'CONFIG_MISSING' | 'CONFIG_INVALID' | 'LOCKFILE_NOT_FOUND' | 'LOCKFILE_UNSUPPORTED' | 'LOCKFILE_PARSE_ERROR' | 'NETWORK_ERROR' | 'NETWORK_TIMEOUT' | 'SITE_NOT_FOUND' | 'UNAUTHORIZED' | 'VALIDATION_ERROR' | 'SERVER_ERROR';
@@ -172,11 +211,27 @@ interface ConfigFile {
172
211
  timeoutMs?: number;
173
212
  environment?: string;
174
213
  widget?: boolean;
214
+ /**
215
+ * Where this app is published. Committed with the UUID rather than kept secret — it is the address
216
+ * the site already serves to everyone. Set it for platforms whose build environment does not say
217
+ * which deployment is the production one.
218
+ */
219
+ url?: string;
220
+ /**
221
+ * What this app is called in the dashboard. Set it when the project's own files do not say — a
222
+ * `package.json` still named after its template, an HTML shell whose title is filled in by script.
223
+ */
224
+ name?: string;
175
225
  }
176
226
  interface ResolveConfigOptions {
177
227
  cwd: string;
178
228
  cliSiteUuid?: string;
179
229
  cliEndpoint?: string;
230
+ /**
231
+ * The claim token from the dashboard's install prompt (`--claim-token`). Outranks
232
+ * PATCHSTACK_CLAIM_TOKEN, and neither is ever written to a file.
233
+ */
234
+ cliClaimToken?: string;
180
235
  /**
181
236
  * When true, resolveConfig throws CONFIG_MISSING if no site UUID is configured.
182
237
  * Defaults to false: callers that can run without a UUID (the first `scan` after
@@ -184,6 +239,16 @@ interface ResolveConfigOptions {
184
239
  * server response.
185
240
  */
186
241
  requireSiteUuid?: boolean;
242
+ /**
243
+ * Resolve the site's public address and name into `siteUrl` and `siteName`.
244
+ *
245
+ * Off by default, and left off by every command that does not push a manifest. Resolving them reads
246
+ * the project's `index.html` and `package.json` and the host's production-URL variables, and the
247
+ * manifest push is the only thing that reports the result — so `status`, `guide`, `login`,
248
+ * `uninstall`, `mark-build` and the map upload have no reason to look, and do not. Both fields come
249
+ * back `null` when it is off.
250
+ */
251
+ detectSiteIdentity?: boolean;
187
252
  }
188
253
  declare function resolveConfig(options: ResolveConfigOptions): Promise<Config>;
189
254
  declare function writeConfigFile(cwd: string, config: ConfigFile): Promise<string>;
package/dist/index.d.ts CHANGED
@@ -41,6 +41,22 @@ interface Config {
41
41
  * Falls back to `apiKey` when unset. Prefer `PATCHSTACK_PULSE_AUTH`.
42
42
  */
43
43
  pulseAuth: string | null;
44
+ /**
45
+ * Where this app is published, reported alongside the manifest so a site provisioned without an
46
+ * address can learn one. `null` when nothing reliable is known — a laptop build, or a platform that
47
+ * publishes no production URL.
48
+ *
49
+ * Optional, and absent unless the config was resolved with `detectSiteIdentity`. Only the manifest
50
+ * push reads it, and it is omitted from that push when it is not a string — so a caller holding a
51
+ * `Config` it built itself keeps working without knowing this field exists.
52
+ */
53
+ siteUrl?: string | null;
54
+ /**
55
+ * What this app is called, reported alongside the manifest so a site provisioned nameless can learn a
56
+ * name. `null` when the project states none — a name Patchstack would only be guessing at is not
57
+ * reported. Optional on the same terms as `siteUrl`.
58
+ */
59
+ siteName?: string | null;
44
60
  endpoint: string;
45
61
  timeoutMs: number;
46
62
  /** Environment to report the manifest under. Defaults to 'production'. */
@@ -51,6 +67,27 @@ interface Config {
51
67
  * `"widget": false` in .patchstackrc.json for dependency-scanning only.
52
68
  */
53
69
  widget: boolean;
70
+ /**
71
+ * The claim token the Patchstack dashboard puts in its install prompt, so the site the first scan
72
+ * provisions is born in that account instead of waiting for a dashboard link. Read from
73
+ * `--claim-token` or `PATCHSTACK_CLAIM_TOKEN` and never written to any file: it names an account,
74
+ * not this project, and it stops working within a day. Optional on the same terms as `siteUrl`.
75
+ */
76
+ claimToken?: string | null;
77
+ }
78
+ /**
79
+ * What Patchstack did with a claim token that rode along with a manifest push.
80
+ *
81
+ * `claimed` — this push gave the site its owner; `owned-by-you` — it already was (a re-run of the
82
+ * prompt); `owned-by-other` — the site belongs to a different account and was left alone;
83
+ * `rejected` — the token had expired or was not one Patchstack issued, and the site stays as it was.
84
+ */
85
+ interface ManifestClaimOutcome {
86
+ state: 'claimed' | 'owned-by-you' | 'owned-by-other' | 'rejected';
87
+ site_id?: number | null;
88
+ /** Where the connected site lives in the dashboard; present for `claimed` and `owned-by-you`. */
89
+ dashboard_url?: string | null;
90
+ reason?: 'expired' | 'invalid' | null;
54
91
  }
55
92
  interface StoreManifestResponse {
56
93
  /** The UUID of the site the manifest was stored against. Always returned. */
@@ -66,6 +103,8 @@ interface StoreManifestResponse {
66
103
  reason?: string;
67
104
  message?: string;
68
105
  error?: string;
106
+ /** Present only when the push carried a claim token. */
107
+ claim?: ManifestClaimOutcome;
69
108
  }
70
109
  declare class PatchstackError extends Error {
71
110
  readonly code: 'CONFIG_MISSING' | 'CONFIG_INVALID' | 'LOCKFILE_NOT_FOUND' | 'LOCKFILE_UNSUPPORTED' | 'LOCKFILE_PARSE_ERROR' | 'NETWORK_ERROR' | 'NETWORK_TIMEOUT' | 'SITE_NOT_FOUND' | 'UNAUTHORIZED' | 'VALIDATION_ERROR' | 'SERVER_ERROR';
@@ -172,11 +211,27 @@ interface ConfigFile {
172
211
  timeoutMs?: number;
173
212
  environment?: string;
174
213
  widget?: boolean;
214
+ /**
215
+ * Where this app is published. Committed with the UUID rather than kept secret — it is the address
216
+ * the site already serves to everyone. Set it for platforms whose build environment does not say
217
+ * which deployment is the production one.
218
+ */
219
+ url?: string;
220
+ /**
221
+ * What this app is called in the dashboard. Set it when the project's own files do not say — a
222
+ * `package.json` still named after its template, an HTML shell whose title is filled in by script.
223
+ */
224
+ name?: string;
175
225
  }
176
226
  interface ResolveConfigOptions {
177
227
  cwd: string;
178
228
  cliSiteUuid?: string;
179
229
  cliEndpoint?: string;
230
+ /**
231
+ * The claim token from the dashboard's install prompt (`--claim-token`). Outranks
232
+ * PATCHSTACK_CLAIM_TOKEN, and neither is ever written to a file.
233
+ */
234
+ cliClaimToken?: string;
180
235
  /**
181
236
  * When true, resolveConfig throws CONFIG_MISSING if no site UUID is configured.
182
237
  * Defaults to false: callers that can run without a UUID (the first `scan` after
@@ -184,6 +239,16 @@ interface ResolveConfigOptions {
184
239
  * server response.
185
240
  */
186
241
  requireSiteUuid?: boolean;
242
+ /**
243
+ * Resolve the site's public address and name into `siteUrl` and `siteName`.
244
+ *
245
+ * Off by default, and left off by every command that does not push a manifest. Resolving them reads
246
+ * the project's `index.html` and `package.json` and the host's production-URL variables, and the
247
+ * manifest push is the only thing that reports the result — so `status`, `guide`, `login`,
248
+ * `uninstall`, `mark-build` and the map upload have no reason to look, and do not. Both fields come
249
+ * back `null` when it is off.
250
+ */
251
+ detectSiteIdentity?: boolean;
187
252
  }
188
253
  declare function resolveConfig(options: ResolveConfigOptions): Promise<Config>;
189
254
  declare function writeConfigFile(cwd: string, config: ConfigFile): Promise<string>;
package/dist/index.js CHANGED
@@ -1189,8 +1189,8 @@ function compareSegments(a, b) {
1189
1189
  var TOKEN_SKEW_MS = 6e4;
1190
1190
  function buildTokenUrl(manifestEndpoint) {
1191
1191
  const url = new URL(manifestEndpoint);
1192
- const path8 = url.pathname.replace(/\/$/, "");
1193
- url.pathname = path8.endsWith("/manifest") ? `${path8.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1192
+ const path9 = url.pathname.replace(/\/$/, "");
1193
+ url.pathname = path9.endsWith("/manifest") ? `${path9.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
1194
1194
  url.search = "";
1195
1195
  url.hash = "";
1196
1196
  return url.toString();
@@ -1270,6 +1270,10 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
1270
1270
  // src/client.ts
1271
1271
  var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
1272
1272
  var DEFAULT_TIMEOUT_MS = 3e4;
1273
+ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
1274
+ function claimTokenHeader(config) {
1275
+ return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
1276
+ }
1273
1277
  function buildEndpointUrl(base, siteUuid) {
1274
1278
  const trimmed = base.replace(/\/$/, "");
1275
1279
  return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
@@ -1291,6 +1295,14 @@ function buildClaimUrl(endpoint, siteUuid) {
1291
1295
  const origin = new URL(endpoint).origin;
1292
1296
  return `${origin}/monitor/claim?site=${encodeURIComponent(siteUuid)}`;
1293
1297
  }
1298
+ function buildManifestBody(config, payload) {
1299
+ return {
1300
+ ...payload,
1301
+ environment: config.environment,
1302
+ ...typeof config.siteUrl === "string" && config.siteUrl !== "" ? { url: config.siteUrl } : {},
1303
+ ...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {}
1304
+ };
1305
+ }
1294
1306
  async function postManifest(config, payload) {
1295
1307
  const url = buildEndpointUrl(config.endpoint, config.siteUuid);
1296
1308
  const timeoutMs = config.timeoutMs;
@@ -1301,9 +1313,10 @@ async function postManifest(config, payload) {
1301
1313
  headers: {
1302
1314
  "Content-Type": "application/json",
1303
1315
  Accept: "application/json",
1304
- "User-Agent": "@patchstack/connect"
1316
+ "User-Agent": "@patchstack/connect",
1317
+ ...claimTokenHeader(config)
1305
1318
  },
1306
- body: JSON.stringify({ ...payload, environment: config.environment }),
1319
+ body: JSON.stringify(buildManifestBody(config, payload)),
1307
1320
  signal: AbortSignal.timeout(timeoutMs)
1308
1321
  });
1309
1322
  } catch (cause) {
@@ -1362,11 +1375,201 @@ function isTimeoutError(cause) {
1362
1375
  }
1363
1376
 
1364
1377
  // src/config.ts
1365
- import { readFile as readFile7, writeFile, chmod } from "fs/promises";
1378
+ import { readFile as readFile8, writeFile, chmod } from "fs/promises";
1379
+ import path7 from "path";
1380
+
1381
+ // src/site-url.ts
1382
+ var RESERVED_SUFFIXES = [
1383
+ ".local",
1384
+ ".localhost",
1385
+ ".localdomain",
1386
+ ".internal",
1387
+ ".intranet",
1388
+ ".invalid",
1389
+ ".test",
1390
+ ".example",
1391
+ ".home.arpa",
1392
+ ".in-addr.arpa",
1393
+ ".ip6.arpa"
1394
+ ];
1395
+ function isUnpublishableHost(hostname) {
1396
+ const host = hostname.toLowerCase().replace(/\.+$/, "");
1397
+ if (host === "") return true;
1398
+ if (host.startsWith("[")) return true;
1399
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
1400
+ if (!host.includes(".")) return true;
1401
+ return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
1402
+ }
1403
+ function normaliseSiteUrl(value) {
1404
+ const trimmed = (value ?? "").trim();
1405
+ if (trimmed === "") return null;
1406
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
1407
+ let parsed;
1408
+ try {
1409
+ parsed = new URL(withScheme);
1410
+ } catch {
1411
+ return null;
1412
+ }
1413
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
1414
+ if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
1415
+ const origin = `${parsed.protocol}//${parsed.host}`;
1416
+ return origin.length <= 191 ? origin : null;
1417
+ }
1418
+ var PRODUCTION_URL_SOURCES = [
1419
+ {
1420
+ // VERCEL_URL is per-deployment and changes every build, so it is deliberately not read here.
1421
+ platform: "vercel",
1422
+ read: (env) => env.VERCEL_ENV === "production" ? env.VERCEL_PROJECT_PRODUCTION_URL : void 0
1423
+ },
1424
+ {
1425
+ // `URL` is generic enough to belong to anything, so it counts only alongside Netlify's own markers.
1426
+ platform: "netlify",
1427
+ read: (env) => env.NETLIFY === "true" && env.CONTEXT === "production" ? env.URL : void 0
1428
+ },
1429
+ {
1430
+ platform: "render",
1431
+ read: (env) => env.RENDER === "true" && env.IS_PULL_REQUEST !== "true" ? env.RENDER_EXTERNAL_URL : void 0
1432
+ },
1433
+ {
1434
+ platform: "railway",
1435
+ read: (env) => env.RAILWAY_ENVIRONMENT_NAME === "production" ? env.RAILWAY_PUBLIC_DOMAIN : void 0
1436
+ }
1437
+ ];
1438
+ function detectSiteUrl(env = process.env) {
1439
+ for (const source of PRODUCTION_URL_SOURCES) {
1440
+ const url = normaliseSiteUrl(source.read(env));
1441
+ if (url !== null) {
1442
+ return { url, platform: source.platform };
1443
+ }
1444
+ }
1445
+ return null;
1446
+ }
1447
+
1448
+ // src/site-name.ts
1449
+ import { readFile as readFile7 } from "fs/promises";
1366
1450
  import path6 from "path";
1451
+ var NAME_MAX_LENGTH = 191;
1452
+ var TEMPLATE_PACKAGE_NAMES = /* @__PURE__ */ new Set([
1453
+ // Lovable
1454
+ "vite_react_shadcn_ts",
1455
+ // Bolt
1456
+ "vite-react-typescript-starter",
1457
+ // Scaffolders and the names people accept from them
1458
+ "vite-project",
1459
+ "my-app",
1460
+ "my-project",
1461
+ "my-v0-project",
1462
+ "app",
1463
+ "frontend",
1464
+ "web",
1465
+ "client",
1466
+ "project",
1467
+ "template",
1468
+ "example",
1469
+ "react-app",
1470
+ "next-app",
1471
+ "nextjs",
1472
+ "starter",
1473
+ "test"
1474
+ ]);
1475
+ var TEMPLATE_TITLES = /* @__PURE__ */ new Set([
1476
+ "vite + react",
1477
+ "vite + react + ts",
1478
+ "vite + vue",
1479
+ "vite + vue + ts",
1480
+ "vite app",
1481
+ "react app",
1482
+ "create next app",
1483
+ "document",
1484
+ "untitled",
1485
+ "index",
1486
+ "home",
1487
+ "app"
1488
+ ]);
1489
+ var NAMED_ENTITIES = {
1490
+ amp: "&",
1491
+ lt: "<",
1492
+ gt: ">",
1493
+ quot: '"',
1494
+ apos: "'",
1495
+ nbsp: " "
1496
+ };
1497
+ function normaliseSiteName(value) {
1498
+ const collapsed = (value ?? "").replace(/\s+/g, " ").trim();
1499
+ if (collapsed === "") return null;
1500
+ return collapsed.slice(0, NAME_MAX_LENGTH);
1501
+ }
1502
+ function decodeEntities(text) {
1503
+ return text.replace(/&([a-z]+);/gi, (whole, name) => NAMED_ENTITIES[name.toLowerCase()] ?? whole);
1504
+ }
1505
+ function nameFromHtmlTitle(html) {
1506
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html);
1507
+ if (match === null) return null;
1508
+ const name = normaliseSiteName(decodeEntities(match[1] ?? ""));
1509
+ if (name === null || TEMPLATE_TITLES.has(name.toLowerCase())) return null;
1510
+ return name;
1511
+ }
1512
+ function nameFromPackageName(raw) {
1513
+ if (typeof raw !== "string") return null;
1514
+ const unscoped = raw.trim().replace(/^@[^/]+\//, "");
1515
+ if (unscoped === "" || TEMPLATE_PACKAGE_NAMES.has(unscoped.toLowerCase())) return null;
1516
+ const words = unscoped.split(/[-_.\s]+/).filter((word) => word !== "").map((word) => word === word.toUpperCase() ? word : word.charAt(0).toUpperCase() + word.slice(1));
1517
+ return normaliseSiteName(words.join(" "));
1518
+ }
1519
+ async function readIfPresent(file) {
1520
+ try {
1521
+ return await readFile7(file, "utf8");
1522
+ } catch {
1523
+ return null;
1524
+ }
1525
+ }
1526
+ async function detectSiteName(cwd) {
1527
+ for (const shell of ["index.html", path6.join("public", "index.html")]) {
1528
+ const html = await readIfPresent(path6.join(cwd, shell));
1529
+ if (html === null) continue;
1530
+ const name = nameFromHtmlTitle(html);
1531
+ if (name !== null) return { name, source: "index.html" };
1532
+ }
1533
+ const manifest = await readIfPresent(path6.join(cwd, "package.json"));
1534
+ if (manifest !== null) {
1535
+ try {
1536
+ const parsed = JSON.parse(manifest);
1537
+ const name = nameFromPackageName(
1538
+ typeof parsed === "object" && parsed !== null ? parsed.name : void 0
1539
+ );
1540
+ if (name !== null) return { name, source: "package.json" };
1541
+ } catch {
1542
+ }
1543
+ }
1544
+ return null;
1545
+ }
1546
+
1547
+ // src/config.ts
1367
1548
  var CONFIG_FILENAME = ".patchstackrc.json";
1368
1549
  var SECRET_FILENAME = ".patchstackrc.local.json";
1369
1550
  var DEFAULT_ENVIRONMENT = "production";
1551
+ async function resolveSiteIdentity(cwd, fromEnv, fromFile) {
1552
+ const configuredUrl = stated(fromEnv.url) ?? stated(fromFile.url);
1553
+ const configuredName = stated(fromEnv.name) ?? stated(fromFile.name);
1554
+ let url;
1555
+ if (configuredUrl === null) {
1556
+ url = detectSiteUrl(process.env)?.url ?? null;
1557
+ } else {
1558
+ url = normaliseSiteUrl(configuredUrl);
1559
+ if (url === null) {
1560
+ throw new PatchstackError(
1561
+ `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.`,
1562
+ "CONFIG_INVALID"
1563
+ );
1564
+ }
1565
+ }
1566
+ const name = configuredName === null ? (await detectSiteName(cwd))?.name ?? null : normaliseSiteName(configuredName);
1567
+ return { url, name };
1568
+ }
1569
+ function stated(value) {
1570
+ const trimmed = (value ?? "").trim();
1571
+ return trimmed === "" ? null : trimmed;
1572
+ }
1370
1573
  async function resolveConfig(options) {
1371
1574
  const fromFile = await readConfigFile(options.cwd);
1372
1575
  const fromSecretFile = await readSecretFile(options.cwd);
@@ -1396,24 +1599,29 @@ async function resolveConfig(options) {
1396
1599
  }
1397
1600
  const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
1398
1601
  const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
1602
+ const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
1603
+ const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
1399
1604
  return {
1400
1605
  siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
1401
1606
  apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
1402
1607
  pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
1608
+ siteUrl: identity.url,
1609
+ siteName: identity.name,
1403
1610
  endpoint,
1404
1611
  timeoutMs,
1405
1612
  environment,
1406
- widget: fromFile.widget !== false
1613
+ widget: fromFile.widget !== false,
1614
+ claimToken
1407
1615
  };
1408
1616
  }
1409
1617
  async function writeConfigFile(cwd, config) {
1410
- const target = path6.join(cwd, CONFIG_FILENAME);
1618
+ const target = path7.join(cwd, CONFIG_FILENAME);
1411
1619
  const content = JSON.stringify(config, null, 2) + "\n";
1412
1620
  await writeFile(target, content, "utf8");
1413
1621
  return target;
1414
1622
  }
1415
1623
  async function writeSecretFile(cwd, secrets) {
1416
- const target = path6.join(cwd, SECRET_FILENAME);
1624
+ const target = path7.join(cwd, SECRET_FILENAME);
1417
1625
  await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
1418
1626
  try {
1419
1627
  await chmod(target, 384);
@@ -1423,10 +1631,10 @@ async function writeSecretFile(cwd, secrets) {
1423
1631
  return { path: target, ...ignore };
1424
1632
  }
1425
1633
  async function ensureIgnored(cwd, entry) {
1426
- const target = path6.join(cwd, ".gitignore");
1634
+ const target = path7.join(cwd, ".gitignore");
1427
1635
  let existing = "";
1428
1636
  try {
1429
- existing = await readFile7(target, "utf8");
1637
+ existing = await readFile8(target, "utf8");
1430
1638
  } catch (err) {
1431
1639
  const code = err.code;
1432
1640
  if (code !== "ENOENT") {
@@ -1446,7 +1654,7 @@ ${entry}
1446
1654
  return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
1447
1655
  }
1448
1656
  try {
1449
- const written = await readFile7(target, "utf8");
1657
+ const written = await readFile8(target, "utf8");
1450
1658
  if (ignoresEntry(written, entry)) return { ignored: true };
1451
1659
  return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
1452
1660
  } catch (err) {
@@ -1490,10 +1698,10 @@ async function readConfigFile(cwd) {
1490
1698
  return readJsonFile(cwd, CONFIG_FILENAME);
1491
1699
  }
1492
1700
  async function readJsonFile(cwd, filename) {
1493
- const target = path6.join(cwd, filename);
1701
+ const target = path7.join(cwd, filename);
1494
1702
  let raw;
1495
1703
  try {
1496
- raw = await readFile7(target, "utf8");
1704
+ raw = await readFile8(target, "utf8");
1497
1705
  } catch (err) {
1498
1706
  if (err.code === "ENOENT") {
1499
1707
  return {};
@@ -1530,6 +1738,8 @@ function readEnv() {
1530
1738
  const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
1531
1739
  return {
1532
1740
  siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
1741
+ url: process.env.PATCHSTACK_SITE_URL ?? void 0,
1742
+ name: process.env.PATCHSTACK_SITE_NAME ?? void 0,
1533
1743
  apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
1534
1744
  pulseAuth: process.env.PATCHSTACK_PULSE_AUTH ?? void 0,
1535
1745
  endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
@@ -1632,7 +1842,7 @@ function isEmptyStack(stack) {
1632
1842
 
1633
1843
  // src/widget.ts
1634
1844
  import { existsSync, readFileSync, writeFileSync } from "fs";
1635
- import path7 from "path";
1845
+ import path8 from "path";
1636
1846
  var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
1637
1847
  var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
1638
1848
  var WIDGET_NEEDLE = "patchstack-widget";
@@ -1670,7 +1880,7 @@ ${indent}</body>`;
1670
1880
  }
1671
1881
  function findSourceShell(cwd) {
1672
1882
  for (const candidate of SOURCE_SHELL_CANDIDATES) {
1673
- if (existsSync(path7.join(cwd, candidate))) {
1883
+ if (existsSync(path8.join(cwd, candidate))) {
1674
1884
  return candidate;
1675
1885
  }
1676
1886
  }
@@ -1681,7 +1891,7 @@ function ensureSourceWidget(cwd, siteUuid) {
1681
1891
  if (shell === null) {
1682
1892
  return { shell: null, action: "no-shell" };
1683
1893
  }
1684
- const file = path7.join(cwd, shell);
1894
+ const file = path8.join(cwd, shell);
1685
1895
  const before = readFileSync(file, "utf8");
1686
1896
  const { html, action } = ensureWidgetInHtml(before, siteUuid);
1687
1897
  if (html !== before) {
@@ -1693,7 +1903,7 @@ function ensureSourceWidget(cwd, siteUuid) {
1693
1903
  // src/index.ts
1694
1904
  async function scanAndReport(options = {}) {
1695
1905
  const cwd = options.cwd ?? process.cwd();
1696
- const config = options.config ?? await resolveConfig({ cwd });
1906
+ const config = options.config ?? await resolveConfig({ cwd, detectSiteIdentity: true });
1697
1907
  const manifest = await scanLockfile(cwd);
1698
1908
  const { payload, stats } = buildWirePayload(manifest, { installPaths: options.installPaths === true });
1699
1909
  const response = await postManifest(config, payload);