@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/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 = null;
1240
- var inflight = null;
1241
- function clearPulseToken() {
1242
- cached = null;
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 (cached !== null && Date.now() < cached.expiresAt - TOKEN_SKEW_MS) {
1247
- return cached.token;
1382
+ if (config.endpointTrusted === false) return null;
1383
+ let tokenUrl;
1384
+ try {
1385
+ tokenUrl = buildTokenUrl(config.endpoint);
1386
+ } catch {
1387
+ return null;
1248
1388
  }
1249
- if (inflight !== null) return inflight;
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;
1394
+ }
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
- inflight = (async () => {
1399
+ let exchange;
1400
+ exchange = (async () => {
1253
1401
  try {
1254
- const response = await fetchImpl(buildTokenUrl(config.endpoint), {
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.json();
1270
- if (typeof body.access_token !== "string" || body.access_token.length === 0) return null;
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 = { token: body.access_token, expiresAt: Date.now() + ttlMs };
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 = null;
1426
+ if (inflight.get(key) === exchange) inflight.delete(key);
1279
1427
  }
1280
1428
  })();
1281
- return inflight;
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 typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
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
- const text = await response.text();
1379
- let body = null;
1565
+ let text = "";
1566
+ let parsed = null;
1380
1567
  try {
1381
- body = text.length > 0 ? JSON.parse(text) : null;
1568
+ text = await readBoundedText(response);
1569
+ parsed = text.length > 0 ? JSON.parse(text) : null;
1382
1570
  } catch {
1383
- body = null;
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
- body?.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.",
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
- body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
1582
+ safeDisplayString(parsed?.message) ?? "Patchstack rejected the manifest payload (validation failed).",
1394
1583
  "VALIDATION_ERROR"
1395
1584
  );
1396
- refused2.fields = validationFields(body);
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.slice(0, 200)}`,
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 empty response.", "SERVER_ERROR");
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, writeFile, chmod } from "fs/promises";
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 endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
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 && !isUuid(siteUuid)) {
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 writeFile(target, content, "utf8");
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 writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
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 writeFile(target, existing + block, "utf8");
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 readFileSync2, writeFileSync } from "fs";
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";
@@ -2008,16 +2346,19 @@ function findSourceShell(cwd) {
2008
2346
  }
2009
2347
  return null;
2010
2348
  }
2011
- function ensureSourceWidget(cwd, siteUuid) {
2012
- const shell = findSourceShell(cwd);
2349
+ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
2350
+ const shell = findSourceShell(cwd) ?? jsxShell;
2013
2351
  if (shell === null) {
2014
2352
  return { shell: null, action: "no-shell" };
2015
2353
  }
2016
2354
  const file = path9.join(cwd, shell);
2017
- const before = readFileSync2(file, "utf8");
2355
+ if (!existsSync(file)) {
2356
+ return { shell: null, action: "no-shell" };
2357
+ }
2358
+ const before = readFileSync3(file, "utf8");
2018
2359
  const { html, action } = ensureWidgetInHtml(before, siteUuid);
2019
2360
  if (html !== before) {
2020
- writeFileSync(file, html);
2361
+ writeProjectFileSync(cwd, file, html, { encoding: "utf8" });
2021
2362
  }
2022
2363
  return { shell, action };
2023
2364
  }