mok ux, autofill device and pf forms, docss for st, docs for start-sdk

This commit is contained in:
Matt Hill
2026-03-12 14:15:45 -06:00
parent 50004da782
commit 0fa069126b
21 changed files with 1147 additions and 154 deletions

View File

@@ -519,32 +519,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
.or_not_found("certificate in chain")?;
println!("📝 Root CA:");
print!("{cert}\n");
println!(concat!(
"To access your Web URL securely, trust your Root CA (displayed above) on your client device(s):\n",
" - MacOS\n",
" 1. Open the Terminal app\n",
" 2. Type or copy/paste the following command (**DO NOT** click Enter/Return yet): pbpaste > ~/Desktop/tunnel-ca.crt\n",
" 3. Copy your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
" 4. Back in Terminal, click Enter/Return. tunnel-ca.crt is saved to your Desktop\n",
" 5. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Mac\n",
" - Linux\n",
" 1. Open gedit, nano, or any editor\n",
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
" 3. Name the file tunnel-ca.crt and save as plaintext\n",
" 4. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Debian+%252F+Ubuntu\n",
" - Windows\n",
" 1. Open the Notepad app\n",
" 2. Copy/paste your Root CA (including -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----)\n",
" 3. Name the file tunnel-ca.crt and save as plaintext\n",
" 4. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Windows\n",
" - Android/Graphene\n",
" 1. Send the tunnel-ca.crt file (created above) to yourself\n",
" 2. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=Android+%252F+Graphene\n",
" - iOS\n",
" 1. Send the tunnel-ca.crt file (created above) to yourself\n",
" 2. Complete by trusting your Root CA: https://docs.start9.com/start-os/0.4.0.x/user-manual/trust-ca.html?platform=iOS\n",
));
println!("Follow instructions to trust your Root CA (recommended): https://docs.start9.com/start-tunnel/installing/index.html#trust-your-root-ca");
return Ok(());
}

422
sdk/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,422 @@
# SDK Architecture
The Start SDK is split into two npm packages that form a layered architecture: **base** provides the foundational types, ABI contract, and effects interface; **package** builds on base to provide the developer-facing SDK facade.
```
┌─────────────────────────────────────────────────────────────┐
│ package/ (@start9labs/start-sdk) │
│ Developer-facing facade, daemon management, health checks, │
│ backup system, file helpers, triggers, subcontainers │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ base/ (@start9labs/start-sdk-base) │ │
│ │ ABI, Effects, OS bindings, actions/input builders, │ │
│ │ ExVer parser, interfaces, dependencies, S9pk, utils │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ ▲
│ Effects calls (RPC) │ Callbacks
▼ │
┌─────────────────────────────────────────────────────────────┐
│ StartOS Runtime (Rust supervisor) │
│ Executes effects, manages containers, networking, storage │
└─────────────────────────────────────────────────────────────┘
```
The SDK follows [Semantic Versioning](https://semver.org/) within the `0.4.0-beta.*` pre-release series. The SDK version tracks independently from the StartOS release versions.
## Base Package (`base/`)
The base package is a self-contained library of types, interfaces, and low-level builders. It has no dependency on the package layer and can be used independently when only type definitions or validation are needed.
### OS Bindings (`base/lib/osBindings/`)
~325 auto-generated TypeScript files defining every type exchanged between the SDK and the StartOS runtime. These cover the full surface area of the system: manifests, actions, health checks, service interfaces, bind parameters, dependency requirements, alerts, SSL, domains, SMTP, networking, images, and more.
All bindings are re-exported through `base/lib/osBindings/index.ts`.
Key types include:
- `Manifest` — The full service package manifest as understood by the OS
- `ActionMetadata` — Describes an action's name, description, visibility, and availability
- `BindParams` — Port binding configuration (protocol, hostId, internal port)
- `ServiceInterface` — A network endpoint exported to users
- `DependencyRequirement` — Version range and health check requirements for a dependency
- `SetHealth` — Health check result reporting
- `HostnameInfo` / `Host` — Hostname and host metadata
### ABI and Core Types (`base/lib/types.ts`)
Defines the Application Binary Interface — the contract every service package must fulfill:
```typescript
namespace ExpectedExports {
main // Start the service daemon(s)
init // Initialize on install/update/restore
uninit // Clean up on uninstall/update/shutdown
manifest // Service metadata
actions // User-invocable operations
createBackup // Export service data
}
```
Also defines foundational types used throughout the SDK:
- `Daemon` / `DaemonReturned` — Running process handles with `wait()` and `term()`
- `CommandType` — Shell string, argv array, or `UseEntrypoint`
- `ServiceInterfaceType``'ui' | 'api' | 'p2p'`
- `SmtpValue` — SMTP server configuration
- `KnownError` — Structured user-facing errors
- `DependsOn` — Package-to-health-check dependency mapping
- `PathMaker`, `MaybePromise`, `DeepPartial`, `DeepReadonly` — Utility types
### Effects Interface (`base/lib/Effects.ts`)
The bridge between TypeScript service code and the StartOS runtime. Every runtime capability is accessed through an `Effects` object passed to lifecycle hooks.
Effects are organized by subsystem:
| Subsystem | Methods | Purpose |
|-----------|---------|---------|
| **Action** | `export`, `clear`, `getInput`, `run`, `createTask`, `clearTasks` | Register and invoke user actions |
| **Control** | `restart`, `shutdown`, `getStatus`, `setMainStatus` | Service lifecycle control |
| **Dependency** | `setDependencies`, `getDependencies`, `checkDependencies`, `mount`, `getInstalledPackages`, `getServiceManifest` | Inter-service dependency management |
| **Health** | `setHealth` | Report health check results |
| **Subcontainer** | `createFs`, `destroyFs` | Container filesystem management |
| **Networking** | `bind`, `getServicePortForward`, `clearBindings`, `getHostInfo`, `getContainerIp`, `getOsIp`, `getOutboundGateway` | Port binding and network info |
| **Interfaces** | `exportServiceInterface`, `getServiceInterface`, `listServiceInterfaces`, `clearServiceInterfaces` | Service endpoint management |
| **Plugin** | `plugin.url.register`, `plugin.url.exportUrl`, `plugin.url.clearUrls` | Plugin system hooks |
| **SSL** | `getSslCertificate`, `getSslKey` | TLS certificate management |
| **System** | `getSystemSmtp`, `setDataVersion`, `getDataVersion` | System-wide configuration |
Effects also support reactive callbacks: many methods accept an optional `callback` parameter that the runtime invokes when the underlying value changes, enabling the reactive subscription patterns (`const()`, `watch()`, etc.).
### Action and Input System (`base/lib/actions/`)
#### Actions (`setupActions.ts`)
The `Action` class defines user-invocable operations. Two factory methods:
- `Action.withInput(id, metadata, inputSpec, prefill, execute)` — Action with a validated form
- `Action.withoutInput(id, metadata, execute)` — Action without user input
`Actions` is a typed map accumulated via `.addAction()` chaining.
#### Input Specification (`actions/input/`)
A builder-pattern system for declaring validated form inputs:
```
inputSpec/
├── builder/
│ ├── inputSpec.ts — InputSpec.of() entry point
│ ├── value.ts — Value class (individual form fields)
│ ├── list.ts — List builder (arrays of values)
│ └── variants.ts — Variants/Union builder (conditional fields)
├── inputSpecTypes.ts — Type definitions for all field types
└── inputSpecConstants.ts — Pre-built specs (SMTP, etc.)
```
Supported field types via `Value`:
- `text`, `textarea`, `number` — Text and numeric input
- `toggle` — Boolean switch
- `select`, `multiselect` — Single/multi-choice dropdown
- `list` — Repeatable array of sub-values
- `color`, `datetime` — Specialized pickers
- `object` — Nested sub-form
- `union` / `dynamicUnion` — Conditional fields based on a discriminator
### Dependencies (`base/lib/dependencies/`)
- `setupDependencies.ts` — Declare what the service depends on (package IDs, version ranges, health checks)
- `dependencies.ts` — Runtime dependency checking via `checkDependencies()`
### Interfaces (`base/lib/interfaces/`)
Network interface declaration and port binding:
- `setupInterfaces.ts` — Top-level `setupServiceInterfaces()` function
- `Host.ts``MultiHost` class for binding ports and exporting interfaces. A single MultiHost can bind a port and export multiple interfaces (e.g. a primary UI and admin UI on the same port with different paths)
- `ServiceInterfaceBuilder.ts` — Builder for constructing `ServiceInterface` objects with name, type, description, scheme overrides, username, path, and query params
- `setupExportedUrls.ts` — URL plugin support for exporting URLs to other services
### Initialization (`base/lib/inits/`)
- `setupInit.ts` — Compose init scripts that run on install, update, restore, or boot
- `setupUninit.ts` — Compose uninit scripts that run on uninstall, update, or shutdown
- `setupOnInit` / `setupOnUninit` — Register callbacks for specific init/uninit events
Init scripts receive a `kind` parameter (`'install' | 'update' | 'restore' | null`) so they can branch logic based on the initialization context.
### Extended Versioning (`base/lib/exver/`)
A PEG parser-based versioning system that extends semver:
- **`Version`** — Standard semantic version (`1.2.3-beta.1`)
- **`ExtendedVersion` (ExVer)** — Adds an optional flavor prefix and a downstream version: `#flavor:upstream:downstream`
- **`VersionRange`** — Boolean expressions over version comparisons (`>=1.0.0 && <2.0.0 || =3.0.0`)
The parser is generated from `exver.pegjs` via Peggy and emitted as `exver.ts`.
ExVer separates upstream project versions from StartOS wrapper versions, allowing the package maintainer's versioning to evolve independently from the upstream software.
### S9pk Format (`base/lib/s9pk/`)
Parser and verifier for `.s9pk` service package archives:
- `S9pk` class — Deserialize and inspect package contents
- Merkle archive support for cryptographic verification of package integrity
- Methods: `deserialize()`, `icon()`, `license()`, etc.
### Utilities (`base/lib/util/`)
~28 utility modules including:
**Reactive subscription wrappers** — Each wraps an Effects callback-based method into a consistent reactive API:
- `Watchable` — Base class providing `const()`, `once()`, `watch()`, `onChange()`, `waitFor()`
- `GetContainerIp`, `GetStatus`, `GetSystemSmtp`, `GetOutboundGateway`, `GetSslCertificate`, `GetHostInfo`, `GetServiceManifest` — Typed wrappers for specific Effects methods
**General utilities:**
- `deepEqual` / `deepMerge` — Deep object comparison and merging
- `patterns` — Hostname regex, port validators
- `splitCommand` — Parse shell command strings into argv arrays
- `Drop` — RAII-style cleanup utility
- `graph` — Dependency graph utilities
## Package Layer (`package/`)
The package layer provides the developer-facing API. It re-exports everything from base and adds higher-level abstractions.
### StartSdk Facade (`package/lib/StartSdk.ts`)
The primary entry point for service developers. Constructed via a builder chain:
```typescript
const sdk = StartSdk.of()
.withManifest(manifest)
.build(true)
```
The `.build()` method returns an object containing the entire SDK surface area, organized by concern:
| Category | Members | Purpose |
|----------|---------|---------|
| **Manifest** | `manifest`, `volumes` | Access manifest data and volume paths |
| **Actions** | `Action.withInput`, `Action.withoutInput`, `Actions`, `action.run`, `action.createTask`, `action.createOwnTask`, `action.clearTask` | Define and manage user actions |
| **Daemons** | `Daemons.of`, `Daemon.of`, `setupMain` | Configure service processes |
| **Health** | `healthCheck.checkPortListening`, `.checkWebUrl`, `.runHealthScript` | Built-in health checks |
| **Interfaces** | `createInterface`, `MultiHost.of`, `setupInterfaces`, `serviceInterface.*` | Network endpoint management |
| **Backups** | `setupBackups`, `Backups.ofVolumes`, `Backups.ofSyncs`, `Backups.withOptions` | Backup configuration |
| **Dependencies** | `setupDependencies`, `checkDependencies` | Dependency declaration and verification |
| **Init/Uninit** | `setupInit`, `setupUninit`, `setupOnInit`, `setupOnUninit` | Lifecycle hooks |
| **Containers** | `SubContainer.of`, `SubContainer.withTemp`, `Mounts.of` | Container execution with mounts |
| **Forms** | `InputSpec.of`, `Value`, `Variants`, `List` | Form input builders |
| **Triggers** | `trigger.defaultTrigger`, `.cooldownTrigger`, `.changeOnFirstSuccess`, `.successFailure` | Health check polling strategies |
| **Reactive** | `getContainerIp`, `getStatus`, `getSystemSmtp`, `getOutboundGateway`, `getSslCertificate`, `getServiceManifest` | Subscription-based data access |
| **Plugins** | `plugin.url.register`, `plugin.url.exportUrl` | Plugin system (gated by manifest `plugins` field) |
| **Effects** | `restart`, `shutdown`, `setHealth`, `mount`, `clearBindings`, ... | Direct effect wrappers |
| **Utilities** | `nullIfEmpty`, `useEntrypoint`, `patterns`, `setDataVersion`, `getDataVersion` | Misc helpers |
### Daemon Management (`package/lib/mainFn/`)
The daemon subsystem manages long-running processes:
```
mainFn/
├── Daemons.ts — Multi-daemon topology builder
├── Daemon.ts — Single daemon wrapper
├── HealthDaemon.ts — Health check executor
├── CommandController.ts — Command execution controller
├── Mounts.ts — Volume/asset/dependency mount builder
├── Oneshot.ts — One-time startup commands
└── index.ts — setupMain() entry point
```
**Daemons** is a builder that accumulates process definitions:
```typescript
sdk.Daemons.of(effects)
.addDaemon('db', { /* command, ready probe, mounts */ })
.addDaemon('app', { requires: ['db'], /* ... */ })
.addHealthCheck('primary', { /* ... */ })
```
Features:
- Startup ordering via `requires` (dependency graph between daemons)
- Ready probes (wait for a daemon to be ready before starting dependents)
- Graceful shutdown with configurable signals and timeouts
- One-shot commands that run before daemons start
**Mounts** declares what to attach to a container:
```typescript
sdk.Mounts.of()
.mountVolume('main', '/data')
.mountAssets('scripts', '/scripts')
.mountDependency('bitcoind', 'main', '/bitcoin-data', { readonly: true })
.mountBackup('/backup')
```
### Health Checks (`package/lib/health/`)
```
health/
├── HealthCheck.ts — Periodic probe with startup grace period
└── checkFns/
├── checkPortListening.ts — TCP port connectivity check
├── checkWebUrl.ts — HTTP(S) status code check
└── runHealthScript.ts — Script exit code check
```
Health checks are paired with **triggers** that control polling behavior:
- `defaultTrigger` — Fixed interval (e.g. every 30s)
- `cooldownTrigger` — Wait longer after failures
- `changeOnFirstSuccess` — Rapid polling until first success, then slow down
- `successFailure` — Different intervals for healthy vs unhealthy states
### Backup System (`package/lib/backup/`)
```
backup/
├── setupBackups.ts — Top-level setup function
└── Backups.ts — Volume selection and rsync options
```
Three builder patterns:
- `Backups.ofVolumes('main', 'data')` — Back up entire volumes
- `Backups.ofSyncs([{ dataPath, backupPath }])` — Custom sync pairs
- `Backups.withOptions({ exclude: ['cache/'] })` — Rsync options
### File Helpers (`package/lib/util/fileHelper.ts`)
Type-safe configuration file management:
```typescript
const configFile = FileHelper.yaml(effects, sdk.volumes.main.path('config.yml'), {
port: 8080,
debug: false,
})
// Reactive reading
const config = await configFile.read.const(effects)
// Partial merge
await configFile.merge({ debug: true })
// Full write
await configFile.write({ port: 9090, debug: true })
```
Supported formats: JSON, YAML, TOML, INI, ENV, and custom parsers.
### Subcontainers (`package/lib/util/SubContainer.ts`)
Execute commands in isolated container environments:
```typescript
// Long-lived subcontainer
const container = await sdk.SubContainer.of(effects, { imageId: 'main' }, mounts, 'app')
// One-shot execution
await sdk.SubContainer.withTemp(effects, { imageId: 'main' }, mounts, 'migrate', async (c) => {
await c.exec(['run-migrations'])
})
```
### Manifest Building (`package/lib/manifest/`)
```typescript
const manifest = setupManifest({
id: 'my-service',
title: 'My Service',
license: 'MIT',
description: { short: '...', long: '...' },
images: { main: { source: { dockerTag: 'myimage:1.0' } } },
volumes: { main: {} },
dependencies: {},
// ...
})
export default buildManifest(manifest)
```
`buildManifest()` finalizes the manifest with the current SDK version, OS version compatibility, and migration version ranges.
### Versioning (`package/lib/version/`)
Helpers for data version management during migrations:
```typescript
await sdk.setDataVersion(effects, '1.2.0:0')
const version = await sdk.getDataVersion(effects)
```
Used in init scripts to track which migration version the service's data has been brought to.
### Internationalization (`package/lib/i18n/`)
```typescript
const t = setupI18n({ en_US: enStrings, es_ES: esStrings })
const greeting = t('hello', { name: 'World' }) // "Hello, World!" or "Hola, World!"
```
Supports locale fallback and Intl-based formatting.
### Triggers (`package/lib/trigger/`)
Polling strategy functions that determine when health checks run:
```typescript
sdk.trigger.defaultTrigger({ timeout: 30_000 })
sdk.trigger.cooldownTrigger({ timeout: 30_000, cooldown: 60_000 })
sdk.trigger.changeOnFirstSuccess({ first: 5_000, then: 30_000 })
sdk.trigger.successFailure({ success: 60_000, failure: 10_000 })
```
## Build Pipeline
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed build instructions, make targets, and development workflow.
At a high level: Peggy generates the ExVer parser, TypeScript compiles both packages in strict mode (base to `baseDist/`, package to `dist/`), hand-written `.js`/`.d.ts` pairs are copied into the output, and `node_modules` are bundled for self-contained distribution.
## Data Flow
A typical service package lifecycle:
```
1. INSTALL / UPDATE / RESTORE
├── init({ effects, kind })
│ ├── Version migrations (if update)
│ ├── setupDependencies()
│ ├── setupInterfaces() → bind ports, export interfaces
│ └── Actions registration → export actions to OS
2. MAIN
│ setupMain() → Daemons.of(effects)
│ ├── Oneshots run first
│ ├── Daemons start in dependency order
│ ├── Health checks begin polling
│ └── Service runs until shutdown/restart
3. SHUTDOWN / UNINSTALL / UPDATE
│ uninit({ effects, target })
│ └── Version down-migrations (if needed)
4. BACKUP (user-triggered)
createBackup({ effects })
└── rsync volumes to backup location
```
## Key Design Patterns
### Builder Pattern
Most SDK APIs use immutable builder chains: `Daemons.of().addDaemon().addHealthCheck()`, `Mounts.of().mountVolume().mountAssets()`, `Actions.of().addAction()`. This provides type accumulation — each chained call narrows the type to reflect what has been configured.
### Effects as Capability System
All runtime interactions go through the `Effects` object rather than direct system calls. This makes the runtime boundary explicit, enables the OS to mediate all side effects, and makes service code testable by providing mock effects.
### Reactive Subscriptions
The `Watchable` base class provides a consistent API for values that can change over time:
- `const(effects)` — Read once; if the value changes, triggers a retry of the enclosing context
- `once()` — Read once without reactivity
- `watch()` — Async generator yielding on each change
- `onChange(callback)` — Invoke callback on each change
- `waitFor(predicate)` — Block until a condition is met
### Type-safe Manifest Threading
The manifest type flows through the entire SDK via generics. When you call `StartSdk.of().withManifest(manifest)`, the manifest's volume names, image IDs, dependency IDs, and plugin list become available as type constraints throughout all subsequent API calls. For example, `Mounts.of().mountVolume()` only accepts volume names declared in the manifest.

109
sdk/CHANGELOG.md Normal file
View File

@@ -0,0 +1,109 @@
# Changelog
## 0.4.0-beta.59 — StartOS v0.4.0-alpha.20 (2026-03-06)
### Added
- Support for preferred external ports besides 443
- Bridge filter kind on service interfaces
### Fixed
- Merge version ranges when adding existing package signer
- Task fix for action task system
## 0.4.0-beta.56 — StartOS v0.4.0-alpha.19 (2026-02-02)
### Added
- `getOutboundGateway` effect and SDK wrapper
- Improved service version migration and data version handling
- `zod-deep-partial` integration with `partialValidator` on `InputSpec`
- SMTP rework with improved provider variants and system SMTP spec
### Changed
- Migrated from `ts-matches` to `zod` across all TypeScript packages
- Builder-style `InputSpec` API with prefill plumbing
- Split `row_actions` into `remove_action` and `overflow_actions` for URL plugins
### Fixed
- Scoped public domain to single binding and return single port check
- Preserved `z` namespace types for SDK consumers
- `--arch` flag falls back to emulation when native image unavailable
## 0.4.0-beta.54 — StartOS v0.4.0-alpha.18 (2026-01-27)
### Added
- Device info RPC
- Hardware acceleration and NVIDIA card support on nonfree images
### Changed
- Consolidated setup flow
- Improved SDK abort handling and `InputSpec` filtering
## 0.4.0-beta.49 — StartOS v0.4.0-alpha.17 (2026-01-10)
### Added
- JSDoc comments on all consumer-facing APIs
- StartTunnel random subnet support
- Port 80 to 5443 tunnel mapping
### Fixed
- `EffectCreator` type corrections
- Allow multiple equal signs in ENV `FileHelper` values
- Miscellaneous alpha.16 follow-up fixes
## 0.4.0-beta.45 — StartOS v0.4.0-alpha.16 (2025-12-18)
### Added
- `map` and `eq` on `getServiceInterface` watcher
- Flavor-aware version range handling
### Changed
- Refactored `StatusInfo` types
- Improved shutdown ordering for daemons
- Improved StartTunnel validation and garbage collection
## 0.4.0-beta.43 — StartOS v0.4.0-alpha.15 (2025-11-26)
### Fixed
- Minor bugfixes for alpha.14
## 0.4.0-beta.42 — StartOS v0.4.0-alpha.14 (2025-11-20)
### Fixed
- Bugfixes for alpha.13
## 0.4.0-beta.41 — StartOS v0.4.0-alpha.13 (2025-11-15)
### Fixed
- Bugfixes for alpha.12
## 0.4.0-beta.40 — StartOS v0.4.0-alpha.12 (2025-11-07)
### Added
- StartTunnel integration
- Configurable `textarea` rows in `InputSpec`
## 0.4.0-beta.39 — StartOS v0.4.0-alpha.11 (2025-09-24)
### Added
- Gateway limiting for StartTunnel
- Improved copy UX around Tor SSL
### Changed
- SDK type updates and internal improvements

View File

@@ -6,3 +6,10 @@ TypeScript SDK for packaging services for StartOS (`@start9labs/start-sdk`).
- `base/` — Core types, ABI definitions, effects interface (`@start9labs/start-sdk-base`)
- `package/` — Full SDK for package developers, re-exports base
## Releasing
When bumping the SDK version (in `package/package.json`), always update `CHANGELOG.md`:
1. Add a new version heading at the top of the file
2. Use the format: `## <sdk-version> — StartOS <os-version> (<date>)`
3. Categorize entries under `### Added`, `### Changed`, `### Fixed`, or `### Removed`

209
sdk/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,209 @@
# Contributing to Start SDK
This guide covers developing the SDK itself. If you're building a service package *using* the SDK, see the [packaging docs](https://docs.start9.com/packaging).
For contributing to the broader StartOS project, see the root [CONTRIBUTING.md](../CONTRIBUTING.md).
## Prerequisites
- **Node.js v22+** (via [nvm](https://github.com/nvm-sh/nvm) recommended)
- **npm** (ships with Node.js)
- **GNU Make**
Verify your setup:
```bash
node --version # v22.x or higher
npm --version
make --version
```
## Repository Layout
```
sdk/
├── base/ # @start9labs/start-sdk-base (core types, ABI, effects)
│ ├── lib/ # TypeScript source
│ ├── package.json
│ ├── tsconfig.json
│ └── jest.config.js
├── package/ # @start9labs/start-sdk (full developer-facing SDK)
│ ├── lib/ # TypeScript source
│ ├── package.json
│ ├── tsconfig.json
│ └── jest.config.js
├── baseDist/ # Build output for base (generated)
├── dist/ # Build output for package (generated, published to npm)
├── Makefile # Build orchestration
├── README.md
├── ARCHITECTURE.md
└── CLAUDE.md
```
## Getting Started
Install dependencies for both sub-packages:
```bash
cd sdk
make node_modules
```
This runs `npm ci` in both `base/` and `package/`.
## Building
### Full Build
```bash
make bundle
```
This runs the complete pipeline: TypeScript compilation, hand-written pair copying, node_modules bundling, formatting, and tests. Outputs land in `baseDist/` (base) and `dist/` (package).
### Individual Targets
| Target | Description |
|--------|-------------|
| `make bundle` | Full build: compile + format + test |
| `make baseDist` | Compile base package only |
| `make dist` | Compile full package (depends on base) |
| `make fmt` | Run Prettier on all `.ts` files |
| `make check` | Type-check without emitting (both packages) |
| `make clean` | Remove all build artifacts and node_modules |
### What the Build Does
1. **Peggy parser generation**`base/lib/exver/exver.pegjs` is compiled to `exver.ts` (the ExVer version parser)
2. **TypeScript compilation** — Strict mode, CommonJS output, declaration files
- `base/` compiles to `baseDist/`
- `package/` compiles to `dist/`
3. **Hand-written pair copying**`.js`/`.d.ts` files without a corresponding `.ts` source are copied into the output directories. These are manually maintained JavaScript files with hand-written type declarations.
4. **Dependency bundling**`node_modules/` is rsynced into both output directories so the published package is self-contained
5. **Formatting** — Prettier formats all TypeScript source
6. **Testing** — Jest runs both test suites
## Testing
```bash
# Run all tests (base + package)
make test
# Run base tests only
make base/test
# Run package tests only
make package/test
# Run a specific test file directly
cd base && npx jest --testPathPattern=exver
cd package && npx jest --testPathPattern=host
```
Tests use [Jest](https://jestjs.io/) with [ts-jest](https://kulshekhar.github.io/ts-jest/) for TypeScript support. Configuration is in each sub-package's `jest.config.js`.
### Test Files
Tests live alongside their subjects or in dedicated `test/` directories:
- `base/lib/test/` — ExVer parsing, input spec types, deep merge, graph utilities, type validation
- `base/lib/util/inMs.test.ts` — Time conversion utility
- `package/lib/test/` — Health checks, host binding, input spec builder
Test files use the `.test.ts` extension and are excluded from compilation via `tsconfig.json`.
## Formatting
```bash
make fmt
```
Runs Prettier with the project config (single quotes, no semicolons, trailing commas, 2-space indent). The Prettier config lives in each sub-package's `package.json`:
```json
{
"trailingComma": "all",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
```
## Type Checking
To check types without building:
```bash
make check
```
Or directly per package:
```bash
cd base && npm run check
cd package && npm run check
```
Both packages use strict TypeScript (`"strict": true`) targeting ES2021 with CommonJS module output.
## Local Development with a Service Package
To test SDK changes against a local service package without publishing to npm:
```bash
# Build and create a local npm link
make link
# In your service package directory
npm link @start9labs/start-sdk
```
This symlinks the built `dist/` into your global node_modules so your service package picks up local changes.
## Publishing
```bash
make publish
```
This builds the full bundle, then runs `npm publish --access=public --tag=latest` from `dist/`. The published package is `@start9labs/start-sdk`.
Only the `dist/` directory is published — it contains the compiled JavaScript, declaration files, bundled dependencies, and package metadata.
## Adding New Features
### Base vs Package
Decide where new code belongs:
- **`base/`** — Types, interfaces, ABI contracts, OS bindings, and low-level builders that have no dependency on the package layer. Code here should be usable independently.
- **`package/`** — Developer-facing API, convenience wrappers, runtime helpers (daemons, health checks, backups, file helpers, subcontainers). Code here imports from base and adds higher-level abstractions.
### Key Conventions
- **Builder pattern** — Most APIs use immutable builder chains (`.addDaemon()`, `.mountVolume()`, `.addAction()`). Each call returns a new type that accumulates configuration.
- **Effects boundary** — All runtime interactions go through the `Effects` interface. Never call system APIs directly.
- **Manifest type threading** — The manifest type flows through generics so that volume names, image IDs, and dependency IDs are type-constrained.
- **Re-export from package** — If you add a new export to base, also re-export it from `package/lib/index.ts` (or expose it through `StartSdk.build()`).
### Adding OS Bindings
Types in `base/lib/osBindings/` mirror Rust types from the StartOS core. When Rust types change, the corresponding TypeScript bindings need updating. These are re-exported through `base/lib/osBindings/index.ts`.
### Writing Tests
- Place test files next to the code they test, or in the `test/` directory
- Use the `.test.ts` extension
- Tests run in Node.js with ts-jest — no browser environment
## Commit Messages
Follow [Conventional Commits](https://www.conventionalcommits.org/):
```
feat(sdk): add WebSocket health check
fix(sdk): correct ExVer range parsing for pre-release versions
test(sdk): add coverage for MultiHost port binding
```
See the root [CONTRIBUTING.md](../CONTRIBUTING.md#commit-messages) for the full convention.

View File

@@ -27,23 +27,24 @@ bundle: baseDist dist | test fmt
base/lib/exver/exver.ts: base/node_modules base/lib/exver/exver.pegjs
cd base && npm run peggy
baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md base/LICENSE
baseDist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) base/package.json base/node_modules base/README.md LICENSE
(cd base && npm run tsc)
rsync -ac --include='*.js' --include='*.d.ts' --include='*/' --exclude='*' base/lib/ baseDist/
rsync -ac base/node_modules baseDist/
cp base/package.json baseDist/package.json
cp base/README.md baseDist/README.md
cp base/LICENSE baseDist/LICENSE
cp LICENSE baseDist/LICENSE
touch baseDist
dist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) package/package.json package/.npmignore package/node_modules package/README.md package/LICENSE
dist: $(PACKAGE_TS_FILES) $(BASE_TS_FILES) package/package.json package/.npmignore package/node_modules README.md LICENSE CHANGELOG.md
(cd package && npm run tsc)
rsync -ac --include='*.js' --include='*.d.ts' --include='*/' --exclude='*' base/lib/ dist/base/lib/
rsync -ac package/node_modules dist/
cp package/.npmignore dist/.npmignore
cp package/package.json dist/package.json
cp package/README.md dist/README.md
cp package/LICENSE dist/LICENSE
cp README.md dist/README.md
cp LICENSE dist/LICENSE
cp CHANGELOG.md dist/CHANGELOG.md
touch dist
full-bundle: bundle
@@ -71,7 +72,7 @@ base/node_modules: base/package-lock.json
node_modules: package/node_modules base/node_modules
publish: bundle package/package.json package/README.md package/LICENSE
publish: bundle package/package.json README.md LICENSE CHANGELOG.md
cd dist && npm publish --access=public --tag=latest
link: bundle

103
sdk/README.md Normal file
View File

@@ -0,0 +1,103 @@
# Start SDK
The Start SDK (`@start9labs/start-sdk`) is a TypeScript framework for packaging services to run on [StartOS](https://github.com/Start9Labs/start-os). It provides a strongly-typed, builder-pattern API for defining every aspect of a service package: manifests, daemons, health checks, networking interfaces, actions, backups, dependencies, configuration, and more.
## Features
- **Type-safe manifest definitions** - Declare your service's identity, metadata, images, volumes, and dependencies with full TypeScript inference.
- **Daemon management** - Define multi-process topologies with startup ordering, ready probes, and graceful shutdown via `Daemons.of().addDaemon()`.
- **Health checks** - Built-in checks for TCP port listening, HTTP(S) endpoints, and custom scripts, with configurable polling strategies (fixed interval, cooldown, adaptive).
- **Network interfaces** - Bind ports, export UI/API/P2P interfaces, and manage hostnames with MultiHost and ServiceInterfaceBuilder.
- **User actions** - Create interactive operations with validated form inputs (text, select, toggle, list, union/variants, and more) that users can trigger from the StartOS UI.
- **Backup and restore** - Rsync-based volume backups with exclude patterns and custom sync paths.
- **Dependency management** - Declare inter-service dependencies with version ranges, health check requirements, and volume mounts.
- **Configuration file helpers** - Read, write, and merge JSON, YAML, TOML, INI, and ENV files with type-safe `FileHelper`.
- **Reactive subscriptions** - Watch for changes to container IPs, SSL certificates, SMTP config, service status, and more with `const()`, `once()`, `watch()`, `onChange()`, and `waitFor()` patterns.
- **Extended versioning (ExVer)** - Flavor-aware semantic versioning with range matching, supporting independent upstream and downstream version tracking.
- **Internationalization** - Built-in i18n support with locale fallback and parameter substitution.
- **Container execution** - Run commands in subcontainers with volume mounts, environment variables, and entrypoint overrides.
- **Plugin system** - Extensible plugin architecture (e.g. `url-v0` for URL management).
## Quick Start
```typescript
import { setupManifest, buildManifest } from '@start9labs/start-sdk'
const manifest = setupManifest({
id: 'my-service',
title: 'My Service',
license: 'MIT',
// ...
})
export default buildManifest(manifest)
```
The primary entry point is the `StartSdk` facade:
```typescript
import { StartSdk } from '@start9labs/start-sdk'
import { manifest } from './manifest'
export const sdk = StartSdk.of().withManifest(manifest).build(true)
```
From there, `sdk` exposes the full toolkit:
```typescript
// Define daemons
export const main = sdk.setupMain(async ({ effects }) =>
sdk.Daemons.of(effects)
.addDaemon('primary', { /* ... */ })
)
// Define actions
export const setName = sdk.Action.withInput('set-name', /* ... */)
// Define interfaces
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
const multi = sdk.MultiHost.of(effects, 'web')
const origin = await multi.bindPort(80, { protocol: 'http' })
const ui = sdk.createInterface(effects, { name: 'Web UI', id: 'ui', /* ... */ })
return [await origin.export([ui])]
})
// Define backups
export const { createBackup, restoreBackup } = sdk.setupBackups(
async () => sdk.Backups.ofVolumes('main')
)
```
## Packages
| Package | npm | Description |
|---------|-----|-------------|
| `package/` | `@start9labs/start-sdk` | Full SDK for service developers |
| `base/` | `@start9labs/start-sdk-base` | Core types, ABI definitions, and effects interface |
## Documentation
For comprehensive packaging guides, tutorials, and API reference:
**[docs.start9.com/packaging](https://docs.start9.com/packaging)**
The packaging docs cover:
- Environment setup and prerequisites
- Project structure and conventions
- Manifest, main, interfaces, actions, and all other service modules
- File models and configuration management
- Versioning, migrations, and initialization
- Dependencies and cross-service communication
- Building and installing `.s9pk` packages
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for environment setup, building from source, testing, and development workflow.
## Architecture
See [ARCHITECTURE.md](ARCHITECTURE.md) for a detailed overview of the SDK's internal structure, module responsibilities, and data flow.
## License
MIT

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Start9 Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,18 +0,0 @@
# Start SDK
## Config Conversion
- Copy the old config json (from the getConfig.ts)
- Install the start-sdk with `npm i`
- paste the config into makeOutput.ts::oldSpecToBuilder (second param)
- Make the third param
```ts
{
StartSdk: "start-sdk/lib",
}
```
- run the script `npm run buildOutput` to make the output.ts
- Copy this whole file into startos/procedures/config/spec.ts
- Fix all the TODO

View File

@@ -1,31 +1,72 @@
import { Component } from '@angular/core'
import { Component, inject } from '@angular/core'
import { i18nPipe } from '@start9labs/shared'
import { TuiButton, TuiDialogContext, TuiIcon } from '@taiga-ui/core'
import { injectContext } from '@taiga-ui/polymorpheus'
import { StateService } from '../services/state.service'
@Component({
standalone: true,
imports: [TuiButton, TuiIcon, i18nPipe],
template: `
<div class="icon-container">
<tui-icon icon="@tui.shield-check" class="mok-icon" />
@if (!stateService.kiosk) {
<div class="animation-container">
<div class="port">
<div class="port-inner"></div>
</div>
<div class="cable">
<div class="cable-connector"></div>
<div class="cable-body"></div>
</div>
</div>
<p>
{{
'Connect a monitor and keyboard to your server before rebooting.'
| i18n
}}
</p>
} @else {
<div class="icon-container">
<tui-icon icon="@tui.monitor" class="monitor-icon" />
</div>
<p>
{{ 'Keep your monitor connected for the next reboot.' | i18n }}
</p>
}
<div class="mok-info">
<p>
{{
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers — such as those for NVIDIA GPUs — are not signed by the default distribution key. Enrolling the StartOS signing key allows your firmware to trust these modules so your hardware can be fully utilized.'
| i18n
}}
</p>
<p>
{{
'On the next boot, a blue screen (MokManager) will appear. You will have 10 seconds to select "Enroll MOK" before it dismisses.'
| i18n
}}
</p>
<p>
{{
'If you miss the window, simply reboot to try again. The blue screen will appear on every boot until the key is enrolled.'
| i18n
}}
</p>
<p class="steps-label">
{{ 'After clicking "Enroll MOK":' | i18n }}
</p>
<ol>
<li>Click "Continue"</li>
<li>
{{ 'When prompted, enter your StartOS password' | i18n }}
</li>
<li>Click "Reboot"</li>
</ol>
</div>
<h3>{{ 'Secure Boot Key Enrollment' | i18n }}</h3>
<p>
{{
'A signing key was enrolled for Secure Boot. On the next reboot, a blue screen (MokManager) will appear.'
| i18n
}}
</p>
<ol>
<li>Select "Enroll MOK"</li>
<li>Select "Continue"</li>
<li>{{ 'Enter your StartOS master password when prompted' | i18n }}</li>
<li>Select "Reboot"</li>
</ol>
<footer>
<button tuiButton (click)="context.completeWith(true)">
{{ 'Got it' | i18n }}
{{ 'Ok' | i18n }}
</button>
</footer>
`,
@@ -41,29 +82,114 @@ import { injectContext } from '@taiga-ui/polymorpheus'
margin-bottom: 1rem;
}
.mok-icon {
.monitor-icon {
width: 3rem;
height: 3rem;
color: var(--tui-status-info);
}
h3 {
margin: 0 0 0.5rem;
.animation-container {
position: relative;
width: 160px;
height: 69px;
}
.port {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 18px;
background: var(--tui-background-neutral-1);
border: 2px solid var(--tui-border-normal);
border-radius: 2px;
}
.port-inner {
position: absolute;
top: 3px;
left: 3px;
right: 3px;
bottom: 3px;
background: var(--tui-background-neutral-2);
border-radius: 1px;
}
.cable {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
animation: slide-in 2s ease-in-out 0.5s infinite;
left: 130px;
}
.cable-connector {
width: 18px;
height: 12px;
background: var(--tui-text-secondary);
border-radius: 1px;
}
.cable-body {
width: 50px;
height: 6px;
background: var(--tui-text-tertiary);
border-radius: 0 3px 3px 0;
}
@keyframes slide-in {
0% {
left: 130px;
opacity: 0;
}
5% {
left: 130px;
opacity: 1;
}
60% {
left: 32px;
opacity: 1;
}
80% {
left: 32px;
opacity: 1;
}
100% {
left: 32px;
opacity: 0;
}
}
.mok-info {
text-align: left;
margin-top: 0.5rem;
p {
margin: 0 0 0.75rem;
color: var(--tui-text-secondary);
}
.steps-label {
margin-bottom: 0.25rem;
font-weight: 500;
color: var(--tui-text-primary);
}
ol {
margin: 0 0 1rem;
padding-left: 1.5rem;
li {
margin-bottom: 0.25rem;
}
}
}
p {
margin: 0 0 1rem;
color: var(--tui-text-secondary);
}
ol {
text-align: left;
margin: 0 0 1.5rem;
padding-left: 1.5rem;
li {
margin-bottom: 0.25rem;
}
}
footer {
@@ -74,4 +200,5 @@ import { injectContext } from '@taiga-ui/polymorpheus'
})
export class MokEnrollmentDialog {
protected readonly context = injectContext<TuiDialogContext<boolean>>()
readonly stateService = inject(StateService)
}

View File

@@ -1,10 +1,10 @@
import {
AfterViewInit,
Component,
DOCUMENT,
ElementRef,
inject,
ViewChild,
DOCUMENT,
} from '@angular/core'
import {
DialogService,
@@ -12,17 +12,17 @@ import {
ErrorService,
i18nPipe,
} from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { TuiIcon, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiAvatar } from '@taiga-ui/kit'
import { TuiCardLarge, TuiCell, TuiHeader } from '@taiga-ui/layout'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { DocumentationComponent } from '../components/documentation.component'
import { MatrixComponent } from '../components/matrix.component'
import { MokEnrollmentDialog } from '../components/mok-enrollment.dialog'
import { RemoveMediaDialog } from '../components/remove-media.dialog'
import { T } from '@start9labs/start-sdk'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { ApiService } from '../services/api.service'
import { StateService } from '../services/state.service'
@Component({
template: `
@@ -50,7 +50,7 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
} @else {
<!-- Step: Download Address Info (non-kiosk only) -->
@if (!stateService.kiosk) {
<button tuiCell="l" [disabled]="downloaded" (click)="download()">
<button tuiCell="l" (click)="download()">
<tui-avatar appearance="secondary" src="@tui.download" />
<div tuiTitle>
{{ 'Download Address Info' | i18n }}
@@ -67,12 +67,12 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
</button>
}
<!-- Step: Remove USB Media (when restart needed) -->
<!-- Step: Restart flow -->
@if (result.needsRestart) {
<button
tuiCell="l"
[class.disabled]="!stateService.kiosk && !downloaded"
[disabled]="(!stateService.kiosk && !downloaded) || usbRemoved"
[disabled]="!stateService.kiosk && !downloaded"
(click)="removeMedia()"
>
<tui-avatar appearance="secondary" src="@tui.usb" />
@@ -90,11 +90,39 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
}
</button>
<!-- Step: Secure Boot Enrollment (when MOK enrolled) -->
@if (stateService.mokEnrolled) {
<button
tuiCell="l"
[class.disabled]="!usbRemoved"
[disabled]="!usbRemoved"
(click)="acknowledgeMok()"
>
<tui-avatar appearance="secondary" src="@tui.shield-check" />
<div tuiTitle>
{{ 'Secure Boot Enrollment' | i18n }}
<div tuiSubtitle>
{{
'Prepare for Secure Boot key enrollment on the next reboot'
| i18n
}}
</div>
</div>
@if (mokAcknowledged) {
<tui-icon icon="@tui.circle-check" class="g-positive" />
}
</button>
}
<!-- Step: Restart Server -->
<button
tuiCell="l"
[class.disabled]="!usbRemoved"
[disabled]="!usbRemoved || rebooted || rebooting"
[class.disabled]="
!usbRemoved || (stateService.mokEnrolled && !mokAcknowledged)
"
[disabled]="
!usbRemoved || (stateService.mokEnrolled && !mokAcknowledged)
"
(click)="reboot()"
>
<tui-avatar appearance="secondary" src="@tui.rotate-cw" />
@@ -116,6 +144,16 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
<tui-icon icon="@tui.circle-check" class="g-positive" />
}
</button>
} @else if (stateService.kiosk) {
<button tuiCell="l" (click)="exitKiosk()">
<tui-avatar appearance="secondary" src="@tui.log-in" />
<div tuiTitle>
{{ 'Continue to Login' | i18n }}
<div tuiSubtitle>
{{ 'Proceed to the StartOS login screen' | i18n }}
</div>
</div>
</button>
}
<!-- Step: Open Local Address (non-kiosk only) -->
@@ -137,22 +175,6 @@ import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
}
<!-- Step: Continue to Login (kiosk only) -->
@if (stateService.kiosk) {
<button
tuiCell="l"
[class.disabled]="result.needsRestart && !rebooted"
[disabled]="result.needsRestart && !rebooted"
(click)="exitKiosk()"
>
<tui-avatar appearance="secondary" src="@tui.log-in" />
<div tuiTitle>
{{ 'Continue to Login' | i18n }}
<div tuiSubtitle>
{{ 'Proceed to the StartOS login screen' | i18n }}
</div>
</div>
</button>
}
}
</section>
`,
@@ -198,6 +220,7 @@ export default class SuccessPage implements AfterViewInit {
lanAddress = ''
downloaded = false
usbRemoved = false
mokAcknowledged = false
rebooting = false
rebooted = false
@@ -212,8 +235,6 @@ export default class SuccessPage implements AfterViewInit {
}
download() {
if (this.downloaded) return
const lanElem = this.document.getElementById('lan-addr')
if (lanElem) lanElem.innerHTML = this.lanAddress
@@ -243,6 +264,19 @@ export default class SuccessPage implements AfterViewInit {
})
}
acknowledgeMok() {
this.dialogs
.openComponent<boolean>(new PolymorpheusComponent(MokEnrollmentDialog), {
label: 'Secure Boot',
size: 'm',
dismissible: false,
closeable: false,
})
.subscribe(() => {
this.mokAcknowledged = true
})
}
exitKiosk() {
this.api.exit()
}
@@ -252,6 +286,8 @@ export default class SuccessPage implements AfterViewInit {
}
async reboot() {
if (this.rebooting || this.rebooted) return
this.rebooting = true
try {
@@ -276,20 +312,6 @@ export default class SuccessPage implements AfterViewInit {
await this.api.exit()
}
}
if (this.stateService.mokEnrolled && this.result.needsRestart) {
this.dialogs
.openComponent<boolean>(
new PolymorpheusComponent(MokEnrollmentDialog),
{
label: 'Secure Boot',
size: 's',
dismissible: false,
closeable: true,
},
)
.subscribe()
}
} catch (e: any) {
this.errorService.handleError(e)
}

View File

@@ -116,7 +116,7 @@ export class MockApiService extends ApiService {
return {
guid: 'mock-data-guid',
attach: !params.dataDrive.wipe,
mokEnrolled: false,
mokEnrolled: true,
}
}

View File

@@ -713,4 +713,13 @@ export default {
790: 'Ein Signaturschlüssel wurde für Secure Boot registriert. Beim nächsten Neustart erscheint ein blauer Bildschirm (MokManager).',
791: 'Geben Sie Ihr StartOS-Master-Passwort ein, wenn Sie dazu aufgefordert werden',
792: 'Verstanden',
793: 'Secure-Boot-Registrierung',
794: 'Vorbereitung auf die Secure-Boot-Schlüsselregistrierung beim nächsten Neustart',
795: 'Schließen Sie vor dem Neustart einen Monitor und eine Tastatur an Ihren Server an.',
796: 'Lassen Sie Ihren Monitor für den nächsten Neustart angeschlossen.',
797: 'Beim nächsten Start erscheint ein blauer Bildschirm (MokManager). Sie haben 10 Sekunden, um "Enroll MOK" auszuwählen, bevor er verschwindet.',
798: 'Falls Sie das Zeitfenster verpassen, starten Sie einfach neu und versuchen Sie es erneut. Der blaue Bildschirm erscheint bei jedem Start, bis der Schlüssel registriert ist.',
799: 'Nach Klick auf "Enroll MOK":',
800: 'Geben Sie bei Aufforderung Ihr StartOS-Passwort ein',
801: 'Ihr System hat Secure Boot aktiviert, was erfordert, dass alle Kernel-Module mit einem vertrauenswürdigen Schlüssel signiert sind. Einige Hardware-Treiber \u2014 wie die für NVIDIA-GPUs \u2014 sind nicht mit dem Standard-Distributionsschlüssel signiert. Die Registrierung des StartOS-Signaturschlüssels ermöglicht es Ihrer Firmware, diesen Modulen zu vertrauen, damit Ihre Hardware vollständig genutzt werden kann.',
} satisfies i18n

View File

@@ -714,4 +714,13 @@ export const ENGLISH: Record<string, number> = {
'A signing key was enrolled for Secure Boot. On the next reboot, a blue screen (MokManager) will appear.': 790,
'Enter your StartOS master password when prompted': 791,
'Got it': 792,
'Secure Boot Enrollment': 793,
'Prepare for Secure Boot key enrollment on the next reboot': 794,
'Connect a monitor and keyboard to your server before rebooting.': 795,
'Keep your monitor connected for the next reboot.': 796,
'On the next boot, a blue screen (MokManager) will appear. You will have 10 seconds to select "Enroll MOK" before it dismisses.': 797,
'If you miss the window, simply reboot to try again. The blue screen will appear on every boot until the key is enrolled.': 798,
'After clicking "Enroll MOK":': 799,
'When prompted, enter your StartOS password': 800,
'Your system has Secure Boot enabled, which requires all kernel modules to be signed with a trusted key. Some hardware drivers \u2014 such as those for NVIDIA GPUs \u2014 are not signed by the default distribution key. Enrolling the StartOS signing key allows your firmware to trust these modules so your hardware can be fully utilized.': 801,
}

View File

@@ -713,4 +713,13 @@ export default {
790: 'Se registró una clave de firma para Secure Boot. En el próximo reinicio, aparecerá una pantalla azul (MokManager).',
791: 'Ingrese su contraseña maestra de StartOS cuando se le solicite',
792: 'Entendido',
793: 'Registro de Secure Boot',
794: 'Prepararse para el registro de clave de Secure Boot en el próximo reinicio',
795: 'Conecte un monitor y un teclado a su servidor antes de reiniciar.',
796: 'Mantenga su monitor conectado para el próximo reinicio.',
797: 'En el próximo arranque, aparecerá una pantalla azul (MokManager). Tendrá 10 segundos para seleccionar "Enroll MOK" antes de que desaparezca.',
798: 'Si pierde la oportunidad, simplemente reinicie para intentar de nuevo. La pantalla azul aparecerá en cada arranque hasta que se registre la clave.',
799: 'Después de hacer clic en "Enroll MOK":',
800: 'Cuando se le solicite, ingrese su contraseña de StartOS',
801: 'Su sistema tiene Secure Boot habilitado, lo que requiere que todos los módulos del kernel estén firmados con una clave de confianza. Algunos controladores de hardware \u2014 como los de las GPU NVIDIA \u2014 no están firmados con la clave de distribución predeterminada. Registrar la clave de firma de StartOS permite que su firmware confíe en estos módulos para que su hardware pueda utilizarse completamente.',
} satisfies i18n

View File

@@ -709,8 +709,17 @@ export default {
786: 'Automatique',
787: 'Trafic sortant',
788: 'Utiliser la passerelle',
789: "Enregistrement de la clé Secure Boot",
790: "Une clé de signature a été enregistrée pour Secure Boot. Au prochain redémarrage, un écran bleu (MokManager) apparaîtra.",
789: 'Enregistrement de la clé Secure Boot',
790: 'Une clé de signature a été enregistrée pour Secure Boot. Au prochain redémarrage, un écran bleu (MokManager) apparaîtra.',
791: 'Entrez votre mot de passe principal StartOS lorsque vous y êtes invité',
792: 'Compris',
793: 'Enregistrement Secure Boot',
794: "Préparez-vous à l'enregistrement de la clé Secure Boot au prochain redémarrage",
795: 'Connectez un moniteur et un clavier à votre serveur avant de redémarrer.',
796: 'Gardez votre moniteur connecté pour le prochain redémarrage.',
797: 'Au prochain démarrage, un écran bleu (MokManager) apparaîtra. Vous aurez 10 secondes pour sélectionner "Enroll MOK" avant qu\'il ne disparaisse.',
798: "Si vous manquez la fenêtre, redémarrez simplement pour réessayer. L'écran bleu apparaîtra à chaque démarrage jusqu'à ce que la clé soit enregistrée.",
799: 'Après avoir cliqué sur "Enroll MOK" :',
800: 'Lorsque vous y êtes invité, entrez votre mot de passe StartOS',
801: "Votre système a Secure Boot activé, ce qui exige que tous les modules du noyau soient signés avec une clé de confiance. Certains pilotes matériels \u2014 comme ceux des GPU NVIDIA \u2014 ne sont pas signés par la clé de distribution par défaut. L'enregistrement de la clé de signature StartOS permet à votre firmware de faire confiance à ces modules afin que votre matériel puisse être pleinement utilisé.",
} satisfies i18n

View File

@@ -713,4 +713,13 @@ export default {
790: 'Klucz podpisu został zarejestrowany dla Secure Boot. Przy następnym uruchomieniu pojawi się niebieski ekran (MokManager).',
791: 'Wprowadź swoje hasło główne StartOS po wyświetleniu monitu',
792: 'Rozumiem',
793: 'Rejestracja Secure Boot',
794: 'Przygotuj się do rejestracji klucza Secure Boot przy następnym uruchomieniu',
795: 'Podłącz monitor i klawiaturę do serwera przed ponownym uruchomieniem.',
796: 'Pozostaw monitor podłączony do następnego uruchomienia.',
797: 'Przy następnym uruchomieniu pojawi się niebieski ekran (MokManager). Będziesz mieć 10 sekund na wybranie "Enroll MOK", zanim zniknie.',
798: 'Jeśli przegapisz okno, po prostu uruchom ponownie i spróbuj jeszcze raz. Niebieski ekran będzie pojawiał się przy każdym uruchomieniu, dopóki klucz nie zostanie zarejestrowany.',
799: 'Po kliknięciu "Enroll MOK":',
800: 'Po wyświetleniu monitu wprowadź swoje hasło StartOS',
801: 'Twój system ma włączony Secure Boot, co wymaga, aby wszystkie moduły jądra były podpisane zaufanym kluczem. Niektóre sterowniki sprzętowe \u2014 takie jak te dla GPU NVIDIA \u2014 nie są podpisane domyślnym kluczem dystrybucji. Zarejestrowanie klucza podpisu StartOS pozwala firmware ufać tym modułom, aby sprzęt mógł być w pełni wykorzystany.',
} satisfies i18n

View File

@@ -6,7 +6,6 @@ import {
Validators,
} from '@angular/forms'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { utils } from '@start9labs/start-sdk'
import {
TUI_IS_MOBILE,
TuiAutoFocus,
@@ -121,15 +120,23 @@ export class DevicesAdd {
protected readonly context =
injectContext<TuiDialogContext<void, DeviceData>>()
private readonly autoSubnet =
!this.context.data.device && this.context.data.subnets().length === 1
? this.context.data.subnets().at(0)
: undefined
protected readonly form = inject(NonNullableFormBuilder).group({
name: [this.context.data.device?.name || '', Validators.required],
subnet: [
this.context.data.device?.subnet,
this.context.data.device?.subnet ?? this.autoSubnet,
[Validators.required, subnetValidator],
],
ip: [
this.context.data.device?.ip || '',
[Validators.required, Validators.pattern(utils.Patterns.ipv4.regex)],
this.context.data.device?.ip ||
(this.autoSubnet ? getIp(this.autoSubnet) : ''),
this.autoSubnet
? [Validators.required, ipInSubnetValidator(this.autoSubnet.range)]
: [],
],
})

View File

@@ -22,10 +22,10 @@ import {
TuiCheckbox,
TuiChevron,
TuiDataListWrapper,
TuiElasticContainer,
TuiFieldErrorPipe,
TuiInputNumber,
TuiSelect,
TuiElasticContainer,
} from '@taiga-ui/kit'
import { TuiForm } from '@taiga-ui/layout'
import { injectContext, PolymorpheusComponent } from '@taiga-ui/polymorpheus'
@@ -167,7 +167,12 @@ export class PortForwardsAdd {
protected readonly form = inject(NonNullableFormBuilder).group({
label: ['', Validators.required],
externalip: ['', Validators.required],
externalip: [
this.context.data.ips().length === 1
? (this.context.data.ips().at(0) ?? '')
: '',
Validators.required,
],
externalport: [null as number | null, Validators.required],
device: [null as MappedDevice | null, Validators.required],
internalport: [null as number | null, Validators.required],

View File

@@ -16,6 +16,6 @@ export interface MappedForward {
}
export interface PortForwardsData {
readonly ips: Signal<any>
readonly ips: Signal<readonly string[]>
readonly devices: Signal<readonly MappedDevice[]>
}