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>
This commit is contained in:
Matt Hill
2025-09-09 21:43:51 -06:00
committed by GitHub
parent 1cc9a1a30b
commit add01ebc68
537 changed files with 19940 additions and 20551 deletions

View File

@@ -27,6 +27,7 @@ import {
/** Used to reach out from the pure js runtime */
export type Effects = {
readonly eventId: string | null
child: (name: string) => Effects
constRetry?: () => void
isInContext: boolean

View File

@@ -1,8 +1,7 @@
import { ExtractInputSpecType, InputSpec, LazyBuild } from "./inputSpec"
import { InputSpec, LazyBuild } from "./inputSpec"
import { List } from "./list"
import { UnionRes, UnionResStaticValidatedAs, Variants } from "./variants"
import {
FilePath,
Pattern,
RandomString,
ValueSpec,
@@ -27,6 +26,12 @@ import {
} from "ts-matches"
import { DeepPartial } from "../../../types"
export const fileInfoParser = object({
path: string,
commitment: object({ hash: string, size: number }),
})
export type FileInfo = typeof fileInfoParser._TYPE
type AsRequired<T, Required extends boolean> = Required extends true
? T
: T | null
@@ -891,47 +896,54 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
}
}, spec.validator)
}
// static file<Store, Required extends boolean>(a: {
// name: string
// description?: string | null
// extensions: string[]
// required: Required
// }) {
// const buildValue = {
// type: "file" as const,
// description: null,
// warning: null,
// ...a,
// }
// return new Value<AsRequired<FilePath, Required>, Store>(
// () => ({
// ...buildValue,
// }),
// asRequiredParser(object({ filePath: string }), a),
// )
// }
// static dynamicFile<Store>(
// a: LazyBuild<
// Store,
// {
// name: string
// description?: string | null
// warning?: string | null
// extensions: string[]
// required: boolean
// }
// >,
// ) {
// return new Value<FilePath | null, Store>(
// async (options) => ({
// type: "file" as const,
// description: null,
// warning: null,
// ...(await a(options)),
// }),
// object({ filePath: string }).nullable(),
// )
// }
static file<Required extends boolean>(a: {
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}) {
const buildValue = {
type: "file" as const,
description: null,
warning: null,
...a,
}
return new Value<AsRequired<FileInfo, Required>>(
() => ({
spec: {
...buildValue,
},
validator: asRequiredParser(fileInfoParser, a),
}),
asRequiredParser(fileInfoParser, a),
)
}
static dynamicFile<Required extends boolean>(
a: LazyBuild<{
name: string
description?: string | null
warning?: string | null
extensions: string[]
required: Required
}>,
) {
return new Value<AsRequired<FileInfo, Required>, FileInfo | null>(
async (options) => {
const spec = {
type: "file" as const,
description: null,
warning: null,
...(await a(options)),
}
return {
spec,
validator: asRequiredParser(fileInfoParser, spec),
}
},
fileInfoParser.nullable(),
)
}
/**
* @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented.
* @example

View File

@@ -66,9 +66,6 @@ export type ValueSpecTextarea = {
immutable: boolean
}
export type FilePath = {
filePath: string
}
export type ValueSpecNumber = {
type: "number"
min: number | null

View File

@@ -5,9 +5,11 @@ import { once } from "../util"
import { InitScript } from "../inits"
import { Parser } from "ts-matches"
type MaybeInputSpec<Type> = {} extends Type ? null : InputSpec<Type>
export type Run<A extends Record<string, any>> = (options: {
effects: T.Effects
input: A
spec: T.inputSpecTypes.InputSpec
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
export type GetInput<A extends Record<string, any>> = (options: {
effects: T.Effects
@@ -47,11 +49,14 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
implements ActionInfo<Id, Type>
{
readonly _INPUT: Type = null as any as Type
private cachedParser?: Parser<unknown, Type>
private prevInputSpec: Record<
string,
{ spec: T.inputSpecTypes.InputSpec; validator: Parser<unknown, Type> }
> = {}
private constructor(
readonly id: Id,
private readonly metadataFn: MaybeFn<T.ActionMetadata>,
private readonly inputSpec: InputSpec<Type>,
private readonly inputSpec: MaybeInputSpec<Type>,
private readonly getInputFn: GetInput<Type>,
private readonly runFn: Run<Type>,
) {}
@@ -81,7 +86,7 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
return new Action(
id,
mapMaybeFn(metadata, (m) => ({ ...m, hasInput: false })),
InputSpec.of({}),
null,
async () => null,
run,
)
@@ -100,10 +105,15 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
return metadata
}
async getInput(options: { effects: T.Effects }): Promise<T.ActionInput> {
const built = await this.inputSpec.build(options)
this.cachedParser = built.validator
let spec = {}
if (this.inputSpec) {
const built = await this.inputSpec.build(options)
this.prevInputSpec[options.effects.eventId!] = built
spec = built.spec
}
return {
spec: built.spec,
eventId: options.effects.eventId!,
spec,
value:
((await this.getInputFn(options)) as
| Record<string, unknown>
@@ -115,15 +125,23 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
effects: T.Effects
input: Type
}): Promise<T.ActionResult | null> {
const parser =
this.cachedParser ?? (await this.inputSpec.build(options)).validator
let spec = {}
if (this.inputSpec) {
const prevInputSpec = this.prevInputSpec[options.effects.eventId!]
if (!prevInputSpec) {
throw new Error(
`getActionInput has not been called for EventID ${options.effects.eventId}`,
)
}
options.input = prevInputSpec.validator.unsafeCast(options.input)
spec = prevInputSpec.spec
}
return (
(await this.runFn({
effects: options.effects,
input: this.cachedParser
? this.cachedParser.unsafeCast(options.input)
: options.input,
})) || null
input: options.input,
spec,
})) ?? null
)
}
}

View File

@@ -77,7 +77,7 @@ export async function checkDependencies<
}
const tasksSatisfied = (packageId: DependencyId) =>
Object.entries(infoFor(packageId).result.tasks).filter(
([_, t]) => t.active && t.task.severity === "critical",
([_, t]) => t?.active && t.task.severity === "critical",
).length === 0
const healthCheckSatisfied = (
packageId: DependencyId,
@@ -146,7 +146,7 @@ export async function checkDependencies<
const throwIfTasksNotSatisfied = (packageId: DependencyId) => {
const dep = infoFor(packageId)
const reqs = Object.entries(dep.result.tasks)
.filter(([_, t]) => t.active && t.task.severity === "critical")
.filter(([_, t]) => t?.active && t.task.severity === "critical")
.map(([id, _]) => id)
if (reqs.length) {
throw new Error(

View File

@@ -2,7 +2,10 @@ import * as T from "../types"
import { once } from "../util"
export type RequiredDependenciesOf<Manifest extends T.SDKManifest> = {
[K in keyof Manifest["dependencies"]]: Manifest["dependencies"][K]["optional"] extends false
[K in keyof Manifest["dependencies"]]: Exclude<
Manifest["dependencies"][K],
undefined
>["optional"] extends false
? K
: never
}[keyof Manifest["dependencies"]]

View File

@@ -1,6 +1,8 @@
// 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 ActionInput = {
eventId: Guid
spec: Record<string, unknown>
value: Record<string, unknown> | 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.
export type ForgetInterfaceParams = { interface: string }
export type AddTunnelParams = { name: string; config: string; public: boolean }

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 { GatewayId } from "./GatewayId"
export type BindingGatewaySetEnabledParams = {
internalPort: number
gateway: GatewayId
enabled: boolean | null
}

View File

@@ -1,8 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PathOrUrl } from "./PathOrUrl"
import type { MetadataSrc } from "./MetadataSrc"
export type DepInfo = {
description: string | null
optional: boolean
s9pk: PathOrUrl | null
}
} & MetadataSrc

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkInterfaceSetInboundParams = {
interface: string
inbound: boolean | null
export type DnsSettings = {
dhcpServers: Array<string>
staticServers: Array<string> | null
}

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 DomainSettings = { gateway: GatewayId }

View File

@@ -1,4 +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 ProcedureId = { procedureId: Guid }
export type EventId = { eventId: Guid }

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.
export type UnsetInboundParams = { interface: string }
export type GatewayId = string

View File

@@ -1,12 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BindInfo } from "./BindInfo"
import type { DomainConfig } from "./DomainConfig"
import type { HostnameInfo } from "./HostnameInfo"
import type { PublicDomainConfig } from "./PublicDomainConfig"
export type Host = {
bindings: { [key: number]: BindInfo }
onions: string[]
domains: { [key: string]: DomainConfig }
publicDomains: { [key: string]: PublicDomainConfig }
privateDomains: Array<string>
/**
* COMPUTED: NetService::update
*/

View File

@@ -1,11 +0,0 @@
// 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 HostAddress =
| { kind: "onion"; address: string }
| {
kind: "domain"
address: string
public: boolean
acme: AcmeProvider | null
}

View File

@@ -3,10 +3,5 @@ import type { IpHostname } from "./IpHostname"
import type { OnionHostname } from "./OnionHostname"
export type HostnameInfo =
| {
kind: "ip"
networkInterfaceId: string
public: boolean
hostname: IpHostname
}
| { kind: "ip"; gatewayId: string; public: boolean; hostname: IpHostname }
| { kind: "onion"; hostname: OnionHostname }

View File

@@ -17,8 +17,7 @@ export type IpHostname =
}
| {
kind: "domain"
domain: string
subdomain: string | null
value: string
port: number | null
sslPort: number | null
}

View File

@@ -6,6 +6,8 @@ export type IpInfo = {
scopeId: number
deviceType: NetworkInterfaceType | null
subnets: string[]
lanIp: string[]
wanIp: string | null
ntpServers: string[]
dnsServers: string[]
}

View File

@@ -1,4 +1,3 @@
// 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 LoginParams = { password: PasswordType | null; ephemeral: boolean }
export type LoginParams = { password: string; ephemeral: 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 { PathOrUrl } from "./PathOrUrl"
export type Metadata = { title: string; icon: PathOrUrl }

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 { Metadata } from "./Metadata"
import type { PathOrUrl } from "./PathOrUrl"
export type MetadataSrc = { metadata: Metadata } | { s9pk: PathOrUrl | null }

View File

@@ -1,7 +1,9 @@
// 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 NetInfo = {
public: boolean
privateDisabled: Array<GatewayId>
publicEnabled: Array<GatewayId>
assignedPort: number | null
assignedSslPort: number | null
}

View File

@@ -1,6 +1,8 @@
// 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 { AcmeSettings } from "./AcmeSettings"
import type { DnsSettings } from "./DnsSettings"
import type { GatewayId } from "./GatewayId"
import type { Host } from "./Host"
import type { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
import type { WifiInfo } from "./WifiInfo"
@@ -8,6 +10,7 @@ import type { WifiInfo } from "./WifiInfo"
export type NetworkInfo = {
wifi: WifiInfo
host: Host
networkInterfaces: { [key: string]: NetworkInterfaceInfo }
gateways: { [key: GatewayId]: NetworkInterfaceInfo }
acme: { [key: AcmeProvider]: AcmeSettings }
dns: DnsSettings
}

View File

@@ -2,7 +2,8 @@
import type { IpInfo } from "./IpInfo"
export type NetworkInterfaceInfo = {
inbound: boolean | null
outbound: boolean | null
name: string | null
public: boolean | null
secure: boolean | null
ipInfo: IpInfo | null
}

View File

@@ -1,6 +1,7 @@
// 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 BindingSetPublicParams = {
internalPort: number
export type NetworkInterfaceSetPublicParams = {
gateway: GatewayId
public: boolean | null
}

View File

@@ -6,6 +6,7 @@ import type { DataUrl } from "./DataUrl"
import type { Hosts } from "./Hosts"
import type { MainStatus } from "./MainStatus"
import type { PackageState } from "./PackageState"
import type { ReplayId } from "./ReplayId"
import type { ServiceInterface } from "./ServiceInterface"
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
import type { TaskEntry } from "./TaskEntry"
@@ -20,7 +21,7 @@ export type PackageDataEntry = {
lastBackup: string | null
currentDependencies: CurrentDependencies
actions: { [key: ActionId]: ActionMetadata }
tasks: { [key: string]: TaskEntry }
tasks: { [key: ReplayId]: TaskEntry }
serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterface }
hosts: Hosts
storeExposedDependents: string[]

View File

@@ -1,4 +1,8 @@
// 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 DomainConfig = { public: boolean; acme: AcmeProvider | null }
export type PublicDomainConfig = {
gateway: GatewayId
acme: AcmeProvider | null
}

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 RemoveTunnelParams = { id: GatewayId }

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

@@ -1,9 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Session } from "./Session"
export type Sessions = {
[key: string]: {
loggedIn: string
lastActive: string
userAgent: string | null
}
}
export type Sessions = { [key: string]: Session }

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 UnsetPublicParams = { gateway: GatewayId }

View File

@@ -17,6 +17,7 @@ export { AddPackageParams } from "./AddPackageParams"
export { AddPackageToCategoryParams } from "./AddPackageToCategoryParams"
export { AddressInfo } from "./AddressInfo"
export { AddSslOptions } from "./AddSslOptions"
export { AddTunnelParams } from "./AddTunnelParams"
export { AddVersionParams } from "./AddVersionParams"
export { Alerts } from "./Alerts"
export { Algorithm } from "./Algorithm"
@@ -33,7 +34,7 @@ export { BackupTargetFS } from "./BackupTargetFS"
export { Base64 } from "./Base64"
export { BindId } from "./BindId"
export { BindInfo } from "./BindInfo"
export { BindingSetPublicParams } from "./BindingSetPublicParams"
export { BindingGatewaySetEnabledParams } from "./BindingGatewaySetEnabledParams"
export { BindOptions } from "./BindOptions"
export { BindParams } from "./BindParams"
export { Blake3Commitment } from "./Blake3Commitment"
@@ -65,17 +66,20 @@ export { DepInfo } from "./DepInfo"
export { Description } from "./Description"
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
export { DeviceFilter } from "./DeviceFilter"
export { DomainConfig } from "./DomainConfig"
export { DnsSettings } from "./DnsSettings"
export { DomainSettings } from "./DomainSettings"
export { Duration } from "./Duration"
export { EchoParams } from "./EchoParams"
export { EditSignerParams } from "./EditSignerParams"
export { EncryptedWire } from "./EncryptedWire"
export { EventId } from "./EventId"
export { ExportActionParams } from "./ExportActionParams"
export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams"
export { FileType } from "./FileType"
export { ForgetInterfaceParams } from "./ForgetInterfaceParams"
export { ForgetGatewayParams } from "./ForgetGatewayParams"
export { FullIndex } from "./FullIndex"
export { FullProgress } from "./FullProgress"
export { GatewayId } from "./GatewayId"
export { GetActionInputParams } from "./GetActionInputParams"
export { GetContainerIpParams } from "./GetContainerIpParams"
export { GetHostInfoParams } from "./GetHostInfoParams"
@@ -96,7 +100,6 @@ export { Governor } from "./Governor"
export { Guid } from "./Guid"
export { HardwareRequirements } from "./HardwareRequirements"
export { HealthCheckId } from "./HealthCheckId"
export { HostAddress } from "./HostAddress"
export { HostId } from "./HostId"
export { HostnameInfo } from "./HostnameInfo"
export { Hosts } from "./Hosts"
@@ -125,6 +128,8 @@ export { Manifest } from "./Manifest"
export { MaybeUtf8String } from "./MaybeUtf8String"
export { MebiBytes } from "./MebiBytes"
export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
export { MetadataSrc } from "./MetadataSrc"
export { Metadata } from "./Metadata"
export { MetricsCpu } from "./MetricsCpu"
export { MetricsDisk } from "./MetricsDisk"
export { MetricsGeneral } from "./MetricsGeneral"
@@ -137,7 +142,7 @@ export { NamedProgress } from "./NamedProgress"
export { NetInfo } from "./NetInfo"
export { NetworkInfo } from "./NetworkInfo"
export { NetworkInterfaceInfo } from "./NetworkInterfaceInfo"
export { NetworkInterfaceSetInboundParams } from "./NetworkInterfaceSetInboundParams"
export { NetworkInterfaceSetPublicParams } from "./NetworkInterfaceSetPublicParams"
export { NetworkInterfaceType } from "./NetworkInterfaceType"
export { OnionHostname } from "./OnionHostname"
export { OsIndex } from "./OsIndex"
@@ -155,9 +160,9 @@ export { PackageVersionInfo } from "./PackageVersionInfo"
export { PasswordType } from "./PasswordType"
export { PathOrUrl } from "./PathOrUrl"
export { Percentage } from "./Percentage"
export { ProcedureId } from "./ProcedureId"
export { Progress } from "./Progress"
export { ProgressUnits } from "./ProgressUnits"
export { PublicDomainConfig } from "./PublicDomainConfig"
export { Public } from "./Public"
export { RecoverySource } from "./RecoverySource"
export { RegistryAsset } from "./RegistryAsset"
@@ -167,7 +172,9 @@ export { RemoveAssetParams } from "./RemoveAssetParams"
export { RemoveCategoryParams } from "./RemoveCategoryParams"
export { RemovePackageFromCategoryParams } from "./RemovePackageFromCategoryParams"
export { RemovePackageParams } from "./RemovePackageParams"
export { RemoveTunnelParams } from "./RemoveTunnelParams"
export { RemoveVersionParams } from "./RemoveVersionParams"
export { RenameGatewayParams } from "./RenameGatewayParams"
export { ReplayId } from "./ReplayId"
export { RequestCommitment } from "./RequestCommitment"
export { RunActionParams } from "./RunActionParams"
@@ -203,7 +210,7 @@ export { TaskSeverity } from "./TaskSeverity"
export { TaskTrigger } from "./TaskTrigger"
export { Task } from "./Task"
export { TestSmtpParams } from "./TestSmtpParams"
export { UnsetInboundParams } from "./UnsetInboundParams"
export { UnsetPublicParams } from "./UnsetPublicParams"
export { UpdatingState } from "./UpdatingState"
export { VerifyCifsParams } from "./VerifyCifsParams"
export { VersionSignerParams } from "./VersionSignerParams"

View File

@@ -101,18 +101,20 @@ export class S9pk {
)
}
async dependencyMetadata(): Promise<Record<PackageId, DependencyMetadata>> {
async dependencyMetadata() {
return Object.fromEntries(
await Promise.all(
Object.entries(this.manifest.dependencies).map(async ([id, info]) => [
id,
{
...(await this.dependencyMetadataFor(id)),
icon: await this.dependencyIconFor(id),
description: info.description,
optional: info.optional,
},
]),
Object.entries(this.manifest.dependencies)
.filter(([_, info]) => !!info)
.map(async ([id, info]) => [
id,
{
...(await this.dependencyMetadataFor(id)),
icon: await this.dependencyIconFor(id),
description: info!.description,
optional: info!.optional,
},
]),
),
)
}

View File

@@ -46,6 +46,7 @@ type EffectsTypeChecker<T extends StringObject = Effects> = {
describe("startosTypeValidation ", () => {
test(`checking the params match`, () => {
typeEquality<EffectsTypeChecker>({
eventId: {} as never,
child: "",
isInContext: {} as never,
onLeaveContext: () => {},

View File

@@ -1,25 +0,0 @@
import { HostnameInfo } from "../types"
export function hostnameInfoToAddress(hostInfo: HostnameInfo): string {
if (hostInfo.kind === "onion") {
return `${hostInfo.hostname.value}`
}
if (hostInfo.kind !== "ip") {
throw Error("Expecting that the kind is ip.")
}
const hostname = hostInfo.hostname
if (hostname.kind === "domain") {
return `${hostname.subdomain ? `${hostname.subdomain}.` : ""}${hostname.domain}`
}
const port = hostname.sslPort || hostname.port
const portString = port ? `:${port}` : ""
if ("ipv4" === hostname.kind || "ipv6" === hostname.kind) {
return `${hostname.value}${portString}`
}
if ("local" === hostname.kind) {
return `${hostname.value}${portString}`
}
throw Error(
"Expecting to have a valid hostname kind." + JSON.stringify(hostname),
)
}

View File

@@ -3,6 +3,7 @@ import { knownProtocols } from "../interfaces/Host"
import { AddressInfo, Host, Hostname, HostnameInfo } from "../types"
import { Effects } from "../Effects"
import { DropGenerator, DropPromise } from "./Drop"
import { IPV6_LINK_LOCAL } from "./ip"
export type UrlString = string
export type HostId = string
@@ -95,9 +96,9 @@ export const addressHostToUrl = (
hostname = host.hostname.value
} else if (host.kind === "ip") {
if (host.hostname.kind === "domain") {
hostname = `${host.hostname.subdomain ? `${host.hostname.subdomain}.` : ""}${host.hostname.domain}`
hostname = host.hostname.value
} else if (host.hostname.kind === "ipv6") {
hostname = host.hostname.value.startsWith("fe80::")
hostname = IPV6_LINK_LOCAL.contains(host.hostname.value)
? `[${host.hostname.value}%${host.hostname.scopeId}]`
: `[${host.hostname.value}]`
} else {
@@ -164,7 +165,7 @@ export const filledAddress = (
addressInfo: AddressInfo,
): FilledAddressInfo => {
const toUrl = addressHostToUrl.bind(null, addressInfo)
const hostnames = host.hostnameInfo[addressInfo.internalPort]
const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? []
return {
...addressInfo,

View File

@@ -1,6 +1,7 @@
/// Currently being used
export { addressHostToUrl } from "./getServiceInterface"
export { getDefaultString } from "./getDefaultString"
export * from "./ip"
/// Not being used, but known to be browser compatible
export { GetServiceInterface, getServiceInterface } from "./getServiceInterface"
@@ -16,6 +17,5 @@ export { splitCommand } from "./splitCommand"
export { nullIfEmpty } from "./nullIfEmpty"
export { deepMerge, partialDiff } from "./deepMerge"
export { deepEqual } from "./deepEqual"
export { hostnameInfoToAddress } from "./Hostname"
export * as regexes from "./regexes"
export { stringFromStdErrOut } from "./stringFromStdErrOut"

85
sdk/base/lib/util/ip.ts Normal file
View File

@@ -0,0 +1,85 @@
export class IpAddress {
readonly octets: number[]
constructor(readonly address: string) {
if (address.includes(":")) {
this.octets = new Array(16).fill(0)
const segs = address.split(":")
let idx = 0
let octIdx = 0
while (segs[idx]) {
const num = parseInt(segs[idx], 16)
this.octets[octIdx++] = num >> 8
this.octets[octIdx++] = num & 255
idx += 1
}
const lastSegIdx = segs.length - 1
if (idx < lastSegIdx) {
idx = lastSegIdx
octIdx = 15
while (segs[idx]) {
const num = parseInt(segs[idx], 16)
this.octets[octIdx--] = num & 255
this.octets[octIdx--] = num >> 8
idx -= 1
}
}
} else {
this.octets = address.split(".").map(Number)
if (this.octets.length !== 4) throw new Error("invalid ipv4 address")
}
if (this.octets.some((o) => o >= 256)) {
throw new Error("invalid ip address")
}
}
static parse(address: string): IpAddress {
return new IpAddress(address)
}
isIpv4(): boolean {
return this.octets.length === 4
}
isIpv6(): boolean {
return this.octets.length === 16
}
isPublic(): boolean {
return this.isIpv4() && !PRIVATE_IPV4_RANGES.some((r) => r.contains(this))
}
}
export class IpNet extends IpAddress {
readonly prefix
constructor(readonly ipnet: string) {
const [address, prefixStr] = ipnet.split("/", 2)
super(address)
this.prefix = Number(prefixStr)
}
static parse(ipnet: string): IpNet {
return new IpNet(ipnet)
}
contains(address: string | IpAddress): boolean {
if (typeof address === "string") address = new IpAddress(address)
if (this.octets.length !== address.octets.length) return false
let prefix = this.prefix
let idx = 0
while (idx < this.octets.length && prefix >= 8) {
if (this.octets[idx] !== address.octets[idx]) {
return false
}
idx += 1
prefix -= 8
}
if (prefix === 0 || idx >= this.octets.length) return true
const mask = 255 << prefix
return (this.octets[idx] & mask) === (address.octets[idx] & mask)
}
}
export const PRIVATE_IPV4_RANGES = [
new IpNet("127.0.0.0/8"),
new IpNet("10.0.0.0/8"),
new IpNet("172.16.0.0/12"),
new IpNet("192.168.0.0/16"),
]
export const IPV6_LINK_LOCAL = new IpNet("fe80::/10")
export const CGNAT = new IpNet("100.64.0.0/10")

View File

@@ -61,7 +61,7 @@ import {
} from "../../base/lib/inits"
import { DropGenerator } from "../../base/lib/util/Drop"
export const OSVersion = testTypeVersion("0.4.0-alpha.9")
export const OSVersion = testTypeVersion("0.4.0-alpha.10")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =
@@ -104,7 +104,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
// prettier-ignore
type StartSdkEffectWrapper = {
[K in keyof Omit<Effects, NestedEffects | InterfaceEffects | MainUsedEffects | CallbackEffects | AlreadyExposed>]: (effects: Effects, ...args: Parameters<Effects[K]>) => ReturnType<Effects[K]>
[K in keyof Omit<Effects, "eventId" | NestedEffects | InterfaceEffects | MainUsedEffects | CallbackEffects | AlreadyExposed>]: (effects: Effects, ...args: Parameters<Effects[K]>) => ReturnType<Effects[K]>
}
const startSdkEffectWrapper: StartSdkEffectWrapper = {
restart: (effects, ...args) => effects.restart(...args),

View File

@@ -197,7 +197,7 @@ async function runRsync(rsyncOptions: {
for (const exclude of options.exclude) {
args.push(`--exclude=${exclude}`)
}
args.push("-actAXH")
args.push("-rlptgocAXH")
args.push("--info=progress2")
args.push("--no-inc-recursive")
args.push(srcPath)

View File

@@ -157,10 +157,14 @@ export class SubContainerOwned<
) {
super()
this.leaderExited = false
this.leader = cp.spawn("start-cli", ["subcontainer", "launch", rootfs], {
killSignal: "SIGKILL",
stdio: "inherit",
})
this.leader = cp.spawn(
"start-container",
["subcontainer", "launch", rootfs],
{
killSignal: "SIGKILL",
stdio: "inherit",
},
)
this.leader.on("exit", () => {
this.leaderExited = true
})
@@ -407,7 +411,7 @@ export class SubContainerOwned<
delete options.cwd
}
const child = cp.spawn(
"start-cli",
"start-container",
[
"subcontainer",
"exec",
@@ -529,7 +533,7 @@ export class SubContainerOwned<
await this.killLeader()
this.leaderExited = false
this.leader = cp.spawn(
"start-cli",
"start-container",
[
"subcontainer",
"launch",
@@ -571,7 +575,7 @@ export class SubContainerOwned<
delete options.cwd
}
return cp.spawn(
"start-cli",
"start-container",
[
"subcontainer",
"exec",

View File

@@ -269,7 +269,7 @@ export class FileHelper<A> {
eq: (left: B | null | undefined, right: B | null) => boolean,
abort?: AbortSignal,
) {
let res
let prev: { value: B | null } | null = null
while (effects.isInContext && !abort?.aborted) {
if (await exists(this.path)) {
const ctrl = new AbortController()
@@ -287,8 +287,10 @@ export class FileHelper<A> {
}
})
.catch((e) => console.error(asError(e)))
if (!eq(res, newRes)) yield newRes
res = newRes
if (!prev || !eq(prev.value, newRes)) {
yield newRes
}
prev = { value: newRes }
await listen
} else {
yield null

View File

@@ -1,5 +1,4 @@
export * from "../../../base/lib/util"
export { GetSslCertificate } from "./GetSslCertificate"
export { hostnameInfoToAddress } from "../../../base/lib/util/Hostname"
export { Drop } from "../../../base/lib/util/Drop"

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.36",
"version": "0.4.0-beta.37",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.36",
"version": "0.4.0-beta.37",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

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