Files
start-os/sdk/base/lib/dependencies/dependencies.ts
Matt Hill add01ebc68 Gateways, domains, and new service interface (#3001)
* add support for inbound proxies

* backend changes

* fix file type

* proxy -> tunnel, implement backend apis

* wip start-tunneld

* add domains and gateways, remove routers, fix docs links

* dont show hidden actions

* show and test dns

* edit instead of chnage acme and change gateway

* refactor: domains page

* refactor: gateways page

* domains and acme refactor

* certificate authorities

* refactor public/private gateways

* fix fe types

* domains mostly finished

* refactor: add file control to form service

* add ip util to sdk

* domains api + migration

* start service interface page, WIP

* different options for clearnet domains

* refactor: styles for interfaces page

* minor

* better placeholder for no addresses

* start sorting addresses

* best address logic

* comments

* fix unnecessary export

* MVP of service interface page

* domains preferred

* fix: address comments

* only translations left

* wip: start-tunnel & fix build

* forms for adding domain, rework things based on new ideas

* fix: dns testing

* public domain, max width, descriptions for dns

* nix StartOS domains, implement public and private domains at interface scope

* restart tor instead of reset

* better icon for restart tor

* dns

* fix sort functions for public and private domains

* with todos

* update types

* clean up tech debt, bump dependencies

* revert to ts-rs v9

* fix all types

* fix dns form

* add missing translations

* it builds

* fix: comments (#3009)

* fix: comments

* undo default

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix: refactor legacy components (#3010)

* fix: comments

* fix: refactor legacy components

* remove default again

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* more translations

* wip

* fix deadlock

* coukd work

* simple renaming

* placeholder for empty service interfaces table

* honor hidden form values

* remove logs

* reason instead of description

* fix dns

* misc fixes

* implement toggling gateways for service interface

* fix showing dns records

* move status column in service list

* remove unnecessary truthy check

* refactor: refactor forms components and remove legacy Taiga UI package (#3012)

* handle wh file uploads

* wip: debugging tor

* socks5 proxy working

* refactor: fix multiple comments (#3013)

* refactor: fix multiple comments

* styling changes, add documentation to sidebar

* translations for dns page

* refactor: subtle colors

* rearrange service page

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix file_stream and remove non-terminating test

* clean  up logs

* support for sccache

* fix gha sccache

* more marketplace translations

* install wizard clarity

* stub hostnameInfo in migration

* fix address info after setup, fix styling on SI page, new 040 release notes

* remove tor logs from os

* misc fixes

* reset tor still not functioning...

* update ts

* minor styling and wording

* chore: some fixes (#3015)

* fix gateway renames

* different handling for public domains

* styling fixes

* whole navbar should not be clickable on service show page

* timeout getState request

* remove links from changelog

* misc fixes from pairing

* use custom name for gateway in more places

* fix dns parsing

* closes #3003

* closes #2999

* chore: some fixes (#3017)

* small copy change

* revert hardcoded error for testing

* dont require port forward if gateway is public

* use old wan ip when not available

* fix .const hanging on undefined

* fix test

* fix doc test

* fix renames

* update deps

* allow specifying dependency metadata directly

* temporarily make dependencies not cliackable in marketplace listings

* fix socks bind

* fix test

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: waterplea <alexander@inkin.ru>
2025-09-10 03:43:51 +00:00

233 lines
7.7 KiB
TypeScript

import { ExtendedVersion, VersionRange } from "../exver"
import {
PackageId,
HealthCheckId,
DependencyRequirement,
CheckDependenciesResult,
} from "../types"
import { Effects } from "../Effects"
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
infoFor: (packageId: DependencyId) => {
requirement: DependencyRequirement
result: CheckDependenciesResult
}
installedSatisfied: (packageId: DependencyId) => boolean
installedVersionSatisfied: (packageId: DependencyId) => boolean
runningSatisfied: (packageId: DependencyId) => boolean
tasksSatisfied: (packageId: DependencyId) => boolean
healthCheckSatisfied: (
packageId: DependencyId,
healthCheckId: HealthCheckId,
) => boolean
satisfied: () => boolean
throwIfInstalledNotSatisfied: (packageId: DependencyId) => null
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => null
throwIfRunningNotSatisfied: (packageId: DependencyId) => null
throwIfTasksNotSatisfied: (packageId: DependencyId) => null
throwIfHealthNotSatisfied: (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => null
throwIfNotSatisfied: (packageId?: DependencyId) => null
}
export async function checkDependencies<
DependencyId extends PackageId = PackageId,
>(
effects: Effects,
packageIds?: DependencyId[],
): Promise<CheckDependencies<DependencyId>> {
let [dependencies, results] = await Promise.all([
effects.getDependencies(),
effects.checkDependencies({
packageIds,
}),
])
if (packageIds) {
dependencies = dependencies.filter((d) =>
(packageIds as PackageId[]).includes(d.id),
)
}
const infoFor = (packageId: DependencyId) => {
const dependencyRequirement = dependencies.find((d) => d.id === packageId)
const dependencyResult = results.find((d) => d.packageId === packageId)
if (!dependencyRequirement || !dependencyResult) {
throw new Error(`Unknown DependencyId ${packageId}`)
}
return { requirement: dependencyRequirement, result: dependencyResult }
}
const installedSatisfied = (packageId: DependencyId) =>
!!infoFor(packageId).result.installedVersion
const installedVersionSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
return (
!!dep.result.installedVersion &&
ExtendedVersion.parse(dep.result.installedVersion).satisfies(
VersionRange.parse(dep.requirement.versionRange),
)
)
}
const runningSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
return dep.requirement.kind !== "running" || dep.result.isRunning
}
const tasksSatisfied = (packageId: DependencyId) =>
Object.entries(infoFor(packageId).result.tasks).filter(
([_, t]) => t?.active && t.task.severity === "critical",
).length === 0
const healthCheckSatisfied = (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => {
const dep = infoFor(packageId)
if (
healthCheckId &&
(dep.requirement.kind !== "running" ||
!dep.requirement.healthChecks.includes(healthCheckId))
) {
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
}
const errors =
dep.requirement.kind === "running"
? dep.requirement.healthChecks
.map((id) => [id, dep.result.healthChecks[id] ?? null] as const)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res?.result !== "success")
: []
return errors.length === 0
}
const pkgSatisfied = (packageId: DependencyId) =>
installedSatisfied(packageId) &&
installedVersionSatisfied(packageId) &&
runningSatisfied(packageId) &&
tasksSatisfied(packageId) &&
healthCheckSatisfied(packageId)
const satisfied = (packageId?: DependencyId) =>
packageId
? pkgSatisfied(packageId)
: dependencies.every((d) => pkgSatisfied(d.id as DependencyId))
const throwIfInstalledNotSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
return null
}
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
if (!dep.result.installedVersion) {
throw new Error(`${dep.result.title || packageId} is not installed`)
}
if (
![dep.result.installedVersion, ...dep.result.satisfies].find((v) =>
ExtendedVersion.parse(v).satisfies(
VersionRange.parse(dep.requirement.versionRange),
),
)
) {
throw new Error(
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
)
}
return null
}
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
throw new Error(`${dep.result.title || packageId} is not running`)
}
return null
}
const throwIfTasksNotSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
const reqs = Object.entries(dep.result.tasks)
.filter(([_, t]) => t?.active && t.task.severity === "critical")
.map(([id, _]) => id)
if (reqs.length) {
throw new Error(
`The following action requests have not been fulfilled: ${reqs.join(", ")}`,
)
}
return null
}
const throwIfHealthNotSatisfied = (
packageId: DependencyId,
healthCheckId?: HealthCheckId,
) => {
const dep = infoFor(packageId)
if (
healthCheckId &&
(dep.requirement.kind !== "running" ||
!dep.requirement.healthChecks.includes(healthCheckId))
) {
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
}
const errors =
dep.requirement.kind === "running"
? dep.requirement.healthChecks
.map((id) => [id, dep.result.healthChecks[id] ?? null] as const)
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
.filter(([_, res]) => res?.result !== "success")
: []
if (errors.length) {
throw new Error(
errors
.map(([id, e]) =>
e
? `Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`
: `Health Check ${id} of ${dep.result.title} does not exist`,
)
.join("; "),
)
}
return null
}
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
throwIfInstalledNotSatisfied(packageId)
throwIfInstalledVersionNotSatisfied(packageId)
throwIfRunningNotSatisfied(packageId)
throwIfTasksNotSatisfied(packageId)
throwIfHealthNotSatisfied(packageId)
return null
}
const throwIfNotSatisfied = (packageId?: DependencyId) =>
packageId
? throwIfPkgNotSatisfied(packageId)
: (() => {
const err = dependencies.flatMap((d) => {
try {
throwIfPkgNotSatisfied(d.id as DependencyId)
} catch (e) {
if (e instanceof Error) return [e.message]
throw e
}
return []
})
if (err.length) {
throw new Error(err.join("; "))
}
return null
})()
return {
infoFor,
installedSatisfied,
installedVersionSatisfied,
runningSatisfied,
tasksSatisfied,
healthCheckSatisfied,
satisfied,
throwIfInstalledNotSatisfied,
throwIfInstalledVersionNotSatisfied,
throwIfRunningNotSatisfied,
throwIfTasksNotSatisfied,
throwIfHealthNotSatisfied,
throwIfNotSatisfied,
}
}