@patchstack/connect 0.5.6 → 0.5.8
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 +2 -2
- package/README.md +4 -4
- package/dist/chunk-4OHAYSV2.js +295 -0
- package/dist/chunk-4OHAYSV2.js.map +1 -0
- package/dist/cli.js +1089 -470
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +450 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +461 -39
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +1 -1
- package/dist/protect.cjs +1226 -244
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +7 -2
- package/dist/protect.d.ts +7 -2
- package/dist/protect.edge.js +992 -179
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +846 -192
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2ALIJGZ3.js → refresh-manifest-IE6SPHG6.js} +168 -41
- package/dist/refresh-manifest-IE6SPHG6.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-IOODP4ZB.js +0 -104
- package/dist/chunk-IOODP4ZB.js.map +0 -1
- package/dist/refresh-manifest-2ALIJGZ3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1195,8 +1195,8 @@ function compareSegments(a, b) {
|
|
|
1195
1195
|
var RULES = [
|
|
1196
1196
|
{ platform: "netlify", any: ["NETLIFY", "NETLIFY_BUILD_BASE", "DEPLOY_PRIME_URL"] },
|
|
1197
1197
|
{ platform: "vercel", any: ["VERCEL", "VERCEL_ENV", "VERCEL_URL"] },
|
|
1198
|
-
{ platform: "cloudflare", any: ["CF_PAGES", "CF_PAGES_URL", "CLOUDFLARE_ACCOUNT_ID"] },
|
|
1199
|
-
{ platform: "aws", any: ["AWS_APP_ID", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV"] },
|
|
1198
|
+
{ platform: "cloudflare", any: ["CF_PAGES", "CF_PAGES_URL", "WORKERS_CI", "WORKERS_CI_BRANCH", "CLOUDFLARE_ACCOUNT_ID"] },
|
|
1199
|
+
{ platform: "aws", any: ["AWS_APP_ID", "AWS_BRANCH", "AWS_LAMBDA_FUNCTION_NAME", "AWS_EXECUTION_ENV"] },
|
|
1200
1200
|
{ platform: "render", any: ["RENDER", "RENDER_SERVICE_ID", "RENDER_EXTERNAL_URL"] },
|
|
1201
1201
|
{ platform: "railway", any: ["RAILWAY_ENVIRONMENT", "RAILWAY_ENVIRONMENT_NAME", "RAILWAY_PROJECT_ID"] },
|
|
1202
1202
|
{ platform: "fly", any: ["FLY_APP_NAME", "FLY_REGION", "FLY_ALLOC_ID"] },
|
|
@@ -1219,6 +1219,128 @@ function detectHostingPlatform(env = process.env) {
|
|
|
1219
1219
|
return { platform: null, evidence: [] };
|
|
1220
1220
|
}
|
|
1221
1221
|
|
|
1222
|
+
// src/protect/safe-origin.js
|
|
1223
|
+
function isSafeOrigin(value) {
|
|
1224
|
+
try {
|
|
1225
|
+
const u = new URL(value);
|
|
1226
|
+
if (u.protocol === "https:") return true;
|
|
1227
|
+
return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]" || u.hostname === "::1");
|
|
1228
|
+
} catch {
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/endpoint-policy.ts
|
|
1234
|
+
function assertConnectableEndpoint(config, target = config.endpoint) {
|
|
1235
|
+
if (config.endpointTrusted === false) {
|
|
1236
|
+
throw new PatchstackError(
|
|
1237
|
+
"The custom endpoint in .patchstackrc.json must be confirmed with --endpoint or PATCHSTACK_ENDPOINT before it can be used.",
|
|
1238
|
+
"CONFIG_INVALID"
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
if (safeRemoteUrl(config.endpoint) === null) {
|
|
1242
|
+
throw new PatchstackError(
|
|
1243
|
+
"Patchstack endpoints must use HTTPS, except for localhost development endpoints.",
|
|
1244
|
+
"CONFIG_INVALID"
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
let endpointOrigin;
|
|
1248
|
+
let targetOrigin;
|
|
1249
|
+
try {
|
|
1250
|
+
endpointOrigin = new URL(config.endpoint).origin;
|
|
1251
|
+
targetOrigin = new URL(target).origin;
|
|
1252
|
+
} catch (cause) {
|
|
1253
|
+
throw new PatchstackError("The configured Patchstack endpoint is not a valid URL.", "CONFIG_INVALID", cause);
|
|
1254
|
+
}
|
|
1255
|
+
if (safeRemoteUrl(target) === null || targetOrigin !== endpointOrigin) {
|
|
1256
|
+
throw new PatchstackError(
|
|
1257
|
+
"A Patchstack request resolved outside the configured endpoint origin.",
|
|
1258
|
+
"CONFIG_INVALID"
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function safeRemoteUrl(value) {
|
|
1263
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
|
|
1264
|
+
try {
|
|
1265
|
+
const parsed = new URL(value);
|
|
1266
|
+
if (!isSafeOrigin(parsed.toString()) || parsed.username !== "" || parsed.password !== "") return null;
|
|
1267
|
+
return parsed.toString();
|
|
1268
|
+
} catch {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function safeDisplayString(value, maxLength = 512) {
|
|
1273
|
+
if (typeof value !== "string") return null;
|
|
1274
|
+
const clean = value.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
|
|
1275
|
+
if (clean.length === 0) return null;
|
|
1276
|
+
return clean.slice(0, maxLength);
|
|
1277
|
+
}
|
|
1278
|
+
function isCanonicalUuid(value) {
|
|
1279
|
+
return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1280
|
+
}
|
|
1281
|
+
function isCredential(value) {
|
|
1282
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) return false;
|
|
1283
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1284
|
+
const code = value.charCodeAt(i);
|
|
1285
|
+
if (code < 33 || code > 126) return false;
|
|
1286
|
+
}
|
|
1287
|
+
const separator = value.lastIndexOf("-");
|
|
1288
|
+
return separator > 0 && separator < value.length - 1 && /^\d+$/.test(value.slice(separator + 1));
|
|
1289
|
+
}
|
|
1290
|
+
function isHeaderValue(value, maxLength = 8192) {
|
|
1291
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false;
|
|
1292
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1293
|
+
const code = value.charCodeAt(i);
|
|
1294
|
+
if (code < 33 || code > 126) return false;
|
|
1295
|
+
}
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// src/bounded-response.ts
|
|
1300
|
+
var MAX_CONTROL_RESPONSE_BYTES = 256 * 1024;
|
|
1301
|
+
async function readBoundedText(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
1302
|
+
const stated2 = Number(response.headers?.get?.("content-length"));
|
|
1303
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) {
|
|
1304
|
+
void response.body?.cancel().catch(() => {
|
|
1305
|
+
});
|
|
1306
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
1307
|
+
}
|
|
1308
|
+
const stream = response.body;
|
|
1309
|
+
if (stream === null || typeof stream.getReader !== "function") {
|
|
1310
|
+
if (stated2 === 0) return "";
|
|
1311
|
+
throw new Error("response body cannot be read with a size bound");
|
|
1312
|
+
}
|
|
1313
|
+
const reader = stream.getReader();
|
|
1314
|
+
const chunks = [];
|
|
1315
|
+
let total = 0;
|
|
1316
|
+
try {
|
|
1317
|
+
for (; ; ) {
|
|
1318
|
+
const { done, value } = await reader.read();
|
|
1319
|
+
if (done) break;
|
|
1320
|
+
total += value.byteLength;
|
|
1321
|
+
if (total > maxBytes) {
|
|
1322
|
+
void reader.cancel().catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
1325
|
+
}
|
|
1326
|
+
chunks.push(value);
|
|
1327
|
+
}
|
|
1328
|
+
} finally {
|
|
1329
|
+
reader.releaseLock();
|
|
1330
|
+
}
|
|
1331
|
+
const bytes = new Uint8Array(total);
|
|
1332
|
+
let offset = 0;
|
|
1333
|
+
for (const chunk of chunks) {
|
|
1334
|
+
bytes.set(chunk, offset);
|
|
1335
|
+
offset += chunk.byteLength;
|
|
1336
|
+
}
|
|
1337
|
+
return new TextDecoder().decode(bytes);
|
|
1338
|
+
}
|
|
1339
|
+
async function readBoundedJson(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
1340
|
+
const text = await readBoundedText(response, maxBytes);
|
|
1341
|
+
return JSON.parse(text);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1222
1344
|
// src/pulse-token.ts
|
|
1223
1345
|
var TOKEN_SKEW_MS = 6e4;
|
|
1224
1346
|
function buildTokenUrl(manifestEndpoint) {
|
|
@@ -1236,22 +1358,48 @@ function parsePulseAuth(credential) {
|
|
|
1236
1358
|
if (!/^\d+$/.test(clientId)) return null;
|
|
1237
1359
|
return { clientId, clientSecret: credential.slice(0, index) };
|
|
1238
1360
|
}
|
|
1239
|
-
var cached =
|
|
1240
|
-
var inflight =
|
|
1241
|
-
function
|
|
1242
|
-
|
|
1361
|
+
var cached = /* @__PURE__ */ new Map();
|
|
1362
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
1363
|
+
function cacheKey(tokenUrl, credential) {
|
|
1364
|
+
return `${tokenUrl}
|
|
1365
|
+
${credential}`;
|
|
1366
|
+
}
|
|
1367
|
+
function clearPulseToken(config) {
|
|
1368
|
+
if (!config || typeof config.pulseAuth !== "string") {
|
|
1369
|
+
cached.clear();
|
|
1370
|
+
inflight.clear();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
const key = cacheKey(buildTokenUrl(config.endpoint), config.pulseAuth);
|
|
1375
|
+
cached.delete(key);
|
|
1376
|
+
inflight.delete(key);
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1243
1379
|
}
|
|
1244
1380
|
async function getPulseToken(config, fetchImpl = fetch) {
|
|
1245
1381
|
if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
|
|
1246
|
-
if (
|
|
1247
|
-
|
|
1382
|
+
if (config.endpointTrusted === false) return null;
|
|
1383
|
+
let tokenUrl;
|
|
1384
|
+
try {
|
|
1385
|
+
tokenUrl = buildTokenUrl(config.endpoint);
|
|
1386
|
+
} catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
if (!isSafeOrigin(tokenUrl)) return null;
|
|
1390
|
+
const key = cacheKey(tokenUrl, config.pulseAuth);
|
|
1391
|
+
const existing = cached.get(key);
|
|
1392
|
+
if (existing !== void 0 && Date.now() < existing.expiresAt - TOKEN_SKEW_MS) {
|
|
1393
|
+
return existing.token;
|
|
1248
1394
|
}
|
|
1249
|
-
|
|
1395
|
+
const pending = inflight.get(key);
|
|
1396
|
+
if (pending !== void 0) return pending;
|
|
1250
1397
|
const credentials = parsePulseAuth(config.pulseAuth);
|
|
1251
1398
|
if (credentials === null) return null;
|
|
1252
|
-
|
|
1399
|
+
let exchange;
|
|
1400
|
+
exchange = (async () => {
|
|
1253
1401
|
try {
|
|
1254
|
-
const response = await fetchImpl(
|
|
1402
|
+
const response = await fetchImpl(tokenUrl, {
|
|
1255
1403
|
method: "POST",
|
|
1256
1404
|
headers: {
|
|
1257
1405
|
"Content-Type": "application/json",
|
|
@@ -1266,25 +1414,27 @@ async function getPulseToken(config, fetchImpl = fetch) {
|
|
|
1266
1414
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
1267
1415
|
});
|
|
1268
1416
|
if (!response.ok) return null;
|
|
1269
|
-
const body = await response
|
|
1270
|
-
if (
|
|
1417
|
+
const body = await readBoundedJson(response);
|
|
1418
|
+
if (!isHeaderValue(body.access_token)) return null;
|
|
1271
1419
|
const expiresIn = Number(body.expires_in);
|
|
1272
1420
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
1273
|
-
cached
|
|
1421
|
+
cached.set(key, { token: body.access_token, expiresAt: Date.now() + ttlMs });
|
|
1274
1422
|
return body.access_token;
|
|
1275
1423
|
} catch {
|
|
1276
1424
|
return null;
|
|
1277
1425
|
} finally {
|
|
1278
|
-
inflight
|
|
1426
|
+
if (inflight.get(key) === exchange) inflight.delete(key);
|
|
1279
1427
|
}
|
|
1280
1428
|
})();
|
|
1281
|
-
|
|
1429
|
+
inflight.set(key, exchange);
|
|
1430
|
+
return exchange;
|
|
1282
1431
|
}
|
|
1283
1432
|
async function pulseAuthHeader(config, fetchImpl = fetch) {
|
|
1284
1433
|
const token = await getPulseToken(config, fetchImpl);
|
|
1285
1434
|
return token === null ? {} : { Authorization: `Bearer ${token}` };
|
|
1286
1435
|
}
|
|
1287
1436
|
async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
1437
|
+
assertConnectableEndpoint(config, url);
|
|
1288
1438
|
const send = async () => {
|
|
1289
1439
|
const auth = await pulseAuthHeader(config, fetchImpl);
|
|
1290
1440
|
const response = await fetchImpl(url, {
|
|
@@ -1295,7 +1445,7 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
1295
1445
|
};
|
|
1296
1446
|
const first = await send();
|
|
1297
1447
|
if (first.response.status === 401 && first.authenticated) {
|
|
1298
|
-
clearPulseToken();
|
|
1448
|
+
clearPulseToken(config);
|
|
1299
1449
|
return (await send()).response;
|
|
1300
1450
|
}
|
|
1301
1451
|
return first.response;
|
|
@@ -1306,7 +1456,7 @@ var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
|
1306
1456
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1307
1457
|
var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
1308
1458
|
function claimTokenHeader(config) {
|
|
1309
|
-
return
|
|
1459
|
+
return config.endpointTrusted !== false && isHeaderValue(config.claimToken) ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1310
1460
|
}
|
|
1311
1461
|
function buildEndpointUrl(base, siteUuid) {
|
|
1312
1462
|
const trimmed = base.replace(/\/$/, "");
|
|
@@ -1345,9 +1495,46 @@ function validationFields(body) {
|
|
|
1345
1495
|
if (typeof errors !== "object" || errors === null || Array.isArray(errors)) return [];
|
|
1346
1496
|
return Object.keys(errors);
|
|
1347
1497
|
}
|
|
1498
|
+
function manifestResponse(body, provisioning) {
|
|
1499
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
|
|
1500
|
+
const value = body;
|
|
1501
|
+
if (typeof value.stored !== "boolean") return null;
|
|
1502
|
+
const uuid = value.uuid === void 0 ? void 0 : isCanonicalUuid(value.uuid) ? value.uuid : null;
|
|
1503
|
+
if (uuid === null || provisioning && uuid === void 0) return null;
|
|
1504
|
+
if (value.api_key !== void 0 && !isCredential(value.api_key)) return null;
|
|
1505
|
+
if (value.manifest_id !== void 0 && (!Number.isInteger(value.manifest_id) || Number(value.manifest_id) <= 0)) return null;
|
|
1506
|
+
const claimValue = value.claim;
|
|
1507
|
+
let claim2;
|
|
1508
|
+
if (claimValue !== void 0) {
|
|
1509
|
+
if (typeof claimValue !== "object" || claimValue === null || Array.isArray(claimValue)) return null;
|
|
1510
|
+
const raw = claimValue;
|
|
1511
|
+
if (!["claimed", "owned-by-you", "owned-by-other", "rejected"].includes(String(raw.state))) return null;
|
|
1512
|
+
const dashboardUrl = raw.dashboard_url === void 0 || raw.dashboard_url === null ? null : safeRemoteUrl(raw.dashboard_url);
|
|
1513
|
+
if (raw.dashboard_url !== void 0 && raw.dashboard_url !== null && dashboardUrl === null) return null;
|
|
1514
|
+
claim2 = {
|
|
1515
|
+
state: raw.state,
|
|
1516
|
+
...Number.isInteger(raw.site_id) ? { site_id: Number(raw.site_id) } : {},
|
|
1517
|
+
...dashboardUrl !== null ? { dashboard_url: dashboardUrl } : {},
|
|
1518
|
+
...raw.reason === "expired" || raw.reason === "invalid" ? { reason: raw.reason } : {}
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const result = {
|
|
1522
|
+
stored: value.stored,
|
|
1523
|
+
...uuid !== void 0 ? { uuid } : {},
|
|
1524
|
+
...typeof value.api_key === "string" ? { api_key: value.api_key } : {},
|
|
1525
|
+
...Number.isInteger(value.manifest_id) ? { manifest_id: Number(value.manifest_id) } : {},
|
|
1526
|
+
...safeDisplayString(value.checksum, 256) !== null ? { checksum: safeDisplayString(value.checksum, 256) } : {},
|
|
1527
|
+
...safeDisplayString(value.reason) !== null ? { reason: safeDisplayString(value.reason) } : {},
|
|
1528
|
+
...safeDisplayString(value.message) !== null ? { message: safeDisplayString(value.message) } : {},
|
|
1529
|
+
...safeDisplayString(value.error) !== null ? { error: safeDisplayString(value.error) } : {},
|
|
1530
|
+
...claim2 !== void 0 ? { claim: claim2 } : {}
|
|
1531
|
+
};
|
|
1532
|
+
return result;
|
|
1533
|
+
}
|
|
1348
1534
|
async function postManifest(config, payload) {
|
|
1349
1535
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
1350
1536
|
const timeoutMs = config.timeoutMs;
|
|
1537
|
+
assertConnectableEndpoint(config, url);
|
|
1351
1538
|
let response;
|
|
1352
1539
|
try {
|
|
1353
1540
|
response = await pulseFetch(config, url, {
|
|
@@ -1375,25 +1562,27 @@ async function postManifest(config, payload) {
|
|
|
1375
1562
|
cause
|
|
1376
1563
|
);
|
|
1377
1564
|
}
|
|
1378
|
-
|
|
1379
|
-
let
|
|
1565
|
+
let text = "";
|
|
1566
|
+
let parsed = null;
|
|
1380
1567
|
try {
|
|
1381
|
-
|
|
1568
|
+
text = await readBoundedText(response);
|
|
1569
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
1382
1570
|
} catch {
|
|
1383
|
-
|
|
1571
|
+
parsed = null;
|
|
1384
1572
|
}
|
|
1573
|
+
const body = manifestResponse(parsed, config.siteUuid === null);
|
|
1385
1574
|
if (response.status === 404) {
|
|
1386
1575
|
throw new PatchstackError(
|
|
1387
|
-
|
|
1576
|
+
safeDisplayString(parsed?.error) ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
|
|
1388
1577
|
"SITE_NOT_FOUND"
|
|
1389
1578
|
);
|
|
1390
1579
|
}
|
|
1391
1580
|
if (response.status === 422) {
|
|
1392
1581
|
const refused2 = new PatchstackError(
|
|
1393
|
-
|
|
1582
|
+
safeDisplayString(parsed?.message) ?? "Patchstack rejected the manifest payload (validation failed).",
|
|
1394
1583
|
"VALIDATION_ERROR"
|
|
1395
1584
|
);
|
|
1396
|
-
refused2.fields = validationFields(
|
|
1585
|
+
refused2.fields = validationFields(parsed);
|
|
1397
1586
|
throw refused2;
|
|
1398
1587
|
}
|
|
1399
1588
|
const refused = authFailureMessage(response.status, config);
|
|
@@ -1402,12 +1591,12 @@ async function postManifest(config, payload) {
|
|
|
1402
1591
|
}
|
|
1403
1592
|
if (response.status < 200 || response.status >= 300) {
|
|
1404
1593
|
throw new PatchstackError(
|
|
1405
|
-
`Patchstack returned ${response.status}: ${text
|
|
1594
|
+
`Patchstack returned ${response.status}: ${safeDisplayString(text, 200) ?? "no response details"}`,
|
|
1406
1595
|
"SERVER_ERROR"
|
|
1407
1596
|
);
|
|
1408
1597
|
}
|
|
1409
1598
|
if (body === null) {
|
|
1410
|
-
throw new PatchstackError("Patchstack returned an
|
|
1599
|
+
throw new PatchstackError("Patchstack returned an unexpected response.", "SERVER_ERROR");
|
|
1411
1600
|
}
|
|
1412
1601
|
return body;
|
|
1413
1602
|
}
|
|
@@ -1419,13 +1608,25 @@ function isTimeoutError(cause) {
|
|
|
1419
1608
|
}
|
|
1420
1609
|
|
|
1421
1610
|
// src/config.ts
|
|
1422
|
-
import { readFile as readFile8,
|
|
1611
|
+
import { readFile as readFile8, chmod, lstat as lstat2, open, rename, unlink } from "fs/promises";
|
|
1423
1612
|
import path8 from "path";
|
|
1613
|
+
import { randomUUID } from "crypto";
|
|
1424
1614
|
|
|
1425
1615
|
// src/environment.ts
|
|
1426
1616
|
import { readFileSync } from "fs";
|
|
1427
1617
|
import path6 from "path";
|
|
1428
1618
|
var set = (value) => value !== void 0 && value !== "";
|
|
1619
|
+
var PRODUCTION_BRANCHES = /* @__PURE__ */ new Set([
|
|
1620
|
+
"main",
|
|
1621
|
+
"master",
|
|
1622
|
+
"production",
|
|
1623
|
+
"prod",
|
|
1624
|
+
"release",
|
|
1625
|
+
"live"
|
|
1626
|
+
]);
|
|
1627
|
+
function environmentFromBranch(variable, branch) {
|
|
1628
|
+
return PRODUCTION_BRANCHES.has(branch.toLowerCase()) ? { environment: "production", evidence: `${variable}=${branch} (a production branch by name)` } : { environment: "sandbox", evidence: `${variable}=${branch} (not a production branch by name)` };
|
|
1629
|
+
}
|
|
1429
1630
|
var DISCRIMINATORS = [
|
|
1430
1631
|
{
|
|
1431
1632
|
platform: "vercel",
|
|
@@ -1454,6 +1655,79 @@ var DISCRIMINATORS = [
|
|
|
1454
1655
|
if (!set(env.RAILWAY_ENVIRONMENT_NAME)) return null;
|
|
1455
1656
|
return env.RAILWAY_ENVIRONMENT_NAME === "production" ? { environment: "production", evidence: "RAILWAY_ENVIRONMENT_NAME=production" } : { environment: "sandbox", evidence: `RAILWAY_ENVIRONMENT_NAME=${env.RAILWAY_ENVIRONMENT_NAME}` };
|
|
1456
1657
|
}
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
// Pages and Workers Builds both name the branch and neither names the tier.
|
|
1661
|
+
platform: "cloudflare",
|
|
1662
|
+
read: (env) => {
|
|
1663
|
+
if (set(env.CF_PAGES) && set(env.CF_PAGES_BRANCH)) {
|
|
1664
|
+
return environmentFromBranch("CF_PAGES_BRANCH", env.CF_PAGES_BRANCH);
|
|
1665
|
+
}
|
|
1666
|
+
if (set(env.WORKERS_CI) && set(env.WORKERS_CI_BRANCH)) {
|
|
1667
|
+
return environmentFromBranch("WORKERS_CI_BRANCH", env.WORKERS_CI_BRANCH);
|
|
1668
|
+
}
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
// Amplify names the branch, and separately whether the build is a pull-request preview.
|
|
1674
|
+
platform: "aws",
|
|
1675
|
+
read: (env) => {
|
|
1676
|
+
if (!set(env.AWS_APP_ID) || !set(env.AWS_BRANCH)) return null;
|
|
1677
|
+
return set(env.AWS_PULL_REQUEST_ID) ? { environment: "sandbox", evidence: "AWS_PULL_REQUEST_ID set (a pull-request preview)" } : environmentFromBranch("AWS_BRANCH", env.AWS_BRANCH);
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
// A Deployment build carries `REPLIT_DEPLOYMENT`; the workspace carries `REPL_ID` alone. Read here,
|
|
1682
|
+
// before the hosted-builder rule, so a workspace build in a Replit project is not its publish step.
|
|
1683
|
+
platform: "replit",
|
|
1684
|
+
read: (env) => {
|
|
1685
|
+
if (set(env.REPLIT_DEPLOYMENT)) {
|
|
1686
|
+
return { environment: "production", evidence: "REPLIT_DEPLOYMENT set (a Replit Deployment build)" };
|
|
1687
|
+
}
|
|
1688
|
+
if (set(env.REPL_ID)) {
|
|
1689
|
+
return { environment: "sandbox", evidence: "REPL_ID set without REPLIT_DEPLOYMENT (the workspace)" };
|
|
1690
|
+
}
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
// GitHub Pages deploys build here. A pull request checks out a merge ref whose name says nothing, so
|
|
1696
|
+
// the event is read first; a tag is a release; otherwise the branch name decides.
|
|
1697
|
+
platform: "github-actions",
|
|
1698
|
+
read: (env) => {
|
|
1699
|
+
if (env.GITHUB_ACTIONS !== "true") return null;
|
|
1700
|
+
if (set(env.GITHUB_EVENT_NAME) && env.GITHUB_EVENT_NAME.startsWith("pull_request")) {
|
|
1701
|
+
return { environment: "sandbox", evidence: `GITHUB_EVENT_NAME=${env.GITHUB_EVENT_NAME} (a pull-request build)` };
|
|
1702
|
+
}
|
|
1703
|
+
if (env.GITHUB_REF_TYPE === "tag") {
|
|
1704
|
+
return { environment: "production", evidence: "GITHUB_REF_TYPE=tag (a tag build)" };
|
|
1705
|
+
}
|
|
1706
|
+
if (!set(env.GITHUB_REF_NAME)) return null;
|
|
1707
|
+
return environmentFromBranch("GITHUB_REF_NAME", env.GITHUB_REF_NAME);
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
// The tier, when a job declares one, is GitLab's own word for it. Without one: a merge request is a
|
|
1712
|
+
// check, a tag is a release, the default branch goes live, and any other branch name decides.
|
|
1713
|
+
platform: "gitlab-ci",
|
|
1714
|
+
read: (env) => {
|
|
1715
|
+
if (env.GITLAB_CI !== "true") return null;
|
|
1716
|
+
if (set(env.CI_ENVIRONMENT_TIER)) {
|
|
1717
|
+
return env.CI_ENVIRONMENT_TIER === "production" ? { environment: "production", evidence: "CI_ENVIRONMENT_TIER=production" } : { environment: "sandbox", evidence: `CI_ENVIRONMENT_TIER=${env.CI_ENVIRONMENT_TIER}` };
|
|
1718
|
+
}
|
|
1719
|
+
if (set(env.CI_MERGE_REQUEST_IID)) {
|
|
1720
|
+
return { environment: "sandbox", evidence: "CI_MERGE_REQUEST_IID set (a merge-request build)" };
|
|
1721
|
+
}
|
|
1722
|
+
if (set(env.CI_COMMIT_TAG)) {
|
|
1723
|
+
return { environment: "production", evidence: "CI_COMMIT_TAG set (a tag build)" };
|
|
1724
|
+
}
|
|
1725
|
+
if (!set(env.CI_COMMIT_BRANCH)) return null;
|
|
1726
|
+
if (set(env.CI_DEFAULT_BRANCH) && env.CI_COMMIT_BRANCH === env.CI_DEFAULT_BRANCH) {
|
|
1727
|
+
return { environment: "production", evidence: `CI_COMMIT_BRANCH=${env.CI_COMMIT_BRANCH} (the default branch)` };
|
|
1728
|
+
}
|
|
1729
|
+
return environmentFromBranch("CI_COMMIT_BRANCH", env.CI_COMMIT_BRANCH);
|
|
1730
|
+
}
|
|
1457
1731
|
}
|
|
1458
1732
|
];
|
|
1459
1733
|
var HOSTED_BUILDER_PACKAGES = [
|
|
@@ -1691,7 +1965,15 @@ async function resolveConfig(options) {
|
|
|
1691
1965
|
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1692
1966
|
const fromEnv = readEnv();
|
|
1693
1967
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1694
|
-
const
|
|
1968
|
+
const endpointRaw = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
1969
|
+
const endpointTrusted = endpointRaw === DEFAULT_ENDPOINT || options.cliEndpoint !== void 0 || fromEnv.endpoint !== void 0 || fromFile.endpoint === void 0;
|
|
1970
|
+
const endpoint = safeRemoteUrl(endpointRaw);
|
|
1971
|
+
if (endpoint === null) {
|
|
1972
|
+
throw new PatchstackError(
|
|
1973
|
+
"Patchstack endpoints must be valid HTTPS URLs, except for localhost development endpoints.",
|
|
1974
|
+
"CONFIG_INVALID"
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1695
1977
|
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1696
1978
|
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
1697
1979
|
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
@@ -1703,7 +1985,7 @@ async function resolveConfig(options) {
|
|
|
1703
1985
|
const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
|
|
1704
1986
|
const environment = environmentRaw ?? inferred.environment;
|
|
1705
1987
|
const environmentEvidence = inferred?.evidence ?? [];
|
|
1706
|
-
if (siteUuid !== null && siteUuid.length > 0 && !
|
|
1988
|
+
if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
|
|
1707
1989
|
throw new PatchstackError(
|
|
1708
1990
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
1709
1991
|
"CONFIG_INVALID"
|
|
@@ -1726,6 +2008,7 @@ async function resolveConfig(options) {
|
|
|
1726
2008
|
siteUrl: identity.url,
|
|
1727
2009
|
siteName: identity.name,
|
|
1728
2010
|
endpoint,
|
|
2011
|
+
endpointTrusted,
|
|
1729
2012
|
timeoutMs,
|
|
1730
2013
|
environment,
|
|
1731
2014
|
environmentEvidence,
|
|
@@ -1736,12 +2019,12 @@ async function resolveConfig(options) {
|
|
|
1736
2019
|
async function writeConfigFile(cwd, config) {
|
|
1737
2020
|
const target = path8.join(cwd, CONFIG_FILENAME);
|
|
1738
2021
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
1739
|
-
await
|
|
2022
|
+
await writeAtomicFile(target, content, 420);
|
|
1740
2023
|
return target;
|
|
1741
2024
|
}
|
|
1742
2025
|
async function writeSecretFile(cwd, secrets) {
|
|
1743
2026
|
const target = path8.join(cwd, SECRET_FILENAME);
|
|
1744
|
-
await
|
|
2027
|
+
await writeAtomicFile(target, JSON.stringify(secrets, null, 2) + "\n", 384);
|
|
1745
2028
|
try {
|
|
1746
2029
|
await chmod(target, 384);
|
|
1747
2030
|
} catch {
|
|
@@ -1749,6 +2032,39 @@ async function writeSecretFile(cwd, secrets) {
|
|
|
1749
2032
|
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1750
2033
|
return { path: target, ...ignore };
|
|
1751
2034
|
}
|
|
2035
|
+
async function writeAtomicFile(target, content, mode) {
|
|
2036
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
2037
|
+
let handle;
|
|
2038
|
+
try {
|
|
2039
|
+
handle = await open(temporary, "wx", mode);
|
|
2040
|
+
await handle.writeFile(content, "utf8");
|
|
2041
|
+
await handle.sync();
|
|
2042
|
+
await handle.close();
|
|
2043
|
+
handle = void 0;
|
|
2044
|
+
try {
|
|
2045
|
+
await rename(temporary, target);
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
const code = error.code;
|
|
2048
|
+
if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
|
|
2049
|
+
try {
|
|
2050
|
+
await unlink(target);
|
|
2051
|
+
} catch (unlinkError) {
|
|
2052
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
2053
|
+
}
|
|
2054
|
+
await rename(temporary, target);
|
|
2055
|
+
}
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
try {
|
|
2058
|
+
await handle?.close();
|
|
2059
|
+
} catch {
|
|
2060
|
+
}
|
|
2061
|
+
try {
|
|
2062
|
+
await unlink(temporary);
|
|
2063
|
+
} catch {
|
|
2064
|
+
}
|
|
2065
|
+
throw error;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1752
2068
|
async function ensureIgnored(cwd, entry) {
|
|
1753
2069
|
const target = path8.join(cwd, ".gitignore");
|
|
1754
2070
|
let existing = "";
|
|
@@ -1767,7 +2083,7 @@ async function ensureIgnored(cwd, entry) {
|
|
|
1767
2083
|
${entry}
|
|
1768
2084
|
`;
|
|
1769
2085
|
try {
|
|
1770
|
-
await
|
|
2086
|
+
await writeAtomicFile(target, existing + block, 420);
|
|
1771
2087
|
} catch (err) {
|
|
1772
2088
|
const code = err.code;
|
|
1773
2089
|
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
@@ -1820,8 +2136,15 @@ async function readJsonFile(cwd, filename) {
|
|
|
1820
2136
|
const target = path8.join(cwd, filename);
|
|
1821
2137
|
let raw;
|
|
1822
2138
|
try {
|
|
2139
|
+
if ((await lstat2(target)).isSymbolicLink()) {
|
|
2140
|
+
throw new PatchstackError(
|
|
2141
|
+
`Could not read ${target}: configuration files must not be symbolic links.`,
|
|
2142
|
+
"CONFIG_INVALID"
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
1823
2145
|
raw = await readFile8(target, "utf8");
|
|
1824
2146
|
} catch (err) {
|
|
2147
|
+
if (err instanceof PatchstackError) throw err;
|
|
1825
2148
|
if (err.code === "ENOENT") {
|
|
1826
2149
|
return {};
|
|
1827
2150
|
}
|
|
@@ -1866,9 +2189,6 @@ function readEnv() {
|
|
|
1866
2189
|
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
1867
2190
|
};
|
|
1868
2191
|
}
|
|
1869
|
-
function isUuid(value) {
|
|
1870
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1871
|
-
}
|
|
1872
2192
|
function isEnvironment(value) {
|
|
1873
2193
|
return value === "production" || value === "sandbox" || value === "local";
|
|
1874
2194
|
}
|
|
@@ -1963,8 +2283,110 @@ function isEmptyStack(stack) {
|
|
|
1963
2283
|
}
|
|
1964
2284
|
|
|
1965
2285
|
// src/widget.ts
|
|
1966
|
-
import { existsSync, readFileSync as
|
|
2286
|
+
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
1967
2287
|
import path9 from "path";
|
|
2288
|
+
|
|
2289
|
+
// src/safe-file.ts
|
|
2290
|
+
import {
|
|
2291
|
+
chmodSync,
|
|
2292
|
+
closeSync,
|
|
2293
|
+
fsyncSync,
|
|
2294
|
+
lstatSync,
|
|
2295
|
+
mkdirSync,
|
|
2296
|
+
openSync,
|
|
2297
|
+
readFileSync as readFileSync2,
|
|
2298
|
+
realpathSync,
|
|
2299
|
+
renameSync,
|
|
2300
|
+
statSync,
|
|
2301
|
+
unlinkSync,
|
|
2302
|
+
writeFileSync
|
|
2303
|
+
} from "fs";
|
|
2304
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2305
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
2306
|
+
function pathError(message) {
|
|
2307
|
+
return Object.assign(new Error(message), { code: "EINVAL" });
|
|
2308
|
+
}
|
|
2309
|
+
function existingEntry(path10) {
|
|
2310
|
+
try {
|
|
2311
|
+
return lstatSync(path10);
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
if (error.code === "ENOENT") return null;
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function assertWritableTarget(target) {
|
|
2318
|
+
const entry = existingEntry(target);
|
|
2319
|
+
if (entry?.isSymbolicLink()) throw pathError(`refusing to replace symbolic link: ${target}`);
|
|
2320
|
+
if (entry && !entry.isFile()) throw pathError(`refusing to replace non-file path: ${target}`);
|
|
2321
|
+
return entry;
|
|
2322
|
+
}
|
|
2323
|
+
function atomicWriteFileSync(target, content, options = {}) {
|
|
2324
|
+
const current = assertWritableTarget(target);
|
|
2325
|
+
const preserveMode = options.preserveMode !== false;
|
|
2326
|
+
const exactMode = options.mode ?? (preserveMode && current ? current.mode & 4095 : options.createMode);
|
|
2327
|
+
const temporary = join(dirname(target), `.${randomUUID2()}.patchstack-tmp`);
|
|
2328
|
+
let descriptor = null;
|
|
2329
|
+
try {
|
|
2330
|
+
descriptor = openSync(temporary, "wx", exactMode ?? 438);
|
|
2331
|
+
writeFileSync(descriptor, content, options.encoding);
|
|
2332
|
+
fsyncSync(descriptor);
|
|
2333
|
+
closeSync(descriptor);
|
|
2334
|
+
descriptor = null;
|
|
2335
|
+
if (exactMode !== void 0) chmodSync(temporary, exactMode);
|
|
2336
|
+
try {
|
|
2337
|
+
renameSync(temporary, target);
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
const code = error.code;
|
|
2340
|
+
if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
|
|
2341
|
+
try {
|
|
2342
|
+
unlinkSync(target);
|
|
2343
|
+
} catch (unlinkError) {
|
|
2344
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
2345
|
+
}
|
|
2346
|
+
renameSync(temporary, target);
|
|
2347
|
+
}
|
|
2348
|
+
} catch (error) {
|
|
2349
|
+
if (descriptor !== null) {
|
|
2350
|
+
try {
|
|
2351
|
+
closeSync(descriptor);
|
|
2352
|
+
} catch {
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
try {
|
|
2356
|
+
unlinkSync(temporary);
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
throw error;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function projectTarget(cwd, target, allowRoot = false) {
|
|
2363
|
+
const logicalRoot = resolve(cwd);
|
|
2364
|
+
const logicalTarget = isAbsolute(target) ? resolve(target) : resolve(logicalRoot, target);
|
|
2365
|
+
const rel = relative(logicalRoot, logicalTarget);
|
|
2366
|
+
if (!allowRoot && rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
2367
|
+
throw pathError(`path is outside the project: ${target}`);
|
|
2368
|
+
}
|
|
2369
|
+
const root = realpathSync(logicalRoot);
|
|
2370
|
+
return { root, path: rel === "" ? root : join(root, rel), parts: rel === "" ? [] : rel.split(sep) };
|
|
2371
|
+
}
|
|
2372
|
+
function assertProjectParents(root, parts) {
|
|
2373
|
+
let current = root;
|
|
2374
|
+
for (const part of parts.slice(0, -1)) {
|
|
2375
|
+
current = join(current, part);
|
|
2376
|
+
const entry = existingEntry(current);
|
|
2377
|
+
if (entry === null) throw Object.assign(new Error(`project directory does not exist: ${current}`), { code: "ENOENT" });
|
|
2378
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2379
|
+
throw pathError(`project path contains a non-directory or symbolic link: ${current}`);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
function writeProjectFileSync(cwd, target, content, options = {}) {
|
|
2384
|
+
const resolved = projectTarget(cwd, target);
|
|
2385
|
+
assertProjectParents(resolved.root, resolved.parts);
|
|
2386
|
+
atomicWriteFileSync(resolved.path, content, options);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/widget.ts
|
|
1968
2390
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
1969
2391
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
1970
2392
|
var WIDGET_NEEDLE = "patchstack-widget";
|
|
@@ -2017,10 +2439,10 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
2017
2439
|
if (!existsSync(file)) {
|
|
2018
2440
|
return { shell: null, action: "no-shell" };
|
|
2019
2441
|
}
|
|
2020
|
-
const before =
|
|
2442
|
+
const before = readFileSync3(file, "utf8");
|
|
2021
2443
|
const { html, action } = ensureWidgetInHtml(before, siteUuid);
|
|
2022
2444
|
if (html !== before) {
|
|
2023
|
-
|
|
2445
|
+
writeProjectFileSync(cwd, file, html, { encoding: "utf8" });
|
|
2024
2446
|
}
|
|
2025
2447
|
return { shell, action };
|
|
2026
2448
|
}
|