@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.
- package/AGENT-INSTALL.md +24 -4
- package/README.md +27 -3
- package/dist/cli.js +319 -61
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +230 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +227 -17
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +22 -1
- package/dist/protect/templates/express-guard.cjs +54 -4
- package/dist/protect/templates/express-guard.js +54 -4
- package/dist/protect/templates/express-guard.ts +43 -5
- package/dist/protect/templates/fastify-plugin.cjs +41 -3
- package/dist/protect/templates/fastify-plugin.js +41 -3
- package/dist/protect/templates/fastify-plugin.ts +30 -2
- package/dist/protect/templates/generic-guard.cjs +58 -5
- package/dist/protect/templates/generic-guard.js +58 -5
- package/dist/protect/templates/generic-guard.ts +47 -4
- package/dist/protect/templates/guard.ts +42 -8
- package/dist/protect/templates/next-middleware.ts +22 -1
- package/dist/protect/templates/nuxt-middleware.ts +22 -1
- package/dist/protect/templates/sveltekit-hooks.ts +22 -1
- package/dist/protect.cjs +351 -97
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.edge.js +56 -24
- package/dist/protect.edge.js.map +1 -1
- package/dist/protect.js +57 -25
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-JNCTIAM5.js → refresh-manifest-2MMPQN2A.js} +243 -35
- package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
- package/package.json +3 -3
- 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
|
|
1193
|
-
url.pathname =
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1634
|
+
const target = path7.join(cwd, ".gitignore");
|
|
1427
1635
|
let existing = "";
|
|
1428
1636
|
try {
|
|
1429
|
-
existing = await
|
|
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
|
|
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 =
|
|
1701
|
+
const target = path7.join(cwd, filename);
|
|
1494
1702
|
let raw;
|
|
1495
1703
|
try {
|
|
1496
|
-
raw = await
|
|
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
|
|
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(
|
|
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 =
|
|
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);
|