@patchstack/connect 0.5.6 → 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/dist/chunk-2K5SHFUM.js +295 -0
- package/dist/chunk-2K5SHFUM.js.map +1 -0
- package/dist/cli.js +1001 -466
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +364 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +375 -37
- 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.js
CHANGED
|
@@ -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,8 +1608,9 @@ 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";
|
|
@@ -1691,7 +1881,15 @@ async function resolveConfig(options) {
|
|
|
1691
1881
|
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1692
1882
|
const fromEnv = readEnv();
|
|
1693
1883
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1694
|
-
const
|
|
1884
|
+
const endpointRaw = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
1885
|
+
const endpointTrusted = endpointRaw === DEFAULT_ENDPOINT || options.cliEndpoint !== void 0 || fromEnv.endpoint !== void 0 || fromFile.endpoint === void 0;
|
|
1886
|
+
const endpoint = safeRemoteUrl(endpointRaw);
|
|
1887
|
+
if (endpoint === null) {
|
|
1888
|
+
throw new PatchstackError(
|
|
1889
|
+
"Patchstack endpoints must be valid HTTPS URLs, except for localhost development endpoints.",
|
|
1890
|
+
"CONFIG_INVALID"
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1695
1893
|
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1696
1894
|
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
1697
1895
|
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
@@ -1703,7 +1901,7 @@ async function resolveConfig(options) {
|
|
|
1703
1901
|
const inferred = environmentRaw === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
|
|
1704
1902
|
const environment = environmentRaw ?? inferred.environment;
|
|
1705
1903
|
const environmentEvidence = inferred?.evidence ?? [];
|
|
1706
|
-
if (siteUuid !== null && siteUuid.length > 0 && !
|
|
1904
|
+
if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
|
|
1707
1905
|
throw new PatchstackError(
|
|
1708
1906
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
1709
1907
|
"CONFIG_INVALID"
|
|
@@ -1726,6 +1924,7 @@ async function resolveConfig(options) {
|
|
|
1726
1924
|
siteUrl: identity.url,
|
|
1727
1925
|
siteName: identity.name,
|
|
1728
1926
|
endpoint,
|
|
1927
|
+
endpointTrusted,
|
|
1729
1928
|
timeoutMs,
|
|
1730
1929
|
environment,
|
|
1731
1930
|
environmentEvidence,
|
|
@@ -1736,12 +1935,12 @@ async function resolveConfig(options) {
|
|
|
1736
1935
|
async function writeConfigFile(cwd, config) {
|
|
1737
1936
|
const target = path8.join(cwd, CONFIG_FILENAME);
|
|
1738
1937
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
1739
|
-
await
|
|
1938
|
+
await writeAtomicFile(target, content, 420);
|
|
1740
1939
|
return target;
|
|
1741
1940
|
}
|
|
1742
1941
|
async function writeSecretFile(cwd, secrets) {
|
|
1743
1942
|
const target = path8.join(cwd, SECRET_FILENAME);
|
|
1744
|
-
await
|
|
1943
|
+
await writeAtomicFile(target, JSON.stringify(secrets, null, 2) + "\n", 384);
|
|
1745
1944
|
try {
|
|
1746
1945
|
await chmod(target, 384);
|
|
1747
1946
|
} catch {
|
|
@@ -1749,6 +1948,39 @@ async function writeSecretFile(cwd, secrets) {
|
|
|
1749
1948
|
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1750
1949
|
return { path: target, ...ignore };
|
|
1751
1950
|
}
|
|
1951
|
+
async function writeAtomicFile(target, content, mode) {
|
|
1952
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
1953
|
+
let handle;
|
|
1954
|
+
try {
|
|
1955
|
+
handle = await open(temporary, "wx", mode);
|
|
1956
|
+
await handle.writeFile(content, "utf8");
|
|
1957
|
+
await handle.sync();
|
|
1958
|
+
await handle.close();
|
|
1959
|
+
handle = void 0;
|
|
1960
|
+
try {
|
|
1961
|
+
await rename(temporary, target);
|
|
1962
|
+
} catch (error) {
|
|
1963
|
+
const code = error.code;
|
|
1964
|
+
if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
|
|
1965
|
+
try {
|
|
1966
|
+
await unlink(target);
|
|
1967
|
+
} catch (unlinkError) {
|
|
1968
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
1969
|
+
}
|
|
1970
|
+
await rename(temporary, target);
|
|
1971
|
+
}
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
try {
|
|
1974
|
+
await handle?.close();
|
|
1975
|
+
} catch {
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
await unlink(temporary);
|
|
1979
|
+
} catch {
|
|
1980
|
+
}
|
|
1981
|
+
throw error;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1752
1984
|
async function ensureIgnored(cwd, entry) {
|
|
1753
1985
|
const target = path8.join(cwd, ".gitignore");
|
|
1754
1986
|
let existing = "";
|
|
@@ -1767,7 +1999,7 @@ async function ensureIgnored(cwd, entry) {
|
|
|
1767
1999
|
${entry}
|
|
1768
2000
|
`;
|
|
1769
2001
|
try {
|
|
1770
|
-
await
|
|
2002
|
+
await writeAtomicFile(target, existing + block, 420);
|
|
1771
2003
|
} catch (err) {
|
|
1772
2004
|
const code = err.code;
|
|
1773
2005
|
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
@@ -1820,8 +2052,15 @@ async function readJsonFile(cwd, filename) {
|
|
|
1820
2052
|
const target = path8.join(cwd, filename);
|
|
1821
2053
|
let raw;
|
|
1822
2054
|
try {
|
|
2055
|
+
if ((await lstat2(target)).isSymbolicLink()) {
|
|
2056
|
+
throw new PatchstackError(
|
|
2057
|
+
`Could not read ${target}: configuration files must not be symbolic links.`,
|
|
2058
|
+
"CONFIG_INVALID"
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
1823
2061
|
raw = await readFile8(target, "utf8");
|
|
1824
2062
|
} catch (err) {
|
|
2063
|
+
if (err instanceof PatchstackError) throw err;
|
|
1825
2064
|
if (err.code === "ENOENT") {
|
|
1826
2065
|
return {};
|
|
1827
2066
|
}
|
|
@@ -1866,9 +2105,6 @@ function readEnv() {
|
|
|
1866
2105
|
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
1867
2106
|
};
|
|
1868
2107
|
}
|
|
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
2108
|
function isEnvironment(value) {
|
|
1873
2109
|
return value === "production" || value === "sandbox" || value === "local";
|
|
1874
2110
|
}
|
|
@@ -1963,8 +2199,110 @@ function isEmptyStack(stack) {
|
|
|
1963
2199
|
}
|
|
1964
2200
|
|
|
1965
2201
|
// src/widget.ts
|
|
1966
|
-
import { existsSync, readFileSync as
|
|
2202
|
+
import { existsSync, readFileSync as readFileSync3 } from "fs";
|
|
1967
2203
|
import path9 from "path";
|
|
2204
|
+
|
|
2205
|
+
// src/safe-file.ts
|
|
2206
|
+
import {
|
|
2207
|
+
chmodSync,
|
|
2208
|
+
closeSync,
|
|
2209
|
+
fsyncSync,
|
|
2210
|
+
lstatSync,
|
|
2211
|
+
mkdirSync,
|
|
2212
|
+
openSync,
|
|
2213
|
+
readFileSync as readFileSync2,
|
|
2214
|
+
realpathSync,
|
|
2215
|
+
renameSync,
|
|
2216
|
+
statSync,
|
|
2217
|
+
unlinkSync,
|
|
2218
|
+
writeFileSync
|
|
2219
|
+
} from "fs";
|
|
2220
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2221
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
2222
|
+
function pathError(message) {
|
|
2223
|
+
return Object.assign(new Error(message), { code: "EINVAL" });
|
|
2224
|
+
}
|
|
2225
|
+
function existingEntry(path10) {
|
|
2226
|
+
try {
|
|
2227
|
+
return lstatSync(path10);
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
if (error.code === "ENOENT") return null;
|
|
2230
|
+
throw error;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
function assertWritableTarget(target) {
|
|
2234
|
+
const entry = existingEntry(target);
|
|
2235
|
+
if (entry?.isSymbolicLink()) throw pathError(`refusing to replace symbolic link: ${target}`);
|
|
2236
|
+
if (entry && !entry.isFile()) throw pathError(`refusing to replace non-file path: ${target}`);
|
|
2237
|
+
return entry;
|
|
2238
|
+
}
|
|
2239
|
+
function atomicWriteFileSync(target, content, options = {}) {
|
|
2240
|
+
const current = assertWritableTarget(target);
|
|
2241
|
+
const preserveMode = options.preserveMode !== false;
|
|
2242
|
+
const exactMode = options.mode ?? (preserveMode && current ? current.mode & 4095 : options.createMode);
|
|
2243
|
+
const temporary = join(dirname(target), `.${randomUUID2()}.patchstack-tmp`);
|
|
2244
|
+
let descriptor = null;
|
|
2245
|
+
try {
|
|
2246
|
+
descriptor = openSync(temporary, "wx", exactMode ?? 438);
|
|
2247
|
+
writeFileSync(descriptor, content, options.encoding);
|
|
2248
|
+
fsyncSync(descriptor);
|
|
2249
|
+
closeSync(descriptor);
|
|
2250
|
+
descriptor = null;
|
|
2251
|
+
if (exactMode !== void 0) chmodSync(temporary, exactMode);
|
|
2252
|
+
try {
|
|
2253
|
+
renameSync(temporary, target);
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
const code = error.code;
|
|
2256
|
+
if (process.platform !== "win32" || code !== "EEXIST" && code !== "EPERM") throw error;
|
|
2257
|
+
try {
|
|
2258
|
+
unlinkSync(target);
|
|
2259
|
+
} catch (unlinkError) {
|
|
2260
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
2261
|
+
}
|
|
2262
|
+
renameSync(temporary, target);
|
|
2263
|
+
}
|
|
2264
|
+
} catch (error) {
|
|
2265
|
+
if (descriptor !== null) {
|
|
2266
|
+
try {
|
|
2267
|
+
closeSync(descriptor);
|
|
2268
|
+
} catch {
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
try {
|
|
2272
|
+
unlinkSync(temporary);
|
|
2273
|
+
} catch {
|
|
2274
|
+
}
|
|
2275
|
+
throw error;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
function projectTarget(cwd, target, allowRoot = false) {
|
|
2279
|
+
const logicalRoot = resolve(cwd);
|
|
2280
|
+
const logicalTarget = isAbsolute(target) ? resolve(target) : resolve(logicalRoot, target);
|
|
2281
|
+
const rel = relative(logicalRoot, logicalTarget);
|
|
2282
|
+
if (!allowRoot && rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
2283
|
+
throw pathError(`path is outside the project: ${target}`);
|
|
2284
|
+
}
|
|
2285
|
+
const root = realpathSync(logicalRoot);
|
|
2286
|
+
return { root, path: rel === "" ? root : join(root, rel), parts: rel === "" ? [] : rel.split(sep) };
|
|
2287
|
+
}
|
|
2288
|
+
function assertProjectParents(root, parts) {
|
|
2289
|
+
let current = root;
|
|
2290
|
+
for (const part of parts.slice(0, -1)) {
|
|
2291
|
+
current = join(current, part);
|
|
2292
|
+
const entry = existingEntry(current);
|
|
2293
|
+
if (entry === null) throw Object.assign(new Error(`project directory does not exist: ${current}`), { code: "ENOENT" });
|
|
2294
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2295
|
+
throw pathError(`project path contains a non-directory or symbolic link: ${current}`);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
function writeProjectFileSync(cwd, target, content, options = {}) {
|
|
2300
|
+
const resolved = projectTarget(cwd, target);
|
|
2301
|
+
assertProjectParents(resolved.root, resolved.parts);
|
|
2302
|
+
atomicWriteFileSync(resolved.path, content, options);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/widget.ts
|
|
1968
2306
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
1969
2307
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
1970
2308
|
var WIDGET_NEEDLE = "patchstack-widget";
|
|
@@ -2017,10 +2355,10 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
2017
2355
|
if (!existsSync(file)) {
|
|
2018
2356
|
return { shell: null, action: "no-shell" };
|
|
2019
2357
|
}
|
|
2020
|
-
const before =
|
|
2358
|
+
const before = readFileSync3(file, "utf8");
|
|
2021
2359
|
const { html, action } = ensureWidgetInHtml(before, siteUuid);
|
|
2022
2360
|
if (html !== before) {
|
|
2023
|
-
|
|
2361
|
+
writeProjectFileSync(cwd, file, html, { encoding: "utf8" });
|
|
2024
2362
|
}
|
|
2025
2363
|
return { shell, action };
|
|
2026
2364
|
}
|