@astralyn/sash 0.1.1 → 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 (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +17 -5
  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 -15
  19. package/dist/commands/logs.js +11 -4
  20. package/dist/commands/shared.js +2 -4
  21. package/dist/commands/status.js +5 -1
  22. package/dist/commands/update.js +7 -3
  23. package/dist/commands/web.js +14 -50
  24. package/dist/contracts.js +168 -348
  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 +129 -117
  29. package/dist/daemon/context.js +13 -7
  30. package/dist/daemon/entry.js +21 -38
  31. package/dist/daemon/errors.js +3 -0
  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 +17 -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 +26 -21
  38. package/dist/daemon/scheduler.js +3 -1
  39. package/dist/daemon/server.js +5 -2
  40. package/dist/daemon-client.js +13 -3
  41. package/dist/daemon-http.js +6 -0
  42. package/dist/daemon-lifecycle.js +19 -196
  43. package/dist/github.js +7 -2
  44. package/dist/http.js +7 -5
  45. package/dist/mihomo-config.js +2 -1
  46. package/dist/paths.js +5 -7
  47. package/dist/profile-model.js +87 -0
  48. package/dist/profile-service.js +250 -587
  49. package/dist/profiles.js +43 -171
  50. package/dist/runtime-lifecycle.js +112 -194
  51. package/dist/runtime-owner.js +37 -76
  52. package/dist/sash-client.js +40 -19
  53. package/dist/settings-service.js +29 -224
  54. package/dist/settings.js +61 -289
  55. package/dist/status.js +24 -16
  56. package/dist/supervisor.js +1 -14
  57. package/dist/sysproxy/common.js +5 -45
  58. package/dist/sysproxy/factory.js +24 -65
  59. package/dist/sysproxy/snapshot.js +24 -257
  60. package/dist/sysproxy.js +1 -4
  61. package/dist/system-proxy-manager.js +37 -35
  62. package/dist/ui/assets/{ConnectionsView-CfaOG2JV.js → ConnectionsView-BgXQM6Z4.js} +1 -1
  63. package/dist/ui/assets/{LogsView-BDiRATXN.js → LogsView-2f542P8z.js} +1 -1
  64. package/dist/ui/assets/{PaginationFooter-DTytr1iu.js → PaginationFooter-cj1tcZej.js} +1 -1
  65. package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
  66. package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
  67. package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
  68. package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
  69. package/dist/ui/assets/{RulesView-CjEyCN1A.js → RulesView-CtKvlckZ.js} +1 -1
  70. package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
  71. package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
  72. package/dist/ui/assets/index-CBInDvdJ.js +19 -0
  73. package/dist/ui/assets/index-mOLy7fkB.css +1 -0
  74. package/dist/ui/assets/{theme-BwDBMsKO.js → theme-D40MF8cQ.js} +1 -1
  75. package/dist/ui/index.html +2 -2
  76. package/docs/architecture-proposal.md +109 -0
  77. package/docs/autostart.md +101 -0
  78. package/docs/backend.md +92 -224
  79. package/docs/frontend.md +60 -106
  80. package/docs/usage.md +92 -164
  81. package/package.json +3 -3
  82. package/dist/commands/upgrade.js +0 -43
  83. package/dist/core-install-transaction.js +0 -114
  84. package/dist/core-update-coordination.js +0 -105
  85. package/dist/core-update-service.js +0 -245
  86. package/dist/managed-state-transaction.js +0 -375
  87. package/dist/offline-mutation.js +0 -58
  88. package/dist/profile-migration.js +0 -101
  89. package/dist/runtime-recovery.js +0 -31
  90. package/dist/sysproxy/darwin.js +0 -287
  91. package/dist/sysproxy/gnome.js +0 -181
  92. package/dist/sysproxy/legacy.js +0 -58
  93. package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
  94. package/dist/ui/assets/CodeEditorModal-D1zn-jRS.js +0 -14
  95. package/dist/ui/assets/ProfileEditorDialog-CyyP6OF1.js +0 -1
  96. package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
  97. package/dist/ui/assets/ProfilesView-hFa7-O72.js +0 -2
  98. package/dist/ui/assets/SettingsFileDialog-CjBSyH4K.js +0 -1
  99. package/dist/ui/assets/SettingsView-D5QvJId6.css +0 -1
  100. package/dist/ui/assets/SettingsView-DsRntRfn.js +0 -2
  101. package/dist/ui/assets/index-CxQTf_P9.css +0 -1
  102. package/dist/ui/assets/index-_mb4OfYp.js +0 -19
package/dist/http.js CHANGED
@@ -87,6 +87,7 @@ export async function fetchWithRetry(url, opts = {}) {
87
87
  const bodyInactivityTimeoutMs = positiveTimeout(opts.bodyInactivityTimeoutMs, 30_000, "bodyInactivityTimeoutMs");
88
88
  const deadlineMs = positiveTimeout(opts.deadlineMs, 60_000, "deadlineMs");
89
89
  const deadline = new AbortController();
90
+ const signal = opts.signal ? AbortSignal.any([deadline.signal, opts.signal]) : deadline.signal;
90
91
  const deadlineTimer = setTimeout(() => {
91
92
  deadline.abort(new Error(`HTTP request deadline exceeded after ${deadlineMs}ms`));
92
93
  }, deadlineMs);
@@ -108,7 +109,7 @@ export async function fetchWithRetry(url, opts = {}) {
108
109
  body: opts.body,
109
110
  headersTimeout: headersTimeoutMs,
110
111
  bodyTimeout: bodyInactivityTimeoutMs,
111
- signal: deadline.signal,
112
+ signal,
112
113
  dispatcher: pickDispatcher(opts),
113
114
  });
114
115
  if (RETRYABLE_STATUS.has(res.statusCode) && attempt < attempts) {
@@ -166,10 +167,10 @@ export async function fetchWithRetry(url, opts = {}) {
166
167
  }
167
168
  catch (err) {
168
169
  lastErr = err;
169
- if (deadline.signal.aborted || attempt === attempts)
170
+ if (signal.aborted || attempt === attempts)
170
171
  break;
171
- await sleep(Math.min(4_000, 300 * 2 ** (attempt - 1)) + Math.random() * 200, deadline.signal);
172
- if (deadline.signal.aborted)
172
+ await sleep(Math.min(4_000, 300 * 2 ** (attempt - 1)) + Math.random() * 200, signal);
173
+ if (signal.aborted)
173
174
  break;
174
175
  }
175
176
  }
@@ -202,6 +203,7 @@ export async function downloadToFile(url, dest, opts) {
202
203
  const deadlineMs = positiveTimeout(opts.deadlineMs, 15 * 60_000, "deadlineMs");
203
204
  const maxRedirects = 5;
204
205
  const deadline = new AbortController();
206
+ const signal = opts.signal ? AbortSignal.any([deadline.signal, opts.signal]) : deadline.signal;
205
207
  const deadlineTimer = setTimeout(() => {
206
208
  deadline.abort(new Error(`Download deadline exceeded after ${deadlineMs}ms`));
207
209
  }, deadlineMs);
@@ -217,7 +219,7 @@ export async function downloadToFile(url, dest, opts) {
217
219
  headers: { "user-agent": USER_AGENT, ...opts.headers },
218
220
  headersTimeout: 30_000,
219
221
  bodyTimeout: stallMs,
220
- signal: deadline.signal,
222
+ signal,
221
223
  dispatcher,
222
224
  });
223
225
  const isRedirect = res.statusCode >= 300 && res.statusCode < 400 && Boolean(res.headers.location);
@@ -227,7 +227,7 @@ export function resolveSubscriptionRedirect(initial, current, location) {
227
227
  }
228
228
  return target;
229
229
  }
230
- export async function fetchSubscriptionProfile(url) {
230
+ export async function fetchSubscriptionProfile(url, signal) {
231
231
  let initial;
232
232
  try {
233
233
  initial = new URL(url);
@@ -247,6 +247,7 @@ export async function fetchSubscriptionProfile(url) {
247
247
  throw new Error(`Subscription fetch deadline exceeded after ${PROFILE_FETCH_DEADLINE_MS}ms`);
248
248
  }
249
249
  res = await fetchWithRetry(current.href, {
250
+ signal,
250
251
  attempts: 3,
251
252
  deadlineMs: remainingDeadlineMs,
252
253
  manualRedirect: true,
package/dist/paths.js CHANGED
@@ -24,27 +24,25 @@ export function sashRoot() {
24
24
  }
25
25
  export function sashLayout(root = sashRoot()) {
26
26
  const exeName = process.platform === "win32" ? "mihomo.exe" : "mihomo";
27
+ const userControlDir = process.platform === "win32"
28
+ ? path.join(envPathOr(path.join(os.homedir(), "AppData", "Local"), process.env.LOCALAPPDATA), "Sash")
29
+ : path.join(root, "state");
27
30
  return {
28
31
  root,
29
32
  binDir: path.join(root, "bin"),
30
33
  coreExe: path.join(root, "bin", exeName),
31
- configFile: path.join(root, "config.yaml"),
34
+ configFile: path.join(root, "runtime", "config.yaml"),
32
35
  settingsFile: path.join(root, "sash.json"),
33
36
  profilesDir: path.join(root, "profiles"),
34
- profilesIndexFile: path.join(root, "profiles", "index.json"),
35
37
  uiDir: path.join(root, "ui"),
36
38
  stateDir: path.join(root, "state"),
37
39
  pidFile: path.join(root, "state", "sash.pid"),
38
40
  daemonPidFile: path.join(root, "state", "sashd.pid"),
39
41
  daemonLeaseFile: path.join(root, "state", "sashd.lock"),
40
42
  daemonStartLockFile: path.join(root, "state", "sashd-start.lock"),
41
- runtimeOperationLockFile: path.join(root, "state", "runtime.lock"),
42
- mutationLockFile: path.join(root, "state", "mutation.lock"),
43
- settingsLockFile: path.join(root, "state", "settings.lock"),
44
43
  systemProxyStateFile: path.join(root, "state", "system-proxy.json"),
45
- managedStateTransactionFile: path.join(root, "state", "managed-state-transaction.json"),
44
+ systemProxyLockFile: path.join(userControlDir, "system-proxy.lock"),
46
45
  installFile: path.join(root, "state", "install.json"),
47
- coreInstallTransactionFile: path.join(root, "state", "core-install-transaction.json"),
48
46
  coreUpdateTransactionFile: path.join(root, "state", "core-update-transaction.json"),
49
47
  logsDir: path.join(root, "logs"),
50
48
  coreLogFile: path.join(root, "logs", "mihomo.log"),
@@ -0,0 +1,87 @@
1
+ import { isCanonicalIsoTimestamp, isPlainObject } from "./json-shape.js";
2
+ export const DEFAULT_PROFILE_INTERVAL_HOURS = 24;
3
+ export const MAX_PROFILE_INTERVAL_HOURS = 24 * 365;
4
+ export function parseProfilesIndex(value) {
5
+ if (!isPlainObject(value) ||
6
+ Object.keys(value).some((key) => key !== "activeId" && key !== "profiles") ||
7
+ !Array.isArray(value.profiles) ||
8
+ (value.activeId !== null && typeof value.activeId !== "string"))
9
+ throw new Error("Profiles index has an invalid shape");
10
+ const profiles = value.profiles.map(parseProfileMeta);
11
+ const ids = new Set(profiles.map((profile) => profile.id));
12
+ if (ids.size !== profiles.length)
13
+ throw new Error("Profiles index has duplicate profile ids");
14
+ if (value.activeId !== null && !ids.has(value.activeId))
15
+ throw new Error("Selected profile is missing");
16
+ return { activeId: value.activeId, profiles };
17
+ }
18
+ export function parseProfileMeta(item) {
19
+ if (!isPlainObject(item) ||
20
+ typeof item.id !== "string" ||
21
+ !/^[0-9]+$/.test(item.id) ||
22
+ typeof item.revision !== "number" ||
23
+ !Number.isSafeInteger(item.revision) ||
24
+ item.revision < 1 ||
25
+ typeof item.name !== "string" ||
26
+ !item.name.trim() ||
27
+ item.name.length > 120 ||
28
+ typeof item.url !== "string" ||
29
+ typeof item.intervalHours !== "number" ||
30
+ !Number.isSafeInteger(item.intervalHours) ||
31
+ item.intervalHours < 0 ||
32
+ item.intervalHours > MAX_PROFILE_INTERVAL_HOURS ||
33
+ !isCanonicalIsoTimestamp(item.createdAt) ||
34
+ !isCanonicalIsoTimestamp(item.updatedAt))
35
+ throw new Error("Profiles index has invalid profile metadata");
36
+ if (item.url) {
37
+ const url = new URL(item.url);
38
+ if (url.protocol !== "https:" && url.protocol !== "http:")
39
+ throw new Error("Invalid profile URL");
40
+ }
41
+ let subInfo;
42
+ if (item.subInfo !== undefined) {
43
+ const info = item.subInfo;
44
+ if (!isPlainObject(info) ||
45
+ typeof info.upload !== "number" ||
46
+ !Number.isFinite(info.upload) ||
47
+ info.upload < 0 ||
48
+ typeof info.download !== "number" ||
49
+ !Number.isFinite(info.download) ||
50
+ info.download < 0 ||
51
+ typeof info.total !== "number" ||
52
+ !Number.isFinite(info.total) ||
53
+ info.total < 0 ||
54
+ (info.expire !== undefined &&
55
+ (typeof info.expire !== "number" || !Number.isFinite(info.expire) || info.expire < 0)))
56
+ throw new Error("Invalid subscription quota");
57
+ subInfo = {
58
+ upload: info.upload,
59
+ download: info.download,
60
+ total: info.total,
61
+ ...(typeof info.expire === "number" ? { expire: info.expire } : {}),
62
+ };
63
+ }
64
+ if (item.homePage !== undefined) {
65
+ if (typeof item.homePage !== "string")
66
+ throw new Error("Invalid profile home page");
67
+ const url = new URL(item.homePage);
68
+ if (url.protocol !== "https:" && url.protocol !== "http:")
69
+ throw new Error("Invalid profile home page");
70
+ }
71
+ if (item.lastError !== undefined &&
72
+ (typeof item.lastError !== "string" || item.lastError.length > 300)) {
73
+ throw new Error("Invalid profile update error");
74
+ }
75
+ return {
76
+ id: item.id,
77
+ revision: item.revision,
78
+ name: item.name,
79
+ url: item.url,
80
+ intervalHours: item.intervalHours,
81
+ createdAt: item.createdAt,
82
+ updatedAt: item.updatedAt,
83
+ ...(subInfo ? { subInfo } : {}),
84
+ ...(typeof item.homePage === "string" ? { homePage: item.homePage } : {}),
85
+ ...(typeof item.lastError === "string" ? { lastError: item.lastError } : {}),
86
+ };
87
+ }