syncorejs 0.2.3 → 0.2.5

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.
Files changed (94) hide show
  1. package/dist/_dashboard/assets/ConfirmActionDialog-Db4VzVp6.js +1 -0
  2. package/dist/_dashboard/assets/circle-x-VsB4Z8W4.js +1 -0
  3. package/dist/_dashboard/assets/data.lazy-DjdU9CzX.js +18 -0
  4. package/dist/_dashboard/assets/file-code-BrOKjG4n.js +1 -0
  5. package/dist/_dashboard/assets/functions.lazy-DvDwAGHq.js +1 -0
  6. package/dist/_dashboard/assets/funnel-BH8EMMJI.js +1 -0
  7. package/dist/_dashboard/assets/index-DT9ZEELb.css +1 -0
  8. package/dist/_dashboard/assets/index-DrSG4qZZ.js +54 -0
  9. package/dist/_dashboard/assets/loader-circle-CmJFSYga.js +1 -0
  10. package/dist/_dashboard/assets/logs.lazy-50KTk5yd.js +1 -0
  11. package/dist/_dashboard/assets/play-DS52VsLN.js +1 -0
  12. package/dist/_dashboard/assets/queries.lazy-CfysRWkz.js +1 -0
  13. package/dist/_dashboard/assets/scheduler.lazy-BB88mZk-.js +1 -0
  14. package/dist/_dashboard/assets/select-THYcR8Wt.js +1 -0
  15. package/dist/_dashboard/assets/separator-BU7xg615.js +1 -0
  16. package/dist/_dashboard/assets/shared-Bh0wwC2k.js +1 -0
  17. package/dist/_dashboard/assets/sql.lazy-CHtU9Qnt.js +13 -0
  18. package/dist/_dashboard/assets/storage.lazy-CneN7wVU.js +1 -0
  19. package/dist/_dashboard/assets/table-2-CH8JoMXf.js +1 -0
  20. package/dist/_dashboard/index.html +18 -0
  21. package/dist/_vendor/cli/app.d.mts.map +1 -1
  22. package/dist/_vendor/cli/app.mjs +16 -5
  23. package/dist/_vendor/cli/app.mjs.map +1 -1
  24. package/dist/_vendor/core/cli.d.mts.map +1 -1
  25. package/dist/_vendor/core/cli.mjs +358 -16
  26. package/dist/_vendor/core/cli.mjs.map +1 -1
  27. package/dist/_vendor/core/index.d.mts +3 -3
  28. package/dist/_vendor/core/runtime/devtools.d.mts.map +1 -1
  29. package/dist/_vendor/core/runtime/devtools.mjs +131 -0
  30. package/dist/_vendor/core/runtime/devtools.mjs.map +1 -1
  31. package/dist/_vendor/core/runtime/functions.d.mts +3 -3
  32. package/dist/_vendor/core/runtime/functions.mjs.map +1 -1
  33. package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs +1 -1
  34. package/dist/_vendor/core/runtime/internal/engines/devtoolsEngine.mjs.map +1 -1
  35. package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs +4 -1
  36. package/dist/_vendor/core/runtime/internal/engines/executionEngine.mjs.map +1 -1
  37. package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs +6 -3
  38. package/dist/_vendor/core/runtime/internal/engines/reactivityEngine.mjs.map +1 -1
  39. package/dist/_vendor/core/runtime/internal/engines/shared.mjs +5 -1
  40. package/dist/_vendor/core/runtime/internal/engines/shared.mjs.map +1 -1
  41. package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs +99 -13
  42. package/dist/_vendor/core/runtime/internal/engines/storageEngine.mjs.map +1 -1
  43. package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs +38 -4
  44. package/dist/_vendor/core/runtime/internal/runtimeKernel.mjs.map +1 -1
  45. package/dist/_vendor/core/runtime/runtime.d.mts +65 -8
  46. package/dist/_vendor/core/runtime/runtime.d.mts.map +1 -1
  47. package/dist/_vendor/core/runtime/runtime.mjs.map +1 -1
  48. package/dist/_vendor/core/transport.d.mts.map +1 -1
  49. package/dist/_vendor/core/transport.mjs +30 -5
  50. package/dist/_vendor/core/transport.mjs.map +1 -1
  51. package/dist/_vendor/devtools-protocol/index.d.ts +75 -1
  52. package/dist/_vendor/devtools-protocol/index.d.ts.map +1 -1
  53. package/dist/_vendor/devtools-protocol/index.js.map +1 -1
  54. package/dist/_vendor/next/index.js +9 -1
  55. package/dist/_vendor/next/index.js.map +1 -1
  56. package/dist/_vendor/platform-expo/index.d.ts +1 -1
  57. package/dist/_vendor/platform-expo/index.d.ts.map +1 -1
  58. package/dist/_vendor/platform-expo/index.js +6 -1
  59. package/dist/_vendor/platform-expo/index.js.map +1 -1
  60. package/dist/_vendor/platform-node/index.d.mts +2 -1
  61. package/dist/_vendor/platform-node/index.d.mts.map +1 -1
  62. package/dist/_vendor/platform-node/index.mjs +27 -2
  63. package/dist/_vendor/platform-node/index.mjs.map +1 -1
  64. package/dist/_vendor/platform-node/ipc-react.mjs +4 -0
  65. package/dist/_vendor/platform-node/ipc-react.mjs.map +1 -1
  66. package/dist/_vendor/platform-web/external-change.d.ts +2 -2
  67. package/dist/_vendor/platform-web/external-change.js +2 -2
  68. package/dist/_vendor/platform-web/external-change.js.map +1 -1
  69. package/dist/_vendor/platform-web/index.d.ts +13 -10
  70. package/dist/_vendor/platform-web/index.d.ts.map +1 -1
  71. package/dist/_vendor/platform-web/index.js +66 -10
  72. package/dist/_vendor/platform-web/index.js.map +1 -1
  73. package/dist/_vendor/platform-web/indexeddb.d.ts +3 -3
  74. package/dist/_vendor/platform-web/indexeddb.js +3 -3
  75. package/dist/_vendor/platform-web/indexeddb.js.map +1 -1
  76. package/dist/_vendor/platform-web/opfs.d.ts +3 -1
  77. package/dist/_vendor/platform-web/opfs.d.ts.map +1 -1
  78. package/dist/_vendor/platform-web/opfs.js +29 -3
  79. package/dist/_vendor/platform-web/opfs.js.map +1 -1
  80. package/dist/_vendor/platform-web/persistence.d.ts +31 -1
  81. package/dist/_vendor/platform-web/persistence.d.ts.map +1 -1
  82. package/dist/_vendor/platform-web/persistence.js.map +1 -1
  83. package/dist/_vendor/platform-web/react.d.ts.map +1 -1
  84. package/dist/_vendor/platform-web/react.js +9 -1
  85. package/dist/_vendor/platform-web/react.js.map +1 -1
  86. package/dist/_vendor/react/index.d.ts +6 -5
  87. package/dist/_vendor/react/index.d.ts.map +1 -1
  88. package/dist/_vendor/react/index.js +6 -5
  89. package/dist/_vendor/react/index.js.map +1 -1
  90. package/dist/_vendor/svelte/index.d.ts +8 -6
  91. package/dist/_vendor/svelte/index.d.ts.map +1 -1
  92. package/dist/_vendor/svelte/index.js +7 -5
  93. package/dist/_vendor/svelte/index.js.map +1 -1
  94. package/package.json +2 -2
@@ -4,7 +4,8 @@ import { SyncoreRuntime } from "./runtime/runtime.mjs";
4
4
  import { createDevtoolsCommandHandler, createDevtoolsSubscriptionHost } from "./runtime/devtools.mjs";
5
5
  import { src_exports } from "./index.mjs";
6
6
  import { generateDevtoolsToken, isAllowedDashboardOrigin, isAuthorizedDashboardRequest, sanitizeDevtoolsToken } from "./devtools-auth.mjs";
7
- import { appendFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
7
+ import { appendFile, mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
+ import { randomUUID } from "node:crypto";
8
9
  import { createServer } from "node:http";
9
10
  import { connect } from "node:net";
10
11
  import path from "node:path";
@@ -36,6 +37,9 @@ const VALID_SYNCORE_TEMPLATES = [
36
37
  let pendingDevBootstrap;
37
38
  let devBootstrapInFlight = false;
38
39
  const PROJECT_TARGET_RUNTIME_ID = "syncore-project-target";
40
+ const STORAGE_ACCESS_TICKET_TTL_MS = 300 * 1e3;
41
+ const STORAGE_ACCESS_CHUNK_BYTES = 1024 * 1024;
42
+ const STORAGE_ACCESS_MAX_PREVIEW_BYTES = 8e4;
39
43
  program.name("syncorejs").description("Syncore local-first toolkit CLI").version("0.1.0");
40
44
  program.command("init").description("Scaffold Syncore in the current directory").option("--template <template>", `Template to scaffold (${VALID_SYNCORE_TEMPLATES.join(", ")}, or auto)`, "auto").option("--force", "Overwrite Syncore-managed files when they already exist").action(async (options) => {
41
45
  const cwd = process.cwd();
@@ -1161,6 +1165,19 @@ var HubFileStorageAdapter = class {
1161
1165
  return null;
1162
1166
  }
1163
1167
  }
1168
+ async readRange(id, offset, length) {
1169
+ let handle;
1170
+ try {
1171
+ handle = await open(this.filePath(id), "r");
1172
+ const buffer = Buffer.alloc(Math.max(length, 0));
1173
+ const result = await handle.read(buffer, 0, buffer.byteLength, Math.max(offset, 0));
1174
+ return buffer.subarray(0, result.bytesRead);
1175
+ } catch {
1176
+ return null;
1177
+ } finally {
1178
+ await handle?.close();
1179
+ }
1180
+ }
1164
1181
  async delete(id) {
1165
1182
  await rm(this.filePath(id), { force: true });
1166
1183
  }
@@ -1252,6 +1269,11 @@ async function createProjectTargetBackend(cwd, externalChangeSignal) {
1252
1269
  driver,
1253
1270
  storage: new HubFileStorageAdapter(storageDirectory),
1254
1271
  platform: "project",
1272
+ runtimeCapabilities: { storage: {
1273
+ available: true,
1274
+ protocol: "file",
1275
+ supportsRange: true
1276
+ } },
1255
1277
  ...externalChangeSignal ? { externalChangeSignal } : {}
1256
1278
  });
1257
1279
  await runtime.start();
@@ -1310,6 +1332,13 @@ function createProjectDevtoolsCapabilities() {
1310
1332
  mutate: true,
1311
1333
  importExport: true
1312
1334
  },
1335
+ storage: {
1336
+ browse: true,
1337
+ download: true,
1338
+ readRange: true,
1339
+ delete: true,
1340
+ maxPreviewBytes: 8e4
1341
+ },
1313
1342
  scheduler: {
1314
1343
  read: true,
1315
1344
  edit: true
@@ -1493,18 +1522,22 @@ async function startDevHub(options) {
1493
1522
  await runDevProjectBootstrap(options.cwd, options.template);
1494
1523
  await setupDevProjectWatch(options.cwd, options.template);
1495
1524
  if (await isLocalPortInUse(devtoolsPort)) {
1525
+ const activeSessionState = await readDevtoolsSessionState(options.cwd) ?? sessionState;
1496
1526
  console.log(`Syncore devtools hub already running at ws://localhost:${devtoolsPort}. Reusing existing hub/dashboard.`);
1497
- return await readDevtoolsSessionState(options.cwd) ?? sessionState;
1527
+ console.log(`Devtools dashboard token: ${activeSessionState.token}`);
1528
+ console.log(`Dashboard shell: ${activeSessionState.authenticatedDashboardUrl}`);
1529
+ return activeSessionState;
1498
1530
  }
1499
1531
  await writeDevtoolsSessionState(options.cwd, sessionState);
1500
- const httpServer = createServer((_request, response) => {
1501
- response.writeHead(200, { "content-type": "application/json" });
1502
- response.end(JSON.stringify({
1503
- ok: true,
1504
- wsPort: devtoolsPort
1505
- }));
1532
+ const httpServer = createServer((request, response) => {
1533
+ handleStorageAccessHttpRequest(request, response).catch((error) => {
1534
+ if (!response.headersSent) writeJsonResponse(response, 500, { error: formatError(error) });
1535
+ else response.destroy(error instanceof Error ? error : void 0);
1536
+ });
1506
1537
  });
1507
1538
  const websocketServer = new WebSocketServer({ server: httpServer });
1539
+ const storageAccessTickets = /* @__PURE__ */ new Map();
1540
+ const pendingHubCommands = /* @__PURE__ */ new Map();
1508
1541
  const runtimeSockets = /* @__PURE__ */ new Map();
1509
1542
  const runtimeHellos = /* @__PURE__ */ new Map();
1510
1543
  const runtimeEvents = /* @__PURE__ */ new Map();
@@ -1571,6 +1604,135 @@ async function startDevHub(options) {
1571
1604
  event
1572
1605
  })}\n`);
1573
1606
  };
1607
+ const requestRuntimeCommand = async (targetRuntimeId, payload, timeoutMs = 3e4) => {
1608
+ if (targetRuntimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) return projectTargetBackend.handleCommand(payload);
1609
+ const target = runtimeSockets.get(targetRuntimeId);
1610
+ if (!target || target.readyState !== WebSocket.OPEN) throw new Error(`Runtime ${targetRuntimeId} is not connected.`);
1611
+ const commandId = `hub:${randomUUID()}`;
1612
+ return await new Promise((resolve, reject) => {
1613
+ const timeout = setTimeout(() => {
1614
+ pendingHubCommands.delete(commandId);
1615
+ reject(/* @__PURE__ */ new Error(`Runtime command ${payload.kind} timed out.`));
1616
+ }, timeoutMs);
1617
+ pendingHubCommands.set(commandId, {
1618
+ resolve,
1619
+ reject,
1620
+ timeout
1621
+ });
1622
+ target.send(JSON.stringify({
1623
+ type: "command",
1624
+ commandId,
1625
+ targetRuntimeId,
1626
+ payload
1627
+ }));
1628
+ });
1629
+ };
1630
+ const createStorageAccessTicket = async (targetRuntimeId, id, purpose) => {
1631
+ const metadata = await requestRuntimeCommand(targetRuntimeId, {
1632
+ kind: "storage.readRange",
1633
+ id,
1634
+ offset: 0,
1635
+ length: 0
1636
+ });
1637
+ if (metadata.kind !== "storage.readRange.result") return {
1638
+ kind: "storage.access.create.result",
1639
+ error: "Runtime returned an unexpected storage access response."
1640
+ };
1641
+ if (metadata.error || !metadata.entry) return {
1642
+ kind: "storage.access.create.result",
1643
+ error: metadata.error ?? "Storage object could not be accessed."
1644
+ };
1645
+ const ticket = randomUUID();
1646
+ const expiresAt = Date.now() + STORAGE_ACCESS_TICKET_TTL_MS;
1647
+ storageAccessTickets.set(ticket, {
1648
+ id: ticket,
1649
+ runtimeId: targetRuntimeId,
1650
+ storageId: id,
1651
+ purpose,
1652
+ entry: metadata.entry,
1653
+ supportsRange: metadata.supportsRange,
1654
+ expiresAt
1655
+ });
1656
+ cleanupExpiredStorageTickets();
1657
+ return {
1658
+ kind: "storage.access.create.result",
1659
+ entry: metadata.entry,
1660
+ url: `http://127.0.0.1:${devtoolsPort}/storage/access/${ticket}`,
1661
+ expiresAt,
1662
+ supportsRange: metadata.supportsRange,
1663
+ maxPreviewBytes: STORAGE_ACCESS_MAX_PREVIEW_BYTES
1664
+ };
1665
+ };
1666
+ const cleanupExpiredStorageTickets = () => {
1667
+ const now = Date.now();
1668
+ for (const [ticket, access] of storageAccessTickets) if (access.expiresAt <= now) storageAccessTickets.delete(ticket);
1669
+ };
1670
+ const handleStorageAccessHttpRequest = async (request, response) => {
1671
+ setStorageCorsHeaders(response);
1672
+ if (request.method === "OPTIONS") {
1673
+ response.writeHead(204);
1674
+ response.end();
1675
+ return;
1676
+ }
1677
+ const requestUrl = new URL(request.url ?? "/", `http://127.0.0.1:${devtoolsPort}`);
1678
+ const match = /^\/storage\/access\/([^/]+)$/.exec(requestUrl.pathname);
1679
+ if (!match) {
1680
+ writeJsonResponse(response, 200, {
1681
+ ok: true,
1682
+ wsPort: devtoolsPort
1683
+ });
1684
+ return;
1685
+ }
1686
+ if (request.method !== "GET" && request.method !== "HEAD") {
1687
+ writeTextResponse(response, 405, "Method not allowed.");
1688
+ return;
1689
+ }
1690
+ cleanupExpiredStorageTickets();
1691
+ const ticket = storageAccessTickets.get(match[1]);
1692
+ if (!ticket || ticket.expiresAt <= Date.now()) {
1693
+ writeTextResponse(response, 401, "Storage access ticket is invalid or expired.");
1694
+ return;
1695
+ }
1696
+ const range = parseStorageRangeHeader(request.headers.range, ticket.entry.size);
1697
+ if ("error" in range) {
1698
+ writeTextResponse(response, range.status, range.error);
1699
+ return;
1700
+ }
1701
+ const start = range.start;
1702
+ const end = range.end;
1703
+ const byteLength = end >= start ? end - start + 1 : 0;
1704
+ if (!ticket.supportsRange && ticket.entry.size > STORAGE_ACCESS_CHUNK_BYTES) {
1705
+ writeTextResponse(response, 409, "This storage backend does not support streaming large files.");
1706
+ return;
1707
+ }
1708
+ writeStorageAccessHeaders(response, ticket, {
1709
+ status: range.partial ? 206 : 200,
1710
+ start,
1711
+ end,
1712
+ byteLength
1713
+ });
1714
+ if (request.method === "HEAD") {
1715
+ response.end();
1716
+ return;
1717
+ }
1718
+ let offset = start;
1719
+ while (offset <= end) {
1720
+ const chunkLength = Math.min(STORAGE_ACCESS_CHUNK_BYTES, end - offset + 1);
1721
+ const chunk = await requestRuntimeCommand(ticket.runtimeId, {
1722
+ kind: "storage.readRange",
1723
+ id: ticket.storageId,
1724
+ offset,
1725
+ length: chunkLength
1726
+ }, 6e4);
1727
+ if (chunk.kind !== "storage.readRange.result") throw new Error("Runtime returned an unexpected storage chunk response.");
1728
+ if (chunk.error || !chunk.base64) throw new Error(chunk.error ?? "Storage chunk could not be read.");
1729
+ const bytes = Buffer.from(chunk.base64, "base64");
1730
+ if (bytes.byteLength === 0) break;
1731
+ await writeResponseChunk(response, bytes);
1732
+ offset += bytes.byteLength;
1733
+ }
1734
+ response.end();
1735
+ };
1574
1736
  websocketServer.on("connection", (socket, request) => {
1575
1737
  const isBrowserDashboardClient = isAllowedDashboardOrigin(request.headers.origin, dashboardPort);
1576
1738
  const isAuthorizedDashboardClient = !isBrowserDashboardClient || isAuthorizedDashboardRequest({
@@ -1616,6 +1778,29 @@ async function startDevHub(options) {
1616
1778
  if (!isAuthorizedDashboardClient) return;
1617
1779
  const targetRuntimeId = message.targetRuntimeId;
1618
1780
  if (!targetRuntimeId) return;
1781
+ if (message.payload.kind === "storage.access.create") {
1782
+ createStorageAccessTicket(targetRuntimeId, message.payload.id, message.payload.purpose).then((payload) => {
1783
+ if (socket.readyState !== WebSocket.OPEN) return;
1784
+ socket.send(JSON.stringify({
1785
+ type: "command.result",
1786
+ commandId: message.commandId,
1787
+ runtimeId: targetRuntimeId,
1788
+ payload
1789
+ }));
1790
+ }).catch((error) => {
1791
+ if (socket.readyState !== WebSocket.OPEN) return;
1792
+ socket.send(JSON.stringify({
1793
+ type: "command.result",
1794
+ commandId: message.commandId,
1795
+ runtimeId: targetRuntimeId,
1796
+ payload: {
1797
+ kind: "storage.access.create.result",
1798
+ error: formatError(error)
1799
+ }
1800
+ }));
1801
+ });
1802
+ return;
1803
+ }
1619
1804
  if (targetRuntimeId === PROJECT_TARGET_RUNTIME_ID && projectTargetBackend) {
1620
1805
  (async () => {
1621
1806
  const payload = await projectTargetBackend.handleCommand(message.payload);
@@ -1719,6 +1904,15 @@ async function startDevHub(options) {
1719
1904
  appendHubLog(message.event);
1720
1905
  } else if (message.type === "event") appendHubLog(message.event);
1721
1906
  if (message.type === "command.result" || message.type === "subscription.data" || message.type === "subscription.error") {
1907
+ if (message.type === "command.result") {
1908
+ const pending = pendingHubCommands.get(message.commandId);
1909
+ if (pending) {
1910
+ clearTimeout(pending.timeout);
1911
+ pendingHubCommands.delete(message.commandId);
1912
+ pending.resolve(message.payload);
1913
+ return;
1914
+ }
1915
+ }
1722
1916
  for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(encoded);
1723
1917
  return;
1724
1918
  }
@@ -1787,19 +1981,18 @@ async function startDevHub(options) {
1787
1981
  console.log(`Electron/Node runtimes: set devtoolsUrl to ws://localhost:${devtoolsPort}.`);
1788
1982
  console.log(`Web/Next apps: connect the dashboard or worker bridge to ws://localhost:${devtoolsPort}.`);
1789
1983
  console.log("Expo apps: use the same hub URL through LAN or adb reverse while developing.");
1790
- const dashboardRoot = path.resolve(CORE_PACKAGE_ROOT, "..", "..", "apps", "dashboard");
1791
- if (await fileExists(path.join(dashboardRoot, "vite.config.ts"))) try {
1792
- await (await (await import("vite")).createServer({
1793
- configFile: path.join(dashboardRoot, "vite.config.ts"),
1794
- root: dashboardRoot,
1795
- server: { port: dashboardPort }
1796
- })).listen();
1984
+ let dashboardStaticServer;
1985
+ const dashboardStaticRoot = await resolvePackagedDashboardRoot();
1986
+ if (!dashboardStaticRoot) throw new Error("Syncore Dashboard build is missing. Expected the packaged dashboard at syncorejs/dist/_dashboard/index.html. Rebuild syncorejs before running `syncorejs dev`.");
1987
+ try {
1988
+ dashboardStaticServer = await startPackagedDashboardServer(dashboardStaticRoot, dashboardPort);
1797
1989
  console.log(`Dashboard shell: ${sessionState.authenticatedDashboardUrl}`);
1798
1990
  } catch (error) {
1799
- console.log(`Dashboard source not started automatically: ${formatError(error)}`);
1991
+ throw new Error(`Syncore Dashboard shell could not start: ${formatError(error)}`, { cause: error });
1800
1992
  }
1801
1993
  const close = () => {
1802
1994
  projectTargetBackend?.dispose();
1995
+ dashboardStaticServer?.close();
1803
1996
  websocketServer.close();
1804
1997
  httpServer.close();
1805
1998
  process.exit(0);
@@ -1808,6 +2001,155 @@ async function startDevHub(options) {
1808
2001
  process.on("SIGTERM", close);
1809
2002
  return sessionState;
1810
2003
  }
2004
+ async function resolvePackagedDashboardRoot() {
2005
+ const candidates = [
2006
+ path.resolve(CORE_PACKAGE_ROOT, "..", "_dashboard"),
2007
+ path.resolve(CORE_PACKAGE_ROOT, "_dashboard"),
2008
+ path.resolve(CORE_PACKAGE_ROOT, "..", "syncore", "dist", "_dashboard")
2009
+ ];
2010
+ for (const candidate of candidates) if (await fileExists(path.join(candidate, "index.html"))) return candidate;
2011
+ return null;
2012
+ }
2013
+ async function startPackagedDashboardServer(root, port) {
2014
+ const server = createServer((request, response) => {
2015
+ serveDashboardAsset(root, request, response).catch((error) => {
2016
+ if (!response.headersSent) writeTextResponse(response, 500, formatError(error));
2017
+ else response.destroy(error instanceof Error ? error : void 0);
2018
+ });
2019
+ });
2020
+ await new Promise((resolve, reject) => {
2021
+ server.once("error", reject);
2022
+ server.listen(port, "127.0.0.1", () => {
2023
+ server.off("error", reject);
2024
+ resolve();
2025
+ });
2026
+ });
2027
+ return server;
2028
+ }
2029
+ async function serveDashboardAsset(root, request, response) {
2030
+ if (request.method !== "GET" && request.method !== "HEAD") {
2031
+ writeTextResponse(response, 405, "Method not allowed.");
2032
+ return;
2033
+ }
2034
+ const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
2035
+ const requestedPath = decodeURIComponent(requestUrl.pathname);
2036
+ const relativePath = requestedPath === "/" ? "index.html" : requestedPath.slice(1);
2037
+ let assetPath = path.resolve(root, relativePath);
2038
+ if (!isPathInside(root, assetPath)) {
2039
+ writeTextResponse(response, 403, "Forbidden.");
2040
+ return;
2041
+ }
2042
+ try {
2043
+ if ((await stat(assetPath)).isDirectory()) assetPath = path.join(assetPath, "index.html");
2044
+ } catch {
2045
+ assetPath = path.join(root, "index.html");
2046
+ }
2047
+ if (!isPathInside(root, assetPath)) {
2048
+ writeTextResponse(response, 403, "Forbidden.");
2049
+ return;
2050
+ }
2051
+ const body = await readFile(assetPath);
2052
+ response.writeHead(200, {
2053
+ "Content-Type": getDashboardAssetContentType(assetPath),
2054
+ "Content-Length": body.byteLength
2055
+ });
2056
+ if (request.method === "HEAD") {
2057
+ response.end();
2058
+ return;
2059
+ }
2060
+ response.end(body);
2061
+ }
2062
+ function isPathInside(root, candidate) {
2063
+ const relative = path.relative(root, candidate);
2064
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
2065
+ }
2066
+ function getDashboardAssetContentType(filePath) {
2067
+ switch (path.extname(filePath)) {
2068
+ case ".html": return "text/html; charset=utf-8";
2069
+ case ".js": return "text/javascript; charset=utf-8";
2070
+ case ".css": return "text/css; charset=utf-8";
2071
+ case ".json": return "application/json; charset=utf-8";
2072
+ case ".svg": return "image/svg+xml";
2073
+ case ".png": return "image/png";
2074
+ case ".ico": return "image/x-icon";
2075
+ case ".woff2": return "font/woff2";
2076
+ default: return "application/octet-stream";
2077
+ }
2078
+ }
2079
+ function setStorageCorsHeaders(response) {
2080
+ response.setHeader("Access-Control-Allow-Origin", "*");
2081
+ response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
2082
+ response.setHeader("Access-Control-Allow-Headers", "Range");
2083
+ response.setHeader("Access-Control-Expose-Headers", "Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type");
2084
+ }
2085
+ function writeJsonResponse(response, status, payload) {
2086
+ response.writeHead(status, { "content-type": "application/json" });
2087
+ response.end(JSON.stringify(payload));
2088
+ }
2089
+ function writeTextResponse(response, status, message) {
2090
+ response.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
2091
+ response.end(message);
2092
+ }
2093
+ function parseStorageRangeHeader(header, size) {
2094
+ if (size === 0) {
2095
+ if (!header) return {
2096
+ start: 0,
2097
+ end: -1,
2098
+ partial: false
2099
+ };
2100
+ return {
2101
+ error: "Requested range is not satisfiable.",
2102
+ status: 416
2103
+ };
2104
+ }
2105
+ if (!header) return {
2106
+ start: 0,
2107
+ end: size - 1,
2108
+ partial: false
2109
+ };
2110
+ if (header.includes(",")) return {
2111
+ error: "Multi-range requests are not supported.",
2112
+ status: 416
2113
+ };
2114
+ const match = /^bytes=(\d+)-(\d*)$/.exec(header);
2115
+ if (!match) return {
2116
+ error: "Only simple byte ranges are supported.",
2117
+ status: 416
2118
+ };
2119
+ const start = Number(match[1]);
2120
+ const end = match[2] ? Number(match[2]) : size - 1;
2121
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || start >= size) return {
2122
+ error: "Requested range is not satisfiable.",
2123
+ status: 416
2124
+ };
2125
+ return {
2126
+ start,
2127
+ end: Math.min(end, size - 1),
2128
+ partial: true
2129
+ };
2130
+ }
2131
+ function writeStorageAccessHeaders(response, ticket, range) {
2132
+ const headers = {
2133
+ "content-type": ticket.entry.contentType ?? "application/octet-stream",
2134
+ "content-length": range.byteLength,
2135
+ "accept-ranges": ticket.supportsRange ? "bytes" : "none",
2136
+ "content-disposition": renderStorageContentDisposition(ticket)
2137
+ };
2138
+ if (range.status === 206) headers["content-range"] = `bytes ${range.start}-${range.end}/${ticket.entry.size}`;
2139
+ response.writeHead(range.status, headers);
2140
+ }
2141
+ function renderStorageContentDisposition(ticket) {
2142
+ return `${ticket.purpose === "download" ? "attachment" : "inline"}; filename="${sanitizeHeaderFilename(ticket.entry.fileName ?? ticket.entry.id)}"; filename*=UTF-8''${encodeURIComponent(ticket.entry.fileName ?? `${ticket.entry.id}.bin`)}`;
2143
+ }
2144
+ function sanitizeHeaderFilename(value) {
2145
+ return value.replaceAll(/["\\\r\n]/g, "_");
2146
+ }
2147
+ async function writeResponseChunk(response, chunk) {
2148
+ if (response.write(chunk)) return;
2149
+ await new Promise((resolve) => {
2150
+ response.once("drain", resolve);
2151
+ });
2152
+ }
1811
2153
  async function writeDevtoolsSessionState(cwd, state) {
1812
2154
  const sessionPath = path.join(cwd, DEVTOOLS_SESSION_FILE);
1813
2155
  await mkdir(path.dirname(sessionPath), { recursive: true });