@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.
- package/CHANGELOG.md +48 -0
- package/README.md +22 -7
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/api.js +14 -45
- package/dist/app-state.js +99 -0
- package/dist/autostart/command.js +24 -0
- package/dist/autostart/context.js +43 -0
- package/dist/autostart/files.js +60 -0
- package/dist/autostart/installation.js +48 -0
- package/dist/autostart/start.js +47 -0
- package/dist/autostart/windows-registry.js +88 -0
- package/dist/autostart/windows.js +59 -0
- package/dist/autostart-contract.js +31 -0
- package/dist/autostart-entry.js +10 -0
- package/dist/autostart.js +69 -0
- package/dist/cli.js +11 -11
- package/dist/commands/auto.js +15 -0
- package/dist/commands/lifecycle.js +5 -31
- package/dist/commands/logs.js +11 -4
- package/dist/commands/shared.js +2 -4
- package/dist/commands/status.js +6 -7
- package/dist/commands/update.js +7 -3
- package/dist/commands/web.js +19 -27
- package/dist/contracts.js +171 -335
- package/dist/core-config-validation.js +22 -38
- package/dist/core-update.js +126 -762
- package/dist/core.js +20 -46
- package/dist/daemon/app.js +132 -113
- package/dist/daemon/context.js +13 -7
- package/dist/daemon/entry.js +21 -38
- package/dist/daemon/errors.js +6 -1
- package/dist/daemon/handlers/autostart.js +18 -0
- package/dist/daemon/handlers/core.js +32 -9
- package/dist/daemon/handlers/daemon.js +32 -6
- package/dist/daemon/handlers/profiles.js +12 -2
- package/dist/daemon/handlers/settings.js +9 -36
- package/dist/daemon/router.js +40 -27
- package/dist/daemon/scheduler.js +3 -1
- package/dist/daemon/server.js +6 -3
- package/dist/daemon/web-auth.js +52 -0
- package/dist/daemon-auth.js +2 -2
- package/dist/daemon-client.js +18 -2
- package/dist/daemon-http.js +7 -0
- package/dist/daemon-lifecycle.js +19 -196
- package/dist/github.js +7 -2
- package/dist/http.js +7 -5
- package/dist/log-follow.js +2 -1
- package/dist/mihomo-config.js +7 -9
- package/dist/paths.js +5 -7
- package/dist/profile-model.js +87 -0
- package/dist/profile-service.js +250 -587
- package/dist/profiles.js +43 -171
- package/dist/runtime-lifecycle.js +112 -194
- package/dist/runtime-owner.js +37 -76
- package/dist/sash-client.js +63 -21
- package/dist/settings-service.js +29 -228
- package/dist/settings.js +61 -285
- package/dist/status.js +24 -40
- package/dist/supervisor.js +1 -14
- package/dist/sysproxy/common.js +5 -45
- package/dist/sysproxy/factory.js +24 -65
- package/dist/sysproxy/snapshot.js +24 -257
- package/dist/sysproxy.js +1 -4
- package/dist/system-proxy-manager.js +37 -35
- package/dist/ui/assets/{ConnectionsView-DNGmZBSU.js → ConnectionsView-BgXQM6Z4.js} +1 -1
- package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-2f542P8z.js} +1 -1
- package/dist/ui/assets/{PaginationFooter-B3kHzRfB.js → PaginationFooter-cj1tcZej.js} +1 -1
- package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
- package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
- package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
- package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
- package/dist/ui/assets/{RulesView-D9vZBiJ1.js → RulesView-CtKvlckZ.js} +1 -1
- package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
- package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
- package/dist/ui/assets/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
- package/dist/ui/assets/index-CBInDvdJ.js +19 -0
- package/dist/ui/assets/index-mOLy7fkB.css +1 -0
- package/dist/ui/assets/{theme-BNq4FkXS.js → theme-D40MF8cQ.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/web-bootstrap.js +113 -0
- package/docs/architecture-proposal.md +109 -0
- package/docs/autostart.md +101 -0
- package/docs/backend.md +92 -216
- package/docs/frontend.md +61 -97
- package/docs/usage.md +88 -186
- package/package.json +10 -6
- package/dist/commands/upgrade.js +0 -43
- package/dist/core-install-transaction.js +0 -114
- package/dist/core-update-coordination.js +0 -105
- package/dist/core-update-service.js +0 -245
- package/dist/managed-state-transaction.js +0 -377
- package/dist/offline-mutation.js +0 -58
- package/dist/profile-migration.js +0 -101
- package/dist/runtime-recovery.js +0 -31
- package/dist/sysproxy/darwin.js +0 -287
- package/dist/sysproxy/gnome.js +0 -181
- package/dist/sysproxy/legacy.js +0 -58
- package/dist/tun-guidance.js +0 -11
- package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
- package/dist/ui/assets/CodeEditorModal-CFEnWsyh.js +0 -14
- package/dist/ui/assets/ProfileEditorDialog-BydoZthX.js +0 -1
- package/dist/ui/assets/ProfilesView-Btc1DOxE.js +0 -2
- package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
- package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
- package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
- package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
- package/dist/ui/assets/index-B61V60w_.js +0 -19
- 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
|
-
|
|
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
|
-
|
|
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((
|
|
57
|
-
status:
|
|
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
|
|
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
|
|
2
|
-
import { parseSettingsPatch, } from "../../contracts.js";
|
|
1
|
+
import { parseSettingsPatch } from "../../contracts.js";
|
|
3
2
|
import { HttpError } from "../../daemon-http.js";
|
|
4
|
-
import {
|
|
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
|
|
11
|
+
patch = parseSettingsPatch(body);
|
|
19
12
|
}
|
|
20
|
-
catch (
|
|
21
|
-
throw new HttpError(400,
|
|
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 {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
}
|
package/dist/daemon/router.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
|
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/
|
|
76
|
+
pattern: path("/sash/core/update"),
|
|
63
77
|
auth: "control",
|
|
64
|
-
handler:
|
|
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
|
-
|
|
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/
|
|
99
|
+
pattern: path("/sash/profiles/order"),
|
|
83
100
|
auth: "control",
|
|
84
|
-
handler:
|
|
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
|
-
|
|
268
|
+
isSessionToken: (token) => ctx.webAuth.isSession(token),
|
|
256
269
|
})) {
|
|
257
270
|
sendError(res, 401, "unauthorized", "Unauthorized control request");
|
|
258
271
|
return;
|
package/dist/daemon/scheduler.js
CHANGED
|
@@ -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
|
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -38,7 +38,7 @@ export function createDaemonServer(deps) {
|
|
|
38
38
|
}
|
|
39
39
|
if (!isWebSocketRequestAuthorized(req, {
|
|
40
40
|
daemonSecret: context.settings.committed().daemonSecret,
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/daemon-auth.js
CHANGED
|
@@ -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 &&
|
|
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
|
|
59
|
+
return opts.isSessionToken(token);
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
export function webSocketAuthResponseProtocol(value) {
|
package/dist/daemon-client.js
CHANGED
|
@@ -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
|
-
|
|
50
|
-
return this.client.
|
|
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();
|
package/dist/daemon-http.js
CHANGED
|
@@ -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
|
}
|