Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad3648ca6c | ||
|
|
0c60409874 | ||
|
|
8d8474c349 | ||
|
|
7aef4171af | ||
|
|
58df8d06e1 | ||
|
|
bc06d323c7 | ||
|
|
092b58cdc2 | ||
|
|
b54c7b23fd | ||
|
|
3dfeb98840 | ||
|
|
d81094861e | ||
|
|
9bc0db7b2a | ||
|
|
aeae58d0d8 | ||
|
|
313535fe70 | ||
|
|
9cf297613d | ||
|
|
8a3ac54204 | ||
|
|
d58d435fde | ||
|
|
ed30446136 | ||
|
|
5920055473 | ||
|
|
7fabf936b8 | ||
|
|
8879897e06 | ||
|
|
8a28596f47 | ||
|
|
027e7434f6 | ||
|
|
9f8a5a5ecc | ||
|
|
d1636a3a2a | ||
|
|
b4bc860533 | ||
|
|
3d29bd5544 | ||
|
|
7682abdc9f | ||
|
|
a48cd5bc22 | ||
|
|
52a5ce895f | ||
|
|
dc3985ee00 | ||
|
|
1707731029 | ||
|
|
cc5a36c086 | ||
|
|
d458f6b9e4 | ||
|
|
1043d4057b | ||
|
|
733660e684 | ||
|
|
7df188b3fd | ||
|
|
6792544c16 | ||
|
|
1325415085 | ||
|
|
1cf940c7dd | ||
|
|
0881bb9b0f | ||
|
|
f7d466eddb | ||
|
|
9167e04e77 | ||
|
|
9854cc4c4f | ||
|
|
761ff5305d | ||
|
|
d7dad3021f | ||
|
|
a422cc70f0 | ||
|
|
f986cbb1e9 | ||
|
|
3d2688c8be | ||
|
|
a387ea124a | ||
|
|
02fcad6a8c | ||
|
|
890c42e62f | ||
|
|
d095ddec03 | ||
|
|
fa7ee4d87f | ||
|
|
000af8bfb1 | ||
|
|
950db9eb8c | ||
|
|
53756caea3 | ||
|
|
87ceb8c87f | ||
|
|
953dfaaad0 | ||
|
|
46fb8e2cc3 | ||
|
|
8b8ba4b618 | ||
|
|
209f2ed7e0 | ||
|
|
9d16dcebd5 | ||
|
|
7e9f571df1 | ||
|
|
0fa40fc63d | ||
|
|
5d6af050ad | ||
|
|
c1df6938b5 | ||
|
|
450503afc3 | ||
|
|
85cd8aee00 | ||
|
|
ba84f9d778 | ||
|
|
c256ce36e5 | ||
|
|
1ba93cce8a | ||
|
|
0b2fb65af3 | ||
|
|
181ad46c29 | ||
|
|
d20af42053 | ||
|
|
ba3d3b2d80 | ||
|
|
1b7c41ea07 | ||
|
|
810589647b | ||
|
|
7f0d3fbb6d | ||
|
|
c6844fcc47 | ||
|
|
36b99968b6 | ||
|
|
aa16fc29d3 | ||
|
|
0c684d4831 | ||
|
|
2eb050294b | ||
|
|
93d34306bc | ||
|
|
625084cfa5 | ||
|
|
c81748da2c | ||
|
|
f48618f94b | ||
|
|
5cb893af62 | ||
|
|
c32582a1cd | ||
|
|
05d7947a29 | ||
|
|
8c8d632557 | ||
|
|
9ea2899ca7 | ||
|
|
e2076cb044 | ||
|
|
b5a6a61e2d | ||
|
|
71db966697 | ||
|
|
5d41473f47 | ||
|
|
35df7afe88 | ||
|
|
512757ca4a | ||
|
|
0003734bed | ||
|
|
694bfeabf3 | ||
|
|
fa3915bc5d | ||
|
|
998b3d3072 | ||
|
|
66fe3e4fea | ||
|
|
5298c6870b | ||
|
|
67a8a06268 | ||
|
|
51c4853d51 |
@@ -0,0 +1,17 @@
|
|||||||
|
name: Bug report
|
||||||
|
description: Report a reproducible client defect.
|
||||||
|
title: 'bug: '
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction
|
||||||
|
attributes:
|
||||||
|
label: Reproduction and expected behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Client version or commit
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Feature request
|
||||||
|
description: Propose a client-side change.
|
||||||
|
title: 'feature: '
|
||||||
|
labels: [enhancement]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: behavior
|
||||||
|
attributes:
|
||||||
|
label: Desired behavior and affected states
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: plan
|
||||||
|
attributes:
|
||||||
|
label: Planning
|
||||||
|
options:
|
||||||
|
- label: I checked PLAN.md and this does not silently change an existing decision.
|
||||||
|
required: true
|
||||||
@@ -9,4 +9,19 @@ jobs:
|
|||||||
runs-on: debian-latest
|
runs-on: debian-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
- run: test -f README.md && test -f AGENTS.md && test -f .prettierrc.json
|
- run: test -f README.md && test -f AGENTS.md && test -f .prettierrc.json
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run format:check
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm run check
|
||||||
|
- run: npm test
|
||||||
|
- run: npm run build
|
||||||
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
- working-directory: native
|
||||||
|
run: cargo fmt -- --check
|
||||||
|
- working-directory: native
|
||||||
|
run: cargo test
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Client release check
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags: ['v*.*.*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-check:
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
- run: test -s CHANGELOG.md && npm ci && npm run format:check && npm run lint && npm run check && npm run build
|
||||||
|
- run: npm test
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
name: Client dependency and security checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Audit JavaScript dependencies when a lockfile exists
|
||||||
|
run: |
|
||||||
|
if test -f package-lock.json; then
|
||||||
|
npm audit --audit-level=high
|
||||||
|
fi
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
native/target/
|
||||||
|
src-tauri/target/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ Read the parent `AGENTS.md`, `PLAN.md`, `TODO.md`, and `docs/wiki/README.md` bef
|
|||||||
- Use Prettier for Svelte, JavaScript/TypeScript, JSON, YAML, and Markdown.
|
- Use Prettier for Svelte, JavaScript/TypeScript, JSON, YAML, and Markdown.
|
||||||
- Never store passwords, tokens, or secrets in source, settings, fixtures, logs, or artifacts.
|
- Never store passwords, tokens, or secrets in source, settings, fixtures, logs, or artifacts.
|
||||||
- Do not edit the parent `PLAN.md` without explicit user permission.
|
- Do not edit the parent `PLAN.md` without explicit user permission.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Notable client changes are recorded here using the Keep a Changelog format.
|
||||||
|
Stable release entries require explicit user authorization.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
- Add administrator backup inventory and manual backup controls.
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Run formatting, linting, type checks, tests, and the AppImage build before submission. Use deterministic mocked server/API fixtures.
|
Run formatting, linting, type checks, tests, and the AppImage build before submission. Use deterministic mocked server/API fixtures.
|
||||||
|
The client uses Prettier for formatting and `svelte-check` for the lint/check
|
||||||
|
gate. Run `npm run format:check`, `npm run lint`, `npm run check`, and
|
||||||
|
`npm run build` before pushing changes.
|
||||||
|
|
||||||
|
The server address is treated as untrusted input. Client requests accept only
|
||||||
|
HTTP(S) origins without embedded credentials, query strings, or fragments.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 ReaperOfVeriod
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -2,9 +2,24 @@
|
|||||||
|
|
||||||
This directory is the home of the standalone Linux client repository.
|
This directory is the home of the standalone Linux client repository.
|
||||||
|
|
||||||
|
The current development shell connects to the server readiness endpoint and
|
||||||
|
provides an administrator login gate. The browser/dev client keeps its bearer
|
||||||
|
session in memory only. The `native/` crate defines the AppImage storage
|
||||||
|
boundary: `settings.json` contains only the server address and username, while
|
||||||
|
the session credential is stored in the Linux kernel keyring with no plaintext
|
||||||
|
fallback.
|
||||||
|
|
||||||
The client is a Svelte-based control surface distributed as an x86-64 AppImage. It communicates with `trixie-server` through the documented versioned HTTP and authenticated WebSocket interfaces.
|
The client is a Svelte-based control surface distributed as an x86-64 AppImage. It communicates with `trixie-server` through the documented versioned HTTP and authenticated WebSocket interfaces.
|
||||||
|
|
||||||
Implementation has not started. See the parent workspace's `PLAN.md`, `TODO.md`, and `AGENTS.md` before beginning work.
|
Connection recovery follows the server policy: the first retry is immediate,
|
||||||
|
then retries wait 3, 10, and 15 seconds, followed by an indefinite 30-second
|
||||||
|
interval until the server reconnects. Manual connection checks do not create
|
||||||
|
overlapping retry loops.
|
||||||
|
|
||||||
|
The client currently includes the live dashboard shell, server connection and
|
||||||
|
retry flow, administrator authentication, Twitch identity controls, chat and
|
||||||
|
moderation surfaces, bot/overlay/plugin administration, and the native keyring
|
||||||
|
storage boundary. Remaining live Twitch acceptance work is tracked in the
|
||||||
|
parent workspace's `TODO.md` and operator checklist.
|
||||||
|
|
||||||
Consumes the versioned contracts from the sibling `trixie-api` repository.
|
Consumes the versioned contracts from the sibling `trixie-api` repository.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# Security
|
# Security
|
||||||
|
|
||||||
Report vulnerabilities privately through the hosting instance. Never include credentials, tokens, or real user data in issues or fixtures.
|
Report vulnerabilities privately through the hosting instance. Never include credentials, tokens, or real user data in issues or fixtures.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Client boundary
|
||||||
|
|
||||||
|
The Linux client is a Svelte/Tauri control surface for `trixie-server`. It
|
||||||
|
renders server-owned state and sends explicit user actions through the shared
|
||||||
|
versioned HTTP/WebSocket boundary.
|
||||||
|
|
||||||
|
The client never stores Twitch refresh tokens or the administrator password,
|
||||||
|
calls Twitch directly, executes plugin code, or treats local UI state as
|
||||||
|
authoritative. Local overlay drafts and display preferences are recoverable
|
||||||
|
presentation data only; the server remains authoritative for publication,
|
||||||
|
authorization, Twitch state, and durable history.
|
||||||
|
|
||||||
|
# Saved-session recovery
|
||||||
|
|
||||||
|
Native startup verifies the keyring session before loading authenticated data.
|
||||||
|
HTTP 401/403 clears the saved session and requests login. Server failures retain
|
||||||
|
the credential and retry immediately, then after 3, 10, 15, and repeatedly 30
|
||||||
|
seconds. Manual login and logout cancel pending restoration. Dashboard-loading
|
||||||
|
failures do not invalidate a successfully verified session.
|
||||||
|
Restored sessions retain the server's mandatory credential-replacement state.
|
||||||
|
An omitted replacement flag is treated conservatively as requiring setup rather
|
||||||
|
than silently allowing the dashboard to consider setup complete.
|
||||||
|
Dashboard loading and realtime connection wait until credential setup is complete;
|
||||||
|
the server independently enforces the same operational-access restriction.
|
||||||
|
The setup form includes a current-password field because restored keyring
|
||||||
|
sessions intentionally do not restore the user's password. This lets setup
|
||||||
|
continue after a client restart without storing the password on disk.
|
||||||
|
|
||||||
|
Run `node --test src/session-recovery.test.js` for response classification and
|
||||||
|
retry-schedule coverage. This is not a substitute for native AppImage acceptance.
|
||||||
|
|
||||||
|
Keyring writes use `keyctl padd` with the session supplied through standard input,
|
||||||
|
never process arguments. Helper stdout/stderr are discarded so credential payloads
|
||||||
|
cannot enter diagnostics. Native tests cover the argument boundary, stdin delivery,
|
||||||
|
and helper failure handling without reading or modifying real keyring entries.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Overlay editor
|
||||||
|
|
||||||
|
The Overlays view edits HTML, CSS, and JavaScript independently. Each source
|
||||||
|
tab shows line numbers and a small completion picker containing literal starter
|
||||||
|
snippets. Selecting a completion only inserts text into the active draft; it
|
||||||
|
does not execute or evaluate the snippet in the client.
|
||||||
|
|
||||||
|
Draft changes are cached locally and autosaved to the server after the debounce
|
||||||
|
period. The live preview uses a sandboxed iframe. Publishing is a separate
|
||||||
|
explicit action that changes the OBS browser-source output; saving a draft does
|
||||||
|
not publish it. Diagnostics remain available through the warning indicator,
|
||||||
|
and the server applies the authoritative size, slug, and browser-source
|
||||||
|
security checks.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Client updates
|
||||||
|
|
||||||
|
The Settings view checks the fixed stable release source for
|
||||||
|
`ReaperOfVeriod/trixy-linux-client` on Gitea. It compares the release tag with
|
||||||
|
the running client version and shows the release page when a newer version is
|
||||||
|
available.
|
||||||
|
|
||||||
|
Applying an update remains manual: review the release, download the x86-64
|
||||||
|
AppImage, replace the existing file, and restart it. The client never accepts
|
||||||
|
an arbitrary update URL, silently replaces itself, or stores credentials in the
|
||||||
|
update flow.
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Trixie</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+284
@@ -0,0 +1,284 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.191"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.154"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trixy-native"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "trixy-native"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
description = "Native Linux storage boundary for the Trixie client"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Native credential boundary
|
||||||
|
|
||||||
|
This crate is the storage boundary used by the future Linux AppImage shell.
|
||||||
|
`Settings` writes only `server_address` and `username` to the client
|
||||||
|
`settings.json`. Session credentials are stored under the Linux kernel
|
||||||
|
keyring service `trixie.server.session`, scoped by server address and
|
||||||
|
username. `load_session` returns `None` when the keyring is unavailable;
|
||||||
|
callers must return to credential entry and must not create a plaintext
|
||||||
|
fallback.
|
||||||
|
|
||||||
|
The crate uses `keyctl` with argument arrays, never a shell command. It does
|
||||||
|
not log or serialize session values.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
//! Native persistence boundary for the Linux client.
|
||||||
|
//!
|
||||||
|
//! Settings are deliberately limited to non-secret connection metadata. The
|
||||||
|
//! session credential is stored as a `user` key in the Linux kernel keyring
|
||||||
|
//! through the `keyctl` utility. If keyring access fails, callers receive an
|
||||||
|
//! error and must ask for credentials again; no plaintext fallback is allowed.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::{
|
||||||
|
fs, io,
|
||||||
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const KEYRING_SERVICE: &str = "trixie.server.session";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub server_address: String,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub fn load(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||||
|
let bytes = fs::read(path)?;
|
||||||
|
serde_json::from_slice(&bytes)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let bytes = serde_json::to_vec_pretty(self)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||||
|
let temporary = path.with_extension("json.tmp");
|
||||||
|
fs::write(&temporary, bytes)?;
|
||||||
|
fs::rename(temporary, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_settings_path() -> PathBuf {
|
||||||
|
let base = std::env::var_os("XDG_CONFIG_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
|
base.join("trixie").join("settings.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_description(settings: &Settings) -> String {
|
||||||
|
let mut digest = Sha256::new();
|
||||||
|
digest.update(settings.server_address.as_bytes());
|
||||||
|
digest.update([0]);
|
||||||
|
digest.update(settings.username.as_bytes());
|
||||||
|
let digest = digest.finalize();
|
||||||
|
format!("{KEYRING_SERVICE}:{}", hex_encode(&digest))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_encode(bytes: &[u8]) -> String {
|
||||||
|
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
let mut result = String::with_capacity(bytes.len() * 2);
|
||||||
|
for byte in bytes {
|
||||||
|
result.push(HEX[(byte >> 4) as usize] as char);
|
||||||
|
result.push(HEX[(byte & 0x0f) as usize] as char);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keyctl(args: &[&str]) -> io::Result<Vec<u8>> {
|
||||||
|
let output = Command::new("keyctl").args(args).output()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"Linux kernel keyring access failed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(output.stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store_session(settings: &Settings, session: &str) -> io::Result<()> {
|
||||||
|
if session.is_empty() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty session"));
|
||||||
|
}
|
||||||
|
let description = key_description(settings);
|
||||||
|
write_key_payload(key_write_command(&description), session.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secret payloads must never be arguments: process listings can expose argv.
|
||||||
|
fn key_write_command(description: &str) -> Command {
|
||||||
|
let mut command = Command::new("keyctl");
|
||||||
|
command.args(["padd", "user", description, "@s"]);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_key_payload(mut command: Command, payload: &[u8]) -> io::Result<()> {
|
||||||
|
let mut child = command
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()?;
|
||||||
|
let result = child.stdin.take().expect("piped stdin").write_all(payload);
|
||||||
|
// Closing stdin above signals EOF. Reap the child even if writing failed.
|
||||||
|
let status = child.wait()?;
|
||||||
|
result?;
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"Linux kernel keyring access failed",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_session(settings: &Settings) -> io::Result<Option<String>> {
|
||||||
|
let description = key_description(settings);
|
||||||
|
let id = match keyctl(&["search", "@s", "user", &description]) {
|
||||||
|
Ok(value) => String::from_utf8_lossy(&value).trim().to_string(),
|
||||||
|
Err(_) => return Ok(None),
|
||||||
|
};
|
||||||
|
if id.is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let value = keyctl(&["pipe", &id])?;
|
||||||
|
String::from_utf8(value)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_session(settings: &Settings) -> io::Result<()> {
|
||||||
|
let description = key_description(settings);
|
||||||
|
let id = String::from_utf8_lossy(&keyctl(&["search", "@s", "user", &description])?)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if id.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
keyctl(&["unlink", &id, "@s"]).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_write_arguments_contain_only_metadata() {
|
||||||
|
let command = key_write_command("test-description");
|
||||||
|
let args: Vec<_> = command.get_args().collect();
|
||||||
|
assert_eq!(args, ["padd", "user", "test-description", "@s"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn payload_is_delivered_via_stdin_and_failures_are_reported() {
|
||||||
|
let mut receiver = Command::new("sh");
|
||||||
|
receiver.args(["-c", "test \"$(cat)\" = fixture-payload"]);
|
||||||
|
write_key_payload(receiver, b"fixture-payload").unwrap();
|
||||||
|
let mut failure = Command::new("sh");
|
||||||
|
failure.args(["-c", "cat >/dev/null; exit 1"]);
|
||||||
|
assert!(write_key_payload(failure, b"fixture-payload").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settings() -> Settings {
|
||||||
|
Settings {
|
||||||
|
server_address: "https://trixie.example".into(),
|
||||||
|
username: "admin".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_round_trip_contains_no_password_field() {
|
||||||
|
let directory = tempfile::tempdir().expect("temporary directory");
|
||||||
|
let path = directory.path().join("settings.json");
|
||||||
|
settings().save(&path).expect("save settings");
|
||||||
|
let text = fs::read_to_string(&path).expect("read settings");
|
||||||
|
assert!(!text.contains("password"));
|
||||||
|
assert_eq!(Settings::load(path).expect("load settings"), settings());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_description_is_stable_but_scoped_to_connection_and_user() {
|
||||||
|
let first = key_description(&settings());
|
||||||
|
assert_eq!(first, key_description(&settings()));
|
||||||
|
let mut different = settings();
|
||||||
|
different.username = "moderator".into();
|
||||||
|
assert_ne!(first, key_description(&different));
|
||||||
|
assert!(first.starts_with(KEYRING_SERVICE));
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1785
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "trixy-linux-client",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0-dev",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test src/*.test.js",
|
||||||
|
"dev": "vite",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"tauri:dev": "tauri dev",
|
||||||
|
"tauri:build": "tauri build",
|
||||||
|
"build": "vite build",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"lint": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/svelte": "^5.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
"@tauri-apps/cli": "^2.0.0",
|
||||||
|
"prettier": "^3.5.0",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
Generated
+4305
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "trixy-linux-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Trixie Linux client"
|
||||||
|
authors = ["ReaperOfVeriod"]
|
||||||
|
license = "MIT"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "trixy_linux_client_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
trixy-native = { path = "../native" }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="#081018"/>
|
||||||
|
<path d="M64 18 104 64 64 110 24 64Z" fill="none" stroke="#00b9f2" stroke-width="8"/>
|
||||||
|
<path d="M64 36 88 64 64 92 40 64Z" fill="#182b3a" stroke="#00b9f2" stroke-width="4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 305 B |
@@ -0,0 +1,65 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use trixy_native::{clear_session, default_settings_path, load_session, store_session, Settings};
|
||||||
|
|
||||||
|
fn error_message(error: std::io::Error) -> String {
|
||||||
|
error.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn load_client_settings() -> Result<Settings, String> {
|
||||||
|
let path = default_settings_path();
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(Settings {
|
||||||
|
server_address: "http://127.0.0.1:8787".to_string(),
|
||||||
|
username: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Settings::load(path).map_err(error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn save_client_settings(settings: Settings) -> Result<(), String> {
|
||||||
|
if settings.server_address.trim().is_empty() || settings.username.trim().is_empty() {
|
||||||
|
return Err("Server address and username are required.".to_string());
|
||||||
|
}
|
||||||
|
Settings::save(&settings, default_settings_path()).map_err(error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn load_server_session(settings: Settings) -> Result<Option<String>, String> {
|
||||||
|
load_session(&settings).map_err(error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn store_server_session(settings: Settings, session: String) -> Result<(), String> {
|
||||||
|
store_session(&settings, &session).map_err(error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn clear_server_session(settings: Settings) -> Result<(), String> {
|
||||||
|
clear_session(&settings).map_err(error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn client_settings_path() -> String {
|
||||||
|
PathBuf::from(default_settings_path())
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
load_client_settings,
|
||||||
|
save_client_settings,
|
||||||
|
load_server_session,
|
||||||
|
store_server_session,
|
||||||
|
clear_server_session,
|
||||||
|
client_settings_path
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running Trixie client");
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
trixy_linux_client_lib::run();
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Trixie",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "nl.reaperofveriod.trixie",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev -- --host 127.0.0.1",
|
||||||
|
"devUrl": "http://127.0.0.1:5173",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Trixie",
|
||||||
|
"width": 1366,
|
||||||
|
"height": 768,
|
||||||
|
"minWidth": 1024,
|
||||||
|
"minHeight": 640,
|
||||||
|
"resizable": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src 'self' http://127.0.0.1:8787 ws://127.0.0.1:8787 https://*.twitch.tv https://*.twitchcdn.net https://cdn.7tv.app; connect-src 'self' http: https: ws: wss:; img-src 'self' data: https:; frame-src https://player.twitch.tv; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"icon": ["icons/icon.png", "icons/icon.svg"],
|
||||||
|
"targets": ["appimage"],
|
||||||
|
"linux": {
|
||||||
|
"deb": { "depends": [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1793
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
import './styles.css';
|
||||||
|
import App from './App.svelte';
|
||||||
|
import { mount } from 'svelte';
|
||||||
|
|
||||||
|
const app = mount(App, {
|
||||||
|
target: document.getElementById('app')!,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/** Classify authoritative rejection separately from temporary server failures.
|
||||||
|
* @param {number} status HTTP response status.
|
||||||
|
*/
|
||||||
|
export function sessionResponseState(status) {
|
||||||
|
if (status === 401 || status === 403) return 'revoked';
|
||||||
|
return status >= 200 && status < 300 ? 'valid' : 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Milliseconds before each retry; attempt zero is the immediate first retry.
|
||||||
|
* @param {number} attempt Zero-based retry index.
|
||||||
|
*/
|
||||||
|
export function sessionRetryDelay(attempt) {
|
||||||
|
return [0, 3000, 10000, 15000][attempt] ?? 30000;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { sessionResponseState, sessionRetryDelay } from './session-recovery.js';
|
||||||
|
|
||||||
|
test('only explicit authentication rejection invalidates a saved session', () => {
|
||||||
|
for (const status of [401, 403]) assert.equal(sessionResponseState(status), 'revoked');
|
||||||
|
for (const status of [404, 429, 500, 502, 503])
|
||||||
|
assert.equal(sessionResponseState(status), 'unavailable');
|
||||||
|
assert.equal(sessionResponseState(200), 'valid');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saved session retries follow the shared recovery schedule', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
Array.from({ length: 7 }, (_, attempt) => sessionRetryDelay(attempt)),
|
||||||
|
[0, 3000, 10000, 15000, 30000, 30000, 30000],
|
||||||
|
);
|
||||||
|
});
|
||||||
+1118
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,5 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"],
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user