@astralyn/sash 0.1.0 → 0.1.2

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 (108) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +22 -7
  3. package/THIRD_PARTY_NOTICES.md +9 -0
  4. package/dist/api.js +14 -45
  5. package/dist/app-state.js +99 -0
  6. package/dist/autostart/command.js +24 -0
  7. package/dist/autostart/context.js +43 -0
  8. package/dist/autostart/files.js +60 -0
  9. package/dist/autostart/installation.js +48 -0
  10. package/dist/autostart/start.js +47 -0
  11. package/dist/autostart/windows-registry.js +88 -0
  12. package/dist/autostart/windows.js +59 -0
  13. package/dist/autostart-contract.js +31 -0
  14. package/dist/autostart-entry.js +10 -0
  15. package/dist/autostart.js +69 -0
  16. package/dist/cli.js +11 -11
  17. package/dist/commands/auto.js +15 -0
  18. package/dist/commands/lifecycle.js +5 -31
  19. package/dist/commands/logs.js +11 -4
  20. package/dist/commands/shared.js +2 -4
  21. package/dist/commands/status.js +6 -7
  22. package/dist/commands/update.js +7 -3
  23. package/dist/commands/web.js +19 -27
  24. package/dist/contracts.js +171 -335
  25. package/dist/core-config-validation.js +22 -38
  26. package/dist/core-update.js +126 -762
  27. package/dist/core.js +20 -46
  28. package/dist/daemon/app.js +132 -113
  29. package/dist/daemon/context.js +13 -7
  30. package/dist/daemon/entry.js +21 -38
  31. package/dist/daemon/errors.js +6 -1
  32. package/dist/daemon/handlers/autostart.js +18 -0
  33. package/dist/daemon/handlers/core.js +32 -9
  34. package/dist/daemon/handlers/daemon.js +32 -6
  35. package/dist/daemon/handlers/profiles.js +12 -2
  36. package/dist/daemon/handlers/settings.js +9 -36
  37. package/dist/daemon/router.js +40 -27
  38. package/dist/daemon/scheduler.js +3 -1
  39. package/dist/daemon/server.js +6 -3
  40. package/dist/daemon/web-auth.js +52 -0
  41. package/dist/daemon-auth.js +2 -2
  42. package/dist/daemon-client.js +18 -2
  43. package/dist/daemon-http.js +7 -0
  44. package/dist/daemon-lifecycle.js +19 -196
  45. package/dist/github.js +7 -2
  46. package/dist/http.js +7 -5
  47. package/dist/log-follow.js +2 -1
  48. package/dist/mihomo-config.js +7 -9
  49. package/dist/paths.js +5 -7
  50. package/dist/profile-model.js +87 -0
  51. package/dist/profile-service.js +250 -587
  52. package/dist/profiles.js +43 -171
  53. package/dist/runtime-lifecycle.js +112 -194
  54. package/dist/runtime-owner.js +37 -76
  55. package/dist/sash-client.js +63 -21
  56. package/dist/settings-service.js +29 -228
  57. package/dist/settings.js +61 -285
  58. package/dist/status.js +24 -40
  59. package/dist/supervisor.js +1 -14
  60. package/dist/sysproxy/common.js +5 -45
  61. package/dist/sysproxy/factory.js +24 -65
  62. package/dist/sysproxy/snapshot.js +24 -257
  63. package/dist/sysproxy.js +1 -4
  64. package/dist/system-proxy-manager.js +37 -35
  65. package/dist/ui/assets/{ConnectionsView-DNGmZBSU.js → ConnectionsView-BgXQM6Z4.js} +1 -1
  66. package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-2f542P8z.js} +1 -1
  67. package/dist/ui/assets/{PaginationFooter-B3kHzRfB.js → PaginationFooter-cj1tcZej.js} +1 -1
  68. package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
  69. package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
  70. package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
  71. package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
  72. package/dist/ui/assets/{RulesView-D9vZBiJ1.js → RulesView-CtKvlckZ.js} +1 -1
  73. package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
  74. package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
  75. package/dist/ui/assets/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
  76. package/dist/ui/assets/index-CBInDvdJ.js +19 -0
  77. package/dist/ui/assets/index-mOLy7fkB.css +1 -0
  78. package/dist/ui/assets/{theme-BNq4FkXS.js → theme-D40MF8cQ.js} +1 -1
  79. package/dist/ui/index.html +2 -2
  80. package/dist/web-bootstrap.js +113 -0
  81. package/docs/architecture-proposal.md +109 -0
  82. package/docs/autostart.md +101 -0
  83. package/docs/backend.md +92 -216
  84. package/docs/frontend.md +61 -97
  85. package/docs/usage.md +88 -186
  86. package/package.json +10 -6
  87. package/dist/commands/upgrade.js +0 -43
  88. package/dist/core-install-transaction.js +0 -114
  89. package/dist/core-update-coordination.js +0 -105
  90. package/dist/core-update-service.js +0 -245
  91. package/dist/managed-state-transaction.js +0 -377
  92. package/dist/offline-mutation.js +0 -58
  93. package/dist/profile-migration.js +0 -101
  94. package/dist/runtime-recovery.js +0 -31
  95. package/dist/sysproxy/darwin.js +0 -287
  96. package/dist/sysproxy/gnome.js +0 -181
  97. package/dist/sysproxy/legacy.js +0 -58
  98. package/dist/tun-guidance.js +0 -11
  99. package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
  100. package/dist/ui/assets/CodeEditorModal-CFEnWsyh.js +0 -14
  101. package/dist/ui/assets/ProfileEditorDialog-BydoZthX.js +0 -1
  102. package/dist/ui/assets/ProfilesView-Btc1DOxE.js +0 -2
  103. package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
  104. package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
  105. package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
  106. package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
  107. package/dist/ui/assets/index-B61V60w_.js +0 -19
  108. package/dist/ui/assets/index-bkyxJG8J.css +0 -1
@@ -1,14 +1,30 @@
1
1
  import { currentCoreVersion } from "../../core.js";
2
+ import { HttpError } from "../../daemon-http.js";
2
3
  import { publicSettings } from "../../settings.js";
3
4
  export function health(ctx) {
4
5
  const body = { token: ctx.token, pid: process.pid, startedAt: ctx.startedAt };
5
6
  return { status: 200, json: body };
6
7
  }
8
+ export function createWebBootstrap(ctx) {
9
+ const bootstrap = ctx.webAuth.createBootstrap();
10
+ const body = { token: bootstrap.token, expiresAt: bootstrap.expiresAt };
11
+ return { status: 200, json: body };
12
+ }
13
+ export async function redeemWebBootstrap(ctx, req) {
14
+ const body = await req.readJson(1024);
15
+ const token = typeof body.token === "string" ? body.token.trim() : "";
16
+ const session = ctx.webAuth.redeemBootstrap(token);
17
+ if (!session)
18
+ throw new HttpError(401, "Invalid or expired bootstrap token");
19
+ const response = { token: session, daemonToken: ctx.token };
20
+ return { status: 200, json: response };
21
+ }
7
22
  export async function daemonStatus(ctx, req) {
8
23
  const settings = ctx.settings.committed();
24
+ const ownership = ctx.supervisor.ownedCoreSnapshot();
9
25
  const runtimeCore = await ctx.supervisor.status();
10
26
  const installedVersion = currentCoreVersion(ctx.layout);
11
- const core = runtimeCore.version || !installedVersion
27
+ let core = runtimeCore.version || !installedVersion
12
28
  ? runtimeCore
13
29
  : { ...runtimeCore, version: installedVersion };
14
30
  let actualProxy;
@@ -21,22 +37,33 @@ export async function daemonStatus(ctx, req) {
21
37
  proxyApplied = inspection.applied;
22
38
  proxyAppliedKnown = inspection.appliedKnown;
23
39
  proxyStateKnown = inspection.stateKnown;
24
- if (proxyStateKnown)
25
- actualProxy = inspection.state;
40
+ actualProxy = inspection.state;
26
41
  proxyQueryError = inspection.queryError;
27
42
  }
28
43
  catch (err) {
29
44
  proxyQueryError = err instanceof Error ? err.message : String(err);
30
45
  }
31
46
  const active = ctx.profiles.active();
47
+ if (core.running && (!ownership || !ctx.supervisor.ownsCore(ownership)))
48
+ core = { running: false };
49
+ const applied = ctx.lifecycle.configuration();
32
50
  const status = {
33
51
  daemon: {
34
52
  pid: process.pid,
53
+ bootId: ctx.token,
35
54
  startedAt: ctx.startedAt,
36
55
  port: settings.daemonPort,
37
56
  },
38
57
  revisions: {
39
58
  profiles: ctx.profileRevision(),
59
+ runtime: ctx.lifecycle.revision,
60
+ },
61
+ configuration: {
62
+ pending: ctx.pendingApply(),
63
+ appliedProfile: applied?.profile ?? null,
64
+ appliedSettings: applied
65
+ ? { mixedPort: applied.settings.mixedPort, allowLan: applied.settings.allowLan }
66
+ : null,
40
67
  },
41
68
  core,
42
69
  systemProxy: {
@@ -53,9 +80,8 @@ export async function daemonStatus(ctx, req) {
53
80
  return { status: 200, json: status };
54
81
  }
55
82
  export function shutdownDaemon(ctx) {
56
- return ctx.shutdown().then((snapshot) => ({
57
- status: 200,
58
- json: { coreWasRunning: snapshot.coreWasRunning },
83
+ return ctx.shutdown().then(() => ({
84
+ status: 204,
59
85
  after: () => {
60
86
  void ctx
61
87
  .closeListener()
@@ -1,8 +1,15 @@
1
+ import { requiredParam } from "../../daemon-http.js";
1
2
  import { ProfileInputError } from "../../profile-service.js";
2
- import { requiredParam } from "../router.js";
3
3
  export function listProfiles(ctx) {
4
4
  return { status: 200, json: ctx.profiles.list() };
5
5
  }
6
+ export async function reorderProfiles(ctx, req) {
7
+ const { ids } = await req.readJson();
8
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string")) {
9
+ throw new ProfileInputError("Missing profile ids array");
10
+ }
11
+ return { status: 200, json: await ctx.profiles.reorder(ids) };
12
+ }
6
13
  export async function addProfile(ctx, req) {
7
14
  const body = await req.readJson();
8
15
  const url = typeof body.url === "string" ? body.url.trim() : "";
@@ -42,7 +49,10 @@ export function readProfileContent(ctx, req) {
42
49
  export async function writeProfileContent(ctx, req) {
43
50
  const body = await req.readJson(8 * 1024 * 1024);
44
51
  const content = typeof body.content === "string" ? body.content : "";
45
- const result = await ctx.profiles.writeContent(requiredParam(req, "id"), content);
52
+ const revision = body.revision;
53
+ if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1)
54
+ throw new ProfileInputError("A positive profile revision is required");
55
+ const result = await ctx.profiles.writeContent(requiredParam(req, "id"), content, revision);
46
56
  return { status: 200, json: result };
47
57
  }
48
58
  export async function renameProfile(ctx, req) {
@@ -1,13 +1,6 @@
1
- import fs from "node:fs";
2
- import { parseSettingsPatch, } from "../../contracts.js";
1
+ import { parseSettingsPatch } from "../../contracts.js";
3
2
  import { HttpError } from "../../daemon-http.js";
4
- import { parseSettingsText, publicSettings } from "../../settings.js";
5
- function writeResultBody(result) {
6
- return {
7
- restartRequired: result.restartRequired,
8
- settings: publicSettings(result.settings),
9
- };
10
- }
3
+ import { publicSettings } from "../../settings.js";
11
4
  export function readSettings(ctx) {
12
5
  return { status: 200, json: publicSettings(ctx.settings.committed()) };
13
6
  }
@@ -15,34 +8,14 @@ export async function patchSettings(ctx, req) {
15
8
  const body = await req.readJson();
16
9
  let patch;
17
10
  try {
18
- patch = parseSettingsPatch(body, "request");
11
+ patch = parseSettingsPatch(body);
19
12
  }
20
- catch (err) {
21
- throw new HttpError(400, err.message);
13
+ catch (error) {
14
+ throw new HttpError(400, error instanceof Error ? error.message : String(error));
22
15
  }
23
16
  const result = await ctx.settingsService.apply(patch);
24
- return { status: 200, json: writeResultBody(result) };
25
- }
26
- export function readSettingsFile(ctx) {
27
- let content;
28
- try {
29
- content = fs.readFileSync(ctx.layout.settingsFile, "utf8");
30
- }
31
- catch {
32
- throw new HttpError(404, "Settings file does not exist yet");
33
- }
34
- return { status: 200, json: { content } };
35
- }
36
- export async function writeSettingsFile(ctx, req) {
37
- const body = await req.readJson(256 * 1024);
38
- const content = typeof body.content === "string" ? body.content : "";
39
- let parsed;
40
- try {
41
- parsed = parseSettingsText(content, ctx.layout.settingsFile);
42
- }
43
- catch (err) {
44
- throw new HttpError(400, err.message);
45
- }
46
- const result = await ctx.settingsService.applyFileSettings(parsed);
47
- return { status: 200, json: writeResultBody(result) };
17
+ return {
18
+ status: 200,
19
+ json: { restartRequired: result.restartRequired, settings: publicSettings(result.settings) },
20
+ };
48
21
  }
@@ -3,11 +3,12 @@ import { parseJsonObjectBody, sendError, sendJson } from "../daemon-http.js";
3
3
  import { forwardHttpToCore } from "../daemon-proxy.js";
4
4
  import { serveStaticUi } from "../daemon-static.js";
5
5
  import { errorToHttp } from "./errors.js";
6
- import { reloadCoreConfig, restartCore, startCore, stopCore } from "./handlers/core.js";
7
- import { daemonStatus, health, shutdownDaemon } from "./handlers/daemon.js";
8
- import { activateProfile, addProfile, importProfile, listProfiles, readProfileContent, removeProfile, renameProfile, updateAllProfiles, updateProfile, writeProfileContent, } from "./handlers/profiles.js";
6
+ import { readAutostart, writeAutostart } from "./handlers/autostart.js";
7
+ import { restartCore, setCoreMode, startCore, stopCore, updateCore } from "./handlers/core.js";
8
+ import { createWebBootstrap, daemonStatus, health, redeemWebBootstrap, shutdownDaemon, } from "./handlers/daemon.js";
9
+ import { activateProfile, addProfile, importProfile, listProfiles, readProfileContent, removeProfile, renameProfile, reorderProfiles, updateAllProfiles, updateProfile, writeProfileContent, } from "./handlers/profiles.js";
9
10
  import { proxyStatus } from "./handlers/proxy.js";
10
- import { patchSettings, readSettings, readSettingsFile, writeSettingsFile, } from "./handlers/settings.js";
11
+ import { patchSettings, readSettings } from "./handlers/settings.js";
11
12
  export function parseDaemonRequestTarget(rawTarget, hostHeader) {
12
13
  if (!rawTarget.startsWith("/") || rawTarget.startsWith("//")) {
13
14
  throw new Error("Unsupported HTTP request-target form");
@@ -49,7 +50,20 @@ export function buildRoutes() {
49
50
  auth: "control",
50
51
  handler: shutdownDaemon,
51
52
  },
53
+ {
54
+ methods: ["POST"],
55
+ pattern: path("/sash/web/bootstrap"),
56
+ auth: "control",
57
+ handler: createWebBootstrap,
58
+ },
59
+ {
60
+ methods: ["POST"],
61
+ pattern: path("/sash/web/session"),
62
+ auth: "public",
63
+ handler: redeemWebBootstrap,
64
+ },
52
65
  { methods: ["POST"], pattern: path("/sash/core/start"), auth: "control", handler: startCore },
66
+ { methods: ["PUT"], pattern: path("/sash/core/mode"), auth: "control", handler: setCoreMode },
53
67
  { methods: ["POST"], pattern: path("/sash/core/stop"), auth: "control", handler: stopCore },
54
68
  {
55
69
  methods: ["POST"],
@@ -59,11 +73,18 @@ export function buildRoutes() {
59
73
  },
60
74
  {
61
75
  methods: ["POST"],
62
- pattern: path("/sash/core/reload"),
76
+ pattern: path("/sash/core/update"),
63
77
  auth: "control",
64
- handler: reloadCoreConfig,
78
+ handler: updateCore,
65
79
  },
66
80
  { methods: ["GET"], pattern: path("/sash/proxy"), auth: "public", handler: proxyStatus },
81
+ { methods: ["GET"], pattern: path("/sash/autostart"), auth: "control", handler: readAutostart },
82
+ {
83
+ methods: ["PUT"],
84
+ pattern: path("/sash/autostart"),
85
+ auth: "control",
86
+ handler: writeAutostart,
87
+ },
67
88
  { methods: ["GET"], pattern: path("/sash/settings"), auth: "public", handler: readSettings },
68
89
  {
69
90
  methods: ["PATCH"],
@@ -71,23 +92,13 @@ export function buildRoutes() {
71
92
  auth: "control",
72
93
  handler: patchSettings,
73
94
  },
74
- {
75
- methods: ["GET"],
76
- pattern: path("/sash/settings/file"),
77
- auth: "control",
78
- handler: readSettingsFile,
79
- },
95
+ { methods: ["GET"], pattern: path("/sash/profiles"), auth: "public", handler: listProfiles },
96
+ { methods: ["POST"], pattern: path("/sash/profiles"), auth: "control", handler: addProfile },
80
97
  {
81
98
  methods: ["PUT"],
82
- pattern: path("/sash/settings/file"),
99
+ pattern: path("/sash/profiles/order"),
83
100
  auth: "control",
84
- handler: writeSettingsFile,
85
- },
86
- {
87
- methods: ["GET", "POST"],
88
- pattern: path("/sash/profiles"),
89
- auth: "public",
90
- handler: (ctx, req) => (req.method === "POST" ? addProfile(ctx, req) : listProfiles(ctx)),
101
+ handler: reorderProfiles,
91
102
  },
92
103
  {
93
104
  methods: ["POST"],
@@ -158,6 +169,14 @@ export function buildRoutes() {
158
169
  ];
159
170
  }
160
171
  function forwardToCore(ctx, req, res, target) {
172
+ const method = req.method?.toUpperCase() ?? "GET";
173
+ const pathname = coreApiTarget(target).split("?")[0] ?? "/";
174
+ const allowedMutation = (method === "PUT" && /^\/proxies\/[^/]+$/.test(pathname)) ||
175
+ (method === "DELETE" && /^\/connections(?:\/[^/]+)?$/.test(pathname));
176
+ if (isControlMutation(method) && !allowedMutation) {
177
+ sendError(res, 403, "conflict", "Use Sash controls to change managed Core configuration");
178
+ return;
179
+ }
161
180
  const runtime = ctx.settings.runtime();
162
181
  forwardHttpToCore(req, res, coreApiTarget(target), runtime.controller, runtime.secret);
163
182
  }
@@ -179,12 +198,6 @@ function methodOrder(method) {
179
198
  const index = METHOD_ORDER.indexOf(method);
180
199
  return index < 0 ? METHOD_ORDER.length : index;
181
200
  }
182
- export function requiredParam(req, name) {
183
- const value = req.params[name];
184
- if (value === undefined)
185
- throw new Error(`Missing route parameter: ${name}`);
186
- return value;
187
- }
188
201
  function writeResponse(res, response) {
189
202
  if (response.after) {
190
203
  const after = response.after;
@@ -252,7 +265,7 @@ export async function dispatch(ctx, routes, req, res) {
252
265
  if (requiresAuth &&
253
266
  !isControlRequestAuthorized(req, {
254
267
  daemonSecret: ctx.settings.committed().daemonSecret,
255
- bootToken: ctx.token,
268
+ isSessionToken: (token) => ctx.webAuth.isSession(token),
256
269
  })) {
257
270
  sendError(res, 401, "unauthorized", "Unauthorized control request");
258
271
  return;
@@ -1,4 +1,4 @@
1
- export function startProfileUpdateScheduler(profiles, scheduler = {}) {
1
+ export function startProfileUpdateScheduler(profiles, scheduler = {}, isActive) {
2
2
  const intervalMs = scheduler.intervalMs ?? 15 * 60 * 1000;
3
3
  const kickoffMs = scheduler.kickoffMs ?? 10_000;
4
4
  const scheduleInterval = scheduler.setInterval ?? setInterval;
@@ -6,6 +6,8 @@ export function startProfileUpdateScheduler(profiles, scheduler = {}) {
6
6
  const scheduleTimeout = scheduler.setTimeout ?? setTimeout;
7
7
  const clearScheduledTimeout = scheduler.clearTimeout ?? clearTimeout;
8
8
  const autoUpdateProfiles = async () => {
9
+ if (!isActive())
10
+ return;
9
11
  try {
10
12
  await profiles.updateDue();
11
13
  }
@@ -38,7 +38,7 @@ export function createDaemonServer(deps) {
38
38
  }
39
39
  if (!isWebSocketRequestAuthorized(req, {
40
40
  daemonSecret: context.settings.committed().daemonSecret,
41
- bootToken: context.token,
41
+ isSessionToken: (token) => context.webAuth.isSession(token),
42
42
  })) {
43
43
  rejectUpgrade(socket, 401, "Unauthorized WebSocket request");
44
44
  return;
@@ -79,7 +79,10 @@ export function createDaemonServer(deps) {
79
79
  rejectUpgrade(socket, 500, "WebSocket proxy failed");
80
80
  }
81
81
  });
82
- const scheduler = startProfileUpdateScheduler(context.profiles, deps.scheduler ?? {});
82
+ let scheduler;
83
+ server.once("listening", () => {
84
+ scheduler = startProfileUpdateScheduler(context.profiles, deps.scheduler ?? {}, () => !context.gate.isClosing);
85
+ });
83
86
  let listenerClosePromise;
84
87
  const closeListener = () => {
85
88
  if (listenerClosePromise)
@@ -94,7 +97,7 @@ export function createDaemonServer(deps) {
94
97
  socket.destroy();
95
98
  await closed;
96
99
  }
97
- scheduler.stop();
100
+ scheduler?.stop();
98
101
  })();
99
102
  listenerClosePromise = attempt;
100
103
  void attempt.catch(() => {
@@ -0,0 +1,52 @@
1
+ import crypto from "node:crypto";
2
+ export const WEB_BOOTSTRAP_TTL_MS = 90_000;
3
+ const MAX_PENDING_BOOTSTRAPS = 32;
4
+ const MAX_SESSIONS = 256;
5
+ function hashToken(token) {
6
+ return crypto.createHash("sha256").update(token).digest("hex");
7
+ }
8
+ export class WebAuthManager {
9
+ pendingBootstraps = new Map();
10
+ sessions = new Map();
11
+ createBootstrap(now = Date.now()) {
12
+ this.sweepExpired(now);
13
+ while (this.pendingBootstraps.size >= MAX_PENDING_BOOTSTRAPS) {
14
+ const oldest = this.pendingBootstraps.keys().next().value;
15
+ if (oldest === undefined)
16
+ break;
17
+ this.pendingBootstraps.delete(oldest);
18
+ }
19
+ const token = crypto.randomBytes(32).toString("hex");
20
+ const expiresAtMs = now + WEB_BOOTSTRAP_TTL_MS;
21
+ this.pendingBootstraps.set(hashToken(token), expiresAtMs);
22
+ return { token, expiresAt: new Date(expiresAtMs).toISOString() };
23
+ }
24
+ redeemBootstrap(token, now = Date.now()) {
25
+ this.sweepExpired(now);
26
+ if (!token)
27
+ return null;
28
+ const key = hashToken(token);
29
+ const expiresAt = this.pendingBootstraps.get(key);
30
+ if (expiresAt === undefined || expiresAt <= now)
31
+ return null;
32
+ this.pendingBootstraps.delete(key);
33
+ while (this.sessions.size >= MAX_SESSIONS) {
34
+ const oldest = this.sessions.keys().next().value;
35
+ if (oldest === undefined)
36
+ break;
37
+ this.sessions.delete(oldest);
38
+ }
39
+ const session = crypto.randomBytes(32).toString("hex");
40
+ this.sessions.set(hashToken(session), true);
41
+ return session;
42
+ }
43
+ isSession(token) {
44
+ return token !== "" && this.sessions.has(hashToken(token));
45
+ }
46
+ sweepExpired(now) {
47
+ for (const [key, expiresAt] of this.pendingBootstraps) {
48
+ if (expiresAt <= now)
49
+ this.pendingBootstraps.delete(key);
50
+ }
51
+ }
52
+ }
@@ -41,7 +41,7 @@ export function isControlRequestAuthorized(req, opts) {
41
41
  if (bearer && opts.daemonSecret && secretsEqual(bearer, opts.daemonSecret))
42
42
  return true;
43
43
  const webToken = firstHeader(req.headers["x-sash-token"]).trim();
44
- return Boolean(webToken && secretsEqual(webToken, opts.bootToken));
44
+ return Boolean(webToken && opts.isSessionToken(webToken));
45
45
  }
46
46
  function webSocketProtocols(value) {
47
47
  return (Array.isArray(value) ? value.join(",") : (value ?? ""))
@@ -56,7 +56,7 @@ export function isWebSocketRequestAuthorized(req, opts) {
56
56
  if (!protocol.startsWith(WEB_SOCKET_TOKEN_PROTOCOL_PREFIX))
57
57
  return false;
58
58
  const token = protocol.slice(WEB_SOCKET_TOKEN_PROTOCOL_PREFIX.length);
59
- return Boolean(token && secretsEqual(token, opts.bootToken));
59
+ return opts.isSessionToken(token);
60
60
  });
61
61
  }
62
62
  export function webSocketAuthResponseProtocol(value) {
@@ -8,6 +8,7 @@ const daemonFetch = async (url, init) => {
8
8
  ...(init.body !== undefined ? { body: init.body } : {}),
9
9
  direct: true,
10
10
  deadlineMs: init.timeoutMs,
11
+ headersTimeoutMs: init.timeoutMs,
11
12
  ...(init.attempts !== undefined ? { attempts: init.attempts } : {}),
12
13
  });
13
14
  return {
@@ -32,6 +33,9 @@ export class SashDaemonClient {
32
33
  health() {
33
34
  return this.client.health();
34
35
  }
36
+ createWebBootstrap() {
37
+ return this.client.createWebBootstrap();
38
+ }
35
39
  async isReachable() {
36
40
  try {
37
41
  return Boolean((await this.health()).token);
@@ -43,11 +47,23 @@ export class SashDaemonClient {
43
47
  status(fresh = false) {
44
48
  return this.client.status(fresh);
45
49
  }
50
+ autostartStatus() {
51
+ return this.client.autostartStatus();
52
+ }
53
+ setAutostart(enabled) {
54
+ return this.client.setAutostart(enabled);
55
+ }
46
56
  startCore() {
47
57
  return this.client.startCore();
48
58
  }
49
- maintenanceShutdown() {
50
- return this.client.shutdown();
59
+ restartCore() {
60
+ return this.client.restartCore();
61
+ }
62
+ updateCore(version) {
63
+ return this.client.updateCore(version);
64
+ }
65
+ patchSettings(patch) {
66
+ return this.client.patchSettings(patch);
51
67
  }
52
68
  async shutdown() {
53
69
  await this.client.shutdown();
@@ -9,6 +9,12 @@ export class HttpError extends Error {
9
9
  this.name = "HttpError";
10
10
  }
11
11
  }
12
+ export function requiredParam(req, name) {
13
+ const value = req.params[name];
14
+ if (value === undefined)
15
+ throw new Error(`Missing route parameter: ${name}`);
16
+ return value;
17
+ }
12
18
  export function parseJsonBody(req, maxBytes = 1024 * 1024) {
13
19
  return new Promise((resolve, reject) => {
14
20
  const chunks = [];
@@ -85,6 +91,7 @@ export function sendJson(res, statusCode, data) {
85
91
  res.writeHead(statusCode, {
86
92
  "Content-Type": "application/json; charset=utf-8",
87
93
  "Content-Length": Buffer.byteLength(body),
94
+ "Cache-Control": "no-store",
88
95
  });
89
96
  res.end(body);
90
97
  }