Feature/fe new registry (#2647)

* bugfixes

* update fe types

* implement new registry types in marketplace and ui

* fix marketplace types to have default params

* add alt implementation toggle

* merge cleanup

* more cleanup and notes

* fix build

* cleanup sync with next/minor

* add exver JS parser

* parse ValidExVer to string

* update types to interface

* add VersionRange and comparative functions

* Parse ExtendedVersion from string

* add conjunction, disjunction, and inversion logic

* consider flavor in satisfiedBy fn

* consider prerelease for ordering

* add compare fn for sorting

* rename fns for consistency

* refactoring

* update compare fn to return null if flavors don't match

* begin simplifying dependencies

* under construction

* wip

* add dependency metadata to CurrentDependencyInfo

* ditch inheritance for recursive VersionRange constructor. Recursive 'satisfiedBy' fn wip

* preprocess manifest

* misc fixes

* use sdk version as osVersion in manifest

* chore: Change the type to just validate and not generate all solutions.

* add publishedAt

* fix pegjs exports

* integrate exver into sdk

* misc fixes

* complete satisfiedBy fn

* refactor - use greaterThanOrEqual and lessThanOrEqual fns

* fix tests

* update dependency details

* update types

* remove interim types

* rename alt implementation to flavor

* cleanup os update

* format exver.ts

* add s9pk parsing endpoints

* fix build

* update to exver

* exver and bug fixes

* update static endpoints + cleanup

* cleanup

* update static proxy verification

* make mocks more robust; fix dep icon fallback; cleanup

* refactor alert versions and update fixtures

* registry bugfixes

* misc fixes

* cleanup unused

* convert patchdb ui seed to camelCase

* update otherVersions type

* change otherVersions: null to 'none'

* refactor and complete feature

* improve static endpoints

* fix install params

* mask systemd-networkd-wait-online

* fix static file fetching

* include non-matching versions in otherVersions

* convert release notes to modal and clean up displayExver

* alert for no other versions

* Fix ack-instructions casing

* fix indeterminate loader on service install

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Shadowy Super Coder <musashidisciple@proton.me>
Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
Co-authored-by: J H <dragondef@gmail.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Lucy
2024-07-22 20:48:12 -04:00
committed by GitHub
parent 0fbb18b315
commit a535fc17c3
196 changed files with 7002 additions and 2162 deletions

View File

@@ -20,7 +20,7 @@ import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
import { Emver, THEME } from '@start9labs/shared'
import { Exver, THEME } from '@start9labs/shared'
import { ConnectionService } from 'src/app/services/connection.service'
import { getManifest } from 'src/app/util/get-package-data'
@@ -99,10 +99,10 @@ export class MenuComponent {
]).pipe(
map(([marketplace, local]) =>
Object.entries(marketplace).reduce((list, [_, store]) => {
store?.packages.forEach(({ manifest: { id, version } }) => {
store?.packages.forEach(({ id, version }) => {
if (
local[id] &&
this.emver.compare(
this.exver.compareExver(
version,
getManifest(local[id]).version || '',
) === 1
@@ -125,7 +125,7 @@ export class MenuComponent {
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly splitPane: SplitPaneTracker,
private readonly emver: Emver,
private readonly exver: Exver,
private readonly connection$: ConnectionService,
) {}
}

View File

@@ -7,7 +7,8 @@ import {
DiskBackupTarget,
} from 'src/app/services/api/api.types'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
import { getErrorMessage, Emver } from '@start9labs/shared'
import { Exver, getErrorMessage } from '@start9labs/shared'
import { Version } from '@start9labs/start-sdk'
@Injectable({
providedIn: 'root',
@@ -20,7 +21,7 @@ export class BackupService {
constructor(
private readonly embassyApi: ApiService,
private readonly emver: Emver,
private readonly exver: Exver,
) {}
async getBackupTargets(): Promise<void> {
@@ -57,14 +58,15 @@ export class BackupService {
hasAnyBackup(target: BackupTarget): boolean {
return Object.values(target.startOs).some(
s => this.emver.compare(s.version, '0.3.6') !== -1,
s => this.exver.compareOsVersion(s.version, '0.3.6') !== 'less',
)
}
hasThisBackup(target: BackupTarget, id: string): boolean {
return (
target.startOs[id] &&
this.emver.compare(target.startOs[id].version, '0.3.6') !== -1
this.exver.compareOsVersion(target.startOs[id].version, '0.3.6') !==
'less'
)
}
}

View File

@@ -28,7 +28,7 @@
[open]="!!open.get(item)"
(openChange)="open.set(item, $event)"
>
{{ item.value | mustache: $any(spec.spec).displayAs }}
{{ item.value | mustache : $any(spec.spec).displayAs }}
<ng-container *ngTemplateOutlet="remove"></ng-container>
</form-object>
<ng-template #control>

View File

@@ -18,8 +18,8 @@
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max"
[min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
@@ -32,8 +32,8 @@
[disabled]="!!spec.disabled"
[readOnly]="readOnly"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper: getLimit) : min"
[max]="spec.max ? (spec.max | tuiMapper: getLimit) : max"
[min]="spec.min ? (spec.min | tuiMapper : getLimit) : min"
[max]="spec.max ? (spec.max | tuiMapper : getLimit) : max"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'
import { endWith, Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { Emver } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { ConfigService } from '../../../services/config.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@@ -9,13 +9,16 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
@Injectable({ providedIn: 'root' })
export class RefreshAlertService extends Observable<boolean> {
private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe(
map(version => !!this.emver.compare(this.config.version, version)),
map(
version =>
this.exver.compareOsVersion(this.config.version, version) !== 'equal',
),
endWith(false),
)
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly emver: Emver,
private readonly exver: Exver,
private readonly config: ConfigService,
) {
super(subscriber => this.stream$.subscribe(subscriber))

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { PackageBackupInfo } from 'src/app/services/api/api.types'
import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -19,7 +19,7 @@ export interface AppRecoverOption extends PackageBackupInfo {
export class ToOptionsPipe implements PipeTransform {
constructor(
private readonly config: ConfigService,
private readonly emver: Emver,
private readonly exver: Exver,
) {}
transform(
@@ -44,7 +44,9 @@ export class ToOptionsPipe implements PipeTransform {
}
private compare(version: string): boolean {
// checks to see if backup was made on a newer version of eOS
return this.emver.compare(version, this.config.version) === 1
// checks to see if backup was made on a newer version of startOS
return (
this.exver.compareOsVersion(version, this.config.version) === 'greater'
)
}
}

View File

@@ -150,7 +150,7 @@ export class AppActionsPage {
try {
await this.embassyApi.uninstallPackage({ id: this.pkgId })
this.embassyApi
.setDbValue<boolean>(['ack-instructions', this.pkgId], false)
.setDbValue<boolean>(['ackInstructions', this.pkgId], false)
.catch(e => console.error('Failed to mark instructions as unseen', e))
this.navCtrl.navigateRoot('/services')
} catch (e: any) {

View File

@@ -11,7 +11,7 @@
</ion-thumbnail>
<ion-label>
<h2 ticker>{{ manifest.title }}</h2>
<p>{{ manifest.version | displayEmver }}</p>
<p>{{ manifest.version }}</p>
<status
[rendering]="pkg.primaryRendering"
[installingInfo]="$any(pkg.entry.stateInfo).installingInfo"
@@ -27,7 +27,7 @@
color="primary"
(click)="launchUi($event, pkg.entry.serviceInterfaces)"
[disabled]="
!(pkg.entry.stateInfo.state | isLaunchable: pkgMainStatus.status)
!(pkg.entry.stateInfo.state | isLaunchable : pkgMainStatus.status)
"
>
<ion-icon slot="icon-only" name="open-outline"></ion-icon>

View File

@@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppListPage } from './app-list.page'
import {
EmverPipesModule,
ExverPipesModule,
ResponsiveColModule,
TextSpinnerComponentModule,
TickerModule,
@@ -29,7 +29,7 @@ const routes: Routes = [
imports: [
CommonModule,
StatusComponentModule,
EmverPipesModule,
ExverPipesModule,
TextSpinnerComponentModule,
LaunchablePipeModule,
UiPipeModule,

View File

@@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppShowPage } from './app-show.page'
import {
EmverPipesModule,
ExverPipesModule,
ResponsiveColModule,
SharedPipesModule,
} from '@start9labs/shared'
@@ -49,7 +49,7 @@ const routes: Routes = [
InstallingProgressPipeModule,
IonicModule,
RouterModule.forChild(routes),
EmverPipesModule,
ExverPipesModule,
LaunchablePipeModule,
UiPipeModule,
ResponsiveColModule,

View File

@@ -7,8 +7,7 @@
<!-- ** installing, updating, restoring ** -->
<ng-container *ngIf="showProgress(pkg); else installed">
<app-show-progress
*ngIf="pkg.stateInfo.installingInfo as installingInfo"
[phases]="installingInfo.progress.phases"
[phases]="pkg.stateInfo.installingInfo.progress.phases"
></app-show-progress>
</ng-container>
@@ -34,9 +33,7 @@
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional
[manifest]="pkg.stateInfo.manifest"
></app-show-additional>
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
</ng-template>

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { PatchDB } from 'patch-db-client'
import {
AllPackageData,
DataModel,
InstallingState,
PackageDataEntry,
@@ -47,17 +48,19 @@ export class AppShowPage {
private readonly pkgId = getPkgId(this.route)
readonly pkgPlus$ = combineLatest([
this.patch.watch$('packageData', this.pkgId),
this.patch.watch$('packageData'),
this.depErrorService.getPkgDepErrors$(this.pkgId),
]).pipe(
tap(([pkg, _]) => {
tap(([allPkgs, _]) => {
const pkg = allPkgs[this.pkgId]
// if package disappears, navigate to list page
if (!pkg) this.navCtrl.navigateRoot('/services')
}),
map(([pkg, depErrors]) => {
map(([allPkgs, depErrors]) => {
const pkg = allPkgs[this.pkgId]
return {
pkg,
dependencies: this.getDepInfo(pkg, depErrors),
dependencies: this.getDepInfo(pkg, allPkgs, depErrors),
status: renderPkgStatus(pkg, depErrors),
}
}),
@@ -81,17 +84,45 @@ export class AppShowPage {
private getDepInfo(
pkg: PackageDataEntry,
allPkgs: AllPackageData,
depErrors: PkgDependencyErrors,
): DependencyInfo[] {
const manifest = getManifest(pkg)
return Object.keys(pkg.currentDependencies)
.filter(id => !!manifest.dependencies[id])
.map(id => this.getDepValues(pkg, manifest, id, depErrors))
.map(id => this.getDepValues(pkg, allPkgs, manifest, id, depErrors))
}
private getDepDetails(
pkg: PackageDataEntry,
allPkgs: AllPackageData,
depId: string,
) {
const { title, icon, versionRange } = pkg.currentDependencies[depId]
if (
allPkgs[depId] &&
(allPkgs[depId].stateInfo.state === 'installed' ||
allPkgs[depId].stateInfo.state === 'updating')
) {
return {
title: allPkgs[depId].stateInfo.manifest!.title,
icon: allPkgs[depId].icon,
versionRange,
}
} else {
return {
title: title ? title : depId,
icon: icon ? icon : 'assets/img/service-icons/fallback.png',
versionRange,
}
}
}
private getDepValues(
pkg: PackageDataEntry,
allPkgs: AllPackageData,
manifest: T.Manifest,
depId: string,
depErrors: PkgDependencyErrors,
@@ -103,11 +134,15 @@ export class AppShowPage {
depErrors,
)
const { title, icon, versionSpec } = pkg.currentDependencies[depId]
const { title, icon, versionRange } = this.getDepDetails(
pkg,
allPkgs,
depId,
)
return {
id: depId,
version: versionSpec,
version: versionRange,
title,
icon,
errorText: errorText
@@ -190,7 +225,7 @@ export class AppShowPage {
const dependentInfo: DependentInfo = {
id: pkgManifest.id,
title: pkgManifest.title,
version: pkg.currentDependencies[depId].versionSpec,
version: pkg.currentDependencies[depId].versionRange,
}
const navigationExtras: NavigationExtras = {
state: { dependentInfo },

View File

@@ -6,11 +6,11 @@
<ion-item>
<ion-label>
<h2>Version</h2>
<p>{{ manifest.version | displayEmver }}</p>
<p>{{ pkg.stateInfo.manifest.version }}</p>
</ion-label>
</ion-item>
<ion-item
*ngIf="manifest.gitHash as gitHash; else noHash"
*ngIf="pkg.stateInfo.manifest.gitHash as gitHash; else noHash"
button
detail="false"
(click)="copy(gitHash)"
@@ -32,20 +32,20 @@
<ion-item button detail="false" (click)="presentModalLicense()">
<ion-label>
<h2>License</h2>
<p>{{ manifest.license }}</p>
<p>{{ pkg.stateInfo.manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item
[href]="manifest.marketingSite"
[disabled]="!manifest.marketingSite"
[href]="pkg.stateInfo.manifest.marketingSite"
[disabled]="!pkg.stateInfo.manifest.marketingSite"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Marketing Site</h2>
<p>{{ manifest.marketingSite || 'Not provided' }}</p>
<p>{{ pkg.stateInfo.manifest.marketingSite || 'Not provided' }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
@@ -54,52 +54,52 @@
<ion-col responsiveCol sizeXs="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="manifest.upstreamRepo"
[href]="pkg.stateInfo.manifest.upstreamRepo"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ manifest.upstreamRepo }}</p>
<p>{{ pkg.stateInfo.manifest.upstreamRepo }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest.wrapperRepo"
[href]="pkg.stateInfo.manifest.wrapperRepo"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ manifest.wrapperRepo }}</p>
<p>{{ pkg.stateInfo.manifest.wrapperRepo }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest.supportSite"
[disabled]="!manifest.supportSite"
[href]="pkg.stateInfo.manifest.supportSite"
[disabled]="!pkg.stateInfo.manifest.supportSite"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ manifest.supportSite || 'Not provided' }}</p>
<p>{{ pkg.stateInfo.manifest.supportSite || 'Not provided' }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="manifest.donationUrl"
[disabled]="!manifest.donationUrl"
[href]="pkg.stateInfo.manifest.donationUrl"
[disabled]="!pkg.stateInfo.manifest.donationUrl"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Donation Link</h2>
<p>{{ manifest.donationUrl || 'Not provided' }}</p>
<p>{{ pkg.stateInfo.manifest.donationUrl || 'Not provided' }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>

View File

@@ -1,9 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { ModalController, ToastController } from '@ionic/angular'
import { copyToClipboard, MarkdownComponent } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import { from } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
InstalledState,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
@Component({
selector: 'app-show-additional',
@@ -12,7 +15,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
})
export class AppShowAdditionalComponent {
@Input()
manifest!: T.Manifest
pkg!: PackageDataEntry<InstalledState>
constructor(
private readonly modalCtrl: ModalController,
@@ -35,16 +38,12 @@ export class AppShowAdditionalComponent {
}
async presentModalLicense() {
const { id, version } = this.manifest
const { id } = this.pkg.stateInfo.manifest
const modal = await this.modalCtrl.create({
componentProps: {
title: 'License',
content: from(
this.api.getStatic(
`/public/package-data/${id}/${version}/LICENSE.md`,
),
),
content: from(this.api.getStaticInstalled(id, 'LICENSE.md')),
},
component: MarkdownComponent,
})

View File

@@ -15,7 +15,7 @@
></ion-icon>
{{ dep.title }}
</h2>
<p>{{ dep.version | displayEmver }}</p>
<p>{{ dep.version }}</p>
<p>
<ion-text [color]="dep.errorText ? 'warning' : 'success'">
{{ dep.errorText || 'satisfied' }}

View File

@@ -9,7 +9,7 @@
<h1 class="montserrat" [class.less-large]="manifest.title.length > 20">
{{ manifest.title }}
</h1>
<h2>{{ manifest.version | displayEmver }}</h2>
<h2>{{ manifest.version }}</h2>
</ion-label>
</div>
</ion-toolbar>

View File

@@ -7,10 +7,12 @@
</p>
<ion-progress-bar
[type]="
phase.progress === true ||
(phase.progress !== false && phase.progress!== null && phase.progress.total)
? 'determinate'
: 'indeterminate'
phase.progress === false ||
(phase.progress !== true &&
phase.progress !== null &&
!phase.progress.total)
? 'indeterminate'
: 'determinate'
"
[color]="phase.progress === true ? 'success' : 'secondary'"
[value]="phase.progress | installingProgress"

View File

@@ -9,5 +9,5 @@ import { T } from '@start9labs/start-sdk'
})
export class AppShowProgressComponent {
@Input()
phases!: T.FullProgress['phases']
phases!: T.NamedProgress[]
}

View File

@@ -10,7 +10,6 @@ import {
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { from, map, Observable } from 'rxjs'
import { PatchDB } from 'patch-db-client'
import { T } from '@start9labs/start-sdk'
import { FormDialogService } from 'src/app/services/form-dialog.service'
import { ConfigModal, PackageConfigData } from 'src/app/modals/config.component'
@@ -32,6 +31,7 @@ export class ToButtonsPipe implements PipeTransform {
private readonly navCtrl: NavController,
private readonly modalCtrl: ModalController,
private readonly apiService: ApiService,
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly formDialog: FormDialogService,
) {}
@@ -42,7 +42,7 @@ export class ToButtonsPipe implements PipeTransform {
return [
// instructions
{
action: () => this.presentModalInstructions(manifest),
action: () => this.presentModalInstructions(pkg),
title: 'Instructions',
description: `Understand how to use ${manifest.title}`,
icon: 'list-outline',
@@ -103,17 +103,20 @@ export class ToButtonsPipe implements PipeTransform {
]
}
private async presentModalInstructions(manifest: T.Manifest) {
private async presentModalInstructions(
pkg: PackageDataEntry<InstalledState>,
) {
this.apiService
.setDbValue<boolean>(['ack-instructions', manifest.id], true)
.setDbValue<boolean>(['ackInstructions', pkg.stateInfo.manifest.id], true)
.catch(e => console.error('Failed to mark instructions as seen', e))
const modal = await this.modalCtrl.create({
componentProps: {
title: 'Instructions',
content: from(
this.apiService.getStatic(
`/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`,
this.api.getStaticInstalled(
pkg.stateInfo.manifest.id,
'instructions.md',
),
),
},

View File

@@ -97,7 +97,4 @@
</ng-template>
</div>
<a
id="install-cert"
href="/eos/local.crt"
></a>
<a id="install-cert" href="/static/local-root-ca.crt"></a>

View File

@@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
SharedPipesModule,
EmverPipesModule,
ExverPipesModule,
ResponsiveColModule,
} from '@start9labs/shared'
import {
@@ -34,7 +34,7 @@ const routes: Routes = [
FormsModule,
RouterModule.forChild(routes),
SharedPipesModule,
EmverPipesModule,
ExverPipesModule,
FilterPackagesPipeModule,
MarketplaceStatusModule,
BadgeMenuComponentModule,

View File

@@ -62,9 +62,10 @@
>
<marketplace-item [pkg]="pkg">
<marketplace-status
*ngIf="localPkgs[pkg.id] as localPkg"
class="status"
[version]="pkg.manifest.version"
[localPkg]="localPkgs[pkg.manifest.id]"
[version]="pkg.version"
[localPkg]="localPkg"
></marketplace-status>
</marketplace-item>
</ion-col>

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { T } from '@start9labs/start-sdk'
import { TuiDialogService } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
@@ -20,12 +21,21 @@ export class MarketplaceListPage {
readonly store$ = this.marketplaceService.getSelectedStore$().pipe(
map(({ info, packages }) => {
const categories = new Set<string>()
if (info.categories.includes('featured')) categories.add('featured')
info.categories.forEach(c => categories.add(c))
categories.add('all')
const categories = new Map<string, T.Category>()
if (info.categories['featured'])
categories.set('featured', info.categories['featured'])
Object.keys(info.categories).forEach(c =>
categories.set(c, info.categories[c]),
)
categories.set('all', {
name: 'All',
description: {
short: 'All registry packages',
long: 'An unfiltered list of all packages available on this registry.',
},
})
return { categories: Array.from(categories), packages }
return { categories, packages }
}),
)

View File

@@ -17,13 +17,6 @@ const routes: Routes = [
m => m.MarketplaceShowPageModule,
),
},
{
path: ':pkgId/notes',
loadChildren: () =>
import('./release-notes/release-notes.module').then(
m => m.ReleaseNotesPageModule,
),
},
]
@NgModule({

View File

@@ -1,11 +1,15 @@
<div class="action-buttons">
<ion-button
*ngIf="localPkg"
*ngIf="localPkg && localPkg.stateInfo.state !== 'removing'"
expand="block"
color="primary"
[routerLink]="['/services', pkg.manifest.id]"
[routerLink]="['/services', pkg.id]"
>
View Installed
{{
localPkg.stateInfo.state === 'installed'
? 'View Installed'
: 'View Installing'
}}
</ion-button>
<ng-container *ngIf="localPkg; else install">
<ng-container
@@ -15,7 +19,7 @@
"
>
<ion-button
*ngIf="(manifest.version | compareEmver: pkg.manifest.version) === -1"
*ngIf="(manifest.version | compareExver : pkg.version) === -1"
expand="block"
color="success"
(click)="tryInstall()"
@@ -23,7 +27,7 @@
Update
</ion-button>
<ion-button
*ngIf="(manifest.version | compareEmver: pkg.manifest.version) === 1"
*ngIf="(manifest.version | compareExver : pkg.version) === 1"
expand="block"
color="warning"
(click)="tryInstall()"
@@ -32,7 +36,7 @@
</ion-button>
<ng-container *ngIf="showDevTools$ | async">
<ion-button
*ngIf="(manifest.version | compareEmver: pkg.manifest.version) === 0"
*ngIf="(manifest.version | compareExver : pkg.version) === 0"
expand="block"
color="success"
(click)="tryInstall()"
@@ -44,8 +48,12 @@
</ng-container>
<ng-template #install>
<ion-button expand="block" color="success" (click)="tryInstall()">
Install
<ion-button
expand="block"
[color]="localFlavor ? 'warning' : 'success'"
(click)="tryInstall()"
>
{{ localFlavor ? 'Switch' : 'Install' }}
</ion-button>
</ng-template>
</div>

View File

@@ -10,7 +10,7 @@ import {
MarketplacePkg,
} from '@start9labs/marketplace'
import {
Emver,
Exver,
ErrorService,
isEmptyObject,
LoadingService,
@@ -44,6 +44,9 @@ export class MarketplaceShowControlsComponent {
@Input()
localPkg!: PackageDataEntry | null
@Input()
localFlavor!: boolean
readonly showDevTools$ = this.ClientStorageService.showDevTools$
constructor(
@@ -52,7 +55,7 @@ export class MarketplaceShowControlsComponent {
@Inject(AbstractMarketplaceService)
private readonly marketplaceService: MarketplaceService,
private readonly loader: LoadingService,
private readonly emver: Emver,
private readonly exver: Exver,
private readonly errorService: ErrorService,
private readonly patch: PatchDB<DataModel>,
) {}
@@ -79,7 +82,7 @@ export class MarketplaceShowControlsComponent {
const localManifest = getManifest(this.localPkg)
if (
this.emver.compare(localManifest.version, this.pkg.manifest.version) !==
this.exver.compareExver(localManifest.version, this.pkg.version) !==
0 &&
hasCurrentDeps(localManifest.id, await getAllPackages(this.patch))
) {
@@ -136,9 +139,9 @@ export class MarketplaceShowControlsComponent {
private async dryInstall(url: string) {
const breakages = dryUpdate(
this.pkg.manifest,
this.pkg,
await getAllPackages(this.patch),
this.emver,
this.exver,
)
if (isEmptyObject(breakages)) {
@@ -152,7 +155,7 @@ export class MarketplaceShowControlsComponent {
}
private async alertInstall(url: string) {
const installAlert = this.pkg.manifest.alerts.install
const installAlert = this.pkg.alerts.install
if (!installAlert) return this.install(url)
@@ -179,7 +182,7 @@ export class MarketplaceShowControlsComponent {
private async install(url: string) {
const loader = this.loader.open('Beginning Install...').subscribe()
const { id, version } = this.pkg.manifest
const { id, version } = this.pkg
try {
await this.marketplaceService.installPackage(id, version, url)

View File

@@ -13,16 +13,16 @@
<br />
<br />
<span
*ngIf="version | satisfiesEmver: dependentInfo.version"
*ngIf="version | satisfiesExver : dependentInfo.version"
class="text"
>
{{ title }} version {{ version | displayEmver }} is compatible.
{{ title }} version {{ version }} is compatible.
</span>
<span
*ngIf="!(version | satisfiesEmver: dependentInfo.version)"
*ngIf="!(version | satisfiesExver : dependentInfo.version)"
class="text text_error"
>
{{ title }} version {{ version | displayEmver }} is NOT compatible.
{{ title }} version {{ version }} is NOT compatible.
</span>
</ion-text>
</p>

View File

@@ -24,10 +24,10 @@ export class MarketplaceShowDependentComponent {
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
get title(): string {
return this.pkg.manifest.title
return this.pkg.title
}
get version(): string {
return this.pkg.manifest.version
return this.pkg.version
}
}

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
import { RouterModule, Routes } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import {
EmverPipesModule,
ExverPipesModule,
MarkdownPipeModule,
SharedPipesModule,
TextSpinnerComponentModule,
@@ -13,6 +13,7 @@ import {
AdditionalModule,
DependenciesModule,
PackageModule,
FlavorsModule,
} from '@start9labs/marketplace'
import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module'
import { MarketplaceShowPage } from './marketplace-show.page'
@@ -35,13 +36,14 @@ const routes: Routes = [
RouterModule.forChild(routes),
TextSpinnerComponentModule,
SharedPipesModule,
EmverPipesModule,
ExverPipesModule,
MarkdownPipeModule,
MarketplaceStatusModule,
PackageModule,
AboutModule,
DependenciesModule,
AdditionalModule,
FlavorsModule,
UiPipeModule,
],
declarations: [

View File

@@ -1,21 +1,14 @@
<marketplace-show-header></marketplace-show-header>
<ion-content class="ion-padding with-widgets">
<ng-container *ngIf="pkg$ | async as pkg else loading">
<ng-container *ngIf="pkg$ | async as pkg; else loading">
<ng-container *ngIf="pkg | empty; else show">
<div
*ngIf="loadVersion$ | async as version"
class="ion-text-center"
style="padding-top: 64px"
>
<div class="ion-text-center" style="padding-top: 64px">
<ion-icon
name="close-circle-outline"
style="font-size: 48px"
></ion-icon>
<h2>
{{ pkgId }} @{{ version === '*' ? 'latest' : version }} not found in
this registry
</h2>
<h2>{{ pkgId }} not found in this registry</h2>
</div>
</ng-container>
@@ -25,21 +18,26 @@
[url]="url"
[pkg]="pkg"
[localPkg]="localPkg$ | async"
[localFlavor]="!!(localFlavor$ | async)"
></marketplace-show-controls>
<marketplace-show-dependent [pkg]="pkg"></marketplace-show-dependent>
<ion-item-group>
<marketplace-about [pkg]="pkg"></marketplace-about>
<marketplace-flavors
*ngIf="flavors$ | async as flavors"
[pkgs]="flavors"
></marketplace-flavors>
<marketplace-dependencies
*ngIf="!(pkg.manifest.dependencies | empty)"
*ngIf="!(pkg.dependencyMetadata | empty)"
[pkg]="pkg"
></marketplace-dependencies>
</ion-item-group>
<marketplace-additional
[pkg]="pkg"
(version)="loadVersion$.next($event)"
(version)="updateVersion($event)"
></marketplace-additional>
</ng-template>
</ng-container>

View File

@@ -1,11 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ActivatedRoute, Router } from '@angular/router'
import { Exver, getPkgId } from '@start9labs/shared'
import {
AbstractMarketplaceService,
MarketplacePkg,
} from '@start9labs/marketplace'
import { PatchDB } from 'patch-db-client'
import { BehaviorSubject } from 'rxjs'
import { filter, shareReplay, switchMap } from 'rxjs/operators'
import { combineLatest, Observable } from 'rxjs'
import { filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/util/get-package-data'
@Component({
selector: 'marketplace-show',
@@ -17,21 +21,61 @@ export class MarketplaceShowPage {
readonly pkgId = getPkgId(this.route)
readonly url = this.route.snapshot.queryParamMap.get('url') || undefined
readonly loadVersion$ = new BehaviorSubject<string>('*')
readonly localPkg$ = combineLatest([
this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)),
this.route.queryParamMap,
]).pipe(
map(([pkg, paramMap]) =>
this.exver.getFlavor(getManifest(pkg).version) === paramMap.get('flavor')
? pkg
: null,
),
shareReplay({ bufferSize: 1, refCount: true }),
)
readonly localPkg$ = this.patch
.watch$('packageData', this.pkgId)
.pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true }))
readonly localFlavor$ = this.localPkg$.pipe(
map(pkg => !pkg),
startWith(false),
)
readonly pkg$ = this.loadVersion$.pipe(
switchMap(version =>
this.marketplaceService.getPackage$(this.pkgId, version, this.url),
readonly pkg$: Observable<MarketplacePkg> = this.route.queryParamMap.pipe(
switchMap(paramMap =>
this.marketplaceService.getPackage$(
this.pkgId,
paramMap.get('version'),
paramMap.get('flavor'),
this.url,
),
),
)
readonly flavors$ = this.route.queryParamMap.pipe(
switchMap(paramMap =>
this.marketplaceService
.getSelectedStore$()
.pipe(
map(s =>
s.packages.filter(
p => p.id === this.pkgId && p.flavor !== paramMap.get('flavor'),
),
),
),
),
)
constructor(
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly patch: PatchDB<DataModel>,
private readonly marketplaceService: AbstractMarketplaceService,
private readonly exver: Exver,
) {}
updateVersion(version: string) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { version },
queryParamsHandling: 'merge',
})
}
}

View File

@@ -1,13 +1,13 @@
<ng-container *ngIf="localPkg">
<ng-container *ngIf="localPkg && sameFlavor">
<div *ngIf="isInstalled(localPkg)">
<ion-text
*ngIf="(version | compareEmver: localVersion) !== 1"
*ngIf="(version | compareExver : localVersion) !== 1"
color="primary"
>
Installed
</ion-text>
<ion-text
*ngIf="(version | compareEmver: localVersion) === 1"
*ngIf="(version | compareExver : localVersion) === 1"
color="success"
>
Update Available

View File

@@ -8,6 +8,7 @@ import {
isRestoring,
getManifest,
} from 'src/app/util/get-package-data'
import { Exver } from '@start9labs/shared'
@Component({
selector: 'marketplace-status',
@@ -16,8 +17,7 @@ import {
})
export class MarketplaceStatusComponent {
@Input() version!: string
@Input() localPkg?: PackageDataEntry
@Input() localPkg!: PackageDataEntry
isInstalled = isInstalled
isInstalling = isInstalling
@@ -26,6 +26,15 @@ export class MarketplaceStatusComponent {
isRestoring = isRestoring
get localVersion(): string {
return this.localPkg ? getManifest(this.localPkg).version : ''
return getManifest(this.localPkg).version
}
get sameFlavor(): boolean {
return (
this.exver.getFlavor(this.version) ===
this.exver.getFlavor(this.localVersion)
)
}
constructor(private readonly exver: Exver) {}
}

View File

@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule } from '@start9labs/shared'
import { ExverPipesModule } from '@start9labs/shared'
import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module'
import { MarketplaceStatusComponent } from './marketplace-status.component'
@@ -9,7 +9,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component'
imports: [
CommonModule,
IonicModule,
EmverPipesModule,
ExverPipesModule,
InstallingProgressPipeModule,
],
declarations: [MarketplaceStatusComponent],

View File

@@ -35,5 +35,5 @@
</ion-item-group>
<!-- hidden element for downloading cert -->
<a id="install-cert" href="/eos/local.crt"></a>
<a id="install-cert" href="/static/local-root-ca.crt"></a>
</ion-content>

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { ServerSpecsPage } from './server-specs.page'
import { EmverPipesModule } from '@start9labs/shared'
import { ExverPipesModule } from '@start9labs/shared'
import { TuiLetModule } from '@taiga-ui/cdk'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
@@ -20,7 +20,7 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
QRComponentModule,
EmverPipesModule,
ExverPipesModule,
TuiLetModule,
],
declarations: [ServerSpecsPage],

View File

@@ -13,7 +13,7 @@
<ion-item>
<ion-label>
<h2>Version</h2>
<p>{{ server.version | displayEmver }}</p>
<p>{{ server.version }}</p>
</ion-label>
</ion-item>
<ion-item>

View File

@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { SideloadPage } from './sideload.page'
import { Routes, RouterModule } from '@angular/router'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { ExverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { DragNDropDirective } from './dnd.directive'
const routes: Routes = [
@@ -19,7 +19,7 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
SharedPipesModule,
EmverPipesModule,
ExverPipesModule,
],
declarations: [SideloadPage, DragNDropDirective],
})

View File

@@ -77,7 +77,7 @@
[src]="toUpload.icon | trustUrl"
/>
<h2>{{ toUpload.manifest.title }}</h2>
<p>{{ toUpload.manifest.version | displayEmver }}</p>
<p>{{ toUpload.manifest.version }}</p>
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@ import { RouterModule, Routes } from '@angular/router'
import { FilterUpdatesPipe, UpdatesPage } from './updates.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import {
EmverPipesModule,
ExverPipesModule,
MarkdownPipeModule,
SharedPipesModule,
} from '@start9labs/shared'
@@ -34,7 +34,7 @@ const routes: Routes = [
RoundProgressModule,
InstallingProgressPipeModule,
StoreIconComponentModule,
EmverPipesModule,
ExverPipesModule,
],
})
export class UpdatesPageModule {}

View File

@@ -30,27 +30,21 @@
>
<ion-accordion-group multiple="true" class="ion-padding-start">
<div *ngFor="let pkg of updates" class="item-container">
<ion-accordion *ngIf="data.localPkgs[pkg.manifest.id] as local">
<ion-accordion *ngIf="data.localPkgs[pkg.id] as local">
<ion-item lines="none" slot="header">
<ion-avatar slot="start" style="width: 50px; height: 50px">
<img [src]="pkg.icon | trustUrl" />
</ion-avatar>
<ion-label>
<h1 style="line-height: 1.3">{{ pkg.manifest.title }}</h1>
<h1 style="line-height: 1.3">{{ pkg.title }}</h1>
<h2 class="inline">
<span>
{{ local.stateInfo.manifest.version | displayEmver }}
</span>
<span>{{ local.stateInfo.manifest.version }}</span>
&nbsp;
<ion-icon name="arrow-forward"></ion-icon>
&nbsp;
<ion-text color="success">
{{ pkg.manifest.version | displayEmver }}
</ion-text>
<ion-text color="success">{{ pkg.version }}</ion-text>
</h2>
<p
*ngIf="marketplaceService.updateErrors[pkg.manifest.id] as error"
>
<p *ngIf="marketplaceService.updateErrors[pkg.id] as error">
<ion-text color="danger">{{ error }}</ion-text>
</p>
</ion-label>
@@ -69,17 +63,17 @@
</ng-container>
<ng-template #notUpdating>
<ion-spinner
*ngIf="marketplaceService.updateQueue[pkg.manifest.id] else updateBtn"
*ngIf="marketplaceService.updateQueue[pkg.id] else updateBtn"
color="dark"
></ion-spinner>
<ng-template #updateBtn>
<ion-button
(click)="tryUpdate(pkg.manifest, host.url, $event)"
[color]="marketplaceService.updateErrors[pkg.manifest.id] ? 'danger' : 'tertiary'"
(click)="tryUpdate(pkg, host.url, $event)"
[color]="marketplaceService.updateErrors[pkg.id] ? 'danger' : 'tertiary'"
strong
>
{{ marketplaceService.updateErrors[pkg.manifest.id] ?
'Retry' : 'Update' }}
{{ marketplaceService.updateErrors[pkg.id] ? 'Retry' :
'Update' }}
</ion-button>
</ng-template>
</ng-template>
@@ -88,12 +82,12 @@
<div class="ion-padding" slot="content">
<div class="notes">
<h5>What's new</h5>
<p [innerHTML]="pkg.manifest.releaseNotes| markdown"></p>
<p [innerHTML]="pkg.releaseNotes| markdown"></p>
</div>
<ion-button
fill="clear"
strong
(click)="viewInMarketplace($event, host.url, pkg.manifest.id)"
(click)="viewInMarketplace($event, host.url, pkg.id)"
>
View listing
<ion-icon slot="end" name="open-outline"></ion-icon>

View File

@@ -13,7 +13,7 @@ import {
MarketplacePkg,
StoreIdentity,
} from '@start9labs/marketplace'
import { Emver, isEmptyObject } from '@start9labs/shared'
import { Exver, isEmptyObject } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core'
import { combineLatest, map, Observable } from 'rxjs'
import { AlertController, NavController } from '@ionic/angular'
@@ -24,7 +24,6 @@ import {
isUpdating,
} from 'src/app/util/get-package-data'
import { dryUpdate } from 'src/app/util/dry-update'
import { T } from '@start9labs/start-sdk'
interface UpdatesData {
hosts: StoreIdentity[]
@@ -59,7 +58,7 @@ export class UpdatesPage {
private readonly patch: PatchDB<DataModel>,
private readonly navCtrl: NavController,
private readonly alertCtrl: AlertController,
private readonly emver: Emver,
private readonly exver: Exver,
) {}
viewInMarketplace(event: Event, url: string, id: string) {
@@ -70,29 +69,29 @@ export class UpdatesPage {
})
}
async tryUpdate(manifest: T.Manifest, url: string, e: Event): Promise<void> {
async tryUpdate(pkg: MarketplacePkg, url: string, e: Event): Promise<void> {
e.stopPropagation()
const { id, version } = manifest
const { id, version } = pkg
delete this.marketplaceService.updateErrors[id]
this.marketplaceService.updateQueue[id] = true
// manifest.id OK because same as local id for update
if (hasCurrentDeps(manifest.id, await getAllPackages(this.patch))) {
this.dryInstall(manifest, url)
// id OK because same as local id for update
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
this.dryInstall(pkg, url)
} else {
this.install(id, version, url)
}
}
private async dryInstall(manifest: T.Manifest, url: string) {
const { id, version, title } = manifest
private async dryInstall(pkg: MarketplacePkg, url: string) {
const { id, version, title } = pkg
const breakages = dryUpdate(
manifest,
pkg,
await getAllPackages(this.patch),
this.emver,
this.exver,
)
if (isEmptyObject(breakages)) {
@@ -159,18 +158,19 @@ export class UpdatesPage {
name: 'filterUpdates',
})
export class FilterUpdatesPipe implements PipeTransform {
constructor(private readonly emver: Emver) {}
constructor(private readonly exver: Exver) {}
transform(
pkgs: MarketplacePkg[],
local: Record<string, PackageDataEntry<InstalledState | UpdatingState>>,
): MarketplacePkg[] {
return pkgs.filter(({ manifest }) => {
const localPkg = local[manifest.id]
return pkgs.filter(({ id, version, flavor }) => {
const localPkg = local[id]
return (
localPkg &&
this.emver.compare(
manifest.version,
this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor &&
this.exver.compareExver(
version,
localPkg.stateInfo.manifest.version,
) === 1
)

File diff suppressed because one or more lines are too long

View File

@@ -3,11 +3,26 @@ import {
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types'
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace'
import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons'
import { Log } from '@start9labs/shared'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
import { T, CB } from '@start9labs/start-sdk'
import { GetPackagesRes } from '@start9labs/marketplace'
const mockBlake3Commitment: T.Blake3Commitment = {
hash: 'fakehash',
size: 0,
}
const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = {
rootSighash: 'fakehash',
rootMaxsize: 0,
}
const mockDescription = {
short: 'Lorem ipsum dolor sit amet',
long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
}
export module Mock {
export const ServerUpdated: T.ServerStatus = {
@@ -37,17 +52,42 @@ export module Mock {
},
}
export const ReleaseNotes: RR.GetReleaseNotesRes = {
'0.19.2':
'Contrary to popular belief, Lorem Ipsum is not simply random text.',
'0.19.1': 'release notes for Bitcoin 0.19.1',
'0.19.0': 'release notes for Bitcoin 0.19.0',
export const RegistryInfo: T.RegistryInfo = {
name: 'Start9 Registry',
icon: REGISTRY_ICON,
categories: {
bitcoin: {
name: 'Bitcoin',
description: mockDescription,
},
featured: {
name: 'Featured',
description: mockDescription,
},
lightning: {
name: 'Lightning',
description: mockDescription,
},
communications: {
name: 'Communications',
description: mockDescription,
},
data: {
name: 'Data',
description: mockDescription,
},
ai: {
name: 'AI',
description: mockDescription,
},
},
}
export const MockManifestBitcoind: T.Manifest = {
id: 'bitcoind',
title: 'Bitcoin Core',
version: '0.21.0',
version: '0.21.0:0',
satisfies: [],
gitHash: 'abcdefgh',
description: {
short: 'A Bitcoin full node by Bitcoin Core.',
@@ -90,7 +130,8 @@ export module Mock {
export const MockManifestLnd: T.Manifest = {
id: 'lnd',
title: 'Lightning Network Daemon',
version: '0.11.1',
version: '0.11.1:0',
satisfies: [],
gitHash: 'abcdefgh',
description: {
short: 'A bolt spec compliant client.',
@@ -116,11 +157,13 @@ export module Mock {
bitcoind: {
description: 'LND needs bitcoin to live.',
optional: true,
s9pk: '',
},
'btc-rpc-proxy': {
description:
'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.',
optional: true,
s9pk: '',
},
},
hasConfig: true,
@@ -143,7 +186,8 @@ export module Mock {
export const MockManifestBitcoinProxy: T.Manifest = {
id: 'btc-rpc-proxy',
title: 'Bitcoin Proxy',
version: '0.2.2',
version: '0.2.2:0',
satisfies: [],
gitHash: 'lmnopqrx',
description: {
short: 'A super charger for your Bitcoin node.',
@@ -168,6 +212,7 @@ export module Mock {
bitcoind: {
description: 'Bitcoin Proxy requires a Bitcoin node.',
optional: false,
s9pk: '',
},
},
hasConfig: false,
@@ -187,149 +232,509 @@ export module Mock {
},
}
export const BitcoinDep: DependencyMetadata = {
export const BitcoinDep: T.DependencyMetadata = {
title: 'Bitcoin Core',
icon: BTC_ICON,
optional: false,
hidden: true,
description: 'Needed to run',
}
export const ProxyDep: DependencyMetadata = {
export const ProxyDep: T.DependencyMetadata = {
title: 'Bitcoin Proxy',
icon: PROXY_ICON,
optional: true,
hidden: false,
description: 'Needed to run',
}
export const MarketplacePkgs: {
[id: string]: {
[version: string]: MarketplacePkg
}
export const OtherPackageVersions: {
[id: T.PackageId]: GetPackagesRes
} = {
bitcoind: {
'0.19.0': {
icon: BTC_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: {
...Mock.MockManifestBitcoind,
version: '0.19.0',
'=26.1.0:0.1.0': {
best: {
'26.1.0:0.1.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
'#knots:26.1.20240325:0': {
title: 'Bitcoin Knots',
description: {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
supportSite: 'https://bitcoinknots.org',
marketingSite: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin', 'featured'],
otherVersions: {
'27.0.0:1.0.0': {
releaseNotes: 'Even better support for Bitcoin and wallets!',
},
'#knots:27.1.0:0': {
releaseNotes: 'Even better support for Bitcoin and wallets!',
},
},
categories: ['bitcoin', 'cryptocurrency'],
versions: ['0.19.0', '0.20.0', '0.21.0'],
dependencyMetadata: {},
publishedAt: new Date().toISOString(),
},
'0.20.0': {
icon: BTC_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: {
...Mock.MockManifestBitcoind,
version: '0.20.0',
'=#knots:26.1.20240325:0': {
best: {
'26.1.0:0.1.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
'#knots:26.1.20240325:0': {
title: 'Bitcoin Knots',
description: {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
supportSite: 'https://bitcoinknots.org',
marketingSite: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin', 'cryptocurrency'],
versions: ['0.19.0', '0.20.0', '0.21.0'],
dependencyMetadata: {},
publishedAt: new Date().toISOString(),
},
'0.21.0': {
icon: BTC_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: {
...Mock.MockManifestBitcoind,
version: '0.21.0',
releaseNotes:
'For a complete list of changes, please visit <a href="https://bitcoincore.org/en/releases/0.21.0/">https://bitcoincore.org/en/releases/0.21.0/</a><br /><ul><li>Taproot!</li><li>New RPCs</li><li>Experimental Descriptor Wallets</li></ul>',
categories: ['bitcoin', 'featured'],
otherVersions: {
'27.0.0:1.0.0': {
releaseNotes: 'Even better support for Bitcoin and wallets!',
},
'#knots:27.1.0:0': {
releaseNotes: 'Even better support for Bitcoin and wallets!',
},
},
categories: ['bitcoin', 'cryptocurrency'],
versions: ['0.19.0', '0.20.0', '0.21.0'],
dependencyMetadata: {},
publishedAt: new Date().toISOString(),
},
latest: {
icon: BTC_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: {
...Mock.MockManifestBitcoind,
releaseNotes:
'For a complete list of changes, please visit <a href="https://bitcoincore.org/en/releases/0.21.0/" target="_blank">https://bitcoincore.org/en/releases/0.21.0/</a><br />Or in [markdown](https://bitcoincore.org/en/releases/0.21.0/)<ul><li>Taproot!</li><li>New RPCs</li><li>Experimental Descriptor Wallets</li></ul>',
},
categories: ['bitcoin', 'cryptocurrency'],
versions: ['0.19.0', '0.20.0', '0.21.0'],
dependencyMetadata: {},
publishedAt: new Date().toISOString(),
},
},
lnd: {
'0.11.0': {
icon: LND_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: {
...Mock.MockManifestLnd,
version: '0.11.0',
releaseNotes: 'release notes for LND 0.11.0',
'=0.17.5:0': {
best: {
'0.17.5:0': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
supportSite: 'https://lightning.engineering/slack.html',
marketingSite: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
dependencyMetadata: {
bitcoind: {
title: 'Bitcoin Core',
icon: BTC_ICON,
description: 'Used for RPC requests',
optional: false,
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
icon: PROXY_ICON,
description: 'Used for authorized proxying of RPC requests',
optional: true,
},
},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.5/lnd.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin', 'lightning', 'cryptocurrency'],
versions: ['0.11.0', '0.11.1'],
dependencyMetadata: {
bitcoind: BitcoinDep,
'btc-rpc-proxy': ProxyDep,
categories: ['lightning', 'featured'],
otherVersions: {
'0.18.0:0.0.1': {
releaseNotes: 'Upstream release and minor fixes.',
},
'0.17.4-beta:1.0-alpha': {
releaseNotes: 'Upstream release to 0.17.4',
},
},
publishedAt: new Date().toISOString(),
},
'0.11.1': {
icon: LND_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: {
...Mock.MockManifestLnd,
version: '0.11.1',
releaseNotes: 'release notes for LND 0.11.1',
'=0.17.4-beta:1.0-alpha': {
best: {
'0.17.4-beta:1.0-alpha': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
supportSite: 'https://lightning.engineering/slack.html',
marketingSite: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
dependencyMetadata: {
bitcoind: {
title: 'Bitcoin Core',
icon: BTC_ICON,
description: 'Used for RPC requests',
optional: false,
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
icon: PROXY_ICON,
description: 'Used for authorized proxying of RPC requests',
optional: true,
},
},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.4/lnd.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin', 'lightning', 'cryptocurrency'],
versions: ['0.11.0', '0.11.1'],
dependencyMetadata: {
bitcoind: BitcoinDep,
'btc-rpc-proxy': ProxyDep,
categories: ['lightning', 'featured'],
otherVersions: {
'0.18.0:0.0.1': {
releaseNotes: 'Upstream release and minor fixes.',
},
'0.17.5:0': {
releaseNotes: 'Upstream release to 0.17.5',
},
},
publishedAt: new Date().toISOString(),
},
latest: {
icon: LND_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: Mock.MockManifestLnd,
categories: ['bitcoin', 'lightning', 'cryptocurrency'],
versions: ['0.11.0', '0.11.1'],
dependencyMetadata: {
bitcoind: BitcoinDep,
'btc-rpc-proxy': ProxyDep,
},
publishedAt: new Date(new Date().valueOf() + 10).toISOString(),
},
},
'btc-rpc-proxy': {
latest: {
icon: PROXY_ICON,
license: 'licenseUrl',
instructions: 'instructionsUrl',
manifest: Mock.MockManifestBitcoinProxy,
categories: ['bitcoin'],
versions: ['0.2.2'],
dependencyMetadata: {
bitcoind: BitcoinDep,
'=0.3.2.6:0': {
best: {
'0.3.2.6:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues',
marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7.1/btc-rpc-proxy.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin'],
otherVersions: {
'0.3.2.7:0': {
releaseNotes: 'Upstream release and minor fixes.',
},
},
publishedAt: new Date().toISOString(),
},
},
}
export const MarketplacePkgsList: RR.GetMarketplacePackagesRes =
Object.values(Mock.MarketplacePkgs).map(service => service['latest'])
export const RegistryPackages: GetPackagesRes = {
bitcoind: {
best: {
'27.0.0:1.0.0': {
title: 'Bitcoin Core',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoind-startos',
upstreamRepo: 'https://github.com/bitcoin/bitcoin',
supportSite: 'https://bitcoin.org',
marketingSite: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v27.0.0/bitcoind.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
'#knots:27.1.0:0': {
title: 'Bitcoin Knots',
description: {
short: 'An alternate fully verifying implementation of Bitcoin',
long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.',
},
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos',
upstreamRepo: 'https://github.com/bitcoinknots/bitcoin',
supportSite: 'https://bitcoinknots.org',
marketingSite: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin', 'featured'],
otherVersions: {
'26.1.0:0.1.0': {
releaseNotes: 'Even better support for Bitcoin and wallets!',
},
'#knots:26.1.20240325:0': {
releaseNotes: 'Even better Knots support for Bitcoin and wallets!',
},
},
},
lnd: {
best: {
'0.18.0:0.0.1': {
title: 'LND',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/start9labs/lnd-startos',
upstreamRepo: 'https://github.com/lightningnetwork/lnd',
supportSite: 'https://lightning.engineering/slack.html',
marketingSite: 'https://lightning.engineering/',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
dependencyMetadata: {
bitcoind: {
title: 'Bitcoin Core',
icon: BTC_ICON,
description: 'Used for RPC requests',
optional: false,
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
icon: null,
description: 'Used for authorized RPC requests',
optional: true,
},
},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.18.0.1/lnd.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['lightning', 'featured'],
otherVersions: {
'0.17.5:0': {
releaseNotes: 'Upstream release to 0.17.5',
},
'0.17.4-beta:1.0-alpha': {
releaseNotes: 'Upstream release to 0.17.4',
},
},
},
'btc-rpc-proxy': {
best: {
'0.3.2.7:0': {
title: 'Bitcoin Proxy',
description: mockDescription,
hardwareRequirements: { arch: null, device: {}, ram: null },
license: 'mit',
wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers',
upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy',
supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues',
marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,
dependencyMetadata: {},
donationUrl: null,
alerts: {
install: 'test',
uninstall: 'test',
start: 'test',
stop: 'test',
restore: 'test',
},
s9pk: {
url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7/btc-rpc-proxy.s9pk',
commitment: mockMerkleArchiveCommitment,
signatures: {},
publishedAt: Date.now().toString(),
},
},
},
categories: ['bitcoin'],
otherVersions: {
'0.3.2.6:0': {
releaseNotes: 'Upstream release and minor fixes.',
},
},
},
}
export const Notifications: ServerNotifications = [
{
@@ -658,13 +1063,13 @@ export module Mock {
packageBackups: {
bitcoind: {
title: 'Bitcoin Core',
version: '0.21.0',
version: '0.21.0:0',
osVersion: '0.3.6',
timestamp: new Date().toISOString(),
},
'btc-rpc-proxy': {
title: 'Bitcoin Proxy',
version: '0.2.2',
version: '0.2.2:0',
osVersion: '0.3.6',
timestamp: new Date().toISOString(),
},
@@ -1489,8 +1894,7 @@ export module Mock {
title: Mock.MockManifestBitcoind.title,
icon: 'assets/img/service-icons/bitcoind.svg',
kind: 'running',
registryUrl: '',
versionSpec: '>=26.0.0',
versionRange: '>=26.0.0',
healthChecks: [],
configSatisfied: true,
},
@@ -1576,8 +1980,7 @@ export module Mock {
title: Mock.MockManifestBitcoind.title,
icon: 'assets/img/service-icons/bitcoind.svg',
kind: 'running',
registryUrl: 'https://registry.start9.com',
versionSpec: '>=26.0.0',
versionRange: '>=26.0.0',
healthChecks: [],
configSatisfied: true,
},
@@ -1585,8 +1988,7 @@ export module Mock {
title: Mock.MockManifestBitcoinProxy.title,
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
kind: 'exists',
registryUrl: 'https://community-registry.start9.com',
versionSpec: '>2.0.0',
versionRange: '>2.0.0',
configSatisfied: false,
},
},

View File

@@ -1,5 +1,4 @@
import { Dump } from 'patch-db-client'
import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace'
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
@@ -223,12 +222,7 @@ export module RR {
export type GetPackageMetricsReq = { id: string } // package.metrics
export type GetPackageMetricsRes = Metric
export type InstallPackageReq = {
id: string
versionSpec?: string
versionPriority?: 'min' | 'max'
registry: string
} // package.install
export type InstallPackageReq = T.InstallParams
export type InstallPackageRes = null
export type GetPackageConfigReq = { id: string } // package.config.get
@@ -287,26 +281,13 @@ export module RR {
progress: string // guid
}
// marketplace
// registry
export type GetMarketplaceInfoReq = { serverId: string }
export type GetMarketplaceInfoRes = StoreInfo
/** these are returned in ASCENDING order. the newest available version will be the LAST in the object */
export type GetRegistryOsUpdateRes = { [version: string]: T.OsVersionInfo }
export type CheckOSUpdateReq = { serverId: string }
export type CheckOSUpdateRes = OSUpdate
export type GetMarketplacePackagesReq = {
ids?: { id: string; version: string }[]
// iff !ids
category?: string
query?: string
page?: number
perPage?: number
}
export type GetMarketplacePackagesRes = MarketplacePkg[]
export type GetReleaseNotesReq = { id: string }
export type GetReleaseNotesRes = { [version: string]: string }
}
export interface OSUpdate {

View File

@@ -1,17 +1,32 @@
import { Observable } from 'rxjs'
import { RR } from './api.types'
import { RPCOptions } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
import {
GetPackageRes,
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
export abstract class ApiService {
// http
// for getting static files: ex icons, instructions, licenses
abstract getStatic(url: string): Promise<string>
// for sideloading packages
abstract uploadPackage(guid: string, body: Blob): Promise<string>
abstract uploadFile(body: Blob): Promise<string>
// for getting static files: ex icons, instructions, licenses
abstract getStaticProxy(
pkg: MarketplacePkg,
path: 'LICENSE.md' | 'instructions.md',
): Promise<string>
abstract getStaticInstalled(
id: T.PackageId,
path: 'LICENSE.md' | 'instructions.md',
): Promise<string>
// websocket
abstract openWebsocket$<T>(
@@ -120,14 +135,23 @@ export abstract class ApiService {
// marketplace URLs
abstract marketplaceProxy<T>(
path: string,
params: Record<string, unknown>,
url: string,
abstract registryRequest<T>(
registryUrl: string,
options: RPCOptions,
): Promise<T>
abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes>
abstract getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo>
abstract getRegistryPackage(
url: string,
id: string,
versionRange: string | null,
): Promise<GetPackageRes>
abstract getRegistryPackages(registryUrl: string): Promise<GetPackagesRes>
// notification
abstract getNotifications(

View File

@@ -18,6 +18,15 @@ import { AuthService } from '../auth.service'
import { DOCUMENT } from '@angular/common'
import { DataModel } from '../patch-db/data-model'
import { Dump, pathFromArray } from 'patch-db-client'
import { T } from '@start9labs/start-sdk'
import {
GetPackageReq,
GetPackageRes,
GetPackagesReq,
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
import { blake3 } from '@noble/hashes/blake3'
@Injectable()
export class LiveApiService extends ApiService {
@@ -29,17 +38,7 @@ export class LiveApiService extends ApiService {
@Inject(PATCH_CACHE) private readonly cache$: Observable<Dump<DataModel>>,
) {
super()
; (window as any).rpcClient = this
}
// for getting static files: ex icons, instructions, licenses
async getStatic(url: string): Promise<string> {
return this.httpRequest({
method: Method.GET,
url,
responseType: 'text',
})
;(window as any).rpcClient = this
}
// for sideloading packages
@@ -62,6 +61,36 @@ export class LiveApiService extends ApiService {
})
}
// for getting static files: ex. instructions, licenses
async getStaticProxy(
pkg: MarketplacePkg,
path: 'LICENSE.md' | 'instructions.md',
): Promise<string> {
const encodedUrl = encodeURIComponent(pkg.s9pk.url)
return this.httpRequest({
method: Method.GET,
url: `/s9pk/proxy/${encodedUrl}/${path}`,
params: {
rootSighash: pkg.s9pk.commitment.rootSighash,
rootMaxsize: pkg.s9pk.commitment.rootMaxsize,
},
responseType: 'text',
})
}
async getStaticInstalled(
id: T.PackageId,
path: 'LICENSE.md' | 'instructions.md',
): Promise<string> {
return this.httpRequest({
method: Method.GET,
url: `/s9pk/installed/${id}.s9pk/${path}`,
responseType: 'text',
})
}
// websocket
openWebsocket$<T>(
@@ -257,24 +286,63 @@ export class LiveApiService extends ApiService {
// marketplace URLs
async marketplaceProxy<T>(
path: string,
qp: Record<string, string>,
baseUrl: string,
async registryRequest<T>(
registryUrl: string,
options: RPCOptions,
): Promise<T> {
const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}`
return this.rpcRequest({
method: 'marketplace.get',
params: { url: fullUrl },
...options,
method: `registry.${options.method}`,
params: { registry: registryUrl, ...options.params },
})
}
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
return this.marketplaceProxy(
'/eos/v0/latest',
qp,
this.config.marketplace.start9,
)
const { serverId } = qp
return this.registryRequest(this.config.marketplace.start9, {
method: 'os.version.get',
params: { serverId },
})
}
async getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo> {
return this.registryRequest(registryUrl, {
method: 'info',
params: {},
})
}
async getRegistryPackage(
registryUrl: string,
id: string,
versionRange: string | null,
): Promise<GetPackageRes> {
const params: GetPackageReq = {
id,
version: versionRange,
sourceVersion: null,
otherVersions: 'short',
}
return this.registryRequest<GetPackageRes>(registryUrl, {
method: 'package.get',
params,
})
}
async getRegistryPackages(registryUrl: string): Promise<GetPackagesRes> {
const params: GetPackagesReq = {
id: null,
version: null,
sourceVersion: null,
otherVersions: 'short',
}
return this.registryRequest<GetPackagesRes>(registryUrl, {
method: 'package.get',
params,
})
}
// notification
@@ -504,6 +572,29 @@ export class LiveApiService extends ApiService {
private async httpRequest<T>(opts: HttpOptions): Promise<T> {
const res = await this.http.httpRequest<T>(opts)
if (res.headers.get('Repr-Digest')) {
// verify
const digest = res.headers.get('Repr-Digest')!
let data: Uint8Array
if (opts.responseType === 'arrayBuffer') {
data = Buffer.from(res.body as ArrayBuffer)
} else if (opts.responseType === 'text') {
data = Buffer.from(res.body as string)
} else if ((opts.responseType as string) === 'blob') {
data = Buffer.from(await (res.body as Blob).arrayBuffer())
} else {
console.warn(
`could not verify Repr-Digest for responseType ${
opts.responseType || 'json'
}`,
)
return res.body
}
const computedDigest = Buffer.from(blake3(data)).toString('base64')
if (`blake3=:${computedDigest}:` === digest) return res.body
console.debug(computedDigest, digest)
throw new Error('File digest mismatch.')
}
return res.body
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared'
import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared'
import { ApiService } from './embassy-api.service'
import {
Operation,
@@ -30,8 +30,12 @@ import {
} from 'rxjs'
import { mockPatchData } from './mock-patch'
import { AuthService } from '../auth.service'
import { StoreInfo } from '@start9labs/marketplace'
import { T } from '@start9labs/start-sdk'
import {
GetPackageRes,
GetPackagesRes,
MarketplacePkg,
} from '@start9labs/marketplace'
const PROGRESS: T.FullProgress = {
overall: {
@@ -48,10 +52,7 @@ const PROGRESS: T.FullProgress = {
},
{
name: 'Validating',
progress: {
done: 0,
total: 40,
},
progress: null,
},
{
name: 'Installing',
@@ -80,14 +81,25 @@ export class MockApiService extends ApiService {
.subscribe()
}
async getStatic(url: string): Promise<string> {
async uploadPackage(guid: string, body: Blob): Promise<string> {
await pauseFor(2000)
return 'success'
}
async getStaticProxy(
pkg: MarketplacePkg,
path: 'LICENSE.md' | 'instructions.md',
): Promise<string> {
await pauseFor(2000)
return markdown
}
async uploadPackage(guid: string, body: Blob): Promise<string> {
async getStaticInstalled(
id: T.PackageId,
path: 'LICENSE.md' | 'instructions.md',
): Promise<string> {
await pauseFor(2000)
return 'success'
return markdown
}
// websocket
@@ -136,7 +148,7 @@ export class MockApiService extends ApiService {
this.stateIndex++
return this.stateIndex === 1 ? 'initializing' : 'running'
return this.stateIndex === 1 ? 'running' : 'running'
}
// db
@@ -448,34 +460,13 @@ export class MockApiService extends ApiService {
// marketplace URLs
async marketplaceProxy(
path: string,
params: Record<string, string>,
url: string,
async registryRequest(
registryUrl: string,
options: RPCOptions,
): Promise<any> {
await pauseFor(2000)
if (path === '/package/v0/info') {
const info: StoreInfo = {
name: 'Start9 Registry',
categories: [
'bitcoin',
'lightning',
'data',
'featured',
'messaging',
'social',
'alt coin',
],
}
return info
} else if (path === '/package/v0/index') {
return Mock.MarketplacePkgsList
} else if (path.startsWith('/package/v0/release-notes')) {
return Mock.ReleaseNotes
} else if (path.includes('instructions') || path.includes('license')) {
return markdown
}
return Error('do not call directly')
}
async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise<RR.CheckOSUpdateRes> {
@@ -483,6 +474,29 @@ export class MockApiService extends ApiService {
return Mock.MarketplaceEos
}
async getRegistryInfo(registryUrl: string): Promise<T.RegistryInfo> {
await pauseFor(2000)
return Mock.RegistryInfo
}
async getRegistryPackage(
url: string,
id: string,
versionRange: string,
): Promise<GetPackageRes> {
await pauseFor(2000)
if (!versionRange) {
return Mock.RegistryPackages[id]
} else {
return Mock.OtherPackageVersions[id][versionRange]
}
}
async getRegistryPackages(registryUrl: string): Promise<GetPackagesRes> {
await pauseFor(2000)
return Mock.RegistryPackages
}
// notification
async getNotifications(
@@ -742,11 +756,11 @@ export class MockApiService extends ApiService {
...Mock.LocalPkgs[params.id],
stateInfo: {
// if installing
// state: PackageState.Installing,
state: 'installing',
// if updating
state: 'updating',
manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
// state: 'updating',
// manifest: mockPatchData.packageData[params.id].stateInfo.manifest!,
// both
installingInfo: {
@@ -1129,11 +1143,7 @@ export class MockApiService extends ApiService {
const progress = JSON.parse(JSON.stringify(PROGRESS))
for (let [i, phase] of progress.phases.entries()) {
if (
!phase.progress ||
typeof phase.progress !== 'object' ||
!phase.progress.total
) {
if (!phase.progress || phase.progress === true || !phase.progress.total) {
await pauseFor(2000)
const patches: Operation<any>[] = [

View File

@@ -88,7 +88,7 @@ export const mockPatchData: DataModel = {
state: 'installed',
manifest: {
...Mock.MockManifestBitcoind,
version: '0.20.0',
version: '0.20.0:0',
},
},
icon: '/assets/img/service-icons/bitcoind.svg',
@@ -295,7 +295,7 @@ export const mockPatchData: DataModel = {
state: 'installed',
manifest: {
...Mock.MockManifestLnd,
version: '0.11.0',
version: '0.11.0:0.0.1',
},
},
icon: '/assets/img/service-icons/lnd.png',
@@ -368,8 +368,7 @@ export const mockPatchData: DataModel = {
title: 'Bitcoin Core',
icon: 'assets/img/service-icons/bitcoind.svg',
kind: 'running',
registryUrl: 'https://registry.start9.com',
versionSpec: '>=26.0.0',
versionRange: '>=26.0.0',
healthChecks: [],
configSatisfied: true,
},
@@ -377,8 +376,7 @@ export const mockPatchData: DataModel = {
title: 'Bitcoin Proxy',
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
kind: 'running',
registryUrl: 'https://community-registry.start9.com',
versionSpec: '>2.0.0',
versionRange: '>2.0.0',
healthChecks: [],
configSatisfied: false,
},

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import {
@@ -39,7 +39,7 @@ export class DepErrorService {
)
constructor(
private readonly emver: Emver,
private readonly exver: Exver,
private readonly patch: PatchDB<DataModel>,
) {}
@@ -87,11 +87,17 @@ export class DepErrorService {
const depManifest = dep.stateInfo.manifest
// incorrect version
if (!this.emver.satisfies(depManifest.version, currentDep.versionSpec)) {
return {
type: 'incorrectVersion',
expected: currentDep.versionSpec,
received: depManifest.version,
if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) {
if (
depManifest.satisfies.some(
v => !this.exver.satisfies(v, currentDep.versionRange),
)
) {
return {
type: 'incorrectVersion',
expected: currentDep.versionRange,
received: depManifest.version,
}
}
}

View File

@@ -1,5 +1,4 @@
import { Injectable } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { BehaviorSubject, combineLatest } from 'rxjs'
import { distinctUntilChanged, map } from 'rxjs/operators'
import { OSUpdate } from 'src/app/services/api/api.types'
@@ -7,6 +6,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDB } from 'patch-db-client'
import { getServerInfo } from 'src/app/util/get-server-info'
import { DataModel } from './patch-db/data-model'
import { Exver } from '@start9labs/shared'
@Injectable({
providedIn: 'root',
@@ -47,15 +47,15 @@ export class EOSService {
constructor(
private readonly api: ApiService,
private readonly emver: Emver,
private readonly patch: PatchDB<DataModel>,
private readonly exver: Exver,
) {}
async loadEos(): Promise<void> {
const { version, id } = await getServerInfo(this.patch)
this.osUpdate = await this.api.checkOSUpdate({ serverId: id })
const updateAvailable =
this.emver.compare(this.osUpdate.version, version) === 1
this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater'
this.updateAvailable$.next(updateAvailable)
}
}

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@angular/core'
import { Inject, Injectable } from '@angular/core'
import {
MarketplacePkg,
AbstractMarketplaceService,
StoreData,
Marketplace,
StoreInfo,
StoreIdentity,
MarketplacePkg,
GetPackageRes,
} from '@start9labs/marketplace'
import {
BehaviorSubject,
@@ -37,8 +37,9 @@ import {
tap,
} from 'rxjs/operators'
import { ConfigService } from './config.service'
import { sameUrl } from '@start9labs/shared'
import { Exver, sameUrl } from '@start9labs/shared'
import { ClientStorageService } from './client-storage.service'
import { ExtendedVersion, T } from '@start9labs/start-sdk'
@Injectable()
export class MarketplaceService implements AbstractMarketplaceService {
@@ -93,11 +94,9 @@ export class MarketplaceService implements AbstractMarketplaceService {
mergeMap(({ url, name }) =>
this.fetchStore$(url).pipe(
tap(data => {
if (data?.info) this.updateStoreName(url, name, data.info.name)
}),
map<StoreData | null, [string, StoreData | null]>(data => {
return [url, data]
if (data?.info.name) this.updateStoreName(url, name, data.info.name)
}),
map<StoreData | null, [string, StoreData | null]>(data => [url, data]),
startWith<[string, StoreData | null]>([url, null]),
),
),
@@ -148,6 +147,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
private readonly patch: PatchDB<DataModel>,
private readonly config: ConfigService,
private readonly clientStorageService: ClientStorageService,
private readonly exver: Exver,
) {}
getKnownHosts$(filtered = false): Observable<StoreIdentity[]> {
@@ -170,28 +170,29 @@ export class MarketplaceService implements AbstractMarketplaceService {
getPackage$(
id: string,
version: string,
optionalUrl?: string,
version: string | null,
flavor: string | null,
registryUrl?: string,
): Observable<MarketplacePkg> {
return this.patch.watch$('ui', 'marketplace').pipe(
switchMap(uiMarketplace => {
const url = optionalUrl || uiMarketplace.selectedUrl
return this.selectedHost$.pipe(
switchMap(selected =>
this.marketplace$.pipe(
switchMap(m => {
const url = registryUrl || selected.url
if (version !== '*' || !uiMarketplace.knownHosts[url]) {
return this.fetchPackage$(id, version, url)
}
const pkg = m[url]?.packages.find(
p =>
p.id === id &&
p.flavor === flavor &&
(!version || this.exver.compareExver(p.version, version) === 0),
)
return this.marketplace$.pipe(
map(m => m[url]),
filter(Boolean),
take(1),
map(
store =>
store.packages.find(p => p.manifest.id === id) ||
({} as MarketplacePkg),
),
)
}),
return !!pkg
? of(pkg)
: this.fetchPackage$(url, id, version, flavor)
}),
),
),
)
}
@@ -210,56 +211,22 @@ export class MarketplaceService implements AbstractMarketplaceService {
): Promise<void> {
const params: RR.InstallPackageReq = {
id,
versionSpec: `=${version}`,
version,
registry: url,
}
await this.api.installPackage(params)
}
fetchInfo$(url: string): Observable<StoreInfo> {
return this.patch.watch$('serverInfo').pipe(
take(1),
switchMap(serverInfo => {
const qp: RR.GetMarketplaceInfoReq = { serverId: serverInfo.id }
return this.api.marketplaceProxy<RR.GetMarketplaceInfoRes>(
'/package/v0/info',
qp,
url,
)
}),
)
fetchInfo$(url: string): Observable<T.RegistryInfo> {
return from(this.api.getRegistryInfo(url))
}
fetchReleaseNotes$(
id: string,
url?: string,
): Observable<Record<string, string>> {
return this.selectedHost$.pipe(
switchMap(m => {
return from(
this.api.marketplaceProxy<Record<string, string>>(
`/package/v0/release-notes/${id}`,
{},
url || m.url,
),
)
}),
)
}
fetchStatic$(id: string, type: string, url?: string): Observable<string> {
return this.selectedHost$.pipe(
switchMap(m => {
return from(
this.api.marketplaceProxy<string>(
`/package/v0/${type}/${id}`,
{},
url || m.url,
),
)
}),
)
fetchStatic$(
pkg: MarketplacePkg,
type: 'LICENSE.md' | 'instructions.md',
): Observable<string> {
return from(this.api.getStaticProxy(pkg, type))
}
private fetchStore$(url: string): Observable<StoreData | null> {
@@ -273,33 +240,57 @@ export class MarketplaceService implements AbstractMarketplaceService {
)
}
private fetchPackages$(
url: string,
params: Omit<RR.GetMarketplacePackagesReq, 'page' | 'per-page'> = {},
): Observable<MarketplacePkg[]> {
const qp: RR.GetMarketplacePackagesReq = {
...params,
page: 1,
perPage: 100,
}
if (qp.ids) qp.ids = JSON.stringify(qp.ids)
return from(
this.api.marketplaceProxy<RR.GetMarketplacePackagesRes>(
'/package/v0/index',
qp,
url,
),
private fetchPackages$(url: string): Observable<MarketplacePkg[]> {
return from(this.api.getRegistryPackages(url)).pipe(
map(packages => {
return Object.entries(packages).flatMap(([id, pkgInfo]) =>
Object.keys(pkgInfo.best).map(version =>
this.convertToMarketplacePkg(
id,
version,
this.exver.getFlavor(version),
pkgInfo,
),
),
)
}),
)
}
private fetchPackage$(
convertToMarketplacePkg(
id: string,
version: string,
version: string | null,
flavor: string | null,
pkgInfo: GetPackageRes,
): MarketplacePkg {
version =
version ||
Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) ||
null
return !version || !pkgInfo.best[version]
? ({} as MarketplacePkg)
: {
id,
version,
flavor,
...pkgInfo,
...pkgInfo.best[version],
}
}
private fetchPackage$(
url: string,
id: string,
version: string | null,
flavor: string | null,
): Observable<MarketplacePkg> {
return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe(
map(pkgs => pkgs[0] || {}),
return from(
this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
).pipe(
map(pkgInfo =>
this.convertToMarketplacePkg(id, version, flavor, pkgInfo),
),
)
}

View File

@@ -50,6 +50,10 @@ export type PackageDataEntry<T extends StateInfo = StateInfo> =
stateInfo: T
}
export type AllPackageData = NonNullable<
T.AllPackageData & Record<string, PackageDataEntry<StateInfo>>
>
export type StateInfo = InstalledState | InstallingState | UpdatingState
export type InstalledState = {

View File

@@ -1,18 +1,19 @@
import { Emver } from '@start9labs/shared'
import { Exver } from '@start9labs/shared'
import { DataModel } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function dryUpdate(
{ id, version }: { id: string; version: string },
pkgs: DataModel['packageData'],
emver: Emver,
exver: Exver,
): string[] {
return Object.values(pkgs)
.filter(
pkg =>
Object.keys(pkg.currentDependencies || {}).some(
pkgId => pkgId === id,
) && !emver.satisfies(version, pkg.currentDependencies[id].versionSpec),
) &&
!exver.satisfies(version, pkg.currentDependencies[id].versionRange),
)
.map(pkg => getManifest(pkg).title)
}