@patchstack/connect 0.5.5 → 0.5.7
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 +1 -1
- package/README.md +1 -0
- package/dist/chunk-2K5SHFUM.js +295 -0
- package/dist/chunk-2K5SHFUM.js.map +1 -0
- package/dist/cli.js +1026 -481
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +368 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +380 -39
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +1 -1
- package/dist/protect.cjs +1135 -241
- 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 +988 -179
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +842 -192
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2ALIJGZ3.js → refresh-manifest-3YWIYENU.js} +82 -39
- package/dist/refresh-manifest-3YWIYENU.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.cjs
CHANGED
|
@@ -1278,6 +1278,128 @@ function detectHostingPlatform(env = process.env) {
|
|
|
1278
1278
|
return { platform: null, evidence: [] };
|
|
1279
1279
|
}
|
|
1280
1280
|
|
|
1281
|
+
// src/protect/safe-origin.js
|
|
1282
|
+
function isSafeOrigin(value) {
|
|
1283
|
+
try {
|
|
1284
|
+
const u = new URL(value);
|
|
1285
|
+
if (u.protocol === "https:") return true;
|
|
1286
|
+
return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]" || u.hostname === "::1");
|
|
1287
|
+
} catch {
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/endpoint-policy.ts
|
|
1293
|
+
function assertConnectableEndpoint(config, target = config.endpoint) {
|
|
1294
|
+
if (config.endpointTrusted === false) {
|
|
1295
|
+
throw new PatchstackError(
|
|
1296
|
+
"The custom endpoint in .patchstackrc.json must be confirmed with --endpoint or PATCHSTACK_ENDPOINT before it can be used.",
|
|
1297
|
+
"CONFIG_INVALID"
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
if (safeRemoteUrl(config.endpoint) === null) {
|
|
1301
|
+
throw new PatchstackError(
|
|
1302
|
+
"Patchstack endpoints must use HTTPS, except for localhost development endpoints.",
|
|
1303
|
+
"CONFIG_INVALID"
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
let endpointOrigin;
|
|
1307
|
+
let targetOrigin;
|
|
1308
|
+
try {
|
|
1309
|
+
endpointOrigin = new URL(config.endpoint).origin;
|
|
1310
|
+
targetOrigin = new URL(target).origin;
|
|
1311
|
+
} catch (cause) {
|
|
1312
|
+
throw new PatchstackError("The configured Patchstack endpoint is not a valid URL.", "CONFIG_INVALID", cause);
|
|
1313
|
+
}
|
|
1314
|
+
if (safeRemoteUrl(target) === null || targetOrigin !== endpointOrigin) {
|
|
1315
|
+
throw new PatchstackError(
|
|
1316
|
+
"A Patchstack request resolved outside the configured endpoint origin.",
|
|
1317
|
+
"CONFIG_INVALID"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function safeRemoteUrl(value) {
|
|
1322
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 2048) return null;
|
|
1323
|
+
try {
|
|
1324
|
+
const parsed = new URL(value);
|
|
1325
|
+
if (!isSafeOrigin(parsed.toString()) || parsed.username !== "" || parsed.password !== "") return null;
|
|
1326
|
+
return parsed.toString();
|
|
1327
|
+
} catch {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function safeDisplayString(value, maxLength = 512) {
|
|
1332
|
+
if (typeof value !== "string") return null;
|
|
1333
|
+
const clean = value.replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g, "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
|
|
1334
|
+
if (clean.length === 0) return null;
|
|
1335
|
+
return clean.slice(0, maxLength);
|
|
1336
|
+
}
|
|
1337
|
+
function isCanonicalUuid(value) {
|
|
1338
|
+
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);
|
|
1339
|
+
}
|
|
1340
|
+
function isCredential(value) {
|
|
1341
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) return false;
|
|
1342
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1343
|
+
const code = value.charCodeAt(i);
|
|
1344
|
+
if (code < 33 || code > 126) return false;
|
|
1345
|
+
}
|
|
1346
|
+
const separator = value.lastIndexOf("-");
|
|
1347
|
+
return separator > 0 && separator < value.length - 1 && /^\d+$/.test(value.slice(separator + 1));
|
|
1348
|
+
}
|
|
1349
|
+
function isHeaderValue(value, maxLength = 8192) {
|
|
1350
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false;
|
|
1351
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1352
|
+
const code = value.charCodeAt(i);
|
|
1353
|
+
if (code < 33 || code > 126) return false;
|
|
1354
|
+
}
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/bounded-response.ts
|
|
1359
|
+
var MAX_CONTROL_RESPONSE_BYTES = 256 * 1024;
|
|
1360
|
+
async function readBoundedText(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
1361
|
+
const stated2 = Number(response.headers?.get?.("content-length"));
|
|
1362
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) {
|
|
1363
|
+
void response.body?.cancel().catch(() => {
|
|
1364
|
+
});
|
|
1365
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
1366
|
+
}
|
|
1367
|
+
const stream = response.body;
|
|
1368
|
+
if (stream === null || typeof stream.getReader !== "function") {
|
|
1369
|
+
if (stated2 === 0) return "";
|
|
1370
|
+
throw new Error("response body cannot be read with a size bound");
|
|
1371
|
+
}
|
|
1372
|
+
const reader = stream.getReader();
|
|
1373
|
+
const chunks = [];
|
|
1374
|
+
let total = 0;
|
|
1375
|
+
try {
|
|
1376
|
+
for (; ; ) {
|
|
1377
|
+
const { done, value } = await reader.read();
|
|
1378
|
+
if (done) break;
|
|
1379
|
+
total += value.byteLength;
|
|
1380
|
+
if (total > maxBytes) {
|
|
1381
|
+
void reader.cancel().catch(() => {
|
|
1382
|
+
});
|
|
1383
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
1384
|
+
}
|
|
1385
|
+
chunks.push(value);
|
|
1386
|
+
}
|
|
1387
|
+
} finally {
|
|
1388
|
+
reader.releaseLock();
|
|
1389
|
+
}
|
|
1390
|
+
const bytes = new Uint8Array(total);
|
|
1391
|
+
let offset = 0;
|
|
1392
|
+
for (const chunk of chunks) {
|
|
1393
|
+
bytes.set(chunk, offset);
|
|
1394
|
+
offset += chunk.byteLength;
|
|
1395
|
+
}
|
|
1396
|
+
return new TextDecoder().decode(bytes);
|
|
1397
|
+
}
|
|
1398
|
+
async function readBoundedJson(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES) {
|
|
1399
|
+
const text = await readBoundedText(response, maxBytes);
|
|
1400
|
+
return JSON.parse(text);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1281
1403
|
// src/pulse-token.ts
|
|
1282
1404
|
var TOKEN_SKEW_MS = 6e4;
|
|
1283
1405
|
function buildTokenUrl(manifestEndpoint) {
|
|
@@ -1295,22 +1417,48 @@ function parsePulseAuth(credential) {
|
|
|
1295
1417
|
if (!/^\d+$/.test(clientId)) return null;
|
|
1296
1418
|
return { clientId, clientSecret: credential.slice(0, index) };
|
|
1297
1419
|
}
|
|
1298
|
-
var cached =
|
|
1299
|
-
var inflight =
|
|
1300
|
-
function
|
|
1301
|
-
|
|
1420
|
+
var cached = /* @__PURE__ */ new Map();
|
|
1421
|
+
var inflight = /* @__PURE__ */ new Map();
|
|
1422
|
+
function cacheKey(tokenUrl, credential) {
|
|
1423
|
+
return `${tokenUrl}
|
|
1424
|
+
${credential}`;
|
|
1425
|
+
}
|
|
1426
|
+
function clearPulseToken(config) {
|
|
1427
|
+
if (!config || typeof config.pulseAuth !== "string") {
|
|
1428
|
+
cached.clear();
|
|
1429
|
+
inflight.clear();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
try {
|
|
1433
|
+
const key = cacheKey(buildTokenUrl(config.endpoint), config.pulseAuth);
|
|
1434
|
+
cached.delete(key);
|
|
1435
|
+
inflight.delete(key);
|
|
1436
|
+
} catch {
|
|
1437
|
+
}
|
|
1302
1438
|
}
|
|
1303
1439
|
async function getPulseToken(config, fetchImpl = fetch) {
|
|
1304
1440
|
if (typeof config.pulseAuth !== "string" || config.pulseAuth.length === 0) return null;
|
|
1305
|
-
if (
|
|
1306
|
-
|
|
1441
|
+
if (config.endpointTrusted === false) return null;
|
|
1442
|
+
let tokenUrl;
|
|
1443
|
+
try {
|
|
1444
|
+
tokenUrl = buildTokenUrl(config.endpoint);
|
|
1445
|
+
} catch {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
if (!isSafeOrigin(tokenUrl)) return null;
|
|
1449
|
+
const key = cacheKey(tokenUrl, config.pulseAuth);
|
|
1450
|
+
const existing = cached.get(key);
|
|
1451
|
+
if (existing !== void 0 && Date.now() < existing.expiresAt - TOKEN_SKEW_MS) {
|
|
1452
|
+
return existing.token;
|
|
1307
1453
|
}
|
|
1308
|
-
|
|
1454
|
+
const pending = inflight.get(key);
|
|
1455
|
+
if (pending !== void 0) return pending;
|
|
1309
1456
|
const credentials = parsePulseAuth(config.pulseAuth);
|
|
1310
1457
|
if (credentials === null) return null;
|
|
1311
|
-
|
|
1458
|
+
let exchange;
|
|
1459
|
+
exchange = (async () => {
|
|
1312
1460
|
try {
|
|
1313
|
-
const response = await fetchImpl(
|
|
1461
|
+
const response = await fetchImpl(tokenUrl, {
|
|
1314
1462
|
method: "POST",
|
|
1315
1463
|
headers: {
|
|
1316
1464
|
"Content-Type": "application/json",
|
|
@@ -1325,25 +1473,27 @@ async function getPulseToken(config, fetchImpl = fetch) {
|
|
|
1325
1473
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
1326
1474
|
});
|
|
1327
1475
|
if (!response.ok) return null;
|
|
1328
|
-
const body = await response
|
|
1329
|
-
if (
|
|
1476
|
+
const body = await readBoundedJson(response);
|
|
1477
|
+
if (!isHeaderValue(body.access_token)) return null;
|
|
1330
1478
|
const expiresIn = Number(body.expires_in);
|
|
1331
1479
|
const ttlMs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn * 1e3 : 36e5;
|
|
1332
|
-
cached
|
|
1480
|
+
cached.set(key, { token: body.access_token, expiresAt: Date.now() + ttlMs });
|
|
1333
1481
|
return body.access_token;
|
|
1334
1482
|
} catch {
|
|
1335
1483
|
return null;
|
|
1336
1484
|
} finally {
|
|
1337
|
-
inflight
|
|
1485
|
+
if (inflight.get(key) === exchange) inflight.delete(key);
|
|
1338
1486
|
}
|
|
1339
1487
|
})();
|
|
1340
|
-
|
|
1488
|
+
inflight.set(key, exchange);
|
|
1489
|
+
return exchange;
|
|
1341
1490
|
}
|
|
1342
1491
|
async function pulseAuthHeader(config, fetchImpl = fetch) {
|
|
1343
1492
|
const token = await getPulseToken(config, fetchImpl);
|
|
1344
1493
|
return token === null ? {} : { Authorization: `Bearer ${token}` };
|
|
1345
1494
|
}
|
|
1346
1495
|
async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
1496
|
+
assertConnectableEndpoint(config, url);
|
|
1347
1497
|
const send = async () => {
|
|
1348
1498
|
const auth = await pulseAuthHeader(config, fetchImpl);
|
|
1349
1499
|
const response = await fetchImpl(url, {
|
|
@@ -1354,7 +1504,7 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
1354
1504
|
};
|
|
1355
1505
|
const first = await send();
|
|
1356
1506
|
if (first.response.status === 401 && first.authenticated) {
|
|
1357
|
-
clearPulseToken();
|
|
1507
|
+
clearPulseToken(config);
|
|
1358
1508
|
return (await send()).response;
|
|
1359
1509
|
}
|
|
1360
1510
|
return first.response;
|
|
@@ -1365,7 +1515,7 @@ var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
|
1365
1515
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1366
1516
|
var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
1367
1517
|
function claimTokenHeader(config) {
|
|
1368
|
-
return
|
|
1518
|
+
return config.endpointTrusted !== false && isHeaderValue(config.claimToken) ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1369
1519
|
}
|
|
1370
1520
|
function buildEndpointUrl(base, siteUuid) {
|
|
1371
1521
|
const trimmed = base.replace(/\/$/, "");
|
|
@@ -1404,9 +1554,46 @@ function validationFields(body) {
|
|
|
1404
1554
|
if (typeof errors !== "object" || errors === null || Array.isArray(errors)) return [];
|
|
1405
1555
|
return Object.keys(errors);
|
|
1406
1556
|
}
|
|
1557
|
+
function manifestResponse(body, provisioning) {
|
|
1558
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
|
|
1559
|
+
const value = body;
|
|
1560
|
+
if (typeof value.stored !== "boolean") return null;
|
|
1561
|
+
const uuid = value.uuid === void 0 ? void 0 : isCanonicalUuid(value.uuid) ? value.uuid : null;
|
|
1562
|
+
if (uuid === null || provisioning && uuid === void 0) return null;
|
|
1563
|
+
if (value.api_key !== void 0 && !isCredential(value.api_key)) return null;
|
|
1564
|
+
if (value.manifest_id !== void 0 && (!Number.isInteger(value.manifest_id) || Number(value.manifest_id) <= 0)) return null;
|
|
1565
|
+
const claimValue = value.claim;
|
|
1566
|
+
let claim2;
|
|
1567
|
+
if (claimValue !== void 0) {
|
|
1568
|
+
if (typeof claimValue !== "object" || claimValue === null || Array.isArray(claimValue)) return null;
|
|
1569
|
+
const raw = claimValue;
|
|
1570
|
+
if (!["claimed", "owned-by-you", "owned-by-other", "rejected"].includes(String(raw.state))) return null;
|
|
1571
|
+
const dashboardUrl = raw.dashboard_url === void 0 || raw.dashboard_url === null ? null : safeRemoteUrl(raw.dashboard_url);
|
|
1572
|
+
if (raw.dashboard_url !== void 0 && raw.dashboard_url !== null && dashboardUrl === null) return null;
|
|
1573
|
+
claim2 = {
|
|
1574
|
+
state: raw.state,
|
|
1575
|
+
...Number.isInteger(raw.site_id) ? { site_id: Number(raw.site_id) } : {},
|
|
1576
|
+
...dashboardUrl !== null ? { dashboard_url: dashboardUrl } : {},
|
|
1577
|
+
...raw.reason === "expired" || raw.reason === "invalid" ? { reason: raw.reason } : {}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
const result = {
|
|
1581
|
+
stored: value.stored,
|
|
1582
|
+
...uuid !== void 0 ? { uuid } : {},
|
|
1583
|
+
...typeof value.api_key === "string" ? { api_key: value.api_key } : {},
|
|
1584
|
+
...Number.isInteger(value.manifest_id) ? { manifest_id: Number(value.manifest_id) } : {},
|
|
1585
|
+
...safeDisplayString(value.checksum, 256) !== null ? { checksum: safeDisplayString(value.checksum, 256) } : {},
|
|
1586
|
+
...safeDisplayString(value.reason) !== null ? { reason: safeDisplayString(value.reason) } : {},
|
|
1587
|
+
...safeDisplayString(value.message) !== null ? { message: safeDisplayString(value.message) } : {},
|
|
1588
|
+
...safeDisplayString(value.error) !== null ? { error: safeDisplayString(value.error) } : {},
|
|
1589
|
+
...claim2 !== void 0 ? { claim: claim2 } : {}
|
|
1590
|
+
};
|
|
1591
|
+
return result;
|
|
1592
|
+
}
|
|
1407
1593
|
async function postManifest(config, payload) {
|
|
1408
1594
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
1409
1595
|
const timeoutMs = config.timeoutMs;
|
|
1596
|
+
assertConnectableEndpoint(config, url);
|
|
1410
1597
|
let response;
|
|
1411
1598
|
try {
|
|
1412
1599
|
response = await pulseFetch(config, url, {
|
|
@@ -1434,25 +1621,27 @@ async function postManifest(config, payload) {
|
|
|
1434
1621
|
cause
|
|
1435
1622
|
);
|
|
1436
1623
|
}
|
|
1437
|
-
|
|
1438
|
-
let
|
|
1624
|
+
let text = "";
|
|
1625
|
+
let parsed = null;
|
|
1439
1626
|
try {
|
|
1440
|
-
|
|
1627
|
+
text = await readBoundedText(response);
|
|
1628
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
1441
1629
|
} catch {
|
|
1442
|
-
|
|
1630
|
+
parsed = null;
|
|
1443
1631
|
}
|
|
1632
|
+
const body = manifestResponse(parsed, config.siteUuid === null);
|
|
1444
1633
|
if (response.status === 404) {
|
|
1445
1634
|
throw new PatchstackError(
|
|
1446
|
-
|
|
1635
|
+
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.",
|
|
1447
1636
|
"SITE_NOT_FOUND"
|
|
1448
1637
|
);
|
|
1449
1638
|
}
|
|
1450
1639
|
if (response.status === 422) {
|
|
1451
1640
|
const refused2 = new PatchstackError(
|
|
1452
|
-
|
|
1641
|
+
safeDisplayString(parsed?.message) ?? "Patchstack rejected the manifest payload (validation failed).",
|
|
1453
1642
|
"VALIDATION_ERROR"
|
|
1454
1643
|
);
|
|
1455
|
-
refused2.fields = validationFields(
|
|
1644
|
+
refused2.fields = validationFields(parsed);
|
|
1456
1645
|
throw refused2;
|
|
1457
1646
|
}
|
|
1458
1647
|
const refused = authFailureMessage(response.status, config);
|
|
@@ -1461,12 +1650,12 @@ async function postManifest(config, payload) {
|
|
|
1461
1650
|
}
|
|
1462
1651
|
if (response.status < 200 || response.status >= 300) {
|
|
1463
1652
|
throw new PatchstackError(
|
|
1464
|
-
`Patchstack returned ${response.status}: ${text
|
|
1653
|
+
`Patchstack returned ${response.status}: ${safeDisplayString(text, 200) ?? "no response details"}`,
|
|
1465
1654
|
"SERVER_ERROR"
|
|
1466
1655
|
);
|
|
1467
1656
|
}
|
|
1468
1657
|
if (body === null) {
|
|
1469
|
-
throw new PatchstackError("Patchstack returned an
|
|
1658
|
+
throw new PatchstackError("Patchstack returned an unexpected response.", "SERVER_ERROR");
|
|
1470
1659
|
}
|
|
1471
1660
|
return body;
|
|
1472
1661
|
}
|
|
@@ -1480,6 +1669,7 @@ function isTimeoutError(cause) {
|
|
|
1480
1669
|
// src/config.ts
|
|
1481
1670
|
var import_promises9 = require("fs/promises");
|
|
1482
1671
|
var import_node_path8 = __toESM(require("path"), 1);
|
|
1672
|
+
var import_node_crypto = require("crypto");
|
|
1483
1673
|
|
|
1484
1674
|
// src/environment.ts
|
|
1485
1675
|
var import_node_fs = require("fs");
|
|
@@ -1750,7 +1940,15 @@ async function resolveConfig(options) {
|
|
|
1750
1940
|
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1751
1941
|
const fromEnv = readEnv();
|
|
1752
1942
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1753
|
-
const
|
|
1943
|
+
const endpointRaw = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
1944
|
+
const endpointTrusted = endpointRaw === DEFAULT_ENDPOINT || options.cliEndpoint !== void 0 || fromEnv.endpoint !== void 0 || fromFile.endpoint === void 0;
|
|
1945
|
+
const endpoint = safeRemoteUrl(endpointRaw);
|
|
1946
|
+
if (endpoint === null) {
|
|
1947
|
+
throw new PatchstackError(
|
|
1948
|
+
"Patchstack endpoints must be valid HTTPS URLs, except for localhost development endpoints.",
|
|
1949
|
+
"CONFIG_INVALID"
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1754
1952
|
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1755
1953
|
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
1756
1954
|
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
@@ -1762,7 +1960,7 @@ async function resolveConfig(options) {
|
|
|
1762
1960
|
const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
|
|
1763
1961
|
const environment = environmentRaw ?? inferred.environment;
|
|
1764
1962
|
const environmentEvidence = inferred?.evidence ?? [];
|
|
1765
|
-
if (siteUuid !== null && siteUuid.length > 0 && !
|
|
1963
|
+
if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
|
|
1766
1964
|
throw new PatchstackError(
|
|
1767
1965
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
1768
1966
|
"CONFIG_INVALID"
|
|
@@ -1785,6 +1983,7 @@ async function resolveConfig(options) {
|
|
|
1785
1983
|
siteUrl: identity.url,
|
|
1786
1984
|
siteName: identity.name,
|
|
1787
1985
|
endpoint,
|
|
1986
|
+
endpointTrusted,
|
|
1788
1987
|
timeoutMs,
|
|
1789
1988
|
environment,
|
|
1790
1989
|
environmentEvidence,
|
|
@@ -1795,12 +1994,12 @@ async function resolveConfig(options) {
|
|
|
1795
1994
|
async function writeConfigFile(cwd, config) {
|
|
1796
1995
|
const target = import_node_path8.default.join(cwd, CONFIG_FILENAME);
|
|
1797
1996
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
1798
|
-
await (
|
|
1997
|
+
await writeAtomicFile(target, content, 420);
|
|
1799
1998
|
return target;
|
|
1800
1999
|
}
|
|
1801
2000
|
async function writeSecretFile(cwd, secrets) {
|
|
1802
2001
|
const target = import_node_path8.default.join(cwd, SECRET_FILENAME);
|
|
1803
|
-
await (
|
|
2002
|
+
await writeAtomicFile(target, JSON.stringify(secrets, null, 2) + "\n", 384);
|
|
1804
2003
|
try {
|
|
1805
2004
|
await (0, import_promises9.chmod)(target, 384);
|
|
1806
2005
|
} catch {
|
|
@@ -1808,6 +2007,39 @@ async function writeSecretFile(cwd, secrets) {
|
|
|
1808
2007
|
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1809
2008
|
return { path: target, ...ignore };
|
|
1810
2009
|
}
|
|
2010
|
+
async function writeAtomicFile(target, content, mode) {
|
|
2011
|
+
const temporary = `${target}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`;
|
|
2012
|
+
let handle;
|
|
2013
|
+
try {
|
|
2014
|
+
handle = await (0, import_promises9.open)(temporary, "wx", mode);
|
|
2015
|
+
await handle.writeFile(content, "utf8");
|
|
2016
|
+
await handle.sync();
|
|
2017
|
+
await handle.close();
|
|
2018
|
+
handle = void 0;
|
|
2019
|
+
try {
|
|
2020
|
+
await (0, import_promises9.rename)(temporary, target);
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
const code = error.code;
|
|
2023
|
+
if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
|
|
2024
|
+
try {
|
|
2025
|
+
await (0, import_promises9.unlink)(target);
|
|
2026
|
+
} catch (unlinkError) {
|
|
2027
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
2028
|
+
}
|
|
2029
|
+
await (0, import_promises9.rename)(temporary, target);
|
|
2030
|
+
}
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
try {
|
|
2033
|
+
await handle?.close();
|
|
2034
|
+
} catch {
|
|
2035
|
+
}
|
|
2036
|
+
try {
|
|
2037
|
+
await (0, import_promises9.unlink)(temporary);
|
|
2038
|
+
} catch {
|
|
2039
|
+
}
|
|
2040
|
+
throw error;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1811
2043
|
async function ensureIgnored(cwd, entry) {
|
|
1812
2044
|
const target = import_node_path8.default.join(cwd, ".gitignore");
|
|
1813
2045
|
let existing = "";
|
|
@@ -1826,7 +2058,7 @@ async function ensureIgnored(cwd, entry) {
|
|
|
1826
2058
|
${entry}
|
|
1827
2059
|
`;
|
|
1828
2060
|
try {
|
|
1829
|
-
await (
|
|
2061
|
+
await writeAtomicFile(target, existing + block, 420);
|
|
1830
2062
|
} catch (err) {
|
|
1831
2063
|
const code = err.code;
|
|
1832
2064
|
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
@@ -1879,8 +2111,15 @@ async function readJsonFile(cwd, filename) {
|
|
|
1879
2111
|
const target = import_node_path8.default.join(cwd, filename);
|
|
1880
2112
|
let raw;
|
|
1881
2113
|
try {
|
|
2114
|
+
if ((await (0, import_promises9.lstat)(target)).isSymbolicLink()) {
|
|
2115
|
+
throw new PatchstackError(
|
|
2116
|
+
`Could not read ${target}: configuration files must not be symbolic links.`,
|
|
2117
|
+
"CONFIG_INVALID"
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
1882
2120
|
raw = await (0, import_promises9.readFile)(target, "utf8");
|
|
1883
2121
|
} catch (err) {
|
|
2122
|
+
if (err instanceof PatchstackError) throw err;
|
|
1884
2123
|
if (err.code === "ENOENT") {
|
|
1885
2124
|
return {};
|
|
1886
2125
|
}
|
|
@@ -1925,9 +2164,6 @@ function readEnv() {
|
|
|
1925
2164
|
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
1926
2165
|
};
|
|
1927
2166
|
}
|
|
1928
|
-
function isUuid(value) {
|
|
1929
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
1930
|
-
}
|
|
1931
2167
|
function isEnvironment(value) {
|
|
1932
2168
|
return value === "production" || value === "sandbox" || value === "local";
|
|
1933
2169
|
}
|
|
@@ -2022,8 +2258,97 @@ function isEmptyStack(stack) {
|
|
|
2022
2258
|
}
|
|
2023
2259
|
|
|
2024
2260
|
// src/widget.ts
|
|
2261
|
+
var import_node_fs3 = require("fs");
|
|
2262
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2263
|
+
|
|
2264
|
+
// src/safe-file.ts
|
|
2025
2265
|
var import_node_fs2 = require("fs");
|
|
2026
|
-
var
|
|
2266
|
+
var import_node_crypto2 = require("crypto");
|
|
2267
|
+
var import_node_path9 = require("path");
|
|
2268
|
+
function pathError(message) {
|
|
2269
|
+
return Object.assign(new Error(message), { code: "EINVAL" });
|
|
2270
|
+
}
|
|
2271
|
+
function existingEntry(path10) {
|
|
2272
|
+
try {
|
|
2273
|
+
return (0, import_node_fs2.lstatSync)(path10);
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
if (error.code === "ENOENT") return null;
|
|
2276
|
+
throw error;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function assertWritableTarget(target) {
|
|
2280
|
+
const entry = existingEntry(target);
|
|
2281
|
+
if (entry?.isSymbolicLink()) throw pathError(`refusing to replace symbolic link: ${target}`);
|
|
2282
|
+
if (entry && !entry.isFile()) throw pathError(`refusing to replace non-file path: ${target}`);
|
|
2283
|
+
return entry;
|
|
2284
|
+
}
|
|
2285
|
+
function atomicWriteFileSync(target, content, options = {}) {
|
|
2286
|
+
const current = assertWritableTarget(target);
|
|
2287
|
+
const preserveMode = options.preserveMode !== false;
|
|
2288
|
+
const exactMode = options.mode ?? (preserveMode && current ? current.mode & 4095 : options.createMode);
|
|
2289
|
+
const temporary = (0, import_node_path9.join)((0, import_node_path9.dirname)(target), `.${(0, import_node_crypto2.randomUUID)()}.patchstack-tmp`);
|
|
2290
|
+
let descriptor = null;
|
|
2291
|
+
try {
|
|
2292
|
+
descriptor = (0, import_node_fs2.openSync)(temporary, "wx", exactMode ?? 438);
|
|
2293
|
+
(0, import_node_fs2.writeFileSync)(descriptor, content, options.encoding);
|
|
2294
|
+
(0, import_node_fs2.fsyncSync)(descriptor);
|
|
2295
|
+
(0, import_node_fs2.closeSync)(descriptor);
|
|
2296
|
+
descriptor = null;
|
|
2297
|
+
if (exactMode !== void 0) (0, import_node_fs2.chmodSync)(temporary, exactMode);
|
|
2298
|
+
try {
|
|
2299
|
+
(0, import_node_fs2.renameSync)(temporary, target);
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
const code = error.code;
|
|
2302
|
+
if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
|
|
2303
|
+
try {
|
|
2304
|
+
(0, import_node_fs2.unlinkSync)(target);
|
|
2305
|
+
} catch (unlinkError) {
|
|
2306
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
2307
|
+
}
|
|
2308
|
+
(0, import_node_fs2.renameSync)(temporary, target);
|
|
2309
|
+
}
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
if (descriptor !== null) {
|
|
2312
|
+
try {
|
|
2313
|
+
(0, import_node_fs2.closeSync)(descriptor);
|
|
2314
|
+
} catch {
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
try {
|
|
2318
|
+
(0, import_node_fs2.unlinkSync)(temporary);
|
|
2319
|
+
} catch {
|
|
2320
|
+
}
|
|
2321
|
+
throw error;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
function projectTarget(cwd, target, allowRoot = false) {
|
|
2325
|
+
const logicalRoot = (0, import_node_path9.resolve)(cwd);
|
|
2326
|
+
const logicalTarget = (0, import_node_path9.isAbsolute)(target) ? (0, import_node_path9.resolve)(target) : (0, import_node_path9.resolve)(logicalRoot, target);
|
|
2327
|
+
const rel = (0, import_node_path9.relative)(logicalRoot, logicalTarget);
|
|
2328
|
+
if (!allowRoot && rel === "" || rel === ".." || rel.startsWith(`..${import_node_path9.sep}`) || (0, import_node_path9.isAbsolute)(rel)) {
|
|
2329
|
+
throw pathError(`path is outside the project: ${target}`);
|
|
2330
|
+
}
|
|
2331
|
+
const root = (0, import_node_fs2.realpathSync)(logicalRoot);
|
|
2332
|
+
return { root, path: rel === "" ? root : (0, import_node_path9.join)(root, rel), parts: rel === "" ? [] : rel.split(import_node_path9.sep) };
|
|
2333
|
+
}
|
|
2334
|
+
function assertProjectParents(root, parts) {
|
|
2335
|
+
let current = root;
|
|
2336
|
+
for (const part of parts.slice(0, -1)) {
|
|
2337
|
+
current = (0, import_node_path9.join)(current, part);
|
|
2338
|
+
const entry = existingEntry(current);
|
|
2339
|
+
if (entry === null) throw Object.assign(new Error(`project directory does not exist: ${current}`), { code: "ENOENT" });
|
|
2340
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2341
|
+
throw pathError(`project path contains a non-directory or symbolic link: ${current}`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
function writeProjectFileSync(cwd, target, content, options = {}) {
|
|
2346
|
+
const resolved = projectTarget(cwd, target);
|
|
2347
|
+
assertProjectParents(resolved.root, resolved.parts);
|
|
2348
|
+
atomicWriteFileSync(resolved.path, content, options);
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// src/widget.ts
|
|
2027
2352
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
2028
2353
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
2029
2354
|
var WIDGET_NEEDLE = "patchstack-widget";
|
|
@@ -2061,22 +2386,25 @@ ${indent}</body>`;
|
|
|
2061
2386
|
}
|
|
2062
2387
|
function findSourceShell(cwd) {
|
|
2063
2388
|
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
2064
|
-
if ((0,
|
|
2389
|
+
if ((0, import_node_fs3.existsSync)(import_node_path10.default.join(cwd, candidate))) {
|
|
2065
2390
|
return candidate;
|
|
2066
2391
|
}
|
|
2067
2392
|
}
|
|
2068
2393
|
return null;
|
|
2069
2394
|
}
|
|
2070
|
-
function ensureSourceWidget(cwd, siteUuid) {
|
|
2071
|
-
const shell = findSourceShell(cwd);
|
|
2395
|
+
function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
2396
|
+
const shell = findSourceShell(cwd) ?? jsxShell;
|
|
2072
2397
|
if (shell === null) {
|
|
2073
2398
|
return { shell: null, action: "no-shell" };
|
|
2074
2399
|
}
|
|
2075
|
-
const file =
|
|
2076
|
-
|
|
2400
|
+
const file = import_node_path10.default.join(cwd, shell);
|
|
2401
|
+
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
2402
|
+
return { shell: null, action: "no-shell" };
|
|
2403
|
+
}
|
|
2404
|
+
const before = (0, import_node_fs3.readFileSync)(file, "utf8");
|
|
2077
2405
|
const { html, action } = ensureWidgetInHtml(before, siteUuid);
|
|
2078
2406
|
if (html !== before) {
|
|
2079
|
-
(
|
|
2407
|
+
writeProjectFileSync(cwd, file, html, { encoding: "utf8" });
|
|
2080
2408
|
}
|
|
2081
2409
|
return { shell, action };
|
|
2082
2410
|
}
|