Compare commits

...

9 Commits

Author SHA1 Message Date
Aiden McClelland
24eb27f005 minor bugfixes for alpha.14 (#3058)
* overwrite AllowedIPs in wg config
mute UnknownCA errors

* fix upgrade issues

* allow start9 user to access journal

* alpha.15

* sort actions lexicographically and show desc in marketplace details

* add registry package download cli command

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
2025-11-26 16:23:08 -07:00
Matt Hill
009d76ea35 More FE fixes (#3056)
* tell user to restart server after kiosk chnage

* remove unused import

* dont show tor address on server setup

* chore: address comments

* revert mock

* chore: remove uptime block on mobile

* utiliser le futur proche

* chore: comments

* don't show loading on authorities tab

* chore: fix mobile unions

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-11-25 23:43:19 +00:00
Aiden McClelland
6e8a425eb1 overwrite AllowedIPs in wg config (#3055)
mute UnknownCA errors
2025-11-21 11:30:21 -07:00
Aiden McClelland
66188d791b fix start-tunnel artifact upload 2025-11-20 10:53:23 -07:00
Aiden McClelland
015ff02d71 fix build 2025-11-20 01:05:50 -07:00
Aiden McClelland
10bfaf5415 fix start-tunnel build 2025-11-20 00:30:33 -07:00
Aiden McClelland
e3e0b85e0c Bugfix/alpha.13 (#3053)
* bugfixes for alpha.13

* minor fixes

* version bump

* start-tunnel workflow

* sdk beta 44

* defaultFilter

* fix reset-password on tunnel auth

* explicitly rebuild types

* fix typo

* ubuntu-latest runner

* add cleanup steps

* fix env on attach
2025-11-19 22:48:49 -07:00
Matt Hill
ad0632892e Various (#3051)
* tell user to restart server after kiosk chnage

* remove unused import

* dont show tor address on server setup

* chore: address comments

* revert mock

* chore: remove uptime block on mobile

* utiliser le futur proche

---------

Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
2025-11-19 10:35:07 -07:00
Aiden McClelland
f26791ba39 fix raspi fsck 2025-11-17 12:17:58 -07:00
79 changed files with 1162 additions and 515 deletions

100
.github/workflows/start-tunnel.yaml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: Start-Tunnel
on:
workflow_call:
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment
options:
- NONE
- dev
- unstable
- dev-unstable
runner:
type: choice
description: Runner
options:
- standard
- fast
arch:
type: choice
description: Architecture
options:
- ALL
- x86_64
- aarch64
- riscv64
push:
branches:
- master
- next/*
pull_request:
branches:
- master
- next/*
env:
NODEJS_VERSION: "24.11.0"
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
jobs:
compile:
name: Build Debian Package
strategy:
fail-fast: true
matrix:
arch: >-
${{
fromJson('{
"x86_64": ["x86_64"],
"aarch64": ["aarch64"],
"riscv64": ["riscv64"],
"ALL": ["x86_64", "aarch64", "riscv64"]
}')[github.event.inputs.platform || 'ALL']
}}
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps:
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel
sudo apt-get autoremove -y
sudo apt-get clean
- run: |
sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }}
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODEJS_VERSION }}
- name: Set up docker QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Configure sccache
uses: actions/github-script@v7
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Make
run: make tunnel-deb
env:
PLATFORM: ${{ matrix.arch }}
SCCACHE_GHA_ENABLED: on
SCCACHE_GHA_VERSION: 0
- uses: actions/upload-artifact@v4
with:
name: start-tunnel_${{ matrix.arch }}.deb
path: results/start-tunnel-*_${{ matrix.arch }}.deb

View File

@@ -67,8 +67,13 @@ jobs:
"ALL": ["x86_64", "aarch64"] "ALL": ["x86_64", "aarch64"]
}')[github.event.inputs.platform || 'ALL'] }')[github.event.inputs.platform || 'ALL']
}} }}
runs-on: ${{ fromJson('["ubuntu-22.04", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }} runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
steps: steps:
- name: Cleaning up unnecessary files
run: |
sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel
sudo apt-get autoremove -y
sudo apt-get clean
- run: | - run: |
sudo mount -t tmpfs tmpfs . sudo mount -t tmpfs tmpfs .
if: ${{ github.event.inputs.runner == 'fast' }} if: ${{ github.event.inputs.runner == 'fast' }}
@@ -134,7 +139,7 @@ jobs:
${{ ${{
fromJson( fromJson(
format( format(
'["ubuntu-22.04", "{0}"]', '["ubuntu-latest", "{0}"]',
fromJson('{ fromJson('{
"x86_64": "buildjet-8vcpu-ubuntu-2204", "x86_64": "buildjet-8vcpu-ubuntu-2204",
"x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204", "x86_64-nonfree": "buildjet-8vcpu-ubuntu-2204",
@@ -267,7 +272,7 @@ jobs:
index: index:
if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }} if: ${{ github.event.inputs.deploy != '' && github.event.inputs.deploy != 'NONE' }}
needs: [image] needs: [image]
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- run: >- - run: >-
curl "https://${{ curl "https://${{

View File

@@ -17,7 +17,7 @@ env:
jobs: jobs:
test: test:
name: Run Automated Tests name: Run Automated Tests
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:

View File

@@ -40,7 +40,6 @@ STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_
fi') fi')
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
REBUILD_TYPES = 1
ifeq ($(REMOTE),) ifeq ($(REMOTE),)
mkdir = mkdir -p $1 mkdir = mkdir -p $1
@@ -63,7 +62,7 @@ endif
.DELETE_ON_ERROR: .DELETE_ON_ERROR:
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel .PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel ts-bindings
all: $(STARTOS_TARGETS) all: $(STARTOS_TARGETS)
@@ -277,10 +276,9 @@ container-runtime/node_modules/.package-lock.json: container-runtime/package-loc
npm --prefix container-runtime ci npm --prefix container-runtime ci
touch container-runtime/node_modules/.package-lock.json touch container-runtime/node_modules/.package-lock.json
sdk/base/lib/osBindings/index.ts: $(shell if [ "$(REBUILD_TYPES)" -ne 0 ]; then echo core/startos/bindings/index.ts; fi) ts-bindings: core/startos/bindings/index.ts
mkdir -p sdk/base/lib/osBindings mkdir -p sdk/base/lib/osBindings
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/ rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
touch sdk/base/lib/osBindings/index.ts
core/startos/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE) core/startos/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
rm -rf core/startos/bindings rm -rf core/startos/bindings

201
agents/VERSION_BUMP.md Normal file
View File

@@ -0,0 +1,201 @@
# StartOS Version Bump Guide
This document explains how to bump the StartOS version across the entire codebase.
## Overview
When bumping from version `X.Y.Z-alpha.N` to `X.Y.Z-alpha.N+1`, you need to update files in multiple locations across the repository. The `// VERSION_BUMP` comment markers indicate where changes are needed.
## Files to Update
### 1. Core Rust Crate Version
**File: `core/startos/Cargo.toml`**
Update the version string (line ~18):
```toml
version = "0.4.0-alpha.15" # VERSION_BUMP
```
**File: `core/Cargo.lock`**
This file is auto-generated. After updating `Cargo.toml`, run:
```bash
cd core
cargo check
```
This will update the version in `Cargo.lock` automatically.
### 2. Create New Version Migration Module
**File: `core/startos/src/version/vX_Y_Z_alpha_N+1.rs`**
Create a new version file by copying the previous version and updating:
```rust
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_14}; // Update to previous version
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_15: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 15.into()] // Update number
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_14::Version; // Update to previous version
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_15.clone() // Update version name
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
// Add migration logic here if needed
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
// Add rollback logic here if needed
Ok(())
}
}
```
### 3. Update Version Module Registry
**File: `core/startos/src/version/mod.rs`**
Make changes in **5 locations**:
#### Location 1: Module Declaration (~line 57)
Add the new module after the previous version:
```rust
mod v0_4_0_alpha_14;
mod v0_4_0_alpha_15; // Add this
```
#### Location 2: Current Type Alias (~line 59)
Update the `Current` type and move the `// VERSION_BUMP` comment:
```rust
pub type Current = v0_4_0_alpha_15::Version; // VERSION_BUMP
```
#### Location 3: Version Enum (~line 175)
Remove `// VERSION_BUMP` from the previous version, add new variant, add comment:
```rust
V0_4_0_alpha_14(Wrapper<v0_4_0_alpha_14::Version>),
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>), // VERSION_BUMP
Other(exver::Version),
```
#### Location 4: as_version_t() Match (~line 233)
Remove `// VERSION_BUMP`, add new match arm, add comment:
```rust
Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
```
#### Location 5: as_exver() Match (~line 284, inside #[cfg(test)])
Remove `// VERSION_BUMP`, add new match arm, add comment:
```rust
Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
```
### 4. SDK TypeScript Version
**File: `sdk/package/lib/StartSdk.ts`**
Update the OSVersion constant (~line 64):
```typescript
export const OSVersion = testTypeVersion("0.4.0-alpha.15");
```
### 5. Web UI Package Version
**File: `web/package.json`**
Update the version field:
```json
{
"name": "startos-ui",
"version": "0.4.0-alpha.15",
...
}
```
**File: `web/package-lock.json`**
This file is auto-generated, but it's faster to update manually. Find all instances of "startos-ui" and update the version field.
## Verification Step
```
make
```
## VERSION_BUMP Comment Pattern
The `// VERSION_BUMP` comment serves as a marker for where to make changes next time:
- Always **remove** it from the old location
- **Add** the new version entry
- **Move** the comment to mark the new location
This pattern helps you quickly find all the places that need updating in the next version bump.
## Summary Checklist
- [ ] Update `core/startos/Cargo.toml` version
- [ ] Create new `core/startos/src/version/vX_Y_Z_alpha_N+1.rs` file
- [ ] Update `core/startos/src/version/mod.rs` in 5 locations
- [ ] Run `cargo check` to update `core/Cargo.lock`
- [ ] Update `sdk/package/lib/StartSdk.ts` OSVersion
- [ ] Update `web/package.json` and `web/package-lock.json` version
- [ ] Verify all changes compile/build successfully
## Migration Logic
The `up()` and `down()` methods in the version file handle database migrations:
- **up()**: Migrates the database from the previous version to this version
- **down()**: Rolls back from this version to the previous version
- **pre_up()**: Runs before migration, useful for pre-migration checks or data gathering
If no migration is needed, return `Ok(Value::Null)` for `up()` and `Ok(())` for `down()`.
For complex migrations, you may need to:
1. Update `type PreUpRes` to pass data between `pre_up()` and `up()`
2. Implement database transformations in the `up()` method
3. Implement reverse transformations in `down()` for rollback support

View File

@@ -1,4 +1,3 @@
- grub-common
- grub-efi - grub-efi
+ parted + parted
+ raspberrypi-net-mods + raspberrypi-net-mods

View File

@@ -25,5 +25,5 @@ apply_rule PREROUTING -p tcp -d $sip --dport $sport -j DNAT --to-destination $di
apply_rule OUTPUT -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport apply_rule OUTPUT -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport
if [ "$UNDO" = 1 ]; then if [ "$UNDO" = 1 ]; then
conntrack -D -p tcp -d $sip --dport $sport conntrack -D -p tcp -d $sip --dport $sport || true # conntrack returns exit 1 if no connections are active
fi fi

View File

@@ -61,7 +61,7 @@ fi
chroot /media/startos/next bash -e << "EOF" chroot /media/startos/next bash -e << "EOF"
if dpkg -s grub-common 2>&1 > /dev/null; then if [ -f /boot/grub/grub.cfg ]; then
grub-install /dev/$(eval $(lsblk -o MOUNTPOINT,PKNAME -P | grep 'MOUNTPOINT="/media/startos/root"') && echo $PKNAME) grub-install /dev/$(eval $(lsblk -o MOUNTPOINT,PKNAME -P | grep 'MOUNTPOINT="/media/startos/root"') && echo $PKNAME)
update-grub update-grub
fi fi
@@ -70,7 +70,7 @@ EOF
sync sync
umount -R /media/startos/next umount -Rl /media/startos/next
umount /media/startos/upper umount /media/startos/upper
umount /media/startos/lower umount /media/startos/lower

View File

@@ -38,7 +38,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43", "version": "0.4.0-beta.44",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -158,6 +158,8 @@ export class RpcListener {
this.unixSocketServer.listen(SOCKET_PATH) this.unixSocketServer.listen(SOCKET_PATH)
console.log("Listening on %s", SOCKET_PATH)
this.unixSocketServer.on("connection", (s) => { this.unixSocketServer.on("connection", (s) => {
let id: IdType = null let id: IdType = null
const captureId = <X>(x: X) => { const captureId = <X>(x: X) => {

2
core/Cargo.lock generated
View File

@@ -7908,7 +7908,7 @@ dependencies = [
[[package]] [[package]]
name = "start-os" name = "start-os"
version = "0.4.0-alpha.13" version = "0.4.0-alpha.15"
dependencies = [ dependencies = [
"aes 0.7.5", "aes 0.7.5",
"arti-client", "arti-client",

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -5,4 +5,4 @@ if tty -s; then
USE_TTY="-it" USE_TTY="-it"
fi fi
alias 'rust-zig-builder'='docker run '"$USE_TTY"' --rm -e "RUSTFLAGS=$RUSTFLAGS" -e "CFLAGS=-D_FORTIFY_SOURCE=2" -e "CXXFLAGS=-D_FORTIFY_SOURCE=2" -e SCCACHE_GHA_ENABLED -e SCCACHE_GHA_VERSION -e ACTIONS_RESULTS_URL -e ACTIONS_RUNTIME_TOKEN -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$HOME/.cache/sccache":/root/.cache/sccache -v "$(pwd)":/workdir -w /workdir -P start9/cargo-zigbuild' alias 'rust-zig-builder'='docker run '"$USE_TTY"' --rm -e "RUSTFLAGS=$RUSTFLAGS" -e "AWS_LC_SYS_CMAKE_TOOLCHAIN_FILE_riscv64gc_unknown_linux_musl=/root/cmake-overrides/toolchain-riscv64-musl-clang.cmake" -e SCCACHE_GHA_ENABLED -e SCCACHE_GHA_VERSION -e ACTIONS_RESULTS_URL -e ACTIONS_RUNTIME_TOKEN -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$HOME/.cache/sccache":/root/.cache/sccache -v "$(pwd)":/workdir -w /workdir -P start9/cargo-zigbuild'

View File

@@ -12,7 +12,7 @@ if [ "${PROFILE}" = "release" ]; then
BUILD_FLAGS="--release" BUILD_FLAGS="--release"
else else
if [ "$PROFILE" != "debug"]; then if [ "$PROFILE" != "debug"]; then
>&2 echo "Unknonw profile $PROFILE: falling back to debug..." >&2 echo "Unknown profile $PROFILE: falling back to debug..."
PROFILE=debug PROFILE=debug
fi fi
fi fi

View File

@@ -15,7 +15,7 @@ license = "MIT"
name = "start-os" name = "start-os"
readme = "README.md" readme = "README.md"
repository = "https://github.com/Start9Labs/start-os" repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.13" # VERSION_BUMP version = "0.4.0-alpha.15" # VERSION_BUMP
[lib] [lib]
name = "startos" name = "startos"

View File

@@ -260,11 +260,7 @@ impl NetworkInterfaceInfo {
} }
pub fn secure(&self) -> bool { pub fn secure(&self) -> bool {
self.secure.unwrap_or_else(|| { self.secure.unwrap_or(false)
self.ip_info.as_ref().map_or(false, |ip_info| {
ip_info.device_type == Some(NetworkInterfaceType::Wireguard)
}) && !self.public()
})
} }
} }

View File

@@ -29,6 +29,7 @@ use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::registry::package::get::GetPackageResponse; use crate::registry::package::get::GetPackageResponse;
use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::s9pk::v2::SIG_CONTEXT;
use crate::upload::upload; use crate::upload::upload;
use crate::util::Never; use crate::util::Never;
use crate::util::io::open_file; use crate::util::io::open_file;
@@ -154,6 +155,8 @@ pub async fn install(
})? })?
.s9pk; .s9pk;
asset.validate(SIG_CONTEXT, asset.all_signers())?;
let progress_tracker = FullProgressTracker::new(); let progress_tracker = FullProgressTracker::new();
let download_progress = progress_tracker.add_phase("Downloading".into(), Some(100)); let download_progress = progress_tracker.add_phase("Downloading".into(), Some(100));
let download = ctx let download = ctx

View File

@@ -366,6 +366,7 @@ impl LxcContainer {
} }
tokio::time::sleep(Duration::from_millis(100)).await; tokio::time::sleep(Duration::from_millis(100)).await;
} }
tracing::info!("Connected to socket in {:?}", started.elapsed());
Ok(UnixRpcClient::new(sock_path)) Ok(UnixRpcClient::new(sock_path))
} }
} }

View File

@@ -217,10 +217,15 @@ where
.write_all(&buffered) .write_all(&buffered)
.await .await
.with_kind(ErrorKind::Network)?; .with_kind(ErrorKind::Network)?;
return Ok(Some(( let stream = match mid.into_stream(Arc::new(cfg)).await {
metadata, Ok(stream) => Box::pin(stream) as AcceptStream,
Box::pin(mid.into_stream(Arc::new(cfg)).await?) as AcceptStream, Err(e) => {
))); tracing::trace!("Error completing TLS handshake: {e}");
tracing::trace!("{e:?}");
return Ok(None);
}
};
return Ok(Some((metadata, stream)));
} }
Ok(None) Ok(None)

View File

@@ -649,16 +649,6 @@ async fn torctl(
.invoke(ErrorKind::Tor) .invoke(ErrorKind::Tor)
.await?; .await?;
let logs = journalctl(
LogSource::Unit(SYSTEMD_UNIT),
Some(0),
None,
Some("0"),
false,
true,
)
.await?;
let mut tcp_stream = None; let mut tcp_stream = None;
for _ in 0..60 { for _ in 0..60 {
if let Ok(conn) = TcpStream::connect(tor_control).await { if let Ok(conn) = TcpStream::connect(tor_control).await {
@@ -720,7 +710,7 @@ async fn torctl(
ErrorKind::Tor, ErrorKind::Tor,
)); ));
} }
Ok((connection, logs)) Ok(connection)
}; };
let pre_handler = async { let pre_handler = async {
while let Some(command) = recv.recv().await { while let Some(command) = recv.recv().await {
@@ -745,7 +735,7 @@ async fn torctl(
Ok(()) Ok(())
}; };
let (mut connection, mut logs) = tokio::select! { let mut connection = tokio::select! {
res = bootstrap => res?, res = bootstrap => res?,
res = pre_handler => return res, res = pre_handler => return res,
}; };
@@ -851,46 +841,59 @@ async fn torctl(
Ok(()) Ok(())
}; };
let log_parser = async { let log_parser = async {
while let Some(log) = logs.try_next().await? { loop {
for (regex, severity) in &*LOG_REGEXES { let mut logs = journalctl(
if regex.is_match(&log.message) { LogSource::Unit(SYSTEMD_UNIT),
let (check, wipe_state) = match severity { Some(0),
ErrorLogSeverity::Fatal { wipe_state } => (false, *wipe_state), None,
ErrorLogSeverity::Unknown { wipe_state } => (true, *wipe_state), Some("0"),
}; false,
let addr = hck_key.public().get_onion_address().to_string(); true,
if !check )
|| TcpStream::connect(tor_socks) .await?;
.map_err(|e| Error::new(e, ErrorKind::Tor)) while let Some(log) = logs.try_next().await? {
.and_then(|mut tor_socks| async move { for (regex, severity) in &*LOG_REGEXES {
tokio::time::timeout( if regex.is_match(&log.message) {
Duration::from_secs(30), let (check, wipe_state) = match severity {
socks5_impl::client::connect(&mut tor_socks, (addr, 80), None) ErrorLogSeverity::Fatal { wipe_state } => (false, *wipe_state),
.map_err(|e| Error::new(e, ErrorKind::Tor)), ErrorLogSeverity::Unknown { wipe_state } => (true, *wipe_state),
) };
let addr = hck_key.public().get_onion_address().to_string();
if !check
|| TcpStream::connect(tor_socks)
.map_err(|e| Error::new(e, ErrorKind::Tor)) .map_err(|e| Error::new(e, ErrorKind::Tor))
.await? .and_then(|mut tor_socks| async move {
}) tokio::time::timeout(
.await Duration::from_secs(30),
.with_ctx(|_| (ErrorKind::Tor, "Tor is confirmed to be down")) socks5_impl::client::connect(
.log_err() &mut tor_socks,
.is_some() (addr, 80),
{ None,
if wipe_state { )
Command::new("systemctl") .map_err(|e| Error::new(e, ErrorKind::Tor)),
.arg("stop") )
.arg("tor") .map_err(|e| Error::new(e, ErrorKind::Tor))
.invoke(ErrorKind::Tor) .await?
.await?; })
tokio::fs::remove_dir_all("/var/lib/tor").await?; .await
.with_ctx(|_| (ErrorKind::Tor, "Tor is confirmed to be down"))
.log_err()
.is_some()
{
if wipe_state {
Command::new("systemctl")
.arg("stop")
.arg("tor")
.invoke(ErrorKind::Tor)
.await?;
tokio::fs::remove_dir_all("/var/lib/tor").await?;
}
return Err(Error::new(eyre!("{}", log.message), ErrorKind::Tor));
} }
return Err(Error::new(eyre!("{}", log.message), ErrorKind::Tor));
} }
} }
} }
} }
// Err(Error::new(eyre!("Log stream terminated"), ErrorKind::Tor))
Ok(())
}; };
let health_checker = async { let health_checker = async {
let mut last_success = Instant::now(); let mut last_success = Instant::now();
@@ -960,20 +963,23 @@ impl TorControl {
_thread: tokio::spawn(async move { _thread: tokio::spawn(async move {
let wipe_state = AtomicBool::new(false); let wipe_state = AtomicBool::new(false);
let mut health_timeout = Duration::from_secs(STARTING_HEALTH_TIMEOUT); let mut health_timeout = Duration::from_secs(STARTING_HEALTH_TIMEOUT);
while let Err(e) = torctl( loop {
tor_control, if let Err(e) = torctl(
tor_socks, tor_control,
&mut recv, tor_socks,
&mut thread_services, &mut recv,
&wipe_state, &mut thread_services,
&mut health_timeout, &wipe_state,
) &mut health_timeout,
.await )
{ .await
tracing::error!("{e}: Restarting tor"); {
tracing::debug!("{e:?}"); tracing::error!("TorControl : {e}");
tracing::debug!("{e:?}");
}
tracing::info!("Restarting Tor");
tokio::time::sleep(Duration::from_secs(1)).await;
} }
tracing::info!("TorControl is shut down.")
}) })
.into(), .into(),
send, send,

View File

@@ -39,6 +39,23 @@ pub struct AddTunnelParams {
public: bool, public: bool,
} }
fn sanitize_config(config: &str) -> String {
let mut res = String::with_capacity(config.len());
for line in config.lines() {
if line
.trim()
.strip_prefix("AllowedIPs")
.map_or(false, |l| l.trim().starts_with("="))
{
res.push_str("AllowedIPs = 0.0.0.0/0, ::/0");
} else {
res.push_str(line);
}
res.push('\n');
}
res
}
pub async fn add_tunnel( pub async fn add_tunnel(
ctx: RpcContext, ctx: RpcContext,
AddTunnelParams { AddTunnelParams {
@@ -86,7 +103,7 @@ pub async fn add_tunnel(
let tmpdir = TmpDir::new().await?; let tmpdir = TmpDir::new().await?;
let conf = tmpdir.join(&iface).with_extension("conf"); let conf = tmpdir.join(&iface).with_extension("conf");
write_file_atomic(&conf, &config).await?; write_file_atomic(&conf, &sanitize_config(&config)).await?;
Command::new("nmcli") Command::new("nmcli")
.arg("connection") .arg("connection")
.arg("import") .arg("import")

View File

@@ -353,7 +353,7 @@ impl FullProgressTracker {
} }
} }
pub fn progress_bar_task(&self, name: &str) -> NonDetachingJoinHandle<()> { pub fn progress_bar_task(&self, name: &str) -> NonDetachingJoinHandle<()> {
let mut stream = self.stream(None); let mut stream = self.stream(Some(Duration::from_millis(200)));
let mut bar = PhasedProgressBar::new(name); let mut bar = PhasedProgressBar::new(name);
tokio::spawn(async move { tokio::spawn(async move {
while let Some(progress) = stream.next().await { while let Some(progress) = stream.next().await {

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@@ -84,6 +85,26 @@ impl RegistryAsset<MerkleArchiveCommitment> {
) )
.await .await
} }
pub async fn download_to(
&self,
path: impl AsRef<Path>,
client: Client,
progress: PhaseProgressTrackerHandle,
) -> Result<
(
S9pk<Section<Arc<BufferedHttpSource>>>,
Arc<BufferedHttpSource>,
),
Error,
> {
let source = Arc::new(
BufferedHttpSource::with_path(path, client, self.url.clone(), progress).await?,
);
Ok((
S9pk::deserialize(&source, Some(&self.commitment)).await?,
source,
))
}
} }
pub struct BufferedHttpSource { pub struct BufferedHttpSource {
@@ -91,6 +112,19 @@ pub struct BufferedHttpSource {
file: UploadingFile, file: UploadingFile,
} }
impl BufferedHttpSource { impl BufferedHttpSource {
pub async fn with_path(
path: impl AsRef<Path>,
client: Client,
url: Url,
progress: PhaseProgressTrackerHandle,
) -> Result<Self, Error> {
let (mut handle, file) = UploadingFile::with_path(path, progress).await?;
let response = client.get(url).send().await?;
Ok(Self {
_download: tokio::spawn(async move { handle.download(response).await }).into(),
file,
})
}
pub async fn new( pub async fn new(
client: Client, client: Client,
url: Url, url: Url,
@@ -103,6 +137,9 @@ impl BufferedHttpSource {
file, file,
}) })
} }
pub async fn wait_for_buffered(&self) -> Result<(), Error> {
self.file.wait_for_complete().await
}
} }
impl ArchiveSource for BufferedHttpSource { impl ArchiveSource for BufferedHttpSource {
type FetchReader = <UploadingFile as ArchiveSource>::FetchReader; type FetchReader = <UploadingFile as ArchiveSource>::FetchReader;

View File

@@ -1,19 +1,27 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use clap::{Parser, ValueEnum}; use clap::{Parser, ValueEnum};
use exver::{ExtendedVersion, VersionRange}; use exver::{ExtendedVersion, VersionRange};
use imbl_value::InternedString; use helpers::to_tmp_path;
use imbl_value::{InternedString, json};
use itertools::Itertools; use itertools::Itertools;
use models::PackageId; use models::PackageId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker, ProgressUnits};
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::device_info::DeviceInfo; use crate::registry::device_info::DeviceInfo;
use crate::registry::package::index::{PackageIndex, PackageVersionInfo}; use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::s9pk::v2::SIG_CONTEXT;
use crate::util::VersionString; use crate::util::VersionString;
use crate::util::io::TrackingIO;
use crate::util::serde::{WithIoFormat, display_serializable}; use crate::util::serde::{WithIoFormat, display_serializable};
use crate::util::tui::choose;
#[derive( #[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
@@ -352,8 +360,7 @@ pub fn display_package_info(
info: Value, info: Value,
) -> Result<(), Error> { ) -> Result<(), Error> {
if let Some(format) = params.format { if let Some(format) = params.format {
display_serializable(format, info); return display_serializable(format, info);
return Ok(());
} }
if let Some(_) = params.rest.id { if let Some(_) = params.rest.id {
@@ -387,3 +394,90 @@ pub fn display_package_info(
} }
Ok(()) Ok(())
} }
#[derive(Debug, Deserialize, Serialize, TS, Parser)]
#[serde(rename_all = "camelCase")]
pub struct CliDownloadParams {
pub id: PackageId,
#[arg(long, short = 'v')]
#[ts(type = "string | null")]
pub target_version: Option<VersionRange>,
#[arg(short, long)]
pub dest: Option<PathBuf>,
}
pub async fn cli_download(
ctx: CliContext,
CliDownloadParams {
ref id,
target_version,
dest,
}: CliDownloadParams,
) -> Result<(), Error> {
let progress_tracker = FullProgressTracker::new();
let mut fetching_progress = progress_tracker.add_phase("Fetching".into(), Some(1));
let download_progress = progress_tracker.add_phase("Downloading".into(), Some(100));
let mut verify_progress = progress_tracker.add_phase("Verifying".into(), Some(10));
let progress = progress_tracker.progress_bar_task("Downloading S9PK...");
fetching_progress.start();
let mut res: GetPackageResponse = from_value(
ctx.call_remote::<RegistryContext>(
"package.get",
json!({
"id": &id,
"targetVersion": &target_version,
}),
)
.await?,
)?;
let PackageVersionInfo { s9pk, .. } = match res.best.len() {
0 => {
return Err(Error::new(
eyre!(
"Could not find a version of {id} that satisfies {}",
target_version.unwrap_or(VersionRange::Any)
),
ErrorKind::NotFound,
));
}
1 => res.best.pop_first().unwrap().1,
_ => {
let choices = res.best.keys().cloned().collect::<Vec<_>>();
let version = choose(
&format!("Multiple flavors of {id} available. Choose a version to download:"),
&choices,
)
.await?;
res.best.remove(version).unwrap()
}
};
s9pk.validate(SIG_CONTEXT, s9pk.all_signers())?;
fetching_progress.complete();
let dest = dest.unwrap_or_else(|| Path::new(".").join(id).with_extension("s9pk"));
let dest_tmp = to_tmp_path(&dest).with_kind(ErrorKind::Filesystem)?;
let (mut parsed, source) = s9pk
.download_to(&dest_tmp, ctx.client.clone(), download_progress)
.await?;
if let Some(size) = source.size().await {
verify_progress.set_total(size);
}
verify_progress.set_units(Some(ProgressUnits::Bytes));
let mut progress_sink = verify_progress.writer(tokio::io::sink());
parsed
.serialize(&mut TrackingIO::new(0, &mut progress_sink), true)
.await?;
progress_sink.into_inner().1.complete();
source.wait_for_buffered().await?;
tokio::fs::rename(dest_tmp, dest).await?;
progress_tracker.complete();
progress.await.unwrap();
println!("Download Complete");
Ok(())
}

View File

@@ -1,4 +1,4 @@
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async, from_fn_async_local};
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
@@ -54,6 +54,12 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_about("List installation candidate package(s)") .with_about("List installation candidate package(s)")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.subcommand(
"download",
from_fn_async_local(get::cli_download)
.no_display()
.with_about("Download an s9pk"),
)
.subcommand( .subcommand(
"category", "category",
category::category_api::<C>() category::category_api::<C>()

View File

@@ -106,7 +106,9 @@ pub struct ExecParams {
#[arg(long)] #[arg(long)]
pty_size: Option<TermSize>, pty_size: Option<TermSize>,
#[arg(short, long)] #[arg(short, long)]
env: Option<PathBuf>, env: Vec<String>,
#[arg(long)]
env_file: Option<PathBuf>,
#[arg(short, long)] #[arg(short, long)]
workdir: Option<PathBuf>, workdir: Option<PathBuf>,
#[arg(short, long)] #[arg(short, long)]
@@ -119,6 +121,7 @@ impl ExecParams {
fn exec(&self) -> Result<(), Error> { fn exec(&self) -> Result<(), Error> {
let ExecParams { let ExecParams {
env, env,
env_file,
workdir, workdir,
user, user,
chroot, chroot,
@@ -131,14 +134,15 @@ impl ExecParams {
ErrorKind::InvalidRequest, ErrorKind::InvalidRequest,
)); ));
}; };
let env_string = if let Some(env) = &env { let env_string = if let Some(env_file) = &env_file {
std::fs::read_to_string(env) std::fs::read_to_string(env_file)
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))? .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
} else { } else {
Default::default() Default::default()
}; };
let env = env_string let env = env_string
.lines() .lines()
.chain(env.iter().map(|l| l.as_str()))
.map(|l| l.trim()) .map(|l| l.trim())
.filter_map(|l| l.split_once("=")) .filter_map(|l| l.split_once("="))
.collect::<BTreeMap<_, _>>(); .collect::<BTreeMap<_, _>>();
@@ -199,6 +203,7 @@ pub fn launch(
force_stderr_tty, force_stderr_tty,
pty_size, pty_size,
env, env,
env_file,
workdir, workdir,
user, user,
chroot, chroot,
@@ -294,8 +299,11 @@ pub fn launch(
let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?; let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
let mut cmd = pty_process::Command::new("/usr/bin/start-container"); let mut cmd = pty_process::Command::new("/usr/bin/start-container");
cmd = cmd.arg("subcontainer").arg("launch-init"); cmd = cmd.arg("subcontainer").arg("launch-init");
if let Some(env) = env { for env in env {
cmd = cmd.arg("--env").arg(env); cmd = cmd.arg("-e").arg(env)
}
if let Some(env_file) = env_file {
cmd = cmd.arg("--env-file").arg(env_file);
} }
if let Some(workdir) = workdir { if let Some(workdir) = workdir {
cmd = cmd.arg("--workdir").arg(workdir); cmd = cmd.arg("--workdir").arg(workdir);
@@ -349,8 +357,11 @@ pub fn launch(
} else { } else {
let mut cmd = StdCommand::new("/usr/bin/start-container"); let mut cmd = StdCommand::new("/usr/bin/start-container");
cmd.arg("subcontainer").arg("launch-init"); cmd.arg("subcontainer").arg("launch-init");
if let Some(env) = env { for env in env {
cmd.arg("--env").arg(env); cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd.arg("--env-file").arg(env_file);
} }
if let Some(workdir) = workdir { if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir); cmd.arg("--workdir").arg(workdir);
@@ -441,6 +452,7 @@ pub fn exec(
force_stderr_tty, force_stderr_tty,
pty_size, pty_size,
env, env,
env_file,
workdir, workdir,
user, user,
chroot, chroot,
@@ -544,8 +556,11 @@ pub fn exec(
let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?; let (pty, pts) = pty_process::open().with_kind(ErrorKind::Filesystem)?;
let mut cmd = pty_process::Command::new("/usr/bin/start-container"); let mut cmd = pty_process::Command::new("/usr/bin/start-container");
cmd = cmd.arg("subcontainer").arg("exec-command"); cmd = cmd.arg("subcontainer").arg("exec-command");
if let Some(env) = env { for env in env {
cmd = cmd.arg("--env").arg(env); cmd = cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd = cmd.arg("--env-file").arg(env_file);
} }
if let Some(workdir) = workdir { if let Some(workdir) = workdir {
cmd = cmd.arg("--workdir").arg(workdir); cmd = cmd.arg("--workdir").arg(workdir);
@@ -599,8 +614,11 @@ pub fn exec(
} else { } else {
let mut cmd = StdCommand::new("/usr/bin/start-container"); let mut cmd = StdCommand::new("/usr/bin/start-container");
cmd.arg("subcontainer").arg("exec-command"); cmd.arg("subcontainer").arg("exec-command");
if let Some(env) = env { for env in env {
cmd.arg("--env").arg(env); cmd.arg("-e").arg(env);
}
if let Some(env_file) = env_file {
cmd.arg("--env-file").arg(env_file);
} }
if let Some(workdir) = workdir { if let Some(workdir) = workdir {
cmd.arg("--workdir").arg(workdir); cmd.arg("--workdir").arg(workdir);

View File

@@ -885,7 +885,7 @@ pub async fn attach(
.arg("start-container") .arg("start-container")
.arg("subcontainer") .arg("subcontainer")
.arg("exec") .arg("exec")
.arg("--env") .arg("--env-file")
.arg( .arg(
Path::new("/media/startos/images") Path::new("/media/startos/images")
.join(image_id) .join(image_id)

View File

@@ -43,7 +43,7 @@ use crate::util::rpc_client::UnixRpcClient;
use crate::volume::data_dir; use crate::volume::data_dir;
use crate::{ARCH, DATA_DIR, PACKAGE_DATA}; use crate::{ARCH, DATA_DIR, PACKAGE_DATA};
const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)] #[derive(Debug)]
pub struct ServiceState { pub struct ServiceState {

View File

@@ -117,6 +117,8 @@ impl ServiceMap {
match Service::load(ctx, id, disposition).await { match Service::load(ctx, id, disposition).await {
Ok(s) => *service = s.into(), Ok(s) => *service = s.into(),
Err(e) => { Err(e) => {
tracing::error!("Error loading service: {e}");
tracing::debug!("{e:?}");
let e = ErrorData::from(e); let e = ErrorData::from(e);
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {

View File

@@ -3,7 +3,7 @@ use imbl::HashMap;
use imbl_value::InternedString; use imbl_value::InternedString;
use itertools::Itertools; use itertools::Itertools;
use patch_db::HasModel; use patch_db::HasModel;
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
@@ -113,27 +113,12 @@ impl AuthContext for TunnelContext {
#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS, Parser)] #[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS, Parser)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
#[ts(export)]
pub struct SignerInfo { pub struct SignerInfo {
pub name: InternedString, pub name: InternedString,
} }
pub fn auth_api<C: Context>() -> ParentHandler<C> { pub fn auth_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new() crate::auth::auth::<C, TunnelContext>()
.subcommand(
"login",
from_fn_async(crate::auth::login_impl::<TunnelContext>)
.with_metadata("login", Value::Bool(true))
.no_cli(),
)
.subcommand(
"logout",
from_fn_async(crate::auth::logout::<TunnelContext>)
.with_metadata("get_session", Value::Bool(true))
.no_display()
.with_about("Log out of current auth session")
.with_call_remote::<CliContext>(),
)
.subcommand("set-password", from_fn_async(set_password_rpc).no_cli()) .subcommand("set-password", from_fn_async(set_password_rpc).no_cli())
.subcommand( .subcommand(
"set-password", "set-password",
@@ -173,19 +158,15 @@ pub fn auth_api<C: Context>() -> ParentHandler<C> {
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| { .with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
return display_serializable(format, res); return display_serializable(format, res);
} }
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![bc => "NAME", "KEY"]); table.add_row(row![bc => "NAME", "KEY"]);
for (key, info) in res { for (key, info) in res {
table.add_row(row![info.name, key]); table.add_row(row![info.name, key]);
} }
table.print_tty(false)?; table.print_tty(false)?;
Ok(()) Ok(())
}) })
.with_about("List authorized keys") .with_about("List authorized keys")
@@ -194,7 +175,7 @@ pub fn auth_api<C: Context>() -> ParentHandler<C> {
) )
} }
#[derive(Debug, Deserialize, Serialize, Parser)] #[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AddKeyParams { pub struct AddKeyParams {
pub name: InternedString, pub name: InternedString,
@@ -216,7 +197,7 @@ pub async fn add_key(
.result .result
} }
#[derive(Debug, Deserialize, Serialize, Parser)] #[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RemoveKeyParams { pub struct RemoveKeyParams {
pub key: AnyVerifyingKey, pub key: AnyVerifyingKey,
@@ -240,7 +221,7 @@ pub async fn list_keys(ctx: TunnelContext) -> Result<HashMap<AnyVerifyingKey, Si
ctx.db.peek().await.into_auth_pubkeys().de() ctx.db.peek().await.into_auth_pubkeys().de()
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize, TS)]
pub struct SetPasswordParams { pub struct SetPasswordParams {
pub password: String, pub password: String,
} }

View File

@@ -1,4 +1,5 @@
use std::io::SeekFrom; use std::io::SeekFrom;
use std::path::Path;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::task::Poll; use std::task::Poll;
@@ -56,6 +57,7 @@ struct Progress {
tracker: PhaseProgressTrackerHandle, tracker: PhaseProgressTrackerHandle,
expected_size: Option<u64>, expected_size: Option<u64>,
written: u64, written: u64,
complete: bool,
error: Option<Error>, error: Option<Error>,
} }
impl Progress { impl Progress {
@@ -111,9 +113,7 @@ impl Progress {
} }
async fn ready(watch: &mut watch::Receiver<Self>) -> Result<(), Error> { async fn ready(watch: &mut watch::Receiver<Self>) -> Result<(), Error> {
match &*watch match &*watch
.wait_for(|progress| { .wait_for(|progress| progress.error.is_some() || progress.complete)
progress.error.is_some() || Some(progress.written) == progress.expected_size
})
.await .await
.map_err(|_| { .map_err(|_| {
Error::new( Error::new(
@@ -126,8 +126,9 @@ impl Progress {
} }
} }
fn complete(&mut self) -> bool { fn complete(&mut self) -> bool {
let mut changed = !self.complete;
self.tracker.complete(); self.tracker.complete();
match self { changed |= match self {
Self { Self {
expected_size: Some(size), expected_size: Some(size),
written, written,
@@ -165,18 +166,21 @@ impl Progress {
true true
} }
_ => false, _ => false,
} };
self.complete = true;
changed
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct UploadingFile { pub struct UploadingFile {
tmp_dir: Arc<TmpDir>, tmp_dir: Option<Arc<TmpDir>>,
file: MultiCursorFile, file: MultiCursorFile,
progress: watch::Receiver<Progress>, progress: watch::Receiver<Progress>,
} }
impl UploadingFile { impl UploadingFile {
pub async fn new( pub async fn with_path(
path: impl AsRef<Path>,
mut progress: PhaseProgressTrackerHandle, mut progress: PhaseProgressTrackerHandle,
) -> Result<(UploadHandle, Self), Error> { ) -> Result<(UploadHandle, Self), Error> {
progress.set_units(Some(ProgressUnits::Bytes)); progress.set_units(Some(ProgressUnits::Bytes));
@@ -185,25 +189,35 @@ impl UploadingFile {
expected_size: None, expected_size: None,
written: 0, written: 0,
error: None, error: None,
complete: false,
}); });
let tmp_dir = Arc::new(TmpDir::new().await?); let file = create_file(path).await?;
let file = create_file(tmp_dir.join("upload.tmp")).await?;
let uploading = Self { let uploading = Self {
tmp_dir: tmp_dir.clone(), tmp_dir: None,
file: MultiCursorFile::open(&file).await?, file: MultiCursorFile::open(&file).await?,
progress: progress.1, progress: progress.1,
}; };
Ok(( Ok((
UploadHandle { UploadHandle {
tmp_dir, tmp_dir: None,
file, file,
progress: progress.0, progress: progress.0,
}, },
uploading, uploading,
)) ))
} }
pub async fn new(progress: PhaseProgressTrackerHandle) -> Result<(UploadHandle, Self), Error> {
let tmp_dir = Arc::new(TmpDir::new().await?);
let (mut handle, mut file) = Self::with_path(tmp_dir.join("upload.tmp"), progress).await?;
handle.tmp_dir = Some(tmp_dir.clone());
file.tmp_dir = Some(tmp_dir);
Ok((handle, file))
}
pub async fn wait_for_complete(&self) -> Result<(), Error> {
Progress::ready(&mut self.progress.clone()).await
}
pub async fn delete(self) -> Result<(), Error> { pub async fn delete(self) -> Result<(), Error> {
if let Ok(tmp_dir) = Arc::try_unwrap(self.tmp_dir) { if let Some(Ok(tmp_dir)) = self.tmp_dir.map(Arc::try_unwrap) {
tmp_dir.delete().await?; tmp_dir.delete().await?;
} }
Ok(()) Ok(())
@@ -234,7 +248,7 @@ impl ArchiveSource for UploadingFile {
#[pin_project::pin_project(project = UploadingFileReaderProjection)] #[pin_project::pin_project(project = UploadingFileReaderProjection)]
pub struct UploadingFileReader { pub struct UploadingFileReader {
tmp_dir: Arc<TmpDir>, tmp_dir: Option<Arc<TmpDir>>,
position: u64, position: u64,
to_seek: Option<SeekFrom>, to_seek: Option<SeekFrom>,
#[pin] #[pin]
@@ -330,7 +344,7 @@ impl AsyncSeek for UploadingFileReader {
#[pin_project::pin_project(PinnedDrop)] #[pin_project::pin_project(PinnedDrop)]
pub struct UploadHandle { pub struct UploadHandle {
tmp_dir: Arc<TmpDir>, tmp_dir: Option<Arc<TmpDir>>,
#[pin] #[pin]
file: File, file: File,
progress: watch::Sender<Progress>, progress: watch::Sender<Progress>,
@@ -377,6 +391,9 @@ impl UploadHandle {
break; break;
} }
} }
if let Err(e) = self.file.sync_all().await {
self.progress.send_if_modified(|p| p.handle_error(&e));
}
} }
} }
#[pin_project::pinned_drop] #[pin_project::pinned_drop]

View File

@@ -53,8 +53,10 @@ mod v0_4_0_alpha_10;
mod v0_4_0_alpha_11; mod v0_4_0_alpha_11;
mod v0_4_0_alpha_12; mod v0_4_0_alpha_12;
mod v0_4_0_alpha_13; mod v0_4_0_alpha_13;
mod v0_4_0_alpha_14;
mod v0_4_0_alpha_15;
pub type Current = v0_4_0_alpha_13::Version; // VERSION_BUMP pub type Current = v0_4_0_alpha_15::Version; // VERSION_BUMP
impl Current { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -169,7 +171,9 @@ enum Version {
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>), V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>),
V0_4_0_alpha_11(Wrapper<v0_4_0_alpha_11::Version>), V0_4_0_alpha_11(Wrapper<v0_4_0_alpha_11::Version>),
V0_4_0_alpha_12(Wrapper<v0_4_0_alpha_12::Version>), V0_4_0_alpha_12(Wrapper<v0_4_0_alpha_12::Version>),
V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::Version>), // VERSION_BUMP V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::Version>),
V0_4_0_alpha_14(Wrapper<v0_4_0_alpha_14::Version>),
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>), // VERSION_BUMP
Other(exver::Version), Other(exver::Version),
} }
@@ -225,7 +229,9 @@ impl Version {
Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_11(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_11(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_12(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_12(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), eyre!("unknown version {v}"),
@@ -273,7 +279,9 @@ impl Version {
Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_11(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_11(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_12(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_12(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(), Version::Other(x) => x.clone(),
} }
} }

View File

@@ -4,7 +4,7 @@ use exver::{PreReleaseSegment, VersionRange};
use imbl_value::InternedString; use imbl_value::InternedString;
use super::v0_3_5::V0_3_0_COMPAT; use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_11}; use super::{v0_4_0_alpha_11, VersionT};
use crate::net::tor::TorSecretKey; use crate::net::tor::TorSecretKey;
use crate::prelude::*; use crate::prelude::*;
@@ -75,7 +75,10 @@ impl VersionT for Version {
} }
fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?; fix_host(&mut db["public"]["serverInfo"]["network"]["host"])?;
db["private"]["keyStore"]["localCerts"] = db["private"]["keyStore"]["local_certs"].clone(); if db["private"]["keyStore"]["localCerts"].is_null() {
db["private"]["keyStore"]["localCerts"] =
db["private"]["keyStore"]["local_certs"].clone();
}
Ok(Value::Null) Ok(Value::Null)
} }

View File

@@ -0,0 +1,37 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_13};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_14: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 14.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_13::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_14.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,37 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{VersionT, v0_4_0_alpha_14};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_15: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 15.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_14::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_15.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument(skip_all)]
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -205,6 +205,7 @@ fi
useradd --shell /bin/bash -G startos -m start9 useradd --shell /bin/bash -G startos -m start9
echo start9:embassy | chpasswd echo start9:embassy | chpasswd
usermod -aG sudo start9 usermod -aG sudo start9
usermod -aG systemd-journal start9
echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd" echo "start9 ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "/etc/sudoers.d/010_start9-nopasswd"

View File

@@ -1,3 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AnyVerifyingKey } from "./AnyVerifyingKey"
import type { ContactInfo } from "./ContactInfo"
export type SignerInfo = { name: string } export type SignerInfo = {
name: string
contact: Array<ContactInfo>
keys: Array<AnyVerifyingKey>
}

View File

@@ -3,7 +3,7 @@ import { knownProtocols } from "../interfaces/Host"
import { AddressInfo, Host, Hostname, HostnameInfo } from "../types" import { AddressInfo, Host, Hostname, HostnameInfo } from "../types"
import { Effects } from "../Effects" import { Effects } from "../Effects"
import { DropGenerator, DropPromise } from "./Drop" import { DropGenerator, DropPromise } from "./Drop"
import { IPV6_LINK_LOCAL } from "./ip" import { IpAddress, IPV6_LINK_LOCAL } from "./ip"
export type UrlString = string export type UrlString = string
export type HostId = string export type HostId = string
@@ -17,7 +17,15 @@ export const getHostname = (url: string): Hostname | null => {
return last return last
} }
type FilterKinds = "onion" | "local" | "domain" | "ip" | "ipv4" | "ipv6" type FilterKinds =
| "onion"
| "local"
| "domain"
| "ip"
| "ipv4"
| "ipv6"
| "localhost"
| "link-local"
export type Filter = { export type Filter = {
visibility?: "public" | "private" visibility?: "public" | "private"
kind?: FilterKinds | FilterKinds[] kind?: FilterKinds | FilterKinds[]
@@ -72,6 +80,12 @@ type FilterReturnTy<F extends Filter> = F extends {
: Exclude<HostnameInfo, FilterReturnTy<E>> : Exclude<HostnameInfo, FilterReturnTy<E>>
: HostnameInfo : HostnameInfo
const defaultFilter = {
exclude: {
kind: ["localhost", "link-local"] as ("localhost" | "link-local")[],
},
}
type Formats = "hostname-info" | "urlstring" | "url" type Formats = "hostname-info" | "urlstring" | "url"
type FormatReturnTy< type FormatReturnTy<
F extends Filter, F extends Filter,
@@ -92,8 +106,11 @@ export type Filled = {
sslUrl: UrlString | null sslUrl: UrlString | null
} }
filter: <F extends Filter, Format extends Formats = "urlstring">( filter: <
filter: F, F extends Filter = typeof defaultFilter,
Format extends Formats = "urlstring",
>(
filter?: F,
format?: Format, format?: Format,
) => FormatReturnTy<F, Format>[] ) => FormatReturnTy<F, Format>[]
@@ -215,7 +232,13 @@ function filterRec(
h.kind === "ip" && h.kind === "ip" &&
h.hostname.kind === "domain") || h.hostname.kind === "domain") ||
(kind.has("ipv4") && h.kind === "ip" && h.hostname.kind === "ipv4") || (kind.has("ipv4") && h.kind === "ip" && h.hostname.kind === "ipv4") ||
(kind.has("ipv6") && h.kind === "ip" && h.hostname.kind === "ipv6")), (kind.has("ipv6") && h.kind === "ip" && h.hostname.kind === "ipv6") ||
(kind.has("localhost") &&
["localhost", "127.0.0.1", "[::1]"].includes(h.hostname.value)) ||
(kind.has("link-local") &&
h.kind === "ip" &&
h.hostname.kind === "ipv6" &&
IPV6_LINK_LOCAL.contains(IpAddress.parse(h.hostname.value)))),
) )
} }
@@ -239,11 +262,14 @@ export const filledAddress = (
...addressInfo, ...addressInfo,
hostnames, hostnames,
toUrls, toUrls,
filter: <F extends Filter, Format extends Formats = "urlstring">( filter: <
filter: F, F extends Filter = typeof defaultFilter,
Format extends Formats = "urlstring",
>(
filter?: F,
format?: Format, format?: Format,
) => { ) => {
const filtered = filterRec(hostnames, filter, false) const filtered = filterRec(hostnames, filter ?? defaultFilter, false)
let res: FormatReturnTy<F, Format>[] = filtered as any let res: FormatReturnTy<F, Format>[] = filtered as any
if (format === "hostname-info") return res if (format === "hostname-info") return res
const urls = filtered.flatMap(toUrlArray) const urls = filtered.flatMap(toUrlArray)

View File

@@ -61,7 +61,7 @@ import {
} from "../../base/lib/inits" } from "../../base/lib/inits"
import { DropGenerator } from "../../base/lib/util/Drop" import { DropGenerator } from "../../base/lib/util/Drop"
export const OSVersion = testTypeVersion("0.4.0-alpha.13") export const OSVersion = testTypeVersion("0.4.0-alpha.15")
// prettier-ignore // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -410,12 +410,17 @@ export class SubContainerOwned<
workdir = options.cwd workdir = options.cwd
delete options.cwd delete options.cwd
} }
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`)
}
}
const child = cp.spawn( const child = cp.spawn(
"start-container", "start-container",
[ [
"subcontainer", "subcontainer",
"exec", "exec",
`--env=/media/startos/images/${this.imageId}.env`, `--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`, `--user=${user}`,
`--workdir=${workdir}`, `--workdir=${workdir}`,
...extra, ...extra,
@@ -530,6 +535,11 @@ export class SubContainerOwned<
workdir = options.cwd workdir = options.cwd
delete options.cwd delete options.cwd
} }
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`)
}
}
await this.killLeader() await this.killLeader()
this.leaderExited = false this.leaderExited = false
this.leader = cp.spawn( this.leader = cp.spawn(
@@ -537,7 +547,7 @@ export class SubContainerOwned<
[ [
"subcontainer", "subcontainer",
"launch", "launch",
`--env=/media/startos/images/${this.imageId}.env`, `--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`, `--user=${user}`,
`--workdir=${workdir}`, `--workdir=${workdir}`,
...extra, ...extra,
@@ -574,12 +584,17 @@ export class SubContainerOwned<
workdir = options.cwd workdir = options.cwd
delete options.cwd delete options.cwd
} }
if (options?.env) {
for (let [k, v] of Object.entries(options.env)) {
extra.push(`--env=${k}=${v}`)
}
}
return cp.spawn( return cp.spawn(
"start-container", "start-container",
[ [
"subcontainer", "subcontainer",
"exec", "exec",
`--env=/media/startos/images/${this.imageId}.env`, `--env-file=/media/startos/images/${this.imageId}.env`,
`--user=${user}`, `--user=${user}`,
`--workdir=${workdir}`, `--workdir=${workdir}`,
...extra, ...extra,

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43", "version": "0.4.0-beta.44",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43", "version": "0.4.0-beta.44",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",
@@ -98,6 +98,7 @@
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.0", "@babel/code-frame": "^7.26.0",
@@ -1643,6 +1644,7 @@
"integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }
@@ -1944,6 +1946,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001669", "caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41", "electron-to-chromium": "^1.5.41",
@@ -3053,6 +3056,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@jest/core": "^29.7.0", "@jest/core": "^29.7.0",
"@jest/types": "^29.6.3", "@jest/types": "^29.6.3",
@@ -4157,6 +4161,7 @@
"integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==", "integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"commander": "^10.0.0", "commander": "^10.0.0",
"source-map-generator": "0.8.0" "source-map-generator": "0.8.0"
@@ -4833,6 +4838,7 @@
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@@ -4953,6 +4959,7 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.43", "version": "0.4.0-beta.44",
"description": "Software development kit to facilitate packaging services for StartOS", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",

158
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.13", "version": "0.4.0-alpha.15",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.13", "version": "0.4.0-alpha.15",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular/animations": "^20.3.0", "@angular/animations": "^20.3.0",
@@ -25,18 +25,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.3.0", "@start9labs/argon2": "^0.3.0",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.55.0", "@taiga-ui/addon-charts": "4.62.0",
"@taiga-ui/addon-commerce": "4.55.0", "@taiga-ui/addon-commerce": "4.62.0",
"@taiga-ui/addon-mobile": "4.55.0", "@taiga-ui/addon-mobile": "4.62.0",
"@taiga-ui/addon-table": "4.55.0", "@taiga-ui/addon-table": "4.62.0",
"@taiga-ui/cdk": "4.55.0", "@taiga-ui/cdk": "4.62.0",
"@taiga-ui/core": "4.55.0", "@taiga-ui/core": "4.62.0",
"@taiga-ui/dompurify": "4.1.11", "@taiga-ui/dompurify": "4.1.11",
"@taiga-ui/event-plugins": "4.7.0", "@taiga-ui/event-plugins": "4.7.0",
"@taiga-ui/experimental": "4.55.0", "@taiga-ui/experimental": "4.62.0",
"@taiga-ui/icons": "4.55.0", "@taiga-ui/icons": "4.62.0",
"@taiga-ui/kit": "4.55.0", "@taiga-ui/kit": "4.62.0",
"@taiga-ui/layout": "4.55.0", "@taiga-ui/layout": "4.62.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
@@ -3950,9 +3950,9 @@
"link": true "link": true
}, },
"node_modules/@taiga-ui/addon-charts": { "node_modules/@taiga-ui/addon-charts": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.62.0.tgz",
"integrity": "sha512-Rwgcc7NaLm75GsHYhqbO4jgNhD1bGbA7Kk0sNFE+Tgz9+V3ARXMuBw7C3cU9UxLSFnOsXz9RYLosmZ3jAVlyuQ==", "integrity": "sha512-tCysUpzEHwRhK/p9hopkt0Jw4jcgA2cF8CYK8mDntghC+fNLnqCVUcrqFIC5plGabAo00WMEz+X+KyGvwvKaVg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -3961,15 +3961,15 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0" "@taiga-ui/polymorpheus": "^4.9.0"
} }
}, },
"node_modules/@taiga-ui/addon-commerce": { "node_modules/@taiga-ui/addon-commerce": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.62.0.tgz",
"integrity": "sha512-eOOBkIJSsagtRkpRZ04xlL8ePIP01d4Xo264zSTg2SRxD6vwR/7/QJlf9108BvIJv/jfTpmFukLwSB9LazqmCw==", "integrity": "sha512-J4+bdHeDe2d7Uh8NNObLl4LzBhWLCdzxNHXPac1bMGB+3gX751Htc9px37FkVZlQGnxQATCbxAVXj7Zjveq/QQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3979,22 +3979,22 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@maskito/angular": "^3.10.3", "@maskito/angular": "^3.11.1",
"@maskito/core": "^3.10.3", "@maskito/core": "^3.11.1",
"@maskito/kit": "^3.10.3", "@maskito/kit": "^3.11.1",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/i18n": "^4.55.0", "@taiga-ui/i18n": "^4.62.0",
"@taiga-ui/kit": "^4.55.0", "@taiga-ui/kit": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-mobile": { "node_modules/@taiga-ui/addon-mobile": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.62.0.tgz",
"integrity": "sha512-NQRozVKcXLs9/rd/s1yI7T5rIokzwHQ5IN/c3NLBUEka1iKUr1ZTch+g9CHJf8GTVB0uAwWKNCgX5LxtiSI5zg==", "integrity": "sha512-seIBG4utgLq2xDJu+YDzksOsVi/V6vsTbm2bljgM1fIBZInbhqk95YOIFZDU9JXT1/vIShcqetavg1vHD1wdkQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4004,18 +4004,18 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/kit": "^4.55.0", "@taiga-ui/kit": "^4.62.0",
"@taiga-ui/layout": "^4.55.0", "@taiga-ui/layout": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-table": { "node_modules/@taiga-ui/addon-table": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.62.0.tgz",
"integrity": "sha512-K5qpOS0UQLDqruJXPNDic8scCRvO3oAN/ZCPQ9XOGDrcvydvo4AKUwjKRPj+pZ/z0ulxbwAAruFFFCvRNnbzaA==", "integrity": "sha512-0rolnsO1puYwUK17si5OOpzFxiziS6/OSbpLOSKrVrMkCgsWCoNDvpgPIwtwS5Mq3iF5cwLfUPbDQM8saG7wxQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4024,18 +4024,18 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/intersection-observer": "^4.12.0", "@ng-web-apis/intersection-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/i18n": "^4.55.0", "@taiga-ui/i18n": "^4.62.0",
"@taiga-ui/kit": "^4.55.0", "@taiga-ui/kit": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/cdk": { "node_modules/@taiga-ui/cdk": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.62.0.tgz",
"integrity": "sha512-vA5nGyx+YIHR1xZeq5D9gSqTRQg74qhe1AOt5FlqFOC0P4LvmLkNg3De7AeahXALNSeRz/DYcqI7WuGo6xpcLQ==", "integrity": "sha512-KWPXEbCHtRp7aIet1L3PySdXpo5Aay4L/36jDzjiFZ/bcbuD2cY/3S2l68zpgv6ZksZA94DuCuaamSEwQIAtPw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4065,9 +4065,9 @@
} }
}, },
"node_modules/@taiga-ui/core": { "node_modules/@taiga-ui/core": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.62.0.tgz",
"integrity": "sha512-Z2ATVNmEAlHEk2cgs/tnS6qZML87IchkPDeRl6HQfBT2fjYVjh1oCzXL07t86Lv6tpvkllyUVqoBCTSvDXs9kA==", "integrity": "sha512-PQW10hFH50g8PgnJpPa/ZrGMWljhIsBHad/utvalmlv8wXQY24i8T1BjrGIOFPOjzs20NEwLOICHf7KdZUtiuA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4082,9 +4082,9 @@
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.12.0", "@ng-web-apis/mutation-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/event-plugins": "^4.7.0", "@taiga-ui/event-plugins": "^4.7.0",
"@taiga-ui/i18n": "^4.55.0", "@taiga-ui/i18n": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
@@ -4120,9 +4120,9 @@
} }
}, },
"node_modules/@taiga-ui/experimental": { "node_modules/@taiga-ui/experimental": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.62.0.tgz",
"integrity": "sha512-3zq2BTl+fE/N/tEr74TzWbyzT5OoS9YEApKobJehKivW5XxZmF/MDRWp45kSe4jDtVbJ2ueI0Jn8h0BDNykkcg==", "integrity": "sha512-EiL5wJ+9LSf0BfZcFX6ioCavLfx26v0BCOUXh52Rtczp85Uh2qTDt2feM0oBDB+0Kj74/+wqqiKi+s3B8ZV3WA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4130,19 +4130,19 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/addon-commerce": "^4.55.0", "@taiga-ui/addon-commerce": "^4.62.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/kit": "^4.55.0", "@taiga-ui/kit": "^4.62.0",
"@taiga-ui/layout": "^4.55.0", "@taiga-ui/layout": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/i18n": { "node_modules/@taiga-ui/i18n": {
"version": "4.59.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.59.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.62.0.tgz",
"integrity": "sha512-IxPzqkORJlSqagvUdmqBL9fuvfRm/Ca/W/bCDEK2GN/4QtSZ0yFzAyQdWduoIJubqyEPMXRbXZGc7WBtDgAMIQ==", "integrity": "sha512-84hD1nI26EAYd5RUhFKxbg+8WKYhc0GBHyf8wfi15xuwaT6oh2gbJx7pNTlGN3klH4CeDB9HF998tkhieevqQw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4155,18 +4155,18 @@
} }
}, },
"node_modules/@taiga-ui/icons": { "node_modules/@taiga-ui/icons": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.62.0.tgz",
"integrity": "sha512-sYqSG9wgUcwHBrDRnMhLCMEkvAAw/SZrvvq0jdY2oWGmKwAj/6WBt+wA+jnFkDDKEZ7mjzdPIQffpVaUjSwsiw==", "integrity": "sha512-vD+bJk3Wot/+NcbdPwAJGBnqXG6T1OJVeg2IkaEE6DBixwdwDpukZWiV9asXyXiJkyEpG2Ar7SASvdCYZEVlxw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
} }
}, },
"node_modules/@taiga-ui/kit": { "node_modules/@taiga-ui/kit": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.62.0.tgz",
"integrity": "sha512-xTvi7viI+wI2ifPv2bsf8prhYWWS4g1lbx059jXV3f5Cttc0Xg6DEb6xpaQOf4loBkcrP2FzkA4njACUuiouzw==", "integrity": "sha512-tdEaXJTks1PZQJAwMiVQTZrtCpaLIYV6T9VdVPZUKAJXq7K6J2kcD0oIISjwE9rqgLVwqytMZrwHx1nSRzkb/A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4177,25 +4177,25 @@
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@maskito/angular": "^3.10.3", "@maskito/angular": "^3.11.1",
"@maskito/core": "^3.10.3", "@maskito/core": "^3.11.1",
"@maskito/kit": "^3.10.3", "@maskito/kit": "^3.11.1",
"@maskito/phone": "^3.10.3", "@maskito/phone": "^3.11.1",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/intersection-observer": "^4.12.0", "@ng-web-apis/intersection-observer": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.12.0", "@ng-web-apis/mutation-observer": "^4.12.0",
"@ng-web-apis/resize-observer": "^4.12.0", "@ng-web-apis/resize-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/i18n": "^4.55.0", "@taiga-ui/i18n": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/layout": { "node_modules/@taiga-ui/layout": {
"version": "4.55.0", "version": "4.62.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.55.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.62.0.tgz",
"integrity": "sha512-C+e4gudZwjIc46VITil5vySas1FPpfe+D4uwLRggJOTuUosZlZHBuc51v91wCCc0pL0Xfu+TD0s8W9kRd1sQHA==", "integrity": "sha512-xd8eLLeR5FE3RhnVMGl1QlC3JXXJLsLAAASpBf9DQsTt+YBBl8BQt/cXGbBcJecC2mJLZlS6zytSkMTHY7VAhw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4204,9 +4204,9 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/cdk": "^4.55.0", "@taiga-ui/cdk": "^4.62.0",
"@taiga-ui/core": "^4.55.0", "@taiga-ui/core": "^4.62.0",
"@taiga-ui/kit": "^4.55.0", "@taiga-ui/kit": "^4.62.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.13", "version": "0.4.0-alpha.15",
"author": "Start9 Labs, Inc", "author": "Start9 Labs, Inc",
"homepage": "https://start9.com/", "homepage": "https://start9.com/",
"license": "MIT", "license": "MIT",
@@ -49,18 +49,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.3.0", "@start9labs/argon2": "^0.3.0",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.55.0", "@taiga-ui/addon-charts": "4.62.0",
"@taiga-ui/addon-commerce": "4.55.0", "@taiga-ui/addon-commerce": "4.62.0",
"@taiga-ui/addon-mobile": "4.55.0", "@taiga-ui/addon-mobile": "4.62.0",
"@taiga-ui/addon-table": "4.55.0", "@taiga-ui/addon-table": "4.62.0",
"@taiga-ui/cdk": "4.55.0", "@taiga-ui/cdk": "4.62.0",
"@taiga-ui/core": "4.55.0", "@taiga-ui/core": "4.62.0",
"@taiga-ui/dompurify": "4.1.11", "@taiga-ui/dompurify": "4.1.11",
"@taiga-ui/event-plugins": "4.7.0", "@taiga-ui/event-plugins": "4.7.0",
"@taiga-ui/experimental": "4.55.0", "@taiga-ui/experimental": "4.62.0",
"@taiga-ui/icons": "4.55.0", "@taiga-ui/icons": "4.62.0",
"@taiga-ui/kit": "4.55.0", "@taiga-ui/kit": "4.62.0",
"@taiga-ui/layout": "4.55.0", "@taiga-ui/layout": "4.62.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",

View File

@@ -70,6 +70,7 @@ import { MarketplaceItemComponent } from './item.component'
<div class="background-border box-shadow-lg shadow-color-light"> <div class="background-border box-shadow-lg shadow-color-light">
<div class="box-container"> <div class="box-container">
<h2 class="additional-detail-title">{{ 'Description' | i18n }}</h2>
<p [innerHTML]="pkg().description.long"></p> <p [innerHTML]="pkg().description.long"></p>
</div> </div>
</div> </div>

View File

@@ -10,7 +10,12 @@ import { MarketplacePkgBase } from '../../../types'
selector: 'marketplace-dep-item', selector: 'marketplace-dep-item',
template: ` template: `
<div class="outer-container"> <div class="outer-container">
<tui-avatar class="dep-img" size="l" [src]="getImage(dep.key)" /> <tui-avatar
appearance="action-grayscale"
class="dep-img"
size="l"
[src]="getImage(dep.key)"
/>
<div> <div>
<tui-line-clamp [linesLimit]="2" [content]="titleContent" /> <tui-line-clamp [linesLimit]="2" [content]="titleContent" />
<ng-template #titleContent> <ng-template #titleContent>

View File

@@ -21,7 +21,10 @@ import { MarketplacePkg } from '../../types'
[queryParams]="{ id: pkg.id, flavor: pkg.flavor }" [queryParams]="{ id: pkg.id, flavor: pkg.flavor }"
queryParamsHandling="merge" queryParamsHandling="merge"
> >
<tui-avatar [src]="pkg.icon | trustUrl" /> <tui-avatar
appearance="action-grayscale"
[src]="pkg.icon | trustUrl"
/>
<span tuiTitle> <span tuiTitle>
{{ pkg.title }} {{ pkg.title }}
<span tuiSubtitle>{{ pkg.version }}</span> <span tuiSubtitle>{{ pkg.version }}</span>

View File

@@ -39,7 +39,9 @@ import { DocsLinkDirective } from '@start9labs/shared'
" "
> >
<div> <div>
<h3 style="color: #f8546a; font-weight: bold">Important!</h3> <h2 style="font-variant-caps: all-small-caps">
Root Certificate Authority
</h2>
<p> <p>
Download your server's Root CA and Download your server's Root CA and
<a <a
@@ -47,7 +49,7 @@ import { DocsLinkDirective } from '@start9labs/shared'
path="/user-manual/trust-ca.html" path="/user-manual/trust-ca.html"
style="color: #6866cc; font-weight: bold; text-decoration: none" style="color: #6866cc; font-weight: bold; text-decoration: none"
> >
follow the instructions follow instructions
</a> </a>
to establish a secure connection with your server. to establish a secure connection with your server.
</p> </p>
@@ -84,15 +86,15 @@ import { DocsLinkDirective } from '@start9labs/shared'
" "
> >
<h2 style="font-variant-caps: all-small-caps"> <h2 style="font-variant-caps: all-small-caps">
Access from home (LAN) Permanent Local Address
</h2> </h2>
<p> <p>
Visit the address below when you are connected to the same WiFi or You must be connected to the same Local Area Network (LAN) as your
Local Area Network (LAN) as your server. server to access this address.
</p> </p>
<p <p
style=" style="
padding: 16px; padding: 16px 0;
font-weight: bold; font-weight: bold;
font-size: 1.1rem; font-size: 1.1rem;
overflow: auto; overflow: auto;
@@ -100,33 +102,6 @@ import { DocsLinkDirective } from '@start9labs/shared'
> >
<code id="lan-addr"></code> <code id="lan-addr"></code>
</p> </p>
<h2 style="font-variant-caps: all-small-caps">
Access on the go (Tor)
</h2>
<p>Visit the address below when you are away from home.</p>
<p>
<span style="font-weight: bold">Note:</span>
This address will only work from a Tor-enabled browser.
<a
docsLink
path="/user-manual/connecting-remotely/tor.html"
style="color: #6866cc; font-weight: bold; text-decoration: none"
>
Follow the instructions
</a>
to get setup.
</p>
<p
style="
padding: 16px;
font-weight: bold;
font-size: 1.1rem;
overflow: auto;
"
>
<code id="tor-addr"></code>
</p>
</section> </section>
</div> </div>
</body> </body>

View File

@@ -7,7 +7,7 @@ import {
DOCUMENT, DOCUMENT,
} from '@angular/core' } from '@angular/core'
import { DownloadHTMLService, ErrorService } from '@start9labs/shared' import { DownloadHTMLService, ErrorService } from '@start9labs/shared'
import { TuiButton, TuiIcon, TuiSurface } from '@taiga-ui/core' import { TuiButton, TuiIcon, TuiLoader, TuiSurface } from '@taiga-ui/core'
import { TuiCardLarge } from '@taiga-ui/layout' import { TuiCardLarge } from '@taiga-ui/layout'
import { DocumentationComponent } from 'src/app/components/documentation.component' import { DocumentationComponent } from 'src/app/components/documentation.component'
import { MatrixComponent } from 'src/app/components/matrix.component' import { MatrixComponent } from 'src/app/components/matrix.component'
@@ -31,10 +31,16 @@ import { StateService } from 'src/app/services/state.service'
<h3>You can now safely unplug your old StartOS data drive</h3> <h3>You can now safely unplug your old StartOS data drive</h3>
} }
<h3>
http://start.local was for setup purposes only. It will no longer
work.
</h3>
<button tuiCardLarge tuiSurface="floating" (click)="download()"> <button tuiCardLarge tuiSurface="floating" (click)="download()">
<strong class="caps">Download address info</strong> <strong class="caps">Download address info</strong>
<span> <span>
start.local was for setup purposes only. It will no longer work. For future reference, this file contains your server's permanent
local address, as well as its Root Certificate Authority (Root CA).
</span> </span>
<strong class="caps"> <strong class="caps">
Download Download
@@ -48,17 +54,18 @@ import { StateService } from 'src/app/services/state.service'
target="_blank" target="_blank"
[attr.href]="disableLogin ? null : lanAddress" [attr.href]="disableLogin ? null : lanAddress"
> >
<strong class="caps">Trust your Root CA</strong>
<span> <span>
In the new tab, follow instructions to trust your server's Root CA In the new tab, follow instructions to trust your server's Root CA
and log in. and log in.
</span> </span>
<strong class="caps"> <strong class="caps">
Open Open Local Address
<tui-icon icon="@tui.external-link" /> <tui-icon icon="@tui.external-link" />
</strong> </strong>
</a> </a>
<app-documentation hidden [lanAddress]="lanAddress" /> <app-documentation hidden [lanAddress]="lanAddress" />
} @else {
<tui-loader />
} }
</section> </section>
`, `,
@@ -97,6 +104,10 @@ import { StateService } from 'src/app/services/state.service'
opacity: var(--tui-disabled-opacity); opacity: var(--tui-disabled-opacity);
pointer-events: none; pointer-events: none;
} }
h3 {
text-align: left;
}
`, `,
imports: [ imports: [
TuiCardLarge, TuiCardLarge,
@@ -105,6 +116,7 @@ import { StateService } from 'src/app/services/state.service'
TuiSurface, TuiSurface,
MatrixComponent, MatrixComponent,
DocumentationComponent, DocumentationComponent,
TuiLoader,
], ],
}) })
export default class SuccessPage implements AfterViewInit { export default class SuccessPage implements AfterViewInit {
@@ -117,7 +129,6 @@ export default class SuccessPage implements AfterViewInit {
readonly stateService = inject(StateService) readonly stateService = inject(StateService)
torAddresses?: string[]
lanAddress?: string lanAddress?: string
cert?: string cert?: string
disableLogin = this.stateService.setupType === 'fresh' disableLogin = this.stateService.setupType === 'fresh'
@@ -127,10 +138,8 @@ export default class SuccessPage implements AfterViewInit {
} }
download() { download() {
const torElem = this.document.getElementById('tor-addr')
const lanElem = this.document.getElementById('lan-addr') const lanElem = this.document.getElementById('lan-addr')
if (torElem) torElem.innerHTML = this.torAddresses?.join('\n') || ''
if (lanElem) lanElem.innerHTML = this.lanAddress || '' if (lanElem) lanElem.innerHTML = this.lanAddress || ''
this.document this.document
@@ -155,9 +164,6 @@ export default class SuccessPage implements AfterViewInit {
try { try {
const ret = await this.api.complete() const ret = await this.api.complete()
if (!this.stateService.kiosk) { if (!this.stateService.kiosk) {
this.torAddresses = ret.torAddresses.map(a =>
a.replace(/^https:/, 'http:'),
)
this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:') this.lanAddress = ret.lanAddress.replace(/^https:/, 'http:')
this.cert = ret.rootCa this.cert = ret.rootCa

View File

@@ -484,7 +484,7 @@ export default {
512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar', 512: 'Der Kiosk-Modus ist auf diesem Gerät nicht verfügbar',
513: 'Aktivieren', 513: 'Aktivieren',
514: 'Deaktivieren', 514: 'Deaktivieren',
515: 'Du verwendest derzeit einen Kiosk. Wenn du den Kiosk-Modus deaktivierst, wird die Verbindung zum Kiosk getrennt.', 515: 'Diese Änderung wird nach dem nächsten Neustart wirksam',
516: 'Empfohlen', 516: 'Empfohlen',
517: 'Möchten Sie diese Aufgabe wirklich verwerfen?', 517: 'Möchten Sie diese Aufgabe wirklich verwerfen?',
518: 'Verwerfen', 518: 'Verwerfen',

View File

@@ -483,7 +483,7 @@ export const ENGLISH = {
'Kiosk Mode is unavailable on this device': 512, 'Kiosk Mode is unavailable on this device': 512,
'Enable': 513, 'Enable': 513,
'Disable': 514, 'Disable': 514,
'You are currently using a kiosk. Disabling Kiosk Mode will result in the kiosk disconnecting.': 515, 'This change will take effect after the next boot': 515,
'Recommended': 516, // as in, we recommend this 'Recommended': 516, // as in, we recommend this
'Are you sure you want to dismiss this task?': 517, 'Are you sure you want to dismiss this task?': 517,
'Dismiss': 518, // as in, dismiss or delete a task 'Dismiss': 518, // as in, dismiss or delete a task

View File

@@ -484,7 +484,7 @@ export default {
512: 'El modo quiosco no está disponible en este dispositivo', 512: 'El modo quiosco no está disponible en este dispositivo',
513: 'Activar', 513: 'Activar',
514: 'Desactivar', 514: 'Desactivar',
515: 'Actualmente estás utilizando un quiosco. Desactivar el modo quiosco provocará su desconexión.', 515: 'Este cambio tendrá efecto después del próximo inicio',
516: 'Recomendado', 516: 'Recomendado',
517: '¿Estás seguro de que deseas descartar esta tarea?', 517: '¿Estás seguro de que deseas descartar esta tarea?',
518: 'Descartar', 518: 'Descartar',

View File

@@ -484,7 +484,7 @@ export default {
512: 'Le mode kiosque nest pas disponible sur cet appareil', 512: 'Le mode kiosque nest pas disponible sur cet appareil',
513: 'Activer', 513: 'Activer',
514: 'Désactiver', 514: 'Désactiver',
515: 'Vous utilisez actuellement un kiosque. Désactiver le mode kiosque entraînera sa déconnexion.', 515: 'Ce changement va prendre effet après le prochain démarrage',
516: 'Recommandé', 516: 'Recommandé',
517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?', 517: 'Êtes-vous sûr de vouloir ignorer cette tâche ?',
518: 'Ignorer', 518: 'Ignorer',

View File

@@ -484,7 +484,7 @@ export default {
512: 'Tryb kiosku jest niedostępny na tym urządzeniu', 512: 'Tryb kiosku jest niedostępny na tym urządzeniu',
513: 'Włącz', 513: 'Włącz',
514: 'Wyłącz', 514: 'Wyłącz',
515: 'Obecnie używasz kiosku. Wyłączenie trybu kiosku spowoduje jego rozłączenie.', 515: 'Ta zmiana zacznie obowiązywać po następnym uruchomieniu',
516: 'Zalecane', 516: 'Zalecane',
517: 'Czy na pewno chcesz odrzucić to zadanie?', 517: 'Czy na pewno chcesz odrzucić to zadanie?',
518: 'Odrzuć', 518: 'Odrzuć',

View File

@@ -10,7 +10,7 @@ import { I18N, i18nKey } from './i18n.providers'
export class i18nPipe implements PipeTransform { export class i18nPipe implements PipeTransform {
private readonly i18n = inject(I18N) private readonly i18n = inject(I18N)
transform(englishKey: i18nKey | null | undefined): string { transform(englishKey: i18nKey | null | undefined | ''): string {
englishKey = englishKey || ('' as i18nKey) englishKey = englishKey || ('' as i18nKey)
return this.i18n()?.[ENGLISH[englishKey]] || englishKey return this.i18n()?.[ENGLISH[englishKey]] || englishKey

View File

@@ -40,7 +40,9 @@ import { HintPipe } from '../pipes/hint.pipe'
[(ngModel)]="selected" [(ngModel)]="selected"
/> />
} }
<tui-data-list-wrapper *tuiTextfieldDropdown new [items]="items" /> @if (!mobile) {
<tui-data-list-wrapper *tuiTextfieldDropdown new [items]="items" />
}
@if (spec | hint; as hint) { @if (spec | hint; as hint) {
<tui-icon [tuiTooltip]="hint" /> <tui-icon [tuiTooltip]="hint" />
} }

View File

@@ -17,6 +17,7 @@ import {
tuiButtonOptionsProvider, tuiButtonOptionsProvider,
TuiDataList, TuiDataList,
TuiDropdown, TuiDropdown,
TuiIcon,
TuiTextfield, TuiTextfield,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -27,13 +28,9 @@ import { InterfaceComponent } from '../interface.component'
selector: 'td[actions]', selector: 'td[actions]',
template: ` template: `
<div class="desktop"> <div class="desktop">
<button <button tuiIconButton appearance="flat-grayscale" (click)="viewDetails()">
tuiIconButton
appearance="flat-grayscale"
iconStart="@tui.info"
(click)="viewDetails()"
>
{{ 'Address details' | i18n }} {{ 'Address details' | i18n }}
<tui-icon class="info" icon="@tui.info" background="@tui.info-filled" />
</button> </button>
@if (interface.value()?.type === 'ui') { @if (interface.value()?.type === 'ui') {
<a <a
@@ -113,6 +110,19 @@ import { InterfaceComponent } from '../interface.component'
white-space: nowrap; white-space: nowrap;
} }
:host-context(.uncommon-hidden) .desktop {
height: 0;
visibility: hidden;
}
.info {
background: var(--tui-status-info);
&::after {
mask-size: 1.5rem;
}
}
.mobile { .mobile {
display: none; display: none;
} }
@@ -127,7 +137,14 @@ import { InterfaceComponent } from '../interface.component'
} }
} }
`, `,
imports: [TuiButton, TuiDropdown, TuiDataList, i18nPipe, TuiTextfield], imports: [
TuiButton,
TuiDropdown,
TuiDataList,
i18nPipe,
TuiTextfield,
TuiIcon,
],
providers: [tuiButtonOptionsProvider({ appearance: 'icon' })], providers: [tuiButtonOptionsProvider({ appearance: 'icon' })],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View File

@@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core' import { ChangeDetectionStrategy, Component, input } from '@angular/core'
import { i18nPipe } from '@start9labs/shared' import { i18nPipe } from '@start9labs/shared'
import { TuiButton } from '@taiga-ui/core'
import { TuiAccordion } from '@taiga-ui/experimental' import { TuiAccordion } from '@taiga-ui/experimental'
import { TuiSkeleton } from '@taiga-ui/kit' import { TuiElasticContainer, TuiSkeleton } from '@taiga-ui/kit'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
@@ -12,91 +13,79 @@ import { InterfaceAddressItemComponent } from './item.component'
selector: 'section[addresses]', selector: 'section[addresses]',
template: ` template: `
<header>{{ 'Addresses' | i18n }}</header> <header>{{ 'Addresses' | i18n }}</header>
<table [appTable]="['Type', 'Access', 'Gateway', 'URL', null]"> <tui-elastic-container>
@for (address of addresses()?.common; track $index) { <table [appTable]="['Type', 'Access', 'Gateway', 'URL', null]">
<tr [address]="address" [isRunning]="isRunning()"></tr> @for (address of addresses()?.common; track $index) {
} @empty { <tr [address]="address" [isRunning]="isRunning()"></tr>
@if (addresses()) { } @empty {
<tr> @if (addresses()) {
<td colspan="5">
<app-placeholder icon="@tui.list-x">
{{ 'No addresses' | i18n }}
</app-placeholder>
</td>
</tr>
} @else {
@for (_ of [0, 1]; track $index) {
<tr> <tr>
<td colspan="6"> <td colspan="5">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div> <app-placeholder icon="@tui.list-x">
{{ 'No addresses' | i18n }}
</app-placeholder>
</td> </td>
</tr> </tr>
} @else {
@for (_ of [0, 1]; track $index) {
<tr>
<td colspan="6">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
</td>
</tr>
}
} }
} }
} <tbody [class.uncommon-hidden]="!uncommon">
</table> @if (addresses()?.uncommon?.length && uncommon) {
@if (addresses()?.uncommon?.length) { <tr [style.background]="'var(--tui-background-neutral-1)'">
<tui-accordion> <td colspan="5"></td>
<tui-expand> </tr>
<hr />
<table class="g-table">
@for (address of addresses()?.uncommon; track $index) {
<tr [address]="address" [isRunning]="isRunning()"></tr>
}
</table>
</tui-expand>
<button
appearance="secondary-grayscale"
iconEnd=""
[(tuiAccordion)]="uncommon"
>
@if (uncommon) {
Hide uncommon
} @else {
Show uncommon
} }
</button> @for (address of addresses()?.uncommon; track $index) {
</tui-accordion> <tr [address]="address" [isRunning]="isRunning()"></tr>
} }
</tbody>
@if (addresses()?.uncommon?.length) {
<caption [style.caption-side]="'bottom'">
<button
tuiButton
size="m"
appearance="secondary-grayscale"
(click)="uncommon = !uncommon"
>
@if (uncommon) {
Hide uncommon
} @else {
Show uncommon
}
</button>
</caption>
}
</table>
</tui-elastic-container>
`, `,
styles: ` styles: `
tui-accordion { .g-table:has(caption) {
border-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
} }
[tuiAccordion], [tuiButton] {
tui-expand { width: 100%;
box-shadow: none; border-top-left-radius: 0;
padding: 0; border-top-right-radius: 0;
}
[tuiAccordion] {
justify-content: center;
height: 3rem;
border-radius: 0 0 var(--tui-radius-m) var(--tui-radius-m) !important;
}
hr {
margin: 0;
height: 0.25rem;
border-radius: 1rem;
}
:host-context(tui-root._mobile) {
[tuiAccordion] {
margin: 0.5rem 0;
border-radius: var(--tui-radius-m) !important;
}
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
imports: [ imports: [
TuiSkeleton,
TuiButton,
TableComponent, TableComponent,
PlaceholderComponent, PlaceholderComponent,
i18nPipe, i18nPipe,
InterfaceAddressItemComponent, InterfaceAddressItemComponent,
TuiAccordion, TuiElasticContainer,
TuiSkeleton,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View File

@@ -8,27 +8,31 @@ import { TuiBadge } from '@taiga-ui/kit'
selector: 'tr[address]', selector: 'tr[address]',
template: ` template: `
@if (address(); as address) { @if (address(); as address) {
<td>{{ address.type }}</td>
<td> <td>
@if (address.access === 'public') { <div class="wrapper">{{ address.type }}</div>
<tui-badge size="s" appearance="primary-success"> </td>
{{ 'public' | i18n }} <td>
</tui-badge> <div class="wrapper">
} @else if (address.access === 'private') { @if (address.access === 'public') {
<tui-badge size="s" appearance="primary-destructive"> <tui-badge size="s" appearance="primary-success">
{{ 'private' | i18n }} {{ 'public' | i18n }}
</tui-badge> </tui-badge>
} @else { } @else if (address.access === 'private') {
- <tui-badge size="s" appearance="primary-destructive">
} {{ 'private' | i18n }}
</tui-badge>
} @else {
-
}
</div>
</td> </td>
<td [style.order]="-1"> <td [style.order]="-1">
<div [title]="address.gatewayName"> <div class="wrapper" [title]="address.gatewayName">
{{ address.gatewayName || '-' }} {{ address.gatewayName || '-' }}
</div> </div>
</td> </td>
<td> <td>
<div [title]="address.url">{{ address.url }}</div> <div class="wrapper" [title]="address.url">{{ address.url }}</div>
</td> </td>
<td <td
actions actions
@@ -48,6 +52,18 @@ import { TuiBadge } from '@taiga-ui/kit'
} }
} }
:host-context(.uncommon-hidden) {
.wrapper {
height: 0;
visibility: hidden;
}
td {
padding-block: 0;
border: hidden;
}
}
div { div {
white-space: normal; white-space: normal;
word-break: break-all; word-break: break-all;

View File

@@ -13,6 +13,8 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
</tr> </tr>
</thead> </thead>
<tbody><ng-content /></tbody> <tbody><ng-content /></tbody>
<ng-content select="tbody" />
<ng-content select="caption" />
`, `,
styles: ` styles: `
:host:has(app-placeholder) thead { :host:has(app-placeholder) thead {

View File

@@ -25,7 +25,7 @@ import { ToManifestPipe } from '../../../pipes/to-manifest'
[queryParams]="services[d.key] ? {} : { search: d.key }" [queryParams]="services[d.key] ? {} : { search: d.key }"
[class.error]="getError(d.key)" [class.error]="getError(d.key)"
> >
<tui-avatar> <tui-avatar appearance="action-grayscale">
<img <img
alt="" alt=""
[src]=" [src]="

View File

@@ -1,17 +1,17 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
inject, computed,
Input, input,
} from '@angular/core' } from '@angular/core'
import { i18nKey, i18nPipe } from '@start9labs/shared' import { i18nPipe } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { TuiLoader } from '@taiga-ui/core' import { TuiLoader } from '@taiga-ui/core'
import { ServiceUptimeComponent } from 'src/app/routes/portal/routes/services/components/uptime.component'
import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe' import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
import { InstallingInfo } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { import {
getInstalledPrimaryStatus,
PrimaryRendering, PrimaryRendering,
PrimaryStatus,
} from 'src/app/services/pkg-status-rendering.service' } from 'src/app/services/pkg-status-rendering.service'
@Component({ @Component({
@@ -19,23 +19,27 @@ import {
template: ` template: `
<header>{{ 'Status' | i18n }}</header> <header>{{ 'Status' | i18n }}</header>
<div> <div>
@if (installingInfo) { @if (info()) {
<h3> <h3>
<tui-loader size="s" [inheritColor]="true" /> <tui-loader size="s" [inheritColor]="true" />
{{ 'Installing' | i18n }} {{ 'Installing' | i18n }}
<span class="loading-dots"></span> <span class="loading-dots"></span>
{{ getText(installingInfo.progress.overall) | i18n }} {{ info() | i18n }}
</h3> </h3>
} @else { } @else {
<h3 [class]="class"> <h3 [class]="class()">
{{ text | i18n }} {{ text() || 'Unknown' | i18n }}
@if (text === 'Task Required') { @if (text() === 'Task Required') {
<small>{{ 'See below' | i18n }}</small> <small>{{ 'See below' | i18n }}</small>
} }
@if (rendering?.showDots) { @if (rendering().showDots) {
<span class="loading-dots"></span> <span class="loading-dots"></span>
} }
@if ($any(pkg().status)?.started; as started) {
<service-uptime [started]="started" />
}
</h3> </h3>
} }
<ng-content /> <ng-content />
@@ -76,6 +80,12 @@ import {
margin: 0 0.25rem -0.125rem 0; margin: 0 0.25rem -0.125rem 0;
} }
service-uptime {
display: none;
width: fit-content;
margin: 0.5rem 0.125rem;
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
:host { :host {
min-height: 0; min-height: 0;
@@ -94,32 +104,33 @@ import {
small { small {
text-align: left; text-align: left;
} }
service-uptime {
display: flex;
}
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiLoader, i18nPipe], imports: [TuiLoader, i18nPipe, ServiceUptimeComponent],
}) })
export class ServiceStatusComponent { export class ServiceStatusComponent {
@Input({ required: true }) readonly pkg = input.required<PackageDataEntry>()
status?: PrimaryStatus readonly connected = input(false)
@Input() protected readonly status = computed((pkg = this.pkg()) =>
installingInfo?: InstallingInfo pkg?.stateInfo.state === 'installed'
? getInstalledPrimaryStatus(pkg)
: pkg?.stateInfo.state,
)
@Input() protected readonly rendering = computed(() => PrimaryRendering[this.status()])
connected = false protected readonly text = computed(
() => this.connected() && this.rendering().display,
)
private readonly i18n = inject(i18nPipe) protected readonly class = computed(() => {
switch (this.connected() && this.rendering().color) {
get text(): i18nKey {
return this.connected ? this.rendering?.display || 'Unknown' : 'Unknown'
}
get class(): string | null {
if (!this.connected) return null
switch (this.rendering?.color) {
case 'danger': case 'danger':
return 'g-negative' return 'g-negative'
case 'warning': case 'warning':
@@ -131,13 +142,10 @@ export class ServiceStatusComponent {
default: default:
return null return null
} }
} })
get rendering() { protected readonly info = computed(
return this.status && PrimaryRendering[this.status] (progress = this.pkg().stateInfo.installingInfo?.progress.overall) =>
} progress ? getProgressText(progress) : '',
)
getText(progress: T.Progress): i18nKey {
return getProgressText(progress)
}
} }

View File

@@ -25,7 +25,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
selector: 'tr[task]', selector: 'tr[task]',
template: ` template: `
<td tuiFade class="row"> <td tuiFade class="row">
<tui-avatar size="xs"> <tui-avatar appearance="action-grayscale" size="xs">
<img [src]="pkg()?.icon || fallback()?.icon" alt="" /> <img [src]="pkg()?.icon || fallback()?.icon" alt="" />
</tui-avatar> </tui-avatar>
<span>{{ title() || fallback()?.title }}</span> <span>{{ title() || fallback()?.title }}</span>
@@ -79,6 +79,10 @@ import { getManifest } from 'src/app/utils/get-package-data'
overflow: hidden; overflow: hidden;
} }
td:not(:last-child) {
padding-inline-end: 1.5rem;
}
td:last-child { td:last-child {
white-space: nowrap; white-space: nowrap;
text-align: right; text-align: right;

View File

@@ -66,7 +66,8 @@ import { distinctUntilChanged } from 'rxjs/operators'
color: var(--tui-text-primary); color: var(--tui-text-primary);
} }
:host-context(table) { :host-context(table),
:host-context(service-status) {
padding: 0; padding: 0;
header { header {

View File

@@ -73,9 +73,7 @@ export default class ServiceActionsRoute {
.pipe( .pipe(
filter(pkg => pkg.stateInfo.state === 'installed'), filter(pkg => pkg.stateInfo.state === 'installed'),
map(pkg => { map(pkg => {
const specialGroup = Object.values(pkg.actions).some( const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
pkg => !!pkg.group,
)
? 'Other' ? 'Other'
: 'General' : 'General'
return { return {
@@ -90,9 +88,15 @@ export default class ServiceActionsRoute {
group: action.group || specialGroup, group: action.group || specialGroup,
})) }))
.sort((a, b) => { .sort((a, b) => {
if (a.group === specialGroup) return 1 if (a.group === specialGroup && b.group !== specialGroup)
if (b.group === specialGroup) return -1 return 1
return a.group.localeCompare(b.group) // Optional: sort others alphabetically if (b.group === specialGroup && a.group !== specialGroup)
return -1
const groupCompare = a.group.localeCompare(b.group) // sort groups lexicographically
if (groupCompare !== 0) return groupCompare
return a.id.localeCompare(b.id) // sort actions within groups lexicographically
}) })
.reduce< .reduce<
Record< Record<

View File

@@ -47,7 +47,9 @@ const INACTIVE: PrimaryStatus[] = [
</div> </div>
<aside class="g-aside"> <aside class="g-aside">
<header tuiCell routerLink="./"> <header tuiCell routerLink="./">
<tui-avatar><img alt="" [src]="service()?.icon" /></tui-avatar> <tui-avatar appearance="action-grayscale">
<img alt="" [src]="service()?.icon" />
</tui-avatar>
<span tuiTitle> <span tuiTitle>
<strong tuiFade>{{ manifest()?.title }}</strong> <strong tuiFade>{{ manifest()?.title }}</strong>
<span tuiSubtitle>{{ manifest()?.version }}</span> <span tuiSubtitle>{{ manifest()?.version }}</span>

View File

@@ -39,11 +39,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
} @else if (installing()) { } @else if (installing()) {
<service-install-progress [pkg]="pkg" /> <service-install-progress [pkg]="pkg" />
} @else if (installed()) { } @else if (installed()) {
<service-status <service-status [connected]="!!connected()" [pkg]="pkg">
[connected]="!!connected()"
[installingInfo]="pkg.stateInfo.installingInfo"
[status]="status()"
>
@if (connected()) { @if (connected()) {
<service-controls [pkg]="pkg" [status]="status()" /> <service-controls [pkg]="pkg" [status]="status()" />
} }
@@ -51,10 +47,8 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
@if (status() !== 'backingUp') { @if (status() !== 'backingUp') {
<service-health-checks [checks]="health()" /> <service-health-checks [checks]="health()" />
<service-uptime <service-uptime class="g-card" [started]="$any(pkg.status).started" />
class="g-card" <service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
[started]="$any(pkg.status)?.started"
/>
@if (errors() | async; as errors) { @if (errors() | async; as errors) {
<service-dependencies <service-dependencies
@@ -63,7 +57,6 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
[errors]="errors" [errors]="errors"
/> />
} }
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
<service-tasks <service-tasks
#tasks="elementRef" #tasks="elementRef"
@@ -91,7 +84,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
</button> </button>
} }
} @else if (removing()) { } @else if (removing()) {
<service-status [connected]="!!connected()" [status]="status()" /> <service-status [connected]="!!connected()" [pkg]="pkg" />
} }
} }
`, `,
@@ -139,6 +132,10 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
> * { > * {
grid-column: span 1; grid-column: span 1;
} }
service-uptime {
display: none;
}
} }
`, `,
host: { class: 'g-subpage' }, host: { class: 'g-subpage' },

View File

@@ -1,6 +1,4 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiSkeleton } from '@taiga-ui/kit'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { AuthorityItemComponent } from './item.component' import { AuthorityItemComponent } from './item.component'
import { AuthorityService } from './authority.service' import { AuthorityService } from './authority.service'
@@ -12,15 +10,11 @@ import { AuthorityService } from './authority.service'
<tr [authority]="{ name: 'Local Root CA' }"></tr> <tr [authority]="{ name: 'Local Root CA' }"></tr>
@for (authority of authorityService.authorities(); track $index) { @for (authority of authorityService.authorities(); track $index) {
<tr [authority]="authority"></tr> <tr [authority]="authority"></tr>
} @empty {
<td [attr.colspan]="4">
<div [tuiSkeleton]="true">{{ 'Loading' | i18n }}</div>
</td>
} }
</table> </table>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiSkeleton, i18nPipe, TableComponent, AuthorityItemComponent], imports: [TableComponent, AuthorityItemComponent],
}) })
export class AuthoritiesTableComponent { export class AuthoritiesTableComponent {
protected readonly authorityService = inject(AuthorityService) protected readonly authorityService = inject(AuthorityService)

View File

@@ -18,7 +18,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
@for (pkg of pkgs() | keyvalue; track $index) { @for (pkg of pkgs() | keyvalue; track $index) {
@if (backupProgress()?.[pkg.key]; as progress) { @if (backupProgress()?.[pkg.key]; as progress) {
<div tuiCell> <div tuiCell>
<tui-avatar> <tui-avatar appearance="action-grayscale">
<img alt="" [src]="pkg.value.icon" /> <img alt="" [src]="pkg.value.icon" />
</tui-avatar> </tui-avatar>
<span tuiTitle> <span tuiTitle>

View File

@@ -4,7 +4,6 @@ import {
Component, Component,
inject, inject,
INJECTOR, INJECTOR,
DOCUMENT,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
@@ -151,7 +150,7 @@ import { SystemWipeComponent } from './wipe.component'
</span> </span>
</span> </span>
@if (server.kiosk !== null) { @if (server.kiosk !== null) {
<button tuiButton appearance="primary" (click)="tryToggleKiosk()"> <button tuiButton appearance="primary" (click)="toggleKiosk()">
{{ server.kiosk ? ('Disable' | i18n) : ('Enable' | i18n) }} {{ server.kiosk ? ('Disable' | i18n) : ('Enable' | i18n) }}
</button> </button>
} }
@@ -242,7 +241,6 @@ export default class SystemGeneralComponent {
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly isTor = inject(ConfigService).isTor() private readonly isTor = inject(ConfigService).isTor()
private readonly document = inject(DOCUMENT)
private readonly dialog = inject(DialogService) private readonly dialog = inject(DialogService)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
private readonly injector = inject(INJECTOR) private readonly injector = inject(INJECTOR)
@@ -326,28 +324,6 @@ export default class SystemGeneralComponent {
.subscribe(() => this.resetTor(this.wipe)) .subscribe(() => this.resetTor(this.wipe))
} }
async tryToggleKiosk() {
if (
this.server()?.kiosk &&
['localhost', '127.0.0.1'].includes(this.document.location.hostname)
) {
return this.dialog
.openConfirm({
label: 'Warning',
data: {
content:
'You are currently using a kiosk. Disabling Kiosk Mode will result in the kiosk disconnecting.',
yes: 'Disable',
no: 'Cancel',
},
})
.pipe(filter(Boolean))
.subscribe(async () => this.toggleKiosk())
}
this.toggleKiosk()
}
async onRepair() { async onRepair() {
this.dialog this.dialog
.openConfirm({ .openConfirm({
@@ -370,7 +346,7 @@ export default class SystemGeneralComponent {
}) })
} }
private async toggleKiosk() { async toggleKiosk() {
const kiosk = this.server()?.kiosk const kiosk = this.server()?.kiosk
const loader = this.loader const loader = this.loader
@@ -379,6 +355,11 @@ export default class SystemGeneralComponent {
try { try {
await this.api.toggleKiosk(!kiosk) await this.api.toggleKiosk(!kiosk)
this.dialog
.openAlert('This change will take effect after the next boot', {
label: 'Restart to apply',
})
.subscribe()
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {

View File

@@ -1,16 +1,20 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import {
ChangeDetectionStrategy,
Component,
inject,
viewChild,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { verify } from '@start9labs/argon2' import { verify } from '@start9labs/argon2'
import { import {
DialogService,
ErrorService, ErrorService,
i18nKey, i18nKey,
i18nPipe, i18nPipe,
LoadingService, LoadingService,
} from '@start9labs/shared' } from '@start9labs/shared'
import { ISB } from '@start9labs/start-sdk' import { ISB } from '@start9labs/start-sdk'
import { TuiButton, TuiTitle } from '@taiga-ui/core' import { TuiAlertService, TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout' import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { from } from 'rxjs' import { from } from 'rxjs'
@@ -70,13 +74,14 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
], ],
}) })
export default class SystemPasswordComponent { export default class SystemPasswordComponent {
private readonly dialog = inject(DialogService) private readonly alerts = inject(TuiAlertService)
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService) private readonly errorService = inject(ErrorService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB) private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
readonly form = viewChild(FormComponent)
readonly spec = toSignal(from(configBuilderToSpec(this.passwordSpec()))) readonly spec = toSignal(from(configBuilderToSpec(this.passwordSpec())))
readonly buttons = [ readonly buttons = [
{ {
@@ -119,7 +124,12 @@ export default class SystemPasswordComponent {
try { try {
await this.api.resetPassword({ oldPassword, newPassword }) await this.api.resetPassword({ oldPassword, newPassword })
this.dialog.openAlert('Password changed').subscribe() this.form()?.form.reset()
this.alerts
.open(this.i18n.transform('Password changed'), {
appearance: 'positive',
})
.subscribe()
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {

View File

@@ -45,7 +45,9 @@ import UpdatesComponent from './updates.component'
<tr (click)="expanded.set(!expanded())"> <tr (click)="expanded.set(!expanded())">
<td> <td>
<div [style.gap.rem]="0.75" [style.padding-inline-end.rem]="1"> <div [style.gap.rem]="0.75" [style.padding-inline-end.rem]="1">
<tui-avatar size="s"><img alt="" [src]="item().icon" /></tui-avatar> <tui-avatar appearance="action-grayscale" size="s">
<img alt="" [src]="item().icon" />
</tui-avatar>
<span tuiTitle [style.margin]="'-0.125rem 0 0'"> <span tuiTitle [style.margin]="'-0.125rem 0 0'">
<b tuiFade>{{ item().title }}</b> <b tuiFade>{{ item().title }}</b>
<span tuiSubtitle tuiFade class="mobile"> <span tuiSubtitle tuiFade class="mobile">

View File

@@ -69,7 +69,7 @@ interface UpdatesData {
[class.g-secondary]="current()?.url !== registry.url" [class.g-secondary]="current()?.url !== registry.url"
(click)="current.set(registry)" (click)="current.set(registry)"
> >
<tui-avatar> <tui-avatar appearance="action-grayscale">
<store-icon [url]="registry.url" /> <store-icon [url]="registry.url" />
</tui-avatar> </tui-avatar>
<span tuiTitle> <span tuiTitle>

View File

@@ -110,7 +110,7 @@ export namespace Mock {
squashfs: { squashfs: {
aarch64: { aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z', publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_aarch64.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64.squashfs',
commitment: { commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=', hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288, size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
}, },
'aarch64-nonfree': { 'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z', publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_aarch64-nonfree.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_aarch64-nonfree.squashfs',
commitment: { commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=', hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968, size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
}, },
raspberrypi: { raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z', publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_raspberrypi.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_raspberrypi.squashfs',
commitment: { commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=', hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008, size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
}, },
x86_64: { x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z', publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_x86_64.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64.squashfs',
commitment: { commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=', hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728, size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
}, },
'x86_64-nonfree': { 'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z', publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.13/startos-0.4.0-alpha.13-33ae46f~dev_x86_64-nonfree.squashfs', url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.14/startos-0.4.0-alpha.14-33ae46f~dev_x86_64-nonfree.squashfs',
commitment: { commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=', hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136, size: 1731035136,
@@ -385,7 +385,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org', docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -420,7 +420,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org', docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -465,7 +465,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org', docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -500,7 +500,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org', docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -547,7 +547,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/', docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.5', releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: LND_ICON, icon: LND_ICON,
sourceVersion: null, sourceVersion: null,
@@ -595,7 +595,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/', docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.4', releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: LND_ICON, icon: LND_ICON,
sourceVersion: null, sourceVersion: null,
@@ -647,7 +647,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org', docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -682,7 +682,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org', docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!', releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: BTC_ICON, icon: BTC_ICON,
sourceVersion: null, sourceVersion: null,
@@ -727,7 +727,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/', docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release and minor fixes.', releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: LND_ICON, icon: LND_ICON,
sourceVersion: null, sourceVersion: null,
@@ -775,7 +775,7 @@ export namespace Mock {
marketingSite: '', marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.', releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6', osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.43', sdkVersion: '0.4.0-beta.44',
gitHash: 'fakehash', gitHash: 'fakehash',
icon: PROXY_ICON, icon: PROXY_ICON,
sourceVersion: null, sourceVersion: null,