Compare commits

..

7 Commits

Author SHA1 Message Date
Matt Hill
5ba68a3124 translations 2026-02-16 00:34:41 -07:00
Matt Hill
bb68c3b91c update binding for API types, add ARCHITECTURE 2026-02-16 00:05:24 -07:00
Aiden McClelland
3518eccc87 feat: add port_forwards field to Host for tracking gateway forwarding rules 2026-02-14 16:40:21 -07:00
Matt Hill
2f19188dae looking good 2026-02-14 16:37:04 -07:00
Aiden McClelland
3a63f3b840 feat: add mdns hostname metadata variant and fix vhost routing
- Add HostnameMetadata::Mdns variant to distinguish mDNS from private domains
- Mark mDNS addresses as private (public: false) since mDNS is local-only
- Fall back to null SNI entry when hostname not found in vhost mapping
- Simplify public detection in ProxyTarget filter
- Pass hostname to update_addresses for mDNS domain name generation
2026-02-14 15:34:48 -07:00
Matt Hill
098d9275f4 new service interfacee page 2026-02-14 12:24:16 -07:00
Matt Hill
d5c74bc22e re-arrange (#3123) 2026-02-14 08:15:50 -07:00
221 changed files with 6934 additions and 9997 deletions

View File

@@ -1,6 +1 @@
{ {}
"attribution": {
"commit": "",
"pr": ""
}
}

101
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,101 @@
# Architecture
StartOS is an open-source Linux distribution for running personal servers. It manages discovery, installation, network configuration, backups, and health monitoring of self-hosted services.
## Tech Stack
- Backend: Rust (async/Tokio, Axum web framework)
- Frontend: Angular 20 + TypeScript + TaigaUI
- Container runtime: Node.js/TypeScript with LXC
- Database/State: Patch-DB (git submodule) - storage layer with reactive frontend sync
- API: JSON-RPC via rpc-toolkit (see `core/rpc-toolkit.md`)
- Auth: Password + session cookie, public/private key signatures, local authcookie (see `core/src/middleware/auth/`)
## Project Structure
```bash
/
├── assets/ # Screenshots for README
├── build/ # Auxiliary files and scripts for deployed images
├── container-runtime/ # Node.js program managing package containers
├── core/ # Rust backend: API, daemon (startd), CLI (start-cli)
├── debian/ # Debian package maintainer scripts
├── image-recipe/ # Scripts for building StartOS images
├── patch-db/ # (submodule) Diff-based data store for frontend sync
├── sdk/ # TypeScript SDK for building StartOS packages
└── web/ # Web UIs (Angular)
```
## Components
- **`core/`** — Rust backend daemon. Produces a single binary `startbox` that is symlinked as `startd` (main daemon), `start-cli` (CLI), `start-container` (runs inside LXC containers), `registrybox` (package registry), and `tunnelbox` (VPN/tunnel). Handles all backend logic: RPC API, service lifecycle, networking (DNS, ACME, WiFi, Tor, WireGuard), backups, and database state management. See [core/ARCHITECTURE.md](core/ARCHITECTURE.md).
- **`web/`** — Angular 20 + TypeScript workspace using Taiga UI. Contains three applications (admin UI, setup wizard, VPN management) and two shared libraries (common components/services, marketplace). Communicates with the backend exclusively via JSON-RPC. See [web/ARCHITECTURE.md](web/ARCHITECTURE.md).
- **`container-runtime/`** — Node.js runtime that runs inside each service's LXC container. Loads the service's JavaScript from its S9PK package and manages subcontainers. Communicates with the host daemon via JSON-RPC over Unix socket. See [container-runtime/CLAUDE.md](container-runtime/CLAUDE.md).
- **`sdk/`** — TypeScript SDK for packaging services for StartOS (`@start9labs/start-sdk`). Split into `base/` (core types, ABI definitions, effects interface, consumed by web as `@start9labs/start-sdk-base`) and `package/` (full SDK for service developers, consumed by container-runtime as `@start9labs/start-sdk`).
- **`patch-db/`** — Git submodule providing diff-based state synchronization. Uses CBOR encoding. Backend mutations produce diffs that are pushed to the frontend via WebSocket, enabling reactive UI updates without polling. See [patch-db repo](https://github.com/Start9Labs/patch-db).
## Build Pipeline
Components have a strict dependency chain. Changes flow in one direction:
```
Rust (core/)
→ cargo test exports ts-rs types to core/bindings/
→ rsync copies to sdk/base/lib/osBindings/
→ SDK build produces baseDist/ and dist/
→ web/ consumes baseDist/ (via @start9labs/start-sdk-base)
→ container-runtime/ consumes dist/ (via @start9labs/start-sdk)
```
Key make targets along this chain:
| Step | Command | What it does |
|---|---|---|
| 1 | `cargo check -p start-os` | Verify Rust compiles |
| 2 | `make ts-bindings` | Export ts-rs types → rsync to SDK |
| 3 | `cd sdk && make baseDist dist` | Build SDK packages |
| 4 | `cd web && npm run check` | Type-check Angular projects |
| 5 | `cd container-runtime && npm run check` | Type-check runtime |
**Important**: Editing `sdk/base/lib/osBindings/*.ts` alone is NOT sufficient — you must rebuild the SDK bundle (step 3) before web/container-runtime can see the changes.
## Cross-Layer Verification
When making changes across multiple layers (Rust, SDK, web, container-runtime), verify in this order:
1. **Rust**: `cargo check -p start-os` — verifies core compiles
2. **TS bindings**: `make ts-bindings` — regenerates TypeScript types from Rust `#[ts(export)]` structs
- Runs `./core/build/build-ts.sh` to export ts-rs types to `core/bindings/`
- Syncs `core/bindings/``sdk/base/lib/osBindings/` via rsync
- If you manually edit files in `sdk/base/lib/osBindings/`, you must still rebuild the SDK (step 3)
3. **SDK bundle**: `cd sdk && make baseDist dist` — compiles SDK source into packages
- `baseDist/` is consumed by `/web` (via `@start9labs/start-sdk-base`)
- `dist/` is consumed by `/container-runtime` (via `@start9labs/start-sdk`)
- Web and container-runtime reference the **built** SDK, not source files
4. **Web type check**: `cd web && npm run check` — type-checks all Angular projects
5. **Container runtime type check**: `cd container-runtime && npm run check` — type-checks the runtime
## Data Flow: Backend to Frontend
StartOS uses Patch-DB for reactive state synchronization:
1. The backend mutates state via `db.mutate()`, producing CBOR diffs
2. Diffs are pushed to the frontend over a persistent WebSocket connection
3. The frontend applies diffs to its local state copy and notifies observers
4. Components watch specific database paths via `PatchDB.watch$()`, receiving updates reactively
This means the UI is always eventually consistent with the backend — after any mutating API call, the frontend waits for the corresponding PatchDB diff before resolving, so the UI reflects the result immediately.
## Further Reading
- [core/ARCHITECTURE.md](core/ARCHITECTURE.md) — Rust backend architecture
- [web/ARCHITECTURE.md](web/ARCHITECTURE.md) — Angular frontend architecture
- [container-runtime/CLAUDE.md](container-runtime/CLAUDE.md) — Container runtime details
- [core/rpc-toolkit.md](core/rpc-toolkit.md) — JSON-RPC handler patterns
- [core/s9pk-structure.md](core/s9pk-structure.md) — S9PK package format
- [docs/exver.md](docs/exver.md) — Extended versioning format
- [docs/VERSION_BUMP.md](docs/VERSION_BUMP.md) — Version bumping guide

View File

@@ -2,17 +2,11 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview ## Architecture
StartOS is an open-source Linux distribution for running personal servers. It manages discovery, installation, network configuration, backups, and health monitoring of self-hosted services. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full system architecture, component map, build pipeline, and cross-layer verification order.
**Tech Stack:** Each major component has its own `CLAUDE.md` with detailed guidance: `core/`, `web/`, `container-runtime/`, `sdk/`.
- Backend: Rust (async/Tokio, Axum web framework)
- Frontend: Angular 20 + TypeScript + TaigaUI
- Container runtime: Node.js/TypeScript with LXC
- Database/State: Patch-DB (git submodule) - storage layer with reactive frontend sync
- API: JSON-RPC via rpc-toolkit (see `core/rpc-toolkit.md`)
- Auth: Password + session cookie, public/private key signatures, local authcookie (see `core/src/middleware/auth/`)
## Build & Development ## Build & Development
@@ -29,33 +23,11 @@ make update-startbox REMOTE=start9@<ip> # Fastest iteration (binary + UI)
make test-core # Run Rust tests make test-core # Run Rust tests
``` ```
### Verifying code changes ## Operating Rules
When making changes across multiple layers (Rust, SDK, web, container-runtime), verify in this order: - Always verify cross-layer changes using the order described in [ARCHITECTURE.md](ARCHITECTURE.md#cross-layer-verification)
- Check component-level CLAUDE.md files for component-specific conventions
1. **Rust**: `cargo check -p start-os` — verifies core compiles - Follow existing patterns before inventing new ones
2. **TS bindings**: `make ts-bindings` — regenerates TypeScript types from Rust `#[ts(export)]` structs
- Runs `./core/build/build-ts.sh` to export ts-rs types to `core/bindings/`
- Syncs `core/bindings/``sdk/base/lib/osBindings/` via rsync
- If you manually edit files in `sdk/base/lib/osBindings/`, you must still rebuild the SDK (step 3)
3. **SDK bundle**: `cd sdk && make baseDist dist` — compiles SDK source into packages
- `baseDist/` is consumed by `/web` (via `@start9labs/start-sdk-base`)
- `dist/` is consumed by `/container-runtime` (via `@start9labs/start-sdk`)
- Web and container-runtime reference the **built** SDK, not source files
4. **Web type check**: `cd web && npm run check` — type-checks all Angular projects
5. **Container runtime type check**: `cd container-runtime && npm run check` — type-checks the runtime
**Important**: Editing `sdk/base/lib/osBindings/*.ts` alone is NOT sufficient — you must rebuild the SDK bundle (step 3) before web/container-runtime can see the changes.
## Architecture
Each major component has its own `CLAUDE.md` with detailed guidance.
- **`core/`** — Rust backend daemon (startbox, start-cli, start-container, registrybox, tunnelbox)
- **`web/`** — Angular frontend workspace (admin UI, setup wizard, marketplace, shared library)
- **`container-runtime/`** — Node.js runtime managing service containers via JSON-RPC
- **`sdk/`** — TypeScript SDK for packaging services (`@start9labs/start-sdk`)
- **`patch-db/`** — Git submodule providing diff-based state synchronization
## Supplementary Documentation ## Supplementary Documentation

View File

@@ -6,27 +6,7 @@ This guide is for contributing to the StartOS. If you are interested in packagin
- [Matrix](https://matrix.to/#/#dev-startos:matrix.start9labs.com) - [Matrix](https://matrix.to/#/#dev-startos:matrix.start9labs.com)
## Project Structure For project structure and system architecture, see [ARCHITECTURE.md](ARCHITECTURE.md).
```bash
/
├── assets/ # Screenshots for README
├── build/ # Auxiliary files and scripts for deployed images
├── container-runtime/ # Node.js program managing package containers
├── core/ # Rust backend: API, daemon (startd), CLI (start-cli)
├── debian/ # Debian package maintainer scripts
├── image-recipe/ # Scripts for building StartOS images
├── patch-db/ # (submodule) Diff-based data store for frontend sync
├── sdk/ # TypeScript SDK for building StartOS packages
└── web/ # Web UIs (Angular)
```
See component READMEs for details:
- [`core`](core/README.md)
- [`web`](web/README.md)
- [`build`](build/README.md)
- [`patch-db`](https://github.com/Start9Labs/patch-db)
## Environment Setup ## Environment Setup

80
TODO.md
View File

@@ -21,7 +21,6 @@ Pending tasks for AI agents. Remove items when completed.
### Design ### Design
**Key distinction**: There are two separate concepts for SSL port usage: **Key distinction**: There are two separate concepts for SSL port usage:
1. **Port ownership** (`assigned_ssl_port`) — A port exclusively owned by a binding, allocated from 1. **Port ownership** (`assigned_ssl_port`) — A port exclusively owned by a binding, allocated from
`AvailablePorts`. Used for server hostnames (`.local`, mDNS, etc.) and iptables forwards. `AvailablePorts`. Used for server hostnames (`.local`, mDNS, etc.) and iptables forwards.
2. **Domain SSL port** — The port used for domain-based vhost entries. A binding does NOT need to own 2. **Domain SSL port** — The port used for domain-based vhost entries. A binding does NOT need to own
@@ -62,7 +61,6 @@ Pending tasks for AI agents. Remove items when completed.
`server.host.binding` and `package.host.binding`). `server.host.binding` and `package.host.binding`).
**How disabling works per address type** (enforcement deferred to Section 3): **How disabling works per address type** (enforcement deferred to Section 3):
- **WAN/LAN IP:port**: Will be enforced via **source-IP gating** in the vhost layer (Section 3). - **WAN/LAN IP:port**: Will be enforced via **source-IP gating** in the vhost layer (Section 3).
- **Hostname-based addresses** (`.local`, domains): Disabled by **not creating the vhost/SNI - **Hostname-based addresses** (`.local`, domains): Disabled by **not creating the vhost/SNI
entry** for that hostname. entry** for that hostname.
@@ -73,7 +71,7 @@ Pending tasks for AI agents. Remove items when completed.
`net_controller.rs`) creates a bespoke dual-vhost setup: port 5443 for private-only access and port `net_controller.rs`) creates a bespoke dual-vhost setup: port 5443 for private-only access and port
443 for public (or public+private). This exists because both public and private traffic arrive on the 443 for public (or public+private). This exists because both public and private traffic arrive on the
same port 443 listener, and the current `InterfaceFilter`/`PublicFilter` model distinguishes same port 443 listener, and the current `InterfaceFilter`/`PublicFilter` model distinguishes
public/private by which *network interface* the connection arrived on — which doesn't work when both public/private by which _network interface_ the connection arrived on — which doesn't work when both
traffic types share a listener. traffic types share a listener.
**Solution**: Determine public vs private based on **source IP** at the vhost level. Traffic arriving **Solution**: Determine public vs private based on **source IP** at the vhost level. Traffic arriving
@@ -81,7 +79,6 @@ Pending tasks for AI agents. Remove items when completed.
anything from the gateway is potentially public). Traffic from LAN IPs is private. anything from the gateway is potentially public). Traffic from LAN IPs is private.
This applies to **all** vhost targets, not just port 443: This applies to **all** vhost targets, not just port 443:
- **Add a `public` field to `ProxyTarget`** (or an enum: `Public`, `Private`, `Both`) indicating - **Add a `public` field to `ProxyTarget`** (or an enum: `Public`, `Private`, `Both`) indicating
what traffic this target accepts, derived from the binding's user-controlled `public` field. what traffic this target accepts, derived from the binding's user-controlled `public` field.
- **Modify `VHostTarget::filter()`** (`vhost.rs:342`): Instead of (or in addition to) checking the - **Modify `VHostTarget::filter()`** (`vhost.rs:342`): Instead of (or in addition to) checking the
@@ -109,7 +106,6 @@ Pending tasks for AI agents. Remove items when completed.
#### 5. Simplify `update()` Domain Vhost Logic (`net_controller.rs`) #### 5. Simplify `update()` Domain Vhost Logic (`net_controller.rs`)
With source-IP gating in the vhost controller: With source-IP gating in the vhost controller:
- **Remove the `== 443` special case** and the 5443 secondary vhost. - **Remove the `== 443` special case** and the 5443 secondary vhost.
- For **server hostnames** (`.local`, mDNS, embassy, startos, localhost): use `assigned_ssl_port` - For **server hostnames** (`.local`, mDNS, embassy, startos, localhost): use `assigned_ssl_port`
(the port the binding owns). (the port the binding owns).
@@ -122,60 +118,18 @@ Pending tasks for AI agents. Remove items when completed.
`ssl_port: assigned_ssl_port`. For domains, report `ssl_port: preferred_external_port` if it was `ssl_port: assigned_ssl_port`. For domains, report `ssl_port: preferred_external_port` if it was
successfully used for the domain vhost, otherwise report `ssl_port: assigned_ssl_port`. successfully used for the domain vhost, otherwise report `ssl_port: assigned_ssl_port`.
#### 6. Frontend: Interfaces Page Overhaul (View/Manage Split) #### 6. Reachability Test Endpoint
The current interfaces page is a single page showing gateways (with toggle), addresses, public
domains, and private domains. It gets split into two pages: **View** and **Manage**.
**SDK**: `preferredExternalPort` is already exposed. No additional SDK changes needed.
##### View Page
Displays all computed addresses for the interface (from `BindInfo.addresses`) as a flat list. For each
address, show: URL, type (IPv4, IPv6, .local, domain), access level (public/private),
gateway name, SSL indicator, enable/disable state, port forward info for public addresses, and a test button
for reachability (see Section 7).
No gateway-level toggles. The old `gateways.component.ts` toggle UI is removed.
**Note**: Exact UI element placement (where toggles, buttons, info badges go) is sensitive.
Prompt the user for specific placement decisions during implementation.
##### Manage Page
Simple CRUD interface for configuring which addresses exist. Two sections:
- **Public domains**: Add/remove. Uses existing RPC endpoints:
- `{server,package}.host.address.domain.public.add`
- `{server,package}.host.address.domain.public.remove`
- **Private domains**: Add/remove. Uses existing RPC endpoints:
- `{server,package}.host.address.domain.private.add`
- `{server,package}.host.address.domain.private.remove`
##### Key Frontend Files to Modify
| File | Change |
|------|--------|
| `web/projects/ui/src/app/routes/portal/components/interfaces/` | Overhaul: split into view/manage |
| `web/projects/ui/src/app/routes/portal/components/interfaces/gateways.component.ts` | Remove (replaced by per-address toggles on View page) |
| `web/projects/ui/src/app/routes/portal/components/interfaces/interface.service.ts` | Update `MappedServiceInterface` to compute enabled addresses from `DerivedAddressInfo` |
| `web/projects/ui/src/app/routes/portal/components/interfaces/addresses/` | Refactor for View page with overflow menu (enable/disable) and test buttons |
| `web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts` | Add routes for view/manage sub-pages |
| `web/projects/ui/src/app/routes/portal/routes/system/system.routes.ts` | Add routes for view/manage sub-pages |
#### 7. Reachability Test Endpoint
New RPC endpoint that tests whether an address is actually reachable, with diagnostic info on New RPC endpoint that tests whether an address is actually reachable, with diagnostic info on
failure. failure.
**RPC endpoint** (`binding.rs` or new file): **RPC endpoint** (`binding.rs` or new file):
- **`test-address`** — Test reachability of a specific address. - **`test-address`** — Test reachability of a specific address.
```ts ```ts
interface BindingTestAddressParams { interface BindingTestAddressParams {
internalPort: number internalPort: number;
address: HostnameInfo address: HostnameInfo;
} }
``` ```
@@ -185,8 +139,8 @@ Pending tasks for AI agents. Remove items when completed.
```ts ```ts
interface TestAddressResult { interface TestAddressResult {
dns: string[] | null // resolved IPs, null if not a domain address or lookup failed dns: string[] | null; // resolved IPs, null if not a domain address or lookup failed
portOpen: boolean | null // TCP connect result, null if not applicable portOpen: boolean | null; // TCP connect result, null if not applicable
} }
``` ```
@@ -205,17 +159,17 @@ Pending tasks for AI agents. Remove items when completed.
### Key Files ### Key Files
| File | Role | | File | Role |
|------|------| | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation, `try_alloc()` for preferred ports | | `core/src/net/forward.rs` | `AvailablePorts` — port pool allocation, `try_alloc()` for preferred ports |
| `core/src/net/host/binding.rs` | `Bindings` (Map wrapper for patchdb), `BindInfo`/`NetInfo`/`DerivedAddressInfo`/`AddressFilter` — per-address enable/disable, `set-address-enabled` RPC | | `core/src/net/host/binding.rs` | `Bindings` (Map wrapper for patchdb), `BindInfo`/`NetInfo`/`DerivedAddressInfo`/`AddressFilter` — per-address enable/disable, `set-address-enabled` RPC |
| `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — computes `DerivedAddressInfo.possible`, vhost/forward/DNS reconciliation, 5443 hack removal | | `core/src/net/net_controller.rs:259` | `NetServiceData::update()` — computes `DerivedAddressInfo.possible`, vhost/forward/DNS reconciliation, 5443 hack removal |
| `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private | | `core/src/net/vhost.rs` | `VHostController` / `ProxyTarget` — source-IP gating for public/private |
| `core/src/net/gateway.rs` | `InterfaceFilter` trait and filter types (`AddressFilter`, `PublicFilter`, etc.) | | `core/src/net/gateway.rs` | `InterfaceFilter` trait and filter types (`AddressFilter`, `PublicFilter`, etc.) |
| `core/src/net/service_interface.rs` | `HostnameInfo` — derives `Ord` for `BTreeSet` usage | | `core/src/net/service_interface.rs` | `HostnameInfo` — derives `Ord` for `BTreeSet` usage |
| `core/src/net/host/address.rs` | `HostAddress` (flattened struct), domain CRUD endpoints | | `core/src/net/host/address.rs` | `HostAddress` (flattened struct), domain CRUD endpoints |
| `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed | | `sdk/base/lib/interfaces/Host.ts` | SDK `MultiHost.bindPort()` — no changes needed |
| `core/src/db/model/public.rs` | Public DB model — port forward mapping | | `core/src/db/model/public.rs` | Public DB model — port forward mapping |
- [ ] Extract TS-exported types into a lightweight sub-crate for fast binding generation - [ ] Extract TS-exported types into a lightweight sub-crate for fast binding generation

View File

@@ -16,16 +16,16 @@ The container runtime communicates with the host via JSON-RPC over Unix socket.
## `/media/startos/` Directory (mounted by host into container) ## `/media/startos/` Directory (mounted by host into container)
| Path | Description | | Path | Description |
|------|-------------| | -------------------- | ----------------------------------------------------- |
| `volumes/<name>/` | Package data volumes (id-mapped, persistent) | | `volumes/<name>/` | Package data volumes (id-mapped, persistent) |
| `assets/` | Read-only assets from s9pk `assets.squashfs` | | `assets/` | Read-only assets from s9pk `assets.squashfs` |
| `images/<name>/` | Container images (squashfs, used for subcontainers) | | `images/<name>/` | Container images (squashfs, used for subcontainers) |
| `images/<name>.env` | Environment variables for image | | `images/<name>.env` | Environment variables for image |
| `images/<name>.json` | Image metadata | | `images/<name>.json` | Image metadata |
| `backup/` | Backup mount point (mounted during backup operations) | | `backup/` | Backup mount point (mounted during backup operations) |
| `rpc/service.sock` | RPC socket (container runtime listens here) | | `rpc/service.sock` | RPC socket (container runtime listens here) |
| `rpc/host.sock` | Host RPC socket (for effects callbacks to host) | | `rpc/host.sock` | Host RPC socket (for effects callbacks to host) |
## S9PK Structure ## S9PK Structure

View File

@@ -89,8 +89,8 @@ export class DockerProcedureContainer extends Drop {
`${packageId}.embassy`, `${packageId}.embassy`,
...new Set( ...new Set(
Object.values(hostInfo?.bindings || {}) Object.values(hostInfo?.bindings || {})
.flatMap((b) => b.addresses.possible) .flatMap((b) => b.addresses.available)
.map((h) => h.hostname.value), .map((h) => h.host),
).values(), ).values(),
] ]
const certChain = await effects.getSslCertificate({ const certChain = await effects.getSslCertificate({

View File

@@ -1245,7 +1245,7 @@ async function updateConfig(
: catchFn( : catchFn(
() => () =>
filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0] filled.addressInfo!.filter({ kind: "mdns" })!.hostnames[0]
.hostname.value, .host,
) || "" ) || ""
mutConfigValue[key] = url mutConfigValue[key] = url
} }

69
core/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,69 @@
# Core Architecture
The Rust backend daemon for StartOS.
## Binaries
The crate produces a single binary `startbox` that is symlinked under different names for different behavior:
- `startbox` / `startd` — Main daemon
- `start-cli` — CLI interface
- `start-container` — Runs inside LXC containers; communicates with host and manages subcontainers
- `registrybox` — Registry daemon
- `tunnelbox` — VPN/tunnel daemon
## Crate Structure
- `startos` — Core library that supports building `startbox`
- `helpers` — Utility functions used across both `startos` and `js-engine`
- `models` — Types shared across `startos`, `js-engine`, and `helpers`
## Key Modules
- `src/context/` — Context types (RpcContext, CliContext, InitContext, DiagnosticContext)
- `src/service/` — Service lifecycle management with actor pattern (`service_actor.rs`)
- `src/db/model/` — Patch-DB models (`public.rs` synced to frontend, `private.rs` backend-only)
- `src/net/` — Networking (DNS, ACME, WiFi, Tor via Arti, WireGuard)
- `src/s9pk/` — S9PK package format (merkle archive)
- `src/registry/` — Package registry management
## RPC Pattern
The API is JSON-RPC (not REST). All endpoints are RPC methods organized in a hierarchical command structure using [rpc-toolkit](https://github.com/Start9Labs/rpc-toolkit). Handlers are registered in a tree of `ParentHandler` nodes, with four handler types: `from_fn_async` (standard), `from_fn_async_local` (non-Send), `from_fn` (sync), and `from_fn_blocking` (blocking). Metadata like `.with_about()` drives middleware and documentation.
See [rpc-toolkit.md](rpc-toolkit.md) for full handler patterns and configuration.
## Patch-DB Patterns
Patch-DB provides diff-based state synchronization. Changes to `db/model/public.rs` automatically sync to the frontend.
**Key patterns:**
- `db.peek().await` — Get a read-only snapshot of the database state
- `db.mutate(|db| { ... }).await` — Apply mutations atomically, returns `MutateResult`
- `#[derive(HasModel)]` — Derive macro for types stored in the database, generates typed accessors
**Generated accessor types** (from `HasModel` derive):
- `as_field()` — Immutable reference: `&Model<T>`
- `as_field_mut()` — Mutable reference: `&mut Model<T>`
- `into_field()` — Owned value: `Model<T>`
**`Model<T>` APIs** (from `db/prelude.rs`):
- `.de()` — Deserialize to `T`
- `.ser(&value)` — Serialize from `T`
- `.mutate(|v| ...)` — Deserialize, mutate, reserialize
- For maps: `.keys()`, `.as_idx(&key)`, `.as_idx_mut(&key)`, `.insert()`, `.remove()`, `.contains_key()`
## i18n
See [i18n-patterns.md](i18n-patterns.md) for internationalization key conventions and the `t!()` macro.
## Rust Utilities & Patterns
See [core-rust-patterns.md](core-rust-patterns.md) for common utilities (Invoke trait, Guard pattern, mount guards, Apply trait, etc.).
## Related Documentation
- [rpc-toolkit.md](rpc-toolkit.md) — JSON-RPC handler patterns
- [i18n-patterns.md](i18n-patterns.md) — Internationalization conventions
- [core-rust-patterns.md](core-rust-patterns.md) — Common Rust utilities
- [s9pk-structure.md](s9pk-structure.md) — S9PK package format

View File

@@ -2,51 +2,24 @@
The Rust backend daemon for StartOS. The Rust backend daemon for StartOS.
## Binaries ## Architecture
- `startbox` — Main daemon (runs as `startd`) See [ARCHITECTURE.md](ARCHITECTURE.md) for binaries, modules, Patch-DB patterns, and related documentation.
- `start-cli` — CLI interface
- `start-container` — Runs inside LXC containers; communicates with host and manages subcontainers
- `registrybox` — Registry daemon
- `tunnelbox` — VPN/tunnel daemon
## Key Modules See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add RPC endpoints, TS-exported types, and i18n keys.
- `src/context/` — Context types (RpcContext, CliContext, InitContext, DiagnosticContext) ## Quick Reference
- `src/service/` — Service lifecycle management with actor pattern (`service_actor.rs`)
- `src/db/model/` — Patch-DB models (`public.rs` synced to frontend, `private.rs` backend-only)
- `src/net/` — Networking (DNS, ACME, WiFi, Tor via Arti, WireGuard)
- `src/s9pk/` — S9PK package format (merkle archive)
- `src/registry/` — Package registry management
## RPC Pattern ```bash
cargo check -p start-os # Type check
make test-core # Run tests
make ts-bindings # Regenerate TS types after changing #[ts(export)] structs
cd sdk && make baseDist dist # Rebuild SDK after ts-bindings
```
See `rpc-toolkit.md` for JSON-RPC handler patterns and configuration. ## Operating Rules
## Patch-DB Patterns - Always run `cargo check -p start-os` after modifying Rust code
- When adding RPC endpoints, follow the patterns in [rpc-toolkit.md](rpc-toolkit.md)
Patch-DB provides diff-based state synchronization. Changes to `db/model/public.rs` automatically sync to the frontend. - When modifying `#[ts(export)]` types, regenerate bindings and rebuild the SDK (see [ARCHITECTURE.md](../ARCHITECTURE.md#build-pipeline))
- When adding i18n keys, add all 5 locales in `core/locales/i18n.yaml` (see [i18n-patterns.md](i18n-patterns.md))
**Key patterns:**
- `db.peek().await` — Get a read-only snapshot of the database state
- `db.mutate(|db| { ... }).await` — Apply mutations atomically, returns `MutateResult`
- `#[derive(HasModel)]` — Derive macro for types stored in the database, generates typed accessors
**Generated accessor types** (from `HasModel` derive):
- `as_field()` — Immutable reference: `&Model<T>`
- `as_field_mut()` — Mutable reference: `&mut Model<T>`
- `into_field()` — Owned value: `Model<T>`
**`Model<T>` APIs** (from `db/prelude.rs`):
- `.de()` — Deserialize to `T`
- `.ser(&value)` — Serialize from `T`
- `.mutate(|v| ...)` — Deserialize, mutate, reserialize
- For maps: `.keys()`, `.as_idx(&key)`, `.as_idx_mut(&key)`, `.insert()`, `.remove()`, `.contains_key()`
## i18n
See `i18n-patterns.md` for internationalization key conventions and the `t!()` macro.
## Rust Utilities & Patterns
See `core-rust-patterns.md` for common utilities (Invoke trait, Guard pattern, mount guards, Apply trait, etc.).

49
core/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,49 @@
# Contributing to Core
For general environment setup, cloning, and build system, see the root [CONTRIBUTING.md](../CONTRIBUTING.md).
## Prerequisites
- [Rust](https://rustup.rs) (nightly for formatting)
- [rust-analyzer](https://rust-analyzer.github.io/) recommended
- [Docker](https://docs.docker.com/get-docker/) (for cross-compilation via `rust-zig-builder` container)
## Common Commands
```bash
cargo check -p start-os # Type check
cargo test --features=test # Run tests (or: make test-core)
make format # Format with nightly rustfmt
cd core && cargo test <test_name> --features=test # Run a specific test
```
## Adding a New RPC Endpoint
1. Define a params struct with `#[derive(Deserialize, Serialize)]`
2. Choose a handler type (`from_fn_async` for most cases)
3. Write the handler function: `async fn my_handler(ctx: RpcContext, params: MyParams) -> Result<MyResponse, Error>`
4. Register it in the appropriate `ParentHandler` tree
5. If params/response should be available in TypeScript, add `#[derive(TS)]` and `#[ts(export)]`
See [rpc-toolkit.md](rpc-toolkit.md) for full handler patterns and all four handler types.
## Adding TS-Exported Types
When a Rust type needs to be available in TypeScript (for the web frontend or SDK):
1. Add `ts_rs::TS` to the derive list and `#[ts(export)]` to the struct/enum
2. Use `#[serde(rename_all = "camelCase")]` for JS-friendly field names
3. For types that don't implement TS (like `DateTime<Utc>`, `exver::Version`), use `#[ts(type = "string")]` overrides
4. For `u64` fields that should be JS `number` (not `bigint`), use `#[ts(type = "number")]`
5. Run `make ts-bindings` to regenerate — files appear in `core/bindings/` then sync to `sdk/base/lib/osBindings/`
6. Rebuild the SDK: `cd sdk && make baseDist dist`
## Adding i18n Keys
1. Add the key to `core/locales/i18n.yaml` with all 5 language translations
2. Use the `t!("your.key.name")` macro in Rust code
3. Follow existing namespace conventions — match the module path where the key is used
4. Use kebab-case for multi-word segments
5. Translations are validated at compile time
See [i18n-patterns.md](i18n-patterns.md) for full conventions.

View File

@@ -22,9 +22,7 @@ several different names for different behavior:
- `start-sdk`: This is a CLI tool that aids in building and packaging services - `start-sdk`: This is a CLI tool that aids in building and packaging services
you wish to deploy to StartOS you wish to deploy to StartOS
## Questions ## Documentation
If you have questions about how various pieces of the backend system work. Open - [ARCHITECTURE.md](ARCHITECTURE.md) — Backend architecture, modules, and patterns
an issue and tag the following people - [CONTRIBUTING.md](CONTRIBUTING.md) — How to contribute to core
- dr-bonez

View File

@@ -271,6 +271,7 @@ pub fn display_action_result<T: Serialize>(
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RunActionParams { pub struct RunActionParams {
pub package_id: PackageId, pub package_id: PackageId,
@@ -362,6 +363,7 @@ pub async fn run_action(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ClearTaskParams { pub struct ClearTaskParams {

View File

@@ -418,6 +418,7 @@ impl AsLogoutSessionId for KillSessionId {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct KillParams { pub struct KillParams {
@@ -435,6 +436,7 @@ pub async fn kill<C: SessionAuthContext>(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ResetPasswordParams { pub struct ResetPasswordParams {

View File

@@ -30,6 +30,7 @@ use crate::util::serde::IoFormat;
use crate::version::VersionT; use crate::version::VersionT;
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct BackupParams { pub struct BackupParams {

View File

@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::PackageId; use crate::PackageId;
use crate::context::CliContext; use crate::context::CliContext;
@@ -13,19 +14,22 @@ pub mod os;
pub mod restore; pub mod restore;
pub mod target; pub mod target;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct BackupReport { pub struct BackupReport {
server: ServerBackupReport, server: ServerBackupReport,
packages: BTreeMap<PackageId, PackageBackupReport>, packages: BTreeMap<PackageId, PackageBackupReport>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct ServerBackupReport { pub struct ServerBackupReport {
attempted: bool, attempted: bool,
error: Option<String>, error: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct PackageBackupReport { pub struct PackageBackupReport {
pub error: Option<String>, pub error: Option<String>,
} }

View File

@@ -30,6 +30,7 @@ use crate::{PLATFORM, PackageId};
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
#[ts(export)]
pub struct RestorePackageParams { pub struct RestorePackageParams {
#[arg(help = "help.arg.package-ids")] #[arg(help = "help.arg.package-ids")]
pub ids: Vec<PackageId>, pub ids: Vec<PackageId>,

View File

@@ -36,7 +36,8 @@ impl Map for CifsTargets {
} }
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CifsBackupTarget { pub struct CifsBackupTarget {
hostname: String, hostname: String,
@@ -72,9 +73,10 @@ pub fn cifs<C: Context>() -> ParentHandler<C> {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct AddParams { pub struct CifsAddParams {
#[arg(help = "help.arg.cifs-hostname")] #[arg(help = "help.arg.cifs-hostname")]
pub hostname: String, pub hostname: String,
#[arg(help = "help.arg.cifs-path")] #[arg(help = "help.arg.cifs-path")]
@@ -87,12 +89,12 @@ pub struct AddParams {
pub async fn add( pub async fn add(
ctx: RpcContext, ctx: RpcContext,
AddParams { CifsAddParams {
hostname, hostname,
path, path,
username, username,
password, password,
}: AddParams, }: CifsAddParams,
) -> Result<KeyVal<BackupTargetId, BackupTarget>, Error> { ) -> Result<KeyVal<BackupTargetId, BackupTarget>, Error> {
let cifs = Cifs { let cifs = Cifs {
hostname, hostname,
@@ -131,9 +133,10 @@ pub async fn add(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct UpdateParams { pub struct CifsUpdateParams {
#[arg(help = "help.arg.backup-target-id")] #[arg(help = "help.arg.backup-target-id")]
pub id: BackupTargetId, pub id: BackupTargetId,
#[arg(help = "help.arg.cifs-hostname")] #[arg(help = "help.arg.cifs-hostname")]
@@ -148,13 +151,13 @@ pub struct UpdateParams {
pub async fn update( pub async fn update(
ctx: RpcContext, ctx: RpcContext,
UpdateParams { CifsUpdateParams {
id, id,
hostname, hostname,
path, path,
username, username,
password, password,
}: UpdateParams, }: CifsUpdateParams,
) -> Result<KeyVal<BackupTargetId, BackupTarget>, Error> { ) -> Result<KeyVal<BackupTargetId, BackupTarget>, Error> {
let id = if let BackupTargetId::Cifs { id } = id { let id = if let BackupTargetId::Cifs { id } = id {
id id
@@ -207,14 +210,15 @@ pub async fn update(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct RemoveParams { pub struct CifsRemoveParams {
#[arg(help = "help.arg.backup-target-id")] #[arg(help = "help.arg.backup-target-id")]
pub id: BackupTargetId, pub id: BackupTargetId,
} }
pub async fn remove(ctx: RpcContext, RemoveParams { id }: RemoveParams) -> Result<(), Error> { pub async fn remove(ctx: RpcContext, CifsRemoveParams { id }: CifsRemoveParams) -> Result<(), Error> {
let id = if let BackupTargetId::Cifs { id } = id { let id = if let BackupTargetId::Cifs { id } = id {
id id
} else { } else {

View File

@@ -34,7 +34,8 @@ use crate::util::{FromStrParser, VersionString};
pub mod cifs; pub mod cifs;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum BackupTarget { pub enum BackupTarget {
@@ -49,7 +50,7 @@ pub enum BackupTarget {
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, TS)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, TS)]
#[ts(type = "string")] #[ts(export, type = "string")]
pub enum BackupTargetId { pub enum BackupTargetId {
Disk { logicalname: PathBuf }, Disk { logicalname: PathBuf },
Cifs { id: u32 }, Cifs { id: u32 },
@@ -111,6 +112,7 @@ impl Serialize for BackupTargetId {
} }
#[derive(Debug, Deserialize, Serialize, TS)] #[derive(Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum BackupTargetFS { pub enum BackupTargetFS {
@@ -210,20 +212,26 @@ pub async fn list(ctx: RpcContext) -> Result<BTreeMap<BackupTargetId, BackupTarg
.collect()) .collect())
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BackupInfo { pub struct BackupInfo {
#[ts(type = "string")]
pub version: Version, pub version: Version,
#[ts(type = "string | null")]
pub timestamp: Option<DateTime<Utc>>, pub timestamp: Option<DateTime<Utc>>,
pub package_backups: BTreeMap<PackageId, PackageBackupInfo>, pub package_backups: BTreeMap<PackageId, PackageBackupInfo>,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PackageBackupInfo { pub struct PackageBackupInfo {
pub title: InternedString, pub title: InternedString,
pub version: VersionString, pub version: VersionString,
#[ts(type = "string")]
pub os_version: Version, pub os_version: Version,
#[ts(type = "string")]
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
} }
@@ -265,6 +273,7 @@ fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) -> Re
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct InfoParams { pub struct InfoParams {
@@ -387,6 +396,7 @@ pub async fn mount(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct UmountParams { pub struct UmountParams {

View File

@@ -8,6 +8,7 @@ use crate::prelude::*;
use crate::{Error, PackageId}; use crate::{Error, PackageId};
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ControlParams { pub struct ControlParams {

View File

@@ -93,6 +93,7 @@ impl Public {
), ),
public_domains: BTreeMap::new(), public_domains: BTreeMap::new(),
private_domains: BTreeMap::new(), private_domains: BTreeMap::new(),
port_forwards: BTreeSet::new(),
}, },
wifi: WifiInfo { wifi: WifiInfo {
enabled: true, enabled: true,

View File

@@ -43,22 +43,28 @@ pub struct DiskInfo {
pub guid: Option<InternedString>, pub guid: Option<InternedString>,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PartitionInfo { pub struct PartitionInfo {
pub logicalname: PathBuf, pub logicalname: PathBuf,
pub label: Option<String>, pub label: Option<String>,
#[ts(type = "number")]
pub capacity: u64, pub capacity: u64,
#[ts(type = "number | null")]
pub used: Option<u64>, pub used: Option<u64>,
pub start_os: BTreeMap<String, StartOsRecoveryInfo>, pub start_os: BTreeMap<String, StartOsRecoveryInfo>,
pub guid: Option<InternedString>, pub guid: Option<InternedString>,
} }
#[derive(Clone, Debug, Default, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Deserialize, Serialize, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StartOsRecoveryInfo { pub struct StartOsRecoveryInfo {
pub hostname: Hostname, pub hostname: Hostname,
#[ts(type = "string")]
pub version: exver::Version, pub version: exver::Version,
#[ts(type = "string")]
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
pub password_hash: Option<String>, pub password_hash: Option<String>,
pub wrapped_key: Option<String>, pub wrapped_key: Option<String>,

View File

@@ -6,7 +6,8 @@ use tracing::instrument;
use crate::util::Invoke; use crate::util::Invoke;
use crate::{Error, ErrorKind}; use crate::{Error, ErrorKind};
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize, ts_rs::TS)]
#[ts(type = "string")]
pub struct Hostname(pub InternedString); pub struct Hostname(pub InternedString);
lazy_static::lazy_static! { lazy_static::lazy_static! {

View File

@@ -177,6 +177,7 @@ pub async fn install(
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SideloadParams { pub struct SideloadParams {
#[ts(skip)] #[ts(skip)]
@@ -185,6 +186,7 @@ pub struct SideloadParams {
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SideloadResponse { pub struct SideloadResponse {
pub upload: Guid, pub upload: Guid,
@@ -284,6 +286,7 @@ pub async fn sideload(
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct CancelInstallParams { pub struct CancelInstallParams {
@@ -521,6 +524,7 @@ pub async fn cli_install(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct UninstallParams { pub struct UninstallParams {

View File

@@ -24,6 +24,7 @@ use tokio::process::{Child, Command};
use tokio_stream::wrappers::LinesStream; use tokio_stream::wrappers::LinesStream;
use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::tungstenite::Message;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS;
use crate::PackageId; use crate::PackageId;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
@@ -109,23 +110,28 @@ async fn ws_handler(
} }
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LogResponse { pub struct LogResponse {
#[ts(as = "Vec<LogEntry>")]
pub entries: Reversible<LogEntry>, pub entries: Reversible<LogEntry>,
start_cursor: Option<String>, start_cursor: Option<String>,
end_cursor: Option<String>, end_cursor: Option<String>,
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LogFollowResponse { pub struct LogFollowResponse {
start_cursor: Option<String>, start_cursor: Option<String>,
guid: Guid, guid: Guid,
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LogEntry { pub struct LogEntry {
#[ts(type = "string")]
timestamp: DateTime<Utc>, timestamp: DateTime<Utc>,
message: String, message: String,
boot_id: String, boot_id: String,
@@ -321,14 +327,17 @@ impl From<BootIdentifier> for String {
} }
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export, concrete(Extra = Empty), bound = "")]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct LogsParams<Extra: FromArgMatches + Args = Empty> { pub struct LogsParams<Extra: FromArgMatches + Args = Empty> {
#[command(flatten)] #[command(flatten)]
#[serde(flatten)] #[serde(flatten)]
#[ts(skip)]
extra: Extra, extra: Extra,
#[arg(short = 'l', long = "limit", help = "help.arg.log-limit")] #[arg(short = 'l', long = "limit", help = "help.arg.log-limit")]
#[ts(optional)]
limit: Option<usize>, limit: Option<usize>,
#[arg( #[arg(
short = 'c', short = 'c',
@@ -336,9 +345,11 @@ pub struct LogsParams<Extra: FromArgMatches + Args = Empty> {
conflicts_with = "follow", conflicts_with = "follow",
help = "help.arg.log-cursor" help = "help.arg.log-cursor"
)] )]
#[ts(optional)]
cursor: Option<String>, cursor: Option<String>,
#[arg(short = 'b', long = "boot", help = "help.arg.log-boot")] #[arg(short = 'b', long = "boot", help = "help.arg.log-boot")]
#[serde(default)] #[serde(default)]
#[ts(optional, type = "number | string")]
boot: Option<BootIdentifier>, boot: Option<BootIdentifier>,
#[arg( #[arg(
short = 'B', short = 'B',

View File

@@ -461,7 +461,8 @@ impl ValueParserFactory for AcmeProvider {
} }
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct InitAcmeParams { pub struct InitAcmeParams {
#[arg(long, help = "help.arg.acme-provider")] #[arg(long, help = "help.arg.acme-provider")]
pub provider: AcmeProvider, pub provider: AcmeProvider,
@@ -486,7 +487,8 @@ pub async fn init(
Ok(()) Ok(())
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct RemoveAcmeParams { pub struct RemoveAcmeParams {
#[arg(long, help = "help.arg.acme-provider")] #[arg(long, help = "help.arg.acme-provider")]
pub provider: AcmeProvider, pub provider: AcmeProvider,

View File

@@ -25,6 +25,7 @@ use serde::{Deserialize, Serialize};
use tokio::net::{TcpListener, UdpSocket}; use tokio::net::{TcpListener, UdpSocket};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::instrument; use tracing::instrument;
use ts_rs::TS;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::Database; use crate::db::model::Database;
@@ -93,7 +94,8 @@ pub fn dns_api<C: Context>() -> ParentHandler<C> {
) )
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct QueryDnsParams { pub struct QueryDnsParams {
#[arg(help = "help.arg.fqdn")] #[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString, pub fqdn: InternedString,
@@ -133,7 +135,8 @@ pub fn query_dns<C: Context>(
.map_err(Error::from) .map_err(Error::from)
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct SetStaticDnsParams { pub struct SetStaticDnsParams {
#[arg(help = "help.arg.dns-servers")] #[arg(help = "help.arg.dns-servers")]
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,

View File

@@ -119,6 +119,7 @@ async fn list_interfaces(
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct ForgetGatewayParams { struct ForgetGatewayParams {
#[arg(help = "help.arg.gateway-id")] #[arg(help = "help.arg.gateway-id")]
gateway: GatewayId, gateway: GatewayId,
@@ -132,6 +133,7 @@ async fn forget_iface(
} }
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
struct RenameGatewayParams { struct RenameGatewayParams {
#[arg(help = "help.arg.gateway-id")] #[arg(help = "help.arg.gateway-id")]
id: GatewayId, id: GatewayId,
@@ -1005,9 +1007,10 @@ impl NetworkInterfaceController {
.as_network_mut() .as_network_mut()
.as_gateways_mut() .as_gateways_mut()
.ser(info)?; .ser(info)?;
let hostname = crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?);
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
for host in all_hosts(db) { for host in all_hosts(db) {
host?.update_addresses(info, &ports)?; host?.update_addresses(&hostname, info, &ports)?;
} }
Ok(()) Ok(())
}) })

View File

@@ -10,6 +10,7 @@ use ts_rs::TS;
use crate::GatewayId; use crate::GatewayId;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::hostname::Hostname;
use crate::net::acme::AcmeProvider; use crate::net::acme::AcmeProvider;
use crate::net::host::{HostApiKind, all_hosts}; use crate::net::host::{HostApiKind, all_hosts};
use crate::prelude::*; use crate::prelude::*;
@@ -24,6 +25,7 @@ pub struct HostAddress {
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS)] #[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct PublicDomainConfig { pub struct PublicDomainConfig {
pub gateway: GatewayId, pub gateway: GatewayId,
pub acme: Option<AcmeProvider>, pub acme: Option<AcmeProvider>,
@@ -157,7 +159,8 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
) )
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct AddPublicDomainParams { pub struct AddPublicDomainParams {
#[arg(help = "help.arg.fqdn")] #[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString, pub fqdn: InternedString,
@@ -194,9 +197,10 @@ pub async fn add_public_domain<Kind: HostApiKind>(
.as_public_domains_mut() .as_public_domains_mut()
.insert(&fqdn, &PublicDomainConfig { acme, gateway })?; .insert(&fqdn, &PublicDomainConfig { acme, gateway })?;
handle_duplicates(db)?; handle_duplicates(db)?;
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })
.await .await
.result?; .result?;
@@ -209,7 +213,8 @@ pub async fn add_public_domain<Kind: HostApiKind>(
.with_kind(ErrorKind::Unknown)? .with_kind(ErrorKind::Unknown)?
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct RemoveDomainParams { pub struct RemoveDomainParams {
#[arg(help = "help.arg.fqdn")] #[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString, pub fqdn: InternedString,
@@ -225,9 +230,10 @@ pub async fn remove_public_domain<Kind: HostApiKind>(
Kind::host_for(&inheritance, db)? Kind::host_for(&inheritance, db)?
.as_public_domains_mut() .as_public_domains_mut()
.remove(&fqdn)?; .remove(&fqdn)?;
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })
.await .await
.result?; .result?;
@@ -236,7 +242,8 @@ pub async fn remove_public_domain<Kind: HostApiKind>(
Ok(()) Ok(())
} }
#[derive(Deserialize, Serialize, Parser)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct AddPrivateDomainParams { pub struct AddPrivateDomainParams {
#[arg(help = "help.arg.fqdn")] #[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString, pub fqdn: InternedString,
@@ -255,9 +262,10 @@ pub async fn add_private_domain<Kind: HostApiKind>(
.upsert(&fqdn, || Ok(BTreeSet::new()))? .upsert(&fqdn, || Ok(BTreeSet::new()))?
.mutate(|d| Ok(d.insert(gateway)))?; .mutate(|d| Ok(d.insert(gateway)))?;
handle_duplicates(db)?; handle_duplicates(db)?;
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })
.await .await
.result?; .result?;
@@ -276,9 +284,10 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
Kind::host_for(&inheritance, db)? Kind::host_for(&inheritance, db)?
.as_private_domains_mut() .as_private_domains_mut()
.mutate(|d| Ok(d.remove(&domain)))?; .mutate(|d| Ok(d.remove(&domain)))?;
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
Kind::host_for(&inheritance, db)?.update_addresses(&gateways, &ports) Kind::host_for(&inheritance, db)?.update_addresses(&hostname, &gateways, &ports)
}) })
.await .await
.result?; .result?;

View File

@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::future::Future; use std::future::Future;
use std::net::{IpAddr, SocketAddrV4};
use std::panic::RefUnwindSafe; use std::panic::RefUnwindSafe;
use clap::Parser; use clap::Parser;
@@ -13,7 +14,8 @@ use ts_rs::TS;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::DatabaseModel; use crate::db::model::DatabaseModel;
use crate::db::model::public::NetworkInterfaceInfo; use crate::db::model::public::{NetworkInterfaceInfo, NetworkInterfaceType};
use crate::hostname::Hostname;
use crate::net::forward::AvailablePorts; use crate::net::forward::AvailablePorts;
use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api}; use crate::net::host::address::{HostAddress, PublicDomainConfig, address_api};
use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding}; use crate::net::host::binding::{BindInfo, BindOptions, Bindings, binding};
@@ -32,6 +34,20 @@ pub struct Host {
pub bindings: Bindings, pub bindings: Bindings,
pub public_domains: BTreeMap<InternedString, PublicDomainConfig>, pub public_domains: BTreeMap<InternedString, PublicDomainConfig>,
pub private_domains: BTreeMap<InternedString, BTreeSet<GatewayId>>, pub private_domains: BTreeMap<InternedString, BTreeSet<GatewayId>>,
/// COMPUTED: port forwarding rules needed on gateways for public addresses to work.
#[serde(default)]
pub port_forwards: BTreeSet<PortForward>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct PortForward {
#[ts(type = "string")]
pub src: SocketAddrV4,
#[ts(type = "string")]
pub dst: SocketAddrV4,
pub gateway: GatewayId,
} }
impl AsRef<Host> for Host { impl AsRef<Host> for Host {
@@ -66,10 +82,13 @@ impl Host {
impl Model<Host> { impl Model<Host> {
pub fn update_addresses( pub fn update_addresses(
&mut self, &mut self,
mdns: &Hostname,
gateways: &OrdMap<GatewayId, NetworkInterfaceInfo>, gateways: &OrdMap<GatewayId, NetworkInterfaceInfo>,
available_ports: &AvailablePorts, available_ports: &AvailablePorts,
) -> Result<(), Error> { ) -> Result<(), Error> {
let this = self.destructure_mut(); let this = self.destructure_mut();
// ips
for (_, bind) in this.bindings.as_entries_mut()? { for (_, bind) in this.bindings.as_entries_mut()? {
let net = bind.as_net().de()?; let net = bind.as_net().de()?;
let opt = bind.as_options().de()?; let opt = bind.as_options().de()?;
@@ -143,6 +162,46 @@ impl Model<Host> {
} }
} }
} }
// mdns
let mdns_host = mdns.local_domain_name();
let mdns_gateways: BTreeSet<GatewayId> = gateways
.iter()
.filter(|(_, g)| {
matches!(
g.ip_info.as_ref().and_then(|i| i.device_type),
Some(NetworkInterfaceType::Ethernet | NetworkInterfaceType::Wireless)
)
})
.map(|(id, _)| id.clone())
.collect();
if let Some(port) = net.assigned_port.filter(|_| {
opt.secure
.map_or(true, |s| !(s.ssl && opt.add_ssl.is_some()))
}) {
available.insert(HostnameInfo {
ssl: opt.secure.map_or(false, |s| s.ssl),
public: false,
host: mdns_host.clone(),
port: Some(port),
metadata: HostnameMetadata::Mdns {
gateways: mdns_gateways.clone(),
},
});
}
if let Some(port) = net.assigned_ssl_port {
available.insert(HostnameInfo {
ssl: true,
public: false,
host: mdns_host,
port: Some(port),
metadata: HostnameMetadata::Mdns {
gateways: mdns_gateways,
},
});
}
// public domains
for (domain, info) in this.public_domains.de()? { for (domain, info) in this.public_domains.de()? {
let metadata = HostnameMetadata::PublicDomain { let metadata = HostnameMetadata::PublicDomain {
gateway: info.gateway.clone(), gateway: info.gateway.clone(),
@@ -173,12 +232,14 @@ impl Model<Host> {
available.insert(HostnameInfo { available.insert(HostnameInfo {
ssl: true, ssl: true,
public: true, public: true,
host: domain.clone(), host: domain,
port: Some(port), port: Some(port),
metadata, metadata,
}); });
} }
} }
// private domains
for (domain, domain_gateways) in this.private_domains.de()? { for (domain, domain_gateways) in this.private_domains.de()? {
if let Some(port) = net.assigned_port.filter(|_| { if let Some(port) = net.assigned_port.filter(|_| {
opt.secure opt.secure
@@ -213,7 +274,7 @@ impl Model<Host> {
available.insert(HostnameInfo { available.insert(HostnameInfo {
ssl: true, ssl: true,
public: true, public: true,
host: domain.clone(), host: domain,
port: Some(port), port: Some(port),
metadata: HostnameMetadata::PrivateDomain { metadata: HostnameMetadata::PrivateDomain {
gateways: domain_gateways, gateways: domain_gateways,
@@ -223,6 +284,46 @@ impl Model<Host> {
} }
bind.as_addresses_mut().as_available_mut().ser(&available)?; bind.as_addresses_mut().as_available_mut().ser(&available)?;
} }
// compute port forwards from available public addresses
let bindings: Bindings = this.bindings.de()?;
let mut port_forwards = BTreeSet::new();
for bind in bindings.values() {
for addr in &bind.addresses.available {
if !addr.public {
continue;
}
let Some(port) = addr.port else {
continue;
};
let gw_id = match &addr.metadata {
HostnameMetadata::Ipv4 { gateway }
| HostnameMetadata::PublicDomain { gateway } => gateway,
_ => continue,
};
let Some(gw_info) = gateways.get(gw_id) else {
continue;
};
let Some(ip_info) = &gw_info.ip_info else {
continue;
};
let Some(wan_ip) = ip_info.wan_ip else {
continue;
};
for subnet in &ip_info.subnets {
let IpAddr::V4(addr) = subnet.addr() else {
continue;
};
port_forwards.insert(PortForward {
src: SocketAddrV4::new(wan_ip, port),
dst: SocketAddrV4::new(addr, port),
gateway: gw_id.clone(),
});
}
}
}
this.port_forwards.ser(&port_forwards)?;
Ok(()) Ok(())
} }
} }

View File

@@ -539,10 +539,11 @@ impl NetService {
.as_network() .as_network()
.as_gateways() .as_gateways()
.de()?; .de()?;
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let mut ports = db.as_private().as_available_ports().de()?; let mut ports = db.as_private().as_available_ports().de()?;
let host = host_for(db, pkg_id.as_ref(), &id)?; let host = host_for(db, pkg_id.as_ref(), &id)?;
host.add_binding(&mut ports, internal_port, options)?; host.add_binding(&mut ports, internal_port, options)?;
host.update_addresses(&gateways, &ports)?; host.update_addresses(&hostname, &gateways, &ports)?;
db.as_private_mut().as_available_ports_mut().ser(&ports)?; db.as_private_mut().as_available_ports_mut().ser(&ports)?;
Ok(()) Ok(())
}) })
@@ -563,6 +564,7 @@ impl NetService {
.as_network() .as_network()
.as_gateways() .as_gateways()
.de()?; .de()?;
let hostname = Hostname(db.as_public().as_server_info().as_hostname().de()?);
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
if let Some(ref pkg_id) = pkg_id { if let Some(ref pkg_id) = pkg_id {
for (host_id, host) in db for (host_id, host) in db
@@ -584,7 +586,7 @@ impl NetService {
} }
Ok(()) Ok(())
})?; })?;
host.update_addresses(&gateways, &ports)?; host.update_addresses(&hostname, &gateways, &ports)?;
} }
} else { } else {
let host = db let host = db
@@ -603,7 +605,7 @@ impl NetService {
} }
Ok(()) Ok(())
})?; })?;
host.update_addresses(&gateways, &ports)?; host.update_addresses(&hostname, &gateways, &ports)?;
} }
Ok(()) Ok(())
}) })

View File

@@ -32,6 +32,9 @@ pub enum HostnameMetadata {
gateway: GatewayId, gateway: GatewayId,
scope_id: u32, scope_id: u32,
}, },
Mdns {
gateways: BTreeSet<GatewayId>,
},
PrivateDomain { PrivateDomain {
gateways: BTreeSet<GatewayId>, gateways: BTreeSet<GatewayId>,
}, },
@@ -67,7 +70,9 @@ impl HostnameMetadata {
Self::Ipv4 { gateway } Self::Ipv4 { gateway }
| Self::Ipv6 { gateway, .. } | Self::Ipv6 { gateway, .. }
| Self::PublicDomain { gateway } => Box::new(std::iter::once(gateway)), | Self::PublicDomain { gateway } => Box::new(std::iter::once(gateway)),
Self::PrivateDomain { gateways } => Box::new(gateways.iter()), Self::PrivateDomain { gateways } | Self::Mdns { gateways } => {
Box::new(gateways.iter())
}
Self::Plugin { .. } => Box::new(std::iter::empty()), Self::Plugin { .. } => Box::new(std::iter::empty()),
} }
} }

View File

@@ -175,13 +175,14 @@ pub async fn remove_tunnel(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
let hostname = crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?);
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
for host in all_hosts(db) { for host in all_hosts(db) {
let host = host?; let host = host?;
host.as_public_domains_mut() host.as_public_domains_mut()
.mutate(|p| Ok(p.retain(|_, v| v.gateway != id)))?; .mutate(|p| Ok(p.retain(|_, v| v.gateway != id)))?;
host.update_addresses(&gateways, &ports)?; host.update_addresses(&hostname, &gateways, &ports)?;
} }
Ok(()) Ok(())
@@ -193,6 +194,7 @@ pub async fn remove_tunnel(
ctx.db ctx.db
.mutate(|db| { .mutate(|db| {
let hostname = crate::hostname::Hostname(db.as_public().as_server_info().as_hostname().de()?);
let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?; let gateways = db.as_public().as_server_info().as_network().as_gateways().de()?;
let ports = db.as_private().as_available_ports().de()?; let ports = db.as_private().as_available_ports().de()?;
for host in all_hosts(db) { for host in all_hosts(db) {
@@ -204,7 +206,7 @@ pub async fn remove_tunnel(
d.retain(|_, gateways| !gateways.is_empty()); d.retain(|_, gateways| !gateways.is_empty());
Ok(()) Ok(())
})?; })?;
host.update_addresses(&gateways, &ports)?; host.update_addresses(&hostname, &gateways, &ports)?;
} }
Ok(()) Ok(())

View File

@@ -278,8 +278,7 @@ impl Accept for VHostBindListener {
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> { ) -> Poll<Result<(Self::Metadata, AcceptStream), Error>> {
// Update listeners when ip_info or bind_reqs change // Update listeners when ip_info or bind_reqs change
while self.ip_info.poll_changed(cx).is_ready() while self.ip_info.poll_changed(cx).is_ready() || self.bind_reqs.poll_changed(cx).is_ready()
|| self.bind_reqs.poll_changed(cx).is_ready()
{ {
let reqs = self.bind_reqs.read_and_mark_seen(); let reqs = self.bind_reqs.read_and_mark_seen();
let listeners = &mut self.listeners; let listeners = &mut self.listeners;
@@ -506,10 +505,8 @@ where
}; };
let src = tcp.peer_addr.ip(); let src = tcp.peer_addr.ip();
// Public if: source is a gateway/router IP (NAT'd internet), // Public: source is outside all known subnets (direct internet)
// or source is outside all known subnets (direct internet) let is_public = !ip_info.subnets.iter().any(|s| s.contains(&src));
let is_public = ip_info.lan_ip.contains(&src)
|| !ip_info.subnets.iter().any(|s| s.contains(&src));
if is_public { if is_public {
self.public.contains(&gw.id) self.public.contains(&gw.id)
@@ -695,6 +692,7 @@ where
let (target, rc) = self.0.peek(|m| { let (target, rc) = self.0.peek(|m| {
m.get(&hello.server_name().map(InternedString::from)) m.get(&hello.server_name().map(InternedString::from))
.or_else(|| m.get(&None))
.into_iter() .into_iter()
.flatten() .flatten()
.filter(|(_, rc)| rc.strong_count() > 0) .filter(|(_, rc)| rc.strong_count() > 0)

View File

@@ -85,6 +85,7 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct SetWifiEnabledParams { pub struct SetWifiEnabledParams {
@@ -150,16 +151,17 @@ pub fn country<C: Context>() -> ParentHandler<C> {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct AddParams { pub struct WifiAddParams {
#[arg(help = "help.arg.wifi-ssid")] #[arg(help = "help.arg.wifi-ssid")]
ssid: String, ssid: String,
#[arg(help = "help.arg.wifi-password")] #[arg(help = "help.arg.wifi-password")]
password: String, password: String,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Result<(), Error> { pub async fn add(ctx: RpcContext, WifiAddParams { ssid, password }: WifiAddParams) -> Result<(), Error> {
let wifi_manager = ctx.wifi_manager.clone(); let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
@@ -229,15 +231,16 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re
Ok(()) Ok(())
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct SsidParams { pub struct WifiSsidParams {
#[arg(help = "help.arg.wifi-ssid")] #[arg(help = "help.arg.wifi-ssid")]
ssid: String, ssid: String,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { pub async fn connect(ctx: RpcContext, WifiSsidParams { ssid }: WifiSsidParams) -> Result<(), Error> {
let wifi_manager = ctx.wifi_manager.clone(); let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
@@ -311,7 +314,7 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<(), Error> { pub async fn remove(ctx: RpcContext, WifiSsidParams { ssid }: WifiSsidParams) -> Result<(), Error> {
let wifi_manager = ctx.wifi_manager.clone(); let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() { if !ssid.is_ascii() {
return Err(Error::new( return Err(Error::new(
@@ -359,11 +362,13 @@ pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<
.result?; .result?;
Ok(()) Ok(())
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct WifiListInfo { pub struct WifiListInfo {
ssids: HashMap<Ssid, SignalStrength>, ssids: HashMap<Ssid, SignalStrength>,
connected: Option<Ssid>, connected: Option<Ssid>,
#[ts(type = "string | null")]
country: Option<CountryCode>, country: Option<CountryCode>,
ethernet: bool, ethernet: bool,
available_wifi: Vec<WifiListOut>, available_wifi: Vec<WifiListOut>,
@@ -374,7 +379,8 @@ pub struct WifiListInfoLow {
strength: SignalStrength, strength: SignalStrength,
security: Vec<String>, security: Vec<String>,
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct WifiListOut { pub struct WifiListOut {
ssid: Ssid, ssid: Ssid,
@@ -560,6 +566,7 @@ pub async fn get_available(ctx: RpcContext, _: Empty) -> Result<Vec<WifiListOut>
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct SetCountryParams { pub struct SetCountryParams {
@@ -605,7 +612,7 @@ pub struct NetworkId(String);
/// Ssid are the names of the wifis, usually human readable. /// Ssid are the names of the wifis, usually human readable.
#[derive( #[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, TS,
)] )]
pub struct Ssid(String); pub struct Ssid(String);
@@ -622,6 +629,7 @@ pub struct Ssid(String);
Hash, Hash,
serde::Serialize, serde::Serialize,
serde::Deserialize, serde::Deserialize,
TS,
)] )]
pub struct SignalStrength(u8); pub struct SignalStrength(u8);

View File

@@ -75,6 +75,7 @@ pub fn notification<C: Context>() -> ParentHandler<C> {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ListNotificationParams { pub struct ListNotificationParams {
@@ -140,6 +141,7 @@ pub async fn list(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ModifyNotificationParams { pub struct ModifyNotificationParams {
@@ -175,6 +177,7 @@ pub async fn remove(
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct ModifyNotificationBeforeParams { pub struct ModifyNotificationBeforeParams {
@@ -326,6 +329,7 @@ pub async fn create(
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, TS)] #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum NotificationLevel { pub enum NotificationLevel {
Success, Success,
@@ -396,26 +400,31 @@ impl Map for Notifications {
} }
} }
#[derive(Debug, Serialize, Deserialize, HasModel)] #[derive(Debug, Serialize, Deserialize, HasModel, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[model = "Model<Self>"] #[model = "Model<Self>"]
pub struct Notification { pub struct Notification {
pub package_id: Option<PackageId>, pub package_id: Option<PackageId>,
#[ts(type = "string")]
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub code: u32, pub code: u32,
pub level: NotificationLevel, pub level: NotificationLevel,
pub title: String, pub title: String,
pub message: String, pub message: String,
#[ts(type = "any")]
pub data: Value, pub data: Value,
#[serde(default = "const_true")] #[serde(default = "const_true")]
pub seen: bool, pub seen: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct NotificationWithId { pub struct NotificationWithId {
id: u32, id: u32,
#[serde(flatten)] #[serde(flatten)]
#[ts(flatten)]
notification: Notification, notification: Notification,
} }

View File

@@ -240,7 +240,7 @@ impl LocaleString {
pub fn localize(&mut self) { pub fn localize(&mut self) {
self.localize_for(&*rust_i18n::locale()); self.localize_for(&*rust_i18n::locale());
} }
pub fn localized(mut self) -> String { pub fn localized(self) -> String {
self.localized_for(&*rust_i18n::locale()) self.localized_for(&*rust_i18n::locale())
} }
} }

View File

@@ -151,7 +151,7 @@ async fn get_action_input(
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] #[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export, rename = "EffectsRunActionParams")]
pub struct RunActionParams { pub struct RunActionParams {
#[serde(default)] #[serde(default)]
#[ts(skip)] #[ts(skip)]

View File

@@ -701,6 +701,7 @@ struct ServiceActorSeed {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct RebuildParams { pub struct RebuildParams {
#[arg(help = "help.arg.package-id")] #[arg(help = "help.arg.package-id")]
pub id: PackageId, pub id: PackageId,

View File

@@ -58,7 +58,8 @@ impl ValueParserFactory for SshPubKey {
} }
} }
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SshKeyResponse { pub struct SshKeyResponse {
pub alg: String, pub alg: String,
@@ -115,15 +116,16 @@ pub fn ssh<C: Context>() -> ParentHandler<C> {
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct AddParams { pub struct SshAddParams {
#[arg(help = "help.arg.ssh-public-key")] #[arg(help = "help.arg.ssh-public-key")]
key: SshPubKey, key: SshPubKey,
} }
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn add(ctx: RpcContext, AddParams { key }: AddParams) -> Result<SshKeyResponse, Error> { pub async fn add(ctx: RpcContext, SshAddParams { key }: SshAddParams) -> Result<SshKeyResponse, Error> {
let mut key = WithTimeData::new(key); let mut key = WithTimeData::new(key);
let fingerprint = InternedString::intern(key.0.fingerprint_md5()); let fingerprint = InternedString::intern(key.0.fingerprint_md5());
let (keys, res) = ctx let (keys, res) = ctx
@@ -150,9 +152,10 @@ pub async fn add(ctx: RpcContext, AddParams { key }: AddParams) -> Result<SshKey
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")] #[command(rename_all = "kebab-case")]
pub struct DeleteParams { pub struct SshDeleteParams {
#[arg(help = "help.arg.ssh-fingerprint")] #[arg(help = "help.arg.ssh-fingerprint")]
#[ts(type = "string")] #[ts(type = "string")]
fingerprint: InternedString, fingerprint: InternedString,
@@ -161,7 +164,7 @@ pub struct DeleteParams {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn remove( pub async fn remove(
ctx: RpcContext, ctx: RpcContext,
DeleteParams { fingerprint }: DeleteParams, SshDeleteParams { fingerprint }: SshDeleteParams,
) -> Result<(), Error> { ) -> Result<(), Error> {
let keys = ctx let keys = ctx
.db .db

View File

@@ -191,7 +191,9 @@ pub async fn governor(
Ok(GovernorInfo { current, available }) Ok(GovernorInfo { current, available })
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct TimeInfo { pub struct TimeInfo {
now: String, now: String,
uptime: u64, uptime: u64,
@@ -331,6 +333,7 @@ pub struct MetricLeaf<T> {
} }
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, TS)] #[derive(Clone, Copy, Debug, PartialEq, PartialOrd, TS)]
#[ts(type = "{ value: string, unit: string }")]
pub struct Celsius(f64); pub struct Celsius(f64);
impl fmt::Display for Celsius { impl fmt::Display for Celsius {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@@ -359,6 +362,7 @@ impl<'de> Deserialize<'de> for Celsius {
} }
} }
#[derive(Clone, Debug, PartialEq, PartialOrd, TS)] #[derive(Clone, Debug, PartialEq, PartialOrd, TS)]
#[ts(type = "{ value: string, unit: string }")]
pub struct Percentage(f64); pub struct Percentage(f64);
impl Serialize for Percentage { impl Serialize for Percentage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -385,6 +389,7 @@ impl<'de> Deserialize<'de> for Percentage {
} }
#[derive(Clone, Debug, TS)] #[derive(Clone, Debug, TS)]
#[ts(type = "{ value: string, unit: string }")]
pub struct MebiBytes(pub f64); pub struct MebiBytes(pub f64);
impl Serialize for MebiBytes { impl Serialize for MebiBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -411,6 +416,7 @@ impl<'de> Deserialize<'de> for MebiBytes {
} }
#[derive(Clone, Debug, PartialEq, PartialOrd, TS)] #[derive(Clone, Debug, PartialEq, PartialOrd, TS)]
#[ts(type = "{ value: string, unit: string }")]
pub struct GigaBytes(f64); pub struct GigaBytes(f64);
impl Serialize for GigaBytes { impl Serialize for GigaBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -490,6 +496,7 @@ pub async fn metrics(ctx: RpcContext) -> Result<Metrics, Error> {
#[derive(Deserialize, Serialize, Clone, Debug, TS)] #[derive(Deserialize, Serialize, Clone, Debug, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct MetricsFollowResponse { pub struct MetricsFollowResponse {
pub guid: Guid, pub guid: Guid,
pub metrics: Metrics, pub metrics: Metrics,
@@ -1211,6 +1218,7 @@ pub async fn set_keyboard(ctx: RpcContext, options: KeyboardOptions) -> Result<(
} }
#[derive(Debug, Clone, Deserialize, Serialize, TS, Parser)] #[derive(Debug, Clone, Deserialize, Serialize, TS, Parser)]
#[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SetLanguageParams { pub struct SetLanguageParams {
#[arg(help = "help.arg.language-code")] #[arg(help = "help.arg.language-code")]

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type AddPrivateDomainParams = { fqdn: string; gateway: GatewayId }

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AcmeProvider } from './AcmeProvider'
import type { GatewayId } from './GatewayId'
export type AddPublicDomainParams = {
fqdn: string
acme: AcmeProvider | null
gateway: GatewayId
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageBackupInfo } from './PackageBackupInfo'
import type { PackageId } from './PackageId'
export type BackupInfo = {
version: string
timestamp: string | null
packageBackups: { [key: PackageId]: PackageBackupInfo }
}

View File

@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BackupTargetId } from './BackupTargetId'
import type { PackageId } from './PackageId'
import type { PasswordType } from './PasswordType'
export type BackupParams = {
targetId: BackupTargetId
oldPassword: PasswordType | null
packageIds: Array<PackageId> | null
password: PasswordType
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageBackupReport } from './PackageBackupReport'
import type { PackageId } from './PackageId'
import type { ServerBackupReport } from './ServerBackupReport'
export type BackupReport = {
server: ServerBackupReport
packages: { [key: PackageId]: PackageBackupReport }
}

View File

@@ -0,0 +1,17 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CifsBackupTarget } from './CifsBackupTarget'
import type { StartOsRecoveryInfo } from './StartOsRecoveryInfo'
export type BackupTarget =
| {
type: 'disk'
vendor: string | null
model: string | null
logicalname: string
label: string | null
capacity: number
used: number | null
startOs: { [key: string]: StartOsRecoveryInfo }
guid: string | null
}
| ({ type: 'cifs' } & CifsBackupTarget)

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type BackupTargetId = string

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from './PackageId'
export type CancelInstallParams = { id: PackageId }

View File

@@ -1,3 +1,3 @@
// 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.
export type Celsius = number export type Celsius = { value: string; unit: string }

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CifsAddParams = {
hostname: string
path: string
username: string
password: string | null
}

View File

@@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { StartOsRecoveryInfo } from './StartOsRecoveryInfo'
export type CifsBackupTarget = {
hostname: string
path: string
username: string
mountable: boolean
startOs: { [key: string]: StartOsRecoveryInfo }
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BackupTargetId } from './BackupTargetId'
export type CifsRemoveParams = { id: BackupTargetId }

View File

@@ -0,0 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BackupTargetId } from './BackupTargetId'
export type CifsUpdateParams = {
id: BackupTargetId
hostname: string
path: string
username: string
password: string | null
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from './PackageId'
import type { ReplayId } from './ReplayId'
export type ClearTaskParams = {
packageId: PackageId
replayId: ReplayId
force: boolean
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from './PackageId'
export type ControlParams = { id: PackageId }

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActionId } from './ActionId'
import type { PackageId } from './PackageId'
export type EffectsRunActionParams = {
packageId?: PackageId
actionId: ActionId
input: any
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type ForgetGatewayParams = { gateway: GatewayId }

View File

@@ -1,3 +1,3 @@
// 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.
export type GigaBytes = number export type GigaBytes = { value: string; unit: string }

View File

@@ -1,10 +1,15 @@
// 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 { Bindings } from './Bindings' import type { Bindings } from './Bindings'
import type { GatewayId } from './GatewayId' import type { GatewayId } from './GatewayId'
import type { PortForward } from './PortForward'
import type { PublicDomainConfig } from './PublicDomainConfig' import type { PublicDomainConfig } from './PublicDomainConfig'
export type Host = { export type Host = {
bindings: Bindings bindings: Bindings
publicDomains: { [key: string]: PublicDomainConfig } publicDomains: { [key: string]: PublicDomainConfig }
privateDomains: { [key: string]: Array<GatewayId> } privateDomains: { [key: string]: Array<GatewayId> }
/**
* COMPUTED: port forwarding rules needed on gateways for public addresses to work.
*/
portForwards: Array<PortForward>
} }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Hostname = string

View File

@@ -5,6 +5,7 @@ import type { PackageId } from './PackageId'
export type HostnameMetadata = export type HostnameMetadata =
| { kind: 'ipv4'; gateway: GatewayId } | { kind: 'ipv4'; gateway: GatewayId }
| { kind: 'ipv6'; gateway: GatewayId; scopeId: number } | { kind: 'ipv6'; gateway: GatewayId; scopeId: number }
| { kind: 'mdns'; gateways: Array<GatewayId> }
| { kind: 'private-domain'; gateways: Array<GatewayId> } | { kind: 'private-domain'; gateways: Array<GatewayId> }
| { kind: 'public-domain'; gateway: GatewayId } | { kind: 'public-domain'; gateway: GatewayId }
| { kind: 'plugin'; package: PackageId } | { kind: 'plugin'; package: PackageId }

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BackupTargetId } from './BackupTargetId'
export type InfoParams = {
targetId: BackupTargetId
serverId: string
password: string
}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AcmeProvider } from './AcmeProvider'
export type InitAcmeParams = { provider: AcmeProvider; contact: Array<string> }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type KillParams = { ids: Array<string> }

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ListNotificationParams = {
before: number | null
limit: number | null
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LogEntry = { timestamp: string; message: string; bootId: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Guid } from './Guid'
export type LogFollowResponse = { startCursor: string | null; guid: Guid }

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LogEntry } from './LogEntry'
export type LogResponse = {
entries: Array<LogEntry>
startCursor: string | null
endCursor: string | null
}

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LogsParams = {
limit?: number
cursor?: string
boot?: number | string
before: boolean
}

View File

@@ -1,3 +1,3 @@
// 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.
export type MebiBytes = number export type MebiBytes = { value: string; unit: string }

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Guid } from './Guid'
import type { Metrics } from './Metrics'
export type MetricsFollowResponse = { guid: Guid; metrics: Metrics }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModifyNotificationBeforeParams = { before: number }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModifyNotificationParams = { ids: number[] }

View File

@@ -0,0 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NotificationLevel } from './NotificationLevel'
import type { PackageId } from './PackageId'
export type Notification = {
packageId: PackageId | null
createdAt: string
code: number
level: NotificationLevel
title: string
message: string
data: any
seen: boolean
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NotificationLevel = 'success' | 'info' | 'warning' | 'error'

View File

@@ -0,0 +1,15 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NotificationLevel } from './NotificationLevel'
import type { PackageId } from './PackageId'
export type NotificationWithId = {
id: number
packageId: PackageId | null
createdAt: string
code: number
level: NotificationLevel
title: string
message: string
data: any
seen: boolean
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Version } from './Version'
export type PackageBackupInfo = {
title: string
version: Version
osVersion: string
timestamp: string
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PackageBackupReport = { error: string | null }

View File

@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { StartOsRecoveryInfo } from './StartOsRecoveryInfo'
export type PartitionInfo = {
logicalname: string
label: string | null
capacity: number
used: number | null
startOs: { [key: string]: StartOsRecoveryInfo }
guid: string | null
}

View File

@@ -1,3 +1,3 @@
// 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.
export type Percentage = number export type Percentage = { value: string; unit: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type PortForward = { src: string; dst: string; gateway: GatewayId }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type QueryDnsParams = { fqdn: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackageId } from './PackageId'
export type RebuildParams = { id: PackageId }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AcmeProvider } from './AcmeProvider'
export type RemoveAcmeParams = { provider: AcmeProvider }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RemoveDomainParams = { fqdn: string }

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GatewayId } from './GatewayId'
export type RenameGatewayParams = { id: GatewayId; name: string }

View File

@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PasswordType } from './PasswordType'
export type ResetPasswordParams = {
oldPassword: PasswordType | null
newPassword: PasswordType | null
}

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BackupTargetId } from './BackupTargetId'
import type { PackageId } from './PackageId'
export type RestorePackageParams = {
ids: Array<PackageId>
targetId: BackupTargetId
password: string
}

View File

@@ -1,9 +1,11 @@
// 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 { ActionId } from './ActionId' import type { ActionId } from './ActionId'
import type { Guid } from './Guid'
import type { PackageId } from './PackageId' import type { PackageId } from './PackageId'
export type RunActionParams = { export type RunActionParams = {
packageId?: PackageId packageId: PackageId
eventId: Guid | null
actionId: ActionId actionId: ActionId
input: any input?: any
} }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ServerBackupReport = { attempted: boolean; error: string | null }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetCountryParams = { country: string }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetLanguageParams = { language: string }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetStaticDnsParams = { servers: Array<string> | null }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetWifiEnabledParams = { enabled: boolean }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SideloadParams = {}

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Guid } from './Guid'
export type SideloadResponse = { upload: Guid; progress: Guid }

View File

@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* So a signal strength is a number between 0-100, I want the null option to be 0 since there is no signal
*/
export type SignalStrength = number

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SshPubKey } from './SshPubKey'
export type SshAddParams = { key: SshPubKey }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SshDeleteParams = { fingerprint: string }

Some files were not shown because too many files have changed in this diff Show More