Files
start-os/web/projects/ui/src/app/services/marketplace.service.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

260 lines
6.5 KiB
TypeScript

import { inject, Injectable } from '@angular/core'
import {
GetPackageRes,
Marketplace,
MarketplacePkg,
StoreDataWithUrl,
StoreIdentity,
} from '@start9labs/marketplace'
import { defaultRegistries, Exver, sameUrl } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { PatchDB } from 'patch-db-client'
import {
BehaviorSubject,
catchError,
combineLatest,
distinctUntilChanged,
filter,
from,
map,
mergeMap,
Observable,
of,
pairwise,
ReplaySubject,
scan,
shareReplay,
startWith,
switchMap,
tap,
} from 'rxjs'
import { RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
const { start9, community } = defaultRegistries
@Injectable({
providedIn: 'root',
})
export class MarketplaceService {
private readonly api = inject(ApiService)
private readonly patch: PatchDB<DataModel> = inject(PatchDB)
private readonly exver = inject(Exver)
readonly registries$: Observable<StoreIdentity[]> = this.patch
.watch$('ui', 'registries')
.pipe(
map(registries => [
toStoreIdentity(start9, registries[start9]),
toStoreIdentity(community, registries[community]),
...Object.entries(registries)
.filter(([u, _]) => !sameUrl(start9, u) && !sameUrl(community, u))
.map(([url, name]) => toStoreIdentity(url, name)),
]),
)
readonly newRegistry$ = this.registries$.pipe(
startWith<StoreIdentity[]>([]),
pairwise(),
mergeMap(([p, c]) => c.filter(a => !p.find(b => sameUrl(a.url, b.url)))),
)
readonly currentRegistryUrl$ = new ReplaySubject<string>(1)
readonly requestErrors$ = new BehaviorSubject<string[]>([])
readonly marketplace$: Observable<Marketplace> = combineLatest([
this.newRegistry$.pipe(
mergeMap(({ url, name }) =>
this.fetchRegistry$(url).pipe(
tap(data => {
if (data?.info.name)
this.updateRegistryName(url, name, data.info.name)
}),
map(data => [url, data] satisfies [string, StoreDataWithUrl | null]),
startWith<[string, StoreDataWithUrl | null]>([url, null]),
),
),
scan<[string, StoreDataWithUrl | null], Marketplace>(
(requests, [url, store]) => ({
...requests,
[url]: store,
}),
{},
),
),
this.registries$,
]).pipe(
map(([marketplace, registries]) =>
Object.fromEntries(
Object.entries(marketplace).filter(([url]) =>
registries.find(store => sameUrl(store.url, url)),
),
),
),
shareReplay(1),
)
readonly currentRegistry$: Observable<StoreDataWithUrl> = combineLatest([
this.marketplace$,
this.currentRegistryUrl$,
this.currentRegistryUrl$.pipe(
distinctUntilChanged(),
switchMap(url => this.fetchRegistry$(url).pipe(startWith(null))),
),
]).pipe(
map(([all, url, current]) => current || all[url]),
filter(Boolean),
shareReplay(1),
)
getPackage$(
id: string,
version: string | null,
flavor: string | null,
registryUrl?: string,
): Observable<MarketplacePkg> {
return this.currentRegistry$.pipe(
switchMap(registry => {
const url = registryUrl || registry.url
const pkg = registry.packages.find(
p =>
p.id === id &&
p.flavor === flavor &&
(!version || this.exver.compareExver(p.version, version) === 0),
)
return pkg ? of(pkg) : this.fetchPackage$(url, id, version, flavor)
}),
)
}
fetchInfo$(registry: string): Observable<T.RegistryInfo> {
return from(this.api.getRegistryInfo({ registry })).pipe(
map(info => ({
...info,
categories: {
all: { name: 'All' },
...info.categories,
},
})),
)
}
fetchStatic$(pkg: MarketplacePkg): Observable<string> {
return from(this.api.getStaticProxy(pkg, 'LICENSE.md'))
}
private fetchRegistry$(url: string): Observable<StoreDataWithUrl | null> {
console.log('FETCHING REGISTRY: ', url)
return combineLatest([this.fetchInfo$(url), this.fetchPackages$(url)]).pipe(
map(([info, packages]) => ({ info, packages, url })),
catchError(e => {
console.error(e)
this.requestErrors$.next(this.requestErrors$.value.concat(url))
return of(null)
}),
)
}
private fetchPackages$(url: string): Observable<MarketplacePkg[]> {
return from(
this.api.getRegistryPackages({
registry: url,
id: null,
targetVersion: null,
otherVersions: 'short',
}),
).pipe(
map(packages => {
return Object.entries(packages).flatMap(([id, pkgInfo]) =>
Object.keys(pkgInfo.best).map(version =>
this.convertRegistryPkgToMarketplacePkg(
id,
version,
this.exver.getFlavor(version),
pkgInfo,
),
),
)
}),
)
}
private fetchPackage$(
url: string,
id: string,
version: string | null,
flavor: string | null,
): Observable<MarketplacePkg> {
return from(
this.api.getRegistryPackage({
registry: url,
id,
targetVersion: version ? `=${version}` : null,
otherVersions: 'short',
}),
).pipe(
map(pkgInfo =>
this.convertRegistryPkgToMarketplacePkg(id, version, flavor, pkgInfo),
),
)
}
private convertRegistryPkgToMarketplacePkg(
id: string,
version: string | null | undefined,
flavor: string | null,
pkgInfo: GetPackageRes,
): MarketplacePkg {
const ver =
version ||
Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) ||
null
const best = ver && pkgInfo.best[ver]
if (!best) {
return {} as MarketplacePkg
}
return {
id,
flavor,
version: ver || '',
...pkgInfo,
...best,
}
}
async installPackage(
id: string,
version: string,
url: string,
): Promise<void> {
const params: RR.InstallPackageReq = {
id,
version,
registry: url,
}
await this.api.installPackage(params)
}
private async updateRegistryName(
url: string,
oldName: string | null,
newName: string,
): Promise<void> {
if (oldName !== newName) {
this.api.setDbValue<string>(['registries', url], newName)
}
}
}
function toStoreIdentity(url: string, name?: string | null): StoreIdentity {
return {
url,
name: name || url,
}
}