@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
package/docs/backend.md
CHANGED
|
@@ -1,260 +1,136 @@
|
|
|
1
|
-
# Backend Architecture
|
|
2
|
-
|
|
3
|
-
Sash uses a loopback-only supervisor daemon (`sashd`) to own the Core process, generated configuration, profile state, system-proxy ownership and the HTTP/WebSocket gateway.
|
|
4
|
-
|
|
5
|
-
```text
|
|
6
|
-
CLI / WebUI
|
|
7
|
-
│ http://127.0.0.1:19090
|
|
8
|
-
▼
|
|
9
|
-
sashd
|
|
10
|
-
├── /sash/* daemon control, Core lifecycle, settings, profiles, system proxy
|
|
11
|
-
├── /core/api/* authenticated controller reverse proxy
|
|
12
|
-
├── ProfileService
|
|
13
|
-
├── RuntimeLifecycle
|
|
14
|
-
│ ├── CoreSupervisor
|
|
15
|
-
│ └── SystemProxyManager
|
|
16
|
-
└── state locks / atomic persistence
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
The Core remains a non-detached child of `sashd`. Runtime transitions, disk mutations and daemon startup are serialized at separate boundaries instead of relying on PID files as locks.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 1. Module Boundaries
|
|
24
|
-
|
|
25
|
-
- `src/daemon.ts`: public facade re-exporting the daemon surface.
|
|
26
|
-
- `src/daemon/router.ts`: the single URLPattern route table plus match → auth → dispatch; `src/daemon/handlers/*` own the per-domain handlers, `src/daemon/errors.ts` maps domain errors onto the unified error envelope, and `src/daemon/context.ts` holds the shared `DaemonContext`/`DaemonGate`.
|
|
27
|
-
- `src/daemon/app.ts`: service assembly around one mutation queue; `src/daemon/server.ts` owns the HTTP/WebSocket listeners and listener close; `src/daemon/scheduler.ts` owns profile auto-update timers; `src/daemon/entry.ts` is the production entrypoint.
|
|
28
|
-
- `src/runtime-lifecycle.ts`: serialized Core/proxy state transitions.
|
|
29
|
-
- `src/supervisor.ts`: child ownership, readiness probes and verified termination.
|
|
30
|
-
- `src/daemon-lifecycle.ts`: daemon discovery, singleton startup, CLI shutdown and the maintenance boundary used by full restarts and Core updates.
|
|
31
|
-
- `src/state-lock.ts`: atomic file leases and cross-process mutation queues.
|
|
32
|
-
- `src/system-proxy-manager.ts`: durable proxy ownership journal, serialized asynchronous OS operations, generation-bound observation cache and conditional recovery.
|
|
33
|
-
- `src/sysproxy.ts` / `src/sysproxy/`: public system-proxy API plus focused Windows, macOS and GNOME asynchronous snapshot/apply backends.
|
|
34
|
-
- `src/profile-service.ts`: profile/config preparation and publication through one-shot opaque capabilities with strict optimistic rechecks.
|
|
35
|
-
- `src/core-install-record.ts`: the canonical install-record codec and release-tag validation shared by install and update paths.
|
|
36
|
-
- `src/core-update.ts`: the low-level executable/install-record transaction, force-repair quarantine and crash recovery.
|
|
37
|
-
- `src/core-update-coordination.ts`: one logical commit/rollback decision across retained managed state and the Core update journal.
|
|
38
|
-
- `src/core-update-service.ts`: staged update, runtime ownership, maintenance, publication and restoration orchestration.
|
|
39
|
-
- `src/offline-mutation.ts` / `src/runtime-recovery.ts`: daemon/offline ownership checks and the fixed legacy-proxy, journaled-proxy, stale-Core and update-recovery order.
|
|
40
|
-
- `src/http.ts` / `src/github.ts`: bounded networking and trusted release downloads, including one absolute asset budget shared across mirror attempts and redirects.
|
|
41
|
-
- `src/settings.ts`: versioned runtime schema, explicit public-field allowlist and candidate validation for `sash.json`.
|
|
42
|
-
- `src/settings-service.ts`: shared online/offline settings preparation, durable publication and runtime-transition orchestration behind a single typed `apply(patch)` transaction.
|
|
43
|
-
- `src/contracts.ts`: browser-safe API contracts and `unknown`-to-typed response parsers shared by the daemon client and WebUI.
|
|
44
|
-
- `src/sash-client.ts`: browser-safe typed client for the daemon-owned `/sash/*` API, built from those parsers; `src/daemon-client.ts` is the Node factory adding retries, deadlines and loopback-only dispatch.
|
|
45
|
-
- `src/runtime-owner.ts`: runtime ownership state machine and the start/stop/restart orchestration consumed by the thin `src/commands/*` wiring modules.
|
|
46
|
-
- `src/json-shape.ts` / `src/error-utils.ts`: domain-neutral JSON shape, canonical timestamp and unknown-error helpers; persistent readers retain their own size, missing and corruption policies.
|
|
47
|
-
- `src/status.ts`: stable CLI status/proxy observations, explicit unknown values and complete/incomplete exit semantics.
|
|
48
|
-
- `src/log-follow.ts`: bounded tail/follow cursors with creation, truncation, identity-rotation and cancellation handling.
|
|
49
|
-
- `web/src/stores/state.ts`: the single reactive WebUI state source, runtime ownership metadata and computed selectors. Focused runtime, profile, Core, telemetry and toast modules import it directly; `web/src/stores/index.ts` is only the stable public re-export facade.
|
|
50
|
-
- `web/src/views/OverviewView.vue`: page header, Core restart and responsive two-column composition. `OverviewGeneralPane.vue` owns common controls and traffic, `OverviewProxyPane.vue` owns mode-driven groups, and `useProxyLatency()` owns generation-bound latency requests.
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## 2. Ownership and Serialization
|
|
55
|
-
|
|
56
|
-
### Daemon ownership
|
|
1
|
+
# Backend Architecture
|
|
57
2
|
|
|
58
|
-
|
|
3
|
+
Sash manages one local Core through one loopback daemon. The [high-level architecture](./architecture-proposal.md) explains the overall design; this document defines its implementation boundaries.
|
|
59
4
|
|
|
60
|
-
|
|
61
|
-
- `state/runtime.lock` serializes top-level `start`, `stop`, `restart` and Core update operations.
|
|
62
|
-
- `state/mutation.lock` separates daemon-owned mutations from offline CLI mutations.
|
|
63
|
-
- `state/settings.lock` prevents concurrent first-run secret generation and settings rewrites.
|
|
64
|
-
- `state/system-proxy.json.lock` serializes proxy journal operations.
|
|
5
|
+
## Ownership
|
|
65
6
|
|
|
66
|
-
|
|
7
|
+
`daemon/entry.ts` acquires the instance lease, initializes `SashStateStore`, restores proxy ownership, terminates verified orphan Core processes, and recovers interrupted binary updates before opening the listener. Missing Core does not prevent management startup.
|
|
67
8
|
|
|
68
|
-
|
|
9
|
+
`daemon/app.ts` assembles the profile, settings, runtime and Windows services. `DaemonGate` in `daemon/context.ts` owns one in-memory mutation queue and shutdown admission. Reads do not wait for this queue. Downloads happen outside it, with deadlines and cancellation; commits reject stale saved-state revisions or runtime changes.
|
|
69
10
|
|
|
70
|
-
CLI commands
|
|
11
|
+
CLI commands use `runtime-owner.ts` and `daemon-lifecycle.ts` for read-only discovery, management startup and API calls. A live but unverified daemon blocks competing startup and cannot be stopped by an unverified signal. CLI discovery uses the observed daemon port. The private browser handoff and startup diagnostics are the only incidental CLI files.
|
|
71
12
|
|
|
72
|
-
|
|
13
|
+
| Module | Responsibility |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `app-state.ts` | The sole atomic application manifest commit |
|
|
16
|
+
| `settings.ts`, `settings-service.ts` | Validate and save preferences; reconcile explicit proxy intent |
|
|
17
|
+
| `profile-model.ts`, `profiles.ts`, `profile-service.ts` | Validate metadata/YAML, manage immutable sources and saved selection |
|
|
18
|
+
| `runtime-lifecycle.ts` | Order Core and proxy changes; retain the applied configuration and runtime revision |
|
|
19
|
+
| `supervisor.ts`, `process.ts` | Owned child handles, version/health checks and verified termination |
|
|
20
|
+
| `core.ts`, `core-update.ts`, `core-install-record.ts` | Trusted downloads and one executable/install-record transaction |
|
|
21
|
+
| `system-proxy-manager.ts`, `sysproxy/` | Windows proxy snapshot, verification and conditional recovery |
|
|
22
|
+
| `autostart.ts`, `autostart/` | Windows current-user registration and launcher validation |
|
|
23
|
+
| `daemon/router.ts`, `daemon/handlers/` | Route matching, authentication, parsing and domain dispatch |
|
|
24
|
+
| `contracts.ts`, `sash-client.ts`, `daemon-client.ts` | Shared browser-safe protocol and direct Node transport |
|
|
73
25
|
|
|
74
|
-
|
|
26
|
+
Persistent locks remain only where separate processes share a resource: daemon startup/singleton ownership and per-user Windows proxy/autostart operations. There are no runtime, settings or offline mutation locks and no CLI maintenance handoff.
|
|
75
27
|
|
|
76
|
-
|
|
28
|
+
## Saved state
|
|
77
29
|
|
|
78
|
-
|
|
79
|
-
2. The Core must pass two readiness probes before it is considered healthy.
|
|
80
|
-
3. After readiness, a bounded `/configs` probe records the actual `tun.enable` state; probe failure is represented as unknown instead of being mistaken for inactive.
|
|
81
|
-
4. Desired system proxy is applied only after readiness.
|
|
82
|
-
5. A deliberate stop restores the previous OS proxy before stopping the Core.
|
|
83
|
-
6. If proxy restoration cannot be proved, the healthy Core is left running.
|
|
84
|
-
7. Late child-exit events and delayed cleanup callbacks cannot clear a replacement child.
|
|
85
|
-
8. Controller status probes retain an owned-child generation snapshot and report stopped if that child exits or is replaced while the probe is pending.
|
|
86
|
-
9. System-proxy application verifies the same healthy owned child before and after OS changes; lost ownership immediately triggers proxy release.
|
|
87
|
-
10. Process ownership is revalidated before every graceful or force signal; failed verification or termination preserves PID ownership.
|
|
30
|
+
The only supported application manifest is:
|
|
88
31
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```text
|
|
98
|
-
/sash/* implemented by sashd
|
|
99
|
-
/core/api/* forwarded to the Core controller (HTTP and WebSocket share one rule)
|
|
100
|
-
/ui/* static dashboard assets
|
|
101
|
-
/ 302 redirect to /ui/
|
|
32
|
+
```ts
|
|
33
|
+
{
|
|
34
|
+
schemaVersion: 2,
|
|
35
|
+
revision: number,
|
|
36
|
+
settings: { mixedPort, controller, secret, allowLan, daemonPort, daemonSecret, systemProxy },
|
|
37
|
+
profiles: { activeId: string | null, profiles: ProfileMeta[] }
|
|
38
|
+
}
|
|
102
39
|
```
|
|
103
40
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| Endpoint | Method | Auth | Description |
|
|
107
|
-
| :--- | :--- | :--- | :--- |
|
|
108
|
-
| `/sash/daemon/health` | `GET` | public | Readiness, PID, start time and per-boot WebUI token. |
|
|
109
|
-
| `/sash/daemon/status` | `GET` | public | Daemon/Core/proxy/public-settings snapshot; `core.tunActive` is the verified runtime TUN state when available, while proxy `appliedKnown`/`stateKnown` and `queryError` preserve OS observation uncertainty. |
|
|
110
|
-
| `/sash/daemon/shutdown` | `POST` | control | Under the daemon mutation queue, snapshot whether Core was running, restore proxy/stop Core, return `{coreWasRunning}`, then close. Cleanup failure returns `500` and leaves the daemon available for retry. |
|
|
111
|
-
| `/sash/core/start` | `POST` | control | Rebuild config, start and wait for readiness; returns `{pid, version?, tunActive?}`. |
|
|
112
|
-
| `/sash/core/stop` | `POST` | control | Restore proxy, then stop the child; `204`. |
|
|
113
|
-
| `/sash/core/restart` | `POST` | control | Rebuild config and execute one serialized replacement; same body as start. |
|
|
114
|
-
| `/sash/core/reload` | `POST` | control | Re-render, validate and reload active config; returns `{proxyCount, source}`. |
|
|
115
|
-
| `/sash/proxy` | `GET` | public | Desired, Sash-owned and observed OS proxy state. |
|
|
116
|
-
| `/sash/settings` | `GET` | public | Public settings projection (secrets omitted). |
|
|
117
|
-
| `/sash/settings` | `PATCH` | control | Partial-object update (`SettingsPatch`); one transaction per apply, returns `{restartRequired, settings}`. Enabling `systemProxy` requires a healthy Core. |
|
|
118
|
-
| `/sash/settings/file` | `GET` | control | Raw canonical `sash.json` text, including secrets. |
|
|
119
|
-
| `/sash/settings/file` | `PUT` | control | Replace settings from raw text; parsed, diffed and applied through the same `SettingsService.apply` path. |
|
|
120
|
-
| `/sash/profiles` | `GET` / `POST` | public / control | List profiles; add a remote subscription. |
|
|
121
|
-
| `/sash/profiles/import` | `POST` | control | Import a local profile document. |
|
|
122
|
-
| `/sash/profiles/active` | `PUT` | control | Activate a profile id or `null`. |
|
|
123
|
-
| `/sash/profiles/update-all` | `POST` | control | Update every profile; always `200`, per-profile failures in `failed`. |
|
|
124
|
-
| `/sash/profiles/:id/content` | `GET` / `PUT` | control | Read or replace raw profile YAML. |
|
|
125
|
-
| `/sash/profiles/:id/update` | `POST` | control | Re-fetch one remote profile. |
|
|
126
|
-
| `/sash/profiles/:id` | `PATCH` / `DELETE` | control | Rename or remove one profile. |
|
|
127
|
-
|
|
128
|
-
Appending `?fresh=1` to status/proxy reads bypasses the short OS-state cache used by normal WebUI polling.
|
|
129
|
-
|
|
130
|
-
### Response envelope
|
|
41
|
+
`ProfileMeta` contains an ID, a content revision, a display name, source URL, update interval, timestamps and optional provider/error metadata. Content lives in `profiles/<id>/<revision>.yaml`. Names and paths are separate; clients cannot provide arbitrary file paths.
|
|
131
42
|
|
|
132
|
-
|
|
43
|
+
Saving source content validates bounded core-format YAML, atomically writes a new immutable file, then atomically commits its reference with the rest of `sash.json`. A crash before the manifest commit leaves the previous source referenced. Deletion commits removal before cleanup. Unreferenced files may remain after interrupted cleanup; they are not a second source of truth. Identical remote bytes retain the content revision. Editor writes must include the revision read when opening the editor; stale writes return a conflict.
|
|
133
44
|
|
|
134
|
-
The
|
|
45
|
+
The manifest is capped at 2 MiB, profile content at 8 MiB. Readers reject invalid schemas, duplicate IDs, invalid revisions, non-regular files and oversized content. Secrets must be nonblank, the controller must be loopback-only, and all listener ports must differ. No legacy formats or migrations are accepted. Existing invalid state is preserved.
|
|
135
46
|
|
|
136
|
-
|
|
47
|
+
`runtime/config.yaml` is derived from the selected saved profile or the built-in DIRECT-only default. Source YAML is preserved verbatim. Sash overlays operational ports, controller credentials and LAN access, removes competing managed keys, disables TUN and rejects separate TUN listeners.
|
|
137
48
|
|
|
138
|
-
|
|
49
|
+
## Save, Apply and stop
|
|
139
50
|
|
|
140
|
-
|
|
51
|
+
Network preference changes, profile selection, edits and scheduled downloads only save state. They do not restart or reload Core. System-proxy intent is a separate OS action: enable requires a healthy owned Core; disable saves the off preference before attempting restoration and can be retried without another manifest write.
|
|
141
52
|
|
|
142
|
-
|
|
53
|
+
Apply executes inside the daemon queue:
|
|
143
54
|
|
|
144
|
-
|
|
55
|
+
1. Generate the candidate from saved state and run Core's configuration validator against a private temporary file.
|
|
56
|
+
2. Restore the original system proxy. Failure leaves the current healthy Core running.
|
|
57
|
+
3. Stop the verified Core and ensure its controller is vacant.
|
|
58
|
+
4. Atomically publish the generated configuration.
|
|
59
|
+
5. Start Core, verify the expected version through consecutive readiness probes, and record the applied configuration.
|
|
60
|
+
6. Reconcile the saved system-proxy preference against the actual running port.
|
|
145
61
|
|
|
146
|
-
|
|
62
|
+
Validation failure leaves the old runtime untouched. Failure after stopping does not roll back saved edits; management stays available and reports the unapplied configuration. There is no general settings/profile/runtime compensation transaction.
|
|
147
63
|
|
|
148
|
-
|
|
64
|
+
Stop and shutdown cancel preparation work, including downloads and the configuration-test child. Shutdown rejects later mutations, drains the active operation, restores proxy state, stops Core, acknowledges with `204`, then closes the listener. Cleanup failure keeps the daemon available for retry. An active binary swap completes or rolls back in order.
|
|
149
65
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- The daemon listener binds only to `127.0.0.1`, rejects non-loopback Host headers, and only accepts loopback Core controller addresses.
|
|
153
|
-
- State-changing methods and every HTTP Core-gateway route require the persistent CLI bearer or per-boot WebUI token. Any request carrying a non-loopback Origin header is rejected outright, regardless of method.
|
|
154
|
-
- WebSocket upgrades validate loopback Origin, authentication and route boundaries.
|
|
155
|
-
- Public settings/status contracts omit controller and daemon secrets.
|
|
156
|
-
- Controller and daemon clients use a direct dispatcher with normal TLS verification; proxy environment variables apply only to remote downloads.
|
|
157
|
-
- Every managed runtime and OS/browser/package helper child removes GitHub/npm tokens, npm credential-file/auth variables and npm registry credentials. Fixed Windows/macOS system tools use trusted absolute paths; Linux desktop helpers are resolved only through absolute PATH entries.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## 5. Settings, Profiles and Config Transactions
|
|
162
|
-
|
|
163
|
-
`sash.json` has explicit `schemaVersion: 1`. Loading validates the JSON root, every field type, nonblank control-character-free secrets, port range, loopback controller address, unknown keys and all three listener ports (`mixedPort`, controller and daemon) for collisions. Version-0 files and removed version metadata migrate to canonical v1. Invalid or future-version files are never overwritten.
|
|
164
|
-
|
|
165
|
-
After managed-state recovery, daemon and offline initialization first migrate a nonblank legacy `subscriptionUrl` into an active meta-only profile. That URL has priority over any pre-profile `config.yaml`. Only when `profiles/index.json` does not exist may Sash import `config.yaml` once as the active local `Imported config` profile. A present empty index is an explicit opt-out. The candidate must be bounded, regular, valid core-format YAML and contain non-default routing content after managed operational keys are stripped: nonempty proxies/providers, or nonempty rules/groups that differ from the Sash DIRECT-only default. Exact generated defaults are not imported. Invalid candidates fail initialization without changing `config.yaml`; successful import journals the profile YAML and index under `mutation.lock` while leaving `config.yaml` byte-for-byte in place. Later `ProfileService` preparation re-renders and, when Core is installed, validates the canonical profile-derived candidate before runtime use.
|
|
166
|
-
|
|
167
|
-
`SettingsService` snapshots committed settings, creates an immutable canonical candidate, then fetches/renders/Core-validates active profile configuration outside the mutation lock. Under the short commit boundary it rechecks settings/profile snapshots and journals settings plus generated config before publication. The daemon exposes only `committedSettings` to GET/status/auth handlers; Core spawn/restart can temporarily use `runtimeSettings` while a candidate transition is in progress. The committed in-memory snapshot changes only after the journaled transition succeeds; failure restores disk/config and the old runtime. Online TUN enable is additionally committed only when the restarted Core reports `tun.enable: true`; inactive or unverified results use the same disk/config/runtime compensation path. Inactive TUN errors direct every platform to rerun a full `sash restart` from an elevated shell, which replaces the daemon itself; they preserve the data root explicitly only where it was customized or `sudo` would change the default home. A Core-only restart (for example from the dashboard) cannot elevate `sashd`.
|
|
168
|
-
|
|
169
|
-
Prepared profile work is never exposed as a mutable internal transaction object. `ProfileService` issues WeakMap-backed, one-shot opaque capabilities that reject forgery, cross-instance use and repeated consumption. Settings publication deliberately binds a weak active-source snapshot (`activeId`, profile identity/URL and exact raw YAML digest), so unrelated metadata updates do not invalidate an otherwise safe settings change. A strict active reload instead binds the committed settings, complete active `ProfileMeta`, active selection and raw digest. Both paths prepare outside the mutation boundary and consume the capability only while rechecking and publishing; only a conflict raised before the publication callback is entered receives the single bounded automatic retry.
|
|
170
|
-
|
|
171
|
-
Profile application follows:
|
|
172
|
-
|
|
173
|
-
1. Parse the untrusted source profile.
|
|
174
|
-
2. Overlay Sash-owned operational keys.
|
|
175
|
-
3. Write an isolated candidate.
|
|
176
|
-
4. Run the installed Core with `-t -d <root> -f <candidate>`.
|
|
177
|
-
5. Enter the short profile commit boundary, re-read the bounded regular index/profile files and verify the target profile identity, URL, active selection, managed-settings snapshot and exact raw-profile SHA-256 captured during preparation.
|
|
178
|
-
6. Snapshot the affected fixed roles (`sash.json`, profile YAML, index and/or `config.yaml`), then atomically persist a `publishing` record in `state/managed-state-transaction.json` before publication.
|
|
179
|
-
7. Ordinary settings/profile publication reloads or restarts only after every file is published, marks the journal `committed`, then clears it. The same journal can include the canonical `sash.json` snapshot. Startup finalizes a committed journal without rolling the published state back.
|
|
180
|
-
8. Core update publication uses a version-3 `core-update` coordination record. After profile/index/config files publish, phase `retained` preserves their exact pre-update snapshots until the Core journal has a durable health/restoration outcome. Ordinary mutations cannot consume this retained state.
|
|
181
|
-
9. On any publication or reload failure, restore every snapshot while continuing after individual restore errors, then reload the prior config when one existed. A rollback reload failure is reported explicitly. Incomplete rollback retains the journal; daemon and offline initialization recover an ordinary `publishing` journal under `mutation.lock` before reading or migrating profile state.
|
|
182
|
-
|
|
183
|
-
The daemon parses profile request bodies before its mutation boundary. Remote fetch, YAML parsing, rendering and Core validation also occur before that boundary; only recheck, publication and runtime reload are serialized. Offline commands use the same split boundary after reloading settings, verifying daemon/orphan-Core ownership and migrating the legacy setting. Remote and stored profile YAML are capped at 8 MiB; `profiles/index.json` is capped at 2 MiB, and both must be regular files. Profile requests use an absolute deadline and explicit hop-by-hop redirects: HTTPS cannot downgrade to HTTP, restricted literal addresses cannot cross origins, and public origins cannot redirect to literal private/loopback targets. Scheduled network fetches use bounded concurrency; state commits remain serialized and recheck profile identity/URL and active selection before publication. Scheduler timers are retained when daemon cleanup or listener closure fails, and are cleared only after successful shutdown.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## 6. System-Proxy Ownership
|
|
188
|
-
|
|
189
|
-
`settings.systemProxy` stores desired state. `state/system-proxy.json` separately records ownership:
|
|
190
|
-
|
|
191
|
-
```text
|
|
192
|
-
prepared: original snapshot + intended target persisted before OS writes
|
|
193
|
-
applied: target was written and read back exactly
|
|
194
|
-
restoring: restoration began and original/target-compatible partial state remains recoverable
|
|
195
|
-
```
|
|
66
|
+
Unexpected Core exits trigger bounded proxy-restoration retries. Late child events cannot clear a replacement child. Status and proxy application verify that ownership remained the same across asynchronous probes. Unknown identity or uncertain termination preserves the PID record.
|
|
196
67
|
|
|
197
|
-
|
|
68
|
+
## Core updates
|
|
198
69
|
|
|
199
|
-
|
|
70
|
+
Core acquisition selects an unmodified upstream release artifact using official GitHub metadata. Mirrors transport bytes only. Initial URLs and redirects must be HTTPS and host-allowlisted; the complete archive must match the official SHA-256 digest. Archives are capped at 128 MiB, extraction at 512 MiB, ZIP paths cannot escape, and the staged executable must report the exact requested version.
|
|
200
71
|
|
|
201
|
-
|
|
72
|
+
App captures the applied configuration when Core is running, or saved configuration when it is stopped. Download and candidate config validation leave management responsive. Before publication the queue rechecks saved-state and runtime revisions.
|
|
202
73
|
|
|
203
|
-
|
|
204
|
-
2. Derive the target and persist `prepared` atomically.
|
|
205
|
-
3. Re-read the OS state to detect changes during preparation.
|
|
206
|
-
4. Apply and verify the target.
|
|
207
|
-
5. Mark the journal `applied`.
|
|
74
|
+
`core-update.ts` receives only the staged executable and runtime callbacks. Its fixed journal contains previous/target install records and three phases:
|
|
208
75
|
|
|
209
|
-
|
|
76
|
+
| Phase | Meaning |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `prepared` | Rollback ownership is durable before moving files |
|
|
79
|
+
| `swapped` | New executable and install metadata are published |
|
|
80
|
+
| `verified` | Health verification and restoration of the original running/stopped state succeeded |
|
|
210
81
|
|
|
211
|
-
|
|
82
|
+
The old executable remains `.bak` until the final phase. Even a stopped update or first install performs a temporary start and health check immediately, with no system proxy enabled, then stops again. Failure restores the old binary/install record and, when applicable, the original running state. Recovery preserves unrecognized or corrupt files and blocks another unsafe update. A verified journal only needs final cleanup.
|
|
212
83
|
|
|
213
|
-
|
|
214
|
-
- macOS: HTTP, HTTPS, SOCKS and automatic proxy URL/state for every active network service. Existing authenticated proxy settings are not taken over because credentials cannot be restored safely.
|
|
215
|
-
- Linux: GNOME `gsettings` mode, automatic URL, HTTP authentication toggle and HTTP/HTTPS/SOCKS endpoints. Other desktop environments are reported unsupported.
|
|
84
|
+
Profile sources, metadata and settings never participate in this transaction. There is no deferred health decision, force-repair quarantine or coordinated second journal.
|
|
216
85
|
|
|
217
|
-
|
|
86
|
+
## Windows integration
|
|
218
87
|
|
|
219
|
-
|
|
88
|
+
System-proxy ownership uses `state/system-proxy.json` with `prepared`, `applied` and `restoring` phases. Before OS writes it stores the original and target Windows registry values. Managed values are proxy enable/server, bypass list and PAC URL; Windows owns `AutoDetect`, which is observed but not written or compared for ownership.
|
|
220
89
|
|
|
221
|
-
|
|
90
|
+
Recovery restores an exact owned target, or original/target-compatible partial values from `prepared`/`restoring`. Third-party changes to an already applied target block restoration. Missing journals never authorize disabling an unrelated proxy. The per-user OS lock is independent of `SASH_HOME` so separate instances cannot write the registry concurrently.
|
|
222
91
|
|
|
223
|
-
|
|
92
|
+
Proxy operations use asynchronous, bounded helper processes. Inspection is cached briefly, shared while in flight, and reports unknown while a local write is pending. Unstable journal observations are retried once and never cached. Desired, Sash-applied and OS-observed proxy state remain separate.
|
|
224
93
|
|
|
225
|
-
-
|
|
226
|
-
- Every initial/redirect URL is HTTPS and host-allowlisted.
|
|
227
|
-
- Downloads are streamed with a 128 MiB archive cap and backpressure.
|
|
228
|
-
- The complete archive must match the SHA-256 digest published in GitHub release metadata.
|
|
229
|
-
- Extraction has a 512 MiB output cap; ZIP deflate is streamed rather than fully inflated in memory.
|
|
230
|
-
- The staged executable must report the exact requested release token via `-v`.
|
|
231
|
-
- The staged executable validates the freshly generated active configuration before publication.
|
|
94
|
+
Autostart uses a current-user registry entry and hidden launcher. See [Automatic Startup](./autostart.md). Other platforms report desktop integration as unsupported; portable CLI/Core primitives remain available.
|
|
232
95
|
|
|
233
|
-
|
|
96
|
+
## HTTP contract
|
|
234
97
|
|
|
235
|
-
|
|
98
|
+
`/sash/*` is implemented by the daemon, `/core/api/*` is the authenticated Core gateway, and `/ui/*` serves the bundled dashboard. The route table is the canonical API inventory.
|
|
236
99
|
|
|
237
|
-
|
|
100
|
+
| Endpoint | Method | Access / result |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `/sash/daemon/health` | GET | Public per-boot identity, PID and start time |
|
|
103
|
+
| `/sash/daemon/status` | GET | Public management/runtime snapshot without secrets |
|
|
104
|
+
| `/sash/daemon/shutdown` | POST | Control; complete cleanup, then `204` and listener close |
|
|
105
|
+
| `/sash/web/bootstrap` | POST | Control; mint one-time browser handoff |
|
|
106
|
+
| `/sash/web/session` | POST | Redeem the handoff token supplied in the body |
|
|
107
|
+
| `/sash/core/start` | POST | Control; idempotent start, applying saved state if stopped |
|
|
108
|
+
| `/sash/core/restart` | POST | Control; Apply saved state and restart Core |
|
|
109
|
+
| `/sash/core/stop` | POST | Control; stop Core, keep management; `204` |
|
|
110
|
+
| `/sash/core/update` | POST | Control; optional `{version}`, returns `{version}` |
|
|
111
|
+
| `/sash/core/mode` | PUT | Control; `{mode}` changes running Core mode |
|
|
112
|
+
| `/sash/proxy` | GET | Public desired/applied/observed state |
|
|
113
|
+
| `/sash/settings` | GET / PATCH | Public settings / control save `{mixedPort?, allowLan?, systemProxy?}` |
|
|
114
|
+
| `/sash/autostart` | GET / PUT | Control; inspect or set `{enabled}` |
|
|
115
|
+
| `/sash/profiles` | GET / POST | Public metadata list / control remote import |
|
|
116
|
+
| `/sash/profiles/import` | POST | Control; local YAML import |
|
|
117
|
+
| `/sash/profiles/order` | PUT | Control; save complete `{ids}` order |
|
|
118
|
+
| `/sash/profiles/active` | PUT | Control; save `{id}` or `null`, without applying |
|
|
119
|
+
| `/sash/profiles/update-all` | POST | Control; saved updates, with per-profile failures |
|
|
120
|
+
| `/sash/profiles/:id/content` | GET / PUT | Control; read YAML/revision or save `{content, revision}` |
|
|
121
|
+
| `/sash/profiles/:id/update` | POST | Control; download and save new content |
|
|
122
|
+
| `/sash/profiles/:id` | PATCH / DELETE | Control; rename or remove |
|
|
238
123
|
|
|
239
|
-
|
|
124
|
+
Status includes `daemon.bootId`, `revisions.profiles` (saved-state revision), `revisions.runtime`, and `configuration: {pending, appliedProfile, appliedSettings}`. Saved selection and actual running configuration are distinct. Proxy observation flags are required; no absent flag is guessed from an old protocol. `?fresh=1` bypasses settled proxy inspection cache.
|
|
240
125
|
|
|
241
|
-
|
|
126
|
+
Success bodies are resources; empty mutations return `204`. Errors use `{error: {code, message}}`. Unknown required fields or malformed successful payloads are rejected by the shared client. Raw settings editing and config reload routes do not exist.
|
|
242
127
|
|
|
243
|
-
|
|
128
|
+
The Core gateway permits queries, node selection and connection deletion. Managed configuration changes must use Sash controls. Mode uses `/sash/core/mode`; traffic and log WebSockets use `/core/api/traffic` and `/core/api/logs`.
|
|
244
129
|
|
|
245
|
-
##
|
|
130
|
+
## Security and persistence
|
|
246
131
|
|
|
247
|
-
|
|
132
|
+
The listener binds to `127.0.0.1`, validates loopback Host/Origin, and parses one canonical origin-form target for authentication and forwarding. Core requests use a direct dispatcher. Browser requests carry private per-boot sessions; the persistent CLI bearer never reaches the browser or Core gateway. Public health identity is not a credential. API responses are non-cacheable.
|
|
248
133
|
|
|
249
|
-
-
|
|
250
|
-
- `profiles/index.json`, `profiles/<id>.yaml`: profile metadata/content.
|
|
251
|
-
- `config.yaml`: runtime config rendered from the active profile or the DIRECT-only default; a qualifying pre-profile file is preserved during its one-time local-profile import.
|
|
252
|
-
- `state/sash.pid`, `state/sashd.pid`: discovery records.
|
|
253
|
-
- `state/system-proxy.json`: durable proxy ownership transaction.
|
|
254
|
-
- `state/managed-state-transaction.json`: recoverable settings/profile/index/config snapshots, including retained Core-update coordination state.
|
|
255
|
-
- `state/install.json`: committed Core version metadata using the shared fixed-shape install-record codec.
|
|
256
|
-
- `state/core-install-transaction.json`: strict first-install publication journal.
|
|
257
|
-
- `state/core-update-transaction.json`: strict version-1 update journal or version-2 force-repair quarantine journal, including deferred managed-start rollback ownership.
|
|
258
|
-
- `state/*.lock`: local-filesystem ownership records.
|
|
134
|
+
`sash web` creates a 90-second single-use handoff in an owner-private local file. The browser removes its fragment before redemption, stores the session in tab storage and verifies the daemon boot. Only hashes are retained in the daemon, with bounded grant/session counts. Restarting Core preserves sessions; restarting the daemon invalidates them.
|
|
259
135
|
|
|
260
|
-
`
|
|
136
|
+
All helper children receive scrubbed environments. Sensitive state/logs use POSIX `0600`; browser handoffs also use owner-only Windows ACLs. Atomic publication uses a same-directory temporary file, file fsync, rename and POSIX directory fsync. Windows sharing violations are retried without deleting unverified files. Use a local filesystem supporting atomic rename and hard links.
|