@serve.zone/appstore 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/cloudly/app.json +2 -2
- package/apps/cloudly/versions/1.1.1/config.json +120 -0
- package/appstore.json +2 -2
- package/changelog.md +18 -0
- package/dist_ts_client/classes.appstoreresolver.d.ts +5 -0
- package/dist_ts_client/classes.appstoreresolver.js +160 -28
- package/dist_ts_client/data.generated.js +131 -5
- package/dist_ts_client/types.d.ts +1 -0
- package/package.json +2 -1
- package/readme.md +7 -4
- package/ts_client/classes.appstoreresolver.ts +186 -26
- package/ts_client/data.generated.ts +130 -4
- package/ts_client/readme.md +4 -1
- package/ts_client/types.ts +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as interfaces from '@serve.zone/interfaces';
|
|
2
|
+
import { SmartVersion } from '@push.rocks/smartversion';
|
|
2
3
|
import packageJson from '../package.json' with { type: 'json' };
|
|
3
4
|
import { appStorePackageData } from './data.generated.js';
|
|
4
5
|
import {
|
|
@@ -34,8 +35,10 @@ export class AppStoreResolver {
|
|
|
34
35
|
private readonly fetchRef: typeof fetch;
|
|
35
36
|
private readonly source: TAppStoreSourceMode;
|
|
36
37
|
private readonly resolveDockerDigests: boolean;
|
|
38
|
+
private readonly requestTimeoutMs: number;
|
|
37
39
|
private readonly now: () => Date;
|
|
38
40
|
private appStoreCache: TAppStoreIndex | null = null;
|
|
41
|
+
private inlineAppMetaCache = new Map<string, interfaces.appstore.IAppStoreAppMeta>();
|
|
39
42
|
private sourceAppCache = new Map<string, IResolvedAppStoreApp>();
|
|
40
43
|
|
|
41
44
|
constructor(optionsArg: IAppStoreResolverOptions = {}) {
|
|
@@ -43,6 +46,10 @@ export class AppStoreResolver {
|
|
|
43
46
|
this.baseUrl = optionsArg.baseUrl || appStoreDefaultRemoteBaseUrl;
|
|
44
47
|
this.fetchRef = optionsArg.fetch || fetch;
|
|
45
48
|
this.resolveDockerDigests = optionsArg.resolveDockerDigests ?? true;
|
|
49
|
+
this.requestTimeoutMs = optionsArg.requestTimeoutMs ?? 30_000;
|
|
50
|
+
if (!Number.isSafeInteger(this.requestTimeoutMs) || this.requestTimeoutMs < 1) {
|
|
51
|
+
throw new Error('App Store requestTimeoutMs must be a positive integer');
|
|
52
|
+
}
|
|
46
53
|
this.now = optionsArg.now || (() => new Date());
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -70,13 +77,20 @@ export class AppStoreResolver {
|
|
|
70
77
|
|
|
71
78
|
public async getAppMeta(appIdArg: string): Promise<interfaces.appstore.IAppStoreAppMeta> {
|
|
72
79
|
const app = await this.getAppStoreApp(appIdArg);
|
|
80
|
+
if (!app) {
|
|
81
|
+
throw new Error(`App Store app '${appIdArg}' is not present in the resolved index`);
|
|
82
|
+
}
|
|
73
83
|
if (app?.source?.type === 'repoManifest') {
|
|
74
84
|
return (await this.resolveRepoManifestSource(app.source, app)).appMeta;
|
|
75
85
|
}
|
|
76
86
|
if (app?.source?.type === 'dockerImage') {
|
|
77
87
|
return this.createAppMetaFromAppStoreApp(app);
|
|
78
88
|
}
|
|
79
|
-
|
|
89
|
+
const appMeta = this.inlineAppMetaCache.get(appIdArg);
|
|
90
|
+
if (!appMeta) {
|
|
91
|
+
throw new Error(`App Store metadata for '${appIdArg}' is unavailable`);
|
|
92
|
+
}
|
|
93
|
+
return structuredClone(appMeta);
|
|
80
94
|
}
|
|
81
95
|
|
|
82
96
|
public async getAppVersionConfig(
|
|
@@ -122,6 +136,7 @@ export class AppStoreResolver {
|
|
|
122
136
|
public async resolveAppStore(appStoreArg: unknown): Promise<TAppStoreIndex> {
|
|
123
137
|
const appStore = parseAppStoreIndex(appStoreArg);
|
|
124
138
|
this.sourceAppCache.clear();
|
|
139
|
+
this.inlineAppMetaCache.clear();
|
|
125
140
|
const apps: TAppStoreApp[] = [];
|
|
126
141
|
|
|
127
142
|
for (const app of appStore.apps) {
|
|
@@ -343,7 +358,10 @@ export class AppStoreResolver {
|
|
|
343
358
|
};
|
|
344
359
|
}
|
|
345
360
|
|
|
346
|
-
|
|
361
|
+
const appMeta = await this.fetchJson(
|
|
362
|
+
`apps/${appArg.id}/app.json`,
|
|
363
|
+
) as interfaces.appstore.IAppStoreAppMeta;
|
|
364
|
+
return this.validateInlineAppMetadata(appArg, appMeta);
|
|
347
365
|
}
|
|
348
366
|
|
|
349
367
|
private async resolveServezoneAppStoreManifest(
|
|
@@ -395,9 +413,17 @@ export class AppStoreResolver {
|
|
|
395
413
|
|
|
396
414
|
const selectedChannel = manifestArg.policy?.defaultChannel || manifestArg.channel || 'stable';
|
|
397
415
|
const channelVersion = manifestArg.channels?.[selectedChannel];
|
|
398
|
-
const declaredLatestVersion = manifestArg.latestVersion || channelVersion || versions[versions.length - 1];
|
|
399
|
-
const latestVersion = sourceVersionToResolvedVersion.get(declaredLatestVersion) || declaredLatestVersion;
|
|
400
416
|
const uniqueVersions = uniqueStrings(versions);
|
|
417
|
+
const declaredLatestVersion = manifestArg.latestVersion
|
|
418
|
+
|| channelVersion
|
|
419
|
+
|| selectImplicitLatestVersion(uniqueVersions, `Appstore manifest '${manifestArg.app.id}'`);
|
|
420
|
+
const latestVersion = sourceVersionToResolvedVersion.get(declaredLatestVersion)
|
|
421
|
+
|| declaredLatestVersion;
|
|
422
|
+
if (!configsByVersion.has(latestVersion)) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Appstore manifest '${manifestArg.app.id}' latestVersion '${declaredLatestVersion}' has no runtime config`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
401
427
|
|
|
402
428
|
const appStoreApp: TAppStoreApp = {
|
|
403
429
|
id: manifestArg.app.id,
|
|
@@ -460,6 +486,50 @@ export class AppStoreResolver {
|
|
|
460
486
|
return configArg;
|
|
461
487
|
}
|
|
462
488
|
|
|
489
|
+
private validateInlineAppMetadata(
|
|
490
|
+
appArg: TAppStoreApp,
|
|
491
|
+
appMetaArg: interfaces.appstore.IAppStoreAppMeta,
|
|
492
|
+
): TAppStoreApp {
|
|
493
|
+
if (appMetaArg.id !== appArg.id) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`App Store metadata ID '${appMetaArg.id}' does not match index app '${appArg.id}'`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
if (!appMetaArg.latestVersion) {
|
|
499
|
+
throw new Error(`App Store metadata for '${appArg.id}' has no latestVersion`);
|
|
500
|
+
}
|
|
501
|
+
const versions = uniqueStrings(
|
|
502
|
+
appMetaArg.versions?.length ? appMetaArg.versions : [appMetaArg.latestVersion],
|
|
503
|
+
);
|
|
504
|
+
if (!versions.includes(appMetaArg.latestVersion)) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`App Store metadata for '${appArg.id}' latestVersion '${appMetaArg.latestVersion}' is not declared in versions`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const semverLatest = selectHighestSemanticVersion(versions);
|
|
510
|
+
if (semverLatest && semverLatest !== appMetaArg.latestVersion) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`App Store metadata for '${appArg.id}' declares latestVersion '${appMetaArg.latestVersion}' but highest semantic version is '${semverLatest}'`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const resolvedLatestVersion = semverLatest || appMetaArg.latestVersion;
|
|
516
|
+
if (appArg.latestVersion !== resolvedLatestVersion) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`App Store index for '${appArg.id}' declares latestVersion '${appArg.latestVersion}' but app metadata resolves '${resolvedLatestVersion}'`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
this.inlineAppMetaCache.set(appArg.id, {
|
|
522
|
+
...appMetaArg,
|
|
523
|
+
latestVersion: resolvedLatestVersion,
|
|
524
|
+
versions,
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
...appArg,
|
|
528
|
+
latestVersion: resolvedLatestVersion,
|
|
529
|
+
versions,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
463
533
|
private createAppMetaFromAppStoreApp(appArg: TAppStoreApp): interfaces.appstore.IAppStoreAppMeta {
|
|
464
534
|
return {
|
|
465
535
|
id: appArg.id,
|
|
@@ -480,6 +550,56 @@ export class AppStoreResolver {
|
|
|
480
550
|
return appStore.apps.find((appArg) => appArg.id === appIdArg);
|
|
481
551
|
}
|
|
482
552
|
|
|
553
|
+
private async withRemoteResponse<T>(
|
|
554
|
+
urlArg: string,
|
|
555
|
+
initArg: RequestInit,
|
|
556
|
+
consumeArg: (responseArg: Response) => Promise<T> | T,
|
|
557
|
+
): Promise<T> {
|
|
558
|
+
const controller = new AbortController();
|
|
559
|
+
const upstreamSignal = initArg.signal;
|
|
560
|
+
const forwardAbort = (): void => {
|
|
561
|
+
controller.abort(upstreamSignal?.reason);
|
|
562
|
+
};
|
|
563
|
+
if (upstreamSignal?.aborted) {
|
|
564
|
+
forwardAbort();
|
|
565
|
+
} else {
|
|
566
|
+
upstreamSignal?.addEventListener('abort', forwardAbort, { once: true });
|
|
567
|
+
}
|
|
568
|
+
const timeout = setTimeout(() => {
|
|
569
|
+
controller.abort(new Error(`App Store request timed out after ${this.requestTimeoutMs}ms: ${urlArg}`));
|
|
570
|
+
}, this.requestTimeoutMs);
|
|
571
|
+
let response: Response | undefined;
|
|
572
|
+
try {
|
|
573
|
+
response = await this.fetchRef(urlArg, {
|
|
574
|
+
...initArg,
|
|
575
|
+
signal: controller.signal,
|
|
576
|
+
});
|
|
577
|
+
return await consumeArg(response);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (controller.signal.aborted && controller.signal.reason instanceof Error) {
|
|
580
|
+
throw controller.signal.reason;
|
|
581
|
+
}
|
|
582
|
+
throw error;
|
|
583
|
+
} finally {
|
|
584
|
+
clearTimeout(timeout);
|
|
585
|
+
upstreamSignal?.removeEventListener('abort', forwardAbort);
|
|
586
|
+
if (response?.body && !response.bodyUsed) {
|
|
587
|
+
void response.body.cancel().catch(() => {});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private async fetchRemoteHeaders(
|
|
593
|
+
urlArg: string,
|
|
594
|
+
initArg: RequestInit = {},
|
|
595
|
+
): Promise<{ status: number; ok: boolean; headers: Headers }> {
|
|
596
|
+
return await this.withRemoteResponse(urlArg, initArg, (responseArg) => ({
|
|
597
|
+
status: responseArg.status,
|
|
598
|
+
ok: responseArg.ok,
|
|
599
|
+
headers: new Headers(responseArg.headers),
|
|
600
|
+
}));
|
|
601
|
+
}
|
|
602
|
+
|
|
483
603
|
private async fetchJson(pathArg: string): Promise<unknown> {
|
|
484
604
|
if (this.source === 'package') {
|
|
485
605
|
const value = appStorePackageData.jsonFiles[pathArg];
|
|
@@ -490,11 +610,12 @@ export class AppStoreResolver {
|
|
|
490
610
|
}
|
|
491
611
|
|
|
492
612
|
const url = `${this.baseUrl}/${pathArg}`;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
613
|
+
return await this.withRemoteResponse(url, {}, async (responseArg) => {
|
|
614
|
+
if (!responseArg.ok) {
|
|
615
|
+
throw new Error(`HTTP ${responseArg.status} for ${url}`);
|
|
616
|
+
}
|
|
617
|
+
return await responseArg.json();
|
|
618
|
+
});
|
|
498
619
|
}
|
|
499
620
|
|
|
500
621
|
public async fetchText(pathArg: string): Promise<string> {
|
|
@@ -509,19 +630,21 @@ export class AppStoreResolver {
|
|
|
509
630
|
}
|
|
510
631
|
|
|
511
632
|
const url = `${this.baseUrl}/${pathArg}`;
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
633
|
+
return await this.withRemoteResponse(url, {}, async (responseArg) => {
|
|
634
|
+
if (!responseArg.ok) {
|
|
635
|
+
throw new Error(`HTTP ${responseArg.status} for ${url}`);
|
|
636
|
+
}
|
|
637
|
+
return await responseArg.text();
|
|
638
|
+
});
|
|
517
639
|
}
|
|
518
640
|
|
|
519
641
|
private async fetchTextFromUrl(urlArg: string): Promise<string> {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
642
|
+
return await this.withRemoteResponse(urlArg, {}, async (responseArg) => {
|
|
643
|
+
if (!responseArg.ok) {
|
|
644
|
+
throw new Error(`HTTP ${responseArg.status} for ${urlArg}`);
|
|
645
|
+
}
|
|
646
|
+
return await responseArg.text();
|
|
647
|
+
});
|
|
525
648
|
}
|
|
526
649
|
|
|
527
650
|
private async fetchDockerManifestDigest(imageArg: IParsedDockerImageReference): Promise<string | null> {
|
|
@@ -535,18 +658,18 @@ export class AppStoreResolver {
|
|
|
535
658
|
].join(', '),
|
|
536
659
|
});
|
|
537
660
|
|
|
538
|
-
let response = await this.
|
|
661
|
+
let response = await this.fetchRemoteHeaders(manifestUrl, { method: 'HEAD', headers });
|
|
539
662
|
if (response.status === 401) {
|
|
540
663
|
const authHeader = response.headers.get('www-authenticate');
|
|
541
664
|
const token = authHeader ? await this.fetchDockerRegistryToken(authHeader, imageArg.repository) : null;
|
|
542
665
|
if (token) {
|
|
543
666
|
headers.set('Authorization', `Bearer ${token}`);
|
|
544
|
-
response = await this.
|
|
667
|
+
response = await this.fetchRemoteHeaders(manifestUrl, { method: 'HEAD', headers });
|
|
545
668
|
}
|
|
546
669
|
}
|
|
547
670
|
|
|
548
671
|
if (!response.ok || !response.headers.get('docker-content-digest')) {
|
|
549
|
-
response = await this.
|
|
672
|
+
response = await this.fetchRemoteHeaders(manifestUrl, { method: 'GET', headers });
|
|
550
673
|
}
|
|
551
674
|
|
|
552
675
|
if (!response.ok) {
|
|
@@ -575,10 +698,14 @@ export class AppStoreResolver {
|
|
|
575
698
|
if (service) tokenUrl.searchParams.set('service', service);
|
|
576
699
|
tokenUrl.searchParams.set('scope', scope);
|
|
577
700
|
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
701
|
+
const tokenResponse = await this.withRemoteResponse<{
|
|
702
|
+
token?: string;
|
|
703
|
+
access_token?: string;
|
|
704
|
+
} | null>(tokenUrl.toString(), {}, async (responseArg) => {
|
|
705
|
+
if (!responseArg.ok) return null;
|
|
706
|
+
return await responseArg.json() as { token?: string; access_token?: string };
|
|
707
|
+
});
|
|
708
|
+
return tokenResponse?.token || tokenResponse?.access_token || null;
|
|
582
709
|
}
|
|
583
710
|
|
|
584
711
|
private validateVolume(volumeArg: interfaces.appstore.IAppStoreVolume, labelArg: string): void {
|
|
@@ -754,6 +881,39 @@ async function createSha256Hex(inputArg: string): Promise<string> {
|
|
|
754
881
|
.join('');
|
|
755
882
|
}
|
|
756
883
|
|
|
884
|
+
function selectHighestSemanticVersion(versionsArg: string[]): string | null {
|
|
885
|
+
if (versionsArg.length === 0) return null;
|
|
886
|
+
const parsedVersions: SmartVersion[] = [];
|
|
887
|
+
for (const versionArg of versionsArg) {
|
|
888
|
+
try {
|
|
889
|
+
parsedVersions.push(new SmartVersion(versionArg));
|
|
890
|
+
} catch {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
let highestIndex = 0;
|
|
895
|
+
for (let index = 1; index < parsedVersions.length; index++) {
|
|
896
|
+
if (parsedVersions[index].greaterThan(parsedVersions[highestIndex])) {
|
|
897
|
+
highestIndex = index;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return versionsArg[highestIndex];
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function selectImplicitLatestVersion(versionsArg: string[], labelArg: string): string {
|
|
904
|
+
if (versionsArg.length === 0) {
|
|
905
|
+
throw new Error(`${labelArg} defines no versions`);
|
|
906
|
+
}
|
|
907
|
+
if (versionsArg.length === 1) return versionsArg[0];
|
|
908
|
+
const semverLatest = selectHighestSemanticVersion(versionsArg);
|
|
909
|
+
if (!semverLatest) {
|
|
910
|
+
throw new Error(
|
|
911
|
+
`${labelArg} must declare latestVersion or a channel when multiple non-semver versions exist`,
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return semverLatest;
|
|
915
|
+
}
|
|
916
|
+
|
|
757
917
|
function uniqueStrings(valuesArg: string[]): string[] {
|
|
758
918
|
return Array.from(new Set(valuesArg.filter(Boolean)));
|
|
759
919
|
}
|
|
@@ -34,10 +34,11 @@ export const appStorePackageData: IAppStorePackageData = {
|
|
|
34
34
|
"description": "Multi-node serve.zone control plane for clusters, workload services, domains, and deployments.",
|
|
35
35
|
"category": "Dev Tools",
|
|
36
36
|
"iconName": "server",
|
|
37
|
-
"latestVersion": "1.1.
|
|
37
|
+
"latestVersion": "1.1.1",
|
|
38
38
|
"versions": [
|
|
39
39
|
"1.0.0",
|
|
40
|
-
"1.1.0"
|
|
40
|
+
"1.1.0",
|
|
41
|
+
"1.1.1"
|
|
41
42
|
],
|
|
42
43
|
"maintainer": "serve.zone",
|
|
43
44
|
"links": {
|
|
@@ -295,6 +296,131 @@ export const appStorePackageData: IAppStorePackageData = {
|
|
|
295
296
|
},
|
|
296
297
|
"minOneboxVersion": "2.2.0"
|
|
297
298
|
},
|
|
299
|
+
"apps/cloudly/versions/1.1.1/config.json": {
|
|
300
|
+
"image": "code.foss.global/serve.zone/cloudly@sha256:510b3edb65eed00d3edce4d482f90c864b26b759e3c8f7605109516791fb47f9",
|
|
301
|
+
"targetPorts": [
|
|
302
|
+
{
|
|
303
|
+
"name": "web",
|
|
304
|
+
"port": 3000,
|
|
305
|
+
"protocol": "http",
|
|
306
|
+
"default": true
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
"envVars": [
|
|
310
|
+
{
|
|
311
|
+
"key": "SERVEZONE_ENVIRONMENT",
|
|
312
|
+
"value": "production",
|
|
313
|
+
"description": "Cloudly runtime environment.",
|
|
314
|
+
"required": true
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"key": "SERVEZONE_PUBLIC_ORIGIN",
|
|
318
|
+
"value": "https://${SERVICE_DOMAIN}",
|
|
319
|
+
"description": "Canonical public Cloudly origin including protocol and optional public port.",
|
|
320
|
+
"required": true
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"key": "SERVEZONE_LISTENPORT",
|
|
324
|
+
"value": "3000",
|
|
325
|
+
"description": "Internal Cloudly HTTP port inside the container.",
|
|
326
|
+
"required": true
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"key": "SERVEZONE_SSLMODE",
|
|
330
|
+
"value": "external",
|
|
331
|
+
"description": "Use external TLS termination through Onebox or dcrouter.",
|
|
332
|
+
"required": true
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"key": "MONGODB_URL",
|
|
336
|
+
"value": "${MONGODB_URI}",
|
|
337
|
+
"description": "Authenticated MongoDB connection URL provisioned by Onebox.",
|
|
338
|
+
"required": true
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"key": "MONGODB_NAME",
|
|
342
|
+
"value": "${MONGODB_DATABASE}",
|
|
343
|
+
"description": "MongoDB database name provisioned by Onebox.",
|
|
344
|
+
"required": true
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"key": "MONGODB_USER",
|
|
348
|
+
"value": "${MONGODB_USERNAME}",
|
|
349
|
+
"description": "MongoDB username provisioned by Onebox.",
|
|
350
|
+
"required": true
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"key": "MONGODB_PASS",
|
|
354
|
+
"value": "${MONGODB_PASSWORD}",
|
|
355
|
+
"description": "MongoDB password provisioned by Onebox.",
|
|
356
|
+
"required": true
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"key": "S3_ENDPOINT",
|
|
360
|
+
"value": "onebox-minio",
|
|
361
|
+
"description": "S3 endpoint host for the MinIO service provisioned by Onebox.",
|
|
362
|
+
"required": true
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"key": "S3_PORT",
|
|
366
|
+
"value": "9000",
|
|
367
|
+
"description": "S3 endpoint port for the MinIO service provisioned by Onebox.",
|
|
368
|
+
"required": true
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"key": "S3_USESSL",
|
|
372
|
+
"value": "false",
|
|
373
|
+
"description": "Use plain HTTP for internal MinIO traffic on the Onebox network.",
|
|
374
|
+
"required": true
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"key": "S3_BUCKET",
|
|
378
|
+
"value": "${S3_BUCKET}",
|
|
379
|
+
"description": "S3 bucket provisioned by Onebox for Cloudly's registry.",
|
|
380
|
+
"required": true
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
"key": "S3_ACCESSKEY",
|
|
384
|
+
"value": "${S3_ACCESS_KEY}",
|
|
385
|
+
"description": "S3 access key provisioned by Onebox.",
|
|
386
|
+
"required": true
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"key": "S3_SECRETKEY",
|
|
390
|
+
"value": "${S3_SECRET_KEY}",
|
|
391
|
+
"description": "S3 secret key provisioned by Onebox.",
|
|
392
|
+
"required": true
|
|
393
|
+
}
|
|
394
|
+
],
|
|
395
|
+
"platformRequirements": {
|
|
396
|
+
"mongodb": true,
|
|
397
|
+
"s3": true
|
|
398
|
+
},
|
|
399
|
+
"platformOidc": {
|
|
400
|
+
"redirectPath": "/auth/oidc/callback",
|
|
401
|
+
"roles": [
|
|
402
|
+
{
|
|
403
|
+
"id": "admin",
|
|
404
|
+
"label": "Administrator",
|
|
405
|
+
"description": "Full Cloudly administration for this hosted instance."
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"id": "user",
|
|
409
|
+
"label": "User",
|
|
410
|
+
"description": "Standard authenticated Cloudly access for this hosted instance."
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
"environmentVariables": {
|
|
414
|
+
"issuerUrl": "SERVEZONE_PLATFORM_OIDC_ISSUER",
|
|
415
|
+
"clientId": "SERVEZONE_PLATFORM_OIDC_CLIENT_ID",
|
|
416
|
+
"clientSecret": "SERVEZONE_PLATFORM_OIDC_CLIENT_SECRET",
|
|
417
|
+
"redirectUri": "SERVEZONE_PLATFORM_OIDC_REDIRECT_URI",
|
|
418
|
+
"audience": "SERVEZONE_PLATFORM_OIDC_AUDIENCE"
|
|
419
|
+
},
|
|
420
|
+
"clientAuthenticationMethod": "client_secret_basic"
|
|
421
|
+
},
|
|
422
|
+
"minOneboxVersion": "2.2.0"
|
|
423
|
+
},
|
|
298
424
|
"apps/ghost/app.json": {
|
|
299
425
|
"id": "ghost",
|
|
300
426
|
"name": "Ghost",
|
|
@@ -1118,7 +1244,7 @@ export const appStorePackageData: IAppStorePackageData = {
|
|
|
1118
1244
|
},
|
|
1119
1245
|
"appstore.json": {
|
|
1120
1246
|
"schemaVersion": 1,
|
|
1121
|
-
"updatedAt": "2026-07-
|
|
1247
|
+
"updatedAt": "2026-07-25T00:00:00Z",
|
|
1122
1248
|
"apps": [
|
|
1123
1249
|
{
|
|
1124
1250
|
"id": "nginx",
|
|
@@ -1326,7 +1452,7 @@ export const appStorePackageData: IAppStorePackageData = {
|
|
|
1326
1452
|
"description": "Multi-node serve.zone control plane for clusters, workload services, domains, and deployments.",
|
|
1327
1453
|
"category": "Dev Tools",
|
|
1328
1454
|
"iconName": "server",
|
|
1329
|
-
"latestVersion": "1.
|
|
1455
|
+
"latestVersion": "1.1.1",
|
|
1330
1456
|
"tags": [
|
|
1331
1457
|
"serve.zone",
|
|
1332
1458
|
"control-plane",
|
package/ts_client/readme.md
CHANGED
|
@@ -32,10 +32,11 @@ const resolver = new AppStoreResolver({
|
|
|
32
32
|
source: 'remote',
|
|
33
33
|
baseUrl: 'https://example.com/appstore',
|
|
34
34
|
fetch,
|
|
35
|
+
requestTimeoutMs: 30_000,
|
|
35
36
|
});
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
Providing `baseUrl` or `fetch` without an explicit `source` also selects remote mode. The default remote base URL is exported as `appStoreDefaultRemoteBaseUrl`.
|
|
39
|
+
Providing `baseUrl` or `fetch` without an explicit `source` also selects remote mode. The default remote base URL is exported as `appStoreDefaultRemoteBaseUrl`. `requestTimeoutMs` bounds each remote catalog, manifest, registry, and response-body operation; it defaults to `30000` and must be a positive integer.
|
|
39
40
|
|
|
40
41
|
## Exports
|
|
41
42
|
|
|
@@ -44,6 +45,8 @@ Providing `baseUrl` or `fetch` without an explicit `source` also selects remote
|
|
|
44
45
|
- `appStoreDefaultRemoteBaseUrl` exposes the default remote base URL for explicit remote consumers.
|
|
45
46
|
- `IAppStoreResolverOptions`, `IAppStoreSourceInfo`, and the `TAppStore*` types describe resolver inputs and returned App Store data shapes.
|
|
46
47
|
|
|
48
|
+
The resolver rejects inconsistent catalogs before returning them: index and app metadata IDs/latest versions must agree, metadata `latestVersion` must occur in `versions`, and an all-semver list must name its highest semantic value as latest. A repo manifest with multiple non-semver versions must declare explicit latest or channel intent.
|
|
49
|
+
|
|
47
50
|
## License and Legal Information
|
|
48
51
|
|
|
49
52
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|